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
14 changes: 14 additions & 0 deletions .claude/skills/forge-ci/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,20 @@ Seen repeatedly on this fork; all are safe to retry once:
- **`gh workflow run` returns HTTP 500** — retry after ~20s; then verify with
`gh run list` that exactly one run was created (a 500 can be
create-then-error).
- **`User for pypi.flet.dev:` → `EOFError: EOF when reading a line`** in a
`Build wheels` step — pypi.flet.dev intermittently answers a `pip install
… --extra-index-url https://pypi.flet.dev …` query with **HTTP 401** (auth
challenge) instead of 404/200; headless pip then prompts for a username and
dies on EOF. It hits build-*tool* resolution (`build wheel
scikit-build-core…`) or a hosted dep (`flet-libcpp-shared…`), so it can red
any leg regardless of package. TELL it apart from a real failure by the
**scattered pattern**: the *same* install command succeeds on sibling legs
(e.g. all 3.14 green, one 3.12/3.13 red) because only some of the parallel
jobs got 401'd. Pure transient — `gh run rerun <id> --failed` clears it
(seen on rapidfuzz 3.14.5: 3/6 legs red on attempt 1, all 6 green on rerun,
no code change). Same root cause blocks *local* android cmake builds, where
it's not transient — see the `forge-error-catalogue` skill (`User for
pypi.flet.dev:` entry).

Real failures reproduce on rerun. Don't retry more than once without reading
the log.
Expand Down
5 changes: 4 additions & 1 deletion .claude/skills/forge-error-catalogue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ instead of re-deriving it.
re-sign; setuptools/Cython/meson already ship dylib), **Apple `MacTypes.h` `Ptr` vs `cv::Ptr` ambiguity** (iOS-only;
hand-written + gen2.py-generated code), **opencv-5 KleidiCV `armv8-a` on x86_64**
and **hardcoded `CMAKE_SYSTEM_PROCESSOR` → ARM asm on the x86_64 sim** (per-arch
fix), **Rust crate with no `target_os="ios"` backend** (`mac_address`, cfg-gate it).
fix), **Rust crate with no `target_os="ios"` backend** (`mac_address`, cfg-gate it),
**`User for pypi.flet.dev:` → `EOFError` at build-tool/host-dep resolution** (the
index 401s, not the recipe — deterministic locally on a fresh cmake cross-venv,
transient/scattered in CI where a rerun clears it).
- **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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,39 @@ uuid-utils 0.17.0 `mobile.patch`.

---

### `User for pypi.flet.dev:` → `EOFError: EOF when reading a line` during a forge build (build-tool / host-dep resolution)

**Symptom:** a build dies not while compiling but while `(build-)pip install …
--extra-index-url https://pypi.flet.dev …` resolves build tools
(`build wheel scikit-build-core>=0.11`, `cmake ninja`) or a hosted dep
(`flet-libcpp-shared>=…`). The tail is `Looking in indexes: https://pypi.org/simple,
https://pypi.flet.dev` → `User for pypi.flet.dev:` → `EOFError: EOF when reading a
line`.

**Cause:** pypi.flet.dev sometimes answers a simple-index query with **HTTP 401**
(auth challenge) instead of 404/200; headless pip treats 401 as "need credentials",
prompts for a username, and gets EOF (no TTY). It is the *index*, not the recipe —
the failing package is often a generic build tool that isn't even hosted there.

**Two flavours, different fixes:**
- **Locally, deterministic on a FRESH cross-venv.** A `forge android:<abi> <cmake-recipe>`
in a venv that has no cached `cmake`/`ninja` hits the 401 every time (iOS often
slips through only because its venv already cached them from a prior build). It
blocks *every* CMake recipe equally (duckdb/pyzmq too) and never reaches compile —
so **validate the Android/v7a build in CI, not locally**, in an env where
pypi.flet.dev 401s. The `flet-lib*` host dep itself downloads fine (it *is* hosted).
- **In CI, transient/scattered.** The same 401 flakes only *some* of the parallel
jobs; the identical install succeeds on sibling legs (e.g. all 3.14 green, one
3.12/3.13 red). Pure infra — `gh run rerun <id> --failed` clears it (rapidfuzz
3.14.5: 3/6 red → all 6 green on rerun, no code change). See the `forge-ci` skill's
"Infra flakes vs real failures".

**Tell it apart from a real failure:** the traceback ends in `handle_401` /
`ask_input` / `EOFError`, and the crash is *before* any `Building CXX object` line.
A real recipe break reproduces on rerun and names a compiler/CMake error.

---

## 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
57 changes: 57 additions & 0 deletions recipes/rapidfuzz/meta.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package:
name: rapidfuzz
version: "3.14.5"

build:
number: 1
script_env:
# rapidfuzz SILENTLY ships a pure-Python wheel (no compiled .so) unless CMake
# is detected AND the C++ build succeeds. Force the extension so a broken
# cross-build fails loudly instead of shipping the slow scalar-Python path.
# (Runtime implementation selection is a *separate* knob — RAPIDFUZZ_IMPLEMENTATION
# — deliberately left unpinned so the compiled module auto-wins at import.)
RAPIDFUZZ_BUILD_EXTENSION: "1"
# {% if sdk == 'android' %}
CMAKE_ARGS: >-
-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
-DCMAKE_CROSSCOMPILING_EMULATOR=/usr/bin/env
-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384
-DCMAKE_MODULE_LINKER_FLAGS=-Wl,-z,max-page-size=16384
-DPython_EXECUTABLE={CROSS_VENV_PYTHON}
-DPython_INCLUDE_DIR={HOST_PYTHON_HOME}/include/python{py_version_short}
-DPython_LIBRARY={HOST_PYTHON_HOME}/lib/libpython{py_version_short}.so
# {% else %}
# The `ninja` pip package crashes importing on the iOS crossenv python
# (sysconfig.get_preferred_scheme('user') -> 'posix_user' invalid on iOS),
# so use the Makefiles generator on iOS; scikit-build-core then never imports
# ninja. (Android keeps Ninja, which works there.)
CMAKE_GENERATOR: Unix Makefiles
CMAKE_ARGS: >-
-DCMAKE_SYSTEM_NAME=iOS
-DCMAKE_OSX_SYSROOT={{ sdk }}
-DCMAKE_OSX_DEPLOYMENT_TARGET={{ sdk_version }}
-DCMAKE_OSX_ARCHITECTURES={{ arch }}
-DCMAKE_CROSSCOMPILING_EMULATOR=/usr/bin/env
-DPython_EXECUTABLE={CROSS_VENV_PYTHON}
-DPython_INCLUDE_DIR={HOST_PYTHON_HOME}/include/python{py_version_short}
-DPython_LIBRARY={HOST_PYTHON_HOME}/lib/libpython{py_version_short}.dylib
# {% endif %}

requirements:
build:
# rapidfuzz builds via scikit-build-core + CMake (Ninja generator); with
# forge's `--no-isolation` these must live in the build venv. scikit-build-core
# and Cython come from the package's own build-system.requires; the PyPI sdist
# ships pre-transpiled .cpp, so Cython never re-runs during the build.
- cmake
- ninja
# {% if sdk == 'android' %}
host:
# rapidfuzz's compiled extensions are C++ (the vendored, header-only
# rapidfuzz-cpp + Taskflow submodules bundled in the sdist). On Android the
# .so links libc++_shared.so, which the device runtime doesn't provide unless bundled.
- flet-libcpp-shared >=27.2.12479018
# {% endif %}
39 changes: 39 additions & 0 deletions recipes/rapidfuzz/tests/test_rapidfuzz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
def test_cpp_extension_loaded():
"""Import the generic compiled module directly — the canary that the C++
extension actually cross-compiled and links its C++ runtime (libc++_shared
on Android). If the recipe had silently fallen back to the pure-Python wheel,
rapidfuzz.fuzz_cpp would be absent and this import would raise."""
import rapidfuzz.fuzz_cpp # noqa: F401


def test_fuzz_ratio():
"""fuzz.ratio runs the compiled scalar edit-distance path. Identical strings
score 100; a small edit scores below 100 but well above 0."""
from rapidfuzz import fuzz

assert fuzz.ratio("hello world", "hello world") == 100.0
partial = fuzz.ratio("hello world", "hallo world")
assert 0.0 < partial < 100.0, partial


def test_levenshtein_distance():
"""distance.Levenshtein exercises the compiled distance module — a known
edit distance proves the metric computes correctly, not just that it loads."""
from rapidfuzz.distance import Levenshtein

assert Levenshtein.distance("kitten", "sitting") == 3
assert Levenshtein.distance("flaw", "lawn") == 2


def test_process_extract_one():
"""process.extractOne drives process_cpp_impl — the module that links
Taskflow (std::thread) and, on 32-bit armeabi-v7a, libatomic for 64-bit
atomics. This is the most cross-compile-fragile module, so pick the best
match from a small list to exercise it end to end."""
from rapidfuzz import process, fuzz

choices = ["apple", "banana", "orange", "pineapple"]
match, score, index = process.extractOne("appel", choices, scorer=fuzz.ratio)
assert match == "apple", match
assert choices[index] == "apple"
assert score > 70.0, score
2 changes: 1 addition & 1 deletion setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ if ! command -v uv &> /dev/null; then
fi

# Pinned flet-dev/python-build release to consume (date-keyed YYYYMMDD, PBS-style).
PYTHON_BUILD_RELEASE="${PYTHON_BUILD_RELEASE:-20260714}"
PYTHON_BUILD_RELEASE="${PYTHON_BUILD_RELEASE:-20260720}"

# Resolve a full X.Y.Z from a bare X.Y minor using the pinned release's
# manifest.json (downloaded + cached under downloads/). Echoes the full version;
Expand Down
Loading