Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Зависимости

Практически любая реальная программа, помимо стандартной библиотеки, использует ряд сторонних библиотек. Когда для сборки программы необходима некая библиотека, то принято говорить, что программа зависит от этой библиотеки.

Именно для указания этих зависимостей служит секция [dependencies] в файле Cargo.toml.

Пример использования зависимости

Предположим, что мы решили написать программу — аналог “подбрасывания монетки”, которая при запуске случайным образом печатает “да” или “нет”.

Проблема в том, что в стандартной библиотеке Rust отсутствует функциональность для работы со случайными числами. Стандартным решением является — воспользоваться сторонней библиотекой rand. Давайте сначала рассмотрим реализацию программы, а потом разберёмся, что, как и откуда мы брали.

1) Создадим новый проект

cargo new decision_maker --bin

2) В Cargo.toml добавим зависимость на библиотеку rand версии 0.9 (актуальная на момент написания этого текста).

[package]
name = "decision_maker"
version = "0.1.0"
edition = "2024"

[dependencies]
rand = "0.9"

3) В главном файле программы src/main.rs напишем такое:

use rand::random;

fn main() {
    let is_yes: bool = random();
    if is_yes {
        println!("YES");
    } else {
        println!("NO");
    }
}

4) Соберём наше приложение:

$ cargo build
   Compiling libc v0.2.176
   Compiling cfg-if v1.0.3
   Compiling zerocopy v0.8.27
   Compiling getrandom v0.3.3
   Compiling rand_core v0.9.3
   Compiling ppv-lite86 v0.2.21
   Compiling rand_chacha v0.9.0
   Compiling rand v0.9.2
   Compiling decision_maker v0.1.0 (/home/stas/dev/proj/rust/decision_maker)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.60s
     Running `target/debug/decision_maker`

и запустим его:

$ ./target/debug/decision_maker
YES

Работает! Теперь давайте разбираться в том, что мы сделали.

Репозиторий библиотек

В Cargo.toml мы добавили зависимость rand = "0.9". Этим мы подключили библиотеку rand версии 0.9. Каким же образом эта библиотека попадает к нам в проект?

Дело в том, что для Rust библиотек существует централизованный репозиторий — crates.io.

Когда Cargo встречает запись о библиотеке в секции [dependencies] файла Cargo.toml, он выкачивает код этой библиотеки с crates.io. Если у библиотеки имеются транзитивные зависимости, то Cargo автоматически выкачивает и их.

В предыдущем примере, когда мы выполнили cargo build, в логе сборки можно было видеть как Cargo компилирует rand и все его транзитивные зависимости: libc, cfg-if, zerocopy, и т.д.

Поиск бибилотек

Остаётся вопрос: как мы узнали о библиотеке rand?

При работе с Rust, поиск библиотек ничем не отличается от поиска библиотек на другом языке программирования: искать в интернете. Если “вбить” в поисковый движок (или ИИ ассистент) запрос о том, как на Rust сгенерировать случайное число, то, скорее всего, первой рекомендацией будет пример с использованием библиотеки rand.

Также можно ознакомиться со сборником популярных библиотек и программ — awesome-rust.

Просмотр дерева зависимостей

Чтобы посмотреть все зависимости, включая транзитивные, которые Cargo выкачал для нашего проекта, можно воспользоваться командой cargo tree:

$ cargo tree
decision_maker v0.1.0 (/home/user/projects/rust/decision_maker)
└── rand v0.9.2
    ├── rand_chacha v0.9.0
    │   ├── ppv-lite86 v0.2.21
    │   │   └── zerocopy v0.8.27
    │   └── rand_core v0.9.3
    │       └── getrandom v0.3.3
    │           ├── cfg-if v1.0.3
    │           └── libc v0.2.176
    └── rand_core v0.9.3 (*)

Как видим, в нашем проекте есть только одна зависимость верхнего уровня — rand, при этом сам rand зависит от восьми других крэйтов.

Фичи библиотек

Очень часто в библиотеках часть функциональности “выключена” по умолчанию и для того чтобы её “включить”, нужно при объявлении зависимости указать определённый флаг — фичу (feature).

Например, мы хотим написать приложение, которое генерирует UUID идентификаторы 4-й версии UUID спецификации.

Сначала создадим проект:

cargo new uuid_v4_generator

На crates.io имеется библиотека uuid, которая поддерживает UUID версий с 1-й по 7-ю. Добавим её в зависимости:

[package]
name = "uuid_v4_generator"
version = "0.1.0"
edition = "2024"

[dependencies]
uuid = "1"

После ознакомления с документацией библиотеки uuid можно написать следующий корректный код программы (src/main.rs), которая генерирует UUID v4.

use uuid::Uuid;

fn main() {
    let uuid_v4 = Uuid::new_v4();
    println!("UUID: {uuid_v4}");
}

Однако при попытке собрать программу мы узнаем, что метод Uuid::new_v4 недоступен. Дело в том, что по умолчанию функциональность, связанная с UUID v4 “выключена”, и чтобы её включить, необходимо объявить зависимость uuid следующим образом:

[package]
name = "uuid_v4_generator"
version = "0.1.0"
edition = "2024"

[dependencies]
uuid = { version = "1", features = ["v4"] }

Теперь всё дожно работать. Соберём и запустим программу:

$ cargo run
   Compiling libc v0.2.180
   Compiling getrandom v0.3.4
   Compiling cfg-if v1.0.4
   Compiling uuid v1.19.0
   Compiling test_rust v0.1.0 (/home/stas/dev/proj/test_rust)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.26s
     Running `target/debug/test_rust`
UUID: 38cfd8a3-06c1-4c90-905c-59c2b010ad0a

Как узнать о фичах, которые предоставляет библиотека?

Как правило информация о доступных фичах, и их описание приводится в официальной документации, которая отображается на странице библиотеки на crates.io.

Однако наиболее достоверным источником информации о фичах является секция [features] в Cargo.toml крэйта.

Например, на crates.io на странице крэйта uuid есть ссылка на github репозиторий с исходным кодом библиотеки.

Перейдя на страницу github репозитория библиотеки и открыв там Cargo.toml, мы легко найдём секцию [features]:

[features]
default = ["std"]
std = ["wasm-bindgen?/std", "js-sys?/std"]
v1 = ["atomic"]
v3 = ["md5"]
v4 = ["rng"]
v5 = ["sha1"]
v6 = ["atomic"]
v7 = ["rng"]
v8 = []
js = ["dep:wasm-bindgen", "dep:js-sys"]

Здесь default — это список фич, включённых по умолчанию. Всё, что следует после: std, v1, v2, и т.д. — доступные фичи. Если фича не присутствует в списке default, значит, она по умолчанию “выключена”, и представленная в ней функциональность не может быть использована, пока фича не будет “включена” явно.

Версии зависимостей

В Rust для задания версий крэйтов принято использовать семантическое версионирование.

Принцип такой:

  • Версия состоит из трёх чисел, резделённых точкой: мажорная_версия.минорная_версия.патч_версия
  • Если мы вносим в код изменения, которые ломают совместимость с предыдущей версией, то мы должны инкрементировать номер мажорной версии
  • Если мы вносим изменения, которые добавляют новую функциональность, но не ломают API, тогда мы должны инкрементировать минорную версию
  • Если мы просто хотим исправить баг, при этом не меняя API и не добавляя новую функциональность, тогда мы инкрементируем патч версию

Почему это важно? Дело в том, что по умолчанию Cargo скачивает самую свежую совместимую по API версию.

Например, на момент написания этого текста, самая свежая версия библиотеки uuid была 1.18.1. И при том, что в Cargo.toml мы указали:

uuid = { version = "1", features = ["v4"] }

фактическая версия, которую взял Cargo — 1.18.1:

$ cargo tree
test_rust v0.1.0 (/home/user/projects/uuid_v4_generator)
└── uuid v1.18.1
    └── getrandom v0.3.3
        ├── cfg-if v1.0.3
        └── libc v0.2.176

А что если мы явно укажем версию 1.17.0?

uuid = { version = "1.17.0", features = ["v4"] }

Ничего не изменится. Cargo упорно продолжает брать последнюю совместимую минорную версию — 1.18.1.

Такое поведение логично, если исходить из правил семантического версионирования. Однако мы живём не в идеальном мире, и можем попасть в ситуацию, когда последняя минорная версия совместима с точки зрения API, однако содержит баг. В этой ситуации мы можем явно указать необходимую версию зависимости при помощи знака =:

uuid = { version = "=1.17.0", features = ["v4"] }

Теперь Cargo взял именно ту версию, которая указана:

$ cargo tree
test_rust v0.1.0 (/home/user/projects/uuid_v4_generator)
└── uuid v1.17.0
    └── getrandom v0.3.3
        ├── cfg-if v1.0.3
        └── libc v0.2.176

Важно заметить, что принцип “самая свежая совместимая по API версия” не всегда означает “последняя минорная версия”. Для библиотек, чья мажорная компонента версии равна нулю, минорные версии считают несовместимыми по API. Т.е. версии 0.3.0 и 0.4.0 считают несовместимыми. В таких ситуациях Cargo будет брать наиболее свежую патч версию, но именно ту минорную версию, которая указана.

Например, если мы укажем зависимость:

rand = "0.8"

тогда, не смотря на то, что на crates.io имеется версия 0.9.2, Cargo возьмёт версию 0.8.5 — самую свежую доступную патч версию для 0.8.

Cargo.lock

Когда Cargo впервые выкачивает зависимости, рядом с файлом Cargo.toml он создаёт файл Cargo.lock.

В Cargo.lock Cargo фиксирует те номера версий, которые были выкачаны с crates.io при первой сборке проекта. Далее Cargo продолжает работать с этими версиями, даже если на crates.io появились более свежие совместимые версии.

Например, мы подключили библиотеку rand вот так:

rand = "0.8"

На момент первого запуска команды cargo build, самая свежая совместимая версия rand была 0.8.2. Это значит, что Cargo зафиксировал в файле Cargo.lock версию rand=0.8.2.
Позже, на crates.io для rand вышла более свежая совместимая версия — 0.8.3, но Cargo всё равно продолжит использовать версию 0.8.2, потому что она зафиксирована в Cargo.lock файле.

Однако если мы удалим текущий Cargo.lock файл, то Cargo создаст новый, и зафиксирует в нём уже более новые, доступные на текущий момент, версии.


Для чего всё это нужно? Как мы сказали, Cargo пытается брать самые свежие совместимые версии библиотек. Однако, учитывая факт, что новые версии библиотек часто привносят неожиданные баги, было бы хорошо иметь возможность зафиксироваться на некой рабочей конфигурации. Именно для этого и существует Cargo.lock.

Хотите обновиться? Просто удалите Cargo.lock. Хотите продолжать оставаться на уже знакомой и работающей комбинации версий? Просто не трогайте Cargo.lock.


Также, следует отметить, что при разработке библиотек, рекомендуется коммитить Cargo.lock в репозиторий кода. Соответственно, при разработке конечных исполняемых приложений, Cargo.lock, как правило, в репозиторий не коммитят.