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 68 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.

README.md Outdated
<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 multiple packages each link against their own separate libc++.so, and they pass C++ objects between them. This exact issue wouldn't arise on Linux, because the manylinux standard requires the C++ library to be provided by the OS. However, similar issues could affect other C++-based libraries if auditwheel includes multiple copies 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.

@mhsmith
Copy link
Contributor Author

mhsmith commented Jul 12, 2025

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. 

This is very strange. It's evidently running Python, as we see it loading several standard library modules, but it doesn't show any Python stdout or stderr. Despite that, the test apparently passes, and then the logcat process returns non-zero. This non-zero return is normal when the emulator is shutting down, but since the testbed hasn't seen any output from Python, it treats this as a fatal error.

The same thing has happened in CI on this PR, but only once. I've never seen it in the CI of Python itself, which uses the same testbed and runs many times every day.

@joerick: Does it still fail with the current version of this PR, and does it fail the same way every time you try?

@mhsmith
Copy link
Contributor Author

mhsmith commented Jul 12, 2025

Ah, now I think I see. Your test script is just import pyinstrument, which I guess isn't supposed to produce any output. If you changed that to something likeimport pyinstrument; print(pyinstrument), then I think it would work.

I should probably change the testbed so it ignores a non-zero return from logcat if it's seen anything that looks like a valid logcat message, whether from Python or not.

That still doesn't explain why this problem happened once in CI in a test which actually was producing output. Maybe the emulator shut down before the output could be read. The output should have occurred before the "TestRunner: finished" message, but it came from a separate thread, so maybe the message ordering isn't guaranteed. If this happens again, I'll try making the testbed pause for a moment after the test script completes before reporting the result.

@henryiii henryiii requested review from freakboy3742 and Copilot July 16, 2025 18:14
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Adds comprehensive support for building and testing Android wheels in cibuildwheel.

  • Introduces a new Android platform implementation (platforms/android.py) with environment setup, build, repair, and test logic.
  • Extends core option handling, typing, packaging, and default schemas to recognize android.
  • Adds end-to-end tests, updates CI workflows, examples, and documentation to cover Android builds.

Reviewed Changes

Copilot reviewed 35 out of 37 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
cibuildwheel/platforms/android.py New Android build, repair, and test logic
cibuildwheel/options.py Make build_frontend always present, default to build
cibuildwheel/typing.py Include android in PlatformName literal
cibuildwheel/util/packaging.py Support ignoring version number in Android wheel tags
docs/platforms.md Add Android platform guide
docs/options.md Add Android-specific environment & option variables
docs/ci-services.md Update example description to include Android
examples/github-deploy.yml Extend GitHub Actions example for Android
azure-pipelines.yml Install JDK for Android SDK tools
.github/workflows/test.yml Enable KVM for Android emulator in CI
unit_test/options_test.py Adjust default frontend test expectations
unit_test/main_tests/main_options_test.py Test parsing of Android config settings
unit_test/architecture_test.py Add arch_synonym tests
test/utils.py Generate deterministic, Android-aware expected wheels
test/test_android.py End-to-end Android wheel build & test scenarios
Comments suppressed due to low confidence (4)

examples/github-deploy.yml:34

  • The example workflow doesn’t include steps to install or configure the Android SDK (ANDROID_HOME); consider adding commands to download the command-line tools, install necessary SDK packages, accept licenses, and set ANDROID_HOME before building Android wheels.
          - os: android-intel

cibuildwheel/platforms/android.py:1

  • [nitpick] The Android platform implementation exceeds 600 lines in one file. Splitting environment setup, toolchain generation, and build/test logic into smaller modules would improve readability and maintainability.
import csv

cibuildwheel/util/packaging.py:161

  • There are no unit tests verifying find_compatible_wheel behavior for Android wheel tags. Adding tests for Android identifier patterns (e.g., android_21_arm64_v8a) would ensure this logic remains correct.
            if platform.startswith(("manylinux", "musllinux", "macosx", "android", "ios")):

docs/platforms.md:317

  • [nitpick] The note about requiring test-sources for iOS was removed; consider reinstating guidance to copy test files into the testbed app via test-sources so users don’t miss this required configuration on iOS.
The iOS test environment can't support running shell scripts, so the [`test-command`](options.md#test-command) value must be specified as if it were a command line being passed to `python -m ...`.

@henryiii
Copy link
Contributor

@freakboy3742 I rerequeted a review from you since it's still "requesting changes".

@freakboy3742
Copy link
Contributor

@freakboy3742 I rerequeted a review from you since it's still "requesting changes".

Acknowledged - I'm currently at EuroPython; I'll try to take a look, but I might not get a chance until I return to my office next week.

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