首页
/ 在编译器外部依赖中使用 `cfg(bootstrap)`:打破 Rust 编译器循环依赖的完整指南

在编译器外部依赖中使用 `cfg(bootstrap)`:打破 Rust 编译器循环依赖的完整指南

2026-09-09 22:33:40作者:宗隆裙

导读

Rust 编译器在自举(bootstrap)构建过程中会遇到一类特殊的循环依赖问题:编译器需要某个外部 crate 的新版本才能构建,而该 crate 的新版本又依赖更新的编译器能力。本文基于 rustc-dev-guide 的官方文档,系统讲解如何用 #[cfg(bootstrap)] 条件编译在编译器与外部依赖之间"拆解"这一循环,包括警告消除配置、bootstrap 构建系统如何注入该 cfg,以及标准化的"升级四步舞"(update dance)流程。读完本文,你将掌握在 compiler-builtins 这类编译器外部依赖中安全使用 #[cfg(bootstrap)] 的完整方法论,并理解 bootstrap 构建系统底层是如何把 --cfg=bootstrap 传递到各构建阶段的。

问题背景:编译器与外部 crate 的循环依赖

Rust 编译器会使用一些外部 crate(例如 compiler-builtins,其源码位于 library/compiler-builtins),这些 crate 与编译器之间可能形成循环依赖:

  • 方向一:编译器为了构建新版本,需要该 crate 提供更新后的实现;
  • 方向二:该 crate 为了适配新版本,又需要更新后的编译器才能编译。

也就是说:编译器需要更新的 crate 才能构建,但 crate 需要更新的编译器才能构建

#[cfg(bootstrap)] 正是用来打破这一循环的工具:在自举阶段(即使用 stage 0 的 bootstrap 编译器构建时),带有 #[cfg(bootstrap)] 的代码会被编译;否则(正常构建阶段),带有 #[cfg(not(bootstrap))] 的代码会被编译。这样同一个 crate 源码可以在不同构建阶段呈现不同的行为,允许"新旧行为并存"地过渡。

在外部 crate 中启用 #[cfg(bootstrap)]

bootstrap 并不是 Rust 内置的标准 cfg 条件名。因此,在普通外部 crate 中直接使用 #[cfg(bootstrap)] 通常会触发 unexpected_cfgs 警告,输出类似如下:

warning: unexpected `cfg` condition name: `bootstrap`
 --> src/main.rs:1:7
  |
1 | #[cfg(bootstrap)]
  |       ^^^^^^^^^
  |
  = help: expected names are: `docsrs`, `feature`, and `test` and 31 more
  = help: consider using a Cargo feature instead
  = help: or consider adding in `Cargo.toml` the `check-cfg` lint config for the lint:
           [lints.rust]
           unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bootstrap)'] }
  = help: or consider adding `println!("cargo::rustc-check-cfg=cfg(bootstrap)");` to the top of the `build.rs`
  = note: `#[warn(unexpected_cfgs)]` on by default

方式一:在 Cargo.toml 中声明 check-cfg

文档推荐的做法是在 crate 的 Cargo.toml 中添加如下配置来消除该警告:

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bootstrap)'] }

添加后,#[cfg(bootstrap)] 就可以像在编译器内部一样使用:当使用 bootstrap 编译器构建时,标注 #[cfg(bootstrap)] 的代码被编译;否则编译 #[cfg(not(bootstrap))] 的代码。

方式二:在 build.rs 中输出 check-cfg

警告提示中还提供了另一种等效做法——在 crate 的 build.rs 顶部添加:

println!("cargo::rustc-check-cfg=cfg(bootstrap)");

两种方式的作用相同:告知 rustc 该 cfg 名称是"被预期"的,从而抑制 unexpected_cfgs 警告。从源码实现看,bootstrap 构建系统内部也正是通过等价机制(向 rustc 传入 --check-cfg 声明)来声明这一 cfg 的,详见下文"bootstrap 如何注入 cfg(bootstrap)"一节。

在编译器内部的典型用法

#[cfg(bootstrap)] 在 rustc 自身源码中已被广泛使用,用于在构建 stage 0 编译器时启用旧行为、在后续阶段启用新行为。以下是当前仓库中的几个真实例子:

这些例子体现了该机制的通用模式:同一份源码,通过 cfg(bootstrap) 在不同构建阶段选择不同代码路径,从而让旧编译器(stage 0)与新编译器(stage 1+)都能成功编译同一份代码。

bootstrap 如何注入 --cfg=bootstrap

理解外部依赖中 #[cfg(bootstrap)] 为何生效,需要弄清 bootstrap 构建系统在底层是如何把这个 cfg 传给 rustc 的。当前仓库的源码清晰地展示了这一过程:

stage 0 时统一追加 --cfg=bootstrap

src/bootstrap/src/core/builder/cargo.rs#L90-L98propagate_rustflag_envs 中,bootstrap 会区分构建阶段:当 build_compiler_stage == 0 时,除继承 RUSTFLAGS_BOOTSTRAP 外,还会显式追加 --cfg=bootstrap

fn propagate_rustflag_envs(&mut self, build_compiler_stage: u32) {
    self.propagate_cargo_env("RUSTFLAGS");
    if build_compiler_stage != 0 {
        self.env("RUSTFLAGS_NOT_BOOTSTRAP");
    } else {
        self.env("RUSTFLAGS_BOOTSTRAP");
        self.arg("--cfg=bootstrap");
    }
}

同理,src/bootstrap/src/core/builder/cargo.rs#L875-L877 在构建 stage 0 编译器时向 host flags 追加 --cfg=bootstrapsrc/bootstrap/src/bin/rustdoc.rs#L58-L63 中 rustdoc shim 也在 stage 0 时显式设置 --cfg=bootstrap(注释指出这是因为 Cargo 不会把 RUSTDOCFLAGS 传给 proc-macro crate,因此必须显式设置)。

声明该 cfg 以避免警告

bootstrap 自身也通过 --check-cfg 机制声明 bootstrap 是合法 cfg。在 src/bootstrap/src/core/builder/cargo.rs#L22-L35 中定义了 EXTRA_CHECK_CFGS 常量:

const EXTRA_CHECK_CFGS: &[(Option<Mode>, &str, Option<&[&'static str]>)] = &[
    (Some(Mode::Rustc), "bootstrap", None),
    (Some(Mode::Codegen), "bootstrap", None),
    (Some(Mode::ToolRustcPrivate), "bootstrap", None),
    (Some(Mode::ToolStd), "bootstrap", None),
    // ...
];

该列表在 src/bootstrap/src/core/builder/cargo.rs#L857-L870 中被转换为实际的 --check-cfg 参数传入 rustc;且当 cfg 名为 bootstrap 时还会额外加入 host flags(因为 Cargo 不会把 RUSTFLAGS 传给 proc-macro crate,需要显式声明该 cfg 合法以避免警告)。这正是外部 crate 需要在自身 Cargo.toml 中声明 check-cfg 的同一原理。

rtstartup 对象的编译

另一个佐证位于 src/bootstrap/src/core/build_steps/compile.rs#L930-L945:bootstrap 在编译 library/rtstartup 下的 rsbegin.rs / rsend.rs 时,使用初始编译器并仅在非 local_rebuild 模式下追加 --cfg bootstraplocal_rebuild 编译器已具备 stage1 特性,无需此 cfg)。

升级四步舞(The update dance):完整流程

文档以"#[naked] 属性变为 unsafe 属性"这一真实改动为例(该改动与 compiler-builtins crate 形成循环依赖),给出了一套可复用的四步升级流程。

Step 1:编译器先接受新旧两种行为

第一步在编译器仓库侧完成:修改编译器,使其同时接受旧行为与新行为。在示例中,做法是"禁用(放宽)一个原本是错误(error)的检查",让新旧两种写法在编译器内部并存。这是整个流程的前提——只有编译器先"兼容两态",外部 crate 才可能在同一份源码中同时表达新旧行为。

Step 2:在外部 crate 中用 #[cfg(bootstrap)]] 区分行为

第二步在外部 crate 仓库侧完成:在 crate 源码中,用 #[cfg(bootstrap)] 分支保留旧行为,用 #[cfg(not(bootstrap))] 分支启用新行为。由于此时编译器已能同时接受两种行为,无论构建者使用的是旧的 bootstrap 编译器还是新的编译器,都能编译成功:

// 旧编译器(bootstrap 阶段)使用旧行为
#[cfg(bootstrap)]
fn f() { /* 旧实现 */ }

// 新编译器使用新行为
#[cfg(not(bootstrap))]
fn f() { /* 新实现 */ }

前提是 crate 已按上文方式在 Cargo.toml 中声明 [lints.rust] unexpected_cfgs 配置,否则会收到 unexpected_cfgs 警告。

Step 3:更新编译器所依赖的 crate 版本

第三步回到编译器仓库:把编译器依赖的外部 crate 更新到包含 Step 2 改动的新版本。对 compiler-builtins 而言,这一步表现为版本号升级(version bump);对其他外部依赖,也可能是 git 子模块(submodule)更新。更新后,编译器的依赖树中已经同时包含新旧行为的兼容实现。

Step 4:从编译器移除旧行为

第四步再次回到编译器仓库:既然编译器现在依赖的是已更新的 crate,它就可以删除 Step 1 中保留的旧行为支持了(例如恢复为严格的错误检查)。至此,循环依赖被彻底打破:

  • 旧编译器 + 旧 crate:通过 #[cfg(bootstrap)] 走旧路径,正常工作;
  • 新编译器 + 新 crate:通过 #[cfg(not(bootstrap))] 走新路径,正常工作;
  • 编译器中的兼容层已被清理,代码恢复整洁。

使用建议与注意事项

  • 这是过渡机制,不是长期特性#[cfg(bootstrap)] 的意义在于让"新编译器"与"新 crate"之间的一次性升级平滑落地。当 step 4 完成后,相关 cfg(bootstrap) 分支通常会被随后的清理工作移除(正如 rustc_hir_typeck// cfg(bootstrap): change the if let to an unwrap. 这类注释所预示的后续收尾)。
  • 警告消除必须显式配置:在外部 crate 中,bootstrap 不是默认合法 cfg,必须通过 [lints.rust] unexpected_cfgsbuild.rs 中的 cargo::rustc-check-cfg 显式声明,否则构建会带着 unexpected_cfgs 警告运行。
  • crate 内新旧行为要保持可并存:Step 2 中 #[cfg(bootstrap)]#[cfg(not(bootstrap))] 两段代码必须能同时通过编译检查(因为 cargo check 等操作可能同时评估两条路径),这是保证升级流程安全的前提。
  • 阶段语义明确:根据 src/bootstrap/src/core/builder/cargo.rs#L90-L98 的源码逻辑,bootstrap cfg 仅在 build_compiler_stage == 0(即使用 stage 0 bootstrap 编译器)时被注入;stage 1 及以上构建不携带该 cfg,因此 #[cfg(not(bootstrap))] 分支会在后续阶段生效。

总结

#[cfg(bootstrap)] 是 Rust 编译器自举构建体系中用于打破"编译器 ↔ 外部 crate"循环依赖的关键机制。它的用法并不复杂:在外部 crate 的 Cargo.toml 中声明 check-cfg 消除警告,然后用 #[cfg(bootstrap)] / #[cfg(not(bootstrap))] 双分支并存新旧行为;配合"编译器先兼容两态 → crate 双分支实现 → 升级编译器依赖版本 → 移除旧行为"的四步升级流程,即可在不破坏任何一方构建的前提下完成跨版本演进。从 src/bootstrap/src/core/builder/cargo.rspropagate_rustflag_envssrc/bootstrap/src/bin/rustdoc.rs 的 rustdoc shim,bootstrap 构建系统在 stage 0 阶段统一注入 --cfg=bootstrap 并在 EXTRA_CHECK_CFGS 中声明其合法性,这为所有参与自举的 crate(包括 compiler-builtins)提供了统一、可靠的行为切换基础。

热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.15 K
2.77 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.36 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
929
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.03 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
534
603
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.47 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
398
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.05 K
529