Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,14 @@ uv run flet build apk # rebuild

---

### `ImportError: cannot import name '<ext>' from '<pkg>'` for a native `.so` — Android, Flet ≥ 0.86 (serious_python 4.3.0)

**Cause:** the traceback shows the package loading from `…/sitepackages.zip/<pkg>/__init__.pyc` and the missing name is a **native pybind/Cython extension** whose `.so` IS in the wheel. serious_python 4.3.0 (Flet 0.86) relocates native extension modules out of the stored `sitepackages.zip` into `jniLibs/<abi>/lib<mangled>.so` and makes them importable via a `sys.meta_path` finder that reads `.soref` markers — but ONLY for **ABI-tagged** names: `serious_python_android/android/build.gradle.kts` matches `extTag = /\.(cpython-[^/]+|abi3)\.so$/`, and anything else falls into the `rel.endsWith(".so") -> dep` branch (copied to jniLibs as a plain dependency lib, **no `.soref`**, never importable). A shim that stages a pybind module under a **plain** name (`_pywrap_….so`, `onnxruntime_pybind11_state.so`) trips exactly this. Normal Cython/pybind recipes are unaffected — their extensions already carry the target `EXT_SUFFIX` (android cp312 = `.cpython-312.so`). iOS is unaffected (different migration path); the same recipe passing pre-0.86 or on iOS is the tell.

**Fix:** name the extension with the target `EXT_SUFFIX` so serious_python recognizes it (`.cpython-*.so` is also a valid CPython extension suffix, so the `from <pkg> import <ext>` still resolves). In the PEP 517 shim, stage it as `f"<ext>{sysconfig.get_config_var('EXT_SUFFIX')}"` instead of a plain `.so` (crossenv returns the target suffix; the recipe already sets `_PYTHON_SYSCONFIGDATA_NAME`). If the recipe's `setup.py` builds `package_data` by globbing hardcoded plain names (onnxruntime's `data = [… for x in libs if glob(… x)]`), also patch that to wildcard the pybind name and package the real basename, else the retagged `.so` is dropped from the wheel. Precedent: `machine/tflite-runtime` (`_pywrap_tensorflow_interpreter_wrapper.cpython-312.so`) and `machine/onnxruntime` (shim retag + setup.py glob patch). Verify: `unzip -l <wheel> | grep '\.cpython-'`.

---

### App launches but `import X` shows old version

**Cause:** flet-cli has a hash-based cache (`build/.hash/`) that decides whether to re-run the pip install step. If you only updated wheels in `dist/` but pyproject deps didn't change, the cache thinks nothing has changed.
Expand Down Expand Up @@ -1203,6 +1211,25 @@ If the wheel for the right tag isn't there, build it: `forge <slice> <pkg>`.

---

### `No matching distribution found for <pkg>` at APK build — the recipe has `excluded_arches`

**Cause:** the recipe declares `excluded_arches` (e.g. onnxruntime drops
`armeabi-v7a`), so no wheel is built for that ABI. But `flet build apk` defaults to
**all** Android ABIs (`arm64-v8a`, `x86_64`, `armeabi-v7a`), and pip fails to resolve
the package for the excluded ABI. The other-recipe tell: a recipe with all ABIs
(tflite) passes while the excluded-ABI recipe fails at the *same* step. Only bites
under the recipe-tester's app build (build wheels succeed on every leg).

**Fix:** restrict the app's Android target to the built ABIs —
`[tool.flet.android] target_arch = ["arm64-v8a", "x86_64"]` (drop the excluded ones;
keep `x86_64` for the CI emulator). `tests/recipe-tester/stage_recipe.sh` now derives
this automatically from the wheels present in `dist-test`/`dist` and writes it into
the generated pyproject, so a recipe with `excluded_arches` no longer needs manual
handling in CI. For a real consumer app, the author sets `target_arch` themselves —
this is the mandatory `target_arch` note that belongs in the recipe's Consumer notes.

---

### `flet build` pip-backtracks a pure-python dep to an ancient version → runtime crash (omegaconf 2.0.0 `UnsupportedValueType: PosixPath`)

**Cause:** flet's mobile resolution requires **wheels**, and a pure-python dep in
Expand Down
2 changes: 2 additions & 0 deletions .claude/skills/new-mobile-recipe/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ The shim's load-bearing rules (each one bought with a failed build):
- Prefer **`-D<pkg>_BUILD_SHARED_LIB=OFF` → one statically-linked pybind module**: no ctypes/dylib gate, and it is exactly the wheel shape that works on BOTH platforms today (onnxruntime's Android design turned out to BE the iOS wheel shape; pywhispercpp/ncnn are the same shape).
- CMake args ride in a recipe `script_env` var (`FORGE_CMAKE_ARGS`) that the shim `shlex.split`s. **Multi-token `-D` values cannot ride inside it** — give linker-flag strings their own env var and let the shim assemble the single `-D` argument (tflite's `FORGE_SHARED_LINKER_FLAGS: -Wl,-z,max-page-size=16384 -L{HOST_PYTHON_HOME}/lib -lpython{py_version_short}`).
- **Trap — `setup.py` platform predicates:** under the crossenv `platform.system()` returns `"Android"` / `"iOS"`, which may match NO upstream branch → the libs list stays unset and the wheel silently ships **without the pybind `.so`** (onnxruntime extends upstream's predicate to `("Linux", "AIX", "Android", "iOS")`; the Darwin branch stays intact for real macOS builds).
- **Name the staged extension with the target `EXT_SUFFIX`, never a plain `.so`.** A shim that hand-stages the pybind module (`shutil.copy2(built, pkg / "_foo.so")`) must instead write `pkg / f"_foo{sysconfig.get_config_var('EXT_SUFFIX')}"` (android cp312 → `_foo.cpython-312.so`). Flet 0.86's serious_python only relocates ABI-tagged (`cpython-*`/`abi3`) extension modules into an importable jniLibs/`.soref` slot on Android; a plain `<name>.so` is misfiled as a dependency lib and fails on-device with `cannot import name` (builds green, iOS fine — a nasty android-only surprise). `.cpython-*.so` is a valid CPython suffix so the import still resolves. If `setup.py` builds `package_data` by globbing hardcoded plain `.so` names, patch that too (wildcard the pybind name, package the real basename) or the retagged file is dropped. See the `forge-error-catalogue` entry "cannot import name … for a native `.so` — Android, Flet ≥ 0.86".
- **Cross-slice cleanup:** forge reuses the unpacked source tree across arch slices in one invocation, so if the shim stages into a dir it does NOT wipe each hook (e.g. an overlay `copytree(dirs_exist_ok=True)`), a prior slice's tagged `.so` lingers. Android is safe (all ABIs share `.cpython-312.so` → overwritten), but **iOS suffixes differ per slice** (`.cpython-312-iphoneos.so` vs `-iphonesimulator.so`) so the wheel ships BOTH and serious_python's `.so`→framework conversion fails on-device. Either `rmtree` the package dir before staging (tflite's shim) or, after retagging, delete every sibling `<name>*.so` that isn't the current slice's target (onnxruntime's shim).

**Real examples:** `machine/onnxruntime:recipes/onnxruntime/` and `machine/tflite-runtime:recipes/tflite-runtime/` — read via `git show <branch>:<path>` if not on those branches. Both are ONE `mobile.patch` (description as text preamble above the first `---` header, per repo convention).

Expand Down
61 changes: 52 additions & 9 deletions recipes/onnxruntime/patches/mobile.patch
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ following the sequence Termux proved for Android (their python-onnxruntime
wrapping setuptools.build_meta that first runs the CMake build (source =
./cmake, binary dir = the source ROOT, args from FORGE_CMAKE_ARGS) so the
staged python package lands in ./onnxruntime/capi/ where the root
setup.py expects it, then delegates wheel assembly to setuptools.
setup.py expects it, then delegates wheel assembly to setuptools. The
shim also retags the pybind module to the target EXT_SUFFIX (e.g.
onnxruntime_pybind11_state.cpython-312.so): serious_python's android
packager only relocates ABI-tagged (cpython-*/abi3) extension modules
into jniLibs with an importable .soref marker, so a plain '<name>.so' is
misfiled as a dependency lib and never becomes importable on device.

2. cmake/CMakeLists.txt (Termux 0004): find Python COMPONENTS Development
before Development.Module -- under a crossenv, the Module-only lookup
Expand All @@ -17,13 +22,16 @@ following the sequence Termux proved for Android (their python-onnxruntime
3. mlas sqnbitgemm avx2 header (Termux 0006): include onnxruntime_config.h
for the x86_64 lane.

4. setup.py: extend the packaged-libs predicate from ("Linux", "AIX") to
also match "Android"/"iOS" -- under the crossenv platform.system()
reports the TARGET OS, which matches no upstream branch, so `libs`
stays empty and the build goes green while the wheel silently ships
WITHOUT the pybind .so (Termux hand-installs it post-hoc instead; we
fix the predicate). The Darwin branch stays intact for real macOS
builds.
4. setup.py: two changes. (a) extend the packaged-libs predicate from
("Linux", "AIX") to also match "Android"/"iOS" -- under the crossenv
platform.system() reports the TARGET OS, which matches no upstream
branch, so `libs` stays empty and the build goes green while the wheel
silently ships WITHOUT the pybind .so (Termux hand-installs it post-hoc
instead; we fix the predicate). The Darwin branch stays intact for real
macOS builds. (b) the package_data glob keys off the plain `libs` names,
so after the shim retags the pybind module (item 1) it would no longer
match -- wildcard the pybind name when globbing and package the real
basename, so the tagged .so is actually included.

Deliberately NOT taken from Termux: their 0001/0002 (system abseil and
protobuf -- we want the FetchContent vendored builds) and 0003 (Android
Expand All @@ -36,7 +44,7 @@ protoc_mac_universal v21.12 on macOS hosts.
diff -ruN a/_forge/forge_ort_backend.py b/_forge/forge_ort_backend.py
--- a/_forge/forge_ort_backend.py 1970-01-01 01:00:00
+++ b/_forge/forge_ort_backend.py 2026-07-08 18:23:28
@@ -0,0 +1,75 @@
@@ -0,0 +1,96 @@
+"""mobile-forge PEP 517 shim for onnxruntime.
+
+Upstream produces wheels via ci_build/build.py: CMake builds the tree and
Expand All @@ -55,10 +63,12 @@ diff -ruN a/_forge/forge_ort_backend.py b/_forge/forge_ort_backend.py
+ slice 1's architecture (the e_machine mismatch the first CI run caught).
+"""
+
+import glob
+import os
+import shlex
+import shutil
+import subprocess
+import sysconfig
+
+from setuptools import build_meta as _orig
+from setuptools.build_meta import * # noqa: F401,F403
Expand Down Expand Up @@ -97,6 +107,25 @@ diff -ruN a/_forge/forge_ort_backend.py b/_forge/forge_ort_backend.py
+ # cheap, idempotent, and guarantees the CURRENT ABI wins.
+ shutil.copytree(os.path.join(bindir, "onnxruntime"), "onnxruntime",
+ dirs_exist_ok=True)
+ # Tag the pybind extension with the target EXT_SUFFIX (e.g.
+ # .cpython-312.so) so serious_python's android packager treats it as an
+ # importable extension module: its jniLibs/.soref meta_path finder only
+ # relocates cpython-*/abi3 names -- a plain '<name>.so' is misfiled as a
+ # dependency lib and never becomes importable.
+ _capi = os.path.join("onnxruntime", "capi")
+ _ext = sysconfig.get_config_var("EXT_SUFFIX") or ".so"
+ _target = "onnxruntime_pybind11_state" + _ext
+ _plain = os.path.join(_capi, "onnxruntime_pybind11_state.so")
+ if os.path.exists(_plain):
+ os.replace(_plain, os.path.join(_capi, _target))
+ # forge reuses the source tree across slices and copytree(dirs_exist_ok)
+ # never deletes, so a prior slice's tagged .so lingers -- on iOS the
+ # per-slice suffixes differ (iphoneos vs iphonesimulator) and the wheel
+ # would ship BOTH, breaking serious_python's .so->framework conversion.
+ # Drop every onnxruntime_pybind11_state*.so that isn't this slice's.
+ for _f in glob.glob(os.path.join(_capi, "onnxruntime_pybind11_state*.so")):
+ if os.path.basename(_f) != _target:
+ os.remove(_f)
+
+
+def get_requires_for_build_wheel(config_settings=None):
Expand Down Expand Up @@ -161,3 +190,17 @@ diff -ruN a/setup.py b/setup.py
libs = [
"onnxruntime_pybind11_state.so",
"libdnnl.so.2",
@@ -540,4 +540,12 @@
]
else:
- data = [path.join("capi", x) for x in libs if glob(path.join("onnxruntime", "capi", x))]
+ # mobile-forge: pybind module ships with the target EXT_SUFFIX (e.g.
+ # .cpython-312.so) so serious_python relocates it as an importable
+ # extension. Glob wildcards the plain pybind name and we package the
+ # real basename (package_data must name the on-disk file).
+ data = []
+ for x in libs:
+ _pat = "onnxruntime_pybind11_state*.so" if x == "onnxruntime_pybind11_state.so" else x
+ for f in glob(path.join("onnxruntime", "capi", _pat)):
+ data.append(path.join("capi", path.basename(f)))
ext_modules = []
80 changes: 80 additions & 0 deletions recipes/tflite-runtime/meta.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{% set version = "2.21.0" %}

package:
name: tflite-runtime
version: '{{ version }}'

build:
number: 1
script_env:
_PYTHON_SYSCONFIGDATA_NAME: '{sysconfigdata_name}'
# Consumed by the patch-added PEP 517 shim (_forge/forge_tflite_backend.py):
# standalone host-flatc build + cross cmake of tensorflow/lite building
# ONLY the _pywrap_tensorflow_interpreter_wrapper pybind module, then
# upstream's exact 5-file python package staging. The wrapper target has
# no FindPython -- Python/numpy/pybind11 headers are plain -I injections
# (upstream's own mechanism); numpy+pybind11 resolve from the cross venv
# via {platlib}, no imports needed (target numpy is not host-runnable).
FORGE_WRAPPER_INCLUDES: >-
-I{HOST_PYTHON_HOME}/include/python{py_version_short}
-I{platlib}/numpy/_core/include
-I{platlib}/pybind11/include
# {% if sdk == 'android' %}
# Multi-token -D values can't ride inside FORGE_CMAKE_ARGS (the shim
# shlex-splits it), so linker flags get their own var. libpython is
# linked explicitly like every other forge pybind wheel: upstream
# relies on manylinux-style lazy resolution, Android wants DT_NEEDED.
FORGE_SHARED_LINKER_FLAGS: >-
-Wl,-z,max-page-size=16384
-L{HOST_PYTHON_HOME}/lib -lpython{py_version_short}
# XNNPACK stays at its default (ON) except armeabi-v7a, where the shim
# follows upstream's own pip script convention (OFF for all 32-bit arm).
FORGE_CMAKE_ARGS: >-
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
-DCMAKE_TOOLCHAIN_FILE={NDK_ROOT}/build/cmake/android.toolchain.cmake
-DANDROID_ABI={ANDROID_ABI}
-DANDROID_NATIVE_API_LEVEL={ANDROID_API_LEVEL}
-DANDROID_STL=c++_shared
# {% else %}
# iOS: the wrapper target links no libpython (no FindPython anywhere),
# and apple's two-level namespace would fail the link on the undefined
# python symbols -- resolve them at dlopen instead.
FORGE_SHARED_LINKER_FLAGS: -Wl,-undefined,dynamic_lookup
FORGE_CMAKE_ARGS: >-
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
-DCMAKE_SYSTEM_NAME=iOS
-DCMAKE_OSX_SYSROOT={{ sdk }}
-DCMAKE_OSX_DEPLOYMENT_TARGET={{ sdk_version }}
-DCMAKE_OSX_ARCHITECTURES={{ arch }}
-DFLATBUFFERS_BUILD_FLATC=OFF
-DFLATBUFFERS_INSTALL=OFF
# {% endif %}

source:
# tflite-runtime has no sdist (upstream wheels come from
# tensorflow/lite/tools/pip_package/build_pip_package_with_cmake.sh);
# build from the TF release tree. The lite/ CMake superbuild fetches its
# pinned deps (abseil/eigen/xnnpack/ruy/flatbuffers/...) at configure.
url: https://github.com/tensorflow/tensorflow/archive/refs/tags/v{{ version }}.tar.gz

requirements:
build:
- setuptools
- wheel
- cmake
host:
# Build-time headers only (via FORGE_WRAPPER_INCLUDES {platlib} paths).
# numpy>=2 headers yield binaries compatible with numpy 1.x AND 2.x;
# numpy is also the wheel's one runtime dep (install_requires in the
# shim's setup.py, matching upstream setup_with_binary.py).
- numpy ^2.0.0
- pybind11
# {% if sdk == 'android' %}
# C++20 -> libc++_shared.so at runtime (iOS links the system libc++).
- flet-libcpp-shared >=27.2.12479018
# {% endif %}

patches:
- mobile.patch
Loading
Loading