Cargo 工作区

在第 12 章中,我们构建了一个包含二进制 crate 和库 crate 的包。随着项目的开发,您可能会发现库 crate 继续变得更大,并且您想要将您的包进一步拆分为多个库 crate。 Cargo 提供了一个名为工作区的功能,可以帮助管理协同开发的多个相关包。

创建工作区

工作区是一组共享相同的 Cargo.lock 和输出目录的包。让我们使用工作区创建一个项目——我们将使用简单的代码,以便我们可以专注于工作区的结构。构建工作区有多种方法,因此我们只展示一种常见的方法。我们将有一个工作区,其中包含一个二进制文件和两个库。二进制文件将提供主要功能,并将依赖于这两个库。一个库将提供一个 add_one 函数,第二个库将提供一个 add_two 函数。这三个 crate 将是同一工作区的一部分。我们将从为工作区创建一个新目录开始

$ mkdir add
$ cd add

接下来,在 add 目录中,我们创建将配置整个工作区的 Cargo.toml 文件。此文件不会有 [package] 部分。相反,它将以 [workspace] 部分开头,该部分将允许我们通过指定二进制 crate 的包的路径来向工作区添加成员;在本例中,该路径是 adder

文件名:Cargo.toml

[workspace]

members = [
    "adder",
]

接下来,我们将通过在 add 目录中运行 cargo new 来创建 adder 二进制 crate

$ 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 目录。即使我们从 adder 目录内部运行 cargo build,编译后的工件仍然会最终出现在 add/target 而不是 add/adder/target 中。Cargo 以这种方式构建工作区中的 target 目录,因为工作区中的 crate 旨在相互依赖。如果每个 crate 都有自己的 target 目录,则每个 crate 都必须重新编译工作区中的其他每个 crate,以将其工件放置在其自己的 target 目录中。通过共享一个 target 目录,crate 可以避免不必要的重建。

在工作区中创建第二个包

接下来,让我们在工作区中创建另一个成员包,并将其称为 add_one。更改顶层 Cargo.toml 以在 members 列表中指定 add_one 路径

文件名:Cargo.toml

[workspace]

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

然后生成一个新的名为 add_one 的库 crate

$ 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 包。首先,我们需要在 adder/Cargo.toml 中添加对 add_one 的路径依赖项。

文件名:adder/Cargo.toml

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

Cargo 不会假定工作区中的 crate 会相互依赖,因此我们需要明确依赖关系。

接下来,让我们在 adder crate 中使用 add_one 函数(来自 add_one crate)。打开 adder/src/main.rs 文件,并在顶部添加 use 行,以将新的 add_one 库 crate 引入作用域。然后更改 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:从 adder crate 使用 add_one 库 crate

让我们通过在顶层 add 目录中运行 cargo build 来构建工作区!

$ 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 目录运行二进制 crate,我们可以通过使用 -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 crate。

依赖工作区中的外部包

请注意,工作区在顶层只有一个 Cargo.lock 文件,而不是每个 crate 的目录中都有一个 Cargo.lock。这确保了所有 crate 都使用相同版本的依赖项。如果我们将 rand 包添加到 adder/Cargo.tomladd_one/Cargo.toml 文件中,Cargo 会将它们都解析为 rand 的一个版本,并将其记录在一个 Cargo.lock 中。使工作区中的所有 crate 使用相同的依赖项意味着这些 crate 将始终相互兼容。让我们将 rand crate 添加到 add_one/Cargo.toml 文件中的 [dependencies] 部分,以便我们可以在 add_one crate 中使用 rand crate

文件名:add_one/Cargo.toml

[dependencies]
rand = "0.8.5"

我们现在可以将 use rand; 添加到 add_one/src/lib.rs 文件中,并通过在 add 目录中运行 cargo build 来构建整个工作区,这将引入并编译 rand crate。我们将收到一个警告,因为我们没有引用我们引入作用域的 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
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 10.18s

顶层 Cargo.lock 现在包含有关 add_onerand 的依赖关系的信息。但是,即使 rand 在工作区的某个地方被使用,我们也无法在工作区中的其他 crate 中使用它,除非我们将 rand 也添加到它们的 Cargo.toml 文件中。例如,如果我们将 use rand; 添加到 adder 包的 adder/src/main.rs 文件中,我们将收到一个错误

$ 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`

要解决此问题,请编辑 adder 包的 Cargo.toml 文件,并指示 rand 也是它的依赖项。构建 adder 包会将 rand 添加到 Cargo.lockadder 的依赖项列表中,但不会下载 rand 的其他副本。Cargo 将确保工作区中每个使用 rand 包的 crate 都将使用相同的版本,只要它们指定兼容版本的 rand,从而节省我们的空间并确保工作区中的 crate 将相互兼容。

如果工作区中的 crate 指定了同一依赖项的不兼容版本,Cargo 将解析它们中的每一个,但仍将尝试解析尽可能少的版本。

请注意,Cargo 仅在语义版本控制的规则内确保兼容性。例如,假设一个工作区有一个 crate 依赖于 rand 0.8.0,而另一个 crate 依赖于 rand 0.8.1。semver 规则表示 0.8.1 与 0.8.0 兼容,因此这两个 crate 都将依赖于 0.8.1(或可能更新的补丁,如 0.8.2)。但是,如果一个 crate 依赖于 rand 0.7.0,而另一个 crate 依赖于 rand 0.8.0,则这些版本在 semver 上不兼容。因此,Cargo 将为每个 crate 使用不同版本的 rand

向工作区添加测试

对于另一个增强功能,让我们在 add_one crate 中添加对 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));
    }
}

现在在顶层 add 目录中运行 cargo test。在像这样的结构化工作区中运行 cargo test 将运行工作区中所有 crate 的测试

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

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-49979ff40686fa8e)

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

输出的第一部分显示 add_one crate 中的 it_works 测试通过了。下一部分显示在 adder crate 中找到了零个测试,然后最后一部分显示在 add_one crate 中找到了零个文档测试。

我们还可以通过使用 -p 标志并指定我们要测试的 crate 的名称,从顶层目录运行工作区中特定 crate 的测试

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

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 crate 的测试,而没有运行 adder crate 的测试。

如果您将工作区中的 crate 发布到 crates.io,则工作区中的每个 crate 都需要单独发布。与 cargo test 类似,我们可以通过使用 -p 标志并指定我们要发布的 crate 的名称来发布工作区中的特定 crate。

为了进行额外的练习,以与 add_one crate 类似的方式向此工作区添加一个 add_two crate!

随着项目的增长,请考虑使用工作区:理解较小的、独立的组件比理解一大块代码更容易。此外,如果工作区中的 crate 经常同时更改,则将它们放在工作区中可以使 crate 之间的协调更加容易。