Cargo 构建系统
本章内容可以作为工具,初学只需要掌握如何创建二进制和库项目,如何编译,如何运行接口,其他按需查看。
1. Cargo 简介
Cargo 是 Rust 官方提供的包管理器和构建系统,它负责:
- 下载并编译项目依赖的第三方库(crate)
- 执行构建、测试、文档生成、代码检查等任务
- 管理工作空间(workspace)中的多个相关联的 crate
- 发布 crate 到 crates.io 或私有注册表
2. Cargo 常用命令(核心)
以下是最常用的 Cargo 命令,覆盖项目生命周期的各个环节。
| 命令 | 作用 | 示例 |
|---|---|---|
cargo new <name> | 创建一个新的二进制项目(默认 --bin) | cargo new my_app |
cargo new --lib <name> | 创建一个新的库项目 | cargo new --lib my_lib |
cargo init | 在当前目录初始化一个 Cargo 项目 | cargo init |
cargo build | 编译项目(默认调试模式) | cargo build |
cargo build --release | 编译项目,启用优化(发布模式) | cargo build --release |
cargo run | 编译并运行二进制可执行文件 | cargo run |
cargo check | 快速检查代码能否编译(不生成可执行文件) | cargo check |
cargo test | 运行测试 | cargo test |
cargo doc | 生成项目文档(打开 target/doc/index.html) | cargo doc --open |
cargo fmt | 自动格式化代码(需要安装 rustfmt) | cargo fmt |
cargo add <crate> | 添加一个依赖到 Cargo.toml(Rust 2021 edition 开始推荐) | cargo add serde |
cargo update | 根据语义化版本规则更新依赖的最新版本 | cargo update |
cargo clean | 删除 target 目录(清理构建产物) | cargo clean |
cargo publish | 将 crate 发布到 crates.io | cargo publish |
💡 提示:可以使用
cargo --list查看所有可用命令。
3. Cargo.toml 配置详解
Cargo.toml 是 Cargo 项目的核心配置文件,使用 TOML 格式。下面按功能分类详细解释常用配置项。
3.1 [package] 段 – 项目元信息
定义项目的基本信息,位于文件最开头。
[package]
name = "my_project" # 项目名称,也是 crate 名,发布到 crates.io 时的标识
version = "0.1.0" # 语义化版本号(SemVer)
edition = "2021" # Rust 语言版本(2015, 2018, 2021, 2024)
authors = ["Alice <alice@example.com>"] # 作者列表
description = "A brief description" # 项目简介(发布时必需)
license = "MIT" # 许可证标识(如 MIT, Apache-2.0)
repository = "https://github.com/user/repo" # 源代码仓库 URL
readme = "README.md" # README 文件路径
keywords = ["cli", "tool"] # 搜索关键词(最多 5 个)
categories = ["command-line-utilities"] # 分类标签,见 crates.io 分类表
exclude = ["ci/*"] # 打包时排除的文件(相对于项目根目录)
include = ["src/**/*"] # 强制包含的文件(会覆盖 exclude)
常用字段说明:
name:只能使用字母、数字、下划线、短横线,不能与现有 crate 冲突。edition:推荐使用"2021"或更新版本。version:遵循MAJOR.MINOR.PATCH格式,初始版本通常为0.1.0。license:如果是多个许可证,使用license-file指定文件路径。
3.2 依赖相关段
3.2.1 [dependencies] – 生产依赖
项目在运行时需要的 crate。
[dependencies]
serde = "1.0" # 指定大版本(兼容 1.0.x)
serde_json = { version = "1.0", optional = true } # 可选依赖
rand = { version = "0.8", features = ["small_rng"] } # 启用特性
tokio = { version = "1", features = ["full"] }
chrono = { git = "https://github.com/chronotope/chrono", branch = "main" } # Git 依赖
regex = { path = "../regex" } # 本地路径依赖
版本指定方式:
| 写法 | 含义 | 示例匹配 |
|---|---|---|
"0.1.0" | 精确版本 | 只 0.1.0 |
"^0.1.0" | 兼容更新(默认,不修改最左非零段) | 0.1.0 到 <0.2.0 |
"~0.1.0" | 允许最后一个非零段增加 | 0.1.0 到 <0.2.0(与 ^ 在 0.x 相同) |
"*" 或 ">=1.0" | 范围语法 | 任意版本 ≥1.0 |
3.2.2 [dev-dependencies] – 开发依赖
只在运行测试、示例、基准测试时使用(例如 assert_cmd, tempfile)。
[dev-dependencies]
assert_cmd = "2.0"
tempfile = "3.0"
3.2.3 [build-dependencies] – 构建依赖
仅在构建脚本 build.rs 中使用的 crate(详见第 9 节)。
[build-dependencies]
cc = "1.0"
3.2.4 [target.'cfg(...)'.dependencies] – 平台特定依赖
只在特定目标平台上引入。
[target.'cfg(windows)'.dependencies]
winapi = "0.3"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
3.3 [features] – 特性开关
定义条件编译特性,让用户可以按需启用部分功能。
[features]
default = ["serde"] # 默认启用的特性
serde = ["dep:serde"] # 启用 serde 支持(dep: 前缀表示启用可选的依赖)
json = ["serde_json"] # json 特性启用后,会引入 serde_json 依赖
使用示例:
[dependencies]
my_crate = { version = "0.1", features = ["json"] }
⚠️
dep:前缀语法需要 Rust 2021 edition。
3.4 [profile.*] – 编译优化配置
定制不同构建模式下的编译器优化级别和调试信息。
| Profile | 对应命令 | 用途 |
|---|---|---|
dev | cargo build(默认) | 快速编译、带调试信息 |
release | cargo build --release | 高度优化、体积小 |
test | cargo test | 类似 dev,但优化测试执行速度 |
bench | cargo bench | 类似 release,用于基准测试 |
常用配置项:
[profile.dev]
opt-level = 0 # 优化级别 0-3,0 表示不优化
debug = true # 是否生成调试符号(或数字 1,2)
lto = false # 链接时优化(Link Time Optimization)
codegen-units = 256 # 并行代码生成单元数(值越大编译越快,但运行时性能可能略降)
[profile.release]
opt-level = 3 # 最大优化
debug = false # 不生成调试符号
lto = "fat" # 启用完整 LTO
codegen-units = 1 # 牺牲编译时间换取极致性能
strip = true # 从二进制中剥离符号(减小体积)
3.5 [workspace] – 工作空间配置
用于管理多个相互关联的 crate(详见第 8 节)。
[workspace]
members = ["crates/*", "core"] # 成员目录列表
exclude = ["crates/experiments"] # 排除的目录
resolver = "2" # 使用最新依赖解析器
default-members = ["crates/main"] # 默认操作的目标成员
3.6 [patch] – 依赖覆盖
临时替换依赖的来源(常用于调试或测试未发布的修改)。
[patch.crates-io]
serde = { git = "https://github.com/your-fork/serde" } # 覆盖 crates.io 上的 serde
[patch."https://github.com/rust-lang/cargo"]
cargo = { path = "../cargo" } # 覆盖 git 仓库中的依赖
3.7 [badges] – 项目徽章
在 crates.io 页面显示持续集成、代码覆盖率等徽章(仅用于元数据,不影响构建)。
[badges]
travis-ci = { repository = "user/repo" }
codecov = { repository = "user/repo" }
4. crate 的概念
在 Rust 中,crate 是编译的基本单元。每个 crate 对应一个编译单元,编译后会生成一个库文件(.rlib、.so、.dylib)或可执行文件。
- 二进制 crate:包含
main函数,可以编译为可执行程序。项目根目录下的src/main.rs或src/bin/*.rs都是二进制 crate。 - 库 crate:不包含
main函数,用于提供 API 供其他 crate 使用。项目根目录下的src/lib.rs定义了库 crate。
crate 可以依赖于其他 crate(在 Cargo.toml 中声明),这些依赖也会在编译时被一起处理。crate 也是 Rust 的命名空间,从外部访问 crate 中的项需要引入其路径。
📦 crate vs package:一个 Cargo 项目(package)可以包含一个或多个 crate(例如一个库 crate + 多个二进制 crate)。通常人们混用这两个词,但严格来说,
cargo new创建的是一个 package。
5. 标准项目结构
一个遵循规范的 Cargo 项目目录通常如下:
my_project/
├── Cargo.toml # 项目配置
├── Cargo.lock # 锁定依赖版本(自动生成,需提交到 Git 但避免合并冲突)
├── src/
│ ├── main.rs # 二进制 crate 入口(默认)
│ ├── lib.rs # 库 crate 入口(如果有)
│ ├── bin/ # 多个二进制 crate
│ │ ├── tool1.rs
│ │ └── tool2.rs
│ └── ... # 其他模块文件(.rs)
├── tests/ # 集成测试文件
│ ├── integration_test.rs
│ └── common.rs # 集成测试共享模块(不会作为测试用例运行)
├── examples/ # 示例代码,展示如何使用库
│ └── demo.rs
├── benches/ # 基准测试(需要引入 `test` 特性)
│ └── bench.rs
└── build.rs # 构建脚本(可选)
说明:
Cargo.toml中的name字段决定了生成的二进制文件或库的名称。- 如果同时存在
src/main.rs和src/lib.rs,则 package 包含一个库 crate 和一个同名的二进制 crate(二进制 crate 默认依赖库 crate)。 src/bin/*.rs中每个文件都会生成一个独立的二进制 crate,名称与文件名相同。
6. Rust 模块构建完整规则
模块系统是 Rust 组织代码的核心。Cargo 只负责编译,而模块的可见性、路径、文件结构由 Rust 本身的规则决定。下面详细解释。
6.1 基本概念
- 模块(module):使用
mod关键字声明,用于将代码分组,控制私有性。 - crate 根:编译器开始编译的入口文件(
main.rs或lib.rs)。这个文件隐式地构成一个与 crate 同名的根模块。 - 模块路径:类似文件系统路径,使用
::分隔。例如std::collections::HashMap。 - 可见性:默认所有项(函数、结构体、常量等)是私有的(
private)。父模块可以访问子模块的私有项,但子模块不能访问父模块的私有项。使用pub关键字使其变为公有。
6.2 声明模块的两种方式
6.2.1 方式一:内联模块
直接在文件中使用 mod 后跟大括号定义模块:
#![allow(unused)]
fn main() {
// src/lib.rs
mod network {
fn connect() {}
}
mod client {
fn request() {}
}
}
这种方式适合小模块,不会创建单独的文件。
6.2.2 方式二:将模块内容放到单独的文件中
- 旧式(2015 edition):创建
mod_name/mod.rs文件。 - 新式(2018 edition 及以后):创建
mod_name.rs文件(推荐)。
示例:假设要在 lib.rs 中声明一个 network 模块。
src/lib.rs:
#![allow(unused)]
fn main() {
mod network; // 声明模块,告诉编译器去查找 network.rs 或 network/mod.rs
}
src/network.rs:
#![allow(unused)]
fn main() {
pub fn connect() {
println!("connected");
}
}
如果需要模块嵌套,例如 network::client,可以:
- 创建
src/network/client.rs文件,并在src/network.rs中写mod client;。 - 或者创建
src/network/mod.rs,在其中写mod client;。
强烈推荐使用新式:每个模块一个同名的 .rs 文件,子模块放在同名目录下。例如:
src/
├── lib.rs
├── network.rs
└── network/
└── client.rs
network.rs 内容:
#![allow(unused)]
fn main() {
mod client; // 声明子模块 client,从 network/client.rs 加载
}
6.3 模块路径与 use
- 绝对路径:从 crate 根开始,以
crate关键字开头。 - 相对路径:从当前模块开始,使用
self、super或直接写标识符。
#![allow(unused)]
fn main() {
// 在 src/lib.rs 中
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// 绝对路径调用
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
self::front_of_house::hosting::add_to_waitlist();
}
使用 use 将路径引入作用域,简化调用:
#![allow(unused)]
fn main() {
use crate::front_of_house::hosting;
hosting::add_to_waitlist();
}
重命名:use std::fmt::Result as FmtResult。
重新导出:pub use crate::some::path 使引入的项成为当前模块的公有 API。
6.4 可见性规则总结
| 修饰符 | 含义 |
|---|---|
无 pub | 仅当前模块及其子模块可访问(私有) |
pub | 任何地方都可访问 |
pub(crate) | 仅在当前 crate 内可见 |
pub(super) | 仅在父模块中可见 |
pub(in crate::some::path) | 在指定路径及其子模块中可见 |
示例:
#![allow(unused)]
fn main() {
pub mod outer {
pub fn public_fn() {}
fn private_fn() {}
pub mod inner {
pub fn inner_public() {}
pub(crate) fn inner_crate_visible() {}
pub(super) fn inner_super_visible() {} // 仅在 outer 模块可见
}
}
}
6.5 完整的模块树示例
假设文件结构:
src/
├── lib.rs
├── models.rs
└── services/
├── mod.rs
└── user.rs
lib.rs:
#![allow(unused)]
fn main() {
mod models;
mod services;
pub use services::user::UserService; // 重新导出,使外部可以直接使用 UserService
}
models.rs:
#![allow(unused)]
fn main() {
pub struct User {
pub name: String,
age: u32, // 私有字段
}
impl User {
pub fn new(name: String) -> Self { ... }
}
}
services/mod.rs:
#![allow(unused)]
fn main() {
mod user; // 加载 services/user.rs
pub use user::UserService;
}
services/user.rs:
#![allow(unused)]
fn main() {
use crate::models::User;
pub struct UserService;
impl UserService {
pub fn create_user(name: String) -> User {
User::new(name)
}
}
}
6.6 重要规则总结
mod声明模块,告诉编译器去加载文件(或内联)。use将模块或项引入当前作用域。- 默认所有项私有,使用
pub暴露 API。 - 模块文件的查找规则(2018+):
mod foo;→foo.rs或foo/mod.rs(优先foo.rs,若不存在则回退到foo/mod.rs)。mod foo { ... }→ 内联模块,无外部文件。
src/main.rs和src/lib.rs都是 crate 根,它们自动被视为crate模块。- 二进制 crate 如果包含
lib.rs,则二进制文件可以将库视为外部依赖(通过use my_package::...),其中my_package是[package]中的name。
7. 库 crate 构建
7.1 创建库 crate
cargo new --lib my_lib
cd my_lib
生成的 src/lib.rs 默认包含测试模块和示例函数。
#![allow(unused)]
fn main() {
// src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(add(2, 2), 4);
}
}
}
7.2 公开 API 设计
- 决定哪些函数、结构体、枚举是
pub。 - 使用
pub use重导出,隐藏内部模块层级,提供扁平化 API。 - 编写文档注释
///或//!(模块级),运行cargo doc --open生成文档。
示例:
#![allow(unused)]
fn main() {
//! 一个用于处理数学运算的库
/// 加法函数
///
/// # Examples
/// ```
/// use my_lib::add;
/// assert_eq!(add(1, 2), 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
}
7.3 库的类型
在 Cargo.toml 中可以通过 [lib] 配置库的生成类型(默认是 rlib)。
[lib]
name = "my_lib" # 库名,默认与 package.name 相同
crate-type = ["rlib", "cdylib"] # 生成 Rust 静态库和 C 兼容的动态库
常见的 crate-type:
rlib:Rust 静态库,用于其他 Rust crate 依赖(默认)。cdylib:C 兼容的动态库(.so,.dylib,.dll),用于从其他语言调用。staticlib:C 兼容的静态库(.a,.lib)。bin:可执行文件(通常不用于库)。
7.4 发布到 crates.io
- 注册账号并获取 API Token:
cargo login <token> - 确保
Cargo.toml中包含description、license等必要字段。 - 检查包:
cargo publish --dry-run - 发布:
cargo publish
更新版本时,修改 version 字段后再次执行 cargo publish。
8. 管理多个 crate – Workspace 详解
工作空间(Workspace) 用于组织多个相互关联的 crate,它们共享一个 Cargo.lock 和一个输出目录(target),方便协同开发和依赖管理。
8.1 创建工作空间
步骤:
- 创建一个空目录,并在其中创建
Cargo.toml(工作空间根配置)。 - 定义
[workspace]和成员。 - 创建成员 crate(可以是库或二进制)。
目录结构示例:
my_workspace/
├── Cargo.toml # 工作空间根配置
├── target/ # 共享构建输出
├── crates/
│ ├── core/ # 一个库 crate
│ │ ├── Cargo.toml
│ │ └── src/lib.rs
│ ├── utils/ # 另一个库 crate
│ │ ├── Cargo.toml
│ │ └── src/lib.rs
│ └── app/ # 二进制 crate
│ ├── Cargo.toml
│ └── src/main.rs
└── Cargo.lock # 自动生成,整个工作空间共享
根 Cargo.toml:
[workspace]
members = ["crates/*", "crates/core"] # 使用 glob 或明确列表
resolver = "2"
# 可选:默认成员(执行 cargo build/run 时如果不指定 -p,则针对这些成员)
default-members = ["crates/app"]
8.2 成员 crate 的配置
每个成员 crate 有自己的 Cargo.toml,但不需要单独的 [workspace] 段。它们可以相互依赖。
例如 crates/app/Cargo.toml:
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[dependencies]
core = { path = "../core" } # 引用工作空间内的另一个 crate
utils = { path = "../utils" }
# 也可以依赖外部 crate,工作空间会统一解析版本
serde = "1.0"
8.3 工作空间操作命令
- 构建所有成员:
cargo build(在根目录执行) - 构建特定成员:
cargo build -p core - 运行特定二进制:
cargo run -p app - 测试所有成员:
cargo test - 检查依赖关系:
cargo tree
8.4 共享依赖与 Cargo.lock
- 所有成员共享同一个
Cargo.lock,确保整个工作空间使用相同版本的依赖。 - 如果成员 A 和 B 都依赖 serde 1.0,Cargo 会解析为同一个版本,避免重复编译。
- 当需要升级依赖时,在根目录执行
cargo update会更新所有成员使用的依赖。
8.5 补丁(patch)在工作空间中的使用
可以在根 Cargo.toml 中统一覆盖依赖:
[workspace]
members = ["crates/*"]
[patch.crates-io]
serde = { git = "https://github.com/your-fork/serde" }
8.6 注意事项
- 每个成员 crate 必须有一个唯一的
name。 - 根目录的
Cargo.toml不能包含[package]段(除非根目录本身也是一个 crate,但通常不推荐混合)。 - 工作空间成员可以嵌套,但通常使用扁平结构。
9. build.rs 详解
构建脚本 build.rs 位于项目根目录,在编译主 crate 之前由 Cargo 执行。它主要用于:
- 生成代码(例如根据配置文件生成 Rust 源码)。
- 编译非 Rust 代码(C/C++ 库)并链接。
- 检测系统环境,设置条件编译标志。
- 添加运行时搜索路径等。
9.1 何时需要使用 build.rs
- 需要链接本地系统库(
libfoo.so)。 - 需要根据构建时的信息(操作系统、CPU 特性)生成代码。
- 需要包装一个 C 库并生成 Rust 绑定(使用
bindgen)。 - 需要执行一些仅在编译时需要的工作(比如检查某个命令是否存在)。
如果不需要这些,就不要加 build.rs,以保持简洁。
9.2 基本用法
创建一个 build.rs 文件,写入任意 Rust 代码。Cargo 会编译并执行它。
最简单的 build.rs:
fn main() {
println!("cargo:rerun-if-changed=build.rs");
// 可以在这里输出特定指令给 Cargo
}
9.3 与 Cargo 通信的指令
通过输出特殊格式的字符串到 stdout 来与 Cargo 交互:
| 指令 | 作用 | 示例 |
|---|---|---|
cargo:rerun-if-changed=PATH | 当指定文件变化时重新运行 build.rs | println!("cargo:rerun-if-changed=src/config.json") |
cargo:rerun-if-env-changed=VAR | 当环境变量变化时重新运行 | println!("cargo:rerun-if-env-changed=TARGET_ARCH") |
cargo:rustc-link-lib=TYPE@NAME | 链接外部库(TYPE 可选 static, dylib, framework) | println!("cargo:rustc-link-lib=static=foo") |
cargo:rustc-link-search=TYPE@PATH | 添加库搜索路径 | println!("cargo:rustc-link-search=native=/usr/local/lib") |
cargo:rustc-flags=FLAGS | 传递额外的链接器标志 | println!("cargo:rustc-flags=-l z") |
cargo:rustc-cfg=KEY[="VALUE"] | 添加条件编译标志,相当于 #[cfg(KEY="VALUE")] | println!("cargo:rustc-cfg=has_feature") |
cargo:rustc-env=VAR=VALUE | 设置编译时的环境变量,可在源代码中使用 env! 读取 | println!("cargo:rustc-env=BUILD_DATE=2025-01-01") |
cargo:warning=MESSAGE | 输出警告信息 | println!("cargo:warning=Unsupported OS") |
💡 所有
cargo:前缀的指令必须以println!输出,每行一个。
9.4 完整示例
场景:项目需要链接本地 foo 库,并检测是否为 Linux 平台来启用特定功能。
build.rs:
fn main() {
// 只在 build.rs 或链接的库变化时重新运行
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=libfoo.a");
// 告诉 rustc 链接 libfoo(静态库)
println!("cargo:rustc-link-lib=static=foo");
// 添加库搜索路径
println!("cargo:rustc-link-search=native=/usr/local/lib");
// 检测操作系统
if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "linux" {
println!("cargo:rustc-cfg=has_linux_support");
}
}
然后在 src/lib.rs 中:
#![allow(unused)]
fn main() {
#[cfg(has_linux_support)]
pub fn linux_specific() {
println!("Only on Linux");
}
}
9.5 依赖
如果 build.rs 需要额外的依赖,可以在 Cargo.toml 中添加 [build-dependencies] 段:
[build-dependencies]
cc = "1.0" # 用于编译 C/C++ 代码
walkdir = "2" # 用于遍历文件
注意:build-dependencies 中的 crate 只能在 build.rs 中使用,不会影响主 crate。
9.6 常见用例:编译 C 代码
使用 cc crate 可以轻松编译和链接 C 源码:
// build.rs
fn main() {
cc::Build::new()
.file("src/foo.c")
.compile("foo");
}
在 Cargo.toml 中:
[build-dependencies]
cc = "1.0"