Skip to content

打包模式

前面基础篇的 Cargo 章节已经简单介绍了 profile 的概念, 这一篇专题深入讨论: 如何把 Rust 项目打包成体积更小、运行更快的发布产物, 以及发布流程中常用的工具链

Rust 项目默认有两种打包模式(profile), 由 Cargo 内置:

模式命令优化程度调试信息编译速度用途
dev(开发模式)cargo build / cargo run无优化完整保留开发调试, 日常写代码
release(发布模式)cargo build --release高度优化默认剥离正式发布, 交付给用户运行
sh
# 开发模式: 编译快, 带调试信息, 产物在 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 优化最好但编译最慢
panicpanic 时的行为"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] 即可定制发布模式:

toml
# Cargo.toml
[profile.release]
opt-level = 3          # 优化拉满
lto = "thin"           # 链接时优化(推荐 thin, 编译速度和优化效果的平衡)
codegen-units = 1      # 单编译单元, 优化效果最好(但编译最慢)
panic = "abort"        # panic 直接终止, 不展开调用栈(减小体积)
strip = "symbols"      # 剥离符号表, 减小二进制体积

自定义 profile

除了内置的 dev / release, 还可以用 inherits 继承一个已有 profile, 创建自定义 profile(比如: 性能测试/发布给特定平台):

toml
# Cargo.toml
# 基于 release 再打开调试信息, 用于性能分析
[profile.release-profiling]
inherits = "release" # 继承 release 的所有设置
debug = true         # 额外保留调试信息
sh
# 使用自定义 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)兼容性最好, 但链接速度慢默认, 无需任何配置
lldLLVM 的链接器, 链接速度快, 内存占用低日常开发, 加快编译速度
mold号称"现代链接器", 比 lld 还快, 支持多线程大型项目, 追求极致编译速度

配置链接器

通过 .cargo/config.toml 给指定目标平台配置链接器:

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
toml
# 使用 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 等常用类型:

rust
// 这些类型不用写 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 等堆相关的类型):

rust
// 禁用标准库: 用于嵌入式/内核/无操作系统的场景
#![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 导入所有常用项:

rust
// 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.tomlbuild.rs 同目录时自动生效, 也可以显式指定:

toml
# 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 时用)

示例: 注入构建时间

rust
// 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}");
}
rust
// 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 metadataJSON 格式输出当前项目/工作区的完整依赖信息, 是 CI 脚本/自动化工具读取项目信息的标准方式

sh
# 输出 JSON(默认 --format-version 1)
cargo metadata --format-version 1

常用字段

字段含义
packages所有包(包含依赖)的列表: 名称/版本/依赖关系等
workspace_members当前工作区自己的包 id 列表
resolve依赖解析结果(最终选定的每个依赖版本)
target_directory编译产物目录(target/)
workspace_root工作区根目录

配合 jq 可以提取想要的信息:

sh
# 查看所有直接依赖的名称
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. 创建发布 PRrelease-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 自动执行:

yaml
# .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 }}

需要提前准备的

  1. GitHub Actions 权限: 仓库 Settings -> Actions -> General, 把 "Workflow permissions" 改为允许创建和批准 Pull Request
  2. crates.io token: 在 crates.io 生成 API token (权限勾选 publish-newpublish-update), 添加到仓库 Secrets 里命名为 CARGO_REGISTRY_TOKEN
  3. 提交规范: 团队提交信息必须遵循 Conventional Commits, 否则版本号无法自动计算

也可以本地跑

不想配置 Github Actions, 也可以本地手动执行:

sh
# 本地生成 release PR(需要先安装: cargo install release-plz)
release-plz release-pr

# 本地直接发布
release-plz release

Released under the MIT License.