From c6484d3a66d2d219f204f289afb7c14038d52169 Mon Sep 17 00:00:00 2001 From: pawank925 <229111348+pawank925@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:55:05 +0530 Subject: [PATCH] Add Android wheels support --- .github/workflows/CI-wheels.yaml | 21 ++++++++++++++ CHANGELOG.md | 4 +++ README.md | 3 +- libzim/libzim.pyx | 2 ++ pyproject.toml | 6 ++++ setup.py | 49 +++++++++++++++++++++++++------- 6 files changed, 74 insertions(+), 11 deletions(-) diff --git a/.github/workflows/CI-wheels.yaml b/.github/workflows/CI-wheels.yaml index 0609c507..c7276358 100644 --- a/.github/workflows/CI-wheels.yaml +++ b/.github/workflows/CI-wheels.yaml @@ -39,6 +39,27 @@ jobs: name: wheels-${{ matrix.os }} path: ./wheelhouse/*.whl + build_wheels_android: + name: Build Android wheels + runs-on: ubuntu-26.04 + env: + LIBZIM_WITHOUT_WRITER: "1" + + 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" + CIBW_ENVIRONMENT_PASS_ANDROID: "LIBZIM_DL_VERSION LIBZIM_WITHOUT_WRITER" + + - 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 330fa057..34fb361b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add Android wheels support via cibuildwheel for arm64_v8a and x86_64 with CPython 3.13+; these wheels are reader-only since libzim is compiled without writer support on Android (#167) + ### Changed - Relax `setuptools`/`wheel`/`cython` build dependency pins to `>=floor,` ranges instead of exact versions, easing downstream packaging; `delocate` stays exactly pinned (#288) diff --git a/README.md b/README.md index 9c515953..d44132d9 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Our [PyPI wheels](https://pypi.org/project/libzim/) bundle a [recent release](ht - GNU/Linux for `x86_64`, `armhf` and `aarch64` - Linux+musl for `x86_64` and `aarch64` - Windows for `x64` +- Android for `arm64_v8a` and `x86_64` with CPython 3.13+ Wheels are available for CPython only (but can be built for Pypy). @@ -33,7 +34,7 @@ Free-threaded CPython is supported: the extension declares itself free-threading Users on other platforms can install the source distribution (see [Building](#Building) below). -Writer support is decided when the extension is built, not when it is installed: there is no install-time extra for it. Set `LIBZIM_WITHOUT_WRITER=1` when building against a libzim compiled without writer support; such a build exposes the reader, search, suggestion, illustration and version modules only. Import `libzim.writer` in a `try`/`except ImportError` block to detect writer support at runtime. +Writer support is decided when the extension is built, not when it is installed: there is no install-time extra for it. Set `LIBZIM_WITHOUT_WRITER=1` when building against a libzim compiled without writer support; such a build exposes the reader, search, suggestion, illustration and version modules only. Import `libzim.writer` in a `try`/`except ImportError` block to detect writer support at runtime. Android wheels are built without writer support, since libzim is compiled without it for Android. ## Contributions diff --git a/libzim/libzim.pyx b/libzim/libzim.pyx index 79af2316..e8f61da9 100644 --- a/libzim/libzim.pyx +++ b/libzim/libzim.pyx @@ -780,6 +780,8 @@ IF not LIBZIM_WITHOUT_WRITER: IF not LIBZIM_WITHOUT_WRITER: writer_module_doc = """libzim writer module + Not available on Android, where libzim is built without writer support. + - Creator to create ZIM files - Item to store ZIM articles metadata - ContentProvider to store an Item's content diff --git a/pyproject.toml b/pyproject.toml index 2c7413a6..66c9cdbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ classifiers = [ "Typing :: Stubs Only", "Operating System :: MacOS", "Operating System :: POSIX", + "Operating System :: Android", ] [project.urls] @@ -118,6 +119,7 @@ libzim = [ "*.pyi", "libzim.9.dylib", "libzim.so.9", + "libzim.so", "zim-9.dll", "icuuc74.dll", "icutu74.dll", @@ -153,6 +155,10 @@ archs = ["x86_64", "arm64"] # > requires changing wheel names test-skip = "*_x86_64" +[tool.cibuildwheel.android] +archs = ["arm64_v8a", "x86_64"] +test-skip = "*" + [tool.hatch.build] exclude = [ "/.github", diff --git a/setup.py b/setup.py index 935b6971..68998e63 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 @@ -50,11 +51,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 @@ -93,19 +103,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 @@ -134,6 +152,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 @@ -149,6 +168,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, @@ -183,13 +204,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 = "" @@ -448,7 +475,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"