Rust nightly 升级后依赖触发 ICE:锁定 nightly 日期与 vendor 打补丁的做法
这两天 Rust 圈的 nightly 又"炸"了一个常用依赖:编译器一升级,以前能编过的项目突然在编译中途直接 ICE,连报错都来不及给。不少人的第一反应是降回 stable 或者怀疑自己的代码写错了。
我没跟着降级。我干了另一件事——去翻了一个开源手绘白板(boundless,无界白板)的构建修复记录,看它怎么在三天内把这颗雷排掉:先锁 nightly 日期止血,再给依赖打 vendor 补丁根治。
看完我可以负责任地告诉你三件事。
第一,ICE 不是你的代码错了,是新版编译器加了一道硬检查,撞上了依赖的老写法。
第二,修它分三步:锁一个已知能编的 nightly 日期、把带日期的 nightly 走正确的 CI 输入、再把依赖的声明改成编译器规定的写法。
第三,最阴的不是 ICE 本身,是缓存把它藏了起来——CI 一直绿,只是因为它根本没重新编译。
1. 先看病:ICE 是什么,雷埋在哪
先给大家科普一个概念「ICE」:Internal Compiler Error,编译器内部错误。普通编译错误是"你的代码有问题,编译器告诉你哪错了";ICE 是"编译器自己崩了",它连错都报不明白,只会甩一句"请向 rustc 报告这个问题"。你的代码没错,错的是编译器遇到了它处理不了的输入。
翻译成人话:ICE 是编译器的 bug,不是你的 bug——但修编译器轮不到你,绕过去才是你的活。
boundless 的现场是这样的。这个项目必须用 nightly 构建,因为它依赖的 gpui-pre 用了不稳定的 std::hint::cold_path,仓库根目录的 rust-toolchain.toml 写死的就是 channel = "nightly"。而它间接依赖的 pathfinder_simd 0.5.6,在 src/arm/mod.rs 里用 extern "C" 加 link_name = "llvm.*" 的方式声明 NEON 内建函数——这是一种"借 C 的壳调 LLVM 内建函数"的老写法。
2026 年 11 月 10 日之后的 nightly,把这种写法升级成了硬 ABI 检查:单态化(monomorphize,泛型实例化那一步)时直接 ICE。而且只在 macOS/arm64 的 release 构建触发,debug 构建和别的平台都好好的。上游当时还没有修复版。
学完这一节你能带走的第一个方法:看到 ICE 先别改自己的代码,先看 ICE 堆栈指向谁——指向依赖的声明、指向标准库、指向宏展开,修法完全不同。 这次堆栈指向的是依赖里的一组 NEON 声明,所以修的方向是"绕开这组声明",而不是"改业务代码"。
2. 第一步:先锁一个已知能编的 nightly 日期
定位到病根之后,第一步不是立刻修依赖,而是先让 CI 恢复绿色——锁 nightly。
证据很扎实:v0.4.11 在 2026-11-10 的 nightly 上构建成功,所以就锁 nightly-2026-11-10。改动在两个 workflow 文件里,.github/workflows/ci.yml 和 .github/workflows/release.yml,用的是 dtolnay/rust-toolchain 这个 action:
- name: Install Rust toolchain
# Same pin as release.yml: newer nightlies ICE on pathfinder_simd's
# aarch64 NEON declarations. Keep the two in sync.
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-11-10
注意两个细节。第一,ci.yml 和 release.yml 必须保持同步,注释里明确写了"Keep the two in sync"——只锁一个,另一个下次发版时还会炸。第二,注释里要写清解锁条件:release.yml 的注释写的是"上游修复后可解锁"(bump the pin once pathfinder_simd or rustc resolves it)。锁版本不写解锁条件,三个月后没人敢动它,锁就变成了永久债务。
翻译成人话:锁 nightly 不是认输,是先止血——先让构建恢复,再慢慢治根。 而且要锁就锁到"最后一个已知好的日期",不要锁到"大概能用的某个月"。
3. 第二步:带日期的 nightly 不能写在 @ref 后面
这一步是作者亲手踩的坑,值得单独讲——因为它太容易复现了。
第一次锁版本时,作者写的是:
uses: dtolnay/rust-toolchain@nightly-2026-11-10
结果 Set up job 直接失败。原因很简单:@ 后面是 action 自身的版本 ref,这个 action 只有 @master、@v1、@stable、@nightly 这几个 ref,根本不存在 @nightly-2026-11-10 这个 ref。带日期的 nightly 必须走 toolchain: 输入:
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-11-10
翻译成人话:@ 后面是 action 的版本,toolchain: 里面才是工具链的版本——两个"版本"不在同一个位置。 这是 GitHub Actions 里最常见的概念混淆之一:uses: 的 @ref 永远指 action 仓库的 git ref,跟你要装的工具链版本没关系。
学完这一节你能带走的第二个方法:CI 里"装工具链"和"用工具链"是两件事——前者看 action 文档的 ref 列表,后者看 rustup 的 toolchain 语法。 报错信息里出现"not a valid ref"或者 Set up job 秒失败,先检查是不是把工具链版本写进了 action ref。
4. 第三步:vendor 打补丁,把声明改成编译器规定的写法
锁 nightly 只是止血,根治要动依赖本身。但上游没发修复版,怎么办?答案是 vendor 打补丁:把依赖的源码拷进自己仓库,改掉有问题的那一行,再用 [patch.crates-io] 指过去。
boundless 的做法分两步。先在根目录 Cargo.toml 里加:
[patch.crates-io]
# pathfinder_simd 0.5.6 declares NEON LLVM intrinsics via extern "C" +
# link_name = "llvm.*" — nightlies ≥ 2026-11 ICE on it during
# monomorphization (aarch64 only; see vendor/pathfinder_simd for the one-line
# fix). Drop this patch when upstream ships a fixed version.
pathfinder_simd = { path = "vendor/pathfinder_simd" }
再改 vendored 源码里真正的那一行:vendor/pathfinder_simd/src/arm/mod.rs 中,把那组 NEON 内建函数的声明从 extern "C" 改成 extern "llvm-intrinsic",link_name 保持不变:
// Vendored patch: `extern "C"` + `link_name = "llvm.*"` declarations ICE on
// nightlies ≥ 2026-11 (hard monomorphization ABI check, aarch64 only) — rustc
// prescribes the `extern "llvm-intrinsic"` ABI instead.
extern "llvm-intrinsic" {
#[link_name = "llvm.fabs.v2f32"]
fn fabs_v2f32(a: float32x2_t) -> float32x2_t;
#[link_name = "llvm.aarch64.neon.frecpe.v2f32"]
fn vrecpe_v2f32(a: float32x2_t) -> float32x2_t;
// ……其余 fabs/floor/ceil/round/sqrt 的 v2/v4 版本同理
}
翻译成人话:以前是"借 C 的壳调 LLVM 内建函数",现在直接告诉编译器"这就是 LLVM 内建函数"——壳不要了,编译器的硬检查也就没东西可查了。 这是 rustc 官方给出的处方,不是作者自己发明的写法,所以改动虽小,依据很硬。
验证也做了:本地 rustc aarch64-apple-darwin 上 typecheck 通过。注释里同样写清了移除条件:“Drop this vendor patch once upstream releases a fix”——补丁是临时的,注释就是它的"保质期标签"。
学完这一节你能带走的第三个方法:上游没修的依赖问题,用 [patch.crates-io] + vendor 目录自己修——但补丁要满足三个条件:改动最小(这次只改声明的 ABI 一行语义)、依据是官方处方而非自创写法、注释写清移除条件。 三个条件缺一个,vendor 补丁就会从"临时绷带"变成"永久技术债"。
5. 为什么 v0.4.11 侥幸通过:缓存把雷藏了起来
整个事件里最值得讲的不是修法,是"为什么它藏了这么久才炸"。
v0.4.11 在 11-10 之后的 nightly 上也构建成功过——不是因为它没踩雷,是因为 rust-cache 命中了旧产物,根本没重新编译 pathfinder_simd。直到一次 gpui 生态升级(gpui-pre 0.3.12 / gpui-component 0.9.1)改了缓存键,CI 被迫冷编译,雷才炸出来。
翻译成人话:CI 一直绿,不代表代码能编——可能只是它很久没真正编过了。 缓存是构建速度的朋友,却是构建正确性的盲区:它跳过的那部分,恰恰是你最需要验证的那部分。
学完这一节你能带走的第四个方法:给 CI 加一个定期的冷编译(清缓存跑一次全量构建),或者至少在升级大依赖后手动触发一次无缓存构建。 依赖升级改缓存键是常事,冷编译就是那次升级的"体检"——有病早发现,别等到发版那天才发现。
6. 为什么这样设计:三层防御
回头看这三步——锁 nightly、修正 CI 输入、vendor 补丁——其实是三层防御,各管一层:
第一层是时间止血(锁 nightly): 让 CI 立刻恢复绿色,代价是暂时放弃新 nightly 的修复与功能。它的有效期写在注释里,上游修复就解锁。
第二层是配置正确(toolchain 输入): 工具链版本必须走正确的输入通道,写错位置连安装那步都过不去。这一层修的是"人对工具的误解",不是代码。
第三层是根治(vendor 补丁): 把有问题的声明改成编译器规定的写法。补丁进了仓库,本地浮动 nightly 也不怕这颗雷了——锁 nightly 从此只是"防新 nightly 引入别的硬检查"的安全网,而不是唯一的救命绳。
三层的顺序也有讲究:先止血(几分钟恢复 CI),再修配置(几分钟改 workflow),最后根治(vendor 一个 crate、改一行语义、做一次验证)。顺序反过来——先花半天研究 vendor 补丁,CI 在此期间一直红着——就是拿构建的可用性给完美主义交税。
给开发者的建议
- 看到 ICE 先读堆栈定责任方。 指向依赖声明、标准库还是宏展开,修法完全不同;先定责再动手,别改自己的业务代码背编译器的锅。
- 锁 nightly 要锁到"最后一个已知好的日期",并写清解锁条件。 注释里写明"上游修复后可解锁",否则三个月后没人敢动这行配置。
- CI 和 release 的工具链版本必须同步。 只锁一处,另一处发版时还会炸;注释里写明"Keep the two in sync"。
- 带日期的 nightly 走
toolchain:输入,别写进@ref。@后面是 action 自身的版本,跟工具链版本没关系;Set up job 秒失败先查这个。 - vendor 补丁三原则:改动最小、依据官方处方、注释写清移除条件。 缺一个,临时绷带就会变成永久技术债。
- 定期冷编译一次。 缓存命中跳过的编译是最危险的"绿色"——依赖大升级后,手动触发一次无缓存构建,等于给构建做一次体检。
说到底,nightly 升级炸依赖不是灾难,是 nightly 的日常。灾难的是没有分层:没有锁版本的止血、没有 vendor 补丁的根治、没有冷编译的体检,同一颗雷能炸你三次。boundless 这次只被炸了一次,因为它三层都补上了——而且每层都写明了什么时候可以拆掉。
项目开源在 GitHub:tsingliuwin/boundless,觉得有用欢迎点个 star。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/tsingwin/article/details/170761814



