ChipCode 的编译门禁:exit code 之外还得看什么
“编译通过”是嵌入式开发里最容易被误读的一句话。
make 返回 0,屏幕上没有红色的 error,于是你觉得可以烧了。但返回 0 只证明一件事:编译器按你给的输入走完了流程,没有遇到让它停下来的错误。
它不证明产物是新的,不证明产物是对的,不证明链接进来的东西是你以为的那份。
ChipCode 在构建之后做了一组额外检查,统称“编译门禁”。这篇讲每条检查在拦什么。
检查一:警告不是零
第一道,也是最简单的一道:把警告当信号,不当噪声。
我们不是把警告升级成错误(那样很多第三方库根本编不过),而是把新增警告列出来。
做法是记住上一次构建的警告集合,这次构建完做一次差集。新增的警告会被标红显示。
const prev = loadWarningBaseline(projectId); // Set<string>
const now = parseWarnings(buildOutput); // Set<string>
const added = [...now].filter(w => !prev.has(w));
if (added.length > 0) {
report({
level: "warn", items: added });
}
为什么要做差集而不是全量?因为一个几十万行的工程,警告本来就有几百条。每次都列出来,用户会直接关掉这个面板。
而“新增三条警告”是具体的、可行动的。
差集里最常见的三种:
- 未使用变量(往往是重构时漏删的,逻辑可能还引用着旧值)
- 隐式类型转换(
int到uint8_t截断,在传感器数据上很致命) - 有符号无符号比较(
if (len > sizeof(buf)),sizeof是无符号的,len为负数时永远是 false)
第三种我们真的拦下过 bug:一个判断缓冲区长度的条件,因为类型问题永远不成立,等于没有检查。
检查二:产物是不是新的
这条前面提过,但值得展开。
make 判断“要不要重编”靠的是文件时间戳。时间戳会被这些事搞乱:
- 从压缩包里解出来的文件,时间戳是打包时的
- 跨时区协作,或者系统时间被改过
- 网络盘 / 虚拟机的时钟漂移
- 某些编辑器保存时保留了原 mtime
任何一条发生,make 都会认为“目标文件比源文件新”,于是什么都不做,返回 0。
用户看到的现象是:改了代码,烧进去,行为没变。
我们的检查很简单:构建结束后,产物的 mtime 必须晚于本次构建的开始时间。不满足就报错,让人去 make clean 或者检查时钟。
const builtAt = fs.statSync(elfPath).mtimeMs;
if (builtAt < buildStartedAt - 1000) {
throw new GateError("产物未更新,可能命中了错误的增量构建");
}
减 1000 毫秒是给文件系统时间精度留的余量。有些文件系统只精确到秒。
检查三:产物大小是否异常
这条不是万能的,但成本极低。
我们记录最近若干次成功构建的产物大小,构建完做一次比对。如果新产物的大小和上一次相差超过阈值(我们用 30%),就提示一句。
它能拦下两类问题:
一是链接脚本错了。 比如 .text 段被放到 RAM 里,导致 .bin 大小暴涨,或者反过来,某些段被丢弃,.bin 小了一大截。
二是链接了旧的目标文件。 目录里残留的 .o 被一起链进去,产物大小会明显偏离。
这两类问题的共同特征是:它们不会让编译失败。 产物生成得好好的,只是内容不对。
检查四:烧录前的产物一致性
这一条比较特殊,它检查的不是产物质量,而是“你要烧的东西”和“你刚编的东西”是不是同一个。
场景:用户编完了,手动改了一个文件,然后直接点烧录。烧进去的是旧固件。
我们的做法是构建时记下产物哈希,烧录前重新算一次。不一致就拦下,问一句“产物已变化,是否重新构建”。
这听起来有点啰嗦,但它避免的是“改了半天发现没生效”这种极其浪费时间的情况。
门禁失败之后是什么
这里有个设计选择:门禁失败,是中止,还是警告?
我们的分法是两类:
硬门禁(中止):产物不存在、产物未更新、哈希不一致。
这三条的共同点是“继续下去必然是错的”——你要烧的东西根本不是你以为的那个。
软门禁(警告):新增警告、大小异常、构建耗时突变。
这三条可能是问题,也可能不是。比如新增一条 unused variable 完全无害;产物大小变化 30% 也可能是因为你确实加了一个大数组。
软门禁不拦,但它会在阶段状态里留一条记录。在“软件”这个阶段被标记完成之前,这些记录会被列出来,要求用户逐条确认。
这就是我们处理“不确定”的方式:不替你判断,但要你面对。
为什么不做静态分析
有人问过:为什么不直接上 clang-tidy、cppcheck,一次扫干净?
我们试过,结论是:对嵌入式项目,误报率太高。
大量的寄存器操作、位域、volatile 指针转换,在通用静态分析器看来都是可疑的,实际上完全正确。用户被误报淹没之后,就会关掉整个功能——那时候连真的问题也看不到了。
我们现在的策略是“只报能确定的事”。警告差集、产物新鲜度、哈希一致,这些都有明确的判定标准,不会误报。
宁可少报,不要误报。 这是在工具可信度上最重要的一条取舍——一旦用户开始患疑提示的准确性,所有提示都会失效。
最后
编译门禁这个东西,用户平时感觉不到。
它只在出问题的时候说一句话:“等一下,这个产物可能不是你想的那个。”
大部分时候用户会骂一句麻烦,然后去 make clean。
但总有那么几次,它拦下的是那种“折腾到凌晨两点才发现根本没重新编译”的事故。
那几次就够本了。