Skip to content

Commit 2e348e3

Browse files
committed
Target rows become executable, and a machine-scoped scan stops being 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.
1 parent d62d46e commit 2e348e3

15 files changed

Lines changed: 684 additions & 28 deletions

‎.agents/docs/2026-09-11-platform-targets-design-review.md‎

Lines changed: 242 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,9 @@ package -- no new mechanism, again.
302302
| # | question | recommendation | why |
303303
|---|---|---|---|
304304
| R1 | the simulator's spelling | `env = "sim"`, giving `aarch64-ios-sim` and `x86_64-ios-sim` as their own rows | matches Rust's pair modulo a vendor elision mcpp already does; satisfies the row comment's own objection, which was to NOT having a separate row |
305-
| R2 | Android's API level | a `[target.<triple>] api = <n>` manifest key, and **it must enter the build fingerprint** | every system surveyed keeps it out of the triple (Rust: outside; CMake: `ANDROID_PLATFORM`; Gradle: `minSdk`), because it is a per-PROJECT minimum. The fingerprint is non-negotiable: it selects which bionic symbols exist, so two levels are two ABIs |
305+
| R2 | Android's API level | **`min_api_level` under `[target.<triple>]`, reusing the `macos_deployment_target` design** -- see 12.1 | mcpp maintains its own vocabulary and MAPS to a compiler target, so where LLVM carries the level says nothing about where mcpp stores it. macOS already does this exactly: a manifest key, a clean canonical triple, the level appended by `llvm_triple(param)` -- which already takes a version -- and the value in the fingerprint. One NDK serves a range of levels, so it is a project decision and not a toolchain property. No new rows |
306306
| R3 | the payload identity shown for `emsdk` | a display identity on the resolved toolchain, not a fourth `Family` | `em++` is clang and a fourth family would be a false claim about the compiler; what is missing is only that nothing prints which archive answered |
307-
| R4 | `@system` for a non-MSVC family | generalise it, with the row deciding whether it is permitted | the refusal argues from MSVC's uniqueness, and iOS is the second instance of exactly that situation. `xim:iphoneos-sdk` already names "locate what the machine has" as its third tier, and the engine has no spelling for it |
307+
| R4 | `@system` for a non-MSVC family | **withdrawn** -- see 12.1a | the second instance dissolved: `xim:iphoneos-sdk` serves iOS as a package, so no host locator is required. Generalising would admit `gcc@system`, which the existing refusal names by name and which costs hermeticity. A refusal should not be relaxed without a case |
308308
| R5 | device and simulator sessions | a `xim:` package shipping a runner program, named by `runner` | cargo states this boundary explicitly; no engine change, no new member family, and it puts platform knowledge in the ecosystem |
309309
| R6 | the tier each row can reach | `verified` for wasm (reached); `preview` for both Android rows and for iOS | Rust rates all three Tier 2. `verified` for Android is reachable and needs a CI lane, not a design |
310310
| R7 | signing a Mach-O or a `.app` | package `rcodesign` as `xim:rcodesign` and have `dist-apple` prefer it | MPL-2.0 with prebuilt static binaries for linux-musl (both arches), macOS universal and Windows. It removes the last host dependency from the iOS BUILD path, leaving only a device, the Simulator runtime and a notarization credential -- none of which is a program |
@@ -794,3 +794,243 @@ the second.
794794
The two Android rows are the nearest, and the thing blocking them is not
795795
payload work — both payloads are published and both execution routes are
796796
measured. It is one identity decision.
797+
798+
## 12. Self-review of this proposal, before implementing any of it
799+
800+
### 12.1 R2, three times, and the design mcpp already has
801+
802+
This recommendation was written one way, reversed on a measurement, and then
803+
reversed back when the measurement turned out to answer a different question.
804+
The sequence is recorded because the mistake in the middle is instructive.
805+
806+
**First answer: a manifest key.** Because the API level is a per-project
807+
minimum -- what Gradle calls `minSdk` -- and because every system surveyed
808+
keeps it out of the triple.
809+
810+
**The reversal, and why it was wrong.** A real clang was asked:
811+
812+
clang -target aarch64-linux-android21 -print-effective-triple
813+
-> aarch64-unknown-linux-android21
814+
815+
LLVM puts the level in the **env** field, so `env = "android<N>"` looked like
816+
LLVM's own model rather than an invention, with `llvm_triple()` staying pure.
817+
818+
That measurement is correct and it settles nothing here, because **mcpp
819+
maintains its own target vocabulary and MAPS it to a compiler target.** Where
820+
LLVM carries the level is a fact about the EFFECTIVE triple. Where mcpp carries
821+
it is a question about the CANONICAL one, and the two are deliberately
822+
different -- as `prepare.cppm` says in as many words: "The triple is mcpp's
823+
vocabulary (`aarch64-macos`); the flag carries the spelling a compiler takes
824+
(`arm64-apple-macos14.0`)."
825+
826+
The general lesson: **a measurement of another tool's model does not settle a
827+
question about ours.** It told me where LLVM writes the level, and I read it as
828+
telling me where mcpp should store it.
829+
830+
**The design mcpp already has, and which R2 should reuse.** macOS solved this
831+
exact problem and the machinery is complete on all three counts:
832+
833+
| | macOS, today | Android, proposed |
834+
|---|---|---|
835+
| manifest key | `macos_deployment_target = "14.0"` in `[package]` | `api = 24` in `[target.<triple>]` |
836+
| canonical triple | `aarch64-macos` -- clean | `aarch64-linux-android` -- clean |
837+
| effective target | `arm64-apple-macos14.0`, composed by `llvm_triple(param)` | `aarch64-unknown-linux-android24`, same call |
838+
| fingerprint | `put(s, "macos", b.macosDeploymentTarget)` | the same, one line |
839+
840+
`llvm_triple()` **already takes a version parameter** -- it is called as
841+
`want->llvm_triple(macos::deployment_target(...))` -- so the objection that a
842+
manifest key would give it a second input was already false when I raised it.
843+
The function is not pure of versions today; it is pure of the *manifest*, which
844+
is the property that matters, and the caller supplies the value.
845+
846+
**And one NDK serves a range of levels**, so the level is not a property of the
847+
toolchain either: naming `android-ndk@30.0.16248370` does not pin API 24. It is
848+
a project decision, which is what a manifest key is for.
849+
850+
So R2 is: **`api` under `[target.<triple>]`, appended to the effective triple
851+
by the existing parameter, and entered into the fingerprint the way
852+
`macos_deployment_target` already is.** No new rows, no new mechanism, and the
853+
table does not multiply as levels are added.
854+
855+
#### The field name, chosen against Android's own vocabulary
856+
857+
`api = 24` was the first spelling and it is too vague: it says nothing about
858+
WHICH property of the API is meant, and mcpp has no other `api` key to anchor
859+
the reading. The naming convention to follow is `macos_deployment_target`'s --
860+
**named in the platform's own words** -- so the question is what Android calls
861+
this.
862+
863+
Read from the NDK's own documentation rather than recalled:
864+
865+
| source | spelling | what the docs say |
866+
|---|---|---|
867+
| NDK CMake toolchain | `ANDROID_PLATFORM` | "specifies the **minimum API level** supported by the application or library" |
868+
| the same, alias | `ANDROID_NATIVE_API_LEVEL` | "Alias for `ANDROID_PLATFORM`" |
869+
| Android.mk | `TARGET_PLATFORM` | "The Android **API level** number the build system is targeting" |
870+
| Gradle | `minSdk` | the NDK docs state `ANDROID_PLATFORM` "corresponds to the application's `minSdkVersion`" |
871+
872+
So Android's concept name is **"API level"** -- the term its documentation uses
873+
most -- and the specific quantity here is the **minimum**.
874+
875+
Judged against that:
876+
877+
| candidate | verdict |
878+
|---|---|
879+
| `api` | rejected. Says nothing about which property, and anchors to nothing |
880+
| `ndk_api_version` | rejected on two counts. "version" is not Android's word, which is "level"; and `ndk_` names the TOOLCHAIN, while one NDK serves a RANGE of levels -- so naming it after the NDK reintroduces exactly the confusion 12.1 resolved |
881+
| `platform` | rejected. It is the NDK's own variable name, and `platform` is badly overloaded in mcpp -- a module, and the `xpm` platform tables |
882+
| `min_sdk_version` | rejected. Gradle's `minSdk` is an application-manifest concept for the Java side; for native code the NDK's word is API level, and mcpp is not building an app |
883+
| **`min_api_level`** | **chosen.** "API level" is Android's own term; "min" states the semantics the NDK docs state themselves; no platform prefix, because `[target.aarch64-linux-android]` already supplies it |
884+
885+
The kinship with `macos_deployment_target` is worth stating: both answer "the
886+
oldest OS release this artifact must run on", and both are named in their
887+
platform's vocabulary rather than in a shared abstraction. A single
888+
`min_os_version` for both would be more uniform and would cost the existing
889+
key a rename and both platforms their own words -- which is the trade this
890+
codebase has consistently declined.
891+
892+
#### The usage model
893+
894+
# mcpp.toml
895+
[package]
896+
name = "app"
897+
version = "0.1.0"
898+
899+
# The minimum Android API level this project supports -- the same decision
900+
# Gradle spells `minSdk`. One NDK serves a range, so this is the project's
901+
# to make and not the toolchain's.
902+
[target.aarch64-linux-android]
903+
min_api_level = 24
904+
905+
and what each layer then sees:
906+
907+
mcpp build --target aarch64-linux-android
908+
909+
canonical triple aarch64-linux-android identity: output directory,
910+
cfg(env = "android"), ABI tag
911+
effective target aarch64-unknown-linux-android24 what clang is given
912+
fingerprint includes 24 so 21 and 24 are two build
913+
directories, never one
914+
915+
# unset is legal and means the NDK's own default, which is what
916+
# `clang -target aarch64-linux-android` normalises to.
917+
918+
The parallel with the macOS key is exact, down to `[package]` versus
919+
`[target.<triple>]` being the only difference -- and that difference is right:
920+
a deployment target applies to every Apple artifact a project produces, while
921+
an API level applies to one target row.
922+
923+
### 12.1a R4 is withdrawn, because its second instance dissolved
924+
925+
R4 proposed generalising `@system` beyond MSVC, on the grounds that the iOS SDK
926+
is a second instance of "a proprietary thing that only exists where it is
927+
installed".
928+
929+
After the rest of this document, that is no longer true. `xim:iphoneos-sdk`
930+
exists and the licence permits at least the fetch-upstream tier, so iOS is
931+
served by a PACKAGE and needs no host locator. The locator tier its header
932+
documents is a fallback that nothing currently requires.
933+
934+
And the risk is concrete rather than theoretical: generalising the spelling
935+
admits `gcc@system`, which the existing refusal names and refuses by name, and
936+
which would let a build use the host's compiler and silently lose hermeticity
937+
-- the property the whole payload model exists for.
938+
939+
So the honest conclusion is not "defer until the narrow form is designed". It
940+
is that **the motivating case evaporated, and a refusal should not be relaxed
941+
without one.** If a real instance appears, the narrow form -- a per-row
942+
permission defaulting to denied -- is the shape to design then.
943+
944+
### 12.2 Two recommendations should be split by what they cost to be wrong about
945+
946+
R1 (`env = "sim"`) and R8 (accept four-field spellings) are both cheap and
947+
reversible: a new row is additive, and widening a parser is additive. They can
948+
go in without further argument.
949+
950+
R4 (generalise `@system`) is neither. It removes a refusal whose comment argues
951+
at length for why it exists, and a wrong generalisation admits
952+
`gcc@system` -- the exact spelling that comment refuses by name. The safe form
953+
is narrow: a per-row permission, defaulting to denied, so the refusal's
954+
reasoning stays true for every row that has not opted in.
955+
956+
### 12.3 What this proposal does not measure, stated plainly
957+
958+
* `rcodesign` has not been run. Its capabilities are quoted from its own
959+
changelog and documentation. Signing a real `.app` and having macOS accept
960+
it is the criterion, and no macOS machine has been involved.
961+
* `pymobiledevice3` has not been run, and there is no iOS device here.
962+
* Darling has not been run.
963+
* The macOS and Windows legs of every payload completed today are declared
964+
from verified hashes and have not been executed.
965+
966+
Every one of those is a claim about somebody else's software, and this session's
967+
record is that **every wrong guess in it was about what a vendor had done, and
968+
every one was cheap to check and was not checked.** The four above are the
969+
places that pattern would recur.
970+
971+
### 12.4 The task list, so nothing is left half-done
972+
973+
Grouped by repository, because the one-PR-per-repo rule makes the grouping the
974+
plan.
975+
976+
**mcpp (one PR, 2026.9.11.3)** -- the general capability, all of it:
977+
978+
| # | task | state |
979+
|---|---|---|
980+
| E1 | `wasm32-emscripten` resolves, builds and runs | **done**, measured `1-2-3` |
981+
| E2 | the seven gates the wasm row needed | **done** |
982+
| E3 | `Format::Wasm` and its mechanism | **done** |
983+
| E4 | wasm is a capability pin | **done** |
984+
| E5 | matrix expectations for 12 wasm cells | **done** |
985+
| E6 | unit tests for E1-E4 | **done** |
986+
| E7 | the EOL debian leg swapped for debian-12 | **done** |
987+
| E8 | R8: `parse()` accepts `wasm32-unknown-emscripten`, `aarch64-apple-ios` | todo |
988+
| E9 | R1: `aarch64-ios-sim` and `x86_64-ios-sim` rows | todo |
989+
| E10 | R3: a payload display identity, so emsdk is not shown as `llvm` | todo |
990+
| E11 | the `.wasm` sibling as an implicit link output | todo |
991+
| E12 | R6: Android and iOS rows to `preview` where the evidence supports it | todo |
992+
| E13 | docs: `20-toolchains`, `21-the-target-triple`, `22-target-side` + zh | todo |
993+
| E14 | CHANGELOG | todo |
994+
995+
| E15 | R2: `min_api_level` under `[target.<triple>]`, via `llvm_triple(param)` and the fingerprint, per §12.1 | todo |
996+
997+
R4 is not in that list because it is **withdrawn** (§12.1a), not deferred: its
998+
motivating case dissolved once `xim:iphoneos-sdk` covered iOS, and relaxing a
999+
refusal without a case is how `gcc@system` gets in.
1000+
1001+
**xim-pkgindex** -- one PR, already open as #812 plus the platform completion:
1002+
1003+
| # | task | state |
1004+
|---|---|---|
1005+
| X1 | Android CN mirrors under clause 3.5 | **done**, #812 |
1006+
| X2 | emsdk, NDK and emulator on all three hosts | **done**, hashes verified |
1007+
| X3 | `xim:python` aarch64 and a GLOBAL url | **done** |
1008+
| X4 | R7: `xim:rcodesign` | todo |
1009+
| X5 | R12: `xim:pymobiledevice3` | todo |
1010+
| X6 | R5: the two runner programs | todo, and they are new software rather than packaging |
1011+
1012+
**mcpp-plugins** -- one PR after the engine release:
1013+
1014+
| # | task | state |
1015+
|---|---|---|
1016+
| P1 | `dist-*` family, three members | **done**, 0.6.0 tagged |
1017+
| P2 | R9: `dist-ipa` | todo |
1018+
| P3 | R7's plugin half: prefer `xim:rcodesign` over the host's codesign | todo |
1019+
1020+
**mcpp-index**: publish `mcpp:plugins@0.6.0`. One task, blocked on the tag's
1021+
sha256.
1022+
1023+
**Recorded and not attempted**: R10 (`dmg`/`pkg` creators), R11 (Darling),
1024+
R13's implementation (the pattern is documented; the runner programs are X6).
1025+
1026+
### 12.5 The dependency order, and the one place it is not obvious
1027+
1028+
mcpp engine ──► release ──► mcpp-plugins ──► mcpp-index
1029+
│ │
1030+
└──► xim payloads ──────────┘
1031+
(independent, merge first)
1032+
1033+
The non-obvious edge is **X6 before P2 is wrong**. A device runner has nothing
1034+
to install until `dist-ipa` exists, so P2 precedes X6 -- the reverse of the
1035+
usual "payloads first" rule, and the reason is that here the payload consumes
1036+
the plugin's output rather than feeding it.

0 commit comments

Comments
 (0)