# How to Build Multi-Platform Wheels with cibuildwheel


Python packages with compiled extensions (C, C++, Rust, Cython) need platform-specific [wheels](https://pydevtools.com/handbook/reference/wheel.md) so users can install them without a compiler. [cibuildwheel](https://pydevtools.com/handbook/reference/cibuildwheel.md) automates building these wheels across operating systems, architectures, and Python versions in CI.

This guide sets up a GitHub Actions workflow that builds wheels for Linux, macOS, and Windows, then publishes them to [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md).

## Prerequisites

- A Python project with a compiled extension and a [PEP 517](https://pydevtools.com/handbook/explanation/what-is-pep-517.md)-compliant [build backend](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md) (setuptools, scikit-build-core, maturin, etc.)
- A `[build-system]` table in [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md)
- A GitHub repository
- A PyPI account with [trusted publishing](https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing.md) configured for the repository

## Configure cibuildwheel

Add a `[tool.cibuildwheel]` section to `pyproject.toml`. This example targets CPython 3.11 through 3.13, skips 32-bit builds, and runs pytest after each wheel build:

```toml {filename="pyproject.toml"}
[tool.cibuildwheel]
build = ["cp311-*", "cp312-*", "cp313-*"]
skip = ["*-win32", "*-manylinux_i686"]
test-command = "pytest {project}/tests"
test-requires = ["pytest"]
```

`{project}` expands to the root of your source checkout, so `{project}/tests` points at your test directory.

> [!TIP]
> Set `build-frontend = "build[uv]"` to use [uv](https://pydevtools.com/handbook/reference/uv.md) as the build frontend. This speeds up dependency installation inside each build environment.

### Install system dependencies

If the compiled extension links against system libraries, install them with `before-all`. This command runs once per platform before any wheel builds start:

```toml {filename="pyproject.toml"}
[tool.cibuildwheel.linux]
before-all = "yum install -y libfoo-devel"

[tool.cibuildwheel.macos]
before-all = "brew install libfoo"
```

> [!NOTE]
> Linux builds run inside manylinux containers (CentOS-based by default), so use `yum` or `dnf` for package installation, not `apt`.

## Create the GitHub Actions workflow

Create `.github/workflows/build-wheels.yml`:

```yaml {filename=".github/workflows/build-wheels.yml"}
name: Build and publish wheels

on:
  push:
    branches: [main]
  pull_request:
  release:
    types: [published]

jobs:
  build_wheels:
    name: Build wheels on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
          - os: ubuntu-24.04-arm
          - os: macos-latest
          - os: windows-latest

    steps:
      - uses: actions/checkout@v4

      - name: Build wheels
        uses: pypa/cibuildwheel@v3.4

      - uses: actions/upload-artifact@v4
        with:
          name: wheels-${{ matrix.os }}
          path: ./wheelhouse/*.whl

  build_sdist:
    name: Build source distribution
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build sdist
        run: pipx run build --sdist

      - uses: actions/upload-artifact@v4
        with:
          name: sdist
          path: dist/*.tar.gz

  publish:
    name: Publish to PyPI
    needs: [build_wheels, build_sdist]
    runs-on: ubuntu-latest
    if: github.event_name == 'release' && github.event.action == 'published'
    environment: pypi
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true

      - uses: pypa/gh-action-pypi-publish@release/v1
```

### How this workflow operates

The workflow has three jobs:

`build_wheels` runs cibuildwheel on four runners. `ubuntu-latest` builds x86_64 Linux wheels, `ubuntu-24.04-arm` builds aarch64 Linux wheels natively, `macos-latest` builds arm64 macOS wheels, and `windows-latest` builds AMD64 Windows wheels. Each runner uploads its wheels as a separate artifact.

`build_sdist` builds a source distribution using `python -m build --sdist`. This gives users without a matching wheel a way to compile from source.

`publish` runs only when a GitHub Release is published. It downloads all wheel and sdist artifacts, merges them into one directory, and uploads to PyPI using [trusted publishing](https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing.md) (no API tokens needed).

> [!IMPORTANT]
> The `permissions: id-token: write` and `environment: pypi` lines are required for trusted publishing. Configure the trusted publisher on PyPI before the first release. See [How to publish to PyPI with trusted publishing](https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing.md).

## Build macOS wheels for both architectures

The `macos-latest` runner produces arm64 (Apple Silicon) wheels. To also build Intel wheels, add a second macOS entry or configure cross-compilation:

```yaml
matrix:
  include:
    - os: macos-latest           # arm64
    - os: macos-15-intel          # x86_64 (Intel runner)
```

Alternatively, set `archs` in `pyproject.toml` to cross-compile both on one runner:

```toml {filename="pyproject.toml"}
[tool.cibuildwheel.macos]
archs = ["x86_64", "arm64"]
```

Cross-compiling from arm64 to x86_64 (or vice versa) works for most projects, but test failures are possible if test code uses architecture-specific behavior. Use `test-skip` to skip tests on the cross-compiled architecture:

```toml {filename="pyproject.toml"}
[tool.cibuildwheel]
test-skip = ["*-macosx_x86_64"]
```

## Ship iOS and Android wheels

cibuildwheel 3.0 added iOS support and 3.1 added Android, both implementing the Python 3.13 mobile platform PEPs ([PEP 730](https://peps.python.org/pep-0730/) for iOS, [PEP 738](https://peps.python.org/pep-0738/) for Android). PyPI began accepting both wheel sets in February 2025 ([warehouse PR #17559](https://github.com/pypi/warehouse/pull/17559)), so the publish step in this workflow already works for them.

The runner requirements differ from desktop platforms:

- **iOS.** macOS runner with Xcode and the iOS simulator installed. Build targets are `arm64_iphoneos`, `arm64_iphonesimulator`, and `x86_64_iphonesimulator`. Tests execute inside the simulator.
- **Android.** Linux or macOS runner with an Android SDK installed. Build targets are `arm64_v8a` and `x86_64`.

Add mobile targets as separate matrix entries so the workflow can keep desktop builds parallel:

```yaml
matrix:
  include:
    - os: macos-latest
      cibw_platform: ios
    - os: ubuntu-latest
      cibw_platform: android
```

Then pass the platform to cibuildwheel:

```yaml
- name: Build wheels
  uses: pypa/cibuildwheel@v3.4
  env:
    CIBW_PLATFORM: ${{ matrix.cibw_platform || 'auto' }}
```

`CIBW_PLATFORM: auto` is the default for desktop entries, so the same workflow step covers desktop and mobile. [Briefcase](https://pydevtools.com/handbook/reference/briefcase.md) packages the iOS or Android *application* that depends on these library wheels; cibuildwheel only produces the library wheels themselves.

## Verify the build locally

Test the cibuildwheel configuration without pushing to CI by running it locally with [uv](https://pydevtools.com/handbook/reference/uv.md):

```bash
uvx cibuildwheel --platform linux
```

This builds wheels for the current platform. The `--platform` flag accepts `linux`, `macos`, or `windows`. Built wheels appear in `./wheelhouse/`.

## Learn More

- [cibuildwheel documentation](https://cibuildwheel.pypa.io/)
- [cibuildwheel reference](https://pydevtools.com/handbook/reference/cibuildwheel.md)
- [How to publish to PyPI with trusted publishing](https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing.md)
- [What is a wheel?](https://pydevtools.com/handbook/reference/wheel.md)
- [What is a build backend?](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md)
- [Build a Python library with a C extension](https://pydevtools.com/handbook/tutorial/build-a-python-library-with-a-c-extension.md)
- [Build a Python library with a C++ extension](https://pydevtools.com/handbook/tutorial/build-a-python-library-with-a-cpp-extension.md)
- [Build a Python library with a Rust extension](https://pydevtools.com/handbook/tutorial/build-a-python-library-with-a-rust-extension.md)
