Skip to content

xpkg store 的已安装查找忽略 namespace:同短名同版本的包会静默顶替,install() 被跳过 #533

Description

@Sunrisepeak

概述

xpkg store 的目录是带命名空间的(<ns>-x-<name>/<version>/),但判断「这个包装过没有」的那层查找不比对 namespace,只看 (name, version)。于是不同命名空间下同短名同版本的两个包会互相顶替 —— 而且失败是静默的,最终报出来的错误和真正的原因毫无关系。

实测环境:mcpp 2026.8.29.1,xlings 2026.8.27.4。


复现

compat.libdrm 是一个源码构建包,install() 里建两个 include 根(libdrm 的公开头在根、uapi 头在 libdrm/ 子目录)。把它的版本定为上游版本 2.4.123:

-- pkgs/c/compat.libdrm.lua
["2.4.123"] = {
    url = { GLOBAL = "https://dri.freedesktop.org/libdrm/libdrm-2.4.123.tar.xz" },
    sha256 = "a2b98567a149a74b0f50e91e825f9c0315d86e7be9b74394dae8b298caadb79e",
},

在任何装了 Mesa 的机器上构建都会失败,因为 xim:mesa 的依赖里有 xim:libdrm@>=2.4,所以 store 里一定有 xim-x-libdrm/2.4.123。

发生了什么

$ mcpp test
   Compiling compat.libdrm v2.4.123          <- 看起来解析成功
error: build failed
failed: bin/libdrm.so
 -shared @bin/libdrm.so.rsp -o bin/libdrm.so ... -Wl,-soname,libdrm.so.2
/bin/sh: 1: -shared: not found

/bin/sh: 1: -shared: not found 与真正的原因隔了三层:

  1. store 认为 libdrm@2.4.123 已安装(它看到的是 xim-x-libdrm/2.4.123),于是跳过 install();
  2. 但版本目录照样被建出来并标记为装好了:
$ ls -a <store>/compat-x-libdrm/2.4.123/
.  ..  .mcpp_ok  .xpkg-install.json  mcpp_generated/

.mcpp_ok、.xpkg-install.json 都在,mcpp_generated/ 是 mcpp 自己的 generated_files 写的 —— 只有描述符 install() 该建的那棵树不存在;

  1. 于是 sources 一个都没匹配上,mcpp 生成了一条零输入的 c_shared 边:
build bin/libdrm.so : c_shared          # 对照正常情况:c_shared obj/a.o obj/b.o ...
  1. 因为没有对象要编,cc 变量根本没被写进 build.ninja(只有 cxx),$cc -shared ... 展开成 -shared ...,shell 去执行 -shared。

关键点:与是否声明该依赖无关

这一点值得强调 —— 我最初以为是「本包声明了 xim:libdrm 才会撞」,不是。上面这个 compat.libdrm 完全没有 xim:* 依赖(纯源码构建,deps = {}),撞的是 store 里碰巧存在的任何同名同版本包。

判定方法

把版本号临时改成一个不可能撞的值:

-["2.4.123"] = {
+["2.4.123-probe"] = {

install() 立刻被调用,而且里面的错误会大声报出来:

error: xlings install_packages failed (exit 1) for 'compat.libdrm@2.4.123-probe'
  xlings reported: E_INTERNAL: [libdrm] failed: install hook failed:
    compat.libdrm.lua:219: attempt to call a nil value (field 'tmpdir')

同一份描述符,只差版本号:一个静默跳过、报无关错误,一个正常执行、错误可见。


影响

这条规则实际上是在说:索引里任何 compat 包的 <短名>@<版本> 都不能与生态里某个 xim:* 包相同,而包作者无从知道生态里有什么、将来会加什么。

具体到这次的图形栈,四个包全部被迫改版本号:

包 想用 实际只能用 撞的是
compat.libdrm 2.4.123 2.4.123.1 xim:libdrm@2.4.123
compat.wayland 1.23.1 1.23.1.1 xim:wayland@1.23.1
freedesktop.wayland(模块层) 1.23.1 1.23.1.2 上面两个
compat.libffi 3.4.4(与生态同版) 3.4.8 xim:libffi@3.4.4

compat.expat 同理只能取 2.7.1 而不是生态的 2.6.2。

也就是说:一个包想和生态用同一个上游版本,是表达不出来的 —— 而这恰恰是最常见、最应该被鼓励的情形(消费者和 Mesa 用同一份 libdrm)。


建议

store 的已安装查找按 (namespace, name, version) 匹配,与目录布局 <ns>-x-<name>/<version>/ 一致。

这是 xlings#381(index 侧按 (namespace, name) 建键)在 store 侧的同一个缺口。

在修好之前,至少让它不要静默:如果查找命中了一个 namespace 不同的包,要么报错,要么在跳过 install() 时打印出「命中的是 <ns>-x-<name>/<version>」。现在的表现是把一个包管理器的身份问题伪装成了一条链接器命令行错误。


相关

  • 索引侧的四个包与形态讨论:mcpplibs/mcpp-index(源码构建的图形栈)
  • 顺带一个小的:Form A 的纯 C 库包([targets.x] kind = "shared",没有任何 .cppm)会稳定报
    warning: src/<name>.cppm: lib target without conventional lib root。C 库没有 lib root 可言,这个告警会传染给每个消费者。

Activity

  1. Sunrisepeak commented on Aug 30, 2026

    @Sunrisepeak
    MemberAuthor

    已修复并发布,分两个仓库。已在沙箱里对已发布产物核验过(见下)。

    根因确实在 xlings 的 store 侧,但位置和你写的不同

    你定位的 installer.cppm:903 是七月的快照。上游后来把「这个包装过没有」的四个回答者收敛进了一个 install_state 模块 —— 而这条回落活了下来,成了那个模块存在的意义所要消灭的第五个回答者,位置搬到了 installer.cpp:2628:

    else if (!payloadInstalled) {
        auto db = Config::versions();
        auto resolved = xvm::match_version(db, node.name, node.version);  // ← 裸短名

    主路径 (catalog.cpp:302) 早就是带命名空间的;installation_state 也收命名空间参数。只有这条回落不是,而它正是「没有 installed hook」的源码构建包会走的那条。

    修法:coordinate_from_payload_path 自己的头注释就写着答案 —— 安装器写下了那个路径,所以 <...>/xpkgs/<ns>-x-<package>/<version> 不是关于包的证据,它就是安装时记录下的包身份。新的 payload_path_names_another_package 读它,回落对能证明属于别人的 DB 条目不再采信。

    刻意做成「拒绝」而不是「要求」:路径解析不出 store 坐标的(自管理工具、迁移过的载荷、xpkgs/ 之外的任何东西)一律照旧接受。只有能证明归属不同的那一格改变了行为。

    → openxlings/xlings#576,已发布 v2026.8.30.2

    mcpp 侧另有两条独立缺陷,你报告里那条错误消息是其中之一

    /bin/sh: 1: -shared: not found 我用纯 mcpp 输入复现了 —— 一个匹配到零源码的 kind = "shared",不碰 xlings。而且它有个没人报的静默孪生:

    $ ar rcs libempty.a ; echo $? ; stat -c %s libempty.a
    0
    8                    # 空档案,而构建报告「成功」
    

    kind = "lib" 的同一缺陷退 0、写出 8 字节 .a、Finished dev —— 消费者要到后面才因 undefined symbol 失败,离成因更远。所以修法是在 plan 期拒绝零输入链接单元(点名 target),而不是给链接器一条更好的消息。

    另外:.mcpp_ok 不再凭「callee 退 0 + 目录存在」落章 —— 那个目录是安装器动工之前就建好的。现在要求至少有一项不是 mcpp/xlings 自己写的。并且这个判据也跑在快路径上,所以已经被这个 bug 污染过的 store 会在下次构建自愈,不需要谁知道该删哪个目录。

    顺带你提的那条 lib root 告警也修了:谓词从「产不产库」收窄到「是不是模块库」。

    → #536,已发布 2026.8.30.1,内部 xlings floor 抬到 2026.8.30.2

    沙箱核验(已发布产物,从索引装)

    ok  xim:mcpp@2026.8.30.1 resolved and installed from the index
    ok  xlings pinned = 2026.8.30.2
    ok  kind=shared refused, and the message names the target
    ok  kind=shared never reaches the shell
    ok  kind=lib   refused, and the message names the target
    ok  kind=lib   never reaches the shell
    passed: 11   failed: 0
    

    ⚠️ 一件这个修复没有授权的事

    索引仍然不应该故意发布撞名的 <短名>@<版本>。 floor 以下的客户端照样静默跳过 install(),而索引是所有已装客户端都要消费的数据。这个修复针对的是误撞 —— 按你机器上的数据,那已经是 20 个短名宽。

    所以你表格里那四个包的版本号,我的建议是不动:它们现在能用,而裸版本钉是精确的,回退要穷举每个消费者逐个重钉,期间旧客户端会静默失败。新包在 floor 被采纳后再用上游版本号,按包逐个判断。理由写在 .agents/docs/2026-08-30-cross-repo-fix-plan-532-533-534.md §1 和 §6.2。

    如果你认为这四个包值得回退,那是个产品判断,我把代价列在那份文档里了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions