Skip to content

Rust build.rs

1. 什么是 build.rs

build.rs 是 Cargo 在编译主代码之前自动运行的构建脚本,可以:

  • 检测目标平台、OS、CPU 架构
  • 生成 Rust 源代码(proto、模板、绑定等)
  • 链接本地 C/C++ 库
  • 编译 C/C++ 源文件并静态链接
  • 向编译器传递条件编译标志
  • 嵌入版本号、构建时间等元信息

执行顺序

cargo build
    │
    ▼
编译 build.rs(作为独立二进制)
    │
    ▼
运行 build.rs 输出的 cargo: 指令
    │
    ▼
编译主代码(src/)

文件位置

my-project/
├── Cargo.toml
├── build.rs        ← 默认位置,无需配置
└── src/
    └── main.rs

自定义路径(Cargo.toml):

[package]
name    = "my-project"
version = "0.1.0"
build   = "scripts/build.rs"   # 自定义 build.rs 路径

禁用构建脚本:

[package]
build = false

2. 基础用法

最小示例

// build.rs
fn main() {
    println!("cargo:warning=构建脚本正在运行");
}

build.rs 的依赖声明

build.rs 的依赖在 [build-dependencies] 中声明,与主代码的依赖完全独立

[build-dependencies]
cc          = "1"           # 编译 C/C++ 代码
tonic-build = "0.12"        # 生成 gRPC 代码
prost-build = "0.13"        # 生成 protobuf 代码
bindgen     = "0.69"        # 生成 C 绑定

[dependencies]
# 主代码依赖,build.rs 不能使用这些
tokio = { version = "1", features = ["full"] }

3. Cargo 指令(cargo:)

build.rs 通过向标准输出打印特定格式的指令与 Cargo 通信。

完整指令列表

// 链接动态库(默认)
println!("cargo:rustc-link-lib=ssl");          // -lssl
println!("cargo:rustc-link-lib=dylib=ssl");    // 显式动态库

// 链接静态库
println!("cargo:rustc-link-lib=static=mylib"); // -Bstatic -lmylib

// 链接框架(macOS)
println!("cargo:rustc-link-lib=framework=CoreFoundation");
// 添加库搜索路径
println!("cargo:rustc-link-search=/usr/local/lib");
println!("cargo:rustc-link-search=native=/path/to/libs");

// 路径类型
// native   → 本地库(默认)
// framework → macOS framework
// all      → 所有类型
println!("cargo:rustc-link-search=all=/opt/custom/lib");

cargo:rustc-cfg — 条件编译标志

// 设置 cfg 标志,主代码中用 #[cfg(feature_x)] 判断
println!("cargo:rustc-cfg=feature_x");
println!("cargo:rustc-cfg=has_avx2");

// 带值的 cfg
println!("cargo:rustc-cfg=database=\"postgres\"");

cargo:rustc-env — 设置编译期环境变量

// 主代码中用 env!("MY_VAR") 读取
println!("cargo:rustc-env=MY_VAR=hello");
println!("cargo:rustc-env=BUILD_TIME=2026-05-14T10:00:00Z");
println!("cargo:rustc-env=GIT_HASH=abc123def");

cargo:rerun-if-changed — 控制重新构建

// 只有这些文件变化时才重新运行 build.rs
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=proto/user.proto");
println!("cargo:rerun-if-changed=src/ffi/mylib.h");

// 监听整个目录(目录内任何文件变化都触发)
println!("cargo:rerun-if-changed=proto/");

cargo:rerun-if-env-changed — 环境变量变化时重新构建

// 指定环境变量变化时重新运行
println!("cargo:rerun-if-env-changed=CC");
println!("cargo:rerun-if-env-changed=MY_LIB_PATH");

cargo:warning — 输出警告

println!("cargo:warning=正在使用实验性功能");
println!("cargo:warning=找不到系统库,将使用内置版本");

cargo:rustc-flags — 传递链接器标志

println!("cargo:rustc-flags=-L /usr/local/lib -l ssl");
// 传递给链接器的参数
println!("cargo:rustc-link-arg=-Wl,--gc-sections");
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/local/lib");

cargo:metadata — 自定义元数据(供父包脚本读取)

// 作为依赖库时,父包的 build.rs 可以通过 DEP_xxx_yyy 读取
println!("cargo:version=1.2.3");
println!("cargo:include=/path/to/headers");

4. 环境变量

Cargo 运行 build.rs 时会注入大量环境变量。

构建目标信息

fn main() {
    // 目标三元组,如 "x86_64-unknown-linux-gnu"
    let target = std::env::var("TARGET").unwrap();

    // 目标架构,如 "x86_64" / "aarch64" / "wasm32"
    let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();

    // 目标操作系统,如 "linux" / "macos" / "windows" / "none"
    let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();

    // 目标环境,如 "gnu" / "msvc" / "musl"
    let env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap();

    // 目标指针宽度,如 "64" / "32"
    let pointer_width = std::env::var("CARGO_CFG_TARGET_POINTER_WIDTH").unwrap();

    // 目标字节序,如 "little" / "big"
    let endian = std::env::var("CARGO_CFG_TARGET_ENDIAN").unwrap();

    // 目标操作系统系列,如 "unix" / "windows"
    let family = std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap_or_default();

    println!("目标:{target},架构:{arch},OS:{os}");
}

构建模式信息

fn main() {
    // "debug" 或 "release"
    let profile = std::env::var("PROFILE").unwrap();

    // 优化级别:"0"/"1"/"2"/"3"/"s"/"z"
    let opt_level = std::env::var("OPT_LEVEL").unwrap();

    // 是否开启 debug 信息:"0"/"1"/"2"
    let debug = std::env::var("DEBUG").unwrap();

    // 并行编译的 job 数
    let num_jobs = std::env::var("NUM_JOBS").unwrap();

    if profile == "release" {
        println!("cargo:rustc-cfg=release_build");
    }
}

路径信息

fn main() {
    // Cargo 输出目录(生成文件放这里)
    let out_dir = std::env::var("OUT_DIR").unwrap();

    // 包的 Cargo.toml 所在目录
    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();

    // 工作区根目录
    let workspace_dir = std::env::var("CARGO_WORKSPACE_DIR")
        .unwrap_or_else(|_| manifest_dir.clone());

    println!("OUT_DIR: {out_dir}");
    println!("MANIFEST_DIR: {manifest_dir}");
}

包信息

fn main() {
    let name    = std::env::var("CARGO_PKG_NAME").unwrap();
    let version = std::env::var("CARGO_PKG_VERSION").unwrap();
    let authors = std::env::var("CARGO_PKG_AUTHORS").unwrap();
    let desc    = std::env::var("CARGO_PKG_DESCRIPTION").unwrap();
    let repo    = std::env::var("CARGO_PKG_REPOSITORY").unwrap();

    // 版本各部分
    let major = std::env::var("CARGO_PKG_VERSION_MAJOR").unwrap();
    let minor = std::env::var("CARGO_PKG_VERSION_MINOR").unwrap();
    let patch = std::env::var("CARGO_PKG_VERSION_PATCH").unwrap();

    println!("cargo:rustc-env=PKG_VERSION={version}");
}

Feature 标志

fn main() {
    // 检查是否启用了某个 feature
    // 格式:CARGO_FEATURE_<大写FEATURE名>(连字符替换为下划线)
    if std::env::var("CARGO_FEATURE_ASYNC").is_ok() {
        println!("cargo:rustc-cfg=has_async");
    }

    if std::env::var("CARGO_FEATURE_TLS").is_ok() {
        // 启用了 tls feature,链接 openssl
        println!("cargo:rustc-link-lib=ssl");
        println!("cargo:rustc-link-lib=crypto");
    }
}

依赖包元数据

fn main() {
    // 读取名为 "mylib" 的依赖包在其 build.rs 中发布的元数据
    // 对应:println!("cargo:version=1.2.3"); 在 mylib 的 build.rs 中
    if let Ok(ver) = std::env::var("DEP_MYLIB_VERSION") {
        println!("mylib 版本: {ver}");
    }
    if let Ok(inc) = std::env::var("DEP_MYLIB_INCLUDE") {
        println!("mylib 头文件路径: {inc}");
    }
}

5. 条件编译

根据平台生成不同代码

fn main() {
    let os   = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
    let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();

    match os.as_str() {
        "linux" => {
            println!("cargo:rustc-cfg=platform_linux");
            println!("cargo:rustc-link-lib=pthread");
        }
        "macos" => {
            println!("cargo:rustc-cfg=platform_macos");
            println!("cargo:rustc-link-lib=framework=CoreFoundation");
            println!("cargo:rustc-link-lib=framework=Security");
        }
        "windows" => {
            println!("cargo:rustc-cfg=platform_windows");
            println!("cargo:rustc-link-lib=ws2_32");
            println!("cargo:rustc-link-lib=userenv");
        }
        _ => {}
    }

    if arch == "x86_64" || arch == "aarch64" {
        println!("cargo:rustc-cfg=has_64bit");
    }
}

在主代码中使用

// src/main.rs

#[cfg(platform_linux)]
fn get_memory_info() -> u64 {
    // Linux 实现
    0
}

#[cfg(platform_macos)]
fn get_memory_info() -> u64 {
    // macOS 实现
    0
}

#[cfg(platform_windows)]
fn get_memory_info() -> u64 {
    // Windows 实现
    0
}

// 使用带值的 cfg
#[cfg(database = "postgres")]
fn connect() { /* postgres 实现 */ }

#[cfg(database = "sqlite")]
fn connect() { /* sqlite 实现 */ }

检测系统能力

use std::process::Command;

fn main() {
    // 检测是否支持 AVX2 指令集(x86_64)
    let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
    if arch == "x86_64" {
        if is_avx2_available() {
            println!("cargo:rustc-cfg=has_avx2");
        }
    }

    // 检测系统库是否存在
    if pkg_config_exists("openssl") {
        println!("cargo:rustc-cfg=has_openssl");
    }
}

fn is_avx2_available() -> bool {
    // 编译一个小程序测试
    let test = "
        #include <immintrin.h>
        int main() { __m256i x = _mm256_setzero_si256(); return 0; }
    ";
    compile_test_c(test)
}

fn pkg_config_exists(lib: &str) -> bool {
    Command::new("pkg-config")
        .args(["--exists", lib])
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

fn compile_test_c(_src: &str) -> bool {
    // 简化示例
    true
}

6. 链接本地 C/C++ 库

链接已安装的系统库

fn main() {
    // 链接系统中的 libssl 和 libcrypto
    println!("cargo:rustc-link-lib=ssl");
    println!("cargo:rustc-link-lib=crypto");

    // 如果库不在标准路径,需要指定搜索路径
    if let Ok(lib_dir) = std::env::var("OPENSSL_LIB_DIR") {
        println!("cargo:rustc-link-search=native={lib_dir}");
    }

    // 重新构建时机控制
    println!("cargo:rerun-if-env-changed=OPENSSL_LIB_DIR");
}

使用 pkg-config 自动探测

[build-dependencies]
pkg-config = "0.3"
fn main() {
    // 自动找到 openssl 的库路径和版本
    pkg_config::Config::new()
        .atleast_version("1.1")
        .probe("openssl")
        .unwrap();
    // 自动输出:
    // cargo:rustc-link-lib=ssl
    // cargo:rustc-link-lib=crypto
    // cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu
    // cargo:include=/usr/include/openssl
}

链接静态库

use std::path::PathBuf;

fn main() {
    let lib_dir = PathBuf::from(
        std::env::var("MYLIB_DIR").unwrap_or_else(|_| "/usr/local".to_string())
    );

    // 静态链接
    println!(
        "cargo:rustc-link-search=native={}",
        lib_dir.join("lib").display()
    );
    println!("cargo:rustc-link-lib=static=mylib");

    // 静态库一般需要同时链接它依赖的库
    println!("cargo:rustc-link-lib=pthread");
    println!("cargo:rustc-link-lib=m");      // libm(数学库)
}

7. 用 cc crate 编译 C/C++ 代码

cc 是最常用的编译 C/C++ 源文件的工具。

[build-dependencies]
cc = "1"

编译单个 C 文件

fn main() {
    cc::Build::new()
        .file("src/ffi/helper.c")
        .compile("helper");
    // 自动输出:cargo:rustc-link-lib=static=helper
}

编译多个文件并配置

fn main() {
    cc::Build::new()
        // 源文件
        .files([
            "src/ffi/util.c",
            "src/ffi/codec.c",
            "src/ffi/parser.c",
        ])
        // 头文件搜索路径
        .include("src/ffi/include")
        .include("/usr/local/include")
        // 编译器标志
        .flag("-O2")
        .flag("-fPIC")
        .flag_if_supported("-Wno-unused-parameter")
        // 预处理宏定义
        .define("MY_FEATURE", "1")
        .define("VERSION", "\"1.2.3\"")
        // 是否显示编译警告
        .warnings(false)
        // 优化级别(继承 Cargo profile)
        .opt_level(2)
        .compile("mylib");
}

编译 C++ 代码

fn main() {
    cc::Build::new()
        .cpp(true)                  // 启用 C++ 模式
        .file("src/ffi/engine.cpp")
        .file("src/ffi/renderer.cpp")
        .flag_if_supported("-std=c++17")
        .flag_if_supported("-stdlib=libc++")  // macOS
        .compile("engine");

    // C++ 标准库链接
    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
    if target_os == "macos" {
        println!("cargo:rustc-link-lib=c++");
    } else {
        println!("cargo:rustc-link-lib=stdc++");
    }
}

平台差异处理

fn main() {
    let mut build = cc::Build::new();
    build.file("src/ffi/common.c");

    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
    match target_os.as_str() {
        "linux" => {
            build.file("src/ffi/platform_linux.c");
            build.define("PLATFORM_LINUX", None);
        }
        "macos" => {
            build.file("src/ffi/platform_macos.c");
            build.define("PLATFORM_MACOS", None);
        }
        "windows" => {
            build.file("src/ffi/platform_win.c");
            build.define("PLATFORM_WINDOWS", None);
        }
        _ => {}
    }

    build.compile("platform_lib");
}

8. 代码生成

生成 Rust 源文件

生成的文件必须放在 OUT_DIR 中,通过 include! 宏引入:

// build.rs
use std::fs;
use std::path::PathBuf;

fn main() {
    let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());

    // 生成一个 Rust 文件
    let code = r#"
pub const VERSION: &str = "1.2.3";
pub const BUILD_TIME: &str = "2026-05-14T10:00:00Z";

pub fn platform() -> &'static str {
    env!("CARGO_CFG_TARGET_OS")
}
"#;

    fs::write(out_dir.join("generated.rs"), code).unwrap();

    println!("cargo:rerun-if-changed=build.rs");
}
// src/main.rs
include!(concat!(env!("OUT_DIR"), "/generated.rs"));

fn main() {
    println!("版本: {}", VERSION);
    println!("构建时间: {}", BUILD_TIME);
}

嵌入 Git 信息

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

fn main() {
    // 获取 git commit hash
    let git_hash = Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .output()
        .ok()
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .unwrap_or_else(|| "unknown".to_string());

    // 获取 git branch
    let git_branch = Command::new("git")
        .args(["rev-parse", "--abbrev-ref", "HEAD"])
        .output()
        .ok()
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .unwrap_or_else(|| "unknown".to_string());

    println!("cargo:rustc-env=GIT_HASH={}", git_hash.trim());
    println!("cargo:rustc-env=GIT_BRANCH={}", git_branch.trim());

    // git 状态变化时重新构建
    println!("cargo:rerun-if-changed=.git/HEAD");
    println!("cargo:rerun-if-changed=.git/refs");
}
// src/main.rs
fn main() {
    println!("Git Hash:   {}", env!("GIT_HASH"));
    println!("Git Branch: {}", env!("GIT_BRANCH"));
}

从模板生成代码

// build.rs
use std::fs;
use std::path::PathBuf;

fn main() {
    let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
    let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());

    // 读取错误码配置文件(如 JSON/TOML)
    let codes_path = manifest_dir.join("src/error_codes.json");
    let codes_json = fs::read_to_string(&codes_path).unwrap();
    let codes: Vec<serde_json::Value> = serde_json::from_str(&codes_json).unwrap();

    // 生成错误码枚举
    let mut code = String::from("pub enum ErrorCode {\n");
    for item in &codes {
        let name  = item["name"].as_str().unwrap();
        let value = item["code"].as_u64().unwrap();
        code.push_str(&format!("    {} = {},\n", name, value));
    }
    code.push_str("}\n");

    fs::write(out_dir.join("error_codes.rs"), code).unwrap();

    println!("cargo:rerun-if-changed=src/error_codes.json");
    println!("cargo:rerun-if-changed=build.rs");
}

生成 FFI 绑定(bindgen)

[build-dependencies]
bindgen = "0.69"
// build.rs
use std::path::PathBuf;

fn main() {
    let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());

    // 从 C 头文件自动生成 Rust FFI 绑定
    bindgen::Builder::default()
        .header("src/ffi/mylib.h")
        // 只生成指定前缀的绑定
        .allowlist_function("mylib_.*")
        .allowlist_type("MyLib.*")
        .allowlist_var("MYLIB_.*")
        // 生成的类型派生这些 trait
        .derive_debug(true)
        .derive_default(true)
        // 输出文件
        .generate()
        .expect("bindgen 生成失败")
        .write_to_file(out_dir.join("bindings.rs"))
        .expect("写入绑定文件失败");

    println!("cargo:rerun-if-changed=src/ffi/mylib.h");
    println!("cargo:rustc-link-lib=mylib");
}
// src/ffi.rs
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

9. 生成 Proto 文件(tonic / prost)

基础用法

[build-dependencies]
tonic-build = "0.12"
// build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/hello.proto")?;
    Ok(())
}

完整配置

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::configure()
        // 是否生成服务端代码
        .build_server(true)
        // 是否生成客户端代码
        .build_client(true)
        // 为所有生成的消息添加额外 derive
        .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
        // 只为特定消息添加
        .type_attribute(
            "user.v1.User",
            "#[derive(Hash, Eq)]",
        )
        // 为字段添加属性
        .field_attribute(
            "user.v1.User.password",
            "#[serde(skip_serializing)]",
        )
        // 生成文件描述符(供 gRPC 反射使用)
        .file_descriptor_set_path(
            std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap())
                .join("proto_descriptor.bin"),
        )
        // 指定编译的 proto 文件和 include 路径
        .compile_protos(
            &[
                "proto/user/v1/user.proto",
                "proto/order/v1/order.proto",
            ],
            &["proto/"],
        )?;

    // 告诉 Cargo:proto 文件变化时重新构建
    println!("cargo:rerun-if-changed=proto/");

    Ok(())
}

引入生成的代码

// src/proto/mod.rs

// 方式一:tonic::include_proto! 宏(推荐)
pub mod user {
    tonic::include_proto!("user.v1");
}

// 方式二:手动 include!
pub mod order {
    include!(concat!(env!("OUT_DIR"), "/order.v1.rs"));
}

// 引入文件描述符(用于 gRPC 反射)
pub const FILE_DESCRIPTOR_SET: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/proto_descriptor.bin"));

10. 读取外部文件与目录

正确构造文件路径

use std::path::PathBuf;
use std::fs;

fn main() {
    let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
    let out_dir      = PathBuf::from(std::env::var("OUT_DIR").unwrap());

    // 读取项目根目录的文件
    let config_path = manifest_dir.join("config/default.toml");
    let config = fs::read_to_string(&config_path)
        .expect("读取配置文件失败");

    // 写入生成文件到 OUT_DIR
    let out_path = out_dir.join("config_embed.rs");
    let code = format!(
        r#"pub const DEFAULT_CONFIG: &str = r#"{}"#;"#,
        config
    );
    fs::write(out_path, code).unwrap();

    println!("cargo:rerun-if-changed={}", config_path.display());
}

遍历目录生成代码

use std::fs;
use std::path::PathBuf;

fn main() {
    let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
    let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());

    let sql_dir = manifest_dir.join("migrations");
    let mut migrations = vec![];

    // 遍历 SQL 文件
    for entry in fs::read_dir(&sql_dir).unwrap() {
        let entry = entry.unwrap();
        let path = entry.path();
        if path.extension().and_then(|e| e.to_str()) == Some("sql") {
            let name = path.file_stem().unwrap().to_str().unwrap().to_string();
            let sql  = fs::read_to_string(&path).unwrap();
            migrations.push((name, sql));
            println!("cargo:rerun-if-changed={}", path.display());
        }
    }

    // 按文件名排序
    migrations.sort_by(|a, b| a.0.cmp(&b.0));

    // 生成迁移数组
    let mut code = String::from(
        "pub static MIGRATIONS: &[(&str, &str)] = &[\n"
    );
    for (name, sql) in &migrations {
        code.push_str(&format!(
            "    ({:?}, {:?}),\n",
            name, sql
        ));
    }
    code.push_str("];\n");

    fs::write(out_dir.join("migrations.rs"), code).unwrap();
    println!("cargo:rerun-if-changed={}", sql_dir.display());
}

11. 缓存与重新构建控制

默认行为

如果 build.rs 没有输出任何 rerun-if-changed 指令,Cargo 会在以下情况重新运行: - 任何源文件变化 - 任何环境变量变化 - 每次 cargo build(某些版本)

这会导致频繁的不必要重新构建。

精确控制重新构建

fn main() {
    // ✓ 明确声明监听的文件
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed=proto/user.proto");
    println!("cargo:rerun-if-changed=src/ffi/mylib.h");
    println!("cargo:rerun-if-changed=src/ffi/helper.c");

    // ✓ 监听环境变量
    println!("cargo:rerun-if-env-changed=OPENSSL_DIR");
    println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");

    // 其他构建逻辑...
}

只监听目录(目录内任何文件变化都触发)

fn main() {
    // 监听整个 proto 目录
    println!("cargo:rerun-if-changed=proto/");
    println!("cargo:rerun-if-changed=migrations/");

    // 注意:目录路径末尾加 / 监听的是目录内容
    // 不加 / 监听的是目录本身的元数据
}

缓存生成结果(避免重复生成)

use std::path::PathBuf;
use std::fs;

fn main() {
    let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
    let output  = out_dir.join("generated.rs");

    // 检查输入文件的修改时间
    let input_modified = fs::metadata("src/schema.json")
        .and_then(|m| m.modified())
        .ok();
    let output_modified = fs::metadata(&output)
        .and_then(|m| m.modified())
        .ok();

    let needs_regen = match (input_modified, output_modified) {
        (Some(i), Some(o)) => i > o,
        _ => true,
    };

    if needs_regen {
        generate_code(&output);
    }

    println!("cargo:rerun-if-changed=src/schema.json");
    println!("cargo:rerun-if-changed=build.rs");
}

fn generate_code(output: &PathBuf) {
    // 生成代码逻辑...
    fs::write(output, "// 生成的代码").unwrap();
}

12. 与主代码通信

通过环境变量传递(编译期常量)

// build.rs
fn main() {
    println!("cargo:rustc-env=APP_VERSION=1.2.3");
    println!("cargo:rustc-env=BUILD_PROFILE={}", 
        std::env::var("PROFILE").unwrap());
}
// src/main.rs
const APP_VERSION: &str = env!("APP_VERSION");
const BUILD_PROFILE: &str = env!("BUILD_PROFILE");

fn main() {
    println!("版本: {}", APP_VERSION);
    // 运行时也可用 option_env! 处理可能不存在的变量
    let git_hash = option_env!("GIT_HASH").unwrap_or("unknown");
    println!("Git: {}", git_hash);
}

通过 include! 传递代码

// build.rs
use std::path::PathBuf;

fn main() {
    let out = PathBuf::from(std::env::var("OUT_DIR").unwrap());

    std::fs::write(
        out.join("constants.rs"),
        format!(
            "pub const MAX_CONNECTIONS: usize = {};\n\
             pub const SERVER_NAME: &str = {:?};\n",
            num_cpus(),
            hostname(),
        ),
    ).unwrap();
}

fn num_cpus() -> usize {
    std::thread::available_parallelism()
        .map(|n| n.get())
        .unwrap_or(4)
}

fn hostname() -> String {
    std::env::var("HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
}
// src/config.rs
include!(concat!(env!("OUT_DIR"), "/constants.rs"));

通过 cfg 传递布尔标志

// build.rs
fn main() {
    // 检测是否有 AVX2 支持
    println!("cargo:rustc-cfg=simd_avx2");

    // 检测操作系统特性
    let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
    if os == "linux" {
        println!("cargo:rustc-cfg=has_epoll");
    }
}
// src/io.rs
#[cfg(has_epoll)]
mod epoll_impl { /* ... */ }

#[cfg(not(has_epoll))]
mod fallback_impl { /* ... */ }

#[cfg(simd_avx2)]
fn fast_hash(data: &[u8]) -> u64 {
    // AVX2 加速版本
    todo!()
}

13. 实战示例

示例一:完整的 gRPC 服务构建脚本

// build.rs
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let out_dir = PathBuf::from(std::env::var("OUT_DIR")?);

    // 生成 proto 代码 + 文件描述符
    tonic_build::configure()
        .build_server(true)
        .build_client(true)
        .file_descriptor_set_path(out_dir.join("grpc_descriptor.bin"))
        .type_attribute(
            ".",
            "#[derive(serde::Serialize, serde::Deserialize)]",
        )
        .compile_protos(
            &[
                "proto/user/v1/user_service.proto",
                "proto/health/v1/health.proto",
            ],
            &["proto/"],
        )?;

    // 嵌入 git 信息
    embed_git_info();

    // 嵌入构建时间
    let build_time = chrono::Utc::now().to_rfc3339();
    println!("cargo:rustc-env=BUILD_TIME={build_time}");

    // 监听文件变化
    println!("cargo:rerun-if-changed=proto/");
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed=.git/HEAD");

    Ok(())
}

fn embed_git_info() {
    let hash = run_git(&["rev-parse", "--short", "HEAD"])
        .unwrap_or_else(|| "unknown".to_string());
    let branch = run_git(&["rev-parse", "--abbrev-ref", "HEAD"])
        .unwrap_or_else(|| "unknown".to_string());
    let dirty = run_git(&["status", "--porcelain"])
        .map(|s| !s.trim().is_empty())
        .unwrap_or(false);

    println!("cargo:rustc-env=GIT_HASH={}", hash.trim());
    println!("cargo:rustc-env=GIT_BRANCH={}", branch.trim());
    println!("cargo:rustc-env=GIT_DIRTY={}", dirty);
}

fn run_git(args: &[&str]) -> Option<String> {
    std::process::Command::new("git")
        .args(args)
        .output()
        .ok()
        .filter(|o| o.status.success())
        .and_then(|o| String::from_utf8(o.stdout).ok())
}

示例二:混合 C 代码的 Rust 库

// build.rs
fn main() {
    let target_os   = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
    let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();

    // 编译核心 C 代码
    let mut build = cc::Build::new();
    build
        .file("native/core.c")
        .file("native/codec.c")
        .include("native/include")
        .define("RUST_FFI", "1");

    // 平台特定代码
    match target_os.as_str() {
        "linux" => {
            build.file("native/platform/linux.c");
            println!("cargo:rustc-link-lib=pthread");
            println!("cargo:rustc-link-lib=rt");
        }
        "macos" => {
            build.file("native/platform/macos.c");
            println!("cargo:rustc-link-lib=framework=CoreFoundation");
        }
        "windows" => {
            build.file("native/platform/windows.c");
            println!("cargo:rustc-link-lib=ws2_32");
        }
        _ => {}
    }

    // 架构优化
    if target_arch == "x86_64" {
        build.file("native/arch/x86_64.c");
        build.flag_if_supported("-mavx2");
        println!("cargo:rustc-cfg=has_x86_64_opt");
    } else if target_arch == "aarch64" {
        build.file("native/arch/aarch64.c");
        println!("cargo:rustc-cfg=has_aarch64_opt");
    }

    build.compile("mycore");

    // 生成 FFI 绑定
    let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
    bindgen::Builder::default()
        .header("native/include/mycore.h")
        .allowlist_function("mycore_.*")
        .generate()
        .unwrap()
        .write_to_file(out.join("mycore_bindings.rs"))
        .unwrap();

    println!("cargo:rerun-if-changed=native/");
    println!("cargo:rerun-if-changed=build.rs");
}

示例三:嵌入式开发(no_std)构建脚本

// build.rs(适用于裸机/嵌入式)
use std::path::PathBuf;

fn main() {
    let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());

    // 链接脚本(告诉链接器内存布局)
    println!(
        "cargo:rustc-link-search={}",
        manifest_dir.display()
    );
    println!("cargo:rustc-link-arg=-Tmemory.x");
    println!("cargo:rustc-link-arg=-Tlink.x");

    println!("cargo:rerun-if-changed=memory.x");
    println!("cargo:rerun-if-changed=link.x");
    println!("cargo:rerun-if-changed=build.rs");
}

14. 调试 build.rs

查看 build.rs 的输出

# 显示构建脚本的输出(包括 cargo: 指令和 stderr)
cargo build -vv 2>&1 | grep -A 50 "Running.*build-script"

# 只查看警告
cargo build 2>&1 | grep "warning:"

在 build.rs 中打印调试信息

fn main() {
    // 警告会显示在 cargo build 输出中
    println!("cargo:warning=调试: OUT_DIR = {}",
        std::env::var("OUT_DIR").unwrap_or_default());

    // 普通 eprintln! 输出到 stderr(-vv 模式下可见)
    eprintln!("[build.rs] 正在处理...");
}

查看生成的文件

# 找到 OUT_DIR 路径
cargo build -vv 2>&1 | grep "OUT_DIR"
# 输出类似:OUT_DIR = /path/to/target/debug/build/myapp-xxx/out

# 查看生成的文件
ls target/debug/build/myapp-*/out/

临时禁用 build.rs 缓存强制重新构建

# 删除 build 脚本的 fingerprint,强制重新运行
cargo clean -p mypackage
cargo build

# 或者修改 build.rs 的任意内容(触发重新编译)
touch build.rs && cargo build

常用调试模式

fn main() {
    // 将所有环境变量打印出来(仅调试时使用)
    if std::env::var("BUILD_DEBUG").is_ok() {
        for (key, val) in std::env::vars() {
            if key.starts_with("CARGO") || key.starts_with("TARGET") {
                eprintln!("[build.rs] {key} = {val}");
            }
        }
    }
}
BUILD_DEBUG=1 cargo build -vv

15. 常见陷阱

陷阱 1:路径使用相对路径

// ❌ 相对路径在 build.rs 中不可靠(工作目录不固定)
let content = std::fs::read_to_string("config.toml").unwrap();

// ✓ 使用 CARGO_MANIFEST_DIR 构造绝对路径
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let content = std::fs::read_to_string(
    std::path::Path::new(&manifest).join("config.toml")
).unwrap();

陷阱 2:生成文件放在 src/ 目录

// ❌ 不要将生成文件放在 src/ 或项目其他目录(污染源码树,影响 git)
std::fs::write("src/generated.rs", code).unwrap();

// ✓ 必须放在 OUT_DIR
let out = std::env::var("OUT_DIR").unwrap();
std::fs::write(format!("{out}/generated.rs"), code).unwrap();

陷阱 3:忘记声明 rerun-if-changed

// ❌ 没有声明:每次 cargo build 都会重新运行(很慢)
fn main() {
    tonic_build::compile_protos("proto/hello.proto").unwrap();
}

// ✓ 明确声明监听的文件
fn main() {
    tonic_build::compile_protos("proto/hello.proto").unwrap();
    println!("cargo:rerun-if-changed=proto/hello.proto");
    println!("cargo:rerun-if-changed=build.rs");
}

陷阱 4:build.rs 中使用 [dependencies] 中的 crate

# ❌ build.rs 不能使用 [dependencies] 中的 crate
[dependencies]
serde = "1"

# ✓ build.rs 专用依赖放在 [build-dependencies]
[build-dependencies]
serde = "1"
serde_json = "1"

陷阱 5:panic 导致构建失败时信息不清晰

// ❌ unwrap 在 build.rs 中 panic 时错误信息难以定位
let val = std::env::var("MY_VAR").unwrap();

// ✓ 提供清晰的错误信息
let val = std::env::var("MY_VAR")
    .expect("请设置环境变量 MY_VAR,例如:export MY_VAR=/path/to/lib");

// 或者返回 Result
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let val = std::env::var("MY_VAR")
        .map_err(|_| "缺少环境变量 MY_VAR")?;
    Ok(())
}

陷阱 6:Windows 路径反斜杠问题

// ❌ 在 Windows 上反斜杠可能引起问题
println!("cargo:rustc-link-search=C:\\libs\\mylib");

// ✓ 使用 Path 自动处理路径分隔符
let path = std::path::Path::new("C:\\libs\\mylib");
println!("cargo:rustc-link-search={}", path.display());

// 或者直接用正斜杠(Windows 链接器通常也接受)
println!("cargo:rustc-link-search=native=/usr/local/lib");

快速参考

常用指令速查

// 链接库
println!("cargo:rustc-link-lib=ssl");
println!("cargo:rustc-link-lib=static=mylib");

// 库搜索路径
println!("cargo:rustc-link-search=native=/usr/local/lib");

// 条件编译标志
println!("cargo:rustc-cfg=has_feature_x");

// 编译期环境变量
println!("cargo:rustc-env=MY_KEY=value");

// 重新构建控制
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=MY_ENV");

// 警告输出
println!("cargo:warning=提示信息");

常用环境变量速查

std::env::var("OUT_DIR")              // 生成文件输出目录
std::env::var("CARGO_MANIFEST_DIR")   // Cargo.toml 所在目录
std::env::var("PROFILE")              // "debug" / "release"
std::env::var("TARGET")               // 目标三元组
std::env::var("CARGO_CFG_TARGET_OS")  // 目标 OS
std::env::var("CARGO_CFG_TARGET_ARCH")// 目标架构
std::env::var("CARGO_PKG_VERSION")    // 包版本号
std::env::var("CARGO_FEATURE_<NAME>") // Feature 是否启用