Cargo 工作空间(Workspace)
本教程共 78 篇 · 第 57 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:理解 Cargo 工作空间如何把多个 package 组织在一起共享依赖与构建产物,区分根 package 和虚拟清单两种形式,并学会用
[workspace]配置成员、用-p指定构建对象。
当项目长大成一个由多个包组成的集合时,如果每个包各建各的 Cargo.lock、各编各的 target,依赖会被重复下载、重复编译,又慢又占空间。工作空间(workspace)就是来解决这个问题的。
1-1 工作空间是什么
工作空间是多个 package 的集合,它们共享同一份 Cargo.lock、同一个输出目录(默认 target/),以及同一套 profile 等设置。组成工作空间的包叫”成员”(member)。
几个关键特性要记牢:
- 所有成员共享根目录下的同一份
Cargo.lock。 - 所有成员共享根目录下的同一个
target输出目录,编译产物不重复。 - 只有根
Cargo.toml里的[patch]、[replace]、[profile.*]生效,成员包里的这些配置会被自动忽略。
Note共享依赖和编译产物是工作空间最大的价值。大型项目里十几个包如果各自独立,光编译 LLVM 后的依赖缓存就能撑爆磁盘;共享之后只编一次。
1-2 两种类型
工作空间分两种形态,区别在于根 Cargo.toml 里有没有 [package]。
根 package 类型:Cargo.toml 同时有 [package] 和 [workspace]。最外层的那个包就是根。例如 ripgrep 在最外层包里写:
[workspace]
members = [
"crates/globset",
"crates/grep",
"crates/cli",
"crates/matcher",
]
这里最外层目录就是工作空间的根,它本身也是一个可运行的包。
虚拟清单(virtual manifest)类型:Cargo.toml 有 [workspace] 但没有 [package]。适合”没有主包、所有包都平铺在子目录”的场景。例如 rust-analyzer 的根 Cargo.toml:
[workspace]
members = ["xtask/", "lib/*", "crates/*"]
exclude = ["crates/proc_macro_test/imp"]
它根目录不是一个包,纯粹用来统领下面所有成员。
Tip简单项目用”根 package”最省事;如果你想要一个干净的、不含业务代码的总控目录,就用”虚拟清单”。两者没有对错,看团队习惯。
1-3 [workspace] 怎么配
[workspace] 段落决定哪些包是成员:
[workspace]
members = ["member1", "path/to/member2", "crates/*"]
exclude = ["crates/foo", "path/to/other"]
members 列出成员所在目录(目录里要有自己的 Cargo.toml)。它支持 glob 通配,比如 crates/* 匹配 crates 下所有包。
exclude 把某些目录踢出工作空间。典型用法:先用 crates/* 把 crates 全收进来,再用 exclude = ["crates/foo"] 把其中一个特殊成员排除。
还有个便利规则:如果某个本地依赖通过 path 引入、且位于工作空间目录内,它会自动成为成员,不用写进 members。
你也可以写个空的 [workspace] 配合 [package]:
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[workspace]
此时成员包括根包 hello 本身,加上所有在工作空间内、通过 path 引入的本地依赖。
1-4 选择用哪个工作空间
Cargo 默认会向上查找父目录,寻找带 [workspace] 的 Cargo.toml 来定位工作空间。当你 cd 进某个成员子目录敲命令时,它会自动沿父目录往上找到根。
如果成员不在工作空间子目录下(比如放在别处),Cargo 往上找找不到,就得在成员包里手动指定:
[package]
name = "my_member"
version = "0.1.0"
edition = "2024"
[package.workspace]
package.workspace 指向工作空间根目录的位置,显式告诉 Cargo”我用的是哪个工作空间”。
1-5 指定构建哪个包
工作空间里命令默认作用于”当前目录所在的包”。想精确指定,用 -p 或 --package:
cargo build -p member1
cargo test --workspace
--workspace 表示作用于所有成员。若当前目录是虚拟清单根,不带参数的命令会作用在所有成员上(等价于 --workspace)。
还可以用 default-members 在没有显式参数时,限制默认操作的成员:
[workspace]
members = ["path/to/member1", "path/to/member2", "path/to/member3/*"]
default-members = ["path/to/member2", "path/to/member3/foo"]
这样裸跑 cargo build 就只编 default-members 列出的,而不是全部。
1-6 workspace.metadata
和 package.metadata 类似,workspace.metadata 会被 Cargo 忽略——就算你存了自定义配置也不会报警告。它适合给工具存工作空间级别的元信息:
[workspace]
members = ["member1", "member2"]
[workspace.metadata.webcontents]
root = "path/to/webproject"
tool = ["npm", "run", "build"]
比如前端构建工具可以读这段配置,决定去哪找 web 项目、用哪条命令打包。Cargo 本身不碰它,只当透明容器。
Tip工作空间是”大项目拆小包”的标准姿势。等你写一个包含核心库、命令行、Web 服务多个部件的应用时,把它们作为成员放进一个工作空间,依赖和构建一步到位。
1-7 日常命令速查与常见坑
进了工作空间,命令大多能加 -p 精确指向某个成员。几个高频操作:
cargo build -p member1 # 只编某个成员
cargo test --workspace # 跑所有成员的测试
cargo build --workspace # 编全部成员
如果你在虚拟清单根目录直接敲 cargo build(不带参数),它等价于 --workspace,会把所有成员都编一遍。想限制默认范围,用前面讲的 default-members。
Warning在工作空间里改了某个共享依赖的版本,记得在根目录跑
cargo update让它统一生效——因为所有成员共用同一份Cargo.lock。某个成员目录里单独跑cargo update可能会让你疑惑”为什么没生效”,根目录才是真相所在。
另一个常见坑:成员包的 [profile.*] 配置会被忽略,只有根的生效。新手常在某个成员里调 opt-level 发现没反应,正是这个原因。所有编译调优都该写到根 Cargo.toml。
还有,工作空间成员之间互相依赖时,用 path 依赖最自然:
[dependencies]
utils = { path = "../utils" }
这样 utils 自动成为工作空间成员,改动即时可见、无需发版到 crates.io 就能联调。等 utils 稳定了,再把它发到 crates.io、改用版本号依赖也不迟。
1-8 什么时候该用工作空间
不是项目一开始就要上工作空间。如果你只有一个包、代码量不大,普通单包结构足够了,过早拆分反而增加心智负担。判断信号是:当你的代码自然分成了”可独立复用”的几块(比如一个核心算法库、一个命令行前端、一个 Web 服务),且它们要共享依赖和工具配置时,工作空间就值得了。
Tip2024 Edition 还支持”工作空间继承”:成员可以继承根工作空间的
version、edition、rust-version等字段,避免在多个Cargo.toml里重复写、还容易写不一致。小项目用不上,但成员一多就显出价值。
还有人把工作空间当成”monorepo”来用——所有相关服务、工具、库都放一个仓库里统一管理。这很常见,但也要权衡:仓库越大,克隆和 CI 越重。合理拆分成员、用 default-members 控制默认范围,能让大仓库也保持轻快。
下一章我们看如何用 cargo doc 把注释变成漂亮的网页文档。