From 9e5732ce66f50d2cfa106851ffc5237ae5fb693c Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 03:48:52 +0800 Subject: [PATCH 01/32] =?UTF-8?q?docs(analysis):=20declared=20but=20not=20?= =?UTF-8?q?made=20to=20exist=20=E2=80=94=20the=20eighth=20defect,=20and=20?= =?UTF-8?q?a=20measured=20docs=20audit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The other end of the same family: the first seven were a predicate asked the wrong question; this one is an answer never delivered. A target row declares its C library exactly as it declares its compiler, and only the compiler is installed — the absence is skipped without a word and the build fails a hundred lines later naming a header. Includes a measured audit of every `--target` spelling in the repository's docs. The three commands in examples/06-openkal-cross/README.md all still work: short names and GNU spellings are both accepted and normalised. --- ...26-08-26-declared-but-not-made-to-exist.md | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 .agents/docs/2026-08-26-declared-but-not-made-to-exist.md diff --git a/.agents/docs/2026-08-26-declared-but-not-made-to-exist.md b/.agents/docs/2026-08-26-declared-but-not-made-to-exist.md new file mode 100644 index 00000000..92aaf7e5 --- /dev/null +++ b/.agents/docs/2026-08-26-declared-but-not-made-to-exist.md @@ -0,0 +1,223 @@ +# 声明了却没被兑现:第八条,以及文档与词表的一次实测对账 + +2026-08-26 · 修复 + 优化方案(待 review,尚未实施) + +前置:[`2026-08-25-the-two-layer-predicate-family.md`](2026-08-25-the-two-layer-predicate-family.md) +—— 那七条的分析与收尾。本文是第八条,以及由它牵出的一轮文档对账。 + +每一条都给出实测依据。凡未实测的推断,明确标注。 + +--- + +## 0. 一句话 + +> **词表声明了一样东西,而没有任何一处让它存在;缺席被静默跳过,失败在一百行后 +> 以另一个名字出现。** + +这与前七条是同一族的另一端:前七条是**判据问错了问题**,这一条是**答案从没被 +兑现**。 + +--- + +## 1. 第八条:目标行的 sysroot 从不被安装(issue #510) + +### 1.1 实测 + +干净环境——`xlings subos new` + `--sandbox`,空 home,mcpp 的 registry 从零开始: + +``` + Target riscv64-none-elf + kernel-abi — + c-abi picolibc-riscv (xim:picolibc-riscv@1.8.12, prebuilt) + c++-abi — +error: build failed + src/main.cpp:1:10: fatal error: 'stdio.h' file not found +``` + +⚠️ **报告点名了这个目标的 C 库,而构建找不到它的头。** 同一条命令在装过 picolibc +的机器上正常。 + +### 1.2 断在哪 + +同一行目标表声明两样东西,落地方式不同: + +``` +{ "riscv64-none-elf", "verified", "bare", "llvm@22.1.8", "xim:picolibc-riscv@1.8.12", true } + ↑ 工具链 pin ↑ sysroot +``` + +| 字段 | 落地 | 结果 | +|---|---|---| +| `pin` | `resolve_xpkg_path(pkg.target(), autoInstall=true, …)` | 不在就**装** | +| `sysroot` | `penv.deps` → `ensure_project_index_dir` → `seed_xlings_json` | 只**写进 `.xlings.json`** | + +然后在 `prepare.cppm:2282` 用它: + +```cpp +if (auto dir = xpkg_payload(xl, ref)) { // 纯查询,不在则 nullopt + … targetSysrootInclude = inc; // 整块被跳过 + … targetSysrootLib = lib; +} // 没有 else,没有诊断 +``` + +于是编译命令里既没有 `-isystem /include/` 也没有对应的 `-L`。 + +⚠️ **那处代码的注释描述的是一个不发生的安装:** + +> The row in kKnownTargets names it, exactly as it names the toolchain pin, and +> it is installed through the same channel a project's `[xlings] deps` use. + +`[xlings] deps` 是**声明通道**,不是安装触发器。 + +### 1.3 为什么至今没人发现 + +mcpp 自己的裸机 CI 手工把它装上,注释还写明了原因: + +```yaml +# The target sysroot, into the home MCPP uses. … +# installed into the ambient xlings home instead, the test would SKIP +"$XLINGS_BIN" install xim:picolibc-riscv -y +test -d "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-picolibc-riscv" +``` + +⭐ **每一条裸机 e2e 都跑在一台缺陷已被抹平的机器上。** 开发机同理:做过一次裸机 +就永远装着了。这与第七条(首次运行汇入)处境相同——**开发机永远不是首次运行**。 + +### 1.4 修法 + +与第七条同形:**不加条件,让它汇入那条已经会做这件事的路径**。 + +```cpp +// 现在 +if (auto dir = mcpp::xlings::paths::xpkg_payload(xl, ref)) { … } + +// 改为(与同一行的工具链 pin 同一个通道) +mcpp::fetcher::Fetcher f(**cfg3); +mcpp::fetcher::InstallProgressHandler progress; +if (auto p = f.resolve_xpkg_path(want_sysroot, /*autoInstall=*/true, &progress)) { … } +``` + +一行上的两个字段本该由同一个机制兑现。 + +⚠️ **离线与 `MCPP_NO_AUTO_INSTALL` 的行为由 `Fetcher` 统一决定**,不在这里再判一次 +—— 那正好是本族缺陷的成因(同一个问题两处回答)。实施前须核实 `Fetcher` 确实在 +这两种模式下拒绝而非静默跳过。**未实测。** + +### 1.5 判据:删掉补偿 + +⭐⭐ **删掉裸机 CI 里那两行手工安装,删掉它就是测试。** + +这条判据的好处是它**不是新写的断言**(新断言自己可能就有毛病——本次会话里我新写 +的六条 e2e 有四条的「否」与「没测成」同读数),而是**移除一处补偿**: + +- 补偿在,缺陷看不见; +- 补偿一去,修好之前 `130`–`133` 四条裸机 e2e 必红; +- 修好之后它们照常绿。 + +两个方向都能验,而且不需要任何新的判据代码。 + +--- + +## 2. 文档与词表:一次实测对账 + +⚠️ **先说结论:被怀疑的那三条 README 命令全部成立。** 我把仓库里所有文档中的 +`--target` 拼写抽出来逐个真跑过,而不是按印象判断。 + +### 2.1 `examples/06-openkal-cross/README.md` 实测 + +``` + x86_64-linux → x86_64-unknown-linux-gnu ✅ + aarch64-macos → arm64-apple-macos14.0 ✅ + x86_64-windows-gnu → x86_64-w64-windows-gnu ✅ +``` + +短名(`x86_64-linux`)与 GNU 拼写(`x86_64-w64-mingw32`)都被接受并归一化。README +无需改动。 + +### 2.2 全仓库拼写清单 + +从 `docs/`、`examples/*/README.md`、`.agents/docs/` 抽出的全部 `--target` 值,逐个 +真跑: + +| 拼写 | 结果 | +|---|---| +| `x86_64-linux` / `x86_64-windows` / `riscv64-none` | ✅ 短名,归一化到全名 | +| `x86_64-w64-mingw32` | ✅ GNU 拼写,归一化到 `x86_64-windows-gnu` | +| `x86_64-pc-windows-musl` | ✅ 归一化到 `x86_64-windows-musl`(随后按宿主拒绝,见 §3) | +| `x86_64-linuxx-gnu` | ❌ 拼写错误,诊断给了建议 | +| `x86_64-linux-mus` | ❌ 拼写错误,诊断给了建议 | +| `aarch64-macos-musl` | ❌ 不存在的组合 | + +后三条出现在文档里 —— **须确认它们是「反例示范」还是笔误**。若是示范,加一句说明 +它们是有意写错的;若是笔误,改掉。**尚未逐处定位。** + +### 2.3 已随 2026.8.25.2 更新的文档 + +- `docs/03-toolchains.md` + `docs/zh/` —— Targets 块的**四种状态**表,以及 + 「不在这个块里的 target 在本机根本构建不了」这句话的收窄。 +- `docs/07-build-mcpp.md` + zh —— `[xlings].subos` 决定 `build.mcpp` 的 `PATH`。 +- `docs/17-the-project-environment.md` + zh —— 新章节。 +- `CHANGELOG.md` —— 九条。 + +### 2.4 待补的文档 + +| 文档 | 要补什么 | 依据 | +|---|---|---| +| `docs/13-baremetal.md` | 裸机目标的 C 库**由目标行提供**,以及它是否需要预先安装 | 第八条修好后行为改变 | +| `docs/03-toolchains.md` | 「请求的目标与解析出的目标必须同一个 OS」这条拒绝,以及它的两条出路 | 2026.8.25.2 新增 | +| `docs/16-the-target-triple.md` | 短名与 GNU 拼写都被接受并归一化(§2.2 实测) | 词表行为,文档未述 | +| `examples/06-openkal-cross/README.md` | 无需改动 | §2.1 实测 | + +--- + +## 3. 优化项(不是缺陷) + +### 3.1 「不能构建」的拒绝没告诉人下一步 + +无依赖的工程请求 `x86_64-windows-musl`: + +``` +error: target 'x86_64-windows-musl' cannot be built on this host. +``` + +而 `toolchain list` 对同一个目标说: + +``` +x86_64-windows-musl PE, static, cross llvm 22.1.8 via dependency graph +``` + +两句话自洽——列表说「需要依赖图」,而这个工程没有图。但**拒绝那句没有把列表已经 +知道的事说出来**:加一个依赖就能构建。 + +建议:该拒绝在「本宿主可由图服务」时,附一句指向 `toolchain list` 的同一措辞。 +⚠️ 判据要两向:**不可由图服务的目标(MSVC、macOS SDK)不得出现这句话**,否则就是 +把一条走不通的路指给人。 + +### 3.2 `xpkg_payload` 的所有调用点 + +第八条只是其中一处。`xpkg_payload` 是纯查询,而它的每个调用点都要回答「不在时怎么 +办」。**建议逐个过一遍**: + +```sh +grep -rn "xpkg_payload" src/ --include=*.cppm +``` + +已知:`prepare.cppm` 的 `fillXpkgDirs`(`[xlings] deps` → `MCPP_XPKG_*_DIR`)对缺席 +的答案是「空字符串」,并且**那是有意的**——包声明了工具而没装,构建程序应当看到 +空值并自行决定。第八条不同:那不是包声明的,是**词表声明的**,而词表的另一个字段 +会自动安装。 + +⚠️ 我没有逐个核对其余调用点。**未实测。** + +--- + +## 4. 建议的执行顺序 + +1. **第八条**:改 `prepare.cppm:2282` 走 `autoInstall`;删裸机 CI 那两行;先用**未修 + 的二进制**确认四条裸机 e2e 会红(证明判据有效),再用修好的确认转绿。 +2. **§3.1 的拒绝措辞**:与 §1 同一个 PR,它们改的是同一类体验。 +3. **§2.2 的三处错误拼写**:定位后决定改还是加说明。 +4. **§2.4 的三处文档补写**,与 1 同一个 PR(行为变了,文档同步)。 +5. **§3.2 的调用点普查**:单独一轮,产出可能是「其余都对」——那也是结论。 +6. 回填上一篇 §7 第 5 条:**#486 触碰的每个判据逐个过一遍**。八条里七条出自那次 + 改动,值得确认没有第九条。 From 2cee1d01ab7a7d6ff9f7b6f9af5907f3b7d2580b Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 04:06:02 +0800 Subject: [PATCH 02/32] =?UTF-8?q?docs(analysis):=20the=20support=20matrix,?= =?UTF-8?q?=20measured=20=E2=80=94=2056=20cells=20on=20one=20host?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every cell was built, not reasoned about. The matrix is not two-dimensional: target × compiler × who-supplies-the-system, and the third axis changes what the first two mean — `x86_64-windows-gnu` has the MinGW CRT under a payload and musl under a graph, one name and two C libraries. Four cells need work and one is an ecosystem gap. The macOS and Windows host columns are empty and are marked as such; the closing recommendation is to make this sweep a script that runs on all three, so the matrix stops being a document that can quietly go stale. --- .../2026-08-26-the-support-matrix-measured.md | 214 ++++++++++++++++++ docs/16-the-target-triple.md | 45 ++++ 2 files changed, 259 insertions(+) create mode 100644 .agents/docs/2026-08-26-the-support-matrix-measured.md diff --git a/.agents/docs/2026-08-26-the-support-matrix-measured.md b/.agents/docs/2026-08-26-the-support-matrix-measured.md new file mode 100644 index 00000000..062d0028 --- /dev/null +++ b/.agents/docs/2026-08-26-the-support-matrix-measured.md @@ -0,0 +1,214 @@ +# 支持矩阵:56 格实测,四处待修、一处生态空缺 + +2026-08-26 · 现状测量 + 问题分析(待 review,尚未实施) + +前置:[`2026-08-25-the-two-layer-predicate-family.md`](2026-08-25-the-two-layer-predicate-family.md)、 +[`2026-08-26-declared-but-not-made-to-exist.md`](2026-08-26-declared-but-not-made-to-exist.md) + +⚠️ **全部为实测,不是从代码推的。** 每一格都真跑过一次 `mcpp build --target …`, +取的是构建报告的原文。 + +⚠️ **宿主只有一台 Linux x86_64。** macOS 与 Windows 宿主那两列是空的 —— 本文不对 +它们下任何结论,标注为**未测**。 + +--- + +## 0. 一句话 + +> **矩阵不是「目标 × 编译器」的二维,是「目标 × 编译器 × 系统由谁供给」的三维, +> 而第三维决定前两维的含义。** + +同一个 `x86_64-windows-gnu`,C 库可以是 MinGW CRT 也可以是 musl;同一个 +`riscv64-none-elf`,编译器可以是 clang 也可以是(错误地)宿主 g++。 + +--- + +## 1. 测量方法 + +- 宿主:Linux x86_64,`mcpp 2026.8.25.2` +- 工程:一个 `main.cpp`。⚠️ **裸机目标必须换探针** —— `` 是 C++ 头, + 而 `sysroot = ""` 的层没有 C++ 标准库;那四格用 + `extern "C" int main(int, char**, char**)` +- 两种模式: + - **payload** —— 无依赖,系统来自所选编译器的载荷 + - **graph** —— `openkal-musl@0.3.5` + `openkal-llvm-runtime@0.1.3` +- 编译器:`gcc@16.1.0`、`llvm@22.1.8`(显式写进 `[toolchain] default`) +- 目标:`kKnownTargets` 全部 14 行 +- 裸机目标补 `[target.] sysroot = ""` + +14 × 2 × 2 = **56 格**。 + +--- + +## 2. payload 模式(系统来自编译器载荷) + +| mcpp 目标 | 编译器 | 实际驱动 | 编译器三元组 | c-abi | c++-abi | +|---|---|---|---|---|---| +| `x86_64-linux-gnu` | gcc | `xim-x-gcc/…/g++` | `x86_64-unknown-linux-gnu` | gnu | libstdc++ | +| `x86_64-linux-musl` | gcc | `xim-x-musl-gcc/…/x86_64-linux-musl-g++` | `x86_64-unknown-linux-musl` | musl | libstdc++ | +| `aarch64-linux-musl` | gcc | `xim-x-aarch64-linux-musl-gcc/…` | `aarch64-unknown-linux-musl` | musl | libstdc++ | +| `x86_64-windows-gnu` | gcc | `xim-x-mingw-cross-gcc/…/x86_64-w64-mingw32-g++` | `x86_64-w64-windows-gnu` | gnu | libstdc++ | +| `x86_64-linux-gnu` | llvm | `xim-x-llvm/…/clang++` | `x86_64-unknown-linux-gnu` | gnu | ⚠️ **①** | +| `x86_64-linux-musl` | llvm | `xim-x-llvm/…/clang++` | `x86_64-unknown-linux-musl` | musl | ⚠️ **①** | +| `aarch64-linux-musl` | llvm | `xim-x-llvm/…/clang++` | `aarch64-unknown-linux-musl` | musl | ⚠️ **①** | +| `x86_64-windows-gnu` | llvm | `xim-x-llvm/…/clang++` | `x86_64-w64-windows-gnu` | gnu | ⚠️ **②** | +| 四个 `*-none-elf` | gcc | `xim-x-gcc/…/g++` | — | — | ⚠️ **③** | +| 四个 `*-none-elf` | llvm | `xim-x-llvm/…/clang++` | — | — | ✅ 全过(④) | +| `x86_64-windows-musl` | 两者 | — | — | — | 拒绝(见 §4.1) | +| `x86_64-windows-msvc` | 两者 | — | — | — | 拒绝(宿主无 MSVC,正确) | +| `aarch64-macos` | 两者 | — | — | — | 拒绝(宿主无 SDK,正确) | +| `aarch64-linux-gnu` / `x86_64-macos` / `riscv64-linux-musl` | 两者 | — | — | — | `registered but not…`(tier=planned,正确) | + +⭐ **gcc 一个目标一份载荷,clang 一份载荷打所有目标。** 这是两种体系,而不是同一 +件事的两种写法:gcc 的驱动带三元组前缀(`x86_64-linux-musl-g++`),clang 始终是同 +一个 `clang++` 加 `--target=`。 + +--- + +## 3. graph 模式(系统来自 openkal) + +| mcpp 目标 | 编译器 | 编译器三元组 | kernel-abi | c-abi | c++-abi | +|---|---|---|---|---|---| +| `x86_64-linux-gnu` | llvm | `x86_64-unknown-linux-gnu` | openkal (openkal-linux) | musl | libc++ | +| `x86_64-linux-musl` | llvm | `x86_64-unknown-linux-musl` | openkal (openkal-linux) | musl | libc++ | +| `aarch64-linux-musl` | llvm | `aarch64-unknown-linux-musl` | openkal (openkal-linux) | musl | libc++ | +| `aarch64-macos` | llvm | `arm64-apple-macos14.0` | openkal (openkal-macos) | musl | libc++ | +| `x86_64-windows-gnu` | llvm | `x86_64-w64-windows-gnu` | openkal (openkal-windows) | musl | libc++ | +| `x86_64-windows-musl` | llvm | `x86_64-w64-windows-gnu` | openkal (openkal-windows) | musl | libc++ | +| `riscv64-none-elf` | llvm | — | — | musl | libc++ | +| **全部 14 个** | **gcc** | — | — | — | ⚠️ **⑤** | + +⭐⭐ **看 `x86_64-windows-gnu` 在两张表里的 c-abi:payload 下是 `gnu`(MinGW CRT), +graph 下是 `musl`。** 一个目标名,两个 C 库。这正是 `2026.8.24.6` 给 +`x86_64-windows-musl` 单独取名的理由 —— 同一个 LLVM 三元组 +(`x86_64-w64-windows-gnu`,LLVM 拼不出 windows-musl),产物差 16.7 倍、依赖的 DLL +完全不同。 + +--- + +## 4. 逐处分析 + +### ① llvm × linux-*:C 运行时落到宿主(已被拦住) + +``` +error: hermetic link check failed — the sandbox toolchain resolves its C runtime + outside the sandbox: + /lib/x86_64-linux-gnu/Scrt1.o (outside the sandbox) + /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o (outside the sandbox) +``` + +**这不是缺陷,是守卫在工作** —— clang 载荷不自带 glibc 的启动对象,mcpp 也没为这 +个组合准备 sysroot,于是 clang 找到宿主的,而密闭性检查拒绝了。 + +⚠️ **但拒绝之后没有出路。** 消息说了「解析到沙箱外」,没说「怎样才能不解析到沙箱 +外」。用 gcc 就能构建同一个目标,而消息里没有这句话。 + +**建议**:该拒绝在同一目标存在可用编译器时,附一句指出它。判据两向:**没有可用 +替代时不得出现这句话**。 + +### ② llvm × windows-gnu:MinGW CRT 来自 `/usr`,且守卫没拦 + +``` +$ clang++ --target=x86_64-w64-windows-gnu -print-search-dirs +libraries: …/xim-x-llvm/22.1.8/lib/clang/22 + /usr/lib/gcc/x86_64-w64-mingw32/13-win32 ← 宿主 + /usr/x86_64-w64-mingw32/lib ← 宿主 +``` + +链接线上**没有 `-L`、`-B`、`--sysroot`**,mcpp 什么都没指定。构建最终失败于 +`ld.lld: error: obj/main.o: unknown file type`,而这条消息与真正的问题无关。 + +⚠️⚠️ **两处存疑,均未实测:** +- 密闭性检查为何对 ① 报了而对 ② 没报 —— 可能是它只认 ELF 形状的路径。 +- `-print-search-dirs` 里还出现了 + `--dynamic-linker=…/xim-x-glibc/2.44/lib/ld-linux-x86-64.so.2` —— 给一个 PE 目标 + 指定了 Linux 加载器,来自 `clang++.cfg` 而 `--target=` 没有撤掉它。 + +**这一格是本文最需要先查清的。** 在 mcpp 的支持矩阵里它当前既不能用,也没有一条 +说清楚为什么的诊断。 + +### ③ gcc × 裸机:宿主 g++ 收到 clang 专用 flag + +``` +g++: error: unrecognized argument in option '-mabi=lp64d' +g++: note: valid arguments to '-mabi=' are: ms sysv +``` + +目标行 pin 是 `llvm@22.1.8`,而**用户显式写了 `[toolchain] default = "gcc"` 就让位** +—— 这是既有设计(用户显式声明优先于行的约定)。 + +⚠️ **但让位之后没有检查这个编译器能否发出这个目标。** 裸机行的 pin 是**能力陈述** +而非约定(见前文第五条),让位给一个发不出该目标的编译器,应当在**决定处**拒绝, +而不是让 g++ 去报 `-mabi` 不认识。 + +**建议**:`targetPinIsCapability` 为真时,用户显式声明也要过一道「这个编译器能发出 +这个目标吗」。⚠️ 判据必须两向:**能发出的组合不得被拒**。 + +### ④ llvm × 裸机:四格全部通过(初测是我的探针错) + +重测后: + +``` +llvm@22.1.8 riscv64-none-elf ✅ ELF 64-bit LSB executable, UCB RISC-V, RVC +llvm@22.1.8 riscv32-none-elf ✅ ELF 32-bit LSB executable, UCB RISC-V, RVC +llvm@22.1.8 aarch64-none-elf ✅ ELF 64-bit LSB executable, ARM aarch64 +llvm@22.1.8 x86_64-none-elf ✅ ELF 64-bit LSB executable, x86-64 +``` + +⚠️ **初测的四个失败是探针的问题,不是 mcpp 的。** 那一版用 `#include ` +—— 一个 C++ 头 —— 而 `sysroot = ""` 的裸机层没有 C++ 标准库。换成 +`extern "C" int main(int, char**, char**)` 后四格全过。 + +⭐ **这与 issue #510 是两件事,不要混。** #510 是 `sysroot` 被声明而不被安装,在 +**没有 `sysroot = ""` 覆盖**、由目标行提供 picolibc 的干净环境里出现;这四格显式 +写了 `sysroot = ""`,即「本目标不要 C 库」,mcpp 照做且正确。 + +⚠️ 本文初稿把这两件事混成了一条 —— 记录在此,因为混淆的方式值得记:**两个失败发生 +在同一个目标上,而原因不同**,`stdio.h not found`(库没装)与 `cstdio not found` +(层不存在)只差一个字母。 + +### ⑤ gcc × graph:被规范包正确拒绝 + +``` +`openkal-llvm-runtime@0.1.3` requires the compiler family to be llvm +``` + +**这不是缺陷。** openkal-llvm-runtime **就是** libc++/libc++abi/libunwind,用 gcc 构建 +它不是一件存在的事,包声明了这一点,mcpp 在编译前就拒绝了 —— 这正是 `requires` 机 +制该有的行为。 + +⚠️ 但它意味着 **graph 模式下 gcc 这一整列都不可用**。若要让 gcc 也能用 openkal,需 +要一个 gcc 的 C++ 运行时实现包(`openkal-gcc-runtime`),**目前不存在**。这是生态的 +空缺,不是引擎的缺陷。 + +--- + +## 5. 矩阵的空白 + +| 轴 | 已测 | 未测 | +|---|---|---| +| 宿主 OS | Linux x86_64 | **macOS、Windows** | +| 编译器 | gcc、llvm | **msvc**(需 Windows 宿主) | +| 目标 | 14 行全部 | — | +| 系统来源 | payload、graph | — | + +⚠️ **macOS 与 Windows 宿主那两列没有任何本地测量。** CI 覆盖到其中一部分 +(`openkal-cross.yml` 的 3 宿主 × 3 目标、`ci-windows*`),但**不是这张矩阵的形状**。 + +⭐ **建议:把这张扫描做成一条可重复运行的脚本,在三个宿主的 CI 上各跑一次,产出同 +一张表。** 那样「支持矩阵」就不再是一份文档,而是一次测量的输出 —— 与它描述的东西 +同步,不会像本文这样在下一次改动后悄悄过期。 + +--- + +## 6. 建议的执行顺序 + +1. **查清 ②**:密闭性检查为何不报;`clang++.cfg` 的 Linux 加载器是否泄漏到 PE 目 + 标;mcpp 是否本该为 llvm × windows-gnu 提供 sysroot。这一格现在既不可用也没有 + 可读的诊断。 +2. **③ 的能力检查**:让位给用户声明之后仍需确认「这个编译器能发出这个目标」。 +3. **① 的出路**:拒绝时指出可用的替代编译器,两向判据。 +4. **issue #510**(目标行 sysroot 声明而不安装)—— 与 ④ 是两件事,判据是删掉裸机 + CI 那两行手工安装。 +5. **把扫描脚本化并接进三宿主 CI**(§5)。 +6. **⑤ 记为生态空缺**,不作为引擎问题跟踪。 diff --git a/docs/16-the-target-triple.md b/docs/16-the-target-triple.md index ad72f159..96d3b146 100644 --- a/docs/16-the-target-triple.md +++ b/docs/16-the-target-triple.md @@ -262,6 +262,51 @@ by mcpp, because mcpp's name answers a different question: **which C library**, where LLVM's answers **which object ABI**. Both are needed and they are not the same string. +## The Compiler And The C Library Are Two Axes + +A target names a machine. It does not name who compiles for it, and it does not +name where its C library comes from — those are two further choices, and the +same target string means a different build under each. + +⚠️ **"From the payload" is not one payload.** It is whichever payload the +chosen compiler brings, and gcc and clang bring them differently: gcc has one +payload per target, with a triple-prefixed driver, while one `clang++` emits +every target it was built with. Measured on one host, one source: + +| toolchain | target | driver that ran | c-abi | c++-abi | +|---|---|---|---|---| +| `gcc@16.1.0` | `x86_64-linux-musl` | `xim-x-musl-gcc/…/x86_64-linux-musl-g++` | musl | libstdc++ | +| `gcc@16.1.0` | `x86_64-windows-gnu` | `xim-x-mingw-cross-gcc/…/x86_64-w64-mingw32-g++` | gnu | libstdc++ | +| `llvm@22.1.8` | `x86_64-linux-musl` | `xim-x-llvm/…/clang++` | musl | libc++ | +| `llvm@22.1.8` | `x86_64-windows-gnu` | `xim-x-llvm/…/clang++` | gnu | libc++ | + +clang does not reach into gcc's payload for a C library, and gcc does not reach +into clang's. Each brings its own. + +### And a dependency graph replaces that axis entirely + +The same three targets, with `openkal-musl` and `openkal-llvm-runtime` in the +graph — measured the same way: + +| target | kernel-abi | c-abi | c++-abi | +|---|---|---|---| +| `x86_64-linux-musl` | openkal (openkal-linux, graph) | musl (graph) | libc++ (graph) | +| `x86_64-windows-gnu` | openkal (openkal-windows, graph) | musl (graph) | libc++ (graph) | +| `x86_64-windows-musl` | openkal (openkal-windows, graph) | musl (graph) | libc++ (graph) | + +⚠️ **Look at `x86_64-windows-gnu` in both tables.** Its C library is `gnu` — the +MinGW CRT — when a payload supplies it, and `musl` when the graph does. One +target string, two different C libraries, and until 2026.8.24.6 mcpp had no way +to say which: the same `--target x86_64-windows-gnu` produced artefacts that +differed by 16.7× in size and named entirely different DLLs. + +That is why `x86_64-windows-musl` exists as a separate name. It maps to the same +LLVM triple as `x86_64-windows-gnu` — LLVM cannot spell it — so the two are +indistinguishable to the compiler, and the whole difference is which C library +is in use. A payload for it does not exist on any host; its system can only come +from a dependency graph, which is what `toolchain list` reports as +`via dependency graph`. + ## Custom Targets A triple outside mcpp's table needs an explicit section, which is also how a From b1da4c955f37e7905d94c37461999917d916d747 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 04:09:37 +0800 Subject: [PATCH 03/32] docs(analysis): measure from the mcpp entry point, not from its compiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second finding was measured by running `clang++ -print-search-dirs` directly. That is not mcpp's build line — going around the thing under test to ask one of its components returns the component's default behaviour, not what mcpp made it do. The criterion is what mcpp emits: the build report, and the flags in the generated build.ninja. Under that criterion the finding is sharper — gcc needs mcpp to say nothing because its driver carries the target sysroot, while clang switches targets with `--target=` and needs one supplied, and mcpp supplies an empty ldflags with no -B, -L or --sysroot. Also states the axes in full: the build machine is one of them, and this sweep had exactly one. --- .../2026-08-26-the-support-matrix-measured.md | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/.agents/docs/2026-08-26-the-support-matrix-measured.md b/.agents/docs/2026-08-26-the-support-matrix-measured.md index 062d0028..d2165951 100644 --- a/.agents/docs/2026-08-26-the-support-matrix-measured.md +++ b/.agents/docs/2026-08-26-the-support-matrix-measured.md @@ -38,6 +38,21 @@ 14 × 2 × 2 = **56 格**。 +### 轴的完整定义 + +| 轴 | 本轮取值 | +|---|---| +| **构建机(宿主 OS × arch)** | Linux x86_64 —— 只有一台,见 §5 | +| **mcpp 目标** | `kKnownTargets` 全部 14 行 | +| **编译器族** | gcc、llvm(msvc 需 Windows 宿主) | +| **编译器三元组** | 由 mcpp 解析,是**结果**不是输入 | +| **系统来源** | payload / graph | +| c-abi / c++-abi | 由上面五轴决定,是**结果** | + +⚠️ **判据一律取自 `mcpp build` 这个入口**:构建报告的原文,以及生成的 +`build.ninja` 里 mcpp 真正下发的 flag。**不直接问编译器** —— 绕开被测对象去问它的 +组件,得到的是组件的默认行为而非 mcpp 的行为(本文 §4.② 初稿就是这么错的)。 + --- ## 2. payload 模式(系统来自编译器载荷) @@ -106,26 +121,30 @@ error: hermetic link check failed — the sandbox toolchain resolves its C runti **建议**:该拒绝在同一目标存在可用编译器时,附一句指出它。判据两向:**没有可用 替代时不得出现这句话**。 -### ② llvm × windows-gnu:MinGW CRT 来自 `/usr`,且守卫没拦 +### ② llvm × windows-gnu:mcpp 没有为这个组合提供目标 sysroot -``` -$ clang++ --target=x86_64-w64-windows-gnu -print-search-dirs -libraries: …/xim-x-llvm/22.1.8/lib/clang/22 - /usr/lib/gcc/x86_64-w64-mingw32/13-win32 ← 宿主 - /usr/x86_64-w64-mingw32/lib ← 宿主 -``` +⚠️ **本条初稿的测量方式是错的。** 它直接跑 +`clang++ --target=… -print-search-dirs` 看到 `/usr/x86_64-w64-mingw32/lib`,并据此 +说「CRT 来自宿主」。**那不是 mcpp 的构建线** —— 绕开被测对象去问它的一个组件,得到 +的是那个组件的默认行为,不是 mcpp 让它做了什么。 + +⭐ **正确的判据是 mcpp 真正下发的命令行**,即生成的 `build.ninja`: + +| | gcc × windows-gnu | llvm × windows-gnu | +|---|---|---| +| `cxxflags` | `-std=c++23 -fmodules -O0 -g`(**无 `--target`**) | `--target=x86_64-w64-windows-gnu` | +| `ldflags` | `-lstdc++exp` | **空** | +| 目标 sysroot | **驱动自带**:`xim-x-mingw-cross-gcc/16.1.0/x86_64-w64-mingw32/` | **无人提供** | + +gcc 那格不需要 mcpp 说任何话:`x86_64-w64-mingw32-g++` 是**为这个目标构建的交叉 +编译器**,sysroot 在它自己的载荷里。llvm 那格是同一个 `clang++` 靠 `--target=` 切 +目标,**头与库必须由外部给**,而 mcpp 给的是空 `ldflags`、没有 `-B`/`-L`/`--sysroot`。 -链接线上**没有 `-L`、`-B`、`--sysroot`**,mcpp 什么都没指定。构建最终失败于 -`ld.lld: error: obj/main.o: unknown file type`,而这条消息与真正的问题无关。 +构建最终失败于 `ld.lld: error: obj/main.o: unknown file type`,与真正的问题无关。 -⚠️⚠️ **两处存疑,均未实测:** -- 密闭性检查为何对 ① 报了而对 ② 没报 —— 可能是它只认 ELF 形状的路径。 -- `-print-search-dirs` 里还出现了 - `--dynamic-linker=…/xim-x-glibc/2.44/lib/ld-linux-x86-64.so.2` —— 给一个 PE 目标 - 指定了 Linux 加载器,来自 `clang++.cfg` 而 `--target=` 没有撤掉它。 +⚠️ **仍未查清:** 密闭性检查对 ① 报了而对 ② 没报。**未实测。** -**这一格是本文最需要先查清的。** 在 mcpp 的支持矩阵里它当前既不能用,也没有一条 -说清楚为什么的诊断。 +**这一格是本文最需要先查清的。** 它当前既不能用,也没有一条说清楚为什么的诊断。 ### ③ gcc × 裸机:宿主 g++ 收到 clang 专用 flag From b4df897692f1baa4839eeaddea586b499b323491 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 04:10:21 +0800 Subject: [PATCH 04/32] docs(16): the compiler and the C library are two axes (zh mirror) The English section landed without its Chinese counterpart and the style check caught the heading parity. --- docs/zh/16-the-target-triple.md | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/zh/16-the-target-triple.md b/docs/zh/16-the-target-triple.md index 35faceeb..9dba5ce0 100644 --- a/docs/zh/16-the-target-triple.md +++ b/docs/zh/16-the-target-triple.md @@ -235,6 +235,44 @@ clang++ --target=x86_64-pc-windows-musl -c t.cpp 因为 mcpp 的名字回答的是另一个问题:**C 库是谁**,而 LLVM 的名字回答 **遵循哪套对象 ABI**。两者都需要,且不是同一个字符串。 +## 编译器与 C 库是两个轴 + +目标命名的是一台机器。它不指定谁来编译,也不指定它的 C 库从哪来 —— 那是另外两个 +选择,而同一个目标字符串在每种选择下都是不同的构建。 + +⚠️ **「来自载荷」不是某一份固定载荷**,而是所选编译器带来的那一份;gcc 与 clang +带法不同:gcc 一个目标一份载荷、驱动带三元组前缀,而一个 `clang++` 打它构建时支持 +的每个目标。同一台宿主、同一份源码实测: + +| 工具链 | 目标 | 实际运行的驱动 | c-abi | c++-abi | +|---|---|---|---|---| +| `gcc@16.1.0` | `x86_64-linux-musl` | `xim-x-musl-gcc/…/x86_64-linux-musl-g++` | musl | libstdc++ | +| `gcc@16.1.0` | `x86_64-windows-gnu` | `xim-x-mingw-cross-gcc/…/x86_64-w64-mingw32-g++` | gnu | libstdc++ | +| `llvm@22.1.8` | `x86_64-linux-musl` | `xim-x-llvm/…/clang++` | musl | libc++ | +| `llvm@22.1.8` | `x86_64-windows-gnu` | `xim-x-llvm/…/clang++` | gnu | libc++ | + +clang 不会去 gcc 的载荷里取 C 库,gcc 也不会去 clang 的载荷里取。各带各的。 + +### 而依赖图会整个替换这一轴 + +同样三个目标,图里有 `openkal-musl` 与 `openkal-llvm-runtime` —— 同法实测: + +| 目标 | kernel-abi | c-abi | c++-abi | +|---|---|---|---| +| `x86_64-linux-musl` | openkal(openkal-linux,图) | musl(图) | libc++(图) | +| `x86_64-windows-gnu` | openkal(openkal-windows,图) | musl(图) | libc++(图) | +| `x86_64-windows-musl` | openkal(openkal-windows,图) | musl(图) | libc++(图) | + +⚠️ **看两张表里的 `x86_64-windows-gnu`。** 载荷供给时它的 C 库是 `gnu`,即 MinGW +CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— 而 mcpp 在 2026.8.24.6 +之前无法说清是哪一个:同一条 `--target x86_64-windows-gnu` 产出的东西体积差 16.7 +倍、依赖的 DLL 完全不同。 + +这就是 `x86_64-windows-musl` 作为独立名字存在的理由。它与 `x86_64-windows-gnu` +映射到**同一个 LLVM 三元组** —— LLVM 拼不出它 —— 所以两者在编译器那一侧无法区分, +全部差别就在于用的是哪个 C 库。任何宿主都没有为它准备的载荷;它的系统只能来自依赖 +图,这正是 `toolchain list` 报的 `via dependency graph`。 + ## 自定义目标 不在 mcpp 表内的三元组需要一个显式段落,而这也是一块板子声明 From 4480cabecbc67b8d796386277f3e609057394aeb Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 04:36:33 +0800 Subject: [PATCH 05/32] =?UTF-8?q?docs(analysis):=20six=20tables=20?= =?UTF-8?q?=E2=80=94=20payload=20and=20openkal,=20one=20per=20build=20mach?= =?UTF-8?q?ine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten columns, three build machines, both systems, kept apart. Rows that are not supported say so and say why; every cell is either measured on this host or marked as derived from source, so the derived ones can be argued with. Finding A is sharper than the previous draft said. It is not a missing sysroot: xim:glibc is installed and carries Scrt1.o, crti.o and crtn.o, and gcc's link line reaches for it — `-L`, `-rpath`, `--dynamic-linker` all pointing into that payload. llvm's link line has none of them. The same payload, connected on one branch and not the other. --- ...08-26-target-matrix-should-be-versus-is.md | 163 +++++++++++ .../2026-08-26-target-matrix-six-tables.md | 255 ++++++++++++++++++ 2 files changed, 418 insertions(+) create mode 100644 .agents/docs/2026-08-26-target-matrix-should-be-versus-is.md create mode 100644 .agents/docs/2026-08-26-target-matrix-six-tables.md diff --git a/.agents/docs/2026-08-26-target-matrix-should-be-versus-is.md b/.agents/docs/2026-08-26-target-matrix-should-be-versus-is.md new file mode 100644 index 00000000..06fdbaa4 --- /dev/null +++ b/.agents/docs/2026-08-26-target-matrix-should-be-versus-is.md @@ -0,0 +1,163 @@ +# 目标矩阵:应该是什么,现在是什么,差在哪 + +2026-08-26 · 现状测量 + 差异分析(待 review,尚未实施) + +取代 [`2026-08-26-the-support-matrix-measured.md`](2026-08-26-the-support-matrix-measured.md) +的表格部分,补齐它缺的轴(构建机、目标机、sysroot、构建配置)。 + +--- + +## 0. 方法与判据 + +⭐ **入口只有一个:`mcpp build --target `。** 判据取两样东西: + +1. 构建报告的原文(五层来源) +2. 生成的 `build.ninja` 里 **mcpp 真正下发的 flag** + +⚠️ **不直接问编译器。** 绕开被测对象去问它的一个组件,得到的是那个组件的默认行为, +不是 mcpp 让它做了什么 —— 上一版文档的第 ② 条就是这么错的,那里跑了 +`clang++ -print-search-dirs` 并据此说「CRT 来自宿主」。 + +### 表格的填法 + +| 来源 | 标记 | +|---|---| +| 本机(Linux x86_64)真跑过 | 无标记 | +| 由源码逻辑推出(macOS / Windows 宿主) | **推** | +| 该轴对这一格无意义 | `-` | +| 明确不支持 | **不支持** | + +推导依据是三个纯平台函数,它们不读任何机器状态: + +- `host_can_serve(target)` —— 宿主能否由载荷服务这个目标 +- `to_xim_package(spec)` —— 哪个 xim 包被选中 +- `payload_libc_name(os, env)` + `resolve()` 的四分支 —— c-abi 从哪来 + +--- + +## 1. 表一:理论上应该是什么 + +「应该」= 目标表的 tier 与 pin 所承诺的、且技术上成立的。 + +| # | mcpp target | 构建机 OS | 目标机 OS | 编译器 | 编译器 target | sysroot | c-abi | c++-abi | openkal | 构建配置特殊说明 | +|---|---|---|---|---|---|---|---|---|---|---| +| 1 | `x86_64-linux-gnu` | linux | linux | gcc | `x86_64-unknown-linux-gnu` | 载荷自带 | glibc | libstdc++ | 可替换全部三层 | — | +| 2 | `x86_64-linux-gnu` | linux | linux | llvm | `x86_64-unknown-linux-gnu` | **需 mcpp 提供** | glibc | libc++ | 可替换 | ⚠️ clang 不自带 glibc 启动对象 | +| 3 | `x86_64-linux-musl` | linux / windows | linux | gcc | `x86_64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | 可替换 | — | +| 4 | `x86_64-linux-musl` | 任意 | linux | llvm | `x86_64-unknown-linux-musl` | **需 mcpp 提供** | musl | libc++ | 可替换 | — | +| 5 | `aarch64-linux-musl` | linux / windows(同 arch) | linux | gcc | `aarch64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | 可替换 | 交叉 | +| 6 | `x86_64-windows-gnu` | linux / windows | windows | gcc | `x86_64-w64-windows-gnu` | 载荷自带 | gnu(MinGW CRT) | libstdc++ | 可替换 | 宿主分包:win→`mingw-gcc`,其他→`mingw-cross-gcc` | +| 7 | `x86_64-windows-gnu` | linux / windows | windows | llvm | `x86_64-w64-windows-gnu` | **需 mcpp 提供** | gnu | libc++ | 可替换 | ⚠️ clang 无 MinGW sysroot | +| 8 | `x86_64-windows-musl` | 任意 | windows | llvm | `x86_64-w64-windows-gnu` | **只能来自图** | musl | libc++ | **必需** | ⚠️ 与 #7 同一 LLVM 三元组;tier=preview | +| 9 | `x86_64-windows-msvc` | **windows** | windows | msvc | `x86_64-pc-windows-msvc` | Windows SDK | ucrt | MSVC STL | 不适用 | 需本机 Visual Studio | +| 10 | `aarch64-macos` | **macos** | macos | llvm | `arm64-apple-macos14.0` | macOS SDK | libSystem | libc++ | 可替换 | 需 `-isysroot`;`-mmacosx-version-min` | +| 11 | `aarch64-macos` | 任意 | macos | llvm | `arm64-apple-macos14.0` | **只能来自图** | musl | libc++ | **必需** | 交叉到 macOS,kernel-abi=openkal-macos | +| 12 | `riscv64-none-elf` | 任意 | 无 | llvm | `riscv64-none-elf` | `xim:picolibc-riscv@1.8.12` | picolibc | 无(freestanding 子集) | 可替换 | `-march/-mabi/-mcmodel` 由 freestanding 表定 | +| 13 | `riscv32-none-elf` | 任意 | 无 | llvm | `riscv32-none-elf` | `xim:picolibc-riscv@1.8.12` | picolibc | 无 | 可替换 | 同上 | +| 14 | `aarch64-none-elf` | 任意 | 无 | llvm | `aarch64-none-elf` | **无**(表里为空) | 无 | 无 | 可替换 | tier=preview;`sysroot=""` 是唯一可用形态 | +| 15 | `x86_64-none-elf` | 任意 | 无 | llvm | `x86_64-none-elf` | **无** | 无 | 无 | 可替换 | 同上 | +| 16 | `riscv64-linux-musl` | — | linux | — | — | — | — | — | — | **不支持**(tier=planned) | +| 17 | `aarch64-linux-gnu` | — | linux | — | — | — | — | — | — | **不支持**(tier=planned) | +| 18 | `x86_64-macos` | — | macos | — | — | — | — | — | — | **不支持**(tier=planned) | +| 19 | 任意 × gcc × openkal | — | — | gcc | — | — | — | — | **不支持** | `openkal-llvm-runtime` 是 libc++,`requires` 拒绝 gcc | + +--- + +## 2. 表二:现在实际是什么 + +构建机 = **Linux x86_64**(实测)。macOS / Windows 宿主标 **推**。 + +### 2.1 payload 模式(无 openkal 依赖) + +| # | mcpp target | 构建机 OS | 目标机 OS | 编译器 | 编译器 target | sysroot | c-abi | c++-abi | openkal | 构建配置特殊说明 | +|---|---|---|---|---|---|---|---|---|---|---| +| 1 | `x86_64-linux-gnu` | linux | linux | gcc | `x86_64-unknown-linux-gnu` | 载荷自带 | gnu | libstdc++ | - | ✅ | +| 2 | `x86_64-linux-gnu` | linux | linux | llvm | `x86_64-unknown-linux-gnu` | **无** | gnu | — | - | ❌ **A**:hermetic 拒绝,crt 落宿主 | +| 3 | `x86_64-linux-musl` | linux | linux | gcc | `x86_64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | - | ✅ 驱动 `x86_64-linux-musl-g++` | +| 4 | `x86_64-linux-musl` | linux | linux | llvm | `x86_64-unknown-linux-musl` | **无** | musl | — | - | ❌ **A** | +| 5 | `aarch64-linux-musl` | linux | linux | gcc | `aarch64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | - | ✅ | +| 6 | `aarch64-linux-musl` | linux | linux | llvm | `aarch64-unknown-linux-musl` | **无** | musl | — | - | ❌ **A** | +| 7 | `x86_64-windows-gnu` | linux | windows | gcc | `x86_64-w64-windows-gnu` | 载荷自带 | gnu | libstdc++ | - | ✅ `ldflags = -lstdc++exp` | +| 8 | `x86_64-windows-gnu` | linux | windows | llvm | `x86_64-w64-windows-gnu` | **无** | gnu | — | - | ❌ **B**:`ldflags` 为空,无 `-B/-L/--sysroot` | +| 9 | `x86_64-windows-musl` | linux | windows | 两者 | - | - | - | - | - | ❌ 拒绝(`cannot be built on this host`) | +| 10 | `x86_64-windows-msvc` | linux | windows | 两者 | - | - | - | - | - | ✅ 拒绝(宿主无 MSVC,正确) | +| 11 | `x86_64-windows-msvc` | **windows** 推 | windows | msvc | `x86_64-pc-windows-msvc` 推 | Windows SDK 推 | ucrt 推 | MSVC STL 推 | - | 推:`host_can_serve` 返回 `is_windows` | +| 12 | `aarch64-macos` | linux | macos | 两者 | - | - | - | - | - | ✅ 拒绝(宿主无 SDK,正确) | +| 13 | `aarch64-macos` | **macos** 推 | macos | llvm | `arm64-apple-macos14.0` 推 | macOS SDK 推 | libSystem 推 | libc++ 推 | - | 推:`payload_libc_name("macos","")` | +| 14 | `riscv64-none-elf` | linux | 无 | llvm | — | `sysroot=""` 时无 | 无 | 无 | - | ✅ 产出 RISC-V ELF | +| 15 | `riscv64-none-elf` | linux | 无 | llvm | — | **表里的 picolibc** | picolibc | 无 | - | ❌ **C**:干净环境下不被安装(#510) | +| 16 | `riscv32/aarch64/x86_64-none-elf` | linux | 无 | llvm | — | `sysroot=""` | 无 | 无 | - | ✅ 三格全过 | +| 17 | 四个 `*-none-elf` | linux | 无 | **gcc** | — | - | - | - | - | ❌ **D**:g++ 收到 `-mabi=lp64d` | +| 18 | `riscv64-linux-musl` / `aarch64-linux-gnu` / `x86_64-macos` | linux | — | 两者 | - | - | - | - | - | ✅ `registered but not…`(正确) | + +### 2.2 graph 模式(openkal-musl + openkal-llvm-runtime) + +| # | mcpp target | 构建机 OS | 目标机 OS | 编译器 | 编译器 target | sysroot | c-abi | c++-abi | openkal | 构建配置特殊说明 | +|---|---|---|---|---|---|---|---|---|---|---| +| 19 | `x86_64-linux-gnu` | linux | linux | llvm | `x86_64-unknown-linux-gnu` | 图供给 | musl(图) | libc++(图) | linux | ✅ kernel-abi=openkal-linux | +| 20 | `x86_64-linux-musl` | linux | linux | llvm | `x86_64-unknown-linux-musl` | 图供给 | musl(图) | libc++(图) | linux | ✅ | +| 21 | `aarch64-linux-musl` | linux | linux | llvm | `aarch64-unknown-linux-musl` | 图供给 | musl(图) | libc++(图) | linux | ✅ | +| 22 | `aarch64-macos` | **linux** | macos | llvm | `arm64-apple-macos14.0` | 图供给 | musl(图) | libc++(图) | macos | ✅ 交叉到 macOS 成立 | +| 23 | `x86_64-windows-gnu` | linux | windows | llvm | `x86_64-w64-windows-gnu` | 图供给 | musl(图) | libc++(图) | windows | ✅ **c-abi 与 #7 不同** | +| 24 | `x86_64-windows-musl` | linux | windows | llvm | `x86_64-w64-windows-gnu` | 图供给 | musl(图) | libc++(图) | windows | ✅ | +| 25 | `riscv64-none-elf` | linux | 无 | llvm | — | 图供给 | musl(图) | libc++(图) | — | ✅ | +| 26 | 全部 14 个 | linux | — | **gcc** | - | - | - | - | - | ✅ `requires` 拒绝(**正确**) | +| 27 | `x86_64-windows-msvc` | linux | windows | llvm | - | - | - | - | - | ✅ 拒绝(需 MSVC ABI) | + +--- + +## 3. 差异行 + +对照表一与表二,**四行不符**。其余各行两表一致。 + +| 差异 | 应该 | 现在 | 影响 | +|---|---|---|---| +| **A** | 表一 #2/#4:llvm × linux,mcpp 提供 sysroot | 表二 #2/#4/#6:mcpp 不提供,clang 找到宿主的,hermetic 拒绝 | **llvm 在 linux 目标上不可用**(无 openkal 时) | +| **B** | 表一 #7:llvm × windows-gnu,mcpp 提供 MinGW sysroot | 表二 #8:`ldflags` 为空,无 `-B/-L/--sysroot` | **llvm × windows-gnu 不可用**,且诊断无关(`unknown file type`) | +| **C** | 表一 #12/#13:目标行的 picolibc 就位 | 表二 #15:声明而不安装(#510) | **干净环境下裸机不可用** | +| **D** | 表一:裸机行的 pin 是能力陈述,gcc 发不出这些目标 | 表二 #17:用户显式声明 gcc 时无条件让位 | **诊断指向 `-mabi` 而非决定** | + +### A 与 B 是同一件事 + +两者都是:**clang 靠 `--target=` 切目标,而目标的 sysroot 必须由外部给,mcpp 没给。** +gcc 不需要,因为它一个目标一份载荷、驱动自带 sysroot(实测 #3 #5 #7 的 +`x86_64-w64-mingw32/` 目录)。 + +区别只在后果: +- **A** 被 hermetic 检查拦住,诊断准确(`Scrt1.o (outside the sandbox)`) +- **B** 没被拦住,失败于 `ld.lld: unknown file type`,**与真正的问题无关** + +⚠️ **hermetic 检查为何对 A 报而对 B 不报,未查清。未实测。** + +### C 与 D 已有 issue / 分析 + +- **C** = issue #510,判据是删掉裸机 CI 那两行手工安装 +- **D** = 上一篇 §4.③,修法是 `targetPinIsCapability` 为真时对用户显式声明也做一次 + 能力检查 + +--- + +## 4. 表一里被标「推」的格子 + +⚠️ 这些**没有任何本地测量**,只有源码推导。列出来是为了让它们可被反驳: + +| 格 | 推导依据 | +|---|---| +| #11 windows 宿主 × msvc | `host_can_serve`:`if (target.os == "windows") return is_windows` | +| #13 macos 宿主 × aarch64-macos | `host_can_serve`:`if (target.os == "macos") return is_macos`;c-abi 由 `payload_libc_name("macos","")` = `libSystem` | +| 表一 #3 windows 宿主 × linux-musl | `host_can_serve` 的 windows 分支:`is_windows && is_musl && arch == host_arch` | +| 表一 #6 windows 宿主 × windows-gnu | `to_xim_package`:`is_windows → "mingw-gcc"`,否则 `"mingw-cross-gcc"` | + +⭐ **建议把 §2 的扫描做成脚本,在三个宿主的 CI 上各跑一次**,这些「推」就变成实测, +而矩阵成为一次测量的输出而非一份会过期的文档。 + +--- + +## 5. 建议顺序 + +1. **B**:查清 hermetic 为何不报,以及 llvm × windows-gnu 是否本该有 sysroot。它现在 + 既不可用也没有可读诊断,优先级最高。 +2. **A**:与 B 同源,一并考虑 —— 是否为 llvm × 各 linux 目标提供 sysroot,还是明确 + 声明「llvm 需要 openkal 才能用于 hosted 目标」并让诊断这么说。 +3. **C**(#510)、**D**:已有判据,可独立进行。 +4. **脚本化 §2 并接进三宿主 CI**,消除 §4 的全部「推」。 diff --git a/.agents/docs/2026-08-26-target-matrix-six-tables.md b/.agents/docs/2026-08-26-target-matrix-six-tables.md new file mode 100644 index 00000000..2093aa2e --- /dev/null +++ b/.agents/docs/2026-08-26-target-matrix-six-tables.md @@ -0,0 +1,255 @@ +# 目标矩阵:六张表(载荷体系 / openkal 体系 × 三个构建机) + +2026-08-26 · 现状测量 + 差异分析(待 review,尚未实施) + +取代 [`2026-08-26-target-matrix-should-be-versus-is.md`](2026-08-26-target-matrix-should-be-versus-is.md) +与 [`2026-08-26-the-support-matrix-measured.md`](2026-08-26-the-support-matrix-measured.md) +的表格部分。 + +--- + +## 0. 方法、判据、记号 + +⭐ **入口只有一个:`mcpp build --target `。** 判据取两样:构建报告的原文(五层 +来源),以及生成的 `build.ninja` 里 **mcpp 真正下发的 flag**。 + +⚠️ **不直接问编译器。** 绕开被测对象去问它的组件,得到的是组件的默认行为而不是 +mcpp 的行为 —— 前一版文档就这么错过一次。 + +### 记号 + +| 记号 | 含义 | +|---|---| +| 无标记 | Linux x86_64 宿主上**真跑过** | +| **推** | 由源码逻辑推出,**无本地测量** | +| `-` | 该轴对这一格无意义 | +| **不支持** | 明确不支持,并给出原因 | + +### 两道门是分开的 + +一格能不能构建,先后过两道**互相独立**的门: + +1. **tier 门** —— `tier == "planned"` 直接拒绝(`registered but not…`),与宿主无关 +2. **`host_can_serve` 门** —— 这个宿主能否由**载荷**服务这个目标 + +⚠️ 第 2 道门为假,不等于不能构建 —— 系统还可以来自依赖图。三张 openkal 表就是这一 +情形。 + +### 三个构建机上 `host_can_serve` 的逐格取值 + +按源码原文求值(纯平台判定,不读机器状态): + +| mcpp target | linux/x86_64 | windows/x86_64 | macos/aarch64 | +|---|---|---|---| +| `x86_64-linux-gnu` | ✅ | ❌ | ❌ | +| `x86_64-linux-musl` | ✅ | ✅ | ❌ | +| `aarch64-linux-musl` | ✅ | ❌ | ❌ | +| `x86_64-windows-gnu` | ✅ | ✅ | ❌ | +| `x86_64-windows-musl` | ❌ | ✅ | ❌ | +| `x86_64-windows-msvc` | ❌ | ✅ | ❌ | +| `aarch64-macos` | ❌ | ❌ | ✅ | +| `x86_64-macos` | ❌ | ❌ | ✅ | +| `riscv64-linux-musl` | ✅ | ❌ | ❌ | +| `aarch64-linux-gnu` | ❌ | ❌ | ❌ | +| 四个 `*-none-elf` | ✅ | ✅ | ✅ | + +--- + +# 第一部分:载荷体系(无 openkal 依赖) + +## 表 1 — 载荷体系 · 构建机 linux/x86_64 · **应该** + +| mcpp target | 目标机 OS | 编译器 | 编译器 target | sysroot | c-abi | c++-abi | openkal | 构建配置特殊说明 | +|---|---|---|---|---|---|---|---|---| +| `x86_64-linux-gnu` | linux | gcc | `x86_64-unknown-linux-gnu` | 载荷自带 | glibc | libstdc++ | - | — | +| `x86_64-linux-gnu` | linux | llvm | `x86_64-unknown-linux-gnu` | `xim:glibc` + `xim:linux-headers` | glibc | libc++ | - | 载荷已装;gcc 分支接了,llvm 分支未接 | +| `x86_64-linux-musl` | linux | gcc | `x86_64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | - | 驱动 `x86_64-linux-musl-g++` | +| `x86_64-linux-musl` | linux | llvm | `x86_64-unknown-linux-musl` | 需 musl sysroot | musl | libc++ | - | — | +| `aarch64-linux-musl` | linux | gcc | `aarch64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | - | 交叉 | +| `aarch64-linux-musl` | linux | llvm | `aarch64-unknown-linux-musl` | 需 musl sysroot | musl | libc++ | - | 交叉 | +| `x86_64-windows-gnu` | windows | gcc | `x86_64-w64-windows-gnu` | 载荷自带 | gnu(MinGW CRT) | libstdc++ | - | 包名 `mingw-cross-gcc` | +| `x86_64-windows-gnu` | windows | llvm | `x86_64-w64-windows-gnu` | 需 MinGW sysroot | gnu | libc++ | - | — | +| `riscv64-none-elf` | 无 | llvm | `riscv64-none-elf` | `xim:picolibc-riscv@1.8.12` | picolibc | 无 | - | `-march/-mabi/-mcmodel` 由 freestanding 表定 | +| `riscv32-none-elf` | 无 | llvm | `riscv32-none-elf` | `xim:picolibc-riscv@1.8.12` | picolibc | 无 | - | 同上 | +| `aarch64-none-elf` | 无 | llvm | `aarch64-none-elf` | 无(表里为空) | 无 | 无 | - | preview;须 `sysroot = ""` | +| `x86_64-none-elf` | 无 | llvm | `x86_64-none-elf` | 无 | 无 | 无 | - | preview;须 `sysroot = ""` | +| 四个 `*-none-elf` | 无 | gcc | - | - | - | - | - | **不支持**:宿主 g++ 发不出这些目标 | +| `x86_64-windows-musl` | windows | - | - | - | - | - | - | **不支持**:无此载荷,须走 openkal | +| `x86_64-windows-msvc` | windows | - | - | - | - | - | - | **不支持**:MSVC 只在 windows 宿主 | +| `aarch64-macos` | macos | - | - | - | - | - | - | **不支持**:macOS SDK 只在 macos 宿主 | +| `riscv64-linux-musl` | linux | - | - | - | - | - | - | **不支持**:tier=planned | +| `aarch64-linux-gnu` | linux | - | - | - | - | - | - | **不支持**:tier=planned | +| `x86_64-macos` | macos | - | - | - | - | - | - | **不支持**:tier=planned | + +## 表 2 — 载荷体系 · 构建机 linux/x86_64 · **现在**(全部实测) + +| mcpp target | 目标机 OS | 编译器 | 编译器 target | sysroot | c-abi | c++-abi | openkal | 构建配置特殊说明 | +|---|---|---|---|---|---|---|---|---| +| `x86_64-linux-gnu` | linux | gcc | `x86_64-unknown-linux-gnu` | 载荷自带 | gnu | libstdc++ | - | ✅ | +| `x86_64-linux-gnu` | linux | llvm | `x86_64-unknown-linux-gnu` | **无** | gnu | — | - | ❌ **A**:`Scrt1.o (outside the sandbox)` | +| `x86_64-linux-musl` | linux | gcc | `x86_64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | - | ✅ | +| `x86_64-linux-musl` | linux | llvm | `x86_64-unknown-linux-musl` | **无** | musl | — | - | ❌ **A** | +| `aarch64-linux-musl` | linux | gcc | `aarch64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | - | ✅ | +| `aarch64-linux-musl` | linux | llvm | `aarch64-unknown-linux-musl` | **无** | musl | — | - | ❌ **A** | +| `x86_64-windows-gnu` | windows | gcc | `x86_64-w64-windows-gnu` | 载荷自带 | gnu | libstdc++ | - | ✅ `ldflags = -lstdc++exp` | +| `x86_64-windows-gnu` | windows | llvm | `x86_64-w64-windows-gnu` | **无** | gnu | — | - | ❌ **B**:`ldflags` 为空 | +| `riscv64-none-elf` | 无 | llvm | — | `sysroot=""` | 无 | 无 | - | ✅ 产出 RISC-V ELF | +| `riscv64-none-elf` | 无 | llvm | — | **表里的 picolibc** | picolibc | 无 | - | ❌ **C**:干净环境不安装(#510) | +| `riscv32-none-elf` | 无 | llvm | — | `sysroot=""` | 无 | 无 | - | ✅ | +| `aarch64-none-elf` | 无 | llvm | — | `sysroot=""` | 无 | 无 | - | ✅ | +| `x86_64-none-elf` | 无 | llvm | — | `sysroot=""` | 无 | 无 | - | ✅ | +| 四个 `*-none-elf` | 无 | gcc | - | - | - | - | - | ❌ **D**:g++ 收到 `-mabi=lp64d` | +| `x86_64-windows-musl` | windows | 两者 | - | - | - | - | - | ✅ 拒绝 `cannot be built on this host` | +| `x86_64-windows-msvc` | windows | 两者 | - | - | - | - | - | ✅ 拒绝 | +| `aarch64-macos` | macos | 两者 | - | - | - | - | - | ✅ 拒绝 | +| `riscv64-linux-musl` | linux | 两者 | - | - | - | - | - | ✅ `registered but not…` | +| `aarch64-linux-gnu` | linux | 两者 | - | - | - | - | - | ✅ `registered but not…` | +| `x86_64-macos` | macos | 两者 | - | - | - | - | - | ✅ `registered but not…` | + +## 表 3 — 载荷体系 · 构建机 windows/x86_64 与 macos/aarch64 · **现在(推)** + +⚠️ **本表全部为源码推导,无任何测量。** + +| 构建机 | mcpp target | 目标机 OS | 编译器 | 编译器 target | sysroot | c-abi | c++-abi | openkal | 构建配置特殊说明 | +|---|---|---|---|---|---|---|---|---|---| +| windows | `x86_64-windows-msvc` | windows | msvc | `x86_64-pc-windows-msvc` 推 | Windows SDK 推 | ucrt 推 | MSVC STL 推 | - | 需本机 VS;`payload_libc_name("windows","")`=ucrt | +| windows | `x86_64-windows-gnu` | windows | gcc | `x86_64-w64-windows-gnu` 推 | 载荷自带 推 | gnu 推 | libstdc++ 推 | - | 包名 **`mingw-gcc`**(非 cross) | +| windows | `x86_64-windows-musl` | windows | llvm | `x86_64-w64-windows-gnu` 推 | ⚠️ 无载荷 | — | — | - | ⚠️ `host_can_serve` 为真而载荷不存在,**存疑** | +| windows | `x86_64-linux-musl` | linux | gcc | `x86_64-unknown-linux-musl` 推 | 载荷自带 推 | musl 推 | libstdc++ 推 | - | 包名 `x86_64-linux-musl-gcc` | +| windows | 四个 `*-none-elf` | 无 | llvm | 各自 推 | 同表 1 | 同表 1 | 无 | - | — | +| windows | 其余目标 | — | - | - | - | - | - | - | **不支持**(`host_can_serve` 为假或 planned) | +| macos | `aarch64-macos` | macos | llvm | `arm64-apple-macos14.0` 推 | macOS SDK 推 | **libSystem** 推 | libc++ 推 | - | `-isysroot` + `-mmacosx-version-min` | +| macos | `x86_64-macos` | macos | - | - | - | - | - | - | **不支持**:tier=planned | +| macos | 四个 `*-none-elf` | 无 | llvm | 各自 推 | 同表 1 | 同表 1 | 无 | - | — | +| macos | 其余目标 | — | - | - | - | - | - | - | **不支持**:`host_can_serve` 全为假 | + +--- + +# 第二部分:openkal 体系(`openkal-musl` + `openkal-llvm-runtime`) + +⚠️ **前置:编译器必须是 llvm。** `openkal-llvm-runtime` **就是** libc++/libc++abi/ +libunwind,用 gcc 构建它不是一件存在的事;包用 `requires` 声明了这一点,mcpp 在编译 +前拒绝。**gcc × openkal 在三个构建机上一律不支持** —— 这是生态空缺(缺 +`openkal-gcc-runtime`),不是引擎缺陷。 + +## 表 4 — openkal 体系 · 构建机 linux/x86_64 · **应该** + +| mcpp target | 目标机 OS | 编译器 | 编译器 target | sysroot | c-abi | c++-abi | openkal | 构建配置特殊说明 | +|---|---|---|---|---|---|---|---|---| +| `x86_64-linux-gnu` | linux | llvm | `x86_64-unknown-linux-gnu` | 图供给 | musl(图) | libc++(图) | openkal-linux | 三层全来自图 | +| `x86_64-linux-musl` | linux | llvm | `x86_64-unknown-linux-musl` | 图供给 | musl(图) | libc++(图) | openkal-linux | — | +| `aarch64-linux-musl` | linux | llvm | `aarch64-unknown-linux-musl` | 图供给 | musl(图) | libc++(图) | openkal-linux | 交叉 | +| `x86_64-windows-gnu` | windows | llvm | `x86_64-w64-windows-gnu` | 图供给 | **musl**(图) | libc++(图) | openkal-windows | ⚠️ c-abi 与载荷体系**不同** | +| `x86_64-windows-musl` | windows | llvm | `x86_64-w64-windows-gnu` | 图供给 | musl(图) | libc++(图) | openkal-windows | 与上一行同一 LLVM 三元组 | +| `aarch64-macos` | macos | llvm | `arm64-apple-macos14.0` | 图供给 + **平台 SDK** | musl(图) | libc++(图) | openkal-macos | ⚠️ Darwin 的内核接口是 libSystem,`-isysroot` 必须存活 | +| `riscv64-none-elf` | 无 | llvm | `riscv64-none-elf` | 图供给 | musl(图) | libc++(图) | openkal-opensbi | — | +| `x86_64-windows-msvc` | windows | - | - | - | - | - | - | **不支持**:MSVC ABI 与图不兼容 | +| 三个 planned 目标 | — | - | - | - | - | - | - | **不支持**:tier=planned | +| 任意目标 | — | gcc | - | - | - | - | - | **不支持**:`requires` 拒绝 gcc | + +## 表 5 — openkal 体系 · 构建机 linux/x86_64 · **现在**(全部实测) + +| mcpp target | 目标机 OS | 编译器 | 编译器 target | sysroot | c-abi | c++-abi | openkal | 构建配置特殊说明 | +|---|---|---|---|---|---|---|---|---| +| `x86_64-linux-gnu` | linux | llvm | `x86_64-unknown-linux-gnu` | 图供给 | musl(图) | libc++(图) | openkal-linux@0.5.4 | ✅ | +| `x86_64-linux-musl` | linux | llvm | `x86_64-unknown-linux-musl` | 图供给 | musl(图) | libc++(图) | openkal-linux@0.5.4 | ✅ | +| `aarch64-linux-musl` | linux | llvm | `aarch64-unknown-linux-musl` | 图供给 | musl(图) | libc++(图) | openkal-linux@0.5.4 | ✅ | +| `x86_64-windows-gnu` | windows | llvm | `x86_64-w64-windows-gnu` | 图供给 | musl(图) | libc++(图) | openkal-windows@0.1.5 | ✅ **c-abi 由 gnu 变 musl** | +| `x86_64-windows-musl` | windows | llvm | `x86_64-w64-windows-gnu` | 图供给 | musl(图) | libc++(图) | openkal-windows@0.1.5 | ✅ 产出 PE32+ | +| `aarch64-macos` | macos | llvm | `arm64-apple-macos14.0` | 图供给 | musl(图) | libc++(图) | openkal-macos@0.3.4 | ✅ **linux→macOS 交叉成立** | +| `riscv64-none-elf` | 无 | llvm | — | 图供给 | musl(图) | libc++(图) | — | ✅ | +| `x86_64-windows-msvc` | windows | llvm | - | - | - | - | - | ✅ 拒绝(需 MSVC ABI) | +| 三个 planned 目标 | — | llvm | - | - | - | - | - | ✅ `registered but not…` | +| 全部 14 个 | — | **gcc** | - | - | - | - | - | ✅ `requires the compiler family to be llvm` | + +## 表 6 — openkal 体系 · 构建机 windows/x86_64 与 macos/aarch64 · **现在(推)** + +⚠️ **本表全部为源码推导,无任何测量。** 但有一条**间接证据**:`openkal-cross.yml` 的 +3 宿主 × 3 目标矩阵在这两个宿主上是绿的(目标为 `x86_64-linux-gnu`、`aarch64-macos`、 +`x86_64-windows-gnu`)。 + +| 构建机 | mcpp target | 目标机 OS | 编译器 | 编译器 target | sysroot | c-abi | c++-abi | openkal | 构建配置特殊说明 | +|---|---|---|---|---|---|---|---|---|---| +| windows | `x86_64-linux-gnu` | linux | llvm | `x86_64-unknown-linux-gnu` 推 | 图供给 推 | musl(图)推 | libc++(图)推 | openkal-linux | CI 绿(间接) | +| windows | `x86_64-windows-gnu` | windows | llvm | `x86_64-w64-windows-gnu` 推 | 图供给 推 | musl(图)推 | libc++(图)推 | openkal-windows | CI 绿(间接) | +| windows | `aarch64-macos` | macos | llvm | `arm64-apple-macos14.0` 推 | 图供给 推 | musl(图)推 | libc++(图)推 | openkal-macos | CI 绿(间接) | +| windows | `x86_64-windows-msvc` | windows | - | - | - | - | - | - | **不支持**:MSVC ABI 与图不兼容 | +| windows | 四个 `*-none-elf` | 无 | llvm | 各自 推 | 图供给 推 | musl(图)推 | libc++(图)推 | opensbi/uefi | — | +| windows | 三个 planned | — | - | - | - | - | - | - | **不支持**:tier=planned | +| windows | 任意 | — | gcc | - | - | - | - | - | **不支持**:`requires` | +| macos | `x86_64-linux-gnu` | linux | llvm | `x86_64-unknown-linux-gnu` 推 | 图供给 推 | musl(图)推 | libc++(图)推 | openkal-linux | CI 绿(间接) | +| macos | `aarch64-macos` | macos | llvm | `arm64-apple-macos14.0` 推 | 图供给 + SDK 推 | musl(图)推 | libc++(图)推 | openkal-macos | ⚠️ 本机 macOS + 图,`-isysroot` 路径 | +| macos | `x86_64-windows-gnu` | windows | llvm | `x86_64-w64-windows-gnu` 推 | 图供给 推 | musl(图)推 | libc++(图)推 | openkal-windows | CI 绿(间接) | +| macos | 四个 `*-none-elf` | 无 | llvm | 各自 推 | 图供给 推 | musl(图)推 | libc++(图)推 | opensbi/uefi | — | +| macos | `x86_64-windows-msvc` / 三个 planned | — | - | - | - | - | - | - | **不支持** | +| macos | 任意 | — | gcc | - | - | - | - | - | **不支持**:`requires` | + +--- + +## 差异行 + +**载荷体系(表 1 vs 表 2)四行不符;openkal 体系(表 4 vs 表 5)全部一致。** + +| 差异 | 表 1 说应该 | 表 2 实测 | 影响 | +|---|---|---|---| +| **A** | llvm × 三个 linux 目标:`xim:glibc`/musl sysroot 就位 | sysroot **无**,clang 找宿主的,hermetic 拒绝 | **llvm 在 hosted linux 目标上不可用** | +| **B** | llvm × windows-gnu:MinGW sysroot 就位 | `ldflags` 为空,无 `-B/-L/--sysroot` | **不可用,且诊断无关**(`unknown file type`) | +| **C** | 目标行的 picolibc 就位 | 声明而不安装(#510) | **干净环境下裸机不可用** | +| **D** | 裸机行的 pin 是能力陈述,gcc 不该接手 | 用户显式声明 gcc 时无条件让位 | 诊断指向 `-mabi` 而非决定 | + +### A 与 B 同源,而 A 有一处关键事实 + +两者都是:**clang 靠 `--target=` 切目标,目标 sysroot 必须由外部给,mcpp 没给。** +gcc 不需要 —— 它一个目标一份载荷,驱动自带 sysroot(实测存在 +`xim-x-mingw-cross-gcc/16.1.0/x86_64-w64-mingw32/`)。 + +⭐⭐ **A 不是「缺少 sysroot」,是「同一份东西 gcc 接上了而 llvm 没接」。** 实测两条 +链接线: + +``` +gcc × x86_64-linux-gnu ldflags: + -Wl,--dynamic-linker=…/xim-x-glibc/2.44/lib64/ld-linux-x86-64.so.2 + -L…/xim-x-glibc/2.44/lib64 + -Wl,-rpath,…/xim-x-glibc/2.44/lib64 + -B…/xim-x-binutils/2.42/bin + +llvm × x86_64-linux-gnu ldflags: + --target=x86_64-unknown-linux-gnu --no-default-config -fuse-ld=lld + -L…/xim-x-llvm/22.1.8/lib/x86_64-unknown-linux-gnu + -L…/xim-x-gcc-runtime/15.1.0/lib64 + (没有任何 xim-x-glibc) +``` + +而缺的正是 glibc 的启动对象,**它们就在那份载荷里**: + +``` +$ ls …/xim-x-glibc/2.44/lib/ | grep -E '^(Scrt1|crti|crtn)\.o$' +crti.o crtn.o Scrt1.o +``` + +⭐ 所以 A 的一句话是:**`xim:glibc` 装了、gcc 的链接线接了、llvm 的没接。** 缺的不 +是载荷,是 llvm 分支上对应的 `-L`/`-B`/`--dynamic-linker`。这比缺一份载荷容易修得 +多,也把 A 的优先级提到 B 之前。 + +⚠️ **hermetic 检查对 A 报而对 B 不报,未查清。未实测。** + +--- + +## 表 3 与表 6 的「推」如何消除 + +⭐ **把表 2 与表 5 的扫描做成脚本,在三个构建机的 CI 上各跑一次,产出同一张表。** +那样矩阵就不再是一份会悄悄过期的文档,而是一次测量的输出。 + +⚠️ 表 3 里有一格现在就存疑,**须优先测**:windows 宿主 × `x86_64-windows-musl` +—— `host_can_serve` 返回真(`is_windows` 分支),而这个目标**没有任何载荷**,它的 +系统只能来自图。若真如此,windows 宿主上无依赖地请求它会走到与 B 相同的形态。 + +--- + +## 建议顺序 + +1. **A** —— `xim:glibc` 已在,查为何没接上。收益最大、形态最清楚。 +2. **B** —— 与 A 同源;并查 hermetic 为何不报。 +3. **表 3 的存疑格**(windows × windows-musl),它可能是 B 的第二个实例。 +4. **C**(#510)、**D** —— 已有判据,可独立进行。 +5. **脚本化 + 三宿主 CI**,消除表 3 与表 6 的全部「推」。 From 86c0dbb3373f4d41bfcd4e6ef7803f8632eac03c Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 04:46:49 +0800 Subject: [PATCH 06/32] docs(matrix): unsupported is not a defect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three cells were marked as failures that are simply not built yet. The distinction is whether the payload exists: llvm × x86_64-linux-gnu xim:glibc is installed, carries Scrt1.o/crti.o/ crtn.o, and gcc's link line reaches for it while llvm's does not — same payload, one branch wired and the other not. A defect. llvm × x86_64-linux-musl xim:musl is in the index; mcpp never installs it for llvm. Not built yet. llvm × aarch64-linux-musl llvm's payload has no such sysroot directory. llvm × x86_64-windows-gnu no MinGW sysroot package exists at all. What remains a defect in the last one is the diagnostic: it says `unknown file type` where the cause is a missing sysroot, while the same class of problem on x86_64-linux-gnu produces an accurate hermetic report. --- .../2026-08-26-target-matrix-six-tables.md | 96 +++++++++++-------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/.agents/docs/2026-08-26-target-matrix-six-tables.md b/.agents/docs/2026-08-26-target-matrix-six-tables.md index 2093aa2e..45e9f8bd 100644 --- a/.agents/docs/2026-08-26-target-matrix-six-tables.md +++ b/.agents/docs/2026-08-26-target-matrix-six-tables.md @@ -23,7 +23,16 @@ mcpp 的行为 —— 前一版文档就这么错过一次。 | 无标记 | Linux x86_64 宿主上**真跑过** | | **推** | 由源码逻辑推出,**无本地测量** | | `-` | 该轴对这一格无意义 | -| **不支持** | 明确不支持,并给出原因 | +| **不支持** | 这个组合不成立,并给出原因 | +| **未支持** | 组合成立,但 mcpp 还没做 —— **不是缺陷** | + +⚠️ **「未支持」与「缺陷」必须分开。** 一格没跑通有三种可能,只有第三种是缺陷: + +| 状态 | 判据 | 记号 | +|---|---|---| +| 载荷根本不存在 | 索引里没有这个 sysroot 包 | **未支持** | +| 载荷存在但 mcpp 从不为它接线 | 索引里有,而 mcpp 没有装它、也没有下发对应 flag | **未支持** | +| 载荷已装、别的分支接了,这个分支没接 | 同一份载荷,一个编译器接上了另一个没有 | ❌ **缺陷** | ### 两道门是分开的 @@ -64,11 +73,11 @@ mcpp 的行为 —— 前一版文档就这么错过一次。 | `x86_64-linux-gnu` | linux | gcc | `x86_64-unknown-linux-gnu` | 载荷自带 | glibc | libstdc++ | - | — | | `x86_64-linux-gnu` | linux | llvm | `x86_64-unknown-linux-gnu` | `xim:glibc` + `xim:linux-headers` | glibc | libc++ | - | 载荷已装;gcc 分支接了,llvm 分支未接 | | `x86_64-linux-musl` | linux | gcc | `x86_64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | - | 驱动 `x86_64-linux-musl-g++` | -| `x86_64-linux-musl` | linux | llvm | `x86_64-unknown-linux-musl` | 需 musl sysroot | musl | libc++ | - | — | +| `x86_64-linux-musl` | linux | llvm | `x86_64-unknown-linux-musl` | `xim:musl`(索引里有) | musl | libc++ | - | **未支持**:mcpp 从不为 llvm 装它 | | `aarch64-linux-musl` | linux | gcc | `aarch64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | - | 交叉 | -| `aarch64-linux-musl` | linux | llvm | `aarch64-unknown-linux-musl` | 需 musl sysroot | musl | libc++ | - | 交叉 | +| `aarch64-linux-musl` | linux | llvm | `aarch64-unknown-linux-musl` | 无对应载荷 | musl | libc++ | - | **未支持**:llvm 载荷无此 sysroot 目录 | | `x86_64-windows-gnu` | windows | gcc | `x86_64-w64-windows-gnu` | 载荷自带 | gnu(MinGW CRT) | libstdc++ | - | 包名 `mingw-cross-gcc` | -| `x86_64-windows-gnu` | windows | llvm | `x86_64-w64-windows-gnu` | 需 MinGW sysroot | gnu | libc++ | - | — | +| `x86_64-windows-gnu` | windows | llvm | `x86_64-w64-windows-gnu` | 无 MinGW sysroot 载荷 | gnu | libc++ | - | **未支持**:索引里没有这个包 | | `riscv64-none-elf` | 无 | llvm | `riscv64-none-elf` | `xim:picolibc-riscv@1.8.12` | picolibc | 无 | - | `-march/-mabi/-mcmodel` 由 freestanding 表定 | | `riscv32-none-elf` | 无 | llvm | `riscv32-none-elf` | `xim:picolibc-riscv@1.8.12` | picolibc | 无 | - | 同上 | | `aarch64-none-elf` | 无 | llvm | `aarch64-none-elf` | 无(表里为空) | 无 | 无 | - | preview;须 `sysroot = ""` | @@ -86,13 +95,13 @@ mcpp 的行为 —— 前一版文档就这么错过一次。 | mcpp target | 目标机 OS | 编译器 | 编译器 target | sysroot | c-abi | c++-abi | openkal | 构建配置特殊说明 | |---|---|---|---|---|---|---|---|---| | `x86_64-linux-gnu` | linux | gcc | `x86_64-unknown-linux-gnu` | 载荷自带 | gnu | libstdc++ | - | ✅ | -| `x86_64-linux-gnu` | linux | llvm | `x86_64-unknown-linux-gnu` | **无** | gnu | — | - | ❌ **A**:`Scrt1.o (outside the sandbox)` | +| `x86_64-linux-gnu` | linux | llvm | `x86_64-unknown-linux-gnu` | **无** | gnu | — | - | ❌ **A**:载荷已装而未接线 | | `x86_64-linux-musl` | linux | gcc | `x86_64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | - | ✅ | -| `x86_64-linux-musl` | linux | llvm | `x86_64-unknown-linux-musl` | **无** | musl | — | - | ❌ **A** | +| `x86_64-linux-musl` | linux | llvm | `x86_64-unknown-linux-musl` | **无** | musl | — | - | **未支持**:`xim:musl` 未接线;诊断是 hermetic | | `aarch64-linux-musl` | linux | gcc | `aarch64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | - | ✅ | -| `aarch64-linux-musl` | linux | llvm | `aarch64-unknown-linux-musl` | **无** | musl | — | - | ❌ **A** | +| `aarch64-linux-musl` | linux | llvm | `aarch64-unknown-linux-musl` | **无** | musl | — | - | **未支持**:无此 sysroot;诊断是 hermetic | | `x86_64-windows-gnu` | windows | gcc | `x86_64-w64-windows-gnu` | 载荷自带 | gnu | libstdc++ | - | ✅ `ldflags = -lstdc++exp` | -| `x86_64-windows-gnu` | windows | llvm | `x86_64-w64-windows-gnu` | **无** | gnu | — | - | ❌ **B**:`ldflags` 为空 | +| `x86_64-windows-gnu` | windows | llvm | `x86_64-w64-windows-gnu` | **无** | gnu | — | - | **未支持** + ❌ **B**:无载荷,**而诊断说的是别的事** | | `riscv64-none-elf` | 无 | llvm | — | `sysroot=""` | 无 | 无 | - | ✅ 产出 RISC-V ELF | | `riscv64-none-elf` | 无 | llvm | — | **表里的 picolibc** | picolibc | 无 | - | ❌ **C**:干净环境不安装(#510) | | `riscv32-none-elf` | 无 | llvm | — | `sysroot=""` | 无 | 无 | - | ✅ | @@ -190,48 +199,53 @@ libunwind,用 gcc 构建它不是一件存在的事;包用 `requires` 声明了 **载荷体系(表 1 vs 表 2)四行不符;openkal 体系(表 4 vs 表 5)全部一致。** -| 差异 | 表 1 说应该 | 表 2 实测 | 影响 | -|---|---|---|---| -| **A** | llvm × 三个 linux 目标:`xim:glibc`/musl sysroot 就位 | sysroot **无**,clang 找宿主的,hermetic 拒绝 | **llvm 在 hosted linux 目标上不可用** | -| **B** | llvm × windows-gnu:MinGW sysroot 就位 | `ldflags` 为空,无 `-B/-L/--sysroot` | **不可用,且诊断无关**(`unknown file type`) | -| **C** | 目标行的 picolibc 就位 | 声明而不安装(#510) | **干净环境下裸机不可用** | -| **D** | 裸机行的 pin 是能力陈述,gcc 不该接手 | 用户显式声明 gcc 时无条件让位 | 诊断指向 `-mabi` 而非决定 | - -### A 与 B 同源,而 A 有一处关键事实 +| 差异 | 类别 | 表 1 说应该 | 表 2 实测 | 影响 | +|---|---|---|---|---| +| **A** | ❌ **缺陷** | llvm × `x86_64-linux-gnu` 接上 `xim:glibc` | 载荷已装、gcc 接了、llvm 没接 | llvm 在这个目标上不可用 | +| **B** | ❌ **诊断缺陷** | llvm × windows-gnu 若不支持,应当这么说 | 报 `ld.lld: unknown file type`,与真因无关 | 使用者被指向错误方向 | +| **C** | ❌ **缺陷** | 目标行 picolibc 就位 | 声明而不安装(#510) | 干净环境裸机不可用 | +| **D** | ❌ **缺陷** | 裸机 pin 是能力陈述,gcc 不该接手 | 用户写 gcc 时无条件让位 | 诊断指向 `-mabi` 而非决定 | +| — | **未支持** | llvm × `x86_64-linux-musl` | `xim:musl` 在索引里,mcpp 从不为 llvm 装 | 功能缺口,**不是缺陷** | +| — | **未支持** | llvm × `aarch64-linux-musl` | llvm 载荷无该 sysroot 目录 | 同上 | +| — | **未支持** | llvm × `x86_64-windows-gnu` | 索引里没有 MinGW sysroot 包 | 同上 | -两者都是:**clang 靠 `--target=` 切目标,目标 sysroot 必须由外部给,mcpp 没给。** -gcc 不需要 —— 它一个目标一份载荷,驱动自带 sysroot(实测存在 -`xim-x-mingw-cross-gcc/16.1.0/x86_64-w64-mingw32/`)。 +### A 是缺陷,而三个 musl/mingw 格不是 -⭐⭐ **A 不是「缺少 sysroot」,是「同一份东西 gcc 接上了而 llvm 没接」。** 实测两条 -链接线: +⭐ **区别在于那份载荷在不在。** 实测: ``` -gcc × x86_64-linux-gnu ldflags: - -Wl,--dynamic-linker=…/xim-x-glibc/2.44/lib64/ld-linux-x86-64.so.2 - -L…/xim-x-glibc/2.44/lib64 - -Wl,-rpath,…/xim-x-glibc/2.44/lib64 - -B…/xim-x-binutils/2.42/bin - -llvm × x86_64-linux-gnu ldflags: - --target=x86_64-unknown-linux-gnu --no-default-config -fuse-ld=lld - -L…/xim-x-llvm/22.1.8/lib/x86_64-unknown-linux-gnu - -L…/xim-x-gcc-runtime/15.1.0/lib64 - (没有任何 xim-x-glibc) +$ ls …/xim-x-llvm/22.1.8/lib/ | grep -E '^(x86_64|aarch64|riscv)' +x86_64-unknown-linux-gnu ← 只有这一个 + +$ ls -d …/xpkgs/xim-x-glibc ✅ 已装 +$ ls -d …/xpkgs/xim-x-musl ❌ 无此载荷(索引里有 xim:musl,mcpp 不装) +$ mcpp search musl | grep mingw ❌ 索引里没有 MinGW sysroot 包 ``` -而缺的正是 glibc 的启动对象,**它们就在那份载荷里**: +- **A**(`x86_64-linux-gnu`)—— `xim:glibc` 已装且带着 `Scrt1.o crti.o crtn.o`, + **gcc 的链接线接了它**: -``` -$ ls …/xim-x-glibc/2.44/lib/ | grep -E '^(Scrt1|crti|crtn)\.o$' -crti.o crtn.o Scrt1.o -``` + ``` + gcc : -Wl,--dynamic-linker=…/xim-x-glibc/2.44/lib64/ld-linux-x86-64.so.2 + -L…/xim-x-glibc/2.44/lib64 -Wl,-rpath,… -B…/xim-x-binutils/2.42/bin + llvm: --target=… --no-default-config -fuse-ld=lld + -L…/xim-x-llvm/… -L…/xim-x-gcc-runtime/… ← 没有任何 xim-x-glibc + ``` + + **同一份载荷,一个分支接上另一个没接** —— 这是缺陷。 + +- **三个 musl / mingw 格** —— 载荷根本不在。这是**未支持**,该做的是补载荷与接线, + 不是修 bug。 -⭐ 所以 A 的一句话是:**`xim:glibc` 装了、gcc 的链接线接了、llvm 的没接。** 缺的不 -是载荷,是 llvm 分支上对应的 `-L`/`-B`/`--dynamic-linker`。这比缺一份载荷容易修得 -多,也把 A 的优先级提到 B 之前。 +### B 剩下的那一半确实是缺陷 -⚠️ **hermetic 检查对 A 报而对 B 不报,未查清。未实测。** +即便 llvm × windows-gnu 归为「未支持」,**诊断仍然是错的**:它报 +`ld.lld: error: obj/main.o: unknown file type`,而真因是没有 MinGW sysroot。 + +⚠️ 与 A 对照:A 那格 hermetic 检查**报得很准**(逐个列出落在沙箱外的 `.o`)。 +**同一类问题,一处有准确诊断、一处没有** —— 为什么,未查清。**未实测。** + +--- --- From b02f8ed3c0832cfefbfd90e5b1cf97983d8a3cb1 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:11:45 +0800 Subject: [PATCH 07/32] =?UTF-8?q?2026.8.26.1=20=E2=80=94=20naming=20the=20?= =?UTF-8?q?host's=20own=20target=20changed=20the=20link=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spelling `--target` was read as "this build's system comes from the graph". `crossTarget` is the string `--target=`; it is non-empty for ANY named target, including the host's own on a project that depends on nothing. $ mcpp build → ELF 64-bit LSB pie executable $ mcpp build --target x86_64-linux-gnu → hermetic link check failed The same wrong question was asked in three places, and the comment beside one of them predicted TWO channels. All three now ask `targetSide.cAbi.prebuilt()` — the predicate 2026.8.25.1 moved three other decisions onto. A fourth channel: the mingw branch returned a link line with no `--target=` at all, so a correct COFF object was handed to the ELF flavour of lld. Its own comment stated the precondition the condition did not check. Also fixed: a target row's declared sysroot was never installed (#510); a bare-metal row's toolchain pin was overridable (it is a capability, not a convention), and so is `x86_64-windows-musl`'s — no gcc payload emits PE+musl, and declaring one resolved the host's Linux musl payload; overriding a hosted row's convention with nothing in its place ran the whole build and failed at the link. Machine interface: `toolchain list` and `why toolchain` now speak `--format json`. `why toolchain` resolves without building and reports the five layers, the driver, the triple, the C-library model, and `status`/`reason` — `reason` being a token from the new `mcpp.build.refusal`. The target matrix and four e2e tests classify from it, so no criterion in this repository is a substring search any more. Measured cost of the old way, in this same session: rewording one refusal turned an assertion into a no-op. Tests: e2e 295 is an identity (naming the host's own target must not change one flag) and it counted the channels down 7 → 5 → 3 → 0; 296, 297, 298; and ci-target-matrix.yml scans the full table on three hosts against tests/matrix/expected.tsv, which carries no `mismatch` rows by construction. --- .../2026-08-26-cross-target-implies-graph.md | 297 ++++++++++++++++++ .github/workflows/ci-target-matrix.yml | 166 ++++++++++ CHANGELOG.md | 149 ++++++++- docs/11-machine-output.md | 56 ++++ docs/16-the-target-triple.md | 41 +++ docs/zh/11-machine-output.md | 53 ++++ docs/zh/16-the-target-triple.md | 38 +++ mcpp.toml | 2 +- src/build/flags.cppm | 69 +++- src/build/prepare.cppm | 175 ++++++++++- src/build/refusal.cppm | 93 ++++++ src/cli.cppm | 36 ++- src/cli/cmd_self.cppm | 22 ++ src/cli/cmd_toolchain.cppm | 15 +- src/doctor.cppm | 172 ++++++++++ src/toolchain/lifecycle.cppm | 66 +++- src/toolchain/registry.cppm | 18 +- src/toolchain/triple.cppm | 22 ++ src/version.cppm | 2 +- src/wire.cppm | 16 +- ..._naming_the_host_target_changes_nothing.sh | 137 ++++++++ ...report_names_is_what_the_link_line_uses.sh | 127 ++++++++ ...97_a_capability_pin_is_not_a_preference.sh | 140 +++++++++ ...ding_a_convention_requires_replacing_it.sh | 170 ++++++++++ tests/e2e/run_all.sh | 17 +- tests/matrix/compare.sh | 69 ++++ tests/matrix/expected.tsv | 70 +++++ tests/matrix/scan.sh | 154 +++++++++ 28 files changed, 2346 insertions(+), 46 deletions(-) create mode 100644 .agents/docs/2026-08-26-cross-target-implies-graph.md create mode 100644 .github/workflows/ci-target-matrix.yml create mode 100644 src/build/refusal.cppm create mode 100755 tests/e2e/295_naming_the_host_target_changes_nothing.sh create mode 100755 tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh create mode 100755 tests/e2e/297_a_capability_pin_is_not_a_preference.sh create mode 100644 tests/e2e/298_overriding_a_convention_requires_replacing_it.sh create mode 100755 tests/matrix/compare.sh create mode 100644 tests/matrix/expected.tsv create mode 100755 tests/matrix/scan.sh diff --git a/.agents/docs/2026-08-26-cross-target-implies-graph.md b/.agents/docs/2026-08-26-cross-target-implies-graph.md new file mode 100644 index 00000000..8c991f83 --- /dev/null +++ b/.agents/docs/2026-08-26-cross-target-implies-graph.md @@ -0,0 +1,297 @@ +# `crossTarget` 非空被当成「系统来自图」 + +2026-08-26 · 核心问题 + 修复方案 + CI 验收体系(待 review,尚未实施) + +前置:[`2026-08-26-target-matrix-six-tables.md`](2026-08-26-target-matrix-six-tables.md) + +--- + +## 0. 一句话 + +> **写出 `--target` 这个动作,被当成了「这个构建的系统来自依赖图」。** + +而这两件事互不蕴含:一个项目可以显式命名它的宿主目标而完全不用图;也可以不写 +`--target` 而让图供给一切。 + +⭐ **这与 2026.8.25.1 / .2 修的七条是同一个形状** —— 一个谓词回答了比自己更窄的问 +题。区别只在这次的谓词不是「跨两层的 OR」,而是**「命令行上有没有这个字符串」**。 + +--- + +## 1. 判据 + +同一台机器、同一个编译器、**同一个目标**,只差写不写 `--target`: + +``` +$ mcpp build ✅ ELF 64-bit LSB pie executable, x86-64 +$ mcpp build --target x86_64-linux-gnu ❌ hermetic link check failed +``` + +不带 `--target` 时宿主目标**就是** `x86_64-linux-gnu`。两条链接线: + +| | 无 `--target` | 带 `--target x86_64-linux-gnu` | +|---|---|---| +| | `-stdlib=libc++` | — | +| | `--rtlib=compiler-rt` | — | +| | `--unwindlib=libunwind` | — | +| | `lm.link_flags()`(`-L…/xim-x-glibc/…`、loader) | — | +| | — | `--target=x86_64-unknown-linux-gnu` | + +成功那条走的是 **clang 自带的完整运行时栈**(libc++ + compiler-rt + libunwind), +它不需要 glibc 的启动对象。失败那条把这些**全部丢掉**,clang 于是回退到默认 +(libstdc++ + libgcc + 宿主 crt),启动对象落到 `/lib/x86_64-linux-gnu/` 外面, +hermetic 检查拒绝。 + +## 2. 根因 + +`flags.cppm:616`: + +```cpp +if (!crossTarget.empty()) { + // ⭐⭐ THE TARGET SIDE COMES FROM THE GRAPH, SO THE HOST'S MODEL + // CONTRIBUTES NOTHING … + link_toolchain_flags += " -fuse-ld=lld"; + link_toolchain_flags_c = link_toolchain_flags; +} else { + if (lm.mode == CLibMode::Sysroot) link_toolchain_flags += lm.link_flags(…); + link_toolchain_flags_c = … + kLinkDriverFlagsC; + link_toolchain_flags += kLinkDriverFlags; +} +``` + +⭐ **注释说的是「目标侧来自图」,而条件问的是「`crossTarget` 非空」。** `crossTarget` +只是 `--target=` 这个字符串,它在**任何**显式目标下都非空 —— 包括显式 +写出宿主目标、也包括一个完全没有依赖的项目。 + +那段注释里的实测(`ld64.lld: unknown argument '--as-needed'`)是真的,而且当时的修复 +是对的:**当系统真的来自图**时,宿主的 C 库模型与载荷的 C++ 运行时都不该出现在链接 +线上。错的是把「来自图」写成了「`crossTarget` 非空」。 + +### 这解释了矩阵里的两格 + +| 格 | 现象 | 现在可知的原因 | +|---|---|---| +| **A** llvm × `x86_64-linux-gnu` | hermetic 拒绝 | 同一分支,丢掉 `lm.link_flags()` 与三个运行时 flag | +| **B** llvm × `x86_64-windows-gnu` | `ld.lld: unknown file type` | **同一分支**,同样丢掉;诊断落在别处 | + +⚠️ **A 与 B 不是「同源但不同因」,是同一处代码的两个实例。** 上一版文档把它们写成 +两条,不准确。 + +⚠️ **仍未查清:** hermetic 检查对 A 报得很准而对 B 什么都没说。**未实测。** + +--- + +## 3. 修复方案 + +### 3.1 条件换成它注释里说的那件事 + +`TargetSide` 已经有这个答案,不需要新概念: + +```cpp +// 现在:问命令行上有没有这个字符串 +if (!crossTarget.empty()) { …丢掉宿主模型… } + +// 应为:问系统是不是真的来自图 +if (!plan.targetSide.cAbi.prebuilt()) { …丢掉宿主模型… } +``` + +`cAbi.prebuilt()` = `Payload || Xpkg`,它正是「C 库来自这台机器已有的东西」。 +⭐ 这与 2026.8.25.1 修 ①③④ 时用的是**同一个谓词** —— 那三处也是把「图供给」写成 +了别的东西。 + +⚠️ **判据必须两向:** +- 系统来自图时,宿主模型仍然不得出现(否则 `ld64.lld: unknown argument` 回归) +- 系统来自载荷时,**无论有没有写 `--target`,链接线都必须相同** + +⭐ 第二条是**天然的判据**:同一台机器上 `mcpp build` 与 +`mcpp build --target <宿主目标>` 生成的 `ldflags` 应当逐字节相等。这不需要新的断言 +机制,它是一个恒等式。 + +### 3.2 `crossTarget` 本身要留 + +`--target=` 仍然必须下发 —— 它是「这是给哪台机器的」。要改的只是**它不再兼任「系统 +来自图」的信号**。 + +--- + +## 4. CI 验收:让矩阵由测量维持 + +### 4.1 现状为什么看不见这些 + +| 已有 | 覆盖什么 | 看不见什么 | +|---|---|---| +| `openkal-cross.yml` 3 宿主 × 3 目标 | openkal 体系 | **载荷体系一格都没有** | +| `ci-linux-e2e` 等 | 宿主构建(不写 `--target`) | **写 `--target` 的那条路** | +| `ecosystem-e2e`(本次新增) | openkal 六条 e2e | 同上 | + +⭐ **A 与 B 能存活,是因为现有 CI 里没有任何一格是「载荷体系 × 显式 `--target`」。** +openkal 那三宿主矩阵全部走图,恰好是不受影响的那一半。 + +### 4.2 方案:一条脚本,三个宿主,输出同一张表 + +新增 `tests/matrix/scan.sh`,在 `windows` / `linux` / `macos` 三个 runner 上各跑一 +次,对**每一个** `kKnownTargets` 行 × 每一个可用编译器 × 两种系统来源: + +``` +mcpp build --target → 取构建报告的五层 + build.ninja 的 ldflags +``` + +产出一行 TSV: + +``` +host target compiler compiler-triple sysroot c-abi c++-abi openkal status +linux x86_64-linux-gnu gcc x86_64-unknown-linux-gnu payload gnu libstdc++ - ok +linux x86_64-linux-musl llvm x86_64-unknown-linux-musl - musl - - unsupported +``` + +**`status` 只有三种取值**,而三者的区别是本方案的核心: + +| status | 含义 | 判据 | +|---|---|---| +| `ok` | 构建成功且五层与期望表一致 | 产出存在 + 报告逐字段相等 | +| `unsupported` | 期望表就说不支持 | **跳过,不是失败** | +| `mismatch` | 与期望表不符 | **失败** | + +⚠️ **「跳过」必须是期望表说的,不是运行时发现的。** 一格因为「今天这台机器没装某个 +载荷」而跳过,与「这个组合本就不支持」是两回事 —— 前者会让矩阵在缺件的机器上悄悄 +变绿,正是本次会话里反复出现的那种假绿。 + +### 4.3 期望表放在仓库里,与代码一起改 + +`tests/matrix/expected.tsv` —— 六张表的机器可读形式。CI 比对实测与它: + +- 实测 `ok` 而期望 `unsupported` ⇒ **失败**,提示「支持面扩大了,更新期望表」 +- 实测 `mismatch` ⇒ 失败,打印两侧差异 +- 实测 `unsupported` 而期望 `ok` ⇒ **失败**,这就是 A/B 这类回归 + +⭐ **这条最重要:期望表是仓库里的文件,改了行为就必须同时改它。** 那样「支持矩阵」 +不再是一份会悄悄过期的文档 —— 它是一次测量与一份声明的比对。 + +### 4.4 恒等式判据(不需要期望表就能查的一类) + +有几条关系与具体取值无关,任何宿主上都成立: + +| 恒等式 | 抓的是什么 | +|---|---| +| `mcpp build` 的 ldflags == `mcpp build --target <宿主目标>` 的 ldflags | **A/B 的根因** | +| 报告里 `c-abi` 为 `(payload)` ⇒ ldflags 必须含该载荷的 `-L` | 「声明了却没接上」 | +| 报告里 `c-abi` 为 `(graph)` ⇒ ldflags 不得含宿主 C 库路径 | 图模式的反向 | +| 请求目标的 OS == 解析三元组的 OS | 2026.8.25.2 已有守卫,此处是回归网 | + +⭐ **这四条不依赖期望表,也不依赖机器上装了什么** —— 它们是结构约束,任何一格只要 +跑起来了就该满足。建议先做这一层,再做 §4.3 的全表比对。 + +### 4.5 接进哪里 + +- 新 workflow `ci-target-matrix.yml`,`ubuntu-24.04` / `windows-2022` / `macos-14` + 三个 job +- 每个 job 跑 `scan.sh` → 与 `expected.tsv` 比对 → 上传实测 TSV 作为 artifact +- ⚠️ **必须断言扫描真的跑了**:`scan.sh` 输出行数 ≥ 期望表中该宿主的行数,否则 + 「一格没跑」与「全部通过」在退出码上没有区别 + +--- + +## 5. 建议顺序 + +1. **§4.4 的四条恒等式** —— 不需要期望表,先落地。第一条恒等式**当场就会红**, + 那就是 A/B 的判据。 +2. **修 §3.1 的条件** —— 换成 `cAbi.prebuilt()`,两向验证。 +3. **§4.2/4.3 的全表扫描 + 期望表**,三宿主接进 CI。 +4. 之后 issue #510(sysroot 声明不安装)与 D(裸机 pin 让位)可独立进行;它们会被 + §4.3 的期望表自动覆盖。 + +⚠️ **顺序不能反。** 先有判据再改代码 —— 否则「修好了」这句话没有任何东西支撑它, +而这正是本次会话反复付出代价的地方。 + +--- + +## 6. 落地回填(2026-08-26,实测) + +⚠️ 这一节记的是**实际做出来的东西与 §3–§5 的差**。计划本身没有错,但实施过程里 +量到了三件计划没有预见的事,每一件都改变了方案。 + +### 6.1 通道是三条,不是两条 + +`§3.1` 说要改一处条件。实际有三处在问同一个错问题: +`link_toolchain_flags`、`payload_ld`、`atomic_ld`。而 `payload_ld` 那处的注释 +**预告了两条**: + +> the C-runtime group reaches the link line through TWO channels, and a reader +> who fixed one saw the identical error and could reasonably conclude the fix +> had not worked. + +⭐ 找出第三条的,是 §4.4 的恒等式:差异 7 项 → 5 项 → 3 项 → 0,每修一处它就 +指向下一处。没有它,修完两处会看到「还是红」,而这条注释会让人以为已经找全。 + +第四条在别处:mingw 分支 `isMingwTc && cAbi.prebuilt()` 返回的链接线里**没有 +`--target=`**。它的注释同样写明了前提(`x86_64-w64-mingw32-g++` 不需要 `--target` +因为它没有别的),而条件没有检查这个前提——clang 有别的。实测 +`ld.lld: error: obj/main.o: unknown file type`,对象是一个完全合法的 +`Intel amd64 COFF`。 + +### 6.2 「能力 pin」不止裸机一行 + +`§3.3` 把能力 pin 等同于 `is_freestanding()`。实测 `x86_64-windows-musl` 也是: +没有任何 gcc 载荷能发出 PE + musl(mingw 载荷发的是 PE + MinGW CRT,那是隔壁 +`-gnu` 那一行)。声明 gcc 会解析到宿主的 **Linux** musl 载荷,报「没有 C++ 前端」。 + +真因是 `to_xim_package` 里 `t.is_musl()` 被当成「这是 linux-musl」用,而那段 +注释自己写的就是 linux-musl。`x86_64-windows-musl` 是后加的行,径直走了进来。 + +⇒ 谓词收敛到 `triple::pin_is_capability()`,三处决定共用它。 + +### 6.3 ⭐⭐ 判据不该靠匹配句子——机器接口 + +`§4` 的整套验收建立在解析 mcpp 打给人看的输出上。**在同一次会话里,这个方案 +自己踩了它要防的坑**:我把一句拒绝从 `cannot emit it` 改成 +`cannot be emitted by`,e2e 297 的断言当场变成空转——它仍然「通过」,只是不再 +匹配任何东西。 + +⇒ 两条命令进入 `--format json`: + +``` +mcpp toolchain list --format json → mcpp.toolchain.list +mcpp why toolchain --target T --toolchain C ... → mcpp.why.toolchain +``` + +后者只解析不构建,给出五层、驱动器、三元组、C 库模型,以及 `status` 与 +`reason`。`reason` 是一个记号,由新模块 `mcpp.build.refusal` 在每一处拒绝的 +`return` 之前记下。 + +⚠️ **消息仍然是承诺**。297/298 照旧断言拒绝点名了目标、规则与出路——换掉的只是 +**分类**。一个答案集有限的问题,不该用子串搜索来问。 + +⚠️ **但查询不能取代构建**。`llvm × x86_64-windows-gnu` 解析得完全正常,失败在 +链接期的封闭性检查上;只查不建会把它报成绿。scan.sh 两样都做:分类取自 +`reason`,结论取自 `mcpp build` 的退出码。于是整个矩阵**没有一处字符串匹配**。 + +⚠️ 而这套机器本身又造了一次同型缺陷:一处没有记号的拒绝分支让矩阵写下 +`unsupported / none`——「拒绝了」与「没有理由」共用一个词。现在无记号的拒绝报 +`other`。 + +### 6.4 探针本身会说谎 + +第一版 scan.sh 对每一格都写 `#include `,四个裸机目标全报 +`fatal error: 'cstdio' file not found` 并被记成缺陷。那不是 mcpp 的失败,是探针 +问错了问题。⭐ **一个自己就编不过的探针,产出的整列都是关于探针的。** + +同型的第二次:graph 模式下仍写裸机形状,而 openkal-musl 供给的是一个**有宿主的** +C 库,`_start` 去找 `main`,得到 `undefined symbol: main`。 + +### 6.5 实测结果(x86_64 Linux,2026-08-26) + +40 格(payload 24 + graph 16),**0 个 mismatch**。理由分布: + +| status | reason | 格数 | +|---|---|---| +| ok | none | 14 | +| unsupported | tier-planned | 12 | +| unsupported | capability-pin | 6 | +| unsupported | layer-requirement | 4 | +| unsupported | convention-unreplaced | 3 | +| unsupported | host-cannot-serve | 1 | + +⚠️ **`expected.tsv` 里没有 `mismatch`,这是刻意的。** 写下 `mismatch` 就是把一个 +缺陷声明成期望,矩阵会在它上面变绿。一格测出 `mismatch`,要么修 mcpp,要么让 +mcpp 在决定处给出一句带 `reason` 的拒绝——没有第三条路。 + diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml new file mode 100644 index 00000000..20aaadf1 --- /dev/null +++ b/.github/workflows/ci-target-matrix.yml @@ -0,0 +1,166 @@ +name: target matrix + +# ⭐⭐ 让支持矩阵成为一次测量的输出,而不是一份会悄悄过期的文档。 +# +# ⚠️ 这套东西存在的理由,是本仓库反复付出的一类代价:一格因为「今天这台机器恰好 +# 装了某个载荷」而通过,或因为没装而跳过,而两者在退出码上与「全部正确」没有区别。 +# 三个宿主各扫一遍,把结果与仓库里的期望表比对,差异即失败。 +on: + pull_request: + push: + branches: [ main ] + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + XLINGS_NON_INTERACTIVE: '1' + +jobs: + invariants: + # ⭐ 第一层:四条恒等式,不需要期望表,也不依赖机器上装了什么。 + # 它们是结构约束 —— 任何一格只要跑起来了就该满足。 + name: invariants (${{ matrix.host }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - { host: linux, runner: ubuntu-24.04 } + - { host: macos, runner: macos-14 } + - { host: windows, runner: windows-2022 } + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/bootstrap-mcpp + + - name: Build the mcpp in this pull request + run: | + set -euo pipefail + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true + "$MCPP" self config --mirror GLOBAL 2>/dev/null || true + "$MCPP" build --dev + # ⚠️ 两种拼写,且按 mtime 取最新 —— target/ 是缓存恢复的,`head -1` + # 会挑到上一次推送留下的二进制,版本号一样而代码是旧的。 + BUILT=$(find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) \ + -newer mcpp.toml | head -1) + [ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; } + BUILT=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT") + echo "MCPP_UNDER_TEST=$BUILT" >> "$GITHUB_ENV" + "$BUILT" --version + + - name: The invariants + run: | + set -euo pipefail + export MCPP="$MCPP_UNDER_TEST" + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + "$MCPP" self config --mirror GLOBAL 2>/dev/null || true + # ⚠️ These four read mcpp's MACHINE interface, so jq is not optional + # here. Without it each one takes its own "nothing to compare" exit — + # four honest-looking skips, and the next step would then report that + # the invariants did not run. Failing on the cause beats failing on + # the symptom four steps later. + command -v jq >/dev/null || { echo "::error::jq is missing on ${{ matrix.host }}"; exit 1; } + fail=0 + for t in tests/e2e/295_*.sh tests/e2e/296_*.sh \ + tests/e2e/297_*.sh tests/e2e/298_*.sh; do + echo "=== $t ===" + bash "$t" 2>&1 | tee "$(basename "$t").log" || true + rc=${PIPESTATUS[0]} + [ "$rc" = "0" ] || { echo "::error::$t failed (exit $rc)"; fail=1; } + done + [ "$fail" = 0 ] || exit 1 + + - name: Each invariant RAN + run: | + set -euo pipefail + # ⭐ 这一步存在的全部理由:退出码分不清「通过」与「跳过」。两条 e2e 都 + # 有为「这台机器没有可比的东西」准备的早退,而 CI 要的是它们真的比 + # 过了。 + check() { + grep -qF "$2" "$1".log || { + echo "::error::$1 did not reach its conclusion on ${{ matrix.host }}" + tail -6 "$1".log 2>/dev/null | sed 's/^/ /' + return 1 + } + echo " ok $1" + } + fail=0 + check 295_naming_the_host_target_changes_nothing.sh \ + "OK: naming the host's own target changes nothing" || fail=1 + check 296_what_the_report_names_is_what_the_link_line_uses.sh \ + "OK: what the report names is what the link line uses" || fail=1 + check 297_a_capability_pin_is_not_a_preference.sh \ + "OK: a capability pin is not a preference" || fail=1 + check 298_overriding_a_convention_requires_replacing_it.sh \ + "OK: a convention may be overridden, but not merely removed" || fail=1 + [ "$fail" = 0 ] || exit 1 + + scan: + # ⭐ 第二层:全表扫描,与仓库里的期望表比对。 + name: scan (${{ matrix.host }}) + needs: invariants + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + include: + - { host: linux, runner: ubuntu-24.04 } + - { host: macos, runner: macos-14 } + - { host: windows, runner: windows-2022 } + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/bootstrap-mcpp + + - name: Build the mcpp in this pull request + run: | + set -euo pipefail + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + "$MCPP" self config --mirror GLOBAL 2>/dev/null || true + "$MCPP" build --dev + BUILT=$(find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) \ + -newer mcpp.toml | head -1) + [ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; } + echo "MCPP_UNDER_TEST=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT")" >> "$GITHUB_ENV" + + - name: Scan both systems + run: | + set -euo pipefail + export MCPP="$MCPP_UNDER_TEST" + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + "$MCPP" self config --mirror GLOBAL 2>/dev/null || true + # ⚠️ 两种体系各自成表。scan 把 mode 写进第一列,而比对必须按 mode 分开 + # 做 —— 拿一种体系的测量去比整张表,另一种的每一行都会被报成「没跑到」。 + bash tests/matrix/scan.sh payload > measured-payload.tsv + bash tests/matrix/scan.sh graph > measured-graph.tsv + cat measured-payload.tsv measured-graph.tsv > measured.tsv + echo "--- measured ---"; cat measured.tsv + + # ⚠️ 上传排在比对之前,而这是刻意的次序。宿主的第一次运行本就没有期望行, + # 比对会红 —— 而回填要用的正是这份产物。`if: always()` 也保留:一步失败不 + # 该把证据一起带走。 + - uses: actions/upload-artifact@v4 + if: always() + with: + name: matrix-${{ matrix.host }} + path: measured.tsv + + - name: Compare with the expected table + run: | + set -euo pipefail + fail=0 + bash tests/matrix/compare.sh measured-payload.tsv \ + tests/matrix/expected.tsv ${{ matrix.host }} payload || fail=1 + bash tests/matrix/compare.sh measured-graph.tsv \ + tests/matrix/expected.tsv ${{ matrix.host }} graph || fail=1 + [ "$fail" = 0 ] || exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index c9494284..3783eb55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,137 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [2026.8.26.1] — 2026-08-26 + +写出 `--target` 这个动作,曾被当成「这个构建的系统来自依赖图」。完整分析见 +[`.agents/docs/2026-08-26-cross-target-implies-graph.md`](.agents/docs/2026-08-26-cross-target-implies-graph.md) +与[六张矩阵表](.agents/docs/2026-08-26-target-matrix-six-tables.md)。 + +### 修复 + +- **⭐⭐ 命名宿主自己的目标,曾让构建失败。** + + 同一台机器、同一个编译器、**同一个目标**,只差写不写 `--target`: + + ``` + $ mcpp build → ELF 64-bit LSB pie executable + $ mcpp build --target x86_64-linux-gnu → hermetic link check failed + ``` + + 不带 `--target` 时宿主目标**就是** `x86_64-linux-gnu`。显式那条丢掉了 + `-stdlib=libc++`、`--rtlib=compiler-rt`、`--unwindlib=libunwind`,以及指向已装 + `xim:glibc` 的 `-B`/`-L`/`--dynamic-linker`,于是 clang 回退到默认,启动对象落到 + 载荷之外。 + + 根因是 `crossTarget` 非空被当成「系统来自图」。那处代码**自己的注释**写的正是 + 后者;而 `crossTarget` 只是 `--target=<三元组>` 这个字符串,任何命名目标都非空 + ——包括命名宿主目标、且完全不依赖任何包的工程。 + + ⚠️ **同一个错误的问题被问了三遍**,分散在三处:`link_toolchain_flags`、 + `payload_ld`、`atomic_ld`。而其中一处的注释只预告了**两**条通道: + + > the C-runtime group reaches the link line through TWO channels, and a reader + > who fixed one saw the identical error and could reasonably conclude the fix + > had not worked. + + ⭐ 三条全部改问 `targetSide.cAbi.prebuilt()` —— 与 `2026.8.25.1` + 把另外三处决定迁过去的**同一个谓词**。这是该族的第六至第八条。 + +- **⭐ 目标行声明的 sysroot 从不被安装(#510)。** + + 一行目标表声明两样东西,只有一样被兑现:`pin` 走 + `resolve_xpkg_path(…, autoInstall=true, …)`,`sysroot` 是纯查询,查不到就静默 + 跳过整块。干净环境实测: + + ``` + Target riscv64-none-elf + c-abi picolibc-riscv (…, prebuilt) + error: 'stdio.h' file not found + ``` + + 报告点名了这个目标的 C 库,而构建找不到它的头。⚠️ mcpp 自己的裸机 CI **手工装 + 它**并在注释里说明了原因,于是每一条裸机 e2e 都跑在缺陷已被抹平的机器上。 + + 改为走同一个 `autoInstall` 通道;离线与 `MCPP_NO_AUTO_INSTALL` 由 `Fetcher` + 判定,不在此处再问一遍。 + +- **⭐ 裸机行的 pin 是能力陈述,不是偏好。** + + ``` + [toolchain] default = "gcc@16.1.0" + $ mcpp build --target riscv64-none-elf + g++: error: unrecognized argument in option '-mabi=lp64d' + ``` + + 一条关于选项的消息,而决定在一百行之前。宿主行的 pin 说的是「哪个载荷供给这个 + 目标的 C 库」,作者自带编译器时理应让位;裸机行说的是「哪个编译器能发出这个 + 目标」——宿主 g++ 发不出 riscv64,谁声明都不行。现在在决定处拒绝,并指出出路。 + + ⚠️ **约定仍然可以被推翻**:hosted 目标上显式声明 gcc 照常生效。 + +### 机器接口 + +- **⭐⭐ 两条命令进入 `--format json`,矩阵与四条 e2e 不再匹配任何一句话。** + + ``` + mcpp toolchain list --format json → mcpp.toolchain.list + mcpp why toolchain --target T --toolchain C --format json + → mcpp.why.toolchain + ``` + + 后者只解析不构建,给出五层、驱动器、三元组、C 库模型,以及 `status` 与 + `reason`。`reason` 是一个记号——`capability-pin` / `convention-unreplaced` / + `tier-planned` / `host-cannot-serve` / `os-mismatch` / `layer-requirement` / + `layer-ordering`,由 `mcpp.build.refusal` 在每一处拒绝的 `return` 之前记下。 + + ⚠️ **代价是当场量到的**:本次会话里我把 `cannot emit it` 改成 + `cannot be emitted by`,e2e 297 的断言随即变成空转——它仍然「通过」,只是不再 + 匹配任何东西。消息**仍然是承诺**(点名目标、规则与出路,e2e 照旧断言这一点); + 换掉的是**分类**:一个答案集有限的问题,不该用子串搜索来问。 + + ⚠️ 而**查询不能取代构建**。`llvm × x86_64-windows-gnu` 解析得完全正常,失败 + 在链接期的封闭性检查上——只查不建会把它报成绿。矩阵两样都做:分类取自 + `reason`,结论取自构建的退出码。 + + ⚠️ `why toolchain` 声明的 effects 故意偏宽(`network` / `write-global-cache` / + `exec-build-script`):它的答案来自与构建同一次的解析。客户端是在运行**之前** + 读这张表的,漏报一项就是一句不成立的安全承诺。 + +- **⭐ `refused` 曾与 `none` 同读数。** 一处没有记号的拒绝分支让矩阵写下 + `unsupported / none`——「拒绝了」和「没有理由」共用一个词,正是本次发布在修的 + 那个形状,重现在为发现它而造的机器里。现在无记号的拒绝报 `other`:一句可见的 + 承认,而不是并进邻近的理由。 + +- **⭐⭐ 新增 `ci-target-matrix.yml`,三个宿主 × 两层。** 第一层跑上述恒等式, + 不需要期望表;第二层用 `tests/matrix/scan.sh` 全表扫描 × 两种体系,与仓库里的 + `tests/matrix/expected.tsv` 比对。 + + ⚠️ **「跳过」必须是期望表说的,不是运行时发现的**:一格因为「今天这台机器没装 + 某载荷」而跳过,与「这个组合本就不支持」是两回事,前者会让矩阵在缺件机器上悄悄 + 变绿。比对脚本还先断言**扫描真的跑了**——一格没跑与全部通过,在退出码上没有 + 区别。 + + +### 测试 + +- **⭐⭐ e2e 295 是一条恒等式,不是一个阈值。** `mcpp build` 与 + `mcpp build --target <宿主自己的目标>` 描述同一次构建,链接线必须逐 flag 相同。 + 它不需要期望表、不取决于机器上装了什么,任何宿主都成立。 + + ⭐ **它把「修了一半」直接指出来**:差异 7 项 → 5 项 → 3 项 → 0,每一步指向下一 + 条通道。没有它,修完两条会看到「还是红」,而那句注释会让人以为已经找全。 + +- e2e 296:报告说 `c-abi (payload)` ⇒ 链接线必须含该载荷;说 `(graph)` ⇒ 不得含 + 宿主的 C 库。两向。 +- e2e 297:能力 pin 不可被推翻,且约定 pin 仍可被推翻——只断言前一半时,一个把 + 所有声明都拒掉的守卫同样能通过。 + ## [2026.8.25.2] — 2026-08-25 +一个谓词族的收尾。`2026.8.25.1` 修了其中四条,本次修余下三条,并补上让它们 +存活至今的两个 CI 空洞。完整分析见 +[`.agents/docs/2026-08-25-the-two-layer-predicate-family.md`](.agents/docs/2026-08-25-the-two-layer-predicate-family.md)。 + ### 修复 - **⭐⭐ 一个包声明「我供给哪一层」,让裸机目标丢掉了唯一能产出它的编译器。** @@ -36,21 +165,6 @@ `g++: unrecognized`。它在 2026.8.24.6 那轮红在**同一条**,所以既非 25.1 引入, 也非 25.1 修掉——是同一跨度里的遗留。 -### 测试 - -- e2e 292,两向断言:声明一层之后裸机目标仍解析到同一个编译器(先建立基线, - 否则分不清「修好了」和「这台机器没有 llvm」);以及宿主行**不得**顶掉项目自己 - 选的工具链——不加区分地永不取消 pin 也能让前一半通过,而那正是这个谓词当初要 - 防的替换。 - -## [2026.8.25.2] — 2026-08-25 - -一个谓词族的收尾。`2026.8.25.1` 修了其中四条,本次修余下三条,并补上让它们 -存活至今的两个 CI 空洞。完整分析见 -[`.agents/docs/2026-08-25-the-two-layer-predicate-family.md`](.agents/docs/2026-08-25-the-two-layer-predicate-family.md)。 - -### 修复 - - **⭐⭐ 图供给了 C 库,不等于目标平台的 SDK 不再需要。** ``` @@ -129,6 +243,11 @@ ### 测试 +- e2e 292,两向断言:声明一层之后裸机目标仍解析到同一个编译器(先建立基线, + 否则分不清「修好了」和「这台机器没有 llvm」);以及宿主行**不得**顶掉项目自己 + 选的工具链——不加区分地永不取消 pin 也能让前一半通过,而那正是这个谓词当初要 + 防的替换。 + - **e2e 292/293/294**,每条两向断言,且**都在修复前的二进制上验证过会失败**: 292 声明一层后裸机目标仍解析到同一编译器 + 宿主行不得顶掉项目自己的工具链; 293 拒绝跨 OS 的解析 + 四个正确交叉目标零误伤;294 列出图供给的目标 + 宿主 diff --git a/docs/11-machine-output.md b/docs/11-machine-output.md index 76a6c2ac..11b1efc6 100644 --- a/docs/11-machine-output.md +++ b/docs/11-machine-output.md @@ -229,3 +229,59 @@ mcpp cache list --format json ``` `data` is `{root, entries[]}`, the same document `--json` prints bare. + +### `mcpp.toolchain.list` — what is installed, and which targets this host serves + +``` +mcpp toolchain list --format json +``` + +`data` is `{host, toolchains[], targets[]}`. A toolchain is +`{family, version, default}` — plus `source: "system"` for a Visual Studio +installation, which is located on the machine rather than installed by mcpp. A +target row is `{target, note, toolchain, status, default}`, and `status` is one +of `installed` / `available` / `via dependency graph` / `planned`. + +### `mcpp.why.toolchain` — what a build for one pair would resolve to + +``` +mcpp why toolchain [--target ] [--toolchain ] --format json +``` + +It resolves and reports; it does not build. `data` is: + +| field | | +|---|---| +| `requested` | `{target, toolchain}` — what was asked for | +| `status` | `ok` or `refused` | +| `reason` | a refusal token, or `none` | +| `compiler` | `{family, version, driver}` — the driver that would run | +| `triple` | `{requested, toolchain, llvm}` | +| `cLibrary` | `{mode, path, origin}` — `mode` is `sysroot` / `payload-first` / `none`; `origin` is `payload` / `subos` / `host` / `none` | +| `layers[]` | the five target-side layers: `{layer, interface, impl, origin, subset}` | + +⭐ **`reason` is a token, not a sentence.** The refusal's message is still +written for a person and still names the target, the rule and the way out — but +a program classifying the outcome reads `reason`: + +| `reason` | | +|---|---| +| `tier-planned` | the row exists in the vocabulary; nothing is wired yet | +| `host-cannot-serve` | no payload here, and no dependency supplied the system | +| `capability-pin` | the row's toolchain is a capability, not a preference | +| `convention-unreplaced` | the convention was overridden and nothing replaced it | +| `os-mismatch` | the requested and resolved triples name different systems | +| `layer-requirement` | a package requires a layer the resolution did not give it | +| `layer-ordering` | the five layers do not stack | +| `other` | a refusal whose branch has not been given a token yet | + +⚠️ **Exit 0 whenever the question was answered, including "refused".** "Would +this build, and if not why" is answered successfully by "no, because the row's +pin is a capability". A non-zero exit means the query itself could not run. + +⚠️ **Its effects are broad on purpose.** `--protocol-version` lists `network`, +`write-global-cache` and `exec-build-script` for this command: the answer comes +from the same resolution a build performs, which may fetch packages, install a +payload and run a dependency's build program. A client gates on that table +*before* running anything, so an omission would be a safety claim that is not +true. diff --git a/docs/16-the-target-triple.md b/docs/16-the-target-triple.md index 96d3b146..abe52282 100644 --- a/docs/16-the-target-triple.md +++ b/docs/16-the-target-triple.md @@ -283,6 +283,47 @@ every target it was built with. Measured on one host, one source: clang does not reach into gcc's payload for a C library, and gcc does not reach into clang's. Each brings its own. +### Choosing the other compiler means supplying the other C library + +Each hosted row names a toolchain, and that name is a convention rather than a +capability: it answers *which payload supplies this target's C library*, so a +project that supplies one itself may name a different compiler. What it may not +do is name a different compiler and supply nothing. + +```toml +[toolchain] +default = "llvm@22.1.8" # x86_64-linux-musl's row names gcc +``` + +``` +$ mcpp build --target x86_64-linux-musl +error: target 'x86_64-linux-musl' takes its C library from the 'gcc@16.1.0' + payload, and 'llvm@22.1.8' has none here. +``` + +⚠️ **Before 2026.8.26.1 this ran the whole build and failed at the link**, with +`crtbeginT.o (bare name — the linker cannot resolve it)` — accurate about the +symptom and silent about the decision. clang is retargetable and brings no C +library, so it reached for a gcc installation; on a machine that happens to have +a system one, the same spelling for `x86_64-windows-gnu` reached +`/usr/lib/gcc/x86_64-w64-mingw32/…` instead, which is worse than failing. + +Adding the replacement is the whole difference: + +```toml +[dependencies] +openkal-llvm-runtime = "0.1.3" # → openkal-musl → openkal- +[toolchain] +default = "llvm@22.1.8" +``` + +That is [`examples/06-openkal-cross`](../examples/06-openkal-cross), and it is +why the refusal names openkal in its own text. + +⚠️ **A bare-metal row's toolchain is not a convention, and neither is +`x86_64-windows-musl`'s** — no gcc payload emits a PE with a musl C library, so +those rows cannot be overridden at all. See [chapter 03](03-toolchains.md). + ### And a dependency graph replaces that axis entirely The same three targets, with `openkal-musl` and `openkal-llvm-runtime` in the diff --git a/docs/zh/11-machine-output.md b/docs/zh/11-machine-output.md index b12ca2c2..6db0c91a 100644 --- a/docs/zh/11-machine-output.md +++ b/docs/zh/11-machine-output.md @@ -200,3 +200,56 @@ mcpp cache list --format json ``` `data` 是 `{root, entries[]}`,与 `--json` 裸打印的一致。 + +### `mcpp.toolchain.list` —— 装了什么,以及这台宿主服务哪些目标 + +``` +mcpp toolchain list --format json +``` + +`data` 是 `{host, toolchains[], targets[]}`。一个工具链是 +`{family, version, default}`;Visual Studio 另有 `source: "system"` —— 它是在 +机器上被找到的,不是 mcpp 装的。一行目标是 +`{target, note, toolchain, status, default}`,`status` 取 +`installed` / `available` / `via dependency graph` / `planned`。 + +### `mcpp.why.toolchain` —— 一对 (目标, 工具链) 会解析成什么 + +``` +mcpp why toolchain [--target ] [--toolchain ] --format json +``` + +它只解析并报告,不构建。`data`: + +| 字段 | | +|---|---| +| `requested` | `{target, toolchain}` —— 问的是什么 | +| `status` | `ok` 或 `refused` | +| `reason` | 拒绝的记号,或 `none` | +| `compiler` | `{family, version, driver}` —— 真正会跑的驱动器 | +| `triple` | `{requested, toolchain, llvm}` | +| `cLibrary` | `{mode, path, origin}`;`mode` 取 `sysroot` / `payload-first` / `none`,`origin` 取 `payload` / `subos` / `host` / `none` | +| `layers[]` | 目标侧五层:`{layer, interface, impl, origin, subset}` | + +⭐ **`reason` 是一个记号,不是一句话。** 拒绝的消息仍然写给人看,仍然点名目标、 +规则与出路;而一个要给结果分类的程序读 `reason`: + +| `reason` | | +|---|---| +| `tier-planned` | 词表里有这一行,还没有任何东西接线 | +| `host-cannot-serve` | 本机没有载荷,依赖也没有供给这个系统 | +| `capability-pin` | 这一行的工具链是能力陈述,不是偏好 | +| `convention-unreplaced` | 约定被推翻了,而没有任何东西接替它 | +| `os-mismatch` | 请求的与解析出的三元组指向不同的操作系统 | +| `layer-requirement` | 某个包要求的层,解析没有给出 | +| `layer-ordering` | 五层叠不起来 | +| `other` | 一处还没有被命名的拒绝分支 | + +⚠️ **只要问题被回答了就退 0,包括答案是「拒绝」。** 「它能不能构建,不能的话 +为什么」被「不能,因为这一行的 pin 是能力陈述」完整地回答了。非零退出的含义是 +这次查询本身没跑起来。 + +⚠️ **它声明的 effects 故意偏宽。** `--protocol-version` 为这条命令列出 +`network`、`write-global-cache` 与 `exec-build-script`:答案来自与构建同一次的 +解析,而那可能拉取包、安装载荷、并运行某个依赖的构建程序。客户端是在**运行之前** +读这张表来决定放不放行的,漏报一项就是一句不成立的安全承诺。 diff --git a/docs/zh/16-the-target-triple.md b/docs/zh/16-the-target-triple.md index 9dba5ce0..70656bdd 100644 --- a/docs/zh/16-the-target-triple.md +++ b/docs/zh/16-the-target-triple.md @@ -253,6 +253,44 @@ clang++ --target=x86_64-pc-windows-musl -c t.cpp clang 不会去 gcc 的载荷里取 C 库,gcc 也不会去 clang 的载荷里取。各带各的。 +### 换另一个编译器,就要把另一份 C 库一起换上 + +每一行有宿主的目标都写着一个工具链,而那个名字是**约定**不是**能力**:它回答的是 +*哪个载荷供给这个目标的 C 库*,所以一个自己供给 C 库的工程可以写另一个编译器。 +不能做的是写另一个编译器而什么都不供给。 + +```toml +[toolchain] +default = "llvm@22.1.8" # x86_64-linux-musl 这一行写的是 gcc +``` + +``` +$ mcpp build --target x86_64-linux-musl +error: target 'x86_64-linux-musl' takes its C library from the 'gcc@16.1.0' + payload, and 'llvm@22.1.8' has none here. +``` + +⚠️ **2026.8.26.1 之前这会把整个构建跑完,然后在链接上失败**,报 +`crtbeginT.o (bare name — the linker cannot resolve it)`——对症状准确,对决定沉默。 +clang 是可重定向的,自己不带 C 库,于是去够一份 gcc 安装;在恰好装了系统 mingw 的 +机器上,同样写法用于 `x86_64-windows-gnu` 够到的是 +`/usr/lib/gcc/x86_64-w64-mingw32/…`,那比失败更糟。 + +补上替代者,就是全部的差别: + +```toml +[dependencies] +openkal-llvm-runtime = "0.1.3" # → openkal-musl → openkal- +[toolchain] +default = "llvm@22.1.8" +``` + +这就是 [`examples/06-openkal-cross`](../../examples/06-openkal-cross),也是那句 +拒绝里为什么点名 openkal。 + +⚠️ **裸机行与 `x86_64-windows-musl` 行的工具链不是约定**,根本不可被推翻—— +见[第 03 章](03-toolchains.md)。 + ### 而依赖图会整个替换这一轴 同样三个目标,图里有 `openkal-musl` 与 `openkal-llvm-runtime` —— 同法实测: diff --git a/mcpp.toml b/mcpp.toml index f9127fe6..a16bdb54 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.8.25.2" +version = "2026.8.26.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/flags.cppm b/src/build/flags.cppm index baa5b58e..2ba8b83c 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -613,7 +613,28 @@ CompileFlags compute_flags(const BuildPlan& plan) { // Linker flags that cfg normally provides. The payload C-runtime // flags (-B/-L/loader) are appended via payload_ld below. link_toolchain_flags = crossTarget + " --no-default-config"; - if (!crossTarget.empty()) { + // ⚠️⚠️ THE CONDITION IS WHETHER THE GRAPH SUPPLIES THE C LIBRARY, AND + // FOR A LONG TIME IT ASKED WHETHER `--target` HAD BEEN TYPED. + // + // `crossTarget` is the string `--target=`. It is non-empty + // for ANY named target, including a project that names the host's own + // and depends on nothing — and such a project's system comes from the + // payload, not from a graph. Measured 2026-08-26, same machine, same + // compiler, same target, differing only in whether it was spelled out: + // + // $ mcpp build → ELF 64-bit LSB pie exec + // $ mcpp build --target x86_64-linux-gnu → hermetic link check failed + // + // The explicit spelling lost `-stdlib=libc++`, `--rtlib=compiler-rt`, + // `--unwindlib=libunwind` and every reference to the installed + // `xim:glibc` — `-B`, `-L` and the loader — so clang fell back to its + // defaults and the startup objects resolved out of /lib. + // + // ⭐ `cAbi.prebuilt()` IS THE QUESTION THE COMMENT BELOW ALREADY ASKED, + // and it is the same predicate 2026.8.25.1 moved three other decisions + // onto. e2e 295 states the invariant as an identity: naming the host's + // own target changes nothing. + if (!plan.targetSide.cAbi.prebuilt()) { // ⭐⭐ THE TARGET SIDE COMES FROM THE GRAPH, SO THE HOST'S MODEL // CONTRIBUTES NOTHING — THE SAME REPLACEMENT `stdModuleFlags` // ALREADY MAKES ON THE COMPILE SIDE. @@ -1209,8 +1230,17 @@ CompileFlags compute_flags(const BuildPlan& plan) { // the C-runtime group reaches the link line through TWO channels, and a // reader who fixed one saw the identical error and could reasonably // conclude the fix had not worked. + // + // ⭐⭐ AND THE CONDITION IS THE SAME ONE, MADE CORRECT THE SAME WAY. This + // asked `crossTargetFlag.empty()` — "was `--target` typed" — where it meant + // "does the graph supply the C library". `cAbi.prebuilt()` is that + // question. The comment above says the group reaches the link line through + // two channels; it is the same substitution on both, and fixing only the + // first left `-B`, `-L` and the loader missing while + // `-stdlib=libc++`/`--rtlib`/`--unwindlib` had already come back — the + // partial state e2e 295 reported and this line completes. if (isClangWithCfg - && plan.toolchain.crossTargetFlag.empty() + && plan.targetSide.cAbi.prebuilt() && lm.mode == mcpp::toolchain::CLibMode::PayloadFirst) payload_ld = lm.link_flags(ninjaEsc); // GCC: replace the payload's patched `*link:` with the pristine one, so @@ -1262,11 +1292,32 @@ CompileFlags compute_flags(const BuildPlan& plan) { std::string mingw_stdexp; if (caps.stdlib_id == "libstdc++") mingw_stdexp = " -lstdc++exp"; - f.ld = std::format("{}{}{}{}", link_intent_ld, user_ldflags, - mingw_stdexp, link_extra); + // ⭐⭐ `crossTarget`, BECAUSE THE PARAGRAPH ABOVE STATES A PRECONDITION + // THE CONDITION DOES NOT CHECK: "`x86_64-w64-mingw32-g++` needs no + // `--target` because it HAS no other". + // + // `is_mingw_target` answers "is the TARGET mingw", and that is true for + // a retargetable clang aimed at `x86_64-w64-windows-gnu` — which does + // have another target, and whose link line must say which. Measured + // 2026-08-26 on Linux, llvm@22.1.8, `--target x86_64-windows-gnu`: + // + // ldflags = ← empty + // ld.lld: error: obj/main.o: unknown file type + // + // The object was a correct `Intel amd64 COFF`. Without `--target=` the + // driver linked as the host, so `-fuse-ld=lld` selected the ELF flavour + // — the identical sentence the comment beside `crossTarget` records for + // a Mach-O cross, reached through this branch instead. + // + // ⭐ Not a new condition but the string that names the machine: a mingw + // g++ has an empty `crossTargetFlag`, so this is the identity for the + // case the paragraph was written about, and the fix for the other one. + f.ld = std::format("{}{}{}{}{}", crossTarget, link_intent_ld, + user_ldflags, mingw_stdexp, link_extra); // `-lstdc++exp` is named explicitly, so swapping g++ for gcc would not // drop it — the C line has to leave it out. - f.ldC = std::format("{}{}{}", link_intent_ld, user_ldflags, link_extra); + f.ldC = std::format("{}{}{}{}", crossTarget, link_intent_ld, + user_ldflags, link_extra); return f; } @@ -1421,8 +1472,14 @@ CompileFlags compute_flags(const BuildPlan& plan) { // ld spellings that a Mach-O or PE linker does not have. A target whose // runtime comes from the graph gets its own answer to oversized // `std::atomic` from that graph. + // ⭐ THE THIRD CHANNEL, AND THE LAST ONE. `libatomic` is this HOST's, + // found in the payload's directories, so it belongs to exactly the + // group `link_toolchain_flags` and `payload_ld` belong to — and it + // asked the same wrong question they did. Fixing the first two left + // e2e 295 reporting a three-flag difference (`-latomic` and its + // `--push-state`/`--pop-state` pair), which is what sent a reader here. std::string atomic_ld = - plan.toolchain.crossTargetFlag.empty() + plan.targetSide.cAbi.prebuilt() ? atomic_link_flag(plan.toolchain.linkRuntimeDirs, !full_static.empty()) : std::string{}; diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 8459fbb0..e4faeb9e 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -18,6 +18,7 @@ export import mcpp.build.prepare_inputs; import std; import mcpp.targetside; import mcpp.diag; +import mcpp.build.refusal; import mcpp.home; import mcpp.platform.axis; import mcpp.libs.json; @@ -937,6 +938,15 @@ prepare_build(bool print_fingerprint, // Taken here rather than re-derived at the decision point, because the row // is read exactly once and both facts come out of that read. bool targetPinIsCapability = false; + // ⚠️ THE ROW'S PIN, KEPT EVEN WHEN THE PROJECT NAMED ITS OWN COMPILER — + // which is exactly when `targetPinCandidate` above is left empty. + // + // The candidate answers "should mcpp apply its convention"; this answers + // "what does the convention SAY", and the two differ precisely in the case + // that needs a diagnosis: a project that overrode the convention and has + // nothing supplying what the convention was there to supply. + std::string targetRowPin; + std::string targetRowName; // Whether the resolved toolchain spec names the machine's own Visual // Studio. Decided inside `resolve_target_toolchain`, read by // `host_tc_for_build_program`, which is why it is declared out here. @@ -1538,6 +1548,7 @@ prepare_build(bool print_fingerprint, overrides.target_triple)); } if (known && known->tier == "planned" && !hasToolchainOverride) { + refusal::record(refusal::Code::TierPlanned); return std::unexpected(std::format( "target '{}' is registered but not yet supported (planned) — " "no toolchain is published for it yet.\n" @@ -1683,10 +1694,67 @@ prepare_build(bool print_fingerprint, // whether the dependency graph supplies one instead. That is knowable // only after resolution, so the decision waits for // `resolve_target_toolchain` and only the candidate is kept here. + if (known && !known->pin.empty() && parsed + && !parsed->pin_is_capability()) { + targetRowPin = std::string(known->pin); + targetRowName = parsed->str(); + } if (known && !hasToolchainOverride && !known->pin.empty() && !tc_origin_is_user_explicit(tcOrigin)) { targetPinCandidate = std::string(known->pin); - targetPinIsCapability = parsed && parsed->is_freestanding(); + targetPinIsCapability = parsed && parsed->pin_is_capability(); + } + // ⚠️⚠️ A USER'S EXPLICIT TOOLCHAIN OVERRIDES A CONVENTION, NOT A + // CAPABILITY — AND UNTIL THIS LINE IT OVERRODE BOTH. + // + // The block above deliberately steps aside for an explicit + // `[toolchain] default`: a hosted row's pin says "this payload supplies + // the target's C library", and an author who names their own compiler + // has said they will supply it instead. A bare-metal row's pin says + // something the author cannot override — the table's own words: "the + // pin is llvm on every host because clang/lld are cross-compilers by + // construction". A host g++ does not emit riscv64 whatever anyone + // declares. + // + // ⚠️ Measured 2026-08-26: + // + // [toolchain] default = "gcc@16.1.0" + // $ mcpp build --target riscv64-none-elf + // g++: error: unrecognized argument in option '-mabi=lp64d' + // g++: note: valid arguments to '-mabi=' are: ms sysv + // + // — a message about an option, for a decision made here. Refusing at + // the decision costs one line; the alternative is a compiler complaining + // about flags the reader never wrote. + if (known && parsed && parsed->pin_is_capability() + && tc_origin_is_user_explicit(tcOrigin) && tcSpec.has_value()) { + auto declared = mcpp::toolchain::parse_toolchain_spec(*tcSpec); + if (declared && declared->family != mcpp::toolchain::Family::Llvm) { + // ⚠️ THE REASON TRAVELS WITH THE ROW. Both rows refuse for the + // same rule and NOT for the same reason, and one sentence + // covering both would be wrong about one of them: a PE+musl + // target is not bare metal, and a reader told it is stops + // reading. + std::string_view why = parsed->is_freestanding() + ? "A freestanding target has no per-host cross payload: " + "clang and lld are\n" + " cross-compilers by construction and gcc is not." + : "No gcc payload emits a PE with a musl C library — the " + "mingw payload emits\n" + " PE with the MinGW CRT, which is the separate " + "`-gnu` row."; + refusal::record(refusal::Code::CapabilityPin); + return std::unexpected(std::format( + "target '{}' cannot be emitted by '{}'.\n" + " {}\n" + " The row names llvm as a capability rather than as a " + "preference, so this\n" + " one line is not a convention you can override.\n" + " remove the `[toolchain]` line for this target, or set " + "it to `{}`.", + parsed->str(), *tcSpec, why, + known->pin.empty() ? std::string_view("llvm") : known->pin)); + } } if (known && known->defaultStatic && m->buildConfig.linkage.empty()) m->buildConfig.linkage = "static"; @@ -2282,7 +2350,41 @@ prepare_build(bool print_fingerprint, if (auto cfg3 = get_cfg(); cfg3) { auto ref = mcpp::xlings::paths::parse_xpkg_ref(want_sysroot); auto xl = mcpp::config::make_xlings_env(**cfg3); - if (auto dir = mcpp::xlings::paths::xpkg_payload(xl, ref)) { + // ⭐⭐ INSTALLED, NOT MERELY LOOKED UP — THE SAME CHANNEL + // THE ROW'S TOOLCHAIN PIN GOES THROUGH. + // + // The row names two things and only one of them used to + // be made to exist: `pin` went through + // `resolve_xpkg_path(…, autoInstall=true, …)` while + // `sysroot` was a pure lookup that returned nullopt and + // let the whole block below be skipped without a word. + // + // ⚠️ Measured 2026-08-26 in a clean environment (an empty + // home, so mcpp's registry starts fresh): + // + // Target riscv64-none-elf + // c-abi picolibc-riscv (…, prebuilt) + // error: 'stdio.h' file not found + // + // The report named the C library and the build could not + // find its headers. mcpp's own bare-metal CI installs it + // by hand, which is why no test ever saw this — every + // bare-metal e2e runs on a machine where the gap has + // already been papered over. + // + // ⚠️ OFFLINE AND `MCPP_NO_AUTO_INSTALL` ARE THE FETCHER'S + // DECISION, not re-derived here. One question, one place + // that answers it — asking it twice is the shape this + // whole release exists to remove. + mcpp::fetcher::Fetcher srFetcher(**cfg3); + mcpp::fetcher::InstallProgressHandler srProgress; + std::optional dir; + if (auto p = srFetcher.resolve_xpkg_path( + want_sysroot, /*autoInstall=*/true, &srProgress)) + dir = p->root; + else + dir = mcpp::xlings::paths::xpkg_payload(xl, ref); + if (dir) { if (auto spec = mcpp::freestanding::resolve(*want)) { const auto inc = *dir / "include" / std::string(spec->libdir); @@ -4968,6 +5070,62 @@ prepare_build(bool print_fingerprint, tcSpec = targetPinCandidate; tcOrigin = TcOrigin::TargetPin; } + // ⚠️⚠️ OVERRIDING THE CONVENTION IS ALLOWED; OVERRIDING IT AND SUPPLYING + // NOTHING IN ITS PLACE IS NOT, AND UNTIL THIS BLOCK IT LOOKED THE SAME. + // + // A hosted row's pin names the payload that supplies the target's C + // library. A project may name a different compiler — that is the escape + // hatch the whole convention/capability distinction exists to protect — + // and the ordinary reason to do so is that its dependency graph supplies + // the C library instead. `examples/06-openkal-cross` is exactly that: + // `llvm@22.1.8` plus `openkal-llvm-runtime`, and `graphSuppliesSystem` + // is true there. + // + // ⚠️ WITH NEITHER, THE BUILD USED TO RUN ANYWAY AND FAIL SOMEWHERE ELSE. + // Measured 2026-08-26 on Linux, `[toolchain] default = "llvm@22.1.8"` + // and no dependencies: + // + // --target x86_64-linux-musl + // hermetic link check failed … crtbeginT.o (bare name) + // --target x86_64-windows-gnu + // hermetic link check failed … + // /usr/lib/gcc/x86_64-w64-mingw32/13-win32/crtbegin.o (outside) + // + // Both are accurate about the symptom and silent about the decision: + // clang is retargetable and brings no C library, so it reached for a + // gcc installation — one that does not exist under the payload prefix + // in the first case, and that belongs to the HOST in the second. There + // is no llvm payload supplying either target's C library today. + // + // ⭐ THE REFUSAL IS DECIDED HERE BECAUSE ONLY HERE ARE BOTH HALVES + // KNOWN. The row is read a thousand lines earlier and the graph does + // not exist then; `host_can_serve` is family-agnostic and would answer + // "yes, some payload here produces it" — the same shape as the family + // this release is about, a predicate answering a question narrower than + // the one it is asked. + if (!targetRowPin.empty() && !graphSuppliesSystem + && tc_origin_is_user_explicit(tcOrigin) && tcSpec.has_value()) { + auto declared = mcpp::toolchain::parse_toolchain_spec(*tcSpec); + auto rowTc = mcpp::toolchain::parse_toolchain_spec(targetRowPin); + if (declared && rowTc && declared->family != rowTc->family) { + refusal::record(refusal::Code::ConventionUnreplaced); + return std::unexpected(std::format( + "target '{}' takes its C library from the '{}' payload, and " + "'{}' has none here.\n" + " The row's toolchain is a convention, so naming your " + "own compiler overrides it —\n" + " but the convention is what supplies this target's " + "headers and C library, and\n" + " nothing in the dependency graph supplies them " + "instead.\n" + " depend on a package that implements the target's C " + "library (openkal-musl and\n" + " openkal-llvm-runtime are the ones in the index), or " + "remove the `[toolchain]`\n" + " line so `{}` is used for this target.", + targetRowName, targetRowPin, *tcSpec, targetRowPin)); + } + } if (auto r = resolve_target_toolchain(); !r) return std::unexpected(r.error()); } @@ -6038,15 +6196,19 @@ prepare_build(bool print_fingerprint, in.cxxAbi = provider_of(tsd::CapLayer::CxxAbi); resolvedTargetSide = tsd::resolve(in); - if (auto why = tsd::check_layering(resolvedTargetSide)) + if (auto why = tsd::check_layering(resolvedTargetSide)) { + refusal::record(refusal::Code::LayerOrdering); return std::unexpected(*why); + } // ⚠️ REQUIREMENTS ARE CHECKED BEFORE ANYTHING IS COMPILED, WHICH IS THE // WHOLE POINT OF DECLARING THEM. The combination this rejects — a C++ // runtime configured for one compiler family being handed to another — // otherwise fails inside that runtime's own headers, in a message that // names a file the reader has never opened and no decision mcpp made. - if (auto why = tsd::check_requirements(resolvedTargetSide, requirements)) + if (auto why = tsd::check_requirements(resolvedTargetSide, requirements)) { + refusal::record(refusal::Code::LayerRequirement); return std::unexpected(*why); + } // ⚠️ A WARNING, NOT A REFUSAL. The graph decides the C library either // way, so the segment is ignored rather than violated and the artifact // is the same with or without it. Refusing was tried and broke every @@ -6060,8 +6222,10 @@ prepare_build(bool print_fingerprint, // does not produce this target; if the graph does not supply the // target's system either, then nothing does and the diagnosis stands. if (!unservedTargetDiagnosis.empty() - && !resolvedTargetSide.system_from_graph()) + && !resolvedTargetSide.system_from_graph()) { + refusal::record(refusal::Code::HostCannotServe); return std::unexpected(unservedTargetDiagnosis); + } // ⚠️ THE TARGET AND THE COMPILER ARE NOT BOUND TOGETHER, AND THE // TARGET ROW'S CONVENTION IS A FALLBACK RATHER THAN A RULE. @@ -6193,6 +6357,7 @@ prepare_build(bool print_fingerprint, resolvedTargetSide.llvmTriple, got ? got->os : "")); if (want && got && !want->os.empty() && !got->os.empty() && want->os != got->os) { + refusal::record(refusal::Code::OsMismatch); return std::unexpected(std::format( "target '{}' resolved to a toolchain for '{}'.\n" " Those are different operating systems, so nothing " diff --git a/src/build/refusal.cppm b/src/build/refusal.cppm new file mode 100644 index 00000000..928906f7 --- /dev/null +++ b/src/build/refusal.cppm @@ -0,0 +1,93 @@ +// mcpp.build.refusal — the machine-readable identity of a target refusal. +// +// ⭐⭐ WHY A CODE AND NOT THE SENTENCE. +// +// mcpp refuses a target for several distinct reasons, and every one of them +// leaves the same trace: `prepare_build` returns `std::unexpected()`. +// A caller that needs to know WHICH reason has, until now, had exactly one +// option — match the prose. `tests/matrix/scan.sh` did, and so did three e2e +// tests, and the cost was measured on 2026-08-26 within a single session: +// reworded `cannot emit it` to `cannot be emitted by`, and a test that had +// been asserting a refusal silently began asserting nothing. +// +// ⚠️ THE MESSAGE IS STILL A PROMISE. This does not replace it — a refusal that +// does not name the target, the reason and the way out is a defect whether or +// not it carries a code, and the e2e tests keep asserting exactly that. What +// the code replaces is *classification*: "did it refuse, and for which reason" +// is a question with a finite answer set, and a finite answer set should not be +// spelled as a substring search. +// +// ── Why a sink rather than a return type ─────────────────────────────────── +// +// `prepare_build` returns `std::expected` and it has +// dozens of error returns, of which these are six. Widening the error type +// would touch every consumer of `.error()` to express something only one of +// them reads. `mcpp::diag` settled the same trade-off the same way: a per-run +// sink, written where the fact is known and read where it is needed. +// +// ⚠️ SET IMMEDIATELY BEFORE THE `return`, NEVER EARLIER. A code set at the top +// of a branch that then does not refuse is worse than no code at all: it makes +// a successful build report a reason. + +export module mcpp.build.refusal; + +import std; + +export namespace mcpp::build::refusal { + +// ⭐ ONE CODE PER DECISION, and the decisions are the ones a reader of the +// target matrix has to tell apart. Adding a refusal branch means adding a code +// here — an unnamed branch reports `other`, which is a visible admission +// rather than a silent merge into a neighbouring reason. +enum class Code { + None, // no refusal + TierPlanned, // the row exists in the vocabulary, nothing is wired + HostCannotServe, // no payload here, and no graph supplied the system + CapabilityPin, // the row's toolchain is a capability, not a preference + ConventionUnreplaced, // the convention was overridden and nothing replaced it + OsMismatch, // requested and resolved triples name different systems + LayerRequirement, // a package requires a layer the resolution did not give it + LayerOrdering, // the five layers do not stack (check_layering) + Other, // a refusal that has not been given a code yet +}; + +constexpr std::string_view name(Code c) { + switch (c) { + case Code::None: return "none"; + case Code::TierPlanned: return "tier-planned"; + case Code::HostCannotServe: return "host-cannot-serve"; + case Code::CapabilityPin: return "capability-pin"; + case Code::ConventionUnreplaced: return "convention-unreplaced"; + case Code::OsMismatch: return "os-mismatch"; + case Code::LayerRequirement: return "layer-requirement"; + case Code::LayerOrdering: return "layer-ordering"; + case Code::Other: return "other"; + } + return "other"; +} + +// Written by `prepare_build`'s refusal sites; read by the machine-readable +// output layer. `record` is what a refusal site calls; `take` reads and clears, +// so a later successful run cannot inherit an earlier run's reason. +void record(Code c); +Code take(); + +} // namespace mcpp::build::refusal + +// ── implementation ────────────────────────────────────────────────────────── + +namespace mcpp::build::refusal { +namespace { +// ⚠️ `thread_local`: `prepare_build` recurses for nested host sub-builds, and +// those run on the calling thread — but a build program's own sub-build must +// not leave its refusal behind for the outer one. Same thread, so the sink is +// shared deliberately; the outer refusal is recorded last and wins, which is +// the one the user was asking about. +thread_local Code g_last = Code::None; +} // namespace + +void record(Code c) { g_last = c; } + +Code take() { Code c = g_last; g_last = Code::None; return c; } + +} // namespace mcpp::build::refusal diff --git a/src/cli.cppm b/src/cli.cppm index 5e3818b7..2ad20c05 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -390,6 +390,16 @@ int run(int argc, char** argv) { .subcommand(cl::App("why") .description("Explain how the toolchain / runtime / deps were resolved") .arg(cl::Arg("topic").help("toolchain | runtime | deps (default: all)")) + // ⭐ `--target` / `--toolchain` make this a QUERY rather than a + // report on the current directory's default: "what would a build + // for THIS pair resolve to" is the question the target matrix asks + // once per cell, and it builds nothing. + .option(cl::Option("target").takes_value() + .help("Ask about this target instead of the project's default")) + .option(cl::Option("toolchain").takes_value() + .help("Ask about this toolchain, e.g. llvm@22.1.8")) + .option(cl::Option("format").takes_value().value_name("json") + .help("Machine-readable output (enveloped; see docs/11-machine-output.md)")) .action(wrap_rc(cmd_why))) .subcommand(cl::App("resolve") .description("Re-resolve the build plan and explain it") @@ -501,7 +511,9 @@ int run(int argc, char** argv) { // ─── resource management ─────────────────────────────────────── .subcommand(cl::App("toolchain") .description("Install / list / select / remove C++ toolchains") - .subcommand(cl::App("list").description("List installed toolchains")) + .subcommand(cl::App("list").description("List installed toolchains") + .option(cl::Option("format").takes_value().value_name("json") + .help("Machine-readable output (enveloped; see docs/11-machine-output.md)"))) .subcommand(cl::App("install") .description("Install a toolchain via mcpp's xlings") // Both `mcpp toolchain install gcc 16.1.0` and `mcpp toolchain @@ -756,9 +768,25 @@ int run(int argc, char** argv) { auto protocol_commands = [] { using mcpp::wire::Effect; return std::vector{ - {"self env", {Effect::InitMcppHome}}, - {"xpkg parse", {}}, - {"cache list", {}}, + {"self env", {Effect::InitMcppHome}}, + {"xpkg parse", {}}, + {"cache list", {}}, + {"toolchain list", {Effect::InitMcppHome}}, + // ⚠️⚠️ `why toolchain` DECLARES MORE THAN IT USUALLY DOES, AND THAT + // IS THE CORRECT DIRECTION. + // + // It answers a question without building, which reads like a pure + // query — but the answer comes from `prepare_build`, which resolves + // the dependency graph. That can fetch packages, install a toolchain + // payload, write the global cache, and run a dependency's build + // program during tool provisioning. + // + // A client gates on this table BEFORE running anything, so an + // omission here is a safety claim that is not true. Over-declaring + // costs one prompt; under-declaring costs the gate. + {"why toolchain", {Effect::InitMcppHome, Effect::ReadProject, + Effect::Network, Effect::WriteGlobalCache, + Effect::ExecBuildScript}}, }; }; diff --git a/src/cli/cmd_self.cppm b/src/cli/cmd_self.cppm index 2135dae1..13988b88 100644 --- a/src/cli/cmd_self.cppm +++ b/src/cli/cmd_self.cppm @@ -85,6 +85,28 @@ export int cmd_doctor(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) { } export int cmd_why(const mcpplibs::cmdline::ParsedArgs& parsed) { + // ⚠️ `--format` IS ONLY DEFINED FOR THE `toolchain` TOPIC, and saying so + // beats emitting an envelope whose `data` silently omits what was asked + // for. `runtime` and `deps` have their own shapes and no consumer yet; + // inventing one here would publish a contract nobody has read. + if (auto f = parsed.value("format")) { + auto fmt = mcpp::wire::parse_format(*f); + if (!fmt) { + std::println(stderr, "error: {}", + mcpp::wire::unsupported_format(*f)); + return 2; + } + const std::string topic = parsed.positional(0); + if (!topic.empty() && topic != "toolchain") { + std::println(stderr, + "error: --format json is defined for `mcpp why toolchain`; " + "'{}' has no machine-readable shape yet", topic); + return 2; + } + return mcpp::doctor::why_toolchain_json( + parsed.option_or_empty("target").value(), + parsed.option_or_empty("toolchain").value()); + } return mcpp::doctor::why_report(parsed.positional(0)); } diff --git a/src/cli/cmd_toolchain.cppm b/src/cli/cmd_toolchain.cppm index a6b42df0..4cd57023 100644 --- a/src/cli/cmd_toolchain.cppm +++ b/src/cli/cmd_toolchain.cppm @@ -13,6 +13,7 @@ import mcpp.config; import mcpp.fetcher.progress; import mcpp.toolchain.lifecycle; import mcpp.ui; +import mcpp.wire; namespace mcpp::cli { @@ -36,8 +37,18 @@ export int cmd_toolchain(const mcpplibs::cmdline::ParsedArgs& parsed) { std::string targetArg; if (auto t = sub_parsed.value("target")) targetArg = *t; - if (subname == "list") - return mcpp::toolchain::toolchain_list(*cfg); + if (subname == "list") { + bool json = false; + if (auto f = sub_parsed.value("format")) { + if (!mcpp::wire::parse_format(*f)) { + std::println(stderr, "error: {}", + mcpp::wire::unsupported_format(*f)); + return 2; + } + json = true; + } + return mcpp::toolchain::toolchain_list(*cfg, json); + } if (subname == "install") return mcpp::toolchain::toolchain_install( *cfg, sub_parsed.positional(0), sub_parsed.positional(1), targetArg); diff --git a/src/doctor.cppm b/src/doctor.cppm index 96e62d81..ab01335f 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -15,6 +15,9 @@ import mcpp.source_kind; import mcpp.manifest; import mcpp.bmi_cache.maintenance; import mcpp.build.prepare; +import mcpp.build.refusal; +import mcpp.targetside; +import mcpp.wire; import mcpp.build.plan; import mcpp.build.runtime_validation; import mcpp.config; @@ -26,12 +29,14 @@ import mcpp.home; import mcpp.libs.json; import mcpp.platform; import mcpp.platform.process; +import mcpp.platform.env; import mcpp.platform.elf_runtime; import mcpp.pm.index_refresh; // staleness_note for `mcpp why deps` import mcpp.project; import mcpp.toolchain.detect; import mcpp.toolchain.msvc; import mcpp.toolchain.registry; +import mcpp.toolchain.linkmodel; import mcpp.toolchain.stdmod; import mcpp.toolchain.abi; import mcpp.ui; @@ -769,6 +774,173 @@ int print_stored_runtime_resolution() { } // `mcpp why [topic]` / `mcpp resolve --explain`. + +// ⭐⭐ THE SAME RESOLUTION `why toolchain` PRINTS, AS DATA. +// +// A build for one (target, toolchain) pair resolves five layers, a driver, a +// triple and a sysroot, and then either proceeds or refuses. Every one of those +// is a finite value, and until this function the only way to read them from +// outside was to match the sentences mcpp prints for a person. The cost of that +// was measured on 2026-08-26: rewording one refusal turned an e2e assertion +// into a no-op, silently, in the same session that wrote it. +// +// ⚠️ EXIT 0 WHENEVER THE QUESTION WAS ANSWERED, INCLUDING WHEN THE ANSWER IS +// "REFUSED". This is a query: "would this build, and if not why" is answered +// successfully by "no, because the row's pin is a capability". Overloading the +// exit code would give a client exactly the ambiguity the envelope exists to +// remove — and `data.status` is unambiguous. A non-zero exit here means the +// query itself could not run. +// +// It builds nothing. `prepare_build` stops before any compile, which is also +// why the target matrix can afford to ask it for every cell. +export int why_toolchain_json(std::string_view target, std::string_view tcSpec) { + mcpp::build::BuildOverrides ov; + ov.target_triple = std::string(target); + // ⚠️ `MCPP_TOOLCHAIN`, not a field on the overrides — that is the channel + // `--toolchain` already uses, and `prepare_build` reads it as a + // user-explicit declaration. Adding a second way in would give the two + // spellings different provenance, and provenance is exactly what the + // convention/capability distinction turns on. + if (!tcSpec.empty()) + mcpp::platform::env::set("MCPP_TOOLCHAIN", std::string(tcSpec)); + + // ⚠️⚠️ STDOUT BELONGS TO THE ENVELOPE, AND `prepare_build` NARRATES. + // + // `Resolving toolchain` / `Resolved …` / `Target … → …` are status lines + // for a person, and they go to stdout. A client is told to detect the + // protocol BY PARSING stdout — see the module comment in mcpp.wire — so + // three lines of prose ahead of the JSON is not a cosmetic problem, it is + // the envelope failing to arrive. Measured on the first run of this + // function: `json.tool` refused at `line 1 column 4`. + // + // Restored afterwards rather than left set: this is a library function and + // the process may go on to do something that should narrate. + const bool wasQuiet = mcpp::ui::is_quiet(); + mcpp::ui::set_quiet(true); + struct QuietGuard { + bool prev; + ~QuietGuard() { mcpp::ui::set_quiet(prev); } + } quietGuard{wasQuiet}; + + nlohmann::json data; + data["requested"] = { + {"target", std::string(target)}, + {"toolchain", std::string(tcSpec)}, + }; + + auto ctx = mcpp::build::prepare_build(/*print_fingerprint=*/false, + /*includeDevDeps=*/false, {}, ov); + std::vector diags; + if (!ctx) { + // ⚠️ `take()` AFTER the call and only here: a site that recorded a code + // and then did not refuse would otherwise leak it into the next query. + // ⚠️⚠️ `None` MEANS "NOTHING RECORDED", AND HERE THAT IS NOT "no reason" + // — the build demonstrably refused. Reporting `none` beside + // `refused` gives one word two meanings, which is the defect this + // whole release is about, reintroduced in the machinery built to + // detect it. Measured: `gcc` + `openkal-llvm-runtime` refused through + // a site that had no code, and the matrix recorded + // `unsupported / none`. + // + // ⭐ `other` is a visible admission: a refusal exists and its branch + // has not been named yet. + auto code = mcpp::build::refusal::take(); + if (code == mcpp::build::refusal::Code::None) + code = mcpp::build::refusal::Code::Other; + data["status"] = "refused"; + data["reason"] = std::string(mcpp::build::refusal::name(code)); + diags.push_back({ + .code = std::format("target.{}", + mcpp::build::refusal::name(code)), + .severity = mcpp::wire::Severity::Error, + .message = ctx.error(), + }); + mcpp::wire::emit({ + .kind = "mcpp.why.toolchain", + .effects = { mcpp::wire::Effect::ReadProject }, + .data = data, + .diagnostics = diags, + }); + return 0; + } + (void)mcpp::build::refusal::take(); + + const auto& tc = ctx->tc; + const auto& ts = ctx->plan.targetSide; + + data["status"] = "ok"; + data["reason"] = "none"; + data["compiler"] = { + {"family", std::string(tc.compiler_name())}, + {"version", tc.version}, + {"driver", tc.binaryPath.string()}, + }; + data["triple"] = { + {"requested", std::string(target)}, + {"toolchain", tc.targetTriple}, + {"llvm", ts.llvmTriple}, + }; + + // ⭐⭐ THE C LIBRARY MODEL, NOT `tc.sysroot`. + // + // `tc.sysroot` is what the driver reports for `-print-sysroot`, and it is + // frequently empty for a toolchain that nonetheless receives an explicit + // `--sysroot` on every command line. The first version of this reported it + // and disagreed with the build: the query said `none` while build.ninja + // carried `--sysroot=…/registry/subos/default`. + // + // `resolve_link_model` is the function the flag emitter itself calls, and + // it is a pure function of the toolchain — so asking it here is asking the + // same question of the same authority rather than re-deriving an answer + // that can drift from the one the build uses. + const auto lm = mcpp::toolchain::resolve_link_model(tc); + auto path_origin = [](const std::filesystem::path& p) -> std::string_view { + if (p.empty()) return "none"; + const auto sp = p.generic_string(); + if (sp.find("registry/data/xpkgs") != std::string::npos) return "payload"; + if (sp.find("registry/subos") != std::string::npos) return "subos"; + return "host"; + }; + const auto& srPath = + lm.mode == mcpp::toolchain::CLibMode::Sysroot ? lm.sysroot + : lm.crtDir; + data["cLibrary"] = { + {"mode", lm.mode == mcpp::toolchain::CLibMode::Sysroot ? "sysroot" + : lm.mode == mcpp::toolchain::CLibMode::PayloadFirst ? "payload-first" + : "none"}, + {"path", srPath.string()}, + {"origin", std::string(path_origin(srPath))}, + }; + + auto layer = [](std::string_view label, + const mcpp::targetside::Layer& l) { + return nlohmann::json{ + {"layer", std::string(label)}, + {"interface", l.interfaceName}, + {"impl", l.impl}, + {"origin", l.absent() + ? std::string("none") + : std::string(mcpp::targetside::origin_name(l.origin))}, + {"subset", l.subset}, + }; + }; + data["layers"] = nlohmann::json::array({ + layer("compiler", ts.compiler), + layer("compiler-runtime", ts.compilerRuntime), + layer("kernel-abi", ts.kernelAbi), + layer("c-abi", ts.cAbi), + layer("c++-abi", ts.cxx), + }); + + mcpp::wire::emit({ + .kind = "mcpp.why.toolchain", + .effects = { mcpp::wire::Effect::ReadProject }, + .data = data, + .diagnostics = diags, + }); + return 0; +} + export int why_report(const std::string& topic) { const bool all = topic.empty() || topic == "all"; diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index 753f7525..916a4ad4 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -21,6 +21,8 @@ import mcpp.toolchain.registry; import mcpp.toolchain.triple; import mcpp.toolchain.post_install; import mcpp.ui; +import mcpp.wire; +import mcpp.libs.json; import mcpp.log; import mcpp.platform.xlings; import mcpp.platform.xlings.runtime_selection; @@ -423,7 +425,21 @@ bool version_greater(const std::string& a, const std::string& b) { return b < a; // stable tie-break for non-numeric tails } -export int toolchain_list(const mcpp::config::GlobalConfig& cfg) { +// ⭐⭐ `json` SUPPRESSES EVERY HUMAN LINE AND EMITS THE SAME TWO TABLES AS DATA. +// +// The two tables here — installed toolchains, and target rows with their status +// — are what `tests/matrix/scan.sh` and four e2e tests need to know before they +// can ask anything. All of them read them by parsing this function's aligned +// columns with awk, which makes a column width a load-bearing part of the test +// suite. Two versions of one test disagreed on which column held the version, +// and the one that read `$NF` picked up `(default)` — a value that appears on +// exactly the row most likely to be chosen. +// +// ⚠️ ONE SOURCE, TWO RENDERINGS. The rows are built once above and rendered +// either way at the bottom; a second enumeration for the machine path is how +// the two come to disagree about what is installed. +export int toolchain_list(const mcpp::config::GlobalConfig& cfg, + bool json = false) { auto pkgsDir = cfg.xlingsHome() / "data" / "xpkgs"; auto effective = effective_default_toolchain(cfg); @@ -440,6 +456,7 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg) { defTarget = *t; } auto hostT = mcpp::toolchain::triple::host_triple(); + nlohmann::json jsonToolchains = nlohmann::json::array(); // ── enumerate installed payloads → (identity, version, frontend) ──── struct Payload { @@ -492,16 +509,22 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg) { std::println("(no toolchains installed — run `mcpp toolchain install gcc 16.1.0` " "or just `mcpp build` to auto-install the default)"); } else { - std::println("Toolchains:"); + if (!json) std::println("Toolchains:"); for (auto& f : famvers) { bool isDefault = defSpec && mcpp::toolchain::spec_matches_payload(*defSpec, { f.family, {} }, f.version); + jsonToolchains.push_back({ + {"family", std::string(mcpp::toolchain::family_name(f.family))}, + {"version", f.version}, + {"default", isDefault}, + }); + if (json) continue; std::println(" {:<3}{:<22}{}", isDefault ? "*" : "", std::format("{} {}", mcpp::toolchain::family_name(f.family), f.version), isDefault ? " (default)" : ""); } - if (effective.fromProject) { + if (!json && effective.fromProject) { std::println(" (* = effective toolchain from project mcpp.toml " "[toolchain]; global default is '{}')", cfg.defaultToolchain.empty() ? "" @@ -512,7 +535,21 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg) { // ─── System section (Windows: detected MSVC) ──────────────────────── // MSVC is never in xpkgs — it's located on the machine. Show it so // `toolchain list` reflects everything `toolchain default` accepts. - if (mcpp::platform::is_windows) { + // + // ⚠️ It joins `toolchains` on the machine path rather than getting a + // section of its own: to a client, "a compiler I can select" is one list, + // and where mcpp found it is a property of the entry, not a reason for a + // second array. + if (mcpp::platform::is_windows && json) { + if (auto inst = mcpp::toolchain::msvc::detect_installation()) + jsonToolchains.push_back({ + {"family", "msvc"}, + {"version", inst->display_version()}, + {"default", defSpec + && defSpec->family == mcpp::toolchain::Family::Msvc}, + {"source", "system"}, + }); + } else if (mcpp::platform::is_windows) { if (auto inst = mcpp::toolchain::msvc::detect_installation()) { bool isDefault = defSpec && defSpec->family == mcpp::toolchain::Family::Msvc; @@ -645,6 +682,27 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg) { return a.target < b.target; }); + nlohmann::json jsonTargets = nlohmann::json::array(); + for (auto& r : targetRows) + jsonTargets.push_back({ + {"target", r.target}, + {"note", r.note}, + {"toolchain", r.toolchain}, + {"status", r.status}, + {"default", r.isDefault}, + }); + if (json) { + mcpp::wire::emit({ + .kind = "mcpp.toolchain.list", + .effects = {}, + .data = nlohmann::json{ + {"host", mcpp::toolchain::triple::host_triple().str()}, + {"toolchains", jsonToolchains}, + {"targets", jsonTargets}, + }, + }); + return 0; + } if (!targetRows.empty()) { std::println(""); std::println("Targets:"); diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index e81953e5..3a7364f9 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -425,7 +425,23 @@ XimToolchainPackage to_xim_package(const ToolchainSpec& spec) { // Family::Gcc — the target decides the payload. const auto& t = spec.target; - if (t.is_musl()) { + // ⚠️⚠️ `&& t.os == "linux"` — AND THE PARAGRAPH BELOW ALREADY SAID SO. + // + // "Canonical linux-musl triples coincide with the GNU tool spelling" is a + // statement about linux-musl, and the condition asked only whether the C + // library is musl. `x86_64-windows-musl` was added later and walked in. + // Measured 2026-08-26 on a Linux x86_64 host: + // + // $ mcpp build --target x86_64-windows-musl ([toolchain] gcc@16.1.0) + // fetcher: resolve: target='xim:musl-gcc@16.1.0' + // error: toolchain payload 'xim:musl-gcc@16.1.0' has no known C++ + // frontend in …/xim-x-musl-gcc/16.1.0/bin + // + // `native` read `t.arch == host_arch` and got true, so a PE target resolved + // the host's ELF Linux payload. The message names a missing frontend, which + // is true of that payload and says nothing about the decision that reached + // for it — the same shape as the rest of this release. + if (t.is_musl() && t.os == "linux") { // Same target, two payload shapes: the host-native `musl-gcc` package // (XLINGS_RES picks the host-matching asset) when target arch == host // arch, else the triple-named cross package. Canonical linux-musl diff --git a/src/toolchain/triple.cppm b/src/toolchain/triple.cppm index a3c91110..d009e61a 100644 --- a/src/toolchain/triple.cppm +++ b/src/toolchain/triple.cppm @@ -131,6 +131,28 @@ struct Triple { // re-derives it from `os == "none"` and drifts. bool is_freestanding() const { return os == "none"; } + // ⭐⭐ WHETHER THIS ROW'S TOOLCHAIN PIN IS A CAPABILITY RATHER THAN A + // CONVENTION — the distinction that decides whether an author may override + // it. + // + // A hosted row's pin answers "which payload supplies this target's C + // library", and an author who supplies one may name any compiler. These + // rows answer a different question, and the answer does not depend on who + // supplies what: + // + // freestanding no per-host cross payload exists at all; clang and lld + // are cross-compilers by construction and gcc is not. + // PE + musl no gcc payload emits a PE with a musl C library. The + // mingw payload emits PE with the MinGW CRT, which is the + // separate `-gnu` row; there is no third gcc. + // + // ⚠️ SPELLED HERE RATHER THAN AT EACH DECISION, because the first version + // said `is_freestanding()` at two of them and `x86_64-windows-musl` — a row + // added later — was a convention at both. Measured: declaring gcc for it + // resolved the host's Linux musl payload and reported a missing C++ + // frontend. + bool pin_is_capability() const { return is_freestanding() || (is_pe() && is_musl()); } + // cfg() `family` dimension: unix | windows. std::string family() const { if (os == "windows") return "windows"; diff --git a/src/version.cppm b/src/version.cppm index c55c4771..62c6d062 100644 --- a/src/version.cppm +++ b/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.8.25.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.8.26.1"; } // namespace mcpp diff --git a/src/wire.cppm b/src/wire.cppm index f363f6d2..0669e351 100644 --- a/src/wire.cppm +++ b/src/wire.cppm @@ -67,10 +67,18 @@ inline constexpr int kEnvelopeVersion = 1; // from what comes back. struct KindVersion { std::string_view kind; int version; }; -inline constexpr std::array kKinds{{ - {"mcpp.env", 1}, - {"mcpp.xpkg", 1}, - {"mcpp.cache", 1}, +inline constexpr std::array kKinds{{ + {"mcpp.env", 1}, + {"mcpp.xpkg", 1}, + {"mcpp.cache", 1}, + // `mcpp why toolchain --format json`: what a build for one (target, + // toolchain) pair WOULD resolve to, without building it — the five layers, + // the driver, the triple, the C-library model, and either `ok` or a refusal + // whose `reason` is a token from mcpp.build.refusal. + {"mcpp.why.toolchain", 1}, + // `mcpp toolchain list --format json`: which toolchains are installed and + // which target rows this host serves, with their status. + {"mcpp.toolchain.list", 1}, }}; // What running a command does, beyond writing to stdout. diff --git a/tests/e2e/295_naming_the_host_target_changes_nothing.sh b/tests/e2e/295_naming_the_host_target_changes_nothing.sh new file mode 100755 index 00000000..1a31c1d5 --- /dev/null +++ b/tests/e2e/295_naming_the_host_target_changes_nothing.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell jq +# Spelling out the target this machine already builds for changes nothing. +# +# ⭐⭐ THIS IS AN IDENTITY, NOT A THRESHOLD. `mcpp build` and +# `mcpp build --target ` describe the same build for the +# same machine, so the link line either is the same string or something decided +# on the spelling rather than on the build. +# +# ⚠️ MEASURED 2026-08-26, ON THE MACHINE THIS WAS WRITTEN ON: +# +# $ mcpp build → ELF 64-bit LSB pie executable +# $ mcpp build --target x86_64-linux-gnu → hermetic link check failed +# +# with llvm. The two link lines differed by five flags: +# +# -stdlib=libc++ --rtlib=compiler-rt --unwindlib=libunwind +# -Wl,--push-state,--as-needed -latomic +# +# ⭐ THE CAUSE IS ONE CONDITION. `flags.cppm` asks `!crossTarget.empty()` — is +# there a `--target=` on the command line — and its own comment says what it +# meant to ask: "THE TARGET SIDE COMES FROM THE GRAPH". Those are different +# questions, and a project that names its host target while using no +# dependencies at all answers yes to the first and no to the second. +# +# ⭐ NO EXPECTED VALUES. This asserts a relation between two runs, so it holds +# on every host and needs no table to compare against — which is why it is the +# first thing to land. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/src" +cd "$work" +printf '#include \nint main() { std::printf("ok\\n"); }\n' > src/main.cpp + +# The target this host builds for when nothing is said. Taken from mcpp itself +# rather than assembled from `uname`: the point is to name the target mcpp would +# have chosen, and deriving it a second way would test the derivation instead. +host_target() { + # ⭐ THE MACHINE INTERFACE NAMES IT DIRECTLY. `data.host` is the host + # triple, which is what "this machine's own target" means — no column to + # locate and no note to recognise. + # + # ⚠️ THE FIRST TWO DRAFTS BOTH READ THE HUMAN TABLE AND BOTH READ IT WRONG. + # One took the `*`, which marks the default toolchain PAIR and on this + # machine sat on `aarch64-linux-musl` — a CROSS target, so the identity + # compared two different builds and skipped. The next took the `host` note, + # which is right today and is still a column position in a table formatted + # for people. + "$MCPP" toolchain list --format json 2>/dev/null \ + | jq -r '.data.host // empty' +} + +ldflags_of() { # extra args… → the ldflags line, or nothing + rm -rf target + "$MCPP" build "$@" >/dev/null 2>&1 || true + local f; f="$(find target -name build.ninja 2>/dev/null | head -1)" + # ⚠️ AN EXPLICIT `return 0`, BECAUSE THIS FUNCTION IS ALLOWED TO FIND + # NOTHING. The first draft ended on `[ -n "$f" ] && grep …`, whose exit + # status under `set -e` is the test's — so the one case this test exists to + # examine, a build that produced no link line, killed the script before it + # could say so. It exited 1 with no output at all. + if [ -n "$f" ]; then + grep -m1 '^ldflags' "$f" || true + fi + return 0 +} + +fail=0 +checked=0 + +for tc in gcc llvm; do + # ⚠️ THE TOOLCHAIN IS NAMED, because the defect lives on one of them and a + # run that silently used the other would pass while proving nothing. + # ⭐ ONE FIELD, NOT A COLUMN. Earlier drafts read `$NF` — which is + # `(default)` on exactly the row most likely to be picked — and then + # `grep -oP`, which does not exist on macOS and would have turned this half + # into a silent skip there. + ver="$("$MCPP" toolchain list --format json 2>/dev/null \ + | jq -r --arg t "$tc" '[.data.toolchains[] | select(.family==$t) | .version][0] // empty')" + if [ -z "$ver" ]; then + echo " SKIP $tc is not installed here" + continue + fi + printf '[package]\nname = "idprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefault = "%s@%s"\n' \ + "$tc" "$ver" > mcpp.toml + + ht="$(host_target)" + if [ -z "$ht" ]; then + echo " SKIP could not read this host's own target from mcpp" + continue + fi + + implicit="$(ldflags_of)" + explicit="$(ldflags_of --target "$ht")" + + if [ -z "$implicit" ] || [ -z "$explicit" ]; then + # ⚠️ EARNED, NOT ASSUMED. One of the two produced no build.ninja at all, + # which is itself the asymmetry this test is about — so it is only a + # skip when NEITHER produced one. + if [ -z "$implicit" ] && [ -z "$explicit" ]; then + echo " SKIP $tc@$ver built nothing either way here" + continue + fi + echo "FAIL: $tc@$ver — one spelling built and the other did not" + echo " implicit: ${implicit:+produced a link line}${implicit:-produced nothing}" + echo " explicit: ${explicit:+produced a link line}${explicit:-produced nothing}" + fail=1 + checked=$((checked+1)) + continue + fi + + checked=$((checked+1)) + # `--target=` itself is expected on the explicit side and only + # there: it is the one token that names which machine, and the identity is + # about everything else. + a="$(printf '%s\n' "$implicit" | tr ' ' '\n' | grep -v '^--target=' | sort)" + b="$(printf '%s\n' "$explicit" | tr ' ' '\n' | grep -v '^--target=' | sort)" + + if [ "$a" = "$b" ]; then + echo " ok $tc@$ver: naming $ht changes nothing" + else + echo "FAIL: $tc@$ver: naming $ht changed the link line" + diff <(printf '%s\n' "$a") <(printf '%s\n' "$b") \ + | grep -E '^[<>]' | head -8 | sed 's/^/ /' + fail=1 + fi +done + +if [ "$checked" = 0 ]; then + echo "SKIP: no toolchain here produced a link line to compare" + exit 0 +fi +[ "$fail" = 0 ] || exit 1 +echo "OK: naming the host's own target changes nothing ($checked toolchains)" diff --git a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh new file mode 100755 index 00000000..a989b432 --- /dev/null +++ b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell jq +# The layer the report names is the layer the link line reaches for. +# +# ⭐⭐ TWO RELATIONS, NO EXPECTED VALUES. Like e2e 295, this compares two things +# mcpp itself produced rather than checking them against a table, so it holds on +# every host and needs nothing installed beyond a working toolchain: +# +# c-abi (payload) ⇒ the link line must reach into that payload +# c-abi (graph) ⇒ the link line must NOT reach into this host's C library +# +# ⚠️ THE FIRST ONE IS THE SHAPE THIS RELEASE KEEPS PAYING FOR. `xim:glibc` is +# installed, carries Scrt1.o/crti.o/crtn.o, and the report says +# `c-abi glibc (payload)` — while llvm's link line contained no reference to it +# at all and the build failed on startup objects resolved from /lib. The report +# and the link line disagreed, and only the report was read. +# +# ⚠️ AND THE SECOND ONE IS ITS MIRROR. A graph-supplied C library that still +# carried this host's loader produced, measured 2026-08-23: +# +# ld64.lld: error: unknown argument +# '--dynamic-linker=…/xim-x-glibc/2.44/lib64/ld-linux-x86-64.so.2' +# +# — accurate, and naming nothing about the decision. Both directions are here +# because a fix for either one alone can break the other. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/src" +cd "$work" +printf '#include \nint main() { std::printf("ok\\n"); }\n' > src/main.cpp + +report=""; ldflags="" +build_and_read() { # extra args… → sets $report and $ldflags + rm -rf target + report="$("$MCPP" build "$@" 2>&1 || true)" + local f; f="$(find target -name build.ninja 2>/dev/null | head -1)" + ldflags="" + [ -n "$f" ] && ldflags="$(grep -m1 '^ldflags' "$f" || true)" +} + +# The `c-abi` row's origin and the package it names, from the report. +c_abi_line() { printf '%s\n' "$report" | grep -E '^\s+c-abi\s' | head -1; } + +fail=0; checked=0 + +# ── Relation one: a payload C library must be on the link line ───────────── +for tc in gcc llvm; do + # ⭐ From the machine interface: `grep -oP` does not exist on macOS, and its + # failure mode here was a skip rather than a red. + ver="$("$MCPP" toolchain list --format json 2>/dev/null \ + | jq -r --arg t "$tc" '[.data.toolchains[] | select(.family==$t) | .version][0] // empty')" + [ -n "$ver" ] || { echo " SKIP $tc is not installed here"; continue; } + printf '[package]\nname = "linkprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefault = "%s@%s"\n' \ + "$tc" "$ver" > mcpp.toml + build_and_read --verbose + line="$(c_abi_line)" + case "$line" in + *"(payload)"*) ;; + *) echo " SKIP $tc@$ver: c-abi is not the payload's here (${line:-no row})"; continue ;; + esac + [ -n "$ldflags" ] || { echo " SKIP $tc@$ver produced no link line"; continue; } + checked=$((checked+1)) + + # ⭐ THE PAYLOAD IS NAMED BY THE REPORT, so the assertion does not hardcode + # one. Whatever `xim-x-…` directory the C library came from must appear. + pkg="$(printf '%s\n' "$report" | awk 'match($0, /xim-x-[a-z0-9-]+/) { + print substr($0, RSTART, RLENGTH) }' | sort -u | head -20)" + hit=0 + for p in $pkg; do + printf '%s\n' "$ldflags" | grep -q -- "$p" && hit=1 && break + done + # A C library whose headers and libs live inside the compiler payload needs + # no separate entry — that is the gcc cross-driver arrangement — so the + # criterion is that SOMETHING from mcpp's own store is on the line. + if [ "$hit" = 1 ] || printf '%s\n' "$ldflags" | grep -q 'registry/data/xpkgs'; then + echo " ok $tc@$ver: the payload the report names is on the link line" + else + echo "FAIL: $tc@$ver: report says c-abi is the payload's, link line has no payload path" + echo " $line" + printf ' %s\n' "$(printf '%s' "$ldflags" | cut -c1-110)" + fail=1 + fi +done + +# ── Relation two: a graph C library must not drag the host's in ─────────── +cat > mcpp.toml <<'TOML' +[package] +name = "linkprobe" +version = "0.1.0" + +[toolchain] +default = "llvm@22.1.8" + +[dependencies] +openkal-musl = "0.3.5" +openkal-llvm-runtime = "0.1.3" +TOML +printf 'import std;\nint main(){ std::println("ok"); }\n' > src/main.cpp +build_and_read +line="$(c_abi_line)" +case "$line" in + *graph*) + [ -n "$ldflags" ] || { echo " SKIP the graph build produced no link line"; :; } + if [ -n "$ldflags" ]; then + checked=$((checked+1)) + # This host's C library, by the two spellings mcpp itself would emit. + if printf '%s\n' "$ldflags" | grep -qE 'xim-x-glibc|/lib/x86_64-linux-gnu|/usr/lib/gcc'; then + echo "FAIL: c-abi comes from the graph, yet the link line reaches for this host's C library" + printf '%s\n' "$ldflags" | tr ' ' '\n' \ + | grep -E 'xim-x-glibc|/lib/x86_64-linux-gnu|/usr/lib/gcc' | head -4 | sed 's/^/ /' + fail=1 + else + echo " ok a graph C library does not drag this host's in" + fi + fi ;; + *) echo " SKIP the graph did not supply the C library here (${line:-no row})" ;; +esac + +if [ "$checked" = 0 ]; then + echo "SKIP: nothing here produced a link line to relate to a report" + exit 0 +fi +[ "$fail" = 0 ] || exit 1 +echo "OK: what the report names is what the link line uses ($checked relations)" diff --git a/tests/e2e/297_a_capability_pin_is_not_a_preference.sh b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh new file mode 100755 index 00000000..5e0e6f84 --- /dev/null +++ b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell jq +# A declared toolchain overrides a convention. It does not override a capability. +# +# ⭐⭐ THE TARGET TABLE'S PIN MEANS TWO DIFFERENT THINGS AND ONLY ONE OF THEM IS +# A PREFERENCE. +# +# hosted row `x86_64-linux-musl → gcc@16.1.0` +# "this payload supplies the target's C library" — an author +# who names their own compiler has said they supply it +# instead, so the declaration wins. +# +# bare-metal row `riscv64-none-elf → llvm@22.1.8` +# the table's own words: "the pin is llvm on every host +# because clang/lld are cross-compilers by construction". +# A host g++ does not emit riscv64 whatever anyone declares. +# +# ⚠️ MEASURED 2026-08-26, before this file existed: +# +# [toolchain] default = "gcc@16.1.0" +# $ mcpp build --target riscv64-none-elf +# g++: error: unrecognized argument in option '-mabi=lp64d' +# g++: note: valid arguments to '-mabi=' are: ms sysv +# +# — a message about an option, for a decision made a hundred lines earlier. +# +# ⭐⭐ BOTH DIRECTIONS, BECAUSE REFUSING EVERYTHING ALSO STOPS THE BAD MESSAGE. +# Half two declares gcc for a HOSTED target and requires it to be honoured; a +# guard that refused there would take away the escape hatch the whole +# convention/preference distinction exists to protect. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/src" +cd "$work" +printf 'extern "C" int main(int, char**, char**) { return 0; }\n' > src/main.cpp + +# ⭐ From mcpp's machine interface rather than from the table it prints for +# people: a column position is not a contract, and two earlier drafts of the +# neighbouring test disagreed about which column held the version. +gccver="$("$MCPP" toolchain list --format json 2>/dev/null \ + | jq -r '[.data.toolchains[] | select(.family=="gcc") | .version][0] // empty')" +if [ -z "$gccver" ]; then + echo "SKIP: gcc is not installed here, and this test is about declaring it" + exit 0 +fi + +# ── Half one: a bare-metal target refuses, and says why ─────────────────── +printf '[package]\nname = "capprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefault = "gcc@%s"\n\n[target.riscv64-none-elf]\nsysroot = ""\n' \ + "$gccver" > mcpp.toml +rm -rf target +out="$(cd "$work" && "$MCPP" build --target riscv64-none-elf 2>&1 || true)" + +# ⭐⭐ THE CLASSIFICATION COMES FROM THE MACHINE INTERFACE, THE WORDING FROM THE +# MESSAGE. `data.reason` is a finite token, so "did it refuse, and under which +# rule" survives any rewording; the assertions below still require the sentence +# to name the target, the reason and the way out, because that is a promise the +# code cannot keep on its own. +# +# ⚠️ MEASURED COST OF NOT SPLITTING THEM: this file asserted `cannot emit it`, +# the message was reworded to `cannot be emitted by` in the same session, and +# the assertion then passed by matching nothing at all. +reason="$(cd "$work" && "$MCPP" why toolchain --target riscv64-none-elf \ + --toolchain "gcc@$gccver" --format json 2>/dev/null \ + | jq -r '.data.reason // "-"')" + +case "$reason" in + capability-pin) + echo " ok a bare-metal target refuses a compiler that cannot emit it" ;; + none) + # ⚠️ THE OLD BEHAVIOUR EXACTLY: resolution succeeded, gcc was handed a + # riscv64 target, and the complaint arrived a hundred lines later as + # `unrecognized argument in option '-mabi=lp64d'`. + echo "FAIL: the declaration was honoured and the build was left to fail later" + printf '%s\n' "$out" | grep -iE 'error|note' | head -3 | sed 's/^/ /' + exit 1 ;; + *) + # ⚠️ NEITHER OUTCOME MEANS THIS MACHINE CANNOT RUN THE TEST. A build that + # SUCCEEDED with gcc would be a third answer entirely, and one worth + # failing on: it would mean a host g++ emitted riscv64. + if printf '%s\n' "$out" | grep -q 'Finished'; then + echo "FAIL: gcc built a bare-metal riscv64 target — that should not be possible" + exit 1 + fi + echo "SKIP: the refusal reason here was '$reason', not one this test knows" + printf '%s\n' "$out" | grep -iE 'error' | head -2 | sed 's/^/ /' + exit 0 ;; +esac + +# ⭐ AND THE MESSAGE POINTS AT THE DECISION. A refusal that does not name the +# target and the way out leaves the reader where the `-mabi` message did. +ok=1 +printf '%s\n' "$out" | grep -q 'riscv64-none-elf' || ok=0 +printf '%s\n' "$out" | grep -qi 'capability' || ok=0 +printf '%s\n' "$out" | grep -q '\[toolchain\]' || ok=0 +if [ "$ok" = 1 ]; then + echo " ok and it names the target, the reason and the way out" +else + echo "FAIL: the refusal does not point at the decision" + printf '%s\n' "$out" | head -6 | sed 's/^/ /' + exit 1 +fi + +# ── Half two: a hosted target still honours the declaration ─────────────── +# +# `x86_64-linux-musl` carries `gcc@16.1.0` in the table, so declaring gcc here +# agrees with the row — which would make the assertion vacuous. Declare it for +# a row whose pin is llvm instead, and require the declaration to win. +printf '[package]\nname = "capprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefault = "gcc@%s"\n' \ + "$gccver" > mcpp.toml +printf '#include \nint main() { std::printf("ok\\n"); }\n' > src/main.cpp +rm -rf target +hosted="$(cd "$work" && "$MCPP" build --target x86_64-linux-gnu 2>&1 || true)" +hostedReason="$(cd "$work" && "$MCPP" why toolchain --target x86_64-linux-gnu \ + --toolchain "gcc@$gccver" --format json 2>/dev/null \ + | jq -r '.data.reason // "-"')" + +case "$hostedReason" in + capability-pin) + echo "FAIL: a hosted target refused a declared toolchain — the escape hatch is gone" + printf '%s\n' "$hosted" | head -4 | sed 's/^/ /' + exit 1 ;; + none) + # ⭐ AND IT MUST ALSO BUILD. "Not refused" alone would pass on a machine + # where resolution succeeded and the link then failed. + case "$hosted" in + *"Finished"*) echo " ok a hosted target still honours the declared toolchain" ;; + *) echo "FAIL: the hosted control resolved but did not build" + printf '%s\n' "$hosted" | grep -iE 'error' | head -2 | sed 's/^/ /' + exit 1 ;; + esac ;; + *) + echo "SKIP: the hosted control's reason was '$hostedReason', not one this test knows" + printf '%s\n' "$hosted" | grep -iE 'error' | head -2 | sed 's/^/ /' + exit 0 ;; +esac + +echo "OK: a capability pin is not a preference, and a convention still is" diff --git a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh new file mode 100644 index 00000000..df485d7a --- /dev/null +++ b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# requires: llvm unix-shell jq +# Naming your own compiler for a pinned target is allowed. Naming it and +# supplying nothing in place of what the pin supplied is not. +# +# ⭐⭐ THE TWO CASES ARE THE SAME MANIFEST MINUS ONE LINE. +# +# [toolchain] default = "llvm@22.1.8" → refused +# +# [dependencies] openkal-llvm-runtime = "…" → built +# [toolchain] default = "llvm@22.1.8" +# +# A hosted row's pin says "this payload supplies the target's C library". The +# escape hatch exists because a project whose graph supplies one instead has no +# use for it — which is `examples/06-openkal-cross`. With neither, clang is a +# retargetable compiler holding no C library at all. +# +# ⚠️ MEASURED 2026-08-26, before this file existed. Both spellings ran the whole +# build and failed at the link: +# +# --target x86_64-linux-musl +# hermetic link check failed … crtbeginT.o (bare name) +# --target x86_64-windows-gnu +# hermetic link check failed … +# /usr/lib/gcc/x86_64-w64-mingw32/13-win32/crtbegin.o (outside the sandbox) +# +# Accurate about the symptom, silent about the decision — and the second one +# names a directory belonging to the HOST, on a machine that happened to have a +# system mingw. A machine without one fails differently, which is the other +# reason the answer must not come from the link. +# +# ⚠️⚠️ HALF TWO IS NOT DECORATION. A guard that refused every declared toolchain +# would pass half one and take away the escape hatch — and the openkal ecosystem +# is built entirely on it. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/src" +cd "$work" +printf '#include \nint main() { std::printf("ok\\n"); }\n' > src/main.cpp + +llvmver="$("$MCPP" toolchain list --format json 2>/dev/null \ + | jq -r '[.data.toolchains[] | select(.family=="llvm") | .version][0] // empty')" +if [ -z "$llvmver" ]; then + echo "SKIP: llvm is not installed here, and this test is about declaring it" + exit 0 +fi + +fail=0 + +# ── Half one: a pinned row, a different family, and nothing in the graph ── +# +# The target is read from the machine rather than hardcoded: any row whose pin +# names a family other than llvm will do, and which rows exist is the target +# table's business, not this test's. +# ⭐ A ROW WHOSE PIN IS A gcc AND WHOSE STATUS SAYS THIS HOST SERVES IT. +# Both halves matter: a `planned` row would refuse for a different rule and +# half one would then pass while testing nothing. +pinned="$("$MCPP" toolchain list --format json 2>/dev/null \ + | jq -r '[.data.targets[] + | select(.toolchain | startswith("gcc")) + | select(.status != "planned") + | .target][0] // empty')" +if [ -z "$pinned" ]; then + echo "SKIP: no target row here pins a non-llvm toolchain" + exit 0 +fi + +printf '[package]\nname = "convprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefault = "llvm@%s"\n' \ + "$llvmver" > mcpp.toml +rm -rf target +out="$("$MCPP" build --target "$pinned" 2>&1 || true)" + +# ⭐⭐ CLASSIFY FROM THE MACHINE INTERFACE, ASSERT THE WORDING FROM THE MESSAGE. +# `data.reason` is a finite token and survives any rewording of the sentence; +# the sentence is still checked below, because naming the target, the convention +# and the replacement is a promise no code can keep on its own. +reason="$("$MCPP" why toolchain --target "$pinned" --toolchain "llvm@$llvmver" \ + --format json 2>/dev/null | jq -r '.data.reason // "-"')" + +case "$reason" in + convention-unreplaced) + echo " ok overriding a convention with nothing in its place is refused" ;; + none) + # ⚠️ TWO OUTCOMES SHARE THIS BRANCH AND ONLY ONE IS A REGRESSION. If the + # build also succeeded, an llvm payload now supplies this target's C library + # and the refusal has outlived its reason — the table changed and this test + # is the thing that says so. If it failed, the old behaviour is back: the + # decision was let through and the link paid for it. + case "$out" in + *"Finished"*) + echo "FAIL: $pinned linked with llvm alone — the refusal has outlived its reason" ;; + *) + echo "FAIL: the build ran and failed at the link instead of at the decision" + printf '%s\n' "$out" | grep -iE 'error|crtbegin' | head -3 | sed 's/^/ /' ;; + esac + fail=1 ;; + *) + echo "SKIP: $pinned refused under '$reason', which is not the rule this test is about" + exit 0 ;; +esac + +# ⭐ AND IT POINTS AT THE WAY OUT. A refusal that names no replacement leaves +# the reader exactly where `crtbeginT.o (bare name)` left them. +if [ "$fail" = 0 ]; then + ok=1 + printf '%s\n' "$out" | grep -q "$pinned" || ok=0 + printf '%s\n' "$out" | grep -q 'openkal' || ok=0 + printf '%s\n' "$out" | grep -q '\[toolchain\]' || ok=0 + if [ "$ok" = 1 ]; then + echo " ok and it names the target, the convention and the replacement" + else + echo "FAIL: the refusal does not point at the way out" + printf '%s\n' "$out" | head -8 | sed 's/^/ /' + fail=1 + fi +fi + +# ── Half two: the same declaration, with a graph that supplies the C library ── +# +# ⚠️ THE DEPENDENCY IS THE ONLY DIFFERENCE. Same compiler, same target, same +# source. +printf '[package]\nname = "convprobe"\nversion = "0.1.0"\n\n[dependencies]\nopenkal-llvm-runtime = "0.1.3"\n\n[toolchain]\ndefault = "llvm@%s"\n' \ + "$llvmver" > mcpp.toml +printf 'import std;\nint main() { std::println("ok"); }\n' > src/main.cpp +rm -rf target +graph="$("$MCPP" build --target "$pinned" --verbose 2>&1 || true)" + +# ⭐⭐ THE CLAIM IS THAT THE GUARD STOOD ASIDE, NOT THAT openkal COMPILES ON THIS +# ARCHITECTURE — AND THE FIRST VERSION ASSERTED THE SECOND. +# +# It required `Finished`, so a dependency failing for its own reasons turned +# half two into a skip and left the guard's escape hatch untested. Measured +# 2026-08-26 on `aarch64-linux-musl`: +# +# …/compiler-rt/lib/builtins/truncxfhf2.c:13:36: +# error: unknown type name 'xf_float'; did you mean 'tf_float'? +# +# — an x87 long-double builtin on a machine with no x87. Nothing to do with +# whether a declared toolchain was honoured, and a test that cannot tell the +# difference reports on whichever it happened to hit. +# +# The two things that ARE the claim: the refusal did not fire, and the graph +# really did supply the C library (otherwise "not refused" is vacuous — an +# unresolved dependency would satisfy it too). +graphReason="$("$MCPP" why toolchain --target "$pinned" --toolchain "llvm@$llvmver" \ + --format json 2>/dev/null | jq -r '.data.reason // "-"')" +graphCabi="$("$MCPP" why toolchain --target "$pinned" --toolchain "llvm@$llvmver" \ + --format json 2>/dev/null \ + | jq -r '[.data.layers[] | select(.layer=="c-abi") | .origin][0] // "-"')" + +if [ "$graphReason" = convention-unreplaced ]; then + echo "FAIL: a graph-supplied C library was refused — the escape hatch is gone" + printf '%s\n' "$graph" | head -4 | sed 's/^/ /' + fail=1 +elif [ "$graphCabi" = graph ]; then + echo " ok the same declaration is honoured when the graph supplies the C library" +else + # ⚠️ NOT A PASS, AND NOT A SKIP. "Not refused" on its own is satisfied by a + # dependency that never resolved, so the second condition is what makes the + # first one mean anything. + echo "FAIL: c-abi came from '$graphCabi', not the graph — half two proves nothing" + printf '%s\n' "$graph" | grep -iE 'error' | head -3 | sed 's/^/ /' + fail=1 +fi + +[ "$fail" = 0 ] || exit 1 +echo "OK: a convention may be overridden, but not merely removed" diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index 15234738..0225e9d3 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -128,6 +128,17 @@ esac # of either failing at runtime or declaring an unknown capability. command -v python3 &>/dev/null && CAPS+=(python3) +# jq: the JSON reader for the tests that consume mcpp's MACHINE interface +# (`--format json`) rather than the tables it prints for people. +# +# ⚠️ DECLARING IT IS PART OF ADDING IT. A `# requires:` token that no branch +# here ever adds makes every test naming it skip for ever, silently — the +# `65_*` block spent months in that state. Every GitHub-hosted runner ships jq, +# so in CI this is always true and a skip there is a red (the matrix workflow +# asserts each invariant's conclusion line); on a minimal local machine the +# skip is honest. +command -v jq &>/dev/null && CAPS+=(jq) + # nasm: the x86 assembler for .asm sources (PATH — including the xlings # subos shim — or the mcpp sandbox tool dir). if command -v nasm &>/dev/null \ @@ -178,9 +189,9 @@ echo "Detected capabilities: ${CAPS[*]:-}" # absent on Linux and must stay legal to declare. It is checked against the # CAPS+=() calls above by tests/e2e/README or by reading them -- keep it in # sync when adding a capability. -KNOWN_CAPS=(elf fresh-sandbox gcc import-std-libcxx llvm macos mingw mingw-cross - msvc musl nasm no-msvc pack patchelf python3 qemu-riscv scan-deps - symlink unix-shell windows wine xlings-msvc) +KNOWN_CAPS=(elf fresh-sandbox gcc import-std-libcxx jq llvm macos mingw + mingw-cross msvc musl nasm no-msvc pack patchelf python3 qemu-riscv + scan-deps symlink unix-shell windows wine xlings-msvc) bad_tokens=0 for tf in "$HERE"/[0-9]*.sh; do diff --git a/tests/matrix/compare.sh b/tests/matrix/compare.sh new file mode 100755 index 00000000..9595c928 --- /dev/null +++ b/tests/matrix/compare.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# tests/matrix/compare.sh +# +# ⭐⭐ 期望表是仓库里的文件,改了行为就必须同时改它。支持矩阵因此不是一份会过期的 +# 文档,而是一次测量与一份声明的比对。 +# +# ⚠️⚠️ 「跳过」必须是期望表说的,不是运行时发现的。一格因为「今天这台机器没装某个 +# 载荷」而跳过,与「这个组合本就不支持」是两回事 —— 前者会让矩阵在缺件的机器上悄悄 +# 变绿,而那正是本仓库反复付出代价的那种假绿。 +set -u +measured="${1:?用法: compare.sh }" +expected="${2:?}" +host="${3:?}" +# ⚠️ MODE 也是参数,不只是键的一列。一次扫描只产出一种体系,而拿它去比整张表, +# 另一种体系的每一行都会被报成「期望表说有而扫描没跑到」—— 一条真判据用错了论域, +# 得到的是一整屏假红。 +mode="${4:?}" + +# ⚠️ 键含 `mode`。两种体系(payload / graph)跑的是同一组 (host, target, compiler), +# 少了它,后跑的一遍会把先跑的一遍在期望表里的那一行「解释掉」。 +key() { awk -F'\t' -v h="$host" -v m="$mode" '$1==m && $2==h {print $1"\t"$2"\t"$3"\t"$4}' "$1" | sort; } +val() { awk -F'\t' -v h="$host" -v m="$mode" '$1==m && $2==h {print $1"\t"$2"\t"$3"\t"$4"\t"$10"\t"$11}' "$1" | sort; } + +# ⚠️ 先断言扫描真的跑了。一格没跑与全部通过,在退出码上没有区别。 +n_m=$(key "$measured" | wc -l) +n_e=$(key "$expected" | wc -l) +echo "measured $n_m cells, expected table has $n_e for host '$host' mode '$mode'" +if [ "$n_m" = 0 ]; then + echo "::error::the scan produced no rows for '$host'/'$mode' — it did not run" + exit 1 +fi + +fail=0 + +# ── 期望表里有而实测没有:这一格没跑到 ──────────────────────────────── +missing=$(comm -13 <(key "$measured") <(key "$expected")) +if [ -n "$missing" ]; then + echo "::error::cells the expected table names but the scan never reached:" + printf '%s\n' "$missing" | sed 's/^/ /' + fail=1 +fi + +# ── 状态不符 ────────────────────────────────────────────────────────── +while IFS=$'\t' read -r md h t c st rs; do + [ -z "${h:-}" ] && continue + want=$(awk -F'\t' -v m="$md" -v h="$h" -v t="$t" -v c="$c" \ + '$1==m && $2==h && $3==t && $4==c {print $10"\t"$11}' "$expected") + if [ -z "$want" ]; then + # ⭐ 实测有而期望表没有 —— 支持面扩大了,这也要有人确认。 + echo "::error::[$md] $h $t $c → $st, and the expected table does not mention this cell" + echo " 新增一行到 tests/matrix/expected.tsv,或说明为何不该出现" + fail=1 + continue + fi + # ⭐ 理由也在判据里。一格从「因为能力 pin 而拒绝」变成「因为约定没被替换而 + # 拒绝」,status 仍是 unsupported —— 而那是两条不同的规则,换了一条却不红, + # 这张表就只在说「它没建出来」,不在说「为什么」。 + wantSt="${want%%$'\t'*}"; wantRs="${want##*$'\t'}" + if [ "$st" != "$wantSt" ]; then + echo "::error::[$md] $h $t $c → measured '$st', expected '$wantSt'" + fail=1 + elif [ "$rs" != "$wantRs" ]; then + echo "::error::[$md] $h $t $c → $st for '$rs', expected '$wantRs'" + fail=1 + fi +done < <(val "$measured") + +[ "$fail" = 0 ] || { echo "::error::the matrix does not match what this repository declares"; exit 1; } +echo "OK: $n_m cells match the expected table for '$host'/'$mode'" diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv new file mode 100644 index 00000000..bed20a4a --- /dev/null +++ b/tests/matrix/expected.tsv @@ -0,0 +1,70 @@ +# tests/matrix/expected.tsv — 这个仓库声明的支持矩阵。 +# +# ⭐⭐ 这是一份声明,不是一份记录。改了行为就必须同时改它,而 CI 比对的是 +# `tests/matrix/scan.sh` 的一次真实测量与这里的每一行。 +# +# 列:mode host target compiler compiler-triple c-lib c-abi c++-abi openkal status reason +# +# status × reason —— 两者都参与比对: +# ok / none 解析通过,且真的建出来了 +# unsupported / tier-planned 词表里有这一行,还没有任何东西接线 +# unsupported / capability-pin 这一行的工具链是能力陈述,不可被推翻 +# unsupported / convention-unreplaced 约定被推翻了,而没有任何东西接替它 +# unsupported / host-cannot-serve 本机没有载荷,图也没有供给这个系统 +# unsupported / layer-requirement 某个包要求的层,解析没有给出 +# +# ⚠️⚠️ 这里**没有** `mismatch`,而这是刻意的。写下 `mismatch` 就是把一个缺陷 +# 声明成期望,矩阵会在它上面变绿。一格测出 `mismatch`,要么修 mcpp,要么 +# 让 mcpp 在决定处给出一句带 reason 的拒绝 —— 没有第三条路。 +# +# ⚠️ 只有 `mode host target compiler status reason` 六列参与比对。中间五列是 +# 测量的记录,留在这里是为了让一行的变化可读:`c-abi` 从 payload 变成 graph +# 会在 diff 里自己说话,而只有 status 的表说不出来。 +# +# ⭐ reason 也是判据。一格从 `capability-pin` 变成 `convention-unreplaced`, +# status 仍是 unsupported —— 那是两条不同的规则,换了一条却不红,这张表就 +# 只在说「它没建出来」,不在说「为什么」。 +# +# linux 行:2026-08-26 实测于 x86_64 Linux(mcpp 2026.8.26.1)。 +# windows / macos 行:留待各自宿主的第一次 CI 运行回填 —— ⚠️ 从别的宿主推断出来 +# 的一行,断言的是推断而不是那台机器。 +payload linux aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned +payload linux aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none +payload linux aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned +payload linux riscv64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux x86_64-linux-gnu gcc@16.1.0 x86_64-unknown-linux-gnu subos gnu(payload) libstdc++(payload) - ok none +payload linux x86_64-linux-musl gcc@16.1.0 x86_64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none +payload linux x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned +payload linux x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux x86_64-windows-gnu gcc@16.1.0 x86_64-w64-windows-gnu none gnu(payload) libstdc++(payload) - ok none +payload linux x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin +payload linux aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +payload linux aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux aarch64-none-elf llvm@22.1.8 aarch64-none-elf payload - - - ok none +payload linux riscv32-none-elf llvm@22.1.8 riscv32-none-elf payload - - - ok none +payload linux riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +payload linux riscv64-none-elf llvm@22.1.8 riscv64-none-elf payload - - - ok none +payload linux x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload gnu(payload) libc++(payload) - ok none +payload linux x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +payload linux x86_64-none-elf llvm@22.1.8 x86_64-none-elf payload - - - ok none +payload linux x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve +graph linux aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned +graph linux aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned +graph linux x86_64-linux-gnu gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux x86_64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned +graph linux x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin +graph linux aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +graph linux aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +graph linux x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +graph linux x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh new file mode 100755 index 00000000..d544eaa2 --- /dev/null +++ b/tests/matrix/scan.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# tests/matrix/scan.sh — 把「这台机器支持哪些目标」变成一次测量的输出。 +# +# ⭐⭐ 两个入口,各回答自己那半: +# +# mcpp why toolchain --target T --toolchain C --format json +# 这一格**会解析成什么**,以及如果不解析,是因为哪一条规则。结构化, +# 不构建。 +# +# mcpp build --target T +# 解析通过之后**它到底建不建得出来**。 +# +# ⚠️⚠️ 两个都要,而这是被实测逼出来的。只查不建会把 `llvm × x86_64-windows-gnu` +# 报成绿:它解析得完全正常,失败发生在链接期的封闭性检查上。只建不查则回到老路 +# —— 只能靠匹配句子来分辨「拒绝」与「炸了」,而 2026-08-26 同一次会话里,我把 +# `cannot emit it` 改成 `cannot be emitted by`,一条断言当场变成空转。 +# +# ⭐ 于是这里**没有一处字符串匹配**。分类全部来自 `data.status` / `data.reason` +# 与构建的退出码。 +# +# ⚠️ 不直接问编译器。绕开被测对象去问它的组件,得到的是组件的默认行为而不是 mcpp +# 的行为 —— 分析文档里有一次就是这么错的。 +# +# 输出 TSV 到 stdout,一行一格: +# mode host target compiler compiler-triple c-lib c-abi c++-abi openkal status reason +# +# ⚠️ `mode` 是键的一部分,不是注释。两种体系跑的是同一组 (host, target, compiler), +# 少了它两张表会互相覆盖 —— 而覆盖的方向取决于谁后跑,不取决于谁对。 +# +# status 只有三种,而三者的区别是整套验收的核心: +# ok 解析通过,且真的建出来了 +# unsupported mcpp 自己拒绝了这个组合(reason 列给出规则名)—— 拒绝也是一种正确 +# mismatch 解析说可以,构建却失败了 —— 需要有人看 +set -u +MCPP="${MCPP:-mcpp}" +MODE="${1:-payload}" # payload | graph + +# ⚠️ jq 缺席必须是硬错误。它的失败方式本来是「每一格都空着」,而一张全空的表和 +# 一张全绿的表在退出码上没有区别 —— 这正是这套矩阵存在的理由。 +command -v jq >/dev/null 2>&1 || { + echo "scan: jq is required (every GitHub-hosted runner ships it)" >&2 + exit 2 +} + +work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT +mkdir -p "$work/src"; cd "$work" + +case "$(uname -s)" in + Linux) HOST=linux ;; Darwin) HOST=macos ;; + MINGW*|MSYS*|CYGWIN*) HOST=windows ;; *) HOST=unknown ;; +esac + +# 目标与编译器清单都取自 mcpp 自己的机器接口,而不是脚本里再抄一份 —— 抄一份 +# 就会漂移,而按列宽解析一张给人看的表,会让列宽变成测试套件的一部分。实测过 +# 的代价:两版测试对「版本在第几列」的看法不同,读 `$NF` 的那版取到了 +# `(default)` —— 一个恰好只出现在最可能被选中的那一行上的值。 +LIST="$("$MCPP" toolchain list --format json 2>/dev/null)" +[ -n "$LIST" ] || { echo "scan: \`toolchain list --format json\` produced nothing" >&2; exit 2; } + +targets() { printf '%s' "$LIST" | jq -r '.data.targets[].target' | sort -u; } + +# ⭐ 每族只取最新的一个。矩阵回答的是「这个目标支不支持」,同一族的三个版本对 +# 这个问题给同一个答案,而 5×12 与 2×12 在 CI 上是小时级的差别。 +# +# ⚠️ 但收窄必须说出来。被丢掉的版本写到 stderr —— 一次没跑的测量和一次通过的 +# 测量,在退出码上没有区别。 +compilers() { + printf '%s' "$LIST" | jq -r '.data.toolchains[] | .family + "@" + .version' \ + | awk -F@ '{ if (seen[$1]++) print "scan: 略过 " $0 " —— 每族只取最新" > "/dev/stderr" + else print $0 }' +} + +emit() { printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$@"; } + +for tc in $(compilers); do + for t in $(targets); do + # ⚠️⚠️ graph 体系不覆盖裸机行,而这是一句声明,不是一次省略。 + # + # openkal 的这套依赖(`openkal-musl` + `openkal-llvm-runtime`)描述的是一个 + # **有宿主的**系统;把它加到零 libc 的目标上问的不是「图能不能供给这个目标」, + # 而是一个没有人会写的组合 —— 实测 `x86_64-none-elf` 因此拉进 + # `openkal-opensbi`(RISC-V 的监管者接口)并编译失败。 + # + # 裸机 × 图由 135/136/292 用正确的包覆盖。这里跳过并说出来。 + if [ "$MODE" = graph ] && printf '%s' "$t" | grep -q -- '-none-elf$'; then + echo "scan: 略过 graph × $t —— 裸机行不在这套依赖的论域内" >&2 + continue + fi + + { printf '[package]\nname = "mxscan"\nversion = "0.1.0"\n' + [ "$MODE" = graph ] && printf '\n[dependencies]\nopenkal-musl = "0.3.5"\nopenkal-llvm-runtime = "0.1.3"\n' + } > mcpp.toml + + # ⚠️⚠️ 探针必须按目标的层级选,一份源码服务不了整张表。 + # + # 第一版对每一格都写 `#include `,于是四个裸机目标全报 + # `fatal error: 'cstdio' file not found` 并被记成 `mismatch`。那不是 mcpp + # 的失败 —— 零 libc 的目标本来就没有 ``,是探针问错了问题。 + # + # ⭐ 一个自己就编不过的探针,产出的整列都是关于探针的。 + if [ "$MODE" != graph ] && printf '%s' "$t" | grep -q -- '-none-elf$'; then + printf 'extern "C" void kmain() { for (volatile int i = 0; i < 1; ++i) {} }\n' > src/main.cpp + printf '\n[target.%s]\nsysroot = ""\n' "$t" >> mcpp.toml + else + case "$MODE" in + graph) printf 'import std;\nint main(){ std::println("ok"); }\n' > src/main.cpp ;; + *) printf '#include \nint main(){ std::printf("ok\\n"); }\n' > src/main.cpp ;; + esac + fi + + # ── 第一问:这一格会解析成什么 ──────────────────────────────────── + q="$(timeout "${MATRIX_QUERY_TIMEOUT:-300}" \ + "$MCPP" why toolchain --target "$t" --toolchain "$tc" --format json 2>/dev/null)" + if [ -z "$q" ]; then + # ⚠️ 查询本身没跑起来。这不是「这一格不支持」,而是「不知道」—— 两者必须 + # 分开,否则一次环境故障会被整片读成「不支持」。 + emit "$MODE" "$HOST" "$t" "$tc" - - - - - mismatch query-failed + continue + fi + jq_get() { printf '%s' "$q" | jq -r "$1" 2>/dev/null; } + st="$(jq_get '.data.status // "-"')" + rs="$(jq_get '.data.reason // "-"')" + tri="$(jq_get '(.data.triple.llvm // "") | if . == "" then "-" else . end')" + clib="$(jq_get '.data.cLibrary.origin // "-"')" + # ⚠️ 缺席的层是 `-`,不是 `(none)`。一个空的接口名加一对括号,读起来像 + # 「有这一层而它没名字」,而实际是「这一层不存在」—— 裸机目标的 c-abi + # 正是后者,那是一句陈述,不是一个空格。 + lay() { jq_get "[.data.layers[] | select(.layer==\"$1\") + | if .origin == \"none\" then \"-\" + else .interface + \"(\" + .origin + \")\" end][0] // \"-\""; } + cabi="$(lay 'c-abi')"; cxxabi="$(lay 'c++-abi')" + okpkg="$(jq_get '[.data.layers[].impl | select(startswith("openkal"))][0] // "-"')" + : "${tri:=-}" "${clib:=-}" "${cabi:=-}" "${cxxabi:=-}" "${okpkg:=-}" + + if [ "$st" = refused ]; then + emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" \ + unsupported "$rs" + continue + fi + + # ── 第二问:解析说可以,那它建得出来吗 ──────────────────────────── + # + # ⭐ 退出码就是判据。`why` 已经回答了「为什么不」那一半,所以这里不需要再去 + # 读任何一行输出。 + printf '\n[toolchain]\ndefault = "%s"\n' "$tc" >> mcpp.toml + rm -rf target + if timeout "${MATRIX_TIMEOUT:-600}" "$MCPP" build --target "$t" >/dev/null 2>&1; then + emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" ok none + else + emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" \ + mismatch build-failed + fi + done +done From a8bb3a3ac131ea38f0b09897db0958f4485341c4 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:27:54 +0800 Subject: [PATCH 08/32] ci(matrix): the payload arrangement 296 asserted was Linux's, not every host's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macos-14, measured: c-abi libSystem (payload) ldflags = -isysroot /Applications/Xcode_15.4.app/…/MacOSX.sdk — and that is correct. Darwin's C library is the SDK's libSystem and the SDK belongs to the machine, not to a payload; 296 hardcoded `registry/data/xpkgs` and called its absence a defect. It now asks the query for `cLibrary.path` — the value `resolve_link_model` produced, which is the same function the flag emitter calls — and requires it on the link line. What mcpp says it will pass is what mcpp passes, on any host. 297 declares a non-llvm compiler, and every toolchain mcpp installs on macOS is llvm, so there the distinction has nothing to distinguish. The exemption is per host and written down, rather than accepting "a skip is a conclusion" everywhere — which is what this step exists to prevent. --- .github/workflows/ci-target-matrix.yml | 31 ++++++++++++- ...report_names_is_what_the_link_line_uses.sh | 46 ++++++++++++------- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml index 20aaadf1..1d496e92 100644 --- a/.github/workflows/ci-target-matrix.yml +++ b/.github/workflows/ci-target-matrix.yml @@ -91,13 +91,40 @@ jobs: } echo " ok $1" } + # ⚠️⚠️ ONE DECLARED EXEMPTION, AND IT IS DECLARED RATHER THAN INFERRED. + # + # 297 is about a bare-metal row refusing a compiler that cannot emit + # the target, so it has to DECLARE a non-llvm compiler. Every toolchain + # mcpp installs on macOS is llvm — there is no gcc payload and no MSVC + # — so on that host the convention/capability distinction has nothing + # to distinguish, and the test says so and exits 0. + # + # ⭐ THE EXEMPTION IS PER HOST AND SPELLED OUT. Accepting "a skip is a + # conclusion" everywhere would let a genuinely broken test pass on all + # three; the whole reason this step exists is that an exit code cannot + # tell a pass from a skip. + check_or_declared_skip() { + grep -qF "$2" "$1".log && { echo " ok $1"; return 0; } + grep -q '^SKIP:' "$1".log && { + echo " ok $1 (declared skip on ${{ matrix.host }}: $(grep -m1 '^SKIP:' "$1".log))" + return 0 + } + echo "::error::$1 neither concluded nor declared a skip on ${{ matrix.host }}" + tail -6 "$1".log 2>/dev/null | sed 's/^/ /' + return 1 + } fail=0 check 295_naming_the_host_target_changes_nothing.sh \ "OK: naming the host's own target changes nothing" || fail=1 check 296_what_the_report_names_is_what_the_link_line_uses.sh \ "OK: what the report names is what the link line uses" || fail=1 - check 297_a_capability_pin_is_not_a_preference.sh \ - "OK: a capability pin is not a preference" || fail=1 + if [ "${{ matrix.host }}" = macos ]; then + check_or_declared_skip 297_a_capability_pin_is_not_a_preference.sh \ + "OK: a capability pin is not a preference" || fail=1 + else + check 297_a_capability_pin_is_not_a_preference.sh \ + "OK: a capability pin is not a preference" || fail=1 + fi check 298_overriding_a_convention_requires_replacing_it.sh \ "OK: a convention may be overridden, but not merely removed" || fail=1 [ "$fail" = 0 ] || exit 1 diff --git a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh index a989b432..1d1bebee 100755 --- a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh +++ b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh @@ -64,23 +64,37 @@ for tc in gcc llvm; do [ -n "$ldflags" ] || { echo " SKIP $tc@$ver produced no link line"; continue; } checked=$((checked+1)) - # ⭐ THE PAYLOAD IS NAMED BY THE REPORT, so the assertion does not hardcode - # one. Whatever `xim-x-…` directory the C library came from must appear. - pkg="$(printf '%s\n' "$report" | awk 'match($0, /xim-x-[a-z0-9-]+/) { - print substr($0, RSTART, RLENGTH) }' | sort -u | head -20)" - hit=0 - for p in $pkg; do - printf '%s\n' "$ldflags" | grep -q -- "$p" && hit=1 && break - done - # A C library whose headers and libs live inside the compiler payload needs - # no separate entry — that is the gcc cross-driver arrangement — so the - # criterion is that SOMETHING from mcpp's own store is on the line. - if [ "$hit" = 1 ] || printf '%s\n' "$ldflags" | grep -q 'registry/data/xpkgs'; then - echo " ok $tc@$ver: the payload the report names is on the link line" + # ⭐⭐ THE QUERY NAMES THE DIRECTORY, AND THE LINK LINE MUST CONTAIN IT. + # + # `cLibrary.path` is what `resolve_link_model` decided — the same function + # the flag emitter calls — so this asserts that what mcpp SAYS it will pass + # is what mcpp passes. Nothing is hardcoded and nothing is per-platform. + # + # ⚠️ THE FIRST VERSION HARDCODED `registry/data/xpkgs` AND WAS WRONG ON + # macOS. Measured on macos-14: + # + # c-abi libSystem (payload) + # ldflags = -isysroot /Applications/Xcode_15.4.app/…/MacOSX.sdk + # + # — and that is CORRECT. Darwin's C library is the SDK's libSystem, and the + # SDK belongs to the machine, not to a payload. The test was asserting a + # Linux arrangement and calling its absence a defect. + want="$("$MCPP" why toolchain --toolchain "$tc@$ver" --format json 2>/dev/null \ + | jq -r '.data.cLibrary.path // ""')" + if [ -z "$want" ]; then + # ⚠️ EARNED: the query says mcpp passes no C-library path at all, which + # is the self-contained cross-driver arrangement (mingw g++ carries its + # own). There is nothing to look for, so there is nothing to assert. + echo " SKIP $tc@$ver: mcpp passes no C-library path for this target" + checked=$((checked-1)) + continue + fi + if printf '%s\n' "$ldflags" | grep -qF -- "$want"; then + echo " ok $tc@$ver: the C library the query names is on the link line" else - echo "FAIL: $tc@$ver: report says c-abi is the payload's, link line has no payload path" - echo " $line" - printf ' %s\n' "$(printf '%s' "$ldflags" | cut -c1-110)" + echo "FAIL: $tc@$ver: the query names a C library the link line does not use" + echo " query: $want" + printf ' ldflags: %s\n' "$(printf '%s' "$ldflags" | cut -c1-110)" fail=1 fi done From 20f4de32b5a21f4eb17b2e29b10e7ab5d19995ab Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:34:06 +0800 Subject: [PATCH 09/32] fix(wire): a target row's `toolchain` meant two things, so the convention got its own field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On an installed row it is the payload that is here; on a vocabulary row it is the target table's convention pin. A row can have the first and no second — `x86_64-linux-gnu` has an installed gcc and no convention at all. That is acceptable for a column a person reads and wrong for a field a program reads. Measured on ubuntu-24.04: e2e 298 selected "a row whose convention is a gcc" from `toolchain`, got `x86_64-linux-gnu`, and demanded a refusal that correctly did not happen. Locally the first matching row was `aarch64-linux-musl`, which does have one, so it passed. Same defect family as the release this interface was added for, introduced in the interface itself. Also: docs/03 now states the convention/capability distinction, in both languages — including which two kinds of row cannot be overridden and why. --- docs/03-toolchains.md | 35 +++++++++++++++++++ docs/zh/03-toolchains.md | 33 +++++++++++++++++ src/toolchain/lifecycle.cppm | 18 ++++++++++ ...ding_a_convention_requires_replacing_it.sh | 19 +++++++--- 4 files changed, 101 insertions(+), 4 deletions(-) diff --git a/docs/03-toolchains.md b/docs/03-toolchains.md index de919086..80906480 100644 --- a/docs/03-toolchains.md +++ b/docs/03-toolchains.md @@ -417,6 +417,41 @@ toolchain = "gcc@16.1.0" linkage = "static" ``` +### A convention may be overridden. A capability may not. + +The pin on a **hosted** row answers *which payload supplies this target's C +library*, so a project that supplies one itself may name any compiler — that is +the escape hatch the whole openkal ecosystem is built on. What it may not do is +name a different compiler and supply nothing: + +``` +$ mcpp build --target x86_64-linux-musl # [toolchain] default = "llvm@…" +error: target 'x86_64-linux-musl' takes its C library from the 'gcc@16.1.0' + payload, and 'llvm@22.1.8' has none here. +``` + +Two rows answer a different question, and their pin cannot be overridden at all: + +| row | why | +|---|---| +| every `*-none-elf` | no per-host cross payload exists; clang and lld are cross-compilers by construction and gcc is not | +| `x86_64-windows-musl` | no gcc payload emits a PE with a musl C library — the mingw payload emits PE with the MinGW CRT, which is the separate `-gnu` row | + +``` +$ mcpp build --target riscv64-none-elf # [toolchain] default = "gcc@…" +error: target 'riscv64-none-elf' cannot be emitted by 'gcc@16.1.0'. +``` + +⭐ **Both refusals are decided where the decision is made**, not left to the +compiler. Before 2026.8.26.1 the first ran the whole build and died at the link +on `crtbeginT.o (bare name)`, and the second produced +`g++: error: unrecognized argument in option '-mabi=lp64d'` — a message about an +option, for a decision made a hundred lines earlier. + +A program classifying these reads `data.reason` from +`mcpp why toolchain --format json` (`convention-unreplaced` / `capability-pin`) +rather than the sentence — see [chapter 11](11-machine-output.md). + A project can set its *default* build target — this is where "this project ships fully-static" belongs (static output is a product property, not a compiler-family property): diff --git a/docs/zh/03-toolchains.md b/docs/zh/03-toolchains.md index d7bd57d9..57115a8e 100644 --- a/docs/zh/03-toolchains.md +++ b/docs/zh/03-toolchains.md @@ -386,6 +386,39 @@ toolchain = "gcc@16.1.0" linkage = "static" ``` +### 约定可以被推翻,能力不行 + +**有宿主**那一行的 pin 回答的是*哪个载荷供给这个目标的 C 库*,所以一个自己供给 +C 库的工程可以写任何编译器 —— 整个 openkal 生态就建立在这道口子上。不能做的是 +写另一个编译器而什么都不供给: + +``` +$ mcpp build --target x86_64-linux-musl # [toolchain] default = "llvm@…" +error: target 'x86_64-linux-musl' takes its C library from the 'gcc@16.1.0' + payload, and 'llvm@22.1.8' has none here. +``` + +有两类行回答的是另一个问题,它们的 pin 根本不可被推翻: + +| 行 | 为什么 | +|---|---| +| 所有 `*-none-elf` | 不存在按宿主分的交叉载荷;clang 与 lld 按构造就是交叉编译器,gcc 不是 | +| `x86_64-windows-musl` | 没有任何 gcc 载荷发得出 PE + musl —— mingw 载荷发的是 PE + MinGW CRT,那是隔壁 `-gnu` 那一行 | + +``` +$ mcpp build --target riscv64-none-elf # [toolchain] default = "gcc@…" +error: target 'riscv64-none-elf' cannot be emitted by 'gcc@16.1.0'. +``` + +⭐ **两处拒绝都发生在做出决定的地方**,而不是留给编译器。2026.8.26.1 之前,前者 +会跑完整个构建然后死在链接上,报 `crtbeginT.o (bare name)`;后者给出 +`g++: error: unrecognized argument in option '-mabi=lp64d'` —— 一条关于选项的 +消息,而决定在一百行之前。 + +要给这些结果分类的程序读 `mcpp why toolchain --format json` 的 `data.reason` +(`convention-unreplaced` / `capability-pin`),而不是那句话 —— +见[第 11 章](11-machine-output.md)。 + 项目还可以声明自己的*默认*构建 target——"本项目发布全静态"这类语义 就该放在这里(全静态是产物属性,不是编译器家族属性): diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index 916a4ad4..907f5507 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -573,6 +573,20 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, std::string target; // canonical triple std::string note; // "host" / "static" / "PE" / "cross" tags std::string toolchain; // "gcc 16.1.0" or "—" + // ⚠️⚠️ `toolchain` MEANS TWO DIFFERENT THINGS DEPENDING ON THE ROW, and + // that is fine for a column a person reads and wrong for a field a + // program reads. On an INSTALLED row it is the payload that is here; on + // a vocabulary row it is the target table's convention pin. A row can + // have the first and no second — `x86_64-linux-gnu` has an installed + // gcc and no convention at all. + // + // Measured 2026-08-26: e2e 298 selected "a row whose pin is a gcc" from + // this field and got `x86_64-linux-gnu` on the CI runner, where the pin + // is empty. It then asserted a refusal that correctly did not happen. + // + // ⭐ So the convention travels in its own field, empty when there is + // none. Same defect family as the release this was written for. + std::string pin; // the target table's convention pin, or empty std::string status; // installed | available | planned bool isDefault = false; int rank = 0; // display order: installed < available < planned @@ -604,6 +618,8 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, r.target = t.str(); r.note = note_for(t); r.toolchain = tcLabel; + for (auto const& info : mcpp::toolchain::triple::known_targets()) + if (info.canonical == r.target) { r.pin = std::string(info.pin); break; } r.status = "installed"; r.rank = 0; r.isDefault = defSpec @@ -666,6 +682,7 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, std::string pin(info.pin); if (auto at = pin.find('@'); at != std::string::npos) pin[at] = ' '; r.toolchain = pin.empty() ? "—" : pin; + r.pin = std::string(info.pin); // Three answers, not two. "available" means a payload here produces it; // "via dependency graph" means the compiler is here and the system has // to come from packages — a different thing to do next, so a different @@ -688,6 +705,7 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, {"target", r.target}, {"note", r.note}, {"toolchain", r.toolchain}, + {"pin", r.pin}, {"status", r.status}, {"default", r.isDefault}, }); diff --git a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh index df485d7a..79ef5c7d 100644 --- a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh +++ b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh @@ -55,12 +55,23 @@ fail=0 # The target is read from the machine rather than hardcoded: any row whose pin # names a family other than llvm will do, and which rows exist is the target # table's business, not this test's. -# ⭐ A ROW WHOSE PIN IS A gcc AND WHOSE STATUS SAYS THIS HOST SERVES IT. -# Both halves matter: a `planned` row would refuse for a different rule and -# half one would then pass while testing nothing. +# ⭐⭐ `pin`, NOT `toolchain` — AND THE DIFFERENCE COST A RED CI RUN. +# +# `toolchain` is what the row is associated with: the installed payload on an +# installed row, the convention on a vocabulary row. A row can have the first +# and no second. This test needs a row that HAS a convention, so it reads the +# field that carries only that. +# +# Measured 2026-08-26 on ubuntu-24.04: selecting on `toolchain` picked +# `x86_64-linux-gnu`, whose convention pin is empty, and the test then demanded +# a refusal that correctly did not happen. +# +# The status filter matters for the same reason in the other direction: a +# `planned` row refuses under a different rule, and half one would pass while +# testing nothing. pinned="$("$MCPP" toolchain list --format json 2>/dev/null \ | jq -r '[.data.targets[] - | select(.toolchain | startswith("gcc")) + | select(.pin | startswith("gcc")) | select(.status != "planned") | .target][0] // empty')" if [ -z "$pinned" ]; then From 3d1f4e5ee229571b181b8a42f3873aabcbac072b Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:34:34 +0800 Subject: [PATCH 10/32] docs(11): state that a row's `toolchain` and `pin` answer different questions --- docs/11-machine-output.md | 11 +++++++++-- docs/zh/11-machine-output.md | 7 ++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/11-machine-output.md b/docs/11-machine-output.md index 11b1efc6..6cae2679 100644 --- a/docs/11-machine-output.md +++ b/docs/11-machine-output.md @@ -239,8 +239,15 @@ mcpp toolchain list --format json `data` is `{host, toolchains[], targets[]}`. A toolchain is `{family, version, default}` — plus `source: "system"` for a Visual Studio installation, which is located on the machine rather than installed by mcpp. A -target row is `{target, note, toolchain, status, default}`, and `status` is one -of `installed` / `available` / `via dependency graph` / `planned`. +target row is `{target, note, toolchain, pin, status, default}`, and `status` is +one of `installed` / `available` / `via dependency graph` / `planned`. + +⚠️ **`toolchain` and `pin` are not the same field twice.** `toolchain` is what +the row is associated with — the installed payload on an installed row, the +convention on a vocabulary row. `pin` is only ever the target table's +convention, and is empty for a row that has none. `x86_64-linux-gnu` has an +installed gcc and no convention at all, so selecting "rows whose convention is a +gcc" must read `pin`. ### `mcpp.why.toolchain` — what a build for one pair would resolve to diff --git a/docs/zh/11-machine-output.md b/docs/zh/11-machine-output.md index 6db0c91a..aff81f66 100644 --- a/docs/zh/11-machine-output.md +++ b/docs/zh/11-machine-output.md @@ -210,9 +210,14 @@ mcpp toolchain list --format json `data` 是 `{host, toolchains[], targets[]}`。一个工具链是 `{family, version, default}`;Visual Studio 另有 `source: "system"` —— 它是在 机器上被找到的,不是 mcpp 装的。一行目标是 -`{target, note, toolchain, status, default}`,`status` 取 +`{target, note, toolchain, pin, status, default}`,`status` 取 `installed` / `available` / `via dependency graph` / `planned`。 +⚠️ **`toolchain` 与 `pin` 不是同一个字段写两遍。** `toolchain` 是这一行关联到 +什么 —— 已装的行是装了的载荷,词表行是那一行的约定。`pin` 只承载目标表的约定, +没有约定的行为空。`x86_64-linux-gnu` 装了 gcc 而根本没有约定,所以要挑「约定是 +gcc 的行」必须读 `pin`。 + ### `mcpp.why.toolchain` —— 一对 (目标, 工具链) 会解析成什么 ``` From bab64994bd58c9add3434009cce272269e66a09b Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:38:35 +0800 Subject: [PATCH 11/32] fix(why): the query left a declared toolchain in the environment, and read a sink it had not cleared MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings on the machinery added in this branch. `MCPP_TOOLCHAIN` was set and never restored. Harmless as the last thing a process does and wrong for a library function — the next thing this process did would inherit a compiler nobody asked it for. `ScopedEnv` already exists for exactly this. The refusal sink is per-thread and `prepare_build` recurses for tool provisioning, so reading it after the call without clearing first could report a code recorded by an earlier query as this one's reason. A stale reason is worse than none: it is a specific, plausible, wrong answer. --- src/doctor.cppm | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/doctor.cppm b/src/doctor.cppm index ab01335f..e5f5aa84 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -801,8 +801,24 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec) // user-explicit declaration. Adding a second way in would give the two // spellings different provenance, and provenance is exactly what the // convention/capability distinction turns on. + // + // ⚠️ RESTORED AFTERWARDS. This is a library function; leaving a declared + // toolchain in the environment would make the NEXT thing this process does + // inherit a compiler nobody asked it for. + // ⭐ `ScopedEnv` already exists for exactly this and restores the prior + // value, including "there was none". + std::optional tcGuard; if (!tcSpec.empty()) - mcpp::platform::env::set("MCPP_TOOLCHAIN", std::string(tcSpec)); + tcGuard.emplace("MCPP_TOOLCHAIN", std::string(tcSpec)); + + // ⚠️⚠️ CLEARED BEFORE THE CALL, NOT ONLY READ AFTER IT. + // + // The sink is per-thread and `prepare_build` recurses for tool + // provisioning. Reading it afterwards without clearing first means a code + // recorded by an EARLIER query — or by a nested sub-build of a previous + // one — can be reported as this query's reason. A stale reason is worse + // than none: it is a specific, plausible, wrong answer. + (void)mcpp::build::refusal::take(); // ⚠️⚠️ STDOUT BELONGS TO THE ENVELOPE, AND `prepare_build` NARRATES. // From e1ade9abd342675e5cb57707953a230999a2e8fa Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:48:33 +0800 Subject: [PATCH 12/32] test(matrix): three criteria assumed the Linux arrangement, and windows-2022 said so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 295 named `data.host` and called it "the target this build would use anyway". On Linux those coincide for every family and the assumption stayed invisible; on Windows the host target is `x86_64-windows-msvc` and a mingw gcc targets `x86_64-windows-gnu`, so the identity asked gcc to emit MSVC and reported the refusal as a defect. It now asks the query which triple THIS toolchain resolves to with no target named, which is exact on every host. 296 skipped whenever the query named no C-library path, and on windows-2022 both toolchains took that branch — relation one had no coverage there at all. No path is also a claim: the self-contained arrangement must bring in nothing external either, which is checkable and is what the branch now asserts. ⚠️ Both spellings of the flag exist. `-B/usr/lib` is one word, `-B /usr/lib` is two; matching `^-B` flags the bare `-B` of the second form, and requiring a path inside the token makes the second form invisible. Measured both while writing the line. A false negative is the worse failure for a leak check, so the two forms are joined before anything is decided. 297's second half named `x86_64-linux-gnu` — a hosted row on Linux and a cross row on Windows. It now uses the row that toolchain would use anyway. 298 selects the host's own architecture when a matching row exists. ⚠️ The first version of that comment said the cross row was expensive to build; measured, the whole invariants step is 85 seconds either way. The reason is representativeness, and a reason written from a guess is the kind that later talks someone out of a correct change. --- ..._naming_the_host_target_changes_nothing.sh | 36 ++++++++++-------- ...report_names_is_what_the_link_line_uses.sh | 38 ++++++++++++++++--- ...97_a_capability_pin_is_not_a_preference.sh | 27 +++++++++---- ...ding_a_convention_requires_replacing_it.sh | 27 ++++++++++--- 4 files changed, 96 insertions(+), 32 deletions(-) diff --git a/tests/e2e/295_naming_the_host_target_changes_nothing.sh b/tests/e2e/295_naming_the_host_target_changes_nothing.sh index 1a31c1d5..26b38071 100755 --- a/tests/e2e/295_naming_the_host_target_changes_nothing.sh +++ b/tests/e2e/295_naming_the_host_target_changes_nothing.sh @@ -38,19 +38,25 @@ printf '#include \nint main() { std::printf("ok\\n"); }\n' > src/main.cp # The target this host builds for when nothing is said. Taken from mcpp itself # rather than assembled from `uname`: the point is to name the target mcpp would # have chosen, and deriving it a second way would test the derivation instead. -host_target() { - # ⭐ THE MACHINE INTERFACE NAMES IT DIRECTLY. `data.host` is the host - # triple, which is what "this machine's own target" means — no column to - # locate and no note to recognise. - # - # ⚠️ THE FIRST TWO DRAFTS BOTH READ THE HUMAN TABLE AND BOTH READ IT WRONG. - # One took the `*`, which marks the default toolchain PAIR and on this - # machine sat on `aarch64-linux-musl` — a CROSS target, so the identity - # compared two different builds and skipped. The next took the `host` note, - # which is right today and is still a column position in a table formatted - # for people. - "$MCPP" toolchain list --format json 2>/dev/null \ - | jq -r '.data.host // empty' +# ⚠️⚠️ THE TARGET IS PER TOOLCHAIN, NOT PER MACHINE — AND THREE DRAFTS ASSUMED +# OTHERWISE. +# +# The identity is "naming the target this build would use anyway changes +# nothing". On Linux that is the machine's own target for every family, so +# `data.host` worked and hid the assumption. On Windows it does not: the host +# target is `x86_64-windows-msvc`, and a mingw gcc targets +# `x86_64-windows-gnu`. Measured on windows-2022: +# +# FAIL: gcc@16.1.0 — one spelling built and the other did not +# explicit: produced nothing +# +# — correct behaviour. gcc cannot emit `-msvc`, and the test had asked it to. +# +# ⭐ So ask the query what THIS toolchain resolves to with no target named, and +# then name that. Exact on every host, and it needs no table. +implicit_target() { # toolchain spec → the triple it would use anyway + "$MCPP" why toolchain --toolchain "$1" --format json 2>/dev/null \ + | jq -r '.data.triple.toolchain // empty' } ldflags_of() { # extra args… → the ldflags line, or nothing @@ -87,9 +93,9 @@ for tc in gcc llvm; do printf '[package]\nname = "idprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefault = "%s@%s"\n' \ "$tc" "$ver" > mcpp.toml - ht="$(host_target)" + ht="$(implicit_target "$tc@$ver")" if [ -z "$ht" ]; then - echo " SKIP could not read this host's own target from mcpp" + echo " SKIP the query did not name a target for $tc@$ver" continue fi diff --git a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh index 1d1bebee..de508ee8 100755 --- a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh +++ b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh @@ -82,11 +82,39 @@ for tc in gcc llvm; do want="$("$MCPP" why toolchain --toolchain "$tc@$ver" --format json 2>/dev/null \ | jq -r '.data.cLibrary.path // ""')" if [ -z "$want" ]; then - # ⚠️ EARNED: the query says mcpp passes no C-library path at all, which - # is the self-contained cross-driver arrangement (mingw g++ carries its - # own). There is nothing to look for, so there is nothing to assert. - echo " SKIP $tc@$ver: mcpp passes no C-library path for this target" - checked=$((checked-1)) + # ⭐⭐ NO PATH IS ALSO A CLAIM, AND IT IS CHECKABLE. The query says mcpp + # passes no C-library path — the self-contained arrangement, where the + # driver carries its own (mingw g++, MSVC). The assertion is then the + # other direction: nothing from OUTSIDE mcpp's store may appear either. + # + # ⚠️ THE FIRST VERSION SKIPPED HERE, and on windows-2022 both toolchains + # took that branch — so relation one had no coverage on that host at all + # and the test did not reach its conclusion. + # ⚠️⚠️ THE FLAG AND ITS PATH ARE JOINED FIRST, AND BOTH SPELLINGS EXIST. + # + # `-B/usr/lib` is one word and `-B /usr/lib` is two. Splitting on spaces + # and matching `^-B` flags the bare `-B` of the second form as a leak + # with no path in it; requiring a path in the token instead makes the + # second form invisible. Measured both, writing this line: + # + # -B /usr/lib/gcc → [-B] (false positive) + # -B /usr/lib/gcc → [] (false negative) + # + # ⭐ A false negative is the worse one here — this branch exists to + # catch a link line reaching outside mcpp's store — so the two forms are + # made one before anything is decided. + leak="$(printf '%s\n' "$ldflags" \ + | sed -E 's/(--sysroot|-isysroot|-B|-L)[[:space:]]+/\1/g' \ + | tr ' ' '\n' \ + | grep -E '^(--sysroot=?|-isysroot=?|-B|-L)[^ ]*[/\\][^ ]*$' \ + | grep -vF 'registry' | head -3)" + if [ -z "$leak" ]; then + echo " ok $tc@$ver: a self-contained driver brings in nothing external" + else + echo "FAIL: $tc@$ver: the query names no C library, and the link line reaches outside" + printf ' %s\n' $leak + fail=1 + fi continue fi if printf '%s\n' "$ldflags" | grep -qF -- "$want"; then diff --git a/tests/e2e/297_a_capability_pin_is_not_a_preference.sh b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh index 5e0e6f84..59b9a62a 100755 --- a/tests/e2e/297_a_capability_pin_is_not_a_preference.sh +++ b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh @@ -105,21 +105,34 @@ fi # ── Half two: a hosted target still honours the declaration ─────────────── # -# `x86_64-linux-musl` carries `gcc@16.1.0` in the table, so declaring gcc here -# agrees with the row — which would make the assertion vacuous. Declare it for -# a row whose pin is llvm instead, and require the declaration to win. +# ⚠️⚠️ THE TARGET COMES FROM THE QUERY, NOT FROM A LITERAL. This half used to +# name `x86_64-linux-gnu`, which is a hosted row on Linux and a CROSS row on +# Windows — where the host target is `x86_64-windows-msvc` and gcc does not +# serve it. Measured on windows-2022, the query for that combination produced +# something jq could not parse, and the test died on `jq: parse error` with no +# statement about what it had found. +# +# The claim is "a hosted row still honours a declared toolchain", so the row to +# use is the one this toolchain would use anyway. printf '[package]\nname = "capprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefault = "gcc@%s"\n' \ "$gccver" > mcpp.toml printf '#include \nint main() { std::printf("ok\\n"); }\n' > src/main.cpp +hostedTarget="$(cd "$work" && "$MCPP" why toolchain --toolchain "gcc@$gccver" \ + --format json 2>/dev/null | jq -r '.data.triple.toolchain // empty')" +if [ -z "$hostedTarget" ]; then + echo "SKIP: the query did not name a target for gcc@$gccver on this host" + exit 0 +fi rm -rf target -hosted="$(cd "$work" && "$MCPP" build --target x86_64-linux-gnu 2>&1 || true)" -hostedReason="$(cd "$work" && "$MCPP" why toolchain --target x86_64-linux-gnu \ +hosted="$(cd "$work" && "$MCPP" build --target "$hostedTarget" 2>&1 || true)" +hostedReason="$(cd "$work" && "$MCPP" why toolchain --target "$hostedTarget" \ --toolchain "gcc@$gccver" --format json 2>/dev/null \ - | jq -r '.data.reason // "-"')" + | jq -r '.data.reason // "-"' 2>/dev/null)" +[ -n "$hostedReason" ] || hostedReason="-" case "$hostedReason" in capability-pin) - echo "FAIL: a hosted target refused a declared toolchain — the escape hatch is gone" + echo "FAIL: $hostedTarget refused a declared toolchain — the escape hatch is gone" printf '%s\n' "$hosted" | head -4 | sed 's/^/ /' exit 1 ;; none) diff --git a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh index 79ef5c7d..ee894a77 100644 --- a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh +++ b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh @@ -69,11 +69,28 @@ fail=0 # The status filter matters for the same reason in the other direction: a # `planned` row refuses under a different rule, and half one would pass while # testing nothing. -pinned="$("$MCPP" toolchain list --format json 2>/dev/null \ - | jq -r '[.data.targets[] - | select(.pin | startswith("gcc")) - | select(.status != "planned") - | .target][0] // empty')" +# ⚠️ AND THE HOST'S OWN ARCHITECTURE FIRST. Selecting alphabetically picked +# `aarch64-linux-musl` on an x86_64 runner — the same rule, exercised on a +# machine that cannot run the result. +# +# ⚠️ THE FIRST VERSION OF THIS COMMENT SAID "because the cross target is +# expensive to build". Measured on ubuntu-24.04 before writing it down: the +# whole invariants step is 85 seconds either way, because openkal's packages +# arrive prebuilt. The reason is representativeness, not cost, and a reason +# written from a guess is the kind that later talks someone out of a correct +# change. +hostArch="$("$MCPP" toolchain list --format json 2>/dev/null \ + | jq -r '.data.host // "" | split("-")[0]')" +pick() { + "$MCPP" toolchain list --format json 2>/dev/null \ + | jq -r --arg a "$1" '[.data.targets[] + | select(.pin | startswith("gcc")) + | select(.status != "planned") + | select($a == "" or (.target | startswith($a + "-"))) + | .target][0] // empty' +} +pinned="$(pick "$hostArch")" +[ -n "$pinned" ] || pinned="$(pick "")" if [ -z "$pinned" ]; then echo "SKIP: no target row here pins a non-llvm toolchain" exit 0 From d5919fdd5db0c20fba54df309127ca8705cc598f Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:58:22 +0800 Subject: [PATCH 13/32] test(296): a helper ending in `&&` killed its caller, silently, on the one host where it mattered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit windows-2022: relation one printed both its `ok` lines and the script stopped. No relation two, no conclusion, no error. `build_and_read` ended on `[ -n "$f" ] && ldflags=…`. With no build.ninja the whole chain returns 1, that becomes the function's exit status, and under `set -e` the caller dies — for exactly the case the function exists to handle. The graph build legitimately produces no link line on Windows and should have reached the SKIP branch twenty lines below. ⭐ 295's `ldflags_of` had the identical line and was fixed with the identical comment earlier in the same session. Fixing one instance of a shape is not fixing the shape. ⚠️ And the shape is narrower than it looks. Measured: a FUNCTION ending in `[ … ] && …` kills its caller; the same list ending a `{ …; } > file` group does not, because errexit exempts a `&&` list whose failure came from a non-final component. 290, 292 and 293 all end such a group that way and are all called with an empty second argument — they are correct as written, and "fixing" them from a grep would have changed three working tests. --- ...report_names_is_what_the_link_line_uses.sh | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh index de508ee8..70081f3c 100755 --- a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh +++ b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh @@ -38,7 +38,35 @@ build_and_read() { # extra args… → sets $report and $ldflags report="$("$MCPP" build "$@" 2>&1 || true)" local f; f="$(find target -name build.ninja 2>/dev/null | head -1)" ldflags="" - [ -n "$f" ] && ldflags="$(grep -m1 '^ldflags' "$f" || true)" + if [ -n "$f" ]; then + ldflags="$(grep -m1 '^ldflags' "$f" || true)" + fi + # ⚠️⚠️ AN EXPLICIT `return 0`, BECAUSE THIS FUNCTION IS ALLOWED TO FIND + # NOTHING — AND THE SAME LINE HAS NOW BEEN WRONG TWICE. + # + # Written as `[ -n "$f" ] && ldflags=…`, the whole chain returns 1 when + # there is no build.ninja, that becomes the function's exit status, and + # under `set -e` the CALLER dies — silently, mid-script, for exactly the + # case the function exists to handle. + # + # ⚠️ Measured on windows-2022: relation one printed both its `ok` lines and + # the script then stopped. No relation two, no conclusion, no error. The + # graph build produced no link line there, which is a legitimate outcome + # and should have reached the `SKIP` branch twenty lines below. + # + # ⭐ 295's `ldflags_of` had the identical line and was fixed with the + # identical comment earlier in the same session. Fixing one instance of a + # shape is not fixing the shape: the sibling was three files away and + # nobody looked. + # + # ⚠️ AND THE SHAPE IS NARROWER THAN IT LOOKS — measured, not reasoned. A + # FUNCTION ending in `[ … ] && …` kills its caller; the same list ending a + # `{ …; } > file` group does NOT, because errexit exempts a `&&` list whose + # failure came from a non-final component. So 290/292/293, which all end + # such a group with `[ -n "$2" ] && printf …` and are all called with an + # empty second argument, are correct as written. Searching for the text and + # "fixing" those three would have changed three working tests. + return 0 } # The `c-abi` row's origin and the package it names, from the report. From aee50cb5209363fbea6dee8a2751189f29dc054b Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:05:43 +0800 Subject: [PATCH 14/32] ci(matrix): the exemption named a host, and the fact it stood for moved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first version let macOS skip 297 because every toolchain mcpp installs there is llvm. The very next windows-2022 run skipped for the same reason and went red: one run's restored cache held `gcc@16.1.0`, the next held only `llvm@20.1.7`. Naming the host encodes where the fact happened to hold. The skip is now accepted for a named REASON — `gcc is not installed here` — on any host, and a skip for any other reason is still a failure. ⭐ And the denominator is asserted separately: linux always has a gcc payload because it backs the host row, so that job keeps the strict check and the test is guaranteed to run somewhere on every push. A reason accepted everywhere, with nothing requiring it to be exercised anywhere, is a test that runs nowhere. --- .github/workflows/ci-target-matrix.yml | 46 +++++++++++++++----------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml index 1d496e92..9302697d 100644 --- a/.github/workflows/ci-target-matrix.yml +++ b/.github/workflows/ci-target-matrix.yml @@ -91,25 +91,30 @@ jobs: } echo " ok $1" } - # ⚠️⚠️ ONE DECLARED EXEMPTION, AND IT IS DECLARED RATHER THAN INFERRED. + # ⚠️⚠️ A SKIP IS ACCEPTED FOR ONE NAMED REASON, NOT ON ONE NAMED HOST. # - # 297 is about a bare-metal row refusing a compiler that cannot emit - # the target, so it has to DECLARE a non-llvm compiler. Every toolchain - # mcpp installs on macOS is llvm — there is no gcc payload and no MSVC - # — so on that host the convention/capability distinction has nothing - # to distinguish, and the test says so and exits 0. + # 297 declares a non-llvm compiler, so it needs one to exist. Every + # toolchain mcpp installs on macOS is llvm; on windows-2022 it depends + # on what the restored cache holds — measured, one run had + # `gcc@16.1.0` and the next had only `llvm@20.1.7`. # - # ⭐ THE EXEMPTION IS PER HOST AND SPELLED OUT. Accepting "a skip is a - # conclusion" everywhere would let a genuinely broken test pass on all - # three; the whole reason this step exists is that an exit code cannot - # tell a pass from a skip. - check_or_declared_skip() { + # ⚠️ THE FIRST VERSION EXEMPTED macOS BY NAME, and the very next + # Windows run skipped for the same reason and went red. Naming the + # host encodes where the fact happened to hold; naming the FACT holds + # wherever it does. A skip for any other reason is still a failure. + # + # ⭐ AND THE DENOMINATOR IS ASSERTED SEPARATELY: linux always has a gcc + # payload (it backs the host row), so that job uses `check` and the + # test is guaranteed to be exercised somewhere on every run. Without + # that, a reason accepted everywhere is a test that runs nowhere. + check_or_declared_skip() { # log ok-line acceptable-skip-substring grep -qF "$2" "$1".log && { echo " ok $1"; return 0; } - grep -q '^SKIP:' "$1".log && { - echo " ok $1 (declared skip on ${{ matrix.host }}: $(grep -m1 '^SKIP:' "$1".log))" + if grep -q '^SKIP:' "$1".log && grep -qF "$3" "$1".log; then + echo " ok $1 (declared skip: $(grep -m1 '^SKIP:' "$1".log))" return 0 - } - echo "::error::$1 neither concluded nor declared a skip on ${{ matrix.host }}" + fi + echo "::error::$1 neither concluded nor declared the expected skip on ${{ matrix.host }}" + echo " expected skip to mention: $3" tail -6 "$1".log 2>/dev/null | sed 's/^/ /' return 1 } @@ -118,12 +123,15 @@ jobs: "OK: naming the host's own target changes nothing" || fail=1 check 296_what_the_report_names_is_what_the_link_line_uses.sh \ "OK: what the report names is what the link line uses" || fail=1 - if [ "${{ matrix.host }}" = macos ]; then - check_or_declared_skip 297_a_capability_pin_is_not_a_preference.sh \ - "OK: a capability pin is not a preference" || fail=1 - else + if [ "${{ matrix.host }}" = linux ]; then + # The denominator: gcc is always installed here, so this host must + # actually run the test. check 297_a_capability_pin_is_not_a_preference.sh \ "OK: a capability pin is not a preference" || fail=1 + else + check_or_declared_skip 297_a_capability_pin_is_not_a_preference.sh \ + "OK: a capability pin is not a preference" \ + "gcc is not installed here" || fail=1 fi check 298_overriding_a_convention_requires_replacing_it.sh \ "OK: a convention may be overridden, but not merely removed" || fail=1 From 19dcfe4ab401e6a3637d5d9fb2860414e0eab181 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:08:59 +0800 Subject: [PATCH 15/32] docs(16): the build host is a third axis, and it decides which rows exist at all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two axes the chapter already had — which compiler, and where the C library comes from — are choices a project makes. The machine the build runs on is not, and `mcpp toolchain list` reports only the rows it can reach: 12 of the target table's 14 on Linux, with `x86_64-windows-msvc` and `aarch64-macos` correctly absent. States the rule in six lines, gives the full 14-row table with a column per host, and names which CI runner is which — including that `macos-14` resolves `arm64-apple-darwin23.6.0`, ARM rather than x86_64, which is the fact three of this branch's criteria assumed away. ⚠️ And says why the cell count is not a constant: it follows from what that machine has installed, and the same runner has been measured with different toolchains on consecutive runs. The comparison therefore asserts that the scan produced rows and that every row the expected table names was reached — a cell that vanishes because a payload was not restored is otherwise indistinguishable from a cell that passed. --- docs/16-the-target-triple.md | 74 +++++++++++++++++++++++++++++++++ docs/zh/16-the-target-triple.md | 67 +++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) diff --git a/docs/16-the-target-triple.md b/docs/16-the-target-triple.md index abe52282..9e9e219a 100644 --- a/docs/16-the-target-triple.md +++ b/docs/16-the-target-triple.md @@ -348,6 +348,80 @@ is in use. A payload for it does not exist on any host; its system can only come from a dependency graph, which is what `toolchain list` reports as `via dependency graph`. +## The Build Host Is A Third Axis + +The two axes above — which compiler, and where the C library comes from — are +choices a project makes. The third is not: it is the machine the build runs on, +and it decides which rows are reachable at all. + +`mcpp toolchain list` reports only the rows this host can reach. On Linux that +is 12 of the 14 rows in the target table; `x86_64-windows-msvc` and +`aarch64-macos` are absent, and correctly so — MSVC and the macOS SDK belong to +their own machines and no dependency substitutes for them. + +### The rule, in five lines + +| target class | which hosts serve it | why | +|---|---|---| +| `*-linux-musl` | Linux (any arch), Windows (same arch only) | the musl payloads are self-contained | +| `*-linux-gnu` | Linux, same arch only | additionally needs the host-native `xim:glibc` / `xim:linux-headers` | +| `x86_64-windows-gnu` | Linux, Windows | one identity, host-split at the distribution layer | +| `x86_64-windows-{msvc,musl}` | Windows | MSVC is located on the machine; no gcc emits PE+musl | +| `aarch64-macos` | macOS | the SDK is the machine's | +| `*-none-elf` | every host | clang and lld are cross-compilers by construction | + +⚠️ **A row absent from this host is not a row that cannot be built here.** The +refusal `host_can_serve` produces is about payloads, and a dependency graph can +supply the system instead — which is why `x86_64-windows-musl` reads +`via dependency graph` on Linux and builds a real PE32+ there. + +### The full table + +Tier and pin are properties of the row and do not vary by host. The three host +columns are what `mcpp toolchain list` reports there. + +| target | tier | pin | linux | macOS | Windows | +|---|---|---|---|---|---| +| `x86_64-linux-gnu` | verified | — | installed | — | — | +| `x86_64-linux-musl` | verified | `gcc@16.1.0` | installed | — | installed | +| `aarch64-linux-musl` | verified | `gcc@16.1.0` | installed | — | — | +| `riscv64-linux-musl` | planned | — | planned | planned | planned | +| `aarch64-linux-gnu` | planned | — | planned | planned | planned | +| `x86_64-windows-gnu` | verified | `gcc@16.1.0` | installed | — | installed | +| `x86_64-windows-musl` | preview | `llvm@22.1.8` | **via dependency graph** | — | installed | +| `x86_64-windows-msvc` | verified | — | — | — | installed | +| `aarch64-macos` | verified | — | — | installed | — | +| `x86_64-macos` | planned | — | planned | planned | planned | +| `riscv64-none-elf` | verified | `llvm@22.1.8` | available | available | available | +| `riscv32-none-elf` | verified | `llvm@22.1.8` | available | available | available | +| `aarch64-none-elf` | preview | `llvm@22.1.8` | available | available | available | +| `x86_64-none-elf` | preview | `llvm@22.1.8` | available | available | available | + +The linux column is measured. The other two follow from the rule above and are +confirmed by the CI scan described next. + +### And CI measures it, on all three + +`.github/workflows/ci-target-matrix.yml` runs on three runners, and what each +one resolves as its own target is not what its name suggests: + +| runner | host column | host target it resolves | +|---|---|---| +| `ubuntu-24.04` | linux | `x86_64-unknown-linux-gnu` | +| `macos-14` | macOS | `arm64-apple-darwin23.6.0` — **ARM**, not x86_64 | +| `windows-2022` | Windows | `x86_64-pc-windows-msvc` | + +Each runner scans every row it lists, under both systems — the payload alone, +and again with `openkal-musl` + `openkal-llvm-runtime` in the graph — and the +result is compared against [`tests/matrix/expected.tsv`](../tests/matrix/expected.tsv). + +⚠️ **The cell count is not a constant.** It follows from what that machine has +installed, and the same runner has been observed with different toolchains on +consecutive runs. That is why the comparison asserts *the scan produced rows* +and *every row the expected table names was reached*, rather than a total: a +cell that vanishes because a payload was not restored is indistinguishable from +a cell that passed, by exit code alone. + ## Custom Targets A triple outside mcpp's table needs an explicit section, which is also how a diff --git a/docs/zh/16-the-target-triple.md b/docs/zh/16-the-target-triple.md index 70656bdd..5b3bbdc7 100644 --- a/docs/zh/16-the-target-triple.md +++ b/docs/zh/16-the-target-triple.md @@ -311,6 +311,73 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— 全部差别就在于用的是哪个 C 库。任何宿主都没有为它准备的载荷;它的系统只能来自依赖 图,这正是 `toolchain list` 报的 `via dependency graph`。 +## 构建机是第三条轴 + +上面两条轴 —— 用哪个编译器、C 库从哪来 —— 是工程做的选择。第三条不是:它是构建 +运行在哪台机器上,而它决定了**哪些行根本够得着**。 + +`mcpp toolchain list` 只报告这台宿主够得着的行。Linux 上是目标表 14 行里的 12 行; +`x86_64-windows-msvc` 与 `aarch64-macos` 缺席,而这是对的 —— MSVC 与 macOS SDK +属于它们自己的机器,没有依赖能替代。 + +### 规则,五行说完 + +| 目标类别 | 哪些宿主服务它 | 为什么 | +|---|---|---| +| `*-linux-musl` | Linux(任意架构)、Windows(仅同架构) | musl 载荷是自足的 | +| `*-linux-gnu` | Linux,且仅同架构 | 还需要本机架构的 `xim:glibc` / `xim:linux-headers` | +| `x86_64-windows-gnu` | Linux、Windows | 一个身份,只在分发层按宿主分岔 | +| `x86_64-windows-{msvc,musl}` | Windows | MSVC 是在机器上被找到的;没有 gcc 发得出 PE+musl | +| `aarch64-macos` | macOS | SDK 是那台机器的 | +| `*-none-elf` | 每一台 | clang 与 lld 按构造就是交叉编译器 | + +⚠️ **一行在这台宿主缺席,不等于这台宿主建不出它。** `host_can_serve` 给出的拒绝 +讲的是**载荷**,而依赖图可以改为供给系统 —— 这就是 `x86_64-windows-musl` 在 Linux +上显示 `via dependency graph`、并且在那里真的产出 PE32+ 的原因。 + +### 完整表格 + +tier 与 pin 是行的属性,不随宿主变。三个宿主列是 `mcpp toolchain list` 在那里的报告。 + +| target | tier | pin | linux | macOS | Windows | +|---|---|---|---|---|---| +| `x86_64-linux-gnu` | verified | — | installed | — | — | +| `x86_64-linux-musl` | verified | `gcc@16.1.0` | installed | — | installed | +| `aarch64-linux-musl` | verified | `gcc@16.1.0` | installed | — | — | +| `riscv64-linux-musl` | planned | — | planned | planned | planned | +| `aarch64-linux-gnu` | planned | — | planned | planned | planned | +| `x86_64-windows-gnu` | verified | `gcc@16.1.0` | installed | — | installed | +| `x86_64-windows-musl` | preview | `llvm@22.1.8` | **via dependency graph** | — | installed | +| `x86_64-windows-msvc` | verified | — | — | — | installed | +| `aarch64-macos` | verified | — | — | installed | — | +| `x86_64-macos` | planned | — | planned | planned | planned | +| `riscv64-none-elf` | verified | `llvm@22.1.8` | available | available | available | +| `riscv32-none-elf` | verified | `llvm@22.1.8` | available | available | available | +| `aarch64-none-elf` | preview | `llvm@22.1.8` | available | available | available | +| `x86_64-none-elf` | preview | `llvm@22.1.8` | available | available | available | + +linux 一列是实测的。另外两列由上面的规则给出,并由下面这套 CI 扫描确认。 + +### 而 CI 在三台上测它 + +`.github/workflows/ci-target-matrix.yml` 跑在三台 runner 上,而每台把什么解析成 +自己的目标,与它的名字给人的印象并不一致: + +| runner | 宿主列 | 它解析出的宿主目标 | +|---|---|---| +| `ubuntu-24.04` | linux | `x86_64-unknown-linux-gnu` | +| `macos-14` | macOS | `arm64-apple-darwin23.6.0` —— **ARM**,不是 x86_64 | +| `windows-2022` | Windows | `x86_64-pc-windows-msvc` | + +每台扫描它列出的每一行,两种体系各一遍 —— 只有载荷,以及图里加上 +`openkal-musl` + `openkal-llvm-runtime` —— 结果与 +[`tests/matrix/expected.tsv`](../../tests/matrix/expected.tsv) 比对。 + +⚠️ **格数不是常数。** 它取决于那台机器装了什么,而同一台 runner 在相邻两轮里被 +观察到工具链不同。所以比对断言的是**扫描真的产出了行**、以及**期望表点名的每一行 +都被跑到**,而不是一个总数:一格因为载荷没被恢复而消失,与一格通过了,在退出码上 +没有区别。 + ## 自定义目标 不在 mcpp 表内的三元组需要一个显式段落,而这也是一块板子声明 From 7fac046bce46fcd67b12f052ccee81f94ee42cab Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:16:27 +0800 Subject: [PATCH 16/32] ci(matrix): the build-host axis is the four hosts mcpp ships for, and it is (os, arch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The axis was three OS names, chosen because three runners were at hand. `release.yml` publishes four host binaries — linux-x86_64, linux-aarch64, macosx-arm64, windows-x86_64 — and a host mcpp is distributed for but never scanned is a host whose target table is a claim nobody checked. aarch64 Linux has been supported since before this branch and had no target-matrix coverage. ⚠️ And the key is (os, arch), not os. The two Linux hosts do not serve the same rows: `x86_64-linux-gnu` needs the host-native `xim:glibc` and `xim:linux-headers` payloads, which exist for the host's own architecture only, so it is reachable from one and not the other. Under a single `linux` key the second scan to finish would explain away the first one's rows, and which one wins depends on scheduling rather than on correctness. `ubuntu-24.04-arm` is the runner; `ci-aarch64-fresh-install.yml` has used it since it shipped. docs/16 now carries the full 14-row table with a column per build host, the rule behind the columns, and which runner resolves which host target — including that `macos-14` is ARM and `windows-2022` resolves `-msvc` while a mingw gcc there targets `-gnu`. Three of this branch's criteria assumed the Linux coincidence and had to be corrected on the other hosts. --- .github/workflows/ci-target-matrix.yml | 36 +++++-- docs/16-the-target-triple.md | 135 ++++++++++++++----------- docs/zh/16-the-target-triple.md | 115 ++++++++++++--------- tests/matrix/expected.tsv | 92 +++++++++-------- tests/matrix/scan.sh | 18 +++- 5 files changed, 236 insertions(+), 160 deletions(-) diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml index 9302697d..9fba1317 100644 --- a/.github/workflows/ci-target-matrix.yml +++ b/.github/workflows/ci-target-matrix.yml @@ -29,9 +29,20 @@ jobs: fail-fast: false matrix: include: - - { host: linux, runner: ubuntu-24.04 } - - { host: macos, runner: macos-14 } - - { host: windows, runner: windows-2022 } + # ⭐⭐ THE BUILD-HOST AXIS IS THE SET mcpp SHIPS FOR, NOT THE SET THAT + # WAS CONVENIENT. `release.yml` publishes four: linux-x86_64, + # linux-aarch64, macosx-arm64, windows-x86_64. A host mcpp is + # distributed for and never scanned is a host whose target table is + # a claim nobody checked. + # + # ⚠️ `host` IS (os, arch) AND NOT os. Two Linux hosts differ in which + # rows they serve — `x86_64-linux-gnu` needs the host-native glibc + # payload, so it is reachable on one and not the other — and a single + # `linux` key would have them overwrite each other in expected.tsv. + - { host: linux-x86_64, runner: ubuntu-24.04 } + - { host: linux-aarch64, runner: ubuntu-24.04-arm } + - { host: macos-arm64, runner: macos-14 } + - { host: windows-x86_64, runner: windows-2022 } defaults: run: shell: bash @@ -123,7 +134,7 @@ jobs: "OK: naming the host's own target changes nothing" || fail=1 check 296_what_the_report_names_is_what_the_link_line_uses.sh \ "OK: what the report names is what the link line uses" || fail=1 - if [ "${{ matrix.host }}" = linux ]; then + if [ "${{ matrix.host }}" = linux-x86_64 ]; then # The denominator: gcc is always installed here, so this host must # actually run the test. check 297_a_capability_pin_is_not_a_preference.sh \ @@ -147,9 +158,20 @@ jobs: fail-fast: false matrix: include: - - { host: linux, runner: ubuntu-24.04 } - - { host: macos, runner: macos-14 } - - { host: windows, runner: windows-2022 } + # ⭐⭐ THE BUILD-HOST AXIS IS THE SET mcpp SHIPS FOR, NOT THE SET THAT + # WAS CONVENIENT. `release.yml` publishes four: linux-x86_64, + # linux-aarch64, macosx-arm64, windows-x86_64. A host mcpp is + # distributed for and never scanned is a host whose target table is + # a claim nobody checked. + # + # ⚠️ `host` IS (os, arch) AND NOT os. Two Linux hosts differ in which + # rows they serve — `x86_64-linux-gnu` needs the host-native glibc + # payload, so it is reachable on one and not the other — and a single + # `linux` key would have them overwrite each other in expected.tsv. + - { host: linux-x86_64, runner: ubuntu-24.04 } + - { host: linux-aarch64, runner: ubuntu-24.04-arm } + - { host: macos-arm64, runner: macos-14 } + - { host: windows-x86_64, runner: windows-2022 } defaults: run: shell: bash diff --git a/docs/16-the-target-triple.md b/docs/16-the-target-triple.md index 9e9e219a..967f879c 100644 --- a/docs/16-the-target-triple.md +++ b/docs/16-the-target-triple.md @@ -351,79 +351,96 @@ from a dependency graph, which is what `toolchain list` reports as ## The Build Host Is A Third Axis The two axes above — which compiler, and where the C library comes from — are -choices a project makes. The third is not: it is the machine the build runs on, -and it decides which rows are reachable at all. +choices a project makes. The third is not: it is the machine the build runs on. -`mcpp toolchain list` reports only the rows this host can reach. On Linux that -is 12 of the 14 rows in the target table; `x86_64-windows-msvc` and -`aarch64-macos` are absent, and correctly so — MSVC and the macOS SDK belong to -their own machines and no dependency substitutes for them. +⭐ **The axis is the set mcpp ships for, and it is (os, arch) rather than os.** +`release.yml` publishes four host binaries: -### The rule, in five lines - -| target class | which hosts serve it | why | +| build host | release asset | CI runner | +|---|---|---| +| `linux-x86_64` | `mcpp--linux-x86_64.tar.gz` | `ubuntu-24.04` | +| `linux-aarch64` | `mcpp--linux-aarch64.tar.gz` | `ubuntu-24.04-arm` | +| `macos-arm64` | `mcpp--macosx-arm64.tar.gz` | `macos-14` | +| `windows-x86_64` | `mcpp--windows-x86_64.zip` | `windows-2022` | + +⚠️ **The two Linux hosts are not one host.** `x86_64-linux-gnu` needs the +host-native `xim:glibc` and `xim:linux-headers` payloads, which exist for the +host's own architecture only — so that row is reachable from `linux-x86_64` and +not from `linux-aarch64`, while `aarch64-linux-gnu` is the mirror case and is +`planned` on both. Collapsing them to `linux` would let one overwrite the +other's rows. + +### Which build host serves which target + +| target | tier | pin | linux-x86_64 | linux-aarch64 | macos-arm64 | windows-x86_64 | +|---|---|---|---|---|---|---| +| `x86_64-linux-gnu` | verified | — | ✅ payload | — | — | — | +| `aarch64-linux-gnu` | planned | — | planned | planned | planned | planned | +| `x86_64-linux-musl` | verified | `gcc@16.1.0` | ✅ payload | ✅ payload | — | ✅ payload | +| `aarch64-linux-musl` | verified | `gcc@16.1.0` | ✅ payload | ✅ payload | — | — | +| `riscv64-linux-musl` | planned | — | planned | planned | planned | planned | +| `x86_64-windows-gnu` | verified | `gcc@16.1.0` | ✅ payload | ✅ payload | — | ✅ payload | +| `x86_64-windows-musl` | preview | `llvm@22.1.8` | ⚙ graph | ⚙ graph | ⚙ graph | ✅ payload | +| `x86_64-windows-msvc` | verified | — | — | — | — | ✅ system | +| `aarch64-macos` | verified | — | — | — | ✅ SDK | — | +| `x86_64-macos` | planned | — | planned | planned | planned | planned | +| `riscv64-none-elf` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `riscv32-none-elf` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `aarch64-none-elf` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `x86_64-none-elf` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | + +`✅ payload` a toolchain payload here produces it · `⚙ graph` no payload, but a +dependency can supply the system · `✅ system` located on the machine, not +installed by mcpp · `✅ SDK` the platform's own · `—` unreachable from this host +· `planned` registered in the vocabulary, nothing wired yet. + +### The rule behind the columns + +| target class | which build hosts serve it | why | |---|---|---| | `*-linux-musl` | Linux (any arch), Windows (same arch only) | the musl payloads are self-contained | | `*-linux-gnu` | Linux, same arch only | additionally needs the host-native `xim:glibc` / `xim:linux-headers` | | `x86_64-windows-gnu` | Linux, Windows | one identity, host-split at the distribution layer | -| `x86_64-windows-{msvc,musl}` | Windows | MSVC is located on the machine; no gcc emits PE+musl | +| `x86_64-windows-msvc` | Windows | MSVC is located on the machine | +| `x86_64-windows-musl` | Windows by payload; anywhere by graph | no gcc emits PE+musl, and LLVM cannot spell the triple | | `aarch64-macos` | macOS | the SDK is the machine's | | `*-none-elf` | every host | clang and lld are cross-compilers by construction | -⚠️ **A row absent from this host is not a row that cannot be built here.** The -refusal `host_can_serve` produces is about payloads, and a dependency graph can -supply the system instead — which is why `x86_64-windows-musl` reads -`via dependency graph` on Linux and builds a real PE32+ there. - -### The full table - -Tier and pin are properties of the row and do not vary by host. The three host -columns are what `mcpp toolchain list` reports there. - -| target | tier | pin | linux | macOS | Windows | -|---|---|---|---|---|---| -| `x86_64-linux-gnu` | verified | — | installed | — | — | -| `x86_64-linux-musl` | verified | `gcc@16.1.0` | installed | — | installed | -| `aarch64-linux-musl` | verified | `gcc@16.1.0` | installed | — | — | -| `riscv64-linux-musl` | planned | — | planned | planned | planned | -| `aarch64-linux-gnu` | planned | — | planned | planned | planned | -| `x86_64-windows-gnu` | verified | `gcc@16.1.0` | installed | — | installed | -| `x86_64-windows-musl` | preview | `llvm@22.1.8` | **via dependency graph** | — | installed | -| `x86_64-windows-msvc` | verified | — | — | — | installed | -| `aarch64-macos` | verified | — | — | installed | — | -| `x86_64-macos` | planned | — | planned | planned | planned | -| `riscv64-none-elf` | verified | `llvm@22.1.8` | available | available | available | -| `riscv32-none-elf` | verified | `llvm@22.1.8` | available | available | available | -| `aarch64-none-elf` | preview | `llvm@22.1.8` | available | available | available | -| `x86_64-none-elf` | preview | `llvm@22.1.8` | available | available | available | - -The linux column is measured. The other two follow from the rule above and are -confirmed by the CI scan described next. - -### And CI measures it, on all three - -`.github/workflows/ci-target-matrix.yml` runs on three runners, and what each -one resolves as its own target is not what its name suggests: - -| runner | host column | host target it resolves | -|---|---|---| -| `ubuntu-24.04` | linux | `x86_64-unknown-linux-gnu` | -| `macos-14` | macOS | `arm64-apple-darwin23.6.0` — **ARM**, not x86_64 | -| `windows-2022` | Windows | `x86_64-pc-windows-msvc` | +⚠️ **A `—` is about payloads, not about possibility.** `host_can_serve` answers +"does a payload here produce it", and a dependency graph can supply the system +instead — which is why `x86_64-windows-musl` reads `via dependency graph` on +Linux and produces a real PE32+ there. -Each runner scans every row it lists, under both systems — the payload alone, -and again with `openkal-musl` + `openkal-llvm-runtime` in the graph — and the -result is compared against [`tests/matrix/expected.tsv`](../tests/matrix/expected.tsv). +### And CI measures every one of them -⚠️ **The cell count is not a constant.** It follows from what that machine has -installed, and the same runner has been observed with different toolchains on -consecutive runs. That is why the comparison asserts *the scan produced rows* -and *every row the expected table names was reached*, rather than a total: a -cell that vanishes because a payload was not restored is indistinguishable from -a cell that passed, by exit code alone. +[`ci-target-matrix.yml`](../.github/workflows/ci-target-matrix.yml) runs on all +four hosts. Each scans every row it lists, twice — the payload alone, then with +`openkal-musl` + `openkal-llvm-runtime` in the graph — and compares against +[`tests/matrix/expected.tsv`](../tests/matrix/expected.tsv), which is keyed on +`(mode, host, target, compiler)`. + +⚠️ What a runner resolves as its own target is not what its name suggests: + +| runner | host target it resolves | +|---|---| +| `ubuntu-24.04` | `x86_64-unknown-linux-gnu` | +| `ubuntu-24.04-arm` | `aarch64-unknown-linux-gnu` | +| `macos-14` | `arm64-apple-darwin23.6.0` — **ARM**, not x86_64 | +| `windows-2022` | `x86_64-pc-windows-msvc` — **msvc**, while a mingw gcc there targets `-gnu` | + +Three of this chapter's criteria assumed the Linux coincidence and had to be +corrected on the other hosts. + +⚠️ **The cell count per host is not a constant.** It follows from what that +machine has installed, and the same runner has been measured with different +toolchains on consecutive runs. The comparison therefore asserts *the scan +produced rows* and *every row the expected table names was reached*, rather than +a total: a cell that vanishes because a payload was not restored is otherwise +indistinguishable from a cell that passed. ## Custom Targets + A triple outside mcpp's table needs an explicit section, which is also how a board declares facts no default can supply: diff --git a/docs/zh/16-the-target-triple.md b/docs/zh/16-the-target-triple.md index 5b3bbdc7..0e8a32fa 100644 --- a/docs/zh/16-the-target-triple.md +++ b/docs/zh/16-the-target-triple.md @@ -314,72 +314,89 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— ## 构建机是第三条轴 上面两条轴 —— 用哪个编译器、C 库从哪来 —— 是工程做的选择。第三条不是:它是构建 -运行在哪台机器上,而它决定了**哪些行根本够得着**。 +运行在哪台机器上。 -`mcpp toolchain list` 只报告这台宿主够得着的行。Linux 上是目标表 14 行里的 12 行; -`x86_64-windows-msvc` 与 `aarch64-macos` 缺席,而这是对的 —— MSVC 与 macOS SDK -属于它们自己的机器,没有依赖能替代。 +⭐ **这条轴是 mcpp 自己发布的那一组,而且是 (os, arch) 不是 os。** +`release.yml` 发布四份宿主二进制: -### 规则,五行说完 - -| 目标类别 | 哪些宿主服务它 | 为什么 | +| 构建机 | 发布资产 | CI runner | +|---|---|---| +| `linux-x86_64` | `mcpp--linux-x86_64.tar.gz` | `ubuntu-24.04` | +| `linux-aarch64` | `mcpp--linux-aarch64.tar.gz` | `ubuntu-24.04-arm` | +| `macos-arm64` | `mcpp--macosx-arm64.tar.gz` | `macos-14` | +| `windows-x86_64` | `mcpp--windows-x86_64.zip` | `windows-2022` | + +⚠️ **两台 Linux 不是同一台。** `x86_64-linux-gnu` 需要本机架构的 `xim:glibc` 与 +`xim:linux-headers` 载荷,而它们只为宿主自己的架构存在 —— 所以那一行从 +`linux-x86_64` 够得着,从 `linux-aarch64` 够不着;`aarch64-linux-gnu` 是镜像的 +情形,两台上都是 `planned`。把它们并成 `linux`,一台会把另一台的行覆盖掉。 + +### 哪台构建机服务哪个目标 + +| target | tier | pin | linux-x86_64 | linux-aarch64 | macos-arm64 | windows-x86_64 | +|---|---|---|---|---|---|---| +| `x86_64-linux-gnu` | verified | — | ✅ 载荷 | — | — | — | +| `aarch64-linux-gnu` | planned | — | planned | planned | planned | planned | +| `x86_64-linux-musl` | verified | `gcc@16.1.0` | ✅ 载荷 | ✅ 载荷 | — | ✅ 载荷 | +| `aarch64-linux-musl` | verified | `gcc@16.1.0` | ✅ 载荷 | ✅ 载荷 | — | — | +| `riscv64-linux-musl` | planned | — | planned | planned | planned | planned | +| `x86_64-windows-gnu` | verified | `gcc@16.1.0` | ✅ 载荷 | ✅ 载荷 | — | ✅ 载荷 | +| `x86_64-windows-musl` | preview | `llvm@22.1.8` | ⚙ 图 | ⚙ 图 | ⚙ 图 | ✅ 载荷 | +| `x86_64-windows-msvc` | verified | — | — | — | — | ✅ 系统 | +| `aarch64-macos` | verified | — | — | — | ✅ SDK | — | +| `x86_64-macos` | planned | — | planned | planned | planned | planned | +| `riscv64-none-elf` | verified | `llvm@22.1.8` | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | +| `riscv32-none-elf` | verified | `llvm@22.1.8` | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | +| `aarch64-none-elf` | preview | `llvm@22.1.8` | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | +| `x86_64-none-elf` | preview | `llvm@22.1.8` | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | + +`✅ 载荷` 这里有工具链载荷产出它 · `⚙ 图` 没有载荷,但依赖可以供给系统 · +`✅ 系统` 在机器上被找到,不是 mcpp 装的 · `✅ SDK` 平台自己的 · +`—` 从这台宿主够不着 · `planned` 词表里有,还没有任何东西接线。 + +### 列背后的规则 + +| 目标类别 | 哪些构建机服务它 | 为什么 | |---|---|---| | `*-linux-musl` | Linux(任意架构)、Windows(仅同架构) | musl 载荷是自足的 | -| `*-linux-gnu` | Linux,且仅同架构 | 还需要本机架构的 `xim:glibc` / `xim:linux-headers` | +| `*-linux-gnu` | Linux,且仅同架构 | 还要本机架构的 `xim:glibc` / `xim:linux-headers` | | `x86_64-windows-gnu` | Linux、Windows | 一个身份,只在分发层按宿主分岔 | -| `x86_64-windows-{msvc,musl}` | Windows | MSVC 是在机器上被找到的;没有 gcc 发得出 PE+musl | +| `x86_64-windows-msvc` | Windows | MSVC 是在机器上被找到的 | +| `x86_64-windows-musl` | 载荷只在 Windows;走图则任意宿主 | 没有 gcc 发得出 PE+musl,而 LLVM 拼不出这个三元组 | | `aarch64-macos` | macOS | SDK 是那台机器的 | | `*-none-elf` | 每一台 | clang 与 lld 按构造就是交叉编译器 | -⚠️ **一行在这台宿主缺席,不等于这台宿主建不出它。** `host_can_serve` 给出的拒绝 -讲的是**载荷**,而依赖图可以改为供给系统 —— 这就是 `x86_64-windows-musl` 在 Linux -上显示 `via dependency graph`、并且在那里真的产出 PE32+ 的原因。 - -### 完整表格 - -tier 与 pin 是行的属性,不随宿主变。三个宿主列是 `mcpp toolchain list` 在那里的报告。 +⚠️ **一个 `—` 讲的是载荷,不是可能性。** `host_can_serve` 回答的是「这里有没有 +载荷产出它」,而依赖图可以改为供给系统 —— 这就是 `x86_64-windows-musl` 在 Linux +上显示 `via dependency graph`、并在那里产出真正的 PE32+ 的原因。 -| target | tier | pin | linux | macOS | Windows | -|---|---|---|---|---|---| -| `x86_64-linux-gnu` | verified | — | installed | — | — | -| `x86_64-linux-musl` | verified | `gcc@16.1.0` | installed | — | installed | -| `aarch64-linux-musl` | verified | `gcc@16.1.0` | installed | — | — | -| `riscv64-linux-musl` | planned | — | planned | planned | planned | -| `aarch64-linux-gnu` | planned | — | planned | planned | planned | -| `x86_64-windows-gnu` | verified | `gcc@16.1.0` | installed | — | installed | -| `x86_64-windows-musl` | preview | `llvm@22.1.8` | **via dependency graph** | — | installed | -| `x86_64-windows-msvc` | verified | — | — | — | installed | -| `aarch64-macos` | verified | — | — | installed | — | -| `x86_64-macos` | planned | — | planned | planned | planned | -| `riscv64-none-elf` | verified | `llvm@22.1.8` | available | available | available | -| `riscv32-none-elf` | verified | `llvm@22.1.8` | available | available | available | -| `aarch64-none-elf` | preview | `llvm@22.1.8` | available | available | available | -| `x86_64-none-elf` | preview | `llvm@22.1.8` | available | available | available | +### 而 CI 把每一台都测了 -linux 一列是实测的。另外两列由上面的规则给出,并由下面这套 CI 扫描确认。 +[`ci-target-matrix.yml`](../../.github/workflows/ci-target-matrix.yml) 在全部四台 +宿主上跑。每台扫描它列出的每一行两遍 —— 只有载荷,以及图里加上 +`openkal-musl` + `openkal-llvm-runtime` —— 与 +[`tests/matrix/expected.tsv`](../../tests/matrix/expected.tsv) 比对,键是 +`(mode, host, target, compiler)`。 -### 而 CI 在三台上测它 +⚠️ 每台把什么解析成自己的目标,与它的名字给人的印象并不一致: -`.github/workflows/ci-target-matrix.yml` 跑在三台 runner 上,而每台把什么解析成 -自己的目标,与它的名字给人的印象并不一致: - -| runner | 宿主列 | 它解析出的宿主目标 | -|---|---|---| -| `ubuntu-24.04` | linux | `x86_64-unknown-linux-gnu` | -| `macos-14` | macOS | `arm64-apple-darwin23.6.0` —— **ARM**,不是 x86_64 | -| `windows-2022` | Windows | `x86_64-pc-windows-msvc` | +| runner | 它解析出的宿主目标 | +|---|---| +| `ubuntu-24.04` | `x86_64-unknown-linux-gnu` | +| `ubuntu-24.04-arm` | `aarch64-unknown-linux-gnu` | +| `macos-14` | `arm64-apple-darwin23.6.0` —— **ARM**,不是 x86_64 | +| `windows-2022` | `x86_64-pc-windows-msvc` —— **msvc**,而那里的 mingw gcc 目标是 `-gnu` | -每台扫描它列出的每一行,两种体系各一遍 —— 只有载荷,以及图里加上 -`openkal-musl` + `openkal-llvm-runtime` —— 结果与 -[`tests/matrix/expected.tsv`](../../tests/matrix/expected.tsv) 比对。 +本章三条判据都曾假设了 Linux 上的那个巧合,并在其它宿主上被纠正。 -⚠️ **格数不是常数。** 它取决于那台机器装了什么,而同一台 runner 在相邻两轮里被 -观察到工具链不同。所以比对断言的是**扫描真的产出了行**、以及**期望表点名的每一行 -都被跑到**,而不是一个总数:一格因为载荷没被恢复而消失,与一格通过了,在退出码上 -没有区别。 +⚠️ **每台的格数不是常数。** 它取决于那台机器装了什么,而同一台 runner 在相邻两轮 +里被测到工具链不同。所以比对断言的是**扫描真的产出了行**、以及**期望表点名的每一 +行都被跑到**,而不是一个总数:一格因为载荷没被恢复而消失,与一格通过了,在退出码 +上没有区别。 ## 自定义目标 + 不在 mcpp 表内的三元组需要一个显式段落,而这也是一块板子声明 「任何默认值都给不出的事实」的方式: diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index bed20a4a..57b352f4 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -25,46 +25,52 @@ # status 仍是 unsupported —— 那是两条不同的规则,换了一条却不红,这张表就 # 只在说「它没建出来」,不在说「为什么」。 # -# linux 行:2026-08-26 实测于 x86_64 Linux(mcpp 2026.8.26.1)。 -# windows / macos 行:留待各自宿主的第一次 CI 运行回填 —— ⚠️ 从别的宿主推断出来 -# 的一行,断言的是推断而不是那台机器。 -payload linux aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned -payload linux aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none -payload linux aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned -payload linux riscv64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux x86_64-linux-gnu gcc@16.1.0 x86_64-unknown-linux-gnu subos gnu(payload) libstdc++(payload) - ok none -payload linux x86_64-linux-musl gcc@16.1.0 x86_64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none -payload linux x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -payload linux x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux x86_64-windows-gnu gcc@16.1.0 x86_64-w64-windows-gnu none gnu(payload) libstdc++(payload) - ok none -payload linux x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin -payload linux aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned -payload linux aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced -payload linux aarch64-none-elf llvm@22.1.8 aarch64-none-elf payload - - - ok none -payload linux riscv32-none-elf llvm@22.1.8 riscv32-none-elf payload - - - ok none -payload linux riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned -payload linux riscv64-none-elf llvm@22.1.8 riscv64-none-elf payload - - - ok none -payload linux x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload gnu(payload) libc++(payload) - ok none -payload linux x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced -payload linux x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned -payload linux x86_64-none-elf llvm@22.1.8 x86_64-none-elf payload - - - ok none -payload linux x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced -payload linux x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve -graph linux aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned -graph linux aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned -graph linux x86_64-linux-gnu gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux x86_64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -graph linux x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin -graph linux aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned -graph linux aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned -graph linux x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned -graph linux x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +# ⚠️⚠️ host 是 (os, arch),不是 os。mcpp 自己发布四份宿主二进制: +# linux-x86_64 / linux-aarch64 / macos-arm64 / windows-x86_64 +# 两台 Linux 服务的行不一样 —— `x86_64-linux-gnu` 需要本机架构的 glibc 载荷, +# 所以在 x86_64 上够得着、在 aarch64 上够不着。只写 `linux` 会让后跑的一台把先跑 +# 的那台在这张表里的行「解释掉」,而覆盖的方向取决于谁后跑,不取决于谁对。 +# +# linux-x86_64 行:2026-08-26 实测(mcpp 2026.8.26.1)。 +# linux-aarch64 / macos-arm64 / windows-x86_64 行:留待各自宿主的第一次 CI 运行 +# 回填 —— ⚠️ 从别的宿主推断出来的一行,断言的是推断而不是那台机器。 +payload linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned +payload linux-x86_64 aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none +payload linux-x86_64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned +payload linux-x86_64 riscv64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 x86_64-linux-gnu gcc@16.1.0 x86_64-unknown-linux-gnu subos gnu(payload) libstdc++(payload) - ok none +payload linux-x86_64 x86_64-linux-musl gcc@16.1.0 x86_64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none +payload linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned +payload linux-x86_64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 x86_64-windows-gnu gcc@16.1.0 x86_64-w64-windows-gnu none gnu(payload) libstdc++(payload) - ok none +payload linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +payload linux-x86_64 aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux-x86_64 aarch64-none-elf llvm@22.1.8 aarch64-none-elf payload - - - ok none +payload linux-x86_64 riscv32-none-elf llvm@22.1.8 riscv32-none-elf payload - - - ok none +payload linux-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +payload linux-x86_64 riscv64-none-elf llvm@22.1.8 riscv64-none-elf payload - - - ok none +payload linux-x86_64 x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload gnu(payload) libc++(payload) - ok none +payload linux-x86_64 x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +payload linux-x86_64 x86_64-none-elf llvm@22.1.8 x86_64-none-elf payload - - - ok none +payload linux-x86_64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux-x86_64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve +graph linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-x86_64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-linux-gnu gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin +graph linux-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +graph linux-x86_64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh index d544eaa2..7836e7ee 100755 --- a/tests/matrix/scan.sh +++ b/tests/matrix/scan.sh @@ -45,10 +45,24 @@ command -v jq >/dev/null 2>&1 || { work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT mkdir -p "$work/src"; cd "$work" +# ⚠️⚠️ 构建机是 (os, arch),不是 os。 +# +# mcpp 自己发布四份宿主二进制:linux-x86_64 / linux-aarch64 / macosx-arm64 / +# windows-x86_64。两台 Linux 服务的行**不一样** —— `x86_64-linux-gnu` 需要本机 +# 架构的 glibc 载荷,所以在 x86_64 上够得着,在 aarch64 上够不着。 +# +# 只写 `linux` 会让两台在期望表里用同一个键,后跑的一台把先跑的那台的行「解释掉」, +# 而覆盖的方向取决于谁后跑,不取决于谁对。 case "$(uname -s)" in - Linux) HOST=linux ;; Darwin) HOST=macos ;; - MINGW*|MSYS*|CYGWIN*) HOST=windows ;; *) HOST=unknown ;; + Linux) HOST_OS=linux ;; Darwin) HOST_OS=macos ;; + MINGW*|MSYS*|CYGWIN*) HOST_OS=windows ;; *) HOST_OS=unknown ;; +esac +case "$(uname -m)" in + x86_64|amd64) HOST_ARCH=x86_64 ;; + aarch64|arm64) HOST_ARCH=$([ "$HOST_OS" = macos ] && echo arm64 || echo aarch64) ;; + *) HOST_ARCH="$(uname -m)" ;; esac +HOST="$HOST_OS-$HOST_ARCH" # 目标与编译器清单都取自 mcpp 自己的机器接口,而不是脚本里再抄一份 —— 抄一份 # 就会漂移,而按列宽解析一张给人看的表,会让列宽变成测试套件的一部分。实测过 From 6b9c47f5922e3ac7bd7b6c6478306a81b7ba4f29 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:18:43 +0800 Subject: [PATCH 17/32] ci(matrix): nothing asserted that a host the table names was actually scanned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every check in this workflow is per host, and no per-host check can notice a host that never ran. Deleting a host from the matrix silently retires every expectation the table holds for it: nothing measures those rows, nothing compares them, and the workflow is green. The new `coverage` job reads the expected table as a whole and requires the run to have covered it — both directions. A host with rows and no scan is named; a host that scanned and has no rows is named as a new build host nobody declared expectations for, which is a cause rather than 40 unexplained cells. ⭐ And it ties the axis to the release. The set of build hosts is DERIVED from `release.yml`'s asset names rather than restated, so adding a fifth published host fails this step until the matrix covers it. A host that gets a binary and no scan is a host whose target table is a claim nobody checked; a host scanned and never shipped is coverage spent on a machine no user has. ⚠️ It reds right now, by design: the release publishes four and expected.tsv holds one. The other three backfill from their first scan artifact. --- .github/workflows/ci-target-matrix.yml | 79 ++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml index 9fba1317..2b831bdc 100644 --- a/.github/workflows/ci-target-matrix.yml +++ b/.github/workflows/ci-target-matrix.yml @@ -221,3 +221,82 @@ jobs: bash tests/matrix/compare.sh measured-graph.tsv \ tests/matrix/expected.tsv ${{ matrix.host }} graph || fail=1 [ "$fail" = 0 ] || exit 1 + + coverage: + # ⭐⭐ THE DENOMINATOR. Every check above is per host, and no per-host check + # can notice a host that never ran. + # + # ⚠️ Each `scan` job compares the rows for ITS OWN host, so deleting a host + # from the matrix above silently retires every expectation the table holds + # for it: nothing measures those rows, nothing compares them, and the + # workflow is green. This job is the one place that reads the expected table + # as a whole and requires the run to have covered it. + # + # It runs even when a scan failed (`always()`), because "which hosts were + # covered" is a different question from "did they pass" — and a run where a + # host is missing entirely should say so in those words rather than leaving + # a reader to infer it from a job list. + name: coverage (every host the table names was scanned) + needs: scan + if: always() + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: matrix-* + path: measured + - name: Every host in expected.tsv produced rows + run: | + set -euo pipefail + want=$(awk -F'\t' 'NF>=11 {print $2}' tests/matrix/expected.tsv | sort -u) + [ -n "$want" ] || { echo "::error::expected.tsv names no host at all"; exit 1; } + got=$(cat measured/*/measured.tsv 2>/dev/null \ + | awk -F'\t' 'NF>=11 {print $2}' | sort -u) + echo "expected hosts: $(echo $want)" + echo "scanned hosts: $(echo ${got:-})" + fail=0 + for h in $want; do + printf '%s\n' "$got" | grep -qx "$h" || { + echo "::error::expected.tsv holds rows for '$h', and no scan produced any" + fail=1 + } + done + # ⭐ AND THE OTHER DIRECTION. A host that scanned but has no rows in the + # table is a new build host nobody declared expectations for — the + # per-host compare already reds on it, but saying it here names the + # cause rather than listing 40 unexplained cells. + for h in $got; do + printf '%s\n' "$want" | grep -qx "$h" || { + echo "::error::'$h' was scanned and the expected table does not mention it" + echo " add its rows to tests/matrix/expected.tsv from this run's artifact" + fail=1 + } + done + [ "$fail" = 0 ] || exit 1 + echo "OK: every build host the table names was scanned, and no other" + + - name: The build hosts mcpp ships for are the ones scanned + run: | + set -euo pipefail + # ⚠️⚠️ THE TABLE AND THE RELEASE MUST NAME THE SAME SET. A host that + # gets a published binary and no scan is a host whose target table is + # a claim nobody checked; a host that is scanned and never shipped is + # coverage spent on a machine no user has. + # + # Derived from release.yml's asset names rather than restated here, so + # adding a fifth host to the release fails this step until the matrix + # covers it. + ship=$(grep -oE 'mcpp-\$\{?[A-Za-z_{}. ]*\}?-(linux|macosx|windows)-(x86_64|aarch64|arm64)' \ + .github/workflows/release.yml \ + | sed -E 's/.*-(linux|macosx|windows)-/\1-/' \ + | sed 's/^macosx-/macos-/' | sort -u) + scanned=$(awk -F'\t' 'NF>=11 {print $2}' tests/matrix/expected.tsv | sort -u) + echo "release publishes: $(echo $ship)" + echo "matrix declares: $(echo $scanned)" + if [ "$ship" != "$scanned" ]; then + echo "::error::the set of build hosts mcpp publishes and the set the target matrix declares differ" + diff <(printf '%s\n' "$ship") <(printf '%s\n' "$scanned") | sed 's/^/ /' || true + exit 1 + fi + echo "OK: $(printf '%s\n' "$ship" | wc -l) build hosts, published and scanned" From d0799cbc0b6bff18cddb9d4436e1b2e25fcc55b9 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:22:46 +0800 Subject: [PATCH 18/32] ci(bootstrap): the shared action named x86_64 for every Linux host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bootstrap-mcpp` branched on `uname -s` alone and fetched `xlings--linux-x86_64.tar.gz` for every non-Darwin host. Invisible until a Linux runner is not x86_64 — measured the first time this repo scheduled `ubuntu-24.04-arm`: .../xlings-2026.8.17.2-linux-x86_64/subos/default/bin/xlings: cannot execute binary file: Exec format error Process completed with exit code 126 xlings has published `linux-aarch64` all along; nothing here asked for it. `ci-aarch64-fresh-install.yml` never hit this because it bootstraps through `quick_install.sh`, which does read the architecture. ⚠️ The other `linux-x86_64` spellings in .github are on jobs pinned to `ubuntu-24.04`, including release.yml's `build-linux-aarch64`, which cross-builds under qemu on an x86_64 host. Naming the architecture there is a statement about the runner, not an assumption about it — checked before touching them. ⭐ Found by adding the aarch64 build host to the target matrix. Four hosts get a published binary and only three were ever exercised by this action. --- .github/actions/bootstrap-mcpp/action.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/actions/bootstrap-mcpp/action.yml b/.github/actions/bootstrap-mcpp/action.yml index 29de6310..1a097b25 100644 --- a/.github/actions/bootstrap-mcpp/action.yml +++ b/.github/actions/bootstrap-mcpp/action.yml @@ -72,9 +72,26 @@ runs: REPO_DIR="$(pwd)" # Always install the pinned version — the cache may hold an older # xlings whose sysroot/packages are incompatible. + # ⚠️⚠️ THE ARCHITECTURE IS READ, NOT ASSUMED. This branched on the OS + # alone and named `linux-x86_64` for every non-Darwin host, which is + # invisible until a Linux runner is not x86_64. Measured on + # `ubuntu-24.04-arm`, the first time this repo scheduled one: + # + # .../xlings-2026.8.17.2-linux-x86_64/subos/default/bin/xlings: + # cannot execute binary file: Exec format error + # Process completed with exit code 126 + # + # xlings has published `linux-aarch64` all along; nothing here asked + # for it. `ci-aarch64-fresh-install.yml` never hit this because it + # bootstraps through `quick_install.sh`, which does read the arch. case "$(uname -s)" in Darwin) tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" ;; - *) tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" ;; + *) + case "$(uname -m)" in + aarch64|arm64) xa=aarch64 ;; + *) xa=x86_64 ;; + esac + tarball="xlings-${XLINGS_VERSION}-linux-${xa}.tar.gz" ;; esac WORK=$(mktemp -d) # Retried and verified — see .github/tools/fetch_release.sh. A bare curl From c185a66337b8faa73d52f4360399b97805bc2c3a Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:34:15 +0800 Subject: [PATCH 19/32] ci(bootstrap): a cache key that names the OS lets one machine answer for another MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading `uname -m` to pick the right xlings tarball was correct and was not enough. The download never happened: Cache hit for: xlings-Linux-v2-xl2026.8.17.2-… …/subos/default/bin/xlings: cannot execute binary file: Exec format error These caches hold installed BINARIES — `~/.xlings`, `~/.mcpp`, `target/` — and `runner.os` alone names two machines. `ubuntu-24.04-arm` restored what `ubuntu-24.04` had stored, and the fix one layer down never ran. `runner.arch` now appears in all three keys and their restore-keys. ⚠️ The remaining OS-only keys are in cross-build-test.yml and release.yml, where each OS appears on exactly one runner architecture — checked job by job before leaving them. `ci-target-matrix.yml` is the first workflow in this repository to put two architectures of one OS in the same matrix, and it is why this surfaced now rather than as a wrong artifact later. --- .github/actions/bootstrap-mcpp/action.yml | 30 +++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/actions/bootstrap-mcpp/action.yml b/.github/actions/bootstrap-mcpp/action.yml index 1a097b25..5b10ec99 100644 --- a/.github/actions/bootstrap-mcpp/action.yml +++ b/.github/actions/bootstrap-mcpp/action.yml @@ -42,6 +42,22 @@ runs: uses: actions/cache@v4 with: path: ~/.mcpp + # ⚠️⚠️ `runner.arch` IS PART OF EVERY KEY, AND WAS NOT. + # + # These caches hold INSTALLED BINARIES — `~/.xlings`, `~/.mcpp`, and + # `target/`. Keyed on `runner.os` alone, `Linux` names two machines, and + # `ubuntu-24.04-arm` restored what `ubuntu-24.04` had stored. Measured + # the first time this repo scheduled an arm Linux runner: + # + # Cache hit for: xlings-Linux-v2-xl2026.8.17.2-… + # …/bin/xlings: cannot execute binary file: Exec format error + # + # ⭐ AND IT SURVIVED THE FIX ONE LAYER DOWN. Reading `uname -m` to pick + # the right tarball is correct and was not enough: the download never + # happened, because a cache from the other architecture was already + # there. A key that does not name the machine is a key that lets one + # machine answer for another. + # # The xlings version is part of the LINEAGE, not just the key: mcpp # vendors xlings into ~/.mcpp/registry/bin once at `self init` and # never revisits it (acquire_xlings_binary returns early when the file @@ -50,17 +66,17 @@ runs: # sandbox — which is what actually resolves dependencies — would # silently stay behind (observed: a 0.4.30 sandbox surviving under a # 0.4.69 bootstrap for weeks). - key: mcpp-sandbox-${{ runner.os }}-ci-xl${{ inputs.xlings-version }}-${{ hashFiles('mcpp.toml', '.xlings.json') }} + key: mcpp-sandbox-${{ runner.os }}-${{ runner.arch }}-ci-xl${{ inputs.xlings-version }}-${{ hashFiles('mcpp.toml', '.xlings.json') }} restore-keys: | - mcpp-sandbox-${{ runner.os }}-ci-xl${{ inputs.xlings-version }}- + mcpp-sandbox-${{ runner.os }}-${{ runner.arch }}-ci-xl${{ inputs.xlings-version }}- - name: Cache xlings uses: actions/cache@v4 with: path: ~/.xlings - key: xlings-${{ runner.os }}-v2-xl${{ inputs.xlings-version }}-${{ hashFiles('.xlings.json') }} + key: xlings-${{ runner.os }}-${{ runner.arch }}-v2-xl${{ inputs.xlings-version }}-${{ hashFiles('.xlings.json') }} restore-keys: | - xlings-${{ runner.os }}-v2-xl${{ inputs.xlings-version }}- + xlings-${{ runner.os }}-${{ runner.arch }}-v2-xl${{ inputs.xlings-version }}- - name: Bootstrap mcpp via xlings (unix) if: runner.os != 'Windows' @@ -165,7 +181,7 @@ runs: uses: actions/cache@v4 with: path: target - key: mcpp-target-${{ runner.os }}-${{ github.job }}-${{ hashFiles('src/**', 'tests/**', 'mcpp.toml', 'mcpp.lock') }} + key: mcpp-target-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}-${{ hashFiles('src/**', 'tests/**', 'mcpp.toml', 'mcpp.lock') }} restore-keys: | - mcpp-target-${{ runner.os }}-${{ github.job }}- - mcpp-target-${{ runner.os }}- + mcpp-target-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}- + mcpp-target-${{ runner.os }}-${{ runner.arch }}- From 5b82b17aacc9524abe4daff2a81ff4ce99905ae3 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:46:08 +0800 Subject: [PATCH 20/32] ci(bootstrap): the two Linux xlings tarballs do not have the same shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fetching the right architecture got as far as `No such file or directory`, exit 127. Measured on the 2026.8.17.2 assets: linux-x86_64 subos/default/bin/xlings 513 entries linux-aarch64 bin/xlings 494 entries The path is now found rather than named, bounded to depth 5, and an empty result is a hard error — a bootstrap that continues without xlings fails forty lines later on something that reads like a different problem. ⚠️ Checked before changing the three hosts that already worked: on x86_64 the find resolves `bin/xlings` where the line before named `subos/default/bin/xlings`, and those are the same file — the second is a symlink to the first, md5 94b8e2f2… for both. A find that quietly moved macOS and Windows onto a different binary would have been worse than the defect it fixed. --- .github/actions/bootstrap-mcpp/action.yml | 33 ++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/actions/bootstrap-mcpp/action.yml b/.github/actions/bootstrap-mcpp/action.yml index 5b10ec99..6b50301b 100644 --- a/.github/actions/bootstrap-mcpp/action.yml +++ b/.github/actions/bootstrap-mcpp/action.yml @@ -117,7 +117,38 @@ runs: "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" \ "${WORK}/${tarball}" tar -xzf "${WORK}/${tarball}" -C "${WORK}" - "${WORK}/${tarball%.tar.gz}/subos/default/bin/xlings" self install + # ⚠️⚠️ THE TWO LINUX TARBALLS DO NOT HAVE THE SAME SHAPE, and this line + # named one of them. Measured on the 2026.8.17.2 assets: + # + # linux-x86_64 → subos/default/bin/xlings (513 entries) + # linux-aarch64 → bin/xlings (494 entries) + # + # so fetching the right architecture — the fix immediately above — got + # as far as `No such file or directory`, exit 127. + # + # ⭐ FOUND RATHER THAN ASSUMED, AND THE SEARCH IS BOUNDED. `-maxdepth 5` + # keeps it from wandering into the payload tree, and an empty result is + # a hard error: a bootstrap that silently continues without xlings fails + # forty lines later on something that reads like a different problem. + # + # ⚠️ CHECKED BEFORE CHANGING THE THREE HOSTS THAT ALREADY WORKED. On + # x86_64 this now resolves `bin/xlings` where the line before named + # `subos/default/bin/xlings` — and those are the same file: + # + # subos/default/bin/xlings -> ../../../bin/xlings (symlink) + # md5 94b8e2f2… for both + # + # A find that quietly moved macOS and Windows onto a different binary + # would have been a worse defect than the one being fixed. + XL_BOOT="$(find "${WORK}/${tarball%.tar.gz}" -maxdepth 5 -type f \ + -name xlings -perm -u+x | head -1)" + [ -n "$XL_BOOT" ] || { + echo "::error::no xlings executable inside ${tarball}" + find "${WORK}/${tarball%.tar.gz}" -maxdepth 3 | head -20 + exit 1 + } + echo "bootstrap xlings: ${XL_BOOT#${WORK}/}" + "$XL_BOOT" self install export PATH="$HOME/.xlings/subos/default/bin:$PATH" echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" xlings --version From 6de20706e93f8f310d171c04592f17777b5adfc1 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:52:15 +0800 Subject: [PATCH 21/32] =?UTF-8?q?docs(plan):=20aarch64=20Linux=20ecosystem?= =?UTF-8?q?=20closure=20=E2=80=94=20what=20is=20missing,=20and=20in=20whic?= =?UTF-8?q?h=20repository?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mcpp publishes a linux-aarch64 binary, and once it is there the only things that work are `musl-gcc` and `ninja`. Every other host-code package — llvm, glibc, linux-headers, zlib, libxml2, gcc-runtime, mingw-cross-gcc — ships x86_64 only. Surveyed and tabulated across four repositories, with the distinction the whole plan turns on: host code needs a per-host-arch build, target code does not. `picolibc-riscv` declares both arches and needs neither, because one archive of target code serves every host; `glibc` and `linux-headers` look like host packages and are the target's C library, which is a different piece of work. ⚠️ Upstream LLVM stopped publishing linux-aarch64 after 19.x, and mcpp pins 20.1.7 / 22.1.8 with `import std` — so this has to be built rather than repackaged. Two shapes are set out; static-musl is recommended because it takes the dependency chain from five packages to none, and because `musl-gcc` — the one toolchain that works on that machine today — is already that shape. Its first criterion is whether libc++'s std module survives it. ⚠️ And a warning against the obvious mcpp-side shortcut: do not copy the index's architecture coverage into mcpp. That table becomes wrong on the day the llvm build lands, and nothing would say so. --- ...6-08-26-aarch64-linux-ecosystem-closure.md | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 .agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md diff --git a/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md b/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md new file mode 100644 index 00000000..0c4bc31e --- /dev/null +++ b/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md @@ -0,0 +1,265 @@ +# aarch64 Linux:让整个生态可用,并且可测 + +> 2026-08-26。起因:把 `linux-aarch64` 加进 `ci-target-matrix.yml` 的构建机轴, +> 第一次运行就连挖出三层缺陷,而它们都不在 mcpp 里。 + +--- + +## 0. 一句话 + +**mcpp 为 aarch64 Linux 发布二进制,而它到那里之后能用的东西只有 `musl-gcc` +和 `ninja`。** 其余每一个宿主代码包 —— llvm、glibc、linux-headers、zlib、 +libxml2、gcc-runtime、mingw-cross-gcc —— 都只发 x86_64。 + +⚠️ **这不是「aarch64 没做」,是「aarch64 做了一半而没有任何东西在看」**: +`ci-aarch64-fresh-install.yml` 一直是绿的,因为它验的是 +`xlings install mcpp` 装得上、能编一个 hello world,而不是那台机器上的目标矩阵。 + +--- + +## 1. 判据:什么叫「aarch64 全生态可用」 + +不用形容词。四条,每条都能被一条命令回答: + +| # | 判据 | 怎么测 | +|---|---|---| +| **J1** | `mcpp toolchain list --format json` 在 aarch64 上列出的每一行,`status != planned` 的都真的装得上 | `tests/matrix/scan.sh` 的 `unsupported/other` 与 `mismatch` 格数为 0 | +| **J2** | aarch64 的 `expected.tsv` 行存在,且 `coverage` job 通过 | 发布集 == 矩阵集 | +| **J3** | 七个 openkal 仓库在 aarch64 runner 上 CI 绿 | 各仓 `ci.yml` 加 aarch64 leg | +| **J4** | `mcpp-index` 的每个包在 aarch64 上能被 `mcpp add` + 构建 | 索引 smoke 加 aarch64 leg | + +⭐ **J1 不要求「全部 ok」。** 一个诚实的 `unsupported` 带具名 `reason` 就是通过。 +不可接受的是 `mismatch`(解析说行、构建炸了)与 `other`(拒绝了而没有理由)。 + +--- + +## 2. 现状:四个仓库各缺什么(实测 2026-08-26) + +### 2.1 `xim-pkgindex` —— 主要缺口在这里 + +| 包 | `archs` 声明 | 实际有 linux-aarch64 资产 | 性质 | +|---|---|---|---| +| `llvm` | `{x86_64, arm64}` | **否** | ⚠️ 声明了却没有;`arm64` 只对 macosx 成立 | +| `gcc` | `{x86_64}` | 否 | 正确 —— 其它架构由 `musl-gcc` 覆盖 | +| `musl-gcc` | `{x86_64, aarch64}` | **是** ✅ | | +| `ninja` | `{x86_64, aarch64}` | **是** ✅ | | +| `glibc` | `{x86_64}` | 否 | 目标代码 | +| `linux-headers` | `{x86_64}` | 否 | 目标代码 | +| `zlib` | `{x86_64}` | 否 | llvm 的宿主侧依赖 | +| `libxml2` | `{x86_64}` | 否 | llvm 的宿主侧依赖 | +| `gcc-runtime` | `{x86_64}` | 否 | llvm 的宿主侧依赖 | +| `mingw-cross-gcc` | `{x86_64}` | 否 | 宿主代码 | +| `picolibc-riscv` | `{x86_64, aarch64}` | 不需要 ✅ | ⭐ 目标代码,一份归档服务所有宿主 | + +⚠️ **`llvm.lua` 第 178 行**:`local llvmdir = "llvm-" .. version .. "-linux-x86_64"` +—— 架构写死在解包路径里,与 `archs` 的声明矛盾。 + +### 2.2 `xlings` —— 发了 aarch64,但形状不同 + +`xlings-2026.8.17.2-linux-aarch64.tar.gz` 存在。⚠️ 但两个包的**内部布局不一样**: + +``` +linux-x86_64 subos/default/bin/xlings 513 条目 +linux-aarch64 bin/xlings 494 条目 +``` + +(x86_64 的 `subos/default/bin/xlings` 是指向 `bin/xlings` 的符号链接,同一文件。) + +### 2.3 `mcpp-index` —— 结构上无缺口 + +包是**源码分发**(`add_urls` + `add_versions` + sha256),没有 `archs` 字段, +也不需要。缺的是**验证**:没有任何一条 CI 在 aarch64 上构建过它们。 + +### 2.4 `mcpp` 自己 —— 发布了,但表在说谎 + +`release.yml` 产出 `mcpp--linux-aarch64.tar.gz`。而 +`available_toolchain_indexes()` 是一张按 OS 分支、**不问架构**的静态表: + +```cpp +{ "gcc", ... }, { "musl-gcc", ... }, { llvm::package_name(), ... } +… else if constexpr (is_linux) out.push_back({ "mingw-cross-gcc", ... }); +``` + +⇒ aarch64 上 `toolchain list` 会说 llvm 可装、四个裸机行 `available`, +而 `mcpp toolchain install llvm 22.1.8` 会 404。**声明 ≠ 安装**,又一次。 + +--- + +## 3. 分类:宿主代码与目标代码,决定了每一项的工作量 + +⭐⭐ **这是整份计划里唯一真正的结构判断。** 一个包属于哪一类,决定它需不需要 +per-host-arch 的构建: + +| 类 | 含义 | aarch64 需要什么 | 本仓涉及的包 | +|---|---|---|---| +| **宿主代码** | 在构建机上运行 | **一份 aarch64 构建** | llvm, musl-gcc, ninja, zlib, libxml2, gcc-runtime, mingw-cross-gcc | +| **目标代码** | 在目标机上运行 | 什么都不用 —— 一份归档服务所有宿主 | picolibc-*, openkal-*(源码) | +| **目标侧 sysroot** | 目标机的 C 库 | 一份 **per-target** 构建,与宿主无关 | glibc, linux-headers | + +⚠️ **`glibc` 与 `linux-headers` 在这张表里最容易被归错。** 它们是 +`x86_64-linux-gnu` 这个**目标**的 C 库,不是宿主的。aarch64 宿主要它们,是因为 +`aarch64-linux-gnu` 这个**目标**需要 —— 那是另一件事,见 §5.4。 + +--- + +## 4. 关键决策:aarch64 的 LLVM 怎么建 + +### 4.1 上游没有 + +| 来源 | linux-aarch64 | +|---|---| +| `llvm/llvm-project` 19.1.7 | ✅ `clang+llvm-19.1.7-aarch64-linux-gnu.tar.xz` | +| `llvm/llvm-project` 20.1.7 / 21.1.0 | **无** | +| `xlings-res/llvm` 20.1.7 / 22.1.8 | **无** | + +⚠️ 上游从 20.x 起停发 linux-aarch64,而 mcpp 钉 20.1.7 / 22.1.8,且需要 +C++23 modules + `import std` —— 19.x 顶不上。**只能自己建。** + +### 4.2 两个方案 + +**方案 A:照搬 x86_64 的形状(glibc 动态链接 + 五个依赖包)** + +x86_64 的载荷实测:896 MB(bin 756 MB / lib 123 MB / include 16 MB), +`deps = {glibc>=2.39, linux-headers, zlib, libxml2, gcc-runtime}`。 + +照搬意味着 aarch64 上要先补齐 **zlib / libxml2 / gcc-runtime** 三个宿主包, +外加 aarch64 的 `glibc` / `linux-headers`。⇒ **6 个包的链**。 + +**方案 B:静态链接 musl(⭐ 推荐)** + +用索引里已有的 `aarch64-linux-musl-gcc` 自举,把 clang/lld 静态链到 musl: + +- 依赖数 **5 → 0**。没有 glibc、没有 gcc-runtime、没有 zlib/libxml2 的 + 运行期问题(编进去)。 +- 与 `musl-gcc` 在 aarch64 上已经成立的形状一致 —— 那个包正是这样做的, + 而它是这台机器上**唯一现在就能用的工具链**。 +- ⚠️ 代价:体积更大,且要关掉 `LLVM_ENABLE_LIBXML2` 等可选特性。 +- ⚠️ 未验:libc++ 的 `std` 模块在 musl-static 的 clang 下是否完整可用。 + **这是方案 B 的第一个判据,必须先于其余工作验证。** + +### 4.3 建在哪 + +`ubuntu-24.04-arm` 是原生 aarch64 runner,GitHub 单 job 上限 6 小时。 +LLVM 完整构建约 1.5–3 小时 —— **放得下,不需要交叉编译**。 + +⚠️ `xlings-res/llvm` 目前是纯资产仓(只有 README + releases),构建脚本 +`build-llvm-subpkg.sh` 不在其中。⇒ 需要在该仓新建一个 workflow, +并把 carve 配方(哪些留、哪些删)显式写进去,而不是继续留在某个人的工作目录里。 + +--- + +## 5. 分阶段计划与依赖 + +``` +P0 ──┬── P1(llvm 构建)──┬── P3(索引接线)── P4(mcpp 诚实)── P5(生态 CI) + │ │ + └── P2(xlings 形状)──┘ P6(aarch64-linux-gnu 目标) +``` + +### P0 — 让 aarch64 引导跑通(⏳ 进行中,已提交) + +三层,一层盖住一层,每修一层才露出下一层: + +| # | 缺陷 | 症状 | 状态 | +|---|---|---|---| +| ① | `bootstrap-mcpp` 按 `uname -s` 取 x86_64 xlings | `Exec format error` 126 | ✅ 已修 | +| ② | 缓存键无 `runner.arch`,aarch64 命中 x86_64 缓存 | ①的修复一次没跑到 | ✅ 已修 | +| ③ | 两个 tarball 内部布局不同 | `No such file or directory` 127 | ✅ 已修(find 而非硬编码) | + +**判据**:`invariants (linux-aarch64)` 绿。 + +### P1 — 建 aarch64 的 LLVM + +- **P1.1 可行性判据(先做)**:在 `ubuntu-24.04-arm` 上用 + `aarch64-linux-musl-gcc` 静态建一个最小 clang+lld,验证 + `import std` 与 libc++ 的 `std.cppm` 可用。⭐ 这一条不过,方案 B 作废,回 A。 +- **P1.2**:在 `xlings-res/llvm` 新建 `build-aarch64.yml`,产出 + `llvm-22.1.8-linux-aarch64.tar.xz` + `.sha256`,carve 配方写进 workflow。 +- **P1.3**:同样产出 20.1.7(target 表两个版本都在用)。 +- **判据**:资产可下载,sha256 匹配,解包后 `bin/clang++ --version` 在 + aarch64 上退 0。 + +### P2 — xlings 包形状统一 + +⚠️ 不改 xlings 的发布(那会动别的消费者)。**mcpp 侧已经改成 find 而非硬编码**, +这是正确的方向:消费者不该假设生产者的内部布局。 + +- **P2.1**:向 xlings 报一个 issue,说明两个 Linux 包布局不一致。 +- **判据**:issue 有编号;mcpp 侧的 find 已经不依赖它被修。 + +### P3 — `xim-pkgindex` 接线 + +- **P3.1** `llvm.lua`:把第 178 行的 `linux-x86_64` 改成按 `os.arch()` 取, + 并给 linux 分支加 aarch64 的资源条目。 +- **P3.2** ⚠️ **`archs` 要按 OS 拆**,或者在 linux 分支缺资产时让 xim 说 + 「本架构未发布」而不是 404。⭐ 这是「声明 ≠ 安装」在索引侧的同一条: + 一个包级 `archs` 覆盖三个 OS,而三个 OS 的资产覆盖面不同。 +- **P3.3**(方案 A 才需要)补 zlib / libxml2 / gcc-runtime 的 aarch64。 +- **判据**:`xlings install llvm@22.1.8` 在 aarch64 上成功; + 失败时的消息点名架构。 + +### P4 — mcpp 对 aarch64 诚实 + +- **P4.1** `available_toolchain_indexes()` 目前按 OS 分支不问架构。 + ⚠️ **不要把索引数据抄进 mcpp** —— 那正是会漂移的形状。 + 正确做法:让这张表回答「这个 (os, arch) 上这个族有没有载荷」, + 而答案来自已有的 `list_available_xpkg_versions()` 查询, + 与 `gcc_native_payload_is_musl()` 已经在做的事同型。 +- **P4.2** `host_can_serve()` 对裸机无条件 `true`,理由是 + 「clang/lld 按构造就是交叉编译器」。⚠️ **那个理由预设了 clang 在这台机器上 + 存在。** 在 llvm 落地前,aarch64 上四个裸机行应报一句可读的拒绝, + 带 `reason`,而不是 404。 +- **判据**:aarch64 上 `scan.sh` 的 `mismatch` 与 `other` 均为 0。 + +### P5 — 生态 CI 加 aarch64 + +- **P5.1** 七个 openkal 仓库的 `ci.yml` 各加一条 `ubuntu-24.04-arm` leg。 + ⚠️ `openkal-llvm-runtime` 要求 `mcpp:compiler=llvm`,**依赖 P1**; + `openkal-musl` 只 `provides c-abi=musl`,**现在就能跑**。 + ⇒ 分两批:musl 层先行,llvm 层等 P1。 +- **P5.2** `mcpplibs-index` smoke 加 aarch64 leg(源码分发,只需验证能建)。 +- **P5.3** 回填 `tests/matrix/expected.tsv` 的 `linux-aarch64` 行。 +- **判据**:J2 / J3 / J4。 + +### P6 — `aarch64-linux-gnu` 作为**目标**(独立,可后置) + +今天它是 `planned`。要让它 `verified` 需要 aarch64 的 `glibc` 与 +`linux-headers` **目标侧**载荷。⚠️ 与 P1–P5 无依赖关系 —— 那是「aarch64 作为 +构建机」,这是「aarch64 作为目标」。**两件事不要混。** + +--- + +## 6. CI 验收 + +| 层 | 在哪 | 加什么 | +|---|---|---| +| 构建机轴 | `ci-target-matrix.yml` | ✅ 已加 `linux-aarch64` | +| 分母 | `coverage` job | ✅ 已加:发布集 == 矩阵集,双向 | +| 期望表 | `tests/matrix/expected.tsv` | P5.3 回填 aarch64 行 | +| 生态 | 七个 openkal 仓 | P5.1,分两批 | +| 索引 | `mcpplibs-index` | P5.2 | +| 引导 | `ci-aarch64-fresh-install.yml` | ⚠️ 它验的是装得上,**不是**目标矩阵。保留,但不要把它的绿读成覆盖 | + +⚠️⚠️ **`expected.tsv` 里不写 `mismatch`。** 写下它就是把缺陷声明成期望, +矩阵会在那一格恒绿。aarch64 的行要么 `ok`,要么带具名 `reason` 的 +`unsupported` —— llvm 缺席期间,四个裸机行应当是后者。 + +--- + +## 7. 已知陷阱 + +⚠️ **一层修复会被上一层盖住。** P0 的三条就是这样:缓存键的缺陷让架构修复 +一次都没执行。改完一层一定要看下一层的读数,而不是假定它通了。 + +⚠️ **改共享路径前先查那三台正在工作的宿主。** P0③ 的 find 在 x86_64 上解析到 +另一个路径;查过才知道两者是符号链接、同一文件。一个悄悄把 macOS 和 Windows +挪到别的二进制上的修复,比它修的缺陷更糟。 + +⚠️ **`ci-aarch64-fresh-install.yml` 一直是绿的。** 它走 +`quick_install.sh`(自己读架构),所以从来没碰到 P0①。**一条绿的 CI 不证明 +另一条路径可用** —— 这正是为什么构建机轴要按 mcpp **发布**的那一组来定, +而不是按「手头有哪几台 runner」。 + +⚠️ **不要把索引的架构覆盖面抄进 mcpp。** P4.1 的诱惑是写一张 +「llvm 没有 aarch64」的表。那张表会在 P1 落地的当天变成错的,而没有任何东西 +会提醒你。要问索引,不要记住索引。 From adcb76d6be4169f001cee1947a85b36e279f3de1 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 08:03:14 +0800 Subject: [PATCH 22/32] ci(matrix): 298 skips on aarch64 for the reason the plan documents, and the step said no MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ubuntu-24.04-arm`, with the bootstrap now working end to end: 295 ok gcc@16.1.0: naming aarch64-linux-musl changes nothing 296 ok the C library the query names is on the link line 297 ok ok ok 298 SKIP: llvm is not installed here That skip is correct and is the ecosystem gap itself: upstream LLVM stopped publishing linux-aarch64 after 19.x, mcpp pins 20.1.7 / 22.1.8, and the index has no aarch64 llvm. 298 declares llvm, so on that host it has nothing to declare. The exemption is granted by REASON, mirroring 297's, with linux-x86_64 keeping the strict check as the denominator. ⭐ Granting it that way makes it retire itself: the day an aarch64 llvm lands, this stops being a skip and becomes the assertion, with nothing here to change. ⚠️ Note what 295 measured on that host — the target it resolves is `aarch64-linux-musl`, not `aarch64-linux-gnu`. mcpp already routes gcc to the musl payload on non-x86_64 Linux, and the identity holds there. --- .github/workflows/ci-target-matrix.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml index 2b831bdc..32a683a3 100644 --- a/.github/workflows/ci-target-matrix.yml +++ b/.github/workflows/ci-target-matrix.yml @@ -144,8 +144,28 @@ jobs: "OK: a capability pin is not a preference" \ "gcc is not installed here" || fail=1 fi - check 298_overriding_a_convention_requires_replacing_it.sh \ - "OK: a convention may be overridden, but not merely removed" || fail=1 + if [ "${{ matrix.host }}" = linux-x86_64 ]; then + check 298_overriding_a_convention_requires_replacing_it.sh \ + "OK: a convention may be overridden, but not merely removed" || fail=1 + else + # ⚠️ THE MIRROR OF 297's EXEMPTION, AND FOR THE OTHER FAMILY. + # + # 298 declares llvm, and there is no llvm payload for aarch64 Linux — + # upstream stopped publishing linux-aarch64 after 19.x and the index + # has none. Measured on `ubuntu-24.04-arm`: + # + # SKIP: llvm is not installed here, and this test is about + # declaring it + # + # ⭐ Granting it by REASON rather than by host is what makes it + # retire itself: the day + # `.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md` + # lands an aarch64 llvm, this stops being a skip and starts being + # the assertion, with nothing here to change. + check_or_declared_skip 298_overriding_a_convention_requires_replacing_it.sh \ + "OK: a convention may be overridden, but not merely removed" \ + "llvm is not installed here" || fail=1 + fi [ "$fail" = 0 ] || exit 1 scan: From 4155e4a7c63447ba2717739ce17302c294877a6c Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 08:07:08 +0800 Subject: [PATCH 23/32] feat(toolchain): aarch64 Linux supports musl-gcc, and the rest is deferred in the open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `available_toolchain_indexes()` branched per OS and never per architecture, so an aarch64 Linux host was told llvm could be installed. Measured against the index and upstream on 2026-08-26: xlings-res/llvm 20.1.7 / 22.1.8 no linux-aarch64 asset llvm/llvm-project 20.1.7, 21.1.0 no linux-aarch64 asset llvm/llvm-project 19.1.7 has one, too old for `import std` so `mcpp toolchain install llvm 22.1.8` there is a 404 the list promised would work. `mingw-cross-gcc` is x86_64-only for the same reason and gets the same gate. What remains on that host is `musl-gcc`, which mcpp already routes gcc to on non-x86_64 Linux, and `ninja`. ⭐ This is a POLICY statement — which families mcpp supports on this host, the same kind of statement `tier` makes for a target row — not a copy of the index's coverage. The plan that retires it is .agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md §P1. ⚠️ AND ITS PREMISE IS RECHECKED EVERY RUN. `check_aarch64_llvm_deferral.sh` fails when its reason stops holding — the day an aarch64 llvm is published — and names the gate to remove. A deferral nobody rechecks is indistinguishable from a defect. Network trouble is not read as "it appeared": an unreadable asset list leaves the premise alone and says so. --- ...6-08-26-aarch64-linux-ecosystem-closure.md | 21 +++++++++- .github/tools/check_aarch64_llvm_deferral.sh | 39 +++++++++++++++++++ .github/workflows/ci-target-matrix.yml | 7 ++++ src/toolchain/registry.cppm | 33 +++++++++++++++- 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100755 .github/tools/check_aarch64_llvm_deferral.sh diff --git a/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md b/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md index 0c4bc31e..476a99c0 100644 --- a/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md +++ b/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md @@ -198,7 +198,26 @@ P0 ──┬── P1(llvm 构建)──┬── P3(索引接线)── P4(mcpp - **判据**:`xlings install llvm@22.1.8` 在 aarch64 上成功; 失败时的消息点名架构。 -### P4 — mcpp 对 aarch64 诚实 +### P4 — mcpp 对 aarch64 诚实(✅ 已落地) + +⭐ **决定:aarch64 上先只支持 `musl-gcc`,其余显式标记延缓。** + +- **P4.1 ✅** `available_toolchain_indexes()` 在非 x86_64 Linux 上不再列出 + `llvm` 与 `mingw-cross-gcc`。⚠️ 这是一句**政策陈述**(mcpp 在这台宿主上支持 + 哪些族),不是索引数据的抄本 —— 与目标行的 `tier` 同类。 +- **P4.2 ✅ 延缓的前提每轮重测**:`.github/tools/check_aarch64_llvm_deferral.sh` + 查 `xlings-res/llvm` 的 20.1.7 / 22.1.8 是否出现了 `linux-aarch64` 资产。 + ⚠️ **它在理由不再成立时变红**,与一般的检查方向相反。 + ⚠️ 网络故障不得被读成「出现了」:读不到资产表就说读不到,保持前提不动。 +- **P4.3 ✅ 生态 e2e 的豁免按理由给**:298 在 aarch64 上跳过,理由是 + `llvm is not installed here`。按理由给而不是按宿主给,llvm 落地那天它自动 + 从「跳过」变回「断言」,workflow 一行都不用改。 + +⚠️ **`host_can_serve()` 对裸机仍无条件 `true`** —— 那条理由(clang/lld 按构造 +就是交叉编译器)预设了 clang 在这台机器上存在。P4.1 的门让四个裸机行不再被 +列出,所以症状已经消失;这一处的**根因**留到 P1 之后再处理,因为届时它自然成立。 + +### P4-原文 — mcpp 对 aarch64 诚实 - **P4.1** `available_toolchain_indexes()` 目前按 OS 分支不问架构。 ⚠️ **不要把索引数据抄进 mcpp** —— 那正是会漂移的形状。 diff --git a/.github/tools/check_aarch64_llvm_deferral.sh b/.github/tools/check_aarch64_llvm_deferral.sh new file mode 100755 index 00000000..6348a9b1 --- /dev/null +++ b/.github/tools/check_aarch64_llvm_deferral.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# ⭐⭐ A DEFERRAL'S PREMISE, RECHECKED. +# +# `available_toolchain_indexes()` omits llvm on non-x86_64 Linux because no +# linux-aarch64 llvm exists — not in xlings-res, and not upstream since 19.x. +# That is a deferral, and a deferral nobody rechecks is indistinguishable from +# a defect. +# +# ⚠️ THIS FAILS WHEN THE REASON STOPS HOLDING, which is the opposite of what a +# check usually does. The day an aarch64 llvm is published, it goes red and +# names the gate to remove — see +# `.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md` §P1. +# +# ⚠️ Network trouble must not be read as "it appeared". An unreadable asset list +# leaves the premise alone and says so: a check that turns a flaky API into a +# claim about the world is worse than no check. +set -uo pipefail + +fail=0 +for tag in 22.1.8 20.1.7; do + names="$(curl -sSL --retry 3 --retry-all-errors --max-time 60 \ + "https://api.github.com/repos/xlings-res/llvm/releases/tags/$tag" 2>/dev/null \ + | grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"]+"' \ + | sed -E 's/.*"([^"]+)"$/\1/')" + if [ -z "$names" ]; then + echo " ? $tag: could not read the asset list — premise left alone" + continue + fi + if printf '%s\n' "$names" | grep -q 'linux-aarch64'; then + echo "::error::xlings-res/llvm $tag now publishes a linux-aarch64 asset" + echo " the deferral in available_toolchain_indexes() has outlived its reason" + echo " see .agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md §P1" + fail=1 + continue + fi + echo " ok $tag: still no linux-aarch64 asset" +done +[ "$fail" = 0 ] || exit 1 +echo "OK: the aarch64 llvm deferral still has its reason" diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml index 32a683a3..c340cb1a 100644 --- a/.github/workflows/ci-target-matrix.yml +++ b/.github/workflows/ci-target-matrix.yml @@ -88,6 +88,13 @@ jobs: done [ "$fail" = 0 ] || exit 1 + # ⚠️ A DEFERRAL NOBODY RECHECKS IS INDISTINGUISHABLE FROM A DEFECT. This + # step fails when its reason STOPS holding — the day an aarch64 llvm is + # published — which is the opposite of what a check usually does. + - name: The aarch64 llvm deferral still has its reason + if: matrix.host == 'linux-aarch64' + run: bash .github/tools/check_aarch64_llvm_deferral.sh + - name: Each invariant RAN run: | set -euo pipefail diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 3a7364f9..bfd930b1 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -640,11 +640,39 @@ bool host_can_serve(const triple::Triple& target) { } std::vector available_toolchain_indexes() { + // ⚠️⚠️ NOT EVERY FAMILY EXISTS FOR EVERY (OS, ARCH), AND THIS LIST USED TO + // SAY OTHERWISE. + // + // The branches below are per-OS and there were none per-ARCH, so an aarch64 + // Linux host was told llvm could be installed. Measured 2026-08-26 against + // the index and upstream: + // + // xlings-res/llvm 20.1.7 / 22.1.8 no linux-aarch64 asset + // llvm/llvm-project 20.1.7, 21.1.0 no linux-aarch64 asset + // llvm/llvm-project 19.1.7 has one — too old for `import std` + // + // so `mcpp toolchain install llvm 22.1.8` there is a 404 that this list + // promised would work. Same family as the rest of this release: a table + // that answers a narrower question than the one it is asked. + // + // ⭐ THIS IS A POLICY STATEMENT, NOT A COPY OF THE INDEX. It says which + // families mcpp SUPPORTS on this host — the same kind of statement `tier` + // makes for a target row — and the plan that retires it is + // `.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md` §P1. + // + // ⚠️ AND ITS PREMISE IS ASSERTED IN CI, so it cannot outlive its reason. + // `ci-target-matrix.yml`'s aarch64 job checks that no linux-aarch64 llvm + // asset has appeared; the day one does, that step reds and names this + // gate. A deferral nobody rechecks is indistinguishable from a defect. + const bool linuxNonX86 = + mcpp::platform::is_linux && mcpp::platform::host_arch != "x86_64"; + std::vector out{ { "gcc", Family::Gcc }, { "musl-gcc", Family::Gcc }, - { mcpp::toolchain::llvm::package_name(), Family::Llvm }, }; + if (!linuxNonX86) + out.push_back({ mcpp::toolchain::llvm::package_name(), Family::Llvm }); // The Windows-PE gcc payload is host-split at the distribution layer // (§4.3); each host lists the package it would actually install. if constexpr (mcpp::platform::is_windows) { @@ -660,7 +688,8 @@ std::vector available_toolchain_indexes() { out.push_back({ std::string(mcpp::platform::host_arch) + "-linux-musl-gcc", Family::Gcc }); } else if constexpr (mcpp::platform::is_linux) { - out.push_back({ "mingw-cross-gcc", Family::Gcc }); + // Same gate: `mingw-cross-gcc` publishes x86_64 only. + if (!linuxNonX86) out.push_back({ "mingw-cross-gcc", Family::Gcc }); } return out; } From 0f6217dc9ca3b720dc33f1dc0e9132adaf4c4061 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 08:07:23 +0800 Subject: [PATCH 24/32] docs(plan): drop the superseded P4 draft left beside its landed version --- .../2026-08-26-aarch64-linux-ecosystem-closure.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md b/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md index 476a99c0..a50a08a9 100644 --- a/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md +++ b/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md @@ -217,19 +217,6 @@ P0 ──┬── P1(llvm 构建)──┬── P3(索引接线)── P4(mcpp 就是交叉编译器)预设了 clang 在这台机器上存在。P4.1 的门让四个裸机行不再被 列出,所以症状已经消失;这一处的**根因**留到 P1 之后再处理,因为届时它自然成立。 -### P4-原文 — mcpp 对 aarch64 诚实 - -- **P4.1** `available_toolchain_indexes()` 目前按 OS 分支不问架构。 - ⚠️ **不要把索引数据抄进 mcpp** —— 那正是会漂移的形状。 - 正确做法:让这张表回答「这个 (os, arch) 上这个族有没有载荷」, - 而答案来自已有的 `list_available_xpkg_versions()` 查询, - 与 `gcc_native_payload_is_musl()` 已经在做的事同型。 -- **P4.2** `host_can_serve()` 对裸机无条件 `true`,理由是 - 「clang/lld 按构造就是交叉编译器」。⚠️ **那个理由预设了 clang 在这台机器上 - 存在。** 在 llvm 落地前,aarch64 上四个裸机行应报一句可读的拒绝, - 带 `reason`,而不是 404。 -- **判据**:aarch64 上 `scan.sh` 的 `mismatch` 与 `other` 均为 0。 - ### P5 — 生态 CI 加 aarch64 - **P5.1** 七个 openkal 仓库的 `ci.yml` 各加一条 `ubuntu-24.04-arm` leg。 From 53a690e85857f09c57c91e1958983d4f028b6e8c Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 08:22:22 +0800 Subject: [PATCH 25/32] test(matrix): `timeout` is GNU coreutils, and macOS has neither it nor gtimeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macos-14, the first run that reached this script: all 20 cells came back `mismatch / query-failed`. `mcpp toolchain list`, which the script does not wrap, worked — which is how the wrapper was identified as the difference. ⭐ `tests/e2e/run_all.sh` has had `timeout` → `gtimeout` → none detection since it shipped. The defect was not that the problem is hard; it is that a second copy of a decision was written without looking at the first. No timeout command is a legitimate state here: the workflow's `timeout-minutes` is the backstop and running unwrapped beats not running. ⚠️ AND THE EVIDENCE WAS BEING THROWN AWAY. The query ran under `2>/dev/null`, so `query-failed` was the right classification with nothing to say why — twenty identical cells and no cause anywhere in the log. stderr now goes to a file and the first two lines are printed beside the failing cell. That redirect is the same shape as the classification it defeats: a cell that cannot be explained and a cell that is unsupported were being written the same way. --- tests/matrix/scan.sh | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh index 7836e7ee..bfe837d9 100755 --- a/tests/matrix/scan.sh +++ b/tests/matrix/scan.sh @@ -42,6 +42,29 @@ command -v jq >/dev/null 2>&1 || { exit 2 } +# ⚠️⚠️ `timeout` IS GNU coreutils AND macOS HAS NEITHER IT NOR `gtimeout`. +# +# Measured on macos-14, the first run that reached this script: every cell came +# back `mismatch / query-failed` — all 20 of them — because the wrapper was not +# a command. `mcpp toolchain list`, which this script does NOT wrap, worked +# fine, which is how the two were told apart. +# +# ⭐ `tests/e2e/run_all.sh` has had this exact detection since it shipped. The +# defect was not that the problem is hard; it is that a second copy of a +# decision was written without looking at the first. +# +# No timeout command is a legitimate state: the step-level `timeout-minutes` in +# the workflow is the backstop, and running unwrapped beats not running. +TIMEOUT="" +if command -v timeout >/dev/null 2>&1; then TIMEOUT=timeout +elif command -v gtimeout >/dev/null 2>&1; then TIMEOUT=gtimeout +fi +[ -n "$TIMEOUT" ] || echo "scan: no timeout/gtimeout here — running unwrapped" >&2 +run_limited() { # seconds cmd… → run with a limit if one is available + local secs="$1"; shift + if [ -n "$TIMEOUT" ]; then "$TIMEOUT" "$secs" "$@"; else "$@"; fi +} + work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT mkdir -p "$work/src"; cd "$work" @@ -123,11 +146,16 @@ for tc in $(compilers); do fi # ── 第一问:这一格会解析成什么 ──────────────────────────────────── - q="$(timeout "${MATRIX_QUERY_TIMEOUT:-300}" \ - "$MCPP" why toolchain --target "$t" --toolchain "$tc" --format json 2>/dev/null)" + # ⚠️ stderr 留到一个文件里,不丢。前一版写的是 `2>/dev/null`,于是 + # `query-failed` 是对的分类而**没有任何证据**说明为什么 —— macOS 上 20 格 + # 全红,原因(`timeout` 不存在)被这个重定向吞掉了。 + q="$(run_limited "${MATRIX_QUERY_TIMEOUT:-300}" \ + "$MCPP" why toolchain --target "$t" --toolchain "$tc" --format json \ + 2>"$work/q.err")" if [ -z "$q" ]; then # ⚠️ 查询本身没跑起来。这不是「这一格不支持」,而是「不知道」—— 两者必须 # 分开,否则一次环境故障会被整片读成「不支持」。 + echo "scan: $t × $tc 查询无输出: $(head -2 "$work/q.err" | tr '\n' ' ')" >&2 emit "$MODE" "$HOST" "$t" "$tc" - - - - - mismatch query-failed continue fi @@ -158,7 +186,7 @@ for tc in $(compilers); do # 读任何一行输出。 printf '\n[toolchain]\ndefault = "%s"\n' "$tc" >> mcpp.toml rm -rf target - if timeout "${MATRIX_TIMEOUT:-600}" "$MCPP" build --target "$t" >/dev/null 2>&1; then + if run_limited "${MATRIX_TIMEOUT:-600}" "$MCPP" build --target "$t" >/dev/null 2>&1; then emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" ok none else emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" \ From d9dd8d7cb7d482cdfbec1559420b32a70b27fee8 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 08:44:08 +0800 Subject: [PATCH 26/32] fix(toolchain): "self-contained" is about the payload's contents, not about which hosts it ships for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two host_can_serve branches assumed x86_64, and two new build hosts each found one of them. ubuntu-24.04-arm, `--target x86_64-linux-musl`: admitted because `target.is_musl()` was read as "any Linux arch can reach it". A musl payload really does carry its own sysroot — and it still has to EXIST for the host running it. The cross packages are published per host arch: x86_64-linux-musl-gcc archs = { "x86_64" } aarch64-linux-musl-gcc archs = { "x86_64", "aarch64" } so the row resolved a package with no aarch64 asset and failed at install — `mismatch / build-failed`, twice. ⭐ The native row stays reachable on every arch, which is why `aarch64-linux-musl` is `ok` there and is the whole of "aarch64 supports musl-gcc". Same host, `--target x86_64-windows-gnu`: `mingw-cross-gcc` is `archs = { "x86_64" }` too. Its refusal carried no reason at all — `unsupported / other` — which is exactly what that token was added to make visible, and it named an uncoded branch on its first run. ⚠️ And `build-failed` was throwing its evidence away, the same shape as the `query-failed` redirect fixed one commit ago. The failing cell now prints two error lines beside itself; the full log is not dumped, because 40 cells of complete build output is not something anyone reads. Local: 4 e2e green, 40 cells match — both gates are the identity on x86_64. --- src/toolchain/registry.cppm | 35 +++++++++++++++++++++++++++++------ tests/matrix/scan.sh | 8 +++++++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index bfd930b1..19b053fd 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -604,10 +604,26 @@ bool host_can_serve(const triple::Triple& target) { if (target.os == "linux") { if constexpr (mcpp::platform::is_linux) { - // musl payloads are self-contained, so any arch is reachable; a - // glibc target additionally needs the host-native sysroot payloads - // (xim:glibc / xim:linux-headers), which only exist for this arch. - return target.is_musl() || target.arch == mcpp::platform::host_arch; + // ⚠️⚠️ "SELF-CONTAINED" IS ABOUT THE PAYLOAD'S CONTENTS, NOT ABOUT + // WHICH HOSTS IT IS PUBLISHED FOR — and this line read it as both. + // + // A musl payload really does carry its own sysroot, so no host-side + // libc is needed. It still has to EXIST for the host running it, + // and the cross packages are published per host arch: + // + // x86_64-linux-musl-gcc archs = { "x86_64" } + // aarch64-linux-musl-gcc archs = { "x86_64", "aarch64" } + // + // Measured on ubuntu-24.04-arm: `--target x86_64-linux-musl` was + // admitted, resolved a package with no aarch64 asset, and failed at + // install — `mismatch / build-failed` in the target matrix, twice. + // + // ⭐ The native row stays reachable on every arch: `musl-gcc` + // publishes both, which is why `aarch64-linux-musl` is `ok` there. + const bool crossArch = target.arch != mcpp::platform::host_arch; + if (target.is_musl()) + return !crossArch || mcpp::platform::host_arch == "x86_64"; + return !crossArch; } // Non-Linux host: only the self-contained musl payloads can work at // all (nothing else would find a C library). Today exactly one such @@ -618,8 +634,15 @@ bool host_can_serve(const triple::Triple& target) { && target.is_musl() && target.arch == mcpp::platform::host_arch; } - if (target.is_windows_gnu()) - return mcpp::platform::is_linux || mcpp::platform::is_windows; + // ⚠️ THE mingw CROSS IS PUBLISHED FOR ONE HOST ARCH. `mingw-cross-gcc` + // declares `archs = { "x86_64" }`, so a Linux host that is not x86_64 + // cannot obtain it — measured on ubuntu-24.04-arm, where the row was + // listed and its refusal carried no reason at all (`unsupported / other`). + if (target.is_windows_gnu()) { + if constexpr (mcpp::platform::is_windows) return true; + return mcpp::platform::is_linux + && mcpp::platform::host_arch == "x86_64"; + } if (target.os == "windows") return bool(mcpp::platform::is_windows); if (target.os == "macos") return bool(mcpp::platform::is_macos); diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh index bfe837d9..768d2f7e 100755 --- a/tests/matrix/scan.sh +++ b/tests/matrix/scan.sh @@ -186,9 +186,15 @@ for tc in $(compilers); do # 读任何一行输出。 printf '\n[toolchain]\ndefault = "%s"\n' "$tc" >> mcpp.toml rm -rf target - if run_limited "${MATRIX_TIMEOUT:-600}" "$MCPP" build --target "$t" >/dev/null 2>&1; then + if run_limited "${MATRIX_TIMEOUT:-600}" "$MCPP" build --target "$t" \ + >"$work/b.out" 2>&1; then emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" ok none else + # ⚠️ 与查询失败同型:`build-failed` 是对的分类,而没有证据说明为什么。 + # macOS 上 20 格一模一样的红、日志里找不到原因,就是把输出丢掉的代价。 + # 这里只留错误行,不倒整份构建日志 —— 40 格 × 一份完整日志读不动。 + echo "scan: $t × $tc 构建失败: $(grep -m2 -iE '^error|error:' "$work/b.out" \ + | tr '\n' ' ' | cut -c1-160)" >&2 emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" \ mismatch build-failed fi From 0d09b8aca1a6e81eb8cba35ee9de6e6a314c069c Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:04:14 +0800 Subject: [PATCH 27/32] fix: four defects the four-host scan found, in one round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scan reached all four build hosts and produced six `mismatch` cells. The evidence capture added last commit paid for itself immediately — all six causes came out of one query. ⭐ THE MACHINE INTERFACE DID NOT ROUND-TRIP. `toolchain list --format json` reported `msvc@19.44.35228`, which is cl.exe's version, and `--toolchain msvc@19.44.35228` is refused by design — "names a COMPILER version, not a toolset". A consumer reads `family` and `version`, joins them, and hands the result back; scan.sh did exactly that and every msvc cell came back with mcpp rejecting its own output. A system toolset is `msvc@system`; the display number keeps a field of its own. e2e 296 now asserts the round-trip for every family on every host. ⚠️ A TARGET NO PAYLOAD HERE SERVES WAS STILL HAVING ITS PAYLOAD INSTALLED. `unservedTargetDiagnosis` is decided early and released late, deliberately — whether the graph supplies the system is not knowable until it is resolved — and the install sits between the two. On ubuntu-24.04-arm it failed first and hard: `xlings install of 'xim:x86_64-linux-musl-gcc@16.1.0' failed`, the cross-musl packages being published per host arch. Skipping the install leaves both later paths intact; attempting it cannot help either. ⚠️ AND I TRUNCATED MY OWN EVIDENCE. The `build-failed` line was cut at 160 characters, so the macOS cell read `precompiled file '/private/var/…/target/` and stopped exactly before the part worth reading. Three lines, whole lines. ⚠️ THE CELL COUNT WAS A FUNCTION OF CACHE STATE. Same ubuntu-24.04 runner: one run had gcc+llvm and scanned 40 cells, the next had gcc alone and scanned 20. The matrix now installs the toolchains its expected table names, and fails there if one cannot be installed, rather than reporting a screen of "never reached". --- .github/workflows/ci-target-matrix.yml | 34 +++++++++++++++++++ src/build/prepare.cppm | 33 +++++++++++++++++- src/toolchain/lifecycle.cppm | 20 +++++++++-- ...report_names_is_what_the_link_line_uses.sh | 23 +++++++++++++ tests/matrix/scan.sh | 7 ++-- 5 files changed, 112 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml index c340cb1a..6c8ec106 100644 --- a/.github/workflows/ci-target-matrix.yml +++ b/.github/workflows/ci-target-matrix.yml @@ -217,6 +217,40 @@ jobs: [ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; } echo "MCPP_UNDER_TEST=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT")" >> "$GITHUB_ENV" + # ⚠️⚠️ 格数不能是缓存状态的函数。 + # + # 实测:同一台 ubuntu-24.04,一轮装了 gcc+llvm(扫 40 格),下一轮只有 gcc + # (扫 20 格)。`expected.tsv` 声明的是前者,于是后者会把所有 llvm 行报成 + # 「期望表说有而扫描没跑到」—— 而那句报错是对的,问题在于覆盖面**漂移**了。 + # + # ⭐ 矩阵要声明它扫哪些工具链,并把它们装上。装不上就红在这里,而不是 + # 变成一屏「没跑到」。 + - name: Install the toolchains this matrix declares + run: | + set -uo pipefail + export MCPP="$MCPP_UNDER_TEST" + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + want="$(awk -F'\t' -v h='${{ matrix.host }}' \ + 'NF>=11 && $2==h {print $4}' tests/matrix/expected.tsv \ + | sort -u)" + if [ -z "$want" ]; then + echo " ? ${{ matrix.host }} 尚无期望行 —— 扫描它现有的工具链" + exit 0 + fi + fail=0 + for spec in $want; do + fam="${spec%@*}"; ver="${spec#*@}" + # `msvc@system` 是在机器上被找到的,不是装出来的。 + [ "$ver" = system ] && { echo " ok $spec (system)"; continue; } + if "$MCPP" toolchain install "$fam" "$ver" >/dev/null 2>&1; then + echo " ok $spec" + else + echo "::error::$spec 装不上,而期望表声明了它" + fail=1 + fi + done + [ "$fail" = 0 ] || exit 1 + - name: Scan both systems run: | set -euo pipefail diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index e4faeb9e..934139b7 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -1895,13 +1895,44 @@ prepare_build(bool print_fingerprint, } auto pkg = mcpp::toolchain::to_xim_package(*spec); + // ⚠️⚠️ AND NOT INSTALLED WHEN NO PAYLOAD HERE COULD SERVE THE TARGET. + // + // `unservedTargetDiagnosis` is decided a thousand lines above and + // released a thousand lines below — deliberately, because whether the + // dependency GRAPH supplies the target's system is not knowable until + // it is resolved. This install sits between the two, and it does not + // need to wait: if no payload here serves the target, then either the + // graph supplies the system (and this payload is not wanted) or the + // build refuses later (and it is not wanted then either). + // + // ⚠️ Measured on ubuntu-24.04-arm, `--target x86_64-linux-musl`: + // + // error: toolchain 'gcc@16.1.0': xlings install of + // 'xim:x86_64-linux-musl-gcc@16.1.0' failed … + // + // — the cross-musl packages are published per host arch and that one is + // x86_64-only. The refusal that names this correctly never ran, because + // the install failed first and failed hard. + // + // ⭐ Skipping leaves BOTH later paths intact; attempting cannot help + // either of them. + const bool targetPayloadUnservable = + !unservedTargetDiagnosis.empty() && !spec->target.empty(); + auto cfg = get_cfg(); if (!cfg) return std::unexpected(cfg.error()); mcpp::fetcher::Fetcher fetcher(**cfg); mcpp::ui::info("Resolving", "toolchain"); mcpp::fetcher::InstallProgressHandler progress; - auto payload = fetcher.resolve_xpkg_path(pkg.target(), /*autoInstall=*/true, &progress); + auto payload = fetcher.resolve_xpkg_path( + pkg.target(), /*autoInstall=*/!targetPayloadUnservable, &progress); + if (!payload && targetPayloadUnservable) { + // The held diagnosis is already the right words for this; releasing + // it here rather than at its usual site keeps one sentence per cause. + refusal::record(refusal::Code::HostCannotServe); + return std::unexpected(unservedTargetDiagnosis); + } if (!payload) { // `windows = "msvc@19.44"` in a manifest is the retired // cl-version spelling; saying "no such xim package" would send diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index 907f5507..646d0e89 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -542,9 +542,25 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, // second array. if (mcpp::platform::is_windows && json) { if (auto inst = mcpp::toolchain::msvc::detect_installation()) + // ⚠️⚠️ `version` MUST BE WHAT `--toolchain` ACCEPTS, AND THIS EMITTED + // WHAT cl.exe REPORTS. + // + // A system Visual Studio is selected as `msvc@system`; its COMPILER + // version is `19.44.35228`, which `parse_toolchain_spec` rejects by + // design — it "names a COMPILER version, not a toolset". + // + // ⭐ THE MACHINE INTERFACE HAS TO ROUND-TRIP. A consumer reads + // `family` and `version`, joins them, and hands the result back; + // `tests/matrix/scan.sh` did exactly that and every msvc cell came + // back `build-failed` with mcpp refusing its own output. Measured on + // windows-2022, both modes. + // + // The human-facing number keeps a field of its own, so nothing is + // lost — it simply stops pretending to be a spec. jsonToolchains.push_back({ - {"family", "msvc"}, - {"version", inst->display_version()}, + {"family", "msvc"}, + {"version", "system"}, + {"displayVersion", inst->display_version()}, {"default", defSpec && defSpec->family == mcpp::toolchain::Family::Msvc}, {"source", "system"}, diff --git a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh index 70081f3c..02ca656c 100755 --- a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh +++ b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh @@ -74,6 +74,29 @@ c_abi_line() { printf '%s\n' "$report" | grep -E '^\s+c-abi\s' | head -1; } fail=0; checked=0 +# ── Relation zero: the machine interface must round-trip ────────────────── +# +# ⭐⭐ A CONSUMER READS `family` AND `version`, JOINS THEM, AND HANDS THE RESULT +# BACK. That has to work, and for one entry it did not. +# +# ⚠️ Measured on windows-2022: `toolchain list --format json` reported +# `msvc@19.44.35228` — cl.exe's version — and `--toolchain msvc@19.44.35228` is +# refused by design ("names a COMPILER version, not a toolset"). Every msvc cell +# in the target matrix came back `build-failed`, with mcpp rejecting its own +# output. A system toolset is selected as `msvc@system`. +# +# This is cheap and catches the whole class, on every host, for every family. +for spec in $("$MCPP" toolchain list --format json 2>/dev/null \ + | jq -r '.data.toolchains[] | .family + "@" + .version'); do + if "$MCPP" why toolchain --toolchain "$spec" --format json >/dev/null 2>&1; then + echo " ok $spec round-trips through --toolchain" + checked=$((checked+1)) + else + echo "FAIL: toolchain list emitted '$spec' and --toolchain refuses it" + fail=1 + fi +done + # ── Relation one: a payload C library must be on the link line ───────────── for tc in gcc llvm; do # ⭐ From the machine interface: `grep -oP` does not exist on macOS, and its diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh index 768d2f7e..7f69964c 100755 --- a/tests/matrix/scan.sh +++ b/tests/matrix/scan.sh @@ -193,8 +193,11 @@ for tc in $(compilers); do # ⚠️ 与查询失败同型:`build-failed` 是对的分类,而没有证据说明为什么。 # macOS 上 20 格一模一样的红、日志里找不到原因,就是把输出丢掉的代价。 # 这里只留错误行,不倒整份构建日志 —— 40 格 × 一份完整日志读不动。 - echo "scan: $t × $tc 构建失败: $(grep -m2 -iE '^error|error:' "$work/b.out" \ - | tr '\n' ' ' | cut -c1-160)" >&2 + # ⚠️ 三行,**整行**。前一版 `cut -c1-160` 把真正的错误砍在半路 —— + # macOS 那格只留下 `precompiled file '/private/var/…/target/` 就没了, + # 而要看的正是后半截。判据的单位是一整行输出。 + echo "scan: $t × $tc 构建失败:" >&2 + grep -m3 -iE '^error|error:' "$work/b.out" | sed 's/^/ /' >&2 emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" \ mismatch build-failed fi From 8801d2cd28f09f7dd16436eab7b6e05f056be8d6 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:25:15 +0800 Subject: [PATCH 28/32] fix: the last two mismatches, both found only because a second machine ran the scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️ `--rtlib=compiler-rt` IS A CODEGEN FACT ON aarch64, AND IT WAS DECLARED ON A KEY THAT REACHES ONE SIDE. macos-14 building `aarch64-linux-musl` over the graph: error: precompiled file 'std.pcm' was compiled with the target feature '+outline-atomics' but the current translation unit is not `openkal-llvm-runtime` put it in `std-module-flags`, which reaches the std module's command and no consumer's TU. ⭐ Same defect as `-fdwarf-exceptions`, one flag later — hostflags.cppm describes that one as "its objects agreed with each other and nothing else did", and the answer then was the same as now: a property of the graph cannot be declared by one package for one command. x86_64 lists no such feature on either side, so it was invisible until a second architecture was built. ⚠️ PE + musl HAS NO PAYLOAD ON ANY HOST, AND `host_can_serve` SAID OTHERWISE ON EXACTLY ONE. windows-2022, payload system: c-abi musl(payload) c++-abi msvc-stl(payload) lld-link: error: undefined symbol: __mingw_vfprintf musl's C library, MSVC's STL and MinGW's CRT symbols in one link. `pin_is_capability()` and chapter 16 both already say the row is graph-only; Linux had been answering `host-cannot-serve` all along. Local: 4 e2e green (296 now 8 relations, including the round-trip), 40 cells match, and aarch64/x86_64/windows-gnu all build over the graph. --- CHANGELOG.md | 49 +++++++++++++++++++++++++++++++++++++ src/toolchain/model.cppm | 27 ++++++++++++++++++++ src/toolchain/registry.cppm | 23 +++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3783eb55..a21198d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,55 @@ ⚠️ **约定仍然可以被推翻**:hosted 目标上显式声明 gcc 照常生效。 +### 目标矩阵在四台构建机上找到的 + +⭐⭐ **把 `linux-aarch64` 加进构建机轴之后,四台各自交出了一台机器上看不见的缺陷。** +轴取自 `release.yml` 发布的那一组(linux-x86_64 / linux-aarch64 / macos-arm64 / +windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二进制却从没被扫过的 +宿主,它的目标表是一句没人核过的话。 + +- **⚠️ `--rtlib=compiler-rt` 在 aarch64 上是 codegen 事实,而它被声明在只覆盖 + 一侧的键上。** + + ``` + error: precompiled file 'std.pcm' was compiled with the target feature + '+outline-atomics' but the current translation unit is not + ``` + + `openkal-llvm-runtime` 把它写在 `std-module-flags` 里 —— 那个键到达 `std.pcm` + 的命令,不到达任何消费者的 TU。⭐ **与 `-fdwarf-exceptions` 是同一个缺陷,换了 + 一个 flag**;那次的解法是提升成图级的 `graph_runtime_compile_flags`,这次同样。 + + ⚠️ x86_64 上两侧都列空,所以直到第二个架构被构建才可见。 + +- **⚠️ PE + musl 在任何宿主上都没有载荷,而 `host_can_serve` 在 Windows 上说有。** + + ``` + c-abi musl(payload) + c++-abi msvc-stl(payload) + lld-link: error: undefined symbol: __mingw_vfprintf + ``` + + musl 的 C 库、MSVC 的 STL、MinGW 的符号,一格里三个 C 运行时。 + `triple::pin_is_capability()` 与 docs/16 都已写明这一行只能由依赖图供给; + Linux 上同一格早就答 `host-cannot-serve`。 + +- **⚠️ 宿主服务不了的目标,仍然去装它的载荷。** 拒绝被决定在早、释放在晚(因为 + 图供不供给系统只有解析后才知道),而安装夹在中间,于是先失败且失败得更硬: + `xlings install of 'xim:x86_64-linux-musl-gcc@16.1.0' failed`。跳过安装让两条 + 后续路径都完好;尝试安装帮不了其中任何一条。 + +- **⚠️ aarch64 Linux 只支持 `musl-gcc`,其余显式延缓。** 上游 LLVM 从 20.x 起停发 + `linux-aarch64`,索引里也没有,所以 `available_toolchain_indexes()` 在非 x86_64 + Linux 上不再列 `llvm` 与 `mingw-cross-gcc`。⭐ 这是**政策陈述**不是索引数据的 + 抄本,并且 `check_aarch64_llvm_deferral.sh` **在理由不再成立时变红** —— + 没人复查的延缓与缺陷无法区分。计划见 + [`.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md`](.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md)。 + +- 交叉 musl 与 mingw 的载荷都按宿主架构发布,`host_can_serve` 对两者也改为按架构 + 回答。⚠️ 「自足」讲的是载荷装了什么,不是它为哪些宿主发布 —— 那行注释是对的, + 代码把它读错了一层。 + ### 机器接口 - **⭐⭐ 两条命令进入 `--format json`,矩阵与四条 e2e 不再匹配任何一句话。** diff --git a/src/toolchain/model.cppm b/src/toolchain/model.cppm index e7c98f8f..2111db2b 100644 --- a/src/toolchain/model.cppm +++ b/src/toolchain/model.cppm @@ -427,6 +427,33 @@ std::vector graph_runtime_compile_flags(const Toolchain& tc) { if (!tc.targetCxxRuntime) return out; auto t = triple::parse(tc.targetTriple); if (!t) return out; + // ⚠️⚠️ AND aarch64's RUNTIME LIBRARY CHOICE, WHICH IS A CODEGEN FACT THERE. + // + // On aarch64 `--rtlib=compiler-rt` is not a link-time preference: it moves + // the target-feature set. Measured by `openkal-llvm-runtime`, whose own + // manifest records it: + // + // --target=aarch64-…-musl -fmv +fp-armv8 +neon +v8a + // --target=aarch64-…-musl --rtlib=compiler-rt +fp-armv8 +neon +outline-atomics + // + // The package declared it in `std-module-flags`, which reaches the std + // module's command and nothing else, so `std.pcm` and every consumer TU + // disagreed. Measured on macos-14 building `aarch64-linux-musl` over the + // graph: + // + // error: precompiled file 'std.pcm' was compiled with the target feature + // '+outline-atomics' but the current translation unit is not + // error: current translation unit is compiled with the target feature + // '-fmv' but the precompiled file 'std.pcm' was not + // + // ⭐ SAME DEFECT AS `-fdwarf-exceptions`, ONE FLAG LATER — see the note in + // hostflags.cppm, which describes that one in these words: "its objects + // agreed with each other and nothing else did". A property of the graph + // cannot be declared by one package for one command. + // + // ⚠️ x86_64 HAS NO SUCH FEATURE, so both sides listed nothing there and the + // defect was invisible until a second architecture was built. + if (t->arch == "aarch64") out.emplace_back("--rtlib=compiler-rt"); if (t->is_pe()) out.emplace_back("-fdwarf-exceptions"); if (t->is_pe() || t->os == "macos") out.emplace_back("-femulated-tls"); // ⭐⭐ MACH-O ONLY, AND THE REASON IS THAT WEAK-DEF IS A RUN-TIME MECHANISM diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 19b053fd..08aead02 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -643,6 +643,29 @@ bool host_can_serve(const triple::Triple& target) { return mcpp::platform::is_linux && mcpp::platform::host_arch == "x86_64"; } + // ⚠️⚠️ PE + musl HAS NO PAYLOAD ON ANY HOST, INCLUDING WINDOWS. + // + // `triple::pin_is_capability()` already says so — no gcc emits a PE with a + // musl C library, and LLVM cannot spell the triple — and chapter 16 states + // it in those words: "A payload for it does not exist on any host; its + // system can only come from a dependency graph." + // + // This line disagreed, on exactly one host. Measured on windows-2022, + // payload system: + // + // c-abi musl (payload) + // c++-abi msvc-stl (payload) + // lld-link: error: undefined symbol: __main + // lld-link: error: undefined symbol: __mingw_vfprintf + // + // — musl's C library, MSVC's STL and MinGW's CRT symbols in one link. On + // Linux the same cell already answered `host-cannot-serve`, which is the + // right answer everywhere. + // + // ⭐ The graph path is untouched: this refusal is held and released only + // when nothing supplies the target's system, and `graph × windows-musl` is + // `ok` on both hosts. + if (target.is_pe() && target.is_musl()) return false; if (target.os == "windows") return bool(mcpp::platform::is_windows); if (target.os == "macos") return bool(mcpp::platform::is_macos); From 5120be0271256f3ed4f4cbb701469dcd0071e79e Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:51:30 +0800 Subject: [PATCH 29/32] test(matrix): a trailing CR made 38 of Windows's 48 cells an unnamed refusal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit payload windows-x86_64 aarch64-linux-gnu^M llvm@22.1.8 … unsupported other git-bash runs jq with stdout in text mode, so `\n` becomes `\r\n`, and `\r` is not in IFS — every word keeps one. `--target "aarch64-linux-gnu\r"` does not parse, and the whole host was scanning a corrupted target list. ⭐ `other` earned its place again. Thirty-eight identical unnamed refusals are impossible to read as anything but a systemic fault; had the uncoded branch been folded into a neighbouring reason, that host would have looked plausibly "mostly unsupported". ⚠️ AND THE POISONED ROWS NEARLY BECAME THE EXPECTED TABLE. Backfilling all four hosts, 124 measured rows produced 80 — the 44 Windows rows whose target name carried a CR were dropped by the field-count filter, and the 4 that survived would have been declared as fact and gone green for ever. Row counts were checked; Windows is now marked as awaiting a clean measurement. ⭐ The strip is at the point of reading — one `jq_r` helper — not at each use. The latter is the same decision written N times, which this branch has already paid for four times today. The other three hosts are backfilled from their own machines: linux-x86_64 40, linux-aarch64 16 (musl-gcc only, llvm deferred), macos-arm64 20. --- ..._naming_the_host_target_changes_nothing.sh | 4 +- ...report_names_is_what_the_link_line_uses.sh | 6 +-- ...97_a_capability_pin_is_not_a_preference.sh | 8 ++-- ...ding_a_convention_requires_replacing_it.sh | 12 ++--- tests/matrix/expected.tsv | 48 +++++++++++++++++-- tests/matrix/scan.sh | 20 ++++++-- 6 files changed, 75 insertions(+), 23 deletions(-) diff --git a/tests/e2e/295_naming_the_host_target_changes_nothing.sh b/tests/e2e/295_naming_the_host_target_changes_nothing.sh index 26b38071..d15bc1f2 100755 --- a/tests/e2e/295_naming_the_host_target_changes_nothing.sh +++ b/tests/e2e/295_naming_the_host_target_changes_nothing.sh @@ -56,7 +56,7 @@ printf '#include \nint main() { std::printf("ok\\n"); }\n' > src/main.cp # then name that. Exact on every host, and it needs no table. implicit_target() { # toolchain spec → the triple it would use anyway "$MCPP" why toolchain --toolchain "$1" --format json 2>/dev/null \ - | jq -r '.data.triple.toolchain // empty' + | jq -r '.data.triple.toolchain // empty' | tr -d '\r' } ldflags_of() { # extra args… → the ldflags line, or nothing @@ -85,7 +85,7 @@ for tc in gcc llvm; do # `grep -oP`, which does not exist on macOS and would have turned this half # into a silent skip there. ver="$("$MCPP" toolchain list --format json 2>/dev/null \ - | jq -r --arg t "$tc" '[.data.toolchains[] | select(.family==$t) | .version][0] // empty')" + | jq -r --arg t "$tc" '[.data.toolchains[] | select(.family==$t) | .version][0] // empty' | tr -d '\r')" if [ -z "$ver" ]; then echo " SKIP $tc is not installed here" continue diff --git a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh index 02ca656c..7450f48e 100755 --- a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh +++ b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh @@ -87,7 +87,7 @@ fail=0; checked=0 # # This is cheap and catches the whole class, on every host, for every family. for spec in $("$MCPP" toolchain list --format json 2>/dev/null \ - | jq -r '.data.toolchains[] | .family + "@" + .version'); do + | jq -r '.data.toolchains[] | .family + "@" + .version' | tr -d '\r'); do if "$MCPP" why toolchain --toolchain "$spec" --format json >/dev/null 2>&1; then echo " ok $spec round-trips through --toolchain" checked=$((checked+1)) @@ -102,7 +102,7 @@ for tc in gcc llvm; do # ⭐ From the machine interface: `grep -oP` does not exist on macOS, and its # failure mode here was a skip rather than a red. ver="$("$MCPP" toolchain list --format json 2>/dev/null \ - | jq -r --arg t "$tc" '[.data.toolchains[] | select(.family==$t) | .version][0] // empty')" + | jq -r --arg t "$tc" '[.data.toolchains[] | select(.family==$t) | .version][0] // empty' | tr -d '\r')" [ -n "$ver" ] || { echo " SKIP $tc is not installed here"; continue; } printf '[package]\nname = "linkprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefault = "%s@%s"\n' \ "$tc" "$ver" > mcpp.toml @@ -131,7 +131,7 @@ for tc in gcc llvm; do # SDK belongs to the machine, not to a payload. The test was asserting a # Linux arrangement and calling its absence a defect. want="$("$MCPP" why toolchain --toolchain "$tc@$ver" --format json 2>/dev/null \ - | jq -r '.data.cLibrary.path // ""')" + | jq -r '.data.cLibrary.path // ""' | tr -d '\r')" if [ -z "$want" ]; then # ⭐⭐ NO PATH IS ALSO A CLAIM, AND IT IS CHECKABLE. The query says mcpp # passes no C-library path — the self-contained arrangement, where the diff --git a/tests/e2e/297_a_capability_pin_is_not_a_preference.sh b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh index 59b9a62a..558284d4 100755 --- a/tests/e2e/297_a_capability_pin_is_not_a_preference.sh +++ b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh @@ -41,7 +41,7 @@ printf 'extern "C" int main(int, char**, char**) { return 0; }\n' > src/main.cpp # people: a column position is not a contract, and two earlier drafts of the # neighbouring test disagreed about which column held the version. gccver="$("$MCPP" toolchain list --format json 2>/dev/null \ - | jq -r '[.data.toolchains[] | select(.family=="gcc") | .version][0] // empty')" + | jq -r '[.data.toolchains[] | select(.family=="gcc") | .version][0] // empty' | tr -d '\r')" if [ -z "$gccver" ]; then echo "SKIP: gcc is not installed here, and this test is about declaring it" exit 0 @@ -64,7 +64,7 @@ out="$(cd "$work" && "$MCPP" build --target riscv64-none-elf 2>&1 || true)" # the assertion then passed by matching nothing at all. reason="$(cd "$work" && "$MCPP" why toolchain --target riscv64-none-elf \ --toolchain "gcc@$gccver" --format json 2>/dev/null \ - | jq -r '.data.reason // "-"')" + | jq -r '.data.reason // "-"' | tr -d '\r')" case "$reason" in capability-pin) @@ -118,7 +118,7 @@ printf '[package]\nname = "capprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefau "$gccver" > mcpp.toml printf '#include \nint main() { std::printf("ok\\n"); }\n' > src/main.cpp hostedTarget="$(cd "$work" && "$MCPP" why toolchain --toolchain "gcc@$gccver" \ - --format json 2>/dev/null | jq -r '.data.triple.toolchain // empty')" + --format json 2>/dev/null | jq -r '.data.triple.toolchain // empty' | tr -d '\r')" if [ -z "$hostedTarget" ]; then echo "SKIP: the query did not name a target for gcc@$gccver on this host" exit 0 @@ -127,7 +127,7 @@ rm -rf target hosted="$(cd "$work" && "$MCPP" build --target "$hostedTarget" 2>&1 || true)" hostedReason="$(cd "$work" && "$MCPP" why toolchain --target "$hostedTarget" \ --toolchain "gcc@$gccver" --format json 2>/dev/null \ - | jq -r '.data.reason // "-"' 2>/dev/null)" + | jq -r '.data.reason // "-"' 2>/dev/null | tr -d '\r')" [ -n "$hostedReason" ] || hostedReason="-" case "$hostedReason" in diff --git a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh index ee894a77..787506eb 100644 --- a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh +++ b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh @@ -42,7 +42,7 @@ cd "$work" printf '#include \nint main() { std::printf("ok\\n"); }\n' > src/main.cpp llvmver="$("$MCPP" toolchain list --format json 2>/dev/null \ - | jq -r '[.data.toolchains[] | select(.family=="llvm") | .version][0] // empty')" + | jq -r '[.data.toolchains[] | select(.family=="llvm") | .version][0] // empty' | tr -d '\r')" if [ -z "$llvmver" ]; then echo "SKIP: llvm is not installed here, and this test is about declaring it" exit 0 @@ -80,14 +80,14 @@ fail=0 # written from a guess is the kind that later talks someone out of a correct # change. hostArch="$("$MCPP" toolchain list --format json 2>/dev/null \ - | jq -r '.data.host // "" | split("-")[0]')" + | jq -r '.data.host // "" | split("-")[0]' | tr -d '\r')" pick() { "$MCPP" toolchain list --format json 2>/dev/null \ | jq -r --arg a "$1" '[.data.targets[] | select(.pin | startswith("gcc")) | select(.status != "planned") | select($a == "" or (.target | startswith($a + "-"))) - | .target][0] // empty' + | .target][0] // empty' | tr -d '\r' } pinned="$(pick "$hostArch")" [ -n "$pinned" ] || pinned="$(pick "")" @@ -106,7 +106,7 @@ out="$("$MCPP" build --target "$pinned" 2>&1 || true)" # the sentence is still checked below, because naming the target, the convention # and the replacement is a promise no code can keep on its own. reason="$("$MCPP" why toolchain --target "$pinned" --toolchain "llvm@$llvmver" \ - --format json 2>/dev/null | jq -r '.data.reason // "-"')" + --format json 2>/dev/null | jq -r '.data.reason // "-"' | tr -d '\r')" case "$reason" in convention-unreplaced) @@ -174,10 +174,10 @@ graph="$("$MCPP" build --target "$pinned" --verbose 2>&1 || true)" # really did supply the C library (otherwise "not refused" is vacuous — an # unresolved dependency would satisfy it too). graphReason="$("$MCPP" why toolchain --target "$pinned" --toolchain "llvm@$llvmver" \ - --format json 2>/dev/null | jq -r '.data.reason // "-"')" + --format json 2>/dev/null | jq -r '.data.reason // "-"' | tr -d '\r')" graphCabi="$("$MCPP" why toolchain --target "$pinned" --toolchain "llvm@$llvmver" \ --format json 2>/dev/null \ - | jq -r '[.data.layers[] | select(.layer=="c-abi") | .origin][0] // "-"')" + | jq -r '[.data.layers[] | select(.layer=="c-abi") | .origin][0] // "-"' | tr -d '\r')" if [ "$graphReason" = convention-unreplaced ]; then echo "FAIL: a graph-supplied C library was refused — the escape hatch is gone" diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index 57b352f4..05ca6749 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -31,9 +31,25 @@ # 所以在 x86_64 上够得着、在 aarch64 上够不着。只写 `linux` 会让后跑的一台把先跑 # 的那台在这张表里的行「解释掉」,而覆盖的方向取决于谁后跑,不取决于谁对。 # -# linux-x86_64 行:2026-08-26 实测(mcpp 2026.8.26.1)。 -# linux-aarch64 / macos-arm64 / windows-x86_64 行:留待各自宿主的第一次 CI 运行 -# 回填 —— ⚠️ 从别的宿主推断出来的一行,断言的是推断而不是那台机器。 +# 全部四台:2026-08-26 由 `ci-target-matrix.yml` 实测(mcpp 2026.8.26.1)。 +# ⚠️ 每一行都来自它自己那台机器 —— 从别的宿主推断出来的一行,断言的是推断而不是 +# 那台机器。 +# +# ⚠️ 各台格数不同,而这是事实不是遗漏: +# linux-x86_64 40 gcc + llvm × 12 目标(payload 24 / graph 16) +# linux-aarch64 16 只有 musl-gcc —— llvm 在非 x86_64 Linux 上被显式延缓 +# macos-arm64 20 只有 llvm × 12 目标(payload 12 / graph 8) +# windows-x86_64 -- ⚠️ 待回填:上一轮那台的测量被 CR 污染(见下),数据不可用 +payload linux-aarch64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned +payload linux-aarch64 aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none +payload linux-aarch64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned +payload linux-aarch64 riscv64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported host-cannot-serve +payload linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned +payload linux-aarch64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve payload linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned payload linux-x86_64 aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none payload linux-x86_64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin @@ -58,6 +74,24 @@ payload linux-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned payload linux-x86_64 x86_64-none-elf llvm@22.1.8 x86_64-none-elf payload - - - ok none payload linux-x86_64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced payload linux-x86_64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve +payload macos-arm64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +payload macos-arm64 aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload macos-arm64 aarch64-macos llvm@22.1.8 arm64-apple-macos14.0 host libSystem(payload) libc++(payload) - ok none +payload macos-arm64 aarch64-none-elf llvm@22.1.8 aarch64-none-elf host - - - ok none +payload macos-arm64 riscv32-none-elf llvm@22.1.8 riscv32-none-elf host - - - ok none +payload macos-arm64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +payload macos-arm64 riscv64-none-elf llvm@22.1.8 riscv64-none-elf host - - - ok none +payload macos-arm64 x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload macos-arm64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +payload macos-arm64 x86_64-none-elf llvm@22.1.8 x86_64-none-elf host - - - ok none +payload macos-arm64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload macos-arm64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve +graph linux-aarch64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-aarch64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-aarch64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-aarch64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported host-cannot-serve +graph linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve graph linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned graph linux-x86_64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement graph linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned @@ -74,3 +108,11 @@ graph linux-x86_64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl paylo graph linux-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned graph linux-x86_64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph linux-x86_64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +graph macos-arm64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 aarch64-macos llvm@22.1.8 arm64-apple-macos14.0 host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +graph macos-arm64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +graph macos-arm64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh index 7f69964c..db942661 100755 --- a/tests/matrix/scan.sh +++ b/tests/matrix/scan.sh @@ -91,10 +91,20 @@ HOST="$HOST_OS-$HOST_ARCH" # 就会漂移,而按列宽解析一张给人看的表,会让列宽变成测试套件的一部分。实测过 # 的代价:两版测试对「版本在第几列」的看法不同,读 `$NF` 的那版取到了 # `(default)` —— 一个恰好只出现在最可能被选中的那一行上的值。 -LIST="$("$MCPP" toolchain list --format json 2>/dev/null)" +# ⚠️⚠️ CR 必须剥掉,而不是指望它不出现。 +# +# git-bash 里 jq 以文本模式写 stdout,`\n` 变成 `\r\n`,而 `\r` 不在 IFS 里 —— +# 于是每个词尾都挂着一个 CR。实测 windows-2022:48 格里 38 格是 +# `unsupported / other`,因为 `--target "aarch64-linux-gnu\r"` 解析不了。 +# 整台机器的扫描测的是一张被污染的目标表。 +# +# ⭐ 剥在**读进来的那一处**,不在每个使用点 —— 后者是同一个决定写 N 遍。 +jq_r() { jq -r "$@" | tr -d '\r'; } + +LIST="$("$MCPP" toolchain list --format json 2>/dev/null | tr -d '\r')" [ -n "$LIST" ] || { echo "scan: \`toolchain list --format json\` produced nothing" >&2; exit 2; } -targets() { printf '%s' "$LIST" | jq -r '.data.targets[].target' | sort -u; } +targets() { printf '%s' "$LIST" | jq_r '.data.targets[].target' | sort -u; } # ⭐ 每族只取最新的一个。矩阵回答的是「这个目标支不支持」,同一族的三个版本对 # 这个问题给同一个答案,而 5×12 与 2×12 在 CI 上是小时级的差别。 @@ -102,7 +112,7 @@ targets() { printf '%s' "$LIST" | jq -r '.data.targets[].target' | sort -u; } # ⚠️ 但收窄必须说出来。被丢掉的版本写到 stderr —— 一次没跑的测量和一次通过的 # 测量,在退出码上没有区别。 compilers() { - printf '%s' "$LIST" | jq -r '.data.toolchains[] | .family + "@" + .version' \ + printf '%s' "$LIST" | jq_r '.data.toolchains[] | .family + "@" + .version' \ | awk -F@ '{ if (seen[$1]++) print "scan: 略过 " $0 " —— 每族只取最新" > "/dev/stderr" else print $0 }' } @@ -151,7 +161,7 @@ for tc in $(compilers); do # 全红,原因(`timeout` 不存在)被这个重定向吞掉了。 q="$(run_limited "${MATRIX_QUERY_TIMEOUT:-300}" \ "$MCPP" why toolchain --target "$t" --toolchain "$tc" --format json \ - 2>"$work/q.err")" + 2>"$work/q.err" | tr -d '\r')" if [ -z "$q" ]; then # ⚠️ 查询本身没跑起来。这不是「这一格不支持」,而是「不知道」—— 两者必须 # 分开,否则一次环境故障会被整片读成「不支持」。 @@ -159,7 +169,7 @@ for tc in $(compilers); do emit "$MODE" "$HOST" "$t" "$tc" - - - - - mismatch query-failed continue fi - jq_get() { printf '%s' "$q" | jq -r "$1" 2>/dev/null; } + jq_get() { printf '%s' "$q" | jq_r "$1" 2>/dev/null; } st="$(jq_get '.data.status // "-"')" rs="$(jq_get '.data.reason // "-"')" tri="$(jq_get '(.data.triple.llvm // "") | if . == "" then "-" else . end')" From 71603328bb62ce4afffa18a7d847f5377bb59fc3 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:21:10 +0800 Subject: [PATCH 30/32] fix(freestanding): the caller ignored a contract its callee had written down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `resolve_lld` says, in its own comment: "Returns \"\" when the payload has no LLD, and the caller must treat that as a hard error rather than falling back — falling back is the failure." The caller did fall back, and not by choice: one condition set two things and only one of them could fail. With an `lldEmulation` the branch assigned `f.ldDriver = in.lld` AND emitted linker-vocabulary flags, so an empty `in.lld` left the flags and took the linker away. Measured on windows-2022, `--target x86_64-none-elf`: clang++: error: unknown argument: '-m' clang++: error: unknown argument: '--no-dynamic-linker' — ld.lld's words in clang++'s mouth. ⭐ `x86_64-none-elf` is the only row carrying an `lldEmulation`; riscv and aarch64 reach lld through the driver. One row on one host was the whole exposure. `compute_flags` returns flags rather than an expected, so the refusal cannot be raised there. It now falls back to the DRIVER line — the shape the other three bare-metal rows already use — and says so through `diag::degraded`. If that fails, it fails in the driver's own vocabulary. ⚠️ And an `other` cell now prints the refusal's message. `other` is a visible admission that a branch has no name yet, and an admission has to be followable: without the message the next reader sees the same unexplained `unsupported` the token was introduced to prevent. --- src/build/flags.cppm | 37 ++++++++++++++++++++++++++++++++++++- tests/matrix/scan.sh | 7 +++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 2ba8b83c..9cb92a34 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -14,6 +14,8 @@ export module mcpp.build.flags; import std; import mcpp.build.distribution; import mcpp.build.plan; +import mcpp.build.refusal; +import mcpp.diag; import mcpp.freestanding.target; import mcpp.freestanding.linkline; import mcpp.manifest.types; @@ -1718,8 +1720,41 @@ CompileFlags compute_flags(const BuildPlan& plan) { // mcpp.freestanding.target. `--no-default-config` is then not // needed either: the payload's clang config file is a DRIVER // config, and nothing here runs the driver. + // ⚠️⚠️ THE CALLEE'S COMMENT STATES A CONTRACT AND THE CALLER DID + // NOT KEEP IT. `resolve_lld` says: "Returns \"\" when the payload + // has no LLD, and the caller must treat that as a hard error rather + // than falling back — falling back is the failure." + // + // One condition set two things and only one of them could fail: + // with an `lldEmulation` the branch assigned `f.ldDriver = in.lld` + // AND emitted linker-vocabulary flags. An empty `in.lld` left the + // flags and took the linker away, so `ld.lld`'s words went to + // `clang++`. Measured on windows-2022, `--target x86_64-none-elf`: + // + // clang++: error: unknown argument: '-m' + // clang++: error: unknown argument: '--no-dynamic-linker' + // + // ⭐ `x86_64-none-elf` is the ONLY row carrying an `lldEmulation` — + // the riscv and aarch64 rows reach lld through the driver — so one + // row on one host was the whole exposure. + // + // ⚠️ `compute_flags` returns flags, not an expected, so the refusal + // cannot be raised here. Falling back to the DRIVER line is the + // honest second choice: it is the shape the other three bare-metal + // rows already use, and its failure, if any, is stated in the + // driver's own vocabulary rather than in a linker's. + const bool lldDirect = + !spec->lldEmulation.empty() && !in.lld.empty(); + if (!spec->lldEmulation.empty() && in.lld.empty()) + mcpp::diag::degraded( + "freestanding/link", + std::format("target '{}' asks for a direct lld link and this " + "payload ships none", spec->triple), + "the compiler driver links instead, as it does for every " + "other bare-metal row", + "install a toolchain whose payload contains ld.lld"); std::string fsLd; - if (!spec->lldEmulation.empty()) { + if (lldDirect) { f.ldDriver = in.lld; fsLd = mcpp::freestanding::link_flags_direct(*spec, in, ninjaEsc); } else { diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh index db942661..93d4b0fb 100755 --- a/tests/matrix/scan.sh +++ b/tests/matrix/scan.sh @@ -185,6 +185,13 @@ for tc in $(compilers); do : "${tri:=-}" "${clib:=-}" "${cabi:=-}" "${cxxabi:=-}" "${okpkg:=-}" if [ "$st" = refused ]; then + # ⚠️ `other` 是「拒绝了而这一处分支还没有名字」。它是一句可见的承认, + # 而承认之后要能查 —— 否则下一个人看到的仍是一个没有原因的 unsupported。 + # 拒绝的**消息**在信封的 diagnostics 里,这里把它打出来。 + if [ "$rs" = other ]; then + echo "scan: $t × $tc 无名拒绝:" >&2 + jq_get '.diagnostics[0].message // "(无消息)"' | head -3 | sed 's/^/ /' >&2 + fi emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" \ unsupported "$rs" continue From 01779f2df459c1953a2bac0e771528bf1e559a82 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:45:49 +0800 Subject: [PATCH 31/32] =?UTF-8?q?fix(freestanding):=20refuse=20at=20the=20?= =?UTF-8?q?decision=20=E2=80=94=20quoting=20a=20comment=20is=20not=20obeyi?= =?UTF-8?q?ng=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last commit read `resolve_lld`'s contract, quoted it, and then did the thing it forbids. The fallback to the driver line reproduced exactly what the bypass exists to prevent, on windows-2022: clang++: error: linker (via gcc) command failed collect2.exe: error: ld returned 1 exit status The row's own column comment says why the driver is bypassed for it: "it would hand the link to a host `g++` that cannot take our linker's path". ⚠️ Quoting a comment into one's own and then not following it is worse than not having read it — the words were right there, in the sentence I wrote. ⭐ There is no third shape. The row needs lld by name; when the payload has none, the answer is a refusal at the decision. `prepare` now makes it after the toolchain resolves, with the code `lld-required-absent`. And two more codes for refusals that had none. `build.mcpp under a cross --target needs a resolvable host toolchain` was a correct, readable sentence recorded as four identical `other` cells — the message was right and the classification was missing; it is now `host-tool-toolchain`. The std module precompile failure keeps `other` honestly: it is a build failure surfacing through the refusal channel, not a rule, and its site cannot reach the sink. Local: 4 e2e green, 40 cells match, and all four bare-metal rows still build here — this host has lld, so the refusal correctly does not fire. --- src/build/flags.cppm | 35 +---------------------------- src/build/prepare.cppm | 50 ++++++++++++++++++++++++++++++++++++++++++ src/build/refusal.cppm | 6 +++++ 3 files changed, 57 insertions(+), 34 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 9cb92a34..53921d38 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -1720,41 +1720,8 @@ CompileFlags compute_flags(const BuildPlan& plan) { // mcpp.freestanding.target. `--no-default-config` is then not // needed either: the payload's clang config file is a DRIVER // config, and nothing here runs the driver. - // ⚠️⚠️ THE CALLEE'S COMMENT STATES A CONTRACT AND THE CALLER DID - // NOT KEEP IT. `resolve_lld` says: "Returns \"\" when the payload - // has no LLD, and the caller must treat that as a hard error rather - // than falling back — falling back is the failure." - // - // One condition set two things and only one of them could fail: - // with an `lldEmulation` the branch assigned `f.ldDriver = in.lld` - // AND emitted linker-vocabulary flags. An empty `in.lld` left the - // flags and took the linker away, so `ld.lld`'s words went to - // `clang++`. Measured on windows-2022, `--target x86_64-none-elf`: - // - // clang++: error: unknown argument: '-m' - // clang++: error: unknown argument: '--no-dynamic-linker' - // - // ⭐ `x86_64-none-elf` is the ONLY row carrying an `lldEmulation` — - // the riscv and aarch64 rows reach lld through the driver — so one - // row on one host was the whole exposure. - // - // ⚠️ `compute_flags` returns flags, not an expected, so the refusal - // cannot be raised here. Falling back to the DRIVER line is the - // honest second choice: it is the shape the other three bare-metal - // rows already use, and its failure, if any, is stated in the - // driver's own vocabulary rather than in a linker's. - const bool lldDirect = - !spec->lldEmulation.empty() && !in.lld.empty(); - if (!spec->lldEmulation.empty() && in.lld.empty()) - mcpp::diag::degraded( - "freestanding/link", - std::format("target '{}' asks for a direct lld link and this " - "payload ships none", spec->triple), - "the compiler driver links instead, as it does for every " - "other bare-metal row", - "install a toolchain whose payload contains ld.lld"); std::string fsLd; - if (lldDirect) { + if (!spec->lldEmulation.empty()) { f.ldDriver = in.lld; fsLd = mcpp::freestanding::link_flags_direct(*spec, in, ninjaEsc); } else { diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 934139b7..fea2d783 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -2622,6 +2622,11 @@ prepare_build(bool print_fingerprint, return std::pair{explicit_compiler, *tc}; if (hostTcCache) return *hostTcCache; if (!tcSpec || *tcSpec == "system" || tcSpecIsMsvc) { + // ⭐ A READABLE REFUSAL THAT HAD NO CODE, so the target matrix + // recorded four identical `other` cells for it. The sentence was + // right; the classification was missing. Measured on windows-2022 + // with `msvc@system` declared and any cross target. + refusal::record(refusal::Code::HostToolToolchain); return std::unexpected(std::string( "build.mcpp under a cross --target needs a resolvable host " "toolchain — set one via [toolchain] or `mcpp toolchain default`")); @@ -6226,6 +6231,51 @@ prepare_build(bool print_fingerprint, in.cAbi = provider_of(tsd::CapLayer::CAbi); in.cxxAbi = provider_of(tsd::CapLayer::CxxAbi); + // ⚠️⚠️ A ROW THAT LINKS THROUGH lld DIRECTLY, ON A PAYLOAD WITH NO lld. + // + // `x86_64-none-elf` is the only row carrying an `lldEmulation`, and its + // column comment in mcpp.freestanding.target says why the driver is + // bypassed for it: the driver "would hand the link to a host `g++` that + // cannot take our linker's path". + // + // ⚠️ MEASURED TWICE ON windows-2022, AND THE SECOND TIME WAS MY OWN + // FALLBACK. First, an empty `resolve_lld` left linker vocabulary on a + // driver line: + // + // clang++: error: unknown argument: '-m' + // + // Then, falling back to the driver line reproduced exactly what the + // bypass exists to prevent: + // + // clang++: error: linker (via gcc) command failed + // collect2.exe: error: ld returned 1 exit status + // + // ⭐ There is no third shape. The row needs lld by name; when the + // payload has none, the answer is a refusal at the decision, not a + // different link. + if (auto fsT = tc.has_value() + ? mcpp::toolchain::triple::parse(tc->targetTriple) + : std::nullopt; + fsT && fsT->is_freestanding()) { + auto fsSpec = mcpp::freestanding::resolve(*fsT); + if (fsSpec && !fsSpec->lldEmulation.empty() + && mcpp::freestanding::resolve_lld(tc->binaryPath).empty()) { + refusal::record(refusal::Code::LldRequiredAbsent); + return std::unexpected(std::format( + "target '{}' links through lld directly, and this toolchain " + "payload ships none.\n" + " The row carries an lld emulation ('{}'), which means " + "the compiler driver is\n" + " bypassed — for this target it would hand the link to " + "a host linker that\n" + " cannot take a freestanding ELF.\n" + " install a toolchain whose payload contains ld.lld, " + "or build this target\n" + " from a host that has one.", + fsT->str(), fsSpec->lldEmulation)); + } + } + resolvedTargetSide = tsd::resolve(in); if (auto why = tsd::check_layering(resolvedTargetSide)) { refusal::record(refusal::Code::LayerOrdering); diff --git a/src/build/refusal.cppm b/src/build/refusal.cppm index 928906f7..41ddc826 100644 --- a/src/build/refusal.cppm +++ b/src/build/refusal.cppm @@ -48,6 +48,9 @@ enum class Code { OsMismatch, // requested and resolved triples name different systems LayerRequirement, // a package requires a layer the resolution did not give it LayerOrdering, // the five layers do not stack (check_layering) + LldRequiredAbsent, // the row links through lld directly and none is here + HostToolToolchain, // build.mcpp under a cross target has no host toolchain + StdModulePrecompile, // the std module could not be precompiled Other, // a refusal that has not been given a code yet }; @@ -61,6 +64,9 @@ constexpr std::string_view name(Code c) { case Code::OsMismatch: return "os-mismatch"; case Code::LayerRequirement: return "layer-requirement"; case Code::LayerOrdering: return "layer-ordering"; + case Code::LldRequiredAbsent: return "lld-required-absent"; + case Code::HostToolToolchain: return "host-tool-toolchain"; + case Code::StdModulePrecompile: return "std-module-precompile"; case Code::Other: return "other"; } return "other"; From 99036d9ca7914ad7df4581acbf830a62edd55a44 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:08:33 +0800 Subject: [PATCH 32/32] =?UTF-8?q?test(matrix):=20all=20four=20build=20host?= =?UTF-8?q?s=20backfilled=20=E2=80=94=20116=20cells,=20no=20mismatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each row comes from its own machine. Validated before being accepted: every line exactly 11 fields, no CR anywhere, 116 rows total — the checks that would have caught the previous attempt, where 44 Windows rows were silently dropped by the field filter and the 4 survivors nearly became fact. linux-x86_64 40 14 ok linux-aarch64 16 1 ok musl-gcc only, llvm deferred macos-arm64 20 10 ok windows-x86_64 40 9 ok ⚠️ One `other` remains and it is honest: `std module precompile failed` is raised in stdmod.cppm, which cannot reach the refusal sink. It is a build failure surfacing through the refusal channel rather than a rule, and folding it into a neighbouring reason would be the wrong repair. --- CHANGELOG.md | 29 +++++++++++++++++++++++++++ tests/matrix/expected.tsv | 42 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a21198d9..1ae48eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,35 @@ ### 目标矩阵在四台构建机上找到的 +⭐⭐ **116 格,0 个 `mismatch`。** 四台各自的实测写在 +[`tests/matrix/expected.tsv`](tests/matrix/expected.tsv),每一行都来自它自己那台 +机器 —— 从别的宿主推断出来的一行,断言的是推断而不是那台机器。 + +| 宿主 | 格数 | ok | +|---|---|---| +| `linux-x86_64` | 40 | 14 | +| `linux-aarch64` | 16 | 1 | +| `macos-arm64` | 20 | 10 | +| `windows-x86_64` | 40 | 9 | + +| status | reason | 格数 | +|---|---|---| +| ok | none | 34 | +| unsupported | tier-planned | 36 | +| unsupported | capability-pin | 16 | +| unsupported | convention-unreplaced | 12 | +| unsupported | host-cannot-serve | 7 | +| unsupported | layer-requirement | 5 | +| unsupported | host-tool-toolchain | 4 | +| unsupported | lld-required-absent | 1 | +| unsupported | other | 1 | + +⚠️ 最后那一格是**诚实的** `other`:`std module precompile failed` 由 +`stdmod.cppm` 发出,那里够不到拒绝记号的沉淀点。它是**构建失败经拒绝通道浮出**, +不是一条规则 —— 把它硬塞进邻近的理由才是错的。 + + + ⭐⭐ **把 `linux-aarch64` 加进构建机轴之后,四台各自交出了一台机器上看不见的缺陷。** 轴取自 `release.yml` 发布的那一组(linux-x86_64 / linux-aarch64 / macos-arm64 / windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二进制却从没被扫过的 diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index 05ca6749..58344101 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -39,7 +39,7 @@ # linux-x86_64 40 gcc + llvm × 12 目标(payload 24 / graph 16) # linux-aarch64 16 只有 musl-gcc —— llvm 在非 x86_64 Linux 上被显式延缓 # macos-arm64 20 只有 llvm × 12 目标(payload 12 / graph 8) -# windows-x86_64 -- ⚠️ 待回填:上一轮那台的测量被 CR 污染(见下),数据不可用 +# windows-x86_64 40 llvm + msvc@system payload linux-aarch64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned payload linux-aarch64 aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none payload linux-aarch64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin @@ -86,6 +86,30 @@ payload macos-arm64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned payload macos-arm64 x86_64-none-elf llvm@22.1.8 x86_64-none-elf host - - - ok none payload macos-arm64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced payload macos-arm64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve +payload windows-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +payload windows-x86_64 aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload windows-x86_64 aarch64-none-elf llvm@22.1.8 aarch64-none-elf none - - - ok none +payload windows-x86_64 riscv32-none-elf llvm@22.1.8 riscv32-none-elf none - - - ok none +payload windows-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +payload windows-x86_64 riscv64-none-elf llvm@22.1.8 riscv64-none-elf none - - - ok none +payload windows-x86_64 x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload windows-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +payload windows-x86_64 x86_64-none-elf llvm@22.1.8 - - - - - unsupported lld-required-absent +payload windows-x86_64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload windows-x86_64 x86_64-windows-msvc llvm@22.1.8 x86_64-pc-windows-msvc none msvc(payload) msvc-stl(payload) - ok none +payload windows-x86_64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve +payload windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported convention-unreplaced +payload windows-x86_64 aarch64-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 riscv32-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 riscv64-linux-musl msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 riscv64-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported convention-unreplaced +payload windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 x86_64-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported convention-unreplaced +payload windows-x86_64 x86_64-windows-msvc msvc@system x86_64-pc-windows-msvc none msvc(payload) (payload) - ok none +payload windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin graph linux-aarch64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned graph linux-aarch64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement graph linux-aarch64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned @@ -116,3 +140,19 @@ graph macos-arm64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl host m graph macos-arm64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned graph macos-arm64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph macos-arm64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +graph windows-x86_64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 x86_64-windows-msvc llvm@22.1.8 - - - - - unsupported other +graph windows-x86_64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 riscv64-linux-musl msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 x86_64-windows-msvc msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin