feat(targetside): five layers, and the largest of them had no name - #494
Merged
Conversation
一次真实使用暴露了十条问题:一个 hello-world 工程,`[dependencies]` 里只加了一行 `openkal-llvm-runtime`,随后在四个目标上连续失败,每一次的错误信息都来自编译器, 没有一条提到 mcpp 作出的决定。 `01d6cef` 把「目标侧从哪来」收敛到了一处解析,但它的消费者仍停在旧模型上。 本次补齐模型本身。 ── 1. 第五层:compiler-runtime ────────────────────────────── 实测 `openkal-llvm-runtime` 编译的 729 个对象里,**498 个是 compiler-rt 的 builtins**、21 个是 libunwind 的。这个包最大的一块此前声明在 `mcpp:c++-abi` 名下 —— 而 `__udivti3` 是一个纯 C 程序需要的东西,与 C++ 无关。 把 builtins 算作 C++ 运行时的一部分,与本文件开头记录的那个缺陷同形: 一个交叉到 macOS 的 C 程序被问「有没有 C++ 运行时」,答「没有」, 链接行因而保留了载荷自带的 libc++。**一个只有部分程序需要的层仍然是层。** `compiler` 同时成为一层,因为规则二需要一个被检查的对象。它是唯一一个 包不能供给的层:族与族之间的差异(flag 拼写、模块模型、BMI 格式、驱动 cfg) 是引擎必须持有的事实,不是数据能描述的。⚠️ `compiler` 层上报的是**族名**(`llvm`)而不是驱动名(`clang`)。 使用者书写的每一处都用族名,报告用驱动名会让 `requires = ["mcpp:compiler=llvm"]` 永远不可满足。 ── 2. requires:在引擎里不出现实现名的前提下执行分层规则 ──── requires = ["mcpp:compiler=llvm"] libc++ 的源码、尤其它的 std 模块源,由 clang 编译。把它递给 gcc 会在 libc++ 自己的头文件深处失败。该事实属于包 —— 在引擎里写 `if (stdlib == "libc++" && compiler == gcc)` 会把两个实现名放进引擎。 实测,修复前后同一条命令: error: std module precompile failed (rc=1): …/std.cppm:16: fatal error: __config: No such file or directory error: `openkal-llvm-runtime@0.1.1` requires the compiler to be `llvm`. compiler gcc (16.1.0, payload) required llvm (required by openkal-llvm-runtime@0.1.1) Select that compiler — yours outranks mcpp's own default: mcpp toolchain default llvm⚠️ 检查在**编译开始之前**运行,这正是声明它的全部意义。 ── 3. 规则一:每层恰好一个供给者 ──────────────────────────── 此前两个包供给同一层时,**图遍历顺序里第一个静默胜出** —— 那个顺序既不是 作者写的,也不是他能预测的 —— 而落选者的 `[build]` 段仍然进入命令行, 于是一份 C 库的头配另一份 C 库的实现。 C 库、内核接口、C++ 运行时是**互斥的选择**,不是可叠加的贡献; `[build] runner` 早已按同一条规则处理。判据是失败模态:选错不会让链接失败, 会得到一个能跑、偶尔崩的程序。 ── 4. std-module* 从 [package] 移入 [build] ──────────────── 模块源是这个包的一个翻译单元 —— 它以包的 include 目录与定义被编译, 引擎自己的注释早就这么写了。放在 `[package]` 下损失的恰恰是位置所决定的那件事: `[build]` 可条件化而 `[package]` 不可,于是一个在多种 C 库之上供给同一 C++ 运行时的包,无法为不同 C 库给出不同的 flag。`-D_GNU_SOURCE` 对 musl 与 glibc 是对的,对 picolibc 是错的,而此前没有拼写能表达这个差别。 `[package]` 写法保留为别名。 ── 5. 报告按需暴露 ──────────────────────────────────────── 零配置构建的五个层全部解析自同一份载荷,五行 `(payload)` 回答的是无人提出的 问题。默认只列出来源不是编译器载荷的层;`MCPP_VERBOSE=1` 列出全部; **诊断始终列出它所依据的每一层**,包括平凡的那些 —— 省略证据的错误信息 无法被读者复核。 Target x86_64-linux-gnu ← 零配置:一行 ── 6. Family 去掉 OpenkalLlvm ────────────────────────────── 它命名同一份 llvm 载荷,携带的是一条关于目标侧的事实,而 `mcpp.targetside` 从包的声明中解析该事实。保留枚举项的代价不止是一条死分支: 可用工具链列表按族枚举,一份载荷挂在两个族名下就出现两次, 而安装状态按族记录 ⇒ **第二份被报成未安装,并被推荐给已经装了它的人。** 拼写归一为 `llvm` 保留(e2e 269 守着)。 ── 7. 词表 pin 替换用户默认时,说出来 ──────────────────────⚠️ 让全局默认压过词表 pin 的做法被实测否掉:一个无依赖的工程、全局默认 `llvm@22.1.8`、`--target x86_64-windows-gnu`,**从能构建变成不能构建** —— clang 单独不携带该目标的 C 运行时,而行所命名的载荷携带。那是升级把一个 可用的构建变成不可用的。 行所 pin 的不是「偏好的编译器」,而是「供给该目标 C 库的载荷」; 用户的默认能否代替它,取决于是否有别的东西供给目标侧 —— 而那要到依赖图 解析之后才知道。因此保留行为,并补上两条它一直欠缺的话: Resolved gcc@16.1.0 → x86_64-windows-gnu → … target default for x86_64-windows-gnu, replacing your llvm@22.1.8 — override with `[target.x86_64-windows-gnu] toolchain` warning: this project's target side comes from its dependency graph, so llvm@22.1.8 would have served x86_64-windows-gnu. 第二条在图已知之后发出 —— 那是「这次替换本可不必」第一次可判定的时刻。⚠️ 结构性修法是把 pin 的决定与目标侧一样后移。未在本次落地:`tc` 在解析后到 图之间被读写 **39 处**(有效三元组、cross flag、目标 sysroot、MSVC 运行时契约), 在那里重解析会让它们乱序重做。 ── 兼容性 ──────────────────────────────────────────────── * `hosted-standard-library` 继续表示 C++ 层; * `openkal-llvm` 拼写继续解析; * `[package]` 下的三个 std-module 键继续被接受; * 实测:**旧引擎(2026.8.24.1)读带 `requires` 的清单构建成功** —— TOML 侧忽略未知键,xpkg 侧警告而非报错。已发布的包因此可以先行声明。 ── 测试 ────────────────────────────────────────────────── 单元:test_targetside 26 个(五层 × 四来源、能力语法五层全覆盖、 规则一二各自的诊断文本),全套 93 passed / 0 failed。 e2e:新增 280(五层与报告收窄)、281(两条规则各自的拒绝 + 包不得供给 compiler); 268/269 的断言改用 MCPP_VERBOSE 读取全栈 —— 它们的意图不变, 变的是默认报告不再打印载荷层。⚠️ 本机 e2e 279 条中 26 条红,**逐条与基线二进制对照后全部为既有失败** (本机全局默认是 llvm,而它们断言 GCC 的 `gcm.cache`)。CI 是判据。 设计文档:.agents/docs/2026-08-24-target-side-design.md 规范:docs/spec/target-side.md(SPEC-002) 使用文档:docs/14-target-side.md + docs/zh/14-target-side.md
…s row
自查发现:`format_layers` 用「到目前为止有没有打印过东西」来决定一个缺席的层
要不要打印,而这让它依赖**行序**。
裸机构建的 `kernel-abi` 是缺席的,并且排在预制的 `c-abi` 之上,于是:
Target riscv64-none-elf
c-abi picolibc-riscv (xim:picolibc-riscv@1.8.12, prebuilt)
c++-abi —
缺席的 `kernel-abi` 被吞掉了,而下方同样缺席的 `c++-abi` 打印了 ——
两个同为「这一层没有供给者」的陈述,一个在场一个不在场。
「一台裸机没有内核」正是这份报告要说的话之一。改成两遍:
先判定这次是否展示整栈,再逐行输出。
Target riscv64-none-elf
kernel-abi —
c-abi picolibc-riscv (xim:picolibc-riscv@1.8.12, prebuilt)
c++-abi —
test: TargetSideReport.AnAbsentLayerAboveAPrintedOneIsStillPrinted;
test_targetside 27 passed。
…ap, not a typo⚠️ 这条由生态 CI 实测暴露,而它让层名词表**对已发布的包永远不可扩展**。 `openkal-llvm-runtime` 声明本次新命名的那一层,被上一版引擎读到: error: dependency 'openkal-llvm-runtime': mcpp.toml: error: `provides = ["mcpp:compiler-runtime=compiler-rt"]` names no capability mcpp knows. 保留前缀是闭集,为的是让**拼写错误成为错误而不是一个被静默禁用的行为**。 在解析期直接拒绝,让这个闭集在第二个、没人打算要的意义上也闭上了: 一个声明了「读者发布之后才被命名的层」的包,**整份清单加载不了**。 ── 谁的清单决定答案 ────────────────────────────────────── * **根工程**的清单里出现未知层名 ⇒ **错误**。那是作者自己的拼写, 而他正看着这次构建。 * **依赖**的清单里出现未知层名 ⇒ **警告并忽略该层**。那份清单是对着一个更新的 引擎写的,而「忽略未知的并说出来」正是本引擎对其它每一种未知键已有的做法 (`warn_unknown_xpkg_keys` 的注释:should not fail outright, only tell the user what it ignored)。⚠️ 警告放在扫描**全部包**的那个循环里,不放在 `warn_unknown_xpkg_keys`: 后者只走到经索引解析的依赖,而 path / git 依赖自带清单,先前一条警告都收不到。⚠️ 这条规定只对**此后的**引擎生效。一个包若要声明某个层名,其使用者的引擎仍须 不早于该层名被引入的版本 —— 本次修的是「从今往后可扩展」,不是追溯。 test: e2e 281 增两格(依赖声明未知层 ⇒ 构建成功且点名被忽略的层; 根工程拼错 ⇒ 报错并列出五个层名);unit 93 passed。 spec: SPEC-002 §5.1 拆成两条;docs/14 中英同步。
Sunrisepeak
added a commit
to mcpplibs/openkal-llvm-runtime
that referenced
this pull request
Aug 24, 2026
⚠️ 实测:该层名在 2026.8.24.2 之前的每一个已发布构建工具上都让整份清单 加载失败 —— 不是「该层被忽略」,是这个包用不了: error: dependency 'openkal-llvm-runtime': mcpp.toml: error: `provides = ["mcpp:compiler-runtime=compiler-rt"]` names no capability mcpp knows. 那条拒绝本身已在 mcpp 2026.8.24.2 修掉(依赖的清单里出现未知层名改为警告并 忽略,根工程自己的仍然报错),但**修复不能追溯到已经发布出去的工具**。 本仓 CI 的 MCPP_VERSION 目前是 2026.8.19.4。 `requires = ["mcpp:compiler=llvm"]` 保留 —— 实测旧引擎忽略未知的 [package] 键, 因此这一半今天就能发。声明留在注释里,连同它的依据与解除条件。 engine: mcpp-community/mcpp#494
生态 e2e 暴露两处: 1. 警告只说了「不认识」,没有列出**存在的**层名 —— 而它替代的那条错误列了。 一条比它所替代的错误说得更少的警告,是一条穿着更轻处罚的更差诊断。 两处警告(依赖清单、任意包)改为复用 `parse_capability` 的原文。 2. e2e 268 的断言写的是**严重级别**,而这个前缀存在的理由是**不静默**。⚠️ 但一条断言若跟着策略改就该说清它现在测什么:改为断言三件事 —— 拼错的名字被报出、被拼错的那一层**没有被填上**、以及旁边拼对的那一条 **仍然解析**(一个坏条目只该花掉一层,不是整个包)。 删掉的那条「必须在编译开始之前被拒绝」不再成立,而它成立的地方仍被守着: 根工程自己的拼写错误与 `requires` 不满足,都在 e2e 281 里,都在编译之前。 unit 93 passed;e2e 268/269/280/281 全绿。
macOS CI 报出两件事,一件是我的测试写错,一件是这份报告刚刚让一个既有缺陷
变得可见。
── 1. `c-abi glibc (payload)`,在 macOS 上 ────────────────
`resolve` 对没有 env 段的三元组回落到字面量 `glibc`,而 macOS 的规范三元组
正是没有 env 段的。这条一直在,但此前报告只打印「这次构建有话要说」的那几层,
于是一个错误的标签从未被打印出来;把整栈显示出来,错标签就成了错陈述。
c-abi glibc (payload) ← macOS
改为按平台取名:macOS `libSystem`、Windows `ucrt`、其余 `glibc`;
三元组自己说了 env 的仍以它为准。
⚠️ 这几个名字可以写在引擎里,而包供给的实现名不可以 —— 区别在于载荷是 mcpp
自己分发的,它知道里面装着什么。保留能力语法存在的理由正是守住这条界线。
── 2. `c++-abi` 在 BSD grep 的 ERE 里是非法的 ────────────
grep: repetition-operator operand invalid
`c++` 在扩展正则里是「重复算子作用于重复算子」,GNU grep 容忍,BSD grep 直接
拒绝。e2e 280 的层名循环改用 `grep -F` 定长匹配。
test: TargetSideResolve.ThePayloadCLibraryIsNamedForItsPlatform;
unit 93 passed;本机全量 e2e 288 pass / 26 fail,失败集与基线逐条相同。
Sunrisepeak
force-pushed
the
feat/target-side-five-layers
branch
from
August 24, 2026 11:48
995c962 to
3c45cfe
Compare
Sunrisepeak
added a commit
to mcpplibs/openkal-llvm-runtime
that referenced
this pull request
Aug 24, 2026
…layer⚠️ 实测:该层名在 2026.8.24.2 之前的每一个已发布构建工具上都让整份清单 加载失败 —— 不是「该层被忽略」,是这个包用不了: error: dependency 'openkal-llvm-runtime': mcpp.toml: error: `provides = ["mcpp:compiler-runtime=compiler-rt"]` names no capability mcpp knows. 那条拒绝本身已在 mcpp 2026.8.24.2 修掉(依赖的清单里出现未知层名改为警告并 忽略,根工程自己的仍然报错),但**修复不能追溯到已经发布出去的工具**。 本仓 CI 的 MCPP_VERSION 目前是 2026.8.19.4。 `requires = ["mcpp:compiler=llvm"]` 保留 —— 实测旧引擎忽略未知的 [package] 键, 因此这一半今天就能发。声明留在注释里,连同它的依据与解除条件。 engine: mcpp-community/mcpp#494
Sunrisepeak
added a commit
to mcpplibs/openkal-windows
that referenced
this pull request
Aug 24, 2026
…C library
-[target.'cfg(all(windows, env = "gnu"))'.build]
+[target.'cfg(all(windows, not(env = "msvc")))'.build]
ldflags = ["-lntdll", "-lsynchronization", "-lshell32", "-lkernel32"]
这四个是 Win32 导入库 —— 本包所实现的**平台接口**的性质,不是 C 库的性质。
`env = "gnu"` 一直在代表「GNU/PE 的 ABI 而不是 MSVC 的」,而在传统栈上两者重合。
它们在 C 库改由依赖图供给的那一刻不再重合:一次 `x86_64-windows-gnu` 的
openkal 构建解析出的 C 库是 musl —— mcpp 自己的报告就这么打印:
kernel-abi openkal (openkal-windows@0.1.3, graph)
c-abi musl (openkal-musl@0.3.3, graph)
而拼作 `x86_64-windows-musl` 的是同一次构建的诚实名字。旧谓词下第二个拼法
不带这四个库中的任何一个,实测:
ld.lld: error: undefined symbol: __declspec(dllimport) GetStdHandle
ld.lld: error: undefined symbol: __declspec(dllimport) WriteFile
…
两份 build.ninja 的 ldflags 逐 token 对比,差别只有这四个 token;
连 `--target=x86_64-w64-windows-gnu` 都完全相同 —— 两个 mcpp 三元组翻译成
同一个 LLVM 三元组。
实测(改动后):
Finished dev in 2.75s
test3.exe: PE32+ executable (console) x86-64, 14 sections
imports: ntdll.dll / api-ms-win-core-synch-l1-2-0.dll / SHELL32.dll / KERNEL32.dll
wine test3.exe → Hello from test3!
`cxxflags = ["-fno-exceptions", "-fno-rtti"]` 在同一张表里,因此一并生效于
两种拼法 —— 那也是对的:它们的理由同样是 ABI 而非 C 库。
⚠️ 兼容性:`x86_64-windows-gnu` 的求值结果不变(`gnu != msvc`),
新增覆盖的是 `msvc` 之外的其它 env 拼法。
related: mcpp-community/mcpp#494
Sunrisepeak
added a commit
to mcpplibs/openkal-llvm-runtime
that referenced
this pull request
Aug 24, 2026
…eeds
这个包编译 729 个对象,其中 **498 个是 compiler-rt 的 builtins**、21 个是
libunwind 的、159 个 libcxx、51 个 libcxxabi。也就是说它最大的一块此前声明在
`mcpp:c++-abi` 名下 —— 而 `__udivti3` 及其同类是一个**纯 C 程序**需要的东西,
与 C++ 无关。
把 builtins 算作 C++ 运行时的一部分,等价于断言 C 程序不需要整数除法。
该断言已经产生过一次实测缺陷:一个交叉到 macOS 的 C 程序被问「有没有 C++
运行时」,答「没有」,链接行因而保留了编译器载荷自带的 libc++,
把一个 Linux 共享对象递给了 Mach-O 链接器。
provides = [..., "mcpp:compiler-runtime=compiler-rt"]
── requires ──────────────────────────────────────────────
libc++ 的源码、尤其它的 std 模块源,由 clang 编译。递给 gcc 的实测结果:
fatal error: __config: No such file or directory
该消息命名一个读者从未打开过的文件,以及一个 mcpp 从未作出的决定。
把需求写在包里,构建工具就能在编译任何东西之前拒绝这个组合,
并且能够指出族名,而不必把这条事实写进构建工具:
requires = ["mcpp:compiler=llvm"]
实测(mcpp 2026.8.24.2):
error: `openkal-llvm-runtime@0.1.1` requires the compiler to be `llvm`.
compiler gcc (16.1.0, payload)
required llvm (required by openkal-llvm-runtime@0.1.1)
Select that compiler — yours outranks mcpp's own default:
mcpp toolchain default llvm
── 兼容性 ────────────────────────────────────────────────
⚠️ 实测:**旧引擎(mcpp 2026.8.24.1)读带 `requires` 的清单构建成功** ——
TOML 侧忽略未知键。因此本次声明不要求使用者先升级。
`hosted-standard-library` 与 `mcpp:c++-abi=libc++` 两条拼写均保留。
engine: mcpp-community/mcpp#494
Sunrisepeak
added a commit
to mcpplibs/openkal-llvm-runtime
that referenced
this pull request
Aug 24, 2026
…layer⚠️ 实测:该层名在 2026.8.24.2 之前的每一个已发布构建工具上都让整份清单 加载失败 —— 不是「该层被忽略」,是这个包用不了: error: dependency 'openkal-llvm-runtime': mcpp.toml: error: `provides = ["mcpp:compiler-runtime=compiler-rt"]` names no capability mcpp knows. 那条拒绝本身已在 mcpp 2026.8.24.2 修掉(依赖的清单里出现未知层名改为警告并 忽略,根工程自己的仍然报错),但**修复不能追溯到已经发布出去的工具**。 本仓 CI 的 MCPP_VERSION 目前是 2026.8.19.4。 `requires = ["mcpp:compiler=llvm"]` 保留 —— 实测旧引擎忽略未知的 [package] 键, 因此这一半今天就能发。声明留在注释里,连同它的依据与解除条件。 engine: mcpp-community/mcpp#494
Sunrisepeak
added a commit
to mcpplibs/openkal-windows
that referenced
this pull request
Aug 24, 2026
…C library (#6) -[target.'cfg(all(windows, env = "gnu"))'.build] +[target.'cfg(all(windows, not(env = "msvc")))'.build] ldflags = ["-lntdll", "-lsynchronization", "-lshell32", "-lkernel32"] 这四个是 Win32 导入库 —— 本包所实现的**平台接口**的性质,不是 C 库的性质。 `env = "gnu"` 一直在代表「GNU/PE 的 ABI 而不是 MSVC 的」,而在传统栈上两者重合。 它们在 C 库改由依赖图供给的那一刻不再重合:一次 `x86_64-windows-gnu` 的 openkal 构建解析出的 C 库是 musl —— mcpp 自己的报告就这么打印: kernel-abi openkal (openkal-windows@0.1.3, graph) c-abi musl (openkal-musl@0.3.3, graph) 而拼作 `x86_64-windows-musl` 的是同一次构建的诚实名字。旧谓词下第二个拼法 不带这四个库中的任何一个,实测: ld.lld: error: undefined symbol: __declspec(dllimport) GetStdHandle ld.lld: error: undefined symbol: __declspec(dllimport) WriteFile … 两份 build.ninja 的 ldflags 逐 token 对比,差别只有这四个 token; 连 `--target=x86_64-w64-windows-gnu` 都完全相同 —— 两个 mcpp 三元组翻译成 同一个 LLVM 三元组。 实测(改动后): Finished dev in 2.75s test3.exe: PE32+ executable (console) x86-64, 14 sections imports: ntdll.dll / api-ms-win-core-synch-l1-2-0.dll / SHELL32.dll / KERNEL32.dll wine test3.exe → Hello from test3! `cxxflags = ["-fno-exceptions", "-fno-rtti"]` 在同一张表里,因此一并生效于 两种拼法 —— 那也是对的:它们的理由同样是 ABI 而非 C 库。⚠️ 兼容性:`x86_64-windows-gnu` 的求值结果不变(`gnu != msvc`), 新增覆盖的是 `msvc` 之外的其它 env 拼法。 related: mcpp-community/mcpp#494
Sunrisepeak
added a commit
to mcpplibs/openkal-llvm-runtime
that referenced
this pull request
Aug 24, 2026
…layer (#2) * feat: declare the compiler-runtime layer and the compiler family it needs 这个包编译 729 个对象,其中 **498 个是 compiler-rt 的 builtins**、21 个是 libunwind 的、159 个 libcxx、51 个 libcxxabi。也就是说它最大的一块此前声明在 `mcpp:c++-abi` 名下 —— 而 `__udivti3` 及其同类是一个**纯 C 程序**需要的东西, 与 C++ 无关。 把 builtins 算作 C++ 运行时的一部分,等价于断言 C 程序不需要整数除法。 该断言已经产生过一次实测缺陷:一个交叉到 macOS 的 C 程序被问「有没有 C++ 运行时」,答「没有」,链接行因而保留了编译器载荷自带的 libc++, 把一个 Linux 共享对象递给了 Mach-O 链接器。 provides = [..., "mcpp:compiler-runtime=compiler-rt"] ── requires ────────────────────────────────────────────── libc++ 的源码、尤其它的 std 模块源,由 clang 编译。递给 gcc 的实测结果: fatal error: __config: No such file or directory 该消息命名一个读者从未打开过的文件,以及一个 mcpp 从未作出的决定。 把需求写在包里,构建工具就能在编译任何东西之前拒绝这个组合, 并且能够指出族名,而不必把这条事实写进构建工具: requires = ["mcpp:compiler=llvm"] 实测(mcpp 2026.8.24.2): error: `openkal-llvm-runtime@0.1.1` requires the compiler to be `llvm`. compiler gcc (16.1.0, payload) required llvm (required by openkal-llvm-runtime@0.1.1) Select that compiler — yours outranks mcpp's own default: mcpp toolchain default llvm ── 兼容性 ────────────────────────────────────────────────⚠️ 实测:**旧引擎(mcpp 2026.8.24.1)读带 `requires` 的清单构建成功** —— TOML 侧忽略未知键。因此本次声明不要求使用者先升级。 `hosted-standard-library` 与 `mcpp:c++-abi=libc++` 两条拼写均保留。 engine: mcpp-community/mcpp#494 * feat: require an llvm-family compiler, and name the compiler-runtime layer⚠️ 实测:该层名在 2026.8.24.2 之前的每一个已发布构建工具上都让整份清单 加载失败 —— 不是「该层被忽略」,是这个包用不了: error: dependency 'openkal-llvm-runtime': mcpp.toml: error: `provides = ["mcpp:compiler-runtime=compiler-rt"]` names no capability mcpp knows. 那条拒绝本身已在 mcpp 2026.8.24.2 修掉(依赖的清单里出现未知层名改为警告并 忽略,根工程自己的仍然报错),但**修复不能追溯到已经发布出去的工具**。 本仓 CI 的 MCPP_VERSION 目前是 2026.8.19.4。 `requires = ["mcpp:compiler=llvm"]` 保留 —— 实测旧引擎忽略未知的 [package] 键, 因此这一半今天就能发。声明留在注释里,连同它的依据与解除条件。 engine: mcpp-community/mcpp#494
Sunrisepeak
added a commit
that referenced
this pull request
Aug 31, 2026
* 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
一次真实使用暴露了十条问题:一个 hello-world 工程,
[dependencies]里只加了一行openkal-llvm-runtime,随后在四个目标上连续失败,每一次的错误信息都来自编译器,没有一条提到 mcpp 作出的决定。
01d6cef把「目标侧从哪来」收敛到了一处解析,本 PR 补齐模型本身。compiler-runtime。 实测openkal-llvm-runtime编译的 729 个对象里 498 个是 compiler-rt 的 builtins、21 个是 libunwind 的 —— 此前全部声明在mcpp:c++-abi名下。__udivti3是一个纯 C 程序需要的东西。compiler同时成为一层(唯一一个包不能供给的层),因为规则二需要一个被检查的对象。requires = ["mcpp:<层>=<实现>"]。 在引擎里不出现实现名的前提下执行分层规则。检查在编译开始之前运行。[build]段仍进入命令行。std-module*从[package]移入[build]。 位置决定的那件事就是可条件化:-D_GNU_SOURCE对 musl/glibc 是对的、对 picolibc 是错的,此前没有拼写能表达。MCPP_VERBOSE=1输出五层;诊断始终输出全部。Family去掉OpenkalLlvm。 一份载荷挂两个族名 ⇒ 可用列表里出现两次,第二份被报成未安装并推荐给已经装了它的人。拼写归一为llvm保留。修复前后,同一条命令
让全局默认压过词表 pin:一个无依赖的工程、全局默认
llvm@22.1.8、--target x86_64-windows-gnu,从能构建变成不能构建 —— clang 单独不携带该目标的C 运行时。那是升级把可用的构建变成不可用的,已回退。
行所 pin 的不是「偏好的编译器」而是「供给该目标 C 库的载荷」,用户默认能否代替它
取决于是否有别的东西供给目标侧 —— 那要到图解析之后才知道。因此保留行为并补上两条
它一直欠缺的话(状态行说明替换 + 图已知后指出这次替换本可不必)。
结构性修法是把 pin 的决定后移,未在本 PR 落地:
tc在解析后到图之间被读写 39 处。兼容性
hosted-standard-library继续表示 C++ 层openkal-llvm拼写继续解析(e2e 269 守着)[package]下三个 std-module 键继续被接受requires的清单构建成功 —— TOML 侧忽略未知键,xpkg 侧警告而非报错。已发布的包可以先行声明。Test plan
test_targetside26 个(五层 × 四来源、能力语法五层全覆盖、规则一二各自的诊断文本);全套 93 passed / 0 failed280_target_side_layers(报告收窄 + verbose 五层 + 编译器层报族名)、281_target_side_rules(requires 拒绝 / 两个供给者 / 包不得供给 compiler),均本机通过MCPP_VERBOSE读取全栈 —— 意图不变,变的是默认报告不再打印载荷层gcm.cache)。CI 是判据。check_docs_style.sh通过(中英标题层级序列一致、参考文档无第二人称)生态侧(独立 PR)
mcpplibs/openkal-llvm-runtime与mcpplibs/openkal-windows的适配随后提交。设计文档
.agents/docs/2026-08-24-target-side-design.md;规范docs/spec/target-side.md(SPEC-002);使用文档docs/14-target-side.md+ 中文版。