Multi python support (3.12 / 3.13 / 3.14)#69
Merged
Conversation
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 flet-dev#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.
…hon 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.
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).
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.
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.
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.
`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 != "").
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.
)" This reverts commit 308dfae.
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.
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.
…roid
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.
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.
…OME 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.
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.
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.
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.
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.
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).
… 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).
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.
…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).
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.
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.
…build-wheels-version
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.
- 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.
FeodorFitsner
approved these changes
Jun 10, 2026
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.
Summary
Add Python 3.13 + 3.14 to mobile-forge's build matrix. Split the monolithic CI workflow into an orchestrator + per-Python child so we can fan out cleanly, and fix the recipe / forge bugs that surfaced once cp3.13 + cp3.14 actually started building. Final outcome on
ALL × 3 pythons × android,iOS: all 8 non-numpy failures resolved; the 24-job numpy cluster needsnumpypublished for cp3.13/cp3.14 onpypi.flet.dev(out of scope here).CI workflow: https://github.com/ndonkoHenri/mobile-forge/actions/runs/27285920722
CI workflows
.github/workflows/build-wheels.yml(orchestrator)mobile_test_pythons(default"3.12.13") — Python versions whose recipe mobile tests should actually run. Other versions still build wheels but skip the APK / iOS-sim stage. Use"ALL"to test every version. Keeps push/PR CI fast (cp3.12 only) while letting dispatch widen on demand.DEFAULT_PYTHONS_PUBLISH/DEFAULT_PYTHONS_DEVsplit into a singleDEFAULT_PYTHONSenv var. Both had the same value, the branching was no-op.SMOKE_TEST_PACKAGESenv var so the fallback recipe list lives in one place..github/workflows/build-wheels-version.yml(new — reusable child)python_version,archs,packages,prebuild_recipes,mobile_test_pythons,python_build_run_id.pkg-config+sqlite3via apt. iOS lane intentionally installs nothing — macos-26 ships pkgconf preinstalled.detect-testsgate: only setshas_tests=truewhenmatrix.python_shortis listed inmobile_test_pythons; otherwise downstream KVM / NDK / APK-stage / emulator-test steps all skip.mobile_test_pythons=ALLbypasses the gate.dist-testwheel renamer regex simplified tocpXY-cpXYonly — thecp37-abi3alternative is dead code after the PR Preserve upstream wheel Python/ABI tag in fix_wheel (#61) #67 revert.MOBILE_FORGE_CACHE_DOWNLOADS_OFF=1env var: forces forge to re-download source tarballs per arch. Guards against poisoned-cache failures when an upstream mirror serves bad content for the first arch and every later arch in the fan-out reuses the corrupt file.Forge code
src/forge/build.pyHOST_PYTHON_HOME— always points at the on-disk python install in the support tree (install/<sdk>/<arch>/python-X.Y.Zon Android,Python.xcframework/<slice>on iOS). Exposed in the basecompile_env()so bothSimplePackageBuilderandPythonPackageBuilderrecipes can reference it via{HOST_PYTHON_HOME}inscript_envtemplating. Needed because cp3.14's crossenv relocatessysconfig_data["prefix"]to a path that doesn't reliably containlib/libpython.X.Yorinclude/pythonX.Y— recipes that pinned against{prefix}broke.PKG_CONFIG_PATHnow also scans<venv>/{build,cross}/lib/pythonX.Y/site-packages/*/share/pkgconfigso meson'sdependency('pybind11')findspybind11.pc(shipped inside the wheel rather thanlib/pkgconfig).PKG_CONFIG_LIBDIRset to the same paths — overrides pkg-config's default search list so it can't reach/opt/homebrew/lib/pkgconfigand link macOS Homebrew dylibs into iOS builds (Pillow tripped on this).c_link_args/cpp_link_argsnow append-framework Pythonon iOS (not LDFLAGS env). Required so meson recipes (contourpy with pybind11) resolve Python C API symbols at link time, without breaking autoconf-based builds whose hello.c probe links against$LDFLAGSand would otherwise fail with "C compiler cannot create executables". Gated onhost_os == "iOS"(Python.framework only ships in the iOS support tree).MOBILE_FORGE_CACHE_DOWNLOADS_OFFenv var consumer.src/forge/cross.py/opt/homebrew/bin+/usr/local/binto the iOS PATH. macos-26 ships pkgconf preinstalled at/opt/homebrew/bin/pkg-config; without this entry meson can't find it and aborts with"Pkg-config for machine host machine not found".setup.shlib/pkgconfig/python-X.Y.pcafter extracting the support tree (replaces CI-baked/usr/local/...paths with the actual on-disk prefix). Also substitutes$(BLDLIBRARY)so meson's pkg-config path resolves on cp3.14.Recipe changes
recipes/coolprop/meta.yamlAnchored
-DPython_LIBRARY/-DPython_INCLUDE_DIR(and thePython3_*aliases) on{HOST_PYTHON_HOME}instead of{prefix}, on both Android and iOS branches. On cp3.14,{prefix}resolves to the crossenv-relocated path which doesn't havelib/libpython3.14.{so,dylib}orinclude/python3.14/laid out; FindPython then bails with"Could NOT find Python (missing: Interpreter Development.Module)".{HOST_PYTHON_HOME}always points at the support tree slice that does have those files.recipes/flet-libcurl/build.shAdded Layer 3 sibling-
openssl-*probe for Android. cp3.14's python-build tarball moved openssl headers out of the python install dir (python-3.13.x/include/openssl/) into a siblingopenssl-X.Y.Z-N/include/openssl/next to it. Probe falls back through three layers:$PYTHON_PREFIX/include,$HOST_PYTHON_HOME/include,$HOST_PYTHON_HOME/../openssl-*. Resolvesbad --with-openssl prefix.recipes/flet-libgdal/build.sh+recipes/flet-libproj/build.shSame layout-drift fix as flet-libcurl, applied to sqlite3. cp3.14 moved
sqlite3.hinto a siblingsqlite-X.Y.Zdir; the recipes now probe$HOST_PYTHON_HOME/includefirst, then$HOST_PYTHON_HOME/../sqlite-*/include. The.solibrary still lives bundled inside the python install dir on every version we ship, so-DSQLite3_LIBRARYuses$HOST_PYTHON_HOME/lib/libsqlite3_python.so. iOS branches unaffected (those use$SDK_ROOT/usrfor sqlite3).recipes/flet-libfreetype/meta.yamlSwitched source URL from
download.savannah.gnu.orgtodownloads.sourceforge.net. Savannah's 302 redirects to a rotating set of nongnu.org mirrors; the one CI was steered onto (ftp.cc.uoc.gr) intermittently served truncated / HTML content, leaving forge with"Can't identify archive type of freetype-2.13.3.tar.gz". SourceForge's mirror chain has been stable for ten years.PR #67 revert
Revert "Preserve upstream wheel Python/ABI tag in fix_wheel"— re-tagging logic was preservingcp37-abi3for wheels that claimed abi3 but weren't actually abi3-compatible (tokenizers hadDT_NEEDED libpython3.12.so). Reverted sofix_wheelalways emitscpXY-cpXYtags, which is what every consumer in the matrix actually wants.Verification
ALL × 3py × android,iOSdispatch (run 27243162509): 32 failures.PKG_CONFIG_LIBDIR)-framework Pythonvia meson cross-file)HOST_PYTHON_HOMEfor iOS)numpypublished for cp3.13/cp3.14 onpypi.flet.dev.