# Package compatibility report

What this pipeline has actually built and run, measured on real packages rather
than on the fixtures written to succeed. Every wheel below was produced by
`scripts/build_extension.py` from a pinned, hash-verified source, with no
package-specific code path anywhere in the builder.

ABI for every entry: `sbxabi1-c2637d04695ad927`, tag
`cp313-cp313-emscripten_5_0_6_wasm32`, Emscripten 5.0.6, CPython 3.13.5.

## Status vocabulary

| status | meaning |
| --- | --- |
| `supported-generic` | builds and runs with no recipe beyond identity and source |
| `supported-recipe` | needs declared configuration (native library, build env, build tool) but no patch |
| `supported-patched` | needs a visible, tested patch for an assumption false on this target |
| `blocked-platform-capability` | the platform does not provide what the package needs; no patch can add it |
| `blocked-toolchain` | the build system or toolchain cannot produce this artifact yet |
| `not-yet-attempted` | not tried |

## Real packages

| package | version | cohort | status | backend | wheel sha256 (16) | size |
| --- | --- | --- | --- | --- | --- | --- |
| typing-extensions | 4.12.2 | 1 pure control | `supported-generic` | — (PyPI pure wheel) | — | — |
| MarkupSafe | 3.0.2 | 2 setuptools C | `supported-generic` | setuptools | `00619948d4c33812` | 12 KB |
| msgpack | 1.1.0 | 3 Cython | `supported-recipe` | setuptools | `b5336ed6394b0307` | 62 KB |
| PyYAML | 6.0.2 | 3 + 5 Cython and native library | `supported-recipe` | setuptools | `23c9ad8ee878f41e` | 129 KB |
| pydantic-core | 2.46.5 | 4 Rust/PyO3 | `supported-recipe` | pyo3 | `01706c4ed32d7f24` | 1951 KB |
| pydantic-core | 2.23.2 | 4 Rust/PyO3 | `supported-recipe` | pyo3 | `e1bea0f995650d93` | 1934 KB |
| numpy | 2.2.6 | 8 scientific stress case | `supported-recipe` | meson | `93ae7230479566b4` | 4917 KB |
| siphash24 | 1.9 | 6 + 7 Meson, patched subproject | `supported-patched` | meson | `cf672c37b0f8e4cd` | 54 KB |
| simplejson | 3.19.3 | — | `supported-generic` | — (PyPI pure wheel) | — | — |
| psutil | 6.1.0 | — | `blocked-platform-capability` | setuptools | — | — |

Eight independent real packages across six build shapes — pure, setuptools C,
Cython, Cython plus a separately built native library, Rust/PyO3, and Meson via
meson-python — install and are exercised by
`test/python-runtime/package-cohorts.test.ts`. Every cohort in the roadmap now
has a real package in it. That test
also loads four of the compiled ones into a single interpreter at once, which
is the only check that proves the shapes coexist rather than merely each
working alone.

### What each entry adds

**typing-extensions** — the control. No cross build, no platform tag. Proves
the installer still does the ordinary thing when nothing is compiled.

**MarkupSafe** — one C source, no dependencies, no configuration. The purest
measurement of the generic setuptools path: the recipe states identity and
source and nothing else. Its documented pure-Python fallback is why the test
asserts `markupsafe._speedups` specifically; an extension that failed to build
would otherwise install and import cleanly.

**msgpack** — a Cython extension with no external library, so a failure here
would be about generated C rather than about linking. Needs only
`buildRequires: ["Cython==3.1.4"]`.

**PyYAML** — Cython generates `_yaml.c`, which links the
libyaml built by the native dependency system. It selects between its pure and
compiled implementations through its own `PYYAML_FORCE_CYTHON` and
`PYYAML_FORCE_LIBYAML` variables, which the recipe sets through `buildEnv` —
build configuration, which belongs in a recipe, as distinct from compiler
flags, which never do. The test asserts `yaml.CSafeLoader` parsed the document,
which only exists when the extension built.

**pydantic-core** — Rust, cross compiled with a rebuilt `std` and wasm
exceptions. Two versions, because pydantic pins its core exactly and resolving
that pin is part of what the index has to get right.

**simplejson** — recorded for what it shows about policy rather than about
building. It publishes both C-accelerated platform wheels and a pure
`py3-none-any` fallback, and the resolver takes the pure one. That is correct:
a pure wheel is portable and carries no ABI risk. The limitation is real and
worth stating plainly — **a package that publishes a pure fallback cannot
currently be accelerated by supplying a compiled wheel, because the pure wheel
always wins**. Changing that is a policy decision, not an oversight, so no
compiled simplejson wheel is shipped.

**siphash24** — the densest case, and the only one that needed a patch. Three
things at once: a frontend that is not setuptools (meson-python, driven with a
cross file generated from the ABI contract); two dependencies resolved through
Meson `wrap` files, which Meson satisfies by cloning from git *during the
build* — disabled here, with the subprojects supplied instead as pinned,
hash-verified archives recorded in the wheel's provenance; and a patch, because
one of those subprojects builds its library with `both_libraries`, which always
produces a shared library. `ld.wasm` has none, and Meson refuses at configure
time rather than at link time, so the project was unconfigurable until the
library was made static. The test asserts the first published SipHash-2-4
reference vector, which comes from outside this repository.

**numpy** — the case this pipeline was built toward, and deliberately the last
one attempted, so that it drove none of the design. It needed **no patch** and
no mechanism that did not already exist for smaller packages: Meson through
meson-python, a cross file generated from the ABI contract, pinned build tools,
and its own documented `allow-noblas` switch for a platform with no BLAS. It
built on the first attempt.

Its assertions are numerical rather than structural. NumPy has no pure-Python
fallback to hide behind, but a miscompiled one imports perfectly well and
computes wrong answers, so each check has an exactly known result: `a @ a.T`,
an inverse multiplied back to the identity, a round-trip FFT, a reduction.
`long double` deserves naming — Meson cannot run a program on the target to
learn its layout, so the value is derived from the pinned compiler's own
`__LDBL_MANT_DIG__` (113, little-endian, so `IEEE_QUAD_LE`) and the built
extension reports the matching 16-byte itemsize. Getting that wrong would have
been silently wrong arithmetic rather than a build failure.

NumPy's *own* test suite runs inside the runtime, which is the strongest
evidence in this document because none of it was written here:

| module | result |
| --- | --- |
| `numpy._core.tests.test_umath` | 4388 passed, 362 skipped, 2 xfailed, 5 xpassed |
| `numpy.linalg.tests.test_linalg` | 416 passed, 21 skipped, 2 xfailed |
| `numpy.fft.tests.test_pocketfft` | 144 passed, 4 skipped |

4,948 upstream tests pass and none fails. Reproduce with `pip install numpy
pytest hypothesis` in a container, then:

```bash
python3 -m pytest --pyargs numpy._core.tests.test_umath -q --capture=no
```

`--capture=no` is required, and the reason is a runtime limitation rather than
anything to do with NumPy: pytest's default output capture opens a temporary
file descriptor and reading it back fails with `OSError: [Errno 8] Bad file
descriptor`, so *no* tests run at all. It belongs with the other pre-existing
process- and descriptor-level gaps recorded in the project state, not with
package support.

**psutil** — attempted deliberately as a negative case, and the failure is more
useful than the success would have been.

## The psutil finding

Command: `python3 scripts/build_extension.py recipes/psutil/recipe.json`
Source: `psutil-6.1.0.tar.gz`, sha256 `353815f5…c7a`

Earliest unsupported assumption, quoted from the build:

```
emcc … -DPSUTIL_POSIX=1 -DPSUTIL_OSX=1 … -c psutil/_psutil_common.c
psutil/_psutil_common.h:131:14: fatal error: 'mach/mach_time.h' file not found
```

`-DPSUTIL_OSX=1` is the whole finding. `sysconfig` has been pointed at the
target throughout, so the compiler, its flags and the extension suffix are all
correct — but psutil's `setup.py` selects its platform implementation from
`sys.platform`, which reports the *running* interpreter's operating system.
Under a cross build that is the build machine, so on macOS it compiled the
Darwin backend and asked for a Mach header.

This is assumption 7 in `cross-build.md` in its most common concrete form, and
it generalises well beyond psutil: any package that branches on `sys.platform`
in its build will describe the build machine. It is worth recording that
`_PYTHON_HOST_PLATFORM` does **not** fix this — that variable changes
`sysconfig.get_platform()`, not `sys.platform`.

psutil is nevertheless classified `blocked-platform-capability` rather than
`supported-patched`. A patch could make it select a POSIX backend, but psutil
reads process and system tables through syscalls Emscripten does not implement;
the capability is absent from the platform, not from the build. Patching it
would convert a clear build failure into a runtime one, which is the trade this
project refuses everywhere else.

## Cohorts

All eight cohorts now have a real package. Cohorts 6 (a package requiring a
visible cross-compilation patch) and 7 (a PEP 517 backend that is not
setuptools) are both covered by siphash24; cohort 8 by NumPy.

## Packages with no recipe

Everything above has a recipe checked in, because each was measured
deliberately. Most packages need none: `pip install <anything>` will generate
one, build the wheel and install it, when a builder is enabled — see
`build-on-miss.md`. `ujson` is the worked example, built with no recipe, no
patch and no hand-pinned tool:

```
$ pip install ujson
Collecting ujson
Building ujson from source (no wheel for this runtime yet)
Built ujson
Successfully installed ujson-6.0.0
```

A package that cannot be built this way still lands in the vocabulary above
rather than as an opaque failure: a backend with no adapter is
`blocked-toolchain` and says which backend, and a package with no source
distribution says that instead.

## Reproducing

```bash
make -C python-runtime native-deps
python3 python-runtime/scripts/build_extension.py python-runtime/recipes/<name>/recipe.json
python3 python-runtime/scripts/build_index.py
npx vitest run test/python-runtime/package-cohorts.test.ts
```

Every wheel above rebuilds byte-identically; the digests in this table are
therefore checkable rather than decorative.
