首页 / Rust 入门教程 / Cargo 工作空间(Workspace)

Rust 入门教程

Cargo 工作空间(Workspace)

本教程共 78 篇 · 第 57 篇 · 更新于 2026-08-08 · 约 9 分钟阅读

RustRust 入门教程Cargoworkspace工作空间members虚拟清单

本节目标:理解 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 服务),且它们要共享依赖和工具配置时,工作空间就值得了。

Tip

2024 Edition 还支持”工作空间继承”:成员可以继承根工作空间的 versioneditionrust-version 等字段,避免在多个 Cargo.toml 里重复写、还容易写不一致。小项目用不上,但成员一多就显出价值。

还有人把工作空间当成”monorepo”来用——所有相关服务、工具、库都放一个仓库里统一管理。这很常见,但也要权衡:仓库越大,克隆和 CI 越重。合理拆分成员、用 default-members 控制默认范围,能让大仓库也保持轻快。

下一章我们看如何用 cargo doc 把注释变成漂亮的网页文档。