diff --git a/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md b/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md new file mode 100644 index 00000000..95cf9a0d --- /dev/null +++ b/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md @@ -0,0 +1,473 @@ +# 商业级可用:mcpp × xlings 的裸机与嵌入式总体方案 + +2026-09-04 · 多仓库总体方案 · **v4:P0 引擎切片已实施**(PR #550);其余批次待实施 + +前置讨论: +[`2026-08-21-baremetal-ecosystem-assessment.md`](2026-08-21-baremetal-ecosystem-assessment.md)(七角度评估) · +[`2026-08-20-baremetal-user-facing-scenarios.md`](2026-08-20-baremetal-user-facing-scenarios.md)(场景) · +[`2026-07-24-embedded-platform-support-design.md`](2026-07-24-embedded-platform-support-design.md)(issue #276) · +[`2026-08-27-openkal-ecosystem-design-plan.md`](2026-08-27-openkal-ecosystem-design-plan.md)(生态接口判据) + +> ⭐ **只想看结论的读者直接去 §8**:十三条决定,每条附判据与它推翻了上一版的哪一句。 +> §2 是唯一的新设计,其余各章是数据与归属。 + +--- + +## 0. 一句话,与本文的骨架 + +> **裸机与嵌入式的"极致体验"不是功能的总和,是「一件事只说一次,并且说在唯一知道 +> 它的那一层」。** + +所以本文不是一张功能清单,而是一张**归属表**。每一条待办先过同一个三问,再决定它 +落在哪个仓库。 + +### 0.1 三侧划分的判据 + +| 侧 | 它回答的问题 | 判别问法 | 契约形式 | +|---|---|---|---| +| **mcpp 引擎** | 对**所有**工程都一样的决定,或**只有引擎同时知道两边的地址** | 「换一块板、换一个厂商,这条会变吗?」**会变 ⇒ 不在这侧** | 表 + 单一读点 | +| **build.mcpp 插件(包)** | 属于**某块板 / 某个库**的决定,消费者不该重述 | 「谁知道这件事?」**板知道 ⇒ 在这侧** | directive 协议 + `import mcpp` 类型化 API | +| **xlings 生态** | 一个**要被装到机器上的东西** | 「它是个产物,还是个决定?」**产物 ⇒ 这侧** | xim 描述符 + `xpm.<平台>.deps` | + +⭐ **这三条不是新发明的,是把仓库里已经在用的三条规则合成一句。** + +* `docs/13`:*location is a target fact, selection is a board fact* +* `docs/14`:五层 target side × 四种来源(`payload` / `prebuilt` / `graph` / 缺席) +* openkal SPEC clause 10 的基数(openarch 一个 · openkal 每接口一个 · openhal 多个) + +⚠️ **判据的反面同样承重,方案里每一条都要过它**:**一个决定被两侧同时表达,不会在 +加进去的那天失败,会在加下一条语义时变成构建失败。** 这个代价这个仓库付过四次 +(#233 / #240 / #242 / #344)。凡是本文里出现「引擎也知道一点、包也知道一点」的条目, +都必须重写到只剩一侧。 + +### 0.2 「商业级可用」的六个轴 + +不给判据的「商业级」是口号。本文把它拆成六条,每条都能被证伪: + +| 轴 | 判据(一句话) | 今天 | +|---|---|---| +| **A 覆盖** | 目标机器能不能编出正确的目标文件 | 裸机四行,**Cortex-M 缺席** | +| **B 可信** | 判据是否产生在能观察到该现象的环境里 | **全部来自 QEMU**,零真机 | +| **C 闭环** | 从 `new` 到烧录、调试、看串口是不是一条命令链 | **烧录/调试/串口三项完全缺席** | +| **D 可复现** | 同一份输入,两次、两地、两台机器得到同一产物 | ⚠️ **`mcpp.lock` 自己的头注释:"does not yet pin future builds"** —— 它是记录不是钉 | +| **E 可交付** | 企业采购会问的:SBOM、许可、离线内网、支持窗口 | **近乎空白**(`sbom`/`spdx` 全仓零命中) | +| **F 可扩展** | 第三方加一块板 / 一个厂商,引擎 diff 为零 | ⭐ **今天最强的一条,方案要保护它** | + +--- + +## 1. 资产负债表(实测,2026-09-04) + +### 1.1 已经就位,且比预期多 + +| | 实测 | +|---|---| +| 引擎已模块化 | `modules/` 九个包(`buildmcpp` `manifest` `toolchain-model` `platform` `dyndep` `libs` `log` `source-kind` `versioning`) | +| 目标侧模型 | `docs/14` 五层 × 四来源;`hosted-standard-library` 之后**裸机可以有完整 C++**(异常、RTTI、`import std`、`filesystem`) | +| 插件面 | 15 条 directive × 7 种 scope(`PackagePrivate` `LinkGlobal` `SourceSet` `RunGlobal` `RerunKey` `Advisory` `GraphNode`)+ 类型化 `import mcpp` + `action{}` 图节点 + `dep_bin` 宿主工具 | +| 模拟器 | ⭐ **`xim:qemu-arm@9.2.4-1` 已带 `qemu-system-arm`**,机器表实测覆盖 M0(`microbit`)/ M3(`mps2-an385`)/ M4(`mps2-an386`,`b-l475e-iot01a`)/ M7(`mps2-an500`)/ M33(`mps2-an505`,双核 `an521`)/ M55(`mps3-an547`)。**已发布、已装机、一次未用** | +| triple 解析 | 已认识 `eabi`/`eabihf` 且带 `envExplicit`;`normalize_arch` 直通,`cfg(arch=…)` 无封闭词表 | +| ISA 表的列 | `extra` 列(x86_64 为 `-mno-red-zone` 加的)**正好是 `-mfloat-abi`/`-mfpu` 的形状** | +| 运行槽 | `Slot::Runner` + `Scope::RunGlobal`:板级包逐 token 拼 argv,工程可覆盖且被报告 | +| 离线 | `--offline` → `MCPP_OFFLINE` | + +### 1.2 缺口,按侧分 + +| 侧 | 缺什么 | +|---|---| +| **引擎** | Cortex-M 目标行族;`flash`/`monitor`/`debug` 三个槽;**runner 的独占语义**(§2.3);**freestanding 链接无 `--gc-sections`**(实测全仓零命中,§3.1.1);lock 的钉住语义;SBOM 输出 | +| **插件(包)** | ARM 板级包零;真机板级包零 | +| **xlings** | ⚠️ **187 个包里 `openocd`/`probe-rs`/`pyocd`/`gdb`/`dfu-util`/`esptool`/`jlink` 零命中** —— 整个真机工具层不存在。(ARM 的 C 库**不走这一侧**,见 §3.1.1) | +| **判据** | 真机 CI 不存在;裸机 + `import std` 在 mcpp 侧只有 unit test,e2e 在生态仓 | +| **文档** | `docs/13` 停在 `2026.8.21.3`,其「异常关闭 / `import std` 不可用」两条已被 `2026.8.28.2` 推翻 | + +--- + +## 2. C 轴是重心 —— 但比 v1 想的小得多 + +这是全文唯一需要**新设计**的地方。v1 把它写成"三个新槽 + 三种进程语义",自审后 +两条判断把它缩小了一半。 + +### 2.1 ⭐⭐ 真机上的 `mcpp run` / `mcpp test` 今天就不需要引擎改动 + +`probe-rs run --chip ` 一个进程做完:烧录、复位、接 RTT/半主机、**把固件 +经半主机 `SYS_EXIT` 报的退出码作为自己的退出码**。这与 `qemu-system-* -semihosting` +的形状**逐字段相同** —— 都是「一次性、读退出码」。 + +⇒ 真机板级包只需把 `runner` 槽填成 `probe-rs run --chip STM32L475VGTx {}`, +**`mcpp run` 就在真机上工作**,引擎零 diff;`mcpp test` 也一样,**前提是一次只跑一个** +—— 那个前提今天要工程自己记住,§2.3 把它交给板级包。 + +⚠️ **这条推翻了 v1 的分批**:v1 把"引擎槽表"放在真机之前。实际上通往真机 +`mcpp test` 的关键路径是 **xlings 的 `probe-rs` 包 + 一个真机板级包**,槽表是 +**附加**的(`flash` 只烧不跑、`monitor`、`debug`),不在关键路径上。 + +### 2.2 三个新槽,两种语义 —— 不是三种 + +v1 给 `debug` 发明了第三种语义 `Paired`(server + 客户端,交互式,tty 透传)。 +自审:**那是把 IDE 的活揽进 mcpp**。`probe-rs gdb` / `openocd` 起的是一个 **server**; +连上去的 gdb / DAP 客户端属于 VS Code、CLion、用户的终端 —— mcpp 已经有一条给 IDE +的机器接口(`docs/11`),边界就画在那里。 + +| 槽 | 语义 | 引擎做的事 | +|---|---|---| +| `run` | `OneShot` | (已有)读退出码 | +| `flash` | `OneShot` | 读退出码;**读回校验是工具的事,退出码承载它** | +| `monitor` | `LongLived` | 起进程、透传 stdio、把 Ctrl-C 交给它、**不把"永不退出"当失败** | +| `debug` | `LongLived` | 同上,并在机器输出通道上发一行 `{ "gdb_port": …, "elf": … }` 供 IDE 连接 | + +⇒ **`Semantics ∈ {OneShot, LongLived}`,两个值**。`debug` 在两值语义下与 `monitor` +同形,**移回 P1**;v1 把它推到 P2' 的理由(可能要会话协议)随边界一起消失。 + +### 2.3 ⚠️ 真机带来一列模拟器从来不需要的:独占 + +实测 `execute.cppm:2022`:`mcpp test` 用一个 **`workers` 大小的线程池**并发跑测试 +二进制。QEMU 可以开 N 个实例;**一块物理板是一把互斥锁** —— 两个 `probe-rs` 抢一个 +探针,结果不是失败而是**乱**。 + +⇒ 板级包发一条 `mcpp:runner-exclusive=1`(`Scope::RunGlobal`,布尔),引擎在运行 +阶段把 workers 钳到 1。**板知道自己是独占的,工程不该记得 `-j1`**。这是 x86_64 +长出 `extra` 列的同一形状:第一块真板挖出的不是缺陷,是表少一列。 + +### 2.4 线协议:三行,不是一种新语法 + +v1 写了 `mcpp:device-action=:`。自审:那是在 directive 表旁边**发明第二 +套语法**。`runner` 当初就是表里**一行**,新槽照做: + +``` +表: {"flash", …, Slot::Flash, Scope::RunGlobal, …} 语义 OneShot + {"monitor", …, Slot::Monitor, Scope::RunGlobal, …} 语义 LongLived + {"debug", …, Slot::Debug, Scope::RunGlobal, …} 语义 LongLived + {"runner-exclusive", …, Scope::RunGlobal, …} 布尔 +类型化: mcpp::flash(tok) · mcpp::monitor(tok) · mcpp::debug(tok) · mcpp::runner_exclusive() +覆盖: [target.].flash = [...] 与 runner 同轴、同一句 note +命令: mcpp flash · mcpp monitor · mcpp debug 同一个读点 +``` + +⚠️ 每加一条 directive 要改的位置是九处(`build-mcpp-extensibility-architecture` +记的),这是已知代价,不是新代价。 + +### 2.5 三侧分工 + +| 侧 | 负责 | 例 | +|---|---|---| +| **引擎** | 四行表、语义列、单一读点、覆盖与 note、命令族、产物集(`.elf` `.bin` **`.hex`** `.map`) | | +| **板级包** | 每个槽的 argv 与条件降级 | `probe-rs run --chip STM32L475VGTx {}` · `probe-rs download --chip … --verify {}` · `probe-rs gdb --chip …` | +| **xlings** | 把工具装到机器上 | `xim:probe-rs`(P0')· `xim:openocd`(P2)· `xim:arm-none-eabi-gdb` | + +⚠️ **今天第三列一个都没有** ⇒ xlings 侧**先行**,不是并行。 + +--- + +## 3. 逐轴方案 + +### 3.1 A 覆盖:Cortex-M 目标行族 + +| 侧 | 事项 | 备注 | +|---|---|---| +| 引擎 | `kKnownTargets` + `freestanding::kTable` 加 **7** 行:`thumbv6m-eabi` `thumbv7m-eabi` `thumbv7em-eabi` `thumbv7em-eabihf` `thumbv8m.base-eabi` `thumbv8m.main-eabi` `thumbv8m.main-eabihf`(均 `-none-`) | `lldEmulation` **留空**(clang 有 arm 的 BareMetal 工具链);`mcmodel` **留空**;`-mfloat-abi`/`-mfpu` 走已存在的 `extra` 列;`mabi = "aapcs"` 同 aarch64 行 | +| 引擎 | 表形状:**7 行,不引入 `-mcpu` 第二轴** | 判据在表自己的注释里:「让 `--target ` 单独足够产出正确的目标文件」。`thumbv6m` 与 `thumbv7em` 的目标文件互不兼容 ⇒ 是行不是偏好 | +| 引擎 | ⚠️ **`-mfpu` 的默认值要实测不要类推**:`thumbv7em-none-eabihf` 不给 `-mfpu` 时 clang 选什么,用 `-###` 问机器 | aarch64 行 `lp64`→`aapcs` 的教训:表按类比填,会错在类比最强的那格 | +| 引擎 | `libdir` 与 `sysroot` 列:**7 行全部留空** | 图来源没有多库目录;列只在 sysroot 存在时被读(§3.1.1) | +| 引擎 | ⭐ **freestanding 编译加 `-ffunction-sections -fdata-sections`,链接加 `--gc-sections`** | 实测全仓零命中。依赖的目标文件**无条件进链接**(`docs/13:423`),没有它图来源的 C 库会把整份 picolibc 放进每个 MCU 镜像 | +| 引擎 | 分级:**2 行 `verified`**(`v6m-eabi`、`v7em-eabihf`,各有板)**5 行 `preview`** | 与 aarch64/x86_64 落地时相同 | +| **mcpp-index** | ⭐ **`mcpplibs/picolibc`:源码包**,`provides = ["mcpp:c-abi=picolibc"]`;**不建 `xim:picolibc-arm`** | §3.1.1。上游 zip(实测 API:`picolibc-arm-none-eabi-1.8.12-15.3.rel1.zip`,Arm GNU 15.3/GCC 编)无论如何不可用 —— `docs/14` 规则拒绝 llvm 下的 libgcc;而"自建 7 个多库"是 prebuilt 来源自带的成本 | +| **mcpp-index** | `mcpplibs/compiler-rt-builtins`:源码包,`provides = ["mcpp:compiler-runtime=compiler-rt"]`;`picolibc` 依赖它 | 只 vendor `compiler-rt/lib/builtins`,目标无关。M0 无硬件除法(`__aeabi_idiv`)、软浮点全套、ryu 的 128 位移位全在这里 | + +**工作量**:引擎侧 7 行数据 + 两个 flag。C 库是源码包,主要工作是 vendor + 生成配置 +(`openkal-musl` 的 `musl-generated/`、`openkal-llvm-runtime` 的 `llvm-generated/` 是现成先例)。 + +#### 3.1.1 ⭐ 为什么 C 库走 mcpp-index 而不是 xim 载荷(review 第一轮的追问,已决定) + +`docs/14` 原文:*"Moving a layer from a prebuilt payload into the dependency graph +removes engine work rather than adding it. This is the mechanism by which one source +reaches several platforms without an engine change."* + +v2 初稿为 prebuilt 来源列出的工作,逐条对照: + +| prebuilt 来源要做的 | 图来源 | +|---|---| +| 7 个多库各建一次 | **没有多库**:C 库用与程序完全相同的 `compile_flags` 编译,`-mfpu`/浮点 ABI 一致是**按构造**成立的 | +| `libdir` 列与包目录逐字节对上(#481 的形状) | **列为空** | +| builtins 与 C 库同包,否则第一次 printf 挂 | 两个包,`picolibc` 依赖 `compiler-rt-builtins`,由五层规则「配置于其下的层」表达 | +| 五宿主镜像、`.sha256` 侧文件、CDN 传播等待 | 一个源码 tarball,宿主无关 | +| 版本钉在目标表里,**在 lock 之外** | 进 `mcpp.lock`(D 轴) | +| 每个架构一个包(`picolibc-riscv/aarch64/x86` 已是三个) | **一个包服务全部 11 行裸机目标** | + +代价与前提: + +* ⚠️ **首次构建付编译一次 picolibc 的墙钟**。全局依赖缓存(`docs/05 §2.10`, + `$MCPP_HOME/build-cache/v1/`,跨工程、按工具链 × profile × 版本键)使它是**每台机器 + 每个目标档一次**。第一天的探针里量它。 +* ⚠️⚠️ **前提是 `--gc-sections`**。依赖的目标文件无条件进链接(`docs/13:423`),而全仓 + 没有任何 `gc-sections`/`function-sections`(实测零命中)。hosted 目标不在乎,QEMU 的 + 128 MB RAM 也不在乎;**64 KB flash 的 MCU 放不下整份 picolibc**。这是图来源的 C 库在 + MCU 上暴露的第一条引擎缺口,也是任何商业 MCU 工具链的标配。 + ⚠️ 随之而来的:向量表没有任何引用,`--gc-sections` 会把它删掉 —— 板级链接脚本必须 + `KEEP(*(.vectors))`;判据是镜像能启动,不是链接退 0。 +* 板级包不再 `link_lib("c")`,改为**依赖** `picolibc`;链接脚本走 picolibc 的 `build.mcpp` + 发 `link-search`(LinkGlobal)+ 板级 `board.ld` 里 `INCLUDE picolibc.ld` —— picolibc + 上游设计的用法,零新 API。 +* `sysroot_dir()` 对图来源返回空;板级包按 `aarch64-virt-rt` 已有的"缺席"分支处理。 +* ⚠️ 这与 `docs/13` 记的「板级包不该绑定 libc」不冲突:那条针对的是 `[xlings] deps` + 里**按架构分的 prebuilt** 绑定;源码依赖是架构无关的。 + +⭐ **判据**(同时是三个既有 prebuilt 载荷要不要迁移的判据):同一份半主机 hello, +riscv64 上用 `mcpplibs/picolibc` 与用 `xim:picolibc-riscv` 各链一次,**`Size text` 相当且 +输出相同**。差得多 ⇒ gc-sections 没生效或配置生成错了;相当 ⇒ 三个 prebuilt 包之后 +可以各自退休。这也是 mcpp 自己的 e2e 里**第一个"图来源 c-abi 上裸机目标"**的用例 +(今天零个,实测 `tests/e2e` 无 `c-abi=`)。 + +### 3.2 板的选择(v1 的问题 2,已决定) + +判据:两块板要在**每一条会影响判据的轴上都不同**,并且其中一块要**同时存在于 QEMU +与真机** —— 那样同一个板级包有两个观察环境,「模拟器 vs 真机」这条差异本身成为可测的。 + +| | 板 A | 板 B | +|---|---|---| +| 板 | **B-L475E-IOT01A**(ST) | **Raspberry Pi Pico**(RP2040) | +| 核 / 行 | Cortex-M4F · `thumbv7em-none-eabihf` | Cortex-M0+ · `thumbv6m-none-eabi` | +| 浮点 / 除法 | 硬浮点、硬除法 | **软浮点、无除法指令** —— builtins 判据在这块板上 | +| 探针 | 板载 ST-LINK v2-1 | CMSIS-DAP(debugprobe / 第二块 Pico) | +| 启动 | flash 直接执行 | **ROM bootloader + 镜像内 256 字节 boot2**,另有 UF2 拖放 | +| 核数 | 单核 | 双核 | +| QEMU | ⭐ **有**(`b-l475e-iot01a`,QEMU 9.0+ 的 stm32l4x5 模型) | 无 | + +⇒ P0(仅 QEMU)用 **`b-l475e-iot01a`(M4F)+ `microbit`(M0,nRF51,QEMU 有)** +两台机器,覆盖两个浮点档;P1'(真机)用 **B-L475E-IOT01A + Pico**。`b-l475e-iot01a-rt` +一个包服务 P0 与 P1' 两行 —— 这是本方案里判据最强的一处。 + +⚠️ **第一天的探针**:stm32l4x5 的 QEMU 模型较新,先确认 +`qemu-system-arm -machine b-l475e-iot01a -semihosting -kernel hello.elf` 能打印。 +不能 ⇒ P0 回落到 `mps2-an385`(M3),板 A 的 QEMU 行留到模型修好。 + +⚠️ **不选 mps2-an385 做主板的理由**:它是 FPGA 原型板,没人拥有;一个只在 QEMU 里 +存在的板级包永远得不到真机那一行。 + +### 3.3 B 可信:真机判据 + +⭐ **第一条判据不是「测试通过」,是「产物真的进了 flash」**:`probe-rs download --verify` +读回比对,退出码承载。「烧录命令退 0」与「芯片里是这份镜像」是两件事(`.mcpp_ok` +只证进程退 0 的同形)。 + +⚠️ **HIL 会长成 flaky,而 flaky 判据比没有判据更糟**(它训练所有人重跑)。 +⇒ 真机 job 从第一天起分开报告「探针没找到」与「测试失败」,前者不染红 PR。 +`probe-rs list` 在跑测试前作分母。 + +### 3.4 D 可复现 —— 比 v1 写的更严重 + +`mcpp.lock` 头注释原文:*"It does not yet pin future builds: index dependencies are +re-resolved from their constraints each time."* ⇒ **今天没有任何东西钉住一次解析**; +`--locked` 不是"加一个 flag",是**先让 lock 成为钉,再有 flag 可以校验它**。 + +| 侧 | 事项 | 判据 | +|---|---|---| +| 引擎 | lock 从记录升级为钉:再次解析**先读 lock**,不符才走约束;`--locked` 时不符即失败 | 改 lock 后 `mcpp build` 用的版本随之变 | +| 引擎 | lock 覆盖**工具链与载荷**(今天 pin 在目标表里,不在 lock 里) | 换机器拿到同一个 llvm | +| 引擎 | `-ffile-prefix-map`,产物不嵌绝对路径与时间戳 | **两台机器两个目录,sha256 相同** | +| xlings | 索引可钉到 commit 而非 `latest` | 「消费者先发布,`latest` 才能动」的解药 | + +⚠️ 可复现的判据**必须跨机器**。同机两次相同只证明缓存工作。 + +### 3.5 E 可交付(v1 的问题 3,已决定:按依赖拆,不按阶段排) + +| 依赖 | 事项 | 放哪 | +|---|---|---| +| **零工程** | 支持窗口与破坏性变更通知期(书面) | **P0'**,与 P0 并行 | +| **零工程** | 传递闭包的许可清单(文档 + `mcpp explain` 已有的图) | P0' | +| **一条输出格式** | `mcpp sbom`(SPDX / CycloneDX):图与版本 mcpp 全知道 | P0' | +| **工程** | `--deny-license` 门 | P2 | +| **工程** | 整仓离线快照一条命令(索引 + 载荷 + 包 → 可拷进内网的目录) | P2 | +| **文档** | 回填 `docs/13` 两条失效限制 | **P0**,本周 | + +v1 按工程依赖把 E 整个排到 P2。自审:**其中一半没有工程依赖**,排后面只是惯性。 + +### 3.6 F 可扩展:保护今天最强的一条 + +⭐ **加一块板 = 加一个包,引擎 diff 为零。** 本方案的判据:P0 与 P1' 的三个板级包 +(`b-l475e-iot01a-rt` / `microbit-rt` / `pico-rt`)**每一个**都必须在 mcpp 引擎零 diff +的前提下落地(§2 的四行表除外 —— 那是一次性的,且三个包共用)。 + +写成 CI 可检查的:板级包仓库的 PR **不得**要求新版 mcpp(除非显式声明); +`if constexpr (requires { mcpp::x(…) })` 对不存在的限定名是**硬错误**,所以降级靠 +引擎侧那条「编译失败且错误含『不是 `mcpp` 的成员』则追加升级提示」的补偿,新增的 +`flash`/`monitor`/`debug` 也靠它。 + +--- + +## 4. 多仓库分工总表 + +| 仓库 | 角色 | 主要条目 | +|---|---|---| +| **mcpp** | 表、槽、单一读点、命令族、lock 语义、SBOM | 7 行 thumb;freestanding `--gc-sections`;`flash`/`monitor`/`debug`/`runner-exclusive` 四条 directive + 语义列;`.hex`;lock 成钉 + `--locked`;`mcpp sbom`;`docs/13` 回填;裸机 `import std` 的 mcpp 侧 e2e | +| **openxlings/xim-pkgindex** | 工具 | **`probe-rs`**;`arm-none-eabi-gdb`;`openocd`(P2);离线快照 | +| **mcpplibs/mcpp-index** | 登记与模板 + **目标侧源码包** | 三个板级包描述符;**`picolibc`、`compiler-rt-builtins` 两个新源码包**(§3.1.1);模板随包走 | +| **新建 ×3:板级包** | 槽 + 链接脚本 + 启动 | `b-l475e-iot01a-rt`(QEMU + 真机)· `microbit-rt`(QEMU)· `pico-rt`(真机,含 boot2) | +| **mcpplibs/openarch** | 机器机制层 | 第四后端 **aarch32(A/R)先于 Cortex-M**(§5 P3) | +| **openkal ***、`std-freestanding` | 语言与环境层 | 无新结构;随目标增加回归 | +| **文档** | `docs/13` 回填、`docs/18-devices`(新)、支持窗口声明 | | + +--- + +## 5. 分批清单 + +每批带**判据**,判据不成立则该批不算完成。 + +### 第一天的探针(任何批开始前) + +1. `qemu-system-arm -machine b-l475e-iot01a -semihosting` 能否打印(决定 §3.2 的回落) +2. `clang --target=thumbv7em-none-eabihf -###` 的默认 `-mfpu`(填表) +3. `probe-rs` 发布二进制的 `LD_TRACE_LOADED_OBJECTS=1` 闭包(决定它是不是"自带库载荷") +4. picolibc 从源码经 mcpp 编译一遍的墙钟,以及**第二个工程**是否命中全局缓存(决定首次 + 构建体验;判据是第二个工程的 `Cached` 行与墙钟,不是第一个的) + +### P0 —— 能编 Cortex-M + +1. 引擎:7 行(2 verified + 5 preview);**freestanding `-ffunction-sections -fdata-sections` + + `--gc-sections`**;`docs/13` 回填;裸机 `import std` 的 mcpp 侧 e2e +2. mcpp-index:`mcpplibs/picolibc` + `mcpplibs/compiler-rt-builtins` 源码包(§3.1.1) +3. 包:`b-l475e-iot01a-rt`、`microbit-rt` +4. **判据**:两个模板 `mcpp new && mcpp run` 在 CI 的 qemu 里打印;断言 PASS 行; + **microbit 上一次浮点 printf + 一次整数除法**(M4F 那行对 builtins 缺口是假绿); + **riscv64 上图来源与 prebuilt 的 `Size text` 相当、输出相同**(§3.1.1 的判据) + +### P0' —— 与 P0 并行 + +5. xlings:`xim:probe-rs`(五宿主、双镜像、`.sha256` 侧文件) +6. E 轴零工程项:支持窗口声明、许可清单、`mcpp sbom` +7. **判据**:干净机器 `xlings install probe-rs -y` 后 `probe-rs list` 有输出且闭包不越界 + +### P1 —— 真机(关键路径:§2.1) + +8. 包:`pico-rt`;`b-l475e-iot01a-rt` 加真机 `runner`(`probe-rs run`) +9. 引擎:`runner-exclusive`(§2.3)—— **唯一在真机关键路径上的引擎改动** +10. 自托管 runner + 两块板;探针缺席与测试失败分开报告 +11. **判据**:`mcpp test` 在两块板上读到退出码;`probe-rs download --verify` 读回通过 + +### P1' —— 闭环槽 + +12. 引擎:`flash` / `monitor` / `debug` 三行 + 语义列;`.hex` +13. 包:三个板级包填三个槽 +14. **判据**:`mcpp debug` 发出的 `{gdb_port, elf}` 能被一个 gdb 连上并停在 `main` + +### P2 —— 可交付与第二实现 + +15. lock 成钉 + `--locked`;跨机器 sha256 判据 +16. `--deny-license`;离线快照 +17. xlings:`xim:openocd` —— **同一板级包契约的第二个实现**,这本身是槽抽象的判据 + +### P3 —— openarch 第四后端 + +18. **aarch32(Cortex-A/R 32 位)优先**:14 个 ABI 函数一个不缺(CP15 的 + `TPIDRPRW`/`TPIDRURW` 恰是两个指针槽;真 MMU;`VBAR`;DMB/DSB/ISB),且它是第一台 + 32 位机器,会挖出 `arch_pte_make_leaf` 返回 `arch_u64` 这条没被问过的宽度假设 +19. **Cortex-M 后端在其后**,因为要先回答**规范问题**:openarch 的可行性闸是「上下文 + 切换 + 页表项」两件,M-profile 没有 MMU、没有页表项,只满足一件。14 个函数里 + 5 个可写、4 个不可能(`pte_*`)、5 个语义改变(`trap_set_handler`;`percpu`/`tls` + 四个 —— 无 TPIDR 类寄存器,退化成全局变量后「两个槽是不同的」变成恒真)。 + ⇒ 先决定**接口允不允许部分后端** + +### P3' —— 嵌入式 Linux(issue #276) + +20. `[target.*].sysroot` 接受外部路径(今天走 `parse_xpkg_ref`);pkg-config +21. 硬边界不变:target 侧必须支持 C++23 modules(GCC ≥ 15);方案是 config② + (mcpp 自带匹配 libc 的交叉编译器 + 只经 sysroot 取库) + +--- + +## 6. 会踩的坑(取自本仓库已付过的代价) + +| # | 坑 | 本方案哪一条会踩 | +|---|---|---| +| 1 | **判据必须放在能观察到该现象的环境里,矩阵的每一行是一个独立观察环境** | 真机 job、qemu job:按**行**穷举 | +| 2 | **`# requires:` 的 e2e 在 shard 上可能一次都没跑过**,`run_all.sh` 跳过退 0 | 守卫住在 job 里,断言 PASS 行 | +| 3 | **判据的「否」与「没测成」同读数** | 烧录校验、闭包检查:带**分母**(`probe-rs list`) | +| 4 | **声明 ≠ 安装**,`[xlings] deps` 不是安装触发器 | probe-rs 的安装边写在索引描述符 `xpm.<平台>.deps`;判据「拿走再装回来」 | +| 5 | **带 `build.mcpp` 的包必须 Form A** | 三个板级包 | +| 6 | **索引陈旧有五层**,第五层是 xlings 消费 `artifact:`;判据是 `Publish Index Artifact` 在那个 commit 上绿 | 发布后等 ~10 分钟再跑依赖它的 CI | +| 7 | **发布形态 ≠ 开发形态**:生态 CI 全用工作树替换 ⇒ 冲突不存在 | 沙箱解析**已发布**包是唯一验发布物的路径 | +| 8 | **消费者先发布,`latest` 才能动** | `picolibc-arm` / 板级包的发布顺序 | +| 9 | **裸名工具经 PATH 解析到属于另一个 home 的 shim** | 所有槽的 argv 绝对路径 | +| 10 | **自带库载荷 + 声明 loader 依赖 ⇒ elfpatch 整条替换 RPATH,装完即坏** | `openocd`(libusb/hidapi/ftdi)是这一类;`probe-rs` 第一天的探针就是为了确认它不是 | +| 11 | ⚠️ **Windows 上 `GenerateConsoleCtrlEvent` 打到整个控制台**,job object 才是机制 | `LongLived` 槽的 Ctrl-C 归属 —— `monitor`/`debug` 在 Windows 上的第一个缺陷会是这个 | +| 12 | **`pipefail` + `grep -q` 把匹配成功读成 141** | 新脚本用 `grep -c` | +| 13 | **依赖缓存曾经"同进程命中、跨进程付全价"**,`Cached` 一行不是判据 | picolibc 的首次构建体验:判据是**第二个工程**的墙钟 | +| 14 | **`--gc-sections` 会删掉没被引用的向量表** | 板级链接脚本 `KEEP(*(.vectors))`;判据是镜像能启动 | + +--- + +## 7. 明确不做的 + +* **L4 build backend**(mcpp 作 Yocto/Buildroot 后端)—— 2026-07-24 维护者决策,不改 +* **消费厂商预编译 C++ 库** —— 「C++ 只源码,二进制只 C-ABI + 封装」 +* **mcpp 驱动 gdb / DAP 客户端** —— 那是 IDE 的边界(§2.2) +* **ESP32 / Xtensa** —— 非 LLVM 上游后端 +* **Cortex-M 的 openarch 后端在规范决定之前落地**(§5 P3) + +--- + +## 8. 十三条决定(自审十条 + review 第一轮三条) + +| # | 决定 | 判据 | 推翻了 v1 的哪一句 | +|---|---|---|---| +| D1 | **真机 `mcpp run`/`mcpp test` 走现有 `runner` 槽,引擎零 diff** | `probe-rs run` = 烧录 + 复位 + 半主机退出码,一个进程,与 qemu `-semihosting` 同形 | "P1 引擎槽表在真机之前" | +| D2 | **语义列两个值 `{OneShot, LongLived}`,不是三个** | `debug` 只起 server,客户端归 IDE(`docs/11` 已有那条边界) | "`debug` 是 `Paired`,可能要会话协议" | +| D3 | **`debug` 回到 P1'**,与 `monitor` 同批 | D2 之后它与 `monitor` 同形 | "`debug` 推到 P2'" | +| D4 | **线协议 = directive 表加四行**,不发明 `device-action=:` | `runner` 当初就是一行;第二套语法是第二个解析器 | v1 §2.1 的语法 | +| D5 | **新增 `runner-exclusive`** | 实测 `execute.cppm:2022` 线程池并发跑测试;板是互斥锁 | v1 没有它 —— 第一块真板挖出的那一列 | +| D6 | **`probe-rs` 先于 `openocd`**,后者 P2 作第二实现 | 单二进制、五宿主预编译、内置芯片库、`run` 一步到位;openocd 是"自带库载荷"形状 | "openocd / probe-rs 并列" | +| D7 | **C 库进 mcpp-index 作源码包(`picolibc` + `compiler-rt-builtins`),不建 `xim:picolibc-arm`** | `docs/14`:层从 prebuilt 移进图是**减**引擎工作;多库、`libdir`、五宿主镜像全是 prebuilt 来源自带的成本;上游 zip(GCC 15.3 编)无论如何不可用 | v2 "自建 7 个多库"(review 追问后改判) | +| D8 | **板 = B-L475E-IOT01A + Pico;QEMU 行 = `b-l475e-iot01a` + `microbit`** | 每条轴都不同(核/浮点/探针/启动/核数),且板 A 同时在 QEMU 与真机 | "mps2-an385 开路" | +| D9 | **E 轴按依赖拆**:零工程项进 P0',工程项留 P2 | 支持窗口/许可清单/`sbom` 没有工程前置 | "E 整体在 P2" | +| D10 | **D 轴升级严重度**:lock 先成钉,再有 `--locked` | lock 头注释原文 "does not yet pin future builds" | "缺的是 `--locked` flag" | +| D11 | **freestanding 加 `-ffunction-sections -fdata-sections` + `--gc-sections`**,作 D7 的前提 | 依赖目标文件无条件进链接(`docs/13:423`)+ 全仓无 gc-sections(实测)⇒ 图来源 C 库会撑爆 MCU flash | v1/v2 都没有 —— 图来源在 MCU 上暴露的第一条引擎缺口 | +| D12 | 自托管 runner **归板级包仓**,mcpp 仓只留模拟器行 | 判据跟着包走;`openkal-llvm-runtime` 自跑 riscv64 qemu 的先例 | (v2 §9 问题 1,维护者已定) | +| D13 | **不做 `.uf2`**;`pico-rt` 的 `flash` 走 `probe-rs download` | RP2040 一家的格式 | (v2 §9 问题 2,维护者已定) | + +## 9. 待决项 + +无。v2 §9 的两个问题已定为 D12、D13;D7 经追问改判,前提为 D11。 + +--- + +## 10. 实施状态(2026-09-04,PR #550) + +### 已落地:P0 的引擎切片 + +| 条目 | 状态 | +|---|---| +| Cortex-M 目标行 | ✅ **七行**,`kKnownTargets` 与 `freestanding::kTable` 各七行 | +| D11 死代码段消除 | ✅ `-ffunction-sections -fdata-sections` + `--gc-sections`(驱动路径与直接 lld 路径) | +| `docs/13` 回填两条失效限制 | ✅ 中英双份 | +| 判据 | ✅ e2e 332(四行在 QEMU 中启动)+ 三条单测;CI `bare-metal e2e` 作业已绿 | + +### ⚠️ 实施推翻的两处方案原文 + +**① `extra` 列不承载浮点 ABI —— triple 已经承载了它。** +方案 §3.1 写「`-mfloat-abi`/`-mfpu` 走已存在的 `extra` 列」。实测 llvm 22.1.8: +clang 从 `eabi`/`eabihf` 后缀**自行导出** `-mfloat-abi`,无需引擎发任何标志。 + +⭐ **但同一次实测挖出方案没有预见的一条**:浮点 ABI 只约束浮点值如何跨越函数边界, +**不约束函数内部发什么指令**。`thumbv7em` 架构蕴含 FPv4-SP,于是软浮点 ABI 下 +clang 对一次 float 乘法仍发出 `vmul.f32` —— 在没有 FPU 的 Cortex-M4 上于运行期 +触发异常,而编译与链接都是干净的。`extra` 列因此确实被用上了,承载的是 `-mfpu=none`, +不是方案写的那两个标志。 + +**② 七行不是六行。** 方案 §3.1 列了六行(三个 arch × eabi/eabihf 的子集);实际是 +七行 —— `thumbv8m.base` 没有 `eabihf` 变体(M23 无 FPU),而 `thumbv7em` 与 +`thumbv8m.main` 各有两个。 + +### ⚠️ 实施中发现的、方案没有的一条纪律 + +`-mfpu=none` 第一版只加在**实测到非零 FPU 指令数**的行上。那让表记录的是一次 +**测量**而不是一条**保证**,并且各行的差异没有任何读者能看出来。对所有行量化的 +单元测试报出了它。⇒ **一行陈述它保证的性质,而不是从一个可以改变的默认值继承它。** + +### 未实施(按方案原顺序) + +`P0` 的 `mcpplibs/picolibc` + `compiler-rt-builtins` 源码包(§3.1.1)· 板级包三个 · +`P0'` 的 `xim:probe-rs` 与 E 轴零工程项 · `P1`/`P1'` 真机 · `P1'` 槽表 · +`P2` lock 成钉与 SBOM · `P3` openarch 第四后端 · `P3'` 嵌入式 Linux。 + +⚠️ **软浮点行在没有 builtins 时链接不了浮点代码**(实测:`undefined symbol: +__aeabi_fmul`),这正是 §3.1.1 把 `compiler-rt-builtins` 与 C 库并列为 P0 的理由。 +整数程序不受影响 —— e2e 332 的四行启动用例即为整数程序。 diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml index c60fafc3..80604a21 100644 --- a/.github/workflows/ci-linux-e2e.yml +++ b/.github/workflows/ci-linux-e2e.yml @@ -114,7 +114,7 @@ jobs: # test, and assert its PASS line appeared. A skip fails this job. # ────────────────────────────────────────────────────────────────── baremetal: - name: bare-metal e2e (riscv64-none-elf, qemu) + name: bare-metal e2e (riscv64-none-elf + cortex-m, qemu) runs-on: ubuntu-24.04 timeout-minutes: 40 env: @@ -153,6 +153,18 @@ jobs: XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \ "$XLINGS_BIN" install xim:picolibc-riscv -y test -d "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-picolibc-riscv" + # ⚠️ The M-profile emulator, in BOTH homes for the reason above. + # `xim:qemu-arm` carries `qemu-system-arm` as well as + # `qemu-system-aarch64`; test 332 addresses it by absolute path out of + # the payload, so what matters is that the payload EXISTS in the home + # mcpp uses rather than that a shim resolves. + "$XLINGS_BIN" install xim:qemu-arm -y + XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \ + "$XLINGS_BIN" install xim:qemu-arm -y + # Reachable AND runnable before the tests, so that a missing emulator + # fails this step rather than silently skipping test 332. + ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm \ + | sort -V | tail -1 | xargs -I{} {} --version | head -1 - name: Bare-metal e2e timeout-minutes: 25 @@ -175,7 +187,8 @@ jobs: for t in tests/e2e/130_freestanding_riscv_build_and_run.sh \ tests/e2e/131_freestanding_bsp_supplies_everything.sh \ tests/e2e/132_freestanding_test_and_artifacts.sh \ - tests/e2e/133_freestanding_std_subset.sh; do + tests/e2e/133_freestanding_std_subset.sh \ + tests/e2e/332_cortex_m_builds_and_boots.sh; do echo "=== $t ===" bash "$t" 2>&1 | tee "$(basename "$t").log" rc=${PIPESTATUS[0]} @@ -196,6 +209,18 @@ jobs: grep -q 'PASS: the freestanding std subset' \ 133_freestanding_std_subset.sh.log || { echo "133 (std subset) skipped on the runner that must run it"; exit 1; } + # ⚠️ 332 declares `# requires: qemu-arm`, which no sharded runner has + # — so on the shards it exits 0 without running. This job is the only + # place its PASS line can be demanded. + grep -q 'PASS: cortex-m rows build, boot' \ + 332_cortex_m_builds_and_boots.sh.log || { + echo "332 (cortex-m) skipped on the runner that must run it"; exit 1; } + # ⭐ And a count, because four `grep -q` calls that each matched say + # nothing about how many rows the script actually booted: a fixture + # that stopped iterating would still print its PASS line. + booted=$(grep -c 'booted on ' 332_cortex_m_builds_and_boots.sh.log || true) + [ "$booted" = "4" ] || { + echo "332 booted $booted rows, expected 4"; exit 1; } # ────────────────────────────────────────────────────────────────── # Hermetic (no host toolchain): the ONLY environment class that diff --git a/CHANGELOG.md b/CHANGELOG.md index cf6ff563..d242dd51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,36 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [2026.9.4.1] — 2026-09-04 + +Cortex-M 落地为七个目标行,freestanding 链接开启死代码段消除。 + +裸机目标表从四行增至十一行。M-profile 是七行而不是一行:为 `thumbv7em` 构建的 +目标文件使用 Cortex-M0 没有的指令,两种拼写产出互不兼容的目标文件,而表存在的 +理由正是让 `--target ` 单独足以产出正确的目标文件。 + +```toml +[build] +target = "thumbv7em-none-eabihf" +``` + +⚠️ **浮点 ABI 不决定 FPU 是否被使用。** `eabi`/`eabihf` 由 clang 从 triple 读出, +它约束浮点值如何跨越函数边界,不约束函数内部发什么指令 —— 而 `thumbv7em` 架构 +蕴含 FPv4-SP。实测:软浮点 ABI 下 clang 对一次 float 乘法仍发出 `vmul.f32`,在 +没有 FPU 的 Cortex-M4 上于运行期触发异常,而编译与链接都是干净的。每个软浮点行 +因此携带 `-mfpu=none`,包括架构本来就没有 FPU 的那几行 —— 一行陈述它保证的性质, +而不是从一个可以改变的默认值继承它。 + +freestanding 编译加 `-ffunction-sections -fdata-sections`、链接加 `--gc-sections`。 +依赖的目标文件无条件进入链接(不像归档成员那样按未定义符号拉取),当 C 库改由 +依赖图提供时,没有死代码段消除的镜像会装进整份 C 库,而 Cortex-M 器件只有几十 KB。 + +⚠️ **链接脚本因此以新的方式承重**:中断向量表不被任何东西引用,`--gc-sections` +会回收它,板级脚本必须写 `KEEP(*(.vectors))`。 + +同时回填了 `docs/13` 中两条已被 2026.8.28.2 推翻的限制:当图中有包提供 +`hosted-standard-library` 时,裸机目标上的异常、RTTI 与 `import std` 均可用。 + ## [2026.9.3.2] — 2026-09-03 `[xlings.workspace]` 的**推荐书写形态**定为命名空间在键上,官方包全部使用它; diff --git a/README.md b/README.md index 2c660390..01335697 100644 --- a/README.md +++ b/README.md @@ -382,6 +382,11 @@ the right toolchain payload is resolved and installed automatically. | `aarch64-macos` | llvm *(macOS default)* | ✅ | | `riscv64-none-elf` | llvm 22 — bare metal, no OS; needs no per-host cross payload ² | ✅ | | `riscv32-none-elf` | llvm 22 — bare metal, no OS; needs no per-host cross payload ² | ✅ | +| `thumbv6m-none-eabi` | llvm 22 — Cortex-M0/M0+/M1, bare metal ² | ✅ | +| `thumbv7m-none-eabi` | llvm 22 — Cortex-M3, bare metal ² | ✅ | +| `thumbv7em-none-eabihf` | llvm 22 — Cortex-M4F/M7F, hard float ² | ✅ | +| `thumbv8m.main-none-eabi` | llvm 22 — Cortex-M33/M55, soft float ² | ✅ | +| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 22 — builds and links; no emulator run recorded | 🔄 | | `riscv64-linux-musl` | — | 🔄 | | `aarch64-linux-gnu` | — | 🔄 | | `x86_64-macos` | — | 🔄 | diff --git a/docs/13-baremetal.md b/docs/13-baremetal.md index cdb752c2..4c44ae5c 100644 --- a/docs/13-baremetal.md +++ b/docs/13-baremetal.md @@ -13,7 +13,7 @@ covers the hosted link model this chapter departs from. ## Overview A freestanding target is a target whose `os` field is `none`. The target table -at `modules/toolchain-model/src/triple.cppm` carries four of them: +at `modules/toolchain-model/src/triple.cppm` carries eleven of them: | Triple | Tier | C library | |---|---|---| @@ -21,10 +21,63 @@ at `modules/toolchain-model/src/triple.cppm` carries four of them: | `riscv32-none-elf` | verified | `xim:picolibc-riscv` | | `aarch64-none-elf` | preview | none by default — the zero-libc tier; `xim:picolibc-aarch64` is declarable | | `x86_64-none-elf` | preview | none by default — the zero-libc tier; `xim:picolibc-x86` is declarable | +| `thumbv6m-none-eabi` | verified | none by default — Cortex-M0/M0+/M1 | +| `thumbv7m-none-eabi` | verified | none by default — Cortex-M3 | +| `thumbv7em-none-eabi` | preview | none by default — Cortex-M4/M7, soft float | +| `thumbv7em-none-eabihf` | verified | none by default — Cortex-M4F/M7F, hard float | +| `thumbv8m.base-none-eabi` | preview | none by default — Cortex-M23 | +| `thumbv8m.main-none-eabi` | verified | none by default — Cortex-M33/M55, soft float | +| `thumbv8m.main-none-eabihf` | preview | none by default — Cortex-M33F/M55F, hard float | `verified` means an image has been built **and run** for the row. `preview` -means it builds and has been observed to run, but is not yet covered by the -engine's own emulator jobs. +means it builds and links, and no emulator run has been recorded. + +### M-profile is seven rows rather than one + +Every other bare-metal family above is one row per architecture. Cortex-M is +not. An object built for `thumbv7em` uses instructions a Cortex-M0 does not +have, and the two spellings produce incompatible objects rather than expressing +a preference. The table exists so that `--target ` alone suffices to +produce a correct object file; a single `arm-none-eabi` row plus an `-mcpu` that +each project remembered would move a correctness decision out of the table and +into every manifest. + +The `eabi`/`eabihf` suffix is the float ABI, and clang derives it from the +triple without help: measured on llvm 22.1.8, `thumbv7em-none-eabi` yields +`-mfloat-abi soft` and `thumbv7em-none-eabihf` yields `hard`. + +⚠️ **The float ABI does not settle whether the FPU is used.** It governs how +floating-point values cross a function boundary, not what the compiler may emit +inside one, and the `thumbv7em` architecture implies FPv4-SP. Measured: under +the soft-float ABI clang still emits `vmul.f32` for a float multiply. On a +Cortex-M4 without an FPU that instruction faults at run time, after a clean +compile and a clean link. Every soft-float row therefore carries `-mfpu=none`, +including the rows describing architectures that have no FPU at all — a row +states the property it guarantees rather than inheriting it from a default. + +Cortex-M needs no `lldEmulation` column entry: clang has a *BareMetal* toolchain +for arm, so these triples reach `ld.lld` through the driver as the RISC-V and +aarch64 rows do. 32-bit ARM has no `-mcmodel` axis, so that column is empty too. + +### Dead-section elimination + +Freestanding builds compile with `-ffunction-sections -fdata-sections` and link +with `--gc-sections`. Both halves belong to the engine rather than to a project +because a dependency's translation units must carry them, and a project cannot +reach those. + +The flags became necessary rather than merely economical when a C library began +arriving from the dependency graph. A dependency's object files enter the link +unconditionally, unlike an archive member, which is pulled only while its symbol +is undefined. That costs nothing when the C library is a prebuilt archive and +the target has megabytes; a Cortex-M part has kilobytes, and without dead-section +elimination every image would carry the whole of the C library. + +⚠️ **A linker script becomes load-bearing in a new way.** An interrupt vector +table is referenced by nothing — the hardware reads it by address — so +`--gc-sections` collects it. A board's script must say `KEEP(*(.vectors))`. +Measured: with the `KEEP` present, a function nothing calls is dropped, the +table survives, and the image boots. ⚠️ The last two rows default to no C library, and that is a statement rather than an omission: the first consumer of both rows — the `openarch` layer of @@ -190,8 +243,8 @@ by pointing `main` at the source file that carries `_start`. | Linker selection | `ld.lld` is addressed by **absolute path**, derived from the driver's own directory. `-fuse-ld=lld` resolves by name and finds GNU ld on any machine with binutils earlier on `PATH`, which then fails with `unrecognised emulation mode: elf64lriscv`. | | ISA flags | `-march`, `-mabi` and `-mcmodel` come from one row per target in `src/freestanding/target.cppm`, so `--target ` alone is sufficient to produce a correct object file. | | C library | The **target's**, resolved by mcpp from the target's own table row exactly as the compiler is. A bare-metal project declares no libc, just as a hosted project declares no glibc. The engine places the sysroot's library directory on the link search path, so a board-support package selects out of it by bare name (`-lc`, `-lcrt0-semihost`). | -| Exceptions and RTTI | Off on every translation unit in the graph, including a dependency's. There is no unwinder and no `libc++abi`, so nothing can throw; `std::optional::value()` alone would otherwise reference `__cxa_throw` and three further undefined symbols. The setting belongs to the target rather than to a project's `cxxflags` because a BMI records it, and a dependency compiled with exceptions cannot be imported by a unit without them. | -| `import std` | Unavailable, and rejected at configure time with a diagnostic rather than at link time. | +| Exceptions and RTTI | Off on every translation unit in the graph, including a dependency's, **unless a package supplies a C++ runtime built for this target**. There is otherwise no unwinder and no `libc++abi`, so nothing can throw; `std::optional::value()` alone would reference `__cxa_throw` and three further undefined symbols. The setting belongs to the target rather than to a project's `cxxflags` because a BMI records it, and a dependency compiled with exceptions cannot be imported by a unit without them. A package declaring `provides = ["hosted-standard-library"]` reverses the default: exceptions and RTTI are enabled, `-ffreestanding` is dropped, and `-fasynchronous-unwind-tables` is added. | +| `import std` | Available when a package in the graph provides `hosted-standard-library` and names its own `std` module source; otherwise rejected at configure time with a diagnostic rather than at link time. | | Entry point | `int main()` is available whenever something supplies a `crt0`. A board-support package normally does. | | Default linkage | Static, and not as a preference: there is no loader, so there is no other option. | @@ -716,7 +769,7 @@ targets, but that expectation is **not** covered by a test. | Limitation | Observed behaviour | |---|---| | `std::format`, `std::sort` over builtin scalar types, and a complete `std::string` | Fail at **link** time naming the undefined symbol. libc++ places these entities in the compiled library — the scalar `__sort` instantiations are `extern template`, with no macro that disables them — so a target-built `libc++.a` is required. No such payload is published. | -| Exceptions and RTTI | Disabled across the whole graph. `try`/`catch` is unavailable at compile time. A board shipping a target-built `libc++abi` and unwinder has a genuine case for re-enabling them; that is the point at which this becomes a manifest key. | +| Exceptions and RTTI | Disabled across the whole graph **unless a package provides `hosted-standard-library`**, which `mcpplibs/openkal-llvm-runtime` does by carrying `libc++`, `libc++abi` and `libunwind` configured for the target. Without such a package `try`/`catch` remains unavailable at compile time. | | Board coverage | One board family. `riscv32-none-elf` demonstrates that the ISA table is data, not that a second machine has been ported. ARM Cortex-M has not been attempted. | | C library substitution | Expressible since 2026.8.20.2 through `[target.].sysroot`, and **verified only for the empty value** (the zero-libc tier). Pointing it at a different C library is accepted and installed through the same channel, but no second bare-metal C library is published, so that path is untested. | | `qemu-riscv` on `win32-arm64` | The upstream package publishes no asset for that host, so installation fails on it. The failure is correct rather than silent, but the host cannot run a bare-metal image. | diff --git a/docs/16-the-target-triple.md b/docs/16-the-target-triple.md index bd884e1e..a5137790 100644 --- a/docs/16-the-target-triple.md +++ b/docs/16-the-target-triple.md @@ -428,6 +428,13 @@ other's rows. | `riscv32-none-elf` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | | `aarch64-none-elf` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | | `x86_64-none-elf` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv6m-none-eabi` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv7m-none-eabi` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv7em-none-eabi` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv7em-none-eabihf` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv8m.base-none-eabi` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv8m.main-none-eabi` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv8m.main-none-eabihf` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | `✅ payload` a toolchain payload here produces it · `⚙ graph` no payload, but a dependency can supply the system · `✅ system` located on the machine, not diff --git a/docs/zh/13-baremetal.md b/docs/zh/13-baremetal.md index 214084ac..ee57def5 100644 --- a/docs/zh/13-baremetal.md +++ b/docs/zh/13-baremetal.md @@ -11,7 +11,7 @@ ## 概述 freestanding 目标是 `os` 字段为 `none` 的目标。`modules/toolchain-model/src/triple.cppm` -的目标表中有四个: +的目标表中有十一个: | Triple | 档位 | C 库 | |---|---|---| @@ -19,9 +19,53 @@ freestanding 目标是 `os` 字段为 `none` 的目标。`modules/toolchain-mode | `riscv32-none-elf` | verified | `xim:picolibc-riscv` | | `aarch64-none-elf` | preview | 默认无 —— 零 libc 层;`xim:picolibc-aarch64` 可声明 | | `x86_64-none-elf` | preview | 默认无 —— 零 libc 层;`xim:picolibc-x86` 可声明 | - -`verified` 意味着该行的镜像被构建**并被运行**过。`preview` 意味着它构建得出、 -也被观察到能运行,但尚未纳入引擎自己的模拟器作业。 +| `thumbv6m-none-eabi` | verified | 默认无 —— Cortex-M0/M0+/M1 | +| `thumbv7m-none-eabi` | verified | 默认无 —— Cortex-M3 | +| `thumbv7em-none-eabi` | preview | 默认无 —— Cortex-M4/M7,软浮点 | +| `thumbv7em-none-eabihf` | verified | 默认无 —— Cortex-M4F/M7F,硬浮点 | +| `thumbv8m.base-none-eabi` | preview | 默认无 —— Cortex-M23 | +| `thumbv8m.main-none-eabi` | verified | 默认无 —— Cortex-M33/M55,软浮点 | +| `thumbv8m.main-none-eabihf` | preview | 默认无 —— Cortex-M33F/M55F,硬浮点 | + +`verified` 意味着该行的镜像被构建**并被运行**过。`preview` 意味着它构建并链接得出, +尚无模拟器运行记录。 + +### M-profile 是七行而不是一行 + +上面每一个裸机族都是一个架构一行。Cortex-M 不是:为 `thumbv7em` 构建的目标文件 +使用 Cortex-M0 没有的指令,两种拼写产出的是互不兼容的目标文件,而不是一种偏好。 +这张表存在,是为了让 `--target ` 单独就足以产出正确的目标文件;若只写一行 +`arm-none-eabi` 再让每个工程各自记住一个 `-mcpu`,就把一个正确性决定从表里搬进了 +每一份清单。 + +`eabi`/`eabihf` 后缀即浮点 ABI,clang 直接从 triple 读出它:实测 llvm 22.1.8, +`thumbv7em-none-eabi` 得到 `-mfloat-abi soft`,`thumbv7em-none-eabihf` 得到 `hard`。 + +⚠️ **浮点 ABI 并不决定 FPU 是否被使用。** 它约束浮点值如何跨越函数边界,不约束 +编译器在函数内部可以发什么指令,而 `thumbv7em` 架构蕴含 FPv4-SP。实测:在软浮点 +ABI 下,clang 对一次 float 乘法仍然发出 `vmul.f32`。在没有 FPU 的 Cortex-M4 上, +这条指令在运行期触发异常 —— 而编译与链接都是干净的。因此每一个软浮点行都携带 +`-mfpu=none`,**包括那些架构本来就没有 FPU 的行**:一行应当陈述它所保证的性质, +而不是从一个随时可以改变的默认值继承它。 + +Cortex-M 不需要 `lldEmulation` 列:clang 有面向 arm 的 *BareMetal* 工具链,这些 +triple 与 RISC-V、aarch64 各行一样经驱动到达 `ld.lld`。32 位 ARM 没有 `-mcmodel` +这个轴,所以该列同样为空。 + +### 死代码段消除 + +freestanding 构建以 `-ffunction-sections -fdata-sections` 编译,以 `--gc-sections` +链接。两半都属于引擎而不属于工程,因为依赖的翻译单元也必须带上它们,而工程够不到 +那些单元。 + +这两个标志从「划算」变成「必需」,发生在 C 库开始由依赖图提供的时候。依赖的目标 +文件是**无条件**进入链接的,不像归档成员那样只在符号仍未定义时才被拉入。当 C 库是 +预编译归档、目标又有若干兆字节时,这不花什么代价;而 Cortex-M 器件只有几十 KB, +没有死代码段消除,每个镜像都会装进整份 C 库。 + +⚠️ **链接脚本因此以一种新的方式承重。** 中断向量表不被任何东西引用 —— 硬件按地址 +读取它 —— 所以 `--gc-sections` 会把它回收。板级脚本必须写 `KEEP(*(.vectors))`。 +实测:有这条 `KEEP` 时,无人调用的函数被丢弃、向量表被保留、镜像能够启动。 ⚠️ 后两行**默认**没有 C 库,这是声明而非遗漏:这两行的第一个消费者 —— 机器机制层 `openarch` —— 一个 C 库符号都不引用,而**如果四行里没有一行默认在这一层,就没有 @@ -167,8 +211,8 @@ extern "C" int main() { | 链接器选择 | `ld.lld` 按**绝对路径**寻址,由驱动自身所在目录推导。`-fuse-ld=lld` 按名字解析,在任何 binutils 位于 `PATH` 更靠前位置的机器上都会找到 GNU ld,随后以 `unrecognised emulation mode: elf64lriscv` 失败。 | | ISA 参数 | `-march`、`-mabi` 与 `-mcmodel` 来自 `src/freestanding/target.cppm` 中每个目标一行的表,因此仅凭 `--target ` 就足以产出正确的目标文件。 | | C 库 | 属于**目标**,由 mcpp 从目标自己的表行解析,与解析编译器的方式完全一致。裸机工程不声明 libc,正如宿主工程不声明 glibc。引擎把 sysroot 的库目录放入链接搜索路径,板级支持包因此可以按裸名从中选取(`-lc`、`-lcrt0-semihost`)。 | -| 异常与 RTTI | 在图中每个翻译单元上都关闭,包括依赖的翻译单元。没有 unwinder 也没有 `libc++abi`,因此任何东西都无法抛出;仅 `std::optional::value()` 一处就会引用 `__cxa_throw` 以及另外三个未定义符号。该设定属于目标而非工程的 `cxxflags`,因为 BMI 会记录它,而带异常编出的依赖无法被不带异常的单元导入。 | -| `import std` | 不可用,并且在配置期以诊断拒绝,而不是在链接期失败。 | +| 异常与 RTTI | 在图中每个翻译单元上都关闭,包括依赖的翻译单元,**除非有包提供为该目标构建的 C++ 运行时**。否则既没有 unwinder 也没有 `libc++abi`,任何东西都无法抛出;仅 `std::optional::value()` 一处就会引用 `__cxa_throw` 以及另外三个未定义符号。该设定属于目标而非工程的 `cxxflags`,因为 BMI 会记录它,而带异常编出的依赖无法被不带异常的单元导入。声明 `provides = ["hosted-standard-library"]` 的包会翻转这个默认:异常与 RTTI 开启,`-ffreestanding` 被去掉,并加上 `-fasynchronous-unwind-tables`。 | +| `import std` | 当图中有包提供 `hosted-standard-library` 并指明自己的 `std` 模块源码时可用;否则在配置期以诊断拒绝,而不是在链接期失败。 | | 入口点 | 只要有东西提供 `crt0`,`int main()` 就可用。板级支持包通常提供它。 | | 默认链接方式 | 静态,而且这不是偏好:没有加载器,因此没有别的选项。 | @@ -643,7 +687,7 @@ int main() { | 边界 | 观察到的行为 | |---|---| | `std::format`、内建标量类型上的 `std::sort`、以及完整的 `std::string` | 在**链接**期失败并点名未定义符号。libc++ 把这些实体放在编译版库中 —— 标量 `__sort` 的实例化是 `extern template`,没有可用于关闭它们的宏 —— 因此需要为目标编出的 `libc++.a`。该载荷尚未发布。 | -| 异常与 RTTI | 在整张图上关闭。`try`/`catch` 在编译期即不可用。一块随包提供目标版 `libc++abi` 与 unwinder 的板子有重新开启它们的正当理由;那也正是这一项应当成为一个清单键的时刻。 | +| 异常与 RTTI | 在整张图上关闭,**除非有包提供 `hosted-standard-library`** —— `mcpplibs/openkal-llvm-runtime` 就通过携带为该目标配置过的 `libc++`、`libc++abi` 与 `libunwind` 做到了这一点。没有这样的包时,`try`/`catch` 在编译期仍不可用。 | | 板子覆盖面 | 只有一个板级家族。`riscv32-none-elf` 证明的是 ISA 表为数据,而不是已移植第二台机器。ARM Cortex-M 尚未尝试。 | | 替换 C 库 | 自 2026.8.20.2 起可经 `[target.].sysroot` 表达,而**仅空值一侧经过验证**(零 libc 档)。指向另一份 C 库同样被接受并经同一通道安装,但生态中没有第二份裸机 C 库,该路径未经测试。 | | `win32-arm64` 上的 `qemu-riscv` | 上游包未为该宿主发布资产,因此在其上安装会失败。该失败是正确的而非静默的,但该宿主无法运行裸机镜像。 | diff --git a/mcpp.toml b/mcpp.toml index c38496d9..5427c6bd 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.3.2" +version = "2026.9.4.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index db1eb3e8..f0be2b56 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -351,6 +351,49 @@ inline constexpr TargetInfo kKnownTargets[] = { // above `aarch64-none-elf`: the first consumer is `openarch`, which // references no C library symbol. { "x86_64-none-elf", "preview", "bare","llvm@22.1.8","", true }, + // ── Cortex-M ──────────────────────────────────────────────────────────── + // + // ⚠️ SEVEN ROWS AND NOT ONE, BECAUSE "Cortex-M" IS NOT AN INSTRUCTION SET. + // + // Every other bare-metal family here is one row per architecture. M-profile + // is not: an object built for `thumbv7em` uses instructions a Cortex-M0 + // does not have, and one built for `thumbv6m` runs on both but leaves the + // larger part unused. The two spellings are not a preference a board + // expresses — they produce incompatible objects — so they are rows. + // + // The rule this table states about itself governs: it exists so that + // `--target ` ALONE is enough to produce a correct object file. A + // single `arm-none-eabi` row plus an `-mcpu` the project remembers would + // move a correctness decision out of the table and into every manifest. + // + // ⭐ THE `eabi`/`eabihf` SUFFIX IS THE FLOAT ABI, AND CLANG ALREADY READS + // IT. Measured on llvm 22.1.8 (`-###`, `-cc1` line): `thumbv7em-none-eabi` + // gives `-mfloat-abi soft` and `-none-eabihf` gives `hard`, with no flag + // from us. So the ABI needs no entry in the ISA table's `extra` column — + // only the FPU does, and only on the soft rows. See `kThumbSoftExtra`. + // + // ⚠️ `sysroot` IS EMPTY ON EVERY ROW, AND THAT IS THE POINT RATHER THAN A + // GAP. The three older bare-metal families name an `xim:` payload here; a C + // library for these targets arrives from the DEPENDENCY GRAPH instead + // (`mcpp:c-abi=picolibc`, docs/14). A prebuilt payload would have to ship + // one multilib per row — seven here — and the `libdir` column would have to + // match its layout byte for byte, which is the defect #481 fixed. A source + // package is compiled with the consuming target's own flags, so the ABI + // agreement holds by construction and there is no multilib at all. + // + // ⚠️ THE TIER COLUMN RECORDS WHAT WAS RUN, NOT WHAT WAS REASONED. Measured + // 2026-09-04 under `xim:qemu-arm@9.2.4-1`: each `verified` row below built + // an image that BOOTED on the named machine and printed over semihosting — + // thumbv6m on `microbit`, thumbv7m on `mps2-an385`, thumbv7em-eabihf on + // `mps2-an386`, thumbv8m.main-eabi on `mps2-an505`. The three `preview` + // rows build and link; no emulator run has been recorded for them. + { "thumbv6m-none-eabi", "verified", "bare","llvm@22.1.8","", true }, + { "thumbv7m-none-eabi", "verified", "bare","llvm@22.1.8","", true }, + { "thumbv7em-none-eabi", "preview", "bare","llvm@22.1.8","", true }, + { "thumbv7em-none-eabihf", "verified", "bare","llvm@22.1.8","", true }, + { "thumbv8m.base-none-eabi","preview", "bare","llvm@22.1.8","", true }, + { "thumbv8m.main-none-eabi","verified", "bare","llvm@22.1.8","", true }, + { "thumbv8m.main-none-eabihf","preview","bare","llvm@22.1.8","", true }, }; inline std::span known_targets() { return kKnownTargets; } diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index de82c29a..255fcdb0 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.9.3.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.4.1"; } // namespace mcpp diff --git a/src/freestanding/linkline.cppm b/src/freestanding/linkline.cppm index 913a9c09..d0c873c8 100644 --- a/src/freestanding/linkline.cppm +++ b/src/freestanding/linkline.cppm @@ -89,6 +89,9 @@ inline std::string link_flags(const Spec& s, const LinkInputs& in, // suggesting it does. for (auto const& f : compile_flags(s)) { out += ' '; out += f; } out += " -nostdlib -nostartfiles -static"; + // The link half of the pair set in mcpp.freestanding.target's + // `compile_flags`. Routed through the driver, hence `-Wl,`. + out += " -Wl,--gc-sections"; if (!in.lld.empty()) out += " -fuse-ld=" + esc(in.lld); // ⚠️ BEFORE the libraries the board selects, and it has to be on THIS line @@ -129,6 +132,11 @@ inline std::string link_flags_direct(const Spec& s, const LinkInputs& in, // No loader exists, so an image must not name one. The driver line reaches // this through `-static`; here it is said to the linker. out += " --no-dynamic-linker"; + // Said to the linker directly here, as `-Wl,--gc-sections` is said through + // the driver on the line above. The two paths must agree: a target whose + // link is driven by `ld.lld` itself would otherwise keep every section its + // dependencies emit, and the difference would show only as size. + out += " --gc-sections"; if (!in.sysrootLib.empty()) out += " -L" + esc(in.sysrootLib); if (!in.linkerScript.empty()) diff --git a/src/freestanding/target.cppm b/src/freestanding/target.cppm index 88871473..b545194e 100644 --- a/src/freestanding/target.cppm +++ b/src/freestanding/target.cppm @@ -108,6 +108,46 @@ inline constexpr std::string_view kX86_64NoneExtra[] = { "-mno-red-zone", }; +// ⚠️ THE SOFT-FLOAT M-PROFILE ROWS, AND WHY THE FLOAT ABI IS NOT ENOUGH. +// +// `thumbv7em-none-eabi` and `-eabihf` differ in their float ABI, and clang +// derives that from the triple with no help from us (measured, `-###`: +// `-mfloat-abi soft` and `hard` respectively). It would be reasonable to stop +// there. Measured on llvm 22.1.8, that is wrong: +// +// clang --target=thumbv7em-none-eabi -O2 -S (float a*b+1.0f) +// vmul.f32 ← an FPU instruction +// +// The float ABI governs how floating-point values cross a function boundary. +// It does not govern whether the compiler may USE the FPU inside one, and the +// `thumbv7em` architecture implies FPv4-SP, so clang emits FPU instructions +// under either ABI. A plain Cortex-M4 — the part this row exists to serve, +// since the -eabihf row serves M4F — has no FPU, and the instruction faults. +// +// ⚠️ The failure is a hard fault at run time on real silicon, with a clean +// compile and a clean link. It is the shape this table exists to prevent. +// +// `-mfpu=none` restores the property the row's name claims. Measured: the +// instruction count returns to zero, and the `-eabihf` rows must NOT receive +// this flag — there it would discard the FPU the row exists to use. +// +// ⚠️ EVERY SOFT ROW CARRIES IT, INCLUDING THE ONES WHERE NOTHING WAS MEASURED. +// +// Only `thumbv7em-none-eabi` was observed emitting an FPU instruction; +// `thumbv6m`, `thumbv7m` and `thumbv8m.base` describe architectures with no FPU +// at all, and `thumbv8m.main-none-eabi` emits none today because its default +// CPU is `generic`. Applying the flag only where a count was non-zero would +// make the table record a MEASUREMENT rather than a GUARANTEE, and the rows +// would then differ for a reason no reader could see. +// +// A row states the property it promises. `-mfpu=none` is accepted on all four +// (measured), costs nothing where there is no FPU, and is what a unit test can +// quantify over — which is the only place this rule can be stated, since a +// build of any one row cannot speak for the others. +inline constexpr std::string_view kThumbSoftExtra[] = { + "-mfpu=none", +}; + // ⚠️ THE `libdir` COLUMN WAS EMPTY ON THE LAST TWO ROWS UNTIL 2026-08-21, AND // THAT WAS CORRECT UNTIL THE DAY IT WAS NOT. // @@ -184,6 +224,31 @@ inline constexpr Spec kTable[] = { // resolves no C library, so a value here could never be checked. { "x86_64-none-elf", "x86-64", "sysv", "small", "x86-64/sysv", kX86_64NoneExtra, "elf_x86_64" }, + // ── Cortex-M ──────────────────────────────────────────────────────────── + // + // `mcmodel` is EMPTY on every row: 32-bit ARM has no `-mcmodel` axis, and + // `compile_flags` already omits the flag when this column is empty. + // + // `lldEmulation` is EMPTY on every row, and that was measured rather than + // assumed. clang has a BareMetal toolchain for arm, so these triples reach + // `ld.lld` through the driver exactly as the riscv and aarch64 rows do — + // the x86_64 row's problem does not recur here. Verified end to end: a + // freestanding thumb object links under `ld.lld` and boots under QEMU. + // + // `libdir` is EMPTY on every row because `sysroot` is (see the target + // table): the column is read only when a sysroot has been resolved, and a + // value here could never be checked. + // + // ⚠️ `-mabi=aapcs`, matching the aarch64 row and for the same reason: on + // ARM `-mabi` names a procedure call standard, not a data model. + // triple march mabi mcmodel libdir extra + { "thumbv6m-none-eabi", "armv6-m", "aapcs", "", "", kThumbSoftExtra }, + { "thumbv7m-none-eabi", "armv7-m", "aapcs", "", "", kThumbSoftExtra }, + { "thumbv7em-none-eabi", "armv7e-m", "aapcs", "", "", kThumbSoftExtra }, + { "thumbv7em-none-eabihf", "armv7e-m", "aapcs", "", "" }, + { "thumbv8m.base-none-eabi", "armv8-m.base","aapcs","", "", kThumbSoftExtra }, + { "thumbv8m.main-none-eabi", "armv8-m.main","aapcs","", "", kThumbSoftExtra }, + { "thumbv8m.main-none-eabihf","armv8-m.main","aapcs","", "" }, }; // The single read point. Returns nullopt for anything that is not a known @@ -226,6 +291,30 @@ inline std::vector compile_flags(const Spec& s, // `-ffreestanding` so the ordering of this function's output stays a // function of the table rather than of the row. for (auto flag : s.extra) out.emplace_back(flag); + // ⭐⭐ ONE SECTION PER FUNCTION, SO THE LINKER CAN DROP WHAT NOTHING CALLS. + // + // These two flags do nothing on their own; they are the half of + // `--gc-sections` that has to happen at compile time, and the link half is + // in mcpp.freestanding.linkline. Both halves are here rather than left to a + // project because a dependency's translation units must carry them too, and + // a project cannot reach those. + // + // ⚠️ WHY THIS BECAME NECESSARY RATHER THAN MERELY NICE. A dependency's + // object files enter the link unconditionally (docs/13), unlike an archive + // member, which is pulled only while its symbol is undefined. That costs + // nothing when the C library is a prebuilt archive and the target has + // megabytes — the arrangement every bare-metal row had until now. A C + // library that arrives from the dependency graph is object files, and a + // Cortex-M part has kilobytes: without this, every image carries the whole + // of the C library whether or not it calls into it. + // + // ⚠️ AND IT MAKES A LINKER SCRIPT LOAD-BEARING IN A NEW WAY: an interrupt + // vector table is referenced by nothing — the hardware reads it by address + // — so `--gc-sections` collects it. A board's script must say + // `KEEP(*(.vectors))`. Measured: with the KEEP present, a dead function is + // dropped and the table survives; the image boots. + out.emplace_back("-ffunction-sections"); + out.emplace_back("-fdata-sections"); // ⭐ AND `-ffreestanding` ITSELF IS ONE OF THE THINGS THE GRAPH DECIDES. // // The paragraph above this function names what the flag changes: "no `main` diff --git a/tests/e2e/332_cortex_m_builds_and_boots.sh b/tests/e2e/332_cortex_m_builds_and_boots.sh new file mode 100755 index 00000000..6535d107 --- /dev/null +++ b/tests/e2e/332_cortex_m_builds_and_boots.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +# requires: llvm unix-shell qemu-arm +# Cortex-M: the M-profile rows build, boot, and collect what nothing calls. +# +# ⚠️ THE ROWS THAT MATTER HERE ARE THE ONES A REASONED TABLE WOULD HAVE GOT +# WRONG. Two properties are asserted that a build alone cannot show: +# +# * `--gc-sections` reaches a bare-metal link. Without it a C library that +# arrives from the dependency graph — object files, which enter a link +# unconditionally — puts its whole self into every image, and a Cortex-M +# part has kilobytes. The assertion is that a function nothing calls is +# ABSENT from the image while the interrupt vector table, which nothing +# references either, SURVIVES because the script says KEEP. +# +# * the soft-float rows do not emit FPU instructions. `thumbv7em`'s +# architecture implies an FPU, so clang emits `vmul.f32` for a float +# multiply under the soft-float ABI as readily as under the hard one; on a +# Cortex-M4 without an FPU that faults at run time, with a clean compile and +# a clean link. The row carries `-mfpu=none` for this, and the assertion is +# an instruction count. +# +# ⚠️ AND THE IMAGE IS RUN, NOT INSPECTED. A freestanding image that links is not +# evidence: the entry point and the ordering of the vector table are only +# exercised by a machine that fetches from address zero. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +qemu_arm() { + local d + for d in "${MCPP_HOME:-$HOME/.mcpp}/registry" "$HOME/.xlings"; do + local c + c=$(ls "$d"/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm 2>/dev/null | sort -V | tail -1) + [ -n "$c" ] && [ -x "$c" ] && { echo "$c"; return 0; } + done + command -v qemu-system-arm 2>/dev/null && return 0 + return 1 +} +QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 0; } + +llvm_tool() { + local c + c=$(ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-llvm/*/bin/"$1" 2>/dev/null | sort -V | tail -1) + [ -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; } + +mkdir -p "$work/mcu/src" +cd "$work/mcu" + +cat > mcpp.toml <<'TOML' +[package] +name = "mcu" +version = "0.1.0" +TOML + +# ⚠️ `volatile` on the operands. Without it the multiply is constant-folded and +# the FPU assertion below passes for a reason unrelated to the flag. +cat > src/main.cpp <<'CPP' +namespace { +inline void sh(int op, const void* a) { + register int r0 __asm__("r0") = op; + register const void* r1 __asm__("r1") = a; + __asm__ volatile("bkpt 0xAB" :: "r"(r0), "r"(r1) : "memory"); +} +} +extern "C" unsigned __stack_top; + +// Referenced by nothing. The image must not contain it. +extern "C" void collected_because_nothing_calls_it() { sh(0x04, (void*)"UNREACHABLE\n"); } + +extern "C" void Reset_Handler() { + sh(0x04, (void*)"cortex-m ok\n"); + sh(0x18, (void*)0x20026); // semihosting SYS_EXIT, ADP_Stopped_ApplicationExit + for (;;) {} +} + +// Referenced by nothing either — the hardware reads it by address. It survives +// only because the linker script says KEEP. +extern "C" __attribute__((section(".vectors"), used)) +void* const vectors[] = { (void*)&__stack_top, (void*)Reset_Handler }; +CPP + +cat > build.mcpp <<'BUILD' +import mcpp; +int main() { mcpp::link_script("link.ld"); return 0; } +BUILD + +# The memory map is a BOARD fact, not a target fact — which is why it is written +# here per machine rather than derived from the triple. +emit_ld() { + cat > link.ld < FLASH + .data : { *(.data*) } > RAM + .bss : { *(.bss*) *(COMMON) } > RAM + __stack_top = ORIGIN(RAM) + LENGTH(RAM); +} +EOF +} + +ran=0 +boot_row() { # triple machine cpuflag flash_org flash_len ram_org ram_len + local triple=$1 machine=$2 cpuflag=$3 + emit_ld "$4" "$5" "$6" "$7" + rm -rf target + "$MCPP" build --target "$triple" >/dev/null 2>&1 || { + echo "FAIL: $triple did not build"; exit 1; } + local elf + elf=$(find target -type f -name mcu | head -1) + [ -n "$elf" ] || { echo "FAIL: $triple produced no artefact"; exit 1; } + + # gc-sections: the dead function is gone, the vector table is not. + if [ "$("$NM" "$elf" | grep -c collected_because_nothing_calls_it)" != "0" ]; then + echo "FAIL: $triple kept a function nothing calls (--gc-sections not applied)"; exit 1 + fi + if [ "$("$NM" "$elf" | grep -c vectors)" != "1" ]; then + echo "FAIL: $triple lost the vector table (KEEP not honoured)"; exit 1 + fi + + local out + out=$(timeout 30 "$QEMU" -machine "$machine" $cpuflag -nographic -semihosting \ + -no-reboot -kernel "$elf" 2>&1 | head -3) + case "$out" in + *"cortex-m ok"*) ;; + *) echo "FAIL: $triple did not boot on $machine; got: $out"; exit 1 ;; + esac + echo " ok $triple booted on $machine" + ran=$((ran + 1)) +} + +# The four rows the target table marks `verified`, each on the machine that +# earned it. nRF51 has 16K of RAM; a generic map hard-faults before `main`. +boot_row thumbv6m-none-eabi microbit "" 0x00000000 256K 0x20000000 16K +boot_row thumbv7m-none-eabi mps2-an385 "-cpu cortex-m3" 0x00000000 4M 0x20000000 4M +boot_row thumbv7em-none-eabihf mps2-an386 "-cpu cortex-m4" 0x00000000 4M 0x20000000 4M +boot_row thumbv8m.main-none-eabi mps2-an505 "" 0x10000000 4M 0x38000000 512K + +# ⚠️ A COUNT, BECAUSE A LOOP THAT RAN ZERO TIMES ALSO REACHES THIS LINE. +[ "$ran" = "4" ] || { echo "FAIL: expected 4 rows to boot, got $ran"; exit 1; } + +# ── The float ABI, asserted on both sides of the pair ────────────────────── +# +# ⚠️ A SEPARATE PROJECT, AND THAT SEPARATION IS ITSELF A MEASUREMENT. Float +# arithmetic cannot live in the fixture above: on a soft-float row it lowers +# onto `__aeabi_fmul`, and this tier has no C library and no builtins to resolve +# it against, so every boot row would fail to link. The boot fixture is +# therefore integer-only and the float question is asked here. +# +# ⚠️ AND ONE SIDE ALONE PROVES NOTHING. "The soft row emitted no FPU +# instruction" is equally true of a build that emitted no float code, so the +# hard row is measured from the same source as the control. +mkdir -p "$work/fp/src" +cd "$work/fp" +cp "$work/mcu/build.mcpp" . +printf '[package]\nname = "fp"\nversion = "0.1.0"\n' > mcpp.toml +cat > src/main.cpp <<'CPP' +extern "C" unsigned __stack_top; +// `volatile`, so the multiply survives constant folding and is really emitted. +volatile float fa = 3.0f, fb = 4.0f, fout = 0.0f; +extern "C" void Reset_Handler() { fout = fa * fb + 1.0f; for (;;) {} } +extern "C" __attribute__((section(".vectors"), used)) +void* const vectors[] = { (void*)&__stack_top, (void*)Reset_Handler }; +CPP +emit_ld 0x00000000 4M 0x20000000 4M + +rm -rf target +"$MCPP" build --target thumbv7em-none-eabihf >/dev/null 2>&1 || { + echo "FAIL: the hard-float row did not build"; exit 1; } +hard=$("$OBJDUMP" -d "$(find target -type f -name fp | head -1)" \ + | grep -cE '\bv[a-z]+\.f32' || true) +[ "$hard" -gt 0 ] || { + echo "FAIL: thumbv7em-none-eabihf emitted no FPU instruction — the control is vacuous"; exit 1; } +echo " ok thumbv7em-none-eabihf uses the FPU ($hard instructions)" + +# ⭐ The soft row's proof is its LINK FAILURE, and that is the strongest form +# available at this tier. `-mfpu=none` makes the compiler lower the multiply +# onto `__aeabi_fmul` rather than `vmul.f32`; with no C library and no builtins +# there is nothing for that call to resolve against. A link that fails naming +# `__aeabi_fmul` therefore states exactly what is asserted: the multiply did +# NOT become an FPU instruction. +rm -rf target +soft_out=$("$MCPP" build --target thumbv7em-none-eabi 2>&1 || true) +case "$soft_out" in + *__aeabi_fmul*) echo " ok thumbv7em-none-eabi lowered the multiply onto a libcall, not the FPU" ;; + *) echo "FAIL: thumbv7em-none-eabi did not reference __aeabi_fmul; -mfpu=none may not be applied" + echo "$soft_out" | tail -5 | sed 's/^/ /'; exit 1 ;; +esac + +echo "PASS: cortex-m rows build, boot, collect dead code and honour the float ABI" diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index a9279640..9a6eb8f0 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -84,6 +84,18 @@ case "$OS" in # qemu-riscv: the emulator a bare-metal riscv artifact runs in # (xim:qemu-riscv, or any qemu-system-riscv64 on PATH). command -v qemu-system-riscv64 &>/dev/null && CAPS+=(qemu-riscv) + # qemu-arm: the emulator an M-profile artifact runs in. Unlike + # `qemu-riscv` this also looks inside the xim payload, because + # `xim:qemu-arm` installs no shim for `qemu-system-arm` on every host + # and the tests address it by absolute path out of the payload anyway. + # Probing only PATH would report the capability absent on a machine + # that has it, and the test would skip while looking supported. + if command -v qemu-system-arm &>/dev/null \ + || ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm \ + &>/dev/null \ + || ls "$HOME"/.xlings/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm &>/dev/null; then + CAPS+=(qemu-arm) + fi # pack capability: ELF + patchelf both required if [[ " ${CAPS[*]} " == *" patchelf "* ]]; then CAPS+=(pack) @@ -209,8 +221,8 @@ echo "Detected capabilities: ${CAPS[*]:-}" # CAPS+=() calls above by tests/e2e/README or by reading them -- keep it in # sync when adding a capability. KNOWN_CAPS=(elf fresh-sandbox gcc import-std-libcxx jq llvm macos mingw - mingw-cross msvc musl nasm no-msvc pack patchelf python3 qemu-riscv - scan-deps symlink unix-shell windows wine xlings-msvc) + mingw-cross msvc musl nasm no-msvc pack patchelf python3 qemu-arm + qemu-riscv scan-deps symlink unix-shell windows wine xlings-msvc) bad_tokens=0 for tf in "$HERE"/[0-9]*.sh; do diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index 58344101..4260de3e 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -40,39 +40,106 @@ # linux-aarch64 16 只有 musl-gcc —— llvm 在非 x86_64 Linux 上被显式延缓 # macos-arm64 20 只有 llvm × 12 目标(payload 12 / graph 8) # windows-x86_64 40 llvm + msvc@system +graph linux-aarch64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-aarch64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-aarch64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-aarch64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported host-cannot-serve +graph linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve +graph linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +graph linux-x86_64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-linux-gnu gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin +graph linux-x86_64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +graph macos-arm64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 aarch64-macos llvm@22.1.8 arm64-apple-macos14.0 host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +graph macos-arm64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +graph macos-arm64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +graph windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +graph windows-x86_64 riscv64-linux-musl msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 x86_64-windows-msvc llvm@22.1.8 - - - - - unsupported other +graph windows-x86_64 x86_64-windows-msvc msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin payload linux-aarch64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned payload linux-aarch64 aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none payload linux-aarch64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned payload linux-aarch64 riscv64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv6m-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv7em-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv7em-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv7m-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv8m.base-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv8m.main-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv8m.main-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported host-cannot-serve payload linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned payload linux-aarch64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve payload linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none -payload linux-x86_64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 riscv64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 x86_64-linux-gnu gcc@16.1.0 x86_64-unknown-linux-gnu subos gnu(payload) libstdc++(payload) - ok none -payload linux-x86_64 x86_64-linux-musl gcc@16.1.0 x86_64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none -payload linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 x86_64-windows-gnu gcc@16.1.0 x86_64-w64-windows-gnu none gnu(payload) libstdc++(payload) - ok none -payload linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +payload linux-x86_64 aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none payload linux-x86_64 aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux-x86_64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-none-elf llvm@22.1.8 aarch64-none-elf payload - - - ok none +payload linux-x86_64 riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 riscv32-none-elf llvm@22.1.8 riscv32-none-elf payload - - - ok none +payload linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned payload linux-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +payload linux-x86_64 riscv64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 riscv64-none-elf llvm@22.1.8 riscv64-none-elf payload - - - ok none +payload linux-x86_64 thumbv6m-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv6m-none-eabi llvm@22.1.8 thumbv6m-none-eabi payload - - - ok none +payload linux-x86_64 thumbv7em-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv7em-none-eabi llvm@22.1.8 thumbv7em-none-eabi payload - - - ok none +payload linux-x86_64 thumbv7em-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv7em-none-eabihf llvm@22.1.8 thumbv7em-none-eabihf payload - - - ok none +payload linux-x86_64 thumbv7m-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv7m-none-eabi llvm@22.1.8 thumbv7m-none-eabi payload - - - ok none +payload linux-x86_64 thumbv8m.base-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv8m.base-none-eabi llvm@22.1.8 thumbv8m.base-none-eabi payload - - - ok none +payload linux-x86_64 thumbv8m.main-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv8m.main-none-eabi llvm@22.1.8 thumbv8m.main-none-eabi payload - - - ok none +payload linux-x86_64 thumbv8m.main-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv8m.main-none-eabihf llvm@22.1.8 thumbv8m.main-none-eabihf payload - - - ok none +payload linux-x86_64 x86_64-linux-gnu gcc@16.1.0 x86_64-unknown-linux-gnu subos gnu(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload gnu(payload) libc++(payload) - ok none +payload linux-x86_64 x86_64-linux-musl gcc@16.1.0 x86_64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned payload linux-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +payload linux-x86_64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 x86_64-none-elf llvm@22.1.8 x86_64-none-elf payload - - - ok none +payload linux-x86_64 x86_64-windows-gnu gcc@16.1.0 x86_64-w64-windows-gnu none gnu(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve payload macos-arm64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned payload macos-arm64 aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced @@ -81,78 +148,53 @@ payload macos-arm64 aarch64-none-elf llvm@22.1.8 aarch64-none-elf host - - - ok payload macos-arm64 riscv32-none-elf llvm@22.1.8 riscv32-none-elf host - - - ok none payload macos-arm64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned payload macos-arm64 riscv64-none-elf llvm@22.1.8 riscv64-none-elf host - - - ok none +payload macos-arm64 thumbv6m-none-eabi llvm@22.1.8 thumbv6m-none-eabi host - - - ok none +payload macos-arm64 thumbv7em-none-eabi llvm@22.1.8 thumbv7em-none-eabi host - - - ok none +payload macos-arm64 thumbv7em-none-eabihf llvm@22.1.8 thumbv7em-none-eabihf host - - - ok none +payload macos-arm64 thumbv7m-none-eabi llvm@22.1.8 thumbv7m-none-eabi host - - - ok none +payload macos-arm64 thumbv8m.base-none-eabi llvm@22.1.8 thumbv8m.base-none-eabi host - - - ok none +payload macos-arm64 thumbv8m.main-none-eabi llvm@22.1.8 thumbv8m.main-none-eabi host - - - ok none +payload macos-arm64 thumbv8m.main-none-eabihf llvm@22.1.8 thumbv8m.main-none-eabihf host - - - ok none payload macos-arm64 x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced payload macos-arm64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned payload macos-arm64 x86_64-none-elf llvm@22.1.8 x86_64-none-elf host - - - ok none payload macos-arm64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced payload macos-arm64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve payload windows-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced -payload windows-x86_64 aarch64-none-elf llvm@22.1.8 aarch64-none-elf none - - - ok none -payload windows-x86_64 riscv32-none-elf llvm@22.1.8 riscv32-none-elf none - - - ok none -payload windows-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 riscv64-none-elf llvm@22.1.8 riscv64-none-elf none - - - ok none -payload windows-x86_64 x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced -payload windows-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 x86_64-none-elf llvm@22.1.8 - - - - - unsupported lld-required-absent -payload windows-x86_64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced -payload windows-x86_64 x86_64-windows-msvc llvm@22.1.8 x86_64-pc-windows-msvc none msvc(payload) msvc-stl(payload) - ok none -payload windows-x86_64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve payload windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced payload windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported convention-unreplaced +payload windows-x86_64 aarch64-none-elf llvm@22.1.8 aarch64-none-elf none - - - ok none payload windows-x86_64 aarch64-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 riscv32-none-elf llvm@22.1.8 riscv32-none-elf none - - - ok none payload windows-x86_64 riscv32-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned payload windows-x86_64 riscv64-linux-musl msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 riscv64-none-elf llvm@22.1.8 riscv64-none-elf none - - - ok none payload windows-x86_64 riscv64-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv6m-none-eabi llvm@22.1.8 thumbv6m-none-eabi none - - - ok none +payload windows-x86_64 thumbv6m-none-eabi msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv7em-none-eabi llvm@22.1.8 thumbv7em-none-eabi none - - - ok none +payload windows-x86_64 thumbv7em-none-eabi msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv7em-none-eabihf llvm@22.1.8 thumbv7em-none-eabihf none - - - ok none +payload windows-x86_64 thumbv7em-none-eabihf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv7m-none-eabi llvm@22.1.8 thumbv7m-none-eabi none - - - ok none +payload windows-x86_64 thumbv7m-none-eabi msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv8m.base-none-eabi llvm@22.1.8 thumbv8m.base-none-eabi none - - - ok none +payload windows-x86_64 thumbv8m.base-none-eabi msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv8m.main-none-eabi llvm@22.1.8 thumbv8m.main-none-eabi none - - - ok none +payload windows-x86_64 thumbv8m.main-none-eabi msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv8m.main-none-eabihf llvm@22.1.8 thumbv8m.main-none-eabihf none - - - ok none +payload windows-x86_64 thumbv8m.main-none-eabihf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced payload windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported convention-unreplaced +payload windows-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned payload windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 x86_64-none-elf llvm@22.1.8 - - - - - unsupported lld-required-absent payload windows-x86_64 x86_64-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced payload windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported convention-unreplaced +payload windows-x86_64 x86_64-windows-msvc llvm@22.1.8 x86_64-pc-windows-msvc none msvc(payload) msvc-stl(payload) - ok none payload windows-x86_64 x86_64-windows-msvc msvc@system x86_64-pc-windows-msvc none msvc(payload) (payload) - ok none +payload windows-x86_64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve payload windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin -graph linux-aarch64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-aarch64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux-aarch64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-aarch64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported host-cannot-serve -graph linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve -graph linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 x86_64-linux-gnu gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux-x86_64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin -graph linux-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned -graph linux-x86_64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned -graph linux-x86_64 x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux-x86_64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned -graph linux-x86_64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux-x86_64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned -graph macos-arm64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 aarch64-macos llvm@22.1.8 arm64-apple-macos14.0 host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned -graph macos-arm64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned -graph macos-arm64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph windows-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph windows-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph windows-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph windows-x86_64 x86_64-windows-msvc llvm@22.1.8 - - - - - unsupported other -graph windows-x86_64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported tier-planned -graph windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 riscv64-linux-musl msvc@system - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 x86_64-windows-msvc msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh index c53f98c2..771d9592 100755 --- a/tests/matrix/scan.sh +++ b/tests/matrix/scan.sh @@ -154,7 +154,14 @@ for tc in $(compilers); do # `openkal-opensbi`(RISC-V 的监管者接口)并编译失败。 # # 裸机 × 图由 135/136/292 用正确的包覆盖。这里跳过并说出来。 - if [ "$MODE" = graph ] && printf '%s' "$t" | grep -q -- '-none-elf$'; then + # + # ⚠️ THE SAME PREDICATE AS THE PROBE BELOW, AND IT HAD THE SAME DEFECT. + # Written as `-none-elf$`, this skipped only the four targets that spell + # their environment `elf`; the Cortex-M rows spell it `eabi`/`eabihf`, so + # they were NOT skipped and produced fourteen graph cells describing a + # combination this paragraph says is out of scope. One rule, two copies — + # changing one and missing the other is how it failed. + if [ "$MODE" = graph ] && printf '%s' "$t" | grep -qE -- '-none-[a-z0-9]+$'; then echo "scan: 略过 graph × $t —— 裸机行不在这套依赖的论域内" >&2 continue fi @@ -170,7 +177,16 @@ for tc in $(compilers); do # 的失败 —— 零 libc 的目标本来就没有 ``,是探针问错了问题。 # # ⭐ 一个自己就编不过的探针,产出的整列都是关于探针的。 - if [ "$MODE" != graph ] && printf '%s' "$t" | grep -q -- '-none-elf$'; then + # + # ⚠️ AND THE PREDICATE IS THE OS FIELD, NOT A FILENAME SUFFIX. This read + # `-none-elf$` until 2026-09-04, which is a spelling rather than a property: + # the Cortex-M rows are `…-none-eabi` and `…-none-eabihf`, so they fell into + # the hosted branch, got `#include ` — and produced a whole column of + # `build-failed` describing the probe rather than the target. Exactly the + # defect the paragraph above records, reintroduced by matching a substring. + # + # A freestanding triple is `arch-none-env`; the OS field is what says so. + if [ "$MODE" != graph ] && printf '%s' "$t" | grep -qE -- '-none-[a-z0-9]+$'; then printf 'extern "C" void kmain() { for (volatile int i = 0; i < 1; ++i) {} }\n' > src/main.cpp printf '\n[target.%s]\nsysroot = ""\n' "$t" >> mcpp.toml else diff --git a/tests/unit/test_freestanding.cpp b/tests/unit/test_freestanding.cpp index 37d7814a..7d2cdcba 100644 --- a/tests/unit/test_freestanding.cpp +++ b/tests/unit/test_freestanding.cpp @@ -469,3 +469,79 @@ TEST(FreestandingTarget, EveryIsaProfileHasABareRowInTheTargetTable) { << spec.triple << " has an ISA profile but is not marked bare"; } } + +// ── Cortex-M ──────────────────────────────────────────────────────────────── +// +// ⭐ THESE ARE RULES OVER THE TABLE, WHICH IS THE ONLY PLACE THEY CAN BE +// STATED. The e2e boots four M-profile images and measures instruction counts, +// but it can only speak about the rows it happens to exercise. A rule quantified +// over every row is what stops the eighth one from being added wrong, and the +// property it protects is invisible in a build: a soft-float row that reaches +// the FPU compiles, links, and faults on real silicon. + +namespace { +bool is_m_profile(std::string_view triple) { return triple.starts_with("thumb"); } +bool has_flag(const mcpp::freestanding::Spec& s, std::string_view f) { + for (auto e : s.extra) if (e == f) return true; + return false; +} +} // namespace + +// The pair that the float-ABI suffix does NOT settle on its own. clang reads +// `eabi`/`eabihf` and sets `-mfloat-abi` accordingly, but the ABI governs how +// floats cross a call boundary, not whether the compiler may use the FPU inside +// one — and `thumbv7em` implies an FPU. Measured on llvm 22.1.8: without +// `-mfpu=none` the soft row emits `vmul.f32`. +TEST(FreestandingTarget, SoftFloatMProfileRowsDisableTheFpu) { + int soft = 0, hard = 0; + for (const auto& spec : mcpp::freestanding::known()) { + if (!is_m_profile(spec.triple)) continue; + if (spec.triple.ends_with("-eabihf")) { + ++hard; + EXPECT_FALSE(has_flag(spec, "-mfpu=none")) + << spec.triple << " is a hard-float row and must not discard the FPU"; + } else { + ++soft; + EXPECT_TRUE(has_flag(spec, "-mfpu=none")) + << spec.triple + << " is a soft-float row without -mfpu=none: it will emit FPU" + " instructions that fault on a part with no FPU"; + } + } + // ⚠️ A denominator, because a loop that never ran satisfies every EXPECT + // above. Both halves must be non-empty for the contrast to mean anything. + EXPECT_GT(soft, 0); + EXPECT_GT(hard, 0); +} + +// 32-bit ARM has no `-mcmodel`, and clang has a BareMetal toolchain for arm so +// its driver reaches `ld.lld` without the x86_64 row's workaround. Both columns +// being empty is what says so; a filled one would be a value nothing checks. +TEST(FreestandingTarget, MProfileRowsNeedNoCodeModelAndNoDirectLldDriving) { + int rows = 0; + for (const auto& spec : mcpp::freestanding::known()) { + if (!is_m_profile(spec.triple)) continue; + ++rows; + EXPECT_TRUE(spec.mcmodel.empty()) << spec.triple << " has no -mcmodel axis"; + EXPECT_TRUE(spec.lldEmulation.empty()) + << spec.triple << " reaches ld.lld through the clang driver"; + EXPECT_EQ(spec.mabi, "aapcs") << spec.triple; + } + EXPECT_GT(rows, 0); +} + +// The compile half of `--gc-sections`, which has to hold for EVERY freestanding +// target rather than only the M-profile ones: a dependency's translation units +// carry these flags too, and a project cannot reach those. +TEST(FreestandingTarget, EveryRowCompilesWithPerFunctionSections) { + int rows = 0; + for (const auto& spec : mcpp::freestanding::known()) { + ++rows; + auto flags = mcpp::freestanding::compile_flags(spec); + EXPECT_NE(std::find(flags.begin(), flags.end(), "-ffunction-sections"), + flags.end()) << spec.triple; + EXPECT_NE(std::find(flags.begin(), flags.end(), "-fdata-sections"), + flags.end()) << spec.triple; + } + EXPECT_GT(rows, 0); +}