Skip to content

Commit f7e4584

Browse files
committed
docs(design): SDK toolchains, the payload/engine seam, and iOS local verification
Four items, in dependency order: one measured and needing only to be written down, one deletion, one structural change, one new capability. A. `aarch64-linux-android` becomes `verified`. The earlier design's qemu-user route was correct and only its extraction tool was broken -- `debugfs` in `xim:e2fsprogs@1.47.3` is a broken build, and `xim:7zip` reads ext4 directly. Measured: 7zip extracts the bionic loader, qemu-aarch64-static runs the mcpp-built artifact, `1-2-3`, exit 0. B. The toolchain axis already behaves like every other: default from the row's pin, override through `[target.<triple>] toolchain`, version free, payload name fixed for a capability row. The change is to WITHDRAW the `ndk` alias, which `compat.cppm` accepts and the capability gate refuses. C. A payload describes itself through `.mcpp-toolchain.json`, written by the recipe that already knows the layout, so the engine stops carrying three NDK-specific facts. Absence is compatibility; a malformed descriptor is refused by name. D. iOS uses `xim:llvm` and a LOCATED SDK, which is the split `aarch64-macos` is already verified on. `macos.cppm` locates its SDK with `xcrun` today, so the change is a parameter rather than a concept, and the locator sits on the SYSROOT axis -- which is why `@system` stays withdrawn instead of generalised. The simulator is a `runner` from a package. The host surface added is two named items on one host, both category (b) of the recorded policy.
1 parent b718f49 commit f7e4584

2 files changed

Lines changed: 314 additions & 1 deletion

File tree

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
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.

.agents/docs/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
1818
---
1919
```
2020

21-
277 records.
21+
278 records.
2222

2323
## By subject
2424

@@ -46,6 +46,7 @@ Records that declare one. Everything else is listed by date below.
4646

4747
### targets
4848

49+
- [SDK toolchains, the payload/engine seam, and iOS local verification](2026-09-11-sdk-toolchains-and-ios-local-verification.md) — active
4950
- [Where a platform's knowledge belongs: iOS, Android and Web across the engine, the index and the plugins](2026-09-11-platform-targets-design-review.md) — active
5051

5152
### triage
@@ -57,6 +58,7 @@ Records that declare one. Everything else is listed by date below.
5758
### 2026-09
5859

5960
- [Six open issues: what each one actually is, and what would answer it](2026-09-11-six-open-issues-analysis.md) — active
61+
- [SDK toolchains, the payload/engine seam, and iOS local verification](2026-09-11-sdk-toolchains-and-ios-local-verification.md) — active
6062
- [Where a platform's knowledge belongs: iOS, Android and Web across the engine, the index and the plugins](2026-09-11-platform-targets-design-review.md) — active
6163
- [The category the plugin taxonomy does not name, and what a platform actually decomposes into](2026-09-11-distribution-plugins-and-platform-decomposition.md) — active
6264
- [Two answers and two silences: the scanner's second grammar, and the manifest keys nothing reads](2026-09-09-two-answers-and-two-silences.md) — active

0 commit comments

Comments
 (0)