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..a50a08a9 --- /dev/null +++ b/.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md @@ -0,0 +1,271 @@ +# 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 诚实(✅ 已落地) + +⭐ **决定: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 之后再处理,因为届时它自然成立。 + +### 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 落地的当天变成错的,而没有任何东西 +会提醒你。要问索引,不要记住索引。 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/.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 触碰的每个判据逐个过一遍**。八条里七条出自那次 + 改动,值得确认没有第九条。 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..45e9f8bd --- /dev/null +++ b/.agents/docs/2026-08-26-target-matrix-six-tables.md @@ -0,0 +1,269 @@ +# 目标矩阵:六张表(载荷体系 / 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 宿主上**真跑过** | +| **推** | 由源码逻辑推出,**无本地测量** | +| `-` | 该轴对这一格无意义 | +| **不支持** | 这个组合不成立,并给出原因 | +| **未支持** | 组合成立,但 mcpp 还没做 —— **不是缺陷** | + +⚠️ **「未支持」与「缺陷」必须分开。** 一格没跑通有三种可能,只有第三种是缺陷: + +| 状态 | 判据 | 记号 | +|---|---|---| +| 载荷根本不存在 | 索引里没有这个 sysroot 包 | **未支持** | +| 载荷存在但 mcpp 从不为它接线 | 索引里有,而 mcpp 没有装它、也没有下发对应 flag | **未支持** | +| 载荷已装、别的分支接了,这个分支没接 | 同一份载荷,一个编译器接上了另一个没有 | ❌ **缺陷** | + +### 两道门是分开的 + +一格能不能构建,先后过两道**互相独立**的门: + +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` | `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 | 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++ | - | **未支持**:索引里没有这个包 | +| `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**:载荷已装而未接线 | +| `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 | — | - | **未支持**:`xim:musl` 未接线;诊断是 hermetic | +| `aarch64-linux-musl` | linux | gcc | `aarch64-unknown-linux-musl` | 载荷自带 | musl | libstdc++ | - | ✅ | +| `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**:无载荷,**而诊断说的是别的事** | +| `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 × `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 包 | 同上 | + +### A 是缺陷,而三个 musl/mingw 格不是 + +⭐ **区别在于那份载荷在不在。** 实测: + +``` +$ 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 包 +``` + +- **A**(`x86_64-linux-gnu`)—— `xim:glibc` 已装且带着 `Scrt1.o crti.o crtn.o`, + **gcc 的链接线接了它**: + + ``` + 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。 + +### B 剩下的那一半确实是缺陷 + +即便 llvm × windows-gnu 归为「未支持」,**诊断仍然是错的**:它报 +`ld.lld: error: obj/main.o: unknown file type`,而真因是没有 MinGW sysroot。 + +⚠️ 与 A 对照:A 那格 hermetic 检查**报得很准**(逐个列出落在沙箱外的 `.o`)。 +**同一类问题,一处有准确诊断、一处没有** —— 为什么,未查清。**未实测。** + +--- + +--- + +## 表 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 的全部「推」。 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..d2165951 --- /dev/null +++ b/.agents/docs/2026-08-26-the-support-matrix-measured.md @@ -0,0 +1,233 @@ +# 支持矩阵: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 格**。 + +### 轴的完整定义 + +| 轴 | 本轮取值 | +|---|---| +| **构建机(宿主 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 模式(系统来自编译器载荷) + +| 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:mcpp 没有为这个组合提供目标 sysroot + +⚠️ **本条初稿的测量方式是错的。** 它直接跑 +`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`。 + +构建最终失败于 `ld.lld: error: obj/main.o: unknown file type`,与真正的问题无关。 + +⚠️ **仍未查清:** 密闭性检查对 ① 报了而对 ② 没报。**未实测。** + +**这一格是本文最需要先查清的。** 它当前既不能用,也没有一条说清楚为什么的诊断。 + +### ③ 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/.github/actions/bootstrap-mcpp/action.yml b/.github/actions/bootstrap-mcpp/action.yml index 29de6310..6b50301b 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' @@ -72,9 +88,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 @@ -84,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 @@ -148,7 +212,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 }}- 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 new file mode 100644 index 00000000..6c8ec106 --- /dev/null +++ b/.github/workflows/ci-target-matrix.yml @@ -0,0 +1,363 @@ +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: + # ⭐⭐ 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 + 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 + + # ⚠️ 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 + # ⭐ 这一步存在的全部理由:退出码分不清「通过」与「跳过」。两条 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" + } + # ⚠️⚠️ A SKIP IS ACCEPTED FOR ONE NAMED REASON, NOT ON ONE NAMED HOST. + # + # 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 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; } + if grep -q '^SKIP:' "$1".log && grep -qF "$3" "$1".log; then + echo " ok $1 (declared skip: $(grep -m1 '^SKIP:' "$1".log))" + return 0 + 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 + } + 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 + 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 \ + "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 + 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: + # ⭐ 第二层:全表扫描,与仓库里的期望表比对。 + name: scan (${{ matrix.host }}) + needs: invariants + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + include: + # ⭐⭐ 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 + 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" + + # ⚠️⚠️ 格数不能是缓存状态的函数。 + # + # 实测:同一台 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 + 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 + + 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" diff --git a/CHANGELOG.md b/CHANGELOG.md index c9494284..1ae48eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,215 @@ > 本文件追踪 `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 照常生效。 + +### 目标矩阵在四台构建机上找到的 + +⭐⭐ **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 —— 一台拿到发布二进制却从没被扫过的 +宿主,它的目标表是一句没人核过的话。 + +- **⚠️ `--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 不再匹配任何一句话。** + + ``` + 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 +243,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 +321,11 @@ ### 测试 +- e2e 292,两向断言:声明一层之后裸机目标仍解析到同一个编译器(先建立基线, + 否则分不清「修好了」和「这台机器没有 llvm」);以及宿主行**不得**顶掉项目自己 + 选的工具链——不加区分地永不取消 pin 也能让前一半通过,而那正是这个谓词当初要 + 防的替换。 + - **e2e 292/293/294**,每条两向断言,且**都在修复前的二进制上验证过会失败**: 292 声明一层后裸机目标仍解析到同一编译器 + 宿主行不得顶掉项目自己的工具链; 293 拒绝跨 OS 的解析 + 四个正确交叉目标零误伤;294 列出图供给的目标 + 宿主 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/11-machine-output.md b/docs/11-machine-output.md index 76a6c2ac..6cae2679 100644 --- a/docs/11-machine-output.md +++ b/docs/11-machine-output.md @@ -229,3 +229,66 @@ 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, 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 + +``` +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 ad72f159..967f879c 100644 --- a/docs/16-the-target-triple.md +++ b/docs/16-the-target-triple.md @@ -262,8 +262,185 @@ 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. + +### 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 +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`. + +## 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. + +⭐ **The axis is the set mcpp ships for, and it is (os, arch) rather than os.** +`release.yml` publishes four host binaries: + +| 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` | 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 `—` 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. + +### And CI measures every one of them + +[`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/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/docs/zh/11-machine-output.md b/docs/zh/11-machine-output.md index b12ca2c2..aff81f66 100644 --- a/docs/zh/11-machine-output.md +++ b/docs/zh/11-machine-output.md @@ -200,3 +200,61 @@ 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, pin, status, default}`,`status` 取 +`installed` / `available` / `via dependency graph` / `planned`。 + +⚠️ **`toolchain` 与 `pin` 不是同一个字段写两遍。** `toolchain` 是这一行关联到 +什么 —— 已装的行是装了的载荷,词表行是那一行的约定。`pin` 只承载目标表的约定, +没有约定的行为空。`x86_64-linux-gnu` 装了 gcc 而根本没有约定,所以要挑「约定是 +gcc 的行」必须读 `pin`。 + +### `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 35faceeb..0e8a32fa 100644 --- a/docs/zh/16-the-target-triple.md +++ b/docs/zh/16-the-target-triple.md @@ -235,8 +235,168 @@ 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 的载荷里取。各带各的。 + +### 换另一个编译器,就要把另一份 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` —— 同法实测: + +| 目标 | 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`。 + +## 构建机是第三条轴 + +上面两条轴 —— 用哪个编译器、C 库从哪来 —— 是工程做的选择。第三条不是:它是构建 +运行在哪台机器上。 + +⭐ **这条轴是 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` | +| `x86_64-windows-gnu` | Linux、Windows | 一个身份,只在分发层按宿主分岔 | +| `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+ 的原因。 + +### 而 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)`。 + +⚠️ 每台把什么解析成自己的目标,与它的名字给人的印象并不一致: + +| 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` | + +本章三条判据都曾假设了 Linux 上的那个巧合,并在其它宿主上被纠正。 + +⚠️ **每台的格数不是常数。** 它取决于那台机器装了什么,而同一台 runner 在相邻两轮 +里被测到工具链不同。所以比对断言的是**扫描真的产出了行**、以及**期望表点名的每一 +行都被跑到**,而不是一个总数:一格因为载荷没被恢复而消失,与一格通过了,在退出码 +上没有区别。 + ## 自定义目标 + 不在 mcpp 表内的三元组需要一个显式段落,而这也是一块板子声明 「任何默认值都给不出的事实」的方式: 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..53921d38 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; @@ -613,7 +615,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 +1232,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 +1294,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 +1474,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..fea2d783 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"; @@ -1827,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 @@ -2282,7 +2381,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); @@ -2489,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`")); @@ -4968,6 +5106,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()); } @@ -6037,16 +6231,65 @@ 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)) + 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 +6303,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 +6438,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..41ddc826 --- /dev/null +++ b/src/build/refusal.cppm @@ -0,0 +1,99 @@ +// 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) + 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 +}; + +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::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"; +} + +// 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..e5f5aa84 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,189 @@ 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. + // + // ⚠️ 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()) + 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. + // + // `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..646d0e89 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,37 @@ 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()) + // ⚠️⚠️ `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", "system"}, + {"displayVersion", 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; @@ -536,6 +589,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 @@ -567,6 +634,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 @@ -629,6 +698,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 @@ -645,6 +715,28 @@ 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}, + {"pin", r.pin}, + {"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/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 e81953e5..08aead02 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 @@ -588,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 @@ -602,8 +634,38 @@ 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"; + } + // ⚠️⚠️ 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); @@ -624,11 +686,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) { @@ -644,7 +734,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; } 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..d15bc1f2 --- /dev/null +++ b/tests/e2e/295_naming_the_host_target_changes_nothing.sh @@ -0,0 +1,143 @@ +#!/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. +# ⚠️⚠️ 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' | tr -d '\r' +} + +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' | tr -d '\r')" + 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="$(implicit_target "$tc@$ver")" + if [ -z "$ht" ]; then + echo " SKIP the query did not name a target for $tc@$ver" + 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..7450f48e --- /dev/null +++ b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh @@ -0,0 +1,220 @@ +#!/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="" + 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. +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' | 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)) + 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 + # 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' | 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 + 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 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 // ""' | 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 + # 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 + echo " ok $tc@$ver: the C library the query names is on the link line" + else + 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 + +# ── 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..558284d4 --- /dev/null +++ b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh @@ -0,0 +1,153 @@ +#!/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' | tr -d '\r')" +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 // "-"' | tr -d '\r')" + +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 ─────────────── +# +# ⚠️⚠️ 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' | tr -d '\r')" +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 "$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 | tr -d '\r')" +[ -n "$hostedReason" ] || hostedReason="-" + +case "$hostedReason" in + capability-pin) + echo "FAIL: $hostedTarget 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..787506eb --- /dev/null +++ b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh @@ -0,0 +1,198 @@ +#!/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' | tr -d '\r')" +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. +# ⭐⭐ `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. +# ⚠️ 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]' | 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' | tr -d '\r' +} +pinned="$(pick "$hostArch")" +[ -n "$pinned" ] || pinned="$(pick "")" +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 // "-"' | tr -d '\r')" + +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 // "-"' | 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] // "-"' | tr -d '\r')" + +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..58344101 --- /dev/null +++ b/tests/matrix/expected.tsv @@ -0,0 +1,158 @@ +# 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 —— 那是两条不同的规则,换了一条却不红,这张表就 +# 只在说「它没建出来」,不在说「为什么」。 +# +# ⚠️⚠️ 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` 会让后跑的一台把先跑 +# 的那台在这张表里的行「解释掉」,而覆盖的方向取决于谁后跑,不取决于谁对。 +# +# 全部四台: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 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 +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 +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 +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 +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 +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 +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 +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 +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 diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh new file mode 100755 index 00000000..93d4b0fb --- /dev/null +++ b/tests/matrix/scan.sh @@ -0,0 +1,222 @@ +#!/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 +} + +# ⚠️⚠️ `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" + +# ⚠️⚠️ 构建机是 (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_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 自己的机器接口,而不是脚本里再抄一份 —— 抄一份 +# 就会漂移,而按列宽解析一张给人看的表,会让列宽变成测试套件的一部分。实测过 +# 的代价:两版测试对「版本在第几列」的看法不同,读 `$NF` 的那版取到了 +# `(default)` —— 一个恰好只出现在最可能被选中的那一行上的值。 +# ⚠️⚠️ 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; } + +# ⭐ 每族只取最新的一个。矩阵回答的是「这个目标支不支持」,同一族的三个版本对 +# 这个问题给同一个答案,而 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 + + # ── 第一问:这一格会解析成什么 ──────────────────────────────────── + # ⚠️ 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" | tr -d '\r')" + 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 + 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 + # ⚠️ `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 + fi + + # ── 第二问:解析说可以,那它建得出来吗 ──────────────────────────── + # + # ⭐ 退出码就是判据。`why` 已经回答了「为什么不」那一半,所以这里不需要再去 + # 读任何一行输出。 + printf '\n[toolchain]\ndefault = "%s"\n' "$tc" >> mcpp.toml + rm -rf target + 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 格 × 一份完整日志读不动。 + # ⚠️ 三行,**整行**。前一版 `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 + done +done