🎡 cibuildwheel 3.0

cibuildwheel 3.0.0 is out, with some very big additions. We’ve added GraalPy, Python 3.14 (and 3.14t) betas, and iOS support! We’ve got several new options: test-sources, test-environment, and (experimental) pyodide-version. We now fully use enable (and PyPy requires using it), and we no longer inject setuptools and wheel in build environments. Defaults have changed, too: build is now the default frontend, manylinux_2_28 is the default manylinux image, with 32-bit linux now being opt-in. We’ve removed support for Python 3.6 and 3.7, we now require 3.11+ to run cibuildwheel itself, and EoL manylinux/musllinux images now need to be fully specified.

We’ve had some fantastic releases of cibuildwheel since my last post over 2.19, so I’ll include a few of the new features from those releases, too. I’ll also note a few of the features being worked on for future releases.

iOS wheels (3.0)

We now support building iOS wheels! Building wheels for iOS is a bit more complex than standard wheels, so there have been a few new options added to support this.

test-sources

First, you can’t access files on the host system from iOS. So we’ve introduced a new setting, test-sources. If you specify test-sources, then anything you list gets copied to the testing directory. For example, say you had this:

[tool.cibuildwheel]
test-command = "pytest {project}/tests"

Now, you can replace it with:

[tool.cibuildwheel]
test-command = "python -m pytest tests"
test-sources = ["tests", "pyproject.toml"]

Notice you no longer need placeholders like {project}! If you have test configuration, remember to copy those files in too (like pyproject.toml). If you have no test files outside your package, you can set this to [] to silence the iOS warning.

This is optional for other platforms, but it is required for iOS. Only items inside your package or in test-sources are visible to the tests.

xbuild-tools

The second issue is that this is a cross-compile; you need to tell cibuildwheel what tools are safe to use from the host. You can do that with the new xbuild-tools setting:

[tool.cibuildwheel.ios]
xbuild-tools = ["cmake", "ninja"]

It is probably a good idea to scope this to ios for now; we might eventually find a use for it for other platforms. If you are using CMake, you need 4.0 or newer; use brew upgrade cmake before running cibuildwheel on GitHub Actions runners until they update.

test-command

The test-command setting is special in iOS, as you aren’t talking to a shell. Every test-command entry must start with python -m (we will add this for you if the command is pytest, but print a warning).

Architectures

iOS has three architectures. They are:

  • arm64_iphoneos: for devices (can’t be tested)
  • arm_iphonesimulator: for iOS simulators on Apple Silicon (can be tested on AS)
  • x64_64_iphonesimulator: for iOS simulators on Intel machines (can be tested on Intel)

You can build all architectures by setting the architectures to all; only the native simulator architecture can be tested. auto (the default) builds the native device and simulator wheels on ARM, and just the simulator on Intel. You should always distribute at least the device and ARM simulator wheel, as app developers need both to test and ship. For now, the Intel simulator wheel is nice too.

Wheels for NumPy

If you need a binary such as NumPy, currently there aren’t wheels uploaded to PyPI (this should change eventually). For now, the "https://pypi.anaconda.org/beeware/simple" index should be added to get wheels; remember to set PIP_ONLY_BINARY to numpy or :all:, and PIP_PREFER_BINARY can be set as well. By the way, you currently need to use the build backend (which uses pip to install); uv doesn’t support iOS yet.

If you need to limit dependencies for iOS, you can do it by checking sys_platform for ios, such as:

[dependency-groups]
test = [
  "pytest",
  "pytest-xdist; sys_platform != 'ios'",
]

(iOS doesn’t have processes, so no pytest-xdist.)

Full example

Configuration:

[tool.cibuildwheel]
test-sources = ["tests", "pyproject.toml"]
test-command = "python -m pytest tests"
environment.PIP_ONLY_BINARY = "numpy"
environment.PIP_PREFER_BINARY = "1"
ios.test-groups = ["test"]
ios.xbuild-tools = ["cmake", "ninja"]
ios.environment.PIP_EXTRA_INDEX_URL = "https://pypi.anaconda.org/beeware/simple"

GitHub Actions:

jobs:
  build-ios:
    name: iOS wheel
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - run: brew upgrade cmake
      - uses: pypa/cibuildwheel@v3.0
        env:
          CIBW_PLATFORM: ios
          CIBW_ARCHS: all

Android support is coming in cibuildwheel 3.1. Android will have two architectures and can be built from linux or macOS, though there are interesting tradeoffs on each.

Enables: GraalPy, Python 3.14, free-threading, PyPy (3.0 and 2.22)

We introduced a new enable system in cibuildwheel 2.22, and it’s now fully replacing the specialized CIBW_PRERELEASE_PYTHONS and CIBW_FREE_THREADED_SUPPORT variables with a unified system. As a result, you can now specify pre-release Pythons in your pyproject.toml if you want to; but we’ve added special handling for this to make it easy to use both. If you specify:

[tool.cibuildwheel]
enable = ["cpython-freethreading", "pypy"]

in your pyproject.toml, then build cibuildwheel with CIBW_ENABLE=cpython-prerelease, all of those will be enabled; the environment variable will only extend the static configuration. If you need to avoid certain wheels dynamically, use CIBW_SKIP or adjust CIBW_BUILD; the enables simply indicate you are aware of these special cases and are willing to work with them in your selectors.

Selector: pypy and pypy-eol

These new selectors mean you now opt-in to PyPy. They are:

  • pypy: PyPy 3.10 and PyPy 3.11
  • pypy-eol: PyPy 3.8 and PyPy 3.9

Most packages that support PyPy have to make at least a little effort to do so, so this makes it a little easier to build with cibuildwheel out-of-the box. Most importantly, the pypy-eol category means it’s much easier to avoid building for versions of PyPy that no longer are supported and have unfixed bugs (looking at you, PyPy 3.8). Most packages that support PyPy do not need to build for end-of-life versions. The requires-python setting is not PyPy aware, so this should really help packages that want to build for supported versions.

Selector: graalpy

We support a new interpreter: GraalPy, which is built on GraalVM. If you are using a binding tool, you need to make sure that tool supports GraalPy; a pybind11 3.0 release candidate is required, for example. There are several caveats:

  • GraalPy 24.2 on Windows is buggy, you might need to skip tests or skip it entirely. I get a pytest missing module crash.
  • GraalPy identifiers use py<version>_<graalpy_version>, which is our first identifier like that, though it could be used on others in the future if there are ever more than one ABI per Python version.
  • There are some GraalPy wheels for NumPy in "https://www.graalvm.org/python/wheels/". For some reason, there is no “simple” suffix on this index. Some combinations are missing, though, like ARM manylinux and Intel macOS.
  • UV does support GraalPy, and is much faster than pip running on GraalPy.

Here’s an example configuration (using uv, so remember to install uv first):

[tool.cibuildwheel]
build-frontend = "build[uv]"
test-command = "pytest tests"
test-skip = [
  "gp311_242-macosx_x86_64",
  "gp311_242-manylinux_aarch64",
  "gp311_242-win*",
]
environment.UV_ONLY_BINARY = "numpy"
environment.UV_PREFER_BINARY = "1"

[[tool.cibuildwheel.overrides]]
select = ["gp*"]
inherit.environment = "append"
environment.UV_INDEX = "https://www.graalvm.org/python/wheels/"
environment.UV_INDEX_STRATEGY = "unsafe-best-match"

Note: if you want to test on GraalPy on GitHub Actions without cibuildwheel, in your normal test workflow, setup-python support GraalPy except on Windows.

Other Selectors

The other selectors are:

  • cpython-prerelease: activates Python 3.14. Reminder that it will not be ABI stable until RC 1.
  • cpython-freethreading: activates Python 3.13t (and 3.14t if combined with cpython-prerelease
  • cpython-experimental-riscv64: A very experimental build that can’t be uploaded to PyPI yet and requires custom images.
  • pyodide-prerelease: Enables the unreleased 0.28 Pyodide’s Python 3.13. These wheels may not work with the final release of Pyodide 0.28.
  • all: enables all selectors.

As with the old options or the --platform/CIBW_PLATFORM setting, you don’t need bother with them if you are using --only to target a specific selector.

Platform default changes and removals (3.0)

Time marches on, and the announced changes to defaults have happened on schedule. manylinux2014 is no longer the default, with manylinux_2_28 taking its place. We’ve also removed the old shortcuts for EoL images manylinux1, manylinux2010, manylinux_2_24, and musllinux_1_1; if you need to use these, you must specify the full image URL (and be warned, some of the older tags have been deleted to save space on quay.io; the most recent tags are still available). The new images don’t have 32-bit versions, so we’ve removed 32-bit linux from "auto" (the default); use archs = ["auto64", "auto32"] to get the full set back. We might do the same thing to 32-bit Windows in the future, too.

We’ve also dropped Python 3.6 and Python 3.7 support; these have also been removed from the manylinux images following the timeline announced a year ago. You’ll have to use a cibuildwheel 2.x to build for these EoL Python (reminder that even 3.8 is EoL!).

We now use build by default instead of pip, which was noted for several years. pip (and build[uv]) remain available, and we are working on a native uv build-frontend (should be workspace-aware) in the future.

Setuptools and wheel are no longer installed in environments by default, matching later Pythons. Use pyproject.toml or specify build dependencies manually.

We’ve also dropped support for Appveyor, as they weren’t able to work with us to run our CI. It might work there, but we can’t be sure anymore.

Pyodide

Pyodide has seen significant updates; we no longer require the host Python (the one you run cibuildwheel with) to be Python 3.12; instead we use python-build-standalone to download an appropriate version. This was required for our new experimental support for Pyodide’s 3.13 wheels; you can’t run a program from 3.12 and 3.13 simultaneously! We also support selecting a specific version of pyodide with pyodide-version (you need the enable option to select one that has a newer Python identifier, though). This is experimental while we work on a more general system to customize (and maybe add!) selectors. And, of course, there’s the aforementioned selector for beta Pyodide (Python 3.13).

Other 3.0

We now require the host Python (the one you use to run cibuildwheel) to be Python 3.11 or newer. All CI platforms have this version or greater available, and it dramatically simplifies our maintenance (and testing) to not have to maintain old versions of host Python. As a side effect, we have the extra leeway now to test on pre-release 3.14 as host Python too. If you use 3.14, try running cibuildwheel --help and enjoy the new colorful output! You can see this in our docs, too.

We’ve dramatically reworked our docs. Now we have many new and more focused pages, The TOML configuration comes first, with badges showing all the possible ways to set this, also including command line options. And now we even build our docs with Python 3.14 to get color output for the help display.

We also have one more new option, test-environment, which allows you to specify test-only environment variables.

You can also specify dependency-versions inline now.

The Scientific Python nightly wheels (any version)

One sort-of related change is the development of SPEC 4, which creates a nightly wheel repository for scientific Python packages. Many core packages, like NumPy, are publishing wheels there. Besides getting the latest development copies of packages, this is also a fantastic way to get access to new platforms that haven’t made it into a full release yet. During PyCon 2025, a few days after CPython 3.14 beta 1 was released, we released cibuildwheel 3.0 beta 1 with Python 3.14 support, and worked with NumPy to get 3.14 wheels into the nightlies. This is the earliest we’ve ever been able to build against NumPy on a pre-release Python!

To use them, you need something like this:

[tool.cibuildwheel]
environment.PIP_ONLY_BINARY = "numpy"
environment.PIP_PREFER_BINARY = "1"

[[tool.cibuildwheel.overrides]]
select = ["cp314*"]
inherit.environment = "append"
environment.PIP_EXTRA_INDEX_URL = "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/"
environment.PIP_PRERELEASE = "allow"
[tool.cibuildwheel]
environment.UV_ONLY_BINARY = "numpy"
environment.UV_PREFER_BINARY = "1"

[[tool.cibuildwheel.overrides]]
select = ["cp314*"]
inherit.environment = "append"
environment.UV_INDEX = "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/"
environment.UV_INDEX_STRATEGY = "unsafe-best-match"
environment.UV_PRERELEASE = "allow"

With uv, you can also configure this, but I believe it currently only affects the high-level interface:

[tool.uv.sources]
numpy = { index = "scientific_python", marker = "python_version >='3.14'"}

[[tool.uv.index]]
name = "scientific_python"
url = "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple"

Dependency Groups (2.22)

Following the acceptance of dependency-groups PEP 735, we added test-groups, which can be used like this:

[dependency-groups]
test = ["pytest"]
dev = [{ include-group = "test" }]

[tool.cibuildwheel]
test-command = "pytest {project}/tests"
test-groups = ["test"]

This is preferable to using extras (test-extras), because it avoids exposing test dependencies as public package metadata, has native nesting support, and if you specify a dev group, you can get out-of-the-box uv run support, too! (Note: currently, we use the dependency-groups package to parse these, though pip and uv now natively support a --group option).

Inheriting overrides improvement (2.21)

The inherit feature, featured in my last post, got a fix in cibuildwheel 2.21 to work much better with config-settings, with keys overriding old keys, rather than extending them as a list (?!).

Upcoming features

Android support is being worked on! It requires changes to CPython’s android.py built-in script too, so missed the 3.0 deadline, but should make it into cibuildwheel 3.1.

We expect to support uv natively as a frontend (in addition to build[uv]). This should bring support for uv’s workspaces, in theory.

We might be able to provide higher quality pass-through of config-settings to build in the future, based on a proposed addition to build.

Update now!

- uses: pypa/cibuildwheel@v3.0

If you use pybind11, you’ll need a pybind11 3.0 release candidate to support Python 3.14 or GraalPy. iOS might work with 2.x, but the 3.0 RCs are tested against iOS too. For any binding library including pybind11 and mypyc, there’s a bug that will be fixed in the next Python 3.14 beta (beta 3) with setting __dict__, but it’s pretty rare to run into it in practice (except maybe in pickling).

The latest versions of scikit-build-core support all these, as well.

Here’s a quick checklist to follow when updating to 3.0:

  • Make sure you run cibuildwheel with 3.11+, or you won’t get the upgrade.
  • Do you need PyPy? Add "pypy" to your enables. If you need EoL PyPy (3.8, 3.9), also include "pypy-eol".
  • Do you need 32-bit linux? Add auto32 to your archs.
  • Replace free-threaded-support = true with enable = ["cpython-freethreading"] (or CIBW_FREETHREADED_SUPPORT: 1 with CIBW_ENABLE: cpython-freethreading).
  • Replace CIBW_PRERELEASE_PYTHONS: 1 with CIBW_ENABLE: cpython-prerelease or enable = ["cpython-prerelease"].
  • Verify you are happy with manylinux_2_28, or set the image explicitly. Old versions (2010 or earlier, musllinux_1_1) must be fully specified images.

If you still need CPython 3.7 or 3.6, you’ll need to have a cibuildwheel 2.x job for those.

Once you’ve upgraded, consider adding iOS, GraalPy, or 3.14 beta wheels!