Skip to content

Source-build darwin/iOS Python + Android dep-wheel layout for 3.13+#10

Merged
FeodorFitsner merged 10 commits into
mainfrom
darwin-std-pkg
Jun 3, 2026
Merged

Source-build darwin/iOS Python + Android dep-wheel layout for 3.13+#10
FeodorFitsner merged 10 commits into
mainfrom
darwin-std-pkg

Conversation

@FeodorFitsner
Copy link
Copy Markdown
Contributor

Summary

Two largely independent strands rolled into one branch:

  1. Source-build Python for darwin / iOS across 3.12 / 3.13 / 3.14 (replacing the previous beeware-derived prebuilt-tarball flow), driven by new darwin/build_ios.py + darwin/build_macos.py and a set of vendored CPython patches. This was the original motivation for the darwin-std-pkg branch.
  2. Android dep-wheel layout + build-details.json rewrite for 3.13+ so mobile-forge's make_dep_wheels.py can produce Android wheels for native deps (openssl, bzip2, libffi, xz, sqlite) on the official CPython Android tooling path, and so maturin's linker actually finds libpython3.so on Mac developer machines.

Highlights

darwin / iOS source-build flow

  • darwin/build_ios.py + darwin/build_macos.py — new entry points for the darwin-side build, replacing the beeware Python-Apple-support recipe path on 3.12 and adding a fresh source-build flow for 3.13 / 3.14. Emits install/ + support/ trees in the layout mobile-forge expects.
  • darwin/ios_patches/3.12/ and darwin/ios_patches/3.13/ — CPython patches needed to build for iOS from source on each version.
  • darwin/macos_support/app-store-compliance.patch — App Store compliance fixup for the macOS framework.
  • darwin/package-{ios,macos}-for-dart.sh — refresh the dart-side packaging to match the new layout.

Android dep-wheel layout (3.13+)

  • android/extract_mobile_forge_deps.py (new) + android/build.sh (wires it in) — CPython's official Android tooling drops OpenSSL / bzip2 / libffi / xz / sqlite intermixed inside python-<ver>/lib/. mobile-forge expects them as sibling install/android/<abi>/<lib>-<ver>-<N>/ install trees (the 3.12 layout). The new script reorganizes the post-install tree to match, detecting each lib's version from headers / pkgconfig, copying the right files, and appending <lib>: <ver>-<N> entries to support/<X.Y>/android/VERSIONS. Build numbers start at 1 per lib for 3.13+, independent of the 3.12 counters.
  • android/normalize_mobile_forge_install.py — adds rewrite_build_details_json so the build-details.json shipped on 3.14 has its absolute paths re-anchored at the consumer's install prefix. Uses the JSON's own base_prefix field as the substitution source rather than hard-coding /usr/local, matching the narrow-prefix discipline Fix sysconfig relocator mangling NDK paths under /usr/local #9 introduced for the sysconfig relocator. Without this, maturin reads libpython.dynamic_stableabi = /usr/local/lib/libpython3.14.so straight through and the consumer linker fails with unable to find library -lpython3.

Test framework follow-ups

  • android/tests/test_built_build_details_json.py (new) — @post_build, @requires_python("3.14"). Asserts every shipped build-details.json has its paths anchored at the install prefix, that no /usr/local strings leak through, and that libpython.dynamic_stableabi points at a file that exists on disk.

CI / workflow

  • .github/workflows/build-python-version.ymlmacos-26 runner (needed for the newer Xcode required by 3.14 iOS source-build) and the matrix expansion across 3.12 / 3.13 / 3.14 for darwin. De-duplicated if: key on the publish-release job from the merge with main.

Merge of main

Includes upstream changes from main (PR #8: NDK relocation fix, PR #9: relocator narrowing + Android test framework). My rewrite_build_details_json adapted to follow the narrow-substitution model #9 introduced.

Test plan

  • 3.14 Android arm64-v8a: mobile-forge consumes the new dep-wheel layout and successfully builds cryptography end-to-end on macOS.
  • Local re-normalize of an already-built 3.14 tree: rewrite_build_details_json is idempotent.
  • android/tests/test_built_build_details_json.py passes against a real built tree.
  • Re-run the CI matrix on this branch and confirm post-build tests pass on all three Python versions × ABIs.
  • mobile-forge consumer side: 3.13 Android arm64-v8a build of any Rust-bound recipe (parallel mobile-forge PR).

🤖 Generated with Claude Code

FeodorFitsner and others added 10 commits June 2, 2026 13:15
…3/3.13.13/3.14.5

Move iOS/macOS off the beeware Python-Apple-support repo onto CPython's standard
build mechanism, and stop hand-pinning the Linux python-build-standalone release.

macOS (build_macos.py): universal2 Python.framework built from source for all
versions. OpenSSL + xz (+ zstd on 3.14) built universal2 from source (the macOS SDK
ships neither OpenSSL nor lzma.h); OpenSSL is shared and bundled into the framework
with @rpath install names, matching the released layout; binaries stripped.

iOS (build_ios.py): one unified path using CPython's in-tree `Apple build iOS` tool.
3.14 is native; 3.13/3.12 apply a vendored back-port patch (ios_patches/) that adds the
Apple tooling (+ the PEP 730 runtime for 3.12) -- no dependency on the
Python-Apple-support repo. Reshapes the cross-build output into both the dart bundle and
the mobile-forge install/support tree (per-arch dep dirs, VERSIONS, bin stub,
platform-config sysconfig), and embeds the iOS deployment version into HOST_GNU_TYPE so
mobile-forge's crossenv resolves the iOS release.

Linux (resolve_pbs.py): auto-resolve the newest python-build-standalone release for the
target micro+arch (PYTHON_DIST_RELEASE kept as optional override); keep x86_64_v2.

Workflows: bump matrix to 3.12.13/3.13.13/3.14.5, darwin runner -> macos-26, and only
publish GitHub release assets from the main branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
serious_python loads the stdlib from python-stdlib/ and C extensions from
python-xcframeworks/*.fwork, so the full un-pruned stdlib carried inside the iOS
Python.xcframework (lib/python3.X, ~200 MB incl. the CPython test suite) and the
per-slice lib-dynload/*.so were dead weight in shipped apps. Drop them in
package-ios-for-dart.sh: the iOS dart artifact goes from ~89 MB to ~22 MB compressed
(308 MB -> 70 MB extracted), with all runtime essentials intact.

Also extend the per-platform stdlib excludes (darwin/android/linux/windows) to drop a
few more never-needed items: lib2to3 (deprecated; removed upstream in 3.13+), the
curses C extensions (the curses package is already excluded), and xxsubtype.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Apple build tool leaves the iOS framework binary and extension modules unstripped.
Strip local symbols (strip -x, keeps exported PyInit_/C-API symbols needed for dynamic
linking) in build_ios.py's reshape, before they become python-xcframeworks. Xcode re-signs
on embed. ~4 MB off the extracted iOS dart artifact (70 -> 66 MB).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Running an app in embedded mode never enters the interactive REPL, so the REPL/dev-only
modules are dead weight. Confirmed via import trace (a plain script imports none of them;
site.py only touches _pyrepl/rlcompleter inside the interactive hook). Remove from all
platform excludes: _pyrepl (~300 KB), rlcompleter, tabnanny, the easter eggs this/
antigravity, and the frozen demo modules __hello__/__phello__.

Deliberately kept (a library or breakpoint() may import them): unittest (mock), pdb/bdb,
pydoc, doctest, code/codeop, multiprocessing, venv.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CPython's official Android tooling, used by python-build's 3.13+ build
path, builds OpenSSL/bzip2/libffi/xz/SQLite as part of the host build
and installs them intermixed under $PREFIX/{include,lib}/. mobile-forge
make_dep_wheels.py expects each as a sibling per-lib install dir with
its own include/+lib/ — which is what python-build's 3.12 path
produces (by downloading separate prebuilt tarballs).

For 3.13/3.14 today, the sibling install dirs don't exist and the
generated support/<X.Y>/android/VERSIONS file lists no deps, so
make_dep_wheels.py emits zero Android dep wheels. Recipes with host
requirements like `openssl>=3.0.12` (cryptography) then fail at
`pip install --only-binary=:all:` with "No matching distribution found
for openssl".

Add extract_mobile_forge_deps.py: after the CPython host build copy,
detect each bundled lib's version from its header/pkgconfig (or the
known-pinned value for bzip2), materialize a sibling install dir at
install/android/<abi>/<lib>-<ver>-<N>/{include,lib}/, and append a
corresponding `<lib>: <ver>-<N>` entry to VERSIONS. Build number
starts at 1 for each dep, independent of the 3.12 counter.

Idempotent — re-running replaces sibling dirs and de-duplicates VERSIONS
entries so a tree can be re-extracted in place without manual cleanup.

After this lands, `source ./setup.sh 3.13.13` will produce Android dep
wheels for arm64-v8a + x86_64, and recipes that host-depend on
openssl/bzip2/libffi/xz/sqlite resolve normally.
CPython 3.14's Android tooling emits `lib/python<X.Y>/build-details.json`
that downstream cross-compile tooling (notably `maturin`) reads to
decide where libpython lives, where the headers are, and which
pkg-config dir to consult. The values are absolute build-time paths
rooted at `/usr/local`, so on every consumer machine the file claims
libpython sits at `/usr/local/lib/libpython3.14.so` — which is empty.
That makes `maturin` add `-L/usr/local/lib` and the linker fails with
`unable to find library -lpython3` even when libpython is sitting right
there at the install's actual `lib/`.

Add a one-shot rewrite to `normalize_mobile_forge_install.py` so the
JSON gets re-anchored to the install path at the same time we append
the sysconfigdata relocation block. Idempotent (no `/usr/local`
remaining after first pass).

3.13 is unaffected because it doesn't ship build-details.json.
# Conflicts:
#	.github/workflows/build-python-version.yml
`rewrite_build_details_json` hard-coded `/usr/local` as the build-time
prefix to substitute. That works for the only consumer schema we ship
today (3.14's CPython Android tooling installs to /usr/local), but it
violates the narrow-substitution discipline #9 introduced for the
sysconfig relocator: an upstream CPython change that moves the install
root to anything else would silently miss the rewrite, and unrelated
strings under /usr/local that future schema fields might carry would
get clobbered.

Switch the substitution source to the JSON's own `base_prefix` field.
That field is the canonical record of where Python was installed at
build time — by construction every other absolute path in the file
shares it as a prefix, and reading it from the data instead of from
this script's constants means CPython tooling drift gets handled
automatically.

Idempotent for the same reason as before: once `base_prefix` has been
overwritten with the consumer's install path, the build-time string
no longer appears anywhere in the file. The early-return on
`build_time_prefix == prefix_str` makes that explicit so the function
can be safely re-invoked on an already-rewritten tree.

Adds android/tests/test_built_build_details_json.py covering:

  - at least one build-details.json is shipped per ABI (catches the
    function getting dropped from main() or CPython silently stopping
    emission of the file)
  - no `/usr/local` strings survive (narrower than the
    anchored-at-prefix check below, but targeted at the historical
    pre-fix shape)
  - every documented absolute-path field is anchored at the install
    prefix (catches partial rewrites and build-time-prefix changes)
  - libpython.dynamic_stableabi points at a file that exists on disk
    (the linker `-L` target — catches the tarball shipping with the
    path right but the file missing)

Gated `@requires_python("3.14")` because 3.13 and 3.12 don't ship
build-details.json (CPython's Android tooling added it in 3.14).
Merge of origin/main into darwin-std-pkg landed two copies of the
`if: github.ref == 'refs/heads/main'` guard on the publish-release job:
one above `needs:` and one below it. YAML mappings can't have duplicate
keys, so GitHub Actions rejected the workflow at parse time:

  Invalid workflow file: .github/workflows/build-python-version.yml#L1
  (Line: 204, Col: 5): 'if' is already defined

Both copies were the same condition (added independently on each side of
the merge), so removing the second one is purely syntactic — no behavior
change.
@FeodorFitsner FeodorFitsner merged commit 923bfec into main Jun 3, 2026
31 checks passed
@FeodorFitsner FeodorFitsner deleted the darwin-std-pkg branch June 3, 2026 22:24
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.

1 participant