fix: 2026.8.8.1 的 #352 修复对真实 subos 完全不生效 —— wire format 读错了 - #377
Closed
speak-agent wants to merge 3 commits into
Closed
fix: 2026.8.8.1 的 #352 修复对真实 subos 完全不生效 —— wire format 读错了#377speak-agent wants to merge 3 commits into
speak-agent wants to merge 3 commits into
Conversation
… env 2026.8.8.1 taught `mcpp run` to hand a program its subos's environment, and recorded which subos in the build cache so the fast path could do it too. What it did not do is notice a cache entry written before that field existed. The reader's own comment claimed it did -- "absent ⇒ the run fast path treats the entry as a miss" -- and no such check was ever written. A comment describing behaviour the code does not have is worse than the missing check. The consequence is the whole #352 fix quietly not applying to anyone who upgrades, and not just for one run: the fast path's identity is the profile, the cache mode and the resource list, and its fingerprint check compares a cached entry against ITSELF. Nothing there notices that a different mcpp wrote the entry, so the pre-upgrade build is replayed until something else happens to invalidate it. Measured on a real upgrade from 2026.8.7.1 rather than reasoned about: build with the old binary, run with the new one, and the environment never arrives. An empty subosDir cannot stand in for "predates the field" -- a system toolchain outside the xpkgs store legitimately has no subos -- so presence is tracked separately. One rebuild after upgrading, and the field is there. The e2e ages a cache by stripping the line, which is exactly what an older mcpp's cache looks like, and asserts three things: the fast path is refused, the rebuild applies the environment, and the field is recorded so the next run does not repeat the work.
2026.8.8.1 shipped a reader for a format that does not exist. `envs` is an
OBJECT keyed by binding:
"envs": { "mesa@25.0.7.1": [ {"var":…,"op":…,"value":…}, … ], … }
The reader expected an array of {binding, decls} objects, so `is_array()` was
false, the loop never ran, and every variable came back unset. Against a real
subos on an NVIDIA host the released build applies nothing at all --
LIBGL_DRIVERS_PATH unset, __EGL_VENDOR_LIBRARY_DIRS unset, XDG_DATA_DIRS still
the host's. The whole #352 fix is inert.
Ten unit tests and an e2e passed the whole way, because every fixture was
hand-written in the same invented shape the parser expected. Both sides agreed
and both were wrong. A fixture composed from the same understanding as the
parser cannot catch a misunderstanding of the wire format; only one taken from
the writer can. RealXlingsCapture is that -- a verbatim copy of what a real
xlings wrote, reformatted for width and nothing else.
The parser is now transcribed from xlings's own reader rather than modelled on
it, which also caught a second divergence: xlings drops a declaration whose op
is neither "set" nor "prepend", and this accepted any op and treated everything
non-"set" as a prepend. A reader more permissive than its writer eventually
applies something the writer meant to reject.
Verified the way it should have been the first time: the fixed binary against
the subos a real `xlings install graphics` produced, with all three variables
arriving and the shared EGL vendor directory de-duplicated to one entry.
The e2e wrote its subos manifest in the same shape the broken reader expected, so it kept passing while the feature did nothing against a real subos. Fixing the reader made it fail, which is the correct outcome and the reason the fix is trustworthy. The comment now says the structure is a wire format rather than a convenience, because the invented shape is the tidier-looking of the two and someone will want to "simplify" it back.
Member
Author
|
关闭,内容并入一个覆盖三份设计文档的跨仓实现。 本 PR 的两处修复不作废,会原样带进新 PR:
关闭的原因是范围: |
Sunrisepeak
pushed a commit
that referenced
this pull request
Aug 8, 2026
…rected The doc lived only on the branch of the closed PR #377, so every reference to it from code comments, docs, and the implementation plan pointed at a file the repository did not contain. New §9 records what landing it changed about the design itself. §9.1 is the one that matters: the compatibility fallback described in §3.5 reads gcc's specs and clang's cfg -- files the same change stops writing. On an existing machine they are still there; on a fresh install they are not, and nothing downstream says so. That asymmetry is why local verification was green while CI was not, and it generalises past this change: when removing a mechanism, enumerate who reads its output.
speak-agent
added a commit
that referenced
this pull request
Aug 8, 2026
#378) 一个 payload 交付两样可分离的东西:编译的**能力**,以及关于如何链接的**主张**。mcpp 要前者,后者自己给 —— 链接行才是构建决策该待的地方,因为它是逐次构建而变的那一个。四个看起来无关的缺陷收敛到这一条上。 设计:`.agents/docs/2026-08-08-payload-version-and-contract-drift-design.md`(含 §9 实施回写)。替代已关闭的 #377。 ## 1. 产物加载哪个 glibc,由权威决定,不再靠目录顺序 旧规则向目录问「那个 glibc」并取 `readdir` 的第一项。只装一个时它永远正确,所以从没有东西逼它正确 —— 而一条带 `xim:glibc@>=2.38` 的依赖就足以装进第二个。mcpp-index 上真实发生过:编译侧取 2.44,产物 interpreter(冻结在 gcc specs 里)仍是 2.39,二进制引用 `GLIBC_2.42` 却跑在没有它的运行时上,**报错落在与那条依赖无关的包上**。 解析顺序:`[xlings] subos` 自述 → 活动 subos 自述 → 兼容记录(specs / cfg / **PT_INTERP**)→ 单例 payload。计入工具链指纹(11 字段)。 两条判据是落地时被 CI 逼出来的,已写回设计: - **没得选就不算猜。** 恰好一个 glibc payload 时没有选择可言,拒答等于拒答一个只有一个答案的问题。两个及以上才沉默 —— 那才是事故的形状。 - **化石不算权威。** `specs`/`.cfg`/`PT_INTERP` 都是过去某刻的记录,payload 被换掉时没人回头改。一条记录只有在它指的东西还在,才算数。subos 自己的声明同样适用。 把「查不到」也当成「有歧义」的代价是产物掉到宿主 loader —— 比猜错版本更糟,因为它连沙箱都出去了。 ## 2. 不再改写 GCC 的 `specs` 旧重写用单路径 needle 配双路径 replacement,每个跑过它的 home 都漏下一条:一台开发机产出的**每个** gcc 产物里都带 **68 条**陈旧 `RUNPATH`,全指向已删除的 `mktemp` 目录。没有东西发现它们,因为一条死 RUNPATH 只多花一点搜索时间。 改为 `-specs=` 一份逐构建生成的干净文件(`-dumpspecs` 取**内建** `*link:`,去掉 loader/rpath)。不带 `+` 的 `-specs=` 是**替换**,payload 分毫未动。逐构建、不需写权限,继承来的只读 payload 也能用。 删掉 `*link:` 也删掉了它提供的东西,因此 loader 与每条 rpath 由 mcpp 显式写在链接行上 —— 包括**编译器自己的 `lib64`**(`libgcc_s.so.1` 在那)。 ## 3. 落后于 pin 的 vendored xlings 会被替换 —— 但只在替换品确实更新时 一个 home 会永远留着第一次获取的 xlings(实测 2026.8.2.1 对 pin 2026.8.6.3)。`subos_info` 是 2026.8.5.1 才有的,所以那台机器上 subos 的自述一直被太旧的客户端丢弃,#352 的修复无论 mcpp 多新都无法生效。 修复的第一版直接删了重取,把 2026.8.2.1 换成系统的 **0.4.51** —— 更旧、且同样缺那个特性。现在先给替换品定价再动手。 ## 4. `--sysroot` 的判据从「存在」改为「归属」 gcc 把 `--sysroot=<...>/.xlings/subos/default` 当字符串烙进去,而一台机器上有很多同名目录 —— 烙入的路径经常**存在、却属于另一个 checkout**。实测:本仓库的构建解析到了无关仓库下的 sysroot。归属先于可用性。 --- ## 验证 | | | |---|---| | 单测 | **67/67**(新增 5 个文件、28 条断言) | | 本地全量 e2e | **186 passed / 6 failed / 8 skipped**,6 个失败全在已记录基线内(03/09/20/33/59/98),其中 03/09/20/155/184 逐一用**已发布的 2026.8.6.3** 复现确认;`178` 反而转绿 | | CI | **18/18 全绿** | | 版本 | 2026.8.8.2;xlings pin → 2026.8.7.1(索引最新;上游 2026.8.8.1 未收录,pin 过去会让 CI 装不到) | runtime binding 四条来源逐条实测:新建自述 subos、注入 `glibc@2.99`(不存在)、挪开 `specs` **且**把 gcc 自身 `PT_INTERP` 改指宿主、三条来源全打掉只剩单例。 ## 这一轮真正的教训 **十轮 CI 抓出 11 个缺陷,其中 8 个在本地定向子集全绿时不可见。** 形状高度一致:修补落在控制流或本机状态到不了的地方。 - 兼容回落读的是**本 PR 让 mcpp 不再写**的文件 —— 每台老机器都还留着它,所以本机永远绿 - 归属谓词加在 `probe_sysroot` **第一步就 return 掉**的函数里 - `--no-cache` 删 `target/`,带走了我放进去的 spec;e2e 201 只跑暖构建,**那个专为此机制存在的测试看不见它** - 我给 201 加的 PayloadFirst 分支,**把修复拿掉后照样通过** —— 本机 gcc 的 baked sysroot 指向另一个仓库且存在,把它拉回了 Sysroot 模式。守卫因此改成**指名模式**的单测,并逐一验证过红→绿 - 判据本身也错过:**「跑不跑得起来」在任何有宿主工具链的机器上都是 yes**,哪怕产物正在偷用宿主的库。换成「是否从 `/lib`、`/usr/lib` 加载任何库」 - 最后一条是**误报**:`ldd` 用宿主 loader 内省,总把 payload 解释器报成解析到宿主 —— 一个会拦下正确工作的检查,代价一样大 以及一个低级错误:`mcpp.toml` 的 `[toolchain] default` 被本地试验写成 llvm,`git add -A` 一起提交,五个 job 连带失败。**多个 job 同时开始失败,几乎总是指向一处共同输入被改动。**
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
① wire format 我读错了 —— 功能完全不工作
盘上真实的
envs是以 binding 为键的对象:我的 reader 期望的是数组
[{binding, decls}]⇒is_array()为假 ⇒ 循环一次都不执行。对着一台 NVIDIA 机器上真实的 subos,发布版的行为是:10 个单测和 1 条 e2e 一路全绿,因为每一份 fixture 都是我按同一个臆想手写的。两边一致,两边都错。
根因不是笔误,是方法:用与解析器同源的理解去造 fixture,抓不出对 wire format 的误解;只有取自写入方的 fixture 能抓。所以新增
RealXlingsCapture—— 真实 xlings 输出的逐字副本,只调了换行宽度。parser 现在是从 xlings 自己的 reader 誊写的,不是照着它建模。这顺带抓到第二处分歧:xlings 会丢弃
op不是set/prepend的声明,而我原来任何 op 都收、非set一律当 prepend。比写入方更宽容的读取方,迟早会应用一个写入方本想拒绝的东西。② 升级后复用升级前的缓存
快路径的 identity 是 profile / cache mode / resource 列表;fingerprint 校验拿缓存条目和它自己比。没有一项会注意到这条目是另一个 mcpp 写的,所以升级后的 mcpp 会一直复用升级前的构建。
reader 的注释声称它把旧缓存当 miss —— 那个检查从来没写过。注释描述了代码没有的行为,比漏掉更糟。
实测(2026.8.7.1 → 2026.8.8.1 真实升级):环境从没到达。修复后第 1 次 run 走全路径重建,之后快路径正常。
空的
subosDir不能代表「早于这个字段」 —— 系统工具链合法地没有 subos —— 所以「这一行存在过没有」单独记录。③ e2e 的 fixture 也是那个臆想格式
修好 reader 之后它正确地失败了,这正是这次修复可信的理由。注释里写明了这是 wire format 不是写法偏好 —— 臆想的那个形状看起来更整齐,一定会有人想「简化」回去。
验证
RealXlingsCapture200_subos_env_reaches_program:冷路径 / 缓存快路径 / 旧缓存降级 / 「无声明时不凭空发明」四条xlings install graphics产出的 subos,三个变量全部到位,共享的 EGL vendor 目录去重成一条Refs #352