Skip to content

feat: date-based version scheme + xlings pin single source of truth + fix #291 (2026.7.27.1) - #292

Merged
Sunrisepeak merged 2 commits into
mainfrom
feat/date-version-and-xlings-pin
Jul 27, 2026
Merged

feat: date-based version scheme + xlings pin single source of truth + fix #291 (2026.7.27.1)#292
Sunrisepeak merged 2 commits into
mainfrom
feat/date-version-and-xlings-pin

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Jul 27, 2026

Copy link
Copy Markdown
Member

三件事:mcpp 版本号改日期格式、这个格式踩到的一处比较断点、以及 #291

设计与计划:.agents/docs/2026-07-27-date-version-and-xlings-pin-design.md / -plan.md

1. 版本号规范

YYYY.M.D.N,月/日不补零,与 xlings 生态对齐(xlings 同日从 0.4.70 迁到 2026.7.27.x)。

.0 保留给正式版本 / 稳定版本;日常迭代默认从 .1 开始。

本次取 2026.7.27.1。跨方案序单调:0.0.109 < 2026.7.27.1

2. 修复:4 段版本号被静默截断,致 E0006 索引底线失效

version_req::Version 原本严格三段,parse_version("2026.7.27.1") 解析出 {2026,7,27}丢弃第 4 段且不报错 —— 同一天内所有版本比较相等。

后果落在 pm/index_contract.cppm:索引写 min_mcpp = "2026.7.27.5"、用户跑 2026.7.27.1 → 相等 → have >= need 成立 → 底线检查放行。而挡住这种情况正是它存在的唯一理由。

修法:扩到 4 段 + 新增 components(源串写了几段)。两条约束:

  • 比较只看 4 个数字。用 = default<=> 会把 components 也纳入,导致 "1.2""1.2.0"
  • str() 必须能回写第 4 段 —— 它是载荷性的:pm/resolver.cppm:138 用它重建已解析的依赖版本串,该串流向 lock 文件与 xlings wire 地址。.0 结尾的版本塌成三段就会指向不存在的索引 key。

既有三段依赖解析逐字节不变,已加 ^/~ 回归用例。

3. #291:私有 glibc payload 不再无条件进运行目标的 LD_LIBRARY_PATH

该变量被整棵进程子树继承。运行目标若会 shell out(provider 调 popen("mcpp test ...")),/bin/sh宿主二进制 —— PT_INTERP 烙死在文件里,装载它的永远是宿主 ld.so,而这个变量把 payload 的 libc.so.6 递给它。

libc 与 ld.so 通过 GLIBC_PRIVATE 版本锁定(实测 payload libc.so.6ld-linux-x86-64.so.2GLIBC_PRIVATE 依赖),必须同一次构建。于是宿主 glibc ≠ payload 就崩在装载器里、main 之前,stdout 全空、零诊断。

这不是 issue 里说的「构建机不同致 ABI 不兼容」,而是纯版本错配 —— 受影响的是所有宿主 glibc 与 payload 不同的用户。它活这么久,正因为版本相同时不复现。

payload 目录是 LD_LIBRARY_PATH唯一不同时在可执行文件 RUNPATH 中的一项(flags.cppm 刻意排除以保持静态/musl 链接干净)。它唯一的用途是让 dlopen() 的库(其 DT_NEEDED 闭包不查主程序 RUNPATH)解析到同一份 libc。故改为仅当构建确有此类依赖库时注入depRuntimeLibraryDirs 非空)—— 真实的 host-GL 透传走 compat.glx-runtime[runtime] library_dirs,正落在该条件内,行为不变;零依赖二进制不再拿到。

process.cppm::strip_private_glibc 已保护 mcpp 自己的子进程,但够不到再外一层:变量是 mcpp 有意设给目标的。能控制的是不必要时不发。

4. xlings pin 收敛到唯一真源 + 机器校验

全部升到 2026.7.27.2src/xlings.cppm::kXlingsVersion 成为唯一真源,.github/tools/check_version_pins.sh 强制 .github/全部 16 个 pin 点、7 个文件一致。

这替换掉了那句「keep in lock-step with release.yml / cross-build-test.yml / ci-linux-e2e.yml」的注释 —— 清单本身就不全,漏了两个 composite action,CI 沙箱因此在 0.4.30 静默数周(#289)。

落实检查时找出四类此前无人记录的问题:

发现 位置
三处硬编码 aarch64 xlings tarball 字面量( XLINGS_VERSION 插值) release.yml:345/347/349
bootstrap pin 落后到 v0.4.38 ×2 ci-fresh-install.yml:94/230
bootstrap pin 落后到 v0.4.51 ci-fresh-install.yml:291
版本正则只截三段 → 对日期版本不报错而是两侧同样截断,pin 校验退化为「同日任意序号都算匹配」 install_pinned_mcpp.sh ×2

守卫自身的不变量写错了,CI 抓到了

第一版守卫断言四处版本全等,导致 CI 全红:package 'mcpp@2026.7.27.1' not found

.xlings.json / MCPP_PIN 指的是bootstrap 用哪个已发布 mcpp,必须落后,发布后单独 bump(ci-fresh-install.yml:41 注释原本就写着)。现改为三条不变量:mcpp.toml == MCPP_VERSION;两个 bootstrap 站点互相一致;bootstrap pin 不得新于在建版本(4 键数值排序,日期方案才排得对)。最后这条能在本地一秒内复现 CI 那次失败。

验证

  • 单元 37/37,新增 20(version_req)+ 5(index_contract)
  • e2e 158 通过 / 0 失败 / 5 跳过
  • 负向对照(每项都验证过「不修就会失败」):
    • 只把解析上界改回 3 → 5 条行为断言 + E0006 静默放行那条真失败,三段回归用例照常通过。(先前只做「换回旧文件」的对照不合格 —— 那只会得到编译错误,证明测试引用了新字段,不证明能捕获截断)
    • 用未修的 mcpp 跑 e2e 166 → 精确报出 LDLP=[.../xim-x-glibc/2.39/lib64]
    • 守卫三条不变量逐条改坏 → 逐条报出文件与两侧值
  • 发布资产已核实v2026.7.27.2openxlings/xlingsd2learn/xlings 均存在,四平台齐全、命名同构、直连 GET 200。release.yml 那份 aarch64 xlings 由 if curl 保护,资产缺失只会静默降级,故必须先验

顺带发现并已提 issue

#293tests/e2e/_inherit_toolchain.sh 把开发者真实 payload 符号链接进临时 MCPP_HOME,测试的 elfpatch/specs 重写会穿过链接写回真实 payload,把 mktemp -d 路径烙死;临时目录一删,本机 gcc 永久报 cannot execute: required file not found。本次就撞上了:~/.xlings/.../xim-x-gcc/16.1.0 的 24 个可执行文件 + specs 全部损坏,22_doctor_cache_publish.sh 因此失败 —— 一个测试的失败源于上一次测试运行。已用 patchelf 手工修复本机并验证真实编译运行通过。

记录的上游边界(不在本 PR 范围)

xlings 侧对 4 段版本有三处不足,同样作用于 xlings 自己的版本

行为 状况
xlings install mcpp@2026.7.27.1 ✅ 走版本表字面 key 精确匹配,不经 semver
xlings install mcpp(latest) ✅ 读 latest.ref 字面串
semver::parse ❌ 4 段直接 return nullopt
sort_desc ⚠️ 解析失败 → 回退字典序;同日第 10 次发布起 .10 排在 .9 之下
quick_install.sh 镜像择优 ⚠️ 只排前三段(仅未显式指定版本时执行,本仓一律显式 pin,不触发)

前两行是 mcpp 能装上的原因,已被 xlings 自己的 2026.7.27.x 在生产验证。

…2026.7.27.1)

Two things that had to ship together: mcpp's version format moves to the
date form YYYY.M.D.N, and that format broke a comparison that had to be
fixed first.

Version scheme
--------------
`YYYY.M.D.N`, month/day unpadded, matching the xlings ecosystem (xlings
migrated from 0.4.70 to 2026.7.27.x the same day). The fourth segment:

  `.0` is reserved for formal / stable releases; routine iterations start
  at `.1`.

Ordering across the change is monotonic — 0.0.109 < 2026.7.27.1.

Fix: four-segment versions were silently truncated
-------------------------------------------------
`version_req::Version` was strictly three segments, so
parse_version("2026.7.27.1") produced {2026, 7, 27} and dropped the fourth
without an error — every release of a given day compared EQUAL.

That lands on pm/index_contract.cppm: an index declaring
min_mcpp = "2026.7.27.5" against a running 2026.7.27.1 compares equal,
`have >= need` holds, and the E0006 floor lets a too-old mcpp through —
the single thing that check exists to prevent.

Version now carries a fourth component plus `components` (how many segments
the source wrote). Two constraints:

  - ordering compares only the four numbers; if `components` took part,
    "1.2" would stop equalling "1.2.0".
  - str() must render the fourth segment, because pm/resolver.cppm returns
    it as the RESOLVED dependency version, which flows into the lock file
    and the xlings wire address. A version ending in ".0" collapsed to three
    segments would address an index key that does not exist.

Existing three-segment resolution is byte-identical: the fourth component
is 0 on both sides, so ^ / ~ / = are unchanged (regression cases added).

xlings pins: one source of truth, machine-checked
-------------------------------------------------
All pins move to 2026.7.27.2, and `pinned::kXlingsVersion` becomes the
single source of truth that .github/tools/check_version_pins.sh enforces
across all 16 pin sites in 7 files — plus mcpp's own version across its 4
locations.

This replaces a comment that said to keep three named files in lock-step by
hand. That list was ALREADY incomplete: it omitted both composite actions,
which is how CI's sandbox sat on 0.4.30 for weeks unnoticed. Building the
check surfaced three more pin sites nobody had recorded (release.yml's
hardcoded aarch64 xlings tarball literals, which are not interpolated from
XLINGS_VERSION) and three stale bootstraps (ci-fresh-install.yml on v0.4.38
twice and v0.4.51).

install_pinned_mcpp.sh's version regex matched three segments only, which
does not fail on a date version — it truncates both sides identically, so
the pin check would silently accept any release of that day.

Verified: v2026.7.27.2 exists on both openxlings/xlings and d2learn/xlings
with all four platform assets, same naming as 0.4.69, direct GET 200. The
aarch64 bundling step is guarded by `if curl`, so a missing asset would
degrade silently rather than fail.
… glibc (#291)

Also corrects the pin invariant this branch's own guard got wrong, which is
what turned every CI job red.

#291 — private glibc payload on LD_LIBRARY_PATH
-----------------------------------------------
LD_LIBRARY_PATH is inherited by the entire process subtree. When the run
target is something that shells out — a course provider calling
popen("mcpp test ...") — /bin/sh is a HOST binary: its PT_INTERP is baked
in, so it loads the HOST ld.so, while this variable hands it the PAYLOAD
libc.so.6. glibc's libc and ld.so are version-locked through GLIBC_PRIVATE
(verified: the payload libc.so.6 carries a GLIBC_PRIVATE dependency on
ld-linux-x86-64.so.2), so on any host whose glibc differs from the
payload's the shell dies of SIGSEGV inside the dynamic linker, before main,
with empty stdout and no diagnostic.

This is NOT "compiled on a different build machine, ABI-incompatible" as
originally reported — it is plain version mismatch, and it hits every user
whose host glibc differs. It survived this long precisely because it does
not reproduce when the two happen to match.

The payload dir is the one entry on LD_LIBRARY_PATH that is not also in the
executable's RUNPATH — flags.cppm excludes it deliberately to keep static
and musl links clean. Its only purpose is letting a dlopen()'d library,
whose own DT_NEEDED closure never consults the executable's RUNPATH,
resolve the same libc. So emit it only when the build actually has such a
library (depRuntimeLibraryDirs non-empty). The real host-GL passthrough
case reaches mcpp through compat.glx-runtime's `[runtime] library_dirs`,
which populates exactly that vector, so it is unaffected; a
zero-dependency binary no longer gets it.

process.cppm's strip_private_glibc already protects mcpp's own children. It
cannot reach one hop further out — mcpp sets the variable for the target on
purpose, and what the target spawns is beyond its control. Not emitting it
when unnecessary is within its control.

e2e 166 pins both directions: a zero-dependency project must NOT receive
it, a project with `[runtime] library_dirs` MUST. It asserts on the emitted
environment rather than on whether a shell crashes — a crash-based test
passes for the wrong reason wherever host and payload glibc match, which is
how this went unnoticed.

e2e 65 dropped its glibc-payload assertion: its fixture has no
dependencies, so it never modelled the dlopen case its own header
describes.

Pin guard: the bootstrap pin is SUPPOSED to lag
-----------------------------------------------
The guard added earlier in this branch asserted that all four version
locations are equal. That is wrong, and CI proved it — every job failed
with `package 'mcpp@2026.7.27.1' not found`.

.xlings.json and MCPP_PIN name a mcpp that is already published, so they
must agree with each other but must NOT track the version being built; they
are bumped in a separate commit after the release lands in xim-pkgindex
(as ci-fresh-install.yml's own comment says). Both are back to 0.0.109.

The guard now checks three invariants instead: mcpp.toml == MCPP_VERSION;
the two bootstrap sites agree; and the bootstrap pin is never NEWER than
the version being built — a four-key numeric sort, so the date scheme
orders correctly. That last check reproduces the exact CI failure locally
in under a second.
@Sunrisepeak Sunrisepeak changed the title feat: date-based version scheme + xlings pin single source of truth (2026.7.27.1) feat: date-based version scheme + xlings pin single source of truth + fix #291 (2026.7.27.1) Jul 27, 2026
@Sunrisepeak
Sunrisepeak merged commit a401945 into main Jul 27, 2026
21 of 25 checks passed
@Sunrisepeak
Sunrisepeak deleted the feat/date-version-and-xlings-pin branch July 27, 2026 07:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant