出于代码可读性的需要,我们倾向于把大而全的代码模块分割成小而精的代码模块。Rust 的包管理工具 cargo
就提供了实现这个特性的功能,即 _workspace_
。
什么是 workspace
一个 _workspace_
其实就是共享同一个 Cargo.lock 以及同一个 target 输出目录的多个 crates 的集合。比如说,在一个项目中,我们分了两个库,一个可执行程序,这个可执行程序依赖于那两个库。那么这里我们就有了三个 crates,然我们把这个三个 crates 以同级的方式放在同一个目录下。那么它们三个就组成了一个_ workspace_
。
创建 workspace
创建 _workspace_
的方式比较简单。比如我们这里就有两个库,一个依赖那两个库的可执行程序,假设这个可执行程序的名称就是 adder,那两个库分别是 add_one, add_two。
首先我们创建一个 add 文件夹,并进入到该目录下,然后创建一个 Cargo.toml 文件,如下:
mkdir add
cd add
touch Cargo.toml
然后在 _add/Cargo.toml_
文件中添加如下内容:
[workspace]
members = [
"adder",
"add_one",
"add_two",
]
可以看到,该 Cargo.toml 文件和普通单独 crates 模块中的 Cargo.toml 文件有所不同,它不需要 【package】以及其它的元数据,直接就是以 【workspace】这个段开始。
比如我们在该 workspace 顶层的 Cargo.toml 中添加一个 [dependencies]
段,则使用 cargo
编译的时候,就会遇到如下的错误:
PS D:\Projects\Rust\add> cargo run
error: failed to parse manifest at `D:\Projects\Rust\add\Cargo.toml`
Caused by:
this virtual manifest specifies a [dependencies] section, which is not allowed
错误提示你,这是一个虚清单文件,不被允许添加 [dependencies]
段。
然后我们分别在 add 文件夹下 使用 cargo new
命令创建 adder,add_one,add_two 三个 crates模块。如下:
cargo new --bin adder
cargo new --lib add_one
cargo new --lib add_two
做好上面的设置之后,add 文件的目录结构看起来应该如下所示:
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── add_two
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
设置 workspace 中各 crates 的依赖
假设 add_one 中提供了一个函数 add_one(i32)->32
函数,add_two 中提供了一个函数 add_two(i32)->i32
函数。因为 Cargo 并不会假设 _workspace_
中的 crates 相互之间有依赖,所以如果我们需要在 adder 中需要调用这两个函数,那么我们需要在 adder 的 Cargo.toml 中手动添加依赖,如下:
[dependencies]
add_one = { path = "../add_one" }
add_two = { path = "../add_two" }
然后我们在 add/adder/src/main.rs 中进行如下调用:
use add_one::add_one;
use add_two::add_two;
fn main() {
println!("call add_one for 12: {}", add_one(12));
println!("call add_two for 100: {}", add_two(100));
}
workspace 中依赖的外部包
注意,我们前面说了,同一个 _workspace_
在 workspace 顶层目录是共享同一个 Cargo.lock 的,所以,这就保证了一个 _workspace_
下的所有的 crates 对于所有的依赖都具有相同的版本,比如,add_one 需要依赖一个外部包 rand,add_two 也需要依赖一个外部包 rand,且它们依赖 rand 的版本不同,cargo 会解析对应的 toml 文件并保留其中一个版本后写到 _workspace_
的 Cargo.lock 文件中。
注意,对于外部依赖,每个用到的 crate 都需要在自己的 toml 文件中显式写明,并没有一个公共的地方写一次之后整个 _workspace_
内的 crates 都可以直接使用了。这样的好处就是各个 crates 都是真正独立的,单独拿出来都是可以正常编译使用的。
workspace 下的运行测试
即使是同一个 _workspace_
中,也有可能里面有多个可执行程序需要单独编译,单独运行,比如在有多个可执行程序的 _workspace_
中执行 cargo run
命令的时候,cargo
就会提示你,它无法确定你要运行的时候哪一个 bin
文件,需要你手动执行 --bin
选项指定或是在 toml 文件中以 default-run
的方式指定默认运行的程序。如下:
PS D:\Projects\Rust\add> cargo run
error: `cargo run` could not determine which binary to run. Use the `--bin` option to specify a binary, or the `default-run` manifest key.
available binaries: adder, multi
PS D:\Projects\Rust\add> cargo run --bin adder
warning: D:\Projects\Rust\add\Cargo.toml: unused manifest key: workspace.default-run
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target\debug\adder.exe`
call add_one for 12: 13
call add_two for 100: 102
其实你使用 -p 选项也是可以的,如下:
PS D:\Projects\Rust\add> cargo run -p adder
warning: D:\Projects\Rust\add\Cargo.toml: unused manifest key: workspace.default-run
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target\debug\adder.exe`
call add_one for 12: 13
call add_two for 100: 102
同样的,对于各 crates 的单元测试也是一样,简单的执行 cargo test
命令,会运行 _workspace_
下的所有单元测试,如下:
使用 cargo test -p add_one
则只会运行 add_one 下的单元测试。如下:
总结
可以看到,_workspace_
对于分割大代码,管理各个crates提供了很方便的工具,使得同一个项目的大量代码可以通过分割成各个小模块,分别独立管理,也易于理解。
subsrate 代码就是利用 _workspace_
管理的一个很大的项目。使得整个代码的目录结构清晰明了。