|
| 1 | +--- |
| 2 | +subject: targets |
| 3 | +status: landed |
| 4 | +--- |
| 5 | + |
| 6 | +# A verified Web run that asked the host for node |
| 7 | + |
| 8 | +**Status:** implemented in mcpp 2026.9.12.1 and openxlings/xim-pkgindex#823. |
| 9 | + |
| 10 | +## What was measured |
| 11 | + |
| 12 | +The sandbox verification of the published mcpp 2026.9.11.4 ran 27 checks in an |
| 13 | +`xlings subos use <name> --sandbox` environment: a fresh `$HOME`, a fresh |
| 14 | +`/tmp`, and no `mcpp` or `node` on PATH. Twenty-six held. The one that did not |
| 15 | +was the Web run: |
| 16 | + |
| 17 | +``` |
| 18 | +mcpp run --target wasm32-emscripten |
| 19 | + Compiling w v0.1.0 (.) |
| 20 | + Finished dev [unoptimized + debuginfo] in 0.84s |
| 21 | + Running `target/wasm32-emscripten/.../bin/w` |
| 22 | +/usr/bin/env: 'node': No such file or directory |
| 23 | +``` |
| 24 | + |
| 25 | +The build was correct. The run depended on the host. |
| 26 | + |
| 27 | +## Why the development host could not see it |
| 28 | + |
| 29 | +An Emscripten link produces a JavaScript launcher whose first line is |
| 30 | +`#!/usr/bin/env node`. When neither the project nor its dependency graph |
| 31 | +declares a runner, `mcpp run` executes the artefact, and the kernel resolves the |
| 32 | +interpreter from the PATH mcpp inherited. `choose_device_action` consults the |
| 33 | +manifest and the graph and nothing else, and the spawned artefact receives no |
| 34 | +toolchain directory on its PATH. |
| 35 | + |
| 36 | +The ecosystem had already installed the right program. `xim:emsdk` declares |
| 37 | +`xim:node` as a runtime dependency and writes that payload's `bin/node` into its |
| 38 | +own `.emscripten`, deliberately so that linking does not depend on PATH. Running |
| 39 | +the result was the one step that still did. |
| 40 | + |
| 41 | +The row's `verified` record states how it was measured: `node bin/<name>`, with |
| 42 | +a `node` the measuring machine had. The development host has one on PATH through |
| 43 | +its default xlings environment. So every measurement on that host, including the |
| 44 | +dry run of the verification script itself, read the host's `node` and passed. |
| 45 | +Only an environment without one could distinguish "the ecosystem runs this" from |
| 46 | +"this machine runs this". |
| 47 | + |
| 48 | +## Where the answer belongs |
| 49 | + |
| 50 | +Three placements were considered and rejected. |
| 51 | + |
| 52 | +- **The engine reads `NODE_JS` from emsdk's `.emscripten`.** This would put an |
| 53 | + emsdk layout fact into the engine, which is the coupling the payload |
| 54 | + descriptor was introduced to remove (2026-09-11 record, item C). |
| 55 | +- **The recipe places a `node` link inside the emsdk payload's `bin/`.** Recipe |
| 56 | + hooks in this ecosystem have no reliable symlink or permission primitive, and |
| 57 | + a link would restate a fact the recipe already records as a path. |
| 58 | +- **Projects declare `[target.wasm32-emscripten] runner` and `xim:node`.** A |
| 59 | + `verified` row that needs two lines of vocabulary to run is not the row the |
| 60 | + 2026.9.11.3 release described, where changing a flag was the whole cost. |
| 61 | + |
| 62 | +The payload knows which program runs what its compiler produces, so the payload |
| 63 | +states it. `.mcpp-toolchain.json` gains a fourth key. |
| 64 | + |
| 65 | +## The contract |
| 66 | + |
| 67 | +```json |
| 68 | +{ |
| 69 | + "schema": 1, |
| 70 | + "frontend": "emscripten/em++", |
| 71 | + "runner": "<store>/xim-x-node/26.7.0/bin/node" |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +- **One program, no arguments.** The artefact path is appended. The key cannot |
| 76 | + carry flags, which keeps the descriptor what it was: a set of answers to |
| 77 | + questions the engine already asks, and not a flag channel. |
| 78 | +- **Relative or absolute.** A relative runner obeys the rules `frontend` obeys |
| 79 | + and resolves against the directory the descriptor was read from. An absolute |
| 80 | + runner exists because the program is usually a dependency's, and it is |
| 81 | + honoured only inside the package store that holds the payload |
| 82 | + (`<store>/<package>/<version>`), compared on canonical paths so that a store |
| 83 | + reached through a symbolic link is still that store. |
| 84 | +- **Outside the store is ignored, malformed is refused.** Where a store lives is |
| 85 | + a property of the machine, so a runner outside it is not honoured and the run |
| 86 | + proceeds as it did before the key existed. A structurally wrong value (not a |
| 87 | + string, empty, a backslash, a `.` or `..` component) is refused by name at |
| 88 | + read time, like every other key. |
| 89 | +- **Last in precedence, run slot only.** A project's `[target.<triple>] runner` |
| 90 | + and a dependency's `mcpp::runner(...)` are statements about this program and |
| 91 | + outrank a statement about everything a compiler produces. `--no-runner` still |
| 92 | + executes the artefact directly. `flash`, `monitor` and `debug` name actions a |
| 93 | + board package owns, and a compiler has no opinion about them. |
| 94 | + |
| 95 | +## Compatibility |
| 96 | + |
| 97 | +Both directions hold without coordination. An mcpp that predates the key ignores |
| 98 | +it, so the recipe ships first. A payload installed before the recipe wrote the |
| 99 | +descriptor has none, and its artefact runs through its own shebang exactly as |
| 100 | +before; reinstalling the payload adds the descriptor. |
| 101 | + |
| 102 | +## Criteria |
| 103 | + |
| 104 | +| claim | criterion | measured | |
| 105 | +|---|---|---| |
| 106 | +| a runner in the payload's store is the default for its compiler | unit test with the emsdk shape: compiler at `emscripten/em++`, runner a sibling payload's `bin/node` | yes | |
| 107 | +| a runner outside the store is not honoured | unit test with `/usr/bin/node`; and the same test with the store rule removed | yes, and the probe turned it red | |
| 108 | +| malformed runners are refused by name and by key | seven shapes, each refusal naming the file and `"runner"` | yes | |
| 109 | +| the wiring reaches `mcpp run` | in the sandbox, no `node` on PATH, with a descriptor in the sandbox's own emsdk payload | `1-2-3` | |
| 110 | +| the runner came from the descriptor and not from somewhere else | the same run with the descriptor removed | fails at `/usr/bin/env: 'node'` again | |
| 111 | +| CI holds the claim against the published recipe | `scan (linux-x86_64)` runs a Web program with a fresh `MCPP_HOME`, so the emsdk payload is the one the index publishes rather than one restored from cache, and with a decoy `node` first on PATH that exits 97 with a marker; the step fails if the payload has no descriptor, if the decoy ran, or if `1-2-3` is not a line of the output | on this PR, after openxlings/xim-pkgindex#823 was published | |
| 112 | + |
| 113 | +The first attempt at the sandbox reading measured nothing. The development binary |
| 114 | +was dynamically linked, and its loader lives in the host's `~/.mcpp`, which the |
| 115 | +sandbox replaces with its own; both readings were "cannot execute: required file |
| 116 | +not found" and said nothing about runners. The reading above used a |
| 117 | +`x86_64-linux-musl` static build, the form a release ships. |
| 118 | + |
| 119 | +## What remains |
| 120 | + |
| 121 | +An emsdk payload already installed on a machine keeps running through PATH until |
| 122 | +it is reinstalled. The published-artefact check is the sandbox verification |
| 123 | +script's Web section, run again after this release. |
0 commit comments