Target rows become executable, and a machine-scoped scan stops being paid per target (2026.9.11.3) - #610
Merged
Merged
Conversation
Section 9. Four findings that are about the ecosystem rather than about any
one change:
* A proxy check refused correct output TWICE IN TWO REPOSITORIES on the same
day -- the MSI size ratio and, earlier, the AppImage size bound. Both are
now direct questions: the AppImage is run and its output asserted, the MSI
is installed and the extracted program compared byte for byte.
* The #599 fix reproduced the shape of the defect it fixed. It reasoned about
which shard runs 233 instead of enumerating the jobs that run the suite,
and the macOS lane caught the third one. The correction is an enumeration,
which can be re-checked.
* The ecosystem rule cost one new payload and otherwise only declarations.
The interesting case is `xim:python`: its x86_64-only table was the STATED
reason emsdk could not declare an interpreter -- an argument that was true
and was a request for a package, not a licence to use the host. `/dev/kvm`
is where the rule stops, and a test asserts it is the only warning left.
* The rule composes with the licence, not above it: the ecosystem supplies
what it may, and the licence says what it may. Getting that order wrong is
asymmetric -- three GitCode objects were uploaded before the licence was
checked, GitCode assets cannot be deleted, and the only remedy is that no
recipe references them.
Also states what is still open rather than implying it: the four target rows
are the next PR with their size named, and `xim:wix` is a gap with a known
shape.
… upgrade path is measured across two binaries THE CORRECTION LANDED IN THE RECORD AND NOT IN THE CODE. The design document carries the re-taken 2026-09-11 measurement -- Emscripten 6.0.9 reports `_LIBCPP_VERSION 220108` (llvm 22.1.8) and clang 24.0.0git -- while the `wasm32-emscripten` row's own comment still said llvm 20.1.7, `200100` and clang 22.0.0git. Two copies of one decision, and the second written without reading the first. The rule those numbers support is unaffected and is exactly why they have to be right: the module surface must match the LIBRARY, never the compiler. A stale number in a comment becomes a stale number in a diagnostic, and then in somebody's install command. Also simpler than the comment first said: 6.0.9 SHIPS the surface (134 files), so `em++` links `import std` with no generated surface at all. SECTION 11.8 GAINS A TWO-BINARY MEASUREMENT. An absent-tolerance claim is about what a previous version wrote, so a unit test over a synthesised header is not the whole criterion: 1. released 2026.9.10.2 writes `graph=normal;schedule=none;accel=default` 2. 2026.9.11.2 appends `;dist=none` -- fingerprint change, full rebuild, no error 3. 2026.9.11.2 again -- `Finished dev in 0.00s`, the fast path replays 4. 2026.9.10.2 again -- `0.00s`, the downgrade does not choke Step 4 says what the tolerance is worth: the version is in the fingerprint, so two binaries never share a graph directory and an older mcpp never reads a `dist=` field at all. The read side is defence in depth rather than a live path -- the same conclusion 11.2 reaches from the other direction.
… four other build systems The ownership question for iOS, Android and Web, answered against what cargo, Zig, CMake, Bazel and the platform tools actually do rather than against intuition, and against the seven engine changes the wasm row required. The comparison is more reassuring than not. rustc's target list is closed and in the compiler, which is what `kKnownTargets` is, and its tiers map almost exactly onto this table's -- Tier 1 is "built AND tested", Tier 2 "guaranteed to build" with tests not always run, Tier 3 code with no guarantees. That produces a calibration: all three of these platforms are Tier 2 in Rust, so `verified` for wasm is a STRONGER claim than Rust makes for its own wasm target and `planned` for Android and iOS is weaker than Rust's. Zig is the strongest counter-model and draws the same line: it bundles libc for 97 targets and ships neither the Apple SDK nor the Android NDK. CMake puts the target knowledge in a file the SDK ships, which is why its cross-compilation is per-SDK folklore. Bazel registers platforms from external rulesets. And nobody reimplements a packaging format. THE RUNNER SECTION IS THE PART THIS SETTLES RATHER THAN FLAGS. Running an artefact has three shapes -- it runs itself (wasm, via a shebang), a translator wraps it (qemu-user), or a session exercises it (emulator, simulator, device) -- and mcpp's `runner` is an argv prefix that covers the first two and cannot express the third. Cargo's reference states the boundary explicitly: managing devices and simulators is out of scope and "that responsibility falls to the runner program itself". So the session is a xim PACKAGE shipping a program, not an engine feature and not a plugin -- a plugin is a build-time module and a runner is a run-time program, which is a difference in lifetime rather than in subject. And the simulator is a TARGET, not a runner. Six recommendations are stated as decisions, four of which are engine-side and none of which is in a plugin -- which is itself the finding: the plugin boundary is already right, and what is unfinished sits upstream of it.
…ffers by platform, and codesign was never a host dependency Three questions answered by measurement rather than by recall. IS `wasm32-emscripten` STANDARD. No -- the industry name is the four-field `wasm32-unknown-emscripten`, which is what `em++ -v` passes to its own clang on this machine and what rustc's table lists. mcpp's short form is its own canonical spelling, the same vendor elision it already performs for `aarch64-macos` and `x86_64-windows-gnu`, with `llvm_triple()` restoring the field. That is defensible and should be stated as what it is rather than implied to be the common name -- hence R8: `parse()` should accept the four-field spelling every other toolchain prints. And the wasm family is nine targets in rustc, which settles the simulator question from an unexpected direction: Rust appends a FOURTH component for a variant (`-threads` there, `-sim` for iOS), and mcpp's `env` is exactly that slot, already used that way by `gnu`, `musl` and `eabihf`. `env = "sim"` is the field doing its job, not a workaround. IS THE TOOLCHAIN BOUND TO THE SDK. Two answers, not one. Emscripten and the NDK each ship compiler and sysroot as ONE archive, which is precisely the set `has_own_sysroot()` names -- the predicate is not an arbitrary pair. iOS does not: any sufficiently new clang plus `-isysroot`, which makes the row structurally the SAME SHAPE as bare metal (a generic llvm pin plus a separately-versioned sysroot package), so the `sysroot` column already exists for it and the row needs no new table machinery. Also worth stating because the naming misleads: "the Android SDK" names two unrelated things, and the four xim packages split on what Google actually ships rather than on a decomposition choice. WHAT MUST GO THROUGH THE HOST. Exactly three things, and a falsifiable test for them: a kernel facility, a proprietary RUNTIME that exists only on its own OS, or a credential. `/dev/kvm`, the Simulator or a device, and notarization. Everything that is a PROGRAM can be packaged -- and the survey found the third instance of this ecosystem mistaking a program for a host dependency: `rcodesign` (crate apple-codesign, MPL-2.0) signs Mach-O binaries, `.app` bundles, `.pkg` and `.dmg` with no Apple hardware or OS, and ships prebuilt static binaries for linux-musl on both arches, macOS universal and Windows. So `codesign` is not a host dependency; it is an unpackaged one (R7). The test also narrows R4: `msvc@system` is the (b) case, and the existing refusal is right about the general shape and wrong only about believing MSVC is its sole instance.
…s blocked by an image licence rather than by tooling SIGNING BELONGS IN THREE LAYERS THAT ALREADY EXIST, in this order: `rcodesign` is a program so it is a xim package; what invokes it is a `dist-*` member; what the user types is `mcpp pack --format <name>`. Release 0.29.0 signs BUNDLES and not only flat Mach-O binaries -- its changelog discusses `--shallow` bundle mode and child-bundle signing "compatible with the behavior of Apple's codesign" -- which is exactly what a `.app` inside an `.ipa` needs. `--format ipa` needs NO NEW TOOL AT ALL: an `.ipa` is a zip containing `Payload/<Name>.app/`, so clang plus the iPhoneOS SDK produce the Mach-O (both xim), `dist-apple` assembles the bundle, `rcodesign` signs it, and a zip step finishes. So iOS PACKAGING closes entirely inside the ecosystem. `--format dmg` and `--format pkg` each need a creator as well as a signer (`libdmg-hfsplus`, `xar` -- both open source, neither measured), and are recorded as gaps with a known shape rather than claimed. THE iOS RUNTIME BLOCKER IS A LICENSED IMAGE, NOT A MISSING EMULATOR, and the obvious answer is wrong in an instructive way. QEMU can emulate ARM iOS hardware and community projects boot iOS on it; what none can supply is the iOS kernel and root filesystem, because distributing iOS images is against Apple's terms. The emulator is not the scarce thing. That is the SAME SHAPE as the Android question with the opposite answer, and the comparison is the point: Android's emulator is Apache-2.0 and its `default` images are AOSP, so the loop closes -- and `qemu-aarch64 -L` needs no emulator at all. iOS cannot close, for a reason that is not about mcpp, xlings or effort. It is the one row whose execution is blocked by a licence rather than by work. On macOS the Simulator is the host's and that is correct: it is a (b)-category proprietary runtime and `xcrun simctl spawn` is an argv prefix, so section 5's model already covers it once R1 gives the simulator a row. And Darling is recorded as an UNMEASURED candidate for the macOS rows -- GPL-3.0, actively developed, and it reimplements Darwin's libraries rather than redistributing them, so unlike the iOS image it carries no licence blocker. A row does not move on a plausible mechanism.
…s the simulator route
ASKED DIRECTLY AND IT IS THE STRONGEST RESULT IN THE SECTION: running on real
hardware needs no Apple or Google software on either platform.
Android adb push + adb shell Apache-2.0, already packaged
iOS pymobiledevice3 GPL-3.0, pure Python 3, no compiled
extensions, Linux/Windows/macOS,
2736 stars, pushed 2026-09-10
iOS libimobiledevice LGPL-2.1, the C library it models,
8177 stars, pushed 2026-06-10
pymobiledevice3 requires no Xcode, works with the system usbmuxd, and covers
app management plus iOS 17+ tooling over a tunnel; libimobiledevice needs no
jailbreak. So the iOS row's execution story is not "needs a device on a Mac":
build, bundle, sign, package, deploy and run all close, and the ONLY line that
does not is a provisioning profile and signing identity -- a credential, which
is category (c) and which no package manager closes. That is the same boundary
a developer already lives with under Xcode.
It is materially better than the simulator route and the reason is worth
stating: the simulator is blocked by a licensed IMAGE that cannot be
redistributed, while a real device supplies its own OS and the only thing
crossing the boundary is a signature the developer already owns.
WHERE A DEVICE SESSION LIVES: the runner program absorbs deployment, so no
fourth verb is needed -- "adb push" followed by "adb shell" is one operation
from mcpp's side. Both mechanisms are used for what each is: a PLUGIN produces
the artefact (dist-ipa, build time), a PACKAGE deploys and runs it (the runner
program, run time), and the ENGINE names the runner (the existing "runner"
key). The ordering is a real dependency, not a convention: the iOS device
runner has nothing to install until dist-ipa has produced a signed file, so R9
precedes R12.
THE iOS IMAGE IS A LOCATOR AND NEVER A RE-HOST. An image in a public index is
redistribution of Apple's operating system whatever it is labelled, and this
differs from the Android decision earlier in this document in a way worth
stating precisely: there, Apache-2.0 licence files were verified INSIDE the
archives and clause 3.5 genuinely applies; here there is no open-source
component to invoke. The legitimate form is the locator tier
iphoneos-sdk.lua already documents -- the index carries a path and a probe,
not bytes -- and R4 is its engine half. With R12 in place no image is on the
critical path at all.
… is a machine fact and a declaration is not Three homes for an iOS image path were considered, and two are wrong for reasons worth recording because each looked right first. A LOCATOR PACKAGE has nothing to probe. Locators work where the thing has a conventional location -- vswhere, /Applications/Xcode.app -- and an image a user legally owns is wherever they put it. Its version axis would be meaningless too: a locator for 18.0 cannot verify that what it found is 18.0. THE PATH IN build.mcpp FAILS ON THIS REPOSITORY'S OWN ANALYSIS. A build program is per-package, committed, and its declarations are persisted in the cache record; a path is per-MACHINE, not committable, and must not be a build input or identical sources produce different build directories. That is exactly the precedence #564 established for `[build] default_jobs` -- invocation > project > machine -- and an image path sits on the machine level as a job count does. And a sharper failure: a variable read through `env_or` is NOT in the build program's contract environment, so it is not in its re-run key. Change the path and the program does not re-run, while the cache record persists the old runner (tag "runner" is non-empty). A path that was changed and appears not to have been is the defect class recorded most often here, and this would introduce it deliberately. THE DESIGN: the runner PROGRAM owns the path, resolved at run time. The engine names a runner and does not know what an image is; the index ships the emulator and no image; the plugin declares WHICH runner and produces the artefact; the program reads its own configuration each invocation. The path enters mcpp at no point -- not the index, the build program, the cache record or the fingerprint. That is the boundary cargo states for itself and which section 5.2 already quotes, applied one level further: "that responsibility falls to the runner program itself." It dissolves the staleness rather than mitigating it, because a run-time resolution has no record to go stale. The distinction the section arrives at is one sentence: declaring a NAME is cache-safe, baking a PATH is not.
…paid per target (2026.9.11.3)
WASM32-EMSCRIPTEN IS `verified`. `mcpp run --target wasm32-emscripten` prints
`1-2-3` on a source that imports std, from the row alone -- and the project
side needed NO new vocabulary. That is the first real measurement of what this
design costs: seven engine changes, and a project that builds for Linux builds
for the web by changing one flag.
Each of the seven was found by the previous one's failure, and each was a
predicate that was CORRECT about the rows its author had in mind:
1. the payload chosen by the TARGET in `to_xim_package`, or `xim:llvm`
answers for a wasm target
2. `XimToolchainPackage::frontendSubdir`, or `em++` is looked for in `bin/`
3. `Triple::has_own_sysroot()` in the SHARED PRODUCER `host_compile_tokens`.
The first attempt put it at one of its three callers, so ordinary compiles
stopped taking this host's headers and the std module precompile went on
taking them -- which is where it had been failing. One decision, one site,
three readers.
4. the same predicate in `resolve_link_model` -- the MODEL, not its two
channels; the comment at the second channel already recorded that a reader
who fixed only the first "saw the identical error"
5. the same finding twice: `discover_link_runtime_dirs` put the COMPILER's
own runtime directories on the ARTEFACT's link line. For every row that
predates these two, those are the same directory.
6. `host_can_serve` -- the first version returned true unconditionally, the
same over-broad shape as the branches above it. The TARGET MATRIX caught
it: `xim:emsdk` publishes linux only.
7. `Format::Wasm`, which the module PREDICTED AND DEFERRED in its own
comment. While it was missing, every wasm build warned about a `libc++.so`
that cannot exist for the target.
Wasm also joins the capability pins: nothing else emits WebAssembly, so a
declared `gcc@16.1.0` is a request that cannot be honoured, and saying so beats
resolving gcc and failing inside it.
A MACHINE-SCOPED SCAN WAS PAID PER TARGET: 17948ms -> 4ms.
mcpp build loader-tags stage 170ms one 21 MB binary
mcpp test loader-tags stage 17948ms 108 binaries, 2.4 GB
and flat at ~17.9s across every target measured, so the cost is the whole
artefact set rather than the one being built. `mcpp test` drives the backend
once per target, so a 110-target suite paid it 110 times -- 3 minutes became a
projected 33.
`check_dlopen_surface` ran a full `inspect_elf_runtime` over EVERY linked
artefact to collect SONAMEs, before it discovered the surface was empty. Its
record said `members=0, walked=0` every time, and the artefacts are
executables, which cannot carry `DT_SONAME` at all. Both halves fixed, and the
shape is worth naming: the expensive work ran BEFORE the cheap test that makes
it unnecessary. The record is still published -- a field that disappears is
worse than one that says why it is empty.
`min_api_level`, REUSING THE MECHANISM `macos_deployment_target` ALREADY HAS.
Measured: `clang -target aarch64-linux-android21 -print-effective-triple`
answers `aarch64-unknown-linux-android21`, so the level belongs on the effective
triple -- and NOT on the canonical one, because mcpp keeps its own target
vocabulary and maps it. macOS is already this shape. So `[target.<triple>]
min_api_level = 24`, fused by the parameter `llvm_triple()` already takes, and
in the FINGERPRINT because the level selects which bionic symbols are visible.
The key's slot is renamed `minPlatformVersion`: a target is either Apple or
Android, so one slot cannot hold both, and both answer one question.
The name comes from Android's own words -- the NDK's CMake toolchain documents
`ANDROID_PLATFORM` as "the minimum API level supported by the application or
library". `ndk_api_version` was rejected twice over: "version" is not Android's
word, and `ndk_` names the TOOLCHAIN while one NDK serves a RANGE of levels.
THE FOUR-FIELD SPELLING IS ACCEPTED. `em++ -v` passes
`-target wasm32-unknown-emscripten` and rustc lists that spelling; refusing what
every other toolchain prints is a cost with no benefit. `str()` still returns
the three-field form, which is what keeps the output directory, `cfg()` and the
ABI tag single-valued.
AND THE EOL DISTRO LEG IS SWAPPED. debian-11 fails with `E: Release file ... is
expired` -- bullseye is end-of-life and its security metadata has expired,
which is a property of the distribution. Measured while replacing it: debian 11
and ubuntu 20.04 both carry glibc 2.31, so the "older glibc" coverage was
ALREADY duplicated by the ubuntu-2004 leg. Bookworm's 2.36 sits between that
and debian-testing, so the leg now covers a point the matrix did not have.
… line names the payload THE RULE IS SHARED AND THE REASON IS NOT, which `prepare.cppm` already said: "one sentence covering both would be wrong about one of them: a PE+musl target is not bare metal, and a reader told it is stops reading." A third row was then added without a third reason, so the sentence became wrong about the new one. Measured: `--target wasm32-emscripten` with a declared gcc was refused CORRECTLY and explained with "No gcc payload emits a PE with a musl C library" -- a true sentence about a different row. AND THE GATE ASKED THE WRONG QUESTION, which is the defect a reader would not think to look for. It tested `family != Llvm`, right while every capability-pinned row pinned llvm. `wasm32-emscripten` pins `emsdk@6.0.9`, and emsdk normalises to the LLVM FAMILY because `em++` is clang -- so a declared `llvm@22.1.8` passed the gate, was never refused, and resolved the generic llvm payload for a target it cannot emit. The condition is now the row's own pin, which is the question it was always answering. The closing line names that pin too, instead of the fixed word "llvm". `tests/e2e/640` holds five properties: each row's own sentence, the declared-llvm gap, and the closing line naming this row's pin. Against released 2026.9.10.2 the three wasm cases go red and the two pre-existing rows stay green. THE RESOLVED LINE NAMES WHICH PAYLOAD ANSWERED. `emsdk@6.0.9` printed as `Resolved llvm@6.0.9` -- indistinguishable from the real `xim:llvm`, and not what the user typed. `mcpp toolchain list` has the same problem and the matrix scan takes one toolchain per family, so two llvm-family payloads on one host could not both be enumerated. The family and the payload are two questions -- which flag vocabulary, and which archive -- and `to_xim_package` already answered the second from the target. `ToolchainSpec::payloadName` is the field that lets it be said. Empty means the family's own payload, which is every row but these two, so no existing output moves. AND THE MATRIX EXPECTATIONS ARE THE TRUTHFUL READING RATHER THAN THE AMBITIOUS ONE. Naming `emsdk@6.0.9` in the declared-toolchain column made the scan try to INSTALL it on every host -- `emsdk@6.0.9 装不上,而期望表声明了它` -- because that column is the axis the scan installs from. With wasm a capability pin, the verdict for a declared gcc or llvm is `capability-pin` on a host that can serve the row and `host-cannot-serve` on the two where `xim:emsdk` is not published. Verified against the engine rather than assumed: gcc refused, llvm refused, emsdk resolves and builds. 109 unit tests pass; 638, 639, 640 and 233 pass.
… stop disagreeing
`mcpp build --target aarch64-linux-android` produces a real Android artifact
with no project vocabulary beyond `--target`:
aarch64-linux-android -> ELF 64-bit LSB pie, ARM aarch64,
interpreter /system/bin/linker64
x86_64-linux-android -> ELF 64-bit LSB pie, x86-64, same interpreter
Both rows share ONE pin, `android-ndk@30.0.16248370`, and that is where every
defect below came from: the NDK names no architecture, `--target` does, so each
site that fails to say which target is exposed here and by nothing else. On
wasm none of them appear, because `em++` has exactly one target.
`preview`, not `verified`: both were BUILT and neither was EXECUTED, which is
the difference between the two tiers.
## Five engine gaps, each found by the previous one's failure
1. The std module's PRECOMPILE never received `--target`. `stdModuleTargetFlags`
reached only the codegen command, on the reading that step one needs headers
and step two needs the machine. Step one needs both: a `--precompile` that
does not say the target resolves the standard library's own
`#include <__config>` against the building machine. This file already
recorded the identical error text from a Windows host in 2026-08 -- same
cause, different route. The precompile now takes the machine from whichever
source carries it, preferring `stdModuleFlags` because it is a superset.
2. `-D__BIONIC_CTYPE_INLINE=`. bionic declares `isalnum` and its neighbours
`static inline`; libc++'s module surface exports them with `using
std::isalnum`, and a using-declaration cannot export internal linkage, so
the precompile fails on 14 names at once. Scoped to the std module: the rule
being satisfied is about exporting from a module, and a unit that includes
<ctype.h> directly is entitled to bionic's inline definitions.
3. "A toolchain that ships its own sysroot is told nothing" was one token too
strong, and this function already said so further down. The early return
stood in front of "THE TRIPLE, SAID OUT LOUD", which states the opposite
rule for the same reason. Both are right about their own object: the SYSTEM
is the payload's, WHICH TARGET is still mcpp's to say. Reported by the module
loader rather than by either compile, then eight cascading "use of
undeclared identifier 'std'" lines, which is what a reader sees first.
4. The LINK line had no target either. Both link branches are skipped for such
a target and that is correct for what they carry -- C library, C++ runtime,
crt objects and loader are all inside the SDK. What the driver cannot do is
guess which. Falling through empty linked the target's objects with the
host's startup files: six host objects on an aarch64 link. The new third
branch carries `crossTarget` and nothing else.
5. `discover_link_runtime_dirs`' gate was evaluated at the wrong time. The gate
is right; it runs during DETECTION, before the target is assigned, when
`targetTriple` is still the host's. The artifact is what showed it: `-L
<ndk>/.../prebuilt/linux-x86_64/lib/x86_64-unknown-linux-gnu`, whose last
component is this machine's triple, so the string names the question asked.
## Two artifact checks called a correct artifact a defect
Same shape both times: a rule true of a HOST-TARGETED artifact, applied to a
cross one.
- The hermetic link check counted `/system/bin/linker64` as outside the
sandbox. It is the one path this function inspects that is NOT resolved here:
it is recorded in the artifact and read by the DEVICE. Android's is fixed by
ABI and cannot be inside a payload. The message was accurate about what it
saw and wrong about what it meant, and it invited the reader to reinstall an
unrelated glibc payload.
- Runtime closure rule B compared `PT_INTERP` against the HOST's
`RuntimeBinding glibc@2.44` and reported a proven defect -- "one process
cannot mix runtime payloads" is a true sentence about a process that will
never exist. The linux/glibc guard above it does not cover this: an Android
triple's `os` IS `linux`, deliberately.
## host_can_serve stops compiling "Linux" into the engine
The predicate returned `mcpp::platform::is_linux` for own-sysroot rows, and its
own comment named the expiry: "when a darwin or windows NDK lands in the index
-- upstream publishes both -- this is the one line that changes." It landed.
Two halves of one goal then disagreed: the index published the payload on three
hosts while the engine deleted the row from `toolchain list` on two of them.
The symptom was not a wrong answer but an ABSENT one -- on macOS,
`mcpp build --target wasm32-emscripten` reported a target this table knows as
one it had never heard of, which is what the `planned` tier exists to avoid.
Neither half is where it was found. `scan (macos-arm64)` and
`scan (windows-x86_64)` failed on a CELL COUNT, 24 measured against 25
declared, and the one missing cell named the row. A per-host job comparing
against a checked-in table is the only thing here that can see a row disappear.
That predicate's unit test was ALSO true by arithmetic on one host:
`EXPECT_EQ(host_can_serve(*wasm), mcpp::platform::is_linux)` passes on Linux
because there `is_linux` IS `true`. A criterion whose expected value is the host
it runs on cannot report a change on the other two. Now stated unconditionally,
with the "this predicate can still say no" half restored.
## The capability-pin reason fell through a second time
Android became a capability row while the reason chain still had three arms, so
it was explained with the PE+musl sentence -- the same wrong answer recorded
for wasm, reached the same way: a fourth case falling into an `else` written as
the third's answer. So the last arm now names its own row and the fallthrough
is generic; a capability added later gets a sentence that is merely unspecific
instead of false.
Android's reason is not the other three's. They are refused because the
toolchain cannot emit the FORMAT; a stock clang emits aarch64 ELF perfectly
well. What it cannot supply is bionic, and no package adds that to another
compiler.
## The simulator is a row, not a runner
`env = "sim"` gives `aarch64-ios-sim` and `x86_64-ios-sim` -- Rust's pair modulo
the vendor segment this table already elides. Apple's own `-simulator` spelling
parses to the same row, because clang prints that form and a reader who pastes
it back must not be told mcpp has never heard of it. Both arches, because the
simulator runs the HOST's architecture. Both `planned`: the blocker is the same
licence question as the device row, not a payload.
## The API level's default comes from the payload
`min_platform_version` returned an empty string with the comment "the NDK's own
default, which clang supplies". Never verified, and false: bionic's
<sys/cdefs.h> refuses an unversioned triple outright. So the level is mandatory
and a project that never heard of API levels still needs one. It comes from the
NDK's own `meta/platforms.json` -- `{"min": 21, "max": 37}` for r30 -- so a
newer NDK changes the default by being installed rather than by being edited
into this file. 0 when unreadable, which the caller turns into a refusal naming
`min_api_level`; a guessed level would be worse, because it selects which
bionic symbols exist. The same false claim had a second copy in
`llvm_triple()`'s comment and is corrected there too.
## Two vocabulary consequences of the rows leaving `planned`
- A bare `aarch64-linux` must never complete to Android. The rows share the
`arch-os` prefix because the kernel IS Linux; that does not make bionic a
candidate C library for a request that named none. `aarch64-linux` had two
supported siblings and resolved as ambiguous, where it had completed to
`aarch64-linux-musl` before, and both outcomes of that ambiguity are wrong.
Excluded from `siblings` too, since that list is what the diagnostic prints.
- `aarch64-unknown-linux-android21` now parses. mcpp PRINTS that string one
line above the build it describes, and pasting it back answered `unknown
target`, because the env match read `k == "android"` while the API level rides
that segment. The msvc branch below carries the identical note for the
identical reason; Android was missed. One prefix covers all four spellings.
## Tests
- `tests/e2e/641` asserts the vocabulary half, which is what a runner without a
704 MB NDK can assert: both rows name the payload, neither is refused for its
tier, the simulator rows say `planned` rather than `unknown`, the effective
triple parses back, a bare `aarch64-linux` never mentions android, and
`min_api_level` has a floor refused by name.
- `tests/e2e/640` case 6 is EXHAUSTIVE, because adding an arm is what keeps
failing: it takes every pinned row from `toolchain list`, declares a
toolchain that is not its pin, and asserts the PE+musl sentence appears for
exactly one. Measured: 17 capability rows of 34 targets, PE+musl printed once.
The denominator comes from the engine, so a row added tomorrow is in it
without this file being edited -- and it agrees with the unit test's
independently written set of 17.
- `tests/matrix/expected.tsv`: the Android cells move to `capability-pin` and
the wasm cells on macOS/Windows move off `host-cannot-serve`, because the
refusal is now the row's rather than the host's. `android-ndk` deliberately
does NOT enter the declared-compiler column: the scan installs from that axis.
Verified locally, 54/54 payload and 28/28 graph cells for linux-x86_64.
- 109 unit test binaries pass. Three tests failed first and each was correct to:
one asserted the Android rows were planned, one asserted Android was not a
capability pin with the note "it moves when the row does", and one asserted a
bare `aarch64-linux` completion that two new supported siblings had made
ambiguous.
… the platform's emulator `adb push` then `adb shell ./andtest` printed `1-2-3`, exit 0, on an API 24 x86_64 system image under Google's own emulator with KVM, from a source that imports std and no project vocabulary beyond `--target`. So one pin serves two rows at two tiers, and the difference is execution rather than confidence in the build. The device row stays `preview` and the reason is recorded where the next attempt will look: the emulator refuses a foreign guest outright, and the documented qemu-user fallback needs four files extracted from an ext4 partition image by `debugfs` -- the one program in `xim:e2fsprogs@1.47.3` that is a broken build. That is an ecosystem defect with its own record in the index, and fixing it moves this row without an engine change. The docs now also reconcile the two records that would otherwise disagree: the support table's Windows cell for both Android rows is `—` because the index publishes no Windows NDK (its archive ships no libc++ module surface), while `mcpp toolchain list` still shows the rows there, because which hosts an index serves is the index's answer and not a constant compiled into the engine.
…rning stops advising what is now refused
EXAMPLE. `examples/13-platform-targets` is one source with no `cfg`, built for
Linux, WebAssembly and both Android ABIs by changing only `--target`. The
curriculum had twelve examples and none for any of the three platforms this
release adds, which is a gap in the one place a reader looks first.
Its host build needs no payload, so it is in BUILD rather than SKIP -- and
that is itself worth one build: a manifest naming targets the runner has no
payload for must still parse and build for the host. The cross legs are not
built there; ci-target-matrix scans every row on four hosts and tests/e2e/641
asserts the vocabulary.
Every claim in its README is measured against the artifact the README
describes, not carried over from another one: the host and wasm runs print
`1-2-3`, both Android artifacts are the file(1) output shown, and the x86_64
artifact was pushed to an API 24 emulator image and executed there.
A MISLEADING WARNING, FOUND BY BUILDING openkal FOR ANDROID. When a project's
target side comes from the dependency graph, mcpp warns that the row's
convention pin was unnecessary and tells the reader to declare their own
default instead. On a capability row both halves of that are false:
warning: ... so gcc@16.1.0 would have served x86_64-linux-android.
State the preference: [target.x86_64-linux-android]
toolchain = "gcc@16.1.0"
$ (declaring exactly that)
error: target 'x86_64-linux-android' cannot be emitted by 'gcc@16.1.0'.
Following the advice replaced a warning with an error. The first claim is false
on its own terms too: this gcc payload cannot emit an Android object whatever
the graph supplies. `graph` answers "who supplies the SYSTEM"; a capability pin
answers "who can emit the FORMAT AND THE SYSTEM", and only the second decides
whether a substitution was avoidable. The warning is now gated on
`pin_is_capability()`, whose row set is asserted exhaustively in
test_toolchain_triple, so the gate cannot silently cover every row.
DOCS: docs/24-openkal-cross.md (+zh) now answers what the three new platforms
mean for openkal, because the answers differ and the difference is where an
implementation sits relative to a C library.
Android shares openkal-linux UNCHANGED. It is written on the Linux
kernel's own system calls and borrows nothing from a C library;
Android's kernel IS Linux, the per-arch syscall ABI is identical,
and sys.h dispatches on __x86_64__/__aarch64__ -- architecture, not
OS. `cfg(os = "linux")` is true for an Android triple, so a
portable program selects it with the line a Linux consumer already
writes. Measured: openkal-linux compiles for both ABIs, and a
program written against openkal alone -- no C library, no
`import std` -- printed `openkal: 1-2-3` with exit 0 on an API 24
emulator image.
iOS would share openkal-macos by the same argument, and the argument
is not evidence: the SDKs are not redistributable, so there is
nothing to build against and the rows claim nothing.
Web needs a NEW implementation and a different shape. Emscripten has no
kernel and no system calls; an implementation would have to sit
ABOVE its C library, which the specification permits and which is
new software rather than a sharing decision. Until then the row is
served the ordinary way, by the `xim:emsdk` payload, which is what
its `verified` tier records.
The docs/21 support table and the two Android rows' Windows cells were also
reconciled with the index: `—` there means no payload is published for that
host, while `toolchain list` still shows the rows, because which hosts an index
serves is the index's answer and not a constant compiled into the engine.
…ve criteria THE PRE-MERGE REVIEW'S OWN FINDING. Mapping every behaviour change in this PR to a criterion left seven without one, all of them "the Android build works" -- testable only with a 704 MB payload CI does not have. Two of the seven are pure functions of a `Toolchain` and needed no payload at all, which makes them the two worth closing. `std_module_build_commands`: the PRECOMPILE carries the machine flags when only `stdModuleTargetFlags` has them, and states the target exactly ONCE when `stdModuleFlags` also does -- the superset case, where concatenating both would put `--target=` on the command line twice. `host_compile_tokens`: an own-sysroot target receives EXACTLY the target flag and nothing else. Asserted as the whole vector rather than as "contains", because the property is that nothing else is emitted: this host's glibc headers reaching a wasm compile is the measured failure the gate exists for. THE CONTROL TOOK TWO ATTEMPTS AND BOTH FAILURES WERE THE CONTROL'S. Asserting that a hosted target receives more than one token failed, because a bare `Toolchain` carrying no payload has nothing to reconstruct either -- both sides produced exactly the triple. Reaching for `--no-default-config` without a payload that HAS a cfg was the same mistake once removed. `FakeClangPayload` writes a `clang++.cfg`, which is what makes the discriminator real, and the bypass is the right one to use because withholding it from an SDK is a property the gate's own comment states. Both verified in both directions: reverting each fix turns its test red.
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.
One PR, one version, the whole mcpp-side capability. Commits are the
checkpoints.
Two design records drive it:
.agents/docs/2026-09-11-platform-targets-design-review.md(the ownershipquestion for iOS/Android/Web, answered against cargo, Zig, CMake, Bazel and the
platform tools) and section 11 of the distribution record.
wasm32-emscriptenisverifiedFrom the row alone -- no
[target.…]override -- on a source that writesimport std;. And the project side needed no new vocabulary: a projectthat builds for Linux builds for the web by changing one flag. That is the
first real measurement of what this design costs, and it is seven engine
changes.
Each was found by the previous one's failure, and each was a predicate that was
correct about the rows its author had in mind:
to_xim_packagexim:llvmanswering for a wasm targetXimToolchainPackage::frontendSubdirem++inbin/has_own_sysroot()in the shared producerstdint.hin a wasm compileresolve_link_model--dynamic-linker=…ld-linux-x86-64.so.2reachingwasm-lddiscover_link_runtime_dirshost_can_serveFormat::Wasmand its mechanismlibc++.sothat cannot exist for the targetThree of those are worth reading as one finding. #3 was first put at one of
its three callers, so ordinary compiles stopped taking the host's headers and
the std module precompile went on taking them -- which is where it had been
failing. #4 is the model and not its two channels, and the comment at the
second channel already recorded that a reader who fixed only the first "saw the
identical error and could reasonably conclude the fix had not worked". #5 is
#4's finding a second time: for every row predating these two, "the
compiler's own runtime" and "the artefact's runtime" are the same directory.
#7 the module had predicted and deferred in its own comment: "Adding
Format::Wasmis deferred to whoever gives this module a mechanism for it."#6 was caught by the target matrix, not by reasoning: the first version
returned
truefor any SDK target, which is the same over-broad shape as thebranches above it.
xim:emsdkpublishes Linux only, so macOS and Windows mustanswer
host-cannot-serve.A machine-scoped scan was paid per target: 17948ms to 4ms
Flat at ~17.9s across every target measured, so the cost is the whole
artefact set rather than the one being built.
mcpp testdrives the backendonce per target, so a 110-target suite paid it 110 times -- 3 minutes became a
projected 33.
check_dlopen_surfaceran a fullinspect_elf_runtimeover every linkedartefact to collect SONAMEs, before discovering the surface was empty. Its
own record said
members=0, walked=0every time, and the artefacts areexecutables, which cannot carry
DT_SONAMEat all.Both halves are fixed and the shape is worth naming: the expensive work ran
before the cheap test that makes it unnecessary. The record is still
published -- a field that disappears is worse than one that says why it is
empty. Measured after: 3051ms once, then 4ms per target; the suite runs in
5.77s.
min_api_level, reusing whatmacos_deployment_targetalready doesThis recommendation was written one way, reversed on a measurement, and
reversed back when the measurement turned out to answer a different question.
The record keeps all three, because the mistake in the middle is the
instructive part.
clang -target aarch64-linux-android21 -print-effective-tripleanswersaarch64-unknown-linux-android21, so LLVM carries the level in the envfield. That says where LLVM writes it and nothing about where mcpp should store
it -- mcpp keeps its own vocabulary and maps to a compiler target, and
prepare.cppmsays so in as many words: "The triple is mcpp's vocabulary(
aarch64-macos); the flag carries the spelling a compiler takes(
arm64-apple-macos14.0)."macOS already solved this, and
llvm_triple()already takes the versionparameter -- so the objection that a manifest key would give it a second
input was false when I raised it.
aarch64-linux-android-- names the output dir,cfg(env=), the ABI tagaarch64-unknown-linux-android24The fingerprint slot is renamed
minPlatformVersion: a target is either Appleor Android, so one slot cannot hold both, and both answer one question. The
rename costs no extra rebuild -- the mcpp version is already in that key.
The field name is Android's own: the NDK's CMake toolchain documents
ANDROID_PLATFORMas "the minimum API level supported by the application orlibrary".
ndk_api_versionwas rejected twice over -- "version" is notAndroid's word (it is level), and
ndk_names the toolchain while one NDKserves a range, which would reintroduce exactly the confusion this
resolves.
The four-field spelling is accepted
em++ -vpasses-target wasm32-unknown-emscripten; rustc lists thatspelling. Refusing what every other toolchain prints is a cost with no benefit.
str()still returns the three-field form, which is what keeps the outputdirectory,
cfg()and the ABI tag single-valued.The EOL distro leg is swapped
debian-11fails withE: Release file ... is expired-- bullseye isend-of-life and its security metadata has expired, which is a property of the
distribution rather than of this workflow. Measured while replacing it: debian
11 and ubuntu 20.04 both carry glibc 2.31, so the "older glibc" coverage was
already duplicated by the ubuntu-2004 leg. Bookworm's 2.36 sits between that
and debian-testing, so the leg now covers a point the matrix did not have.
Verification
109 unit tests pass (5.77s, from a projected 33 minutes).
tests/e2e/639(eight scanner criteria) and
tests/e2e/233pass. 12 wasm matrix cellsrewritten --
okon the two Linux hosts,host-cannot-serveon macOS andWindows, which is where
xim:emsdkis published.mcpp run --target wasm32-emscriptenre-verified after every change in this branch.Deliberately not in this PR
R4 (generalising
@system) is withdrawn, not deferred: its motivatingsecond instance dissolved once
xim:iphoneos-sdkcovered iOS as a package, andrelaxing a refusal without a case is how
gcc@systemgets in -- the exactspelling that refusal names.
The
.wasmsibling as an explicit implicit output is not here. It isalready produced (em++ writes it beside
-o), so this is a graph-bookkeepingimprovement rather than a capability, and it is recorded as E11.