Изучение рабочих пространств Cargo в Rust

Beginner

This tutorial is from open-source community. Access the source code

Введение

Добро пожаловать в Cargo Workspaces. Эта лаборатория является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

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

Cargo Workspaces

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

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

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

mkdir add
cd add

Далее, в директории add создаем файл Cargo.toml, который будет настраивать целое рабочее пространство. Этот файл не будет иметь секции [package]. Вместо этого он будет начинаться с секции [workspace], которая позволит нам добавить члены в рабочее пространство, указав путь к пакету с нашим бинарным ящиком; в этом случае путь — это adder:

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

[workspace]

members = [
    "adder",
]

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

$ cargo new adder
     Created binary (application) `adder` package

На этом этапе мы можем собрать рабочее пространство, выполнив 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. Измените верхнеуровневый Cargo.toml, чтобы указать путь к add_one в списке members:

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

[workspace]

members = [
    "adder",
    "add_one",
]

Затем сгенерируйте новый библиотечный ящик под названием add_one:

$ cargo new add_one --lib
Created library $(add_one) package

Теперь в директории 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 и добавьте строку use в начале, чтобы включить новый библиотечный ящик add_one в область видимости. Затем измените функцию main, чтобы вызвать функцию add_one, как показано в Листинге 14-7.

Имя файла: adder/src/main.rs

use add_one;

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 [unoptimized + debuginfo] target(s) in 0.68s

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

$ cargo run -p adder
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     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)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 10.18s

Теперь верхнеуровневый 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, будет использовать одну и ту же версию, экономя таким образом место и обеспечивая совместимость ящиков в рабочем пространстве.

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

Для следующего улучшения давайте добавим тест функции 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 в рабочем пространстве, структурированном так, как это одно, запустит тесты для всех ящиков в рабочем пространстве:

[object Object]

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

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

[object Object]

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

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

Для дополнительной практики добавьте ящик add_two в это рабочее пространство аналогичным образом, как и ящик add_one!

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

Резюме

Поздравляем! Вы завершили лабораторную работу по Cargo Workspaces. Вы можете выполнить больше лабораторных работ в LabEx, чтобы улучшить свои навыки.