From 3be5e772f799b46b621cd097f491773cd119c37f Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 22 Oct 2026 08:53:43 +0200 Subject: [PATCH 1/3] recipe: cffi 2.1.1 with working ffi.callback() on iOS cffi only uses libffi's closure API on Apple when Apple's own ffi.h defines FFI_AVAILABLE_APPLE; with our upstream libffi it falls back to a W+X mmap that iOS refuses, so every ffi.callback() raised MemoryError and variadic calls returned garbage on arm64. ios-ffi-closure-alloc.patch enables libffi's trampoline-table closures on iOS (upstream python-cffi #265, unreleased). 2.1.1 rather than a 2.0.0 rebuild: PyPI ships official cffi 2.1.1 iOS wheels (cp313-cp315) without the fix, and they outrank 2.0.0. Build 1 wins the tie. soundfile: file-object reads now work on iOS, so the MemoryError tripwire test goes and the README stops calling the limit permanent. --- recipes/cffi/meta.yaml | 5 +- .../cffi/patches/ios-ffi-closure-alloc.patch | 54 ++++++++++ recipes/cffi/patches/mobile.patch | 28 +++-- recipes/cffi/tests/test_cffi.py | 102 +++++++++++++++++- recipes/soundfile/README.md | 49 ++++----- .../examples/codec-roundtrip/README.md | 8 +- .../codec-roundtrip/src/audio_codecs.py | 4 +- recipes/soundfile/tests/test_soundfile.py | 55 ++++------ 8 files changed, 226 insertions(+), 79 deletions(-) create mode 100644 recipes/cffi/patches/ios-ffi-closure-alloc.patch diff --git a/recipes/cffi/meta.yaml b/recipes/cffi/meta.yaml index 92269e12..09714175 100644 --- a/recipes/cffi/meta.yaml +++ b/recipes/cffi/meta.yaml @@ -1,6 +1,6 @@ package: name: cffi - version: "2.0.0" + version: "2.1.1" build: number: 1 @@ -10,4 +10,5 @@ requirements: - libffi patches: - - mobile.patch \ No newline at end of file + - mobile.patch + - ios-ffi-closure-alloc.patch diff --git a/recipes/cffi/patches/ios-ffi-closure-alloc.patch b/recipes/cffi/patches/ios-ffi-closure-alloc.patch new file mode 100644 index 00000000..dc9dfa31 --- /dev/null +++ b/recipes/cffi/patches/ios-ffi-closure-alloc.patch @@ -0,0 +1,54 @@ +Make ffi.callback() and variadic calls work on iOS (python-cffi/cffi#265). + +cffi uses libffi's ffi_closure_alloc()/ffi_prep_closure_loc()/ffi_prep_cif_var() +on Apple only when ffi.h defines FFI_AVAILABLE_APPLE, which only the macOS SDK's +ffi.h does. iOS links the support tree's upstream libffi, so every +ffi.callback() fell back to malloc_closure.h's mmap(PROT_WRITE|PROT_EXEC), +which iOS refuses ("MemoryError: Cannot allocate write+execute memory"), and +variadic calls went through ffi_prep_cif(), which passes garbage on arm64, +where Apple puts varargs on the stack. The fallback's ffi_prep_closure() is +unusable on arm64 even with W+X allowed: that libffi uses trampoline tables. + +Same defines as upstream #265 (90e8cc434a, merged 2026-07-26, in no release +as of 2.1.1), narrowed to TARGET_OS_IPHONE rather than upstream's hunk +verbatim, because forge applies patches with a non-interactive `patch -p1`: +on a release that already carries #265 the verbatim hunk is taken as +"previously applied", answered "Assume -R? [y]", and silently REVERTED with +exit 0 (measured on the 2.1.1 tree with #265 applied). This variant lands +there as a redundant, dead branch instead. Android is not __APPLE__ and forge +builds no macOS, so behaviour on every forge target equals upstream's. Drop +this patch on a cffi release that contains #265. + +--- a/src/c/_cffi_backend.c ++++ b/src/c/_cffi_backend.c +@@ -99,6 +99,10 @@ + * This is also used on macOS, provided we are executing on macOS 10.15 or + * above. It's a mess because it needs runtime checks in that case. + */ ++#ifdef __APPLE__ ++# include ++#endif ++ + #ifdef __NetBSD__ + + # define CFFI_CHECK_FFI_CLOSURE_ALLOC 1 +@@ -117,6 +121,19 @@ + # define CFFI_CHECK_FFI_PREP_CIF_VAR __builtin_available(macos 10.15, ios 13, watchos 6, tvos 13, *) + # define CFFI_CHECK_FFI_PREP_CIF_VAR_MAYBE 1 + ++#elif defined(__APPLE__) && TARGET_OS_IPHONE ++ ++/* iOS links upstream libffi, whose ffi.h lacks FFI_AVAILABLE_APPLE (only the ++ * macOS SDK's has it). Falling to malloc_closure.h means a W+X mmap iOS ++ * refuses, and ffi_prep_cif() for varargs, which arm64 Apple passes on the ++ * stack. The static libffi always has all three. python-cffi/cffi#265. */ ++# define CFFI_CHECK_FFI_CLOSURE_ALLOC 1 ++# define CFFI_CHECK_FFI_CLOSURE_ALLOC_MAYBE 1 ++# define CFFI_CHECK_FFI_PREP_CLOSURE_LOC 1 ++# define CFFI_CHECK_FFI_PREP_CLOSURE_LOC_MAYBE 1 ++# define CFFI_CHECK_FFI_PREP_CIF_VAR 1 ++# define CFFI_CHECK_FFI_PREP_CIF_VAR_MAYBE 1 ++ + #elif defined(__EMSCRIPTEN__) + + # define CFFI_CHECK_FFI_CLOSURE_ALLOC 1 diff --git a/recipes/cffi/patches/mobile.patch b/recipes/cffi/patches/mobile.patch index b55f9d93..fa3144a6 100644 --- a/recipes/cffi/patches/mobile.patch +++ b/recipes/cffi/patches/mobile.patch @@ -1,8 +1,19 @@ -diff --git a/setup.py b/setup.py -index f9cabaa..c592203 100644 +Cross-compile cffi against the target's libffi, not the build machine's. + +setup.py: drop the /usr/include/ffi and /usr/include/libffi defaults and the +pkg-config probe, which would point a cross build at the host's libffi +headers; forge's cross environment supplies the target paths. + +malloc_closure.h: guard the Linux-only PaX getline() scan by +__ANDROID_API__ >= 18, as Chaquopy does (bionic gained getline() at API 18). +A no-op at forge's API 24. + +Regenerated against 2.1.1, where the setup.py hunk moved by 6 lines. + +diff -ruN a/setup.py b/setup.py --- a/setup.py +++ b/setup.py -@@ -9,8 +9,7 @@ import setuptools +@@ -9,8 +9,7 @@ sources = ['src/c/_cffi_backend.c'] libraries = ['ffi'] @@ -12,7 +23,7 @@ index f9cabaa..c592203 100644 define_macros = [('FFI_BUILDING', '1')] # for linking with libffi static library library_dirs = [] extra_compile_args = [] -@@ -144,7 +143,7 @@ if sys.platform == "win32" and uses_msvc(): +@@ -150,7 +149,7 @@ sources.extend(os.path.join(COMPILE_LIBFFI, filename) for filename in _filenames) else: @@ -21,11 +32,10 @@ index f9cabaa..c592203 100644 ask_supports_thread() ask_supports_sync_synchronize() -diff --git a/src/c/malloc_closure.h b/src/c/malloc_closure.h -index bebb93d..f82dbe4 100644 +diff -ruN a/src/c/malloc_closure.h b/src/c/malloc_closure.h --- a/src/c/malloc_closure.h +++ b/src/c/malloc_closure.h -@@ -42,6 +42,8 @@ emutramp_enabled_check (void) +@@ -42,6 +42,8 @@ return 0; ret = 0; @@ -34,7 +44,7 @@ index bebb93d..f82dbe4 100644 while (getline (&buf, &len, f) != -1) if (!strncmp (buf, "PaX:", 4)) { -@@ -51,6 +53,8 @@ emutramp_enabled_check (void) +@@ -51,6 +53,8 @@ break; } free (buf); @@ -42,4 +52,4 @@ index bebb93d..f82dbe4 100644 + fclose (f); return ret; - } \ No newline at end of file + } diff --git a/recipes/cffi/tests/test_cffi.py b/recipes/cffi/tests/test_cffi.py index 51a80945..3c00a817 100644 --- a/recipes/cffi/tests/test_cffi.py +++ b/recipes/cffi/tests/test_cffi.py @@ -1,7 +1,101 @@ -def test_basic(): - from cffi import FFI +import gc +import sys +import threading +from typing import Any +from cffi import FFI + +CDEF = """ + size_t strlen(char *str); + void qsort(void *base, size_t nmemb, size_t size, + int (*compar)(const void *, const void *)); + typedef unsigned long pthread_t; + int pthread_create(pthread_t *thread, void *attr, + void *(*start)(void *), void *arg); + int pthread_join(pthread_t thread, void **retval); + int snprintf(char *str, size_t size, const char *format, ...); +""" + + +def _libc() -> tuple[FFI, Any]: + """ABI-mode handle on the C library: bionic's public libc.so on Android, the + process's own symbols (libSystem) on iOS.""" ffi = FFI() - ffi.cdef("size_t strlen(char *str);") - lib = ffi.dlopen(None) + ffi.cdef(CDEF) + # Not sys.platform: Android's Python 3.12 still reports "linux". + android = hasattr(sys, "getandroidapilevel") + return ffi, ffi.dlopen("libc.so" if android else None) + + +def test_basic() -> None: + """ABI-mode call into libc through a dlopen'd handle.""" + ffi, lib = _libc() assert lib.strlen(ffi.new("char[]", b"hello world")) == 11 + + +def test_callback_called_from_c() -> None: + """C calls a Python ffi.callback() (qsort's comparator). On iOS this needs + libffi's ffi_closure_alloc(): cffi's own allocator asks for W+X pages.""" + ffi, lib = _libc() + + @ffi.callback("int(const void *, const void *)") + def cmp(a: Any, b: Any) -> int: + x, y = ffi.cast("int *", a)[0], ffi.cast("int *", b)[0] + return (x > y) - (x < y) + + arr = ffi.new("int[]", [5, 3, 9, 1, 7, 2]) + lib.qsort(arr, 6, ffi.sizeof("int"), cmp) + assert list(arr) == [1, 2, 3, 5, 7, 9] + + +def test_callback_from_native_thread() -> None: + """A callback entered on a thread Python did not create, as PortAudio and + libsndfile's virtual I/O do.""" + ffi, lib = _libc() + seen: list[tuple[int, int]] = [] + + @ffi.callback("void *(void *)") + def start(arg: Any) -> Any: + seen.append((threading.get_ident(), int(ffi.cast("intptr_t", arg)))) + return ffi.NULL + + tid = ffi.new("pthread_t *") + assert lib.pthread_create(tid, ffi.NULL, start, ffi.cast("void *", 1234)) == 0 + assert lib.pthread_join(tid[0], ffi.NULL) == 0 + assert len(seen) == 1 + assert seen[0][1] == 1234 + assert seen[0][0] != threading.get_ident() + + +def test_many_callbacks_freed_and_reallocated() -> None: + """3000 live closures, freed, then 3000 more. On arm64 iOS a libffi trampoline + table holds 1024, so this grows past one table, releases them and starts again.""" + ffi, _ = _libc() + cbs = [ + ffi.callback("int(int)", (lambda k: lambda x: x + k)(k)) for k in range(3000) + ] + assert [cbs[k](1) for k in (0, 1023, 1024, 2999)] == [1, 1024, 1025, 3000] + del cbs + gc.collect() + again = [ffi.callback("int(int)", lambda x: -x) for _ in range(3000)] + assert again[0](5) == -5 + assert again[-1](7) == -7 + + +# Last on purpose: a backend without ffi_prep_cif_var() can crash here, not just fail. +def test_variadic_call() -> None: + """snprintf with a string, an int and a double in the variadic part. arm64 + Apple passes varargs on the stack, so this is garbage or a crash unless cffi + prepares the call with ffi_prep_cif_var().""" + ffi, lib = _libc() + buf = ffi.new("char[64]") + n = lib.snprintf( + buf, + 64, + b"[%s] [%d] [%.2f]", + ffi.new("char[]", b"hi"), + ffi.cast("int", 42), + ffi.cast("double", 3.5), + ) + assert ffi.string(buf) == b"[hi] [42] [3.50]" + assert n == 16 diff --git a/recipes/soundfile/README.md b/recipes/soundfile/README.md index bd042bb9..e4849ef1 100644 --- a/recipes/soundfile/README.md +++ b/recipes/soundfile/README.md @@ -61,27 +61,26 @@ file, so it is how you check a samplerate or duration before deciding to decode. ### Reading audio the user picked [`FilePicker`](https://flet.dev/docs/controls/filepicker/) hands back a path on both -platforms, and `sf.read()` takes it directly. **A real path is the only form that works -everywhere** — see the iOS note below. +platforms, and `sf.read()` takes it directly. -Where you hold bytes instead (a download, a file read out of `assets/`), `soundfile` does -accept any file object, and on Android that works: +Where you hold bytes instead (a download, a file read out of `assets/`), pass any binary +file object: ```python import io -data, sr = sf.read(io.BytesIO(raw_bytes)) # Android and desktop only +data, sr = sf.read(io.BytesIO(raw_bytes)) ``` -**On iOS this raises `MemoryError`,** and no version of the package will fix it. That path -uses libsndfile's virtual I/O, which means handing the C library a `ffi.callback()`; cffi -writes that trampoline into memory at runtime, and iOS refuses write+execute pages to an -app without the JIT entitlement. The message names the cause: +A file object goes through libsndfile's virtual I/O, which calls back into Python through +`ffi.callback()`. That works on both platforms with **cffi 2.1.1-1 or later from +pypi.flet.dev**, which pip picks over PyPI's own iOS wheel of the same version. Apps built +against an older cffi raised this on iOS for every file object: MemoryError: Cannot allocate write+execute memory for ffi.callback(). -Write the bytes to a file and read that instead — it costs one write, needs no -per-platform branch, and is the recommended form on both platforms: +A real path is still the simplest form, with no callbacks at all. If the bytes are going to +disk anyway, read them back by path: ```python import os @@ -92,9 +91,6 @@ with open(path, "wb") as f: data, sr = sf.read(path) ``` -Only the *file-object* form is affected. Paths, and integer file descriptors (which go -through `sf_open_fd`), use no callbacks and work normally on iOS. - ### Storage `soundfile` reads and writes nothing of its own — no config, no cache, no env vars. Files you @@ -142,9 +138,10 @@ device, which is not something you can assume for every package. being killed by the OS. Pass `dtype="float32"` unless you specifically need the precision, and prefer `blocks()` over reading whole files. -- **iOS cannot read audio from a file object.** Repeating it here because it is the one - thing that behaves differently between the two platforms, and it fails at the point of - use rather than at import. `io.BytesIO` in, `MemoryError` out; write a file first. +- **`MemoryError: Cannot allocate write+execute memory` on iOS means an old cffi.** File + objects need cffi 2.1.1-1 or later; with PyPI's 2.1.1 or our 2.0.0-1 the app imports + fine and fails at the first file object. Check which cffi the build resolved rather + than looking at soundfile. - **Opus only encodes at 48 kHz.** libsndfile will not resample for you; `sf.write(..., format="OGG", subtype="OPUS")` at any other rate raises. Resample first — [`soxr`](../soxr) @@ -218,6 +215,10 @@ download six wheels whose contents already live inside `libsndfile.so`. `py2.py3-none-any` for an unknown `sys.platform`. forge's `fix_wheel` overwrites the tag, which is what makes our wheel outrank PyPI's `any` wheel at the same version. Do not "fix" the tag in a patch. +- **File objects on iOS depend on the cffi recipe's `ios-ffi-closure-alloc.patch`** + (python-cffi/cffi#265, in no cffi release as of 2.1.1). If PyPI ships a newer cffi with + iOS wheels before pypi.flet.dev does, apps resolve it, and `test_read_from_file_object` + goes red on iOS unless that release carries #265. - **FLAC 1.5.0 needs `-DCMAKE_SYSTEM_VERSION` on Android** and `-DENABLE_MULTITHREADING=OFF`; both have comments in `recipes/flet-libflac/build.sh` saying what breaks without them. @@ -241,10 +242,10 @@ download six wheels whose contents already live inside `libsndfile.so`. ### Coverage gaps The device tests cover loading the library, WAV round trips in three subtypes, every -container and codec, virtual I/O from `BytesIO` (Android) and its `MemoryError` on iOS, -the write-a-file workaround, block reads with seeking, and the format registry. They do not -cover: writing to `FLET_APP_STORAGE_*` (a path question, not a -libsndfile one), any real recorded audio file, `sf.info()`, the `RAW` format's manual -`samplerate`/`channels`/`subtype` arguments, or multi-threaded decoding. The security -backports are compile-verified only — there are no regression tests for the five fixes, since -reproducing them needs the malformed inputs from upstream's fuzz corpus. +container and codec, virtual I/O from `BytesIO` on both platforms, bytes spilled to a path, +block reads with seeking, and the format registry. They do not cover: writing to +`FLET_APP_STORAGE_*` (a path question, not a libsndfile one), any real recorded audio +file, `sf.info()`, the `RAW` format's manual `samplerate`/`channels`/`subtype` arguments, +or multi-threaded decoding. The security backports are compile-verified only — there are +no regression tests for the five fixes, since reproducing them needs the malformed inputs +from upstream's fuzz corpus. diff --git a/recipes/soundfile/examples/codec-roundtrip/README.md b/recipes/soundfile/examples/codec-roundtrip/README.md index e297fbb7..d31abd3f 100644 --- a/recipes/soundfile/examples/codec-roundtrip/README.md +++ b/recipes/soundfile/examples/codec-roundtrip/README.md @@ -20,10 +20,10 @@ What it demonstrates: quantisation, not codec loss — a distinction worth seeing once. - **Writing into Flet's storage.** Each round trip writes a real file under [`FLET_APP_STORAGE_TEMP`](https://flet.dev/docs/reference/environment-variables#flet_app_storage_temp) - and deletes it again. Files rather than `io.BytesIO` deliberately: a file object routes - through libsndfile's virtual I/O, which needs a cffi callback, which needs write+execute - memory that iOS refuses — so the file-object form works on Android and raises - `MemoryError` on iOS. A path works on both. + and deletes it again. Files rather than `io.BytesIO` deliberately: a path needs no + callbacks, so it works with any cffi. A file object routes through libsndfile's virtual + I/O, which calls back into Python through `ffi.callback()`; that works on both platforms + too, but on iOS only with cffi 2.1.1-1 or later from pypi.flet.dev. - **Compute off the UI thread.** The sweep runs in [`page.run_thread(...)`](https://flet.dev/docs/controls/page/#flet.Page.run_thread) with a spinner up, ending in the explicit diff --git a/recipes/soundfile/examples/codec-roundtrip/src/audio_codecs.py b/recipes/soundfile/examples/codec-roundtrip/src/audio_codecs.py index 83ef6fa5..1b01038f 100644 --- a/recipes/soundfile/examples/codec-roundtrip/src/audio_codecs.py +++ b/recipes/soundfile/examples/codec-roundtrip/src/audio_codecs.py @@ -41,8 +41,8 @@ def roundtrip(label, fmt, subtype, rate): audible-codec-sized for the lossy ones. Returns an `error` string instead if the format is not compiled in. - Files, not io.BytesIO: a file object would route through libsndfile's virtual I/O, - which needs a cffi callback, which needs write+execute memory that iOS refuses. + Files, not io.BytesIO: a path needs no virtual I/O callbacks, so it works with any + cffi. A file object works too, but on iOS only with cffi 2.1.1-1 or later. """ source = signal(rate) # FLET_APP_STORAGE_TEMP on device; tempfile's default elsewhere. diff --git a/recipes/soundfile/tests/test_soundfile.py b/recipes/soundfile/tests/test_soundfile.py index bdf42a8a..b293713c 100644 --- a/recipes/soundfile/tests/test_soundfile.py +++ b/recipes/soundfile/tests/test_soundfile.py @@ -1,5 +1,5 @@ import io -import sys +from pathlib import Path import numpy as np import pytest @@ -8,20 +8,22 @@ SR = 16000 -def _tone(seconds=0.25, freq=440.0, sample_rate=SR): +def _tone( + seconds: float = 0.25, freq: float = 440.0, sample_rate: int = SR +) -> np.ndarray: """Deterministic mono float64 sine — no RNG, no assets, no network.""" t = np.arange(int(sample_rate * seconds)) / sample_rate return np.sin(2.0 * np.pi * freq * t) -def test_libsndfile_loaded(): +def test_libsndfile_loaded() -> None: """The cffi loader found the flet-libsndfile .so and bound the C API — this is the single claim the mobile patch exists to make true.""" assert sf.__libsndfile_version__ assert sf.SoundFile is not None -def test_wav_roundtrip(tmp_path): +def test_wav_roundtrip(tmp_path: Path) -> None: """A float64 signal written as 32-bit float WAV reads back bit-identical, with the samplerate and channel count preserved.""" path = str(tmp_path / "tone.wav") @@ -34,7 +36,7 @@ def test_wav_roundtrip(tmp_path): np.testing.assert_array_equal(y, x) -def test_stereo_and_int_subtypes(tmp_path): +def test_stereo_and_int_subtypes(tmp_path: Path) -> None: """Two-channel PCM_16 and PCM_24 round-trip within their quantisation step — covers the integer conversion paths, not just the float passthrough.""" x = np.stack([_tone(), 0.5 * _tone(freq=880.0)], axis=1) @@ -62,7 +64,9 @@ def test_stereo_and_int_subtypes(tmp_path): ("MP3", "mp3", "MPEG_LAYER_III", True), ], ) -def test_container_roundtrip(tmp_path, fmt, ext, subtype, lossy): +def test_container_roundtrip( + tmp_path: Path, fmt: str, ext: str, subtype: str, lossy: bool +) -> None: """Every container this wheel's libsndfile is built for encodes and decodes on device — including the ones that only work because flet-libflac, flet-libogg, flet-libvorbis, flet-libopus, flet-libmpg123 and flet-libmp3lame are linked in. @@ -85,11 +89,11 @@ def test_container_roundtrip(tmp_path, fmt, ext, subtype, lossy): assert np.max(np.abs(y - x)) <= 2**-15 -@pytest.mark.skipif(sys.platform == "ios", reason="no W+X memory for ffi.callback()") -def test_read_from_file_object(): +def test_read_from_file_object() -> None: """sf.read() works on an in-memory binary stream via libsndfile's virtual I/O — the path an app takes for audio fetched over the network, where there may be - no real file to open. Android only; see the iOS counterpart below.""" + no real file to open. Virtual I/O hands libsndfile cffi callbacks, so on iOS + this needs cffi >= 2.1.1-1 (libffi's closure allocator, not W+X pages).""" buf = io.BytesIO() x = _tone() sf.write(buf, x, SR, format="WAV", subtype="DOUBLE") @@ -100,30 +104,13 @@ def test_read_from_file_object(): np.testing.assert_array_equal(y, x) -@pytest.mark.skipif(sys.platform != "ios", reason="iOS-only limitation") -def test_file_object_is_unavailable_on_ios(): - """Passing a file object on iOS raises MemoryError, and this is permanent: virtual - I/O hands libsndfile a `ffi.callback()`, cffi writes that trampoline at runtime, and - iOS refuses write+execute pages to an app without the JIT entitlement. Asserted - rather than skipped so the README's "write it to a file first" guidance is checked, - and so a future cffi or OS change that lifts it does not go unnoticed.""" - buf = io.BytesIO() - with pytest.raises(MemoryError, match="write.execute"): - sf.write(buf, _tone(), SR, format="WAV", subtype="DOUBLE") - - -def test_path_roundtrip_is_the_ios_workaround(tmp_path): - """The replacement for the file-object path: spill the bytes to a real file and read - it back. Works on every platform, so an app needs no per-platform branch.""" - raw = io.BytesIO() +def test_bytes_spilled_to_a_path(tmp_path: Path) -> None: + """The README's simplest form for bytes in hand: write them to a real file and + read that by path. No virtual I/O anywhere, so this works with any cffi.""" x = _tone() - try: - sf.write(raw, x, SR, format="WAV", subtype="DOUBLE") - encoded = raw.getvalue() - except MemoryError: # iOS: produce the same bytes without virtual I/O - staged = str(tmp_path / "staged.wav") - sf.write(staged, x, SR, subtype="DOUBLE") - encoded = open(staged, "rb").read() + staged = tmp_path / "staged.wav" + sf.write(str(staged), x, SR, subtype="DOUBLE") + encoded = staged.read_bytes() path = tmp_path / "from_bytes.wav" path.write_bytes(encoded) @@ -132,7 +119,7 @@ def test_path_roundtrip_is_the_ios_workaround(tmp_path): np.testing.assert_array_equal(y, x) -def test_blockwise_read(tmp_path): +def test_blockwise_read(tmp_path: Path) -> None: """SoundFile seeking and block reads reassemble the whole signal — the memory- bounded path for files too large to hold in RAM on a phone.""" path = str(tmp_path / "tone.wav") @@ -151,7 +138,7 @@ def test_blockwise_read(tmp_path): np.testing.assert_array_equal(f.read(100), x[SR // 2 : SR // 2 + 100]) -def test_available_formats_advertises_the_linked_codecs(): +def test_available_formats_advertises_the_linked_codecs() -> None: """libsndfile's own format registry lists the codec containers, proving they were compiled in rather than merely present at link time.""" formats = sf.available_formats() From d21df7f10ec1fb8949d23f8011b7fe04d13d2de6 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 22 Oct 2026 18:57:01 +0200 Subject: [PATCH 2/3] ci: bump py3-none test wheels to build 9999 too dist-test renamed only cp-tagged wheels, so a rebuilt flet-lib* at an already published version and build number could lose to its published copy in the device test. Android's loop also missed abi3; both loops now match cpXY-cpXY, cpXY-abi3 and py3-none. --- .github/workflows/build-wheels-version.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-wheels-version.yml b/.github/workflows/build-wheels-version.yml index 7dbd4842..db40de69 100644 --- a/.github/workflows/build-wheels-version.yml +++ b/.github/workflows/build-wheels-version.yml @@ -495,10 +495,11 @@ jobs: for w in "$GITHUB_WORKSPACE"/dist/*.whl; do [[ -e "$w" ]] || continue base=$(basename "$w") - # name-version[-buildtag]-cpXY-cpXY-plat.whl - # → name-version-9999-cpXY-cpXY-plat.whl + # name-version[-buildtag]-{cpXY-cpXY|cpXY-abi3|py3-none}-plat.whl + # → name-version-9999-...-plat.whl. py3-none covers the build.sh + # flet-lib* wheels a prebuild_recipes chain puts here. new=$(printf '%s\n' "$base" \ - | sed -E 's/^([^-]+-[^-]+)(-[0-9]+)?-(cp[0-9]+-cp[0-9]+)-/\1-9999-\3-/') + | sed -E 's/^([^-]+-[^-]+)(-[0-9]+)?-((cp[0-9]+|py3)-(cp[0-9]+|abi[0-9]+|none))-/\1-9999-\3-/') cp "$w" "$GITHUB_WORKSPACE/dist-test/$new" done @@ -562,7 +563,7 @@ jobs: [[ -e "$w" ]] || continue base=$(basename "$w") new=$(printf '%s\n' "$base" \ - | sed -E 's/^([^-]+-[^-]+)(-[0-9]+)?-(cp[0-9]+-(cp[0-9]+|abi[0-9]+))-/\1-9999-\3-/') + | sed -E 's/^([^-]+-[^-]+)(-[0-9]+)?-((cp[0-9]+|py3)-(cp[0-9]+|abi[0-9]+|none))-/\1-9999-\3-/') cp "$w" "$GITHUB_WORKSPACE/dist-test/$new" done From fb419afa1a2db956b1bd5a05192bd19fad2e1e37 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 22 Oct 2026 19:31:10 +0200 Subject: [PATCH 3/3] ci: pin Ubuntu runners to ubuntu-24.04 ubuntu-latest will move to 26.04. The Android lane (NDK builds, KVM emulator) runs there, so that move would change every recipe's CI at once and look like recipe breakage. Pin it, like macOS is pinned to macos-26, and bump on purpose. --- .github/workflows/build-wheels-version.yml | 4 ++-- .github/workflows/build-wheels.yml | 2 +- .github/workflows/zizmor.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-wheels-version.yml b/.github/workflows/build-wheels-version.yml index db40de69..6c4da06c 100644 --- a/.github/workflows/build-wheels-version.yml +++ b/.github/workflows/build-wheels-version.yml @@ -133,7 +133,7 @@ permissions: jobs: setup: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} has_jobs: ${{ steps.set-matrix.outputs.has_jobs }} @@ -180,7 +180,7 @@ jobs: for pkg in $build_packages; do pkg_name="${pkg%%:*}" if [[ "$arch" == "android" ]]; then - runner="ubuntu-latest" + runner="ubuntu-24.04" platform="android" rust_targets="aarch64-linux-android,arm-linux-androideabi,x86_64-linux-android" else diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index f8345833..23df609e 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -111,7 +111,7 @@ permissions: jobs: detect: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: pythons_json: ${{ steps.detect-pythons.outputs.pythons_json }} canonical_python: ${{ steps.detect-pythons.outputs.canonical_python }} diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index e9ba32c0..c8ffd438 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -9,7 +9,7 @@ permissions: {} jobs: zizmor: name: Run zizmor - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: security-events: write contents: read