Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .claude/skills/forge-error-catalogue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ instead of re-deriving it.
the NDK toolchain pins `CMAKE_SYSTEM_VERSION` to 1; read `ANDROID_PLATFORM_LEVEL`**.
- **Runtime failures** (device/emulator/simulator) — **the Flet 0.86 Android
`sitepackages.zip` class** (its umbrella entry explains "why only now"):
`NotADirectoryError` on a bundled data file → **`extract_packages`** meta field;
`NotADirectoryError` on a bundled data file → **`extract_packages`** meta field,
or, when the package's own import reads it (xgboost `VERSION`), **an
`importlib.resources` patch** — a meta-only fix passes CI and breaks consumers;
untagged native `.so` `ModuleNotFoundError`/"circular import" → **forge `fix_wheel`
ABI-tags `PyInit_*` `.so`**; pycryptodome `Cannot load native module …` →
**`importlib.find_spec().origin`**; llama `FileNotFoundError` / ctypes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,18 @@ because `copyOpt` copies only `**/*.so`); the fix there is to ship the data file
**inside the python package's own wheel** + load it from memory — see the dedicated
python-magic entry below.

**When the package itself reads the file on every import, patch the read instead.**
`extract_packages` in a recipe's `meta.yaml` reaches only mobile-forge's test app, so
the CI run goes green while every consumer app still crashes until its author adds the
same entry to their own `pyproject.toml`. If the read is one small file in the package's
own code, swap the `open(os.path.join(os.path.dirname(__file__), …))` for
`importlib.resources.files(__package__).joinpath(…).read_text()`, which zipimport
serves. xgboost 3.4 (`_c_api._py_version()` opens `xgboost/VERSION`, called from
`__init__` and again from `_load_lib`) died on `import xgboost` with
`…/sitepackages.zip/xgboost/VERSION`; `mobile.patch` hunk 4 fixes it with no consumer
configuration. The 3.3.0 recipe predated 0.86 and never met it. Reproduce on a desktop
in seconds: put the package in a zip on `sys.path` and import it.

---

### `RuntimeError: Failed to load shared library '.../<pkg>/lib/lib<X>.so'` / `dlopen failed: library "..." not found` (Android, at import — a **ctypes** loader that raises before its own jniLibs fallback)
Expand Down Expand Up @@ -1439,7 +1451,12 @@ Android** build, while the *same recipe passes on py3.12*. llama-cpp-python hit
Unsupported platform` at import on 3.13, before its jniLibs bare-soname fallback could run.

**Fix:** add `or sys.platform == "android"` wherever `"linux"` is special-cased (Android is a
Linux kernel, so the linux path is almost always what you want), and bump the build. This is a
Linux kernel, so the linux path is almost always what you want), and bump the build.
**iOS is different: flet's runtime reports `"ios"` on every version, 3.12 included**
(`Py_GetPlatform()` in its 3.12.13 `Python.framework` returns the literal `"ios"`, and its
`_sysconfigdata__ios_*` sets `MACHDEP` to `ios`). So an `ios` branch is enough, and the
`darwin` branch can stay byte-identical to upstream (xgboost). A comment in
llama-cpp-python's `mobile.patch` says 3.12 iOS reports `"darwin"`; it does not. This is a
**general py3.12→3.13 tell** — grep a recipe's patches/loaders for `sys.platform` before trusting
a 3.13 build. Distinct from the 3.13/3.14 Android **x86_64 `SIGSYS`/seccomp `open()`** crash,
which is a *native* abort from python-build's mimalloc (fixed in the `20260712` snapshot), not a
Expand Down Expand Up @@ -1693,8 +1710,12 @@ misses. The bundled libs (llama-cpp-python's libllama/libggml*) were relocated t
**Fix:** after the on-disk probes miss, fall back to `ctypes.CDLL("lib<name>.so")`
(bare soname → the Android linker resolves it from jniLibs), for both the dependency
preload and the main lib; load `RTLD_GLOBAL` so preloaded deps satisfy the main
lib's DT_NEEDED. Verified: llama-cpp-python (android). See also "Unable to find
… shared library" (the `find_library`-returns-None sibling).
lib's DT_NEEDED. Verified: llama-cpp-python (android). xgboost's `libpath.py` has
the same `os.path.exists` gate (its error would be `XGBoostLibraryNotFound: Cannot
find XGBoost Library in the candidate path`); its patch returns `["libxgboost.so"]`
when nothing exists and `sys.getandroidapilevel` does — present on 3.12, where
`sys.platform` still says `"linux"`. Verified on an arm64 emulator, Flet 1.0.3.
See also "Unable to find … shared library" (the `find_library`-returns-None sibling).

---

Expand Down
7 changes: 5 additions & 2 deletions .claude/skills/local-recipe-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ carries every control value: `xcrun simctl spawn "$UDID" log show --last 2m --st

1. **Use forge's `dist/` wheel, NOT `build/.../target/wheels/`.** The latter is maturin's raw output — **unstripped**. For polars that meant a **1.27 GB** `.so` (vs 130 MB stripped); it blows up install space and may not load. forge strips + repacks into `dist/`. Always test the `dist/` wheel.

2. **Build the recipe against the SAME Python `flet build` bundles — and know which one that is.** `flet build` **0.86.5 defaults to Python 3.14** (measured 2026-09-14: the app's staged site-packages hold `_cffi_backend.cpython-314-*.so`); the snippets above pass `--python-version 3.12` to pin it, and CI's recipe-tester does the same. A **consumer** example app built the plain way (`flet build apk`, no flag) therefore gets 3.14, so build the recipe for 3.14 (`source ./setup.sh 3.14`) before that pass, or pass the flag. `flet-lib*` build.sh recipes are `py3-none-<plat>` and version-independent — only the Python package needs the extra build.
2. **Build the recipe against the SAME Python `flet build` bundles — and know which one that is.** `flet build` **0.86.5 defaults to Python 3.14** (measured 2026-09-14: the app's staged site-packages hold `_cffi_backend.cpython-314-*.so`), and so does 1.0.3 (3.14.7; a forge build against the 3.14.6 support tree loads fine); the snippets above pass `--python-version 3.12` to pin it, and CI's recipe-tester does the same. A **consumer** example app built the plain way (`flet build apk`, no flag) therefore gets 3.14, so build the recipe for 3.14 (`source ./setup.sh 3.14`) before that pass, or pass the flag. `flet-lib*` build.sh recipes are `py3-none-<plat>` and version-independent — only the Python package needs the extra build.

Two different failure shapes if you get it wrong. For a **compiled** package: forge's Android Rust `.so` hard-links `libpythonX.Y.so` (`DT_NEEDED`) — so the **`abi3` wheel tag is misleading**; it still needs the matching `libpython` at `dlopen`. A 3.14-built wheel in a 3.12 app fails: `dlopen … libpython3.14.so` missing → the package reports its "binary missing" (e.g. polars `NameError: PySeries`). Verify with `llvm-readelf -d <so> | grep NEEDED`. If you only have a different support tree, you can retag a wheel for flet's python with `uvx --from wheel wheel tags --python-tag cp312 --abi-tag abi3 --remove <whl>`, but the underlying `libpython` link still has to match — so really, build on the right python. For a **pure-Python** package with a mobile patch there is no link error at all — pip simply finds no `cp3XX` match, silently installs PyPI's `py3-none-any` wheel, and the app fails on device with the unpatched loader's own error. Tell: the app's `build/site-packages/<abi>/<pkg>-*.dist-info/METADATA` is missing the `flet-lib*` `Requires-Dist` the recipe promotes.

Expand Down Expand Up @@ -174,11 +174,14 @@ carries every control value: `xcrun simctl spawn "$UDID" log show --last 2m --st
# config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
sed -i '' 's/IPHONEOS_DEPLOYMENT_TARGET = 13.0;/IPHONEOS_DEPLOYMENT_TARGET = 15.0;/' Runner.xcodeproj/project.pbxproj
cd .. && SERIOUS_PYTHON_APP=$PWD/../python-app SERIOUS_PYTHON_SITE_PACKAGES=$PWD/../site-packages \
SERIOUS_PYTHON_VERSION=3.14 SP_NATIVE_SET=$(cat ../.serious_python_spm_key) \
SERIOUS_PYTHON_VERSION=3.14 SERIOUS_PYTHON_BUNDLE_ID=com.flet.<app-name> \
SP_NATIVE_SET=$(cat ../.serious_python_spm_key) \
flutter build ios --simulator --build-name 1.0.0 # VERSION = the Python flet built for
# → build/flutter/build/ios/iphonesimulator/<app>.app
```
A fresh `flet build` regenerates the template, so repeat the patch after every one.
The env is flet-cli's `_flutter_path_env` (`build_base.py`); without
`SERIOUS_PYTHON_BUNDLE_ID` the embedded frameworks get `org.python.*` identifiers.

11. **Two booted simulators make `simctl booted` ambiguous.** With more than one sim booted, `simctl install booted …` targets one device and your subsequent `get_app_container booted …` may query the OTHER — the app "isn't installed" / the container is empty despite a successful install. Use the explicit `$UDID` for every simctl call (as the loop above does); never rely on `booted` unless you've verified exactly one device is booted (`xcrun simctl list devices | grep -c Booted`).

Expand Down
1 change: 1 addition & 0 deletions .claude/skills/new-mobile-recipe/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Match the package to one of these shapes. Each maps to a template in `templates/
| C-ext needing patches | Upstream `setup.py` reaches for host `/usr/include`, has `if sys.platform` branches, or hardcodes paths | `templates/meta-with-patches.yaml` + patch file in `patches/` |
| meson-python build backend | `[build-system].build-backend = "mesonpy"` (scipy, scikit-image, scikit-learn, pywavelets, contourpy) | No template — copy from `recipes/pywavelets/` (on main): `build.backend-args:` list with `-Csetup-args=--cross-file` + `-Csetup-args={MESON_CROSS_FILE}`; extra meson options ride the same way (`-Csetup-args=-D<opt>=<val>`) |
| scikit-build-core / CMake with sdist | `[build-system].build-backend = "scikit_build_core.build"` (duckdb, onnx, ml-dtypes) | No template — copy from `recipes/duckdb/` (on main) or `recipes/onnx/` (branch `machine/onnx-insightface`); CMake args via `script_env` `CMAKE_ARGS` (skbuild appends it after its own defaults) |
| Package that **ctypes-loads its own C API library** from `<pkg>/lib/` (no extension module) | xgboost, lightgbm: CMake builds `lib<x>.so`, the Python side finds it by a `__file__` path | Copy `recipes/xgboost/` (scikit-build-core lanes + one `mobile.patch`): serious_python framework-izes the lib (`.so` or `.dylib`) behind a `<stem>.fwork` pointer on iOS, so teach the loader `ios`/`android` `sys.platform` (both iOS 3.12+ and Android 3.13+; Android 3.12 says `linux`) + that pointer + an Android bare-soname fallback, drop `SOVERSION`, and grep for every other `__file__` read — Android site-packages is a zip |
| C-ext consuming an existing flet-lib | Already-built `flet-libX` covers the C dep (libxml2, libcurl, libssl via openssl, etc.) | Adapt `templates/meta-with-patches.yaml` + add `requirements.host` |
| Native library itself (flet-lib*), **static** | New C library a Python C-extension links at build time (libxml2, libcurl, libgeos…) | `templates/meta-flet-lib.yaml` + `templates/build-flet-lib.sh` |
| Native library, **ctypes-loaded (shared)** | A pure-Python wrapper `dlopen`s the lib at runtime via `ctypes` (pyzbar→libzbar, python-magic→libmagic) | `templates/meta-flet-lib.yaml` + `templates/build-flet-lib-shared.sh`; see Pattern H |
Expand Down
Loading
Loading