Skip to content

Latest commit

 

History

History
417 lines (291 loc) · 23.6 KB

File metadata and controls

417 lines (291 loc) · 23.6 KB

一个包一个版本:xlings 地址的身份,以及 2026.9.6.5 之后的文档对齐

状态:已实现(mcpp 2026.9.6.6 / mcpp-plugins 0.2.4)。§10 是实现期的实测回填, 其中三条推翻了本文正文。原文保留,不回改——被推翻的判断本身是这份记录的一部分。本文的每一条「现状」都标注了它是出来的还是出来的。 伴随文档:2026-09-07-general-build-infrastructure-gaps-design.md(§15 记录了本轮 五处缺口),本文处理它遗留的第六处,以及随之而来的文档与生态对齐。

0. 这份方案要解决的一句话

版本在一个地方属于包的身份,在另一个地方不属于。 于是同一个包被两处以不同版本声明 时,系统认为那是两个包,把两份都装下来。

1. 现状:版本语义已经在了,缺的是身份的一致

1.1 版本语义已经可用(实测,两个方向)

[xlings.workspace] 的版本位接受范围表达式,并且真的被求解:

manifest 读数
"xim:shaderc" = ">=2026.1" Provisioning [xlings.workspace] entries (xim:shaderc@>=2026.1),构建通过
"xim:shaderc" = ">=2099.1" 被拒绝:package 'xim:shaderc@>=2099.1' not found in the synced index

反向那条是判据的关键:只跑正向,一个把范围当字面量放过去的实现同样会「通过」。

索引侧也早在用同一套写法 —— xim:libX11@>=1.8xim:glibc@>=2.38 出现在多个 recipe 的 deps 里。

结论:不需要引入版本语义。 「钉具体版本还是钉范围」已经是作者的选择。

1.2 但身份有两套定义(代码)

站点 身份是什么 冲突时
merge_conditional_config(prepare.cppm:264) package_of()(ns, name),不含版本 更具体的赢,并发 xlings/axis-override 告警说明用了哪条
graph_xlings_split find(rootSpecs, spec)整个地址串 不同串 = 不同包,两份都进供给列表

于是工程钉 xim:cuda-nvcc@13.3.33、规则包钉 @12.9.86 时,两份都装 —— 每份 2–3 GB。 而 fillXpkgDirs 里「同名保留第一个值」的规则让 xpkg_dir 答工程那份,所以装了两份、 用了一份,且没有任何一句话提到这件事。

正确的那套语义已经写好了,只是没有用在第二个站点上。

2. 修法:一条规则,两条配套,不需要求解器

2.1 规则

包的身份是 (namespace, name)。版本永远是约束**,不是身份的一部分。**

这一条同时给出两件被要求的性质:

  • 「所有地方都支持版本语义」—— 因为不再有第二种身份,就没有第二处需要单独支持;
  • 「钉具体版本还是钉范围由开发者选」—— 因为两者都只是约束,系统不区分。

减少机制:今天有两套身份定义,统一后剩一套。

2.2 裁决:离消费者更近的赢,并且说出来

同一身份上有多条声明时,离消费者更近的那条赢:工程 > 依赖。同一 manifest 内则是 更具体的赢:[target.<selector>] > 顶层。

这是两条不同的序,不该用一个词盖住:「更具体」说的是谓词的窄窄,「更近」说的是声明者 与最终产物的距离。后者是新的一条,前者 merge_conditional_config 已经在做,并且已经有 xlings/axis-override 负责「说出来」。跨包这一趟复用同一条告警。

两个依赖互相冲突(规则 A 要 >=8.5、规则 B 要 <8.5,工程什么都没写)时没有「更近」 可言,那种情形直接走 §2.3 的拒绝,消息点出两个包各自的约束。

2.3 校验:赢家必须满足其余每一条约束

这是唯一新增的一条,也是让「不需要求解器」成立的那条。

选版本不需要求交 —— 裁决说了算。求交只在「赢家违反了输家的约束」时才有意义,而那种情形 正确的动作是拒绝并点名两边,不是悄悄选一个:

error: `xim:cann-toolkit` is pinned to 8.3.RC1 by this project, and
       `mcpp:plugins` (feature `rules-ascendc`) requires >=8.5.0.
       fix: raise the pin, or drop it and let the rule's constraint decide.

所以是「验证一个已选定的版本」,不是「在约束集合里搜索」。前者是一次比较,后者要知道 索引里有哪些版本可选。

2.3.1 这条捷径的已知代价,不能不写

不求交意味着系统会拒绝一些求解器本可以满足的组合。工程写 >=8.0、规则写 8.5.0, 而索引里最新是 8.3:裁决让工程赢,>=8.0 解析到 8.3,校验发现它不满足规则的 8.5.0, 于是拒绝 —— 尽管 8.5.0 本身同时满足两边。

这是有意接受的代价,理由有三条:拒绝的消息里已经写着确切的出路(「抬 pin,或者去掉 pin 让规则的约束决定」);求解器要引入「可选版本集合」这个此处还不存在的输入;而这种组合 (一边范围、一边精确、且交集不在范围的最大值上)在实践中是少数。

如果它变成多数,那就是引入求交的信号 —— 而不是现在。

2.4 为什么这比「按包去重」更准确

我在讨论中先把出路列成三选一(不钉版本 / 引擎按包去重 / 维持现状)。那个划分是错的: 身份统一之后,「不钉版本」只是约束的一种取值,不需要单独成为一条路。三条塌缩成一条。

3. 影响面:三个站点必须一起改

单独改 graph_xlings_split 会造出本轮 §15.5 那种形状的缺陷 —— 装的是 A,xpkg_dir 答的是 B。三处共同定义了「哪个版本被安装、哪个版本被回答」:

  1. graph_xlings_split —— 去重与 root/graph 的划分
  2. fillXpkgDirs —— 「同名保留第一个值」,决定 xpkg_dir 答谁
  3. provision_xlings_addresses —— 实际收到的地址列表

判据必须同时观察「装了什么」和「答了什么」,只看其中一个会漏掉这类缺陷。

4. 判据

# 构造 断言 今天
C1 工程与其规则包钉同一包的不同版本 store 里只出现一个版本目录
C2 同上 xpkg_dir 答的就是那个目录 绿(但与 C1 一起看才有意义)
C3 同上 输出里有一句说明谁赢了
C4 工程钉的版本不满足规则的约束 拒绝,消息同时点出两边与各自的约束
C5 只有规则声明(工程什么都不写) 装规则要求的版本,构建通过 需实测
C6 范围不可满足 拒绝 绿(§1.1 已测)
C7 工程只写 [build-dependencies] 那一行,不写任何 [xlings.workspace] 规则声明的 toolkit 被装上,构建通过 需实测(§6.1)
C8 同上,再加工程自己的钉 只装工程钉的那一版,且构建通过 红(即 C1,带真实 payload)

C1 与 C4 是这份方案存在的理由;C6 是既有行为,列出来是为了防止改动把它弄坏。

5. 文档对齐:实测出来的清单

以下每一条都用 grep 在树上核过,不是回忆。

5.1 明确过期,必须改

文件 问题
docs/specs/manifest-semantics.md §4.3.1 仍写「<selector> 禁止命名五个目标侧层(acceleratorc-abi…)」。2026.9.6.5 起 accelerator 被接受。且该句自身不自洽:说「五个」而括号里列了六个
docs/11-machine-output.md 本轮三处新拒绝(设备源无 action、未声明的后端、缺 host module)都没有 refusal code —— 实测 refusal::record 在三处附近均零次出现,而 build_program.cppm 已经 import 了 mcpp.build.refusal,所以这是遗漏不是能力缺失。机器消费者分不开它们,而这份文档正是拒绝词表的规范
examples/09-heterogeneous/README.md 表格只列四个目录,multi-backend/cann/ 缺席;结尾「Start with cuda/. The other three assume it.」在六个目录下已不成立

5.2 本轮已改,列出以便复查

docs/05-mcpp-toml.md / zh(exportsaccelerator 谓词键、accel 门控 xlings 与 dependencies、build-dependencies 的更正)、docs/07-build-mcpp.md / zh(link-flag、 设备源必须到达 action、规则只取自己认领的扩展名、未提供的 import 被点名拒绝)、 docs/20-heterogeneous-builds.md / zh(accelerator = "none" 进词表、CPU 回退改用 none.asc/.cce 进扩展名表、rules-ascendc 进 lane 表、四条 lane → 五条)。

5.3 不需要改

docs/14-target-side.md 的「五个层」指的就是被解析的那五个,从来不含 accelerator —— 与本轮的日程划分恰好一致。带 (2026.9.6.x+) 的行是历史标记,保持原样。

6. 生态侧:每个规则自带它的环境

6.1 规则拥有默认,工程拥有例外

这是本方案在用户侧的主要收益,比 §1.2 的「装两份」更常被碰到。

今天一个要编 CUDA 核的工程写两块:一条 [build-dependencies] 边选规则,再加四行 [xlings.workspace] 把 toolkit、cudart、libcurand、cccl 各自钉一遍。第二块是规则 已经知道的事——哪个包、最低哪一版,由写规则的人决定,而不是由用它的人重复。

改成:

# rules/cuda.cppm 所在的包,mcpp-plugins
[target.'cfg(accelerator = "cuda")'.feature-xlings.rules-cuda]
"xim:cuda-nvcc"   = ">=12.9.86"
"xim:cuda-cudart" = ">=12.9.79"
"xim:libcurand"   = ">=10.3.10.19"
"xim:cuda-cccl"   = ">=12.9.27"

两重门:feature 说「要不要这个规则」,selector 说「哪些构建真的下载」。CPU-only 构建 两道门都不过,一个字节都不装。

于是工程侧只剩一行:

[build-dependencies.mcpp]
plugins = { version = "0.2.4", features = ["rules-cuda"], host-module = true }

五个规则一起改,不留一个例外:rules-cudarules-hiprules-syclrules-spirvrules-ascendc前一稿写「CUDA 暂不动」是错的 —— 留一个例外就等于告诉用户「有时要 自己写、有时不要」,而「什么时候要」没有任何地方说得清。理由那一稿写的是「12.9 与 13.x 耦合驱动下界,那个决定属于工程」;可决定属于工程不等于默认属于工程 —— 下界写成 >=12.9.86 就把「至少要这么新」交给规则,把「具体哪一版」留给工程,两件事各归各位。

6.2 例外怎么写,以及为什么它现在才安全

工程仍然可以钉:

[target.'cfg(accelerator = "cuda")'.xlings.workspace]
"xim:cuda-nvcc" = "13.3.33"

这条覆盖之所以能用,正是 §2 的裁决与校验。 没有身份统一,它不是覆盖而是第二份安装: xim:cuda-nvcc@13.3.33xim:cuda-nvcc@>=12.9.86 是两个串、两条记录、两份 2 GB 的 payload,而 xpkg_dir 只答其中一个。所以 §6 不是 §2 的下游应用,它是 §2 成立的判据 ——一个用户会走的、今天会坏的路径。

顺序因此是固定的:引擎先落地,插件再发。反过来做,插件的默认与工程的钉一起装两份。

6.3 文档要说的一句话

docs/20docs/05 各加一段,说清三件事:默认由规则带,不需要写;要换版本就在工程里 写同名条目,离消费者近的赢;赢家不满足规则的下界时构建被拒绝并点名两边。

6.4 examples

  • cann/appcuda/sycl/hip/vulkan/:删掉各自的 [xlings.workspace] 加速器块。这是最直接的读数——示例的行数就是用户要写的行数
  • multi-backend:保留 xim:cuda-nvcc = "12.9.86" 一行,并把注释改成「这是覆盖 的示范」。整个生态需要恰好一个地方示范例外路径,多一个都是噪声。
  • 09-heterogeneous/README.md:补 multi-backend/cann/ 两行,改结尾那句。

6.5 索引

无改动。xim:cann-toolkit 与 CUDA/SYCL 各包都已发布;规则包改用 feature-xlings 只需 plugins 的一次小版本。

7. mcpp 依赖的 xlings pin

src/xlings/xlings.cppmpinned::kXlingsVersion2026.8.30.2 抬到 2026.9.5.1(当前最新)。这个常量是 .github/tools/check_version_pins.sh唯一 真源:该脚本把 .github/ 下每一处 xlings 版本与它比对,不等就红。所以这一处改动是 一个常量加脚本枚举出来的那几处工作流,不是一次搜索替换 —— 判据是脚本本身通过。

它与本方案的关系是直接的:[xlings.workspace] 的范围求解由 xlings 侧执行,而 §2 的 校验建立在「范围真的被求解」之上(§1.1 的两个方向就是在验这件事)。停在三个月前的 pin 让这条依赖变成一个没人核过的假设。

8. 分期与依赖

内容 依赖
§5.1 三处文档 + 三处新拒绝补 refusal code + §7 的 pin
§2 的身份统一(三个站点)+ C1/C3/C4/C5 一期的 refusal code(C4 要一个码)
§6.1 五个规则自带环境 + §6.4 示例瘦身 二期必须先发布

三期跨仓库且顺序不可交换:插件的默认要靠引擎的裁决才不会变成第二份安装。

9. 自我 review:讨论过程中被推翻的四处判断

我先说的 为什么错
「要不要引入版本语义」 语义已经在,实测两个方向。问题从头就不是这个
出路是三选一(不钉版本 / 按包去重 / 维持现状) 身份统一之后三条塌缩成一条:「不钉版本」只是约束的一种取值
需要一个版本求解器 过重。选版本由裁决决定,求交只在校验时需要,而校验是一次比较不是搜索
「CUDA 暂不动,那个决定属于工程」 混淆了决定权默认值。下界属于规则、具体版本属于工程,两者不冲突;留一个例外反而让「什么时候要自己写」无处可查(§6.1)

前三处有同一个形状:在没有把已有机制查清楚之前就开始设计新机制。 §1.1 那两行实测 是十几秒的事,而它把整份方案的规模从「引入约束求解」缩到「统一一处身份定义」。

第四处形状不同:我把「这里有真实的复杂度」读成了「这里应该保持现状」。 12.9 与 13.x 的驱动下界差异是真的,但它论证的是「工程要能覆盖」,而我拿它论证了「规则不该有 默认」——同一个事实支持的是相反的结论。

10. 实现期的实测回填

写下来的四条里,有三条改了正文的结论。

10.1 xpkg_dir 根本回答不了范围 —— 方案漏了这条

§1.1 只测了「范围被供给」,没测「范围被回答」。实现时才发现 xpkg_payload_at 把整个版本位当目录名比对:>=8.5.0 装上了载荷,然后回答 nullopt

这是让规则包无法声明下界的那道缝,而整份 §6 建立在它之上。 少了这一处修复, [feature-xlings] 里写 >= 的规则会「声明、装上、然后找不到」。

形状是熟悉的一种:判据只走了一半的路。供给与查询是同一条链的两端,而 §1.1 的两个 方向都落在供给那一端 —— 反向腿(>=2099.1 被拒)证明了范围被求解,却完全不涉及求解 之后谁去读它。

修法:版本位是常量就先按目录名精确匹配(保留「钉了就是钉了」,也保留 8.0.RC1 这类 解析不了的拼法可寻址),是约束就在已安装的版本里挑满足它的最高一个。判据里有一条 双侧有界的范围 —— 只用下界的话,一个忽略约束直接取最新的实现照样通过。

10.2 落败的精确钉不是「违反」,只有被陈述的要求才是

正文 §2.3 写「赢家必须满足其余每一条约束」。按字面实现,两条互不相同的精确钉会 互相「违反」,于是每一个「依赖钉了某工具、工程也钉了」的组合都在升级当天变成硬 失败 —— 而那个分歧正是 §2.2 的裁决存在的理由。

改成:>= / ^ / ~ / 逗号组合是要求,可以被违反;不带运算符的裸版本是选择, 由裁决处理并被报告。§2.3 的例子本来就是「钉 vs 下界」,所以这是把正文的措辞收紧到它 自己的例子上。

这条不对称随后成了机制。 规则包写精确版本 = 提供一个默认;写 >= = 陈述一条要求。 同一个字段承载两种意图,而使用侧看到的行为恰好不同:覆盖默认是安静的,击穿要求是被 拒绝的。

10.3 默认的形状是「耦合判断」,不是风格 —— §6.1 的通篇 >= 是错的

§6.1 写 "xim:cuda-nvcc" = ">=12.9.86"。实测:索引里有 12.9.86 与 13.3.33,>= 取 最高,于是默认变成 13.3.33 —— 而 13.x 把驱动下界抬到 r580,本轮实测的机器是 12.4。 一条本意为「至少这么新」的下界,把整条 lane 换成了没验证过的那条。

规则:版本耦合着规则看不见的东西时,默认写精确版本;不耦合时写下界。 CUDA / HIP 的运行时耦合驱动 → 钉 12.9 线;glslang / dpcpp / CANN 不耦合 → >=

10.4 边界:规则声明它编译时用的,工程声明它运行时跑在上面的

xim:mesa-lavapipe 是一个软件 Vulkan 设备。把它挪进 rules-spirv 会给每一个消费者 强加一个软件渲染器,包括有 GPU 的那些。它留在工程里。

同理 compat:cuda-runtime / compat:sycl-runtime / compat:vulkan-runtime:它们是 产物的运行期适配器,而且有一条结构性理由 —— 插件包是一条 [build-dependencies] 边, 它自己的 [dependencies] 有意不到达消费者的 target。

所以「插件自带环境」覆盖的是 xim 载荷那一半,不是全部。这条写进 plugins 的 README 与 manifest 注释,因为它是唯一能回答「为什么这个还要我写」的地方。

10.5 修好一处判据,第一批红的是自己的夹具

把 plugins CI 的 MCPP_VERSION 从 2026.9.6.1 抬到 2026.9.6.6 之后,hip-consumer 当场变红:它的 glob 写 accel = "hip, cuda12.9+{sm_89}",而 [package] accelerators 只有 ["hip"]sycl-consumer 与 mcpp 自己的 examples/09-heterogeneous/{hip,sycl} 同形。

两边的 CI 都看不见它。 mcpp 侧那两个示例在 build_examples.sh 里是 SKIP;plugins 侧的引擎 pin 停在检查出现之前。这是一条只有跨仓库、且两边都动才会暴露的缺陷。

10.6 告警码:新开一个,不复用

正文 §2.2 说跨包这一趟复用 xlings/axis-override。没有采纳:那个码的含义写在它自己的 注释里,是「同一份 manifest 的两条」。跨包的版本分歧是另一件事,而告警码是给机器 读的分类。新码是 xlings/version-override

10.8 生态验证顺手挖出一个引擎缺陷:DT_RPATH 的继承没被建模

为了让 rules-ascendc 的诊断真的被编译一次(五个夹具没有一个激活它),我拿 cann 示例 跑了一遍。它被拒绝了,消息点名八个库找不到。

而它刚刚链接出来的那个产物,把其中七个都解析掉了 —— 直接跑一次,加载器只在第八个 上失败,那一个属于这台机器没有的驱动。模型与加载器给出相反的读数。

真因:DT_RPATH 被整条依赖链继承,DT_RUNPATH 不被继承;而闭包模型只查发起请求 的那个对象自己的搜索路径。厂商工具包恰好是最坏的形状 —— 库与库之间按裸 SONAME 互相 依赖、自己都不带搜索路径,而它们所在的目录只写在可执行文件DT_RPATH 里。

三条值得记下来的:

  1. 这与本轮的身份工作无关,是既有缺陷。用已发布的 2026.9.6.5 加已发布的 plugins 0.2.3 复现,读数一字不差 —— 先做这个对照,再动手。
  2. 它只在「跑一次」时暴露。构建被拒绝,而拒绝消息本身是自洽的;唯一能分开「模型对」 和「加载器对」的动作,是执行那个产物。
  3. 抑制的那一半必须一起写:带 DT_RUNPATH 的对象不使用任何 RPATH,自己的和继承的 都不用。只写正向那条,一个无条件继承的实现同样通过。

更一般的形状:为了让一段没人跑过的代码被编译一次而去跑它,挖到的往往不是那段代码 的问题。 五个夹具覆盖五条规则里的四条,而第五条一次都没被编译过。

10.7 落地顺序(不可交换)

# 仓库 内容 前置
1 mcpp 引擎 + 文档 + 判据 + 2026.9.6.6
2 发布 2026.9.6.6 1 合入
3 mcpp-plugins 五条规则自带环境 + 0.2.4 2(CI 要下载这个引擎)
4 0.2.4 进索引 3 合入
5 mcpp 示例瘦身到 0.2.4 4(示例从索引解析 plugins)

第 5 步是 mcpp 的第二个 PR。它不是拆分,是两次跨仓库发布夹在中间的必然结果: 示例引用的是已发布的 plugins,而那一版依赖第 1 步的引擎。

11. 落地记录

产物 判据
mcpp 引擎 PR#584 → main 83c910022026.9.6.6 分支 38/38 绿,main 30/30 绿;e2e 627–630 在 2026.9.6.5 上实测为红
发布 tag v2026.9.6.6,四平台 两端镜像逐字节核过(GitCode 的 HEAD 返 401,ranged GET 返 206,尺寸四个全等)
索引(xim) openxlings/xim-pkgindex #777 Publish Index Artifact 在合并 commit 上绿;xlings install mcpp@2026.9.6.6 成功
mcpp-plugins PR#8 → main 7a8349330.2.4 main CI 绿,且新判据打印出四条 entries declared by dependencies
索引(mcpp) mcpplibs/mcpp-index #364 lua 解析通过,三个平台表 latest = 0.2.4;mirror-cn-reachable 绿
示例 mcpp 的第二个 PR 25 行声明离开 manifest,留下三行(两个设备 + 一个覆盖示范)

11.1 落地期又挖出两处,都不是本方案的对象

一、DT_RPATH 的继承没被建模(§10.8)。为了让第五条规则的诊断被编译一次而去构建 cann 示例时碰到:引擎拒绝了一个加载器能起来的产物。已修,连同两条腿的单测。

二、mcpp-plugins 的 CI 缓存从来没生效过。 已发布的 mcpp 是自包含的 —— 没有 MCPP_HOME 时它的 home 就是解开的 tarball,而那个 job 缓存的是 ~/.mcpp,一个 mcpp 从不碰的目录。每次运行都是冷的,而 90 分钟的超时预算正是按冷的算的,所以没人觉得 不对。 顺带把我自己新写的判据也坑了一次:rm -rf ~/.mcpp/provisioned 什么都没删, 于是一次构建成功的运行被判红。

本地预演之所以通过,恰恰因为它用的是开发构建 —— 开发构建被排除在自包含模式 之外,确实用 ~/.mcpp同一段脚本,两个不同的对象。 预演要用发布物跑。

11.2 索引描述符里一句不成立的话

mcpp.plugins.lua 原有的措辞说「记录的 floor 会拒绝低版本客户端」。没有这回事: 描述符与包的 manifest 里都没有 per-package 的引擎下界,而索引级的 min_mcpp 是故意 不抬的(抬它会让停在下限的客户端连整个索引都打不开)。floor 是文档。改成写清楚低版本 客户端实际会撞上什么:2026.9.6.5 上规则报工具包缺席并点出下界,≤2026.9.6.4 上 cfg(accelerator = ...) 的工具表被直接拒绝。

11.3 沙箱验证:五节全过,零跳过

xlings subos use verify-966 --sandbox,脚本 base64 传进去,mcpp 按 store 路径寻址, 用的全部是已发布的东西(2026.9.6.6 + mcpp:plugins@0.2.4,CN mirror):

== A. identity ==      ok: mcpp 2026.9.6.6
== B. 范围 ==          ok: 装上并被回答 / 不可满足被拒绝
== C. 一条边 ==        ok: 载荷来自规则(entries declared by dependencies)/ 跑通
== D. 覆盖 ==          ok: 只装一个 glslang
== E. 击穿下界 ==      ok: 拒绝并点出两侧 / 抬钉后通过
0 assertion(s) failed

隔离是可证的,而且这次证了:沙箱 registry 里 12 个包(恰好这次验证需要的 那些,含 mcpp-x-pluginsxim-x-glslang),宿主 222 个。判据落在一个从没见过 这些东西的 registry 上 —— 这是「看内容不要看变量」的正面用法。