当一个交付固件由 Bootloader、主应用、无线子镜像或协处理器固件合成时,仅靠文件名和流水线顺序无法证明镜像配对正确。可靠的构建流程应该把类型选择、目录隔离、失败语义和最终字节校验连成一条证据链。

问题不只是“文件放错了”

多镜像工程常见这样的隐含约定:

1
2
child-debug.bin   -> main debug
child-release.bin -> main release -> production package

如果这个关系只存在于脚本执行顺序或开发者记忆里,就可能出现:

  • debug 和 release 共用 CMake cache,配置相互污染;
  • 子镜像第二种类型构建失败,但第一种已经被发布;
  • 下游缺少目标文件时,悄悄退化成旧文件或单镜像;
  • 源文件选择正确,但合并偏移或长度错误;
  • CI 跳过编译直接重新打包,复用了旧的合并结果。

这些问题往往仍能生成一个“看起来存在”的 bin,因此单纯检查文件是否生成并不够。

可靠流程的六个约束

1. 构建类型必须显式传入

不要临时修改头文件来切换 debug 或 release。应使用白名单参数:

1
variant ∈ {debug, release}

并在配置阶段把它传给 CMake、Kconfig 或项目自己的配置层。未知类型直接失败,不能采用默认值继续。

2. 每种类型使用独立目录

1
2
3
4
5
build/
├── child-debug/
├── child-release/
├── main-debug/
└── main-release/

目录隔离避免上一轮缓存、宏定义和中间文件进入另一种构建。

3. 整套成功后再发布

生产者应先在临时位置完成全部类型:

1
2
3
4
5
build debug
-> verify debug
build release
-> verify release
publish both atomically

如果 release 失败,不能让下游看到“新 debug + 旧 release”的半套资产。发布动作应发生在全部验证通过之后。

4. 缺失时硬失败

当本次任务要求合并子镜像时,消费者应检查:

  • 文件存在且非空;
  • 类型路径与当前构建一致;
  • 大小没有超过目标分区;
  • 偏移和长度在容器范围内。

不要在文件缺失时自动退化为单镜像包。确实需要单镜像,应使用另一个明确开关。

5. 校验最终嵌入字节

比较源文件 SHA256 只能说明“拿到的输入是什么”,不能证明“最终包里嵌入了什么”。

更有力的检查是:

1
2
3
merged image
-> 按 offset 和源文件精确长度提取
-> 与输入子镜像逐字节比较

提取后可以使用:

1
cmake -E compare_files extracted-child.bin child-release.bin

只有比较成功,才能证明最终中间镜像中的对应区域与输入一致。

6. 交付物必须可追溯

每个交付包至少记录:

1
2
3
4
variant=release
path=artifacts/release/child.bin
size=<bytes>
sha256=<hash>

清单用于复盘输入,最终字节比较用于阻止错误交付,两者不能互相替代。

推荐流水线

1
2
3
4
5
6
7
8
9
参数白名单
-> 独立目录配置
-> 编译并检查退出码
-> 校验输入尺寸与哈希
-> 合并
-> 从合并结果提取目标区域
-> 逐字节比较
-> 签名 / 压缩 / 打包
-> 输出交付清单

如果签名、压缩或加密会导致内容变化,应在变换前的最后一个可比中间产物上执行提取比较,并让后续打包显式依赖这一步。

增量构建和 CI 的额外风险

子镜像内容或大小变化时,应触发主镜像重新合并。可以把子镜像注册为 CMake configure dependency,或者在每次打包入口重新计算大小和哈希。

“跳过编译、只重新打包”的 CI 路径尤其需要重新比较,因为它绕过了正常依赖链。缓存可以加速构建,但不能绕过交付正确性证明。

这套方案能证明什么

它能证明:

  • 构建类型选择明确;
  • 输入资产完整;
  • 最终中间镜像嵌入了预期字节;
  • 交付包可以追溯到明确输入。

它不能证明设备运行行为、协议交互、低功耗或升级回滚正确。这些仍需要模拟器、硬件和系统测试。