Рабочие пространства 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.
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
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!
По мере роста вашего проекта рассмотрите возможность использования рабочего пространства: оно позволяет работать с более мелкими, легкими для понимания компонентами, вместо одного большого куска кода. Более того, хранение крейтов в рабочем пространстве может облегчить координацию между крейтами, если они часто изменяются одновременно.