Cargo 工作区

在第 12 章中,我们构建了一个包含二进制 crate 和库 crate 的包。随着项目的开发,您可能会发现库 crate 不断变大,您希望将包进一步拆分为多个库 crate。Cargo 提供了一种称为 工作区 的功能,可以帮助管理并行开发的多个相关包。

创建一个工作区

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

$ mkdir add
$ cd add

接下来,在 add 目录中,我们创建 Cargo.toml 文件,该文件将配置整个工作区。此文件将没有 [package] 部分。 相反,它将以 [workspace] 部分开始,这将允许我们向工作区添加成员。我们还通过将 resolver 设置为 "2" 来确保在工作区中使用最新和最强大的 Cargo 解析器算法。

文件名: Cargo.toml

[workspace]
resolver = "2"

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

$ cargo new adder
    Creating binary (application) `adder` package
      Adding `adder` as member of workspace at `file:///projects/add`

在工作区中运行 cargo new 也会自动将新创建的包添加到工作区 Cargo.toml[workspace] 定义的 members 键中,如下所示:

[workspace]
resolver = "2"
members = ["adder"]

此时,我们可以通过运行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]
resolver = "2"
members = ["adder", "add_one"]

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

$ cargo new add_one --lib
    Creating library `add_one` package
      Adding `add_one` as member of workspace at `file:///projects/add`

您的 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 crate 中的 add_one 函数。打开 adder/src/main.rs 文件,并将 main 函数修改为调用 add_one 函数,如清单 14-7 所示。

Filename: adder/src/main.rs
fn main() {
    let num = 10;
    println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
Listing 14-7: Using the add_one library crate in the adder 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` profile [unoptimized + debuginfo] target(s) in 0.22s

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

在工作区中依赖外部包

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

文件名: 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 (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_onerand的依赖信息。然而,即使rand在工作区的某个地方被使用,除非我们也将其添加到工作区中其他crate的Cargo.toml文件中,否则我们无法在这些crate中使用它。例如,如果我们向adder/src/main.rs文件中的adder包添加use rand;,我们将得到一个错误:

$ 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 添加到 Cargo.lockadder 的依赖项列表中,但不会下载额外的 rand 副本。Cargo 会确保工作区中每个包中使用 rand 包的每个 crate 都使用相同的版本,只要它们指定了兼容的 rand 版本,这为我们节省了空间,并确保工作区中的 crate 之间相互兼容。

如果工作区中的 crate 指定了相同依赖的不同版本,Cargo 将解析每个版本,但仍会尽量减少解析的版本数量。

在工作区中添加测试

为了另一个增强,让我们在 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` profile [unoptimized + debuginfo] target(s) in 0.20s
     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` profile [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 之间的关系。