.. _android-wheel: Android wheels ============== PyJNIus can be published as a prebuilt `PEP 738 `_ Android wheel (``android_*`` tags), so an Android packager can ``pip install`` it instead of compiling from source per app. The wheel is **SDL-agnostic** and **Java-free**: it resolves the JVM ``JNIEnv`` at runtime and ships no Java. In return, the host application must satisfy a small runtime contract (below). .. note:: This describes the *redistributable* wheel. On desktop, PyJNIus still starts its own JVM and bundles the compiled ``NativeInvocationHandler.class`` as before; nothing here changes desktop behavior. The runtime contract -------------------- The wheel does **not** create a JVM on Android; it attaches to the one the host process already has. A conforming host must provide: 1. **An in-process JVM discoverable at import.** The ``JNIEnv`` is resolved at the first ``import jnius`` in this order: * ``SDL_GetAndroidJNIEnv`` from ``libSDL3.so`` (SDL3), else * ``SDL_AndroidGetJNIEnv`` from ``libSDL2.so`` (SDL2), else * ``JNI_GetCreatedJavaVMs`` from ``libnativehelper.so`` (SDL-independent, **API 31+ only**) + ``AttachCurrentThread``. For an SDL host, its SDL library must be **loaded before the first** ``import jnius`` (a Kivy/SDL app loads SDL at startup, which captures the ``JavaVM`` via SDL's own ``JNI_OnLoad``). Each tier tries ``dlsym(RTLD_DEFAULT, ...)`` first, then ``dlopen``\ s the library by soname and resolves the symbol from that handle -- a CPython extension imported via ``dlopen`` does not have the host's ``System.loadLibrary``\ 'd SDL in its default lookup scope. No ``DT_NEEDED`` on any ``libSDL``/``libnativehelper`` is created, so the wheel loads regardless of which (if any) SDL is present. 2. **The Java glue on the app's dex classpath.** PyJNIus's *Python-implements-a-Java-interface* feature (``PythonJavaClass``/``@java_method``) needs the class ``org.jnius.NativeInvocationHandler``. A ``.whl`` carries no ``.dex`` and a class in ``site-packages`` is **not** on ART's classpath, so the wheel does not and cannot deliver it. The packager (bootstrap/build system) must compile and dex ``org.jnius.NativeInvocationHandler`` into the APK. Plain ``autoclass`` and method calls need no glue; only proxies do. .. warning:: **Matched-pair coupling.** The wheel's native ``invoke0`` contract (bound at runtime via ``RegisterNatives``) and the packager's copy of ``NativeInvocationHandler.java`` must be updated together. Nothing enforces this once the glue is out of the wheel -- a mismatch surfaces as a ``ClassNotFound``/``NoSuchMethod`` at first proxy creation. The canonical source lives in the PyJNIus repository at ``jnius/src/org/jnius/NativeInvocationHandler.java``. If no JVM/glue is found, the first ``import jnius`` raises a clear ``RuntimeError`` naming the missing getter, rather than a cryptic dlopen/symbol failure. Building the wheel ------------------ Build on a Linux ``x86_64`` (WSL2 counts) or macOS host with an Android SDK; `cibuildwheel `_ drives the NDK via ``sdkmanager``. CI pins the cibuildwheel action in ``.github/workflows/android-wheels.yml``; the selected Android Python bundles specify the NDK version. Android build settings live in ``[tool.cibuildwheel.android]`` in ``pyproject.toml``; its ``before-build`` check rejects a compiler from an unexpected NDK version. .. code:: bash # 1. Android compiles a *pre-generated* jnius.c (Cython is build-only and not # needed on the target). config.pxi selects the android include branch. printf "DEF JNIUS_PLATFORM = 'android'\n" > jnius/config.pxi cython -3 jnius/jnius.pyx -o jnius/jnius.c # 2. Build from a CLEAN tree. `python -m build --no-isolation` reuses # build/lib.android-*; a stale one from an earlier build re-injects the # Java payload and breaks the Java-free guarantee. A fresh checkout (CI) is # already clean. rm -rf build # 3. arm64_v8a ships to devices; x86_64 is the testability ABI (cibuildwheel's # emulator testbed only runs the build-host arch). cibuildwheel --only cp314-android_arm64_v8a --output-dir wheelhouse cibuildwheel --only cp314-android_x86_64 --output-dir wheelhouse The frontend must be ``build``/``uv`` (Android does not support the ``pip`` frontend); this is set in ``pyproject.toml``. CPython 3.15 (pre-release until Oct 2026) additionally needs ``--enable cpython-prerelease``. **16 KB page alignment.** Android 15/16 requires native ``.so``\ s to have 16 KB-aligned LOAD segments. The build pins ``LDFLAGS = "-Wl,-z,max-page-size=16384"`` in ``[tool.cibuildwheel.android]`` so the wheel is 16 KB-aligned independent of the NDK's default. This aligns only PyJNIus's ``.so`` -- the packager is responsible for aligning the bootstrap libs (SDL, libpython, ...) and for 16 KB zip-aligning the final APK. Verifying a built wheel ---------------------- .. code:: bash # unzip the wheel, then, with the NDK's llvm-readelf: llvm-readelf -d jnius/jnius*.so | grep NEEDED # no libSDL / libnativehelper / libart llvm-readelf -l jnius/jnius*.so | grep LOAD # Align column == 0x4000 (16 KB) llvm-readelf --dyn-syms jnius/jnius*.so | grep -E 'dlopen|dlsym' # runtime-resolved The wheel should contain only the ``.so`` and the Python modules -- no ``src/org``, ``.java`` or ``.class``.