Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for building iOS wheels. #2286

Open
wants to merge 33 commits into
base: main
Choose a base branch
from

Conversation

freakboy3742
Copy link
Contributor

@freakboy3742 freakboy3742 commented Feb 24, 2025

Adds support for building PEP 730 iOS wheels.

Refs #1960.

tl;dr - for simple packages, CIBW_PLATFORM=ios cibuildwheel will produce 3 iOS wheels suitable for use on:

  • ARM64 devices
  • ARM64 simulators on macOS
  • x86_64 simulators on macOS

As proof, this branch has been use to publish iOS wheels for pyspamsum. These wheels were produced using this Github Actions CI configuration.

A branch adding iOS support on Pillow also exists; this hasn't been submitted for inclusion in Pillow because of the dependency on this (as yet unmerged, unreleased) PR. However, it's a demonstration of a non-trivial Python package building iOS wheels.

The details:

Edited 6 Mar 2025, to reflect most recent changes in CIBW_PLATFORM and CIBW_ARCHS.

CIBW_PLATFORM values

iOS is effectively 2 platforms - physical devices, and the simulator. While the API for these two platforms are identical, the ABI is not compatible, even when dealing with a device and simulator with the same CPU architecture. For this reason, iOS support a CIBW_PLATFORM value of ios, and 3 CIBW_ARCHS values - arm64_iphoneos (for ARM64 devices), arm64_iphonesimulator (for simulators running on ARM64 Macs) and x86_64_iphonesimulator (for simulators running on Intel simulators). This also allows for configurations to be specified at the ios level, and specialised at the iphoneos or iphonesimulator level using a select clause in a settings override.

Binary distributions

The binaries used to support iOS builds come from the BeeWare Python Apple Support project. It is hoped that in the 3.14 release timeframe, these artefacts will be produced by Python.org itself - this is an topic I'm currently working on. In the meantime, the use of 'unofficial' support packages puts iOS in a similar boat as the Emscripten backend using Pyodide build artefacts.

Cross platform builds

iOS builds are always cross platform builds, as it not possible to run compilers and other build tools "on device". The BeeWare iOS support package includes tooling that can convert any virtual environment into a cross platform virtual environment - that is, an environment that can run binaries on the build machine (macOS), but, if asked, will respond as if it is an iOS machine. This allows pip, build, and other build tools to perform iOS-appropriate behaviour. As a result, an iOS build environment also means downloading and installing CIBW's macOS tools.

Build frontends

iOS builds support both the pip and build build frontends. In principle, support for build[uv] should be possible, but uv doesn't currently have support for cross-platform builds, and doesn't have support for iOS (or Android) tags.

The build environment

The environment used to run builds does not inherit the full user environment - in particular, PATH is deliberately re-written. This is because UNIX C tooling doesn't do a great job differentiating between "macOS ARM64" and "iOS ARM64" binaries. If (for example) Homebrew is on the path when compilation commands are invoked, it's easy for a macOS version of a library to be linked into the iOS binary, rendering it useless. To prevent this, iOS builds always force PATH to a "known minimal" path, that includes only the bare system utilities, plus cargo (to facilitate Rust builds).

iOS test suites

Running iOS test suites also requires special attention. The iOS test environment can't support running shell scripts, so the entry point must be specified as a completion to python -m .... In addition, the test itself must be run "on device", so the local project directory cannot be used to run tests. As a result, the project must specify test_sources as a minimum subset of files that should be copied to the test environment.

The test process uses the same testbed used by CPython itself to run the CPython test suite. It is an Xcode project that has been configured to have a single Xcode "XCUnit" test - the result of which is whatever the output of python -m <CIBW_TEST_COMMAND> is. This testbed is also included in the BeeWare support package.

@freakboy3742 freakboy3742 marked this pull request as draft February 24, 2025 06:43
@freakboy3742
Copy link
Contributor Author

I've clearly got some CI edge cases to clean up - moving to draft status.

Copy link
Contributor

@joerick joerick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, nice work with this! 🤩

(after just a glance over the changes...)

The text you have in the PR description - we should add that to the docs somewhere. Perhaps a new page about other platforms... that could include some info about Pyodide too.

@timrid
Copy link

timrid commented Feb 24, 2025

First of all, nice work what you are doing with the python support for mobile platforms. I am following most of your steps towards this goal :)

I am currently trying to get cibuildwheel running with pybind11 and cmake using this example project and have the following problem:

The project has cmake as a build dependency. Under macOS there is also a ready-made binary wheel for CMake cmake-3.31.4-py3-none-macosx_10_10_universal2.whl, which is used when building the wheel for macos. However, when I build the iOS wheel, cmake-3.31.4.tar.gz is downloaded and an attempt is made to build cmake from source, which ends in an error (see here).

Since a cross platform build is made for iOS, I would assume that cmake-3.31.4-py3-none-macosx_10_10_universal2.whl should be used for ios instead of the .tar.gz. Is this possibly a bug in make_cross_venv.py?

@freakboy3742
Copy link
Contributor Author

Since a cross platform build is made for iOS, I would assume that cmake-3.31.4-py3-none-macosx_10_10_universal2.whl should be used for ios instead of the .tar.gz. Is this possibly a bug in make_cross_venv.py?

Hrm - that's an interesting one. It's not a clear bug in make_cross_venv to my mind... I'd argue its almost a "missing feature".

The problem is that cmake is in a weird situation as a dependency. Cython would be in a similar situation. On the one hand, it needs to run as part of the build - so we need to install the macOS wheel, so a macOS binary exists. The virtual environment will advertise itself as a sys.platform == "ios" environment, so it will be looking for iOS-compatible wheels. So, when it doesn't find an iOS-compatible binary wheel, it tries to compile one... and fails.

However, an iOS-compatible cmake wheel is a bit of a contradiction... because you won't be able to run cmake on iOS. So it would appear as an iOS-compatible wheel that would break things if you actually added it to an app. If you put a macOS binary into an iOS wheel, it would obviously break.

The alternative approach would be to consider build dependencies to be "non cross", and Install build dependencies using the build platform's wheel tag. That means installing the macOS wheels for cmake, so that cmake can run in the build environment. I'm not 100% sure if the PEP 517 interface is literally the boundary that we need to care about here, or if we need an additional setting or control mechanism to allow the installation of build dependencies independent of package/testing dependencies... some more investigation may be required.

@freakboy3742
Copy link
Contributor Author

The text you have in the PR description - we should add that to the docs somewhere. Perhaps a new page about other platforms... that could include some info about Pyodide too.

That's a good call - I'll add in a section to the docs to that effect.

@henryiii
Copy link
Contributor

henryiii commented Feb 24, 2025

I am currently trying to get cibuildwheel running with pybind11 and cmake using this example project

That example is old, you should be using scikit-build-core and https://github.com/pybind/scikit_build_example as the base.

The project has cmake as a build dependency.

If you already have cmake, then scikit-build-core won't request the cmake wheel, so I think this issue would be solved. I haven't adding any testing for iOS yet, though.

The alternative approach would be to consider build dependencies to be "non cross", and Install build dependencies using the build platform's wheel tag

This is tricky; a good counter example is numpy. If you want to run numpy.get_include, you would want the host wheel. If you want to link to numpy, you'd want the target wheel. I'm not sure this is properly possible unless PEP 517 were expanded to include cross-compilation. Maybe we need some sort of per-package override?

@freakboy3742
Copy link
Contributor Author

I am currently trying to get cibuildwheel running with pybind11 and cmake using this example project

That example is old, you should be using scikit-build-core and https://github.com/pybind/scikit_build_example as the base.

That may be true for that specific example, but there are other projects (PyTorch is one notable example) that have a dependency on cmake, cython, or similar "binary tools that must be runnable in the build environment" requirement.

However, I'll make a note to add https://github.com/pybind/scikit_build_example as a test case for iOS/Android build tooling.

The alternative approach would be to consider build dependencies to be "non cross", and Install build dependencies using the build platform's wheel tag

This is tricky; a good counter example is numpy. If you want to run numpy.get_include, you would want the host wheel. If you want to link to numpy, you'd want the target wheel. I'm not sure this is properly possible unless PEP 517 were expanded to include cross-compilation. Maybe we need some sort of per-package override?

Pandas looks like a good test case here - it needs both cython (which needs to be a macOS version) and numpy (which needs to be the iOS version).

However, I agree that this is fundamentally a gap in PEP 517 itself, and we need an extension of that PEP to cover the cross-compilation case. My off-the-cuff proposal would be to have build-system.requires be "host native" wheels, but a new "build-requires" setting defines wheels that must be "build native". For standard macOS/Linux/Windows builds, the two are the same; but for iOS/Android/other cross-platform builds, the difference would exist. I need to do some more experimentation here.

This would also give an opportunity to formalize what a "cross platform venv" actually means, which what the make_cross_venv script is doing, and pyodide is doing with its own tooling. I was planning to kick off a discuss.python.org thread about cross-venv stuff in the near future, with the intention of that turning into a PEP; adding the PEP517 interface issues to that PEP seems like it might be appropriate.

I guess the bigger question here is whether that discussion needs to precede a PR like this one being merged - i.e., is "can compile numpy and pandas" (which are projects with complex build systems) a prerequisite for adding any iOS/Android support to cibuildwheel. This would admittedly be a big gap, but I question whether "the perfect is the enemy of the good" in this instance.

@freakboy3742
Copy link
Contributor Author

@joerick I've restored most of the old examples of GitHub actions configuration, only including the "full" example for the last "deployment" case. I've also split out the platform details into standalone pages. That required a bit of surgery to the MKDocs configuration; I'm not sure if I've landed on the best layout for that new content, but the new content is there - I'm open to suggestions on other ways to structure that content.

I've fixed the issue with Linux and Windows CI - it required changing the "test" that is run to do something other than invoking system(), because system() isn't available on iOS.

The tests are still failing macOS CI - that appears to be due to this bug in the testbed, which I've got a fix in flight. As soon as that PR is merged, I can publish updated support packages that will include the patch.

@mhsmith
Copy link

mhsmith commented Feb 25, 2025

If you want to run numpy.get_include, you would want the host wheel. If you want to link to numpy, you'd want the target wheel. I'm not sure this is properly possible unless PEP 517 were expanded to include cross-compilation.

NumPy itself may also need to change to support this, because even if we did install the iOS wheel, we wouldn't be able to import it to call get_include.

In our Android recipes we worked around this by patching everything that used NumPy to set __NUMPY_SETUP__. But I see this doesn't appear in the iOS recipes, so maybe it's not necessary anymore.

@freakboy3742
Copy link
Contributor Author

freakboy3742 commented Feb 25, 2025

But I see this doesn't appear in the iOS recipes, so maybe it's not necessary anymore.

IIRC, this cleanup was possible because of the switch to Meson as a build system. The iOS patch is still on the 1.X branch, not the 2.X branch, but building with Meson was (unsurprisingly) a lot cleaner than the older setuptools build.

However - even then, I'm not sure it would be a problem with the way this PR is structured. The cross-env handling means that you can invoke Python code - and it runs as if sys.platform == "ios". As a result, having a setup.py that references platform-specific behavior isn't a problem - in fact, it's desirable. The presumptive patch for Pillow that I have developed does this.

EDIT: ... as long as the code being executed is pure Python. I'm not sure if this is the case with numpy.get_include()

@@ -42,12 +42,15 @@
sys.executable,
"-m",
"pytest",
"--dist",
"loadgroup",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows the build to run all the iOS tests on the same machine; see the iOS tests for the reason.

f"--numprocesses={args.num_processes}",
"-x",
"--durations",
"0",
"--timeout=2400",
"test",
"-vv",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generates a lot more output - but when a test fails, it helps to have as much context as possible. It was difficult to diagnose some of the iOS failures with the low verbosity output.

@freakboy3742 freakboy3742 marked this pull request as ready for review February 26, 2025 01:39
@freakboy3742
Copy link
Contributor Author

This is now passing CI, and the documentation issues referenced in the earlier pre-review have been addressed, so I think this is ready for a full review.

@freakboy3742 freakboy3742 requested a review from joerick February 26, 2025 01:41
@mhsmith
Copy link

mhsmith commented Feb 26, 2025

as long as the code being executed is pure Python. I'm not sure if this is the case with numpy.get_include()

get_include itself is pure-Python, but at least in the older versions I've built for Android, import numpy would immediately try to import its binary modules. If this has changed with Meson, then that's great news.

@henryiii
Copy link
Contributor

henryiii commented Feb 27, 2025

I'm curious, would it make sense to use the architecture instead of the platform for simulator? So basically split native and simulator architectures? Then "auto" could build all of them? I feel like this would behave more like the other platforms, and still would provide a way to split things up if you needed too. Windows and UNIX architecture names differ already, so it would have precedent.

@freakboy3742 freakboy3742 requested a review from mhsmith March 7, 2025 04:45
@freakboy3742 freakboy3742 requested a review from mayeut March 9, 2025 02:39
Comment on lines +58 to +66
| | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM | iOS |
|-----------------|-------|-------|---------|-----------|-----------|-------------|-----|
| GitHub Actions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅³ |
| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² | |
| Travis CI | ✅ | | ✅ | ✅ | | | |
| AppVeyor | ✅ | ✅ | ✅ | | ✅ | ✅² | |
| CircleCI | ✅ | ✅ | | ✅ | ✅ | | |
| Gitlab CI | ✅ | ✅ | ✅ | ✅¹ | ✅ | | |
| Cirrus CI | ✅ | ✅ | ✅ | ✅ | ✅ | | |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only refers to GitHub Actions supporting building wheels for iOS.
There's probably little reason for other providers not to support this ?
It's probably already at least partially tested i this PR.

Comment on lines -455 to -456

If not listed above, `auto` is the same as `native`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be kept below the table ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the purpose of this sentence is, because it actually is listed above for all supported platforms.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linux s390x/ppc64le/... for example are not listed in the table.

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.

6 participants