From c082c9479e0501fafc184c690d521db7d619b00d Mon Sep 17 00:00:00 2001 From: Gemini Cloud AI <222482969+Manamama-Gemini-Cloud-AI-01@users.noreply.github.com> Date: Sun, 10 May 2026 16:56:41 +0200 Subject: [PATCH] feat(python): add support for local Termux/Android builds - Recognizes 'android' platform in setup.py - Implements NumKong source bundling for Android to bypass Bionic linker limitations - Disables static-libstdc++ on Android to match Termux environment - Skips manual RTLD_GLOBAL load in __init__.py on Android --- MANIFEST.in | 3 +++ python/numkong_bundle.cpp | 32 +++++++++++++++++++++++++++++ python/usearch/__init__.py | 4 ++++ setup.py | 41 ++++++++++++++++++++------------------ 4 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 python/numkong_bundle.cpp diff --git a/MANIFEST.in b/MANIFEST.in index d2fc82a47..7d840f3e3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,4 @@ include python/usearch/py.typed +recursive-include numkong/include *.h *.hpp +recursive-include numkong/c *.c *.h +recursive-include stringzilla/include *.h *.hpp diff --git a/python/numkong_bundle.cpp b/python/numkong_bundle.cpp new file mode 100644 index 000000000..ff6f6a8b0 --- /dev/null +++ b/python/numkong_bundle.cpp @@ -0,0 +1,32 @@ +/** + * @file python/numkong_bundle.cpp + * @brief A single-file C++ wrapper for NumKong C files to ensure static linkage on Android. + */ + +extern "C" { +#include "../numkong/c/numkong.c" +#include "../numkong/c/dispatch_bf16.c" +#include "../numkong/c/dispatch_bf16c.c" +#include "../numkong/c/dispatch_e2m3.c" +#include "../numkong/c/dispatch_e3m2.c" +#include "../numkong/c/dispatch_e4m3.c" +#include "../numkong/c/dispatch_e5m2.c" +#include "../numkong/c/dispatch_f16.c" +#include "../numkong/c/dispatch_f16c.c" +#include "../numkong/c/dispatch_f32.c" +#include "../numkong/c/dispatch_f32c.c" +#include "../numkong/c/dispatch_f64.c" +#include "../numkong/c/dispatch_f64c.c" +#include "../numkong/c/dispatch_i16.c" +#include "../numkong/c/dispatch_i32.c" +#include "../numkong/c/dispatch_i4.c" +#include "../numkong/c/dispatch_i64.c" +#include "../numkong/c/dispatch_i8.c" +#include "../numkong/c/dispatch_other.c" +#include "../numkong/c/dispatch_u1.c" +#include "../numkong/c/dispatch_u16.c" +#include "../numkong/c/dispatch_u32.c" +#include "../numkong/c/dispatch_u4.c" +#include "../numkong/c/dispatch_u64.c" +#include "../numkong/c/dispatch_u8.c" +} diff --git a/python/usearch/__init__.py b/python/usearch/__init__.py index c8bced53a..b340ff124 100644 --- a/python/usearch/__init__.py +++ b/python/usearch/__init__.py @@ -24,6 +24,10 @@ # any bundled sibling DLLs, then load without `RTLD_GLOBAL`. os.add_dll_directory(os.path.dirname(_numkong_path)) numkong_lib = ctypes.CDLL(_numkong_path) + elif sys.platform == "android": + # On Android, NumKong is statically bundled into the USearch extension + # because the Bionic linker does not support `RTLD_GLOBAL`. + pass else: # On Linux/macOS we need `RTLD_GLOBAL` so USearch's compiled module can # resolve NumKong symbols at its own load time. diff --git a/setup.py b/setup.py index 88b32c74f..e0839ebfe 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ def get_bool_env_w_name(name: str, preference: bool) -> tuple: # Check the environment variables +is_android: bool = sys.platform == "android" is_linux: bool = sys.platform == "linux" is_macos: bool = sys.platform == "darwin" is_windows: bool = sys.platform == "win32" @@ -28,7 +29,7 @@ def get_bool_env_w_name(name: str, preference: bool) -> tuple: is_gcc = False is_clang = False -if is_linux: +if is_linux or is_android: cxx = os.environ.get("CXX") if cxx: try: @@ -45,11 +46,16 @@ def get_bool_env_w_name(name: str, preference: bool) -> tuple: # ? Using `ctypes.CDLL(numkong.__file__)` breaks the CI # ? with "Windows fatal exception: access violation". prefer_numkong: bool = not is_windows -prefer_openmp: bool = is_linux and is_gcc +prefer_openmp: bool = (is_linux or is_android) and is_gcc use_numkong: bool = get_bool_env("USEARCH_USE_NUMKONG", prefer_numkong) use_openmp: bool = get_bool_env("USEARCH_USE_OPENMP", prefer_openmp) +# Dynamic LOADING of a separate NumKong library often fails at runtime on Android +# due to the Bionic linker's behavior with RTLD_GLOBAL and Python extensions. +# In such cases, we bundle NumKong source files directly into the extension. +use_numkong_bundle: bool = use_numkong and is_android + # Common arguments for all platforms macros_args.append(("USEARCH_USE_OPENMP", "1" if use_openmp else "0")) macros_args.append(("USEARCH_USE_NUMKONG", "1" if use_numkong else "0")) @@ -61,7 +67,7 @@ def get_bool_env_w_name(name: str, preference: bool) -> tuple: macros_args.extend( [ ("NK_DYNAMIC_DISPATCH", "1" if use_numkong else "0"), - ("NK_TARGET_NEON", "0"), # ? Hide-out all complex intrinsics + ("NK_TARGET_NEON", "1" if is_android and ("arm" in machine or "aarch64" in machine) else "0"), ("NK_TARGET_NEONBFDOT", "0"), # ? Hide-out all complex intrinsics ("NK_TARGET_NEONHALF", "0"), # ? Hide-out all complex intrinsics ("NK_TARGET_NEONSDOT", "0"), # ? Hide-out all complex intrinsics @@ -98,7 +104,7 @@ def get_bool_env_w_name(name: str, preference: bool) -> tuple: ] ) -if is_linux: +if is_linux or is_android: compile_args.append("-std=c++17") compile_args.append("-O3") # Maximize performance compile_args.append("-ffast-math") # Maximize floating-point performance @@ -110,7 +116,10 @@ def get_bool_env_w_name(name: str, preference: bool) -> tuple: # Linking to NumKong compile_args.append("-Wl,--unresolved-symbols=ignore-in-shared-libs") - link_args.append("-static-libstdc++") + + # On Android we don't usually want to link libstdc++ statically + if is_linux: + link_args.append("-static-libstdc++") if use_openmp: compile_args.append("-fopenmp") @@ -133,26 +142,17 @@ def get_bool_env_w_name(name: str, preference: bool) -> tuple: link_args.append("-undefined") link_args.append("dynamic_lookup") - # Linking OpenMP requires additional preparation in CIBuildWheel. - # We must install `brew install llvm` ahead of time. - # import subprocess as cli - # llvm_base = cli.check_output(["brew", "--prefix", "llvm"]).strip().decode("utf-8") - # if len(llvm_base): - # compile_args.append(f"-I{llvm_base}/include") - # compile_args.append("-Xpreprocessor -fopenmp") - # link_args.append(f"-L{llvm_base}/lib") - # link_args.append("-lomp") - # macros_args.append(("USEARCH_USE_OPENMP", "1")) - if is_windows: compile_args.append("/std:c++17") - compile_args.append("/O2") - compile_args.append("/fp:fast") # Enable fast math for MSVC + compile_args.append("/O2") # Maximize performance + compile_args.append("/fp:fast") # Maximize floating-point performance compile_args.append("/W1") # Reduce warnings verbosity link_args.append("/FORCE") # Force linking with missing NumKong symbols sources = ["python/lib.cpp"] +if use_numkong_bundle: + sources.append("python/numkong_bundle.cpp") ext_modules = [ Pybind11Extension( @@ -184,7 +184,10 @@ def get_bool_env_w_name(name: str, preference: bool) -> tuple: ] if use_numkong: include_dirs.append("numkong/include") - install_requires.append("numkong") + if use_numkong_bundle: + include_dirs.append("numkong/c") + else: + install_requires.append("numkong") # With Clang, `setuptools` doesn't properly use the `language="c++"` argument we pass.