目录
AI提问:
在 Rust 多应用、多模块项目中,各个应用和模块通常会在各自的
Cargo.toml中声明依赖,因此容易出现同一个依赖重复配置、版本不一致的问题。如何统一管理这些公共依赖的版本,确保各个应用和模块使用相同版本?是否可以像设置全局变量一样集中声明依赖版本,以便后续升级时只修改一处即可?
AI回复:
可以。Rust 的 Cargo Workspace 原生提供了“统一管理依赖版本”的功能:
[workspace.dependencies]
它相当于多模块项目里的全局依赖版本配置。以后升级依赖时,只需要修改根目录的 Cargo.toml,各应用和模块都会继承新的版本。Cargo 官方称其为 Workspace dependency inheritance(工作空间依赖继承)。(Rust 文档)
一、推荐的项目结构
例如一个包含多个后端应用和公共模块的 Rust 项目:
my-project/
├── Cargo.toml
├── Cargo.lock
│
├── apps/
│ ├── api-server/
│ │ ├── Cargo.toml
│ │ └── src/
│ └── task-worker/
│ ├── Cargo.toml
│ └── src/
│
└── crates/
├── common/
│ ├── Cargo.toml
│ └── src/
├── config/
│ ├── Cargo.toml
│ └── src/
└── database/
├── Cargo.toml
└── src/
其中:
apps/api-server:API 服务apps/task-worker:后台任务服务crates/common:公共工具crates/config:配置管理crates/database:数据库访问
所有成员共享根目录的 Cargo.lock 和 target 目录。(Rust 文档)
二、在根目录统一声明依赖
根目录的 Cargo.toml:
[workspace]
resolver = "3"
members = [
"apps/api-server",
"apps/task-worker",
"crates/common",
"crates/config",
"crates/database",
]
[workspace.package]
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
[workspace.dependencies]
# 异步运行时
tokio = { version = "1.48.0", features = ["full"] }
# Web 框架
salvo = { version = "0.85.0", features = ["affix-state"] }
# 序列化
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
# 配置
toml = "0.9.8"
dotenvy = "0.15.7"
# 错误处理
anyhow = "1.0.100"
thiserror = "2.0.17"
# 日志
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
# 数据库
sea-orm = {
version = "1.1.17",
features = [
"sqlx-mysql",
"runtime-tokio-rustls",
"macros",
],
}
# 工作空间内部模块
common = { path = "crates/common" }
config = { path = "crates/config" }
database = { path = "crates/database" }
这里的:
[workspace.dependencies]
就是你要找的“全局依赖版本配置”。
但它不属于传统编程语言里的变量,Cargo 也不支持下面这种自定义变量插值:
# 不支持这种写法
[variables]
tokio_version = "1.48.0"
[dependencies]
tokio = "${tokio_version}"
应该直接使用 Cargo 提供的 [workspace.dependencies]。
三、子应用继承根目录依赖
1. api-server
apps/api-server/Cargo.toml:
[package] name = "api-server" version.workspace = true edition.workspace = true rust-version.workspace = true [dependencies] tokio.workspace = true salvo.workspace = true serde.workspace = true serde_json.workspace = true anyhow.workspace = true tracing.workspace = true tracing-subscriber.workspace = true common.workspace = true config.workspace = true database.workspace = true
下面两种写法完全等价:
tokio.workspace = true
等价于:
tokio = { workspace = true }
通常推荐第一种,比较简洁。
2. task-worker
apps/task-worker/Cargo.toml:
[package] name = "task-worker" version.workspace = true edition.workspace = true rust-version.workspace = true [dependencies] tokio.workspace = true serde.workspace = true serde_json.workspace = true anyhow.workspace = true tracing.workspace = true common.workspace = true config.workspace = true database.workspace = true
这里没有再次写:
tokio = "1.48.0"
而是:
tokio.workspace = true
因此,api-server 和 task-worker 使用的 Tokio 版本要求都来自根目录。
3. database 模块
crates/database/Cargo.toml:
[package] name = "database" version.workspace = true edition.workspace = true rust-version.workspace = true [dependencies] sea-orm.workspace = true serde.workspace = true thiserror.workspace = true config.workspace = true
四、以后如何统一升级版本
假设现在根目录是:
[workspace.dependencies]
tokio = { version = "1.48.0", features = ["full"] }
以后要升级 Tokio,只修改根目录:
[workspace.dependencies]
tokio = { version = "1.49.0", features = ["full"] }
然后执行:
cargo update -p tokio
或者重新构建:
cargo build --workspace
所有使用:
tokio.workspace = true
的应用和模块都会使用新的统一版本要求。
五、Cargo.toml 和 Cargo.lock 的区别
这个区别非常重要。
Cargo.toml:声明版本范围
tokio = "1.48.0"
它通常并不表示“只能使用 1.48.0”。
Cargo 默认使用 ^ 兼容规则,上面的写法大致相当于:
tokio = "^1.48.0"
允许的范围是:
>= 1.48.0 < 2.0.0
因此,Cargo 可能最终解析到:
1.49.0 1.50.0 ……
只要没有进入 2.x。
Cargo.lock:记录实际安装版本
例如:
[[package]] name = "tokio" version = "1.49.0"
Workspace 内的所有包共享根目录的一个 Cargo.lock,后续构建会尽量继续使用锁文件中记录的实际版本。(Rust 文档)
可以这样理解:
| 文件 | 作用 |
|---|---|
Cargo.toml |
声明允许使用什么版本 |
Cargo.lock |
记录本次实际使用什么版本 |
六、怎样严格锁定一个版本
如果你要求必须使用指定版本,例如必须是:
tokio 1.48.0
可以加等号:
[workspace.dependencies]
tokio = { version = "=1.48.0", features = ["full"] }
含义是:
只能使用 1.48.0
以下写法的区别:
tokio = "1.48.0"
表示:
>= 1.48.0 且 < 2.0.0
而:
tokio = "=1.48.0"
表示:
只能是 1.48.0
不过通常不建议对所有库都使用 =,因为限制过严可能导致依赖解析冲突。Cargo 官方也建议仅在确实需要严格同步、已知新版本有兼容问题等场景使用精确版本。(Rust 文档)
对于普通业务项目,我更推荐:
tokio = "1.48.0"
然后:
- 提交根目录的
Cargo.lock - CI 使用
cargo build --locked - 依赖升级时主动执行
cargo update
这样既统一,又不至于把版本条件限制得过死。
七、某个应用需要额外 Feature 怎么办
假设根目录定义:
[workspace.dependencies]
serde = { version = "1.0.228", features = ["derive"] }
普通模块:
[dependencies] serde.workspace = true
某个应用还需要 rc 功能:
[dependencies]
serde = {
workspace = true,
features = ["rc"],
}
最终该应用获得的功能是:
derive + rc
子模块增加的 features 会和根目录声明的 features 合并,而不是覆盖。(Rust 文档)
八、default-features 最好在哪里配置
例如所有项目都不需要 Tokio 的默认功能,可以在根目录统一设置:
[workspace.dependencies]
tokio = {
version = "1.48.0",
default-features = false,
features = [
"rt-multi-thread",
"macros",
"signal",
],
}
成员中只需要:
[dependencies] tokio.workspace = true
不要在子模块里试图修改:
tokio = {
workspace = true,
default-features = false,
}
继承依赖时,成员主要可以额外指定:
featuresoptional
其他依赖属性,例如 version、default-features,应该在根 Workspace 中统一定义。(Rust 文档)
九、可选依赖如何处理
根 Workspace 不能直接把依赖声明成 optional = true:
# 不推荐,在 workspace.dependencies 中不能这样声明
[workspace.dependencies]
redis = {
version = "0.32.0",
optional = true,
}
应该在根目录只声明版本:
[workspace.dependencies] redis = "0.32.0"
然后在需要可选功能的成员中声明:
[dependencies]
redis = {
workspace = true,
optional = true,
}
[features]
default = []
redis-cache = ["dep:redis"]
这样:
cargo build
不会启用 Redis。
执行:
cargo build --features redis-cache
才会启用 Redis。
十、开发依赖和构建依赖也能继承
不仅普通依赖可以统一管理,下面这些都支持 Workspace 继承:
[dependencies]
[dev-dependencies]
[build-dependencies]
[target.'cfg(...)'.dependencies]
例如根目录:
[workspace.dependencies]
tokio = { version = "1.48.0", features = ["full"] }
mockall = "0.13.1"
cc = "1.2.49"
成员模块:
[dependencies] tokio.workspace = true [dev-dependencies] mockall.workspace = true [build-dependencies] cc.workspace = true
这些都是 Cargo 官方支持的继承方式。(Rust 文档)
十一、内部模块也应该统一声明
多模块项目中,内部模块可以一起放进 [workspace.dependencies]。
根目录:
[workspace.dependencies]
common = { path = "crates/common" }
config = { path = "crates/config" }
database = { path = "crates/database" }
应用中:
[dependencies] common.workspace = true config.workspace = true database.workspace = true
相比每个模块重复写:
common = { path = "../../crates/common" }
config = { path = "../../crates/config" }
database = { path = "../../crates/database" }
这种方式更加统一,也避免相对路径层级错误。
十二、依赖不会自动加到所有模块
根目录定义了:
[workspace.dependencies] tokio = "1.48.0" serde = "1.0.228" sea-orm = "1.1.17"
并不代表所有成员都会自动依赖它们。
每个成员仍然要显式选择:
[dependencies] tokio.workspace = true serde.workspace = true
如果某个模块不需要 SeaORM,就不要声明:
sea-orm.workspace = true
这种设计可以避免每个子模块都携带大量无关依赖。
十三、如何检查是否存在重复版本
可以执行:
cargo tree --duplicates
简写:
cargo tree -d
例如输出:
http v0.2.12 └── old-library v1.0.0 http v1.3.1 └── salvo v0.85.0
这说明依赖树里同时存在两个不兼容版本:
http 0.2.12 http 1.3.1
查看某个库被谁依赖:
cargo tree -i tokio
查看指定版本:
cargo tree -i tokio@1.48.0
需要注意:即使你统一了所有直接依赖,也可能因为第三方库的间接依赖要求不同,出现同一个库的多个大版本。例如:
库 A 依赖 http 0.2 库 B 依赖 http 1.x
这种情况下,Cargo 可能必须同时编译两个版本。这并不是 [workspace.dependencies] 能完全消除的,因为它主要统一的是你项目中的直接依赖声明。
十四、推荐的完整配置
对于你的 Rust 多应用、多模块项目,可以采用下面这套写法。
根目录 Cargo.toml:
[workspace]
resolver = "3"
members = [
"apps/*",
"crates/*",
]
[workspace.package]
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
license = "MIT"
authors = ["Your Team"]
[workspace.dependencies]
# Web
salvo = "0.85.0"
# Async
tokio = {
version = "1.48.0",
features = ["full"],
}
# Serialization
serde = {
version = "1.0.228",
features = ["derive"],
}
serde_json = "1.0.145"
toml = "0.9.8"
# Database
sea-orm = {
version = "1.1.17",
features = [
"sqlx-mysql",
"runtime-tokio-rustls",
"macros",
],
}
# Configuration
dotenvy = "0.15.7"
# Error handling
anyhow = "1.0.100"
thiserror = "2.0.17"
# Logging
tracing = "0.1.41"
tracing-subscriber = {
version = "0.3.20",
features = ["env-filter"],
}
# Internal crates
common = { path = "crates/common" }
config = { path = "crates/config" }
database = { path = "crates/database" }
成员模块:
[package] name = "api-server" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true authors.workspace = true [dependencies] salvo.workspace = true tokio.workspace = true serde.workspace = true serde_json.workspace = true anyhow.workspace = true tracing.workspace = true tracing-subscriber.workspace = true common.workspace = true config.workspace = true database.workspace = true
十五、最终建议
你的场景建议采用:
[workspace.dependencies]
核心规则是:
# 根 Cargo.toml [workspace.dependencies] tokio = "1.48.0" serde = "1.0.228"
# 子模块 Cargo.toml [dependencies] tokio.workspace = true serde.workspace = true
版本升级时只改根目录:
tokio = "1.49.0"
然后执行:
cargo update -p tokio cargo check --workspace cargo test --workspace
生产和 CI 构建建议使用:
cargo build --release --locked
这样可以保证构建严格使用已经提交的 Cargo.lock,避免 CI 或生产构建时悄悄解析出不同版本。