Skip to content

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

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:

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 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:

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:

.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/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 (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:

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:

pyproject.toml
[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 linux

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

Learn More

Last updated on

Please submit corrections and feedback...