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

Рабочие пространства Cargo

В Главе 12 мы создали пакет, включающий бинарный крейт и библиотечный крейт. По мере развития проекта вы можете обнаружить, что библиотечный крейт продолжает расти, и вы хотите разделить пакет на несколько библиотечных крейтов. Cargo предлагает возможность под названием рабочие пространства (workspaces), которая помогает управлять несколькими связанными пакетами, разрабатываемыми одновременно.

Создание рабочего пространства

Рабочее пространство — это набор пакетов, которые используют один и тот же файл Cargo.lock и одну выходную директорию. Давайте создадим проект с использованием рабочего пространства — мы будем использовать тривиальный код, чтобы сосредоточиться на структуре рабочего пространства. Существует несколько способов структурировать рабочее пространство, поэтому мы покажем только один распространённый вариант. У нас будет рабочее пространство, содержащее один бинарный крейт и две библиотеки. Бинарный крейт, который будет предоставлять основную функциональность, будет зависеть от двух библиотек. Одна библиотека будет предоставлять функцию add_one, а другая — функцию add_two. Эти три крейта будут частью одного рабочего пространства. Начнём с создания новой директории для рабочего пространства:

$ mkdir add
$ cd add

Далее, в директории add, мы создадим файл Cargo.toml, который будет конфигурировать всё рабочее пространство. В этом файле не будет секции [package]. Вместо этого он начнётся с секции [workspace], которая позволит нам добавлять участников (members) в рабочее пространство. Мы также специально укажем использовать последнюю и лучшую версию алгоритма резолвера Cargo в нашем рабочем пространстве, установив resolver в "3".

Имя файла: Cargo.toml

[workspace]
resolver = "3"

Далее мы создадим бинарный крейт adder, выполнив cargo new внутри директории add:

$ cargo new adder
    Creating binary (application) `adder` package
      Adding `adder` as member of workspace at `file:///projects/add`

Запуск cargo new внутри рабочего пространства также автоматически добавляет вновь созданный пакет в ключ members в определении [workspace] в файле Cargo.toml рабочего пространства, вот так:

[workspace]
resolver = "3"
members = ["adder"]

На этом этапе мы можем собрать рабочее пространство, выполнив cargo build в корневой директории add. Файлы в вашей директории add должны выглядеть так:

├── Cargo.lock
├── Cargo.toml
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

Рабочее пространство имеет одну директорию target на верхнем уровне, в которую будут помещаться скомпилированные артефакты; у пакета adder нет своей собственной директории target. Даже если мы запустим cargo build изнутри директории adder, скомпилированные артефакты всё равно окажутся в add/target, а не в add/adder/target. Cargo структурирует директорию target в рабочем пространстве именно так, потому что крейты в рабочем пространстве должны зависеть друг от друга. Если бы у каждого крейта была своя директория target, каждому крейту пришлось бы перекомпилировать все остальные крейты в рабочем пространстве, чтобы разместить артефакты в своей собственной директории target. Используя одну общую директорию target, крейты могут избежать ненужных пересборок.

Создание второго пакета в рабочем пространстве

Далее создадим ещё один пакет-участник в рабочем пространстве и назовём его add_one. Сгенерируем новый библиотечный крейт с именем add_one:

$ cargo new add_one --lib
    Creating library `add_one` package
      Adding `add_one` as member of workspace at `file:///projects/add`

Теперь верхний Cargo.toml будет включать путь add_one в список members:

Имя файла: Cargo.toml

[workspace]
resolver = "3"
members = ["adder", "add_one"]

Ваша директория add теперь должна содержать эти директории и файлы:

├── Cargo.lock
├── Cargo.toml
├── add_one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

В файле add_one/src/lib.rs добавим функцию add_one:

Имя файла: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

Теперь мы можем сделать так, чтобы пакет adder с нашим бинарным крейтом зависел от пакета add_one с нашей библиотекой. Сначала нам нужно добавить зависимость по пути (path dependency) на add_one в файл adder/Cargo.toml.

Имя файла: adder/Cargo.toml

[dependencies]
add_one = { path = "../add_one" }

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

Далее давайте используем функцию add_one (из крейта add_one) в крейте adder. Откройте файл adder/src/main.rs и измените функцию main так, чтобы она вызывала функцию add_one, как в Листинге 14-7.

Filename: adder/src/main.rs
fn main() {
    let num = 10;
    println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
Listing 14-7: Использование библиотечного крейта add_one в крейте adder

Давайте соберём рабочее пространство, выполнив cargo build в верхней директории add!

$ cargo build
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s

Чтобы запустить бинарный крейт из директории add, мы можем указать, какой пакет в рабочем пространстве мы хотим запустить, используя аргумент -p и имя пакета с cargo run:

$ cargo run -p adder
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/adder`
Hello, world! 10 plus one is 11!

Это запускает код в adder/src/main.rs, который зависит от крейта add_one.

Зависимость от внешнего пакета в рабочем пространстве

Обратите внимание, что рабочее пространство имеет только один файл Cargo.lock на верхнем уровне, а не по Cargo.lock в каждой директории крейта. Это гарантирует, что все крейты используют одну и ту же версию всех зависимостей. Если мы добавим пакет rand в файлы adder/Cargo.toml и add_one/Cargo.toml, Cargo разрешит обе зависимости к одной версии rand и запишет это в один Cargo.lock. Обеспечение использования всеми крейтами в рабочем пространстве одних и тех же зависимостей означает, что крейты всегда будут совместимы друг с другом. Давайте добавим крейт rand в секцию [dependencies] в файле add_one/Cargo.toml, чтобы мы могли использовать крейт rand в крейте add_one:

Имя файла: add_one/Cargo.toml

[dependencies]
rand = "0.8.5"

Теперь мы можем добавить use rand; в файл add_one/src/lib.rs, и сборка всего рабочего пространства выполненная cargo build в директории add подтянет и скомпилирует крейт rand. Мы получим одно предупреждение, потому что не используем импортированный rand:

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.5
   --snip--
   Compiling rand v0.8.5
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
 --> add_one/src/lib.rs:1:5
  |
1 | use rand;
  |     ^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s

Верхний Cargo.lock теперь содержит информацию о зависимости add_one от rand. Однако, хотя rand используется где-то в рабочем пространстве, мы не можем использовать его в других крейтах в рабочем пространстве, если не добавим rand и в их файлы Cargo.toml. Например, если мы добавим use rand; в файл adder/src/main.rs для пакета adder, мы получим ошибку:

$ cargo build
  --snip--
   Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
 --> adder/src/main.rs:2:5
  |
2 | use rand;
  |     ^^^^ no external crate `rand`

Чтобы исправить это, отредактируйте файл Cargo.toml для пакета adder и укажите, что rand также является для него зависимостью. Сборка пакета adder добавит rand в список зависимостей для adder в Cargo.lock, но дополнительные копии rand загружаться не будут. Cargo гарантирует, что каждый крейт в каждом пакете рабочего пространства, использующий пакет rand, будет использовать одну и ту же версию, при условии что они указывают совместимые версии rand, экономя место и обеспечивая совместимость крейтов в рабочем пространстве.

Если крейты в рабочем пространстве указывают несовместимые версии одной и той же зависимости, Cargo разрешит каждую из них, но всё равно попытается разрешить как можно меньше версий.

Обратите внимание, что Cargo обеспечивает совместимость только в рамках правил [Семантического версионирования]. Например, предположим, что в рабочем пространстве один крейт зависит от rand 0.8.0, а другой — от rand 0.8.1. Правила semver говорят, что 0.8.1 совместим с 0.8.0, поэтому оба крейта будут зависеть от 0.8.1 (или потенциально более позднего патча, например, 0.8.2). Но если один крейт зависит от rand 0.7.0, а другой — от rand 0.8.0, эти версии семантически несовместимы. Следовательно, Cargo будет использовать разные версии rand для каждого крейта.

Добавление теста в рабочее пространство

В качестве ещё одного улучшения давайте добавим тест функции add_one::add_one внутри крейта add_one:

Имя файла: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(3, add_one(2));
    }
}

Теперь запустите cargo test в верхней директории add. Запуск cargo test в рабочем пространстве, структурированном подобно этому, выполнит тесты для всех крейтов в рабочем пространстве:

$ cargo test
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
     Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/adder-3a47283c568d2b6a)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Первая часть вывода показывает, что тест it_works в крейте add_one прошёл. Следующая часть показывает, что в крейте adder не было найдено тестов, а последняя часть показывает, что не было найдено документационных тестов в крейте add_one.

Мы также можем запускать тесты для одного конкретного крейта в рабочем пространстве из верхней директории, используя флаг -p и указывая имя крейта, который мы хотим протестировать:

$ cargo test -p add_one
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Этот вывод показывает, что cargo test запустил тесты только для крейта add_one и не запускал тесты для крейта adder.

Если вы публикуете крейты в рабочем пространстве на crates.io, каждый крейт в рабочем пространстве нужно будет публиковать отдельно. Как и cargo test, мы можем опубликовать конкретный крейт в нашем рабочем пространстве, используя флаг -p и указав имя крейта, который мы хотим опубликовать.

Для дополнительной практики добавьте крейт add_two в это рабочее пространство подобно крейту add_one!

По мере роста вашего проекта рассмотрите возможность использования рабочего пространства: оно позволяет работать с более мелкими, легкими для понимания компонентами, вместо одного большого куска кода. Более того, хранение крейтов в рабочем пространстве может облегчить координацию между крейтами, если они часто изменяются одновременно.