From 481c2d6d9b5956fd7a695caafe962850b2b36e81 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 11 Jun 2026 16:29:55 +0200 Subject: [PATCH 1/7] setup.sh: force uv to use a managed (relocatable) interpreter Add --python-preference only-managed to the uv venv that builds the forge environment. On the macos-26 CI runner uv otherwise matches an exact --python 3.14.5 to Homebrew's framework CPython (/opt/homebrew/.../Python.framework). crossenv mis-detects sys.prefix/exec_prefix with a *framework* build ("Unexpected value in sys.prefix, expected .../cross, got .../build"), which mis-routes target-arch host requirements (numpy) into the *build* venv instead of the *cross* venv. The build-platform numpy then never gets installed, so the build backend's in-process introspection (import numpy; numpy.get_include(), run by meson/setuptools through the cross-python) resolves to a non-importable target wheel and dies with "No module named numpy._core._multiarray_umath". python-build-standalone (uv-managed) interpreters are relocatable and do not trip this. Linux already gets a managed interpreter; this makes macOS consistent. Requires uv new enough to ship managed 3.14.5 (>= 0.11.20; CI already uses it). Root-caused by reproducing locally: a Homebrew framework build python reproduces the exact CI failure; a managed PBS python builds clean. Fixes the cp3.14 iOS numpy-consumer cluster (blis, shapely, rasterio, matplotlib, pandas, contourpy, ...). --- setup.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 59e147c..d1c0b6a 100755 --- a/setup.sh +++ b/setup.sh @@ -221,7 +221,19 @@ venv_dir="$(pwd)/venv$PYTHON_VER" if [ ! -d $venv_dir ]; then echo "Creating Python $PYTHON_VER virtual environment for build in $venv_dir..." - uv venv --seed --python="$PYTHON_VERSION" $venv_dir + # `--python-preference only-managed` forces uv to use a relocatable + # python-build-standalone interpreter and NEVER a system one. This is + # required for crossenv: a macOS *framework* build of CPython (e.g. + # Homebrew's `python@3.14`, which the macos-26 CI runner ships and uv + # would otherwise match for an exact `--python 3.14.5`) makes crossenv + # mis-detect sys.prefix/exec_prefix ("Unexpected value in sys.prefix, + # expected .../cross, got .../build"). That mis-routes target-arch host + # requirements (e.g. numpy) into the *build* venv instead of the *cross* + # venv, so the host (build-platform) numpy never gets installed and the + # build backend's `import numpy` (for numpy.get_include()) resolves to a + # non-importable target wheel -> "No module named numpy._core._multiarray_umath". + # Linux already gets a managed interpreter; this makes macOS consistent. + uv venv --seed --python-preference only-managed --python="$PYTHON_VERSION" $venv_dir source $venv_dir/bin/activate uv pip install -e . From 7b9d597f2a93127204c5b8b807e5f2104c3d34ab Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 11 Jun 2026 16:29:55 +0200 Subject: [PATCH 2/7] pandas: set PYTHONSAFEPATH so the build cwd cannot shadow stdlib io meson runs the cross-python from the unpacked pandas *source* dir to introspect numpy (import numpy; numpy.get_include()). Python puts that cwd on sys.path[0], and pandas ships a top-level pandas/io/ package that shadows the stdlib io module mid-import, so numpy's C-extension init dies with "cannot import name 'TextIOWrapper' from 'io'". Set PYTHONSAFEPATH=1 in this recipe's script_env: it drops the implicit cwd entry (Python 3.11+) without touching PYTHONPATH, so crossenv's import bridge is unaffected. Scoped to pandas on purpose -- a global setting also drops the *script dir* for `python codegen.py` invocations and breaks recipes whose build generators import sibling modules (numpy's genapi, opencv's hdr_parser). Verified: pandas iOS builds clean with this; numpy still builds without it. --- recipes/pandas/meta.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/recipes/pandas/meta.yaml b/recipes/pandas/meta.yaml index 3d8fdee..95291b7 100644 --- a/recipes/pandas/meta.yaml +++ b/recipes/pandas/meta.yaml @@ -20,6 +20,18 @@ patches: build: number: 1 + # meson runs the cross-python from the unpacked pandas *source* dir to + # introspect numpy (`import numpy; numpy.get_include()`). Python puts that + # cwd on sys.path[0], and pandas ships a top-level `pandas/io/` package + # that then shadows the stdlib `io` module mid-import, so numpy's + # C-extension init dies with "cannot import name 'TextIOWrapper' from 'io'". + # PYTHONSAFEPATH (3.11+) drops the implicit cwd entry without touching + # PYTHONPATH, so crossenv's import bridge is unaffected. Scoped to this + # recipe on purpose: a global setting would also drop the *script dir* for + # `python codegen.py` invocations and break recipes whose build generators + # import sibling modules (numpy's `genapi`, opencv's `hdr_parser`). + script_env: + PYTHONSAFEPATH: "1" backend-args: - -Csetup-args=--cross-file - -Csetup-args={MESON_CROSS_FILE} From 0c17490fa15a0aaf72a0e3cf1481e65485ace399 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 11 Jun 2026 16:29:55 +0200 Subject: [PATCH 3/7] opencv-python: anchor Python lib/include on HOST_PYTHON_HOME (android + iOS) On cp3.14 the cross-venv prefix ({prefix} -> .../cross) no longer carries libpython{X.Y} or the python headers; they live in the support tree (android: install/.../python-X.Y.Z; iOS: the Python.xcframework slice). The CMAKE_ARGS pointed -DPYTHON3_LIBRARIES / -DPYTHON3_INCLUDE_PATH at {prefix}, so on 3.14: * android: ninja aborted linking cv2 with "lib/libpython3.14.so ... missing and no known rule to make it" * iOS: the cv2 compile aborted with "'Python.h' file not found" Point both at {HOST_PYTHON_HOME} (android .so, iOS .dylib) -- the same fix already proven on the coolprop recipe. {HOST_PYTHON_HOME} resolves to the on-disk python install for every python-build version, unlike {prefix} which relocated on 3.14. The explanatory note is kept out of the `>-` CMAKE_ARGS block: inside a YAML folded scalar a leading `#` is literal text, not a comment, so it would be passed to cmake verbatim -- and a `{X.Y}` inside it broke forge's str.format templating with KeyError: 'X'. --- recipes/opencv-python/meta.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/recipes/opencv-python/meta.yaml b/recipes/opencv-python/meta.yaml index 8ac0960..14cb2ca 100644 --- a/recipes/opencv-python/meta.yaml +++ b/recipes/opencv-python/meta.yaml @@ -37,8 +37,8 @@ build: -DCMAKE_SHARED_LINKER_FLAGS="-Wl,-z,max-page-size=16384" -DCMAKE_MODULE_LINKER_FLAGS="-Wl,-z,max-page-size=16384" -DOPENCV_FORCE_PYTHON_LIBS=ON - -DPYTHON3_INCLUDE_PATH={prefix}/include/python{py_version_short} - -DPYTHON3_LIBRARIES={prefix}/lib/libpython{py_version_short}.so + -DPYTHON3_INCLUDE_PATH={HOST_PYTHON_HOME}/include/python{py_version_short} + -DPYTHON3_LIBRARIES={HOST_PYTHON_HOME}/lib/libpython{py_version_short}.so -DPYTHON3_NUMPY_INCLUDE_DIRS={platlib}/numpy/_core/include # {% else %} CMAKE_ARGS: >- @@ -56,7 +56,7 @@ build: -DBUILD_EXAMPLES=OFF -DWITH_OPENCL=OFF -DOPENCV_FORCE_PYTHON_LIBS=ON - -DPYTHON3_INCLUDE_PATH={prefix}/include/python{py_version_short} - -DPYTHON3_LIBRARIES={prefix}/lib/libpython{py_version_short}.so + -DPYTHON3_INCLUDE_PATH={HOST_PYTHON_HOME}/include/python{py_version_short} + -DPYTHON3_LIBRARIES={HOST_PYTHON_HOME}/lib/libpython{py_version_short}.dylib -DPYTHON3_NUMPY_INCLUDE_DIRS={platlib}/numpy/_core/include # {% endif %} \ No newline at end of file From f54c405fbe21cbf4f086770cdd7b8485c07f5f65 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 11 Jun 2026 17:27:04 +0200 Subject: [PATCH 4/7] ci: temporarily disable concurrency during cp3.14 fix verification Comment out the build-wheels concurrency group so overlapping verification dispatches (and the push events that carry the fix commits) do not cancel each other mid-run. Revert before merge. --- .github/workflows/build-wheels.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 96982a5..40814c2 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -72,9 +72,11 @@ on: required: false # Cancel in-flight runs when a newer event arrives for the same logical branch. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} - cancel-in-progress: true +# TEMP: disabled during cp3.14 fix verification so overlapping dispatches +# (and the push that lands them) don't cancel each other. +# concurrency: +# group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} +# cancel-in-progress: true env: DEFAULT_PYTHONS: "3.12.13,3.13.13,3.14.5" From 6711d5fe556ddb0ffaf134bf0c3360ee7556302d Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 11 Jun 2026 19:20:25 +0200 Subject: [PATCH 5/7] numpy: replace flaky wall-clock test_performance with a matmul correctness check The recipe test asserted `duration < 0.7` for a 500x500 matmul as a proxy for "OpenBLAS is linked" (with OpenBLAS <=0.4s, without >=1.0s on the original test devices). But mobile-forge's numpy is built WITHOUT OpenBLAS (its config reports blas name="none"), so the matmul runs on the unaccelerated fallback and its wall-clock time swings wildly on loaded / emulated CI simulators -- it flaked at 1.43s (3.14) and 2.18s (3.13) for the same wheel, randomly redding the iOS mobile-test lane. Rename to test_matmul and assert the *result* (a @ I == a) plus shape instead of the elapsed time; keep the timing as an informational print. This still exercises the dense GEMM path (and would catch a broken dot() or a BLAS .so that fails to load) but is deterministic. Also add docstrings to test_basic / test_matmul to match test_fft. Surfaced by running the recipe mobile tests on cp3.13/cp3.14 for the first time (mobile_test_pythons covering 3.13/3.14); test_basic and test_fft pass, only the timing gate was unreliable. --- recipes/numpy/test_numpy.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/recipes/numpy/test_numpy.py b/recipes/numpy/test_numpy.py index 51a7d33..dbd902c 100644 --- a/recipes/numpy/test_numpy.py +++ b/recipes/numpy/test_numpy.py @@ -1,25 +1,33 @@ def test_basic(): + """Smoke-test core ndarray creation and elementwise arithmetic.""" from numpy import array assert (array([1, 2]) + array([3, 5])).tolist() == [4, 7] -def test_performance(): +def test_matmul(): + """Exercise the dense matmul (GEMM) path and verify its result. + + Asserts correctness, not speed. mobile-forge builds numpy WITHOUT OpenBLAS + (its config reports blas name="none"), so a dense matmul runs on the + unaccelerated fallback whose wall-clock time swings widely on loaded / + emulated devices and is not a reliable signal. The matmul `a @ I` must + equal `a`; the elapsed time is printed for visibility only. + """ from time import time import numpy as np - start_time = time() SIZE = 500 a = np.random.rand(SIZE, SIZE) - b = np.random.rand(SIZE, SIZE) - np.dot(a, b) - # With OpenBLAS, the test devices take at most 0.4 seconds. Without OpenBLAS, they take - # at least 1.0 seconds. + start_time = time() + product = np.dot(a, np.eye(SIZE)) # full GEMM; a @ I == a duration = time() - start_time print(f"{duration:.3f}") - assert duration < 0.7 + + assert product.shape == (SIZE, SIZE) + assert np.allclose(product, a) def test_fft(): From 270b362ba1ccae5207a29cc410cac8e8bdd55ac6 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 11 Jun 2026 19:25:04 +0200 Subject: [PATCH 6/7] Revert "ci: temporarily disable concurrency during cp3.14 fix verification" This reverts commit f54c405 -- concurrency was only disabled to keep the overlapping verification dispatches from cancelling each other; restore it now that verification is done. --- .github/workflows/build-wheels.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 40814c2..96982a5 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -72,11 +72,9 @@ on: required: false # Cancel in-flight runs when a newer event arrives for the same logical branch. -# TEMP: disabled during cp3.14 fix verification so overlapping dispatches -# (and the push that lands them) don't cancel each other. -# concurrency: -# group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} -# cancel-in-progress: true +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true env: DEFAULT_PYTHONS: "3.12.13,3.13.13,3.14.5" From d5a1ef07d0b9af2d3c0aa34929e5cd1bbab02de1 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 11 Jun 2026 19:34:28 +0200 Subject: [PATCH 7/7] improve some docs --- recipes/pandas/meta.yaml | 15 +++++---------- setup.sh | 12 +----------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/recipes/pandas/meta.yaml b/recipes/pandas/meta.yaml index 95291b7..1dfce1e 100644 --- a/recipes/pandas/meta.yaml +++ b/recipes/pandas/meta.yaml @@ -20,17 +20,12 @@ patches: build: number: 1 - # meson runs the cross-python from the unpacked pandas *source* dir to - # introspect numpy (`import numpy; numpy.get_include()`). Python puts that - # cwd on sys.path[0], and pandas ships a top-level `pandas/io/` package - # that then shadows the stdlib `io` module mid-import, so numpy's - # C-extension init dies with "cannot import name 'TextIOWrapper' from 'io'". - # PYTHONSAFEPATH (3.11+) drops the implicit cwd entry without touching - # PYTHONPATH, so crossenv's import bridge is unaffected. Scoped to this - # recipe on purpose: a global setting would also drop the *script dir* for - # `python codegen.py` invocations and break recipes whose build generators - # import sibling modules (numpy's `genapi`, opencv's `hdr_parser`). script_env: + # meson introspects numpy by running the cross-python from pandas's source + # dir, whose top-level `pandas/io/` shadows the stdlib `io` on sys.path[0] + # -> numpy's C-ext init fails ("cannot import name 'TextIOWrapper' from + # 'io'"). PYTHONSAFEPATH drops that implicit cwd entry (leaving PYTHONPATH, + # so crossenv's bridge still works). PYTHONSAFEPATH: "1" backend-args: - -Csetup-args=--cross-file diff --git a/setup.sh b/setup.sh index d1c0b6a..285392c 100755 --- a/setup.sh +++ b/setup.sh @@ -222,17 +222,7 @@ if [ ! -d $venv_dir ]; then echo "Creating Python $PYTHON_VER virtual environment for build in $venv_dir..." # `--python-preference only-managed` forces uv to use a relocatable - # python-build-standalone interpreter and NEVER a system one. This is - # required for crossenv: a macOS *framework* build of CPython (e.g. - # Homebrew's `python@3.14`, which the macos-26 CI runner ships and uv - # would otherwise match for an exact `--python 3.14.5`) makes crossenv - # mis-detect sys.prefix/exec_prefix ("Unexpected value in sys.prefix, - # expected .../cross, got .../build"). That mis-routes target-arch host - # requirements (e.g. numpy) into the *build* venv instead of the *cross* - # venv, so the host (build-platform) numpy never gets installed and the - # build backend's `import numpy` (for numpy.get_include()) resolves to a - # non-importable target wheel -> "No module named numpy._core._multiarray_umath". - # Linux already gets a managed interpreter; this makes macOS consistent. + # python-build-standalone interpreter and NEVER a system one. uv venv --seed --python-preference only-managed --python="$PYTHON_VERSION" $venv_dir source $venv_dir/bin/activate