Рабочие пространства Cargo
В главе 12 мы создали пакет, который включал бинарный крейт и библиотечный крейт. По мере развития вашего проекта вы можете обнаружить, что библиотечный крейт продолжает увеличиваться, и вы хотите разделить ваш пакет дальше на несколько библиотечных крейтов. Cargo предлагает функцию под названием рабочие пространства (workspaces), которая может помочь управлять несколькими связанными пакетами, разрабатываемыми совместно.
Создание рабочего пространства
Рабочее пространство — это набор пакетов, которые используют один и тот же файл Cargo.lock и выходной каталог. Давайте создадим проект, используя рабочее пространство — мы будем использовать тривиальный код, чтобы сосредоточиться на структуре рабочего пространства. Существует несколько способов структурировать рабочее пространство, поэтому мы просто покажем один распространенный способ. У нас будет рабочее пространство, содержащее бинарный крейт и две библиотеки. Бинарный крейт, который будет предоставлять основную функциональность, будет зависеть от двух библиотек. Одна библиотека будет предоставлять функцию add_one, а другая библиотека — функцию add_two. Эти три крейта будут частью одного рабочего пространства. Мы начнем с создания нового каталога для рабочего пространства:
$ mkdir add
$ cd add
Затем в каталоге add мы создаем файл Cargo.toml, который будет настраивать все рабочее пространство. Этот файл не будет иметь раздела [package]. Вместо этого он начнется с раздела [workspace], который позволит нам добавлять участников в рабочее пространство. Мы также обязательно используем новейшую и лучшую версию алгоритма резолвера Cargo в нашем рабочем пространстве, установив значение resolver в "3":
Файл: Cargo.toml
[workspace]
resolver = "3"
Далее мы создадим бинарный крейт adder, выполнив cargo new внутри каталога add:
$ cargo new adder
Created 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 должны выглядеть так:
├── 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
Created 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, в котором находится наша библиотека. Сначала нам нужно добавить зависимость по пути на 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.
<Листинг number="14-7" file-name="adder/src/main.rs" caption="Использование библиотечного крейта add_one из крейта adder">
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
</Листинг>
Давайте соберем рабочее пространство, выполнив 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. Однако, даже though 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 разрешит каждую из них, но все равно попытается разрешить как можно меньше версий.
Добавление теста в рабочее пространство
Для другого улучшения давайте добавим тест функции 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!
По мере роста вашего проекта рассмотрите возможность использования рабочего пространства: оно позволяет вам работать с меньшими, более понятными компонентами, чем один большой блок кода. Кроме того, хранение крейтов в рабочем пространстве может облегчить координацию между крейтами, если они часто изменяются одновременно.