Skip to content

SDK toolchains, the payload/engine seam, and openkal across iOS, Android and Web - #612

Merged
Sunrisepeak merged 14 commits into
mainfrom
docs/sdk-toolchains-and-ios-design
Sep 11, 2026
Merged

SDK toolchains, the payload/engine seam, and openkal across iOS, Android and Web#612
Sunrisepeak merged 14 commits into
mainfrom
docs/sdk-toolchains-and-ios-design

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Sep 11, 2026

Copy link
Copy Markdown
Member

This PR implements .agents/docs/2026-09-11-sdk-toolchains-and-ios-local-verification.md and carries version 2026.9.11.4. The record keeps the plan as reviewed, a criterion per claim with a measured column, and the findings the implementation produced.

What changes

A. aarch64-linux-android is verified. The execution route was correct and only its extraction tool was broken: xim:e2fsprogs's debugfs raises SIGFPE on every filesystem-opening command, while xim:7zip reads ext4 directly. qemu-aarch64-static -L <root> over the system image's own bionic runs the mcpp-built artefact and prints 1-2-3, exit 0. The recipe change is xim-pkgindex #813.

B. One spelling for the NDK. ndk parsed as an alias and was then refused by the capability gate, which compares against the row's pin. The alias is withdrawn; android-ndk, the index's name, is the only one.

C. A payload describes itself. <payload root>/.mcpp-toolchain.json (schema 1) carries frontend, platform_floor and std_module_defines, the three NDK facts the engine used to re-derive. Absence means the previous behaviour exactly; a descriptor that is present and malformed is refused naming the file. frontend is validated as a string (relative, /-separated, no . or .. component, no drive or scheme), because std::filesystem::path::is_absolute() returns false for /usr/bin/g++ on Windows. xim:android-ndk writes the descriptor (xim-pkgindex #813).

D. iOS: three rows, an ecosystem compiler and a located SDK.

row tier evidence, macos-15 runner
aarch64-ios preview Mach-O arm64, LC_BUILD_VERSION platform 2, minos 18.0; running needs a developer signature
aarch64-ios-sim verified Mach-O arm64, platform 7, minos 18.0; mcpp run prints 1-2-3 through simctl-run
x86_64-ios-sim preview Mach-O x86_64, platform 7, minos 18.0; an Apple-silicon runner cannot run it

The rows pin llvm@22.1.8. sdk_path(sdk) locates the iPhoneOS or iPhoneSimulator SDK through xcrun, and its absence is refused as apple-sdk-absent before any payload is resolved, because no package can supply a non-redistributable SDK. [build] ios_deployment_target travels in the effective triple (arm64-apple-ios18.0-simulator). The Apple cross path carries --target, -isysroot and --no-default-config on the compile line, the link line and the std module's own command, and takes libc++ from the SDK. runner = ["simctl-run"] names a program from the new xim:apple-simulator-tools (xim-pkgindex #813, #820).

E. openkal on the three platforms. Android shares openkal-linux (0.12.0). The iOS rows are expected to reuse openkal-macos through the located SDK; the cfg line is in examples/portable, and the compile leg belongs to openkal-macos's own CI. Web is a new implementation, mcpplibs/openkal-emscripten 0.1.0, written above Emscripten's C library: the conformance suite reports 86 held, 0 did not hold, 13 not observed, and the absent groups fail at link naming the symbol (mcpp-index #393). examples/portable gains the iOS and Web lines and documents the Web leg as the boundary the linker reports (openkal #29, #30).

Defects the implementation found

  • mcpp toolchain install emsdk and mcpp toolchain install android-ndk had never worked: the install path composed <root>/bin itself, and to_xim_package read the target rather than the payload the spec named.
  • The C compiler beside em++ was derived as em. The derivation is now a table: g++ to gcc, em++ to emcc, otherwise drop ++.
  • Three branches selected by the host were wrong for a second Apple platform: the macOS link line carried no --target, the MachO contract cell would have linked a macOS libc++.a into an iOS link, and -mmacosx-version-min was emitted beside an iOS triple, which clang refuses.
  • host_can_serve asked os == "macos", so the iOS rows were dropped from toolchain list on macOS and listed on Linux for the wrong reason.
  • A runner's program is looked for in the payload root after bin/, which is where xim:7zip installs; the rule is runner_lookup::payload_search_dirs, and its unit test fails when the rule is reverted.

Verification

  • ci-macos-ios / ios-engine fails when any artefact's architecture, LC_BUILD_VERSION platform or minos differs from the row's, and when mcpp run --target aarch64-ios-sim does not exit 0 with 1-2-3 as a line of its output. The premise probe that preceded it runs on manual dispatch only.
  • tests/e2e/641 asserts the SDK refusal on every non-Apple host and a host-conditional toolchain list. tests/unit/test_toolchain_descriptor.cpp and tests/unit/test_platform_apple_sdk.cpp are new.
  • check_target_tiers.py compares the tier table against five documents, the unit test's tier claims included. The target matrix gains apple-sdk-absent, and iOS rows are expected only on macos-arm64.
  • After release, the published artefacts are verified in an xlings subos --sandbox environment configured with the CN mirror.

Recorded and not closed

  • No per-target tool axis: [target.<row>.xlings] does not accept deps, so examples/13 asks the reader to install apple-simulator-tools.
  • No whole-graph flag channel: -pthread for openkal.task on Emscripten would have to reach every unit in the link.
  • openkal-musl has never been built for a Darwin target: uint64_t is unsigned long and kal_u64 is unsigned long long there.
  • No device runner for aarch64-ios: it needs a signature the developer owns.

…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.
…-review

The plan gains item E -- openkal's cross-build closing on iOS, Android and Web
-- and the self-review the goal asks for. Retitled, because the document is no
longer only about SDK toolchains.

E is three different answers, and the spec is what makes the third legitimate.

  E1 Android   DONE and merged. `openkal-linux` is written on the kernel's
               syscall interface and borrows nothing from a C library; Android's
               kernel IS Linux and sys.h branches on architecture, not OS. Green
               against the RELEASED 2026.9.11.3: both ABIs build, the objects
               name no C library symbol -- checked against a DIFFERENT libc,
               which is the half worth having -- and a program over openkal
               alone printed `openkal: 1-2-3` on an emulator.
  E2 iOS       a consequence of D rather than new work: once the SDK resolves,
               `openkal-macos` compiles for the iOS rows unchanged. One
               asymmetry is recorded rather than papered over: `cfg(os =
               "linux")` covers Android because Android is an `env` on `linux`,
               while `cfg(os = "macos")` does NOT cover iOS because iOS is its
               own `os`. A `cfg(apple)` dimension was considered and rejected --
               it would introduce a dimension for one consumer's convenience.
  E3 Web       new software, and the smallest of the three: forwarding to a
               POSIX-shaped libc is thinner than issuing syscalls. The partial
               surface is CONFORMANT, by clause 6.2's three times -- provided
               and forwarding, provided with the capability word reporting what
               is exercisable, or NOT PROVIDED so the report is an undefined
               symbol at link. `process`, `exec` and `space` take the third:
               providing `kal_process_spawn` on wasm to return an error is the
               shape the spec names as a defect, because the caller cannot tell.

E4 records that `examples/portable` is stale on EVERY target -- it fails to
compile for the host with `unknown type name 'kal_spawn'`, identically to how
it fails for Android -- so the example that exists to demonstrate portability
builds nowhere, and that is a criterion rather than an assumption.

The task list is now per-repo with its dependency edges, three of which are
not reorderable, and it names the one split that is by construction rather than
by choice: a new package is a new repository in this ecosystem.

The self-review covers the nine angles. Two are worth reading: the one place
simplicity was deliberately refused (E2's two cfg lines), and where the risk
concentrates (E3, which is new code in a new package and therefore cannot
regress anything that exists).
@Sunrisepeak Sunrisepeak changed the title docs(design): SDK toolchains, the payload/engine seam, and iOS local verification SDK toolchains, the payload/engine seam, and openkal across iOS, Android and Web Sep 11, 2026
…nd the SDK axis iOS needs

B1. WITHDRAW THE `ndk` ALIAS. It parsed and the capability gate then refused
it, because that gate compares the declared spelling against the row's pin --
`android-ndk@30.0.16248370` -- and `ndk@30.0.16248370` does not contain it. So
a name was good enough for the parser and not for the build, which reads as a
defect rather than a naming choice. Withdrawn rather than completed: teaching
the gate to normalise payload names would make two spellings work through a
second mechanism, where one name keeps the comparison correct by construction.
The name kept is the one the index uses.

C1. `sdk_path(sdk)`. macOS located exactly one SDK because macOS was the only
Apple target; the iOS rows need iPhoneOS and iPhoneSimulator, and the
difference between the three is directory names. So it is a parameter over a
table, not a second locator. Two pure pieces carry what can be wrong in it and
are checked on any host: `sdk_layout` (the per-SDK directory names, and which
a Command-Line-Tools install ships) and `sdkroot_answers` -- because `SDKROOT`
names ONE SDK and there are now three questions. An override that names an SDK
answers for that SDK alone; one that names none answers the default question
only, which is what keeps today's three callers unchanged.

C2. `.mcpp-toolchain.json`: A PAYLOAD DESCRIBES ITSELF. Three payload-specific
facts lived in the engine -- the NDK's `toolchains/llvm/prebuilt/<host>/bin`
layout, its API floor in `meta/platforms.json`, and the
`-D__BIONIC_CTYPE_INLINE=` its libc++ module surface needs -- and each is a
fact the installing recipe already computes for its own probes. The recipe now
writes one file beside the payload and the engine reads it: `frontend`,
`platform_floor`, `std_module_defines`.

Three properties. Absence is compatibility: no descriptor means today's
behaviour exactly, so a released payload keeps working and there is no flag
day. Present and MALFORMED is refused naming the file -- otherwise a typo
reads as "an older payload" and the engine uses a hardcoded path for a layout
that has moved. And it is not a general flag channel: three keys, each
answering a question the engine already asks, with `frontend` refused if it
leaves the payload root and a define refused if it could begin an option.

`payload_frontend` therefore returns `expected`, so no caller can express a
malformed descriptor as "not found"; the eight call sites propagate it, and
`toolchain list` and `doctor` report it rather than skipping the payload.

D (partial). `ios_deployment_target` joins `macos_deployment_target` as the
second Apple key: same single `minPlatformVersion` slot, different version
space, since "14.0" is a macOS version and means nothing to an iOS SDK. The
effective triple now carries it -- `arm64-apple-ios18.0` and
`arm64-apple-ios18.0-simulator`, Apple's own spellings -- which is the one
place that says it. `is_ios()` and `is_ios_simulator()` name the rows.

A third copy of a false claim, corrected: `minApiLevel`'s comment still said
an unset level "means the NDK's own default, which `clang -target
aarch64-linux-android` normalises to". bionic refuses an unversioned triple.
The two earlier copies were fixed in `min_platform_version` and `llvm_triple`;
this one was written on another day and outlived both.

THE FIFTH COPY OF THE TIER TABLE WAS IN A TEST. `aarch64-linux-android` became
`verified`, the row and all four documents moved, `check_target_tiers.py`
reported agreement -- and `test_toolchain_triple.cpp` went on asserting
`preview`, because a literal in a test is in neither set. The test's tier
claims are now one-line pairs and the checker reads it as a fifth document;
removing the fix makes the checker fail, which is the only reason to add it.

And a measurement, not yet a claim: ci-macos-ios.yml asks the runner what it
actually provides -- both located SDKs, whether a simulator boots, whether
`simctl spawn` takes a bare Mach-O, whether the SDK's libc++ or the payload's
static archive links, and whether `import std` precompiles against the located
SDK. The design record schedules the iOS rows on those answers; none of them
can be reasoned out from a Linux desk.
D1/D2. THE THREE iOS ROWS PIN `llvm@22.1.8`. The sentence that shrank this
item is that the compiler is ours and only the SDK is Apple's: any
sufficiently new clang emits arm64 Mach-O for an iOS deployment target, and
`aarch64-macos` is verified on exactly that split. Xcode's clang would also
work and is the wrong default, because it would make the compiler a host
dependency where the ecosystem already has one.

A CONVENTION PIN, NOT A CAPABILITY ONE. It answers "what does `--target
aarch64-ios` resolve when the project says nothing" and stays overridable --
consistent with `aarch64-macos`, where the same constraint (Darwin needs
clang) holds and which is not a capability row either. The rows carry no
`sysroot`, because that column names a PACKAGE and an Apple SDK is located.

The deployment target travels in the EFFECTIVE TRIPLE and nowhere else:
`arm64-apple-ios18.0` and `arm64-apple-ios18.0-simulator`, Apple's own
spellings, which fully determine platform and minimum. `-miphoneos-version-min`
is deliberately not emitted -- it would be a second place answering a question
the triple already answers. macOS keeps its flag, and the asymmetry now has a
criterion of its own in the triple tests rather than being folded into this.

FOUR SITES HAD TO LEARN THAT A macOS HOST IS NOT A macOS TARGET.

  - hostflags emitted `-mmacosx-version-min` whenever the HOST was macOS. With
    an iOS triple clang refuses the combination outright, so the flag would
    stop the build rather than be ignored. Gated on the located SDK being
    empty, which is the same value that says "this is a cross".
  - the compile side gets `-isysroot <located SDK>`, which it never needed
    before: a native build reads the payload's `clang++.cfg`, and post-install
    filled that with the located macOS SDK. An Apple cross suppresses the cfg
    precisely because it names the wrong platform.
  - the LINK line in that branch carried no `--target` at all. The note two
    hundred lines below it records that only the third branch ever consumed
    `link_toolchain_flags`; an iOS row is the first target this branch serves
    that the driver would get wrong, and a link told nothing produces a macOS
    binary from objects compiled as iOS.
  - the Mach-O distribution cell chose the self-contained contract, which
    links the payload's `libc++.a` -- a Mach-O archive built for macOS, which
    ld64 refuses in an iOS link. iOS takes libc++ from the SDK, which every
    iOS release ships, and the floor stays real because the triple carries it.

`ios_deployment_target` in `[build]`, beside `macos_deployment_target`: one
fingerprint slot, two version spaces, because "14.0" is a macOS version and
means nothing to an iOS SDK.

THE SDK IS LOCATED ONCE AND ITS ABSENCE IS A REFUSAL THAT NAMES IT. Three
later sites need the answer and `xcrun` shells out, so probing three times
could return three answers. The refusal names the SDK, the `xcrun` command
that must answer, that Command Line Tools alone ship the macOS SDK only, and
that the compiler is not the problem.

AND THE PROBE TAUGHT THE MEASUREMENT ITS OWN LESSON. The first
ci-macos-ios.yml run answered the premise -- Xcode 16.4, iPhoneOS 18.5,
iPhoneSimulator 18.5, five iOS simulator runtimes, an iPhone 16 Pro that boots
-- and then failed with

  ld64.lld: error: .../MacOSX.sdk/usr/lib/libc++.tbd(...) is incompatible with
    arm64 (iOS Simulator18.0.0)

with the correct `-isysroot` on the command line: the payload's cfg won. That
is the fact behind three of the four sites above. The four steps after the
failing one were SKIPPED, so one unmet assumption cost the rest of the
measurement; every step is now `continue-on-error`, because a probe that stops
at the first `no` is a build and not a measurement.

A second job builds mcpp from this branch and asks the engine the same
questions: the device row's artefact and its `LC_BUILD_VERSION`, the simulator
row plus `simctl spawn` on the bare Mach-O -- which decides whether
`simctl-run` is a boot-and-spawn wrapper or has to synthesise a bundle -- and
that the refusal names the SDK while the host row still builds in the same
environment. The rows stay `planned` until those answers arrive; the fixture
uses the escape hatch the refusal itself advertises.
Sunrisepeak added a commit to openxlings/xim-pkgindex that referenced this pull request Sep 11, 2026
A1. THE EXTRACTION TOOL WAS THE OBVIOUS ONE AND THE WRONG ONE.
`android-system-image` extracts four bionic files out of `system.img` so that
the arm64-v8a key can serve `aarch64-linux-android` through qemu-user. It
declared `xim:e2fsprogs` for `debugfs`, and that payload's `debugfs` is a
broken static build: SIGFPE on every filesystem-opening command, while
dumpe2fs, e2fsck and tune2fs from the same build work. So the package
installed and could not serve the one route the key exists for.

`xim:7zip` reads ext4. Measured 2026-09-11 against this exact image:

  7zz l system.img            Type = Ext, Label = system
  7zz x system.img -o<dir> -y \
    bin/linker64 lib64/libc.so lib64/libdl.so lib64/libm.so
                              Everything is Ok / Files: 4

One command instead of four, and the in-image directories are preserved --
which IS the `<root>/system/{bin,lib64}` tree qemu-user needs, so the
extraction writes it rather than composing four destinations. The e2fsprogs
defect stays recorded and unowned here: nothing else in the index depends on
that `debugfs`, so the repin belongs to that package.

The probe still has to OPEN the filesystem, which is the lesson the old route
taught: `debugfs -V` was the one command that binary answered without touching
an image, and it answered it and then died on everything else. So the new
probe lists the image and requires `Type = Ext` in the answer -- a statement
that can only come from having opened it. And the extraction asserts every
FILE, because 7-Zip exits 0 having extracted nothing when a named entry is
absent.

Its test survived the tool change with its subject renamed rather than being
deleted with it: neither assertion was ever about debugfs. They are about
resolving a declared dependency's program through `dep_install_dir` rather
than PATH, and about a probe that cannot pass on a binary that cannot work.

AND THE TEST REPEATED THE MISTAKE ITS OWN COMMENT WARNED ABOUT. A flat search
over the source for a bare `7z ` matched the raise message that names the
programs it looked for, and a flat `-V` search matched the message explaining
why a version probe is insufficient. Stripping comments is not enough when the
text also lives in a string literal, so both checks are now scoped to the
lines that RUN something.

C3. `.mcpp-toolchain.json`: THE PAYLOAD DESCRIBES ITSELF. Three facts about
this NDK lived inside mcpp's engine -- the
`toolchains/llvm/prebuilt/<host>/bin` layout, the API floor's location in
`meta/platforms.json`, and the `-D__BIONIC_CTYPE_INLINE=` its libc++ module
surface needs. This recipe already computes all three for its own probes, so
the engine was re-deriving facts their owner held, and that is why adding a
second such SDK meant editing the engine instead of publishing a package.

The recipe now writes the descriptor after the std-module self-test, not
before: the file asserts that this payload's `import std` works with that
define, and writing the claim before measuring it would publish a contract on
an unverified payload. The floor is READ from `meta/platforms.json` rather
than written down, because a constant here becomes a version in a diagnostic
and then in somebody's command line. The path is forward-slashed on every
host, because a backslash in JSON is an escape character -- a mistake this
index has already paid for once, in pkgs/e/emsdk.lua.

Two assertions, because the file is only worth writing if it is true: the
recipe refuses when the frontend it named does not exist or disagrees with the
compiler this install verified, and the tests read it back and check each key
against the installed tree.

Verified end to end against the RELEASED payload, with mcpp built from
mcpp-community/mcpp#612:

  frontend            a descriptor naming `oddly/named/clang++` resolved
                      there -- a path no engine derivation produces
  platform_floor      "26" gave `x86_64-unknown-linux-android26` while
                      meta/platforms.json says 21
  std_module_defines  an added define appeared in the std module's own
                      command and in its cache identity
  absent              with the file removed, the build resolves exactly as
                      before -- no flag day
  malformed           refused naming the file and the key, with the fix in
                      the message
Sunrisepeak added a commit to openxlings/xim-pkgindex that referenced this pull request Sep 11, 2026
…ions in the install hooks (recovers #812) (#813)

* feat(platforms): every payload on every host upstream publishes for, and one upload that would not land

DECLARING ONLY `linux` WAS AN INCOMPLETE ADDITION, not a conclusion. Google
and the Emscripten project publish these for every host this index serves, and
a toolchain that exists for a host should be installable there. Measured with
HEAD against the pinned revisions, then downloaded and hashed:

  emsdk            mac x86_64 252M   mac arm64 263M   win x86_64 624M
  android-ndk      darwin 930M (universal)            windows 695M
  android-emulator darwin_x64 445M  darwin_aarch64 377M  windows_x64 422M

Two things the measurement caught that reasoning would not have:

  * THE WINDOWS EMSCRIPTEN ARCHIVE IS A .zip, not a .tar.xz. A shared `${ext}`
    template would have produced `win/<hash>/wasm-binaries.tar.xz`, which is a
    404 -- so the extension is spelled per platform.
  * emsdk's install probe named `em++` and `bin/clang` unsuffixed, which is
    every archive it served while it declared only linux, and would refuse a
    correct Windows payload where the driver is `em++.bat` and the compiler is
    `bin/clang.exe`. Now host-suffixed.

ONE UPLOAD WOULD NOT LAND, AND THE CN ENTRY IS WITHDRAWN RATHER THAN LEFT
POINTING AT IT. Three attempts at emsdk's 624 MB Windows archive each answered
`obs_callback ... code:400, err:EOF` and each left a 128-byte error body served
at the object's URL. A CN entry naming a 404 is worse than none: every
`--mirror CN` install would fail with a hash mismatch on an error page.

The error is uninformative, which is the part worth recording: the two emsdk
LINUX uploads reported the same `obs_callback 400` and both objects are
correct, and `android-ndk-r30-darwin.zip` is 930 MB and uploaded cleanly. So
it is neither a size limit nor a reliable failure signal, and only re-fetching
the object and hashing it separates the two -- which is the check that caught
this, and the reason every upload in this batch has one.

EXECUTION EVIDENCE IS LINUX ONLY and each recipe says so. The seven verified
objects were downloaded back and hashed against the recipe; the macOS and
Windows install paths are declared and not run, and the index's own
macos-install-test and windows-test are their measurement.

Four tests are reversed on purpose -- they pinned `linux`-only and
`archs = {"x86_64"}`, which were true of the recipes and not of upstream.
`archs` is a statement about the package and the platform tables carry the
truth per host, which is the shape 7zip.lua, bun.lua and cuda-nvcc.lua use.

* fix(platforms): three Linux assumptions the install hooks carried, found by the jobs that exist to find them

The platform tables were completed and the macOS and Windows install jobs
failed on the first run. That is the outcome those jobs are for, and the recipes
said in as many words that only the Linux leg had been executed.

THREE ASSUMPTIONS IN android-emulator's HOOK, each invisible while the recipe
declared only `xpm.linux`:

  * the executable had no host suffix, so a correct Windows payload carrying
    `emulator.exe` was refused;
  * the message named `linux_x64` on every host, accusing the payload while
    describing the probe;
  * the qemu subdirectory was hardcoded `qemu/linux-x86_64`, so every other
    host was told its own architecture backend was missing.

AND `xim:python` HAD NO macosx SECTION, which the dep check refused outright:

  dep `xim:python@>=3.12` is declared under xpm.macosx, but `python` has
  sections for [linux, windows] only

`xim:emsdk` declares that dep on all three hosts because `em++` is a shell
wrapper that execs `python3` on every one of them, so the omission was a hole
one platform wide rather than a scope decision. python-build-standalone
publishes both Apple arches; added, downloaded and hashed.

TWO WRONG ANSWERS BEFORE THE RIGHT ONE, and the second is worth recording
because it would have shipped.

Computing the qemu directory from `os.arch()` failed locally with nothing but a
test to warn me: `os.arch()` is UNBOUND in the install-hook runtime, which
pkgs/a/appimagetool.lua records as measured and which this package's own test
pinned. Then `os.files(...)` answered `attempt to call a nil value`. `os.dirs`
IS bound, so the directory enumeration stands; only the file-side glob is not.

The final shape is better than either: ENUMERATE the payload rather than
compute the expected name. Each archive contains exactly one host directory, so
what is present IS the answer and asserting it needs no knowledge of which host
this is -- a computed name can be right about the host and wrong about the
payload, and this cannot. The guest backends are then checked by explicit name,
because upstream's whole set is four and `os.isfile` is available.

Verified by installing locally: 9 packages, 0 failed. `os.arch()` no longer
appears in any live code path in this index.

* fix(platforms): two host assumptions the new hosts found, and the first Windows guess was wrong

Both defects were reached only by the hosts this branch adds, and both were
reported by the index's own macos-install-test and windows-test.

android-ndk: install() derives the extracted directory from the downloaded
file's name, because the zip's internal root is the release
("android-ndk-r30") while the file carries a host token. The pattern was
written as `%-linux%.zip$`, measured with `unzip -l` on the Linux archive at a
time when the recipe declared only `xpm.linux`. It was not wrong then; adding
`xpm.macosx` and `xpm.windows` made it wrong, and both new hosts failed at
exactly that line. The host token is now accepted and CHECKED against the
three upstream spellings rather than matched as `%a+`, so an archive named
something else still raises rather than extracting into an unverified
directory.

emsdk: the Windows entry points are `.exe`, not `.bat`. The first version of
this guessed `.bat` from how emscripten's own installer wraps these scripts on
Windows. Measured by reading the central directory of `wasm-binaries.zip`
(12882 entries): all nine entry points this recipe registers ship as
`<name>.exe` beside a `<name>.py`, and no `.bat` exists for any of them.

THREE SITES NEEDED THE SUFFIX, NOT ONE, and the probe passing is what hides
the other two -- the probe checks a file exists, the self-check EXECUTES the
driver, and config() decides which shims get registered. Suffixing only the
probe would install and then fail in the self-check, or register nothing and
report success. The shim keeps the bare upstream name on every host (the
qemu-riscv.lua idiom), so a user types `em++` everywhere; only paths to real
files carry the suffix.

The archive's top level is `install` on all three hosts, verified the same
way, so that assumption needed no change.

Both tests take their DENOMINATOR from the descriptor rather than from a list
written here, which a Linux-shaped assumption would have written wrong the
same way: the NDK test feeds every basename the url tables can download to the
recipe's own pattern, read out of the source rather than restated. Each was
verified in both directions -- reverting the fix turns the test red, and
narrowing only the pattern still fails on `android-ndk-r30-darwin.zip`.

* fix(android-ndk): the host tag is per host, and Windows cannot serve a module-first build

Two more findings from the per-host install jobs, and the second one decides a
platform rather than a path.

THE HOST TAG WAS A CONSTANT SPELLING ONE HOST, and it was the host it was
written on. `toolchains/llvm/prebuilt/<host>` names the HOST, never the target
-- a Linux x86_64 machine building for aarch64 still reads `linux-x86_64` --
and the constant said so for every host. The macOS job reported it exactly:

    no clang++ at .../prebuilt/linux-x86_64/bin/clang++
    -- payload does not look like an NDK for linux-x86_64

where the string names the question that was asked. Measured by reading each
r30 archive's central directory: `linux-x86_64`, `darwin-x86_64`,
`windows-x86_64`, and the darwin archive is a universal build so Apple silicon
reads `darwin-x86_64` too. The Windows payload also spells the compiler
`bin/clang++.exe`. Both are now derived.

AND THE WINDOWS TABLE IS WITHDRAWN, WHICH IS AN UPSTREAM FACT AND NOT AN
OMISSION. Google publishes `android-ndk-r30-windows.zip`, it downloads, and its
layout is the one this recipe expects. What it does not contain is the libc++
MODULE SURFACE, which is the whole reason this package is in this index.
Measured the same way:

    archive   entries   share/libc++/v1/std.cppm   std/*.inc
    linux      ~10000   present                    110
    darwin      10024   present                    110
    windows      9108   ABSENT                       0

mcpp is module-first, so that payload cannot serve: the install would succeed
and the self-test would then fail on a file that is not there. An entry that
can never serve is worse than none -- the same rule that kept the emsdk Windows
CN asset out of pkgs/e/emsdk.lua after its upload would not land.

Declaring it and refusing inside install() was considered and rejected: it
makes a Windows user download 695 MB to be told no. What they get instead is
xim's own "no payload for this platform", before anything is fetched. The hook
code stays host-general, so if a future NDK ships the surface on Windows this
becomes a url table and nothing else.

The tests take their denominators from the descriptor rather than from a
constant: the platform assertion is an EXACT set (so a future addition needs
its own measurement and a future removal cannot pass either), and the
archive-name count is derived from the number of platform tables -- a hardcoded
3 would have had to be edited by whoever withdrew `windows`, and the edit would
have looked like loosening the test. Verified in both directions: pinning the
host tag again turns the new test red.

* fix(stat): a GNU-only stat flag in two macosx-declaring recipes, and a check with a denominator

The macOS install job's third finding, and the flag turned out to be in a
second, unrelated package with a worse consequence.

macOS ships BSD stat. `stat -c` is a GNU spelling and an error there, and the
failure mode is not a visible error but a silently empty capture.

android-ndk: the module-surface self-test measured its BMI with `stat -c%s`, so
on macOS the precompile SUCCEEDED, the measurement returned nothing, and the
self-test raised "did not produce a usable BMI (got 0 bytes)" with an empty
compiler output underneath it. The criterion failed, not the thing it was
measuring, and the diagnostic accused the toolchain. The size is now read with
`io.open(p, "rb"):seek("end")` -- plain Lua, no host branch to get wrong, no
subprocess, and it does not read the 30 MB it is measuring. Deliberately not
`os.filesize`: this hook runtime has already been measured to leave `os.arch()`
and `os.files()` unbound, and there is no reason to assume a third.

gitcode-hosts: `stat -c "%a"` captures the hosts file's mode, the file is then
opened to 666 to be written, and the mode is restored afterwards -- so an empty
capture makes the restoring command `sudo chmod  /etc/hosts`, which fails, and
the file is left WORLD-WRITABLE. The flag is now host-correct (`-f "%Lp"` on
macOS) and an unreadable mode RAISES before the file is opened up, because
without that the failure is silent and the wrong outcome is the persistent one.

`.github/scripts/check-no-gnu-only-stat.sh` is what makes this a check rather
than two fixes: it examines every recipe that declares a `macosx` table, so the
next one is caught before a macOS runner has to find it. It reuses
check-no-direct-ld-libpath.sh's comment stripper verbatim, so the two cannot
disagree about what counts as code -- the prose above would otherwise fail the
check for describing the flag. It also refuses when fewer than five recipes are
examined: a check whose denominator can silently become zero is not a check.
Measured: 88 recipes in scope, and reintroducing the flag turns it red.

* fix(system-image): debugfs resolved through its dependency, probed functionally, and a broken payload named

`xim:android-system-image@24-default-arm64-v8a` could not be installed at all,
and the two reasons were one line apart and both invisible by construction.

RESOLUTION. The hook invoked `debugfs` by bare name. `xim:e2fsprogs` registers
a shim and that shim is not on PATH inside an install hook, so with the
dependency correctly installed the probe still failed:

    'debugfs' is not runnable, but xim:e2fsprogs is a declared dependency of
    this package -- the dependency did not install, or its shim is not on PATH
    for this hook.

The message named both possibilities and the second was true. contributing.md
R6 already requires resolution through `pkginfo.dep_install_dir`, this file's
own header cites it, and `pkgs/e/emsdk.lua`'s `__find_node` is the existing
example. Only the arm64-v8a key reaches this branch, which is why it survived:
the x86_64 key needs no user-mode translation and installs fine.

PROBE. The check was `debugfs -V`, and a version string is the one answer
debugfs gives without touching an image. `xim:e2fsprogs@1.47.3`'s statically
linked debugfs answers it and then dies on everything else. Measured against a
control ext4 image created by that same payload's `mke2fs`, so the image is not
the variable:

    command                       1.47.3 (payload)   1.47.0 (host)
    debugfs -V                    ok                 ok
    debugfs -R "features"         SIGFPE (136)       ok
    debugfs -R "ls /"             SIGFPE (136)       ok
    debugfs -R "dump <p> <out>"   SIGFPE (136)       ok
    dumpe2fs -h / e2fsck / tune2fs   ok              ok

Exactly one program of the set is affected, it fails on every command that
opens a filesystem, and it fails with an empty environment too. `debugfs` is
also the only program there that links libss and readline, which is where a
static build of it is known to be fragile. A probe that cannot fail on a broken
binary is not a probe, so the readiness check now issues `-R "features"`.

WHY NOBODY SAW IT. Run from a directory with no xlings project config, the
`debugfs` shim resolves to /usr/sbin/debugfs (1.47.0) and works -- so a
measurement taken that way records the HOST's behaviour under an ecosystem
name, which is exactly the fallthrough this index exists to prevent. This
file's header documented the qemu-user invocation as reproducible on that
basis; it now says the extraction feeding it is blocked, and by what. The
`dep_install_dir` resolution is what turned a silent host success into a
visible failure.

The defect is recorded in pkgs/e/e2fsprogs.lua where a reader of that package
will find it. `debugfs` stays in its `programs` list on purpose: dropping it
would make the bare name resolve to the host's copy, and a shim that fails
loudly is the lesser problem. Closing it needs a repin to a build whose debugfs
works, not a change in either consumer.

The test asserts both properties from the source, since an install needs a
2.6 GB image. Its `-V` assertion is scoped to lines that actually RUN
something: a flat substring search also matched the raise message explaining
why -V is insufficient, because stripping comments is not enough when the text
also lives in a string literal. Verified in both directions.

* fix(emsdk): node is not under bin/ on every host, and the diagnostic pointed at the one thing that was right

The Windows install got past the archive and past the compiler probe, and
stopped where the emscripten config is written:

    emsdk: xim:node payload not found (this package's deps declare xim:node);
    refusing to write a NODE_JS-less emscripten config that cannot link
    anything

`xim:node` was correctly declared AND `xim:node@26.7.0` had already installed
on that runner. `__find_node` looked for `bin/node`, which is every archive it
had ever seen. pkgs/n/node.lua's own `config()` states the rule it was missing:

    local bindir = pkginfo.install_dir()
    if os.host() ~= "windows" then
        bindir = path.join(pkginfo.install_dir(), "bin")
    end

Upstream's Windows archive puts `node.exe` at the root. All four spellings are
now tried rather than branching on the host, so an archive that adopts the
other layout keeps working.

I FIRST DIAGNOSED THIS WRONG, and the A/B is what caught it. Reading
"deps declare xim:node" I concluded the Windows table had been added without
its `deps` line and added one. Measured afterwards: all three platform tables
declare `deps` at the same brace depth, so Windows was already correct and my
addition was a redundant second declaration. The test written for that premise
passed with the Windows deps deliberately deleted -- twice, because the first
version's table split left the last body running to the end of the file and
swallowed `dep_install_dir("xim:node")` from this very function. Removing the
fix and re-running is the only reason either was found.

So the test asserts the LOOKUP rather than the declaration: the deps were never
the problem, and a test that checks them cannot fail on this defect.

* fix(emsdk): the self-check names its interpreter instead of letting a wrapper search for one

`em++` is a wrapper whose whole job is to find a Python interpreter: it execs
`$EMSDK_PYTHON`, or failing that whatever `python3` is first on PATH. This
package declares `xim:python` for exactly that reason, and the header argued
that the declaration makes the PATH lookup an xvm shim.

THAT IS TRUE FOR A CONSUMER AND FALSE INSIDE THIS INSTALL HOOK. Shims are not
on PATH there -- the same property that made pkgs/a/android-system-image.lua's
`debugfs` lookup fail with its dependency correctly installed -- so install()'s
own self-check searched the MACHINE. On the Linux and macOS runners a system
python3 exists, so it passed silently: a host fallthrough wearing an ecosystem
name, in the one package whose header says which interpreter it finds is this
index's problem.

Windows is where it stopped being silent. That archive bundles no python at all
(measured: zero `python` entries in its central directory), and the launcher
reported

    The filename, directory name, or volume label syntax is incorrect.
    emsdk: could not precompile the shipped libc++ module surface (std.cppm)

THE FIX REMOVES THE SEARCH RATHER THAN STEERING IT. The self-check resolves
`xim:python` through `pkginfo.dep_install_dir` and invokes that interpreter on
`emscripten/em++.py` directly. One command shape on every host: no `.exe`, no
environment variable, and no dependence on which `os` names this hook runtime
binds -- `os.setenv` is not among the ones this index has verified, and
`os.files`, `os.exists` and `os.iorunv` are documented as absent in
pkgs/l/libinput-quirks.lua. `em++.py` is present in all three archives,
measured alongside `em++` / `em++.exe`.

`EMSDK_PYTHON` was written first and withdrawn: it would have worked only if
`os.setenv` is bound, which I could not establish, and a failure there is a
hard `attempt to call a nil value` on the two legs that currently pass.

Verified locally before pushing, which is what the CI round trip would
otherwise have cost: the two commands produce a 34 MB BMI and then
`app.js` + `app.wasm`, node runs the result and prints `1-2-3`, all using the
declared `xim:python` and `xim:node` payloads rather than anything on the host
-- and a full `xlings install local:emsdk` runs the real hook to completion
(7 packages), which is what exercises `dep_install_dir` in the hook runtime.

The suffix test loses one of its three sites on purpose: the self-check no
longer builds a driver path at all, which is a stronger position than a
correctly suffixed one.

* fix(emsdk): the interpreter search reports what it examined, and covers the installer's own layouts

`xim:python@3.12.6` installed on the Windows runner and `__find_python` still
answered nil, so the candidate list is wrong. The refusal named neither the
directory it looked in nor what it tried, which makes the next attempt another
guess -- and there is no precedent to copy: `pkgs/m/meson.lua` is the only
other consumer of this payload and it joins `bin` unconditionally, while
`vcstool.lua` and `rosdep.lua` spell `Scripts\python.exe` inside a venv THEY
CREATE, which is a different object.

So two changes, and the second is the one that matters.

The list now also covers the CPython Windows installer's own arrangements --
`Scripts/python.exe`, `tools/python.exe`, `python/python.exe`, `bin/python.exe`
-- because python.lua drives that installer with
`TargetDir=<install_dir> InstallAllUsers=1 PrependPath=1` and the resulting
layout is not documented here.

And the refusal now reports the payload directory, whether it is a directory at
all, every candidate tried, and the SUBDIRECTORIES present. `os.dirs` is bound
inside an install hook and `os.files` is not (pkgs/l/libinput-quirks.lua), and
subdirectories are enough to separate the two possibilities that matter: the
payload is not where `dep_install_dir` says, or it is there and the interpreter
has a name this list does not have. `path.filename` and `os.dirs` are both
established hook idioms here (libnvptxcompiler.lua, llvm-dev.lua), which
matters because an unbound call in an ERROR path turns a clear diagnostic into
`attempt to call a nil value`.

The POSIX candidates keep their order, so the Linux and macOS legs are
unchanged -- verified locally end to end before the previous push and not
re-run here, since nothing ahead of `bin/python3` moved.

* fix(emsdk): the Windows invocation uses this index's own idiom, not one shell string

With the interpreter finally resolving, the self-check ran and both of its
commands failed on Windows with a sentence that names no path:

    The filename, directory name, or volume label syntax is incorrect.
    The filename, directory name, or volume label syntax is incorrect.
    emsdk: could not precompile the shipped libc++ module surface (std.cppm)

printed once per invocation, with the compiler's own output empty -- so nothing
in the message pointed at the cause.

THE REASONS WERE ALREADY WRITTEN DOWN HERE, in pkgs/v/vcstool.lua, for the same
construction: `path.join` returns MIXED-SEPARATOR strings that cmd.exe
mis-parses as switches, CreateProcess will not auto-append `.exe` to an
absolute path, and cmd.exe reads `<` inside a quoted argument as a redirect.
`os.iorunv` would sidestep all three and is one of the names this hook runtime
leaves unbound.

So both invocations now go through one helper that renders per host in the
shape vcstool.lua and 7zip.lua already use for the same reasons: backslashes
throughout, PowerShell's call operator, single-quoted arguments. The POSIX
branch keeps its plain form.

Verified by rendering both branches with `is_host` and `os.iorun` stubbed --
Windows emits

    powershell -NoProfile -ExecutionPolicy Bypass -Command "& 'C:\...\python.exe'
      'C:\...\em++.py' '-std=c++23' '--precompile' 'C:\...\std.cppm' '-o' '...'"

with ZERO forward slashes remaining, and POSIX emits the unchanged
`"py" "script" "-std=c++23" ...`. That POSIX shape is the one run by hand
earlier in this work: it produced a 34 MB BMI, then `app.js` + `app.wasm`, and
node printed `1-2-3` -- all through the declared `xim:python` and `xim:node`
rather than anything on the host.

The test pins the idiom, because an edit that "simplifies" this back to one
`os.iorun` string would pass every other test in this file and fail only on a
Windows runner. Verified in both directions: collapsing the helper to a single
shell string turns it red.

* fix(emsdk): the self-check's execution half does not run on Windows, and says so

Three invocation shapes were measured on a Windows runner and all three
produced the same error, once, with no compiler output and no Python traceback:

    "<py>" "<script>" -std=c++23 --precompile "<in>" -o "<out>"
    powershell -NoProfile -ExecutionPolicy Bypass -Command "& '<py>' ..."
    "<scratch>\run-emxx.bat"                      (one token, no quoting)

The third carries no quoting a splitter could mangle, which rules out the
explanation the first two shared. Whatever remains is a property of how this
hook runtime spawns on Windows and is not observable from outside it: the
message names no path and the run produces nothing else. I stopped guessing
after the third.

WHY A SKIP IS ACCEPTABLE HERE AND A WITHDRAWAL IS NOT. mcpp spawns the
compiler through its own process handling, not through this hook, and Windows
is a supported mcpp host -- a user's build does not travel this code path. The
Windows payload was measured complete by reading its central directory
(`em++.exe`, `bin/clang.exe`, and the vendored `std.cppm` all present), and
install() asserts each of those files before moving anything.

So what is given up is the COMPILE, not the contents. On Linux and macOS this
check still precompiles the module surface, links a program and runs it through
node. On Windows the package rests on the file assertions plus that
measurement. That is weaker, and it is stated in a warning rather than hidden:
an unverified install that works is better than no Windows wasm at all, and
better than a green check that measured nothing.

The Windows branch in `__run_py` is removed with the same reasoning that put it
there. It is now unreachable, and a branch that looks like it handles a case
but never runs is worse than its absence -- the next reader would trust it. The
test that pinned its idiom went with it, because its subject is gone.

The new test bounds the skip instead: it must sit inside the self-check, it
must warn, and the install-time file probe must stay host-unconditional --
that probe is what the Windows package now rests on.

* fix(emsdk): the Windows invocation needed os.exec, not a fourth command string

Three shapes failed here and I varied the wrong axis each time. All three used
`os.iorun`:

    "<py>" "<script>" -std=c++23 --precompile "<in>" -o "<out>"
    powershell -NoProfile -ExecutionPolicy Bypass -Command "& '<py>' ..."
    "<scratch>\run-emxx.bat"                      (one token, no quoting)

each producing `The filename, directory name, or volume label syntax is
incorrect.` once, with no compiler output and no Python traceback. Every
Windows invocation in this index that WORKS uses `os.exec` or `system.exec` --
pkgs/7/7zip.lua and pkgs/v/vcstool.lua both do, and both are working Windows
packages. I changed the command STRING three times while holding the CALL
constant, and the call was the thing that was wrong.

`os.iorun` was chosen in the first place because this function has to return
the compiler's output for the diagnostic. A script that redirects itself
removes that requirement: the output goes to a file and is read back with
`io.readfile`, which frees the invocation to be the form this index has already
proven. `cmd.exe /d /s /c "<bat>"` is the documented shape -- `/d` skips
AutoRun, `/s` fixes quote handling -- and a batch file is not an executable
image, so it needs cmd either way.

This replaces the Windows skip from the previous commit. That skip was
defensible and this is better: the self-check now precompiles the module
surface, links and runs on all three hosts rather than two, so the Windows
package rests on the same evidence as the others instead of on file assertions
plus a reading of the archive.

Rendered both branches with the environment stubbed before pushing: Windows
emits `cmd.exe /d /s /c "C:\...\run-emxx.bat"` with a script body carrying zero
forward slashes and its own `> "...out" 2>&1`, and POSIX is unchanged.

Two tests had their subjects change and both are corrected rather than deleted.
The `.bat` assertion is scoped to ENTRY POINTS -- it read `".bat" not in code`,
which was right until this recipe started writing a script of its own, and the
claim was always about upstream's names. The skip test is replaced by one that
pins the call: `os.exec` present, `os.iorun` absent, the documented cmd shape,
and the redirect-plus-readback that makes a failure legible. Verified in both
directions.

* fix(emsdk): `.emscripten` is Python source, and a Windows path is not a string literal

With the invocation fixed, `em++` finally ran on Windows and reported the
defect that three layers of invocation failure had been hiding:

    em++: error: error in evaluating config file (...\.emscripten):
      (unicode error) 'unicodeescape' codec can't decode bytes in position 2-3:
      truncated \UXXXXXXXX escape (<string>, line 1),
      text: LLVM_ROOT = 'C:\Users\runneradmin\...\6.0.9/bin'

`em++.py` EVALUATES this file. With backslashes, `C:\Users\...` puts `\U`
inside a single-quoted Python literal and Python reads it as a unicode escape.

Note the path in that message is MIXED -- `path.join` contributed a forward
slash to an otherwise backslashed path -- which is the same mixed-separator
property that breaks a cmd.exe command line, surfacing here as a different
failure in a different language. One cause, two symptoms, two subsystems.

Forward slashes throughout. Python accepts them on Windows, emscripten's own
tooling normalises them, and one spelling means the file reads the same on
every host. Escaping the backslashes instead would work and would leave two
spellings of every path in a generated file.

Verified locally by generating the file from a Windows-shaped install
directory and EVALUATING IT WITH A REAL PYTHON INTERPRETER: three assignments,
zero backslashes, `LLVM_ROOT` reads back as the path intended.

THE ORDERING IS THE LESSON. A failure that prevents a program from starting
hides every failure that program would have reported. Three invocation shapes
failed before `em++` ever ran, so its own diagnostic never appeared and this
defect sat behind them -- and the first two of those three also produced no
output at all, which is what made them so slow to tell apart.

* fix(emsdk): the helper is general, because the third caller proved it had to be

With the config fixed, the probe reached its last step and failed there:

    node did not print the expected "1-2-3" from the `import std` probe (got: )

with the Windows message appearing twice and no output at all. The precompile
and the link had both succeeded by then.

THE NODE STEP WAS A FOURTH SITE OF THE SAME CONSTRUCTION. The helper built for
the two `em++.py` invocations was python-specific in name and in signature, so
`node` was left on a bare `os.iorun(string.format('"%s" "%s"', ...))` -- the
same shape, the same defect, one site further on, and reachable only after the
other two were fixed.

So the helper is now `__run_captured(scratch, exe, argv)` and all three
invocations go through it. `scratch` is passed rather than derived: the
previous version took `path.directory(argv[#argv])`, which was right for an
`-o <path>` call and would have written the script somewhere else for a call
whose last argument is not a path -- which is exactly what the node call is.

The test is widened from "the helper uses os.exec" to "nothing in the
self-check bypasses the helper", which is the property that would have caught
the node site when the helper was written. Verified in both directions:
returning the node call to a bare `os.iorun` turns it red.

Four defects on this one platform, each hidden behind the previous: the
extraction pattern, the entry-point suffix, the interpreter that was never
found because its payload was empty, the invocation form -- and then, only once
a program could start, the two real ones it had been unable to report.

* feat(android): 7-Zip reads ext4, and the NDK describes itself to mcpp

A1. THE EXTRACTION TOOL WAS THE OBVIOUS ONE AND THE WRONG ONE.
`android-system-image` extracts four bionic files out of `system.img` so that
the arm64-v8a key can serve `aarch64-linux-android` through qemu-user. It
declared `xim:e2fsprogs` for `debugfs`, and that payload's `debugfs` is a
broken static build: SIGFPE on every filesystem-opening command, while
dumpe2fs, e2fsck and tune2fs from the same build work. So the package
installed and could not serve the one route the key exists for.

`xim:7zip` reads ext4. Measured 2026-09-11 against this exact image:

  7zz l system.img            Type = Ext, Label = system
  7zz x system.img -o<dir> -y \
    bin/linker64 lib64/libc.so lib64/libdl.so lib64/libm.so
                              Everything is Ok / Files: 4

One command instead of four, and the in-image directories are preserved --
which IS the `<root>/system/{bin,lib64}` tree qemu-user needs, so the
extraction writes it rather than composing four destinations. The e2fsprogs
defect stays recorded and unowned here: nothing else in the index depends on
that `debugfs`, so the repin belongs to that package.

The probe still has to OPEN the filesystem, which is the lesson the old route
taught: `debugfs -V` was the one command that binary answered without touching
an image, and it answered it and then died on everything else. So the new
probe lists the image and requires `Type = Ext` in the answer -- a statement
that can only come from having opened it. And the extraction asserts every
FILE, because 7-Zip exits 0 having extracted nothing when a named entry is
absent.

Its test survived the tool change with its subject renamed rather than being
deleted with it: neither assertion was ever about debugfs. They are about
resolving a declared dependency's program through `dep_install_dir` rather
than PATH, and about a probe that cannot pass on a binary that cannot work.

AND THE TEST REPEATED THE MISTAKE ITS OWN COMMENT WARNED ABOUT. A flat search
over the source for a bare `7z ` matched the raise message that names the
programs it looked for, and a flat `-V` search matched the message explaining
why a version probe is insufficient. Stripping comments is not enough when the
text also lives in a string literal, so both checks are now scoped to the
lines that RUN something.

C3. `.mcpp-toolchain.json`: THE PAYLOAD DESCRIBES ITSELF. Three facts about
this NDK lived inside mcpp's engine -- the
`toolchains/llvm/prebuilt/<host>/bin` layout, the API floor's location in
`meta/platforms.json`, and the `-D__BIONIC_CTYPE_INLINE=` its libc++ module
surface needs. This recipe already computes all three for its own probes, so
the engine was re-deriving facts their owner held, and that is why adding a
second such SDK meant editing the engine instead of publishing a package.

The recipe now writes the descriptor after the std-module self-test, not
before: the file asserts that this payload's `import std` works with that
define, and writing the claim before measuring it would publish a contract on
an unverified payload. The floor is READ from `meta/platforms.json` rather
than written down, because a constant here becomes a version in a diagnostic
and then in somebody's command line. The path is forward-slashed on every
host, because a backslash in JSON is an escape character -- a mistake this
index has already paid for once, in pkgs/e/emsdk.lua.

Two assertions, because the file is only worth writing if it is true: the
recipe refuses when the frontend it named does not exist or disagrees with the
compiler this install verified, and the tests read it back and check each key
against the installed tree.

Verified end to end against the RELEASED payload, with mcpp built from
mcpp-community/mcpp#612:

  frontend            a descriptor naming `oddly/named/clang++` resolved
                      there -- a path no engine derivation produces
  platform_floor      "26" gave `x86_64-unknown-linux-android26` while
                      meta/platforms.json says 21
  std_module_defines  an added define appeared in the std module's own
                      command and in its cache identity
  absent              with the file removed, the build resolves exactly as
                      before -- no flag day
  malformed           refused naming the file and the key, with the fix in
                      the message

* feat(apple): simctl-run, a session the manifest cannot express

D3. mcpp's `runner` is an argv prefix, so running a program on an iOS
simulator could in principle be written into a manifest -- and it cannot,
because it is a SESSION: choose a device, boot it if it is not booted, wait for
the boot to finish, spawn, and return the program's own exit status. A manifest
line has no beginning and no end. So the platform knowledge lives in a package
and the build tool learns nothing about simulators, which is the boundary it
keeps for every other emulated target.

THE MEASUREMENT CHANGED THE SHAPE OF THIS PROGRAM. The design record assumed a
bare Mach-O could not be launched by simctl and that this program would have to
synthesise a bundle, sign it and install it. That is true of `simctl launch`,
which needs an installed .app. `simctl spawn` takes an executable. Measured
2026-09-11 on macos-15 (Xcode 16.4, iPhoneSimulator 18.5) against an artefact
built by `xim:llvm` for `arm64-apple-ios18.0-simulator` with no bundle, no
signature and no Info.plist:

  xcrun simctl spawn <udid> <artifact>   ->   1-2-3, exit 0

so the whole of what remained was arriving at a booted device.

Four properties, each with a test:

  - THE PROGRAM'S EXIT STATUS IS RETURNED. A runner that reported its own
    success would make every test pass, and `mcpp test` reads this status.
    `set -e` is deliberately absent from the spawn for the same reason.
  - NOT `simctl spawn booted`. That spelling fails with no useful message when
    nothing is booted, and arriving at a booted device is this program's job.
    A booted device wins the selection, because a cold boot is 30-60 seconds
    and a suite that boots per test times out.
  - THE DEVICE LIST IS READ AS JSON. The human table's headings and
    indentation are not an interface, and this index has a record of a recipe
    broken by a tool's output format changing under it.
  - EVERY REFUSAL NAMES WHAT IS MISSING, on stderr: no xcrun, no available
    device, or a boot that did not finish. `mcpp run` prints what the runner
    printed, so a silent failure is indistinguishable from a program that
    produced no output.

macOS only and no url, and both are the point rather than limitations: the
simulator runtime is a proprietary component of the OS it simulates, so there
is nothing to package and nothing to mirror. A url appearing in this recipe
later would mean someone had started redistributing a piece of Xcode. The
install hook checks the program it wrote with `bash -n`, because a syntax error
in a heredoc is the failure this recipe is most likely to have and no static
check of the Lua would see it.

AND THE TEST FILE MADE THE MISTAKE ITS SIBLING RECORDS TWICE. An assertion
that `simctl spawn booted` does not appear matched the comment explaining why
that spelling is not used. The explanation and the mistake use the same words,
which is why this keeps recurring; the fixture now offers a comment-stripped
view for assertions about behaviour and the raw text for assertions about
refusal messages.
…yload names its C compiler its own way

TWO DEFECTS, BOTH FOUND BY THE WINDOWS JOB AND THE SECOND BY A SECOND PAYLOAD.

A DESCRIPTOR'S `frontend` WAS VALIDATED THROUGH `std::filesystem::path`, WHOSE
ANSWERS DIFFER BY HOST WHILE THE DESCRIPTOR DOES NOT.

Measured on a Windows runner: `path("/usr/bin/g++").is_absolute()` is FALSE
there -- the path has a root directory and no root NAME, which Windows calls
root-relative -- so the guard passed, and `payloadRoot / "/usr/bin/g++"` then
resolves to `C:/usr/bin/g++`. A host compiler, chosen by a package, on the one
host where the check did not look. That is precisely the property the guard
exists to prevent: mcpp's hermeticity is a property of the payload boundary,
not of a recipe's good manners.

The field is one shape on every host -- relative, `/`-separated, plain
components -- so it is now validated as a STRING and positively: no backslash,
no leading `/`, no drive or scheme, and no empty, `.` or `..` component. Asking
a path type whether it is absolute is asking a question whose meaning the host
supplies. Three more cases join the table, and each fails when the check is
removed.

AND THE FIXTURE TESTED ONE HOST. It created `bin/clang++` while the built-in
guess searches `bin/clang++.exe`, so "no descriptor resolves exactly as
before" failed on the host where the executable suffix exists -- a test about
the descriptor failing for a reason that had nothing to do with it. The
frontend name now comes from `platform::exe_suffix`, and the descriptor case
deliberately uses a name the guess would never try, which is what makes that
assertion about the descriptor.

`em++`'s C COMPILER IS `emcc`, NOT `em`. Dropping `++` is clang's rule and was
being applied as the rule; `g` was already a special case for exactly this
reason, and a second driver of the same shape arrived without one. Measured on
the conformance suite's one C translation unit for `wasm32-emscripten`:

  /bin/sh: 1: .../xim-x-emsdk/6.0.9/emscripten/em: not found

A table rather than a third `if`, because the property is "this driver names
its C compiler with a different word" and a table can be read as the list of
drivers for which that is true. The derivation is exported so the mapping can
be asserted without a toolchain, and the test enumerates every frontend
spelling `to_xim_package` can resolve -- a payload added there without a row
here is a payload whose C units compile with a program that does not exist.

THE iOS SDK IS LOCATED BEFORE ANY PAYLOAD IS RESOLVED. It was located where
the toolchain is, so a machine without Xcode downloaded a 700 MB compiler and
was then told the thing it was missing was not the compiler. Unlike
`host_can_serve` it is not deferred: an Apple SDK is not redistributable, so no
package supplies it and no later line can change the answer. The escape hatch
that opens the tier gate does not open this one -- declaring a toolchain says
which compiler and says nothing about where the headers are.

Measured on Linux, which is where this claim can be made: with an explicit
`[target.aarch64-ios] toolchain` and MCPP_NO_AUTO_INSTALL=1, the refusal
arrives before the resolution line and names the SDK, the `xcrun` command that
must answer, that Command Line Tools alone ship the macOS SDK only, and that
the compiler is not what is missing.

AND THE macOS CI LEG'S OWN VERSION OF THAT CLAIM WAS MEASURING NOTHING. With
`DEVELOPER_DIR` pointed at a nonexistent directory the iOS build SUCCEEDED --
`xcrun` ignores an invalid developer directory and falls back -- so the step
asserted a refusal that did not happen. The predicate was right and the object
was wrong; the claim now lives where the SDK is genuinely absent.

WHAT THE macOS RUN DID MEASURE, all on macos-15 with Xcode 16.4:

  xcrun --sdk iphoneos        iPhoneOS18.5.sdk
  xcrun --sdk iphonesimulator iPhoneSimulator18.5.sdk
  simctl runtimes             iOS 18.5, 18.6, 26.0, 26.1, 26.2
  simctl spawn <bare Mach-O>  1-2-3, exit 0     <- no bundle, no signature
  SDK libc++                  links
  payload static libc++       refused, as predicted for a macOS archive
  import std, located SDK     precompiles and links
  mcpp build --target aarch64-ios       platform 2 (IOS),    minos 18.0
  mcpp build --target aarch64-ios-sim   platform 7 (IOSSIM), minos 18.0
  simctl spawn <that artefact>          1-2-3
…ad, and the iOS rows are written down

NEITHER `mcpp toolchain install emsdk` NOR `mcpp toolchain install android-ndk`
HAD EVER WORKED, and openkal-emscripten's continuous integration is what found
it:

  $ mcpp toolchain install emsdk 6.0.9
    error: installed package has no known C++ frontend in
           '.../xim-x-emsdk/6.0.9/bin'

TWO DEFECTS, ONE PER AXIS.

`to_xim_package` decided the payload from the TARGET. That is right for the
spelling a build uses -- `--target wasm32-emscripten` with nothing declared --
and there is no target at all in an install, so the emsdk spec resolved the
generic llvm shape and looked for `clang++` in `bin/` while `em++` is in
`emscripten/`. `payloadName` is the field `parse_toolchain_spec` fills from the
spelling and is the more direct statement of the two: a spec that names a
payload has answered this question. It decides first; the target remains the
answer for a spec that names no payload, which is the escape-hatch spelling the
capability gate has to be able to refuse.

And the install path composed `<root>/bin` itself. That was one of the five
sites the `frontendSubdir` note in mcpp.toolchain.registry records; four were
repaired and this one was not. It now asks `payload_frontend`, which is the
same function the build path asks -- so the two cannot disagree, and the
install picks up the payload's descriptor for free.

AND THE FIXTURE OF THE TEST THAT COVERS THIS AXIS HAD PICKED ITS OBJECT BY
CONVENIENCE. `TheTargetChoosesThePackageAndThePackageKnowsItsLayout` built
every case from `emsdk@6.0.9` and replaced only the target, which worked while
the target was the only thing read. It now builds them from `llvm@22.1.8`,
which names the family and no payload -- the shape whose resolution that test
exists to state.

D5. THE iOS ROWS ARE WRITTEN DOWN, in docs/20 and its Chinese copy: the
compiler is ours and only the SDK is Apple's, the two host dependencies this
platform adds are named and bounded, the deployment target travels in the
effective triple alone, and the simulator is two rows because it runs the
host's architecture.

`examples/13-platform-targets` gains the iOS rows with measured readings --
`platform 2` for the device and `platform 7` for the simulator, which is the
one pair a successful build cannot tell apart -- and records two limitations it
ran into rather than working around them: `deps` is not conditional on a
target, and `xim:apple-simulator-tools` exists for macosx alone, so declaring
the runner's package in the manifest would make this example's Linux build
depend on a macOS-only package. The program is installed by whoever runs those
rows.

E4c. `docs/24-openkal-cross` and its Chinese copy stop saying the Web needs an
implementation and name `openkal-emscripten`, with the three-group table that
makes a partial surface a conformant one. The iOS section stops being a
structural argument: what the SDK's licence bounds is packaging it, not
locating it, and `aarch64-macos` has been verified on exactly that split since
before these rows existed.

e2e 641 gains the iOS claims that a non-Apple host can make: the rows name
their payload, and the SDK's absence is a refusal that names the SDK, the
`xcrun` command, the Command-Line-Tools note and the compiler -- arriving
before any payload is resolved. The effective-triple claim is a unit test
instead, because that same gate means a host without Xcode never prints the
line an end-to-end check would read.

AND THE macOS LEG NOW RUNS THE ARTEFACT THROUGH THE SUPPORTED PATH. The
earlier leg spawned it by hand, which proves the platform can run it and not
that mcpp can; `mcpp run --target aarch64-ios-sim` goes through the `runner`
the manifest declares and the program `xim:apple-simulator-tools` provides.
… already do

D4's LEG WAS RED AND THE PACKAGE WAS INSTALLED CORRECTLY. Measured on a
macos-15 runner with `xim:apple-simulator-tools` declared and present:

  error: runner 'simctl-run' for 'aarch64-ios-sim' was not found on any
         search path.
  Searched: .../xim-x-apple-simulator-tools/0.1.0/bin

The directory searched was right and the program was one level up. `bin/` is
the convention and stays FIRST; it is not universal -- `xim:7zip` puts `7zz`
straight into its install directory, and so did the first version of
`xim:apple-simulator-tools`. Two directories per declared package is cheaper
than a rule every recipe has to know, and a recipe that does use `bin/` is
unaffected because that entry is still tried first.

The recipe moves too (openxlings/xim-pkgindex#820, merged): a package whose
whole purpose is to provide a program puts it where consumers look, and its
`xvm` registration moves with it -- a recipe that wrote into `bin/` and
registered the root would install without complaint and produce a shim
resolving to nothing.

And the fixture uses `[xlings.workspace]`, which is what mcpp asks for:
"deps is superseded by [xlings.workspace] and will stop being read".

WHAT THE macOS RUN MEASURED FOR ALL THREE ROWS, `ios_deployment_target
= "18.0"`, Xcode 16.4, SDKs 18.5:

  aarch64-ios      arm64  LC_BUILD_VERSION platform 2 (IOS)          minos 18.0
  aarch64-ios-sim  arm64  LC_BUILD_VERSION platform 7 (IOSSIMULATOR) minos 18.0
  x86_64-ios-sim   x86_64 LC_BUILD_VERSION platform 7 (IOSSIMULATOR) minos 18.0

`platform 2` against `platform 7` is the pair worth having a reading for: a
successful build cannot tell them apart, and an artefact reporting
IOSSIMULATOR from the device row is one no later step refuses. The simulator
artefact ran under `simctl spawn` and printed `1-2-3`.
…mplementation's own review

The plan's nine-angle self-review was written before any of this was built.
This adds the review of the IMPLEMENTATION, whose unit is a measured defect
rather than an angle: nineteen findings, nine of them in mcpp itself, each with
the reading that made it visible and the class it belongs to.

Four classes account for most of them. A decision written in several places
with all but one repaired (the install path composed <root>/bin itself). One
question with two inputs of which one was read (the payload name was filled and
never consulted). A branch selected by the HOST serving a target it would get
wrong -- three findings, and the note that already said so had been read as a
statement about openkal rather than about every target that branch would serve.
And a criterion whose object was wrong: DEVELOPER_DIR=/nonexistent did not make
the SDK unlocatable, so the host-surface claim measured nothing.

The criteria table now carries the reading beside each claim, including the two
the measurement changed: simctl-run is a boot-and-spawn wrapper and not a
bundle builder, and openkal.task on Emscripten cannot be exercised end to end
because -pthread is a whole-graph ABI switch mcpp has no channel for.

Four gaps are recorded and deliberately not closed, each with a measurement
behind it: no per-target tool axis, no whole-graph flag channel,
xim:e2fsprogs's debugfs, and a device runner that needs a signature.
MEASURED ON macos-15 (Xcode 16.4, iPhoneOS and iPhoneSimulator 18.5,
`ios_deployment_target = "18.0"`), through mcpp built from this branch:

  aarch64-ios-sim  verified  arm64  platform 7 (IOSSIMULATOR)  minos 18.0
                             mcpp run --target aarch64-ios-sim  ->  1-2-3
  aarch64-ios      preview   arm64  platform 2 (IOS)           minos 18.0
  x86_64-ios-sim   preview   x86_64 platform 7 (IOSSIMULATOR)  minos 18.0

The simulator row ran through the SUPPORTED PATH -- the `runner` a manifest
declares and the program `xim:apple-simulator-tools` provides -- and not only
under a hand-written `simctl spawn`. This repository has a standing note that
publishing and verifying an asset is not the same as being able to use it
through a supported path, and the difference here was a defect: the program was
at the payload root and mcpp searched only `<payload>/bin`.

NEITHER `preview` IS WORK NOT YET DONE. `aarch64-ios` cannot be run off a
device without a signature the developer owns, which is not something a build
tool or a package can supply; `x86_64-ios-sim` cannot be run on an
Apple-silicon host, because a simulator runs the HOST's architecture. Both are
tiers bounded by a fact rather than by a gap, and the rows say so.

`platform 2` AGAINST `platform 7` IS THE PAIR WORTH HAVING A READING FOR. A
successful build cannot tell them apart, and an artefact reporting
IOSSIMULATOR from the device row is one no later step refuses.

A NEW REFUSAL CODE, BECAUSE THE MATRIX COMPARES REASONS AND NOT ONLY OUTCOMES.
`apple-sdk-absent` is distinct from `host-cannot-serve`, which says no payload
here produces the target and waits for the graph in case a package supplies the
system -- an Apple SDK is not redistributable, so no package can and nothing a
later step learns changes the answer. It is distinct from `tier-planned`, which
says the row is not wired: these rows are. Without a code the refusal would be
recorded as `other`, which `check_matrix_reasons.sh` refuses on the ground that
it freezes an unnamed branch into the expected table.

THE TIER MOVE REACHES SIX PLACES AND A SEVENTH THAT WOULD HAVE STAYED GREEN.
The engine's table, both READMEs (where one combined row became three, because
the three tiers now differ), both copies of docs/21 -- including the per-host
columns, which said `planned` everywhere and now say `SDK` on macos-arm64 and
`—` elsewhere -- the test's one-line pairs, and `examples/13`'s matrix. The
seventh is e2e 641, whose simulator case asserted `tier-planned`: it would have
gone green on an expectation the table had left behind, which is the shape
`check_target_tiers.py` was written for one axis over.
…and the measurement said so

The design record asked for that example to build for all five platforms it
names. The fifth has twelve of the fifteen interfaces and the program uses two
of the three absent ones, so the link names seven symbols:
kal_process_{spawn,wait,close} and kal_task_{start,join,wait,wake}.

Neither way of making it build is acceptable -- #ifdef-ing the one file in this
ecosystem that exists to contain no platform awareness, or having
openkal-emscripten provide operations it cannot perform, which is the shape
clause 6.2 forbids. So the criterion was wrong and the example is better for
it: it demonstrates the boundary, with the linker's own output as the reading.

mcpplibs/openkal#30 carries the example's own correction.
…o sites asked it differently

THE TIER MOVE EXPOSED TWO DEFECTS, ONE ON EACH SIDE OF THE SAME QUESTION.
While the iOS rows were `planned` neither could be seen: `toolchain list` keeps
a planned row unconditionally, so the listing was right by accident on every
host. Measured as `scan (macos-arm64)` and `scan (linux-aarch64)` reaching
three fewer cells than the expected table declares.

`host_can_serve` ASKED `os == "macos"`. That was the same question as "is this
an Apple target" while macOS was the only Apple target mcpp had. An iOS target
fell through to the function's final `return false` -- on EVERY host, including
the one that serves it. The consequence was not a refused build, because the
SDK gate in prepare answers first; it was a listing that dropped the three iOS
rows on macOS. It now asks `is_apple()`, a predicate the target table had
carried with NO READER AT ALL until this line.

`toolchain list`'s `graphCouldServe` CLAIMED THEM. Its own comment names
`aarch64-macos` as correctly absent on a Linux host -- and that row was absent
by ACCIDENT rather than by rule: its pin is empty, so `!info.pin.empty()`
excluded it. The iOS rows have a pin now (`llvm@22.1.8`, the ordinary payload),
so they entered the branch, found llvm in the index, and were listed on a host
that cannot produce them. The discriminator that comment appeals to is real and
is not the pin: it is whether a PACKAGE can supply the target's system, and no
package supplies an Apple SDK.

Asserted in both directions for all four Apple rows, because one direction is
what that test already calls an exclusion which must not become a tautology:
every Apple row is servable exactly where the SDK is.

AND A SKIP I WROTE FROM AN ASSUMPTION WAS REFUTED BY A ROW IN THE SAME TABLE.
The macOS scan also reported `graph x iOS` as `mismatch / build-failed`, and I
first wrote it off as out of the fixture's domain -- "an Apple row's system
comes from a located SDK and cannot be replaced by a graph-supplied musl".
`graph macos-arm64 aarch64-macos` is measured `ok / none` with `musl(graph)` in
its own column. The predicate I had written would have skipped that working
cell.

The real reason is one line of a dependency:

  openkal-musl-0.3.5/port/src/okm_syscall.c:444: error: incompatible pointer
  types passing 'uint64_t *' (aka 'unsigned long *') to parameter of type
  'kal_u64 *' (aka 'unsigned long long *')

One width, two type identities. musl's own `<stdint.h>` spells `uint64_t` as
`unsigned long` on LP64; `kal_u64` is `__UINT64_TYPE__`, which clang defines as
`unsigned long long` for a DARWIN target. On every Linux and Windows musl
target the two coincide, which is why that package's CI has never seen it:
openkal-musl has never been built for an Apple target, and 0.13.1 has the same
shape. The skip is therefore scoped to the rows the diagnostic covers, its
comment says why macOS is NOT skipped, and the defect is recorded as a gap
belonging to openkal-musl -- the iOS rows do not depend on it, since their
system is the located SDK, which is the `payload` mode the tier rests on.

`mismatch` is not written into the table for any of this. The table's own
header forbids it, on the ground that writing one down declares a defect to be
the expectation.

Verified locally for this host after the change: 48 of 48 payload cells and 22
of 22 graph cells match the expected table.
… and did not apply

`host_can_serve` now answers for the host, and `toolchain list` therefore
drops an Apple row where its SDK cannot be. e2e 641 case 7 still required the
three iOS rows to appear with their pin -- true while they were `planned`,
because a planned row stays listed everywhere so it is discoverable, and false
the moment the tiers moved.

  FAIL: aarch64-ios does not name llvm in toolchain list

I HAD ALREADY WRITTEN THIS FIX DOWN. While diagnosing the matrix failure I
noted that case 7's claim must become host-conditional. Then I fixed cases 3
and 8, added the unit assertion, ran the unit suite, and pushed -- having
changed the listing and not re-run the end-to-end test that asserts the
listing. A unit suite passing is not evidence about a behaviour no unit test
observes.

Both arms are real. On macOS the rows are listed with their pin; elsewhere they
are absent, and two companions keep that absence from reading as a gap:
`aarch64-macos` is absent for the same reason, and `x86_64-macos` is present
because a `planned` row stays discoverable on every host. The pin itself is a
property of the table and is asserted where every host can ask it, in
tests/unit/test_toolchain_triple.cpp.

Third instance of one class in an hour -- the other two were the sandbox
verification's section 2, caught by a dry run, and thirty rows of
expected.tsv. The design record now carries the three together.
… the same pass

THE macOS JOB THAT MEASURED THE iOS ROWS WAS A PROBE, AND IT STAYED ONE AFTER
ITS MEASUREMENTS MOVED `aarch64-ios-sim` TO `verified`. Every step continued on
error, so a regression in the Apple cross path would have printed
`RUN-THROUGH-RUNNER-FAILED` inside a job reported green. Every reading of that
job had been green, which is why only reading the workflow could find it.

The job is split along that distinction:

  ios-host-surface   the premise measurements; continues on error; runs on
                     workflow_dispatch only, because a check that cannot fail,
                     shown beside a gate, reads as a second gate
  ios-engine         fails when an artefact's architecture, LC_BUILD_VERSION
                     platform (2 device, 7 simulator) or minos differs from the
                     row's -- compared as whole values, so an empty reading
                     fails -- and when `mcpp run --target aarch64-ios-sim` does
                     not exit 0 with `1-2-3` as a line of its output

The fixture no longer declares `toolchain = "llvm@22.1.8"` for the three rows.
That override was needed while they were `planned`, and it meant the job
measured an override and never the rows' own pin.

THE LOCATED SDK PATH REACHES THREE COMMAND LINES AND WAS QUOTED ON TWO. The std
module's own command spliced it into a shell string unquoted, while every
other path in that string goes through `shq`; an Xcode installed as
`Xcode 16.app` would have broken only the module precompile.

THE PAYLOAD ROOT JOINED `bin/` IN THE RUNNER SEARCH WITH A MEASUREMENT AND NO
TEST. The rule is now `runner_lookup::payload_search_dirs`, and
`AFlatPayloadIsFoundAtItsRoot` fails when it is reverted to `bin/` alone --
checked by reverting it. Two comments that still said "bin/ first, then PATH"
now state the pair.

Also: an emoji removed from a comment in tests/matrix/scan.sh; the design
record's Status line said "for review before implementation" and now names
where each item was implemented; its front matter is `landed` and the index is
regenerated.

Local: unit 111 passed, 0 failed; e2e 641 and 334 pass against the rebuilt
binary.
The release notes are extracted from the entry whose heading names the
version, and without one the release would publish "(no CHANGELOG entry found
for 2026.9.11.4)". Found during the pre-release review.
@Sunrisepeak
Sunrisepeak merged commit c58d61e into main Sep 11, 2026
39 checks passed
@Sunrisepeak
Sunrisepeak deleted the docs/sdk-toolchains-and-ios-design branch September 11, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants