diff --git a/.circleci/config.yml b/.circleci/config.yml
index 62cefcab..3a657f7e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,7 +1,7 @@
version: 2
jobs:
- osx-python3.6:
+ osx-python3.9:
macos:
xcode: 12.5.1
environment:
@@ -17,9 +17,9 @@ jobs:
command: venv/bin/python ./bin/run_tests.py
no_output_timeout: 30m
- linux-python3.6:
+ linux-python3.9:
docker:
- - image: circleci/python:3.6
+ - image: circleci/python:3.9
environment:
PYTHON: python3
# Temporarily restrict the tests that are run on CircleCI to prevent
@@ -41,5 +41,5 @@ workflows:
version: 2
all-tests:
jobs:
- - osx-python3.6
- - linux-python3.6
+ - osx-python3.9
+ - linux-python3.9
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index ce05ac5c..6fddca0d 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,6 +5,3 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- ignore:
- - dependency-name: "actions/*"
- update-types: ["version-update:semver-minor", "version-update:semver-patch"]
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a588b526..6861e96f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -20,7 +20,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- - uses: actions/setup-python@v3
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.x"
- uses: pre-commit/action@v3.0.0
- name: Check manifest
run: pipx run nox -s check_manifest
@@ -35,40 +37,52 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest, windows-latest, macos-11]
+ os: [ubuntu-18.04, windows-latest, macos-11]
python_version: ['3.10']
timeout-minutes: 180
steps:
- uses: actions/checkout@v3
- - uses: actions/setup-python@v3
+ - uses: actions/setup-python@v4
name: Install Python ${{ matrix.python_version }}
with:
python-version: ${{ matrix.python_version }}
+ # Install podman on this CI instance for podman tests on linux
+ # Snippet from: https://github.com/redhat-actions/podman-login/blob/main/.github/workflows/example.yml
+ - name: Install latest podman
+ if: runner.os == 'Linux'
+ run: |
+ . /etc/os-release
+ echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
+ curl -sSfL "https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/Release.key" | sudo apt-key add -
+ sudo apt-get update
+ sudo apt-get -y upgrade
+ sudo apt-get -y install podman
+
- name: Install dependencies
run: |
python -m pip install ".[test]"
- - name: Sample build
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Sample build')"
- run: |
- python bin/sample_build.py
-
- - name: Get some sample wheels
+ - name: Generate a sample project
run: |
python -m test.test_projects test.test_0_basic.basic_project sample_proj
- cibuildwheel --output-dir wheelhouse sample_proj
+
+ - name: Run a sample build (GitHub Action)
+ uses: ./
+ with:
+ package-dir: sample_proj
+ output-dir: wheelhouse
env:
CIBW_ARCHS_MACOS: x86_64 universal2 arm64
- uses: actions/upload-artifact@v3
with:
- name: sample_wheels
+ name: sample_wheels_action
path: wheelhouse
- name: Test cibuildwheel
run: |
- python ./bin/run_tests.py
+ python ./bin/run_tests.py --run-podman
test-emulated:
name: Test emulated cibuildwheel using qemu
@@ -77,7 +91,9 @@ jobs:
timeout-minutes: 180
steps:
- uses: actions/checkout@v3
- - uses: actions/setup-python@v3
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install ".[test]"
diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml
index 40af3595..208b36c4 100644
--- a/.github/workflows/update-dependencies.yml
+++ b/.github/workflows/update-dependencies.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- - uses: actions/setup-python@v3
+ - uses: actions/setup-python@v4
with:
python-version: "3.11-dev"
- uses: excitedleigh/setup-nox@v2.1.0
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 679254fd..c3e480da 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,20 +14,15 @@ repos:
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
- rev: v2.34.0
+ rev: v2.37.2
hooks:
- id: pyupgrade
- name: PyUpgrade 3.6+
- args: ["--py36-plus"]
- exclude: ^bin/
- - id: pyupgrade
- name: PyUpgrade 3.7+ on bin
- exclude: ^(cibuildwheel|unit_test|test)/
args: ["--py37-plus"]
+ exclude: ^cibuildwheel/resources/.*py$
# Autoremoves unused imports
- repo: https://github.com/hadialqattan/pycln
- rev: v1.3.3
+ rev: v2.0.4
hooks:
- id: pycln
args: [--all]
@@ -37,30 +32,33 @@ repos:
rev: 5.10.1
hooks:
- id: isort
+ args: ["-a", "from __future__ import annotations"]
+ exclude: ^cibuildwheel/resources/.*py$
- repo: https://github.com/psf/black
- rev: 22.3.0
+ rev: 22.6.0
hooks:
- id: black
- repo: https://github.com/asottile/setup-cfg-fmt
- rev: v1.20.1
+ rev: v1.20.2
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.961
+ rev: v0.971
hooks:
- id: mypy
- name: mypy 3.6 on cibuildwheel/
- exclude: ^(bin|cibuildwheel/resources|docs)/.*py$
- args: ["--python-version=3.6"]
+ name: mypy 3.7 on cibuildwheel/
+ exclude: ^cibuildwheel/resources/.*py$
+ args: ["--python-version=3.7"]
additional_dependencies: &mypy-dependencies
- nox
- packaging>=21.0
- pygithub
- rich
- tomli
+ - tomli_w
- types-certifi
- types-click
- types-dataclasses
@@ -69,11 +67,6 @@ repos:
- types-requests
- bracex
- dataclasses
- - id: mypy
- name: mypy 3.7+ on bin/
- files: ^((bin|docs)/.*py)$
- args: ["--python-version=3.7"]
- additional_dependencies: *mypy-dependencies
- id: mypy
name: mypy 3.10
args: ["--python-version=3.10"]
diff --git a/.travis.yml b/.travis.yml
index aad9e5a5..4256a69b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,21 +8,21 @@ branches:
jobs:
include:
- - name: Linux | x86_64 + i686 | Python 3.6
- python: 3.6
+ - name: Linux | x86_64 + i686 | Python 3.7
+ python: 3.7
services: docker
env: PYTHON=python
- - name: Linux | arm64 | Python 3.6
- python: 3.6
+ - name: Linux | arm64 | Python 3.7
+ python: 3.7
services: docker
arch: arm64-graviton2
group: edge
virt: vm
env: PYTHON=python
- - name: Linux | ppc64le | Python 3.6
- python: 3.6
+ - name: Linux | ppc64le | Python 3.7
+ python: 3.7
services: docker
arch: ppc64le
allow_failure: True
@@ -32,18 +32,18 @@ jobs:
# c.f. https://travis-ci.community/t/running-out-of-disk-space-quota-when-using-docker-on-ppc64le/11634
- PYTEST_ADDOPTS='-k "not test_manylinuxXXXX_only"'
- - name: Windows | x86_64 | Python 3.6
+ - name: Windows | x86_64 | Python 3.7
os: windows
language: shell
before_install:
- - choco upgrade python3 -y --version 3.6.8 --limit-output
+ - choco upgrade python3 -y --version 3.7.9 --limit-output
# Update root certificates to fix SSL error; see http://www.chawn.com/RootCerts.htm
- powershell "md C:\temp\certs; CertUtil -generateSSTFromWU C:\temp\certs\RootStore.sst; Get-ChildItem -Path C:\\temp\certs\Rootstore.sst | Import-Certificate -CertStoreLocation Cert:\\LocalMachine\\Root\\ | out-null"
env:
- - PYTHON=C:\\Python36\\python
+ - PYTHON=C:\\Python37\\python
- - name: Linux | s390x | Python 3.6
- python: 3.6
+ - name: Linux | s390x | Python 3.7
+ python: 3.7
services: docker
arch: s390x
env: PYTHON=python
diff --git a/CI.md b/CI.md
index 8ac4ada5..76167c35 100644
--- a/CI.md
+++ b/CI.md
@@ -1,11 +1,11 @@
This is a summary of the Python versions and platforms covered by the different CI platforms:
-| | 3.6 | 3.7 | 3.8 |
-|----------|------------------------------|-----------------------------|------------------|
-| Linux | Travis CI / CircleCI | AppVeyor¹ / GitHub Actions | Azure Pipelines |
-| macOS | CircleCI | AppVeyor¹ / GitHub Actions | Azure Pipelines |
-| Windows | Travis CI / Azure Pipelines | AppVeyor¹ / GitHub Actions | Azure Pipelines |
+| | 3.7 | 3.8 | 3.9 | 3.10 |
+|----------|-----------------------|---------------------------|----------|----------------|
+| Linux | AppVeyor¹ / Travis CI | Azure Pipelines / GitLab | CircleCI | GitHub Actions |
+| macOS | AppVeyor¹ / Travis CI | Azure Pipelines | CircleCI | GitHub Actions |
+| Windows | AppVeyor¹ / Travis CI | Azure Pipelines | | GitHub Actions |
> ¹ AppVeyor only runs the "basic" test to reduce load.
-Non-x86 architectures are covered on Travis CI using Python 3.6.
+Non-x86 architectures are covered on Travis CI using Python 3.7.
diff --git a/README.md b/README.md
index e66a835e..fecef336 100644
--- a/README.md
+++ b/README.md
@@ -80,16 +80,16 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-20.04, windows-2019, macOS-10.15]
+ os: [ubuntu-20.04, windows-2019, macOS-11]
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
# Used to host cibuildwheel
- - uses: actions/setup-python@v2
+ - uses: actions/setup-python@v3
- name: Install cibuildwheel
- run: python -m pip install cibuildwheel==2.6.1
+ run: python -m pip install cibuildwheel==2.8.1
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
@@ -97,7 +97,7 @@ jobs:
# env:
# CIBW_SOME_OPTION: value
- - uses: actions/upload-artifact@v2
+ - uses: actions/upload-artifact@v3
with:
path: ./wheelhouse/*.whl
```
@@ -130,6 +130,7 @@ Options
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel |
| | [`CIBW_MANYLINUX_*_IMAGE`
`CIBW_MUSLLINUX_*_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images |
+| | [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.readthedocs.io/en/stable/options/#container-engine) | Specify which container engine to use when building Linux wheels |
| | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses |
| **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel |
| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
@@ -203,6 +204,37 @@ Changelog
+### v2.8.1
+
+_18 July 2022_
+
+- 🐛 Fix a bug when building CPython 3.8 wheels on an Apple Silicon machine where testing would always fail. cibuildwheel will no longer attempt to test the arm64 part of CPython 3.8 wheels because we use the x86_64 installer of CPython 3.8 due to its macOS system version backward-compatibility. See [#1169](https://github.com/pypa/cibuildwheel/pull/1169) for more details. (#1171)
+- 🛠 Update the prerelease CPython 3.11 to 3.11.0b4. (#1180)
+- 🛠 The GitHub Action will ensure a compatible version of Python is installed on the runner (#1114)
+- 📚 A few docs improvements
+
+### v2.8.0
+
+_5 July 2022_
+
+- ✨ You can now run cibuildwheel on Podman, as an alternate container engine to Docker (which remains the default). This is useful in environments where a Docker daemon isn't available, for example, it can be run inside a Docker container, or without root access. To use Podman, set the [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.readthedocs.io/en/stable/options/#container-engine) option. (#966)
+- ✨ Adds support for building `py3-none-{platform}` wheels. This works the same as ABI3 - wheels won't be rebuilt, but tests will still be run across all selected versions of Python.
+
+ > These wheels contain native extension code, but don't use the Python APIs. Typically, they're bridged to Python using a FFI module like [ctypes](https://docs.python.org/3/library/ctypes.html) or [cffi](https://cffi.readthedocs.io/en/latest/). Because they don't use Python ABI, the wheels are more compatible - they work across many Python versions.
+
+ Check out this [example ctypes project](https://github.com/joerick/python-ctypes-package-sample) to see an example of how it works. (#1151)
+- 🛠 cibuildwheel will now error if multiple builds in a single run produce the same wheel filename, as this indicates a misconfiguration. (#1152)
+- 📚 A few docs improvements and updates to keep things up-to-date.
+
+### v2.7.0
+
+_17 June 2022_
+
+- 🌟 Added support for the new `manylinux_2_28` images. These new images are based on AlmaLinux, the community-driven successor to CentOS, unlike manylinux_2_24, which was based on Debian. To build on these images, set your [`CIBW_MANYLINUX_*_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#linux-image) option to `manylinux_2_28`. (#1026)
+- 🐛 Fix a bug where tests were not being run on CPython 3.11 (when CIBW_PRERELEASE_PYTHONS was set) (#1138)
+- ✨ You can now build Linux wheels on Windows, as long as you have Docker installed and set to 'Linux containers' (#1117)
+- 🐛 Fix a bug on macOS that caused cibuildwheel to crash trying to overwrite a previously-built wheel of the same name. (#1129)
+
### v2.6.1
_7 June 2022_
@@ -216,36 +248,6 @@ _25 May 2022_
- 🌟 Added the ability to test building wheels on CPython 3.11! Because CPython 3.11 is in beta, these wheels should not be distributed, because they might not be compatible with the final release, but it's available to build for testing purposes. Use the flag [`--prerelease-pythons` or `CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#prerelease-pythons) to test. This version of cibuildwheel includes CPython 3.11.0b1. (#1109)
- 📚 Added an interactive diagram showing how cibuildwheel works to the [docs](https://cibuildwheel.readthedocs.io/en/stable/#how-it-works) (#1100)
-### v2.5.0
-
-_29 April 2022_
-
-- ✨ Added support for building ABI3 wheels. cibuildwheel will now recognise when an ABI3 wheel was produced, and skip subsequent build steps where the previously built wheel is compatible. Tests still will run on all selected versions of Python, using the ABI3 wheel. Check [this entry](https://cibuildwheel.readthedocs.io/en/stable/faq/#abi3) in the docs for more info. (#1091)
-- ✨ You can now build wheels directly from sdist archives, in addition to source directories. Just call cibuildwheel with an sdist argument on the command line, like `cibuildwheel mypackage-1.0.0.tar.gz`. For more details, check the [`--help` output](https://cibuildwheel.readthedocs.io/en/stable/options/#command-line) (#1096)
-- 🐛 Fix a bug where cibuildwheel would crash when no builds are selected and `--allow-empty` is passed (#1086)
-- 🐛 Workaround a permissions issue on Linux relating to newer versions of git and setuptools_scm (#1095)
-- 📚 Minor docs improvements
-
-### v2.4.0
-
-_2 April 2022_
-
-- ✨ cibuildwheel now supports running locally on Windows and macOS (as well as Linux). On macOS, you'll have to install the versions of Pythons that you want to use from Python.org, and cibuildwheel will use them. On Windows, cibuildwheel will install it's own versions of Python. Check out [the documentation](https://cibuildwheel.readthedocs.io/en/stable/setup/#local) for instructions. (#974)
-- ✨ Added support for building PyPy 3.9 wheels. (#1031)
-- ✨ Listing at the end of the build now displays the size of each wheel (#975)
-- 🐛 Workaround a connection timeout bug on Travis CI ppc64le runners (#906)
-- 🐛 Fix an encoding error when reading setup.py in the wrong encoding (#977)
-- 🛠 Setuptools updated to 61.3.0, including experimental support for reading config from pyproject.toml(PEP 621). This could change the behaviour of your build if you have a pyproject.toml with a `[project]` table, because that takes precedence over setup.py and setup.cfg. Check out the [setuptools docs](https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html) and the [project metadata specification](https://packaging.python.org/en/latest/specifications/declaring-project-metadata/) for more info.
-- 🛠 Many other dependency updates.
-- 📚 Minor docs improvements
-
-### v2.3.1
-
-_14 December 2021_
-
-- 🐛 Setting pip options like `PIP_USE_DEPRECATED` in `CIBW_ENVIRONMENT` no longer adversely affects cibuildwheel's ability to set up a Python environment (#956)
-- 📚 Docs fixes and improvements
-
---
diff --git a/action.yml b/action.yml
index 703cb33a..a27269dd 100644
--- a/action.yml
+++ b/action.yml
@@ -20,10 +20,17 @@ branding:
runs:
using: composite
steps:
+ # Set up a non-EOL, cibuildwheel & pipx supported Python version
+ - uses: actions/setup-python@v4
+ id: python
+ with:
+ python-version: "3.7 - 3.10"
+ update-environment: false
# Redirecting stderr to stdout to fix interleaving issue in Actions.
- run: >
pipx run
+ --python '${{ steps.python.outputs.python-path }}'
--spec '${{ github.action_path }}'
cibuildwheel
${{ inputs.package-dir }}
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index a5497a46..f8142ce8 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -11,7 +11,7 @@ jobs:
python ./bin/run_tests.py
- job: macos_38
- pool: {vmImage: 'macOS-10.15'}
+ pool: {vmImage: 'macOS-11'}
steps:
- task: UsePythonVersion@0
inputs:
@@ -20,17 +20,6 @@ jobs:
python -m pip install -e ".[dev]" pytest-azurepipelines
python ./bin/run_tests.py
-- job: windows_36
- pool: {vmImage: 'windows-2019'}
- timeoutInMinutes: 180
- steps:
- - task: UsePythonVersion@0
- inputs:
- versionSpec: '3.6'
- - bash: |
- python -m pip install -e ".[dev]" pytest-azurepipelines
- python ./bin/run_tests.py
-
- job: windows_38
pool: {vmImage: 'windows-2019'}
timeoutInMinutes: 180
diff --git a/bin/bump_version.py b/bin/bump_version.py
index 7e2b1b30..cab036ca 100755
--- a/bin/bump_version.py
+++ b/bin/bump_version.py
@@ -184,7 +184,7 @@ def bump_version() -> None:
print()
release_url = "https://github.com/pypa/cibuildwheel/releases/new?" + urllib.parse.urlencode(
- {"tag": new_version}
+ {"tag": f"v{new_version}"}
)
print("Then create a release at the URL:")
print(f" {release_url}")
diff --git a/bin/run_tests.py b/bin/run_tests.py
index 44c6d302..583d13c7 100755
--- a/bin/run_tests.py
+++ b/bin/run_tests.py
@@ -1,33 +1,49 @@
#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
import os
import subprocess
import sys
from pathlib import Path
if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--run-podman", action="store_true", default=False, help="run podman tests (linux only)"
+ )
+ args = parser.parse_args()
+
# move cwd to the project root
os.chdir(Path(__file__).resolve().parents[1])
- # run the unit tests
+ # unit tests
unit_test_args = [sys.executable, "-m", "pytest", "unit_test"]
- # run the docker unit tests only on Linux
+
if sys.platform.startswith("linux"):
+ # run the docker unit tests only on Linux
unit_test_args += ["--run-docker"]
+
+ if args.run_podman:
+ unit_test_args += ["--run-podman"]
+
subprocess.run(unit_test_args, check=True)
- # run the integration tests
- subprocess.run(
- [
- sys.executable,
- "-m",
- "pytest",
- "--numprocesses=2",
- "-x",
- "--durations",
- "0",
- "--timeout=2400",
- "test",
- ],
- check=True,
- )
+ # integration tests
+ integration_test_args = [
+ sys.executable,
+ "-m",
+ "pytest",
+ "--numprocesses=2",
+ "-x",
+ "--durations",
+ "0",
+ "--timeout=2400",
+ "test",
+ ]
+
+ if sys.platform.startswith("linux") and args.run_podman:
+ integration_test_args += ["--run-podman"]
+
+ subprocess.run(integration_test_args, check=True)
diff --git a/bin/update_dependencies.py b/bin/update_dependencies.py
index 69e62cfa..c0d7f200 100755
--- a/bin/update_dependencies.py
+++ b/bin/update_dependencies.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
-# This file supports 3.6+
+
+from __future__ import annotations
import os
import shutil
diff --git a/bin/update_docker.py b/bin/update_docker.py
index 73da5a1b..4d1af209 100755
--- a/bin/update_docker.py
+++ b/bin/update_docker.py
@@ -2,8 +2,8 @@
from __future__ import annotations
import configparser
+from dataclasses import dataclass
from pathlib import Path
-from typing import NamedTuple
import requests
@@ -11,7 +11,8 @@ DIR = Path(__file__).parent.resolve()
RESOURCES = DIR.parent / "cibuildwheel/resources"
-class Image(NamedTuple):
+@dataclass(frozen=True)
+class Image:
manylinux_version: str
platform: str
image_name: str
@@ -45,6 +46,12 @@ images = [
Image("manylinux_2_24", "pypy_x86_64", "quay.io/pypa/manylinux_2_24_x86_64", None),
Image("manylinux_2_24", "pypy_i686", "quay.io/pypa/manylinux_2_24_i686", None),
Image("manylinux_2_24", "pypy_aarch64", "quay.io/pypa/manylinux_2_24_aarch64", None),
+ # manylinux_2_28 images
+ Image("manylinux_2_28", "x86_64", "quay.io/pypa/manylinux_2_28_x86_64", None),
+ Image("manylinux_2_28", "aarch64", "quay.io/pypa/manylinux_2_28_aarch64", None),
+ Image("manylinux_2_28", "ppc64le", "quay.io/pypa/manylinux_2_28_ppc64le", None),
+ Image("manylinux_2_28", "pypy_x86_64", "quay.io/pypa/manylinux_2_28_x86_64", None),
+ Image("manylinux_2_28", "pypy_aarch64", "quay.io/pypa/manylinux_2_28_aarch64", None),
# musllinux_1_1 images
Image("musllinux_1_1", "x86_64", "quay.io/pypa/musllinux_1_1_x86_64", None),
Image("musllinux_1_1", "i686", "quay.io/pypa/musllinux_1_1_i686", None),
diff --git a/bin/update_readme_changelog.py b/bin/update_readme_changelog.py
index 90b2ffd8..4a4625ff 100755
--- a/bin/update_readme_changelog.py
+++ b/bin/update_readme_changelog.py
@@ -1,5 +1,7 @@
#!/usr/bin/env python3
+from __future__ import annotations
+
import re
import sys
from pathlib import Path
diff --git a/bin/update_virtualenv.py b/bin/update_virtualenv.py
index 2e67007e..3704463d 100755
--- a/bin/update_virtualenv.py
+++ b/bin/update_virtualenv.py
@@ -6,8 +6,8 @@ import difflib
import logging
import subprocess
import sys
+from dataclasses import dataclass
from pathlib import Path
-from typing import NamedTuple
import click
import rich
@@ -36,7 +36,8 @@ GET_VIRTUALENV_URL_TEMPLATE: Final[
] = f"{GET_VIRTUALENV_GITHUB}/blob/{{version}}/public/virtualenv.pyz?raw=true"
-class VersionTuple(NamedTuple):
+@dataclass(frozen=True, order=True)
+class VersionTuple:
version: Version
version_string: str
diff --git a/cibuildwheel/__init__.py b/cibuildwheel/__init__.py
index fab833f3..b923f228 100644
--- a/cibuildwheel/__init__.py
+++ b/cibuildwheel/__init__.py
@@ -1 +1,3 @@
-__version__ = "2.6.1"
+from __future__ import annotations
+
+__version__ = "2.8.1"
diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py
index f8158b31..034673a2 100644
--- a/cibuildwheel/__main__.py
+++ b/cibuildwheel/__main__.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import argparse
import os
import shutil
@@ -7,7 +9,6 @@ import tempfile
import textwrap
from pathlib import Path
from tempfile import mkdtemp
-from typing import List, Set, Union
import cibuildwheel
import cibuildwheel.linux
@@ -46,7 +47,7 @@ def main() -> None:
auto-detected platform or to run cibuildwheel on your development
machine. Specifying "macos" or "windows" only works on that
operating system, but "linux" works on all three, as long as
- Docker is installed. Default: auto.
+ Docker/Podman is installed. Default: auto.
""",
)
@@ -91,7 +92,7 @@ def main() -> None:
Path to the package that you want wheels for. Default: the working
directory. Can be a directory inside the working directory, or an
sdist. When set to a directory, the working directory is still
- considered the 'project' and is copied into the Docker container
+ considered the 'project' and is copied into the build container
on Linux. When set to a tar.gz sdist file, --config-file
and --output-dir are relative to the current directory, and other
paths are relative to the expanded SDist directory.
@@ -116,7 +117,7 @@ def main() -> None:
help="Enable pre-release Python versions if available.",
)
- args = parser.parse_args(namespace=CommandLineArguments())
+ args = CommandLineArguments(**vars(parser.parse_args()))
args.package_dir = args.package_dir.resolve()
@@ -208,7 +209,6 @@ def build_in_directory(args: CommandLineArguments) -> None:
sys.exit(0)
# Add CIBUILDWHEEL environment variable
- # This needs to be passed on to the docker container in linux.py
os.environ["CIBUILDWHEEL"] = "1"
# Python is buffering by default when running on the CI platforms, giving problems interleaving subprocess call output with unflushed calls to 'print'
@@ -258,7 +258,7 @@ def build_in_directory(args: CommandLineArguments) -> None:
log.warning(f"Can't delete temporary folder '{str(tmp_path)}'")
-def print_preamble(platform: str, options: Options, identifiers: List[str]) -> None:
+def print_preamble(platform: str, options: Options, identifiers: list[str]) -> None:
print(
textwrap.dedent(
"""
@@ -288,13 +288,13 @@ def print_preamble(platform: str, options: Options, identifiers: List[str]) -> N
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],
- ]
+ platform: PlatformName, build_selector: BuildSelector, architectures: set[Architecture]
+) -> list[str]:
+ python_configurations: (
+ list[cibuildwheel.linux.PythonConfiguration]
+ | list[cibuildwheel.windows.PythonConfiguration]
+ | list[cibuildwheel.macos.PythonConfiguration]
+ )
if platform == "linux":
python_configurations = cibuildwheel.linux.get_python_configurations(
@@ -314,7 +314,7 @@ def get_build_identifiers(
return [config.identifier for config in python_configurations]
-def detect_warnings(*, options: Options, identifiers: List[str]) -> List[str]:
+def detect_warnings(*, options: Options, identifiers: list[str]) -> list[str]:
warnings = []
# warn about deprecated {python} and {pip}
diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py
index 437ff5e8..82a78b30 100644
--- a/cibuildwheel/architecture.py
+++ b/cibuildwheel/architecture.py
@@ -1,8 +1,9 @@
+from __future__ import annotations
+
import functools
import platform as platform_module
import re
from enum import Enum
-from typing import Set
from .typing import Final, Literal, PlatformName, assert_never
@@ -32,11 +33,11 @@ class Architecture(Enum):
ARM64 = "ARM64"
# Allow this to be sorted
- def __lt__(self, other: "Architecture") -> bool:
+ def __lt__(self, other: Architecture) -> bool:
return self.value < other.value
@staticmethod
- def parse_config(config: str, platform: PlatformName) -> "Set[Architecture]":
+ def parse_config(config: str, platform: PlatformName) -> set[Architecture]:
result = set()
for arch_str in re.split(r"[\s,]+", config):
if arch_str == "auto":
@@ -54,12 +55,12 @@ class Architecture(Enum):
return result
@staticmethod
- def auto_archs(platform: PlatformName) -> "Set[Architecture]":
+ 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
+ # x86_64 machines can run i686 containers
result.add(Architecture.i686)
if platform == "windows" and native_architecture == Architecture.AMD64:
@@ -72,7 +73,7 @@ class Architecture(Enum):
return result
@staticmethod
- def all_archs(platform: PlatformName) -> "Set[Architecture]":
+ def all_archs(platform: PlatformName) -> set[Architecture]:
all_archs_map = {
"linux": {
Architecture.x86_64,
@@ -87,7 +88,7 @@ class Architecture(Enum):
return all_archs_map[platform]
@staticmethod
- def bitness_archs(platform: PlatformName, bitness: Literal["64", "32"]) -> "Set[Architecture]":
+ def bitness_archs(platform: PlatformName, bitness: Literal["64", "32"]) -> set[Architecture]:
archs_32 = {Architecture.i686, Architecture.x86}
auto_archs = Architecture.auto_archs(platform)
@@ -101,7 +102,7 @@ class Architecture(Enum):
def allowed_architectures_check(
platform: PlatformName,
- architectures: Set[Architecture],
+ architectures: set[Architecture],
) -> None:
allowed_architectures = Architecture.all_archs(platform)
diff --git a/cibuildwheel/bashlex_eval.py b/cibuildwheel/bashlex_eval.py
index 9eb5eac3..47e4aea3 100644
--- a/cibuildwheel/bashlex_eval.py
+++ b/cibuildwheel/bashlex_eval.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
import subprocess
-from typing import Callable, Dict, List, NamedTuple, Optional, Sequence
+from dataclasses import dataclass
+from typing import Callable, Dict, List, Sequence
import bashlex
@@ -7,20 +10,19 @@ import bashlex
EnvironmentExecutor = Callable[[List[str], Dict[str, str]], str]
-def local_environment_executor(command: List[str], env: Dict[str, str]) -> str:
- return subprocess.run(
- command, env=env, universal_newlines=True, stdout=subprocess.PIPE, check=True
- ).stdout
+def local_environment_executor(command: list[str], env: dict[str, str]) -> str:
+ return subprocess.run(command, env=env, text=True, stdout=subprocess.PIPE, check=True).stdout
-class NodeExecutionContext(NamedTuple):
- environment: Dict[str, str]
+@dataclass(frozen=True)
+class NodeExecutionContext:
+ environment: dict[str, str]
input: str
executor: EnvironmentExecutor
def evaluate(
- value: str, environment: Dict[str, str], executor: Optional[EnvironmentExecutor] = None
+ value: str, environment: dict[str, str], executor: EnvironmentExecutor | None = None
) -> str:
if not value:
# empty string evaluates to empty string
@@ -101,7 +103,7 @@ def evaluate_nodes_as_compound_command(
def evaluate_nodes_as_simple_command(
- nodes: List[bashlex.ast.node], context: NodeExecutionContext
+ nodes: list[bashlex.ast.node], context: NodeExecutionContext
) -> str:
command = [evaluate_node(part, context=context) for part in nodes]
return context.executor(command, context.environment)
diff --git a/cibuildwheel/environment.py b/cibuildwheel/environment.py
index 3bf982d2..a544852a 100644
--- a/cibuildwheel/environment.py
+++ b/cibuildwheel/environment.py
@@ -1,5 +1,7 @@
+from __future__ import annotations
+
import dataclasses
-from typing import Any, Dict, List, Mapping, Optional, Sequence
+from typing import Any, Mapping, Sequence
import bashlex
@@ -12,7 +14,7 @@ class EnvironmentParseError(Exception):
pass
-def split_env_items(env_string: str) -> List[str]:
+def split_env_items(env_string: str) -> list[str]:
"""Splits space-separated variable assignments into a list of individual assignments.
>>> split_env_items('VAR=abc')
@@ -47,8 +49,8 @@ class EnvironmentAssignment(Protocol):
def evaluated_value(
self,
*,
- environment: Dict[str, str],
- executor: Optional[bashlex_eval.EnvironmentExecutor] = None,
+ environment: dict[str, str],
+ executor: bashlex_eval.EnvironmentExecutor | None = None,
) -> str:
"""Returns the value of this assignment, as evaluated in the environment"""
@@ -84,8 +86,8 @@ class EnvironmentAssignmentBash:
def evaluated_value(
self,
- environment: Dict[str, str],
- executor: Optional[bashlex_eval.EnvironmentExecutor] = None,
+ environment: dict[str, str],
+ executor: bashlex_eval.EnvironmentExecutor | None = None,
) -> str:
return bashlex_eval.evaluate(self.value, environment=environment, executor=executor)
@@ -100,7 +102,7 @@ class EnvironmentAssignmentBash:
@dataclasses.dataclass
class ParsedEnvironment:
- assignments: List[EnvironmentAssignment]
+ assignments: list[EnvironmentAssignment]
def __init__(self, assignments: Sequence[EnvironmentAssignment]) -> None:
self.assignments = list(assignments)
@@ -108,8 +110,8 @@ class ParsedEnvironment:
def as_dictionary(
self,
prev_environment: Mapping[str, str],
- executor: Optional[bashlex_eval.EnvironmentExecutor] = None,
- ) -> Dict[str, str]:
+ executor: bashlex_eval.EnvironmentExecutor | None = None,
+ ) -> dict[str, str]:
environment = dict(**prev_environment)
for assignment in self.assignments:
diff --git a/cibuildwheel/extra.py b/cibuildwheel/extra.py
index 60eecbad..36389e6f 100644
--- a/cibuildwheel/extra.py
+++ b/cibuildwheel/extra.py
@@ -2,8 +2,9 @@
These are utilities for the `/bin` scripts, not for the `cibuildwheel` program.
"""
+from __future__ import annotations
+
from io import StringIO
-from typing import Dict, List
from .typing import Protocol
@@ -15,7 +16,7 @@ class Printable(Protocol):
...
-def dump_python_configurations(inp: Dict[str, Dict[str, List[Dict[str, Printable]]]]) -> str:
+def dump_python_configurations(inp: dict[str, dict[str, list[dict[str, Printable]]]]) -> str:
output = StringIO()
for header, values in inp.items():
output.write(f"[{header}]\n")
diff --git a/cibuildwheel/functools_cached_property_38.py b/cibuildwheel/functools_cached_property_38.py
index e1c45ac2..cbe4537a 100644
--- a/cibuildwheel/functools_cached_property_38.py
+++ b/cibuildwheel/functools_cached_property_38.py
@@ -1,5 +1,7 @@
+from __future__ import annotations
+
from threading import RLock
-from typing import Any, Callable, Generic, Optional, Type, TypeVar, overload
+from typing import Any, Callable, Generic, TypeVar, overload
__all__ = ["cached_property"]
@@ -11,11 +13,11 @@ _T = TypeVar("_T")
class cached_property(Generic[_T]):
def __init__(self, func: Callable[[Any], _T]):
self.func = func
- self.attrname: Optional[str] = None
+ self.attrname: str | None = None
self.__doc__ = func.__doc__
self.lock = RLock()
- def __set_name__(self, owner: Type[Any], name: str) -> None:
+ def __set_name__(self, owner: type[Any], name: str) -> None:
if self.attrname is None:
self.attrname = name
elif name != self.attrname:
@@ -25,14 +27,14 @@ class cached_property(Generic[_T]):
)
@overload
- def __get__(self, instance: None, owner: Optional[Type[Any]] = ...) -> "cached_property[_T]":
+ def __get__(self, instance: None, owner: type[Any] | None = ...) -> cached_property[_T]:
...
@overload
- def __get__(self, instance: object, owner: Optional[Type[Any]] = ...) -> _T:
+ def __get__(self, instance: object, owner: type[Any] | None = ...) -> _T:
...
- def __get__(self, instance: Optional[object], owner: Optional[Type[Any]] = None) -> Any:
+ def __get__(self, instance: object | None, owner: type[Any] | None = None) -> Any:
if instance is None:
return self
if self.attrname is None:
diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py
index 036eb36f..2a0ec43f 100644
--- a/cibuildwheel/linux.py
+++ b/cibuildwheel/linux.py
@@ -1,44 +1,51 @@
+from __future__ import annotations
+
import subprocess
import sys
import textwrap
-from pathlib import Path, PurePath
-from typing import Iterator, List, NamedTuple, Set, Tuple
+from dataclasses import dataclass
+from pathlib import Path, PurePath, PurePosixPath
+from typing import Iterator, Tuple
from .architecture import Architecture
-from .docker_container import DockerContainer
from .logger import log
+from .oci_container import OCIContainer
from .options import Options
from .typing import OrderedDict, PathOrStr, assert_never
from .util import (
+ AlreadyBuiltWheelError,
BuildSelector,
NonPlatformWheelError,
- find_compatible_abi3_wheel,
+ find_compatible_wheel,
get_build_verbosity_extra_flags,
prepare_command,
read_python_configs,
+ unwrap,
)
-class PythonConfiguration(NamedTuple):
+@dataclass(frozen=True)
+class PythonConfiguration:
version: str
identifier: str
path_str: str
@property
- def path(self) -> PurePath:
- return PurePath(self.path_str)
+ def path(self) -> PurePosixPath:
+ return PurePosixPath(self.path_str)
-class BuildStep(NamedTuple):
- platform_configs: List[PythonConfiguration]
+@dataclass(frozen=True)
+class BuildStep:
+ platform_configs: list[PythonConfiguration]
platform_tag: str
- docker_image: str
+ container_image: str
def get_python_configurations(
build_selector: BuildSelector,
- architectures: Set[Architecture],
-) -> List[PythonConfiguration]:
+ architectures: set[Architecture],
+) -> list[PythonConfiguration]:
full_python_configs = read_python_configs("linux")
@@ -54,7 +61,7 @@ def get_python_configurations(
]
-def docker_image_for_python_configuration(config: PythonConfiguration, options: Options) -> str:
+def container_image_for_python_configuration(config: PythonConfiguration, options: Options) -> str:
build_options = options.build_options(config.identifier)
# e.g
# identifier is 'cp310-manylinux_x86_64'
@@ -74,11 +81,11 @@ def docker_image_for_python_configuration(config: PythonConfiguration, options:
def get_build_steps(
- options: Options, python_configurations: List[PythonConfiguration]
+ options: Options, python_configurations: list[PythonConfiguration]
) -> Iterator[BuildStep]:
"""
Groups PythonConfigurations into BuildSteps. Each BuildStep represents a
- separate Docker container.
+ separate container instance.
"""
steps = OrderedDict[Tuple[str, str, str], BuildStep]()
@@ -86,32 +93,34 @@ def get_build_steps(
_, platform_tag = config.identifier.split("-", 1)
before_all = options.build_options(config.identifier).before_all
- docker_image = docker_image_for_python_configuration(config, options)
+ container_image = container_image_for_python_configuration(config, options)
- step_key = (platform_tag, docker_image, before_all)
+ step_key = (platform_tag, container_image, before_all)
if step_key in steps:
steps[step_key].platform_configs.append(config)
else:
steps[step_key] = BuildStep(
- platform_configs=[config], platform_tag=platform_tag, docker_image=docker_image
+ platform_configs=[config],
+ platform_tag=platform_tag,
+ container_image=container_image,
)
yield from steps.values()
-def build_on_docker(
+def build_in_container(
*,
options: Options,
- platform_configs: List[PythonConfiguration],
- docker: DockerContainer,
+ platform_configs: list[PythonConfiguration],
+ container: OCIContainer,
container_project_path: PurePath,
container_package_dir: PurePath,
) -> None:
- container_output_dir = PurePath("/output")
+ container_output_dir = PurePosixPath("/output")
- log.step("Copying project into Docker...")
- docker.copy_into(Path.cwd(), container_project_path)
+ log.step("Copying project into container...")
+ container.copy_into(Path.cwd(), container_project_path)
before_all_options_identifier = platform_configs[0].identifier
before_all_options = options.build_options(before_all_options_identifier)
@@ -119,11 +128,11 @@ def build_on_docker(
if before_all_options.before_all:
log.step("Running before_all...")
- env = docker.get_environment()
+ env = container.get_environment()
env["PATH"] = f'/opt/python/cp38-cp38/bin:{env["PATH"]}'
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
env = before_all_options.environment.as_dictionary(
- env, executor=docker.environment_executor
+ env, executor=container.environment_executor
)
before_all_prepared = prepare_command(
@@ -131,15 +140,15 @@ def build_on_docker(
project=container_project_path,
package=container_package_dir,
)
- docker.call(["sh", "-c", before_all_prepared], env=env)
+ container.call(["sh", "-c", before_all_prepared], env=env)
- built_wheels: List[PurePath] = []
+ built_wheels: list[PurePosixPath] = []
for config in platform_configs:
log.build_start(config.identifier)
build_options = options.build_options(config.identifier)
- dependency_constraint_flags: List[PathOrStr] = []
+ dependency_constraint_flags: list[PathOrStr] = []
if build_options.dependency_constraints:
constraints_file = build_options.dependency_constraints.get_for_python_version(
@@ -147,43 +156,43 @@ def build_on_docker(
)
container_constraints_file = PurePath("/constraints.txt")
- docker.copy_into(constraints_file, container_constraints_file)
+ container.copy_into(constraints_file, container_constraints_file)
dependency_constraint_flags = ["-c", container_constraints_file]
log.step("Setting up build environment...")
- env = docker.get_environment()
+ env = container.get_environment()
# put this config's python top of the list
python_bin = config.path / "bin"
env["PATH"] = f'{python_bin}:{env["PATH"]}'
- env = build_options.environment.as_dictionary(env, executor=docker.environment_executor)
+ env = build_options.environment.as_dictionary(env, executor=container.environment_executor)
# check config python is still on PATH
- which_python = docker.call(["which", "python"], env=env, capture_output=True).strip()
- if PurePath(which_python) != python_bin / "python":
+ which_python = container.call(["which", "python"], env=env, capture_output=True).strip()
+ if PurePosixPath(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,
)
sys.exit(1)
- which_pip = docker.call(["which", "pip"], env=env, capture_output=True).strip()
- if PurePath(which_pip) != python_bin / "pip":
+ which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip()
+ if PurePosixPath(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,
)
sys.exit(1)
- abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier)
- if abi3_wheel:
+ compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
+ if compatible_wheel:
log.step_end()
print(
- f"\nFound previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
+ f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
)
- repaired_wheels = [abi3_wheel]
+ repaired_wheels = [compatible_wheel]
else:
if build_options.before_build:
@@ -193,19 +202,19 @@ def build_on_docker(
project=container_project_path,
package=container_package_dir,
)
- docker.call(["sh", "-c", before_build_prepared], env=env)
+ container.call(["sh", "-c", before_build_prepared], env=env)
log.step("Building wheel...")
- temp_dir = PurePath("/tmp/cibuildwheel")
+ temp_dir = PurePosixPath("/tmp/cibuildwheel")
built_wheel_dir = temp_dir / "built_wheel"
- docker.call(["rm", "-rf", built_wheel_dir])
- docker.call(["mkdir", "-p", built_wheel_dir])
+ container.call(["rm", "-rf", built_wheel_dir])
+ container.call(["mkdir", "-p", built_wheel_dir])
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
if build_options.build_frontend == "pip":
- docker.call(
+ container.call(
[
"python",
"-m",
@@ -220,7 +229,7 @@ def build_on_docker(
)
elif build_options.build_frontend == "build":
config_setting = " ".join(verbosity_flags)
- docker.call(
+ container.call(
[
"python",
"-m",
@@ -235,11 +244,11 @@ def build_on_docker(
else:
assert_never(build_options.build_frontend)
- built_wheel = docker.glob(built_wheel_dir, "*.whl")[0]
+ built_wheel = container.glob(built_wheel_dir, "*.whl")[0]
repaired_wheel_dir = temp_dir / "repaired_wheel"
- docker.call(["rm", "-rf", repaired_wheel_dir])
- docker.call(["mkdir", "-p", repaired_wheel_dir])
+ container.call(["rm", "-rf", repaired_wheel_dir])
+ container.call(["mkdir", "-p", repaired_wheel_dir])
if built_wheel.name.endswith("none-any.whl"):
raise NonPlatformWheelError()
@@ -249,21 +258,27 @@ def build_on_docker(
repair_command_prepared = prepare_command(
build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
)
- docker.call(["sh", "-c", repair_command_prepared], env=env)
+ container.call(["sh", "-c", repair_command_prepared], env=env)
else:
- docker.call(["mv", built_wheel, repaired_wheel_dir])
+ container.call(["mv", built_wheel, repaired_wheel_dir])
- repaired_wheels = docker.glob(repaired_wheel_dir, "*.whl")
+ repaired_wheels = container.glob(repaired_wheel_dir, "*.whl")
+
+ for repaired_wheel in repaired_wheels:
+ if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
+ raise AlreadyBuiltWheelError(repaired_wheel.name)
if build_options.test_command and build_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.
- docker.call(["pip", "install", "virtualenv", *dependency_constraint_flags], env=env)
- venv_dir = PurePath(docker.call(["mktemp", "-d"], capture_output=True).strip()) / "venv"
+ container.call(["pip", "install", "virtualenv", *dependency_constraint_flags], env=env)
+ venv_dir = (
+ PurePath(container.call(["mktemp", "-d"], capture_output=True).strip()) / "venv"
+ )
- docker.call(["python", "-m", "virtualenv", "--no-download", venv_dir], env=env)
+ container.call(["python", "-m", "virtualenv", "--no-download", venv_dir], env=env)
virtualenv_env = env.copy()
virtualenv_env["PATH"] = f"{venv_dir / 'bin'}:{virtualenv_env['PATH']}"
@@ -274,7 +289,7 @@ def build_on_docker(
project=container_project_path,
package=container_package_dir,
)
- docker.call(["sh", "-c", before_test_prepared], env=virtualenv_env)
+ container.call(["sh", "-c", before_test_prepared], env=virtualenv_env)
# Install the wheel we just built
# Note: If auditwheel produced two wheels, it's because the earlier produced wheel
@@ -283,14 +298,14 @@ def build_on_docker(
# different external shared libraries. so it doesn't matter which one we run the tests on.
# Let's just pick the first one.
wheel_to_test = repaired_wheels[0]
- docker.call(
+ container.call(
["pip", "install", str(wheel_to_test) + build_options.test_extras],
env=virtualenv_env,
)
# Install any requirements to run the tests
if build_options.test_requires:
- docker.call(["pip", "install", *build_options.test_requires], env=virtualenv_env)
+ container.call(["pip", "install", *build_options.test_requires], env=virtualenv_env)
# Run the tests from a different directory
test_command_prepared = prepare_command(
@@ -298,15 +313,15 @@ def build_on_docker(
project=container_project_path,
package=container_package_dir,
)
- docker.call(["sh", "-c", test_command_prepared], cwd="/root", env=virtualenv_env)
+ container.call(["sh", "-c", test_command_prepared], cwd="/root", env=virtualenv_env)
# clean up test environment
- docker.call(["rm", "-rf", venv_dir])
+ container.call(["rm", "-rf", venv_dir])
# move repaired wheels to output
- if abi3_wheel is None:
- docker.call(["mkdir", "-p", container_output_dir])
- docker.call(["mv", *repaired_wheels, container_output_dir])
+ if compatible_wheel is None:
+ container.call(["mkdir", "-p", container_output_dir])
+ container.call(["mv", *repaired_wheels, container_output_dir])
built_wheels.extend(
container_output_dir / repaired_wheel.name for repaired_wheel in repaired_wheels
)
@@ -315,19 +330,27 @@ def build_on_docker(
log.step("Copying wheels back to host...")
# copy the output back into the host
- docker.copy_out(container_output_dir, options.globals.output_dir)
+ container.copy_out(container_output_dir, options.globals.output_dir)
log.step_end()
def build(options: Options, tmp_path: Path) -> None: # pylint: disable=unused-argument
try:
- # check docker is installed
- subprocess.run(["docker", "--version"], check=True, stdout=subprocess.DEVNULL)
+ # check the container engine is installed
+ subprocess.run(
+ [options.globals.container_engine, "--version"], check=True, stdout=subprocess.DEVNULL
+ )
except subprocess.CalledProcessError:
print(
- "cibuildwheel: Docker not found. Docker is required to run Linux builds. "
- "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",
+ unwrap(
+ f"""
+ cibuildwheel: {options.globals.container_engine} not found. An
+ OCI exe like Docker or Podman is required to run Linux builds.
+ 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,
)
sys.exit(2)
@@ -341,26 +364,27 @@ def build(options: Options, tmp_path: Path) -> None: # pylint: disable=unused-a
if cwd != abs_package_dir and cwd not in abs_package_dir.parents:
raise Exception("package_dir must be inside the working directory")
- container_project_path = PurePath("/project")
+ container_project_path = PurePosixPath("/project")
container_package_dir = container_project_path / abs_package_dir.relative_to(cwd)
for build_step in get_build_steps(options, python_configurations):
try:
ids_to_build = [x.identifier for x in build_step.platform_configs]
- log.step(
- f"Starting Docker image {build_step.docker_image} for {', '.join(ids_to_build)}..."
- )
+ log.step(f"Starting container image {build_step.container_image}...")
- with DockerContainer(
- docker_image=build_step.docker_image,
+ print(f"info: This container will host the build for {', '.join(ids_to_build)}...")
+
+ with OCIContainer(
+ image=build_step.container_image,
simulate_32_bit=build_step.platform_tag.endswith("i686"),
cwd=container_project_path,
- ) as docker:
+ engine=options.globals.container_engine,
+ ) as container:
- build_on_docker(
+ build_in_container(
options=options,
platform_configs=build_step.platform_configs,
- docker=docker,
+ container=container,
container_project_path=container_project_path,
container_package_dir=container_package_dir,
)
@@ -373,7 +397,7 @@ def build(options: Options, tmp_path: Path) -> None: # pylint: disable=unused-a
sys.exit(1)
-def _matches_prepared_command(error_cmd: List[str], command_template: str) -> bool:
+def _matches_prepared_command(error_cmd: list[str], command_template: str) -> bool:
if len(error_cmd) < 3 or error_cmd[0:2] != ["sh", "-c"]:
return False
command_prefix = command_template.split("{", maxsplit=1)[0].strip()
diff --git a/cibuildwheel/logger.py b/cibuildwheel/logger.py
index 0a4be14d..6f408b47 100644
--- a/cibuildwheel/logger.py
+++ b/cibuildwheel/logger.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
import codecs
import os
import re
import sys
import time
-from typing import IO, AnyStr, Optional, Union
+from typing import IO, AnyStr
from cibuildwheel.typing import Final
from cibuildwheel.util import CIProvider, detect_ci_provider
@@ -39,10 +41,10 @@ class Logger:
fold_mode: str
colors_enabled: bool
unicode_enabled: bool
- active_build_identifier: Optional[str] = None
- build_start_time: Optional[float] = None
- step_start_time: Optional[float] = None
- active_fold_group_name: Optional[str] = None
+ active_build_identifier: str | None = None
+ build_start_time: float | None = None
+ step_start_time: float | None = None
+ active_fold_group_name: str | None = None
def __init__(self) -> None:
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
@@ -120,7 +122,7 @@ class Logger:
self.step_start_time = None
- def step_end_with_error(self, error: Union[BaseException, str]) -> None:
+ def step_end_with_error(self, error: BaseException | str) -> None:
self.step_end(success=False)
self.error(error)
@@ -131,7 +133,7 @@ class Logger:
c = self.colors
print(f"{c.yellow}Warning{c.end}: {message}\n", file=sys.stderr)
- def error(self, error: Union[BaseException, str]) -> None:
+ def error(self, error: BaseException | str) -> None:
if self.fold_mode == "github":
print(f"::error::{error}\n", file=sys.stderr)
else:
@@ -174,11 +176,11 @@ class Logger:
return identifier.lower()[:20]
@property
- def colors(self) -> "Colors":
+ def colors(self) -> Colors:
return Colors(enabled=self.colors_enabled)
@property
- def symbols(self) -> "Symbols":
+ def symbols(self) -> Symbols:
return Symbols(unicode=self.unicode_enabled)
diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py
index 7100cd05..00820674 100644
--- a/cibuildwheel/macos.py
+++ b/cibuildwheel/macos.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import functools
import os
import platform
@@ -5,8 +7,9 @@ import re
import shutil
import subprocess
import sys
+from dataclasses import dataclass
from pathlib import Path
-from typing import Dict, List, NamedTuple, Sequence, Set, Tuple, cast
+from typing import Sequence, Tuple, cast
from filelock import FileLock
@@ -17,13 +20,14 @@ from .options import Options
from .typing import Literal, PathOrStr, assert_never
from .util import (
CIBW_CACHE_PATH,
+ AlreadyBuiltWheelError,
BuildFrontend,
BuildSelector,
NonPlatformWheelError,
call,
detect_ci_provider,
download,
- find_compatible_abi3_wheel,
+ find_compatible_wheel,
get_build_verbosity_extra_flags,
get_pip_version,
install_certifi_script,
@@ -35,7 +39,7 @@ from .util import (
)
-def get_macos_version() -> Tuple[int, int]:
+def get_macos_version() -> tuple[int, int]:
"""
Returns the macOS major/minor version, as a tuple, e.g. (10, 15) or (11, 0)
@@ -49,20 +53,21 @@ def get_macos_version() -> Tuple[int, int]:
return cast(Tuple[int, int], version)
-def get_macos_sdks() -> List[str]:
+def get_macos_sdks() -> list[str]:
output = call("xcodebuild", "-showsdks", capture_stdout=True)
return [m.group(1) for m in re.finditer(r"-sdk (macosx\S+)", output)]
-class PythonConfiguration(NamedTuple):
+@dataclass(frozen=True)
+class PythonConfiguration:
version: str
identifier: str
url: str
def get_python_configurations(
- build_selector: BuildSelector, architectures: Set[Architecture]
-) -> List[PythonConfiguration]:
+ build_selector: BuildSelector, architectures: set[Architecture]
+) -> list[PythonConfiguration]:
full_python_configs = read_python_configs("macos")
@@ -131,7 +136,7 @@ def setup_python(
dependency_constraint_flags: Sequence[PathOrStr],
environment: ParsedEnvironment,
build_frontend: BuildFrontend,
-) -> Dict[str, str]:
+) -> dict[str, str]:
tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0]
log.step(f"Installing Python {implementation_id}...")
@@ -292,7 +297,7 @@ def build(options: Options, tmp_path: Path) -> None:
)
shell(before_all_prepared, env=env)
- built_wheels: List[Path] = []
+ built_wheels: list[Path] = []
for config in python_configurations:
build_options = options.build_options(config.identifier)
@@ -321,13 +326,13 @@ def build(options: Options, tmp_path: Path) -> None:
build_options.build_frontend,
)
- abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier)
- if abi3_wheel:
+ compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
+ if compatible_wheel:
log.step_end()
print(
- f"\nFound previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
+ f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
)
- repaired_wheel = abi3_wheel
+ repaired_wheel = compatible_wheel
else:
if build_options.before_build:
log.step("Running before_build...")
@@ -408,11 +413,14 @@ def build(options: Options, tmp_path: Path) -> None:
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
+ if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
+ raise AlreadyBuiltWheelError(repaired_wheel.name)
+
log.step_end()
if build_options.test_command and build_options.test_selector(config.identifier):
machine_arch = platform.machine()
- testing_archs: List[Literal["x86_64", "arm64"]]
+ testing_archs: list[Literal["x86_64", "arm64"]]
if config_is_arm64:
testing_archs = ["arm64"]
@@ -459,6 +467,23 @@ def build(options: Options, tmp_path: Path) -> None:
# skip this test
continue
+ if testing_arch == "arm64" and config.identifier.startswith("cp38-"):
+ log.warning(
+ unwrap(
+ """
+ While cibuildwheel can build CPython 3.8 universal2/arm64 wheels, we
+ cannot test the arm64 part of them, even when running on an Apple
+ Silicon machine. This is because we use the x86_64 installer of
+ CPython 3.8. See the discussion in
+ https://github.com/pypa/cibuildwheel/pull/1169 for the details. To
+ silence this warning, set `CIBW_TEST_SKIP: cp38-macosx_*:arm64`.
+ """
+ )
+ )
+
+ # skip this test
+ continue
+
log.step(
"Testing wheel..."
if testing_arch == machine_arch
@@ -482,7 +507,7 @@ def build(options: Options, tmp_path: Path) -> None:
# define a custom 'call' function that adds the arch prefix each time
call_with_arch = functools.partial(call, *arch_prefix)
- shell_with_arch = functools.partial(shell, *arch_prefix)
+ shell_with_arch = functools.partial(call, *arch_prefix, "/bin/sh", "-c")
# Use --no-download to ensure determinism by using seed libraries
# built into virtualenv
@@ -534,7 +559,12 @@ def build(options: Options, tmp_path: Path) -> None:
)
# we're all done here; move it to output (overwrite existing)
- if abi3_wheel is None:
+ if compatible_wheel is None:
+ try:
+ (build_options.output_dir / repaired_wheel.name).unlink()
+ except FileNotFoundError:
+ pass
+
shutil.move(str(repaired_wheel), build_options.output_dir)
built_wheels.append(build_options.output_dir / repaired_wheel.name)
diff --git a/cibuildwheel/docker_container.py b/cibuildwheel/oci_container.py
similarity index 56%
rename from cibuildwheel/docker_container.py
rename to cibuildwheel/oci_container.py
index 8ee8e573..91714c7e 100644
--- a/cibuildwheel/docker_container.py
+++ b/cibuildwheel/oci_container.py
@@ -1,30 +1,46 @@
+from __future__ import annotations
+
import io
import json
import os
import platform
import shlex
+import shutil
import subprocess
import sys
import uuid
-from pathlib import Path, PurePath
+from pathlib import Path, PurePath, PurePosixPath
from types import TracebackType
-from typing import IO, Dict, List, Optional, Sequence, Type, cast
+from typing import IO, Dict, Sequence, cast
from cibuildwheel.util import CIProvider, detect_ci_provider
-from .typing import PathOrStr, PopenBytes
+from .typing import Literal, PathOrStr, PopenBytes
+
+ContainerEngine = Literal["docker", "podman"]
-class DockerContainer:
+class OCIContainer:
"""
- An object that represents a running Docker container.
+ An object that represents a running OCI (e.g. Docker) container.
Intended for use as a context manager e.g.
- `with DockerContainer(docker_image = 'ubuntu') as docker:`
+ `with OCIContainer(image = 'ubuntu') as docker:`
A bash shell is running in the remote container. When `call()` is invoked,
the command is relayed to the remote shell, and the results are streamed
back to cibuildwheel.
+
+ Example:
+ >>> from cibuildwheel.oci_container import * # NOQA
+ >>> from cibuildwheel.options import _get_pinned_container_images
+ >>> image = _get_pinned_container_images()['x86_64']['manylinux2014']
+ >>> # Test the default container
+ >>> with OCIContainer(image=image) as self:
+ ... self.call(["echo", "hello world"])
+ ... self.call(["cat", "/proc/1/cgroup"])
+ ... print(self.get_environment())
+ ... print(self.debug_info())
"""
UTILITY_PYTHON = "/opt/python/cp38-cp38/bin/python"
@@ -34,19 +50,25 @@ class DockerContainer:
bash_stdout: IO[bytes]
def __init__(
- self, *, docker_image: str, simulate_32_bit: bool = False, cwd: Optional[PathOrStr] = None
+ self,
+ *,
+ image: str,
+ simulate_32_bit: bool = False,
+ cwd: PathOrStr | None = None,
+ engine: ContainerEngine = "docker",
):
- if not docker_image:
- raise ValueError("Must have a non-empty docker image to run.")
+ if not image:
+ raise ValueError("Must have a non-empty image to run.")
- self.docker_image = docker_image
+ self.image = image
self.simulate_32_bit = simulate_32_bit
self.cwd = cwd
- self.name: Optional[str] = None
+ self.name: str | None = None
+ self.engine = engine
+
+ def __enter__(self) -> OCIContainer:
- def __enter__(self) -> "DockerContainer":
self.name = f"cibuildwheel-{uuid.uuid4()}"
- cwd_args = ["-w", str(self.cwd)] if self.cwd else []
# work-around for Travis-CI PPC64le Docker runs since 2021:
# this avoids network splits
@@ -57,24 +79,25 @@ class DockerContainer:
network_args = ["--network=host"]
shell_args = ["linux32", "/bin/bash"] if self.simulate_32_bit else ["/bin/bash"]
+
subprocess.run(
[
- "docker",
+ self.engine,
"create",
"--env=CIBUILDWHEEL",
f"--name={self.name}",
"--interactive",
"--volume=/:/host", # ignored on CircleCI
*network_args,
- *cwd_args,
- self.docker_image,
+ self.image,
*shell_args,
],
check=True,
)
+
self.process = subprocess.Popen(
[
- "docker",
+ self.engine,
"start",
"--attach",
"--interactive",
@@ -89,15 +112,21 @@ class DockerContainer:
self.bash_stdout = self.process.stdout
# run a noop command to block until the container is responding
- self.call(["/bin/true"])
+ self.call(["/bin/true"], cwd="/")
+
+ if self.cwd:
+ # Although `docker create -w` does create the working dir if it
+ # does not exist, podman does not. There does not seem to be a way
+ # to setup a workdir for a container running in podman.
+ self.call(["mkdir", "-p", os.fspath(self.cwd)], cwd="/")
return self
def __exit__(
self,
- exc_type: Optional[Type[BaseException]],
- exc_val: Optional[BaseException],
- exc_tb: Optional[TracebackType],
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
) -> None:
self.bash_stdin.write(b"exit 0\n")
@@ -106,10 +135,19 @@ class DockerContainer:
self.bash_stdin.close()
self.bash_stdout.close()
+ if self.engine == "podman":
+ # This works around what seems to be a race condition in the podman
+ # backend. The full reason is not understood. See PR #966 for a
+ # discussion on possible causes and attempts to remove this line.
+ # For now, this seems to work "well enough".
+ self.process.wait()
+
assert isinstance(self.name, str)
subprocess.run(
- ["docker", "rm", "--force", "-v", self.name], stdout=subprocess.DEVNULL, check=False
+ [self.engine, "rm", "--force", "-v", self.name],
+ stdout=subprocess.DEVNULL,
+ check=False,
)
self.name = None
@@ -122,53 +160,94 @@ class DockerContainer:
if from_path.is_dir():
self.call(["mkdir", "-p", to_path])
subprocess.run(
- f"tar cf - . | docker exec -i {self.name} tar --no-same-owner -xC {shell_quote(to_path)} -f -",
+ f"tar cf - . | {self.engine} exec -i {self.name} tar --no-same-owner -xC {shell_quote(to_path)} -f -",
shell=True,
check=True,
cwd=from_path,
)
else:
- subprocess.run(
- f'cat {shell_quote(from_path)} | docker exec -i {self.name} sh -c "cat > {shell_quote(to_path)}"',
- shell=True,
- check=True,
- )
+ with subprocess.Popen(
+ [
+ self.engine,
+ "exec",
+ "-i",
+ str(self.name),
+ "sh",
+ "-c",
+ f"cat > {shell_quote(to_path)}",
+ ],
+ stdin=subprocess.PIPE,
+ ) as exec_process:
+ exec_process.stdin = cast(IO[bytes], exec_process.stdin)
+
+ with open(from_path, "rb") as from_file:
+ shutil.copyfileobj(from_file, exec_process.stdin)
+
+ exec_process.stdin.close()
+ exec_process.wait()
+
+ if exec_process.returncode:
+ raise subprocess.CalledProcessError(
+ exec_process.returncode, exec_process.args, None, None
+ )
def copy_out(self, from_path: PurePath, to_path: Path) -> None:
# note: we assume from_path is a dir
to_path.mkdir(parents=True, exist_ok=True)
- subprocess.run(
- f"docker exec -i {self.name} tar -cC {shell_quote(from_path)} -f - . | tar -xf -",
- shell=True,
- check=True,
- cwd=to_path,
- )
+ if self.engine == "podman":
+ subprocess.run(
+ [
+ self.engine,
+ "cp",
+ f"{self.name}:{from_path}/.",
+ str(to_path),
+ ],
+ check=True,
+ cwd=to_path,
+ )
+ elif self.engine == "docker":
+ # There is a bug in docker that prevents a simple 'cp' invocation
+ # from working https://github.com/moby/moby/issues/38995
+ command = f"{self.engine} exec -i {self.name} tar -cC {shell_quote(from_path)} -f - . | tar -xf -"
+ subprocess.run(
+ command,
+ shell=True,
+ check=True,
+ cwd=to_path,
+ )
+ else:
+ raise KeyError(self.engine)
- def glob(self, path: PurePath, pattern: str) -> List[PurePath]:
- glob_pattern = os.path.join(str(path), pattern)
+ def glob(self, path: PurePosixPath, pattern: str) -> list[PurePosixPath]:
+ glob_pattern = path.joinpath(pattern)
path_strings = json.loads(
self.call(
[
self.UTILITY_PYTHON,
"-c",
- f"import sys, json, glob; json.dump(glob.glob({glob_pattern!r}), sys.stdout)",
+ f"import sys, json, glob; json.dump(glob.glob({str(glob_pattern)!r}), sys.stdout)",
],
capture_output=True,
)
)
- return [PurePath(p) for p in path_strings]
+ return [PurePosixPath(p) for p in path_strings]
def call(
self,
args: Sequence[PathOrStr],
- env: Optional[Dict[str, str]] = None,
+ env: dict[str, str] | None = None,
capture_output: bool = False,
- cwd: Optional[PathOrStr] = None,
+ cwd: PathOrStr | None = None,
) -> str:
+ if cwd is None:
+ # Podman does not start the a container in a specific working dir
+ # so we always need to specify it when making calls.
+ cwd = self.cwd
+
chdir = f"cd {cwd}" if cwd else ""
env_assignments = (
" ".join(f"{shlex.quote(k)}={shlex.quote(v)}" for k, v in env.items())
@@ -237,7 +316,7 @@ class DockerContainer:
return output
- def get_environment(self) -> Dict[str, str]:
+ def get_environment(self) -> dict[str, str]:
env = json.loads(
self.call(
[
@@ -250,10 +329,26 @@ class DockerContainer:
)
return cast(Dict[str, str], env)
- def environment_executor(self, command: List[str], environment: Dict[str, str]) -> str:
+ def environment_executor(self, command: list[str], environment: dict[str, str]) -> str:
# used as an EnvironmentExecutor to evaluate commands and capture output
return self.call(command, env=environment, capture_output=True)
+ def debug_info(self) -> str:
+ if self.engine == "podman":
+ command = f"{self.engine} info --debug"
+ else:
+ command = f"{self.engine} info"
+ completed = subprocess.run(
+ command,
+ shell=True,
+ check=True,
+ cwd=self.cwd,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ )
+ output = str(completed.stdout, encoding="utf8", errors="surrogateescape")
+ return output
+
def shell_quote(path: PurePath) -> str:
- return shlex.quote(str(path))
+ return shlex.quote(os.fspath(path))
diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py
index 5d9744af..30803604 100644
--- a/cibuildwheel/options.py
+++ b/cibuildwheel/options.py
@@ -1,22 +1,14 @@
+from __future__ import annotations
+
import functools
import os
import sys
import traceback
from configparser import ConfigParser
from contextlib import contextmanager
+from dataclasses import asdict, dataclass
from pathlib import Path
-from typing import (
- Any,
- Dict,
- Generator,
- List,
- Mapping,
- NamedTuple,
- Optional,
- Set,
- Tuple,
- Union,
-)
+from typing import Any, Dict, Generator, List, Mapping, Union, cast
if sys.version_info >= (3, 11):
import tomllib
@@ -27,6 +19,7 @@ from packaging.specifiers import SpecifierSet
from .architecture import Architecture
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
+from .oci_container import ContainerEngine
from .projectfiles import get_requires_python_str
from .typing import PLATFORMS, Literal, PlatformName, TypedDict
from .util import (
@@ -45,9 +38,10 @@ from .util import (
)
+@dataclass
class CommandLineArguments:
platform: Literal["auto", "linux", "macos", "windows"]
- archs: Optional[str]
+ archs: str | None
output_dir: Path
config_file: str
package_dir: Path
@@ -56,26 +50,29 @@ class CommandLineArguments:
prerelease_pythons: bool
-class GlobalOptions(NamedTuple):
+@dataclass(frozen=True)
+class GlobalOptions:
package_dir: Path
output_dir: Path
build_selector: BuildSelector
test_selector: TestSelector
- architectures: Set[Architecture]
+ architectures: set[Architecture]
+ container_engine: ContainerEngine
-class BuildOptions(NamedTuple):
+@dataclass(frozen=True)
+class BuildOptions:
globals: GlobalOptions
environment: ParsedEnvironment
before_all: str
- before_build: Optional[str]
+ before_build: str | None
repair_command: str
- manylinux_images: Optional[Dict[str, str]]
- musllinux_images: Optional[Dict[str, str]]
- dependency_constraints: Optional[DependencyConstraints]
- test_command: Optional[str]
- before_test: Optional[str]
- test_requires: List[str]
+ manylinux_images: dict[str, str] | None
+ musllinux_images: dict[str, str] | None
+ dependency_constraints: DependencyConstraints | None
+ test_command: str | None
+ before_test: str | None
+ test_requires: list[str]
test_extras: str
build_verbosity: int
build_frontend: BuildFrontend
@@ -97,16 +94,17 @@ class BuildOptions(NamedTuple):
return self.globals.test_selector
@property
- def architectures(self) -> Set[Architecture]:
+ def architectures(self) -> set[Architecture]:
return self.globals.architectures
Setting = Union[Dict[str, str], List[str], str, int]
-class Override(NamedTuple):
+@dataclass(frozen=True)
+class Override:
select_pattern: str
- options: Dict[str, Setting]
+ options: dict[str, Setting]
MANYLINUX_OPTIONS = {f"manylinux-{build_platform}-image" for build_platform in MANYLINUX_ARCHS}
@@ -127,7 +125,7 @@ class ConfigOptionError(KeyError):
pass
-def _dig_first(*pairs: Tuple[Mapping[str, Setting], str], ignore_empty: bool = False) -> Setting:
+def _dig_first(*pairs: tuple[Mapping[str, Setting], str], ignore_empty: bool = False) -> Setting:
"""
Return the first dict item that matches from pairs of dicts and keys.
Will throw a KeyError if missing.
@@ -169,10 +167,10 @@ class OptionsReader:
def __init__(
self,
- config_file_path: Optional[Path] = None,
+ config_file_path: Path | None = None,
*,
platform: PlatformName,
- disallow: Optional[Dict[str, Set[str]]] = None,
+ disallow: dict[str, set[str]] | None = None,
) -> None:
self.platform = platform
self.disallow = disallow or {}
@@ -182,8 +180,8 @@ class OptionsReader:
self.default_options, self.default_platform_options = self._load_file(defaults_path)
# Load the project config file
- config_options: Dict[str, Any] = {}
- config_platform_options: Dict[str, Any] = {}
+ config_options: dict[str, Any] = {}
+ config_platform_options: dict[str, Any] = {}
if config_file_path is not None:
config_options, config_platform_options = self._load_file(config_file_path)
@@ -202,8 +200,8 @@ class OptionsReader:
self.config_options = config_options
self.config_platform_options = config_platform_options
- self.overrides: List[Override] = []
- self.current_identifier: Optional[str] = None
+ self.overrides: list[Override] = []
+ self.current_identifier: str | None = None
config_overrides = self.config_options.get("overrides")
@@ -244,7 +242,7 @@ class OptionsReader:
return name in allowed_option_names
- def _load_file(self, filename: Path) -> Tuple[Dict[str, Any], Dict[str, Any]]:
+ def _load_file(self, filename: Path) -> tuple[dict[str, Any], dict[str, Any]]:
"""
Load a toml file, returns global and platform as separate dicts.
"""
@@ -257,7 +255,7 @@ class OptionsReader:
return global_options, platform_options
@property
- def active_config_overrides(self) -> List[Override]:
+ def active_config_overrides(self) -> list[Override]:
if self.current_identifier is None:
return []
return [
@@ -265,7 +263,7 @@ class OptionsReader:
]
@contextmanager
- def identifier(self, identifier: Optional[str]) -> Generator[None, None, None]:
+ def identifier(self, identifier: str | None) -> Generator[None, None, None]:
self.current_identifier = identifier
try:
yield
@@ -277,8 +275,8 @@ class OptionsReader:
name: str,
*,
env_plat: bool = True,
- sep: Optional[str] = None,
- table: Optional[TableFmt] = None,
+ sep: str | None = None,
+ table: TableFmt | None = None,
ignore_empty: bool = False,
) -> str:
"""
@@ -342,7 +340,7 @@ class Options:
)
@property
- def config_file_path(self) -> Optional[Path]:
+ def config_file_path(self) -> Path | None:
args = self.command_line_arguments
if args.config_file:
@@ -356,7 +354,7 @@ class Options:
return None
@cached_property
- def package_requires_python_str(self) -> Optional[str]:
+ def package_requires_python_str(self) -> str | None:
args = self.command_line_arguments
return get_requires_python_str(Path(args.package_dir))
@@ -376,7 +374,7 @@ class Options:
# This is not supported in tool.cibuildwheel, as it comes from a standard location.
# Passing this in as an environment variable will override pyproject.toml, setup.cfg, or setup.py
- requires_python_str: Optional[str] = (
+ requires_python_str: str | None = (
os.environ.get("CIBW_PROJECT_REQUIRES_PYTHON") or self.package_requires_python_str
)
requires_python = None if requires_python_str is None else SpecifierSet(requires_python_str)
@@ -392,15 +390,25 @@ class Options:
archs_config_str = args.archs or self.reader.get("archs", sep=" ")
architectures = Architecture.parse_config(archs_config_str, platform=self.platform)
+ container_engine_str = self.reader.get("container-engine")
+
+ if container_engine_str not in ["docker", "podman"]:
+ msg = f"cibuildwheel: Unrecognised container_engine '{container_engine_str}', only 'docker' and 'podman' are supported"
+ print(msg, file=sys.stderr)
+ sys.exit(2)
+
+ container_engine = cast(ContainerEngine, container_engine_str)
+
return GlobalOptions(
package_dir=package_dir,
output_dir=output_dir,
build_selector=build_selector,
test_selector=test_selector,
architectures=architectures,
+ container_engine=container_engine,
)
- def build_options(self, identifier: Optional[str]) -> BuildOptions:
+ def build_options(self, identifier: str | None) -> BuildOptions:
"""
Compute BuildOptions for a single run configuration.
"""
@@ -452,9 +460,9 @@ class Options:
pass
if dependency_versions == "pinned":
- dependency_constraints: Optional[
+ dependency_constraints: None | (
DependencyConstraints
- ] = DependencyConstraints.with_defaults()
+ ) = DependencyConstraints.with_defaults()
elif dependency_versions == "latest":
dependency_constraints = None
else:
@@ -469,13 +477,13 @@ class Options:
except ValueError:
build_verbosity = 0
- manylinux_images: Dict[str, str] = {}
- musllinux_images: Dict[str, str] = {}
+ manylinux_images: dict[str, str] = {}
+ musllinux_images: dict[str, str] = {}
if self.platform == "linux":
- all_pinned_docker_images = _get_pinned_docker_images()
+ all_pinned_container_images = _get_pinned_container_images()
for build_platform in MANYLINUX_ARCHS:
- pinned_images = all_pinned_docker_images[build_platform]
+ pinned_images = all_pinned_container_images[build_platform]
config_value = self.reader.get(
f"manylinux-{build_platform}-image", ignore_empty=True
@@ -492,7 +500,7 @@ class Options:
manylinux_images[build_platform] = image
for build_platform in MUSLLINUX_ARCHS:
- pinned_images = all_pinned_docker_images[build_platform]
+ pinned_images = all_pinned_container_images[build_platform]
config_value = self.reader.get(f"musllinux-{build_platform}-image")
@@ -522,7 +530,7 @@ class Options:
build_frontend=build_frontend,
)
- def check_for_invalid_configuration(self, identifiers: List[str]) -> None:
+ def check_for_invalid_configuration(self, identifiers: list[str]) -> None:
if self.platform in ["macos", "windows"]:
before_all_values = {self.build_options(i).before_all for i in identifiers}
@@ -545,15 +553,15 @@ class Options:
deprecated_selectors("CIBW_SKIP", build_selector.skip_config)
deprecated_selectors("CIBW_TEST_SKIP", test_selector.skip_config)
- def summary(self, identifiers: List[str]) -> str:
+ def summary(self, identifiers: list[str]) -> str:
lines = [
f"{option_name}: {option_value!r}"
- for option_name, option_value in sorted(self.globals._asdict().items())
+ for option_name, option_value in sorted(asdict(self.globals).items())
]
build_option_defaults = self.build_options(identifier=None)
- for option_name, default_value in sorted(build_option_defaults._asdict().items()):
+ for option_name, default_value in sorted(asdict(build_option_defaults).items()):
if option_name == "globals":
continue
@@ -561,7 +569,7 @@ class Options:
# if any identifiers have an overridden value, print that too
for identifier in identifiers:
- option_value = self.build_options(identifier=identifier)._asdict()[option_name]
+ option_value = getattr(self.build_options(identifier=identifier), option_name)
if option_value != default_value:
lines.append(f" {identifier}: {option_value!r}")
@@ -578,7 +586,7 @@ def compute_options(
@functools.lru_cache(maxsize=None)
-def _get_pinned_docker_images() -> Mapping[str, Mapping[str, str]]:
+def _get_pinned_container_images() -> Mapping[str, Mapping[str, str]]:
"""
This looks like a dict of dicts, e.g.
{ 'x86_64': {'manylinux1': '...', 'manylinux2010': '...', 'manylinux2014': '...'},
@@ -587,10 +595,10 @@ def _get_pinned_docker_images() -> Mapping[str, Mapping[str, str]]:
... }
"""
- pinned_docker_images_file = resources_dir / "pinned_docker_images.cfg"
- all_pinned_docker_images = ConfigParser()
- all_pinned_docker_images.read(pinned_docker_images_file)
- return all_pinned_docker_images
+ pinned_images_file = resources_dir / "pinned_docker_images.cfg"
+ all_pinned_images = ConfigParser()
+ all_pinned_images.read(pinned_images_file)
+ return all_pinned_images
def deprecated_selectors(name: str, selector: str, *, error: bool = False) -> None:
diff --git a/cibuildwheel/projectfiles.py b/cibuildwheel/projectfiles.py
index 90d388f6..b5c3cbdf 100644
--- a/cibuildwheel/projectfiles.py
+++ b/cibuildwheel/projectfiles.py
@@ -1,8 +1,10 @@
+from __future__ import annotations
+
import ast
import sys
from configparser import ConfigParser
from pathlib import Path
-from typing import Any, Optional
+from typing import Any
if sys.version_info >= (3, 11):
import tomllib
@@ -24,7 +26,7 @@ else:
class Analyzer(ast.NodeVisitor):
def __init__(self) -> None:
- self.requires_python: Optional[str] = None
+ self.requires_python: str | None = None
def visit(self, node: ast.AST) -> None:
for inner_node in ast.walk(node):
@@ -43,7 +45,7 @@ class Analyzer(ast.NodeVisitor):
self.requires_python = get_constant(node.value)
-def setup_py_python_requires(content: str) -> Optional[str]:
+def setup_py_python_requires(content: str) -> str | None:
try:
tree = ast.parse(content)
analyzer = Analyzer()
@@ -53,7 +55,7 @@ def setup_py_python_requires(content: str) -> Optional[str]:
return None
-def get_requires_python_str(package_dir: Path) -> Optional[str]:
+def get_requires_python_str(package_dir: Path) -> str | None:
"""Return the python requires string from the most canonical source available, or None"""
# Read in from pyproject.toml:project.requires-python
diff --git a/cibuildwheel/resources/build-platforms.toml b/cibuildwheel/resources/build-platforms.toml
index fe8528d3..533c4db6 100644
--- a/cibuildwheel/resources/build-platforms.toml
+++ b/cibuildwheel/resources/build-platforms.toml
@@ -84,9 +84,9 @@ python_configurations = [
{ identifier = "cp310-macosx_x86_64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.5/python-3.10.5-macos11.pkg" },
{ identifier = "cp310-macosx_arm64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.5/python-3.10.5-macos11.pkg" },
{ identifier = "cp310-macosx_universal2", version = "3.10", url = "https://www.python.org/ftp/python/3.10.5/python-3.10.5-macos11.pkg" },
- { identifier = "cp311-macosx_x86_64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.0/python-3.11.0b3-macos11.pkg" },
- { identifier = "cp311-macosx_arm64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.0/python-3.11.0b3-macos11.pkg" },
- { identifier = "cp311-macosx_universal2", version = "3.11", url = "https://www.python.org/ftp/python/3.11.0/python-3.11.0b3-macos11.pkg" },
+ { identifier = "cp311-macosx_x86_64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.0/python-3.11.0b4-macos11.pkg" },
+ { identifier = "cp311-macosx_arm64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.0/python-3.11.0b4-macos11.pkg" },
+ { identifier = "cp311-macosx_universal2", version = "3.11", url = "https://www.python.org/ftp/python/3.11.0/python-3.11.0b4-macos11.pkg" },
{ identifier = "pp37-macosx_x86_64", version = "3.7", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.9-osx64.tar.bz2" },
{ identifier = "pp38-macosx_x86_64", version = "3.8", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.9-osx64.tar.bz2" },
{ identifier = "pp39-macosx_x86_64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.9-osx64.tar.bz2" },
@@ -104,11 +104,11 @@ python_configurations = [
{ identifier = "cp39-win_amd64", version = "3.9.13", arch = "64" },
{ identifier = "cp310-win32", version = "3.10.5", arch = "32" },
{ identifier = "cp310-win_amd64", version = "3.10.5", arch = "64" },
- { identifier = "cp311-win32", version = "3.11.0-b3", arch = "32" },
- { identifier = "cp311-win_amd64", version = "3.11.0-b3", arch = "64" },
+ { identifier = "cp311-win32", version = "3.11.0-b4", arch = "32" },
+ { identifier = "cp311-win_amd64", version = "3.11.0-b4", arch = "64" },
{ identifier = "cp39-win_arm64", version = "3.9.10", arch = "ARM64" },
{ identifier = "cp310-win_arm64", version = "3.10.5", arch = "ARM64" },
- { identifier = "cp311-win_arm64", version = "3.11.0-b3", arch = "ARM64" },
+ { identifier = "cp311-win_arm64", version = "3.11.0-b4", arch = "ARM64" },
{ identifier = "pp37-win_amd64", version = "3.7", arch = "64", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.9-win64.zip" },
{ identifier = "pp38-win_amd64", version = "3.8", arch = "64", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.9-win64.zip" },
{ identifier = "pp39-win_amd64", version = "3.9", arch = "64", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.9-win64.zip" },
diff --git a/cibuildwheel/resources/constraints-python310.txt b/cibuildwheel/resources/constraints-python310.txt
index 9c96aa12..f3d3b7f9 100644
--- a/cibuildwheel/resources/constraints-python310.txt
+++ b/cibuildwheel/resources/constraints-python310.txt
@@ -6,7 +6,7 @@
#
delocate==0.10.2
# via -r cibuildwheel/resources/constraints.in
-distlib==0.3.4
+distlib==0.3.5
# via virtualenv
filelock==3.7.1
# via virtualenv
@@ -14,9 +14,9 @@ platformdirs==2.5.2
# via virtualenv
six==1.16.0
# via virtualenv
-typing-extensions==4.2.0
+typing-extensions==4.3.0
# via delocate
-virtualenv==20.14.1
+virtualenv==20.15.1
# via -r cibuildwheel/resources/constraints.in
wheel==0.37.1
# via
@@ -24,7 +24,7 @@ wheel==0.37.1
# delocate
# The following packages are considered to be unsafe in a requirements file:
-pip==22.1.2
+pip==22.2
# via -r cibuildwheel/resources/constraints.in
-setuptools==62.4.0
+setuptools==63.2.0
# via -r cibuildwheel/resources/constraints.in
diff --git a/cibuildwheel/resources/constraints-python311.txt b/cibuildwheel/resources/constraints-python311.txt
index 82e6c46d..849abf49 100644
--- a/cibuildwheel/resources/constraints-python311.txt
+++ b/cibuildwheel/resources/constraints-python311.txt
@@ -6,7 +6,7 @@
#
delocate==0.10.2
# via -r cibuildwheel/resources/constraints.in
-distlib==0.3.4
+distlib==0.3.5
# via virtualenv
filelock==3.7.1
# via virtualenv
@@ -14,9 +14,9 @@ platformdirs==2.5.2
# via virtualenv
six==1.16.0
# via virtualenv
-typing-extensions==4.2.0
+typing-extensions==4.3.0
# via delocate
-virtualenv==20.14.1
+virtualenv==20.15.1
# via -r cibuildwheel/resources/constraints.in
wheel==0.37.1
# via
@@ -24,7 +24,7 @@ wheel==0.37.1
# delocate
# The following packages are considered to be unsafe in a requirements file:
-pip==22.1.2
+pip==22.2
# via -r cibuildwheel/resources/constraints.in
-setuptools==62.4.0
+setuptools==63.2.0
# via -r cibuildwheel/resources/constraints.in
diff --git a/cibuildwheel/resources/constraints-python36.txt b/cibuildwheel/resources/constraints-python36.txt
index 9ca0fd11..8ab9294a 100644
--- a/cibuildwheel/resources/constraints-python36.txt
+++ b/cibuildwheel/resources/constraints-python36.txt
@@ -6,7 +6,7 @@
#
delocate==0.10.2
# via -r cibuildwheel/resources/constraints.in
-distlib==0.3.4
+distlib==0.3.5
# via virtualenv
filelock==3.4.1
# via virtualenv
@@ -22,7 +22,7 @@ typing-extensions==4.1.1
# via
# delocate
# importlib-metadata
-virtualenv==20.14.1
+virtualenv==20.15.1
# via -r cibuildwheel/resources/constraints.in
wheel==0.37.1
# via
diff --git a/cibuildwheel/resources/constraints-python37.txt b/cibuildwheel/resources/constraints-python37.txt
index e0a25472..78ea26b2 100644
--- a/cibuildwheel/resources/constraints-python37.txt
+++ b/cibuildwheel/resources/constraints-python37.txt
@@ -6,31 +6,31 @@
#
delocate==0.10.2
# via -r cibuildwheel/resources/constraints.in
-distlib==0.3.4
+distlib==0.3.5
# via virtualenv
filelock==3.7.1
# via virtualenv
-importlib-metadata==4.11.4
+importlib-metadata==4.12.0
# via virtualenv
platformdirs==2.5.2
# via virtualenv
six==1.16.0
# via virtualenv
-typing-extensions==4.2.0
+typing-extensions==4.3.0
# via
# delocate
# importlib-metadata
-virtualenv==20.14.1
+virtualenv==20.15.1
# via -r cibuildwheel/resources/constraints.in
wheel==0.37.1
# via
# -r cibuildwheel/resources/constraints.in
# delocate
-zipp==3.8.0
+zipp==3.8.1
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
-pip==22.1.2
+pip==22.2
# via -r cibuildwheel/resources/constraints.in
-setuptools==62.4.0
+setuptools==63.2.0
# via -r cibuildwheel/resources/constraints.in
diff --git a/cibuildwheel/resources/constraints-python38.txt b/cibuildwheel/resources/constraints-python38.txt
index 971a8e42..aa06de6e 100644
--- a/cibuildwheel/resources/constraints-python38.txt
+++ b/cibuildwheel/resources/constraints-python38.txt
@@ -6,7 +6,7 @@
#
delocate==0.10.2
# via -r cibuildwheel/resources/constraints.in
-distlib==0.3.4
+distlib==0.3.5
# via virtualenv
filelock==3.7.1
# via virtualenv
@@ -14,9 +14,9 @@ platformdirs==2.5.2
# via virtualenv
six==1.16.0
# via virtualenv
-typing-extensions==4.2.0
+typing-extensions==4.3.0
# via delocate
-virtualenv==20.14.1
+virtualenv==20.15.1
# via -r cibuildwheel/resources/constraints.in
wheel==0.37.1
# via
@@ -24,7 +24,7 @@ wheel==0.37.1
# delocate
# The following packages are considered to be unsafe in a requirements file:
-pip==22.1.2
+pip==22.2
# via -r cibuildwheel/resources/constraints.in
-setuptools==62.4.0
+setuptools==63.2.0
# via -r cibuildwheel/resources/constraints.in
diff --git a/cibuildwheel/resources/constraints-python39.txt b/cibuildwheel/resources/constraints-python39.txt
index 8f6d1c00..0aed606e 100644
--- a/cibuildwheel/resources/constraints-python39.txt
+++ b/cibuildwheel/resources/constraints-python39.txt
@@ -6,7 +6,7 @@
#
delocate==0.10.2
# via -r cibuildwheel/resources/constraints.in
-distlib==0.3.4
+distlib==0.3.5
# via virtualenv
filelock==3.7.1
# via virtualenv
@@ -14,9 +14,9 @@ platformdirs==2.5.2
# via virtualenv
six==1.16.0
# via virtualenv
-typing-extensions==4.2.0
+typing-extensions==4.3.0
# via delocate
-virtualenv==20.14.1
+virtualenv==20.15.1
# via -r cibuildwheel/resources/constraints.in
wheel==0.37.1
# via
@@ -24,7 +24,7 @@ wheel==0.37.1
# delocate
# The following packages are considered to be unsafe in a requirements file:
-pip==22.1.2
+pip==22.2
# via -r cibuildwheel/resources/constraints.in
-setuptools==62.4.0
+setuptools==63.2.0
# via -r cibuildwheel/resources/constraints.in
diff --git a/cibuildwheel/resources/constraints.txt b/cibuildwheel/resources/constraints.txt
index 8f6d1c00..0aed606e 100644
--- a/cibuildwheel/resources/constraints.txt
+++ b/cibuildwheel/resources/constraints.txt
@@ -6,7 +6,7 @@
#
delocate==0.10.2
# via -r cibuildwheel/resources/constraints.in
-distlib==0.3.4
+distlib==0.3.5
# via virtualenv
filelock==3.7.1
# via virtualenv
@@ -14,9 +14,9 @@ platformdirs==2.5.2
# via virtualenv
six==1.16.0
# via virtualenv
-typing-extensions==4.2.0
+typing-extensions==4.3.0
# via delocate
-virtualenv==20.14.1
+virtualenv==20.15.1
# via -r cibuildwheel/resources/constraints.in
wheel==0.37.1
# via
@@ -24,7 +24,7 @@ wheel==0.37.1
# delocate
# The following packages are considered to be unsafe in a requirements file:
-pip==22.1.2
+pip==22.2
# via -r cibuildwheel/resources/constraints.in
-setuptools==62.4.0
+setuptools==63.2.0
# via -r cibuildwheel/resources/constraints.in
diff --git a/cibuildwheel/resources/defaults.toml b/cibuildwheel/resources/defaults.toml
index 5890c8ed..5e0bdacf 100644
--- a/cibuildwheel/resources/defaults.toml
+++ b/cibuildwheel/resources/defaults.toml
@@ -19,6 +19,8 @@ before-test = ""
test-requires = []
test-extras = []
+container-engine = "docker"
+
manylinux-x86_64-image = "manylinux2014"
manylinux-i686-image = "manylinux2014"
manylinux-aarch64-image = "manylinux2014"
diff --git a/cibuildwheel/resources/pinned_docker_images.cfg b/cibuildwheel/resources/pinned_docker_images.cfg
index c4f81a5d..1fe4bf41 100644
--- a/cibuildwheel/resources/pinned_docker_images.cfg
+++ b/cibuildwheel/resources/pinned_docker_images.cfg
@@ -1,43 +1,48 @@
[x86_64]
-manylinux1 = quay.io/pypa/manylinux1_x86_64:2022-06-12-a846b05
-manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-06-13-c365205
-manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2022-06-13-c365205
-manylinux_2_24 = quay.io/pypa/manylinux_2_24_x86_64:2022-06-13-c365205
-musllinux_1_1 = quay.io/pypa/musllinux_1_1_x86_64:2022-06-13-c365205
+manylinux1 = quay.io/pypa/manylinux1_x86_64:2022-07-17-f8ddacc
+manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-07-21-947f13c
+manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2022-07-21-947f13c
+manylinux_2_24 = quay.io/pypa/manylinux_2_24_x86_64:2022-07-21-947f13c
+manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2022-07-21-947f13c
+musllinux_1_1 = quay.io/pypa/musllinux_1_1_x86_64:2022-07-21-947f13c
[i686]
-manylinux1 = quay.io/pypa/manylinux1_i686:2022-06-12-a846b05
-manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-06-13-c365205
-manylinux2014 = quay.io/pypa/manylinux2014_i686:2022-06-13-c365205
-manylinux_2_24 = quay.io/pypa/manylinux_2_24_i686:2022-06-13-c365205
-musllinux_1_1 = quay.io/pypa/musllinux_1_1_i686:2022-06-13-c365205
+manylinux1 = quay.io/pypa/manylinux1_i686:2022-07-17-f8ddacc
+manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-07-21-947f13c
+manylinux2014 = quay.io/pypa/manylinux2014_i686:2022-07-21-947f13c
+manylinux_2_24 = quay.io/pypa/manylinux_2_24_i686:2022-07-21-947f13c
+musllinux_1_1 = quay.io/pypa/musllinux_1_1_i686:2022-07-21-947f13c
[pypy_x86_64]
-manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-06-13-c365205
-manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2022-06-13-c365205
-manylinux_2_24 = quay.io/pypa/manylinux_2_24_x86_64:2022-06-13-c365205
+manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-07-21-947f13c
+manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2022-07-21-947f13c
+manylinux_2_24 = quay.io/pypa/manylinux_2_24_x86_64:2022-07-21-947f13c
+manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2022-07-21-947f13c
[pypy_i686]
-manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-06-13-c365205
-manylinux2014 = quay.io/pypa/manylinux2014_i686:2022-06-13-c365205
-manylinux_2_24 = quay.io/pypa/manylinux_2_24_i686:2022-06-13-c365205
+manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-07-21-947f13c
+manylinux2014 = quay.io/pypa/manylinux2014_i686:2022-07-21-947f13c
+manylinux_2_24 = quay.io/pypa/manylinux_2_24_i686:2022-07-21-947f13c
[aarch64]
-manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2022-06-13-c365205
-manylinux_2_24 = quay.io/pypa/manylinux_2_24_aarch64:2022-06-13-c365205
-musllinux_1_1 = quay.io/pypa/musllinux_1_1_aarch64:2022-06-13-c365205
+manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2022-07-21-947f13c
+manylinux_2_24 = quay.io/pypa/manylinux_2_24_aarch64:2022-07-21-947f13c
+manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2022-07-21-947f13c
+musllinux_1_1 = quay.io/pypa/musllinux_1_1_aarch64:2022-07-21-947f13c
[ppc64le]
-manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2022-06-13-c365205
-manylinux_2_24 = quay.io/pypa/manylinux_2_24_ppc64le:2022-06-13-c365205
-musllinux_1_1 = quay.io/pypa/musllinux_1_1_ppc64le:2022-06-13-c365205
+manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2022-07-21-947f13c
+manylinux_2_24 = quay.io/pypa/manylinux_2_24_ppc64le:2022-07-21-947f13c
+manylinux_2_28 = quay.io/pypa/manylinux_2_28_ppc64le:2022-07-21-947f13c
+musllinux_1_1 = quay.io/pypa/musllinux_1_1_ppc64le:2022-07-21-947f13c
[s390x]
-manylinux2014 = quay.io/pypa/manylinux2014_s390x:2022-06-13-c365205
-manylinux_2_24 = quay.io/pypa/manylinux_2_24_s390x:2022-06-13-c365205
-musllinux_1_1 = quay.io/pypa/musllinux_1_1_s390x:2022-06-13-c365205
+manylinux2014 = quay.io/pypa/manylinux2014_s390x:2022-07-21-947f13c
+manylinux_2_24 = quay.io/pypa/manylinux_2_24_s390x:2022-07-21-947f13c
+musllinux_1_1 = quay.io/pypa/musllinux_1_1_s390x:2022-07-21-947f13c
[pypy_aarch64]
-manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2022-06-13-c365205
-manylinux_2_24 = quay.io/pypa/manylinux_2_24_aarch64:2022-06-13-c365205
+manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2022-07-21-947f13c
+manylinux_2_24 = quay.io/pypa/manylinux_2_24_aarch64:2022-07-21-947f13c
+manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2022-07-21-947f13c
diff --git a/cibuildwheel/resources/virtualenv.toml b/cibuildwheel/resources/virtualenv.toml
index 2296e59f..c9ae14af 100644
--- a/cibuildwheel/resources/virtualenv.toml
+++ b/cibuildwheel/resources/virtualenv.toml
@@ -1,2 +1,2 @@
-version = "20.14.1"
-url = "https://github.com/pypa/get-virtualenv/blob/20.14.1/public/virtualenv.pyz?raw=true"
+version = "20.15.1"
+url = "https://github.com/pypa/get-virtualenv/blob/20.15.1/public/virtualenv.pyz?raw=true"
diff --git a/cibuildwheel/typing.py b/cibuildwheel/typing.py
index 1e6e5787..a546d7bf 100644
--- a/cibuildwheel/typing.py
+++ b/cibuildwheel/typing.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import os
import subprocess
import sys
@@ -36,7 +38,7 @@ else:
PlatformName = Literal["linux", "macos", "windows"]
-PLATFORMS: Final[Set[PlatformName]] = {"linux", "macos", "windows"}
+PLATFORMS: Final[set[PlatformName]] = {"linux", "macos", "windows"}
def assert_never(value: NoReturn) -> NoReturn:
diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py
index db0cc687..6a6a6e2a 100644
--- a/cibuildwheel/util.py
+++ b/cibuildwheel/util.py
@@ -1,5 +1,6 @@
+from __future__ import annotations
+
import contextlib
-import dataclasses
import fnmatch
import itertools
import os
@@ -11,6 +12,7 @@ import sys
import textwrap
import time
import urllib.request
+from dataclasses import dataclass
from enum import Enum
from functools import lru_cache
from pathlib import Path, PurePath
@@ -18,16 +20,11 @@ from time import sleep
from typing import (
Any,
ClassVar,
- Dict,
Generator,
Iterable,
- List,
- NamedTuple,
- Optional,
Sequence,
TextIO,
TypeVar,
- Union,
cast,
overload,
)
@@ -54,7 +51,7 @@ __all__ = [
"MANYLINUX_ARCHS",
"call",
"shell",
- "find_compatible_abi3_wheel",
+ "find_compatible_wheel",
"format_safe",
"prepare_command",
"get_build_verbosity_extra_flags",
@@ -99,8 +96,8 @@ IS_WIN: Final = sys.platform.startswith("win")
@overload
def call(
*args: PathOrStr,
- env: Optional[Dict[str, str]] = None,
- cwd: Optional[PathOrStr] = None,
+ env: dict[str, str] | None = None,
+ cwd: PathOrStr | None = None,
capture_stdout: Literal[False] = ...,
) -> None:
...
@@ -109,8 +106,8 @@ def call(
@overload
def call(
*args: PathOrStr,
- env: Optional[Dict[str, str]] = None,
- cwd: Optional[PathOrStr] = None,
+ env: dict[str, str] | None = None,
+ cwd: PathOrStr | None = None,
capture_stdout: Literal[True],
) -> str:
...
@@ -118,10 +115,10 @@ def call(
def call(
*args: PathOrStr,
- env: Optional[Dict[str, str]] = None,
- cwd: Optional[PathOrStr] = None,
+ env: dict[str, str] | None = None,
+ cwd: PathOrStr | None = None,
capture_stdout: bool = False,
-) -> Optional[str]:
+) -> str | None:
"""
Run subprocess.run, but print the commands first. Takes the commands as
*args. Uses shell=True on Windows due to a bug. Also converts to
@@ -131,7 +128,7 @@ def call(
args_ = [str(arg) for arg in args]
# print the command executing for the logs
print("+ " + " ".join(shlex.quote(a) for a in args_))
- kwargs: Dict[str, Any] = {}
+ kwargs: dict[str, Any] = {}
if capture_stdout:
kwargs["universal_newlines"] = True
kwargs["stdout"] = subprocess.PIPE
@@ -141,9 +138,7 @@ def call(
return cast(str, result.stdout)
-def shell(
- *commands: str, env: Optional[Dict[str, str]] = None, cwd: Optional[PathOrStr] = None
-) -> None:
+def shell(*commands: str, env: dict[str, str] | None = None, cwd: PathOrStr | None = None) -> None:
command = " ".join(commands)
print(f"+ {command}")
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
@@ -201,7 +196,7 @@ def prepare_command(command: str, **kwargs: PathOrStr) -> str:
return format_safe(command, python="python", pip="pip", **kwargs)
-def get_build_verbosity_extra_flags(level: int) -> List[str]:
+def get_build_verbosity_extra_flags(level: int) -> list[str]:
if level > 0:
return ["-" + level * "v"]
elif level < 0:
@@ -210,11 +205,11 @@ def get_build_verbosity_extra_flags(level: int) -> List[str]:
return []
-def read_python_configs(config: PlatformName) -> List[Dict[str, str]]:
+def read_python_configs(config: PlatformName) -> list[dict[str, str]]:
input_file = resources_dir / "build-platforms.toml"
with input_file.open("rb") as f:
loaded_file = tomllib.load(f)
- results: List[Dict[str, str]] = list(loaded_file[config]["python_configurations"])
+ results: list[dict[str, str]] = list(loaded_file[config]["python_configurations"])
return results
@@ -233,7 +228,7 @@ def selector_matches(patterns: str, string: str) -> bool:
# Once we require Python 3.10+, we can add kw_only=True
-@dataclasses.dataclass
+@dataclass(frozen=True)
class BuildSelector:
"""
This class holds a set of build/skip patterns. You call an instance with a
@@ -244,7 +239,7 @@ class BuildSelector:
build_config: str
skip_config: str
- requires_python: Optional[SpecifierSet] = None
+ requires_python: SpecifierSet | None = None
# a pattern that skips prerelease versions, when include_prereleases is False.
PRERELEASE_SKIP: ClassVar[str] = "cp311-*"
@@ -270,7 +265,7 @@ class BuildSelector:
return should_build and not should_skip
-@dataclasses.dataclass
+@dataclass(frozen=True)
class TestSelector:
"""
A build selector that can only skip tests according to a skip pattern.
@@ -317,7 +312,7 @@ def download(url: str, dest: Path) -> None:
dest.write_bytes(response.read())
return
- except urllib.error.URLError:
+ except OSError:
if i == repeat_num - 1:
raise
sleep(3)
@@ -329,7 +324,7 @@ class DependencyConstraints:
self.base_file_path = base_file_path.resolve()
@staticmethod
- def with_defaults() -> "DependencyConstraints":
+ def with_defaults() -> DependencyConstraints:
return DependencyConstraints(base_file_path=resources_dir / "constraints.txt")
def get_for_python_version(self, version: str) -> Path:
@@ -373,6 +368,20 @@ class NonPlatformWheelError(Exception):
super().__init__(message)
+class AlreadyBuiltWheelError(Exception):
+ def __init__(self, wheel_name: str) -> None:
+ message = textwrap.dedent(
+ f"""
+ cibuildwheel: Build failed because a wheel named {wheel_name} was already generated in the current run.
+
+ If you expected another wheel to be generated, check your project configuration, or run
+ cibuildwheel with CIBW_BUILD_VERBOSITY=1 to view build logs.
+ """
+ )
+
+ super().__init__(message)
+
+
def strtobool(val: str) -> bool:
return val.lower() in {"y", "yes", "t", "true", "on", "1"}
@@ -387,7 +396,7 @@ class CIProvider(Enum):
other = "other"
-def detect_ci_provider() -> Optional[CIProvider]:
+def detect_ci_provider() -> CIProvider | None:
if "TRAVIS" in os.environ:
return CIProvider.travis_ci
elif "APPVEYOR" in os.environ:
@@ -418,6 +427,12 @@ def unwrap(text: str) -> str:
return re.sub(r"\s+", " ", text)
+@dataclass(frozen=True)
+class FileReport:
+ name: str
+ size: str
+
+
@contextlib.contextmanager
def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
"""
@@ -432,10 +447,6 @@ def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
yield
final_contents = set(output_dir.iterdir())
- class FileReport(NamedTuple):
- name: str
- size: str
-
new_contents = [
FileReport(wheel.name, f"{(wheel.stat().st_size + 1023) // 1024:,d}")
for wheel in final_contents - existing_contents
@@ -458,7 +469,7 @@ def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
)
-def get_pip_version(env: Dict[str, str]) -> str:
+def get_pip_version(env: dict[str, str]) -> str:
versions_output_text = call(
"python", "-m", "pip", "freeze", "--all", capture_stdout=True, env=env
)
@@ -486,7 +497,7 @@ def _ensure_virtualenv() -> Path:
def _parse_constraints_for_virtualenv(
dependency_constraint_flags: Sequence[PathOrStr],
-) -> Dict[str, str]:
+) -> dict[str, str]:
"""
Parses the constraints file referenced by `dependency_constraint_flags` and returns a dict where
the key is the package name, and the value is the constraint version.
@@ -532,7 +543,7 @@ def _parse_constraints_for_virtualenv(
def virtualenv(
python: Path, venv_path: Path, dependency_constraint_flags: Sequence[PathOrStr]
-) -> Dict[str, str]:
+) -> dict[str, str]:
assert python.exists()
virtualenv_app = _ensure_virtualenv()
constraints = _parse_constraints_for_virtualenv(dependency_constraint_flags)
@@ -574,36 +585,47 @@ def virtualenv(
T = TypeVar("T", bound=PurePath)
-def find_compatible_abi3_wheel(wheels: Sequence[T], identifier: str) -> Optional[T]:
+def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None:
"""
- Finds an ABI3 wheel in `wheels` compatible with the Python interpreter
- specified by `identifier`.
+ Finds a wheel with an abi3 or a none ABI tag in `wheels` compatible with the Python interpreter
+ specified by `identifier` that is previously built.
"""
interpreter, platform = identifier.split("-")
- if not interpreter.startswith("cp3"):
- return None
for wheel in wheels:
_, _, _, tags = parse_wheel_filename(wheel.name)
for tag in tags:
- if tag.abi != "abi3":
+ if tag.abi == "abi3":
+ # ABI3 wheels must start with cp3 for impl and tag
+ if not (interpreter.startswith("cp3") and tag.interpreter.startswith("cp3")):
+ continue
+ elif tag.abi == "none":
+ # CPythonless wheels must include py3 tag
+ if tag.interpreter[:3] != "py3":
+ continue
+ else:
+ # Other types of wheels are not detected, this is looking for previously built wheels.
continue
- if not tag.interpreter.startswith("cp3"):
- continue
- if int(tag.interpreter[3:]) > int(interpreter[3:]):
+
+ if tag.interpreter != "py3" and int(tag.interpreter[3:]) > int(interpreter[3:]):
+ # If a minor version number is given, it has to be lower than the current one.
continue
+
if platform.startswith(("manylinux", "musllinux", "macosx")):
- # Linux, macOS
+ # Linux, macOS require the beginning and ending match (macos/manylinux version doesn't need to)
os_, arch = platform.split("_", 1)
if not tag.platform.startswith(os_):
continue
- if not tag.platform.endswith("_" + arch):
+ if not tag.platform.endswith(f"_{arch}"):
continue
else:
- # Windows
+ # Windows should exactly match
if not tag.platform == platform:
continue
+
+ # If all the filters above pass, then the wheel is a previously built compatible wheel.
return wheel
+
return None
@@ -615,7 +637,7 @@ else:
# Can be replaced by contextlib.chdir in Python 3.11
@contextlib.contextmanager
-def chdir(new_path: Union[Path, str]) -> Generator[None, None, None]:
+def chdir(new_path: Path | str) -> Generator[None, None, None]:
"""Non thread-safe context manager to change the current working directory."""
cwd = os.getcwd()
diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py
index 3e35a912..1a0b71cd 100644
--- a/cibuildwheel/windows.py
+++ b/cibuildwheel/windows.py
@@ -1,11 +1,14 @@
+from __future__ import annotations
+
import os
import platform as platform_module
import shutil
import subprocess
import sys
+from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
-from typing import Dict, List, NamedTuple, Optional, Sequence, Set
+from typing import Sequence
from zipfile import ZipFile
from filelock import FileLock
@@ -18,12 +21,13 @@ from .options import Options
from .typing import PathOrStr, assert_never
from .util import (
CIBW_CACHE_PATH,
+ AlreadyBuiltWheelError,
BuildFrontend,
BuildSelector,
NonPlatformWheelError,
call,
download,
- find_compatible_abi3_wheel,
+ find_compatible_wheel,
get_build_verbosity_extra_flags,
get_pip_version,
prepare_command,
@@ -53,17 +57,18 @@ def get_nuget_args(version: str, arch: str, output_directory: Path) -> List[str]
]
-class PythonConfiguration(NamedTuple):
+@dataclass(frozen=True)
+class PythonConfiguration:
version: str
arch: str
identifier: str
- url: Optional[str] = None
+ url: str | None = None
def get_python_configurations(
build_selector: BuildSelector,
- architectures: Set[Architecture],
-) -> List[PythonConfiguration]:
+ architectures: set[Architecture],
+) -> list[PythonConfiguration]:
full_python_configs = read_python_configs("windows")
@@ -172,7 +177,7 @@ def setup_python(
dependency_constraint_flags: Sequence[PathOrStr],
environment: ParsedEnvironment,
build_frontend: BuildFrontend,
-) -> Dict[str, str]:
+) -> dict[str, str]:
tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0]
python_libs_base = None
@@ -318,7 +323,7 @@ def build(options: Options, tmp_path: Path) -> None:
)
shell(before_all_prepared, env=env)
- built_wheels: List[Path] = []
+ built_wheels: list[Path] = []
for config in python_configurations:
build_options = options.build_options(config.identifier)
@@ -345,13 +350,13 @@ def build(options: Options, tmp_path: Path) -> None:
build_options.build_frontend,
)
- abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier)
- if abi3_wheel:
+ compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
+ if compatible_wheel:
log.step_end()
print(
- f"\nFound previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
+ f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
)
- repaired_wheel = abi3_wheel
+ repaired_wheel = compatible_wheel
else:
# run the before_build command
if build_options.before_build:
@@ -433,6 +438,9 @@ def build(options: Options, tmp_path: Path) -> None:
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
+ if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
+ raise AlreadyBuiltWheelError(repaired_wheel.name)
+
if build_options.test_command and options.globals.test_selector(config.identifier):
log.step("Testing wheel...")
# set up a virtual environment to install and test from, to make sure
@@ -486,7 +494,7 @@ def build(options: Options, tmp_path: Path) -> None:
shell(test_command_prepared, cwd="c:\\", env=virtualenv_env)
# we're all done here; move it to output (remove if already exists)
- if abi3_wheel is None:
+ if compatible_wheel is None:
shutil.move(str(repaired_wheel), build_options.output_dir)
built_wheels.append(build_options.output_dir / repaired_wheel.name)
diff --git a/docs/changelog.md b/docs/changelog.md
index 7f6f2101..6f1da5e4 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,37 @@
title: Changelog
---
+### v2.8.1
+
+_18 July 2022_
+
+- 🐛 Fix a bug when building CPython 3.8 wheels on an Apple Silicon machine where testing would always fail. cibuildwheel will no longer attempt to test the arm64 part of CPython 3.8 wheels because we use the x86_64 installer of CPython 3.8 due to its macOS system version backward-compatibility. See [#1169](https://github.com/pypa/cibuildwheel/pull/1169) for more details. (#1171)
+- 🛠 Update the prerelease CPython 3.11 to 3.11.0b4. (#1180)
+- 🛠 The GitHub Action will ensure a compatible version of Python is installed on the runner (#1114)
+- 📚 A few docs improvements
+
+### v2.8.0
+
+_5 July 2022_
+
+- ✨ You can now run cibuildwheel on Podman, as an alternate container engine to Docker (which remains the default). This is useful in environments where a Docker daemon isn't available, for example, it can be run inside a Docker container, or without root access. To use Podman, set the [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.readthedocs.io/en/stable/options/#container-engine) option. (#966)
+- ✨ Adds support for building `py3-none-{platform}` wheels. This works the same as ABI3 - wheels won't be rebuilt, but tests will still be run across all selected versions of Python.
+
+ > These wheels contain native extension code, but don't use the Python APIs. Typically, they're bridged to Python using a FFI module like [ctypes](https://docs.python.org/3/library/ctypes.html) or [cffi](https://cffi.readthedocs.io/en/latest/). Because they don't use Python ABI, the wheels are more compatible - they work across many Python versions.
+
+ Check out this [example ctypes project](https://github.com/joerick/python-ctypes-package-sample) to see an example of how it works. (#1151)
+- 🛠 cibuildwheel will now error if multiple builds in a single run produce the same wheel filename, as this indicates a misconfiguration. (#1152)
+- 📚 A few docs improvements and updates to keep things up-to-date.
+
+### v2.7.0
+
+_17 June 2022_
+
+- 🌟 Added support for the new `manylinux_2_28` images. These new images are based on AlmaLinux, the community-driven successor to CentOS, unlike manylinux_2_24, which was based on Debian. To build on these images, set your [`CIBW_MANYLINUX_*_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#linux-image) option to `manylinux_2_28`. (#1026)
+- 🐛 Fix a bug where tests were not being run on CPython 3.11 (when CIBW_PRERELEASE_PYTHONS was set) (#1138)
+- ✨ You can now build Linux wheels on Windows, as long as you have Docker installed and set to 'Linux containers' (#1117)
+- 🐛 Fix a bug on macOS that caused cibuildwheel to crash trying to overwrite a previously-built wheel of the same name. (#1129)
+
### v2.6.1
_7 June 2022_
diff --git a/docs/contributing.md b/docs/contributing.md
index c6a5b930..24566e4e 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -94,7 +94,6 @@ This has been moved to using docker, so you only need the following instructions
The dependency update script in the next section requires multiple python versions installed. One way to do this is to use `pyenv`:
```bash
-pyenv install 3.6.11
pyenv install 3.7.8
# Optionally add 3.8 and make it the local version;
# otherwise assuming 3.8+ already is your current python version
@@ -103,7 +102,6 @@ pyenv install 3.7.8
Then, you need to make the required virtual environments:
```bash
-$(pyenv prefix 3.6.11)/bin/python -m venv env36
$(pyenv prefix 3.7.8)/bin/python -m venv env37
```
diff --git a/docs/data/projects.yml b/docs/data/projects.yml
index 7e8c544a..b08d6fc4 100644
--- a/docs/data/projects.yml
+++ b/docs/data/projects.yml
@@ -589,3 +589,8 @@
gh: MarshalX/python-webrtc
ci: [github]
os: [windows, apple, linux]
+
+- name: cf-units
+ gh: SciTools/cf-units
+ ci: [github]
+ os: [apple, linux]
diff --git a/docs/deliver-to-pypi.md b/docs/deliver-to-pypi.md
index 610fe6e5..a81abb4e 100644
--- a/docs/deliver-to-pypi.md
+++ b/docs/deliver-to-pypi.md
@@ -46,7 +46,7 @@ well as several useful actions. Alongside your existing job(s) that runs cibuild
name: Make SDist
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
fetch-depth: 0 # Optional, use if you use setuptools_scm
submodules: true # Optional, use if you have submodules
@@ -54,7 +54,7 @@ well as several useful actions. Alongside your existing job(s) that runs cibuild
- name: Build SDist
run: pipx run build --sdist
- - uses: actions/upload-artifact@v2
+ - uses: actions/upload-artifact@v3
with:
path: dist/*.tar.gz
```
@@ -69,12 +69,12 @@ This requires a [PyPI upload token](https://pypi.org/manage/account/token/), sto
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'published'
steps:
- - uses: actions/download-artifact@v2
+ - uses: actions/download-artifact@v3
with:
name: artifact
path: dist
- - uses: pypa/gh-action-pypi-publish@v1.4.2
+ - uses: pypa/gh-action-pypi-publish@v1.5.0
with:
user: __token__
password: ${{ secrets.pypi_password }}
diff --git a/docs/diagram.md b/docs/diagram.md
index d0557b4f..fe542997 100644
--- a/docs/diagram.md
+++ b/docs/diagram.md
@@ -8,7 +8,7 @@