Skip to content

Commit 61c7446

Browse files
feat(targetside): five layers, and the largest of them had no name (#494)
* feat(targetside): five layers, and the largest of them had no name 一次真实使用暴露了十条问题:一个 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 * fix(targetside): whether an absent layer prints must not depend on its 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。 * docs(changelog): 2026.8.24.2 * fix(manifest): an unknown layer name from a dependency is a version gap, 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 中英同步。 * fix(diag): the warning must be as useful as the error it replaces 生态 e2e 暴露两处: 1. 警告只说了「不认识」,没有列出**存在的**层名 —— 而它替代的那条错误列了。 一条比它所替代的错误说得更少的警告,是一条穿着更轻处罚的更差诊断。 两处警告(依赖清单、任意包)改为复用 `parse_capability` 的原文。 2. e2e 268 的断言写的是**严重级别**,而这个前缀存在的理由是**不静默**。 ⚠️ 但一条断言若跟着策略改就该说清它现在测什么:改为断言三件事 —— 拼错的名字被报出、被拼错的那一层**没有被填上**、以及旁边拼对的那一条 **仍然解析**(一个坏条目只该花掉一层,不是整个包)。 删掉的那条「必须在编译开始之前被拒绝」不再成立,而它成立的地方仍被守着: 根工程自己的拼写错误与 `requires` 不满足,都在 e2e 281 里,都在编译之前。 unit 93 passed;e2e 268/269/280/281 全绿。 * fix(targetside): name the payload's C library for its own platform 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,失败集与基线逐条相同。 --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent bf6d1a4 commit 61c7446

27 files changed

Lines changed: 5832 additions & 122 deletions

.agents/docs/2026-08-24-graph-target-side-optimization-plan.md

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

.agents/docs/2026-08-24-target-side-architecture.md

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

.agents/docs/2026-08-24-target-side-design.md

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

CHANGELOG.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,82 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [2026.8.24.2] — 2026-08-24
7+
8+
### 新增
9+
10+
- **目标侧是五层,而其中最大的一层此前没有名字。**
11+
12+
实测 `openkal-llvm-runtime` 编译的 729 个对象里,**498 个是 compiler-rt 的
13+
builtins**、21 个是 libunwind 的。这个包最大的一块此前声明在 `mcpp:c++-abi`
14+
名下 —— 而 `__udivti3` 及其同类是一个**纯 C 程序**需要的东西,与 C++ 无关。
15+
16+
⚠️ 把 builtins 算作 C++ 运行时的一部分,与 `targetside` 模块开头记录的缺陷同形:
17+
一个交叉到 macOS 的 C 程序被问「有没有 C++ 运行时」,答「没有」,链接行因而
18+
保留了载荷自带的 libc++。**一个只有部分程序需要的层仍然是层。**
19+
20+
五层为 `compiler` / `compiler-runtime` / `kernel-abi` / `c-abi` / `c++-abi`
21+
`compiler` 是唯一一个包不能供给的层 —— 族与族之间的差异(flag 拼写、模块模型、
22+
BMI 格式、驱动 cfg)是引擎必须持有的事实,不是数据能描述的。
23+
24+
⚠️ `compiler` 层上报**族名**(`llvm`)而非驱动名(`clang`):使用者书写的每一处
25+
都用族名,报告用驱动名会让 `requires = ["mcpp:compiler=llvm"]` 永远不可满足。
26+
27+
- **`requires = ["mcpp:<层>=<实现>"]` —— 在引擎里不出现实现名的前提下执行分层规则。**
28+
29+
libc++ 的源码由 clang 编译;递给 gcc 的实测结果是
30+
`fatal error: __config: No such file or directory` —— 一个命名了读者从未打开过的
31+
文件、且不提任何 mcpp 决定的消息。在引擎里写
32+
`if (stdlib == "libc++" && compiler == gcc)` 会把两个实现名放进引擎;
33+
写在包里,引擎只需检查一条它能一般性陈述的关系。
34+
35+
⚠️ 检查在**编译开始之前**运行,这正是声明它的全部意义。
36+
37+
- **规则一:每层恰好一个供给者。**
38+
39+
⚠️ 此前两个包供给同一层时,**图遍历顺序里第一个静默胜出** —— 那个顺序既不是
40+
作者写的,也不是他能预测的 —— 而落选者的 `[build]` 段仍然进入命令行。
41+
判据是失败模态:选错不会让链接失败,会得到一个能跑、偶尔崩的程序。
42+
`[build] runner` 早已按同一条规则处理。
43+
44+
- **`docs/14-target-side.md`(中英)与 `docs/spec/target-side.md`(SPEC-002)。**
45+
46+
### 变更
47+
48+
- **`std-module` / `std-compat-module` / `std-module-flags` 移入 `[build]`**
49+
50+
模块源是这个包的一个翻译单元 —— 引擎自己的注释早就这么写。放在 `[package]`
51+
损失的恰恰是位置所决定的那件事:`[build]` 可条件化而 `[package]` 不可,于是
52+
一个在多种 C 库之上供给同一 C++ 运行时的包无法为不同 C 库给出不同 flag。
53+
`-D_GNU_SOURCE` 对 musl 与 glibc 是对的,对 picolibc 是错的。
54+
`[package]` 写法保留为别名。
55+
56+
- **报告按需暴露。** 零配置构建的五个层全部来自同一份载荷,五行 `(payload)`
57+
回答的是无人提出的问题。默认只列出来源不是编译器载荷的层;`MCPP_VERBOSE=1`
58+
列出全部;**诊断始终列出它所依据的每一层**
59+
60+
- **`Family` 去掉 `OpenkalLlvm`,拼写归一为 `llvm`**
61+
62+
⚠️ 保留枚举项的代价不止一条死分支:可用工具链列表按族枚举,一份载荷挂在两个
63+
族名下就出现两次,而安装状态按族记录 ⇒ **第二份被报成未安装,并被推荐给已经
64+
装了它的人。**
65+
66+
- **词表 pin 替换用户默认时,状态行说出替换与修法;图已知后指出这次替换本可不必。**
67+
68+
⚠️ 让全局默认压过词表 pin 的做法被实测否掉:一个无依赖的工程、全局默认
69+
`llvm@22.1.8``--target x86_64-windows-gnu`,**从能构建变成不能构建**
70+
行所 pin 的不是「偏好的编译器」而是「供给该目标 C 库的载荷」。
71+
结构性修法(把 pin 的决定与目标侧一样后移)未在本版落地:`tc` 在解析后到图之间
72+
被读写 39 处。
73+
74+
### 兼容性
75+
76+
- `hosted-standard-library` 继续表示 C++ 层;
77+
- `openkal-llvm` 拼写继续解析;
78+
- `[package]` 下三个 std-module 键继续被接受;
79+
- ⚠️ 实测:**旧引擎(2026.8.24.1)读带 `requires` 的清单构建成功** ——
80+
TOML 侧忽略未知键,xpkg 侧警告而非报错。已发布的包因此可以先行声明。
81+
682
## [2026.8.20.2] — 2026-08-20
783

884
### 新增

docs/05-mcpp-toml.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,36 @@ blas = "compat.openblas" # equivalently: mcpp build --cap blas=compat.openbl
11011101
compat.openblas = "0.3.0" # the provider must be a real dependency in the graph
11021102
```
11031103

1104+
The reserved prefix `mcpp:` names the target-side layers this engine resolves,
1105+
and those names are validated against a closed set. A package-level `requires`
1106+
array carries the symmetric statement — what a target-side layer must resolve to
1107+
for this package to be usable.
1108+
1109+
```toml
1110+
[package]
1111+
name = "acme.llvm-runtime"
1112+
version = "0.1.0"
1113+
provides = ["mcpp:compiler-runtime=compiler-rt", "mcpp:c++-abi=libc++"]
1114+
requires = ["mcpp:compiler=llvm"]
1115+
```
1116+
1117+
A package that is a standard library states its `std` module source under
1118+
`[build]`, where the flags it needs become conditional like any other build
1119+
input.
1120+
1121+
```toml
1122+
[build]
1123+
std-module = "llvm-generated/std.cppm"
1124+
std-compat-module = "llvm-generated/std.compat.cppm"
1125+
std-module-flags = ["--no-default-config", "-nostdinc++"]
1126+
1127+
[target.'cfg(c-abi = "musl")'.build]
1128+
std-module-flags = ["-D_GNU_SOURCE"]
1129+
```
1130+
1131+
See [14 - The Target Side](14-target-side.md) for the five layers, the rules
1132+
that govern them, and the diagnostics.
1133+
11041134
Binding is **deterministic**:
11051135

11061136
| Providers of a required capability in the graph | Result |

0 commit comments

Comments
 (0)