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

English Original

Rust 工程实践:超越 cargo build 🟢

讲师简介

  • Microsoft SCHIE(Silicon and Cloud Hardware Infrastructure Engineering)团队首席固件架构师
  • 在安全、系统编程(固件、操作系统、虚拟机监控器)、CPU 与平台架构以及 C++ 系统方面经验丰富
  • 2017 年在 AWS EC2 开始使用 Rust,此后长期深度投入

这是一本聚焦 Rust 工具链实践的实用指南,覆盖许多团队往往接触得太晚的关键能力:构建脚本、交叉编译、基准测试、代码覆盖率,以及借助 Miri 和 Valgrind 做安全验证。每章都基于真实硬件诊断代码库中的具体示例展开,该代码库是一个大型多 crate 工作区,因此书中的每项技巧都能直接映射到生产代码。

如何使用本书

本书适合 自定节奏学习或团队工作坊。各章大体独立,你既可以按顺序阅读,也可以直接跳到当前最需要的主题。

难度说明

标记等级含义
🟢入门规则清晰、上手直接,第一天就能用到
🟡中级需要理解工具链内部机制或平台概念
🔴进阶涉及更深的工具链知识、nightly 特性或多工具协同

学习节奏建议

部分章节预计时间关键收获
I — 构建与交付第 01–02 章3–4 小时构建元数据、交叉编译、静态二进制
II — 度量与验证第 03–05 章4–5 小时统计型基准测试、覆盖率门禁、Miri 与 Sanitizer
III — 加固与优化第 06–10 章6–8 小时供应链安全、发布配置、编译期工具、no_std 与 Windows
IV — 集成第 11–13 章3–4 小时生产级 CI/CD 流水线、一线实践技巧与综合练习
16–21 小时完整生产工程流水线视角

练习建议

每章都包含带难度标记的 🏋️ 练习。答案放在可展开的 <details> 区块中,建议先做题,再核对答案。

  • 🟢 练习通常可在 10–15 分钟内完成
  • 🟡 练习通常需要 20–30 分钟,并可能需要本地运行工具
  • 🔴 练习通常需要较多环境准备与实验时间(1 小时以上)

前置知识要求

概念建议学习位置
Cargo 工作区结构Rust Book ch14.3
Feature 标志Cargo Reference — Features
#[cfg(test)] 与基础测试Rust Patterns 第 12 章
unsafe 代码块与 FFI 基础Rust Patterns 第 10 章

章节依赖图

                 ┌─────────────────┐
                 │ 第 00 章         │
                 │ 简介 (Intro)    │
                 └────┬─────┬───┬──┘
        ┌─────┬───┬──┴──┬───┴───┴──┬──────┬──────┐
        ▼     ▼   ▼     ▼          ▼      ▼      ▼
      ch01  ch03 ch04  ch05       ch06   ch09   ch10
      构建  基准 覆盖率 Miri       依赖  no_std Windows
        │     │    │    │          │      │      │
        │     └────┴────┘          │      ▼      │
        │          │               │    ch10     │
        ▼          ▼               ▼   Windows   │
       ch02      ch07            ch07    │       │
       交叉编译 发布配置         发布配置  │       │
        │          │               │     │       │
        │          ▼               │     │       │
        │        ch08              │     │       │
        │      编译工具            │     │       │
        └──────────┴───────────────┴─────┴───────┘
                           │
                           ▼
                         ch11
                    生产级 CI/CD 流水线
                           │
                           ▼
                        ch12 ──── ch13
                       实践技巧   速查卡

可任意顺序阅读:第 01、03、04、05、06、09 章相互独立。
建议在具备前置知识后阅读:第 02 章(依赖第 01 章),第 07–08 章(先学第 03–06 章效果更好),第 10 章(最好先看第 09 章)。
建议最后阅读:第 11 章(综合收束全书)、第 12 章(技巧汇总)、第 13 章(参考速查)。

带说明的内容目录

第一部分 — 构建与交付

#章节难度说明
1第 01 章:构建脚本 — 深入理解 build.rs🟢编译期常量、编译 C 代码、protobuf 生成、系统库链接与反模式
2第 02 章:交叉编译 — 一份源码,多种目标🟡目标三元组、musl 静态二进制、ARM 交叉编译、cross、cargo-zigbuild 与 GitHub Actions

第二部分 — 度量与验证

#章节难度说明
3第 03 章:基准测试 — 衡量真正重要的指标🟡Criterion.rs、Divan、perf 火焰图、PGO 与 CI 中的持续基准测试
4第 04 章:代码覆盖率 — 发现测试遗漏🟢cargo-llvm-cov、cargo-tarpaulin、grcov 与 Codecov/Coveralls 集成
5第 05 章:Miri、Valgrind 与 Sanitizer🔴MIR 解释器、Valgrind memcheck/Helgrind、ASan/MSan/TSan、cargo-fuzz 与 loom

第三部分 — 加固与优化

#章节难度说明
6第 06 章:依赖管理与供应链安全🟢cargo-audit、cargo-deny、cargo-vet、cargo-outdated 与 cargo-semver-checks
7第 07 章:发布配置与二进制体积🟡发布配置结构、LTO 权衡、cargo-bloat 与 cargo-udeps
8第 08 章:编译期与开发者工具🟡sccache、mold、cargo-nextest、cargo-expand、cargo-geiger、工作区 lint 与 MSRV
9第 09 章:no_std 与特性验证🔴cargo-hack、core/alloc/std 分层、自定义 panic handler 与 no_std 代码测试
10第 10 章:Windows 与条件编译🟡#[cfg] 模式、windows-sys/windows crate、cargo-xwin 与平台抽象

第四部分 — 集成

#章节难度说明
11第 11 章:综合实战 — 生产级 CI/CD 流水线🟡GitHub Actions 工作流、cargo-make、pre-commit hook、cargo-dist 与综合实战
12第 12 章:一线实践技巧🟡10 个经验证的实战模式:deny(warnings) 陷阱、缓存调优、依赖去重、RUSTFLAGS 等
13第 13 章:速查卡-命令速览、60+ 条决策表条目以及延伸阅读链接

English Original

构建脚本 — 深入理解 build.rs 🟢

你将学到:

  • build.rs 如何融入 Cargo 构建流水线及其运行机制
  • 五种生产实践模式:编译期常量、C/C++ 编译、protobuf 代码生成、pkg-config 链接和特性检测
  • 会拖慢构建或破坏交叉编译的反模式
  • 如何权衡可追溯性与可复现构建 (Reproducible Builds)

相关章节: 交叉编译 使用构建脚本实现目标平台感知构建 · no_std 与特性验证 扩展了此处设置的 cfg 标志 · CI/CD 流水线 在自动化中编排构建脚本

每个 Cargo 包都可以在 crate 根目录下包含一个名为 build.rs 的文件。 Cargo 会在编译你的 crate 之前 编译并执行该文件。构建脚本通过 stdout 上的 println! 指令与 Cargo 进行通信。

什么是 build.rs 以及它何时运行

┌─────────────────────────────────────────────────────────┐
│                    Cargo 构建流水线                      │
│                                                         │
│  1. 解析依赖                                            │
│  2. 下载 crate                                          │
│  3. 编译 build.rs  ← 普通 Rust 代码,在 HOST(宿机)运行  │
│  4. 执行 build.rs  ← stdout → Cargo 指令                │
│  5. 编译 crate(使用步骤 4 中的指令)                      │
│  6. 链接                                                │
└─────────────────────────────────────────────────────────┘

关键事实:

  • build.rs 在 宿主 (Host) 机器上运行,而不是在目标 (Target) 机器上。在交叉编译期间,构建脚本在你的开发机上运行,即使最终二进制文件针对的是不同的架构。
  • 构建脚本的作用范围仅限于其所属的包。它无法影响其他 crate 的编译方式 —— 除非该包在 Cargo.toml 中声明了 links 键,这允许通过 cargo::metadata=KEY=VALUE 向下游 crate 传递元数据。
  • 只要 Cargo 检测到变更,它就会 每次 运行 —— 除非你发出 cargo::rerun-if-changed 指令来限制重新运行。

注意 (Rust 1.71+):自 Rust 1.71 起,Cargo 会对编译后的 build.rs 二进制文件进行指纹识别 —— 如果二进制文件完全相同,即使源代码时间戳改变了,它也不会重新运行。然而,cargo::rerun-if-changed=build.rs 仍然很有价值:如果没有 任何 rerun-if-changed 指令,Cargo 会在 包内的任何文件 发生变化时重新运行 build.rs(而不仅仅是 build.rs 发生变化)。发出 cargo::rerun-if-changed=build.rs 可以将重新运行限制在仅当 build.rs 本身发生变化时 —— 这在大型 crate 中能显著节省编译时间。

  • 它可以发出 cfg 标志、环境变量、链接器参数 以及主 crate 消费的 文件路径。

最简 Cargo.toml 配置项:

[package]
name = "my-crate"
version = "0.1.0"
edition = "2021"
build = "build.rs"       # 默认值 —— Cargo 会自动寻找 build.rs
# build = "src/build.rs" # 或者将其放在其他位置

Cargo 指令协议

构建脚本通过在标准输出打印指令来与 Cargo 通信。自 Rust 1.77 起,首选前缀是 cargo::(取代了旧的单冒号 cargo: 形式)。

指令用途
cargo::rerun-if-changed=PATH仅当 PATH 变更时重新运行 build.rs
cargo::rerun-if-env-changed=VAR仅当环境变量 VAR 变更时重新运行
cargo::rustc-link-lib=NAME链接原生库 NAME
cargo::rustc-link-search=PATH向库搜索路径添加 PATH
cargo::rustc-cfg=KEY为条件编译设置 #[cfg(KEY)] 标志
cargo::rustc-cfg=KEY="VALUE"设置 #[cfg(KEY = "VALUE")] 标志
cargo::rustc-env=KEY=VALUE设置可通过 env!() 访问的环境变量
cargo::rustc-cdylib-link-arg=FLAG为 cdylib 目标向链接器传递 FLAG
cargo::warning=MESSAGE在编译期间显示警告
cargo::metadata=KEY=VALUE存储可由下游 crate 读取的元数据
// build.rs — 极简示例
fn main() {
    // 仅在 build.rs 本身变化时重新运行
    println!("cargo::rerun-if-changed=build.rs");

    // 设置编译期环境变量
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".into());
    println!("cargo::rustc-env=BUILD_TIMESTAMP={timestamp}");
}

模式 1:编译期常量

最常见的用例:将构建元数据写入二进制文件,以便在运行时报告(git 哈希、构建日期、CI 任务 ID)。

// build.rs
use std::process::Command;

fn main() {
    println!("cargo::rerun-if-changed=.git/HEAD");
    println!("cargo::rerun-if-changed=.git/refs");

    // Git commit hash
    let output = Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .output()
        .expect("git not found");
    let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
    println!("cargo::rustc-env=GIT_HASH={git_hash}");

    // 构建配置 (debug 或 release)
    let profile = std::env::var("PROFILE").unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=BUILD_PROFILE={profile}");

    // 目标三元组 (Target triple)
    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=BUILD_TARGET={target}");
}
#![allow(unused)]
fn main() {
// src/main.rs — 消费构建期的值
fn print_version() {
    println!(
        "{} {} (git:{} target:{} profile:{})",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION"),
        env!("GIT_HASH"),
        env!("BUILD_TARGET"),
        env!("BUILD_PROFILE"),
    );
}
}

内置 Cargo 环境变量(免费获得,无需 build.rs): CARGO_PKG_NAME、CARGO_PKG_VERSION、CARGO_PKG_AUTHORS、 CARGO_PKG_DESCRIPTION、CARGO_MANIFEST_DIR。 查看 完整列表。

模式 2:使用 cc crate 编译 C/C++ 代码

当你的 Rust crate 封装了 C 库或需要小型 C 辅助程序(在硬件接口中很常见)时,cc crate 简化了 inside build.rs 的编译工作。

# Cargo.toml
[build-dependencies]
cc = "1.0"
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=csrc/");

    cc::Build::new()
        .file("csrc/ipmi_raw.c")
        .file("csrc/smbios_parser.c")
        .include("csrc/include")
        .flag("-Wall")
        .flag("-Wextra")
        .opt_level(2)
        .compile("diag_helpers");
    // 这将产生 libdiag_helpers.a 并发出正确的
    // cargo::rustc-link-lib 和 cargo::rustc-link-search 指令。
}
#![allow(unused)]
fn main() {
// src/lib.rs — 编译后的 C 代码的 FFI 绑定
extern "C" {
    fn ipmi_raw_command(
        netfn: u8,
        cmd: u8,
        data: *const u8,
        data_len: usize,
        response: *mut u8,
        response_len: *mut usize,
    ) -> i32;
}

/// 封装了原始 IPMI 命令接口的安全包装。
/// 假设:enum IpmiError { CommandFailed(i32), ... }
pub fn send_ipmi_command(netfn: u8, cmd: u8, data: &[u8]) -> Result<Vec<u8>, IpmiError> {
    let mut response = vec![0u8; 256];
    let mut response_len: usize = response.len();

    // SAFETY: 响应缓冲区足够大,且 response_len 已正确初始化。
    let rc = unsafe {
        ipmi_raw_command(
            netfn,
            cmd,
            data.as_ptr(),
            data.len(),
            response.as_mut_ptr(),
            &mut response_len,
        )
    };

    if rc != 0 {
        return Err(IpmiError::CommandFailed(rc));
    }
    response.truncate(response_len);
    Ok(response)
}
}

对于 C++ 代码,使用 .cpp(true) 和 .flag("-std=c++17"):

// build.rs — C++ 变体
fn main() {
    println!("cargo::rerun-if-changed=cppsrc/");

    cc::Build::new()
        .cpp(true)
        .file("cppsrc/vendor_parser.cpp")
        .flag("-std=c++17")
        .flag("-fno-exceptions")    // 匹配 Rust 的无异常模型
        .compile("vendor_helpers");
}

模式 3:Protocol Buffers 与代码生成

构建脚本非常擅长代码生成 —— 在编译时将 .proto、.fbs 或 .json 等模式文件转换为 Rust 源码。以下是使用 prost-build 的 protobuf 模式:

# Cargo.toml
[build-dependencies]
prost-build = "0.13"
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=proto/");

    prost_build::compile_protos(
        &["proto/diagnostics.proto", "proto/telemetry.proto"],
        &["proto/"],
    )
    .expect("Failed to compile protobuf definitions");
}
#![allow(unused)]
fn main() {
// src/lib.rs — 包含生成的代码
pub mod diagnostics {
    include!(concat!(env!("OUT_DIR"), "/diagnostics.rs"));
}

pub mod telemetry {
    include!(concat!(env!("OUT_DIR"), "/telemetry.rs"));
}
}

OUT_DIR 是 Cargo 提供的目录,构建脚本应在此放置生成的文件。每个 crate 都在 target/ 下拥有自己的 OUT_DIR。

模式 4:使用 pkg-config 链接系统库

对于提供 .pc 文件的系统库(如 systemd、OpenSSL、libpci),pkg-config crate 会探测系统并发出正确的链接指令:

# Cargo.toml
[build-dependencies]
pkg-config = "0.3"
// build.rs
fn main() {
    // 探测 libpci(用于 PCIe 设备枚举)
    pkg_config::Config::new()
        .atleast_version("3.6.0")
        .probe("libpci")
        .expect("libpci >= 3.6.0 not found — install pciutils-dev");

    // 探测 libsystemd(可选 — 用于 sd_notify 集成)
    if pkg_config::probe_library("libsystemd").is_ok() {
        println!("cargo::rustc-cfg=has_systemd");
    }
}
#![allow(unused)]
fn main() {
// src/lib.rs — 基于 pkg-config 探测结果的条件编译
#[cfg(has_systemd)]
mod systemd_notify {
    extern "C" {
        fn sd_notify(unset_environment: i32, state: *const std::ffi::c_char) -> i32;
    }

    pub fn notify_ready() {
        let state = std::ffi::CString::new("READY=1").unwrap();
        // SAFETY: state 是一个有效的以 null 结尾的 C 字符串。
        unsafe { sd_notify(0, state.as_ptr()) };
    }
}

#[cfg(not(has_systemd))]
mod systemd_notify {
    pub fn notify_ready() {
        // 在没有 systemd 的系统上不执行任何操作
    }
}
}

模式 5:特性检测与条件编译

构建脚本可以探测编译环境并设置 cfg 标志,供主 crate 用于条件代码路径。

CPU 架构和操作系统检测(安全 —— 这些是编译期常量):

// build.rs — 检测 CPU 特性和操作系统能力
fn main() {
    println!("cargo::rerun-if-changed=build.rs");

    let target = std::env::var("TARGET").unwrap();
    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();

    // 在 x86_64 上启用 AVX2 优化路径
    if target.starts_with("x86_64") {
        println!("cargo::rustc-cfg=has_x86_64");
    }

    // 在 aarch64 上启用 ARM NEON 路径
    if target.starts_with("aarch64") {
        println!("cargo::rustc-cfg=has_aarch64");
    }

    // 检测 /dev/ipmi0 是否可用(编译期检查)
    if target_os == "linux" && std::path::Path::new("/dev/ipmi0").exists() {
        println!("cargo::rustc-cfg=has_ipmi_device");
    }
}

⚠️ 反模式演示 —— 下面的代码显示了一种诱人但有问题的做法。请勿在生产环境中使用。

// build.rs — 坏习惯:在构建时进行运行时硬件探测
fn main() {
    // 反模式:二进制文件与构建机器的硬件绑定了。
    // 如果你在带 GPU 的机器上构建并部署到不带 GPU 的机器,
    // 二进制文件会默认为存在 GPU。
    if std::process::Command::new("accel-query")
        .arg("--query-gpu=name")
        .arg("--format=csv,noheader")
        .output()
        .is_ok()
    {
        println!("cargo::rustc-cfg=has_accel_device");
    }
}
#![allow(unused)]
fn main() {
// src/gpu.rs — 基于构建期检测进行适配的代码
pub fn query_gpu_info() -> GpuResult {
    #[cfg(has_accel_device)]
    {
        run_accel_query()
    }

    #[cfg(not(has_accel_device))]
    {
        GpuResult::NotAvailable("accel-query not found at build time".into())
    }
}
}

⚠️ 为什么这是错的:对于可选硬件,运行时设备检测几乎总是优于构建时检测。上面产生的二进制文件会 与构建机器的硬件配置绑定 —— 它在部署目标上的行为可能会有所不同。仅对那些在编译时确实固定总结的能力(架构、操作系统、库的可用性)使用构建时检测。对于像 GPU 这样的硬件,应使用 which accel-query 或 accel-mgmt 探测在运行时进行探测。

反模式与坑点

反模式危害修正
缺少 rerun-if-changedbuild.rs 在 每次 构建时都会运行,拖慢迭代速度始终至少发出 cargo::rerun-if-changed=build.rs
在 build.rs 中发起网络请求离线构建失败,不可复现使用 Vendor 文件或单独的 fetch 步骤
写入 src/ 目录Cargo 不期望源码在构建期间改变写入 OUT_DIR 并使用 include!()
重型计算拖慢每次 cargo build将结果缓存至 OUT_DIR,并使用 rerun-if-changed 进行门控
忽略交叉编译直接使用 Command::new("gcc") 而不尊重 $CC使用能正确处理交叉编译工具链的 cc crate
无上下文的 panicunwrap() 会给出模糊的 “build script failed” 错误使用 .expect("描述性消息") 或打印 cargo::warning=

应用:嵌入构建元数据

该项目目前使用 env!("CARGO_PKG_VERSION")进行版本报告。构建脚本可以通过更丰富的元数据来扩展这一点:

// build.rs — 建议的添加项
fn main() {
    println!("cargo::rerun-if-changed=.git/HEAD");
    println!("cargo::rerun-if-changed=.git/refs");
    println!("cargo::rerun-if-changed=build.rs");

    // 嵌入 git 哈希以便在诊断报告中进行溯源
    if let Ok(output) = std::process::Command::new("git")
        .args(["rev-parse", "--short=10", "HEAD"])
        .output()
    {
        let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
        println!("cargo::rustc-env=APP_GIT_HASH={hash}");
    } else {
        println!("cargo::rustc-env=APP_GIT_HASH=unknown");
    }

    // 嵌入构建时间戳以便进行报告关联
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".into());
    println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}");

    // 输出目标三元组 — 在多架构部署中很有用
    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=APP_TARGET={target}");
}
#![allow(unused)]
fn main() {
// src/version.rs — 消费元数据
pub struct BuildInfo {
    pub version: &'static str,
    pub git_hash: &'static str,
    pub build_epoch: &'static str,
    pub target: &'static str,
}

pub const BUILD_INFO: BuildInfo = BuildInfo {
    version: env!("CARGO_PKG_VERSION"),
    git_hash: env!("APP_GIT_HASH"),
    build_epoch: env!("APP_BUILD_EPOCH"),
    target: env!("APP_TARGET"),
};

impl BuildInfo {
    /// 必要时在运行时解析 epoch(在 stable Rust 上,目前还无法实现 const &str → u64 的转换 — 
    /// 还没有用于字符串转整数的 const fn)。
    pub fn build_epoch_secs(&self) -> u64 {
        self.build_epoch.parse().unwrap_or(0)
    }
}

impl std::fmt::Display for BuildInfo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "DiagTool v{} (git:{} target:{})",
            self.version, self.git_hash, self.target
        )
    }
}
}

来自项目的关键洞察:整个代码库中几乎没有任何 build.rs 文件,因为它采用了纯 Rust 开发,没有 C 依赖、没有代码生成,也没有系统库链接。当你确实需要这些功能时,build.rs 是不二之选 —— 但不要“为了加而加”。在大型代码库中,没有构建脚本往往是一个特性,而非缺失。请参阅 依赖管理 了解该项目如何在没有自定义构建逻辑的情况下管理其供应链,这正是架构简洁的有力信号。

亲自尝试

  1. 嵌入 Git 元数据:创建一个 build.rs,将 APP_GIT_HASH 和 APP_BUILD_EPOCH 作为环境变量输出。在 main.rs 中使用 env!() 获取并打印。验证提交代码后哈希值是否发生变化。

  2. 探测系统库:编写一个 build.rs,使用 pkg-config 探测 libz (zlib)。如果找到,则发出 cargo::rustc-cfg=has_zlib 指令。在 main.rs 中根据该标志条件打印 “zlib available” 或 “zlib not found”。基于该 cfg 标志。

  3. 人为触发构建失败:从你的 build.rs 中移除 rerun-if-changed 行,观察在 cargo build 和 cargo test 期间它运行了多少次。然后将其加回,对比差异。

可复现构建 (Reproducible Builds)

第 1 章介绍了如何将时间戳和 Git 哈希嵌入二进制文件。虽然这对溯源很有用,但它会 与可复现构建相冲突 —— 即相同的源代码应当始终产生完全相同的二进制文件。

矛盾点:

目标成效代价
可溯源性二进制文件包含 APP_BUILD_EPOCH每次构建都是唯一的 — 无法验证完整性
可复现性cargo build --locked 始终产生相同输出缺少构建时的元数据

实际解决方案:

# 1. 在 CI 中始终使用 --locked(确保遵循 Cargo.lock)
cargo build --release --locked
# 如果 Cargo.lock 缺失或过时则会失败 — 从而捕获 "在我的机器上能跑" 的问题

# 2. 对于对复现性要求极高的构建,设置 SOURCE_DATE_EPOCH
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) cargo build --release --locked
# 使用最后一次提交的时间戳而非 "现在" — 相同的提交 = 相同的二进制文件
#![allow(unused)]
fn main() {
// 在 build.rs 中:遵循 SOURCE_DATE_EPOCH 以保证复现性
let timestamp = std::env::var("SOURCE_DATE_EPOCH")
    .unwrap_or_else(|_| {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs().to_string())
            .unwrap_or_else(|_| "0".into())
    });
println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}");
}

最佳实践:在构建脚本中使用 SOURCE_DATE_EPOCH,这样发布构建就是可复现的(git-hash + 锁定的依赖 + 确定的时间戳 = 相同的二进制文件),而开发构建依然可以享受实时时间戳带来的便利。

构建流水线决策图

flowchart TD
    START["需要编译期工作吗?"] -->|不需要| SKIP["无需 build.rs"]
    START -->|需要| WHAT{"哪种类型?"}
    
    WHAT -->|"嵌入元数据"| P1["模式 1\n编译期常量"]
    WHAT -->|"编译 C/C++"| P2["模式 2\ncc crate"]
    WHAT -->|"代码 generation"| P3["模式 3\nprost-build / tonic-build"]
    WHAT -->|"链接系统库"| P4["模式 4\npkg-config"]
    WHAT -->|"检测特性"| P5["模式 5\ncfg 标志"]
    
    P1 --> RERUN["始终发出\ncargo::rerun-if-changed"]
    P2 --> RERUN
    P3 --> RERUN
    P4 --> RERUN
    P5 --> RERUN
    
    style SKIP fill:#91e5a3,color:#000
    style RERUN fill:#ffd43b,color:#000
    style P1 fill:#e3f2fd,color:#000
    style P2 fill:#e3f2fd,color:#000
    style P3 fill:#e3f2fd,color:#000
    style P4 fill:#e3f2fd,color:#000
    style P5 fill:#e3f2fd,color:#000

🏋️ 练习

🟢 练习 1:版本标记

创建一个包含 build.rs 的最小 crate,将当前 Git 哈希和构建配置 (profile) 嵌入环境变量。在 main() 中打印它们。验证在 debug 和 release 构建之间输出是否发生改变。

答案
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=.git/HEAD");
    println!("cargo::rerun-if-changed=build.rs");

    let hash = std::process::Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .output()
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=GIT_HASH={hash}");
    println!("cargo::rustc-env=BUILD_PROFILE={}", std::env::var("PROFILE").unwrap_or_default());
}
// src/main.rs
fn main() {
    println!("{} v{} (git:{} profile:{})",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION"),
        env!("GIT_HASH"),
        env!("BUILD_PROFILE"),
    );
}
cargo run          # 显示 profile:debug
cargo run --release # 显示 profile:release

🟡 练习 2:条件系统库

编写一个 build.rs,使用 pkg-config 同时探测 libz 和 libpci。为找到的每个库分别发出核心 cfg 标志。在 main.rs 中,打印构建时检测到了哪些库。

答案
# Cargo.toml
[build-dependencies]
pkg-config = "0.3"
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=build.rs");
    if pkg_config::probe_library("zlib").is_ok() {
        println!("cargo::rustc-cfg=has_zlib");
    }
    if pkg_config::probe_library("libpci").is_ok() {
        println!("cargo::rustc-cfg=has_libpci");
    }
}
// src/main.rs
fn main() {
    #[cfg(has_zlib)]
    println!("✅ 检测到 zlib");
    #[cfg(not(has_zlib))]
    println!("❌ 未找到 zlib");

    #[cfg(has_libpci)]
    println!("✅ 检测到 libpci");
    #[cfg(not(has_libpci))]
    println!("❌ 未找到 libpci");
}

关键收获

  • build.rs 在编译时运行在 宿主机 上 —— 务必发出 cargo::rerun-if-changed 以避免不必要的重新构建。
  • 使用 cc crate(而非原始 gcc 命令)进行 C/C++ 编译 —— 它能正确处理交叉编译工具链。
  • 将生成的文件写入 OUT_DIR,永远不要写入 src/ —— Cargo 不希望源码在构建期间发生改变。
  • 对于可选硬件,运行时探测优于构建时探测。
  • 嵌入时间戳时使用 SOURCE_DATE_EPOCH 以保证构建的可复现性。

English Original

交叉编译 — 一份源码,多种目标 🟡

你将学到:

  • Rust 目标三元组 (Target Triples) 的工作原理以及如何使用 rustup 添加它们
  • 为容器/云端部署构建静态 musl 二进制文件
  • 使用原生工具链、cross 和 cargo-zigbuild 交叉编译到 ARM (aarch64)
  • 为多架构 CI 设置 GitHub Actions 矩阵构建

相关章节: 构建脚本 — 交叉编译期间 build.rs 在 HOST 上运行 · 发布配置 — 交叉编译发布版二进制文件的 LTO 和 strip 设置 · Windows — Windows 交叉编译与 no_std 目标

交叉编译是指在一台机器(宿主机)上构建可在另一台机器(目标机)上运行的可执行文件。宿主机可能是你的 x86_64 笔记本电脑;目标机可能是 ARM 服务器、基于 musl 的容器,甚至是 Windows 机器。 Rust 使这变得非常可行,因为 rustc 本身就是一个交叉编译器 —— 它只需要正确的目标库和兼容的链接器。

目标三元组详解

每个 Rust 编译目标都由一个 目标三元组 (Target Triple) 标识(尽管叫三元组,但通常包含四个部分):

<架构>-<厂商>-<操作系统>-<环境>

示例:
  x86_64  - unknown - linux  - gnu      ← 标准 Linux (glibc)
  x86_64  - unknown - linux  - musl     ← 静态 Linux (musl libc)
  aarch64 - unknown - linux  - gnu      ← ARM 64 位 Linux
  x86_64  - pc      - windows- msvc     ← 带有 MSVC 的 Windows
  aarch64 - apple   - darwin             ← 搭载 Apple Silicon 的 macOS
  x86_64  - unknown - none              ← 裸机 (无 OS)

列出所有可用目标:

# 显示 rustc 可以编译到的所有目标(约 250 个)
rustc --print target-list | wc -l

# 显示系统中已安装的目标
rustup target list --installed

# 显示当前的默认目标
rustc -vV | grep host

使用 rustup 安装工具链

# 添加目标库(该目标的 Rust 标准库)
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-gnu

# 现在你可以进行交叉编译了:
cargo build --target x86_64-unknown-linux-musl
cargo build --target aarch64-unknown-linux-gnu  # 需要链接器 —— 见下文

rustup target add 为你提供了什么:该目标的预编译 std、core 和 alloc 库。它 不 提供 C 链接器或 C 库。对于需要 C 工具链的目标(大多数 gnu 目标),你需要单独安装。

# Ubuntu/Debian — 安装 aarch64 的交叉链接器
sudo apt install gcc-aarch64-linux-gnu

# Ubuntu/Debian — 安装用于静态构建的 musl 工具链
sudo apt install musl-tools

# Fedora
sudo dnf install gcc-aarch64-linux-gnu

.cargo/config.toml — 针对目标的配置

不必在每个命令中都传递 --target,可以在项目根目录或主目录的 .cargo/config.toml 中配置默认值:

# .cargo/config.toml

# 此项目的默认目标(可选 — 省略则保持原生默认值)
# [build]
# target = "x86_64-unknown-linux-musl"

# aarch64 交叉编译的链接器
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
rustflags = ["-C", "target-feature=+crc"]

# musl 静态构建的链接器(通常系统级 gcc 即可胜任)
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
rustflags = ["-C", "target-feature=+crc,+aes"]

# ARM 32 位 (Raspberry Pi, 嵌入式)
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"

# 适用于所有目标的环境变量
[env]
# 示例:设置自定义 sysroot
# SYSROOT = "/opt/cross/sysroot"

配置文件搜索顺序(匹配即停止):

  1. <项目>/.cargo/config.toml
  2. <项目>/../.cargo/config.toml(逐级向上查找父目录)
  3. $CARGO_HOME/config.toml(通常是 ~/.cargo/config.toml)

使用 musl 构建静态二进制文件

为了部署到极简容器(Alpine、scratch Docker 镜像)或无法控制 glibc 版本的系统,请使用 musl 进行构建:

# 安装 musl 目标
rustup target add x86_64-unknown-linux-musl
sudo apt install musl-tools  # 提供 musl-gcc

# 构建完全静态的二进制文件
cargo build --release --target x86_64-unknown-linux-musl

# 验证其是否为静态链接
file target/x86_64-unknown-linux-musl/release/diag_tool
# → ELF 64-bit LSB executable, x86-64, statically linked

ldd target/x86_64-unknown-linux-musl/release/diag_tool
# → not a dynamic executable

静态与动态的权衡:

维度glibc (动态)musl (静态)
二进制体积较小 (共享库)较大 (增加约 5-15 MB)
可移植性需要匹配 glibc 版本在任何 Linux 上均可运行
DNS 解析完整的 nsswitch 支持基础解析器 (不支持 mDNS)
部署需要 sysroot 或容器单一二进制文件,无依赖
性能malloc 稍快malloc 稍慢
dlopen() 支持支持不支持

针对本项目:静态 musl 构建是部署到各种服务器硬件的理想选择,因为你无法保证宿主机的操作系统版本。单一二进制文件的部署模型消除了“在我的机器上能跑”的问题。

交叉编译到 ARM (aarch64)

ARM 服务器(AWS Graviton、Ampere Altra、Grace)在数据中心变得越来越普遍。在 x86_64 宿主机上为 aarch64 进行交叉编译:

# 第 1 步:安装目标 + 交叉链接器
rustup target add aarch64-unknown-linux-gnu
sudo apt install gcc-aarch64-linux-gnu

# 第 2 步:在 .cargo/config.toml 中配置链接器(见上文)

# 第 3 步:构建
cargo build --release --target aarch64-unknown-linux-gnu

# 第 4 步:验证二进制文件
file target/aarch64-unknown-linux-gnu/release/diag_tool
# → ELF 64-bit LSB executable, ARM aarch64

运行目标架构的测试需要:

  • 一台真实的 ARM 机器
  • 或者 QEMU 用户模式模拟
# 安装 QEMU 用户模式(在 x86_64 上运行 ARM 二进制文件)
sudo apt install qemu-user qemu-user-static binfmt-support

# 现在 cargo test 可以通过 QEMU 运行交叉编译的测试
cargo test --target aarch64-unknown-linux-gnu
# (速度较慢 —— 每个测试二进制文件都是模拟运行的。适用于 CI 验证,不建议日常开发使用。)

在 .cargo/config.toml 中将 QEMU 配置为测试运行器:

[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64-static -L /usr/aarch64-linux-gnu"

cross 工具 — 基于 Docker 的交叉编译

cross 工具使用预配置的 Docker 镜像提供“零设置”交叉编译体验:

# 安装 cross (来自 crates.io — 稳定版)
cargo install cross
# 或者来自 git 以获得最新特性 (不太稳定):
# cargo install cross --git https://github.com/cross-rs/cross

# 交叉编译 — 无需手动设置工具链!
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --target x86_64-unknown-linux-musl
cross build --release --target armv7-unknown-linux-gnueabihf

# 交叉测试 — Docker 镜像中已包含 QEMU
cross test --target aarch64-unknown-linux-gnu

工作原理:cross 替换了 cargo 命令,在预装了正确交叉编译工具链的 Docker 容器内运行构建。你的源码被挂载到容器中,输出结果则存放在正常的 target/ 目录下。

通过 Cross.toml 自定义 Docker 镜像:

# Cross.toml
[target.aarch64-unknown-linux-gnu]
# 使用带有额外系统库的自定义 Docker 镜像
image = "my-registry/cross-aarch64:latest"

# 预安装系统包
pre-build = [
    "dpkg --add-architecture arm64",
    "apt-get update && apt-get install -y libpci-dev:arm64"
]

[target.aarch64-unknown-linux-gnu.env]
# 将环境变量传递到容器中
passthrough = ["CI", "GITHUB_TOKEN"]

cross 需要 Docker (或 Podman),但它消除了手动安装交叉编译器、sysroot 和 QEMU 的麻烦。它是 CI 的推荐方案。

使用 Zig 作为交叉编译链接器

Zig 在单个约 40 MB 的下载包中捆绑了 C 编译器和针对约 40 个目标的交叉编译 sysroot。这使其成为 Rust 极佳的交叉链接器:

# 安装 Zig (单一二进制文件,无需包管理器)
# 从 https://ziglang.org/download/ 下载
# 或通过包管理器安装:
sudo snap install zig --classic --beta  # Ubuntu
brew install zig                          # macOS

# 安装 cargo-zigbuild
cargo install cargo-zigbuild

为什么要用 Zig? 关键优势在于 glibc 版本定向 (glibc version targeting)。Zig 允许你指定链接的具体 glibc 版本,确保你的二进制文件能在旧版 Linux 发行版上运行:

# 为 glibc 2.17 构建 (兼容 CentOS 7 / RHEL 7)
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17

# 为 glibc 2.28 构建 aarch64 (Ubuntu 18.04+)
cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.28

# 为 musl 构建 (完全静态)
cargo zigbuild --release --target x86_64-unknown-linux-musl

.2.17 后缀是 Zig 的扩展 —— 它告诉 Zig 链接器使用 glibc 2.17 的符号版本,从而使生成的二进制文件可以在 CentOS 7 及更高版本上运行。无需 Docker,无需 sysroot 管理,也无需安装交叉编译器。

对比:cross vs cargo-zigbuild vs 手动配置:

特性手动配置crosscargo-zigbuild
设置开销高 (需为每个目标安装工具链)低 (需要 Docker)低 (单一二进制)
是否需要 Docker否是否
glibc 版本定向否 (使用宿主机 glibc)否 (使用容器 glibc)是 (精确版本)
测试执行需要 QEMU已包含需要 QEMU
macOS → Linux困难简单简单
Linux → macOS非常困难不支持受限
二进制体积开销无无无

CI 流水线:GitHub Actions 矩阵

一个旨在构建多个目标的生产级 CI 工作流:

# .github/workflows/cross-build.yml
name: Cross-Platform Build

on: [push, pull_request]

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
            name: linux-x86_64
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            name: linux-x86_64-static
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            name: linux-aarch64
            use_cross: true
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            name: windows-x86_64

    runs-on: ${{ matrix.os }}
    name: Build (${{ matrix.name }})

    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Install musl tools
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: sudo apt-get install -y musl-tools

      - name: Install cross
        if: matrix.use_cross
        run: cargo install cross

      - name: Build (native)
        if: "!matrix.use_cross"
        run: cargo build --release --target ${{ matrix.target }}

      - name: Build (cross)
        if: matrix.use_cross
        run: cross build --release --target ${{ matrix.target }}

      - name: Run tests
        if: "!matrix.use_cross"
        run: cargo test --target ${{ matrix.target }}

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: diag_tool-${{ matrix.name }}
          path: target/${{ matrix.target }}/release/diag_tool*

应用:多架构服务器构建

目前的二进制程序还没有设置交叉编译。对于一个部署在各种服务器集群中的硬件诊断工具,建议添加以下结构:

my_workspace/
├── .cargo/
│   └── config.toml          ← 每个目标的链接器配置
├── Cross.toml                ← cross 工具配置
└── .github/workflows/
    └── cross-build.yml       ← 针对 3 个目标的 CI 矩阵

建议的 .cargo/config.toml:

# 此项目的 .cargo/config.toml

# 发布配置优化(已在 Cargo.toml 中,此处仅供参考)
# [profile.release]
# lto = true
# codegen-units = 1
# panic = "abort"
# strip = true

# 针对 ARM 服务器(Graviton, Ampere, Grace)的 aarch64
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

# 用于便携式静态二进制文件的 musl
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"

建议的构建目标:

目标使用场景部署至
x86_64-unknown-linux-gnu默认原生构建标准 x86 服务器
x86_64-unknown-linux-musl静态二进制,适用于任何发行版容器、极简宿主机
aarch64-unknown-linux-gnuARM 服务器Graviton, Ampere, Grace

关键洞察:工作区根目录 Cargo.toml 中的 [profile.release] 已经设置了 lto = true、codegen-units = 1、panic = "abort" 和 strip = true —— 这是交叉编译部署二进制文件的理想配置(见 发布配置 获取完整的收益表)。结合 musl,这将产生一个约 10 MB 的单一静态二进制文件,且无运行时依赖。

交叉编译排错

现象原因修复方法
linker 'aarch64-linux-gnu-gcc' not found缺少交叉链接器工具链sudo apt install gcc-aarch64-linux-gnu
cannot find -lssl (musl 目标)系统 OpenSSL 是 glibc 链接的使用 vendored 特性:openssl = { version = "0.10", features = ["vendored"] }
build.rs 运行了错误的二进制文件build.rs 在 HOST 而非目标机运行在 build.rs 中检查 CARGO_CFG_TARGET_OS,而非 cfg!(target_os)
本地测试通过,在 cross 中失败Docker 镜像缺少测试固件通过 Cross.toml 挂载测试数据:[build.env] volumes = ["./TestArea:/TestArea"]
undefined reference to __cxa_thread_atexit_impl目标机 glibc 版本过旧使用 cargo-zigbuild 并显式指定 glibc 版本:--target x86_64-unknown-linux-gnu.2.17
二进制文件在 ARM 上段错误 (segfault)编译成了错误的 ARM 变体验证目标三元组是否匹配硬件:64 位 ARM 应为 aarch64-unknown-linux-gnu
运行时提示 GLIBC_2.XX not found构建机器的 glibc 版本更高使用 musl 进行静态构建,或使用 cargo-zigbuild 固定 glibc 版本

交叉编译决策树

flowchart TD
    START["需要交叉编译?"] --> STATIC{"是否需要静态二进制文件?"}
    
    STATIC -->|是| MUSL["musl 目标\n--target x86_64-unknown-linux-musl"]
    STATIC -->|否| GLIBC{"是否需要旧版 glibc?"}
    
    GLIBC -->|是| ZIG["cargo-zigbuild\n--target x86_64-unknown-linux-gnu.2.17"]
    GLIBC -->|否| ARCH{"目标架构?"}
    
    ARCH -->|"相同架构"| NATIVE["原生工具链\nrustup target add + 链接器"]
    ARCH -->|"ARM/其他"| DOCKER{"是否有 Docker?"}
    
    DOCKER -->|是| CROSS["cross build\n基于 Docker, 零设置"]
    DOCKER -->|否| MANUAL["手动设置 sysroot\napt install gcc-aarch64-linux-gnu"]
    
    style MUSL fill:#91e5a3,color:#000
    style ZIG fill:#91e5a3,color:#000
    style CROSS fill:#91e5a3,color:#000
    style NATIVE fill:#e3f2fd,color:#000
    style MANUAL fill:#ffd43b,color:#000

🏋️ 练习

🟢 练习 1:静态 musl 二进制文件

为 x86_64-unknown-linux-musl 构建任意 Rust 二进制程序。使用 file 和 ldd 验证其是否为静态链接。

答案
rustup target add x86_64-unknown-linux-musl
cargo new hello-static && cd hello-static
cargo build --release --target x86_64-unknown-linux-musl

# 验证
file target/x86_64-unknown-linux-musl/release/hello-static
# 输出:... statically linked ...

ldd target/x86_64-unknown-linux-musl/release/hello-static
# 输出:not a dynamic executable

🟡 练习 2:GitHub Actions 交叉构建矩阵

编写一个 GitHub Actions 工作流,针对三个目标平台构建 Rust 项目:x86_64-unknown-linux-gnu、x86_64-unknown-linux-musl 和 aarch64-unknown-linux-gnu。使用矩阵 (matrix) 策略。

答案
name: Cross-build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-gnu
          - x86_64-unknown-linux-musl
          - aarch64-unknown-linux-gnu
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - name: Install cross
        run: cargo install cross --locked
      - name: Build
        run: cross build --release --target ${{ matrix.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: binary-${{ matrix.target }}
          path: target/${{ matrix.target }}/release/my-binary

关键收获

  • Rust 的 rustc 本身就是一个交叉编译器 —— 你只需要匹配正确的目标平台和链接器。
  • musl 开箱即用且无运行时依赖,非常适合构建单一静态二进制文件 —— 是容器化部署的理想。
  • cargo-zigbuild 完美解决了针对企业级 Linux 目标的“glibc 版本”问题。
  • cross 是处理 ARM 和其他小众目标的最简路径 —— Docker 负责处理 sysroot。
  • 部署前始终使用 file 和 ldd 进行测试,验证二进制文件是否与目标部署环境匹配。

English Original

基准测试 — 衡量真正重要的指标 🟡

你将学到:

  • 为什么使用 Instant::now() 进行简单计时会产生不可靠的结果
  • 使用 Criterion.rs 进行统计型基准测试,以及更轻量的替代方案 Divan
  • 使用 perf、火焰图 (flamegraphs) 和 PGO (配置文件引导优化) 分析性能热点
  • 在 CI 中设置持续基准测试,自动捕获性能退化

相关章节: 发布配置 — 找到热动点后,优化二进制文件 · CI/CD 流水线 — 流水线中的基准测试任务 · 代码覆盖率 — 覆盖率告诉你测试了什么,基准测试告诉你什么运行得快

“在大约 97% 的时间里,我们应该忘记微小的效率提升:过早的优化是万恶之源。然而,我们不应在剩下的 3% 的关键机会中失之交臂。” —— Donald Knuth

难点不在于 编写 基准测试,而在于编写能产生 有意义、可复现、可操作 的数据的基准测试。本章涵盖了能让你从“它看起来很快”升级到“我们有统计证据表明 PR #347 使解析吞吐量退化了 4.2%”的工具和技巧。

为什么不用 std::time::Instant?

常见的做法及其弊端:

// ❌ 简陋的基准测试 — 结果不可靠
use std::time::Instant;

fn main() {
    let start = Instant::now();
    let result = parse_device_query_output(&sample_data);
    let elapsed = start.elapsed();
    println!("解析耗时 {:?}", elapsed);
    // 问题 1:编译器可能会优化掉 `result`(死代码消除)
    // 问题 2:样本单一 — 无统计学意义
    // 问题 3:CPU 频率缩放、热节流、其他进程干扰
    // 问题 4:未控制冷缓存与热缓存
}

手动计时的问题:

  1. 死代码消除 (Dead code elimination) — 如果结果未被使用,编译器可能会完全跳过计算。
  2. 缺乏预热 (No warm-up) — 第一次运行包含缓存未命中、JIT 效应(虽然在 Rust 中不适用,但操作系统页错误适用)和惰性初始化。
  3. 缺乏统计分析 — 单次测量无法告诉你方差、异常值或置信区间。
  4. 无法检测退化 — 你无法与之前的运行结果进行对比。

Criterion.rs — 统计基准测试

Criterion.rs 是 Rust 微基准测试的事实标准。它使用统计方法产生可靠的测量结果,并自动检测性能退化。

准备工作:

# Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports", "cargo_bench_support"] }

[[bench]]
name = "parsing_bench"
harness = false  # 使用 Criterion 的运行器,而非内置的测试运行器

一个完整的基准测试示例:

#![allow(unused)]
fn main() {
// benches/parsing_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};

/// 解析后的 GPU 信息数据类型
#[derive(Debug, Clone)]
struct GpuInfo {
    index: u32,
    name: String,
    temp_c: u32,
    power_w: f64,
}

/// 待测函数 — 模拟解析设备查询的 CSV 输出
fn parse_gpu_csv(input: &str) -> Vec<GpuInfo> {
    input
        .lines()
        .filter(|line| !line.starts_with('#'))
        .filter_map(|line| {
            let fields: Vec<&str> = line.split(", ").collect();
            if fields.len() >= 4 {
                Some(GpuInfo {
                    index: fields[0].parse().ok()?,
                    name: fields[1].to_string(),
                    temp_c: fields[2].parse().ok()?,
                    power_w: fields[3].parse().ok()?,
                })
            } else {
                None
            }
        })
        .collect()
}

fn bench_parse_gpu_csv(c: &mut Criterion) {
    // 具有代表性的测试数据
    let small_input = "0, Acme Accel-V1-80GB, 32, 65.5\n\
                       1, Acme Accel-V1-80GB, 34, 67.2\n";

    let large_input = (0..64)
        .map(|i| format!("{i}, Acme Accel-X1-80GB, {}, {:.1}\n", 30 + i % 20, 60.0 + i as f64))
        .collect::<String>();

    c.bench_function("parse_2_gpus", |b| {
        b.iter(|| parse_gpu_csv(black_box(small_input)))
    });

    c.bench_function("parse_64_gpus", |b| {
        b.iter(|| parse_gpu_csv(black_box(&large_input)))
    });
}

criterion_group!(benches, bench_parse_gpu_csv);
criterion_main!(benches);
}

运行并阅读结果:

# 运行所有基准测试
cargo bench

# 运行特定的基准测试(按名称过滤)
cargo bench -- parse_64

# 输出示例:
# parse_2_gpus        time:   [1.2345 µs  1.2456 µs  1.2578 µs]
#                      ▲            ▲           ▲
#                      │          置信区间
#                    下限 95%      中位数      上限 95%
#
# parse_64_gpus       time:   [38.123 µs  38.456 µs  38.812 µs]
#                     change: [-1.2345% -0.5678% +0.1234%] (p = 0.12 > 0.05)
#                     未检测到性能变化。

black_box() 的作用:它是一个编译器提示,用于防止死代码消除和过度激进的常量折叠。编译器无法看穿 black_box,因此它必须实际计算结果。

参数化基准测试与基准测试组

对比多种实现或输入规模:

#![allow(unused)]
fn main() {
// benches/comparison_bench.rs
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId, Throughput};

fn bench_parsing_strategies(c: &mut Criterion) {
    let mut group = c.benchmark_group("csv_parsing");

    // 在不同的输入规模上进行测试
    for num_gpus in [1, 8, 32, 64, 128] {
        let input = generate_gpu_csv(num_gpus);

        // 设置吞吐量,以报告每秒处理的字节数
        group.throughput(Throughput::Bytes(input.len() as u64));

        group.bench_with_input(
            BenchmarkId::new("split_based", num_gpus),
            &input,
            |b, input| b.iter(|| parse_split(input)),
        );

        group.bench_with_input(
            BenchmarkId::new("regex_based", num_gpus),
            &input,
            |b, input| b.iter(|| parse_regex(input)),
        );

        group.bench_with_input(
            BenchmarkId::new("nom_based", num_gpus),
            &input,
            |b, input| b.iter(|| parse_nom(input)),
        );
    }
    group.finish();
}

criterion_group!(benches, bench_parsing_strategies);
criterion_main!(benches);
}

报告:Criterion 会在 target/criterion/report/index.html 生成 HTML 报告,包含小提琴图、对比图和退化分析 —— 可直接在浏览器中打开。

Divan — 一个更轻量的替代方案

Divan 是一个新的基准测试框架,它使用属性宏代替了 Criterion 的宏 DSL:

# Cargo.toml
[dev-dependencies]
divan = "0.1"

[[bench]]
name = "parsing_bench"
harness = false
// benches/parsing_bench.rs
use divan::black_box;

const SMALL_INPUT: &str = "0, Acme Accel-V1-80GB, 32, 65.5\n\
                          1, Acme Accel-V1-80GB, 34, 67.2\n";

fn generate_gpu_csv(n: usize) -> String {
    (0..n)
        .map(|i| format!("{i}, Acme Accel-X1-80GB, {}, {:.1}\n", 30 + i % 20, 60.0 + i as f64))
        .collect()
}

fn main() {
    divan::main();
}

#[divan::bench]
fn parse_2_gpus() -> Vec<GpuInfo> {
    parse_gpu_csv(black_box(SMALL_INPUT))
}

#[divan::bench(args = [1, 8, 32, 64, 128])]
fn parse_n_gpus(n: usize) -> Vec<GpuInfo> {
    let input = generate_gpu_csv(n);
    parse_gpu_csv(black_box(&input))
}

// Divan 输出的是简洁的表格:
// ╰─ parse_2_gpus   最快      │ 最慢      │ 中位数    │ 平均值    │ 样本数  │ 迭代次数
//                   1.234 µs │ 1.567 µs │ 1.345 µs │ 1.350 µs │ 100     │ 1600

何时选择 Divan 而非 Criterion:

  • 更简单的 API(属性宏,更少的样板代码)
  • 编译速度更快(依赖更少)
  • 适合开发过程中的快速性能检查

何时选择 Criterion:

  • 跨运行的统计退化检测
  • 带有图表的 HTML 报告
  • 成熟的生态系统,更多的 CI 集成

使用 perf 和火焰图进行性能分析

基准测试告诉你 有多快 —— 性能分析 (profiling) 告诉你 时间都花在哪了。

# 第 1 步:构建时包含调试信息(发布速度,调试符号)
cargo build --release
# 确保调试信息可用:
# [profile.release]
# debug = true          # 临时添加此项进行分析

# 第 2 步:使用 perf 记录
perf record --call-graph=dwarf ./target/release/diag_tool --run-diagnostics

# 第 3 步:生成火焰图
# 安装:cargo install flamegraph
# 安装:cargo install addr2line --features=bin(可选,加速 cargo-flamegraph)
cargo flamegraph --root -- --run-diagnostics
# 生成并打开一个交互式的 SVG 火焰图

# 另一种方式:使用 perf + inferno
perf script | inferno-collapse-perf | inferno-flamegraph > flamegraph.svg

阅读火焰图:

  • 宽度 (Width) = 在该函数中花费的时间(越宽 = 越慢)
  • 高度 (Height) = 调用栈深度(越高 ≠ 越慢,只是更深)
  • 底部 (Bottom) = 入口点,顶部 (Top) = 执行实际工作的叶子函数
  • 寻找顶部宽广的“平原” —— 那些就是你的性能热点。

配置文件引导优化 (PGO):

# 第 1 步:构建带有插桩的可执行文件
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release

# 第 2 步:运行代表性的负载
./target/release/diag_tool --run-full   # 生成性能分析数据

# 第 3 步:合并性能分析数据
# 使用与 rustc 的 LLVM 版本匹配的 llvm-profdata:
# $(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata
# 或者如果你安装了 llvm-tools:rustup component add llvm-tools
llvm-profdata merge -o /tmp/pgo-data/merged.profdata /tmp/pgo-data/

# 第 4 步:根据分析反馈重新构建
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/merged.profdata" cargo build --release
# 典型的提升:计算密集型代码(解析、加密、代码生成)提升 5-20%。
# I/O 密集型或系统调用频繁的代码提升较小,因为 CPU 大部分时间在等待。

提示:在花时间研究 PGO 之前,确保你的 发布配置 已经启用了 LTO —— 这通常只需更少的努力就能获得更大的收益。

hyperfine — 快速端到端计时

hyperfine 对整个命令进行基准测试,而不是单个函数。它非常适合衡量二进制文件的整体性能:

# 安装
cargo install hyperfine
# 或者通过包管理器安装 (Ubuntu 23.04+): sudo apt install hyperfine

# 基础基准测试
hyperfine './target/release/diag_tool --run-diagnostics'

# 比较两种实现
hyperfine './target/release/diag_tool_v1 --run-diagnostics' \
          './target/release/diag_tool_v2 --run-diagnostics'

# 预热运行 + 最小迭代次数
hyperfine --warmup 3 --min-runs 10 './target/release/diag_tool --run-all'

# 将结果导出为 JSON 以便在 CI 中对比
hyperfine --export-json bench.json './target/release/diag_tool --run-all'

何时使用 hyperfine 而非 Criterion:

  • hyperfine:整体二进制文件计时、重构前后的对比、I/O 密集型负载
  • Criterion:单个函数的微基准测试、统计型退化检测

在 CI 中进行持续基准测试

在性能退化版本发布前拦截它们:

# .github/workflows/bench.yml
name: Benchmarks

on:
  pull_request:
    paths: ['**/*.rs', 'Cargo.toml', 'Cargo.lock']

jobs:
  benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - name: Run benchmarks
        # 需要 criterion = { features = ["cargo_bench_support"] } 以支持 --output-format
        run: cargo bench -- --output-format bencher | tee bench_output.txt

      - name: Store benchmark result
        uses: benchmark-action/github-action-benchmark@v1
        with:
          tool: 'cargo'
          output-file-path: bench_output.txt
          github-token: ${{ secrets.GITHUB_TOKEN }}
          auto-push: true
          alert-threshold: '120%'    # 如果慢了 20% 则报警
          comment-on-alert: true
          fail-on-alert: true        # 检测到退化时阻止 PR 合并

CI 关键考虑点:

  • 使用 专用基准测试运行器(而非共享的 CI 节点)以获得一致的结果。
  • 如果使用云端 CI,请将运行器固定到特定的机器类型。
  • 存储历史数据以检测渐进式的退化。
  • 根据负载的容忍度设置阈值(热路径 5%,其他 20%)。

应用:解析性能

该项目有多个对性能敏感的解析路径,它们将从基准测试中受益:

解析热点Crate为什么重要
加速器查询 CSV/XML 输出device_diag每个 GPU 都会调用,每次运行最多 8 次
传感器事件解析event_log繁忙服务器上有数千条记录
PCIe 拓扑 JSONtopology_lib复杂的嵌套结构,经过 Golden-file 验证
报告 JSON 序列化diag_framework最终报告输出,对体积敏感
配置 JSON 加载config_loader启动延迟

建议的第一个基准测试 —— 拓扑结构解析器,它已经有了 Golden-file 测试数据:

#![allow(unused)]
fn main() {
// topology_lib/benches/parse_bench.rs (建议)
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
use std::fs;

fn bench_topology_parse(c: &mut Criterion) {
    let mut group = c.benchmark_group("topology_parse");

    for golden_file in ["S2001", "S1015", "S1035", "S1080"] {
        let path = format!("tests/test_data/{golden_file}.json");
        let data = fs::read_to_string(&path).expect("golden file not found");
        group.throughput(Throughput::Bytes(data.len() as u64));

        group.bench_function(golden_file, |b| {
            b.iter(|| {
                topology_lib::TopologyProfile::from_json_str(
                    criterion::black_box(&data)
                )
            });
        });
    }
    group.finish();
}

criterion_group!(benches, bench_topology_parse);
criterion_main!(benches);
}

亲自尝试

  1. 编写一个 Criterion 基准测试:挑出代码库中任意一个解析函数。创建一个 benches/ 目录,设置一个 Criterion 基准测试来衡量每秒处理的字节数。运行 cargo bench 并查看 HTML 报告。

  2. 生成一份火焰图:在 [profile.release] 中设置 debug = true 构建你的项目,然后运行 cargo flamegraph -- <你的参数>。找出火焰图顶部最宽的三个栈 —— 它们就是你的热点。

  3. 与 hyperfine 对比:安装 hyperfine 并在不同标志下测量二进制文件的整体执行时间。将其与 Criterion 测量的各函数时间进行对比。Criterion 没看到的时间都花在哪了?(提示:I/O、系统调用、进程启动)。

基准测试工具选择

flowchart TD
    START["想要衡量性能?"] --> WHAT{"什么层面?"}

    WHAT -->|"单个函数"| CRITERION["Criterion.rs\n统计型,退化检测"]
    WHAT -->|"快速核对函数"| DIVAN["Divan\n轻量级,属性宏"]
    WHAT -->|"整个二进制文件"| HYPERFINE["hyperfine\n端到端,墙钟时间"]
    WHAT -->|"查找热点"| PERF["perf + 火焰图\nCPU 采样分析器"]

    CRITERION --> CI_BENCH["持续基准测试\n在 GitHub Actions 中"]
    PERF --> OPTIMIZE["配置文件引导\n优化 (PGO)"]

    style CRITERION fill:#91e5a3,color:#000
    style DIVAN fill:#91e5a3,color:#000
    style HYPERFINE fill:#e3f2fd,color:#000
    style PERF fill:#ffd43b,color:#000
    style CI_BENCH fill:#e3f2fd,color:#000
    style OPTIMIZE fill:#ffd43b,color:#000

🏋️ 练习

🟢 练习 1:第一个 Criterion 基准测试

创建一个 crate,包含一个对拥有 10,000 个随机元素的 Vec<u64> 进行排序的函数。编写一个 Criterion 基准测试。尝试切换到 .sort_unstable() 并观察 HTML 报告中性能的差异。

答案
# Cargo.toml
[[bench]]
name = "sort_bench"
harness = false

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
rand = "0.8"
#![allow(unused)]
fn main() {
// benches/sort_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rand::Rng;

fn generate_data(n: usize) -> Vec<u64> {
    let mut rng = rand::thread_rng();
    (0..n).map(|_| rng.gen()).collect()
}

fn bench_sort(c: &mut Criterion) {
    let mut group = c.benchmark_group("sort-10k");

    group.bench_function("stable", |b| {
        b.iter_batched(
            || generate_data(10_000),
            |mut data| { data.sort(); black_box(&data); },
            criterion::BatchSize::SmallInput,
        )
    });

    group.bench_function("unstable", |b| {
        b.iter_batched(
            || generate_data(10_000),
            |mut data| { data.sort_unstable(); black_box(&data); },
            criterion::BatchSize::SmallInput,
        )
    });

    group.finish();
}

criterion_group!(benches, bench_sort);
criterion_main!(benches);
}
cargo bench
open target/criterion/sort-10k/report/index.html

🟡 练习 2:火焰图中的热点

在 [profile.release] 中设置 debug = true 构建项目,生成一份火焰图。找出最宽的前三个栈。

答案
# Cargo.toml
[profile.release]
debug = true  # 保留火焰图所需的符号
cargo install flamegraph
cargo flamegraph --release -- <你的参数>
# 在浏览器中打开 flamegraph.svg
# 顶部最宽的栈就是你的性能热点

关键收获

  • 绝不要用 Instant::now() 进行微基准测试 —— 使用 Criterion.rs 获得统计学上的严谨性和退化检测。
  • black_box() 防止编译器将你的基准测试目标通过内联或折叠优化掉。
  • hyperfine 衡量整体二进制文件的运行时间;Criterion 衡量单个函数 —— 两者配合使用。
  • 火焰图展示时间花在 哪 了;基准测试展示花了 多少 时间。
  • 在 CI 中设置持续基准测试,可在性能退化上线前及时捕获。

English Original

代码覆盖率 — 发现测试遗漏 🟢

你将学到:

  • 使用 cargo-llvm-cov 进行基于源码的覆盖率分析(最准确的 Rust 覆盖率工具)
  • 使用 cargo-tarpaulin 和 Mozilla 的 grcov 进行快速覆盖率检查
  • 在 CI 中使用 Codecov 和 Coveralls 设置覆盖率门禁 (Coverage Gates)
  • 优先处理高风险盲点的覆盖率导向型测试策略

相关章节: Miri 与 Sanitizer — 覆盖率发现未测试的代码,Miri 发现已测试代码中的 UB · 基准测试 — 覆盖率展示 测试了什么,基准测试展示 什么运行得快 · CI/CD 流水线 — 流水线中的覆盖率门禁

代码覆盖率衡量你的测试实际执行了哪些行、分支或函数。它不能证明代码的正确性(覆盖到的行仍可能存在 bug),但它能可靠地揭示 盲点 —— 即没有任何测试覆盖到的代码路径。

本项目在多个 crate 中拥有 1,006 个测试,投入了大量的测试精力。覆盖率分析可以回答:“这些投入是否触及了真正关键的代码?”

使用 llvm-cov 进行基于源码的覆盖率分析

Rust 使用 LLVM,它提供了基于源码的插桩覆盖率 —— 这是目前最准确的覆盖率分析方法。推荐使用的工具是 cargo-llvm-cov:

# 安装
cargo install cargo-llvm-cov

# 或者通过 rustup 安装组件(获取原始 llvm 工具)
rustup component add llvm-tools-preview

基础用法:

# 运行测试并显示每个文件的覆盖率摘要
cargo llvm-cov

# 生成 HTML 报告(可浏览器查阅,带有逐行高亮)
cargo llvm-cov --html
# 输出路径:target/llvm-cov/html/index.html

# 生成 LCOV 格式(用于 CI 集成)
cargo llvm-cov --lcov --output-path lcov.info

# 工作区全量覆盖率(所有 crate)
cargo llvm-cov --workspace

# 仅包含特定的包
cargo llvm-cov --package accel_diag --package topology_lib

# 覆盖率包含文档测试 (doc tests)
cargo llvm-cov --doctests

阅读 HTML 报告:

target/llvm-cov/html/index.html
├── 文件名                │ 函数     │ 行       │ 分支     │ 区域
├─ accel_diag/src/lib.rs │  78.5%  │ 82.3%   │ 61.2%   │  74.1%
├─ sel_mgr/src/parse.rs  │  95.2%  │ 96.8%   │ 88.0%   │  93.5%
├─ topology_lib/src/..   │  91.0%  │ 93.4%   │ 79.5%   │  89.2%
└─ ...

绿色 = 已覆盖    红色 = 未覆盖    黄色 = 部分覆盖(分支)

覆盖率类型说明:

类型衡量内容意义
行覆盖率 (Line)执行了哪些源码行基础的“这段代码被跑到了吗?”
分支覆盖率 (Branch)执行了哪些 if/match 分支捕捉未测试的条件判断
函数覆盖率 (Function)调用了哪些函数发现死代码
区域覆盖率 (Region)命中了哪些代码区域(子表达式)颗粒度最细

cargo-tarpaulin — 快捷路径

cargo-tarpaulin 是一个专门针对 Linux 的覆盖率工具,它的设置更简单(无需安装 LLVM 组件):

# 安装
cargo install cargo-tarpaulin

# 基础覆盖率报告
cargo tarpaulin

# HTML 输出
cargo tarpaulin --out Html

# 使用特定选项
cargo tarpaulin \
    --workspace \
    --timeout 120 \
    --out Xml Html \
    --output-dir coverage/ \
    --exclude-files "*/tests/*" "*/benches/*" \
    --ignore-panics

# 跳过特定的 crate
cargo tarpaulin --workspace --exclude diag_tool  # 排除二进制 crate

tarpaulin 与 llvm-cov 对比:

特性cargo-llvm-covcargo-tarpaulin
准确性基于源码 (最准确)基于 ptrace (偶尔会有误报)
平台任何 (基于 llvm)仅限 Linux
分支覆盖率支持有限支持
文档测试支持不支持
安装设置需要 llvm-tools-preview自包含
速度较快 (编译期插桩)较慢 (ptrace 开销)
稳定性非常稳定偶尔会出现伪阳性

建议:追求准确性时使用 cargo-llvm-cov。如果你只需要在 Linux 上进行快速检查且不想安装 LLVM 工具,可以使用 cargo-tarpaulin。

grcov — Mozilla 的覆盖率工具

grcov 是 Mozilla 开发的覆盖率聚合器。它消费原始的 LLVM 剖析数据并生成多种格式的报告:

# 安装
cargo install grcov

# 第 1 步:构建带有插桩信息的二进制文件
export RUSTFLAGS="-Cinstrument-coverage"
export LLVM_PROFILE_FILE="target/coverage/%p-%m.profraw"
cargo build --tests

# 第 2 步:运行测试(生成 .profraw 文件)
cargo test

# 第 3 步:使用 grcov 进行聚合
grcov target/coverage/ \
    --binary-path target/debug/ \
    --source-dir . \
    --output-types html,lcov \
    --output-path target/coverage/report \
    --branch \
    --ignore-not-existing \
    --ignore "*/tests/*" \
    --ignore "*/.cargo/*"

# 第 4 步:查看报告
open target/coverage/report/html/index.html

何时使用 grcov:当你需要将 多次测试运行的覆盖率合并(例如:单元测试 + 集成测试 + 模糊测试)到单一报告中时,它最为有用。

CI 中的覆盖率:Codecov 与 Coveralls

将覆盖率数据上传至跟踪服务,以便查看历史趋势和 PR 批注:

# .github/workflows/coverage.yml
name: Code Coverage

on: [push, pull_request]

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview

      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@cargo-llvm-cov

      - name: Generate coverage
        run: cargo llvm-cov --workspace --lcov --output-path lcov.info

      - name: Upload to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: lcov.info
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: true

      # 可选:强制要求最低覆盖率
      - name: Check coverage threshold
        run: |
          cargo llvm-cov --workspace --fail-under-lines 80
          # 如果行覆盖率低于 80%,则构建失败

覆盖率门禁 —— 通过读取 JSON 输出,对每个 crate 强制执行最低标准:

# 获取每个 crate 的覆盖率(JSON 格式)
cargo llvm-cov --workspace --json | jq '.data[0].totals.lines.percent'

# 低于阈值则报错
cargo llvm-cov --workspace --fail-under-lines 80
cargo llvm-cov --workspace --fail-under-functions 70
cargo llvm-cov --workspace --fail-under-regions 60

覆盖率导向型测试策略

如果没有策略,覆盖率数值本身毫无意义。以下是如何有效利用覆盖率数据的方法:

第 1 步:按风险进行分类 (Triage by risk)

高覆盖率,高风险     → ✅ 优 — 保持现状
高覆盖率,低风险     → 🔄 可能过度测试 — 如果速度太慢可精简
低覆盖率,高风险     → 🔴 立即编写测试 — 这是 bug 处理的温床
低覆盖率,低风险     → 🟡 跟踪但不必恐慌

第 2 步:关注分支覆盖率,而非行覆盖率

#![allow(unused)]
fn main() {
// 100% 的行覆盖率,但只有 50% 的分支覆盖率 —— 仍然充满风险!
pub fn classify_temperature(temp_c: i32) -> ThermalState {
    if temp_c > 105 {       // ← 使用 temp=110 测试过 → Critical
        ThermalState::Critical
    } else if temp_c > 85 { // ← 使用 temp=90 测试过 → Warning
        ThermalState::Warning
    } else if temp_c < -10 { // ← 从未测试过 → 遗漏了传感器错误情况
        ThermalState::SensorError
    } else {
        ThermalState::Normal  // ← 使用 temp=25 测试过 → Normal
    }
}
}

第 3 步:排除噪音

# 排除测试代码(它们总是“被覆盖”的)
cargo llvm-cov --workspace --ignore-filename-regex 'tests?\.rs$|benches/'

# 排除生成的代码
cargo llvm-cov --workspace --ignore-filename-regex 'target/'

在代码中标记无法测试的部分:

#![allow(unused)]
fn main() {
// 覆盖率工具可以识别这种模式
#[cfg(not(tarpaulin_include))]  // 针对 tarpaulin
fn unreachable_hardware_path() {
    // 该路径需要实际的 GPU 硬件才能触发
}

// 对于 llvm-cov,建议采用更有针对性的方法:
// 接受某些路径需要集成/硬件测试而非单元测试。
// 将它们整理进覆盖率例外清单中。
}

补充测试工具

proptest — 基于属性的测试 (Property-Based Testing) 能发现手动编写测试时遗漏的边界情况:

[dev-dependencies]
proptest = "1"
#![allow(unused)]
fn main() {
use proptest::prelude::*;

proptest! {
    #[test]
    fn parse_never_panics(input in "\\PC*") {
        // proptest 生成数千个随机字符串
        // 如果 parse_gpu_csv 在任何输入上崩溃 (panic),测试就会失败,
        // 并且 proptest 会为你最小化失败用例。
        let _ = parse_gpu_csv(&input);
    }

    #[test]
    fn temperature_roundtrip(raw in 0u16..4096) {
        let temp = Temperature::from_raw(raw);
        let md = temp.millidegrees_c();
        // 属性:毫摄氏度应当总是能从原始值推导出来
        assert_eq!(md, (raw as i32) * 625 / 10);
    }
}
}

insta — 快照测试 (Snapshot Testing) 适用于大型结构化输出(JSON、文本报告):

[dev-dependencies]
insta = { version = "1", features = ["json"] }
#![allow(unused)]
fn main() {
#[test]
fn test_der_report_format() {
    let report = generate_der_report(&test_results);
    // 第一次运行:创建快照文件。后续运行:与快照进行对比。
    // 运行 `cargo insta review` 可以交互式地接受变更。
    insta::assert_json_snapshot!(report);
}
}

何时添加 proptest/insta:如果你的单元测试全都是“常用路径”示例,proptest 会帮你找出遗漏的边界情况。如果你正在测试大型输出格式(JSON 报告、DER 记录),insta 快照比手动编写断言更快且更易维护。

应用:1,000+ 测试的覆盖率蓝图

本项目有 1,000 多个测试,但没有覆盖率跟踪。添加覆盖率分析可以揭示测试投入的分布情况。未覆盖的路径是进行 Miri 与 sanitizer 验证的首选对象:

建议的覆盖率配置:

# 工作区快速覆盖率分析(建议的 CI 命令)
cargo llvm-cov --workspace \
    --ignore-filename-regex 'tests?\.rs$' \
    --fail-under-lines 75 \
    --html

# 针对各个 crate 的覆盖率,进行定向提升
for crate in accel_diag event_log topology_lib network_diag compute_diag fan_diag; do
    echo "=== $crate ==="
    cargo llvm-cov --package "$crate" --json 2>/dev/null | \
        jq -r '.data[0].totals | "Lines: \(.lines.percent | round)%  Branches: \(.branches.percent | round)%"'
done

预期覆盖率较高的 crate(基于测试密度):

  • topology_lib — 拥有 922 行的 Golden-file 测试套件
  • event_log — 拥有 create_test_record() 辅助函数的注册中心
  • cable_diag — 采用了 make_test_event() / make_test_context() 模式的代码

预期覆盖率缺口(基于代码检查):

  • IPMI 通信路径中的错误处理分支
  • GPU 硬件特定的分支(需要实际 GPU 环境)
  • dmesg 解析边界情况(依赖特定平台的输出)

覆盖率的 80/20 法则:从 0% 提升到 80% 覆盖率是比较直接的。从 80% 提升到 95% 则需要日益复杂的测试场景。从 95% 提升到 100% 通常需要大量 #[cfg(not(...))] 排除项,往往得不偿失。在实践中,建议将 80% 行覆盖率和 70% 分支覆盖率 作为底线。

覆盖率排错

现象原因修复方法
llvm-cov 对所有文件显示 0%未启用插桩确保运行的是 cargo llvm-cov,而不是分开运行 cargo test 和 llvm-cov
覆盖率将 unreachable!() 计为未覆盖编译后的代码中确实存在这些分支使用 #[cfg(not(tarpaulin_include))] 或将其加入排除正则
测试二进制文件在覆盖率模式下崩溃插桩信息与 sanitizer 冲突不要同时运行 cargo llvm-cov 和 -Zsanitizer=address;请分开运行
llvm-cov 与 tarpaulin 结果不一致插桩技术不同以 llvm-cov 为准(编译器原生支持);如果差异过大请提交 issue
提示 error: profraw file is malformed测试二进制文件在执行过程中崩溃首先修复测试失败;如果进程异常退出,profraw 文件会损坏
分支覆盖率低得离谱优化器为 match 分支、unwrap 等创建了分支在设置门槛时关注 行 覆盖率;分支覆盖率天然较低

亲自尝试

  1. 衡量你项目的覆盖率:运行 cargo llvm-cov --workspace --html 并打开报告。找出覆盖率最低的三个文件。它们是未经测试,还是由于硬件依赖性而天生难以测试?

  2. 设置覆盖率门禁:在 CI 中添加 cargo llvm-cov --workspace --fail-under-lines 60。故意注释掉一个测试,验证 CI 是否失败。然后逐步将阈值提高到实际覆盖率减去 2% 的水平。

  3. 分支 vs 行覆盖率:编写一个带有 3 分支 match 的函数,仅测试其中 2 个分支。对比行覆盖率(可能显示 66%)与分支覆盖率(可能显示 50%)。哪种指标对你的项目更有参考价值?

覆盖率工具选择

flowchart TD
    START["需要代码覆盖率吗?"] --> ACCURACY{"优先级?"}
    
    ACCURACY -->|"最准确"| LLVM["cargo-llvm-cov\n基于源码,编译器原生驱动"]
    ACCURACY -->|"快速检查"| TARP["cargo-tarpaulin\n仅限 Linux,速度快"]
    ACCURACY -->|"多次运行聚合"| GRCOV["grcov\nMozilla 出品,合并 profile"]
    
    LLVM --> CI_GATE["CI 覆盖率门禁\n--fail-under-lines 80"]
    TARP --> CI_GATE
    
    CI_GATE --> UPLOAD{"上传到?"}
    UPLOAD -->|"Codecov"| CODECOV["codecov/codecov-action"]
    UPLOAD -->|"Coveralls"| COVERALLS["coverallsapp/github-action"]
    
    style LLVM fill:#91e5a3,color:#000
    style TARP fill:#e3f2fd,color:#000
    style GRCOV fill:#e3f2fd,color:#000
    style CI_GATE fill:#ffd43b,color:#000

🏋️ 练习

🟢 练习 1:第一份覆盖率报告

安装 cargo-llvm-cov,在任意 Rust 项目上运行并打开 HTML 报告。找出行覆盖率最低的三个文件。

答案
cargo install cargo-llvm-cov
cargo llvm-cov --workspace --html --open
# 报告会根据覆盖率对文件进行排序 —— 覆盖率最低的排在前面或按列排序
# 寻找低于 50% 的文件 —— 它们就是你的盲点

🟡 练习 2:CI 覆盖率门禁

在 GitHub Actions 工作流中添加一个覆盖率门禁,如果行覆盖率低于 60% 则报错。通过注释掉一个测试来验证其效果。

答案
# .github/workflows/coverage.yml
name: Coverage
on: [push, pull_request]
jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - run: cargo install cargo-llvm-cov
      - run: cargo llvm-cov --workspace --fail-under-lines 60

注释掉一个测试后推送,观察工作流是否失败。

关键收获

  • cargo-llvm-cov 是 Rust 最准确的覆盖率工具 —— 它采用了编译器原生的插桩技术。
  • 覆盖率不能证明代码正确,但 零覆盖率能证明零测试 —— 利用它寻找盲点。
  • 在 CI 中设置覆盖率门禁(例如 --fail-under-lines 80)以防止性能/质量退化。
  • 不要盲目追求 100% 覆盖率 —— 重点关注高风险的代码路径(错误处理、unsafe、解析)。
  • 绝不要在同一次运行中混合使用覆盖率插桩和 sanitizer。

English Original

Miri, Valgrind 与 Sanitizer — 验证 Unsafe 代码 🔴

你将学到:

  • Miri 作为 MIR 解释器 — 它能捕捉到什么(别名违规、UB、泄漏)以及它的局限性(FFI、系统调用)
  • Valgrind memcheck (内存检查)、Helgrind (数据竞态)、Callgrind (性能分析) 和 Massif (堆分析)
  • LLVM sanitizer:ASan、MSan、TSan、LSan 配合 nightly -Zbuild-std 的使用
  • 使用 cargo-fuzz 进行崩溃发现,以及使用 loom 进行并发模型检查
  • 选择合适的验证工具的决策树

相关章节: 代码覆盖率 — 覆盖率发现未测试的路径,Miri 验证已测试的路径 · no_std 与特性验证 — no_std 代码通常需要 unsafe,可用 Miri 验证 · CI/CD 流水线 — 流水线中的 Miri 任务

Safe Rust 在编译时保证了内存安全和无数据竞态。但当你为了 FFI、手写数据结构或性能优化而写下 unsafe 的那一刻起,这些保证就变成了 你 的责任。本章涵盖了用于验证你的 unsafe 代码是否真正履行了其安全承诺的工具。

Miri — Unsafe Rust 解释器

Miri 是 Rust 中层中间表示 (Mid-level Intermediate Representation, MIR) 的 解释器。它不将代码编译为机器码,而是逐步 执行 你的程序,并在每一步操作中对未定义行为 (Undefined Behavior, UB) 进行详尽检查。

# 安装 Miri (仅限 nightly 组件)
rustup +nightly component add miri

# 在 Miri 下运行测试套件
cargo +nightly miri test

# 在 Miri 下运行特定二进制文件
cargo +nightly miri run

# 运行特定测试
cargo +nightly miri test -- test_name

Miri 工作原理:

源码 → rustc → MIR → Miri 解释 MIR
                       │
                       ├─ 跟踪每个指针的来源 (provenance)
                       ├─ 验证每次内存访问
                       ├─ 在每次解引用时检查对齐情况
                       ├─ 检测释放后使用 (use-after-free)
                       ├─ 检测数据竞态 (配合线程)
                       └─ 强制执行 Stacked Borrows / Tree Borrows 规则

Miri 能捕捉到什么 (以及它不能做什么)

Miri 检测项:

类别示例运行时会崩溃吗?
越界访问对分配空间外的 ptr.add(100).read()有时会 (取决于页面布局)
释放后使用通过原始指针读取已掉落的 Box有时会 (取决于分配器)
重复释放调用 drop_in_place 两次通常会
未对齐访问在奇数地址上执行 (ptr as *const u32).read()在某些架构上会
无效值执行 transmute::<u8, bool>(2)静默错误
悬空引用指向已释放内存的 &*ptr不会 (静默损坏)
数据竞态两线程一写一读,无同步间歇性,难以复现
借用规则违规别名化 &mut 引用 (Stacked Borrows)不会 (静默损坏)

Miri 无法检测项:

局限性原因
逻辑 BugMiri 检查内存安全,而非逻辑正确性
并发死锁Miri 检查数据竞态,而非活锁
性能问题解释执行比原生执行慢 10-100 倍
系统/硬件交互Miri 无法模拟系统调用、设备 I/O
所有 FFI 调用无法解释 C 代码 (仅限 Rust MIR)
完备的路径覆盖仅测试你的测试套件所触及的路径

具体示例 — 捕捉在实践中能“跑通”但存在风险的代码:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn test_miri_catches_ub() {
        // 这在 release 构建中可能“正常工作”,但属于未定义行为
        let mut v = vec![1, 2, 3];
        let ptr = v.as_ptr();

        // push 可能会导致重新分配,从而使 ptr 失效
        v.push(4);

        // ❌ UB: 重新分配后 ptr 可能悬空
        // 即使分配器恰好没有移动缓冲区,Miri 也会捕捉到这一点。
        // let _val = unsafe { *ptr };
        // Miri 会报告错误:
        //   "pointer to alloc1234 was dereferenced after this
        //    allocation got freed"
        
        // ✅ 正确:在变更后获取新指针
        let ptr = v.as_ptr();
        let val = unsafe { *ptr };
        assert_eq!(val, 1);
    }
}
}

在真实 Crate 上运行 Miri

针对含有 unsafe 的 crate 的 Miri 实践流程:

# 第 1 步:在 Miri 下运行所有测试
cargo +nightly miri test 2>&1 | tee miri_output.txt

# 第 2 步:如果 Miri 报错,隔离该测试
cargo +nightly miri test -- failing_test_name

# 第 3 步:使用 Miri 的回溯信息进行诊断
MIRIFLAGS="-Zmiri-backtrace=full" cargo +nightly miri test

# 第 4 步:选择借用模型
# Stacked Borrows (默认,最严格):
cargo +nightly miri test

# Tree Borrows (实验性,更宽松):
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test

常用场景的 Miri 标志:

# 禁用隔离 (允许访问文件系统、环境变量)
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test

# Miri 默认开启内存泄漏检测。
# 若要屏蔽泄漏错误 (例如有意为之的泄漏):
# MIRIFLAGS="-Zmiri-ignore-leaks" cargo +nightly miri test

# 为随机化测试设置种子,以保证结果可复现
MIRIFLAGS="-Zmiri-seed=42" cargo +nightly miri test

# 开启严格的来源 (provenance) 检查
MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test

# 组合多个标志
MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-backtrace=full -Zmiri-strict-provenance" \
    cargo +nightly miri test

CI 中的 Miri 配置:

# .github/workflows/miri.yml
name: Miri
on: [push, pull_request]

jobs:
  miri:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@nightly
        with:
          components: miri

      - name: Run Miri
        run: cargo miri test --workspace
        env:
          MIRIFLAGS: "-Zmiri-backtrace=full"
          # 默认开启泄漏检查。
          # 跳过那些使用了 Miri 无法处理的系统调用的测试
          # (如文件 I/O、网络等)

性能提示:Miri 比原生执行慢 10-100 倍。一个原生运行需 5 秒的测试套件,在 Miri 下可能需要 5 分钟。在 CI 中,建议仅对包含 unsafe 代码的关键 crate 运行 Miri。

Valgrind 及其 Rust 集成

Valgrind 是经典的 C/C++ 内存检查器。它同样适用于编译后的 Rust 二进制文件,在机器码层面检查内存错误。

# 安装 Valgrind
sudo apt install valgrind  # Debian/Ubuntu
sudo dnf install valgrind  # Fedora

# 构建时包含调试信息 (Valgrind 需要符号表)
cargo build --tests
# 或者构建带调试信息的 release 版本:
# cargo build --release
# [profile.release]
# debug = true

# 在 Valgrind 下运行特定的测试二进制文件
valgrind --tool=memcheck \
    --leak-check=full \
    --show-leak-kinds=all \
    --track-origins=yes \
    ./target/debug/deps/my_crate-abc123 --test-threads=1

# 运行主二进制文件
valgrind --tool=memcheck \
    --leak-check=full \
    --error-exitcode=1 \
    ./target/debug/diag_tool --run-diagnostics

除 memcheck 外的其他 Valgrind 工具:

工具命令检测内容
Memcheck--tool=memcheck内存泄漏、释放后使用、缓冲区溢出
Helgrind--tool=helgrind数据竞态和锁顺序违规
DRD--tool=drd数据竞态 (不同的检测算法)
Callgrind--tool=callgrindCPU 指令级分析 (路径级)
Massif--tool=massif堆内存随时间的使用情况分析
Cachegrind--tool=cachegrind缓存未命中分析

使用 Callgrind 进行指令级分析:

# 记录指令计数 (比墙钟时间更稳定)
valgrind --tool=callgrind \
    --callgrind-out-file=callgrind.out \
    ./target/release/diag_tool --run-diagnostics

# 使用 KCachegrind 可视化
kcachegrind callgrind.out
# 或使用命令行工具查看:
callgrind_annotate callgrind.out | head -100

Miri vs Valgrind — 如何选择:

维度MiriValgrind
检查 Rust 特有的 UB✅ 支持 Stacked/Tree Borrows❌ 不理解 Rust 特有规则
检查 C FFI 代码❌ 无法解释 C 代码✅ 检查所有机器码
是否需要 Nightly✅ 是❌ 否
运行速度慢 10-100 倍慢 10-50 倍
平台支持任何 (解释 MIR)Linux, macOS (运行原生代码)
数据竞态检测✅ 是✅ 是 (Helgrind/DRD)
内存泄漏检测✅ 是✅ 是 (更彻底)
误报率极低偶有 (尤其是针对分配器时)

建议两者结合使用:

  • Miri 用于纯 Rust 的 unsafe 代码(检查 Stacked Borrows、来源等)。
  • Valgrind 用于包含大量 FFI 的代码以及全程序的泄漏分析。

AddressSanitizer, MemorySanitizer, ThreadSanitizer

LLVM sanitizer 是编译期插桩手段,它会在运行时插入检查。它们比 Valgrind 更快(开销为 2-5 倍,而 Valgrind 为 10-50 倍),且能捕捉不同类别的 Bug。

# 必需:安装 Rust 源码以便在使用 sanitizer 插桩时重新编译 std
rustup component add rust-src --toolchain nightly

# AddressSanitizer (ASan) — 缓冲区溢出、释放后使用、栈溢出
RUSTFLAGS="-Zsanitizer=address" \
    cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# MemorySanitizer (MSan) — 读取未初始化内存
RUSTFLAGS="-Zsanitizer=memory" \
    cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# ThreadSanitizer (TSan) — 数据竞态
RUSTFLAGS="-Zsanitizer=thread" \
    cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# LeakSanitizer (LSan) — 内存泄漏 (默认已包含在 ASan 中)
RUSTFLAGS="-Zsanitizer=leak" \
    cargo +nightly test --target x86_64-unknown-linux-gnu

注意:ASan, MSan 和 TSan 都需要 -Zbuild-std 来重新编译标准库以便插入插桩信息。LSan 则不需要。

Sanitizer 对比:

Sanitizer开销捕捉内容是否需 Nightly是否需 -Zbuild-std
ASan2倍内存, 2倍 CPU缓冲区溢出、释放后使用、栈溢出是是
MSan3倍内存, 3倍 CPU读取未初始化内存是是
TSan5-10倍内存, 5倍 CPU数据竞态是是
LSan极小内存泄漏是否

实践示例 — 使用 TSan 捕捉数据竞态:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;

fn racy_counter() -> u64 {
    // ❌ UB: 未经同步的共享可变状态
    let data = Arc::new(std::cell::UnsafeCell::new(0u64));
    let mut handles = vec![];

    for _ in 0..4 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                // SAFETY: 这是一个不健壮的实现 —— 存在数据竞态!
                unsafe {
                    *data.get() += 1;
                }
            }
        }));
    }

    for h in handles {
        h.join().unwrap();
    }

    // 正确结果应为 4000,但由于竞态,结果是不确定的
    unsafe { *data.get() }
}

// Miri 和 TSan 都能捕捉到这一点:
// Miri:  "Data race detected between (1) write and (2) write"
// TSan:  "WARNING: ThreadSanitizer: data race"
//
// 修复:使用 AtomicU64 或 Mutex<u64>
}

相关工具:模糊测试与并发验证

cargo-fuzz — 覆盖率导向型模糊测试 (用于发现解析器和解码器中的崩溃):

# 安装
cargo install cargo-fuzz

# 初始化模糊测试目标
cargo fuzz init
cargo fuzz add parse_gpu_csv
#![allow(unused)]
fn main() {
// fuzz/fuzz_targets/parse_gpu_csv.rs
#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    if let Ok(s) = std::str::from_utf8(data) {
        // 模糊测试器会生成数百万个输入,试图寻找导致 panic 或崩溃的情况。
        let _ = diag_tool::parse_gpu_csv(s);
    }
});
}
# 运行模糊测试 (持续运行直至手动中断或发现崩溃)
cargo +nightly fuzz run parse_gpu_csv -- -max_total_time=300  # 运行 5 分钟

# 最小化崩溃用例
cargo +nightly fuzz tmin parse_gpu_csv artifacts/parse_gpu_csv/crash-...

何时进行模糊测试:任何解析不可信/半可信输入的函数(如传感器输出、配置文件、网络数据、JSON/CSV)。模糊测试在几乎所有主流 Rust 解析库(如 serde, regex, image)中都发现了真实的 Bug。

loom — 并发模型检查器 (详尽测试原子操作顺序):

[dev-dependencies]
loom = "0.7"
#![allow(unused)]
fn main() {
#[cfg(loom)]
mod tests {
    use loom::sync::atomic::{AtomicUsize, Ordering};
    use loom::thread;

    #[test]
    fn test_counter_is_atomic() {
        loom::model(|| {
            let counter = loom::sync::Arc::new(AtomicUsize::new(0));
            let c1 = counter.clone();
            let c2 = counter.clone();

            let t1 = thread::spawn(move || { c1.fetch_add(1, Ordering::SeqCst); });
            let t2 = thread::spawn(move || { c2.fetch_add(1, Ordering::SeqCst); });

            t1.join().unwrap();
            t2.join().unwrap();

            // loom 会探索所有可能的线程交织情况
            assert_eq!(counter.load(Ordering::SeqCst), 2);
        });
    }
}
}

何时使用 loom:当你编写无锁 (lock-free) 数据结构或自定义同步原语时。Loom 会穷尽探索线程间的各种交织 —— 它是一个模型检查器,而非压力测试器。基于 Mutex/RwLock 的代码通常不需要它。

验证工具决策树

Unsafe 验证决策树:

代码是纯 Rust 吗 (无 FFI)?
├─ 是 → 使用 Miri (捕捉 Rust 特有的 UB, Stacked Borrows)
│        同时在 CI 中运行 ASan 以实现深度防御
└─ 否 (通过 FFI 调用了 C/C++ 代码)
   ├─ 担心内存安全?
   │  └─ 是 → 同时使用 Valgrind memcheck 和 ASan
   ├─ 担心并发问题?
   │  └─ 是 → 使用 TSan (更灵敏) 或 Helgrind (更彻底)
   └─ 担心内存泄漏?
      └─ 是 → 使用 Valgrind --leak-check=full

建议的 CI 矩阵:

# 并行运行所有工具以获得快速反馈
jobs:
  miri:
    runs-on: ubuntu-latest
    steps:
      - uses: dtolnay/rust-toolchain@nightly
        with: { components: miri }
      - run: cargo miri test --workspace

  asan:
    runs-on: ubuntu-latest
    steps:
      - uses: dtolnay/rust-toolchain@nightly
      - run: |
          RUSTFLAGS="-Zsanitizer=address" \
          cargo test -Zbuild-std --target x86_64-unknown-linux-gnu

  valgrind:
    runs-on: ubuntu-latest
    steps:
      - run: sudo apt-get install -y valgrind
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo build --tests
      - run: |
          for test_bin in $(find target/debug/deps -maxdepth 1 -executable -type f ! -name '*.d'); do
            valgrind --error-exitcode=1 --leak-check=full "$test_bin" --test-threads=1
          done

应用:保持 Unsafe 为零 — 及其必要性

本项目在 9 万多行 Rust 代码中实现了 零 unsafe 块。对于一个系统级诊断工具来说,这是一个了不起的成就,证明了 Safe Rust 足以胜任:

  • IPMI 通信(通过 std::process::Command 调用 ipmitool)
  • GPU 查询(通过 std::process::Command 调用 accel-query)
  • PCIe 拓扑解析(纯 JSON/文本解析)
  • SEL 记录管理(纯数据结构)
  • DER 报告生成(JSON 序列化)

本项目何时会需要 unsafe?

引入 unsafe 的潜在触发因素:

场景引入 unsafe 的原因建议的验证手段
基于 ioctl 的直接 IPMI使用 libc::ioctl() 绕过 ipmitool 子进程Miri + Valgrind
直接调用 GPU 驱动接口使用 accel-mgmt FFI 代替 accel-query 解析Valgrind (针对 C 库)
内存映射 PCIe 配置空间使用 mmap 直接读取配置空间ASan + Valgrind
无锁 SEL 缓冲区使用 AtomicPtr 进行并发事件采集Miri + TSan
裸机 / no_std 变体针对底层硬件进行原始指针操作Miri

准备工作:在引入 unsafe 之前,先将验证工具集成到 CI 中:

# Cargo.toml — 为 unsafe 优化添加特性标志 (feature flag)
[features]
default = []
direct-ipmi = []     # 启用直接 ioctl IPMI,而非 ipmitool 子进程
direct-accel-api = []     # 启用 accel-mgmt FFI,而非 accel-query 解析
#![allow(unused)]
fn main() {
// src/ipmi.rs — 放在特性标志后面
#[cfg(feature = "direct-ipmi")]
mod direct {
    //! 通过 /dev/ipmi0 ioctl 直接访问 IPMI 设备。
    //!
    //! # Safety
    //! 本模块使用 `unsafe` 执行 ioctl 系统调用。
    //! 已验证工具:Miri (尽可能)、Valgrind memcheck、ASan。

    use std::os::unix::io::RawFd;

    // ... unsafe ioctl 实现 ...
}

#[cfg(not(feature = "direct-ipmi"))]
mod subprocess {
    //! 通过 ipmitool 子进程执行 IPMI (默认方案,完全安全)。
    // ... 当前实现 ...
}
}

关键洞察:将 unsafe 代码放在 特性标志 后面,这样就可以独立验证。在 CI 中运行 cargo +nightly miri test --features direct-ipmi 来持续验证 unsafe 路径,而不会影响默认的安全构建。

cargo-careful — Stable 上的额外 UB 检查

cargo-careful 运行时会开启标准库的额外检查 —— 捕捉到一些普通构建会忽略的未定义行为,它不需要 nightly 也不像 Miri 那样慢 100 倍:

# 安装 (需要 nightly,但运行代码的速度接近原生)
cargo install cargo-careful

# 运行带有额外 UB 检查的测试 (可捕捉未初始化内存、无效值)
cargo +nightly careful test

# 运行二进制文件
cargo +nightly careful run -- --run-diagnostics

cargo-careful 能捕捉到而普通构建不能捕捉到的:

  • 对 MaybeUninit 和 zeroed() 创建的未初始化内存的读取
  • 通过 transmute 创建无效的 bool、char 或枚举值
  • 未对齐的指针读写
  • copy_nonoverlapping 的范围重叠

它在验证阶梯中的位置:

最小开销                                               最彻底
├─ cargo test ──► cargo careful test ──► Miri ──► ASan ──► Valgrind ─┤
│  (0层开销)       (~1.5倍开销)          (10-100倍) (2倍)    (10-50倍)  │
│  仅 Safe Rust    捕捉部分 UB           纯 Rust    FFI+Rust  FFI+Rust  │

建议:将 cargo +nightly careful test 加入 CI 作为一个快速安全检查。它的运行速度接近原生(不像 Miri),且能捕捉到 Safe Rust 抽象层掩盖的真实 Bug。

Miri 与 Sanitizer 常见排错

现象原因修复方法
Miri does not support FFIMiri 是 Rust 解释器,无法执行 C 代码改用 Valgrind 或 ASan 处理 FFI 代码
error: unsupported operation: can't call foreign functionMiri 触及了 extern "C" 调用模拟 FFI 边界或使用 #[cfg(miri)] 屏蔽
Stacked Borrows violation违反了别名规则 —— 即使代码能“跑通”Miri 是正确的;重构代码以避免 &mut 与 & 别名化
Sanitizer 提示 DEADLYSIGNALASan 检测到了缓冲区溢出检查数组索引、切片操作和指针算术
LeakSanitizer: detected memory leaksBox::leak()、forget() 或遗漏了 drop()有意为之:使用 __lsan_disable() 屏蔽;无意为之:修复泄漏
Miri 运行极其缓慢Miri 是解释执行,而非编译执行仅对 --lib 测试运行,或对繁重的测试标记 #[cfg_attr(miri, ignore)]
TSan: false positive 涉及原子操作TSan 可能无法完美理解 Rust 的原子排序模型添加 TSAN_OPTIONS=suppressions=tsan.supp 进行特定屏蔽

亲自尝试

  1. 触发一次 Miri UB 检测:编写一个 unsafe 函数,对同一个 i32 创建两个 &mut 引用(违反别名规则)。运行 cargo +nightly miri test 并观察 “Stacked Borrows” 错误。通过使用 UnsafeCell 或独立的分配空间来修复它。

  2. 在刻意制造的 Bug 上运行 ASan:创建一个包含 unsafe 数组越界访问的测试。使用 RUSTFLAGS="-Zsanitizer=address" 进行构建并观察 ASan 报告。注意它是如何精准定位到那一行的。

  3. 衡量 Miri 的开销:在同一测试套件上计时 cargo test --lib 与 cargo +nightly miri test --lib,计算减速倍数。以此决定哪些测试应在 CI 的 Miri 任务中运行,哪些应使用 #[cfg_attr(miri, ignore)] 跳过。

安全验证决策树

flowchart TD
    START["是否有 unsafe 代码?"] -->|否| SAFE["Safe Rust — 无需\n额外验证"]
    START -->|是| KIND{"哪种类型?"}
    
    KIND -->|"纯 Rust unsafe"| MIRI["Miri\nMIR 解释器\n捕捉别名、UB、泄漏"]
    KIND -->|"FFI / C 互操作"| VALGRIND["Valgrind memcheck\n或 ASan"]
    KIND -->|"并发 unsafe"| CONC{"是否无锁?"}
    
    CONC -->|"原子操作/无锁"| LOOM["loom\n原子映射模型检查器"]
    CONC -->|"Mutex/共享状态"| TSAN["TSan 或\nMiri -Zmiri-check-number-validity"]
    
    MIRI --> CI_MIRI["CI: cargo +nightly miri test"]
    VALGRIND --> CI_VALGRIND["CI: valgrind --leak-check=full"]
    
    style SAFE fill:#91e5a3,color:#000
    style MIRI fill:#e3f2fd,color:#000
    style VALGRIND fill:#ffd43b,color:#000
    style LOOM fill:#ff6b6b,color:#000
    style TSAN fill:#ffd43b,color:#000

🏋️ 练习

🟡 练习 1:触发 Miri UB 检测

编写一个 unsafe 函数,对同一个 i32 创建两个 &mut 引用(违反别名规则)。运行 cargo +nightly miri test 并观察 Stacked Borrows 错误。修复它。

答案
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn aliasing_ub() {
        let mut x: i32 = 42;
        let ptr = &mut x as *mut i32;
        unsafe {
            // BUG: 同一位置存在两个 &mut 引用
            let _a = &mut *ptr;
            let _b = &mut *ptr; // Miri: Stacked Borrows violation!
        }
    }
}
}

修复:使用独立的分配空间或 UnsafeCell:

#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;

#[test]
fn no_aliasing_ub() {
    let x = UnsafeCell::new(42);
    unsafe {
        let a = &mut *x.get();
        *a = 100;
    }
}
}

🔴 练习 2:ASan 越界检测

创建一个包含 unsafe 数组越界访问的测试。在 nightly 上使用 RUSTFLAGS="-Zsanitizer=address" 进行构建并观察 ASan 报告。

答案
#![allow(unused)]
fn main() {
#[test]
fn oob_access() {
    let arr = [1u8, 2, 3, 4, 5];
    let ptr = arr.as_ptr();
    unsafe {
        let _val = *ptr.add(10); // 越界访问!
    }
}
}
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std \
  --target x86_64-unknown-linux-gnu -- oob_access
# ASan 报告:stack-buffer-overflow at <具体地址>

关键收获

  • Miri 是用于纯 Rust unsafe 代码的首选工具 —— 它能捕捉到别名规则违规、释放后使用,以及那些能通过编译和测试的内存泄漏。
  • Valgrind 是针对 FFI/C 互操作的首选工具 —— 它无需重新编译即可对最终二进制文件生效。
  • Sanitizer (ASan, TSan, MSan) 需要 nightly 环境,但其运行速度接近原生 —— 是大型测试套件的理想选择。
  • loom 专门用于验证无锁并发数据结构。
  • 建议在每次 push 时的 CI 任务中运行 Miri;周期性运行 sanitizer 任务,以免拖慢主流水线。

English Original

依赖管理与供应链安全 🟢

你将学到:

  • 使用 cargo-audit 扫描已知漏洞
  • 使用 cargo-deny 强制执行许可证、漏洞通告和源码策略
  • 使用 Mozilla 的 cargo-vet 进行供应链信任验证
  • 跟踪过期的依赖并检测破坏性的 API 变更 (Breaking Changes)
  • 可视化并去重你的依赖树

相关章节: 发布配置 — cargo-udeps 会在此处清理未使用的依赖 · CI/CD 流水线 — 流水线中的 audit 和 deny 任务 · 构建脚本 — build-dependencies 同样也是供应链的一部分

一个 Rust 二进制文件不只是包含你的代码 —— 它包含了 Cargo.lock 中所有的传递依赖。该依赖树中任何地方出现的漏洞、许可证违规或恶意 crate 都会变成 你 的问题。本章涵盖了使依赖管理变得可审计且自动化的工具。

cargo-audit — 已知漏洞扫描

cargo-audit 会根据 RustSec 漏洞通告数据库 检查你的 Cargo.lock,该数据库跟踪了已发布的 crate 中已知的安全性漏洞。

# 安装
cargo install cargo-audit

# 扫描已知漏洞
cargo audit

# 输出示例:
# Crate:     chrono
# Version:   0.4.19
# Title:     Potential segfault in localtime_r invocations
# Date:      2020-11-10
# ID:        RUSTSEC-2020-0159
# URL:       https://rustsec.org/advisories/RUSTSEC-2020-0159
# 解决方案:  升级至 >= 0.4.20

# 如果存在漏洞,则使 CI 失败
cargo audit --deny warnings

# 为自动化处理生成 JSON 输出
cargo audit --json

# 通过更新 Cargo.lock 修复漏洞
cargo audit fix

CI 集成:

# .github/workflows/audit.yml
name: Security Audit
on:
  schedule:
    - cron: '0 0 * * *'  # 每日检查 —— 漏洞通告会不断更新
  push:
    paths: ['Cargo.lock']

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: rustsec/audit-check@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

cargo-deny — 全面的策略强制执行

cargo-deny 的功能远不止漏洞扫描。它从四个维度强制执行策略:

  1. Advisories (通告) — 已知漏洞(类似于 cargo-audit)
  2. Licenses (许可证) — 允许/禁止的许可证列表
  3. Bans (禁止项) — 禁止的 crate 或重复的版本
  4. Sources (源码) — 允许的注册表 (Registry) 和 Git 源码
# 安装
cargo install cargo-deny

# 初始化配置
cargo deny init
# 创建带有文档注释默认值的 deny.toml

# 运行所有检查
cargo deny check

# 运行特定检查
cargo deny check advisories
cargo deny check licenses
cargo deny check bans
cargo deny check sources

deny.toml 示例:

# deny.toml

[advisories]
vulnerability = "deny"        # 对已知漏洞报错
unmaintained = "warn"         # 对不再维护的 crate 发出警告
yanked = "deny"               # 对已撤回 (yanked) 的 crate 报错
notice = "warn"               # 对通告性质的通知发出警告

[licenses]
unlicensed = "deny"           # 所有 crate 必须有许可证
allow = [
    "MIT",
    "Apache-2.0",
    "BSD-2-Clause",
    "BSD-3-Clause",
    "ISC",
    "Unicode-DFS-2016",
]
copyleft = "deny"             # 该项目禁止使用 GPL/LGPL/AGPL
default = "deny"              # 拒绝任何未明确允许的项目

[bans]
multiple-versions = "warn"    # 如果同一个 crate 出现了 2 个版本则发出警告
wildcards = "deny"            # 禁止在依赖中使用 path = "*"
highlight = "all"             # 显示所有重复项,而不仅是第一个

# 禁止特定的有问题 crate
deny = [
    # openssl-sys 会拉取 C 语言的 OpenSSL —— 建议改用 rustls
    { name = "openssl-sys", wrappers = ["native-tls"] },
]

# 允许特定的重复版本(当无法避免时)
[[bans.skip]]
name = "syn"
version = "1.0"               # syn 1.x 和 2.x 往往共存

对于商业项目而言,许可证强制执行非常有价值:

# 检查依赖树中包含哪些许可证
cargo deny list

# 输出示例:
# MIT          — 127 个 crate
# Apache-2.0   — 89 个 crate
# BSD-3-Clause — 12 个 crate
# MPL-2.0      — 3 个 crate   ← 可能需要法务审核
# Unicode-DFS  — 1 个 crate

cargo-vet — 供应链信任验证

Mozilla 出品的 cargo-vet 解决的是另一个问题:不是“这个 crate 是否有已知的漏洞?”,而是“是否有受信任的人员实际审查过这段代码?”

# 安装
cargo install cargo-vet

# 初始化 (创建 supply-chain/ 目录)
cargo vet init

# 检查哪些 crate 需要审查
cargo vet

# 审查完一个 crate 后进行认证:
cargo vet certify serde 1.0.203
# 记录下你已针对自己的标准审计过 serde 1.0.203

# 从受信任的组织导入审计结果
cargo vet import mozilla
cargo vet import google
cargo vet import bytecode-alliance

工作机制:

supply-chain/
├── audits.toml       ← 你所在团队的审计认证
├── config.toml       ← 信任配置和准则
└── imports.lock      ← 从其他组织锁定的导入项

cargo-vet 对于拥有严格供应链要求的组织(政府、金融、基础设施)最有价值。对于大多数团队来说,cargo-deny 已能提供足够的保护。

cargo-outdated 与 cargo-semver-checks

cargo-outdated — 查找拥有新版本的依赖项:

cargo install cargo-outdated

cargo outdated --workspace
# 输出示例:
# 名称        当前版本  兼容版本  最新版本  类型
# serde       1.0.193  1.0.203 1.0.203  Normal
# regex       1.9.6    1.10.4  1.10.4   Normal
# thiserror   1.0.50   1.0.61  2.0.3    Normal  ← 存在大版本更新

cargo-semver-checks — 在发布前检测破坏性的 API 变更。这对于库 (Library) 类型的 crate 至关重要:

cargo install cargo-semver-checks

# 检查你的更改是否符合语义化版本 (semver)
cargo semver-checks

# 输出示例:
# ✗ 函数 `parse_gpu_csv` 现在变成了私有 (之前是公有)
#   → 这是一个破坏性变更。请提升 MAJOR 版本。
#
# ✗ 结构体 `GpuInfo` 增加了一个新的必需字段 `power_limit_w`
#   → 这是一个破坏性变更。请提升 MAJOR 版本。
#
# ✓ 增加了函数 `parse_gpu_csv_v2` (非破坏性变更)

cargo-tree — 依赖可视化与去重

cargo tree 内置于 Cargo 中(无需安装),它是理解依赖关系图的利器:

# 完整的依赖树
cargo tree

# 查找为什么引入了某个特定的 crate
cargo tree --invert --package openssl-sys
# 显示从你的 crate 到 openssl-sys 的所有路径

# 查找重复的版本
cargo tree --duplicates
# 输出示例:
# syn v1.0.109
# └── serde_derive v1.0.193
#
# syn v2.0.48
# ├── thiserror-impl v1.0.56
# └── tokio-macros v2.2.0

# 仅显示直接依赖
cargo tree --depth 1

# 显示依赖的特性 (features)
cargo tree --format "{p} {f}"

# 统计总依赖数
cargo tree | wc -l

去重策略:当 cargo tree --duplicates 显示同一个 crate 有两个大版本时,请检查是否可以更新依赖链以统一它们。每个重复项都会增加编译时间和二进制体积。

应用:多 Crate 依赖健康管理

本工作区使用 [workspace.dependencies] 进行中心化的版本管理 —— 这是一个极佳的实践。结合 cargo tree --duplicates 进行体积分析,可以防止版本漂移并减少二进制膨胀:

# 根目录 Cargo.toml — 在一处锁定所有版本
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
regex = "1.10"
thiserror = "1.0"
anyhow = "1.0"
rayon = "1.8"

建议为本项目添加:

# 添加到 CI 流水线:
cargo deny init              # 一次性设置
cargo deny check             # 每个 PR 运行 — 检查许可证、漏洞通告、禁止项
cargo audit --deny warnings  # 每次 push 运行 — 漏洞扫描
cargo outdated --workspace   # 每周运行 — 跟踪可用更新

建议为本项目配置 deny.toml:

[advisories]
vulnerability = "deny"
yanked = "deny"

[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-DFS-2016"]
copyleft = "deny"     # 硬件诊断工具 —— 不使用 Copyleft 协议

[bans]
multiple-versions = "warn"   # 跟踪重复项,暂不阻止
wildcards = "deny"

[sources]
unknown-registry = "deny"
unknown-git = "deny"

供应链审计流水线

flowchart LR
    PR["Pull Request"] --> AUDIT["cargo audit\n已知 CVE"]
    AUDIT --> DENY["cargo deny check\n许可证 + 禁止项 + 源码"]
    DENY --> OUTDATED["cargo outdated\n周度计划"]
    OUTDATED --> SEMVER["cargo semver-checks\n仅限 Library crate"]
    
    AUDIT -->|"失败"| BLOCK["❌ 阻止合并"]
    DENY -->|"失败"| BLOCK
    SEMVER -->|"存在破坏性变更"| BUMP["提升 Major 版本"]
    
    style BLOCK fill:#ff6b6b,color:#000
    style BUMP fill:#ffd43b,color:#000
    style PR fill:#e3f2fd,color:#000

🏋️ 练习

🟢 练习 1:审计你的依赖

在任意 Rust 项目上运行 cargo audit 和 cargo deny init && cargo deny check。发现了多少条针对已知漏洞的通告?依赖树中包含多少种许可证类别?

答案
cargo audit
# 留意任何安全通告 —— 通常见于 chrono, time 或较旧的 crate

cargo deny init
cargo deny list
# 显示许可证明细:MIT (N), Apache-2.0 (N) 等。

cargo deny check
# 显示横跨四个维度的完整审计结果

🟡 练习 2:查找并消除重复依赖

在工作区运行 cargo tree --duplicates。找出一个出现两个版本的 crate。你能更新 Cargo.toml 来统一它们吗?衡量一下这对编译时间和二进制体积的影响。

答案
cargo tree --duplicates
# 常见情况:syn 1.x 和 syn 2.x

# 查找是谁拉取了旧版本:
cargo tree --invert --package [email protected]
# 输出示例:serde_derive 1.0.xxx -> syn 1.0.109

# 检查是否有使用 syn 2.x 的新版 serde_derive:
cargo update -p serde_derive
cargo tree --duplicates
# 如果 syn 1.x 消失了,说明你成功消除了一处重复

# 衡量影响:
time cargo build --release  # 更新前后对比
cargo bloat --release --crates | head -20

关键收获

  • cargo audit 能捕捉已知的 CVE 漏洞 —— 建议在每次 push 时以及按每日计划运行。
  • cargo deny 从四个维度强制执行策略:漏洞通告、许可证、禁止项以及源码。
  • 使用 [workspace.dependencies] 在多 crate 工作区中进行版本中心化管理。
  • cargo tree --duplicates 揭示了膨胀;每个重复项都会增加编译时间和二进制体积。
  • cargo-vet 适用于高安全性要求的环境;对于大多数团队来说,cargo-deny 已足够。

English Original

发布配置与二进制体积 🟡

你将学到:

  • 发布配置 (Release Profile) 详解:LTO、codegen-units、panic 策略、strip、opt-level
  • Thin LTO、Fat LTO 与跨语言 LTO 的权衡
  • 使用 cargo-bloat 进行二进制体积分析
  • 使用 cargo-udeps、cargo-machete 和 cargo-shear 清理冗余依赖

相关章节: 编译期工具 — 优化的另一面 · 基准测试 — 在优化前先测量运行时间 · 依赖管理 — 剔除依赖可同时减少体积和编译时间

默认的 cargo build --release 已经非常优秀。但对于生产环境部署 —— 尤其是需要分发到数千台服务器的单一二进制工具而言,“优秀”与“极致优化”之间仍有不小的差距。本章涵盖了配置参数以及衡量二进制体积的工具。

发布配置详解

Cargo 配置项 (Profile) 控制着 rustc 编译代码的方式。默认设置较为保守 —— 旨在保证广泛的兼容性,而非追求极致性能:

# Cargo.toml — Cargo 内置的默认值(如果你什么都不写的话)

[profile.release]
opt-level = 3        # 优化级别 (0=无, 1=基础, 2=良好, 3=激进)
lto = false          # 禁用链接期优化 (Link-time optimization)
codegen-units = 16   # 并行编译单元 (编译较快,但优化空间受限)
panic = "unwind"     # panic 时的栈回溯 (体积较大,可使用 catch_unwind)
strip = "none"       # 保留所有符号和调试信息
overflow-checks = false  # release 模式下不进行整数溢出检查
debug = false        # release 模式不包含调试信息

针对生产优化的配置(本项目已在使用):

[profile.release]
lto = true           # 开启全量跨 crate 优化
codegen-units = 1    # 单个生成单元 —— 提供最大的优化机会
panic = "abort"      # 无回溯开销 —— 更小、更快
strip = true         # 移除所有符号 —— 二进制体积更小

每项设置的影响:

设置项默认 → 优化后二进制体积运行时速度编译时间
lto = false → true—-10% 到 -20%+5% 到 +20%慢 2-5 倍
codegen-units = 16 → 1—-5% 到 -10%+5% 到 +10%慢 1.5-2 倍
panic = "unwind" → "abort"—-5% 到 -10%微乎其微微乎其微
strip = "none" → true—-50% 到 -70%无无
opt-level = 3 → "s"—-10% 到 -30%-5% 到 -10%类似
opt-level = 3 → "z"—-15% 到 -40%-10% 到 -20%类似

其他常用的配置微调:

[profile.release]
# 除上述设置外,还可以:
overflow-checks = true    # 即使在 release 中也保留溢出检查 (安全 > 速度)
debug = "line-tables-only" # 仅保留行表,用于回溯时显示行号,而不包含完整的 DWARF
rpath = false             # 不嵌入运行时库路径
incremental = false       # 禁用增量编译 (为了更干净地构建最终版)

# 针对体积高度优化的构建 (如嵌入式、WASM):
# opt-level = "z"         # 极其激进地针对体积进行优化
# strip = "symbols"       # 只移除符号信息,保留调试段

针对单个 crate 的配置覆盖 (Overrides) — 仅对热点 crate 进行极致优化,其他保持原样:

# 开发模式:优化依赖项,但不优化自己的代码 (保证快速重新编译)
[profile.dev.package."*"]
opt-level = 2          # 开发模式下优化所有依赖项

# 发布模式:覆盖特定 crate 的优化级别
[profile.release.package.serde_json]
opt-level = 3          # 对 JSON 解析进行最大程度优化
codegen-units = 1

# 测试配置:匹配 release 行为,获取准确的集成测试结果
[profile.test]
opt-level = 1          # 基础优化,避免慢速测试超时

深入理解 LTO — Thin vs Fat vs 跨语言

链接期优化 (Link-Time Optimization) 允许 LLVM 跨越 crate 边界进行优化 —— 例如将 serde_json 的函数内联到你的解析代码中,或者移除 regex 中的死代码等。如果不开启 LTO,每个 crate 都是一个独立的“优化孤岛”。

[profile.release]
# 方案 1: Fat LTO (lto = true 时的默认值)
lto = true
# 所有代码合并为一个 LLVM 模块 → 优化空间最大
# 编译最慢,二进制体积最小、运行最快

# 方案 2: Thin LTO
lto = "thin"
# 保持每个 crate 独立,但 LLVM 会进行跨模块优化
# 比 Fat LTO 编译快,优化效果几乎同样出色
# 大多数项目的最佳折衷方案

# 方案 3: 不开启 LTO
lto = false
# 仅进行 crate 内部优化
# 编译最快,二进制体积较大

# 方案 4: 显式关闭
lto = "off"
# 等同于 false

Fat LTO 与 Thin LTO 对比:

维度Fat LTO (true)Thin LTO ("thin")
优化质量最好达到 Fat 的 ~95%
编译时间慢 (所有代码都在一个模块中)中等 (并行处理模块)
内存占用高 (所有 LLVM IR 都在内存中)较低 (流式处理)
并行性无 (单一模块)优秀 (跨模块并行)
推荐场景最终发布构建CI 构建、日常开发

跨语言 LTO — 跨越 Rust 与 C 的边界进行优化:

[profile.release]
lto = true

# Cargo.toml — 使用了 cc crate 的 crate
[build-dependencies]
cc = "1.0"
// build.rs — 启用跨语言 (linker-plugin) LTO
fn main() {
    // cc crate 会遵循环境变量中的 CFLAGS。
    // 对于跨语言 LTO,编译 C 代码时需使用:
    //   -flto=thin -O2
    cc::Build::new()
        .file("csrc/fast_parser.c")
        .flag("-flto=thin")
        .opt_level(2)
        .compile("fast_parser");
}
# 启用链接器插件 LTO (需要兼容的 LLD 或 gold 链接器)
RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
    cargo build --release

跨语言 LTO 允许 LLVM 将 C 函数内联到 Rust 调用者中,反之亦然。这对于频繁调用 C 语言小函数(如 IPMI ioctl 封装函数)的 FFI 密集型代码效果显著。

使用 cargo-bloat 进行二进制体积分析

cargo-bloat 能够回答:“二进制文件中哪些函数和 crate 占用的空间最多?”

# 安装
cargo install cargo-bloat

# 显示占用空间最大的前 20 个函数
cargo bloat --release -n 20
# 输出示例:
#  文件    .text     大小          Crate    名称
#  2.8%   5.1%  78.5KiB  serde_json       serde_json::de::Deserializer::parse_...
#  2.1%   3.8%  58.2KiB  regex_syntax     regex_syntax::ast::parse::ParserI::p...
#  1.5%   2.7%  42.1KiB  accel_diag         accel_diag::vendor::parse_smi_output
#  ...

# 按 crate 显示(哪个依赖最占空间)
cargo bloat --release --crates
# 输出示例:
#  文件    .text     大小 Crate
# 12.3%  22.1%  340KiB serde_json
#  8.7%  15.6%  240KiB regex
#  6.2%  11.1%  170KiB std
#  5.1%   9.2%  141KiB accel_diag
#  ...

# 对比两次构建(优化前后的变化)
cargo bloat --release --crates > before.txt
# ... 进行修改 ...
cargo bloat --release --crates > after.txt
diff before.txt after.txt

常见的代码膨胀来源及修复方法:

膨胀来源典型体积修复方法
regex (全量引擎)200-400 KB如果不需要 Unicode 支持,改用 regex-lite
serde_json (全量)200-350 KB如果注重极致性能,考虑 simd-json 或 sonic-rs
泛型实例化 (Monomorphization)可变在 API 边界处使用 dyn Trait
格式化机制 (Display, Debug)50-150 KB对大型枚举使用 #[derive(Debug)] 会累积不少体积
Panic 错误字符串20-80 KBpanic = "abort" 移除回溯逻辑,strip 移除字符串
未使用的特性 (Features)可变禁用默认特性:serde = { version = "1", default-features = false }

使用 cargo-udeps 清理冗余依赖

cargo-udeps 会找出 Cargo.toml 中声明了但代码中并未实际使用的依赖:

# 安装 (需要 nightly)
cargo install cargo-udeps

# 查找未使用的依赖
cargo +nightly udeps --workspace
# 输出示例:
# 未使用的依赖:
# `diag_tool v0.1.0`
# └── "tempfile" (dev-dependency)
#
# `accel_diag v0.1.0`
# └── "once_cell"    ← 之前需要,现在有了 LazyLock 变成了冗余

每一个未使用的依赖都会:

  • 增加编译时间
  • 增加二进制体积
  • 增加供应链风险
  • 增加潜在的许可证合规复杂性

另一种选择:cargo-machete — 速度更快,基于启发式算法:

cargo install cargo-machete
cargo machete
# 速度快,但由于是单纯的静态扫描,可能会有误报

另一种选择:cargo-shear — 介于 cargo-udeps 和 cargo-machete 之间的平衡点:

cargo install cargo-shear
cargo shear --fix
# 比 machete 慢但比 udeps 快得多
# 误报率远低于 machete

体积优化决策树

flowchart TD
    START["二进制文件太大?"] --> STRIP{"是否设置了 strip = true?"}
    STRIP -->|"否"| DO_STRIP["添加 strip = true\n体积减少 -50% 到 -70%"]
    STRIP -->|"是"| LTO{"是否启用了 LTO?"}
    LTO -->|"否"| DO_LTO["添加 lto = true\n以及 codegen-units = 1"]
    LTO -->|"是"| BLOAT["运行 cargo-bloat\n--crates 查看明细"]
    BLOAT --> BIG_DEP{"是否有超大的依赖?"}
    BIG_DEP -->|"是"| REPLACE["替换为轻量级方案\n或禁用默认特性"]
    BIG_DEP -->|"否"| UDEPS["使用 cargo-udeps\n移除未使用的依赖"]
    UDEPS --> OPT_LEVEL{"还需要更小?"}
    OPT_LEVEL -->|"是"| SIZE_OPT["设置 opt-level = 's' 或 'z'"]

    style DO_STRIP fill:#91e5a3,color:#000
    style DO_LTO fill:#e3f2fd,color:#000
    style REPLACE fill:#ffd43b,color:#000
    style SIZE_OPT fill:#ff6b6b,color:#000

🏋️ 练习

🟢 练习 1:测量 LTO 的影响

先使用默认的编译配置构建项目,然后再尝试添加 lto = true + codegen-units = 1 + strip = true。对比二进制文件的体积和编译时间。

答案
# 默认配置构建
cargo build --release
ls -lh target/release/my-binary
time cargo build --release  # 记录时间

# 优化后的配置 —— 在 Cargo.toml 中添加:
# [profile.release]
# lto = true
# codegen-units = 1
# strip = true
# panic = "abort"

cargo clean
cargo build --release
ls -lh target/release/my-binary  # 通常会减少 30-50% 的体积
time cargo build --release       # 编译时间通常会增加 2-3 倍

🟡 练习 2:找出你项目中最大的 Crate

在项目上运行 cargo bloat --release --crates。识别出最大的依赖项。你能否通过禁用默认特性或切换到轻量级替代方案来减小它的体积?

答案
cargo install cargo-bloat
cargo bloat --release --crates
# 输出示例:
#  文件    .text     大小 Crate
# 12.3%  22.1%  340KiB serde_json
#  8.7%  15.6%  240KiB regex

# 针对 regex — 如果不需要 Unicode 尝试 regex-lite:
# regex-lite = "0.1"  # 比全量 regex 小约 10 倍

# 针对 serde — 如果不需要标准库则禁用默认特性:
# serde = { version = "1", default-features = false, features = ["derive"] }

cargo bloat --release --crates  # 修改后再次对比

关键收获

  • lto = true + codegen-units = 1 + strip = true + panic = "abort" 是生产环境发布版的理想配置。
  • Thin LTO (lto = "thin") 仅需较小的编译成本,即可获得 Fat LTO 约 80% 的收益。
  • cargo-bloat --crates 能清晰展示哪些依赖在吞噬二进制空间。
  • cargo-udeps、cargo-machete 和 cargo-shear 能帮你清理浪费编译时间且增加体积的沉余依赖。
  • 通过针对单个 crate 的配置覆盖 (Profile Overwrites),可以在不拖慢整体构建的前提下优化性能热点。

English Original

编译期与开发工具 🟡

你将学到:

  • 使用 sccache 为本地和 CI 构建加速
  • 使用 mold 实现更快的链接(比默认链接器快 3-10 倍)
  • cargo-nextest:一个更快、信息更丰富的测试运行器
  • 开发者生产力工具:cargo-expand、cargo-geiger、cargo-watch
  • 配置工作区 Lint (Lints)、MSRV 策略以及文档即 CI

相关章节: 发布配置 — LTO 和二进制体积优化 · CI/CD 流水线 — 这些工具在流水线中的集成 · 依赖管理 — 减少依赖 = 编译更快

编译期优化:sccache, mold, cargo-nextest

漫长的编译时间是 Rust 开发者最大的痛点。以下工具组合可以减少 50-80% 的迭代延迟:

sccache — 共享编译缓存:

# 安装
cargo install sccache

# 配置为 Rust 封装器 (Wrapper)
export RUSTC_WRAPPER=sccache

# 或者在 .cargo/config.toml 中永久设置:
# [build]
# rustc-wrapper = "sccache"

# 第一次构建:正常速度(填充缓存)
cargo build --release  # 耗时 3 分钟

# 执行清理并重新构建:命中未改变的 crate 缓存
cargo clean && cargo build --release  # 耗时 45 秒

# 检查缓存统计信息
sccache --show-stats
# 编译请求数            1,234
# 缓存命中数             987 (80%)
# 缓存未命中数           247

sccache 支持共享缓存(S3, GCS, Azure Blob),适用于团队协作和 CI 环境下的缓存共享。

mold — 更快的链接器:

链接通常是编译过程中最慢的阶段。mold 比 lld 快 3-5 倍,比默认的 GNU ld 快 10-20 倍:

# 安装
sudo apt install mold  # Ubuntu 22.04+
# 注意:mold 针对 ELF 目标 (Linux)。macOS 使用的是 Mach-O 而非 ELF。
# macOS 的链接器 (ld64) 本身已经很快;如果你追求更快:
# brew install sold     # sold = 针对 Mach-O 的 mold (实验性)
# 在实践中,macOS 的链接时间很少成为瓶颈。
# 在 .cargo/config.toml 中启用 mold 链接器
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# 验证是否正在使用 mold
cargo build -v 2>&1 | grep mold

cargo-nextest — 更快的测试运行器:

# 安装
cargo install cargo-nextest

# 运行测试(默认并行,对每个测试设置超时及重试)
cargo nextest run

# 相比 cargo test 的关键优势:
# - 每个测试都在独立的进程中运行 → 隔离性更好
# - 带有智能调度的并行执行
# - 每个测试都有超时控制(CI 不会再卡死)
# - 导出 JUnit XML 格式供 CI 使用
# - 可自动重试失败的测试

# 常用配置
cargo nextest run --retries 2 --fail-fast

# 归档测试二进制文件(适用于 CI:构建一次,在多台机器上测试)
cargo nextest archive --archive-file tests.tar.zst
cargo nextest run --archive-file tests.tar.zst
# .config/nextest.toml
[profile.default]
retries = 0
slow-timeout = { period = "60s", terminate-after = 3 }
fail-fast = true

[profile.ci]
retries = 2
fail-fast = false
junit = { path = "test-results.xml" }

综合开发环境配置示例:

# .cargo/config.toml — 优化开发迭代闭环
[build]
rustc-wrapper = "sccache"       # 缓存编译产物

[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]  # 更快的链接

# 开发配置 (Dev profile):优化依赖但不优化自己的代码
# (放入 Cargo.toml)
# [profile.dev.package."*"]
# opt-level = 2

cargo-expand 与 cargo-geiger — 可见性工具

cargo-expand — 查看宏生成的代码:

cargo install cargo-expand

# 展开特定模块中的所有宏
cargo expand --lib accel_diag::vendor

# 展开特定的派生宏
# 假设有:#[derive(Debug, Serialize, Deserialize)]
# cargo expand 将显示生成的 impl 代码块
cargo expand --lib --tests

这对于调试 #[derive] 宏输出、macro_rules! 展开以及理解 serde 为你的类型生成了什么非常有价值。

除了 cargo-expand,你也可以直接在 rust-analyzer 中展开宏:

  1. 将光标移动到目标宏上。
  2. 打开命令面板 (VSCode 中为 F1)。
  3. 搜索 rust-analyzer: Expand macro recursively at caret。

cargo-geiger — 统计依赖树中的 unsafe 使用情况:

cargo install cargo-geiger

cargo geiger
# 输出示例:
# 指标格式:x/y
#   x = 构建实际使用的 unsafe 代码
#   y = crate 中发现的总 unsafe 代码
#
# 函数       表达式       Impls  Traits  方法
# 0/0        0/0          0/0    0/0     0/0      ✅ my_crate
# 0/5        0/23         0/2    0/0     0/3      ✅ serde
# 3/3        14/14        0/0    0/0     2/2      ❗ libc
# 15/15      142/142      4/4    0/0     12/12    ☢️ ring

# 符号含义:
# ✅ = 未使用 unsafe
# ❗ = 使用了少量 unsafe
# ☢️ = 重度使用 unsafe

针对本项目的“零 unsafe”政策,cargo geiger 可以验证是否没有任何依赖项在你实际执行的代码路径中引入了 unsafe 代码。

工作区 Lint 配置 — [workspace.lints]

自 Rust 1.74 起,你可以在 Cargo.toml 中统一配置 Clippy 和编译器 Lint —— 不必再在每个 crate 顶层写 #![deny(...)]:

# 根目录 Cargo.toml — 为所有 crate 配置 lint
[workspace.lints.clippy]
unwrap_used = "warn"         # 倾向于使用 ? 或 expect("原因")
dbg_macro = "deny"           # 禁止在提交的代码中包含 dbg!()
todo = "warn"                # 跟踪未完成的实现
large_enum_variant = "warn"  # 发现意外的体积膨胀

[workspace.lints.rust]
unsafe_code = "deny"         # 强制执行零 unsafe 政策
missing_docs = "warn"        # 鼓励编写文档
# 每个 crate 的 Cargo.toml — 选用工作区 lint 配置
[lints]
workspace = true

这取代了零散的 #![deny(clippy::unwrap_used)] 属性,并确保整个工作区的政策一致。

自动修复 Clippy 警告:

# 让 Clippy 自动应用可机器修复的建议
cargo clippy --fix --workspace --all-targets --allow-dirty

# 修复并应用那些可能改变行为的建议(需仔细审核!)
cargo clippy --fix --workspace --all-targets --allow-dirty -- -W clippy::pedantic

提示:在提交代码前运行 cargo clippy --fix。它可以处理大部分琐碎问题(如未使用导入、冗余克隆、类型简化),节省手动修改的时间。

MSRV 政策与 rust-version

最低支持 Rust 版本 (Minimum Supported Rust Version, MSRV) 确保你的 crate 可以在较旧的工具链上编译。这在部署至 Rust 版本固定的系统时非常重要。

# Cargo.toml
[package]
name = "diag_tool"
version = "0.1.0"
rust-version = "1.75"    # 所需的最低 Rust 版本
# 验证 MSRV 兼容性
cargo +1.75.0 check --workspace

# 自动探测 MSRV
cargo install cargo-msrv
cargo msrv find
# 输出示例:Minimum Supported Rust Version is 1.75.0

# 在 CI 中验证
cargo msrv verify

MSRV 策略建议:

  • 二进制应用程序 (如本项目):使用最新稳定版。通常不需要刻意设置过旧的 MSRV。
  • 库类型 crate (发布到 crates.io):将 MSRV 设置为支持你所用特性的最旧 Rust 版本。通常采取 N-2 策略(比当前版本落后 2 个版本)。
  • 企业部署:设置 MSRV 以匹配你服务器集群中安装的最旧 Rust 版本。

应用:生产环境二进制配置

本项目已经拥有一个出色的 发布配置 (release profile):

# 根目录 Cargo.toml
[profile.release]
lto = true           # ✅ 开启全量跨 crate 优化
codegen-units = 1    # ✅ 最大程度优化
panic = "abort"      # ✅ 移除回溯开销
strip = true         # ✅ 部署时移除符号表

[profile.dev]
opt-level = 0        # ✅ 快速编译
debug = true         # ✅ 完整的调试信息

进一步建议:

# 在开发模式下优化依赖项(加快测试执行速度)
[profile.dev.package."*"]
opt-level = 2

# 测试配置:进行基础优化,防止慢速测试超时
[profile.test]
opt-level = 1

# 在发布版中保留溢出检查(出于安全考虑)
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
overflow-checks = true    # ← 建议添加:捕捉整数溢出
debug = "line-tables-only" # ← 建议添加:保留用于回溯的行表,压缩体积

建议的开发者工具配置:

# .cargo/config.toml (建议)
[build]
rustc-wrapper = "sccache"  # 首次构建后缓存命中率可达 80%+

[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]  # 链接速度提升 3-5 倍

预期收益:

指标当前优化后
发布版二进制体积约 10 MB (已 strip, 开启 LTO)保持一致
开发环境构建耗时约 45s约 25s (sccache + mold)
重新构建 (修改 1 个文件)约 15s约 5s (sccache + mold)
测试执行cargo testcargo nextest — 快 2 倍
依赖漏洞扫描无CI 中使用 cargo audit
许可证合规检查手动CI 中使用 cargo deny 自动化
冗余依赖检测手动CI 中使用 cargo udeps

cargo-watch — 文件变更自动重新构建

cargo-watch 在源文件发生变化时重新运行命令 —— 是实现快速反馈环的关键:

# 安装
cargo install cargo-watch

# 每次保存时自动检查(即时反馈)
cargo watch -x check

# 变更后运行 clippy + 测试
cargo watch -x 'clippy --workspace --all-targets' -x 'test --workspace --lib'

# 仅监听特定的 crate(大型工作区中速度更快)
cargo watch -w accel_diag/src -x 'test -p accel_diag'

# 每次运行前清屏
cargo watch -c -x check

提示:将其与上述的 mold 和 sccache 结合使用,可以在增量修改后实现亚秒级的重新检查。

cargo doc 与工作区文档

对于大型工作区,生成的文档对于代码的可维护性和上手门槛至关重要。cargo doc 使用 rustdoc 从文档注释和类型签名中生成 HTML 文档:

# 为所有工作区 crate 生成文档 (自动在浏览器打开)
cargo doc --workspace --no-deps --open

# 包含私有项 (开发调试时非常有用)
cargo doc --workspace --no-deps --document-private-items

# 仅检查文档链接是否损坏 (快速 CI 检查)
cargo doc --workspace --no-deps 2>&1 | grep -E 'warning|error'

文档内引用 (Intra-doc links) — 无需 URL 即可在跨 crate 的类型间建立链接:

#![allow(unused)]
fn main() {
/// 使用 [`GpuConfig`] 设置运行 GPU 诊断。
///
/// 具体实现请参考 [`crate::accel_diag::run_diagnostics`].
/// 返回 [`DiagResult`],可序列化为
/// [`DerReport`](crate::core_lib::DerReport) 格式。
pub fn run_accel_diag(config: &GpuConfig) -> DiagResult {
    // ...
}
}

在文档中展示平台特定的 API:

#![allow(unused)]
fn main() {
// Cargo.toml: [package.metadata.docs.rs]
// all-features = true
// rustdoc-args = ["--cfg", "docsrs"]

/// 仅限 Windows:通过 Win32 API 读取电池状态。
///
/// 仅可在 `cfg(windows)` 环境下使用。
#[cfg(windows)]
#[doc(cfg(windows))]  // 在文档中显示“仅在 Windows 可用”的徽章
pub fn get_battery_status() -> Option<u8> {
    // ...
}
}

CI 中的文档检查:

# 添加到 CI 工作流
- name: Check documentation
  run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
  # 将损坏的文档链接视为错误

针对本项目:由于包含多个 crate,cargo doc --workspace 是新成员快速了解 API 全貌的最佳方式。建议将 RUSTDOCFLAGS="-D warnings" 加入 CI,以便在合并前拦截损坏的文档链接。

编译期优化决策树

flowchart TD
    START["编译太慢?"] --> WHERE{"瓶颈在哪里?"}

    WHERE -->|"重新编译\n未修改的 crate"| SCCACHE["sccache\n共享编译缓存"]
    WHERE -->|"链接阶段"| MOLD["mold 链接器\n速度提升 3-10 倍"]
    WHERE -->|"执行测试"| NEXTEST["cargo-nextest\n并行测试运行器"]
    WHERE -->|"全方位加速"| COMBO["上述全部工具 +\n使用 cargo-udeps 清理依赖"]

    SCCACHE --> CI_CACHE{"CI 还是本地?"}
    CI_CACHE -->|"CI"| S3["S3/GCS 共享缓存"]
    CI_CACHE -->|"本地"| LOCAL["自动配置的本地磁盘缓存"]

    style SCCACHE fill:#91e5a3,color:#000
    style MOLD fill:#e3f2fd,color:#000
    style NEXTEST fill:#ffd43b,color:#000
    style COMBO fill:#b39ddb,color:#000

🏋️ 练习

🟢 练习 1:配置 sccache + mold

安装 sccache 和 mold,在 .cargo/config.toml 中配置它们,然后测量干净构建 (clean rebuild) 时的编译提速。

答案
# 安装
cargo install sccache
sudo apt install mold  # Ubuntu 22.04+

# 配置 .cargo/config.toml
cat > .cargo/config.toml << 'EOF'
[build]
rustc-wrapper = "sccache"

[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
EOF

# 第一次构建(填充缓存)
time cargo build --release  # 例如 180s

# 执行清理并重新构建(命中缓存)
cargo clean
time cargo build --release  # 例如 45s

sccache --show-stats
# 缓存命中率应在 60-80% 以上

🟡 练习 2:切换到 cargo-nextest

安装 cargo-nextest 并运行你的测试套件。对比其与 cargo test 的墙钟时间。提速效果如何?

答案
cargo install cargo-nextest

# 标准测试运行器
time cargo test --workspace 2>&1 | tail -5

# nextest (并行跨二进制文件执行)
time cargo nextest run --workspace 2>&1 | tail -5

# 对于大型工作区,提速通常在 2-5 倍。
# nextest 还提供:
# - 每个测试的耗时统计
# - 对不稳定的测试进行重试
# - 导出供 CI 使用的 JUnit XML 
cargo nextest run --workspace --retries 2

关键收获

  • 配合 S3/GCS 后端的 sccache 可以跨团队和 CI 共享编译缓存。
  • mold 是目前最快的 ELF 链接器 —— 链接时间可从秒级降至毫秒级。
  • cargo-nextest 并行运行测试二进制文件,并提供更好的输出支持及重试机制。
  • cargo-geiger 用于统计 unsafe 使用量 —— 在引入新依赖前建议先以此检查。
  • [workspace.lints] 可以在大型工作区顶层统一管理 Clippy 和 rustc 的 lint 规则。

English Original

no_std 与特性验证 🔴

你将学到:

  • 使用 cargo-hack 系统地验证特性 (Feature) 组合
  • Rust 的三个层面:core vs alloc vs std 以及各自的适用场景
  • 使用自定义 panic 处理程序和分配器构建 no_std crate
  • 在宿主机和 QEMU 上测试 no_std 代码

相关章节: Windows 与条件编译 — 该话题的另一半:平台相关性 · 交叉编译 — 交叉编译到 ARM 和嵌入式目标 · Miri 与 Sanitizer — 验证 no_std 环境中的 unsafe 代码 · 构建脚本 — 由 build.rs 发出的 cfg 标志

从 8 位微控制器到云端服务器,Rust 几乎运行在任何地方。本章涵盖了基础知识:通过 #![no_std] 剥离标准库,并验证你的特性组合是否能实际通过编译。

使用 cargo-hack 验证特性组合

cargo-hack 能够系统地测试所有特性组合 —— 这对于包含 #[cfg(...)] 代码的 crate 来说至关重要:

# 安装
cargo install cargo-hack

# 检查每个特性是否都能独立通过编译
cargo hack check --each-feature --workspace

# 终极方案:测试所有特性组合(指数级增长!)
# 仅建议在特性数少于 8 个的 crate 上运行。
cargo hack check --feature-powerset --workspace

# 务实的折衷方案:分别测试每个特性运行 + 全部特性运行 + 无特性运行
cargo hack check --each-feature --workspace --no-dev-deps
cargo check --workspace --all-features
cargo check --workspace --no-default-features

为什么这对于本项目很重要:

如果你添加了平台特性(如 linux、windows、direct-ipmi、direct-accel-api),cargo-hack 能及时捕捉到会导致构建失败的组合:

# 示例:控制平台代码的特性
[features]
default = ["linux"]
linux = []                          # Linux 特有的硬件访问
windows = ["dep:windows-sys"]       # Windows 特有的 API
direct-ipmi = []                    # 使用 unsafe 的 IPMI ioctl (见第5章)
direct-accel-api = []                    # 使用 unsafe 的 accel-mgmt FFI (见第5章)
# 验证所有特性在隔离以及组合情况下均能编译通过
cargo hack check --each-feature -p diag_tool
# 捕捉错误:“feature 'windows' doesn't compile without 'direct-ipmi'”
# 捕捉错误:“#[cfg(feature = "linux")] 拼写错误 — 误写成了 'lnux'”

CI 集成:

# 添加到 CI 流水线 (由于仅执行编译检查,速度很快)
- name: Feature matrix check
  run: cargo hack check --each-feature --workspace --no-dev-deps

经验法则:对于任何拥有 2 个以上特性的 crate,建议在 CI 中运行 cargo hack check --each-feature。对于少于 8 个特性的核心库 crate,才运行 --feature-powerset —— 它是指数级的($2^n$ 种组合)。

no_std — 何时以及为何使用

#![no_std] 告诉编译器:“不要链接标准库。” 你的 crate 将只能使用 core(以及可选的 alloc)。为什么要这么做?

场景为什么选择 no_std
嵌入式固件 (ARM Cortex-M, RISC-V)无操作系统、无堆内存、无文件系统
UEFI 诊断工具预启环境,无操作系统 API
内核模块内核空间无法使用用户空间的 std
WebAssembly (WASM)最小化二进制体积,无操作系统依赖
引导程序 (Bootloaders)在任何操作系统加载前运行
带有 C 接口的共享库避免在调用者中引入 Rust 运行时

对于硬件诊断工具,当构建以下内容时,no_std 变得非常相关:

  • 基于 UEFI 的预启诊断工具(在操作系统加载前运行)
  • BMC 固件诊断(资源受限的 ARM SoC)
  • 内核级 PCIe 诊断(内核模块或 eBPF 探针)

core vs alloc vs std — 三个层面

┌─────────────────────────────────────────────────────────────┐
│ std                                                         │
│  包含 core + alloc 的所有内容,外加:                         │
│  • 文件 I/O (std::fs, std::io)                              │
│  • 网络 (std::net)                                          │
│  • 线程 (std::thread)                                       │
│  • 时间 (std::time)                                         │
│  • 环境变量 (std::env)                                       │
│  • 进程 (std::process)                                      │
│  • 操作系统特定 (std::os::unix, std::os::windows)            │
├─────────────────────────────────────────────────────────────┤
│ alloc          (在 #![no_std] 下可用,需手动声明 extern crate│
│                 alloc,且必须配置有全局分配器)                │
│  • String, Vec, Box, Rc, Arc                                │
│  • BTreeMap, BTreeSet                                       │
│  • format!() 宏                                             │
│  • 任何需要堆空间的代码集合与智能指针                         │
├─────────────────────────────────────────────────────────────┤
│ core           (即使在 #![no_std] 下也总是可用)              │
│  • 基础类型 (u8, bool, char 等)                              │
│  • Option, Result                                           │
│  • 迭代器、切片、数组、str (指 slice,而非 String)           │
│  • Trait: Clone, Copy, Debug, Display, From, Into           │
│  • 原子操作 (core::sync::atomic)                             │
│  • Cell, RefCell (core::cell) —— Pin (core::pin)            │
│  • core::fmt (无需分配空间的格式化)                          │
│  • core::mem, core::ptr (低层内存操作)                        │
│  • 数学运算: core::num, 基础算术                             │
└─────────────────────────────────────────────────────────────┘

失去 std 会带来什么影响:

  • 没有 HashMap(需要哈希器 —— 改用 alloc 中的 BTreeMap 或 hashbrown)
  • 没有 println!()(需要标准输出 —— 改用 core::fmt::Write 写入缓冲区)
  • 没有 std::error::Error(自 Rust 1.81 起已在 core 中稳定,但许多生态库尚未迁移)
  • 没有文件 I/O、无网络、无线程(除非由平台 HAL 提供)
  • 没有 Mutex(改用 spin::Mutex 或平台特有的锁)

构建一个 no_std Crate

#![allow(unused)]
fn main() {
// src/lib.rs — 一个 no_std 库 crate
#![no_std]

// 可选:使用堆分配 (heap allocation)
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;

/// 从热传感器读取的温度。
/// 该结构体适用于任何环境 —— 从裸机到 Linux。
#[derive(Clone, Copy, Debug)]
pub struct Temperature {
    /// 原始传感器值(对于典型的 I2C 传感器,每 LSB 为 0.0625°C)
    raw: u16,
}

impl Temperature {
    pub const fn from_raw(raw: u16) -> Self {
        Self { raw }
    }

    /// 转换为摄氏度(定点运算,无需 FPU)
    pub const fn millidegrees_c(&self) -> i32 {
        (self.raw as i32) * 625 / 10 // 0.0625°C 分辨率
    }

    pub fn degrees_c(&self) -> f32 {
        self.raw as f32 * 0.0625
    }
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let md = self.millidegrees_c();
        // 处理 -0.999°C 到 -0.001°C 之间的符号显示
        // 此时 md / 1000 == 0,但数值实际上是负数。
        if md < 0 && md > -1000 {
            write!(f, "-0.{:03}°C", (-md) % 1000)
        } else {
            write!(f, "{}.{:03}°C", md / 1000, (md % 1000).abs())
        }
    }
}

/// 解析以空格分隔的温度值。
/// 使用了 alloc —— 需要全局分配器。
pub fn parse_temperatures(input: &str) -> Vec<Temperature> {
    input
        .split_whitespace()
        .filter_map(|s| s.parse::<u16>().ok())
        .map(Temperature::from_raw)
        .collect()
}

/// 无需分配空间的格式化 —— 直接写入缓冲区。
/// 适用于仅 `core` 的环境(无 alloc,无堆)。
pub fn format_temp_into(temp: &Temperature, buf: &mut [u8]) -> usize {
    use core::fmt::Write;
    struct SliceWriter<'a> {
        buf: &'a mut [u8],
        pos: usize,
    }
    impl<'a> Write for SliceWriter<'a> {
        fn write_str(&mut self, s: &str) -> fmt::Result {
            let bytes = s.as_bytes();
            let remaining = self.buf.len() - self.pos;
            if bytes.len() > remaining {
                // 缓冲区已满 —— 返回错误而非静默截断
                return Err(fmt::Error);
            }
            self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes);
            self.pos += bytes.len();
            Ok(())
        }
    }
    let mut w = SliceWriter { buf, pos: 0 };
    let _ = write!(w, "{}", temp);
    w.pos
}
}
# no_std crate 的 Cargo.toml
[package]
name = "thermal-sensor"
version = "0.1.0"
edition = "2021"

[features]
default = ["alloc"]
alloc = []    # 启用 Vec, String 等
std = []      # 启用完整的 std (隐含启用 alloc)

[dependencies]
# 使用兼容 no_std 的 crate
serde = { version = "1.0", default-features = false, features = ["derive"] }
# ↑ default-features = false 移除了对 std 的依赖!

核心 Crate 模式:许多流行 crate(如 serde, log, rand, embedded-hal)通过 default-features = false 支持 no_std。在 no_std 上下文中使用依赖项之前,请务必检查其是否强制要求 std。请注意,某些 crate(如 regex)至少需要 alloc,无法在仅 core 的环境下运行。

自定义 Panic 处理程序与分配器

在 #![no_std] 二进制文件(而非库)中,你必须提供 panic 处理程序,并可选地提供全局分配器:

// src/main.rs — 一个 no_std 二进制文件 (例如 UEFI 诊断程序)
#![no_std]
#![no_main]

extern crate alloc;

use core::panic::PanicInfo;

// 必需:panic 时的处理逻辑(此时无法执行栈回溯)
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    // 嵌入式环境下:闪烁 LED、写入串口、由于死循环挂起
    // UEFI 环境下:打印到控制台、停机
    // 最简方案:无限循环
    loop {
        core::hint::spin_loop();
    }
}

// 如果使用了 alloc 则必需:提供全局分配器
use alloc::alloc::{GlobalAlloc, Layout};

struct BumpAllocator {
    // 适用于嵌入式/UEFI 的简单线性分配器 (Bump Allocator)
    // 实践中,建议使用 `linked_list_allocator` 或 `embedded-alloc` 等 crate
}

// 警告:这只是一个非功能的占位符!调用 alloc() 将返回空指针,
// 从而导致立即触发 UB(全局分配器契约要求对非零字节的分配返回非空指针)。
// 在实际代码中,请使用成熟的分配器 crate:
//   - embedded-alloc (针对嵌入式目标)
//   - linked_list_allocator (针对 UEFI / 内核)
//   - talc (通用的 no_std 分配器)
unsafe impl GlobalAlloc for BumpAllocator {
    unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
        // 占位符 —— 会导致崩溃!请替换为真实的分配逻辑。
        core::ptr::null_mut()
    }
    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
        // 线性分配器通常不执行释放操作
    }
}

#[global_allocator]
static ALLOCATOR: BumpAllocator = BumpAllocator {};

// 入口点 (平台相关,而非简单的 fn main)
// 针对 UEFI: #[entry] 或 efi_main
// 针对嵌入式: #[cortex_m_rt::entry]

测试 no_std 代码

测试是在宿主机上运行的,而宿主机拥有 std。技巧在于:你的库虽然是 no_std 的,但你的测试运行器使用的是 std:

#![allow(unused)]
fn main() {
// 你的 crate: 在 src/lib.rs 中声明 #![no_std]
// 但测试会自动在 std 下运行:

#[cfg(test)]
mod tests {
    use super::*;
    // 此处 std 可用 —— println!, assert!, Vec 等均可正常工作

    #[test]
    fn test_temperature_conversion() {
        let temp = Temperature::from_raw(800); // 50.0°C
        assert_eq!(temp.millidegrees_c(), 50000);
        assert!((temp.degrees_c() - 50.0).abs() < 0.01);
    }

    #[test]
    fn test_format_into_buffer() {
        let temp = Temperature::from_raw(800);
        let mut buf = [0u8; 32];
        let len = format_temp_into(&temp, &mut buf);
        let s = core::str::from_utf8(&buf[..len]).unwrap();
        assert_eq!(s, "50.000°C");
    }
}
}

在实际目标上测试(当完全没有 std 可用时):

# 使用 defmt-test 进行片上测试 (嵌入式 ARM)
# 使用 uefi-test-runner 进行 UEFI 目标测试
# 使用 QEMU 进行跨架构测试(无需真实硬件)

# 在宿主机上运行 no_std 库测试(始终可行):
cargo test --lib

# 针对 no_std 目标验证 no_std 编译情况:
cargo check --target thumbv7em-none-eabihf  # ARM Cortex-M
cargo check --target riscv32imac-unknown-none-elf  # RISC-V

no_std 决策树

flowchart TD
    START["代码是否需要\n标准库?"] --> NEED_FS{"文件系统、\n网络、线程?"}
    NEED_FS -->|"是"| USE_STD["使用 std\n开发普通应用"]
    NEED_FS -->|"否"| NEED_HEAP{"是否需要堆分配?\nVec, String, Box"}
    NEED_HEAP -->|"是"| USE_ALLOC["#![no_std]\nextern crate alloc"]
    NEED_HEAP -->|"否"| USE_CORE["#![no_std]\n仅使用 core"]
    
    USE_ALLOC --> VERIFY["使用 cargo-hack\n验证特性组合"]
    USE_CORE --> VERIFY
    USE_STD --> VERIFY
    VERIFY --> TARGET{"目标是否有 OS?"}
    TARGET -->|"是"| HOST_TEST["cargo test --lib\n标准测试流程"]
    TARGET -->|"否"| CROSS_TEST["QEMU / defmt-test\n目标设备实战测试"]
    
    style USE_STD fill:#91e5a3,color:#000
    style USE_ALLOC fill:#ffd43b,color:#000
    style USE_CORE fill:#ff6b6b,color:#000

🏋️ 练习

🟡 练习 1:特性组合验证

安装 cargo-hack 并对一个包含多个特性的项目运行 cargo hack check --each-feature --workspace。它是否发现了失效的组合?

答案
cargo install cargo-hack

# 分别检查每个特性
cargo hack check --each-feature --workspace --no-dev-deps

# 如果某个特性组合报错:
# error[E0433]: failed to resolve: use of undeclared crate or module `std`
# → 这意味着某个特性开关处缺少了 #[cfg] 保护。

# 检查全部特性开启 + 零特性开启 + 逐个特性开启:
cargo hack check --each-feature --workspace
cargo check --workspace --all-features
cargo check --workspace --no-default-features

🔴 练习 2:构建 no_std 库

创建一个能够使用 #![no_std] 编译的库 crate。实现一个简单的栈分配环形缓冲区 (Ring Buffer)。验证其可以针对 thumbv7em-none-eabihf (ARM Cortex-M) 通过编译。

答案
#![allow(unused)]
fn main() {
// lib.rs
#![no_std]

pub struct RingBuffer<const N: usize> {
    data: [u8; N],
    head: usize,
    len: usize,
}

impl<const N: usize> RingBuffer<N> {
    pub const fn new() -> Self {
        Self { data: [0; N], head: 0, len: 0 }
    }

    pub fn push(&mut self, byte: u8) -> bool {
        if self.len == N { return false; }
        let idx = (self.head + self.len) % N;
        self.data[idx] = byte;
        self.len += 1;
        true
    }

    pub fn pop(&mut self) -> Option<u8> {
        if self.len == 0 { return None; }
        let byte = self.data[self.head];
        self.head = (self.head + 1) % N;
        self.len -= 1;
        Some(byte)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn push_pop() {
        let mut rb = RingBuffer::<4>::new();
        assert!(rb.push(1));
        assert!(rb.push(2));
        assert_eq!(rb.pop(), Some(1));
        assert_eq!(rb.pop(), Some(2));
        assert_eq!(rb.pop(), None);
    }
}
}
rustup target add thumbv7em-none-eabihf
cargo check --target thumbv7em-none-eabihf
# ✅ 针对裸机 ARM 的编译通过

关键收获

  • cargo-hack --each-feature 对于任何包含条件编译的 crate 来说都是必需的 —— 建议在 CI 中运行。
  • core → alloc → std 是分层架构:每一层都增加了功能,但同时也需要更多的运行时支持。
  • 裸机 #![no_std] 二进制文件需要自定义 panic 处理程序和分配器。
  • 使用 cargo test --lib 在宿主机上测试 no_std 库 —— 通常不需要真实硬件。
  • 仅对特性数少于 8 个的核心库运行 --feature-powerset —— 指数级组合 ($2^n$) 会导致构建爆炸。

English Original

Windows 与条件编译 🟡

你将学到:

  • Windows 支持模式:windows-sys/windows crate 以及 cargo-xwin
  • 使用 #[cfg] 进行条件编译 —— 由编译器检查,而非预处理器
  • 平台抽象架构:何时使用 #[cfg] 块就足够,何时需要使用 Trait
  • 如何在 Linux 上为 Windows 进行交叉编译

相关章节: no_std 与特性验证 — cargo-hack 与特性验证 · 交叉编译 — 通用的交叉构建设置 · 构建脚本 — 由 build.rs 发出的 cfg 标志

Windows 支持 — 平台抽象

Rust 的 #[cfg()] 属性和 Cargo 特性 (Features) 使得单一代码库能够整洁地支持 Linux 和 Windows。本项目已在 platform::run_command 中展示了这种模式:

#![allow(unused)]
fn main() {
// 本项目中的真实模式 —— 平台特定的 shell 调用
pub fn exec_cmd(cmd: &str, timeout_secs: Option<u64>) -> Result<CommandResult, CommandError> {
    #[cfg(windows)]
    let mut child = Command::new("cmd")
        .args(["/C", cmd])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;

    #[cfg(not(windows))]
    let mut child = Command::new("sh")
        .args(["-c", cmd])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;

    // ... 后续部分是平台无关的 ...
}
}

可用的 cfg 断言:

#![allow(unused)]
fn main() {
// 操作系统
#[cfg(target_os = "linux")]         // 仅限 Linux
#[cfg(target_os = "windows")]       // 仅限 Windows
#[cfg(target_os = "macos")]         // 仅限 macOS
#[cfg(unix)]                        // Linux, macOS, BSDs 等
#[cfg(windows)]                     // Windows (简写)

// 架构
#[cfg(target_arch = "x86_64")]      // x86 64 位
#[cfg(target_arch = "aarch64")]     // ARM 64 位
#[cfg(target_arch = "x86")]         // x86 32 位

// 指针宽度(架构无关的替代方案)
#[cfg(target_pointer_width = "64")] // 任何 64 位平台
#[cfg(target_pointer_width = "32")] // 任何 32 位平台

// 环境 / C 库
#[cfg(target_env = "gnu")]          // glibc
#[cfg(target_env = "musl")]         // musl libc
#[cfg(target_env = "msvc")]         // Windows 上的 MSVC 

// 字节序
#[cfg(target_endian = "little")]
#[cfg(target_endian = "big")]

// 组合使用 any(), all(), not()
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[cfg(not(windows))]
}

windows-sys 与 windows Crate

用于直接调用 Windows API:

# Cargo.toml — 使用 windows-sys 进行原始 FFI 调用(更轻量,无抽象)
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
    "Win32_Foundation",
    "Win32_System_Services",
    "Win32_System_Registry",
    "Win32_System_Power",
] }
# 注意:windows-sys 的发布不遵循语义化版本兼容 (0.48 → 0.52 → 0.59)。
# 建议锁定到特定的次要版本 —— 每次发布都可能删除或重命名 API 绑定。
# 在开始新项目前,请前往 https://github.com/microsoft/windows-rs 查看最新版本。

# 或者使用 windows crate 以获得安全封装(更重,但更易用)
# windows = { version = "0.59", features = [...] }
#![allow(unused)]
fn main() {
// src/platform/windows.rs
#[cfg(windows)]
mod win {
    use windows_sys::Win32::System::Power::{
        GetSystemPowerStatus, SYSTEM_POWER_STATUS,
    };

    pub fn get_battery_status() -> Option<u8> {
        let mut status = SYSTEM_POWER_STATUS::default();
        // SAFETY: GetSystemPowerStatus 会向提供的缓冲区写入数据。
        // 该缓冲区的大小和对齐方式正确。
        let ok = unsafe { GetSystemPowerStatus(&mut status) };
        if ok != 0 {
            Some(status.BatteryLifePercent)
        } else {
            None
        }
    }
}
}

windows-sys vs windows crate:

维度windows-syswindows
API 风格原始 FFI (unsafe 调用)Safe Rust 封装
二进制体积极小 (仅包含 extern 声明)较大 (包含封装代码)
编译时间快慢
易用性C 风格,需手动维护安全性符合 Rust 习惯
错误处理原始 BOOL / HRESULTResult<T, windows::core::Error>
适用场景性能关键、轻量级封装应用程序代码、追求开发效率

在 Linux 上为 Windows 交叉编译

# 方案 1: MinGW (GNU ABI)
rustup target add x86_64-pc-windows-gnu
sudo apt install gcc-mingw-w64-x86-64
cargo build --target x86_64-pc-windows-gnu
# 生成 .exe 文件 —— 可在 Windows 运行,链接至 msvcrt

# 方案 2: 通过 xwin 编译 MSVC ABI (实现完全的 MSVC 兼容)
cargo install cargo-xwin
cargo xwin build --target x86_64-pc-windows-msvc
# 会自动下载微软的 CRT 和 SDK 头文件

# 方案 3: 基于 Zig 的交叉编译
cargo zigbuild --target x86_64-pc-windows-gnu

Windows 上的 GNU 与 MSVC ABI:

维度x86_64-pc-windows-gnux86_64-pc-windows-msvc
链接器MinGW ldMSVC link.exe 或 lld-link
C 运行时msvcrt.dll (通用)ucrtbase.dll (现代)
C++ 互操作GCC ABIMSVC ABI
在 Linux 交叉编译容易 (MinGW)支持 (cargo-xwin)
Windows API 支持完整完整
调试信息格式DWARFPDB
推荐用于简单工具、CI 构建深度 Windows 集成

条件编译模式

模式 1:平台特定的模块选择

#![allow(unused)]
fn main() {
// src/platform/mod.rs — 为每个 OS 编译不同的模块
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::*;

#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
pub use windows::*;

// 两个模块都实现了相同的公共 API:
// pub fn get_cpu_temperature() -> Result<f64, PlatformError>
// pub fn list_pci_devices() -> Result<Vec<PciDevice>, PlatformError>
}

模式 2:特性门控 (Feature-gated) 的平台支持

# Cargo.toml
[features]
default = ["linux"]
linux = []              # Linux 特有的硬件访问
windows = ["dep:windows-sys"]  # Windows 特有的 API

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [...], optional = true }
#![allow(unused)]
fn main() {
// 如果试图在未开启特性时为 Windows 构建,则报错:
#[cfg(all(target_os = "windows", not(feature = "windows")))]
compile_error!("请启用 'windows' 特性以构建 Windows 版本");
}

模式 3:基于 Trait 的平台抽象

#![allow(unused)]
fn main() {
/// 硬件访问的平台无关接口。
pub trait HardwareAccess {
    type Error: std::error::Error;

    fn read_cpu_temperature(&self) -> Result<f64, Self::Error>;
    fn read_gpu_temperature(&self, gpu_index: u32) -> Result<f64, Self::Error>;
    fn list_pci_devices(&self) -> Result<Vec<PciDevice>, Self::Error>;
    fn send_ipmi_command(&self, cmd: &IpmiCmd) -> Result<IpmiResponse, Self::Error>;
}

#[cfg(target_os = "linux")]
pub struct LinuxHardware;

#[cfg(target_os = "linux")]
impl HardwareAccess for LinuxHardware {
    type Error = LinuxHwError;

    fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
        // 从 /sys/class/thermal/thermal_zone0/temp 读取
        let raw = std::fs::read_to_string("/sys/class/thermal/thermal_zone0/temp")?;
        Ok(raw.trim().parse::<f64>()? / 1000.0)
    }
    // ...
}

#[cfg(target_os = "windows")]
pub struct WindowsHardware;

#[cfg(target_os = "windows")]
impl HardwareAccess for WindowsHardware {
    type Error = WindowsHwError;

    fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
        // 通过 WMI (Win32_TemperatureProbe) 或 Open Hardware Monitor 读取
        todo!("WMI 温度查询待实现")
    }
    // ...
}

/// 创建对应的平台实现
pub fn create_hardware() -> impl HardwareAccess {
    #[cfg(target_os = "linux")]
    { LinuxHardware }
    #[cfg(target_os = "windows")]
    { WindowsHardware }
}
}

平台抽象架构

对于针对多个平台的项目,建议将代码组织为三个层面:

┌──────────────────────────────────────────────────┐
│ 应用逻辑 (平台无关)                               │
│  diag_tool, accel_diag, network_diag, event_log 等│
│  仅依赖于平台抽象 Trait                            │
├──────────────────────────────────────────────────┤
│ 平台抽象层 (Trait 定义)                           │
│  trait HardwareAccess { ... }                     │
│  trait CommandRunner { ... }                      │
│  trait FileSystem { ... }                         │
├──────────────────────────────────────────────────┤
│ 平台具体实现 (受 cfg 保护)                         │
│  ┌──────────────┐  ┌──────────────┐              │
│  │ Linux 实现    │  │ Windows 实现  │              │
│  │ /sys, /proc  │  │ WMI, 注册表  │              │
│  │ ipmitool     │  │ ipmiutil     │              │
│  │ lspci        │  │ devcon       │              │
│  └──────────────┘  └──────────────┘              │
└──────────────────────────────────────────────────┘

测试抽象层:在单元测试中模拟 (Mock) 平台 Trait:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    struct MockHardware {
        cpu_temp: f64,
        gpu_temps: Vec<f64>,
    }

    impl HardwareAccess for MockHardware {
        type Error = std::io::Error;

        fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
            Ok(self.cpu_temp)
        }

        fn read_gpu_temperature(&self, index: u32) -> Result<f64, Self::Error> {
            self.gpu_temps.get(index as usize)
                .copied()
                .ok_or_else(|| std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    format!("未找到 GPU {index}")
                ))
        }

        fn list_pci_devices(&self) -> Result<Vec<PciDevice>, Self::Error> {
            Ok(vec![]) // 模拟返回空列表
        }

        fn send_ipmi_command(&self, _cmd: &IpmiCmd) -> Result<IpmiResponse, Self::Error> {
            Ok(IpmiResponse::default())
        }
    }

    #[test]
    fn test_thermal_check_with_mock() {
        let hw = MockHardware {
            cpu_temp: 75.0,
            gpu_temps: vec![82.0, 84.0],
        };
        let result = run_thermal_diagnostic(&hw);
        assert!(result.is_ok());
    }
}
}

应用:Linux 优先,Windows 就绪

本项目已经部分实现了 Windows 就绪。可以使用 cargo-hack 验证所有特性组合,并利用 交叉编译 在 Linux 上测试 Windows 版本:

现状:

  • platform::run_command 已使用 #[cfg(windows)] 进行 shell 选择。
  • 测试代码已使用 #[cfg(windows)] / #[cfg(not(windows))] 区分平台特定的测试命令。

建议的 Windows 支持演进路径:

阶段 1:提取平台抽象 Trait (当前 → 2 周)
  ├─ 在 core_lib 中定义 HardwareAccess Trait
  ├─ 将当前的 Linux 代码封装进 LinuxHardware 实现中
  └─ 所有诊断模块依赖于 Trait,而非 Linux 特有的实现

阶段 2:增加 Windows 存根 (Stubs) (2 周)
  ├─ 实现 WindowsHardware,暂留 TODO 存根
  ├─ 在 CI 中增加 x86_64-pc-windows-msvc 编译检查
  └─ 确保测试可以在所有平台上通过 MockHardware 运行

阶段 3:Windows 具体实现 (持续进行)
  ├─ IPMI 实现:通过 ipmiutil.exe 或 OpenIPMI Windows 驱动
  ├─ GPU 实现:通过 accel-mgmt (accel-api.dll) —— 接口与 Linux 保持一致
  ├─ PCIe 实现:通过 Windows Setup API (SetupDiEnumDeviceInfo)
  └─ NIC 实现:通过 WMI (Win32_NetworkAdapter)

在 CI 中增加跨平台构建:

# 添加到 CI 矩阵
- target: x86_64-pc-windows-msvc
  os: windows-latest
  name: windows-x86_64

这可以确保即便在 Windows 实现完全闭环前,代码也能在 Windows 环境下编译通过 —— 从而尽早发现 cfg 错误。

关键洞察:抽象层在第一天不必追求完美。可以先在底层函数中直接使用 #[cfg] 块(如现在的 exec_cmd),当有两三个平台实现时,再考虑重构为 Trait。过早的抽象反而比简单的 #[cfg] 块更糟糕。

条件编译决策树

flowchart TD
    START["是否存在平台特定代码?"] --> HOW_MANY{"支持多少平台?"}
    
    HOW_MANY -->|"2 个 (Linux + Windows)"| CFG_BLOCKS["在底层函数中\n使用 #[cfg] 块"]
    HOW_MANY -->|"3 个或更多"| TRAIT_APPROACH["使用平台 Trait\n以及各平台专属实现"]
    
    CFG_BLOCKS --> WINAPI{"是否需要使用 Windows API?"}
    WINAPI -->|"仅少量需求"| WIN_SYS["windows-sys\n原始 FFI 绑定"]
    WINAPI -->|"重度需求 (COM 等)"| WIN_RS["windows crate\nSafe/习惯用法封装"]
    WINAPI -->|"无需求\n(仅 cfg 区分)"| NATIVE["cfg(windows)\ncfg(unix)"]
    
    TRAIT_APPROACH --> CI_CHECK["使用 cargo-hack\n验证各特性组合"]
    CFG_BLOCKS --> CI_CHECK
    CI_CHECK --> XCOMPILE["在 CI 中交叉编译\ncargo-xwin 或\n原生运行器"]
    
    style CFG_BLOCKS fill:#91e5a3,color:#000
    style TRAIT_APPROACH fill:#ffd43b,color:#000
    style WIN_SYS fill:#e3f2fd,color:#000
    style WIN_RS fill:#e3f2fd,color:#000

🏋️ 练习

🟢 练习 1:平台相关的条件模块

创建一个模块,并分别实现 get_hostname() 函数的 #[cfg(unix)] 和 #[cfg(windows)] 版本。验证其可以通过 cargo check 以及 cargo check --target x86_64-pc-windows-msvc。

答案
#![allow(unused)]
fn main() {
// src/hostname.rs
#[cfg(unix)]
pub fn get_hostname() -> String {
    use std::fs;
    fs::read_to_string("/etc/hostname")
        .unwrap_or_else(|_| "unknown".to_string())
        .trim()
        .to_string()
}

#[cfg(windows)]
pub fn get_hostname() -> String {
    use std::env;
    env::var("COMPUTERNAME").unwrap_or_else(|_| "unknown".to_string())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hostname_is_not_empty() {
        let name = get_hostname();
        assert!(!name.is_empty());
    }
}
}
# 验证 Linux 编译情况
cargo check

# 验证 Windows 编译情况(交叉检查)
rustup target add x86_64-pc-windows-msvc
cargo check --target x86_64-pc-windows-msvc

🟡 练习 2:使用 cargo-xwin 交叉编译 Windows 版本

在 Linux 环境下安装 cargo-xwin,并为 x86_64-pc-windows-msvc 构建一个简单的二进制文件。验证输出是否为 .exe。

答案
cargo install cargo-xwin
rustup target add x86_64-pc-windows-msvc

cargo xwin build --release --target x86_64-pc-windows-msvc
# 会自动下载 Windows SDK 头文件和库

file target/x86_64-pc-windows-msvc/release/my-binary.exe
# 输出示例:PE32+ executable (console) x86-64, for MS Windows

# 你也可以通过 Wine 进行测试:
wine target/x86_64-pc-windows-msvc/release/my-binary.exe

关键收获

  • 先在底层函数中使用 #[cfg] 块;仅在三个或更多平台的实现逻辑发生分叉时才考虑重构为 Trait。
  • windows-sys 提供原始 FFI 接口;windows crate 则提供符合 Rust 习惯的安全封装。
  • cargo-xwin 允许你在 Linux 上交叉编译至 Windows MSVC ABI —— 无需真实的 Windows 机器。
  • 即使只在 Linux 上运行,也建议在 CI 中对 --target x86_64-pc-windows-msvc 进行编译检查。
  • 将 #[cfg] 与 Cargo 特性结合使用,以实现可选的平台支持(如 feature = "windows")。

English Original

综合实践 — 生产级 CI/CD 流水线 🟡

你将学到:

  • 构建多阶段 GitHub Actions CI 工作流 (检查 → 测试 → 覆盖率 → 安全 → 交叉编译 → 发布)
  • 使用 rust-cache 的缓存策略及 save-if 微调
  • 分离日常 push 与按计划运行 (Nightly) 的 Miri 和 sanitizer 任务
  • 使用 Makefile.toml 和 pre-commit 钩子实现任务自动化
  • 使用 cargo-dist 实现自动化发布

相关章节: 构建脚本 · 交叉编译 · 基准测试 · 覆盖率 · Miri/Sanitizer · 依赖管理 · 发布配置 · 编译期工具 · no_std · Windows

单一工具固然有用,但能将它们有机结合并在每次 push 时自动运行的流水线则具有革命性意义。本章将第 1 章至第 10 章涉及的工具整合为一个完整的 CI/CD 工作流。

完整的 GitHub Actions 工作流

在一个工作流文件中并行运行所有验证阶段:

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always
  CARGO_ENCODED_RUSTFLAGS: "-Dwarnings"  # 将警告视为错误 (仅限顶层 crate)
  # 注意:与 RUSTFLAGS 不同,CARGO_ENCODED_RUSTFLAGS 不会影响构建脚本或过程宏,
  # 从而避免了因第三方库的警告导致构建失败。
  # 如果你想对构建脚本也强制执行此规则,请改用 RUSTFLAGS="-Dwarnings"。

jobs:
  # ─── 阶段 1: 快速反馈 (< 2 分钟) ───
  check:
    name: 检查 + Clippy + 格式化
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, rustfmt

      - uses: Swatinem/rust-cache@v2  # 缓存依赖

      - name: 检查 Cargo.lock
        run: cargo fetch --locked

      - name: 检查文档
        run: RUSTDOCFLAGS='-Dwarnings' cargo doc --workspace --all-features --no-deps

      - name: 检查编译
        run: cargo check --workspace --all-targets --all-features

      - name: Clippy 静态检查
        run: cargo clippy --workspace --all-targets --all-features -- -D warnings

      - name: 代码格式化
        run: cargo fmt --all -- --check

  # ─── 阶段 2: 测试 (< 5 分钟) ───
  test:
    name: 测试 (${{ matrix.os }})
    needs: check
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2

      - name: 运行单元测试
        run: cargo test --workspace

      - name: 运行文档测试
        run: cargo test --workspace --doc

  # ─── 阶段 3: 交叉编译 (< 10 分钟) ───
  cross:
    name: 交叉编译 (${{ matrix.target }})
    needs: check
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            use_cross: true
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: 安装 musl-tools
        if: contains(matrix.target, 'musl')
        run: sudo apt-get install -y musl-tools

      - name: 安装 cross
        if: matrix.use_cross
        uses: taiki-e/install-action@cross

      - name: 构建 (原生)
        if: "!matrix.use_cross"
        run: cargo build --release --target ${{ matrix.target }}

      - name: 构建 (使用 cross)
        if: matrix.use_cross
        run: cross build --release --target ${{ matrix.target }}

      - name: 上传产物
        uses: actions/upload-artifact@v4
        with:
          name: binary-${{ matrix.target }}
          path: target/${{ matrix.target }}/release/diag_tool

  # ─── 阶段 4: 覆盖率 (< 10 分钟) ───
  coverage:
    name: 代码覆盖率
    needs: check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - uses: taiki-e/install-action@cargo-llvm-cov

      - name: 生成覆盖率数据
        run: cargo llvm-cov --workspace --lcov --output-path lcov.info

      - name: 强制执行最低覆盖率门禁
        run: cargo llvm-cov --workspace --fail-under-lines 75

      - name: 上传至 Codecov
        uses: codecov/codecov-action@v4
        with:
          files: lcov.info
          token: ${{ secrets.CODECOV_TOKEN }}

  # ─── 阶段 5: 安全验证 (< 15 分钟) ───
  miri:
    name: Miri 验证
    needs: check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@nightly
        with:
          components: miri

      - name: 运行 Miri
        run: cargo miri test --workspace
        env:
          MIRIFLAGS: "-Zmiri-backtrace=full"

  # ─── 阶段 6: 基准测试 (仅限 PR, < 10 分钟) ───
  bench:
    name: 基准测试
    if: github.event_name == 'pull_request'
    needs: check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable

      - name: 运行基准测试
        run: cargo bench -- --output-format bencher | tee bench.txt

      - name: 与基准线对比
        uses: benchmark-action/github-action-benchmark@v1
        with:
          tool: 'cargo'
          output-file-path: bench.txt
          github-token: ${{ secrets.GITHUB_TOKEN }}
          alert-threshold: '115%'
          comment-on-alert: true

流水线执行流程:

                    ┌─────────┐
                    │  check  │  ← clippy + fmt + cargo check (2 min)
                    └────┬────┘
           ┌─────────┬──┴──┬──────────┬──────────┐
           ▼         ▼     ▼          ▼          ▼
       ┌──────┐  ┌──────┐ ┌────────┐ ┌──────┐ ┌──────┐
       │ test │  │cross │ │coverage│ │ miri │ │bench │
       │ (2×) │  │ (2×) │ │        │ │      │ │(PR)  │
       └──────┘  └──────┘ └────────┘ └──────┘ └──────┘
         3 min    8 min     8 min     12 min    5 min

总耗时:约 14 分钟 (在 check 门禁后的并行阶段)

CI 缓存策略

Swatinem/rust-cache@v2 是 Rust CI 的标准缓存 Action。它可以缓存 ~/.cargo 和 target/ 目录,但大型工作区需要进行一定的微调:

# 基础用法 (即上述代码中使用的)
- uses: Swatinem/rust-cache@v2

# 针对大型工作区的微调策略:
- uses: Swatinem/rust-cache@v2
  with:
    # 为每个任务设置独立前缀 —— 防止测试产物污染构建缓存
    prefix-key: "v1-rust"
    key: ${{ matrix.os }}-${{ matrix.target || 'default' }}
    # 仅在 main 分支保存缓存 (PR 分支仅读取不写入)
    save-if: ${{ github.ref == 'refs/heads/main' }}
    # 缓存 Cargo 注册表 + Git 下载内容 + target 目录
    cache-targets: true
    cache-all-crates: true

缓存失效的常见陷阱:

问题修复方法
缓存无限增长 (>5 GB)设置 prefix-key: "v2-rust" 以强制清理旧缓存
不同特性 (features) 互相污染使用 key: ${{ hashFiles('**/Cargo.lock') }}
PR 缓存覆盖了主分支缓存设置 save-if: ${{ github.ref == 'refs/heads/main' }}
交叉编译目标导致体积膨胀为不同的三元组 (target triple) 设置不同的 key

跨任务共享缓存:

check 任务负责保存缓存;后续任务(test, cross, coverage)只需读取。通过在 main 分支上设置 save-if,PR 运行任务可以享受到缓存依赖带来的加速,且不会写回失效的缓存。

对大型工作区的实测收益:从未启动构建需 ~4 分钟 → 缓存态构建需 ~45 秒。仅缓存 Action 这一项就在单次流水线运行中节省了约 25 分钟的累计 CI 时间。

使用 cargo-make 的 Makefile.toml

cargo-make 提供了一个跨平台的任务运行器(不同于 make/Makefile):

# 安装
cargo install cargo-make
# 工作区根目录下的 Makefile.toml

[config]
default_to_workspace = false

# ─── 开发者工作流 ───

[tasks.dev]
description = "本地全量验证 (与 CI 检查项一致)"
dependencies = ["check", "test", "clippy", "fmt-check"]

[tasks.check]
command = "cargo"
args = ["check", "--workspace", "--all-targets"]

[tasks.test]
command = "cargo"
args = ["test", "--workspace"]

[tasks.clippy]
command = "cargo"
args = ["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"]

[tasks.fmt]
command = "cargo"
args = ["fmt", "--all"]

[tasks.fmt-check]
command = "cargo"
args = ["fmt", "--all", "--", "--check"]

# ─── 覆盖率 ───

[tasks.coverage]
description = "生成 HTML 覆盖率报告"
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--workspace", "--html", "--open"]

[tasks.coverage-ci]
description = "生成供 CI 上传使用的 LCOV 数据"
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--workspace", "--lcov", "--output-path", "lcov.info"]

# ─── 基准测试 ───

[tasks.bench]
description = "运行所有基准测试"
command = "cargo"
args = ["bench"]

# ─── 交叉编译 ───

[tasks.build-musl]
description = "构建静态二进制 (musl)"
command = "cargo"
args = ["build", "--release", "--target", "x86_64-unknown-linux-musl"]

[tasks.build-arm]
description = "为 aarch64 构建 (需要 cross)"
command = "cross"
args = ["build", "--release", "--target", "aarch64-unknown-linux-gnu"]

[tasks.build-all]
description = "构建所有发布目标"
dependencies = ["build-musl", "build-arm"]

# ─── 安全验证 ───

[tasks.miri]
description = "在所有测试上运行 Miri"
toolchain = "nightly"
command = "cargo"
args = ["miri", "test", "--workspace"]

[tasks.audit]
description = "检查已知漏洞"
install_crate = "cargo-audit"
command = "cargo"
args = ["audit"]

# ─── 发布 ───

[tasks.release-dry]
description = "预览 cargo-release 的操作"
install_crate = "cargo-release"
command = "cargo"
args = ["release", "--workspace", "--dry-run"]

使用方法:

# 本地模拟 CI 流水线运行
cargo make dev

# 生成并查看覆盖率报告
cargo make coverage

# 为所有目标平台构建
cargo make build-all

# 运行 Miri 安全检查
cargo make miri

# 漏洞审计
cargo make audit

Pre-Commit 钩子:自定义脚本与 cargo-husky

在问题进入 CI 之前就拦截它们。推荐方案是创建一个自定义 Git 钩子 —— 它简单、透明且无外部依赖:

#!/bin/sh
# .githooks/pre-commit

set -e

echo "=== 执行 Pre-commit 检查 ==="

# 先执行速度最快的检查
echo "→ 检查代码格式 (cargo fmt --check)"
cargo fmt --all -- --check

echo "→ 检查编译 (cargo check)"
cargo check --workspace --all-targets

echo "→ 静态分析 (cargo clippy)"
cargo clippy --workspace --all-targets -- -D warnings

echo "→ 快速单元测试 (cargo test --lib)"
cargo test --workspace --lib

echo "=== 所有检查已通过 ==="
# 启用钩子
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit

另一种选择:cargo-husky (通过构建脚本自动安装钩子):

⚠️ 注意:cargo-husky 自 2022 年以来未曾更新。它依然能用,但事实上已无人维护。对于新项目,建议采用上述自定义钩子的方式。

# Cargo.toml — 添加至根 crate 的 dev-dependencies
[dev-dependencies]
cargo-husky = { version = "1", default-features = false, features = [
    "precommit-hook",
    "run-cargo-check",
    "run-cargo-clippy",
    "run-cargo-fmt",
    "run-cargo-test",
] }

发布工作流:cargo-release 与 cargo-dist

cargo-release — 自动化执行版本提升、打标签以及发布操作:

# 安装
cargo install cargo-release
# 工作区根目录下的 release.toml
[workspace]
consolidate-commits = true
pre-release-commit-message = "chore: 发布版本 {{version}}"
tag-message = "v{{version}}"
tag-name = "v{{version}}"

# 禁止发布内部 crate 
[[package]]
name = "core_lib"
release = false

[[package]]
name = "diag_framework"
release = false

# 仅发布主要的二进制 crate
[[package]]
name = "diag_tool"
release = true
# 预览发布操作
cargo release patch --dry-run

# 执行发布 (提升版本号、提交更改、打标签、可选发布至 crates.io)
cargo release patch --execute
# 0.1.0 → 0.1.1

cargo release minor --execute
# 0.1.1 → 0.2.0

cargo-dist — 为 GitHub Releases 生成可下载的离线二进制包:

# 安装
cargo install cargo-dist

# 初始化 (生成 CI 工作流及元数据)
cargo dist init

# 预览构建计划
cargo dist plan

# 执行发布构建 (通常由 CI 任务在 push tag 时执行)
cargo dist build
# 由 `cargo dist init` 添加至 Cargo.toml 的部分
[workspace.metadata.dist]
cargo-dist-version = "0.28.0"
ci = "github"
targets = [
    "x86_64-unknown-linux-gnu",
    "x86_64-unknown-linux-musl",
    "aarch64-unknown-linux-gnu",
    "x86_64-pc-windows-msvc",
]
install-path = "CARGO_HOME"

这会生成一个 GitHub Actions 工作流,在 push 标签时会自动执行:

  1. 为所有目标平台构建二进制文件。
  2. 创建 GitHub Release 并上传 .tar.gz / .zip 压缩包。
  3. 生成 Shell/PowerShell 安装脚本。
  4. 发布到 crates.io (若已配置)。

亲身尝试 — 总结性练习

本练习将贯穿之前每一章。你将为一个全新的 Rust 工作区构建完整的工程化流水线:

  1. 创建一个新工作区,包含两个 crate:库 (core_lib) 和二进制 (cli)。添加一个 build.rs,利用 SOURCE_DATE_EPOCH 嵌入 Git 哈希和构建时间戳 (第 1 章)。

  2. 设置交叉编译,针对 x86_64-unknown-linux-musl 和 aarch64-unknown-linux-gnu。验证两个目标均可通过 cargo zigbuild 或 cross 成功构建 (第 2 章)。

  3. 添加基准测试,使用 Criterion 或 Divan 测量 core_lib 某个函数的性能。本地运行并记录基准线 (第 3 章)。

  4. 测量代码覆盖率,使用 cargo llvm-cov。设置 80% 的最低门槛并确保其通过 (第 4 章)。

  5. 运行 Miri 测试 与 cargo +nightly careful test。如果你编写了 unsafe 代码,请确保对应测试能覆盖到它 (第 5 章)。

  6. 配置 cargo-deny,在 deny.toml 中禁用 openssl 并强制执行 MIT/Apache-2.0 许可证准则 (第 6 章)。

  7. 优化发布配置,启用 lto = "thin", strip = true 和 codegen-units = 1。使用 cargo bloat 对比优化前后的二进制体积变化 (第 7 章)。

  8. 添加特性验证。为一个可选依赖项创建特性标志,并使用 cargo hack --each-feature 确保其能独立编译 (第 9 章)。

  9. 编写 GitHub Actions 工作流 (参考本章),包含上述 6 个阶段。添加微调后的 Swatinem/rust-cache@v2。

达标标准:Push 代码到 GitHub → 所有的 CI 阶段均显示绿色 → cargo dist plan 输出了正确的发布目标。恭喜,你已经拥有了一个生产级的 Rust 流水线。

CI 流水线架构

flowchart LR
    subgraph "阶段 1 — 快速反馈 < 2 分钟"
        CHECK["编译检查/\nClippy/\n格式化"]
    end

    subgraph "阶段 2 — 测试 < 5 分钟"
        TEST["并行单元测试/\n文档测试"]
    end

    subgraph "阶段 3 — 覆盖率"
        COV["llvm-cov\n门禁 80%"]
    end

    subgraph "阶段 4 — 安全审计"
        SEC["漏洞审计/\n许可证检查"]
    end

    subgraph "阶段 5 — 交叉编译"
        CROSS["多平台静态构建\naarch64 + x86_64"]
    end

    subgraph "阶段 6 — 发布 (仅限标签)"
        REL["cargo dist\nGitHub Release"]
    end

    CHECK --> TEST --> COV --> SEC --> CROSS --> REL

    style CHECK fill:#91e5a3,color:#000
    style TEST fill:#91e5a3,color:#000
    style COV fill:#e3f2fd,color:#000
    style SEC fill:#ffd43b,color:#000
    style CROSS fill:#e3f2fd,color:#000
    style REL fill:#b39ddb,color:#000

关键收获

  • 将 CI 划分为并行阶段:先执行快速反馈任务,慢速任务放在门禁之后。
  • 为 Swatinem/rust-cache@v2 使用 save-if: ${{ github.ref == 'refs/heads/main' }} 可防止 PR 导致缓存失效。
  • 使用 schedule: (Nightly) 触发器执行 Miri 或繁重的 sanitizer 任务。
  • Makefile.toml (cargo make) 将多工具工作流打包为单一本地命令。
  • cargo-dist 实现了跨平台发布的模版化 —— 不再需要手动编写海量的平台矩阵 YAML。

English Original

一线实践技巧 🟡

你将学到:

  • 经过实战检验、且不便归入其他章节的工程模式
  • 常见陷阱及其修复方法 —— 从 CI 不稳定到二进制膨胀
  • 可立即应用于任何 Rust 项目的“快赢”技术

相关章节: 本书的每一章 —— 这些技巧横跨了所有主题

本章收集了在生产级 Rust 代码库中反复出现的工程模式。每个技巧都是独立的 —— 你可以按任何顺序阅读。


1. deny(warnings) 陷阱

问题:在源码中使用 #![deny(warnings)] 会在 Clippy 添加新 Lint 后导致构建失败 —— 昨天还能编译的代码今天就由于新规则报错了。

修复:在 CI 中使用 CARGO_ENCODED_RUSTFLAGS 代替源码级的属性:

# CI: 将警告视为错误,而不触及源码
env:
  CARGO_ENCODED_RUSTFLAGS: "-Dwarnings"

或者使用 [workspace.lints] 进行更精细的控制:

# Cargo.toml
[workspace.lints.rust]
unsafe_code = "deny"

[workspace.lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "warn", priority = -1 }

参见 编译期工具,工作区 Lint 了解完整模式。


2. 编译一次,到处测试

问题:在 --lib、--doc 和 --test 之间切换时,cargo test 会重新编译,因为它们使用的是不同的配置项 (Profiles)。

修复:使用 cargo nextest 运行单元测试和集成测试,并单独运行文档测试:

cargo nextest run --workspace        # 快:并行运行,有缓存
cargo test --workspace --doc         # 文档测试 (nextest 无法运行此类测试)

参见 编译期工具 了解 cargo-nextest 的配置。


3. 特性标志 (Feature Flag) 健康管理

问题:一个库 crate 拥有 default = ["std"],但没有人测试过 --no-default-features 情况。某天一个嵌入式用户反馈由于该原因无法编译。

修复:在 CI 中添加 cargo-hack:

- name: 特性矩阵检查
  run: |
    cargo hack check --each-feature --no-dev-deps
    cargo check --no-default-features
    cargo check --all-features

参见 no_std 与特性验证 了解完整模式。


4. 琐事:Lock 文件该提交还是忽略?

经验法则:

Crate 类型是否提交 Cargo.lock?原因
二进制 / 应用程序是确保构建可复现
库 (Library)否 (加入 .gitignore)让下游用户自行选择版本
包含两者的工作区是以二进制应用为准

添加一个 CI 检查项,确保 Lock 文件保持最新:

- name: 检查 Lock 文件
  run: cargo update --locked  # 如果 Cargo.lock 已过期则报错

5. 带有优化依赖项的调试构建

问题:Debug 构建慢得令人痛苦,因为依赖项(尤其是 serde, regex)没有被优化。

修复:在 dev 配置中优化依赖项,但保持你自己的代码不被优化以实现快速重新编译:

# Cargo.toml
[profile.dev.package."*"]
opt-level = 2  # 在开发模式下优化所有依赖项

这会稍微减慢第一次构建的速度,但在开发期间会显著提升运行效率。这对于依赖数据库的服务和解析器尤为重要。

参见 发布配置 了解针对单个 crate 的配置覆盖 (Profile Overrides)。


6. CI 缓存抖动 (Thrashing)

问题:Swatinem/rust-cache@v2 在每个 PR 上都保存一个新缓存,导致存储膨胀并减慢还原速度。

修复:仅从 main 分支保存缓存,从任何地方还原缓存:

- uses: Swatinem/rust-cache@v2
  with:
    save-if: ${{ github.ref == 'refs/heads/main' }}

对于包含多个二进制文件的工作区,可以添加一个 shared-key:

- uses: Swatinem/rust-cache@v2
  with:
    shared-key: "ci-${{ matrix.target }}"
    save-if: ${{ github.ref == 'refs/heads/main' }}

参见 CI/CD 流水线 了解完整工作流。


7. RUSTFLAGS vs CARGO_ENCODED_RUSTFLAGS

问题:RUSTFLAGS="-Dwarnings" 会应用于 一切 —— 包括构建脚本和过程宏。如果 serde_derive 的 build.rs 里有一个警告,你的 CI 就会挂掉。

修复:使用 CARGO_ENCODED_RUSTFLAGS,它只应用于顶层 crate:

# 不佳 —— 可能会因为第三方库构建脚本的警告而报错
RUSTFLAGS="-Dwarnings" cargo build

# 推荐 —— 仅影响你自己的 crate
CARGO_ENCODED_RUSTFLAGS="-Dwarnings" cargo build

# 同样推荐 —— 在 Cargo.toml 中设置工作区 Lint 
[workspace.lints.rust]
warnings = "deny"

8. 使用 SOURCE_DATE_EPOCH 实现可复现构建

问题:在 build.rs 中嵌入 chrono::Utc::now() 会导致构建无法复现 —— 每次构建都会产生不同的二进制哈希。

修复:遵循 SOURCE_DATE_EPOCH:

#![allow(unused)]
fn main() {
// build.rs
let timestamp = std::env::var("SOURCE_DATE_EPOCH")
    .ok()
    .and_then(|s| s.parse::<i64>().ok())
    .unwrap_or_else(|| chrono::Utc::now().timestamp());
println!("cargo:rustc-env=BUILD_TIMESTAMP={timestamp}");
}

参见 构建脚本 了解完整的 build.rs 模式。


9. cargo tree 去重工作流

问题:cargo tree --duplicates 显示有 5 个版本的 syn 和 3 个版本的 tokio-util。编译慢如蜗牛。

修复:系统性地去重:

# 第 1 步:查找重复项
cargo tree --duplicates

# 第 2 步:查找是谁拉取了旧版本
cargo tree --invert --package [email protected]

# 第 3 步:更新导致问题的依赖
cargo update -p serde_derive  # 可能会因此拉取 syn 2.x

# 第 4 步:如果无法更新,在 [patch] 中手动锁定
# [patch.crates-io]
# old-crate = { git = "...", branch = "syn2-migration" }

# 第 5 步:验证
cargo tree --duplicates  # 列表应该变短了

参见 依赖管理 了解 cargo-deny 和供应链安全。


10. 推送前的冒烟测试 (Smoke Test)

问题:你推送了代码,CI 跑了 10 分钟,最后因为格式问题失败了。

修复:在推送前先在本地运行快速检查:

# Makefile.toml (cargo-make)
[tasks.pre-push]
description = "推送前的本地冒烟测试"
script = '''
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --lib
'''
cargo make pre-push  # 耗时 < 30 秒
git push

或者使用 git pre-push 钩子:

#!/bin/sh
# .git/hooks/pre-push
cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings

参见 CI/CD 流水线 了解 Makefile.toml 模式。


🏋️ 练习

🟢 练习 1:应用三个技巧

从本章挑选三个技巧,并应用到一个现有的 Rust 项目中。哪一个对你的影响最大?

答案

典型的“高收益”组合:

  1. [profile.dev.package."*"] opt-level = 2 — 立即提升开发模式下的运行速度(对于解析密集型代码通常提速 2-10 倍)。

  2. CARGO_ENCODED_RUSTFLAGS — 消除了由于第三方库警告导致的 CI 误报。

  3. cargo-hack --each-feature — 通常能在包含 3 个以上特性的项目中找出至少一个失效的特性组合。

# 应用技巧 5:
echo '[profile.dev.package."*"]' >> Cargo.toml
echo 'opt-level = 2' >> Cargo.toml

# 在 CI 中应用技巧 7:
# 将 RUSTFLAGS 替换为 CARGO_ENCODED_RUSTFLAGS

# 应用技巧 3:
cargo install cargo-hack
cargo hack check --each-feature --no-dev-deps

🟡 练习 2:去重你的依赖树

在一个真实项目上运行 cargo tree --duplicates。消除至少一处重复项。测量优化前后的编译耗时。

答案
# 优化前
time cargo build --release 2>&1 | tail -1
cargo tree --duplicates | wc -l  # 统计重复行数

# 查找并修复一处重复
cargo tree --duplicates
cargo tree --invert --package <duplicate-crate>@<old-version>
cargo update -p <parent-crate>

# 优化后
time cargo build --release 2>&1 | tail -1
cargo tree --duplicates | wc -l  # 数值应该变小了

# 典型结果:每消除一个重复项(尤其是像 syn, tokio 这种重型 crate),
# 编译时间可缩短 5-15%。

关键收获

  • 使用 CARGO_ENCODED_RUSTFLAGS 代替 RUSTFLAGS 可避免由于第三方库构建脚本报错。
  • [profile.dev.package."*"] opt-level = 2 是提升开发体验最立竿见影的技巧。
  • 缓存微调(仅在 main 分支执行 save-if)可防止活跃代码库的 CI 存储膨胀。
  • cargo tree --duplicates + cargo update 是不花钱的编译期提速法 —— 建议每月执行一次。
  • 使用 cargo make pre-push 在本地执行快速检查,避免因琐事在 CI 往返上浪费时间。

English Original

速查卡

备忘录:命令一览

# ─── 构建脚本 (Build Scripts) ───
cargo build                          # 先编译 build.rs,再编译 crate
cargo build -vv                      # 详细模式 —— 显示 build.rs 的输出

# ─── 交叉编译 (Cross-Compilation) ───
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17
cross build --release --target aarch64-unknown-linux-gnu

# ─── 基准测试 (Benchmarking) ───
cargo bench                          # 运行所有基准测试
cargo bench -- parse                 # 运行名称匹配 "parse" 的基准测试
cargo flamegraph -- --args           # 根据二进制程序生成火焰图
perf record -g ./target/release/bin  # 记录性能数据
perf report                          # 交互式查看性能数据

# ─── 代码覆盖率 (Coverage) ───
cargo llvm-cov --html                # 生成 HTML 报告
cargo llvm-cov --lcov --output-path lcov.info
cargo llvm-cov --workspace --fail-under-lines 80
cargo tarpaulin --out Html           # 备选工具

# ─── 安全验证 (Safety Verification) ───
cargo +nightly miri test             # 在 Miri 下运行测试
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test
valgrind --leak-check=full ./target/debug/binary
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# ─── 审计与供应链 (Audit & Supply Chain) ───
cargo audit                          # 已知漏洞扫描
cargo audit --deny warnings          # 若存在漏洞通告则使 CI 失败
cargo deny check                     # 许可证 + 漏洞通告 + 禁止项 + 源码检查
cargo deny list                      # 列出依赖树中的所有许可证
cargo vet                            # 供应链信任验证
cargo outdated --workspace           # 查找过期的依赖项
cargo semver-checks                  # 检测破坏性的 API 变更
cargo geiger                         # 统计依赖树中的 unsafe 代码行数

# ─── 二进制优化 (Binary Optimization) ───
cargo bloat --release --crates       # 统计每个 crate 的体积占用
cargo bloat --release -n 20          # 找出体积最大的 20 个函数
cargo +nightly udeps --workspace     # 查找未使用的依赖项
cargo machete                        # 快速检测未使用的依赖项
cargo expand --lib module::name      # 查看宏展开后的结果
cargo msrv find                      # 探测支持的最低 Rust 版本
cargo clippy --fix --workspace --allow-dirty  # 自动修复静态检查出的警告

# ─── 编译期优化 (Compile-Time Optimization) ───
export RUSTC_WRAPPER=sccache         # 开启共享编译缓存
sccache --show-stats                 # 查看缓存命中率统计
cargo nextest run                    # 更快的测试运行器
cargo nextest run --retries 2        # 自动重试不稳定的测试

# ─── 平台工程 (Platform Engineering) ───
cargo check --target thumbv7em-none-eabihf   # 验证 no_std 构建情况
cargo build --target x86_64-pc-windows-gnu   # 交叉编译至 Windows (MinGW)
cargo xwin build --target x86_64-pc-windows-msvc  # 交叉编译至 Windows (MSVC ABI)
cfg!(target_os = "linux")                    # 编译期配置 (求值为 bool 值)

# ─── 发布 (Release) ───
cargo release patch --dry-run        # 预览发布操作
cargo release patch --execute        # 提升版本、提交、打标签并发布
cargo dist plan                      # 预览分发产物

决策表:在什么场景使用什么工具

目标工具何时使用
嵌入 Git 哈希 / 构建信息build.rs二进制程序需要可追溯性
使用 Rust 编译 C 代码build.rs 中的 cc crate需要 FFI 调用小型 C 库
从 Schema 生成代码prost-build / tonic-build使用 Protobuf, gRPC, FlatBuffers
链接系统库build.rs 中的 pkg-config依赖 OpenSSL, libpci, systemd
静态链接的 Linux 二进制程序--target x86_64-unknown-linux-musl容器化或云端部署
针对旧版 glibc 进行构建cargo-zigbuild兼容 RHEL 7, CentOS 7 等老系统
ARM 架构服务器程序cross 或 cargo-zigbuild部署至 Graviton/Ampere 算力
统计学意义上的基准测试Criterion.rs检测性能退化 (Regression)
快速性能检查Divan开发期间的性能分析
找出性能热点cargo flamegraph / perf基准测试发现慢代码后的深度剖析
行/分支覆盖率cargo-llvm-covCI 覆盖率门禁、覆盖缺口分析
快速检查覆盖率cargo-tarpaulin本地开发期间使用
Rust UB (未定义行为) 检测Miri纯 Rust 的 unsafe 代码
C FFI 内存安全检查Valgrind memcheckRust/C 混合的代码库
数据竞态检测TSan 或 Miri并发环境下的 unsafe 代码
缓冲区溢出检测ASanunsafe 指针算术运算
泄漏检测Valgrind 或 LSan需要长时间运行的服务
本地模拟 CIcargo-make开发者工作流自动化
提交前检查 (Pre-commit)cargo-husky 或 Git 钩子推送代码前拦截问题
自动化发布cargo-release + cargo-dist版本管理与分发自动化
依赖项审计cargo-audit / cargo-deny供应链安全保障
许可证合规性cargo-deny (licenses)商业或企业级项目
供应链信任验证cargo-vet高安全性要求的环境
查找过期依赖cargo-outdated定期的维护工作
检测破坏性 API 变更cargo-semver-checks二进制库发布前校验
依赖树分析cargo tree --duplicates优化并清理依赖图中的冗余
二进制体积分析cargo-bloat针对体积敏感的部署场景
查找冗余依赖cargo-udeps / cargo-machete缩减编译时间和二进制体积
LTO 微调lto = true 或 "thin"优化发布版二进制程序
体积优化的二进制程序opt-level = "z" + strip = true嵌入式 / WASM / 容器环境
Unsafe 使用情况审计cargo-geiger安全政策强制执行
宏调试cargo-expand调试 derive 或 macro_rules 输出
链接加速mold 链接器提升开发人员的本地迭代速度
编译缓存sccacheCI 与本地构建加速
测试加速cargo-nextestCI 与本地测试提速
MSRV 兼容性检查cargo-msrv发布库文件时使用
no_std 库开发#![no_std] + default-features = false嵌入式、UEFI、WASM 环境
Windows 交叉编译cargo-xwin / MinGW在 Linux 上构建 Windows 程序
平台抽象模式#[cfg] + Trait 模式支持多操作系统的代码库
调用 Windows APIwindows-sys / windows crate使用 Windows 原生功能
端到端耗时测量hyperfine整体二进制基准测试及前后对比
基于属性的测试proptest发现边缘情况、增强解析器健壮性
快照测试insta验证大型结构化输出
覆盖率导向型模糊测试cargo-fuzz发现解析器中的崩溃 Bug
并发模型检查loom验证无锁数据结构、原子序
特性组合测试cargo-hack针对拥有多个 #[cfg] 特性的 crate
快速 UB 检查 (近乎原生)cargo-carefulCI 安全门禁,比 Miri 更轻量
保存自动重构cargo-watch提升开发迭代反馈速度
工作区文档cargo doc + rustdocAPI 发现、上手文档、文档链接 CI 检查
可复现构建--locked + SOURCE_DATE_EPOCH验证发布版本的完整性
CI 缓存微调Swatinem/rust-cache@v2缩短构建耗时 (冷构建 → 缓存构建)
工作区 Lint 政策Cargo.toml 中的 [workspace.lints]跨 crate 统一 Clippy 和编译器 Lint 规则
自动修复 Lint 警告cargo clippy --fix自动化清理琐碎代码问题

延伸阅读

主题资料
Cargo 构建脚本Cargo Book — Build Scripts
交叉编译Rust Cross-Compilation
cross 工具cross-rs/cross
cargo-zigbuildcargo-zigbuild docs
Criterion.rsCriterion 用户手册
DivanDivan docs
cargo-llvm-covcargo-llvm-cov
cargo-tarpaulintarpaulin docs
MiriMiri GitHub
Rust 中的 Sanitizerrustc Sanitizer 文档
cargo-makecargo-make book
cargo-releasecargo-release docs
cargo-distcargo-dist docs
Profile-guided optimizationRust PGO 指南
火焰图cargo-flamegraph
cargo-denycargo-deny docs
cargo-vetcargo-vet docs
cargo-auditcargo-audit
cargo-bloatcargo-bloat
cargo-udepscargo-udeps
cargo-geigercargo-geiger
cargo-semver-checkscargo-semver-checks
cargo-nextestnextest docs
sccachesccache
mold 链接器mold
cargo-msrvcargo-msrv
LTOrustc 代码生成选项
Cargo ProfilesCargo Book — Profiles
no_stdRust 嵌入式手册
windows-sys cratewindows-rs
cargo-xwincargo-xwin docs
cargo-hackcargo-hack
cargo-carefulcargo-careful
cargo-watchcargo-watch
Rust CI 缓存Swatinem/rust-cache
Rustdoc 手册Rustdoc Book
条件编译Rust 参考手册 — cfg
嵌入式 RustAwesome Embedded Rust
hyperfinehyperfine
proptestproptest
instainsta 快照测试
cargo-fuzzcargo-fuzz
loomloom 并发测试

生成参考资料 —— 作为《Rust 设计模式》与《类型驱动的正确性》的配套指南。

版本 1.3 —— 为了保证内容的完备性,增加了 cargo-hack, cargo-careful, cargo-watch, cargo doc, 可复现构建, CI 缓存策略, 总结性练习以及章节依赖图。


中文版

Rust Engineering Practices — Beyond cargo build

Speaker Intro

  • Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team
  • Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems
  • Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since

A practical guide to the Rust toolchain features that most teams discover too late: build scripts, cross-compilation, benchmarking, code coverage, and safety verification with Miri and Valgrind. Each chapter uses concrete examples drawn from a real hardware-diagnostics codebase — a large multi-crate workspace — so every technique maps directly to production code.

How to Use This Book

This book is designed for self-paced study or team workshops. Each chapter is largely independent — read them in order or jump to the topic you need.

Difficulty Legend

SymbolLevelMeaning
🟢StarterStraightforward tools with clear patterns — useful on day one
🟡IntermediateRequires understanding of toolchain internals or platform concepts
🔴AdvancedDeep toolchain knowledge, nightly features, or multi-tool orchestration

Pacing Guide

PartChaptersEst. TimeKey Outcome
I — Build & Shipch01–023–4 hBuild metadata, cross-compilation, static binaries
II — Measure & Verifych03–054–5 hStatistical benchmarking, coverage gates, Miri/sanitizers
III — Harden & Optimizech06–106–8 hSupply chain security, release profiles, compile-time tools, no_std, Windows
IV — Integratech11–133–4 hProduction CI/CD pipeline, tricks, capstone exercise
16–21 hFull production engineering pipeline

Working Through Exercises

Each chapter contains 🏋️ exercises with difficulty indicators. Solutions are provided in expandable <details> blocks — try the exercise first, then check your work.

  • 🟢 exercises can often be done in 10–15 minutes
  • 🟡 exercises require 20–40 minutes and may involve running tools locally
  • 🔴 exercises require significant setup and experimentation (1+ hour)

Prerequisites

ConceptWhere to learn it
Cargo workspace layoutRust Book ch14.3
Feature flagsCargo Reference — Features
#[cfg(test)] and basic testingRust Patterns ch12
unsafe blocks and FFI basicsRust Patterns ch10

Chapter Dependency Map

                 ┌──────────┐
                 │ ch00     │
                 │  Intro   │
                 └────┬─────┘
        ┌─────┬───┬──┴──┬──────┬──────┐
        ▼     ▼   ▼     ▼      ▼      ▼
      ch01  ch03 ch04  ch05   ch06   ch09
      Build Bench Cov  Miri   Deps   no_std
        │     │    │    │      │      │
        │     └────┴────┘      │      ▼
        │          │           │    ch10
        ▼          ▼           ▼   Windows
       ch02      ch07        ch07    │
       Cross    RelProf     RelProf  │
        │          │           │     │
        │          ▼           │     │
        │        ch08          │     │
        │      CompTime        │     │
        └──────────┴───────────┴─────┘
                   │
                   ▼
                 ch11
               CI/CD Pipeline
                   │
                   ▼
                ch12 ─── ch13
              Tricks    Quick Ref

Read in any order: ch01, ch03, ch04, ch05, ch06, ch09 are independent. Read after prerequisites: ch02 (needs ch01), ch07–ch08 (benefit from ch03–ch06), ch10 (benefits from ch09). Read last: ch11 (ties everything together), ch12 (tricks), ch13 (reference).

Annotated Table of Contents

Part I — Build & Ship

#ChapterDifficultyDescription
1Build Scripts — build.rs in Depth🟢Compile-time constants, compiling C code, protobuf generation, system library linking, anti-patterns
2Cross-Compilation — One Source, Many Targets🟡Target triples, musl static binaries, ARM cross-compile, cross tool, cargo-zigbuild, GitHub Actions

Part II — Measure & Verify

#ChapterDifficultyDescription
3Benchmarking — Measuring What Matters🟡Criterion.rs, Divan, perf flamegraphs, PGO, continuous benchmarking in CI
4Code Coverage — Seeing What Tests Miss🟢cargo-llvm-cov, cargo-tarpaulin, grcov, Codecov/Coveralls CI integration
5Miri, Valgrind, and Sanitizers🔴MIR interpreter, Valgrind memcheck/Helgrind, ASan/MSan/TSan, cargo-fuzz, loom

Part III — Harden & Optimize

#ChapterDifficultyDescription
6Dependency Management and Supply Chain Security🟢cargo-audit, cargo-deny, cargo-vet, cargo-outdated, cargo-semver-checks
7Release Profiles and Binary Size🟡Release profile anatomy, LTO trade-offs, cargo-bloat, cargo-udeps
8Compile-Time and Developer Tools🟡sccache, mold, cargo-nextest, cargo-expand, cargo-geiger, workspace lints, MSRV
9no_std and Feature Verification🔴cargo-hack, core/alloc/std layers, custom panic handlers, testing no_std code
10Windows and Conditional Compilation🟡#[cfg] patterns, windows-sys/windows crates, cargo-xwin, platform abstraction

Part IV — Integrate

#ChapterDifficultyDescription
11Putting It All Together — A Production CI/CD Pipeline🟡GitHub Actions workflow, cargo-make, pre-commit hooks, cargo-dist, capstone
12Tricks from the Trenches🟡10 battle-tested patterns: deny(warnings) trap, cache tuning, dep dedup, RUSTFLAGS, more
13Quick Reference Card—Commands at a glance, 60+ decision table entries, further reading links

Build Scripts — build.rs in Depth 🟢

What you’ll learn:

  • How build.rs fits into the Cargo build pipeline and when it runs
  • Five production patterns: compile-time constants, C/C++ compilation, protobuf codegen, pkg-config linking, and feature detection
  • Anti-patterns that slow builds or break cross-compilation
  • How to balance traceability with reproducible builds

Cross-references: Cross-Compilation uses build scripts for target-aware builds · no_std & Features extends cfg flags set here · CI/CD Pipeline orchestrates build scripts in automation

Every Cargo package can include a file named build.rs at the crate root. Cargo compiles and executes this file before compiling your crate. The build script communicates back to Cargo through println! instructions on stdout.

What build.rs Is and When It Runs

┌─────────────────────────────────────────────────────────┐
│                    Cargo Build Pipeline                  │
│                                                         │
│  1. Resolve dependencies                                │
│  2. Download crates                                     │
│  3. Compile build.rs  ← ordinary Rust, runs on HOST     │
│  4. Execute build.rs  ← stdout → Cargo instructions     │
│  5. Compile the crate (using instructions from step 4)  │
│  6. Link                                                │
└─────────────────────────────────────────────────────────┘

Key facts:

  • build.rs runs on the host machine, not the target. During cross-compilation, the build script runs on your development machine even when the final binary targets a different architecture.
  • The build script’s scope is limited to its own package. It cannot affect how other crates compile — unless the package declares a links key in Cargo.toml, which enables passing metadata to dependent crates via cargo::metadata=KEY=VALUE.
  • It runs every time Cargo detects a change — unless you emit cargo::rerun-if-changed instructions to limit re-runs.

Note (Rust 1.71+): Since Rust 1.71, Cargo fingerprints the compiled build.rs binary — if the binary is identical, it won’t re-run even if source timestamps changed. However, cargo::rerun-if-changed=build.rs is still valuable: without any rerun-if-changed instruction, Cargo re-runs build.rs whenever any file in the package changes (not just build.rs). Emitting cargo::rerun-if-changed=build.rs limits re-runs to only when build.rs itself changes — a significant compile-time saving in large crates.

  • It can emit cfg flags, environment variables, linker arguments, and file paths that the main crate consumes.

The minimal Cargo.toml entry:

[package]
name = "my-crate"
version = "0.1.0"
edition = "2021"
build = "build.rs"       # default — Cargo looks for build.rs automatically
# build = "src/build.rs" # or put it elsewhere

The Cargo Instruction Protocol

Your build script communicates with Cargo by printing instructions to stdout. Since Rust 1.77, the preferred prefix is cargo:: (replacing the older cargo: single-colon form).

InstructionPurpose
cargo::rerun-if-changed=PATHOnly re-run build.rs when PATH changes
cargo::rerun-if-env-changed=VAROnly re-run when environment variable VAR changes
cargo::rustc-link-lib=NAMELink against native library NAME
cargo::rustc-link-search=PATHAdd PATH to the library search path
cargo::rustc-cfg=KEYSet a #[cfg(KEY)] flag for conditional compilation
cargo::rustc-cfg=KEY="VALUE"Set a #[cfg(KEY = "VALUE")] flag
cargo::rustc-env=KEY=VALUESet an environment variable accessible via env!()
cargo::rustc-cdylib-link-arg=FLAGPass FLAG to the linker for cdylib targets
cargo::warning=MESSAGEDisplay a warning during compilation
cargo::metadata=KEY=VALUEStore metadata readable by dependent crates
// build.rs — minimal example
fn main() {
    // Only re-run if build.rs itself changes
    println!("cargo::rerun-if-changed=build.rs");

    // Set a compile-time environment variable
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".into());
    println!("cargo::rustc-env=BUILD_TIMESTAMP={timestamp}");
}

Pattern 1: Compile-Time Constants

The most common use case: baking build metadata into the binary so you can report it at runtime (git hash, build date, CI job ID).

// build.rs
use std::process::Command;

fn main() {
    println!("cargo::rerun-if-changed=.git/HEAD");
    println!("cargo::rerun-if-changed=.git/refs");

    // Git commit hash
    let output = Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .output()
        .expect("git not found");
    let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
    println!("cargo::rustc-env=GIT_HASH={git_hash}");

    // Build profile (debug or release)
    let profile = std::env::var("PROFILE").unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=BUILD_PROFILE={profile}");

    // Target triple
    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=BUILD_TARGET={target}");
}
#![allow(unused)]
fn main() {
// src/main.rs — consuming the build-time values
fn print_version() {
    println!(
        "{} {} (git:{} target:{} profile:{})",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION"),
        env!("GIT_HASH"),
        env!("BUILD_TARGET"),
        env!("BUILD_PROFILE"),
    );
}
}

Built-in Cargo environment variables you get for free, no build.rs needed: CARGO_PKG_NAME, CARGO_PKG_VERSION, CARGO_PKG_AUTHORS, CARGO_PKG_DESCRIPTION, CARGO_MANIFEST_DIR. See the full list.

Pattern 2: Compiling C/C++ Code with the cc Crate

When your Rust crate wraps a C library or needs a small C helper (common in hardware interfaces), the cc crate simplifies compilation inside build.rs.

# Cargo.toml
[build-dependencies]
cc = "1.0"
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=csrc/");

    cc::Build::new()
        .file("csrc/ipmi_raw.c")
        .file("csrc/smbios_parser.c")
        .include("csrc/include")
        .flag("-Wall")
        .flag("-Wextra")
        .opt_level(2)
        .compile("diag_helpers");
    // This produces libdiag_helpers.a and emits the right
    // cargo::rustc-link-lib and cargo::rustc-link-search instructions.
}
#![allow(unused)]
fn main() {
// src/lib.rs — FFI bindings to the compiled C code
extern "C" {
    fn ipmi_raw_command(
        netfn: u8,
        cmd: u8,
        data: *const u8,
        data_len: usize,
        response: *mut u8,
        response_len: *mut usize,
    ) -> i32;
}

/// Safe wrapper around the raw IPMI command interface.
/// Assumes: enum IpmiError { CommandFailed(i32), ... }
pub fn send_ipmi_command(netfn: u8, cmd: u8, data: &[u8]) -> Result<Vec<u8>, IpmiError> {
    let mut response = vec![0u8; 256];
    let mut response_len: usize = response.len();

    // SAFETY: response buffer is large enough and response_len is correctly initialized.
    let rc = unsafe {
        ipmi_raw_command(
            netfn,
            cmd,
            data.as_ptr(),
            data.len(),
            response.as_mut_ptr(),
            &mut response_len,
        )
    };

    if rc != 0 {
        return Err(IpmiError::CommandFailed(rc));
    }
    response.truncate(response_len);
    Ok(response)
}
}

For C++ code, use .cpp(true) and .flag("-std=c++17"):

// build.rs — C++ variant
fn main() {
    println!("cargo::rerun-if-changed=cppsrc/");

    cc::Build::new()
        .cpp(true)
        .file("cppsrc/vendor_parser.cpp")
        .flag("-std=c++17")
        .flag("-fno-exceptions")    // match Rust's no-exception model
        .compile("vendor_helpers");
}

Pattern 3: Protocol Buffers and Code Generation

Build scripts excel at code generation — turning .proto, .fbs, or .json schema files into Rust source at compile time. Here’s the protobuf pattern using prost-build:

# Cargo.toml
[build-dependencies]
prost-build = "0.13"
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=proto/");

    prost_build::compile_protos(
        &["proto/diagnostics.proto", "proto/telemetry.proto"],
        &["proto/"],
    )
    .expect("Failed to compile protobuf definitions");
}
#![allow(unused)]
fn main() {
// src/lib.rs — include the generated code
pub mod diagnostics {
    include!(concat!(env!("OUT_DIR"), "/diagnostics.rs"));
}

pub mod telemetry {
    include!(concat!(env!("OUT_DIR"), "/telemetry.rs"));
}
}

OUT_DIR is a Cargo-provided directory where build scripts should place generated files. Each crate gets its own OUT_DIR under target/.

Pattern 4: Linking System Libraries with pkg-config

For system libraries that provide .pc files (systemd, OpenSSL, libpci), the pkg-config crate probes the system and emits the right link instructions:

# Cargo.toml
[build-dependencies]
pkg-config = "0.3"
// build.rs
fn main() {
    // Probe for libpci (used for PCIe device enumeration)
    pkg_config::Config::new()
        .atleast_version("3.6.0")
        .probe("libpci")
        .expect("libpci >= 3.6.0 not found — install pciutils-dev");

    // Probe for libsystemd (optional — for sd_notify integration)
    if pkg_config::probe_library("libsystemd").is_ok() {
        println!("cargo::rustc-cfg=has_systemd");
    }
}
#![allow(unused)]
fn main() {
// src/lib.rs — conditional compilation based on pkg-config probing
#[cfg(has_systemd)]
mod systemd_notify {
    extern "C" {
        fn sd_notify(unset_environment: i32, state: *const std::ffi::c_char) -> i32;
    }

    pub fn notify_ready() {
        let state = std::ffi::CString::new("READY=1").unwrap();
        // SAFETY: state is a valid null-terminated C string.
        unsafe { sd_notify(0, state.as_ptr()) };
    }
}

#[cfg(not(has_systemd))]
mod systemd_notify {
    pub fn notify_ready() {
        // no-op on systems without systemd
    }
}
}

Pattern 5: Feature Detection and Conditional Compilation

Build scripts can probe the compilation environment and set cfg flags that the main crate uses for conditional code paths.

CPU architecture and OS detection (safe — these are compile-time constants):

// build.rs — detect CPU features and OS capabilities
fn main() {
    println!("cargo::rerun-if-changed=build.rs");

    let target = std::env::var("TARGET").unwrap();
    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();

    // Enable AVX2-optimized paths on x86_64
    if target.starts_with("x86_64") {
        println!("cargo::rustc-cfg=has_x86_64");
    }

    // Enable ARM NEON paths on aarch64
    if target.starts_with("aarch64") {
        println!("cargo::rustc-cfg=has_aarch64");
    }

    // Detect if /dev/ipmi0 is available (build-time check)
    if target_os == "linux" && std::path::Path::new("/dev/ipmi0").exists() {
        println!("cargo::rustc-cfg=has_ipmi_device");
    }
}

⚠️ Anti-pattern demonstration — The code below shows a tempting but problematic approach. Do not use this in production.

// build.rs — BAD: runtime hardware detection at build time
fn main() {
    // ANTI-PATTERN: Binary is baked to the BUILD machine's hardware.
    // If you build on a machine with a GPU and deploy to one without,
    // the binary silently assumes a GPU is present.
    if std::process::Command::new("accel-query")
        .arg("--query-gpu=name")
        .arg("--format=csv,noheader")
        .output()
        .is_ok()
    {
        println!("cargo::rustc-cfg=has_accel_device");
    }
}
#![allow(unused)]
fn main() {
// src/gpu.rs — code that adapts based on build-time detection
pub fn query_gpu_info() -> GpuResult {
    #[cfg(has_accel_device)]
    {
        run_accel_query()
    }

    #[cfg(not(has_accel_device))]
    {
        GpuResult::NotAvailable("accel-query not found at build time".into())
    }
}
}

⚠️ Why this is wrong: Runtime device detection is almost always better than build-time detection for optional hardware. The binary produced above is tied to the build machine’s hardware configuration — it will behave differently on the deployment target. Use build-time detection only for capabilities that are truly fixed at compile time (architecture, OS, library availability). For hardware like GPUs, detect at runtime with which accel-query or accel-mgmt probing.

Anti-Patterns and Pitfalls

Anti-PatternWhy It’s BadFix
No rerun-if-changedbuild.rs runs on every build, slowing iterationAlways emit at least cargo::rerun-if-changed=build.rs
Network calls in build.rsBuilds fail offline, non-reproducibleVendor files or use a separate fetch step
Writing to src/Cargo doesn’t expect source to change during buildWrite to OUT_DIR and use include!()
Heavy computationSlows every cargo buildCache results in OUT_DIR, gate with rerun-if-changed
Ignoring cross-compilationUsing Command::new("gcc") without respecting $CCUse the cc crate which handles cross-compilation toolchains
Panicking without contextunwrap() gives opaque “build script failed” errorUse .expect("descriptive message") or print cargo::warning=

Application: Embedding Build Metadata

The project currently uses env!("CARGO_PKG_VERSION") for version reporting. A build script would extend this with richer metadata:

// build.rs — proposed addition
fn main() {
    println!("cargo::rerun-if-changed=.git/HEAD");
    println!("cargo::rerun-if-changed=.git/refs");
    println!("cargo::rerun-if-changed=build.rs");

    // Embed git hash for traceability in diagnostic reports
    if let Ok(output) = std::process::Command::new("git")
        .args(["rev-parse", "--short=10", "HEAD"])
        .output()
    {
        let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
        println!("cargo::rustc-env=APP_GIT_HASH={hash}");
    } else {
        println!("cargo::rustc-env=APP_GIT_HASH=unknown");
    }

    // Embed build timestamp for report correlation
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".into());
    println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}");

    // Emit target triple — useful in multi-arch deployment
    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=APP_TARGET={target}");
}
#![allow(unused)]
fn main() {
// src/version.rs — consuming the metadata
pub struct BuildInfo {
    pub version: &'static str,
    pub git_hash: &'static str,
    pub build_epoch: &'static str,
    pub target: &'static str,
}

pub const BUILD_INFO: BuildInfo = BuildInfo {
    version: env!("CARGO_PKG_VERSION"),
    git_hash: env!("APP_GIT_HASH"),
    build_epoch: env!("APP_BUILD_EPOCH"),
    target: env!("APP_TARGET"),
};

impl BuildInfo {
    /// Parse the epoch at runtime when needed (const &str → u64 is not
    /// possible on stable Rust — there is no const fn for str-to-int).
    pub fn build_epoch_secs(&self) -> u64 {
        self.build_epoch.parse().unwrap_or(0)
    }
}

impl std::fmt::Display for BuildInfo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "DiagTool v{} (git:{} target:{})",
            self.version, self.git_hash, self.target
        )
    }
}
}

Key insight from the project: The codebase has zero build.rs files across all many crates because it’s pure Rust with no C dependencies, no codegen, and no system library linking. When you need these, build.rs is the tool — but don’t add it “just because.” The absence of build scripts in a large codebase is a feature, not a gap. See Dependency Management for how the project manages its supply chain without custom build logic. is a positive signal of a clean architecture.

Try It Yourself

  1. Embed git metadata: Create a build.rs that emits APP_GIT_HASH and APP_BUILD_EPOCH as environment variables. Consume them with env!() in main.rs and print the build info. Verify the hash changes after a commit.

  2. Probe a system library: Write a build.rs that uses pkg-config to probe for libz (zlib). Emit cargo::rustc-cfg=has_zlib if found. In main.rs, conditionally print “zlib available” or “zlib not found” based on the cfg flag.

  3. Trigger a build failure intentionally: Remove the rerun-if-changed line from your build.rs and observe how many times it reruns during cargo build and cargo test. Then add it back and compare.

Reproducible Builds

Chapter 1 teaches embedding timestamps and git hashes into binaries. This is useful for traceability, but it conflicts with reproducible builds — the property that building the same source always produces the same binary.

The tension:

GoalAchievementCost
TraceabilityAPP_BUILD_EPOCH in binaryEvery build is unique — can’t verify integrity
Reproducibilitycargo build --locked always produces same outputNo build-time metadata

Practical resolution:

# 1. Always use --locked in CI (ensures Cargo.lock is respected)
cargo build --release --locked
# Fails if Cargo.lock is missing or outdated — catches "works on my machine"

# 2. For reproducibility-critical builds, set SOURCE_DATE_EPOCH
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) cargo build --release --locked
# Uses the last commit timestamp instead of "now" — same commit = same binary
#![allow(unused)]
fn main() {
// In build.rs: respect SOURCE_DATE_EPOCH for reproducibility
let timestamp = std::env::var("SOURCE_DATE_EPOCH")
    .unwrap_or_else(|_| {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs().to_string())
            .unwrap_or_else(|_| "0".into())
    });
println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}");
}

Best practice: Use SOURCE_DATE_EPOCH in build scripts so release builds are reproducible (git-hash + locked deps + deterministic timestamp = same binary), while dev builds still get live timestamps for convenience.

Build Pipeline Decision Diagram

flowchart TD
    START["Need compile-time work?"] -->|No| SKIP["No build.rs needed"]
    START -->|Yes| WHAT{"What kind?"}
    
    WHAT -->|"Embed metadata"| P1["Pattern 1\nCompile-Time Constants"]
    WHAT -->|"Compile C/C++"| P2["Pattern 2\ncc crate"]
    WHAT -->|"Code generation"| P3["Pattern 3\nprost-build / tonic-build"]
    WHAT -->|"Link system lib"| P4["Pattern 4\npkg-config"]
    WHAT -->|"Detect features"| P5["Pattern 5\ncfg flags"]
    
    P1 --> RERUN["Always emit\ncargo::rerun-if-changed"]
    P2 --> RERUN
    P3 --> RERUN
    P4 --> RERUN
    P5 --> RERUN
    
    style SKIP fill:#91e5a3,color:#000
    style RERUN fill:#ffd43b,color:#000
    style P1 fill:#e3f2fd,color:#000
    style P2 fill:#e3f2fd,color:#000
    style P3 fill:#e3f2fd,color:#000
    style P4 fill:#e3f2fd,color:#000
    style P5 fill:#e3f2fd,color:#000

🏋️ Exercises

🟢 Exercise 1: Version Stamp

Create a minimal crate with a build.rs that embeds the current git hash and build profile into environment variables. Print them from main(). Verify the output changes between debug and release builds.

Solution
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=.git/HEAD");
    println!("cargo::rerun-if-changed=build.rs");

    let hash = std::process::Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .output()
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=GIT_HASH={hash}");
    println!("cargo::rustc-env=BUILD_PROFILE={}", std::env::var("PROFILE").unwrap_or_default());
}
// src/main.rs
fn main() {
    println!("{} v{} (git:{} profile:{})",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION"),
        env!("GIT_HASH"),
        env!("BUILD_PROFILE"),
    );
}
cargo run          # shows profile:debug
cargo run --release # shows profile:release

🟡 Exercise 2: Conditional System Library

Write a build.rs that probes for both libz and libpci using pkg-config. Emit a cfg flag for each one found. In main.rs, print which libraries were detected at build time.

Solution
# Cargo.toml
[build-dependencies]
pkg-config = "0.3"
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=build.rs");
    if pkg_config::probe_library("zlib").is_ok() {
        println!("cargo::rustc-cfg=has_zlib");
    }
    if pkg_config::probe_library("libpci").is_ok() {
        println!("cargo::rustc-cfg=has_libpci");
    }
}
// src/main.rs
fn main() {
    #[cfg(has_zlib)]
    println!("✅ zlib detected");
    #[cfg(not(has_zlib))]
    println!("❌ zlib not found");

    #[cfg(has_libpci)]
    println!("✅ libpci detected");
    #[cfg(not(has_libpci))]
    println!("❌ libpci not found");
}

Key Takeaways

  • build.rs runs on the host at compile time — always emit cargo::rerun-if-changed to avoid unnecessary rebuilds
  • Use the cc crate (not raw gcc commands) for C/C++ compilation — it handles cross-compilation toolchains correctly
  • Write generated files to OUT_DIR, never to src/ — Cargo doesn’t expect source to change during builds
  • Prefer runtime detection over build-time detection for optional hardware
  • Use SOURCE_DATE_EPOCH to make builds reproducible when embedding timestamps

Cross-Compilation — One Source, Many Targets 🟡

What you’ll learn:

  • How Rust target triples work and how to add them with rustup
  • Building static musl binaries for container/cloud deployment
  • Cross-compiling to ARM (aarch64) with native toolchains, cross, and cargo-zigbuild
  • Setting up GitHub Actions matrix builds for multi-architecture CI

Cross-references: Build Scripts — build.rs runs on HOST during cross-compilation · Release Profiles — LTO and strip settings for cross-compiled release binaries · Windows — Windows cross-compilation and no_std targets

Cross-compilation means building an executable on one machine (the host) that runs on a different machine (the target). The host might be your x86_64 laptop; the target might be an ARM server, a musl-based container, or even a Windows machine. Rust makes this remarkably feasible because rustc is already a cross-compiler — it just needs the right target libraries and a compatible linker.

The Target Triple Anatomy

Every Rust compilation target is identified by a target triple (which often has four parts despite the name):

<arch>-<vendor>-<os>-<env>

Examples:
  x86_64  - unknown - linux  - gnu      ← standard Linux (glibc)
  x86_64  - unknown - linux  - musl     ← static Linux (musl libc)
  aarch64 - unknown - linux  - gnu      ← ARM 64-bit Linux
  x86_64  - pc      - windows- msvc     ← Windows with MSVC
  aarch64 - apple   - darwin             ← macOS on Apple Silicon
  x86_64  - unknown - none              ← bare metal (no OS)

List all available targets:

# Show all targets rustc can compile to (~250 targets)
rustc --print target-list | wc -l

# Show installed targets on your system
rustup target list --installed

# Show current default target
rustc -vV | grep host

Installing Toolchains with rustup

# Add target libraries (Rust std for that target)
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-gnu

# Now you can cross-compile:
cargo build --target x86_64-unknown-linux-musl
cargo build --target aarch64-unknown-linux-gnu  # needs a linker — see below

What rustup target add gives you: the pre-compiled std, core, and alloc libraries for that target. It does not give you a C linker or C library. For targets that need a C toolchain (most gnu targets), you need to install one separately.

# Ubuntu/Debian — install the cross-linker for aarch64
sudo apt install gcc-aarch64-linux-gnu

# Ubuntu/Debian — install musl toolchain for static builds
sudo apt install musl-tools

# Fedora
sudo dnf install gcc-aarch64-linux-gnu

.cargo/config.toml — Per-Target Configuration

Instead of passing --target on every command, configure defaults in .cargo/config.toml at your project root or home directory:

# .cargo/config.toml

# Default target for this project (optional — omit to keep native default)
# [build]
# target = "x86_64-unknown-linux-musl"

# Linker for aarch64 cross-compilation
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
rustflags = ["-C", "target-feature=+crc"]

# Linker for musl static builds (usually just the system gcc works)
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
rustflags = ["-C", "target-feature=+crc,+aes"]

# ARM 32-bit (Raspberry Pi, embedded)
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"

# Environment variables for all targets
[env]
# Example: set a custom sysroot
# SYSROOT = "/opt/cross/sysroot"

Config file search order (first match wins):

  1. <project>/.cargo/config.toml
  2. <project>/../.cargo/config.toml (parent directories, walking up)
  3. $CARGO_HOME/config.toml (usually ~/.cargo/config.toml)

Static Binaries with musl

For deploying to minimal containers (Alpine, scratch Docker images) or systems where you can’t control the glibc version, build with musl:

# Install musl target
rustup target add x86_64-unknown-linux-musl
sudo apt install musl-tools  # provides musl-gcc

# Build a fully static binary
cargo build --release --target x86_64-unknown-linux-musl

# Verify it's static
file target/x86_64-unknown-linux-musl/release/diag_tool
# → ELF 64-bit LSB executable, x86-64, statically linked

ldd target/x86_64-unknown-linux-musl/release/diag_tool
# → not a dynamic executable

Static vs dynamic trade-offs:

Aspectglibc (dynamic)musl (static)
Binary sizeSmaller (shared libs)Larger (~5-15 MB increase)
PortabilityNeeds matching glibc versionRuns anywhere on Linux
DNS resolutionFull nsswitch supportBasic resolver (no mDNS)
DeploymentNeeds sysroot or containerSingle binary, no deps
PerformanceSlightly faster mallocSlightly slower malloc
dlopen() supportYesNo

For the project: A static musl build is ideal for deployment to diverse server hardware where you can’t guarantee the host OS version. The single-binary deployment model eliminates “works on my machine” issues.

Cross-Compiling to ARM (aarch64)

ARM servers (AWS Graviton, Ampere Altra, Grace) are increasingly common in data centers. Cross-compiling for aarch64 from an x86_64 host:

# Step 1: Install target + cross-linker
rustup target add aarch64-unknown-linux-gnu
sudo apt install gcc-aarch64-linux-gnu

# Step 2: Configure linker in .cargo/config.toml (see above)

# Step 3: Build
cargo build --release --target aarch64-unknown-linux-gnu

# Step 4: Verify the binary
file target/aarch64-unknown-linux-gnu/release/diag_tool
# → ELF 64-bit LSB executable, ARM aarch64

Running tests for the target architecture requires either:

  • An actual ARM machine
  • QEMU user-mode emulation
# Install QEMU user-mode (runs ARM binaries on x86_64)
sudo apt install qemu-user qemu-user-static binfmt-support

# Now cargo test can run cross-compiled tests through QEMU
cargo test --target aarch64-unknown-linux-gnu
# (Slow — each test binary is emulated. Use for CI validation, not daily dev.)

Configure QEMU as the test runner in .cargo/config.toml:

[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64-static -L /usr/aarch64-linux-gnu"

The cross Tool — Docker-Based Cross-Compilation

The cross tool provides a zero-setup cross-compilation experience using pre-configured Docker images:

# Install cross (from crates.io — stable releases)
cargo install cross
# Or from git for latest features (less stable):
# cargo install cross --git https://github.com/cross-rs/cross

# Cross-compile — no toolchain setup needed!
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --target x86_64-unknown-linux-musl
cross build --release --target armv7-unknown-linux-gnueabihf

# Cross-test — QEMU included in the Docker image
cross test --target aarch64-unknown-linux-gnu

How it works: cross replaces cargo and runs the build inside a Docker container that has the correct cross-compilation toolchain pre-installed. Your source is mounted into the container, and the output goes to your normal target/ directory.

Customizing the Docker image with Cross.toml:

# Cross.toml
[target.aarch64-unknown-linux-gnu]
# Use a custom Docker image with extra system libraries
image = "my-registry/cross-aarch64:latest"

# Pre-install system packages
pre-build = [
    "dpkg --add-architecture arm64",
    "apt-get update && apt-get install -y libpci-dev:arm64"
]

[target.aarch64-unknown-linux-gnu.env]
# Pass environment variables into the container
passthrough = ["CI", "GITHUB_TOKEN"]

cross requires Docker (or Podman) but eliminates the need to manually install cross-compilers, sysroots, and QEMU. It’s the recommended approach for CI.

Using Zig as a Cross-Compilation Linker

Zig bundles a C compiler and cross-compilation sysroot for ~40 targets in a single ~40 MB download. This makes it a remarkably convenient cross-linker for Rust:

# Install Zig (single binary, no package manager needed)
# Download from https://ziglang.org/download/
# Or via package manager:
sudo snap install zig --classic --beta  # Ubuntu
brew install zig                          # macOS

# Install cargo-zigbuild
cargo install cargo-zigbuild

Why Zig? The key advantage is glibc version targeting. Zig lets you specify the exact glibc version to link against, ensuring your binary runs on older Linux distributions:

# Build for glibc 2.17 (CentOS 7 / RHEL 7 compatibility)
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17

# Build for aarch64 with glibc 2.28 (Ubuntu 18.04+)
cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.28

# Build for musl (fully static)
cargo zigbuild --release --target x86_64-unknown-linux-musl

The .2.17 suffix is a Zig extension — it tells Zig’s linker to use glibc 2.17 symbol versions, so the resulting binary runs on CentOS 7 and later. No Docker, no sysroot management, no cross-compiler installation.

Comparison: cross vs cargo-zigbuild vs manual:

FeatureManualcrosscargo-zigbuild
Setup effortHigh (install toolchain per target)Low (needs Docker)Low (single binary)
Docker requiredNoYesNo
glibc version targetingNo (uses host glibc)No (uses container glibc)Yes (exact version)
Test executionNeeds QEMUIncludedNeeds QEMU
macOS → LinuxDifficultEasyEasy
Linux → macOSVery difficultNot supportedLimited
Binary size overheadNoneNoneNone

CI Pipeline: GitHub Actions Matrix

A production-grade CI workflow that builds for multiple targets:

# .github/workflows/cross-build.yml
name: Cross-Platform Build

on: [push, pull_request]

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
            name: linux-x86_64
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            name: linux-x86_64-static
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            name: linux-aarch64
            use_cross: true
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            name: windows-x86_64

    runs-on: ${{ matrix.os }}
    name: Build (${{ matrix.name }})

    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Install musl tools
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: sudo apt-get install -y musl-tools

      - name: Install cross
        if: matrix.use_cross
        run: cargo install cross

      - name: Build (native)
        if: "!matrix.use_cross"
        run: cargo build --release --target ${{ matrix.target }}

      - name: Build (cross)
        if: matrix.use_cross
        run: cross build --release --target ${{ matrix.target }}

      - name: Run tests
        if: "!matrix.use_cross"
        run: cargo test --target ${{ matrix.target }}

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: diag_tool-${{ matrix.name }}
          path: target/${{ matrix.target }}/release/diag_tool*

Application: Multi-Architecture Server Builds

The binary currently has no cross-compilation setup. For a hardware diagnostics tool deployed across diverse server fleets, the recommended addition:

my_workspace/
├── .cargo/
│   └── config.toml          ← linker configs per target
├── Cross.toml                ← cross tool configuration
└── .github/workflows/
    └── cross-build.yml       ← CI matrix for 3 targets

Recommended .cargo/config.toml:

# .cargo/config.toml for the project

# Release profile optimizations (already in Cargo.toml, shown for reference)
# [profile.release]
# lto = true
# codegen-units = 1
# panic = "abort"
# strip = true

# aarch64 for ARM servers (Graviton, Ampere, Grace)
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

# musl for portable static binaries
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"

Recommended build targets:

TargetUse CaseDeploy To
x86_64-unknown-linux-gnuDefault native buildStandard x86 servers
x86_64-unknown-linux-muslStatic binary, any distroContainers, minimal hosts
aarch64-unknown-linux-gnuARM serversGraviton, Ampere, Grace

Key insight: The [profile.release] in the workspace’s root Cargo.toml already has lto = true, codegen-units = 1, panic = "abort", and strip = true — an ideal release profile for cross-compiled deployment binaries (see Release Profiles for the full impact table). Combined with musl, this produces a single ~10 MB static binary with no runtime dependencies.

Troubleshooting Cross-Compilation

SymptomCauseFix
linker 'aarch64-linux-gnu-gcc' not foundMissing cross-linker toolchainsudo apt install gcc-aarch64-linux-gnu
cannot find -lssl (musl target)System OpenSSL is glibc-linkedUse vendored feature: openssl = { version = "0.10", features = ["vendored"] }
build.rs runs wrong binarybuild.rs runs on HOST, not targetCheck CARGO_CFG_TARGET_OS in build.rs, not cfg!(target_os)
Tests pass locally, fail in crossDocker image missing test fixturesMount test data via Cross.toml: [build.env] volumes = ["./TestArea:/TestArea"]
undefined reference to __cxa_thread_atexit_implOld glibc on targetUse cargo-zigbuild with explicit glibc version: --target x86_64-unknown-linux-gnu.2.17
Binary segfaults on ARMCompiled for wrong ARM variantVerify target triple matches hardware: aarch64-unknown-linux-gnu for 64-bit ARM
GLIBC_2.XX not found at runtimeBuild machine has newer glibcUse musl for static builds, or cargo-zigbuild for glibc version pinning

Cross-Compilation Decision Tree

flowchart TD
    START["Need to cross-compile?"] --> STATIC{"Static binary?"}
    
    STATIC -->|Yes| MUSL["musl target\n--target x86_64-unknown-linux-musl"]
    STATIC -->|No| GLIBC{"Need old glibc?"}
    
    GLIBC -->|Yes| ZIG["cargo-zigbuild\n--target x86_64-unknown-linux-gnu.2.17"]
    GLIBC -->|No| ARCH{"Target arch?"}
    
    ARCH -->|"Same arch"| NATIVE["Native toolchain\nrustup target add + linker"]
    ARCH -->|"ARM/other"| DOCKER{"Docker available?"}
    
    DOCKER -->|Yes| CROSS["cross build\nDocker-based, zero setup"]
    DOCKER -->|No| MANUAL["Manual sysroot\napt install gcc-aarch64-linux-gnu"]
    
    style MUSL fill:#91e5a3,color:#000
    style ZIG fill:#91e5a3,color:#000
    style CROSS fill:#91e5a3,color:#000
    style NATIVE fill:#e3f2fd,color:#000
    style MANUAL fill:#ffd43b,color:#000

🏋️ Exercises

🟢 Exercise 1: Static musl Binary

Build any Rust binary for x86_64-unknown-linux-musl. Verify it’s statically linked using file and ldd.

Solution
rustup target add x86_64-unknown-linux-musl
cargo new hello-static && cd hello-static
cargo build --release --target x86_64-unknown-linux-musl

# Verify
file target/x86_64-unknown-linux-musl/release/hello-static
# Output: ... statically linked ...

ldd target/x86_64-unknown-linux-musl/release/hello-static
# Output: not a dynamic executable

🟡 Exercise 2: GitHub Actions Cross-Build Matrix

Write a GitHub Actions workflow that builds a Rust project for three targets: x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl, and aarch64-unknown-linux-gnu. Use a matrix strategy.

Solution
name: Cross-build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-gnu
          - x86_64-unknown-linux-musl
          - aarch64-unknown-linux-gnu
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - name: Install cross
        run: cargo install cross --locked
      - name: Build
        run: cross build --release --target ${{ matrix.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: binary-${{ matrix.target }}
          path: target/${{ matrix.target }}/release/my-binary

Key Takeaways

  • Rust’s rustc is already a cross-compiler — you just need the right target and linker
  • musl produces fully static binaries with zero runtime dependencies — ideal for containers
  • cargo-zigbuild solves the “which glibc version” problem for enterprise Linux targets
  • cross is the easiest path for ARM and other exotic targets — Docker handles the sysroot
  • Always test with file and ldd to verify the binary matches your deployment target

Benchmarking — Measuring What Matters 🟡

What you’ll learn:

  • Why naive timing with Instant::now() produces unreliable results
  • Statistical benchmarking with Criterion.rs and the lighter Divan alternative
  • Profiling hot spots with perf, flamegraphs, and PGO
  • Setting up continuous benchmarking in CI to catch regressions automatically

Cross-references: Release Profiles — once you find the hot spot, optimize the binary · CI/CD Pipeline — benchmark job in the pipeline · Code Coverage — coverage tells you what’s tested, benchmarks tell you what’s fast

“We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.” — Donald Knuth

The hard part isn’t writing benchmarks — it’s writing benchmarks that produce meaningful, reproducible, actionable numbers. This chapter covers the tools and techniques that get you from “it seems fast” to “we have statistical evidence that PR #347 regressed parsing throughput by 4.2%.”

Why Not std::time::Instant?

The temptation:

// ❌ Naive benchmarking — unreliable results
use std::time::Instant;

fn main() {
    let start = Instant::now();
    let result = parse_device_query_output(&sample_data);
    let elapsed = start.elapsed();
    println!("Parsing took {:?}", elapsed);
    // Problem 1: Compiler may optimize away `result` (dead code elimination)
    // Problem 2: Single sample — no statistical significance
    // Problem 3: CPU frequency scaling, thermal throttling, other processes
    // Problem 4: Cold cache vs warm cache not controlled
}

Problems with manual timing:

  1. Dead code elimination — the compiler may skip the computation entirely if the result isn’t used.
  2. No warm-up — the first run includes cache misses, JIT effects (irrelevant in Rust, but OS page faults apply), and lazy initialization.
  3. No statistical analysis — a single measurement tells you nothing about variance, outliers, or confidence intervals.
  4. No regression detection — you can’t compare against previous runs.

Criterion.rs — Statistical Benchmarking

Criterion.rs is the de facto standard for Rust micro-benchmarks. It uses statistical methods to produce reliable measurements and detects performance regressions automatically.

Setup:

# Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports", "cargo_bench_support"] }

[[bench]]
name = "parsing_bench"
harness = false  # Use Criterion's harness, not the built-in test harness

A complete benchmark:

#![allow(unused)]
fn main() {
// benches/parsing_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};

/// Data type for parsed GPU information
#[derive(Debug, Clone)]
struct GpuInfo {
    index: u32,
    name: String,
    temp_c: u32,
    power_w: f64,
}

/// The function under test — simulate parsing device-query CSV output
fn parse_gpu_csv(input: &str) -> Vec<GpuInfo> {
    input
        .lines()
        .filter(|line| !line.starts_with('#'))
        .filter_map(|line| {
            let fields: Vec<&str> = line.split(", ").collect();
            if fields.len() >= 4 {
                Some(GpuInfo {
                    index: fields[0].parse().ok()?,
                    name: fields[1].to_string(),
                    temp_c: fields[2].parse().ok()?,
                    power_w: fields[3].parse().ok()?,
                })
            } else {
                None
            }
        })
        .collect()
}

fn bench_parse_gpu_csv(c: &mut Criterion) {
    // Representative test data
    let small_input = "0, Acme Accel-V1-80GB, 32, 65.5\n\
                       1, Acme Accel-V1-80GB, 34, 67.2\n";

    let large_input = (0..64)
        .map(|i| format!("{i}, Acme Accel-X1-80GB, {}, {:.1}\n", 30 + i % 20, 60.0 + i as f64))
        .collect::<String>();

    c.bench_function("parse_2_gpus", |b| {
        b.iter(|| parse_gpu_csv(black_box(small_input)))
    });

    c.bench_function("parse_64_gpus", |b| {
        b.iter(|| parse_gpu_csv(black_box(&large_input)))
    });
}

criterion_group!(benches, bench_parse_gpu_csv);
criterion_main!(benches);
}

Running and reading results:

# Run all benchmarks
cargo bench

# Run a specific benchmark by name
cargo bench -- parse_64

# Output:
# parse_2_gpus        time:   [1.2345 µs  1.2456 µs  1.2578 µs]
#                      ▲            ▲           ▲
#                      │       confidence interval
#                   lower 95%    median    upper 95%
#
# parse_64_gpus       time:   [38.123 µs  38.456 µs  38.812 µs]
#                     change: [-1.2345% -0.5678% +0.1234%] (p = 0.12 > 0.05)
#                     No change in performance detected.

What black_box() does: It’s a compiler hint that prevents dead-code elimination and over-aggressive constant folding. The compiler cannot see through black_box, so it must actually compute the result.

Parameterized Benchmarks and Benchmark Groups

Compare multiple implementations or input sizes:

#![allow(unused)]
fn main() {
// benches/comparison_bench.rs
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId, Throughput};

fn bench_parsing_strategies(c: &mut Criterion) {
    let mut group = c.benchmark_group("csv_parsing");

    // Test across different input sizes
    for num_gpus in [1, 8, 32, 64, 128] {
        let input = generate_gpu_csv(num_gpus);

        // Set throughput for bytes-per-second reporting
        group.throughput(Throughput::Bytes(input.len() as u64));

        group.bench_with_input(
            BenchmarkId::new("split_based", num_gpus),
            &input,
            |b, input| b.iter(|| parse_split(input)),
        );

        group.bench_with_input(
            BenchmarkId::new("regex_based", num_gpus),
            &input,
            |b, input| b.iter(|| parse_regex(input)),
        );

        group.bench_with_input(
            BenchmarkId::new("nom_based", num_gpus),
            &input,
            |b, input| b.iter(|| parse_nom(input)),
        );
    }
    group.finish();
}

criterion_group!(benches, bench_parsing_strategies);
criterion_main!(benches);
}

Output: Criterion generates an HTML report at target/criterion/report/index.html with violin plots, comparison charts, and regression analysis — open in a browser.

Divan — A Lighter Alternative

Divan is a newer benchmarking framework that uses attribute macros instead of Criterion’s macro DSL:

# Cargo.toml
[dev-dependencies]
divan = "0.1"

[[bench]]
name = "parsing_bench"
harness = false
// benches/parsing_bench.rs
use divan::black_box;

const SMALL_INPUT: &str = "0, Acme Accel-V1-80GB, 32, 65.5\n\
                          1, Acme Accel-V1-80GB, 34, 67.2\n";

fn generate_gpu_csv(n: usize) -> String {
    (0..n)
        .map(|i| format!("{i}, Acme Accel-X1-80GB, {}, {:.1}\n", 30 + i % 20, 60.0 + i as f64))
        .collect()
}

fn main() {
    divan::main();
}

#[divan::bench]
fn parse_2_gpus() -> Vec<GpuInfo> {
    parse_gpu_csv(black_box(SMALL_INPUT))
}

#[divan::bench(args = [1, 8, 32, 64, 128])]
fn parse_n_gpus(n: usize) -> Vec<GpuInfo> {
    let input = generate_gpu_csv(n);
    parse_gpu_csv(black_box(&input))
}

// Divan output is a clean table:
// ╰─ parse_2_gpus   fastest  │ slowest  │ median   │ mean     │ samples │ iters
//                   1.234 µs │ 1.567 µs │ 1.345 µs │ 1.350 µs │ 100     │ 1600

When to choose Divan over Criterion:

  • Simpler API (attribute macros, less boilerplate)
  • Faster compilation (fewer dependencies)
  • Good for quick perf checks during development

When to choose Criterion:

  • Statistical regression detection across runs
  • HTML reports with charts
  • Established ecosystem, more CI integrations

Profiling with perf and Flamegraphs

Benchmarks tell you how fast — profiling tells you where the time goes.

# Step 1: Build with debug info (release speed, debug symbols)
cargo build --release
# Ensure debug info is available:
# [profile.release]
# debug = true          # Add this temporarily for profiling

# Step 2: Record with perf
perf record --call-graph=dwarf ./target/release/diag_tool --run-diagnostics

# Step 3: Generate a flamegraph
# Install: cargo install flamegraph
# Install: cargo install addr2line --features=bin (optional, speedup cargo-flamegraph)
cargo flamegraph --root -- --run-diagnostics
# Opens an interactive SVG flamegraph

# Alternative: use perf + inferno
perf script | inferno-collapse-perf | inferno-flamegraph > flamegraph.svg

Reading a flamegraph:

  • Width = time spent in that function (wider = slower)
  • Height = call stack depth (taller ≠ slower, just deeper)
  • Bottom = entry point, Top = leaf functions doing actual work
  • Look for wide plateaus at the top — those are your hot spots

Profile-guided optimization (PGO):

# Step 1: Build with instrumentation
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release

# Step 2: Run representative workloads
./target/release/diag_tool --run-full   # generates profiling data

# Step 3: Merge profiling data
# Use the llvm-profdata that matches rustc's LLVM version:
# $(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata
# Or if llvm-tools is installed: rustup component add llvm-tools
llvm-profdata merge -o /tmp/pgo-data/merged.profdata /tmp/pgo-data/

# Step 4: Rebuild with profiling feedback
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/merged.profdata" cargo build --release
# Typical improvement: 5-20% for compute-bound code (parsing, crypto, codegen).
# I/O-bound or syscall-heavy code (like a large project) will see much less benefit
# because the CPU is mostly waiting, not executing hot loops.

Tip: Before spending time on PGO, ensure your release profile already has LTO enabled — it typically delivers a bigger win for less effort.

hyperfine — Quick End-to-End Timing

hyperfine benchmarks entire commands, not individual functions. It’s perfect for measuring overall binary performance:

# Install
cargo install hyperfine
# Or: sudo apt install hyperfine  (Ubuntu 23.04+)

# Basic benchmark
hyperfine './target/release/diag_tool --run-diagnostics'

# Compare two implementations
hyperfine './target/release/diag_tool_v1 --run-diagnostics' \
          './target/release/diag_tool_v2 --run-diagnostics'

# Warm-up runs + minimum iterations
hyperfine --warmup 3 --min-runs 10 './target/release/diag_tool --run-all'

# Export results as JSON for CI comparison
hyperfine --export-json bench.json './target/release/diag_tool --run-all'

When to use hyperfine vs Criterion:

  • hyperfine: whole-binary timing, comparing before/after a refactor, I/O-bound workloads
  • Criterion: micro-benchmarks of individual functions, statistical regression detection

Continuous Benchmarking in CI

Detect performance regressions before they ship:

# .github/workflows/bench.yml
name: Benchmarks

on:
  pull_request:
    paths: ['**/*.rs', 'Cargo.toml', 'Cargo.lock']

jobs:
  benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - name: Run benchmarks
        # Requires criterion = { features = ["cargo_bench_support"] } for --output-format
        run: cargo bench -- --output-format bencher | tee bench_output.txt

      - name: Store benchmark result
        uses: benchmark-action/github-action-benchmark@v1
        with:
          tool: 'cargo'
          output-file-path: bench_output.txt
          github-token: ${{ secrets.GITHUB_TOKEN }}
          auto-push: true
          alert-threshold: '120%'    # Alert if 20% slower
          comment-on-alert: true
          fail-on-alert: true        # Block PR if regression detected

Key CI considerations:

  • Use dedicated benchmark runners (not shared CI) for consistent results
  • Pin the runner to a specific machine type if using cloud CI
  • Store historical data to detect gradual regressions
  • Set thresholds based on your workload’s tolerance (5% for hot paths, 20% for cold)

Application: Parsing Performance

The project has several performance-sensitive parsing paths that would benefit from benchmarks:

Parsing Hot SpotCrateWhy It Matters
accelerator-query CSV/XML outputdevice_diagCalled per-GPU, up to 8× per run
Sensor event parsingevent_logThousands of records on busy servers
PCIe topology JSONtopology_libComplex nested structures, golden-file validated
Report JSON serializationdiag_frameworkFinal report output, size-sensitive
Config JSON loadingconfig_loaderStartup latency

Recommended first benchmark — the topology parser, which already has golden-file test data:

#![allow(unused)]
fn main() {
// topology_lib/benches/parse_bench.rs (proposed)
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
use std::fs;

fn bench_topology_parse(c: &mut Criterion) {
    let mut group = c.benchmark_group("topology_parse");

    for golden_file in ["S2001", "S1015", "S1035", "S1080"] {
        let path = format!("tests/test_data/{golden_file}.json");
        let data = fs::read_to_string(&path).expect("golden file not found");
        group.throughput(Throughput::Bytes(data.len() as u64));

        group.bench_function(golden_file, |b| {
            b.iter(|| {
                topology_lib::TopologyProfile::from_json_str(
                    criterion::black_box(&data)
                )
            });
        });
    }
    group.finish();
}

criterion_group!(benches, bench_topology_parse);
criterion_main!(benches);
}

Try It Yourself

  1. Write a Criterion benchmark: Pick any parsing function in your codebase. Create a benches/ directory, set up a Criterion benchmark that measures throughput in bytes/second. Run cargo bench and examine the HTML report.

  2. Generate a flamegraph: Build your project with debug = true in [profile.release], then run cargo flamegraph -- <your-args>. Identify the three widest stacks at the top of the flamegraph — those are your hot spots.

  3. Compare with hyperfine: Install hyperfine and benchmark the overall execution time of your binary with different flags. Compare it to the per-function times from Criterion. Where does the time go that Criterion doesn’t see? (Answer: I/O, syscalls, process startup.)

Benchmark Tool Selection

flowchart TD
    START["Want to measure performance?"] --> WHAT{"What level?"}

    WHAT -->|"Single function"| CRITERION["Criterion.rs\nStatistical, regression detection"]
    WHAT -->|"Quick function check"| DIVAN["Divan\nLighter, attribute macros"]
    WHAT -->|"Whole binary"| HYPERFINE["hyperfine\nEnd-to-end, wall-clock"]
    WHAT -->|"Find hot spots"| PERF["perf + flamegraph\nCPU sampling profiler"]

    CRITERION --> CI_BENCH["Continuous benchmarking\nin GitHub Actions"]
    PERF --> OPTIMIZE["Profile-Guided\nOptimization (PGO)"]

    style CRITERION fill:#91e5a3,color:#000
    style DIVAN fill:#91e5a3,color:#000
    style HYPERFINE fill:#e3f2fd,color:#000
    style PERF fill:#ffd43b,color:#000
    style CI_BENCH fill:#e3f2fd,color:#000
    style OPTIMIZE fill:#ffd43b,color:#000

🏋️ Exercises

🟢 Exercise 1: First Criterion Benchmark

Create a crate with a function that sorts a Vec<u64> of 10,000 random elements. Write a Criterion benchmark for it, then switch to .sort_unstable() and observe the performance difference in the HTML report.

Solution
# Cargo.toml
[[bench]]
name = "sort_bench"
harness = false

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
rand = "0.8"
#![allow(unused)]
fn main() {
// benches/sort_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rand::Rng;

fn generate_data(n: usize) -> Vec<u64> {
    let mut rng = rand::thread_rng();
    (0..n).map(|_| rng.gen()).collect()
}

fn bench_sort(c: &mut Criterion) {
    let mut group = c.benchmark_group("sort-10k");

    group.bench_function("stable", |b| {
        b.iter_batched(
            || generate_data(10_000),
            |mut data| { data.sort(); black_box(&data); },
            criterion::BatchSize::SmallInput,
        )
    });

    group.bench_function("unstable", |b| {
        b.iter_batched(
            || generate_data(10_000),
            |mut data| { data.sort_unstable(); black_box(&data); },
            criterion::BatchSize::SmallInput,
        )
    });

    group.finish();
}

criterion_group!(benches, bench_sort);
criterion_main!(benches);
}
cargo bench
open target/criterion/sort-10k/report/index.html

🟡 Exercise 2: Flamegraph Hot Spot

Build a project with debug = true in [profile.release], then generate a flamegraph. Identify the top 3 widest stacks.

Solution
# Cargo.toml
[profile.release]
debug = true  # Keep symbols for flamegraph
cargo install flamegraph
cargo flamegraph --release -- <your-args>
# Opens flamegraph.svg in browser
# The widest stacks at the top are your hot spots

Key Takeaways

  • Never benchmark with Instant::now() — use Criterion.rs for statistical rigor and regression detection
  • black_box() prevents the compiler from optimizing away your benchmark target
  • hyperfine measures wall-clock time for the whole binary; Criterion measures individual functions — use both
  • Flamegraphs show where time is spent; benchmarks show how much time is spent
  • Continuous benchmarking in CI catches performance regressions before they ship

Code Coverage — Seeing What Tests Miss 🟢

What you’ll learn:

  • Source-based coverage with cargo-llvm-cov (the most accurate Rust coverage tool)
  • Quick coverage checks with cargo-tarpaulin and Mozilla’s grcov
  • Setting up coverage gates in CI with Codecov and Coveralls
  • A coverage-guided testing strategy that prioritizes high-risk blind spots

Cross-references: Miri and Sanitizers — coverage finds untested code, Miri finds UB in tested code · Benchmarking — coverage shows what’s tested, benchmarks show what’s fast · CI/CD Pipeline — coverage gate in the pipeline

Code coverage measures which lines, branches, or functions your tests actually execute. It doesn’t prove correctness (a covered line can still have bugs), but it reliably reveals blind spots — code paths that no test exercises at all.

With 1,006 tests across many crates, the project has substantial test investment. Coverage analysis answers: “Is that investment reaching the code that matters?”

Source-Based Coverage with llvm-cov

Rust uses LLVM, which provides source-based coverage instrumentation — the most accurate coverage method available. The recommended tool is cargo-llvm-cov:

# Install
cargo install cargo-llvm-cov

# Or via rustup component (for the raw llvm tools)
rustup component add llvm-tools-preview

Basic usage:

# Run tests and show per-file coverage summary
cargo llvm-cov

# Generate HTML report (browsable, line-by-line highlighting)
cargo llvm-cov --html
# Output: target/llvm-cov/html/index.html

# Generate LCOV format (for CI integrations)
cargo llvm-cov --lcov --output-path lcov.info

# Workspace-wide coverage (all crates)
cargo llvm-cov --workspace

# Include only specific packages
cargo llvm-cov --package accel_diag --package topology_lib

# Coverage including doc tests
cargo llvm-cov --doctests

Reading the HTML report:

target/llvm-cov/html/index.html
├── Filename          │ Function │ Line   │ Branch │ Region
├─ accel_diag/src/lib.rs │  78.5%  │ 82.3% │ 61.2% │  74.1%
├─ sel_mgr/src/parse.rs│  95.2%  │ 96.8% │ 88.0% │  93.5%
├─ topology_lib/src/.. │  91.0%  │ 93.4% │ 79.5% │  89.2%
└─ ...

Green = covered    Red = not covered    Yellow = partially covered (branch)

Coverage types explained:

TypeWhat It MeasuresSignificance
Line coverageWhich source lines were executedBasic “was this code reached?”
Branch coverageWhich if/match arms were takenCatches untested conditions
Function coverageWhich functions were calledFinds dead code
Region coverageWhich code regions (sub-expressions) were hitMost granular

cargo-tarpaulin — The Quick Path

cargo-tarpaulin is a Linux-specific coverage tool that’s simpler to set up (no LLVM components needed):

# Install
cargo install cargo-tarpaulin

# Basic coverage report
cargo tarpaulin

# HTML output
cargo tarpaulin --out Html

# With specific options
cargo tarpaulin \
    --workspace \
    --timeout 120 \
    --out Xml Html \
    --output-dir coverage/ \
    --exclude-files "*/tests/*" "*/benches/*" \
    --ignore-panics

# Skip certain crates
cargo tarpaulin --workspace --exclude diag_tool  # exclude the binary crate

tarpaulin vs llvm-cov comparison:

Featurecargo-llvm-covcargo-tarpaulin
AccuracySource-based (most accurate)Ptrace-based (occasional overcounting)
PlatformAny (llvm-based)Linux only
Branch coverageYesLimited
Doc testsYesNo
SetupNeeds llvm-tools-previewSelf-contained
SpeedFaster (compile-time instrumentation)Slower (ptrace overhead)
StabilityVery stableOccasional false positives

Recommendation: Use cargo-llvm-cov for accuracy. Use cargo-tarpaulin when you need a quick check without installing LLVM tools.

grcov — Mozilla’s Coverage Tool

grcov is Mozilla’s coverage aggregator. It consumes raw LLVM profiling data and produces reports in multiple formats:

# Install
cargo install grcov

# Step 1: Build with coverage instrumentation
export RUSTFLAGS="-Cinstrument-coverage"
export LLVM_PROFILE_FILE="target/coverage/%p-%m.profraw"
cargo build --tests

# Step 2: Run tests (generates .profraw files)
cargo test

# Step 3: Aggregate with grcov
grcov target/coverage/ \
    --binary-path target/debug/ \
    --source-dir . \
    --output-types html,lcov \
    --output-path target/coverage/report \
    --branch \
    --ignore-not-existing \
    --ignore "*/tests/*" \
    --ignore "*/.cargo/*"

# Step 4: View report
open target/coverage/report/html/index.html

When to use grcov: It’s most useful when you need to merge coverage from multiple test runs (e.g., unit tests + integration tests + fuzz tests) into a single report.

Coverage in CI: Codecov and Coveralls

Upload coverage data to a tracking service for historical trends and PR annotations:

# .github/workflows/coverage.yml
name: Code Coverage

on: [push, pull_request]

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview

      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@cargo-llvm-cov

      - name: Generate coverage
        run: cargo llvm-cov --workspace --lcov --output-path lcov.info

      - name: Upload to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: lcov.info
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: true

      # Optional: enforce minimum coverage
      - name: Check coverage threshold
        run: |
          cargo llvm-cov --workspace --fail-under-lines 80
          # Fails the build if line coverage drops below 80%

Coverage gates — enforce minimums per crate by reading the JSON output:

# Get per-crate coverage as JSON
cargo llvm-cov --workspace --json | jq '.data[0].totals.lines.percent'

# Fail if below threshold
cargo llvm-cov --workspace --fail-under-lines 80
cargo llvm-cov --workspace --fail-under-functions 70
cargo llvm-cov --workspace --fail-under-regions 60

Coverage-Guided Testing Strategy

Coverage numbers alone are meaningless without a strategy. Here’s how to use coverage data effectively:

Step 1: Triage by risk

High coverage, high risk     → ✅ Good — maintain it
High coverage, low risk      → 🔄 Possibly over-tested — skip if slow
Low coverage, high risk      → 🔴 Write tests NOW — this is where bugs hide
Low coverage, low risk       → 🟡 Track but don't panic

Step 2: Focus on branch coverage, not line coverage

#![allow(unused)]
fn main() {
// 100% line coverage, 50% branch coverage — still risky!
pub fn classify_temperature(temp_c: i32) -> ThermalState {
    if temp_c > 105 {       // ← tested with temp=110 → Critical
        ThermalState::Critical
    } else if temp_c > 85 { // ← tested with temp=90 → Warning
        ThermalState::Warning
    } else if temp_c < -10 { // ← NEVER TESTED → sensor error case missed
        ThermalState::SensorError
    } else {
        ThermalState::Normal  // ← tested with temp=25 → Normal
    }
}
}

Step 3: Exclude noise

# Exclude test code from coverage (it's always "covered")
cargo llvm-cov --workspace --ignore-filename-regex 'tests?\.rs$|benches/'

# Exclude generated code
cargo llvm-cov --workspace --ignore-filename-regex 'target/'

In code, mark untestable sections:

#![allow(unused)]
fn main() {
// Coverage tools recognize this pattern
#[cfg(not(tarpaulin_include))]  // tarpaulin
fn unreachable_hardware_path() {
    // This path requires actual GPU hardware to trigger
}

// For llvm-cov, use a more targeted approach:
// Simply accept that some paths need integration/hardware tests,
// not unit tests. Track them in a coverage exceptions list.
}

Complementary Testing Tools

proptest — Property-Based Testing finds edge cases that hand-written tests miss:

[dev-dependencies]
proptest = "1"
#![allow(unused)]
fn main() {
use proptest::prelude::*;

proptest! {
    #[test]
    fn parse_never_panics(input in "\\PC*") {
        // proptest generates thousands of random strings
        // If parse_gpu_csv panics on any input, the test fails
        // and proptest minimizes the failing case for you.
        let _ = parse_gpu_csv(&input);
    }

    #[test]
    fn temperature_roundtrip(raw in 0u16..4096) {
        let temp = Temperature::from_raw(raw);
        let md = temp.millidegrees_c();
        // Property: millidegrees should always be derivable from raw
        assert_eq!(md, (raw as i32) * 625 / 10);
    }
}
}

insta — Snapshot Testing for large structured outputs (JSON, text reports):

[dev-dependencies]
insta = { version = "1", features = ["json"] }
#![allow(unused)]
fn main() {
#[test]
fn test_der_report_format() {
    let report = generate_der_report(&test_results);
    // First run: creates a snapshot file. Subsequent runs: compares against it.
    // Run `cargo insta review` to accept changes interactively.
    insta::assert_json_snapshot!(report);
}
}

When to add proptest/insta: If your unit tests are all “happy path” examples, proptest will find the edge cases you missed. If you’re testing large output formats (JSON reports, DER records), insta snapshots are faster to write and maintain than hand-written assertions.

Application: 1,000+ Tests Coverage Map

The project has 1,000+ tests but no coverage tracking. Adding it reveals the testing investment distribution. Uncovered paths are prime candidates for Miri and sanitizer verification:

Recommended coverage configuration:

# Quick workspace coverage (proposed CI command)
cargo llvm-cov --workspace \
    --ignore-filename-regex 'tests?\.rs$' \
    --fail-under-lines 75 \
    --html

# Per-crate coverage for targeted improvement
for crate in accel_diag event_log topology_lib network_diag compute_diag fan_diag; do
    echo "=== $crate ==="
    cargo llvm-cov --package "$crate" --json 2>/dev/null | \
        jq -r '.data[0].totals | "Lines: \(.lines.percent | round)%  Branches: \(.branches.percent | round)%"'
done

Expected high-coverage crates (based on test density):

  • topology_lib — 922-line golden-file test suite
  • event_log — registry with create_test_record() helpers
  • cable_diag — make_test_event() / make_test_context() patterns

Expected coverage gaps (based on code inspection):

  • Error handling arms in IPMI communication paths
  • GPU hardware-specific branches (require actual GPU)
  • dmesg parsing edge cases (platform-dependent output)

The 80/20 rule of coverage: Getting from 0% to 80% coverage is straightforward. Getting from 80% to 95% requires increasingly contrived test scenarios. Getting from 95% to 100% requires #[cfg(not(...))] exclusions and is rarely worth the effort. Target 80% line coverage and 70% branch coverage as a practical floor.

Troubleshooting Coverage

SymptomCauseFix
llvm-cov shows 0% for all filesInstrumentation not appliedEnsure you run cargo llvm-cov, not cargo test + llvm-cov separately
Coverage counts unreachable!() as uncoveredThose branches exist in compiled codeUse #[cfg(not(tarpaulin_include))] or add to exclusion regex
Test binary crashes under coverageInstrumentation + sanitizer conflictDon’t combine cargo llvm-cov with -Zsanitizer=address; run them separately
Coverage differs between llvm-cov and tarpaulinDifferent instrumentation techniquesUse llvm-cov as source of truth (compiler-native); file issues for large discrepancies
error: profraw file is malformedTest binary crashed mid-executionFix the test failure first; profraw files are corrupt when the process exits abnormally
Branch coverage seems impossibly lowOptimizer creates branches for match arms, unwrap, etc.Focus on line coverage for practical thresholds; branch coverage is inherently lower

Try It Yourself

  1. Measure coverage on your project: Run cargo llvm-cov --workspace --html and open the report. Find the three files with the lowest coverage. Are they untested, or inherently hard to test (hardware-dependent code)?

  2. Set a coverage gate: Add cargo llvm-cov --workspace --fail-under-lines 60 to your CI. Intentionally comment out a test and verify CI fails. Then raise the threshold to your project’s actual coverage level minus 2%.

  3. Branch vs. line coverage: Write a function with a 3-arm match and test only 2 arms. Compare line coverage (may show 66%) vs. branch coverage (may show 50%). Which metric is more useful for your project?

Coverage Tool Selection

flowchart TD
    START["Need code coverage?"] --> ACCURACY{"Priority?"}
    
    ACCURACY -->|"Most accurate"| LLVM["cargo-llvm-cov\nSource-based, compiler-native"]
    ACCURACY -->|"Quick check"| TARP["cargo-tarpaulin\nLinux only, fast"]
    ACCURACY -->|"Multi-run aggregate"| GRCOV["grcov\nMozilla, combines profiles"]
    
    LLVM --> CI_GATE["CI coverage gate\n--fail-under-lines 80"]
    TARP --> CI_GATE
    
    CI_GATE --> UPLOAD{"Upload to?"}
    UPLOAD -->|"Codecov"| CODECOV["codecov/codecov-action"]
    UPLOAD -->|"Coveralls"| COVERALLS["coverallsapp/github-action"]
    
    style LLVM fill:#91e5a3,color:#000
    style TARP fill:#e3f2fd,color:#000
    style GRCOV fill:#e3f2fd,color:#000
    style CI_GATE fill:#ffd43b,color:#000

🏋️ Exercises

🟢 Exercise 1: First Coverage Report

Install cargo-llvm-cov, run it on any Rust project, and open the HTML report. Find the three files with the lowest line coverage.

Solution
cargo install cargo-llvm-cov
cargo llvm-cov --workspace --html --open
# The report sorts files by coverage — lowest at the bottom
# Look for files under 50% — those are your blind spots

🟡 Exercise 2: CI Coverage Gate

Add a coverage gate to a GitHub Actions workflow that fails if line coverage drops below 60%. Verify it works by commenting out a test.

Solution
# .github/workflows/coverage.yml
name: Coverage
on: [push, pull_request]
jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - run: cargo install cargo-llvm-cov
      - run: cargo llvm-cov --workspace --fail-under-lines 60

Comment out a test, push, and watch the workflow fail.

Key Takeaways

  • cargo-llvm-cov is the most accurate coverage tool for Rust — it uses the compiler’s own instrumentation
  • Coverage doesn’t prove correctness, but zero coverage proves zero testing — use it to find blind spots
  • Set a coverage gate in CI (e.g., --fail-under-lines 80) to prevent regressions
  • Don’t chase 100% coverage — focus on high-risk code paths (error handling, unsafe, parsing)
  • Never combine coverage instrumentation with sanitizers in the same run

Miri, Valgrind, and Sanitizers — Verifying Unsafe Code 🔴

What you’ll learn:

  • Miri as a MIR interpreter — what it catches (aliasing, UB, leaks) and what it can’t (FFI, syscalls)
  • Valgrind memcheck, Helgrind (data races), Callgrind (profiling), and Massif (heap)
  • LLVM sanitizers: ASan, MSan, TSan, LSan with nightly -Zbuild-std
  • cargo-fuzz for crash discovery and loom for concurrency model checking
  • A decision tree for choosing the right verification tool

Cross-references: Code Coverage — coverage finds untested paths, Miri verifies the tested ones · no_std & Features — no_std code often requires unsafe that Miri can verify · CI/CD Pipeline — Miri job in the pipeline

Safe Rust guarantees memory safety and data-race freedom at compile time. But the moment you write unsafe — for FFI, hand-rolled data structures, or performance tricks — those guarantees become your responsibility. This chapter covers the tools that verify your unsafe code actually upholds the safety contracts it claims.

Miri — An Interpreter for Unsafe Rust

Miri is an interpreter for Rust’s Mid-level Intermediate Representation (MIR). Instead of compiling to machine code, Miri executes your program step-by-step with exhaustive checks for undefined behavior at every operation.

# Install Miri (nightly-only component)
rustup +nightly component add miri

# Run your test suite under Miri
cargo +nightly miri test

# Run a specific binary under Miri
cargo +nightly miri run

# Run a specific test
cargo +nightly miri test -- test_name

How Miri works:

Source → rustc → MIR → Miri interprets MIR
                        │
                        ├─ Tracks every pointer's provenance
                        ├─ Validates every memory access
                        ├─ Checks alignment at every deref
                        ├─ Detects use-after-free
                        ├─ Detects data races (with threads)
                        └─ Enforces Stacked Borrows / Tree Borrows rules

What Miri Catches (and What It Cannot)

Miri detects:

CategoryExampleWould Crash at Runtime?
Out-of-bounds accessptr.add(100).read() past allocationSometimes (depends on page layout)
Use after freeReading a dropped Box through raw pointerSometimes (depends on allocator)
Double freeCalling drop_in_place twiceUsually
Unaligned access(ptr as *const u32).read() on odd addressOn some architectures
Invalid valuestransmute::<u8, bool>(2)Silently wrong
Dangling references&*ptr where ptr is freedNo (silent corruption)
Data racesTwo threads, one writing, no synchronizationIntermittent, hard to reproduce
Stacked Borrows violationAliasing &mut referencesNo (silent corruption)

Miri does NOT detect:

LimitationWhy
Logic bugsMiri checks memory safety, not correctness
Concurrency deadlocksMiri checks data races, not livelocks
Performance issuesInterpretation is 10-100× slower than native
OS/hardware interactionMiri can’t emulate syscalls, device I/O
All FFI callsCan’t interpret C code (only Rust MIR)
Exhaustive path coverageOnly tests the paths your test suite reaches

A concrete example — catching unsound code that “works” in practice:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn test_miri_catches_ub() {
        // This "works" in release builds but is undefined behavior
        let mut v = vec![1, 2, 3];
        let ptr = v.as_ptr();

        // Push may reallocate, invalidating ptr
        v.push(4);

        // ❌ UB: ptr may be dangling after reallocation
        // Miri will catch this even if the allocator happens to
        // not move the buffer.
        // let _val = unsafe { *ptr };
        // Error: Miri would report:
        //   "pointer to alloc1234 was dereferenced after this
        //    allocation got freed"
        
        // ✅ Correct: get a fresh pointer after mutation
        let ptr = v.as_ptr();
        let val = unsafe { *ptr };
        assert_eq!(val, 1);
    }
}
}

Running Miri on a Real Crate

Practical Miri workflow for a crate with unsafe:

# Step 1: Run all tests under Miri
cargo +nightly miri test 2>&1 | tee miri_output.txt

# Step 2: If Miri reports errors, isolate them
cargo +nightly miri test -- failing_test_name

# Step 3: Use Miri's backtrace for diagnosis
MIRIFLAGS="-Zmiri-backtrace=full" cargo +nightly miri test

# Step 4: Choose a borrow model
# Stacked Borrows (default, stricter):
cargo +nightly miri test

# Tree Borrows (experimental, more permissive):
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test

Miri flags for common scenarios:

# Disable isolation (allow file system access, env vars)
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test

# Memory leak detection is ON by default in Miri.
# To suppress leak errors (e.g., for intentional leaks):
# MIRIFLAGS="-Zmiri-ignore-leaks" cargo +nightly miri test

# Seed the RNG for reproducible results with randomized tests
MIRIFLAGS="-Zmiri-seed=42" cargo +nightly miri test

# Enable strict provenance checking
MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test

# Multiple flags
MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-backtrace=full -Zmiri-strict-provenance" \
    cargo +nightly miri test

Miri in CI:

# .github/workflows/miri.yml
name: Miri
on: [push, pull_request]

jobs:
  miri:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@nightly
        with:
          components: miri

      - name: Run Miri
        run: cargo miri test --workspace
        env:
          MIRIFLAGS: "-Zmiri-backtrace=full"
          # Leak checking is on by default.
          # Skip tests that use system calls Miri can't handle
          # (file I/O, networking, etc.)

Performance note: Miri is 10-100× slower than native execution. A test suite that runs in 5 seconds natively may take 5 minutes under Miri. In CI, run Miri on a focused subset: crates with unsafe code only.

Valgrind and Its Rust Integration

Valgrind is the classic C/C++ memory checker. It works on compiled Rust binaries too, checking for memory errors at the machine-code level.

# Install Valgrind
sudo apt install valgrind  # Debian/Ubuntu
sudo dnf install valgrind  # Fedora

# Build with debug info (Valgrind needs symbols)
cargo build --tests
# or for release with debug info:
# cargo build --release
# [profile.release]
# debug = true

# Run a specific test binary under Valgrind
valgrind --tool=memcheck \
    --leak-check=full \
    --show-leak-kinds=all \
    --track-origins=yes \
    ./target/debug/deps/my_crate-abc123 --test-threads=1

# Run the main binary
valgrind --tool=memcheck \
    --leak-check=full \
    --error-exitcode=1 \
    ./target/debug/diag_tool --run-diagnostics

Valgrind tools beyond memcheck:

ToolCommandWhat It Detects
Memcheck--tool=memcheckMemory leaks, use-after-free, buffer overflows
Helgrind--tool=helgrindData races and lock-order violations
DRD--tool=drdData races (different detection algorithm)
Callgrind--tool=callgrindCPU instruction profiling (path-level)
Massif--tool=massifHeap memory profiling over time
Cachegrind--tool=cachegrindCache miss analysis

Using Callgrind for instruction-level profiling:

# Record instruction counts (more stable than wall-clock time)
valgrind --tool=callgrind \
    --callgrind-out-file=callgrind.out \
    ./target/release/diag_tool --run-diagnostics

# Visualize with KCachegrind
kcachegrind callgrind.out
# or the text-based alternative:
callgrind_annotate callgrind.out | head -100

Miri vs Valgrind — when to use which:

AspectMiriValgrind
Checks Rust-specific UB✅ Stacked/Tree Borrows❌ Not aware of Rust rules
Checks C FFI code❌ Can’t interpret C✅ Checks all machine code
Needs nightly✅ Yes❌ No
Speed10-100× slower10-50× slower
PlatformAny (interprets MIR)Linux, macOS (runs native code)
Data race detection✅ Yes✅ Yes (Helgrind/DRD)
Leak detection✅ Yes✅ Yes (more thorough)
False positivesVery rareOccasional (especially with allocators)

Use both:

  • Miri for pure-Rust unsafe code (Stacked Borrows, provenance)
  • Valgrind for FFI-heavy code and whole-program leak analysis

AddressSanitizer, MemorySanitizer, ThreadSanitizer

LLVM sanitizers are compile-time instrumentation passes that insert runtime checks. They’re faster than Valgrind (2-5× overhead vs 10-50×) and catch different classes of bugs.

# Required: install Rust source for rebuilding std with sanitizer instrumentation
rustup component add rust-src --toolchain nightly
# AddressSanitizer (ASan) — buffer overflows, use-after-free, stack overflows
RUSTFLAGS="-Zsanitizer=address" \
    cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# MemorySanitizer (MSan) — uninitialized memory reads
RUSTFLAGS="-Zsanitizer=memory" \
    cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# ThreadSanitizer (TSan) — data races
RUSTFLAGS="-Zsanitizer=thread" \
    cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# LeakSanitizer (LSan) — memory leaks (included in ASan by default)
RUSTFLAGS="-Zsanitizer=leak" \
    cargo +nightly test --target x86_64-unknown-linux-gnu

Note: ASan, MSan, and TSan require -Zbuild-std to rebuild the standard library with sanitizer instrumentation. LSan does not.

Sanitizer comparison:

SanitizerOverheadCatchesNightly?-Zbuild-std?
ASan2× memory, 2× CPUBuffer overflow, use-after-free, stack overflowYesYes
MSan3× memory, 3× CPUUninitialized readsYesYes
TSan5-10× memory, 5× CPUData racesYesYes
LSanMinimalMemory leaksYesNo

Practical example — catching a data race with TSan:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;

fn racy_counter() -> u64 {
    // ❌ UB: unsynchronized shared mutable state
    let data = Arc::new(std::cell::UnsafeCell::new(0u64));
    let mut handles = vec![];

    for _ in 0..4 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                // SAFETY: UNSOUND — data race!
                unsafe {
                    *data.get() += 1;
                }
            }
        }));
    }

    for h in handles {
        h.join().unwrap();
    }

    // Value should be 4000 but may be anything due to race
    unsafe { *data.get() }
}

// Both Miri and TSan catch this:
// Miri:  "Data race detected between (1) write and (2) write"
// TSan:  "WARNING: ThreadSanitizer: data race"
//
// Fix: use AtomicU64 or Mutex<u64>
}

cargo-fuzz — Coverage-Guided Fuzzing (finds crashes in parsers and decoders):

# Install
cargo install cargo-fuzz

# Initialize a fuzz target
cargo fuzz init
cargo fuzz add parse_gpu_csv
#![allow(unused)]
fn main() {
// fuzz/fuzz_targets/parse_gpu_csv.rs
#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    if let Ok(s) = std::str::from_utf8(data) {
        // The fuzzer generates millions of inputs looking for panics/crashes.
        let _ = diag_tool::parse_gpu_csv(s);
    }
});
}
# Run the fuzzer (runs until interrupted or crash found)
cargo +nightly fuzz run parse_gpu_csv -- -max_total_time=300  # 5 minutes

# Minimize a crash
cargo +nightly fuzz tmin parse_gpu_csv artifacts/parse_gpu_csv/crash-...

When to fuzz: Any function that parses untrusted/semi-trusted input (sensor output, config files, network data, JSON/CSV). Fuzzing found real bugs in every major Rust parser crate (serde, regex, image).

loom — Concurrency Model Checker (exhaustively tests atomic orderings):

[dev-dependencies]
loom = "0.7"
#![allow(unused)]
fn main() {
#[cfg(loom)]
mod tests {
    use loom::sync::atomic::{AtomicUsize, Ordering};
    use loom::thread;

    #[test]
    fn test_counter_is_atomic() {
        loom::model(|| {
            let counter = loom::sync::Arc::new(AtomicUsize::new(0));
            let c1 = counter.clone();
            let c2 = counter.clone();

            let t1 = thread::spawn(move || { c1.fetch_add(1, Ordering::SeqCst); });
            let t2 = thread::spawn(move || { c2.fetch_add(1, Ordering::SeqCst); });

            t1.join().unwrap();
            t2.join().unwrap();

            // loom explores ALL possible thread interleavings
            assert_eq!(counter.load(Ordering::SeqCst), 2);
        });
    }
}
}

When to use loom: When you have lock-free data structures or custom synchronization primitives. Loom exhaustively explores thread interleavings — it’s a model checker, not a stress test. Not needed for Mutex/RwLock-based code.

When to Use Which Tool

Decision tree for unsafe verification:

Is the code pure Rust (no FFI)?
├─ Yes → Use Miri (catches Rust-specific UB, Stacked Borrows)
│        Also run ASan in CI for defense-in-depth
└─ No (calls C/C++ code via FFI)
   ├─ Memory safety concerns?
   │  └─ Yes → Use Valgrind memcheck AND ASan
   ├─ Concurrency concerns?
   │  └─ Yes → Use TSan (faster) or Helgrind (more thorough)
   └─ Memory leak concerns?
      └─ Yes → Use Valgrind --leak-check=full

Recommended CI matrix:

# Run all tools in parallel for fast feedback
jobs:
  miri:
    runs-on: ubuntu-latest
    steps:
      - uses: dtolnay/rust-toolchain@nightly
        with: { components: miri }
      - run: cargo miri test --workspace

  asan:
    runs-on: ubuntu-latest
    steps:
      - uses: dtolnay/rust-toolchain@nightly
      - run: |
          RUSTFLAGS="-Zsanitizer=address" \
          cargo test -Zbuild-std --target x86_64-unknown-linux-gnu

  valgrind:
    runs-on: ubuntu-latest
    steps:
      - run: sudo apt-get install -y valgrind
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo build --tests
      - run: |
          for test_bin in $(find target/debug/deps -maxdepth 1 -executable -type f ! -name '*.d'); do
            valgrind --error-exitcode=1 --leak-check=full "$test_bin" --test-threads=1
          done

Application: Zero Unsafe — and When You’ll Need It

The project contains zero unsafe blocks across 90K+ lines of Rust. This is a remarkable achievement for a systems-level diagnostics tool and demonstrates that safe Rust is sufficient for:

  • IPMI communication (via std::process::Command to ipmitool)
  • GPU queries (via std::process::Command to accel-query)
  • PCIe topology parsing (pure JSON/text parsing)
  • SEL record management (pure data structures)
  • DER report generation (JSON serialization)

When will the project need unsafe?

The likely triggers for introducing unsafe:

ScenarioWhy unsafeRecommended Verification
Direct ioctl-based IPMIlibc::ioctl() bypasses ipmitool subprocessMiri + Valgrind
Direct GPU driver queriesaccel-mgmt FFI instead of accel-query parsingValgrind (C library)
Memory-mapped PCIe configmmap for direct config-space readsASan + Valgrind
Lock-free SEL bufferAtomicPtr for concurrent event collectionMiri + TSan
Embedded/no_std variantRaw pointer manipulation for bare-metalMiri

Preparation: Before introducing unsafe, add the verification tools to CI:

# Cargo.toml — add a feature flag for unsafe optimizations
[features]
default = []
direct-ipmi = []     # Enable direct ioctl IPMI instead of ipmitool subprocess
direct-accel-api = []     # Enable accel-mgmt FFI instead of accel-query parsing
#![allow(unused)]
fn main() {
// src/ipmi.rs — gated behind a feature flag
#[cfg(feature = "direct-ipmi")]
mod direct {
    //! Direct IPMI device access via /dev/ipmi0 ioctl.
    //!
    //! # Safety
    //! This module uses `unsafe` for ioctl system calls.
    //! Verified with: Miri (where possible), Valgrind memcheck, ASan.

    use std::os::unix::io::RawFd;

    // ... unsafe ioctl implementation ...
}

#[cfg(not(feature = "direct-ipmi"))]
mod subprocess {
    //! IPMI via ipmitool subprocess (default, fully safe).
    // ... current implementation ...
}
}

Key insight: Keep unsafe behind feature flags so it can be verified independently. Run cargo +nightly miri test --features direct-ipmi in CI to continuously verify the unsafe paths without affecting the safe default build.

cargo-careful — Extra UB Checks on Stable

cargo-careful runs your code with extra standard library checks enabled — catching some undefined behavior that normal builds ignore, without requiring nightly or Miri’s 10-100× slowdown:

# Install (requires nightly, but runs your code at near-native speed)
cargo install cargo-careful

# Run tests with extra UB checks (catches uninitialized memory, invalid values)
cargo +nightly careful test

# Run a binary with extra checks
cargo +nightly careful run -- --run-diagnostics

What cargo-careful catches that normal builds don’t:

  • Reads of uninitialized memory in MaybeUninit and zeroed()
  • Creating invalid bool, char, or enum values via transmute
  • Unaligned pointer reads/writes
  • copy_nonoverlapping with overlapping ranges

Where it fits in the verification ladder:

Least overhead                                          Most thorough
├─ cargo test ──► cargo careful test ──► Miri ──► ASan ──► Valgrind ─┤
│  (0× overhead)  (~1.5× overhead)   (10-100×)  (2×)     (10-50×)   │
│  Safe Rust only  Catches some UB    Pure-Rust  FFI+Rust FFI+Rust   │

Recommendation: Add cargo +nightly careful test to CI as a fast safety check. It runs at near-native speed (unlike Miri) and catches real bugs that safe Rust abstractions mask.

Troubleshooting Miri and Sanitizers

SymptomCauseFix
Miri does not support FFIMiri is a Rust interpreter; it can’t execute C codeUse Valgrind or ASan for FFI code instead
error: unsupported operation: can't call foreign functionMiri hit an extern "C" callMock the FFI boundary or gate behind #[cfg(miri)]
Stacked Borrows violationAliasing rule violation — even if code “works”Miri is correct; refactor to avoid aliasing &mut with &
Sanitizer says DEADLYSIGNALASan detected buffer overflowCheck array indexing, slice operations, and pointer arithmetic
LeakSanitizer: detected memory leaksBox::leak(), forget(), or missing drop()Intentional: suppress with __lsan_disable(); unintentional: fix the leak
Miri is extremely slowMiri interprets, doesn’t compile — 10-100× slowerRun only on --lib tests or tag specific tests with #[cfg_attr(miri, ignore)] for slow ones
TSan: false positive with atomicsTSan doesn’t understand Rust’s atomic ordering model perfectlyAdd TSAN_OPTIONS=suppressions=tsan.supp with specific suppressions

Try It Yourself

  1. Trigger a Miri UB detection: Write an unsafe function that creates two &mut references to the same i32 (aliasing violation). Run cargo +nightly miri test and observe the “Stacked Borrows” error. Fix it with UnsafeCell or separate allocations.

  2. Run ASan on a deliberate bug: Create a test that does unsafe out-of-bounds array access. Build with RUSTFLAGS="-Zsanitizer=address" and observe ASan’s report. Note how it pinpoints the exact line.

  3. Benchmark Miri overhead: Time cargo test --lib vs cargo +nightly miri test --lib on the same test suite. Calculate the slowdown factor. Based on this, decide which tests to run under Miri in CI and which to skip with #[cfg_attr(miri, ignore)].

Safety Verification Decision Tree

flowchart TD
    START["Have unsafe code?"] -->|No| SAFE["Safe Rust — no\nverification needed"]
    START -->|Yes| KIND{"What kind?"}
    
    KIND -->|"Pure Rust unsafe"| MIRI["Miri\nMIR interpreter\ncatches aliasing, UB, leaks"]
    KIND -->|"FFI / C interop"| VALGRIND["Valgrind memcheck\nor ASan"]
    KIND -->|"Concurrent unsafe"| CONC{"Lock-free?"}
    
    CONC -->|"Atomics/lock-free"| LOOM["loom\nModel checker for atomics"]
    CONC -->|"Mutex/shared state"| TSAN["TSan or\nMiri -Zmiri-check-number-validity"]
    
    MIRI --> CI_MIRI["CI: cargo +nightly miri test"]
    VALGRIND --> CI_VALGRIND["CI: valgrind --leak-check=full"]
    
    style SAFE fill:#91e5a3,color:#000
    style MIRI fill:#e3f2fd,color:#000
    style VALGRIND fill:#ffd43b,color:#000
    style LOOM fill:#ff6b6b,color:#000
    style TSAN fill:#ffd43b,color:#000

🏋️ Exercises

🟡 Exercise 1: Trigger a Miri UB Detection

Write an unsafe function that creates two &mut references to the same i32 (aliasing violation). Run cargo +nightly miri test and observe the Stacked Borrows error. Fix it.

Solution
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn aliasing_ub() {
        let mut x: i32 = 42;
        let ptr = &mut x as *mut i32;
        unsafe {
            // BUG: Two &mut references to the same location
            let _a = &mut *ptr;
            let _b = &mut *ptr; // Miri: Stacked Borrows violation!
        }
    }
}
}

Fix: use separate allocations or UnsafeCell:

#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;

#[test]
fn no_aliasing_ub() {
    let x = UnsafeCell::new(42);
    unsafe {
        let a = &mut *x.get();
        *a = 100;
    }
}
}

🔴 Exercise 2: ASan Out-of-Bounds Detection

Create a test with unsafe out-of-bounds array access. Build with RUSTFLAGS="-Zsanitizer=address" on nightly and observe ASan’s report.

Solution
#![allow(unused)]
fn main() {
#[test]
fn oob_access() {
    let arr = [1u8, 2, 3, 4, 5];
    let ptr = arr.as_ptr();
    unsafe {
        let _val = *ptr.add(10); // Out of bounds!
    }
}
}
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std \
  --target x86_64-unknown-linux-gnu -- oob_access
# ASan report: stack-buffer-overflow at <exact address>

Key Takeaways

  • Miri is the tool for pure-Rust unsafe — it catches aliasing violations, use-after-free, and leaks that compile and pass tests
  • Valgrind is the tool for FFI/C interop — it works on the final binary without recompilation
  • Sanitizers (ASan, TSan, MSan) require nightly but run at near-native speed — ideal for large test suites
  • loom is purpose-built for verifying lock-free concurrent data structures
  • Run Miri in CI on every push; run sanitizers on a nightly schedule to avoid slowing the main pipeline

Dependency Management and Supply Chain Security 🟢

What you’ll learn:

  • Scanning for known vulnerabilities with cargo-audit
  • Enforcing license, advisory, and source policies with cargo-deny
  • Supply chain trust verification with Mozilla’s cargo-vet
  • Tracking outdated dependencies and detecting breaking API changes
  • Visualizing and deduplicating your dependency tree

Cross-references: Release Profiles — cargo-udeps trims unused dependencies found here · CI/CD Pipeline — audit and deny jobs in the pipeline · Build Scripts — build-dependencies are part of your supply chain too

A Rust binary doesn’t just contain your code — it contains every transitive dependency in your Cargo.lock. A vulnerability, license violation, or malicious crate anywhere in that tree becomes your problem. This chapter covers the tools that make dependency management auditable and automated.

cargo-audit — Known Vulnerability Scanning

cargo-audit checks your Cargo.lock against the RustSec Advisory Database, which tracks known vulnerabilities in published crates.

# Install
cargo install cargo-audit

# Scan for known vulnerabilities
cargo audit

# Output:
# Crate:     chrono
# Version:   0.4.19
# Title:     Potential segfault in localtime_r invocations
# Date:      2020-11-10
# ID:        RUSTSEC-2020-0159
# URL:       https://rustsec.org/advisories/RUSTSEC-2020-0159
# Solution:  Upgrade to >= 0.4.20

# Check and fail CI if vulnerabilities exist
cargo audit --deny warnings

# Generate JSON output for automated processing
cargo audit --json

# Fix vulnerabilities by updating Cargo.lock
cargo audit fix

CI integration:

# .github/workflows/audit.yml
name: Security Audit
on:
  schedule:
    - cron: '0 0 * * *'  # Daily check — advisories appear continuously
  push:
    paths: ['Cargo.lock']

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: rustsec/audit-check@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

cargo-deny — Comprehensive Policy Enforcement

cargo-deny goes far beyond vulnerability scanning. It enforces policies across four dimensions:

  1. Advisories — known vulnerabilities (like cargo-audit)
  2. Licenses — allowed/denied license list
  3. Bans — forbidden crates or duplicate versions
  4. Sources — allowed registries and git sources
# Install
cargo install cargo-deny

# Initialize configuration
cargo deny init
# Creates deny.toml with documented defaults

# Run all checks
cargo deny check

# Run specific checks
cargo deny check advisories
cargo deny check licenses
cargo deny check bans
cargo deny check sources

Example deny.toml:

# deny.toml

[advisories]
vulnerability = "deny"        # Fail on known vulnerabilities
unmaintained = "warn"         # Warn on unmaintained crates
yanked = "deny"               # Fail on yanked crates
notice = "warn"               # Warn on informational advisories

[licenses]
unlicensed = "deny"           # All crates must have a license
allow = [
    "MIT",
    "Apache-2.0",
    "BSD-2-Clause",
    "BSD-3-Clause",
    "ISC",
    "Unicode-DFS-2016",
]
copyleft = "deny"             # No GPL/LGPL/AGPL in this project
default = "deny"              # Deny anything not explicitly allowed

[bans]
multiple-versions = "warn"    # Warn if same crate appears at 2 versions
wildcards = "deny"            # No path = "*" in dependencies
highlight = "all"             # Show all duplicates, not just first

# Ban specific problematic crates
deny = [
    # openssl-sys pulls in C OpenSSL — prefer rustls
    { name = "openssl-sys", wrappers = ["native-tls"] },
]

# Allow specific duplicate versions (when unavoidable)
[[bans.skip]]
name = "syn"
version = "1.0"               # syn 1.x and 2.x often coexist

[sources]
unknown-registry = "deny"     # Only allow crates.io
unknown-git = "deny"          # No random git dependencies
allow-registry = ["https://github.com/rust-lang/crates.io-index"]

License enforcement is particularly valuable for commercial projects:

# Check which licenses are in your dependency tree
cargo deny list

# Output:
# MIT          — 127 crates
# Apache-2.0   — 89 crates
# BSD-3-Clause — 12 crates
# MPL-2.0      — 3 crates   ← might need legal review
# Unicode-DFS  — 1 crate

cargo-vet — Supply Chain Trust Verification

cargo-vet (from Mozilla) addresses a different question: not “does this crate have known bugs?” but “has a trusted human actually reviewed this code?”

# Install
cargo install cargo-vet

# Initialize (creates supply-chain/ directory)
cargo vet init

# Check which crates need review
cargo vet

# After reviewing a crate, certify it:
cargo vet certify serde 1.0.203
# Records that you've audited serde 1.0.203 for your criteria

# Import audits from trusted organizations
cargo vet import mozilla
cargo vet import google
cargo vet import bytecode-alliance

How it works:

supply-chain/
├── audits.toml       ← Your team's audit certifications
├── config.toml       ← Trust configuration and criteria
└── imports.lock      ← Pinned imports from other organizations

cargo-vet is most valuable for organizations with strict supply-chain requirements (government, finance, infrastructure). For most teams, cargo-deny provides sufficient protection.

cargo-outdated and cargo-semver-checks

cargo-outdated — find dependencies that have newer versions:

cargo install cargo-outdated

cargo outdated --workspace
# Output:
# Name        Project  Compat  Latest   Kind
# serde       1.0.193  1.0.203 1.0.203  Normal
# regex       1.9.6    1.10.4  1.10.4   Normal
# thiserror   1.0.50   1.0.61  2.0.3    Normal  ← major version available

cargo-semver-checks — detect breaking API changes before publishing. Essential for library crates:

cargo install cargo-semver-checks

# Check if your changes are semver-compatible
cargo semver-checks

# Output:
# ✗ Function `parse_gpu_csv` is now private (was public)
#   → This is a BREAKING change. Bump MAJOR version.
#
# ✗ Struct `GpuInfo` has a new required field `power_limit_w`
#   → This is a BREAKING change. Bump MAJOR version.
#
# ✓ Function `parse_gpu_csv_v2` was added (non-breaking)

cargo-tree — Dependency Visualization and Deduplication

cargo tree is built into Cargo (no installation needed) and is invaluable for understanding your dependency graph:

# Full dependency tree
cargo tree

# Find why a specific crate is included
cargo tree --invert --package openssl-sys
# Shows all paths from your crate to openssl-sys

# Find duplicate versions
cargo tree --duplicates
# Output:
# syn v1.0.109
# └── serde_derive v1.0.193
#
# syn v2.0.48
# ├── thiserror-impl v1.0.56
# └── tokio-macros v2.2.0

# Show only direct dependencies
cargo tree --depth 1

# Show dependency features
cargo tree --format "{p} {f}"

# Count total dependencies
cargo tree | wc -l

Deduplication strategy: When cargo tree --duplicates shows the same crate at two major versions, check if you can update the dependency chain to unify them. Each duplicate adds compile time and binary size.

Application: Multi-Crate Dependency Hygiene

The workspace uses [workspace.dependencies] for centralized version management — an excellent practice. Combined with cargo tree --duplicates for size analysis, this prevents version drift and reduces binary bloat:

# Root Cargo.toml — all versions pinned in one place
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
regex = "1.10"
thiserror = "1.0"
anyhow = "1.0"
rayon = "1.8"

Recommended additions for the project:

# Add to CI pipeline:
cargo deny init              # One-time setup
cargo deny check             # Every PR — licenses, advisories, bans
cargo audit --deny warnings  # Every push — vulnerability scanning
cargo outdated --workspace   # Weekly — track available updates

Recommended deny.toml for the project:

[advisories]
vulnerability = "deny"
yanked = "deny"

[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-DFS-2016"]
copyleft = "deny"     # Hardware diagnostics tool — no copyleft

[bans]
multiple-versions = "warn"   # Track duplicates, don't block yet
wildcards = "deny"

[sources]
unknown-registry = "deny"
unknown-git = "deny"

Supply Chain Audit Pipeline

flowchart LR
    PR["Pull Request"] --> AUDIT["cargo audit\nKnown CVEs"]
    AUDIT --> DENY["cargo deny check\nLicenses + Bans + Sources"]
    DENY --> OUTDATED["cargo outdated\nWeekly schedule"]
    OUTDATED --> SEMVER["cargo semver-checks\nLibrary crates only"]
    
    AUDIT -->|"Fail"| BLOCK["❌ Block merge"]
    DENY -->|"Fail"| BLOCK
    SEMVER -->|"Breaking change"| BUMP["Bump major version"]
    
    style BLOCK fill:#ff6b6b,color:#000
    style BUMP fill:#ffd43b,color:#000
    style PR fill:#e3f2fd,color:#000

🏋️ Exercises

🟢 Exercise 1: Audit Your Dependencies

Run cargo audit and cargo deny init && cargo deny check on any Rust project. How many advisories are found? How many license categories are in your tree?

Solution
cargo audit
# Note any advisories — often chrono, time, or older crates

cargo deny init
cargo deny list
# Shows license breakdown: MIT (N), Apache-2.0 (N), etc.

cargo deny check
# Shows full audit across all four dimensions

🟡 Exercise 2: Find and Eliminate Duplicate Dependencies

Run cargo tree --duplicates on a workspace. Find a crate that appears at two versions. Can you update Cargo.toml to unify them? Measure the compile-time and binary-size impact.

Solution
cargo tree --duplicates
# Typical: syn 1.x and syn 2.x

# Find who pulls in the old version:
cargo tree --invert --package [email protected]
# Output: serde_derive 1.0.xxx -> syn 1.0.109

# Check if a newer serde_derive uses syn 2.x:
cargo update -p serde_derive
cargo tree --duplicates
# If syn 1.x is gone, you've eliminated a duplicate

# Measure impact:
time cargo build --release  # Before and after
cargo bloat --release --crates | head -20

Key Takeaways

  • cargo audit catches known CVEs — run it on every push and on a daily schedule
  • cargo deny enforces four policy dimensions: advisories, licenses, bans, and sources
  • Use [workspace.dependencies] to centralize version management across a multi-crate workspace
  • cargo tree --duplicates reveals bloat; each duplicate adds compile time and binary size
  • cargo-vet is for high-security environments; cargo-deny is sufficient for most teams

Release Profiles and Binary Size 🟡

What you’ll learn:

  • Release profile anatomy: LTO, codegen-units, panic strategy, strip, opt-level
  • Thin vs Fat vs Cross-Language LTO trade-offs
  • Binary size analysis with cargo-bloat
  • Dependency trimming with cargo-udeps, cargo-machete and cargo-shear

Cross-references: Compile-Time Tools — the other half of optimization · Benchmarking — measure runtime before you optimize · Dependencies — trimming deps reduces both size and compile time

The default cargo build --release is already good. But for production deployment — especially single-binary tools deployed to thousands of servers — there’s a significant gap between “good” and “optimized.” This chapter covers the profile knobs and the tools to measure binary size.

Release Profile Anatomy

Cargo profiles control how rustc compiles your code. The defaults are conservative — designed for broad compatibility, not maximum performance:

# Cargo.toml — Cargo's built-in defaults (what you get if you specify nothing)

[profile.release]
opt-level = 3        # Optimization level (0=none, 1=basic, 2=good, 3=aggressive)
lto = false          # Link-time optimization OFF
codegen-units = 16   # Parallel compilation units (faster compile, less optimization)
panic = "unwind"     # Stack unwinding on panic (larger binary, catch_unwind works)
strip = "none"       # Keep all symbols and debug info
overflow-checks = false  # No integer overflow checks in release
debug = false        # No debug info in release

Production-optimized profile (what the project already uses):

[profile.release]
lto = true           # Full cross-crate optimization
codegen-units = 1    # Single codegen unit — maximum optimization opportunity
panic = "abort"      # No unwinding overhead — smaller, faster
strip = true         # Remove all symbols — smaller binary

The impact of each setting:

SettingDefault → OptimizedBinary SizeRuntime SpeedCompile Time
lto = false → true—-10 to -20%+5 to +20%2-5× slower
codegen-units = 16 → 1—-5 to -10%+5 to +10%1.5-2× slower
panic = "unwind" → "abort"—-5 to -10%NegligibleNegligible
strip = "none" → true—-50 to -70%NoneNone
opt-level = 3 → "s"—-10 to -30%-5 to -10%Similar
opt-level = 3 → "z"—-15 to -40%-10 to -20%Similar

Additional profile tweaks:

[profile.release]
# All of the above, plus:
overflow-checks = true    # Keep overflow checks even in release (safety > speed)
debug = "line-tables-only" # Minimal debug info for backtraces without full DWARF
rpath = false             # Don't embed runtime library paths
incremental = false       # Disable incremental compilation (cleaner builds)

# For size-optimized builds (embedded, WASM):
# opt-level = "z"         # Optimize for size aggressively
# strip = "symbols"       # Strip symbols but keep debug sections

Per-crate profile overrides — optimize hot crates, leave others alone:

# Dev builds: optimize dependencies but not your code (fast recompile)
[profile.dev.package."*"]
opt-level = 2          # Optimize all dependencies in dev mode

# Release builds: override specific crate optimization
[profile.release.package.serde_json]
opt-level = 3          # Maximum optimization for JSON parsing
codegen-units = 1

# Test profile: match release behavior for accurate integration tests
[profile.test]
opt-level = 1          # Some optimization to avoid timeout in slow tests

LTO in Depth — Thin vs Fat vs Cross-Language

Link-Time Optimization lets LLVM optimize across crate boundaries — inlining functions from serde_json into your parsing code, removing dead code from regex, etc. Without LTO, each crate is a separate optimization island.

[profile.release]
# Option 1: Fat LTO (default when lto = true)
lto = true
# All code merged into one LLVM module → maximum optimization
# Slowest compile, smallest/fastest binary

# Option 2: Thin LTO
lto = "thin"
# Each crate stays separate but LLVM does cross-module optimization
# Faster compile than fat LTO, nearly as good optimization
# Best trade-off for most projects

# Option 3: No LTO
lto = false
# Only intra-crate optimization
# Fastest compile, larger binary

# Option 4: Off (explicit)
lto = "off"
# Same as false

Fat LTO vs Thin LTO:

AspectFat LTO (true)Thin LTO ("thin")
Optimization qualityBest~95% of fat
Compile timeSlow (all code in one module)Moderate (parallel modules)
Memory usageHigh (all LLVM IR in memory)Lower (streaming)
ParallelismNone (single module)Good (per-module)
Recommended forFinal release buildsCI builds, development

Cross-language LTO — optimize across Rust and C boundaries:

[profile.release]
lto = true

# Cargo.toml — for crates using the cc crate
[build-dependencies]
cc = "1.0"
// build.rs — enable cross-language (linker-plugin) LTO
fn main() {
    // The cc crate respects CFLAGS from the environment.
    // For cross-language LTO, compile C code with:
    //   -flto=thin -O2
    cc::Build::new()
        .file("csrc/fast_parser.c")
        .flag("-flto=thin")
        .opt_level(2)
        .compile("fast_parser");
}
# Enable linker-plugin LTO (requires compatible LLD or gold linker)
RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
    cargo build --release

Cross-language LTO allows LLVM to inline C functions into Rust callers and vice versa. This is most impactful for FFI-heavy code where small C functions are called frequently (e.g., IPMI ioctl wrappers).

Binary Size Analysis with cargo-bloat

cargo-bloat answers: “What functions and crates are taking up the most space in my binary?”

# Install
cargo install cargo-bloat

# Show largest functions
cargo bloat --release -n 20
# Output:
#  File  .text     Size          Crate    Name
#  2.8%   5.1%  78.5KiB  serde_json       serde_json::de::Deserializer::parse_...
#  2.1%   3.8%  58.2KiB  regex_syntax     regex_syntax::ast::parse::ParserI::p...
#  1.5%   2.7%  42.1KiB  accel_diag         accel_diag::vendor::parse_smi_output
#  ...

# Show by crate (which dependencies are biggest)
cargo bloat --release --crates
# Output:
#  File  .text     Size Crate
# 12.3%  22.1%  340KiB serde_json
#  8.7%  15.6%  240KiB regex
#  6.2%  11.1%  170KiB std
#  5.1%   9.2%  141KiB accel_diag
#  ...

# Compare two builds (before/after optimization)
cargo bloat --release --crates > before.txt
# ... make changes ...
cargo bloat --release --crates > after.txt
diff before.txt after.txt

Common bloat sources and fixes:

Bloat SourceTypical SizeFix
regex (full engine)200-400 KBUse regex-lite if you don’t need Unicode
serde_json (full)200-350 KBConsider simd-json or sonic-rs if perf matters
Generics monomorphizationVariesUse dyn Trait at API boundaries
Formatting machinery (Display, Debug)50-150 KB#[derive(Debug)] on large enums adds up
Panic message strings20-80 KBpanic = "abort" removes unwinding, strip removes strings
Unused featuresVariesDisable default features: serde = { version = "1", default-features = false }

Trimming Dependencies with cargo-udeps

cargo-udeps finds dependencies declared in Cargo.toml that your code doesn’t actually use:

# Install (requires nightly)
cargo install cargo-udeps

# Find unused dependencies
cargo +nightly udeps --workspace
# Output:
# unused dependencies:
# `diag_tool v0.1.0`
# └── "tempfile" (dev-dependency)
#
# `accel_diag v0.1.0`
# └── "once_cell"    ← was needed before LazyLock, now dead

Every unused dependency:

  • Increases compile time
  • Increases binary size
  • Adds supply chain risk
  • Adds potential license complications

Alternative: cargo-machete — faster, heuristic-based approach:

cargo install cargo-machete
cargo machete
# Faster but may have false positives (heuristic, not compilation-based)

Alternative: cargo-shear — sweet spot between cargo-udeps and cargo-machete:

cargo install cargo-shear
cargo shear --fix
# Slower than cargo-machete but much faster than cargo-udeps
# Much less false positives than cargo-machete

Size Optimization Decision Tree

flowchart TD
    START["Binary too large?"] --> STRIP{"strip = true?"}
    STRIP -->|"No"| DO_STRIP["Add strip = true\n-50 to -70% size"]
    STRIP -->|"Yes"| LTO{"LTO enabled?"}
    LTO -->|"No"| DO_LTO["Add lto = true\ncodegen-units = 1"]
    LTO -->|"Yes"| BLOAT["Run cargo-bloat\n--crates"]
    BLOAT --> BIG_DEP{"Large dependency?"}
    BIG_DEP -->|"Yes"| REPLACE["Replace with lighter\nalternative or disable\ndefault features"]
    BIG_DEP -->|"No"| UDEPS["cargo-udeps\nRemove unused deps"]
    UDEPS --> OPT_LEVEL{"Need smaller?"}
    OPT_LEVEL -->|"Yes"| SIZE_OPT["opt-level = 's' or 'z'"]

    style DO_STRIP fill:#91e5a3,color:#000
    style DO_LTO fill:#e3f2fd,color:#000
    style REPLACE fill:#ffd43b,color:#000
    style SIZE_OPT fill:#ff6b6b,color:#000

🏋️ Exercises

🟢 Exercise 1: Measure LTO Impact

Build a project with default release settings, then with lto = true + codegen-units = 1 + strip = true. Compare binary size and compile time.

Solution
# Default release
cargo build --release
ls -lh target/release/my-binary
time cargo build --release  # Note time

# Optimized release — add to Cargo.toml:
# [profile.release]
# lto = true
# codegen-units = 1
# strip = true
# panic = "abort"

cargo clean
cargo build --release
ls -lh target/release/my-binary  # Typically 30-50% smaller
time cargo build --release       # Typically 2-3× slower to compile

🟡 Exercise 2: Find Your Biggest Crate

Run cargo bloat --release --crates on a project. Identify the largest dependency. Can you reduce it by disabling default features or switching to a lighter alternative?

Solution
cargo install cargo-bloat
cargo bloat --release --crates
# Output:
#  File  .text     Size Crate
# 12.3%  22.1%  340KiB serde_json
#  8.7%  15.6%  240KiB regex

# For regex — try regex-lite if you don't need Unicode:
# regex-lite = "0.1"  # ~10× smaller than full regex

# For serde — disable default features if you don't need std:
# serde = { version = "1", default-features = false, features = ["derive"] }

cargo bloat --release --crates  # Compare after changes

Key Takeaways

  • lto = true + codegen-units = 1 + strip = true + panic = "abort" is the production release profile
  • Thin LTO (lto = "thin") gives 80% of Fat LTO’s benefit at a fraction of the compile cost
  • cargo-bloat --crates tells you exactly which dependencies are eating binary space
  • cargo-udeps, cargo-machete and cargo-shear find dead dependencies that waste compile time and binary size
  • Per-crate profile overrides let you optimize hot crates without slowing the whole build

Compile-Time and Developer Tools 🟡

What you’ll learn:

  • Compilation caching with sccache for local and CI builds
  • Faster linking with mold (3-10× faster than the default linker)
  • cargo-nextest: a faster, more informative test runner
  • Developer visibility tools: cargo-expand, cargo-geiger, cargo-watch
  • Workspace lints, MSRV policy, and documentation-as-CI

Cross-references: Release Profiles — LTO and binary size optimization · CI/CD Pipeline — these tools integrate into your pipeline · Dependencies — fewer deps = faster compiles

Compile-Time Optimization: sccache, mold, cargo-nextest

Long compile times are the #1 developer pain point in Rust. These tools collectively can cut iteration time by 50-80%:

sccache — Shared compilation cache:

# Install
cargo install sccache

# Configure as the Rust wrapper
export RUSTC_WRAPPER=sccache

# Or set permanently in .cargo/config.toml:
# [build]
# rustc-wrapper = "sccache"

# First build: normal speed (populates cache)
cargo build --release  # 3 minutes

# Clean + rebuild: cache hits for unchanged crates
cargo clean && cargo build --release  # 45 seconds

# Check cache statistics
sccache --show-stats
# Compile requests        1,234
# Cache hits               987 (80%)
# Cache misses             247

sccache supports shared caches (S3, GCS, Azure Blob) for team-wide and CI cache sharing.

mold — A faster linker:

Linking is often the slowest phase. mold is 3-5× faster than lld and 10-20× faster than the default GNU ld:

# Install
sudo apt install mold  # Ubuntu 22.04+
# Note: mold is for ELF targets (Linux). macOS uses Mach-O, not ELF.
# The macOS linker (ld64) is already quite fast; if you need faster:
# brew install sold     # sold = mold for Mach-O (experimental, less mature)
# In practice, macOS link times are rarely a bottleneck.
# Use mold for linking
# .cargo/config.toml
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# See https://github.com/rui314/mold/blob/main/docs/mold.md#environment-variables
export MOLD_JOBS=1

# Verify mold is being used
cargo build -v 2>&1 | grep mold

cargo-nextest — A faster test runner:

# Install
cargo install cargo-nextest

# Run tests (parallel by default, per-test timeout, retry)
cargo nextest run

# Key advantages over cargo test:
# - Each test runs in its own process → better isolation
# - Parallel execution with smart scheduling
# - Per-test timeouts (no more hanging CI)
# - JUnit XML output for CI
# - Retry failed tests

# Configuration
cargo nextest run --retries 2 --fail-fast

# Archive test binaries (useful for CI: build once, test on multiple machines)
cargo nextest archive --archive-file tests.tar.zst
cargo nextest run --archive-file tests.tar.zst
# .config/nextest.toml
[profile.default]
retries = 0
slow-timeout = { period = "60s", terminate-after = 3 }
fail-fast = true

[profile.ci]
retries = 2
fail-fast = false
junit = { path = "test-results.xml" }

Combined dev configuration:

# .cargo/config.toml — optimize the development inner loop
[build]
rustc-wrapper = "sccache"       # Cache compilation artifacts

[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]  # Faster linking

# Dev profile: optimize deps but not your code
# (put in Cargo.toml)
# [profile.dev.package."*"]
# opt-level = 2

cargo-expand and cargo-geiger — Visibility Tools

cargo-expand — see what macros generate:

cargo install cargo-expand

# Expand all macros in a specific module
cargo expand --lib accel_diag::vendor

# Expand a specific derive
# Given: #[derive(Debug, Serialize, Deserialize)]
# cargo expand shows the generated impl blocks
cargo expand --lib --tests

Invaluable for debugging #[derive] macro output, macro_rules! expansions, and understanding what serde generates for your types.

In addition to cargo-expand, you can also use rust-analyzer to expand macros:

  1. Move cursor to the macro you want to check.
  2. Open command palette (e.g. F1 on VSCode).
  3. Search for rust-analyzer: Expand macro recursively at caret.

cargo-geiger — count unsafe usage across your dependency tree:

cargo install cargo-geiger

cargo geiger
# Output:
# Metric output format: x/y
#   x = unsafe code used by the build
#   y = total unsafe code found in the crate
#
# Functions  Expressions  Impls  Traits  Methods
# 0/0        0/0          0/0    0/0     0/0      ✅ my_crate
# 0/5        0/23         0/2    0/0     0/3      ✅ serde
# 3/3        14/14        0/0    0/0     2/2      ❗ libc
# 15/15      142/142      4/4    0/0     12/12    ☢️ ring

# The symbols:
# ✅ = no unsafe used
# ❗ = some unsafe used
# ☢️ = heavily unsafe

For the project’s zero-unsafe policy, cargo geiger verifies that no dependency introduces unsafe code into the call graph that your code actually exercises.

Workspace Lints — [workspace.lints]

Since Rust 1.74, you can configure Clippy and compiler lints centrally in Cargo.toml — no more #![deny(...)] at the top of every crate:

# Root Cargo.toml — lint configuration for all crates
[workspace.lints.clippy]
unwrap_used = "warn"         # Prefer ? or expect("reason")
dbg_macro = "deny"           # No dbg!() in committed code
todo = "warn"                # Track incomplete implementations
large_enum_variant = "warn"  # Catch accidental size bloat

[workspace.lints.rust]
unsafe_code = "deny"         # Enforce zero-unsafe policy
missing_docs = "warn"        # Encourage documentation
# Each crate's Cargo.toml — opt into workspace lints
[lints]
workspace = true

This replaces scattered #![deny(clippy::unwrap_used)] attributes and ensures consistent policy across the entire workspace.

Auto-fixing Clippy warnings:

# Let Clippy automatically fix machine-applicable suggestions
cargo clippy --fix --workspace --all-targets --allow-dirty

# Fix and also apply suggestions that may change behavior (review carefully!)
cargo clippy --fix --workspace --all-targets --allow-dirty -- -W clippy::pedantic

Tip: Run cargo clippy --fix before committing. It handles trivial issues (unused imports, redundant clones, type simplifications) that are tedious to fix by hand.

MSRV Policy and rust-version

Minimum Supported Rust Version (MSRV) ensures your crate compiles on older toolchains. This matters when deploying to systems with frozen Rust versions.

# Cargo.toml
[package]
name = "diag_tool"
version = "0.1.0"
rust-version = "1.75"    # Minimum Rust version required
# Verify MSRV compliance
cargo +1.75.0 check --workspace

# Automated MSRV discovery
cargo install cargo-msrv
cargo msrv find
# Output: Minimum Supported Rust Version is 1.75.0

# Verify in CI
cargo msrv verify

MSRV in CI:

jobs:
  msrv:
    name: Check MSRV
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: "1.75.0"    # Match rust-version in Cargo.toml
      - run: cargo check --workspace

MSRV strategy:

  • Binary applications (like a large project): Use latest stable. No MSRV needed.
  • Library crates (published to crates.io): Set MSRV to oldest Rust version that supports all features you use. Commonly N-2 (two versions behind current).
  • Enterprise deployments: Set MSRV to match the oldest Rust version installed on your fleet.

Application: Production Binary Profile

The project already has an excellent release profile:

# Current workspace Cargo.toml
[profile.release]
lto = true           # ✅ Full cross-crate optimization
codegen-units = 1    # ✅ Maximum optimization
panic = "abort"      # ✅ No unwinding overhead
strip = true         # ✅ Remove symbols for deployment

[profile.dev]
opt-level = 0        # ✅ Fast compilation
debug = true         # ✅ Full debug info

Recommended additions:

# Optimize dependencies in dev mode (faster test execution)
[profile.dev.package."*"]
opt-level = 2

# Test profile: some optimization to prevent timeout in slow tests
[profile.test]
opt-level = 1

# Keep overflow checks in release (safety)
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
overflow-checks = true    # ← add this: catch integer overflows
debug = "line-tables-only" # ← add this: backtraces without full DWARF

Recommended developer tooling:

# .cargo/config.toml (proposed)
[build]
rustc-wrapper = "sccache"  # 80%+ cache hit after first build

[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]  # 3-5× faster linking

Expected impact on the project:

MetricCurrentWith Additions
Release binary~10 MB (stripped, LTO)Same
Dev build time~45s~25s (sccache + mold)
Rebuild (1 file change)~15s~5s (sccache + mold)
Test executioncargo testcargo nextest — 2× faster
Dep vulnerability scanningNonecargo audit in CI
License complianceManualcargo deny automated
Unused dependency detectionManualcargo udeps in CI

cargo-watch — Auto-Rebuild on File Changes

cargo-watch re-runs a command every time a source file changes — essential for tight feedback loops:

# Install
cargo install cargo-watch

# Re-check on every save (instant feedback)
cargo watch -x check

# Run clippy + tests on change
cargo watch -x 'clippy --workspace --all-targets' -x 'test --workspace --lib'

# Watch only specific crates (faster for large workspaces)
cargo watch -w accel_diag/src -x 'test -p accel_diag'

# Clear screen between runs
cargo watch -c -x check

Tip: Combine with mold + sccache from above for sub-second re-check times on incremental changes.

cargo doc and Workspace Documentation

For a large workspace, generated documentation is essential for discoverability. cargo doc uses rustdoc to produce HTML docs from doc-comments and type signatures:

# Generate docs for all workspace crates (opens in browser)
cargo doc --workspace --no-deps --open

# Include private items (useful during development)
cargo doc --workspace --no-deps --document-private-items

# Check doc-links without generating HTML (fast CI check)
cargo doc --workspace --no-deps 2>&1 | grep -E 'warning|error'

Intra-doc links — link between types across crates without URLs:

#![allow(unused)]
fn main() {
/// Runs GPU diagnostics using [`GpuConfig`] settings.
///
/// See [`crate::accel_diag::run_diagnostics`] for the implementation.
/// Returns [`DiagResult`] which can be serialized to the
/// [`DerReport`](crate::core_lib::DerReport) format.
pub fn run_accel_diag(config: &GpuConfig) -> DiagResult {
    // ...
}
}

Show platform-specific APIs in docs:

#![allow(unused)]
fn main() {
// Cargo.toml: [package.metadata.docs.rs]
// all-features = true
// rustdoc-args = ["--cfg", "docsrs"]

/// Windows-only: read battery status via Win32 API.
///
/// Only available on `cfg(windows)` builds.
#[cfg(windows)]
#[doc(cfg(windows))]  // Shows "Available on Windows only" badge in docs
pub fn get_battery_status() -> Option<u8> {
    // ...
}
}

CI documentation check:

# Add to CI workflow
- name: Check documentation
  run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
  # Treats broken intra-doc links as errors

For the project: With many crates, cargo doc --workspace is the best way for new team members to discover the API surface. Add RUSTDOCFLAGS="-D warnings" to CI to catch broken doc-links before merge.

Compile-Time Decision Tree

flowchart TD
    START["Compile too slow?"] --> WHERE{"Where's the time?"}

    WHERE -->|"Recompiling\nunchanged crates"| SCCACHE["sccache\nShared compilation cache"]
    WHERE -->|"Linking phase"| MOLD["mold linker\n3-10× faster linking"]
    WHERE -->|"Running tests"| NEXTEST["cargo-nextest\nParallel test runner"]
    WHERE -->|"Everything"| COMBO["All of the above +\ncargo-udeps to trim deps"]

    SCCACHE --> CI_CACHE{"CI or local?"}
    CI_CACHE -->|"CI"| S3["S3/GCS shared cache"]
    CI_CACHE -->|"Local"| LOCAL["Local disk cache\nauto-configured"]

    style SCCACHE fill:#91e5a3,color:#000
    style MOLD fill:#e3f2fd,color:#000
    style NEXTEST fill:#ffd43b,color:#000
    style COMBO fill:#b39ddb,color:#000

🏋️ Exercises

🟢 Exercise 1: Set Up sccache + mold

Install sccache and mold, configure them in .cargo/config.toml, then measure the compile time improvement on a clean rebuild.

Solution
# Install
cargo install sccache
sudo apt install mold  # Ubuntu 22.04+

# Configure .cargo/config.toml:
cat > .cargo/config.toml << 'EOF'
[build]
rustc-wrapper = "sccache"

[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
EOF

# First build (populates cache)
time cargo build --release  # e.g., 180s

# Clean + rebuild (cache hits)
cargo clean
time cargo build --release  # e.g., 45s

sccache --show-stats
# Cache hits should be 60-80%+

🟡 Exercise 2: Switch to cargo-nextest

Install cargo-nextest and run your test suite. Compare wall-clock time with cargo test. What’s the speedup?

Solution
cargo install cargo-nextest

# Standard test runner
time cargo test --workspace 2>&1 | tail -5

# nextest (parallel per-test-binary execution)
time cargo nextest run --workspace 2>&1 | tail -5

# Typical speedup: 2-5× for large workspaces
# nextest also provides:
# - Per-test timing
# - Retries for flaky tests
# - JUnit XML output for CI
cargo nextest run --workspace --retries 2

Key Takeaways

  • sccache with S3/GCS backend shares compilation cache across team and CI
  • mold is the fastest ELF linker — link times drop from seconds to milliseconds
  • cargo-nextest runs tests in parallel per-binary with better output and retry support
  • cargo-geiger counts unsafe usage — run it before accepting new dependencies
  • [workspace.lints] centralizes Clippy and rustc lint configuration across a multi-crate workspace

no_std and Feature Verification 🔴

What you’ll learn:

  • Verifying feature combinations systematically with cargo-hack
  • The three layers of Rust: core vs alloc vs std and when to use each
  • Building no_std crates with custom panic handlers and allocators
  • Testing no_std code on host and with QEMU

Cross-references: Windows & Conditional Compilation — the platform half of this topic · Cross-Compilation — cross-compiling to ARM and embedded targets · Miri and Sanitizers — verifying unsafe code in no_std environments · Build Scripts — cfg flags emitted by build.rs

Rust runs everywhere from 8-bit microcontrollers to cloud servers. This chapter covers the foundation: stripping the standard library with #![no_std] and verifying that your feature combinations actually compile.

Verifying Feature Combinations with cargo-hack

cargo-hack tests all feature combinations systematically — essential for crates with #[cfg(...)] code:

# Install
cargo install cargo-hack

# Check that every feature compiles individually
cargo hack check --each-feature --workspace

# The nuclear option: test ALL feature combinations (exponential!)
# Only practical for crates with <8 features.
cargo hack check --feature-powerset --workspace

# Practical compromise: test each feature alone + all features + no features
cargo hack check --each-feature --workspace --no-dev-deps
cargo check --workspace --all-features
cargo check --workspace --no-default-features

Why this matters for the project:

If you add platform features (linux, windows, direct-ipmi, direct-accel-api), cargo-hack catches combinations that break:

# Example: features that gate platform code
[features]
default = ["linux"]
linux = []                          # Linux-specific hardware access
windows = ["dep:windows-sys"]       # Windows-specific APIs
direct-ipmi = []                    # unsafe IPMI ioctl (ch05)
direct-accel-api = []                    # unsafe accel-mgmt FFI (ch05)
# Verify all features compile in isolation AND together
cargo hack check --each-feature -p diag_tool
# Catches: "feature 'windows' doesn't compile without 'direct-ipmi'"
# Catches: "#[cfg(feature = \"linux\")] has a typo — it's 'lnux'"

CI integration:

# Add to CI pipeline (fast — just compilation checks)
- name: Feature matrix check
  run: cargo hack check --each-feature --workspace --no-dev-deps

Rule of thumb: Run cargo hack check --each-feature in CI for any crate with 2+ features. Run --feature-powerset only for core library crates with <8 features — it’s exponential ($2^n$ combinations).

no_std — When and Why

#![no_std] tells the compiler: “don’t link the standard library.” Your crate can only use core (and optionally alloc). Why would you want this?

ScenarioWhy no_std
Embedded firmware (ARM Cortex-M, RISC-V)No OS, no heap, no file system
UEFI diagnostics toolPre-boot environment, no OS APIs
Kernel modulesKernel space can’t use userspace std
WebAssembly (WASM)Minimize binary size, no OS dependencies
BootloadersRun before any OS exists
Shared library with C interfaceAvoid Rust runtime in callers

For hardware diagnostics, no_std becomes relevant when building:

  • UEFI-based pre-boot diagnostic tools (before the OS loads)
  • BMC firmware diagnostics (resource-constrained ARM SoCs)
  • Kernel-level PCIe diagnostics (kernel module or eBPF probe)

core vs alloc vs std — The Three Layers

┌─────────────────────────────────────────────────────────────┐
│ std                                                         │
│  Everything in core + alloc, PLUS:                          │
│  • File I/O (std::fs, std::io)                              │
│  • Networking (std::net)                                    │
│  • Threads (std::thread)                                    │
│  • Time (std::time)                                         │
│  • Environment (std::env)                                   │
│  • Process (std::process)                                   │
│  • OS-specific (std::os::unix, std::os::windows)            │
├─────────────────────────────────────────────────────────────┤
│ alloc          (available with #![no_std] + extern crate    │
│                 alloc, if you have a global allocator)       │
│  • String, Vec, Box, Rc, Arc                                │
│  • BTreeMap, BTreeSet                                       │
│  • format!() macro                                          │
│  • Collections and smart pointers that need heap            │
├─────────────────────────────────────────────────────────────┤
│ core           (always available, even in #![no_std])        │
│  • Primitive types (u8, bool, char, etc.)                    │
│  • Option, Result                                           │
│  • Iterator, slice, array, str (slices, not String)         │
│  • Traits: Clone, Copy, Debug, Display, From, Into          │
│  • Atomics (core::sync::atomic)                             │
│  • Cell, RefCell (core::cell)  — Pin (core::pin)            │
│  • core::fmt (formatting without allocation)                │
│  • core::mem, core::ptr (low-level memory operations)       │
│  • Math: core::num, basic arithmetic                        │
└─────────────────────────────────────────────────────────────┘

What you lose without std:

  • No HashMap (requires a hasher — use BTreeMap from alloc, or hashbrown)
  • No println!() (requires stdout — use core::fmt::Write to a buffer)
  • No std::error::Error (stabilized in core since Rust 1.81, but many ecosystems haven’t migrated)
  • No file I/O, no networking, no threads (unless provided by a platform HAL)
  • No Mutex (use spin::Mutex or platform-specific locks)

Building a no_std Crate

#![allow(unused)]
fn main() {
// src/lib.rs — a no_std library crate
#![no_std]

// Optionally use heap allocation
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;

/// Temperature reading from a thermal sensor.
/// This struct works in any environment — bare metal to Linux.
#[derive(Clone, Copy, Debug)]
pub struct Temperature {
    /// Raw sensor value (0.0625°C per LSB for typical I2C sensors)
    raw: u16,
}

impl Temperature {
    pub const fn from_raw(raw: u16) -> Self {
        Self { raw }
    }

    /// Convert to degrees Celsius (fixed-point, no FPU required)
    pub const fn millidegrees_c(&self) -> i32 {
        (self.raw as i32) * 625 / 10 // 0.0625°C resolution
    }

    pub fn degrees_c(&self) -> f32 {
        self.raw as f32 * 0.0625
    }
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let md = self.millidegrees_c();
        // Handle sign correctly for values between -0.999°C and -0.001°C
        // where md / 1000 == 0 but the value is negative.
        if md < 0 && md > -1000 {
            write!(f, "-0.{:03}°C", (-md) % 1000)
        } else {
            write!(f, "{}.{:03}°C", md / 1000, (md % 1000).abs())
        }
    }
}

/// Parse space-separated temperature values.
/// Uses alloc — requires a global allocator.
pub fn parse_temperatures(input: &str) -> Vec<Temperature> {
    input
        .split_whitespace()
        .filter_map(|s| s.parse::<u16>().ok())
        .map(Temperature::from_raw)
        .collect()
}

/// Format without allocation — writes directly to a buffer.
/// Works in `core`-only environments (no alloc, no heap).
pub fn format_temp_into(temp: &Temperature, buf: &mut [u8]) -> usize {
    use core::fmt::Write;
    struct SliceWriter<'a> {
        buf: &'a mut [u8],
        pos: usize,
    }
    impl<'a> Write for SliceWriter<'a> {
        fn write_str(&mut self, s: &str) -> fmt::Result {
            let bytes = s.as_bytes();
            let remaining = self.buf.len() - self.pos;
            if bytes.len() > remaining {
                // Buffer full — signal the error instead of silently truncating.
                // Callers can check the returned pos for partial writes.
                return Err(fmt::Error);
            }
            self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes);
            self.pos += bytes.len();
            Ok(())
        }
    }
    let mut w = SliceWriter { buf, pos: 0 };
    let _ = write!(w, "{}", temp);
    w.pos
}
}
# Cargo.toml for a no_std crate
[package]
name = "thermal-sensor"
version = "0.1.0"
edition = "2021"

[features]
default = ["alloc"]
alloc = []    # Enable Vec, String, etc.
std = []      # Enable full std (implies alloc)

[dependencies]
# Use no_std-compatible crates
serde = { version = "1.0", default-features = false, features = ["derive"] }
# ↑ default-features = false drops std dependency!

Key crate pattern: Many popular crates (serde, log, rand, embedded-hal) support no_std via default-features = false. Always check whether a dependency requires std before using it in a no_std context. Note that some crates (e.g., regex) require at least alloc and don’t work in core-only environments.

Custom Panic Handlers and Allocators

In #![no_std] binaries (not libraries), you must provide a panic handler and optionally a global allocator:

// src/main.rs — a no_std binary (e.g., UEFI diagnostic)
#![no_std]
#![no_main]

extern crate alloc;

use core::panic::PanicInfo;

// Required: what to do on panic (no stack unwinding available)
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    // In embedded: blink an LED, write to UART, hang
    // In UEFI: write to console, halt
    // Minimal: just loop forever
    loop {
        core::hint::spin_loop();
    }
}

// Required if using alloc: provide a global allocator
use alloc::alloc::{GlobalAlloc, Layout};

struct BumpAllocator {
    // Simple bump allocator for embedded/UEFI
    // In practice, use a crate like `linked_list_allocator` or `embedded-alloc`
}

// WARNING: This is a non-functional placeholder! Calling alloc() will return
// null, causing immediate UB (the global allocator contract requires non-null
// returns for non-zero-sized allocations). In real code, use an established
// allocator crate:
//   - embedded-alloc (embedded targets)
//   - linked_list_allocator (UEFI / OS kernels)
//   - talc (general-purpose no_std)
unsafe impl GlobalAlloc for BumpAllocator {
    /// # Safety
    /// Layout must have non-zero size. Returns null (placeholder — will crash).
    unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
        // PLACEHOLDER — will crash! Replace with real allocation logic.
        core::ptr::null_mut()
    }
    /// # Safety
    /// `_ptr` must have been returned by `alloc` with a compatible layout.
    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
        // No-op for bump allocator
    }
}

#[global_allocator]
static ALLOCATOR: BumpAllocator = BumpAllocator {};

// Entry point (platform-specific, not fn main)
// For UEFI: #[entry] or efi_main
// For embedded: #[cortex_m_rt::entry]

Testing no_std Code

Tests run on the host machine, which has std. The trick: your library is no_std, but your test harness uses std:

#![allow(unused)]
fn main() {
// Your crate: #![no_std] in src/lib.rs
// But tests run under std automatically:

#[cfg(test)]
mod tests {
    use super::*;
    // std is available here — println!, assert!, Vec all work

    #[test]
    fn test_temperature_conversion() {
        let temp = Temperature::from_raw(800); // 50.0°C
        assert_eq!(temp.millidegrees_c(), 50000);
        assert!((temp.degrees_c() - 50.0).abs() < 0.01);
    }

    #[test]
    fn test_format_into_buffer() {
        let temp = Temperature::from_raw(800);
        let mut buf = [0u8; 32];
        let len = format_temp_into(&temp, &mut buf);
        let s = core::str::from_utf8(&buf[..len]).unwrap();
        assert_eq!(s, "50.000°C");
    }
}
}

Testing on the actual target (when std isn’t available at all):

# Use defmt-test for on-device testing (embedded ARM)
# Use uefi-test-runner for UEFI targets
# Use QEMU for cross-architecture tests without hardware

# Run no_std library tests on host (always works):
cargo test --lib

# Verify no_std compilation against a no_std target:
cargo check --target thumbv7em-none-eabihf  # ARM Cortex-M
cargo check --target riscv32imac-unknown-none-elf  # RISC-V

no_std Decision Tree

flowchart TD
    START["Does your code need\nthe standard library?"] --> NEED_FS{"File system,\nnetwork, threads?"}
    NEED_FS -->|"Yes"| USE_STD["Use std\nNormal application"]
    NEED_FS -->|"No"| NEED_HEAP{"Need heap allocation?\nVec, String, Box"}
    NEED_HEAP -->|"Yes"| USE_ALLOC["#![no_std]\nextern crate alloc"]
    NEED_HEAP -->|"No"| USE_CORE["#![no_std]\ncore only"]
    
    USE_ALLOC --> VERIFY["cargo-hack\n--each-feature"]
    USE_CORE --> VERIFY
    USE_STD --> VERIFY
    VERIFY --> TARGET{"Target has OS?"}
    TARGET -->|"Yes"| HOST_TEST["cargo test --lib\nStandard testing"]
    TARGET -->|"No"| CROSS_TEST["QEMU / defmt-test\nOn-device testing"]
    
    style USE_STD fill:#91e5a3,color:#000
    style USE_ALLOC fill:#ffd43b,color:#000
    style USE_CORE fill:#ff6b6b,color:#000

🏋️ Exercises

🟡 Exercise 1: Feature Combination Verification

Install cargo-hack and run cargo hack check --each-feature --workspace on a project with multiple features. Does it find any broken combinations?

Solution
cargo install cargo-hack

# Check each feature individually
cargo hack check --each-feature --workspace --no-dev-deps

# If a feature combination fails:
# error[E0433]: failed to resolve: use of undeclared crate or module `std`
# → This means a feature gate is missing a #[cfg] guard

# Check all features + no features + each individually:
cargo hack check --each-feature --workspace
cargo check --workspace --all-features
cargo check --workspace --no-default-features

🔴 Exercise 2: Build a no_std Library

Create a library crate that compiles with #![no_std]. Implement a simple stack-allocated ring buffer. Verify it compiles for thumbv7em-none-eabihf (ARM Cortex-M).

Solution
#![allow(unused)]
fn main() {
// lib.rs
#![no_std]

pub struct RingBuffer<const N: usize> {
    data: [u8; N],
    head: usize,
    len: usize,
}

impl<const N: usize> RingBuffer<N> {
    pub const fn new() -> Self {
        Self { data: [0; N], head: 0, len: 0 }
    }

    pub fn push(&mut self, byte: u8) -> bool {
        if self.len == N { return false; }
        let idx = (self.head + self.len) % N;
        self.data[idx] = byte;
        self.len += 1;
        true
    }

    pub fn pop(&mut self) -> Option<u8> {
        if self.len == 0 { return None; }
        let byte = self.data[self.head];
        self.head = (self.head + 1) % N;
        self.len -= 1;
        Some(byte)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn push_pop() {
        let mut rb = RingBuffer::<4>::new();
        assert!(rb.push(1));
        assert!(rb.push(2));
        assert_eq!(rb.pop(), Some(1));
        assert_eq!(rb.pop(), Some(2));
        assert_eq!(rb.pop(), None);
    }
}
}
rustup target add thumbv7em-none-eabihf
cargo check --target thumbv7em-none-eabihf
# ✅ Compiles for bare-metal ARM

Key Takeaways

  • cargo-hack --each-feature is essential for any crate with conditional compilation — run it in CI
  • core → alloc → std are layered: each adds capabilities but requires more runtime support
  • Custom panic handlers and allocators are required for bare-metal no_std binaries
  • Test no_std libraries on the host with cargo test --lib — no hardware needed
  • Run --feature-powerset only for core libraries with <8 features — it’s $2^n$ combinations

Windows and Conditional Compilation 🟡

What you’ll learn:

  • Windows support patterns: windows-sys/windows crates, cargo-xwin
  • Conditional compilation with #[cfg] — checked by the compiler, not the preprocessor
  • Platform abstraction architecture: when #[cfg] blocks suffice vs when to use traits
  • Cross-compiling for Windows from Linux

Cross-references: no_std & Features — cargo-hack and feature verification · Cross-Compilation — general cross-build setup · Build Scripts — cfg flags emitted by build.rs

Windows Support — Platform Abstractions

Rust’s #[cfg()] attributes and Cargo features allow a single codebase to target both Linux and Windows cleanly. The project already demonstrates this pattern in platform::run_command:

#![allow(unused)]
fn main() {
// Real pattern from the project — platform-specific shell invocation
pub fn exec_cmd(cmd: &str, timeout_secs: Option<u64>) -> Result<CommandResult, CommandError> {
    #[cfg(windows)]
    let mut child = Command::new("cmd")
        .args(["/C", cmd])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;

    #[cfg(not(windows))]
    let mut child = Command::new("sh")
        .args(["-c", cmd])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;

    // ... rest is platform-independent ...
}
}

Available cfg predicates:

#![allow(unused)]
fn main() {
// Operating system
#[cfg(target_os = "linux")]         // Linux specifically
#[cfg(target_os = "windows")]       // Windows
#[cfg(target_os = "macos")]         // macOS
#[cfg(unix)]                        // Linux, macOS, BSDs, etc.
#[cfg(windows)]                     // Windows (shorthand)

// Architecture
#[cfg(target_arch = "x86_64")]      // x86 64-bit
#[cfg(target_arch = "aarch64")]     // ARM 64-bit
#[cfg(target_arch = "x86")]         // x86 32-bit

// Pointer width (portable alternative to arch)
#[cfg(target_pointer_width = "64")] // Any 64-bit platform
#[cfg(target_pointer_width = "32")] // Any 32-bit platform

// Environment / C library
#[cfg(target_env = "gnu")]          // glibc
#[cfg(target_env = "musl")]         // musl libc
#[cfg(target_env = "msvc")]         // MSVC on Windows

// Endianness
#[cfg(target_endian = "little")]
#[cfg(target_endian = "big")]

// Combinations with any(), all(), not()
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[cfg(not(windows))]
}

The windows-sys and windows Crates

For calling Windows APIs directly:

# Cargo.toml — use windows-sys for raw FFI (lighter, no abstraction)
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
    "Win32_Foundation",
    "Win32_System_Services",
    "Win32_System_Registry",
    "Win32_System_Power",
] }
# NOTE: windows-sys uses semver-incompatible releases (0.48 → 0.52 → 0.59).
# Pin to a single minor version — each release may remove or rename API bindings.
# Check https://github.com/microsoft/windows-rs for the latest version
# before starting a new project.

# Or use the windows crate for safe wrappers (heavier, more ergonomic)
# windows = { version = "0.59", features = [...] }
#![allow(unused)]
fn main() {
// src/platform/windows.rs
#[cfg(windows)]
mod win {
    use windows_sys::Win32::System::Power::{
        GetSystemPowerStatus, SYSTEM_POWER_STATUS,
    };

    pub fn get_battery_status() -> Option<u8> {
        let mut status = SYSTEM_POWER_STATUS::default();
        // SAFETY: GetSystemPowerStatus writes to the provided buffer.
        // The buffer is correctly sized and aligned.
        let ok = unsafe { GetSystemPowerStatus(&mut status) };
        if ok != 0 {
            Some(status.BatteryLifePercent)
        } else {
            None
        }
    }
}
}

windows-sys vs windows crate:

Aspectwindows-syswindows
API styleRaw FFI (unsafe calls)Safe Rust wrappers
Binary sizeMinimal (just extern declarations)Larger (wrapper code)
Compile timeFastSlower
ErgonomicsC-style, manual safetyRust-idiomatic
Error handlingRaw BOOL / HRESULTResult<T, windows::core::Error>
Use whenPerformance-critical, thin wrapperApplication code, ease of use

Cross-Compiling for Windows from Linux

# Option 1: MinGW (GNU ABI)
rustup target add x86_64-pc-windows-gnu
sudo apt install gcc-mingw-w64-x86-64
cargo build --target x86_64-pc-windows-gnu
# Produces a .exe — runs on Windows, links against msvcrt

# Option 2: MSVC ABI via xwin (for full MSVC compatibility)
cargo install cargo-xwin
cargo xwin build --target x86_64-pc-windows-msvc
# Uses Microsoft's CRT and SDK headers downloaded automatically

# Option 3: Zig-based cross-compilation
cargo zigbuild --target x86_64-pc-windows-gnu

GNU vs MSVC ABI on Windows:

Aspectx86_64-pc-windows-gnux86_64-pc-windows-msvc
LinkerMinGW ldMSVC link.exe or lld-link
C runtimemsvcrt.dll (universal)ucrtbase.dll (modern)
C++ interopGCC ABIMSVC ABI
Cross-compile from LinuxEasy (MinGW)Possible (cargo-xwin)
Windows API supportFullFull
Debug info formatDWARFPDB
Recommended forSimple tools, CI buildsFull Windows integration

Conditional Compilation Patterns

Pattern 1: Platform module selection

#![allow(unused)]
fn main() {
// src/platform/mod.rs — compile different modules per OS
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::*;

#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
pub use windows::*;

// Both modules implement the same public API:
// pub fn get_cpu_temperature() -> Result<f64, PlatformError>
// pub fn list_pci_devices() -> Result<Vec<PciDevice>, PlatformError>
}

Pattern 2: Feature-gated platform support

# Cargo.toml
[features]
default = ["linux"]
linux = []              # Linux-specific hardware access
windows = ["dep:windows-sys"]  # Windows-specific APIs

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [...], optional = true }
#![allow(unused)]
fn main() {
// Compile error if someone tries to build for Windows without the feature:
#[cfg(all(target_os = "windows", not(feature = "windows")))]
compile_error!("Enable the 'windows' feature to build for Windows");
}

Pattern 3: Trait-based platform abstraction

#![allow(unused)]
fn main() {
/// Platform-independent interface for hardware access.
pub trait HardwareAccess {
    type Error: std::error::Error;

    fn read_cpu_temperature(&self) -> Result<f64, Self::Error>;
    fn read_gpu_temperature(&self, gpu_index: u32) -> Result<f64, Self::Error>;
    fn list_pci_devices(&self) -> Result<Vec<PciDevice>, Self::Error>;
    fn send_ipmi_command(&self, cmd: &IpmiCmd) -> Result<IpmiResponse, Self::Error>;
}

#[cfg(target_os = "linux")]
pub struct LinuxHardware;

#[cfg(target_os = "linux")]
impl HardwareAccess for LinuxHardware {
    type Error = LinuxHwError;

    fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
        // Read from /sys/class/thermal/thermal_zone0/temp
        let raw = std::fs::read_to_string("/sys/class/thermal/thermal_zone0/temp")?;
        Ok(raw.trim().parse::<f64>()? / 1000.0)
    }
    // ...
}

#[cfg(target_os = "windows")]
pub struct WindowsHardware;

#[cfg(target_os = "windows")]
impl HardwareAccess for WindowsHardware {
    type Error = WindowsHwError;

    fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
        // Read via WMI (Win32_TemperatureProbe) or Open Hardware Monitor
        todo!("WMI temperature query")
    }
    // ...
}

/// Create the platform-appropriate implementation
pub fn create_hardware() -> impl HardwareAccess {
    #[cfg(target_os = "linux")]
    { LinuxHardware }
    #[cfg(target_os = "windows")]
    { WindowsHardware }
}
}

Platform Abstraction Architecture

For a project that targets multiple platforms, organize code into three layers:

┌──────────────────────────────────────────────────┐
│ Application Logic (platform-independent)          │
│  diag_tool, accel_diag, network_diag, event_log, etc.      │
│  Uses only the platform abstraction trait          │
├──────────────────────────────────────────────────┤
│ Platform Abstraction Layer (trait definitions)    │
│  trait HardwareAccess { ... }                     │
│  trait CommandRunner { ... }                      │
│  trait FileSystem { ... }                         │
├──────────────────────────────────────────────────┤
│ Platform Implementations (cfg-gated)              │
│  ┌──────────────┐  ┌──────────────┐              │
│  │ Linux impl   │  │ Windows impl │              │
│  │ /sys, /proc  │  │ WMI, Registry│              │
│  │ ipmitool     │  │ ipmiutil     │              │
│  │ lspci        │  │ devcon       │              │
│  └──────────────┘  └──────────────┘              │
└──────────────────────────────────────────────────┘

Testing the abstraction: Mock the platform trait for unit tests:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    struct MockHardware {
        cpu_temp: f64,
        gpu_temps: Vec<f64>,
    }

    impl HardwareAccess for MockHardware {
        type Error = std::io::Error;

        fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
            Ok(self.cpu_temp)
        }

        fn read_gpu_temperature(&self, index: u32) -> Result<f64, Self::Error> {
            self.gpu_temps.get(index as usize)
                .copied()
                .ok_or_else(|| std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    format!("GPU {index} not found")
                ))
        }

        fn list_pci_devices(&self) -> Result<Vec<PciDevice>, Self::Error> {
            Ok(vec![]) // Mock returns empty
        }

        fn send_ipmi_command(&self, _cmd: &IpmiCmd) -> Result<IpmiResponse, Self::Error> {
            Ok(IpmiResponse::default())
        }
    }

    #[test]
    fn test_thermal_check_with_mock() {
        let hw = MockHardware {
            cpu_temp: 75.0,
            gpu_temps: vec![82.0, 84.0],
        };
        let result = run_thermal_diagnostic(&hw);
        assert!(result.is_ok());
    }
}
}

Application: Linux-First, Windows-Ready

The project is already partially Windows-ready. Use cargo-hack to verify all feature combinations, and cross-compile to test on Windows from Linux:

Already done:

  • platform::run_command uses #[cfg(windows)] for shell selection
  • Tests use #[cfg(windows)] / #[cfg(not(windows))] for platform-appropriate test commands

Recommended evolution path for Windows support:

Phase 1: Extract platform abstraction trait (current → 2 weeks)
  ├─ Define HardwareAccess trait in core_lib
  ├─ Wrap current Linux code behind LinuxHardware impl
  └─ All diagnostic modules depend on trait, not Linux specifics

Phase 2: Add Windows stubs (2 weeks)
  ├─ Implement WindowsHardware with TODO stubs
  ├─ CI builds for x86_64-pc-windows-msvc (compile check only)
  └─ Tests pass with MockHardware on all platforms

Phase 3: Windows implementation (ongoing)
  ├─ IPMI via ipmiutil.exe or OpenIPMI Windows driver
  ├─ GPU via accel-mgmt (accel-api.dll) — same API as Linux
  ├─ PCIe via Windows Setup API (SetupDiEnumDeviceInfo)
  └─ NIC via WMI (Win32_NetworkAdapter)

Cross-platform CI addition:

# Add to CI matrix
- target: x86_64-pc-windows-msvc
  os: windows-latest
  name: windows-x86_64

This ensures the codebase compiles on Windows even before full Windows implementation is complete — catching cfg mistakes early.

Key insight: The abstraction doesn’t need to be perfect on day one. Start with #[cfg] blocks in leaf functions (like exec_cmd already does), then refactor to traits when you have two or more platform implementations. Premature abstraction is worse than #[cfg] blocks.

Conditional Compilation Decision Tree

flowchart TD
    START["Platform-specific code?"] --> HOW_MANY{"How many platforms?"}
    
    HOW_MANY -->|"2 (Linux + Windows)"| CFG_BLOCKS["#[cfg] blocks\nin leaf functions"]
    HOW_MANY -->|"3+"| TRAIT_APPROACH["Platform trait\n+ per-platform impl"]
    
    CFG_BLOCKS --> WINAPI{"Need Windows APIs?"}
    WINAPI -->|"Minimal"| WIN_SYS["windows-sys\nRaw FFI bindings"]
    WINAPI -->|"Rich (COM, etc)"| WIN_RS["windows crate\nSafe idiomatic wrappers"]
    WINAPI -->|"None\n(just #[cfg])"| NATIVE["cfg(windows)\ncfg(unix)"]
    
    TRAIT_APPROACH --> CI_CHECK["cargo-hack\n--each-feature"]
    CFG_BLOCKS --> CI_CHECK
    CI_CHECK --> XCOMPILE["Cross-compile in CI\ncargo-xwin or\nnative runners"]
    
    style CFG_BLOCKS fill:#91e5a3,color:#000
    style TRAIT_APPROACH fill:#ffd43b,color:#000
    style WIN_SYS fill:#e3f2fd,color:#000
    style WIN_RS fill:#e3f2fd,color:#000

🏋️ Exercises

🟢 Exercise 1: Platform-Conditional Module

Create a module with #[cfg(unix)] and #[cfg(windows)] implementations of a get_hostname() function. Verify both compile with cargo check and cargo check --target x86_64-pc-windows-msvc.

Solution
#![allow(unused)]
fn main() {
// src/hostname.rs
#[cfg(unix)]
pub fn get_hostname() -> String {
    use std::fs;
    fs::read_to_string("/etc/hostname")
        .unwrap_or_else(|_| "unknown".to_string())
        .trim()
        .to_string()
}

#[cfg(windows)]
pub fn get_hostname() -> String {
    use std::env;
    env::var("COMPUTERNAME").unwrap_or_else(|_| "unknown".to_string())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hostname_is_not_empty() {
        let name = get_hostname();
        assert!(!name.is_empty());
    }
}
}
# Verify Linux compilation
cargo check

# Verify Windows compilation (cross-check)
rustup target add x86_64-pc-windows-msvc
cargo check --target x86_64-pc-windows-msvc

🟡 Exercise 2: Cross-Compile for Windows with cargo-xwin

Install cargo-xwin and build a simple binary for x86_64-pc-windows-msvc from Linux. Verify the output is a .exe.

Solution
cargo install cargo-xwin
rustup target add x86_64-pc-windows-msvc

cargo xwin build --release --target x86_64-pc-windows-msvc
# Downloads Windows SDK headers/libs automatically

file target/x86_64-pc-windows-msvc/release/my-binary.exe
# Output: PE32+ executable (console) x86-64, for MS Windows

# You can also test with Wine:
wine target/x86_64-pc-windows-msvc/release/my-binary.exe

Key Takeaways

  • Start with #[cfg] blocks in leaf functions; refactor to traits only when three or more platforms diverge
  • windows-sys is for raw FFI; the windows crate provides safe, idiomatic wrappers
  • cargo-xwin cross-compiles to Windows MSVC ABI from Linux — no Windows machine needed
  • Always check --target x86_64-pc-windows-msvc in CI even if you only ship on Linux
  • Combine #[cfg] with Cargo features for optional platform support (e.g., feature = "windows")

Putting It All Together — A Production CI/CD Pipeline 🟡

What you’ll learn:

  • Structuring a multi-stage GitHub Actions CI workflow (check → test → coverage → security → cross → release)
  • Caching strategies with rust-cache and save-if tuning
  • Running Miri and sanitizers on a nightly schedule
  • Task automation with Makefile.toml and pre-commit hooks
  • Automated releases with cargo-dist

Cross-references: Build Scripts · Cross-Compilation · Benchmarking · Coverage · Miri/Sanitizers · Dependencies · Release Profiles · Compile-Time Tools · no_std · Windows

Individual tools are useful. A pipeline that orchestrates them automatically on every push is transformative. This chapter assembles the tools from chapters 1–10 into a cohesive CI/CD workflow.

The Complete GitHub Actions Workflow

A single workflow file that runs all verification stages in parallel:

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always
  CARGO_ENCODED_RUSTFLAGS: "-Dwarnings"  # Treat warnings as errors (top-level crate only)
  # NOTE: Unlike RUSTFLAGS, CARGO_ENCODED_RUSTFLAGS does not affect build scripts
  # or proc-macros, which avoids false failures from third-party warnings.
  # Use RUSTFLAGS="-Dwarnings" instead if you want to enforce on build scripts too.

jobs:
  # ─── Stage 1: Fast feedback (< 2 min) ───
  check:
    name: Check + Clippy + Format
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, rustfmt

      - uses: Swatinem/rust-cache@v2  # Cache dependencies

      - name: Check Cargo.lock
        run: cargo fetch --locked

      - name: Check doc
        run: RUSTDOCFLAGS='-Dwarnings' cargo doc --workspace --all-features --no-deps

      - name: Check compilation
        run: cargo check --workspace --all-targets --all-features

      - name: Clippy lints
        run: cargo clippy --workspace --all-targets --all-features -- -D warnings

      - name: Formatting
        run: cargo fmt --all -- --check

  # ─── Stage 2: Tests (< 5 min) ───
  test:
    name: Test (${{ matrix.os }})
    needs: check
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2

      - name: Run tests
        run: cargo test --workspace

      - name: Run doc tests
        run: cargo test --workspace --doc

  # ─── Stage 3: Cross-compilation (< 10 min) ───
  cross:
    name: Cross (${{ matrix.target }})
    needs: check
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            use_cross: true
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Install musl-tools
        if: contains(matrix.target, 'musl')
        run: sudo apt-get install -y musl-tools

      - name: Install cross
        if: matrix.use_cross
        uses: taiki-e/install-action@cross

      - name: Build (native)
        if: "!matrix.use_cross"
        run: cargo build --release --target ${{ matrix.target }}

      - name: Build (cross)
        if: matrix.use_cross
        run: cross build --release --target ${{ matrix.target }}

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: binary-${{ matrix.target }}
          path: target/${{ matrix.target }}/release/diag_tool

  # ─── Stage 4: Coverage (< 10 min) ───
  coverage:
    name: Code Coverage
    needs: check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - uses: taiki-e/install-action@cargo-llvm-cov

      - name: Generate coverage
        run: cargo llvm-cov --workspace --lcov --output-path lcov.info

      - name: Enforce minimum coverage
        run: cargo llvm-cov --workspace --fail-under-lines 75

      - name: Upload to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: lcov.info
          token: ${{ secrets.CODECOV_TOKEN }}

  # ─── Stage 5: Safety verification (< 15 min) ───
  miri:
    name: Miri
    needs: check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@nightly
        with:
          components: miri

      - name: Run Miri
        run: cargo miri test --workspace
        env:
          MIRIFLAGS: "-Zmiri-backtrace=full"

  # ─── Stage 6: Benchmarks (PR only, < 10 min) ───
  bench:
    name: Benchmarks
    if: github.event_name == 'pull_request'
    needs: check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable

      - name: Run benchmarks
        run: cargo bench -- --output-format bencher | tee bench.txt

      - name: Compare with baseline
        uses: benchmark-action/github-action-benchmark@v1
        with:
          tool: 'cargo'
          output-file-path: bench.txt
          github-token: ${{ secrets.GITHUB_TOKEN }}
          alert-threshold: '115%'
          comment-on-alert: true

Pipeline execution flow:

                    ┌─────────┐
                    │  check  │  ← clippy + fmt + cargo check (2 min)
                    └────┬────┘
           ┌─────────┬──┴──┬──────────┬──────────┐
           ▼         ▼     ▼          ▼          ▼
       ┌──────┐  ┌──────┐ ┌────────┐ ┌──────┐ ┌──────┐
       │ test │  │cross │ │coverage│ │ miri │ │bench │
       │ (2×) │  │ (2×) │ │        │ │      │ │(PR)  │
       └──────┘  └──────┘ └────────┘ └──────┘ └──────┘
         3 min    8 min     8 min     12 min    5 min

Total wall-clock: ~14 min (parallel after check gate)

CI Caching Strategies

Swatinem/rust-cache@v2 is the standard Rust CI cache action. It caches ~/.cargo and target/ between runs, but large workspaces need tuning:

# Basic (what we use above)
- uses: Swatinem/rust-cache@v2

# Tuned for a large workspace:
- uses: Swatinem/rust-cache@v2
  with:
    # Separate caches per job — prevents test artifacts bloating build cache
    prefix-key: "v1-rust"
    key: ${{ matrix.os }}-${{ matrix.target || 'default' }}
    # Only save cache on main branch (PRs read but don't write)
    save-if: ${{ github.ref == 'refs/heads/main' }}
    # Cache Cargo registry + git checkouts + target dir
    cache-targets: true
    cache-all-crates: true

Cache invalidation gotchas:

ProblemFix
Cache grows unbounded (>5 GB)Set prefix-key: "v2-rust" to force fresh cache
Different features pollute cacheUse key: ${{ hashFiles('**/Cargo.lock') }}
PR cache overwrites mainSet save-if: ${{ github.ref == 'refs/heads/main' }}
Cross-compilation targets bloatUse separate key per target triple

Sharing cache between jobs:

The check job saves the cache; downstream jobs (test, cross, coverage) read it. With save-if on main only, PR runs get the benefit of cached dependencies without writing stale caches.

Measured impact on large-scale workspace: Cold build ~4 min → cached build ~45 sec. The cache action alone saves ~25 min of CI time per pipeline run (across all parallel jobs).

Makefile.toml with cargo-make

cargo-make provides a portable task runner that works across platforms (unlike make/Makefile):

# Install
cargo install cargo-make
# Makefile.toml — at workspace root

[config]
default_to_workspace = false

# ─── Developer workflows ───

[tasks.dev]
description = "Full local verification (same checks as CI)"
dependencies = ["check", "test", "clippy", "fmt-check"]

[tasks.check]
command = "cargo"
args = ["check", "--workspace", "--all-targets"]

[tasks.test]
command = "cargo"
args = ["test", "--workspace"]

[tasks.clippy]
command = "cargo"
args = ["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"]

[tasks.fmt]
command = "cargo"
args = ["fmt", "--all"]

[tasks.fmt-check]
command = "cargo"
args = ["fmt", "--all", "--", "--check"]

# ─── Coverage ───

[tasks.coverage]
description = "Generate HTML coverage report"
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--workspace", "--html", "--open"]

[tasks.coverage-ci]
description = "Generate LCOV for CI upload"
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--workspace", "--lcov", "--output-path", "lcov.info"]

# ─── Benchmarks ───

[tasks.bench]
description = "Run all benchmarks"
command = "cargo"
args = ["bench"]

# ─── Cross-compilation ───

[tasks.build-musl]
description = "Build static binary (musl)"
command = "cargo"
args = ["build", "--release", "--target", "x86_64-unknown-linux-musl"]

[tasks.build-arm]
description = "Build for aarch64 (requires cross)"
command = "cross"
args = ["build", "--release", "--target", "aarch64-unknown-linux-gnu"]

[tasks.build-all]
description = "Build for all deployment targets"
dependencies = ["build-musl", "build-arm"]

# ─── Safety verification ───

[tasks.miri]
description = "Run Miri on all tests"
toolchain = "nightly"
command = "cargo"
args = ["miri", "test", "--workspace"]

[tasks.audit]
description = "Check for known vulnerabilities"
install_crate = "cargo-audit"
command = "cargo"
args = ["audit"]

# ─── Release ───

[tasks.release-dry]
description = "Preview what cargo-release would do"
install_crate = "cargo-release"
command = "cargo"
args = ["release", "--workspace", "--dry-run"]

Usage:

# Equivalent of CI pipeline, locally
cargo make dev

# Generate and view coverage
cargo make coverage

# Build for all targets
cargo make build-all

# Run safety checks
cargo make miri

# Check for vulnerabilities
cargo make audit

Pre-Commit Hooks: Custom Scripts and cargo-husky

Catch issues before they reach CI. The recommended approach is a custom git hook — it’s simple, transparent, and has no external dependencies:

#!/bin/sh
# .githooks/pre-commit

set -e

echo "=== Pre-commit checks ==="

# Fast checks first
echo "→ cargo fmt --check"
cargo fmt --all -- --check

echo "→ cargo check"
cargo check --workspace --all-targets

echo "→ cargo clippy"
cargo clippy --workspace --all-targets -- -D warnings

echo "→ cargo test (lib only, fast)"
cargo test --workspace --lib

echo "=== All checks passed ==="
# Install the hook
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit

Alternative: cargo-husky (auto-installs hooks via build script):

⚠️ Note: cargo-husky has not been updated since 2022. It still works but is effectively unmaintained. Consider the custom hook approach above for new projects.

cargo install cargo-husky
# Cargo.toml — add to dev-dependencies of root crate
[dev-dependencies]
cargo-husky = { version = "1", default-features = false, features = [
    "precommit-hook",
    "run-cargo-check",
    "run-cargo-clippy",
    "run-cargo-fmt",
    "run-cargo-test",
] }

Release Workflow: cargo-release and cargo-dist

cargo-release — automates version bumping, tagging, and publishing:

# Install
cargo install cargo-release
# release.toml — at workspace root
[workspace]
consolidate-commits = true
pre-release-commit-message = "chore: release {{version}}"
tag-message = "v{{version}}"
tag-name = "v{{version}}"

# Don't publish internal crates
[[package]]
name = "core_lib"
release = false

[[package]]
name = "diag_framework"
release = false

# Only publish the main binary
[[package]]
name = "diag_tool"
release = true
# Preview release
cargo release patch --dry-run

# Execute release (bumps version, commits, tags, optionally publishes)
cargo release patch --execute
# 0.1.0 → 0.1.1

cargo release minor --execute
# 0.1.1 → 0.2.0

cargo-dist — generates downloadable release binaries for GitHub Releases:

# Install
cargo install cargo-dist

# Initialize (creates CI workflow + metadata)
cargo dist init

# Preview what would be built
cargo dist plan

# Generate the release (usually done by CI on tag push)
cargo dist build
# Cargo.toml additions from `cargo dist init`
[workspace.metadata.dist]
cargo-dist-version = "0.28.0"
ci = "github"
targets = [
    "x86_64-unknown-linux-gnu",
    "x86_64-unknown-linux-musl",
    "aarch64-unknown-linux-gnu",
    "x86_64-pc-windows-msvc",
]
install-path = "CARGO_HOME"

This generates a GitHub Actions workflow that, on tag push:

  1. Builds the binary for all target platforms
  2. Creates a GitHub Release with downloadable .tar.gz / .zip archives
  3. Generates shell/PowerShell installer scripts
  4. Publishes to crates.io (if configured)

Try It Yourself — Capstone Exercise

This exercise ties together every chapter. You will build a complete engineering pipeline for a fresh Rust workspace:

  1. Create a new workspace with two crates: a library (core_lib) and a binary (cli). Add a build.rs that embeds the git hash and build timestamp using SOURCE_DATE_EPOCH (ch01).

  2. Set up cross-compilation for x86_64-unknown-linux-musl and aarch64-unknown-linux-gnu. Verify both targets build with cargo zigbuild or cross (ch02).

  3. Add a benchmark using Criterion or Divan for a function in core_lib. Run it locally and record a baseline (ch03).

  4. Measure code coverage with cargo llvm-cov. Set a minimum threshold of 80% and verify it passes (ch04).

  5. Run cargo +nightly careful test and cargo miri test. Add a test that exercises unsafe code if you have any (ch05).

  6. Configure cargo-deny with a deny.toml that bans openssl and enforces MIT/Apache-2.0 licensing (ch06).

  7. Optimize the release profile with lto = "thin", strip = true, and codegen-units = 1. Measure binary size before/after with cargo bloat (ch07).

  8. Add cargo hack --each-feature verification. Create a feature flag for an optional dependency and ensure it compiles alone (ch09).

  9. Write the GitHub Actions workflow (this chapter) with all 6 stages. Add Swatinem/rust-cache@v2 with save-if tuning.

Success criteria: Push to GitHub → all CI stages green → cargo dist plan shows your release targets. You now have a production-grade Rust pipeline.

CI Pipeline Architecture

flowchart LR
    subgraph "Stage 1 — Fast Feedback < 2 min"
        CHECK["cargo check\ncargo clippy\ncargo fmt"]
    end

    subgraph "Stage 2 — Tests < 5 min"
        TEST["cargo nextest\ncargo test --doc"]
    end

    subgraph "Stage 3 — Coverage"
        COV["cargo llvm-cov\nfail-under 80%"]
    end

    subgraph "Stage 4 — Security"
        SEC["cargo audit\ncargo deny check"]
    end

    subgraph "Stage 5 — Cross-Build"
        CROSS["musl static\naarch64 + x86_64"]
    end

    subgraph "Stage 6 — Release (tag only)"
        REL["cargo dist\nGitHub Release"]
    end

    CHECK --> TEST --> COV --> SEC --> CROSS --> REL

    style CHECK fill:#91e5a3,color:#000
    style TEST fill:#91e5a3,color:#000
    style COV fill:#e3f2fd,color:#000
    style SEC fill:#ffd43b,color:#000
    style CROSS fill:#e3f2fd,color:#000
    style REL fill:#b39ddb,color:#000

Key Takeaways

  • Structure CI as parallel stages: fast checks first, expensive jobs behind gates
  • Swatinem/rust-cache@v2 with save-if: ${{ github.ref == 'refs/heads/main' }} prevents PR cache thrashing
  • Run Miri and heavier sanitizers on a nightly schedule: trigger, not on every push
  • Makefile.toml (cargo make) bundles multi-tool workflows into a single command for local dev
  • cargo-dist automates cross-platform release builds — stop writing platform matrix YAML by hand

Tricks from the Trenches 🟡

What you’ll learn:

  • Battle-tested patterns that don’t fit neatly into one chapter
  • Common pitfalls and their fixes — from CI flake to binary bloat
  • Quick-win techniques you can apply to any Rust project today

Cross-references: Every chapter in this book — these tricks cut across all topics

This chapter collects engineering patterns that come up repeatedly in production Rust codebases. Each trick is self-contained — read them in any order.


1. The deny(warnings) Trap

Problem: #![deny(warnings)] in source code breaks builds when Clippy adds new lints — your code that compiled yesterday fails today.

Fix: Use CARGO_ENCODED_RUSTFLAGS in CI instead of a source-level attribute:

# CI: treat warnings as errors without touching source
env:
  CARGO_ENCODED_RUSTFLAGS: "-Dwarnings"

Or use [workspace.lints] for finer control:

# Cargo.toml
[workspace.lints.rust]
unsafe_code = "deny"

[workspace.lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "warn", priority = -1 }

See Compile-Time Tools, Workspace Lints for the full pattern.


2. Compile Once, Test Everywhere

Problem: cargo test recompiles when switching between --lib, --doc, and --test because they use different profiles.

Fix: Use cargo nextest for unit/integration tests and run doc-tests separately:

cargo nextest run --workspace        # Fast: parallel, cached
cargo test --workspace --doc         # Doc-tests (nextest can't run these)

See Compile-Time Tools for cargo-nextest setup.


3. Feature Flag Hygiene

Problem: A library crate has default = ["std"] but nobody tests --no-default-features. One day an embedded user reports it doesn’t compile.

Fix: Add cargo-hack to CI:

- name: Feature matrix
  run: |
    cargo hack check --each-feature --no-dev-deps
    cargo check --no-default-features
    cargo check --all-features

See no_std and Feature Verification for the full pattern.


4. The Lock File Debate — Commit or Ignore?

Rule of thumb:

Crate TypeCommit Cargo.lock?Why
Binary / applicationYesReproducible builds
LibraryNo (.gitignore)Let downstream choose versions
Workspace with bothYesBinary wins

Add a CI check to ensure the lock file stays up-to-date:

- name: Check lock file
  run: cargo update --locked  # Fails if Cargo.lock is stale

5. Debug Builds with Optimized Dependencies

Problem: Debug builds are painfully slow because dependencies (especially serde, regex) aren’t optimized.

Fix: Optimize deps in dev profile while keeping your code unoptimized for fast recompilation:

# Cargo.toml
[profile.dev.package."*"]
opt-level = 2  # Optimize all dependencies in dev mode

This slows the first build slightly but makes runtime dramatically faster during development. Particularly impactful for database-backed services and parsers.

See Release Profiles for per-crate profile overrides.


6. CI Cache Thrashing

Problem: Swatinem/rust-cache@v2 saves a new cache on every PR, bloating storage and slowing restore times.

Fix: Only save cache from main, restore from anywhere:

- uses: Swatinem/rust-cache@v2
  with:
    save-if: ${{ github.ref == 'refs/heads/main' }}

For workspaces with multiple binaries, add a shared-key:

- uses: Swatinem/rust-cache@v2
  with:
    shared-key: "ci-${{ matrix.target }}"
    save-if: ${{ github.ref == 'refs/heads/main' }}

See CI/CD Pipeline for the full workflow.


7. RUSTFLAGS vs CARGO_ENCODED_RUSTFLAGS

Problem: RUSTFLAGS="-Dwarnings" applies to everything — including build scripts and proc-macros. A warning in serde_derive’s build.rs fails your CI.

Fix: Use CARGO_ENCODED_RUSTFLAGS which only applies to the top-level crate:

# BAD — breaks on third-party build script warnings
RUSTFLAGS="-Dwarnings" cargo build

# GOOD — only affects your crate
CARGO_ENCODED_RUSTFLAGS="-Dwarnings" cargo build

# ALSO GOOD — workspace lints (Cargo.toml)
[workspace.lints.rust]
warnings = "deny"

8. Reproducible Builds with SOURCE_DATE_EPOCH

Problem: Embedding chrono::Utc::now() in build.rs makes builds non-reproducible — every build produces a different binary hash.

Fix: Honor SOURCE_DATE_EPOCH:

#![allow(unused)]
fn main() {
// build.rs
let timestamp = std::env::var("SOURCE_DATE_EPOCH")
    .ok()
    .and_then(|s| s.parse::<i64>().ok())
    .unwrap_or_else(|| chrono::Utc::now().timestamp());
println!("cargo:rustc-env=BUILD_TIMESTAMP={timestamp}");
}

See Build Scripts for the full build.rs patterns.


9. The cargo tree Deduplication Workflow

Problem: cargo tree --duplicates shows 5 versions of syn and 3 of tokio-util. Compile time is painful.

Fix: Systematic deduplication:

# Step 1: Find duplicates
cargo tree --duplicates

# Step 2: Find who pulls the old version
cargo tree --invert --package [email protected]

# Step 3: Update the culprit
cargo update -p serde_derive  # Might pull in syn 2.x

# Step 4: If no update available, pin in [patch]
# [patch.crates-io]
# old-crate = { git = "...", branch = "syn2-migration" }

# Step 5: Verify
cargo tree --duplicates  # Should be shorter

See Dependency Management for cargo-deny and supply chain security.


10. Pre-Push Smoke Test

Problem: You push, CI takes 10 minutes, fails on a formatting issue.

Fix: Run the fast checks locally before push:

# Makefile.toml (cargo-make)
[tasks.pre-push]
description = "Local smoke test before pushing"
script = '''
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --lib
'''
cargo make pre-push  # < 30 seconds
git push

Or use a git pre-push hook:

#!/bin/sh
# .git/hooks/pre-push
cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings

See CI/CD Pipeline for Makefile.toml patterns.


🏋️ Exercises

🟢 Exercise 1: Apply Three Tricks

Pick three tricks from this chapter and apply them to an existing Rust project. Which had the biggest impact?

Solution

Typical high-impact combination:

  1. [profile.dev.package."*"] opt-level = 2 — Immediate improvement in dev-mode runtime (2-10× faster for parsing-heavy code)

  2. CARGO_ENCODED_RUSTFLAGS — Eliminates false CI failures from third-party warnings

  3. cargo-hack --each-feature — Usually finds at least one broken feature combination in any project with 3+ features

# Apply trick 5:
echo '[profile.dev.package."*"]' >> Cargo.toml
echo 'opt-level = 2' >> Cargo.toml

# Apply trick 7 in CI:
# Replace RUSTFLAGS with CARGO_ENCODED_RUSTFLAGS

# Apply trick 3:
cargo install cargo-hack
cargo hack check --each-feature --no-dev-deps

🟡 Exercise 2: Deduplicate Your Dependency Tree

Run cargo tree --duplicates on a real project. Eliminate at least one duplicate. Measure compile-time before and after.

Solution
# Before
time cargo build --release 2>&1 | tail -1
cargo tree --duplicates | wc -l  # Count duplicate lines

# Find and fix one duplicate
cargo tree --duplicates
cargo tree --invert --package <duplicate-crate>@<old-version>
cargo update -p <parent-crate>

# After
time cargo build --release 2>&1 | tail -1
cargo tree --duplicates | wc -l  # Should be fewer

# Typical result: 5-15% compile time reduction per eliminated
# duplicate (especially for heavy crates like syn, tokio)

Key Takeaways

  • Use CARGO_ENCODED_RUSTFLAGS instead of RUSTFLAGS to avoid breaking third-party build scripts
  • [profile.dev.package."*"] opt-level = 2 is the single highest-impact dev experience trick
  • Cache tuning (save-if on main only) prevents CI cache bloat on active repositories
  • cargo tree --duplicates + cargo update is a free compile-time win — do it monthly
  • Run fast checks locally with cargo make pre-push to avoid CI round-trip waste

Quick Reference Card

Cheat Sheet: Commands at a Glance

# ─── Build Scripts ───
cargo build                          # Compiles build.rs first, then crate
cargo build -vv                      # Verbose — shows build.rs output

# ─── Cross-Compilation ───
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17
cross build --release --target aarch64-unknown-linux-gnu

# ─── Benchmarking ───
cargo bench                          # Run all benchmarks
cargo bench -- parse                 # Run benchmarks matching "parse"
cargo flamegraph -- --args           # Generate flamegraph from binary
perf record -g ./target/release/bin  # Record perf data
perf report                          # View perf data interactively

# ─── Coverage ───
cargo llvm-cov --html                # HTML report
cargo llvm-cov --lcov --output-path lcov.info
cargo llvm-cov --workspace --fail-under-lines 80
cargo tarpaulin --out Html           # Alternative tool

# ─── Safety Verification ───
cargo +nightly miri test             # Run tests under Miri
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test
valgrind --leak-check=full ./target/debug/binary
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# ─── Audit & Supply Chain ───
cargo audit                          # Known vulnerability scan
cargo audit --deny warnings          # Fail CI on any advisory
cargo deny check                     # License + advisory + ban + source checks
cargo deny list                      # List all licenses in dep tree
cargo vet                            # Supply chain trust verification
cargo outdated --workspace           # Find outdated dependencies
cargo semver-checks                  # Detect breaking API changes
cargo geiger                         # Count unsafe in dependency tree

# ─── Binary Optimization ───
cargo bloat --release --crates       # Size contribution per crate
cargo bloat --release -n 20          # 20 largest functions
cargo +nightly udeps --workspace     # Find unused dependencies
cargo machete                        # Fast unused dep detection
cargo expand --lib module::name      # See macro expansions
cargo msrv find                      # Discover minimum Rust version
cargo clippy --fix --workspace --allow-dirty  # Auto-fix lint warnings

# ─── Compile-Time Optimization ───
export RUSTC_WRAPPER=sccache         # Shared compilation cache
sccache --show-stats                 # Cache hit statistics
cargo nextest run                    # Faster test runner
cargo nextest run --retries 2        # Retry flaky tests

# ─── Platform Engineering ───
cargo check --target thumbv7em-none-eabihf   # Verify no_std builds
cargo build --target x86_64-pc-windows-gnu   # Cross-compile to Windows
cargo xwin build --target x86_64-pc-windows-msvc  # MSVC ABI cross-compile
cfg!(target_os = "linux")                    # Compile-time cfg (evaluates to bool)

# ─── Release ───
cargo release patch --dry-run        # Preview release
cargo release patch --execute        # Bump, commit, tag, publish
cargo dist plan                      # Preview distribution artifacts

Decision Table: Which Tool When

GoalToolWhen to Use
Embed git hash / build infobuild.rsBinary needs traceability
Compile C code with Rustcc crate in build.rsFFI to small C libraries
Generate code from schemasprost-build / tonic-buildProtobuf, gRPC, FlatBuffers
Link system librarypkg-config in build.rsOpenSSL, libpci, systemd
Static Linux binary--target x86_64-unknown-linux-muslContainer/cloud deployment
Target old glibccargo-zigbuildRHEL 7, CentOS 7 compatibility
ARM server binarycross or cargo-zigbuildGraviton/Ampere deployment
Statistical benchmarksCriterion.rsPerformance regression detection
Quick perf checkDivanDevelopment-time profiling
Find hot spotscargo flamegraph / perfAfter benchmark identifies slow code
Line/branch coveragecargo-llvm-covCI coverage gates, gap analysis
Quick coverage checkcargo-tarpaulinLocal development
Rust UB detectionMiriPure-Rust unsafe code
C FFI memory safetyValgrind memcheckMixed Rust/C codebases
Data race detectionTSan or MiriConcurrent unsafe code
Buffer overflow detectionASanunsafe pointer arithmetic
Leak detectionValgrind or LSanLong-running services
Local CI equivalentcargo-makeDeveloper workflow automation
Pre-commit checkscargo-husky or git hooksCatch issues before push
Automated releasescargo-release + cargo-distVersion management + distribution
Dependency auditingcargo-audit / cargo-denySupply chain security
License compliancecargo-deny (licenses)Commercial / enterprise projects
Supply chain trustcargo-vetHigh-security environments
Find outdated depscargo-outdatedScheduled maintenance
Detect breaking changescargo-semver-checksLibrary crate publishing
Dependency tree analysiscargo tree --duplicatesDedup and trim dep graph
Binary size analysiscargo-bloatSize-constrained deployments
Find unused depscargo-udeps / cargo-macheteTrim compile time and size
LTO tuninglto = true or "thin"Release binary optimization
Size-optimized binaryopt-level = "z" + strip = trueEmbedded / WASM / containers
Unsafe usage auditcargo-geigerSecurity policy enforcement
Macro debuggingcargo-expandDerive / macro_rules debugging
Faster linkingmold linkerDeveloper inner loop
Compilation cachesccacheCI and local build speed
Faster testscargo-nextestCI and local test speed
MSRV compliancecargo-msrvLibrary publishing
no_std library#![no_std] + default-features = falseEmbedded, UEFI, WASM
Windows cross-compilecargo-xwin / MinGWLinux → Windows builds
Platform abstraction#[cfg] + trait patternMulti-OS codebases
Windows API callswindows-sys / windows crateNative Windows functionality
End-to-end timinghyperfineWhole-binary benchmarks, before/after comparison
Property-based testingproptestEdge case discovery, parser robustness
Snapshot testinginstaLarge structured output verification
Coverage-guided fuzzingcargo-fuzzCrash discovery in parsers
Concurrency model checkingloomLock-free data structures, atomic ordering
Feature combination testingcargo-hackCrates with multiple #[cfg] features
Fast UB checks (near-native)cargo-carefulCI safety gate, lighter than Miri
Auto-rebuild on savecargo-watchDeveloper inner loop, tight feedback
Workspace documentationcargo doc + rustdocAPI discovery, onboarding, doc-link CI
Reproducible builds--locked + SOURCE_DATE_EPOCHRelease integrity verification
CI cache tuningSwatinem/rust-cache@v2Build time reduction (cold → cached)
Workspace lint policy[workspace.lints] in Cargo.tomlConsistent Clippy/compiler lints across all crates
Auto-fix lint warningscargo clippy --fixAutomated cleanup of trivial issues

Further Reading

TopicResource
Cargo build scriptsCargo Book — Build Scripts
Cross-compilationRust Cross-Compilation
cross toolcross-rs/cross
cargo-zigbuildcargo-zigbuild docs
Criterion.rsCriterion User Guide
DivanDivan docs
cargo-llvm-covcargo-llvm-cov
cargo-tarpaulintarpaulin docs
MiriMiri GitHub
Sanitizers in Rustrustc Sanitizer docs
cargo-makecargo-make book
cargo-releasecargo-release docs
cargo-distcargo-dist docs
Profile-guided optimizationRust PGO guide
Flamegraphscargo-flamegraph
cargo-denycargo-deny docs
cargo-vetcargo-vet docs
cargo-auditcargo-audit
cargo-bloatcargo-bloat
cargo-udepscargo-udeps
cargo-geigercargo-geiger
cargo-semver-checkscargo-semver-checks
cargo-nextestnextest docs
sccachesccache
mold linkermold
cargo-msrvcargo-msrv
LTOrustc Codegen Options
Cargo ProfilesCargo Book — Profiles
no_stdRust Embedded Book
windows-sys cratewindows-rs
cargo-xwincargo-xwin docs
cargo-hackcargo-hack
cargo-carefulcargo-careful
cargo-watchcargo-watch
Rust CI cacheSwatinem/rust-cache
Rustdoc bookRustdoc Book
Conditional compilationRust Reference — cfg
Embedded RustAwesome Embedded Rust
hyperfinehyperfine
proptestproptest
instainsta snapshot testing
cargo-fuzzcargo-fuzz
loomloom concurrency testing

Generated as a companion reference — a companion to Rust Patterns and Type-Driven Correctness.

Version 1.3 — Added cargo-hack, cargo-careful, cargo-watch, cargo doc, reproducible builds, CI caching strategies, capstone exercise, and chapter dependency diagram for completeness.