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
25 changes: 21 additions & 4 deletions .claude/skills/forge-ci/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ Key structural facts:
wheels that are identical on every Python leg, so they build **only on the
canonical (first-listed, i.e. 3.12) leg**. On the 3.13/3.14 legs they are
filtered out of the package list entirely.
- **Mobile tests run only on the legs listed in `mobile_test_pythons`**
(default `3.12` — and per hard experience, 3.12 is the only leg whose mobile
tests pass on this fork; never dispatch `mobile_test_pythons=ALL`).
- Mobile tests run only on the legs listed in `mobile_test_pythons` (default: `3.12`).
- The mobile test bumps local wheels' build tag to `9999` in `dist-test/` so
pip prefers them over same-version wheels already published on
pypi.flet.dev.
Expand Down Expand Up @@ -208,17 +206,36 @@ Seen repeatedly on this fork; all are safe to retry once:
it's not transient — see the `forge-error-catalogue` skill (`User for
pypi.flet.dev:` entry).

- **`Execution failed for task ':serious_python_android:downloadDistArchive_<abi>'`**
→ `groovy.json.JsonException: Unable to determine the current character …
The current character read is '\0' … index number 255` in the **"Stage
tests + build recipe-tester APK"** step. serious_python's Gradle download
task got a truncated/corrupt body where JSON was expected. Nothing to do
with the recipe. It is **easy to misread as a recipe failure** because the
step name mentions the recipe and the flet output has no `##[error]` of its
own — the log just stops and cleanup terminates orphan java/adb processes.
TELLS that it is this: all wheels built (`Successfully built <pkg>-…whl`
for every ABI) and the `wheels-*` artifact exists for the red leg, the
failure is *after* the wheel phase, and `Gradle task assembleRelease failed
with exit code 1` is the only error. Find the root cause by grepping the
extracted log for `What went wrong` — not for `##[error]`/`Error building
Flet`, which only match the generic tail. Rerun clears it.

Real failures reproduce on rerun. Don't retry more than once without reading
the log.

Note `PKG_VERSION` being empty in `stage_recipe.sh <pkg> ''` is **not** a
fault: it is derived from the *dispatch input* (`packages="pkg:"` → empty),
not from `meta.yaml`, so a trailing colon always stages an unpinned dep.

## Dispatch inputs quick reference

| Input | Notes |
|---|---|
| `packages` | `"name:"` entries, comma-separated; `:` suffix means default version. `ALL` expands to every recipe |
| `prebuild_recipes` | comma-separated, **ordered**, built per-job before packages |
| `python_versions` | defaults to all three; narrow for a quick re-run (e.g. `3.12.13`) |
| `mobile_test_pythons` | default `3.12` — leave it; never `ALL` on this fork. Pass `""` to build wheels WITHOUT the on-device test (e.g. when the test can't pass yet because the fix lives in unreleased serious_python — you'll test locally) |
| `mobile_test_pythons` | default `3.12`. |
| `archs` | default `android,iOS` |
| `python_build_run_id` | a `flet-dev/python-build` Actions run-id whose artifacts to use instead of the pinned release; empty → the hardcoded FALLBACK in `build-wheels-version.yml` (grep `PYTHON_BUILD_RUN_ID: ${{ … || '<id>' }}`). Bump that fallback to ship an unreleased python-build fix to every job |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,72 @@ A real recipe break reproduces on rerun and names a compiler/CMake error.

---

### A recipe flag in `MUPDF_MAKE` / any sub-`make` command line provably has no effect

**Cause:** the upstream build script appends **its own** make arguments *after* the
string it took from your env var, and `make` lets the **last** command-line
assignment win. PyMuPDF's `mupdfwrap.py` does exactly this — it honours
`$MUPDF_MAKE` verbatim and then appends
`' HAVE_GLUT=no HAVE_PTHREAD=yes verbose=yes barcode=yes'`, so the recipe's
`barcode=no` was silently overridden and ~16 MB of ZXing C++ shipped in every
wheel for a feature PyMuPDF exposes no Python API for.

**Fix:** don't trust the flag — **verify the outcome in the built artifact**
(`strings libmupdf.so | grep -c ZXing`), then patch the appending line itself.
The recipe already patches the upstream scripts, so this is one more idempotent
`_mf_edit`: `"verbose=yes barcode=yes"` → `"verbose=yes barcode=no"`. Keep the
`meta.yaml` flag as well and say in a comment that the two must stay in step.
Before flipping a feature off, check the C source has a **stub** rather than
dropping the symbol — MuPDF's `barcode.c` keeps `fz_new_barcode_pixmap` under
`#if !FZ_ENABLE_BARCODE` and throws, so the generated C++ wrapper still links.
(From `recipes/pymupdf`.) The general tell: a `meta.yaml` flag that a build-log
grep shows on the command line *and* whose effect is absent from the binary.

---

### Only one extension in the wheel is missing a link flag forge exports (16 KB alignment, iOS deployment target)

**Cause:** the package links that one library with a build backend that composes
its own link line and never reads `$LDFLAGS`. PyMuPDF's `pipcl` builds
`_extra.so` from `linker_command / general_flags / libpaths / libs /
linker_extra / pythonflags.ldflags / rpath_flag` — forge's `LDFLAGS` is not in
that list, while the other three libraries get everything through MuPDF's own
`make`. Two different symptoms, one cause:

- Android — forge's `_check_elf_alignment` *raises* on the 4 KB `PT_LOAD`,
failing the wheel.
- iOS — no failure at all. The linker just writes a legacy
`LC_VERSION_MIN_IPHONEOS 7.0` where the siblings carry `LC_BUILD_VERSION` /
`minos 13.0`, which only shows up in `otool -l`.

**Fix:** re-add the flags inside the backend, keyed off `CROSS_VENV_SDK`, and
source the values from the environment forge already exports so they cannot
drift (`-Wl,-z,max-page-size=16384` on android; on iOS, re-use the
`-mios-version-min=…` token parsed out of `$CFLAGS`). **Diagnostic:** compare
the load commands of every native file in the wheel against each other —
`llvm-readelf -l` / `otool -l | grep -A3 LC_BUILD_VERSION` — an odd one out is
the one its build backend linked. (From `recipes/pymupdf`.)

---

### The build breaks with no change on your side (unpinned build backend)

**Cause:** the package's `[build-system] requires` names a build backend with
**no version bound**, and forge installs `pyproject["build-system"]["requires"]`
as-is, so every build resolves whatever PyPI serves that day. PyMuPDF requires a
bare `pipcl` — which is simultaneously its PEP 517 backend and the linker for
`_mupdf`/`_extra`, and which the recipe monkeypatches — and pipcl shipped twelve
releases in the four months to 2026-07.

**Fix:** pin it in `requirements.build` (`- pipcl 12`). `install_requirements`
runs before the pyproject requires are installed and targets the same build env,
and a bare `pipcl` requirement is then already satisfied, so the pin wins with no
patching. Raise it deliberately, with a build. **Check for this whenever a recipe
patches or monkeypatches anything in its build backend** — that is the case where
upstream drift becomes your build break. (From `recipes/pymupdf`.)

---

## Runtime failures (on device/emulator/simulator)

### Flet 0.86 changed Android packaging — `sitepackages.zip` + jniLibs relocation (the umbrella behind a whole class of "worked under 0.85, fails now" on-device failures)
Expand Down
10 changes: 4 additions & 6 deletions .claude/skills/local-recipe-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ cp dist/<recipe>-*-android_24_arm64_v8a.whl /tmp/rt_dist/ # forge's dist/ whee
rm -rf tests/recipe-tester/build/site-packages tests/recipe-tester/build/.hash
cd tests/recipe-tester
PIP_FIND_LINKS=/tmp/rt_dist \
uvx --prerelease allow --default-index https://pypi.flet.dev --index https://pypi.org/simple \
--from flet-cli flet build apk --arch arm64-v8a --yes --python-version 3.12
uvx --prerelease allow --with 'flet-cli' --with 'flet' \
flet build apk --arch arm64-v8a --yes --python-version 3.12
cd "$REPO"

# 4. Boot the rootable AVD (gotcha #4/#5), install, launch
Expand Down Expand Up @@ -75,8 +75,8 @@ forge iphoneos:arm64 <recipe> ; forge iphonesimulator:arm64 <recipe> ; forge iph
rm -rf tests/recipe-tester/build/site-packages tests/recipe-tester/build/.hash
cd tests/recipe-tester
PIP_FIND_LINKS="$(realpath ../../dist)" \
uvx --prerelease allow --default-index https://pypi.flet.dev --index https://pypi.org/simple \
--from flet-cli flet build ios-simulator --yes --python-version 3.12 # 0.86 pin — gotcha #13
uvx --prerelease allow --with 'flet-cli' --with 'flet' \
flet build ios-simulator --yes --python-version 3.12 # 0.86 pin — gotcha #13

# 3. Boot any available iPhone sim, install, launch — ALWAYS by explicit UDID
# (gotcha #11: `booted` is ambiguous the moment two sims are booted)
Expand Down Expand Up @@ -138,8 +138,6 @@ for i in $(seq 1 30); do grep EXIT "$DATA/Library/Caches/console.log" 2>/dev/nul

12. **Verify the staged tests + the on-device test COUNT — staging can fail silently.** `stage_recipe.sh` wipes and re-stages `recipe_tests/`; if the invocation ever fails without you noticing (a scripted loop with a bad variable — zsh does NOT word-split unquoted `$VAR` like bash, so a `for r in $RECIPES`-style loop can pass the whole list as ONE argument), the PREVIOUS recipe's tests are still staged and run happily, reporting "N passed" for the wrong package. Two cheap checks after staging: `ls tests/recipe-tester/recipe_tests/` shows YOUR test files, and the "N passed" in console.log matches your recipe's test count. (Bit during the h5py→keras loop: the same 4 stale h5py tests "passed" three times.) **Stronger still — verify the built APK's CONTENTS, not just `recipe_tests/`:** a build that *fails* can leave a STALE `build/apk/recipe-tester.apk` that installs the wrong app entirely. `unzip -l build/apk/recipe-tester.apk` should show your recipe's test `.py` inside `app.zip` AND (for a native recipe) `lib/<abi>/lib*.so` for its libs. Caught an opaque run that silently installed a stale pysodium APK and reported "2 passed" for the wrong package. When in doubt nuke `build/apk` too, not just `build/site-packages`.

13. **Flet >=0.86** — plain `flet build` gets the 0.86+ packaging model; the old `uvx --prerelease allow --default-index https://pypi.flet.dev …` incantation is obsolete (harmless, but stop cargo-culting it). 0.86 ships site-packages as `sitepackages.zip` and relocates native `.so` to jniLibs — a whole class of on-device loader/data-file failures lives there (`forge-error-catalogue` § the `sitepackages.zip` class). **New default trap replacing the old one: 0.86.5's `flet build` bundles Python 3.14 by DEFAULT** — an end-user-default build resolves **cp314** wheels (verified: flet-cv2-example APK shipped `libpython3.14.so` + the cp314 opencv wheel). The loop's explicit `--python-version 3.12` still works and matches `setup.sh 3.12.13`-built recipe wheels; just know that "what users get by default" is now cp314, so a recipe published only for cp312 is invisible to a default build.

## Model assets & test-only deps

`stage_recipe.sh` copies **every** file in `recipes/<pkg>/tests/` into the app (`cp -r tests/. recipe_tests/`), so a model dropped next to the test file becomes an app asset. Two tiers:
Expand Down
Loading
Loading