Rust 入门实战(8):用 Cargo 管理项目

发布时间:2026/8/31 11:05:22
Rust 入门实战(8):用 Cargo 管理项目 上一篇已经用 Tokio 处理异步任务。代码一旦引入运行时、测试工具和可选能力项目管理就不再是“执行一下cargo run”那么简单哪些代码应该被其他程序复用哪些依赖只在测试时出现如何保证同事和 CI 得到相同版本都会直接影响维护成本。本篇把 Cargo 当成工程边界来学习。完成后你不仅能写清单文件还能把同一套拆分方法迁移到命令行工具、Web 服务和多包仓库中。一、先分清 package、crate 与 targetCargo.toml描述的是 package也就是一个可构建、测试和发布的项目单元。crate 是 Rust 编译器的一次编译单元src/lib.rs是库 crate 的根src/main.rs是默认二进制 crate 的根。一个 package 可以同时包含一个库和多个二进制 target还可以拥有集成测试、示例与基准测试。工作区 workspace 则把多个 package 组织在一起共享锁文件和构建目录。这几个概念的区别会影响代码放在哪里。业务规则放进lib.rs调用者便能直接测试函数不必启动整个进程main.rs只解析参数、装配依赖并决定退出码。tests/下的集成测试像外部用户一样只能访问库的公共接口适合验证稳定契约。模块旁的单元测试可以访问私有细节适合验证局部算法。examples/用于可运行的用法演示但不能替代断言明确的测试。下面建立第一个完整项目。执行cargo new task-report --lib用以下内容覆盖src/lib.rs。它只使用标准库既展示公共领域函数也包含单元测试和文档测试复制后可独立执行cargo test。//! 对任务进行归一化并生成进度摘要。#[derive(Debug, Clone, PartialEq, Eq)]pubstructTask{pubtitle:String,pubdone:bool,}implTask{pubfnnew(title:str,done:bool)-Self{Self{title:title.trim().to_owned(),done,}}}/// 生成“已完成/总数”的稳定文本。////// /// use task_report::{summary, Task};/// let tasks vec![Task::new(学习 Cargo, true)];/// assert_eq!(summary(tasks), 完成 1/1 项);/// pubfnsummary(tasks:[Task])-String{letfinishedtasks.iter().filter(|task|task.done).count();format!(完成 {finished}/{} 项,tasks.len())}#[cfg(test)]modtests{usesuper::*;#[test]fntrims_title_and_counts_finished_tasks(){lettasksvec![Task::new( 拆分库 ,true),Task::new(配置 CI,false),];assert_eq!(tasks[0].title,拆分库);assert_eq!(summary(tasks),完成 1/2 项);}}运行输出running 1 test test tests::trims_title_and_counts_finished_tasks ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out running 1 test test src/lib.rs - summary (line 18) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out这里最重要的不是目录形式而是依赖方向入口依赖领域库领域库不反过来依赖命令行或网络框架。以后把终端入口换成 Axum 路由时任务统计规则无需搬家已有测试仍能保护语义。这就是“可迁移”的具体含义。二、依赖声明版本范围与锁文件各管一层普通运行代码需要的 crate 放在[dependencies]只供测试、示例或基准使用的工具放在[dev-dependencies]仅由build.rs使用的依赖才放在[build-dependencies]。分类准确能避免库的使用者下载无关工具也让供应链审查更清晰。不要因为书写方便把所有依赖都堆进普通依赖区。清单中的serde 1.0是兼容版本要求不等于永远固定在某个补丁版本。Cargo 解析依赖后把具体结果写入Cargo.lock。版本要求表达“允许解析什么”锁文件记录“这次实际解析了什么”二者不可互相替代。应用程序和工作区应提交锁文件让本地、CI 与部署尽量使用同一依赖图升级时使用cargo update -p 包名把锁文件变化与测试结果一起审查而不是手工编辑它。检查依赖不能只看自己写下的几行。cargo tree展示传递依赖cargo tree -d找同一 crate 的重复版本cargo tree -e features解释某项 feature 为什么被启用。重复版本不必一律消灭若上游要求不兼容的大版本强行统一可能做不到但它们会增加编译时间和产物体积所以值得查清来源。三、feature 是可叠加能力不是运行时配置Cargo feature 在整个依赖图中按并集合并。只要一个上游启用了某 feature同版本 crate 的其他使用者也会看到它。因此feature 应表达“增加 JSON 输出”“启用 TLS”这类可叠加能力不应表达互斥的开发、测试、生产环境。数据库地址、日志级别等运行期选择应由配置或参数决定。下面是第二个完整项目。执行cargo new cargo-feature-demo保留自动生成的 package 信息并在Cargo.toml末尾添加[features]、下一行添加json []。随后用以下代码覆盖src/main.rs。它没有外部依赖默认与启用 feature 两种组合都能独立编译运行。#[derive(Debug)]structTask{id:u64,title:staticstr,done:bool,}fnrender(task:Task)-String{#[cfg(feature json)]{returnformat!({{\id\:{},\title\:\{}\,\done\:{}}},task.id,task.title,task.done);}#[cfg(not(feature json))]{letmarkiftask.done{x}else{ };format!([{mark}] #{} {},task.id,task.title)}}fnsample_tasks()-VecTask{vec![Task{id:1,title:拆分 library,done:true},Task{id:2,title:检查依赖树,done:false},Task{id:3,title:验证 feature,done:false},]}fnmain(){println!(json_feature{},cfg!(featurejson));fortaskinsample_tasks(){println!({},render(task));}}运行输出json_featurefalse [x] #1 拆分 library [ ] #2 检查依赖树 [ ] #3 验证 feature先运行cargo run可得到以上文本再运行cargo run --features json检查另一编译分支。两者都要进入 CI因为只测试默认组合条件编译中的拼写错误可能长期潜伏。真实项目使用 Serde 生成 JSON不应手工处理转义此处刻意只用标准库是为了让示例无需网络下载即可复现并把注意力放在 feature 的编译行为上。feature 还应尽量保持单向与可加和。例如json [dep:serde_json]表示增加 JSON 能力调用者可以同时启用其他能力。若两个选项确实互斥应重新检查设计无法避免时可以用compile_error!明确拒绝非法组合但不要假定 Cargo 会替你“二选一”。四、工作区统一规则不制造巨型包当核心库、命令行入口和服务器需要分别发布或拥有不同依赖时可以在仓库根创建虚拟工作区。根清单用[workspace]声明成员并设置resolver 2成员仍各自拥有Cargo.toml。工作区共享Cargo.lock与target/cargo test --workspace可以统一验证所有成员。常用的版本、edition 和依赖可分别放入[workspace.package]与[workspace.dependencies]成员通过workspace true继承。这能减少版本漂移却不会自动让成员获得某依赖每个成员仍要显式声明自己使用什么。这个特性很有价值因为从清单即可看出依赖边界不会出现“根目录加了依赖所有包莫名可用”的隐式关系。不要为了看起来专业过早把一个小程序拆成十几个 package。拆分应服务于至少一种真实边界独立发布、不同目标平台、显著不同的依赖、需要限制可见性的领域层或多种入口复用同一核心。若模块已经足够隔离先留在一个 package 内通常更容易导航和重构。五、构建配置与 build.rs 的风险开发构建强调速度和调试信息release 构建强调运行性能两者由 profile 控制。可以设置 LTO、优化等级、调试符号和 panic 策略但每个调整都有代价LTO 可能缩小或加速产物也会增加链接时间去掉调试信息可减小体积却会削弱线上回溯。正确方法是记录构建耗时、二进制大小与代表性性能再决定配置而不是复制一份“最佳参数”。build.rs会在构建阶段执行代码常用于探测系统库或生成绑定也会带来不可复现和供应链风险。优先选择不需要本机隐式状态的方案确需使用时通过cargo:rerun-if-changed精确声明输入不读取凭证不从不固定的远程地址下载内容。构建脚本输出若依赖机器时间、随机数或未声明环境变量即使锁文件相同也可能得到不同产物。六、建立可重复的验收入口日常提交前执行cargo fmt --check、cargo clippy --all-targets --all-features -- -D warnings和cargo test --workspace --all-features。fmt固定表达形式Clippy 提前暴露可疑写法测试验证行为三者职责不同不能互相替代。如果 feature 之间存在关键组合还应显式建立矩阵不能认为--all-features等价于所有真实用户配置。发布前再运行cargo package --list查看将进入包中的文件用cargo metadata核对工作区成员和 target必要时在干净环境构建避免代码悄悄依赖未声明的系统文件。cargo clean能帮助发现缓存掩盖的问题但不应成为每次 CI 的固定步骤因为它会浪费增量编译收益。更可靠的做法是保留快速流水线同时定期增加一次无缓存验证。把这些命令放进仓库脚本或任务入口并让本地和 CI 调用同一个入口。这样迁移到另一套 CI 平台时核心检查仍留在仓库里。团队成员遇到失败也能在本地复现而不是只能阅读平台特有的 YAML。至此我们从异步代码继续向可维护工程迈了一步领域逻辑进入库入口保持轻薄依赖版本有清单与锁文件双重约束可选能力由可叠加 feature 表达多 package 再由工作区统一验证。下一篇接入 Axum、Tokio 与 Serde 时这些边界会直接派上用场路由只负责 HTTP任务规则继续留在可单测的库中外部依赖也能按用途接受审查。参考来源The Cargo BookPackages and CratesCargo ReferenceWorkspacesCargo ReferenceFeaturesCargo ReferenceBuild Scripts 觉得有用就点个赞 收藏方便回头查阅有疑问直接在评论区留言我看到都会回。 本文属于《Rust 入门实战》系列持续更新关注不迷路。 文章里的代码都能直接跑。想要可直接 clone 的完整工程 配套部署脚本 / 踩坑清单评论一声或发邮件到cj2664qq.com我免费发你。如果你正好在做类似系统、或有工程化难题想找人做也欢迎邮件聊一句——我按实际情况评估能落地的就接单或出方案。评论和邮件都能直接找到我不用跳别的平台。