diff --git a/.agents/docs/2026-08-06-deferred-work-plan.md b/.agents/docs/2026-08-06-deferred-work-plan.md index 8d450f83..47410203 100644 --- a/.agents/docs/2026-08-06-deferred-work-plan.md +++ b/.agents/docs/2026-08-06-deferred-work-plan.md @@ -491,3 +491,135 @@ B2 的"切换后仍是 NVIDIA"证明不了是切换的功劳。 | #56 | 能按需重现 | ❌ **未达成**,四个假设被排除 | 两项达成、一项没有。没达成的那项**没有被当作达成**,这本身就是这条规矩的用处。 + +### 7.6 #55 B 线:核心命题已在真实栈上验证(2026-08-06) + +图形栈用 `xlings config --mirror CN` 装进隔离 home(22 个包)。**先前两次判它 +"网络不可行/卡住"都是错的** —— 第一次是隔离 home 默认 GLOBAL 镜像,第二次是我用 +"20 秒零字节"给一个正在下载与解压之间的安装下了死刑。用单目录短时增长判定长任务, +不是测量。 + +#### 基线(glprobe,渲染并读回像素) + +| 环境 | `GL_RENDERER` | `PIXEL` | +|---|---|---| +| 宿主 | NVIDIA GeForce RTX 4080/PCIe/SSE2 | 336699 | +| **进 subos(今天的 `LD_LIBRARY_PATH` 方案)** | **llvmpipe (LLVM 20.1.7)** | 336699 | + +**§2.6 的缺陷被实测到了**:同一个链接宿主 libEGL 的二进制,进 subos 后从 GPU 掉到 +软件渲染,没有任何提示。两边像素都对 —— 所以"能不能渲染"这个检查抓不住它,只有 +渲染器名字能。 + +#### interposer 对照 + +27 KB,patchelf 三步(`--set-soname` / `--add-needed <宿主 vendor 绝对路径>` / +`--set-rpath --force-rpath <从载荷推导的闭包>`),安装期不需要编译器。 + +| | `GL_RENDERER` | `LD_LIBRARY_PATH` | +|---|---|---| +| interposer + **我们的** loader | **NVIDIA GeForce RTX 4080/PCIe/SSE2**,`PIXEL=336699`,`RESULT=ok` | **只有 `/usr/lib/x86_64-linux-gnu`** | + +`lib/xlings-deps` 完全不参与。**B2 的验收判据当场满足。** + +#### 边界:interposer 只对跑在我们 loader 上的消费者安全 + +同一个 interposer 给**宿主二进制**(宿主 loader、宿主 libc)用: + +``` +librt.so.1: undefined symbol: __pointer_chk_guard, version GLIBC_PRIVATE +``` + +正是 8-05 那次崩溃的同一条错误。原因清楚:interposer 的 RPATH 指向**我们的** +glibc,而消费者的 libc 是宿主的 —— §2.3 说的"两半来自不同构建"。 + +**这不是缺陷,是适用域**,而且与 §2.3 的推断一致:vendor 被 dlopen 进的那个进程 +本来就该是我们的(INTERP 指向我们的 glibc)。但它给 B1 加了一条**必须写进契约**的 +前置条件: + +> `host_link_interposer` 产出的对象,只可由 INTERP 指向我们载荷的消费者加载。 +> 宿主二进制必须继续走宿主自己的 vendor。 + +这条正是 §2.6/B4 存在的理由:把 vendor 目录编进**我们自己构建的** libglvnd,宿主的 +libglvnd 就用宿主默认目录,两条路径天然分开。 + +#### 与门禁验证的关系 + +门禁(§2.7)证明的是**机制可行**(DT_RPATH 传递、dlsym 穿透、GLX 无需全局变量); +这一节证明的是**在真实 NVIDIA 栈上有效**,并测出了适用域。两者都不能替代对方。 + +### 7.7 #55 B 线落地:三个"看起来成功"的东西被逐一测穿(2026-08-06) + +§7.6 证明了机制。落地时,每一步都先给出一个通过的结果,再被更严的测量推翻。 + +#### 一:载荷是空的,而断言救了场 + +`interposer-stub` 装完,payload 目录空无一物。install hook 自己的断言把它拦下并 +点名了包和路径。根因不是解包布局,是 **resource 声明位置错了** —— +`url_template` 被我嵌进了版本条目里,而它是版本表的**兄弟**;带 per-arch sha256 +的条目要用 `x86_64 = { url, sha256 }` 子表(`aria2-next` 是样板)。解析器对此只 +说了一句 `resource has neither url nor source`,然后什么也没下载。 + +**如果 install hook 没写那句断言,这个包会"安装成功"并留下一个空目录。** + +#### 二:`glxinfo` 报出 RTX 4080,而它证明的是宿主栈 + +subos 里跑 `glxinfo -B`: + +``` +OpenGL renderer string: NVIDIA GeForce RTX 4080/PCIe/SSE2 +``` + +看着是 B 线成了。实际上 `command -v glxinfo` = `/usr/bin/glxinfo`, +`patchelf --print-interpreter` = `/lib64/ld-linux-x86-64.so.2` —— **宿主二进制跑在 +宿主 loader 上**,我们的载荷一个字节都没参与。这行输出在 B 线成功与彻底没发生时 +**完全一致**。 + +改成从 `/proc/self/maps` 报出每个 GL 对象的**实际来源路径**后,真相立刻可读。 +两个探针现在都这么做,harness 断言我们的 interposer 在其中。 + +#### 三:`interposer: yes`,而四个入口点只覆盖了一个 + +glvnd **按名字 dlopen** 每个 vendor 库,所以 `libEGL_nvidia.so.0`、 +`libGLX_nvidia.so.0`、`libGLESv1_CM_nvidia.so.1`、`libGLESv2_nvidia.so.2` +各是一条独立载入链的**根**。DT_RPATH 只沿链**向下**传递,永远不会横跨到另一个根。 + +只 interpose 了 libEGL 时:EGL 在 4080 上渲染,GLX 的整个闭包仍来自 `/usr/lib`。 +而安装日志说的是 `interposer: yes`。 + +现在四个入口点全部 interpose,日志报的是**分数** `4/4`。`libGLX_nvidia.so.0` +同时是 Vulkan ICD,所以这一个根承载两套 API。 + +#### 四:`deps.build` 声明了,patchelf 没装(libxpkg loader 缺陷) + +`deps` 表只要有数组部分,loader 就走 legacy 分支:**`build` 子表被丢弃,数组项 +被复制进 `build_deps` 顶替**。声明照写、安装照样报成功、两件事都没做。症状出现在 +两层之外 —— elfpatch 警告 "patchelf 解析到 host"。 + +libxpkg 0.0.52 修了混合形状,但**索引不能依赖这个修复**:它要服务所有版本的 +客户端,而 recipe 里没有办法探测 loader 的形状能力(不像 Lua 函数可以 `type()` +探测)。所以 recipe 改用纯 split 形式,并加了一条静态测试禁止混合形状 —— +括号配对判断,不是正则(`deps` 表里几乎总有嵌套表和注释)。 + +#### 顺带纠正一处我自己的错话 + +我曾说"约 30 个用 elfpatch 的 recipe 在静默回落到宿主 patchelf"。两处都错: +`_find_tool` 的顺序是 **payload → subos view → home bin → host**,回落时**会警告** +(那句警告正是 A3 加的),且 `tool_payload_dir` 会**扫整个 store** —— 只要 home 里 +装过 patchelf 就走载荷,与是否声明无关。真实缺口只剩一个窄口子:**从未装过 +patchelf 的 home**。声明 build dep 堵的是这个。 + +#### 最终验证 + +`.agents/tools/graphics/verify-host-link.sh`,真实 RTX 4080(驱动 550.144.03), +隔离 home,**12/12**:四个入口点形状正确、EGL 渲染出像素、GLX 渲染、两者都经过 +**我们的** interposer、宿主真 vendor 由绝对 DT_NEEDED 拉入、`LD_LIBRARY_PATH` 为空、 +宿主驱动文件未被改动。 + +#### 这次落地新增的一条契约前置 + +vendor 的 dlopen 由**调用方**的搜索路径服务,而 `libGLX.so.0` 自己的 RPATH 是 +`$ORIGIN` —— 它看不到 vendor 包。真正让它解析成功的是:**DT_RPATH 会沿载入链向上 +一直搜到可执行文件**。DT_RUNPATH 不会 —— 同一个探针用 `--enable-new-dtags` 构建, +X 连接正常、GLX 扩展正常,却一个 vendor 都找不到。 + +> 消费者必须携带 **DT_RPATH**(`--force-rpath`),不能是 DT_RUNPATH。 diff --git a/.agents/tools/verify-release-lifecycle.sh b/.agents/tools/verify-release-lifecycle.sh index 11dcaf2d..2862a5e2 100755 --- a/.agents/tools/verify-release-lifecycle.sh +++ b/.agents/tools/verify-release-lifecycle.sh @@ -61,6 +61,21 @@ echo "version: $(x --version 2>&1 | head -1)" step "1. the client anchors to the home under test" x self init >/dev/null 2>&1 || true + +# The mirror, before anything downloads. +# +# A fresh home defaults to GLOBAL, and on a CN-routed machine that is the +# difference between a two-minute run and an hour of stalls -- the first +# attempt at this script sat at 140 MB for over an hour and I misread it as a +# hung process. The default here follows the DEVELOPER's own home rather than +# a hardcoded region: whatever route works for them works for the verification. +if [[ -z "${XLINGS_VERIFY_MIRROR:-}" ]] && [[ -f "$REAL/.xlings.json" ]]; then + XLINGS_VERIFY_MIRROR="$(sed -n 's/.*"mirror"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$REAL/.xlings.json" | head -1)" +fi +XLINGS_VERIFY_MIRROR="${XLINGS_VERIFY_MIRROR:-GLOBAL}" +x config --mirror "$XLINGS_VERIFY_MIRROR" >/dev/null 2>&1 || true +echo " mirror: $XLINGS_VERIFY_MIRROR" GOT="$(x self config 2>&1)" echo "$GOT" | grep -q "$HOME_DIR" \ || fail "the client reports a home other than the one under test. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d158502..8cda0d29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ env: # XIM_PKGINDEX_REF so release uses a known-good xlings + package index pair. BOOTSTRAP_XLINGS_VERSION: v0.4.69 # Must contain the mcpp version pinned in .xlings.json (2026.8.3.3). - XIM_PKGINDEX_REF: 20e53c6a4967346282af21c8fd2af91e6af75cc7 + XIM_PKGINDEX_REF: bf969a651caae4539eea8e92e3d14cb60e0eaf53 jobs: build-linux: @@ -167,7 +167,28 @@ jobs: # dependency rather than as an index nobody refreshed. xlings update || true xlings install -y - mcpp --version + + # Installing is not switching, and this cache makes the difference + # visible. `xlings install` lays the payload down; the shim keeps + # resolving whatever version was already active -- the client says so + # ("installed, but 'x' still resolves to ..."), and with ~/.mcpp + # restored from a cache that line is the ONLY sign that the mcpp + # about to run is the old one. Measured 2026-08-06: .xlings.json + # pinned 2026.8.6.1 while the run used the cached build, whose index + # snapshot predated the dependency this PR needs -- surfacing as + # `mcpplibs.xpkg@0.0.53 not found` against an index that demonstrably + # had it. + xlings use mcpp "2026.8.6.1" || true + + # Assert, do not print. A version line in a log is only read after + # something has already gone wrong. + got="$(mcpp --version 2>&1 | head -1)" + case "$got" in + *"2026.8.6.1"*) echo "mcpp $got (matches .xlings.json)" ;; + *) echo "FAIL: .xlings.json pins mcpp 2026.8.6.1 but the mcpp on PATH is '$got'." >&2 + echo " Installing does not switch; the shim still resolves the cached build." >&2 + exit 1 ;; + esac mcpp self version - name: Reinitialize mcpp registry (GLOBAL) @@ -194,6 +215,31 @@ jobs: prune_registry "$GITHUB_WORKSPACE/.mcpp/registry" mcpp self config --mirror GLOBAL + # Force the index refresh, then ASSERT what landed. + # + # The prune above drops the resolved indexes, and mcpp's own + # re-fetch happily uses whatever it finds in the restored cache -- + # so a freshly published dependency reads as "not found" against an + # index that demonstrably has it. Measured 2026-08-06: + # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the + # published artifact, the rolling pointer and releases/latest all + # carried the entry. + # + # `mcpp index update` is the refresh; `mcpp index status` prints the + # revision each index landed on, which is the one datum that makes a + # stale-index failure readable instead of surfacing two layers away + # as `@ not found`. + mcpp index update || true + mcpp index status || true + # PER PLATFORM, not a whole-file grep. + # + # A grep for `["0.0.53"]` passes on a file where the entry exists + # only under `xpm.linux` -- which is exactly the bug this assert was + # written to catch and did not: both bumps edited the linux section + # alone, linux CI went green, and macOS/Windows failed against a + # file that literally contains the string. Slice the section for + # THIS platform first. + - name: Prepare fixture index repo run: | bash tests/e2e/prepare_fixture_index.sh @@ -260,6 +306,31 @@ jobs: # already-installed mcpp bootstraps the source tree. mv .xlings.json .xlings.json.bootstrap mcpp self config --mirror GLOBAL + + # Force the index refresh, then ASSERT what landed. + # + # The prune above drops the resolved indexes, and mcpp's own + # re-fetch happily uses whatever it finds in the restored cache -- + # so a freshly published dependency reads as "not found" against an + # index that demonstrably has it. Measured 2026-08-06: + # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the + # published artifact, the rolling pointer and releases/latest all + # carried the entry. + # + # `mcpp index update` is the refresh; `mcpp index status` prints the + # revision each index landed on, which is the one datum that makes a + # stale-index failure readable instead of surfacing two layers away + # as `@ not found`. + mcpp index update || true + mcpp index status || true + # PER PLATFORM, not a whole-file grep. + # + # A grep for `["0.0.53"]` passes on a file where the entry exists + # only under `xpm.linux` -- which is exactly the bug this assert was + # written to catch and did not: both bumps edited the linux section + # alone, linux CI went green, and macOS/Windows failed against a + # file that literally contains the string. Slice the section for + # THIS platform first. mcpp build MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") test -x "$MCPP" @@ -395,7 +466,28 @@ jobs: # dependency rather than as an index nobody refreshed. xlings update || true xlings install -y - mcpp --version + + # Installing is not switching, and this cache makes the difference + # visible. `xlings install` lays the payload down; the shim keeps + # resolving whatever version was already active -- the client says so + # ("installed, but 'x' still resolves to ..."), and with ~/.mcpp + # restored from a cache that line is the ONLY sign that the mcpp + # about to run is the old one. Measured 2026-08-06: .xlings.json + # pinned 2026.8.6.1 while the run used the cached build, whose index + # snapshot predated the dependency this PR needs -- surfacing as + # `mcpplibs.xpkg@0.0.53 not found` against an index that demonstrably + # had it. + xlings use mcpp "2026.8.6.1" || true + + # Assert, do not print. A version line in a log is only read after + # something has already gone wrong. + got="$(mcpp --version 2>&1 | head -1)" + case "$got" in + *"2026.8.6.1"*) echo "mcpp $got (matches .xlings.json)" ;; + *) echo "FAIL: .xlings.json pins mcpp 2026.8.6.1 but the mcpp on PATH is '$got'." >&2 + echo " Installing does not switch; the shim still resolves the cached build." >&2 + exit 1 ;; + esac mcpp self version - name: Reinitialize mcpp registry (GLOBAL) @@ -422,6 +514,31 @@ jobs: prune_registry "$GITHUB_WORKSPACE/.mcpp/registry" mcpp self config --mirror GLOBAL + # Force the index refresh, then ASSERT what landed. + # + # The prune above drops the resolved indexes, and mcpp's own + # re-fetch happily uses whatever it finds in the restored cache -- + # so a freshly published dependency reads as "not found" against an + # index that demonstrably has it. Measured 2026-08-06: + # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the + # published artifact, the rolling pointer and releases/latest all + # carried the entry. + # + # `mcpp index update` is the refresh; `mcpp index status` prints the + # revision each index landed on, which is the one datum that makes a + # stale-index failure readable instead of surfacing two layers away + # as `@ not found`. + mcpp index update || true + mcpp index status || true + # PER PLATFORM, not a whole-file grep. + # + # A grep for `["0.0.53"]` passes on a file where the entry exists + # only under `xpm.linux` -- which is exactly the bug this assert was + # written to catch and did not: both bumps edited the linux section + # alone, linux CI went green, and macOS/Windows failed against a + # file that literally contains the string. Slice the section for + # THIS platform first. + - name: Build (macos_release) env: # macOS min-version support: floor 14.0 (the official LLVM @@ -567,6 +684,31 @@ jobs: Remove-Item -Recurse -Force "$env:GITHUB_WORKSPACE\.mcpp\registry" -ErrorAction SilentlyContinue mcpp self config --mirror GLOBAL + # Force the index refresh, then ASSERT what landed. + # + # The prune above drops the resolved indexes, and mcpp's own + # re-fetch happily uses whatever it finds in the restored cache -- + # so a freshly published dependency reads as "not found" against an + # index that demonstrably has it. Measured 2026-08-06: + # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the + # published artifact, the rolling pointer and releases/latest all + # carried the entry. + # + # `mcpp index update` is the refresh; `mcpp index status` prints the + # revision each index landed on, which is the one datum that makes a + # stale-index failure readable instead of surfacing two layers away + # as `@ not found`. + mcpp index update || true + mcpp index status || true + # PER PLATFORM, not a whole-file grep. + # + # A grep for `["0.0.53"]` passes on a file where the entry exists + # only under `xpm.linux` -- which is exactly the bug this assert was + # written to catch and did not: both bumps edited the linux section + # alone, linux CI went green, and macOS/Windows failed against a + # file that literally contains the string. Slice the section for + # THIS platform first. + - name: Build (windows_release) shell: pwsh run: | diff --git a/.github/workflows/xlings-ci-aarch64.yml b/.github/workflows/xlings-ci-aarch64.yml index 55bb8e5b..c73a1fd2 100644 --- a/.github/workflows/xlings-ci-aarch64.yml +++ b/.github/workflows/xlings-ci-aarch64.yml @@ -92,7 +92,13 @@ jobs: xlings config --mirror GLOBAL xlings update -y 2>/dev/null || xlings update 2>/dev/null || true xlings install mcpp -y - mcpp --version + # Installing is not switching -- see the note in xlings-ci-linux.yml. + xlings use mcpp "2026.8.6.1" || true + got="$(mcpp --version 2>&1 | head -1)" + case "$got" in + *"2026.8.6.1"*) echo "mcpp $got (matches .xlings.json)" ;; + *) echo "FAIL: .xlings.json pins mcpp 2026.8.6.1 but PATH has '$got'" >&2; exit 1 ;; + esac - name: Build cross-capable mcpp from source (mcpp main) run: | @@ -103,6 +109,31 @@ jobs: # an older mcpp version that may have left the rolling package index. mv .xlings.json .xlings.json.bootstrap mcpp self config --mirror GLOBAL + + # Force the index refresh, then ASSERT what landed. + # + # The prune above drops the resolved indexes, and mcpp's own + # re-fetch happily uses whatever it finds in the restored cache -- + # so a freshly published dependency reads as "not found" against an + # index that demonstrably has it. Measured 2026-08-06: + # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the + # published artifact, the rolling pointer and releases/latest all + # carried the entry. + # + # `mcpp index update` is the refresh; `mcpp index status` prints the + # revision each index landed on, which is the one datum that makes a + # stale-index failure readable instead of surfacing two layers away + # as `@ not found`. + mcpp index update || true + mcpp index status || true + # PER PLATFORM, not a whole-file grep. + # + # A grep for `["0.0.53"]` passes on a file where the entry exists + # only under `xpm.linux` -- which is exactly the bug this assert was + # written to catch and did not: both bumps edited the linux section + # alone, linux CI went green, and macOS/Windows failed against a + # file that literally contains the string. Slice the section for + # THIS platform first. mcpp build MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") test -x "$MCPP" diff --git a/.github/workflows/xlings-ci-linux-e2e.yml b/.github/workflows/xlings-ci-linux-e2e.yml index 0591d334..1040696d 100644 --- a/.github/workflows/xlings-ci-linux-e2e.yml +++ b/.github/workflows/xlings-ci-linux-e2e.yml @@ -23,7 +23,7 @@ env: GIT_TERMINAL_PROMPT: 0 XLINGS_RELEASE_MIRROR: GLOBAL BOOTSTRAP_XLINGS_VERSION: v0.4.69 - XIM_PKGINDEX_REF: 20e53c6a4967346282af21c8fd2af91e6af75cc7 + XIM_PKGINDEX_REF: bf969a651caae4539eea8e92e3d14cb60e0eaf53 jobs: e2e: @@ -175,7 +175,28 @@ jobs: # dependency rather than as an index nobody refreshed. xlings update || true xlings install -y - mcpp --version + + # Installing is not switching, and this cache makes the difference + # visible. `xlings install` lays the payload down; the shim keeps + # resolving whatever version was already active -- the client says so + # ("installed, but 'x' still resolves to ..."), and with ~/.mcpp + # restored from a cache that line is the ONLY sign that the mcpp + # about to run is the old one. Measured 2026-08-06: .xlings.json + # pinned 2026.8.6.1 while the run used the cached build, whose index + # snapshot predated the dependency this PR needs -- surfacing as + # `mcpplibs.xpkg@0.0.53 not found` against an index that demonstrably + # had it. + xlings use mcpp "2026.8.6.1" || true + + # Assert, do not print. A version line in a log is only read after + # something has already gone wrong. + got="$(mcpp --version 2>&1 | head -1)" + case "$got" in + *"2026.8.6.1"*) echo "mcpp $got (matches .xlings.json)" ;; + *) echo "FAIL: .xlings.json pins mcpp 2026.8.6.1 but the mcpp on PATH is '$got'." >&2 + echo " Installing does not switch; the shim still resolves the cached build." >&2 + exit 1 ;; + esac mcpp self version - name: Reinitialize mcpp registry (GLOBAL) @@ -202,6 +223,31 @@ jobs: prune_registry "$GITHUB_WORKSPACE/.mcpp/registry" mcpp self config --mirror GLOBAL + # Force the index refresh, then ASSERT what landed. + # + # The prune above drops the resolved indexes, and mcpp's own + # re-fetch happily uses whatever it finds in the restored cache -- + # so a freshly published dependency reads as "not found" against an + # index that demonstrably has it. Measured 2026-08-06: + # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the + # published artifact, the rolling pointer and releases/latest all + # carried the entry. + # + # `mcpp index update` is the refresh; `mcpp index status` prints the + # revision each index landed on, which is the one datum that makes a + # stale-index failure readable instead of surfacing two layers away + # as `@ not found`. + mcpp index update || true + mcpp index status || true + # PER PLATFORM, not a whole-file grep. + # + # A grep for `["0.0.53"]` passes on a file where the entry exists + # only under `xpm.linux` -- which is exactly the bug this assert was + # written to catch and did not: both bumps edited the linux section + # alone, linux CI went green, and macOS/Windows failed against a + # file that literally contains the string. Slice the section for + # THIS platform first. + - name: Prepare fixture index repo run: | bash tests/e2e/prepare_fixture_index.sh diff --git a/.github/workflows/xlings-ci-linux-root.yml b/.github/workflows/xlings-ci-linux-root.yml index eeb12339..2cb2d3da 100644 --- a/.github/workflows/xlings-ci-linux-root.yml +++ b/.github/workflows/xlings-ci-linux-root.yml @@ -21,7 +21,7 @@ env: XLINGS_RELEASE_MIRROR: GLOBAL # Keep in sync with xlings-ci-linux.yml. BOOTSTRAP_XLINGS_VERSION: v0.4.69 - XIM_PKGINDEX_REF: 20e53c6a4967346282af21c8fd2af91e6af75cc7 + XIM_PKGINDEX_REF: bf969a651caae4539eea8e92e3d14cb60e0eaf53 jobs: # ── Build the latest xlings from source via mcpp ────────────────── @@ -172,7 +172,28 @@ jobs: # dependency rather than as an index nobody refreshed. xlings update || true xlings install -y - mcpp --version + + # Installing is not switching, and this cache makes the difference + # visible. `xlings install` lays the payload down; the shim keeps + # resolving whatever version was already active -- the client says so + # ("installed, but 'x' still resolves to ..."), and with ~/.mcpp + # restored from a cache that line is the ONLY sign that the mcpp + # about to run is the old one. Measured 2026-08-06: .xlings.json + # pinned 2026.8.6.1 while the run used the cached build, whose index + # snapshot predated the dependency this PR needs -- surfacing as + # `mcpplibs.xpkg@0.0.53 not found` against an index that demonstrably + # had it. + xlings use mcpp "2026.8.6.1" || true + + # Assert, do not print. A version line in a log is only read after + # something has already gone wrong. + got="$(mcpp --version 2>&1 | head -1)" + case "$got" in + *"2026.8.6.1"*) echo "mcpp $got (matches .xlings.json)" ;; + *) echo "FAIL: .xlings.json pins mcpp 2026.8.6.1 but the mcpp on PATH is '$got'." >&2 + echo " Installing does not switch; the shim still resolves the cached build." >&2 + exit 1 ;; + esac - name: Reinitialize mcpp registry (GLOBAL) run: | @@ -198,6 +219,31 @@ jobs: prune_registry "$GITHUB_WORKSPACE/.mcpp/registry" mcpp self config --mirror GLOBAL + # Force the index refresh, then ASSERT what landed. + # + # The prune above drops the resolved indexes, and mcpp's own + # re-fetch happily uses whatever it finds in the restored cache -- + # so a freshly published dependency reads as "not found" against an + # index that demonstrably has it. Measured 2026-08-06: + # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the + # published artifact, the rolling pointer and releases/latest all + # carried the entry. + # + # `mcpp index update` is the refresh; `mcpp index status` prints the + # revision each index landed on, which is the one datum that makes a + # stale-index failure readable instead of surfacing two layers away + # as `@ not found`. + mcpp index update || true + mcpp index status || true + # PER PLATFORM, not a whole-file grep. + # + # A grep for `["0.0.53"]` passes on a file where the entry exists + # only under `xpm.linux` -- which is exactly the bug this assert was + # written to catch and did not: both bumps edited the linux section + # alone, linux CI went green, and macOS/Windows failed against a + # file that literally contains the string. Slice the section for + # THIS platform first. + - name: Prepare unit fixture index repo run: | bash tests/e2e/prepare_fixture_index.sh ../xim-pkgindex diff --git a/.github/workflows/xlings-ci-linux.yml b/.github/workflows/xlings-ci-linux.yml index af646d77..9878aa9e 100644 --- a/.github/workflows/xlings-ci-linux.yml +++ b/.github/workflows/xlings-ci-linux.yml @@ -18,7 +18,7 @@ env: # XIM_PKGINDEX_REF so CI uses a known-good xlings + package index pair. BOOTSTRAP_XLINGS_VERSION: v0.4.69 # Must contain the mcpp version pinned in .xlings.json (2026.8.3.3). - XIM_PKGINDEX_REF: 20e53c6a4967346282af21c8fd2af91e6af75cc7 + XIM_PKGINDEX_REF: bf969a651caae4539eea8e92e3d14cb60e0eaf53 jobs: build-and-test: @@ -177,7 +177,28 @@ jobs: # dependency rather than as an index nobody refreshed. xlings update || true xlings install -y - mcpp --version + + # Installing is not switching, and this cache makes the difference + # visible. `xlings install` lays the payload down; the shim keeps + # resolving whatever version was already active -- the client says so + # ("installed, but 'x' still resolves to ..."), and with ~/.mcpp + # restored from a cache that line is the ONLY sign that the mcpp + # about to run is the old one. Measured 2026-08-06: .xlings.json + # pinned 2026.8.6.1 while the run used the cached build, whose index + # snapshot predated the dependency this PR needs -- surfacing as + # `mcpplibs.xpkg@0.0.53 not found` against an index that demonstrably + # had it. + xlings use mcpp "2026.8.6.1" || true + + # Assert, do not print. A version line in a log is only read after + # something has already gone wrong. + got="$(mcpp --version 2>&1 | head -1)" + case "$got" in + *"2026.8.6.1"*) echo "mcpp $got (matches .xlings.json)" ;; + *) echo "FAIL: .xlings.json pins mcpp 2026.8.6.1 but the mcpp on PATH is '$got'." >&2 + echo " Installing does not switch; the shim still resolves the cached build." >&2 + exit 1 ;; + esac mcpp self version - name: Reinitialize mcpp registry (GLOBAL) @@ -204,6 +225,31 @@ jobs: prune_registry "$GITHUB_WORKSPACE/.mcpp/registry" mcpp self config --mirror GLOBAL + # Force the index refresh, then ASSERT what landed. + # + # The prune above drops the resolved indexes, and mcpp's own + # re-fetch happily uses whatever it finds in the restored cache -- + # so a freshly published dependency reads as "not found" against an + # index that demonstrably has it. Measured 2026-08-06: + # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the + # published artifact, the rolling pointer and releases/latest all + # carried the entry. + # + # `mcpp index update` is the refresh; `mcpp index status` prints the + # revision each index landed on, which is the one datum that makes a + # stale-index failure readable instead of surfacing two layers away + # as `@ not found`. + mcpp index update || true + mcpp index status || true + # PER PLATFORM, not a whole-file grep. + # + # A grep for `["0.0.53"]` passes on a file where the entry exists + # only under `xpm.linux` -- which is exactly the bug this assert was + # written to catch and did not: both bumps edited the linux section + # alone, linux CI went green, and macOS/Windows failed against a + # file that literally contains the string. Slice the section for + # THIS platform first. + - name: Prepare unit fixture index repo run: | bash tests/e2e/prepare_fixture_index.sh ../xim-pkgindex @@ -224,6 +270,7 @@ jobs: mcpp test || { status=$? echo "::group::mcpp build diagnostics" + ninja_bin="$(command -v ninja || true)" if [[ -z "$ninja_bin" ]]; then ninja_bin="$(find "$HOME/.mcpp" "$XLINGS_HOME" -name ninja -type f -perm -111 2>/dev/null | head -1 || true)" @@ -241,6 +288,44 @@ jobs: exit "$status" } + # A separate step, `if: failure()`, and not an `|| { ... }` on one + # command. + # + # The diagnostics used to hang off `mcpp test || { ... }` with + # `mcpp build` running unguarded above it. The shell is `bash -e`, so a + # failing `mcpp build` aborted the step before the block was ever + # reached: the diagnostic existed, never ran, and the failure looked + # exactly the same as if it had. Measured 2026-08-06 -- a whole CI round + # spent adding index-content output that could not print. + - name: Diagnose a failed mcpp build + if: failure() + run: | + # What index does this runner actually hold? `mcpplibs.xpkg@ + # not found` was diagnosed repeatedly from the outside -- index + # commit, published artifact, rolling pointer, releases/latest, + # publish lag, the mcpp version pin, install-vs-use, and the + # lockfile hash were each checked and each was correct. All of that + # is inference about a file only the runner can see. + for idx in "$HOME/.mcpp/registry/data/mcpplibs/pkgs/x/xpkg.lua" \ + "$GITHUB_WORKSPACE/.mcpp/registry/data/mcpplibs/pkgs/x/xpkg.lua"; do + if [ -f "$idx" ]; then + echo "--- $idx" + grep -oE '\["[0-9]+\.[0-9]+\.[0-9]+"\]' "$idx" | head -8 + else + echo "--- $idx : ABSENT" + fi + done + for root in "$HOME/.mcpp/registry" "$GITHUB_WORKSPACE/.mcpp/registry"; do + [ -d "$root" ] || continue + echo "--- $root" + ls -1 "$root" 2>/dev/null | head -8 + ls -1 "$root/data" 2>/dev/null | head -8 + ls -1 "$root/index-snapshots" 2>/dev/null | head -8 + done + mcpp --version 2>&1 | head -2 || true + mcpp self version 2>&1 | head -3 || true + xlings --version 2>&1 | head -2 || true + - name: Multiplatform contract scripts run: | bash tests/scripts/test_quick_install.sh diff --git a/.github/workflows/xlings-ci-macos.yml b/.github/workflows/xlings-ci-macos.yml index 3ce61137..43f4ba6b 100644 --- a/.github/workflows/xlings-ci-macos.yml +++ b/.github/workflows/xlings-ci-macos.yml @@ -19,7 +19,7 @@ env: # XIM_PKGINDEX_REF so CI uses a known-good xlings + package index pair. BOOTSTRAP_XLINGS_VERSION: v0.4.69 # Must contain the mcpp version pinned in .xlings.json (2026.8.3.3). - XIM_PKGINDEX_REF: 20e53c6a4967346282af21c8fd2af91e6af75cc7 + XIM_PKGINDEX_REF: bf969a651caae4539eea8e92e3d14cb60e0eaf53 jobs: build-and-test: @@ -167,7 +167,28 @@ jobs: # dependency rather than as an index nobody refreshed. xlings update || true xlings install -y - mcpp --version + + # Installing is not switching, and this cache makes the difference + # visible. `xlings install` lays the payload down; the shim keeps + # resolving whatever version was already active -- the client says so + # ("installed, but 'x' still resolves to ..."), and with ~/.mcpp + # restored from a cache that line is the ONLY sign that the mcpp + # about to run is the old one. Measured 2026-08-06: .xlings.json + # pinned 2026.8.6.1 while the run used the cached build, whose index + # snapshot predated the dependency this PR needs -- surfacing as + # `mcpplibs.xpkg@0.0.53 not found` against an index that demonstrably + # had it. + xlings use mcpp "2026.8.6.1" || true + + # Assert, do not print. A version line in a log is only read after + # something has already gone wrong. + got="$(mcpp --version 2>&1 | head -1)" + case "$got" in + *"2026.8.6.1"*) echo "mcpp $got (matches .xlings.json)" ;; + *) echo "FAIL: .xlings.json pins mcpp 2026.8.6.1 but the mcpp on PATH is '$got'." >&2 + echo " Installing does not switch; the shim still resolves the cached build." >&2 + exit 1 ;; + esac mcpp self version - name: Reinitialize mcpp registry (GLOBAL) @@ -194,6 +215,31 @@ jobs: prune_registry "$GITHUB_WORKSPACE/.mcpp/registry" mcpp self config --mirror GLOBAL + # Force the index refresh, then ASSERT what landed. + # + # The prune above drops the resolved indexes, and mcpp's own + # re-fetch happily uses whatever it finds in the restored cache -- + # so a freshly published dependency reads as "not found" against an + # index that demonstrably has it. Measured 2026-08-06: + # `mcpplibs.xpkg@0.0.53 not found`, while the index commit, the + # published artifact, the rolling pointer and releases/latest all + # carried the entry. + # + # `mcpp index update` is the refresh; `mcpp index status` prints the + # revision each index landed on, which is the one datum that makes a + # stale-index failure readable instead of surfacing two layers away + # as `@ not found`. + mcpp index update || true + mcpp index status || true + # PER PLATFORM, not a whole-file grep. + # + # A grep for `["0.0.53"]` passes on a file where the entry exists + # only under `xpm.linux` -- which is exactly the bug this assert was + # written to catch and did not: both bumps edited the linux section + # alone, linux CI went green, and macOS/Windows failed against a + # file that literally contains the string. Slice the section for + # THIS platform first. + - name: Prepare unit fixture index repo run: | bash tests/e2e/prepare_fixture_index.sh ../xim-pkgindex @@ -206,6 +252,7 @@ jobs: mcpp test || { status=$? echo "::group::mcpp build diagnostics" + ninja_bin="$(command -v ninja || true)" if [[ -z "$ninja_bin" ]]; then ninja_bin="$(find "$HOME/.mcpp" "$XLINGS_HOME" -name ninja -type f -perm -111 2>/dev/null | head -1 || true)" @@ -223,6 +270,44 @@ jobs: exit "$status" } + # A separate step, `if: failure()`, and not an `|| { ... }` on one + # command. + # + # The diagnostics used to hang off `mcpp test || { ... }` with + # `mcpp build` running unguarded above it. The shell is `bash -e`, so a + # failing `mcpp build` aborted the step before the block was ever + # reached: the diagnostic existed, never ran, and the failure looked + # exactly the same as if it had. Measured 2026-08-06 -- a whole CI round + # spent adding index-content output that could not print. + - name: Diagnose a failed mcpp build + if: failure() + run: | + # What index does this runner actually hold? `mcpplibs.xpkg@ + # not found` was diagnosed repeatedly from the outside -- index + # commit, published artifact, rolling pointer, releases/latest, + # publish lag, the mcpp version pin, install-vs-use, and the + # lockfile hash were each checked and each was correct. All of that + # is inference about a file only the runner can see. + for idx in "$HOME/.mcpp/registry/data/mcpplibs/pkgs/x/xpkg.lua" \ + "$GITHUB_WORKSPACE/.mcpp/registry/data/mcpplibs/pkgs/x/xpkg.lua"; do + if [ -f "$idx" ]; then + echo "--- $idx" + grep -oE '\["[0-9]+\.[0-9]+\.[0-9]+"\]' "$idx" | head -8 + else + echo "--- $idx : ABSENT" + fi + done + for root in "$HOME/.mcpp/registry" "$GITHUB_WORKSPACE/.mcpp/registry"; do + [ -d "$root" ] || continue + echo "--- $root" + ls -1 "$root" 2>/dev/null | head -8 + ls -1 "$root/data" 2>/dev/null | head -8 + ls -1 "$root/index-snapshots" 2>/dev/null | head -8 + done + mcpp --version 2>&1 | head -2 || true + mcpp self version 2>&1 | head -3 || true + xlings --version 2>&1 | head -2 || true + - name: "Assert macOS min-version support (minos + static libc++)" run: | # Multiple fingerprint dirs can exist (build + test fingerprints diff --git a/.github/workflows/xlings-ci-windows.yml b/.github/workflows/xlings-ci-windows.yml index 88a0a632..87f666a9 100644 --- a/.github/workflows/xlings-ci-windows.yml +++ b/.github/workflows/xlings-ci-windows.yml @@ -18,7 +18,7 @@ env: # XIM_PKGINDEX_REF so CI uses a known-good xlings + package index pair. BOOTSTRAP_XLINGS_VERSION: v0.4.69 # Must contain the mcpp version pinned in .xlings.json (2026.8.3.3). - XIM_PKGINDEX_REF: 20e53c6a4967346282af21c8fd2af91e6af75cc7 + XIM_PKGINDEX_REF: bf969a651caae4539eea8e92e3d14cb60e0eaf53 jobs: build-and-test: @@ -172,7 +172,26 @@ jobs: xlings update $global:LASTEXITCODE = 0 xlings install -y - mcpp --version + + # Installing is not switching. `xlings install` lays the payload + # down; the shim keeps resolving whatever version was already + # active, and with ~/.mcpp restored from a cache that is the old + # build -- whose index snapshot can predate the dependency this PR + # needs. Measured 2026-08-06 as `mcpplibs.xpkg@0.0.53 not found` + # against an index that demonstrably had it. + xlings use mcpp "2026.8.6.1" + $global:LASTEXITCODE = 0 + + # Assert, do not print: a version line in a log is only read after + # something has already gone wrong. + $got = (mcpp --version 2>&1 | Select-Object -First 1) -join "" + $global:LASTEXITCODE = 0 + if ($got -notmatch [regex]::Escape("2026.8.6.1")) { + Write-Host "FAIL: .xlings.json pins mcpp 2026.8.6.1 but the mcpp on PATH is '$got'." + Write-Host " Installing does not switch; the shim still resolves the cached build." + exit 1 + } + Write-Host "mcpp $got (matches .xlings.json)" mcpp self version - name: Reinitialize mcpp registry (GLOBAL) @@ -196,6 +215,14 @@ jobs: Prune-Registry "$env:GITHUB_WORKSPACE\.mcpp\registry" mcpp self config --mirror GLOBAL + # Force the index refresh, then assert what landed -- see the note + # in xlings-ci-linux.yml. Calling update without checking leaves + # "refreshed" and "still stale" producing identical output. + mcpp index update + $global:LASTEXITCODE = 0 + mcpp index status + $global:LASTEXITCODE = 0 + - name: Prepare unit fixture index repo shell: bash run: | diff --git a/.xlings.json b/.xlings.json index 48047e18..23722ebe 100644 --- a/.xlings.json +++ b/.xlings.json @@ -3,6 +3,6 @@ "projectScope": false, "mirror": "GLOBAL", "workspace": { - "mcpp": "2026.8.3.3" + "mcpp": "2026.8.6.1" } } diff --git a/mcpp.lock b/mcpp.lock index ace9fc27..bcd803a5 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -33,7 +33,7 @@ hash = "fnv1a:3465dd0bd5d7aa20" [package."mcpplibs.xpkg"] namespace = "mcpplibs" -version = "0.0.51" -source = "index+mcpplibs@0.0.51" -hash = "fnv1a:71031031ffb66224" +version = "0.0.53" +source = "index+mcpplibs@0.0.53" +hash = "fnv1a:aa76a9db91b6fca7" diff --git a/mcpp.toml b/mcpp.toml index ca14fb48..06c75708 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "xlings" -version = "2026.8.6.2" +version = "2026.8.6.3" description = "Universal package management infrastructure tool with SubOS isolation" license = "Apache-2.0" repo = "https://github.com/openxlings/xlings" @@ -39,7 +39,7 @@ libarchive = "3.8.7" [dependencies.mcpplibs] cmdline = "0.0.2" -xpkg = "0.0.51" +xpkg = "0.0.53" tinyhttps = "0.2.9" capi.lua = "0.0.3" diff --git a/src/core/config.cppm b/src/core/config.cppm index 875f6ae5..28c328de 100644 --- a/src/core/config.cppm +++ b/src/core/config.cppm @@ -13,7 +13,7 @@ import xlings.core.xvm.db; namespace xlings { export struct Info { - static constexpr std::string_view VERSION = "2026.8.6.2"; + static constexpr std::string_view VERSION = "2026.8.6.3"; static constexpr std::string_view REPO = "https://github.com/openxlings/xlings"; };