Skip to content

Add support for building Android wheels #2349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 56 commits into
base: main
Choose a base branch
from
Open

Conversation

mhsmith
Copy link
Contributor

@mhsmith mhsmith commented Apr 4, 2025

The corresponding CPython PR, from which the Android Python releases in build-platforms.toml are being generated, is python/cpython#132870.

@henryiii
Copy link
Contributor

Let us know if you need (the rest of) CI triggered. :)

@mhsmith
Copy link
Contributor Author

mhsmith commented May 4, 2025

@henryiii: This is close to being complete, so please enable the rest of CI.

@mhsmith mhsmith marked this pull request as ready for review May 6, 2025 20:21
@mhsmith
Copy link
Contributor Author

mhsmith commented May 6, 2025

To run the integration tests on most of the CI machines, it looks like I'll need to automate installation of the correct Python version. For macOS this can be done the same way as iOS, but for Linux there's no existing code to reuse, because the native Linux build uses Docker. So I'll probably implement something that uses python-build-standalone, unless anyone has another suggestion.

Apart from that, I think this PR is complete enough now that it's worth reviewing. @freakboy3742 and anyone else who's interested.

@henryiii
Copy link
Contributor

henryiii commented May 6, 2025

python-build-standalone is fine, in fact, I'd like to use that for pyodide in the future, too.

@joerick
Copy link
Contributor

joerick commented May 7, 2025

I've already written code to install a version of python-build-standalone in #2002, along with version pinning etc. I think it would work nicely here too.

Copy link
Contributor

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments inline; my two high level concerns are:

  1. Whether the Builder class actually delivers any benefit here; and
  2. The general approach around avoiding python -m build in order to get platform-specific build requirements.



@dataclass
class Builder:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say +1, for the sake of consistency with the other platforms, and passing around lots of variables (while it's a bit of a drag) prevents other classes of bugs in its explicitness.

@henryiii
Copy link
Contributor

henryiii commented Jun 5, 2025

Is xbuild-tools not used/needed on Android? (I know we also don't use it currently for pyodide)

@henryiii
Copy link
Contributor

henryiii commented Jun 5, 2025

(I see you've been using merge commits, so I forced myself to merge with main instead of rebasing on it... I needed the recent changes in main to test the pyodide-build fix in my downstream PR, and it's still based on this PR)

We will aim to make this will be the first feature of 3.1, 3.0 is shipping soon and @joerick won't have time to review before 3.0 (and I want to get at least pybind11 working with it, which also will take time).

@mhsmith
Copy link
Contributor Author

mhsmith commented Jun 5, 2025

Is xbuild-tools not used/needed on Android? (I know we also don't use it currently for pyodide)

I think it was only necessary on iOS because when building for the simulator, you're really building for macOS, so when a normal macOS tool is used by accident, the build might continue for some time until eventually failing in a way which doesn't clearly indicate the cause. But Android isn't binary-compatible with either Linux or macOS, so when you use the wrong tool you usually find out immediately.

@mayeut
Copy link
Member

mayeut commented Jun 8, 2025

I wanted to play a bit with this before reviewing (not sure when I'll have time to review it properly).
I ran in kind of the same issues as @henryiii as I'm using a mixture of direct CMake calls to build a native library (issues) and setuptools to build the extension itself (no issues there with the current implementation).

The easiest solution to that is probably for cibuildwheel to set the CMAKE_TOOLCHAIN_FILE environment variable. The toolchain file should be pretty short, since almost everything is already specified by other environment variables which CMake recognizes.

I think there's one thing missing in exported environment variables to create such a toolchain file (or just passing the correct arguments to CMake): the Android NDK path. It can be inferred from the compiler path (in CC) but that seems a bit hacky.

Here are the extra arguments I had to pass to CMake (the last one being specific to my project):

    elif is_android:
        _, level, arch = platform.split("-")
        if arch.startswith("arm"):
            arch = arch.replace("_", "-")
        cc = Path(os.getenv("CC", sysconfig.get_config_var("CC"))).resolve(strict=True)
        android_home_env = os.getenv("ANDROID_HOME")
        if android_home_env is None:
            msg = "The ANDROID_HOME environment variable is required."
            raise ValueError(msg)
        android_home = Path(android_home_env)
        android_ndk_root = android_home / "/".join(cc.relative_to(android_home).parts[:2])
        extra_config.append("-DCMAKE_SYSTEM_NAME=Android")
        extra_config.append(f"-DCMAKE_SYSTEM_VERSION={level}")
        extra_config.append(f"-DCMAKE_ANDROID_ARCH_ABI={arch}")
        extra_config.append(f"-DCMAKE_ANDROID_NDK={android_ndk_root}")
        extra_config.append("-DCMAKE_ANDROID_STL_TYPE=none")

CMake (and maybe other tools) will read the Android NDK path from ANDROID_NDK_ROOT or ANDROID_NDK if not specified otherwise.
I'll need to check for cross-build before doing this in my backend customization. I guess building natively in termux is possible for example.

Should this be handled by the build backends or should it be cibuildwheel responsibility to create the toolchain file & set the CMAKE_TOOLCHAIN_FILE environment variable (which would be a new thing, so far, e.g. on macOS where it's the backend responsibility to pass the correct CMAKE_OSX_ARCHITECTURES) ?

@mhsmith
Copy link
Contributor Author

mhsmith commented Jun 11, 2025

@freakboy3742 FYI: First instance I've seen of "Messages dropped during live streaming" breaking the cibuildwheel iOS tests: https://github.com/pypa/cibuildwheel/actions/runs/15564982650/job/43826807636

@mhsmith
Copy link
Contributor Author

mhsmith commented Jun 11, 2025

should it be cibuildwheel responsibility to create the toolchain file & set the CMAKE_TOOLCHAIN_FILE environment variable

I've now added a minimal implementation of that to this PR.

I think there's one thing missing in exported environment variables to create such a toolchain file (or just passing the correct arguments to CMake): the Android NDK path.

CMake comes with a large and complex body of code to determine the paths and flags to use when calling NDK tools. However, this is unnecessary in cibuildwheel, because we've already set those things in environment variables.

So to avoid confusion, the toolchain file calls set(CMAKE_SYSTEM_VERSION 1), which CMake interprets as "disable NDK auto-detection, the toolchain file is handling everything". This is the same approach used by the toolchain file of the Android Gradle plugin, which has also found CMake's built-in NDK support to be too difficult to adopt so far.

I'm currently still working through all the issues uncovered by pybind11, but I'll look at pybase64 after that.

@joerick joerick removed the Hold for future release This PR might be complete, but is scheduled to be merged in a future release. Don't merge yet. label Jun 11, 2025
@mayeut
Copy link
Member

mayeut commented Jun 11, 2025

I've now added a minimal implementation of that to this PR.

Thanks for your detailed answer about CMake handling @mhsmith. Everything works out of the box with the toolchain file injection added.

@mhsmith
Copy link
Contributor Author

mhsmith commented Jun 11, 2025

OK, I have the pybind11 tests passing now with the code in henryiii/pybind11#23.

On Android the OS doesn't provide a C++ library, so every app needs to bundle its own copy. This required giving cibuildwheel some auditwheel-like code which adds the C++ library to the wheel in the repair step if necessary.

Eventually Android support should be added to auditwheel itself, but I can see that would be a substantial amount of work.

@joerick
Copy link
Contributor

joerick commented Jun 13, 2025

I'm trying this out locally on pyinstrument. Building works, but I'm hitting an error running tests. Here's the output

log
Testing wheel...

+ python -c 'import sysconfig; print(sysconfig.get_platform(), end="")'
+ pip install --only-binary=:all: --platform android_21_arm64_v8a --target /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/site-packages /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/repaired_wheel/pyinstrument-5.0.2-cp313-cp313-android_21_arm64_v8a.whl
Processing /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/repaired_wheel/pyinstrument-5.0.2-cp313-cp313-android_21_arm64_v8a.whl
Installing collected packages: pyinstrument
Successfully installed pyinstrument-5.0.2
+ /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/python/android.py test --managed maxVersion --site-packages /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/site-packages --cwd /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/cwd -c 'import pyinstrument' --
> curl -Lf --retry 5 --retry-all-errors -o /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/python/testbed/gradlew https://raw.githubusercontent.com/gradle/gradle/v8.9.0/gradlew
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  8784  100  8784    0     0   128k      0 --:--:-- --:--:-- --:--:--  129k
> curl -Lf --retry 5 --retry-all-errors -o /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/python/testbed/gradlew.bat https://raw.githubusercontent.com/gradle/gradle/v8.9.0/gradlew.bat
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2894  100  2894    0     0  43655      0 --:--:-- --:--:-- --:--:-- 43848
> curl -Lf --retry 5 --retry-all-errors -o /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/python/testbed/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/gradle/gradle/v8.9.0/gradle/wrapper/gradle-wrapper.jar
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 43504  100 43504    0     0   505k      0 --:--:-- --:--:-- --:--:--  505k
Waiting for managed device - this may take several minutes
> /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/python/testbed/gradlew --console plain maxVersionDebugAndroidTest -Ppython.sitePackages=/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/site-packages -Ppython.cwd=/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/cwd -Pandroid.testInstrumentationRunnerArguments.pythonMode=-c '-Pandroid.testInstrumentationRunnerArguments.pythonModule=import pyinstrument'
> Configure project :app
Passing custom test runner argument android.testInstrumentationRunnerArguments.pythonMode from gradle.properties or command line is not compatible with configuration caching. Please specify this argument using android gradle dsl.
Passing custom test runner argument android.testInstrumentationRunnerArguments.pythonModule from gradle.properties or command line is not compatible with configuration caching. Please specify this argument using android gradle dsl.

> Task :app:checkKotlinGradlePluginConfigurationErrors
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:generateDebugResValues
> Task :app:checkDebugAarMetadata
> Task :app:mapDebugSourceSetPaths
> Task :app:generateDebugResources
> Task :app:packageDebugResources
> Task :app:createDebugCompatibleScreenManifests
> Task :app:extractDeepLinksDebug
> Task :app:parseDebugLocalResources
> Task :app:processDebugMainManifest
> Task :app:processDebugManifest
> Task :app:preDebugAndroidTestBuild SKIPPED
> Task :app:javaPreCompileDebug
> Task :app:mergeDebugResources
> Task :app:checkDebugAndroidTestAarMetadata
> Task :app:processDebugManifestForPackage
> Task :app:generateDebugAndroidTestResValues
> Task :app:mapDebugAndroidTestSourceSetPaths
> Task :app:generateDebugAndroidTestResources
> Task :app:mergeDebugAndroidTestResources
> Task :app:processDebugAndroidTestManifest
> Task :app:javaPreCompileDebugAndroidTest
> Task :app:mergeDebugShaders
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:processDebugAndroidTestResources
> Task :app:processDebugResources
> Task :app:generateDebugPythonAssets
> Task :app:compileDebugKotlin
> Task :app:mergeDebugAssets
> Task :app:compileDebugJavaWithJavac NO-SOURCE
> Task :app:bundleDebugClassesToCompileJar
> Task :app:processDebugJavaRes
> Task :app:checkDebugDuplicateClasses
> Task :app:mergeDebugJavaResource
> Task :app:desugarDebugFileDependencies
> Task :app:compileDebugAndroidTestKotlin
> Task :app:compressDebugAssets
> Task :app:compileDebugAndroidTestJavaWithJavac NO-SOURCE
> Task :app:mergeLibDexDebug
> Task :app:dexBuilderDebug
> Task :app:mergeProjectDexDebug
> Task :app:configureCMakeDebug[arm64-v8a]
> Task :app:mergeExtDexDebug
> Task :app:buildCMakeDebug[arm64-v8a]
> Task :app:generateDebugPythonJniLibs
> Task :app:mergeDebugJniLibFolders
> Task :app:validateSigningDebug
> Task :app:writeDebugAppMetadata
> Task :app:writeDebugSigningConfigVersions
> Task :app:mergeDebugAndroidTestShaders
> Task :app:compileDebugAndroidTestShaders NO-SOURCE
> Task :app:generateDebugAndroidTestAssets UP-TO-DATE
> Task :app:mergeDebugAndroidTestAssets
> Task :app:compressDebugAndroidTestAssets
> Task :app:processDebugAndroidTestJavaRes
> Task :app:desugarDebugAndroidTestFileDependencies
> Task :app:checkDebugAndroidTestDuplicateClasses
> Task :app:mergeDebugAndroidTestJavaResource
> Task :app:dexBuilderDebugAndroidTest
> Task :app:mergeDebugAndroidTestJniLibFolders
> Task :app:mergeLibDexDebugAndroidTest
> Task :app:mergeProjectDexDebugAndroidTest
> Task :app:validateSigningDebugAndroidTest
> Task :app:writeDebugAndroidTestSigningConfigVersions
> Task :app:mergeDebugNativeLibs
> Task :app:mergeDebugAndroidTestNativeLibs NO-SOURCE
> Task :app:stripDebugAndroidTestDebugSymbols NO-SOURCE
> Task :app:mergeExtDexDebugAndroidTest
> Task :app:stripDebugDebugSymbols
> Task :app:packageDebugAndroidTest
> Task :app:createDebugAndroidTestApkListingFileRedirect
> Task :app:packageDebug
> Task :app:createDebugApkListingFileRedirect

> Task :app:maxVersionSetup
Failed to create Emulator snapshot image (1/5). Error: com.android.build.gradle.internal.AvdSnapshotHandler$EmulatorSnapshotCannotCreatedException: Gradle was not able to complete device setup for: dev34_aosp_atd_arm64-v8a_Small_Phone
The emulator failed to open the managed device to generate the snapshot.
This is because the emulator closed unexpectedly (exit value = 1).
The errors recorded from emulator:ERROR   | A snapshot operation for 'dev34_aosp_atd_arm64-v8a_Small_Phone' is pending and timeout has expired. Exiting...
Failed to create Emulator snapshot image (2/5). Error: com.android.build.gradle.internal.AvdSnapshotHandler$EmulatorSnapshotCannotCreatedException: Gradle was not able to complete device setup for: dev34_aosp_atd_arm64-v8a_Small_Phone
The emulator failed to open the managed device to generate the snapshot.
This is because the emulator closed unexpectedly (exit value = 1).
The errors recorded from emulator:ERROR   | A snapshot operation for 'dev34_aosp_atd_arm64-v8a_Small_Phone' is pending and timeout has expired. Exiting...
Failed to create Emulator snapshot image (3/5). Error: com.android.build.gradle.internal.AvdSnapshotHandler$EmulatorSnapshotCannotCreatedException: Gradle was not able to complete device setup for: dev34_aosp_atd_arm64-v8a_Small_Phone
The emulator failed to open the managed device to generate the snapshot.
This is because the emulator closed unexpectedly (exit value = 1).
The errors recorded from emulator:ERROR   | A snapshot operation for 'dev34_aosp_atd_arm64-v8a_Small_Phone' is pending and timeout has expired. Exiting...
Failed to create Emulator snapshot image (4/5). Error: com.android.build.gradle.internal.AvdSnapshotHandler$EmulatorSnapshotCannotCreatedException: Gradle was not able to complete device setup for: dev34_aosp_atd_arm64-v8a_Small_Phone
The emulator failed to open the managed device to generate the snapshot.
This is because the emulator closed unexpectedly (exit value = 1).
The errors recorded from emulator:ERROR   | A snapshot operation for 'dev34_aosp_atd_arm64-v8a_Small_Phone' is pending and timeout has expired. Exiting...

adb shell command timed out. /Users/joerick/Library/Android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed

> Task :app:maxVersionSetup FAILED
Failed to create Emulator snapshot image (5/5). Error: com.android.build.gradle.internal.AvdSnapshotHandler$EmulatorSnapshotCannotCreatedException: Gradle was not able to complete device setup for: dev34_aosp_atd_arm64-v8a_Small_Phone
The emulator failed to open the managed device to generate the snapshot.
This is because the emulator closed unexpectedly (exit value = 1).
The errors recorded from emulator:ERROR   | A snapshot operation for 'dev34_aosp_atd_arm64-v8a_Small_Phone' is pending and timeout has expired. Exiting...
Deleting unbootable snapshot for device: dev34_aosp_atd_arm64-v8a_Small_Phone
Failed to delete snapshot default_boot for device dev34_aosp_atd_arm64-v8a_Small_Phone in qemu
snapshot file /Users/joerick/.android/avd/gradle-managed/dev34_aosp_atd_arm64-v8a_Small_Phone.avd/cache.img.qcow2. qemu-img exit code: 1
Failed to delete snapshot default_boot for device dev34_aosp_atd_arm64-v8a_Small_Phone in qemu
snapshot file /Users/joerick/.android/avd/gradle-managed/dev34_aosp_atd_arm64-v8a_Small_Phone.avd/userdata-qemu.img.qcow2. qemu-img exit code: 1
Failed to delete snapshot default_boot for device dev34_aosp_atd_arm64-v8a_Small_Phone in qemu
snapshot file /Users/joerick/.android/avd/gradle-managed/dev34_aosp_atd_arm64-v8a_Small_Phone.avd/encryptionkey.img.qcow2. qemu-img exit code: 1

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:maxVersionSetup'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.ManagedDeviceInstrumentationTestSetupTask$ManagedDeviceSetupRunnable
   > com.android.build.gradle.internal.AvdSnapshotHandler$EmulatorSnapshotCannotCreatedException: Gradle was not able to complete device setup for: dev34_aosp_atd_arm64-v8a_Small_Phone
     The emulator failed to open the managed device to generate the snapshot.
     This is because the emulator closed unexpectedly (exit value = 1).
     The errors recorded from emulator:ERROR   | A snapshot operation for 'dev34_aosp_atd_arm64-v8a_Small_Phone' is pending and timeout has expired. Exiting...

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 1m 2s
66 actionable tasks: 66 executed
Command "/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/python/testbed/gradlew --console plain maxVersionDebugAndroidTest -Ppython.sitePackages=/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/site-packages -Ppython.cwd=/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/cwd -Pandroid.testInstrumentationRunnerArguments.pythonMode=-c '-Pandroid.testInstrumentationRunnerArguments.pythonModule=import pyinstrument'" returned exit status 1

                                                             ✕ 64.97s
cibuildwheel: error: Command ['/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/python/android.py', 'test', '--managed', 'maxVersion', '--site-packages', '/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/site-packages', '--cwd', '/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-8qyd_pid/cp313-android_arm64_v8a/cwd', '-c', 'import pyinstrument', '--'] failed with code 1. 

Could it be that my Android SDK is old? I've had this one installed from a previous project. If so we should try to document a minimum version.

Edit: I've updated the SDK tools and still the same error...

I've restarted my Mac and that seems to have fixed that particular issue. Now I've got this problem-

Testing wheel...

+ python -c 'import sysconfig; print(sysconfig.get_platform(), end="")'
+ pip install --only-binary=:all: --platform android_21_arm64_v8a --target /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/site-packages /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/repaired_wheel/pyinstrument-5.0.2-cp313-cp313-android_21_arm64_v8a.whl
Processing /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/repaired_wheel/pyinstrument-5.0.2-cp313-cp313-android_21_arm64_v8a.whl
Installing collected packages: pyinstrument
Successfully installed pyinstrument-5.0.2
+ /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/python/android.py test --managed maxVersion --site-packages /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/site-packages --cwd /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/cwd -c 'import pyinstrument' --
> curl -Lf --retry 5 --retry-all-errors -o /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/python/testbed/gradlew https://raw.githubusercontent.com/gradle/gradle/v8.9.0/gradlew
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  8784  100  8784    0     0  53493      0 --:--:-- --:--:-- --:--:-- 53560
> curl -Lf --retry 5 --retry-all-errors -o /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/python/testbed/gradlew.bat https://raw.githubusercontent.com/gradle/gradle/v8.9.0/gradlew.bat
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2894  100  2894    0     0  21590      0 --:--:-- --:--:-- --:--:-- 21759
> curl -Lf --retry 5 --retry-all-errors -o /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/python/testbed/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/gradle/gradle/v8.9.0/gradle/wrapper/gradle-wrapper.jar
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 43504  100 43504    0     0   182k      0 --:--:-- --:--:-- --:--:--  182k
Waiting for managed device - this may take several minutes
Serial: emulator-5554
Waiting for app to start - this may take several minutes
error: closed
Command "/Users/joerick/Library/Android/sdk/platform-tools/adb -s emulator-5554 shell pidof -s org.python.testbed" returned exit status 1
This may be transient, so continuing to wait
PID: 2032
> /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/python/testbed/gradlew --console plain maxVersionDebugAndroidTest -Ppython.sitePackages=/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/site-packages -Ppython.cwd=/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/cwd -Pandroid.testInstrumentationRunnerArguments.pythonMode=-c '-Pandroid.testInstrumentationRunnerArguments.pythonModule=import pyinstrument'Starting a Gradle Daemon (subsequent builds will be faster)

> Configure project :app
Passing custom test runner argument android.testInstrumentationRunnerArguments.pythonMode from gradle.properties or command line is not compatible with configuration caching. Please specify this argument using android gradle dsl.
Passing custom test runner argument android.testInstrumentationRunnerArguments.pythonModule from gradle.properties or command line is not compatible with configuration caching. Please specify this argument using android gradle dsl.

> Task :app:checkKotlinGradlePluginConfigurationErrors
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:generateDebugResValues
> Task :app:checkDebugAarMetadata
> Task :app:mapDebugSourceSetPaths
> Task :app:generateDebugResources
> Task :app:packageDebugResources
> Task :app:createDebugCompatibleScreenManifests
> Task :app:extractDeepLinksDebug
> Task :app:parseDebugLocalResources
> Task :app:processDebugMainManifest
> Task :app:processDebugManifest
> Task :app:mergeDebugResources
> Task :app:preDebugAndroidTestBuild SKIPPED
> Task :app:javaPreCompileDebug
> Task :app:checkDebugAndroidTestAarMetadata
> Task :app:processDebugManifestForPackage
> Task :app:generateDebugAndroidTestResValues
> Task :app:mapDebugAndroidTestSourceSetPaths
> Task :app:generateDebugAndroidTestResources
> Task :app:mergeDebugAndroidTestResources
> Task :app:processDebugAndroidTestManifest
> Task :app:javaPreCompileDebugAndroidTest
> Task :app:mergeDebugShaders
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:processDebugAndroidTestResources
> Task :app:processDebugResources
> Task :app:generateDebugPythonAssets
> Task :app:mergeDebugAssets
> Task :app:checkDebugDuplicateClasses
> Task :app:compressDebugAssets
> Task :app:desugarDebugFileDependencies
> Task :app:mergeLibDexDebug
> Task :app:configureCMakeDebug[arm64-v8a]
> Task :app:buildCMakeDebug[arm64-v8a]
> Task :app:generateDebugPythonJniLibs
> Task :app:mergeDebugJniLibFolders
> Task :app:validateSigningDebug
> Task :app:writeDebugAppMetadata
> Task :app:writeDebugSigningConfigVersions
> Task :app:mergeDebugAndroidTestShaders
> Task :app:compileDebugAndroidTestShaders NO-SOURCE
> Task :app:generateDebugAndroidTestAssets UP-TO-DATE
> Task :app:mergeDebugAndroidTestAssets
> Task :app:compressDebugAndroidTestAssets
> Task :app:mergeDebugNativeLibs
> Task :app:checkDebugAndroidTestDuplicateClasses
> Task :app:desugarDebugAndroidTestFileDependencies
> Task :app:mergeDebugAndroidTestJniLibFolders
> Task :app:mergeLibDexDebugAndroidTest
> Task :app:mergeDebugAndroidTestNativeLibs NO-SOURCE
> Task :app:stripDebugAndroidTestDebugSymbols NO-SOURCE
> Task :app:validateSigningDebugAndroidTest
> Task :app:writeDebugAndroidTestSigningConfigVersions
> Task :app:stripDebugDebugSymbols
> Task :app:compileDebugKotlin
> Task :app:compileDebugJavaWithJavac NO-SOURCE
> Task :app:processDebugJavaRes
> Task :app:bundleDebugClassesToCompileJar
> Task :app:mergeExtDexDebugAndroidTest
> Task :app:mergeDebugJavaResource
> Task :app:compileDebugAndroidTestKotlin
> Task :app:dexBuilderDebug
> Task :app:compileDebugAndroidTestJavaWithJavac NO-SOURCE
> Task :app:processDebugAndroidTestJavaRes
> Task :app:mergeDebugAndroidTestJavaResource
> Task :app:dexBuilderDebugAndroidTest
> Task :app:mergeProjectDexDebugAndroidTest
> Task :app:mergeProjectDexDebug
> Task :app:packageDebugAndroidTest
> Task :app:createDebugAndroidTestApkListingFileRedirect
> Task :app:mergeExtDexDebug
> Task :app:packageDebug
> Task :app:createDebugApkListingFileRedirect
> Task :app:maxVersionSetup
> Task :app:maxVersionDebugAndroidTest
Starting 1 tests on maxVersion
--------- beginning of main
I/.python.testbed: Late-enabling -Xcheck:jni
I/.python.testbed: Using CollectorTypeCC GC.
D/CompatibilityChangeReporter: Compat change id reported: 171979766; UID 10108; state: ENABLED
D/CompatibilityChangeReporter: Compat change id reported: 242716250; UID 10108; state: ENABLED
--------- beginning of system
W/ActivityThread: Package uses different ABI(s) than its instrumentation: package[org.python.testbed]: arm64-v8a, null instrumentation[org.python.testbed.test]: null, null
W/.python.testbed: ClassLoaderContext shared library size mismatch. Expected=2, found=0 (PCL[]{PCL[/system/framework/android.test.base.jar*836642756]#PCL[/system/framework/android.test.mock.jar*3186112116]} | PCL[])
W/.python.testbed: ClassLoaderContext classpath size mismatch. expected=0, found=1 (PCL[] | PCL[/system/framework/android.test.runner.jar*4168958247])
W/.python.testbed: ClassLoaderContext classpath size mismatch. expected=0, found=2 (PCL[] | PCL[/system/framework/android.test.runner.jar*4168958247:/system/framework/android.test.mock.jar*3186112116])
W/ziparchive: Unable to open '/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.dm': No such file or directory
W/ziparchive: Unable to open '/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.dm': No such file or directory
W/ziparchive: Unable to open '/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.dm': No such file or directory
W/ziparchive: Unable to open '/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.dm': No such file or directory
D/nativeloader: Configuring clns-6 for other apk /system/framework/android.test.runner.jar:/system/framework/android.test.mock.jar:/system/framework/android.test.base.jar:/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.apk. target_sdk_version=34, uses_libraries=, library_path=/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/lib/arm64:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/lib/arm64:/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk!/lib/arm64-v8a:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.apk!/lib/arm64-v8a, permitted_path=/data:/mnt/expand:/data/user/0/org.python.testbed
V/GraphicsEnvironment: Currently set values for:
V/GraphicsEnvironment:   angle_gl_driver_selection_pkgs=[]
V/GraphicsEnvironment:   angle_gl_driver_selection_values=[]
V/GraphicsEnvironment: ANGLE GameManagerService for org.python.testbed: false
V/GraphicsEnvironment: org.python.testbed is not listed in per-application setting
V/GraphicsEnvironment: Neither updatable production driver nor prerelease driver is supported.
D/ApplicationLoaders: Returning zygote-cached class loader: /system/framework/android.test.base.jar
D/nativeloader: Configuring clns-7 for other apk /system/framework/android.test.mock.jar. target_sdk_version=34, uses_libraries=ALL, library_path=/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/lib/arm64:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/lib/arm64, permitted_path=/data:/mnt/expand
D/ApplicationLoaders: Returning zygote-cached class loader: /system/framework/android.test.base.jar
D/nativeloader: Configuring clns-8 for other apk /system/framework/android.test.runner.jar. target_sdk_version=34, uses_libraries=ALL, library_path=/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/lib/arm64:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/lib/arm64, permitted_path=/data:/mnt/expand
W/.python.testbed: ClassLoaderContext shared library size mismatch. Expected=2, found=3 (PCL[]{PCL[/system/framework/android.test.base.jar*836642756]#PCL[/system/framework/android.test.mock.jar*3186112116]} | PCL[]{PCL[/system/framework/android.test.base.jar*836642756]#PCL[/system/framework/android.test.mock.jar*3186112116]#PCL[/system/framework/android.test.runner.jar*4168958247]{PCL[/system/framework/android.test.base.jar*836642756]#PCL[/system/framework/android.test.mock.jar*3186112116]}};PCL[/system/framework/android.test.runner.jar*4168958247:/system/framework/android.test.mock.jar*3186112116:/system/framework/android.test.base.jar*836642756:/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk*381495392:/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk!classes2.dex*1626597583:/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk!classes3.dex*494747635:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.apk*968448466:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.apk!classes2.dex*4160919203:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.apk!classes3.dex*209047018])
W/.python.testbed: ClassLoaderContext classpath size mismatch. expected=0, found=1 (PCL[] | PCL[/system/framework/android.test.runner.jar*4168958247]{PCL[/system/framework/android.test.base.jar*836642756]#PCL[/system/framework/android.test.mock.jar*3186112116]#PCL[/system/framework/android.test.runner.jar*4168958247]{PCL[/system/framework/android.test.base.jar*836642756]#PCL[/system/framework/android.test.mock.jar*3186112116]}};PCL[/system/framework/android.test.runner.jar*4168958247:/system/framework/android.test.mock.jar*3186112116:/system/framework/android.test.base.jar*836642756:/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk*381495392:/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk!classes2.dex*1626597583:/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk!classes3.dex*494747635:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.apk*968448466:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.apk!classes2.dex*4160919203:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.apk!classes3.dex*209047018])
W/.python.testbed: ClassLoaderContext classpath size mismatch. expected=0, found=2 (PCL[] | PCL[/system/framework/android.test.runner.jar*4168958247:/system/framework/android.test.mock.jar*3186112116]{PCL[/system/framework/android.test.base.jar*836642756]#PCL[/system/framework/android.test.mock.jar*3186112116]#PCL[/system/framework/android.test.runner.jar*4168958247]{PCL[/system/framework/android.test.base.jar*836642756]#PCL[/system/framework/android.test.mock.jar*3186112116]}};PCL[/system/framework/android.test.runner.jar*4168958247:/system/framework/android.test.mock.jar*3186112116:/system/framework/android.test.base.jar*836642756:/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk*381495392:/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk!classes2.dex*1626597583:/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk!classes3.dex*494747635:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.apk*968448466:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.apk!classes2.dex*4160919203:/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.apk!classes3.dex*209047018])
W/ziparchive: Unable to open '/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.dm': No such file or directory
W/ziparchive: Unable to open '/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.dm': No such file or directory
W/ziparchive: Unable to open '/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.dm': No such file or directory
W/ziparchive: Unable to open '/data/app/~~tNjQjqwaWtfPIqB2MH_N1Q==/org.python.testbed-SiYcnHBOhBCREe0qAZ_P7Q==/base.dm': No such file or directory
I/MonitoringInstr: newApplication called!
I/MonitoringInstr: Instrumentation started!
I/AndroidJUnitRunner: onCreate Bundle[{testTimeoutSeconds=31536000, pythonModule=import pyinstrument, pythonMode=-c, additionalTestOutputDir=/sdcard/Android/media/org.python.testbed/additional_test_output}]
V/TestEventClient: No service name argument was given (testDiscoveryService, testRunEventService or orchestratorService)
D/AndroidJUnitRunner: onStart is called.
I/MonitoringInstr: No JSBridge.
I/InstrConnection: Could not send broadcast or register receiver (isolatedProcess?)
D/TestRequestBuilder: Using class path scanning to discover tests
I/TestRequestBuilder: Scanning classpath to find tests in paths [/data/app/~~qgCUvC-A7DlM11paIU0q5Q==/org.python.testbed.test-QuI3xzNi-361e76beetCRw==/base.apk]
W/.python.testbed: Opening an oat file without a class loader. Are you using the deprecated DexFile APIs?
D/AndroidJUnitRunner: Use the raw file system for managing file I/O.
D/TestExecutor: Adding listener androidx.test.internal.runner.listener.LogRunListener
D/TestExecutor: Adding listener androidx.test.internal.runner.listener.InstrumentationResultPrinter
D/TestExecutor: Adding listener androidx.test.internal.runner.listener.ActivityFinisherRunListener
D/TestExecutor: Adding listener androidx.test.internal.runner.listener.TraceRunListener
I/TestRunner: run started: 1 tests
I/TestRunner: started: testPython(org.python.testbed.PythonSuite)
W/.python.testbed: type=1400 audit(0.0:37): avc:  denied  { read } for  name="overcommit_memory" dev="proc" ino=47927 scontext=u:r:untrusted_app:s0:c108,c256,c512,c768 tcontext=u:object_r:proc_overcommit_memory:s0 tclass=file permissive=0 app=org.python.testbed
W/.python.testbed: type=1400 audit(0.0:38): avc:  granted  { execute } for  path="/data/data/org.python.testbed/files/python/lib/python3.13/lib-dynload/_opcode.cpython-313-aarch64-linux-android.so" dev="dm-37" ino=312725 scontext=u:r:untrusted_app:s0:c108,c256,c512,c768 tcontext=u:object_r:app_data_file:s0:c108,c256,c512,c768 tclass=file app=org.python.testbed
W/.python.testbed: type=1400 audit(0.0:39): avc:  granted  { execute } for  path="/data/data/org.python.testbed/files/python/lib/python3.13/lib-dynload/_json.cpython-313-aarch64-linux-android.so" dev="dm-37" ino=312720 scontext=u:r:untrusted_app:s0:c108,c256,c512,c768 tcontext=u:object_r:app_data_file:s0:c108,c256,c512,c768 tclass=file app=org.python.testbed
W/.python.testbed: type=1400 audit(0.0:40): avc:  granted  { execute } for  path="/data/data/org.python.testbed/files/python/lib/python3.13/lib-dynload/math.cpython-313-aarch64-linux-android.so" dev="dm-37" ino=312754 scontext=u:r:untrusted_app:s0:c108,c256,c512,c768 tcontext=u:object_r:app_data_file:s0:c108,c256,c512,c768 tclass=file app=org.python.testbed
W/.python.testbed: type=1400 audit(0.0:41): avc:  granted  { execute } for  path="/data/data/org.python.testbed/files/python/lib/python3.13/lib-dynload/_ctypes.cpython-313-aarch64-linux-android.so" dev="dm-37" ino=312710 scontext=u:r:untrusted_app:s0:c108,c256,c512,c768 tcontext=u:object_r:app_data_file:s0:c108,c256,c512,c768 tclass=file app=org.python.testbed
--------- beginning of kernel
W/audit   : audit_lost=21 audit_rate_limit=5 audit_backlog_limit=64
E/audit   : rate limit exceeded
I/TestRunner: finished: testPython(org.python.testbed.PythonSuite)
I/TestRunner: run finished: 1 tests, 0 failed, 0 ignored
Command "/Users/joerick/Library/Android/sdk/platform-tools/adb -s emulator-5554 logcat --pid 2032 --format tag" returned exit status 255

                                                             ✕ 96.05s
cibuildwheel: error: Command ['/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/python/android.py', 'test', '--managed', 'maxVersion', '--site-packages', '/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/site-packages', '--cwd', '/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/cwd', '-c', 'import pyinstrument', '--'] failed with code 1. 

It's curious. It looks like the test passes, but an error is reported:

--------- beginning of kernel
W/audit   : audit_lost=21 audit_rate_limit=5 audit_backlog_limit=64
E/audit   : rate limit exceeded
I/TestRunner: finished: testPython(org.python.testbed.PythonSuite)
I/TestRunner: run finished: 1 tests, 0 failed, 0 ignored
Command "/Users/joerick/Library/Android/sdk/platform-tools/adb -s emulator-5554 logcat --pid 2032 --format tag" returned exit status 255

                                                             ✕ 96.05s
cibuildwheel: error: Command ['/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/python/android.py', 'test', '--managed', 'maxVersion', '--site-packages', '/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/site-packages', '--cwd', '/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-396aywo3/cp313-android_arm64_v8a/cwd', '-c', 'import pyinstrument', '--'] failed with code 1. 

Copy link
Contributor

@joerick joerick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really impressive work on this @mhsmith !

if not configs:
return

try:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be worth adding a check here for the ANDROID_HOME environment variable and returning a nice error message if it's not found. Ideally with a link to the docs explaining how to download the SDK.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +604 to +615
call(
python_dir / "android.py",
"test",
"--managed",
"maxVersion",
"--site-packages",
site_packages_dir,
"--cwd",
cwd_dir,
*test_args,
env=build_env,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a cibw thing, but I see in the logs for this that the testbed is using a raw.githubusercontent.com URL for downloading Gradle. We've seen HTTP 429 requests on these URLs in the past, specifically in #2002. I suspect Github might rate-limit based on volume of requests to a URL, so if it shipped in cibuildwheel we'd probably see the same. If you can download from a release URL instead, that might be better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already experienced this problem when running the CPython test suite, and added a workaround in python/cpython#133193. That was 6 weeks ago, and I don't think the download has ever failed again (except for one general outage that affected all of GitHub).

# Some packages may recognize sys.cross_compiling from the crossenv tool.
sys.cross_compiling = True # type: ignore[attr-defined]
sys.getandroidapilevel = cross_getandroidapilevel # type: ignore[attr-defined]
sys.implementation._multiarch = os.environ["HOST"] # type: ignore[attr-defined]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who sets the HOST variable? On my Mac this variable contains the machine hostname e.g.

joerick@joerick5 ~> echo $HOST
joerick5.local

If its set by us, maybe we should consider a more specific name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this context it originates from CPython's android.py script. It was inspired by conda-build, or its compiler activation scripts, which set the HOST and BUILD variables to support build scripts like this.

However, the clash with zsh's variable of the same name has caused some problems:

It's probably too late for Conda to change this, but there's no reason for us to perpetuate it. So I've changed _cross_venv.py to receive the host triplet in a CIBW_HOST_TRIPLET environment variable instead, replacing the boolean CIBW_CROSS_VENV.

Comment on lines +124 to +129
build_env, android_env = setup_env(config, build_options, build_path, python_dir)
before_build(build_options, build_env)
built_wheel = build_wheel(build_options, build_path, android_env)
repaired_wheel = repair_wheel(
build_options, build_path, build_env, android_env, built_wheel
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will make it more verbose, which is a shame, but I think I'd feel better if these functions used keyword args - there are potential errors where e.g. android_env and build_env could get mixed up and the typechecker wouldn't catch it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think using the env var to toggle this venv's cross-build mode is very clever/elegant :)

Comment on lines +228 to +230
pb = ProjectBuilder.from_isolated_env(AndroidEnv(), build_options.package_dir)
if pb.build_system_requires:
call("pip", "install", *pb.build_system_requires, env=build_env)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I understand this, if a package had an android wheel, ProjectBuilder would find that file and return a full URL to it here?

But the pip install command is executed in the build_env. So if it was compiling from a tar.gz it would build for the wrong platform in that case?

Comment on lines +354 to +355
for key in ["CFLAGS", "CXXFLAGS"]:
android_env[key] += " " + opt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know that CFLAGS/CXXFLAGS exists in android_env? I'm confused why this line doesn't raise a KeyError.

Oh, I suppose it's always returned by android.py env. It might be worth making this more robust though.

Comment on lines +339 to +340
env_output = call(python_dir / "android.py", "env", env=build_env, capture_stdout=True)
for line in env_output.splitlines():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we be sure that environment variables won't contain newlines? We've seen a few occasions where that wasn't the case. Generally I prefer to use JSON as the format to pass env vars between processes like this, if possible.

if test_args[:2] in [["python", "-c"], ["python", "-m"]]:
test_args[:3] = [test_args[1], test_args[2], "--"]
elif test_args[0] in ["pytest"]:
test_args[:1] = ["-m", test_args[0], "--"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a warning here to match the iOS behaviour.

msg = unwrap_preserving_paragraphs(f"""


* `python -c command ...` (Android only)
* `python -m module-name ...`
* `pytest ...` (deprecated; converted to `python -m pytest ...`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather just not document this form, I think it's more of a convenience feature for users migrating from other platforms.

Suggested change
* `pytest ...` (deprecated; converted to `python -m pytest ...`)

sts = strcmp(content, "spam");
sts = strcmp(content, "spam") != 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why this change?

<sup>³ Requires a macOS runner; runs tests on the simulator for the runner's architecture.</sup>

<sup>³ Requires a macOS runner; runs tests on the simulator for the runner's architecture.</sup><br>
<sup>⁴ Building for Android requires the runner to be Linux x86_64, macOS ARM64 or macOS x86_64. Testing is supported on the same platforms, but also requires the runner to either be bare-metal, or support nested virtualization. CI platforms known to meet this requirement are: GitHub Actions Linux x86_64.</sup><br>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to this it should also be possible to run the emulator for testing on "Azure Pipelines macOS (Microsoft-hosted agent)". But I haven't tried it yet.

@mayeut
Copy link
Member

mayeut commented Jun 14, 2025

On Android the OS doesn't provide a C++ library, so every app needs to bundle its own copy. This required giving cibuildwheel some auditwheel-like code which adds the C++ library to the wheel in the repair step if necessary.

Should there be a recommendation / example to link against the static one if possible rather than bundle ?

call(
"patchelf",
"--set-rpath",
f"${{ORIGIN}}/{libs_dir.relative_to(path.parent)}",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On this line I get the following error in a private pybind11 project:

Repairing wheel...

+ python -c 'import sysconfig; print(sysconfig.get_config_var("ANDROID_API_LEVEL"), end="")'
+ patchelf --set-soname libc++_shared-cd110349.so /tmp/cibw-run-536mrs3q/cp313-android_x86_64/repaired_wheel/unpacked/myprivatelib_pkg.libs/libc++_shared-cd110349.so
+ patchelf --replace-needed libc++_shared.so libc++_shared-cd110349.so /tmp/cibw-run-536mrs3q/cp313-android_x86_64/repaired_wheel/unpacked/myprivatelib/somelib.cpython-313-x86_64-linux-android.so
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/opt/hostedtoolcache/Python/3.13.4/x64/lib/python3.13/site-packages/cibuildwheel/__main__.py", line 511, in <module>
    main()
    ~~~~^^
  File "/opt/hostedtoolcache/Python/3.13.4/x64/lib/python3.13/site-packages/cibuildwheel/__main__.py", line 63, in main
    main_inner(global_options)
    ~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.13.4/x64/lib/python3.13/site-packages/cibuildwheel/__main__.py", line 207, in main_inner
    build_in_directory(args)
    ~~~~~~~~~~~~~~~~~~^^^^^^
  File "/opt/hostedtoolcache/Python/3.13.4/x64/lib/python3.13/site-packages/cibuildwheel/__main__.py", line 372, in build_in_directory
    platform_module.build(options, tmp_path)
    ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.13.4/x64/lib/python3.13/site-packages/cibuildwheel/platforms/android.py", line 127, in build
    repaired_wheel = repair_wheel(
        build_options, build_path, build_env, android_env, built_wheel
    )
  File "/opt/hostedtoolcache/Python/3.13.4/x64/lib/python3.13/site-packages/cibuildwheel/platforms/android.py", line 421, in repair_wheel
    repair_default(android_env, built_wheel, repaired_wheel_dir)
    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.13.4/x64/lib/python3.13/site-packages/cibuildwheel/platforms/android.py", line 496, in repair_default
    f"${{ORIGIN}}/{libs_dir.relative_to(path.parent)}",
                   ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.13.4/x64/lib/python3.13/pathlib/_local.py", line 385, in relative_to
    raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
ValueError: '/tmp/cibw-run-536mrs3q/cp313-android_x86_64/repaired_wheel/unpacked/myprivatelib_pkg.libs' is not in the subpath of '/tmp/cibw-run-536mrs3q/cp313-android_x86_64/repaired_wheel/unpacked/myprivatelib'

My final project structure should look like this:

myprivatelib_pkg:
-----------------

myprivatelib
├─ __init__.py
├─ version.py
└─ somelib.cpython-313-x86_64-linux-android.so

In my CMakeLists.txt I use:

install(TARGETS somelib DESTINATION myprivatelib)

If I change it to:

install(TARGETS somelib DESTINATION .)

the "Repairing wheel" step works, but my project structure is wrong and my unit-tests are failing.

@mhsmith
Copy link
Contributor Author

mhsmith commented Jun 16, 2025

On Android the OS doesn't provide a C++ library, so every app needs to bundle its own copy. This required giving cibuildwheel some auditwheel-like code which adds the C++ library to the wheel in the repair step if necessary.

Should there be a recommendation / example to link against the static one if possible rather than bundle ?

That would be unsafe when a package has multiple .so files, for the reasons given here.

In theory it may even be unsafe if there are multiple packages each linked against their own separate shared libc++. This exact issue wouldn't arise on Linux because the manylinux standard requires the C++ library to be provided by the OS. However, it could arise in other libraries which auditwheel includes multiple copies of in multiple packages. I think the reason why we usually get away with this on Linux is that separate packages usually interact with each other via Python interfaces rather than C++ ones, so the crash scenarios described in the above link do not occur. The same should be true on Android.

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.

Add support for Android and iOS
6 participants