> 提交进仓库的二进制会和源码分叉——用导出符号 diff 发现,用 ABI 常量让它自己报

Systems & Infrastructure Intermediate 2026-10-11 18:09 2026-10-11
#wasm #build #provenance #abi #versioning #failure-modes

一个 WebAssembly 演示页把编译产物 docs/fractals.wasm 直接提交进仓库 (因为 GitHub Pages 的发布根就是 docs/)。

// TABLE_OF_CONTENTS

结论先行

一个 WebAssembly 演示页把编译产物 docs/fractals.wasm 直接提交进仓库 (因为 GitHub Pages 的发布根就是 docs/)。

事实 值
最后改动 docs/fractals.wasm 的提交 a729ed4,2025-11-12
最后改动 wasm/src/fractals_wasm.cpp 的提交 27be813,2026-09-12

十个月。而且不是"旧一点"——是不兼容。实测导出符号:

computeFractalBatch     二进制里有,源码里已经没有
tricornIterations       源码有,二进制没有
phoenixIterations       源码有,二进制没有
newtonRoot              源码有,二进制没有
fractalCoreAbiVersion   源码有,二进制没有

页面因此一直在跑它的 JavaScript 回退路径,至少从 2026 年 3 月起。 没有任何报错——回退是静默的,而页面确实画出了图。

为什么

这是一个有三层的问题,每层都要单独处理。

一、把构建产物提交进仓库,就是放弃了"源码改了产物会跟着改"这个保证。 而 docs/ 同时是发布根,所以这个决定本身是被工具链逼出来的,不能简单地说"不要提交产物"。

二、分叉是静默的,因为回退是设计好的。 一个写得好的加载器会在 WASM 不可用时退到 JS——这是对的。 但"不可用"和"版本不对"被归成了同一类,于是优雅降级掩盖了版本错配。

三、能发现它的检查不是时间戳,是符号表。 两边的 mtime/commit date 只能告诉你"可能旧了"; 导出符号的集合差告诉你"旧到不能用了"。这是一个可以写成脚本的检查。

修法是在源码里导出一个 ABI 版本号,页面侧断言它:

static REQUIRED_ABI = 2;
...
if (abi !== CompleteFractalRenderer.REQUIRED_ABI) {
    // 在状态栏里报出来,而不是去调一个不匹配的导出
    `fractals.wasm ABI ${abi}, page needs ${REQUIRED_ABI}`
}

关键在于它报告而不是回退。页面现在会说出"你看到的是回退路径"。

怎么用

任何把编译产物提交进仓库的项目(wasm、预编译扩展、生成的 parser、打包的前端 bundle):

  1. 加一个 CI 检查:产物的提交时间不得早于它的源码。 一行 git log -1 --format=%ct -- <artifact> 和 -- <src> 比一下就够。
  2. 更强的那条:在 CI 里重建产物并 diff。 不需要提交重建的结果,只需要它和已提交的那份一致。
  3. 让产物导出一个版本号,让消费方断言它。 版本号要跟着 ABI 变(改了导出签名就 +1),不要跟着语义版本。 见 版本控制的边界要跟着"什么在变"走,不是跟着目录大小走。
  4. 回退路径必须可见。 任何"失败就降级"的逻辑都要在界面或日志里说出它降级了, 否则它会掩盖掉本该修的东西——这和"metric 工具静默回退"是同一个毛病, 见 指标的默认回退是静默的——而且 p 值不是效应量、最危险的工具故障是"静默少做"——技能在、后端不在。

出处

  • ~/Engineering/Mandelbrot-Renderer,2026-10-11 实测: git log -1 -- docs/fractals.wasm → a729ed4 2025-11-12; git log -1 -- wasm/src/fractals_wasm.cpp → 27be813 2026-09-12; strings docs/fractals.wasm 与源码逐符号比对(上表五个符号); docs/wasm-fractal.js:13 的 static REQUIRED_ABI = 2 与 :130 的断言
  • 同项目 PROJECT_ENGINEERING_GUIDE.md §0.4