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 将解析每个版本,但仍会尝试解析尽可能少的版本。

向工作空间添加测试

为了进一步增强功能,让我们在 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 保留在工作空间中可以使 crate 之间的协调更容易。