From 497ff6ad43bc850b438fd7f796e040fcd79d3a12 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 10 Aug 2026 11:17:54 +0800 Subject: [PATCH 1/2] fix(deps): a cold home can install gcc again, and CI can see it when it cannot (2026.8.10.2) 2026.8.10.1 began filling `dependency_store_roots` on every install, and shipped libxpkg 0.0.55 in the same release. Either alone is inert; together they executed a path where `dep_install_dir` gated on how specific the QUERY was rather than on whether the ANSWER was unique. Six of the seven call sites in xim-pkgindex returned nil: gcc and meson could not install on any cold home -- new machines, cold CI caches, `fresh-install`, and every downstream mcpp build -- while godot silently fell back to the host's GL and the graphics banner reported `unknown`. The fix is libxpkg 0.0.56 (`resolved_dep` judges ambiguity, which its own uniqueness guard already decided, and matches the version half as the RANGE a recipe actually writes). This commit takes that dependency and adds the thing that was missing on this side: a way to notice. **E2E-79, and why it has to be cold.** A warm home skips the install, so the config hook never runs -- which is exactly why every lane here stayed green while the released binary could not install a toolchain. The first detector was a downstream repo's cache miss. This test is that detector, in-tree: a consumer declares a namespaced ranged coordinate, the resolver places the dependency, and the hook must find it. Verified against both builds -- it fails on libxpkg 0.0.55 with "this is #524" and passes on 0.0.56. It also pins the other half of the contract, with a second consumer that asks for something it never declared: that must fail, name the coordinate, and say it is not a declared dependency. "Cannot answer" and "is not installed" made the same nil for a whole release, and that is what sent the bug report looking for a missing glibc that was sitting on disk. **`.agents/tools/repro-dep-install-dir.sh`** replays all seven real call shapes against any libxpkg revision offline -- no network, no install, no home touched. It reads the file out of git, and takes `worktree` for an uncommitted one, because otherwise a fix that is not yet committed tests as if it did not exist. Root cause, blast radius and the #525 analysis: `.agents/docs/2026-08-10-issue-524-525-root-cause-and-fix-design.md` Task graph and cross-repo order: `.agents/plans/2026-08-10-issue-524-525-implementation.md` Refs #524, #525 --- ...issue-524-525-root-cause-and-fix-design.md | 428 ++++++++++++++++++ ...2026-08-10-issue-524-525-implementation.md | 142 ++++++ .agents/tools/repro-dep-install-dir.lua | 188 ++++++++ .agents/tools/repro-dep-install-dir.sh | 46 ++ mcpp.lock | 18 +- mcpp.toml | 4 +- src/core/config.cppm | 2 +- .../config_hook_resolves_declared_dep_test.sh | 206 +++++++++ tests/e2e/run_all.sh | 1 + 9 files changed, 1023 insertions(+), 12 deletions(-) create mode 100644 .agents/docs/2026-08-10-issue-524-525-root-cause-and-fix-design.md create mode 100644 .agents/plans/2026-08-10-issue-524-525-implementation.md create mode 100644 .agents/tools/repro-dep-install-dir.lua create mode 100755 .agents/tools/repro-dep-install-dir.sh create mode 100755 tests/e2e/config_hook_resolves_declared_dep_test.sh diff --git a/.agents/docs/2026-08-10-issue-524-525-root-cause-and-fix-design.md b/.agents/docs/2026-08-10-issue-524-525-root-cause-and-fix-design.md new file mode 100644 index 00000000..86cefee1 --- /dev/null +++ b/.agents/docs/2026-08-10-issue-524-525-root-cause-and-fix-design.md @@ -0,0 +1,428 @@ +# #524 / #525 根因分析与修复设计 + +> 状态:**Root cause measured, fix designed, not implemented** —— 两条 issue 的根因都有可运行的复现或实测证据, +> 不是从日志推断的。#524 的修复方案已经在离线 harness 上验证过,#525 的修复方案还只有机制证据、没有端到端实测。 +> +> 日期:2026-08-10 +> +> 对象:`openxlings/xlings#524`(冷装 gcc 失败)、`openxlings/xlings#525`(GLX 拿不到 FBConfig) +> +> 复现工具:`.agents/tools/repro-dep-install-dir.sh`(离线,无需 xlings / 网络 / 安装,不碰任何 home) +> +> 关联:libxpkg `0.0.54 → 0.0.55 → 0.0.56`、xlings `2026.8.10.1`(#519 / #514)、 +> `mcpp-community/mcpp#400`、`mcpp-community/mcpp#401` + +--- + +## 0. 结论先行 + +| | #524 冷装 gcc 失败 | #525 GLX 无 FBConfig | +|---|---|---| +| **根因** | libxpkg 0.0.55 把 `dep_install_dir` 的**匹配条件从「答案是否唯一」换成了「问题是否够具体」**,同时让 explicit-roots 分支**无条件 `return`**、切断了 scan 兜底。index 里没有一条 recipe 写成它要求的形状。 | glvnd 的 **GLX 侧根本没有 `egl_vendor.d` 的对应物**(实测:`libGLX.so.0` 里只有 `libGLX_%s.so.0` 和 `__GLX_VENDOR_LIBRARY_NAME`,没有任何目录变量)。vendor 可达性只能由**执行 dlopen 的那个对象自己的 RPATH** 提供,而我们的 `libGLX.so.0` 的 RPATH 至今只有 `$ORIGIN`。 | +| **怎么引入的** | 收紧规则时**没有对照真实调用点**。7 个调用点里 6 个是「bare + 无版本」,而新规则要求「namespaced + 精确版本」。 | 机制被验证过,但**验证探针用了 `-Wl,--disable-new-dtags`**,把可达性挂在**消费者的链接参数**上。第一个真实消费者(mcpp,默认 `--enable-new-dtags`)一接触就塌。`glxprobe.c:15-28` 把这条前置条件写得清清楚楚,然后当成前提而不是缺陷。 | +| **影响面** | **6/7 调用点断**,比 issue 报的多两条硬失败(meson 冷装同样失败)和两条**静默降级**(godot 悄悄回落宿主 GL、graphics 横幅显示 unknown)。 | 任何用默认链接参数的 GL 消费者。生态里目前**全部**如此。 | +| **0.0.56 修了吗** | **没有**。它只把同一个 nil 变响了:6/7 依然断。 | — | +| **修复** | libxpkg:bare 查询按**答案唯一性**判定(唯一性守卫本来就在),版本半边按**区间**匹配。实测 6→2。剩下 2 条是真正不同的问题,单独决策。 | 让 `xim:libglvnd` 的 `libGLX.so.0` 自带指向 **GLX vendor 目录**的 RUNPATH。这是 issue 提的方向 B 的正确形态:不是环境变量(glvnd 没有),而是一个目录 + 我们自己载荷上的 RPATH。 | + +**两条 issue 是同一个形态的两次发作**,见 §3。 + +--- + +## 1. #524 —— 冷装 `xim:gcc@16.1.0` 失败 + +### 1.1 回归窗口:正好一个 commit + +``` +origin/main 65002df 2026.8.10.1 ← 失败 + f203b6b 2026.8.9.2 ← 成功 +``` + +`v2026.8.9.2..65002df` 之间只有这一个 commit。它同时做了两件事,**两件都是必要条件**: + +1. `src/core/xim/installer.cppm:895` 新增 `configure_dependency_store_roots_()`, + 在 `:2485` 和 `:3294` 两条安装路径上**无条件**填充 `ctx.dependency_store_roots` + (至少含 selectedStore + global xpkgs,所以**永远非空**)。 +2. `mcpp.lock`:`mcpplibs.xpkg` `0.0.54 → 0.0.55`。 + +单独任一方都不会出事:0.0.55 在没有 roots 字段时会走 scan 兜底;0.0.54 在有 roots 时忽略它。 +**是这两个一起发布,才让新分支第一次被执行到。** + +### 1.2 根因:两道闸门,外加一次早退 + +gcc 的 config hook 写的是(`xim-pkgindex/pkgs/g/gcc.lua:544`): + +```lua +local glibc_dir = pkginfo.dep_install_dir("glibc") -- bare name, 无版本 +``` + +而它声明的是 `deps = { "xim:glibc@>=2.39", ... }` —— **namespaced + 区间**。 + +0.0.55 的 `pkginfo.lua` 在这条路径上加了三样东西: + +```lua +-- :234 resolved_dep —— 闸门 1 +function M.resolved_dep(dep_name, dep_version) + local ns, bare = _parse_namespace(dep_name) + if not ns then + if not _is_exact_store_version(dep_version) then return nil end -- :239 ← dep_version 是 nil,直接 nil + ... +``` + +```lua +-- :178 _resolve_dep_via_explicit_roots —— 闸门 2 + if not _is_exact_store_version(dep_version) then return nil end -- :179 ← 同样 nil + local ns, bare = _parse_namespace(dep_name) + if not ns then return nil end -- ← bare 名字,再 nil +``` + +```lua +-- :296 dep_install_dir —— 早退 + local roots = _RUNTIME and _RUNTIME.dependency_store_roots + if type(roots) == "table" then + return _resolve_dep_via_explicit_roots(dep_name, dep_version) -- ← 无条件 return,scan 永远够不到 + end + local result = _resolve_dep_via_scan(...) -- ← 0.0.54 靠这行活着 +``` + +于是 `dep_install_dir("glibc")` 返回 **nil,且一句话都不说**。gcc 的 hook 只能报「glibc payload not found」—— +它说的是它知道的唯一一件事。 + +**闸门 1 是核心缺陷。** 注意它的循环体里**本来就有唯一性守卫**: + +```lua +if record_bare == bare and rec.version == dep_version then + if candidate then return nil end -- 两条记录同名 → 失败关闭 + candidate = rec +end +``` + +歧义已经由这一行判定了。`_is_exact_store_version(dep_version)` 那道前置**没有参与任何消歧**—— +它唯一的作用是把「不带版本的查询」整个拒掉,而那正是 index 里**每一条 recipe 实际写的形状**。 + +> 一句话:**0.0.55 把判定条件从「答案是否唯一」换成了「问题是否够具体」。** +> 这两者只在 recipe 恰好写得很啰嗦时重合,而没有一条 recipe 那么写。 + +### 1.3 复现:离线、可运行、不需要装任何东西 + +`.agents/tools/repro-dep-install-dir.sh` 把真实的 `pkginfo.lua`(直接从 libxpkg checkout 按 rev 取) +挂在 stub 过的 xmake stdlib + 假 xpkgs store 上,按**已发布 recipe 的原样调用形状**重放: + +``` +$ .agents/tools/repro-dep-install-dir.sh # 默认 0.0.54 / 0.0.55 / 0.0.56 +$ .agents/tools/repro-dep-install-dir.sh HEAD fix/xxx # 任意 rev +``` + +| 调用点(recipe 原样) | 0.0.54 | 0.0.55 | 0.0.56 | **fixA** | +|---|---|---|---|---| +| `gcc.lua:544` `dep_install_dir("glibc")` | ok | **nil** | **nil** | ok | +| `llvm.lua:210` `dep_install_dir("glibc")` | ok | **nil** | **nil** | ok | +| `graphics.lua:176` `dep_install_dir("nvidia-gl-host-link")` | ok | **nil** | **nil** | ok | +| `meson.lua:116` `dep_install_dir("python")` | ok | **nil** | **nil** | ok | +| `godot.lua:351/422` `dep_install_dir("mesa")` ¹ | ok | **nil** | **nil** | **nil** | +| `mcpp-vscode-clangd.lua:245` `dep_install_dir("llvm-tools", ver)` ² | ok | **nil** | **nil** | **nil** | +| (对照)`dep_install_dir("xim:glibc")` | ok | ok | ok | ok | +| | 0/7 断 | **6/7 断** | **6/7 断** | **2/7 断** | + +¹ mesa 是**传递**依赖(godot 声明的是 `xim:graphics@>=0.1`),`resolved_deps` 只记录 `node.runtime_deps` +(`installer.cppm:2472`),所以本来就没有它的记录。 +² `llvm-tools` 是 hook 里 `pkgmanager.install()` 装的,不是声明依赖,同样没有记录。 + +**「有 roots / 无 roots」两列都跑了**:无 roots 时三个版本全绿 —— 这直接证明触发条件是 +xlings 2026.8.10.1 开始填这个字段,而不是 recipe 或 home 的问题。 + +### 1.4 libxpkg 0.0.56 不是这条 issue 的修复 + +0.0.56(`fix(pkginfo): an unanswerable dependency query must not look like an absent one`) +已经**正确诊断**了这个问题——commit message 里点名了 `gcc.lua` 和 `llvm.lua` 的 bare 调用。 +但它的改动**只加诊断**: + +```lua +local hit = _resolve_dep_via_explicit_roots(dep_name, dep_version) +if hit then return hit end +... +log.error("dep_install_dir(%s): a bare name cannot be resolved against ...") +return nil -- ← 依然 nil +``` + +实测:**6/7 依然断**,只是现在会打印一条错误。 + +它的立场是「nil 在这里是**正确答案**,错的是**沉默**」。**这个立场对了一半。** +对的一半:explicit roots 确实只能回答精确的 namespaced 坐标,让它猜 `compat-x-zlib` 还是 `other-x-zlib` 是错的。 +错的一半:`resolved_deps` **不是** explicit roots。它是一张**封闭的小表**,就是这个包自己声明的那几条依赖, +「同一个 bare 名字有没有第二条记录」是**可判定的**,不需要猜。0.0.55 把 roots 分支的正确约束 +(必须精确、必须带命名空间)**误加到了 `resolved_deps` 分支上**,而后者恰恰是为了免去调用方重复说一遍而存在的。 + +### 1.5 issue 里的一条假线索 + +> 「安装的是 **2.44**,而 hook 的诊断建议的是 **2.39** —— 二者不一致本身就是线索。」 + +**不是线索。** `xlings install xim:glibc@2.39` 是 `gcc.lua:545` 里的**硬编码字面量**: + +```lua +log.error(" install it first: xlings install xim:glibc@2.39") +``` + +它跟解析结果没有任何关系,2.44 被解析出来是完全正确的(`>=2.39` 允许)。 +顺手改掉这个字面量(改成 hook 真正查的那个坐标),否则下一个人还会顺着它走一遍。 + +日志里**没有**出现 0.0.55 的另外两条消息(`resolver record points to missing payload`、 +`no explicit store roots, fell back to a scan`),这本身就是判别证据:它区分了 +「记录有但 payload 不在盘上」和「压根没匹配上记录」,落在后者。 + +### 1.6 为什么 CI 没拦住 + +- **热 home 天然免疫**:gcc 已装 → 整个 install 被跳过 → config hook 不跑。xlings 自己的 CI 走的是这条路。 +- 唯一会冷装 gcc 的是下游(mcpp)的 sandbox 缓存 miss —— 也就是 issue 里那次 A/B。 + 换句话说,**这个缺陷的第一个探测器在另一个仓库里**。 +- libxpkg 侧 0.0.55 带了 593 行新 `test_executor.cpp`,但测的是 executor 的 C++ 面; + `pkginfo.lua` 的 Lua 行为**没有任何测试**,而这次改的就是它。 + +### 1.7 修复方案 + +**Fix A(libxpkg,根因)——按答案唯一性判定,而不是按问题具体度** + +`M.resolved_dep` 的 bare 分支: + +1. 删掉 `_is_exact_store_version(dep_version)` 前置(它不消歧,只拒绝合法查询)。 +2. 版本半边按**区间**匹配,不按字面量相等 —— 这是 recipe 实际写的语法, + 解析器早就支持(同一形态的旧 bug:xlings#481,`>=2.38` 当字面量比导致匹配不到节点)。 +3. 唯一性守卫保留,并且**说出撞了谁**。 + +实测(`fixA` 列):6/7 → 2/7。歧义与区间行为单独验过: + +| 场景 | 结果 | +|---|---| +| 两个 provider 都叫 bare `zlib` | **nil + 命名双方**(失败关闭)✅ | +| 换成 `compat:zlib` | 正确解析 ✅ | +| `dep_install_dir("glibc", ">=2.39")` → 记录是 2.44 | 命中 ✅ | +| `dep_install_dir("glibc", "2.39")` → 记录是 2.44 | nil ✅ | + +**Fix B(xim-pkgindex)——recipe 按声明的坐标提问** + +`gcc/llvm/meson/graphics` 全部改成 namespaced(`"xim:glibc"`、`"xim:python"`、`"xim:nvidia-gl-host-link"`)。 +这**单独就能修好这四条**(对照行已证),但它是**卫生**不是根因修复:Fix A 不做, +下一条写 bare 的 recipe 还会再踩,而且 godot / clangd 那两条它修不了。**两个都要做。** + +**Fix C(剩下 2 条,需要你拍板)** + +这两条是**真正不同的问题**,不该被 Fix A 顺手糊过去: + +- `godot → mesa`:问的是**传递**依赖的 payload。选项: + (a) godot 直接声明 `xim:mesa@>=25`(它确实用 mesa 的 libdir,声明是诚实的); + (b) xlings 把传递依赖也记进 `resolved_deps`。 + **倾向 (a)** —— (b) 会把 `resolved_deps` 从「我声明了什么」变成「解析器装了什么」,是更大的语义变更。 + **注意这条现在是静默的**:`have_stack=false` → godot 悄悄回落宿主 GL 目录, + 正好是 recipe 注释里说它要防的 mcpp#352(`GLIBC_2.43 not found`)。 +- `clangd → llvm-tools`:hook 里 `pkgmanager.install()` 装的,本来就没有记录。 + 应当走 `tool_payload_dir`(它保留了独立的 scan 路径),而不是 `dep_install_dir`。 + +**Fix D(护栏)——这一类不能再靠下游发现** + +1. **libxpkg**:把本文的 harness 收成 `pkginfo.lua` 的真单测,覆盖上表 7 种调用形状 × 有/无 roots。 + 这是这次唯一能在改动当天就拦住的东西。 +2. **xim-pkgindex**:结构化 lint —— 每个 `pkginfo.dep_install_dir(<字面量>)` 的第一参数 + 必须带命名空间,且必须出现在该 recipe 的 `deps` 里。要**解析** Lua,不要正则 + (regex 改 recipe 「能解析、但意思变了」是本仓库的老账)。 +3. **xlings**:`fresh-install` CI 加一条真·冷装 `xim:gcc@16.1.0`。 + +**发布顺序**:libxpkg(Fix A + 单测)→ bump xlings `mcpp.lock` → xlings patch 版本 → +xim-pkgindex(Fix B/C)→ 通知 mcpp 把 pin 提回。 + +--- + +## 2. #525 —— graphics 栈装好后 GLX 仍无 FBConfig + +### 2.1 实测:GLX 侧没有 `egl_vendor.d` 的对应物,也不可能有 + +在本机宿主 libglvnd 上直接读符号表: + +``` +$ strings /lib/x86_64-linux-gnu/libGLX.so.0 | grep -iE 'vendor|libGLX_' +libGLX_%s.so.0 +__GLX_VENDOR_LIBRARY_NAME +__GLX_FORCE_VENDOR_LIBRARY_%d + +$ strings /lib/x86_64-linux-gnu/libEGL.so.1 | grep -iE 'vendor|library_path' +__EGL_VENDOR_LIBRARY_DIRS +__EGL_VENDOR_LIBRARY_FILENAMES +/etc/glvnd/egl_vendor.d:/usr/share/glvnd/egl_vendor.d +library_path +``` + +对比是彻底的: + +| | EGL | GLX | +|---|---|---| +| 发现机制 | JSON 目录(环境变量可改) | **无** | +| 定位方式 | JSON 里的 `library_path`,**可以是绝对路径** | `dlopen("libGLX_.so.0")`,**只有 SONAME** | +| 环境变量能给什么 | **目录** | **只有名字** | + +所以 issue 里那句「设 `__GLX_VENDOR_LIBRARY_NAME=nvidia` 也没用——名字对了,库仍然找不到」 +是**结构性的**,不是配置漏了:glvnd 从来没给过 GLX 一个说「去哪里找」的接口。 + +**这也顺带解释了 EGL 为什么一直是好的**,而它的好是**误导性**的: +`mesa.lua:161-166` 和 nvidia 的 vendor JSON 都把 SONAME **改写成了绝对路径**。 +绝对路径对「谁加载了 libEGL、它的 RUNPATH 是什么」完全免疫。 +**EGL 通过是因为它走了一条 GLX 没有的路,不是因为发现层配好了。** +`libs/graphics.lua:90-94` 的 `DISCOVERY` 表里三个变量(`LIBGL_DRIVERS_PATH`、 +`__EGL_VENDOR_LIBRARY_DIRS`、`XDG_DATA_DIRS`)—— 没有一个是给 GLX 的,因为没有可给的。 + +### 2.2 那条 dlopen 只有四条搜索路径,我们只能用一条 + +`libGLX.so.0` 内部 `dlopen("libGLX_nvidia.so.0")`,glibc 的顺序: + +| # | 路径 | 在我们这儿 | +|---|---|---| +| 1 | **调用方对象**的 DT_RPATH(且调用方无 DT_RUNPATH 时**沿加载链向上传递**) | ✅ **唯一可用的** | +| 2 | `LD_LIBRARY_PATH` | ❌ 进程全局,会毒死宿主二进制(issue 里的 `timeout: __pointer_chk_guard` 就是它;xim 自己的安装警告说的也是它) | +| 3 | **调用方对象**的 DT_RUNPATH(**不传递**) | ✅ 可用,但必须写在 `libGLX.so.0` 自己身上 | +| 4 | `ld.so.cache` / 默认目录 | ❌ 我们的 ld.so 的 cache 路径在任何地方都不存在(INTERP 切换后宿主兜底就没了) | + +**结论:可达性必须由执行 dlopen 的那个对象自己携带。** 那个对象是 `libGLX.so.0` —— 是我们的文件 +(`xim:libglvnd`),所以我们能改。这一点 2026-08-06 已经用 A/B/C 实测过: +调用方带 DT_RPATH 或 DT_RUNPATH 都能解析,两者都行;调用方什么都不带就失败。 + +### 2.3 被加载的 `libGLX.so.0` 是我们的,但它的 RPATH 只有 `$ORIGIN` + +`compat.glx-runtime` 不是宿主直通,而是**从 SubOS view 里挑选并 symlink 出来的桥** +(mcpp 自己的设计文档:「仍需要从 SubOS view 选择并 symlink GL 库到自己的 runtime 目录…… +这是过渡桥,而不是最终模型」)。所以程序加载到的 `libGLX.so.0` **确实是我们 libglvnd 的载荷**。 + +问题不在于加载到了谁,而在于: + +- `libGLX.so.0` 在 `/xim-x-libglvnd//lib/`,RPATH 是 `$ORIGIN`; +- `libGLX_nvidia.so.0` 在 `/xim-x-nvidia-gl-host-link/0.1.1/lib/`,**另一个载荷目录**; +- `/lib` 把两者并到一起,但**没有任何东西把这个目录写进 libGLX.so.0 的搜索路径**。 + +`$ORIGIN` 展开的是**符号链接解析之后**的真实目录,所以走 subos farm 的符号链接也救不了。 + +而消费者(app)的 RUNPATH 里**即使有** `glx_runtime/lib`(里面可能同时有 vendor 的符号链接), +也帮不上忙 —— **DT_RUNPATH 不传递**,服务不了下游对象发起的 dlopen。这正是陷阱所在。 + +### 2.4 探针为什么一直是绿的 —— 这就是引入点 + +`xim-pkgindex/.agents/tools/graphics/glxprobe.c:15-28`: + +```c +// BUILD REQUIREMENT -- DT_RPATH, not DT_RUNPATH: +// gcc -o glxprobe glxprobe.c -ldl \ +// -Wl,-rpath,/lib -Wl,--disable-new-dtags +// +// glvnd's dlopen of the vendor is served by the CALLING object's search +// path, and libGLX.so.0's own RPATH is `$ORIGIN` -- it cannot see the vendor +// package. What makes it resolve is that DT_RPATH is searched transitively up +// the load chain to the executable ... build the same probe with +// --enable-new-dtags (the default on many distros) and glvnd finds no vendor +// at all +``` + +`verify-stack.sh:166`、`verify-host-link.sh:110,133` 也都带 `-Wl,--disable-new-dtags`。 + +也就是说: + +- 缺陷被**准确地识别了**(注释把失败模式、原因、默认值风险全写对了); +- 然后被**当成对消费者的前置条件**接受,而不是当成我们载荷上的缺口去补; +- 于是**生态里每一条 GLX 绿灯,都是靠一个真实消费者不会传的链接参数换来的**。 + +mcpp 用默认 `--enable-new-dtags` 发 `-Wl,-rpath`(`flags.cppm:711-720`),得到 DT_RUNPATH, +不传递 —— 前置条件破了,vendor 找不到,glvnd 吞掉 dlopen 错误,用户看到的就是 +`GLX: No GLXFBConfigs returned`。 + +**mcpp 没做错任何事。** 把可达性挂在消费者链接参数上,这个契约本身就不成立。 + +### 2.5 修复方案 + +**Fix E(根因,xim-pkgindex)——`xim:libglvnd` 自带 GLX vendor 搜索路径** + +1. `libs/graphics.lua` 增加 `graphics.GLX_VENDOR_DIR`,一个**只放 `libGLX_*.so.0` 符号链接**的目录。 + **必须放在 `usr/` 下**(例如 `usr/lib/glx-vendor`):xlings 只允许 `usr`/`etc`/`share` 开头的 + file asset 目标,被拒绝**不报错、只是不发生** —— `dst = "lib/dri"` 那次就是这么变成 llvmpipe 的 + (`libs/graphics.lua:63-74` 已记录这条规则和那次事故)。 +2. vendor 包(`nvidia-gl-host-link`、`mesa`、`wsl-gl-host-link`)在 config 时把自己的 + GLX 入口点链进这个目录 —— 与它们已经在做的 `graphics.declare_egl_vendor()` 对称。 +3. `libglvnd` 的 config hook 给 `libGLX.so.0` 写上指向该目录的 **DT_RUNPATH**。 + + 为什么是 RUNPATH 而不是 RPATH:调用方自带时两者都能服务这条 dlopen(2026-08-06 A/B/C 实测), + 而 RUNPATH **不传递** —— 它不会渗进 vendor 库自己的依赖查找,那部分由 interposer 单独控制 + (`nvidia-gl-host-link.lua:418-455` 明确需要 DT_RPATH,是**另一个对象**上的另一条规则)。 + **不要**把 `/lib` farm 整个塞进去:farm 里有 `libc.so.6` / `ld-linux` 符号链接, + 进 RPATH 是已知地雷;专用目录从结构上避开了它。 + + 实现入口:`elfpatch.set({rpath=...})` 的 `rpath` 参数**从不被读取**, + 唯一接受 caller rpath 的是 `elfpatch.patch_elf_loader_rpath`。成功判定用 `r.patched > 0` + (缺 patchelf 时返回 `{0,0,0}`,按真值判会把「没发生」当成功)。 + +**Fix F(护栏)——探针必须按真实消费者的方式构建** + +`glxprobe` / `verify-stack.sh` / `verify-host-link.sh` **去掉 `--disable-new-dtags`**,改用默认。 +这一条现在就应该做,且**做完立刻会变红** —— 那就是它该有的样子: +现在的绿灯是假的,Fix E 落地后它才应该真绿。 +再加一条**显式**的 `--disable-new-dtags` 变体,用来区分「vendor 不可达」和「其它环节坏了」。 + +**Fix G(边界,与 mcpp 协同)** + +Fix E 落地后 `compat.glx-runtime` 的 symlink bridge 就不再是可达性的必要条件了 +(mcpp 自己的路线图里它本来就标着「过渡桥,最终应弃用」)。 +建议顺序:xlings 侧先修好并实测,**再**推动 mcpp 弃用桥 —— 反过来会在没有替代品的时候把 GL 彻底断掉。 + +### 2.6 需要在实测中确认的两点 + +以下两条来自 2026-08-06 的记录,写进方案前应当在装好栈的机器上复核(本机没装 graphics 栈,无法就地验证): + +1. `xim:libglvnd` 载荷里 `libGLX.so.0` 的 RPATH **当前确实只有 `$ORIGIN`**(`readelf -d`)。 +2. 给它加上 vendor 目录的 DT_RUNPATH 后,**默认 dtags** 构建的 imgui/GLFW 程序能拿到 FBConfig。 + 判据用 `/proc/self/maps` 里 GL 对象的实际路径,**不要**用 `glxinfo -B` 的 renderer 字符串 —— + renderer 字符串在「我们的栈生效」和「整条链都来自 `/usr/lib`」两种情况下**完全一样**。 + +--- + +## 3. 共同的形态 + +两条 issue 都不是「写错了一行」,而是同一个判断失误的两次发作: + +> **用一个比真实条件更强的前提去验证机制,然后把那个前提当成已经成立。** + +- **#524**:新规则要求「namespaced + 精确版本」。它在**作者手写的调用**下成立, + 在 index 里 7 个真实调用点中 6 个不成立。收紧规则时没有去枚举调用点。 +- **#525**:机制要求「消费者带 DT_RPATH」。它在**手工加 `--disable-new-dtags` 的探针**下成立, + 在任何用默认参数的真实构建系统下不成立。而这条前提被写进注释、当成了契约。 + +两者的失败方式也一致,都是本仓库反复出现的**沉默**: +`dep_install_dir` 返回 nil 不说话(0.0.56 才补上一句),glvnd 吞掉 dlopen 错误只报 `No GLXFBConfigs`。 +**「没找到」和「没发生」产生了相同的输出。** + +对应的通用护栏,两条都指向同一件事:**验证必须用消费者真实的形状**—— +真实的调用写法、真实的链接参数、真实的冷 home。 + +--- + +## 4. 验收判据(可证伪) + +| # | 命令 | 通过条件 | +|---|---|---| +| A1 | `lua5.4 repro_dep_install_dir.lua pkginfo-.lua` | 7 个调用点中 ≥5 个 ok;剩下的只能是 godot/clangd 且各带一条**命名了原因**的诊断 | +| A2 | 歧义用例(两个 provider 同 bare 名) | nil **且**日志点名双方 | +| A3 | 冷 home(全新 `XLINGS_HOME`,`--mirror CN`)`xlings install -y xim:gcc@16.1.0` | 退出 0,`gcc/g++/c++` 三个 shim 都注册 | +| A4 | 同一冷 home `xlings install -y meson` | 退出 0 | +| A5 | `readelf -d /lib/libGLX.so.0` | RUNPATH 含 GLX vendor 目录 | +| A6 | imgui 模板,**默认 dtags** 构建后 `mcpp run` | 出窗口;且 `/proc/self/maps` 里 GL 对象路径在我们的载荷下 | +| A7 | `verify-stack.sh`(去掉 `--disable-new-dtags`) | Fix E 之前**红**,之后**绿** | + +A7 是这次最重要的一条:**它现在必须先红**,否则说明改的不是这个缺陷。 + +--- + +## 5. 已定的决策(2026-08-10) + +| # | 决定 | 结论 | +|---|---|---| +| **D1** | godot → mesa | ✅ **godot 直接声明 `xim:mesa@>=25`**,并按 namespaced 坐标提问。它确实用 mesa 的 libdir,声明是诚实的;不走「把传递依赖记进 `resolved_deps`」那条路——那会把 `resolved_deps` 从「我声明了什么」变成「解析器装了什么」,是更大的语义变更 | +| **D2** | libxpkg 版本 | ✅ **仍叫 0.0.56**。实测 0.0.56 从未发布:mcpp-index 的 `xpkg.lua` 最高 `0.0.55`,git tag 最高 `0.0.55`,`71b9ed7` 只活在未合并的 `fix/dep-install-dir-fallback` 上。所以在该分支上追加 Fix A,一个 PR 发一个 0.0.56 —— 既保留它那批**诊断**(Fix A 之后它们只在真正无解的查询上响),又不制造「同号不同树」的幽灵版本 | +| **D3** | GLX vendor 目录形态 | ✅ **E1** —— 目录放 libglvnd **自己的 payload 内**,`$ORIGIN/glx-vendor`,home 级装齐所有 vendor,per-subos 由 `__GLX_VENDOR_LIBRARY_NAME` 选。理由:vendor 载荷本来就是 per-home 的;「哪个 subos 用哪个 vendor」glvnd 已经有 per-subos 选择器,不需要再用目录表达。E2(subos 绝对路径)会把共享 payload 钉死在一个 subos 上;E3(per-subos stub)不跨包写但多一层,且依赖真 libGLX 保持 DT_RPATH | +| **D4** | 发布方式 | ✅ **#524 与 #525 合并成一轮发布**。#524 是硬失败(任何冷 home 装不上 gcc 和 meson:新机器、CI 冷缓存、`fresh-install`、全部下游 mcpp 构建),必须尽快;#525 同属一个形态,一起发省一轮生态传播 | +| **D5** | 与 mcpp 的桥 | ✅ **串行**:xlings 侧先修好并实测,**再**推动 mcpp 弃用 `compat.glx-runtime` 的 symlink bridge。并行会出现「桥没了、RUNPATH 还没到」的窗口,GL 会完全不可用 | + +**E1 的已知代价**(记录在案,不粉饰):vendor 包要往 libglvnd 的 store 目录里写符号链接, +这是**跨包写**。落地时必须确认 integrity 清理不会把它当孤儿删掉——这条进验收(见 §4 A5)。 + +实施计划与任务依赖:`.agents/plans/2026-08-10-issue-524-525-implementation.md` diff --git a/.agents/plans/2026-08-10-issue-524-525-implementation.md b/.agents/plans/2026-08-10-issue-524-525-implementation.md new file mode 100644 index 00000000..f40bdfee --- /dev/null +++ b/.agents/plans/2026-08-10-issue-524-525-implementation.md @@ -0,0 +1,142 @@ +# #524 / #525 实施计划 —— 任务拆分、依赖关系、跨仓库协作 + +> 设计与根因:`.agents/docs/2026-08-10-issue-524-525-root-cause-and-fix-design.md` +> +> 日期:2026-08-10 | 决策见该文档 §5(D1–D5) +> +> 仓库:`openxlings/libxpkg`、`openxlings/xlings`、`openxlings/xim-pkgindex` +> (+ `mcpplibs/mcpp-index` 与 gitcode 镜像作为发布链的中转) + +--- + +## 1. 八个角度的取舍 + +这一轮改的是**两条被当成契约的错误前提**,所以每个角度的判据都是「契约现在由谁承担」。 + +**架构。** 两条 issue 的共同点是**把责任放在了拿不到信息的一侧**。 +`dep_install_dir` 要求调用方重述解析器已经知道的版本;GLX 可达性要求消费者传一个它不知道要传的链接参数。 +两处都改成**由掌握信息的一侧承担**:`resolved_deps` 自己判定唯一性;`libGLX.so.0` 自己带搜索路径。 +E1 进一步把 vendor 目录放进 libglvnd 的 payload,用 `$ORIGIN` 表达——**位置信息由文件自身携带,不由环境表达**。 + +**稳定性。** 三条防线,都在缺陷发生的那一层: +libxpkg 侧的 Lua 单测(今天是 0,本轮补齐;0.0.55 的 593 行新测试全在 C++ executor 面,改的却是 Lua); +index 侧的结构化 lint(调用形状与声明必须一致); +xlings 侧的**冷** home e2e(热 home 跳过安装、根本不跑 config hook,这正是 CI 看不见 #524 的原因)。 + +**优雅简洁。** Fix A 是**净删除**一个判据(`_is_exact_store_version` 前置),不是新增分支—— +唯一性守卫本来就在,它一直在做正确的事。 +Fix E1 与已有的 `declare_egl_vendor()` 完全对称,`GLX_VENDOR_DIR` 与 `EGL_VENDOR_DIR` 并列,不引入新概念。 + +**用户体验。** 目标是**没有失败可看**。退而求其次,失败必须指向可执行的下一步: +撞名时点名撞了谁(而不是「找不到」),GLX 不可达时说的是哪一个 vendor 目录空着。 +顺手删掉 `gcc.lua:545` 那条硬编码的 `xim:glibc@2.39` —— 它把 issue 报告人引到了一条假线索上。 + +**兼容性。** Fix A **只放宽不收紧**:0.0.55/0.0.56 能解析的,之后全部照旧能解析; +歧义依旧失败关闭。没有 roots 字段的老客户端走原来的 scan 路径,完全不变。 +E1 只往 libglvnd payload 里**加**一个目录、往 `libGLX.so.0` 上**加**一条 RPATH, +没装 graphics 栈的 home 里那个目录是空的,`dlopen` 照旧落到原来的路径上。 + +**跨平台。** `dep_install_dir` 是全平台的,Fix A 的改动不含任何平台分支。 +E1 是 **linux-only** 的(glvnd 只在 linux),必须用平台条件包住, +不能出现「macOS/Windows 上多出一个空目录 + 一次 patchelf 失败」。 +lint 与 e2e 要按平台枚举 recipe 的 `xpm.` 段,不能只看 linux 那一段。 + +**一致性。** 「一个问题一个回答者」:`resolved_deps` 是依赖坐标的唯一回答者, +`tool_payload_dir` 是 libxpkg 自身工具的唯一回答者(clangd 的 `llvm-tools` 归它,不归前者); +GLX vendor 目录是 GLX vendor 可达性的唯一回答者。 +godot 不再问一个它没声明的包——**要用就要声明**。 + +**无感升级。** 这是硬约束:**已经装好的 home 升级后不能变红、不能要求重装**。 +- Fix A 纯放宽,老 home 的任何已装包不受影响。 +- E1 的 RPATH 写在 libglvnd 的 config hook 里,只有**安装/重装 libglvnd 时**才执行。 + 存量 home 里 `libGLX.so.0` 还是老的 RPATH → GLX 依旧不可达 → **这就是要解决的那个问题本身**。 + 所以 libglvnd 必须**换版本键**(同 artifact、新版本),让消费者的 `>=` 下界把它拉起来重跑 config, + 这是 fontconfig 2.15.0.1 用过的模式。**不这样做,存量 home 拿不到修复,而且没有任何提示。** + → 落到 **T7 的验收里,必须在一个「已装老 graphics 栈」的 home 上验证升级路径。** + +--- + +## 2. 任务 DAG + +``` + ┌───────────────────────────────────────────┐ + │ T1 同步/分支/设计doc/计划 (done) │ + └───────────────┬───────────────────────────┘ + ┌───────────────┬───────┴────────┬──────────────┬─────────────┐ + ▼ ▼ ▼ ▼ ▼ + ┌───────────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐ + │T2 libxpkg │ │T5 index │ │T7 index E1 │ │T8 index │ │T10 xlings│ + │ Fix A │ │ Fix B │ │ GLX vendor │ │ Fix F │ │ e2e+tool│ + └───────┬───────┘ ├──────────┤ └──────┬───────┘ │ probes │ │ +docs │ + ┌───────▼───────┐ │T6 index │ │ └────┬─────┘ └────┬─────┘ + │T3 libxpkg │ │ Fix C │ │ │ │ + │ Lua tests │ ├──────────┤ └──────────────┘ │ + └───────┬───────┘ │T9 index │ F 必须先红、E1 后绿 │ + ┌───────▼───────┐ │ lint │ │ + │T4 libxpkg │ └──────────┘ │ + │ 0.0.56 PR │ │ + └───────┬───────┘ │ + │ │ + └──────────────────────┬────────────────────────────────────┘ + ▼ + ┌────────────────────────┐ + │T11 xlings bump+PR+CI │ + └────────────┬───────────┘ + ▼ + ┌────────────────────────┐ + │T12 发布链 │ + │ libxpkg tag → gtc → │ + │ mcpp-index → xlings rel │ + └────────────┬───────────┘ + ▼ + ┌────────────────────────┐ + │T13 subos --sandbox 实测 │ + └────────────────────────┘ +``` + +**可并行的**:T2/T3(libxpkg)与 T5/T6/T7/T8/T9(index)与 T10(xlings)三条线互不阻塞。 +**真串行的只有三处**:T4→T11(xlings 要消费已发布的 libxpkg)、T11→T12(发布链有严格顺序)、T12→T13(对着发布产物验)。 + +**index 侧的三条 PR 内顺序**(同一 PR,但提交顺序有意义): +T8(探针去掉 `--disable-new-dtags`)先落 → **它会红** → T7(E1)再落 → **它转绿**。 +这个红→绿是本轮唯一能证明「修的就是这个缺陷」的判据;顺序反了就只剩一句自我声明。 + +--- + +## 3. 跨仓库发布顺序(唯一可行的那个) + +``` +openxlings/libxpkg PR 合并 → git tag 0.0.56 → GitHub 自动 tarball + ↓ +gitcode 镜像 gtc release publish(字节相同的副本,只改名) + ↓ ⚠ 校验用 GET,不能用 HEAD(HEAD 401 / GET 302→200) +mcpplibs/mcpp-index pkgs/x/xpkg.lua —— GLOBAL + CN URL + sha256, + ↓ 三个平台段(linux/macosx/windows)都要写 + ↓ 等 publish-artifact.yml 跑完;客户端有 TTL, + ↓ `rm -rf ~/.mcpp/registry/data/mcpplibs` 强制刷新 +openxlings/xlings mcpp.toml/mcpp.lock 依赖 bump → 版本号 bump → PR → CI → release + ↓ ⚠ bump-index 与 mirror-binaries 都吞自己的失败, + ↓ 永远不要读成绿:直接查 latest.ref 和 gitcode 资源 +openxlings/xim-pkgindex recipes 合并(Fix B/C/E1/F/lint) + ↓ ⚠ index 是构建产物不是 git clone,合并后 CI 仍会取到旧 index +mcpp-community/mcpp 把 xlings pin 从 2026.8.9.2 提回(由 mcpp 侧执行) +``` + +**index 侧为什么放在 xlings 之后**:Fix B/C 依赖 Fix A 才生效;先合 index、后发 xlings, +中间窗口里 index 已经改成 namespaced 坐标而客户端还是老 libxpkg —— 那条路径本来就是通的(对照行已证), +所以**这个顺序是安全的**,但反过来(先发 xlings 后合 index)窗口更短,优先。 + +--- + +## 4. 验收(全部可证伪,判据见设计文档 §4) + +| # | 归属 | 判据 | +|---|---|---| +| A1/A2 | T3 | `repro-dep-install-dir.sh` ≥5/7 ok;撞名 nil **且**点名双方 | +| A3/A4 | T13 | 冷 home `xlings install -y xim:gcc@16.1.0` 退出 0 且注册 3 个 shim;`meson` 同样 | +| A5 | T7/T13 | `readelf -d libGLX.so.0` 含 vendor RPATH;**且** integrity 清理没删掉跨包写入的符号链接 | +| A6 | T13 | imgui 模板**默认 dtags** 构建后出窗口;判据是 `/proc/self/maps` 里 GL 对象的路径,**不是** `glxinfo` 的 renderer 字符串 | +| A7 | T8→T7 | `verify-stack.sh` 去掉 `--disable-new-dtags` 后:E1 之前**红**,之后**绿** | +| A8 | T7 | **存量 home 升级路径**:已装老 graphics 栈的 home,升级后 GLX 可达(靠 libglvnd 换版本键 + 消费者 `>=` 下界) | + +A7 与 A8 是这一轮最容易被跳过、也最能说明问题的两条。 diff --git a/.agents/tools/repro-dep-install-dir.lua b/.agents/tools/repro-dep-install-dir.lua new file mode 100644 index 00000000..55c85d07 --- /dev/null +++ b/.agents/tools/repro-dep-install-dir.lua @@ -0,0 +1,188 @@ +-- Reproduction harness for openxlings/xlings#524. +-- +-- Loads the REAL pkginfo.lua (path given as arg[1]) against a stubbed xmake +-- stdlib and a fake xpkgs store, then replays the exact call shapes the +-- published recipes use. No xlings, no network, no install. + +local PKGINFO = arg[1] or error("usage: lua repro.lua ") +local LABEL = arg[2] or PKGINFO + +-- ── fake filesystem ────────────────────────────────────────────────── +-- A cold home immediately after the resolver installed gcc's deps: +-- glibc 2.44 and binutils 2.42 are on disk, gcc's own dir exists. +local STORE = "/home/u/.xlings/data/xpkgs" +local DIRS = { + [STORE] = true, + [STORE .. "/xim-x-glibc"] = true, + [STORE .. "/xim-x-glibc/2.44"] = true, + [STORE .. "/xim-x-binutils"] = true, + [STORE .. "/xim-x-binutils/2.42"] = true, + [STORE .. "/xim-x-gcc"] = true, + [STORE .. "/xim-x-gcc/16.1.0"] = true, + [STORE .. "/xim-x-mesa"] = true, + [STORE .. "/xim-x-mesa/25.0.7.2"] = true, + [STORE .. "/xim-x-nvidia-gl-host-link"] = true, + [STORE .. "/xim-x-nvidia-gl-host-link/0.1.1"] = true, + [STORE .. "/xim-x-python"] = true, + [STORE .. "/xim-x-python/3.12.4"] = true, + [STORE .. "/xim-x-llvm-tools"] = true, + [STORE .. "/xim-x-llvm-tools/20.1.0"] = true, +} +local CHILDREN = { + [STORE] = { "xim-x-glibc", "xim-x-binutils", "xim-x-gcc", "xim-x-mesa", + "xim-x-nvidia-gl-host-link", "xim-x-python", "xim-x-llvm-tools" }, + [STORE .. "/xim-x-glibc"] = { "2.44" }, + [STORE .. "/xim-x-binutils"] = { "2.42" }, + [STORE .. "/xim-x-mesa"] = { "25.0.7.2" }, + [STORE .. "/xim-x-nvidia-gl-host-link"] = { "0.1.1" }, + [STORE .. "/xim-x-python"] = { "3.12.4" }, + [STORE .. "/xim-x-llvm-tools"] = { "20.1.0" }, +} + +-- ── xmake-ish stdlib stubs ─────────────────────────────────────────── +path = { + join = function(...) + local parts = {} + for _, p in ipairs({...}) do + if p and p ~= "" then parts[#parts + 1] = tostring(p) end + end + return (table.concat(parts, "/"):gsub("//+", "/")) + end, + filename = function(p) return (tostring(p):gsub(".*/", "")) end, + directory = function(p) return (tostring(p):gsub("/[^/]*$", "")) end, +} +local _os = os +os = setmetatable({ + isdir = function(p) return DIRS[tostring(p)] == true end, + isfile = function() return false end, + host = function() return "linux" end, + dirs = function(pattern) + local base = tostring(pattern):gsub("/%*$", "") + local out = {} + for _, c in ipairs(CHILDREN[base] or {}) do + out[#out + 1] = base .. "/" .. c + end + return out + end, + getenv = function() return nil end, +}, { __index = _os }) + +local MESSAGES = {} +local function record(level) + return function(fmt, ...) + local ok, msg = pcall(string.format, fmt, ...) + MESSAGES[#MESSAGES + 1] = level .. ": " .. (ok and msg or tostring(fmt)) + end +end +_LIBXPKG_MODULES = { + log = { debug = function() end, info = record("info"), + warn = record("warn"), error = record("error") }, +} + +-- ── the runtime the xlings installer hands a config hook ───────────── +-- resolved_deps is keyed by the DECLARED spec, exactly as the resolver +-- records it. gcc declares `xim:glibc@>=2.39`; the resolver picked 2.44. +local function runtime_for(pkg, deps_list, resolved, with_roots) + local rt = { + pkg_name = pkg, + version = "16.1.0", + install_dir = STORE .. "/xim-x-" .. pkg .. "/16.1.0", + xpkg_dir = "/home/u/.xlings/data/xim/pkgindex/pkgs/g", + project_data_dir = "", + deps_list = deps_list, + resolved_deps = resolved, + } + -- xlings >= 2026.8.10.1 always sets this; earlier clients never did. + if with_roots then rt.dependency_store_roots = { STORE } end + return rt +end + +local GCC_RESOLVED = { + ["xim:glibc@>=2.39"] = { + name = "xim:glibc", version = "2.44", + install_dir = STORE .. "/xim-x-glibc/2.44", + }, + ["xim:binutils@2.42"] = { + name = "xim:binutils", version = "2.42", + install_dir = STORE .. "/xim-x-binutils/2.42", + }, +} +local GODOT_RESOLVED = { + ["xim:graphics@>=0.1"] = { + name = "xim:graphics", version = "0.1.0", + install_dir = STORE .. "/xim-x-graphics/0.1.0", + }, +} +local GRAPHICS_RESOLVED = { + ["xim:nvidia-gl-host-link@>=0.1"] = { + name = "xim:nvidia-gl-host-link", version = "0.1.1", + install_dir = STORE .. "/xim-x-nvidia-gl-host-link/0.1.1", + }, +} + +-- ── the cases, as the published recipes actually write them ────────── +local CASES = { + { "gcc.lua:544 dep_install_dir('glibc')", + "gcc", {"xim:glibc@>=2.39", "xim:binutils@2.42"}, GCC_RESOLVED, + "glibc", nil, STORE .. "/xim-x-glibc/2.44" }, + { "llvm.lua:210 dep_install_dir('glibc')", + "llvm", {"xim:glibc@>=2.39"}, GCC_RESOLVED, + "glibc", nil, STORE .. "/xim-x-glibc/2.44" }, + { "graphics.lua:176 dep_install_dir('nvidia-gl-host-link')", + "graphics", {"xim:nvidia-gl-host-link@>=0.1"}, GRAPHICS_RESOLVED, + "nvidia-gl-host-link", nil, STORE .. "/xim-x-nvidia-gl-host-link/0.1.1" }, + { "godot.lua:351 dep_install_dir('mesa') [transitive, not declared]", + "godot", {"xim:graphics@>=0.1"}, GODOT_RESOLVED, + "mesa", nil, STORE .. "/xim-x-mesa/25.0.7.2" }, + { "meson.lua:116 dep_install_dir('python') [raise() on nil]", + "meson", {"xim:python@>=3.9"}, + { ["xim:python@>=3.9"] = { name="xim:python", version="3.12.4", + install_dir = STORE .. "/xim-x-python/3.12.4" } }, + "python", nil, STORE .. "/xim-x-python/3.12.4" }, + { "clangd:245 dep_install_dir('llvm-tools', ver) [pkgmanager-installed]", + "mcpp-vscode-clangd", {"xim:mcpp"}, {}, + "llvm-tools", "20.1.0", STORE .. "/xim-x-llvm-tools/20.1.0" }, + { "proposed fix: dep_install_dir('xim:glibc')", + "gcc", {"xim:glibc@>=2.39", "xim:binutils@2.42"}, GCC_RESOLVED, + "xim:glibc", nil, STORE .. "/xim-x-glibc/2.44" }, +} + +print(("═"):rep(78)) +print("pkginfo under test: " .. LABEL) +print(("═"):rep(78)) +print(string.format("%-52s %-11s %s", "call site", "no roots", "roots set")) +print(("─"):rep(78)) + +local fails = 0 +for _, c in ipairs(CASES) do + local desc, pkg, deps, resolved, name, ver, want = table.unpack(c) + local results = {} + for _, with_roots in ipairs({ false, true }) do + MESSAGES = {} + _RUNTIME = runtime_for(pkg, deps, resolved, with_roots) + package.loaded["pkginfo_under_test"] = nil + local chunk = assert(loadfile(PKGINFO)) + local M = chunk() + local ok, got = pcall(M.dep_install_dir, name, ver) + if not ok then + results[#results + 1] = "RAISED" + elseif got == want then + results[#results + 1] = "ok" + elseif got == nil then + results[#results + 1] = "nil" + else + results[#results + 1] = "WRONG" + end + if with_roots then + results.msgs = MESSAGES + if got ~= want then fails = fails + 1 end + end + end + print(string.format("%-52s %-11s %s", desc, results[1], results[2])) + for _, m in ipairs(results.msgs or {}) do + print(" └─ " .. m:sub(1, 120)) + end +end +print(("─"):rep(78)) +print(string.format("with dependency_store_roots set: %d/%d call sites broken", + fails, #CASES)) diff --git a/.agents/tools/repro-dep-install-dir.sh b/.agents/tools/repro-dep-install-dir.sh new file mode 100755 index 00000000..00ea526b --- /dev/null +++ b/.agents/tools/repro-dep-install-dir.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# openxlings/xlings#524 -- offline reproduction of the dep_install_dir regression. +# +# Loads the REAL pkginfo.lua from a libxpkg checkout, at each revision named on +# the command line, and replays the call shapes the published recipes actually +# use. No xlings, no network, no install, no home touched. +# +# ./repro-dep-install-dir.sh # 0.0.54 / 0.0.55 / 0.0.56 +# ./repro-dep-install-dir.sh HEAD my-fix-branch # any revs you like +# ./repro-dep-install-dir.sh worktree # UNCOMMITTED working tree +# +# `worktree` matters: every other form reads the file out of git, so an +# uncommitted fix silently tests as if it were not there -- which reads as +# "my fix does nothing" rather than "I forgot to commit". +# +# LIBXPKG env var overrides the checkout location. +set -euo pipefail + +here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +libxpkg=${LIBXPKG:-$here/../../../libxpkg} +[[ -d $libxpkg/.git ]] || { echo "no libxpkg checkout at $libxpkg (set LIBXPKG=)" >&2; exit 2; } + +# Default revs are the two that bracket the regression plus the diagnostics-only +# follow-up, resolved by tag-in-subject rather than by hash so this keeps working. +if (( $# )); then revs=("$@"); else + revs=() + for tag in 0.0.54 0.0.55 0.0.56; do + r=$(git -C "$libxpkg" log --all --format='%H %s' \ + | grep -m1 -F "($tag)" | cut -d' ' -f1) || true + [[ -n ${r:-} ]] && revs+=("$r@$tag") + done +fi + +work=$(mktemp -d); trap 'rm -rf "$work"' EXIT +for spec in "${revs[@]}"; do + rev=${spec%%@*}; label=${spec#*@}; [[ $label == "$rev" ]] && label=$rev + if [[ $rev == worktree ]]; then + cp "$libxpkg/src/lua-stdlib/xim/libxpkg/pkginfo.lua" "$work/pkginfo.lua" + [[ $label == worktree ]] && label="worktree (uncommitted)" + elif ! git -C "$libxpkg" show "$rev:src/lua-stdlib/xim/libxpkg/pkginfo.lua" \ + > "$work/pkginfo.lua" 2>/dev/null; then + echo "!! cannot read pkginfo.lua at $rev -- skipped" >&2; continue + fi + lua5.4 "$here/repro-dep-install-dir.lua" "$work/pkginfo.lua" "libxpkg $label" + echo +done diff --git a/mcpp.lock b/mcpp.lock index 4723ac8c..e7a48352 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -17,6 +17,12 @@ version = "3.8.7" source = "index+compat@3.8.7" hash = "fnv1a:e412742c0533bef7" +[package."compat.lua"] +namespace = "compat" +version = "5.4.7" +source = "index+compat@5.4.7" +hash = "fnv1a:6bda787e2c850eac" + [package."compat.lz4"] namespace = "compat" version = "1.10.0" @@ -59,12 +65,6 @@ version = "6.1.9" source = "index+mcpplibs@6.1.9" hash = "fnv1a:b3ae3d5c6e5a7a91" -[package."lua"] -namespace = "mcpplibs" -version = "5.4.7" -source = "index+mcpplibs@5.4.7" -hash = "fnv1a:baac1cce64264c57" - [package."mcpplibs.tinyhttps"] namespace = "mcpplibs" version = "0.2.9" @@ -73,9 +73,9 @@ hash = "fnv1a:3465dd0bd5d7aa20" [package."mcpplibs.xpkg"] namespace = "mcpplibs" -version = "0.0.55" -source = "index+mcpplibs@0.0.55" -hash = "fnv1a:1f2ae5d41f7d9bb2" +version = "0.0.56" +source = "index+mcpplibs@0.0.56" +hash = "fnv1a:1f00ff8945ea9597" [package."mcpplibs.capi.lua"] namespace = "mcpplibs.capi" diff --git a/mcpp.toml b/mcpp.toml index c3001b59..e936d0e4 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "xlings" -version = "2026.8.10.1" +version = "2026.8.10.2" 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.55" +xpkg = "0.0.56" tinyhttps = "0.2.9" capi.lua = "0.0.3" diff --git a/src/core/config.cppm b/src/core/config.cppm index 53a7810a..3a2b604c 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.10.1"; + static constexpr std::string_view VERSION = "2026.8.10.2"; static constexpr std::string_view REPO = "https://github.com/openxlings/xlings"; }; diff --git a/tests/e2e/config_hook_resolves_declared_dep_test.sh b/tests/e2e/config_hook_resolves_declared_dep_test.sh new file mode 100755 index 00000000..6739b132 --- /dev/null +++ b/tests/e2e/config_hook_resolves_declared_dep_test.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# Regression for #524: a config hook must be able to locate the payload of a +# dependency THE RESOLVER JUST INSTALLED, on a home where neither was present. +# +# Why a cold home is the whole point. On a warm home the consumer is already +# installed, so the install is skipped and the config hook never runs -- which +# is exactly why every CI lane stayed green while `xlings install xim:gcc` was +# broken on every fresh machine. The first detector was a downstream repo's +# cache miss. This test is that detector, in-tree. +# +# The shape is gcc's, minimally: declare a namespaced, RANGED coordinate, let +# the resolver pick a concrete version, then ask for the payload from the +# config hook. Under xlings 2026.8.10.1 + libxpkg 0.0.55/0.0.56 the ask +# returned nil and the hook reported "payload not found" with the payload +# sitting next to it. + +set -euo pipefail + +# shellcheck source=./project_test_lib.sh +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/project_test_lib.sh" + +RUNTIME_DIR="$ROOT_DIR/tests/e2e/runtime/config_hook_resolves_declared_dep" +HOME_DIR="$RUNTIME_DIR/xlings-home" +INDEX_DIR="$RUNTIME_DIR/index" +USER_HOME="$RUNTIME_DIR/user" + +cleanup() { rm -rf "$RUNTIME_DIR"; } +trap cleanup EXIT +cleanup + +XLINGS_BIN="$(find_xlings_bin)" + +mkdir -p "$HOME_DIR/subos/default/bin" "$INDEX_DIR/pkgs/d" "$INDEX_DIR/pkgs/c" "$USER_HOME" +printf 'xim_indexrepos = {}\n' > "$INDEX_DIR/xim-indexrepos.lua" + +cat > "$HOME_DIR/.xlings.json" < "$INDEX_DIR/pkgs/d/depender-payload.lua" <<'LUA' +package = { + spec = "1", + name = "depender-payload", + description = "Payload a consumer's config hook must be able to locate", + type = "package", + archs = {"x86_64", "aarch64"}, + status = "stable", + xpm = { + linux = { ["latest"] = { ref = "1.2.0" }, ["1.0.0"] = {}, ["1.2.0"] = {} }, + macosx = { ["latest"] = { ref = "1.2.0" }, ["1.0.0"] = {}, ["1.2.0"] = {} }, + windows = { ["latest"] = { ref = "1.2.0" }, ["1.0.0"] = {}, ["1.2.0"] = {} }, + }, +} +LUA + +# The consumer. Declares a RANGE, asks three ways in config(): +# +# namespaced the coordinate as declared -- must always work +# bare what every recipe in xim-pkgindex actually writes; the +# record set answers it uniquely, so it must work too +# +# The undeclared case gets its own consumer below, because a hook's output is +# only surfaced when the hook FAILS -- which is also the only moment a user +# reads it. +cat > "$INDEX_DIR/pkgs/c/dep-consumer.lua" <<'LUA' +package = { + spec = "1", + name = "dep-consumer", + description = "Resolves its declared dependency from a config hook", + type = "package", + archs = {"x86_64", "aarch64"}, + status = "stable", + xpm = { + linux = { deps = { "xim:depender-payload@>=1.0" }, + ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = {} }, + macosx = { deps = { "xim:depender-payload@>=1.0" }, + ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = {} }, + windows = { deps = { "xim:depender-payload@>=1.0" }, + ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = {} }, + }, +} + +import("xim.libxpkg.pkginfo") + +function config() + local ns = pkginfo.dep_install_dir("xim:depender-payload") + local bare = pkginfo.dep_install_dir("depender-payload") + local range = pkginfo.dep_install_dir("xim:depender-payload", ">=1.0") + + local f = io.open(path.join(pkginfo.install_dir(), "resolved.txt"), "w") + if not f then return false end + f:write("ns=" .. tostring(ns) .. "\n") + f:write("bare=" .. tostring(bare) .. "\n") + f:write("range=" .. tostring(range) .. "\n") + f:close() + + -- Fail the way gcc fails, so a regression shows up as a failed install and + -- not as a file nobody reads. + return ns ~= nil and bare ~= nil and range ~= nil +end +LUA + +RUN() { + ( cd /tmp && env -i \ + HOME="$USER_HOME" \ + USER=xlings-test \ + SHELL=/bin/sh \ + PATH=/usr/bin:/bin \ + NO_COLOR=1 \ + XLINGS_HOME="$HOME_DIR" \ + "$XLINGS_BIN" "$@" ) +} + +log "materialize the isolated index" +RUN update >/dev/null 2>&1 || fail "failed to materialize the isolated index" + +PAYLOAD_ROOT="$HOME_DIR/data/xpkgs/xim-x-depender-payload" +[[ ! -d "$PAYLOAD_ROOT" ]] \ + || fail "home is not cold: $PAYLOAD_ROOT already exists before the install" + +log "cold install: the resolver must place the dependency, then the hook must find it" +set +e +install_output="$(RUN install xim:dep-consumer@1.0.0 -y 2>&1)" +install_rc=$? +set -e +[[ "$install_rc" -eq 0 ]] \ + || fail "consumer install failed (exit $install_rc) -- this is #524 +$install_output" + +RESULT_FILE="$HOME_DIR/data/xpkgs/xim-x-dep-consumer/1.0.0/resolved.txt" +[[ -f "$RESULT_FILE" ]] || fail "config hook recorded nothing +$install_output" + +EXPECT="$PAYLOAD_ROOT/1.2.0" +ns="$(sed -n 's/^ns=//p' "$RESULT_FILE")" +bare="$(sed -n 's/^bare=//p' "$RESULT_FILE")" +range="$(sed -n 's/^range=//p' "$RESULT_FILE")" + +[[ "$ns" == "$EXPECT" ]] \ + || fail "namespaced query resolved '$ns', expected '$EXPECT'" +[[ "$bare" == "$EXPECT" ]] \ + || fail "bare query resolved '$bare', expected '$EXPECT' -- the record set +answers this uniquely, and every recipe in xim-pkgindex writes it this way" +[[ "$range" == "$EXPECT" ]] \ + || fail "ranged query resolved '$range', expected '$EXPECT' -- the caller +restates the RANGE it declared, never the concrete version the resolver picked" +log "an unanswerable coordinate must fail LOUDLY, naming itself" + +# A second consumer, because a hook's output reaches the user only when the +# hook fails. "Cannot answer" and "the dependency is not installed" produce the +# same nil, and #524 is what happens when they also produce the same message: +# the report chased a missing glibc that was sitting on disk. +cat > "$INDEX_DIR/pkgs/c/dep-stranger.lua" <<'LUA' +package = { + spec = "1", + name = "dep-stranger", + description = "Asks for a coordinate it never declared", + type = "package", + archs = {"x86_64", "aarch64"}, + status = "stable", + xpm = { + linux = { deps = { "xim:depender-payload@>=1.0" }, + ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = {} }, + macosx = { deps = { "xim:depender-payload@>=1.0" }, + ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = {} }, + windows = { deps = { "xim:depender-payload@>=1.0" }, + ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = {} }, + }, +} + +import("xim.libxpkg.pkginfo") + +function config() + return pkginfo.dep_install_dir("xim:never-declared") ~= nil +end +LUA + +RUN update >/dev/null 2>&1 || fail "failed to refresh the isolated index" + +set +e +stranger_output="$(RUN install xim:dep-stranger@1.0.0 -y 2>&1)" +stranger_rc=$? +set -e +[[ "$stranger_rc" -ne 0 ]] \ + || fail "asking for an undeclared coordinate must not report success +$stranger_output" +grep -q "never-declared" <<<"$stranger_output" \ + || fail "the failure never named the coordinate that could not be answered +$stranger_output" +grep -qi "not a declared dependency" <<<"$stranger_output" \ + || fail "the failure must say WHY it cannot answer, not just that it did not +$stranger_output" + +log "PASS: a config hook resolves its declared dependency on a cold home" diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index a9263c50..2e528343 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -130,6 +130,7 @@ TESTS=( "E2E-76 |config_install_no_implicit_dir_test.sh||" "E2E-77 |subos_use_candidates_test.sh||" "E2E-78 |subos_runtime_declaration_upgrade_test.sh||" + "E2E-79 |config_hook_resolves_declared_dep_test.sh||" ) PASS=0; FAIL=0; SOFTFAIL=0 From aa2b758f11e1471986a8b7f0d67e9079a78a7224 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 10 Aug 2026 11:47:08 +0800 Subject: [PATCH 2/2] ci(aarch64): pin mcpp to 2026.8.8.4 until mcpp#406, and say what that costs The cross-build lane went red on main, not just here. mcpp 2026.8.10.1's host toolchain post-install fixup binds `glibc@2.39` while the resolver installs 2.44 -- latest since xim-pkgindex#582 -- and refuses to fall back. Established by re-running ONE unchanged run id across the index bump that made mcpp 2026.8.10.1 latest (xim-pkgindex#588, 01:55Z): the same commit passed with mcpp 2026.8.8.4 at 19:09 and failed with 2026.8.10.1 at 03:41. Same workflow, same runner image. Also reproduces locally against UNMODIFIED libxpkg 0.0.55, which is what rules out this branch as the cause. Filed as mcpp-community/mcpp#406. The pin is temporary and it is not free: this job takes the latest mcpp on purpose, to be the canary for exactly this. Pinning it muzzles the canary, so the comment names the removal condition rather than sitting there as an unexplained version. Until #406 lands, breakage against the newest mcpp is invisible on this lane. Also adds the 2026.8.10.2 release notes. --- .../2026-08-10-release-2026.8.10.2-notes.md | 131 ++++++++++++++++++ .github/workflows/xlings-ci-aarch64.yml | 19 ++- 2 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 .agents/docs/2026-08-10-release-2026.8.10.2-notes.md diff --git a/.agents/docs/2026-08-10-release-2026.8.10.2-notes.md b/.agents/docs/2026-08-10-release-2026.8.10.2-notes.md new file mode 100644 index 00000000..08a83daa --- /dev/null +++ b/.agents/docs/2026-08-10-release-2026.8.10.2-notes.md @@ -0,0 +1,131 @@ +# Release notes — 2026.8.10.2 + +> 修复:#524(冷装失败)、#525(GLX 无 vendor) +> +> 根因与实测:`.agents/docs/2026-08-10-issue-524-525-root-cause-and-fix-design.md` +> +> 实施与跨仓库顺序:`.agents/plans/2026-08-10-issue-524-525-implementation.md` +> +> 同批:`openxlings/libxpkg#40`(0.0.56)、`mcpplibs/mcpp-index#199`、 +> `openxlings/xim-pkgindex#589` + +## 一句话 + +**2026.8.10.1 在任何冷 home 上都装不上 gcc 和 meson**,本版修好; +同时补上 GLX vendor 的可达性——它此前只在探针加了一个真实消费者不会传的链接参数时成立。 + +**建议所有 2026.8.10.1 用户升级。** + +## #524 —— 冷 home 装不上工具链 + +### 症状 + +新机器、CI 冷缓存、`fresh-install`、以及所有下游 mcpp 构建: + +``` +[error] [gcc] failed: config hook failed: config hook returned false +[error] [xim:xpkg]: [ERROR] glibc payload not found, but gcc needs it to rewrite + its ELF interpreter away from the build machine's path. +``` + +glibc 就装在旁边。热 home 不受影响——包已装,安装被跳过,config hook 根本不跑, +**这也正是所有 CI 通道一路绿灯的原因**。第一个探测器在另一个仓库(mcpp 的缓存 miss)。 + +### 根因 + +2026.8.10.1 开始无条件填 `dependency_store_roots`,并在**同一个版本**里带上了 libxpkg 0.0.55。 +单独任一方都不出事;合在一起才第一次执行到那条新分支——而它按「问题够不够具体」判定, +而不是按「答案唯不唯一」。 + +关键点:唯一性守卫**本来就在**,也一直在正确地失败关闭。那道额外的版本前置**没有参与任何消歧**, +唯一作用是拒掉「不带版本的查询」——而那正是索引里每一条 recipe 实际写的形状。 + +按已发布 recipe 的原样调用形状重放,7 个调用点断 6 个: + +| recipe | 后果 | +|---|---| +| gcc、meson | **安装失败**——冷 home 上装不上 | +| llvm | 同一条 helper | +| godot | **静默**回落宿主 GL,即 mcpp#352——它声明 `graphics` 就是为了防这个 | +| graphics | **静默**把横幅显示成 unknown,GPU 与 llvmpipe 再次无法区分 | +| clangd | 跳过 clangd 配置 | + +### 修了什么 + +- **libxpkg 0.0.56**:bare 名字按答案唯一性回答,撞名依旧失败关闭并**点名双方**; + 版本半边按**区间**匹配(`>=2.39` 对上解析器选的 `2.44`,这是 #481 同一形态)。 + 只放宽不收紧:0.0.55/0.0.56 能回答的一律照旧,精确 namespaced 坐标查不到依旧是确定的「没有」, + 记录指向不存在的 payload 依旧是 miss,没有 roots 字段的老客户端保持 scan。 +- **xim-pkgindex**:每条 recipe 按它声明的坐标提问;godot 直接声明 `xim:mesa@>=25`; + clangd 改用 `tool_payload_dir`;llvm 删掉一条**结构上只可能求值为 nil** 的 scode 回退分支。 +- **诊断**:「无解」不再和「没装」长得一样。撞名点名双方;未声明的坐标说明它不是本包的依赖, + 并列出本包实际声明了什么。 + +### 现在能看见它了 + +**E2E-79**:冷 home 上,consumer 声明 namespaced + 区间坐标,解析器落地依赖,config hook 必须找到它。 +同一个 xlings 二进制、两个 libxpkg 实测: + +``` +libxpkg 0.0.55 → FAIL: consumer install failed (exit 1) -- this is #524 +libxpkg 0.0.56 → PASS +``` + +**`.agents/tools/repro-dep-install-dir.sh`**:离线重放全部 7 种真实调用形状, +不联网、不安装、不碰任何 home。`worktree` 参数测未提交的工作树—— +其余形式都从 git 读文件,未提交的修复会「测起来像没生效」。 + +**索引侧结构化 lint**(`tests/test_dep_query_coordinates.py`):字面量实参必须 namespaced 且已声明。 +剥掉注释与字符串后再判——recipe 注释里就写着示例调用。 + +## #525 —— graphics 栈装好后 GLX 仍无 FBConfig + +### 根因:glvnd 的 GLX 侧根本没有 `egl_vendor.d` 的对应物 + +实测宿主 libglvnd 1.7 的符号表: + +``` +libGLX.so.0 libGLX_%s.so.0、__GLX_VENDOR_LIBRARY_NAME ← 一个名字,从来不是路径 +libEGL.so.1 __EGL_VENDOR_LIBRARY_DIRS + JSON library_path ← 我们的 recipe 把它改写成绝对路径 +``` + +**EGL 一直是好的,是因为它走了一条 GLX 没有的路。** 于是 vendor 发现是从 `libGLX.so.0` +内部发起的裸 SONAME `dlopen`,只能由**那个对象自己的搜索路径**服务—— +`LD_LIBRARY_PATH` 会毒死宿主二进制,我们的 loader 的 cache 路径在任何地方都不存在。 + +它此前由**消费者**的 DT_RPATH 沿加载链向上传递来服务。这条机制是真的, +而 `.agents/tools/graphics/` 里每一个探针都用 `-Wl,--disable-new-dtags` 买下了它。 +真实构建系统发的是 DT_RUNPATH,不传递。于是 mcpp 的 imgui 模板在一台宿主 `glxinfo` 完全正常的机器上 +拿到 `GLX: No GLXFBConfigs returned`,而这里每一条 GLX 检查当时都是绿的。 + +### 修了什么(E1) + +- `libglvnd` 给自己的 `libGLX.so.0` 加上 `$ORIGIN/glx-vendor` 的 RPATH—— + **追加**在 `selfcontain.seal()` 已经写入的闭包之后,并在写完后**断言**结果,而不是假定。 +- `graphics` 填充那个目录。不由各 vendor 自行注册:libglvnd 重装会 `os.tryrm` 掉自己的载荷, + 连带清空所有注册,而 vendor 不会因为依赖重装而重装。`graphics` 声明了全部 vendor 和 dispatch, + 所以它最后安装,并能整体重建。 +- `$ORIGIN` 而非 subos 绝对路径:一份载荷服务 home 里的每个 subos, + 而「哪个 subos 用哪个 vendor」由 glvnd 本来就有的 `__GLX_VENDOR_LIBRARY_NAME` 决定。 + 子目录而非 `lib/`:后者会被发布进 subos,而 vendor 是 dlopen 的插件不是链接目标—— + 这也把含 `libc.so.6` / `ld-linux` 符号链接的 `/lib` farm 挡在 RPATH 之外。 + +### 存量 home + +`libglvnd 1.7.0.1` 与 `graphics 0.1.1` 是**同一份产物的新版本键**。 +config hook 只在安装时跑,没有新键的话这个修复只会到达全新安装——而且是静默的。 + +### 探针现在按真实消费者的方式构建 + +`glxprobe.c`、`verify-stack.sh`、`verify-host-link.sh` 去掉了 `-Wl,--disable-new-dtags`。 +**它们在 E1 落地前应当是红的**——那个红是「修的就是这个缺陷」的唯一证据。 +`XLINGS_GFX_LEGACY_DTAGS=1` 保留旧标志用于诊断:legacy 过而默认不过,说明坏的是 vendor 目录。 + +## 升级 + +``` +xlings self update +``` + +索引侧的修复(#589)对**旧客户端同样生效**——namespaced 坐标在 0.0.54/0.0.55/0.0.56 上都能解析, +所以 2026.8.10.1 的用户在索引更新后就能装上 gcc,不必等本版。 diff --git a/.github/workflows/xlings-ci-aarch64.yml b/.github/workflows/xlings-ci-aarch64.yml index 8caa3ff5..e44d4404 100644 --- a/.github/workflows/xlings-ci-aarch64.yml +++ b/.github/workflows/xlings-ci-aarch64.yml @@ -106,9 +106,22 @@ jobs: run: | xlings config --mirror GLOBAL xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - xlings install mcpp -y - # This job deliberately takes the LATEST mcpp, not the pin -- it - # exists to catch cross-build breakage against the current client. + # TEMPORARY PIN -- remove when mcpp-community/mcpp#406 is fixed. + # + # This job deliberately takes the LATEST mcpp, not the `.xlings.json` + # pin: it exists to catch cross-build breakage against the current + # client. It did exactly that. mcpp 2026.8.10.1's host toolchain + # post-install fixup binds `glibc@2.39` while the resolver installs + # 2.44 (latest since xim-pkgindex#582) and refuses to fall back, so + # every cross-build lane fails -- on main as well as on PRs. Proven + # by re-running one unchanged run id across the index bump that made + # mcpp 2026.8.10.1 latest: 2026.8.8.4 passed, 2026.8.10.1 failed. + # + # Pinning here muzzles the canary this job is FOR, which is why the + # pin names its own removal condition instead of quietly existing. + # Until #406 lands, breakage against the newest mcpp is invisible + # here. + xlings install mcpp@2026.8.8.4 -y mcpp --version - name: Cross-build release candidate -> aarch64 static, run under qemu