diff --git a/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md b/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md new file mode 100644 index 000000000..32f1ad0c1 --- /dev/null +++ b/.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md @@ -0,0 +1,875 @@ +--- +subject: ecosystem +status: active +--- + +# 生态架构优化方案:四个平面的收敛、已合入项的纠正与新系统的准入(mcpp × openkal × xlings) + +**日期:** 2026-11-12 +**状态:** 第三版,实施中。第二版并入了 D16–D23 的裁决、CI 触发模型(5.11)、固件与裸机(5.12)、归属判据(P16)、 +层级三与固件的本地探针(第 8 节)与多视角自审(第 14 节)。负责人于 2026-11-12 授权按本文实施,第 11.2 节的决定按其推荐 +执行,其中 D5 由"每个仓库一个 PR"取代(mcpp#924 续用,不拆分)。第三版增加 P17 与 5.13(重复实现的收敛),并在第 15 节 +记录实施中对本文的修正与各项的门禁。 +**范围:** mcpp(引擎、内置数据、规范、CI);openkal 家族(规范、各实现、openkal-musl、openkal-llvm-runtime、 +openkal-image、openkal-platform);xlings 生态(xim-pkgindex、xpkgindex-update、xlings-res 资产、dirun);mtest(库与引擎)。 +验证工程(一个 C89 命令解释器)只用于检验,不在上游范围内,本文不点名。 +**输入:** +[2026-11-08 顶层设计](2026-11-08-ecosystem-top-level-design.md)、 +[2026-11-09 目标模型](2026-11-09-target-model-design.md)、 +[2026-11-10 跨平台生态方案](2026-11-10-cross-platform-ecosystem-design.md)(P1–P9、Q1–Q6、E/K/X 工作项仍然有效)、 +[2026-11-10 测试与解析补充](2026-11-10-test-and-resolution-supplement-design.md)、 +[2026-11-12 三平面架构](2026-11-12-three-plane-platform-architecture-design.md)(本文取代其第 3 节的"已成立"陈述,见第 13 节); +mcpp#924 的审计(2026-11-12,head `0819c80e`,分支二进制与 2026.11.10.2 的对照实测);验证工程的本地报告与缺口分析(G1–G8)。 + +**证据标记。** 本文每个事实带一个标记:〔测〕本地实测或 CI 日志可复核;〔读〕读代码或数据得出,未运行; +〔析〕分析与推断,实施前需要先做第 8.4 节的探针。 + +## 1. 目的 + +2026-11-10 方案回答了"能力够不够"。它落地后的一轮实施(mcpp#924 与外围九个 PR)暴露的是另一类问题:能力在增加, +但**同一类缺陷重复出现**——数据驱动的陈述与引擎里新增的系统名并存,本地通过而 CI 与新装用户路径不通过,资产修订两次 +违反同一条布局约定。本文不再逐项补能力,而是回答三个问题: + +1. 哪些结构使这些缺陷可以重复发生,改哪里能让它们**机械地不可能**(第 3–5 节)。 +2. 已合入与在途的改动里,哪些不合理,各自怎样纠正(第 6 节)。负责人对三个生态都有决定权,所以已合入项可以用追加 PR + 纠正;纠正仍然先在本文定稿。 +3. 以验证工程为尺,通用基础设施要到什么程度才算"支持一个程序去到所有平台",包括 32 位与 16 位 DOS 的可能性;这些设施 + 对验证工程之外的领域有什么收益(第 7–9 节)。 + +## 2. 现状台账 + +### 2.1 已合入并有门禁的(基线,不重做) + +| 项 | 版本 | 门禁 | +|---|---|---| +| E1–E10、E13、E14、E15:行与提供者由数据读取、共用入口分别编译、GNU ABI 资源、FreeBSD 两种身份、基座包、层依赖不成环、接口集合暴露 | mcpp 2026.11.10.1–.2 | e2e 1054–1064、1073 | +| E9、X1:`mingw-llvm` 提供者与 `mingw-w64-sysroot` 基座,三种宿主产出 Windows PE | 同上 | e2e 1063(wine) | +| K1、K5、K6、K8、K9:按接口绑定的 okrun、硬链接、stop word 计数、openkal-freebsd、FreeBSD 的 B 路线(K11 按系统约定查找程序:是否已发布待确认) | openkal 0.25 族、openkal-platform 0.26.0 | 各仓 CI;FreeBSD 虚拟机作业 | +| 修订通道:同一版本键下的矫正修订 | xpkgindex-update #9、#10 | 仓内 CI | +| G4:okrun 打开向上的相对名字 | openkal-image 0.6.2 | 仓内 CI | +| 测试体系迁到 mtest | 各仓 | 各仓 CI | + +### 2.2 已合入但需要纠正的(第 6 节给出方案) + +| 项 | 位置 | 问题 | 证据 | +|---|---|---|---| +| `aarch64-linux-gnu-sysroot` 2.44.3 修订 2 | xim #1140 | 归档顶层目录与资产名不一致;`lib/libc++.so` 是指向缺席文件的链接脚本;清单没有 `std-module`;`provides` 写 libgcc 而链接用 compiler-rt | 〔测〕 | +| llvm 23.1.3 修订 2 | xim #1140 → #1143 | 修订 2 使全平台安装失败约 1 小时 40 分后由修订 3 修复;索引 PR 的安装检查有 15 项被跳过 | 〔测〕 | +| FreeBSD 镜像的包名 | xim #1140、#1141 | `freebsd-vm` 与 `freebsd-disk` 两个包并存,约定文档写的是 `-disk-image`,三个名字 | 〔读〕 | +| FreeBSD 运行器 | openkal-freebsd #2 | 运行器声明是 README 里手写的绝对路径加环境变量,只有 x86_64;与 dirun 是同一机制的第二份实现 | 〔读〕 | +| dirun | Sunrisepeak/dirun #1 | 自带测试模块而不用 mtest;Windows 的 pty 是桩;mcpplibs 下留有已归档的同名空仓 | 〔读〕 | +| 评审规则 | xim #1140–#1143 | 在 `REVIEW_REQUIRED` 状态下以管理员权限合入 | 〔测〕 | + +### 2.3 在途未合入的(mcpp#924) + +| 项 | 状态 | 证据 | +|---|---|---| +| G3 资源头第三源、G5 `--systems all`、G6 异系统死亡诊断、G8 `abi longjmp` | Linux 腿绿;各有硬编码或语义问题(第 6 节 C6–C10) | 〔测〕〔读〕 | +| G1 `aarch64-linux-gnu` 交叉 | 只对纯 C 成立;C++ 两种写法都失败;产物的 `PT_INTERP` 与 `RPATH` 是构建机的绝对路径;CI 零覆盖 | 〔测〕 | +| 回归:GCC + `PayloadFirst` 无法链接 | `flags.cppm` 的 GCC 分支被改写;gcc 15.1.0 在 2026.11.10.2 可构建运行,分支上失败 | 〔测〕 | +| 回归:`--target aarch64-linux` 在 x86_64 主机上由 musl 静态变为 glibc 动态 | 行表快照与两个引擎对照 | 〔测〕 | +| 回归:e2e 295(aarch64 主机)、299(Windows 主机)每轮失败 | CI 日志 | 〔测〕 | +| 未定性:Windows 单元测试访问违例、macOS xcode-27 的 1068、Windows 的 860 | 原交接的"缓存毒化"解释与同一轮日志不符 | 〔测〕 | + +### 2.4 尚未实现的 + +| 项 | 来源 | +|---|---| +| K7、E11:32 位一类目标(规范的 32 位约定、ELF32 映像、32 位系统调用、`i686-*` 行的载荷) | 2026-11-10 方案 P5 | +| K2 的收尾、X4:第二类系统(OpenBSD、Haiku、GNU/Hurd)的基座包与 okrun;openkal-netbsd 与 K10 | 同上 P4 | +| K12:openkal-macos 经 libSystem | 同上,讨论项 | +| X5:xlings 的"任意宿主"写法(目标侧数据在三个宿主块里重复同一 URL) | 同上 | +| RP:运行器包(wine 没有 xpkg;qemu-user、adb、虚拟机没有统一声明形式) | 同上第 6.4 节 | +| 跨系统的 `mcpp test` 作为常设门禁 | 同上第 8.2 节 | +| UEFI 的平台包与 openkal 实现;固件与运行器所需的 xim 资产(OVMF、DOSBox、wine) | 探针已做(第 8 节),未开始 | +| DOS、SVR4、Serenity、Plan 9 | 勘测或未开始 | + +## 3. 诊断:六个结构性原因 + +审计发现的三十余个问题归为六个原因。每个原因对应第 4 节的一条原则与第 5 节的一处结构。 + +| # | 原因 | 表现 | +|---|---|---| +| R1 | **行是数据,读行的代码不是。** 行与提供者进了表,但"某个系统的载体以哪个身份构建""某个系统在 `uname` 里叫什么""宿主的系统是哪个"这类**系统级事实**没有表,于是每个消费点各写一个分支 | `pack.cppm` 的 `system == "freebsd"`;`polyglot` 的系统白名单;`execute.cppm` 的三元式;`windows_resources.cpp` 拼 `{arch}-windows-gnu`;`registry.cppm` 拼 `linux-{arch}` | +| R2 | **一条提供者条目承载两种载荷安排。** 原生与交叉写在同一条目上,行为随宿主分叉,"写出宿主自己的目标不改变任何东西"这一不变量随之失效 | e2e 295;默认身份漂移 | +| R3 | **链接模型没有单一求值点。** "C 库由谁供给"在五处用 `base.layers.cAbi.empty()` 重新推断,基座的两种布局走两条通道,其中一条不读折叠后的加载器 | 交叉产物烙入构建机路径;GCC 分支被顺手改坏 | +| R4 | **陈述先于门禁。** "已测""默认行为不变"写进了 PR 正文与设计记录,对应的门禁不存在、被跳过或是红的 | 1076 跳过即通过;三平面文档第 3 节;交接清单 | +| R5 | **资产准入只校验字节,不校验可用。** 修订通道核对摘要与平台齐全,不核对归档布局、清单内容与"装上之后能不能用" | llvm 修订 2;sysroot 修订 2 的三处缺陷 | +| R6 | **运行器仍是文档而不是数据。** 行表里没有"这个身份在这台主机上怎样启动"的条目,每个系统各有一份胶水 | FreeBSD 的手写路径;诊断里对 FreeBSD 产物建议 `qemu-aarch64-static`;三份虚拟机驱动 | + +## 4. 原则 + +2026-11-10 方案的 P1–P9 全部保留。新增六条,各自针对一个原因。 + +- **P10 系统级事实也是数据(R1)。** 凡是按系统名分支的地方,分支的条件上移为系统表的一个键。验收是机械的:引擎源码里 + 系统名的出现次数由一个 CI 检查持有,只许减少(第 5.2 节)。 +- **P11 一条提供者条目只陈述一种载荷安排(R2)。** 原生与交叉是两条条目,各有自己的 `hosts`、`sysroot`、`linkage` + 与优先级。推论:一个身份在其原生主机上,写与不写 `--target` 得到同一条链接行。 +- **P12 只写 `-` 的请求,其回答是契约(R2)。** 新增提供者不得改变任何主机上既有请求的回答。新的提供者以 + 低于现有回答的优先级进入;要改变缺省,是一次单独的、写进 CHANGELOG 的决定。这与 2026-11-10 的 Q3(默认 B 路线)一致。 +- **P13 链接模型求值一次,渲染不再推断(R3)。** "谁供给 C 库、sysroot 在哪、加载器写成什么、哪些目录进 RUNPATH" + 在 prepare 里求成一个值;`flags.cppm` 只渲染这个值,不再读行、提供者或基座。 +- **P14 陈述引用门禁(R4)。** 设计记录与 PR 正文里的"已测"必须指向一个在 CI 里运行的检查;本地测量写作"本地测得", + 并写明主机与载荷布局。跳过不是通过:跳过的脚本以跳过报告,覆盖检查把"无处运行"当作失败。 +- **P16 归属判据。** 一个事实放在哪里,依次问四句,第一个回答"否"的地方就是它的归属: + + | 问 | 回答"否"时归属 | + |---|---| + | 换一个程序,它还成立吗 | 程序自己的仓库(数据结构的大小、平台层的选择、进程与终端的语义) | + | 换一块板、一个固件、一个客体镜像,它还成立吗 | 板级包(平台包)或镜像资产附带的描述(链接脚本、设备地址、模拟器参数、退出约定、客体的包装) | + | 换一个系统,它还成立吗 | 行、系统表、提供者——主流系统在内置数据,长尾系统在平台包 | + | 都成立 | 引擎的机制,或 openkal 的规范 | + + 这是 docs/40 已有的一句话("位置是目标事实,选择是板级事实")向两端的延伸。 + +- **P15 新系统的准入以"引擎零改动"为验收(R1、R6)。** 一个新系统由数据(行、系统表条目、提供者)、包(实现、基座、 + 平台、运行器、镜像)与门禁(一致性、金丝雀)组成。准入需要改引擎,说明缺的是一个通用机制,先补机制,再以该系统验收。 + +- **P17 一个事实只有一个实现。** 引擎里同一个问题有两处回答时,收敛为一处,另一处改为读它。这次实施中遇到的重复 + 按 5.13 的清单逐项收敛;收敛不改变行为,由收敛前后的同一组测试证明。 + +## 5. 目标架构 + +### 5.1 四个平面与一张系统表 + +三平面记录把验证写成"归一化层"。本文把它升为第四个平面,因为 R4 与 R5 都出在这里。 + +``` + ┌──────────────────────────── 程序(语义面:进程模型、终端、路径写法,属于程序自己) ────────────────────────────┐ + │ │ + │ ① 身份面 ② 载荷面 ③ 运行面 ④ 验证面 │ + │ 行 + 提供者 工具链 + 基座包 运行器描述 + 运行器包 金丝雀 × 矩阵 × 准入门禁 │ + │ + 系统表 + 平台包 + 实现 + 镜像资产 │ + │ (mcpp 内置数据 (xim-pkgindex、 (内置数据 / 平台包 / (mcpp CI、xim CI、 │ + │ 与平台包) mcpp-index) xim 资产 / dirun) openkal-conformance) │ + └──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + 机制在 mcpp 引擎:选择规则 · 层的合并 · 链接模型 · 运行器协议 · 打包 · 修订通道的客户端 +``` + +| 平面 | 持有的数据 | 所在 | 不变量 | 门禁 | +|---|---|---|---|---| +| ① 身份 | 行、提供者、**系统表** | `builtin.toml`、平台包 | 目标差异全在数据(P10);一条条目一种安排(P11);请求的回答稳定(P12) | 行表快照;e2e 295、299;系统名检查 | +| ② 载荷 | 工具链、基座包、平台包、openkal 实现 | xim-pkgindex、mcpp-index | 构建主机只需要 mcpp;基座供给 C 库时宿主链接模型整体让位(P13) | 基座准入契约;修订通道的安装检查 | +| ③ 运行 | 运行器描述、运行器包、磁盘镜像 | 内置数据、平台包、xim | 构建不依赖运行器;每个身份在每台主机上要么能启动,要么指名缺哪个运行器 | 运行矩阵 | +| ④ 验证 | 金丝雀工程、目标矩阵、期望输出 | mcpp 仓库、各仓 CI | 陈述引用门禁(P14) | 覆盖检查;夜间矩阵报告 | + +### 5.2 系统级事实:先用已有的行键,系统表只补缺的一项 + +**问题。** 行回答身份级的问题。系统级的问题今天由代码回答。 + +**已有而未被消费的键〔读〕。** SPEC-016 §2.1 的行字段已经包括 `loader`、`execution`、`family`、`exe-suffix`、 +`object-format`。内置数据只在 FreeBSD 四行上写了 `family`,其余键没有值,于是代码里各有一张自己的表。所以这里的 +工作首先是**给已有的键填数据并让代码读它**,而不是新增 schema。 + +**新增的只有两处。** + +```toml +[[platform.system]] # 系统表:一个系统一条,内置数据与平台包同一 schema +name = "freebsd" +uname = "FreeBSD" # 多系统单文件的 POSIX 头里识别该系统的词;没有该键的系统不进这种文件 + +[[platform.provider]] +name = "openkal" +serves = ["x86_64-windows-musl"] +system-headers = "xim:mingw-w64-sysroot@20261104-2" # 系统自身接口的头文件由谁给;只有资源编译等需要它的步骤读 +``` + +"某个系统上自带运行器以哪个身份构建"不进系统表:它只对带运行器与载体的平台有意义,按 P16 属于那个平台。平台的 +`runner` 陈述里 `hosts` 的每一项可以带它的身份(`{ host = "freebsd-x86_64", built-as = "x86_64-freebsd-musl" }`), +由 openkal-platform 写出。 + +**每个硬编码点的去向。** + +| 现在的代码 | 改为读 | +|---|---| +| `pack.cppm` 对 linux、macos、windows、freebsd 各一个分支 | 平台 `runner.hosts` 各项的 `built-as` | +| `polyglot.cppm` 的系统白名单与 `uname` 词 | 系统表的 `uname`;没有该键的系统被拒绝并指名 | +| `execute.cppm` 把宿主映射成三个系统名之一(`unreachable_execution` 里原有一处,mcpp#924 又加一处) | 宿主身份所在行的 `execution`(缺省为 os 段)与目标行的比较 | +| `windows_resources.cpp` 拼 `{arch}-windows-gnu` 再找服务它的提供者 | 选中提供者的 `system-headers` | +| `registry.cppm::host_can_serve` 拼 `linux-{arch}`,musl 另有 `x86_64` 特例 | 只调用 `available_on(提供者, 宿主)`;该函数里不再有架构或系统判断 | +| `linkmodel.cppm::distro_loader_path` 按三元组前缀返回 `/lib64` 或 `/lib` | 行的 `loader`(已有键,补数据) | +| 运行器建议文字里的字面量 `qemu-aarch64-static`(三处) | 5.5 的运行器陈述;没有陈述时只写声明的形状,不写示例程序 | + +**机械门禁。** 新增检查脚本,统计 `src/` 与 `modules/`(除 `modules/platform/` 与数据文件)里系统名词表的出现次数, +与仓库里的基线文件比较:多于基线则失败,少于基线则要求更新基线。P10 由此从评审意见变成检查。 + +**第二个使用者。** 任何平台包定义的系统(长尾系统、DOS、UEFI)自动得到多系统单文件与"已构建,未运行"的回答,不需要 +引擎改动。 + +### 5.3 提供者:拆分、优先级与等级 + +**拆分(P11)。** `aarch64-linux-gnu` 的两种安排写成两条: + +```toml +[[platform.provider]] # 原生:宿主的载荷 +name = "glibc" +serves = ["aarch64-linux-gnu"] +priority = 100 +hosts = ["linux-{arch}"] +toolchain = "llvm@23.1.3" + +[[platform.provider]] # 交叉:基座包 +name = "glibc-sysroot" +serves = ["aarch64-linux-gnu"] +priority = 40 # 低于 musl-gcc 的 100 与 openkal 的 50(P12) +hosts = ["linux-x86_64"] # 归档与宿主无关,范围随验证扩大 +toolchain = "llvm@23.1.3" +sysroot = "xim:aarch64-linux-gnu-sysroot@2.44.3" +linkage = "dynamic" +``` + +这是 `mingw`(10)与 `mingw-llvm`(5)已有的写法。结果:aarch64 主机上两种拼写仍取原生条目(e2e 295 恢复);x86_64 主机 +上 `aarch64-linux` 仍回答 musl,`aarch64-linux-gnu` 写全才取交叉条目。 + +**等级随条目(建议)。** `tier` 现在在行上。同一身份的原生安排是 preview、交叉安排尚无门禁时,行的等级说不清是哪一个。 +提供者条目可以带自己的 `tier`,缺省继承行;`mcpp toolchain list` 显示选中条目的等级。 + +### 5.4 链接模型:一次求值 + +**设计。** prepare 的末尾得到一个值,此后只有它被读取: + +``` +LinkModel + c_library : host-payload | base-package | graph | sdk 谁供给 C 库 + sysroot : path? base-package:包根 + search : { crt: [path], lib: [path] } -B 与 -L + loader : { file: path, name: path } 链接时用到的文件;写进产物的名字 + runpath : [path] 只含产物运行时确实存在的目录 + runtime : { rtlib, unwind, cxx, resource_dir } +``` + +**规则。** + +1. `c_library` 的四个值两两互斥,由选中的提供者条目与依赖图决定;`flags.cppm` 里的五处推断删除。 +2. **加载器的名字与文件分开。** 产物在本机运行的安排(原生的宿主载荷):`name = file`,即今天的密封行为,不变。产物不在 + 本机运行的安排(交叉、另一系统):`name` 取行的 `loader`(SPEC-016 已有的键),`runpath` 为空。构建机的安装路径不进入 + 这类产物。 +3. 运行器拿到 sysroot:运行器模板增加 `{sysroot}` 占位,交叉 glibc 产物的缺省运行器是 `qemu- -L {sysroot} {}`。 +4. 基座包只有一种读法。"包根即 sysroot"与"生态自己的 glibc 包布局"今天走两条通道;基座包的清单是它的陈述,带清单的 + 一律按包根 sysroot 处理,布局探测只用于没有清单的目录。 + +**门禁:链接行的金样。** 渲染是纯函数,所以不需要工具链就能测:对 `编译器族 × c_library × 链接形态 × 原生/交叉` 的每个 +组合保存一份期望的链接词表,作为单元测试。这次 GCC 分支的回归(gcc 15.1.0 的 `-B`、`-L`、加载器整组消失)在这种测试下 +是一行差异。CI 另加一条 gcc `PayloadFirst` 的真实构建腿。 + +### 5.5 运行面:运行器是提供者的陈述 + +**已有的机制〔读〕。** 运行器今天可以写在四处:平台的 `runner = { cmd, host, hosts }`、`[platform.runners]` 的具名 +运行器、项目与本机配置的 `[target.] runner`、作为目标工具的运行器包(`[target..tools]`,docs/41 的运行协议)。 +SPEC-016 §6 已经规定:行的 `execution` 不是本机系统、又没有运行器时,报告"已构建,未运行"。缺的只有一处:**内置提供者 +不能陈述运行器**,于是 wine、qemu-user、虚拟机这些缺省安排没有数据可写。 + +**设计。** 不新增表。提供者条目可以带 `runner`,形状与平台的相同,外加它依赖的包: + +```toml +[[platform.provider]] +name = "openkal" +serves = ["*-freebsd-musl"] +runner = { cmd = ["dirun", "--profile", "{pkg:freebsd-disk-image}/dirun.toml", "--", "{}"], hosts = ["linux-*", "macos-*"], + requires = ["xim:dirun", "xim:freebsd-disk-image@14.5", "xim:qemu-system"] } +``` + +**规则。** + +1. 优先级沿用 P8:项目 `[target.] runner` 先于本机配置,先于平台,先于提供者。 +2. `requires` 里的包在第一次 `mcpp run` 或 `mcpp test` 需要时按工具链同样的同意方式安装;构建从不安装它们。 +3. 模板的占位:`{}` 是产物,`{sysroot}` 是该安排的基座,`{pkg:}` 是 `requires` 中该名字的包的根(一个模板可以 + 用到两个包——镜像与固件——所以占位按名字,而不是一个 `{image}`)。模板的程序先在 `requires` 的包里找,再找 PATH。 +4. **不运行另一系统的程序来观察它。** 内置行与平台定义的行由同一个谓词判断:行的执行系统不是本机、又没有运行器,就在启动 + 之前回答"已构建,未运行",指名身份与可写的声明。另一系统的系统调用号在本机内核上指向别的操作,启动它的后果不确定 + (第 14 节第 2 条)。诊断的文字由数据生成,只有一处。 + +**运行器的五种来历**(是分类,不是 schema 里的键): + +| 来历 | 含义 | 实例 | 取状态的方式 | +|---|---|---|---| +| 用户态模拟 | 同内核、异架构 | `qemu- -L {sysroot}` | 进程的退出码 | +| 接口的宿主实现 | 另一系统的接口在本机的实现 | wine;DOSBox(探针 P2:0.33 秒,退出码如实) | 进程的退出码,或客体包装写出的状态 | +| 映像的加载器 | — | okrun、node | 进程的退出码 | +| 板 | 产物本身就是模拟的机器启动的东西 | qemu 加固件(探针 P5:OVMF 下 2 秒) | 板级包约定的出口(5.12) | +| 客体 | 启动目标系统,在里面运行产物 | dirun 加磁盘镜像 | 标记协议 | + +"客体"只有一个实现:dirun。openkal-freebsd 的 `vm-run.py` 在 dirun 等价后退役。 + +**xim 侧。** 缺的包:wine、DOSBox、OVMF(本机的 `/usr/share/OVMF` 是宿主依赖,按 P9 应是 xpkg)。命名统一为约定文档的 +`-disk-image`;`freebsd-vm` 与 `freebsd-disk` 保留为指向它的别名一个周期后移除。 + +### 5.6 载荷面:基座包的准入契约 + +基座包的 schema 不变(2026-11-10 方案 E5)。增加的是**准入**:一个基座包进入索引之前,由索引仓库的 CI 用已发布的 mcpp +对它跑一组固定探针。 + +| 编号 | 探针 | 这次会拦下的缺陷 | +|---|---|---| +| BP1 | 归档顶层目录等于资产文件名主干(含 `-r`) | llvm 修订 2、sysroot 修订 2 | +| BP2 | 清单能被引擎解析、无未知键;`provides` 的每一层在包内有对应的文件 | `compiler-runtime=libgcc` 而实际给的是 compiler-rt | +| BP3 | C 程序:静态与动态各链接一次 | — | +| BP4 | C++ 程序(头文件):静态与动态各链接一次 | `libc++.so` 指向缺席的 `libc++.so.1` | +| BP5 | 声明 c++-abi 的包:`import std` 的程序构建 | 没有 `std-module` | +| BP6 | 有运行器的身份:BP3–BP5 的产物运行,退出码与输出相符 | — | +| BP7 | 产物里没有构建机的路径(`PT_INTERP`、`RUNPATH`、调试信息之外的字符串) | 烙入的加载器 | + +BP1、BP2 与 BP7 中仅凭安装后的目录树即可回答的部分,是索引仓库里每个基座包测试模块调用的布局契约 +(`tests/lib/base_package.py`);BP3–BP7 中需要引擎的部分是基座包专有的作业,随引擎发布之后进入。**配方自己的验证在 CI +中运行:** 改动直接编辑的配方,在安装作业的每个宿主上运行其模块的 L3 与 L4 测试(15.2)。 + +镜像一致性:CN 镜像滞后时客户端装到旧字节。修订通道的客户端在摘要不符时回落到另一个镜像并报告,而不是由每个 CI 作业 +手工钉 GLOBAL。 + +### 5.7 验证面:金丝雀与矩阵 + +**金丝雀。** mcpp 仓库持有两个中性的小工程,它们把验证工程对生态的要求(第 7 节)写成断言: + +| 工程 | 形状 | 断言 | +|---|---|---| +| `canary-c` | C89、只用 POSIX、两个程序共享源码、带资源脚本、用 `sigsetjmp`、启动子进程、读写文件 | V1–V6、V8–V10 | +| `canary-cxx` | C++23 模块、`import std`、异常、线程 | 每个声明 c++-abi 的安排 | + +**矩阵是数据。** 一份表列出 `身份 × 主机 × {构建, 静态链接, 动态链接, import std, 运行, 测试, 打包, 映像}` 的期望 +(通过、拒绝并给出原因词、不适用)。夜间作业对全表运行两个金丝雀,产出覆盖报告;报告是 CI 的产物。"十八个目标全部 +通过"这类句子从此引用报告的运行号。 + +**顺序。** 不变量测试(295、299 一类)在功能测试之前运行,失败则后者不运行。能力词由 CI 作业预装满足;一个脚本要求的 +能力没有任何作业提供,覆盖检查失败——这条已有,补上的是"删除豁免需要引用一次真实运行"。 + +### 5.8 openkal 侧 + +| 项 | 内容 | +|---|---| +| 环境的三类 | 2026-11-10 方案分了两类(内核 ABI 稳定、libc 才是稳定 ABI)。增加第三类:**单任务环境**(UEFI、DOS、引导链)——没有调度器,启动的程序运行到结束后启动者才继续。规范文本没有禁止这样的实现,并发相关的能力已是可不声明的属性(T9);需要补的是一句明示与一致性套件的覆盖。它有三个使用者,先于其中任何一个的实现 | +| K7 提前 | 32 位一类目标从"设计讨论"提为正式阶段。i686 Linux、SVR4、32 位 DOS、armv7 都卡在它上面;先做 `i686-linux-musl`,因为它有 qemu-user 可以立刻验证 | +| 运行器统一 | 各实现的开发用虚拟机脚本收敛到 dirun;实现仓库只留控制台驱动词 | +| 接口声明的三方核对 | 清单的 `provides-interfaces`、`kal_interfaces()` 的回答、实际导出的名字,一致性套件核对(K1 的配套项,确认是否已全部落地) | +| 探针的跳过条件 | openkal-emscripten 的 longjmp 探针靠匹配引擎的报错文字来判断跳过;改为读引擎版本 | + +### 5.9 测试体系的收敛:e2e 与 bench 归到 mtest 的 C++ 用例 + +**现状〔测〕。** e2e 是 762 个 bash 脚本、约 9 万行,作为 mtest 的 0 级程序运行(一个脚本一个结果)。一次 PR 运行是 +72 个作业、518 作业分钟、墙钟约 45 分钟(运行 43146150459)。 + +| 事实 | 数值 | 含义 | +|---|---|---| +| 同一批 189 个测试,Windows 与 macOS 的耗时 | 71.0 分钟对 21.0 分钟,3.4 倍 | Windows 上 bash 的进程创建是主要开销之一 | +| Windows 的耗时分布 | 219 个测试共 79.7 分钟;耗时最长的 18 个占一半 | 先治长尾,而不是平均用力 | +| 要求 `unix-shell` 的脚本 | 154 个 | 两成的套件在 Windows 上不运行,原因是写法而不是被测行为 | +| 要求 `python3`、`jq` 的脚本 | 99、11 个 | 测试依赖宿主工具,与 P9 不符 | +| 不调用构建、运行、测试、打包的脚本 | 81 个 | 这些是解析与清单层面的断言,不需要是端到端测试 | +| Linux 的 8 个分片在这次运行里的测试时间 | 0.8、1.0、7.3、0.0 分钟…… | 分片数固定,作业的准备时间超过了它运行的测试 | +| 单元测试(168 个文件)在 Windows 上 | 构建 270 秒,运行 11 秒 | C++ 测试的成本在编译,不在运行 | + +**方向。** 收敛,但不是逐个翻译。脚本按三类处理: + +| 类 | 判据 | 去向 | 估计占比〔析〕 | +|---|---|---|---| +| 下沉 | 断言的是解析、清单、选择规则的回答,不需要真实工具链 | 所属模块的单元测试(`modules//tests/`),毫秒级,每个宿主都运行 | 一成到两成 | +| 合并 | 构建一个小工程再断言输出 | 按子系统合成 mtest 的 `E2E` 对象;同一对象的用例共用预热的 home 与夹具工程;原脚本名的句子成为用例名,原编号留作标签 | 七成 | +| 保留 | 断言的是机器状态:全新安装、升级、容器内无宿主工具链、模拟器 | 仍是 0 级脚本,数量由基线文件持有,只许减少 | 一成 | + +**迁移的规则。** 沿用 2026-11-10 方案第 16 节:保留每一条断言,不能迁移的在 PR 中点名;每个子系统先以原脚本验证,再以新 +用例验证,两次运行都引用。迁移按子系统分批,一批一个 PR。编号标签与映射表使"旧脚本的断言去了哪里"可查。 + +**测试支持层。** `tests/support/` 下一个小模块,建立在 `mcpplibs.mtest.e2e` 之上:声明式的夹具工程(文件表到目录树)、 +`mcpp(...)` 调用(选择共用的预热 home 或全新 home)、对 `build.ninja` 的行与 JSON 文档的读取、对 ELF/PE/Mach-O 头的 +读取(引擎的打包模块已有这些读取,不再调用 `readelf`、`file`、`llvm-readobj`)。失败时打印可以直接粘贴到 shell 的命令行, +保住脚本"读起来就是一次会话"的优点。 + +**对 mtest 的升级要求。** mtest 已有的〔读〕:集合(`smoke`、`regression`、`pr`、`full`、`release` 与自定义)、按改动选择 +(`--since`、覆盖图、`file:` 产物、程序的 `sources`)、按历史分片(`--shard`)、预算(`--budget`)、资源与运行级夹具、 +`skip_if`、bench 的判定策略、`gate`、`ci report` 与 `ci timeline`。需要补的: + +| # | 能力 | 取代什么 | +|---|---|---| +| M1 | 能力词的探测写在 `mtest.toml`(探测命令到能力词),引擎逐机器求值;"某能力词无任何作业提供"由引擎报告 | `run_all.sh` 里的探测与白名单;`check_e2e_coverage.py` | +| M2 | 0 级程序的跳过约定(一个专用的退出码),使保留类脚本的跳过也是独立结果 | 脚本里的 `echo PASS (skipped)` | +| M3 | e2e 模块:环境、工作目录、标准输入、超时;输出匹配器;失败时的会话记录与保留的目录 | 脚本里的 `grep`、`sed`、`jq`、`tail` | +| M4 | 交互会话:对伪终端或管道的 expect/send(D23:伪终端在 `mcpplibs.os`) | dirun 的控制台模块;终端类脚本 | +| M5 | 由数据展开的参数化用例;长时间 bench 的续跑 | `bench/matrix.json` 加 `run-standard.sh` 加 `report.py` | +| M6 | `mtest ci plan`:由选择与历史给出每个宿主的作业矩阵(是否有工作、分片数、每片的预计时间) | 固定的分片数;固定的作业集合 | + +M1、M2、M3 是迁移的前置;M4、M5、M6 随用到它们的批次。每项实施前先核对 mtest 的现状,已有的不重做。 + +**bench。** `bench/` 有两部分。引擎对比的 harness 是一个 C++ 程序,自带协议、日志、注册表与运行器,外加 `run-standard.sh`、 +`harness.sh`、`report.py`;`bench/tests/engine.cpp` 已经是 mtest 的 Bench 用例。收敛为:各引擎的适配器留作夹具代码, +矩阵由 M5 展开为参数化的 Bench 用例,日志与续跑交给 mtest 的存储,"不可发布"的判定交给 `mtest gate`,报告由 +`mtest report` 生成。`proto-bmi-release/` 是已结束的实验,移入结果目录。 + +**CI 的触发与编排**见 5.11。 + +**不做的。** 不为了统一而翻译"保留"类脚本;不在迁移中顺手改被测行为;不在第 10 节的 S1(纠正)完成之前开始迁移。 + +### 5.10 dirun 的形状 + +dirun 是 5.5 中 `machine` 类运行器的唯一实现,所以它的形状属于本方案。 + +**现状〔读〕。** 产品代码约 1,300 行,分在四个工作区成员(protocol 95 行、console 246 加 pty 212 行、runner 418 行、 +testing 106 行)与根包(cli 131 行、main 57 行);另有约 1,100 行的 Python 参考实现与它的测试;README 约 150 行。 + +| 问题 | 说明 | +|---|---| +| 拆得过细 | 四个包各自的清单、版本与依赖声明,服务于一千余行代码;包的边界应当对应"有第二个使用者的库",现在没有 | +| 重新实现了生态已有的库 | `modules/testing` 对应 `mcpplibs.mtest`;`src/cli.cppm` 手写参数解析,对应 `mcpplibs.cmdline` | +| 两份实现 | Python 参考被称作"冻结的规范",但它是第二份要维护的实现;合同应当是条款加测试 | +| "与客体系统无关"只成立一半 | 标记协议的包装(`stty -echo`、`rc=$?`、单引号转义)是 POSIX shell 的;`--machine` 到架构的映射写在代码里。客体是 DOS 或 Windows 时两者都不成立 | +| Windows 宿主 | pty 是桩。串口接到标准流时未必需要伪终端:用管道加带超时的读取,三个宿主是同一份代码〔析〕 | +| README | 定位、用法、内部机制(标记协议细节、FAT 工具的回落)、未来计划混在一处 | + +**建议的形状。** + +``` +dirun/ + README.md 一句定位 · 三行上手 · 退出码表 · 指向 docs/ 的链接(五十行以内) + mcpp.toml 一个包;依赖 mcpplibs.os、mcpplibs.cmdline、JSON;开发依赖 mcpplibs.mtest + src/ main.cpp · cli.cppm(cmdline 的声明)· runner.cppm · protocol.cppm · console.cppm + profiles/ 内置的缺省客体描述(POSIX 登录式) + tests/ mtest:单元用例 · 合同条款(cts)· 一个真实引导的 E2E(要求 qemu 能力词) + docs/ contract.md · profiles.md +``` + +- **客体描述是数据。** 引导脚本、命令的包装模板、取状态的写法、转义规则、挂载文件盘的命令,合成一份描述文件,随 + `-disk-image` 资产一起发布。dirun 的核心只有 expect/send、标记切分、覆盖盘与退出码。探针 T4 是一个实例: + DOS 的命令解释器取状态的写法与 POSIX shell 不同。 +- **两种方式,一份核心。** "客体"方式启动一个系统并在其中运行产物;"载荷"方式里产物本身就是被启动的东西(固件应用、 + 裸机程序),状态的读法由板级包的描述给出(5.12)。两者共用串口、超时与清理。 +- **控制台模块的归属。** expect/send 是通用能力,第二个使用者是 mtest 的交互会话(M4)。伪终端属于 `mcpplibs.os`, + expect/send 属于 mtest 的 e2e 模块;dirun 依赖它们。迁出之前留在 `src/console.cppm`。 +- **生态库不够用时升级生态库。** `mcpplibs.cmdline` 若缺少可重复选项或 `--` 之后原样传递,补在 cmdline 里。这条对 + 生态内的每个项目成立:项目内重新实现是对库的缺陷报告没有被提交。 +- **定位一句话。** dirun 在一个引导起来的磁盘镜像里运行一条命令,返回它的输出与退出码;它之于一台虚拟机,如同 + qemu-user 之于一个异架构程序。它不构建镜像、不管理虚拟机、不做配置。 + +### 5.11 CI 的触发模型 + +收敛到 mtest 之后,"这次运行跑什么"由三个轴决定,每个轴都是数据:**事件**(什么时候)、**集合**(跑哪些用例,mtest 的 +集合与按改动选择)、**宿主**(在哪里跑,由 `mtest ci plan` 给出)。 + +**事件对集合。** + +| 事件 | 集合 | bench | 宿主与作业数 | +|---|---|---|---| +| PR 的每次推送 | `pr`:改动影响到的用例,加 `smoke`、`regression`、不变量 | 不跑 | 只有选择非空的宿主;分片数随工作量 | +| PR 带标签 `ci:full`,或手动触发并选 `full` | `full` | 不跑 | 全部宿主 | +| PR 带标签 `ci:bench` | `pr` | 引擎自身开销的 Bench 用例,与 main 的基线比较 | 一台 Linux | +| 合入 main | `full`;更新存储(历史、覆盖图、bench 基线) | 引擎自身开销 | 全部宿主 | +| 夜间 | `full`、金丝雀矩阵、保留类脚本、基座准入的抽查 | 引擎自身开销的趋势 | 全部宿主 | +| 发布 | `release`:`full` 加 bench 判定(`mtest gate --set release`) | 判定 | 全部宿主 | +| 手动触发 | 输入:集合、宿主、用例的模式、是否带 bench | 按输入 | 按输入 | + +- **bench 分两种。** 引擎自身开销的 Bench 用例(`bench/tests/engine.cpp`)便宜,现在随每次 CI 的 Linux 单元测试作业运行; + 改为只在 main、夜间、发布与 `ci:bench` 运行。引擎对比的标准数据集要数小时,只由手动触发,不挂在任何事件上。 +- **全量可以手动。** `workflow_dispatch` 的输入就是 `mtest run` 的选择参数;本地 `mtest run --set full` 与 CI 用同一份集合 + 定义,所以"CI 跑了什么"在本地可以原样重复。 +- **按改动选择的安全边界。** 覆盖图不认识的用例视为受影响;覆盖图过期(`max_age`)时全部视为受影响;内置数据文件 + (`builtin.toml`)的改动影响全部目标类用例;不变量与 `smoke` 永远在 `pr` 里。选择只会多跑,不会漏跑。 + +**依赖与并行。** + +``` + changes ──► plan(mtest ci plan:每个宿主的选择、分片数、预计时间;无构建,约一分钟) + │ + ├─► build(linux) ──► gate(linux) ──┬─► unit · e2e 分片 × N(linux) + ├─► build(windows) ─► gate(windows) ┼─► unit · e2e 分片 × N(windows) + ├─► build(macos) ──► gate(macos) ──┼─► unit · e2e 分片 × N(macos) + └─► build(linux-arm) ─► gate ──────┴─► 需要多个构建的作业:交叉、目标矩阵、升级、openkal +``` + +- **构建立即并行开始。** 构建是每个宿主的长杆(Linux 约 16 分钟),不排在任何闸的后面。 +- **每个宿主一道闸。** 构建完成后先跑该宿主的 `smoke` 与不变量(预算 2–3 分钟),通过才扇出该宿主的分片;任一宿主的闸 + 失败,取消整次运行。绿的路径不增加等待,红的路径在第一个闸处停下。 +- **测试程序随构建产出。** 构建作业同时编译被选中的测试程序,与 mcpp 一起作为产物下发;分片不编译。 +- **作业内并行靠资源声明。** 用例声明它独占的东西(一个 home、整台机器),其余并行;全局的 `E2E_JOBS=2` 取消。分片只在 + 一个宿主的预计时间超过预算时才出现:`分片数 = ⌈预计时间 ÷ 每片预算⌉`。 +- **夹具只建一次。** 预热的 home 与模块缓存是运行级夹具,一个作业建一次,用例声明使用它。 + +**时间成本的账。** 现状(PR 运行 43146150459):72 个作业、518 作业分钟、墙钟约 45 分钟;其中 Windows e2e 的测试时间约 +80 分钟,最长的 18 个占一半。目标〔析,落地时以测量校正〕:PR 墙钟 30 分钟以内,作业分钟减少三成以上,Windows e2e 的测试 +时间 35 分钟以内,失败的推送在闸处停下(数分钟的测试成本,不含已开始的构建)。`mtest ci timeline` 每夜产出一份成本 +报告:作业分钟、关键路径、最慢的用例;集合的预算被突破时由 `gate` 报告。 + +### 5.12 固件与裸机 + +**已有的〔读〕〔测〕。** docs/40 的分层是:引擎持有 ISA 档与 freestanding 的链接行,目标持有编译器与 C 库,**板级支持包** +(一个平台包)持有启动对象、链接脚本与模拟器调用。十余个裸机行是内置数据;RISC-V 与 Cortex-M 的构建与 qemu 运行在 CI 里; +openkal-opensbi 是写在 SBI 上的实现,提供核心三个接口加 `time` 与 `env`,如实缺席 `fs`、`process`、`task`。 + +**这次测到的。** UEFI 不需要引擎认识它(探针 P5):一个三十行的平台包用 `[[platform.define]]` 定义 `x86_64-uefi` +(`clang-triple = "x86_64-unknown-uefi"`、`object-format = "pe"`、`execution = "uefi"`、`exe-suffix = ".efi"`), +已发布的 2026.11.10.2 构建出 EFI 应用,在 qemu 加 OVMF 下打印并返回状态,墙钟 2 秒。 + +**归属(P16 的应用)。** + +| 事实 | 归属 | 不归属 | +|---|---|---| +| ISA、对象格式、产物后缀、执行系统 | 行(数据) | 引擎的分支 | +| 入口符号、子系统、链接脚本、内存布局 | 固件或板的平台包 | mcpp、openkal | +| 设备地址、模拟器的机器型号与参数、固件文件 | 板级包;固件文件是 xim 资产 | mcpp、openkal | +| 状态怎样回到宿主(调试端口、semihosting、测试设备、输出里的标记) | 板级包:它既给程序一侧的出口,也给运行器一侧的读法 | 引擎、dirun 的核心 | +| 固件提供的服务到 `kal_*` 的翻译 | 每种固件接口一个 openkal 实现(openkal-opensbi 已有;UEFI 的启动服务是下一个) | openkal 规范(规范不认识任何固件) | +| 程序用到哪些接口、没有时怎么办 | 程序 | 生态 | + +**三点设计。** + +1. **固件环境是平台包,不是内置行。** UEFI 先以平台包定义自己的行(今天就能做);出现第二个要用同一身份的平台包时, + 再把行升为内置数据。板与固件的差异永远在包里。 +2. **状态的出口由板级包两头负责。** 探针里 x86 的调试端口把状态变成 `状态×2+1`,RISC-V 的测试设备与 ARM 的 semihosting + 各有约定。这些都不进引擎:板级包给程序链接一个出口实现,并在运行器陈述里写出读法。dirun 增加"载荷"方式(产物即 + 被启动的东西),读法来自描述文件,与"客体"方式共用串口、超时与清理。 +3. **固件属于单任务环境。** UEFI 的 `StartImage` 与 DOS 的 EXEC 一样,被启动者结束后启动者才继续。5.8 的规范问题因此有 + 三个使用者(DOS、UEFI、未来的引导链),值得在规范里回答一次。规范文本没有禁止这种实现,并发相关的能力(通道、终止) + 已经是可不声明的属性〔读〕;缺的是一句明示与一致性套件的覆盖。 + +**收益。** openkal 的 UEFI 实现之上是同一个 openkal-musl 与同一个 C++ 运行时:UEFI 应用可以用 C++23 模块与 `import std` +编写,在 CI 里两秒跑完。这对引导加载器、固件工具与教学是直接可用的。 + +### 5.13 重复实现的收敛(P17) + +实施中遇到的"同一个问题有两处以上回答"逐项收敛。每项收敛前后由同一组测试证明行为不变,有意的行为变化单列。 + +| 问题 | 收敛前 | 收敛后 | +|---|---|---| +| 产物是否由本机运行 | 六处各自近似:`hermetic.cppm`(自带 sysroot 或由平台描述)、`runtime_validation.cppm` 三处、`pack.cppm`、`execute.cppm` 两处三元式 | `Triple::system_is_host()` 与 `Triple::loads_on_host()`,读行的 `execution` 与架构;Android 的行以数据陈述 `execution = "android"` | +| 基座供给 C 库时宿主让位 | `flags.cppm` 五处读 `base.layers.cAbi.empty()`,与 `targetSide.cAbi.origin` 并存 | 基座解析处决定一次(带清单即包根 sysroot;另一台机器的基座清空载荷的宿主运行时目录),`flags.cppm` 回到只渲染链接模型 | +| 加载器的名字 | `linkmodel.cppm` 的按架构表(`loader_filename`、`distro_loader_path`),与行的 `loader` 键并存而后者无人读 | 两个函数读行的 `loader`;表删除 | +| 本主机能否服务一个目标 | `registry.cppm::host_can_serve` 约 150 行按系统名的分支,与选择读的 `hosts` 数据并存 | 读提供者的 `hosts`;函数里不再有系统或架构 | +| 一个包引用所指的目录 | `base.cpp` 的 `package_dir` 与 `windows_resources.cpp` 的另一份(顺序相反) | 一个解析,三个使用者(基座、系统头文件、运行器的包) | +| 多系统单文件按什么选择程序 | `polyglot.cppm` 的三元式与系统白名单 | 系统表的 `uname` | +| 运行器建议里的示例程序 | 三处字面量 `qemu-aarch64-static` | 只写声明的形状;可用的运行器由提供者陈述 | + +仍在基线里的(由 `check_system_names.py` 持有,只许减少):打包对 linux、macos、windows 三个系统的约定、 +对象格式与后缀的若干分支、`modules/platform/` 之外的宿主谓词。 + +## 6. 纠正清单 + +每项:现状、纠正、兼容性。C1–C11 在 mcpp#924 合入之前完成;C12–C16 是对已合入项的追加 PR。 + +| # | 现状 | 纠正 | 仓库 | 兼容性 | +|---|---|---|---|---| +| C1 | `flags.cppm` GCC 分支只在 `Sysroot` 模式出链接词,丢了 `crossTarget` 与 `PayloadFirst` 的整组 | 恢复原分支;随 5.4 的金样测试与 gcc `PayloadFirst` 腿一起进 | mcpp | 恢复 2026.11.10.2 的行为 | +| C2 | 原生与交叉共用一条 `glibc` 条目 | 按 5.3 拆分,交叉条目优先级 40 | mcpp(数据) | `aarch64-linux` 的回答与 2026.11.10.2 相同 | +| C3 | 交叉产物的 `PT_INTERP`、`RPATH` 为构建机路径 | 按 5.4:行的 `loader`、空 `runpath`、运行器的 `{sysroot}` | mcpp | 新能力,无既有使用者 | +| C4 | sysroot 修订 2 的四处缺陷 | 发修订 3:顶层目录对齐、补 `libc++.so.1` 一族、清单加 `std-module`、`provides` 与内容一致;先过 BP1–BP7 | xim、xlings-res | 同一版本键的矫正修订 | +| C5 | 1076 在 CI 无处运行,跳过时打印 PASS | 一个 Linux 分片预装基座并提供能力词;脚本增加 `PT_INTERP`、C++、`import std` 三条断言;跳过如实报告 | mcpp | — | +| C6 | 五处系统名硬编码 | 按 5.2:先填已有的行键,新增只有 `uname` 与 `system-headers`;载体的身份由 openkal-platform 陈述 | mcpp、openkal-platform | 行为不变,来源改为数据 | +| C7 | `--systems all` 不再写出未覆盖的对;SPEC 的打包一章仍写"其余写为未覆盖" | 恢复报告:`all` 取数据给出的系统,其中本引擎构建不了的对逐个写出。规范与实现一致 | mcpp | 恢复规范行为 | +| C8 | `abi.longjmp` 不进成员分组键;与 `exceptions` 的组合不校验 | 见决定 D6 | mcpp | 键尚未发布,可自由调整 | +| C9 | G6 启动另一系统的程序、按信号归类它的死亡,再建议 `qemu-aarch64-static` | 撤回这种做法。把 `unreachable_execution` 已有的谓词(只对平台定义的行生效)扩到内置行:启动之前回答"已构建,未运行";文字由运行器数据生成(5.5 第 4 条) | mcpp | 另一系统的产物不再被启动;`--no-runner` 仍是"此处可直接执行"的声明 | +| C10 | 资源头第三源:门控条件写两遍;取载荷失败时静默 | 一个谓词;失败报告为载荷无法安装;来源改为 `system-headers` | mcpp | — | +| C11 | 规范一处未改;中文文档不对等;三平面记录的"已成立"与实测不符 | 每项行为变更同 PR 改 SPEC 与两种语言;三平面记录第 3 节改写为引用门禁,或标为由本文取代 | mcpp | — | +| C12 | 三个镜像包名 | `freebsd-disk-image` 为正名,另两个为别名 | xim | 别名保留一个周期 | +| C13 | FreeBSD 运行器是手写路径 | 5.5 的运行器描述;`vm-run.py` 在 dirun 等价后退役;补 aarch64 | openkal-freebsd、openkal-platform | README 的旧写法继续可用 | +| C14 | 配方自己的安装后验证(L3、L4)在索引 CI 里只对一个包运行;安装作业只做安装与移除 | 安装作业对改动直接编辑的每个配方运行其模块的 L3、L4;基座包的布局契约(`tests/lib/base_package.py`)进入四个 sysroot 包的模块。第一版写的"安装检查被跳过"不成立,见 15.2 | xim | 编辑配方的 PR 多一步验证 | +| C15 | dirun 自带测试模块;mcpplibs 下的归档空仓 | 测试改用 mtest;空仓的去留由负责人决定 | dirun | — | +| C16 | 在评审规则未满足、检查未全绿时以管理员权限合入 | 见决定 D12 | 三个生态 | — | + +**未定性的三个失败**(Windows 单元测试访问违例、xcode-27 的 1068、Windows 的 860)不在纠正清单里:先查明是否由本分支 +引入。访问违例发生在 `test_pack_distribution`,本分支改动了打包;在查明之前不以"环境问题"结案。 + +## 7. 验证工程对生态的要求 + +验证工程是一个 C89、只依赖 POSIX libc 的命令解释器,从同一组源码产出两个程序。把它对生态的要求抽成与它无关的十条, +即 `canary-c` 的断言: + +| # | 要求 | 对应的通用能力 | 状态 | +|---|---|---|---| +| V1 | 上游源码不改即可构建;需要适配时,每个身份的适配行数被统计 | 行、提供者、基座 | 上游原样 18 个身份过 10 个;4 行源码守卫加 5 行清单后过 16 个,去掉资源脚本的变体补到 17 个〔测,本地,2026.11.10.2〕 | +| V2 | 两个程序共享源码,各有入口与宏 | E2 | ✅ | +| V3 | 资源脚本写一次,每个 Windows 身份都能编,其余身份忽略 | E3、G3 | G3 在途 | +| V4 | 同一脚本在每个身份上的输出,归一化后与参考一致 | 运行面、验证面 | 本地做过,无门禁 | +| V5 | 同一份映像的字节在每个 okrun 主机上运行;两次打包字节相同 | oke、okrun | ✅〔测,本地〕,无门禁 | +| V6 | 自带运行器的包覆盖平台声明的全部系统 | G5、平台 `runner.hosts` 的 `built-as` | 在途 | +| V7 | 平台层的选择按 C 库与内核接口,不按系统宏 | `cfg(c-abi = …)` | ✅;属于程序自己 | +| V8 | 每个身份的 `mcpp run` 有回答:运行,或指名缺的运行器 | 运行面 | G6 在途;G7 的运行器已有包 | +| V9 | C 的非局部跳转在每个身份可用 | G8 | 在途 | +| V10 | 产物的名字与后缀由程序决定 | 构建程序 | ✅ | + +**平台覆盖。** 验证工程自己列出的平台,加上本文新增考察的两行: + +| 平台 | 路线 | 取决于 | 状态 | +|---|---|---|---| +| Linux、Windows、macOS、FreeBSD | B 默认,A 可选;oke | — | ✅(FreeBSD 的运行靠本地虚拟机) | +| Android、Web | A(NDK、emsdk) | G7、G8 | 构建 ✅;运行在途 | +| NetBSD | B | openkal-netbsd、K10 | 未开始 | +| OpenBSD、Haiku、GNU/Hurd | A + okrun 在 openkal-posix 上 | X4、K2、各自的磁盘镜像 | 未开始 | +| System V(i386) | 32 位 | K7 | 未开始 | +| **DOS,32 位** | A(DJGPP)或 B(openkal-dos) | 第 8 节 | 〔析〕 | +| **DOS,16 位** | A,仅 C | 第 8 节 | 〔析〕 | + +### 7.1 通用设施就绪后,验证工程能到什么程度 + +"就绪"指第 10 节的 S1–S4 完成;S5、S6 的内容单独标出。层级的划分是推断〔析〕,依据是第 2 节的台账、本地报告与第 8 节的探针。 + +| 层级 | 内容 | 身份数 | 取决于 | +|---|---|---|---| +| 一:每次提交构建并运行 | Linux(x86_64、aarch64 的 musl 与 gnu)、Windows(gnu、musl,wine 与真机)、macOS(两个架构的 musl,真机运行)、FreeBSD(四个身份,dirun)、Android(两个,模拟器)、Web(node)、ok 映像(两个架构,在四种宿主的 okrun 上) | 18 个身份,约 14 个运行环境 | S1–S4 | +| 二:每次提交构建,夜间运行 | NetBSD;OpenBSD、Haiku、GNU/Hurd(A 路线,okrun 在 openkal-posix 上) | 再加 5–7 个 | S6;各系统官方镜像可再分发 | +| 二甲:每次提交构建并运行 | DOS 32 位(DJGPP 加 DOSBox;它的 74 个源文件今天有 70 个不改即过,T5) | 1 个 | S5;程序补作业控制、多路等待、优先级三处 | +| 三:不进门禁 | System V(i386):K7 之后可做一次手工验证;DOS 16 位:程序先要拆掉超过 64 KB 的结构体(T7);PureDarwin:无法验证 | — | 第 8.3、8.4 节 | + +**它得到的。** + +- **一份清单覆盖全部身份。** 资源脚本无条件书写(G3 之后),共用入口的两个程序不需要包装文件(E2 已落地)。源码侧 + 剩下的是取自身路径的 4 行守卫——这是程序自己的平台探测,改为按 C 库选择后可以提交给它的上游。 +- **一台 Linux 机器上的全部验证。** `mcpp test --target <身份>` 经运行器描述到达每个层级一的环境;它自己的 CI 不需要 + Windows、macOS、FreeBSD 的机器就能断言行为同构,真机运行留给发布前。 +- **一条命令的发布矩阵。** 各身份的原生程序、一份多架构的 ok 映像、覆盖 Linux、macOS、Windows、FreeBSD 的多系统 + 单文件;经 xim 成为 xlings 支持的每个宿主上可安装的包。 +- **新代码可以用 C++23 模块。** C89 内核不动,新功能在支持 `import std` 的身份上用模块写,其余身份回落到 C 入口。 + +**通用设施给不了的(语义面,属于程序)。** + +| 方面 | 说明 | +|---|---| +| 进程模型 | DOS 没有并发的子进程与管道,Web 没有进程;管道在这些环境里要由程序用临时文件实现,或者如实不支持 | +| 终端 | 行编辑依赖终端的原始模式;浏览器里的终端界面是程序自己的工作,设施只保证 node 下的行为 | +| 系统的原生语义 | Windows 上的路径写法与命令行原样传递使它按 P4 默认走 A 路线;B 路线的 Windows 程序是可选项 | +| 16 位的体量 | 一个超过 64 KB 的上下文结构体(T7)与 54 KB 的消息表(T6),只能由程序改数据布局、裁语言集合 | +| 不能再分发的系统 | 没有可再分发镜像的系统(System V)进不了 CI,只能有一次性的手工验证 | + +**对它上游的含义。** 它现在手工维护多套构建描述(Makefile、CMake、Meson、xmake、Gradle、Visual Studio 与两种 IDE +工程)。设施就绪后,一份 `mcpp.toml` 在覆盖面上超过它们的并集。是否采用是它上游的决定,不在本方案范围内。 + + +## 8. 层级三与固件:探针的结果 + +本节的每个"测"都是 2026-11-12 在一台 x86_64 Linux 主机上做的本地测量(无 KVM),命令与摘要在 `okexp/t3/PROBES.md`; +它们不是门禁。未测的部分仍标〔析〕。 + +### 8.1 探针 + +| # | 探针 | 结果 | 说明了什么 | +|---|---|---|---| +| T1 | 已发布的 2026.11.10.2,`--target i686-none-elf`,不带 C 库,经 `int 0x80` 写一行后退出 | ELF32;`qemu-i386` 下输出一行,退出码 42〔测〕 | 32 位一类的编译、链接、运行路径在引擎里已经通;K7 缺的是 C 库与运行时,不是引擎 | +| T2 | DJGPP(GCC 12.2)编 C 程序,DOSBox Staging 无显示运行 | 输出与退出码 42 如实;墙钟 0.33 秒〔测〕 | DOS 的运行器是"接口的宿主实现",与 wine 同类,不需要磁盘镜像,可以进每次提交的门禁 | +| T3 | 同上,C++23 | `import std` 没有,`` 没有〔测〕 | 这条路线是"C 与旧标准 C++"的行 | +| T4 | DOSBox 的命令解释器不展开 `%ERRORLEVEL%` | 要用 `if errorlevel N`〔测〕 | 客体的包装必须是数据(D22) | +| T5 | 验证工程的 74 个源文件,DJGPP,不改源码 | 70 个通过;4 个失败在 `SIGTSTP`、`sys/select.h`、`clock_gettime`、`setpriority`〔测〕 | 缺口是作业控制、多路等待与优先级——语义面,属于程序 | +| T6 | 上述 70 个目标文件的大小 | 代码加只读数据 113.8 KB,bss 13.4 KB;四套语言的消息表占 54.0 KB〔测〕 | 16 位下,单语言构建才可能放进一个数据段 | +| T7 | 同一批源文件,Open Watcom 16 位 large 模型 | 13 个通过,61 个失败;主因是它的上下文结构体超过 64 KB(`E1098`)〔测〕 | 16 位的拦路石是程序自己的数据结构,不是工具链或设施 | +| T8 | 三十行的平台包定义 `x86_64-uefi`,已发布的 2026.11.10.2 构建,qemu 加 OVMF 运行 | PE32+ EFI 应用;串口输出一行;状态经调试端口回到宿主;墙钟 2 秒〔测〕 | P15 对固件成立:引擎零改动 | +| T9 | 对照 openkal 规范检查 `openkal.process` | 文本未禁止"启动即等待";通道、终止等并发相关能力已是可不声明的属性〔读〕 | 单任务环境可以提供该接口,缺一句明示与一致性覆盖 | + +没有做的:DOS 的 B 路线(需要 K7 的 i386 C 库);gcc-ia16(没有预编译的发布物);System V 与 PureDarwin(没有可用的 +镜像)。 + +### 8.2 DOS,32 位 + +| 身份(名字待定) | 路线 | C 库 | C++ | 引擎需要什么 | 状态 | +|---|---|---|---|---|---| +| `i386-dos-djgpp` | A | DJGPP 的 libc | libstdc++ 12,无模块 | 零:GCC 族的一个新载荷加一条提供者 | 工具链与运行器已测(T2、T5);经 mcpp 构建未测〔析〕 | +| `i386-dos-musl` | B | openkal-musl(i386) | openkal-llvm-runtime,可以有 `import std` | 零,若 P15 成立 | 〔析〕;前置是 K7 | + +**B 路线**由四个包组成:编译与链接沿用 `i686-none-elf` 行的做法(T1);内核层实现 openkal-dos 经 DPMI 的实模式中断模拟 +调用 `int 21h`,如实提供核心三个接口、`env`、`time`、`fs`、`terminal`、`region`,缺席 `task`、`net`、`datagram`、 +`space`、`map`、`random`;容器用平台包的 `[platform.link] seal`(链接后对产物执行的命令序列)把 ELF 包进带 DPMI 入口桩 +的 MZ 文件,与 ok 映像的 `okld` 封印同一机制;运行器是 DOSBox。 + +**A 路线**是先做的那条:它不检验 openkal,但工具链、运行器、验证工程的源码三者今天就对得上(T2、T5)。 + +### 8.3 DOS,16 位 + +| 方面 | 事实 | 后果 | +|---|---|---| +| 编译器 | LLVM 没有 8086 后端〔析〕;Open Watcom 可用(T7),是自己的命令行;gcc-ia16 是 GCC 命令行,没有预编译发布物 | 不可能有 C++23 与模块;这一行是"仅 C"的行 | +| C 库 | musl 假定平坦的 32 位以上地址空间〔析〕 | 没有 B 路线,没有 openkal,没有映像 | +| 程序 | 一个超过 64 KB 的结构体(T7);消息表 54 KB(T6) | 先要程序改自己的数据布局与语言集合;这之前工具链帮不上 | +| 引擎 | Watcom 需要一种新的驱动方言;gcc-ia16 需要"按能力探测旗标"——旧编译器不接受引擎缺省给的某些旗标〔析〕 | 后者是通用机制,第二个使用者是 avr、msp430、m68k 一类老的 GCC 交叉链 | + +结论:16 位 DOS 在设施一侧只值得做"按能力探测旗标"这一个通用机制,而且要等出现第二个使用者;它本身的可行性由 +程序决定。不立项。 + +### 8.4 32 位一类、System V 与 PureDarwin + +- **32 位一类(K7)。** T1 说明引擎一侧没有缺口。工作全在 openkal 家族:规范的 32 位约定、openkal-linux 的 i386 系统调用层、 + musl 与 C++ 运行时的 32 位构建、32 位映像。先做 `i686-linux-musl`,它有 qemu-user,可以进门禁。 +- **System V(i386)。** K7 之后是一个 openkal 实现加一个平台包。没有可再分发的镜像,进不了 CI;只能有一次性的手工 + 验证,结果写进记录而不是门禁。 +- **PureDarwin。** macOS 的 B 路线产物只依赖 libSystem〔读〕,推断可以直接运行;没有 Mach-O 的宿主运行器,也没有可用 + 镜像,无法验证。不投入。 + +## 9. 辐射范围与收益 + +通用设施的使用者不限于验证工程。每一行的"收益"是在该设施落地后不需要额外引擎工作即可得到的。 + +| 领域 | 用到的平面 | 收益 | +|---|---|---| +| **生态自举** | ①②③ | mcpp 与 xlings 自身经 B 路线产出 FreeBSD 等系统的静态程序,包管理器与构建工具的宿主范围随 openkal 实现的增加而扩大;新宿主上的第一份 mcpp 不需要在该宿主上编译 | +| **库作者的跨系统测试** | ③④ | mcpplibs 的任何库在一台 Linux CI 机器上对 Windows(wine)、FreeBSD(虚拟机)、Android(模拟器)、aarch64(qemu)运行自己的 `mcpp test`;今天这需要四种 CI 机器或手写胶水 | +| **命令行工具的发布** | ①②,oke | 一份映像或一个多系统单文件覆盖各系统;构建可复现(两次打包字节相同已在本地测得),可以作为供应链证明的基础 | +| **嵌入式与固件** | ①②③ | 裸机行已有十余个;金样测试与运行器陈述同样服务它们。UEFI 应用由一个平台包给出(T8,已测),其上接 openkal 的实现后可以用 C++23 模块与 `import std` 写引导加载器与固件工具;"按能力探测旗标"(8.3)覆盖 avr、msp430 一类老的交叉链 | +| **图形与游戏** | ②(基座) | #913:GUI 工程在三种宿主上产出 Windows PE;基座准入契约保证这类载荷的 C++ 与 `import std` 可用 | +| **教学** | ①③④ | 操作系统与编译课程:同一份作业在多个系统的模拟器里运行并自动比对输出;环境由 xlings 一条命令给出 | +| **非 mcpp 用户** | ②,xim | 基座包与磁盘镜像是普通的 xim 资产,CMake、xmake 用户同样可以把 `aarch64-linux-gnu-sysroot` 当作 `--sysroot`;dirun 不含生态词汇,任何 CI 可用 | +| **复古与长尾系统** | 全部 | DOS、SVR4、Haiku 的准入成本由 P15 界定:数据加包。每接入一个,就多一次对"引擎零改动"的验收 | +| **IDE 与语言服务** | ① | 行、系统表与运行器描述是数据,mcpp-ide 可以直接列出"此工程可去的身份、每个身份怎样运行",不需要自己的表 | +| **自动化代理** | ②④ | 密封的工具链与机械门禁使代理的改动可以被检查拦下;本次审计发现的问题大多属于"门禁本可以拦下" | + +## 10. 阶段 + +| 阶段 | 内容 | 出口 | +|---|---|---| +| S0 冻结 | mcpp#924 不合入;本文评审定稿 | 第 11 节的决定全部有结论 | +| S1 纠正 | C1–C11;mcpp#924 拆分(D5);C4 的修订 3 | 分支上 2026.11.10.2 的全部行为恢复;295、299 绿;三个未定性失败有结论 | +| S2 门禁 | 链接行金样、gcc `PayloadFirst` 腿、基座准入契约、安装检查不可跳过、系统名检查的基线 | 本次审计的每个阻断问题都有一个会失败的检查 | +| S3 数据化 | 已有行键的数据(`loader`、`execution`)、系统表的 `uname`、提供者拆分规则入规范、提供者的 `runner` 与 `system-headers`;C12–C15 | 系统名基线下降到只剩 `modules/platform/` | +| S4 验证面 | 两个金丝雀、矩阵数据、夜间报告;跨系统 `mcpp test`;mtest 的 M1–M6 与 e2e、bench 的收敛(5.9,按子系统分批,可与 S3 并行);dirun 的整形(5.10);CI 的触发模型(5.11) | 覆盖报告作为 CI 产物;0 级脚本只剩"保留"类;PR 墙钟与作业分钟达到 5.11 的目标 | +| S5 32 位与固件 | K7(先 `i686-linux-musl`);DOS 的 A 路线(DJGPP 载荷、DOSBox 运行器);UEFI 的平台包与 openkal 实现;单任务环境的规范明示 | `i686-linux-musl` 为 verified;DOS 与 UEFI 的金丝雀进门禁,引擎零改动 | +| S6 新系统 | DOS 的 B 路线(以 P15 验收);第二类系统(K2、X4、各磁盘镜像);openkal-netbsd | 每个系统:引擎零改动 | +| 不排期 | 16 位 DOS(8.3);System V 的一次手工验证;Serenity;Plan 9(仅 C);K12;PureDarwin | 出现第二个使用者或程序一侧就绪时再议 | + +S1 与 S2 之间不插入新能力。S3 之后的每个新系统是对架构的一次验收,而不是一次扩建。 + +## 11. 决定 + +### 11.1 已定(2026-11-12 评审) + +| # | 决定 | +|---|---| +| D16 | e2e 收敛到 mtest 的 C++ 用例,按下沉、合并、保留三类,按子系统分批,在 S1 之后开始 | +| D17 | 保留类允许长期存在,数量由基线持有、只许减少 | +| D18 | bench 的 harness 并入 mtest;bench 不随每次 CI 运行,触发见 5.11 | +| D19 | mtest 的升级项纳入本方案(5.9 的 M1–M6) | +| D20 | CI 按 5.11 编排:事件对集合、按改动选择、每个宿主一道闸、分片数随工作量、可手动触发全量 | +| D21 | dirun 收为一个包,测试用 mtest,命令行用 cmdline;Python 参考在合同条款测试等价后删除 | +| D22 | 客体与板的描述是随资产发布的数据 | +| D23 | 伪终端归 `mcpplibs.os`,expect/send 归 mtest 的 e2e 模块,dirun 依赖二者 | + +### 11.2 待裁决 + +| # | 问题 | 推荐 | 备选 | +|---|---|---|---| +| D1 | `aarch64-linux` 在 x86_64 主机上回答哪个身份 | musl(与 2026.11.10.2 相同);交叉 glibc 条目优先级 40,写全三元组才取到。依据 Q3 与 P12 | 接受变为 glibc,写进 CHANGELOG 作为一次缺省变更 | +| D2 | 是否把"一条条目一种安排"(P11)与"请求的回答是契约"(P12)写进 SPEC-016 | 是 | 只作为评审惯例 | +| D3 | 不在本机运行的动态产物,加载器写成什么 | 行的 `loader`(已有键),`RUNPATH` 为空,运行器给 `{sysroot}` | 维持构建机路径,由 `pack` 改写 | +| D4 | 系统级事实的 schema | 第二版的小方案:填已有的行键;新增只有系统表的 `uname` 与提供者的 `system-headers`;载体身份由平台的 `runner.hosts` 陈述 | 第一版的大方案:系统表带 `family`、`kernel-abi`、`carrier` | +| D5 | mcpp#924 怎样处理 | 拆开:G3、G5、G8 各一个 PR,各带规范与两种语言的文档;G6 按 C9 重做为数据加已有谓词的扩展;G1 推到 S3 之后;版本号在合入时取 | 整体修到绿后合入 | +| D6 | `abi.longjmp` 的形状 | 保留键名;未写时随 `exceptions` 的模型;两者都写且不相容时在解析期拒绝;成员分组键里"声明了才出字段" | 改为一个覆盖异常与跳转的 `unwind` 键 | +| D7 | abi 成员的渲染是否改为提供者上的数据表 | **推迟**:目前只有 Emscripten 一个使用者(P2);出现第二个时再做 | 现在就做 | +| D8 | 运行器包何时安装 | 第一次 `mcpp run`/`mcpp test` 需要时,与工具链同一种同意方式 | 只提示,由用户自己装 | +| D9 | 客体方式的运行器是否只留 dirun | 是;`vm-run.py` 退役;镜像正名 `-disk-image` | 两者并存 | +| D10 | 基座准入契约放在哪里 | xim-pkgindex 的 CI 作业,调用已发布的 mcpp;不新增 mcpp 子命令 | `mcpp` 增加一个检查基座的子命令 | +| D11 | K7 是否提为正式阶段 | 是,先 `i686-linux-musl`;探针 T1 说明引擎一侧没有缺口 | 维持为讨论项 | +| D12 | 合入纪律 | 必需检查未全绿不合入;管理员权限只用于评审规则本身无人可满足的情形,且在 PR 里写明;一个 PR 一件事;交接记录放 `.agents/docs/todos` | — | +| D13 | 提供者条目是否带自己的 `tier` | 是,但只在 C2 的拆分落地时一起做 | 等级只在行上 | +| D14 | 16 位 DOS | 不立项(8.3):拦路石在程序一侧;"按能力探测旗标"等第二个使用者 | 立项做 gcc-ia16 的"仅 C"行 | +| D15 | 单任务环境上的 `openkal.process` | 在规范里明示"启动者可能在被启动者结束后才继续",一致性套件覆盖;在 UEFI 或 DOS 的实现之前完成 | 让这类环境缺席该接口 | +| D24 | 运行器的缺省安排写在哪里 | 提供者条目的 `runner`(与平台的 `runner` 同一形状),不新增表 | 第一版的 `[[platform.runner]]` | +| D25 | G6 的做法 | 不启动另一系统的程序;内置行与平台行同一谓词,启动前回答"已构建,未运行" | 维持 mcpp#924 的子进程加信号归类 | +| D26 | UEFI 的身份放在哪里 | 先由平台包定义(今天可做,T8);出现第二个使用同一身份的平台包时升为内置行 | 现在就加内置行 | +| D27 | DOS 的先后 | A 路线(DJGPP 加 DOSBox)进 S5;B 路线在 K7 之后作为 P15 的验收 | 只做 A;或不做 | +| D28 | 固件所需的宿主文件(OVMF)与运行器(DOSBox、wine)是否做成 xim 包 | 是(P9) | 维持宿主依赖,测试以能力词跳过 | + +## 12. 风险 + +- **schema 变更。** 第二版把新增压到两个键(`uname`、`system-headers`)加平台 `runner.hosts` 的一个字段;平台包的最低 + mcpp 版本仍随之上升。旧平台包没有这些数据时,引擎对内置系统的回答与今天相同(数据的缺省,不是代码分支)。 +- **加载器规则改变交叉产物的运行方式。** 采纳 D3 后,交叉 glibc 产物在构建机上不能直接启动,必须经运行器。这是正确 + 的语义,但与"构建完马上能跑"的直觉不同,诊断要说清楚。 +- **K7 跨六个仓库。** 它是 S5、S6 多数内容的前置;若推迟,DOS 的 B 路线、SVR4 与 i686 一起推迟,A 路线与 UEFI 不受影响。 +- **准入契约拉长索引 PR 的周期。** BP3–BP7 需要已发布的引擎认识被测的载荷;引擎与载荷同时变化时有先后问题——载荷先 + 以候选修订进入,引擎发布后再转正。 +- **e2e 迁移改写九万行。** 风险是迁移中丢断言或改了被测行为。对策已在 5.9:先原脚本后新用例、映射表、一个子系统一个 PR。 + 在 M1–M3 就绪之前不开始;第一批选耗时最长而断言最少的子系统,用它校正 5.11 的目标数字。 +- **按改动选择漏跑。** 对策是 5.11 的安全边界与 main 上每次合入的 `full`:PR 漏掉的,合入后的那次运行会抓到。 +- **范围。** 本方案有七个阶段与一次测试体系迁移。S1 与 S2 是不可分的最小切片;S3 之后的每一项都应当在前一项的门禁 + 绿了之后才开始,同一时间进行中的阶段不超过两个。 +- **探针不是门禁。** 第 8 节的测量是一台主机上的一次运行。它们回答"路是否通",不回答"是否稳定"。 + +## 13. 与既有记录的关系 + +| 记录 | 关系 | +|---|---| +| [2026-11-08 顶层设计](2026-11-08-ecosystem-top-level-design.md) | 保留 | +| [2026-11-09 目标模型](2026-11-09-target-model-design.md) | 保留;本文第 5.2、5.3 节是对它的增补,定稿后进入 SPEC-016 | +| [2026-11-10 跨平台生态方案](2026-11-10-cross-platform-ecosystem-design.md) | 保留;P1–P9 与 Q1–Q6 是本文的前提;其未完成的工作项列在第 2.4 节 | +| [2026-11-10 测试与解析补充](2026-11-10-test-and-resolution-supplement-design.md) | 保留 | +| [2026-11-12 三平面架构](2026-11-12-three-plane-platform-architecture-design.md) | 其第 1、2、4、5 节由本文第 5 节扩展为四个平面;第 3 节"已成立"的七条中,第 1、2 条与实测不符(第 2.3 节),由本文第 2 节的台账取代。该记录随 mcpp#924 的处理一并修订或标为 superseded | + +## 14. 多视角自审 + +### 14.1 自审中发现并已修改的(第一版 → 第二版) + +| # | 视角 | 发现 | 修改 | +|---|---|---|---| +| 1 | 规范一致性 | 第一版把行的 `loader` 当作新键,并让系统表带 `family`、`kernel-abi`。SPEC-016 §2.1 已经有 `loader`、`execution`、`family`、`exe-suffix`、`object-format`,只是内置数据没有填 | 5.2 改为"先填已有的键";新增压到 `uname` 与 `system-headers` | +| 2 | 语义与安全 | 第一版沿用了 mcpp#924 中 G6 的做法(把另一系统的程序作为子进程启动,观察它的死亡)。另一系统的系统调用号在本机内核上是别的操作,参数未定,后果不确定;而 SPEC-016 §6 已经规定这种情形在启动前回答"已构建,未运行",代码里的谓词只是没有覆盖内置行 | C9 与 D25:撤回信号归类,扩展已有谓词 | +| 3 | 简洁 | 第一版新增 `[[platform.runner]]` 表与 `kind` 键。运行器已有四处可写,缺的只是"内置提供者也能陈述" | 5.5 改为提供者条目的 `runner`,不新增表;"种类"降为文档里的分类 | +| 4 | 归属(P16) | 第一版把载体的身份(`carrier`)放进内置的系统表。它只对带运行器的平台有意义 | 移到平台的 `runner.hosts`,由 openkal-platform 陈述 | +| 5 | 第二个使用者(P2) | abi 成员的数据化渲染(D7)只有 Emscripten 一个使用者 | 推迟 | +| 6 | 时间成本 | 第一版的"两级流水"让全部扇出等 Linux 的闸,绿的路径会比现在更慢(Linux 构建约 16 分钟是长杆) | 5.11 改为构建立即并行、每个宿主一道闸、红了取消整次运行 | +| 7 | 证据 | 第一版的 DOS 与固件全是推断;"机器"类运行器把 DOS 也算了进去 | 做了九个探针(第 8 节):DOS 的运行器是 DOSBox,不需要镜像;UEFI 今天就能以平台包构建并运行;16 位的拦路石在程序一侧 | + +### 14.2 各视角的结论 + +| 视角 | 结论 | +|---|---| +| mcpp 规范 | 新增的 schema 是两个键与一个字段;P11、P12 进 SPEC-016;C7 让实现回到 SPEC 的打包章;G6 回到 SPEC-016 §6。没有一项与现行规范冲突 | +| openkal 规范 | 只有一处明示(单任务环境的 `process`)与 K7 的 32 位约定;固件与 DOS 都是新的实现,不改接口清单 | +| xlings 生态 | 新的资产类只有运行器所需的三个包;准入契约与布局检查是对修订通道的补全,不改它的模型 | +| 架构 | 四个平面各有数据、归属与门禁;引擎新增的机制只有链接模型的单值化与运行器模板的两个占位 | +| 不硬编码 | 系统名的出现次数由检查持有;这次列出的七处各有去向;新系统以引擎零改动验收,UEFI 已经通过一次(T8) | +| 归属 | P16 的四问把板、固件、客体、程序的事实各自留在自己的包里;16 位 DOS 的结论(T7)是这条判据的一个实例 | +| 简洁 | 第二版比第一版少一张表、一个键、一个决定;每个保留的新增都指得出它取代的代码 | +| 兼容性 | S1 恢复 2026.11.10.2 的全部行为;新能力以低优先级进入(P12);`abi.longjmp` 尚未发布,可以自由调整 | +| 跨平台 | Windows 的两成套件因写法不运行,这由迁移解决;dirun 的 Windows 宿主取决于"管道代替伪终端"是否成立,未测 | +| 性能与成本 | CI 的目标数字是估计,由第一批迁移校正;bench 从每次 CI 移出 | +| 测试与验证 | 本次审计的每个阻断问题在 S2 之后都有一个会失败的检查;金丝雀把"十八个目标通过"变成 CI 产物 | +| 供应链 | 资产的可用性进入准入;镜像不一致时客户端回落并报告 | +| 可实施性 | 最小切片是 S1 加 S2;其余每项可以独立排期,彼此的依赖写在第 10 节 | + +### 14.3 仍然没有把握的 + +- **第 5.9、5.11 的占比与目标数字**是估计。迁移的三类各占多少要靠一次逐脚本的清点;PR 墙钟与作业分钟的目标要靠第一批 + 迁移后的测量。 +- **mtest 的现状**只读了文档。M1–M6 里可能有已经存在的,实施前逐项核对。 +- **`mcpplibs.cmdline` 的能力**没有核对;dirun 改用它之前先确认可重复选项与 `--` 之后原样传递。 +- **dirun 以管道代替伪终端**没有测过。 +- **DJGPP 经 mcpp 构建**没有测过:探针直接调用了它的编译器。GCC 族的新载荷在引擎里是否真的只是数据,要做一次才知道。 +- **按改动选择对数据文件的处理**:内置数据编进引擎后,覆盖图能否把它与读它的用例对应起来,没有核对;5.11 因此把它 + 写成"影响全部目标类用例"的保守规则。 +- **三个未定性的 CI 失败**(Windows 访问违例、xcode-27 的 1068、Windows 的 860)仍然没有根因。 + +## 15. 实施记录(第三版) + +本节随实施更新。每一项写明它在哪里、由哪个检查持有;本地测量标明主机。 + +### 15.1 已实施 + +| 项 | 位置 | 持有它的检查 | +|---|---|---| +| C1:GCC 分支恢复;基座的运行时目录同样到达 C 链接 | mcpp `flags.cppm` | `tests/unit/test_link_line_golden.cpp`;e2e 1080(在 mcpp#924 的旧头上失败) | +| C2、D1、P11、P12:`glibc` 与 `glibc-sysroot`(优先级 40)两条条目 | mcpp `builtin.toml` | 行表快照(`aarch64-linux` 在三种宿主上的回答不变,新候选排在末尾);e2e 295、1076 | +| C3、D3:不由本机加载的动态产物写行的 `loader`,无本机 RUNPATH;基座包单一读法 | mcpp `base.cpp`、`linkmodel.cppm`、`hermetic.cppm` | 金样测试;e2e 1076(解释器、RUNPATH、产物内无本机路径) | +| C4:sysroot 修订 3 | xlings-res、xim #1146 | 配方的安装钩子与 L4 测试;布局契约 | +| C5:1076 重写(C、C++、`import std`,动态与静态,经提供者的运行器运行);不再有"跳过即通过" | mcpp e2e | 能力词 `x86_64-host`:不适用的宿主被排除并由运行报告 | +| C6、D4:系统表(`uname`)、提供者的 `system-headers`、`tier`、`runner`;平台 `runner.built-as` | mcpp 模式、生成器与平台清单解析;openkal-platform 0.27.0 | 单元测试(polyglot、pack、清单);`check_system_names.py` | +| C7:`--systems all` 取系统表,无程序的对写为未覆盖 | mcpp `pack.cppm` | 单元测试;e2e 986、1078 | +| C8、D6:`abi.longjmp` 与 `exceptions` 不一致时拒绝;声明了才进键 | mcpp `toolchain.cpp`、`project.cppm` | e2e 1079 | +| C9、D25:另一系统的程序在启动前回答"已构建,未运行";mcpp#924 的子进程与信号归类已撤回 | mcpp `execute.cppm` | e2e 1077、1040 | +| C10:系统头文件一个谓词,取所选提供者的 `system-headers`,装不上即拒绝 | mcpp `windows_resources.cpp` | e2e 1075 | +| D8、D24:提供者的 `runner`,`requires` 在第一次运行时安装,占位 `{sysroot}`、`{pkg:}` | mcpp `platform.cpp`、`execute.cppm` | e2e 1076 | +| D13:安排的档位进入选择与 `mcpp why target` | mcpp `target_select.cppm` | 单元测试 | +| D18:bench 只在 main、手动与 `ci:bench` 运行 | mcpp `ci.yml`、`ci-linux.yml` | — | +| C14:配方自己的验证在索引 CI 中运行 | xim #1146 | `tests/test_ci_architecture.py` | +| P10:系统名只减不增 | mcpp `.github/tools/check_system_names.py` 与基线 | CI 的文档与仓库检查作业 | +| P17、5.13:七项重复实现的收敛 | mcpp | 收敛前后同一组单元测试(169 个程序、2627 个用例) | + +本地测量(x86_64 Linux,无 KVM,2026-11-12):交叉 `aarch64-linux-gnu` 的 C、C++ 头文件与 `import std` 程序各自构建并 +经 `mcpp run` 运行,退出码原样返回;`import std` 的静态链接同样。以修订 3 的 sysroot 测得。 + +### 15.2 实施中对本文的修正 + +| 本文原先的说法 | 实测 | 修正 | +|---|---|---| +| "链接模型需要一个新的单值结构"(5.4) | 单值已经存在:`TargetSide::cAbi.origin` 与 `ToolchainLinkModel`。缺陷是 mcpp#924 在渲染处加了第二处推断,以及带清单的基座走了布局探测的通道 | 不新增结构;删去第二处推断,基座解析处决定一次 | +| sysroot 修订 2"归档顶层目录与资产名不一致"是缺陷(2.2) | 该配方没有安装钩子,客户端的缺省解包认 `<名>-<版本>`,修订 2 因此装得正确;改为资产名之后,缺省解包反而把目录多套一层 | 修订 3 的顶层目录取资产名,并由配方的 `install()` 安装,与其余基座包一致。这个问题由 C14 新增的步骤在合入前拦下 | +| 修订 2 有四处缺陷 | 第五处:`libm.a` 是保留了安装期占位符的 ld 脚本,`libm.so` 又被改写为指向它,所以任何 C++ 链接与静态链接都解析不了 libm | 修订 3 以一条规则处理 `lib/` 下的全部 ld 脚本 | +| "索引 PR 的安装检查有 15 项被跳过"(2.2) | 被跳过的是按路径选择、与该改动无关的套件;安装作业在三个宿主上运行了。缺的是安装之后的验证:各配方模块的 L3、L4 在 CI 中只对一个包运行 | C14 改写;修订通道的 guard 不需要改动 | +| D5:mcpp#924 拆成多个 PR | 负责人要求每个仓库一个 PR | mcpp#924 续用,其内容按本文重做 | +| xim-pkgindex 一个 PR | 引擎的 CI 需要索引里已有修订 3;基座准入作业与运行器包又需要已发布的引擎 | 两个 PR:发布前(#1146),发布后 | +| 运行器模板的 `{image}` 占位(5.5) | 一个模板可以用到两个包 | `{pkg:}` | + +### 15.3 尚未实施 + +按第 10 节的阶段。S4 的 e2e 与 bench 向 mtest 的迁移、mtest 的 M1–M6、dirun 的整形、两个金丝雀与夜间矩阵、CI 的按改动选择与每 +宿主一道闸;S5 的 K7、DOS 的 A 路线、UEFI 平台包与单任务环境的规范明示;S6 的新系统;xim 的第二个 PR(基座准入作业 +BP3–BP7、wine、DOSBox、OVMF 的包、`-disk-image` 的正名);openkal-freebsd 的运行器改用 dirun;mcpp#924 三个未定性 +的 CI 失败的根因。 diff --git a/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md new file mode 100644 index 000000000..c5744579e --- /dev/null +++ b/.agents/docs/2026-11-12-three-plane-platform-architecture-design.md @@ -0,0 +1,125 @@ +--- +subject: architecture +status: superseded +superseded_by: 2026-11-12-ecosystem-architecture-optimization-design.md +--- + +# The three-plane platform architecture: mcpp × openkal × xim + +Date: 2026-11-12 +Status: superseded by +[the ecosystem architecture optimization design](2026-11-12-ecosystem-architecture-optimization-design.md), +which extends sections 1, 2, 4 and 5 into four planes. Section 3 was written +before the round it describes was checked: its items 2 and 3 did not land in +the form stated there. A base package is read as a sysroot and the artifact +names the row's loader (not a loader "folded sysroot-relative" beside the +payload's own `-B/-L`), and an artifact of another system is not started at all +(not run as a child whose death is classified). The record of what was +implemented, with the check that holds each item, is section 15 of the +superseding design. + +## 1. The statement + +A platform enters this ecosystem as **data**, never as engine code. Three +data planes carry everything a target needs, and one normalisation layer +decides whether what was built is right: + +| Plane | Carries | Where it lives | Invariant | +|---|---|---|---| +| ① row | serves/hosts/toolchain/sysroot/linkage/abi of a target identity | the toolchain model's table, or a platform package's own rows | target differences are data; the engine holds no per-target branch | +| ② payload | the toolchain bytes, and the base package that is the target's C library | the index (xim), installed by the fetcher through the revision channel | a build host needs mcpp itself and nothing else; where a row names a base package the host's link model yields entirely | +| ③ runner | how an artifact of another system starts (a declared prefix, an emulator, a loader) | row/platform data; resolved by the runner lookup | build never depends on a runner; running consumes the declaration | +| verification | one script, one expected output, normalised (CRLF, colour, codeset, console noise) | the shared harness | the harness knows no target; every executor family answers through it | + +Two red lines hold throughout, and the gates hold them mechanically: +default behaviour is unchanged by any of it (the regressions stay green), and +no host name enters the engine — host differences are statements rows make. + +## 2. The layering the planes serve + +```mermaid +flowchart TB + subgraph consumer["a program of any system"] + A["import the C++ modules / C sources"] + end + subgraph libc["one C library, ported once"] + M["libc above the kernel ABI (spec)
static where the row says static"] + end + subgraph kal["the kernel-ABI specification"] + K["openkal: the interface, its conformance suite
and one implementation per system family"] + end + R1["row ① + payload ②"] --> A + M --> A + K --> M + KF["firmware implementations
(UEFI boot services, RISC-V SBI)"] --> K + KP["kernel implementations
(the reference kernel's own interface,
a BSD kernel's, a Windows kernel's)"] --> K + KX["a POSIX-library implementation
(for systems whose own libc is the interface)"] --> K + R3["runner ③"] -. runs the artifact on.-> consumer +``` + +The specification's own rules make the layering safe: an implementation may +declare an interface absent (the conformance suite decides which absences are +legal), a C library above the interface never sits above an implementation +that would call back into it (layer-cycle refuses that graph at resolution), +and a program states which ABI it speaks by depending on one. + +## 3. What the merged round established + +Measured, not aspirational (each item landed with its gate green): + +1. **Rows as the only place targets differ.** Eighteen identities build from + one engine with zero per-target code; the identity that was missing + arrived as one table row plus one base package. +2. **Base packages.** Where a row names one, the payload is the target's C + library: the driver gets `--sysroot`, the payload's own `-B/-L`, a + loader folded sysroot-relative, and the compiler's runtime directory — + and the host's link words, runtime directories and closure rules yield + entirely. A cross needs nothing from the machine it stands on. +3. **Foreign-kernel execution.** An artifact of another system, of this + machine's architecture, runs as a child: its death by signal is named, + its status returned unchanged, and the runner declaration follows it. +4. **ABI members as data.** `abi longjmp` states how a jump crosses the + WebAssembly boundary; the probe renders the flag on both sides, and a + row that is not the member's subject is refused rather than ignored. +5. **Corrective revisions.** A resource whose bytes must change without a + version change is re-published under the same key with a raised + revision; the updater channel enforces that a raise is total — every + platform, one sha per url, shapes compared. +6. **One file, several systems.** A pack can hold the programs of several + systems: a PE leads with a POSIX script in its DOS header, and every + POSIX system's program rides behind it. +7. **Layer vocabulary.** `kernel-abi` is a layer the rows, the payloads and + the requirement side all speak (`layers.kernel-abi`, `requires`), an + implementation written above a C library never sits beneath one it would + call back into — the resolution refuses that graph before anything is + compiled — and a payload's kernel-abi is the target's system, so a + requirement stated on it holds across every route to the row. + +## 4. What the next systems consume + +The same planes, with no engine work: + +| Scenario | What it uses | What is left to build | +|---|---|---| +| A POSIX-family kernel with its own libc | the POSIX-library implementation of the ABI (already above every such libc), rows, a cross toolchain payload | the toolchain payload; a disk-image asset; an emulator runner | +| Firmware (UEFI, SBI) | the existing firmware implementations; an ordinary PE row and three link flags state a UEFI application — measured, no new target kind; a runner's exit status is the program's (the contract already carries firmware: the harness states the status) | an OVMF/emulator runner entry | +| A system's official media, booted | the disk-image asset class (`-disk-image`): official media assembled into a bootable image, serial console, a marker protocol that separates the program's output from the machine's noise, exit codes carried through | the generic emulator harness (overlay isolation, expect/send primitives, the marker protocol); the naming and attribution conventions | +| Freestanding and bare-metal targets | the freestanding rows; a C-only, no-module path | the conformance hook-ups | +| Any corrective republish | the revision channel | nothing | + +The one boundary this architecture states out loud: the **semantic plane** — +a process model, a terminal — is the program's own. The infrastructure +answers that a target can be built, started and judged; it does not port the +program. + +## 5. Naming (index side) + +- Generic assets and toolchains carry no prefix; a tool whose contract is a + build-system session carries that system's prefix. +- A bootable-image asset is named `-disk-image` — the thing, not a + mechanism, not a format; the asset's file names carry the format. +- Official media is attributed to its publisher in the recipe; repacked + bytes are provenance-recorded, double-mirrored, sha-identical. + +These are conventions of the index, not engine vocabulary; the engine knows +none of the names. diff --git a/.agents/docs/README.md b/.agents/docs/README.md index f7cd9cdd8..4f08c0b63 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -18,12 +18,16 @@ superseded_by: 2026-09-07-....md # when status is superseded --- ``` -358 records. +360 records. ## By subject Records that declare one. Everything else is listed by date below. +### architecture + +- [The three-plane platform architecture: mcpp × openkal × xim](2026-11-12-three-plane-platform-architecture-design.md) — superseded by [2026-11-12-ecosystem-architecture-optimization-design.md](2026-11-12-ecosystem-architecture-optimization-design.md) + ### build - [大型工作区的构建空间与复用(mcpp#874)](2026-10-31-workspace-space-and-reuse-design.md) — active @@ -79,6 +83,7 @@ Records that declare one. Everything else is listed by date below. ### ecosystem +- [生态架构优化方案:四个平面的收敛、已合入项的纠正与新系统的准入(mcpp × openkal × xlings)](2026-11-12-ecosystem-architecture-optimization-design.md) — active - [测试与依赖解析补充方案:mcpp、mcpp test、mtest、mcpp-ide](2026-11-10-test-and-resolution-supplement-design.md) — active - [生态顶层方案:#885 收尾之后的工作流、原则与依赖](2026-11-08-ecosystem-top-level-design.md) — active - [#885 收尾补充方案:目标角色、平台事实、系统接口与版本书写](2026-11-08-ecosystem-885-supplement-design.md) — active @@ -188,6 +193,8 @@ Records that declare one. Everything else is listed by date below. ### 2026-11 +- [The three-plane platform architecture: mcpp × openkal × xim](2026-11-12-three-plane-platform-architecture-design.md) — superseded by [2026-11-12-ecosystem-architecture-optimization-design.md](2026-11-12-ecosystem-architecture-optimization-design.md) +- [生态架构优化方案:四个平面的收敛、已合入项的纠正与新系统的准入(mcpp × openkal × xlings)](2026-11-12-ecosystem-architecture-optimization-design.md) — active - [测试与依赖解析补充方案:mcpp、mcpp test、mtest、mcpp-ide](2026-11-10-test-and-resolution-supplement-design.md) — active - [生态方案:跨平台通用基础设施(mcpp + openkal + xim),以一个 C89 工程为验证](2026-11-10-cross-platform-ecosystem-design.md) — active - [目标与提供者:统一的目标模型(结合 mcpp#911)](2026-11-09-target-model-design.md) — active diff --git a/.github/tools/check_e2e_coverage.py b/.github/tools/check_e2e_coverage.py index 077d34521..1f0253179 100644 --- a/.github/tools/check_e2e_coverage.py +++ b/.github/tools/check_e2e_coverage.py @@ -29,10 +29,16 @@ and neither does a longer name that contains it; excused tests/e2e/coverage-exceptions.tsv lists it with the reason no hosted runner can run it; + owed tests/e2e/coverage-debt.tsv lists it: it skips itself on every + host that starts it (a script leaves with 77 after a `SKIP:` line + when it can hold nothing on a machine, and the run reports it as a + skip). The list is a debt, and it only shrinks: each entry names + what a host lacks, and is removed when a host gains it; uncovered none of these. The check fails. -An exception for a test that ran, or for a test that does not exist, also fails -the check, so that the list cannot outlive the reason it records. +An exception or a debt entry for a test that ran, or for a test that does not +exist, also fails the check, so that neither list can outlive the reason it +records. Some claims need a test to pass on particular hosts, not on some host: docs/21 calls a target row `verified` when CI builds and runs its artefact, and the @@ -133,6 +139,7 @@ def main() -> int: workflows = workflow_tokens([p.read_text(encoding="utf-8") for p in sorted((root / ".github" / "workflows").glob("*.yml"))]) exceptions = read_exceptions(root / "tests" / "e2e" / "coverage-exceptions.tsv") + debt = read_exceptions(root / "tests" / "e2e" / "coverage-debt.tsv") required = read_host_requirements(root / "tests" / "e2e" / "host-requirements.tsv") ran: dict[str, set[str]] = defaultdict(set) @@ -165,17 +172,25 @@ def main() -> int: counts["ran"] += 1 if test in exceptions: stale.append(f"{test} is excused but ran on {', '.join(sorted(ran[test]))}") + if test in debt: + stale.append(f"{test} is listed as owed but ran on {', '.join(sorted(ran[test]))}: " + f"remove it from coverage-debt.tsv") elif test in unaffected: counts["unaffected"] += 1 elif named_by_a_workflow(test, workflows): counts["job"] += 1 elif test in exceptions: counts["excused"] += 1 + elif test in debt: + counts["owed"] += 1 else: uncovered.append(f"{test}: {'; '.join(sorted(skipped[test])) or 'in no report'}") for test in exceptions: if test not in tests: stale.append(f"{test} is excused but does not exist") + for test in debt: + if test not in tests: + stale.append(f"{test} is listed as owed but does not exist") unmet = [] for test, hosts in sorted(required.items()): if test not in tests: @@ -188,7 +203,7 @@ def main() -> int: print(f"{len(tests)} tests: {counts['ran']} ran on a shard, {counts['unaffected']} unaffected by " f"the change, {counts['job']} run by a dedicated job, {counts['excused']} excused, " - f"{len(uncovered)} uncovered") + f"{counts['owed']} owed, {len(uncovered)} uncovered") for (host, shard), ms in sorted(shard_ms.items()): print(f" {host} shard {shard}: {ms / 60000:.1f} min of tests") for line in uncovered: diff --git a/.github/tools/check_system_names.py b/.github/tools/check_system_names.py new file mode 100644 index 000000000..e59c48848 --- /dev/null +++ b/.github/tools/check_system_names.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""check_system_names.py -- the engine's sources name no more systems than the baseline. + +WHY THIS EXISTS + +AGENTS.md section 2 states that the engine names no operating system: target +rows, providers and systems are data (modules/toolchain-model/data/builtin.toml, +SPEC-016), and code that is specific to one host lives in modules/platform/. +A review cannot hold that sentence: a branch on `"freebsd"` reads as an +ordinary line, and several entered the tree beside the tables that were meant +to replace them (design 2026-11-12, cause R1). This check counts instead. + +WHAT IS COUNTED + +Each word of the vocabulary below, in the engine's sources -- `src/` and +`modules/*/src/`, without `modules/platform/` and without tests and data -- +in the two forms a branch on a system takes: the word as a string literal +(`"freebsd"`), and the host predicates of mcpplibs.os (`is_linux`, +`is_windows`, `is_macos`). Comments are not counted. + +THE RATCHET + +`system-names.baseline` records one number per word. More occurrences than the +baseline fail: the new line states a system fact, which belongs in a row, in +the system table or in a provider. Fewer occurrences fail as well, with the +line to write: the baseline follows the code down, so the count never returns. + + check_system_names.py compare with the baseline + check_system_names.py --update rewrite the baseline to the counts found + check_system_names.py --list W print every counted line of word W +""" + +import pathlib +import re +import sys + +ROOT = pathlib.Path(__file__).resolve().parents[2] +BASELINE = pathlib.Path(__file__).with_name("system-names.baseline") + +LITERALS = ["linux", "macos", "windows", "freebsd", "android", "emscripten", "ios", + "darwin", "netbsd", "openbsd", "haiku", "dos", "uefi"] +PREDICATES = ["is_linux", "is_windows", "is_macos"] + +SOURCE_SUFFIXES = {".cppm", ".cpp", ".mcpp"} + + +def sources(): + for base in (ROOT / "src", ROOT / "modules"): + for path in sorted(base.rglob("*")): + if path.suffix not in SOURCE_SUFFIXES or not path.is_file(): + continue + rel = path.relative_to(ROOT).as_posix() + if rel.startswith("modules/platform/"): + continue + parts = rel.split("/") + if "tests" in parts or "target" in parts or "data" in parts: + continue + yield rel, path + + +def code_of(line, state): + """The line without its comments. `state["block"]` carries a /* */ across lines.""" + out = [] + i = 0 + in_string = False + while i < len(line): + if state["block"]: + end = line.find("*/", i) + if end < 0: + return "".join(out) + state["block"] = False + i = end + 2 + continue + ch = line[i] + if in_string: + out.append(ch) + if ch == "\\" and i + 1 < len(line): + out.append(line[i + 1]) + i += 2 + continue + if ch == '"': + in_string = False + i += 1 + continue + if ch == '"': + in_string = True + out.append(ch) + i += 1 + continue + if line.startswith("//", i): + break + if line.startswith("/*", i): + state["block"] = True + i += 2 + continue + out.append(ch) + i += 1 + return "".join(out) + + +def count(): + patterns = {w: re.compile(r'"%s"' % re.escape(w)) for w in LITERALS} + patterns.update({w: re.compile(r"\b%s\b" % w) for w in PREDICATES}) + counts = {w: 0 for w in patterns} + lines = {w: [] for w in patterns} + for rel, path in sources(): + state = {"block": False} + for n, raw in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1): + code = code_of(raw, state) + if not code.strip(): + continue + for word, pat in patterns.items(): + hits = len(pat.findall(code)) + if hits: + counts[word] += hits + lines[word].append(f"{rel}:{n}: {raw.strip()}") + return counts, lines + + +def read_baseline(): + base = {} + if not BASELINE.is_file(): + return base + for line in BASELINE.read_text().splitlines(): + line = line.split("#", 1)[0].strip() + if not line: + continue + word, number = line.split() + base[word] = int(number) + return base + + +def write_baseline(counts): + text = ("# One number per word: its occurrences in the engine's sources\n" + "# (.github/tools/check_system_names.py). The numbers only fall.\n") + for word in LITERALS + PREDICATES: + text += f"{word} {counts[word]}\n" + BASELINE.write_text(text) + + +def main(argv): + counts, lines = count() + if len(argv) == 2 and argv[0] == "--list": + print("\n".join(lines.get(argv[1], []))) + return 0 + if argv == ["--update"]: + write_baseline(counts) + print(f"wrote {BASELINE.relative_to(ROOT)}") + return 0 + base = read_baseline() + more, fewer = [], [] + for word in LITERALS + PREDICATES: + was, now = base.get(word, 0), counts[word] + if now > was: + more.append(f" {word}: {now} occurrences, the baseline is {was}") + elif now < was: + fewer.append(f" {word}: {now} occurrences, the baseline is {was}") + if more: + print("the engine's sources name a system more often than the baseline:") + print("\n".join(more)) + print("A fact of a system is data: a key of its row, of the system table or of a provider\n" + "(modules/toolchain-model/data/builtin.toml, SPEC-016), read where it was branched on.\n" + "Code specific to one host lives in modules/platform/.\n" + "`check_system_names.py --list ` prints the counted lines.") + if fewer: + print("the engine's sources name a system less often than the baseline; record it:") + print("\n".join(fewer)) + print(" python3 .github/tools/check_system_names.py --update") + return 1 if more or fewer else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/.github/tools/system-names.baseline b/.github/tools/system-names.baseline new file mode 100644 index 000000000..106788fcd --- /dev/null +++ b/.github/tools/system-names.baseline @@ -0,0 +1,18 @@ +# One number per word: its occurrences in the engine's sources +# (.github/tools/check_system_names.py). The numbers only fall. +linux 66 +macos 54 +windows 81 +freebsd 2 +android 19 +emscripten 21 +ios 25 +darwin 10 +netbsd 1 +openbsd 0 +haiku 0 +dos 0 +uefi 0 +is_linux 30 +is_windows 130 +is_macos 32 diff --git a/.github/versions.env b/.github/versions.env index f670c4079..4b2a246c1 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,5 +1,5 @@ # The engines CI pins, read by the composite actions (.github/actions/mtest). # mtest: an xlings package (xim:mtest) whose sha256 the index states; the release # bundle's sha256 checks the fallback download. -MTEST_VERSION=0.5.2 -MTEST_SHA256=be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6 +MTEST_VERSION=0.6.1 +MTEST_SHA256=d6caae62095dfc1a3977ce384fb039bb6b97706eaf18e8203dbacb10aa5df9a4 diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 926c41840..cff74a76f 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -24,6 +24,13 @@ name: ci-linux on: workflow_call: + inputs: + # The engine's own costs are measured where the measurement is kept: a + # push to main, a run asked for them, a pull request labelled `ci:bench` + # (design 2026-11-12 §5.11). Every other run leaves them out. + bench: + type: boolean + default: false env: # MCPP_HOME pinned so the cache keys below restore into the same path @@ -100,6 +107,7 @@ jobs: # Bench cases; design 2026-11-10 §16): this commit's mcpp timed on a small # fixture, kept in bench/target/mtest beside the unit runs. - name: The engine's costs (bench, mtest Bench) + if: inputs.bench run: | cd bench && MCPP="$MCPP_FRESH" "$MCPP_FRESH" test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bdd56a2e..fb0315174 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,11 @@ on: pull_request: branches: [ main ] workflow_dispatch: + inputs: + bench: + description: "measure the engine's own costs (bench/tests)" + type: boolean + default: false # A superseded pull-request run is cancelled. Every push to main runs to the # end, in a group of its own commit: its build jobs are the only writers of the @@ -102,6 +107,9 @@ jobs: - name: Check modules/ wiring run: bash .github/tools/check_modules_wiring.sh + - name: Check that the engine's sources name no more systems than the baseline + run: python3 .github/tools/check_system_names.py + - name: Check that headers and conditional directives appear only in the system lock files run: bash .github/tools/check_include_regions.sh @@ -210,6 +218,10 @@ jobs: linux: needs: build-linux uses: ./.github/workflows/ci-linux.yml + with: + # A pull request's label is read when a push starts its run; a run + # started by hand states it as an input. + bench: ${{ github.event_name == 'push' || inputs.bench == true || contains(github.event.pull_request.labels.*.name, 'ci:bench') }} linux-e2e: needs: build-linux diff --git a/AGENTS.md b/AGENTS.md index f8743c12b..c12f83d13 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,7 +77,9 @@ MCPP= bash tests/e2e/_.sh - An e2e script is named by the behaviour it asserts, states on line 2 the capabilities it requires (`# requires: llvm wine`), opens with the design or issue it verifies and its criteria, and uses - an isolated home. Every e2e script runs on some host or is listed in + an isolated home. A script that can hold nothing on a machine prints `SKIP: ` and exits + 77: the run reports a skip, never a pass. A script that skips itself on every host is listed in + `tests/e2e/coverage-debt.tsv`, which only shrinks. Every e2e script runs on some host or is listed in `tests/e2e/coverage-exceptions.tsv`. The scripts run as mtest programs ([`tests/e2e/mtest.toml`](tests/e2e/mtest.toml)). - A subsystem's unit tests live in `modules//tests/` and build against that package alone; diff --git a/CHANGELOG.md b/CHANGELOG.md index b0ca51646..51a5ae7e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,93 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.11.12.1] - 2026-11-12 + +### Added + +- `aarch64-linux-gnu` has a cross arrangement: the provider `glibc-sysroot` + serves it on linux-x86_64 from the base package + `xim:aarch64-linux-gnu-sysroot` (glibc, the kernel headers, libc++ with its + std module, compiler-rt), with llvm. A C program, a C++ program and one that + imports std build for it, dynamically and statically. The entry stands at + priority 40, below the musl entries, so the request `aarch64-linux` is + answered as before on every host and the whole identity reaches the new + entry; the native arrangement on an aarch64 host is unchanged. +- A provider states how its artifacts are started: `runner = { cmd, hosts, + requires }` on a provider entry is the lowest tier of the runner statement, + under the platform's, a build program's, the machine's and the project's. + `{}` is the artifact, `{sysroot}` the arrangement's base, `{pkg:}` the + root of the required package of that name; the packages of `requires` are + installed on the first `mcpp run` or `mcpp test`, never by a build. + `mcpp run --target aarch64-linux-gnu` on linux-x86_64 runs the artifact + under `qemu-aarch64-static -L {sysroot}` with nothing declared. +- The system table: `[[platform.system]] name, uname` states the facts of a + system that no single row carries, in the built-in data and in a platform + package alike. A file that carries one program per system selects by + `uname`; FreeBSD joins it through the table. +- Provider keys `tier` (the tier of one arrangement, where it differs from its + row's) and `system-headers` (the package whose headers state the system's + own interfaces). Row keys `loader` and `execution` are read on built-in rows + as on rows a platform defines. +- A platform's runner states `built-as = { "-" = "" }`: + the identity its runner program for that host pair is built as. A package + that carries its runner builds it so for a system the engine holds no + convention for; `mcpp pack --systems all` takes every system of the table + and writes the pairs no program is built for as not covered. +- `longjmp` joins `threads` and `exceptions` as a member of + `[target..abi]`: how `sigsetjmp` and `siglongjmp` cross the + WebAssembly boundary on an Emscripten row. `"wasm"` renders + `-sSUPPORT_LONGJMP=wasm` on every C and C++ translation unit and on the + link; `"js"` states the JavaScript model the same way. Unstated, the model + follows `exceptions`, as the SDK's default does. A build that states one + model for `exceptions` and the other for `longjmp` is refused with both + statements, and the member is refused on a row that is not Emscripten's. +- A Windows row whose system the graph supplies compiles its resources: its + provider names the package of the system's headers (`system-headers`), read + where the toolchain states no header source. A row that states its own (the + MSVC SDK, a paired payload) resolves nothing and installs nothing. +- `.github/tools/check_system_names.py` holds the number of times the engine's + sources name a system. The count is compared with a baseline that only + falls. +- `tests/unit/test_link_line_golden.cpp` states the C-library group of the + link line for each combination of compiler family, C library supplier and + machine. + +### Changed + +- An artifact of another system is not started when nothing states how it + runs. `mcpp run` and `mcpp test` answer "built, not run" before any process + is spawned, for built-in rows as for rows a platform defines, and name the + `[target.]` runner declaration; the exit status is 2. The kernel of + this machine gives another system's call numbers its own meaning, so a + launch was not an observation. `--no-runner` states that the triple is + native here and is believed. An artifact of this system and another + architecture is launched as before. A host that runs another system's + programs through a registered interpreter passes `--no-runner` or declares + the runner. +- A dynamic artifact that this machine does not load names the row's `loader` + as its program interpreter and carries no RUNPATH of this machine. A native + dynamic artifact names the base's own loader, as before. +- A base package has one reading: a sysroot whose root carries a manifest is + rendered as a sysroot whatever its directories are called. The layout is + probed only for a directory without a manifest. +- Whether this host serves a target from a payload is answered by the + providers' `hosts`, the data the selection reads; the per-system conditions + of `host_can_serve` are removed. +- The messages that print a runner declaration print its shape and no + program. +- Android's rows state `execution = "android"`: the kernel is Linux and the + system is not. +- The openkal targets' platform is openkal-platform 0.27.0 (from 0.26.0): the + same version group, with the runner's `built-as` statement for FreeBSD. + +### Fixed + +- A base package's compiler run-time directory reaches the C link as well as + the C++ one. +- The loader name of a row is read from the row; the per-architecture table + in the link model is removed. + ## [2026.11.10.2] - 2026-11-10 ### Added diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index 1df48eea8..4212d1238 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -857,6 +857,15 @@ names no `windows-aarch64`, so `aarch64-windows-gnu`, a row this mcpp builds, gets no runner from it). An image built on a Windows host is not the bytes of one built elsewhere (#850): pack trees on Linux or macOS. +**What `all` names is the system table (mcpp 2026.11.12.1+).** Every system +the table states (`[[platform.system]]`, built-in or a platform's) and every +system a platform defines a row of for the architecture. A pair no program is +built for is written as not covered, with the reason, and the pack succeeds for +the rest. The program of a system the engine holds no convention for is built +as the identity the platform's runner states for the host pair +(`runner.built-as`, [35](35-platforms.md)); a pack without `--systems` carries +the runner of the packing machine's system alone. + **Where it runs.** The pack writes `runs_on` into `mcpp-package.json` and says it after `Packed`: `self`, the `-` pairs the package runs on with what it carries, and `with.`, the pairs it runs on where the machine diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index 0020b6710..e6710b23e 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -514,12 +514,19 @@ not from `linux-aarch64`, while `aarch64-linux-gnu` is served natively by `linux-aarch64`. Collapsing them to `linux` would let one overwrite the other's rows. +`aarch64-linux-gnu` has a second, cross arrangement (2026.11.12.1+): on +`linux-x86_64` the provider `glibc-sysroot` serves it from a base package +([22](22-target-side.md)). It stands below the musl entries, so `--target +aarch64-linux` is still answered by `aarch64-linux-musl` there and the whole +identity selects the glibc cross; `mcpp run` starts its artifacts through the +provider's runner ([41](41-devices.md)). + ### Build hosts and the targets they serve | target | tier | pin | linux-x86_64 | linux-aarch64 | macos-arm64 | windows-x86_64 | |---|---|---|---|---|---|---| | `x86_64-linux-gnu` | verified | — | payload | — | — | — | -| `aarch64-linux-gnu` | preview | `llvm@23.1.3` | — | payload | — | — | +| `aarch64-linux-gnu` | preview | `llvm@23.1.3` | base package | payload | — | — | | `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 | @@ -574,7 +581,7 @@ vocabulary, nothing wired yet. | `x86_64-windows-gnu` | Linux and Windows by the gcc payload; every host by the MinGW-w64 base package | one identity, host-split at the distribution layer; the `mingw-llvm` provider serves it with clang and `xim:mingw-w64-sysroot` (llvm-mingw's target side: MinGW-w64 on UCRT, libc++, compiler-rt) where no gcc payload exists | | `aarch64-windows-gnu` | every host | the MinGW-w64 base package alone; no gcc payload targets it | | `*-freebsd` | every host, through a platform | the rows are built-in; a platform names each identity's FreeBSD base package (`xim:x86_64-freebsd-sysroot`, `xim:aarch64-freebsd-sysroot`) and is selected with `[target.] platform`, or by any selector the row meets (`[target.freebsd]`, `cfg(os = "freebsd")`); another platform package's provider does not compete with the chosen platform | -| `*-freebsd-musl` | every host, through the graph | openkal-platform's hosted part (0.26.0): openkal-musl over openkal-freebsd standalone, one static program; a request of `-freebsd` with no platform selected for it takes this identity (2026.11.10.2+), as `aarch64-macos` takes the musl identity on a host that is not a Mac | +| `*-freebsd-musl` | every host, through the graph | openkal-platform's hosted part (0.27.0): openkal-musl over openkal-freebsd standalone, one static program; a request of `-freebsd` with no platform selected for it takes this identity (2026.11.10.2+), as `aarch64-macos` takes the musl identity on a host that is not a Mac | | `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 | diff --git a/docs/22-target-side.md b/docs/22-target-side.md index ae6f7e205..c19ad0e48 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -1320,11 +1320,25 @@ A base package is the sysroot of one identity. A platform gives each identity it through each provider's `sysroot` ([35](35-platforms.md)); a system whose driver tells its architectures apart inside one root (MinGW-w64) is served by one package. +**A base package has one reading (2026.11.12.1+).** Its manifest states that the package root +is the sysroot, and it is rendered as one (`--sysroot=`) whatever its directories are +called; the directory layout is probed only for a directory that carries no manifest. Where +this machine does not load the artifact — the base is another architecture's or another +system's — the link takes nothing of the toolchain payload's own run-time directories, a dynamic +artifact names the row's `loader` as its program interpreter (`/lib/ld-linux-aarch64.so.1`), and +no directory of this machine enters its RUNPATH. Such an artifact runs through a runner, which +is given the base as `{sysroot}` ([41](41-devices.md)). A native dynamic artifact names the +base's own loader, where it stands. + The index carries `xim:x86_64-freebsd-sysroot` and `xim:aarch64-freebsd-sysroot` (FreeBSD's base system, with libc++'s module sources) and `xim:mingw-w64-sysroot` (llvm-mingw's target side: MinGW-w64 on UCRT, libc++, libunwind and compiler-rt, for x86_64 and aarch64). On a `*-windows-gnu` row the latter is clang's sysroot; the `mingw-llvm` provider names it, so every -host builds the GNU-ABI Windows rows ([21](21-the-target-triple.md)). +host builds the GNU-ABI Windows rows ([21](21-the-target-triple.md)). It also carries +`xim:aarch64-linux-gnu-sysroot` (glibc for arm64 with the kernel headers, libc++ with its module +sources, and compiler-rt), which the `glibc-sysroot` provider names: on linux-x86_64, +`--target aarch64-linux-gnu` builds C, C++ and `import std` programs over it, dynamically or +statically (2026.11.12.1+). A build program can ask which C library **payload** supplies the sysroot: `mcpp::target_libc()` returns that package's name and @@ -1363,8 +1377,9 @@ package needs. |---|---|---|---| | `threads` | boolean | `-pthread` on a target that is neither PE nor freestanding; nothing on PE and on freestanding targets | the standard library module prebuild, the dependency scan, every C and C++ translation unit of every package, and the link | | `exceptions` *(mcpp 2026.9.12.3+)* | boolean, or `"wasm"` / `"js"` *(2026.10.30.1+)* | on `os = "emscripten"` only, on the compile line through the dialect flags and on the link: `"wasm"` is `-fwasm-exceptions`, WebAssembly's own exception handling; `"js"` and `true` are `-fexceptions`, the JavaScript-based model. Nothing on every other target, where exceptions are already the default | the same set `threads` reaches | +| `longjmp` *(mcpp 2026.11.12.1+)* | `"wasm"` or `"js"` | on `os = "emscripten"` only, on the compile line of every C and C++ translation unit and on the link: `"wasm"` is `-sSUPPORT_LONGJMP=wasm`, WebAssembly's own stack switching; `"js"` is `-sSUPPORT_LONGJMP=emscripten`, the JavaScript model and the toolchain's default. Declared on any other row, it is refused. An absent member renders nothing | the same set `threads` reaches | -Both members enter the dependency cache key through the dialect flags, so a +All three members enter the dependency cache key through the flags they render, so a dependency built without one is never reused by a build with it, and one built with one exception model is never reused by a build with the other. An unknown member, and a value outside the table, are refused naming the values. @@ -1387,6 +1402,23 @@ enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.) ``` +**`longjmp = "wasm"` is what makes C's `sigsetjmp`/`siglongjmp` work under +Emscripten.** With the toolchain's default JavaScript model the pair compiles +into a stack copy, and a non-local exit over two call levels reads past the +module's stack and dies at run time. With `-sSUPPORT_LONGJMP=wasm` on both the +compiles and the link, the pair round-trips. A program whose non-local exits +run deep also wants a bigger stack; that is a project knob of its own +(`ldflags = ["-sSTACK_SIZE=..."]`), not an ABI fact, so the engine does not +render it. + +**`longjmp` and `exceptions` name one mechanism.** WebAssembly's exception +handling carries both the throw and the long jump, and the JavaScript model +carries both. An unstated `longjmp` follows `exceptions`, as the SDK's own +default does. A build that states one model for `exceptions` and the other for +`longjmp` is refused with both statements, before any compile. A declared +`longjmp` is part of the configuration's key; a project that does not state it +keeps the key it had. + **Only the root manifest decides.** The switch belongs to the artefact, and the root is the only package that builds one. A dependency that writes `[target..abi]` is reported (`abi/dependency-table`) and changes diff --git a/docs/35-platforms.md b/docs/35-platforms.md index 131b4ca17..adba6f9ba 100644 --- a/docs/35-platforms.md +++ b/docs/35-platforms.md @@ -203,7 +203,7 @@ manifest, ```toml [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.26.0" # a release, or a series: "openkal-platform@^0.26" +platform = "openkal-platform@0.27.0" # a release, or a series: "openkal-platform@^0.27" ``` and a build that fails while the default is in use and the synced index holds a @@ -355,6 +355,53 @@ driver = "compiler" `driver` is `"compiler"` gives them to no program, and the build says so, naming `[platform.build] ldflags`, whose words reach the compiler's link. +**A provider states how its artifacts start, and what a system is called +(2026.11.12.1+).** Three keys of a provider entry and one table: + +```toml +[[platform.provider]] +serves = ["x86_64-haiku"] +tier = "preview" # of this arrangement; unset: the row's +system-headers = "xim:haiku-headers@1.0" # the system's own interfaces, for a step that compiles against them +runner = { cmd = ["dirun", "--profile", "{pkg:haiku-disk-image}/dirun.toml", "--", "{}"], + hosts = ["linux-x86_64", "macos-aarch64"], + requires = ["xim:dirun@0.2.0", "xim:haiku-disk-image@1.0"] } + +[[platform.system]] +name = "haiku" +uname = "Haiku" # what `uname -s` answers there +``` + +A provider's `runner` is the lowest tier of the runner statement: the +platform's `runner`, a build program's, this machine's `config.toml`, the +project's `[target.] runner` and `MCPP_TARGET__RUNNER` each +replace it. `hosts` are the machines the runner exists on (unset: any). In the +template `{}` is the artifact, `{sysroot}` the base the artifact was linked +against, and `{pkg:}` the root of the package of that name among +`requires`; the template's program is looked for in the required packages +before `PATH`. The packages of `requires` are installed by the first `mcpp +run` or `mcpp test` that needs them, never by a build. + +One provider entry states one arrangement of payloads: a native arrangement and +a cross one are two entries, each with its `hosts`, base and priority. A new +entry does not change the answer of a request that was answered before it, so +it enters below the entries that answer `-` today +([SPEC-016 §4](specs/targets-and-providers.md)). + +A system that states a `uname` joins a file that carries one program per +system (`mcpp pack --pkg-format single-file`); the system that states none +leads the file as its PE. A platform whose runner is built for several systems +states the identity of each: + +```toml +[platform] +runner = { cmd = ["okrun", "{}"], hosts = ["linux-x86_64", "freebsd-x86_64"], + built-as = { "freebsd-x86_64" = "x86_64-freebsd-musl" } } +``` + +`mcpp pack --systems all` takes every system of the table, and writes a pair no +program is built for as not covered. + `mcpp new --template platform` writes a platform package defining `-`. Reading a platform's manifest checks its statement against the rows: a definition of a built-in row, or a row defined twice, is an error diff --git a/docs/41-devices.md b/docs/41-devices.md index ab52f6be0..a8f3829dc 100644 --- a/docs/41-devices.md +++ b/docs/41-devices.md @@ -159,10 +159,22 @@ freebsd-vm = { git = "https://example.org/freebsd-vm.git", tag = "v1" } The tool is built for this machine and started with the argv and the environment above: it transfers the artifact and the files `MCPP_RUNTIME_FILES` lists, runs the argv, streams the program's output, and exits with the -program's status. An artifact of a row a platform defines whose `execution` is -another system than this machine's, with no runner and no execution host -(`run_under`), is **built, not run**: `mcpp run` refuses it naming the keys that -state a runner, and `mcpp test` reports its tests as not run. +program's status. An artifact whose row's `execution` is another system than +this machine's, with no runner and no execution host (`run_under`), is +**built, not run**: `mcpp run` refuses it before anything is started, naming +the keys that state a runner, and `mcpp test` reports its tests as not run. +The rule is one for every row, built-in or defined by a platform +(2026.11.12.1+; before, a built-in row's artifact was started and the kernel's +answer reported). This machine's kernel does not read the system a program +names, and gives that system's call numbers its own meaning, so starting it +observes nothing. `--no-runner` states that the triple is native here, for a +host that runs such programs through a registered interpreter. An artifact of +this system and another architecture is launched as before. + +A provider may state the runner of its arrangement, so a target runs with +nothing declared (docs/35): `mcpp run --target aarch64-linux-gnu` on +linux-x86_64 installs the user-mode emulator on its first run and starts the +artifact under it, with the arrangement's base as the emulator's root. ## Running a distributable diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index 95f514930..1c4344c87 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -552,7 +552,7 @@ 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`. A target row also carries `platform` (2026.10.27.1+): the platform package the -target table selects for it (`mcpplibs.openkal-platform@0.26.0` from 2026.11.10.2, `@0.25.0` in 2026.11.10.1, `@0.24.4` from 2026.11.8.1, `@0.24.3` in 2026.11.7.2; the series `^0.24` in 2026.11.7.1), or `null`. +target table selects for it (`mcpplibs.openkal-platform@0.27.0` from 2026.11.12.1, `@0.26.0` in 2026.11.10.2, `@0.25.0` in 2026.11.10.1, `@0.24.4` from 2026.11.8.1, `@0.24.3` in 2026.11.7.2; the series `^0.24` in 2026.11.7.1), or `null`. **`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 diff --git a/docs/specs/packaging.md b/docs/specs/packaging.md index 524d8c17c..436effc2e 100644 --- a/docs/specs/packaging.md +++ b/docs/specs/packaging.md @@ -4,8 +4,8 @@ |---|---| | 规范编号 | SPEC-014 | | 标题 | 打包与分发:链接、闭包、容器三个维度与构建腿,腿的合并,暂存树与包元数据,运行覆盖、单文件、容器列表、签名、压缩、合并与分发描述 | -| 状态 | 评审中 v2.2 | -| 版本 | 2.1 | +| 状态 | 评审中 v2.3 | +| 版本 | 2.3 | | 最后修改 | 2026-11-10 | | 对应实现 | mcpp >= 2026.10.31.1;v2.2 条款 mcpp >= 2026.11.10.1 | | 相关设计文档 | `.agents/docs/2026-10-30-ecosystem-part2-ide-and-language-service-design.md`(§19.6)、`.agents/docs/2026-10-29-ecosystem-target-sets-packaging-and-editors-design.md`(§4、§13)、`.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md`(§2–§7、§14)、`.agents/docs/2026-10-28-platforms-design.md`(§4.7)、`.agents/docs/2026-10-29-openkal-run-test-pack-design.md`(§2.2、§2.6) | @@ -225,14 +225,18 @@ openkal 目标上 `static` 为一个 `.oke`、无 `.okso`,`shared` 为 `.oke` ### 4.4 `--systems` -`--systems` 的每个词为 ``(`linux`、`macos`、`windows`,取腿的全部架构)、`-`(只取该架构的腿)或 -`all`(三个系统);可重复,也可逗号分隔;缺省为本机系统;未给出时取 `[pack] systems`。对 `self-contained` 的树,为 +`--systems` 的每个词为 ``(系统表中的系统,取腿的全部架构)、`-`(只取该架构的腿)或 +`all`;可重复,也可逗号分隔;缺省为本机系统;未给出时取 `[pack] systems`。`all` 为系统表陈述的全部系统 +(SPEC-016 §2.3,内置的与平台的)加上平台为该架构定义了行的系统;其中没有程序可构建的对逐个写为未覆盖(§4.7), +不被略去也不使打包失败(**mcpp >= 2026.11.12.1**;此前为 `linux`、`macos`、`windows` 三个系统)。对 `self-contained` 的树,为 每个系统 × 腿的每个架构构建平台的载体(不携带任何内容,即运行器形式,SPEC-013 §11)并放入 `libexec//-/`;对映像的单文件,为每条腿与每个系统写一个文件(§5.2)。每个系统的程序在其行 (`-linux-musl`、`-macos`、`-windows-gnu`)上构建;平台的托管安排(SPEC-013 §5.4)服务该行时以它 构建,于是同一程序在任何打包的机器上相同,不需要该系统的 SDK 与 C 运行时;Linux 的程序为静态。托管安排不服务该行时 按惯常行构建,与本机系统和架构相同时取本机的行。本 mcpp 不构建程序的行(目标表标为 planned 或未列出)不放入包,并 -写出该系统与行。 +写出该系统与行。引擎只对 `linux`、`macos`、`windows` 持有"该系统的程序是哪一行"的约定;其余系统的程序是平台运行器 +为该宿主对陈述的身份(`runner.built-as`,SPEC-013 §10),无此陈述、平台也未为它定义行的系统没有程序 +(**mcpp >= 2026.11.12.1**)。 ### 4.5 `mcpp-package.json` @@ -486,3 +490,4 @@ openkal 目标的单文件:载体携带一条腿的映像本身(映像不引用 | 2.0 | 2026-10-30 | mcpp 2026.10.30.1(设计 2026-10-29 §4、§13):§1.4 腿只是命令命名的目标(目标集合按 SPEC-015 展开),腿按目标的种类与平台合并,原生程序每腿一个包,`-o <目录>/`;删除通用包与 `[platform] universal`、`[pack] universal`、`--no-universal` 的作用(迁移见 §2、docs/51);§1.2、§4.2 声明运行器的平台的映像只有一种布局(树),M3 只属于单文件,`--mode static` 为静态树;§4.3 Windows 启动器为引擎的程序,不再生成 `.cmd`,启动器为派生文件;§4.4 `--systems` 接受 ``、`-`、`all` 与 `[pack] systems`;§4.6 合并重新生成启动器、合并 `runs_on`、树以平台名命名;§4.7 运行覆盖 `runs_on` 与 `Runs on`、未覆盖的摘要,空覆盖被拒绝;§5.2 单文件覆盖腿 × 系统;§5.4 容器列表;§7 签名含 Windows 启动器;§8 条目按 `runs_on` 描述;§9 `pkgFormats`、`runsOn`;判据 3、4、7、9 改写,判据 10–13(e2e 983–987)。 | | 2.1 | 2026-10-31 | mcpp 2026.10.31.1(设计 2026-10-30 part 2 §19.6):§5.3 载体模型 `row` 与 `polyglot`,`--pkg-format polyglot` 的范围、布局、启动、缓存、签名与其他选项;§1.3、§1.5、§5.4、§7、§8 相应条目;`mcpp run --pkg-format` 直接运行 `single-file` 与 `polyglot`、拒绝 `tar`、`zip`、`dir`;§4.3 macOS 上带 quarantine 标记的运行器;判据 14(e2e 992)。 | | 2.2 | 2026-11-10 | mcpp 2026.11.10.1(设计 2026-11-10 E7,mcpp#911 N8):§1.2 另一系统的产物从文件读取闭包,不运行本机加载器;`--mode static` 链接全静态程序并在打包前核对。 | +| 2.3 | 2026-11-12 | mcpp 2026.11.12.1(设计 2026-11-12 C6、C7):§4.4 `--systems all` 取系统表,无程序可构建的对写为未覆盖;三个系统之外的程序按 `runner.built-as` 构建;单文件按系统表的 `uname` 选择程序(SPEC-016 §2.3)。 | diff --git a/docs/specs/platforms.md b/docs/specs/platforms.md index 58db714ad..e5470ddbb 100644 --- a/docs/specs/platforms.md +++ b/docs/specs/platforms.md @@ -4,9 +4,9 @@ |---|---| | 规范编号 | SPEC-013 | | 标题 | 平台:一类目标的可复用配置,其声明、选择、合并、对依赖图的补全、链接与封装、运行器与载体 | -| 状态 | 评审中 v1.7 | -| 版本 | 1.5 | -| 最后修改 | 2026-11-10 | +| 状态 | 评审中 v1.8 | +| 版本 | 1.8 | +| 最后修改 | 2026-11-12 | | 对应实现 | mcpp >= 2026.10.27.1;§10 的执行宿主与 §11 的载体架构 mcpp >= 2026.10.29.1;`runner.hosts`、载体列表、`descriptor_keys` 与 `universal` 的弃用 mcpp >= 2026.10.30.1;§7.5 版本组参与解析、§14 平台系列与 §15.1 系列的锁 mcpp >= 2026.11.7.1;§14 内置平台为确切版本与提示 mcpp >= 2026.11.7.2;v1.7 条款 mcpp >= 2026.11.10.1;逐条标注 | | 相关设计文档 | `.agents/docs/2026-10-28-platforms-design.md`、`.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md`、`.agents/docs/2026-10-29-openkal-run-test-pack-design.md` | | 相关 issue | mcpp#846、mcpp#847、mcpp#848 | @@ -312,7 +312,13 @@ plugin = "build.mcpp" | `cmd` | argv,非空;`{}` 为产物 | | `host` | `"same-arch"`:产物只在其架构的机器上运行;`"any"`:运行器可以从任何机器到达产物(模拟器、探针) | | `exclusive` | 为真时该目标的运行不重叠,`mcpp test` 串行运行 | -| `hosts` | 运行器程序存在的 `-`(`os` 为 `linux`、`macos`、`windows`):需要该运行器的包能在哪里运行(SPEC-014 §4.4 的 `runs_on.with`)。`host = "any"` 时与产物的架构无关。**mcpp >= 2026.10.30.1** | +| `hosts` | 运行器程序存在的 `-`(`os` 为系统表中的系统,SPEC-016 §2.3):需要该运行器的包能在哪里运行(SPEC-014 §4.4 的 `runs_on.with`)。`host = "any"` 时与产物的架构无关。**mcpp >= 2026.10.30.1** | +| `requires` | 运行器从哪些包安装(`xim:<名>@<版本>`)。第一次 `mcpp run` 或 `mcpp test` 时按工具链的同一方式安装;构建不安装。模板的程序先在这些包中查找,再查 PATH。替换 `cmd` 的陈述同时替换它。**mcpp >= 2026.11.12.1** | +| `built-as` | `{ "-" = "<身份>" }`:为该宿主对构建的运行器程序是哪个身份。自带运行器的包(SPEC-014 §4.4)对引擎不持有约定的系统按它构建。**mcpp >= 2026.11.12.1** | + +模板中 `{}` 为产物,`{sysroot}` 为该目标的基座(SPEC-006),`{pkg:}` 为 `requires` 中该名字的包的根 +(**mcpp >= 2026.11.12.1**)。运行器陈述的最低一级是所选提供者条目的 `runner`(SPEC-016 §6),其上依次为平台、构建程序、 +本机配置、项目与环境变量。 `host = "same-arch"` 而产物的架构不是本机架构时,`mcpp run` 与 `mcpp test` 拒绝运行,消息给出 `[target.] run_under` 的写法;`run_under` 存在或给出 `--no-runner` 时不拒绝。运行器的合并顺序与 §6 @@ -405,7 +411,7 @@ run_under = ["qemu-aarch64"] ## 14. 内置平台与目标表 宿主目标(Linux、macOS、Windows、Android、iOS、Emscripten)的约定留在引擎中,无平台包。目标表有平台列: -`x86_64-openkal`、`aarch64-openkal` 的值为一个确切的平台版本,即当前引擎验证过的版本(mcpp 2026.11.10.2 为 `mcpplibs.openkal-platform@0.26.0`), +`x86_64-openkal`、`aarch64-openkal` 的值为一个确切的平台版本,即当前引擎验证过的版本(mcpp 2026.11.12.1 为 `mcpplibs.openkal-platform@0.27.0`), 也是这两个目标的内置支持;选择来源报告为 `built-in`。项目可以以 `[target.] platform` 选择另一个版本或系列。 锁中记录的版本不满足内置版本时不被采用,报告写出采用的版本与锁中记录的版本。使用内置版本的构建失败、且已同步的索引中 有更新版本时,构建以一条 `tip` 写出两个版本与选择它的写法(mcpp >= 2026.11.7.2;2026.11.7.1 的值为系列 `^0.24`)。**已实现** @@ -548,3 +554,4 @@ openkal-platform 的 `targets` 因此不含 WebAssembly。Web 经 `wasm32-emscri | 1.4 | 2026-11-07 | mcpp 2026.11.7.1(设计 2026-11-07 §4.1.4、§4.2.2 A2/A3):§7.5 版本组参与解析,解析后的检查保留为断言;§3.2 平台引用接受 SemVer 约束;§14 目标表的平台列为系列 `^0.24`,系列取引擎可读的最新版本;§15.1 系列从锁中读回,`mcpp update` 移动它;判据 5 改写、14 新增(e2e 1020、1021)。 | | 1.6 | 2026-11-09 | mcpp 2026.11.9.1(SPEC-016,mcpp#911):§1 `[[platform.define]]` 与 `[[platform.provider]]` 为一般形式,`[platform]`、`[platform.hosted]` 为其简写;§9.2 `driver = "compiler"` 在每一行生效,平台的产物名模式不被构建程序改变;§11 平台定义的系统上的载体与 `--systems`;主机对为任意系统词。 | | 1.7 | 2026-11-10 | mcpp 2026.11.10.1(设计 2026-11-10 E8,mcpp#911 N6):§4 `link.flags` 在 `driver = "compiler"` 时给出警告。 | +| 1.8 | 2026-11-12 | mcpp 2026.11.12.1(设计 2026-11-12 §5.2、§5.5):§10 运行器的 `requires` 与 `built-as`,模板占位 `{sysroot}`、`{pkg:}`,提供者的 `runner` 为陈述的最低一级;`hosts` 的系统取自系统表。 | diff --git a/docs/specs/target-side.md b/docs/specs/target-side.md index 94a5b6ef8..a6db8ce28 100644 --- a/docs/specs/target-side.md +++ b/docs/specs/target-side.md @@ -4,7 +4,7 @@ |---|---| | 规范编号 | SPEC-002 | | 标题 | 目标侧模型与能力声明(`mcpp:` 保留命名空间) | -| 状态 | 评审中 v1.7 | +| 状态 | 评审中 v1.8 | | 最后修改 | 2026-11-10 | | 对应实现 | mcpp >= 2026.8.24.2;§2.2 的对象形式与 §7 mcpp >= 2026.10.26.1 | | 相关设计文档 | `.agents/docs/2026-08-24-target-side-design.md`、`.agents/docs/2026-10-23-target-base-and-execution-host-design.md`、`.agents/docs/2026-10-26-control-contract-runtime-and-ide-ecosystem-design.md` §6 | @@ -115,6 +115,10 @@ mcpp:<层名>[=<实现名>] 在 sysroot 的位置被指名(工程、本机配置、平台或提供者的 `sysroot`)、且根目录带 `mcpp.toml` 的预制目录是**基座包**。 它按普通包读取,不带角色标记: +- **一种读法(mcpp >= 2026.11.12.1)。** 清单陈述包根即 sysroot,所以基座包**必须**按 sysroot 渲染,不论其目录怎样 + 命名;目录布局(`include/features.h` 与 `lib64`,即生态自身的 glibc 包布局)只对不带清单的目录探测。产物不由本机 + 加载时的链接规则见 SPEC-016 §6。 + - `[package] provides` 回答它供给的层(`mcpp:c-abi=…`、`mcpp:c++-abi=…`、`mcpp:compiler-runtime=…`)与 `hosted-standard-library`;这些层的来源为 `prebuilt`,报告附基座包的 `<名>@<版本>`。 - `[build]` 的键与依赖图中供给者的含义相同,路径相对包根,**包根即 sysroot**: @@ -391,3 +395,4 @@ hosted 与否是 C 环境的一个声明:提供 `hosted-standard-library` 的 sy | v1.6 | 2026-11-10 | 随 mcpp 2026.11.10.1:新增 §2.5 基座包(按普通包读取:`provides` 回答层,`[build]` 陈述层,包根即 sysroot;新键 `compiler_runtime_dir`)与 §3.8 层供给者的需求不成环(`layer-cycle`)。 | | v1.7 | 2026-11-10 | 随 mcpp 2026.11.10.2:§3.4 写明段与 C 库名的对应,MSVC 载荷的 `c-abi` 回答 `ucrt`。 | | v1.2 | 2026-10-23 | 随 mcpp 2026.10.24.1:§1.3 `prebuilt` 放宽为被点名、按内容识别的预制目录;§2.4 改为由测量回答、`provides` 声明或核对,标为已实现;规则一增加基座是一个供给者与 c-abi 独占律;规则二增加基座上的编译器运行库与可测量的下限;规则三增加可叠加层;新增 §6 目标侧执行(组合、运行器契约、物理边界)。 | +| v1.8 | 2026-11-12 | 随 mcpp 2026.11.12.1:§2.5 基座包只有一种读法(带清单即包根 sysroot,布局只对无清单的目录探测)。 | diff --git a/docs/specs/targets-and-providers.md b/docs/specs/targets-and-providers.md index 015d7612a..8c1fa9d91 100644 --- a/docs/specs/targets-and-providers.md +++ b/docs/specs/targets-and-providers.md @@ -4,11 +4,11 @@ |---|---| | 规范编号 | SPEC-016 | | 标题 | 目标与提供者:行、提供者、选择、平台定义的行、报告与锁 | -| 状态 | 评审中 v1.1 | -| 版本 | 1.1 | -| 最后修改 | 2026-11-10 | -| 对应实现 | mcpp >= 2026.11.9.1;v1.1 条款 mcpp >= 2026.11.10.1 | -| 相关设计文档 | `.agents/docs/2026-11-09-target-model-design.md`、`.agents/docs/2026-11-10-cross-platform-ecosystem-design.md` | +| 状态 | 评审中 v1.2 | +| 版本 | 1.2 | +| 最后修改 | 2026-11-12 | +| 对应实现 | mcpp >= 2026.11.9.1;v1.1 条款 mcpp >= 2026.11.10.1;v1.2 条款 mcpp >= 2026.11.12.1 | +| 相关设计文档 | `.agents/docs/2026-11-09-target-model-design.md`、`.agents/docs/2026-11-10-cross-platform-ecosystem-design.md`、`.agents/docs/2026-11-12-ecosystem-architecture-optimization-design.md` | | 相关 issue | mcpp#911、mcpp#913 | | 相关规范 | [SPEC-001](package-identity.md)(引用文法)、[SPEC-002](target-side.md)(层与能力)、[SPEC-013](platforms.md)(平台的语句)、[SPEC-014](packaging.md)(打包)、[SPEC-015](target-sets.md)(目标集合) | | 使用文档 | [21 —— 目标三元组](../zh/21-the-target-triple.md)、[35 —— 平台](../zh/35-platforms.md) | @@ -27,6 +27,7 @@ | 请求 | `--target`、`[build] target`、config 的缺省目标中写出的字符串 | | 身份 | 一个行的三元组;决定输出目录、缓存键、`cfg` 求值与锁中的记录 | | 行 | 身份的数据:段、档位、机器描述、对象格式、执行系统等(§2) | +| 系统 | 一个 os 段所指的操作系统;不属于任何单独一行的事实记在系统表(§2.3) | | 提供者 | 一个身份的一份完整供给:工具链、各层、链接、C 环境(§3) | | 选择 | 由请求得到身份与提供者的规则(§4) | @@ -45,6 +46,9 @@ - `march`、`mabi`、`code-model`、`flags`、`link-emulation` 是行的机器描述,引擎原样使用,**不可**被替换;机器不同的目标以 另一行表达(§5)。 - `clang-triple` 是交给 clang 的目标拼写,未写时由段推出;它只改变编译器所见的三元组,不改变身份(v1.1)。 +- `loader` 是该行的动态程序所写的程序解释器:运行它的机器上的路径(`/lib/ld-linux-aarch64.so.1`)。内置行与平台定义的行 + 同一读法;引擎不另持有按架构的加载器表(v1.2)。 +- `execution` 是产物运行的系统,未写时为 os 段。内核相同而系统不同的行**必须**写出它(Android 的行为 `android`)(v1.2)。 ### 2.2 内置行 @@ -54,10 +58,25 @@ - 内置行的 os 与 env 是解析器接受的词,与平台定义的行相同(v1.1)。 - 主流系统的两种身份都是内置数据(v1.1):系统自身 ABI 的行(`x86_64-freebsd`、`aarch64-freebsd`)没有内置提供者,由平台 提供;未选平台时,请求被拒绝并指名 `[target.<身份>] platform`。openkal 的 hosted 行(`x86_64-freebsd-musl`、 - `aarch64-freebsd-musl`)由 openkal 提供者经 openkal-platform 0.26.0 服务(openkal-freebsd 0.1.0 发布后,mcpp >= 2026.11.10.2)。 + `aarch64-freebsd-musl`)由 openkal 提供者经 openkal-platform 服务(0.26.0 起,openkal-freebsd 0.1.0 发布后,mcpp >= 2026.11.10.2;mcpp 2026.11.12.1 钉 0.27.0)。 - 32 位 hosted 行(`i686-linux-musl`、`i686-linux-gnu`、`i686-openkal`)为 planned(v1.1):请求按 planned 拒绝,而非按 未知目标;显式的 `[target.<身份>] toolchain` 可提前启用。 +### 2.3 系统表(v1.2) + +`[[platform.system]]` 陈述一个系统不属于任何单独一行的事实,内置数据与平台包同一 schema。 + +| 键 | 语义 | +|---|---| +| `name` | 该系统各行的 os 段 | +| `uname` | `uname -s` 在该系统上的回答;每个系统一个程序的单文件按它选择程序 | + +- 陈述了 `uname` 的系统可以进入多系统单文件;表中未陈述 `uname` 的系统,其程序是文件开头的 PE;表中没有的系统被拒绝, + 并指名 `[[platform.system]]`。 +- `mcpp pack --systems all` 取表中的全部系统,加上平台定义了行的系统;其中没有程序可构建的对逐个写为未覆盖(SPEC-014 §4.5)。 +- 同一系统陈述两次时取先陈述者;内置数据先于平台。 +- 引擎源码中系统名的出现次数由检查持有(`.github/tools/check_system_names.py` 与其基线),只许减少。 + ## 3. 提供者 | 键 | 语义 | @@ -70,6 +89,9 @@ | `toolchain`、`sysroot`、`linkage`、`layers`、`link` | 同 SPEC-013 §1 | | `hosted` | C 环境是否 hosted | | `execution` | 产物运行的系统 | +| `tier` | 这一安排的档位;未写时为行的档位(v1.2) | +| `system-headers` | 陈述系统自身接口的头文件所在的包;只由对它们编译的步骤读取(资源编译)(v1.2) | +| `runner` | 该安排的产物怎样启动:`{ cmd = [argv], hosts = [...], requires = [...] }`(§6)(v1.2) | - 引擎不含提供者的类别,也不含实现名;所有提供者同一结构,只有数据不同。 - 内置提供者:glibc 载荷、musl-gcc、MinGW-w64(与 gcc、llvm 配对)、`mingw-llvm`(v1.1)、MSVC、Apple SDK、Android NDK、 @@ -80,6 +102,13 @@ driver-internal),在项目与本机都未陈述时,先取所选提供者的 `sysroot`(包,或相对平台清单的 `{ path = "<目录>" }`), 再取平台的 `sysroot`(SPEC-006 §3.9.2 的引擎默认档),按项目陈述的同一规则解析;是基座包时按 SPEC-002 §2.5 读取。 平台由此为每个身份指名自己的基座。 +- **一条条目陈述一种载荷安排(v1.2)。** 原生安排与交叉安排是两条条目,各有自己的 `hosts`、`sysroot`、`linkage` 与 + 优先级;其事实随所在宿主而变的条目**必须**拆开。推论:一个身份在其原生宿主上,写与不写 `--target` 得到同一条链接行。 +- **`glibc-sysroot`(v1.2)。** 以基座包 `xim:aarch64-linux-gnu-sysroot` 与 llvm 服务 `aarch64-linux-gnu` 的交叉安排,在 + `linux-x86_64` 可得,优先级 40,动态链接,档位 preview;缺省运行器为 `qemu-aarch64-static -L {sysroot} {}`。原生安排仍是 + `glibc`(优先级 100,`linux-{arch}`)。 +- **`system-headers`(v1.2)。** 资源脚本由 llvm-rc 编译、而工具链未陈述任何头文件来源的行,取所选提供者的 `system-headers` + 所指的包的 `include`;已陈述来源的行(MSVC 的 SDK、配对载荷)不解析它,也不安装该包。该包无法安装时构建被拒绝并指名它。 - **`mingw-llvm`(v1.1)。** 以基座包 `xim:mingw-w64-sysroot`(llvm-mingw 的目标侧:UCRT 上的 MinGW-w64、libc++、libunwind、 compiler-rt)与 llvm 服务 `x86_64-windows-gnu` 与 `aarch64-windows-gnu`,在任一宿主可得,优先级 5,低于有 gcc 载荷之处的 MinGW-w64 提供者。基座是 clang 的 sysroot;其 libc++ 的模块源码是 std 模块;编译器运行时取自基座所指的目录;静态链接取基座的 @@ -97,6 +126,10 @@ 5. **身份随 C 库。** 依赖图提供 C 库(`mcpp:c-abi`)而请求未写 env 时,身份取实际 C 库的行,重新准备一次。写出的 env 是项目的 拼写,不被替换;与图不一致时报告(目标报告的 `asks for the … C ABI`),不拒绝。 +6. **请求的回答是契约(v1.2)。** 新增的提供者条目**禁止**改变任一宿主上既有请求的回答:它以低于现有回答的优先级进入, + 只由写全身份的请求或显式选择取得。改变一个请求的缺省回答是一次单独的决定,记入 CHANGELOG 与 §8。行表快照 + (`tests/fixtures/target-rows.snapshot`)记录每个请求在每种宿主上的回答。 + ## 5. 平台定义的行与提供者 平台包以 `[[platform.define]]` 定义行(§2.1 的字段),以 `[[platform.provider]]` 声明提供者(§3 的键);`[platform]` 与 @@ -119,6 +152,22 @@ - **执行。** 行的 `execution` 不是本机系统时:构建不以本机加载器检查产物的运行时闭包;`mcpp run` 与 `mcpp test` 在无运行器、 无执行主机时报告「已构建,未运行」。运行器包是目标的工具(`[target..tools]`),以 docs/41 的运行协议启动。打包从产物 文件读取其闭包,不运行本机加载器(SPEC-014,v1.1)。 +- **一个谓词(v1.2)。** 「产物是否由本机运行」只有两个问法,各在一处回答,内置行与平台定义的行相同:执行系统是否为本机 + 系统(启动之前问);执行系统与架构是否都是本机的(检查加载器与运行时闭包之前问)。 +- **不启动另一系统的程序(v1.2)。** 执行系统不是本机系统、又没有运行器与执行主机时,`mcpp run` 与 `mcpp test` 在启动任何 + 进程之前回答「已构建,未运行」,写出两个系统与可写的声明;退出码为 2。本机内核不读取程序所属的系统,另一系统的系统调用 + 号在本机另有含义,所以启动不是观察。`--no-runner` 陈述该身份在本机可直接执行,照此执行。同一系统、另一架构的产物照旧 + 启动:内核拒绝它,或由已登记的模拟器运行它。 +- **程序解释器(v1.2)。** 由本机运行的动态产物写基座自己的加载器所在的路径;不由本机运行的动态产物写行的 `loader`,且 + 不带任何本机目录的 RUNPATH。构建机的安装路径**禁止**进入后一类产物。 +- **基座包只有一种读法(v1.2)。** 根目录带清单的基座是包根即 sysroot(SPEC-002 §2.5),不论其目录怎样命名;目录布局只对 + 不带清单的目录探测。另一台机器的基座不取工具链载荷为其自身宿主构建的运行时目录。 +- **提供者的运行器(v1.2)。** 提供者条目的 `runner` 是运行器陈述的最低一级:其上依次是平台、构建程序、本机配置、项目与 + 环境变量(SPEC-013 §4.5)。`hosts` 列出该运行器所在的宿主,空为任一宿主。模板中 `{}` 是产物,`{sysroot}` 是该安排的 + 基座,`{pkg:}` 是 `requires` 中该名字的包的根;模板的程序先在 `requires` 的包中查找,再查 PATH。`requires` 的包 + 在第一次 `mcpp run` 或 `mcpp test` 时按工具链的同一方式安装;构建不安装它们。 +- **运行器的身份(v1.2)。** 平台的 `runner` 可陈述 `built-as = { "-" = "<身份>" }`:为该宿主对构建的运行器程序 + 是哪个身份。自带运行器的包按它为各系统构建运行器;引擎只对它一向认识的三个系统保留约定,其余系统无此陈述即无程序。 - **`link.flags` 与链接驱动(v1.1)。** `[platform.link] flags` 只交给 ld.lld;`driver = "compiler"` 的平台陈述它时,构建 给出警告并指名 `[platform.build] ldflags`。 @@ -138,6 +187,8 @@ ## 8. 兼容性 - 已有身份字符串、输出目录与 `cfg` 结果不变;`[target.X]`、`descriptor_keys`、`[platform.hosted]` 照旧可读。 +- 行为变化(v1.2):执行系统不是本机系统的产物,在没有运行器时不再被启动(此前 `mcpp run` 启动它并报告内核的回答); + 经 binfmt 登记了另一系统解释器的宿主以 `--no-runner` 或一条 `runner` 声明照旧运行。 - 行为变化:只写 `-` 的请求在本宿主不可得其标准身份时取下一个可得的身份(无 MSVC 的宿主上 `x86_64-windows` 为 `x86_64-windows-musl`;非 macOS 宿主上 `aarch64-macos` 为 `aarch64-macos-musl`;x86_64 宿主上 `aarch64-linux` 为 `aarch64-linux-musl`);依赖图提供 musl 而请求未写 env 时身份为 `-musl`。 @@ -148,3 +199,4 @@ |---|---|---| | 1.0 | 2026-11-09 | 初版:行、提供者、选择、平台定义的行、C 环境、执行、报告与锁 | | 1.1 | 2026-11-10 | `clang-triple`;内置行的系统词;FreeBSD 两种身份与 32 位 planned 行;提供者 sysroot 为基座陈述的最后一级;`mingw-llvm` 与 `aarch64-windows-gnu`;C 库由平台描述的行与内置行同一读法;打包读文件闭包;`link.flags` 的警告;构建程序的 `MCPP_KERNEL_ABI_INTERFACES` | +| 1.2 | 2026-11-12 | 行的 `loader` 与 `execution` 为内置行所读;系统表(`uname`);提供者的 `tier`、`system-headers`、`runner`;一条条目一种安排;请求的回答是契约;`glibc-sysroot`;一个执行谓词,另一系统的程序不被启动;程序解释器与基座包的单一读法;运行器的占位、`requires` 与 `built-as` | diff --git a/docs/specs/test.md b/docs/specs/test.md index 2d1522969..bdc46d7ac 100644 --- a/docs/specs/test.md +++ b/docs/specs/test.md @@ -66,6 +66,8 @@ | L3 | L2 与 P7(O5) | 每个用例的资源与工件:调度与按影响选择 | - **P1.1** 不实现协议的程序是 L0 程序。运行器**必须**原样运行它,并**必须**只以退出状态判定:0 为通过,其余为失败。 + 运行器的配置**可以**为这类程序陈述一个表示"在本机不适用"的退出状态(mtest 的 `skip_exit`,mtest >= 0.6.1):程序以它 + 退出时,其用例为跳过,原因取其输出中最后一行以 `SKIP` 开头的文字。跳过**不得**计入通过。 - **P1.2** 运行器**禁止**把协议变量(P2)交给不知其实现协议的程序,除非它接受一个 L0 程序完整运行一次(P2.4)。 #### P2. 握手 diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md index 645333d9c..db6b7f81f 100644 --- a/docs/zh/10-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -736,6 +736,10 @@ Cygwin 的 shell 交给 `bin/.exe`,以该目录中的映像与自身的 openkal-platform 的不含 `windows-aarch64`,因此本 mcpp 能构建的 `aarch64-windows-gnu` 行不从它得到运行器)。在 Windows 主机上构建的映像与其他主机上的字节不同 (#850):树在 Linux 或 macOS 上打包。 +**`all` 所指的是系统表(mcpp 2026.11.12.1+)。** 表中陈述的每个系统(`[[platform.system]]`,内置的或平台的),加上平台为该 +架构定义了行的系统。没有程序可构建的对写为未覆盖并附原因,其余照常打包。引擎不持有约定的系统,其程序按平台运行器为该宿主 +对陈述的身份构建(`runner.built-as`,[35](35-platforms.md));不带 `--systems` 的打包只携带打包机自身系统的运行器。 + **在哪里运行。** 打包把 `runs_on` 写进 `mcpp-package.json`,并在 `Packed` 之后写出:`self` 为凭包内携带的东西即可 运行的 `-`,`with.` 为机器装有该运行器时可运行的组合(腿的架构 × 平台的 `runner.hosts`)。 diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index 424c4d437..56aa136f0 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -473,12 +473,16 @@ docs/22。 着;`aarch64-linux-gnu` 由 `linux-aarch64` 原生宿主服务。把它们 合并成 `linux`,会让一台的行覆盖掉另一台的行。 +`aarch64-linux-gnu` 还有第二种、交叉的安排(2026.11.12.1+):在 `linux-x86_64` 上,提供者 `glibc-sysroot` 以一个基座包 +服务它([22](22-target-side.md))。它排在 musl 的条目之下,所以 `--target aarch64-linux` 在那里仍由 `aarch64-linux-musl` +回答,写全身份才取 glibc 的交叉安排;`mcpp run` 经该提供者的运行器启动其产物([41](41-devices.md))。 + ### 构建机与它们服务的目标 | target | tier | pin | linux-x86_64 | linux-aarch64 | macos-arm64 | windows-x86_64 | |---|---|---|---|---|---|---| | `x86_64-linux-gnu` | verified | — | 载荷 | — | — | — | -| `aarch64-linux-gnu` | preview | `llvm@23.1.3` | — | 载荷 | — | — | +| `aarch64-linux-gnu` | preview | `llvm@23.1.3` | 基座包 | 载荷 | — | — | | `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 | @@ -531,7 +535,7 @@ system · `系统` 在机器上被找到,不是 mcpp 安装的 · `SDK` 平台 | `x86_64-windows-gnu` | Linux 与 Windows 经 gcc 载荷;每一台宿主经 MinGW-w64 基座包 | 一个身份,只在分发层按宿主拆分;没有 gcc 载荷之处,`mingw-llvm` 提供者以 clang 与 `xim:mingw-w64-sysroot`(llvm-mingw 的目标侧:UCRT 上的 MinGW-w64、libc++、compiler-rt)服务它 | | `aarch64-windows-gnu` | 每一台宿主 | 只由 MinGW-w64 基座包服务;没有 gcc 载荷以它为目标 | | `*-freebsd` | 每一台宿主,经平台 | 行是内置的;平台为每个身份指名 FreeBSD 基座包(`xim:x86_64-freebsd-sysroot`、`xim:aarch64-freebsd-sysroot`),以 `[target.<行>] platform` 或该行满足的任一选择器(`[target.freebsd]`、`cfg(os = "freebsd")`)选择;其他平台包的提供者不与所选平台竞争 | -| `*-freebsd-musl` | 每一台宿主,经依赖图 | openkal-platform 的 hosted 部分(0.26.0):openkal-freebsd 的 standalone 形态之上的 openkal-musl,一个静态程序;未为 `-freebsd` 选平台时,该请求取这一身份(2026.11.10.2+),正如非 Mac 宿主上的 `aarch64-macos` 取 musl 身份 | +| `*-freebsd-musl` | 每一台宿主,经依赖图 | openkal-platform 的 hosted 部分(0.27.0):openkal-freebsd 的 standalone 形态之上的 openkal-musl,一个静态程序;未为 `-freebsd` 选平台时,该请求取这一身份(2026.11.10.2+),正如非 Mac 宿主上的 `aarch64-macos` 取 musl 身份 | | `x86_64-windows-msvc` | Windows | MSVC 是在机器上被找到的 | | `x86_64-windows-musl` | payload 只在 Windows 上;图则任意宿主 | 没有 gcc 能发出 PE + musl,且 LLVM 拼不出这个三元组 | | `aarch64-macos` | macOS | SDK 是那台机器自己的 | diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 0b70acfd9..cc651c25b 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -1110,10 +1110,18 @@ std-module-flags = ["-D__fenv_static="] 一个基座包是一个身份的 sysroot。平台以各提供者的 `sysroot` 为每个身份指名自己的基座([35](35-platforms.md));驱动能在 一个根内区分架构的系统(MinGW-w64)由一个包服务。 +**基座包只有一种读法(2026.11.12.1+)。** 它的清单陈述包根即 sysroot,所以不论其目录怎样命名,都按 sysroot 渲染 +(`--sysroot=<根>`);目录布局只对不带清单的目录探测。产物不由本机加载时——基座属于另一架构或另一系统——链接不取工具链 +载荷自身的运行时目录,动态产物以行的 `loader` 为程序解释器(`/lib/ld-linux-aarch64.so.1`),其 RUNPATH 不含本机的任何 +目录。这类产物经运行器运行,运行器以 `{sysroot}` 得到该基座([41](41-devices.md))。原生的动态产物仍写基座自己的加载器 +所在的路径。 + 索引中有 `xim:x86_64-freebsd-sysroot` 与 `xim:aarch64-freebsd-sysroot`(FreeBSD 的基本系统,附 libc++ 的模块源码), 以及 `xim:mingw-w64-sysroot`(llvm-mingw 的目标侧:UCRT 上的 MinGW-w64、libc++、libunwind 与 compiler-rt,x86_64 与 aarch64)。在 `*-windows-gnu` 行上后者是 clang 的 sysroot;`mingw-llvm` 提供者指名它,因此每一台宿主都能构建 GNU ABI 的 -Windows 行([21](21-the-target-triple.md))。 +Windows 行([21](21-the-target-triple.md))。索引中还有 `xim:aarch64-linux-gnu-sysroot`(arm64 的 glibc,附内核头文件、 +带模块源码的 libc++ 与 compiler-rt),由 `glibc-sysroot` 提供者指名:在 linux-x86_64 上,`--target aarch64-linux-gnu` 以它 +构建 C、C++ 与 `import std` 的程序,动态或静态皆可(2026.11.12.1+)。 构建程序可以询问供给 sysroot 的是哪个 C 库**载荷**:`mcpp::target_libc()` 返回该包的名字,`mcpp::target_libc_profile()` 返回目标 ISA 档位对应的 @@ -1147,11 +1155,21 @@ WebAssembly 上,每个目标文件、预编译的标准库模块与链接必 |---|---|---|---| | `threads` | 布尔 | 在既非 PE 也非 freestanding 的目标上为 `-pthread`;在 PE 与 freestanding 目标上不产生任何 flag | 标准库模块的预构建、依赖扫描、所有包的每个 C 与 C++ 翻译单元,以及链接 | | `exceptions` *(mcpp 2026.9.12.3+)* | 布尔,或 `"wasm"` / `"js"` *(2026.10.30.1+)* | 只在 `os = "emscripten"` 上,经方言 flag 进入编译行,也进入链接行:`"wasm"` 为 `-fwasm-exceptions`,即 WebAssembly 自身的异常处理;`"js"` 与 `true` 为 `-fexceptions`,即基于 JavaScript 的模型。在其余每个目标上什么都不产生,因为那里异常本来就是默认开启的 | 与 `threads` 相同的那一套 | +| `longjmp` *(mcpp 2026.11.12.1+)* | `"wasm"` 或 `"js"` | 只在 `os = "emscripten"` 上,进入每个 C 与 C++ 翻译单元的编译行与链接行:`"wasm"` 为 `-sSUPPORT_LONGJMP=wasm`,即 WebAssembly 自身的栈切换;`"js"` 为 `-sSUPPORT_LONGJMP=emscripten`,即 JavaScript 模型,也是工具链的默认。声明在其余任何行上都会被拒绝。成员缺省时不产生任何 flag | 与 `threads` 相同的那一套 | -两个成员都经由方言 flag 进入依赖缓存键,因此没有启用某个成员的构建,不会 +三个成员都经由它们渲染的 flag 进入依赖缓存键,因此没有启用某个成员的构建,不会 被启用它的构建复用,以一种异常模型构建的依赖也不会被另一种模型的构建复用。 未知成员,以及表外的取值,都会被拒绝,并列出可取的值。 +**`longjmp = "wasm"` 使 C 的 `sigsetjmp`/`siglongjmp` 在 Emscripten 下可用。** 工具链默认的 JavaScript 模型把这一对编译成 +栈拷贝,越过两层调用的非局部退出会读到模块的栈之外,在运行时终止。编译与链接都带上 `-sSUPPORT_LONGJMP=wasm` 后,这一对 +可以往返。非局部退出很深的程序还需要更大的栈;那是项目自己的旋钮(`ldflags = ["-sSTACK_SIZE=..."]`),不是 ABI 事实, +所以引擎不渲染它。 + +**`longjmp` 与 `exceptions` 所指的是同一机制。** WebAssembly 的异常处理同时承载抛出与长跳转,JavaScript 模型也同时承载 +二者。未写 `longjmp` 时它随 `exceptions`,与 SDK 自身的缺省一致。为 `exceptions` 写一种模型、为 `longjmp` 写另一种的构建, +在任何编译之前被拒绝,并写出两处陈述。声明了的 `longjmp` 是配置键的一部分;未声明它的项目保持原有的键。 + **新程序应选 `"wasm"`。** 它的代码比 JavaScript 模型更小、更快,要求宿主支持 WebAssembly 异常处理:Safari 15.2、 Firefox 100、Chrome 95、Node 17 及以上。`true` 保持自 2026.9.12.3 起的含义,即 JavaScript 模型。依赖的 `requires_abi = { exceptions = true }` 要求的是"有异常"而不是某种模型,由根的选择满足。 diff --git a/docs/zh/35-platforms.md b/docs/zh/35-platforms.md index 19e435388..59982cc71 100644 --- a/docs/zh/35-platforms.md +++ b/docs/zh/35-platforms.md @@ -168,7 +168,7 @@ error: openkal-musl is requested as '^0.27.0' by app, and the platform openkal-p ```toml [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.26.0" # 一个版本,或一个系列:"openkal-platform@^0.26" +platform = "openkal-platform@0.27.0" # 一个版本,或一个系列:"openkal-platform@^0.27" ``` 使用默认平台的构建失败、且已同步的索引中有更新版本时,最后输出一行 `tip:`,写出两个版本与上面这一行。 @@ -293,6 +293,42 @@ driver = "compiler" **`[platform.link] flags` 是给 ld.lld 的词(2026.11.10.1+)。** `driver` 为 `"compiler"` 的平台不把它们交给任何程序,构建 会说明这一点,并指名 `[platform.build] ldflags`——后者的词进入编译器的链接。 +**提供者陈述其产物怎样启动,以及一个系统叫什么(2026.11.12.1+)。** 提供者条目的三个键与一张表: + +```toml +[[platform.provider]] +serves = ["x86_64-haiku"] +tier = "preview" # 这一安排的档位;未写时为行的档位 +system-headers = "xim:haiku-headers@1.0" # 系统自身接口的头文件,供对它们编译的步骤读取 +runner = { cmd = ["dirun", "--profile", "{pkg:haiku-disk-image}/dirun.toml", "--", "{}"], + hosts = ["linux-x86_64", "macos-aarch64"], + requires = ["xim:dirun@0.2.0", "xim:haiku-disk-image@1.0"] } + +[[platform.system]] +name = "haiku" +uname = "Haiku" # `uname -s` 在该系统上的回答 +``` + +提供者的 `runner` 是运行器陈述的最低一级:平台的 `runner`、构建程序的、本机 `config.toml` 的、项目的 +`[target.] runner` 与 `MCPP_TARGET__RUNNER` 依次替换它。`hosts` 是该运行器所在的宿主(未写为任一宿主)。 +模板中 `{}` 是产物,`{sysroot}` 是产物链接时所用的基座,`{pkg:}` 是 `requires` 中该名字的包的根;模板的程序先在 +`requires` 的包中查找,再查 `PATH`。`requires` 的包由第一次需要它们的 `mcpp run` 或 `mcpp test` 安装,构建不安装。 + +一条提供者条目陈述一种载荷安排:原生安排与交叉安排是两条条目,各有自己的 `hosts`、基座与优先级。新增的条目不改变 +此前已有回答的请求,所以它以低于现有 `-` 回答的优先级进入 +([SPEC-016 §4](../specs/targets-and-providers.md))。 + +陈述了 `uname` 的系统可进入每个系统一个程序的单文件(`mcpp pack --pkg-format single-file`);未陈述的系统,其程序是 +文件开头的 PE。运行器为多个系统构建的平台,陈述各自的身份: + +```toml +[platform] +runner = { cmd = ["okrun", "{}"], hosts = ["linux-x86_64", "freebsd-x86_64"], + built-as = { "freebsd-x86_64" = "x86_64-freebsd-musl" } } +``` + +`mcpp pack --systems all` 取表中的全部系统,没有程序可构建的对写为未覆盖。 + `mcpp new <名> --template platform` 生成一个定义 `-<名>` 的平台包。读取平台清单时按行检查其语句:定义内置行、 同一行定义两次是错误,无论平台在何处被读取;不服务任何行的提供者、以同一优先级服务同一身份的两个提供者是平台自身构建的 警告。`mcpp why platform` 列出平台定义的行与所服务的身份,各附提供者与优先级:在平台目录中是其自身,在项目中是其目标所选 diff --git a/docs/zh/41-devices.md b/docs/zh/41-devices.md index d152a22b9..d41ac2cb2 100644 --- a/docs/zh/41-devices.md +++ b/docs/zh/41-devices.md @@ -137,8 +137,14 @@ freebsd-vm = { git = "https://example.org/freebsd-vm.git", tag = "v1" } ``` 该工具为本机构建,以上述 argv 与环境启动:它传送产物及 `MCPP_RUNTIME_FILES` 所列文件,执行 argv,转发程序的输出, -并以程序的退出码退出。平台定义的行中,`execution` 为本机以外系统的产物,若无 runner 也无执行主机(`run_under`), -即**已构建、未运行**:`mcpp run` 拒绝并给出声明 runner 的键,`mcpp test` 把其测试报告为未运行。 +并以程序的退出码退出。行的 `execution` 为本机以外系统的产物,若无 runner 也无执行主机(`run_under`), +即**已构建、未运行**:`mcpp run` 在启动任何进程之前拒绝,并给出声明 runner 的键,`mcpp test` 把其测试报告为未运行。 +这条规则对每一行相同,无论内置还是平台定义(2026.11.12.1+;此前内置行的产物会被启动,并报告内核的回答)。本机内核不读取 +程序所属的系统,另一系统的系统调用号在本机另有含义,所以启动它什么也观察不到。`--no-runner` 陈述该身份在本机可直接执行, +供经已登记的解释器运行此类程序的宿主使用。同一系统、另一架构的产物照旧启动。 + +提供者可以陈述其安排的运行器,于是目标无需任何声明即可运行(docs/35):在 linux-x86_64 上, +`mcpp run --target aarch64-linux-gnu` 在第一次运行时安装用户态模拟器,并以该安排的基座为模拟器的根启动产物。 ## 运行一个可分发物 diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index d92b7a778..37a71c175 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -488,7 +488,7 @@ mcpp toolchain list --format json 的形状是 `{target, note, toolchain, pin, status, default}`,`status` 取 `installed` / `available` / `via dependency graph` / `planned` 之一。 目标行另有 `platform`(2026.10.27.1+):目标表为该目标选择的平台包 -(2026.11.10.2 起为 `mcpplibs.openkal-platform@0.26.0`,2026.11.10.1 为 `@0.25.0`,2026.11.8.1 起为 `@0.24.4`,2026.11.7.2 为 `@0.24.3`;2026.11.7.1 为系列 `^0.24`),或 `null`。 +(2026.11.12.1 起为 `mcpplibs.openkal-platform@0.27.0`,2026.11.10.2 为 `@0.26.0`,2026.11.10.1 为 `@0.25.0`,2026.11.8.1 起为 `@0.24.4`,2026.11.7.2 为 `@0.24.3`;2026.11.7.1 为系列 `^0.24`),或 `null`。 **`toolchain` 与 `pin` 不是把同一个字段写了两遍。** `toolchain` 表示这一行 关联到什么 —— 已装的行是装好的载荷,词表行是它自己的约定。`pin` 只承载目标 diff --git a/mcpp.toml b/mcpp.toml index c56ce4699..dc39019df 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.11.10.2" +version = "2026.11.12.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/manifest/src/platform.cppm b/modules/manifest/src/platform.cppm index 9ac684653..fee29752e 100644 --- a/modules/manifest/src/platform.cppm +++ b/modules/manifest/src/platform.cppm @@ -120,6 +120,12 @@ struct Runner { bool hostsSet = false; bool exclusive = false; bool exclusiveSet = false; + // The identity the runner program of a host pair is built as, where the + // platform builds it for a system (`built-as = { "freebsd-x86_64" = + // "x86_64-freebsd-musl" }`): a package that carries its runner reads it. + std::map builtAs; + // The packages the runner is installed from on the first run. + std::vector requires_; Origin origin; bool set = false; }; @@ -267,6 +273,16 @@ struct ProviderSpec { std::map layers; Link link; std::string execution; + // The tier of this arrangement (empty: the row's), the package of the + // system's own headers, and how its artifacts are started (SPEC-016 §3, §6). + std::string tier, systemHeaders; + Runner runner; + Origin origin; +}; + +// A SYSTEM (`[[platform.system]]`, SPEC-016 §2.3). +struct SystemSpec { + std::string name, uname; Origin origin; }; @@ -308,6 +324,7 @@ struct Statement { std::optional cHosted; std::vector defines; std::vector providers; + std::vector systems; int priority = 100; bool prioritySet = false; @@ -317,7 +334,8 @@ struct Statement { && tools.empty() && build.empty() && buildCfg.empty() && !link.driver.set && !link.flags.set && !link.start.set && !link.script.set && !link.inputsSet && !link.exe.set && !link.shared.set && !link.sealSet - && versions.empty() && !hosted.set && defines.empty() && providers.empty(); + && versions.empty() && !hosted.set && defines.empty() && providers.empty() + && systems.empty(); } }; @@ -352,7 +370,7 @@ constexpr bool is_layer(std::string_view k) { inline constexpr std::string_view kPlatformKeys[] = { "targets", "toolchain", "sysroot", "runner", "runners", "carrier", "plugin", "universal", "descriptor_keys", "layers", "tools", "build", "link", "versions", "hosted", - "define", "provider", "priority", + "define", "provider", "system", "priority", }; inline constexpr std::string_view kDefineKeys[] = { "triple", "tier", "note", "standard", "exact", "clang-triple", "object-format", "family", @@ -361,8 +379,9 @@ inline constexpr std::string_view kDefineKeys[] = { }; inline constexpr std::string_view kProviderKeys[] = { "name", "serves", "priority", "hosts", "pairs", "toolchain", "sysroot", "linkage", "base", - "hosted", "layers", "link", "execution", + "hosted", "layers", "link", "execution", "tier", "system-headers", "runner", }; +inline constexpr std::string_view kSystemKeys[] = { "name", "uname" }; inline constexpr std::string_view kBuildKeys[] = { "cflags", "cxxflags", "asmflags", "ldflags" }; inline constexpr std::string_view kLinkKeys[] = { "driver", "flags", "start", "script", "inputs", "exe", "shared", "seal", diff --git a/modules/manifest/src/platform_providers.cppm b/modules/manifest/src/platform_providers.cppm index 691bc147a..e267ff0cc 100644 --- a/modules/manifest/src/platform_providers.cppm +++ b/modules/manifest/src/platform_providers.cppm @@ -23,6 +23,8 @@ struct StatedProvider { int priority = 100; std::string toolchain, sysroot, linkage; bool shorthand = false; // `[platform]` or `[platform.hosted]` + std::string tier, systemHeaders; + std::vector runner, runnerHosts, runnerRequires; }; // `name` is the platform's name (`platform_name`), the name of every provider @@ -78,7 +80,8 @@ std::vector stated_providers(const Statement& s, std::string_vie std::vector out; for (auto const& p : s.providers) out.push_back({ p.name.empty() ? std::string(name) : p.name, p.serves, p.hosts, p.pairs, p.priority, - p.toolchain.empty() ? s.toolchain.value : p.toolchain, p.sysroot, p.linkage, false }); + p.toolchain.empty() ? s.toolchain.value : p.toolchain, p.sysroot, p.linkage, false, + p.tier, p.systemHeaders, p.runner.cmd, p.runner.hosts, p.runner.requires_ }); if (s.targets.set) out.push_back({ std::string(name), s.targets.value, {"*"}, {}, s.prioritySet ? s.priority : 100, s.toolchain.value, {}, {}, true }); diff --git a/modules/manifest/src/platform_toml.cppm b/modules/manifest/src/platform_toml.cppm index 660f4b1ba..714de9d84 100644 --- a/modules/manifest/src/platform_toml.cppm +++ b/modules/manifest/src/platform_toml.cppm @@ -268,10 +268,26 @@ std::expected Reader::runner(const toml::Value& v, std::str if (!h) return std::unexpected(h.error()); r.hosts = std::move(*h); r.hostsSet = true; + } else if (k == "requires") { + auto w = strings(val, key); + if (!w) return std::unexpected(w.error()); + r.requires_ = std::move(*w); + } else if (k == "built-as") { + if (!val.is_table()) + return std::unexpected(std::format( + "{} must be a table of \"-\" = \"\"", key)); + for (auto const& [pair, triple] : val.as_table()) { + if (!mcpp::manifest::platform::is_host_pair(pair) || !triple.is_string() + || triple.as_string().empty()) + return std::unexpected(std::format( + "{}.{} must name the identity the runner of that host pair is built as, " + "for example \"freebsd-x86_64\" = \"x86_64-freebsd-musl\"", key, pair)); + r.builtAs[pair] = triple.as_string(); + } } else { warn(std::format( - "{}: '{}' is not a key of a runner (ignored); the keys are cmd, host, hosts, exclusive", - where, k)); + "{}: '{}' is not a key of a runner (ignored); the keys are cmd, host, hosts, " + "exclusive, requires, built-as", where, k)); } } } else { @@ -616,6 +632,13 @@ std::expected Reader::provider(const toml::Table& t, } if (path.empty()) return std::unexpected(std::format("{} is a table without `path`", key)); p.sysroot = "path:" + absolute(path).generic_string(); + } else if (k == "runner") { + auto r = runner(v, key); + if (!r) return std::unexpected(r.error()); + p.runner = std::move(*r); + } else if (k == "tier" || k == "system-headers") { + if (!v.is_string()) return std::unexpected(std::format("{} must be a string", key)); + (k == "tier" ? p.tier : p.systemHeaders) = v.as_string(); } else if (k == "name" || k == "toolchain" || k == "sysroot" || k == "linkage" || k == "execution" || k == "base") { if (!v.is_string()) @@ -650,6 +673,8 @@ std::expected Reader::provider(const toml::Table& t, return std::unexpected(std::format("{}.linkage is static or dynamic", where)); if (p.base != "platform" && p.base != "none") return std::unexpected(std::format("{}.base is \"platform\" or \"none\"", where)); + if (!p.tier.empty() && p.tier != "verified" && p.tier != "preview" && p.tier != "planned") + return std::unexpected(std::format("{}.tier is verified, preview or planned", where)); return p; } @@ -757,6 +782,29 @@ std::expected Reader::statement(const toml::Table& t) co return std::unexpected(std::format("{} must be an integer", key)); s.priority = static_cast(v.as_int()); s.prioritySet = true; + } else if (k == "system") { + if (!v.is_array()) + return std::unexpected(std::format("{} is an array of tables: write [[platform.system]]", key)); + std::size_t i = 0; + for (auto const& e : v.as_array()) { + const auto where = std::format("[[platform.system]] #{}", ++i); + if (!e.is_table()) return std::unexpected(std::format("{} must be a table", where)); + SystemSpec sys; + sys.origin = Origin{ tier, file.generic_string(), 0, where }; + for (auto const& [sk, sv] : e.as_table()) { + if (!contains(kSystemKeys, sk)) { + warn(std::format("{}: '{}' is not a key of a system (ignored); the keys are {}", + where, sk, list_of(kSystemKeys))); + continue; + } + if (!sv.is_string()) + return std::unexpected(std::format("{}.{} must be a string", where, sk)); + (sk == "name" ? sys.name : sys.uname) = sv.as_string(); + } + if (sys.name.empty()) + return std::unexpected(std::format("{} names no system: write name = \"\"", where)); + s.systems.push_back(std::move(sys)); + } } else if (k == "define" || k == "provider") { if (!v.is_array()) return std::unexpected(std::format( diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index f120a9ceb..b235a3389 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -4481,14 +4481,28 @@ std::expected parse_document(const t::Document& documen "[target.{}].abi must be a table, e.g. `[target.{}.abi]` " "with `threads = true`", triple, triple))); for (auto& [ak, av] : ait->second.as_table()) { - if (ak != "threads" && ak != "exceptions") + if (ak != "threads" && ak != "exceptions" && ak != "longjmp") return std::unexpected(error(origin, std::format( "[target.{}.abi] has no member '{}'; the members are: " - "threads, exceptions", triple, ak))); + "threads, exceptions, longjmp", triple, ak))); // `exceptions` also takes the model on Emscripten (design // 2026-10-29 §6): "wasm" or "js"; `true` keeps "js". const bool model = ak == "exceptions" && av.is_string() && (av.as_string() == "wasm" || av.as_string() == "js"); + // `longjmp` is a model only (2026.11.12.1): "wasm" or + // "js", no boolean -- the engine's default is the + // toolchain's own ("js"), and an absent member is that + // default rather than a switch turned off. + if (ak == "longjmp") { + if (!av.is_string() + || (av.as_string() != "wasm" && av.as_string() != "js")) + return std::unexpected(error(origin, std::format( + "[target.{}.abi].longjmp must be \"wasm\" or \"js\"", + triple))); + cc.abiLongjmpModel = av.as_string(); + cc.abiLongjmpDeclared = true; + continue; + } if (!av.is_bool() && !model) return std::unexpected(error(origin, ak == "exceptions" ? std::format("[target.{}.abi].exceptions must be true, false, " diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index d07c3bf97..21453c223 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -1173,6 +1173,15 @@ struct BuildConfig : BuildInputs { // "wasm" (`-fwasm-exceptions`), "js" (`-fexceptions`), or empty for // `exceptions = true`, which keeps its meaning since 2026.9.12.3: "js". std::string abiExceptionsModel; + // `[target..abi] longjmp` -- the third member (2026.11.12.1): + // how `sigsetjmp`/`siglongjmp` cross the WebAssembly boundary on an + // Emscripten row. A model, not a switch: "wasm" (`-sSUPPORT_LONGJMP=wasm`, + // WebAssembly's own stack switching) or "js" (`-sSUPPORT_LONGJMP=emscripten`, + // the JavaScript model, which is also the toolchain's default). The root's + // statement only, rendered into every translation unit and the link, like + // `threads`; the model reaches every cache key through those flags. + bool abiLongjmpDeclared = false; + std::string abiLongjmpModel; std::string cStandard; // Escape hatch for the hermetic link check: a sandbox toolchain whose // CRT/loader resolve OUTSIDE the sandbox is a hard error by default @@ -1732,6 +1741,9 @@ struct ConditionalConfig { bool abiExceptions = false; bool abiExceptionsDeclared = false; std::string abiExceptionsModel; + // `[target..abi] longjmp` -- see BuildConfig::abiLongjmpModel. + bool abiLongjmpDeclared = false; + std::string abiLongjmpModel; // `[target..build] dialect_cxxflags` -- a GRAPH-WIDE switch under a // target condition (issue #717, design 2026-09-27 §6). Like `abiThreads`/ // `abiExceptions` above, only the ROOT's value is rendered; unlike them it @@ -1846,7 +1858,7 @@ inline bool is_empty(const ConditionalConfig& c) { && c.buildDependencies.empty() && c.featureDeps.empty() && c.targetKinds.empty() && c.xlings.empty() && c.xlings.overrides.empty() - && !c.abiThreadsDeclared && !c.abiExceptionsDeclared + && !c.abiThreadsDeclared && !c.abiExceptionsDeclared && !c.abiLongjmpDeclared && !c.requiresAbiThreads && !c.requiresAbiExceptions && c.requires_.empty() && c.privateRequires.empty() && c.featureRequiresAbiThreads.empty() && c.featureRequiresAbiExceptions.empty() diff --git a/modules/toolchain-model/build.mcpp b/modules/toolchain-model/build.mcpp index 80cd9d78a..6b79f4586 100644 --- a/modules/toolchain-model/build.mcpp +++ b/modules/toolchain-model/build.mcpp @@ -16,12 +16,15 @@ namespace { constexpr std::string_view kRowKeys[] = { "triple", "tier", "note", "standard", "exact", "march", "mabi", "code-model", "multilib", "flags", - "link-emulation", "clang-triple", "family", + "link-emulation", "clang-triple", "family", "object-format", "data-model", "exe-suffix", + "shared-suffix", "loader", "execution", }; constexpr std::string_view kProviderKeys[] = { "name", "serves", "priority", "hosts", "probe", "toolchain", "pairs", "sysroot", "linkage", - "platform", + "platform", "tier", "system-headers", "runner", }; +constexpr std::string_view kRunnerKeys[] = { "cmd", "hosts", "requires" }; +constexpr std::string_view kSystemKeys[] = { "name", "uname" }; constexpr std::string_view kTiers[] = { "verified", "preview", "planned" }; struct Failure { std::string text; }; @@ -113,13 +116,18 @@ std::string row_entry(const toml::Table& t, Arrays& arrays, std::set& rows) { @@ -146,13 +154,40 @@ std::string provider_entry(const toml::Table& t, Arrays& arrays, const std::set< const auto linkage = text(t, "linkage", where); if (!linkage.empty() && linkage != "static" && linkage != "dynamic") throw Failure{std::format("{}: linkage `{}` is not static or dynamic", where, linkage)}; + const auto tier = text(t, "tier", where); + if (!tier.empty() && std::ranges::find(kTiers, tier) == std::end(kTiers)) + throw Failure{std::format("{}: tier `{}` is not verified, preview or planned", where, tier)}; + // `runner = { cmd = [...], hosts = [...], requires = [...] }`: how an + // artifact of the arrangement is started, where, and from which packages. + std::vector runner, runnerHosts, runnerRequires; + if (auto it = t.find("runner"); it != t.end()) { + if (!it->second.is_table()) throw Failure{std::format("{}: `runner` is not a table", where)}; + auto const& r = it->second.as_table(); + const auto rwhere = where + " runner"; + known_keys(r, kRunnerKeys, rwhere); + runner = words(r, "cmd", rwhere); + runnerHosts = words(r, "hosts", rwhere); + runnerRequires = words(r, "requires", rwhere); + if (runner.empty()) throw Failure{std::format("{}: `cmd` is empty", rwhere)}; + } return std::format( " {{ .name = {}, .serves = {}, .priority = {}, .hosts = {}, .probe = {}, .toolchain = {}, " - ".pairs = {}, .sysroot = {}, .linkage = {}, .platform = {} }},\n", + ".pairs = {}, .sysroot = {}, .linkage = {}, .platform = {}, .tier = {}, .systemHeaders = {}, " + ".runner = {}, .runnerHosts = {}, .runnerRequires = {} }},\n", lit(name), arrays.add(serves), priority, arrays.add(words(t, "hosts", where)), lit(text(t, "probe", where)), lit(text(t, "toolchain", where)), arrays.add(words(t, "pairs", where)), lit(text(t, "sysroot", where)), lit(linkage), - lit(text(t, "platform", where))); + lit(text(t, "platform", where)), lit(tier), lit(text(t, "system-headers", where)), + arrays.add(runner), arrays.add(runnerHosts), arrays.add(runnerRequires)); +} + +std::string system_entry(const toml::Table& t, std::set& seen) { + known_keys(t, kSystemKeys, "[[platform.system]]"); + const auto name = text(t, "name", "[[platform.system]]"); + if (name.empty()) throw Failure{"[[platform.system]]: an entry without `name`"}; + if (!seen.insert(name).second) throw Failure{std::format("[[platform.system]] {}: stated twice", name)}; + return std::format(" {{ .name = {}, .uname = {} }},\n", lit(name), + lit(text(t, "uname", "[[platform.system]] " + name))); } const toml::Array& entries(const toml::Document& doc, std::string_view path) { @@ -166,7 +201,8 @@ const toml::Array& entries(const toml::Document& doc, std::string_view path) { std::string generate(const toml::Document& doc) { Arrays arrays; std::set rowNames; - std::string rows, providers; + std::set systemNames; + std::string rows, providers, systems; for (auto const& e : entries(doc, "platform.define")) { if (!e.is_table()) throw Failure{"[[platform.define]]: an entry that is not a table"}; rows += row_entry(e.as_table(), arrays, rowNames); @@ -175,6 +211,10 @@ std::string generate(const toml::Document& doc) { if (!e.is_table()) throw Failure{"[[platform.provider]]: an entry that is not a table"}; providers += provider_entry(e.as_table(), arrays, rowNames); } + for (auto const& e : entries(doc, "platform.system")) { + if (!e.is_table()) throw Failure{"[[platform.system]]: an entry that is not a table"}; + systems += system_entry(e.as_table(), systemNames); + } return std::format( "// Generated by modules/toolchain-model/build.mcpp from data/builtin.toml.\n" "export module mcpp.toolchain.builtin_targets;\n\n" @@ -184,8 +224,9 @@ std::string generate(const toml::Document& doc) { "export namespace mcpp::toolchain::builtin {{\n\n" "inline constexpr mcpp::toolchain::target::Row kRows[] = {{\n{}}};\n\n" "inline constexpr mcpp::toolchain::target::Provider kProviders[] = {{\n{}}};\n\n" + "inline constexpr mcpp::toolchain::target::System kSystems[] = {{\n{}}};\n\n" "}} // namespace mcpp::toolchain::builtin\n", - arrays.decls, rows, providers); + arrays.decls, rows, providers, systems); } } // namespace diff --git a/modules/toolchain-model/data/builtin.toml b/modules/toolchain-model/data/builtin.toml index 4629e010d..77a92a3ec 100644 --- a/modules/toolchain-model/data/builtin.toml +++ b/modules/toolchain-model/data/builtin.toml @@ -30,6 +30,14 @@ # is not derived from the identity (`x86_64-unknown-freebsd14.0`) # family the system's family, `unix` or `windows` (`cfg(unix)`); a # system the engine has always known answers it by convention +# loader the program interpreter a dynamic program of the row names: the +# path on the machine that runs it. A link whose artifact this +# machine does not load writes it; a native link writes the +# payload's own loader +# execution the system the row's artifacts run on, when it is not the os +# segment (`android`: the kernel is Linux, the system is not) +# object-format, data-model, exe-suffix, shared-suffix +# facts of a system the engine's conventions do not answer # march, mabi, code-model, multilib, flags, link-emulation # the ISA profile of a bare-metal row: `-march`, `-mabi`, # `-mcmodel` (empty: the driver's default), the multilib @@ -51,16 +59,37 @@ # toolchain ships the whole sysroot itself # linkage the default linkage, `static` or `dynamic` # platform a platform package that implements it (SPEC-013) +# tier the tier of this arrangement, when it differs from the row's +# system-headers the package whose headers state the system's own interfaces, +# read by the steps that compile against them (resources) +# runner how an artifact of the arrangement is started when nothing +# nearer says: `{ cmd = [argv], hosts = [...], requires = [...] }`. +# `{}` is the artifact, `{sysroot}` the arrangement's base; +# `requires` are installed on the first run, never by a build +# +# ONE ENTRY STATES ONE ARRANGEMENT OF PAYLOADS. A native arrangement and a cross +# one are two entries, each with its `hosts`; an entry whose facts depend on the +# host it is read on is two entries written as one. A NEW ENTRY DOES NOT CHANGE +# THE ANSWER OF A REQUEST THAT WAS ANSWERED BEFORE IT: it enters below the +# priority of the entries that answer `arch-os` today (SPEC-016 §4.7). +# +# System keys (`[[platform.system]]`) +# name the os segment the system's rows carry +# uname the word `uname -s` answers there; a file that carries one +# program per system selects by it, and a system without the +# key joins no such file # ── Linux ──────────────────────────────────────────────────────────────────── [[platform.define]] triple = "x86_64-linux-gnu" +loader = "/lib64/ld-linux-x86-64.so.2" tier = "verified" standard = true [[platform.define]] triple = "x86_64-linux-musl" +loader = "/lib/ld-musl-x86_64.so.1" tier = "verified" standard = true @@ -70,11 +99,13 @@ standard = true # (SPEC-009 §10.7). [[platform.define]] triple = "aarch64-linux-gnu" +loader = "/lib/ld-linux-aarch64.so.1" tier = "preview" standard = true [[platform.define]] triple = "aarch64-linux-musl" +loader = "/lib/ld-musl-aarch64.so.1" tier = "verified" standard = true @@ -126,6 +157,7 @@ standard = false [[platform.define]] triple = "riscv64-linux-musl" +loader = "/lib/ld-musl-riscv64.so.1" tier = "planned" standard = true @@ -135,11 +167,13 @@ standard = true # an explicit `[target.] toolchain` opts in early. [[platform.define]] triple = "i686-linux-musl" +loader = "/lib/ld-musl-i386.so.1" tier = "planned" standard = true [[platform.define]] triple = "i686-linux-gnu" +loader = "/lib/ld-linux.so.2" tier = "planned" standard = true @@ -171,6 +205,7 @@ standard = false # COMPAT_FREEBSD14, on 15. [[platform.define]] triple = "x86_64-freebsd" +loader = "/libexec/ld-elf.so.1" family = "unix" tier = "preview" standard = true @@ -178,6 +213,7 @@ clang-triple = "x86_64-unknown-freebsd14.0" [[platform.define]] triple = "aarch64-freebsd" +loader = "/libexec/ld-elf.so.1" family = "unix" tier = "preview" standard = true @@ -363,12 +399,14 @@ link-emulation = "elf_i386" [[platform.define]] triple = "aarch64-linux-android" +execution = "android" tier = "verified" standard = true exact = true [[platform.define]] triple = "x86_64-linux-android" +execution = "android" tier = "verified" standard = true exact = true @@ -417,8 +455,7 @@ standard = true # which serves the musl identities of every system, 50; other conventions 10; a # base package beside llvm where a payload of the system's own exists too, 5. -# glibc from the payload, at the host's default toolchain natively. A cross to -# another architecture's glibc pins llvm; the payload exists for the host's arch. +# glibc from the payload, at the host's default toolchain natively. [[platform.provider]] name = "glibc" serves = ["x86_64-linux-gnu"] @@ -432,6 +469,23 @@ priority = 100 hosts = ["linux-{arch}"] toolchain = "llvm@23.1.3" +# THE CROSS ARRANGEMENT OF THE SAME IDENTITY: the C library, libc++ and the +# compiler run-time come from a base package whose root is the sysroot, since +# the toolchain's payload carries the libraries of its own host only. Below the +# musl entries (100, 50), so `aarch64-linux` is answered as before it existed +# and the whole identity reaches it. The loader the artifact names is the row's; +# it runs through the runner, which is given the base as its root. +[[platform.provider]] +name = "glibc-sysroot" +serves = ["aarch64-linux-gnu"] +priority = 40 +hosts = ["linux-x86_64"] +toolchain = "llvm@23.1.3" +sysroot = "xim:aarch64-linux-gnu-sysroot@2.44.3" +linkage = "dynamic" +tier = "preview" +runner = { cmd = ["qemu-aarch64-static", "-L", "{sysroot}", "{}"], hosts = ["linux-x86_64"], requires = ["xim:qemu-user-aarch64@7.2.0"] } + # The musl cross payloads: native on each Linux arch, every arch from x86_64, # and the Canadian cross on a Windows host of the same arch. [[platform.provider]] @@ -546,14 +600,46 @@ hosts = ["*"] toolchain = "llvm@23.1.3" pairs = ["llvm"] linkage = "static" -platform = "mcpplibs.openkal-platform@0.26.0" +platform = "mcpplibs.openkal-platform@0.27.0" [[platform.provider]] name = "openkal" -serves = ["x86_64-windows-musl", "*-linux-musl", "*-macos-musl", "*-freebsd-musl"] +serves = ["*-linux-musl", "*-macos-musl", "*-freebsd-musl"] priority = 50 hosts = ["*"] toolchain = "llvm@23.1.3" pairs = ["llvm"] linkage = "static" -platform = "mcpplibs.openkal-platform@0.26.0" +platform = "mcpplibs.openkal-platform@0.27.0" + +# The same arrangement on Windows, whose own interfaces a resource script +# compiles against: their headers are the MinGW-w64 base's, which states them +# for every architecture under one `include`. +[[platform.provider]] +name = "openkal" +serves = ["x86_64-windows-musl"] +priority = 50 +hosts = ["*"] +toolchain = "llvm@23.1.3" +pairs = ["llvm"] +linkage = "static" +platform = "mcpplibs.openkal-platform@0.27.0" +system-headers = "xim:mingw-w64-sysroot@20261104-2" + +# ── Systems ────────────────────────────────────────────────────────────────── + +[[platform.system]] +name = "linux" +uname = "Linux" + +[[platform.system]] +name = "macos" +uname = "Darwin" + +[[platform.system]] +name = "freebsd" +uname = "FreeBSD" + +# A Windows program is not selected by `uname`: it leads the file. +[[platform.system]] +name = "windows" diff --git a/modules/toolchain-model/src/base.cppm b/modules/toolchain-model/src/base.cppm index 160768ced..3d3334421 100644 --- a/modules/toolchain-model/src/base.cppm +++ b/modules/toolchain-model/src/base.cppm @@ -451,6 +451,10 @@ struct Resolution { // What a binding artifact loads: the base's dynamic loader and the // directories its RUNPATH names. std::filesystem::path loader; + // What a floor's dynamic artifact names as its program interpreter: the + // row's `loader`, a path on the machine that runs it (SPEC-016 §6). Empty + // for a binding (which names `loader`) and for a static link. + std::string interpreter; std::vector runtimeDirs; // Kernel headers the base lacks, supplied by the engine (§2.4, additive). std::vector addedIncludes; diff --git a/modules/toolchain-model/src/linkmodel.cppm b/modules/toolchain-model/src/linkmodel.cppm index dbe03def1..3c0648f36 100644 --- a/modules/toolchain-model/src/linkmodel.cppm +++ b/modules/toolchain-model/src/linkmodel.cppm @@ -22,6 +22,8 @@ import mcpplibs.os.platform; import mcpp.toolchain.apple_sdk; import mcpp.toolchain.model; import mcpp.toolchain.triple; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_schema; export namespace mcpp::toolchain { @@ -302,50 +304,27 @@ struct ClangDriverModel { } }; -// ── loader resolution: data over hardcodes ─────────────────────────────── +// ── loader resolution: the row's data ──────────────────────────────────── // -// Priority: -// 1. Triple map (x86_64/aarch64/riscv64/loongarch64 glibc + musl) — -// loader file names are platform-ABI-level stable conventions. -// 2. Glob: first `ld-*.so*` regular file in the lib dir (covers arches -// the map doesn't know yet). -// -// A third, declared-metadata source (a persisted `.xpkg-exports.json` -// written by the installer) was evaluated and removed: its only consumer -// would have been this resolver, while the two sources above already cover -// every real payload — see docs/91-toolchain-internals.md for the record. -// -// Returns the loader's absolute path, or empty when none was found (callers -// then omit --dynamic-linker and the hermeticity check reports the gap). - -// Loader *file name* for a target triple; empty when the arch is unknown. -std::string loader_filename(std::string_view targetTriple) { - const bool musl = targetTriple.find("musl") != std::string_view::npos; - struct ArchLoader { std::string_view arch, gnuName, muslArch; }; - static constexpr std::array kMap{{ - {"x86_64", "ld-linux-x86-64.so.2", "x86_64"}, - {"aarch64", "ld-linux-aarch64.so.1", "aarch64"}, - {"riscv64", "ld-linux-riscv64-lp64d.so.1", "riscv64"}, - {"loongarch64", "ld-linux-loongarch-lp64d.so.1", "loongarch64"}, - {"i686", "ld-linux.so.2", "i386"}, - }}; - for (auto& m : kMap) { - if (targetTriple.starts_with(m.arch)) { - if (musl) return std::format("ld-musl-{}.so.1", m.muslArch); - return std::string(m.gnuName); - } - } - return {}; +// A row states the program interpreter its dynamic programs name (`loader`, +// SPEC-016 §2.1): the path on the machine that runs them. The file name of +// that path is what a payload's library directory holds, and the path itself +// is what a shipped binary's PT_INTERP reads. A triple no row states one for +// has neither; `resolve_loader` then takes the first `ld-*.so*` of the +// directory. + +// The distro-side loader path of a target (`mcpp pack`, the host's own root). +std::string distro_loader_path(std::string_view targetTriple) { + auto t = triple::parse(targetTriple); + auto const* r = target::find_row(t ? t->str() : std::string(targetTriple)); + return r ? std::string(r->loader) : std::string{}; } -// The distro-side loader path a *shipped* binary's PT_INTERP should point at -// (LSB layout), by target triple. Used by `mcpp pack`. -std::string distro_loader_path(std::string_view targetTriple) { - auto name = loader_filename(targetTriple); - if (name.empty()) return {}; - if (targetTriple.starts_with("x86_64")) - return "/lib64/" + name; // LSB-mandated symlink on glibc distros - return "/lib/" + name; +// Its file name; empty when the row states none. +std::string loader_filename(std::string_view targetTriple) { + const auto p = distro_loader_path(targetTriple); + const auto slash = p.rfind('/'); + return slash == std::string::npos ? p : p.substr(slash + 1); } std::filesystem::path resolve_loader(const std::filesystem::path& libDir, @@ -353,7 +332,7 @@ std::filesystem::path resolve_loader(const std::filesystem::path& libDir, if (libDir.empty()) return {}; std::error_code ec; - // 1. Triple map. + // 1. The row's loader. if (auto name = loader_filename(targetTriple); !name.empty()) { auto p = libDir / name; if (std::filesystem::exists(p, ec)) return p; @@ -577,6 +556,13 @@ ToolchainLinkModel resolve_link_model(const Toolchain& tc) { lm.loader = tc.base.loader; for (auto const& d : tc.base.runtimeDirs) lm.libDirs.push_back(d); } + // A FLOOR'S DYNAMIC ARTIFACT NAMES THE ROW'S LOADER (SPEC-016 §6): + // the path on the machine that runs it, which the base resolution + // took from the row. No directory joins it: the artifact's + // libraries are that machine's. + else if (tc.base.runtime == base::Runtime::Floor && !tc.base.interpreter.empty()) { + lm.loader = tc.base.interpreter; + } if (tc.base.multiarch && !tc.base.multiarchTuple.empty()) { lm.multiarchIncludes.push_back(root / "usr" / "include" / tc.base.multiarchTuple); lm.multiarchLibDirs.push_back(root / "usr" / "lib" / tc.base.multiarchTuple); diff --git a/modules/toolchain-model/src/target_rows.cppm b/modules/toolchain-model/src/target_rows.cppm index 2719e549d..4d91dc566 100644 --- a/modules/toolchain-model/src/target_rows.cppm +++ b/modules/toolchain-model/src/target_rows.cppm @@ -27,6 +27,7 @@ struct DefinitionSet { std::deque> lists; std::vector rows; std::vector providers; + std::vector systems; std::string_view keep(std::string s) { return strings.emplace_back(std::move(s)); } std::span keep(const std::vector& v) { @@ -45,6 +46,11 @@ std::vector rows(); std::vector providers(); const Row* find_row(std::string_view triple); +// The system table: the built-in systems, then those the platforms state. A +// system stated twice is the first statement's. +std::vector systems(); +const System* find_system(std::string_view name); + // Whether a defined row names this os, or this env under this os: the words the // parser accepts beyond its own. bool defines_os(std::string_view os); @@ -134,6 +140,24 @@ const Row* find_row(std::string_view triple) { return nullptr; } +std::vector systems() { + std::vector out; + for (auto const& b : mcpp::toolchain::builtin::kSystems) out.push_back(&b); + auto& r = registry(); + std::lock_guard g(r.lock); + for (auto const& s : r.sets) + for (auto const& d : s.systems) + if (std::ranges::none_of(out, [&](const System* k) { return k->name == d.name; })) + out.push_back(&d); + return out; +} + +const System* find_system(std::string_view name) { + for (auto const* s : systems()) + if (s->name == name) return s; + return nullptr; +} + bool defines_os(std::string_view os) { // The built-in rows' systems are words of the language too (design // 2026-11-10 E4): a system the data names -- FreeBSD -- needs no word in diff --git a/modules/toolchain-model/src/target_schema.cppm b/modules/toolchain-model/src/target_schema.cppm index a36e0a4a1..928cbad1f 100644 --- a/modules/toolchain-model/src/target_schema.cppm +++ b/modules/toolchain-model/src/target_schema.cppm @@ -59,6 +59,26 @@ struct Provider { std::string_view sysroot; // a payload, or `toolchain` std::string_view linkage; // static | dynamic std::string_view platform; // a platform package reference + std::string_view tier; // of this arrangement; empty: the row's + // The package whose headers state the system's own interfaces, for the + // steps that compile against them and not against the C library (a + // resource compiler's `windows.h`). + std::string_view systemHeaders; + // HOW AN ARTIFACT OF THE ARRANGEMENT IS STARTED (SPEC-016 §6): the argv + // template, the hosts on which that program exists (empty: every host), + // and the packages it is installed from on the first run. + std::span runner; + std::span runnerHosts; + std::span runnerRequires; +}; + +// A SYSTEM (SPEC-016 §2.3): the facts of an operating system that no single +// row states. `uname` is the word `uname -s` answers there, which a file that +// carries one program per system selects by; a system without it joins no +// such file. +struct System { + std::string_view name; + std::string_view uname; }; // THE PATTERN OF `serves`: segment by segment, `*` standing for any one segment diff --git a/modules/toolchain-model/src/target_select.cppm b/modules/toolchain-model/src/target_select.cppm index 506676fd2..395e8db6d 100644 --- a/modules/toolchain-model/src/target_select.cppm +++ b/modules/toolchain-model/src/target_select.cppm @@ -31,6 +31,11 @@ struct Candidate { int priority = 0; // after the overrides bool available = false; std::string why; // why it is not available here + // The tier of the arrangement: the provider entry's, else the row's. + std::string_view tier() const { + if (provider && !provider->tier.empty()) return provider->tier; + return row ? row->tier : std::string_view{}; + } }; struct Context { @@ -128,6 +133,11 @@ Selection select(std::string_view request, std::string_view arch, std::string_vi } else if (row.tier == "planned") { c.available = false; c.why = std::format("{} is planned", row.triple); + } else if (p->tier == "planned") { + // The arrangement's own tier (SPEC-016 §3): registered intent, + // not a payload that exists. + c.available = false; + c.why = std::format("{} of {} is planned", p->name, row.triple); } s.candidates.push_back(std::move(c)); } diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 60df8b5bd..9a5f7f1b8 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -311,6 +311,17 @@ struct Triple { // -- belongs to such a target; what does is the platform's sysroot or base. bool c_library_by_platform() const; + // WHERE THE ROW'S ARTIFACT RUNS, COMPARED WITH THIS MACHINE (SPEC-016 §6), + // asked in one place. `system_is_host`: its execution system is this + // machine's -- the question a launch asks, since a kernel refuses or + // emulates a program of its own system and another architecture, and gives + // a program of another system's call numbers its own meaning for them. + // `loads_on_host`: the system and the architecture both -- the question a + // check of the artifact's loader and run-time closure asks, since those + // describe a process of this machine. + bool system_is_host() const; + bool loads_on_host() const; + // APPLE, AS ONE QUESTION. `os == "macos"` was the whole of it while macOS // was the only Apple row; iOS shares the object format, the linker, the // `arm64` spelling and `codesign`, and differs in the SDK and the @@ -693,6 +704,11 @@ inline Triple host_triple() { return t; } +inline bool Triple::system_is_host() const { return execution_system() == host_triple().os; } +inline bool Triple::loads_on_host() const { + return system_is_host() && arch == mcpp::toolchain::platform::host_arch; +} + // ── Version pins (single site; §4.6 of the design doc) ─────────────────────── // Every default/convention toolchain version literal lives here. Help and // error strings format these — never inline a pinned version elsewhere. diff --git a/modules/toolchain-model/tests/test_target_select.cpp b/modules/toolchain-model/tests/test_target_select.cpp index 17ced6af1..e2e6adf90 100644 --- a/modules/toolchain-model/tests/test_target_select.cpp +++ b/modules/toolchain-model/tests/test_target_select.cpp @@ -134,6 +134,39 @@ UTest an_arm_pattern_takes_the_standard_c_library{"TargetSelect/AnArmPatternTake expect(!s.fallback); const Ask written{"aarch64-linux-gnu", "aarch64", "linux", "gnu", true}; auto w = on("linux-aarch64", written); - expect(fatal(eq(w.candidates.size(), std::size_t(1)))); expect(eq(w.identity(), "aarch64-linux-gnu")); + expect(eq(provider_of(w), "glibc")); + expect(!w.fallback); +}}; + +// ONE ENTRY, ONE ARRANGEMENT; A NEW ENTRY CHANGES NO ANSWER (SPEC-016 §3, §4.6). +// The identity has a native arrangement and a cross one, as two entries. The +// cross entry stands below the entries that answer `aarch64-linux`, so that +// request keeps the answer it had before the entry existed, on every host, and +// only the whole identity reaches the cross arrangement. +UTest a_cross_arrangement_is_its_own_entry_and_changes_no_answer{"TargetSelect/ACrossArrangementIsItsOwnEntryAndChangesNoAnswer", [] { + const Ask written{"aarch64-linux-gnu", "aarch64", "linux", "gnu", true}; + // On its own architecture: the native entry, and the cross one is a + // candidate that host does not have. + auto native = on("linux-aarch64", written); + expect(fatal(eq(native.candidates.size(), std::size_t(2)))); + expect(eq(provider_of(native), "glibc")); + expect(bool(native.candidates[0].available)); + expect(eq(native.candidates[1].provider->name, "glibc-sysroot")); + expect(!native.candidates[1].available); + // From x86_64: the whole identity takes the cross entry, with its base. + auto cross = on("linux-x86_64", written); + expect(eq(cross.identity(), "aarch64-linux-gnu")); + expect(eq(provider_of(cross), "glibc-sysroot")); + expect(fatal(cross.provider != nullptr)); + expect(eq(cross.provider->sysroot, "xim:aarch64-linux-gnu-sysroot@2.44.3")); + expect(eq(cross.provider->linkage, "dynamic")); + expect(!cross.provider->runner.empty()); + // The request without an env is answered as before the entry existed. + auto plain = on("linux-x86_64", {"aarch64-linux", "aarch64", "linux", "gnu"}); + expect(eq(plain.identity(), "aarch64-linux-musl")); + expect(eq(provider_of(plain), "musl-gcc")); + // Elsewhere neither arrangement exists, and the answer is the graph's identity. + auto mac = on("macos-aarch64", {"aarch64-linux", "aarch64", "linux", "gnu"}); + expect(eq(mac.identity(), "aarch64-linux-musl")); }}; diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index b05648255..769ff0148 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.11.10.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.11.12.1"; } // namespace mcpp diff --git a/src/build/execute.cppm b/src/build/execute.cppm index ad4cb0065..05eb4894f 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -1356,35 +1356,44 @@ ExecutionHost locate_run_under(const mcpp::build::BuildContext& ctx, std::string return h; } -// THE PHYSICAL BOUNDARY (§3.8): an artifact no path from this machine can -// execute is refused before anything is spawned, naming the key that adds one. -// Today that is an Apple artifact on a host that is not macOS: the simulator -// and the OS run only there. `--no-runner` states that the triple is native -// here, and is believed. +// THE PHYSICAL BOUNDARY (§3.8, SPEC-016 §6): an artifact this machine does +// not execute is refused before anything is spawned, naming the key that adds +// a way. One predicate for every row, built-in or defined: the row's execution +// system against this machine's. `--no-runner` states that the triple is +// native here, and is believed. std::optional unreachable_execution(const mcpp::build::BuildContext& ctx, std::string_view tripleKey, bool noRunner, bool runnerDeclared = false) { if (noRunner || !ctx.runUnder.empty()) return std::nullopt; auto t = mcpp::toolchain::triple::parse(tripleKey); - // AN ARTIFACT OF A SYSTEM A PLATFORM DEFINES runs where its row's - // `execution` names (mcpp#911 G11): on this machine when that is this - // machine's system, else through a runner -- a runner package's program, - // which reaches a VM, a remote host or a device by the run protocol - // (docs/41) -- or an execution host. With neither it is built, not run. - if (auto const* d = t ? t->defined() : nullptr; d && !runnerDeclared) { - const std::string_view system = d->execution.empty() ? std::string_view(t->os) : d->execution; - const std::string_view here = mcpplibs::os::is_windows ? "windows" - : mcpplibs::os::is_macos ? "macos" : "linux"; - if (system != here) - return std::format( - "{} artifacts run on {}, not on this {} host: built, not run.\n" - " State how they run: [target.{}] runner = [\"\", \"{{}}\"], the program a " - "runner package builds\n" - " ([target.{}.tools] = {{ ... }}; the run protocol is docs/41), or an " - "execution host:\n" - " [target.{}] run_under = [\"remote-run\", \"\", \"--\"].", - tripleKey, system, here, tripleKey, tripleKey, tripleKey); - } + // An Apple artifact off macOS: no runner reaches it, only an execution host. + if (!mcpplibs::os::is_macos && t && t->is_apple()) + return std::format( + "cannot run {} artifacts on this host: {} runs only on macOS.\n" + " Name an execution host: [target.{}] run_under = [\"remote-run\", \"\", \"--\"]\n" + " (in mcpp.toml for the team, or ~/.mcpp/config.toml for this machine).", + tripleKey, t->is_ios_simulator() ? "the iOS Simulator" : t->is_ios() ? "iOS" : "macOS", + tripleKey); + // AN ARTIFACT OF ANOTHER SYSTEM IS NOT STARTED TO SEE WHAT HAPPENS (SPEC-016 + // §6). A row's artifacts run on its execution system -- the `execution` its + // row states, built-in or defined, else its own -- and that is this + // machine's system or it is not. Where it is not, the artifact runs through + // a runner (a provider's, a platform's, a runner package's program by the + // run protocol, docs/41) or an execution host; with neither it is built, + // not run. The kernel of this machine gives another system's call numbers + // its own meaning, so a launch is not an observation. An artifact of this + // system and another architecture is launched as before: the kernel + // refuses it or an emulator it knows runs it. + if (t && !t->is_freestanding() && !runnerDeclared && !t->system_is_host()) + return std::format( + "{} artifacts run on {}, not on this {} host: built, not run.\n" + " State how they run: [target.{}] runner = [\"\", \"{{}}\"], the program a " + "runner package builds\n" + " ([target.{}.tools] = {{ ... }}; the run protocol is docs/41), or an " + "execution host:\n" + " [target.{}] run_under = [\"remote-run\", \"\", \"--\"].", + tripleKey, t->execution_system(), mcpp::toolchain::triple::host_triple().os, + tripleKey, tripleKey, tripleKey); // A RUNNER THAT RUNS THE ARTIFACT ON A MACHINE OF ITS ARCHITECTURE // (`runner = { ..., host = "same-arch" }`, SPEC-013): an ok image's loader // is a program of this machine, and an image of another architecture is @@ -1402,14 +1411,7 @@ std::optional unreachable_execution(const mcpp::build::BuildContext tripleKey, mcpp::toolchain::platform::host_arch, r.cmd.empty() ? std::string() : r.cmd.front(), mcpp::platform::origin_text(r.origin), tripleKey); - if (mcpplibs::os::is_macos) return std::nullopt; - if (!t || !t->is_apple()) return std::nullopt; - return std::format( - "cannot run {} artifacts on this host: {} runs only on macOS.\n" - " Name an execution host: [target.{}] run_under = [\"remote-run\", \"\", \"--\"]\n" - " (in mcpp.toml for the team, or ~/.mcpp/config.toml for this machine).", - tripleKey, t->is_ios_simulator() ? "the iOS Simulator" : t->is_ios() ? "iOS" : "macOS", - tripleKey); + return std::nullopt; } // Compile a prepared BuildContext. Shared between `mcpp build` and `mcpp run` @@ -1448,6 +1450,14 @@ struct RunnerChoice { bool ignored = false; // --no-runner dropped a declared template bool longLived = false; // declared by the package; no natural end bool fromPayload = false; // the toolchain payload's runner, nothing declared + // The packages the runner is installed from that do not stand on this + // machine (SPEC-016 §6): a run or a test installs them and asks again; a + // build installs nothing. + std::vector missing; + // A place the template names that this build does not have (`{sysroot}` + // with no base, `{pkg:}` outside `requires`): the launch is refused + // naming it, since the word would reach the runner as it is written. + std::string unplaced; // The spelling that names this target in the manifest: the canonical form, // which is also the output directory's name and the key every // `[target.]` reader resolves. Every diagnostic below prints this @@ -1459,6 +1469,64 @@ struct RunnerChoice { std::string tripleKey; }; +// THE PLACES A RUNNER'S TEMPLATE NAMES (SPEC-016 §6). `{sysroot}` is the base +// the artifact was linked against (a user-mode emulator's root); `{pkg:}` +// the root of the package of that name among `requires` (an image, a firmware +// file); the template's program is taken from a required package before PATH. +// Packages are located where they stand, and the ones that do not stand are +// named for the caller: what installs them is a run, never this lookup. +void settle_runner_places(const BuildContext& ctx, RunnerChoice& c, + std::span requires_) { + std::map roots; + for (auto const& ref : requires_) { + auto name = ref.substr(ref.find(':') == std::string::npos ? 0 : ref.find(':') + 1); + name = name.substr(0, name.find('@')); + if (auto dir = mcpp::build::package_root(ref, false)) roots[name] = *dir; + else c.missing.push_back(ref); + } + if (c.tmpl.empty()) return; + for (auto const& [name, root] : roots) { + auto found = mcpp::build::runner_lookup::locate( + c.tmpl.front(), mcpp::build::runner_lookup::payload_search_dirs(root), {}); + if (!found.program) continue; + c.tmpl.front() = found.program->string(); + break; + } + auto place = [](std::string& word, std::string_view key, const std::string& value) { + for (auto at = word.find(key); at != std::string::npos; at = word.find(key, at + value.size())) + word.replace(at, key.size(), value); + }; + for (auto& word : c.tmpl) { + if (!ctx.tc.base.root.empty()) place(word, "{sysroot}", ctx.tc.base.root.generic_string()); + for (auto const& [name, root] : roots) + place(word, std::format("{{pkg:{}}}", name), root.generic_string()); + // What is left after the packages stand is a place nothing gives. + if (!c.unplaced.empty() || !c.missing.empty()) continue; + if (word.find("{sysroot}") != std::string::npos) c.unplaced = "{sysroot}"; + else if (auto at = word.find("{pkg:"); at != std::string::npos) + c.unplaced = word.substr(at, word.find('}', at) == std::string::npos + ? std::string::npos : word.find('}', at) - at + 1); + } +} + +// The refusal of a template whose place this build does not have. +std::string unplaced_message(const RunnerChoice& c) { + return std::format( + "the runner of '{}' names {}, and this build has no such place: `{{sysroot}}` is the base " + "the target is linked against, `{{pkg:}}` a package of the runner's `requires`", + c.tripleKey, c.unplaced); +} + +// What a run or a test does about `missing`: installs each through the channel +// a toolchain pin uses, with the same consent. Empty: all stand. +std::string install_runner_packages(const RunnerChoice& c) { + for (auto const& ref : c.missing) + if (auto dir = mcpp::build::package_root(ref, true); !dir) + return std::format("the runner of '{}' is installed from {}, and {}", c.tripleKey, ref, + dir.error()); + return {}; +} + // ONE READER FOR FOUR SLOTS, PARAMETERISED BY THE SLOT. // // `run`, `flash`, `monitor` and `debug` resolve identically: a dependency @@ -1493,6 +1561,7 @@ RunnerChoice choose_device_action(const BuildContext& ctx, c.tmpl = pf.runner.cmd; c.fromManifest = overrides_program(pf.runner.origin, !ctx.manifest.buildConfig.runner.empty()); + settle_runner_places(ctx, c, pf.runner.requires_); } } else if (auto it = pf.runners.find(std::string(which)); it != pf.runners.end()) { c.tmpl = it->second.value; @@ -3294,7 +3363,18 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, // while debugging, or naming `qemu-aarch64-static` for a cross target. const bool isRunSlot = runner_name.empty(); const std::string slotName{runner_name}; - const auto choice = choose_device_action(ctx, runner_name, no_runner); + auto choice = choose_device_action(ctx, runner_name, no_runner); + if (!choice.missing.empty()) { + if (auto why = install_runner_packages(choice); !why.empty()) { + std::println(libc::stderr(), "error: {}", why); + return 2; + } + choice = choose_device_action(ctx, runner_name, no_runner); + } + if (!choice.unplaced.empty()) { + std::println(libc::stderr(), "error: {}", unplaced_message(choice)); + return 2; + } if (choice.ignored) mcpp::ui::info("note", std::format( "--no-runner: ignoring the runner declared for {}", choice.tripleKey)); @@ -4640,6 +4720,23 @@ static void test_bulk(TestBuild& tb, const TestOptions& testOpts, Keep&& keep) { static void test_resolve_runner(TestBuild& tb, const TestOptions& testOpts, bool json) { auto* ctx = &*tb.ctx; tb.runnerChoice = choose_device_action(*ctx, testOpts.runnerName, testOpts.noRunner); + if (!tb.runnerChoice.missing.empty()) { + // A package that cannot be installed leaves the tests not run, said + // once for the build as a missing program is. + if (auto why = install_runner_packages(tb.runnerChoice); !why.empty()) { + tb.invocationNotRunReason = why; + std::println(libc::stderr(), "error: {}", why); + tb.runnerFatal = 2; + return; + } + tb.runnerChoice = choose_device_action(*ctx, testOpts.runnerName, testOpts.noRunner); + } + if (!tb.runnerChoice.unplaced.empty()) { + tb.invocationNotRunReason = unplaced_message(tb.runnerChoice); + std::println(libc::stderr(), "error: {}", tb.invocationNotRunReason); + tb.runnerFatal = 2; + return; + } auto& runnerChoice = tb.runnerChoice; if (!testOpts.runnerName.empty() && runnerChoice.tmpl.empty()) { tb.invocationNotRunReason = std::format("no runner named '{}' for '{}'", diff --git a/src/build/flags.cppm b/src/build/flags.cppm index d0363ef51..ad027b2fe 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -1983,15 +1983,17 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // compiler runtime stands (`compiler_runtime_dir`: clang reads it as its // resource directory, gcc through `-B`), and, when it supplies the C++ // layer, its `ldflags` in place of the driver's own C++ library, which - // names the toolchain's. - auto base_link_words = [&] { + // names the toolchain's. The run-time directory is as much a C link's as a + // C++ one's (`kLinkDriverFlagsC` states the same of the driver's own + // selection); the C++ library's words belong to the C++ link alone. + auto base_link_words = [&](bool cxx) { std::string words; auto const& L = plan.toolchain.base.layers; if (!L.compilerRuntimeDir.empty()) words += plan.toolchain.compiler == mcpp::toolchain::CompilerId::Clang ? " -resource-dir=" + ninjaEsc(L.compilerRuntimeDir) : " -B" + ninjaEsc(L.compilerRuntimeDir); - if (!L.cxx.empty() && !L.ldflags.empty()) { + if (cxx && !L.cxx.empty() && !L.ldflags.empty()) { words += " -nostdlib++"; for (auto const& w : L.ldflags) words += " " + ninja_command_word(w); } @@ -2065,11 +2067,12 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { const std::string baseStatic = !plan.toolchain.pairedSysroot.empty() && !layers.cxx.empty() && f.linkage == "static" ? " -static" : ""; - f.ld += baseStatic + base_link_words(); + f.ld += baseStatic + base_link_words(true); // `-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("{}{}{}{}{}{}", crossTarget, paired, link_intent_ld, user_ldflags, link_extra, baseStatic); + f.ldC += base_link_words(false); return f; } @@ -2699,7 +2702,8 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { } // A BASE PACKAGE'S RUN-TIMES (design 2026-11-10 E5). - f.ld += base_link_words(); + f.ld += base_link_words(true); + f.ldC += base_link_words(false); // A LINKER STATED BY ROLE (mcpp#755, `tools = { ld = ... }`), AFTER EVERY // SHAPE HAS BUILT ITS LINE. diff --git a/src/build/hermetic.cppm b/src/build/hermetic.cppm index e63bed78f..85037f3ea 100644 --- a/src/build/hermetic.cppm +++ b/src/build/hermetic.cppm @@ -245,9 +245,9 @@ std::expected verify_hermetic_link( // invited the reader to reinstall a glibc payload that has nothing to do // with it. if (auto tt = mcpp::toolchain::triple::parse(tc.targetTriple); - tt && (tt->has_own_sysroot() || tt->c_library_by_platform())) { + tt && (!tt->loads_on_host() || tt->c_library_by_platform())) { mcpp::log::verbose("hermetic", std::format( - "target {} carries its own sysroot; its loader ({}) is a path on " + "target {} is not loaded by this machine; its loader ({}) is a path on " "the target and is not checked against the sandbox", tc.targetTriple, effectiveLoader)); } else if (!effectiveLoader.empty()) { diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 31f35bbb1..b21a703de 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -467,6 +467,7 @@ export struct TargetCandidate { std::string identity, provider, why; int priority = 0; bool available = false; + std::string tier; // of the arrangement: the provider entry's, else the row's }; export struct TargetSelection { @@ -803,6 +804,12 @@ export std::string profile_override_from_flags(std::string_view profileOption, // Command-level overrides (--target / --static). // Empty defaults preserve pre-existing behaviour exactly. +// THE ROOT OF A PACKAGE A STATEMENT NAMES (`xim:@`), for the +// readers outside prepare (a runner's packages, SPEC-016 §6): where the payload +// stands, installed first when `install` and it does not stand. +export std::expected +package_root(const std::string& ref, bool install); + export struct BuildOverrides { // Where the package being built LIVES (its mcpp.toml). Empty = walk up from // the process cwd, which is what every user-facing invocation does. Set by diff --git a/src/build/prepare/base.cpp b/src/build/prepare/base.cpp index 51795fd10..1ce36bc39 100644 --- a/src/build/prepare/base.cpp +++ b/src/build/prepare/base.cpp @@ -27,6 +27,7 @@ import mcpp.toolchain.base; import mcpp.toolchain.base_probe; import mcpp.toolchain.triple; import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_select; import mcpp.toolchain.target_schema; import mcpp.ui; import mcpp.invocation; @@ -285,18 +286,23 @@ bool provides_c_abi(const tb::Statement& s, std::string* impl) { return false; } -// The directory a package statement names, installed through the channel a -// row's toolchain pin uses (and `MCPP_NO_AUTO_INSTALL` / offline are the -// fetcher's to decide, as they are there). +// The directory a package statement names: where its payload stands, and, +// when `install`, installed through the channel a row's toolchain pin uses +// (`MCPP_NO_AUTO_INSTALL` and offline are the fetcher's to decide, as they +// are there). One resolution for every statement that names a package -- a +// base, the system's headers, a runner's packages. std::expected -package_dir(PrepareState& state, const std::string& ref) { - auto cfg = state.get_cfg(true); - if (!cfg) return std::unexpected(cfg.error()); - mcpp::fetcher::Fetcher f(**cfg); +package_dir_in(const mcpp::config::GlobalConfig& cfg, const std::string& ref, bool install) { + auto xl = mcpp::config::make_xlings_env(cfg); + if (!install) { + if (auto d = mcpp::provider::payload(*mcpp::provider::open(xl), mcpp::provider::parse_ref(ref))) + return *d; + return std::unexpected(std::format("the package {} is not installed", ref)); + } + mcpp::fetcher::Fetcher f(cfg); mcpp::fetcher::InstallProgressHandler progress; if (auto p = f.resolve_xpkg_path(ref, mcpp::fetcher::PayloadIntent::Use, &progress)) return p->root; - auto xl = mcpp::config::make_xlings_env(**cfg); if (auto d = mcpp::provider::payload(*mcpp::provider::open(xl), mcpp::provider::parse_ref(ref))) return *d; return std::unexpected(std::format("the package {} could not be installed", ref)); @@ -304,6 +310,36 @@ package_dir(PrepareState& state, const std::string& ref) { } // namespace +std::expected +package_dir(PrepareState& state, const std::string& ref, bool install) { + auto cfg = state.get_cfg(true); + if (!cfg) return std::unexpected(cfg.error()); + return package_dir_in(**cfg, ref, install); +} + +std::expected package_root(const std::string& ref, bool install) { + auto cfg = mcpp::config::load_or_init(/*quiet=*/true); + if (!cfg) return std::unexpected(cfg.error().message); + return package_dir_in(*cfg, ref, install); +} + +const mcpp::toolchain::target::Provider* chosen_provider(const PrepareState& state) { + if (state.targetChoice.provider.empty()) return nullptr; + const auto host = mcpp::toolchain::target::host_pair(mcpp::toolchain::platform::name, + mcpp::toolchain::platform::host_arch); + const mcpp::toolchain::target::Provider* named = nullptr; + for (auto const* p : mcpp::toolchain::target::providers()) { + if (p->name != state.targetChoice.provider + || !mcpp::toolchain::target::serves(*p, state.targetChoice.identity)) + continue; + const auto dash = state.targetChoice.identity.find('-'); + if (mcpp::toolchain::target::available_on(*p, host, state.targetChoice.identity.substr(0, dash))) + return p; + if (!named) named = p; + } + return named; +} + // THE ONE RESOLUTION (§4.2 steps 5-7) for the rows whose base the engine // renders itself: a gnu-sysroot (Linux), and the identity and report of every // other stated base. The Apple, MSVC and multilib rows locate theirs where @@ -410,7 +446,7 @@ std::expected step2_resolve_base(PrepareState& state) { && (stmt->form == tb::Form::Package || stmt->form == tb::Form::Path)) { fs::path dir = stmt->form == tb::Form::Path ? fs::path(stmt->path) : fs::path{}; if (stmt->form == tb::Form::Package) { - auto p = package_dir(state, stmt->text); + auto p = package_dir(state, stmt->text, true); if (!p) return std::unexpected(std::format("[target.{}].sysroot = \"{}\": {}", tt->str(), stmt->text, p.error())); dir = *p; @@ -460,15 +496,17 @@ std::expected step2_resolve_base(PrepareState& state) { return std::unexpected(missing_path_message(*tt, *stmt)); } } else { - auto p = package_dir(state, stmt->text); + auto p = package_dir(state, stmt->text, true); if (!p) return std::unexpected(std::format("[target.{}].sysroot = \"{}\": {}", tt->str(), stmt->text, p.error())); dir = *p; } tc.base.layers = {}; - if (auto b = read_base_package(dir, tc.base.layers); !b) { + const auto basePackage = read_base_package(dir, tc.base.layers); + if (!basePackage) { refusal::record(refusal::Code::TargetBase); - return std::unexpected(std::format("[target.{}].sysroot = \"{}\": {}", tt->str(), stmt->text, b.error())); + return std::unexpected(std::format("[target.{}].sysroot = \"{}\": {}", tt->str(), stmt->text, + basePackage.error())); } const auto ecosystemLinux = tc.base.payload ? tc.base.payload->linuxInclude : fs::path{}; const bool native = tt->os == "linux" && tt->arch == mcpp::toolchain::platform::host_arch @@ -481,7 +519,11 @@ std::expected step2_resolve_base(PrepareState& state) { std::vector evidence; std::vector hostLibcDirs; std::string impl; - if (tb::is_payload_layout(dir)) { + // A BASE PACKAGE HAS ONE READING (SPEC-002 §2.5): its manifest states that + // the package root is the sysroot, so it is rendered as one whatever its + // directories are called. The layout is consulted only for a directory + // that states nothing about itself. + if (!*basePackage && tb::is_payload_layout(dir)) { // The ecosystem's own layout: a glibc package (`xim:glibc@2.39`). It // is linked as the default base is, from its own directories. tc.base.payload = tb::payload_layout_paths(dir, ecosystemLinux); @@ -538,6 +580,22 @@ std::expected step2_resolve_base(PrepareState& state) { && !fs::exists(dir / "usr" / "include" / "linux" / "limits.h", ec)) tc.base.addedIncludes.push_back(ecosystemLinux); } + + // THE LOADER AN ARTIFACT NAMES IS A PATH ON THE MACHINE THAT RUNS IT + // (SPEC-016 §6). A native dynamic link names the base's own loader, where + // it stands; an artifact this machine does not load names the row's + // `loader`, and no directory of this machine enters it. + tc.base.interpreter.clear(); + if (!native && !staticLink) + if (auto const* r = mcpp::toolchain::target::find_row(tt->str())) + tc.base.interpreter = std::string(r->loader); + // THE PAYLOAD'S RUN-TIME DIRECTORIES ARE ITS HOST'S. A retargetable driver + // discovers them for the machine it runs on (`lib/`), and a + // base of another machine takes none of them: on the link they precede the + // base's own (measured: the x86_64 libatomic.a ahead of the sysroot's on an + // aarch64 link), and in the artifact they are a RUNPATH of this machine. + // Decided here, where the base is resolved, so no renderer asks again. + if (!native && mcpp::toolchain::is_clang(tc)) tc.linkRuntimeDirs.clear(); // A base package's own `provides` is its declaration, as a statement's is. if (tc.base.libc.empty() && !tc.base.layers.cAbi.empty()) tc.base.libc = tc.base.layers.cAbi; // ITS C++ RUN-TIME STANDS WHERE THE PAYLOAD'S WOULD (E5): the std module is diff --git a/src/build/prepare/config.cpp b/src/build/prepare/config.cpp index d55e58190..1dd4a485f 100644 --- a/src/build/prepare/config.cpp +++ b/src/build/prepare/config.cpp @@ -342,6 +342,10 @@ void merge_conditional_config(mcpp::manifest::Manifest& m, m.buildConfig.abiExceptionsModel = cc.abiExceptionsModel; m.buildConfig.abiExceptionsDeclared = true; } + if (cc.abiLongjmpDeclared) { + m.buildConfig.abiLongjmpModel = cc.abiLongjmpModel; + m.buildConfig.abiLongjmpDeclared = true; + } // `[target..build] dialect_cxxflags` (#717): recorded for every // package, like the abi switches above, but APPENDED rather than // replaced -- there is no "last matching section wins" here, because diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index 4a76a7553..0621cc8dd 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -2214,7 +2214,7 @@ std::expected phase13_finish(PrepareState& state) { for (auto const& k : c.candidates) s.candidates.push_back({ k.row ? std::string(k.row->triple) : std::string{}, k.provider ? std::string(k.provider->name) : std::string{}, - k.why, k.priority, k.available }); + k.why, k.priority, k.available, std::string(k.tier()) }); } if (!state.runUnder.empty()) { auto const& o = state.runUnderOrigin; diff --git a/src/build/prepare/platform.cpp b/src/build/prepare/platform.cpp index 2fa18e722..9c2da5519 100644 --- a/src/build/prepare/platform.cpp +++ b/src/build/prepare/platform.cpp @@ -215,22 +215,26 @@ void define_platform(const mp::Statement& s, std::string_view shortName, std::st r.defined = true; set.rows.push_back(r); } - auto provider = [&](std::string pname, const std::vector& serves, int priority, - std::string_view toolchain, const std::vector& hosts, - const std::vector& pairs, std::string_view sysroot, - std::string_view linkage) { + auto provider = [&](const mp::StatedProvider& sp) { target::Provider p; - p.name = set.keep(std::move(pname)); - p.serves = set.keep(serves); - p.priority = priority; - p.hosts = set.keep(hosts.empty() ? std::vector{"*"} : hosts); - p.pairs = set.keep(pairs); - p.toolchain = set.keep(std::string(toolchain)); - p.sysroot = set.keep(std::string(sysroot)); - p.linkage = set.keep(std::string(linkage)); + p.name = set.keep(sp.name); + p.serves = set.keep(sp.serves); + p.priority = sp.priority; + p.hosts = set.keep(sp.hosts.empty() ? std::vector{"*"} : sp.hosts); + p.pairs = set.keep(sp.pairs); + p.toolchain = set.keep(sp.toolchain); + p.sysroot = set.keep(sp.sysroot); + p.linkage = set.keep(sp.linkage); p.platform = set.keep(std::string(ref)); + p.tier = set.keep(sp.tier); + p.systemHeaders = set.keep(sp.systemHeaders); + p.runner = set.keep(sp.runner); + p.runnerHosts = set.keep(sp.runnerHosts); + p.runnerRequires = set.keep(sp.runnerRequires); set.providers.push_back(p); }; + for (auto const& sys : s.systems) + set.systems.push_back({ set.keep(sys.name), set.keep(sys.uname) }); // A platform the engine's own providers name (openkal-platform) is served by // those; its shorthands are not registered twice. const bool builtin = std::ranges::any_of(mcpp::toolchain::builtin::kProviders, [&](auto const& b) { @@ -238,8 +242,7 @@ void define_platform(const mp::Statement& s, std::string_view shortName, std::st return r && *r && (*r)->spec.shortName == shortName; }); for (auto const& p : mp::stated_providers(s, name)) - if (!p.shorthand || !builtin) - provider(p.name, p.serves, p.priority, p.toolchain, p.hosts, p.pairs, p.sysroot, p.linkage); + if (!p.shorthand || !builtin) provider(p); target::define(std::move(set)); } @@ -996,7 +999,26 @@ mp::Statement platform_statement(const PrepareState& state, std::vectorrunner.empty()) { + const auto host = mcpp::toolchain::target::host_pair(mcpp::toolchain::platform::name, + mcpp::toolchain::platform::host_arch); + const bool here = p->runnerHosts.empty() + || std::ranges::any_of(p->runnerHosts, [&](std::string_view h) { + return mcpp::toolchain::target::host_matches(h, host, t.arch); + }); + if (here) { + s.runner.cmd.assign(p->runner.begin(), p->runner.end()); + s.runner.requires_.assign(p->runnerRequires.begin(), p->runnerRequires.end()); + s.runner.origin = mp::Origin{ mp::Tier::BuiltIn, {}, 0, + std::format("the provider '{}' of {}", p->name, state.targetChoice.identity) }; + s.runner.set = true; + } + } + if (P.active) mcpp::platform::merge(s, state.platformData); mp::Statement program = state.programPlatform; // The root's own program states them as well (applied into its manifest); // it is the last program to run, and it speaks for the project. diff --git a/src/build/prepare/state.cppm b/src/build/prepare/state.cppm index b69abbd96..66559eccd 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -42,6 +42,7 @@ import mcpp.toolchain.stdmod; import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; +import mcpp.toolchain.target_schema; import mcpp.toolchain.target_select; import mcpp.toolchain.base; import mcpp.build.linkage_form; // #519 — which form each dependency takes @@ -1156,6 +1157,14 @@ base_statement(PrepareState& state, const mcpp::toolchain::triple::Triple& t); const std::string* base_statement_text(PrepareState& state, const mcpp::toolchain::triple::Triple& t); std::expected step2_resolve_base(PrepareState& state); +// The directory a package statement names (`xim:@`): where its +// payload stands, installed first when `install` and it does not stand. +std::expected +package_dir(PrepareState& state, const std::string& ref, bool install); +// The provider entry the selection took: the one of that name that serves the +// identity, the entry available on this host before one that is not. Null when +// no provider serves it. +const mcpp::toolchain::target::Provider* chosen_provider(const PrepareState& state); // The execution host's wrapper (`run_under`), decided once (§3.7). std::expected step2_resolve_run_under(PrepareState& state); diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index 859aec7bc..ce240a045 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -1428,6 +1428,47 @@ static std::expected step1_device_axis_and_layer_merge(Prepar state.add_once(state.m->buildConfig.ldflags, flag); state.add_once(state.m->buildConfig.graphLdflags, flag); } + // `[target..abi] longjmp` -- the third member (2026.11.12.1): + // how `sigsetjmp`/`siglongjmp` cross the WebAssembly boundary. The flag + // changes the CODE emcc emits for the two calls, so it reaches both + // sides -- every translation unit's flags (C and C++ alike; the C library + // calls are what it is about) and the link, the channels `threads` uses. + // Declared on any row but Emscripten's it is refused, not ignored: the + // member names a WebAssembly model, and a build that wrote it for another + // target has a selector that matches the wrong row. + if (state.m->buildConfig.abiLongjmpDeclared) { + if (!emscripten) + return std::unexpected(std::format( + "[target..abi] longjmp states a WebAssembly long-jump model, " + "and the target '{}' is not an Emscripten row; the member belongs to " + "`[target.'cfg(os = \"emscripten\")'.abi]` alone", + state.overrides.target_triple.empty() + ? mcpp::toolchain::triple::host_triple().str() + : state.overrides.target_triple)); + // THE TWO MEMBERS NAME ONE MECHANISM (design 2026-11-12 D6): WebAssembly's + // exception handling carries both the throw and the long jump, and the + // JavaScript one carries both. A build that states one model for + // exceptions and the other for long jumps is refused here, with both + // statements, rather than by the SDK's driver after the compiles. + // Unstated, the long-jump model is the SDK's own default, which + // follows the exception model. + const bool wasmLongjmp = state.m->buildConfig.abiLongjmpModel == "wasm"; + if (abiExceptionsRendered + && wasmLongjmp != (state.m->buildConfig.abiExceptionsModel == "wasm")) + return std::unexpected(std::format( + "[target..abi] longjmp = \"{}\" and exceptions = \"{}\" name two " + "unwinding mechanisms for one program: state the same model for both, or leave " + "`longjmp` unstated and it follows `exceptions`", + state.m->buildConfig.abiLongjmpModel, + state.m->buildConfig.abiExceptionsModel.empty() + ? std::string("js") : state.m->buildConfig.abiExceptionsModel)); + const std::string flag = "-sSUPPORT_LONGJMP=" + + (wasmLongjmp ? std::string("wasm") : std::string("emscripten")); + state.add_once(state.m->buildConfig.dialectCxxflags, flag); + state.add_once(state.m->buildConfig.cflags, flag); + state.add_once(state.m->buildConfig.ldflags, flag); + state.add_once(state.m->buildConfig.graphLdflags, flag); + } // #851: emcc runs wasm-opt and minifies the JavaScript launcher AT THE // LINK, from the optimisation level the link is given; a link without // one is -O0 whatever the compiles were. The profile's level reaches it. diff --git a/src/build/prepare/windows_resources.cpp b/src/build/prepare/windows_resources.cpp index 4039134a0..48c37ee63 100644 --- a/src/build/prepare/windows_resources.cpp +++ b/src/build/prepare/windows_resources.cpp @@ -30,6 +30,8 @@ import mcpp.toolchain.stdmod; import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_schema; import mcpp.build.linkage_form; // #519 — which form each dependency takes import mcpp.build.plan; import mcpp.build.schedule.policy; @@ -72,6 +74,38 @@ import mcpp.bmi_cache; namespace mcpp::build { +// THE SYSTEM'S OWN HEADERS, FOR A ROW WHOSE TOOLCHAIN STATES NONE (SPEC-016 +// §3, `system-headers`). A row whose system the graph supplies has no paired +// payload, and its compiler carries no headers of the system, so a resource +// script's `winres.h` reaches neither of the include sources the toolchain +// states. The provider the selection took names the package that has them. +// +// ONE QUESTION, ASKED BY BOTH STEPS THAT READ THE ANSWER: is the script +// compiled by llvm-rc (which reads no INCLUDE of its own) for a row that +// states no header source. A row that states its headers -- the SDK's, a +// paired payload's -- resolves nothing, so preparing it installs no package +// its build does not read. Where the provider names a package and it cannot +// be installed, the build is refused with the package's name: a resource +// compile left to fail on a missing header would name neither. +static bool resource_headers_wanted(const mcpp::toolchain::Toolchain& tc, + const mcpp::build::resources::RcTool* tool, + std::string_view dialectId, bool msvcStyle) { + return tool && msvcStyle && tool->name().find("llvm-rc") != std::string::npos + && tc.base.msvc.sdkRoot.empty() + && mcpp::build::resources::system_include_dirs(tc, dialectId).empty(); +} + +static std::expected system_headers_root(PrepareState& state) { + auto const* p = chosen_provider(state); + if (!p || p->systemHeaders.empty()) return std::filesystem::path{}; + auto dir = package_dir(state, std::string(p->systemHeaders), true); + if (!dir) + return std::unexpected(std::format( + "the provider '{}' of {} takes the system's headers from {}, and {}", + p->name, state.targetChoice.identity, p->systemHeaders, dir.error())); + return *dir / "include"; +} + // The resource compiler of the plan's resource units, and each unit's flags. static std::expected step13_resource_compiler(PrepareState& state, BuildContext& ctx) { @@ -100,9 +134,17 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { ctx.plan.rcPath = tool->path; ctx.plan.rcStyle = tool->style; + // Below the toolchain's own include sources, never beside them. + std::filesystem::path systemHeaders; + if (resource_headers_wanted(*state.tc, &*tool, dialectId, msvcStyle)) { + auto headers = system_headers_root(state); + if (!headers) return std::unexpected(headers.error()); + systemHeaders = *headers; + } + // UTF-8 input, always. `[package]` metadata is user text and // routinely non-ASCII; without this llvm-rc refuses the script - // outright ("Non-ASCII 8-bit codepoint can't be interpreted in + // outright ("Non-ASCII 8-bit codepage can't be interpreted in // the current codepage") rather than mangling it, so a project // with a Chinese description could not build at all. // @@ -116,7 +158,7 @@ step13_resource_compiler(PrepareState& state, BuildContext& ctx) { const std::string ip = msvcStyle ? "/I" : "-I"; std::vector systemIncludes; if (msvcStyle && tool->name().find("llvm-rc") != std::string::npos) - for (auto const& dir : rsrc::system_include_dirs(*state.tc, dialectId)) + for (auto const& dir : rsrc::system_include_dirs(*state.tc, dialectId, systemHeaders)) systemIncludes.push_back(ip + dir.string()); for (auto& ru : ctx.plan.resourceUnits) { if (auto target = rsrc::coff_target_flag(*tool, state.tc->targetTriple); @@ -161,6 +203,54 @@ static std::vector resource_subjects(PrepareState& state, BuildContext& return subjects; } + +// ─── The per-subject manifest policy: code page and long paths ──────────── +// +// The predicates step13 applies to every subject, as functions so the gate's +// 400-line budget holds (split at the boundary the block already had). + +static bool subject_declares_own_manifest(const mcpp::manifest::Manifest& M, + const Subject& S, bool hostToolBuild) +{ + return hostToolBuild + && std::ranges::any_of(M.resources.files, [&](const auto& f) { + const auto abs = (f.is_absolute() ? f : (S.dir / f)).lexically_normal(); + return mcpp::build::resources::scan_rc(abs).declaresManifest; + }); +} + +static std::string_view target_code_page(const mcpp::manifest::Target& t, + bool hostToolBuild, bool ownManifest) +{ + if (!t.windowsCodePage.empty()) return t.windowsCodePage; + return (hostToolBuild && t.is_program() && !ownManifest) ? "utf-8" : "legacy"; +} + +// `windows_long_paths` (design 2026-10-16 §11), with the same default: a +// host tool is long-path aware unless it says otherwise. +static bool target_long_paths(const mcpp::manifest::Target& t, + bool hostToolBuild, bool ownManifest) +{ + const auto& stated = t.windowsLongPaths; + if (stated) return *stated; + return hostToolBuild && t.is_program() && !ownManifest; +} + +// A test program carries the code page `[test] windows_code_page` gave it, +// and nothing by default: the host-tool default is a program's. +static bool target_takes_code_page(const mcpp::manifest::Target& t) +{ + return t.is_program() || t.kind == mcpp::manifest::Target::TestBinary; +} + +// Whether an image carries an application manifest of mcpp's: a UTF-8 code +// page or long paths asked for. +static bool target_manifested(const mcpp::manifest::Target& t, + bool hostToolBuild, bool ownManifest) +{ + return target_code_page(t, hostToolBuild, ownManifest) == "utf-8" + || target_long_paths(t, hostToolBuild, ownManifest); +} std::expected step13_windows_resources(PrepareState& state, BuildContext& ctx) { // ─── Windows resources: [resources] → a tracked link input (mcpp#365) ── // @@ -200,6 +290,13 @@ std::expected step13_windows_resources(PrepareState& state, B // windres), found as the compile step below finds it (E3). const auto rcTool = mcpp::build::resources::find_rc_tool(*state.tc, dialectId); const bool msvcStyle = rcTool ? rcTool->style == "msvc" : dialectId == "msvc"; + namespace rsrc = mcpp::build::resources; + std::filesystem::path systemHeaders; + if (resource_headers_wanted(*state.tc, rcTool ? &*rcTool : nullptr, dialectId, msvcStyle)) { + auto headers = system_headers_root(state); + if (!headers) return std::unexpected(headers.error()); + systemHeaders = *headers; + } for (auto const& S : subjects) { const auto& M = *S.m; @@ -210,32 +307,16 @@ std::expected step13_windows_resources(PrepareState& state, B && (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary); }); }; - const bool ownManifest = hostToolBuild - && std::ranges::any_of(M.resources.files, [&](const auto& f) { - const auto abs = (f.is_absolute() ? f : (S.dir / f)).lexically_normal(); - return mcpp::build::resources::scan_rc(abs).declaresManifest; - }); - auto codePageOf = [&](const mcpp::manifest::Target& t) -> std::string_view { - if (!t.windowsCodePage.empty()) return t.windowsCodePage; - return (hostToolBuild && t.is_program() && !ownManifest) ? "utf-8" : "legacy"; + const bool ownManifest = subject_declares_own_manifest(M, S, hostToolBuild); + auto codePageOf = [&](const mcpp::manifest::Target& t) { + return target_code_page(t, hostToolBuild, ownManifest); }; - // `windows_long_paths` (design 2026-10-16 §11), with the same - // defaults: a host tool is long-path aware unless it says otherwise. auto longPathsOf = [&](const mcpp::manifest::Target& t) { - const auto& stated = t.windowsLongPaths; - if (stated) return *stated; - return hostToolBuild && t.is_program() && !ownManifest; - }; - // A test program carries the code page `[test] windows_code_page` - // gave it, and nothing by default: the host-tool default is a - // program's. - auto takesCodePage = [](const mcpp::manifest::Target& t) { - return t.is_program() || t.kind == mcpp::manifest::Target::TestBinary; + return target_long_paths(t, hostToolBuild, ownManifest); }; - // Whether an image carries an application manifest of mcpp's: a - // UTF-8 code page or long paths asked for. + auto takesCodePage = target_takes_code_page; auto manifested = [&](const mcpp::manifest::Target& t) { - return codePageOf(t) == "utf-8" || longPathsOf(t); + return target_manifested(t, hostToolBuild, ownManifest); }; const bool anyUtf8Image = std::ranges::any_of(M.targets, [&](const auto& t) { return active_target(t) && takesCodePage(t) && manifested(t); @@ -385,7 +466,7 @@ std::expected step13_windows_resources(PrepareState& state, B // quoted `"winres.h"` is the toolchain's header, not a file // a build action produces (E3). auto scanIncludes = rcIncludes; - for (auto const& d : rsrc::system_include_dirs(*state.tc, dialectId)) + for (auto const& d : rsrc::system_include_dirs(*state.tc, dialectId, systemHeaders)) scanIncludes.push_back(d); auto scan = rsrc::scan_rc(rcSrc, scanIncludes); if (scan.versionInfoNamedByString) { diff --git a/src/build/resources.cppm b/src/build/resources.cppm index 143c1fd55..8aa61cc84 100644 --- a/src/build/resources.cppm +++ b/src/build/resources.cppm @@ -100,8 +100,15 @@ std::string coff_target_flag(const RcTool& tool, std::string_view targetTriple); // MSVC backend states, and on a GNU-ABI row the paired payload's (or a cross // gcc's own) `-w64-mingw32/include` (design 2026-11-10 E3). One list for the // compile's `/I` and for the scan of what the script reads. +// +// `system_headers` is the third source: the include directory of the package +// the selected provider names for the system's own headers (`system-headers`, +// SPEC-016 §3), for a row whose toolchain states none. It is read ONLY when +// the two sources above found nothing, so a GNU-ABI row keeps its paired +// payload's headers and an MSVC row its SDK's. std::vector system_include_dirs(const mcpp::toolchain::Toolchain& tc, - std::string_view dialectId); + std::string_view dialectId, + const std::filesystem::path& system_headers = {}); // Split a Windows environment list (PATH, INCLUDE, LIB) into its entries. // @@ -205,7 +212,8 @@ bool is_llvm_windres(const std::filesystem::path& p) { } std::vector system_include_dirs(const mcpp::toolchain::Toolchain& tc, - std::string_view dialectId) { + std::string_view dialectId, + const std::filesystem::path& system_headers) { std::vector out; for (auto const& ev : tc.envOverrides) { if (ev.key != "INCLUDE") continue; @@ -233,6 +241,16 @@ std::vector system_include_dirs(const mcpp::toolchain::To // built, not where it is). add_triple_dirs(tc.binaryPath.parent_path().parent_path()); } + // THE THIRD SOURCE, ONLY WHERE THE TWO ABOVE FOUND NOTHING: a PE row whose + // system the graph supplies has neither a paired payload nor a cross gcc, + // and its llvm payload carries no Windows headers -- but the MinGW-w64 + // sysroot payload the rows declare does, with every architecture's headers + // under one `/include` (E9's layout). The caller resolved that + // payload; this stays a probe beside the directories it already probed, + // never a search of PATH. + if (out.empty() && !system_headers.empty() + && std::filesystem::is_directory(system_headers, ec)) + out.push_back(system_headers); return out; } diff --git a/src/build/runner_lookup.cppm b/src/build/runner_lookup.cppm index 84e66e055..82ee0722c 100644 --- a/src/build/runner_lookup.cppm +++ b/src/build/runner_lookup.cppm @@ -152,7 +152,9 @@ inline std::string spawn_failed_message(std::string_view program, int e, // The hosted sibling of mcpp::freestanding::no_runner_message. It reports what // the kernel answered rather than asserting why: on a hosted triple mcpp does // not know whether the refusal is a foreign ISA or a file that is not an -// executable at all, and the example it prints is a user-mode emulator. +// executable at all. The declaration it prints is the key's shape; which +// program runs the artifact is the statement of a provider, a platform or the +// project (SPEC-016 §6), and no program is named here. inline std::string unrunnable_message(std::string_view triple, const std::filesystem::path& artifact, int e) { return std::format( @@ -160,7 +162,7 @@ inline std::string unrunnable_message(std::string_view triple, " The artifact was built for '{}'. Declare how to run it here:\n" "\n" " [target.{}]\n" - " runner = [\"qemu-aarch64-static\"]\n" + " runner = [\"\", \"{{}}\"]\n" "\n" " The artifact path is appended, or substituted for `{{}}` if the " "template contains it.\n" diff --git a/src/build/runtime_validation.cppm b/src/build/runtime_validation.cppm index fb2b960be..142d94c82 100644 --- a/src/build/runtime_validation.cppm +++ b/src/build/runtime_validation.cppm @@ -679,29 +679,20 @@ ValidationReport validate_changed_artifacts( // `os == "linux"` on purpose -- it IS the kernel -- so every Linux-shaped // decision in the tree is right about it except the ones that mean "this // machine". + // ONE QUESTION FOR EVERY SUCH ROW (SPEC-016 §6): does this machine load the + // artifact. An SDK's row, a row of another system and a row of another + // architecture all answer no, and the row's data answers it -- Android's + // `execution`, a defined row's, the architecture segment. An artifact for + // another system says so, since its closure is the one thing not checked. if (auto tt = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); - tt && tt->has_own_sysroot()) { + tt && !tt->loads_on_host()) { + if (!before.empty() && tt->c_library_by_platform() && !tt->system_is_host()) + mcpp::ui::info("note", std::format( + "the artifacts are for {}, not this machine: their runtime closure is not " + "checked here", tt->execution_system())); record_without_rules(plan, before); return report; } - // THE ARTIFACT'S OWN SYSTEM DECIDES (mcpp#911 G5): a row a platform defines - // states where its artifacts run, and an artifact for another system is - // judged by that system's loader, not this machine's. Its closure is not - // checked here, and the build says so rather than calling it a defect. - // A row whose C library a platform describes, compared with this machine's - // system rather than with a name (E1; the comparison read "linux", so a - // row run on its own macOS host was called foreign there). Built-in rows - // keep the checks their own providers answer for. - if (auto tt = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); tt) - if (auto const sys = tt->execution_system(); - tt->c_library_by_platform() && sys != mcpp::toolchain::triple::host_triple().os) { - if (!before.empty()) - mcpp::ui::info("note", std::format( - "the artifacts are for {}, not this machine: their runtime closure is not " - "checked here", sys)); - record_without_rules(plan, before); - return report; - } auto doc = read_cache(plan.outputDir); bool changedCache = false; @@ -978,8 +969,7 @@ check_symbol_provision(const mcpp::build::BuildPlan& plan, // machine's search directories do not hold (E1, mcpp#911 N4: a FreeBSD // program was compared with the host's glibc). if (auto tt = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); - tt && tt->c_library_by_platform() - && tt->execution_system() != mcpp::toolchain::triple::host_triple().os) + tt && !tt->loads_on_host()) return findings; // The flags this build hands the linker, as ONE vector, because the diff --git a/src/doctor.cppm b/src/doctor.cppm index 27b6ed4eb..3d92d000b 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -1518,7 +1518,8 @@ nlohmann::json target_selection_json(const mcpp::build::BuildContext& ctx) { auto candidates = nlohmann::json::array(); for (auto const& c : s.candidates) candidates.push_back({ {"identity", c.identity}, {"provider", c.provider}, - {"priority", c.priority}, {"available", c.available}, {"why", c.why} }); + {"priority", c.priority}, {"available", c.available}, {"why", c.why}, + {"tier", c.tier} }); return { {"request", s.request}, {"identity", s.identity}, {"provider", s.provider}, {"priority", s.priority}, {"fallback", s.fallback}, {"why", s.why}, {"candidates", candidates} }; @@ -1661,8 +1662,8 @@ export int why_report(const std::string& topic, const std::string& features, if (!s.candidates.empty()) std::println(" candidates, in order:"); for (auto const& c : s.candidates) { const bool taken = c.identity == s.identity && c.provider == s.provider; - std::println(" {:<26} {:<14} {:>4} {}", c.identity, - c.provider.empty() ? std::string("(platform)") : c.provider, c.priority, + std::println(" {:<26} {:<14} {:>4} {:<9} {}", c.identity, + c.provider.empty() ? std::string("(platform)") : c.provider, c.priority, c.tier, taken ? std::string("taken") : c.available ? std::string("available") : "not available: " + c.why); } diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index a3d4bca69..35299015c 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -563,7 +563,8 @@ std::expected, Error> single_file(const Plan& // on -- an image's single file, a self-contained tree's runner -- when this mcpp // builds no program for it: a row its table states as planned (`x86_64-macos`) // or does not state (`aarch64-windows-gnu`). Empty when it builds one. -std::string unbuilt_row(std::string_view system, std::string_view arch); +std::string unbuilt_row(std::string_view system, std::string_view arch, + const mcpp::platform::Resolved* P = nullptr); // The systems `--systems` names for an architecture (design 2026-10-29 // §4.1): an ``, an `-` of that architecture, or every system @@ -2338,11 +2339,7 @@ std::string dependency_entry(const mcpp::manifest::platform::PackageRef& r) { } // The system names a package speaks of, and this machine's. -constexpr std::string_view host_system() { - if constexpr (mcpplibs::os::is_windows) return "windows"; - else if constexpr (mcpplibs::os::is_macos) return "macos"; - else return "linux"; -} +constexpr std::string_view host_system() { return mcpp::toolchain::platform::name; } // The triple a program of `system` on `arch` is built for, and the mode it // is packed in: a Linux program is static (musl), the one form that runs on @@ -2350,7 +2347,9 @@ constexpr std::string_view host_system() { // macOS or Windows takes this machine's own row; another system's is its // conventional row. `single`: the program is the whole package (a single // file), so on Windows it takes its C++ run-time from the system. -struct ProgramRow { std::string triple; std::string mode; std::string platform; }; +// `served`: false for a system whose row nothing states -- neither the engine's +// conventions nor the platform -- which no program is built for. +struct ProgramRow { std::string triple; std::string mode; std::string platform; bool served = true; }; // The platform a generated program of `triple` is built over, as `--platform` // names it: the target's platform where its hosted arrangement serves the @@ -2380,9 +2379,19 @@ std::string platform_ref_of(const mcpp::platform::Resolved& P) { ProgramRow program_row(std::string_view system, std::string_view arch, bool single, const mcpp::platform::Resolved* P = nullptr) { + // THE IDENTITY THE PLATFORM BUILDS ITS RUNNER AS (`runner.built-as`, + // SPEC-013 §11): the platform that serves a system says which of its rows a + // program for that system is, and the engine holds no such table. One + // static program over the platform's hosted arrangement, the same whichever + // machine packs it. + if (P && P->active) + if (auto it = P->fields.runner.builtAs.find(std::format("{}-{}", system, arch)); + it != P->fields.runner.builtAs.end()) + return { it->second, "static", hosted_platform_for(P, it->second) }; // A SYSTEM A PLATFORM DEFINES (mcpp#911 G10): its row of that architecture, // built over the platform that defines it, as the platform's carrier and - // packs of every other row are. + // packs of every other row are. A system nothing states a row for has no + // program. if (system != "linux" && system != "macos" && system != "windows") { if (P && P->active) for (auto const& d : P->fields.defines) { @@ -2390,7 +2399,7 @@ ProgramRow program_row(std::string_view system, std::string_view arch, bool sing if (t && t->os == system && t->arch == arch) return { t->str(), "static", platform_ref_of(*P) }; } - return { std::format("{}-{}", arch, system), "static", {} }; + return { std::format("{}-{}", arch, system), "static", {}, false }; } if (system == "linux") { const auto triple = std::format("{}-linux-musl", arch); @@ -2522,6 +2531,7 @@ std::string_view launcher_platform() { // state (`aarch64-windows-gnu`), or states as planned (`x86_64-macos`, its // toolchain not published), is not built. bool row_is_built(const ProgramRow& row) { + if (!row.served) return false; if (row.triple.empty()) return true; // this machine's own row auto t = mcpp::toolchain::triple::parse(row.triple); if (!t) return false; @@ -3202,8 +3212,7 @@ run(const Plan& plan, const mcpp::config::GlobalConfig& cfg) // with it; every other name is the target system's. const bool foreignSystem = [&] { auto t = mcpp::toolchain::triple::parse(plan.triple); - return t && t->c_library_by_platform() - && t->execution_system() != mcpp::toolchain::triple::host_triple().os; + return t && !t->loads_on_host(); }(); std::vector targetSystemNames; auto foreign_closure = [&]() -> std::expected { @@ -3517,9 +3526,12 @@ std::vector systems_for(const std::vector& stated, std }; for (auto const& e : stated) { if (e == "all") { - for (auto os : { "linux", "macos", "windows" }) add(os); - // And every system the platforms of this process define a row of - // for the architecture (mcpp#911 G10). + // Every system the table states (SPEC-016 §2.3), built-in or a + // platform's, and every system the platforms of this process + // define a row of for the architecture (mcpp#911 G10). A pair no + // program is built for is written as uncovered (SPEC-014 §4.5), + // not left out. + for (auto const* s : mcpp::toolchain::target::systems()) add(std::string(s->name)); for (auto const* r : mcpp::toolchain::target::rows()) if (r->defined && r->arch == arch && !r->os.empty() && r->os != "none") add(std::string(r->os)); @@ -3541,7 +3553,7 @@ std::vector single_file_rows(const Plan& plan) { for (auto const& system : systems_for(plan.opts.systems, arch)) { SingleFileRow r{ system, arch, {} }; if (all) - if (auto row = unbuilt_row(system, arch); !row.empty()) + if (auto row = unbuilt_row(system, arch, &plan.platform); !row.empty()) r.skipped = std::format("no carrier row: this mcpp builds no program for {}", row); if (all && r.skipped.empty() && plan.platform.active) if (auto why = detail::unbuilt_carrier(plan.platform, system, arch, true)) @@ -3559,8 +3571,9 @@ bool output_names_directory(const std::filesystem::path& output) { return std::filesystem::is_directory(output, ec); } -std::string unbuilt_row(std::string_view system, std::string_view arch) { - const auto row = detail::program_row(system, arch, true); +std::string unbuilt_row(std::string_view system, std::string_view arch, + const mcpp::platform::Resolved* P) { + const auto row = detail::program_row(system, arch, true, P); return detail::row_is_built(row) ? std::string() : row.triple; } @@ -3772,7 +3785,7 @@ std::expected, Error> single_file(const Plan& const std::string system = !t ? std::string(detail::host_system()) : t->os == "windows" ? std::string("windows") : t->os == "macos" ? std::string("macos") : std::string("linux"); - const auto row = detail::program_row(system, arch, true); + const auto row = detail::program_row(system, arch, true, &plan.platform); if (!write(dir / "payload.zst", *payload) || !write(dir / "mcpp.toml", d::launcher_manifest(stem, plan.packageVersion) + detail::static_row_pin(row)) || !write(dir / "src" / "main.cpp", d::launcher_main()) diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index 683732810..48f27a308 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -965,7 +965,7 @@ std::optional check_format(PackRun& run, GroupJob& g) { const bool all = std::ranges::find(run.opts.systems, "all") != run.opts.systems.end(); for (auto const& arch : archs) for (auto const& system : mcpp::pack::systems_for(run.opts.systems, arch)) { - auto row = mcpp::pack::unbuilt_row(system, arch); + auto row = mcpp::pack::unbuilt_row(system, arch, &ctx.plan.platform); if (row.empty()) built.push_back(std::format("{}-{}", system, arch)); else if (!all) unbuilt.push_back(std::format("{}-{} ({})", system, arch, row)); if (row.empty() || !all) ++files; @@ -1260,7 +1260,7 @@ std::optional stage_member(PackRun& run, GroupJob& g, MemberJob& m) { : std::vector{}) { const auto pair = std::format("{}-{}", system, arch); if (covered(pair)) continue; - const auto row = mcpp::pack::unbuilt_row(system, arch); + const auto row = mcpp::pack::unbuilt_row(system, arch, &plan->platform); out.notCovered.push_back(std::format("{} ({})", pair, !row.empty() ? std::format("no runner row: this mcpp builds no program for {}", row) : system == "windows" && !windowsLauncher ? std::string("no Windows launcher") diff --git a/src/pack/polyglot.cppm b/src/pack/polyglot.cppm index a51f50ad2..e10f802ec 100644 --- a/src/pack/polyglot.cppm +++ b/src/pack/polyglot.cppm @@ -26,11 +26,14 @@ export module mcpp.pack.polyglot; import std; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_schema; export namespace mcpp::pack::polyglot { -// One system's program: `system` is `linux`, `macos` or `windows`, `arch` -// `x86_64` or `aarch64`. +// One system's program: `system` is a system of the table (SPEC-016 §2.3), +// `arch` `x86_64` or `aarch64`. A system that states a `uname` is selected by +// the script; the one that states none leads the file as its PE. struct Program { std::string system; std::string arch; @@ -64,9 +67,16 @@ void put32(std::string& b, std::size_t o, std::uint32_t v) { for (int i = 0; i < 4; ++i) b[o + static_cast(i)] = static_cast(v >> (8 * i) & 0xFF); } +// The word `uname -s` answers on a system: the system table's (SPEC-016 +// §2.3). Empty for a system that states none, or that no table knows. +std::string uname_of(std::string_view system) { + auto const* s = mcpp::toolchain::target::find_system(system); + return s ? std::string(s->uname) : std::string{}; +} + // `uname -s`-`uname -m` spellings of a system and architecture. std::string uname_pattern(std::string_view system, std::string_view arch) { - const std::string s = system == "macos" ? "Darwin" : "Linux"; + const std::string s = uname_of(system); if (arch == "x86_64") return std::format("{0}-x86_64|{0}-amd64", s); return std::format("{0}-aarch64|{0}-arm64", s); } @@ -139,16 +149,20 @@ std::expected assemble(std::string_view name, const st const Program* windows = nullptr; std::vector posix; for (auto const& p : programs) { - if (p.system == "windows") { + if (!uname_of(p.system).empty()) { + posix.push_back(p); + } else if (mcpp::toolchain::target::find_system(p.system)) { + // A system that states no `uname` is not selected by the script: + // its program is the PE the file leads with. if (windows) return std::unexpected(std::format( "a polyglot file leads with one Windows program, and this pack has two ({}, {})", windows->arch, p.arch)); windows = &p; - } else if (p.system == "linux" || p.system == "macos") { - posix.push_back(p); } else { - return std::unexpected(std::format("a polyglot file holds no program of {}", p.system)); + return std::unexpected(std::format( + "a polyglot file holds no program of {}: no system of that name is stated " + "([[platform.system]] name, uname)", p.system)); } } if (programs.empty()) return std::unexpected(std::string("a polyglot file holds at least one program")); diff --git a/src/platform/platform.cppm b/src/platform/platform.cppm index 024641ff9..d40f9bb72 100644 --- a/src/platform/platform.cppm +++ b/src/platform/platform.cppm @@ -351,11 +351,15 @@ void merge(mp::Statement& b, const mp::Statement& o) { if (o.sysroot.set) { b.sysroot = o.sysroot; b.sysrootProvides = o.sysrootProvides; } // Field by field: an argv-only statement keeps the platform's `host` and // `exclusive`; a table, or a program's `run-host`, states what it states. + // `requires` are the packages of THAT command, so a statement that + // replaces the command replaces them. if (o.runner.set) { b.runner.cmd = o.runner.cmd; + b.runner.requires_ = o.runner.requires_; b.runner.origin = o.runner.origin; b.runner.set = true; } + if (!o.runner.builtAs.empty()) b.runner.builtAs = o.runner.builtAs; if (!o.runner.host.empty()) b.runner.host = o.runner.host; if (o.runner.exclusiveSet) { b.runner.exclusive = o.runner.exclusive; diff --git a/src/project.cppm b/src/project.cppm index 4c5e9619b..e3d6c364a 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -1095,14 +1095,22 @@ export std::string root_position_key( auto abiThreads = b.abiThreadsDeclared ? (b.abiThreads ? "1" : "0") : "-"; std::string abiExceptions = b.abiExceptionsDeclared ? (b.abiExceptions ? "1" + b.abiExceptionsModel : "0") : "-"; + // `abi longjmp` joins the key WHERE IT IS DECLARED, and only there: this + // key is a cross-version contract -- a project's plan stays current for an + // engine of another release reading it -- so a project that does not state + // the member keeps the key it had, and one that states it has a key an + // engine that cannot read the member never wrote. + std::string abiLongjmp = b.abiLongjmpDeclared ? b.abiLongjmpModel : std::string{}; if (applies) for (auto const& cc : m.conditionalConfigs) { - if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared && !cc.abiExceptionsDeclared) + if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared + && !cc.abiExceptionsDeclared && !cc.abiLongjmpDeclared) continue; if (!applies(cc.predicate)) continue; dialect.insert(dialect.end(), cc.dialectCxxflags.begin(), cc.dialectCxxflags.end()); if (cc.abiThreadsDeclared) abiThreads = cc.abiThreads ? "1" : "0"; if (cc.abiExceptionsDeclared) abiExceptions = cc.abiExceptions ? "1" + cc.abiExceptionsModel : "0"; + if (cc.abiLongjmpDeclared) abiLongjmp = cc.abiLongjmpModel; } list("dialect_cxxflags", dialect); field("cxx_runtime", b.cxxRuntime); @@ -1116,6 +1124,7 @@ export std::string root_position_key( field("ios_deployment_target", b.iosDeploymentTarget); field("abi_threads", abiThreads); field("abi_exceptions", abiExceptions); + if (!abiLongjmp.empty()) field("abi_longjmp", abiLongjmp); field("accel", b.accel); field("bmi_schedule", b.bmiSchedule); field("jobs", b.jobs); @@ -1196,6 +1205,8 @@ virtual_workspace_root(const mcpp::manifest::Manifest& workspace, b.abiExceptions = f.abiExceptions; b.abiExceptionsModel = f.abiExceptionsModel; b.abiExceptionsDeclared = f.abiExceptionsDeclared; + b.abiLongjmpModel = f.abiLongjmpModel; + b.abiLongjmpDeclared = f.abiLongjmpDeclared; b.allowHostLibs = f.allowHostLibs; b.macosDeploymentTarget = f.macosDeploymentTarget; b.iosDeploymentTarget = f.iosDeploymentTarget; @@ -1210,7 +1221,8 @@ virtual_workspace_root(const mcpp::manifest::Manifest& workspace, // member declares (or inherits) reaches the build when the member is // planned under a virtual root (#786). Nothing a package owns travels. for (auto const& cc : first.conditionalConfigs) { - if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared && !cc.abiExceptionsDeclared) + if (cc.dialectCxxflags.empty() && !cc.abiThreadsDeclared + && !cc.abiExceptionsDeclared && !cc.abiLongjmpDeclared) continue; mcpp::manifest::ConditionalConfig row; row.predicate = cc.predicate; @@ -1220,6 +1232,8 @@ virtual_workspace_root(const mcpp::manifest::Manifest& workspace, row.abiExceptions = cc.abiExceptions; row.abiExceptionsModel = cc.abiExceptionsModel; row.abiExceptionsDeclared = cc.abiExceptionsDeclared; + row.abiLongjmpModel = cc.abiLongjmpModel; + row.abiLongjmpDeclared = cc.abiLongjmpDeclared; v.conditionalConfigs.push_back(std::move(row)); } return v; diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index e7c0932ae..30bde9149 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -16,6 +16,8 @@ import mcpp.toolchain.detect; import mcpp.toolchain.msvc; import mcpp.toolchain.registry; import mcpp.toolchain.triple; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_schema; import mcpp.toolchain.post_install; import mcpp.ui; import mcpp.wire; @@ -754,9 +756,22 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, // Apple SDK is not redistributable, so none can, which makes every // Apple row like `aarch64-macos` and unlike `x86_64-windows-musl`. // Stating it removes the reliance on an empty field. + // + // + // AND AN APPLE ROW A PLATFORM SERVES IS NOT AN SDK'S (2026.11.12.1): the + // musl identities of macOS take their system from the platform's + // packages and need no SDK. They are listed where their system is this + // machine's, as they were while `host_can_serve` answered every Apple + // row by the host's system -- then as if a payload produced them, now + // as what the data says, the graph's. (They build on every host; the + // list and the matrix name them on a macOS host only, as before.) + const bool platformServesHere = t->system_is_host() && std::ranges::any_of( + mcpp::toolchain::target::providers(), [&](auto const* p) { + return !p->platform.empty() && mcpp::toolchain::target::serves(*p, info.canonical); + }); bool graphCouldServe = false; if (!planned && !installable_here(*t) && !info.pin.empty() - && !t->is_apple()) { + && (platformServesHere || !t->is_apple())) { auto at = info.pin.find('@'); auto fam = info.pin.substr(0, at == std::string_view::npos ? info.pin.size() : at); diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 243196fbe..df76d9b50 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -29,6 +29,9 @@ import mcpp.toolchain.llvm; import mcpp.toolchain.model; import mcpp.toolchain.msvc; import mcpp.toolchain.triple; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_select; export namespace mcpp::toolchain { @@ -1296,155 +1299,25 @@ bool is_system_toolchain(const ToolchainSpec& spec) { bool host_can_serve(const triple::Triple& target) { if (target.empty()) return true; // host target - // AN SDK THAT SHIPS ITS OWN SYSROOT IS SERVED WHERE THE SDK IS PUBLISHED, - // AND THE ARCH IN THE TRIPLE IS THE GUEST'S. - // - // Every branch below reasons about a cross payload per host arch, because - // that is how a compiler targeting another Linux or another Windows is - // published here. An Emscripten or Android SDK is published per HOST and - // serves every guest arch from one archive: one `xim:emsdk` compiles for - // wasm32 regardless of the machine's arch, and one NDK serves both Android - // arches from a single `--target=-linux-android`. - // - // Without this the wasm row's own pin was not enough. Measured: with - // `emsdk@6.0.9` in the table, `mcpp build --target wasm32-emscripten` - // still answered "No toolchain payload here produces it" and listed - // seventeen servable targets -- because `target.os` is neither "linux" nor - // a Windows form, so control reached a `return false` written for triples - // nobody publishes a payload for. - // - // "WHEREVER" WAS TOO BROAD ONCE, AND THE TARGET MATRIX IS WHAT CAUGHT IT. - // The first version returned true unconditionally, which is the same - // mistake as the branches it sits above: a predicate correct about the - // objects its author had in mind. It was then narrowed to - // `mcpplibs::os::is_linux`, because `xim:emsdk` and `xim:android-ndk` - // both declared ONLY an `xpm.linux` table -- so on macOS or Windows there - // was no payload to install and the honest answer was the same - // `host-cannot-serve` every other unpublished combination gets. That - // comment named its own expiry: "when a darwin or windows NDK lands in the - // index -- upstream publishes both -- this is the one line that changes." - // - // IT HAS LANDED, SO THIS IS THAT LINE. Both packages now declare - // `xpm.linux`, `xpm.macosx` and `xpm.windows`, and the index's own - // per-host install jobs are the measurement rather than the declaration: - // on macOS and Windows each payload downloads, extracts, passes its - // recipe's compiler probe and registers its shims. Two host assumptions - // inside those recipes were found by exactly those jobs and fixed there, - // which is where a host-shaped packaging defect belongs -- not here. - // - // Keyed on the target and no longer on the host, because these payloads - // are published per host OS and carry every guest arch: one `xim:emsdk` - // compiles for wasm32 regardless of the machine's arch, and one NDK serves - // both Android arches. The remaining per-host question is whether the - // payload EXISTS, and that is the index's answer to give, not a constant - // compiled into the engine. A row whose pin the index cannot satisfy on - // this host fails at install with the package's own diagnostic, which - // names the payload -- strictly better than this function silently - // deleting the row from `toolchain list`, which reported a target mcpp - // knows as one it has never heard of. - if (target.has_own_sysroot()) return true; - - if (target.os == "linux") { - if constexpr (mcpplibs::os::is_linux) { - // "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::toolchain::platform::host_arch; - if (target.is_musl()) - return !crossArch || mcpp::toolchain::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 - // payload exists — the windows-hosted canadian cross, built per host - // arch — so an arch-crossing combination stays unserviceable until one - // is published. macOS has no Linux-targeting payload at all. - return mcpplibs::os::is_windows - && target.is_musl() - && target.arch == mcpp::toolchain::platform::host_arch; - } - // 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 (mcpplibs::os::is_windows) return true; - return mcpplibs::os::is_linux - && mcpp::toolchain::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(mcpplibs::os::is_windows); - // APPLE, AND ONE ARM FOR BOTH OF ITS PLATFORMS. - // - // This read `os == "macos"`, which was the same question while macOS was - // the only Apple target mcpp had. An iOS target then fell through to the - // `return false` at the end of this function -- on EVERY host, including - // the one that serves it. - // - // The consequence was not a refused build: the SDK gate in prepare answers - // before this one. It was `toolchain list`, which drops a row this says no - // host can serve -- so the three iOS rows were absent from the list on - // macOS, and present on linux-x86_64 for the wrong reason (see - // `graphCouldServe` in mcpp.toolchain.lifecycle). Measured as a - // target-matrix scan that reached three fewer cells on linux-aarch64 than - // the expected table declares. - // - // `is_apple()` is the predicate the table already carries, and until now - // it had no reader at all. - if (target.is_apple()) return bool(mcpplibs::os::is_macos); - - // Bare metal: every host can serve it, and that is a property of the - // toolchain rather than a claim about payload coverage. clang and lld are - // cross-compilers by construction — one binary emits every target it was - // built with — so a freestanding target needs NO per-host cross payload, - // unlike every hosted case above, which needs a C library that only exists - // for some (host, target) pairs. - // - // Serviceable is not the same as complete: a target with no C library - // still links only `-nostdlib` programs. That gap belongs to the ecosystem - // (a libc wrapper package), and saying `false` here would hide it behind - // "this host cannot build it", which is the wrong diagnosis. - if (target.is_freestanding()) return true; - // An ok image the same way: clang and lld emit it on every host, and what - // the image holds above `kal_*` is the graph's. - if (target.loader_supplies_os()) return true; - - return false; + // THE DATA ANSWERS (SPEC-016 §3): a provider that brings its own payloads + // serves the identity, and its `hosts` name this machine. The question is + // about payloads, so a provider a platform package implements is not + // counted -- whether the graph supplies the system is asked where the + // graph is known. Which hosts a payload exists for is a fact of the index, + // written once beside the provider; this function names no system and no + // architecture. + const auto host = mcpp::toolchain::target::host_pair(mcpp::toolchain::platform::name, + mcpp::toolchain::platform::host_arch); + const auto identity = target.str(); + for (auto const* p : mcpp::toolchain::target::providers()) + if (p->platform.empty() && mcpp::toolchain::target::serves(*p, identity) + && mcpp::toolchain::target::available_on(*p, host, target.arch)) + return true; + // A triple no row defines is a freestanding one the driver emits by + // itself (clang and lld are cross-compilers by construction), or an ok + // image, whose every part above `kal_*` is the graph's. + if (!mcpp::toolchain::target::find_row(identity)) return target.is_freestanding(); + return target.loader_supplies_os(); } std::vector available_toolchain_indexes() { diff --git a/tests/e2e/1004_an_external_toolchain_finds_mcpps_ninja.sh b/tests/e2e/1004_an_external_toolchain_finds_mcpps_ninja.sh index c3e3ef0c5..3194e67bc 100755 --- a/tests/e2e/1004_an_external_toolchain_finds_mcpps_ninja.sh +++ b/tests/e2e/1004_an_external_toolchain_finds_mcpps_ninja.sh @@ -19,14 +19,14 @@ source "$(dirname "$0")/_inherit_toolchain.sh" fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } gxx=$(ls -d "$MCPP_HOME"/registry/data/xpkgs/xim-x-gcc/*/bin/g++ 2>/dev/null | head -1) -[ -n "$gxx" ] || { echo "SKIP: no gcc payload to reach from outside the store"; exit 0; } +[ -n "$gxx" ] || { echo "SKIP: no gcc payload to reach from outside the store"; exit 77; } ninja=$(ls -d "$MCPP_HOME"/registry/data/xpkgs/xim-x-ninja/*/ninja 2>/dev/null | head -1) -[ -n "$ninja" ] || { echo "SKIP: mcpp's home holds no ninja"; exit 0; } +[ -n "$ninja" ] || { echo "SKIP: mcpp's home holds no ninja"; exit 77; } ln -s "$(dirname "$(dirname "$gxx")")" "$TMP/external-gcc" # The xlings mcpp drives, named outright: the PATH below holds no xlings. export MCPP_VENDORED_XLINGS="${MCPP_VENDORED_XLINGS:-$(command -v xlings || true)}" -[ -n "$MCPP_VENDORED_XLINGS" ] || [ -x "$MCPP_HOME/registry/bin/xlings" ] || { echo "SKIP: no xlings to name"; exit 0; } +[ -n "$MCPP_VENDORED_XLINGS" ] || [ -x "$MCPP_HOME/registry/bin/xlings" ] || { echo "SKIP: no xlings to name"; exit 77; } # A PATH with the usual tools and no ninja. An external toolchain is complete # where it is installed (MSYS2's GCC carries its assembler in its own `bin'); diff --git a/tests/e2e/100_cppfly_reflection.sh b/tests/e2e/100_cppfly_reflection.sh index 38cb52f41..90453e142 100755 --- a/tests/e2e/100_cppfly_reflection.sh +++ b/tests/e2e/100_cppfly_reflection.sh @@ -19,7 +19,7 @@ USER_MCPP="${MCPP_HOME:-$HOME/.mcpp}" GXX=$(find "$USER_MCPP/registry/data/xpkgs/xim-x-gcc" -name "g++" -path "*/bin/*" 2>/dev/null | sort | tail -1) if [[ -z "$GXX" ]] || ! echo 'int main(){}' | "$GXX" -freflection -std=c++26 -x c++ - -fsyntax-only -o /dev/null 2>/dev/null; then echo "SKIP-INLINE: no -freflection-capable gcc payload available" - exit 0 + exit 77 fi GCC_VER=$(basename "$(dirname "$(dirname "$GXX")")") diff --git a/tests/e2e/1010_the_std_module_takes_the_platforms_flags.sh b/tests/e2e/1010_the_std_module_takes_the_platforms_flags.sh index a6f15ad05..b80cf29bb 100755 --- a/tests/e2e/1010_the_std_module_takes_the_platforms_flags.sh +++ b/tests/e2e/1010_the_std_module_takes_the_platforms_flags.sh @@ -15,7 +15,7 @@ # that imports std builds and runs with it, and the module's command names it. # The flag is the x86_64 one, so the test asks it of an x86_64 machine only. set -e -case "$(uname -m)" in x86_64|amd64) ;; *) echo "SKIP: -fcf-protection is an x86_64 flag"; exit 0 ;; esac +case "$(uname -m)" in x86_64|amd64) ;; *) echo "SKIP: -fcf-protection is an x86_64 flag"; exit 77 ;; esac source "$(dirname "$0")/_toolchain_env.sh" TMP=$(mktemp -d) diff --git a/tests/e2e/101_cppfly_llvm_soft.sh b/tests/e2e/101_cppfly_llvm_soft.sh index d846caea5..b2e17936e 100755 --- a/tests/e2e/101_cppfly_llvm_soft.sh +++ b/tests/e2e/101_cppfly_llvm_soft.sh @@ -11,7 +11,7 @@ trap "rm -rf $TMP" EXIT source "$(dirname "$0")/_llvm_env.sh" if [[ ! -d "$LLVM_ROOT" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION:-none} is not installed" - exit 0 + exit 77 fi export MCPP_HOME="$TMP/mcpp-home" diff --git a/tests/e2e/1025_the_default_platform_is_one_release.sh b/tests/e2e/1025_the_default_platform_is_one_release.sh index ee23800ac..4021aa86d 100755 --- a/tests/e2e/1025_the_default_platform_is_one_release.sh +++ b/tests/e2e/1025_the_default_platform_is_one_release.sh @@ -7,9 +7,9 @@ # # Criteria: # A. A project that states no platform builds `x86_64-openkal` over -# openkal-platform 0.26.0, from the target table, and mcpp.lock records it. +# openkal-platform 0.27.0, from the target table, and mcpp.lock records it. # B. A lock that records another release of it does not decide: the build -# says which it takes and which the lock recorded, and records 0.26.0. +# says which it takes and which the lock recorded, and records 0.27.0. # C. A platform the project states is taken as it is (a series here). set -e TMP=$(mktemp -d) @@ -30,20 +30,20 @@ PY # A "$MCPP" build --target x86_64-openkal > a.log 2>&1 || fail "A: the openkal build failed" a.log -grep -q 'Platform *openkal-platform 0.26.0' a.log || fail "A: the build does not use openkal-platform 0.26.0" a.log -[[ "$(locked)" == "0.26.0" ]] || fail "A: mcpp.lock does not record 0.26.0" mcpp.lock +grep -q 'Platform *openkal-platform 0.27.0' a.log || fail "A: the build does not use openkal-platform 0.27.0" a.log +[[ "$(locked)" == "0.27.0" ]] || fail "A: mcpp.lock does not record 0.27.0" mcpp.lock # B $PY - <<'PY' import re text = open("mcpp.lock").read() -text = re.sub(r'(\[package\."openkal-platform"\][^\[]*?version\s*=\s*")0\.26\.0(")', r'\g<1>0.24.3\2', text, count=1) +text = re.sub(r'(\[package\."openkal-platform"\][^\[]*?version\s*=\s*")0\.27\.0(")', r'\g<1>0.24.3\2', text, count=1) open("mcpp.lock", "w").write(text) PY [[ "$(locked)" == "0.24.3" ]] || fail "B: the fixture could not edit mcpp.lock" mcpp.lock "$MCPP" build --target x86_64-openkal > b.log 2>&1 || fail "B: the openkal build failed" b.log grep -q "default; mcpp.lock recorded v0.24.3" b.log || fail "B: the build does not say which release it takes over the lock's" b.log -[[ "$(locked)" == "0.26.0" ]] || fail "B: mcpp.lock was not rewritten to 0.26.0" mcpp.lock +[[ "$(locked)" == "0.27.0" ]] || fail "B: mcpp.lock was not rewritten to 0.27.0" mcpp.lock # C cat >> mcpp.toml <<'EOT' diff --git a/tests/e2e/1028_a_projects_state_directory_ignores_itself.sh b/tests/e2e/1028_a_projects_state_directory_ignores_itself.sh index 7550ab073..f9a67cbf1 100755 --- a/tests/e2e/1028_a_projects_state_directory_ignores_itself.sh +++ b/tests/e2e/1028_a_projects_state_directory_ignores_itself.sh @@ -10,7 +10,7 @@ # Criterion: after a build in a repository whose .gitignore names only # target/, `git status` lists nothing under .mcpp/, and `.mcpp/.gitignore` is `*`. set -e -command -v git > /dev/null || { echo "SKIP: no git on this machine"; exit 0; } +command -v git > /dev/null || { echo "SKIP: no git on this machine"; exit 77; } TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } @@ -22,7 +22,7 @@ printf 'int main() { return 0; }\n' > src/main.cpp printf 'target/\n' > .gitignore git init -q . "$MCPP" build > build.log 2>&1 || fail "the build failed" build.log -[ -d .mcpp ] || { echo "SKIP: this build wrote no state directory"; exit 0; } +[ -d .mcpp ] || { echo "SKIP: this build wrote no state directory"; exit 77; } grep -qx '\*' .mcpp/.gitignore || fail ".mcpp/.gitignore does not ignore everything" .mcpp/.gitignore git status --porcelain --untracked-files=all > status.txt grep -q '^?? \.mcpp/' status.txt && fail "version control sees the state directory" status.txt diff --git a/tests/e2e/1030_the_low_space_warning_knows_what_the_last_build_took.sh b/tests/e2e/1030_the_low_space_warning_knows_what_the_last_build_took.sh index 4424e2511..2dcaece3b 100755 --- a/tests/e2e/1030_the_low_space_warning_knows_what_the_last_build_took.sh +++ b/tests/e2e/1030_the_low_space_warning_knows_what_the_last_build_took.sh @@ -22,7 +22,7 @@ cd "$TMP" cd app "$MCPP" build > a.log 2>&1 || fail "the build failed" a.log rec=$(find target -name .mcpp-written | head -1) -[ -n "$rec" ] || { echo "SKIP: the file system reported no change in free space"; exit 0; } +[ -n "$rec" ] || { echo "SKIP: the file system reported no change in free space"; exit 77; } grep -Eq '^[0-9]+$' "$rec" || fail "A: .mcpp-written is not a number" "$rec" echo "ok: the build recorded what it took ($(cat "$rec") bytes)" echo 1152921504606846976 > "$rec" # 1 EiB: more than any file system here has free diff --git a/tests/e2e/1033_an_ok_image_runs_on_the_system_that_built_it.sh b/tests/e2e/1033_an_ok_image_runs_on_the_system_that_built_it.sh index 7b79e2135..3867476b4 100755 --- a/tests/e2e/1033_an_ok_image_runs_on_the_system_that_built_it.sh +++ b/tests/e2e/1033_an_ok_image_runs_on_the_system_that_built_it.sh @@ -25,7 +25,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER_ARCH=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER_ARCH=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac mkdir -p "$TMP/app/src" "$TMP/app/tests" diff --git a/tests/e2e/104_asm_sources_gas.sh b/tests/e2e/104_asm_sources_gas.sh index 203cecb8a..4eeadc948 100755 --- a/tests/e2e/104_asm_sources_gas.sh +++ b/tests/e2e/104_asm_sources_gas.sh @@ -8,7 +8,7 @@ set -e # x86_64 assembly — host-aware skip elsewhere (aarch64 self-host e2e). -[[ "$(uname -m)" == "x86_64" ]] || { echo "SKIP: x86_64-only asm"; exit 0; } +[[ "$(uname -m)" == "x86_64" ]] || { echo "SKIP: x86_64-only asm"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh b/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh index c36a6408c..1e7b747be 100755 --- a/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh +++ b/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh @@ -15,7 +15,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" set -e t=$(mktemp -d); trap 'rm -rf "$t"' EXIT fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } -[ "$(uname -m)" = x86_64 ] || { echo "SKIP: an x86_64 host"; exit 0; } +[ "$(uname -m)" = x86_64 ] || { echo "SKIP: an x86_64 host"; exit 77; } mkdir -p "$t/src" && cd "$t" cat > mcpp.toml < build.log 2>&1 || fail "the hosted macOS build" build.log grep -q "openkal-platform" build.log || { echo "SKIP: aarch64-macos-musl is not served by openkal-platform's hosted arrangement here" - exit 0 + exit 77 } bin=$(find target -path '*/bin/early' -type f | head -1) [ -n "$bin" ] || fail "no image was produced" build.log diff --git a/tests/e2e/105_asm_sources_nasm.sh b/tests/e2e/105_asm_sources_nasm.sh index ec8b0e433..ed731098d 100755 --- a/tests/e2e/105_asm_sources_nasm.sh +++ b/tests/e2e/105_asm_sources_nasm.sh @@ -9,7 +9,7 @@ set -e # 2026.10.26.1 ([build] compile_commands); this test reads it. export MCPP_COMPILE_COMMANDS=compile_commands.json -[[ "$(uname -m)" == "x86_64" ]] || { echo "SKIP: x86-only NASM"; exit 0; } +[[ "$(uname -m)" == "x86_64" ]] || { echo "SKIP: x86-only NASM"; exit 77; } SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" diff --git a/tests/e2e/1073_the_freebsd_musl_identity_is_served_by_openkal.sh b/tests/e2e/1073_the_freebsd_musl_identity_is_served_by_openkal.sh index 375d44460..132adc7b7 100755 --- a/tests/e2e/1073_the_freebsd_musl_identity_is_served_by_openkal.sh +++ b/tests/e2e/1073_the_freebsd_musl_identity_is_served_by_openkal.sh @@ -3,7 +3,7 @@ # 1073 -- FreeBSD's musl identity is served by openkal (design 2026-11-10 E4, K8, # K9; docs/21 'Build hosts and the targets they serve'). # -# `x86_64-freebsd-musl` is openkal-platform's hosted part (0.26.0): openkal-musl +# `x86_64-freebsd-musl` is openkal-platform's hosted part (0.26.0 on): openkal-musl # over openkal-freebsd in its standalone arrangement, linked as one static # program with no C library of the system beneath. A request of `x86_64-freebsd` # with no platform selected for it takes that identity, as `aarch64-macos` diff --git a/tests/e2e/1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh b/tests/e2e/1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh new file mode 100755 index 000000000..06f578b08 --- /dev/null +++ b/tests/e2e/1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# requires: llvm +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1075 -- a Windows row whose system the graph supplies compiles its resources +# with the Windows headers the platform's rows declare (2026.11.11.1, G3; +# design 2026-11-10 E3's third source). +# +# `x86_64-windows-musl` takes its system from the graph (openkal-platform's +# hosted part), so its toolchain has no paired payload, and its llvm binary +# carries no Windows headers of its own: neither include source +# `system_include_dirs` read existed, and a script that includes +# `"winres.h"` failed in llvm-rc with `winres.h: No such file or directory`. +# The MinGW-w64 sysroot payload a platform row declares for the +# architecture's GNU-ABI identity holds them, under one `include/` for every +# architecture; every PE row shares that set, and the two existing sources +# keep their precedence. +# +# Criteria: the project builds with `[resources]` on x86_64-windows-musl and +# the program carries a VERSIONINFO resource; a GNU-ABI row (1058) is not +# re-tested here. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -25; exit 1; } +mkdir -p "$t/src" +cat > "$t/mcpp.toml" <<'TOML' +[package] +name = "verinfo" +version = "0.1.0" + +[resources] +files = ["app.rc"] + +[targets.verinfo] +kind = "bin" +main = "src/main.c" +TOML +echo 'int main(void) { return 0; }' > "$t/src/main.c" +cat > "$t/app.rc" <<'RC' +#include "winres.h" +1 VERSIONINFO +FILEVERSION 1,2,3,4 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "ProductName", "verinfo" + END + END +END +RC +cd "$t" +out=$("$MCPP" build --target x86_64-windows-musl --toolchain "llvm@${LLVM_VERSION}" 2>&1) \ + || fail "[resources] did not build on x86_64-windows-musl" "$out" +exe=$(find target/x86_64-windows-musl -path '*/bin/verinfo.exe' -type f | head -1) +[ -n "$exe" ] || fail "no program" "$out" +# The llvm payload of the build, not the host's tools. +readobj="$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-readobj" +[ -x "$readobj" ] || fail "no llvm-readobj in the llvm payload ($readobj)" +"$readobj" --coff-resources "$exe" | grep -q 'VERSIONINFO' \ + || fail "the program carries no VERSIONINFO" "$("$readobj" --coff-resources "$exe" | head -20)" +echo "PASS: 1075 a graph-supplied Windows row finds its resource headers" diff --git a/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh b/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh new file mode 100755 index 000000000..5d5db3abb --- /dev/null +++ b/tests/e2e/1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# requires: llvm elf x86_64-host +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1076 -- the cross arrangement of aarch64-linux-gnu: its C library, C++ +# library and compiler run-time come from a base package, its artifacts name +# the row's loader, and they run through the provider's runner (SPEC-016 §3, +# §6; design 2026-11-12 C2, C3, C5). +# +# The row has two provider entries: `glibc`, the host's payload on an aarch64 +# host, and `glibc-sysroot`, a base package on linux-x86_64 at a priority +# below the musl entries. The second is what this script holds: +# +# 1. `aarch64-linux` is answered as before the entry existed (musl); the +# whole identity reaches the cross entry. +# 2. A C program, a C++ program over the headers and a program that imports +# std each build, dynamically; the last one statically as well. +# 3. Each dynamic artifact is an aarch64 ELF whose interpreter is the row's +# `loader`, with no RUNPATH and no path of this machine in it. +# 4. `mcpp run` starts each through the provider's runner, which this run +# installs, and the program's exit status and output arrive. +# +# The entry's hosts are linux-x86_64, which the capability line states: on +# another host the script is excluded, and the run reports it as such. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -25; exit 1; } + +readelf="$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-readelf" + +T=aarch64-linux-gnu +LOADER=/lib/ld-linux-aarch64.so.1 + +project() { #
+ mkdir -p "$t/$1/src" + printf '[package]\nname = "cross"\nversion = "0.1.0"\n\n[targets.cross]\nkind = "bin"\nmain = "src/%s"\n' "$2" \ + > "$t/$1/mcpp.toml" + printf '%b' "$3" > "$t/$1/src/$2" +} +project c main.c '#include \nint main(void) { printf("cross-c\\n"); return 42; }\n' +project cxx main.cpp '#include \nint main() { std::println("cross-cxx"); return 43; }\n' +project std main.cpp 'import std;\nint main() { std::println("cross-std"); return 44; }\n' + +# 1. The request `aarch64-linux` keeps its answer. +cd "$t/c" +out=$("$MCPP" why target --target aarch64-linux 2>&1) || fail "why target refused" "$out" +grep -q "aarch64-linux-musl" <<<"$out" || fail "aarch64-linux is no longer answered by the musl identity" "$out" + +check() { # + cd "$t/$1" + out=$("$MCPP" build --target "$T" 2>&1) || fail "$1: $T did not build over the base package" "$out" + grep -q "glibc-sysroot\|aarch64-linux-gnu-sysroot" <<<"$out" || fail "$1: the base package is not what served it" "$out" + exe=$(find "target/$T" -path '*/bin/cross' -type f | head -1) + [ -n "$exe" ] || fail "$1: no program" "$out" + [ -x "$readelf" ] || fail "no llvm-readelf in the llvm payload ($readelf)" + hdr=$("$readelf" -h "$exe") + grep -q "Machine:.*AArch64" <<<"$hdr" || fail "$1: the artifact is not an aarch64 ELF" "$hdr" + interp=$("$readelf" -l "$exe" | sed -n 's/.*Requesting program interpreter: \(.*\)\]/\1/p') + [ "$interp" = "$LOADER" ] || fail "$1: the interpreter is '$interp', the row's loader is $LOADER" + dyn=$("$readelf" -d "$exe") + if grep -qiE "RPATH|RUNPATH" <<<"$dyn"; then fail "$1: the artifact carries a run-time path of this machine" "$dyn"; fi + if grep -aq "${MCPP_HOME:-$HOME/.mcpp}" "$exe"; then fail "$1: a path of this machine's store is in the artifact"; fi + rc=0 + run=$("$MCPP" run --target "$T" 2>&1) || rc=$? + [ "$rc" -eq "$3" ] || fail "$1: mcpp run exited $rc, the program returns $3" "$run" + grep -q "^$2\$" <<<"$run" || fail "$1: the program's output did not arrive" "$run" + grep -q "Running .*qemu-aarch64" <<<"$run" || fail "$1: the run did not go through the provider's runner" "$run" +} +check c cross-c 42 +check cxx cross-cxx 43 +check std cross-std 44 + +# 2. Statically: no interpreter, and the same exit status through the runner. +cd "$t/std" +printf '\n[target.%s]\nlinkage = "static"\n' "$T" >> mcpp.toml +out=$("$MCPP" build --target "$T" 2>&1) || fail "the static import-std program did not build" "$out" +exe=$(find "target/$T" -path '*/bin/cross' -type f -newer mcpp.toml | head -1) +[ -n "$exe" ] || fail "no static program" "$out" +if "$readelf" -l "$exe" | grep -q "program interpreter"; then fail "a static program names an interpreter"; fi +rc=0 +run=$("$MCPP" run --target "$T" 2>&1) || rc=$? +[ "$rc" -eq 44 ] || fail "static: mcpp run exited $rc, the program returns 44" "$run" + +echo "PASS: 1076 an aarch64-linux-gnu cross comes from a sysroot payload" diff --git a/tests/e2e/1077_a_program_of_another_system_is_built_not_run.sh b/tests/e2e/1077_a_program_of_another_system_is_built_not_run.sh new file mode 100755 index 000000000..7d7756844 --- /dev/null +++ b/tests/e2e/1077_a_program_of_another_system_is_built_not_run.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# requires: llvm +# 1077 -- a program of another system is built, not run: `mcpp run` starts +# nothing it cannot observe (SPEC-016 §6; design 2026-11-12 C9, D25). +# +# A row's artifacts run on its execution system. Where that is not this +# machine's system and nothing states a runner, the artifact is not started: +# this machine's kernel maps a static program of its own architecture without +# reading the system it names, and gives that system's call numbers its own +# meaning. The answer is given before anything is spawned, by one predicate for +# built-in rows and for rows a platform defines. +# +# Criteria: on a host whose system is not FreeBSD, `mcpp run --target +# -freebsd-musl` builds the program, exits 2 without running it, states +# "built, not run", names both systems and prints the `[target.]` +# runner declaration for the row; `mcpp test` of the same target reports the +# tests as built, not run. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } + +case "$(uname -m)" in x86_64|amd64) ARCH=x86_64 ;; *) ARCH=aarch64 ;; esac +T="$ARCH-freebsd-musl" + +cd "$TMP" +"$MCPP" new app > /dev/null 2>&1 +cd app +"$MCPP" build --target "$T" > b.log 2>&1 || fail "the static FreeBSD program did not build" b.log +exe=$(find "target/$T" -path '*/bin/app*' -type f | head -1) +[ -n "$exe" ] || fail "no program" b.log + +# A marker the program would write if it were started: the program of `mcpp +# new` prints, so its output in the log is the evidence of a launch. +set +e +"$MCPP" run --target "$T" > run.log 2>&1 +rc=$? +set -e +[ "$rc" -eq 2 ] || fail "run exited $rc, expected the refusal's 2" run.log +grep -q "built, not run" run.log || fail "the run does not say built, not run" run.log +grep -q "artifacts run on freebsd" run.log || fail "the artifact's system is not named" run.log +grep -q "\[target.$T\] runner = " run.log || fail "the runner declaration does not name the row" run.log +grep -q "Running" run.log && fail "the artifact was started" run.log + +# `--no-runner` states that the triple is native here, and is believed: the +# launch is then the caller's, and the refusal above does not apply. +set +e +"$MCPP" run --target "$T" --no-runner > direct.log 2>&1 +set -e +grep -q "built, not run" direct.log && fail "--no-runner was not believed" direct.log + +echo "PASS: 1077 a program of another system is built, not run" diff --git a/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh b/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh new file mode 100755 index 000000000..23de90eec --- /dev/null +++ b/tests/e2e/1078_systems_all_carries_the_runners_the_platform_declares.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# requires: llvm unix-shell +# 1078 -- `--systems all` carries the runners the platform's own data names +# (2026.11.11.1, G5): this machine's system, the rows a platform defines, and +# the systems its runner states a host of -- FreeBSD's among them, whose +# runner is built as -freebsd-musl over the platform's hosted +# arrangement. +# +# The default is unchanged: a pack without `--systems` carries the runners +# of the legs' own systems -- this machine's system for every architecture +# the package has a leg of -- and no other system's. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } + +case "$(uname -m)" in + x86_64|amd64) ARCH=x86_64 ;; + aarch64|arm64) ARCH=aarch64 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; +esac +case "$(uname -s)" in Linux) OS=linux ;; *) echo "SKIP: a Linux host"; exit 77 ;; esac + +cd "$TMP" +"$MCPP" new app > /dev/null 2>&1 +cd app + +# The default: this machine's system for each leg's architecture, nothing +# else (`mcpp new app` carries an x86_64 and an aarch64 leg). A `dir` pack's +# tree lands under target/dist whatever `-o` says (design 2026-10-29 §4.6), +# so the pack is located, not assumed. +"$MCPP" pack --target openkal --mode self-contained --pkg-format dir > d.log 2>&1 \ + || fail "the default pack did not build" d.log +one=$(find target/dist -mindepth 1 -maxdepth 1 -type d -newer mcpp.toml | head -1) +[ -n "$one" ] || fail "no pack tree under target/dist" d.log +[ -d "$one/libexec/okrun/${OS}-${ARCH}" ] || fail "the default pack carries no runner of its own system" d.log +others=$(ls "$one/libexec/okrun" 2>/dev/null | grep -v "^${OS}-" || true) +[ -z "$others" ] || fail "the default pack carries a system other than this machine's: $others" d.log + +# `--systems all`: the data's answer, FreeBSD included. The same tree name +# replaces the default's, so dist is cleared first and the tree that lands +# is the all-pack's own. +rm -rf target/dist +"$MCPP" pack --target openkal --mode self-contained --systems all --pkg-format dir > a.log 2>&1 \ + || fail "--systems all did not pack" a.log +all_=$(find target/dist -mindepth 1 -maxdepth 1 -type d | head -1) +[ -n "$all_" ] || fail "no --systems all tree under target/dist" a.log +for pair in "linux-x86_64" "linux-aarch64" "freebsd-x86_64" "freebsd-aarch64"; do + [ -d "$all_/libexec/okrun/$pair" ] || fail "no runner carried for $pair" a.log +done +# The Windows row builds wherever this mcpp builds it; where it does not, the +# warning says so instead. +if [ ! -d "$all_/libexec/okrun/windows-x86_64" ]; then + grep -q "no runner for windows-x86_64" a.log \ + || fail "the Windows runner is neither carried nor said" a.log +fi +echo "PASS: 1078 --systems all carries the runners the platform declares" diff --git a/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh b/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh new file mode 100755 index 000000000..6521b9cb8 --- /dev/null +++ b/tests/e2e/1079_an_emscripten_rows_longjmp_model_round_trips.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# requires: elf +# 1079 -- `longjmp` is the third `abi` member: the model a WebAssembly +# program's sigsetjmp/siglongjmp cross the boundary by (2026.11.11.1). +# +# With the toolchain's default JavaScript model the pair compiles into a +# stack copy, and a non-local exit over two call levels reads past the +# module's stack and dies at run time (measured: memory access out of bounds) +# while the build and the link succeed -- a failure nothing names. With +# `-sSUPPORT_LONGJMP=wasm` on both the compiles and the link, the pair +# round-trips. A deep exit wants a bigger stack; that is the project's own +# knob (`-sSTACK_SIZE` in ldflags), not an ABI fact, and the engine does not +# render it. +# +# Same self-probe gate as 653/650: `# requires: elf` only, because whether +# THIS machine has the emsdk payload is not a portable capability. Declared +# on a non-Emscripten row the member is refused naming the row's own os. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } + +have_emsdk=0 +for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripten \ + "$HOME"/.xlings/data/xpkgs/xim-x-emsdk/*/emscripten; do + [[ -x "$d/em++" ]] && have_emsdk=1 +done +if [ "$have_emsdk" -ne 1 ] || ! command -v node >/dev/null 2>&1; then + echo "SKIP: 1079 -- no emsdk payload or node on this machine" + exit 77 +fi + +mkdir -p "$TMP/app/src" +cd "$TMP/app" +cat > mcpp.toml <<'TOML' +[package] +name = "lj" +version = "0.1.0" + +[targets.lj] +kind = "bin" +main = "src/main.c" + +[target.wasm32-emscripten] +runner = ["node"] + +[target.'cfg(os = "emscripten")'.abi] +longjmp = "wasm" + +[target.'cfg(os = "emscripten")'.build] +ldflags = ["-sSTACK_SIZE=1048576"] +TOML +cat > src/main.c <<'C' +/* Emscripten's libc declares the sigsetjmp/siglongjmp aliases behind the + feature macros, like every libc; the fixture states the one it needs. */ +#define _GNU_SOURCE 1 +#include +#include + +static jmp_buf home; + +static void level_two(void) { + volatile int kept = 7; + siglongjmp(home, 1); + (void)kept; +} + +static void level_one(void) { level_two(); } + +int main(void) { + volatile int marker = 0; + if (sigsetjmp(home, 1) == 0) { + marker = 1; + level_one(); + printf("unreachable\n"); + return 1; + } + /* The volatile locals of the frames unwound by the non-local exit. */ + if (marker != 1) { printf("marker lost\n"); return 2; } + printf("round-trip\n"); + return 0; +} +C + +"$MCPP" build --target wasm32-emscripten > b.log 2>&1 || fail "the wasm build with longjmp = \"wasm\" failed" b.log +"$MCPP" run --target wasm32-emscripten > run.log 2>&1 || fail "the run failed -- the non-local exit did not round-trip" run.log +grep -q '^round-trip$' run.log || fail "the program did not reach its end" run.log + +# The model is a member of Emscripten rows alone: a Linux build that declares +# it is refused naming that fact, not silently satisfied. +cat > mcpp.toml <<'TOML' +[package] +name = "lj" +version = "0.1.0" + +[targets.lj] +kind = "bin" +main = "src/main.c" + +[target.'cfg(os = "linux")'.abi] +longjmp = "wasm" +TOML +if "$MCPP" build --target x86_64-linux-musl > refuse.log 2>&1; then + fail "longjmp = \"wasm\" on a Linux row was accepted" +fi +grep -q "not an Emscripten row" refuse.log || fail "the refusal does not name the row's own os" refuse.log +echo "PASS: 1079 an Emscripten row's longjmp model round-trips" diff --git a/tests/e2e/1080_gcc_over_the_hosts_payload_links_from_the_payload.sh b/tests/e2e/1080_gcc_over_the_hosts_payload_links_from_the_payload.sh new file mode 100644 index 000000000..df856f589 --- /dev/null +++ b/tests/e2e/1080_gcc_over_the_hosts_payload_links_from_the_payload.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# requires: gcc elf +# 1080 -- gcc over the host's C library payload names the payload on its link: +# the crt search prefix, the library directory and the loader (design +# 2026-11-12 C1; SPEC-006). +# +# A gcc whose recorded sysroot does not exist on this machine is linked +# "payload first": the link names the glibc payload's directory for the crt +# objects (`-B`), for the libraries (`-L`, with its run-time path) and for the +# loader. A rewrite of that branch of the flag renderer once dropped the whole +# group, and the crt objects then resolved from the host's /lib -- on a machine +# that had them; the hermetic link check refused the build. No test of the tree +# built with such a gcc, so the first report was a failed build elsewhere. +# +# gcc 15.1.0 records the sysroot of the machine that built it, which exists on +# no other; the engine's own gcc line (16.x) records the subos root and is +# linked through `--sysroot`. Criteria: a project built with gcc@15.1.0 links, +# its link line names the payload three times as above, and the program runs. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -25; exit 1; } + +cd "$t" +"$MCPP" new app > /dev/null 2>&1 +cd app +out=$("$MCPP" build --toolchain gcc@15.1.0 2>&1) || fail "the project did not build with gcc@15.1.0" "$out" +ninja=$(find target -name build.ninja | head -1) +[ -n "$ninja" ] || fail "no build.ninja" "$out" +line=$(grep -m1 -E '^ *ldflags *=' "$ninja") || fail "no link flags in build.ninja" "$(head -40 "$ninja")" +grep -q -- '-B[^ ]*xim-x-glibc' <<<"$line" || fail "the link names no crt search prefix in the payload" "$line" +grep -q -- '-L[^ ]*xim-x-glibc' <<<"$line" || fail "the link names no library directory in the payload" "$line" +grep -q -- '--dynamic-linker=[^ ]*xim-x-glibc' <<<"$line" || fail "the link names no loader in the payload" "$line" +if grep -q -- '--sysroot=' <<<"$line"; then + fail "this gcc was linked through a sysroot: the script holds the payload-first form" "$line" +fi +run=$("$MCPP" run --toolchain gcc@15.1.0 2>&1) || fail "the program did not run" "$run" +grep -q "Hello" <<<"$run" || fail "the program's output did not arrive" "$run" +echo "PASS: 1080 gcc over the host's payload links from the payload" diff --git a/tests/e2e/108_cfg_conditional_sources.sh b/tests/e2e/108_cfg_conditional_sources.sh index ed256719c..0d8fe517a 100755 --- a/tests/e2e/108_cfg_conditional_sources.sh +++ b/tests/e2e/108_cfg_conditional_sources.sh @@ -12,7 +12,7 @@ HOST_ARCH="$(uname -m)" case "$HOST_ARCH" in x86_64) THIS=x86_64; OTHER=aarch64 ;; aarch64|arm64) THIS=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: unmapped arch $HOST_ARCH"; exit 0 ;; + *) echo "SKIP: unmapped arch $HOST_ARCH"; exit 77 ;; esac cd "$TMP" diff --git a/tests/e2e/130_freestanding_riscv_build_and_run.sh b/tests/e2e/130_freestanding_riscv_build_and_run.sh index c23106df3..32734dff2 100755 --- a/tests/e2e/130_freestanding_riscv_build_and_run.sh +++ b/tests/e2e/130_freestanding_riscv_build_and_run.sh @@ -54,7 +54,7 @@ for d in "$HOME"/.mcpp/registry/data/xpkgs/*-x-qemu-riscv/*/bin \ "$HOME"/.xlings/data/xpkgs/*-x-qemu-riscv/*/bin; do [[ -x "$d/qemu-system-riscv64" ]] && QEMU="$d/qemu-system-riscv64" done -[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 0; } +[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/131_freestanding_bsp_supplies_everything.sh b/tests/e2e/131_freestanding_bsp_supplies_everything.sh index 3a4c0bc2f..216ee053a 100755 --- a/tests/e2e/131_freestanding_bsp_supplies_everything.sh +++ b/tests/e2e/131_freestanding_bsp_supplies_everything.sh @@ -33,7 +33,7 @@ set -e # about the seam, and a missing payload is an environment fact. MH="${MCPP_HOME:-$HOME/.mcpp}" SYSROOT_ROOT="$MH/registry/data/xpkgs/xim-x-picolibc-riscv" -[[ -d "$SYSROOT_ROOT" ]] || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 0; } +[[ -d "$SYSROOT_ROOT" ]] || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 77; } # Resolve the emulator to a PATH-independent absolute path. # @@ -48,7 +48,7 @@ for d in "$HOME"/.mcpp/registry/data/xpkgs/*-x-qemu-riscv/*/bin \ "$HOME"/.xlings/data/xpkgs/*-x-qemu-riscv/*/bin; do [[ -x "$d/qemu-system-riscv64" ]] && QEMU="$d/qemu-system-riscv64" done -[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 0; } +[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/132_freestanding_test_and_artifacts.sh b/tests/e2e/132_freestanding_test_and_artifacts.sh index b4e4c1648..0817b0a63 100755 --- a/tests/e2e/132_freestanding_test_and_artifacts.sh +++ b/tests/e2e/132_freestanding_test_and_artifacts.sh @@ -27,11 +27,11 @@ for d in "$HOME"/.mcpp/registry/data/xpkgs/*-x-qemu-riscv/*/bin \ "$HOME"/.xlings/data/xpkgs/*-x-qemu-riscv/*/bin; do [[ -x "$d/qemu-system-riscv64" ]] && QEMU="$d/qemu-system-riscv64" done -[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 0; } +[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 77; } MH="${MCPP_HOME:-$HOME/.mcpp}" [[ -d "$MH/registry/data/xpkgs/xim-x-picolibc-riscv" ]] \ - || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 0; } + || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/133_freestanding_std_subset.sh b/tests/e2e/133_freestanding_std_subset.sh index 1de03eb99..e9ada8f8d 100755 --- a/tests/e2e/133_freestanding_std_subset.sh +++ b/tests/e2e/133_freestanding_std_subset.sh @@ -26,11 +26,11 @@ for d in "$HOME"/.mcpp/registry/data/xpkgs/*-x-qemu-riscv/*/bin \ "$HOME"/.xlings/data/xpkgs/*-x-qemu-riscv/*/bin; do [[ -x "$d/qemu-system-riscv64" ]] && QEMU="$d/qemu-system-riscv64" done -[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 0; } +[[ -n "$QEMU" ]] || { echo "SKIP: no qemu-system-riscv64"; exit 77; } MH="${MCPP_HOME:-$HOME/.mcpp}" [[ -d "$MH/registry/data/xpkgs/xim-x-picolibc-riscv" ]] \ - || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 0; } + || { echo "SKIP: xim:picolibc-riscv not installed in $MH"; exit 77; } # libc++'s headers come from the llvm payload; pick by CONTENT, not by name # order — an empty directory for an older version sorts first. @@ -38,10 +38,10 @@ LLVM="" for d in "$MH"/registry/data/xpkgs/xim-x-llvm/*/; do [[ -x "$d/bin/clang++" ]] && LLVM="$d" done -[[ -n "$LLVM" ]] || { echo "SKIP: no llvm payload with clang++"; exit 0; } +[[ -n "$LLVM" ]] || { echo "SKIP: no llvm payload with clang++"; exit 77; } LLVM="${LLVM%/}" [[ -d "$LLVM/share/libc++/v1/std" ]] \ - || { echo "SKIP: llvm payload has no libc++ std/*.inc"; exit 0; } + || { echo "SKIP: llvm payload has no libc++ std/*.inc"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/13_toolchain_pin.sh b/tests/e2e/13_toolchain_pin.sh index 0efd404d2..84c39c29c 100755 --- a/tests/e2e/13_toolchain_pin.sh +++ b/tests/e2e/13_toolchain_pin.sh @@ -27,7 +27,7 @@ if [[ -d "$HOME/.xlings/data/xpkgs/xim-x-gcc/16.1.0" ]]; then else echo "SKIP: system xlings has no gcc 16.1.0" echo "OK" - exit 0 + exit 77 fi export MCPP_HOME="$SANDBOX_HOME" diff --git a/tests/e2e/150_clang_module_operator_template.sh b/tests/e2e/150_clang_module_operator_template.sh index 2b6cd53b3..b77dbd28e 100755 --- a/tests/e2e/150_clang_module_operator_template.sh +++ b/tests/e2e/150_clang_module_operator_template.sh @@ -21,11 +21,11 @@ source "$SCRIPT_DIR/_llvm_env.sh" if [[ ! -d "$LLVM_ROOT" ]]; then echo "SKIP: no llvm payload installed" - exit 0 + exit 77 fi CLANGXX="$LLVM_ROOT/bin/clang++" -[[ -x "$CLANGXX" ]] || { echo "SKIP: no clang++ in $LLVM_ROOT"; exit 0; } +[[ -x "$CLANGXX" ]] || { echo "SKIP: no clang++ in $LLVM_ROOT"; exit 77; } TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT diff --git a/tests/e2e/156_nested_ld_library_path.sh b/tests/e2e/156_nested_ld_library_path.sh index dde37385b..100961120 100755 --- a/tests/e2e/156_nested_ld_library_path.sh +++ b/tests/e2e/156_nested_ld_library_path.sh @@ -14,7 +14,7 @@ MCPP_STATE_HOME=${MCPP_HOME:-"$HOME/.mcpp"} GLIBC_LIB=$(ls -d "$MCPP_STATE_HOME"/registry/data/xpkgs/xim-x-glibc/*/lib64 2>/dev/null | head -1) if [[ -z "$GLIBC_LIB" ]]; then echo "SKIP: no private glibc payload installed" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/163_identity_first_resolution.sh b/tests/e2e/163_identity_first_resolution.sh index c34dd848f..e1be88561 100755 --- a/tests/e2e/163_identity_first_resolution.sh +++ b/tests/e2e/163_identity_first_resolution.sh @@ -35,7 +35,7 @@ for c in "$MCPP_HOME/indices/mcpplibs/pkgs/c/chriskohlhoff.asio.lua"; do done if [ -z "$SRC" ]; then echo "SKIP 163_identity_first_resolution (no asio descriptor in registry)" - exit 0 + exit 77 fi mkidx() { # mkidx diff --git a/tests/e2e/165_bare_name_cross_namespace_wire_address.sh b/tests/e2e/165_bare_name_cross_namespace_wire_address.sh index 6bb12e0ce..19e7cf16f 100755 --- a/tests/e2e/165_bare_name_cross_namespace_wire_address.sh +++ b/tests/e2e/165_bare_name_cross_namespace_wire_address.sh @@ -32,7 +32,7 @@ cd "$TMP" SRC="$MCPP_HOME/indices/mcpplibs/pkgs/c/chriskohlhoff.asio.lua" if [ ! -f "$SRC" ]; then echo "SKIP 165_bare_name_cross_namespace_wire_address (no asio descriptor in registry)" - exit 0 + exit 77 fi # A `compat`-namespaced package named by its SPEC-001 short name. The diff --git a/tests/e2e/166_run_env_no_private_glibc.sh b/tests/e2e/166_run_env_no_private_glibc.sh index ccf56a611..c855ad328 100755 --- a/tests/e2e/166_run_env_no_private_glibc.sh +++ b/tests/e2e/166_run_env_no_private_glibc.sh @@ -85,7 +85,7 @@ GLIBC_STORE=$(ls -d "$HOME"/.mcpp/registry/data/xpkgs/xim-x-glibc/*/ 2>/dev/null if [ -z "$GLIBC_STORE" ]; then echo "SKIP (second half): no private glibc payload installed" echo OK - exit 0 + exit 77 fi cd "$TMP" diff --git a/tests/e2e/168_build_mcpp_musl_host_static.sh b/tests/e2e/168_build_mcpp_musl_host_static.sh index f6b0e6965..58fc4692e 100755 --- a/tests/e2e/168_build_mcpp_musl_host_static.sh +++ b/tests/e2e/168_build_mcpp_musl_host_static.sh @@ -13,7 +13,7 @@ set -euo pipefail if [[ "$(uname -s)" != "Linux" ]]; then echo "SKIP: Linux ELF regression" echo "OK" - exit 0 + exit 77 fi : "${MCPP:?MCPP must point to the mcpp binary under test}" @@ -58,7 +58,7 @@ MUSL_VERSION=$(find_musl_version || true) if [[ -z "$MUSL_VERSION" ]]; then echo "SKIP: no musl-gcc payload installed — not testing the musl host helper" echo "OK" - exit 0 + exit 77 fi echo "musl toolchain: gcc@${MUSL_VERSION}-musl" diff --git a/tests/e2e/201_gcc_no_specs_pollution.sh b/tests/e2e/201_gcc_no_specs_pollution.sh index 20499ad4e..3ce136101 100755 --- a/tests/e2e/201_gcc_no_specs_pollution.sh +++ b/tests/e2e/201_gcc_no_specs_pollution.sh @@ -49,7 +49,7 @@ bin=$(find target -type f -name prog -path '*/bin/*' | head -1) # `readelf` may be an xvm shim needing a per-tool pin; use the payload's own. readelf=$(ls "$MCPP_HOME"/registry/data/xpkgs/xim-x-binutils/*/bin/readelf 2>/dev/null | head -1) -[[ -x "$readelf" ]] || { echo "SKIP: no payload readelf to inspect with"; exit 0; } +[[ -x "$readelf" ]] || { echo "SKIP: no payload readelf to inspect with"; exit 77; } runpath=$("$readelf" -d "$bin" 2>/dev/null | grep -iE 'RUNPATH|RPATH' | sed 's/.*\[\(.*\)\]/\1/' || true) diff --git a/tests/e2e/208_private_libc_stays_in_the_binary.sh b/tests/e2e/208_private_libc_stays_in_the_binary.sh index 189617cc0..8300a308c 100755 --- a/tests/e2e/208_private_libc_stays_in_the_binary.sh +++ b/tests/e2e/208_private_libc_stays_in_the_binary.sh @@ -101,7 +101,7 @@ NINJA=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) GLIBC_LIB=$(grep -oE '/[^ ",]*/xim-x-glibc/[0-9.]+/lib(64)?' "$NINJA" | head -1 || true) if [[ -z "$GLIBC_LIB" ]]; then echo "SKIP: this toolchain has no private glibc payload" - exit 0 + exit 77 fi echo "private libc payload: $GLIBC_LIB" diff --git a/tests/e2e/209_runtime_verdict_never_reruns_green.sh b/tests/e2e/209_runtime_verdict_never_reruns_green.sh index d9b947f96..1aa2fe1bf 100755 --- a/tests/e2e/209_runtime_verdict_never_reruns_green.sh +++ b/tests/e2e/209_runtime_verdict_never_reruns_green.sh @@ -41,7 +41,7 @@ cd app CACHE=$(find target -name '.mcpp-runtime-verdicts.json' | head -1) if [[ -z "$CACHE" ]]; then echo "SKIP: no glibc runtime binding on this host (no stored verdicts)" - exit 0 + exit 77 fi # Every artifact must have been validated and recorded as passing, or the rest diff --git a/tests/e2e/220_farm_only_library_runs.sh b/tests/e2e/220_farm_only_library_runs.sh index 605597a26..074e32065 100755 --- a/tests/e2e/220_farm_only_library_runs.sh +++ b/tests/e2e/220_farm_only_library_runs.sh @@ -161,7 +161,7 @@ if [[ -z "$FARM_LIB" ]]; then echo "SKIP: this SubOS farm provides no library the payloads do not already" echo " provide, so there is nothing whose loading proves the farm is" echo " reachable. Install any library into the SubOS to exercise this." - exit 0 + exit 77 fi echo "farm-only library: -l$FARM_LIB (from $FARM_DIR)" diff --git a/tests/e2e/230_bench_harness.sh b/tests/e2e/230_bench_harness.sh index 4d9b7ce2e..897f7719e 100755 --- a/tests/e2e/230_bench_harness.sh +++ b/tests/e2e/230_bench_harness.sh @@ -19,7 +19,7 @@ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # delegator lives in mcpp's own tree and can simply point at what was built. if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$REPO/.github/tools/newest_artifact.sh" "$REPO" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 77; } export MCPP fi exec bash "$REPO/bench/tests/harness.sh" diff --git a/tests/e2e/231_jobs_option.sh b/tests/e2e/231_jobs_option.sh index 0042f0556..8ffa6b991 100755 --- a/tests/e2e/231_jobs_option.sh +++ b/tests/e2e/231_jobs_option.sh @@ -20,7 +20,7 @@ set -e if [ -z "${MCPP:-}" ]; then _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 77; } case "$MCPP" in /*|?:[/\\]*) ;; *) MCPP="$_root/$MCPP" ;; esac export MCPP fi diff --git a/tests/e2e/234_bmi_schedule_on.sh b/tests/e2e/234_bmi_schedule_on.sh index c05246336..c4dd79bb7 100755 --- a/tests/e2e/234_bmi_schedule_on.sh +++ b/tests/e2e/234_bmi_schedule_on.sh @@ -31,7 +31,7 @@ set -e _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 77; } fi # ABSOLUTISE WHATEVER WE GOT, not just what we derived. This test `cd`s into a # temp directory, so a RELATIVE MCPP — which is what `ls -t target/*/*/bin/mcpp` @@ -90,7 +90,7 @@ EOF # detach-codegen is the GCC strategy. On a machine whose default toolchain # resolves to clang the decision is TwoPhase and there is no semaphore at all, # so the second contract has nothing to test — say so rather than pass hollowly. -say_skip_reason() { echo "SKIP: $1"; exit 0; } +say_skip_reason() { echo "SKIP: $1"; exit 77; } echo "== 1. builds with bmi_schedule = \"on\" ==" "$MCPP" build --release --verbose > build.log 2>&1 || { diff --git a/tests/e2e/235_std_object_only_when_needed.sh b/tests/e2e/235_std_object_only_when_needed.sh index 393cda5d7..21972c022 100755 --- a/tests/e2e/235_std_object_only_when_needed.sh +++ b/tests/e2e/235_std_object_only_when_needed.sh @@ -25,7 +25,7 @@ set -e _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 77; } fi case "$MCPP" in /*) ;; *) MCPP="$_root/$MCPP" ;; esac [ -x "$MCPP" ] || { echo "FAIL: MCPP=$MCPP is not executable"; exit 1; } diff --git a/tests/e2e/236_module_extensions_default_toolchain.sh b/tests/e2e/236_module_extensions_default_toolchain.sh index 3ab7ca0a2..ad7f24312 100755 --- a/tests/e2e/236_module_extensions_default_toolchain.sh +++ b/tests/e2e/236_module_extensions_default_toolchain.sh @@ -26,7 +26,7 @@ set -e _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 77; } fi case "$MCPP" in /*|?:[/\\]*) ;; *) MCPP="$_root/$MCPP" ;; esac [ -x "$MCPP" ] || { echo "FAIL: MCPP=$MCPP is not executable"; exit 1; } diff --git a/tests/e2e/237_default_subos_without_info.sh b/tests/e2e/237_default_subos_without_info.sh index bb2c0bc7c..a3a052a6b 100755 --- a/tests/e2e/237_default_subos_without_info.sh +++ b/tests/e2e/237_default_subos_without_info.sh @@ -44,7 +44,7 @@ set -e _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 77; } fi case "$MCPP" in /*) ;; *) MCPP="$_root/$MCPP" ;; esac [ -x "$MCPP" ] || { echo "FAIL: MCPP=$MCPP is not executable"; exit 1; } @@ -62,7 +62,7 @@ mkdir -p "$MCPP_HOME/registry/data" "$MCPP_HOME/registry/subos/default" if [ -d "$REAL_HOME/registry/data/xpkgs" ]; then ln -s "$REAL_HOME/registry/data/xpkgs" "$MCPP_HOME/registry/data/xpkgs" else - echo "SKIP: no xpkgs payloads to inherit from $REAL_HOME"; exit 0 + echo "SKIP: no xpkgs payloads to inherit from $REAL_HOME"; exit 77 fi for d in "$REAL_HOME/registry"/*; do b="$(basename "$d")" @@ -146,7 +146,7 @@ PY )" [ -n "$GLIBC_VER" ] && [ -d "$GLIBC_DIR/$GLIBC_VER" ] \ || GLIBC_VER="$(ls "$GLIBC_DIR" 2>/dev/null | head -1)" -[ -n "$GLIBC_VER" ] || { echo "SKIP: no glibc payload to bind to"; exit 0; } +[ -n "$GLIBC_VER" ] || { echo "SKIP: no glibc payload to bind to"; exit 77; } python3 - "$MCPP_HOME/registry/subos/default/.xlings.json" "$GLIBC_VER" <<'PY' import json, sys path, ver = sys.argv[1], sys.argv[2] diff --git a/tests/e2e/238_c_only_unit_links_with_c_driver.sh b/tests/e2e/238_c_only_unit_links_with_c_driver.sh index 6f4717896..313f08441 100755 --- a/tests/e2e/238_c_only_unit_links_with_c_driver.sh +++ b/tests/e2e/238_c_only_unit_links_with_c_driver.sh @@ -31,7 +31,7 @@ set -e _root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" - [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 0; } + [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet"; exit 77; } fi case "$MCPP" in /*) ;; *) MCPP="$_root/$MCPP" ;; esac [ -x "$MCPP" ] || { echo "FAIL: MCPP=$MCPP is not executable"; exit 1; } diff --git a/tests/e2e/239_msvc_managed_toolset.sh b/tests/e2e/239_msvc_managed_toolset.sh index 942bbba55..b00bb7bee 100755 --- a/tests/e2e/239_msvc_managed_toolset.sh +++ b/tests/e2e/239_msvc_managed_toolset.sh @@ -63,7 +63,7 @@ cd "$TMP" # so that is the question to ask. if ! "$MCPP" toolchain list 2>&1 | grep -q "$TOOLSET"; then echo "SKIP: this index does not offer msvc $TOOLSET (publish window? offline?)" - exit 0 + exit 77 fi # 1) install it. Any failure from here on is a FAILURE. diff --git a/tests/e2e/254_pack_library_platform_coverage.sh b/tests/e2e/254_pack_library_platform_coverage.sh index 69e2f6b95..45b0dc1db 100755 --- a/tests/e2e/254_pack_library_platform_coverage.sh +++ b/tests/e2e/254_pack_library_platform_coverage.sh @@ -22,11 +22,15 @@ # # So the silence is as much the feature as the warning, and both are asserted. # -# THE PER-HOST PICKS BELOW MIRROR host_can_serve (registry.cppm:542-566). -# That is deliberate and it is a tripwire: if mcpp ever gains, say, a -# macOS-hosted Windows toolchain, this test starts failing — which is the -# correct outcome, because the table it encodes will have changed and the -# expectations here have to be re-derived rather than assumed. +# THE PER-HOST PICKS BELOW MIRROR THE PROVIDERS' `hosts` (data/builtin.toml, +# which `host_can_serve` reads since 2026.11.12.1). That is deliberate and it +# is a tripwire: when a provider's hosts change, this test starts failing — +# which is the correct outcome, because the table it encodes will have changed +# and the expectations here have to be re-derived rather than assumed. It +# tripped once, as predicted: the `mingw-llvm` provider (2026.11.10.1) serves +# the GNU-ABI Windows rows on every host, a macOS host included, and the +# function this test mirrored then still said a macOS host builds for macOS +# alone. set -e TMP=$(mktemp -d) @@ -79,10 +83,10 @@ echo "host platform: $HOST_OS (from target/$host_triple)" case "$HOST_OS" in linux) UNSERVABLE=macos SERVABLE_UNPACKED=windows ;; windows) UNSERVABLE=macos SERVABLE_UNPACKED=linux ;; - # macOS serves exactly one target — "macOS has no Linux-targeting payload at - # all", and PE needs a Windows or Linux host — so there is no - # servable-but-unpacked platform to name here. Structural, not a gap. - macos) UNSERVABLE=windows SERVABLE_UNPACKED= ;; + # macOS has no Linux-targeting payload (a Linux identity is the graph's + # there, which is not this question), and it builds PE through the + # MinGW-w64 base package as every host does. + macos) UNSERVABLE=linux SERVABLE_UNPACKED=windows ;; esac # ── 1. packed, not declared ───────────────────────────────────────────── diff --git a/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh b/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh index 678f45d8b..c4f93e737 100755 --- a/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh +++ b/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh @@ -58,12 +58,12 @@ cd app cross=x86_64-linux-musl if ! "$MCPP" build --target "$cross" >/dev/null 2>&1; then - echo "SKIP: this machine cannot build $cross"; exit 0 + echo "SKIP: this machine cannot build $cross"; exit 77 fi rm -rf target if ! "$MCPP" run --target "$cross" >/dev/null 2>&1; then - echo "SKIP: $cross builds but does not run here"; exit 0 + echo "SKIP: $cross builds but does not run here"; exit 77 fi # ── The command under test ────────────────────────────────────────────────── diff --git a/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh b/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh index fe1f22e84..336f63da4 100755 --- a/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh +++ b/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh @@ -106,7 +106,7 @@ esac case "$out" in *"c-abi"*"graph"*) echo "SKIP: the C library came from the graph too — not the shape under test" - exit 0 ;; + exit 77 ;; esac if [ "$rc" != 0 ]; then diff --git a/tests/e2e/286_the_openkal_stack_still_builds.sh b/tests/e2e/286_the_openkal_stack_still_builds.sh index b751a8ecc..da7d86a16 100755 --- a/tests/e2e/286_the_openkal_stack_still_builds.sh +++ b/tests/e2e/286_the_openkal_stack_still_builds.sh @@ -92,7 +92,7 @@ if ! out="$("$MCPP" build 2>&1)"; then case "$out" in *"not found in the synced index"*|*"install_packages failed"*) echo "SKIP: the openkal packages are not reachable from here" - exit 0 ;; + exit 77 ;; esac echo "FAIL: the openkal stack did not build" printf '%s\n' "$out" | grep -iE 'error' | head -5 diff --git a/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh b/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh index 535b6942e..711bccaed 100755 --- a/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh +++ b/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh @@ -62,9 +62,9 @@ CPP if ! out="$("$MCPP" build --target "$TARGET" 2>&1)"; then case "$out" in *"not found in the synced index"*|*"install_packages failed"*) - echo "SKIP: the openkal packages are not reachable from here"; exit 0 ;; + echo "SKIP: the openkal packages are not reachable from here"; exit 77 ;; *"cannot be built on this host"*) - echo "SKIP: $TARGET cannot be built here"; exit 0 ;; + echo "SKIP: $TARGET cannot be built here"; exit 77 ;; esac echo "FAIL: the cross build failed" printf '%s\n' "$out" | grep -iE 'error' | head -5 diff --git a/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh b/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh index 4c85b9450..a1294f539 100755 --- a/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh +++ b/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh @@ -85,9 +85,9 @@ CPP if ! out="$("$MCPP" build --target "$TARGET" 2>&1)"; then case "$out" in *"not found in the synced index"*|*"install_packages failed"*) - echo "SKIP: openkal-opensbi is not reachable from here"; exit 0 ;; + echo "SKIP: openkal-opensbi is not reachable from here"; exit 77 ;; *"cannot be built on this host"*) - echo "SKIP: $TARGET cannot be built here"; exit 0 ;; + echo "SKIP: $TARGET cannot be built here"; exit 77 ;; esac echo "FAIL: the freestanding openkal build failed" printf '%s\n' "$out" | grep -iE 'error' | head -5 diff --git a/tests/e2e/289_one_host_reaches_every_openkal_target.sh b/tests/e2e/289_one_host_reaches_every_openkal_target.sh index 2384063d4..fed6c6cb3 100755 --- a/tests/e2e/289_one_host_reaches_every_openkal_target.sh +++ b/tests/e2e/289_one_host_reaches_every_openkal_target.sh @@ -144,7 +144,7 @@ printf '\n built %d, skipped %d, failed %d\n' "$built" "$skipped" "$failed" # reporting as a skip of the whole file rather than as success. if [ "$built" = 0 ]; then echo "SKIP: no target could be built here" - exit 0 + exit 77 fi echo "OK: one host reached $built of the ecosystem's targets" diff --git a/tests/e2e/28_target_static.sh b/tests/e2e/28_target_static.sh index 07b86a2cf..cbb8fea19 100755 --- a/tests/e2e/28_target_static.sh +++ b/tests/e2e/28_target_static.sh @@ -18,7 +18,7 @@ if ! command -v x86_64-linux-musl-g++ >/dev/null 2>&1 \ then echo "SKIP: no musl-gcc 15.1.0 available — not testing --target=*-linux-musl" echo "OK" - exit 0 + exit 77 fi # Detect installed GNU gcc version for the default-build regression check. diff --git a/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh b/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh index 2389458cf..4fd1a3e7c 100755 --- a/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh +++ b/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh @@ -70,7 +70,7 @@ make_project "$work/plain" "" plain="$(run_probe "$work/plain")" if [ -z "$plain" ]; then echo "SKIP: the build program did not report — it may not have run here" - exit 0 + exit 77 fi if [ "$plain" = "$inherited" ]; then @@ -93,7 +93,7 @@ subos = "default"' declared="$(run_probe "$work/declared")" if [ -z "$declared" ]; then echo "SKIP: the declaring project's build program did not report" - exit 0 + exit 77 fi if [ "$declared" = "$inherited" ]; then diff --git a/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh b/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh index 0307579d1..efb6a5639 100755 --- a/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh +++ b/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh @@ -53,7 +53,7 @@ base="$(resolved "$work/plain" riscv64-none-elf)" case "$base" in llvm@*) echo " ok baseline: the bare-metal row resolves $base" ;; *) echo "SKIP: the bare-metal row did not resolve to llvm here (got '${base:-nothing}')" - exit 0 ;; + exit 77 ;; esac make_project "$work/declares" 'provides = ["mcpp:kernel-abi=openkal"]' diff --git a/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh b/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh index 3aa312e0d..46fc9bfed 100755 --- a/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh +++ b/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh @@ -134,7 +134,7 @@ done if [ "$built" = 0 ]; then echo "SKIP: no target resolved here — the sweep proved nothing ($skipped skipped)" - exit 0 + exit 77 fi echo " ok $built correct cross targets went through untouched" diff --git a/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh b/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh index 333d33c0d..209340872 100755 --- a/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh +++ b/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh @@ -29,7 +29,7 @@ targets="$(printf '%s\n' "$out" | awk '/^Targets:/,0')" if [ -z "$targets" ]; then echo "SKIP: this build printed no Targets section" - exit 0 + exit 77 fi row() { printf '%s\n' "$targets" | grep -E "^\s+\*?\s*$1(\s|$)" | head -1; } @@ -41,7 +41,7 @@ row() { printf '%s\n' "$targets" | grep -E "^\s+\*?\s*$1(\s|$)" | head -1; } # means something else. case "$(uname -s)" in Linux) ;; - *) echo "SKIP: the graph-served row under test is a Linux-host arrangement"; exit 0 ;; + *) echo "SKIP: the graph-served row under test is a Linux-host arrangement"; exit 77 ;; esac wm="$(row x86_64-windows-musl)" @@ -72,7 +72,7 @@ esac n="$(printf '%s\n' "$targets" | grep -cE '^\s+\*?\s*[a-z0-9_]+-' || true)" if [ "${n:-0}" -lt 3 ]; then echo "SKIP: only ${n:-0} target rows printed — too few to assert an absence" - exit 0 + exit 77 fi echo " ok $n target rows printed, so an absence below means something" diff --git a/tests/e2e/295_naming_the_host_target_changes_nothing.sh b/tests/e2e/295_naming_the_host_target_changes_nothing.sh index ea3667efc..3a2a07b89 100755 --- a/tests/e2e/295_naming_the_host_target_changes_nothing.sh +++ b/tests/e2e/295_naming_the_host_target_changes_nothing.sh @@ -187,7 +187,7 @@ done if [ "$checked" = 0 ]; then echo "SKIP: no toolchain here produced a link line to compare" - exit 0 + exit 77 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 index bc007ade7..23e1ec037 100755 --- a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh +++ b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh @@ -215,7 +215,7 @@ esac if [ "$checked" = 0 ]; then echo "SKIP: nothing here produced a link line to relate to a report" - exit 0 + exit 77 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 index a7b64a319..59a35721d 100755 --- a/tests/e2e/297_a_capability_pin_is_not_a_preference.sh +++ b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh @@ -45,7 +45,7 @@ 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 + exit 77 fi # LISTED IS NOT USABLE. A registry entry extracted from another host's @@ -69,7 +69,7 @@ serveReason="$(cd probe && "$MCPP" why toolchain --toolchain "gcc@$gccver" \ | jq -r '.data.reason // "none"' | tr -d '\r')" if [ "$serveReason" != "none" ]; then echo "SKIP: gcc is not installed here, and this test is about declaring it" - exit 0 + exit 77 fi # ── Half one: a bare-metal target refuses, and says why ─────────────────── @@ -146,7 +146,7 @@ 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 + exit 77 fi rm -rf target hosted="$(cd "$work" && "$MCPP" build --target "$hostedTarget" 2>&1 || true)" diff --git a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh index d3d4ae54e..fd48a419c 100644 --- a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh +++ b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh @@ -46,7 +46,7 @@ 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 + exit 77 fi fail=0 @@ -118,7 +118,7 @@ pinned="$(pick "$hostArch")" [ -n "$pinned" ] || pinned="$(pick "")" if [ -z "$pinned" ]; then echo "SKIP: no target row here is served by a non-llvm convention" - exit 0 + exit 77 fi printf '[package]\nname = "convprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefault = "llvm@%s"\n' \ @@ -152,7 +152,7 @@ case "$reason" in fail=1 ;; *) echo "SKIP: $pinned refused under '$reason', which is not the rule this test is about" - exit 0 ;; + exit 77 ;; esac # AND IT POINTS AT THE WAY OUT. A refusal that names no replacement leaves diff --git a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh index 92ea117f0..7fa686c6f 100755 --- a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh +++ b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh @@ -65,7 +65,8 @@ resolved_row() { # request → the row it resolved to t="$(printf '%s' "$doc" | jq -r '.data.triple.llvm // empty')" if [ -n "$t" ]; then printf '%s' "$t"; return; fi printf '%s' "$doc" | jq -r '.diagnostics[0].message // empty' \ - | head -1 | sed -n "s/^[^']*'\\([^']*\\)'.*/\\1/p" + | head -1 | sed -n -e "s/^[^']*'\([^']*\)'.*/\1/p" \ + -e "s/^\[target\.\([^]]*\)\].*/\1/p" } message_of() { "$MCPP" why toolchain --target "$1" --format json 2>/dev/null \ @@ -86,9 +87,27 @@ case "$r" in host-cannot-serve) echo " ok --target aarch64-linux completed (this host serves no such payload)" ;; *) - echo "FAIL: --target aarch64-linux refused for reason '$r'" - message_of aarch64-linux | sed 's/^/ /' - exit 1 ;; + # THE ROW'S PAYLOAD IS A PACKAGE, AND A PACKAGE MAY NOT BE INSTALLABLE + # WHERE THIS RUNS (2026.11.11.1, G1): the GNU row names its sysroot, and + # an index without it -- a CI runner ahead of the index PR, an offline + # machine -- refuses about the PACKAGE, quoting the row it resolved. That + # refusal still answers this half's question: the request reached a row + # of the system, which the refusal's subject and the row checks below + # both name. Anything else it names is not about the row and stays red. + if [ "$row" = aarch64-linux-gnu ] || [ "$row" = aarch64-unknown-linux-gnu ] \ + || [ "$row" = aarch64-linux-musl ] || [ "$row" = aarch64-unknown-linux-musl ]; then + if message_of aarch64-linux | grep -q "could not be installed"; then + echo " ok --target aarch64-linux reached a row whose payload package is not installable here" + else + echo "FAIL: --target aarch64-linux refused for reason '$r'" + message_of aarch64-linux | sed 's/^/ /' + exit 1 + fi + else + echo "FAIL: --target aarch64-linux refused for reason '$r'" + message_of aarch64-linux | sed 's/^/ /' + exit 1 + fi ;; esac # AND THE ROW IS THE IDENTITY, NOT THE SPELLING. The output directory is the diff --git a/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh b/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh index 9005c910d..e9db8f66e 100755 --- a/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh +++ b/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh @@ -74,7 +74,7 @@ done if [ -z "$want" ]; then echo "SKIP: this project already resolves '$current' and no other family is" echo " installed here, so no requirement could differ from it" - exit 0 + exit 77 fi printf '[package]\nname = "needs-%s"\nversion = "0.1.0"\nrequires = ["mcpp:compiler=%s"]\n' \ diff --git a/tests/e2e/31_transitive_deps.sh b/tests/e2e/31_transitive_deps.sh index 2ff5edcb6..50218cee0 100755 --- a/tests/e2e/31_transitive_deps.sh +++ b/tests/e2e/31_transitive_deps.sh @@ -18,7 +18,7 @@ MUSL_PAYLOAD="$MCPP_HOME/registry/data/xpkgs/xim-x-musl-gcc/15.1.0/bin/x86_64-li if [[ ! -x "$MUSL_PAYLOAD" ]]; then echo "SKIP: no reusable musl-gcc xpkg payload" echo "OK" - exit 0 + exit 77 fi # ── 1. Grandchild: a header-providing C lib whose `[build].include_dirs` diff --git a/tests/e2e/330_runner_hosted_targets.sh b/tests/e2e/330_runner_hosted_targets.sh index 3392503c8..5e5d36347 100644 --- a/tests/e2e/330_runner_hosted_targets.sh +++ b/tests/e2e/330_runner_hosted_targets.sh @@ -121,7 +121,9 @@ if [[ "$(uname -s)" == Linux ]]; then grep -q "this host cannot execute" <<<"$out" || fail "unrunnable message missing: $out" grep -q "Exec format error" <<<"$out" || fail "the kernel's answer is missing: $out" grep -q "\[target.$HOST\]" <<<"$out" || fail "paste-able key missing: $out" - grep -q 'runner = \["qemu-aarch64-static"\]' <<<"$out" || fail "runner example missing: $out" + grep -q 'runner = \["", "{}"\]' <<<"$out" || fail "the runner declaration's shape is missing: $out" + # No program is named: which one runs the artifact is a statement of the data (SPEC-016 §6). + if grep -q "qemu" <<<"$out"; then fail "the message names an emulator: $out"; fi grep -q -- "--no-runner" <<<"$out" || fail "escape hatch not mentioned: $out" # The criterion measured the patched artifact, not a rebuilt one. [[ "$(od -An -tx1 -j18 -N2 "$bin" | tr -d ' ')" == "ffff" ]] \ diff --git a/tests/e2e/332_cortex_m_builds_and_boots.sh b/tests/e2e/332_cortex_m_builds_and_boots.sh index 1f81d41c3..67de8dfa0 100755 --- a/tests/e2e/332_cortex_m_builds_and_boots.sh +++ b/tests/e2e/332_cortex_m_builds_and_boots.sh @@ -38,7 +38,7 @@ qemu_arm() { command -v qemu-system-arm 2>/dev/null && return 0 return 1 } -QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 0; } +QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 77; } llvm_tool() { local c @@ -46,8 +46,8 @@ llvm_tool() { [ -n "$c" ] && { echo "$c"; return 0; } command -v "$1" 2>/dev/null } -NM="$(llvm_tool llvm-nm)" || { echo "SKIP: llvm-nm not found"; exit 0; } -OBJDUMP="$(llvm_tool llvm-objdump)" || { echo "SKIP: llvm-objdump not found"; exit 0; } +NM="$(llvm_tool llvm-nm)" || { echo "SKIP: llvm-nm not found"; exit 77; } +OBJDUMP="$(llvm_tool llvm-objdump)" || { echo "SKIP: llvm-objdump not found"; exit 77; } mkdir -p "$work/mcu/src" cd "$work/mcu" diff --git a/tests/e2e/333_named_runners_locked_and_sbom.sh b/tests/e2e/333_named_runners_locked_and_sbom.sh index 57c1b70b8..8d55025f1 100755 --- a/tests/e2e/333_named_runners_locked_and_sbom.sh +++ b/tests/e2e/333_named_runners_locked_and_sbom.sh @@ -92,7 +92,7 @@ cat > src/main.cpp <<'CPP' int main() { return 0; } CPP -"$MCPP" build >/dev/null 2>&1 || { echo "SKIP: cmdline@0.0.1 unavailable"; exit 0; } +"$MCPP" build >/dev/null 2>&1 || { echo "SKIP: cmdline@0.0.1 unavailable"; exit 77; } test -f mcpp.lock || { echo "FAIL: no mcpp.lock after a build with a dependency"; exit 1; } "$MCPP" build --locked >/dev/null 2>&1 \ diff --git a/tests/e2e/334_dependency_declared_tool_is_found.sh b/tests/e2e/334_dependency_declared_tool_is_found.sh index 37ff097e6..2ae3a734d 100755 --- a/tests/e2e/334_dependency_declared_tool_is_found.sh +++ b/tests/e2e/334_dependency_declared_tool_is_found.sh @@ -76,7 +76,7 @@ PATH_WITHOUT_TOOL="$PATH" case ":$PATH_WITHOUT_TOOL:" in *":$work/fakepkg/bin:"*) echo "FAIL: fixture leaked onto PATH"; exit 1 ;; esac -command -v demo-tool >/dev/null 2>&1 && { echo "SKIP: a demo-tool already on PATH"; exit 0; } +command -v demo-tool >/dev/null 2>&1 && { echo "SKIP: a demo-tool already on PATH"; exit 77; } # `[xlings] deps` resolution needs a real xim package, which this fixture is # not. What is asserted instead is the ordering the fix establishes: the lookup diff --git a/tests/e2e/336_armv7a_builds_and_boots.sh b/tests/e2e/336_armv7a_builds_and_boots.sh index 9542e2dac..5e903d673 100755 --- a/tests/e2e/336_armv7a_builds_and_boots.sh +++ b/tests/e2e/336_armv7a_builds_and_boots.sh @@ -29,7 +29,7 @@ qemu_arm() { command -v qemu-system-arm 2>/dev/null && return 0 return 1 } -QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 0; } +QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 77; } llvm_tool() { local c @@ -37,8 +37,8 @@ llvm_tool() { [ -n "$c" ] && { echo "$c"; return 0; } command -v "$1" 2>/dev/null } -NM="$(llvm_tool llvm-nm)" || { echo "SKIP: llvm-nm not found"; exit 0; } -OBJDUMP="$(llvm_tool llvm-objdump)" || { echo "SKIP: llvm-objdump not found"; exit 0; } +NM="$(llvm_tool llvm-nm)" || { echo "SKIP: llvm-nm not found"; exit 77; } +OBJDUMP="$(llvm_tool llvm-objdump)" || { echo "SKIP: llvm-objdump not found"; exit 77; } mkdir -p "$work/soc/src" cd "$work/soc" diff --git a/tests/e2e/338_cortex_m_picolibc_sysroot.sh b/tests/e2e/338_cortex_m_picolibc_sysroot.sh index 13247bed8..7112ffe62 100755 --- a/tests/e2e/338_cortex_m_picolibc_sysroot.sh +++ b/tests/e2e/338_cortex_m_picolibc_sysroot.sh @@ -34,7 +34,7 @@ qemu_arm() { command -v qemu-system-arm 2>/dev/null && return 0 return 1 } -QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 0; } +QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 77; } llvm_tool() { local c @@ -42,7 +42,7 @@ llvm_tool() { [ -n "$c" ] && { echo "$c"; return 0; } command -v "$1" 2>/dev/null } -READELF="$(llvm_tool llvm-readelf)" || { echo "SKIP: llvm-readelf not found"; exit 0; } +READELF="$(llvm_tool llvm-readelf)" || { echo "SKIP: llvm-readelf not found"; exit 77; } # The payload has to be present. Installing it here rather than skipping # would make the test about the install; skipping when it is absent keeps the @@ -61,7 +61,7 @@ sysroot_dir() { done return 1 } -SYSROOT="$(sysroot_dir)" || { echo "SKIP: picolibc-arm is not installed"; exit 0; } +SYSROOT="$(sysroot_dir)" || { echo "SKIP: picolibc-arm is not installed"; exit 77; } mkdir -p "$work/fw/src" cd "$work/fw" diff --git a/tests/e2e/36_llvm_toolchain.sh b/tests/e2e/36_llvm_toolchain.sh index 13eb23085..6fea4f9db 100755 --- a/tests/e2e/36_llvm_toolchain.sh +++ b/tests/e2e/36_llvm_toolchain.sh @@ -14,7 +14,7 @@ else fi if [[ ! -x "$CLANGPP_BIN" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/37_llvm_import_std.sh b/tests/e2e/37_llvm_import_std.sh index efb043124..55e28d3b4 100755 --- a/tests/e2e/37_llvm_import_std.sh +++ b/tests/e2e/37_llvm_import_std.sh @@ -9,18 +9,18 @@ OS="$(uname -s)" # check in run_all.sh already gates this, but guard here too for direct runs. if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then echo "SKIP: libc++ std.cppm not available on Windows" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/38_llvm_modules.sh b/tests/e2e/38_llvm_modules.sh index f03ee7e12..d1460d5fb 100755 --- a/tests/e2e/38_llvm_modules.sh +++ b/tests/e2e/38_llvm_modules.sh @@ -13,18 +13,18 @@ OS="$(uname -s)" # check in run_all.sh already gates this, but guard here too for direct runs. if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then echo "SKIP: libc++ std.cppm not available on Windows" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/39_llvm_incremental.sh b/tests/e2e/39_llvm_incremental.sh index 09cc26e8b..fcf918408 100755 --- a/tests/e2e/39_llvm_incremental.sh +++ b/tests/e2e/39_llvm_incremental.sh @@ -7,11 +7,11 @@ source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/40_llvm_bmi_cache.sh b/tests/e2e/40_llvm_bmi_cache.sh index 3f7b2b566..d324ef305 100755 --- a/tests/e2e/40_llvm_bmi_cache.sh +++ b/tests/e2e/40_llvm_bmi_cache.sh @@ -9,18 +9,18 @@ OS="$(uname -s)" # check in run_all.sh already gates this, but guard here too for direct runs. if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then echo "SKIP: libc++ std.cppm not available on Windows" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/41_llvm_std_compat.sh b/tests/e2e/41_llvm_std_compat.sh index bcb03bd4a..33acfbc1e 100755 --- a/tests/e2e/41_llvm_std_compat.sh +++ b/tests/e2e/41_llvm_std_compat.sh @@ -9,18 +9,18 @@ OS="$(uname -s)" # capability check in run_all.sh already gates this, but guard here too. if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then echo "SKIP: libc++ std.compat.cppm not available on Windows" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.compat.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.compat.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/47_cdb_prebuilt_module_path_abs.sh b/tests/e2e/47_cdb_prebuilt_module_path_abs.sh index d74491a0b..f83c35865 100755 --- a/tests/e2e/47_cdb_prebuilt_module_path_abs.sh +++ b/tests/e2e/47_cdb_prebuilt_module_path_abs.sh @@ -40,7 +40,7 @@ fi command -v jq >/dev/null 2>&1 || { echo "SKIP: jq not on PATH (preinstalled on GitHub-hosted runners)" - exit 0 + exit 77 } # jq returns each value JSON-unescaped (\\ → \, etc.). Stage to a temp diff --git a/tests/e2e/47_llvm_atomic_link.sh b/tests/e2e/47_llvm_atomic_link.sh index 42d794fe1..72194d06c 100755 --- a/tests/e2e/47_llvm_atomic_link.sh +++ b/tests/e2e/47_llvm_atomic_link.sh @@ -10,18 +10,18 @@ set -e OS="$(uname -s)" if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then echo "SKIP: libatomic/-latomic path is Linux-only" - exit 0 + exit 77 fi if [[ "$OS" == Darwin* ]]; then echo "SKIP: libatomic is a GNU/Linux runtime; not applicable on macOS" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi # The fix only emits -latomic when a link-resolvable libatomic is present in # the toolchain (self-guarding). If a slimmed package does not bundle it, the @@ -29,7 +29,7 @@ fi if [[ ! -e "$LLVM_ROOT/lib/x86_64-unknown-linux-gnu/libatomic.so" \ && ! -e "$LLVM_ROOT/lib/x86_64-unknown-linux-gnu/libatomic.a" ]]; then echo "SKIP: llvm@${LLVM_VERSION} package ships no link-resolvable libatomic" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/650_wasm_row_names_its_launcher.sh b/tests/e2e/650_wasm_row_names_its_launcher.sh index e8e0eb279..722b5771f 100755 --- a/tests/e2e/650_wasm_row_names_its_launcher.sh +++ b/tests/e2e/650_wasm_row_names_its_launcher.sh @@ -78,7 +78,7 @@ fi if [ "$have_emsdk" -ne 1 ]; then echo "SKIP: 650/2 -- no emsdk payload installed (looked under xim-x-emsdk/*/emscripten)" echo "PASS: 650 (part 1 only)" - exit 0 + exit 77 fi echo "== 650/2: build, run and pack a 1-2-3 program for wasm32-emscripten ==" diff --git a/tests/e2e/653_exceptions_is_the_second_abi_member.sh b/tests/e2e/653_exceptions_is_the_second_abi_member.sh index 1f0601bb2..523ef25ab 100755 --- a/tests/e2e/653_exceptions_is_the_second_abi_member.sh +++ b/tests/e2e/653_exceptions_is_the_second_abi_member.sh @@ -39,8 +39,7 @@ for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripte done if [ "$have_emsdk" -ne 1 ]; then echo "SKIP: 653 -- no emsdk payload installed (looked under xim-x-emsdk/*/emscripten)" - echo "PASS: 653 (skipped)" - exit 0 + exit 77 fi echo "== 653/1: a Linux build of a manifest carrying the table is byte-identical to one without ==" diff --git a/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh b/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh index dc8b1b312..b811da2d9 100755 --- a/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh +++ b/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh @@ -76,7 +76,7 @@ for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripte done if [ "$have_emsdk" -ne 1 ]; then echo "SKIP: 657 -- no emsdk payload installed (looked under xim-x-emsdk/*/emscripten)" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/65_toolchain_runtime_dirs_for_run.sh b/tests/e2e/65_toolchain_runtime_dirs_for_run.sh index dad558146..684c0513f 100755 --- a/tests/e2e/65_toolchain_runtime_dirs_for_run.sh +++ b/tests/e2e/65_toolchain_runtime_dirs_for_run.sh @@ -19,14 +19,14 @@ set -e OS="$(uname -s)" if [[ "$OS" != "Linux" ]]; then echo "SKIP: LD_LIBRARY_PATH runtime-dir check is Linux-specific" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/684_a_runner_supplied_twice_is_refused.sh b/tests/e2e/684_a_runner_supplied_twice_is_refused.sh index 2bd651ab6..46585cdac 100755 --- a/tests/e2e/684_a_runner_supplied_twice_is_refused.sh +++ b/tests/e2e/684_a_runner_supplied_twice_is_refused.sh @@ -80,7 +80,7 @@ case "$arch" in arm64) arch=aarch64 ;; esac case "$(uname -s)" in Linux) row="$arch-linux-gnu" ;; Darwin) row="$arch-macos" ;; - *) echo "SKIP: no canonical host row for $(uname -s)"; exit 0 ;; + *) echo "SKIP: no canonical host row for $(uname -s)"; exit 77 ;; esac printf '\n[target.%s.runners]\napp = ["%s/run-C.sh"]\n' "$row" "$TMP" >> mcpp.toml "$MCPP" build > manifest.log 2>&1 || fail "2: the manifest's runner did not settle it" manifest.log diff --git a/tests/e2e/713_a_git_dependency_selects_a_repository_member.sh b/tests/e2e/713_a_git_dependency_selects_a_repository_member.sh index 940266b1c..9b4ebc440 100755 --- a/tests/e2e/713_a_git_dependency_selects_a_repository_member.sh +++ b/tests/e2e/713_a_git_dependency_selects_a_repository_member.sh @@ -33,7 +33,7 @@ fi fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -command -v git >/dev/null || { echo "SKIP: git is not installed"; exit 0; } +command -v git >/dev/null || { echo "SKIP: git is not installed"; exit 77; } mkdir -p repo/src repo/tool/src cat > repo/mcpp.toml <<'EOF' diff --git a/tests/e2e/730_bare_compat_dependency_is_not_a_refresh_miss.sh b/tests/e2e/730_bare_compat_dependency_is_not_a_refresh_miss.sh index 61cb92382..26781f35d 100755 --- a/tests/e2e/730_bare_compat_dependency_is_not_a_refresh_miss.sh +++ b/tests/e2e/730_bare_compat_dependency_is_not_a_refresh_miss.sh @@ -25,7 +25,7 @@ for cand in cjson argparse gtest ftxui; do done if [ -z "$DESC" ]; then echo "SKIP: no compat descriptor in the local mcpplibs index" - exit 0 + exit 77 fi VER=$(grep -o '\["[0-9][0-9.]*"\]' "$INDEX_ROOT/compat.$DESC.lua" | head -1 | tr -d '[]"') [ -n "$VER" ] || fail "could not read a version of compat.$DESC" diff --git a/tests/e2e/738_a_graph_supplied_target_closes_the_hosts_own_search.sh b/tests/e2e/738_a_graph_supplied_target_closes_the_hosts_own_search.sh index a536ca388..03c6dbdbb 100755 --- a/tests/e2e/738_a_graph_supplied_target_closes_the_hosts_own_search.sh +++ b/tests/e2e/738_a_graph_supplied_target_closes_the_hosts_own_search.sh @@ -111,7 +111,7 @@ if ! out="$("$MCPP" build --target x86_64-windows-gnu --cache off 2>&1)"; then case "$out" in *"not found in the synced index"*|*"install_packages failed"*) echo "SKIP: the openkal packages are not reachable from here" - exit 0 ;; + exit 77 ;; esac echo "FAIL: the openkal windows-gnu stack did not build" printf '%s\n' "$out" | grep -iE 'error' | head -5 diff --git a/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh b/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh index ff226d854..5960ea5b5 100755 --- a/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh +++ b/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh @@ -51,7 +51,7 @@ find_llvm() { LLVM_VERSION="$(find_llvm || true)" if [ -z "$LLVM_VERSION" ]; then echo "SKIP: no xim-x-llvm payload on this host to cross-resolve aarch64-macos with" - exit 0 + exit 77 fi echo "using llvm@$LLVM_VERSION" diff --git a/tests/e2e/760_msvc_toolset_is_chosen_once.sh b/tests/e2e/760_msvc_toolset_is_chosen_once.sh index 695d1332d..93c765c0e 100755 --- a/tests/e2e/760_msvc_toolset_is_chosen_once.sh +++ b/tests/e2e/760_msvc_toolset_is_chosen_once.sh @@ -25,7 +25,7 @@ trap "rm -rf $TMP" EXIT cd "$TMP" VSWHERE="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" -[[ -x "$VSWHERE" ]] || { echo "SKIP: no vswhere on this runner"; exit 0; } +[[ -x "$VSWHERE" ]] || { echo "SKIP: no vswhere on this runner"; exit 77; } VSROOT="$("$VSWHERE" -latest -prerelease -products '*' \ -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 \ -property installationPath | tr -d '\r')" diff --git a/tests/e2e/778_a_graph_link_searches_no_host_directory.sh b/tests/e2e/778_a_graph_link_searches_no_host_directory.sh index ccf788026..15e31cd95 100644 --- a/tests/e2e/778_a_graph_link_searches_no_host_directory.sh +++ b/tests/e2e/778_a_graph_link_searches_no_host_directory.sh @@ -73,7 +73,7 @@ C skip_if_unreachable() { if grep -qE 'not found in the synced index|install_packages failed' "$1"; then echo "SKIP: the openkal packages are not reachable from here" - exit 0 + exit 77 fi } diff --git a/tests/e2e/86_llvm_hermetic_link.sh b/tests/e2e/86_llvm_hermetic_link.sh index 6db541797..73837ad15 100755 --- a/tests/e2e/86_llvm_hermetic_link.sh +++ b/tests/e2e/86_llvm_hermetic_link.sh @@ -16,17 +16,17 @@ set -e OS="$(uname -s)" if [[ "$OS" != Linux ]]; then echo "SKIP: hermetic CRT/loader model is Linux-only" - exit 0 + exit 77 fi source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} is not installed" - exit 0 + exit 77 fi if [[ ! -f "$LLVM_ROOT/share/libc++/v1/std.cppm" ]]; then echo "SKIP: xlings llvm@${LLVM_VERSION} has no libc++ std.cppm" - exit 0 + exit 77 fi TMP=$(mktemp -d) diff --git a/tests/e2e/875_toolchain_by_path.sh b/tests/e2e/875_toolchain_by_path.sh index 36a68fd6e..ef0e6774c 100755 --- a/tests/e2e/875_toolchain_by_path.sh +++ b/tests/e2e/875_toolchain_by_path.sh @@ -13,7 +13,7 @@ set -e MCPP="${MCPP:-mcpp}" source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then - echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 0 + echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 77 fi work="$(mktemp -d)" # Paths written INTO a manifest go through host_path (see _host_path.sh). diff --git a/tests/e2e/876_toolchain_phase.sh b/tests/e2e/876_toolchain_phase.sh index 30ddf4b5a..8256e7412 100755 --- a/tests/e2e/876_toolchain_phase.sh +++ b/tests/e2e/876_toolchain_phase.sh @@ -9,7 +9,7 @@ set -e MCPP="${MCPP:-mcpp}" source "$(dirname "$0")/_llvm_env.sh" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then - echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 0 + echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 77 fi work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT diff --git a/tests/e2e/890_a_toolchain_declared_for_an_unservable_target_is_still_installed.sh b/tests/e2e/890_a_toolchain_declared_for_an_unservable_target_is_still_installed.sh index 4d2720cff..0a21a5369 100755 --- a/tests/e2e/890_a_toolchain_declared_for_an_unservable_target_is_still_installed.sh +++ b/tests/e2e/890_a_toolchain_declared_for_an_unservable_target_is_still_installed.sh @@ -41,7 +41,7 @@ export MCPP_HOME="$TMP/mcpp-home" # its CI home is the macOS leg of the target matrix. case "$(uname -s)" in Darwin|MINGW*|MSYS*|CYGWIN*) ;; - *) echo "SKIP: hosted-guest refusal semantics only bite on a non-linux host"; exit 0 ;; + *) echo "SKIP: hosted-guest refusal semantics only bite on a non-linux host"; exit 77 ;; esac mkdir -p app/src diff --git a/tests/e2e/891_a_host_header_is_written_not_searched.sh b/tests/e2e/891_a_host_header_is_written_not_searched.sh index b5c7edb86..af93d44f4 100755 --- a/tests/e2e/891_a_host_header_is_written_not_searched.sh +++ b/tests/e2e/891_a_host_header_is_written_not_searched.sh @@ -28,7 +28,7 @@ for h in zlib.h X11/Xlib.h curl/curl.h openssl/ssl.h; do done if [ -z "$header" ]; then echo "SKIP: 891 needs a header that only the host's /usr/include has" - exit 0 + exit 77 fi mkdir -p "$TMP/app/src" diff --git a/tests/e2e/918_an_older_packaging_is_used_not_replaced.sh b/tests/e2e/918_an_older_packaging_is_used_not_replaced.sh index 03334a573..d4eb0db01 100755 --- a/tests/e2e/918_an_older_packaging_is_used_not_replaced.sh +++ b/tests/e2e/918_an_older_packaging_is_used_not_replaced.sh @@ -32,11 +32,11 @@ cd probe # The payload the build resolved: `Resolved → /bin/`. COMPILER=$(sed -n 's/.*Resolved [^ ]* → \(.*\)$/\1/p' first.log | tail -1) -[ -n "$COMPILER" ] || { echo "SKIP: the build resolved no managed toolchain payload"; exit 0; } +[ -n "$COMPILER" ] || { echo "SKIP: the build resolved no managed toolchain payload"; exit 77; } case "$COMPILER" in @mcpp/*) COMPILER="$MCPP_HOME/${COMPILER#@mcpp/}" ;; esac PAYLOAD=$(cd "$(dirname "$COMPILER")/.." && pwd) STAMP="$PAYLOAD/.xpkg-install.json" -[ -f "$STAMP" ] || { echo "SKIP: $PAYLOAD carries no install record"; exit 0; } +[ -f "$STAMP" ] || { echo "SKIP: $PAYLOAD carries no install record"; exit 77; } # The payload in this home becomes a directory of its own whose children link # to the inherited one, with a record of its own: the inherited payload (the diff --git a/tests/e2e/925_a_stated_sysroot_is_the_c_library.sh b/tests/e2e/925_a_stated_sysroot_is_the_c_library.sh index 7c1d4fedd..c916b9725 100755 --- a/tests/e2e/925_a_stated_sysroot_is_the_c_library.sh +++ b/tests/e2e/925_a_stated_sysroot_is_the_c_library.sh @@ -40,14 +40,14 @@ $2 $1 TOML } -[[ "$(uname -m)" == x86_64 ]] || { echo "SKIP: the fixture sysroot is x86_64"; exit 0; } +[[ "$(uname -m)" == x86_64 ]] || { echo "SKIP: the fixture sysroot is x86_64"; exit 77; } # The default build first: it installs the payloads the fixture is made of, # and its output directory is the one a stated base must not reuse. manifest "" "" out="$("$MCPP" build 2>&1)" || fail "the default build failed" default_dir=$(ls -d target/x86_64-linux-gnu/*/ | head -1) -make_glibc_sysroot "$work/app/third_party/sysroot" || { echo "SKIP: no glibc payload to build a sysroot from"; exit 0; } +make_glibc_sysroot "$work/app/third_party/sysroot" || { echo "SKIP: no glibc payload to build a sysroot from"; exit 77; } SR="$work/app/third_party/sysroot" artifact() { ls target/x86_64-linux-gnu/*/bin/app | xargs ls -t | head -1; } diff --git a/tests/e2e/926_the_hosts_own_root_is_a_base.sh b/tests/e2e/926_the_hosts_own_root_is_a_base.sh index bf130f0d9..789cc79a7 100755 --- a/tests/e2e/926_the_hosts_own_root_is_a_base.sh +++ b/tests/e2e/926_the_hosts_own_root_is_a_base.sh @@ -20,7 +20,7 @@ fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } tuple="$(uname -m)-linux-gnu" if [[ ! -e "/usr/lib/$tuple/crt1.o" ]]; then - echo "SKIP: this host is not a Debian multiarch layout (/usr/lib/$tuple/crt1.o)"; exit 0 + echo "SKIP: this host is not a Debian multiarch layout (/usr/lib/$tuple/crt1.o)"; exit 77 fi host_glibc=$(getconf GNU_LIBC_VERSION | awk '{print $2}') diff --git a/tests/e2e/928_an_apple_sdk_is_located_on_any_host.sh b/tests/e2e/928_an_apple_sdk_is_located_on_any_host.sh index 5c2656b6e..f8dec40b3 100755 --- a/tests/e2e/928_an_apple_sdk_is_located_on_any_host.sh +++ b/tests/e2e/928_an_apple_sdk_is_located_on_any_host.sh @@ -11,7 +11,7 @@ set -e MCPP="${MCPP:-mcpp}" -[[ "$(uname -s)" == Linux ]] || { echo "SKIP: the non-Apple arm"; exit 0; } +[[ "$(uname -s)" == Linux ]] || { echo "SKIP: the non-Apple arm"; exit 77; } work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT export NO_COLOR=1 MCPP_NO_AUTO_INSTALL=1 diff --git a/tests/e2e/930_a_toolchain_phase_states_the_base.sh b/tests/e2e/930_a_toolchain_phase_states_the_base.sh index 8ecbb806d..6d42f7509 100755 --- a/tests/e2e/930_a_toolchain_phase_states_the_base.sh +++ b/tests/e2e/930_a_toolchain_phase_states_the_base.sh @@ -10,14 +10,14 @@ set -e MCPP="${MCPP:-mcpp}" source "$(dirname "$0")/_llvm_env.sh" source "$(dirname "$0")/_sysroot_fixture.sh" -[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 0; } -[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || { echo "SKIP: the fixture sysroot is x86_64 Linux"; exit 0; } +[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload installed ($LLVM_ROOT)"; exit 77; } +[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || { echo "SKIP: the fixture sysroot is x86_64 Linux"; exit 77; } work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT export NO_COLOR=1 fail() { echo "FAIL: $*"; echo "----"; echo "${out:-}"; exit 1; } -make_glibc_sysroot "$work/sr" || { echo "SKIP: no glibc payload to build a sysroot from"; exit 0; } +make_glibc_sysroot "$work/sr" || { echo "SKIP: no glibc payload to build a sysroot from"; exit 77; } mkdir -p "$work/app/src" cd "$work/app" cat > mcpp.toml </dev/null; done; exit 1; } source "$(dirname "$0")/_toolchain_env.sh" -[[ "$(uname -s)" == Linux ]] || { echo "SKIP: 933 is about ELF"; exit 0; } -[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload"; exit 0; } +[[ "$(uname -s)" == Linux ]] || { echo "SKIP: 933 is about ELF"; exit 77; } +[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload"; exit 77; } GLIB="$MCPP_E2E_GCC_ROOT/lib64" -[[ -e "$GLIB/libstdc++.so.6" && -x "$MCPP_E2E_GCC_ROOT/bin/g++" ]] || { echo "SKIP: no gcc payload"; exit 0; } +[[ -e "$GLIB/libstdc++.so.6" && -x "$MCPP_E2E_GCC_ROOT/bin/g++" ]] || { echo "SKIP: no gcc payload"; exit 77; } project() { # $1 dir, $2 target kind, $3 extra manifest text mkdir -p "$1/src" "$1/tests" diff --git a/tests/e2e/934_llvm_with_a_graph_supplied_libstdcxx.sh b/tests/e2e/934_llvm_with_a_graph_supplied_libstdcxx.sh index e6d17e277..e61267a96 100644 --- a/tests/e2e/934_llvm_with_a_graph_supplied_libstdcxx.sh +++ b/tests/e2e/934_llvm_with_a_graph_supplied_libstdcxx.sh @@ -16,11 +16,11 @@ TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } source "$(dirname "$0")/_toolchain_env.sh" -[[ "$(uname -s)" == Linux ]] || { echo "SKIP: 934 uses the Linux gcc payload"; exit 0; } -[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload"; exit 0; } +[[ "$(uname -s)" == Linux ]] || { echo "SKIP: 934 uses the Linux gcc payload"; exit 77; } +[[ -x "$LLVM_ROOT/bin/clang++" ]] || { echo "SKIP: no llvm payload"; exit 77; } G="$MCPP_E2E_GCC_ROOT" [[ -e "$G/include/c++/$GCC_VERSION/bits/std.cc" && -e "$G/lib64/libstdc++.so.6" ]] \ - || { echo "SKIP: no gcc payload with bits/std.cc"; exit 0; } + || { echo "SKIP: no gcc payload with bits/std.cc"; exit 77; } # The provider, as `gcc.libstdcxx`'s install() lays it out. P="$TMP/libstdcxx" diff --git a/tests/e2e/938_a_relink_never_exposes_a_partial_file.sh b/tests/e2e/938_a_relink_never_exposes_a_partial_file.sh index 5492ac68d..2c3a93ec9 100644 --- a/tests/e2e/938_a_relink_never_exposes_a_partial_file.sh +++ b/tests/e2e/938_a_relink_never_exposes_a_partial_file.sh @@ -10,7 +10,7 @@ # A the link rule writes a temporary name and renames it (POSIX); # B starting the program in a loop while it is relinked never fails to exec. set -e -case "$(uname -s)" in Linux|Darwin) ;; *) echo "SKIP: POSIX publication"; exit 0 ;; esac +case "$(uname -s)" in Linux|Darwin) ;; *) echo "SKIP: POSIX publication"; exit 77 ;; esac TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } diff --git a/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh b/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh index e82890ab6..b22a87697 100755 --- a/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh +++ b/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh @@ -40,7 +40,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac # No C library, no C++ run-time, no compiler run-time: the image is its own. diff --git a/tests/e2e/951_an_ok_image_needs_no_declaration.sh b/tests/e2e/951_an_ok_image_needs_no_declaration.sh index 084678f94..a20f79895 100755 --- a/tests/e2e/951_an_ok_image_needs_no_declaration.sh +++ b/tests/e2e/951_an_ok_image_needs_no_declaration.sh @@ -40,7 +40,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac T="$HOST_ARCH-openkal" diff --git a/tests/e2e/953_the_host_graph_runs_build_programs_as_images.sh b/tests/e2e/953_the_host_graph_runs_build_programs_as_images.sh index ee59d3934..09dfe9511 100755 --- a/tests/e2e/953_the_host_graph_runs_build_programs_as_images.sh +++ b/tests/e2e/953_the_host_graph_runs_build_programs_as_images.sh @@ -23,7 +23,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64|aarch64|arm64) ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac mkdir -p "$TMP/app/src" "$TMP/app/helper/src" diff --git a/tests/e2e/955_the_openkal_set_is_one_tree_of_every_architecture.sh b/tests/e2e/955_the_openkal_set_is_one_tree_of_every_architecture.sh index 91fd5cd2a..6d0f5c3b7 100755 --- a/tests/e2e/955_the_openkal_set_is_one_tree_of_every_architecture.sh +++ b/tests/e2e/955_the_openkal_set_is_one_tree_of_every_architecture.sh @@ -29,9 +29,9 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac -case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX launcher"; exit 0 ;; esac +case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX launcher"; exit 77 ;; esac T="$HOST_ARCH-openkal" mkdir -p "$TMP/app/src" diff --git a/tests/e2e/957_packages_merge_and_describe_themselves.sh b/tests/e2e/957_packages_merge_and_describe_themselves.sh index bb9a78d33..ee361f12e 100755 --- a/tests/e2e/957_packages_merge_and_describe_themselves.sh +++ b/tests/e2e/957_packages_merge_and_describe_themselves.sh @@ -31,7 +31,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) T=x86_64-openkal ;; aarch64|arm64) T=aarch64-openkal ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac mkdir -p "$TMP/app/src" diff --git a/tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh b/tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh index 664b0f781..e17c49fc9 100755 --- a/tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh +++ b/tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh @@ -39,7 +39,7 @@ TMP_HOST="$(host_path "$TMP")" case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac HT="$HOST_ARCH-openkal" OT="$OTHER-openkal" diff --git a/tests/e2e/961_a_single_file_carries_the_image_in_a_program_of_its_architecture.sh b/tests/e2e/961_a_single_file_carries_the_image_in_a_program_of_its_architecture.sh index 261905414..f7678f69a 100755 --- a/tests/e2e/961_a_single_file_carries_the_image_in_a_program_of_its_architecture.sh +++ b/tests/e2e/961_a_single_file_carries_the_image_in_a_program_of_its_architecture.sh @@ -35,7 +35,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) OTHER=aarch64 ;; aarch64|arm64) OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac case "$(uname -s)" in Darwin) SYSTEM=macos; XSYSTEM=macosx ;; diff --git a/tests/e2e/963_a_target_set_builds_every_member_and_runs_where_it_is_reached.sh b/tests/e2e/963_a_target_set_builds_every_member_and_runs_where_it_is_reached.sh index f0ab22719..83d9a3c0e 100755 --- a/tests/e2e/963_a_target_set_builds_every_member_and_runs_where_it_is_reached.sh +++ b/tests/e2e/963_a_target_set_builds_every_member_and_runs_where_it_is_reached.sh @@ -33,7 +33,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac HT="$HOST_ARCH-openkal" OT="$OTHER-openkal" diff --git a/tests/e2e/964_tests_built_once_run_from_an_archive.sh b/tests/e2e/964_tests_built_once_run_from_an_archive.sh index f61b31679..2adea746b 100755 --- a/tests/e2e/964_tests_built_once_run_from_an_archive.sh +++ b/tests/e2e/964_tests_built_once_run_from_an_archive.sh @@ -33,7 +33,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac OT="$OTHER-openkal" diff --git a/tests/e2e/981_a_web_link_has_the_profiles_level_and_the_wasm_exception_model.sh b/tests/e2e/981_a_web_link_has_the_profiles_level_and_the_wasm_exception_model.sh index 3ab66267d..72b96e31b 100755 --- a/tests/e2e/981_a_web_link_has_the_profiles_level_and_the_wasm_exception_model.sh +++ b/tests/e2e/981_a_web_link_has_the_profiles_level_and_the_wasm_exception_model.sh @@ -18,8 +18,7 @@ for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripte done if [ "$have_emsdk" -ne 1 ]; then echo "SKIP: 981 -- no emsdk payload installed" - echo "PASS: 981 (skipped)" - exit 0 + exit 77 fi mkdir -p "$TMP/app/src" diff --git a/tests/e2e/983_a_pack_holds_the_legs_it_names.sh b/tests/e2e/983_a_pack_holds_the_legs_it_names.sh index 9e16636b7..30c85014b 100644 --- a/tests/e2e/983_a_pack_holds_the_legs_it_names.sh +++ b/tests/e2e/983_a_pack_holds_the_legs_it_names.sh @@ -34,9 +34,9 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64; OTHER=aarch64 ;; aarch64|arm64) HOST_ARCH=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac -case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX launcher"; exit 0 ;; esac +case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX launcher"; exit 77 ;; esac HT="$HOST_ARCH-openkal" mkdir -p "$TMP/app/src" diff --git a/tests/e2e/984_a_set_of_programs_is_a_package_per_leg.sh b/tests/e2e/984_a_set_of_programs_is_a_package_per_leg.sh index 6cad66582..95d6216c7 100644 --- a/tests/e2e/984_a_set_of_programs_is_a_package_per_leg.sh +++ b/tests/e2e/984_a_set_of_programs_is_a_package_per_leg.sh @@ -21,7 +21,7 @@ TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -[ "$(uname -s)" = Linux ] || { echo "SKIP: the linux-musl rows are built on Linux"; exit 0; } +[ "$(uname -s)" = Linux ] || { echo "SKIP: the linux-musl rows are built on Linux"; exit 77; } mkdir -p "$TMP/app/src" cd "$TMP/app" @@ -31,7 +31,7 @@ printf 'import std;\nint main() { std::println("per leg"); }\n' > src/main.cpp # A if ! "$MCPP" pack --target linux-musl -o out/ > a.log 2>&1; then if grep -qE "cannot be built on this host|install_packages failed|not found in the synced index" a.log; then - echo "SKIP: a linux-musl row cannot be built here"; exit 0 + echo "SKIP: a linux-musl row cannot be built here"; exit 77 fi fail "A: pack --target linux-musl" a.log fi diff --git a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh index 0571bfee9..9423beb80 100644 --- a/tests/e2e/986_single_files_cover_the_legs_and_systems.sh +++ b/tests/e2e/986_single_files_cover_the_legs_and_systems.sh @@ -27,9 +27,9 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac -case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX host"; exit 0 ;; esac +case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX host"; exit 77 ;; esac mkdir -p "$TMP/app/src" cd "$TMP/app" @@ -54,9 +54,13 @@ echo "ok: --systems - makes that pair's file" # C if [ "$OS" = linux ]; then "$MCPP" pack --target openkal --pkg-format single-file --systems all -o all/ > c.log 2>&1 || fail "C: --systems all" c.log - for pair in linux-x86_64 linux-aarch64 windows-x86_64 macos-aarch64; do + # `all` is the system table (SPEC-016 §2.3): FreeBSD is a system of it, and + # the platform's runner states the identity its FreeBSD program is built as + # (`runner.built-as`, openkal-platform 0.27.0), so its files are made too. + for pair in linux-x86_64 linux-aarch64 windows-x86_64 macos-aarch64 freebsd-x86_64 freebsd-aarch64; do ls all/sf-0.1.0-$pair* > /dev/null 2>&1 || fail "C: no file for $pair" c.log done + # A pair no program is built for is written, not left out. grep -q "Not covered macos-x86_64 (no carrier row" c.log || fail "C: the pair without a row is not said" c.log echo "ok: --systems all makes every file this mcpp can and says the rest" fi diff --git a/tests/e2e/987_the_universal_spellings_are_migrated.sh b/tests/e2e/987_the_universal_spellings_are_migrated.sh index 355f5f54c..178c68299 100644 --- a/tests/e2e/987_the_universal_spellings_are_migrated.sh +++ b/tests/e2e/987_the_universal_spellings_are_migrated.sh @@ -27,7 +27,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac HT="$HOST_ARCH-openkal" diff --git a/tests/e2e/988_the_windows_launcher_finds_its_package_from_a_windows_path.sh b/tests/e2e/988_the_windows_launcher_finds_its_package_from_a_windows_path.sh index e6512cccc..4ac23096d 100755 --- a/tests/e2e/988_the_windows_launcher_finds_its_package_from_a_windows_path.sh +++ b/tests/e2e/988_the_windows_launcher_finds_its_package_from_a_windows_path.sh @@ -23,7 +23,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -s)-$(uname -m)" in Linux-x86_64|Linux-amd64) ;; - *) echo "SKIP: the Windows runner is x86_64 and wine runs it on Linux x86_64"; exit 0 ;; + *) echo "SKIP: the Windows runner is x86_64 and wine runs it on Linux x86_64"; exit 77 ;; esac mkdir -p "$TMP/app/src" diff --git a/tests/e2e/98_reflection_import_std.sh b/tests/e2e/98_reflection_import_std.sh index 4d9430b8a..82431d54d 100755 --- a/tests/e2e/98_reflection_import_std.sh +++ b/tests/e2e/98_reflection_import_std.sh @@ -16,7 +16,7 @@ trap "rm -rf $TMP" EXIT GXX=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-gcc" -name "g++" -path "*/bin/*" 2>/dev/null | sort | tail -1) if [[ -z "$GXX" ]] || ! echo 'int main(){}' | "$GXX" -freflection -std=c++26 -x c++ - -fsyntax-only -o /dev/null 2>/dev/null; then echo "SKIP-INLINE: no -freflection-capable gcc payload available" - exit 0 + exit 77 fi mkdir -p "$TMP/proj/src" "$TMP/proj/meta_dep/src" diff --git a/tests/e2e/992_a_polyglot_file_runs_on_every_system_it_holds.sh b/tests/e2e/992_a_polyglot_file_runs_on_every_system_it_holds.sh index 6c108c3eb..8a5fec583 100755 --- a/tests/e2e/992_a_polyglot_file_runs_on_every_system_it_holds.sh +++ b/tests/e2e/992_a_polyglot_file_runs_on_every_system_it_holds.sh @@ -34,9 +34,9 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac -case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX host"; exit 0 ;; esac +case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX host"; exit 77 ;; esac mkdir -p "$TMP/app/src" cd "$TMP/app" diff --git a/tests/e2e/994_the_contract_names_the_target_side.sh b/tests/e2e/994_the_contract_names_the_target_side.sh index d5d0313ad..ead3423c9 100755 --- a/tests/e2e/994_the_contract_names_the_target_side.sh +++ b/tests/e2e/994_the_contract_names_the_target_side.sh @@ -22,7 +22,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) ARCH=x86_64 ;; aarch64|arm64) ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac cd "$TMP" diff --git a/tests/e2e/995_the_windows_launcher_builds_over_any_platform_release.sh b/tests/e2e/995_the_windows_launcher_builds_over_any_platform_release.sh index d9525df95..897db1bba 100755 --- a/tests/e2e/995_the_windows_launcher_builds_over_any_platform_release.sh +++ b/tests/e2e/995_the_windows_launcher_builds_over_any_platform_release.sh @@ -20,7 +20,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -s)-$(uname -m)" in Linux-x86_64|Linux-amd64) ;; - *) echo "SKIP: the Windows launcher is built here on Linux x86_64"; exit 0 ;; + *) echo "SKIP: the Windows launcher is built here on Linux x86_64"; exit 77 ;; esac mkdir -p "$TMP/app/src" diff --git a/tests/e2e/996_an_ok_image_waits_for_a_prepare_action.sh b/tests/e2e/996_an_ok_image_waits_for_a_prepare_action.sh index 190741ca9..a32efce89 100755 --- a/tests/e2e/996_an_ok_image_waits_for_a_prepare_action.sh +++ b/tests/e2e/996_an_ok_image_waits_for_a_prepare_action.sh @@ -22,7 +22,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 case "$(uname -m)" in x86_64|amd64) HOST_ARCH=x86_64 ;; aarch64|arm64) HOST_ARCH=aarch64 ;; - *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 77 ;; esac T="$HOST_ARCH-openkal" diff --git a/tests/e2e/999_debug_information_is_stored_once.sh b/tests/e2e/999_debug_information_is_stored_once.sh index 46645df3f..e548bbb3b 100755 --- a/tests/e2e/999_debug_information_is_stored_once.sh +++ b/tests/e2e/999_debug_information_is_stored_once.sh @@ -19,7 +19,7 @@ TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT cd "$TMP" fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -[ "$(uname -s)" = Linux ] || { echo "SKIP: the split form is checked on Linux (ELF)"; exit 0; } +[ "$(uname -s)" = Linux ] || { echo "SKIP: the split form is checked on Linux (ELF)"; exit 77; } mkdir -p app/src cd app diff --git a/tests/e2e/coverage-debt.tsv b/tests/e2e/coverage-debt.tsv new file mode 100644 index 000000000..0a0a4289c --- /dev/null +++ b/tests/e2e/coverage-debt.tsv @@ -0,0 +1,5 @@ +# Tests that skip themselves on every host of CI that starts them, read by +# .github/tools/check_e2e_coverage.py. One `\t` line each. +# A debt, not an excuse: the list only shrinks. An entry whose test ran fails the +# check until the entry is removed; a test that begins to skip itself everywhere is +# uncovered until a host is given what it needs, or it is listed here with the reason. diff --git a/tests/e2e/mtest.toml b/tests/e2e/mtest.toml index 1177d9e99..baed72940 100644 --- a/tests/e2e/mtest.toml +++ b/tests/e2e/mtest.toml @@ -14,6 +14,9 @@ kind = "e2e" # A script is level 0, whatever its text says: several name the protocol's marker in their comments, # and a script told the protocol's variables would pass them to what it runs. level = 0 +# A script that cannot hold anything on this machine leaves with 77 after a `SKIP: ` line: +# its case is skipped, with that reason, and is not counted among the passes (mtest 0.6.1). +skip_exit = 77 timeout = "10m" tags_from = { "# requires:" = "requires:" } resources_from = ["# resources:"] diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index f7294893b..dc6e380e1 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -59,6 +59,11 @@ echo "MCPP_HOME: $MCPP_HOME" CAPS=() OS="$(uname -s)" +# x86_64-host: the machine's architecture, for a script that holds a provider +# whose `hosts` name one (the cross entry of aarch64-linux-gnu is published for +# linux-x86_64). A script that cannot hold anything elsewhere is excluded by +# this token, which the run reports; it does not pass by printing a line. +case "$(uname -m)" in x86_64|amd64) CAPS+=(x86_64-host) ;; esac case "$OS" in Linux) @@ -274,7 +279,7 @@ echo "Detected capabilities: ${CAPS[*]:-}" KNOWN_CAPS=(android-device android-ndk elf fresh-sandbox gcc import-std-libcxx jq llvm macos mingw mingw-cross mingw-host-headers msvc musl nasm no-msvc pack patchelf python3 qemu-arm qemu-riscv scan-deps symlink unix-shell windows wine - xlings-msvc) + x86_64-host xlings-msvc) bad_tokens=0 for tf in "$HERE"/[0-9]*.sh; do @@ -537,14 +542,22 @@ try: except (OSError, ValueError): sys.exit(0) for c in run.get("cases", []): - status = {"pass": "pass", "flaky": "pass", "timeout": "timeout"}.get(c.get("status"), "fail") - print(status, int(c.get("ms", 0)), c.get("program", c.get("id", ""))) + status = {"pass": "pass", "flaky": "pass", "timeout": "timeout", "skip": "skip"}.get(c.get("status"), "fail") + # The fourth word on: why a skipped script did not apply here (its `SKIP:` line). + print(status, int(c.get("ms", 0)), c.get("program", c.get("id", "")), c.get("reason", "")) PY )" rm -f "$record" - while read -r status ms name; do + while read -r status ms name why; do name="${name%$'\r'}" # Windows' Python ends a printed line with CR LF + why="${why%$'\r'}" [[ -n "$name" ]] || continue + # A script that skipped itself ran nothing: it is reported as a skip with + # its reason, which the coverage check reads as "did not run here". + if [[ "$status" == skip ]]; then + ((SKIP++)); report skip "$name" "$ms" "skipped itself: ${why:-no reason given}" + continue + fi TIMINGS+=("$ms $name") case "$status" in pass) ((PASS++)); report pass "$name" "$ms" "" ;; @@ -623,6 +636,12 @@ for test in "$HERE"/[0-9]*.sh; do echo "PASS: $name (${_dur})" ((PASS++)) report pass "$name" "$_dur_ms" "" + elif [[ $rc -eq 77 ]]; then + # The script's own word for "nothing to hold on this machine" (the + # `skip_exit` of tests/e2e/mtest.toml): a skip, not a pass. + echo "SKIP: $name (skipped itself)" + ((SKIP++)) + report skip "$name" "$_dur_ms" "skipped itself" elif [[ $rc -eq 124 ]]; then report timeout "$name" "$_dur_ms" "exceeded ${E2E_TEST_TIMEOUT}s" # GNU timeout: 124 = killed after deadline (TERM); 137 = SIGKILL after grace. diff --git a/tests/fixtures/target-rows.snapshot b/tests/fixtures/target-rows.snapshot index 712dc7e6c..1ae622d7a 100644 --- a/tests/fixtures/target-rows.snapshot +++ b/tests/fixtures/target-rows.snapshot @@ -113,7 +113,7 @@ row aarch64-windows-gnu descriptor-keys windows sysroot xim:mingw-w64-sysroot@20261104-2 row x86_64-windows-musl - table tier=preview note=PE pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=preview note=PE pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str x86_64-windows-musl llvm x86_64-w64-windows-gnu llvm@15.0 x86_64-w64-windows-gnu @@ -170,7 +170,7 @@ row aarch64-macos descriptor-keys macosx sysroot row aarch64-macos-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str aarch64-macos-musl llvm arm64-apple-macos14.0 llvm@15.0 arm64-apple-macos15.0 @@ -208,7 +208,7 @@ row riscv64-linux-musl descriptor-keys linux sysroot row i686-linux-musl - table tier=planned note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=planned note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str i686-linux-musl llvm i686-unknown-linux-musl llvm@15.0 i686-unknown-linux-musl @@ -246,7 +246,7 @@ row i686-linux-gnu descriptor-keys linux sysroot row i686-openkal - table tier=planned note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=planned note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str i686-openkal llvm i686-unknown-none-elf llvm@15.0 i686-unknown-none-elf @@ -284,7 +284,7 @@ row x86_64-macos descriptor-keys macosx sysroot row x86_64-macos-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str x86_64-macos-musl llvm x86_64-apple-macos14.0 llvm@15.0 x86_64-apple-macos15.0 @@ -341,7 +341,7 @@ row aarch64-freebsd descriptor-keys freebsd sysroot row x86_64-freebsd-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str x86_64-freebsd-musl llvm x86_64-unknown-freebsd14.0 llvm@15.0 x86_64-unknown-freebsd14.0 @@ -360,7 +360,7 @@ row x86_64-freebsd-musl descriptor-keys freebsd sysroot row aarch64-freebsd-musl - table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str aarch64-freebsd-musl llvm aarch64-unknown-freebsd14.0 llvm@15.0 aarch64-unknown-freebsd14.0 @@ -817,7 +817,7 @@ row wasm32-emscripten descriptor-keys emscripten linux sysroot row x86_64-openkal - table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str x86_64-openkal llvm x86_64-unknown-none-elf llvm@15.0 x86_64-unknown-none-elf @@ -836,7 +836,7 @@ row x86_64-openkal descriptor-keys openkal linux sysroot row aarch64-openkal - table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.26.0 + table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.27.0 str aarch64-openkal llvm aarch64-unknown-none-elf llvm@15.0 aarch64-unknown-none-elf @@ -861,9 +861,9 @@ request x86_64-linux on windows-x86_64 -> x86_64-linux-musl musl-gcc fallback=1 | x86_64-linux-gnu/glibc/100/x x86_64-linux-musl/musl-gcc/100 x86_64-linux-musl/openkal/50 request aarch64-linux parsed aarch64-linux-gnu envExplicit=0 - on linux-x86_64 -> aarch64-linux-musl musl-gcc fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100 aarch64-linux-musl/openkal/50 - on macos-aarch64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 - on windows-x86_64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 + on linux-x86_64 -> aarch64-linux-musl musl-gcc fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100 aarch64-linux-musl/openkal/50 aarch64-linux-gnu/glibc-sysroot/40 + on macos-aarch64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 aarch64-linux-gnu/glibc-sysroot/40/x + on windows-x86_64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 aarch64-linux-gnu/glibc-sysroot/40/x request riscv64-linux parsed riscv64-linux-gnu envExplicit=0 on linux-x86_64 -> riscv64-linux-musl musl-gcc fallback=0 | riscv64-linux-musl/musl-gcc/100/x riscv64-linux-musl/openkal/50/x @@ -971,7 +971,7 @@ set linux-musl -> x86_64-linux-musl aarch64-linux-musl | set windows -> x86_64-windows-gnu aarch64-windows-gnu | set windows-msvc -> x86_64-windows-msvc | set macos -> aarch64-macos | -set openkal -> x86_64-openkal aarch64-openkal | mcpplibs.openkal-platform@0.26.0 +set openkal -> x86_64-openkal aarch64-openkal | mcpplibs.openkal-platform@0.27.0 set none-elf -> riscv64-none-elf riscv32-none-elf aarch64-none-elf x86_64-none-elf i686-none-elf | set none-eabihf -> thumbv7em-none-eabihf thumbv8m.main-none-eabihf armv7a-none-eabihf | set android -> aarch64-linux-android x86_64-linux-android | diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index 1365e5843..e9bb32510 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -85,10 +85,10 @@ # sysroot 的缺陷,修正后建出。 # # 2026.11.10.2 改变的格(设计 2026-11-10 K9): -# - `x86_64-freebsd-musl`、`aarch64-freebsd-musl` 由 openkal-platform 0.26.0 的 hosted 部分服务 +# - `x86_64-freebsd-musl`、`aarch64-freebsd-musl` 由 openkal-platform 0.27.0 的 hosted 部分服务 # (openkal-freebsd standalone 之上的 openkal-musl 0.29.2):payload 体系以 llvm 建出;gcc 与 msvc # 是 `capability-pin`;graph 体系里图中钉的版本在平台版本组之外,`platform-version-group`。 -# - openkal 各行的平台为 openkal-platform 0.26.0,openkal 列为 openkal-llvm-runtime@0.20.1。 +# - openkal 各行的平台为 openkal-platform 0.27.0,openkal 列为 openkal-llvm-runtime@0.20.1。 # # 中间五列是报告器输出,不参与支持判定。现有报告器把 aarch64-ios 的 # c-abi 输出为 glibc(payload),这不代表 iOS 产物实际链接 glibc。Apple @@ -295,7 +295,7 @@ payload linux-x86_64 aarch64-freebsd-musl llvm@23.1.3 aarch64-unknown-freebsd14. payload linux-x86_64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported convention-unreplaced -payload linux-x86_64 aarch64-linux-gnu llvm@23.1.3 - - - - - unsupported host-cannot-serve +payload linux-x86_64 aarch64-linux-gnu llvm@23.1.3 aarch64-unknown-linux-gnu payload glibc(prebuilt) libc++(prebuilt) - ok none 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-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced payload linux-x86_64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin diff --git a/tests/scripts/test_check_e2e_coverage.py b/tests/scripts/test_check_e2e_coverage.py index 54233693a..e1a9c77ba 100644 --- a/tests/scripts/test_check_e2e_coverage.py +++ b/tests/scripts/test_check_e2e_coverage.py @@ -46,7 +46,8 @@ def run_check(self, *extra: str) -> subprocess.CompletedProcess: def test_ran_named_and_excused_tests_are_covered(self) -> None: r = self.run_check() self.assertEqual(r.returncode, 0, r.stdout) - self.assertIn("1 ran on a shard, 0 unaffected by the change, 2 run by a dedicated job, 1 excused, 0 uncovered", r.stdout) + self.assertIn("1 ran on a shard, 0 unaffected by the change, 2 run by a dedicated job, 1 excused, " + "0 owed, 0 uncovered", r.stdout) def test_a_name_in_a_comment_or_inside_a_longer_name_does_not_count(self) -> None: e2e = self.root / "tests" / "e2e" @@ -88,6 +89,27 @@ def test_a_test_a_set_left_out_is_unaffected_and_meets_its_host(self) -> None: self.assertEqual(r.returncode, 1) self.assertIn("REQUIRED ON HOST: 50_untouched.sh did not pass on linux", r.stdout) + def test_a_test_that_skips_itself_everywhere_is_uncovered_until_it_is_owed(self) -> None: + """A script that leaves with its skip status ran nothing: it is not a pass.""" + (self.root / "tests" / "e2e" / "55_skips_itself.sh").write_text("# x\n# requires: llvm\n") + for host in ("linux", "macos"): + (self.reports / f"e2e-report-{host}-3.tsv").write_text( + "skip\t55_skips_itself.sh\t40\tskipped itself: no emulator on this machine\n") + r = self.run_check() + self.assertEqual(r.returncode, 1) + self.assertIn("UNCOVERED: 55_skips_itself.sh", r.stdout) + self.assertIn("skipped itself: no emulator on this machine", r.stdout) + debt = self.root / "tests" / "e2e" / "coverage-debt.tsv" + debt.write_text("# test\twhat the hosts lack\n55_skips_itself.sh\tno shard installs the emulator\n") + r = self.run_check() + self.assertEqual(r.returncode, 0, r.stdout) + self.assertIn("1 owed, 0 uncovered", r.stdout) + # The debt only shrinks: an entry whose test ran is stale. + (self.reports / "e2e-report-linux-4.tsv").write_text("pass\t55_skips_itself.sh\t900\t\n") + r = self.run_check() + self.assertEqual(r.returncode, 1) + self.assertIn("55_skips_itself.sh is listed as owed but ran on linux", r.stdout) + def test_an_exception_for_a_test_that_ran_fails(self) -> None: (self.reports / "e2e-report-macos-1.tsv").write_text("pass\t30_excused.sh\t10\t\n") r = self.run_check() diff --git a/tests/unit/test_link_line_golden.cpp b/tests/unit/test_link_line_golden.cpp new file mode 100644 index 000000000..98edfa93d --- /dev/null +++ b/tests/unit/test_link_line_golden.cpp @@ -0,0 +1,199 @@ +// THE LINK LINE'S C-LIBRARY GROUP, AS A TABLE (design 2026-11-12 §5.4). +// +// `compute_flags` renders, for every link, the words that name the C library: +// the crt search prefix, the library directories, the loader and the sysroot. +// Which words appear is decided by four facts -- the compiler family, who +// supplies the C library, whether this machine loads the artifact, and the +// linkage -- and each combination has one right answer. This file writes the +// answers down. A change to the renderer that loses a group shows here as one +// differing line, without a toolchain and without a link. +// +// The measurement behind it: a rewrite of the GCC branch dropped `-B`, `-L` +// and the loader of every gcc build over the host's payload (the crt objects +// then resolved from /lib), and no check of the tree asked for those words. +// +// The fixtures are directories of empty files: the renderer reads paths and +// the presence of a loader and of a driver's `.cfg`, never file contents. + +import std; +import mcpplibs.mtest; +import mcpp.build.flags; +import mcpp.build.plan; +import mcpp.manifest; +import mcpp.targetside; +import mcpp.toolchain.base; +import mcpp.toolchain.model; +import mcpplibs.os.platform; + +using namespace mcpplibs::mtest; +using namespace mcpp::build; +namespace tc = mcpp::toolchain; +namespace ts = mcpp::targetside; +namespace fs = std::filesystem; + +namespace { + +struct Tree { + fs::path root; + Tree() { + static std::atomic n{0}; + root = fs::temp_directory_path() + / std::format("mcpp-link-golden-{}-{}", std::chrono::steady_clock::now().time_since_epoch().count(), n++); + fs::create_directories(root); + } + ~Tree() { std::error_code ec; fs::remove_all(root, ec); } + fs::path file(const fs::path& rel) const { + fs::create_directories((root / rel).parent_path()); + std::ofstream(root / rel) << ""; + return root / rel; + } + fs::path dir(const fs::path& rel) const { + fs::create_directories(root / rel); + return root / rel; + } +}; + +BuildPlan plan_for(tc::CompilerId compiler, const fs::path& driver, std::string triple) { + BuildPlan plan; + plan.projectRoot = fs::temp_directory_path() / "mcpp-link-golden"; + plan.outputDir = plan.projectRoot / "target" / "test"; + plan.manifest.package.name = "golden"; + plan.toolchain.compiler = compiler; + plan.toolchain.version = "test"; + plan.toolchain.binaryPath = driver; + plan.toolchain.targetTriple = std::move(triple); + const bool clang = compiler == tc::CompilerId::Clang; + plan.targetSide.compiler = { ts::Origin::Payload, clang ? "llvm" : "gcc", "", false }; + plan.targetSide.kernelAbi = { ts::Origin::Payload, "linux", "", false }; + plan.targetSide.cAbi = { ts::Origin::Payload, "glibc", "", false }; + plan.targetSide.cxx = { ts::Origin::Payload, clang ? "libc++" : "libstdc++", "", false }; + return plan; +} + +// The words of `line` that name the C library, in order, with `tree` written +// as ``: what the table below compares. +std::vector c_library_words(std::string_view line, const fs::path& tree) { + std::vector out; + const auto t = tree.generic_string(); + for (auto part : std::views::split(line, ' ')) { + std::string w(part.begin(), part.end()); + const bool named = w.starts_with("-B") || w.starts_with("-L") || w.starts_with("--sysroot") + || w.starts_with("-Wl,--sysroot") || w.starts_with("-Wl,-rpath") + || w.starts_with("-Wl,--dynamic-linker") || w.starts_with("--target="); + if (!named) continue; + for (auto at = w.find(t); at != std::string::npos; at = w.find(t, at + 3)) w.replace(at, t.size(), ""); + out.push_back(std::move(w)); + } + return out; +} + +using Words = std::vector; + +void expect_words(std::string_view what, const Words& got, const Words& want) { + if (auto&& failed = expect(eq(got, want)); !failed) failed << what; +} + +} // namespace + +// gcc over the host's payload: the crt prefix, the library directory with its +// run-time path, the compiler's own run-time directory, and the payload's +// loader. The regression this file exists for emptied this list. +UTest gcc_over_the_hosts_payload{"LinkLineGolden/GccOverTheHostsPayload", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the ELF link line is rendered on a Linux host"); + Tree t; + const auto glibc = t.dir("glibc/lib64"); + t.file("glibc/lib64/ld-linux-x86-64.so.2"); + t.dir("glibc/include"); + t.dir("gcc/lib64"); + auto plan = plan_for(tc::CompilerId::GCC, t.file("gcc/bin/g++"), "x86_64-linux-gnu"); + plan.toolchain.base.payload = tc::PayloadPaths{ t.root / "glibc/include", glibc, {} }; + const auto f = compute_flags(plan); + const Words want{ "-B/glibc/lib64", "-L/glibc/lib64", "-Wl,-rpath,/glibc/lib64", + "-L/gcc/lib64", "-Wl,-rpath,/gcc/lib64", + "-Wl,--dynamic-linker=/glibc/lib64/ld-linux-x86-64.so.2" }; + expect_words("the C++ link", c_library_words(f.sysroot, t.root), want); +}}; + +// clang with its configuration over the host's payload: the same group, the +// compiler's run-time being compiler-rt's and not a directory of the list. +UTest clang_over_the_hosts_payload{"LinkLineGolden/ClangOverTheHostsPayload", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the ELF link line is rendered on a Linux host"); + Tree t; + const auto glibc = t.dir("glibc/lib64"); + t.file("glibc/lib64/ld-linux-x86-64.so.2"); + t.dir("glibc/include"); + t.file("llvm/bin/clang++.cfg"); + t.dir("llvm/include/c++/v1"); + auto plan = plan_for(tc::CompilerId::Clang, t.file("llvm/bin/clang++"), "x86_64-linux-gnu"); + plan.toolchain.base.payload = tc::PayloadPaths{ t.root / "glibc/include", glibc, {} }; + const auto f = compute_flags(plan); + const Words want{ "-B/glibc/lib64", "-L/glibc/lib64", "-Wl,-rpath,/glibc/lib64", + "-Wl,--dynamic-linker=/glibc/lib64/ld-linux-x86-64.so.2" }; + expect_words("the C++ link", c_library_words(f.ld, t.root), want); + expect_words("the C link", c_library_words(f.ldC, t.root), want); +}}; + +// A base package of another machine (SPEC-016 §6): the sysroot and the row's +// loader, and nothing of this machine -- no directory of the payload, no +// run-time path. The artifact's interpreter is a path on the machine that +// runs it. +UTest clang_over_a_base_of_another_machine{"LinkLineGolden/ClangOverABaseOfAnotherMachine", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the ELF link line is rendered on a Linux host"); + Tree t; + const auto base = t.dir("base"); + t.file("llvm/bin/clang++.cfg"); + t.dir("llvm/include/c++/v1"); + auto plan = plan_for(tc::CompilerId::Clang, t.file("llvm/bin/clang++"), "aarch64-linux-gnu"); + plan.toolchain.crossTargetFlag = "--target=aarch64-unknown-linux-gnu"; + plan.toolchain.base.root = base; + plan.toolchain.base.form = tc::base::Form::Package; + plan.toolchain.base.text = "xim:aarch64-linux-gnu-sysroot@2.44.3"; + plan.toolchain.base.runtime = tc::base::Runtime::Floor; + plan.toolchain.base.interpreter = "/lib/ld-linux-aarch64.so.1"; + plan.toolchain.base.layers.cAbi = "glibc"; + plan.targetSide.cAbi = { ts::Origin::Xpkg, "glibc", "xim:aarch64-linux-gnu-sysroot@2.44.3", false }; + const auto f = compute_flags(plan); + const Words want{ "--target=aarch64-unknown-linux-gnu", "--sysroot=/base", + "-Wl,--dynamic-linker=/lib/ld-linux-aarch64.so.1" }; + expect_words("the C++ link", c_library_words(f.sysroot, t.root), want); + // No word of the line names a directory of this machine for the run time. + expect(eq(f.ld.find("-rpath"), std::string::npos)) << f.ld; +}}; + +// The same base, linked statically: no interpreter is named, since a static +// program has none. +UTest a_static_link_over_a_base_names_no_loader{"LinkLineGolden/AStaticLinkOverABaseNamesNoLoader", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the ELF link line is rendered on a Linux host"); + Tree t; + const auto base = t.dir("base"); + t.file("llvm/bin/clang++.cfg"); + t.dir("llvm/include/c++/v1"); + auto plan = plan_for(tc::CompilerId::Clang, t.file("llvm/bin/clang++"), "aarch64-linux-gnu"); + plan.toolchain.crossTargetFlag = "--target=aarch64-unknown-linux-gnu"; + plan.toolchain.base.root = base; + plan.toolchain.base.form = tc::base::Form::Package; + plan.toolchain.base.text = "xim:aarch64-linux-gnu-sysroot@2.44.3"; + plan.toolchain.base.runtime = tc::base::Runtime::Floor; + plan.targetSide.cAbi = { ts::Origin::Xpkg, "glibc", "xim:aarch64-linux-gnu-sysroot@2.44.3", false }; + const auto f = compute_flags(plan); + const Words want{ "--target=aarch64-unknown-linux-gnu", "--sysroot=/base" }; + expect_words("the C++ link", c_library_words(f.sysroot, t.root), want); +}}; + +// The C library from the dependency graph: the host's model contributes no +// word. What the link names is the graph's, stated by its packages. +UTest the_graph_supplies_the_c_library{"LinkLineGolden/TheGraphSuppliesTheCLibrary", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the ELF link line is rendered on a Linux host"); + Tree t; + const auto glibc = t.dir("glibc/lib64"); + t.file("glibc/lib64/ld-linux-x86-64.so.2"); + t.file("llvm/bin/clang++.cfg"); + t.dir("llvm/include/c++/v1"); + auto plan = plan_for(tc::CompilerId::Clang, t.file("llvm/bin/clang++"), "x86_64-linux-musl"); + plan.toolchain.base.payload = tc::PayloadPaths{ t.root / "glibc/include", glibc, {} }; + plan.targetSide.kernelAbi = { ts::Origin::Graph, "openkal", "openkal-linux@0", false }; + plan.targetSide.cAbi = { ts::Origin::Graph, "musl", "openkal-musl@0", false }; + plan.targetSide.cxx = { ts::Origin::Graph, "libc++", "openkal-llvm-runtime@0", false }; + const auto f = compute_flags(plan); + expect_words("the link's own group", c_library_words(f.sysroot, t.root), Words{}); +}}; diff --git a/tests/unit/test_pack_modes.cpp b/tests/unit/test_pack_modes.cpp index dcc94bab2..7d2e79cd7 100644 --- a/tests/unit/test_pack_modes.cpp +++ b/tests/unit/test_pack_modes.cpp @@ -59,7 +59,9 @@ UTest the_words_name_systems_for_an_architecture{"PackSystems/TheWordsNameSystem using V = std::vector; expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "x86_64"), V{ "linux" })); expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "aarch64"), (V{ "linux", "macos" }))); - expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux", "macos", "windows" }))); + // `all` is the system table's answer, in the table's order (SPEC-016 §2.3). + expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), + (V{ "linux", "macos", "freebsd", "windows" }))); expect(eq(mcpp::pack::systems_for({}, "x86_64").size(), 1u)); }}; diff --git a/tests/unit/test_pack_polyglot.cpp b/tests/unit/test_pack_polyglot.cpp index 88866317e..c03f48ca4 100644 --- a/tests/unit/test_pack_polyglot.cpp +++ b/tests/unit/test_pack_polyglot.cpp @@ -60,11 +60,24 @@ UTest without_windows_the_file_is_a_script_and_each_program_lies_where_its_line_ expect(ne(script.find("Darwin-aarch64|Darwin-arm64)"), std::string_view::npos)); }}; +// The word a system is selected by is the system table's `uname` (SPEC-016 +// §2.3): a system the table states joins the file with no word of the code. +UTest a_system_of_the_table_is_selected_by_its_uname{"PackPolyglot/ASystemOfTheTableIsSelectedByItsUname", [] { + auto file = pg::assemble("app", { program("linux", "x86_64", "LINUX"), + program("freebsd", "x86_64", "FREEBSD") }); + expect(fatal(file.has_value())); + expect(ne(file->find("FreeBSD-x86_64|FreeBSD-amd64)"), std::string::npos)); + expect(ne(file->find("Linux-x86_64|Linux-amd64)"), std::string::npos)); +}}; + UTest refuses_what_it_cannot_lay_out{"PackPolyglot/RefusesWhatItCannotLayOut", [] { expect(!(pg::assemble("app", {}).has_value())); expect(!(pg::assemble("a'b", { program("linux", "x86_64", "x") }).has_value())) << "the name is quoted in the script"; - expect(!(pg::assemble("app", { program("freebsd", "x86_64", "x") }).has_value())); + // A system no table states has no `uname` word to be selected by. + auto unstated = pg::assemble("app", { program("plan9", "x86_64", "x") }); + expect(fatal(!unstated.has_value())); + expect(ne(unstated.error().find("[[platform.system]]"), std::string::npos)) << unstated.error(); auto notPe = pg::assemble("app", { program("windows", "x86_64", std::string(256, 'x')) }); expect(fatal(!notPe.has_value())); if (auto&& failed = expect(ne(notPe.error().find("no PE"), std::string::npos)); !failed) failed << notPe.error(); diff --git a/tests/unit/test_runner_lookup.cpp b/tests/unit/test_runner_lookup.cpp index 144aca228..badcbc3e5 100644 --- a/tests/unit/test_runner_lookup.cpp +++ b/tests/unit/test_runner_lookup.cpp @@ -111,7 +111,10 @@ UTest unrunnable_message_names_kernel_answer_triple_and_key{"RunnerLookup/Unrunn expect(ne(msg.find("/x/bin/app"), std::string::npos)) << msg; expect(ne(msg.find("built for 'aarch64-linux-musl'"), std::string::npos)) << msg; expect(ne(msg.find("[target.aarch64-linux-musl]"), std::string::npos)) << msg; - expect(ne(msg.find("runner = [\"qemu-aarch64-static\"]"), std::string::npos)) << msg; + // The key's shape, and no program: which one runs the artifact is a + // statement of the data (SPEC-016 §6). + expect(ne(msg.find("runner = [\"\", \"{}\"]"), std::string::npos)) << msg; + expect(eq(msg.find("qemu"), std::string::npos)) << msg; expect(ne(msg.find("--no-runner"), std::string::npos)) << msg; }};