Skip to content

Multi python support (3.12 / 3.13 / 3.14)#69

Merged
ndonkoHenri merged 29 commits into
flet-dev:mainfrom
ndonkoHenri:multi-python
Jun 11, 2026
Merged

Multi python support (3.12 / 3.13 / 3.14)#69
ndonkoHenri merged 29 commits into
flet-dev:mainfrom
ndonkoHenri:multi-python

Conversation

@ndonkoHenri

Copy link
Copy Markdown

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 needs numpy published for cp3.13/cp3.14 on pypi.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)

  • Split out of the old single-file workflow. Now only does event detection + Python fan-out, then calls the child once per Python version.
  • New input 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.
  • Collapsed the dead DEFAULT_PYTHONS_PUBLISH / DEFAULT_PYTHONS_DEV split into a single DEFAULT_PYTHONS env var. Both had the same value, the branching was no-op.
  • Added SMOKE_TEST_PACKAGES env var so the fallback recipe list lives in one place.

.github/workflows/build-wheels-version.yml (new — reusable child)

  • One run per Python version. Inherits python_version, archs, packages, prebuild_recipes, mobile_test_pythons, python_build_run_id.
  • Android lane installs pkg-config + sqlite3 via apt. iOS lane intentionally installs nothing — macos-26 ships pkgconf preinstalled.
  • detect-tests gate: only sets has_tests=true when matrix.python_short is listed in mobile_test_pythons; otherwise downstream KVM / NDK / APK-stage / emulator-test steps all skip. mobile_test_pythons=ALL bypasses the gate.
  • dist-test wheel renamer regex simplified to cpXY-cpXY only — the cp37-abi3 alternative is dead code after the PR Preserve upstream wheel Python/ABI tag in fix_wheel (#61) #67 revert.
  • MOBILE_FORGE_CACHE_DOWNLOADS_OFF=1 env 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.py

  • New env var HOST_PYTHON_HOME — always points at the on-disk python install in the support tree (install/<sdk>/<arch>/python-X.Y.Z on Android, Python.xcframework/<slice> on iOS). Exposed in the base compile_env() so both SimplePackageBuilder and PythonPackageBuilder recipes can reference it via {HOST_PYTHON_HOME} in script_env templating. Needed because cp3.14's crossenv relocates sysconfig_data["prefix"] to a path that doesn't reliably contain lib/libpython.X.Y or include/pythonX.Y — recipes that pinned against {prefix} broke.
  • PKG_CONFIG_PATH now also scans <venv>/{build,cross}/lib/pythonX.Y/site-packages/*/share/pkgconfig so meson's dependency('pybind11') finds pybind11.pc (shipped inside the wheel rather than lib/pkgconfig).
  • PKG_CONFIG_LIBDIR set to the same paths — overrides pkg-config's default search list so it can't reach /opt/homebrew/lib/pkgconfig and link macOS Homebrew dylibs into iOS builds (Pillow tripped on this).
  • meson cross-file's c_link_args / cpp_link_args now append -framework Python on 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 $LDFLAGS and would otherwise fail with "C compiler cannot create executables". Gated on host_os == "iOS" (Python.framework only ships in the iOS support tree).
  • Comment added explaining the MOBILE_FORGE_CACHE_DOWNLOADS_OFF env var consumer.

src/forge/cross.py

  • Added /opt/homebrew/bin + /usr/local/bin to 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.sh

  • Relocates lib/pkgconfig/python-X.Y.pc after 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.
  • Multi-Python plumbing: per-version support paths, per-version venv selection.

Recipe changes

recipes/coolprop/meta.yaml

Anchored -DPython_LIBRARY / -DPython_INCLUDE_DIR (and the Python3_* 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 have lib/libpython3.14.{so,dylib} or include/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.sh

Added 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 sibling openssl-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-*. Resolves bad --with-openssl prefix.

recipes/flet-libgdal/build.sh + recipes/flet-libproj/build.sh

Same layout-drift fix as flet-libcurl, applied to sqlite3. cp3.14 moved sqlite3.h into a sibling sqlite-X.Y.Z dir; the recipes now probe $HOST_PYTHON_HOME/include first, then $HOST_PYTHON_HOME/../sqlite-*/include. The .so library still lives bundled inside the python install dir on every version we ship, so -DSQLite3_LIBRARY uses $HOST_PYTHON_HOME/lib/libsqlite3_python.so. iOS branches unaffected (those use $SDK_ROOT/usr for sqlite3).

recipes/flet-libfreetype/meta.yaml

Switched source URL from download.savannah.gnu.org to downloads.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 preserving cp37-abi3 for wheels that claimed abi3 but weren't actually abi3-compatible (tokenizers had DT_NEEDED libpython3.12.so). Reverted so fix_wheel always emits cpXY-cpXY tags, which is what every consumer in the matrix actually wants.

Verification

  • Original ALL × 3py × android,iOS dispatch (run 27243162509): 32 failures.
  • After this branch: 8 of 8 non-numpy failures fixed and verified:
    • pillow iOS × cp3.12+cp3.13+cp3.14 (PKG_CONFIG_LIBDIR)
    • contourpy iOS × cp3.13+cp3.14 (-framework Python via meson cross-file)
    • coolprop iOS cp3.14 (HOST_PYTHON_HOME for iOS)
    • cryptography iOS cp3.12 (cleared with the PATH / pkg-config family changes)
    • flet-libfreetype Android cp3.12 (SourceForge URL)
  • 24 numpy-cluster failures remain — need numpy published for cp3.13/cp3.14 on pypi.flet.dev.

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.
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.
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.
@ndonkoHenri ndonkoHenri merged commit 518c0ad into flet-dev:main Jun 11, 2026
34 checks passed
@ndonkoHenri ndonkoHenri deleted the multi-python branch June 11, 2026 08:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants