Skip to content

Commit 3fc57dd

Browse files
committed
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.
1 parent eac311e commit 3fc57dd

15 files changed

Lines changed: 598 additions & 639 deletions

CHANGELOG.md

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,58 +5,52 @@
55
66
## [2026.9.4.2] — 2026-09-04
77

8-
`runner` 长出三个兄弟槽,`--locked` 成为断言,新增 `mcpp sbom`
8+
runner 有了名字,`--locked` 成为断言,`mcpp emit sbom`
99

10-
四件事在一个工程被采用之前会被问到,而它们此前都没有答案:产物如何抵达设备、
11-
构建是否可复现、里面装了什么、以及模拟器与真机是一个包还是两个。
10+
### ⭐⭐ 一条命令,加具名的例外
1211

13-
### 设备槽
12+
`mcpp run` 覆盖常见情形的**全部,真实硬件也一样**。在设备上「运行一个程序」意味着
13+
写进去、复位、接上输出、读回退出状态 —— 这是**一条**命令(`probe-rs run`
14+
`qemu-system-* -kernel`),不是几条。板级包把它作为**默认** runner,于是开发者从
15+
模拟器换到真板时,**敲的命令不变**
1416

15-
`run` / `flash` / `monitor` / `debug` 是同一种形状 —— **板知道、工具执行**的一段
16-
argv。`runner` 从 2026.8.19 起就承载着这个形状;再写三个特例命令就是把它再承载
17-
三遍。所以**槽成了参数**:指令表各一行、一个读点、一种 CLI 形状。
18-
19-
```toml
20-
[target.thumbv7em-none-eabihf]
21-
flash = ["probe-rs", "download", "--verify", "--chip", "STM32L475VG", "{}"]
17+
```bash
18+
mcpp run # 默认;模拟器与真板同一条
19+
mcpp run --runner flash # 具名的例外:只写不跑、看串口、起调试服务端、擦片
20+
mcpp run --list-runners # 这个工程提供了哪些
2221
```
2322

24-
⚠️ **没有任何 argv 能说出它们哪个会终止。** `run`/`flash` 跑完并交回判决;
25-
`monitor`/`debug` 没有自然终点,于是「进程还活着」对后者是成功、对前者是卡死。
26-
引擎从****读出这件事,因为 `openocd -c "program … exit"``openocd -c "init"`
27-
的拼写直到板所选的那个参数为止都一样。
23+
⚠️ **引擎不认识任何 runner 名字。** `flash``serve``deploy``submit`
24+
`logcat` 对它一样陌生。**引擎里若有一份固定的名字表,就等于由引擎决定哪些领域可被
25+
表达** —— 一个 web 包将无法自己加 `serve`
2826

29-
⚠️ **`runner-exclusive` 是第一件物理板需要而模拟器从不需要的事** 一块板一个探针
30-
是互斥锁,两个工作者去够它不会失败 —— 它们互相穿插。板自己说一次,工程永远不必
31-
记得 `-j1`
27+
**写程序名,不要写路径** mcpp 先找本包 `[xlings] deps` 声明的载荷 `bin/`,
28+
再找 `PATH`。用 `xpkg_dir` 拼绝对路径是多余的,而且引入了一个失败模式:声明不是
29+
安装,查询返回空则没有配置任何 runner 而无话可说
3230

33-
### 模拟器与硬件是一个 feature,不是一次分叉
31+
⚠️ 是否终止由 `mcpp::runner_longlived(name)` **声明**:
32+
`openocd -c "program … exit"` 会终止而 `openocd -c "init"` 不会,拼写到最后一个
33+
参数为止都一样,没有任何 argv 能表达这个区别。
3434

35-
两者差别只在设备槽的 argv。⭐ 这**不需要任何引擎机制** —— `mcpp::has_feature()`
36-
本来就存在,分层按规定在起作用
35+
`mcpp::run_exclusive()` 陈述「这个目标的运行不能重叠」—— 对一块板、一张 GPU、
36+
一个串口、一个单席位 license 同样成立,`mcpp test` 据此串行化
3737

3838
### `--locked`
3939

40-
锁一直是解析之后写、从不读回;它自己的头注释就这么说。现在它是一条**断言**:
41-
发生的解析必须等于记录的解析,不等则点名移动了的包与两个版本。
42-
43-
⚠️⚠️ **而它绝不能遇上快路径。** 实测:在加上那道闸之前,一份被故意改坏的锁通过了
44-
`mcpp build --locked` 并打印 `Finished` —— 旗标被接受、构建正确、断言从未跑到。
45-
**被跳过的判据比不存在的判据更糟,因为那个绿会被当成一次验证。**
46-
47-
### `mcpp sbom`
40+
锁一直是解析之后写、从不读回。现在它是断言:发生的解析必须等于记录的解析,不等则
41+
**点名移动了的包与两个版本**
4842

49-
CycloneDX 1.5,覆盖**已记录**的解析。物料清单需要的一切都已在 `mcpp.lock` 里,
50-
所以这是一种输出格式而不是一套机制。⚠️ 它读锁而不是重新解析 —— 一份描述了与所
51-
构建者不同的图的文档比没有更糟。
43+
⚠️⚠️ **它绝不能遇上快路径。** 实测:加闸之前,一份被故意改坏的锁通过了
44+
`mcpp build --locked` 并打印 `Finished` —— 旗标被接受、构建正确、**断言从未跑到**
5245

53-
### 两个传播点,以及先被漏掉的那一个
46+
### `mcpp emit sbom`
5447

55-
依赖提供的 RunGlobal 条目抵达根工程,走的是与包自身指令**不同**的代码路径。只接了
56-
前者时,`mcpp flash` 报「no flash is configured」,而 `mcpp run` 找得到同一个构建
57-
程序三行之外发出的 runner。两处现在都遍历槽表,而不是各自点名 `runner`
48+
CycloneDX 1.5,覆盖**已记录**的解析。⚠️ 读锁而不是重新解析 —— 一份描述了与所构建
49+
者不同的图的文档比没有更糟。归在 `emit` 之下而不是新开一级命令:`emit` 已经是
50+
「生成描述本工程的文档」
5851

59-
指令协议版本 6。新增 `docs/18-devices.md``docs/19-supported-versions.md`(中英双份)。
52+
新增 `docs/18-devices.md``docs/19-supported-versions.md`(中英双份)。
53+
指令协议版本 6。
6054

6155
## [2026.9.4.1] — 2026-09-04
6256

docs/18-devices.md

Lines changed: 94 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,154 @@
11
# 18 — Reaching a Device
22

3-
This document specifies how mcpp executes, writes, observes and debugs an
4-
artifact that runs somewhere other than the machine that built it, and how a
5-
project selects between an emulator and physical hardware.
3+
This document specifies how mcpp executes an artifact that runs somewhere other
4+
than the machine that built it, how a package supplies additional ways of
5+
reaching it, and how a project selects between an emulator and physical
6+
hardware.
67

78
Related documents: [13 — Bare-Metal and Freestanding Targets](13-baremetal.md)
8-
covers the targets these actions apply to; [07 — build.mcpp](07-build-mcpp.md)
9-
is the reference for the directive protocol a board-support package speaks;
10-
[11 — Machine Output](11-machine-output.md) is the interface a debugger client
11-
or IDE uses.
9+
covers the targets this most often applies to; [07 — build.mcpp](07-build-mcpp.md)
10+
is the reference for the directive protocol a package speaks; [11 — Machine
11+
Output](11-machine-output.md) is the interface a debugger client or IDE uses.
1212

13-
## Four actions, one shape
13+
## One command, and named exceptions
1414

1515
An artifact that cannot run on the build machine needs something to stand in
16-
front of it. Four things are asked of such an artifact, and all four are an
17-
argv that a board knows and a tool performs:
16+
front of it. That thing is a **runner**: an argv the package supplies and a tool
17+
performs, with the artifact appended or substituted for `{}`.
1818

19-
| Command | Slot | What it does |
20-
|---|---|---|
21-
| `mcpp run` | `runner` | executes the artifact |
22-
| `mcpp flash` | `flash` | writes it to the device |
23-
| `mcpp monitor` | `monitor` | observes what the device prints |
24-
| `mcpp debug` | `debug` | starts the device's debug server |
19+
```bash
20+
mcpp run # the default runner
21+
mcpp run --runner flash # a named one
22+
mcpp run --list-runners # what this project supplies
23+
```
2524

26-
Each is declared the same way, by a board-support package:
25+
`mcpp run` is the whole of the common case, including on real hardware. On a
26+
device, running a program means writing it, resetting, attaching to its output
27+
and reading its exit status — which is one command (`probe-rs run`, `qemu-system-*
28+
-kernel`), not several. A board therefore supplies that as its **default**
29+
runner, and the command a developer types does not change when they move from an
30+
emulator to a board.
2731

28-
```cpp
29-
mcpp::flash("probe-rs");
30-
mcpp::flash("download");
31-
mcpp::flash("--verify");
32-
mcpp::flash("--chip");
33-
mcpp::flash("STM32L475VG");
34-
```
32+
Named runners exist for what remains: writing an image without running it,
33+
observing a console, starting a debug server, erasing a part, deploying without
34+
starting.
3535

36-
or by a project, overriding what a dependency supplied:
36+
⚠️ **The engine knows no runner names.** `flash`, `serve`, `deploy`, `submit`
37+
and `logcat` are equally unknown to it: it knows only that a package may supply
38+
named runners, and performs the argv it finds. A fixed set of names in the
39+
engine would decide, in the engine, which domains are expressible.
3740

38-
```toml
39-
[target.thumbv7em-none-eabihf]
40-
flash = ["probe-rs", "download", "--verify", "--chip", "STM32L475VG", "{}"]
41-
```
41+
## What a package supplies
4242

43-
The artifact path is appended, or substituted for `{}` when the template
44-
contains it. One token per call: argv is ordered, and a single string cannot
45-
say where its boundaries are.
43+
```cpp
44+
mcpp::runner("qemu-system-arm"); // the default: argv token by token
45+
mcpp::runner("-machine"); mcpp::runner("mps2-an385"); …
4646

47-
The program is located by mcpp rather than by the system: a declared payload's
48-
`bin/` first, then `PATH`. A tool that is nowhere is an error decided before
49-
any process starts, rather than a fallback to bare execution.
47+
mcpp::runner("flash", "probe-rs"); // a named runner
48+
mcpp::runner("flash", "download"); …
5049

51-
## Termination is a property of the slot
50+
mcpp::runner_longlived("monitor"); // no natural end
51+
mcpp::run_exclusive(); // this target's runs cannot overlap
52+
```
5253
53-
The four actions differ in one way the engine must act on, and no argv can
54-
express it.
54+
⭐ **Name the program, not its path.** mcpp locates it: the `bin/` of a payload
55+
this package declared under `[xlings] deps` first, then `PATH`. Writing an
56+
absolute path computed from `mcpp::xpkg_dir` is unnecessary, and it introduces a
57+
failure mode — a declaration is not an install, so the lookup can return empty
58+
and leave no runner configured with nothing said about why. Naming the program
59+
lets mcpp report exactly which directories it searched.
5560
56-
| Semantics | Slots | Meaning |
57-
|---|---|---|
58-
| `OneShot` | `run`, `flash` | runs to completion; the exit code is the verdict |
59-
| `LongLived` | `monitor`, `debug` | has no natural end; the operator ends it |
61+
## What a project overrides
6062
61-
`openocd -c "program image.elf verify reset exit"` terminates and `openocd -c
62-
"init"` does not, and the two are spelled alike up to the argument the board
63-
chose. The engine therefore reads termination from the slot, and a board cannot
64-
get it wrong by writing its argv differently.
63+
```toml
64+
[target.thumbv7em-none-eabihf]
65+
runner = ["qemu-system-arm", "-machine", "mps2-an385", "-kernel"]
66+
67+
[target.thumbv7em-none-eabihf.runners]
68+
flash = ["probe-rs", "download", "--verify", "--chip", "STM32L475VG", "{}"]
69+
monitor = ["probe-rs", "attach", "--chip", "STM32L475VG"]
70+
```
6571

66-
`mcpp debug` starts a **server** and stops there. The client that attaches is
67-
the user's debugger or their IDE, which learns what it needs through the
68-
machine-output protocol. mcpp does not drive the client.
72+
Precedence is the ordinary one: what the author of the project wrote beats what
73+
a dependency supplied, and the override is reported rather than applied in
74+
silence. Exactly one dependency may supply a given name; a second is an error
75+
naming both packages.
6976

70-
## Absence is reported, never substituted
77+
## Termination is declared, not inferred
7178

72-
`mcpp run` on a hosted target with no runner executes the artifact directly,
73-
because the host can run it. There is no corresponding reading of "no flasher":
74-
nothing else writes an image to a device. An undeclared `flash`, `monitor` or
75-
`debug` is therefore an error on every target, naming the slot and printing the
76-
key to paste.
79+
| | Meaning |
80+
|---|---|
81+
| default | runs to completion; the exit code is the verdict |
82+
| `runner_longlived(name)` | has no natural end; the operator ends it |
7783

78-
Succeeding at `mcpp flash` by running the program on the build host would be
79-
the exact failure the slot exists to prevent.
84+
`openocd -c "program image.elf verify reset exit"` terminates and `openocd -c
85+
"init"` does not, and the two are spelled alike up to the argument the package
86+
chose. No argv can express which is which, and the engine has no list of names
87+
to infer it from — so the package states it.
8088

81-
## An exclusive device
89+
`mcpp run --runner debug` starts a **server** and stops there. The client that
90+
attaches is the user's debugger or IDE, which learns what it needs through the
91+
machine-output protocol.
8292

83-
A physical board is a mutex; an emulator is not. `mcpp test` runs test binaries
84-
on a worker pool, and two processes reaching for one probe do not fail cleanly
85-
— they interleave, and the verdict describes neither test.
93+
## Runs that cannot overlap
8694

87-
The board states this about itself:
95+
`mcpp test` runs test binaries on a worker pool. One board on one probe, one
96+
GPU, one serial port, or a tool with a single-seat licence admits one user at a
97+
time, and two workers reaching for it do not fail cleanly — they interleave, and
98+
the verdict describes neither test.
8899

89-
```cpp
90-
mcpp::runner_exclusive();
91-
```
100+
The package states this about itself with `mcpp::run_exclusive()`, and `mcpp
101+
test` then serialises. A project never has to remember `-j1`.
92102

93-
`mcpp test` then runs one test at a time on that target, and reports that it is
94-
doing so. A project never has to remember `-j1`.
103+
Named for the property rather than for the hardware: nothing here is about
104+
devices.
95105

96106
## Emulator and hardware are one package
97107

98108
A board reached through an emulator and the same board reached through a debug
99-
probe differ in the argv of their device slots and in nothing else. The linker
109+
probe differ in the argv of their runners and in nothing else. The linker
100110
script, the startup code, the memory map and the exported module are the same
101-
board. Publishing two packages to vary four strings duplicates all of it and
111+
board. Publishing two packages to vary a few strings duplicates all of it and
102112
lets the copies drift.
103113

104114
The choice is therefore a feature of one package:
105115

106116
```toml
107117
[features]
108118
default = ["emulator"]
109-
emulator = []
110-
hardware = []
119+
emulator = {}
120+
hardware = {}
111121
```
112122

113123
```cpp
114124
int main() {
115125
if (mcpp::has_feature("hardware")) {
116126
for (auto a : {"probe-rs", "run", "--chip", "STM32L475VG"})
117-
mcpp::runner(a);
118-
for (auto a : {"probe-rs", "download", "--verify", "--chip", "STM32L475VG"})
119-
mcpp::flash(a);
120-
mcpp::runner_exclusive();
127+
mcpp::runner(a); // the DEFAULT moves
128+
for (auto a : {"probe-rs", "gdb", "--chip", "STM32L475VG"})
129+
mcpp::runner("debug", a);
130+
mcpp::runner_longlived("debug");
131+
mcpp::run_exclusive();
121132
} else {
122-
mcpp::runner(qemu_path());
123-
for (auto a : {"-machine", "mps2-an385", "-nographic", "-semihosting",
124-
"-no-reboot", "-kernel"})
133+
for (auto a : {"qemu-system-arm", "-machine", "mps2-an385", "-nographic",
134+
"-semihosting", "-no-reboot", "-kernel"})
125135
mcpp::runner(a);
126136
}
127137
return 0;
128138
}
129139
```
130140

131-
The consumer selects an environment where it selects everything else:
132-
133141
```toml
134142
[dependencies]
135-
demo-board-rt = { version = "0.1.0", features = ["hardware"] }
143+
cortex-m-rt = { version = "0.1.0", features = ["hardware"] }
136144
```
137145

138-
A slot the chosen environment does not supply stays absent. An emulator has no
139-
debug probe, so under the emulator feature `mcpp debug` reports that none is
140-
configured rather than inventing one.
146+
The consumer's command does not change. A runner the chosen environment does not
147+
supply stays absent: an emulator has no debug probe, so under the emulator
148+
feature `mcpp run --runner debug` reports that no such runner exists and lists
149+
the ones that do.
141150

142-
This required no engine mechanism. The engine reads slots and knows nothing
151+
This required no engine mechanism. The engine reads runners and knows nothing
143152
about emulators or probes; `mcpp::has_feature` already existed. That the
144153
question is answerable without adding anything is the layering working as
145154
specified.
146-
147-
## Precedence and reporting
148-
149-
Two producers exist for every slot, with ordinary precedence: what the author
150-
of the project wrote beats what a dependency supplied. The override is reported
151-
rather than applied in silence.
152-
153-
```
154-
note [target.thumbv7em-none-eabihf].flash overrides the flash a dependency supplied
155-
```
156-
157-
Exactly one dependency may supply a given slot. Link flags from two
158-
dependencies concatenate and that is correct; two flashers cannot, and
159-
appending produces an argv that is neither one's. A second provider is an error
160-
naming both packages.

docs/19-supported-versions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ resolver.
6464

6565
## Bill of materials
6666

67-
`mcpp sbom` writes a CycloneDX 1.5 document describing the **recorded**
67+
`mcpp emit sbom` writes a CycloneDX 1.5 document describing the **recorded**
6868
resolution:
6969

7070
```bash
71-
mcpp sbom -o sbom.json
71+
mcpp emit sbom -o sbom.json
7272
```
7373

7474
It reads `mcpp.lock` rather than resolving again, because a document describing

0 commit comments

Comments
 (0)