Skip to content

Commit 01f40c7

Browse files
committed
docs(record): three vendors measured at current versions, and Android execution is conditional
Documentation only. The design record's section 3.1 measured NDK r27, Emscripten 4.0.19 and guessed about Apple. All three were re-measured while the payloads were built, and none of the three guesses held. TWO OF THE THREE VENDORS NOW SHIP THE `import std` SURFACE THEMSELVES. NDK r30 133 files ships it _LIBCPP_VERSION 210000 Emscripten 6.0.9 134 files ships it 220108 iPhoneOS 26.5 0 files does not 210106 So the generation machinery the section describes at length is needed for one target rather than three, and what a recipe owes instead is narrower: pin the value measured and refuse a change. For Android that is all it can be -- r30's libc++ comes from an AOSP mirror revision (`r574158c`) with no upstream tag to compare against. Emscripten's numbers in the record were two releases stale; the lesson it drew from them -- match the LIBRARY, not the compiler -- is right, which is why the correction is to the numbers and not to the argument. APPLE WAS THE LARGEST CORRECTION AND WAS RECORDED SEPARATELY: iOS CAN be measured from a Linux host, Apple's libc++ IS a build of a public revision, and the surface is absent but derivable. An `import std` Mach-O arm64 binary was linked here. ANDROID EXECUTION IS CONDITIONAL, AND THE CONDITION DECIDES THE TIER. x86_64-linux-android static: runs directly dynamic: does not aarch64-linux-android static: runs under qemu dynamic: does not Dynamic fails for two independent reasons and neither is a missing flag. The artifact's `PT_INTERP` names `/system/bin/linker64`, which exists nowhere in the 2.3 GB NDK -- `-L` and `QEMU_LD_PREFIX` affect library SEARCH, not the interpreter path. And bionic's `libc.so` is inert: 1147 exported functions in 9176 bytes of `.text`, every one inspected disassembling to `bti c; ret`. That is the documented stub shape; the bodies come from the device. For contrast `libc++_shared.so` is real code at 724 KB, because it is the one runtime the NDK owns. So the honest claim is "runnable only when statically linked", and the tier follows from the DEFAULT configuration rather than from the best case. The rows carry `defaultStatic = false` and that is correct about the platform: a real Android application links dynamically against the device's bionic. Setting the flag true to make an execution check pass would misdescribe the platform to flatter the measurement. A `--no-default-config` finding is recorded with the Apple work because that is where it surfaced, but it is a property of the `xim:llvm` payload and applies to any non-Linux cross target driven through it: `bin/clang++.cfg` injects the host's glibc and libc++ unconditionally, `-nostdinc++` does not displace them, and the criterion is `clang -v`'s search list rather than whether the compile succeeds. Section 8 names the pattern the corrections share rather than only listing them: every wrong guess was about what a vendor had done, and every one was cheap to check and was not checked.
1 parent 51dff99 commit 01f40c7

1 file changed

Lines changed: 94 additions & 7 deletions

File tree

.agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -386,16 +386,103 @@ int main() {
386386
app: ELF 64-bit LSB pie executable, ARM aarch64, interpreter /system/bin/…
387387
```
388388

389-
The binary was not executed — no device or emulator here — so this is "compiles
390-
and links", not "runs".
389+
### Android execution: measured, and the answer is conditional
390+
391+
The binary above was not executed when this was first written. It has been
392+
since, and the result is split in a way that decides the row's tier rather than
393+
merely informing it. Measured 2026-09-11 with NDK r30 and `qemu-aarch64` 8.2.2:
394+
395+
| | static | dynamic (the row's default) |
396+
|---|---|---|
397+
| `x86_64-linux-android` | **runs**, direct execution, no wrapper | does not run off-device |
398+
| `aarch64-linux-android` | **runs** under plain `qemu-aarch64`, no flags | does not run off-device |
399+
400+
Both confirmed with the `import std` program, printing `1-2-3`.
401+
402+
**Dynamic fails for two independent, compounding reasons**, and neither is a
403+
missing flag:
404+
405+
1. **The loader is device-only.** Every dynamic artifact carries
406+
`interpreter /system/bin/linker64`. `-L` and `QEMU_LD_PREFIX` affect library
407+
SEARCH, not the interpreter path, so qemu opens the literal absolute path and
408+
reports `Could not open '/system/bin/linker64'`. An exhaustive search of the
409+
2.3 GB installed NDK finds **zero** files named `linker` or `linker64`: the
410+
Android dynamic linker ships only inside a system image.
411+
2. **Even with a loader, bionic's libc is inert.** `libc.so`'s `.text` is 9176
412+
bytes for 1147 exported functions — about eight bytes each — and every
413+
function inspected disassembles to exactly `bti c; ret`. That is the
414+
documented NDK stub shape: it exists so the linker can resolve names, and
415+
the real bodies come from the device. For contrast `libc++_shared.so` is
416+
real code, 724 KB, because it is the one runtime the NDK is responsible for.
417+
418+
**So the honest claim is "runnable only when statically linked", and the tier
419+
follows from the DEFAULT configuration rather than from the best case.** The
420+
rows carry `defaultStatic = false`, and that is correct about the platform: a
421+
real Android application links dynamically against the device's bionic, and
422+
setting the flag true to make a CI check pass would misdescribe the platform to
423+
flatter the measurement. Android is therefore `preview`, with execution
424+
recorded for a configuration that is not the default — which is more than
425+
`preview` usually means and less than `verified` requires.
426+
427+
`x86_64-linux-android` needs no runner at all for the static case; mcpp's
428+
"no runner declared, attempt direct execution" default is already right.
429+
`aarch64-linux-android` needs `runner = ["qemu-aarch64"]` and nothing else.
430+
431+
What dynamic execution would take is an emulator harness rather than an
432+
addition: a system image per (ABI, API level) at multiple gigabytes each,
433+
`adb`/`emulator`/`avdmanager` on top of the NDK, KVM or nested virtualisation
434+
that an ordinary runner does not have, and boot times in tens of seconds
435+
against qemu-user's near-instant start. That is its own package and its own CI
436+
lane.
437+
438+
### The state as of 2026-09-11, all three vendors measured at current versions
439+
440+
The sections above are the 2026-09 measurement of the versions then current
441+
(NDK r27, Emscripten 4.0.19). Every one of them was re-taken while building the
442+
payloads, and the picture changed:
443+
444+
| | NDK **r30** | Emscripten **6.0.9** | iPhoneOS **26.5** |
445+
|---|---|---|---|
446+
| `_LIBCPP_VERSION` | 210000 | 220108 | 210106 |
447+
| a public `llvmorg-*` tag? | **no** — AOSP `r574158c` | yes, 22.1.8 | yes, 21.1.6 |
448+
| ships the module surface | **yes**, 133 files | **yes**, 134 files | **no**, 0 files |
449+
| needs a define to build it | `-D__BIONIC_CTYPE_INLINE=` | none | none |
450+
| execution reachable here | see below | **yes** — node | no |
451+
452+
**Two of the three vendors now ship the surface themselves**, and the third's
453+
is derivable because its version is public. So the generation machinery this
454+
section describes at length is needed for one target rather than three, and
455+
what the recipes actually owe is narrower than the section implied: **pin the
456+
`_LIBCPP_VERSION` you measured and refuse a change.** For Android that is all
457+
it can be — an AOSP mirror revision has no upstream tag to compare against.
458+
459+
The other corrections worth carrying:
460+
461+
- **Emscripten's version was wrong in this document, in the direction the
462+
document warned about.** It recorded `_LIBCPP_VERSION 200100` and clang
463+
22.0.0git; 6.0.9 reports `220108` and clang 24.0.0git. The lesson it drew --
464+
that the surface must match the LIBRARY and not the compiler -- is right, and
465+
the numbers it drew it from are two releases stale.
466+
- **`emsdk` cannot be a payload at all.** `emscripten-core/emsdk` publishes
467+
**zero** GitHub releases, and its `emsdk.py` fetches the real toolchain over
468+
the network at install time. The recipe therefore names what `emsdk.py`
469+
itself downloads: an `emscripten-releases-builds` bundle on Google Cloud
470+
Storage, addressed by a 40-hex commit hash and thus immutable. The `latest`
471+
alias moves several times a week and is resolved once, in the recipe, rather
472+
than at install time.
473+
- **`em++` needs a Python interpreter before it opens any config file**, being
474+
a `/bin/sh` wrapper that execs Python. And `NODE_JS` is mandatory for
475+
*linking*, not only for running the result: measured, a broken `NODE_JS`
476+
survives `-c` and fails the link on `tools/compiler.mjs`.
391477

392478
### Emscripten: measured, works, no define
393479

394-
Named modules work as shipped. The module surface is absent as it is on
395-
Android, and the version to match is **not** the one the compiler reports:
396-
`em++` is clang 22.0.0git while its libc++ is `_LIBCPP_VERSION 200100`, LLVM
397-
20.1. llvm 22.1.8's surface therefore fails on `'flat_set' file not found`;
398-
llvm 20.1.7's builds with **no additional flags at all** (31 MB BMI).
480+
Named modules work as shipped. The module surface was absent in 4.0.19 as it
481+
was on Android, and the version to match is **not** the one the compiler
482+
reports: `em++` was clang 22.0.0git while its libc++ was `_LIBCPP_VERSION
483+
200100`, LLVM 20.1. llvm 22.1.8's surface therefore failed on `'flat_set' file
484+
not found`; llvm 20.1.7's built with **no additional flags at all** (31 MB
485+
BMI). Both halves of that have since moved -- see the table above.
399486

400487
End to end, and this one did run:
401488

0 commit comments

Comments
 (0)