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"]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/.