Skip to content

runner beyond bare metal: honour [target.<triple>].runner on hosted targets, typed launch failures, not-run reporting, [xlings] values per host (#544) - #545

Merged
Sunrisepeak merged 12 commits into
mainfrom
fix/runner-hosted-targets
Sep 2, 2026
Merged

runner beyond bare metal: honour [target.<triple>].runner on hosted targets, typed launch failures, not-run reporting, [xlings] values per host (#544)#545
Sunrisepeak merged 12 commits into
mainfrom
fix/runner-hosted-targets

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Closes #544.

The defect

[target.<triple>].runner was parsed, validated and documented for every exact triple, and consulted only when os == "none". On a hosted cross target (aarch64-linux-musl built on x86_64) mcpp run executed the artifact bare, the kernel refused it with ENOEXEC, and run_exec turned the refusal into a bare 127 with nothing printed; mcpp test reported FAIL (exit 127), which states that the test ran. The bounded launcher's own invariant ("could not spawn" and "ran and failed" must not share an exit code) was defeated one layer down, and both deadline wrappers spawned a second time and discarded the first errno.

Design record: .agents/docs/2026-09-02-runner-beyond-baremetal-design.md (sections 0 and 4 are normative). Plan: .agents/docs/2026-09-02-issue544-runner-implementation-plan.md.

What changes

Launcher layer. DeadlineRun and BoundedOutcome carry the spawn error; run_exec, capture_exec and both deadline wrappers take a trailing int* spawn_error. A refused spawn is reported exactly once: typed to the caller when it asked, printed by the launcher otherwise. There is no second spawn.

choose_runner reads both producers for every target. The freestanding predicate decides one thing only: whether an absent runner is fatal before any spawn. The runner's first element is located by mcpp — the bin/ of each payload declared under [xlings] deps, then PATH — because a bare name on PATH reaches an xvm shim that answers for the current SubOS rather than for the package (e2e 130 recorded this in CI). Not found anywhere is decided before any spawn and is an error, not a fallback to bare execution.

mcpp run. A declared runner is used; a runner that cannot be found or started is an error naming the program, the directories searched and the errno; an artifact the kernel refuses with no runner declared is reported with the key to write. All three exit 2, distinct from 1 ("ran and failed"). The run fast path declines an entry whose target has a runner declared (runner= line in .build_cache), so the second mcpp run behaves as the first.

mcpp test. TestResult::St::NotRun with a reason. The runner is resolved once per invocation; the first refused spawn sets an invocation-wide flag, later tests are reported not-run without a spawn, and the reason is printed once. The summary line carries the count at the same weight as failures; --message-format json carries not_run and reason per record and not_run / not_run_reason on the summary; --workspace adds tests_not_run and unrunnable_members. Exit code 2 whenever the count is non-zero — the code the freestanding no-runner path already returns for the same situation.

--no-runner on run and test: execute directly, ignoring a declared runner. The manifest has no host axis; this is the operator on a host where the triple is native stating that fact.

[xlings] values per host platform. A deps entry and a [xlings.workspace] value may be { linux = "...", macos = "...", windows = "...", default = "..." }, the form xlings' own .xlings.json accepts for workspace; macosx is accepted as xlings' spelling. Resolved against the host at manifest load, so every downstream reader keeps its flat list. An unknown key is an error. Previously [xlings.workspace] dropped a table value in silence, and [xlings] deps had no conditional form at all, which made deps = ["qemu-user-aarch64"] (an x86_64-only package) a hard build error on every other host.

The [target.<triple>] unknown-key sweep now covers arrays (runnerX = [...] is reported) and lists runner among the supported keys.

Decisions taken on review (design §0)

  • D1: no fallback from a missing runner to bare execution; it reproduced the failure the key exists to prevent, and read ENOENT as "absent" when it also means "found, interpreter missing".
  • D2: not-run exits 2, not 0; skip-exits-zero is how # requires: e2e never ran in CI.
  • D3: --no-runner replaces the withdrawn principle "a declaration this machine cannot satisfy must not disable a capability it has".
  • D5: runner = "" deferred (no producer emits mcpp:runner= for a hosted triple); spelling settled as [] when needed.
  • D7: per-platform [xlings] values, in xlings' own form.

Behaviour changes for existing manifests

  • A runner already declared under a hosted triple stops being inert. Where its program is missing on a host, mcpp run / mcpp test on that host now fail with a message instead of silently running the artifact bare; --no-runner is the escape.
  • A mcpp test run that reported FAIL (exit 127) for unrunnable artifacts now reports NOT RUN and exits 2 (still non-zero).
  • [xlings] deps and [xlings.workspace] accept a table value; a table with an unknown platform key is now an error where it was previously dropped.

Verification

  • Unit: test_process_run_exec (+9), test_manifest (+6), new test_runner_lookup (9). Full unit suite: 97 test binaries pass.
  • e2e: new 330_runner_hosted_targets.sh — no # requires: beyond unix-shell; the runner is a recording shell script and the unrunnable artifact is a host binary with e_machine patched to 0xffff, so it runs identically on a host with and without binfmt_misc. Sections: declared runner used on both run doors; --no-runner; runner not found (run and test); unrunnable artifact on run; mcpp test with one and two tests (streaming and capturing paths), human and JSON; array typo in the sweep. Fails at its first section on 2026.9.1.1 and passes on this branch.
  • Neighbours re-run on this branch: e2e 01, 130, 131, 132, 178.
  • check_docs_style.sh, check_modules_wiring.sh, check_version_pins.sh pass.

Docs: 05 (§2.7.1, new §2.7.3 and §2.7.3.1, §2.13), 11 (the mcpp test stream), 13, 15, 17, each with its docs/zh/ twin.

Version: 2026.9.2.1 (mcpp.toml, modules/versioning/src/version.cppm); the bootstrap pin stays at 2026.9.1.1 until the release is indexed.

…an unloadable file through /bin/sh (measured on CI)
…pelling matched on Linux and never on macOS (measured on CI)
… 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).
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.
@Sunrisepeak

Copy link
Copy Markdown
Member Author

Self-review pass, and what it found

Four commits since the initial round; the first three are defects this pass found, and all 37 checks are green on e5a81df.

The diagnostics named a triple no [target.…] lookup resolves. choose_runner resolves the key against the canonical spelling — the output directory's name — while six message sites printed tc.targetTriple, the spelling the driver reported. On Linux the two strings coincide; on macOS the driver says arm64-apple-darwin24.6.0. Three of the six print a [target.<triple>] block for the reader to paste, so following the advice would have produced a key that is never read. RunnerChoice::tripleKey is now derived once, beside the lookup that uses it. e2e 330 §3 asserts the message names the key the manifest uses and was the check failing on macOS ARM64 (run 33609434208); it passes on e5a81df.

The CHANGELOG entry was missing for a version bump that is part of this PR.

The design's coverage claim was wrong. §11 stated that the payload-bin/ rule is exercised "with a locally staged package directory"; no such e2e row exists, and staging one is not available — the store is shared with the machine's real payloads, and a synthetic [xlings] deps entry is refused by the provisioning pass before the lookup is reached. §11 now states where the rule is actually covered: locate's ordering in tests/unit/test_runner_lookup.cpp, and the wiring in the release's sandbox verification.

Pre-verification of that sandbox step, run locally against this branch

=== A. cross build + run through the declared runner ===
     Running `qemu-aarch64-static … target/aarch64-linux-musl/…/bin/xrun`
CROSS-RAN

=== B. mcpp test through the runner ===
 test result ok. 1 passed; 0 failed; finished in 0.20s

=== C. the payload lookup wins over the PATH shim ===
/home/speak/.xlings/subos/current/bin/qemu-aarch64-static
[error] qemu-aarch64-static is not installed in this subos (_)

=== D. no runner declared, artifact this host cannot load ===
error: this host cannot execute '…/bin/xrun': Exec format error (error 8).
       The artifact was built for 'aarch64-linux-musl'. Declare how to run it here:
           [target.aarch64-linux-musl]
           runner = ["qemu-aarch64-static"]

=== E. mcpp test reports not-run and exits 2 ===
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); …)

Section C is the load-bearing one: the bare name on PATH is an xvm shim that refuses to run, and A and B succeeded anyway, which is only possible because the lookup found the program under the payload declared by [xlings] deps = [{ linux = "qemu-user-aarch64" }].

D used a patched e_machine rather than the real artifact: this host has qemu-aarch64 registered in binfmt_misc with flags PO, so an unpatched aarch64-linux-musl binary starts here. That is the same asymmetry §2 of the design measured, and the reason no criterion in this PR depends on which machine runs it.

@Sunrisepeak
Sunrisepeak merged commit 8de3847 into main Sep 2, 2026
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[target.<triple>].runner is validated but never consulted for a hosted foreign-ISA target; mcpp run and mcpp test then lose the exec failure

2 participants