From aa969a8c1eb94f0767651b7af57f02e6738a1b9b Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 2 Jun 2026 17:50:15 -0700 Subject: [PATCH] Fix Android NDK relocation for Python 3.13+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mobile-forge sysconfig relocation block was a no-op on macOS for 3.13/3.14 builds because two related bugs combined to leave `_build_ndk` empty and—even when set—matched the wrong substring. 1. android/build.sh: the existing `$toolchain` fallbacks only fired for the 3.12 build path (which sources `android-env.sh`, setting `$toolchain`) or when `$HOME/ndk/r27d` exists. For 3.13+, CPython's in-tree Android tooling resolves the NDK under `$ANDROID_HOME/ndk/$cpython_ndk_version/` without ever exporting `NDK_HOME`, so `$toolchain` stayed empty and `normalize_mobile_forge_install.py --ndk-toolchain ""` baked `_build_ndk = ''` into the sysconfig. Add a third fallback that reads the NDK location CPython actually used. 2. android/normalize_mobile_forge_install.py: the relocation block did install-prefix substitution before NDK substitution. For 3.13+ the CI's NDK lives under `/usr/local/lib/android/sdk/ndk/...`, which is inside one of `_install_prefixes` — so the prefix rule rewrote the NDK path string first, leaving nothing for the NDK rule to match. Reorder so NDK substitution runs first. Symptom on macOS: `forge android:arm64-v8a ` crashed in crossenv with "Cannot find cross-compiler" pointing at `/lib/android/sdk/ndk/.../prebuilt/linux-x86_64/bin/...` — binaries that aren't shipped (the tarball only contains the host the build ran on) and the wrong host slot for macOS anyway. After both fixes, the relocation block finds the user's local NDK (via `~/Library/Android/sdk/ndk/`, `NDK_HOME`, etc.) and rewrites CC/CXX/AR/STRIP/RANLIB to the matching `darwin-x86_64` toolchain. 3.12 was unaffected because its build path sourced `android-env.sh` (setting `$toolchain`) and the CI NDK lived under `~/ndk/r27d/...`, which is not inside `_install_prefixes`. --- android/build.sh | 11 +++++++++++ android/normalize_mobile_forge_install.py | 11 +++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/android/build.sh b/android/build.sh index 478e01f..a6c1518 100755 --- a/android/build.sh +++ b/android/build.sh @@ -192,6 +192,17 @@ fi if [ -z "${toolchain:-}" ] && [ -n "${NDK_HOME:-}" ]; then toolchain=$(echo "$NDK_HOME"/toolchains/llvm/prebuilt/*) fi +# 3.13+: CPython's in-tree Android tooling auto-resolves the NDK under +# $ANDROID_HOME/ndk/$cpython_ndk_version/ without ever exporting NDK_HOME, +# so neither of the branches above fires. Walk that path so normalize sees +# a non-empty build-time toolchain and the relocation block can substitute +# on consumer hosts. +if [ -z "${toolchain:-}" ] && [ -n "${ANDROID_HOME:-}" ] && [ -n "${cpython_ndk_version:-}" ]; then + candidate="$ANDROID_HOME/ndk/$cpython_ndk_version/toolchains/llvm/prebuilt" + if [ -d "$candidate" ]; then + toolchain=$(echo "$candidate"/*) + fi +fi python3 "$script_dir/normalize_mobile_forge_install.py" "$PREFIX" \ --ndk-toolchain "${toolchain:-}" diff --git a/android/normalize_mobile_forge_install.py b/android/normalize_mobile_forge_install.py index f2db369..bc0ca51 100644 --- a/android/normalize_mobile_forge_install.py +++ b/android/normalize_mobile_forge_install.py @@ -107,10 +107,17 @@ def _local_toolchain(): for _key, _value in tuple(build_time_vars.items()): if not isinstance(_value, str): continue - for _old_prefix in _install_prefixes: - _value = _value.replace(_old_prefix, _prefix) + # NDK substitution must run before install-prefix substitution: when + # the build-time NDK lives under one of `_install_prefixes` (e.g. the + # GitHub runner places NDK under `/usr/local/lib/android/sdk/ndk/...`), + # rewriting the prefix first would mangle the NDK string and leave + # nothing for the NDK rule to match. Swapping order keeps both rules + # independent: NDK fully resolves to the local toolchain, then any + # remaining install-prefix references get re-anchored. if _build_ndk and _local_ndk: _value = _value.replace(_build_ndk, _local_ndk) + for _old_prefix in _install_prefixes: + _value = _value.replace(_old_prefix, _prefix) build_time_vars[_key] = _value