From f717d8574a0cb195c4e05d3b55961dfec6014c36 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 26 Jun 2026 16:12:45 -0700 Subject: [PATCH 1/2] fix(android): bundle every ABI's _sysconfigdata into the ABI-common stdlib.zip The pure stdlib.zip is built once from the primary ABI (abis.first(), e.g. arm64-v8a), but _sysconfigdata__ is arch-specific: each ABI ships its own (e.g. _sysconfigdata__android_x86_64-linux-android) and CPython imports the one matching the running device at startup (sysconfig, pulled in by ctypes). So on a non-primary ABI (e.g. an x86_64 emulator) the embedded interpreter crashed with 'ModuleNotFoundError: No module named _sysconfigdata__android_x86_64-linux-android'. The primary splitStdlib task now also harvests every other ABI's stdlib/_sysconfigdata* from its libpythonbundle.so into stdlib.zip (depending on the other ABIs' untar and holding non-primary tasks until the primary has read their bundles). --- .../android/build.gradle.kts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/serious_python_android/android/build.gradle.kts b/src/serious_python_android/android/build.gradle.kts index dc41a973..791aff7d 100644 --- a/src/serious_python_android/android/build.gradle.kts +++ b/src/serious_python_android/android/build.gradle.kts @@ -259,6 +259,16 @@ for (abi in abis) { // (+ .soref markers in stdlib.zip), stdlib/* -> stdlib.zip root, then delete it. tasks.register("splitStdlib_$abi") { dependsOn("untarFile_$abi") + // The ABI-common stdlib.zip is built once (from the primary ABI) but must + // also carry every OTHER ABI's arch-specific `_sysconfigdata__` (see + // the harvest in doLast). Make the primary task depend on the other ABIs' + // untar so their bundles exist to read, and hold non-primary tasks until + // the primary has read them (each task deletes its own bundle at the end). + if (isPrimary) { + abis.forEach { other -> if (other != abi) dependsOn("untarFile_$other") } + } else { + mustRunAfter("splitStdlib_$primaryAbi") + } // The doLast rewrites jniLibs/ (mangled libs in, bundle out); declare it as a // tracked output and always re-run so AGP's native-libs merge re-packages. outputs.dir(jniDir) @@ -292,6 +302,33 @@ for (abi in abis) { } } } + // `_sysconfigdata__` is the one pure-stdlib module whose name and + // contents are ARCH-SPECIFIC: each ABI ships its own (e.g. + // `_sysconfigdata__android_x86_64-linux-android`) and CPython imports + // the one matching the running device at startup (sysconfig, pulled in + // by ctypes). Since stdlib.zip is ABI-common and built from the primary + // ABI only, harvest every other ABI's sysconfigdata into it — otherwise + // a non-primary ABI (e.g. an x86_64 emulator when arm64-v8a is primary) + // crashes with `ModuleNotFoundError: _sysconfigdata__android_...`. + if (isPrimary) { + abis.filter { it != abi }.forEach { otherAbi -> + val otherBundle = File(file("src/main/jniLibs/$otherAbi"), "libpythonbundle.so") + if (otherBundle.exists()) { + ZipFile(otherBundle).use { ozf -> + val oen = ozf.entries() + while (oen.hasMoreElements()) { + val oe = oen.nextElement() + if (!oe.isDirectory && oe.name.startsWith("stdlib/_sysconfigdata")) { + zip?.add( + oe.name.removePrefix("stdlib/"), + ozf.getInputStream(oe).readBytes(), + ) + } + } + } + } + } + } zip?.add("_sp_bootstrap.py", bootstrapPy.readBytes()) // finder at zip root // The dart-bridge Android shim (F) installs the finder before `site`. A // sitecustomize fallback can be re-enabled for bridges without that shim: From c64b74adcf62b380e7c5cc52c03ec9dfb12a4a78 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 26 Jun 2026 17:06:58 -0700 Subject: [PATCH 2/2] fix(android): harvest only arch-specific _sysconfigdata__android (avoid dup) The previous harvest matched stdlib/_sysconfigdata*, which also catches the generic, ABI-identical _sysconfigdata__linux_ that some Python versions (e.g. 3.12) ship in every ABI. The primary ABI already adds that via the stdlib loop, so re-adding it from a non-primary ABI threw 'java.util.zip.ZipException: duplicate entry: _sysconfigdata__linux_.pyc'. Match only the per-ABI _sysconfigdata__android_ modules (unique per ABI), which is exactly what CPython imports on-device. Add CHANGELOG entry (4.1.1). --- src/serious_python_android/CHANGELOG.md | 4 ++++ .../android/build.gradle.kts | 22 ++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/serious_python_android/CHANGELOG.md b/src/serious_python_android/CHANGELOG.md index 54eafbf5..e8022c7a 100644 --- a/src/serious_python_android/CHANGELOG.md +++ b/src/serious_python_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.1.1 + +* Fix the embedded interpreter crashing on startup on a **non-primary ABI** (e.g. an x86_64 emulator when `arm64-v8a` is the primary ABI) with `ModuleNotFoundError: No module named '_sysconfigdata__android_-linux-android'`. The ABI-common `stdlib.zip` was built from the primary ABI only, dropping every other ABI's arch-specific `_sysconfigdata__android_` module — which CPython imports at startup via `sysconfig` (pulled in by `ctypes`). The primary `splitStdlib` task now also harvests each other ABI's `_sysconfigdata__android_` into `stdlib.zip` (only that arch-specific module, so the generic ABI-identical `_sysconfigdata__linux_` shipped by some versions like 3.12 isn't duplicated). + ## 4.1.0 * Run the `extractAsset` / `unzipAsset` / `loadLibrary` method-channel handlers on a background `Executor` (posting the `Result` back on the main looper) instead of inline on the platform main thread. The first-launch asset unpack and the pyjnius native-library load no longer block Android's `Choreographer`, so Flutter's vsync isn't starved and on-screen animations (e.g. a boot/splash spinner) stay smooth while the app starts. diff --git a/src/serious_python_android/android/build.gradle.kts b/src/serious_python_android/android/build.gradle.kts index 791aff7d..ba6e279f 100644 --- a/src/serious_python_android/android/build.gradle.kts +++ b/src/serious_python_android/android/build.gradle.kts @@ -302,14 +302,18 @@ for (abi in abis) { } } } - // `_sysconfigdata__` is the one pure-stdlib module whose name and - // contents are ARCH-SPECIFIC: each ABI ships its own (e.g. - // `_sysconfigdata__android_x86_64-linux-android`) and CPython imports - // the one matching the running device at startup (sysconfig, pulled in - // by ctypes). Since stdlib.zip is ABI-common and built from the primary - // ABI only, harvest every other ABI's sysconfigdata into it — otherwise - // a non-primary ABI (e.g. an x86_64 emulator when arm64-v8a is primary) + // `_sysconfigdata__android_` is ARCH-SPECIFIC: each ABI ships its + // own (e.g. `_sysconfigdata__android_x86_64-linux-android`) and CPython + // imports the one matching the running device at startup (sysconfig, + // pulled in by ctypes). Since stdlib.zip is ABI-common and built from the + // primary ABI only, harvest every other ABI's into it — otherwise a + // non-primary ABI (e.g. an x86_64 emulator when arm64-v8a is primary) // crashes with `ModuleNotFoundError: _sysconfigdata__android_...`. + // + // Match ONLY the `_sysconfigdata__android_` files (unique per ABI), + // not the generic, ABI-identical `_sysconfigdata__linux_` that some + // versions (e.g. 3.12) also ship — the primary already added that via the + // stdlib loop above, so harvesting it again would be a duplicate zip entry. if (isPrimary) { abis.filter { it != abi }.forEach { otherAbi -> val otherBundle = File(file("src/main/jniLibs/$otherAbi"), "libpythonbundle.so") @@ -318,7 +322,9 @@ for (abi in abis) { val oen = ozf.entries() while (oen.hasMoreElements()) { val oe = oen.nextElement() - if (!oe.isDirectory && oe.name.startsWith("stdlib/_sysconfigdata")) { + if (!oe.isDirectory && + oe.name.startsWith("stdlib/_sysconfigdata__android") + ) { zip?.add( oe.name.removePrefix("stdlib/"), ozf.getInputStream(oe).readBytes(),