Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cargo 构建系统

本章内容可以作为工具,初学只需要掌握如何创建二进制和库项目,如何编译,如何运行接口,其他按需查看。

1. Cargo 简介

Cargo 是 Rust 官方提供的包管理器和构建系统,它负责:

  • 下载并编译项目依赖的第三方库(crate)
  • 执行构建、测试、文档生成、代码检查等任务
  • 管理工作空间(workspace)中的多个相关联的 crate
  • 发布 crate 到 crates.io 或私有注册表

2. Cargo 常用命令(核心)

以下是最常用的 Cargo 命令,覆盖项目生命周期的各个环节。

命令作用示例
cargo new <name>创建一个新的二进制项目(默认 --bincargo 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.htmlcargo doc --open
cargo fmt自动格式化代码(需要安装 rustfmtcargo 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.iocargo 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对应命令用途
devcargo build(默认)快速编译、带调试信息
releasecargo build --release高度优化、体积小
testcargo test类似 dev,但优化测试执行速度
benchcargo 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.rssrc/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.rssrc/lib.rs,则 package 包含一个库 crate 和一个同名的二进制 crate(二进制 crate 默认依赖库 crate)。
  • src/bin/*.rs 中每个文件都会生成一个独立的二进制 crate,名称与文件名相同。

6. Rust 模块构建完整规则

模块系统是 Rust 组织代码的核心。Cargo 只负责编译,而模块的可见性、路径、文件结构由 Rust 本身的规则决定。下面详细解释。

6.1 基本概念

  • 模块(module):使用 mod 关键字声明,用于将代码分组,控制私有性。
  • crate 根:编译器开始编译的入口文件(main.rslib.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 关键字开头。
  • 相对路径:从当前模块开始,使用 selfsuper 或直接写标识符。
#![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 重要规则总结

  1. mod 声明模块,告诉编译器去加载文件(或内联)。
  2. use 将模块或项引入当前作用域。
  3. 默认所有项私有,使用 pub 暴露 API。
  4. 模块文件的查找规则(2018+):
    • mod foo;foo.rsfoo/mod.rs(优先 foo.rs,若不存在则回退到 foo/mod.rs)。
    • mod foo { ... } → 内联模块,无外部文件。
  5. src/main.rssrc/lib.rs 都是 crate 根,它们自动被视为 crate 模块。
  6. 二进制 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

  1. 注册账号并获取 API Token:cargo login <token>
  2. 确保 Cargo.toml 中包含 descriptionlicense 等必要字段。
  3. 检查包:cargo publish --dry-run
  4. 发布:cargo publish

更新版本时,修改 version 字段后再次执行 cargo publish

8. 管理多个 crate – Workspace 详解

工作空间(Workspace) 用于组织多个相互关联的 crate,它们共享一个 Cargo.lock 和一个输出目录(target),方便协同开发和依赖管理。

8.1 创建工作空间

步骤

  1. 创建一个空目录,并在其中创建 Cargo.toml(工作空间根配置)。
  2. 定义 [workspace] 和成员。
  3. 创建成员 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.rsprintln!("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, frameworkprintln!("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"