Preserve upstream wheel Python/ABI tag in fix_wheel (#61)#67
Merged
Conversation
fix_wheel was unconditionally rewriting the WHEEL Tag with `self.wheel_tag` (cp3X-cp3X-<platform>), discarding the tag maturin / the upstream backend had emitted. For abi3 crates like cryptography this turned `cp37-abi3-<platform>` into `cp312-cp312-<platform>` and the produced wheel name followed, which was wrong on three counts: - semantically incorrect: the inner _rust.abi3.so is stable-ABI; - unnecessarily restrictive: pip refuses the wheel on cp313+; - wasteful: forces per-Python-version rebuilds. Now `fix_wheel` keeps the existing pythontag-abitag portion and swaps only the platform component, falling back to self.wheel_tag only when the upstream wheel didn't carry a Tag header. Restores the cp3X-abi3-* wheels we shipped before commit 4cf1c1f.
ndonkoHenri
added a commit
that referenced
this pull request
Jun 8, 2026
* bump all packages
recipes: bump 13 major-tier upstreams to latest PyPI
Major-level bumps (X in X.Y.Z) — semver allows breaking API/ABI
changes. Highest risk. Patches/mobile.patch are intentionally left
untouched: CI will surface every recipe whose patch no longer applies,
whose setup.py reorganized, or whose build system flipped. We iterate
from there.
Special notes per recipe (see audit summary):
* cffi 1 → 2 is the keystone — bcrypt, argon2-cffi-bindings,
pycryptodome (transitively via cffi-on-Android) all consume it.
* opaque 0.2.0 → 1.0.0: upstream 1.0.0 tried to fix
install_requires=("pysodium") but shipped a string-not-tuple. Our
mobile.patch likely still needed; verify on first CI failure.
* pillow 11 → 12: meson build system tweaks; the host-dep patches
will need rework.
* regex 2024.* → 2026.* and rpds-py 0.23 → 2026.5 are CalVer
bumps — mechanically major but semantically just continuous
progress.
Every touched recipe also has build.number reset to 1 (was 10).
argon2-cffi-bindings 21.2.0 → 25.1.0
bcrypt 4.2.0 → 5.0.0
cffi 1.17.1 → 2.0.0
markupsafe 2.1.5 → 3.0.3
opaque 0.2.0 → 1.0.0
pandas 2.2.3 → 3.0.3
pillow 11.1.0 → 12.2.0
protobuf 5.28.3 → 7.35.0
regex 2024.11.6 → 2026.5.9 (CalVer)
rpds-py 0.23.1 → 2026.5.1 (scheme switched to CalVer)
time-machine 2.16.0 → 3.2.0
websockets 13.0.1 → 16.0
zope.interface 7.2 → 8.5
recipes: bump 27 minor-tier upstreams to latest PyPI
Minor-level bumps (Y in X.Y.Z) — semver allows new features but no
breaking changes. Most should build and test cleanly; expect occasional
patch refreshes if upstream renamed a setup.py entry point or moved a
header. Patches/mobile.patch are untouched; CI will surface what needs
refreshing.
Every touched recipe also has build.number reset to 1 (was 10).
aiohttp 3.9.5 → 3.14.0
bitarray 3.6.1 → 3.8.1
blis 1.0.0 → 1.3.3
brotli 1.1.0 → 1.2.0
gdal 3.10.0 → 3.13.1
google-crc32c 1.6.0 → 1.8.0
greenlet 3.1.1 → 3.5.1
grpcio 1.67.1 → 1.81.0
jiter 0.8.2 → 0.15.0
jq 1.8.0 → 1.11.0
kiwisolver 1.4.7 → 1.5.0
lru-dict 1.3.0 → 1.4.1
msgspec 0.18.6 → 0.21.1
numpy 2.2.2 → 2.4.6
opencv-python 4.10.0.84 → 4.13.0.92
pendulum 3.0.0 → 3.2.0
pycryptodome 3.21.0 → 3.23.0
pycryptodomex 3.21.0 → 3.23.0
pyjnius 1.6.1 → 1.7.0 (Android-only)
pymongo 4.10.1 → 4.17.0
pynacl 1.5.0 → 1.6.2
pyogrio 0.10.0 → 0.12.1
shapely 2.0.6 → 2.1.2
tiktoken 0.9.0 → 0.13.0
tokenizers 0.21.0 → 0.23.1
yarl 1.11.1 → 1.24.2
zstandard 0.23.0 → 0.25.0
recipes: bump 9 patch-tier upstreams to latest PyPI
Patch-level bumps (Z in X.Y.Z) — semver promises these are bug-fix-only,
so risk is low and patches/mobile.patch should still apply cleanly. We
leave existing patches untouched for this commit and let CI surface any
regressions; if a patch needs a refresh, that's a follow-up.
Per the bump policy: every touched recipe also has build.number reset
to 1 (was 10 — from the 'Bump all recipe build numbers to 10 for
python-build vNext' wave).
contourpy 1.3.1 → 1.3.3
lxml 6.1.0 → 6.1.1 (Jinja `{% set version %}` source)
matplotlib 3.10.0 → 3.10.9
msgpack 1.1.0 → 1.1.2
pyobjus 1.2.3 → 1.2.4 (iOS-only)
pyproj 3.7.0 → 3.7.2
pyyaml 6.0.2 → 6.0.3
ruamel.yaml.clib 0.2.12 → 0.2.15
sqlalchemy 2.0.36 → 2.0.50
* websockets: quote version "16.0" — avoid YAML float coercion
YAML 1.1 parses bare 16.0 as a float, then forge build.py:867 tries
Path.cwd() / 16.0 and raises TypeError. Quoting forces a string. The
other recipes were already fine: most have three-part versions (which
YAML keeps as strings due to multiple dots), and the only other
two-part version on the branch (zope.interface 8.5) was already
quoted as 8.5 in single quotes.
* markupsafe: fix test_speedups_loaded for 3.x API rename
markupsafe 3.0 renamed the C accelerator entry point from "escape"
to "_escape_inner" (the public markupsafe.escape() now dispatches to
it). The recipe-tester asserts callable(_speedups.escape), which
failed on 3.x with AttributeError. Probe whichever name the installed
version exposes so the test stays useful across version bumps.
Verified locally with markupsafe 3.0.3 → both tests pass.
* opencv-python: target 4.12.0.88 (latest with sdist on PyPI)
opencv-python stopped publishing sdists from 4.13.0.90 onward — only
binary wheels for desktop platforms exist for 4.13+. forge's
get_pypi_source_urls() raises KeyError because there is no sdist URL
to fetch. 4.12.0.88 is the latest version that still ships an sdist,
so the recipe can build from source for our cross-targets.
Upstream API delta 4.10.0.84 -> 4.12.0.88 is just 2 minors; no
breaking surface for our use.
* gdal: revert to 3.10.0 — defer the libgdal chain to a separate effort
gdal Python wheels hard-require an exact major.minor match with the
libgdal C library (configure.ac: "Python bindings of GDAL X.Y.Z
require at least libgdal X.Y.Z"). Bumping the Python recipe to 3.13.1
needs flet-libgdal bumped too — and that cascades to fiona, rasterio,
pyogrio, which all currently pass on 3.10.0 and would all break in
the same way.
Net for this bulk-bump pass: leave gdal + flet-libgdal at 3.10.0,
keep the 4 currently-green recipes green, and revisit the libgdal
chain as a dedicated effort once flet-libgdal 3.13.x is published to
pypi.flet.dev (so downstream consumers can pull it transitively).
Reset build.number to 10 (the pre-bump value) so this commit
preserves equivalence with main for the gdal recipe.
* recipes: rebase 4 setup.py mobile.patches for upstream version drift
pycryptodome 3.23.0 + pycryptodomex 3.23.0
python_requires line gained ", !=3.6.*". Patch context updated +
line numbers shifted 521 -> 523. Semantic change unchanged:
install_requires=['cffi'].
opaque 1.0.0
Upstream tried to fix the install_requires problem the recipe was
patching for, but shipped install_requires = ("pysodium") -- string
inside parens is not a tuple; setuptools treats it as a single
string requirement which sometimes works but is brittle. Switched
the patch from "insert install_requires line" to "rewrite the
broken string-as-tuple into a proper list". Context also updated
to match the LGPLv3+ license classifier the 1.0.0 release added.
pynacl 1.6.2
Dropped the cosmetic blank-line hunk (no semantic effect); kept the
cmdclass/distclass commenting hunk, with line numbers shifted from
218 -> 197 because the test_requirements block above shrunk in
1.6.x. Same setup.py outcome as before.
All four verified locally: patch -p1 against fresh sdist applies
cleanly with no rejects.
* shapely: drop mobile.patch — 2.1+ has native env-var cross-compile
shapely 2.1.0 added the official GEOS_INCLUDE_PATH /
GEOS_LIBRARY_PATH env-var path in setup.py. When both are set,
setup.py skips the geos-config probe AND the version-compat check,
and links against geos_c directly. That replaces our 2.0.x-era
mobile.patch's by-hand-commenting of the geos-config block.
* Drop recipes/shapely/patches/mobile.patch entirely.
* Set GEOS_INCLUDE_PATH = {platlib}/opt/include and
GEOS_LIBRARY_PATH = {platlib}/opt/lib in script_env.
* Keep the iOS -undefined dynamic_lookup LDFLAGS so the C
extension can defer libgeos -> libgeos_c static-cascade refs at
link time (same fix pattern as recipes/pyogrio + recipes/fiona).
* pillow: rebase setup patch for 12.x — drop the obsolete add_imaging_libs context
pillow 12.x removed the `self.add_imaging_libs = ""` line that lived
just below `initialize_options` in 11.x, so hunk 1's trailing context
no longer matched. Hunks 2 + 3 still fuzzed cleanly because the lines
they touch were stable.
Renamed setup-11.x.patch -> setup-12.x.patch and bumped the meta.yaml
Jinja selector to `version >= (12,0,0)`. (We were already at 12.2.0
on this branch; nobody downstream is consuming the renamed-away
11.x-only variant.) Hunk 1 line numbers shifted 355 -> 372; line-count
metadata also corrected (8/6 vs 9/7) since one removed line dropped.
Verified locally: patch -p1 against fresh pillow-12.2.0 sdist applies
all three hunks with no rejects.
* recipes: rebase pandas + pyobjus patches for upstream catch-up
pandas 3.0.3
pandas 3.0 bumped its own build-system pin to meson-python>=0.17.1
(we used to pin >=0.16.0), so hunk 1 of mobile.patch is now a
no-op. Dropped it. The [tool.meson-python] meson="meson-wrapper.py"
hunk + the meson-wrapper.py file are still needed for our
cross-build (forces meson to invoke under the cross-Python so
sysconfig probes hit the TARGET, not the BUILD host). Line numbers
for the append-section hunk shifted 809 -> 833 because upstream
grew the codespell ignore list.
pyobjus 1.2.4
Heavy upstream cleanup: the old "iOS vs darwin platform-fork" in
setup.py is gone (now always builds from pyobjus.pyx via Cython 3),
and most of the Py2 leftovers in pyobjus_conversions.pxi were
removed too (`type(arg) is long`, `<long>long(arg)`, etc.). Our
mobile.patch shrunk from 9 hunks to 3:
- keep _runtime.h ffi/ffi.h -> ffi.h
- keep common.pxi ffi/ffi.h -> ffi.h (line 109 -> 110)
- keep one remaining isinstance(arg, (str, unicode)) -> str hunk
at line 428 (last Py2 isinstance check upstream missed)
Dropped: 4 setup.py hunks (now redundant) + 2 conversions.pxi hunks
(already done upstream).
Both verified locally: patch -p1 against fresh sdist applies cleanly.
* recipes: rebase blis + grpcio + pyjnius; drop pendulum patch (upstream fix)
blis 1.3.3
- Major file restructure in upstream: bli_pthread.c's barrier impl
moved from a #elif after bli_pthread_barrier_wait (line 594 in 1.0)
to a standalone #if defined(__APPLE__) || defined(_MSC_VER) block
near the top (line 322). Rewrote the hunk for the new location.
- blis.h hunk dropped entirely: upstream now ships
`#if defined(__APPLE__) || defined(__ANDROID__)` for the barrier
typedefs at line 1540, so our patch is a no-op.
- setup.py hunks 1-3 fuzz-matched cleanly at new line numbers;
hunk 4 (-mios-version-min) regenerated with current offsets.
grpcio 1.81.0
- Dropped the third_party/zlib/zutil.h fdopen-redefine hunk: the
vendored zlib was updated and no longer contains the
`#ifndef fdopen / #define fdopen(fd,mode) NULL` block at all.
Our `#if !defined(__APPLE__)` wrapper is now unneeded.
- setup.py hunks (CARES Android config, OPENSSL_ROOT_DIR, etc.)
still apply cleanly.
pendulum 3.2.0
- Patch DELETED entirely. Our patch was a 32-bit overflow fix in
rust/src/helpers.rs (`usize/isize` arithmetic overflowing on
armeabi-v7a/x86). Upstream 3.2 rewrote local_time() to use i64
throughout — naturally 64-bit, no overflow. Our patch and the
`patches:` line in meta.yaml are gone.
pyjnius 1.7.0
- The MetaJavaClass.__new__ proxy block was refactored to use
NewObjectArray + SetObjectArrayElement instead of malloc + direct
indexing. Our patch's intent (redirect FindClass calls through
PyJni_FindClass so they see Flet's app classloader) still
applies — rewrote the failing hunk to match the new code shape
(single SetObjectArrayElement(... PyJni_FindClass(...)) instead
of two FindClass replacements). All other hunks fuzz-matched.
All four verified locally with patch -p1 against fresh sdists, no
rejects.
* recipes: drop numpy mobile patch (upstreamed); cap tokenizers at 0.22.2
numpy 2.4.6
Drop recipes/numpy/patches/mobile-2.2.2.patch entirely. The patch
was a one-hunk tweak to vendored-meson's AppleDynamicLinker.
get_std_shared_module_args (-bundle -> -dynamiclib for iOS shared
modules). Numpy upstreamed this in 2.4.x: vendored-meson's
AppleDynamicLinker now branches on `if self.system == 'ios': return
['-dynamiclib']` natively. The recipe's meta.yaml moves the
`patches:` line inside the `version < (2, 0)` Jinja branch so the
legacy 1.26.4 patch still applies if someone ever pins back, while
2.x recipes build patch-free.
tokenizers 0.22.2
Backed off from the bulk-bump's 0.23.1 target. tokenizers 0.23.1
introduced a process-wide BPE cache generation counter as
`static NEXT_CACHE_ID: AtomicU64` in src/models/bpe/model.rs.
AtomicU64 isn't available on `arm-linux-androideabi` (armeabi-v7a)
because ARMv7-A doesn't have native 64-bit atomic ops — the build
fails with `error[E0432]: unresolved import
std::sync::atomic::AtomicU64`. 0.22.2 is the latest stable release
without this dep. Re-attempt 0.23+ once we carry a recipe-side
patch swapping AtomicU64 for the portable-atomic crate, or after
upstream gates the cache id on `cfg(target_has_atomic = "64")`.
* ci: disable concurrency cancel-in-progress during bumps iteration
Comment out the concurrency block so quick follow-up pushes during the
recipe-bump iteration each get their own full CI run (instead of older
runs getting killed mid-flight). Will restore once the bumps branch
settles.
* blis + grpcio: fix the two compile failures CI surfaced
blis 1.3.3
Re-add the blis.h barrier-typedef hunk I incorrectly dropped last
pass. Upstream did NOT extend the gate to include __ANDROID__
(only kept __APPLE__); my earlier grep was on a fuzz-matched
intermediate state, not the fresh sdist. Without this hunk Android
falls into the typedef pthread_barrier_t bli_pthread_barrier_t
branch -> bli_pthread.c then references barrier->mutex/cond/count
/tripCount on the bionic native struct, which only has opaque
internal members -> "no member named mutex in pthread_barrier_t".
iOS happened to pass because Apples
* pyobjus: bypass new iOS build-time check for ios-deps-install/
pyobjus 1.2.4's setup.py added a hard RuntimeError when the
ios-deps-install/ directory is missing -- upstream's
.ci/build_ios_dependencies.sh produces it (their own libffi build).
Mobile-forge supplies libffi via the recipe's libffi host dep
(headers + libs under {platlib}/opt, surfaced through CFLAGS /
CPPFLAGS / LDFLAGS), so we don't want that check fatally aborting.
Replaces the iOS branch in setup.py with a minimal libraries.append
("objc") and a comment pointing at the rationale; lets forge's build
env handle libffi paths.
Verified locally: patch -p1 against fresh pyobjus-1.2.4 sdist applies
all 4 hunks (_runtime.h, common.pxi, pyobjus_conversions.pxi, setup.py)
with no rejects.
* opencv-python: drop bogus a.diff hunk + add cmake build dep; pillow: disable brotli
opencv-python 4.12.0.88
Android: the mobile.patch had a stray empty-file diff at the very
top declaring a.diff (mode 100644, hash e69de29). patch choked on
it in CI -- "1 out of 1 hunk FAILED -- saving rejects to file
pyproject.toml.rej" was misleading; the actual choke was on the
zero-byte a.diff block confusing patch's parser. Removed.
iOS: opencv-python's setup.py runs its own dep probe that hard
requires cmake>=3.5 on PATH. macOS runners pre-install cmake but
forge's iOS cross-venv stripped it from the shimmed PATH. Add the
pip-installable cmake package to requirements.build so it lands
in the build venv's bin/.
pillow 12.2.0 (iOS link failure)
Pillow 12 added brotli to the optional-feature list and links
-lbrotlicommon -lbrotlidec whenever its headers are reachable.
The iOS cross site-packages exposes brotli headers (via the python
xcframework include tree), so feature.brotli sticks, but no
matching static archive is in our link search path -> linker
fails with "ld: library 'brotlicommon' not found". Force
self.disable_brotli = True AFTER pillow's feature-detection loop
in setup-12.x.patch's initialize_options hunk, so the detection
result is overridden and brotli stays out of LIBS.
Verified locally: opencv-python patch applies all hunks with no
rejects; pillow patch still applies, with disable_brotli = True
landing right before the raqm/fribidi vendor loop.
* recipes: defer 3 bumps — opaque, pynacl, shapely — each blocked by a dep-chain dependency
These three pass at the BUILD step on the bump-target version but fail
at the recipe-tester RUNTIME, each for a different reason that is
out-of-scope for this bulk-bump branch:
opaque 1.0.0 -> 0.2.0
Wheel builds, imports. test_registration_and_credential_roundtrip
fails on both Android and iOS with
"AttributeError: undefined symbol: opaque_FinalizeRequest_core".
opaque 1.0 calls a libopaque C symbol that does NOT exist in the
currently-shipping `flet-libopaque 0.99.8` host dep. Needs
flet-libopaque bumped + republished in lockstep. Same dep-chain
pattern we deferred for gdal.
pynacl 1.6.2 -> 1.5.0
pip resolution fails at build with
"ERROR: No matching distribution found for cffi>=2.0.0".
pynacl 1.6 bumped its cffi runtime/build requirement from cffi>=1.4
to cffi>=2.0. We bumped cffi to 2.0.0 in the bulk pass but the new
cffi wheels are not yet published to pypi.flet.dev. Defer pynacl
until cffi 2.0 is published, then reland.
shapely 2.1.2 -> 2.0.6 (iOS only — Android still passes 2.1.2)
Android tests all green. iOS test fails at import time with
"ImportError: dlopen ... symbol not found in flat namespace
'__ZN4geos4geom23GeometryComponentFilter9filter_roEPKNS0_8GeometryE'"
(geos::geom::GeometryComponentFilter::filter_ro). The C++ virtual
method shapely 2.1's wheel was linked against doesn't exist in our
flet-libgeos 3.13.0 build at runtime on iOS (Android picks it up
via the shared libgeos.so; iOS uses the static archive and the
resolver misses). Needs flet-libgeos rebuilt with the extra
symbol, or a recipe-side patch. Restored the 2.0.6 mobile.patch
(the env-var approach added for 2.1+ isn't needed at 2.0).
Recap: 12 bumps in this batch are still green (gdal, markupsafe,
pillow, pycryptodome/x, websockets, and friends). 3 deferred. After
the matching infra bumps land (flet-libopaque + flet-libgeos), and
once cffi 2.0 is published, these three can come back in one focused
follow-up.
* opencv-python + pillow: finish off the 3 failures CI surfaced last sweep
opencv-python 4.12.0.88
Drop the obsolete pyproject.toml hunk that tried to relax
setuptools==59.2.0 to bare setuptools. Upstream 4.12 restructured
that pin into two python_version-conditional lines:
"setuptools==59.2.0; python_version<'3.12'",
"setuptools<70.0.0; python_version>='3.12'",
Our hunk's context no longer matched, breaking the whole patch on
Android. Removed -- the python_version>='3.12' branch (setuptools
<70) is fine for our build env.
Add a 2nd Ptr-prefix hunk for pyopencv_videoio.hpp:120
("Ptr<cv::IStreamReader>" -> "cv::Ptr<cv::IStreamReader>"). The
existing patch only prefixed the VideoCapture overload at line 26;
iOS clang flagged the IStreamReader overload too with
"reference to 'Ptr' is ambiguous".
pillow 12.2.0 iOS
My earlier disable_brotli = True hook landed correctly but didn't
actually keep brotli out of the link line, because pillow 12 has a
HARDCODED iOS-specific libs.extend at setup.py line ~1001:
if sys.platform == "ios":
libs.extend(["z", "bz2", "brotlicommon", "brotlidec", "png"])
That bypasses the feature.brotli check entirely. Added a 4th hunk
to drop "brotlicommon", "brotlidec" from that list -- libs.extend
becomes ["z", "bz2", "png"] on iOS. Android isn't affected (this
block is iOS-only).
All verified locally with patch -p1 against fresh sdists; both
recipes' patches apply cleanly with no rejects.
* pillow iOS: also drop png from the hardcoded transitive-deps list
The brotli removal exposed the next layer: pillow 12 hardcodes
`-lpng` on iOS for the _imagingft extension too, and our build
doesn't provide libpng either (pillow recipe comment notes
"PNG support is internal: libpng is not used"). The wheel link
fails with `ld: library png not found`. Trim png from
libs.extend so only [z, bz2] remain.
Android isn't affected (the block is iOS-only).
* ci: add prebuild_libs input — validate native-lib chain bumps in one run
Adds a workflow_dispatch input `prebuild_libs` and a new `prebuild`
job that runs BEFORE the main `build` job. When the input is set,
each listed native-lib recipe (typically a `flet-lib*`) is built
first; its dist/*.whl is uploaded as an artifact named
`prebuild-<platform>-<recipe>-<run_id>-<run_attempt>`. The `build`
job then downloads every matching artifact at start, copies the
wheels into its own `dist/`, and exports
`PIP_FIND_LINKS=$GITHUB_WORKSPACE/dist` so forge's host-dep pip
resolution prefers the freshly-built wheels over whatever
pypi.flet.dev has published.
End-to-end chain-dep validation in a single dispatch — instead of
the current 3-step dance:
1. publish flet-libgdal 3.13.1 to pypi.flet.dev
2. wait
3. dispatch gdal/fiona/rasterio/pyogrio against the new lib
becomes one shot:
gh workflow run build-wheels.yml --ref <branch> \
-f prebuild_libs="flet-libgdal:3.13.1" \
-f packages="gdal:3.13.1,fiona:1.10.2,rasterio:1.5.1,pyogrio:0.12.1" \
-f archs="android,iOS"
Once green, you know the chain works -- publish flet-libgdal for
real and re-dispatch the consumers without prebuild_libs.
Implementation shape:
* The `setup` job gains a second output, `prebuild_matrix`, computed
the same way as the main matrix but over `prebuild_libs` instead
of `packages`. Emits `{"include":[]}` when unset.
* The `prebuild` job is a stripped-down clone of `build` (checkout +
free disk + setup uv + Setup Rust + NDK install + `forge <arch>
<package>`), followed by an `actions/upload-artifact` step that
uploads dist/*.whl. Skipped via `if:` when prebuild_libs is empty
or the matrix is empty.
* The `build` job's `needs:` adds `prebuild`. Its `if:` allows
`prebuild.result` to be 'skipped' (input unset) or 'success'
(input set + all libs built); 'failure' propagates and the build
job is skipped, since consuming a broken chain dep is doomed.
* A new "Seed dist/ from prebuild artifacts" step is added BEFORE
"Build wheels". Uses `gh run download` with a per-platform pattern
so iOS builds don't end up with Android wheels in their dist/
(and vice versa). Runs only when `inputs.prebuild_libs != ''`.
* `PIP_FIND_LINKS=$GITHUB_WORKSPACE/dist` is added to the Build
wheels step env. Harmless when dist/ is empty (forge's own output
dir; pip just sees an empty directory listing).
No change to existing push / pull_request triggers — those don't
set prebuild_libs, so prebuild is always skipped and the build job
runs unchanged.
* pynacl: re-bump to 1.6.2 to validate via cffi chain prebuild
Re-apply the pynacl 1.6.2 bump that we deferred earlier (commit
48709e1). Going to dispatch this branch with prebuild_libs="cffi:2.0.0"
so the workflow builds cffi 2.0.0 wheels FIRST, seeds them into the
build job dist/, and pynacl 1.6.2 should then resolve cffi>=2.0.0 via
PIP_FIND_LINKS instead of failing at the pypi.flet.dev probe.
If green, that validates both: (a) prebuild_libs works end-to-end,
and (b) the cffi 2.0 chain is safe to publish.
* opaque chain: bump flet-libopaque 1.0.1 + opaque 1.0.0 for prebuild test
Re-stage the opaque chain bump we deferred earlier (commit 48709e1).
The runtime test had failed with
"AttributeError: undefined symbol: opaque_FinalizeRequest_core" --
opaque 1.0.0's Python wrapper calls that symbol but flet-libopaque
0.99.8 (the host dep) was built from libopaque <= 0.99.x which doesn't
export it. Verified against stef/libopaque GitHub: tags v0.99.8/v0.99.10
contain zero occurrences of opaque_FinalizeRequest_core; v1.0.0/v1.0.1
contain 2 occurrences each.
* flet-libopaque: 0.99.8 -> 1.0.1, build_number reset to 1. Existing
src/makefile patch applies cleanly to v1.0.1.
* opaque: 0.2.0 -> 1.0.0, build_number reset to 1, host dep updated
to flet-libopaque 1.0.1. mobile.patch rewrites upstream 1.0.0's
broken install_requires = ("pysodium") (string-in-parens, not a
tuple) to a proper list install_requires=["pysodium"].
Dispatch shape:
gh workflow run build-wheels.yml --ref ci/prebuild-libs-input \
-f prebuild_libs="flet-libopaque:1.0.1" \
-f packages="opaque:1.0.0" \
-f archs="android,iOS"
* gdal chain: bump flet-libgdal 3.13.1 + sync 4 consumers for prebuild test
The gdal chain was the largest of the deferred bumps from the bulk-bump
pass: bumping the GDAL Python bindings to a new major.minor needs
flet-libgdal (the host C library) bumped to the matching version AND
fiona / rasterio / pyogrio all re-pinned in lockstep, since their
extension modules link against libgdal directly.
Bumps:
* flet-libgdal: 3.10.0 -> 3.13.1, build_number reset to 1. build.sh
uses generic CMake flags (GDAL_BUILD_OPTIONAL_DRIVERS=OFF,
OGR_BUILD_OPTIONAL_DRIVERS=OFF, GDAL_USE_*=OFF) that should remain
compatible across the 3.10 -> 3.13 jump.
* gdal: 3.10.0 -> 3.13.1, build_number reset to 1, host dep updated
to flet-libgdal 3.13.1, GDAL_VERSION script_env synced.
* fiona: kept at 1.10.1 (latest stable), host dep + GDAL_VERSION
script_env updated to 3.13.1, build_number reset to 1.
* rasterio: kept at 1.5.0 (latest stable), host dep + GDAL_VERSION
script_env updated to 3.13.1, build_number reset to 1.
* pyogrio: kept at 0.12.1 (latest stable), host dep + GDAL_VERSION
script_env updated to 3.13.1, build_number bumped 1 -> 2.
Dispatch shape (validates the whole chain end-to-end without
round-tripping flet-libgdal through pypi.flet.dev first):
gh workflow run build-wheels.yml --ref ci/prebuild-libs-input \
-f prebuild_libs="flet-libgdal:3.13.1" \
-f packages="gdal:3.13.1,fiona:1.10.1,rasterio:1.5.0,pyogrio:0.12.1" \
-f archs="android,iOS"
* shapely chain: bump flet-libgeos 3.14.1 + shapely 2.1.2 + iOS force_load fix
Re-stage the shapely chain bump deferred in 48709e1 (commit message
explained the runtime failure). Issue and fix:
The defer commit reverted shapely 2.1.2 -> 2.0.6 (Android worked at
2.1; iOS dlopen failed at import with "symbol not found in flat
namespace '__ZN4geos4geom23GeometryComponentFilter9filter_roEPKNS0_8GeometryE'"
-- the C++ mangled name for `geos::geom::GeometryComponentFilter::filter_ro(const Geometry*)`).
Root cause is the static-link C++ vtable elision: shapely 2.1's C
extension subclasses GeometryComponentFilter and overrides the virtual
filter_ro(); the base-class vtable lives in libgeos.a's object file
but the linker only pulls object files that satisfy *direct* symbol
references -- vtables expected by C++ dispatch aren't included. At
dlopen, dyld eagerly resolves the flat namespace and aborts on the
missing vtable entry. Android picks the symbol up via shared
libgeos.so, so this only ever bit iOS.
Fix: add `-Wl,-force_load,{platlib}/opt/lib/libgeos.a` to shapely's
iOS LDFLAGS. force_load pulls every object out of libgeos.a -- the
base-class vtable comes along, dyld is satisfied. Kept the existing
`-undefined dynamic_lookup` for libgeos_c-side externals shapely.so
defers to dlopen.
This static-cascade pattern is the same family as fiona/pyogrio/rasterio,
but those only needed the dynamic_lookup half because their missing
symbols were from *external* deps of libgdal (proj/tiff/curl) -- not
C++ vtables internal to libgdal itself.
Bumps:
* flet-libgeos: 3.13.0 -> 3.14.1 (latest stable, build_number reset
to 1). build.sh's generic CMake invocation should remain compatible.
* shapely: 2.0.6 -> 2.1.2 (latest stable, build_number reset to 1).
Restored the 2.1 env-var cross-compile path (GEOS_INCLUDE_PATH /
GEOS_LIBRARY_PATH) added by 49775ba, dropped the 2.0.x mobile.patch
(no longer needed -- and `recipes/shapely/patches/` removed since
it's now empty).
Dispatch shape:
gh workflow run build-wheels.yml --ref ci/prebuild-libs-input \
-f prebuild_libs="flet-libgeos:3.14.1" \
-f packages="shapely:2.1.2" \
-f archs="android,iOS"
* flet-libgdal: patch GDAL 3.13 to skip Python detection when bindings off
The gdal chain dispatch (run 27070408877) failed at the prebuild step
because GDAL 3.13's CMakeLists.txt unconditionally calls
`find_package(Python 3.8 COMPONENTS Interpreter Development NumPy)`
at line 229 -- regardless of -DBUILD_PYTHON_BINDINGS=OFF, which our
build.sh already passes. Under CMake 4.x the search trips CMake policy
CMP0190:
Python: When cross-compiling, Interpreter and/or Compiler components
cannot be searched when CMAKE_CROSSCOMPILING_EMULATOR variable is not
specified (see policy CMP0190).
This is a no-op block for our build -- we have BUILD_PYTHON_BINDINGS=OFF
and want zero Python detection -- so the cleanest fix is to wrap the
find_package(Python) block (and its Python_NumPy follow-up) in
`if (BUILD_PYTHON_BINDINGS OR NOT DEFINED BUILD_PYTHON_BINDINGS)`.
When mobile-forge passes -DBUILD_PYTHON_BINDINGS=OFF the variable is set
to a falsy value, the guard fails, and the whole Python detection is
skipped. For upstream users who don't define the cache variable, the
NOT DEFINED clause keeps default behavior intact.
Verified the patch applies cleanly to upstream GDAL v3.13.1 CMakeLists.txt
via `patch -p1 --dry-run`.
* flet-libgeos: pin 3.13.1 (3.14 breaks iOS via thread_local in Interrupt.cpp)
GEOS 3.14.0 introduced a `CurrentThreadInterrupt::ThreadCallback`
mechanism in src/util/Interrupt.cpp that uses C++ `thread_local`
storage. iOS clang refuses this with:
error: thread-local storage is not supported for the current target
thread_local geos::util::CurrentThreadInterrupt::ThreadCallback* callback_thread = nullptr;
iOS theoretically supports TLS as of iOS 9.0+, but the iOS simulator
target (x86_64 13.0 in our build) hits a known clang/libc++ ABI gap
that rejects `thread_local` for static class members. Android builds
3.14.1 fine but iOS fails at 98% of the libgeos compile.
GEOS 3.13.1 (latest 3.13.x patch release) doesn't have the thread_local
addition -- src/util/Interrupt.cpp still uses plain `bool requested`
and `Callback* callback` globals. Pinning there gives us a small stable
bump (3.13.0 -> 3.13.1) without the iOS-breaking change.
The GeometryComponentFilter::filter_ro vtable that motivated the shapely
chain has been in libgeos.a since GEOS 3.0+ -- the original vtable
elision failure on iOS is fixed by shapely's new `-Wl,-force_load,libgeos.a`
LDFLAGS in 49f6...d635a19, not by the GEOS version bump itself.
* flet-libgdal: pass BUILD_PYTHON_BINDINGS=OFF in iOS build.sh too
The previous patch (d5919ae) wrapped GDAL CMakeLists.txt's Python
detection block in
`if (BUILD_PYTHON_BINDINGS OR NOT DEFINED BUILD_PYTHON_BINDINGS)`,
which fences off the find_package(Python) call only when the user
explicitly passes -DBUILD_PYTHON_BINDINGS=OFF. The condition is FALSE
both when undefined (the NOT DEFINED clause was the bug -- I wrote it
to be permissive for upstream non-mobile users, but our cross-compile
always wants the block off) and when explicitly OFF.
Android already passes -DBUILD_PYTHON_BINDINGS=OFF on line 31; iOS did
not. Add the matching flag to the iOS cmake invocation so the patched
condition evaluates FALSE and the block is fully skipped.
Verified by re-reading the run 27070846249 log: the find_package error
moved from CMakeLists.txt:229 (pre-patch) to CMakeLists.txt:235
(post-patch -- the line offset matches the 6 new lines my patch
inserted above). Block was still being entered because the iOS
build.sh never defined BUILD_PYTHON_BINDINGS at all.
* gdal: rebase config.patch for upstream 3.13.1 setup.py drift
Upstream gdal/setup.py reorganized between 3.10.0 (the prior recipe
version) and 3.13.1:
* `libraries = ['gdal']` moved from line 54 to line 57 (the file
acquired an extra `gcore/multidim` include dir entry in the
hardcoded `/home/runner/work/gdal/gdal/python_pkgs` block).
* `get_gdal_config()` moved from line 228 to line 242 (intervening
build_ext-class refactors added a few setup-time methods).
Re-anchor both hunks to the new line numbers and replace the legacy
`/home/even/gdal/3.10/...` hardcoded path context with the new
`/home/runner/work/gdal/gdal/python_pkgs/...` path that the upstream
release build now bakes in. Hunk logic unchanged -- the GDAL_LIBS
env-var override and the per-option GDAL_<OPTION> env-var lookup
stay in place.
Verified by extracting the gdal-3.13.1.tar.gz sdist from PyPI,
git-initializing it, and running `patch -p1 --dry-run` against the
new patch: applies cleanly.
* flet-libgdal: pass GDAL_USE_OPENMP=OFF on both arches
The Android consumer wheels (gdal, fiona, rasterio, pyogrio) all built
cleanly against the freshly prebuilt flet-libgdal 3.13.1 but failed
at recipe-tester runtime on the Android emulator with:
ImportError: dlopen failed: library "libomp.so" not found
GDAL 3.13 added OpenMP-based parallelism in a handful of code paths
(checksum, JP2 codec, raster reprojection). cmake/helpers/CheckDependentLibraries.cmake:438
runs `gdal_check_package(OpenMP "Whether GDAL should use OpenMP"
COMPONENTS "C" CAN_DISABLE)`, which auto-enables OpenMP whenever
CMake's FindOpenMP succeeds. On the Linux runner cross-compiling
for Android, FindOpenMP discovers the host's libgomp, adds
`-fopenmp` to the link line, and stamps a DT_NEEDED libomp.so into
the resulting libgdal.so. Android's Python runtime doesn't ship
libomp.so, so dlopen aborts on the first import of any module
linked transitively through libgdal.so. iOS escaped because
FindOpenMP gave up earlier in the toolchain (no host libomp visible
to the iOS sysroot search), but the disable is symmetric defense
in depth on the iOS side too.
The CAN_DISABLE clause on the OpenMP gdal_check_package call means
`-DGDAL_USE_OPENMP=OFF` is the supported way to opt out. Same
pattern as the existing OPENSSL / CURL / EXPAT / LIBXML2 disables
we already pass on Android.
Symptom shown by gdal_test_chain dispatch 27071503295: all 4 Android
consumers fail at first `import` (ImportError on libomp.so missing
for fiona/rasterio/pyogrio; ModuleNotFoundError for `osgeo._gdal`
because Python masks the dlopen failure as a missing-module error
when the .so file is present but un-loadable). iOS consumer wheels
all passed.
* ci: fold prebuild_libs into the build job's Build wheels step
Refactor 50ec338's two-job chain-dep staging into a single shell loop
inside the existing Build wheels step. Mechanically equivalent —
`prebuild_libs="flet-libgdal:3.13.1"` still builds the lib first, the
wheel still lands in dist/, PIP_FIND_LINKS still steers the consumer's
host-dep pip resolution at it -- but the workflow shrinks from 569
lines to 425 (−165, +21).
What goes away:
* setup job's `prebuild_matrix` output + the 50-line `set-prebuild-matrix`
step that duplicated the main matrix's arch fan-out
* the entire `prebuild` job (75 lines: own runner setup, NDK install,
forge invocation, artifact upload)
* the build job's `Seed dist/ from prebuild artifacts` step (20 lines:
`gh run download` + copy into dist/)
* `build.needs: [setup, prebuild]` → `build.needs: setup`
* the `if: always() && needs.setup.result == 'success' &&
(needs.prebuild.result == 'success' || skipped)` propagation gate
What replaces it (5 lines inside Build wheels):
if [[ -n "${PREBUILD_LIBS:-}" ]]; then
for lib in $(echo "$PREBUILD_LIBS" | tr ',' ' '); do
forge "$FORGE_ARCH" "$lib"
done
fi
Trade-off vs. the prior two-job design: for a chain like gdal (one lib
+ 4 consumers) the lib now rebuilds inside every consumer-arch runner
— 4× per arch instead of 1× per arch. ~+60% runner-minutes on the
worst case. Wall-clock is unchanged (the prebuild was on the critical
path either way), and for the smaller chains (pynacl, opaque, shapely
— each one consumer) the cost is identical to before. Acceptable price
for the YAML being much easier to read and reason about, especially
the failure semantics (no more `needs:` + result-juggling), and the
artifact upload/download dance disappears entirely.
Existing push / pull_request triggers unaffected — they don't set
prebuild_libs, the `if` guard skips the loop, the consumer build runs
exactly as it did before 50ec338.
* recipes: fix build_number on gdal chain consumers for pip wheel selection
The gdal chain bump (flet-libgdal 3.10.0 -> 3.13.1) republishes
fiona and rasterio at the SAME upstream version (1.10.1, 1.5.0)
but linked against the new libgdal. PEP 427 wheel selection picks
the higher build_tag when (name, version) collide -- so the rebuild
must outrank what's currently on pypi.flet.dev (both at build 10),
or downstream installs silently keep the old libgdal-3.10-linked
wheel and the chain bump becomes invisible.
* fiona 1.10.1: build_number 1 -> 11 (must beat fiona-1.10.1-10)
* rasterio 1.5.0: build_number 1 -> 11 (must beat rasterio-1.5.0-10)
* pyogrio 0.12.1: build_number 2 -> 1 (no collision since version
changed from 0.10.0; bring in line with the branch's
"version bump -> build_number = 1" convention)
Verified the published wheels via pip download against
https://pypi.flet.dev (android_24_x86_64, ios_13_0_arm64_iphoneos
both at build 10 for fiona / rasterio / pyogrio / gdal).
* Preserve upstream wheel Python/ABI tag in `fix_wheel` (#67)
fix_wheel was unconditionally rewriting the WHEEL Tag with
`self.wheel_tag` (cp3X-cp3X-<platform>), discarding the tag maturin /
the upstream backend had emitted. For abi3 crates like cryptography
this turned `cp37-abi3-<platform>` into `cp312-cp312-<platform>` and
the produced wheel name followed, which was wrong on three counts:
- semantically incorrect: the inner _rust.abi3.so is stable-ABI;
- unnecessarily restrictive: pip refuses the wheel on cp313+;
- wasteful: forces per-Python-version rebuilds.
Now `fix_wheel` keeps the existing pythontag-abitag portion and
swaps only the platform component, falling back to self.wheel_tag
only when the upstream wheel didn't carry a Tag header. Restores
the cp3X-abi3-* wheels we shipped before commit 4cf1c1f.
Co-authored-by: Feodor Fitsner <feodor@appveyor.com>
* restore ci concurrency
---------
Co-authored-by: Feodor Fitsner <feodor@appveyor.com>
ndonkoHenri
added a commit
to ndonkoHenri/mobile-forge
that referenced
this pull request
Jun 10, 2026
PR flet-dev#67 (preserve upstream Python/ABI tag in fix_wheel) was reverted in 00b7239 because the abi3 tokenizers wheel was a fake-abi3 (had a DT_NEEDED libpython3.12.so). post-revert, fix_wheel always emits `cpXY-cpXY` tags and never `cp37-abi3`. The renamer's `|abi[0-9]+` alternation and the matching cp37-abi3 comment now describe a shape we no longer produce, so simplify both branches (android lane + iOS lane) to match the actual output.
ndonkoHenri
added a commit
that referenced
this pull request
Jun 11, 2026
* ci: add multi-Python matrix dimension (3.12 / 3.13 / 3.14)
Extends `build-wheels.yml` from `archs × packages` to a 3D matrix
`archs × pythons × packages`. The Python set is chosen per-event so PR
iteration stays fast (cp312 only) while push-to-main publishes the full
cp312/cp313/cp314 wheel set to pypi.flet.dev.
Plumbing:
* New `python_versions` workflow_dispatch input — comma-separated
patches (e.g. "3.12.13,3.13.13,3.14.5"). Empty = event default.
* New env vars `DEFAULT_PYTHONS_PUBLISH` ("3.12.13,3.13.13,3.14.5")
and `DEFAULT_PYTHONS_DEV` ("3.12.13"). Replaces the single
`UV_PYTHON: "3.12.13"` env that was previously hardcoded.
* New `setup.detect-pythons` step picks the active set: input wins,
else push-to-main = PUBLISH, else = DEV.
* `setup.set-matrix` gains an outer `for py in $PYTHONS` loop. Each
matrix entry now carries `python_version` (full, e.g. 3.13.13) and
`python_short` (minor, e.g. 3.13). Display names + artifact names
gain a `py3.X` prefix, e.g. "py3.13 android: pillow 12.2.0 #1"
and `py3.13-android-pillow`.
* `build.Build wheels` step env declares
`UV_PYTHON: ${{ matrix.python_version }}`. setup.sh already accepts
the version as $1 and pulls the matching python-build support
tarball from flet-dev/python-build/releases/v${minor} -- no
setup.sh / forge changes required.
Test phase gating:
The recipe-tester runtime (the APK / iOS sim app produced by
`flet build apk`) targets a 3.12 interpreter. Wheels built for
cp313 / cp314 are still produced and (on push-to-main) published,
but their runtime tests are skipped here until Flet's runtime
catches up. `detect-tests` early-returns `has_tests=false` when
matrix.python_short != 3.12 so KVM setup, emulator boot, APK build,
iOS sim build, etc. all skip cleanly.
Iteration knobs:
* `packages` workflow_dispatch default + the `detect-packages`
SMOKE_TEST fallback both change from `pydantic-core:2.33.2` to
`pydantic-core:2.33.2,numpy:2.4.6,pillow:12.2.0`. Pushing any
non-recipe file (like this commit) now fans the matrix out across
3 packages × 2 archs × 3 pythons = 18 jobs, exercising the full
build path for the multi-Python rollout. Once stable, this default
can revert to the smaller smoke-test or be lifted to ALL.
* The top-level `concurrency` block is commented out (3 lines).
During the rollout we want every push to leave a full CI trail so
per-Python failures can be diagnosed independently.
Cost shape:
* PR open / push to non-main: same as before (1x py3.12 default).
* Push to main: 3x runner-minutes vs. before, producing
cp312/cp313/cp314 wheels for every recipe.
* `workflow_dispatch -f python_versions=...` allows arbitrary subsets
for targeted iteration (e.g. only py3.14 to find what's not
upstream-ready).
Follow-ups (out of scope here):
* Once the rollout is stable, restore `concurrency.cancel-in-progress`.
* Once Flet's runtime supports 3.13 / 3.14, drop the test-phase
3.12-only gate in `detect-tests`.
* Optional: add `package.python_versions` to the meta.yaml schema as
an opt-out gate, in the same shape as the existing
`package.platforms`, for recipes that can't build on a given Python.
* improve
* setup.sh + forge: relocate pkg-config .pc files so meson can find Python on 3.14
Mobile-forge-side counterpart to the python-build fix on the
fix/sysconfig-usr-local-paths branch. Same goal -- make meson's
py.dependency() find the Python C dep on Python 3.14 Android -- but
applied at the consumer side (mobile-forge) instead of at the producer
side (python-build). Useful when running against a python-build
tarball that hasn't shipped the upstream fix yet.
The bug recap (full diagnosis in the discussion thread): CPython's
autoconf bakes prefix=/usr/local into every shipped python-X.Y.pc, and
into INCLUDEPY/LIBDIR sysconfig vars. On Python 3.13 meson's
py.dependency() somehow tolerated the bogus path; on 3.14 it tightened
the check and reports "Python dependency not found", failing
numpy 2.4.6 on Android.
Three changes:
* setup.sh: new relocate_pkgconfig_prefix() helper called immediately
after every download_support extraction. Walks lib/pkgconfig/*.pc
under the extracted tree and rewrites `prefix=<absolute>` to
`prefix=${pcfiledir}/../..` -- pkg-config's standard relocatable
form. Idempotent. Portable in-place sed (macOS / Linux). Helper
unset at end of the sourced script so it doesn't leak into the
caller's shell.
* .github/workflows/build-wheels.yml: add `pkg-config` to the apt
install list on Linux runners. macOS has it via Xcode tooling.
* src/forge/build.py: in compile_env(), export PKG_CONFIG_PATH
pointing at the per-arch <install_root>/lib/pkgconfig. Per-arch
because Android has 4 ABIs each with its own python install, and
pkg-config picks the first .pc on its path -- letting forge set
this per-build keeps the right slice selected. Set in env so meson
inherits it through the cross-file invocation.
With these three pieces, meson's py.dependency() resolves Python via
pkg-config (never falls through to the sysconfig path that 3.14 is
strict about), and the consumer-side install root flows correctly
into the -I/-L flags. Tested locally against the cached python-build
3.14.5 Android tarball: pkg-config --cflags python-3.14 emits the
expected -I.../include/python3.14 path under the consumer prefix.
The same fix shape lands as the python-build PR's
relocate_pkgconfig_files() function -- this branch is the
"what if upstream hasn't shipped yet" workaround.
* forge: declare pkg-config in meson cross-file [binaries]
Meson cross-compile mode otherwise reports "Found pkg-config: NO"
even when pkg-config is installed and on PATH — it considers
pkg-config a build-machine tool and refuses to use it for target
dep resolution without an explicit cross-file declaration. With this
declaration meson honors PKG_CONFIG_PATH (set per-arch in
compile_env()), reads the .pc files relocated by setup.sh, and
emits the consumer-correct -I/-L flags. Verified by run 27150078838
log: "Found pkg-config: NO" → expected to switch to YES after
this lands.
Without this, the consumer-side .pc relocation in setup.sh is
unreachable from meson, and the upstream-fix path (Rule 3 in
sysconfigdata) is the only way through — but meson 1.8 + 3.14
sysconfig fallback fails for additional reasons (separate
investigation).
* setup.sh: also substitute $(BLDLIBRARY) in python-X.Y.pc
CPython autoconf ships `Libs: -L${libdir} $(BLDLIBRARY)` in
python-X.Y.pc. The `$(BLDLIBRARY)` is supposed to expand to
`-lpython3.X` at install time but never does, and pkg-config passes
the literal through to the linker which then fails with:
clang++: error: no such file or directory: "$(BLDLIBRARY)"
caught by run 27151078461 -- meson found python via pkg-config and
got 87% through the numpy build before the link step died on this.
python-X.Y-embed.pc is unaffected (ships -lpythonX.Y directly), but
meson defaults to the non-embed .pc.
Fix: extend the existing setup.sh .pc relocator to also substitute
$(BLDLIBRARY) with -lpython${X.Y} (derived from the .pc filename)
in python-X.Y.pc only. Idempotent.
Should also land in python-build upstream so the released tarballs
ship with the fix baked in; tracked separately.
* forge: include host python pkgconfig dir in PKG_CONFIG_PATH
Previously only install_root/lib/pkgconfig (the flet-libs install
location) was on PKG_CONFIG_PATH. python-X.Y.pc lives at
host_python_home/lib/pkgconfig, which was unreachable.
Run A (release tarball) happened to work despite this -- some
combination of crossenv layout + meson python module search
heuristics found it via another path. Run B (upstream tarball with
Rule 3 applied to sysconfigdata) did not, suggesting the relocated
sysconfigdata closes off whatever path A was using.
The robust fix is to explicitly add the python install pkgconfig
dir to PKG_CONFIG_PATH so meson always finds python-X.Y.pc via
pkg-config, regardless of tarball variant or sysconfigdata state.
* ci: split per-python matrix into orchestrator + reusable child workflow
Reorganizes the build-wheels workflow so the GitHub Actions UI groups
runs hierarchically by Python version instead of one ~300-cell flat
matrix. Same pattern python-build uses (build-python.yml +
build-python-version.yml) -- after the split the orchestrator summary
shows one row per Python (`Python 3.12.13`, `Python 3.13.13`,
`Python 3.14.5`), each of which expands into its own run page with
the per-recipe matrix.
.github/workflows/build-wheels.yml (orchestrator, was ~520 lines, now ~135):
* Keeps push / pull_request / workflow_dispatch / workflow_call
triggers and the GEMFURY_TOKEN-optional secret schema.
* `detect` job: decides this run's Python set (DEFAULT_PYTHONS_PUBLISH
on push-to-main, DEV elsewhere, input override) AND resolves the
package set (changed-recipes for push/PR, input for dispatch/call,
ALL expansion still supported). Emits pythons_json + packages.
* `build` job: matrix over python_version, `name: Python ${{ matrix.python_version }}`,
`uses: ./.github/workflows/build-wheels-version.yml` -- one child run
per Python.
.github/workflows/build-wheels-version.yml (new, ~430 lines):
* `on:` has BOTH workflow_call AND workflow_dispatch -- the dispatch
half is for one-off single-Python iteration when going through the
orchestrator is overkill.
* `inputs.python_version` is required; archs / packages /
prebuild_recipes / python_build_run_id mirror the orchestrator's
forwarded values.
* `setup` job: set-matrix now archs × packages only (Python axis lives
at the parent level).
* `build` job: same content as the previous flat workflow. UV_PYTHON
reads from `inputs.python_version` instead of `matrix.python_version`.
Job names lose the `py3.X` prefix (parent run is already labeled).
Artifact names keep `py${py_short}-${platform}-${pkg_name}` so
cross-Python artifacts stay distinct on the parent.
* Publish gate unchanged: success() && push && main && inputs.python_build_run_id == ''
-- skips on validation calls from python-build, same semantics as
before.
UI win: the cross-repo workflow_call shape we set up earlier for
python-build->mobile-forge validation also gets cleaner -- one
"Python X.Y.Z" row per matrix entry under the orchestrator instead
of hundreds of leaf jobs mixed across pythons.
* ci(orchestrator): pass GEMFURY_TOKEN explicitly instead of inherit
`secrets: inherit` on the orchestrators inner call to the child
workflow only works within the same organization. When this orchestrator
is invoked via workflow_call from flet-dev/python-build (which lives
under a different account), the inner inherit fails to validate at
startup and the whole run aborts with conclusion=startup_failure and
no jobs scheduled (caught on python-build run 27199283896).
Switch to explicit secrets passing. When called from python-build,
`secrets.GEMFURY_TOKEN` evaluates to empty (python-build doesnt have
one to pass through, and inherit isnt allowed across orgs), which the
child accepts because it declares GEMFURY_TOKEN as required: false and
the publish step is already gated off for validation calls
(inputs.python_build_run_id != "").
* ci(child): job_name -> "recipe ver (platform) #build"
Reorders the leaf job display name so the recipe is the leading
field. GitHub Actions sorts matrix rows alphabetically; with the
previous "${platform}: ${pkg_name} ..." form, the UI lumped all
android rows together followed by all ios rows, so each recipe-s
two platform results were ~50 entries apart on a wide matrix.
New form: "${pkg_name} ${version} (${platform}) #${build}". Each
recipe-s (android) and (ios) rows now sort adjacent (and (android)
< (ios) keeps the within-pair order stable). The artifact_name
keeps its own `py${py_short}-${platform}-${pkg_name}` form for
cross-platform uniqueness on uploads.
* Revert "Preserve upstream wheel Python/ABI tag in fix_wheel (#61)"
This reverts commit 308dfae.
* flet-libcurl: probe sibling openssl-* dir for 3.14+ Android layout
python-build's 3.14 Android tarball moved openssl from inside the python
install dir (where 3.12/3.13 had it, alongside libcrypto.so /
include/openssl/) to a sibling directory next to it:
install/android/<abi>/openssl-3.0.20-1/include/openssl/ssl.h
install/android/<abi>/openssl-3.0.20-1/lib/libssl.{a,so}
The existing fallback chain only knew the in-python-dir layout, so on
3.14 it fell through to $PYTHON_PREFIX with no openssl headers and
curl's configure aborted:
configure: error: <cross_venv>/cross is a bad --with-openssl prefix!
Add a third layer that globs $PYTHON_PREFIX/../openssl-* for the first
directory carrying include/openssl/ssl.h and points OPENSSL_PREFIX
there. 3.12/3.13 keep using Layer 2 (the in-python-dir path is checked
and matches before the new glob runs), so this is purely additive.
Caught by mobile-forge ALL × 3-python dispatch
27201546228 — flet-libcurl 3.14 Android was 1 of 31 reds; this is the
first of the non-numpy clusters we are working through.
* forge: expose HOST_PYTHON_HOME + use it in flet-libcurl openssl probe
Adds a new env var HOST_PYTHON_HOME to forge's compile_env() pointing at
the support-tree python install directory for the active SDK / arch
(e.g. `MOBILE_FORGE_<SDK>_SUPPORT_PATH/install/<sdk>/<arch>/python-<X.Y.Z>`
on Android, the matching Python.xcframework slice on iOS). This is
distinct from PYTHON_PREFIX -- PYTHON_PREFIX comes from the cross-venv's
relocated sysconfigdata `prefix`, which on python-build 3.14+ Android
maps into the cross-venv directory rather than back into the support
tree. HOST_PYTHON_HOME always names a real directory on disk inside
the support tree, so recipes can reach sibling artifacts shipped
alongside Python.
Fixes flet-libcurl 3.14 Android: 35f89e0 added a Layer 3 fallback that
globbed `$PYTHON_PREFIX/../openssl-*` for the sibling-directory openssl
layout python-build introduced in 3.14. On 3.14 PYTHON_PREFIX no longer
points inside the support tree, so that glob looked under the cross-venv
build directory instead and never found the sibling. Switch the Layer 3
glob to `$HOST_PYTHON_HOME/../openssl-*` -- which IS in the support tree
-- and the openssl-3.0.20-1 sibling is found.
Verified by run 27233703688 showing the same "bad --with-openssl prefix"
error as before 35f89e0 because PYTHON_PREFIX-rooted glob found nothing.
* flet-libgdal + flet-libproj: probe sibling sqlite-* dir for 3.14+ Android
python-build's 3.14 Android tarball moved sqlite3 headers from inside
the python install dir (where 3.12/3.13 had them, alongside
libsqlite3_python.so) to a sibling directory next to it:
install/android/<abi>/sqlite-3.50.4-1/include/sqlite3.h
install/android/<abi>/sqlite-3.50.4-1/lib/libsqlite3.so
(The library is still also bundled inside the python install dir on
3.14 -- only the header moved.) Both recipes pointed
`-DSQLite3_INCLUDE_DIR=$PYTHON_PREFIX/include`, so on 3.14 CMake's
FindSQLite3 looked under <cross_venv>/cross/include/ and aborted:
CMake Error at .../FindSQLite3.cmake:99 (file):
file STRINGS file "<cross_venv>/cross/include/sqlite3.h" cannot be read.
Mirror the openssl Layer 3 pattern: probe $HOST_PYTHON_HOME/include
first (the 3.12/3.13 bundled-in-python layout), then glob
$HOST_PYTHON_HOME/../sqlite-* for the sibling layout (3.14+). Pass the
resolved path as -DSQLite3_INCLUDE_DIR. Also swap the
-DSQLite3_LIBRARY target from $PYTHON_PREFIX/lib/ to $HOST_PYTHON_HOME/lib/
-- the .so still lives bundled inside the python install dir on every
version we've shipped, and $HOST_PYTHON_HOME points there reliably while
$PYTHON_PREFIX no longer does on 3.14.
iOS branch unaffected (uses $SDK_ROOT/usr for sqlite3, system-provided).
Caught by mobile-forge ALL × 3-python dispatch 27201546228 — these are
3 of the 7 remaining non-numpy failures (flet-libgdal + flet-libproj
×1 each on 3.14 Android; flet-libcurl was the 4th, already fixed in
35f89e0 + 99d5416). coolprop and the iOS contourpy / freetype clusters
are tracked separately.
* forge: expose HOST_PYTHON_HOME on every recipe's compile_env
99d5416 added HOST_PYTHON_HOME inside SimplePackageBuilder.compile()'s
kwargs path so build.sh recipes (flet-libcurl, flet-libgdal,
flet-libproj) could refer to the support tree's python install
without depending on the crossenv-relocated sysconfig prefix. That
left PythonPackageBuilder recipes (pip-wheel + PEP-517 backend, no
build.sh) without access -- coolprop tripped over this with a
KeyError on its CMAKE_ARGS template.
Lift the env-var injection into the base compile_env so every recipe
sees it via script_env templating, and drop the now-duplicate entry
from SimplePackageBuilder.compile(). The flet-lib* recipes already
referencing $HOST_PYTHON_HOME continue to work unchanged.
Caught by mobile-forge ALL × 3-python dispatch 27201546228 --
coolprop × 3.14 × (arm64-v8a + x86_64) needed the same support-tree
anchor those build.sh recipes use, but with template-var expansion
in meta.yaml.
* coolprop: anchor Python_LIBRARY / Python_INCLUDE_DIR on HOST_PYTHON_HOME for Android
cp314's crossenv relocates `sysconfig_data["prefix"]` to a path that
doesn't have `lib/libpython3.14.so` or `include/python3.14/` laid
out (the on-disk layout still lives in the support tree's
`install/android/<arch>/python-X.Y.Z/`). The `{prefix}`-based
CMAKE_ARGS that worked for cp312/cp313 therefore pointed FindPython
at non-existent files on cp314, and Development.Module was reported
missing.
Switch the Android branch's Python_LIBRARY / Python_INCLUDE_DIR
(and the Python3_* aliases) onto `{HOST_PYTHON_HOME}` — exposed in
every recipe's script_env templating since the previous commit --
which always resolves to the support tree's actual python install
dir regardless of python-build version or crossenv relocation. This
is the same layout-drift fix already applied to flet-libcurl's
openssl probe and flet-libgdal / flet-libproj's sqlite3 probe.
iOS branch unchanged: there the host_python_home -> Python.xcframework
slice and {prefix} both resolve to the same xcframework path, so
cp312/cp313 and cp314 all agree.
Caught by mobile-forge ALL × 3-python dispatch 27201546228 -- the
last 2 of the 7 non-numpy 3.14 failures. Verified green on run
27241029378.
* ci(child): install pkg-config on iOS lane (macos-26) too
The Android branch already installs pkg-config + sqlite3 via apt
because meson's py.dependency() / config-tool fallback both refuse
to operate without a pkg-config binary on PATH, even when
PKG_CONFIG_PATH already points to the right .pc files. The iOS lane
runs on macos-26 which doesn't ship pkg-config either, so add a
brew install in the else branch.
Caught by mobile-forge ALL × 3-python dispatch 27201546228 --
contourpy × 3.14 × iOS × (3 archs) failed identically with:
meson: "Did not find pkg-config by name 'pkg-config'"
meson: "pybind11-config found: NO"
../meson.build:23: ERROR: Dependency lookup for pybind11 with
method 'pkg-config' failed: Pkg-config for machine host machine
not found. Giving up.
* forge: add /opt/homebrew/bin (and /usr/local/bin) to PATH on iOS host
mobile-forge's cross_kwargs() rebuilds PATH from scratch to avoid
leaking unrelated host tooling into the build subprocess. The list
covered system dirs (/usr/bin etc.) but not the Homebrew prefix,
so the iOS lane's `brew install pkg-config` (added in 899565c)
was invisible to meson at build time -- the binary lives under
/opt/homebrew/bin/pkg-config on Apple Silicon runners.
Without /opt/homebrew/bin on PATH meson keeps emitting
"Did not find pkg-config by name pkg-config" / "Pkg-config for
machine host machine not found", which blocks
`dependency(pybind11)` lookups in contourpy and any other meson-
based recipe.
Add both /opt/homebrew/bin (Apple Silicon) and /usr/local/bin
(Intel-mac fallback) ahead of the system /usr/bin entries so brew-
installed tools take precedence.
* forge: scan site-packages/*/share/pkgconfig for PKG_CONFIG_PATH
Pure-Python wheels like pybind11 ship their .pc file inside the
installed wheel:
<site-packages>/pybind11/share/pkgconfig/pybind11.pc
Meson's `dependency('pybind11')` via the pkg-config method searches
PKG_CONFIG_PATH for pybind11.pc, but until now mobile-forge only
added host_python_home/lib/pkgconfig and install_root/lib/pkgconfig
to it. Neither caught the wheel-bundled layout, so contourpy
(meson + pybind11) failed at meson configure with:
Run-time dependency pybind11 found: NO
../meson.build:23:15: ERROR: Dependency "pybind11" not found
(tried pkg-config and config-tool)
Glob both <venv>/build/ and <venv>/cross/ site-packages for any
*/share/pkgconfig dir and append to PKG_CONFIG_PATH. The glob picks
up pybind11 without naming it, and will catch any future Python
wheel that ships a .pc file the same way.
* ci(child): attempt mobile tests on cp3.13/cp3.14 with continue-on-error
Previously the detect-tests step force-skipped the mobile test lane
entirely for any python_short other than 3.12, because the
recipe-tester runtime (`flet build apk` APK + iOS simulator app)
embeds a 3.12 interpreter. Result: cp3.13/cp3.14 wheels were built
and published but never even attempted on a device.
For comprehensive ALL × 3-python sweeps the operator wants the
attempt to happen anyway so any incidental signal is visible (a
cp3.12 published wheel covering for our local cp3.13 build still
catches some regressions in the test app and Flet runtime). Drop
the python-short gate from detect-tests and mark every downstream
mobile-test step continue-on-error when python_short != 3.12.
cp3.12 keeps strict pass/fail semantics; cp3.13 + cp3.14 surface
notices instead of hard-failing the job.
* forge: add -framework Python to iOS LDFLAGS
The cargo branch in compile_env() already emits both
`-F <Python.xcframework slice>` and `-framework Python` (line
above) -- the iOS linker rejects macOS's `-undefined dynamic_lookup`
extension-module convention and needs Python C API symbols resolved
at link time. The regular ldflags path was only adding `-F` without
the framework name, so meson-driven builds (numpy, contourpy with
pybind11, …) inherited a link line with the search path but no
matching framework to consume from it, and link aborted with:
Undefined symbols for architecture arm64:
"_PyBaseObject_Type", referenced from:
pybind11::detail::make_object_base_type(...)
"_PyErr_SetString", referenced from:
...
Setuptools-based extensions ship a -framework Python via the cross
sysconfig's LDSHARED, so they weren't blocked by this gap. Adding
the same flag to LDFLAGS here is a no-op duplicate for them and
fixes meson recipes.
Caught by mobile-forge ALL × 3-python dispatch 27201546228 --
contourpy × 3.14 × iOS × (3 archs) finished compile cleanly but
died at link with the symbol list above (run 27242803466).
* forge + flet-libfreetype: stop pkg-config from leaking macOS Homebrew dylibs into iOS; switch freetype source to SourceForge
Two unrelated fixes that fell out of the focused re-dispatch
(27266113749 + 27266115971):
1. forge: PKG_CONFIG_LIBDIR sanitization
The /opt/homebrew/bin entry on PATH (76a47e0) made pkg-config
discoverable for meson on iOS, but pkg-config's default search
list still pointed at /opt/homebrew/lib/pkgconfig. Pillow's
setup.py scans the world for libtiff / liblcms2 / libpng /
harfbuzz / freetype / ... via pkg-config and now found them all
under Homebrew. Those are macOS dylibs:
ld: building for 'iOS', but linking in dylib
(/opt/homebrew/Cellar/little-cms2/2.19/lib/liblcms2.2.dylib)
built for 'macOS'
Set PKG_CONFIG_LIBDIR to the same support-tree-only list we put
in PKG_CONFIG_PATH. LIBDIR is pkg-config's *exclusive* search
path -- it ignores its built-in default when set -- so Homebrew's
pkgconfig dir becomes invisible. contourpy / meson still resolve
pybind11 + python because both are still listed via PKG_CONFIG_PATH
and now LIBDIR.
2. flet-libfreetype: switch source URL
download.savannah.gnu.org 302-redirects to a rotating set of
nongnu.org mirrors. The one CI was steered onto (ftp.cc.uoc.gr)
intermittently serves truncated/HTML content, leaving forge
with an unidentifiable archive:
RuntimeError: Can't identify archive type of
downloads/freetype-2.13.3.tar.gz
Switch to https://downloads.sourceforge.net/...; the SourceForge
mirror chain is shorter and has been stable for releases like
this for a decade.
Both fixes are CI-level (not recipe-level). Caught by ALL × 3-python
re-dispatch 27266113749 (pillow iOS × 3 pythons all failed) +
27266115971 (flet-libfreetype × all 4 android archs failed).
* coolprop: mirror HOST_PYTHON_HOME switch to iOS branch
5ef9820 swapped Python_LIBRARY / Python_INCLUDE_DIR onto
{HOST_PYTHON_HOME} for Android only -- the iOS branch was left
referencing {prefix}. cp314's crossenv relocates sysconfig prefix
on iOS too, so FindPython now lands on a path with no libpython /
include and aborts with the same 'Could NOT find Python (missing:
Interpreter Development.Module)' as Android did before the fix.
On iOS, host_python_home resolves to the matching Python.xcframework
slice (e.g. <support>/Python.xcframework/ios-arm64/), which ships
lib/libpython3.14.dylib + include/python3.14/. Same shape as
Android's python install dir, so the meta.yaml change is a direct
mirror of the Android template -- just .dylib instead of .so.
Caught by mobile-forge ALL × 3-python re-dispatch 27266113749 --
coolprop iOS × 3.14 failure with line:15 (find_package) error
identical to what {HOST_PYTHON_HOME} fixed on Android.
* forge: move iOS -framework Python from LDFLAGS into meson cross-file c/cpp link_args
e4e3899 put `-framework Python` into iOS LDFLAGS so contourpy
(meson + pybind11) could resolve Python C API symbols at link time.
That fixed contourpy but broke flet-libfreetype: its
autoconf-generated configure links a hello.c against $LDFLAGS to
probe the C compiler, and the framework reference aborts the probe
with `C compiler cannot create executables`.
Push the framework link down into the meson cross-file's
c_links_args / cpp_links_args instead. Only meson-driven builds see
it; autoconf's hello.c probe gets the bare $LDFLAGS (without the
framework load command) and the probe succeeds. Cargo and setuptools
recipes are unaffected -- they already get `-framework Python` via
cargo_ldflags and the cross sysconfig's LDSHARED respectively.
Caught by fix-verification dispatch 27267428388 (flet-libfreetype
iOS × 3.12 failed at configure 'C compiler cannot create
executables' once -framework Python landed in LDFLAGS).
* ci(child): only `brew install pkg-config` when missing on iOS lane
macos-26 ships pkgconf preinstalled (and that formula provides the
pkg-config binary), so the unconditional `brew install pkg-config`
in 899565c reliably hit brew's "already installed and up-to-date"
notice -- which GitHub Actions surfaces as a yellow
`##[warning]` on every iOS-lane job.
Guard the install with `command -v pkg-config` so the brew call
only fires when the runner image actually lacks the binary. The
warning disappears; semantics are unchanged.
* ci(child): drop `brew install pkg-config` on iOS lane
macos-26 ships pkgconf preinstalled, which provides the pkg-config
binary meson needs. The install in 899565c was therefore a no-op
that only ever surfaced the "already installed" warning. Leave a
note next to the Android-only apt block so the absence is
intentional, not forgotten.
If a future runner image drops pkgconf, meson configure will fail
with "Did not find pkg-config by name pkg-config" -- exactly the
signal that triggered 899565c originally -- and we re-add a guarded
install.
* ci(workflow): streamline comments and improve job_name formatting in build-wheels-version
* ci(child): drop stale abi3 branch from dist-test wheel renamer
PR #67 (preserve upstream Python/ABI tag in fix_wheel) was reverted
in 00b7239 because the abi3 tokenizers wheel was a fake-abi3 (had a
DT_NEEDED libpython3.12.so). post-revert, fix_wheel always emits
`cpXY-cpXY` tags and never `cp37-abi3`. The renamer's
`|abi[0-9]+` alternation and the matching cp37-abi3 comment now
describe a shape we no longer produce, so simplify both branches
(android lane + iOS lane) to match the actual output.
* ci(workflow): add mobile_test_pythons input to allow selective mobile test execution
* ci(workflow): update build-wheels-version inputs and caching behavior
- Adjusted `packages` input description for clarity.
- Improved handling of `MOBILE_FORGE_CACHE_DOWNLOADS_OFF` to disable caching and prevent poisoned-cache issues.
- Refined NDK version environment variable comments for better context.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See #61
Was reverted in #65 on purpose.