Skip to content

Commit 7020b2d

Browse files
ambvwebknjazfreakboy3742
authored
Add support for building ARM64 iOS wheels on CI (#181)
* Add support for building ARM64 iOS wheels on CI * s/skip/skipif/ * Bump ios jobs to macos-15 and build-time Python 3.13 * Restore the `platform` field in the ios matrix, it's used by libffi downloads * surewhynot Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]> * Revert "surewhynot" This reverts commit c4e3a87. * Pin to macos-14 Co-authored-by: Russell Keith-Magee <[email protected]> * Attempt #2 to forego tar.gz extraction * Be specific about what's raised in xfail * Revert "Attempt #2 to forego tar.gz extraction" This reverts commit a0cf74a. --------- Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]> Co-authored-by: Russell Keith-Magee <[email protected]>
1 parent 0df96b1 commit 7020b2d

File tree

2 files changed

+137
-3
lines changed

2 files changed

+137
-3
lines changed

.github/workflows/ci.yaml

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ jobs:
112112
- { spec: cp314-manylinux_aarch64, arch: aarch64 }
113113
- { spec: cp314t-manylinux_aarch64, arch: aarch64 }
114114
115-
# aarch64 musllinux
115+
# aarch64 musllinux
116116
- { spec: cp39-musllinux_aarch64, arch: aarch64, omit: ${{ env.skip_ci_redundant_jobs }} }
117117
- { spec: cp310-musllinux_aarch64, arch: aarch64, omit: ${{ env.skip_ci_redundant_jobs }} }
118118
- { spec: cp311-musllinux_aarch64, arch: aarch64, omit: ${{ env.skip_ci_redundant_jobs }} }
@@ -379,8 +379,111 @@ jobs:
379379
if-no-files-found: error
380380
if: ${{ env.skip_artifact_upload != 'true' }}
381381

382+
make_ios_matrix:
383+
runs-on: ubuntu-24.04
384+
outputs:
385+
matrix_json: ${{ steps.make_matrix.outputs.matrix_json }}
386+
steps:
387+
- uses: actions/checkout@v4
388+
- name: make a matrix
389+
id: make_matrix
390+
uses: ./.github/actions/dynamatrix
391+
with:
392+
matrix_yaml: |
393+
include:
394+
# arm64 iOS device
395+
- { spec: cp313-ios_arm64_iphoneos, platform: 'iphoneos' }
396+
- { spec: cp314-ios_arm64_iphoneos, platform: 'iphoneos' }
397+
398+
# arm64 iOS simulator
399+
- { spec: cp313-ios_arm64_iphonesimulator, platform: 'iphonesimulator' }
400+
- { spec: cp314-ios_arm64_iphonesimulator, platform: 'iphonesimulator' }
401+
402+
ios:
403+
needs: [python_sdist, make_ios_matrix]
404+
runs-on: macos-14
405+
strategy:
406+
fail-fast: false
407+
matrix: ${{ fromJSON(needs.make_ios_matrix.outputs.matrix_json) }}
408+
409+
steps:
410+
- name: fetch sdist artifact
411+
id: fetch_sdist
412+
uses: actions/download-artifact@v4
413+
with:
414+
name: ${{ needs.python_sdist.outputs.sdist_artifact_name }}
415+
416+
- name: install python
417+
uses: actions/setup-python@v5
418+
with:
419+
python-version: '3.13'
420+
421+
- name: build wheel prereqs
422+
run: |
423+
set -eux
424+
python3 -m pip install --user --upgrade cibuildwheel>=3.1.0
425+
brew uninstall --ignore-dependencies libffi 2>&1 || true
426+
427+
- name: download libffi for iOS
428+
env:
429+
CFFI_IOS_LIBFFI_VERSION: '3.4.7-2'
430+
run: |
431+
set -eux
432+
433+
# Download prebuilt libffi from beeware/cpython-apple-source-deps
434+
platform="${{ matrix.platform }}"
435+
version="${CFFI_IOS_LIBFFI_VERSION}"
436+
url="https://github.com/beeware/cpython-apple-source-deps/releases/download/libFFI-${version}/libffi-${version}-${platform}.arm64.tar.gz"
437+
438+
echo "Downloading libffi for iOS (${platform})..."
439+
curl -L -o libffi-ios.tar.gz "${url}"
440+
441+
# Extract libffi
442+
mkdir -p libffi-ios
443+
tar zxf libffi-ios.tar.gz -C libffi-ios
444+
445+
# Set up paths for cibuildwheel
446+
echo "LIBFFI_IOS_DIR=$(pwd)/libffi-ios" >> "$GITHUB_ENV"
447+
448+
- name: build/test wheels
449+
id: build
450+
env:
451+
CIBW_BUILD: ${{ matrix.spec }}
452+
CIBW_ENABLE: cpython-prerelease
453+
CIBW_PLATFORM: ios
454+
SDKROOT: ${{ matrix.platform }}
455+
CIBW_TEST_REQUIRES: pytest setuptools
456+
CIBW_TEST_SOURCES: cffi
457+
# Running tests from `testing/` will not work since they try to compile C code on device
458+
CIBW_TEST_COMMAND: python -m pytest -sv cffi/src/c/
459+
# Environment variables for the build
460+
CIBW_ENVIRONMENT: >
461+
CFLAGS="-I${LIBFFI_IOS_DIR}/include"
462+
LDFLAGS="-L${LIBFFI_IOS_DIR}/lib"
463+
PYTHONUNBUFFERED=1
464+
# Pass through our custom env vars
465+
CIBW_ENVIRONMENT_PASS_IOS: LIBFFI_IOS_DIR PYTHONUNBUFFERED
466+
run: |
467+
set -eux
468+
469+
mkdir cffi
470+
471+
tar zxf ${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz --strip-components=1 -C cffi
472+
473+
python3 -m cibuildwheel --output-dir dist cffi
474+
475+
echo "artifact_name=$(ls ./dist/)" >> "$GITHUB_OUTPUT"
476+
477+
- name: upload artifacts
478+
uses: actions/upload-artifact@v4
479+
with:
480+
name: ${{ steps.build.outputs.artifact_name }}
481+
path: dist/*.whl
482+
if-no-files-found: error
483+
if: ${{ env.skip_artifact_upload != 'true' }}
484+
382485
merge_artifacts:
383-
needs: [python_sdist, linux, macos, windows]
486+
needs: [python_sdist, linux, macos, windows, ios]
384487
runs-on: ubuntu-24.04
385488
steps:
386489
- name: merge all artifacts
@@ -451,7 +554,7 @@ jobs:
451554
452555
check:
453556
if: always()
454-
needs: [python_sdist, linux, macos, windows, clang_TSAN, pytest-run-parallel, merge_artifacts]
557+
needs: [python_sdist, linux, macos, windows, ios, clang_TSAN, pytest-run-parallel, merge_artifacts]
455558
runs-on: ubuntu-24.04
456559
steps:
457560
- name: Verify all previous jobs succeeded (provides a single check to sample for gating purposes)

src/c/test_c.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
except ImportError:
1818
pass
1919

20+
is_ios = sys.platform == 'ios'
21+
22+
2023
def _setup_path():
2124
import os, sys
2225
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
@@ -1230,6 +1233,11 @@ def test_cannot_pass_struct_with_array_of_length_0():
12301233
BFunc2 = new_function_type((BInt,), BStruct, False)
12311234
pytest.raises(NotImplementedError, cast(BFunc2, 123), 123)
12321235

1236+
@pytest.mark.xfail(
1237+
is_ios,
1238+
reason="For an unknown reason f(1, cast(BInt, 42)) returns 36792864",
1239+
raises=AssertionError,
1240+
)
12331241
def test_call_function_9():
12341242
BInt = new_primitive_type("int")
12351243
BFunc9 = new_function_type((BInt,), BInt, True) # vararg
@@ -1362,6 +1370,7 @@ def test_write_variable():
13621370
pytest.raises(ValueError, ll.write_variable, BVoidP, "stderr", stderr)
13631371

13641372

1373+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
13651374
def test_callback():
13661375
BInt = new_primitive_type("int")
13671376
def make_callback():
@@ -1378,6 +1387,7 @@ def cb(n):
13781387
assert str(e.value) == "'int(*)(int)' expects 1 arguments, got 0"
13791388

13801389

1390+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
13811391
@pytest.mark.thread_unsafe("mocks sys.unraiseablehook")
13821392
def test_callback_exception():
13831393
def check_value(x):
@@ -1435,6 +1445,7 @@ def oops(*args):
14351445
assert ff(bigvalue) == -42
14361446

14371447

1448+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
14381449
def test_callback_return_type():
14391450
for rettype in ["signed char", "short", "int", "long", "long long",
14401451
"unsigned char", "unsigned short", "unsigned int",
@@ -1455,6 +1466,7 @@ def cb(n):
14551466
assert f(max - 1) == max
14561467
assert f(max) == 42
14571468

1469+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
14581470
def test_a_lot_of_callbacks():
14591471
BIGNUM = 10000
14601472
if 'PY_DOT_PY' in globals(): BIGNUM = 100 # tests on py.py
@@ -1470,6 +1482,7 @@ def cb(n):
14701482
for i, f in enumerate(flist):
14711483
assert f(-142) == -142 + i
14721484

1485+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
14731486
def test_callback_receiving_tiny_struct():
14741487
BSChar = new_primitive_type("signed char")
14751488
BInt = new_primitive_type("int")
@@ -1485,6 +1498,7 @@ def cb(s):
14851498
n = f(p[0])
14861499
assert n == -42
14871500

1501+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
14881502
def test_callback_returning_tiny_struct():
14891503
BSChar = new_primitive_type("signed char")
14901504
BInt = new_primitive_type("int")
@@ -1502,6 +1516,7 @@ def cb(n):
15021516
assert s.a == -10
15031517
assert s.b == -30
15041518

1519+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
15051520
def test_callback_receiving_struct():
15061521
BSChar = new_primitive_type("signed char")
15071522
BInt = new_primitive_type("int")
@@ -1518,6 +1533,7 @@ def cb(s):
15181533
n = f(p[0])
15191534
assert n == 42
15201535

1536+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
15211537
def test_callback_returning_struct():
15221538
BSChar = new_primitive_type("signed char")
15231539
BInt = new_primitive_type("int")
@@ -1537,6 +1553,7 @@ def cb(n):
15371553
assert s.a == -10
15381554
assert s.b == 1E-42
15391555

1556+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
15401557
def test_callback_receiving_big_struct():
15411558
BInt = new_primitive_type("int")
15421559
BStruct = new_struct_type("struct foo")
@@ -1561,6 +1578,7 @@ def cb(s):
15611578
n = f(p[0])
15621579
assert n == 42
15631580

1581+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
15641582
def test_callback_returning_big_struct():
15651583
BInt = new_primitive_type("int")
15661584
BStruct = new_struct_type("struct foo")
@@ -1586,6 +1604,7 @@ def cb():
15861604
for i, name in enumerate("abcdefghij"):
15871605
assert getattr(s, name) == 13 - i
15881606

1607+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
15891608
def test_callback_returning_void():
15901609
BVoid = new_void_type()
15911610
BFunc = new_function_type((), BVoid, False)
@@ -1694,6 +1713,7 @@ def test_enum_overflow():
16941713
pytest.raises(OverflowError, new_enum_type,
16951714
"foo", ("AA",), (testcase,), BPrimitive)
16961715

1716+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
16971717
def test_callback_returning_enum():
16981718
BInt = new_primitive_type("int")
16991719
BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, -20), BInt)
@@ -1710,6 +1730,7 @@ def cb(n):
17101730
assert f(20) == 20
17111731
assert f(21) == 21
17121732

1733+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
17131734
def test_callback_returning_enum_unsigned():
17141735
BInt = new_primitive_type("int")
17151736
BUInt = new_primitive_type("unsigned int")
@@ -1727,6 +1748,7 @@ def cb(n):
17271748
assert f(20) == 20
17281749
assert f(21) == 21
17291750

1751+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
17301752
def test_callback_returning_char():
17311753
BInt = new_primitive_type("int")
17321754
BChar = new_primitive_type("char")
@@ -1741,6 +1763,7 @@ def _hacked_pypy_uni4():
17411763
pyuni4 = {1: True, 2: False}[len(u+'\U00012345')]
17421764
return 'PY_DOT_PY' in globals() and not pyuni4
17431765

1766+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
17441767
def test_callback_returning_wchar_t():
17451768
BInt = new_primitive_type("int")
17461769
BWChar = new_primitive_type("wchar_t")
@@ -2310,6 +2333,8 @@ def _test_wchar_variant(typename):
23102333
assert str(q) == repr(q)
23112334
pytest.raises(RuntimeError, string, q)
23122335
#
2336+
if is_ios:
2337+
return # cannot allocate executable memory for the callback() below
23132338
def cb(p):
23142339
assert repr(p).startswith("<cdata '%s *' 0x" % typename)
23152340
return len(string(p))
@@ -2546,6 +2571,7 @@ def test_errno():
25462571
f(); f()
25472572
assert get_errno() == 95
25482573

2574+
@pytest.mark.skipif(is_ios, reason="Cannot allocate executable memory on iOS")
25492575
def test_errno_callback():
25502576
if globals().get('PY_DOT_PY'):
25512577
pytest.skip("cannot run this test on py.py (e.g. fails on Windows)")
@@ -2996,6 +3022,11 @@ def test_string_assignment_to_byte_array():
29963022
except ImportError:
29973023
pass # win32
29983024

3025+
@pytest.mark.skipif(
3026+
is_ios,
3027+
reason="For an unknown reason fscanf() doesn't read anything on 3.14"
3028+
" and crashes on 3.13 (that's why it's not an xfail)",
3029+
)
29993030
def test_FILE():
30003031
if sys.platform == "win32":
30013032
pytest.skip("testing FILE not implemented")

0 commit comments

Comments
 (0)