Skip to content

Commit e28a635

Browse files
committed
docs: runner on hosted targets, not-run reporting, [xlings] values per host platform (#544)
1 parent be0beee commit e28a635

10 files changed

Lines changed: 333 additions & 2 deletions

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 |

docs/zh/05-mcpp-toml.md

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -952,7 +952,7 @@ linkage = "static"
952952
|---|---|---|
953953
| **裸 OS 别名** | 单个 OS / 族 —— 简洁且常用的形式 | `[target.windows]``[target.unix]` |
954954
| **`cfg(...)` 谓词** | 复合条件(arch / env / 组合子) | `[target.'cfg(all(linux, not(arch = "aarch64")))']` |
955-
| **精确三元组** | 某个具体目标(同时承载 `toolchain` / `linkage`) | `[target.x86_64-linux-musl]` |
955+
| **精确三元组** | 某个具体目标(同时承载 `toolchain` / `linkage` / `sysroot` / `runner`,见 §2.7.3) | `[target.x86_64-linux-musl]` |
956956

957957
一个选择器可以承载平台条件的**依赖****构建 flag**:
958958

@@ -1122,6 +1122,63 @@ mcpp **刻意不提供默认 runner**。用哪个模拟器、哪个机器型号
11221122
镜像用 `-bios none -semihosting`)—— 引擎一旦猜一个,另一块板就得跟它打架。板级
11231123
支持包通常会提供它。
11241124

1125+
### 2.7.3 hosted 目标上的 `runner`(2026.9.2.1+)
1126+
1127+
`[target.<triple>].runner` 对每一个精确三元组生效,不限于裸机。一个 hosted 交叉产物
1128+
—— 在 x86_64 机器上构建的 `aarch64-linux-musl` —— 有的宿主能直接执行(binfmt_misc
1129+
注册了 qemu-user),有的宿主以 `Exec format error` 拒绝;属于哪一种是机器的性质,不是
1130+
三元组的性质。mcpp 不预测它:要么通过工程声明的 runner 执行产物,要么尝试直接执行并
1131+
报告内核的回答。
1132+
1133+
```toml
1134+
[target.aarch64-linux-musl]
1135+
runner = ["qemu-aarch64-static"]
1136+
```
1137+
1138+
规则对 `mcpp run``mcpp test` 相同:
1139+
1140+
- **声明了 runner 就使用它。** 其第一个元素由 mcpp 定位:先在 `[xlings] deps`(§2.13)
1141+
声明的每个载荷的 `bin/` 目录里找,再找 `PATH``PATH` 上的裸名会命中 xvm shim,而
1142+
shim 按当前 SubOS 而非按包作答;先查载荷,runner 才能直接写工程声明过的程序名。
1143+
- **声明的 runner 找不到或启动不了是错误**,错误里带程序名、搜索过的目录和 errno。
1144+
不回落到直接执行:让产物在另一个解释器下带着另一组参数运行,正是这个键要防止的
1145+
失败。
1146+
- **没有 runner 且内核拒绝产物:** `mcpp run` 报告拒绝原因与应当写的键,退出码 2。
1147+
`mcpp test` 把每个测试报告为未运行,原因只打印一次,退出码 2(§2.7.3.1)。
1148+
- **`--no-runner`** 直接执行产物并忽略声明的 runner。它陈述的是关于本机的事实 ——
1149+
这个三元组在本机是原生的 —— 清单没有承载它的轴;为 x86_64 开发者写的 runner 在
1150+
aarch64 机器上仍可用。
1151+
1152+
通过 `[xlings] deps` 装模拟器是 CI 任务或单一宿主类别工程的形态。索引里的
1153+
`qemu-user-aarch64` 只为 x86_64 Linux 构建,而 `[xlings] deps` 在每台构建本工程的
1154+
宿主上都会 provisioning,所以条目按平台写(§2.13):
1155+
1156+
```toml
1157+
[xlings]
1158+
deps = [{ linux = "qemu-user-aarch64" }]
1159+
1160+
[target.aarch64-linux-musl]
1161+
runner = ["qemu-aarch64-static"]
1162+
```
1163+
1164+
宿主装不了的包是硬构建错误,所以不带平台形式的条目会让工程在 macOS 与 Windows 上
1165+
无法构建。同样没有这个包的 Linux/aarch64 宿主传 `--no-runner`
1166+
1167+
#### 2.7.3.1 `mcpp test` 与未运行的测试
1168+
1169+
产物在本机无法执行的测试既没有通过也没有失败。`mcpp test` 把它报告为**未运行**,
1170+
在确立原因时打印一次,在汇总里重复原因的第一行,退出码 2:
1171+
1172+
```
1173+
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
1174+
smoke ... not run
1175+
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)
1176+
```
1177+
1178+
退出码 1 含义不变 —— 有测试运行并失败;0 表示每个测试都运行并通过。
1179+
`--message-format json` 在每条记录上带 `"status":"not_run"``reason`,在汇总记录上
1180+
`not_run` / `not_run_reason`(见 [11 —— 机器可读输出](11-machine-output.md))。
1181+
11251182
### 2.8 `[features]` —— Feature(Cargo 风格,可加性)
11261183

11271184
#### 表形式 —— 让 feature 贡献的不止是隐含 feature
@@ -1555,6 +1612,24 @@ OPENBLAS_NUM_THREADS = "1"
15551612
host 工具(`make`/`cmake`/`protoc`…)、按项目固定工具版本、或设构建期环境变量——无需手改
15561613
`.xlings.json``[toolchain]`(§2.7)仍是编译器的便捷简写;`[xlings.workspace]` 是其通用形式。
15571614

1615+
**按宿主平台取值(2026.9.2.1+)。** `deps` 的一个条目与 `[xlings.workspace]` 的一个值可以是
1616+
按平台为键的表,即 xlings 自身 `.xlings.json``workspace` 接受的形式:
1617+
1618+
```toml
1619+
[xlings]
1620+
deps = ["xim:ninja", { linux = "qemu-user-aarch64" }, { windows = "nasm", default = "yasm" }]
1621+
1622+
[xlings.workspace]
1623+
gcc = { linux = "15.1.0" }
1624+
llvm = { macos = "20", default = "22" }
1625+
```
1626+
1627+
键为 `linux``macos``windows``default`;`macosx` 作为 xlings 对 `macos` 的拼写也被
1628+
接受。mcpp 在加载清单时按运行它的宿主解析这张表:宿主对应的键优先,`default` 兜底,两者
1629+
都没有时该条目在本宿主上不作声明 —— 是缺席,不是空值。未知的键是错误,不是被丢弃的条目。
1630+
这条轴只到宿主操作系统:一个包存在于该 OS 但不存在于该架构时,在那台宿主上仍是
1631+
provisioning 错误。
1632+
15581633
`subos` 选择根项目用于 build/run 的**本地开发 OS 环境**。未声明该键时固定使用 mcpp 已初始化、
15591634
经 release 验证的 `McppDefault`;`subos = "default"` 则仍是显式的
15601635
`NamedSubos("default")`。没有 CLI/环境变量 override,也不会隐式跟随 xlings active/current。

docs/zh/11-machine-output.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,47 @@ mcpp why toolchain [--target <triple>] [--toolchain <spec>] --format json
316316
`network``write-global-cache``exec-build-script`:答案来自与构建同一次的
317317
解析,而那可能拉取包、安装载荷、并运行某个依赖的构建程序。客户端是在**运行之前**
318318
读这张表来决定放不放行的,漏报一项就是一句不成立的安全承诺。
319+
320+
### `mcpp test --message-format json` —— 测试流
321+
322+
```
323+
mcpp test [pattern] [--workspace] --message-format json
324+
```
325+
326+
这条流早于 §2 的信封,也不被信封包裹:它是 NDJSON,每个测试结束时一条记录,随后每个
327+
成员一条汇总记录。`--workspace` 运行以一条 `workspace_summary` 记录结束。§6 的保证
328+
对它同样成立 —— 字段只增不减,字段含义不变 —— 下表是 2026.9.2.1 时的契约。
329+
330+
每个测试:
331+
332+
| 字段 | |
333+
|---|---|
334+
| `member` | workspace 成员;workspace 之外为 `""` |
335+
| `test` | 按路径命名的测试名(`tests/00-a/0.cpp``00-a/0`) |
336+
| `status` | `pass``compile_fail``run_fail``not_run` |
337+
| `exit_code` | 测试的退出状态;`not_run` 时为 `0` |
338+
| `signal` | 状态编码了信号时是信号号,否则 `null` |
339+
| `duration_ms` | 这个测试构建+运行的墙钟时间 |
340+
| `timed_out` |`--timeout` 杀掉时为 `true`(`run_fail`) |
341+
| `compile_output``run_output` | 捕获的诊断输出 |
342+
| `reason` |`not_run`:一句话说明原因;其余为 `""` |
343+
344+
汇总记录 `{"summary": {...}}`:
345+
346+
| 字段 | |
347+
|---|---|
348+
| `member``passed``failed` | 计数 |
349+
| `not_run` | 已构建但没有执行的测试数 |
350+
| `not_run_reason` | 它们共同的原因,或 `""` |
351+
| `elapsed_ms``build_ms``run_ms` | 墙钟时间,分段 |
352+
353+
⚠️ **`not_run` 既不是 `pass` 也不是 `run_fail`,退出码也这么说(2026.9.2.1)。**
354+
本机无法加载测试产物(交叉目标未声明 runner 时的 `Exec format error`),或声明的
355+
`[target.<triple>].runner` 找不到、启动不了时,测试为 `not_run`。这是关于整次调用的
356+
事实:确立一次,其余测试直接报告为 `not_run` 而不再启动,进程以 **2** 退出。退出码 1
357+
含义不变 —— 有测试运行并失败;0 表示每个测试都运行并通过。只读退出码判 pass/fail 的
358+
客户端必须处理 2;由 `failed == 0` 推断「全部通过」的客户端还必须读 `not_run`
359+
360+
`workspace_summary` 增加 `tests_not_run`(各成员之和)与 `unrunnable_members`(所有
361+
测试都 `not_run` 的成员),与既有的 `not_run` 列表并列;后者仍然指
362+
`--workspace-timeout` 到达时尚未开始的成员。

0 commit comments

Comments
 (0)