diff --git a/.github/workflows/CI-wheels.yaml b/.github/workflows/CI-wheels.yaml index 0609c507..4a061bfc 100644 --- a/.github/workflows/CI-wheels.yaml +++ b/.github/workflows/CI-wheels.yaml @@ -39,6 +39,26 @@ jobs: name: wheels-${{ matrix.os }} path: ./wheelhouse/*.whl + build_wheels_android: + name: Build Android wheels + runs-on: ubuntu-26.04 + + steps: + - uses: actions/checkout@v7 + + - name: Build Android wheels + uses: pypa/cibuildwheel@v4.3 + env: + CIBW_PLATFORM: android + CIBW_BUILD: "cp313-android_arm64_v8a cp313-android_x86_64 cp314-android_arm64_v8a cp314-android_x86_64 cp315-android_arm64_v8a cp315-android_x86_64" + LIBZIM_DL_VERSION: "9.8.2-1" + CIBW_BUILD_VERBOSITY: "3" + + - uses: actions/upload-artifact@v7 + with: + name: wheels-android + path: ./wheelhouse/*.whl + build_sdist: name: Build source distribution runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index ddd1713a..8f119b2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add a CI job building the documentation on every PR/push so this kind of breakage is caught automatically (#279) - Add support for Python 3.15 (#281) +- Add Android wheels support via cibuildwheel for arm64_v8a and x86_64 with CPython 3.13+ (#167) - Build wheels against the stable ABI (abi3) so a single wheel per platform covers all supported CPython versions (#263) ## [3.13.1] - 2026-10-05 diff --git a/pyproject.toml b/pyproject.toml index 49de6f57..ad1955f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ classifiers = [ "Typing :: Stubs Only", "Operating System :: MacOS", "Operating System :: POSIX", + "Operating System :: Android", ] [project.urls] @@ -106,6 +107,7 @@ libzim = [ "*.pyi", "libzim.9.dylib", "libzim.so.9", + "libzim.so", "zim-9.dll", "icuuc74.dll", "icutu74.dll", @@ -141,6 +143,9 @@ archs = ["x86_64", "arm64"] # > requires changing wheel names test-skip = "*_x86_64" +[tool.cibuildwheel.android] +archs = ["arm64_v8a", "x86_64"] + [tool.hatch.build] exclude = [ "/.github", diff --git a/setup.py b/setup.py index 3198a0aa..c13de6b8 100755 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ import zipfile from ctypes.util import find_library from pathlib import Path +from typing import ClassVar from Cython.Build import cythonize from Cython.Distutils.build_ext import new_build_ext as build_ext @@ -49,11 +50,20 @@ class Config: # windows _msvc_debug: bool = bool(os.getenv("MSVC_DEBUG")) - supported_platforms = { # noqa: RUF012 + supported_platforms: ClassVar[dict[str, list[str]]] = { "Darwin": ["x86_64", "arm64"], "Linux": ["x86_64", "aarch64"], "Linux-musl": ["x86_64", "aarch64"], "Windows": ["amd64"], + "Android": ["arm64_v8a", "x86_64"], + } + + _android_triplets: ClassVar[frozenset[str]] = frozenset( + {"aarch64-linux-android", "x86_64-linux-android"} + ) + _android_arch_map: ClassVar[dict[str, str]] = { + "aarch64-linux-android": "arm64_v8a", + "x86_64-linux-android": "x86_64", } base_dir: pathlib.Path = Path(__file__).parent @@ -92,19 +102,27 @@ def is_nightly(self) -> bool: @property def platform(self) -> str: - """Platform building for: Darwin, Linux""" + """Platform building for: Darwin, Linux, Android""" + if os.getenv("CIBW_HOST_TRIPLET") in self._android_triplets: + return "Android" return sysplatform.system() @property def platform_libc(self) -> str: """Platform adjusted for libc variant: Darwin, Linux, Linux-musl""" + if self.platform == "Android": + return "Android" if self.platform == "Linux" and self.is_musl: return "Linux-musl" return self.platform @property def arch(self) -> str: - # macOS x86_64|arm64 - linux x86_64|aarch64 + # macOS x86_64|arm64 - linux x86_64|aarch64 - android arm64_v8a|x86_64 + + host_triplet = os.getenv("CIBW_HOST_TRIPLET", "") + if host_triplet in self._android_triplets: + return self._android_arch_map[host_triplet] # when using cibuildwheel on macOS to cross-compile, # `_PYTHON_HOST_PLATFORM` contains @@ -133,6 +151,7 @@ def libzim_fname(self): "Darwin": f"libzim.{self.libzim_major}.dylib", "Linux": f"libzim.so.{self.libzim_major}", "Windows": f"zim-{self.libzim_major}.dll", + "Android": "libzim.so", }[self.platform] @property @@ -148,6 +167,8 @@ def archive_format(self): @property def is_musl(self) -> bool: """whether running on a musl system (Alpine)""" + if self.platform == "Android": + return False ps = subprocess.run( ["/usr/bin/env", "ldd", "--version"], capture_output=True, @@ -182,13 +203,19 @@ def get_download_filename(self, arch: str | None = None) -> str: if self.platform == "Windows" and arch == "amd64": arch = "x86_64" - lzplatform = {"Darwin": "macos", "Linux": "linux", "Windows": "win"}.get( - self.platform - ) + if self.platform == "Android": + android_arch_map = {"arm64_v8a": "arm64", "x86_64": "x86_64"} + arch = android_arch_map.get(arch, arch) + lzplatform = "android" + variant = "" + else: + lzplatform = {"Darwin": "macos", "Linux": "linux", "Windows": "win"}.get( + self.platform + ) - variant = "" - if self.platform == "Linux": - variant = "-musl" if self.is_musl else "-manylinux" + variant = "" + if self.platform == "Linux": + variant = "-musl" if self.is_musl else "-manylinux" if self.is_latest_nightly: version_suffix = "" @@ -441,7 +468,9 @@ def get_cython_extension() -> list[Extension]: include_dirs.append("include") library_dirs = ["libzim"] - if config.platform != "Windows": + if config.platform == "Android": + runtime_library_dirs = ["$ORIGIN/libzim/"] + elif config.platform != "Windows": runtime_library_dirs = ( [f"@loader_path/libzim/{config.libzim_fname}"] if config.platform == "Darwin"