|
| 1 | +--- |
| 2 | +subject: targets |
| 3 | +status: active |
| 4 | +--- |
| 5 | + |
| 6 | +# SDK toolchains, the payload/engine seam, and iOS local verification |
| 7 | + |
| 8 | +**Status:** design, for review. Nothing here is implemented. |
| 9 | + |
| 10 | +**Scope.** Four items, in dependency order. The first is measured and only needs |
| 11 | +writing down; the second removes something rather than adding; the third is the |
| 12 | +only structural change; the fourth is the one with a new capability. |
| 13 | + |
| 14 | +| | item | shape | |
| 15 | +|---|---|---| |
| 16 | +| A | `aarch64-linux-android` becomes `verified`, and the extraction tool changes | evidence exists, two edits | |
| 17 | +| B | the `ndk` toolchain alias is withdrawn | deletion | |
| 18 | +| C | a payload describes itself, so the engine stops knowing the NDK | new contract at an existing seam | |
| 19 | +| D | iOS builds and runs on the simulator, locally and in CI | one parameter, one row column, one runner package | |
| 20 | + |
| 21 | +## A. Android's device row has an execution path after all |
| 22 | + |
| 23 | +`aarch64-linux-android` was recorded `preview` on the grounds that no execution |
| 24 | +path exists from an x86_64 host. That was wrong, and the reason it looked right |
| 25 | +is worth stating: the route the earlier design named was correct, and only its |
| 26 | +EXTRACTION TOOL was broken. |
| 27 | + |
| 28 | +Measured 2026-09-11: |
| 29 | + |
| 30 | +``` |
| 31 | +7zz l <system.img> Type = Ext ; bin/linker64 1120256 bytes |
| 32 | +7zz x <system.img> … 5 files extracted |
| 33 | +qemu-aarch64-static -L <root> <the mcpp-built aarch64 artifact> |
| 34 | + -> 1-2-3 exit 0 |
| 35 | +``` |
| 36 | + |
| 37 | +`xim:e2fsprogs@1.47.3`'s `debugfs` SIGFPEs on every filesystem-opening command, |
| 38 | +including against a control image made by that same payload's `mke2fs`, while |
| 39 | +`dumpe2fs`, `e2fsck` and `tune2fs` from the same archive work. `xim:7zip` reads |
| 40 | +ext4 directly and is already in the index. |
| 41 | + |
| 42 | +**Two consequences.** The row becomes `verified`, with the vehicle recorded: |
| 43 | +the platform's own emulator for `x86_64-linux-android`, qemu-user plus the |
| 44 | +system image's bionic for `aarch64-linux-android`. And |
| 45 | +`pkgs/a/android-system-image.lua` takes its four files with `xim:7zip` instead |
| 46 | +of `xim:e2fsprogs`, which makes the arm64 key installable for anyone rather |
| 47 | +than only where a working host `debugfs` happens to exist. |
| 48 | + |
| 49 | +The e2fsprogs defect stays recorded where it is. It is not this change's to |
| 50 | +fix, and nothing else in the index depends on that program. |
| 51 | + |
| 52 | +## B. The toolchain axis already has the semantics asked of it |
| 53 | + |
| 54 | +The question was whether an SDK toolchain behaves like any other: a default |
| 55 | +that needs no declaration, and a per-target override in `mcpp.toml`. Measured, |
| 56 | +it does: |
| 57 | + |
| 58 | +``` |
| 59 | +no declaration -> Resolved emsdk@6.0.9 (auto-installed) |
| 60 | +toolchain = "emsdk@6.0.9" -> Resolved emsdk@6.0.9 |
| 61 | +toolchain = "emsdk@5.0.0" -> resolves, install fails: not in the index |
| 62 | +toolchain = "android-ndk@30.0.16248370" -> Resolved android-ndk@… |
| 63 | +toolchain = "llvm@22.1.8" -> refused: capability pin |
| 64 | +``` |
| 65 | + |
| 66 | +So the default is the row's pin, the override is the ordinary |
| 67 | +`[target.<triple>] toolchain` key, the **version** is free within what the |
| 68 | +index publishes, and the **payload name** is fixed for a capability row. That |
| 69 | +is the same shape every other toolchain has, and nothing needs to change. |
| 70 | + |
| 71 | +**What does change is a half-supported spelling.** `compat.cppm` accepts `ndk` |
| 72 | +as an alias for `android-ndk`, and the capability gate compares the declared |
| 73 | +spelling against the pin's own name -- so `toolchain = "ndk@30.0.16248370"` |
| 74 | +parses and is then refused. A spelling the parser accepts and the gate rejects |
| 75 | +is worse than one spelling: it reads as a defect at the point of use. |
| 76 | + |
| 77 | +**Withdraw the alias** rather than teach the gate to normalise. One payload, |
| 78 | +one name. The gate's comparison is then correct by construction instead of |
| 79 | +correct by a second mechanism, and there is one string in the ecosystem for |
| 80 | +this payload -- the one the index uses. |
| 81 | + |
| 82 | +## C. The payload describes itself |
| 83 | + |
| 84 | +### The coupling, named |
| 85 | + |
| 86 | +Three NDK-specific facts live in the general engine today: |
| 87 | + |
| 88 | +| fact | site | what it encodes | |
| 89 | +|---|---|---| |
| 90 | +| `toolchains/llvm/prebuilt/<host>/bin` | `registry.cppm` `frontendSubdir` + `ndk_host_tag()` | the NDK's internal directory layout, per host | |
| 91 | +| the API floor | `registry.cppm` `ndk_min_api_level()` | that the floor lives in `meta/platforms.json`, and that file's schema | |
| 92 | +| `-D__BIONIC_CTYPE_INLINE=` | `prepare.cppm` | that this libc++ module surface needs it against this bionic | |
| 93 | + |
| 94 | +Each is a fact the **installing recipe already knows**: `pkgs/a/android-ndk.lua` |
| 95 | +computes `host_tag()` for its own probes, reads the payload to check its libc++ |
| 96 | +version, and applies the same define in its own self-test. The engine |
| 97 | +re-derives all three, which is why adding a second such SDK means editing the |
| 98 | +engine rather than publishing a package. |
| 99 | + |
| 100 | +### The contract |
| 101 | + |
| 102 | +The recipe writes one file beside the payload at install time; the engine reads |
| 103 | +it if present. |
| 104 | + |
| 105 | +``` |
| 106 | +<payload root>/.mcpp-toolchain.json |
| 107 | +{ |
| 108 | + "schema": 1, |
| 109 | + "frontend": "toolchains/llvm/prebuilt/linux-x86_64/bin/clang++", |
| 110 | + "platform_floor": "21", |
| 111 | + "std_module_defines": ["__BIONIC_CTYPE_INLINE="] |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +- **`frontend`** is a path relative to the payload root, already host-resolved |
| 116 | + by the recipe. The engine stops computing a host tag, and `frontendSubdir` |
| 117 | + becomes the fallback for a payload that ships no descriptor. |
| 118 | +- **`platform_floor`** is the string `llvm_triple(param)` already takes. It is |
| 119 | + the payload's answer, not a constant compiled in -- which is the property |
| 120 | + `ndk_min_api_level()` was written to get and this generalises. |
| 121 | +- **`std_module_defines`** reach the std module's own command assembly, which |
| 122 | + is a separate channel from the compile flags. They enter the build |
| 123 | + fingerprint, because they change what the module compiles to. |
| 124 | + |
| 125 | +### Three properties this has to have |
| 126 | + |
| 127 | +**Absence is compatibility, not silence.** No descriptor means today's |
| 128 | +behaviour exactly, so a released payload keeps working and there is no flag |
| 129 | +day. A descriptor that is PRESENT AND MALFORMED is refused, naming the file -- |
| 130 | +otherwise a typo reads as "an older payload" and the engine silently uses the |
| 131 | +hardcoded path for a layout that has moved. |
| 132 | + |
| 133 | +**It is not a general flag channel.** Three keys, each answering a question the |
| 134 | +engine already asks. A payload cannot inject arbitrary compile flags: that is |
| 135 | +`[build]` in a manifest, which is the project's to write, and a payload that |
| 136 | +could would be a package changing a build it does not own. |
| 137 | + |
| 138 | +**The descriptor is the recipe's output, not a file in the archive.** Upstream |
| 139 | +does not ship it and should not have to. The recipe writes it, which also means |
| 140 | +the recipe's tests can assert its content -- the same place the layout facts |
| 141 | +are already asserted. |
| 142 | + |
| 143 | +### What this does not do |
| 144 | + |
| 145 | +It does not make the NDK a fourth compiler family. `android-ndk` and `emsdk` |
| 146 | +normalise to `Family::Llvm` because their compiler IS clang; the family answers |
| 147 | +"which flag vocabulary does this compiler speak" and the payload name answers |
| 148 | +"which archive provides it". Two axes, and this change touches neither. |
| 149 | + |
| 150 | +## D. iOS: an ecosystem compiler and a located SDK |
| 151 | + |
| 152 | +### The compiler is ours; only the SDK is Apple's |
| 153 | + |
| 154 | +This is the question that shrinks the whole item. iOS does NOT need Xcode's |
| 155 | +clang. It needs: |
| 156 | + |
| 157 | +| | comes from | why | |
| 158 | +|---|---|---| |
| 159 | +| the compiler | `xim:llvm` | any sufficiently new clang emits arm64 Mach-O for an iOS deployment target | |
| 160 | +| the C++ runtime | the payload's libc++ | as on every other Apple row | |
| 161 | +| the **SDK** | the machine's Xcode | headers and stub libraries, not redistributable | |
| 162 | +| running on a simulator | the machine's `simctl` | a proprietary runtime that exists only on its own OS | |
| 163 | + |
| 164 | +`aarch64-macos` is `verified` today on exactly this split, which is the |
| 165 | +precedent: `xim:llvm` compiles, and the macOS SDK is located. |
| 166 | + |
| 167 | +### The mechanism exists |
| 168 | + |
| 169 | +`modules/platform/src/macos/macos.cppm` already has |
| 170 | + |
| 171 | +```cpp |
| 172 | +std::optional<std::filesystem::path> sdk_path(); // xcrun --show-sdk-path |
| 173 | +``` |
| 174 | + |
| 175 | +and it already tries `xcrun --sdk macosx --show-sdk-path` as its second form. |
| 176 | +So the change is a **parameter**, not a concept: |
| 177 | + |
| 178 | +```cpp |
| 179 | +std::optional<std::filesystem::path> sdk_path(std::string_view sdk = "macosx"); |
| 180 | +// "iphoneos" | "iphonesimulator" |
| 181 | +``` |
| 182 | +
|
| 183 | +Three existing callers are unchanged by the default argument. |
| 184 | +
|
| 185 | +### What `@system` means, and why it is not needed here |
| 186 | +
|
| 187 | +`@system` marks a toolchain mcpp LOCATES rather than installs -- `msvc@system` |
| 188 | +is the one instance, because MSVC is a compiler that cannot be redistributed. |
| 189 | +`parse_toolchain_spec` refuses `@system` for every other family by name, and |
| 190 | +that refusal is right: admitting `gcc@system` would cost hermeticity for no |
| 191 | +reason, since gcc is packaged. |
| 192 | +
|
| 193 | +The earlier design record proposed generalising it (R4) and then withdrew it. |
| 194 | +**It stays withdrawn, and this item does not revive it.** The thing being |
| 195 | +located for iOS is not a compiler, it is a **sysroot** -- and |
| 196 | +`TargetInfo::sysroot` plus `[target.<triple>].sysroot` are an axis that already |
| 197 | +exists. So: |
| 198 | +
|
| 199 | +``` |
| 200 | +row aarch64-ios pin = llvm@22.1.8 sysroot = <located iphoneos SDK> |
| 201 | +row aarch64-ios-sim pin = llvm@22.1.8 sysroot = <located iphonesimulator SDK> |
| 202 | +row x86_64-ios-sim pin = llvm@22.1.8 sysroot = <located iphonesimulator SDK> |
| 203 | +``` |
| 204 | +
|
| 205 | +A locator on the sysroot axis cannot admit `gcc@system`, because it says |
| 206 | +nothing about compilers. That is why this is the narrow change and R4 was not. |
| 207 | +
|
| 208 | +### Deployment target |
| 209 | +
|
| 210 | +The `-m…-version-min` flag differs between the two, which is the second reason |
| 211 | +the simulator is its own row rather than a flag on the device row: |
| 212 | +
|
| 213 | +``` |
| 214 | +aarch64-ios -miphoneos-version-min=<floor> |
| 215 | +*-ios-sim -mios-simulator-version-min=<floor> |
| 216 | +``` |
| 217 | +
|
| 218 | +The floor is the project's, on the axis that already carries |
| 219 | +`macos_deployment_target` and `min_api_level` -- one slot in the fingerprint, |
| 220 | +because a target is Apple or Android and never both. |
| 221 | +
|
| 222 | +### Running it |
| 223 | +
|
| 224 | +The engine learns nothing about simulators. `runner` is an argv prefix and the |
| 225 | +session belongs to a package: |
| 226 | +
|
| 227 | +```toml |
| 228 | +[target.aarch64-ios-sim] |
| 229 | +runner = ["simctl-run"] |
| 230 | +``` |
| 231 | + |
| 232 | +`simctl-run` is a program from a new `xim:apple-simulator-tools`, and it owns |
| 233 | +everything simulator-shaped: a bare Mach-O executable cannot be launched by |
| 234 | +`simctl`, so the program wraps it in a minimal bundle, boots or reuses a |
| 235 | +device, installs, launches, collects stdout and the exit code, and tears down. |
| 236 | +That is the R5 boundary -- platform knowledge in the ecosystem -- and it is |
| 237 | +also why the runner cannot be `xcrun simctl spawn` written into a manifest: a |
| 238 | +manifest cannot express the bundle. |
| 239 | + |
| 240 | +The device row keeps `runner` unset. An artefact cannot be run off an iOS |
| 241 | +device without a signature the developer owns, which is R12's separate subject. |
| 242 | + |
| 243 | +### The host surface, bounded and named |
| 244 | + |
| 245 | +This item adds exactly two host dependencies, both on macOS only: |
| 246 | + |
| 247 | +| what | why it is permitted | |
| 248 | +|---|---| |
| 249 | +| the iPhoneOS / iPhoneSimulator SDK, via `xcrun` | a proprietary runtime that exists only on its own OS -- category (b) of the recorded policy | |
| 250 | +| `simctl`, via the runner program | the same category | |
| 251 | + |
| 252 | +Everything else is ecosystem: compiler, C++ runtime, linker, packaging. The |
| 253 | +recorded rule is that a host surface must be minimal, named and written down, |
| 254 | +and two named items on one host is the whole of it. Neither is a fallthrough: |
| 255 | +each is reached deliberately, and its absence is an error that names it. |
| 256 | + |
| 257 | +## Task list and dependencies |
| 258 | + |
| 259 | +``` |
| 260 | +A1 android-system-image: debugfs -> xim:7zip xim-pkgindex |
| 261 | +A2 aarch64-linux-android -> verified + docs mcpp (independent) |
| 262 | +B1 withdraw the `ndk` alias mcpp |
| 263 | +C1 sdk_path(sdk) parameter mcpp (D depends) |
| 264 | +C2 .mcpp-toolchain.json reader + malformed refusal mcpp |
| 265 | +C3 android-ndk.lua writes the descriptor xim-pkgindex (after C2) |
| 266 | +D1 three iOS rows: pin llvm, located sysroot mcpp (after C1) |
| 267 | +D2 -m*-version-min per row mcpp (after D1) |
| 268 | +D3 xim:apple-simulator-tools with simctl-run xim-pkgindex |
| 269 | +D4 runner wired for the two sim rows mcpp (after D3) |
| 270 | +D5 docs: 20-toolchains SDK section + zh, examples/13 mcpp |
| 271 | +``` |
| 272 | + |
| 273 | +`A2`, `B1`, `C1`, `C2`, `D1`, `D2`, `D4`, `D5` are one mcpp PR with a version |
| 274 | +number, per the single-PR rule. `A1`, `C3`, `D3` are one xim-pkgindex PR. `C3` |
| 275 | +must land after `C2` ships, because a descriptor no engine reads is a file |
| 276 | +nothing checks. |
| 277 | + |
| 278 | +## Criteria |
| 279 | + |
| 280 | +Each claim below fails when its subject is removed, which is the only reason to |
| 281 | +write it down. |
| 282 | + |
| 283 | +| claim | criterion | |
| 284 | +|---|---| |
| 285 | +| A: the device row runs | qemu-user run in CI on a Linux runner, asserting `1-2-3` and exit 0; the extraction is the packaged 7zip, not a host tool | |
| 286 | +| B: one spelling | `toolchain = "ndk@…"` is refused AT PARSE with "unknown toolchain", not by the capability gate | |
| 287 | +| C: the engine stops knowing | delete `ndk_host_tag()`'s call site and the build still resolves, because the descriptor answered; and a malformed descriptor is refused naming the file | |
| 288 | +| C: no flag day | a payload with no descriptor resolves exactly as today -- asserted against the released android-ndk | |
| 289 | +| D: iOS builds | macOS runner, `xim:llvm` plus the located SDK, artefact is Mach-O arm64 with the iOS platform in `LC_BUILD_VERSION` | |
| 290 | +| D: the simulator runs | macOS runner, `mcpp run --target aarch64-ios-sim` prints `1-2-3` | |
| 291 | +| D: the host surface is bounded | on a macOS runner with `xcode-select` pointing nowhere, both iOS rows fail with a message naming the SDK -- and no other row changes | |
| 292 | + |
| 293 | +**One premise needs measuring before D is scheduled**: that GitHub's macOS |
| 294 | +runners ship both an iOS SDK and a bootable simulator. If they ship the SDK but |
| 295 | +no simulator, D1/D2 are still verifiable in CI and D4 is a local-only claim, |
| 296 | +which changes the tier the sim rows can reach and nothing else in this design. |
| 297 | + |
| 298 | +## Deliberately not done |
| 299 | + |
| 300 | +- **`xim:iphoneos-sdk` as a package.** The earlier record listed it at |
| 301 | + "whichever of three licence tiers applies". The SDK is not redistributable, |
| 302 | + so the tier is the locator, and a package that only locates is a package that |
| 303 | + ships nothing -- the row's `sysroot` column says it more directly. |
| 304 | +- **Xcode's clang as the toolchain.** It would work and it is the wrong default: |
| 305 | + it makes the compiler a host dependency where the ecosystem already has one, |
| 306 | + and it would make the iOS rows the only Apple rows that do not use |
| 307 | + `xim:llvm`. |
| 308 | +- **Fixing `xim:e2fsprogs`.** Recorded, not owned here. Nothing else in the |
| 309 | + index depends on its `debugfs`. |
| 310 | +- **A device runner for `aarch64-ios`.** R12's subject, and it needs a |
| 311 | + signature rather than a package. |
0 commit comments