Cargo 是 Rust 的构建与包管理工具,它让我们以一致的方式创建、构建、测试与发布 crate。crates.io 是官方的开源包仓库,承载着丰富的生态。

Rust 项目的目录结构遵循约定优于配置的原则。对于不同类型的项目,Cargo 有明确的组织规范:
二进制(可执行)项目以 src/main.rs 作为入口点。当我们运行 cargo run 时,Cargo 会寻找这个文件并编译执行。
|myproj/ Cargo.toml src/ main.rs tests/ examples/ benches/
在 Cargo.toml 中声明依赖时,我们使用语义化版本(Semantic Versioning)来指定兼容的版本范围。Cargo 提供了几种灵活的版本指定方式:
"1" 等同于 "^1",表示接受 >=1.0.0, <2.0.0 的任何版本"0.3" 等同于 "^0.3",表示接受 >=0.3.0, <0.4.0 的版本"1.2.3" 等同于 "^1.2.3",表示接受 >=1.2.3, <2.0.0 的版本除了版本号外,我们还可以为每个依赖配置多个选项来实现精确控制:
features:启用指定的特性列表,如 ["derive", "alloc"]default-features:是否启用默认特性,设为 false 可禁用所有默认特性optional:将依赖标记为可选,只有在相应特性被启用时才会包含git、branch、tag、rev:从 Git 仓库获取依赖path:使用本地路径的依赖这种灵活的依赖管理机制让我们能够根据实际需求裁剪功能,避免引入不必要的代码和依赖,从而优化编译时间和最终的二进制文件大小。
|[package] name = "demo" version = "0.1.0" edition = "2021" [dependencies] serde = { version = "1", features = ["derive"] } anyhow = "1"
[dev-dependencies] 是专门用于开发阶段的依赖声明区域,这些依赖只会在运行测试、构建示例程序或执行基准测试时被包含,不会影响正常的库构建或发布。这类依赖通常包括测试框架、断言库、模拟工具等开发辅助工具。
[build-dependencies] 则用于声明构建脚本 build.rs 所需的依赖。构建脚本在编译主项目之前执行,常用于代码生成、环境检测、C 库绑定等预处理任务。这些依赖只在构建时需要,不会被链接到最终的可执行文件中。
两者的主要区别在于生效时机:开发依赖在开发和测试阶段生效,而构建依赖在编译预处理阶段生效。合理使用这两种依赖类型可以保持生产环境的精简性,同时为开发和构建流程提供必要的工具支持。
|[dev-dependencies] pretty_assertions = "1" [build-dependencies] cc = "*"
Rust 的特性(features)系统是一个强大的条件编译机制,它允许我们根据实际需求来裁剪库的功能。通过特性系统,我们可以创建模块化的代码库,用户只需启用他们需要的功能,从而显著减少编译时间、依赖数量和最终二进制文件的大小。
特性的核心思想是将可选功能封装在条件编译块中。当某个特性被启用时,相关的代码才会被编译进最终程序。我们使用 #[cfg(feature = "特性名")] 属性来标记这些条件代码块。这种机制特别适用于以下场景:
通过合理设计特性,我们可以让同一个库适应不同的使用场景,既满足追求极致性能的嵌入式开发,也能支持功能丰富的桌面应用开发。
|[features] default = ["json"] json = [] yaml = []
|#[cfg(feature = "json")] pub fn encode_json() {} #[cfg(feature = "yaml")] pub fn encode_yaml() {}
Rust 提供了一种优雅的机制来处理可选依赖:我们可以将某个依赖声明为 optional = true,然后在 [features] 部分引用这个依赖名称。
这样做的效果是,只有当对应的特性被启用时,这个依赖才会被实际下载和编译,从而实现"启用依赖即启用功能"的效果。
这种模式特别适用于那些提供额外功能但不是核心必需的依赖。例如,一个数据处理库可能支持多种序列化格式,但用户通常只需要其中的一种或几种。通 过将序列化库声明为可选依赖,我们可以让用户根据实际需求来选择启用哪些功能,避免引入不必要的依赖。
当我们在 [features] 中引用一个可选依赖时,Cargo 会自动为该依赖创建一个同名的特性。
启用这个特性不仅会激活相关的条件编译代码,还会触发对应依赖的下载和编译。这种设计让特性系统和依赖管理紧密结合,为用户提供了灵活而简洁的配置方式。
|[dependencies] serde_json = { version = "1", optional = true } [features] json = ["serde_json"]
工作空间(workspace)是 Rust 项目管理的一个重要概念,它允许我们将多个相关的 crate 组织在一个统一的项目结构中。这种组织方式带来了诸多好处:所有成员 crate 共享同一个 Cargo.lock 文件,确保依赖版本的一致性;共享构建缓存和输出目录,显著提升编译效率;统一管理依赖版本,避免版本冲突。
工作空间的结构相对简单但功能强大。在项目根目录下,我们需要创建一个顶层的 Cargo.toml 文件,这个文件不定义具体的包,而是通过 [workspace] 段落来声明工作空间的配置。其中最重要的是 members 字段,它列出了所有属于这个工作空间的 crate 目录。每个成员 crate 都有自己的 Cargo.toml 文件,定义各自的依赖和配置,但它们都受到工作空间顶层配置的统一管理。
这种设计特别适合大型项目的模块化开发。例如,我们可能有一个核心业务逻辑库、一个命令行工具、一个 Web 服务器,以及一些共享的工具库。通过工作空间,这些相关但独立的组件可以在保持各自独立性的同时,享受统一的依赖管理和构建优化。
|[workspace] members = ["core", "cli"]
在实际开发过程中,我们经常需要依赖一些尚未发布到 crates.io 的代码库,或者需要使用特定版本的依赖。Cargo 为这种情况提供了灵活的依赖指定方式,主要包括路径依赖和 Git 依赖两种形式。
路径依赖允许我们直接引用本地文件系统中的 crate,这在工作空间开发中特别有用。当我们将一个大型项目拆分为多个相互依赖的 crate 时,可以通过路径依赖来建立它们之间的联系。这种方式的优势在于代码修改会立即反映到依赖方,无需重新发布,极大地提升了开发效率。
Git 依赖则让我们能够直接从 Git 仓库获取依赖,这对于使用开源项目的最新开发版本或者私有仓库中的代码非常有用。我们可以指定具体的分支、标签或提交哈希,确保使用的代码版本完全符合预期。这种方式在团队协作开发中尤其常见,例如当我们需要使用同事开发的新功能,但该功能还未正式发布时。
需要注意的是,虽然路径依赖和 Git 依赖在开发阶段很有用,但在发布 crate 到 crates.io 时,所有依赖必须是已发布的版本或者通过 crates.io 可以访问的。这是为了确保用户能够顺利下载和构建我们的 crate。
|[dependencies] my_core = { path = "../core" } utils = { git = "https://example.com/repo.git", rev = "abcdef" }
Rust 的构建配置通过 profiles 来精确控制编译器的行为,主要包括开发模式的 [profile.dev] 和发布模式的 [profile.release] 两种配置。这两种配置在优化策略上有着根本性的差异,分别针对开发效率和运行性能进行了不同的权衡。
在开发模式下,[profile.dev] 的默认配置优先考虑编译速度和调试便利性。优化等级被设置为 opt-level = 0,这意味着编译器几乎不进行优化,从而显著缩短编译时间。同时,调试信息被完整保留(debug = 2),让我们能够使用调试器进行断点调试、查看变量值和调用栈。此外,开发模式默认不启用链接时优化(LTO),并且保持较高的代码生成单元数量,这些设置都有助于加快编译速度。
相比之下,[profile.release] 则完全专注于生成高性能的最终产品。优化等级被提升到 opt-level = 3,这是最高级别的优化,编译器会进行内联展开、死代码消除、循环优化等多种高级优化技术。链接时优化(LTO)通常被启用,它允许编译器在链接阶段进行全局优化,进一步提升性能并减小可执行文件体积。代码生成单元数量也被降低到 1,这有助于更好的优化效果。同时,调试信息通常被移除或简化,以减小最终文件的大小。
此外,我们还可以通过调整 panic 策略来进一步优化发布版本。将 panic = "abort" 设置可以让程序在遇到 panic 时直接终止,而不是展开调用栈,这不仅能够减小可执行文件的体积,还能略微提升性能。这种设置特别适用于那些不需要优雅错误处理的应用场景。
|[profile.release] opt-level = 3 lto = true codegen-units = 1 panic = "abort"