Skip to content

Commit 8de3847

Browse files
runner beyond bare metal: honour [target.<triple>].runner on hosted targets, typed launch failures, not-run reporting, [xlings] values per host (#544) (#545)
* platform: carry the spawn errno up instead of re-spawning; run_exec is never silent (#544) * manifest: [xlings] values per host platform; the target sweep reports array typos and lists runner (#544) * build: runner lookup through declared payloads then PATH, with typed messages (#544) * run/test: honour the declared runner on every target; --no-runner; not-run reporting with exit 2; e2e 330 (#544) * docs: runner on hosted targets, not-run reporting, [xlings] values per host platform (#544) * release: 2026.9.2.1 * tests: the ENOEXEC unit test is Linux-only — macOS posix_spawnp runs an unloadable file through /bin/sh (measured on CI) * run/test: look the runner up by the canonical triple — the driver's spelling matched on Linux and never on macOS (measured on CI) * run/test: diagnostics name the canonical triple, which is the key the reader wrote `choose_runner` resolves `[target.<triple>].runner` against the canonical spelling — the output directory's name, and the key every other `[target.<triple>]` reader uses. The four diagnostics it feeds printed `tc.targetTriple`, the spelling the driver reported, which on a Linux host is the same string and on macOS is `arm64-apple-darwin24.6.0`. Two consequences, both user-facing. The not-found message named a triple the author never wrote. The unrunnable message printed a `[target.…]` block to paste whose key no lookup would ever match, so following the advice would have left the artifact running bare a second time. `RunnerChoice` now carries `tripleKey`, derived once beside the lookup that uses it, so the two cannot disagree. The triple is parsed once rather than three times. Measured: e2e 330 §3 asserts the message names the triple the manifest key uses and failed only on macOS ARM64 (run 33609434208). * run/test: the two notes and the freestanding error name the key as well Six message sites remained on `tc.targetTriple` after the previous commit, and three of them print a `[target.<triple>]` key the reader is meant to act on: the override note, its `mcpp test` twin, and `no_runner_message`, which shows a complete key-and-value block to paste. On macOS each named `arm64-apple-darwin24.6.0`, a spelling no `[target.…]` lookup resolves, so following the advice would have produced a key that is never read. The remaining two, the `--no-runner` notes, name the target a runner was declared for, and the declaration is under the canonical key. Every diagnostic on both paths now reads `RunnerChoice::tripleKey`. On Linux the two spellings coincide, which is why e2e 130, 131 and 132 pass unchanged. * CHANGELOG: 2026.9.2.1 * design: state where the payload-bin rule is actually covered --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent aeba151 commit 8de3847

29 files changed

Lines changed: 3569 additions & 154 deletions

.agents/docs/2026-09-02-issue544-runner-implementation-plan.md

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

.agents/docs/2026-09-02-runner-beyond-baremetal-design.md

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

CHANGELOG.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,79 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [2026.9.2.1] — 2026-09-02
7+
8+
`[target.<triple>].runner` 对每一个目标生效,启动失败不再无声,`mcpp test` 把跑不起来
9+
的测试报成 not run 并退 2,`[xlings]` 的值可以按宿主平台给出。
10+
11+
设计与实测见
12+
[`.agents/docs/2026-09-02-runner-beyond-baremetal-design.md`](.agents/docs/2026-09-02-runner-beyond-baremetal-design.md)
13+
14+
> **一个键被解析、被类型检查、被文档记录,而读它的那处代码在读之前就返回了。**
15+
> `choose_runner``os == "none"` 之外一律返回空模板,于是宿主交叉目标
16+
> (x86_64 上构建的 `aarch64-linux-musl`)的 runner 从未被查询过:`mcpp run` 裸执行
17+
> 产物,内核以 `ENOEXEC` 拒绝,而 `run_exec` 把这次拒绝变成一个不打印任何东西的
18+
> 127。同一层的有界启动器在自己的声明里写着「『起不来』与『跑了但失败』不能共用
19+
> 一个退出码」,而它的两个调用方都靠**再 spawn 一次**来回落,把第一次的 errno 丢掉。
20+
21+
### 修复
22+
23+
- **`[target.<triple>].runner` 对每个目标生效。** freestanding 谓词只决定一件事:
24+
没有 runner 时是否在任何 spawn 之前就失败。宿主能不能执行一个外来 ISA 的产物不再
25+
被预测 —— 这台机器上 `binfmt_misc` 注册了 qemu-user 就能跑,报告 #544 的那台不能,
26+
而两者的三元组相同。mcpp 要么执行工程声明的 runner,要么尝试启动并报告内核的回答。
27+
28+
- **runner 的程序由 mcpp 定位,不由 `posix_spawnp` 定位。** 先在 `[xlings] deps`
29+
声明的每个载荷的 `bin/` 里找,再走 `PATH`。裸名在 `PATH` 上解析到的是 xvm 垫片,
30+
而垫片按**当前 SubOS** 回答而不是按包所在的位置回答(e2e 130 在 CI 里记录过这一条:
31+
同一个 job 里 `qemu-system-riscv64 --version` 成功,而 `mcpp run` 执行同一个裸名
32+
得到「未安装」)。「哪儿都找不到」在任何 spawn 之前判定,并且是错误而不是回落到
33+
裸执行:让产物在另一个解释器下带着另一组参数运行,正是这个键存在要防止的失败。
34+
35+
- **启动失败被定型并且只被报告一次。** `DeadlineRun``BoundedOutcome` 带上
36+
spawn 错误码;`run_exec``capture_exec` 与两个 deadline 包装各多一个末位
37+
`int* spawn_error`。调用方要了错误码就由调用方报告,没要就由启动器自己报告。
38+
没有第二次 spawn。
39+
40+
- **`mcpp test` 有了第四种状态。** 产物这台宿主装载不了的测试既没有通过也没有失败。
41+
它被报成 **not run**,原因在确立时打印一次,在汇总行里以与失败同等的分量再出现一次,
42+
退出码是 2 —— freestanding 的 no-runner 路径对同一种处境早就用这个码。1 保持
43+
「跑了并且失败」,0 保持「每个测试都跑了并且通过」。`--message-format json` 带上
44+
`"status":"not_run"` 与每条记录的 `reason`,汇总记录带上 `not_run` /
45+
`not_run_reason`;`--workspace` 另加 `tests_not_run``unrunnable_members`
46+
47+
- **`--no-runner`,`mcpp run``mcpp test` 都接受。** 「这台宿主能直接执行该产物」
48+
是关于宿主的事实,而 manifest 没有宿主轴:`[target.<triple>]` 按目标索引,
49+
`[xlings] deps` 没有任何索引。为 x86_64 开发者写的 runner 在 aarch64 宿主上同样会
50+
被读到,而那里模拟器既无用也装不上。旗标由那台宿主上的操作者给出,因为只有那里
51+
知道这件事。
52+
53+
- **`[xlings]` 的值可以按宿主平台给出。** `deps` 的一项与 `[xlings.workspace]` 的一个
54+
值都可以写成 `{ linux = "...", macos = "...", windows = "...", default = "..." }` ——
55+
xlings 自己的 `.xlings.json``workspace` 接受的就是这个形式,`macosx` 作为它的
56+
拼法一并接受。在 manifest 加载时对本机解析,因此下游每一个读者看到的仍然是一张平表。
57+
未知的平台键是硬错误。在此之前 `[xlings.workspace]`**静默丢掉**一个表值,而
58+
`[xlings] deps` 根本没有条件化形式 —— 这让 `deps = ["qemu-user-aarch64"]`
59+
(索引里只为 x86_64 构建的包)在其余每一类宿主上都是硬构建错误。
60+
61+
- **`[target.<triple>]` 的未知键普查覆盖数组。** `runnerX = ["x"]` 会被报出来,
62+
而支持键的清单里补上了 `runner` —— 此前普查刻意跳过数组,代价是这张表读取的唯一
63+
一个数组键既不在清单里,拼错了也无人报告。
64+
65+
- **诊断名出的是规范拼法。** 六处消息此前打印驱动报出的三元组;其中三处打印的是
66+
一段供粘贴的 `[target.<triple>]`,而在 macOS 上那是 `arm64-apple-darwin24.6.0`,
67+
没有任何 `[target.…]` 查询会命中它。`RunnerChoice::tripleKey` 与查询本身在同一处
68+
求出,两者不可能各说各的。
69+
70+
### 行为变化
71+
72+
- 宿主三元组下已经声明的 `runner` 从此生效。在其程序缺失的宿主上,`mcpp run` /
73+
`mcpp test` 现在带消息失败,而不是静默裸跑产物;`--no-runner` 是出口。
74+
- 此前对跑不起来的产物报 `FAIL (exit 127)``mcpp test` 现在报 `NOT RUN` 并退 2
75+
(仍然非零,CI 作业不会因此改变颜色)。
76+
- `[xlings] deps``[xlings.workspace]` 接受表值;带未知平台键的表此前被丢弃,
77+
现在是错误。
78+
679
## [2026.9.1.1] — 2026-09-01
780

881
#540 的七条审计,加上核验它们时挖出的四条没有人报过的。它们几乎全是同一族:

docs/05-mcpp-toml.md

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1086,7 +1086,7 @@ The selector `<sel>` has three forms:
10861086
|---|---|---|
10871087
| **bare OS alias** | a single OS / family — the concise, common form | `[target.windows]`, `[target.unix]` |
10881088
| **`cfg(...)` predicate** | a compound condition (arch / env / combinators) | `[target.'cfg(all(linux, not(arch = "aarch64")))']` |
1089-
| **exact triple** | one specific target (also carries `toolchain` / `linkage`) | `[target.x86_64-linux-musl]` |
1089+
| **exact triple** | one specific target (also carries `toolchain` / `linkage` / `sysroot` / `runner`; see §2.7.3) | `[target.x86_64-linux-musl]` |
10901090

10911091
A selector may carry platform-conditional **dependencies** and **build flags**:
10921092

@@ -1284,6 +1284,75 @@ different argv (`-bios default` for an OpenSBI boot, `-bios none -semihosting`
12841284
for a picolibc image) — and an engine that guesses one is an engine the other
12851285
board has to fight. A board-support package normally supplies it.
12861286

1287+
### 2.7.3 `runner` on a hosted target (2026.9.2.1+)
1288+
1289+
`[target.<triple>].runner` applies to every exact triple, not only to bare
1290+
metal. A hosted cross artifact — `aarch64-linux-musl` built on an x86_64
1291+
machine — is executable by some hosts (binfmt_misc with qemu-user registered)
1292+
and refused by others with `Exec format error`, and which of the two applies is
1293+
a property of the machine, not of the triple. mcpp does not predict it. It
1294+
either executes the artifact through the runner the project declared, or it
1295+
attempts direct execution and reports what the kernel answered.
1296+
1297+
```toml
1298+
[target.aarch64-linux-musl]
1299+
runner = ["qemu-aarch64-static"]
1300+
```
1301+
1302+
The rules, for `mcpp run` and `mcpp test` alike:
1303+
1304+
- **A declared runner is used.** Its first element is located by mcpp: first in
1305+
the `bin/` directory of each payload declared under `[xlings] deps` (§2.13),
1306+
then on `PATH`. A bare name on `PATH` resolves to an xvm shim, which answers
1307+
for the current SubOS rather than for the package; the payload lookup is what
1308+
lets a runner name a program the project declared.
1309+
- **A declared runner that cannot be found or started is an error**, with the
1310+
program, the directories searched and the errno. There is no fallback to
1311+
direct execution: running the artifact under a different interpreter with
1312+
different arguments is the failure the key exists to prevent.
1313+
- **No runner, and the kernel refuses the artifact:** `mcpp run` reports the
1314+
refusal and the key to write, and exits 2. `mcpp test` reports every test as
1315+
not run, with the reason once, and exits 2 (§2.7.3.1).
1316+
- **`--no-runner`** executes the artifact directly and ignores a declared
1317+
runner. It states a fact about this host — the triple is native here — that
1318+
the manifest has no axis to carry; a project whose runner was written for
1319+
x86_64 developers is still readable on an aarch64 machine.
1320+
1321+
Provisioning the emulator through `[xlings] deps` is the form for a CI job or
1322+
a project built on one host class. `qemu-user-aarch64` in the index is built
1323+
for x86_64 Linux only, and `[xlings] deps` provisions on every host that builds
1324+
the project, so the entry is written per platform (§2.13):
1325+
1326+
```toml
1327+
[xlings]
1328+
deps = [{ linux = "qemu-user-aarch64" }]
1329+
1330+
[target.aarch64-linux-musl]
1331+
runner = ["qemu-aarch64-static"]
1332+
```
1333+
1334+
A package the host cannot install is a hard build error, so an entry without
1335+
the platform form would make the project unbuildable on macOS and Windows. The
1336+
Linux/aarch64 host, where the package does not exist either, passes
1337+
`--no-runner`.
1338+
1339+
#### 2.7.3.1 `mcpp test` and tests that were not run
1340+
1341+
A test whose artifact this host cannot execute has neither passed nor failed.
1342+
`mcpp test` reports it as **not run**, prints the reason once when it is
1343+
established, repeats the first line of the reason in the summary, and exits 2:
1344+
1345+
```
1346+
warning: this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); declare [target.aarch64-linux-musl].runner, or pass --no-runner on a host that can
1347+
smoke ... not run
1348+
error: test result: NOT RUN. 0 passed; 0 failed; 1 not run (this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); ...); finished in 0.41s (build 0.39s + run 0.00s)
1349+
```
1350+
1351+
Exit code 1 keeps its meaning — a test ran and failed — and 0 means every test
1352+
ran and passed. `--message-format json` carries `"status":"not_run"` and a
1353+
`reason` on each record, and `not_run` / `not_run_reason` on the summary
1354+
record (see [11 — Machine-Readable Output](11-machine-output.md)).
1355+
12871356
### 2.8 `[features]` — Features (Cargo-style, additive)
12881357

12891358
```toml
@@ -1820,6 +1889,27 @@ build needs (`make`/`cmake`/`protoc`/…), pin tool versions per project, or set
18201889
build-time env vars — without hand-editing `.xlings.json`. `[toolchain]` (§2.7) remains
18211890
the ergonomic shorthand for the compiler; `[xlings.workspace]` is the general form.
18221891

1892+
**Values per host platform (2026.9.2.1+).** A `deps` entry and a
1893+
`[xlings.workspace]` value may be a table keyed by platform, the form xlings'
1894+
own `.xlings.json` accepts for `workspace`:
1895+
1896+
```toml
1897+
[xlings]
1898+
deps = ["xim:ninja", { linux = "qemu-user-aarch64" }, { windows = "nasm", default = "yasm" }]
1899+
1900+
[xlings.workspace]
1901+
gcc = { linux = "15.1.0" }
1902+
llvm = { macos = "20", default = "22" }
1903+
```
1904+
1905+
The keys are `linux`, `macos`, `windows` and `default`; `macosx` is accepted as
1906+
xlings' spelling of `macos`. mcpp resolves the table against the host it runs
1907+
on when the manifest is loaded: the host's key wins, `default` is the fallback,
1908+
and a table with neither declares nothing on that host — the entry is absent,
1909+
not empty. An unknown key is an error rather than a dropped entry. The axis is
1910+
the host operating system only; a package that exists for the OS but not for
1911+
the architecture is still a provisioning error on that host.
1912+
18231913
`subos` selects the root project's **local build/run OS environment**. If the
18241914
key is absent, mcpp uses its initialized, release-verified `McppDefault` SubOS;
18251915
`subos = "default"` is an explicit `NamedSubos("default")` selection. There is

docs/11-machine-output.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,54 @@ from the same resolution a build performs, which may fetch packages, install a
358358
payload and run a dependency's build program. A client gates on that table
359359
*before* running anything, so an omission would be a safety claim that is not
360360
true.
361+
362+
### `mcpp test --message-format json` — the test stream
363+
364+
```
365+
mcpp test [pattern] [--workspace] --message-format json
366+
```
367+
368+
This stream predates the envelope of §2 and is not wrapped in it: it is NDJSON,
369+
one record per test as each finishes, then one summary record per member. A
370+
`--workspace` run ends with one `workspace_summary` record. The §6 guarantees
371+
apply to it — fields are added and never removed, and a field's meaning never
372+
changes — and the fields below are the contract as of 2026.9.2.1.
373+
374+
Per test:
375+
376+
| field | |
377+
|---|---|
378+
| `member` | the workspace member, or `""` outside a workspace |
379+
| `test` | the path-based test name (`tests/00-a/0.cpp``00-a/0`) |
380+
| `status` | `pass`, `compile_fail`, `run_fail`, or `not_run` |
381+
| `exit_code` | the test's exit status; `0` for `not_run` |
382+
| `signal` | the signal number when the status encodes one, else `null` |
383+
| `duration_ms` | build+run wall time of this test |
384+
| `timed_out` | `true` when `--timeout` killed it (`run_fail`) |
385+
| `compile_output`, `run_output` | captured diagnostics |
386+
| `reason` | `not_run` only: why, in one sentence; `""` otherwise |
387+
388+
Summary record, `{"summary": {...}}`:
389+
390+
| field | |
391+
|---|---|
392+
| `member`, `passed`, `failed` | counts |
393+
| `not_run` | tests that were built and not executed |
394+
| `not_run_reason` | the reason shared by all of them, or `""` |
395+
| `elapsed_ms`, `build_ms`, `run_ms` | wall time, split |
396+
397+
⚠️ **`not_run` is neither `pass` nor `run_fail`, and the exit code says so
398+
(2026.9.2.1).** A test is `not_run` when this host cannot load its artifact
399+
(`Exec format error` on a cross target with no runner declared), or when the
400+
declared `[target.<triple>].runner` could not be found or started. The
401+
condition is a fact about the invocation: it is established once, the
402+
remaining tests are reported `not_run` without being started, and the process
403+
exits **2**. Exit 1 keeps meaning "a test ran and failed"; exit 0 means every
404+
test ran and passed. A client that read the exit code alone as pass/fail must
405+
handle 2, and a client that inferred "everything passed" from `failed == 0`
406+
must also read `not_run`.
407+
408+
`workspace_summary` adds `tests_not_run` (the sum over members) and
409+
`unrunnable_members` (members all of whose tests were `not_run`), alongside the
410+
existing `not_run` list, which continues to name members the
411+
`--workspace-timeout` stopped before they started.

docs/13-baremetal.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,14 @@ error: no runner is configured for 'riscv64-none-elf' — a freestanding artifac
549549
A board-support package normally supplies this so you do not have to.
550550
```
551551

552+
The key is not specific to bare metal. A hosted cross target — an
553+
`aarch64-linux-musl` artifact on an x86_64 host — takes the same
554+
`[target.<triple>].runner`, with a user-mode emulator such as
555+
`qemu-aarch64-static` in place of the system emulator; on such a target an
556+
absent runner is not an error until the kernel refuses the artifact. The rules
557+
for hosted targets, the `--no-runner` escape and the not-run reporting of
558+
`mcpp test` are in [5 — mcpp.toml](05-mcpp-toml.md), §2.7.3.
559+
552560
## Writing a board-support package
553561

554562
A board-support package is an ordinary mcpp package. It declares the emulator

docs/15-openkal-cross.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,25 @@ runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic",
190190
firmware mode to use are board facts, and an engine that guesses one is an
191191
engine a different board has to fight.
192192

193+
A hosted cross target takes the same key with a user-mode emulator
194+
(2026.9.2.1). An `aarch64-linux-musl` artifact built on an x86_64 host is
195+
executed through `qemu-aarch64-static` when the project declares it, and the
196+
package that provides the emulator is declared for the hosts that can install
197+
it:
198+
199+
```toml
200+
[xlings]
201+
deps = [{ linux = "qemu-user-aarch64" }]
202+
203+
[target.aarch64-linux-musl]
204+
runner = ["qemu-aarch64-static"]
205+
```
206+
207+
Without the key, `mcpp run` reports the kernel's refusal (`Exec format error`)
208+
and the key to write, and `mcpp test` reports every test as not run and exits
209+
2. A host that executes the artifact natively passes `--no-runner`. The rules
210+
are in [5 — mcpp.toml](05-mcpp-toml.md), §2.7.3.
211+
193212
### The Source Is The Same, The Program Is Not
194213

195214
"The same source" is a claim about the toolchain and the standard library, and

docs/17-the-project-environment.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,19 @@ instead of installing, and names the packages so they can be provisioned
124124
out of band — the same two knobs `[toolchain]` honours, for the same reason: an
125125
unasked-for download is not something a build decides on a project's behalf.
126126

127+
The declaration is provisioned on every host that builds the project, and a
128+
package the host cannot install is an error, not a skipped entry. A tool that
129+
exists for one host platform only is therefore declared for that platform
130+
(2026.9.2.1): `deps = [{ linux = "qemu-user-aarch64" }]` declares the emulator
131+
on Linux and nothing elsewhere. The keys and the resolution rule are in
132+
chapter 5, §2.13.
133+
134+
**The runner.** A program under `[xlings] deps` is also where
135+
`[target.<triple>].runner` looks first for its first element, before `PATH`
136+
(chapter 5, §2.7.3). The two keys together provision a user-mode emulator on a
137+
CI host and execute a cross-built artifact through it, without the manifest
138+
naming the payload's path.
139+
127140
## 6. What belongs somewhere else
128141

129142
| Need | Where it goes |

0 commit comments

Comments
 (0)