Repository navigation
[bug] 全局 build cache:obj 布局随消费方包组合变化(#233 消歧),而 cache key 不含消费方 → 同一 key 下第二个消费者必挂 'missing and no known rule' #344
Description
现象
全局 build cache 命中后,ninja 在 graph 阶段直接失败:
Cached compat.zlib v1.3.2 (15 units)
error: build failed
ninja: error: '<cache>/v1/pkg/compat/compat.zlib@1.3.2/58d45813f79658a1/obj/compress.o',
needed by 'obj/compress.o', missing and no known rule to make it
mcpp 自己认为命中(Cached … (15 units)),而 stage_file 边引用的那个文件在条目里不存在,
图里又没有回退的 compile 边,于是构建停在 graph 阶段,一个 TU 都没编。
根因
同一个 cache key 下,.o 的路径取决于「消费方工程里有哪些包」,而 key 不含消费方。
v2026.7.30.2 的设计说明写得很清楚,键的七个轴刻意不含 root 的身份与 flags:
不含 root 的身份与 flags —— 实测 root 的
[build] cflags/cxxflags不会下发到依赖 TU,
这正是跨工程共享成立的机制依据。
这个前提对编译产物的内容成立,但对产物的文件名不成立:#233 引入的 obj 路径消歧
是按整个 build dir 内的 basename 冲突触发的,也就是按消费方的包组合触发的。
- 消费方同时拉了
compat.zlib和compat.bzip2(两者都有compress.c)→ 触发消歧,
zlib 的对象写成obj/compat_zlib/zlib-1.3.2/compress.o; - 消费方只拉
compat.zlib→ 不触发消歧,对象是平的obj/compress.o。
两者算出的 cache key 完全相同(58d45813f79658a1),于是先跑的那个把自己的布局写进条目,
后跑的那个按自己的布局去取 —— 必然缺一个文件。
最小复现(mcpp 2026.8.3.3 / linux-x86_64 / gcc 16.1.0)
用 mcpplibs/mcpp-index main 的两个 workspace 成员:
tests/examples/archive(拉 zlib + bzip2 + …)和 tests/examples/libpng(只拉 zlib)。
方向 A —— archive 先跑:
$ mcpp test -p archive
test result ok. 2 passed; 0 failed
$ mcpp test -p libpng
ninja: error: '<cache>/…/58d45813f79658a1/obj/compress.o',
needed by 'obj/compress.o', missing and no known rule to make it此时条目里 15 个 .o 有 14 个是平的,只有 compress.o 是嵌套的:
obj/adler32.o
obj/crc32.o
…
obj/compat_zlib/zlib-1.3.2/compress.o ← 唯一一个被消歧过的
方向 B —— 清空 cache,libpng 先跑(完全对称):
$ rm -rf <cache> && mcpp test -p libpng
test result ok. 1 passed; 0 failed
$ mcpp test -p archive
Cached compat.zlib v1.3.2 (15 units)
ninja: error: '<cache>/…/58d45813f79658a1/obj/compat_zlib/zlib-1.3.2/compress.o',
needed by 'obj/compat_zlib/zlib-1.3.2/compress.o', missing and no known rule to make it同一个 key,15 个 .o 这次全是平的,缺的正好是嵌套的那个。谁第二个跑谁挂。
与 #233 / #240 的关系
这是同一族的第三处「消歧只做了一半」:
- [bug] 对象路径按「父目录名+文件名」折叠,不同目录同名源文件冲突(multiple rules generate *.ddi) #233:编译边本身撞名;
- [bug] #233 消歧后链接输入未跟随改名:依赖包与消费者同名源(如双方都有 main.cpp)即 'obj/main.o missing and no known rule' #240:编译边消歧了,链接输入没跟着改名;
- 本条:编译边消歧了,写进全局 cache 的布局跟着变了,而 key 没跟着变。
#240 里那句「包作者侧无法规避」在这里同样成立 —— zlib 与 bzip2 各自上游就叫 compress.c,
描述符既不能改上游文件名,也没有任何字段能控制 obj 路径。所以这条修不到描述符侧。
影响
mcpp-index CI 把 pin 从 0.0.109 提到 2026.8.3.1 后,全量 workspace 跑:
| leg | 结果 |
|---|---|
| linux | 13/47 成员失败 |
| macos | 11/47 成员失败 |
| windows | 8/47 成员失败 |
同一批成员在 0.0.109 上是 Workspace all 46 member(s) passed。挂的全是「zlib / ffmpeg 的下游、
但包组合与先跑的那个成员不同」的成员:eui-neo×6、opencv-module×3、freetype、libpng、
sdl2、vulkan。
失败模态对使用者不友好:错误信息只说某个 cache 路径缺文件,不提消歧、也不提命中判定,
而 Cached … (15 units) 还在上一行说命中成功。
Workaround
--cache local 可以绕过(已验证 mcpp test -p libpng --cache local 通过)。
但这会让整个 CI 失去 cache 收益。
可能的修法方向(供参考)
- cache 条目按规范化的、与消费方无关的布局存放(例如永远用消歧后的
obj/<target>/<mirror-path>/x.o),stage_file边在读取时映射到本次 build dir 的实际路径; - 或者把「本次 build dir 的消歧结果」纳入 key —— 但这会按消费方分裂条目,与跨工程共享的设计目标冲突,
所以 1 更贴近设计意图。
另外无论怎么修,cache 命中后如果 stage_file 的源不存在,应当回落到 compile 边或明确报
build cache corruption,而不是让 ninja 在 graph 阶段报一条看不出所以然的错。
环境
- mcpp
2026.8.3.3linux-x86_64(解包的 release 二进制,cache 冷启动)/ gcc 16.1.0 - 同样的错误在 mcpp-index CI 的
2026.8.3.1上于 linux / macos / windows 三平台复现 - build cache 首次出现在
v2026.7.30.2;0.0.109(index 此前的 pin)无此功能,故此前未暴露
Activity
fix(build): link through a response file on every platform, not just …
fix(cache): a package's object layout must not depend on the consumer (…
chore(ci): pin mcpp 2026.8.3.3 + move the index floor; bypass the glo…
feat: add boost-ext.ut 2.3.1 C++23 module package (#142)
feat(build.mcpp): 机制架构落地 —— 指令表收敛、协议契约、host 工具、构建图节点、规则包 (2026.8.5.1) (…
fix(link): 响应文件按行分隔,拆掉 link.exe 的单行 128 KiB 上限 (2026.8.5.3) (#360)
fix(link): 命令长度上限从架构上消掉,不再补第八个洞 (2026.8.5.4) (#361)
fix: 「请求的版本 → 装好的目录」有两个调用方,而我先只修了一个
Resolve the target side once, and let every translation unit read it (#…