Rust 学习从 Rspack 的仓库结构开始。Rspack 有许多 crate,它们需要共享 Rust 版本、依赖版本和锁文件;Cargo workspace 就是这层组织机制。
1. workspace 是什么
一个 workspace 是多个相关 package 的集合。根目录的 Cargo.toml 描述公共规则,crates/*/Cargo.toml 描述各自职责。每个 crate 仍可以单独声明库或二进制入口,但 Cargo 会把它们放进同一张依赖图。
典型结构:
text
rspack/
Cargo.toml
Cargo.lock
crates/
rspack_core/Cargo.toml
node_binding/Cargo.toml
workspace 适合大型 Rust 工程,因为 core、绑定和插件可以独立编译、测试和发布,同时共享依赖解析结果。
2. workspace = true 做了什么
Rspack 成员清单中可以看到:
toml
edition.workspace = true
version.workspace = true
rspack_core = { workspace = true }
它表示"到 workspace 根清单查找这个字段"。根清单可以集中声明版本:
toml
[workspace.package]
edition = "2021"
version = "0.1.0"
[workspace.dependencies]
rspack_core = { path = "crates/rspack_core", version = "0.1.0" }
成员只写 workspace = true,就不会各自重复维护版本字符串和路径。依赖升级时修改根配置,Cargo 重新解析整个 workspace。
3. 统一版本不等于统一 feature
成员可以在继承依赖后追加 feature:
toml
napi-derive = { workspace = true, features = ["compat-mode", "type-def"] }
版本来自根清单,feature 来自当前成员。feature 是 Cargo 的编译配置,可能影响条件编译和生成代码,所以每个 crate 仍需要明确自己的需求。
4. feature 到底是什么
Cargo feature 是一个编译期开关。它本质上是一个名字,Cargo 根据启用的名字向编译器传递 cfg(feature = "...") 条件;代码可以据此选择是否编译某些模块、依赖或实现:
toml
[features]
browser = []
rust
#[cfg(feature = "browser")]
pub fn platform_name() -> &'static str {
"browser"
}
执行 cargo build --features browser 时,这段函数参与编译;不启用时,函数不会进入当前构建。feature 只影响编译配置,不是运行时变量,也不是环境变量。
Rspack 的 crates/node_binding/Cargo.toml 中有:
toml
[features]
browser = ["rspack_binding_api/browser"]
debug_tool = ["rspack_binding_api/debug_tool"]
plugin = ["rspack_binding_api/plugin"]
这里左边是 rspack_node 自己对外提供的开关,右边是依赖 crate 的 feature。用户执行 cargo build -p rspack_node --features browser 时,Cargo 会同时启用 rspack_binding_api 的 browser feature。这个写法叫 feature 转发:上层 crate 暴露一个统一入口,下层 crate 负责真正的实现。
下层的定义更加具体:
toml
[features]
browser = ["dep:rspack_browser", "rspack_napi/browser"]
debug_tool = ["rspack_core/debug_tool"]
dep:rspack_browser 表示启用一个 optional dependency;rspack_core/debug_tool 表示继续打开另一个依赖 crate 的 feature。于是一个开关可以沿依赖图向下传播,最终决定哪些代码和依赖被编译。
5. 依赖 feature、crate feature 与 default-features
要区分三种写法:
toml
[features]
plugin = ["rspack_util/plugin"]
[dependencies]
napi = { workspace = true, features = ["async", "tokio_rt"] }
swc_core = { workspace = true, default-features = false, features = ["ecma_transforms_react"] }
features 表中的 plugin 是当前 crate 的 feature。它需要用户用 --features plugin 显式打开,并可以转发到其他 crate。
依赖表中的 features = ... 是直接启用依赖 crate 的 feature。上例中 napi 的异步和 Tokio 运行时支持会随当前 crate 一起编译;它不要求用户再单独执行 --features async。
default-features = false 会关闭该依赖默认启用的 feature,然后只保留当前列出的 ecma_transforms_react。这常用于减少编译内容、避免不需要的系统能力,或让 Rspack 明确控制依赖的功能集合。关闭默认 feature 后,依赖可能缺少默认 API,代码和平台配置必须随之匹配。
Rspack 根 Cargo.toml 使用 resolver = "2"。在同一次构建中,依赖 feature 通常按启用请求合并;如果两个 crate 分别要求同一个依赖的不同 feature,最终构建会包含它们的并集。resolver 2 会减少 dev-dependencies、build-dependencies 和部分平台依赖之间不必要的 feature 串联,但它不会把同一个正常依赖强行拆成两份独立 feature 实例。因此 feature 应被设计成可组合的增量能力。
6. 如何选择 feature
可以按下面的顺序判断:
- 这是当前 crate 的能力开关吗?写在 features,必要时转发到依赖。
- 这是第三方库必须启用的 API 吗?写在依赖项的 features。
- 默认能力是否会引入不需要的代码或平台依赖?考虑 default-features = false,再逐项添加需要的 feature。
- feature 是否会改变公开 API、依赖数量或目标平台?若会,应该在 crate 文档和构建命令中说明。
feature 适合表达编译期能力组合,例如 Rspack 的浏览器绑定、调试工具和插件支持;不适合保存用户运行时才决定的配置,运行时配置应使用普通参数、环境变量或配置文件。
7. 最小可运行示例
建立一个 workspace:
text
demo/
Cargo.toml
crates/a/src/lib.rs
crates/b/src/main.rs
根 Cargo.toml:
toml
[workspace]
members = ["crates/a", "crates/b"]
resolver = "2"
[workspace.dependencies]
demo-a = { path = "crates/a" }
crates/b/Cargo.toml:
toml
[package]
name = "demo-b"
version.workspace = true
edition.workspace = true
[dependencies]
demo-a = { workspace = true }
执行 cargo run -p demo-b 时,Cargo 会先解析两个成员,再编译 demo-a,最后链接 demo-b。成员名、package 名和目录名可以不同,依赖表中的 package 名才是引用依据。
8. 常见误区
- workspace = true 只能继承根 workspace 已声明的字段,不能凭空创建依赖。
- workspace 统一解析版本,不代表每个 crate 都启用了相同 feature。
- Cargo.lock 位于 workspace 根时,通常由整个工程共享;不要把它理解成单个 crate 的私有配置。
- crate 之间仍要保持清晰依赖方向;workspace 不会自动阻止循环设计。
简要说明
从 Rspack 的 Cargo 配置认识 workspace、workspace = true 和依赖继承,理解多 crate 工程如何统一版本并保留局部 feature。