feat(freestanding): a BSP supplies the runner, mcpp test runs on bare metal, and the artifact set is real - #457
Merged
Conversation
…re metal, and the artifact set is real
Three of the four gaps the phase-3 plan named. The consumer's manifest now has
no `[target.*]` section at all:
[dependencies]
board = { path = "../board" }
$ mcpp run --target riscv64-none-elf → boots in qemu
$ mcpp test --target riscv64-none-elf → ok_one ok, ok_two ok,
deliberate_fail FAIL (exit 1)
$ mcpp build --target riscv64-none-elf → firmware, .bin, .map
Size firmware text 8836 data 80 bss 5668 total 14584
R1 — THE RUNNER BELONGS TO THE BOARD-SUPPORT PACKAGE
`mcpp:runner=`, one argv token per line in emission order, with its own
`Slot::Runner` and its own `Scope::RunGlobal`. Both are new values and neither
is a detail: LdFlags would put an emulator's argv on the linker command line,
and reusing LinkGlobal would hand this the conflict rule for link flags — two
dependencies' link flags concatenate correctly, two runners cannot.
It has to come from `build.mcpp` rather than from a manifest key, and that is
measured, not stylistic: the emulator must be named by ABSOLUTE path (a bare
name resolves through PATH to a shim that dispatches against its owner home —
CI reported `xlings: 'qemu-system-riscv64' is not installed` from a job where
the same name had answered `--version` two steps earlier), the path carries a
home and a version, and only a build program can compute it.
Three sides verified: a BSP supplying it means the consumer needs no
`[target.*]`; two dependencies supplying it is a hard error naming BOTH; a
consumer writing its own wins and is told which dependency it overrode.
⚠️ R3 — TWO PLAN ASSUMPTIONS WERE WRONG, AND MEASURING THEM DELETED MOST OF IT
The plan called for a `batch` mode and a structured stdout protocol. Both
premises were false:
* "qemu cold start is ~0.4s, so 30 isolated cases cost 12s" — measured 12ms
per start (5 runs in 63ms). 30 cases cost 0.36s. The reason for batching
disappeared, and with it the "on timeout, re-run isolated to find the
culprit" machinery: isolated already names the culprit.
* "bare metal has no exit code, so results need their own channel" —
semihosting propagates the firmware's `main` return value to the emulator's
exit code (`return 7` → qemu exits 7, verified). "Exit code is the verdict"
holds here exactly as on the host.
So `mcpp test` needed one change: route the test binary through the SAME runner
`mcpp run` uses. Both callers now read `choose_runner()` — deriving it twice is
the shape this codebase has paid for repeatedly.
R4 — THE ARTIFACT SET
`.bin` via llvm-objcopy, `.map` via `-Wl,-Map=`, and a size summary, because
capacity is the constraint on a bare-metal target and mcpp already knows the
number. ⚠️ `.bin` is a SEPARATE ninja edge on the ELF, not a second output of
the link: folding it in would make the two share one up-to-date check, and an
incremental build would hand back a stale image. Pinned by content, not mtime.
Also fixes an inconsistency this work introduced: `mcpp run` took
`--target-triple` while every other subcommand took `--target`. `--target` now
works everywhere; `--target-triple` stays as `run`'s unambiguous alias, because
its POSITIONAL is also called target and means a binary name.
TESTS
91/91 unit (7 new on the runner directive and the artifact helpers) and three
e2e, all three run by the `baremetal` CI job which asserts each PASS line
appeared — every one of them can exit 0 without running.
…ady is
Adding `--target` to `mcpp run` for symmetry with every other subcommand broke
`mcpp run <binary>`:
$ mcpp run q
error: unknown target 'q'
The parser keys options and positionals by the same word, and this subcommand's
POSITIONAL is called target and means the binary NAME. `e2e/73` caught it
within the hour.
The positional's declaration already carried a comment saying so; I overrode it
reaching for consistency. `run` keeps `--target-triple`; `test` has no such
positional and keeps `--target`.
Consistency is one of the criteria, and it ranks below not breaking things.
…oo old
A package that adopts a new typed directive cannot be used by an older mcpp,
and until now the only thing the reader saw was
error: 'runner' is not a member of 'mcpp'
which reads like the package author's typo rather than the reader's engine
being out of date. The wire protocol's own answer (protocol_error names
`mcpp self update`) never reaches this case: it needs the program to have
compiled first.
The package cannot solve it either. Measured:
if constexpr (requires { mcpp::runner("qemu"); }) // hard error
A requires-expression over a qualified name that does not exist is ill-formed,
not `false`, so there is no in-language feature probe.
So the compile failure carries the hint, matched across all three frontends'
spellings and anchored on our namespace so an ordinary error does not get it.
This covers every future typed-API addition, not just `runner`.
Also corrects docs/07 (both languages), which still claimed an unknown
directive "can only be a typo" within one protocol version — the engine's own
diagnostic was corrected away from that when link-script landed: the protocol
number is stamped by whichever mcpp compiled the program, so two matching
numbers say nothing about whether the key is from the future.
The collision was real — `mcpp run q` became `mcpp run --target=q` — but retreating to `--target-triple` on this one subcommand paid for it with a lasting inconsistency. The actual cause is narrower than "one word, two axes": ParsedArgs::value() falls back from an unset option to a positional OF THE SAME NAME, and that positional was named `target`. cmd_run never reads the name (it takes positional(0) by index) — it only labels the slot in --help, where `bin` is the more accurate word. So `--target` works on `run` like everywhere else, `--target-triple` stays as an alias because it shipped in 2026.8.19.1, and e2e/130 now pins both spellings while e2e/73 pins the positional.
R5's builtin template was the wrong shape — mcpp's builtin registry is frozen at `bin` by design and templates ship with the package that provides them, so the template lives in riscv-virt-rt and mcpp needs no change at all. R2 hit a real gap: a dependency's `[xlings] deps` is materialized for the ROOT project only, so a consumer of the board package would get neither libc nor emulator. The fix is xim's own install-time dependency edge in the descriptor, verified by removing picolibc from the store and watching `mcpp add` put it back — not by observing an already-provisioned machine.
`mcpp run` and `mcpp test` on a bare-metal target now take their runner from an ordinary dependency, so a firmware project's manifest names no emulator, no linker script, no libc and no load address — it has no [target.*] section at all. Builds emit .bin and .map beside the ELF and print a text/data/bss summary, because capacity is the constraint that decides whether an image ships. New surface: * `mcpp:runner=` / `mcpp::runner(token)` — a package says how to execute * `mcpp run --target` — the spelling every other subcommand uses * a missing `mcpp::` API in build.mcpp now says the engine may be too old The ecosystem half ships alongside: mcpplibs/riscv-virt-rt supplies the board (and, from 0.2.0, `mcpp new <name> --template riscv-virt-rt`).
…plies crt0 docs/05 §2.7.2 told the reader there is no `main` on a bare-metal target and to point the target at a file carrying `_start`. That is only true for a board with no C library at all — with picolibc's semihosting crt0, which is what a board-support package ships, the entry point is an ordinary `int main()` whose return value reaches the host. Also leads the section with the short path (`mcpp new blinky --template riscv-virt-rt`), since the hand-written form below it is what to reach for when writing a package for a board that has none — not what to start with.
… that does not exist
It ended with a copy-pasteable
[dependencies]
mcpplibs.std.freestanding = "0.1"
and no such package is published. Following the advice fails at the very next
command with "package not found", which sends the reader off to debug their
index instead of their code.
Now it points at what a bare-metal project actually has — the module its board
package exports, named concretely — and describes a freestanding subset package
as a shape rather than a line to paste, saying plainly that none is published
yet. The concrete line comes back when one ships.
Every suggestion in a diagnostic is a promise; this one had not been checked
against reality before being written.
…fect `-Wl,-Map=` is a flag on the link command, so the map cannot be its own edge — a second edge claiming to produce it would run the link twice. It was therefore written but never declared, which means nothing tracked it: delete `firmware.map` and ninja considers the ELF up to date and puts nothing back. Declaring it as an implicit output of the link edge fixes that and makes `ninja -t clean` remove it with everything else. e2e/132 now deletes the map and asserts it returns, which is the only form of this test that can fail before the fix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
第三阶段四个缺口里的三个。消费者的 manifest 现在完全没有
[target.*]段:R1 ⭐ runner 归属板级支持包
mcpp:runner=,一行一个 argv token、按发射顺序,带自己的Slot::Runner与自己的Scope::RunGlobal。两个都是新值,且都不是细节:LdFlags会把模拟器的 argv 放到链接器命令行上;LinkGlobal会让它继承链接 flag 的冲突规则 —— 两个依赖的链接 flag 拼接是对的,两个 runner 不能拼。它必须来自
build.mcpp而不是 manifest 键,这是实测结论:模拟器必须用绝对路径(裸名会经 PATH 解析到一个按拥有它的 home 派发的 shim —— CI 报过xlings: 'qemu-system-riscv64' is not installed,而同一 job 两步之前--version刚跑通),而那个路径含 home 与版本,只有构建程序算得出来。三侧已验:
[target.*]段也能mcpp run计划要
batch模式 + 结构化 stdout 协议。两个前提都被实测否掉:main的返回值传给 qemu 退出码(return 7→ 退出码 7)。「退出码即判据」在裸机上原样成立⇒
mcpp test只需要一件事:让测试二进制走mcpp run用的同一个 runner。两个调用方现在都读choose_runner()—— 同一决策两处推导是本仓反复付过的形状。R4:产物形态
.bin(llvm-objcopy)·.map(-Wl,-Map=)· size 摘要 —— 裸机的核心约束是容量,而 mcpp 在链接结束的那一刻就知道这个数。.bin是以 ELF 为输入的独立 ninja 边,不是链接的第二输出:合进去会让两者共用一次 up-to-date 检查,增量构建会拿到陈旧镜像。判据按内容钉,不是 mtime。顺带修掉一处我自己造成的不一致
mcpp run先前用--target-triple,而其余子命令用--target。现在--target到处可用;--target-triple作为run的无歧义别名保留 —— 它的位置参数恰好也叫 target,含义是二进制名。测试
[target.*])· 132 裸机 test + 产物集(含故意失败的用例必须被点名且让退出码非零,以及改一行源码后.bin内容必须变)baremetalCI job 跑这三条并断言各自的 PASS 行出现过 —— 三个脚本都可能 exit 0 而没跑