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

Build Scripts — build.rs in Depth 🟢
构建脚本:深入理解 build.rs 🟢

What you’ll learn:
本章将学到什么:

  • How build.rs fits into the Cargo build pipeline and when it runs
    build.rs 在 Cargo 构建流程中的位置,以及它到底什么时候运行
  • Five production patterns: compile-time constants, C/C++ compilation, protobuf codegen, pkg-config linking, and feature detection
    五种生产级用法:编译期常量、C/C++ 编译、protobuf 代码生成、pkg-config 链接和 feature 检测
  • 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
交叉阅读: 交叉编译 里会继续用 build.rs 做目标感知构建;no_std 与 feature 会用到这里设置的 cfg 标志;CI/CD 流水线 负责把这些构建脚本放进自动化流程。

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.
每个 Cargo 包都可以在 crate 根目录放一个名为 build.rs 的文件。Cargo 会在编译 crate 本体之前,先把它编译并执行一遍。构建脚本和 Cargo 的通信方式也很朴素,就是往标准输出里打印特定格式的 println! 指令。

What build.rs Is and When It Runs
build.rs 是什么,它何时运行

┌─────────────────────────────────────────────────────────┐
│                    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                                                │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│                    Cargo 构建流水线                      │
│                                                         │
│  1. 解析依赖                                            │
│  2. 下载 crate                                          │
│  3. 编译 build.rs   ← 普通 Rust 程序,运行在 HOST 上     │
│  4. 执行 build.rs   ← stdout 回传 Cargo 指令             │
│  5. 编译 crate 本体 ← 使用第 4 步给出的配置             │
│  6. 链接                                                │
└─────────────────────────────────────────────────────────┘

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.
    build.rs 运行在 host 机器上,不是 target。哪怕最后产物是别的架构,构建脚本也还是在当前开发机上执行。
  • The build script’s scope is limited to its own package. It cannot directly control how other crates compile, unless the package declares links and emits metadata for dependents.
    构建脚本的作用域只限于当前 package。它本身改不了其他 crate 的编译方式,除非 package 用了 links,再通过 metadata 往依赖方传数据。
  • It runs every time Cargo thinks something relevant changed, unless you use cargo::rerun-if-changed or cargo::rerun-if-env-changed to缩小重跑范围。
    如果不主动用 cargo::rerun-if-changedcargo::rerun-if-env-changed 缩小范围,Cargo 很容易在很多构建里重复执行它。
  • It can emit cfg flags, environment variables, linker arguments, and generated file paths for the main crate to consume.
    它可以输出 cfg 标志、环境变量、链接参数,以及生成文件路径,让主 crate 在后续编译中使用。

Note (Rust 1.71+): Since Rust 1.71, Cargo fingerprints the compiled build.rs binary. If the binary itself stays identical, Cargo may skip rerunning it even when timestamps changed. Even so, cargo::rerun-if-changed=build.rs still matters a lot, because without any rerun rule, Cargo treats changes to any file in the package as a reason to rerun the script.
补充说明(Rust 1.71+):从 Rust 1.71 起,Cargo 会给编译出的 build.rs 二进制做指纹检查。如果二进制内容没变,它可能会跳过重跑。但 cargo::rerun-if-changed=build.rs 依然非常重要,因为只要没有显式 rerun 规则,Cargo 就会把 package 里任何文件的变化都当成重跑理由。

The minimal Cargo.toml entry:
最小的 Cargo.toml 写法是这样:

[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
Cargo 指令协议

Your build script communicates with Cargo by printing instructions to stdout. Since Rust 1.77, the preferred prefix is cargo:: instead of the older cargo: form.
构建脚本和 Cargo 的通信方式,就是往 stdout 打指令。从 Rust 1.77 开始,推荐使用 cargo:: 前缀,而不是老的 cargo:

Instruction
指令
Purpose
作用
cargo::rerun-if-changed=PATHOnly re-run build.rs when PATH changes
只有当指定路径变化时才重跑 build.rs。
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 library search path
把路径加入库搜索目录。
cargo::rustc-cfg=KEYSet a #[cfg(KEY)] flag
设置 #[cfg(KEY)] 标志。
cargo::rustc-cfg=KEY="VALUE"Set a #[cfg(KEY = "VALUE")] flag
设置带值的 cfg 标志。
cargo::rustc-env=KEY=VALUESet an env var visible via env!()
设置后续可被 env!() 读取的环境变量。
cargo::rustc-cdylib-link-arg=FLAGPass linker arg to cdylib targets
给 cdylib 目标传链接参数。
cargo::warning=MESSAGEDisplay a warning during compilation
在编译时打印警告。
cargo::metadata=KEY=VALUEStore metadata for dependent crates
给依赖当前包的 crate 传递元数据。
// 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
模式 1:编译期常量

The most common use case is embedding build metadata into the binary, such as git hash, build profile, target triple, or build timestamp.
最常见的用法就是把构建元数据嵌进二进制里,例如 git hash、构建配置、target triple 或构建时间。

// 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 variables that do not require build.rs: CARGO_PKG_NAMECARGO_PKG_VERSIONCARGO_PKG_AUTHORSCARGO_PKG_DESCRIPTIONCARGO_MANIFEST_DIR
Cargo 自带的环境变量 其实已经有不少,像 CARGO_PKG_NAMECARGO_PKG_VERSIONCARGO_PKG_AUTHORSCARGO_PKG_DESCRIPTIONCARGO_MANIFEST_DIR,这些都不需要 build.rs 就能直接用。

Pattern 2: Compiling C/C++ Code with the cc Crate
模式 2:用 cc crate 编译 C/C++

When your Rust crate wraps a C library or needs a small native helper, the cc crate is the standard choice inside build.rs.
如果 Rust crate 需要包一层 C 库,或者本身就要带一点小型原生辅助代码,那 cc 基本就是 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");
}
#![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;
}

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();

    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, add .cpp(true) and the right language standard flag:
如果要编 C++,就再加上 .cpp(true) 和对应的标准参数。

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")
        .compile("vendor_helpers");
}

Pattern 3: Protocol Buffers and Code Generation
模式 3:Protocol Buffers 与代码生成

Build scripts are also perfect for compile-time code generation. A classic example is protobuf generation via prost-build:
构建脚本特别适合做编译期代码生成。最典型的例子就是用 prost-build 生成 protobuf 代码。

[build-dependencies]
prost-build = "0.13"
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() {
pub mod diagnostics {
    include!(concat!(env!("OUT_DIR"), "/diagnostics.rs"));
}

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

OUT_DIR is the Cargo-provided directory meant for generated files. Never write generated Rust source back into src/ during the build.
OUT_DIR 是 Cargo 专门给生成文件准备的目录。构建过程中生成的 Rust 代码别往 src/ 里硬写,老老实实放进 OUT_DIR

Pattern 4: Linking System Libraries with pkg-config
模式 4:用 pkg-config 链接系统库

For system libraries that ship .pc files, the pkg-config crate can probe the system and emit the right link flags.
如果系统库自带 .pc 文件,那 pkg-config 就能帮忙探测环境,并自动吐出合适的链接参数。

[build-dependencies]
pkg-config = "0.3"
fn main() {
    pkg_config::Config::new()
        .atleast_version("3.6.0")
        .probe("libpci")
        .expect("libpci >= 3.6.0 not found — install pciutils-dev");

    if pkg_config::probe_library("libsystemd").is_ok() {
        println!("cargo::rustc-cfg=has_systemd");
    }
}
#![allow(unused)]
fn main() {
#[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();
        unsafe { sd_notify(0, state.as_ptr()) };
    }
}

#[cfg(not(has_systemd))]
mod systemd_notify {
    pub fn notify_ready() {}
}
}

Pattern 5: Feature Detection and Conditional Compilation
模式 5:特性检测与条件编译

Build scripts can inspect the compilation environment and emit cfg flags used by the main crate for conditional code paths.
构建脚本还可以探测当前编译环境,再往主 crate 里塞 cfg 标志,让代码走不同分支。

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();

    if target.starts_with("x86_64") {
        println!("cargo::rustc-cfg=has_x86_64");
    }

    if target.starts_with("aarch64") {
        println!("cargo::rustc-cfg=has_aarch64");
    }

    if target_os == "linux" && std::path::Path::new("/dev/ipmi0").exists() {
        println!("cargo::rustc-cfg=has_ipmi_device");
    }
}

⚠️ Anti-pattern demonstration — the following approach looks tempting but should not be used in production.
⚠️ 反面示范:下面这种写法看着诱人,实际上很坑,生产环境别这么干。

fn main() {
    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() {
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 hardware should usually be detected at runtime, not baked in at build time. Otherwise the binary becomes tied to the build machine’s hardware layout.
⚠️ 这为什么是错的:硬件是否存在,通常应该在运行时检测,而不是在构建时写死。否则产物会莫名其妙地和构建机的硬件环境绑定在一起。

Anti-Patterns and Pitfalls
反模式与常见坑

Anti-Pattern
反模式
Why It’s Bad
为什么糟糕
Fix
修正方式
No rerun-if-changed
不写 rerun-if-changed
build.rs runs on every build
每次构建都重跑,拖慢开发
Always emit at least cargo::rerun-if-changed=build.rs
最少也要写上 build.rs 自己。
Network calls in build.rs
在 build.rs 里打网络
Breaks offline and reproducible builds
离线构建和可复现构建都会出问题
Vendor files or split into a fetch step
把文件预置好,或者把下载挪到单独步骤。
Writing to src/
src/ 写生成代码
Cargo does not expect sources to mutate during build
Cargo 不期待源文件在构建中被改动
Write to OUT_DIR
改写到 OUT_DIR
Heavy computation
在 build.rs 里做重计算
Slows every cargo build
所有构建都跟着变慢
Cache in OUT_DIR and gate reruns
把结果缓存起来,再配合 rerun 规则。
Ignoring cross-compilation
无视交叉编译环境
Raw gcc commands often break on non-native targets
手写 gcc 命令很容易在跨平台时炸
Prefer cc crate
优先用 cc crate。
Panicking without context
直接 unwrap() 爆掉
Error message is opaque
报错又臭又短,看不明白
Use .expect("...") or cargo::warning=
给出明确上下文。

Application: Embedding Build Metadata
应用场景:嵌入构建元数据

The project currently uses env!("CARGO_PKG_VERSION") for version reporting. A build.rs would let it report richer metadata such as git hash, build epoch, and target triple.
当前工程已经用 env!("CARGO_PKG_VERSION") 输出版本号了。如果再补一个 build.rs,就能把 git hash、构建时间戳、target triple 这些信息一起嵌进去。

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

    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() {
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"),
};
}

Key insight from the project: having zero build.rs files across a large codebase is often a good sign. If the project is pure Rust, does not wrap C code, does not generate code, and does not need system library probing, then not having build scripts means the architecture stayed clean.
结合当前工程的一点观察:一个大代码库里完全没有 build.rs,很多时候反而是好事。如果项目是纯 Rust、没有 C 依赖、没有代码生成、也不需要探测系统库,那没有构建脚本就说明架构相当干净。

Try It Yourself
动手试一试

  1. Embed git metadata: Create a build.rs that emits APP_GIT_HASH and APP_BUILD_EPOCH, consume them with env!() in main.rs, and verify the hash changes after a commit.
    1. 嵌入 git 元数据:写一个 build.rs 输出 APP_GIT_HASHAPP_BUILD_EPOCH,在 main.rs 里用 env!() 读取,并验证提交后 hash 会变化。

  2. Probe a system library: Use pkg-config to probe libz, emit cargo::rustc-cfg=has_zlib when found, and let main.rs print whether zlib is available.
    2. 探测系统库:用 pkg-config 探测 libz,找到时输出 has_zlib,再让 main.rs 在构建后打印 zlib 是否可用。

  3. Trigger a build failure intentionally: Remove rerun-if-changed and observe how often build.rs reruns during cargo build and cargo test, then add it back and compare.
    3. 故意制造一次不合理重跑:先删掉 rerun-if-changed,看看 cargo buildcargo testbuild.rs 会重跑多少次,再把它加回来做对比。

Reproducible Builds
可复现构建

Chapter 1 encourages embedding timestamps and git hashes into binaries for traceability. But that directly conflicts with reproducible builds, where the same source should produce the same binary.
这一章前面提倡把时间戳和 git hash 嵌进二进制,方便追踪来源。但这件事和“可复现构建”天然是有冲突的,因为后者要求同一份源码产出完全一致的二进制。

The tension:
两者的拉扯关系:

Goal
目标
Achievement
得到什么
Cost
代价
Traceability
可追踪性
APP_BUILD_EPOCH in binary
二进制里带构建信息
Every build is unique
每次构建都不一样
Reproducibility
可复现性
Same source → same output
同源码得同产物
No live build timestamp
实时构建信息会受限制

Practical resolution:
更务实的处理方式:

# 1. Always use --locked in CI
cargo build --release --locked

# 2. For reproducible builds, set SOURCE_DATE_EPOCH
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) cargo build --release --locked
#![allow(unused)]
fn main() {
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: respect SOURCE_DATE_EPOCH in build.rs. That way, release builds can stay reproducible while local development builds still keep convenient live timestamps.
更好的实践:在 build.rs 里优先读取 SOURCE_DATE_EPOCH。这样发布构建还能维持可复现,本地开发构建也仍然能保留实时时间戳。

Build Pipeline Decision Diagram
构建脚本决策图

flowchart TD
    START["Need compile-time work?<br/>需要编译期处理吗?"] -->|No<br/>不需要| SKIP["No build.rs needed<br/>不用 build.rs"]
    START -->|Yes<br/>需要| WHAT{"What kind?<br/>属于哪类需求?"}
    
    WHAT -->|"Embed metadata<br/>嵌元数据"| P1["Pattern 1<br/>Compile-Time Constants"]
    WHAT -->|"Compile C/C++<br/>编 C/C++"| P2["Pattern 2<br/>cc crate"]
    WHAT -->|"Code generation<br/>代码生成"| P3["Pattern 3<br/>prost-build / tonic-build"]
    WHAT -->|"Link system lib<br/>链接系统库"| P4["Pattern 4<br/>pkg-config"]
    WHAT -->|"Detect features<br/>检测 feature"| P5["Pattern 5<br/>cfg flags"]
    
    P1 --> RERUN["Always emit<br/>cargo::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
🟢 练习 1:版本戳

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.
创建一个最小 crate,用 build.rs 把当前 git hash 和 build profile 写进环境变量,再在 main() 里打印出来,并验证 debug 与 release 构建结果不同。

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());
}
fn main() {
    println!("{} v{} (git:{} profile:{})",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION"),
        env!("GIT_HASH"),
        env!("BUILD_PROFILE"),
    );
}
cargo run
cargo run --release

🟡 Exercise 2: Conditional System Library
🟡 练习 2:条件系统库探测

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.
写一个 build.rs,用 pkg-config 探测 libzlibpci。哪个找到就发哪个 cfg 标志,然后在 main.rs 里打印构建时探测到了哪些库。

Solution 参考答案
[build-dependencies]
pkg-config = "0.3"
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");
    }
}
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
    build.rs 运行在 host 上,想避免莫名其妙地重跑,就一定要写 cargo::rerun-if-changed
  • Use the cc crate, not raw gcc commands, for C/C++ compilation
    编译 C/C++ 时优先用 cc crate,别自己手搓 gcc 命令。
  • Write generated files to OUT_DIR, never to src/
    生成文件放进 OUT_DIR,别污染 src/
  • Prefer runtime detection over build-time detection for optional hardware
    可选硬件能力更适合运行时探测,而不是构建时写死。
  • Use SOURCE_DATE_EPOCH when you need reproducible builds with embedded timestamps
    既想嵌时间戳,又想保留可复现构建,就去用 SOURCE_DATE_EPOCH