|
| 1 | +# 一个包一个版本:xlings 地址的身份,以及 2026.9.6.5 之后的文档对齐 |
| 2 | + |
| 3 | +> 状态:设计,未实现。本文的每一条「现状」都标注了它是**读**出来的还是**跑**出来的。 |
| 4 | +> 伴随文档:`2026-09-07-general-build-infrastructure-gaps-design.md`(§15 记录了本轮 |
| 5 | +> 五处缺口),本文处理它遗留的第六处,以及随之而来的文档与生态对齐。 |
| 6 | +
|
| 7 | +## 0. 这份方案要解决的一句话 |
| 8 | + |
| 9 | +**版本在一个地方属于包的身份,在另一个地方不属于。** 于是同一个包被两处以不同版本声明 |
| 10 | +时,系统认为那是两个包,把两份都装下来。 |
| 11 | + |
| 12 | +## 1. 现状:版本语义已经在了,缺的是身份的一致 |
| 13 | + |
| 14 | +### 1.1 版本语义已经可用(**实测**,两个方向) |
| 15 | + |
| 16 | +`[xlings.workspace]` 的版本位接受范围表达式,并且**真的被求解**: |
| 17 | + |
| 18 | +| manifest | 读数 | |
| 19 | +|---|---| |
| 20 | +| `"xim:shaderc" = ">=2026.1"` | `Provisioning [xlings.workspace] entries (xim:shaderc@>=2026.1)`,构建通过 | |
| 21 | +| `"xim:shaderc" = ">=2099.1"` | 被拒绝:`package 'xim:shaderc@>=2099.1' not found in the synced index` | |
| 22 | + |
| 23 | +反向那条是判据的关键:只跑正向,一个把范围当字面量放过去的实现同样会「通过」。 |
| 24 | + |
| 25 | +索引侧也早在用同一套写法 —— `xim:libX11@>=1.8`、`xim:glibc@>=2.38` 出现在多个 recipe |
| 26 | +的 `deps` 里。 |
| 27 | + |
| 28 | +**结论:不需要引入版本语义。** 「钉具体版本还是钉范围」已经是作者的选择。 |
| 29 | + |
| 30 | +### 1.2 但身份有两套定义(**读**代码) |
| 31 | + |
| 32 | +| 站点 | 身份是什么 | 冲突时 | |
| 33 | +|---|---|---| |
| 34 | +| `merge_conditional_config`(prepare.cppm:264) | `package_of()` → `(ns, name)`,**不含版本** | 更具体的赢,并发 `xlings/axis-override` 告警说明用了哪条 | |
| 35 | +| `graph_xlings_split` | `find(rootSpecs, spec)` → **整个地址串** | 不同串 = 不同包,两份都进供给列表 | |
| 36 | + |
| 37 | +于是工程钉 `xim:cuda-nvcc@13.3.33`、规则包钉 `@12.9.86` 时,两份都装 —— 每份 2–3 GB。 |
| 38 | +而 `fillXpkgDirs` 里「同名保留第一个值」的规则让 `xpkg_dir` 答工程那份,所以**装了两份、 |
| 39 | +用了一份**,且没有任何一句话提到这件事。 |
| 40 | + |
| 41 | +正确的那套语义已经写好了,只是没有用在第二个站点上。 |
| 42 | + |
| 43 | +## 2. 修法:一条规则,两条配套,不需要求解器 |
| 44 | + |
| 45 | +### 2.1 规则 |
| 46 | + |
| 47 | +> **包的身份是 `(namespace, name)`。版本永远是**约束**,不是身份的一部分。** |
| 48 | +
|
| 49 | +这一条同时给出两件被要求的性质: |
| 50 | + |
| 51 | +- 「所有地方都支持版本语义」—— 因为不再有第二种身份,就没有第二处需要单独支持; |
| 52 | +- 「钉具体版本还是钉范围由开发者选」—— 因为两者都只是约束,系统不区分。 |
| 53 | + |
| 54 | +它**减少**机制:今天有两套身份定义,统一后剩一套。 |
| 55 | + |
| 56 | +### 2.2 裁决:离消费者更近的赢,并且说出来 |
| 57 | + |
| 58 | +同一身份上有多条声明时,**离消费者更近的那条赢**:工程 > 依赖。同一 manifest 内则是 |
| 59 | +**更具体的赢**:`[target.<selector>]` > 顶层。 |
| 60 | + |
| 61 | +这是两条不同的序,不该用一个词盖住:「更具体」说的是谓词的窄窄,「更近」说的是声明者 |
| 62 | +与最终产物的距离。后者是新的一条,前者 `merge_conditional_config` 已经在做,并且已经有 |
| 63 | +`xlings/axis-override` 负责「说出来」。跨包这一趟复用同一条告警。 |
| 64 | + |
| 65 | +**两个依赖互相冲突**(规则 A 要 `>=8.5`、规则 B 要 `<8.5`,工程什么都没写)时没有「更近」 |
| 66 | +可言,那种情形直接走 §2.3 的拒绝,消息点出两个包各自的约束。 |
| 67 | + |
| 68 | +### 2.3 校验:赢家必须满足其余每一条约束 |
| 69 | + |
| 70 | +这是**唯一新增**的一条,也是让「不需要求解器」成立的那条。 |
| 71 | + |
| 72 | +选版本不需要求交 —— 裁决说了算。求交只在「赢家违反了输家的约束」时才有意义,而那种情形 |
| 73 | +正确的动作是**拒绝并点名两边**,不是悄悄选一个: |
| 74 | + |
| 75 | +``` |
| 76 | +error: `xim:cann-toolkit` is pinned to 8.3.RC1 by this project, and |
| 77 | + `mcpp:plugins` (feature `rules-ascendc`) requires >=8.5.0. |
| 78 | + fix: raise the pin, or drop it and let the rule's constraint decide. |
| 79 | +``` |
| 80 | + |
| 81 | +所以是「验证一个已选定的版本」,不是「在约束集合里搜索」。前者是一次比较,后者要知道 |
| 82 | +索引里有哪些版本可选。 |
| 83 | + |
| 84 | +### 2.3.1 这条捷径的已知代价,不能不写 |
| 85 | + |
| 86 | +不求交意味着**系统会拒绝一些求解器本可以满足的组合**。工程写 `>=8.0`、规则写 `8.5.0`, |
| 87 | +而索引里最新是 8.3:裁决让工程赢,`>=8.0` 解析到 8.3,校验发现它不满足规则的 `8.5.0`, |
| 88 | +于是拒绝 —— 尽管 8.5.0 本身同时满足两边。 |
| 89 | + |
| 90 | +这是有意接受的代价,理由有三条:拒绝的消息里已经写着确切的出路(「抬 pin,或者去掉 pin |
| 91 | +让规则的约束决定」);求解器要引入「可选版本集合」这个此处还不存在的输入;而这种组合 |
| 92 | +(一边范围、一边精确、且交集不在范围的最大值上)在实践中是少数。 |
| 93 | + |
| 94 | +**如果它变成多数,那就是引入求交的信号** —— 而不是现在。 |
| 95 | + |
| 96 | +### 2.4 为什么这比「按包去重」更准确 |
| 97 | + |
| 98 | +我在讨论中先把出路列成三选一(不钉版本 / 引擎按包去重 / 维持现状)。**那个划分是错的**: |
| 99 | +身份统一之后,「不钉版本」只是约束的一种取值,不需要单独成为一条路。三条塌缩成一条。 |
| 100 | + |
| 101 | +## 3. 影响面:三个站点必须一起改 |
| 102 | + |
| 103 | +单独改 `graph_xlings_split` 会造出本轮 §15.5 那种形状的缺陷 —— 装的是 A,`xpkg_dir` |
| 104 | +答的是 B。三处共同定义了「哪个版本被安装、哪个版本被回答」: |
| 105 | + |
| 106 | +1. `graph_xlings_split` —— 去重与 root/graph 的划分 |
| 107 | +2. `fillXpkgDirs` —— 「同名保留第一个值」,决定 `xpkg_dir` 答谁 |
| 108 | +3. `provision_xlings_addresses` —— 实际收到的地址列表 |
| 109 | + |
| 110 | +判据必须同时观察「装了什么」和「答了什么」,只看其中一个会漏掉这类缺陷。 |
| 111 | + |
| 112 | +## 4. 判据 |
| 113 | + |
| 114 | +| # | 构造 | 断言 | 今天 | |
| 115 | +|---|---|---|---| |
| 116 | +| C1 | 工程与其规则包钉同一包的**不同版本** | store 里只出现**一个**版本目录 | 红 | |
| 117 | +| C2 | 同上 | `xpkg_dir` 答的就是那个目录 | 绿(但与 C1 一起看才有意义) | |
| 118 | +| C3 | 同上 | 输出里有一句说明谁赢了 | 红 | |
| 119 | +| C4 | 工程钉的版本**不满足**规则的约束 | 拒绝,消息同时点出两边与各自的约束 | 红 | |
| 120 | +| C5 | 只有规则声明(工程什么都不写) | 装规则要求的版本,构建通过 | 需实测 | |
| 121 | +| C6 | 范围不可满足 | 拒绝 | 绿(§1.1 已测) | |
| 122 | +| C7 | 工程只写 `[build-dependencies]` 那一行,不写任何 `[xlings.workspace]` | 规则声明的 toolkit 被装上,构建通过 | 需实测(§6.1) | |
| 123 | +| C8 | 同上,再加工程自己的钉 | 只装工程钉的那一版,且构建通过 | 红(即 C1,带真实 payload) | |
| 124 | + |
| 125 | +C1 与 C4 是这份方案存在的理由;C6 是既有行为,列出来是为了防止改动把它弄坏。 |
| 126 | + |
| 127 | +## 5. 文档对齐:实测出来的清单 |
| 128 | + |
| 129 | +以下每一条都用 grep 在树上核过,不是回忆。 |
| 130 | + |
| 131 | +### 5.1 明确过期,必须改 |
| 132 | + |
| 133 | +| 文件 | 问题 | |
| 134 | +|---|---| |
| 135 | +| `docs/specs/manifest-semantics.md` §4.3.1 | 仍写「`<selector>` **禁止**命名五个目标侧层(`accelerator`、`c-abi`…)」。2026.9.6.5 起 `accelerator` **被接受**。且该句自身不自洽:说「五个」而括号里列了六个 | |
| 136 | +| `docs/11-machine-output.md` | 本轮三处新拒绝(设备源无 action、未声明的后端、缺 host module)**都没有 refusal code** —— 实测 `refusal::record` 在三处附近均零次出现,而 `build_program.cppm` **已经 import 了 `mcpp.build.refusal`**,所以这是遗漏不是能力缺失。机器消费者分不开它们,而这份文档正是拒绝词表的规范 | |
| 137 | +| `examples/09-heterogeneous/README.md` | 表格只列四个目录,`multi-backend/` 与 `cann/` 缺席;结尾「Start with `cuda/`. The other three assume it.」在六个目录下已不成立 | |
| 138 | + |
| 139 | +### 5.2 本轮已改,列出以便复查 |
| 140 | + |
| 141 | +`docs/05-mcpp-toml.md` / zh(`exports`、`accelerator` 谓词键、accel 门控 xlings 与 |
| 142 | +dependencies、build-dependencies 的更正)、`docs/07-build-mcpp.md` / zh(`link-flag`、 |
| 143 | +设备源必须到达 action、规则只取自己认领的扩展名、未提供的 import 被点名拒绝)、 |
| 144 | +`docs/20-heterogeneous-builds.md` / zh(`accelerator = "none"` 进词表、CPU 回退改用 |
| 145 | +`none`、`.asc`/`.cce` 进扩展名表、rules-ascendc 进 lane 表、四条 lane → 五条)。 |
| 146 | + |
| 147 | +### 5.3 不需要改 |
| 148 | + |
| 149 | +`docs/14-target-side.md` 的「五个层」指的就是被解析的那五个,从来不含 `accelerator` —— |
| 150 | +与本轮的日程划分**恰好一致**。带 `(2026.9.6.x+)` 的行是历史标记,保持原样。 |
| 151 | + |
| 152 | +## 6. 生态侧:每个规则自带它的环境 |
| 153 | + |
| 154 | +### 6.1 规则拥有默认,工程拥有例外 |
| 155 | + |
| 156 | +这是本方案在用户侧的**主要**收益,比 §1.2 的「装两份」更常被碰到。 |
| 157 | + |
| 158 | +今天一个要编 CUDA 核的工程写两块:一条 `[build-dependencies]` 边选规则,再加四行 |
| 159 | +`[xlings.workspace]` 把 toolkit、cudart、libcurand、cccl 各自钉一遍。第二块是**规则 |
| 160 | +已经知道的事**——哪个包、最低哪一版,由写规则的人决定,而不是由用它的人重复。 |
| 161 | + |
| 162 | +改成: |
| 163 | + |
| 164 | +```toml |
| 165 | +# rules/cuda.cppm 所在的包,mcpp-plugins |
| 166 | +[target.'cfg(accelerator = "cuda")'.feature-xlings.rules-cuda] |
| 167 | +"xim:cuda-nvcc" = ">=12.9.86" |
| 168 | +"xim:cuda-cudart" = ">=12.9.79" |
| 169 | +"xim:libcurand" = ">=10.3.10.19" |
| 170 | +"xim:cuda-cccl" = ">=12.9.27" |
| 171 | +``` |
| 172 | + |
| 173 | +两重门:feature 说「要不要这个规则」,selector 说「哪些构建真的下载」。CPU-only 构建 |
| 174 | +两道门都不过,一个字节都不装。 |
| 175 | + |
| 176 | +于是工程侧只剩一行: |
| 177 | + |
| 178 | +```toml |
| 179 | +[build-dependencies.mcpp] |
| 180 | +plugins = { version = "0.2.4", features = ["rules-cuda"], host-module = true } |
| 181 | +``` |
| 182 | + |
| 183 | +五个规则一起改,不留一个例外:`rules-cuda`、`rules-hip`、`rules-sycl`、`rules-spirv`、 |
| 184 | +`rules-ascendc`。**前一稿写「CUDA 暂不动」是错的** —— 留一个例外就等于告诉用户「有时要 |
| 185 | +自己写、有时不要」,而「什么时候要」没有任何地方说得清。理由那一稿写的是「12.9 与 13.x |
| 186 | +耦合驱动下界,那个决定属于工程」;可决定属于工程**不等于**默认属于工程 —— 下界写成 |
| 187 | +`>=12.9.86` 就把「至少要这么新」交给规则,把「具体哪一版」留给工程,两件事各归各位。 |
| 188 | + |
| 189 | +### 6.2 例外怎么写,以及为什么它现在才安全 |
| 190 | + |
| 191 | +工程仍然可以钉: |
| 192 | + |
| 193 | +```toml |
| 194 | +[target.'cfg(accelerator = "cuda")'.xlings.workspace] |
| 195 | +"xim:cuda-nvcc" = "13.3.33" |
| 196 | +``` |
| 197 | + |
| 198 | +**这条覆盖之所以能用,正是 §2 的裁决与校验。** 没有身份统一,它不是覆盖而是第二份安装: |
| 199 | +`xim:cuda-nvcc@13.3.33` 与 `xim:cuda-nvcc@>=12.9.86` 是两个串、两条记录、两份 2 GB 的 |
| 200 | +payload,而 `xpkg_dir` 只答其中一个。所以 §6 不是 §2 的下游应用,它是 §2 **成立的判据** |
| 201 | +——一个用户会走的、今天会坏的路径。 |
| 202 | + |
| 203 | +顺序因此是固定的:引擎先落地,插件再发。反过来做,插件的默认与工程的钉一起装两份。 |
| 204 | + |
| 205 | +### 6.3 文档要说的一句话 |
| 206 | + |
| 207 | +`docs/20` 与 `docs/05` 各加一段,说清三件事:默认由规则带,不需要写;要换版本就在工程里 |
| 208 | +写同名条目,离消费者近的赢;赢家不满足规则的下界时构建被拒绝并点名两边。 |
| 209 | + |
| 210 | +### 6.4 examples |
| 211 | + |
| 212 | +- `cann/app`、`cuda/`、`sycl/`、`hip/`、`vulkan/`:删掉各自的 `[xlings.workspace]` |
| 213 | + 加速器块。这是最直接的读数——**示例的行数就是用户要写的行数**。 |
| 214 | +- `multi-backend`:**保留** `xim:cuda-nvcc = "12.9.86"` 一行,并把注释改成「这是覆盖 |
| 215 | + 的示范」。整个生态需要恰好一个地方示范例外路径,多一个都是噪声。 |
| 216 | +- `09-heterogeneous/README.md`:补 `multi-backend/` 与 `cann/` 两行,改结尾那句。 |
| 217 | + |
| 218 | +### 6.5 索引 |
| 219 | + |
| 220 | +无改动。`xim:cann-toolkit` 与 CUDA/SYCL 各包都已发布;规则包改用 feature-xlings 只需 |
| 221 | +plugins 的一次小版本。 |
| 222 | + |
| 223 | +## 7. mcpp 依赖的 xlings pin |
| 224 | + |
| 225 | +`src/xlings/xlings.cppm` 的 `pinned::kXlingsVersion` 从 `2026.8.30.2` 抬到 |
| 226 | +`2026.9.5.1`(当前最新)。这个常量是 `.github/tools/check_version_pins.sh` 的**唯一 |
| 227 | +真源**:该脚本把 `.github/` 下每一处 xlings 版本与它比对,不等就红。所以这一处改动是 |
| 228 | +一个常量加脚本枚举出来的那几处工作流,不是一次搜索替换 —— 判据是脚本本身通过。 |
| 229 | + |
| 230 | +它与本方案的关系是直接的:`[xlings.workspace]` 的范围求解由 xlings 侧执行,而 §2 的 |
| 231 | +校验建立在「范围真的被求解」之上(§1.1 的两个方向就是在验这件事)。停在三个月前的 |
| 232 | +pin 让这条依赖变成一个没人核过的假设。 |
| 233 | + |
| 234 | +## 8. 分期与依赖 |
| 235 | + |
| 236 | +| 期 | 内容 | 依赖 | |
| 237 | +|---|---|---| |
| 238 | +| 一 | §5.1 三处文档 + 三处新拒绝补 refusal code + §7 的 pin | 无 | |
| 239 | +| 二 | §2 的身份统一(三个站点)+ C1/C3/C4/C5 | 一期的 refusal code(C4 要一个码) | |
| 240 | +| 三 | §6.1 五个规则自带环境 + §6.4 示例瘦身 | 二期**必须先发布** | |
| 241 | + |
| 242 | +三期跨仓库且顺序不可交换:插件的默认要靠引擎的裁决才不会变成第二份安装。 |
| 243 | + |
| 244 | +## 9. 自我 review:讨论过程中被推翻的四处判断 |
| 245 | + |
| 246 | +| 我先说的 | 为什么错 | |
| 247 | +|---|---| |
| 248 | +| 「要不要引入版本语义」 | 语义已经在,实测两个方向。问题从头就不是这个 | |
| 249 | +| 出路是三选一(不钉版本 / 按包去重 / 维持现状) | 身份统一之后三条塌缩成一条:「不钉版本」只是约束的一种取值 | |
| 250 | +| 需要一个版本求解器 | 过重。选版本由裁决决定,求交只在**校验**时需要,而校验是一次比较不是搜索 | |
| 251 | +| 「CUDA 暂不动,那个决定属于工程」 | 混淆了**决定权**与**默认值**。下界属于规则、具体版本属于工程,两者不冲突;留一个例外反而让「什么时候要自己写」无处可查(§6.1) | |
| 252 | + |
| 253 | +前三处有同一个形状:**在没有把已有机制查清楚之前就开始设计新机制。** §1.1 那两行实测 |
| 254 | +是十几秒的事,而它把整份方案的规模从「引入约束求解」缩到「统一一处身份定义」。 |
| 255 | + |
| 256 | +第四处形状不同:**我把「这里有真实的复杂度」读成了「这里应该保持现状」。** 12.9 与 |
| 257 | +13.x 的驱动下界差异是真的,但它论证的是「工程要能覆盖」,而我拿它论证了「规则不该有 |
| 258 | +默认」——同一个事实支持的是相反的结论。 |
0 commit comments