Cargo 与依赖:建立可重复的 Rust 开发闭环
课程概览 · 第 20 章
Cargo 是项目的依赖、构建、测试、文档与发布入口。本章先提供可提前查阅的项目创建与命令速查,再解释工具链、清单、锁文件和构建配置的职责。单包、workspace 和 feature 示例是三个独立项目,不要求读者把课程所有业务模型拼在一起。
前置知识与学习目标
“创建项目与常用命令”可以在第 01 章前查阅;后续内容需要第 14 章的 package/crate 与第 15 章的 cfg。学完本章应能:
- 创建独立项目,区分 check、run、test 与其他命令提供的证据。
- 分清工具链、Edition、MSRV、清单范围与锁文件的职责。
- 读准版本需求,并用 --locked、定点更新管理解析变化。
- 按真实构建边界组织单包、workspace 和最小 feature 集。
创建项目与常用命令
下面使用显式 +stable,不修改全局默认工具链。stable 是可随 rustup update 更新的渠道,不是锁定具体 rustc 版本;Edition 2024 则是语言兼容规则集。
独立示例(工具命令;在存放新项目的目录执行,创建仅标准库的二进制包;之后的 Cargo 命令在 task_cli 根执行):
1 | |
new 默认创建 src/main.rs;库项目使用 --lib,入口为 src/lib.rs。check 做编译检查而不执行应用 main,run 执行选定二进制,test 执行选定测试并检查断言。空测试集通过不能证明业务正确;check 也可能执行构建脚本和过程宏,并非“绝不运行任何代码”。
| 命令(均可加 +stable) | 用途与边界 |
|---|---|
cargo check | 类型与借用检查等,不执行应用 main 或测试断言 |
cargo run | 构建并执行二进制;多 bin 时可用 --bin 指定 |
cargo test | 执行选定单元/集成测试和支持目标的 doctest |
cargo fmt --check | 需要 rustfmt;检查格式,不改文件 |
cargo clippy --all-targets -- -D warnings | 需要 clippy;对选定目标 lint,警告升为错误 |
cargo doc --no-deps | 构建本项目文档,不构建依赖文档 |
cargo add serde | 修改依赖声明并可能更新解析结果 |
cargo tree -i serde | 查看哪些依赖引入了 serde |
cargo update -p serde | 在允许范围内更新指定依赖,相关依赖也可能变化 |
本地和 CI 应使用相同 feature/target 组合运行格式、lint 与行为测试。命令成功分别提供对应证据,不能概括为全部逻辑正确、安全或跨平台兼容。
工具链与 package/crate
| 工具或概念 | 职责 |
|---|---|
| rustup | 安装、更新、选择工具链及 rustfmt/clippy 等组件 |
| rustc | 编译一个 crate,也可由开发者直接调用 |
| Cargo | 解析依赖,调度 rustc,编排测试与构建缓存 |
| package | 一个 Cargo.toml 描述的发布/构建单元 |
| crate | 一次编译单元,如一个 library 或 binary target |
| workspace | 多个 package 的共同工作区,共享 lock 和 target 等 |
一个 package 可以同时有 src/lib.rs 与 src/main.rs,并通过 src/bin 增加多个二进制 target;这不是同一个 crate 有多个入口。library 是供其他 crate 调用的接口,binary 是带 main 的可执行目标。feature 是编译期能力开关,不是运行期配置。
rustup default stable 会改变全局默认选择,本课程不需要它。cargo +stable 显式选择已安装稳定工具链,而更新 stable 会改变其版本。需要精确复现时可在项目记录具体工具链版本,但还须记录目标平台、配置与依赖。
代码片段(工具链配置文件 rust-toolchain.toml;用于项目根,不是下文项目的必需文件;数字为本次课程验证使用版本,组件须已安装或由 rustup 获取):
1 | |
manifest 范围与 lockfile
Cargo.toml 声明包元数据、依赖版本范围、features 和 targets;Cargo.lock 记录实际解析的版本、来源和适用的校验信息;target 目录是产物与缓存,通常不提交。
binary 和 library 仓库默认都提交 Cargo.lock,供本项目开发和 CI 使用。下游依赖库时不采用该库仓库的 lock,而由下游自己的解析图和锁文件决定版本。提交库锁文件并不约束使用它的应用。参见 Cargo.toml 与 Cargo.lock。
已有且仍满足当前清单的 lock 会尽量保留版本,但清单变化、缺失锁文件、成员元数据变化或显式 update 等都可能要求重新解析、更新 lock。--locked 要求锁文件存在且不需更新,否则报错;它不会替你补齐或修复锁文件。
锁文件不是字节级可复现构建保证。工具链、target、环境变量、构建脚本、系统链接库与 feature 组合仍可能改变产物;在线获取依赖与真正离线构建也不是同一件事。
版本约束与更新
裸版本号默认是 caret 需求,serde = "1.2.3" 不是精确版本。Cargo 按约束求解,并尽量沿用锁定结果,不保证每次构建都选择最新版本。
| 写法 | 允许区间或含义 |
|---|---|
1 或 ^1 | >=1.0.0, <2.0.0 |
1.2 或 ^1.2 | >=1.2.0, <2.0.0 |
1.2.3 或 ^1.2.3 | >=1.2.3, <2.0.0 |
~1.2.3 | >=1.2.3, <1.3.0 |
~1.2 | >=1.2.0, <1.3.0 |
1.2.* | >=1.2.0, <1.3.0 |
* | 任意普通版本;维护项目通常避免无上界声明 |
=1.2.3 | 精确版本 1.2.3 |
>=1.2, <1.5 | 两个条件同时满足(AND) |
^0.3 | >=0.3.0, <0.4.0 |
^0.0.3 | >=0.0.3, <0.0.4 |
Cargo 不支持用多区间 OR 串拼接版本需求。0.x 的 caret 上界是 Cargo 的兼容规则:按最左非零分量确定上界;SemVer 只说明 0.x API 尚不稳定,并未把所有 minor 定义成稳定的大版本。作者仍可能在允许范围内引入不兼容行为,因此更新后需要测试。官方规则见 版本需求语法。
普通需求默认不匹配预发布版本。显式包含某一 major.minor.patch 的预发布号会允许该核心版本的合适预发布版本;同一需求仍可能匹配其兼容范围内的稳定版本。不要把允许 beta 解释为只允许 beta,或认为一个预发布声明会开放全部未来核心版本的预发布。
代码片段(依赖维护命令;在已经声明 serde 的项目根执行,版本仅用于展示语法,须满足实际清单约束):
1 | |
=1.2.3 在声明层限定版本,lock 在解析层记录选择,两者用途不同。update -p 可能连带调整依赖关系,不应把它理解成只可能修改锁文件的一行。更改清单后应审阅 lock 差异及相关行为测试,而不是把所有非 update 引发的变化一概判成异常。
MSRV 与 resolver
rust-version 声明项目的最低支持 Rust 版本(MSRV),需要在该版本上验证代码和依赖才可信;Edition 不是 rustc 版本。Edition 2024 的语言最低版本是 1.85,但使用更新 API 的项目可以有更高 MSRV。2024 的 unsafe_op_in_unsafe_fn 默认是 warning,显式 unsafe 块是本课程规范,不是 Edition 默认硬错误,详见第 19 章。
resolver 2 已引入部分 host/target、开发依赖等 feature 分离;resolver 3 主要增加 MSRV 感知解析的默认 fallback。解析优先考虑包声明的 rust-version,未声明时还可能参考当前工具链;没有合适版本时仍可能选择不兼容依赖,因此不保证自动满足所有下游 MSRV。共享同一依赖的已启用 features 仍可能合并,不能说 resolver 3 消除了全部 feature 合并。参见 resolver 与 rust-version。
workspace 版本与 Edition 可在根声明,成员按需继承,但各成员仍是独立发布单元,不是自动联动发布。
代码片段(标准 Cargo 语法;根 Cargo.toml 的追加节,不是完整文件;依赖下文 workspace 根定义):
1 | |
代码片段(crates/domain/Cargo.toml 的替换 package 节,沿用下文 workspace 的根与源码;若要使用,先追加上一块根节):
1 | |
三个独立项目:单包、workspace、features
标准库单包
多文件示例(项目目录 single_package;仅标准库;将两个完整文件放到对应路径,在根执行 cargo +stable run):
文件 single_package/Cargo.toml:
1 | |
多文件示例,文件 single_package/src/main.rs(依赖上块 manifest,仅标准库):
1 | |
dev-dependencies 用于测试、示例和基准等开发目标,不作为普通库依赖提供给下游;bench 也是开发目标,不能只说它对测试生效。
三成员 workspace
以下文件共同组成一个独立项目,不省略所谓“配上源码即可”的部分。根是虚拟 workspace,没有自己的 package。成员间的 path 相对各自 manifest 所在目录解析;发布 crates.io 库时还需要合适的版本要求及发布顺序,单独 path 不是公开 registry 的版本协议。
文件布局:
1 | |
多文件示例,workspace_example/Cargo.toml(完整根清单;无第三方依赖;所有命令在 workspace_example 根执行):
1 | |
多文件示例,workspace_example/crates/domain/Cargo.toml(完整):
1 | |
多文件示例,workspace_example/crates/domain/src/lib.rs(完整,依赖 domain 清单):
1 | |
多文件示例,workspace_example/crates/api/Cargo.toml(完整,path 指向 domain):
1 | |
多文件示例,workspace_example/crates/api/src/lib.rs(完整,依赖 api 清单及 domain):
1 | |
多文件示例,workspace_example/apps/server/Cargo.toml(完整,path 指向两个库成员):
1 | |
多文件示例,workspace_example/apps/server/src/main.rs(完整,依赖 server 清单及两个库):
1 | |
多文件示例(工具命令;执行目录 workspace_example,依赖以上七个文件):
1 | |
预期输出(server 的程序输出;不包括 Cargo 构建日志):
1 | |
workspace 根 Cargo.lock 覆盖成员,共享 target 缓存,不表示自动共享所有依赖声明或发布策略。若成员出现循环依赖,Cargo 会拒绝,应修正层次而不是隐藏依赖。
故意失败:编译错误(Cargo 构建前的锁文件一致性错误,不是 Rust 类型错误;仅在上述项目的临时副本中演示):先通过普通 check 生成 lock,再仅把 crates/domain/Cargo.toml 的 package.version 改为 0.1.1,不改 version() 的示例字符串。执行目录仍为 workspace_example:
1 | |
代码片段(恢复命令;沿用上块临时项目与变化,普通 check 可更新根 lock):
1 | |
更新 package 元数据需要更新锁文件;version() 是普通函数,字符串不会自动随 package.version 改变。这里的错误类别是需要更新 lock,具体报错措辞随 Cargo 版本变化。
feature 门控可选依赖
多文件示例(独立 feature_example;唯一第三方依赖为可选 colored 3;在该目录根执行命令):文件 feature_example/Cargo.toml(完整):
1 | |
多文件示例,文件 feature_example/src/main.rs(完整,依赖上块 manifest):
1 | |
多文件示例(工具命令;执行目录 feature_example,依赖上面的两个文件):
1 | |
两条路径的文本内容都是 task-cli,是否显示 ANSI 颜色取决于终端、环境与库策略,不能承诺任何输出目标必然显绿。optional 依赖默认不启用,dep:colored 显式关联 feature 与依赖并避免额外隐式依赖 feature;这里因显式声明了同名 colored feature,用户仍以该名字启用。features 应按能力设计且通常可叠加;运行时可变配置走参数、环境变量或文件。
config 发现与依赖维护
.cargo/config.toml 配置 Cargo 的别名、rustflags、linker/runner、网络、registry 等行为,与描述 package 的 Cargo.toml 不同。它还可以影响 source、patch、resolver 等设置,不能概括为“绝不影响解析或锁文件”。
Cargo 从执行时的当前目录逐级向上查找配置,再读 CARGO_HOME(默认 ~/.cargo)的配置。近处配置优先,多处数组一般拼接;支持的环境变量和命令行 --config 还会覆盖相应配置。workspace 根调用不会自动读取成员子目录中的配置;从根使用 --manifest-path 指向成员也不能改变这个发现起点。
代码片段(团队配置 workspace_example/.cargo/config.toml;可选,不是 workspace 运行的必要文件;完整示意内容,适用于安装 clippy 的环境):
1 | |
代码片段(个人 ~/.cargo/config.toml,不提交;选择镜像的配置语法,使用前自行确认信任及可达性,不是课程运行前置):
1 | |
团队配置只提交跨机器成立的内容;代理、个人镜像和本机绝对路径留在个人环境。CI 建议写完整命令以便审查,但 Cargo alias 并非在 CI 技术上不能使用,前提是存在同一配置。
引入依赖前查看 MSRV、许可证、默认 features、build.rs、过程宏、已知 advisories 与维护状态。构建脚本和过程宏在构建时执行代码,需按供应链风险审查;cargo audit/cargo deny 是另行安装的工具,扫描已知问题不代表没有未知漏洞。新增或升级依赖后审阅清单与 lock 差异,再运行相关测试;不要把 Cargo 的解析规则当作依赖作者必然遵守兼容性的保证。
深入阅读:构建与发布边界
固定工具链、lock、目标平台、features 和环境能减少差异,但仍需检查本机库、链接器、build scripts 与网络输入。--locked 限制解析变化,离线构建还需依赖缓存或 vendoring;二者不可互换。源清单的精确 = 需求也不锁定工具链或环境。
workspace 只在独立发布、构建目标或依赖边界真实存在时引入,不要把每个模块都拆成 crate。多个成员共享解析会增加版本协调成本,公共依赖仍可能统一 feature。更多模块边界见第 14 章,属性和条件编译见第 15 章,基准与实际测量见第 21 章。
自测
- check、run、test 各自能证明什么?空测试集通过有什么局限?
- 库仓库为何默认提交 lock,下游又为何不采用它?锁文件能保证字节级复现吗?
1.2.3、=1.2.3、^0.3与逗号条件分别是什么意思?- resolver 3 对 MSRV 做了什么,为什么不能保证所有下游自动适配?
- workspace 根调用为什么可能读不到成员配置?colored feature 为什么不保证任何终端显色?
参考答案
- check 编译检查但不执行应用断言;run 执行 main;test 执行选定测试。没有相应测试就没有对应行为证据。
- lock 固定本仓库开发/CI 的解析结果;下游用自己的依赖图。工具链、target、环境和构建脚本仍影响产物。
- 裸版本为 caret,>=1.2.3 且 <2.0.0;等号为精确版本;^0.3 上限 <0.4.0;逗号为 AND,不支持 OR 拼接。
- 默认优先 fallback 到兼容 rust-version 的依赖;无合适版本仍可能选不兼容版本,也无法保证代码与依赖真实支持声明的 MSRV。
- 配置从当前目录向上发现而非向成员向下查找;colored 的输出策略还取决于终端和环境,启用的是能力分支。






