Commit df4f75e
* feat(build): 库可以替用户拉起工具链,构建程序可以声明「一组文件」(#359) (2026.8.6.2)
#355 让依赖产出的 host 工具可用之后,grpc-m 成了第一个真实使用者,并暴露出
两个缺口:工具**被构建了**却传不到消费者,以及 build.mcpp 无法安全地 glob
自己的输入。两者形状相同 —— **新增一种提供物/输入时,没有任何地方逼你回答
「它怎么传播」「它的指纹怎么取」**。这是本仓库反复付学费的「同一决策在 N 处
推导」的镜像:一个必答问题在**零处**被表达。
## 提供物进表,传播由 `reexport` 决定
新增 `src/build/provisions.cppm`:ProvisionKind 表(tool / host-module /
dep-dir),每行必须回答「裸名可寻址吗」「跨边要不要 reexport」;传播是一条
不动点,与 computeUsageRequirements 同形。
依赖 spec 新增 `reexport = true`(默认 false):把这条边的构建期提供物交给
**本包自己的消费者**。于是 grpc 可以在描述符里声明 protoc + 插件 + 规则模块,
它的用户只写一条依赖。
刻意不复用边上的 `visibility`:它默认就是 "public",搭车等于默认传播,而
「把工具交给消费者」是供应链主张,必须显式写下来。(设计初稿以「include_dirs
默认 private」类比,那条对本仓库不成立 —— prepare.cppm 无条件把
privateBuild.includeDirs 拷进 publicUsage;真正 private-only 的是 build.mcpp
注入的那些。)
裸名不再靠追加顺序决定。全限定 `MCPP_DEP_<NS>_<NAME>_BIN_<TOOL>` 总是发布;
裸名按与包身份同一套阶梯绑定(mcpplibs → compat → 无命名空间 → 唯一候选),
争用时给出诊断而不是默默选一个 —— 否则「版本错配不可表达」这条性质会从另一
扇门跑回来。
`dep_dir()` 与 host 模块一并沿同一条规则传播(此前分别只覆盖直接依赖、只认
root 的 dependencies)。
## `rerun-if-changed-glob`:输入可以是一个集合
`build.mcpp` 的重跑键此前只有「文件内容」和「环境变量」两种形态,于是 glob
**结构性不安全**:新增一个 .proto 不改变任何已声明文件的哈希,程序不重跑,
新文件静默不生成。
新指令的指纹是**排序后的匹配路径集合**,不含内容、size、mtime —— 内容已由
File 条目覆盖,而 mtime 在 checkout/容器/rsync 下不稳定(本仓库在
file_time_type 的 epoch 上摔过)。构建输出目录与 .git 永不进入集合,否则宽
模式会对着自己的产物无限重跑。
两层必须一起改:工程级 fast path 会整个跳过 prepare,而新增文件不移动任何
mtime —— 那正是它此前静默无效的地方。缓存记录因此新增 `root` 行,fast path
才能按每份缓存自己的根去求值。protocol → 2,cache epoch → 2。
## 让库能按平台裁剪,让子构建说出真话
`ConditionalConfig` 带着三张依赖表却独缺 `feature-deps`,正是该类型注释为
#258 记录过的失败形状。补上 `[target.<sel>.feature-deps.<f>]`:否则一旦由
库决定请求什么,不支持的平台就变成用户改不掉的硬错(Windows 的工具子构建
缺陷至今未定位)。feature 本身无条件注册,只有它拉进来的东西是条件性的。
顺带把条件依赖表收进同一条 funnel:此前三个调用点合并构建输入,只有 root
那个还额外合并依赖表,而注释把它说成「out of scope」。
工具子构建失败现在带上 chain、scratch 目录和可直接重跑的命令,
`MCPP_TOOL_BUILD_VERBOSE=1` 关掉内层输出过滤 —— Windows 那个缺陷至今未定位
的直接原因就是真正的报错没进日志。
## 其他
- `path_matches_glob` 从 scanner 的匿名命名空间搬进 `mcpp.modgraph.glob`:
glob 输入的指纹必须与 `sources = [...]` 选中同一批文件,两个「应该一致」的
匹配器就是那个病。
- 内置 xlings 升到 2026.8.6.2。
## 验证
- 新单测:传播规则(未 reexport 不外泄 / 逐跳传递 / 环终止)、裸名阶梯四级、
glob 指纹(增删变、改内容不变、输出目录不参与、跨平台排序一致)。
- 新 e2e 193/194/195。193 已用「让 propagate 忽略 reexport」验证会变红。
- 新增 `EveryDependencySpecKeyIsAccepted`:dep-spec 的键必须同时写进两处,
漏掉第一处时诊断是**误导性**的(bool 被告知「必须是字符串或嵌套表」),
#359 加 `reexport` 时就撞上了。
- 本机 e2e 179 passed / 7 failed / 8 skipped;7 条在 main 上逐条复现,均为环境性。
* fix(ci): 自举 pin 指向索引里已经不存在的版本
`ci-aarch64-fresh-install` 的 self-host 步报 `xlings: version '2026.8.5.3'
not found for 'mcpp' — available: 2026.8.6.1`。
自举 pin 是「从哪个已发布版本开始自举」,按设计不随每次发布走;但它是**上界
之下的选择**,不是一个可以无限期不动的常量 —— xim-pkgindex 对 mcpp 只暴露最
新条目,pin 落在它之外就成了「装不上」。判据因此是「该版本此刻仍可解析」,
而不是「它是不是最新」。
* feat(build): feature-dep 与已声明依赖同键时按加法合并
`mergeActiveFeatureDeps` 用的是 `try_emplace`,键已存在就丢弃 feature 的
spec。这条规则对 version/path/git 是对的(条件段不该静默覆盖无条件段),对
`tools` / `reexport` 却丢掉了这个 feature 存在的全部理由。
gRPC 就是反例,而且是本 issue 的头号用例:它**无条件**依赖 compat.protobuf,
而 `codegen` feature 需要往**同一条边**加 `tools = ["protoc"], reexport = true`。
挪到无条件条目上不可行 —— 那会让每个 grpc 消费者都构建 protoc(~157 个额外
TU),而这正是 tools 默认关闭要避免的。
于是:tools / features 取并集,host-module / reexport 取或,身份字段不合并。
与逐边 feature 请求本来遵循的规则一致。
e2e 193 补第 3 段覆盖它,并已用「不合并 tools」验证会变红
(`rulepkg: no codegen tool`)。
* test(e2e): 195 用诊断而不是 --strict 判定
`--strict` 会把**所有**降级提升为错误,包括与本测试无关的那些 —— Windows 上
clang 报「this toolchain and platform combination emits no GNU depfile」,于是
195 在 Windows 上因为一件它并不关心的事而红。
本测试要断言的是「没有任何谓词匹配的平台上,请求该 feature 不是未知 feature
错误」,那就直接断言那条诊断不出现。
* fix(manifest): 不认识的依赖键降级而不是让整份 manifest 加载失败
`reexport` 要写进**已发布包**的 manifest(grpc 的 [feature-deps.codegen]),
于是冒出一个此前没人问过的问题:比它旧的 mcpp 读到会怎样?
答案曾经是整份加载失败,而且报错误导 —— `reexport = true` 被告知
「must be a string, inline dep table, or nested table」。
根因是**一个谓词兼任两职**:`looks_like_inline_dep_spec` 既判定「内联 spec 还是
嵌套命名空间表」,又枚举「哪些键有意义」。不认识的键因此走不到「未知选项」那条
路上 —— 表直接判不出是 spec,被当成命名空间。
后果不是提示不友好,而是**任何已发布包都永远无法采用新键**。这与 #349 确立的
是同一条性质:数据不得决定程序是否可用。
判据换轴:内联 spec 的判定是「它是否指名了一个来源」(path/version/git/workspace)。
嵌套命名空间表的键是**包名**,不会有包叫 `version`,所以不会误判。识别为 spec
之后,不认识的键记为降级(--strict 仍拒绝),与 xpkg 读取器的 xpkgUnknownKeys
「record rather than swallow」同一条纪律。
救不了已经发布出去的旧客户端,但从这一版起这类问题不再复发。
* fix(toolchain): 固定的 host 工具链装不上时,回退到已经在本机可用的 target 工具链
`ci-aarch64-fresh-install` 在 aarch64 上构建 xlings 时死在:
[error] download failed for xim:gcc@16.1.0: HTTP 404
error: host toolchain for build.mcpp ('gcc@16.1.0'): ...
链条是:图里有包带 build.mcpp ⇒ 需要 HOST 编译器 ⇒ 取 `[toolchain] default`
⇒ 那是 `gcc@16.1.0`,而 xim 的 gcc 声明 `archs = { "x86_64" }` ⇒ 404。
`[toolchain]` 的键是 OS,**没有 arch 这根轴**(#367),所以 aarch64 问的和
x86_64 问的是同一个包。结果是:凡是依赖图里出现 build.mcpp 的工程,在 aarch64
上就是死路。
但这台机器上**已经有**一个能用的编译器:当解析出的 target 与 host 只差 libc
环境(aarch64-linux-musl 跑在 aarch64 Linux 上),target 工具链就是本机原生的
—— 它在这里能跑,它产出的二进制在这里也能跑,拿它编译一个构建程序完全成立。
因此:固定的 host 工具链取不到时,若 arch 与 os 与 host 相同,就用 target 工具
链,并发一条命名双方的告警。成功路径一字未改;此前的死路变成一次可解释的降级。
#367 记录的仍是根因(host 工具链选择缺 arch 轴),这条只是让它不再是绝路。
* test(ci): aarch64 自举步的 xlings 一半用的是**已安装**的 mcpp,不是被审的代码
同一条 job 里,mcpp 用 "$m"(刚构建出来的)验证,紧接着的 xlings 构建却用
裸 `mcpp` —— 即已安装的那个。于是这一半从来没看见过 PR。
这正是上面那段注释为 clone ref 记录过的缺陷,只是位置往下挪了几行;暴露方式
也一样:一个**只在这条构建里出现**的 aarch64 失败,其修复在这里无法验证,因为
跑它的二进制早于该修复。
顺带两处:`m` 改为绝对路径(`cd /tmp/xlings-src` 之后还要用),并显式把
MCPP_HOME 传下去 —— mcpp 从**二进制所在位置**推导 home,放在 /tmp/mcpp-src
下的二进制否则会认领一个空 home,把整套生态重新自举一遍。
* fix(toolchain): aarch64 上原生 GCC 的载荷是 musl-gcc,不是 glibc gcc (#367)
`ci-aarch64-fresh-install` 报 `download failed for xim:gcc@16.1.0: HTTP 404`。
**先前的归因是错的**:我判成「`[toolchain]` 缺 arch 轴」,并动手给 manifest 加了
一根选择器轴。生态其实**早就支持 aarch64** —— xlings-res 的 musl-gcc 发行版里
就有 `musl-gcc-16.1.0-linux-aarch64.tar.gz`,而 `xim:gcc` 声明的是
`archs = { "x86_64" }`。所以缺的不是让用户去写 arch,而是 mcpp 问错了包。
同一台机器上 target 一侧是对的:
`gcc@16.1.0 → aarch64-linux-musl → xim-x-musl-gcc`。因为 target 一侧把 triple
注入了 spec,`to_xim_package` 的 musl 分支便按「同一 target,两种载荷形态」选中
了原生包。而 build.mcpp 的 **host** 解析刻意不注入 target,于是落到最后那条
glibc 分支,拿到只有 x86_64 资产的 `gcc`。
「这个 spec 在这台机器上对应哪个载荷」正是 `to_xim_package` 存在的意义,上面
那条 musl 分支已经在回答它的另一半。所以修在这里:Linux 上瞄准本机的 GCC spec,
在非 x86_64 架构上解析到 `musl-gcc`。用户不该被要求去编码「某个工具链包是为
哪些架构构建的」。
同时撤回上一版那个「host 工具链装不上就回退到 target 工具链」的兜底 —— 它是
错误归因的产物;正确解析之后它是死代码,而一条失败路径上的隐式回退不该白留。
判定抽成自由函数并接收 host arch,而不是读编译期常量:这样 aarch64 的答案能在
x86_64 机器上被测到 —— 而「测一个手边没有的架构」正是重点。
* docs(release): 自举 pin 的失败又被归因成「索引删了版本」,第三次
09-release.md §5 已经为 3b1cb6b 记过一次这个更正,而本 PR 的 c1a3e45 又写了一遍
「指向索引里已经不存在的版本」。它是错的:openxlings/xim-pkgindex 与
d2learn/xim-pkgindex 都列着 63 个 mcpp 版本,`2026.8.5.3` 在里面。
值得注意的是 `available:` 实际列出的东西 —— 只有一个版本,正是那次 job 刚装上
的那个。这是「已安装版本」的形状,不是索引清单;而那一步解析的是 .xlings.json
的 **workspace** pin,workspace 作用域正是既有的那个陷阱。真实机制未坐实,不写
成结论。
pin 的 bump 本身没问题(§4 明确说这是合理的做法,而且它确实解开了那条 job)。
教训更窄,而且反复被重学:**在断言「索引删了它」之前,先读索引。** 一条 curl
就能定。
---------
Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
1 parent eb196a2 commit df4f75e
38 files changed
Lines changed: 2441 additions & 274 deletions
File tree
- .agents/docs
- .github
- actions
- bootstrap-mcpp
- setup-macos-llvm
- workflows
- docs
- zh
- src
- build
- manifest
- modgraph
- pm
- toolchain
- tests
- e2e
- unit
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
123 | | - | |
| 123 | + | |
| 124 | + | |
124 | 125 | | |
125 | 126 | | |
126 | 127 | | |
127 | 128 | | |
128 | | - | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
129 | 144 | | |
130 | 145 | | |
131 | 146 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
155 | | - | |
| 155 | + | |
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
| |||
292 | 292 | | |
293 | 293 | | |
294 | 294 | | |
295 | | - | |
| 295 | + | |
296 | 296 | | |
297 | 297 | | |
298 | 298 | | |
| |||
363 | 363 | | |
364 | 364 | | |
365 | 365 | | |
366 | | - | |
| 366 | + | |
367 | 367 | | |
368 | 368 | | |
369 | 369 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
126 | | - | |
| 126 | + | |
127 | 127 | | |
128 | 128 | | |
129 | 129 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
118 | 118 | | |
119 | 119 | | |
120 | 120 | | |
121 | | - | |
| 121 | + | |
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
| |||
255 | 255 | | |
256 | 256 | | |
257 | 257 | | |
258 | | - | |
| 258 | + | |
259 | 259 | | |
260 | 260 | | |
261 | 261 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
| 99 | + | |
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| |||
288 | 288 | | |
289 | 289 | | |
290 | 290 | | |
291 | | - | |
| 291 | + | |
292 | 292 | | |
293 | 293 | | |
294 | 294 | | |
| |||
358 | 358 | | |
359 | 359 | | |
360 | 360 | | |
361 | | - | |
| 361 | + | |
362 | 362 | | |
363 | | - | |
| 363 | + | |
364 | 364 | | |
365 | | - | |
| 365 | + | |
366 | 366 | | |
367 | 367 | | |
368 | 368 | | |
| |||
440 | 440 | | |
441 | 441 | | |
442 | 442 | | |
443 | | - | |
| 443 | + | |
444 | 444 | | |
445 | 445 | | |
446 | 446 | | |
| |||
622 | 622 | | |
623 | 623 | | |
624 | 624 | | |
625 | | - | |
| 625 | + | |
626 | 626 | | |
627 | 627 | | |
628 | 628 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
0 commit comments