On each CI runner, one invocation:
On Linux, builds happen inside manylinux containers: old, standardized
distro images that guarantee the wheel runs on essentially any Linux.
A wheel built on ubuntu-latest doesn't require Ubuntu.
Runs on GitHub Actions, GitLab CI, Azure Pipelines, and more —
and locally, which is a great way to debug.
Lives in pyproject.toml (every option also has a CIBW_* environment
variable for one-off overrides):
[tool.cibuildwheel]
test-groups = ["test"]
test-command = "pytest {project}/tests"
build-frontend = "build[uv]"
test-groups + test-command — install your test group, run pytest{project} = your checkout; tests runbuild-frontend = "build[uv]" — faster environments, just needs uvskip (e.g. "*musllinux*"), environment (compiler flags — LinuxJust a Python app — builds wheels for the platform you're on. Linux wheels
use Docker/Podman, so you can build them from macOS or Windows. (On macOS,
native builds need python.org Python — target Linux there.)
Building every version is slow; --only takes one build identifier:
uvx cibuildwheel --print-build-identifiers
uvx cibuildwheel --only cp314-manylinux_x86_64
ls wheelhouse
Identifiers are <python>-<platform>: cp314-manylinux_x86_64,
cp313-macosx_arm64, cp312-win_amd64, ...
30+ wheels takes time — don't build on every PR:
# .github/workflows/cd.yml
name: CD
on:
workflow_dispatch:
release:
types:
- published
pull_request:
paths:
- .github/workflows/cd.yml
The paths: trick rebuilds wheels when the workflow file itself changes in a
PR — you can't break the release pipeline without noticing.
Don't forget the SDist — it's what everyone without a matching wheel (and
every distro) builds from:
make_sdist:
name: Make SDist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # if you use setuptools_scm
submodules: true # if you have submodules
- uses: astral-sh/setup-uv@v8
- name: Build SDist
run: uv build --sdist
- uses: actions/upload-artifact@v7
with:
name: cibw-sdist
path: dist/*.tar.gz
build_wheels:
name: Wheel on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm,
windows-latest, windows-11-arm,
macos-15-intel, macos-latest]
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: true
- uses: astral-sh/setup-uv@v8
- uses: pypa/cibuildwheel@v4.1
- uses: actions/upload-artifact@v7
with:
name: cibw-wheels-${{ matrix.os }}
path: wheelhouse/*.whl
pyproject.tomlupload_all:
needs: [build_wheels, make_sdist]
environment: pypi
permissions:
id-token: write
attestations: write
contents: read
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'published'
steps:
- uses: actions/download-artifact@v8
with:
pattern: cibw-*
path: dist
merge-multiple: true
- uses: actions/attest-build-provenance@v4
with:
subject-path: "dist/*"
- uses: pypa/gh-action-pypi-publish@release/v1
needs: [build_wheels, make_sdist] — nothing uploads unless every wheelpattern: cibw-* +merge-multiple: true (that's why every artifact name starts with cibw-)if:, environment: pypi,pyproject.toml; debug locally withuvx cibuildwheel --only ...