From 277ae403ecf8bdc1223d6155c3ca81b1f4d9846a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Thu, 24 Jul 2025 17:00:24 +0200 Subject: [PATCH 01/10] Add support for building ARM64 iOS wheels on CI --- .github/workflows/ci.yaml | 109 ++++++++++++++++++++++++++++++++++++-- src/c/test_c.py | 30 +++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 95473337..6db3688a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -111,7 +111,7 @@ jobs: - { spec: cp313-manylinux_aarch64, arch: aarch64, omit: ${{ env.skip_ci_redundant_jobs }} } - { spec: cp314-manylinux_aarch64, arch: aarch64, cibw_version: cibuildwheel~=3.0b1 } - # aarch64 musllinux + # aarch64 musllinux - { spec: cp39-musllinux_aarch64, arch: aarch64, omit: ${{ env.skip_ci_redundant_jobs }} } - { spec: cp310-musllinux_aarch64, arch: aarch64, omit: ${{ env.skip_ci_redundant_jobs }} } - { spec: cp311-musllinux_aarch64, arch: aarch64, omit: ${{ env.skip_ci_redundant_jobs }} } @@ -373,8 +373,111 @@ jobs: if-no-files-found: error if: ${{ env.skip_artifact_upload != 'true' }} + make_ios_matrix: + runs-on: ubuntu-24.04 + outputs: + matrix_json: ${{ steps.make_matrix.outputs.matrix_json }} + steps: + - uses: actions/checkout@v4 + - name: make a matrix + id: make_matrix + uses: ./.github/actions/dynamatrix + with: + matrix_yaml: | + include: + # arm64 iOS device + - { spec: cp313-ios_arm64_iphoneos, platform: 'iphoneos' } + - { spec: cp314-ios_arm64_iphoneos, platform: 'iphoneos' } + + # arm64 iOS simulator + - { spec: cp313-ios_arm64_iphonesimulator, platform: 'iphonesimulator' } + - { spec: cp314-ios_arm64_iphonesimulator, platform: 'iphonesimulator' } + + ios: + needs: [python_sdist, make_ios_matrix] + runs-on: macos-14 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.make_ios_matrix.outputs.matrix_json) }} + + steps: + - name: fetch sdist artifact + id: fetch_sdist + uses: actions/download-artifact@v4 + with: + name: ${{ needs.python_sdist.outputs.sdist_artifact_name }} + + - name: install python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: build wheel prereqs + run: | + set -eux + python3 -m pip install --user --upgrade cibuildwheel>=3.1.0 + brew uninstall --ignore-dependencies libffi 2>&1 || true + + - name: download libffi for iOS + env: + CFFI_IOS_LIBFFI_VERSION: '3.4.7-2' + run: | + set -eux + + # Download prebuilt libffi from beeware/cpython-apple-source-deps + platform="${{ matrix.platform }}" + version="${CFFI_IOS_LIBFFI_VERSION}" + url="https://github.com/beeware/cpython-apple-source-deps/releases/download/libFFI-${version}/libffi-${version}-${platform}.arm64.tar.gz" + + echo "Downloading libffi for iOS (${platform})..." + curl -L -o libffi-ios.tar.gz "${url}" + + # Extract libffi + mkdir -p libffi-ios + tar zxf libffi-ios.tar.gz -C libffi-ios + + # Set up paths for cibuildwheel + echo "LIBFFI_IOS_DIR=$(pwd)/libffi-ios" >> "$GITHUB_ENV" + + - name: build/test wheels + id: build + env: + CIBW_BUILD: ${{ matrix.spec }} + CIBW_ENABLE: cpython-prerelease + CIBW_PLATFORM: ios + SDKROOT: ${{ matrix.platform }} + CIBW_TEST_REQUIRES: pytest setuptools + CIBW_TEST_SOURCES: cffi + # Running tests from `testing/` will not work since they try to compile C code on device + CIBW_TEST_COMMAND: python -m pytest -sv cffi/src/c/ + # Environment variables for the build + CIBW_ENVIRONMENT: > + CFLAGS="-I${LIBFFI_IOS_DIR}/include" + LDFLAGS="-L${LIBFFI_IOS_DIR}/lib" + PYTHONUNBUFFERED=1 + # Pass through our custom env vars + CIBW_ENVIRONMENT_PASS_IOS: LIBFFI_IOS_DIR PYTHONUNBUFFERED + run: | + set -eux + + mkdir cffi + + tar zxf ${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz --strip-components=1 -C cffi + + python3 -m cibuildwheel --output-dir dist cffi + + echo "artifact_name=$(ls ./dist/)" >> "$GITHUB_OUTPUT" + + - name: upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.build.outputs.artifact_name }} + path: dist/*.whl + if-no-files-found: error + if: ${{ env.skip_artifact_upload != 'true' }} + merge_artifacts: - needs: [python_sdist, linux, macos, windows] + needs: [python_sdist, linux, macos, windows, ios] runs-on: ubuntu-24.04 steps: - name: merge all artifacts @@ -387,7 +490,7 @@ jobs: check: if: always() - needs: [python_sdist, linux, macos, windows, merge_artifacts] + needs: [python_sdist, linux, macos, windows, ios, merge_artifacts] runs-on: ubuntu-24.04 steps: - name: Verify all previous jobs succeeded (provides a single check to sample for gating purposes) diff --git a/src/c/test_c.py b/src/c/test_c.py index e1121ed8..243a248a 100644 --- a/src/c/test_c.py +++ b/src/c/test_c.py @@ -17,6 +17,9 @@ except ImportError: pass +is_ios = sys.platform == 'ios' + + def _setup_path(): import os, sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) @@ -1229,6 +1232,10 @@ def test_cannot_pass_struct_with_array_of_length_0(): BFunc2 = new_function_type((BInt,), BStruct, False) pytest.raises(NotImplementedError, cast(BFunc2, 123), 123) +@pytest.mark.xfail( + is_ios, + reason="For an unknown reason f(1, cast(BInt, 42)) returns 36792864", +) def test_call_function_9(): BInt = new_primitive_type("int") BFunc9 = new_function_type((BInt,), BInt, True) # vararg @@ -1360,6 +1367,7 @@ def test_write_variable(): pytest.raises(ValueError, ll.write_variable, BVoidP, "stderr", stderr) +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback(): BInt = new_primitive_type("int") def make_callback(): @@ -1376,6 +1384,7 @@ def cb(n): assert str(e.value) == "'int(*)(int)' expects 1 arguments, got 0" +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_exception(): def check_value(x): if x == 10000: @@ -1432,6 +1441,7 @@ def oops(*args): assert ff(bigvalue) == -42 +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_return_type(): for rettype in ["signed char", "short", "int", "long", "long long", "unsigned char", "unsigned short", "unsigned int", @@ -1452,6 +1462,7 @@ def cb(n): assert f(max - 1) == max assert f(max) == 42 +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_a_lot_of_callbacks(): BIGNUM = 10000 if 'PY_DOT_PY' in globals(): BIGNUM = 100 # tests on py.py @@ -1467,6 +1478,7 @@ def cb(n): for i, f in enumerate(flist): assert f(-142) == -142 + i +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_receiving_tiny_struct(): BSChar = new_primitive_type("signed char") BInt = new_primitive_type("int") @@ -1482,6 +1494,7 @@ def cb(s): n = f(p[0]) assert n == -42 +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_returning_tiny_struct(): BSChar = new_primitive_type("signed char") BInt = new_primitive_type("int") @@ -1499,6 +1512,7 @@ def cb(n): assert s.a == -10 assert s.b == -30 +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_receiving_struct(): BSChar = new_primitive_type("signed char") BInt = new_primitive_type("int") @@ -1515,6 +1529,7 @@ def cb(s): n = f(p[0]) assert n == 42 +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_returning_struct(): BSChar = new_primitive_type("signed char") BInt = new_primitive_type("int") @@ -1534,6 +1549,7 @@ def cb(n): assert s.a == -10 assert s.b == 1E-42 +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_receiving_big_struct(): BInt = new_primitive_type("int") BStruct = new_struct_type("struct foo") @@ -1558,6 +1574,7 @@ def cb(s): n = f(p[0]) assert n == 42 +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_returning_big_struct(): BInt = new_primitive_type("int") BStruct = new_struct_type("struct foo") @@ -1583,6 +1600,7 @@ def cb(): for i, name in enumerate("abcdefghij"): assert getattr(s, name) == 13 - i +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_returning_void(): BVoid = new_void_type() BFunc = new_function_type((), BVoid, False) @@ -1691,6 +1709,7 @@ def test_enum_overflow(): pytest.raises(OverflowError, new_enum_type, "foo", ("AA",), (testcase,), BPrimitive) +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_returning_enum(): BInt = new_primitive_type("int") BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, -20), BInt) @@ -1707,6 +1726,7 @@ def cb(n): assert f(20) == 20 assert f(21) == 21 +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_returning_enum_unsigned(): BInt = new_primitive_type("int") BUInt = new_primitive_type("unsigned int") @@ -1724,6 +1744,7 @@ def cb(n): assert f(20) == 20 assert f(21) == 21 +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_returning_char(): BInt = new_primitive_type("int") BChar = new_primitive_type("char") @@ -1738,6 +1759,7 @@ def _hacked_pypy_uni4(): pyuni4 = {1: True, 2: False}[len(u+'\U00012345')] return 'PY_DOT_PY' in globals() and not pyuni4 +@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS") def test_callback_returning_wchar_t(): BInt = new_primitive_type("int") BWChar = new_primitive_type("wchar_t") @@ -2307,6 +2329,8 @@ def _test_wchar_variant(typename): assert str(q) == repr(q) pytest.raises(RuntimeError, string, q) # + if is_ios: + return # cannot allocate executable memory for the callback() below def cb(p): assert repr(p).startswith(" Date: Thu, 24 Jul 2025 17:11:42 +0200 Subject: [PATCH 02/10] s/skip/skipif/ --- src/c/test_c.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c/test_c.py b/src/c/test_c.py index 243a248a..541ea69f 100644 --- a/src/c/test_c.py +++ b/src/c/test_c.py @@ -3018,7 +3018,7 @@ def test_string_assignment_to_byte_array(): except ImportError: pass # win32 -@pytest.mark.skip( +@pytest.mark.skipif( is_ios, reason="For an unknown reason fscanf() doesn't read anything on 3.14" " and crashes on 3.13 (that's why it's not an xfail)", From 5fd5b264de4c58e9b1a8d1d0a275cd0eb5fc9c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 25 Jul 2025 12:14:05 +0200 Subject: [PATCH 03/10] Bump ios jobs to macos-15 and build-time Python 3.13 --- .github/workflows/ci.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6db3688a..4c793483 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -386,16 +386,16 @@ jobs: matrix_yaml: | include: # arm64 iOS device - - { spec: cp313-ios_arm64_iphoneos, platform: 'iphoneos' } - - { spec: cp314-ios_arm64_iphoneos, platform: 'iphoneos' } + - { spec: cp313-ios_arm64_iphoneos } + - { spec: cp314-ios_arm64_iphoneos } # arm64 iOS simulator - - { spec: cp313-ios_arm64_iphonesimulator, platform: 'iphonesimulator' } - - { spec: cp314-ios_arm64_iphonesimulator, platform: 'iphonesimulator' } + - { spec: cp313-ios_arm64_iphonesimulator } + - { spec: cp314-ios_arm64_iphonesimulator } ios: needs: [python_sdist, make_ios_matrix] - runs-on: macos-14 + runs-on: macos-15 strategy: fail-fast: false matrix: ${{ fromJSON(needs.make_ios_matrix.outputs.matrix_json) }} @@ -410,7 +410,7 @@ jobs: - name: install python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.13' - name: build wheel prereqs run: | From f97f89c6d53a7d8a9b8134c70e15fe12d10f01ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 25 Jul 2025 12:17:16 +0200 Subject: [PATCH 04/10] Restore the `platform` field in the ios matrix, it's used by libffi downloads --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4c793483..4e3c5093 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -386,12 +386,12 @@ jobs: matrix_yaml: | include: # arm64 iOS device - - { spec: cp313-ios_arm64_iphoneos } - - { spec: cp314-ios_arm64_iphoneos } + - { spec: cp313-ios_arm64_iphoneos, platform: 'iphoneos' } + - { spec: cp314-ios_arm64_iphoneos, platform: 'iphoneos' } # arm64 iOS simulator - - { spec: cp313-ios_arm64_iphonesimulator } - - { spec: cp314-ios_arm64_iphonesimulator } + - { spec: cp313-ios_arm64_iphonesimulator, platform: 'iphonesimulator' } + - { spec: cp314-ios_arm64_iphonesimulator, platform: 'iphonesimulator' } ios: needs: [python_sdist, make_ios_matrix] From c4e3a87b7e1804d6d2bad4a421b3dbc2f78058a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 30 Jul 2025 01:46:02 +0200 Subject: [PATCH 05/10] surewhynot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- .github/workflows/ci.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7b4807e8..22f47d2f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -466,11 +466,7 @@ jobs: run: | set -eux - mkdir cffi - - tar zxf ${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz --strip-components=1 -C cffi - - python3 -m cibuildwheel --output-dir dist cffi + python3 -Im cibuildwheel --output-dir dist "${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz" echo "artifact_name=$(ls ./dist/)" >> "$GITHUB_OUTPUT" From 716f435ba44565925a8b8c01761a7ce7e502b314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 30 Jul 2025 08:23:10 +0200 Subject: [PATCH 06/10] Revert "surewhynot" This reverts commit c4e3a87b7e1804d6d2bad4a421b3dbc2f78058a8. --- .github/workflows/ci.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 22f47d2f..7b4807e8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -466,7 +466,11 @@ jobs: run: | set -eux - python3 -Im cibuildwheel --output-dir dist "${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz" + mkdir cffi + + tar zxf ${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz --strip-components=1 -C cffi + + python3 -m cibuildwheel --output-dir dist cffi echo "artifact_name=$(ls ./dist/)" >> "$GITHUB_OUTPUT" From 585ef3dea6a810f9aeac33199c9d251a28f3ca51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 20 Aug 2025 10:46:24 +0200 Subject: [PATCH 07/10] Pin to macos-14 Co-authored-by: Russell Keith-Magee --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7b4807e8..bd3ae875 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -401,7 +401,7 @@ jobs: ios: needs: [python_sdist, make_ios_matrix] - runs-on: macos-15 + runs-on: macos-14 strategy: fail-fast: false matrix: ${{ fromJSON(needs.make_ios_matrix.outputs.matrix_json) }} From a0cf74a366d5c0ea5d64d338021de1981a0ff469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 20 Aug 2025 10:49:40 +0200 Subject: [PATCH 08/10] Attempt #2 to forego tar.gz extraction --- .github/workflows/ci.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bd3ae875..fbe538f4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -466,11 +466,7 @@ jobs: run: | set -eux - mkdir cffi - - tar zxf ${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz --strip-components=1 -C cffi - - python3 -m cibuildwheel --output-dir dist cffi + python3 -m cibuildwheel --output-dir dist "${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz" echo "artifact_name=$(ls ./dist/)" >> "$GITHUB_OUTPUT" From 82638d4e3dd83027cce91647fdd573b6a65c9a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 20 Aug 2025 10:54:09 +0200 Subject: [PATCH 09/10] Be specific about what's raised in xfail --- src/c/test_c.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/c/test_c.py b/src/c/test_c.py index 40deeaa0..74ced8b9 100644 --- a/src/c/test_c.py +++ b/src/c/test_c.py @@ -1236,6 +1236,7 @@ def test_cannot_pass_struct_with_array_of_length_0(): @pytest.mark.xfail( is_ios, reason="For an unknown reason f(1, cast(BInt, 42)) returns 36792864", + raises=AssertionError, ) def test_call_function_9(): BInt = new_primitive_type("int") From 8b23c71ec475f631384ad2ee2484d21be129665c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 20 Aug 2025 11:20:05 +0200 Subject: [PATCH 10/10] Revert "Attempt #2 to forego tar.gz extraction" This reverts commit a0cf74a366d5c0ea5d64d338021de1981a0ff469. --- .github/workflows/ci.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fbe538f4..bd3ae875 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -466,7 +466,11 @@ jobs: run: | set -eux - python3 -m cibuildwheel --output-dir dist "${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz" + mkdir cffi + + tar zxf ${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz --strip-components=1 -C cffi + + python3 -m cibuildwheel --output-dir dist cffi echo "artifact_name=$(ls ./dist/)" >> "$GITHUB_OUTPUT"