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], через которую в рабочее пространство добавляются участники. Заодно укажем самую свежую версию алгоритма разрешения зависимостей 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 на верхнем уровне, куда попадают результаты компиляции; своей папки target у пакета adder нет. Даже если запустить cargo build изнутри папки adder, результаты сборки всё равно окажутся в add/target, а не в add/adder/target. Cargo устраивает папку 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`

Теперь в списке members в Cargo.toml верхнего уровня есть путь add_one:

Файл: 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 с библиотекой. Сначала добавим в adder/Cargo.toml зависимость от add_one по пути:

Файл: adder/Cargo.toml

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

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

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

Файл: adder/src/main.rs
fn main() {
    let num = 10;
    println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
Листинг 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 — на верхнем уровне, а не по файлу в папке каждого крейта. Так все крейты гарантированно используют одни и те же версии всех зависимостей. Если добавить пакет rand и в adder/Cargo.toml, и в add_one/Cargo.toml, Cargo сведёт оба к одной версии rand и запишет её в единственный Cargo.lock. Когда все крейты рабочего пространства используют одни и те же зависимости, они всегда совместимы друг с другом. Добавим крейт rand в секцию [dependencies] файла add_one/Cargo.toml, чтобы пользоваться им в крейте add_one:

Файл: add_one/Cargo.toml

[dependencies]
rand = "0.10.1"

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

$ cargo build
    Updating crates.io index
  Downloaded rand v0.10.1
   --snip--
   Compiling rand v0.10.1
   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)]` (part of `#[warn(unused)]`) 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 нельзя, пока вы не добавите 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 в Cargo.lock в список зависимостей adder добавится rand, но ещё одна копия rand скачиваться не будет. Cargo проследит, чтобы все крейты всех пакетов рабочего пространства, использующие 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!

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