如何根据环境变量条件编译?

解读

国内 Rust 岗位面试中,条件编译是区分“写过 Cargo.toml”与“真正落地过跨平台/多形态交付”的试金石。
面试官想确认三点:

  1. 是否知道 Rust 的条件编译入口是 cfg 系列属性,而非 Makefile 或 bash 脚本;
  2. 能否把“环境变量→cfg 开关→代码裁剪”这条链路讲完整,并指出在 Windows/Linux/macOS 持续集成(CI)下的差异;
  3. 是否理解 build.rs 的“编译时”与“运行时”区别,避免把运行期 env::var 当成条件编译用。
    答得太浅(只说 #[cfg(target_os)]) 会被追问“那业务开关怎么办”;答得太偏(上来就写 syn/quote 宏)会被嫌“过度设计”。用 Cargo 官方机制 + 最小 build.rs 样板是最安全且国内团队可落地的方案。

知识点

  1. 编译期 vs 运行期:条件编译必须在编译前确定,因此只能依赖 build.rs 或 rustc 内置的 cfg 规则;运行期 std::env::var 无法影响代码是否被编入。
  2. Cargo 环境变量约定:CARGO_CFG_、CARGO_FEATURE_ 由 Cargo 自动注入;自定义业务开关需通过 rustc-cfg=KEY 指令从 build.rs 输出到编译器。
  3. cfg 与 cfg_attr:#[cfg] 直接裁剪代码块;#[cfg_attr] 可按条件追加属性(如允许 clippy 跳过某段代码),国内代码审查常要求显式标注。
  4. 国内 CI 场景:GitHub/Gitee Actions、GitLab-CI、Jenkins 均通过 export RUSTFLAGS="--cfg custom_cfg"build.rs 读 env 两种方式注入;后者在 Windows 路径含空格时更稳定。
  5. clippy/fmt 兼容性:条件编译分支若未被默认 feature 启用,CI 里需加 --all-features--cfg custom_cfg 才能保证静态检查全覆盖,否则会被国内质量门禁拦下。

答案

分三步落地,不依赖任何第三方 crate,可在 1.70+ 稳定版通过:

  1. build.rs 中读取环境变量并发出 cfg 指令:
fn main() {
    // 业务开关:ENABLE_SPECIAL
    if std::env::var("ENABLE_SPECIAL").is_ok() {
        println!("cargo:rustc-cfg=special");
    }
    // 触发重新编译的条件
    println!("cargo:rerun-if-env-changed=ENABLE_SPECIAL");
}
  1. src/lib.rs 里使用编译期 cfg:
#[cfg(special)]
pub fn fast_path() -> &'static str {
    "special algorithm"
}

#[cfg(not(special))]
pub fn fast_path() -> &'static str {
    "default algorithm"
}
  1. Cargo.toml 中无需额外配置,CI 脚本里按需注入:
# Linux/macOS
ENABLE_SPECIAL=1 cargo build --release
# Windows PowerShell
$env:ENABLE_SPECIAL="1"; cargo build --release

关键点

  • 必须 rerun-if-env-changed,否则本地切分支后不会重编;
  • 若环境变量值需要解析(如 ENABLE_SPECIAL=verbose),在 build.rs 里用 std::env::var("ENABLE_SPECIAL").as_deref() == Ok("verbose") 再发不同 cfg;
  • 如果开关多,建议统一走 cfg!(special) 宏,避免 #[cfg] 与 if cfg! 混用导致覆盖率误判。

拓展思考

  1. feature 与环境变量混用:国内交付常要求“同一分支、不同客户形态”。此时可在 Cargo.toml 声明 [features] special=[],然后在 build.rs 里 仅当 cfg!(feature = "special") 为真时才读取环境变量做二次裁剪,实现“feature 选入口,环境变量选算法”的两级开关,兼顾 Rust 官方 feature 约定与业务灵活度。
  2. 跨 crate 传递 cfg:若 workspace 中 A 依赖 B,且 B 的算法由环境变量决定,需要在 B 的 build.rs 中 println!("cargo:rustc-cfg=special"),并在 A 的 build.rs 中通过 dep: 前缀探测 B 是否开启 special,再决定是否开启自己的 special,从而避免国内常见的“子 crate 开了、主 crate 没开”的链接期符号缺失问题。
  3. no_std 嵌入式场景:在 .cargo/config.toml 里使用 rustflags = ["--cfg", "board_stm32"] 可直接给整个 workspace 注入,无需 build.rs,节省固件体积;但此时要注意 cargo-clippy 默认不会加该 cfg,需在 CI 中显式 export RUSTFLAGS="--cfg board_stm32" 再跑 clippy,否则会被国内车规/安规审查打回。