Skip to content

Commit ece01a2

Browse files
feat(device): 具名 runner、工具档位、--locked 断言与 emit sbom (2026.9.4.2) (#551)
* feat(device): runner gains three siblings, --locked asserts, and mcpp sbom Four axes a project is asked about before it is adopted, and none of them had an answer: how the artefact reaches a device, whether the build is reproducible, what went into it, and whether an emulator and real silicon are one package or two. ## The device slots `run`, `flash`, `monitor` and `debug` are one shape — an argv the BOARD knows and a TOOL performs, addressed absolutely, with the artefact appended or substituted for `{}`. `runner` has carried that shape since 2026.8.19; three special-case commands would have carried it three more times. So the slot is the parameter: one directive-table row each, one reader, one CLI shape. ⚠️ What no argv can express is which of them ENDS. `run` and `flash` finish and hand back a verdict; `monitor` and `debug` have no natural end, so a live process is success for them and a hang for the other two. `Semantics` answers that from the slot, because `openocd -c "program … exit"` and `openocd -c "init"` are spelled alike up to the argument the board chose. `debug` starts a SERVER and stops there. The client is the user's debugger or their IDE, which reaches mcpp through docs/11; driving it would put mcpp in the middle of a session it has nothing to add to. ⚠️ `runner-exclusive` is the first thing a physical board needed that an emulator never did. `mcpp test` runs binaries on a worker pool; one probe on one device is a mutex, and two workers reaching for it do not fail — they interleave, and the verdict is about neither test. The board knows this about itself, so it says so once and no project remembers `-j1`. ## Emulator or hardware is a feature, not a fork A board reached through QEMU and the same board reached through a probe differ in the argv of their device slots and nothing else. Publishing two packages would duplicate a linker script, startup code and a module surface to vary four strings. `mcpp::has_feature()` already existed, so this needed no engine work at all — it is what the layering was for. e2e 333 builds one board package and drives it both ways. ## --locked The lock has always been written after resolution and never read back; its own header said so. This makes it an ASSERTION rather than a pin: the resolution that happens must equal the one recorded, and a difference names the package that moved and both versions. That is the half reproducibility needs first, and it is what Cargo's flag of the same name means. ⚠️⚠️ And it must not meet the fast path. Measured before that guard existed: a deliberately corrupted lock passed `mcpp build --locked` and printed "Finished" — the flag accepted, the build correct, the assertion never run. A criterion that is skipped is worse than one that is absent, because the green reads as a verification. ## mcpp sbom CycloneDX 1.5 over the recorded resolution. Everything a bill of materials names is already in mcpp.lock, so this is an output format rather than a mechanism: it resolves nothing and asks the network for nothing. ⚠️ It reads the lock rather than re-resolving, which is the one property such a document must have — an SBOM describing a different graph from the one that was built is worse than none. Asserted in e2e 333 by editing the lock and checking the output follows it. An unknown licence is emitted as NOASSERTION rather than omitted: an absent key reads as "not examined", and a reviewer cannot filter on silence. ## Two propagation sites, and the one that was missed first Dependency-supplied RunGlobal entries reach the root through a different path from a package's own directives. Wiring only `apply()` left `mcpp flash` reporting "no flash is configured" while `mcpp run` found the runner the same build program emitted three lines away — measured. Both sites now iterate the slot table instead of naming `runner`. Protocol version 6. 97/97 unit tests; freestanding e2e 130-139 and 332 green. * docs: the device layer and the compatibility commitment (2026.9.4.2) docs/18 specifies the four device actions, why termination is a property of the slot rather than of the argv, and why the emulator/hardware choice is a feature of one board package rather than two packages. docs/19 states what a project's own review process asks and cannot currently cite: which releases are supported, which surfaces are stable, and which — build fingerprints, cache layout, target/ — are explicitly not interfaces. Both mirrored in Chinese, per this repository's convention. The plan document records the second round: two findings the plan did not predict (a second propagation path for dependency-supplied RunGlobal entries, and two fast paths that would have made the new slots and --locked silently vacuous), and the six-axis reading that follows. * refactor(runner): runners have names, and the engine knows none of them Replaces the four hardcoded device slots of the previous commit. Three constraints, applied in order, produced this shape: 1. A top-level command must be usable in every domain. 2. The default must cover the common case; options carry the rest. 3. The core carries the general framework; the rest is configuration. The previous design failed all three. `mcpp flash` / `monitor` / `debug` were dead commands in any project that is not firmware, and they put embedded vocabulary into the engine: a web package could not add `serve`, nor a cluster package `submit`, without an engine release. Measured before this change: `flash`, `monitor` and `debugger` appeared 56 times across 8 engine files, and a fifth action would have touched nine places — a cost this commit's predecessor quoted in its own message and then paid four times. ⭐⭐ AND THE SECOND CONSTRAINT CAUGHT THE DEEPER ERROR. On real hardware, running a program IS writing it, resetting, attaching and reading the exit status — `probe-rs run` is one command, exactly as `qemu-system-* -kernel` is. The two are one action in two environments, not two actions. The previous design required `mcpp run --runner flash` there, making the most common thing a developer does the one needing an extra argument. So a board's `hardware` feature moves the DEFAULT runner, and the command does not change between an emulator and a board. Named runners serve what remains: writing without running, observing a console, a debug server, erasing. The engine now knows only that named runners exist. The name is data, carried in the value of one directive, and `flash`, `serve`, `submit` and `logcat` cost it the same: nothing. ⭐ Termination is declared rather than inferred, because no argv can express it and the engine has no list of names to infer from: `openocd -c "program … exit"` terminates, `openocd -c "init"` does not, and the two are spelled alike up to the argument the package chose. `runner-exclusive` becomes `run-exclusive`: the property is that this target's runs cannot overlap, which holds for one board on one probe, one GPU, one serial port and a single-seat licence alike. Nothing about it is a device. `mcpp sbom` becomes `mcpp emit sbom`. `emit` already meant "generate a document describing this project" and already carried `-o`; a separate top-level command was a second spelling of an abstraction that existed. That was the same mistake as the first one, made twice in a day: adding a command instead of extending an abstraction. ⭐ The unit test that quantifies over the directive table caught a real defect here: the table's declared size still said 20 after a row was removed, leaving a default-constructed entry with an empty wire name. 97/97 unit tests; e2e 130-139, 332 and the rewritten 333 green. * fix(runner): a tool declared by a DEPENDENCY is reachable by bare name ⚠️⚠️ THE CASE THE FEATURE EXISTS FOR WAS THE ONE THAT DID NOT WORK. `runner_lookup` (#544) lets a runner name its program without writing a payload's home-and-version path into a manifest. But the directories it searched were collected from `runtimeOwnerManifest.xlings.deps` — the ROOT project's declarations alone. So the bare name resolved when the CONSUMER declared the tool, and failed when the board-support package did. That is backwards. A board package is precisely the thing that knows which emulator or probe reaches its machine; requiring the consumer to declare it as well is the duplication the board package exists to remove. Measured on `mcpplibs/aarch64-virt-rt` with its runner reduced to the bare name `qemu-system-aarch64`: `mcpp run --target aarch64-none-elf` searched PATH, did not find it, and reported a missing runner — while the emulator sat installed in the payload the board had declared two lines above. With this change the same example boots and prints. The collection now spans every package in the graph, root first: a consumer that declares its own payload still decides, and a dependency answers when the consumer said nothing. A payload declared but not installed contributes nothing and the lookup continues to PATH, unchanged. ⭐ This is what makes the board-package simplification real. Naming the program replaces `mcpp::xpkg_dir` + `std::format` + a conditional + a `mcpp::warning` fallback — eleven lines — and it DELETES a failure mode rather than moving it: `xpkg_dir` answers empty for anyone building from a checkout, so the old shape configured no runner and needed an advisory to explain why. There is nothing to explain when the lookup itself reports which directories it searched. `tests/e2e/334` covers it, and asserts the half that matters more than resolution: a declared runner whose program is missing is an ERROR, never a fallback to executing the artifact on the build host. 97/97 unit; e2e 130-139, 332-334 green. * docs(plan): the named-runner design, and what implementing it found Two findings the design did not predict, both of which only appear when the thing is built: ⭐⭐⭐ `xlingsDepBinDirs` was collected from the ROOT manifest alone, so a board package naming its emulator by bare name did not resolve — the exact case the simplification exists for, and backwards from what it should be. ⭐⭐ openarch has a primitive for "switch to another saved context" and none for "switch the context this trap will return to". Every architecture needs the second in order to preempt. `examples/switch` could not have found it: it never enters a trap. * docs(plan): v8 — the four decisions, and the two publish orders that run opposite §15 records what was settled, including that batch 1's engine interfaces become a compatibility contract on release, so their shape is frozen before it merges. §16 re-orders the batches with picolibc parallel rather than queued, and writes down a pair I had not stated together: * the board packages must wait for the ENGINE to publish, because the bare-name lookup they now rely on ships with it — the mirror of "consumers publish first"; * picolibc must publish before the cortex-m-rt version that names it, because a version reference cannot resolve to an unpublished package — the ordinary case of the same rule. ⭐ Which is what the libc feature buys: cortex-m-rt 0.1.0 ships on the zero-libc tier waiting for nobody, and gains the feature at 0.2.0. The near tier is not held behind the far one. * feat(xlings): tools gain a tier, and a dependency's declaration is installed ## Two halves of one defect `[xlings.workspace]` had one list. A board-support package naming both an emulator (to run) and a debug probe (to reach hardware) installed both for every consumer, including one that only wanted the library to compile — while package dependencies have had `[dependencies]` / `[build-dependencies]` / `[dev-dependencies]` since the beginning. [xlings.workspace] "xim:qemu-arm" = "9.2.4-1" # unchanged behaviour "xim:probe-rs" = { version = "0.24.0", when = "run" } [feature-xlings.hardware] "xim:probe-rs" = "0.24.0" # never fetched otherwise | when | installed by | reaches a consumer | |---|---|---| | (omitted) | every verb that builds | yes | | build | every verb that builds | yes | | run | `mcpp run`, `mcpp test` | yes | | dev | only the package that declared it, as the root | no | Omitting `when` is the pre-2026.9.4.2 behaviour exactly, so no manifest changes. The tier rides the entry rather than a second table, the shape `[dependencies]` already uses; `[feature-xlings.<f>]` is `[feature-deps.<f>]`'s spelling. ## ⚠️⚠️ And provisioning now spans the graph, because looking did already The previous release extended the runner's bare-name LOOKUP to every package in the graph and left provisioning at the root. That is a search of directories nothing installed into — a lookup that can only fail. Both are one expression now, so they cannot drift. Provisioning happens in two passes for an ordering reason: the root's list is needed before the graph exists (a toolchain may depend on it), and a dependency's is not known until resolution. The second pass provisions only what the first did not, under its own stamp, so an unchanged project still pays for no xlings round trip. ## ⚠️ The hazard a tier introduces, and the gate for it `mcpp build` installs less than `mcpp run` needs, and the run fast path exists precisely to skip the pass that would install the difference. The build cache records whether a build left a run-tier tool unprovisioned; `try_fast_run` declines such an entry, exactly as it declines one that records a runner. ## The criterion tests what mcpp REQUESTS, not what got installed Verifying a tier by installing needs a clean machine and a network. It is also the wrong object: what the tier changes is the SET, and `MCPP_NO_AUTO_INSTALL=1` refuses to provision while naming exactly that set. `tests/e2e/335` reads both commands on one project, so the criterion carries its own denominator — an mcpp that provisioned nothing, or a manifest that never parsed, fails the `run` half. ## Discoverability `mcpp why runners` lists what this project supplies, beside everything else it reports resolving; `mcpp run --list-runners` is the same read alone. No stamped one-time hint: a cache hit does not re-run, so it would be silent on the build that needed it most. 97/97 unit (5 new); e2e 88, 130-131, 327, 332-335 green. * feat(target): armv7-a, the first 32-bit row with a memory management unit Every other 32-bit row in the table is M-profile: an MPU that describes regions by base and limit, and no page-table entry at all. A-profile has a real MMU with a walker, so `armv7a-none-eabi` / `-eabihf` are the first targets on which an address-space abstraction can be asked what a 32-BIT machine's entry looks like — short descriptors are 32 bits wide, long (LPAE) ones 64. openarch's layer has never been able to put that question to a 32-bit machine. Both rows are `verified`, and the column records what was RUN. Measured 2026-09-04 under `xim:qemu-arm@9.2.4-1`: each built an image that booted on `-M virt -cpu cortex-a15`, printed over semihosting and reported its exit status. ## ⚠️ The soft row needs `-mfpu=none`, measured on THIS architecture `armv7-a` is not `thumbv7em`, so the M-profile measurement says nothing about it. Measured on llvm 22.1.8, the same float multiply: 1 VFP instruction under the soft-float ABI, 0 with `-mfpu=none`. ## ⚠️ And the semihosting exit call is spelled differently `SYS_EXIT` (0x18) on AArch32 takes the reason code in `r1` DIRECTLY; the `{reason, code}` block every Cortex-M board here passes is `SYS_EXIT_EXTENDED` (0x20), which exists because a 32-bit `r1` cannot carry both. Measured: passing the block to 0x18 prints correctly and reports the WRONG exit status — a program exiting 0 reported 1. `tests/e2e/336` asserts the status and not only the output, and takes it from qemu rather than from the tail of a pipeline. ## ⭐ The unit test's predicate was a spelling, not a property `SoftFloatMProfileRowsDisableTheFpu` filtered on `starts_with("thumb")`. The rule it states applies to every 32-bit ARM row with a float-ABI suffix, so adding these two left the rule in force and the test SILENTLY not covering them: the loop skipped the new rows and every assertion still passed. The predicate now asks the property, and the test counts A-profile rows so the denominator cannot go back to zero unnoticed. 97/97 unit; e2e 332, 336 green. * ci: 336 runs on the one job that has qemu-arm, and its PASS line is demanded 336 declares `# requires: qemu-arm`, which no sharded runner has, so on a shard it exits 0 without running a single row. Adding it to the loop and not to the assertions below would have been that defect one layer in — the same shape the comment above 332 already records. Both the PASS line and a row count are demanded, because a fixture that stopped iterating still prints its PASS line. * test(matrix): the two ARMv7-A rows, on every host the table declares ⚠️⚠️ THE SUPPORT MATRIX IS A DECLARATION, AND ADDING A TARGET ROW WITHOUT ADDING IT HERE IS A RED SCAN RATHER THAN A SILENT GAP — which is what the table is for. `scan (linux-aarch64)` reported the two new targets as measured cells with no expectation. Twelve cells: four hosts × the compilers each declares. The shape follows the M-profile rows exactly, because the answer is the same one — llvm serves them from a payload that needs no per-host cross build, and every gcc/msvc cell is a `capability-pin` refusal. Measured locally on linux-x86_64: 42/42 payload and 16/16 graph cells match. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent fd1a539 commit ece01a2

41 files changed

Lines changed: 3633 additions & 293 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 商业级可用:mcpp × xlings 的裸机与嵌入式总体方案
22

3-
2026-09-04 · 多仓库总体方案 · **v4:P0 引擎切片已实施**(PR #550);其余批次待实施
3+
2026-09-04 · 多仓库总体方案 · **v5:P0 引擎切片 + B/C/D/E 四轴已实施**(PR #550#551)
44

55
前置讨论:
66
[`2026-08-21-baremetal-ecosystem-assessment.md`](2026-08-21-baremetal-ecosystem-assessment.md)(七角度评估) ·
@@ -471,3 +471,42 @@ clang 对一次 float 乘法仍发出 `vmul.f32` —— 在没有 FPU 的 Cortex
471471
⚠️ **软浮点行在没有 builtins 时链接不了浮点代码**(实测:`undefined symbol:
472472
__aeabi_fmul`),这正是 §3.1.1 把 `compiler-rt-builtins` 与 C 库并列为 P0 的理由。
473473
整数程序不受影响 —— e2e 332 的四行启动用例即为整数程序。
474+
475+
---
476+
477+
## 11. 第二轮实施(2026-09-04,PR #551)
478+
479+
### 11.1 六轴读数的变化
480+
481+
|| v4 | 现在 |
482+
|---|---|---|
483+
| **A 覆盖** | 🟡 引擎能编七行 | 🟡 不变(C 库源码包与板级包仍未做) |
484+
| **B 可信** | ❌ 零真机 | 🟢 **模拟器与真机成为同一个包的两个 feature**;真机路径可声明、可解析、判据齐备,尚无实机运行记录 |
485+
| **C 闭环** | ❌ 三个槽都没有 |`flash`/`monitor`/`debug` + `runner-exclusive`,四槽一读点 |
486+
| **D 可复现** |`--locked` 不存在 |`--locked`/`--frozen` 断言并点名漂移;关掉快路径以免空转 |
487+
| **E 可交付** | ❌ 全空白 | 🟢 `mcpp sbom`(CycloneDX 1.5)+ `docs/19` 支持窗口;许可闭包门与离线快照仍未做 |
488+
| **F 可扩展** || ✅ 未受损:新板 = 新包,引擎 diff 为零 |
489+
490+
### 11.2 ⭐⭐ 方案 §2.2 的判断被实施证实,§2.3 的被加强
491+
492+
* **两值语义是对的。** `debug` 起服务端、客户端归 IDE 这条边界成立,`debug`
493+
`monitor` 在实现里逐字段同形,没有出现方案担心的「会话协议」。
494+
* **`runner-exclusive` 比方案写的更必要。** 方案说它是「第一块真板挖出的一列」;
495+
实施时发现它还必须**只紧不松** —— 图里任何一个包知道设备是互斥的,它就是互斥
496+
的,后来的包保持沉默不得放松它。
497+
498+
### 11.3 ⚠️ 实施挖出的、方案没有的两条
499+
500+
1. **一条规则的第二份拷贝。** 依赖提供的 RunGlobal 条目抵达根工程走的是与
501+
`apply()` **不同**的路径(`prepare.cppm` 的 BFS 之后)。只接了前者时,
502+
`mcpp flash` 报「没有配置」而 `mcpp run` 找得到同一个构建程序发出的 runner。
503+
两处现在都遍历槽表。
504+
2. **快路径会让新槽与 `--locked` 双双空转。** `try_fast_run` 直接 exec 缓存产物,
505+
于是 `mcpp flash` 打印 `Running target/…/bin/p`;`try_fast_build` 跳过解析,
506+
于是被改坏的锁通过了 `--locked`。两处都按**性质**设闸(槽是不是 run、是不是
507+
要求断言),不是按旗标。
508+
509+
### 11.4 仍未做
510+
511+
`mcpplibs/picolibc` + `compiler-rt-builtins` 源码包 · 三个板级包 · `xim:probe-rs` ·
512+
真机 CI · 许可闭包门(`--deny-license`)· 离线整仓快照 · openarch 第四后端(P3)。

.agents/docs/2026-09-04-named-runners-and-the-universal-command-surface.md

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

.github/workflows/ci-linux-e2e.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ jobs:
188188
tests/e2e/131_freestanding_bsp_supplies_everything.sh \
189189
tests/e2e/132_freestanding_test_and_artifacts.sh \
190190
tests/e2e/133_freestanding_std_subset.sh \
191-
tests/e2e/332_cortex_m_builds_and_boots.sh; do
191+
tests/e2e/332_cortex_m_builds_and_boots.sh \
192+
tests/e2e/336_armv7a_builds_and_boots.sh; do
192193
echo "=== $t ==="
193194
bash "$t" 2>&1 | tee "$(basename "$t").log"
194195
rc=${PIPESTATUS[0]}
@@ -221,6 +222,15 @@ jobs:
221222
booted=$(grep -c 'booted on ' 332_cortex_m_builds_and_boots.sh.log || true)
222223
[ "$booted" = "4" ] || {
223224
echo "332 booted $booted rows, expected 4"; exit 1; }
225+
# ⚠️ 336 IS HERE FOR THE SAME REASON, AND ADDING IT TO THE LOOP ABOVE
226+
# WITHOUT ADDING IT HERE WOULD HAVE BEEN THE SAME DEFECT: it declares
227+
# `# requires: qemu-arm`, so on a shard it exits 0 having run nothing.
228+
grep -q 'PASS: armv7-a rows build, boot' \
229+
336_armv7a_builds_and_boots.sh.log || {
230+
echo "336 (armv7-a) skipped on the runner that must run it"; exit 1; }
231+
a32=$(grep -c 'booted on virt' 336_armv7a_builds_and_boots.sh.log || true)
232+
[ "$a32" = "2" ] || {
233+
echo "336 booted $a32 rows, expected 2"; exit 1; }
224234
225235
# ──────────────────────────────────────────────────────────────────
226236
# Hermetic (no host toolchain): the ONLY environment class that

CHANGELOG.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,117 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [2026.9.4.2] — 2026-09-04
7+
8+
runner 有了名字,工具有了档位,`--locked` 成为断言,`mcpp emit sbom`
9+
10+
### ⭐⭐ 一条命令,加具名的例外
11+
12+
`mcpp run` 覆盖常见情形的**全部,真实硬件也一样**。在设备上「运行一个程序」意味着
13+
写进去、复位、接上输出、读回退出状态 —— 这是**一条**命令(`probe-rs run`
14+
`qemu-system-* -kernel`),不是几条。板级包把它作为**默认** runner,于是开发者从
15+
模拟器换到真板时,**敲的命令不变**
16+
17+
```bash
18+
mcpp run # 默认;模拟器与真板同一条
19+
mcpp run --runner flash # 具名的例外:只写不跑、看串口、起调试服务端、擦片
20+
mcpp run --list-runners # 这个工程提供了哪些
21+
```
22+
23+
⚠️ **引擎不认识任何 runner 名字。** `flash``serve``deploy``submit`
24+
`logcat` 对它一样陌生。**引擎里若有一份固定的名字表,就等于由引擎决定哪些领域可被
25+
表达** —— 一个 web 包将无法自己加 `serve`
26+
27+
**写程序名,不要写路径。** mcpp 先找本包 `[xlings] deps` 声明的载荷 `bin/`,
28+
再找 `PATH`。用 `xpkg_dir` 拼绝对路径是多余的,而且引入了一个失败模式:声明不是
29+
安装,查询返回空则没有配置任何 runner 而无话可说。
30+
31+
⚠️ 是否终止由 `mcpp::runner_longlived(name)` **声明**:
32+
`openocd -c "program … exit"` 会终止而 `openocd -c "init"` 不会,拼写到最后一个
33+
参数为止都一样,没有任何 argv 能表达这个区别。
34+
35+
`mcpp::run_exclusive()` 陈述「这个目标的运行不能重叠」—— 对一块板、一张 GPU、
36+
一个串口、一个单席位 license 同样成立,`mcpp test` 据此串行化。
37+
38+
### `--locked`
39+
40+
锁一直是解析之后写、从不读回。现在它是断言:发生的解析必须等于记录的解析,不等则
41+
**点名移动了的包与两个版本**
42+
43+
⚠️⚠️ **它绝不能遇上快路径。** 实测:加闸之前,一份被故意改坏的锁通过了
44+
`mcpp build --locked` 并打印 `Finished` —— 旗标被接受、构建正确、**断言从未跑到**
45+
46+
### `mcpp emit sbom`
47+
48+
CycloneDX 1.5,覆盖**已记录**的解析。⚠️ 读锁而不是重新解析 —— 一份描述了与所构建
49+
者不同的图的文档比没有更糟。归在 `emit` 之下而不是新开一级命令:`emit` 已经是
50+
「生成描述本工程的文档」。
51+
52+
### ⭐⭐ 工具有了档位,而且依赖声明的工具现在真的会被装
53+
54+
包依赖从一开始就有 `[dependencies]` / `[build-dependencies]` / `[dev-dependencies]`
55+
这条轴,工具只有一张表。一个同时点名模拟器与调试探针的板级包,会把两个都装给每一位
56+
消费者,包括只想把库编出来的那一位。
57+
58+
```toml
59+
[xlings.workspace]
60+
"xim:qemu-arm" = "9.2.4-1" # 不写就是从前的行为
61+
"xim:probe-rs" = { version = "0.24.0", when = "run" }
62+
63+
[feature-xlings.hardware]
64+
"xim:probe-rs" = "0.24.0" # 不要这个 feature 就永不下载
65+
```
66+
67+
| `when` | 由谁安装 | 传播到消费者 |
68+
|---|---|---|
69+
| *(不写)* | 每个构建命令 ||
70+
| `build` | 每个构建命令 ||
71+
| `run` | `mcpp run``mcpp test` ||
72+
| `dev` | 只有声明它的那个包作为根时 | **** |
73+
74+
**不写 `when` 保持今天的行为,所以没有迁移。**
75+
76+
⚠️⚠️ **同时:`[xlings.workspace]` 的供给扩到全图。** 在此之前只有根工程的声明会被
77+
安装,而查找(runner 按裸名找程序)已经跨全图 —— **在没有任何东西安装过的目录里
78+
查找,是只可能失败的查找**。两者现在由同一个表达式定义。
79+
80+
⚠️ 档位带来的一个危险已被堵上:`mcpp build` 装得比 `mcpp run` 需要的少,而 run 的
81+
快路径正是为跳过那一步存在的。构建缓存记下「这次构建留下了未安装的 run 档工具」,
82+
`mcpp run` 的快路径据此拒绝该条目 —— 与它拒绝声明了 runner 的条目同理。
83+
84+
判据不测「装成了没有」,而测**mcpp 要装什么**:`MCPP_NO_AUTO_INSTALL=1` 下拒绝供给
85+
**点名它本来要装的集合**,于是 `build``run` 两条命令的差集就是被测的性质,
86+
一次下载都不需要(`tests/e2e/335`)。
87+
88+
### ⭐ ARMv7-A:第一个带内存管理单元的 32 位目标行
89+
90+
`armv7a-none-eabi``armv7a-none-eabihf` 两行,`verified`。表里其余每个 32 位行都是
91+
M-profile:MPU 按基址与上限描述区域,没有页表项。A-profile 有真正的 MMU,于是它是第一个
92+
能被问「**32 位**机器的页表项长什么样」的目标 —— 这正是 openarch 的地址空间抽象从未被
93+
一台 32 位机器问过的问题。
94+
95+
实测 2026-09-04(`xim:qemu-arm@9.2.4-1`):两行都在 `-M virt -cpu cortex-a15` 上启动、
96+
经半主机打印并报回退出状态。
97+
98+
⚠️ **软浮点行同样需要 `-mfpu=none`,而这是在这个架构上重新实测的**,不是从 M-profile
99+
推过来的:`armv7a-none-eabi` 在软浮点 ABI 下对一次 float 乘法仍发出 VFP 指令。
100+
101+
⚠️ **半主机的退出调用与 M-profile 拼法不同。** AArch32 的 `SYS_EXIT`(`0x18`)把原因码
102+
**直接**放在 `r1`;Cortex-M 传的 `{reason, code}` 块是 `SYS_EXIT_EXTENDED`(`0x20`)。
103+
实测:把块传给 `0x18` 打印正确而**退出状态是错的**,只看输出的测试看不出来。
104+
105+
⭐ 那条量化 `-mfpu=none` 的单元测试,谓词曾是 `starts_with("thumb")` —— 一个**拼法**
106+
而不是它要陈述的性质。新行加进来时规则适用而测试**静默跳过**了它们,每条断言依然通过。
107+
谓词已改为「32 位 ARM」。
108+
109+
### 发现性
110+
111+
`mcpp why runners` 列出本工程提供的 runner,与其余解析结果并列;
112+
`mcpp run --list-runners` 是同一份读取,单独报告。
113+
114+
新增 `docs/18-devices.md``docs/19-supported-versions.md`(中英双份)。
115+
指令协议版本 6。
116+
6117
## [2026.9.4.1] — 2026-09-04
7118

8119
Cortex-M 落地为七个目标行,freestanding 链接开启死代码段消除。

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ the right toolchain payload is resolved and installed automatically.
386386
| `thumbv7m-none-eabi` | llvm 22 — Cortex-M3, bare metal ² ||
387387
| `thumbv7em-none-eabihf` | llvm 22 — Cortex-M4F/M7F, hard float ² ||
388388
| `thumbv8m.main-none-eabi` | llvm 22 — Cortex-M33/M55, soft float ² ||
389+
| `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 22 — Cortex-A 32-bit, bare metal ² ||
389390
| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 22 — builds and links; no emulator run recorded | 🔄 |
390391
| `riscv64-linux-musl` || 🔄 |
391392
| `aarch64-linux-gnu` || 🔄 |

docs/05-mcpp-toml.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,6 +1920,65 @@ triples say one, descriptors and xlings' project file say the other) and both
19201920
are accepted wherever a platform is named. A table with no key for this host
19211921
and no `default` declares nothing here.
19221922

1923+
#### `when` — which verbs need this tool (mcpp 2026.9.4.2+)
1924+
1925+
```toml
1926+
[xlings.workspace]
1927+
"xim:qemu-arm" = "9.2.4-1" # every build, as before
1928+
"xim:codegen" = { version = "1.0", when = "build" }
1929+
"xim:probe-rs" = { version = "0.24.0", when = "run" }
1930+
"xim:clang-tidy"= { version = "20", when = "dev" }
1931+
```
1932+
1933+
Package dependencies have had this axis since the beginning —
1934+
`[dependencies]`, `[build-dependencies]`, `[dev-dependencies]`. Tools had one
1935+
list, so a board-support package that named both an emulator and a debug probe
1936+
installed both for every consumer, including one that only wanted the library
1937+
to compile.
1938+
1939+
| `when` | Installed by | Reaches a consumer |
1940+
|---|---|---|
1941+
| *(omitted)* | every verb that builds | yes |
1942+
| `build` | every verb that builds | yes |
1943+
| `run` | `mcpp run`, `mcpp test` | yes |
1944+
| `dev` | only the package that declared it, as the root | **no** |
1945+
1946+
**Omitting `when` is the pre-2026.9.4.2 behaviour exactly**, so no manifest has
1947+
to change. Narrowing is optional; it is not a question an author has to answer.
1948+
1949+
`dev` is the only tier that does not propagate. It means *"while the package
1950+
that declared this is itself being developed"*, so a dependency's `dev` entry is
1951+
never installed for a consumer. Every other tier does reach one, which is the
1952+
point of a board package knowing its own machine: it declares the emulator once
1953+
and every consumer gets it.
1954+
1955+
The tier is written on the entry rather than as a second table, on the same
1956+
reasoning that makes `[dependencies]` accept both `dep = "1.0"` and
1957+
`dep = { version = "1.0", features = [...] }`. A scoped entry must name
1958+
`version` even to leave it empty (`version = ""` means *present, any version*),
1959+
because `{ when = "run" }` and a misspelt `version` key would otherwise be
1960+
indistinguishable.
1961+
1962+
#### `[feature-xlings.<feature>]` — a tool a feature needs
1963+
1964+
```toml
1965+
[features]
1966+
default = ["emulator"]
1967+
emulator = {}
1968+
hardware = {}
1969+
1970+
[feature-xlings.hardware]
1971+
"xim:probe-rs" = "0.24.0"
1972+
```
1973+
1974+
The same table, gated on a feature, spelled the way `[feature-deps.<feature>]`
1975+
is. A consumer who never asks for `hardware` never downloads a probe driver.
1976+
Entries here accept `when` exactly as the unconditional ones do.
1977+
1978+
A feature name no `[features]` table declares is reported as a schema warning:
1979+
it activates for nobody and installs nothing, and a tool whose absence is only
1980+
visible as *"the device is never reachable"* is the hardest kind to diagnose.
1981+
19231982
#### Which version a tool the project did not name resolves to
19241983

19251984
| The project declares | The version comes from |

docs/13-baremetal.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ covers the hosted link model this chapter departs from.
1313
## Overview
1414

1515
A freestanding target is a target whose `os` field is `none`. The target table
16-
at `modules/toolchain-model/src/triple.cppm` carries eleven of them:
16+
at `modules/toolchain-model/src/triple.cppm` carries thirteen of them:
1717

1818
| Triple | Tier | C library |
1919
|---|---|---|
@@ -28,10 +28,30 @@ at `modules/toolchain-model/src/triple.cppm` carries eleven of them:
2828
| `thumbv8m.base-none-eabi` | preview | none by default — Cortex-M23 |
2929
| `thumbv8m.main-none-eabi` | verified | none by default — Cortex-M33/M55, soft float |
3030
| `thumbv8m.main-none-eabihf` | preview | none by default — Cortex-M33F/M55F, hard float |
31+
| `armv7a-none-eabi` | verified | none by default — Cortex-A 32-bit, soft float |
32+
| `armv7a-none-eabihf` | verified | none by default — Cortex-A 32-bit, hard float |
3133

3234
`verified` means an image has been built **and run** for the row. `preview`
3335
means it builds and links, and no emulator run has been recorded.
3436

37+
### ARMv7-A is the first 32-bit row with a memory management unit
38+
39+
Every other 32-bit row above is M-profile: an MPU that describes regions by base
40+
and limit, and no page-table entry at all. A-profile has a real MMU with a
41+
walker, so it is the first target on which an address-space abstraction can be
42+
asked what a *32-bit* machine's entry looks like — short descriptors are 32 bits
43+
wide, long (LPAE) ones 64. That question cannot be put to a machine with no
44+
entries, which is why `openarch`'s Cortex-M backend declines the capability.
45+
46+
**The semihosting exit call is not spelled the way M-profile spells it.**
47+
`SYS_EXIT` (`0x18`) on AArch32 takes the reason code in `r1` *directly*; the
48+
`{reason, code}` block a Cortex-M board passes is `SYS_EXIT_EXTENDED` (`0x20`),
49+
which exists because a 32-bit `r1` cannot carry both. Measured: passing the
50+
block to `0x18` prints correctly and then reports the wrong exit status, so a
51+
board that only checks its output cannot see the difference. This is a *board*
52+
fact rather than a target fact; it is recorded here because it is where the next
53+
person writing such a board will look.
54+
3555
### M-profile is seven rows rather than one
3656

3757
Every other bare-metal family above is one row per architecture. Cortex-M is

docs/17-the-project-environment.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,12 @@ exists for one host platform only is therefore declared for that platform
151151
on Linux and nothing elsewhere. The keys and the resolution rule are in
152152
chapter 5, §2.13.
153153

154-
**The runner.** A program under `[xlings] deps` is also where
154+
**Which verbs install it.** An entry may name a tier —
155+
`{ version = "0.24.0", when = "run" }` — and a `[feature-xlings.<feature>]`
156+
table gates one on a feature. A tool the project will not use is then not
157+
downloaded: chapter 5, §2.13. Omitting the tier is the historical behaviour.
158+
159+
**The runner.** A program under `[xlings.workspace]` is also where
155160
`[target.<triple>].runner` looks first for its first element, before `PATH`
156161
(chapter 5, §2.7.3). The two keys together provision a user-mode emulator on a
157162
CI host and execute a cross-built artifact through it, without the manifest
@@ -164,7 +169,8 @@ naming the payload's path.
164169
| a library the program links | `[dependencies]` |
165170
| the compiler | `[toolchain]`, chapter 3 |
166171
| a host tool a dependency produces | `tools = [...]`, chapter 7 |
167-
| a tool present in the environment | `[xlings] deps` |
172+
| a tool present in the environment | `[xlings.workspace]` |
173+
| a tool only one verb or one feature needs | `when = "run"`, `[feature-xlings.<f>]` |
168174
| which environment | `[xlings] subos` |
169175

170176
## 7. Related chapters

0 commit comments

Comments
 (0)