From 71dba8a05e69425241eacb207251bc182dd700e6 Mon Sep 17 00:00:00 2001 From: Maxime Barbier Date: Tue, 29 Sep 2026 11:26:08 +0200 Subject: [PATCH] Add/fix several recipes (numpy, cryptography, libthorvg, vosk, srt, wikipedia + more) A batch of recipe additions/fixes accumulated while building a Kivy/KivyMD Android app against a fairly large, varied dependency set. ## numpy: bump v2.3.0 -> v2.3.5 Fixes an NDK/libc++ build failure (missing include) present in v2.3.0's meson build. ## cryptography: fix cross-compiling cryptography-cffi's build.rs The cryptography-cffi Rust crate's build.rs needs the *target* Python's include dir to compile the cffi-generated _openssl.c, and needs a working C compiler for its target. Two fixes to RustCompiledComponentsRecipe make this work generally, not just for cryptography: - get_recipe_env() now also sets a per-target CC_/AR_ pair (in addition to the existing CARGO_TARGET_*_LINKER): crates using the `cc` crate in their build.rs fall back to guessing --target= from the Rust triple without it, which for some targets (e.g. armv7-linux-androideabi) produces an invalid triple, picking up the wrong and breaking CPython's LONG_BIT sanity check in pyport.h. - MesonRecipe.ensure_args() no longer mutates the inherited class-level extra_build_args list in place, which would otherwise bleed one recipe's meson cross-file args into unrelated PyProjectRecipe subclasses sharing the same class attribute. cryptography's own recipe gets a small patch (via a new P4A_PYTHON_INCLUDE_DIR env var read from build.rs) to prefer the target's include dir over introspecting the host Python for it. ## libthorvg: fix libomp.so glob for both lib/clang and lib64/clang NDK layouts The glob only matched one of the two directory layouts different NDK versions use, raising a bare IndexError when it found nothing instead of a clear BuildInterruptingException. ## vosk: fix Android runtime crash; add srt, wikipedia recipes vosk imports srt unconditionally (not just from its optional CLI tool), and srt has no wheel on PyPI -- without a recipe of its own it was silently dropped, and the app crashed at runtime with ModuleNotFoundError. Same story for wikipedia, a plain build blocker (pip install has nothing to build it with, no wheel on PyPI either). vosk also needed: - open_dll() patched to recognize sys.platform == "android" (only "linux" and a few others were recognized, so vosk's own bundled native library loader didn't find itself on Android) - --upgrade --force-reinstall on its own pip install step, since a `pip install .` with a stale but present install can otherwise no-op ("Requirement already satisfied") instead of picking up a rebuild. ## recipes: add/update several generic recipes New recipes: libonnxruntime, MarkupSafe, murmurhash, onnxruntime, preshed, sherpa-ncnn, sherpa-onnx, spacy, srsly, thinc, kivymd, blis, cymem, bitstring, lifxlan, jsonschema. Updates: freetype-py (fall back to distutils when needed), liblzma, pycairo (meson patch), regex -- version/patch bumps picked up while building against this set of recipes. Kept jsonschema, bitstring and lifxlan despite them shipping a universal wheel themselves: their own dependency chains need something that doesn't -- - jsonschema needs rpds-py, Rust-compiled, no universal wheel, no recipe of its own yet. - bitstring's current PyPI releases need bitarray and tibs, both compiled; this recipe pins the last version that doesn't (3.1.9), rather than building anything. - lifxlan pulls in bitstring unconstrained in its own setup.py. Without this recipe (or without --no-deps carving out an exception), pip would resolve a bitstring version needing bitarray, which doesn't build/run correctly cross-compiled -- reproduced as a wrong-arch .so crashing at runtime (dlopen failed: ... EM_X86_64 ... instead of EM_AARCH64) while building the app this PR is drawn from. Deliberately left out several other packages initially considered: materialshapes, meteofrance-api, mpmath, pydantic, pywizlight, simplemma, sympy, tibs. All ship a universal wheel, and none of their own dependencies need native compilation or lack one either, so a recipe adds nothing over listing them in an app's requirements (or another recipe's python_depends) -- same as colorama/certifi/idna/ urllib3 etc. already are. pydantic-core (the compiled Rust part pydantic pins) already has its own recipe. ## Testing Every recipe here has been exercised by a real, full clean Android build (arm64-v8a + armeabi-v7a) of a Kivy/KivyMD app with a fairly large dependency set (the app also uses sherpa-onnx, vosk, cryptography, numpy, kivymd, and several of the newly-added recipes directly) -- BUILD SUCCESSFUL, APK installs and runs correctly on-device. Co-Authored-By: Claude Sonnet 5 --- pythonforandroid/recipe.py | 24 ++++- .../recipes/MarkupSafe/__init__.py | 16 ++++ .../recipes/bitstring/__init__.py | 12 +++ pythonforandroid/recipes/blis/__init__.py | 50 ++++++++++ .../recipes/cryptography/__init__.py | 20 +++- .../android-cross-include-dir.patch | 38 ++++++++ pythonforandroid/recipes/cymem/__init__.py | 16 ++++ .../freetype-py/fall-back-to-distutils.patch | 15 +++ .../recipes/jsonschema/__init__.py | 28 ++++++ pythonforandroid/recipes/kivymd/__init__.py | 21 ++++ pythonforandroid/recipes/kivymd/kv.patch | 8 ++ pythonforandroid/recipes/liblzma/__init__.py | 2 +- .../recipes/libonnxruntime/__init__.py | 53 ++++++++++ .../patches/cse_arm32_alignment.patch | 73 ++++++++++++++ .../libonnxruntime/patches/mlasi_bfloat.patch | 14 +++ .../recipes/libthorvg/__init__.py | 18 +++- pythonforandroid/recipes/lifxlan/__init__.py | 12 +++ .../recipes/murmurhash/__init__.py | 16 ++++ pythonforandroid/recipes/numpy/__init__.py | 2 +- .../recipes/onnxruntime/__init__.py | 96 +++++++++++++++++++ .../onnxruntime/patches/cmakelists.patch | 10 ++ .../onnxruntime/patches/mlasi_bfloat.patch | 14 +++ .../onnxruntime/patches/onnx_numpy.patch | 11 +++ pythonforandroid/recipes/preshed/__init__.py | 18 ++++ pythonforandroid/recipes/pycairo/__init__.py | 4 +- pythonforandroid/recipes/pycairo/meson.patch | 24 ++++- pythonforandroid/recipes/regex/__init__.py | 2 +- .../recipes/sherpa-ncnn/__init__.py | 48 ++++++++++ .../recipes/sherpa-onnx/__init__.py | 58 +++++++++++ pythonforandroid/recipes/spacy/__init__.py | 70 ++++++++++++++ pythonforandroid/recipes/srsly/__init__.py | 17 ++++ pythonforandroid/recipes/srt/__init__.py | 17 ++++ pythonforandroid/recipes/thinc/__init__.py | 43 +++++++++ pythonforandroid/recipes/vosk/__init__.py | 37 +++++-- .../recipes/vosk/android-platform.patch | 14 +++ .../recipes/wikipedia/__init__.py | 16 ++++ 36 files changed, 913 insertions(+), 24 deletions(-) create mode 100644 pythonforandroid/recipes/MarkupSafe/__init__.py create mode 100644 pythonforandroid/recipes/bitstring/__init__.py create mode 100644 pythonforandroid/recipes/blis/__init__.py create mode 100644 pythonforandroid/recipes/cryptography/android-cross-include-dir.patch create mode 100644 pythonforandroid/recipes/cymem/__init__.py create mode 100644 pythonforandroid/recipes/freetype-py/fall-back-to-distutils.patch create mode 100644 pythonforandroid/recipes/jsonschema/__init__.py create mode 100644 pythonforandroid/recipes/kivymd/__init__.py create mode 100644 pythonforandroid/recipes/kivymd/kv.patch create mode 100644 pythonforandroid/recipes/libonnxruntime/__init__.py create mode 100644 pythonforandroid/recipes/libonnxruntime/patches/cse_arm32_alignment.patch create mode 100644 pythonforandroid/recipes/libonnxruntime/patches/mlasi_bfloat.patch create mode 100644 pythonforandroid/recipes/lifxlan/__init__.py create mode 100644 pythonforandroid/recipes/murmurhash/__init__.py create mode 100644 pythonforandroid/recipes/onnxruntime/__init__.py create mode 100644 pythonforandroid/recipes/onnxruntime/patches/cmakelists.patch create mode 100644 pythonforandroid/recipes/onnxruntime/patches/mlasi_bfloat.patch create mode 100644 pythonforandroid/recipes/onnxruntime/patches/onnx_numpy.patch create mode 100644 pythonforandroid/recipes/preshed/__init__.py create mode 100644 pythonforandroid/recipes/sherpa-ncnn/__init__.py create mode 100644 pythonforandroid/recipes/sherpa-onnx/__init__.py create mode 100644 pythonforandroid/recipes/spacy/__init__.py create mode 100644 pythonforandroid/recipes/srsly/__init__.py create mode 100644 pythonforandroid/recipes/srt/__init__.py create mode 100644 pythonforandroid/recipes/thinc/__init__.py create mode 100644 pythonforandroid/recipes/vosk/android-platform.patch create mode 100644 pythonforandroid/recipes/wikipedia/__init__.py diff --git a/pythonforandroid/recipe.py b/pythonforandroid/recipe.py index 1d5e3e9104..a73ecd93c0 100644 --- a/pythonforandroid/recipe.py +++ b/pythonforandroid/recipe.py @@ -1563,6 +1563,10 @@ def write_build_options(self, arch): return option_data def ensure_args(self, *args): + # Guard against mutating the inherited class-variable list, which would + # bleed meson cross-file args into unrelated PyProjectRecipe subclasses. + if "extra_build_args" not in self.__dict__: + self.extra_build_args = list(self.extra_build_args) for arg in args: if arg not in self.extra_build_args: self.extra_build_args.append(arg) @@ -1619,11 +1623,8 @@ def get_recipe_env(self, arch, **kwargs): # Set rust build target build_target = self.RUST_ARCH_CODES[arch.arch] - cargo_linker_name = "CARGO_TARGET_{}_LINKER".format( - build_target.upper().replace("-", "_") - ) - env["CARGO_BUILD_TARGET"] = build_target - env[cargo_linker_name] = join( + build_target_underscore = build_target.upper().replace("-", "_") + ndk_clang = join( self.ctx.ndk.llvm_prebuilt_dir, "bin", "{}{}-clang".format( @@ -1634,6 +1635,19 @@ def get_recipe_env(self, arch, **kwargs): self.ctx.ndk_api, ), ) + env["CARGO_BUILD_TARGET"] = build_target + env["CARGO_TARGET_{}_LINKER".format(build_target_underscore)] = ndk_clang + # Crates using the `cc` crate in their build.rs (e.g. cryptography-cffi + # to compile the cffi-generated _openssl.c) need a per-target CC too: + # without it, `cc` falls back to the generic CC env var and guesses + # its own --target= from the Rust triple, which for some targets + # (e.g. armv7-linux-androideabi) produces an invalid triple like + # "armv7-none-linux-android" (no API level, wrong vendor). That + # causes the wrong to be picked up, breaking CPython's + # LONG_BIT sanity check in pyport.h. Giving `cc` the NDK's + # per-target clang directly avoids that guesswork. + env["CC_{}".format(build_target.replace("-", "_"))] = ndk_clang + env["AR_{}".format(build_target.replace("-", "_"))] = self.ctx.ndk.llvm_ar realpython_dir = self.ctx.python_recipe.get_build_dir(arch.arch) env["RUSTFLAGS"] = "-Clink-args=-L{} -L{}".format( diff --git a/pythonforandroid/recipes/MarkupSafe/__init__.py b/pythonforandroid/recipes/MarkupSafe/__init__.py new file mode 100644 index 0000000000..b295beb632 --- /dev/null +++ b/pythonforandroid/recipes/MarkupSafe/__init__.py @@ -0,0 +1,16 @@ +from pythonforandroid.recipe import CompiledComponentsPythonRecipe + + +class MarkupSafeRecipe(CompiledComponentsPythonRecipe): + version = "2.1.5" + url = "https://pypi.python.org/packages/source/M/MarkupSafe/MarkupSafe-{version}.tar.gz" + depends = [ + "setuptools", + "cython", + "pyparsing", + ] + call_hostpython_via_targetpython = False + install_in_hostpython = True + + +recipe = MarkupSafeRecipe() diff --git a/pythonforandroid/recipes/bitstring/__init__.py b/pythonforandroid/recipes/bitstring/__init__.py new file mode 100644 index 0000000000..92058cbbfd --- /dev/null +++ b/pythonforandroid/recipes/bitstring/__init__.py @@ -0,0 +1,12 @@ +from pythonforandroid.recipe import PythonRecipe + + +class BitstringRecipe(PythonRecipe): + name = "bitstring" + version = "3.1.9" + url = "https://pypi.python.org/packages/source/b/bitstring/bitstring-{version}.tar.gz" + depends = ["setuptools"] + call_hostpython_via_targetpython = False + + +recipe = BitstringRecipe() diff --git a/pythonforandroid/recipes/blis/__init__.py b/pythonforandroid/recipes/blis/__init__.py new file mode 100644 index 0000000000..863356d913 --- /dev/null +++ b/pythonforandroid/recipes/blis/__init__.py @@ -0,0 +1,50 @@ +from pythonforandroid.archs import Archx86_64 +from pythonforandroid.recipe import CompiledComponentsPythonRecipe + + +class BlisRecipe(CompiledComponentsPythonRecipe): + version = "1.2.1" + url = "https://github.com/explosion/cython-blis/archive/refs/tags/release-v{version}.tar.gz" + depends = [ + "setuptools", + "cython", + ] + hostpython_prerequisites = [ # build only + "numpy", + ] + call_hostpython_via_targetpython = False + install_in_hostpython = True + + def get_recipe_env(self, arch=None, with_flags_in_cc=False): + env = super().get_recipe_env(arch, with_flags_in_cc) + + # see https://github.com/flame/blis/blob/master/config_registry + # if isinstance(arch, ArchAarch_64): + # env["BLIS_ARCH"] = "arm64" + if isinstance(arch, Archx86_64): + env["BLIS_ARCH"] = "x86_64" + else: + env["BLIS_ARCH"] = "generic" + + if "CC" in env: + cli = env["CC"].split()[0] + ccache_bin = cli if "ccache" in cli else "" + + env["BLIS_COMPILER"] = " ".join( + [ + ccache_bin, + arch.get_clang_exe(with_target=True), + ] + ) + + return env + + def get_hostrecipe_env(self, arch): + env = super().get_hostrecipe_env(arch) + # see https://github.com/explosion/cython-blis/blob/bdb10be0698002e1d93f79cd91fc1a38f0da3fb3/setup.py#L290 + env["BLIS_COMPILER"] = "/usr/bin/ccache gcc" + + return env + + +recipe = BlisRecipe() diff --git a/pythonforandroid/recipes/cryptography/__init__.py b/pythonforandroid/recipes/cryptography/__init__.py index e54eaa1bd8..ecf2f56024 100644 --- a/pythonforandroid/recipes/cryptography/__init__.py +++ b/pythonforandroid/recipes/cryptography/__init__.py @@ -8,6 +8,8 @@ class CryptographyRecipe(RustCompiledComponentsRecipe): version = '46.0.3' url = 'https://github.com/pyca/cryptography/archive/refs/tags/{version}.tar.gz' depends = ['openssl', 'cffi'] + hostpython_prerequisites = ['maturin', 'cffi'] + patches = ['android-cross-include-dir.patch'] def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) @@ -17,7 +19,23 @@ def get_recipe_env(self, arch, **kwargs): openssl_libs = "{}_OPENSSL_LIB_DIR".format(build_target) env[openssl_include] = join(openssl_build_dir, 'include') env[openssl_libs] = join(openssl_build_dir) + env["ANDROID_API_LEVEL"] = str(self.ctx.ndk_api) + # cryptography-cffi's build.rs (see android-cross-include-dir.patch) + # asks the *host* python for its own include_dirs via setuptools, + # which is wrong when cross-compiling: it returns hostpython3's + # native Python.h instead of the Android target's, breaking + # CPython's LONG_BIT sanity check in pyport.h. + env["P4A_PYTHON_INCLUDE_DIR"] = self.ctx.python_recipe.include_root(arch.arch) + # Android's bionic linker only resolves symbols from explicitly listed + # NEEDED libraries. abi3 extensions don't link against libpython, so + # _Py_NoneStruct (and other Python symbols) can't be found at dlopen + # time. Explicitly link against libpython so Android resolves them. + python_ver = self.python_major_minor_version + env["RUSTFLAGS"] = ( + env.get("RUSTFLAGS", "") + + " -Clink-args=-lpython{}".format(python_ver) + ) return env -recipe = CryptographyRecipe() +recipe = CryptographyRecipe() \ No newline at end of file diff --git a/pythonforandroid/recipes/cryptography/android-cross-include-dir.patch b/pythonforandroid/recipes/cryptography/android-cross-include-dir.patch new file mode 100644 index 0000000000..1448f76e1f --- /dev/null +++ b/pythonforandroid/recipes/cryptography/android-cross-include-dir.patch @@ -0,0 +1,38 @@ +--- a/src/rust/cryptography-cffi/build.rs 2026-09-28 02:01:44.655202871 +0200 ++++ b/src/rust/cryptography-cffi/build.rs 2026-09-28 02:01:44.658202904 +0200 +@@ -47,16 +47,25 @@ + ) + .unwrap(); + println!("cargo:rustc-cfg=python_implementation=\"{python_impl}\""); +- let python_includes = run_python_script( +- &python, +- "import os; \ +- import setuptools.dist; \ +- import setuptools.command.build_ext; \ +- b = setuptools.command.build_ext.build_ext(setuptools.dist.Distribution()); \ +- b.finalize_options(); \ +- print(os.pathsep.join(b.include_dirs), end='')", +- ) +- .unwrap(); ++ // p4a: when cross-compiling for Android, `python` here is the host ++ // (native, non-Android) Python used to run build tooling -- asking it ++ // for its own include_dirs via setuptools returns the *host's* ++ // Python.h, not the Android target's, which fails CPython's LONG_BIT ++ // sanity check when compiled for a different word size/ABI. Let the ++ // p4a recipe override this explicitly with the target's include dir. ++ let python_includes = match env::var("P4A_PYTHON_INCLUDE_DIR") { ++ Ok(dir) => dir, ++ Err(_) => run_python_script( ++ &python, ++ "import os; \ ++ import setuptools.dist; \ ++ import setuptools.command.build_ext; \ ++ b = setuptools.command.build_ext.build_ext(setuptools.dist.Distribution()); \ ++ b.finalize_options(); \ ++ print(os.pathsep.join(b.include_dirs), end='')", ++ ) ++ .unwrap(), ++ }; + let openssl_c = Path::new(&out_dir).join("_openssl.c"); + + let mut build = cc::Build::new(); diff --git a/pythonforandroid/recipes/cymem/__init__.py b/pythonforandroid/recipes/cymem/__init__.py new file mode 100644 index 0000000000..fb0cb03a4d --- /dev/null +++ b/pythonforandroid/recipes/cymem/__init__.py @@ -0,0 +1,16 @@ +from pythonforandroid.recipe import CompiledComponentsPythonRecipe + + +class CymemRecipe(CompiledComponentsPythonRecipe): + version = "2.0.11" + url = "https://pypi.python.org/packages/source/c/cymem/cymem-{version}.tar.gz" + site_packages_name = "cymem" + depends = [ + "setuptools", + "cython", + ] + call_hostpython_via_targetpython = False + install_in_hostpython = True + + +recipe = CymemRecipe() diff --git a/pythonforandroid/recipes/freetype-py/fall-back-to-distutils.patch b/pythonforandroid/recipes/freetype-py/fall-back-to-distutils.patch new file mode 100644 index 0000000000..0f06f1854a --- /dev/null +++ b/pythonforandroid/recipes/freetype-py/fall-back-to-distutils.patch @@ -0,0 +1,15 @@ +diff -ruN freetype-py.orig/setup.py freetype-py/setup.py +--- freetype-py.orig/setup.py 2020-07-09 20:58:51.000000000 +0700 ++++ freetype-py/setup.py 2022-03-02 19:28:17.948831134 +0700 +@@ -12,7 +12,10 @@ + from io import open + from os import path + +-from setuptools import setup ++try: ++ from setuptools import setup ++except ImportError: ++ from distutils.core import setup + + if os.environ.get("FREETYPEPY_BUNDLE_FT"): + print("# Will build and bundle FreeType.") diff --git a/pythonforandroid/recipes/jsonschema/__init__.py b/pythonforandroid/recipes/jsonschema/__init__.py new file mode 100644 index 0000000000..faab595ddb --- /dev/null +++ b/pythonforandroid/recipes/jsonschema/__init__.py @@ -0,0 +1,28 @@ +import os +import sys + +import sh + +from pythonforandroid.logger import shprint +from pythonforandroid.recipe import PythonRecipe + + +class JsonschemaRecipe(PythonRecipe): + name = "jsonschema" + version = "4.17.3" + url = None + depends = [] + + def build_arch(self, arch): + env = {k: v for k, v in os.environ.items() + if k not in ("PYTHONHOME", "PYTHONPATH", "PYTHONNOUSERSITE")} + shprint( + sh.Command(sys.executable), "-m", "pip", "install", + f"{self.name}=={self.version}", + "--target", self.ctx.get_python_install_dir(arch.arch), + "--only-binary", ":all:", + _env=env, + ) + + +recipe = JsonschemaRecipe() diff --git a/pythonforandroid/recipes/kivymd/__init__.py b/pythonforandroid/recipes/kivymd/__init__.py new file mode 100644 index 0000000000..d6c2e94df0 --- /dev/null +++ b/pythonforandroid/recipes/kivymd/__init__.py @@ -0,0 +1,21 @@ +from pythonforandroid.recipe import PythonRecipe + + +class kivyMDRecipe(PythonRecipe): + version = "365aa9b96eee63e0e29c04de297dd222f478fce5" + url = "https://github.com/kivymd/KivyMD/archive/{version}.zip" + + depends = [ + "kivy", + "materialyoucolor", + "exceptiongroup", + "asyncgui", + "asynckivy", + ] + site_packages_name = "kivyMD" + patches = ["kv.patch"] + call_hostpython_via_targetpython = False + install_in_hostpython = True + + +recipe = kivyMDRecipe() diff --git a/pythonforandroid/recipes/kivymd/kv.patch b/pythonforandroid/recipes/kivymd/kv.patch new file mode 100644 index 0000000000..14f8814f80 --- /dev/null +++ b/pythonforandroid/recipes/kivymd/kv.patch @@ -0,0 +1,8 @@ +--- a/setup.py ++++ b/setup.py +@@ -80,4 +80,4 @@ + try: +- out_files.append(filepath.split(f"kivymd{os.sep}")[1]) ++ out_files.append(filepath.split(f"kivymd{os.sep}")[-1]) + except IndexError: + out_files.append(filepath) diff --git a/pythonforandroid/recipes/liblzma/__init__.py b/pythonforandroid/recipes/liblzma/__init__.py index 0b880bc484..1c1e0ecaef 100644 --- a/pythonforandroid/recipes/liblzma/__init__.py +++ b/pythonforandroid/recipes/liblzma/__init__.py @@ -12,7 +12,7 @@ class LibLzmaRecipe(Recipe): version = '5.2.4' - url = 'https://tukaani.org/xz/xz-{version}.tar.gz' + url = 'https://github.com/tukaani-project/xz/archive/refs/tags/v{version}.tar.gz' built_libraries = {'liblzma.so': 'p4a_install/lib'} def build_arch(self, arch: Arch) -> None: diff --git a/pythonforandroid/recipes/libonnxruntime/__init__.py b/pythonforandroid/recipes/libonnxruntime/__init__.py new file mode 100644 index 0000000000..da58bab078 --- /dev/null +++ b/pythonforandroid/recipes/libonnxruntime/__init__.py @@ -0,0 +1,53 @@ +from multiprocessing import cpu_count +from os.path import join + +import sh + +from pythonforandroid.util import current_directory, ensure_dir +from pythonforandroid.logger import shprint +from pythonforandroid.recipe import Recipe + + +class LibOnnxRuntimeRecipe(Recipe): + name = "libonnxruntime" + version = '1.24.4' + url = 'https://github.com/microsoft/onnxruntime/archive/refs/tags/v{version}.tar.gz' + built_libraries = {'libonnxruntime.so': 'build'} + patches = ['patches/mlasi_bfloat.patch', 'patches/cse_arm32_alignment.patch'] + + def build_arch(self, arch): + source_dir = self.get_build_dir(arch.arch) + build_dir = join(source_dir, 'build') + cmake_dir = join(source_dir, 'cmake') + toolchain_file = join( + self.ctx.ndk_dir, 'build', 'cmake', 'android.toolchain.cmake', + ) + + ensure_dir(build_dir) + with current_directory(build_dir): + env = self.get_recipe_env(arch) + shprint(sh.cmake, cmake_dir, + f'-DANDROID_ABI={arch.arch}', + f'-DANDROID_NATIVE_API_LEVEL={self.ctx.ndk_api}', + f'-DCMAKE_TOOLCHAIN_FILE={toolchain_file}', + '-DCMAKE_BUILD_TYPE=Release', + '-Donnxruntime_BUILD_SHARED_LIB=ON', + '-Donnxruntime_ENABLE_PYTHON=OFF', + '-Donnxruntime_BUILD_UNIT_TESTS=OFF', + '-Donnxruntime_USE_NNAPI_BUILTIN=ON', + _env=env) + shprint(sh.make, f'-j{cpu_count()}', _env=env) + + def get_include_dir(self, arch): + """Headers dir containing onnxruntime_c_api.h for sherpa-onnx.""" + return join( + self.get_build_dir(arch.arch), + 'include', 'onnxruntime', 'core', 'session', + ) + + def get_lib_dir(self, arch): + """Dir containing libonnxruntime.so.""" + return join(self.get_build_dir(arch.arch), 'build') + + +recipe = LibOnnxRuntimeRecipe() diff --git a/pythonforandroid/recipes/libonnxruntime/patches/cse_arm32_alignment.patch b/pythonforandroid/recipes/libonnxruntime/patches/cse_arm32_alignment.patch new file mode 100644 index 0000000000..0d5f016994 --- /dev/null +++ b/pythonforandroid/recipes/libonnxruntime/patches/cse_arm32_alignment.patch @@ -0,0 +1,73 @@ +--- a/onnxruntime/core/optimizer/common_subexpression_elimination.cc ++++ b/onnxruntime/core/optimizer/common_subexpression_elimination.cc +@@ -172,29 +172,18 @@ bool AreRangesEqual(const Range& lhs, const Range& rhs) { + } + + // Check if two tensor attributes are equal scalar tensors, mainly to support ConstantOfShape Op. +-// Currently support float, float16 and int64 data types, and requires the data are raw data in TensorProto. + bool AreScalarTensorAttributeEqual(const ONNX_NAMESPACE::TensorProto& lhs_t, const ONNX_NAMESPACE::TensorProto& rhs_t) { + if (!(utils::HasDataType(lhs_t) && utils::HasDataType(rhs_t) && lhs_t.data_type() == rhs_t.data_type() && +- (lhs_t.data_type() == onnx::TensorProto_DataType_FLOAT || +- lhs_t.data_type() == onnx::TensorProto_DataType_FLOAT16 || +- lhs_t.data_type() == onnx::TensorProto_DataType_INT64) && +- lhs_t.dims_size() == 1 && rhs_t.dims_size() == 1 && lhs_t.dims()[0] == 1 && rhs_t.dims()[0] == 1 && +- utils::HasRawData(lhs_t) && utils::HasRawData(rhs_t))) { ++ lhs_t.data_type() != onnx::TensorProto_DataType_STRING && ++ lhs_t.dims_size() == 1 && rhs_t.dims_size() == 1 && lhs_t.dims()[0] == 1 && rhs_t.dims()[0] == 1)) { + return false; + } +- const void* lhs_value = lhs_t.raw_data().data(); +- const void* rhs_value = rhs_t.raw_data().data(); +- switch (lhs_t.data_type()) { +- case onnx::TensorProto_DataType_FLOAT: +- return *reinterpret_cast(lhs_value) == *reinterpret_cast(rhs_value); +- case onnx::TensorProto_DataType_FLOAT16: +- return *reinterpret_cast(lhs_value) == *reinterpret_cast(rhs_value); +- case onnx::TensorProto_DataType_INT64: +- return *reinterpret_cast(lhs_value) == *reinterpret_cast(rhs_value); +- default: +- break; ++ std::vector unpacked_lhs_tensor, unpacked_rhs_tensor; ++ if (!utils::UnpackInitializerData(lhs_t, unpacked_lhs_tensor).IsOK() || ++ !utils::UnpackInitializerData(rhs_t, unpacked_rhs_tensor).IsOK()) { ++ return false; + } +- return false; ++ return unpacked_lhs_tensor == unpacked_rhs_tensor; + } + + bool AreEqual(const ONNX_NAMESPACE::AttributeProto& lhs, const ONNX_NAMESPACE::AttributeProto& rhs) { +@@ -235,26 +224,16 @@ bool AreEqual(const ONNX_NAMESPACE::AttributeProto& lhs, const ONNX_NAMESPACE:: + return false; + } + +-// Support scalar float/int64/fp16 tensor attribute only for now, and requires data is raw data in TensorProto. ++// Support scalar tensor attribute only for now. + std::size_t GetTensorAttributeHash(const ONNX_NAMESPACE::TensorProto& attr_t) { + std::size_t hash = 0; +- if (utils::HasDataType(attr_t) && attr_t.dims_size() == 1 && attr_t.dims()[0] == 1 && utils::HasRawData(attr_t)) { ++ if (utils::HasDataType(attr_t) && attr_t.dims_size() == 1 && attr_t.dims()[0] == 1) { + int data_type = attr_t.data_type(); +- switch (data_type) { +- case onnx::TensorProto_DataType_FLOAT: +- UpdateHash(data_type, hash); +- UpdateHash(*reinterpret_cast(attr_t.raw_data().data()), hash); +- break; +- case onnx::TensorProto_DataType_FLOAT16: +- UpdateHash(data_type, hash); +- UpdateHash(static_cast(*reinterpret_cast(attr_t.raw_data().data())), hash); +- break; +- case onnx::TensorProto_DataType_INT64: +- UpdateHash(data_type, hash); +- UpdateHash(*reinterpret_cast(attr_t.raw_data().data()), hash); +- break; +- default: +- break; ++ ORT_ENFORCE(data_type != onnx::TensorProto_DataType_STRING, "Unexpected tensor string type"); ++ std::vector unpacked_tensor; ++ if (utils::UnpackInitializerData(attr_t, unpacked_tensor).IsOK()) { ++ UpdateHash(data_type, hash); ++ UpdateHashWithContainer(unpacked_tensor, hash); + } + } + return hash; diff --git a/pythonforandroid/recipes/libonnxruntime/patches/mlasi_bfloat.patch b/pythonforandroid/recipes/libonnxruntime/patches/mlasi_bfloat.patch new file mode 100644 index 0000000000..011f895bfd --- /dev/null +++ b/pythonforandroid/recipes/libonnxruntime/patches/mlasi_bfloat.patch @@ -0,0 +1,14 @@ +--- a/onnxruntime/core/mlas/lib/mlasi.h ++++ b/onnxruntime/core/mlas/lib/mlasi.h +@@ -49,6 +49,11 @@ + #if defined(__arm__) || defined(__aarch64__) + #include + #endif ++ ++#if defined(__aarch64__) && !defined(__ARM_FEATURE_BF16) ++typedef uint16_t bfloat16_t; ++#endif ++ + #if defined(__x86_64__) || defined(__i386__) + #if !defined(signature_VORTEX_ebx) && !defined(signature_NEXGEN_ebx) && !defined(signature_AMD_ebx)//workaround for Bug 96238 - [i386] cpuid.h header needs include guards + #include \ No newline at end of file diff --git a/pythonforandroid/recipes/libthorvg/__init__.py b/pythonforandroid/recipes/libthorvg/__init__.py index c9eaefc90f..567fe93473 100644 --- a/pythonforandroid/recipes/libthorvg/__init__.py +++ b/pythonforandroid/recipes/libthorvg/__init__.py @@ -1,6 +1,8 @@ from pythonforandroid.recipe import Recipe, MesonRecipe from os.path import join -from pythonforandroid.util import ensure_dir, current_directory +from pythonforandroid.util import ( + BuildInterruptingException, ensure_dir, current_directory, +) from pythonforandroid.logger import shprint from multiprocessing import cpu_count from glob import glob @@ -93,9 +95,17 @@ def build_arch(self, arch): "x86_64": "x86_64", } lib_arch = arch_map[arch.arch] - # clang version directory is variable, so glob it - pattern = join(self.ctx.ndk.llvm_prebuilt_dir, "lib/clang/*/lib/linux", lib_arch) - clang_lib_dir = glob(pattern)[0] + # clang version directory is variable, and the NDK toolchain + # ships it under lib/clang on some NDK releases and lib64/clang + # on others (e.g. r25b), so glob both. + pattern = join(self.ctx.ndk.llvm_prebuilt_dir, "lib*/clang/*/lib/linux", lib_arch) + matches = glob(pattern) + if not matches: + raise BuildInterruptingException( + f"Could not find libomp.so for {lib_arch} under " + f"{self.ctx.ndk.llvm_prebuilt_dir} (tried pattern {pattern})" + ) + clang_lib_dir = matches[0] libomp = join(clang_lib_dir, "libomp.so") shprint(sh.cp, libomp, join("install", "lib")) diff --git a/pythonforandroid/recipes/lifxlan/__init__.py b/pythonforandroid/recipes/lifxlan/__init__.py new file mode 100644 index 0000000000..a49e4cf443 --- /dev/null +++ b/pythonforandroid/recipes/lifxlan/__init__.py @@ -0,0 +1,12 @@ +from pythonforandroid.recipe import PythonRecipe + + +class LifxlanRecipe(PythonRecipe): + name = "lifxlan" + version = "1.2.9" + url = "https://pypi.python.org/packages/source/l/lifxlan/lifxlan-{version}.tar.gz" + depends = ["setuptools", "bitstring", "ifaddr"] + call_hostpython_via_targetpython = False + + +recipe = LifxlanRecipe() diff --git a/pythonforandroid/recipes/murmurhash/__init__.py b/pythonforandroid/recipes/murmurhash/__init__.py new file mode 100644 index 0000000000..1aa4a3abcb --- /dev/null +++ b/pythonforandroid/recipes/murmurhash/__init__.py @@ -0,0 +1,16 @@ +from pythonforandroid.recipe import CompiledComponentsPythonRecipe + + +class MurmurhashRecipe(CompiledComponentsPythonRecipe): + version = "1.0.13" + url = "https://pypi.python.org/packages/source/m/murmurhash/murmurhash-{version}.tar.gz" + site_packages_name = "murmurhash" + depends = [ + "setuptools", + "cython", + ] + call_hostpython_via_targetpython = False + install_in_hostpython = True + + +recipe = MurmurhashRecipe() diff --git a/pythonforandroid/recipes/numpy/__init__.py b/pythonforandroid/recipes/numpy/__init__.py index e3f72f0ed2..e4ca772aca 100644 --- a/pythonforandroid/recipes/numpy/__init__.py +++ b/pythonforandroid/recipes/numpy/__init__.py @@ -8,7 +8,7 @@ class NumpyRecipe(MesonRecipe): - version = "v2.3.0" + version = "v2.3.5" url = "git+https://github.com/numpy/numpy" extra_build_args = ["-Csetup-args=-Dblas=none", "-Csetup-args=-Dlapack=none"] opt_depends = ["libopenblas"] diff --git a/pythonforandroid/recipes/onnxruntime/__init__.py b/pythonforandroid/recipes/onnxruntime/__init__.py new file mode 100644 index 0000000000..b96f9075cc --- /dev/null +++ b/pythonforandroid/recipes/onnxruntime/__init__.py @@ -0,0 +1,96 @@ +from pythonforandroid.recipe import PyProjectRecipe, Recipe +from pythonforandroid.toolchain import current_directory, shprint +from os.path import join, exists +import sh, sys +from multiprocessing import cpu_count + +class OnnxRuntimeRecipe(PyProjectRecipe): + version = "1.22.1" + url = "https://github.com/microsoft/onnxruntime/archive/refs/tags/v{version}.tar.gz" + + depends = ["setuptools", "wheel", "numpy", "protobuf", "pybind11"] + patches = [ + 'patches/onnx_numpy.patch', + 'patches/mlasi_bfloat.patch', + ] + build_in_src = True + + def get_recipe_env(self, arch=None): + env = super().get_recipe_env(arch) + python_include_dir = self.ctx.python_recipe.include_root(arch.arch) + print(f"Python include dir: {python_include_dir}") + env['CPPFLAGS'] += f' -Wno-unused-variable -I{python_include_dir}' + env['CXXFLAGS'] += f' -I{python_include_dir}' + env['CFLAGS'] += f' -I{python_include_dir}' + env["Python_INCLUDE_DIRS"] = python_include_dir + + # Make sure cross python/protoc are visible + print(f"Host Python: {self.ctx.hostpython}") + print(f"NDK Dir: {self.ctx.ndk_dir}") + return env + + def build_arch(self, arch): + + env = self.get_recipe_env(arch) + ANDROID_PLATFORM = str(self.ctx.ndk_api) + + build_dir = self.get_build_dir(arch.arch) + print(f"Build dir: {build_dir}") + cmake_dir = join(build_dir, "cmake") + capi_dir = join(build_dir, "onnxruntime", "capi") + dist_dir = join(build_dir, "dist") + py_build_dir = Recipe.get_recipe("hostpython3", self.ctx).get_build_dir(arch.arch) + print(f"Python build dir: {py_build_dir}") + #python_include_dir = join(py_build_dir, 'Include') # from build dir + python_include_dir = self.ctx.python_recipe.include_root(arch.arch) + pybind11_recipe = self.get_recipe('pybind11', self.ctx) + pybind11_include_dir = pybind11_recipe.get_include_dir(arch) + print(f"Python include dir: {python_include_dir}") + print(f"Does Python.h exist? {exists(join(python_include_dir, 'Python.h'))}") + python_link_root = self.ctx.python_recipe.link_root(arch.arch) + python_link_version = self.ctx.python_recipe.link_version + python_library = join(python_link_root, + 'libpython{}.so'.format(python_link_version)) + python_site_packages = self.ctx.get_site_packages_dir(arch) + python_include_numpy = join(python_site_packages, + 'numpy', 'core', 'include') # from python-installs dir + print(f"Python include numpy: {python_include_numpy}") + toolchain_file = join(self.ctx.ndk_dir, + 'build/cmake/android.toolchain.cmake') + protoc_path = sh.which("protoc") + python_path = self.ctx.hostpython + shprint(sh.mkdir, "-p", capi_dir) + shprint(sh.mkdir, "-p", dist_dir) + + cmake_args = [ + "cmake", + cmake_dir, + #f"-DCMAKE_INSTALL_PREFIX={build_dir}", + f"-DCMAKE_TOOLCHAIN_FILE={toolchain_file}", + f"-DANDROID_ABI={arch.arch}", + f"-DANDROID_PLATFORM={ANDROID_PLATFORM}", + #"-DCMAKE_POLICY_VERSION_MINIMUM=3.5", + "-Donnxruntime_ENABLE_PYTHON=ON", + "-Donnxruntime_BUILD_SHARED_LIB=OFF", + "-DPYBIND11_USE_CROSSCOMPILING=TRUE", + "-Donnxruntime_USE_NNAPI_BUILTIN=ON", + "-Donnxruntime_USE_XNNPACK=ON", + f"-DONNX_CUSTOM_PROTOC_EXECUTABLE=/usr/bin/protoc", + f"-DPython_NumPy_INCLUDE_DIR={python_include_numpy}", + f"-DPython_EXECUTABLE={python_path}", + f"-Dpybind11_INCLUDE_DIRS={pybind11_include_dir};{python_include_dir};{python_include_numpy}", + f"-DPython_LIBRARY={python_library}", + f"-DPython_LIBRARIES={python_library}", + "-DCMAKE_BUILD_TYPE=RELEASE", + "-Donnxruntime_BUILD_UNIT_TESTS=OFF", + ] + + with current_directory(build_dir): + # Build the cmake part before building the wheel + shprint(sh.Command("cmake"), *cmake_args, _env=env) + shprint(sh.make, '-j' + str(cpu_count()), _env=env) + + # Build & Install wheel into target python site-packages + super().build_arch(arch) + +recipe = OnnxRuntimeRecipe() diff --git a/pythonforandroid/recipes/onnxruntime/patches/cmakelists.patch b/pythonforandroid/recipes/onnxruntime/patches/cmakelists.patch new file mode 100644 index 0000000000..917c619b31 --- /dev/null +++ b/pythonforandroid/recipes/onnxruntime/patches/cmakelists.patch @@ -0,0 +1,10 @@ +--- a/cmake/CMakeLists.txt ++++ b/cmake/CMakeLists.txt +@@ -677,6 +677,7 @@ + if (onnxruntime_REQUIRE_PYTHON_EMBED_LIB) + find_package(Python 3.10 COMPONENTS Interpreter Development NumPy) + else() ++ find_package(Python 3.10 COMPONENTS Development) + find_package(Python 3.10 COMPONENTS Interpreter Development.Module NumPy) + endif() + message("Numpy version: ${Python_NumPy_VERSION}") diff --git a/pythonforandroid/recipes/onnxruntime/patches/mlasi_bfloat.patch b/pythonforandroid/recipes/onnxruntime/patches/mlasi_bfloat.patch new file mode 100644 index 0000000000..011f895bfd --- /dev/null +++ b/pythonforandroid/recipes/onnxruntime/patches/mlasi_bfloat.patch @@ -0,0 +1,14 @@ +--- a/onnxruntime/core/mlas/lib/mlasi.h ++++ b/onnxruntime/core/mlas/lib/mlasi.h +@@ -49,6 +49,11 @@ + #if defined(__arm__) || defined(__aarch64__) + #include + #endif ++ ++#if defined(__aarch64__) && !defined(__ARM_FEATURE_BF16) ++typedef uint16_t bfloat16_t; ++#endif ++ + #if defined(__x86_64__) || defined(__i386__) + #if !defined(signature_VORTEX_ebx) && !defined(signature_NEXGEN_ebx) && !defined(signature_AMD_ebx)//workaround for Bug 96238 - [i386] cpuid.h header needs include guards + #include \ No newline at end of file diff --git a/pythonforandroid/recipes/onnxruntime/patches/onnx_numpy.patch b/pythonforandroid/recipes/onnxruntime/patches/onnx_numpy.patch new file mode 100644 index 0000000000..1f936c9ab9 --- /dev/null +++ b/pythonforandroid/recipes/onnxruntime/patches/onnx_numpy.patch @@ -0,0 +1,11 @@ +--- a/cmake/onnxruntime_python.cmake ++++ b/cmake/onnxruntime_python_patch.cmake +@@ -205,7 +205,7 @@ + onnxruntime_common + onnxruntime_flatbuffers + ${pybind11_lib} +- Python::NumPy ++ ${Python_LIBRARIES} + ) + set(onnxruntime_pybind11_state_dependencies + ${onnxruntime_EXTERNAL_DEPENDENCIES} diff --git a/pythonforandroid/recipes/preshed/__init__.py b/pythonforandroid/recipes/preshed/__init__.py new file mode 100644 index 0000000000..65c94b0b77 --- /dev/null +++ b/pythonforandroid/recipes/preshed/__init__.py @@ -0,0 +1,18 @@ +from pythonforandroid.recipe import CompiledComponentsPythonRecipe + + +class PreshedRecipe(CompiledComponentsPythonRecipe): + version = "3.0.10" + url = "https://pypi.python.org/packages/source/p/preshed/preshed-{version}.tar.gz" + site_packages_name = "preshed" + depends = [ + "setuptools", + "cython", + "cymem", + "murmurhash", + ] + call_hostpython_via_targetpython = False + install_in_hostpython = True + + +recipe = PreshedRecipe() diff --git a/pythonforandroid/recipes/pycairo/__init__.py b/pythonforandroid/recipes/pycairo/__init__.py index 2e9474568b..354e77a96e 100644 --- a/pythonforandroid/recipes/pycairo/__init__.py +++ b/pythonforandroid/recipes/pycairo/__init__.py @@ -14,10 +14,12 @@ def build_arch(self, arch): include_path = join(self.get_recipe('libcairo', self.ctx).get_build_dir(arch), "install", "include", "cairo") lib_path = self.ctx.get_libs_dir(arch.arch) + python_include = self.ctx.python_recipe.include_root(arch.arch) - self.extra_build_args += [ + self.extra_build_args = type(self).extra_build_args + [ f'-Csetup-args=-Dcairo_include={include_path}', f'-Csetup-args=-Dcairo_lib={lib_path}', + f'-Csetup-args=-Dpython_include={python_include}', ] super().build_arch(arch) diff --git a/pythonforandroid/recipes/pycairo/meson.patch b/pythonforandroid/recipes/pycairo/meson.patch index aabbf5ae3f..f9ed380022 100644 --- a/pythonforandroid/recipes/pycairo/meson.patch +++ b/pythonforandroid/recipes/pycairo/meson.patch @@ -4,21 +4,39 @@ diff '--color=auto' -uNr pycairo-1.28.0/cairo/meson.build pycairo-1.28.0.mod/cai @@ -28,7 +28,10 @@ fs.copyfile(python_file, python_file) endforeach - + -cairo_dep = dependency('cairo', version: cair_version_req, required: cc.get_id() != 'msvc') +cairo_dep = declare_dependency( + include_directories: include_directories(get_option('cairo_include')), + link_args: ['-L' + get_option('cairo_lib'), '-lcairo'] +) - + if cc.get_id() == 'msvc' and not cairo_dep.found() if cc.has_header('cairo.h') +@@ -47,10 +50,16 @@ + if not for_wheel + install_headers('py3cairo.h', subdir: 'pycairo') + endif + ++python_inc_dirs = [] ++if get_option('python_include') != '' ++ python_inc_dirs += [include_directories(get_option('python_include'))] ++endif ++ + pyext = python.extension_module('_cairo', sources, + dependencies : [cairo_dep], ++ include_directories: python_inc_dirs, + install: true, + subdir : 'cairo', + c_args: pyext_c_args + main_c_args, + ) diff '--color=auto' -uNr pycairo-1.28.0/meson_options.txt pycairo-1.28.0.mod/meson_options.txt --- pycairo-1.28.0/meson_options.txt 2025-04-15 00:22:30.000000000 +0530 +++ pycairo-1.28.0.mod/meson_options.txt 2025-07-14 21:56:52.824191314 +0530 -@@ -1,3 +1,5 @@ +@@ -1,3 +1,6 @@ option('python', type : 'string', value : 'python3') option('tests', type : 'boolean', value : true, description : 'build unit tests') option('wheel', type : 'boolean', value : false, description : 'build for a Python wheel') +option('cairo_include', type: 'string', value: '', description: 'Path to cairo headers') +option('cairo_lib', type: 'string', value: '', description: 'Path to cairo libraries') ++option('python_include', type: 'string', value: '', description: 'Path to Python headers for cross-compilation') diff --git a/pythonforandroid/recipes/regex/__init__.py b/pythonforandroid/recipes/regex/__init__.py index 6ac914845f..25e5a32267 100644 --- a/pythonforandroid/recipes/regex/__init__.py +++ b/pythonforandroid/recipes/regex/__init__.py @@ -3,7 +3,7 @@ class RegexRecipe(CompiledComponentsPythonRecipe): name = 'regex' - version = '2019.06.08' + version = '2026.1.15' url = 'https://pypi.python.org/packages/source/r/regex/regex-{version}.tar.gz' # noqa depends = ['setuptools'] diff --git a/pythonforandroid/recipes/sherpa-ncnn/__init__.py b/pythonforandroid/recipes/sherpa-ncnn/__init__.py new file mode 100644 index 0000000000..9161c6de38 --- /dev/null +++ b/pythonforandroid/recipes/sherpa-ncnn/__init__.py @@ -0,0 +1,48 @@ +from multiprocessing import cpu_count +from os.path import join + +import sh + +from pythonforandroid.util import current_directory, ensure_dir +from pythonforandroid.logger import shprint +from pythonforandroid.recipe import Recipe + + +class SherpaNcnnRecipe(Recipe): + name = "sherpa-ncnn" + version = '2.1.15' + url = 'https://github.com/k2-fsa/sherpa-ncnn/archive/refs/tags/v{version}.tar.gz' + built_libraries = { + 'libncnn.so': 'build/install/lib', + 'libsherpa-ncnn-jni.so': 'build/install/lib', + } + + def build_arch(self, arch): + source_dir = self.get_build_dir(arch.arch) + build_dir = join(source_dir, 'build') + install_dir = join(build_dir, 'install') + toolchain_file = join( + self.ctx.ndk_dir, 'build', 'cmake', 'android.toolchain.cmake', + ) + + ensure_dir(build_dir) + with current_directory(build_dir): + env = self.get_recipe_env(arch) + shprint(sh.cmake, source_dir, + f'-DANDROID_ABI={arch.arch}', + f'-DANDROID_NATIVE_API_LEVEL={self.ctx.ndk_api}', + f'-DCMAKE_TOOLCHAIN_FILE={toolchain_file}', + f'-DCMAKE_INSTALL_PREFIX={install_dir}', + '-DCMAKE_BUILD_TYPE=Release', + '-DBUILD_SHARED_LIBS=ON', + '-DSHERPA_NCNN_ENABLE_PORTAUDIO=OFF', + '-DSHERPA_NCNN_ENABLE_BINARY=OFF', + '-DSHERPA_NCNN_ENABLE_TEST=OFF', + '-DSHERPA_NCNN_ENABLE_C_API=OFF', + '-DSHERPA_NCNN_ENABLE_GENERATE_INT8_SCALE_TABLE=OFF', + _env=env) + shprint(sh.make, f'-j{cpu_count()}', _env=env) + shprint(sh.make, 'install/strip', _env=env) + + +recipe = SherpaNcnnRecipe() diff --git a/pythonforandroid/recipes/sherpa-onnx/__init__.py b/pythonforandroid/recipes/sherpa-onnx/__init__.py new file mode 100644 index 0000000000..a64f280a8e --- /dev/null +++ b/pythonforandroid/recipes/sherpa-onnx/__init__.py @@ -0,0 +1,58 @@ +from multiprocessing import cpu_count +from os.path import join + +import sh + +from pythonforandroid.util import current_directory, ensure_dir +from pythonforandroid.logger import shprint +from pythonforandroid.recipe import Recipe + + +class SherpaOnnxRecipe(Recipe): + name = "sherpa-onnx" + version = '1.12.38' + url = 'https://github.com/k2-fsa/sherpa-onnx/archive/refs/tags/v{version}.tar.gz' + depends = ['libonnxruntime'] + built_libraries = { + 'libsherpa-onnx-jni.so': 'build/install/lib', + } + + def build_arch(self, arch): + source_dir = self.get_build_dir(arch.arch) + build_dir = join(source_dir, 'build') + install_dir = join(build_dir, 'install') + toolchain_file = join( + self.ctx.ndk_dir, 'build', 'cmake', 'android.toolchain.cmake', + ) + + ort_recipe = self.get_recipe('libonnxruntime', self.ctx) + ort_lib_dir = ort_recipe.get_lib_dir(arch) + ort_include_dir = ort_recipe.get_include_dir(arch) + + ensure_dir(build_dir) + with current_directory(build_dir): + env = self.get_recipe_env(arch) + env['SHERPA_ONNXRUNTIME_LIB_DIR'] = ort_lib_dir + env['SHERPA_ONNXRUNTIME_INCLUDE_DIR'] = ort_include_dir + + shprint(sh.cmake, source_dir, + f'-DANDROID_ABI={arch.arch}', + f'-DANDROID_NATIVE_API_LEVEL={self.ctx.ndk_api}', + f'-DCMAKE_TOOLCHAIN_FILE={toolchain_file}', + f'-DCMAKE_INSTALL_PREFIX={install_dir}', + '-DCMAKE_BUILD_TYPE=Release', + '-DBUILD_SHARED_LIBS=ON', + '-DSHERPA_ONNX_ENABLE_BINARY=OFF', + '-DSHERPA_ONNX_ENABLE_PYTHON=OFF', + '-DSHERPA_ONNX_ENABLE_TESTS=OFF', + '-DSHERPA_ONNX_ENABLE_CHECK=OFF', + '-DSHERPA_ONNX_ENABLE_PORTAUDIO=OFF', + '-DSHERPA_ONNX_ENABLE_C_API=OFF', + '-DSHERPA_ONNX_ENABLE_WEBSOCKET=OFF', + '-DSHERPA_ONNX_LINK_LIBSTDCPP_STATICALLY=OFF', + _env=env) + shprint(sh.make, f'-j{cpu_count()}', _env=env) + shprint(sh.make, 'install/strip', _env=env) + + +recipe = SherpaOnnxRecipe() diff --git a/pythonforandroid/recipes/spacy/__init__.py b/pythonforandroid/recipes/spacy/__init__.py new file mode 100644 index 0000000000..e0ba551536 --- /dev/null +++ b/pythonforandroid/recipes/spacy/__init__.py @@ -0,0 +1,70 @@ +from pythonforandroid.recipe import CompiledComponentsPythonRecipe + + +class SpacyRecipe(CompiledComponentsPythonRecipe): + version = "3.8.7" + url = "https://pypi.python.org/packages/source/s/spacy/spacy-{version}.tar.gz" + site_packages_name = "spacy" + depends = [ + "setuptools", + "cython", + # Explosion-provided dependencies + "spacy-legacy", + "spacy-loggers", + "murmurhash", + "cymem", + "preshed", + "thinc", + "wasabi", + "srsly", + "catalogue", + "weasel", + "typer", + "tqdm", + "numpy", + "requests", + "pydantic", + "jinja2", + # Third party dependencies + "confection", + "packaging", + "click", + "typing_extensions", + "shellingham", + "rich", + "markdown-it-py", + "mdurl", + "Pygments", + "cloudpathlib", + "smart-open", + "wrapt", + "markupsafe", + # Official Python utilities + "packaging", + "langcodes", + # # Required for Russian language + # "pymorphy2", + # "pymorphy2_dicts_ru", + # "DAWG-Python", + # "appdirs", + # "pyparsing", + ] + call_hostpython_via_targetpython = False + + def get_recipe_env(self, arch=None, with_flags_in_cc=True): + env = super().get_recipe_env(arch, with_flags_in_cc) + env["CXXFLAGS"] = env["CFLAGS"] + " -frtti -fexceptions" + + if with_flags_in_cc: + env["CXX"] += " -frtti -fexceptions" + + env["LDFLAGS"] += f" -L{self.get_stl_library(arch)}" + env["LIBS"] = env.get("LIBS", "") + f" -l{self.stl_lib_name}" + return env + + def postbuild_arch(self, arch): + super().postbuild_arch(arch) + self.install_stl_lib(arch) + + +recipe = SpacyRecipe() diff --git a/pythonforandroid/recipes/srsly/__init__.py b/pythonforandroid/recipes/srsly/__init__.py new file mode 100644 index 0000000000..7c9cce4f79 --- /dev/null +++ b/pythonforandroid/recipes/srsly/__init__.py @@ -0,0 +1,17 @@ +from pythonforandroid.recipe import CompiledComponentsPythonRecipe + + +class SrslyRecipe(CompiledComponentsPythonRecipe): + version = "2.5.1" + url = "https://pypi.python.org/packages/source/s/srsly/srsly-{version}.tar.gz" + site_packages_name = "srsly" + depends = [ + "setuptools", + "cython", + "catalogue", + ] + call_hostpython_via_targetpython = False + install_in_hostpython = True + + +recipe = SrslyRecipe() diff --git a/pythonforandroid/recipes/srt/__init__.py b/pythonforandroid/recipes/srt/__init__.py new file mode 100644 index 0000000000..23daa92ffe --- /dev/null +++ b/pythonforandroid/recipes/srt/__init__.py @@ -0,0 +1,17 @@ +from pythonforandroid.recipe import PythonRecipe + + +class SrtRecipe(PythonRecipe): + # srt only ships an sdist on PyPI (no wheel at all), which p4a's bulk + # pip install of recipe-less requirements can't handle + # (--only-binary=:all:). A minimal recipe routes it through the + # normal setup.py install path instead. Needed as a real runtime + # dependency: vosk/__init__.py does `import srt` unconditionally. + name = "srt" + version = "3.5.3" + url = "https://pypi.python.org/packages/source/s/srt/srt-{version}.tar.gz" + depends = ["setuptools"] + call_hostpython_via_targetpython = False + + +recipe = SrtRecipe() diff --git a/pythonforandroid/recipes/thinc/__init__.py b/pythonforandroid/recipes/thinc/__init__.py new file mode 100644 index 0000000000..b50cf2033e --- /dev/null +++ b/pythonforandroid/recipes/thinc/__init__.py @@ -0,0 +1,43 @@ +from pythonforandroid.recipe import CompiledComponentsPythonRecipe + + +class ThincRecipe(CompiledComponentsPythonRecipe): + version = "8.3.4" + url = "https://pypi.python.org/packages/source/t/thinc/thinc-{version}.tar.gz" + site_packages_name = "thinc" + depends = [ + "setuptools", + "cython", + # Explosion-provided dependencies + "murmurhash", + "cymem", + "preshed", + "blis", + "srsly", + "wasabi", + "catalogue", + "confection", + "numpy", + "pydantic", + "packaging", + ] + call_hostpython_via_targetpython = False + install_in_hostpython = True + + def get_recipe_env(self, arch=None, with_flags_in_cc=True): + env = super().get_recipe_env(arch, with_flags_in_cc) + env["CXXFLAGS"] = env["CFLAGS"] + " -frtti -fexceptions" + + if with_flags_in_cc: + env["CXX"] += " -frtti -fexceptions" + + env["LDFLAGS"] += f" -L{self.get_stl_library(arch)}" + env["LIBS"] = env.get("LIBS", "") + f" -l{self.stl_lib_name}" + return env + + def postbuild_arch(self, arch): + super().postbuild_arch(arch) + self.install_stl_lib(arch) + + +recipe = ThincRecipe() diff --git a/pythonforandroid/recipes/vosk/__init__.py b/pythonforandroid/recipes/vosk/__init__.py index b677e09753..0a858ce173 100644 --- a/pythonforandroid/recipes/vosk/__init__.py +++ b/pythonforandroid/recipes/vosk/__init__.py @@ -2,6 +2,8 @@ import zipfile from pythonforandroid.logger import info +import sh + from pythonforandroid.recipe import PythonRecipe, current_directory, shprint from pythonforandroid.util import BuildInterruptingException, ensure_dir @@ -10,16 +12,28 @@ class VoskRecipe(PythonRecipe): version = "0.3.45" url = "https://github.com/alphacep/vosk-api/archive/refs/tags/v{version}.tar.gz" site_packages_name = "vosk" - depends = ["cffi"] - python_depends = ["requests", "tqdm", "srt", "websockets"] + # srt is a real, unconditional `import srt` in vosk/__init__.py (not + # just used by the optional vosk-transcriber CLI as we first assumed + # -- removing it from python_depends alone broke the app at runtime + # with ModuleNotFoundError). It has no wheel on PyPI (sdist only), so + # it can't go through p4a's --only-binary=:all: pip stage like + # requests/tqdm (also imported unconditionally, but wheel-available) + # do; it needs its own recipe instead, listed here as a real + # recipe-to-recipe dependency rather than python_depends. + # websockets is NOT imported by vosk/__init__.py itself (only by an + # unused submodule), so it's genuinely unneeded here. + depends = ["cffi", "srt"] + python_depends = [] + # vosk/__init__.py's open_dll() only recognizes win32/linux/darwin for + # picking the native library to load. CPython's own Android build sets + # sys.platform to "android" (a distinct value since CPython 3.13, not + # "linux"), so it hits the `else: raise TypeError("Unsupported + # platform")` branch. libvosk.so is still a plain ELF .so either way. + patches = ["android-platform.patch"] hostpython_prerequisites = [ "setuptools", "wheel", "cffi", - "requests", - "tqdm", - "srt", - "websockets", ] call_hostpython_via_targetpython = False @@ -42,13 +56,22 @@ def build_arch(self, arch): install_dir = self.ctx.get_python_install_dir(arch.arch) info("Installing Vosk Python bindings into site-packages") + hostpython = sh.Command(self.real_hostpython_location) with current_directory(python_dir): shprint( - self._host_recipe.pip, + hostpython, + "-m", "pip", "install", ".", "--compile", "--no-deps", + # Without these, pip sees a same-version vosk already sitting + # in install_dir from a previous build and silently skips + # reinstalling it ("Requirement already satisfied"), even + # after the source (patches, version) changed -- a stale + # install can then survive across rebuilds undetected. + "--upgrade", + "--force-reinstall", "--target", install_dir, _env=env, diff --git a/pythonforandroid/recipes/vosk/android-platform.patch b/pythonforandroid/recipes/vosk/android-platform.patch new file mode 100644 index 0000000000..25c9a68b0f --- /dev/null +++ b/pythonforandroid/recipes/vosk/android-platform.patch @@ -0,0 +1,14 @@ +--- a/python/vosk/__init__.py 2026-09-28 02:36:56.659984729 +0200 ++++ b/python/vosk/__init__.py 2026-09-28 02:36:56.660984738 +0200 +@@ -26,7 +26,10 @@ + if hasattr(os, "add_dll_directory"): + os.add_dll_directory(dlldir) + return _ffi.dlopen(os.path.join(dlldir, "libvosk.dll")) +- elif sys.platform == "linux": ++ elif sys.platform in ("linux", "android"): ++ # "android" is CPython's own sys.platform value for its native ++ # Android builds (distinct from "linux" since CPython 3.13); the ++ # library is still a plain ELF .so, loaded the same way as on Linux. + return _ffi.dlopen(os.path.join(dlldir, "libvosk.so")) + elif sys.platform == "darwin": + return _ffi.dlopen(os.path.join(dlldir, "libvosk.dyld")) diff --git a/pythonforandroid/recipes/wikipedia/__init__.py b/pythonforandroid/recipes/wikipedia/__init__.py new file mode 100644 index 0000000000..ae1cd49efd --- /dev/null +++ b/pythonforandroid/recipes/wikipedia/__init__.py @@ -0,0 +1,16 @@ +from pythonforandroid.recipe import PythonRecipe + + +class WikipediaRecipe(PythonRecipe): + # wikipedia only ships an sdist on PyPI (no wheel at all), which + # p4a's bulk pip install of recipe-less requirements can't handle + # (--only-binary=:all:). A minimal recipe routes it through the + # normal setup.py install path instead. + name = "wikipedia" + version = "1.4.0" + url = "https://pypi.python.org/packages/source/w/wikipedia/wikipedia-{version}.tar.gz" + depends = ["setuptools", "beautifulsoup4", "requests"] + call_hostpython_via_targetpython = False + + +recipe = WikipediaRecipe()