Skip to content

Commit 3da6b0c

Browse files
fix: #540 的七条审计,以及核验它们时挖出的四条 (2026.9.1.1) (#542)
* fix: #540 的七条审计,以及核验它们时挖出的四条 (2026.9.1.1) 七条里六条成立,一条判据打偏。核验过程本身挖出四条没有人报过的,其中一条比原报告 的全部七条都严重。它们几乎全是同一族:**mcpp 关于自己说了一句话,而 mcpp 不遵守它。** 完整核验、量化与设计见 `.agents/docs/2026-08-31-issue540-seven-audit-findings.md`。 ── 1. 供给从不检查自己是否成功(未报告,最严重)──────────────── `xlings::call` 返回 `expected<CallResult, string>`,只要子进程跑起来就处于**值**态 —— 能力自身的状态在 `CallResult` 里面,因为 xlings 讲完 NDJSON 协议后按设计退 0。 #531 的调用点只测了 `if (!r)`,于是 xlings 能报出的每一种失败都被读成成功。实测: $ mcpp build # deps = ["definitely-not-a-real-package"] Provisioning [xlings] deps (definitely-not-a-real-package) Finished dev [unoptimized + debuginfo] in 0.12s $ cat .mcpp/.xlings-deps.stamp definitely-not-a-real-package ← 记为已完成 $ mcpp build Finished dev in 0.00s ← 连 Provisioning 都不再打印 xlings 报得完全正确(`E_NOT_FOUND` + `{"exitCode":1,"kind":"result"}`),`call()` 也 解析对了。⚠️ 正确写法就在同一个文件里:依赖安装路径写的是 `if (r && r->exitCode != 0 && …)`。#531 的注释说它修的缺陷是「声明看起来被接受了却 什么都没做,这是一个配置键能有的最坏形态」—— 没人读结果,它的修法重现了那个形态, 而记号把它变成永久的。 ── 2. 该路径不认两个自动安装开关(未报告)──────────────────── 它自称与 `[toolchain]` 平权,而那条先例在 `MCPP_OFFLINE` 或 `MCPP_NO_AUTO_INSTALL` 下硬错并报出触发的是哪一个。⚠️ 一个专门导出 `MCPP_NO_AUTO_INSTALL` 来阻止意外下载 的 CI,会从一条从没听说过这个变量的路径上拿到下载。 拦的是安装**动作**而不是整块:已供给好的工程仍然离线构建得出来。 ── 3. 记号记录全局效果却存在项目里(未报告)──────────────── 安装落在 registry(刻意如此),而 `<project>/.mcpp/.xlings-deps.stamp` 记着它。清掉 或换掉 `MCPP_HOME`,项目仍然声称已装;`mcpp clean` 只删 `target/`,也清不掉。改按 依赖列表哈希存进 registry,并且只在成功时写。 ⚠️ 搬迁不得让昨天能跑的构建今天被拒。自审时发现:升级后每个已供给的工程读起来都是 「未供给」,配上第 2 条的闸,离线首次构建会被拒。旧记号因此在**唯一一处**被采信 —— 就是那道闸 —— 因为在那里网络关着,没有别的办法查证。它绝不被提升进 registry:写它 的那个版本不读结果,所以它的含义是「尝试过」而不是「成功了」,别处采信等于把缺陷 带过修它的这次升级。 ── 4. 三份手抄的词汇表,三份都漂移了 ─────────────────────── `kKnownBuildKeys`、`kKnownConditionalBuildKeys` 与 xpkg 的 `target_cfg` 列表,都是 别处已有机器可读形式(紧挨其上的读取点、`BuildInputs` 的成员表)的转录。代价不是 少一条警告,而是**一条假的警告**。 * `[build] std-module` / `std-compat-module` / `std-module-flags` 被读取却报 unsupported —— `kKnownBuildKeys` 的**第二次**漂移,而第一次的详细叙述就在它上方 八行。 * 条件轴拒绝 `BuildInputs` 的两个成员:`std-module-flags`(#494 就是为这条轴才把它 挪上来的,成员注释写着「membership here is what makes the cfg axis carry it」) 与 `private_include_dirs` —— 后者更严重,xpkg 描述符的 `target_cfg` 块,也就是 **同一条轴的另一套语法**,是接受它的。 * `[features]` 是唯一一个完全没有 schema 检查的结构化段落。 两条列表的消息现在都由列表本身生成。新增 6 个单测,每个都带否定对照 —— 「没有警告」 这类断言会被一个把检查整个删掉的解析器满足。 ── 5. cfg(<层> = "…"):文档记载而从未接线的特性 ──────────── docs/14 用一整节记载它,连「为什么不能用 feature 选择代替」和作用域约束都论证过; 而 `cfgpred::Ctx` 只由三元组构造,`match_kv` 只认 os/arch/family/env。于是每一个这样 的段落被**静默**丢弃,包成功构建在错误的 C 库配置上。实测:`cfg(env="gnu")` 生效、 `cfg(c-abi="glibc")` 不生效、零诊断。8 处文档如此(中英各 4)。 实现:目标侧解析(`tsd::resolve`)与 P1689 扫描之间有一段空窗,而 build.mcpp 已经在 用它 —— 它按同样的形状把 directive tail 镜像进 `packages[0]`。第二趟合并用同样的 `directives::mark` + `fold_private_tail`,不另造机制。 ⚠️ 两趟必须不相交,而只靠 `matches()` 做不到:`cfg(any(linux, c-abi="musl"))` 的 三元组腿在第一趟就为真,第二趟会再匹配一次,`append()` 是追加式的于是贡献两遍。 按**是否命名了层**归属,而不是按答案。e2e 328 数 `-D` 出现次数来守这条。 ⚠️ 层谓词不能选择依赖(层是从依赖图解析出来的),这种段落被报出并忽略。 ── 6. 未知的 cfg 键现在会说话 ────────────────────────────── 求值器过去对未知键返回假,而那与「这一段本就不该匹配」读数完全相同。⚠️ 词汇表从 求值器**导出**而不是被转录 —— 否则这条诊断自己就会成为第 4 条里的第四份手抄件。 求值器同时就是校验器:一次遍历回答三个问题,因为另写一个校验器就是同一份文法的 第二个解析器,而本仓库已经为其中一个付过账。 `ident()` 现在接受 `-` 与 `+`,否则 `c-abi` 会被扫成裸词 `c` 加一堆垃圾,诊断能报的 就只有字母 `c`。 ── 7. c-abi 层报的是库名,不再是三元组的 env 段 ────────────── 这条是实现第 5 条时才暴露的:谓词是一次比较,而比较有两侧,而此前的设计工作从没问过 右侧的取值是什么。它在普通 Linux 宿主上是 `gnu`(`payload_libc_name` 原样返回 env 段),而 docs/14 的表一直写着 `glibc`/`musl`/`picolibc`,e2e 296 的文件头也把它期望的 报告写作 `c-abi glibc (payload)`。⚠️ **一个只被打印的值没有拼写纪律,把它提升为用户 比较的对象会追溯地强加一条。** 请求侧保留三元组的拼写(规范 §3.4:env 段是对 c-abi 的请求而非答案),两者经 `c_abi_request_satisfied` 比较而非按相等 —— 否则每一次普通 `-gnu` 构建都会被报成请求 不匹配。Windows 上 `-gnu` 命名的是工具链的 MinGW 形态,其 C 运行时是 UCRT,因此映射 按 OS 分叉。 ── 8. 退出码:补上 runtime 的一半,并写下被指派的契约 ───────── 原报告说 docs/11 的表漏了 `4`。判据打偏了:那张表按信封命令划定,而**没有一个信封 命令给得出 4** —— `self env --format json` 恰恰是被特意做成绕开产生 4 的 `load_or_init` 的。表真正漏的是 `1`(`xpkg parse` 五处返回)。 2026-08-08 的协议设计文档 §R4 把完整契约指派给了 `docs/spec/`,一直没有写。现在写了: `docs/spec/exit-codes.md`(SPEC-003),0/1/2/4/70/127 全表 + 稳定性承诺。 ── 9. 其余文本 ───────────────────────────────────────────── * `mcpp build --help` / `mcpp test --help` 说默认档位是 release,而它是 dev。六处说得 对(含一条 e2e 与 mcpp 自己的 mcpp.toml),两处说错;`prepare.cppm` 那条字段注释是 没被报告的第三处。 * `mcpp index update <name>` 承诺按索引筛选而只筛项目级。限制此前只写在一条注释里 —— 一个只有实现者看得到的地方,从外面看与「这功能坏了」无从区分。 * docs/13 与 docs/17 仍在说 `[xlings] deps` 不是安装触发器(#531 之后为假)。 * `mcpp::target_libc()` 的文档改为它实际回答的问题:供给 sysroot 的那个**载荷**包, 而这个值是目标侧解析的一项**输入**。 ── 测试 ──────────────────────────────────────────────────── 单元:test_manifest 新增 6 个(三份词汇表各一正一负),test_targetside 新增 2 个; 96 个测试二进制全过。 e2e:新增 327(供给失败会报出来 + 两个开关 + 搬迁连续性,五条断言,前两条不需要网络)、 328(层谓词生效/不生效/恰好一次 + 未知键 + --strict)、329(退出码契约,含「退 1 且 stdout 带信封」)。 ⚠️ 判据的分母:327 的核心断言跨**两次**调用 —— 为失败而写的记号在写它的那一次里 不可见,只有第二次构建才分得开「失败了」与「失败了并被记成完成」。328 数 `-D` 的 出现次数而不是用预处理器判断,因为预处理器分不开一个 `-D` 和两个。 * fix(provisioning): stamp key uses uint64_t, not size_t 一个 32 位宿主会把 64 位的 FNV offset basis 截断,于是它拥有一份与别人不同的键空间 而没有任何东西说明为什么。碰撞本身两侧都不是正确性问题 —— 记号文件存的是**列表**, 比较也是针对内容的,所以两个共键的列表会重新供给而不是悄悄采用对方的记录。 * refactor(provisioning): 闸不再多套一层缩进 把 `have != want` 换成一个 `needProvision` 布尔,于是自动安装闸可以在供给块**之前** 求值并在采信旧记号时把它关掉 —— 供给块本身保持原来的嵌套层级。行为不变;改的是让 一个千行的 PR 里这一段仍然读得下去。 * fix(features): 保留键的诊断指向一个存在的拼写 自审读出:`deps` 的那条消息提供了 `optional = true`,而 mcpp 从来没有这个键。 一条把读者送去一个解析器不认识的键的诊断,与本次发布正在移除的那些警告是同一个 缺陷,只是外了一层。文档化的机制是 `[feature-deps.<name>]`(docs/05 §2.8.2)。 同步中英两份 docs/05。 * test(e2e 328): 补上依赖那一条腿 —— 它才是这个特性的动机 自审读出:328 此前每一条断言都能被一个只给 packages[0] 打补丁的实现满足。而 docs/14 是为「供给某一层、并支持其下方多个实现的包」写的这个特性 —— 那是一个**库**, 以别人的依赖身份被走到。与本 pass 共用同一段窗口的 build.mcpp tail 只打补丁给根包 (对它自己的用途是对的),照抄那个形状会让这个特性唯一存在的对象没被服务,而所有 只看根包的断言照样全绿。 新增的这条带否定对照:依赖里不匹配的那一段必须不生效。 * docs(11): 记下 layers[].interface 的取值变化 它是机器接口上的一个字段,而取值从 `gnu` 变成了 `glibc`/`ucrt`。§6 承诺的是 字段的**含义**不变 —— 含义确实没变 —— 但按字面量取值的客户端会受影响,而契约页 不说这件事,就只能靠对方撞上。 * test(e2e 328): 层谓词不能选依赖 —— 补上这条的判据 实现了这条诊断,却从没跑过它 —— 而「判据写了、绿了、却从没跑到」正是本次发布在修的 那一族。带一条同样重要的对照:同一个谓词下的 build 输入必须**仍然生效**,否则一条 警告就是把一次静默丢弃换成了另一次。 * test(e2e 88): fixture 声明了两个从不存在的版本 `make@4.4` 与 `cmake@3.28` —— 索引里是 make 4.3 与 cmake 4.4.2/4.0.2,两个都从没 解析过。这一点此前不可见,因为 #531 的供给不读自己的结果:xlings 报出的每一种失败都 被当成成功。于是**这条最直接覆盖 `[xlings] deps` 的测试,是建立在一次从未成功的安装 之上做断言的** —— 正是 #531 想要终结的那个状态。 结果被读之后,fixture 的错误第一次可见:构建停下来了。这不是回归,是这条修复第一个 抓到的真实例子,而它抓到的是仓库自己的测试。 改用 `ninja@1.12.1`:mcpp 能构建的地方它一定已装,所以供给短路,这条测试仍然不花 任何下载。 * docs: 记下 D12 的第一个捕获对象是本仓库自己的测试 e2e 88 的 fixture 声明 `make@4.4` 与 `cmake@3.28`,而索引里是 make 4.3、 cmake 4.4.2/4.0.2 —— 两个版本从来不存在。这条测试在 #531 的整个生命周期里都是绿的, 因为供给不读自己的结果。 ⭐ 一般形状:**fixture 里的取值在有东西开始检查它们的那一刻,就不再是随意的了。** 在「这段文字有没有到达那个文件」是唯一断言的时候,它们只是自由字符串。 * docs(spec-003): 穷举核对写成读数,而不是「我数了一遍」 §4 原本给了一个退出码清单并声称穷举,而那个清单少了三处 `return 4`(pack/pipeline、 cmd_toolchain、pm/commands —— 都是同一个 `load_or_init` 失败,所以 `4` 的含义没变, 共 11 处而非 8 处),也没提 `20` 与 `1024`。 改成:贴出产生读数的命令,然后**按出处**逐个归类。⚠️ `4` 同时落在两栏 —— 它在 `index_management.cppm` 里是退出码,在 `runtime/elf.cppm` 里是 `R_RISCV_COPY`。 一份声称穷举的规范,自己就得能被复核。 * test(e2e 328): 两处只在 Windows 上才成立的问题 ⚠️ **`cfg(all(unix, …))` 在 Windows 上正确地为假**,于是 fixture 的 #error 触发, Windows e2e 1/2 变红。这条腿要证明的是「三元组键与层键**组合**」,那么它的三元组 那一半就必须在测试会跑的每个平台上为真。改成 `any(unix, windows)`。 「恰好一次」那条腿同理:三元组腿为假的地方,只有一条路能匹配,这个守卫就不再守任何 东西 —— 它存在的理由正是第一趟会经三元组腿匹配而第二趟经层腿匹配。 ⚠️ **注释里的反引号落在**未加引号**的 heredoc 里,变成了命令替换。** fixture 要 插值 $CABI 所以 heredoc 不能加引号;套件因此打印 `syntax error: unexpected end of file` 而**测试照样通过**。注释移到 heredoc 之外。 顺带:`-DPROBE_ONCE=1` 的计数改为同时接受 `/D`(Windows 自举可能驱动 MSVC)。 本机全量:355 passed / 3 failed —— 三条(62、168、208)在已发布的 2026.8.30.2 上以 相同消息失败,是本机 musl/glibc 载荷的缺口,对照已跑。 --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent aef5191 commit 3da6b0c

28 files changed

Lines changed: 2769 additions & 64 deletions

.agents/docs/2026-08-31-issue540-seven-audit-findings.md

Lines changed: 968 additions & 0 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,99 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [2026.9.1.1] — 2026-09-01
7+
8+
#540 的七条审计,加上核验它们时挖出的四条没有人报过的。它们几乎全是同一族:
9+
**mcpp 关于自己说了一句话,而 mcpp 不遵守它。**
10+
11+
完整核验、量化与设计见
12+
[`.agents/docs/2026-08-31-issue540-seven-audit-findings.md`](.agents/docs/2026-08-31-issue540-seven-audit-findings.md)
13+
14+
> **一条规则写在一处,却由它的一份手抄件来执行。** `kKnownBuildKeys`
15+
> `kKnownConditionalBuildKeys` 与 xpkg 的 `target_cfg` 列表,都是别处已有机器可读
16+
> 形式(紧挨其上的 `doc->get_*()` 读取点、`BuildInputs` 的成员表)的转录,三份都
17+
> 漂移了 —— 而 `kKnownBuildKeys` 漂移了**两次**,第二次就发生在描述第一次的注释上方
18+
> 八行。代价不是少了一条警告,而是**一条假的警告**:`[build] has unsupported key
19+
> 'std-module' (ignored)`,说的与实际发生的事情正相反。
20+
21+
### 修复
22+
23+
- **`[xlings] deps` 的供给从不检查自己是否成功。**(它的第一个真实受害者是本仓库自己的 e2e 88 —— 见 CHANGELOG 末尾) `xlings::call` 返回
24+
`expected<CallResult, string>`,只要子进程跑起来就处于****态 —— 能力自身的状态
25+
`CallResult` 里面,因为 xlings 讲完 NDJSON 协议后按设计退 0。调用点只测了
26+
`if (!r)`,于是 xlings 能报出的每一种失败都被读成了成功:
27+
28+
```
29+
$ mcpp build # deps = ["definitely-not-a-real-package"]
30+
Provisioning [xlings] deps (definitely-not-a-real-package)
31+
Finished dev [unoptimized + debuginfo] in 0.12s
32+
```
33+
34+
记号随后把这次假成功变成**永久**的 —— 下一次构建连 `Provisioning` 都不再打印。
35+
#531 自己的注释写着它修的缺陷是「声明看起来被接受了却什么都没做,这是一个配置键
36+
能有的最坏形态」;没人读结果,它的修法重现了那个形态。正确写法就在同一个文件里
37+
——依赖安装路径写的是 `if (r && r->exitCode != 0 && …)`
38+
39+
- **该路径不认 `MCPP_OFFLINE`,也不认 `MCPP_NO_AUTO_INSTALL`** 它自称与
40+
`[toolchain]` 平权,而那条先例在任一开关下**硬错**并且报出触发的是哪一个。现在
41+
两个都认,拦的是安装**动作**而不是整块 —— 已供给好的工程仍然离线构建得出来。
42+
43+
- **记号记录的是全局效果,却存在项目里。** 安装落在 registry(刻意如此,原注释说明
44+
了理由),而 `<project>/.mcpp/.xlings-deps.stamp` 记着它。清掉或换掉 `MCPP_HOME`,
45+
项目仍然声称已装;`mcpp clean` 只删 `target/`,也清不掉。改按依赖列表的哈希存进
46+
registry,并且**只在成功时写**
47+
48+
- **`[build] std-module` / `std-compat-module` / `std-module-flags` 被读取,却被报成
49+
unsupported。** 三个键在 #494 被移入 `[build]` 正是为了让它们可条件化,而
50+
`kKnownBuildKeys` 从未收录 —— 唯一一句关于它们的话说反了。
51+
52+
- **条件轴拒绝 `BuildInputs` 的两个成员。** `std-module-flags`(#494 就是为这条轴
53+
才把它挪上来的)与 `private_include_dirs` —— 后者更严重:xpkg 描述符的
54+
`target_cfg` 块,也就是**同一条轴的另一套语法**,是接受它的。两条列表的消息现在都
55+
由列表本身生成。
56+
57+
- **`[features]` 是唯一一个完全没有 schema 检查的结构化段落。**`include_dirs`
58+
误写进 feature 里会零诊断地构建成功,而同样的错误写在 `[build]` 里会被报出来。
59+
60+
- **`mcpp build --help` / `mcpp test --help` 说默认档位是 release,而它是 dev。**
61+
六处说得对(解析器、它的注释、docs/05、一条 e2e、mcpp 自己的 mcpp.toml、
62+
`mcpp pack --help`),两处说错。`prepare.cppm` 里那条字段注释是没被报告的第三处。
63+
64+
- **`mcpp index update <name>` 承诺按索引筛选,而它只筛项目级索引。** 限制此前只写在
65+
一条注释里 —— 一个只有实现者看得到的地方,从外面看与「这功能坏了」无从区分。
66+
67+
### 新增
68+
69+
- **`[target.'cfg(<层> = "…")'.build]` —— 按已解析目标侧条件化(#494 / #540)。**
70+
docs/14 用一整节记载了这个能力,包括为什么它不能用 feature 选择代替;而
71+
`cfgpred::Ctx` 只由三元组构造,所以每一个这样的段落都被**静默**丢弃,包成功构建
72+
在错误的 C 库配置上。五个层名 `compiler` / `compiler-runtime` / `kernel-abi` /
73+
`c-abi` / `c++-abi` 现在是谓词的键,可与三元组键在 `all`/`any`/`not` 下组合。
74+
75+
⚠️ 层谓词**不能**选择依赖 —— 层是从依赖图解析出来的 —— 这种段落会被报出并忽略,
76+
而不是被静默丢弃。
77+
78+
- **mcpp 不认识的 cfg 键会被报出来。** 求值器过去对未知键返回假,而那与「这一段本就
79+
不该匹配」读数完全相同。词汇表从求值器**导出**而不是被转录 —— 否则这条诊断自己就
80+
会成为本次发布正在修的那第五份手抄件。
81+
82+
- **[`docs/spec/exit-codes.md`](docs/spec/exit-codes.md)(SPEC-003)。** 2026-08-08
83+
的协议设计文档 §R4 把这份契约指派给了 `docs/spec/`,它一直没有写。`docs/11` 那张表
84+
落地的是 usage/internal 的一半;命令**跑了并且失败**时返回的 `1` 既不在表里也不在
85+
别处 —— 而按信封命令划定的那张表**给不出** `4`
86+
87+
### 变更
88+
89+
- **`c-abi` 层报的是库名,不再是三元组的 env 段。** 二者在 `musl` 上重合,在 `gnu`
90+
上分叉:Linux 上它请求的是 glibc,Windows 上它命名的是工具链的 MinGW 形态,而后者
91+
的 C 运行时是 UCRT。docs/14 一直把实现列作 `glibc`/`musl`/`picolibc`,e2e 296 的
92+
文件头也把它期望的报告写作 `c-abi glibc (payload)`。在这个值只被打印的年代这只是
93+
措辞不一致;它现在是用户书写的谓词值。请求那一侧保留三元组的拼写(规范 §3.4),
94+
两者经 `c_abi_request_satisfied` 比较而非按相等。
95+
96+
- **`mcpp::target_libc()` 的文档改为它实际回答的问题** —— 供给 sysroot 的那个**载荷**
97+
包,而这个值是目标侧解析的一项**输入**。要按已解析的层分支,用层谓词。
98+
699
## [2026.8.30.2] — 2026-08-30
7100

8101
六处缺陷,来自 #527 / #529 的分析,外加一处在实现 review 时挖出来、没有人报过的。

docs/05-mcpp-toml.md

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,16 +1114,31 @@ for arch/env conditions and combinators.
11141114
`build` with `cflags` / `cxxflags` / `ldflags` / `sources` (mcpp 0.0.95+ —
11151115
conditional source globs, e.g. gating `src/x86/**/*.asm` behind
11161116
`cfg(arch = "x86_64")`; `!`-exclusion globs work here too), plus `flags` and
1117-
`include_dirs` / `include_dirs_after` (mcpp 0.0.102+).
1117+
`include_dirs` / `include_dirs_after` (mcpp 0.0.102+), plus
1118+
`private_include_dirs` and `std-module-flags` (mcpp 2026.9.1.1+).
11181119
- **What `build` accepts is exactly the set of *additive build inputs*** — the
11191120
things that combine by appending and are consumed after the predicate is
1120-
evaluated. `linkage`, `target`, and the profile knobs are deliberately not
1121-
among them: they are *inputs to* target selection (conditioning `target` on
1122-
a predicate evaluated against `target` is circular), or they need
1123-
override-rather-than-append semantics.
1121+
evaluated, which is the member list of `BuildInputs`. `linkage`, `target`,
1122+
and the profile knobs are deliberately not among them: they are *inputs to*
1123+
target selection (conditioning `target` on a predicate evaluated against
1124+
`target` is circular), or they need override-rather-than-append semantics.
1125+
A key outside the set is reported and ignored; the message lists the set it
1126+
checked against, so it cannot drift from the check.
11241127
- **Evaluated against the resolved target** — the `--target` triple for a cross
11251128
build, otherwise the host. So a native Linux build never even *downloads* a
11261129
`[target.windows]` dependency.
1130+
- **Predicate keys**: `os`, `arch`, `family`, `env` — the triple's coordinates —
1131+
and, from mcpp 2026.9.1.1, the five target-side layer names `compiler`,
1132+
`compiler-runtime`, `kernel-abi`, `c-abi`, `c++-abi`
1133+
([14 — The Target Side](14-target-side.md)). Barewords `linux` / `macos` /
1134+
`windows` / `unix` are sugar for the matching `os` / `family` test. A key
1135+
outside this set is reported as a schema warning and the section does not
1136+
apply — it used to answer false in silence, which is indistinguishable from
1137+
a section that correctly did not match.
1138+
- **A layer predicate cannot select dependencies.** A layer is resolved *from*
1139+
the dependency graph, so a dependency chosen by one would decide the answer
1140+
it is asking for. `[target.'cfg(c-abi = "musl")'.dependencies]` is reported
1141+
and ignored; the `build` inputs under the same predicate do apply.
11271142
- **Precedence**: an exact-triple table wins over a `cfg`/alias table; multiple
11281143
matching predicate tables have their flags concatenated. Conditional entries
11291144
are appended **after** the unconditional `[build]` ones, so under GNU
@@ -1178,11 +1193,21 @@ The value is an xpkg reference or the empty string; a bare name is rejected when
11781193
the manifest is parsed, because accepting it would install nothing and then fail
11791194
much later naming a missing libc.
11801195

1181-
A build program can ask which C library was resolved: `mcpp::target_libc()`
1182-
returns its package name and `mcpp::target_libc_profile()` the sub-directory for
1183-
the target's ISA profile. Both are empty on the zero-libc tier. See
1196+
A build program can ask which C library **payload** supplies the sysroot:
1197+
`mcpp::target_libc()` returns that package's name and
1198+
`mcpp::target_libc_profile()` the sub-directory for the target's ISA profile.
1199+
Both are empty on the zero-libc tier. See
11841200
[13 — Bare-Metal and Freestanding Targets](13-baremetal.md).
11851201

1202+
⚠️ **That is not the same question as "which C library did the target side
1203+
resolve to".** `target_libc()` names the payload mcpp installed, and that value
1204+
is an *input* to target-side resolution — a package in the dependency graph can
1205+
supply the C library instead, in which case the resolved `c-abi` is not what
1206+
this returns. To branch on the resolved layer, use a layer predicate:
1207+
`[target.'cfg(c-abi = "musl")'.build]` ([14 — The Target
1208+
Side](14-target-side.md)). This paragraph said "which C library was resolved"
1209+
until 2026.9.1.1, which was the wrong one of the two.
1210+
11861211
### 2.7.2 Bare metal (`os = none`) — freestanding targets
11871212

11881213
`riscv64-none-elf` and `riscv32-none-elf` are targets with no operating system
@@ -1304,6 +1329,13 @@ simd = { sources = ["src/simd/**"], flags = [
13041329
{ glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"] } ] }
13051330
```
13061331

1332+
- **The table form accepts exactly** `implies`, `forward`, `defines`, `sources`,
1333+
`flags`, `requires`, `provides`. Anything else is reported as a schema warning
1334+
and ignored (mcpp 2026.9.1.1+); `deps` is reported separately as reserved and
1335+
points at `[feature-deps.<name>]`. Before that release `[features]` was
1336+
the one structured section with no schema check at all, so a misplaced
1337+
`include_dirs` inside a feature built successfully with no diagnostic while
1338+
the identical mistake in `[build]` was reported.
13071339
- `defines` are **bare** macro names (no `-D`); each desugars to `-D<x>` on the
13081340
package's own compile when the feature is active — exactly like `[targets.*]
13091341
defines`. They are restricted by convention to the package's **own** namespaced

docs/11-machine-output.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,26 @@ A request that does not yet know what it will be given must not write into the
100100
channel the protocol owns. Combined with §1, a client's rule is complete: no
101101
JSON on stdout means "not supported", whatever the reason.
102102

103-
Exit codes:
103+
Exit codes **of the enveloped commands** — the kinds `--protocol-version`
104+
advertises. This table is scoped to them on purpose; a code another command
105+
returns is not in it, and adding one would document something these commands
106+
cannot produce. The full mapping across all of mcpp is
107+
[the exit-code contract](spec/exit-codes.md).
104108

105109
| code | meaning |
106110
|---|---|
107111
| 0 | success |
112+
| 1 | the command ran and failed — see stderr, and `diagnostics` when stdout carries an envelope |
108113
| 2 | usage error — unknown option, unsupported value |
109114
| 70 | internal error (uncaught exception) |
110115
| 127 | unknown command |
111116

117+
⚠️ **`1` can arrive with an envelope on stdout.** `mcpp xpkg parse` reports a
118+
descriptor that violates the name form as JSON *and* exits 1: the document is
119+
the answer, and the exit code says the answer is a rejection. §1 still holds —
120+
parse stdout, do not branch on the code — but a client that treats any non-zero
121+
exit as "no output" will discard a document it was given.
122+
112123
## 4. Effects — what a command does before it prints
113124

114125
An IDE with an untrusted-workspace gate has to decide **before** running.
@@ -309,6 +320,16 @@ A field was added rather than `cLibrary` renamed or `mode` widened, because §6
309320
promises that fields are added and never removed and that a field's meaning
310321
never changes.
311322

323+
⚠️ **`layers[].interface` changed VALUE for a payload-supplied glibc in
324+
2026.9.1.1** — from `gnu` to `glibc`, and on Windows from `gnu` to `ucrt`. The
325+
field's meaning is unchanged (it still names the implementation), so §6 holds;
326+
what changed is that it stopped reporting the triple's env segment, which is a
327+
request rather than an implementation and is not the name of any C library. The
328+
values are now the ones [14 — The Target Side](14-target-side.md) has always
329+
listed, and a package may compare against them in a `cfg(c-abi = …)` predicate.
330+
A client keying on the literal `gnu` needs updating; `musl`, `picolibc` and
331+
`libSystem` are unaffected.
332+
312333
**`reason` is a token, not a sentence.** The refusal's message is still
313334
written for a person and still names the target, the rule and the way out — but
314335
a program classifying the outcome reads `reason`:

docs/13-baremetal.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -555,12 +555,22 @@ A board-support package is an ordinary mcpp package. It declares the emulator
555555
it needs under `[xlings] deps`, exports one C++ module for consumers, and emits
556556
its board facts from `build.mcpp`.
557557

558-
⚠️ **A declaration under `[xlings] deps` is not an install trigger.** It is what
559-
lets `mcpp::xpkg_dir` answer *"where did that package land"*; it installs
560-
nothing. A board package listed in the index descriptor's platform `deps` gets
561-
its emulator installed with it, and never meets this. A project that declares
562-
its own — because no board package serves its machines — will find `xpkg_dir`
563-
empty on a clean machine and must say so:
558+
**A declaration under `[xlings] deps` provisions the package on the first
559+
build** (since 2026.8.29). It is also what lets `mcpp::xpkg_dir` answer *"where
560+
did that package land"*. Both halves matter: the same declaration installs the
561+
emulator and tells the build program where it went.
562+
563+
The provisioning is the contract `[toolchain]` has always had — declare it,
564+
mcpp installs it on first use — and it obeys the same two knobs: under
565+
`--offline` / `MCPP_OFFLINE` or `MCPP_NO_AUTO_INSTALL` mcpp refuses instead,
566+
naming the packages so they can be installed out of band.
567+
568+
⚠️ **A build program still must not assume the directory exists.** Provisioning
569+
runs for the package that DECLARES the deps; a build program can be reached
570+
through paths where that has not happened — a dependency of a project that
571+
declares nothing, an environment where the knobs above refused — so
572+
`xpkg_dir` may still come back empty and the program must say so rather than
573+
emitting a broken runner:
564574

565575
```cpp
566576
if (const char* dir = mcpp::xpkg_dir("xim", "qemu-riscv"); dir && *dir) {

docs/14-target-side.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ The target side of a build consists of five layers.
1717
| `compiler` | the program that compiles | `llvm`, `gcc`, `msvc` |
1818
| `compiler-runtime` | the compiler's own runtime: integer and floating-point builtins, the unwinder | `compiler-rt` with `libunwind`, `libgcc` |
1919
| `kernel-abi` | the platform interface, or its equivalent | `linux`, `windows`, `darwin`, `openkal` |
20-
| `c-abi` | the C library | `glibc`, `musl`, `picolibc` |
20+
| `c-abi` | the C library | `glibc`, `musl`, `picolibc`, `ucrt`, `libSystem` |
2121
| `c++-abi` | the C++ library and its ABI runtime | `libc++` with `libc++abi`, `libstdc++`, MSVC STL |
2222

2323
### Membership Criteria
@@ -263,10 +263,38 @@ Requiring a feature selection for this would oblige a project to restate what
263263
the target triple or its dependency graph has already established, and permit
264264
the two statements to disagree.
265265

266+
The predicate keys are the five layer names, and their values are the interface
267+
names in the table at the top of this chapter — the same strings the `Target`
268+
report prints. They combine with the triple keys under `all`/`any`/`not`:
269+
270+
```toml
271+
[target.'cfg(all(linux, c-abi = "musl"))'.build]
272+
cxxflags = ["-D_GNU_SOURCE"]
273+
```
274+
275+
⚠️ **A layer names the library, not the triple's env segment.** They coincide
276+
for `musl` and diverge for `gnu`: on Linux that segment asks for glibc, and on
277+
Windows it names the MinGW flavour of the toolchain, whose C runtime is the same
278+
UCRT the MSVC flavour links. The spelling is `c-abi = "glibc"`, never
279+
`c-abi = "gnu"`; the request, as opposed to the answer, is `env = "gnu"` — a
280+
different question (`docs/spec/target-side.md` §3.4).
281+
282+
⚠️ **`env` and `c-abi` are not interchangeable.** `env` is what the triple
283+
*asked* for; `c-abi` is what the graph and the payload *answered*. An
284+
`openkal-musl` in the dependency graph supplies musl under an `x86_64-linux-gnu`
285+
triple, and only `c-abi` sees that.
286+
266287
These predicates are available in `[build]` sections only. The target side is
267288
resolved after dependency resolution, so a dependency selected by one would
268-
form a cycle. A package whose C libraries require different dependencies is
269-
split per C library, or depends on the union and selects sources in `[build]`.
289+
form a cycle; `[target.'cfg(<layer> = …)'.dependencies]` is reported and
290+
ignored rather than silently dropped. A package whose C libraries require
291+
different dependencies is split per C library, or depends on the union and
292+
selects sources in `[build]`.
293+
294+
A key mcpp does not know — a typo, or a predicate from a newer mcpp — is
295+
reported as a schema warning and the section does not apply. It used to
296+
evaluate to false in silence, which reads exactly like a section that correctly
297+
did not match.
270298

271299
## Diagnostics
272300

docs/17-the-project-environment.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,18 @@ gives the full rule.
111111
question does not arise.
112112

113113
Against that: an isolated environment is a directory that has to be created and
114-
populated before the first build, and mcpp will not do it. A project whose
115-
tools are ordinary and whose versions do not matter is better off declaring
116-
nothing and inheriting the machine's.
114+
populated, and the first build pays for it. Since 2026.8.29 mcpp does that
115+
work — a declared `[xlings] deps` is provisioned on first use, and a named
116+
`[xlings] subos` that does not exist yet is created rather than refused — but
117+
the cost is real: the first build on a clean machine downloads and installs
118+
before it compiles anything. A project whose tools are ordinary and whose
119+
versions do not matter is better off declaring nothing and inheriting the
120+
machine's.
121+
122+
Under `--offline` / `MCPP_OFFLINE` or `MCPP_NO_AUTO_INSTALL`, mcpp refuses
123+
instead of installing, and names the packages so they can be provisioned
124+
out of band — the same two knobs `[toolchain]` honours, for the same reason: an
125+
unasked-for download is not something a build decides on a project's behalf.
117126

118127
## 6. What belongs somewhere else
119128

docs/spec/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
|---|---|---|---|---|
3333
| [SPEC-001](package-identity.md) | 包身份(`package.namespace` / `package.name`)、`[dependencies]` 选择器与匹配机制 | 评审中 v1.1 | 2026-08-03 | mcpp >= 0.0.106 |
3434
| [SPEC-002](target-side.md) | 目标侧模型与能力声明(`mcpp:` 保留命名空间、五层、三条规则) | 评审中 v1.0 | 2026-08-24 | mcpp >= 2026.8.24.2 |
35+
| [SPEC-003](exit-codes.md) | 退出码契约(分类、语义、稳定性承诺) | 评审中 v1.0 | 2026-09-01 | mcpp >= 2026.9.1.1 |
3536

3637
## 文档约定
3738

0 commit comments

Comments
 (0)