How to Build Multi-Platform Wheels with cibuildwheel
Python packages with compiled extensions (C, C++, Rust, Cython) need platform-specific wheels so users can install them without a compiler. cibuildwheel 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.
Prerequisites
- A Python project with a compiled extension and a PEP 517-compliant build backend (setuptools, scikit-build-core, maturin, etc.)
- A
[build-system]table in pyproject.toml - A GitHub repository
- A PyPI account with trusted publishing 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:
[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 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:
[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:
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/[email protected]
- 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/v1How 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 (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.
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:
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:
[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:
[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 for iOS, PEP 738 for Android). PyPI began accepting both wheel sets in February 2025 (warehouse PR #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, andx86_64_iphonesimulator. Tests execute inside the simulator. - Android. Linux or macOS runner with an Android SDK installed. Build targets are
arm64_v8aandx86_64.
Add mobile targets as separate matrix entries so the workflow can keep desktop builds parallel:
matrix:
include:
- os: macos-latest
cibw_platform: ios
- os: ubuntu-latest
cibw_platform: androidThen pass the platform to cibuildwheel:
- name: Build wheels
uses: pypa/[email protected]
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 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:
uvx cibuildwheel --platform linuxThis builds wheels for the current platform. The --platform flag accepts linux, macos, or windows. Built wheels appear in ./wheelhouse/.