在编译器外部依赖中使用 `cfg(bootstrap)`:打破 Rust 编译器循环依赖的完整指南
导读
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 编译器时启用旧行为、在后续阶段启用新行为。以下是当前仓库中的几个真实例子:
- compiler/rustc_arena/src/lib.rs#L13:
#![cfg_attr(bootstrap, feature(never_type))],仅在 bootstrap 阶段启用never_type特性; - compiler/rustc_ast_ir/src/lib.rs#L10:
#![cfg_attr(feature = "nightly", cfg_attr(bootstrap, feature(never_type)))],在 nightly 特性下进一步区分 bootstrap 阶段; - compiler/rustc_attr_ir/src/attribute_docs.rs#L15:
#[cfg_attr(not(bootstrap), doc(attribute = "rustc_dump_clauses"))],非 bootstrap 阶段才附加文档属性; - compiler/rustc_hir_typeck/src/fn_ctxt/checks.rs#L522:代码注释
// cfg(bootstrap): change the if let to an unwrap.,说明此处代码在旧编译器上采用if let写法,待 bootstrap 阶段结束后可改为unwrap。
这些例子体现了该机制的通用模式:同一份源码,通过 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-L98 的 propagate_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=bootstrap;src/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 bootstrap(local_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_cfgs或build.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 的源码逻辑,
bootstrapcfg 仅在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.rs 的 propagate_rustflag_envs 到 src/bootstrap/src/bin/rustdoc.rs 的 rustdoc shim,bootstrap 构建系统在 stage 0 阶段统一注入 --cfg=bootstrap 并在 EXTRA_CHECK_CFGS 中声明其合法性,这为所有参与自举的 crate(包括 compiler-builtins)提供了统一、可靠的行为切换基础。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K635- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python60
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java131
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java80
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript90
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python290