From 8ed5d590fb807b0346f786ecd1dde76d2efc260b Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 24 Jul 2026 19:48:46 +0200 Subject: [PATCH 1/3] recipe: rapidfuzz 3.14.5 Fast fuzzy string matching (flet-dev/flet#6717). Self-contained scikit-build-core + CMake + Cython C++ package, duckdb/pyzmq archetype, zero patches. The vendored rapidfuzz-cpp and Taskflow deps are header-only and bundled in the sdist, so there is no external native library to build. Notes: - RAPIDFUZZ_BUILD_EXTENSION=1 forces the compiled C++ extension; without it scikit-build-core silently falls back to a pure-Python wheel (no .so). - Its x86 SSE2/AVX2 SIMD variants are separate CMake targets gated on $RAPIDFUZZ_ARCH_X86/X64 (try_compile on __x86_64__/__i386__), so ARM slices build only the scalar path -- no x86 intrinsics reach the ARM compiler. - flet-libcpp-shared is an Android-only host dep (libc++_shared) for the C++ .so; iOS links the system libc++. Locally verified: ios_13_0_arm64_iphonesimulator builds clean (all five compiled modules incl. process_cpp_impl), MH_BUNDLE->MH_DYLIB fix applied, "C++ Extension built successfully". --- recipes/rapidfuzz/meta.yaml | 57 +++++++++++++++++++++++ recipes/rapidfuzz/tests/test_rapidfuzz.py | 39 ++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 recipes/rapidfuzz/meta.yaml create mode 100644 recipes/rapidfuzz/tests/test_rapidfuzz.py diff --git a/recipes/rapidfuzz/meta.yaml b/recipes/rapidfuzz/meta.yaml new file mode 100644 index 00000000..e89fa290 --- /dev/null +++ b/recipes/rapidfuzz/meta.yaml @@ -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 %} diff --git a/recipes/rapidfuzz/tests/test_rapidfuzz.py b/recipes/rapidfuzz/tests/test_rapidfuzz.py new file mode 100644 index 00000000..5a7191da --- /dev/null +++ b/recipes/rapidfuzz/tests/test_rapidfuzz.py @@ -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 From a30fc75b4bceeb96ac8a3a0d7d10f618d14f904a Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 24 Jul 2026 20:45:19 +0200 Subject: [PATCH 2/3] update skills --- .claude/skills/forge-ci/SKILL.md | 14 ++++++++ .claude/skills/forge-error-catalogue/SKILL.md | 5 ++- .../references/failure-catalogue.md | 33 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/.claude/skills/forge-ci/SKILL.md b/.claude/skills/forge-ci/SKILL.md index 112df256..027bad6d 100644 --- a/.claude/skills/forge-ci/SKILL.md +++ b/.claude/skills/forge-ci/SKILL.md @@ -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 --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. diff --git a/.claude/skills/forge-error-catalogue/SKILL.md b/.claude/skills/forge-error-catalogue/SKILL.md index 85e3c93f..8ed16081 100644 --- a/.claude/skills/forge-error-catalogue/SKILL.md +++ b/.claude/skills/forge-error-catalogue/SKILL.md @@ -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; diff --git a/.claude/skills/forge-error-catalogue/references/failure-catalogue.md b/.claude/skills/forge-error-catalogue/references/failure-catalogue.md index aba4c5a1..057de25c 100644 --- a/.claude/skills/forge-error-catalogue/references/failure-catalogue.md +++ b/.claude/skills/forge-error-catalogue/references/failure-catalogue.md @@ -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: ` + 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 --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) From b11ff417e91c39adf7a50a787b88f153dd6e2ed1 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 24 Jul 2026 20:47:53 +0200 Subject: [PATCH 3/3] bump python-build release to 20260720 --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 701b0e1a..380597d3 100755 --- a/setup.sh +++ b/setup.sh @@ -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;