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

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

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

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