打包模式
前面基础篇的 Cargo 章节已经简单介绍了 profile 的概念, 这一篇专题深入讨论: 如何把 Rust 项目打包成体积更小、运行更快的发布产物, 以及发布流程中常用的工具链
Rust 项目默认有两种打包模式(profile), 由 Cargo 内置:
| 模式 | 命令 | 优化程度 | 调试信息 | 编译速度 | 用途 |
|---|---|---|---|---|---|
| dev(开发模式) | cargo build / cargo run | 无优化 | 完整保留 | 快 | 开发调试, 日常写代码 |
| release(发布模式) | cargo build --release | 高度优化 | 默认剥离 | 慢 | 正式发布, 交付给用户运行 |
# 开发模式: 编译快, 带调试信息, 产物在 target/debug/
cargo build
# 发布模式: 高度优化, 产物在 target/release/
cargo build --release
# 直接以发布模式运行(会先构建再运行)
cargo run --release注意点
- 发布给用户/部署到服务器的程序, 一定要用
--release构建, debug 模式的产物没有优化, 性能可能差 10 倍以上 - 发布产物默认放在
target/release/目录, 直接把里面的可执行文件拷贝出去即可 - 测试默认使用 dev 模式(
cargo test), 所以测试环境跑得快的代码, 在 release 下可能行为/性能不同(比如溢出检查)
打包优化参数
发布模式的优化程度可以通过 Cargo.toml 的 [profile.*] 配置自定义, 常用参数如下:
常用优化参数
| 参数 | 作用 | 常用取值 |
|---|---|---|
| opt-level | 优化等级 | 0~3(数字越大优化越狠), "s"(优化体积), "z"(极致体积) |
| lto | 链接时优化(跨 crate 优化) | false(关), true(全量), "thin"(推荐, 快) |
| codegen-units | 并行编译单元数 | 默认 16, 设为 1 优化最好但编译最慢 |
| panic | panic 时的行为 | "unwind"(默认, 展开调用栈), "abort"(直接终止) |
| strip | 是否剥离符号表/调试信息 | "none", "debuginfo", "symbols"(体积最小) |
| debug | 是否保留调试信息 | 0/false(不保留), 1, 2/true(完整) |
| debug-assertions | 是否启用调试断言(debug_assert!) | true/false |
| overflow-checks | 是否做整数溢出检查 | true/false |
| incremental | 增量编译(只重新编译改动的部分) | true/false |
配置不同 profile
在 Cargo.toml 中修改 [profile.release] 即可定制发布模式:
# Cargo.toml
[profile.release]
opt-level = 3 # 优化拉满
lto = "thin" # 链接时优化(推荐 thin, 编译速度和优化效果的平衡)
codegen-units = 1 # 单编译单元, 优化效果最好(但编译最慢)
panic = "abort" # panic 直接终止, 不展开调用栈(减小体积)
strip = "symbols" # 剥离符号表, 减小二进制体积自定义 profile
除了内置的 dev / release, 还可以用 inherits 继承一个已有 profile, 创建自定义 profile(比如: 性能测试/发布给特定平台):
# Cargo.toml
# 基于 release 再打开调试信息, 用于性能分析
[profile.release-profiling]
inherits = "release" # 继承 release 的所有设置
debug = true # 额外保留调试信息# 使用自定义 profile 构建, 产物在 target/release-profiling/
cargo build --profile release-profiling减小体积三板斧
想让发布产物最小, 常用组合:
panic = "abort": 去掉展开调用栈的代码strip = "symbols": 去掉符号表lto = true+codegen-units = 1: 更激进的优化(也去掉部分冗余代码)
配合 cargo build --release 后, 可以用 ls -lh target/release/xxx 查看实际大小
链接器
Rust 编译的最后一步是链接(link): 把编译好的目标文件和标准库/依赖库合并成可执行文件, 这一步由链接器完成, 默认使用系统自带的链接器
常用链接器
| 链接器 | 特点 | 适用场景 |
|---|---|---|
| 系统默认(如 GNU ld) | 兼容性最好, 但链接速度慢 | 默认, 无需任何配置 |
| lld | LLVM 的链接器, 链接速度快, 内存占用低 | 日常开发, 加快编译速度 |
| mold | 号称"现代链接器", 比 lld 还快, 支持多线程 | 大型项目, 追求极致编译速度 |
配置链接器
通过 .cargo/config.toml 给指定目标平台配置链接器:
# .cargo/config.toml
[target.x86_64-unknown-linux-gnu]
linker = "clang" # 使用 clang 作为链接器驱动
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=lld"] # 让链接器使用 lld# 使用 mold(需要先安装: brew install mold / apt install mold)
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]注意点
- 链接器配置写在
.cargo/config.toml(项目根目录), 而不是Cargo.toml target.xxx中的xxx是目标平台三元组, 可以用rustc -vV查看当前平台- 配置了不存在的链接器会编译报错, 需要先安装对应工具
prelude 设置
标准库 prelude
Rust 默认会把 std::prelude 自动导入到每个文件, 所以不用 use 就能直接用 Vec / String / Option / Result 等常用类型:
// 这些类型不用写 use, 因为 std::prelude 已经自动导入了
fn main() {
let mut v = Vec::new();
v.push(1);
let s = String::from("hello");
let x: Option<i32> = Some(10);
let r: Result<i32, String> = Ok(10);
println!("{v:?} {s} {x:?} {r:?}");
}禁用 prelude
某些场景(嵌入式/内核/极致精简)不需要标准库, 可以用 #![no_std] 禁用, 此时自动导入的是 core::prelude (只有最基础的类型, 没有 String/Vec 等堆相关的类型):
// 禁用标准库: 用于嵌入式/内核/无操作系统的场景
#![no_std]
// 此时 String/Vec 不可用(它们依赖堆分配), 只能使用 core 里的基础类型
fn main() {
let x: Option<i32> = Some(10); // Option 来自 core, 可以直接用
// let s = String::from("hello"); // 编译报错: String 不存在
}注意点
#![no_std]环境下程序仍需要手动提供#[panic_handler]等底层实现, 否则编译会报错#[panic_handler] function required, 一般配合嵌入式框架使用- 自己写
#![no_std]二进制的场景很少, 了解概念即可, 大多数项目不需要
自定义 prelude
对于库项目, 一个常见的做法是提供 prelude 模块, 把最常用的导出集中在一起, 让使用者一行 use 导入所有常用项:
// lib.rs: 定义一个 prelude 模块, 集中导出常用类型
pub mod prelude {
pub use crate::models::*; // 结构体
pub use crate::traits::*; // trait
pub use crate::utils::*; // 工具函数
}
// 使用者只需要:
// use my_lib::prelude::*;什么时候用自定义 prelude
- 库(被其他人使用): 提供
prelude模块, 方便使用者一键导入常用项 - 大型项目内部: 把常用的类型/trait 集中到
prelude模块, 减少重复use - 注意: prelude 只是"集中导出", 不是自动导入, 使用者仍需写
use xxx::prelude::*;
build.rs 设置
build.rs 是什么
build.rs 是构建脚本(build script), 在编译主代码之前自动运行, 常用于: 生成代码/链接系统库/注入编译时信息
| 用途 | 示例 |
|---|---|
| 生成代码 | 根据配置文件生成 Rust 源码 |
| 链接系统库 | 链接 C 库/系统库, 设置库搜索路径 |
| 注入编译时信息 | 把版本号/构建时间/git commit 写进程序 |
| 平台检测 | 检测操作系统/CPU 特性, 设置 cfg 标志 |
默认 Cargo.toml 与 build.rs 同目录时自动生效, 也可以显式指定:
# Cargo.toml
[package]
build = "build.rs" # 指定构建脚本(默认就是 build.rs)
# build 阶段专用的依赖(不会进入最终产物)
[build-dependencies]
# 比如: 在 build.rs 里解析 JSON 配置生成代码
serde_json = "1"常用的 cargo 指令
build.rs 通过 println!("cargo:指令") 告诉 Cargo 该怎么做:
| 指令 | 作用 |
|---|---|
| rerun-if-changed | 指定文件变化时才重新运行 build.rs |
| rustc-env | 设置编译期环境变量(代码里用 env! 读取) |
| rustc-cfg | 设置 cfg 标志(代码里用 #[cfg(...)] 判断) |
| rustc-link-lib | 链接系统库(如 rustc-link-lib=z) |
| rustc-link-search | 添加库搜索路径(链接第三方 .a/.so 时用) |
示例: 注入构建时间
// build.rs
use std::process::Command;
fn main() {
// 1. 只有 build.rs / Cargo.toml 变化时才重新运行
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=Cargo.toml");
// 2. 获取当前 git commit(没有 git 环境则用 unknown)
let commit = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "unknown".to_string());
// 3. 注入为编译期环境变量, 代码里用 env!("GIT_COMMIT") 读取
println!("cargo:rustc-env=GIT_COMMIT={commit}");
}// main.rs
fn main() {
// env!: 编译期读取 build.rs 注入的环境变量(不存在会编译报错)
println!("git commit: {}", env!("GIT_COMMIT"));
}注意点
env!是编译期读取的宏, 值在编译时就固定了, 不是运行时读环境变量build.rs本身也是一个独立的 Rust 程序, 可以cargo run之外独立调试- 不要忘记写
cargo:rerun-if-changed, 否则改文件不会触发重新构建, 容易踩坑
cargo metadata 了解
cargo metadata 以 JSON 格式输出当前项目/工作区的完整依赖信息, 是 CI 脚本/自动化工具读取项目信息的标准方式
# 输出 JSON(默认 --format-version 1)
cargo metadata --format-version 1常用字段
| 字段 | 含义 |
|---|---|
packages | 所有包(包含依赖)的列表: 名称/版本/依赖关系等 |
workspace_members | 当前工作区自己的包 id 列表 |
resolve | 依赖解析结果(最终选定的每个依赖版本) |
target_directory | 编译产物目录(target/) |
workspace_root | 工作区根目录 |
配合 jq 可以提取想要的信息:
# 查看所有直接依赖的名称
cargo metadata --format-version 1 | jq '.packages[].name'
# 查看当前 crate 的版本
cargo metadata --format-version 1 | jq -r '.packages[] | select(.name == "my_app") | .version'
# 查看编译产物目录
cargo metadata --format-version 1 | jq -r '.target_directory'使用场景
- CI 中判断"版本有没有变化, 要不要触发发布"
- 工具脚本自动生成依赖清单/许可证列表
- 构建工具读取
target_directory定位产物
release-plz && github actions
release-plz 是什么
release-plz 是一个自动化发布工具, 基于 Conventional Commits 规范提交信息, 自动完成: 计算下一个版本号 -> 更新 Cargo.toml -> 生成 CHANGELOG -> 发布到 crates.io / GitHub Release
前提: 提交信息遵循 Conventional Commits 规范 (
feat:升 minor,fix:升 patch, 带!或BREAKING CHANGE升 major)
工作流程
| 阶段 | 命令 | 做什么 |
|---|---|---|
| 1. 创建发布 PR | release-plz release-pr | 扫描 main 分支提交, 计算新版本, 更新 Cargo.toml 并生成 CHANGELOG, 创建一个 release PR |
| 2. 合并 release PR | (手动/自动合并) | 确认无误后合并 |
| 3. 发布 | release-plz release | 检测到 release PR 被合并, 自动发布到 crates.io、打 git tag、创建 GitHub Release |
GitHub Actions 配置
在项目里加一个 workflow, 每次推送到 main 自动执行:
# .github/workflows/release-plz.yml
name: Release-plz
on:
push:
branches:
- main
jobs:
# Release unpublished packages.
release-plz-release:
name: Release-plz release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- &checkout
name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- &install-rust
name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: release-plz/action@v0.5
with:
command: release
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
# 注意是 GH_TOKEN github actions 不允许 设置 GITHUB 前缀的 secret
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
# Create a PR with the new versions and changelog, preparing the next release.
release-plz-pr:
name: Release-plz PR
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
concurrency:
group: release-plz-${{ github.ref }}
cancel-in-progress: false
steps:
- *checkout
- *install-rust
- name: Run release-plz
uses: release-plz/action@v0.5
with:
command: release-pr
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}需要提前准备的
- GitHub Actions 权限: 仓库 Settings -> Actions -> General, 把 "Workflow permissions" 改为允许创建和批准 Pull Request
- crates.io token: 在 crates.io 生成 API token (权限勾选
publish-new和publish-update), 添加到仓库 Secrets 里命名为CARGO_REGISTRY_TOKEN - 提交规范: 团队提交信息必须遵循 Conventional Commits, 否则版本号无法自动计算
也可以本地跑
不想配置 Github Actions, 也可以本地手动执行:
# 本地生成 release PR(需要先安装: cargo install release-plz)
release-plz release-pr
# 本地直接发布
release-plz release