diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml
index 1b77f657..c483b270 100644
--- a/.github/workflows/update-dependencies.yml
+++ b/.github/workflows/update-dependencies.yml
@@ -17,9 +17,11 @@ jobs:
python-version: 3.9
architecture: x64
- name: Install dependencies
- run: python -m pip install requests pip-tools
- - name: Run update
+ run: python -m pip install ".[dev]"
+ - name: "Run update: dependencies"
run: python ./bin/update_dependencies.py
+ - name: "Run update: python configs"
+ run: python ./bin/update_pythons.py --force
- name: Create Pull Request
if: github.ref == 'refs/heads/master'
uses: peter-evans/create-pull-request@v3
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 020d0068..623e5d60 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,12 +16,20 @@ repos:
hooks:
- id: isort
+- repo: https://github.com/psf/black
+ rev: 20.8b1
+ hooks:
+ - id: black
+ files: ^bin/update_pythons.py$
+ args: ["--line-length=120"]
+
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.790
hooks:
- id: mypy
- files: ^(cibuildwheel/|test/|bin/projects.py|unit_test/)
+ files: ^(cibuildwheel/|test/|bin/projects.py|bin/update_pythons.py|unit_test/)
pass_filenames: false
+ additional_dependencies: [packaging, click]
- repo: https://github.com/asottile/pyupgrade
rev: v2.7.4
diff --git a/README.md b/README.md
index 276b15a2..af701624 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ cibuildwheel
Python wheels are great. Building them across **Mac, Linux, Windows**, on **multiple versions of Python**, is not.
-`cibuildwheel` is here to help. `cibuildwheel` runs on your CI server - currently it supports GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, and CircleCI - and it builds and tests your wheels across all of your platforms.
+`cibuildwheel` is here to help. `cibuildwheel` runs on your CI server - currently it supports GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, and GitLab CI - and it builds and tests your wheels across all of your platforms.
What does it do?
@@ -31,21 +31,22 @@ What does it do?
² Only supported on Travis
³ Beta support until Travis CI fixes a bug
-- Builds manylinux, macOS and Windows wheels for CPython and PyPy using Azure Pipelines, Travis CI, AppVeyor, and CircleCI
+- Builds manylinux, macOS, and Windows wheels for CPython and PyPy
+- Works on GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, and GitLab CI
- Bundles shared library dependencies on Linux and macOS through [auditwheel](https://github.com/pypa/auditwheel) and [delocate](https://github.com/matthew-brett/delocate)
- Runs the library test suite against the wheel-installed version of your library
Usage
-----
-`cibuildwheel` currently works on **Travis CI**, **Azure Pipelines**, **AppVeyor**, **GitHub Actions**, **CircleCI**, and **Gitlab CI**. Check the table below for supported platforms on each service:
+`cibuildwheel` runs inside a CI service. Supported platforms depend on which service you're using:
| | Linux | macOS | Windows | Linux ARM |
|-----------------|-------|-------|---------|--------------|
+| GitHub Actions | ✅ | ✅ | ✅ | ✴️¹ |
| Azure Pipelines | ✅ | ✅ | ✅ | ✴️¹ |
| Travis CI | ✅ | ✅ | ✅ | ✅ |
| AppVeyor | ✅ | ✅ | ✅ | |
-| GitHub Actions | ✅ | ✅ | ✅ | ✴️¹ |
| CircleCI | ✅ | ✅ | | |
| Gitlab CI | ✅ | | | |
@@ -80,7 +81,7 @@ jobs:
name: Install Python
- name: Install cibuildwheel
- run: python -m pip install cibuildwheel==1.7.4
+ run: python -m pip install cibuildwheel==1.8.0
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
@@ -92,7 +93,7 @@ jobs:
path: ./wheelhouse/*.whl
```
-For more information, including building on Python 2, PyPI deployment, and the use of other CI services like Travis CI, Appveyor, Azure Pipelines, or CircleCI, check out the [documentation](https://cibuildwheel.readthedocs.org) and the [examples](https://github.com/joerick/cibuildwheel/tree/master/examples).
+For more information, including building on Python 2, PyPI deployment, and the use of other CI services, check out the [documentation](https://cibuildwheel.readthedocs.org) and the [examples](https://github.com/joerick/cibuildwheel/tree/master/examples).
Options
-------
@@ -101,6 +102,7 @@ Options
|---|--------|-------------|
| **Build selection** | [`CIBW_PLATFORM`](https://cibuildwheel.readthedocs.io/en/stable/options/#platform) | Override the auto-detected target platform |
| | [`CIBW_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip)
[`CIBW_SKIP`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) | Choose the Python versions to build |
+| | [`CIBW_ARCHS_LINUX`](https://cibuildwheel.readthedocs.io/en/stable/options/#archs) | Build non-native architectures |
| **Build customization** | [`CIBW_ENVIRONMENT`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment) | Set environment variables needed during the build |
| | [`CIBW_BEFORE_ALL`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
@@ -111,6 +113,7 @@ Options
| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
| | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
| | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require |
+| | [`CIBW_TEST_SKIP`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-skip) | Skip running tests on some builds |
| **Other** | [`CIBW_BUILD_VERBOSITY`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-verbosity) | Increase/decrease the output of pip wheel |
@@ -129,17 +132,21 @@ Here are some repos that use cibuildwheel.
|-----------------------------------|----|----|:------|
| [scikit-learn][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The machine learning library. A complex but clean config using many of cibuildwheel's features to build a large project with Cython and C++ extensions. |
| [Matplotlib][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The venerable Matplotlib, a Python library with C++ portions |
+| [MyPy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | MyPyC, the compiled component of MyPy. |
| [psutil][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Cross-platform lib for process and system monitoring in Python |
-| [twisted-iocpsupport][] | ![github icon][] | ![windows icon][] | A submodule of Twisted that hooks into native C APIs using Cython. |
| [scikit-image][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Image processing library. Uses cibuildwheel to build and test a project that uses Cython with platform-native code. |
+| [twisted-iocpsupport][] | ![github icon][] | ![windows icon][] | A submodule of Twisted that hooks into native C APIs using Cython. |
| [websockets][] | ![travisci icon][] | ![apple icon][] ![linux icon][] | Library for building WebSocket servers and clients. Mostly written in Python, with a small C 'speedups' extension module. |
| [pyzmq][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Python bindings for zeromq, the networking library. Uses Cython and CFFI. |
| [aiortc][] | ![github icon][] | ![apple icon][] ![linux icon][] | WebRTC and ORTC implementation for Python using asyncio. |
+| [numexpr][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Fast numerical array expression evaluator for Python, NumPy, PyTables, pandas, bcolz and more |
| [h5py][] | ![azurepipelines icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | HDF5 for Python -- The h5py package is a Pythonic interface to the HDF5 binary data format. |
| [coverage.py][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The coverage tool for Python |
| [River][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | 🌊 Online machine learning in Python |
| [PyAV][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Pythonic bindings for FFmpeg's libraries. |
| [Dependency Injector][] | ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Dependency injection framework for Python, uses Windows TravisCI |
+| [PyTables][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | A Python package to manage extremely large amounts of data |
+| [ruptures][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Extensive Cython + NumPy [pyproject.toml](https://github.com/deepcharles/ruptures/blob/master/pyproject.toml) example. |
| [aioquic][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | QUIC and HTTP/3 implementation in Python |
| [google neuroglancer][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | WebGL-based viewer for volumetric data |
| [AutoPy][] | ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Includes a Windows Travis build. |
@@ -148,10 +155,15 @@ Here are some repos that use cibuildwheel.
| [python-rapidjson][] | ![travisci icon][] ![gitlab icon][] ![appveyor icon][] | ![windows icon][] ![linux icon][] | Python wrapper around rapidjson |
| [Rtree][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Rtree: spatial index for Python GIS ¶ |
| [KDEpy][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Kernel Density Estimation in Python |
-| [pybind11 python_example][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Example pybind11 module built with a Python-based build system |
+| [cyvcf2][] | ![github icon][] | ![apple icon][] ![linux icon][] | cython + htslib == fast VCF and BCF processing |
| [pybind11 cmake_example][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Example pybind11 module built with a CMake-based build system |
+| [pybind11 python_example][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Example pybind11 module built with a Python-based build system |
+| [dd-trace-py][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Uses custom alternate arch emulation on GitHub |
| [iminuit][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Jupyter-friendly Python interface for C++ MINUIT2 |
| [jq.py][] | ![travisci icon][] | ![apple icon][] ![linux icon][] | Python bindings for jq |
+| [matrixprofile][] | ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | A Python 3 library making time series data mining tasks, utilizing matrix profile algorithms, accessible to everyone. |
+| [CTranslate2][] | ![github icon][] | ![apple icon][] ![linux icon][] | Includes libraries from the [Intel oneAPI toolkit](https://software.intel.com/content/www/us/en/develop/tools/oneapi/base-toolkit.html). The Linux wheels also include CUDA libraries for GPU execution. |
+| [Tokenizer][] | ![github icon][] ![travisci icon][] | ![apple icon][] ![linux icon][] | Fast and customizable text tokenization library with BPE and SentencePiece support |
| [bx-python][] | ![travisci icon][] | ![apple icon][] ![linux icon][] | A library that includes Cython extensions. |
| [boost-histogram][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Supports full range of wheels, including PyPy and alternate archs. |
| [pybase64][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Fast Base64 encoding/decoding in Python |
@@ -161,22 +173,27 @@ Here are some repos that use cibuildwheel.
| [numpythia][] | ![github icon][] | ![apple icon][] ![linux icon][] | The interface between PYTHIA and NumPy |
| [fathon][] | ![travisci icon][] | ![apple icon][] ![linux icon][] | python package for DFA (Detrended Fluctuation Analysis) and related algorithms |
| [pyinstrument_cext][] | ![travisci icon][] ![appveyor icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | A simple C extension, without external dependencies |
+| [Imagecodecs (fork)][] | ![azurepipelines icon][] | ![apple icon][] ![linux icon][] | Over 20 external dependencies in compiled libraries, custom docker image, `libomp`, `openblas` and `install_name_tool` for macOS. |
| [xmlstarlet][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Python 3.6+ CFFI bindings with true MSVC build. |
| [pybind11 scikit_build_example][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | An example combining scikit-build and pybind11 |
[scikit-learn]: https://github.com/scikit-learn/scikit-learn
[Matplotlib]: https://github.com/matplotlib/matplotlib
+[MyPy]: https://github.com/mypyc/mypy_mypyc-wheels
[psutil]: https://github.com/giampaolo/psutil
-[twisted-iocpsupport]: https://github.com/twisted/twisted-iocpsupport
[scikit-image]: https://github.com/scikit-image/scikit-image
+[twisted-iocpsupport]: https://github.com/twisted/twisted-iocpsupport
[websockets]: https://github.com/aaugustin/websockets
[pyzmq]: https://github.com/zeromq/pyzmq
[aiortc]: https://github.com/aiortc/aiortc
+[numexpr]: https://github.com/pydata/numexpr
[h5py]: https://github.com/h5py/h5py
[coverage.py]: https://github.com/nedbat/coveragepy
[River]: https://github.com/online-ml/river
[PyAV]: https://github.com/PyAV-Org/PyAV
[Dependency Injector]: https://github.com/ets-labs/python-dependency-injector
+[PyTables]: https://github.com/PyTables/PyTables
+[ruptures]: https://github.com/deepcharles/ruptures
[aioquic]: https://github.com/aiortc/aioquic
[google neuroglancer]: https://github.com/google/neuroglancer
[AutoPy]: https://github.com/autopilot-rs/autopy
@@ -185,10 +202,15 @@ Here are some repos that use cibuildwheel.
[python-rapidjson]: https://github.com/python-rapidjson/python-rapidjson
[Rtree]: https://github.com/Toblerity/rtree
[KDEpy]: https://github.com/tommyod/KDEpy
-[pybind11 python_example]: https://github.com/pybind/python_example
+[cyvcf2]: https://github.com/brentp/cyvcf2
[pybind11 cmake_example]: https://github.com/pybind/cmake_example
+[pybind11 python_example]: https://github.com/pybind/python_example
+[dd-trace-py]: https://github.com/DataDog/dd-trace-py
[iminuit]: https://github.com/scikit-hep/iminuit
[jq.py]: https://github.com/mwilliamson/jq.py
+[matrixprofile]: https://github.com/matrix-profile-foundation/matrixprofile
+[CTranslate2]: https://github.com/OpenNMT/CTranslate2
+[Tokenizer]: https://github.com/OpenNMT/Tokenizer
[bx-python]: https://github.com/bxlab/bx-python
[boost-histogram]: https://github.com/scikit-hep/boost-histogram
[pybase64]: https://github.com/mayeut/pybase64
@@ -198,6 +220,7 @@ Here are some repos that use cibuildwheel.
[numpythia]: https://github.com/scikit-hep/numpythia
[fathon]: https://github.com/stfbnc/fathon
[pyinstrument_cext]: https://github.com/joerick/pyinstrument_cext
+[Imagecodecs (fork)]: https://github.com/czaki/imagecodecs_build
[xmlstarlet]: https://github.com/dimitern/xmlstarlet
[pybind11 scikit_build_example]: https://github.com/pybind/scikit_build_example
@@ -211,46 +234,56 @@ Here are some repos that use cibuildwheel.
[apple icon]: docs/data/readme_icons/apple.svg
[linux icon]: docs/data/readme_icons/linux.svg
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-> Add your repo here! Send a PR, adding your information to `docs/data/projects.yml`.
+> Add your repo here! Let us know on [GitHub Discussions](https://github.com/joerick/cibuildwheel/discussions/485), or send a PR, adding your information to `docs/data/projects.yml`.
>
> I'd like to include notes here to indicate why an example might be interesting to cibuildwheel users - the styles/technologies/techniques used in each. Please include that in future additions!
@@ -270,6 +303,27 @@ Changelog
+### 1.8.0
+
+_22 January 2021_
+
+- 🌟 Added support for emulated builds! You can now build manylinux wheels on
+ ARM64`aarch64`, as well as `ppc64le` and 's390x'. To build under emulation,
+ register QEMU via binfmt_misc and set the
+ [`CIBW_ARCHS_LINUX`](https://cibuildwheel.readthedocs.io/en/latest/options/#archs)
+ option to the architectures you want to run. See
+ [this FAQ entry](https://cibuildwheel.readthedocs.io/en/latest/faq/#emulation)
+ for more information. (#482)
+- ✨ Added `CIBW_TEST_SKIP` option. This allows you to choose certain builds
+ whose tests you'd like to skip. This might be useful when running a slow
+ test suite under emulation, for example. (#537)
+- ✨ Added `curly-{brace,bracket,paren}` style globbing to `CIBW_BUILD` and
+ `CIBW_SKIP`. This gives more expressivity, letting you do things like
+ `CIBW_BUILD=cp39-manylinux_{aarch64,ppc64le}`. (#527)
+- 🛠 cibuildwheel will now exit with an error if it's called with options that
+ skip all builds on a platform. This feature can be disabled by adding
+ `--allow-empty` on the command line. (#545)
+
### 1.7.4
_2 January 2021_
@@ -734,6 +788,8 @@ Massive props also to-
- @zfrenchee for [help debugging many issues](https://github.com/joerick/cibuildwheel/issues/2)
- @lelit for some great bug reports and [contributions](https://github.com/joerick/cibuildwheel/pull/73)
- @mayeut for a [phenomenal PR](https://github.com/joerick/cibuildwheel/pull/71) patching Python itself for better compatibility!
+- @czaki for being a super-contributor over many PRs and helping out with countless issues!
+- @mattip for his help with adding PyPy support to cibuildwheel
See also
========
diff --git a/action.yml b/action.yml
index 9d23e023..6429ff78 100644
--- a/action.yml
+++ b/action.yml
@@ -19,8 +19,10 @@ runs:
# This needs powershell-core because github.action path is a Windows-style
# path on Windows. Powershell-core understands both types of paths.
- - run: python -m pip install ${{ github.action_path }}
+ - run: >
+ pipx run
+ --spec ${{ github.action_path }}
+ cibuildwheel
+ ${{ inputs.package-dir }}
+ --output-dir ${{ inputs.output-dir }}
shell: pwsh
-
- - run: python -m cibuildwheel ${{ inputs.package-dir }} --output-dir ${{ inputs.output-dir }}
- shell: bash
diff --git a/bin/bump_version.py b/bin/bump_version.py
index 1520108a..498d3ad2 100755
--- a/bin/bump_version.py
+++ b/bin/bump_version.py
@@ -1,8 +1,10 @@
#!/usr/bin/env python3
+
import glob
import os
import subprocess
+import sys
import urllib.parse
from pathlib import Path
@@ -23,6 +25,8 @@ config = [
]
+# This file requires Python 3.7
+# Setting -> None will cause MyPy to notice this.
@click.command()
def bump_version():
current_version = cibuildwheel.__version__
@@ -49,7 +53,7 @@ def bump_version():
if repo_has_uncommitted_changes:
print('error: Uncommitted changes detected.')
- exit(1)
+ sys.exit(1)
print( 'Current version:', current_version) # noqa
new_version = input(' New version: ').strip()
@@ -59,7 +63,7 @@ def bump_version():
except InvalidVersion:
print("error: This version doesn't conform to PEP440")
print(' https://www.python.org/dev/peps/pep-0440/')
- exit(1)
+ sys.exit(1)
actions = []
@@ -68,7 +72,7 @@ def bump_version():
if not paths:
print(f'error: Pattern {path_pattern} didn’t match any files')
- exit(1)
+ sys.exit(1)
find_pattern = version_pattern.format(current_version)
replace_pattern = version_pattern.format(new_version)
@@ -83,8 +87,8 @@ def bump_version():
)
if not found_at_least_one_file_needing_update:
- print(f'error: Didn’t find any occurences of “{find_pattern}” in “{path_pattern}”')
- exit(1)
+ print(f'error: Didn’t find any occurrences of “{find_pattern}” in “{path_pattern}”')
+ sys.exit(1)
print()
print("Here's the plan:")
@@ -102,7 +106,7 @@ def bump_version():
if answer != 'y':
print('Aborted')
- exit(1)
+ sys.exit(1)
for path, find, replace in actions:
contents = path.read_text(encoding='utf8')
diff --git a/bin/make_dependency_update_pr.py b/bin/make_dependency_update_pr.py
index b89d371b..902d3f6d 100755
--- a/bin/make_dependency_update_pr.py
+++ b/bin/make_dependency_update_pr.py
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import os
+import sys
import textwrap
import time
from pathlib import Path
@@ -26,7 +27,7 @@ def main():
if git_repo_has_changes():
print('Your git repo has uncommitted changes. Commit or stash before continuing.')
- exit(1)
+ sys.exit(1)
previous_branch = shell('git rev-parse --abbrev-ref HEAD',
check=True,
diff --git a/bin/projects.py b/bin/projects.py
index 22898cc3..c6cbbacf 100755
--- a/bin/projects.py
+++ b/bin/projects.py
@@ -20,7 +20,7 @@ from typing import Any, Dict, List, Optional, TextIO
import click
import yaml
-from github import Github
+from github import Github, GithubException
ICONS = (
"appveyor",
@@ -53,7 +53,12 @@ class Project:
self.online = github is not None
if github is not None:
- repo = github.get_repo(self.stars_repo)
+ try:
+ repo = github.get_repo(self.stars_repo)
+ except GithubException:
+ print(f"Broken: {self.stars_repo}")
+ raise
+
self.num_stars = repo.stargazers_count
self.pushed_at = repo.pushed_at
if not self.notes:
@@ -167,7 +172,7 @@ def str_projects(
return io.getvalue()
-@click.command()
+@click.command(help="Try ./bin/projects.py docs/data/projects.yml --readme README.md")
@click.argument("input", type=click.File("r"))
@click.option("--online/--no-online", default=True, help="Get info from GitHub")
@click.option("--auth", help="GitHub authentication token")
diff --git a/bin/run_example_ci_configs.py b/bin/run_example_ci_configs.py
index 6d99f59f..ad5f6a41 100755
--- a/bin/run_example_ci_configs.py
+++ b/bin/run_example_ci_configs.py
@@ -98,7 +98,7 @@ def run_example_ci_configs(config_files=None):
if git_repo_has_changes():
print('Your git repo has uncommitted changes. Commit or stash before continuing.')
- exit(1)
+ sys.exit(1)
previous_branch = shell('git rev-parse --abbrev-ref HEAD',
check=True,
diff --git a/bin/sample_build.py b/bin/sample_build.py
index f73aac5d..8fad4ed8 100755
--- a/bin/sample_build.py
+++ b/bin/sample_build.py
@@ -22,4 +22,4 @@ if __name__ == '__main__':
options.project_python_path, project_dir
], check=True)
- exit(subprocess.run([sys.executable, '-m', 'cibuildwheel'], cwd=project_dir).returncode)
+ sys.exit(subprocess.run([sys.executable, '-m', 'cibuildwheel'], cwd=project_dir).returncode)
diff --git a/bin/update_dependencies.py b/bin/update_dependencies.py
index 62717c31..bd113eb5 100755
--- a/bin/update_dependencies.py
+++ b/bin/update_dependencies.py
@@ -28,8 +28,8 @@ if '--no-docker' in sys.argv:
'--output-file', f'cibuildwheel/resources/constraints-python{python_version}.txt'
])
else:
- image = 'quay.io/pypa/manylinux2010_x86_64:latest'
- subprocess.check_call(['docker', 'pull', image])
+ image_runner = 'quay.io/pypa/manylinux2010_x86_64:latest'
+ subprocess.check_call(['docker', 'pull', image_runner])
for python_version in PYTHON_VERSIONS:
abi_flags = '' if int(python_version) >= 38 else 'm'
python_path = f'/opt/python/cp{python_version}-cp{python_version}{abi_flags}/bin/'
@@ -37,7 +37,7 @@ else:
'docker', 'run', '--rm',
'-e', 'CUSTOM_COMPILE_COMMAND',
'-v', f'{os.getcwd()}:/volume',
- '--workdir', '/volume', image,
+ '--workdir', '/volume', image_runner,
'bash', '-c',
f'{python_path}pip install pip-tools &&'
f'{python_path}pip-compile --allow-unsafe --upgrade '
diff --git a/bin/update_pythons.py b/bin/update_pythons.py
new file mode 100755
index 00000000..04f075de
--- /dev/null
+++ b/bin/update_pythons.py
@@ -0,0 +1,310 @@
+#!/usr/bin/env python3
+
+import copy
+import difflib
+import logging
+from pathlib import Path
+from typing import Dict, Optional, Union
+
+import click
+import requests
+import rich
+import toml
+from packaging.specifiers import Specifier
+from packaging.version import Version
+from rich.logging import RichHandler
+from rich.syntax import Syntax
+
+from cibuildwheel.extra import InlineArrayDictEncoder
+from cibuildwheel.typing import Final, Literal, TypedDict
+
+log = logging.getLogger("cibw")
+
+# Looking up the dir instead of using utils.resources_dir
+# since we want to write to it.
+DIR: Final[Path] = Path(__file__).parent.parent.resolve()
+RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"
+
+
+ArchStr = Literal["32", "64"]
+
+
+class ConfigWinCP(TypedDict):
+ identifier: str
+ version: str
+ arch: str
+
+
+class ConfigWinPP(TypedDict):
+ identifier: str
+ version: str
+ arch: str
+ url: str
+
+
+class ConfigMacOS(TypedDict):
+ identifier: str
+ version: str
+ url: str
+
+
+AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigMacOS]
+
+
+# The following set of "Versions" classes allow the initial call to the APIs to
+# be cached and reused in the `update_version_*` methods.
+
+
+class WindowsVersions:
+ def __init__(self, arch_str: ArchStr) -> None:
+
+ response = requests.get("https://api.nuget.org/v3/index.json")
+ response.raise_for_status()
+ api_info = response.json()
+
+ for resource in api_info["resources"]:
+ if resource["@type"] == "PackageBaseAddress/3.0.0":
+ endpoint = resource["@id"]
+
+ ARCH_DICT = {"32": "win32", "64": "win_amd64"}
+ PACKAGE_DICT = {"32": "pythonx86", "64": "python"}
+
+ self.arch_str = arch_str
+ self.arch = ARCH_DICT[arch_str]
+ package = PACKAGE_DICT[arch_str]
+
+ response = requests.get(f"{endpoint}{package}/index.json")
+ response.raise_for_status()
+ cp_info = response.json()
+
+ versions = (Version(v) for v in cp_info["versions"])
+ self.versions = sorted(v for v in versions if not v.is_devrelease)
+
+ def update_version_windows(self, spec: Specifier) -> Optional[ConfigWinCP]:
+ versions = sorted(v for v in self.versions if spec.contains(v))
+ if not all(v.is_prerelease for v in versions):
+ versions = [v for v in versions if not v.is_prerelease]
+ log.debug(f"Windows {self.arch} {spec} has {', '.join(str(v) for v in versions)}")
+
+ if not versions:
+ return None
+
+ version = versions[-1]
+ identifier = f"cp{version.major}{version.minor}-{self.arch}"
+ result = ConfigWinCP(
+ identifier=identifier,
+ version=str(version),
+ arch=self.arch_str,
+ )
+ return result
+
+
+class PyPyVersions:
+ def __init__(self, arch_str: ArchStr):
+
+ response = requests.get("https://downloads.python.org/pypy/versions.json")
+ response.raise_for_status()
+
+ releases = [r for r in response.json() if r["pypy_version"] != "nightly"]
+ for release in releases:
+ release["pypy_version"] = Version(release["pypy_version"])
+ release["python_version"] = Version(release["python_version"])
+
+ self.releases = [
+ r for r in releases if not r["pypy_version"].is_prerelease and not r["pypy_version"].is_devrelease
+ ]
+ self.arch = arch_str
+
+ def update_version_windows(self, spec: Specifier) -> ConfigWinCP:
+ if self.arch != "32":
+ raise RuntimeError("64 bit releases not supported yet on Windows")
+
+ releases = [r for r in self.releases if spec.contains(r["python_version"])]
+ releases = sorted(releases, key=lambda r: r["pypy_version"])
+
+ if not releases:
+ raise RuntimeError(f"PyPy Win {self.arch} not found for {spec}! {self.releases}")
+
+ release = releases[-1]
+ version = release["python_version"]
+ identifier = f"pp{version.major}{version.minor}-win32"
+
+ (url,) = [rf["download_url"] for rf in release["files"] if "" in rf["platform"] == "win32"]
+
+ return ConfigWinPP(
+ identifier=identifier,
+ version=f"{version.major}.{version.minor}",
+ arch="32",
+ url=url,
+ )
+
+ def update_version_macos(self, spec: Specifier) -> ConfigMacOS:
+ if self.arch != "64":
+ raise RuntimeError("Other archs not supported yet on macOS")
+
+ releases = [r for r in self.releases if spec.contains(r["python_version"])]
+ releases = sorted(releases, key=lambda r: r["pypy_version"])
+
+ if not releases:
+ raise RuntimeError(f"PyPy macOS {self.arch} not found for {spec}!")
+
+ release = releases[-1]
+ version = release["python_version"]
+ identifier = f"pp{version.major}{version.minor}-macosx_x86_64"
+
+ (url,) = [
+ rf["download_url"] for rf in release["files"] if "" in rf["platform"] == "darwin" and rf["arch"] == "x64"
+ ]
+
+ return ConfigMacOS(
+ identifier=identifier,
+ version=f"{version.major}.{version.minor}",
+ url=url,
+ )
+
+
+class CPythonVersions:
+ def __init__(self, plat_arch: str, file_ident: str) -> None:
+
+ response = requests.get("https://www.python.org/api/v2/downloads/release/?is_published=true")
+ response.raise_for_status()
+
+ releases_info = response.json()
+
+ self.versions_dict: Dict[Version, int] = {}
+ for release in releases_info:
+ # Removing the prefix, Python 3.9 would use: release["name"].removeprefix("Python ")
+ version = Version(release["name"][7:])
+
+ if not version.is_prerelease and not version.is_devrelease:
+ uri = int(release["resource_uri"].rstrip("/").split("/")[-1])
+ self.versions_dict[version] = uri
+
+ self.file_ident = file_ident
+ self.plat_arch = plat_arch
+
+ def update_version_macos(self, spec: Specifier) -> Optional[ConfigMacOS]:
+
+ sorted_versions = sorted(v for v in self.versions_dict if spec.contains(v))
+
+ for version in reversed(sorted_versions):
+ # Find the first patch version that contains the requested file
+ uri = self.versions_dict[version]
+ response = requests.get(f"https://www.python.org/api/v2/downloads/release_file/?release={uri}")
+ response.raise_for_status()
+ file_info = response.json()
+
+ urls = [rf["url"] for rf in file_info if self.file_ident in rf["url"]]
+ if urls:
+ return ConfigMacOS(
+ identifier=f"cp{version.major}{version.minor}-{self.plat_arch}",
+ version=f"{version.major}.{version.minor}",
+ url=urls[0],
+ )
+
+ return None
+
+
+# This is a universal interface to all the above Versions classes. Given an
+# identifier, it updates a config dict.
+
+
+class AllVersions:
+ def __init__(self) -> None:
+ self.windows_32 = WindowsVersions("32")
+ self.windows_64 = WindowsVersions("64")
+ self.windows_pypy = PyPyVersions("32")
+
+ self.macos_6 = CPythonVersions(plat_arch="macosx_x86_64", file_ident="macosx10.6.pkg")
+ self.macos_9 = CPythonVersions(plat_arch="macosx_x86_64", file_ident="macosx10.9.pkg")
+ self.macos_u2 = CPythonVersions(plat_arch="macosx_universal2", file_ident="macos11.0.pkg")
+ self.macos_pypy = PyPyVersions("64")
+
+ def update_config(self, config: Dict[str, str]) -> None:
+ identifier = config["identifier"]
+ version = Version(config["version"])
+ spec = Specifier(f"=={version.major}.{version.minor}.*")
+ log.info(f"Reading in '{identifier}' -> {spec} @ {version}")
+ orig_config = copy.copy(config)
+ config_update: Optional[AnyConfig]
+
+ # We need to use ** in update due to MyPy (probably a bug)
+ if "macosx_x86_64" in identifier:
+ if identifier.startswith("pp"):
+ config_update = self.macos_pypy.update_version_macos(spec)
+ else:
+ config_update = self.macos_9.update_version_macos(spec) or self.macos_6.update_version_macos(spec)
+ assert config_update is not None, f"MacOS {spec} not found!"
+ config.update(**config_update)
+ elif "win32" in identifier:
+ if identifier.startswith("pp"):
+ config.update(**self.windows_pypy.update_version_windows(spec))
+ else:
+ config_update = self.windows_32.update_version_windows(spec)
+ if config_update:
+ config.update(**config_update)
+ elif "win_amd64" in identifier:
+ config_update = self.windows_64.update_version_windows(spec)
+ if config_update:
+ config.update(**config_update)
+
+ if config != orig_config:
+ log.info(f" Updated {orig_config} to {config}")
+
+
+@click.command()
+@click.option("--force", is_flag=True)
+@click.option("--level", default="INFO", type=click.Choice(["INFO", "DEBUG", "TRACE"], case_sensitive=False))
+def update_pythons(force: bool, level: str) -> None:
+
+ logging.basicConfig(
+ level="INFO",
+ format="%(message)s",
+ datefmt="[%X]",
+ handlers=[RichHandler(rich_tracebacks=True, markup=True)],
+ )
+ log.setLevel(level)
+
+ all_versions = AllVersions()
+ toml_file_path = RESOURCES_DIR / "build-platforms.toml"
+
+ original_toml = toml_file_path.read_text()
+ configs = toml.loads(original_toml)
+
+ for config in configs["windows"]["python_configurations"]:
+ all_versions.update_config(config)
+
+ for config in configs["macos"]["python_configurations"]:
+ all_versions.update_config(config)
+
+ result_toml = toml.dumps(configs, encoder=InlineArrayDictEncoder()) # type: ignore
+
+ rich.print() # spacer
+
+ if original_toml == result_toml:
+ rich.print("[green]Check complete, Python configurations unchanged.")
+ return
+
+ rich.print("Python configurations updated.")
+ rich.print("Changes:")
+ rich.print()
+
+ toml_relpath = toml_file_path.relative_to(DIR).as_posix()
+ diff_lines = difflib.unified_diff(
+ original_toml.splitlines(keepends=True),
+ result_toml.splitlines(keepends=True),
+ fromfile=toml_relpath,
+ tofile=toml_relpath,
+ )
+ rich.print(Syntax("".join(diff_lines), "diff", theme="ansi_light"))
+ rich.print()
+
+ if force:
+ toml_file_path.write_text(result_toml)
+ rich.print("[green]TOML file updated.")
+ else:
+ rich.print("[yellow]File left unchanged. Use --force flag to update.")
+
+
+if __name__ == "__main__":
+ update_pythons()
diff --git a/cibuildwheel/__init__.py b/cibuildwheel/__init__.py
index 043606c7..b2809757 100644
--- a/cibuildwheel/__init__.py
+++ b/cibuildwheel/__init__.py
@@ -1 +1 @@
-__version__ = '1.7.4'
+__version__ = '1.8.0'
diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py
index c80b50a3..3b7e2af7 100644
--- a/cibuildwheel/__main__.py
+++ b/cibuildwheel/__main__.py
@@ -5,18 +5,20 @@ import textwrap
import traceback
from configparser import ConfigParser
from pathlib import Path
-from typing import Any, Dict, List, Optional, Set, overload
+from typing import Dict, List, Optional, Set, Union, overload
import cibuildwheel
import cibuildwheel.linux
import cibuildwheel.macos
import cibuildwheel.windows
+from cibuildwheel.architecture import Architecture, allowed_architectures_check
from cibuildwheel.environment import EnvironmentParseError, parse_environment
+from cibuildwheel.typing import PLATFORMS, PlatformName, assert_never
from cibuildwheel.util import (
- Architecture,
BuildOptions,
BuildSelector,
DependencyConstraints,
+ TestSelector,
Unbuffered,
detect_ci_provider,
resources_dir,
@@ -46,6 +48,8 @@ def get_option_from_environment(option_name: str, platform: Optional[str] = None
def main() -> None:
+ platform: PlatformName
+
parser = argparse.ArgumentParser(
description='Build wheels for all the platforms.',
epilog='''
@@ -73,7 +77,7 @@ def main() -> None:
on this machine. Set this option to build an architecture
via emulation, for example, using binfmt_misc and QEMU.
Default: auto.
- Choices: auto, {}
+ Choices: auto, native, all, {}
'''.format(", ".join(a.name for a in Architecture)))
parser.add_argument('--output-dir',
default=os.environ.get('CIBW_OUTPUT_DIR', 'wheelhouse'),
@@ -91,6 +95,9 @@ def main() -> None:
parser.add_argument('--print-build-identifiers',
action='store_true',
help='Print the build identifiers matched by the current invocation and exit.')
+ parser.add_argument('--allow-empty',
+ action='store_true',
+ help='Do not report an error code if the build does not match any wheels.')
args = parser.parse_args()
@@ -107,7 +114,7 @@ def main() -> None:
supported. You can run on your development machine or other CI providers using the
--platform argument. Check --help output for more information.
'''), file=sys.stderr)
- exit(2)
+ sys.exit(2)
if sys.platform.startswith('linux'):
platform = 'linux'
elif sys.platform == 'darwin':
@@ -118,7 +125,11 @@ def main() -> None:
print('cibuildwheel: Unable to detect platform from "sys.platform" in a CI environment. You can run '
'cibuildwheel using the --platform argument. Check --help output for more information.',
file=sys.stderr)
- exit(2)
+ sys.exit(2)
+
+ if platform not in PLATFORMS:
+ print(f'cibuildwheel: Unsupported platform: {platform}', file=sys.stderr)
+ sys.exit(2)
package_dir = Path(args.package_dir)
output_dir = Path(args.output_dir)
@@ -127,10 +138,13 @@ def main() -> None:
repair_command_default = 'auditwheel repair -w {dest_dir} {wheel}'
elif platform == 'macos':
repair_command_default = 'delocate-listdeps {wheel} && delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel}'
- else:
+ elif platform == 'windows':
repair_command_default = ''
+ else:
+ assert_never(platform)
build_config, skip_config = os.environ.get('CIBW_BUILD', '*'), os.environ.get('CIBW_SKIP', '')
+ test_skip = os.environ.get('CIBW_TEST_SKIP', '')
environment_config = get_option_from_environment('CIBW_ENVIRONMENT', platform=platform, default='')
before_all = get_option_from_environment('CIBW_BEFORE_ALL', platform=platform, default='')
before_build = get_option_from_environment('CIBW_BEFORE_BUILD', platform=platform)
@@ -142,14 +156,15 @@ def main() -> None:
test_extras = get_option_from_environment('CIBW_TEST_EXTRAS', platform=platform, default='')
build_verbosity_str = get_option_from_environment('CIBW_BUILD_VERBOSITY', platform=platform, default='')
- build_selector = BuildSelector(build_config, skip_config)
+ build_selector = BuildSelector(build_config=build_config, skip_config=skip_config)
+ test_selector = TestSelector(skip_config=test_skip)
try:
environment = parse_environment(environment_config)
except (EnvironmentParseError, ValueError):
print(f'cibuildwheel: Malformed environment option "{environment_config}"', file=sys.stderr)
traceback.print_exc(None, sys.stderr)
- exit(2)
+ sys.exit(2)
if dependency_versions == 'pinned':
dependency_constraints: Optional[DependencyConstraints] = DependencyConstraints.with_defaults()
@@ -174,7 +189,7 @@ def main() -> None:
if not any((package_dir / name).exists()
for name in ["setup.py", "setup.cfg", "pyproject.toml"]):
print('cibuildwheel: Could not find setup.py, setup.cfg or pyproject.toml at root of package', file=sys.stderr)
- exit(2)
+ sys.exit(2)
if args.archs is not None:
archs_config_str = args.archs
@@ -182,9 +197,12 @@ def main() -> None:
archs_config_str = get_option_from_environment('CIBW_ARCHS', platform=platform, default='auto')
archs = Architecture.parse_config(archs_config_str, platform=platform)
+ identifiers = get_build_identifiers(platform, build_selector, archs)
+
if args.print_build_identifiers:
- print_build_identifiers(platform, build_selector, archs)
- exit(0)
+ for identifier in identifiers:
+ print(identifier)
+ sys.exit(0)
manylinux_images: Optional[Dict[str, str]] = None
if platform == 'linux':
@@ -227,6 +245,7 @@ def main() -> None:
before_all=before_all,
build_verbosity=build_verbosity,
build_selector=build_selector,
+ test_selector=test_selector,
repair_command=repair_command,
environment=environment,
dependency_constraints=dependency_constraints,
@@ -238,6 +257,17 @@ def main() -> None:
print_preamble(platform, build_options)
+ try:
+ allowed_architectures_check(platform, build_options.architectures)
+ except ValueError as err:
+ print("cibuildwheel:", *err.args, file=sys.stderr)
+ sys.exit(4)
+
+ if not identifiers:
+ print(f'cibuildwheel: No build identifiers selected: {build_selector}', file=sys.stderr)
+ if not args.allow_empty:
+ sys.exit(3)
+
if not output_dir.exists():
output_dir.mkdir(parents=True)
@@ -248,8 +278,7 @@ def main() -> None:
elif platform == 'macos':
cibuildwheel.macos.build(build_options)
else:
- print(f'cibuildwheel: Unsupported platform: {platform}', file=sys.stderr)
- exit(2)
+ assert_never(platform)
def detect_obsolete_options() -> None:
@@ -263,7 +292,7 @@ def detect_obsolete_options() -> None:
os.environ[alternative] = os.environ[deprecated]
else:
print(f"Option '{alternative}' is not empty. Please unset '{deprecated}'")
- exit(2)
+ sys.exit(2)
# Check for deprecated identifiers in 'CIBW_BUILD' and 'CIBW_SKIP' options
for option in ['CIBW_BUILD', 'CIBW_SKIP']:
@@ -299,19 +328,22 @@ def print_preamble(platform: str, build_options: BuildOptions) -> None:
print('\nHere we go!\n')
-def print_build_identifiers(
- platform: str, build_selector: BuildSelector, architectures: Set[Architecture]
-) -> None:
- python_configurations: List[Any] = []
+def get_build_identifiers(
+ platform: PlatformName, build_selector: BuildSelector, architectures: Set[Architecture]
+) -> List[str]:
+ python_configurations: Union[List[cibuildwheel.linux.PythonConfiguration],
+ List[cibuildwheel.windows.PythonConfiguration],
+ List[cibuildwheel.macos.PythonConfiguration]]
if platform == 'linux':
python_configurations = cibuildwheel.linux.get_python_configurations(build_selector, architectures)
elif platform == 'windows':
python_configurations = cibuildwheel.windows.get_python_configurations(build_selector, architectures)
elif platform == 'macos':
python_configurations = cibuildwheel.macos.get_python_configurations(build_selector, architectures)
+ else:
+ assert_never(platform)
- for config in python_configurations:
- print(config.identifier)
+ return [config.identifier for config in python_configurations]
def detect_warnings(platform: str, build_options: BuildOptions) -> List[str]:
diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py
new file mode 100644
index 00000000..63e3a8bc
--- /dev/null
+++ b/cibuildwheel/architecture.py
@@ -0,0 +1,99 @@
+import functools
+import platform as platform_module
+import re
+from enum import Enum
+from typing import Set
+
+from .typing import PlatformName, assert_never
+
+PRETTY_NAMES = {'linux': 'Linux', 'macos': 'macOS', 'windows': 'Windows'}
+
+
+@functools.total_ordering
+class Architecture(Enum):
+ value: str
+
+ # mac/linux archs
+ x86_64 = 'x86_64'
+
+ # linux archs
+ i686 = 'i686'
+ aarch64 = 'aarch64'
+ ppc64le = 'ppc64le'
+ s390x = 's390x'
+
+ # mac archs
+ universal2 = 'universal2'
+ arm64 = 'arm64'
+
+ # windows archs
+ x86 = 'x86'
+ AMD64 = 'AMD64'
+
+ # Allow this to be sorted
+ def __lt__(self, other: "Architecture") -> bool:
+ return self.value < other.value
+
+ @staticmethod
+ def parse_config(config: str, platform: PlatformName) -> 'Set[Architecture]':
+ result = set()
+ for arch_str in re.split(r'[\s,]+', config):
+ if arch_str == 'auto':
+ result |= Architecture.auto_archs(platform=platform)
+ elif arch_str == 'native':
+ result.add(Architecture(platform_module.machine()))
+ elif arch_str == 'all':
+ result |= Architecture.all_archs(platform=platform)
+ else:
+ result.add(Architecture(arch_str))
+ return result
+
+ @staticmethod
+ def auto_archs(platform: PlatformName) -> 'Set[Architecture]':
+ native_architecture = Architecture(platform_module.machine())
+ result = {native_architecture}
+
+ if platform == 'linux' and native_architecture == Architecture.x86_64:
+ # x86_64 machines can run i686 docker containers
+ result.add(Architecture.i686)
+
+ if platform == 'windows' and native_architecture == Architecture.AMD64:
+ result.add(Architecture.x86)
+
+ if platform == 'macos' and native_architecture == Architecture.arm64:
+ # arm64 can build and test both archs of a universal2 wheel.
+ result.add(Architecture.universal2)
+
+ return result
+
+ @staticmethod
+ def all_archs(platform: PlatformName) -> 'Set[Architecture]':
+ if platform == 'linux':
+ return {Architecture.x86_64, Architecture.i686, Architecture.aarch64, Architecture.ppc64le, Architecture.s390x}
+ elif platform == 'macos':
+ return {Architecture.x86_64, Architecture.arm64, Architecture.universal2}
+ elif platform == 'windows':
+ return {Architecture.x86, Architecture.AMD64}
+ else:
+ assert_never(platform)
+
+
+def allowed_architectures_check(
+ platform: PlatformName,
+ architectures: Set[Architecture],
+) -> None:
+
+ allowed_architectures = Architecture.all_archs(platform)
+
+ msg = f'{PRETTY_NAMES[platform]} only supports {sorted(allowed_architectures)} at the moment.'
+
+ if platform != 'linux':
+ msg += ' If you want to set emulation architectures on Linux, use CIBW_ARCHS_LINUX instead.'
+
+ if not architectures <= allowed_architectures:
+ msg = f'Invalid archs option {architectures}. ' + msg
+ raise ValueError(msg)
+
+ if not architectures:
+ msg = 'Empty archs option set. ' + msg
+ raise ValueError(msg)
diff --git a/cibuildwheel/docker_container.py b/cibuildwheel/docker_container.py
index aee933d2..e18eed8c 100644
--- a/cibuildwheel/docker_container.py
+++ b/cibuildwheel/docker_container.py
@@ -30,6 +30,9 @@ class DockerContainer:
bash_stdout: IO[bytes]
def __init__(self, docker_image: str, simulate_32_bit: bool = False, cwd: Optional[PathOrStr] = None):
+ if not docker_image:
+ raise ValueError("Must have a non-empty docker image to run.")
+
self.docker_image = docker_image
self.simulate_32_bit = simulate_32_bit
self.cwd = cwd
diff --git a/cibuildwheel/extra.py b/cibuildwheel/extra.py
new file mode 100644
index 00000000..3c2ef4fb
--- /dev/null
+++ b/cibuildwheel/extra.py
@@ -0,0 +1,24 @@
+"""
+These are utilities for the `/bin` scripts, not for the `cibuildwheel` program.
+"""
+
+from typing import Any, Dict
+
+import toml.encoder
+from packaging.version import Version
+
+
+class InlineArrayDictEncoder(toml.encoder.TomlEncoder): # type: ignore
+ def __init__(self) -> None:
+ super().__init__()
+ self.dump_funcs[Version] = lambda v: f'"{v}"'
+
+ def dump_sections(self, o: Dict[str, Any], sup: str) -> Any:
+ if all(isinstance(a, list) for a in o.values()):
+ val = ""
+ for k, v in o.items():
+ inner = ",\n ".join(self.dump_inline_table(d_i).strip() for d_i in v)
+ val += f"{k} = [\n {inner},\n]\n"
+ return val, self._dict()
+ else:
+ return super().dump_sections(o, sup)
diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py
index c5d32d9e..bde5e34f 100644
--- a/cibuildwheel/linux.py
+++ b/cibuildwheel/linux.py
@@ -4,17 +4,18 @@ import textwrap
from pathlib import Path, PurePath
from typing import List, NamedTuple, Set
+from .architecture import Architecture
from .docker_container import DockerContainer
from .logger import log
from .typing import PathOrStr
from .util import (
- Architecture,
BuildOptions,
BuildSelector,
NonPlatformWheelError,
- allowed_architectures_check,
get_build_verbosity_extra_flags,
prepare_command,
+ read_python_configs,
+ resources_dir,
)
@@ -29,42 +30,13 @@ class PythonConfiguration(NamedTuple):
def get_python_configurations(
- build_selector: BuildSelector, architectures: Set[Architecture]
+ build_selector: BuildSelector,
+ architectures: Set[Architecture]
) -> List[PythonConfiguration]:
- python_configurations = [
- PythonConfiguration(version='2.7', identifier='cp27-manylinux_x86_64', path_str='/opt/python/cp27-cp27m'),
- PythonConfiguration(version='2.7', identifier='cp27-manylinux_x86_64', path_str='/opt/python/cp27-cp27mu'),
- PythonConfiguration(version='3.5', identifier='cp35-manylinux_x86_64', path_str='/opt/python/cp35-cp35m'),
- PythonConfiguration(version='3.6', identifier='cp36-manylinux_x86_64', path_str='/opt/python/cp36-cp36m'),
- PythonConfiguration(version='3.7', identifier='cp37-manylinux_x86_64', path_str='/opt/python/cp37-cp37m'),
- PythonConfiguration(version='3.8', identifier='cp38-manylinux_x86_64', path_str='/opt/python/cp38-cp38'),
- PythonConfiguration(version='3.9', identifier='cp39-manylinux_x86_64', path_str='/opt/python/cp39-cp39'),
- PythonConfiguration(version='2.7', identifier='cp27-manylinux_i686', path_str='/opt/python/cp27-cp27m'),
- PythonConfiguration(version='2.7', identifier='cp27-manylinux_i686', path_str='/opt/python/cp27-cp27mu'),
- PythonConfiguration(version='3.5', identifier='cp35-manylinux_i686', path_str='/opt/python/cp35-cp35m'),
- PythonConfiguration(version='3.6', identifier='cp36-manylinux_i686', path_str='/opt/python/cp36-cp36m'),
- PythonConfiguration(version='3.7', identifier='cp37-manylinux_i686', path_str='/opt/python/cp37-cp37m'),
- PythonConfiguration(version='3.8', identifier='cp38-manylinux_i686', path_str='/opt/python/cp38-cp38'),
- PythonConfiguration(version='3.9', identifier='cp39-manylinux_i686', path_str='/opt/python/cp39-cp39'),
- PythonConfiguration(version='2.7', identifier='pp27-manylinux_x86_64', path_str='/opt/python/pp27-pypy_73'),
- PythonConfiguration(version='3.6', identifier='pp36-manylinux_x86_64', path_str='/opt/python/pp36-pypy36_pp73'),
- PythonConfiguration(version='3.7', identifier='pp37-manylinux_x86_64', path_str='/opt/python/pp37-pypy37_pp73'),
- PythonConfiguration(version='3.5', identifier='cp35-manylinux_aarch64', path_str='/opt/python/cp35-cp35m'),
- PythonConfiguration(version='3.6', identifier='cp36-manylinux_aarch64', path_str='/opt/python/cp36-cp36m'),
- PythonConfiguration(version='3.7', identifier='cp37-manylinux_aarch64', path_str='/opt/python/cp37-cp37m'),
- PythonConfiguration(version='3.8', identifier='cp38-manylinux_aarch64', path_str='/opt/python/cp38-cp38'),
- PythonConfiguration(version='3.9', identifier='cp39-manylinux_aarch64', path_str='/opt/python/cp39-cp39'),
- PythonConfiguration(version='3.5', identifier='cp35-manylinux_ppc64le', path_str='/opt/python/cp35-cp35m'),
- PythonConfiguration(version='3.6', identifier='cp36-manylinux_ppc64le', path_str='/opt/python/cp36-cp36m'),
- PythonConfiguration(version='3.7', identifier='cp37-manylinux_ppc64le', path_str='/opt/python/cp37-cp37m'),
- PythonConfiguration(version='3.8', identifier='cp38-manylinux_ppc64le', path_str='/opt/python/cp38-cp38'),
- PythonConfiguration(version='3.9', identifier='cp39-manylinux_ppc64le', path_str='/opt/python/cp39-cp39'),
- PythonConfiguration(version='3.5', identifier='cp35-manylinux_s390x', path_str='/opt/python/cp35-cp35m'),
- PythonConfiguration(version='3.6', identifier='cp36-manylinux_s390x', path_str='/opt/python/cp36-cp36m'),
- PythonConfiguration(version='3.7', identifier='cp37-manylinux_s390x', path_str='/opt/python/cp37-cp37m'),
- PythonConfiguration(version='3.8', identifier='cp38-manylinux_s390x', path_str='/opt/python/cp38-cp38'),
- PythonConfiguration(version='3.9', identifier='cp39-manylinux_s390x', path_str='/opt/python/cp39-cp39'),
- ]
+
+ full_python_configs = read_python_configs('linux')
+
+ python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
# return all configurations whose arch is in our `architectures` set,
# and match the build/skip rules
@@ -76,8 +48,6 @@ def get_python_configurations(
def build(options: BuildOptions) -> None:
- allowed_architectures_check("linux", options)
-
try:
subprocess.check_output(['docker', '--version'])
except Exception:
@@ -85,7 +55,7 @@ def build(options: BuildOptions) -> None:
'If you\'re building on Travis CI, add `services: [docker]` to your .travis.yml.'
'If you\'re building on Circle CI in Linux, add a `setup_remote_docker` step to your .circleci/config.yml',
file=sys.stderr)
- exit(2)
+ sys.exit(2)
assert options.manylinux_images is not None
python_configurations = get_python_configurations(options.build_selector, options.architectures)
@@ -136,7 +106,7 @@ def build(options: BuildOptions) -> None:
if config.identifier.startswith("pp"):
# Patch PyPy to make sure headers get installed into a venv
patch_version = '_27' if config.version == '2.7' else ''
- patch_path = Path(__file__).absolute().parent / 'resources' / f'pypy_venv{patch_version}.patch'
+ patch_path = resources_dir / f'pypy_venv{patch_version}.patch'
patch_docker_path = PurePath('/pypy_venv.patch')
docker.copy_into(patch_path, patch_docker_path)
try:
@@ -165,12 +135,12 @@ def build(options: BuildOptions) -> None:
which_python = docker.call(['which', 'python'], env=env, capture_output=True).strip()
if PurePath(which_python) != python_bin / 'python':
print("cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.", file=sys.stderr)
- exit(1)
+ sys.exit(1)
which_pip = docker.call(['which', 'pip'], env=env, capture_output=True).strip()
if PurePath(which_pip) != python_bin / 'pip':
print("cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.", file=sys.stderr)
- exit(1)
+ sys.exit(1)
if options.before_build:
log.step('Running before_build...')
@@ -210,7 +180,7 @@ def build(options: BuildOptions) -> None:
repaired_wheels = docker.glob(repaired_wheel_dir, '*.whl')
- if options.test_command:
+ if options.test_command and options.test_selector(config.identifier):
log.step('Testing wheel...')
# set up a virtual environment to install and test from, to make sure
@@ -260,7 +230,7 @@ def build(options: BuildOptions) -> None:
except subprocess.CalledProcessError as error:
log.step_end_with_error(f'Command {error.cmd} failed with code {error.returncode}. {error.stdout}')
troubleshoot(options.package_dir, error)
- exit(1)
+ sys.exit(1)
def troubleshoot(package_dir: Path, error: Exception) -> None:
diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py
index 7aed4c1a..8635f8ea 100644
--- a/cibuildwheel/macos.py
+++ b/cibuildwheel/macos.py
@@ -8,20 +8,21 @@ import tempfile
from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Set, Tuple, cast
+from .architecture import Architecture
from .environment import ParsedEnvironment
from .logger import log
from .typing import PathOrStr
from .util import (
- Architecture,
BuildOptions,
BuildSelector,
NonPlatformWheelError,
- allowed_architectures_check,
download,
get_build_verbosity_extra_flags,
get_pip_script,
install_certifi_script,
prepare_command,
+ read_python_configs,
+ resources_dir,
unwrap,
)
@@ -58,21 +59,10 @@ class PythonConfiguration(NamedTuple):
def get_python_configurations(build_selector: BuildSelector,
architectures: Set[Architecture]) -> List[PythonConfiguration]:
- python_configurations = [
- # CPython
- PythonConfiguration(version='2.7', identifier='cp27-macosx_x86_64', url='https://www.python.org/ftp/python/2.7.18/python-2.7.18-macosx10.9.pkg'),
- PythonConfiguration(version='3.5', identifier='cp35-macosx_x86_64', url='https://www.python.org/ftp/python/3.5.4/python-3.5.4-macosx10.6.pkg'),
- PythonConfiguration(version='3.6', identifier='cp36-macosx_x86_64', url='https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg'),
- PythonConfiguration(version='3.7', identifier='cp37-macosx_x86_64', url='https://www.python.org/ftp/python/3.7.9/python-3.7.9-macosx10.9.pkg'),
- PythonConfiguration(version='3.8', identifier='cp38-macosx_x86_64', url='https://www.python.org/ftp/python/3.8.7/python-3.8.7-macosx10.9.pkg'),
- PythonConfiguration(version='3.9', identifier='cp39-macosx_x86_64', url='https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg'),
- PythonConfiguration(version='3.9', identifier='cp39-macosx_arm64', url='https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg'),
- PythonConfiguration(version='3.9', identifier='cp39-macosx_universal2', url='https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg'),
- # PyPy
- PythonConfiguration(version='2.7', identifier='pp27-macosx_x86_64', url='https://downloads.python.org/pypy/pypy2.7-v7.3.3-osx64.tar.bz2'),
- PythonConfiguration(version='3.6', identifier='pp36-macosx_x86_64', url='https://downloads.python.org/pypy/pypy3.6-v7.3.3-osx64.tar.bz2'),
- PythonConfiguration(version='3.7', identifier='pp37-macosx_x86_64', url='https://downloads.python.org/pypy/pypy3.7-v7.3.3-osx64.tar.bz2'),
- ]
+
+ full_python_configs = read_python_configs('macos')
+
+ python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
# filter out configs that don't match any of the selected architectures
python_configurations = [c for c in python_configurations
@@ -163,7 +153,7 @@ def install_pypy(version: str, url: str) -> Path:
call(['tar', '-C', '/tmp', '-xf', downloaded_tar_bz2])
# Patch PyPy to make sure headers get installed into a venv
patch_version = '_27' if version == '2.7' else ''
- patch_path = Path(__file__).absolute().parent / 'resources' / f'pypy_venv{patch_version}.patch'
+ patch_path = resources_dir / f'pypy_venv{patch_version}.patch'
call(['patch', '--force', '-p1', '-d', installation_path, '-i', patch_path])
installation_bin_path = installation_path / 'bin'
@@ -211,7 +201,7 @@ def setup_python(python_configuration: PythonConfiguration,
which_python = subprocess.check_output(['which', 'python'], env=env, universal_newlines=True).strip()
if which_python != '/tmp/cibw_bin/python':
print("cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.", file=sys.stderr)
- exit(1)
+ sys.exit(1)
# install pip & wheel
call(['python', get_pip_script, *dependency_constraint_flags], env=env, cwd="/tmp")
@@ -221,7 +211,7 @@ def setup_python(python_configuration: PythonConfiguration,
which_pip = subprocess.check_output(['which', 'pip'], env=env, universal_newlines=True).strip()
if which_pip != '/tmp/cibw_bin/pip':
print("cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.", file=sys.stderr)
- exit(1)
+ sys.exit(1)
# Set MACOSX_DEPLOYMENT_TARGET to 10.9, if the user didn't set it.
# CPython 3.5 defaults to 10.6, and pypy defaults to 10.7, causing
@@ -254,8 +244,6 @@ def setup_python(python_configuration: PythonConfiguration,
def build(options: BuildOptions) -> None:
- allowed_architectures_check("macos", options)
-
temp_dir = Path(tempfile.mkdtemp(prefix='cibuildwheel'))
built_wheel_dir = temp_dir / 'built_wheel'
repaired_wheel_dir = temp_dir / 'repaired_wheel'
@@ -343,7 +331,7 @@ def build(options: BuildOptions) -> None:
log.step_end()
- if options.test_command:
+ if options.test_command and options.test_selector(config.identifier):
machine_arch = platform.machine()
testing_archs: List[str] = []
@@ -447,4 +435,4 @@ def build(options: BuildOptions) -> None:
log.build_end()
except subprocess.CalledProcessError as error:
log.step_end_with_error(f'Command {error.cmd} failed with code {error.returncode}. {error.stdout}')
- exit(1)
+ sys.exit(1)
diff --git a/cibuildwheel/resources/build-platforms.toml b/cibuildwheel/resources/build-platforms.toml
new file mode 100644
index 00000000..41d06d45
--- /dev/null
+++ b/cibuildwheel/resources/build-platforms.toml
@@ -0,0 +1,69 @@
+[linux]
+python_configurations = [
+ { identifier = "cp27-manylinux_x86_64", version = "2.7", path_str = "/opt/python/cp27-cp27m" },
+ { identifier = "cp27-manylinux_x86_64", version = "2.7", path_str = "/opt/python/cp27-cp27mu" },
+ { identifier = "cp35-manylinux_x86_64", version = "3.5", path_str = "/opt/python/cp35-cp35m" },
+ { identifier = "cp36-manylinux_x86_64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
+ { identifier = "cp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
+ { identifier = "cp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
+ { identifier = "cp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
+ { identifier = "cp27-manylinux_i686", version = "2.7", path_str = "/opt/python/cp27-cp27m" },
+ { identifier = "cp27-manylinux_i686", version = "2.7", path_str = "/opt/python/cp27-cp27mu" },
+ { identifier = "cp35-manylinux_i686", version = "3.5", path_str = "/opt/python/cp35-cp35m" },
+ { identifier = "cp36-manylinux_i686", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
+ { identifier = "cp37-manylinux_i686", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
+ { identifier = "cp38-manylinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
+ { identifier = "cp39-manylinux_i686", version = "3.9", path_str = "/opt/python/cp39-cp39" },
+ { identifier = "pp27-manylinux_x86_64", version = "2.7", path_str = "/opt/python/pp27-pypy_73" },
+ { identifier = "pp36-manylinux_x86_64", version = "3.6", path_str = "/opt/python/pp36-pypy36_pp73" },
+ { identifier = "pp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
+ { identifier = "cp35-manylinux_aarch64", version = "3.5", path_str = "/opt/python/cp35-cp35m" },
+ { identifier = "cp36-manylinux_aarch64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
+ { identifier = "cp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
+ { identifier = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
+ { identifier = "cp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
+ { identifier = "cp35-manylinux_ppc64le", version = "3.5", path_str = "/opt/python/cp35-cp35m" },
+ { identifier = "cp36-manylinux_ppc64le", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
+ { identifier = "cp37-manylinux_ppc64le", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
+ { identifier = "cp38-manylinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
+ { identifier = "cp39-manylinux_ppc64le", version = "3.9", path_str = "/opt/python/cp39-cp39" },
+ { identifier = "cp35-manylinux_s390x", version = "3.5", path_str = "/opt/python/cp35-cp35m" },
+ { identifier = "cp36-manylinux_s390x", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
+ { identifier = "cp37-manylinux_s390x", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
+ { identifier = "cp38-manylinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
+ { identifier = "cp39-manylinux_s390x", version = "3.9", path_str = "/opt/python/cp39-cp39" },
+]
+
+[macos]
+python_configurations = [
+ { identifier = "cp27-macosx_x86_64", version = "2.7", url = "https://www.python.org/ftp/python/2.7.18/python-2.7.18-macosx10.9.pkg" },
+ { identifier = "cp35-macosx_x86_64", version = "3.5", url = "https://www.python.org/ftp/python/3.5.4/python-3.5.4-macosx10.6.pkg" },
+ { identifier = "cp36-macosx_x86_64", version = "3.6", url = "https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg" },
+ { identifier = "cp37-macosx_x86_64", version = "3.7", url = "https://www.python.org/ftp/python/3.7.9/python-3.7.9-macosx10.9.pkg" },
+ { identifier = "cp38-macosx_x86_64", version = "3.8", url = "https://www.python.org/ftp/python/3.8.7/python-3.8.7-macosx10.9.pkg" },
+ { identifier = "cp39-macosx_x86_64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg" },
+ { identifier = "cp39-macosx_arm64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg" },
+ { identifier = "cp39-macosx_universal2", version = "3.9", url = "https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg" },
+ { identifier = "pp27-macosx_x86_64", version = "2.7", url = "https://downloads.python.org/pypy/pypy2.7-v7.3.3-osx64.tar.bz2" },
+ { identifier = "pp36-macosx_x86_64", version = "3.6", url = "https://downloads.python.org/pypy/pypy3.6-v7.3.3-osx64.tar.bz2" },
+ { identifier = "pp37-macosx_x86_64", version = "3.7", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.3-osx64.tar.bz2" },
+]
+
+[windows]
+python_configurations = [
+ { identifier = "cp27-win32", version = "2.7.18", arch = "32" },
+ { identifier = "cp27-win_amd64", version = "2.7.18", arch = "64" },
+ { identifier = "cp35-win32", version = "3.5.4", arch = "32" },
+ { identifier = "cp35-win_amd64", version = "3.5.4", arch = "64" },
+ { identifier = "cp36-win32", version = "3.6.8", arch = "32" },
+ { identifier = "cp36-win_amd64", version = "3.6.8", arch = "64" },
+ { identifier = "cp37-win32", version = "3.7.9", arch = "32" },
+ { identifier = "cp37-win_amd64", version = "3.7.9", arch = "64" },
+ { identifier = "cp38-win32", version = "3.8.7", arch = "32" },
+ { identifier = "cp38-win_amd64", version = "3.8.7", arch = "64" },
+ { identifier = "cp39-win32", version = "3.9.1", arch = "32" },
+ { identifier = "cp39-win_amd64", version = "3.9.1", arch = "64" },
+ { identifier = "pp27-win32", version = "2.7", arch = "32", url = "https://downloads.python.org/pypy/pypy2.7-v7.3.3-win32.zip" },
+ { identifier = "pp36-win32", version = "3.6", arch = "32", url = "https://downloads.python.org/pypy/pypy3.6-v7.3.3-win32.zip" },
+ { identifier = "pp37-win32", version = "3.7", arch = "32", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.3-win32.zip" },
+]
diff --git a/cibuildwheel/resources/constraints-python27.txt b/cibuildwheel/resources/constraints-python27.txt
index d5d41c2d..c9c443af 100644
--- a/cibuildwheel/resources/constraints-python27.txt
+++ b/cibuildwheel/resources/constraints-python27.txt
@@ -37,7 +37,7 @@ six==1.15.0
# virtualenv
typing==3.7.4.3
# via importlib-resources
-virtualenv==20.2.2
+virtualenv==20.4.0
# via -r cibuildwheel/resources/constraints.in
wheel==0.36.2
# via
diff --git a/cibuildwheel/resources/constraints-python35.txt b/cibuildwheel/resources/constraints-python35.txt
index d51a1299..b0df8f2e 100644
--- a/cibuildwheel/resources/constraints-python35.txt
+++ b/cibuildwheel/resources/constraints-python35.txt
@@ -18,7 +18,7 @@ importlib-resources==3.2.1
# via virtualenv
six==1.15.0
# via virtualenv
-virtualenv==20.2.2
+virtualenv==20.4.0
# via -r cibuildwheel/resources/constraints.in
wheel==0.36.2
# via
diff --git a/cibuildwheel/resources/constraints-python36.txt b/cibuildwheel/resources/constraints-python36.txt
index 2d912194..e510460e 100644
--- a/cibuildwheel/resources/constraints-python36.txt
+++ b/cibuildwheel/resources/constraints-python36.txt
@@ -12,15 +12,15 @@ distlib==0.3.1
# via virtualenv
filelock==3.0.12
# via virtualenv
-importlib-metadata==3.3.0
+importlib-metadata==3.4.0
# via virtualenv
-importlib-resources==4.1.1
+importlib-resources==5.1.0
# via virtualenv
six==1.15.0
# via virtualenv
typing-extensions==3.7.4.3
# via importlib-metadata
-virtualenv==20.2.2
+virtualenv==20.4.0
# via -r cibuildwheel/resources/constraints.in
wheel==0.36.2
# via
@@ -34,5 +34,5 @@ zipp==3.4.0
# The following packages are considered to be unsafe in a requirements file:
pip==20.3.3
# via -r cibuildwheel/resources/constraints.in
-setuptools==51.1.1
+setuptools==51.3.3
# via -r cibuildwheel/resources/constraints.in
diff --git a/cibuildwheel/resources/constraints-python37.txt b/cibuildwheel/resources/constraints-python37.txt
index 907b755b..181f65ee 100644
--- a/cibuildwheel/resources/constraints-python37.txt
+++ b/cibuildwheel/resources/constraints-python37.txt
@@ -12,13 +12,13 @@ distlib==0.3.1
# via virtualenv
filelock==3.0.12
# via virtualenv
-importlib-metadata==3.3.0
+importlib-metadata==3.4.0
# via virtualenv
six==1.15.0
# via virtualenv
typing-extensions==3.7.4.3
# via importlib-metadata
-virtualenv==20.2.2
+virtualenv==20.4.0
# via -r cibuildwheel/resources/constraints.in
wheel==0.36.2
# via
@@ -30,5 +30,5 @@ zipp==3.4.0
# The following packages are considered to be unsafe in a requirements file:
pip==20.3.3
# via -r cibuildwheel/resources/constraints.in
-setuptools==51.1.1
+setuptools==51.3.3
# via -r cibuildwheel/resources/constraints.in
diff --git a/cibuildwheel/resources/constraints-python38.txt b/cibuildwheel/resources/constraints-python38.txt
index 261ebccd..d31a4324 100644
--- a/cibuildwheel/resources/constraints-python38.txt
+++ b/cibuildwheel/resources/constraints-python38.txt
@@ -14,7 +14,7 @@ filelock==3.0.12
# via virtualenv
six==1.15.0
# via virtualenv
-virtualenv==20.2.2
+virtualenv==20.4.0
# via -r cibuildwheel/resources/constraints.in
wheel==0.36.2
# via
@@ -24,5 +24,5 @@ wheel==0.36.2
# The following packages are considered to be unsafe in a requirements file:
pip==20.3.3
# via -r cibuildwheel/resources/constraints.in
-setuptools==51.1.1
+setuptools==51.3.3
# via -r cibuildwheel/resources/constraints.in
diff --git a/cibuildwheel/resources/constraints-python39.txt b/cibuildwheel/resources/constraints-python39.txt
index 261ebccd..d31a4324 100644
--- a/cibuildwheel/resources/constraints-python39.txt
+++ b/cibuildwheel/resources/constraints-python39.txt
@@ -14,7 +14,7 @@ filelock==3.0.12
# via virtualenv
six==1.15.0
# via virtualenv
-virtualenv==20.2.2
+virtualenv==20.4.0
# via -r cibuildwheel/resources/constraints.in
wheel==0.36.2
# via
@@ -24,5 +24,5 @@ wheel==0.36.2
# The following packages are considered to be unsafe in a requirements file:
pip==20.3.3
# via -r cibuildwheel/resources/constraints.in
-setuptools==51.1.1
+setuptools==51.3.3
# via -r cibuildwheel/resources/constraints.in
diff --git a/cibuildwheel/resources/constraints.txt b/cibuildwheel/resources/constraints.txt
index 261ebccd..d31a4324 100644
--- a/cibuildwheel/resources/constraints.txt
+++ b/cibuildwheel/resources/constraints.txt
@@ -14,7 +14,7 @@ filelock==3.0.12
# via virtualenv
six==1.15.0
# via virtualenv
-virtualenv==20.2.2
+virtualenv==20.4.0
# via -r cibuildwheel/resources/constraints.in
wheel==0.36.2
# via
@@ -24,5 +24,5 @@ wheel==0.36.2
# The following packages are considered to be unsafe in a requirements file:
pip==20.3.3
# via -r cibuildwheel/resources/constraints.in
-setuptools==51.1.1
+setuptools==51.3.3
# via -r cibuildwheel/resources/constraints.in
diff --git a/cibuildwheel/resources/pinned_docker_images.cfg b/cibuildwheel/resources/pinned_docker_images.cfg
index 6aa7d846..2267f923 100644
--- a/cibuildwheel/resources/pinned_docker_images.cfg
+++ b/cibuildwheel/resources/pinned_docker_images.cfg
@@ -1,22 +1,22 @@
[x86_64]
-manylinux1 = quay.io/pypa/manylinux1_x86_64:2020-12-31-84e1e23
-manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2020-12-31-4928808
-manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2020-12-31-56195b3
+manylinux1 = quay.io/pypa/manylinux1_x86_64:2021-01-11-28ab838
+manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2021-01-12-ff3830d
+manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2021-01-12-c8250d8
[i686]
-manylinux1 = quay.io/pypa/manylinux1_i686:2020-12-31-84e1e23
-manylinux2010 = quay.io/pypa/manylinux2010_i686:2020-12-31-4928808
-manylinux2014 = quay.io/pypa/manylinux2014_i686:2020-12-31-56195b3
+manylinux1 = quay.io/pypa/manylinux1_i686:2021-01-11-28ab838
+manylinux2010 = quay.io/pypa/manylinux2010_i686:2021-01-12-ff3830d
+manylinux2014 = quay.io/pypa/manylinux2014_i686:2021-01-12-c8250d8
[pypy_x86_64]
manylinux2010 = pypywheels/manylinux2010-pypy_x86_64:2020-12-11-f1e0e80
[aarch64]
-manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2020-12-31-56195b3
+manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2021-01-12-c8250d8
[ppc64le]
-manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2020-12-31-56195b3
+manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2021-01-12-c8250d8
[s390x]
-manylinux2014 = quay.io/pypa/manylinux2014_s390x:2020-12-31-56195b3
+manylinux2014 = quay.io/pypa/manylinux2014_s390x:2021-01-12-c8250d8
diff --git a/cibuildwheel/typing.py b/cibuildwheel/typing.py
index ffe8afb6..9a650d54 100644
--- a/cibuildwheel/typing.py
+++ b/cibuildwheel/typing.py
@@ -1,6 +1,16 @@
import os
import subprocess
-from typing import TYPE_CHECKING, Union
+import sys
+from typing import TYPE_CHECKING, NoReturn, Set, Union
+
+if sys.version_info < (3, 8):
+ from typing_extensions import Final, Literal, TypedDict
+else:
+ from typing import Final, Literal, TypedDict
+
+
+__all__ = ("Final", "Literal", "TypedDict", "Set", "Union", "PopenBytes", "PathOrStr", "PlatformName", "PLATFORMS", "assert_never")
+
if TYPE_CHECKING:
PopenBytes = subprocess.Popen[bytes]
@@ -8,3 +18,11 @@ if TYPE_CHECKING:
else:
PopenBytes = subprocess.Popen
PathOrStr = Union[str, "os.PathLike[str]"]
+
+
+PlatformName = Literal["linux", "macos", "windows"]
+PLATFORMS: Final[Set[PlatformName]] = {"linux", "macos", "windows"}
+
+
+def assert_never(value: NoReturn) -> NoReturn:
+ assert False, f'Unhandled value: {value} ({type(value).__name__})' # noqa: B011
diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py
index 556e3568..9e0bc5d8 100644
--- a/cibuildwheel/util.py
+++ b/cibuildwheel/util.py
@@ -1,26 +1,26 @@
-import functools
+import fnmatch
+import itertools
import os
-import platform as platform_module
import re
import ssl
-import sys
import textwrap
import urllib.request
from enum import Enum
-from fnmatch import fnmatch
from pathlib import Path
from time import sleep
from typing import Dict, List, NamedTuple, Optional, Set
+import bracex
import certifi
+import toml
+from .architecture import Architecture
from .environment import ParsedEnvironment
-from .typing import PathOrStr
+from .typing import PathOrStr, PlatformName
-if sys.version_info < (3, 8):
- from typing_extensions import Literal
-else:
- from typing import Literal
+resources_dir = Path(__file__).parent / 'resources'
+get_pip_script = resources_dir / 'get-pip.py'
+install_certifi_script = resources_dir / "install_certifi.py"
def prepare_command(command: str, **kwargs: PathOrStr) -> str:
@@ -42,21 +42,45 @@ def get_build_verbosity_extra_flags(level: int) -> List[str]:
return []
-class BuildSelector:
- def __init__(self, build_config: str, skip_config: str):
+def read_python_configs(config: PlatformName) -> List[Dict[str, str]]:
+ input_file = resources_dir / 'build-platforms.toml'
+ loaded_file = toml.load(input_file)
+ results: List[Dict[str, str]] = list(loaded_file[config]['python_configurations'])
+ return results
+
+
+class IdentifierSelector:
+ """
+ This class holds a set of build/skip patterns. You call an instance with a
+ build identifier, and it returns True if that identifier should be
+ included.
+ """
+ def __init__(self, *, build_config: str, skip_config: str):
self.build_patterns = build_config.split()
self.skip_patterns = skip_config.split()
def __call__(self, build_id: str) -> bool:
- def match_any(patterns: List[str]) -> bool:
- return any(fnmatch(build_id, pattern) for pattern in patterns)
- return match_any(self.build_patterns) and not match_any(self.skip_patterns)
+ build_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in self.build_patterns)
+ skip_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in self.skip_patterns)
+
+ build: bool = any(fnmatch.fnmatch(build_id, pat) for pat in build_patterns)
+ skip: bool = any(fnmatch.fnmatch(build_id, pat) for pat in skip_patterns)
+ return build and not skip
def __repr__(self) -> str:
if not self.skip_patterns:
- return f'BuildSelector({" ".join(self.build_patterns)!r})'
+ return f'{self.__class__.__name__}({" ".join(self.build_patterns)!r})'
else:
- return f'BuildSelector({" ".join(self.build_patterns)!r} - {" ".join(self.skip_patterns)!r})'
+ return f'{self.__class__.__name__}({" ".join(self.build_patterns)!r} - {" ".join(self.skip_patterns)!r})'
+
+
+class BuildSelector(IdentifierSelector):
+ pass
+
+
+class TestSelector(IdentifierSelector):
+ def __init__(self, *, skip_config: str):
+ super().__init__(build_config="*", skip_config=skip_config)
# Taken from https://stackoverflow.com/a/107717
@@ -131,60 +155,6 @@ class DependencyConstraints:
return f'{self.__class__.__name__}{self.base_file_path!r})'
-@functools.total_ordering
-class Architecture(Enum):
- value: str
-
- # mac/linux archs
- x86_64 = 'x86_64'
-
- # linux archs
- i686 = 'i686'
- aarch64 = 'aarch64'
- ppc64le = 'ppc64le'
- s390x = 's390x'
-
- # mac archs
- universal2 = 'universal2'
- arm64 = 'arm64'
-
- # windows archs
- x86 = 'x86'
- AMD64 = 'AMD64'
-
- # Allow this to be sorted
- def __lt__(self, other: "Architecture") -> bool:
- return self.value < other.value
-
- @staticmethod
- def parse_config(config: str, platform: str) -> 'Set[Architecture]':
- result = set()
- for arch_str in re.split(r'[\s,]+', config):
- if arch_str == 'auto':
- result |= Architecture.auto_archs(platform=platform)
- else:
- result.add(Architecture(arch_str))
- return result
-
- @staticmethod
- def auto_archs(platform: str) -> 'Set[Architecture]':
- native_architecture = Architecture(platform_module.machine())
- result = {native_architecture}
-
- if platform == 'linux' and native_architecture == Architecture.x86_64:
- # x86_64 machines can run i686 docker containers
- result.add(Architecture.i686)
-
- if platform == 'windows' and native_architecture == Architecture.AMD64:
- result.add(Architecture.x86)
-
- if platform == 'macos' and native_architecture == Architecture.arm64:
- # arm64 can build and test both archs of a universal2 wheel.
- result.add(Architecture.universal2)
-
- return result
-
-
class BuildOptions(NamedTuple):
package_dir: Path
output_dir: Path
@@ -197,17 +167,13 @@ class BuildOptions(NamedTuple):
manylinux_images: Optional[Dict[str, str]]
dependency_constraints: Optional[DependencyConstraints]
test_command: Optional[str]
+ test_selector: TestSelector
before_test: Optional[str]
test_requires: List[str]
test_extras: str
build_verbosity: int
-resources_dir = Path(__file__).resolve().parent / 'resources'
-get_pip_script = resources_dir / 'get-pip.py'
-install_certifi_script = resources_dir / "install_certifi.py"
-
-
class NonPlatformWheelError(Exception):
def __init__(self) -> None:
message = textwrap.dedent('''
@@ -258,36 +224,6 @@ def detect_ci_provider() -> Optional[CIProvider]:
return None
-PRETTY_NAMES = {'linux': 'Linux', 'macos': 'macOS', 'windows': 'Windows'}
-
-ALLOWED_ARCHITECTURES = {
- 'linux': {Architecture.x86_64, Architecture.i686, Architecture.aarch64, Architecture.ppc64le, Architecture.s390x},
- 'macos': {Architecture.x86_64, Architecture.universal2, Architecture.arm64},
- 'windows': {Architecture.AMD64, Architecture.x86},
-}
-
-
-def allowed_architectures_check(
- name: Literal['linux', 'macos', 'windows'],
- options: BuildOptions,
-) -> None:
-
- allowed_architectures = ALLOWED_ARCHITECTURES[name]
-
- msg = f'{PRETTY_NAMES[name]} only supports {sorted(allowed_architectures)} at the moment.'
-
- if name != 'linux':
- msg += ' If you want to set emulation architectures on Linux, use CIBW_ARCHS_LINUX instead.'
-
- if not options.architectures <= allowed_architectures:
- msg = f'Invalid archs option {options.architectures}. ' + msg
- raise ValueError(msg)
-
- if not options.architectures:
- msg = 'Empty archs option set. ' + msg
- raise ValueError(msg)
-
-
def unwrap(text: str) -> str:
'''
Unwraps multi-line text to a single line
diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py
index e70e1571..429150eb 100644
--- a/cibuildwheel/windows.py
+++ b/cibuildwheel/windows.py
@@ -9,19 +9,19 @@ from zipfile import ZipFile
import toml
+from .architecture import Architecture
from .environment import ParsedEnvironment
from .logger import log
from .typing import PathOrStr
from .util import (
- Architecture,
BuildOptions,
BuildSelector,
NonPlatformWheelError,
- allowed_architectures_check,
download,
get_build_verbosity_extra_flags,
get_pip_script,
prepare_command,
+ read_python_configs,
)
IS_RUNNING_ON_AZURE = Path('C:\\hostedtoolcache').exists()
@@ -52,36 +52,25 @@ class PythonConfiguration(NamedTuple):
version: str
arch: str
identifier: str
- url: Optional[str]
+ url: Optional[str] = None
-def get_python_configurations(build_selector: BuildSelector, architectures: Set[Architecture]) -> List[PythonConfiguration]:
+def get_python_configurations(
+ build_selector: BuildSelector,
+ architectures: Set[Architecture],
+) -> List[PythonConfiguration]:
+
+ full_python_configs = read_python_configs('windows')
+
+ python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
+
map_arch = {
'32': Architecture.x86,
'64': Architecture.AMD64,
}
- python_configurations = [
- # CPython
- PythonConfiguration(version='2.7.18', arch='32', identifier='cp27-win32', url=None),
- PythonConfiguration(version='2.7.18', arch='64', identifier='cp27-win_amd64', url=None),
- PythonConfiguration(version='3.5.4', arch='32', identifier='cp35-win32', url=None),
- PythonConfiguration(version='3.5.4', arch='64', identifier='cp35-win_amd64', url=None),
- PythonConfiguration(version='3.6.8', arch='32', identifier='cp36-win32', url=None),
- PythonConfiguration(version='3.6.8', arch='64', identifier='cp36-win_amd64', url=None),
- PythonConfiguration(version='3.7.9', arch='32', identifier='cp37-win32', url=None),
- PythonConfiguration(version='3.7.9', arch='64', identifier='cp37-win_amd64', url=None),
- PythonConfiguration(version='3.8.7', arch='32', identifier='cp38-win32', url=None),
- PythonConfiguration(version='3.8.7', arch='64', identifier='cp38-win_amd64', url=None),
- PythonConfiguration(version='3.9.1', arch='32', identifier='cp39-win32', url=None),
- PythonConfiguration(version='3.9.1', arch='64', identifier='cp39-win_amd64', url=None),
- # PyPy
- PythonConfiguration(version='2.7', arch='32', identifier='pp27-win32', url='https://downloads.python.org/pypy/pypy2.7-v7.3.3-win32.zip'),
- PythonConfiguration(version='3.6', arch='32', identifier='pp36-win32', url='https://downloads.python.org/pypy/pypy3.6-v7.3.3-win32.zip'),
- PythonConfiguration(version='3.7', arch='32', identifier='pp37-win32', url='https://downloads.python.org/pypy/pypy3.7-v7.3.3-win32.zip'),
- ]
-
- if IS_RUNNING_ON_TRAVIS:
+ custom_compiler = os.environ.get('DISTUTILS_USE_SDK') and os.environ.get('MSSdk')
+ if IS_RUNNING_ON_TRAVIS and not custom_compiler:
# cannot install VCForPython27.msi which is needed for compiling C software
# try with (and similar): msiexec /i VCForPython27.msi ALLUSERS=1 ACCEPT=YES /passive
python_configurations = [c for c in python_configurations if not c.version.startswith('2.7')]
@@ -164,7 +153,7 @@ def setup_python(python_configuration: PythonConfiguration, dependency_constrain
where_python = subprocess.check_output(['where', 'python'], env=env, universal_newlines=True).splitlines()[0].strip()
if where_python != str(installation_path / 'python.exe'):
print("cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.", file=sys.stderr)
- exit(1)
+ sys.exit(1)
# make sure pip is installed
if not (installation_path / 'Scripts' / 'pip.exe').exists():
@@ -173,7 +162,7 @@ def setup_python(python_configuration: PythonConfiguration, dependency_constrain
where_pip = subprocess.check_output(['where', 'pip'], env=env, universal_newlines=True).splitlines()[0].strip()
if where_pip.strip() != str(installation_path / 'Scripts' / 'pip.exe'):
print("cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.", file=sys.stderr)
- exit(1)
+ sys.exit(1)
log.step('Installing build tools...')
@@ -210,15 +199,13 @@ def pep_518_cp35_workaround(package_dir: Path, env: Dict[str, str]) -> None:
log.step('Performing PEP518 workaround...')
with tempfile.TemporaryDirectory() as d:
reqfile = Path(d) / "requirements.txt"
- with reqfile.open("w") as f:
+ with reqfile.open('w') as f:
for r in requirements:
print(r, file=f)
call(['pip', 'install', '-r', reqfile], env=env)
def build(options: BuildOptions) -> None:
- allowed_architectures_check("windows", options)
-
temp_dir = Path(tempfile.mkdtemp(prefix='cibuildwheel'))
built_wheel_dir = temp_dir / 'built_wheel'
repaired_wheel_dir = temp_dir / 'repaired_wheel'
@@ -288,7 +275,7 @@ def build(options: BuildOptions) -> None:
repaired_wheel = next(repaired_wheel_dir.glob('*.whl'))
- if options.test_command:
+ if options.test_command and options.test_selector(config.identifier):
log.step('Testing wheel...')
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
@@ -341,4 +328,4 @@ def build(options: BuildOptions) -> None:
log.build_end()
except subprocess.CalledProcessError as error:
log.step_end_with_error(f'Command {error.cmd} failed with code {error.returncode}. {error.stdout}')
- exit(1)
+ sys.exit(1)
diff --git a/docs/data/projects.yml b/docs/data/projects.yml
index 19acaf94..37a3114d 100644
--- a/docs/data/projects.yml
+++ b/docs/data/projects.yml
@@ -229,3 +229,61 @@
ci: [travisci]
os: [windows, apple, linux]
notes: Dependency injection framework for Python, uses Windows TravisCI
+
+- name: numexpr
+ gh: pydata/numexpr
+ ci: [github, travisci]
+ os: [windows, apple, linux]
+
+- name: PyTables
+ gh: PyTables/PyTables
+ ci: [github]
+ os: [windows, apple, linux]
+
+- name: dd-trace-py
+ gh: DataDog/dd-trace-py
+ ci: [github]
+ os: [windows, apple, linux]
+ notes: Uses custom alternate arch emulation on GitHub
+
+- name: Tokenizer
+ gh: OpenNMT/Tokenizer
+ ci: [github, travisci]
+ os: [apple, linux]
+
+- name: matrixprofile
+ gh: matrix-profile-foundation/matrixprofile
+ ci: [travisci]
+ os: [windows, apple, linux]
+
+- name: cyvcf2
+ gh: brentp/cyvcf2
+ ci: [github]
+ os: [apple, linux]
+
+- name: CTranslate2
+ gh: OpenNMT/CTranslate2
+ ci: [github]
+ os: [apple, linux]
+ notes: Includes libraries from the [Intel oneAPI toolkit](https://software.intel.com/content/www/us/en/develop/tools/oneapi/base-toolkit.html). The Linux wheels also include CUDA libraries for GPU execution.
+
+- name: MyPy
+ gh: mypyc/mypy_mypyc-wheels
+ stars: python/mypy
+ ci: [github]
+ os: [apple, linux, windows]
+ notes: MyPyC, the compiled component of MyPy.
+
+- name: Imagecodecs (fork)
+ gh: czaki/imagecodecs_build
+ stars: cgohlke/imagecodecs
+ ci: [azurepipelines]
+ ci_config: azure-pipelines.yaml
+ os: [apple, linux]
+ notes: Over 20 external dependencies in compiled libraries, custom docker image, `libomp`, `openblas` and `install_name_tool` for macOS.
+
+- name: ruptures
+ gh: deepcharles/ruptures
+ ci: [github]
+ os: [apple, linux, windows]
+ notes: Extensive Cython + NumPy [pyproject.toml](https://github.com/deepcharles/ruptures/blob/master/pyproject.toml) example.
diff --git a/docs/faq.md b/docs/faq.md
index 07dc171e..799059d9 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -113,10 +113,10 @@ There are two suggested methods for keeping cibuildwheel up to date that instead
If you use GitHub Actions for builds, you can use cibuildwheel as an action:
```yaml
-uses: joerick/cibuildwheel@v1.7.4
+uses: joerick/cibuildwheel@v1.8.0
```
-This is a composite step that just installs and runs cibuildwheel. You can set command-line options as `with:` parameters, and use `env:` as normal.
+This is a composite step that just runs cibuildwheel using pipx. You can set command-line options as `with:` parameters, and use `env:` as normal.
Then, your `dependabot.yml` file could look like this:
@@ -139,7 +139,7 @@ The second option, and the only one that supports other CI systems, is using a `
```bash
# requirements-cibw.txt
-cibuildwheel==1.7.4
+cibuildwheel==1.8.0
```
Then your install step would have `python -m pip install -r requirements-cibw.txt` in it. Your `dependabot.yml` file could look like this:
diff --git a/docs/options.md b/docs/options.md
index ccfc4665..3a1c0cd8 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -75,7 +75,7 @@ Space-separated list of builds to build and skip. Each build has an identifier l
When both options are specified, both conditions are applied and only builds with a tag that matches `CIBW_BUILD` and does not match `CIBW_SKIP` will be built.
-When setting the options, you can use shell-style globbing syntax (as per [`fnmatch`](https://docs.python.org/3/library/fnmatch.html)). All the build identifiers supported by cibuildwheel are shown below:
+When setting the options, you can use shell-style globbing syntax, as per [`fnmatch`](https://docs.python.org/3/library/fnmatch.html) with the addition of curly bracket syntax `{option1,option2}`, provided by [`bracex`](https://pypi.org/project/bracex/). All the build identifiers supported by cibuildwheel are shown below: