Compare commits

..
10 Commits
Author SHA1 Message Date
Joe Rickerby b4a353c0c6 Upgrade to macos-13 for the intel runners 2024-02-04 11:14:18 +00:00
Joe Rickerby e62fa652c5 Typo 2024-02-04 11:12:29 +00:00
Joe Rickerby 4f22ef141f fix error message asserts 2024-02-02 18:19:10 +00:00
Joe Rickerby 4d8122307e Merge remote-tracking branch 'origin/main' into macos-13-xlarge 2024-02-02 17:01:22 +00:00
Joe Rickerby f68f2ea820 Update docs, README and examples 2024-02-02 16:53:10 +00:00
Joe Rickerby 9abb06c66d update error message for skipped arm64 tests 2024-02-02 12:28:40 +00:00
Henry Schreiner 3254807509 Update test.yml 2024-01-30 18:01:10 -05:00
Henry Schreiner 7f908b0257 ci: pre-install pipx on M1 2024-01-30 12:06:31 -05:00
Henry Schreiner 3c0df0b567 Update .github/workflows/test.yml 2024-01-30 11:42:21 -05:00
mayeut ec181518ca chore(CI): test on GHA macos-13-xlarge runner
PyPA is now on an enterprise plan.
Let's use the new macos-13-xlarge runner in order to test on macOS arm64.
2023-12-16 10:24:47 +01:00
202 changed files with 6351 additions and 17782 deletions
+32 -38
View File
@@ -1,43 +1,25 @@
version: 2.1 version: 2
commands:
cibw_prepare_environment:
description: "Prepare the environment for testing."
steps:
- run:
name: Prepare the environment.
command: bash .circleci/prepare.sh
cibw_run_tests:
description: "Runs tests, with CIBW_ENABLE=all on the main branch"
steps:
- run:
name: Test
command: |
if [ "${CIRCLE_BRANCH}" == "main" ]; then
echo "INFO: Exporting CIBW_ENABLE=all for main branch test run."
export CIBW_ENABLE=all
else
echo "INFO: CIBW_ENABLE not set for this branch test run."
fi
venv/bin/python ./bin/run_tests.py
no_output_timeout: 30m
jobs: jobs:
osx-python312: osx-python3.9:
macos: macos:
xcode: 15.4.0 xcode: 12.5.1
resource_class: macos.m1.medium.gen1
environment: environment:
PYTHON: python3 PYTHON: python3
steps: steps:
- checkout - checkout
- cibw_prepare_environment
- cibw_run_tests
linux-python312: - run:
name: Prepare the environment.
command: bash .circleci/prepare.sh
- run:
name: Test.
command: venv/bin/python ./bin/run_tests.py
no_output_timeout: 30m
linux-python3.9:
docker: docker:
- image: cimg/python:3.12 - image: circleci/python:3.9
environment: environment:
PYTHON: python3 PYTHON: python3
# Temporarily restrict the tests that are run on CircleCI to prevent # Temporarily restrict the tests that are run on CircleCI to prevent
@@ -46,12 +28,18 @@ jobs:
steps: steps:
- checkout - checkout
- setup_remote_docker - setup_remote_docker
- cibw_prepare_environment
- cibw_run_tests - run:
name: Prepare the environment.
command: bash .circleci/prepare.sh
- run:
name: Test.
command: venv/bin/python ./bin/run_tests.py
no_output_timeout: 30m
linux-aarch64: linux-aarch64:
machine: machine:
image: default image: ubuntu-2004:2022.04.1
resource_class: arm.medium resource_class: arm.medium
environment: environment:
PYTHON: python3 PYTHON: python3
@@ -60,13 +48,19 @@ jobs:
PYTEST_ADDOPTS: -k "unit_test or main_tests or test_0_basic or test_docker_images" PYTEST_ADDOPTS: -k "unit_test or main_tests or test_0_basic or test_docker_images"
steps: steps:
- checkout - checkout
- cibw_prepare_environment
- cibw_run_tests - run:
name: Prepare the environment.
command: bash .circleci/prepare.sh
- run:
name: Test.
command: venv/bin/python ./bin/run_tests.py
no_output_timeout: 30m
workflows: workflows:
version: 2 version: 2
all-tests: all-tests:
jobs: jobs:
- osx-python312 - osx-python3.9
- linux-python312 - linux-python3.9
- linux-aarch64 - linux-aarch64
+3 -9
View File
@@ -2,15 +2,9 @@
set -o errexit set -o errexit
set -o xtrace set -o xtrace
if [ "$(uname -s)" == "Darwin" ]; then
sudo softwareupdate --install-rosetta --agree-to-license
else
docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
fi
$PYTHON --version $PYTHON --version
$PYTHON -m venv venv $PYTHON -m pip --version
venv/bin/python -m pip install -U pip $PYTHON -m virtualenv -p "$PYTHON" venv
venv/bin/python -m pip install -e. --group test venv/bin/python -m pip install -e ".[dev]"
venv/bin/python -m pip freeze venv/bin/python -m pip freeze
venv/bin/python --version venv/bin/python --version
+22 -37
View File
@@ -1,12 +1,12 @@
only_if: changesInclude('.cirrus.yml') || ($CIRRUS_BRANCH == "main" && !changesIncludeOnly('.github/*', 'bin/*', 'docs/*', '.circleci/*', '.travis.yml', '.pre-commit-config.yaml', '.readthedocs.yml', 'azure-pipelines.yml', 'README.md', 'mkdocs.yml', 'noxfile.py')) || $CIRRUS_BRANCH =~ 'cirrus.*' only_if: changesInclude('.cirrus.yml') || ($BRANCH == "main" && !changesIncludeOnly('docs/*', '.pre-commit-config.yaml'))
run_tests: &RUN_TESTS run_tests: &RUN_TESTS
install_cibuildwheel_script: install_cibuildwheel_script:
- python -m pip install -U pip - python -m pip install -e ".[dev]" pytest-custom-exit-code
- python -m pip install -e. --group test
run_cibuildwheel_tests_script: run_cibuildwheel_tests_script:
- python ./bin/run_tests.py - python ./bin/run_tests.py
linux_x86_task: linux_x86_task:
timeout_in: 120m timeout_in: 120m
compute_engine_instance: compute_engine_instance:
@@ -15,15 +15,9 @@ linux_x86_task:
platform: linux platform: linux
cpu: 8 cpu: 8
memory: 8G memory: 8G
env:
VENV_ROOT: ${HOME}/venv-cibuildwheel
PATH: ${VENV_ROOT}/bin:${PATH}
install_pre_requirements_script: install_pre_requirements_script:
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all - apt install -y python3-venv python-is-python3
- add-apt-repository -y ppa:deadsnakes/ppa
- apt-get update
- apt-get install -y python3.12-venv
- python3.12 -m venv ${VENV_ROOT}
<<: *RUN_TESTS <<: *RUN_TESTS
linux_aarch64_task: linux_aarch64_task:
@@ -34,20 +28,14 @@ linux_aarch64_task:
platform: linux platform: linux
cpu: 4 cpu: 4
memory: 4G memory: 4G
env:
VENV_ROOT: ${HOME}/venv-cibuildwheel
PATH: ${VENV_ROOT}/bin:${PATH}
install_pre_requirements_script: install_pre_requirements_script:
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all - apt install -y python3-venv python-is-python3
- add-apt-repository -y ppa:deadsnakes/ppa
- apt-get update
- apt-get install -y python3.12-venv
- python3.12 -m venv ${VENV_ROOT}
<<: *RUN_TESTS <<: *RUN_TESTS
windows_x86_task: windows_x86_task:
# The task takes ~55 minutes while the timeout happens after 60 minutes by default, # The task takes ~55 minutes while the timeout happens
# let's allow some wiggle room. # after 60 minutes by default, let's allow some wiggle room.
timeout_in: 120m timeout_in: 120m
windows_container: windows_container:
image: cirrusci/windowsservercore:visualstudio2022 image: cirrusci/windowsservercore:visualstudio2022
@@ -55,35 +43,32 @@ windows_x86_task:
memory: 8G memory: 8G
install_pre_requirements_script: install_pre_requirements_script:
- certutil -generateSSTFromWU roots.sst - choco install -y --no-progress python3 --version 3.10.6
- certutil -addstore -f root roots.sst
- del roots.sst
- choco install -y --no-progress python3 --version 3.12.4
- refreshenv - refreshenv
- powershell -Command "$cleanPath = $env:PATH -replace ';+$', ''; Add-Content -Path $env:CIRRUS_ENV -Value ('PATH=' + $cleanPath)" - echo PATH=%PATH% >> "%CIRRUS_ENV%"
<<: *RUN_TESTS <<: *RUN_TESTS
macos_arm64_task: macos_arm64_task:
macos_instance: macos_instance:
image: ghcr.io/cirruslabs/macos-runner:sequoia image: ghcr.io/cirruslabs/macos-monterey-xcode
env: env:
VENV_ROOT: ${HOME}/venv-cibuildwheel PATH: /opt/homebrew/opt/python@3.10/bin:$PATH
PATH: ${VENV_ROOT}/bin:${PATH}
install_pre_requirements_script: install_pre_requirements_script:
- brew install python@3.12 - brew install python@3.10
- python3.12 -m venv ${VENV_ROOT} - ln -s python3 /opt/homebrew/opt/python@3.10/bin/python
<<: *RUN_TESTS <<: *RUN_TESTS
macos_arm64_cp38_task: macos_arm64_cp38_task:
macos_instance: macos_instance:
image: ghcr.io/cirruslabs/macos-runner:sequoia image: ghcr.io/cirruslabs/macos-monterey-xcode
env: env:
VENV_ROOT: ${HOME}/venv-cibuildwheel PATH: /opt/homebrew/opt/python@3.10/bin:$PATH
PATH: ${VENV_ROOT}/bin:${PATH} PYTEST_ADDOPTS: --run-cp38-universal2 -k 'test_cp38_arm64_testing_universal2_installer or test_arch_auto'
PYTEST_ADDOPTS: --run-cp38-universal2 -k 'test_cp38_arm64_testing_universal2_installer or test_arch_auto or test_dummy_serial'
install_pre_requirements_script: install_pre_requirements_script:
- brew install python@3.12 - brew install python@3.10
- python3.12 -m venv ${VENV_ROOT} - ln -s python3 /opt/homebrew/opt/python@3.10/bin/python
- curl -fsSLO https://www.python.org/ftp/python/3.8.10/python-3.8.10-macos11.pkg - curl -fsSLO https://www.python.org/ftp/python/3.8.10/python-3.8.10-macos11.pkg
- sudo installer -pkg python-3.8.10-macos11.pkg -target / - sudo installer -pkg python-3.8.10-macos11.pkg -target /
- rm python-3.8.10-macos11.pkg - rm python-3.8.10-macos11.pkg
-7
View File
@@ -1,7 +0,0 @@
# Specification files for platform maintainers
cibuildwheel/platforms/ios.py @freakboy3742
cibuildwheel/platforms/pyodide.py @hoodmane @ryanking13 @agriyakhetarpal
cibuildwheel/platforms/android.py @mhsmith
# Constraints files
cibuildwheel/resources/constraints-pyodide*.txt @hoodmane @ryanking13 @agriyakhetarpal
+32
View File
@@ -0,0 +1,32 @@
{
"problemMatcher": [
{
"severity": "warning",
"pattern": [
{
"regexp": "^([^:]+):(\\d+):(\\d+): ([A-DF-Z]\\d+): \\033\\[[\\d;]+m([^\\033]+).*$",
"file": 1,
"line": 2,
"column": 3,
"code": 4,
"message": 5
}
],
"owner": "pylint-warning"
},
{
"severity": "error",
"pattern": [
{
"regexp": "^([^:]+):(\\d+):(\\d+): (E\\d+): \\033\\[[\\d;]+m([^\\033]+).*$",
"file": 1,
"line": 2,
"column": 3,
"code": 4,
"message": 5
}
],
"owner": "pylint-error"
}
]
}
+15 -15
View File
@@ -11,32 +11,32 @@ jobs:
dist: dist:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: hynek/build-and-inspect-python-package@v2 - name: Build SDist and wheel
run: pipx run build
- uses: actions/upload-artifact@v4
with:
name: cibw-sdist
path: dist/*
- name: Check metadata
run: pipx run twine check dist/*
publish: publish:
needs: [dist] needs: [dist]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'published' if: github.event_name == 'release' && github.event.action == 'published'
environment: environment: pypi
name: pypi
url: https://pypi.org/p/cibuildwheel
permissions: permissions:
id-token: write id-token: write
attestations: write
steps: steps:
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v4
with: with:
name: Packages pattern: cibw-*
path: dist path: dist
merge-multiple: true
- name: Generate artifact attestation for sdist and wheel
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
with:
subject-path: "dist/cibuildwheel-*"
- uses: pypa/gh-action-pypi-publish@release/v1 - uses: pypa/gh-action-pypi-publish@release/v1
with:
attestations: true
+50 -280
View File
@@ -4,27 +4,10 @@ on:
push: push:
branches: branches:
- main - main
- 2.x
pull_request: pull_request:
types:
- opened
- synchronize
- reopened
- labeled
paths-ignore: paths-ignore:
- .ci* - 'docs/**'
- bin/*
- docs/**
- examples/azure-pipelines-*
- examples/ci*
- examples/travis-ci-*
- .pre-commit-config.yaml - .pre-commit-config.yaml
- .readthedocs.yml
- .travis.yml
- README.md
- azure-pipelines.yml
- mkdocs.yml
- noxfile.py
workflow_dispatch: workflow_dispatch:
# allow manual runs on branches without a PR # allow manual runs on branches without a PR
@@ -34,140 +17,65 @@ concurrency:
jobs: jobs:
lint: lint:
name: Linters (mypy, ruff, etc.) name: Linters (mypy, flake8, etc.)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-python@v6 - uses: actions/setup-python@v5
id: python
with: with:
python-version: "3.x" python-version: "3.x"
- uses: j178/prek-action@v1 - uses: pre-commit/action@v3.0.0
- name: Check manifest
run: pipx run nox -s check_manifest
- name: PyLint checks - name: PyLint checks
run: pipx run --python "${{ steps.python.outputs.python-path }}" nox -s pylint -- --output-format=github run: |
echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json"
sample: pipx run nox -s pylint
name: Generate a sample project
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
name: Install Python
with:
python-version: "3.x"
- uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: uv sync --no-dev --group test
- name: Generate a sample project
run: uv run --no-sync -m test.test_projects test.test_0_basic.basic_project sample_proj
- uses: actions/upload-artifact@v6
with:
name: sample_proj
path: sample_proj
test: test:
name: Test on ${{ matrix.os }} (${{ matrix.python_version }}) ${{ matrix.test_select }} name: Test cibuildwheel on ${{ matrix.os }}
needs: sample needs: lint
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false
matrix: matrix:
os: [ubuntu-20.04, windows-latest, macos-13, macos-14]
python_version: ['3.12']
include: include:
# Min Python - os: ubuntu-22.04
- os: ubuntu-latest python_version: '3.8'
python_version: '3.11'
# Max Python
- os: ubuntu-latest
python_version: '3.14'
- os: ubuntu-latest
python_version: '3.13'
test_select: android
- os: ubuntu-24.04-arm
python_version: '3.13'
- os: windows-latest
python_version: '3.13'
- os: windows-11-arm
python_version: '3.13'
- os: macos-15-intel
python_version: '3.13'
- os: macos-15
python_version: '3.13'
- os: macos-15-intel
python_version: '3.13'
test_select: ios
- os: macos-15
python_version: '3.13'
test_select: ios
# Exercise iOS on a non-default simulator.
test_runtime: 'args: --simulator "iPhone 16e,OS=18.5"'
- os: macos-15-intel
python_version: '3.13'
test_select: android
# Exercise Android on a non-default simulator
test_runtime: 'args: --managed minVersion'
- os: macos-15
python_version: '3.13'
test_select: android
timeout-minutes: 180 timeout-minutes: 180
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-python@v6 - uses: actions/setup-python@v5
name: Install Python ${{ matrix.python_version }} name: Install Python ${{ matrix.python_version }}
with: with:
python-version: ${{ matrix.python_version }} python-version: ${{ matrix.python_version }}
allow-prereleases: true
- uses: astral-sh/setup-uv@v7 # 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: Free up disk space - name: Install latest podman
if: runner.os == 'Linux' && matrix.test_select != 'android'
run: |
sudo rm -rf $ANDROID_HOME/ndk /opt/hostedtoolcache/CodeQL \
/usr/local/lib/node_modules /usr/local/share/chromium \
/usr/local/share/powershell
df -m
# for oci_container unit tests
- name: Set up QEMU
if: runner.os == 'Linux' if: runner.os == 'Linux'
uses: docker/setup-qemu-action@v3
- name: Set up Xcode
if: ${{ startsWith(matrix.os, 'macos-15') }}
run: | run: |
# GitHub recommends explicitly selecting the desired Xcode version . /etc/os-release
sudo xcode-select --switch /Applications/Xcode_16.4.app 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 install podman
# free some space to prevent reaching GHA disk space limits
- name: Clean docker images
if: runner.os == 'Linux'
run: |
docker system prune -a -f
df -h
- name: Install dependencies - name: Install dependencies
run: uv sync --no-dev --group test
- uses: joerick/pr-labels-action@v1.0.9
- name: Set CIBW_ENABLE
shell: bash
run: | run: |
if [[ "${{ github.ref_name }}" == "main" ]]; then python -m pip install ".[test]"
CIBW_ENABLE=all
else
# get the default CIBW_ENABLE value from the test module
CIBW_ENABLE=$(uv run --no-sync python -c 'import sys, test.utils as c; sys.stdout.write(c.DEFAULT_CIBW_ENABLE)')
# if this is a PR, check for labels - name: Generate a sample project
if [[ -n "$GITHUB_PR_LABEL_CI_PYPY" ]]; then run: |
CIBW_ENABLE+=" pypy" python -m test.test_projects test.test_0_basic.basic_project sample_proj
fi
if [[ -n "$GITHUB_PR_LABEL_CI_GRAALPY" ]]; then
CIBW_ENABLE+=" graalpy"
fi
fi
echo "CIBW_ENABLE=${CIBW_ENABLE}" >> $GITHUB_ENV
- name: Download a sample project
uses: actions/download-artifact@v5
with:
name: sample_proj
path: sample_proj
- name: Run a sample build (GitHub Action) - name: Run a sample build (GitHub Action)
uses: ./ uses: ./
@@ -176,174 +84,36 @@ jobs:
output-dir: wheelhouse output-dir: wheelhouse
env: env:
CIBW_ARCHS_MACOS: x86_64 universal2 arm64 CIBW_ARCHS_MACOS: x86_64 universal2 arm64
CIBW_BUILD_FRONTEND: ${{ matrix.test_select && 'build' || 'build[uv]' }}
CIBW_PLATFORM: ${{ matrix.test_select }}
CIBW_TEST_RUNTIME: ${{ matrix.test_runtime }}
- name: Run a sample build (GitHub Action, only) - uses: actions/upload-artifact@v4
uses: ./
if: matrix.test_select == ''
with:
package-dir: sample_proj
output-dir: wheelhouse_only
only: cp313-${{ runner.os == 'Linux' && (runner.arch == 'ARM64' && 'manylinux_aarch64' || 'manylinux_x86_64') || (runner.os == 'Windows' && 'win_amd64' || 'macosx_x86_64') }}
- name: Create custom configuration file
shell: bash
run: |
cat > sample_proj/cibw.toml <<EOF
[tool.cibuildwheel]
# Only build on CPython 3.13 on native arch
archs = ["native"]
build = "cp313-*"
# Skip musllinux
skip = "*-musllinux*"
EOF
- name: Run a sample build (GitHub Action, config-file)
uses: ./
env:
CIBW_PLATFORM: ${{ matrix.test_select }}
CIBW_TEST_RUNTIME: ${{ matrix.test_runtime }}
with:
package-dir: sample_proj
output-dir: wheelhouse_config_file
config-file: sample_proj/cibw.toml
- name: Check Action artifacts
shell: bash
run: |
test $(find wheelhouse -name '*.whl' | wc -l) -ge 1
test $(find wheelhouse_config_file -name '*.whl' | wc -l) -eq 1
- name: Check Action artifacts (native build only)
if: matrix.test_select == ''
shell: bash
run: |
test $(find wheelhouse_only -name '*.whl' | wc -l) -eq 1
- uses: actions/upload-artifact@v6
with: with:
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: wheelhouse/*.whl path: wheelhouse/*.whl
- name: Test cibuildwheel - name: Test cibuildwheel
env:
CIBW_TEST_RUNTIME: ${{ matrix.test_runtime }}
run: uv run --no-sync bin/run_tests.py --test-select=${{ matrix.test_select || 'native' }} ${{ (runner.os == 'Linux' && runner.arch == 'X64') && '--run-podman' || '' }}
emulated-archs:
name: Get qemu emulated architectures
needs: lint
runs-on: ubuntu-latest
outputs:
archs: ${{ steps.archs.outputs.archs }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.x"
- uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: uv sync --no-dev --group test
- name: Get qemu emulated architectures
id: archs
run: | run: |
OUTPUT=$(uv run --no-sync python -c "from json import dumps; from test.utils import EMULATED_ARCHS; print(dumps(EMULATED_ARCHS))") python ./bin/run_tests.py --run-podman
echo "${OUTPUT}"
echo "archs=${OUTPUT}" >> "$GITHUB_OUTPUT"
test-emulated: test-emulated:
name: Test Linux ${{ matrix.arch }} using qemu name: Test emulated cibuildwheel using qemu
needs: emulated-archs needs: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 180 timeout-minutes: 180
strategy:
matrix:
arch: ${{ fromJSON(needs.emulated-archs.outputs.archs) }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-python@v6 - uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
- uses: astral-sh/setup-uv@v7
- name: Install dependencies - name: Install dependencies
run: uv sync --no-dev --group test run: |
python -m pip install ".[test]"
- name: Set up QEMU - name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Run the emulation tests - name: Run the emulation tests
run: uv run --no-sync pytest --run-emulation ${{ matrix.arch }} test/test_emulation.py run: |
pytest --run-emulation test/test_emulation.py
test-pyodide:
name: Test pyodide
needs: sample
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
name: Install Python 3.12
with:
python-version: '3.12'
- uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: uv sync --no-dev --group test
- name: Download a sample project
uses: actions/download-artifact@v5
with:
name: sample_proj
path: sample_proj
- name: Run a sample build (GitHub Action)
uses: ./
with:
package-dir: sample_proj
output-dir: wheelhouse
env:
CIBW_PLATFORM: pyodide
- name: Run tests with 'CIBW_PLATFORM' set to 'pyodide'
run: uv run --no-sync ./bin/run_tests.py
env:
CIBW_PLATFORM: pyodide
test-uv-extras:
name: Test uv extra on ${{ matrix.os }} ${{ matrix.test_select }}
needs: sample
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
test_select: android
- os: windows-latest
- os: windows-11-arm
- os: macos-15-intel
- os: macos-15
- os: macos-15-intel
test_select: android
- os: macos-15
test_select: android
steps:
- uses: actions/checkout@v6
- name: Download a sample project
uses: actions/download-artifact@v5
with:
name: sample_proj
path: sample_proj
- name: Run a sample build (GitHub Action)
uses: ./
with:
extras: uv
package-dir: sample_proj
output-dir: wheelhouse
env:
CIBW_ARCHS_MACOS: x86_64 universal2 arm64
CIBW_BUILD: cp314-*
CIBW_BUILD_FRONTEND: 'build[uv]'
CIBW_PLATFORM: ${{ matrix.test_select }}
+16 -18
View File
@@ -18,21 +18,13 @@ jobs:
update-dependencies: update-dependencies:
name: Update dependencies name: Update dependencies
if: github.repository_owner == 'pypa' || github.event_name != 'schedule' if: github.repository_owner == 'pypa' || github.event_name != 'schedule'
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4
# we use this step to grab a Github App auth token, so that PRs generated by this workflow - uses: wntrblm/nox@2023.04.22
# run the GHA tests.
- uses: actions/create-github-app-token@v2
id: generate-token
if: github.ref == 'refs/heads/main' && github.repository == 'pypa/cibuildwheel'
with: with:
app-id: ${{ secrets.CIBUILDWHEEL_BOT_APP_ID }} python-versions: "3.7, 3.8, 3.9, 3.10, 3.11, 3.12"
private-key: ${{ secrets.CIBUILDWHEEL_BOT_APP_PRIVATE_KEY }}
- uses: actions/checkout@v6
- uses: wntrblm/nox@2025.11.12
- name: "Run update: dependencies" - name: "Run update: dependencies"
run: nox --force-color -s update_constraints run: nox --force-color -s update_constraints
@@ -41,9 +33,18 @@ jobs:
- name: "Run update: docs user projects" - name: "Run update: docs user projects"
run: nox --force-color -s update_proj -- --auth=${{ secrets.GITHUB_TOKEN }} run: nox --force-color -s update_proj -- --auth=${{ secrets.GITHUB_TOKEN }}
# we use this step to grab a Github App auth token, so that PRs generated by this workflow
# run the GHA tests.
- uses: tibdex/github-app-token@v2
id: generate-token
if: github.ref == 'refs/heads/main' && github.repository == 'pypa/cibuildwheel'
with:
app_id: ${{ secrets.CIBUILDWHEEL_BOT_APP_ID }}
private_key: ${{ secrets.CIBUILDWHEEL_BOT_APP_PRIVATE_KEY }}
- name: Create Pull Request - name: Create Pull Request
if: github.ref == 'refs/heads/main' && github.repository == 'pypa/cibuildwheel' if: github.ref == 'refs/heads/main' && github.repository == 'pypa/cibuildwheel'
uses: peter-evans/create-pull-request@v8 uses: peter-evans/create-pull-request@v5
with: with:
commit-message: Update dependencies commit-message: Update dependencies
title: '[Bot] Update dependencies' title: '[Bot] Update dependencies'
@@ -52,10 +53,7 @@ jobs:
PR generated by "Update dependencies" [workflow](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). PR generated by "Update dependencies" [workflow](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}).
branch: update-dependencies-pr branch: update-dependencies-pr
sign-commits: true committer: "cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>"
author: "cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>"
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
delete-branch: true delete-branch: true
labels: |
CI: GraalPy
CI: PyPy
dependencies
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Update the ${{ env.TAG_NAME }} tag - name: Update the ${{ env.TAG_NAME }} tag
id: update-major-minor-tag id: update-major-minor-tag
+6 -1
View File
@@ -84,7 +84,9 @@ celerybeat-schedule
# virtualenv # virtualenv
.venv .venv
venv*/ venv/
venv3/
venv2/
ENV/ ENV/
env/ env/
env2/ env2/
@@ -110,5 +112,8 @@ all_known_setup.yaml
# mkdocs # mkdocs
site/ site/
# Virtual environments
venv*
# PyCharm # PyCharm
.idea/ .idea/
+5 -31
View File
@@ -1,5 +1,5 @@
linux: linux:
image: python:3.12 image: python:3.8
services: services:
- name: docker:dind - name: docker:dind
entrypoint: ["env", "-u", "DOCKER_HOST"] entrypoint: ["env", "-u", "DOCKER_HOST"]
@@ -13,15 +13,9 @@ linux:
# skip all but the basic tests # skip all but the basic tests
# (comment the below line in a PR to debug a Gitlab-specific issue) # (comment the below line in a PR to debug a Gitlab-specific issue)
PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
CIBW_ENABLE: "all"
script: script:
- curl -sSL https://get.docker.com/ | sh - curl -sSL https://get.docker.com/ | sh
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all - python -m pip install -e ".[dev]" pytest-custom-exit-code
- python -m pip install -U pip
- python -m pip install -e. pytest-custom-exit-code --group test
- python ./bin/run_tests.py - python ./bin/run_tests.py
windows: windows:
@@ -29,29 +23,9 @@ windows:
variables: variables:
PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
before_script: before_script:
- choco install python -y --allow-downgrade --version 3.12.4 - choco install python -y --version 3.8.6
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
CIBW_ENABLE: "all"
script: script:
- py -m pip install -U pip - py -m pip install -e ".[dev]" pytest-custom-exit-code
- py -m pip install -e. pytest-custom-exit-code --group test
- py bin\run_tests.py - py bin\run_tests.py
tags: tags:
- saas-windows-medium-amd64 - windows
macos:
image: macos-14-xcode-15
variables:
PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
CIBW_ENABLE: "all"
script:
- python3 -m pip install -U pip
- python3 -m pip install -e. pytest-custom-exit-code --group test
- python3 ./bin/run_tests.py
tags:
- saas-macos-medium-m1
+29 -31
View File
@@ -1,7 +1,7 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 rev: v4.5.0
hooks: hooks:
- id: check-case-conflict - id: check-case-conflict
- id: check-merge-conflict - id: check-merge-conflict
@@ -14,47 +14,47 @@ repos:
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.14 rev: v0.1.14
hooks: hooks:
- id: ruff-check - id: ruff
args: ["--fix", "--show-fixes"] args: ["--fix", "--show-fixes"]
- id: ruff-format - id: ruff-format
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.5.0
hooks:
- id: setup-cfg-fmt
args: [--include-version-classifiers, --min-py-version=3.8, --max-py-version=3.12]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1 rev: v1.8.0
hooks: hooks:
- id: mypy - id: mypy
name: mypy 3.11 on cibuildwheel/ name: mypy 3.8 on cibuildwheel/
args: ["--python-version=3.11"] exclude: ^cibuildwheel/resources/.*py|bin/generate_schema.py$
exclude: ^cibuildwheel/resources/_cross_venv.py$ # Requires Python 3.13 or later args: ["--python-version=3.8"]
additional_dependencies: &mypy-dependencies additional_dependencies: &mypy-dependencies
- bracex - bracex
- build - nox
- dependency-groups>=1.2
- humanize
- nox>=2025.2.9
- orjson
- packaging - packaging
- pyelftools
- pygithub - pygithub
- pytest
- rich - rich
- tomli
- tomli_w - tomli_w
- types-certifi - types-certifi
- types-click - types-click
- types-jinja2 - types-jinja2
- types-pyyaml - types-pyyaml
- types-requests - types-requests
- types-setuptools
- uv
- validate-pyproject - validate-pyproject
- id: mypy - id: mypy
name: mypy 3.14 name: mypy 3.12
args: ["--python-version=3.14"] exclude: ^cibuildwheel/resources/.*py$
args: ["--python-version=3.12"]
additional_dependencies: *mypy-dependencies additional_dependencies: *mypy-dependencies
- repo: https://github.com/shellcheck-py/shellcheck-py - repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1 rev: v0.9.0.6
hooks: hooks:
- id: shellcheck - id: shellcheck
@@ -64,33 +64,31 @@ repos:
name: Disallow improper capitalization name: Disallow improper capitalization
language: pygrep language: pygrep
entry: PyBind|Numpy|Cmake|Github|PyTest entry: PyBind|Numpy|Cmake|Github|PyTest
types: [markdown] types:
- markdown
exclude: ^docs/working-examples\.md$ # Autogenerated exclude: ^docs/working-examples\.md$ # Autogenerated
- id: cog - id: update-readme-changelog
name: Cog the README name: Update README changelog
language: python language: python
pass_filenames: false entry: bin/update_readme_changelog.py
entry: cog -c -P -r -I ./bin README.md files: ^docs/changelog.md$
files: '^(README\.md|docs/changelog\.md|docs/options\.md|bin/readme.*)$'
additional_dependencies: [cogapp>=3.5]
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.4.1 rev: v2.2.6
hooks: hooks:
- id: codespell - id: codespell
args: ["-w"] args: ["-L", "sur", "-w"]
exclude: ^docs/working-examples\.md$ # Autogenerated exclude: ^docs/working-examples\.md$ # Autogenerated
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.36.1 rev: 0.27.0
hooks: hooks:
- id: check-dependabot - id: check-dependabot
- id: check-github-actions - id: check-github-actions
- id: check-github-workflows - id: check-github-workflows
- id: check-gitlab-ci - id: check-gitlab-ci
# Disabled due to schema issue for now: - id: check-readthedocs
# - id: check-readthedocs
- id: check-travis - id: check-travis
- id: check-jsonschema - id: check-jsonschema
name: Check projects name: Check projects
+13 -6
View File
@@ -3,9 +3,16 @@
version: 2 version: 2
build: build:
os: ubuntu-24.04 os: ubuntu-22.04
commands: tools:
- asdf plugin add uv python: "3.11"
- asdf install uv latest
- asdf global uv latest python:
- NO_COLOR=1 uv run --python 3.14 --managed-python --no-dev --group docs mkdocs build --strict --site-dir $READTHEDOCS_OUTPUT/html install:
- method: pip
path: .
extra_requirements:
- docs
mkdocs:
configuration: mkdocs.yml
+60
View File
@@ -0,0 +1,60 @@
os: linux
dist: focal
language: python
branches:
only:
- main
jobs:
include:
- name: Linux | x86_64 + i686 | Python 3.9
python: 3.9
services: docker
env: PYTHON=python
- name: Linux | arm64 | Python 3.9
python: 3.9
services: docker
arch: arm64-graviton2
group: edge
virt: vm
env: PYTHON=python
- name: Linux | ppc64le | Python 3.9
python: 3.9
services: docker
arch: ppc64le
allow_failure: True
env:
- PYTHON=python
# skip test_manylinuxXXXX_only, it uses too much disk space
# 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.9
os: windows
language: shell
before_install:
- choco upgrade python3 -y --version 3.9.13 --limit-output --params "/InstallDir:C:\\Python39"
env:
- PYTHON=C:\\Python39\\python
- name: Linux | s390x | Python 3.9
python: 3.9
services: docker
arch: s390x
env: PYTHON=python
install:
- $PYTHON -m pip install -U pip
- $PYTHON -m pip install -e ".[dev]" pytest-custom-exit-code
script: |
# travis_wait disable the output while waiting
# use the same kind of tricks as in multibuild
(while true; do echo "travis_keep_alive"; sleep 300; done) &
SPINNER_PID=$!
disown
$PYTHON ./bin/run_tests.py
kill -9 ${SPINNER_PID}
+7 -5
View File
@@ -1,9 +1,11 @@
This is a summary of the host Python versions and platforms covered by the different CI platforms: This is a summary of the host Python versions and platforms covered by the different CI platforms:
| | 3.11 | 3.12 | 3.13 | 3.14 | | | 3.8 | 3.9 | 3.10 | 3.12 |
|---------|----------------------------------|---------------------------------|----------------|----------------| |---------|--------------------------------------------------------|-----------------------|-----------|----------------|
| Linux | Azure Pipelines / GitHub Actions | CircleCI¹ / Cirrus CI / GitLab¹ | GitHub Actions | GitHub Actions | | Linux | AppVeyor¹ / Azure Pipelines / GitLab¹ / GitHub Actions | CircleCI¹ / Travis CI | Cirrus CI | GitHub Actions |
| macOS | Azure Pipelines | CircleCI¹ / Cirrus CI / GitLab¹ | GitHub Actions | | | macOS | AppVeyor¹ / Azure Pipelines | CircleCI¹ / Travis CI | Cirrus CI | GitHub Actions |
| Windows | Azure Pipelines | Cirrus CI / GitLab¹ | GitHub Actions | | | Windows | AppVeyor¹ / Azure Pipelines / GitLab¹ | Travis CI | Cirrus CI | GitHub Actions |
> ¹ Runs a reduced set of tests to reduce CI load > ¹ Runs a reduced set of tests to reduce CI load
Non-x86 architectures are covered on Travis CI using Python 3.9.
+1 -1
View File
@@ -1,6 +1,6 @@
This project is licensed under the 'BSD 2-clause license'. This project is licensed under the 'BSD 2-clause license'.
Copyright (c) 2017-2025, Joe Rickerby and contributors. All rights reserved. Copyright (c) 2017-2023, Joe Rickerby and contributors. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:
+6
View File
@@ -0,0 +1,6 @@
recursive-include cibuildwheel *.cfg
recursive-include cibuildwheel *.in
recursive-include cibuildwheel *.py
recursive-include cibuildwheel *.toml
recursive-include cibuildwheel *.txt
recursive-include cibuildwheel *.typed
+103 -152
View File
@@ -2,72 +2,67 @@ cibuildwheel
============ ============
[![PyPI](https://img.shields.io/pypi/v/cibuildwheel.svg)](https://pypi.python.org/pypi/cibuildwheel) [![PyPI](https://img.shields.io/pypi/v/cibuildwheel.svg)](https://pypi.python.org/pypi/cibuildwheel)
[![Documentation Status](https://readthedocs.org/projects/cibuildwheel/badge/?version=stable)](https://cibuildwheel.pypa.io/en/stable/?badge=stable) [![Documentation Status](https://readthedocs.org/projects/cibuildwheel/badge/?version=stable)](https://cibuildwheel.readthedocs.io/en/stable/?badge=stable)
[![Actions Status](https://github.com/pypa/cibuildwheel/workflows/Test/badge.svg)](https://github.com/pypa/cibuildwheel/actions) [![Actions Status](https://github.com/pypa/cibuildwheel/workflows/Test/badge.svg)](https://github.com/pypa/cibuildwheel/actions)
[![Travis Status](https://img.shields.io/travis/com/pypa/cibuildwheel/main?logo=travis)](https://travis-ci.com/pypa/cibuildwheel)
[![Appveyor status](https://ci.appveyor.com/api/projects/status/gt3vwl88yt0y3hur/branch/main?svg=true)](https://ci.appveyor.com/project/joerick/cibuildwheel/branch/main)
[![CircleCI Status](https://img.shields.io/circleci/build/gh/pypa/cibuildwheel/main?logo=circleci)](https://circleci.com/gh/pypa/cibuildwheel) [![CircleCI Status](https://img.shields.io/circleci/build/gh/pypa/cibuildwheel/main?logo=circleci)](https://circleci.com/gh/pypa/cibuildwheel)
[![Azure Status](https://dev.azure.com/joerick0429/cibuildwheel/_apis/build/status/pypa.cibuildwheel?branchName=main)](https://dev.azure.com/joerick0429/cibuildwheel/_build/latest?definitionId=4&branchName=main) [![Azure Status](https://dev.azure.com/joerick0429/cibuildwheel/_apis/build/status/pypa.cibuildwheel?branchName=main)](https://dev.azure.com/joerick0429/cibuildwheel/_build/latest?definitionId=4&branchName=main)
[Documentation](https://cibuildwheel.pypa.io) [Documentation](https://cibuildwheel.readthedocs.org)
<!--intro-start--> <!--intro-start-->
Python wheels are great. Building them across **Mac, Linux, Windows**, on **multiple versions of Python**, is not. Python wheels are great. Building them across **Mac, Linux, Windows**, on **multiple versions of Python**, is not.
`cibuildwheel` is here to help. `cibuildwheel` runs on your CI server - currently it supports GitHub Actions, Azure Pipelines, CircleCI, and GitLab CI - and it builds and tests your wheels across all of your platforms. `cibuildwheel` is here to help. `cibuildwheel` runs on your CI server - currently it supports GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, and GitLab CI - and it builds and tests your wheels across all of your platforms.
What does it do? What does it do?
---------------- ----------------
While cibuildwheel itself requires a recent Python version to run (we support the last three releases), it can target the following versions to build wheels: | | macOS Intel | macOS Apple Silicon | Windows 64bit | Windows 32bit | Windows Arm64 | manylinux<br/>musllinux x86_64 | manylinux<br/>musllinux i686 | manylinux<br/>musllinux aarch64 | manylinux<br/>musllinux ppc64le | manylinux<br/>musllinux s390x |
|----------------|----|-----|-----|-----|-----|----|-----|----|-----|-----|
| CPython 3.6 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ |
| CPython 3.7 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ |
| CPython 3.8 | ✅ | ✅ | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ |
| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ |
| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ |
| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ |
| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ |
| PyPy 3.7 v7.3 | ✅ | N/A | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
| PyPy 3.8 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
| PyPy 3.9 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
| PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
| | macOS Intel | macOS Apple Silicon | Windows 64bit | Windows 32bit | Windows Arm64 | manylinux<br/>musllinux x86_64 | manylinux<br/>musllinux i686 | manylinux<br/>musllinux aarch64 | manylinux<br/>musllinux ppc64le | manylinux<br/>musllinux s390x | manylinux<br/>musllinux armv7l | Android | iOS | Pyodide | <sup>¹ PyPy is only supported for manylinux wheels.</sup><br>
|--------------------|----|-----|----|-----|-----|----|-----|----|-----|-----|---|-----|-----|-----|
| CPython 3.8 | ✅ | ✅ | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | ✅⁴ |
| CPython 3.13³ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | ✅ | ✅ | ✅⁴ |
| CPython 3.14 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | ✅ | ✅ | N/A |
| PyPy 3.8 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
| PyPy 3.9 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
| PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
| PyPy 3.11 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
| GraalPy 3.11 v24.2 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | N/A | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
| GraalPy 3.12 v25.0 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | N/A | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
<sup>¹ PyPy & GraalPy are only supported for manylinux wheels.</sup><br>
<sup>² Windows arm64 support is experimental.</sup><br> <sup>² Windows arm64 support is experimental.</sup><br>
<sup>³ Free-threaded mode requires opt-in on 3.13 using [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable).</sup><br>
<sup>⁴ Experimental, not yet supported on PyPI, but can be used directly in web deployment. Use `--platform pyodide` to build.</sup><br>
<sup>⁵ manylinux armv7l support is experimental. As there are no RHEL based image for this architecture, it's using an Ubuntu based image instead.</sup><br>
- Builds manylinux, musllinux, macOS, and Windows wheels for CPython, PyPy, and GraalPy - Builds manylinux, musllinux, macOS 10.9+, and Windows wheels for CPython and PyPy
- Works on GitHub Actions, Azure Pipelines, CircleCI, GitLab CI, and Cirrus CI - Works on GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, GitLab CI, and Cirrus CI
- Bundles shared library dependencies on Linux and macOS through [auditwheel](https://github.com/pypa/auditwheel) and [delocate](https://github.com/matthew-brett/delocate) - Bundles shared library dependencies on Linux and macOS through [auditwheel](https://github.com/pypa/auditwheel) and [delocate](https://github.com/matthew-brett/delocate)
- Runs your library's tests against the wheel-installed version of your library - Runs your library's tests against the wheel-installed version of your library
See the [cibuildwheel 1 documentation](https://cibuildwheel.pypa.io/en/1.x/) if you need to build unsupported versions of Python, such as Python 2. See the [cibuildwheel 1 documentation](https://cibuildwheel.readthedocs.io/en/1.x/) if you need to build unsupported versions of Python, such as Python 2.
Usage Usage
----- -----
`cibuildwheel` runs inside a CI service. Supported platforms depend on which service you're using: `cibuildwheel` runs inside a CI service. Supported platforms depend on which service you're using:
| | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM | Android | iOS | | | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM |
|-----------------|-------|-------|---------|-----------|-----------|-------------|---------|-----| |-----------------|-------|-------|---------|-----------|-----------|-------------|
| GitHub Actions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅⁴ | ✅³ | | GitHub Actions | ✅ | ✅ | ✅ | ✅¹ | ✅ | ✅² |
| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² | ✅⁴ | ✅³ | | Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² |
| CircleCI | ✅ | ✅ | | ✅ | ✅ | | ✅⁴ | ✅³ | | Travis CI | ✅ | | ✅ | ✅ | | |
| Gitlab CI | ✅ | ✅ | ✅ | ✅¹ | ✅ | | ✅ | ✅³ | | AppVeyor | ✅ | ✅ | ✅ | | | ✅² |
| Cirrus CI | ✅ | ✅ | | ✅ | ✅ | | ✅⁴ | | | CircleCI | ✅ | ✅ | | ✅ | ✅ | |
| Gitlab CI | ✅ | | ✅ | ✅¹ | | |
| Cirrus CI | ✅ | ✅ | ✅ | ✅ | ✅ | |
<sup>¹ [Requires emulation](https://cibuildwheel.pypa.io/en/stable/faq/#emulation), distributed separately. Other services may also support Linux ARM through emulation or third-party build hosts, but these are not tested in our CI.</sup><br> <sup>¹ [Requires emulation](https://cibuildwheel.readthedocs.io/en/stable/faq/#emulation), distributed separately. Other services may also support Linux ARM through emulation or third-party build hosts, but these are not tested in our CI.</sup><br>
<sup>² [Uses cross-compilation](https://cibuildwheel.pypa.io/en/stable/faq/#windows-arm64). It is not possible to test `arm64` on this CI platform.</sup><br> <sup>² [Uses cross-compilation](https://cibuildwheel.readthedocs.io/en/stable/faq/#windows-arm64). It is not possible to test `arm64` on this CI platform.</sup>
<sup>³ Requires a macOS runner; runs tests on the simulator for the runner's architecture. </sup><br>
<sup>⁴ Building for Android requires the runner to be Linux x86_64, macOS ARM64 or macOS x86_64. Testing has [additional requirements](https://cibuildwheel.pypa.io/en/stable/platforms/#android).</sup><br>
<!--intro-end--> <!--intro-end-->
@@ -76,7 +71,6 @@ Example setup
To build manylinux, musllinux, macOS, and Windows wheels on GitHub Actions, you could use this `.github/workflows/wheels.yml`: To build manylinux, musllinux, macOS, and Windows wheels on GitHub Actions, you could use this `.github/workflows/wheels.yml`:
<!--generic-github-start-->
```yaml ```yaml
name: Build name: Build
@@ -88,32 +82,30 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-15-intel, macos-latest] os: [ubuntu-latest, windows-latest, macOS-12, macOS-14]
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
# Used to host cibuildwheel # Used to host cibuildwheel
- uses: actions/setup-python@v5 - uses: actions/setup-python@v3
- name: Install cibuildwheel - name: Install cibuildwheel
run: python -m pip install cibuildwheel==3.3.1 run: python -m pip install cibuildwheel==2.16.5
- name: Build wheels - name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse run: python -m cibuildwheel --output-dir wheelhouse
# to supply options, put them in 'env', like: # to supply options, put them in 'env', like:
# env: # env:
# CIBW_SOME_OPTION: value # CIBW_SOME_OPTION: value
# ...
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
``` ```
<!--generic-github-end-->
For more information, including PyPI deployment, and the use of other CI services or the dedicated GitHub Action, check out the [documentation](https://cibuildwheel.pypa.io) and the [examples](https://github.com/pypa/cibuildwheel/tree/main/examples). For more information, including PyPI deployment, and the use of other CI services or the dedicated GitHub Action, check out the [documentation](https://cibuildwheel.readthedocs.org) and the [examples](https://github.com/pypa/cibuildwheel/tree/main/examples).
How it works How it works
------------ ------------
@@ -122,50 +114,35 @@ The following diagram summarises the steps that cibuildwheel takes on each platf
![](docs/data/how-it-works.png) ![](docs/data/how-it-works.png)
<sup>Explore an interactive version of this diagram [in the docs](https://cibuildwheel.pypa.io/en/stable/#how-it-works).</sup> <sup>Explore an interactive version of this diagram [in the docs](https://cibuildwheel.readthedocs.io/en/stable/#how-it-works).</sup>
Options
<!--[[[cog from readme_options_table import get_table; print(get_table()) ]]]--> -------
<!-- This table is auto-generated from docs/options.md by bin/readme_options_table.py -->
| | Option | Description | | | Option | Description |
|---|---|---| |---|--------|-------------|
| **Build selection** | [`platform`](https://cibuildwheel.pypa.io/en/stable/options/#platform) | Override the auto-detected target platform | | **Build selection** | [`CIBW_PLATFORM`](https://cibuildwheel.readthedocs.io/en/stable/options/#platform) | Override the auto-detected target platform |
| | [`build`<br>`skip`](https://cibuildwheel.pypa.io/en/stable/options/#build-skip) | Choose the Python versions to build | | | [`CIBW_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) <br> [`CIBW_SKIP`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) | Choose the Python versions to build |
| | [`archs`](https://cibuildwheel.pypa.io/en/stable/options/#archs) | Change the architectures built on your machine by default. | | | [`CIBW_ARCHS`](https://cibuildwheel.readthedocs.io/en/stable/options/#archs) | Change the architectures built on your machine by default. |
| | [`project-requires-python`](https://cibuildwheel.pypa.io/en/stable/options/#requires-python) | Manually set the Python compatibility of your project | | | [`CIBW_PROJECT_REQUIRES_PYTHON`](https://cibuildwheel.readthedocs.io/en/stable/options/#requires-python) | Manually set the Python compatibility of your project |
| | [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable) | Enable building with extra categories of selectors present. | | | [`CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#prerelease-pythons) | Enable building with pre-release versions of Python if available |
| | [`allow-empty`](https://cibuildwheel.pypa.io/en/stable/options/#allow-empty) | Suppress the error code if no wheels match the specified build identifiers | | **Build customization** | [`CIBW_BUILD_FRONTEND`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-frontend) | Set the tool to use to build, either "pip" (default for now) or "build" |
| **Build customization** | [`build-frontend`](https://cibuildwheel.pypa.io/en/stable/options/#build-frontend) | Set the tool to use to build, either "build" (default), "build\[uv\]", or "pip" | | | [`CIBW_ENVIRONMENT`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment) | Set environment variables needed during the build |
| | [`config-settings`](https://cibuildwheel.pypa.io/en/stable/options/#config-settings) | Specify config-settings for the build backend. | | | [`CIBW_ENVIRONMENT_PASS_LINUX`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment-pass) | Set environment variables on the host to pass-through to the container during the build. |
| | [`environment`](https://cibuildwheel.pypa.io/en/stable/options/#environment) | Set environment variables | | | [`CIBW_BEFORE_ALL`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
| | [`environment-pass`](https://cibuildwheel.pypa.io/en/stable/options/#environment-pass) | Set environment variables on the host to pass-through to the container. | | | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
| | [`before-all`](https://cibuildwheel.pypa.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. | | | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel |
| | [`before-build`](https://cibuildwheel.pypa.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build | | | [`CIBW_MANYLINUX_*_IMAGE`<br/>`CIBW_MUSLLINUX_*_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images |
| | [`xbuild-tools`](https://cibuildwheel.pypa.io/en/stable/options/#xbuild-tools) | Binaries on the path that should be included in an isolated cross-build environment. | | | [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.readthedocs.io/en/stable/options/#container-engine) | Specify which container engine to use when building Linux wheels |
| | [`repair-wheel-command`](https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel | | | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses |
| | [`manylinux-*-image`<br>`musllinux-*-image`](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) | Specify manylinux / musllinux container images | | **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel |
| | [`container-engine`](https://cibuildwheel.pypa.io/en/stable/options/#container-engine) | Specify the container engine to use when building Linux wheels | | | [`CIBW_BEFORE_TEST`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
| | [`dependency-versions`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) | Control the versions of the tools cibuildwheel uses | | | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
| | [`pyodide-version`](https://cibuildwheel.pypa.io/en/stable/options/#pyodide-version) | Specify the Pyodide version to use for `pyodide` platform builds | | | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require |
| **Testing** | [`test-command`](https://cibuildwheel.pypa.io/en/stable/options/#test-command) | The command to test each built wheel | | | [`CIBW_TEST_SKIP`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-skip) | Skip running tests on some builds |
| | [`before-test`](https://cibuildwheel.pypa.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel | | **Other** | [`CIBW_BUILD_VERBOSITY`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-verbosity) | Increase/decrease the output of pip wheel |
| | [`test-sources`](https://cibuildwheel.pypa.io/en/stable/options/#test-sources) | Paths that are copied into the working directory of the tests |
| | [`test-requires`](https://cibuildwheel.pypa.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
| | [`test-extras`](https://cibuildwheel.pypa.io/en/stable/options/#test-extras) | Install your wheel for testing using `extras_require` |
| | [`test-groups`](https://cibuildwheel.pypa.io/en/stable/options/#test-groups) | Specify test dependencies from your project's `dependency-groups` |
| | [`test-skip`](https://cibuildwheel.pypa.io/en/stable/options/#test-skip) | Skip running tests on some builds |
| | [`test-environment`](https://cibuildwheel.pypa.io/en/stable/options/#test-environment) | Set environment variables for the test environment |
| | [`test-runtime`](https://cibuildwheel.pypa.io/en/stable/options/#test-runtime) | Controls how the tests will be executed. |
| **Debugging** | [`debug-keep-container`](https://cibuildwheel.pypa.io/en/stable/options/#debug-keep-container) | Keep the container after running for debugging. |
| | [`debug-traceback`](https://cibuildwheel.pypa.io/en/stable/options/#debug-traceback) | Print full traceback when errors occur. |
| | [`build-verbosity`](https://cibuildwheel.pypa.io/en/stable/options/#build-verbosity) | Increase/decrease the output of the build |
These options can be specified in a pyproject.toml file, as well; see [configuration](https://cibuildwheel.readthedocs.io/en/stable/options/#configuration).
<!--[[[end]]] (sum: dbfwOkj/k/) -->
These options can be specified in a pyproject.toml file, or as environment variables, see [configuration docs](https://cibuildwheel.pypa.io/en/latest/configuration/).
Working examples Working examples
---------------- ----------------
@@ -179,27 +156,28 @@ Here are some repos that use cibuildwheel.
| Name | CI | OS | Notes | | Name | CI | OS | Notes |
|-----------------------------------|----|----|:------| |-----------------------------------|----|----|:------|
| [scikit-learn][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The machine learning library. A complex but clean config using many of cibuildwheel's features to build a large project with Cython and C++ extensions. | | [scikit-learn][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The machine learning library. A complex but clean config using many of cibuildwheel's features to build a large project with Cython and C++ extensions. |
| [duckdb][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | DuckDB is an analytical in-process SQL database management system |
| [pytorch-fairseq][] | ![github icon][] | ![apple icon][] ![linux icon][] | Facebook AI Research Sequence-to-Sequence Toolkit written in Python. | | [pytorch-fairseq][] | ![github icon][] | ![apple icon][] ![linux icon][] | Facebook AI Research Sequence-to-Sequence Toolkit written in Python. |
| [NumPy][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The fundamental package for scientific computing with Python. | | [NumPy][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The fundamental package for scientific computing with Python. |
| [NCNN][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | ncnn is a high-performance neural network inference framework optimized for the mobile platform |
| [Tornado][] | ![github icon][] | ![linux icon][] ![apple icon][] ![windows icon][] | Tornado is a Python web framework and asynchronous networking library. Uses stable ABI for a small C extension. | | [Tornado][] | ![github icon][] | ![linux icon][] ![apple icon][] ![windows icon][] | Tornado is a Python web framework and asynchronous networking library. Uses stable ABI for a small C extension. |
| [Matplotlib][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The venerable Matplotlib, a Python library with C++ portions | | [Matplotlib][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The venerable Matplotlib, a Python library with C++ portions |
| [MyPy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | The compiled version of MyPy using MyPyC. | | [NCNN][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | ncnn is a high-performance neural network inference framework optimized for the mobile platform |
| [Prophet][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Tool for producing high quality forecasts for time series data that has multiple seasonality with linear or non-linear growth. | | [Prophet][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Tool for producing high quality forecasts for time series data that has multiple seasonality with linear or non-linear growth. |
| [MyPy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | The compiled version of MyPy using MyPyC. |
| [Kivy][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Open source UI framework written in Python, running on Windows, Linux, macOS, Android and iOS | | [Kivy][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Open source UI framework written in Python, running on Windows, Linux, macOS, Android and iOS |
| [duckdb][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | DuckDB is an in-process SQL OLAP Database Management System |
[scikit-learn]: https://github.com/scikit-learn/scikit-learn [scikit-learn]: https://github.com/scikit-learn/scikit-learn
[duckdb]: https://github.com/duckdb/duckdb [pytorch-fairseq]: https://github.com/pytorch/fairseq
[pytorch-fairseq]: https://github.com/facebookresearch/fairseq
[NumPy]: https://github.com/numpy/numpy [NumPy]: https://github.com/numpy/numpy
[NCNN]: https://github.com/Tencent/ncnn
[Tornado]: https://github.com/tornadoweb/tornado [Tornado]: https://github.com/tornadoweb/tornado
[Matplotlib]: https://github.com/matplotlib/matplotlib [Matplotlib]: https://github.com/matplotlib/matplotlib
[MyPy]: https://github.com/mypyc/mypy_mypyc-wheels [NCNN]: https://github.com/Tencent/ncnn
[Prophet]: https://github.com/facebook/prophet [Prophet]: https://github.com/facebook/prophet
[MyPy]: https://github.com/mypyc/mypy_mypyc-wheels
[Kivy]: https://github.com/kivy/kivy [Kivy]: https://github.com/kivy/kivy
[duckdb]: https://github.com/duckdb/duckdb
[appveyor icon]: docs/data/readme_icons/appveyor.svg
[github icon]: docs/data/readme_icons/github.svg [github icon]: docs/data/readme_icons/github.svg
[azurepipelines icon]: docs/data/readme_icons/azurepipelines.svg [azurepipelines icon]: docs/data/readme_icons/azurepipelines.svg
[circleci icon]: docs/data/readme_icons/circleci.svg [circleci icon]: docs/data/readme_icons/circleci.svg
@@ -212,7 +190,7 @@ Here are some repos that use cibuildwheel.
<!-- END bin/projects.py --> <!-- END bin/projects.py -->
> ️ That's just a handful, there are many more! Check out the [Working Examples](https://cibuildwheel.pypa.io/en/stable/working-examples) page in the docs. > ️ That's just a handful, there are many more! Check out the [Working Examples](https://cibuildwheel.readthedocs.io/en/stable/working-examples) page in the docs.
Legal note Legal note
---------- ----------
@@ -226,102 +204,75 @@ This is similar to static linking, so it might have some license implications. C
Changelog Changelog
========= =========
<!-- [[[cog from readme_changelog import mini_changelog; print(mini_changelog()) ]]] --> <!-- START bin/update_readme_changelog.py -->
### v3.3.1 <!-- this section was generated by bin/update_readme_changelog.py -- do not edit manually -->
_5 January 2026_ ### v2.16.5
- 🛠 Update dependencies and container pins, including updating to CPython 3.14.2. (#2708) _30 January 2024_
### v3.3.0 - 🐛 Fix an incompatibility with the GitHub Action and new GitHub Runner images for Windows that bundle Powershell 7.3+ (#1741)
- 🛠 Preliminary support for new `macos-14` arm64 runners (#1743)
_12 November 2025_ ### v2.16.4
- 🐛 Fix an incompatibility with Docker v29 (#2660) _28 January 2024_
- ✨ Adds `test-runtime` option, to customise how tests on simulated/emulated environments are run (#2636)
- ✨ Adds support for new `manylinux_2_35` images on 32-bit ARM `armv7l`, offering better C++20 compatibility (#2656)
- ✨ `build[uv]` is now supported on Android (#2587)
- ✨ You can now install extras (such as `uv`) with a simple option on the GitHub Action (#2630)
- ✨ `{project}` and `{package}` placeholders are now supported in `repair-wheel-command` (#2589)
- 🛠 The versions set with `dependency-versions` no longer constrain packages specified by your `build-system.requires`. Previously, on platforms other than Linux, the constraints in this option would remain in the environment during the build. This has been tidied up make behaviour more consistent between platforms, and to prevent version conflicts. (#2583)
- 🛠 Improve the handling of `test-command` on Android, enabling more options to be passed (#2590)
- 📚 Docs improvements (#2618)
### v3.2.1 - 🛠 Update manylinux pins to upgrade from a problematic PyPy version. (#1737)
_12 October 2025_ ### v2.16.3
- 🛠 Update to CPython 3.14.0 final (#2614) _26 January 2024_
- 🐛 Fix the default MACOSX_DEPLOYMENT_TARGET on Python 3.14 (#2613)
- 📚 Docs improvements (#2617)
### v3.2.0 - 🐛 Fix a bug when building from sdist, where relative paths to files in the package didn't work because the working directory was wrong (#1687)
- 🛠 Adds the ability to disable mounting the host filesystem in containers to `/host`, through the `disable_host_mount` suboption on [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.readthedocs.io/en/stable/options/#container-engine).
- 📚 A lot of docs improvements! (#1708, #1705, #1686, #1679, #1667, #1665)
_22 September 2025_ ### v2.16.2
- ✨ Adds GraalPy v25 (Python 3.12) support (#2597) _3 October 2023_
- 🛠 Update to CPython 3.14.0rc3 (#2602)
- 🛠 Adds CPython 3.14.0 prerelease support for Android, and a number of improvements to Android builds (#2568, #2591)
- 🛠 Improvements to testing on Android, passing environment markers when installing the venv, and providing more debug output when build-verbosity is set (#2575)
- ⚠️ PyPy 3.10 was moved to `pypy-eol` in the `enable` option, as it is now end-of-life. (#2521)
- 📚 Docs improvements (#2574, #2601, #2598)
### v3.1.4 - 🛠 Updates CPython 3.12 version to 3.12.0, final release (#1635)
- ✨ Adds a debug option [`CIBW_DEBUG_KEEP_CONTAINER`](https://cibuildwheel.readthedocs.io/en/stable/options/#cibw_debug_keep_container) to stop cibuildwheel deleting build containers after the build finishes. (#1620)
- 📚 Adds support for `[tool.cibuildwheel]` checking by adding a schema compatible with the [validate-pyproject](https://github.com/abravalheri/validate-pyproject/) tool (#1622, #1628, #1629)
- 🐛 Fix parsing of `CIBW_CONTAINER_ENGINE` and `CIBW_BUILD_FRONTEND` options to not break arguments on `:` characters (#1621)
- 🐛 Fix the evaluation order of `CIBW_ENVIRONMENT` and `CIBW_ENVIRONMENT_PASS` so that `CIBW_ENVIRONMENT` assignments can reference environment variables passed through from the host machine. (#1617)
- 🛠 Supports manylinux images' deferred installation of interpreters through the `manylinux-interpreters` tool (#1630)
_19 August 2025_ ### v2.16.1
- ✨ Add a `--clean-cache` command to clean up our cache (#2489) _26 September 2023_
- 🛠 Update Python to 3.14rc2 and other patch version bumps (#2542, #2556)
- 🛠 Update Pyodide to 0.28.2 (#2562, #2558)
- 🐛 Fix resolution with `pyodide-build` when `dependency-versions` is set (#2548)
- 🐛 Set `CMAKE_FIND_ROOT_PATH_MODE_PACKAGE` to `BOTH` on Android (#2547)
- 🐛 Add `patchelf` dependency for platforms that can build Android wheels (#2552)
- 🐛 Ignore empty values for `CIBW_ARCHS` like most other environment variables (#2541)
- 💼 The `color` and `suggest_on_error` argparse options are now default in 3.14rc1+ (#2554)
- 💼 Use the virtualenv release URL instead of blob URL (should be more robust) (#2555)
- 🧪 For iOS, lowering to macos-14 is needed for now due to issues with GitHub's runner images (#2557)
- 🧪 Split out platforms iOS and Android in our tests (#2519)
- 🧪 Fix and enable doctests (#2546)
- 📚 Improve our docs on free-threading (#2549)
<!-- [[[end]]] (sum: PDe+dJWkRl) --> - 🛠 Updates the prerelease CPython 3.12 version to 3.12.0rc3 (#1625)
- 🛠 Only calls `linux32` in containers when necessary (#1599)
<!-- END bin/update_readme_changelog.py -->
--- ---
That's the last few versions. That's the last few versions.
️ **Want more changelog? Head over to [the changelog page in the docs](https://cibuildwheel.pypa.io/en/stable/changelog/).** ️ **Want more changelog? Head over to [the changelog page in the docs](https://cibuildwheel.readthedocs.io/en/stable/changelog/).**
--- ---
Contributing Contributing
============ ============
For more info on how to contribute to cibuildwheel, see the [docs](https://cibuildwheel.pypa.io/en/latest/contributing/). For more info on how to contribute to cibuildwheel, see the [docs](https://cibuildwheel.readthedocs.io/en/latest/contributing/).
Everyone interacting with the cibuildwheel project via codebase, issue tracker, chat rooms, or otherwise is expected to follow the [PSF Code of Conduct](https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md). Everyone interacting with the cibuildwheel project via codebase, issue tracker, chat rooms, or otherwise is expected to follow the [PSF Code of Conduct](https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md).
Maintainers Maintainers
----------- -----------
Core:
- Joe Rickerby [@joerick](https://github.com/joerick) - Joe Rickerby [@joerick](https://github.com/joerick)
- Yannick Jadoul [@YannickJadoul](https://github.com/YannickJadoul) - Yannick Jadoul [@YannickJadoul](https://github.com/YannickJadoul)
- Matthieu Darbois [@mayeut](https://github.com/mayeut) - Matthieu Darbois [@mayeut](https://github.com/mayeut)
- Henry Schreiner [@henryiii](https://github.com/henryiii) - Henry Schreiner [@henryiii](https://github.com/henryiii)
- Grzegorz Bokota [@Czaki](https://github.com/Czaki) - Grzegorz Bokota [@Czaki](https://github.com/Czaki)
Platform maintainers:
- Russell Keith-Magee [@freakboy3742](https://github.com/freakboy3742) (iOS)
- Agriya Khetarpal [@agriyakhetarpal](https://github.com/agriyakhetarpal) (Pyodide)
- Hood Chatham [@hoodmane](https://github.com/hoodmane) (Pyodide)
- Gyeongjae Choi [@ryanking13](https://github.com/ryanking13) (Pyodide)
- Tim Felgentreff [@timfel](https://github.com/timfel) (GraalPy)
- Malcolm Smith [@mhsmith](https://github.com/mhsmith) (Android)
Credits Credits
------- -------
@@ -344,4 +295,4 @@ See also
Another very similar tool to consider is [matthew-brett/multibuild](http://github.com/matthew-brett/multibuild). `multibuild` is a shell script toolbox for building a wheel on various platforms. It is used as a basis to build some of the big data science tools, like SciPy. Another very similar tool to consider is [matthew-brett/multibuild](http://github.com/matthew-brett/multibuild). `multibuild` is a shell script toolbox for building a wheel on various platforms. It is used as a basis to build some of the big data science tools, like SciPy.
If you are building Rust wheels, you can get by without some of the tricks required to make GLIBC work via manylinux; this is especially relevant for cross-compiling, which is easy with Rust. See [maturin-action](https://github.com/PyO3/maturin-action) for a tool that is optimized for building Rust wheels and cross-compiling. If you are building Rust wheels, you can get by without some of the tricks required to make GLIBC work via manylinux; this is especially relevant for cross-compiling, which is easy with Rust. See [maturin-action](https://github.com/messense/maturin-action) for a tool that is optimized for building Rust wheels and cross-compiling.
+22 -71
View File
@@ -17,10 +17,6 @@ inputs:
description: 'Build a specific wheel only. No need for arch/platform if this is set' description: 'Build a specific wheel only. No need for arch/platform if this is set'
required: false required: false
default: '' default: ''
extras:
description: 'Comma-separated list of extras to install'
required: false
default: ''
branding: branding:
icon: package icon: package
color: yellow color: yellow
@@ -28,90 +24,45 @@ branding:
runs: runs:
using: composite using: composite
steps: steps:
- uses: actions/setup-python@v6 # Set up a non-EOL, cibuildwheel & pipx supported Python version
- uses: actions/setup-python@v5
id: python id: python
with: with:
python-version: "3.11 - 3.14" python-version: "3.8 - 3.12"
update-environment: false update-environment: false
- id: cibw # macos-14 (M1) may be missing pipx (due to it not having CPython)
run: | - run: |
# Install cibuildwheel "${{ steps.python.outputs.python-path }}" -m pip install pipx
"${{ steps.python.outputs.python-path }}" -u << "EOF"
import os
import shutil
import sys
import venv
from pathlib import Path
from subprocess import run
EXTRAS = set(e.strip() for e in "${{ inputs.extras }}".split(",") if e.strip())
class EnvBuilder(venv.EnvBuilder):
def __init__(self):
super().__init__()
def setup_scripts(self, context):
pass
def post_setup(self, context):
super().post_setup(context)
self.bin_path = Path(context.env_exe).parent
install_spec = r"${{ github.action_path }}"
if EXTRAS:
install_spec += f"[{','.join(sorted(EXTRAS))}]"
run([sys.executable, "-m", "pip", "--python", context.env_exe, "install", install_spec], check=True)
print("::group::Install cibuildwheel")
venv_path = Path(r"${{ runner.temp }}") / "cibw"
if venv_path.exists():
shutil.rmtree(venv_path)
builder = EnvBuilder()
builder.create(venv_path)
exposed_binaries = {"cibuildwheel"}
if "uv" in EXTRAS:
exposed_binaries.add("uv")
clean_bin_path = builder.bin_path.parent / f"{builder.bin_path.name}.clean"
clean_bin_path.mkdir()
for path in list(builder.bin_path.iterdir()):
if path.stem in exposed_binaries:
try:
os.symlink(path, clean_bin_path / path.name)
except OSError:
import shutil
shutil.copy2(path, clean_bin_path / path.name)
full_path = f"{clean_bin_path}{os.pathsep}{os.environ['PATH']}"
with open(os.environ["GITHUB_OUTPUT"], "at") as f:
f.write(f"updated-path={full_path}\n")
print("::endgroup::")
EOF
shell: bash shell: bash
# Redirecting stderr to stdout to fix interleaving issue in Actions. # Redirecting stderr to stdout to fix interleaving issue in Actions.
- run: > - run: >
"${{ steps.python.outputs.python-path }}" -m pipx run
--spec '${{ github.action_path }}'
cibuildwheel cibuildwheel
"${{ inputs.package-dir }}" "${{ inputs.package-dir }}"
${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} --output-dir "${{ inputs.output-dir }}"
${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} --config-file "${{ inputs.config-file }}"
${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} --only "${{ inputs.only }}"
2>&1 2>&1
env:
PATH: "${{ steps.cibw.outputs.updated-path }}"
shell: bash shell: bash
if: runner.os != 'Windows' if: runner.os != 'Windows'
# Windows needs powershell to interact nicely with Meson # Windows needs powershell to interact nicely with Meson
# $PSNativeCommandArgumentPassing was introduced in pwsh 7.3 and the
# legacy behaviour is needed for backwards compatibility with how this
# was called in the past.
- run: > - run: >
if ($PSNativeCommandArgumentPassing) {
$PSNativeCommandArgumentPassing = 'Legacy'
};
& "${{ steps.python.outputs.python-path }}" -m pipx run
--spec "${{ github.action_path }}"
cibuildwheel cibuildwheel
"${{ inputs.package-dir }}" "${{ inputs.package-dir }}"
${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} --output-dir '"${{ inputs.output-dir }}"'
${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} --config-file '"${{ inputs.config-file }}"'
${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} --only '"${{ inputs.only }}"'
env:
PATH: "${{ steps.cibw.outputs.updated-path }}"
shell: pwsh shell: pwsh
if: runner.os == 'Windows' if: runner.os == 'Windows'
+35
View File
@@ -0,0 +1,35 @@
environment:
matrix:
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu
APPVEYOR_JOB_NAME: "python38-x64-ubuntu"
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
APPVEYOR_JOB_NAME: "python38-x64-vs2015"
- APPVEYOR_BUILD_WORKER_IMAGE: macos
APPVEYOR_JOB_NAME: "python38-x64-macos"
stack: python 3.8
build: off
init:
- cmd: set PATH=C:\Python38;C:\Python38\Scripts;%PATH%
- ps: |
$BRANCH = if ($env:APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH) { $env:APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH } else { $env:APPVEYOR_REPO_BRANCH }
if (-not ($BRANCH -eq 'main' -or $BRANCH.ToLower().StartsWith('appveyor-'))) {
$env:PYTEST_ADDOPTS='-k "unit_test or test_0_basic" --suppress-no-test-exit-code'
}
python -m pip install -U pip
install: python -m pip install -e ".[dev]" pytest-custom-exit-code
# the '-u' flag is required so the output is in the correct order.
# See https://github.com/pypa/cibuildwheel/pull/24 for more info.
test_script: python -u ./bin/run_tests.py
branches:
only:
- main
skip_commits:
files:
- docs/*
+28 -66
View File
@@ -1,76 +1,38 @@
pr: pr:
paths: paths:
exclude: exclude:
- .github/** - docs/*
- bin/*
- docs/**
- examples/github-*
- examples/ci*
- examples/travis-ci-*
- .ci*
- .pre-commit-config.yaml - .pre-commit-config.yaml
- .readthedocs.yml
- .travis.yml
- README.md
- mkdocs.yml
- noxfile.py
jobs: jobs:
- job: tests - job: linux_38
strategy: timeoutInMinutes: 120
matrix: pool: {vmImage: 'Ubuntu-20.04'}
linux_311:
imageName: "ubuntu-latest"
pythonVersion: "3.11"
testSelect: "native"
android_311:
imageName: "ubuntu-latest"
pythonVersion: "3.11"
testSelect: "android"
macos_311:
imageName: "macos-latest"
pythonVersion: "3.11"
testSelect: "native"
ios_311:
imageName: "macos-14" # See https://github.com/actions/runner-images/issues/12777
pythonVersion: "3.11"
testSelect: "ios"
android_macos_311:
imageName: "macos-latest"
pythonVersion: "3.11"
testSelect: "android"
windows_311:
imageName: "windows-latest"
pythonVersion: "3.11"
testSelect: "native"
timeoutInMinutes: 180
pool:
vmImage: $(imageName)
steps: steps:
- task: UsePythonVersion@0 - task: UsePythonVersion@0
inputs: inputs:
versionSpec: $(pythonVersion) versionSpec: '3.8'
- task: JavaToolInstaller@0
condition: and(eq(variables['testSelect'], 'android'), eq(variables['Agent.OS'], 'Linux'))
inputs:
versionSpec: '17'
jdkArchitectureOption: 'x64'
jdkSourceOption: 'PreInstalled'
- bash: docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
condition: and(eq(variables['imageName'], 'ubuntu-latest'), eq(variables['testSelect'], 'native'))
displayName: 'Install binfmt on Linux'
- bash: python -m pip install -U pip && python -m pip install -e. --group test
displayName: 'Update pip and install cibuildwheel --group test'
- bash: echo "##vso[task.setvariable variable=CIBW_ENABLE;]all"
condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
displayName: Set CIBW_ENABLE to all (main branch)
- bash: | - bash: |
echo "CIBW_ENABLE = $CIBW_ENABLE" python -m pip install -e ".[dev]"
python ./bin/run_tests.py --test-select $(testSelect) python ./bin/run_tests.py
displayName: 'Run tests'
- job: macos_38
pool: {vmImage: 'macOS-11'}
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- bash: |
python -m pip install -e ".[dev]"
python ./bin/run_tests.py
- job: windows_38
pool: {vmImage: 'windows-2019'}
timeoutInMinutes: 180
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- bash: |
python -m pip install -e ".[dev]"
python ./bin/run_tests.py
+15 -15
View File
@@ -1,29 +1,30 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# /// script
# dependencies = ["click", "packaging", "prek"]
# ///
from __future__ import annotations
import glob
import os import os
import subprocess import subprocess
import sys import sys
import tomllib
import urllib.parse import urllib.parse
from pathlib import Path from pathlib import Path
import click import click
from packaging.version import InvalidVersion, Version from packaging.version import InvalidVersion, Version
import cibuildwheel
config = [ config = [
# file path, version find/replace format # file path, version find/replace format
("pyproject.toml", 'version = "{}"'),
("README.md", "cibuildwheel=={}"), ("README.md", "cibuildwheel=={}"),
("cibuildwheel/__init__.py", '__version__ = "{}"'), ("cibuildwheel/__init__.py", '__version__ = "{}"'),
("docs/faq.md", "cibuildwheel=={}"), ("docs/faq.md", "cibuildwheel=={}"),
("docs/faq.md", "cibuildwheel@v{}"), ("docs/faq.md", "cibuildwheel@v{}"),
("docs/setup.md", "cibuildwheel=={}"),
("examples/*", "cibuildwheel=={}"), ("examples/*", "cibuildwheel=={}"),
("examples/*", "cibuildwheel@v{}"), ("examples/*", "cibuildwheel@v{}"),
("setup.cfg", "version = {}"),
] ]
RED = "\u001b[31m" RED = "\u001b[31m"
@@ -33,8 +34,7 @@ OFF = "\u001b[0m"
@click.command() @click.command()
def bump_version() -> None: def bump_version() -> None:
with open("pyproject.toml", "rb") as f: current_version = cibuildwheel.__version__
current_version = tomllib.load(f)["project"]["version"]
try: try:
commit_date_str = subprocess.run( commit_date_str = subprocess.run(
@@ -68,8 +68,8 @@ def bump_version() -> None:
sys.exit(1) sys.exit(1)
# fmt: off # fmt: off
print( "Current version:", current_version) print( 'Current version:', current_version)
new_version = input(" New version: ").strip() new_version = input(' New version: ').strip()
# fmt: on # fmt: on
try: try:
@@ -82,7 +82,7 @@ def bump_version() -> None:
actions = [] actions = []
for path_pattern, version_pattern in config: for path_pattern, version_pattern in config:
paths = list(Path().glob(path_pattern)) paths = [Path(p) for p in glob.glob(path_pattern)]
if not paths: if not paths:
print(f"error: Pattern {path_pattern} didn't match any files") print(f"error: Pattern {path_pattern} didn't match any files")
@@ -129,8 +129,8 @@ def bump_version() -> None:
contents = contents.replace(find, replace) contents = contents.replace(find, replace)
path.write_text(contents, encoding="utf8") path.write_text(contents, encoding="utf8")
print("Files updated. If you want to update docs/changelog.md as part of") print("Files updated. If you want to update the changelog as part of this")
print("this commit, do that now.") print("commit, do that now.")
print() print()
while input('Type "done" to continue: ').strip().lower() != "done": while input('Type "done" to continue: ').strip().lower() != "done":
@@ -139,7 +139,7 @@ def bump_version() -> None:
# run pre-commit to update the README changelog # run pre-commit to update the README changelog
subprocess.run( subprocess.run(
[ [
"prek", "pre-commit",
"run", "run",
"--files=docs/changelog.md", "--files=docs/changelog.md",
], ],
@@ -149,7 +149,7 @@ def bump_version() -> None:
# run pre-commit to check that no errors occurred on the second run # run pre-commit to check that no errors occurred on the second run
subprocess.run( subprocess.run(
[ [
"prek", "pre-commit",
"run", "run",
"--files=docs/changelog.md", "--files=docs/changelog.md",
], ],
@@ -181,7 +181,7 @@ def bump_version() -> None:
print() print()
print("Push the new version to GitHub with:") print("Push the new version to GitHub with:")
print(f" git push && git push origin v{new_version}") print(" git push && git push --tags")
print() print()
release_url = "https://github.com/pypa/cibuildwheel/releases/new?" + urllib.parse.urlencode( release_url = "https://github.com/pypa/cibuildwheel/releases/new?" + urllib.parse.urlencode(
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
CIBW_PLATFORM=linux pytest "$@"
-69
View File
@@ -1,69 +0,0 @@
#!/usr/bin/env python3
import sys
import textwrap
from pathlib import Path
import click
from cibuildwheel.extra import get_pyodide_xbuildenv_info
@click.command()
@click.argument(
"pyodide-version",
type=str,
)
@click.option(
"--output-file",
type=click.Path(),
default=None,
help="Output file to write the constraints to. If not provided, the constraints will be printed to stdout.",
)
def generate_pyodide_constraints(pyodide_version: str, output_file: str | None = None) -> None:
"""
Generate constraints for a specific Pyodide version. The constraints are
generated based on the Pyodide version's xbuildenv info, which is retrieved
from the Pyodide repository.
These constraints should then be 'pinned' using `uv pip compile`.
Example usage:
bin/generate_pyodide_constraints.py 0.27.0
"""
xbuildenv_info = get_pyodide_xbuildenv_info()
try:
pyodide_version_xbuildenv_info = xbuildenv_info["releases"][pyodide_version]
except KeyError as e:
msg = f"Pyodide version {pyodide_version} not found in xbuildenv info. Versions available: {', '.join(xbuildenv_info['releases'].keys())}"
raise click.BadParameter(msg) from e
pyodide_build_min_version = pyodide_version_xbuildenv_info.get("min_pyodide_build_version")
pyodide_build_max_version = pyodide_version_xbuildenv_info.get("max_pyodide_build_version")
pyodide_build_specifier_parts: list[str] = []
if pyodide_build_min_version:
pyodide_build_specifier_parts.append(f">={pyodide_build_min_version}")
if pyodide_build_max_version:
pyodide_build_specifier_parts.append(f"<={pyodide_build_max_version}")
pyodide_build_specifier = ",".join(pyodide_build_specifier_parts)
constraints_txt = textwrap.dedent(f"""
pip
build[virtualenv]
pyodide-build{pyodide_build_specifier}
click<8.2
""")
if output_file is None:
print(constraints_txt)
else:
Path(output_file).write_text(constraints_txt)
print(f"Constraints written to {output_file}", file=sys.stderr)
if __name__ == "__main__":
generate_pyodide_constraints()
+43 -132
View File
@@ -1,8 +1,4 @@
#!/usr/bin/env -S uv run -q #!/usr/bin/env python
# /// script
# dependencies = ["pyyaml"]
# ///
import argparse import argparse
import copy import copy
@@ -11,30 +7,13 @@ from typing import Any
import yaml import yaml
parser = argparse.ArgumentParser(allow_abbrev=False) parser = argparse.ArgumentParser()
parser.add_argument("--schemastore", action="store_true", help="Generate schema_store version") parser.add_argument("--schemastore", action="store_true", help="Generate schema_store version")
args = parser.parse_args() args = parser.parse_args()
starter = """ starter = """
$schema: http://json-schema.org/draft-07/schema#
$id: https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json $id: https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json
$defs: $schema: http://json-schema.org/draft-07/schema
inherit:
enum:
- none
- prepend
- append
default: none
description: How to inherit the parent's value.
enable:
enum:
- cpython-freethreading
- cpython-prerelease
- graalpy
- pyodide-prerelease
- pypy
- pypy-eol
description: A Python version or flavor to enable.
additionalProperties: false additionalProperties: false
description: cibuildwheel's settings. description: cibuildwheel's settings.
type: object type: object
@@ -57,21 +36,19 @@ properties:
type: string_array type: string_array
build-frontend: build-frontend:
default: default default: default
description: Set the tool to use to build, either "build" (default), "build[uv]", or "pip" description: Set the tool to use to build, either "pip" (default for now) or "build"
oneOf: oneOf:
- enum: [pip, build, "build[uv]", default] - enum: [pip, build, default]
- type: string - type: string
pattern: '^pip; ?args:' pattern: '^pip; ?args:'
- type: string - type: string
pattern: '^build; ?args:' pattern: '^build; ?args:'
- type: string
pattern: '^build\\[uv\\]; ?args:'
- type: object - type: object
additionalProperties: false additionalProperties: false
required: [name] required: [name]
properties: properties:
name: name:
enum: [pip, build, "build[uv]"] enum: [pip, build]
args: args:
type: array type: array
items: items:
@@ -107,31 +84,7 @@ properties:
dependency-versions: dependency-versions:
default: pinned default: pinned
description: Specify how cibuildwheel controls the versions of the tools it uses description: Specify how cibuildwheel controls the versions of the tools it uses
oneOf: type: string
- enum: [pinned, latest]
- type: string
description: Path to a file containing dependency versions, or inline package specifications, starting with "packages:"
not:
enum: [pinned, latest]
- type: object
additionalProperties: false
properties:
file:
type: string
- type: object
additionalProperties: false
properties:
packages:
type: array
items:
type: string
enable:
description: Enable or disable certain builds.
oneOf:
- $ref: "#/$defs/enable"
- type: array
items:
$ref: "#/$defs/enable"
environment: environment:
description: Set environment variables needed during the build. description: Set environment variables needed during the build.
type: string_table type: string_table
@@ -142,9 +95,6 @@ properties:
manylinux-aarch64-image: manylinux-aarch64-image:
type: string type: string
description: Specify alternative manylinux / musllinux container images description: Specify alternative manylinux / musllinux container images
manylinux-armv7l-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-i686-image: manylinux-i686-image:
type: string type: string
description: Specify alternative manylinux / musllinux container images description: Specify alternative manylinux / musllinux container images
@@ -160,9 +110,6 @@ properties:
manylinux-pypy_x86_64-image: manylinux-pypy_x86_64-image:
type: string type: string
description: Specify alternative manylinux / musllinux container images description: Specify alternative manylinux / musllinux container images
manylinux-riscv64-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-s390x-image: manylinux-s390x-image:
type: string type: string
description: Specify alternative manylinux / musllinux container images description: Specify alternative manylinux / musllinux container images
@@ -172,33 +119,21 @@ properties:
musllinux-aarch64-image: musllinux-aarch64-image:
type: string type: string
description: Specify alternative manylinux / musllinux container images description: Specify alternative manylinux / musllinux container images
musllinux-armv7l-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-i686-image: musllinux-i686-image:
type: string type: string
description: Specify alternative manylinux / musllinux container images description: Specify alternative manylinux / musllinux container images
musllinux-ppc64le-image: musllinux-ppc64le-image:
type: string type: string
description: Specify alternative manylinux / musllinux container images description: Specify alternative manylinux / musllinux container images
musllinux-riscv64-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-s390x-image: musllinux-s390x-image:
type: string type: string
description: Specify alternative manylinux / musllinux container images description: Specify alternative manylinux / musllinux container images
musllinux-x86_64-image: musllinux-x86_64-image:
type: string type: string
description: Specify alternative manylinux / musllinux container images description: Specify alternative manylinux / musllinux container images
xbuild-tools:
description: Binaries on the path that should be included in an isolated cross-build environment
type: string_array
pyodide-version:
type: string
description: Specify the version of Pyodide to use
repair-wheel-command: repair-wheel-command:
description: Execute a shell command to repair each built wheel.
type: string_array type: string_array
description: Execute a shell command to repair each built wheel.
skip: skip:
description: Choose the Python versions to skip. description: Choose the Python versions to skip.
type: string_array type: string_array
@@ -208,43 +143,19 @@ properties:
test-extras: test-extras:
description: Install your wheel for testing using `extras_require` description: Install your wheel for testing using `extras_require`
type: string_array type: string_array
test-sources:
description: Test files that are required by the test environment
type: string_array
test-groups:
description: Install extra groups when testing
type: string_array
test-requires: test-requires:
description: Install Python dependencies before running the tests description: Install Python dependencies before running the tests
type: string_array type: string_array
test-skip: test-skip:
description: Skip running tests on some builds. description: Skip running tests on some builds.
type: string_array type: string_array
test-environment:
description: Set environment variables for the test environment
type: string_table
test-runtime:
description: Additional configuration for the test runner
oneOf:
- type: string
pattern: '^$'
- type: object
additionalProperties: false
- type: string
pattern: 'args:'
- type: object
additionalProperties: false
required: [args]
properties:
args:
type: array
items:
type: string
""" """
schema = yaml.safe_load(starter) schema = yaml.safe_load(starter)
if args.schemastore:
schema["$id"] += "#"
string_array = yaml.safe_load( string_array = yaml.safe_load(
""" """
- type: string - type: string
@@ -303,36 +214,23 @@ items:
additionalProperties: false additionalProperties: false
properties: properties:
select: {} select: {}
inherit:
type: object
additionalProperties: false
properties:
before-all: {"$ref": "#/$defs/inherit"}
before-build: {"$ref": "#/$defs/inherit"}
xbuild-tools: {"$ref": "#/$defs/inherit"}
before-test: {"$ref": "#/$defs/inherit"}
config-settings: {"$ref": "#/$defs/inherit"}
container-engine: {"$ref": "#/$defs/inherit"}
environment: {"$ref": "#/$defs/inherit"}
environment-pass: {"$ref": "#/$defs/inherit"}
repair-wheel-command: {"$ref": "#/$defs/inherit"}
test-command: {"$ref": "#/$defs/inherit"}
test-extras: {"$ref": "#/$defs/inherit"}
test-sources: {"$ref": "#/$defs/inherit"}
test-requires: {"$ref": "#/$defs/inherit"}
test-environment: {"$ref": "#/$defs/inherit"}
test-runtime: {"$ref": "#/$defs/inherit"}
""" """
) )
for key, value in schema["properties"].items(): for key, value in schema["properties"].items():
value["title"] = f"CIBW_{key.replace('-', '_').upper()}" value["title"] = f'CIBW_{key.replace("-", "_").upper()}'
non_global_options = {k: {"$ref": f"#/properties/{k}"} for k in schema["properties"]} if args.schemastore:
non_global_options = {
k: {"$ref": f"#/properties/tool/properties/cibuildwheel/properties/{k}"}
for k in schema["properties"]
}
else:
non_global_options = {k: {"$ref": f"#/properties/{k}"} for k in schema["properties"]}
del non_global_options["build"] del non_global_options["build"]
del non_global_options["skip"] del non_global_options["skip"]
del non_global_options["container-engine"]
del non_global_options["test-skip"] del non_global_options["test-skip"]
del non_global_options["enable"]
overrides["items"]["properties"]["select"]["oneOf"] = string_array overrides["items"]["properties"]["select"]["oneOf"] = string_array
overrides["items"]["properties"] |= non_global_options.copy() overrides["items"]["properties"] |= non_global_options.copy()
@@ -342,7 +240,6 @@ del overrides["items"]["properties"]["archs"]
not_linux = non_global_options.copy() not_linux = non_global_options.copy()
del not_linux["environment-pass"] del not_linux["environment-pass"]
del not_linux["container-engine"]
for key in list(not_linux): for key in list(not_linux):
if "linux-" in key: if "linux-" in key:
del not_linux[key] del not_linux[key]
@@ -360,9 +257,6 @@ oses = {
"linux": as_object(non_global_options), "linux": as_object(non_global_options),
"windows": as_object(not_linux), "windows": as_object(not_linux),
"macos": as_object(not_linux), "macos": as_object(not_linux),
"pyodide": as_object(not_linux),
"android": as_object(not_linux),
"ios": as_object(not_linux),
} }
oses["linux"]["properties"]["repair-wheel-command"] = { oses["linux"]["properties"]["repair-wheel-command"] = {
@@ -379,10 +273,27 @@ del oses["linux"]["properties"]["dependency-versions"]
schema["properties"]["overrides"] = overrides schema["properties"]["overrides"] = overrides
schema["properties"] |= oses schema["properties"] |= oses
if args.schemastore: if not args.schemastore:
schema["$id"] = "https://json.schemastore.org/partial-cibuildwheel.json" print(json.dumps(schema, indent=2))
schema["description"] = ( raise SystemExit(0)
"cibuildwheel's settings. Generated with ./bin/generate_schema.py --schemastore from cibuildwheel."
)
print(json.dumps(schema, indent=2)) schema_store_txt = """
$id: https://json.schemastore.org/cibuildwheel.json
$schema: http://json-schema.org/draft-07/schema#
additionalProperties: false
description: cibuildwheel's toml file, generated with ./bin/generate_schema.py --schemastore from cibuildwheel.
type: object
properties:
tool:
type: object
properties:
cibuildwheel:
type: object
"""
schema_store = yaml.safe_load(schema_store_txt)
schema_store["properties"]["tool"]["properties"]["cibuildwheel"]["properties"] = schema[
"properties"
]
print(json.dumps(schema_store, indent=2))
+3
View File
@@ -11,6 +11,9 @@ This will cache the results to all_known_setup.yaml; you can reprint
the results without the `--online` setting. the results without the `--online` setting.
""" """
from __future__ import annotations
import ast import ast
from collections.abc import Iterable, Iterator from collections.abc import Iterable, Iterator
from pathlib import Path from pathlib import Path
+10 -11
View File
@@ -1,30 +1,29 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import os import os
import subprocess
import sys import sys
import textwrap import textwrap
import time import time
from pathlib import Path from pathlib import Path
from subprocess import run
import click import click
def shell(cmd: str, *, check: bool, **kwargs: object) -> subprocess.CompletedProcess[str]: def shell(cmd, *, check: bool, **kwargs):
return subprocess.run([cmd], shell=True, check=check, **kwargs) # type: ignore[call-overload, no-any-return] return run([cmd], shell=True, check=check, **kwargs)
def git_repo_has_changes() -> bool: def git_repo_has_changes():
unstaged_changes: bool = shell("git diff-index --quiet HEAD --", check=False).returncode != 0 unstaged_changes = shell("git diff-index --quiet HEAD --", check=False).returncode != 0
staged_changes: bool = ( staged_changes = shell("git diff-index --quiet --cached HEAD --", check=False).returncode != 0
shell("git diff-index --quiet --cached HEAD --", check=False).returncode != 0
)
return unstaged_changes or staged_changes return unstaged_changes or staged_changes
@click.command() @click.command()
def main() -> None: def main():
project_root = Path(__file__).parent / ".." project_root = Path(__file__).parent / ".."
os.chdir(project_root) os.chdir(project_root)
@@ -55,10 +54,10 @@ def main() -> None:
f""" f"""
Update the versions of our dependencies. Update the versions of our dependencies.
PR generated by `{Path(__file__).name}`. PR generated by `{os.path.basename(__file__)}`.
""" """
) )
subprocess.run( run(
[ [
"gh", "gh",
"pr", "pr",
+13 -21
View File
@@ -1,13 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# /// script
# dependencies = [
# "click",
# "pyyaml",
# "pygithub",
# ]
# ///
""" """
Convert a yaml project list into a nice table. Convert a yaml project list into a nice table.
@@ -17,22 +9,25 @@ Suggested usage:
git diff git diff
""" """
from __future__ import annotations
import builtins import builtins
import functools import functools
import textwrap import textwrap
import urllib.request import urllib.request
import xml.dom.minidom import xml.dom.minidom
from collections.abc import Iterable, Mapping, Sequence from collections.abc import Iterable, Mapping, Sequence
from datetime import UTC, datetime from datetime import datetime
from io import StringIO from io import StringIO
from pathlib import Path from pathlib import Path
from typing import Any, Self, TextIO from typing import Any, TextIO
import click import click
import yaml import yaml
from github import Github, GithubException from github import Github, GithubException
ICONS = ( ICONS = (
"appveyor",
"github", "github",
"azurepipelines", "azurepipelines",
"circleci", "circleci",
@@ -77,12 +72,12 @@ class Project:
self.notes = notes self.notes = notes
else: else:
self.num_stars = 0 self.num_stars = 0
self.pushed_at = datetime.now(UTC) self.pushed_at = datetime.utcnow()
name_len = len(self.name) + 4 name_len = len(self.name) + 4
self.__class__.NAME = max(self.__class__.NAME, name_len) self.__class__.NAME = max(self.__class__.NAME, name_len)
def __lt__(self, other: Self) -> bool: def __lt__(self, other: Project) -> bool:
if self.online: if self.online:
return self.num_stars < other.num_stars return self.num_stars < other.num_stars
else: else:
@@ -92,8 +87,8 @@ class Project:
def header(cls) -> str: def header(cls) -> str:
return textwrap.dedent( return textwrap.dedent(
f"""\ f"""\
| {"Name":{cls.NAME}} | CI | OS | Notes | | {'Name':{cls.NAME}} | CI | OS | Notes |
|{"":-^{cls.NAME + 2}}|----|----|:------|""" |{'':-^{cls.NAME+2 }}|----|----|:------|"""
) )
@property @property
@@ -124,7 +119,7 @@ class Project:
return f"[{self.name}]: {self.url}" return f"[{self.name}]: {self.url}"
def info(self) -> str: def info(self) -> str:
days = (datetime.now(UTC) - self.pushed_at).days days = (datetime.utcnow() - self.pushed_at).days
return f"<!-- {self.name}: {self.num_stars}, last pushed {days} days ago -->" return f"<!-- {self.name}: {self.num_stars}, last pushed {days} days ago -->"
@@ -135,7 +130,6 @@ def fetch_icon(icon_name: str) -> None:
document = xml.dom.minidom.parseString(original_svg_data) document = xml.dom.minidom.parseString(original_svg_data)
svgElement = document.documentElement svgElement = document.documentElement
assert svgElement is not None
assert svgElement.nodeName == "svg" assert svgElement.nodeName == "svg"
svgElement.setAttribute("width", "16px") svgElement.setAttribute("width", "16px")
svgElement.setAttribute("fill", "#606060") svgElement.setAttribute("fill", "#606060")
@@ -170,9 +164,7 @@ def get_projects(
return sorted((Project(item, github) for item in config), reverse=online) return sorted((Project(item, github) for item in config), reverse=online)
def render_projects( def render_projects(projects: Sequence[Project], *, dest_path: Path, include_info: bool = True):
projects: Sequence[Project], *, dest_path: Path, include_info: bool = True
) -> str:
io = StringIO() io = StringIO()
print = functools.partial(builtins.print, file=io) print = functools.partial(builtins.print, file=io)
@@ -203,7 +195,7 @@ def insert_projects_table(
projects: Sequence[Project], projects: Sequence[Project],
input_filename: str, input_filename: str,
include_info: bool = True, include_info: bool = True,
) -> None: ):
text = file.read_text() text = file.read_text()
projects_table = render_projects(projects, include_info=include_info, dest_path=file) projects_table = render_projects(projects, include_info=include_info, dest_path=file)
@@ -217,7 +209,7 @@ def insert_projects_table(
generated_note = f"<!-- this section is generated by bin/projects.py. Don't edit it directly, instead, edit {input_filename} -->" generated_note = f"<!-- this section is generated by bin/projects.py. Don't edit it directly, instead, edit {input_filename} -->"
new_text = ( new_text = (
f"{text[: start + len(start_str)]}\n{generated_note}\n\n{projects_table}\n{text[end:]}" f"{text[:start + len(start_str)]}\n{generated_note}\n\n{projects_table}\n{text[end:]}"
) )
file.write_text(new_text) file.write_text(new_text)
-23
View File
@@ -1,23 +0,0 @@
#!/usr/bin/env python3
import re
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent / ".."
CHANGELOG_FILE = PROJECT_ROOT / "docs" / "changelog.md"
# https://regexr.com/622ds
FIRST_5_CHANGELOG_ENTRIES_REGEX = re.compile(r"""(^###.*?(?=###)){5}""", re.DOTALL | re.MULTILINE)
def mini_changelog() -> str:
changelog_text = CHANGELOG_FILE.read_text()
mini_changelog_match = FIRST_5_CHANGELOG_ENTRIES_REGEX.search(changelog_text)
assert mini_changelog_match, "Failed to find the first few changelog entries"
return f"\n{mini_changelog_match.group(0).strip()}\n"
if __name__ == "__main__":
print(mini_changelog())
-68
View File
@@ -1,68 +0,0 @@
#!/usr/bin/env python3
import dataclasses
import re
from pathlib import Path
from typing import Final
DIR: Final[Path] = Path(__file__).parent.parent.resolve()
OPTIONS_MD: Final[Path] = DIR / "docs" / "options.md"
SECTION_HEADER_REGEX = re.compile(r"^## (?P<name>.*?)$", re.MULTILINE)
# https://regexr.com/8f1ff
OPTION_HEADER_REGEX = re.compile(
r"^### (?P<name>.*?){.*#(?P<id>\S+).*}\n+> ?(?P<desc>.*)$", re.MULTILINE
)
@dataclasses.dataclass(kw_only=True)
class Option:
name: str
id: str
desc: str
section: str
def get_table() -> str:
options_md = OPTIONS_MD.read_text(encoding="utf-8")
sections = SECTION_HEADER_REGEX.split(options_md)[1:]
options = []
for section_name, section_content in zip(sections[0::2], sections[1::2], strict=True):
for match in OPTION_HEADER_REGEX.finditer(section_content):
option = Option(
name=match.group("name").strip(),
id=match.group("id").strip(),
desc=match.group("desc").strip(),
section=section_name.strip(),
)
options.append(option)
table_md = "\n<!-- This table is auto-generated from docs/options.md by bin/readme_options_table.py -->\n\n"
table_md += "| | Option | Description |\n"
table_md += "|---|---|---|\n"
last_section: str | None = None
for option in options:
cells: list[str] = []
cells.append(f"**{option.section}**" if option.section != last_section else "")
last_section = option.section
url = f"https://cibuildwheel.pypa.io/en/stable/options/#{option.id}"
name = option.name.replace(", ", "<br>") # Replace commas with line breaks
cells.append(f"[{name}]({url})")
cells.append(option.desc)
table_md += "| " + " | ".join(cells) + " |\n"
table_md += "\n"
return table_md
if __name__ == "__main__":
print(get_table())
+29 -51
View File
@@ -1,35 +1,34 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import os import os
import re
import shutil import shutil
import subprocess
import sys import sys
import textwrap import textwrap
import time import time
import typing import typing
from glob import glob
from pathlib import Path from pathlib import Path
from subprocess import run
from urllib.parse import quote from urllib.parse import quote
import click import click
DIR = Path(__file__).parent.parent.resolve()
def shell(cmd, *, check: bool, **kwargs):
return run([cmd], shell=True, check=check, **kwargs)
def shell(cmd: str, *, check: bool, **kwargs: object) -> subprocess.CompletedProcess[str]: def git_repo_has_changes():
return subprocess.run([cmd], shell=True, check=check, **kwargs) # type: ignore[call-overload, no-any-return]
def git_repo_has_changes() -> bool:
unstaged_changes = shell("git diff-index --quiet HEAD --", check=False).returncode != 0 unstaged_changes = shell("git diff-index --quiet HEAD --", check=False).returncode != 0
staged_changes = shell("git diff-index --quiet --cached HEAD --", check=False).returncode != 0 staged_changes = shell("git diff-index --quiet --cached HEAD --", check=False).returncode != 0
return unstaged_changes or staged_changes return unstaged_changes or staged_changes
def generate_basic_project(path: Path) -> None: def generate_basic_project(path):
sys.path.insert(0, "") sys.path.insert(0, "")
from test.test_projects.c import new_c_project # noqa: PLC0415 from test.test_projects.c import new_c_project
project = new_c_project() project = new_c_project()
project.generate(path) project.generate(path)
@@ -39,32 +38,14 @@ class CIService(typing.NamedTuple):
name: str name: str
dst_config_path: str dst_config_path: str
badge_md: str badge_md: str
config_file_transform: typing.Callable[[str], str] = lambda x: x # identity by default
def github_config_file_transform(content: str) -> str:
# one of the the github configs only builds on main, so we need to remove that restriction
# so our example build will run on the test branch.
#
# replace:
# """
# push:
# branches:
# - main
# """
# with:
# """
# push:
# """"
content = re.sub(
r"push:\n\s+branches:\n\s+- main",
"push:",
content,
)
return content
services = [ services = [
CIService(
name="appveyor",
dst_config_path="appveyor.yml",
badge_md="[![Build status](https://ci.appveyor.com/api/projects/status/gt3vwl88yt0y3hur/branch/{branch}?svg=true)](https://ci.appveyor.com/project/joerick/cibuildwheel/branch/{branch})",
),
CIService( CIService(
name="azure-pipelines", name="azure-pipelines",
dst_config_path="azure-pipelines.yml", dst_config_path="azure-pipelines.yml",
@@ -78,8 +59,7 @@ services = [
CIService( CIService(
name="github", name="github",
dst_config_path=".github/workflows/example.yml", dst_config_path=".github/workflows/example.yml",
badge_md="[![Build](https://github.com/pypa/cibuildwheel/actions/workflows/example.yml/badge.svg?branch={branch})](https://github.com/pypa/cibuildwheel/actions?query=branch%3A{branch})", badge_md="[![Build](https://github.com/pypa/cibuildwheel/workflows/Build/badge.svg?branch={branch})](https://github.com/pypa/cibuildwheel/actions)",
config_file_transform=github_config_file_transform,
), ),
CIService( CIService(
name="travis-ci", name="travis-ci",
@@ -99,8 +79,9 @@ services = [
] ]
def ci_service_for_config_file(config_file: Path) -> CIService: def ci_service_for_config_file(config_file):
filename = config_file.name filename = Path(config_file).name
try: try:
return next(s for s in services if filename.startswith(s.name)) return next(s for s in services if filename.startswith(s.name))
except StopIteration: except StopIteration:
@@ -117,18 +98,16 @@ def run_example_ci_configs(config_files=None):
""" """
if len(config_files) == 0: if len(config_files) == 0:
config_files = Path("examples").glob("*-minimal.yml") config_files = glob("examples/*-minimal.yml")
else:
config_files = [Path(f) for f in config_files]
# check each CI service has at most 1 config file # check each CI service has at most 1 config file
configs_by_service = set() configs_by_service = {}
for config_file in config_files: for config_file in config_files:
service = ci_service_for_config_file(config_file) service = ci_service_for_config_file(config_file)
if service.name in configs_by_service: if service.name in configs_by_service:
msg = "You cannot specify more than one config per CI service" msg = "You cannot specify more than one config per CI service"
raise Exception(msg) raise Exception(msg)
configs_by_service.add(service.name) configs_by_service[service.name] = config_file
if git_repo_has_changes(): if git_repo_has_changes():
print("Your git repo has uncommitted changes. Commit or stash before continuing.") print("Your git repo has uncommitted changes. Commit or stash before continuing.")
@@ -149,25 +128,23 @@ def run_example_ci_configs(config_files=None):
for config_file in config_files: for config_file in config_files:
service = ci_service_for_config_file(config_file) service = ci_service_for_config_file(config_file)
src_config_file = Path(config_file)
dst_config_file = example_project / service.dst_config_path dst_config_file = example_project / service.dst_config_path
dst_config_file.parent.mkdir(parents=True, exist_ok=True) dst_config_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(src_config_file, dst_config_file)
contents = config_file.read_text(encoding="utf8") run(["git", "add", example_project], check=True)
contents = service.config_file_transform(contents)
dst_config_file.write_text(contents, encoding="utf8")
subprocess.run(["git", "add", example_project], check=True)
message = textwrap.dedent( message = textwrap.dedent(
f"""\ f"""\
Test example CI configs Test example minimal configs
Testing files: {[str(f) for f in config_files]} Testing files: {config_files}
Generated from branch: {previous_branch} Generated from branch: {previous_branch}
Time: {timestamp} Time: {timestamp}
""" """
) )
subprocess.run(["git", "commit", "--no-verify", "--message", message], check=True) run(["git", "commit", "--no-verify", "--message", message], check=True)
shell(f"git subtree --prefix={example_project} push origin {branch_name}", check=True) shell(f"git subtree --prefix={example_project} push origin {branch_name}", check=True)
print("---") print("---")
@@ -197,5 +174,6 @@ def run_example_ci_configs(config_files=None):
if __name__ == "__main__": if __name__ == "__main__":
os.chdir(DIR) os.chdir(os.path.dirname(__file__))
os.chdir("..")
run_example_ci_configs(standalone_mode=True) run_example_ci_configs(standalone_mode=True)
+5 -82
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import argparse import argparse
import os import os
@@ -8,61 +9,19 @@ import sys
from pathlib import Path from pathlib import Path
if __name__ == "__main__": if __name__ == "__main__":
if sys.version_info < (3, 13): parser = argparse.ArgumentParser()
default_cpu_count = os.cpu_count() or 2
else:
default_cpu_count = os.process_cpu_count() or 2
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument( parser.add_argument(
"--run-podman", action="store_true", default=False, help="run podman tests (linux only)" "--run-podman", action="store_true", default=False, help="run podman tests (linux only)"
) )
parser.add_argument(
"--num-processes",
action="store",
default=default_cpu_count,
help="number of processes to use for testing",
)
parser.add_argument(
"--test-select",
choices={"all", "native", "android", "ios", "pyodide"},
default="all",
help="Either 'native' or 'android'/'ios'/'pyodide'",
)
args = parser.parse_args() args = parser.parse_args()
# move cwd to the project root # move cwd to the project root
os.chdir(Path(__file__).resolve().parents[1]) os.chdir(Path(__file__).resolve().parents[1])
# doc tests
doc_test_args = [sys.executable, "-m", "pytest", "cibuildwheel"]
print(
"\n\n================================== DOC TESTS ==================================",
flush=True,
)
result = subprocess.run(doc_test_args, check=False)
if result.returncode not in (0, 5):
# Allow case where no doctests are collected (returncode 5) because
# circleci sets an explicit "-k" filter that disables doctests. There
# isn't a pattern that will only select doctests. This can be removed
# and have check=True if the circleci PYTEST_ADDOPTS is removed.
raise subprocess.CalledProcessError(
result.returncode, result.args, output=result.stdout, stderr=result.stderr
)
# unit tests # unit tests
print(
"\n\n================================== UNIT TESTS ==================================",
flush=True,
)
unit_test_args = [sys.executable, "-m", "pytest", "unit_test"] unit_test_args = [sys.executable, "-m", "pytest", "unit_test"]
if ( if sys.platform.startswith("linux"):
sys.platform.startswith("linux")
and os.environ.get("CIBW_PLATFORM", "linux") == "linux"
and args.test_select in ["all", "native"]
):
# run the docker unit tests only on Linux # run the docker unit tests only on Linux
unit_test_args += ["--run-docker"] unit_test_args += ["--run-docker"]
@@ -71,53 +30,17 @@ if __name__ == "__main__":
subprocess.run(unit_test_args, check=True) subprocess.run(unit_test_args, check=True)
print( # integration tests
"\n\n=========================== SERIAL INTEGRATION TESTS ===========================",
flush=True,
)
match args.test_select:
case "all":
marks = []
case "native":
marks = ["not pyodide", "not android", "not ios"]
case mark:
marks = [f"{mark}"]
# Run the serial integration tests without multiple processes
serial_integration_test_args = [
sys.executable,
"-m",
"pytest",
"-m",
f"{' and '.join(['serial', *marks])}",
"-x",
"--durations",
"0",
"--timeout=2400",
"test",
"-vv",
]
subprocess.run(serial_integration_test_args, check=True)
print(
"\n\n========================= NON-SERIAL INTEGRATION TESTS =========================",
flush=True,
)
integration_test_args = [ integration_test_args = [
sys.executable, sys.executable,
"-m", "-m",
"pytest", "pytest",
"-m", "--numprocesses=2",
f"{' and '.join(['not serial', *marks])}",
f"--numprocesses={args.num_processes}",
"-x", "-x",
"--durations", "--durations",
"0", "0",
"--timeout=2400", "--timeout=2400",
"test", "test",
"-vv",
] ]
if sys.platform.startswith("linux") and args.run_podman: if sys.platform.startswith("linux") and args.run_podman:
+2 -1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import argparse import argparse
import os import os
@@ -12,7 +13,7 @@ if __name__ == "__main__":
# move cwd to the project root # move cwd to the project root
os.chdir(Path(__file__).resolve().parents[1]) os.chdir(Path(__file__).resolve().parents[1])
parser = argparse.ArgumentParser(description="Runs a sample build", allow_abbrev=False) parser = argparse.ArgumentParser(description="Runs a sample build")
parser.add_argument("project_python_path", nargs="?", default="test.test_0_basic.basic_project") parser.add_argument("project_python_path", nargs="?", default="test.test_0_basic.basic_project")
options = parser.parse_args() options = parser.parse_args()
+51 -83
View File
@@ -1,84 +1,70 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import configparser import configparser
import dataclasses from dataclasses import dataclass
from pathlib import Path from pathlib import Path
import requests import requests
from packaging.version import Version
DIR = Path(__file__).parent.resolve() DIR = Path(__file__).parent.resolve()
RESOURCES = DIR.parent / "cibuildwheel/resources" RESOURCES = DIR.parent / "cibuildwheel/resources"
@dataclasses.dataclass(frozen=True) @dataclass(frozen=True)
class Image: class Image:
manylinux_version: str manylinux_version: str
platforms: list[str] platform: str
image_name: str image_name: str
tag: str | None = None # Set this to pin the image tag: str | None # Set this to pin the image
use_platform_suffix: bool = False
class PyPAImage(Image):
def __init__(self, manylinux_version: str, platforms: list[str], tag: str | None = None):
image_name = f"quay.io/pypa/{manylinux_version}"
super().__init__(manylinux_version, platforms, image_name, tag, True)
images = [ images = [
# manylinux1 images
Image("manylinux1", "x86_64", "quay.io/pypa/manylinux1_x86_64", None),
Image("manylinux1", "i686", "quay.io/pypa/manylinux1_i686", None),
# manylinux2010 images
Image("manylinux2010", "x86_64", "quay.io/pypa/manylinux2010_x86_64", None),
Image("manylinux2010", "i686", "quay.io/pypa/manylinux2010_i686", None),
Image("manylinux2010", "pypy_x86_64", "quay.io/pypa/manylinux2010_x86_64", None),
Image("manylinux2010", "pypy_i686", "quay.io/pypa/manylinux2010_i686", None),
# manylinux2014 images # manylinux2014 images
PyPAImage( Image("manylinux2014", "x86_64", "quay.io/pypa/manylinux2014_x86_64", None),
"manylinux2014", Image("manylinux2014", "i686", "quay.io/pypa/manylinux2014_i686", None),
[ Image("manylinux2014", "aarch64", "quay.io/pypa/manylinux2014_aarch64", None),
"x86_64", Image("manylinux2014", "ppc64le", "quay.io/pypa/manylinux2014_ppc64le", None),
"i686", Image("manylinux2014", "s390x", "quay.io/pypa/manylinux2014_s390x", None),
"aarch64", Image("manylinux2014", "pypy_x86_64", "quay.io/pypa/manylinux2014_x86_64", None),
"ppc64le", Image("manylinux2014", "pypy_i686", "quay.io/pypa/manylinux2014_i686", None),
"s390x", Image("manylinux2014", "pypy_aarch64", "quay.io/pypa/manylinux2014_aarch64", None),
"pypy_x86_64", # manylinux_2_24 images
"pypy_i686", Image("manylinux_2_24", "x86_64", "quay.io/pypa/manylinux_2_24_x86_64", None),
"pypy_aarch64", Image("manylinux_2_24", "i686", "quay.io/pypa/manylinux_2_24_i686", None),
], Image("manylinux_2_24", "aarch64", "quay.io/pypa/manylinux_2_24_aarch64", None),
), Image("manylinux_2_24", "ppc64le", "quay.io/pypa/manylinux_2_24_ppc64le", None),
Image("manylinux_2_24", "s390x", "quay.io/pypa/manylinux_2_24_s390x", None),
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 # manylinux_2_28 images
PyPAImage( Image("manylinux_2_28", "x86_64", "quay.io/pypa/manylinux_2_28_x86_64", None),
"manylinux_2_28", 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),
"x86_64", Image("manylinux_2_28", "s390x", "quay.io/pypa/manylinux_2_28_s390x", None),
"i686", Image("manylinux_2_28", "pypy_x86_64", "quay.io/pypa/manylinux_2_28_x86_64", None),
"aarch64", Image("manylinux_2_28", "pypy_aarch64", "quay.io/pypa/manylinux_2_28_aarch64", None),
"ppc64le", # musllinux_1_1 images
"s390x", Image("musllinux_1_1", "x86_64", "quay.io/pypa/musllinux_1_1_x86_64", None),
"pypy_x86_64", Image("musllinux_1_1", "i686", "quay.io/pypa/musllinux_1_1_i686", None),
"pypy_i686", Image("musllinux_1_1", "aarch64", "quay.io/pypa/musllinux_1_1_aarch64", None),
"pypy_aarch64", Image("musllinux_1_1", "ppc64le", "quay.io/pypa/musllinux_1_1_ppc64le", None),
], Image("musllinux_1_1", "s390x", "quay.io/pypa/musllinux_1_1_s390x", None),
),
# manylinux_2_31 images
PyPAImage("manylinux_2_31", ["armv7l"]),
# manylinux_2_34 images
PyPAImage(
"manylinux_2_34",
[
"x86_64",
"i686",
"aarch64",
"ppc64le",
"s390x",
"pypy_x86_64",
"pypy_i686",
"pypy_aarch64",
],
),
# manylinux_2_35 images
PyPAImage("manylinux_2_35", ["armv7l"]),
# manylinux_2_39 images
PyPAImage("manylinux_2_39", ["riscv64"]),
# musllinux_1_2 images # musllinux_1_2 images
PyPAImage( Image("musllinux_1_2", "x86_64", "quay.io/pypa/musllinux_1_2_x86_64", None),
"musllinux_1_2", ["x86_64", "i686", "aarch64", "ppc64le", "s390x", "armv7l", "riscv64"] Image("musllinux_1_2", "i686", "quay.io/pypa/musllinux_1_2_i686", None),
), Image("musllinux_1_2", "aarch64", "quay.io/pypa/musllinux_1_2_aarch64", None),
Image("musllinux_1_2", "ppc64le", "quay.io/pypa/musllinux_1_2_ppc64le", None),
Image("musllinux_1_2", "s390x", "quay.io/pypa/musllinux_1_2_s390x", None),
] ]
config = configparser.ConfigParser() config = configparser.ConfigParser()
@@ -104,21 +90,6 @@ for image in images:
for (name, info) in tags_dict.items() for (name, info) in tags_dict.items()
if info["manifest_digest"] == latest_tag["manifest_digest"] if info["manifest_digest"] == latest_tag["manifest_digest"]
) )
elif image.image_name.startswith("ghcr.io/"):
repository = image.image_name[8:]
response = requests.get(
"https://ghcr.io/token", params={"scope": f"repository:{repository}:pull"}
)
response.raise_for_status()
token = response.json()["token"]
response = requests.get(
f"https://ghcr.io/v2/{repository}/tags/list",
headers={"Authorization": f"Bearer {token}"},
)
response.raise_for_status()
ghcr_tags = [(Version(tag), tag) for tag in response.json()["tags"] if tag != "latest"]
ghcr_tags.sort(reverse=True)
tag_name = ghcr_tags[0][1]
else: else:
response = requests.get(f"https://hub.docker.com/v2/repositories/{image.image_name}/tags") response = requests.get(f"https://hub.docker.com/v2/repositories/{image.image_name}/tags")
response.raise_for_status() response.raise_for_status()
@@ -134,13 +105,10 @@ for image in images:
) )
tag_name = pinned_tag["name"] tag_name = pinned_tag["name"]
for platform in image.platforms: if not config.has_section(image.platform):
if not config.has_section(platform): config[image.platform] = {}
config[platform] = {}
suffix = "" config[image.platform][image.manylinux_version] = f"{image.image_name}:{tag_name}"
if image.use_platform_suffix:
suffix = f"_{platform.removeprefix('pypy_')}"
config[platform][image.manylinux_version] = f"{image.image_name}{suffix}:{tag_name}"
with open(RESOURCES / "pinned_docker_images.cfg", "w") as f: with open(RESOURCES / "pinned_docker_images.cfg", "w") as f:
config.write(f) config.write(f)
+4 -2
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import subprocess import subprocess
import sys import sys
@@ -17,7 +18,7 @@ except ImportError:
) )
def main() -> None: def main():
subprocess.run(["mkdocs", "build"], check=True) subprocess.run(["mkdocs", "build"], check=True)
hti = Html2Image(custom_flags=["--force-device-scale-factor=2"]) hti = Html2Image(custom_flags=["--force-device-scale-factor=2"])
@@ -41,7 +42,8 @@ def main() -> None:
) )
dest_path = Path("docs/data/how-it-works.png") dest_path = Path("docs/data/how-it-works.png")
dest_path.unlink(missing_ok=True) if dest_path.exists():
dest_path.unlink()
Path(screenshot).rename(dest_path) Path(screenshot).rename(dest_path)
-146
View File
@@ -1,146 +0,0 @@
#!/usr/bin/env python3
import dataclasses
import difflib
import logging
import tomllib
from pathlib import Path
from typing import Final
import click
import packaging.specifiers
import requests
import rich
from packaging.version import InvalidVersion, Version
from rich.logging import RichHandler
from rich.syntax import Syntax
log = logging.getLogger("cibw")
# Looking up the dir instead of using utils.resources_dir
# since we want to write to it.
DIR: Final[Path] = Path(__file__).parent.parent.resolve()
RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"
NODEJS_DIST: Final[str] = "https://nodejs.org/dist/"
NODEJS_INDEX: Final[str] = f"{NODEJS_DIST}index.json"
@dataclasses.dataclass(frozen=True, order=True)
class VersionTuple:
version: Version
version_string: str
def parse_nodejs_index() -> list[VersionTuple]:
versions: list[VersionTuple] = []
response = requests.get(NODEJS_INDEX)
response.raise_for_status()
versions_info = response.json()
for version_info in versions_info:
version_string = version_info.get("version", "???")
if not version_info.get("lts", False):
log.debug("Ignoring non LTS release %r", version_string)
continue
if "linux-x64" not in version_info.get("files", []):
log.warning(
"Ignoring release %r which does not include a linux-x64 binary", version_string
)
continue
try:
version = Version(version_string)
if version.is_devrelease:
log.info("Ignoring development release %r", str(version))
continue
if version.is_prerelease:
log.info("Ignoring pre-release %r", str(version))
continue
versions.append(VersionTuple(version, version_string))
except InvalidVersion:
log.warning("Ignoring release %r", version_string)
versions.sort(reverse=True)
return versions
@click.command()
@click.option("--force", is_flag=True)
@click.option(
"--level", default="INFO", type=click.Choice(["WARNING", "INFO", "DEBUG"], case_sensitive=False)
)
def update_nodejs(force: bool, level: str) -> None:
logging.basicConfig(
level="INFO",
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True, markup=True)],
)
log.setLevel(level)
toml_file_path = RESOURCES_DIR / "nodejs.toml"
original_toml = toml_file_path.read_text()
with toml_file_path.open("rb") as f:
nodejs_data = tomllib.load(f)
nodejs_data.pop("url")
major_versions = [VersionTuple(Version(key), key) for key in nodejs_data]
major_versions.sort(reverse=True)
versions = parse_nodejs_index()
# update existing versions, 1 per LTS
for major_version in major_versions:
current = Version(nodejs_data[major_version.version_string])
specifier = packaging.specifiers.SpecifierSet(
specifiers=f"=={major_version.version.major}.*"
)
for version in versions:
if specifier.contains(version.version) and version.version > current:
nodejs_data[major_version.version_string] = version.version_string
break
# check for a new major LTS to insert
if versions and versions[0].version.major > major_versions[0].version.major:
major_versions.insert(
0,
VersionTuple(Version(str(versions[0].version.major)), f"v{versions[0].version.major}"),
)
nodejs_data[major_versions[0].version_string] = versions[0].version_string
versions_toml = "\n".join(
f'{major_version.version_string} = "{nodejs_data[major_version.version_string]}"'
for major_version in major_versions
)
result_toml = f'url = "{NODEJS_DIST}"\n{versions_toml}\n'
rich.print() # spacer
if original_toml == result_toml:
rich.print("[green]Check complete, nodejs version unchanged.")
return
rich.print("nodejs version updated.")
rich.print("Changes:")
rich.print()
toml_relpath = toml_file_path.relative_to(DIR).as_posix()
diff_lines = difflib.unified_diff(
original_toml.splitlines(keepends=True),
result_toml.splitlines(keepends=True),
fromfile=toml_relpath,
tofile=toml_relpath,
)
rich.print(Syntax("".join(diff_lines), "diff", theme="ansi_light"))
rich.print()
if force:
toml_file_path.write_text(result_toml)
rich.print("[green]TOML file updated.")
else:
rich.print("[yellow]File left unchanged. Use --force flag to update.")
if __name__ == "__main__":
update_nodejs()
-59
View File
@@ -1,59 +0,0 @@
#!/usr/bin/env python3
import json
from cibuildwheel.extra import github_api_request
from cibuildwheel.util.python_build_standalone import (
PythonBuildStandaloneAsset,
PythonBuildStandaloneReleaseData,
)
from cibuildwheel.util.resources import PYTHON_BUILD_STANDALONE_RELEASES
def main() -> None:
"""
This script updates the vendored list of release assets to the latest
version of astral-sh/python-build-standalone.
"""
# Get the latest release tag from the GitHub API
latest_release = github_api_request("repos/astral-sh/python-build-standalone/releases/latest")
latest_tag = latest_release["tag_name"]
# Get the list of assets for the latest release
github_assets = github_api_request(
f"repos/astral-sh/python-build-standalone/releases/tags/{latest_tag}"
)["assets"]
assets = [
PythonBuildStandaloneAsset(name=ga["name"], url=ga["browser_download_url"])
for ga in github_assets
if ga["name"].endswith("install_only.tar.gz")
]
# Try to keep output order stable
assets = sorted(assets, key=lambda x: x["name"])
# Write the assets to the JSON file. One day, we might need to support
# multiple releases, but for now, we only support the latest one
json_file_contents = PythonBuildStandaloneReleaseData(
releases=[
{
"tag": latest_tag,
"assets": assets,
}
]
)
with PYTHON_BUILD_STANDALONE_RELEASES.open("w", encoding="utf-8") as f:
json.dump(json_file_contents, f, indent=2)
# Add a trailing newline, our pre-commit hook requires it
f.write("\n")
print(
f"Updated {PYTHON_BUILD_STANDALONE_RELEASES.name} with {len(assets)} assets for tag {latest_tag}"
)
if __name__ == "__main__":
main()
+58 -265
View File
@@ -1,15 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import copy
import difflib import difflib
import logging import logging
import operator
import re
import tomllib
from collections.abc import Mapping, MutableMapping from collections.abc import Mapping, MutableMapping
from pathlib import Path from pathlib import Path
from typing import Any, Final, Literal, TypedDict from typing import Any, Final, Literal, TypedDict, Union
from xml.etree import ElementTree as ET
import click import click
import requests import requests
@@ -19,8 +17,8 @@ from packaging.version import Version
from rich.logging import RichHandler from rich.logging import RichHandler
from rich.syntax import Syntax from rich.syntax import Syntax
from cibuildwheel.extra import dump_python_configurations, get_pyodide_xbuildenv_info from cibuildwheel._compat import tomllib
from cibuildwheel.platforms.android import android_triplet from cibuildwheel.extra import dump_python_configurations
log = logging.getLogger("cibw") log = logging.getLogger("cibw")
@@ -33,18 +31,26 @@ RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"
ArchStr = Literal["32", "64", "ARM64"] ArchStr = Literal["32", "64", "ARM64"]
class Config(TypedDict): class ConfigWinCP(TypedDict):
identifier: str identifier: str
version: str version: str
arch: str
class ConfigUrl(Config): class ConfigWinPP(TypedDict):
identifier: str
version: str
arch: str
url: str url: str
class ConfigPyodide(Config): class ConfigMacOS(TypedDict):
default_pyodide_version: str identifier: str
node_version: str version: str
url: str
AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigMacOS]
# The following set of "Versions" classes allow the initial call to the APIs to # The following set of "Versions" classes allow the initial call to the APIs to
@@ -52,7 +58,7 @@ class ConfigPyodide(Config):
class WindowsVersions: class WindowsVersions:
def __init__(self, arch_str: ArchStr, free_threaded: bool) -> None: def __init__(self, arch_str: ArchStr) -> None:
response = requests.get("https://api.nuget.org/v3/index.json") response = requests.get("https://api.nuget.org/v3/index.json")
response.raise_for_status() response.raise_for_status()
api_info = response.json() api_info = response.json()
@@ -66,11 +72,7 @@ class WindowsVersions:
self.arch_str = arch_str self.arch_str = arch_str
self.arch = ARCH_DICT[arch_str] self.arch = ARCH_DICT[arch_str]
self.free_threaded = free_threaded
package = PACKAGE_DICT[arch_str] package = PACKAGE_DICT[arch_str]
if free_threaded:
package = f"{package}-freethreaded"
response = requests.get(f"{endpoint}{package}/index.json") response = requests.get(f"{endpoint}{package}/index.json")
response.raise_for_status() response.raise_for_status()
@@ -78,7 +80,7 @@ class WindowsVersions:
self.version_dict = {Version(v): v for v in cp_info["versions"]} self.version_dict = {Version(v): v for v in cp_info["versions"]}
def update_version_windows(self, spec: Specifier) -> Config | None: def update_version_windows(self, spec: Specifier) -> ConfigWinCP | None:
# Specifier.filter selects all non pre-releases that match the spec, # Specifier.filter selects all non pre-releases that match the spec,
# unless there are only pre-releases, then it selects pre-releases # unless there are only pre-releases, then it selects pre-releases
# instead (like pip) # instead (like pip)
@@ -90,82 +92,12 @@ class WindowsVersions:
if not versions: if not versions:
return None return None
flags = "t" if self.free_threaded else ""
version = versions[0] version = versions[0]
identifier = f"cp{version.major}{version.minor}{flags}-{self.arch}" identifier = f"cp{version.major}{version.minor}-{self.arch}"
return Config( return ConfigWinCP(
identifier=identifier, identifier=identifier,
version=self.version_dict[version], version=self.version_dict[version],
) arch=self.arch_str,
class GraalPyVersions:
def __init__(self) -> None:
response = requests.get("https://api.github.com/repos/oracle/graalpython/releases")
response.raise_for_status()
releases = response.json()
gp_version_re = re.compile(r"-(\d+\.\d+\.\d+)$")
cp_version_re = re.compile(r"Python (\d+\.\d+(?:\.\d+)?)")
for release in releases:
m = gp_version_re.search(release["tag_name"])
if m:
release["graalpy_version"] = Version(m.group(1))
m = cp_version_re.search(release["body"])
if m:
release["python_version"] = Version(m.group(1))
self.releases = [r for r in releases if "graalpy_version" in r and "python_version" in r]
def update_version(self, identifier: str, spec: Specifier) -> ConfigUrl:
if "x86_64" in identifier or "amd64" in identifier:
arch = "x86_64"
elif "arm64" in identifier or "aarch64" in identifier:
arch = "aarch64"
else:
msg = f"{identifier} not supported yet on GraalPy"
raise RuntimeError(msg)
gpspec_str = identifier.split("-")[0].split("_")[1]
if "." not in gpspec_str and len(gpspec_str) == 3:
gpspec_str = gpspec_str[:2] + "." + gpspec_str[-1]
gpspec = Specifier(f"=={gpspec_str}.*")
releases = [r for r in self.releases if spec.contains(r["python_version"])]
releases = [r for r in self.releases if gpspec.contains(r["graalpy_version"])]
releases = sorted(releases, key=lambda r: r["graalpy_version"])
if not releases:
msg = f"GraalPy {arch} not found for {spec}!"
raise RuntimeError(msg)
release = releases[-1]
version = release["python_version"]
gpversion = release["graalpy_version"]
if "macosx" in identifier:
arch = "x86_64" if "x86_64" in identifier else "arm64"
platform = "macos"
elif "win" in identifier:
arch = "aarch64" if "arm64" in identifier else "x86_64"
platform = "windows"
else:
msg = "GraalPy provides downloads for macOS and Windows and is included for manylinux"
raise RuntimeError(msg)
arch = "amd64" if arch == "x86_64" else "aarch64"
ext = "zip" if "win" in identifier else "tar.gz"
(url,) = (
rf["browser_download_url"]
for rf in release["assets"]
if rf["name"].endswith(f"{platform}-{arch}.{ext}")
and rf["name"].startswith(f"graalpy-{gpversion.major}")
)
return ConfigUrl(
identifier=identifier,
version=f"{version.major}.{version.minor}",
url=url,
) )
@@ -194,9 +126,9 @@ class PyPyVersions:
] ]
return urls[0] if urls else "" return urls[0] if urls else ""
def update_version_windows(self, spec: Specifier) -> ConfigUrl: def update_version_windows(self, spec: Specifier) -> ConfigWinCP:
releases = [r for r in self.releases if spec.contains(r["python_version"])] releases = [r for r in self.releases if spec.contains(r["python_version"])]
releases = sorted(releases, key=operator.itemgetter("pypy_version")) releases = sorted(releases, key=lambda r: r["pypy_version"])
releases = [r for r in releases if self.get_arch_file(r)] releases = [r for r in releases if self.get_arch_file(r)]
if not releases: if not releases:
@@ -210,19 +142,20 @@ class PyPyVersions:
identifier = f"pp{version.major}{version.minor}-{version_arch}" identifier = f"pp{version.major}{version.minor}-{version_arch}"
url = self.get_arch_file(release) url = self.get_arch_file(release)
return ConfigUrl( return ConfigWinPP(
identifier=identifier, identifier=identifier,
version=f"{version.major}.{version.minor}", version=f"{version.major}.{version.minor}",
arch=self.arch,
url=url, url=url,
) )
def update_version_macos(self, spec: Specifier) -> ConfigUrl: def update_version_macos(self, spec: Specifier) -> ConfigMacOS:
if self.arch not in {"64", "ARM64"}: if self.arch not in {"64", "ARM64"}:
msg = f"'{self.arch}' arch not supported yet on macOS" msg = f"'{self.arch}' arch not supported yet on macOS"
raise RuntimeError(msg) raise RuntimeError(msg)
releases = [r for r in self.releases if spec.contains(r["python_version"])] releases = [r for r in self.releases if spec.contains(r["python_version"])]
releases = sorted(releases, key=operator.itemgetter("pypy_version")) releases = sorted(releases, key=lambda r: r["pypy_version"])
if not releases: if not releases:
msg = f"PyPy macOS {self.arch} not found for {spec}!" msg = f"PyPy macOS {self.arch} not found for {spec}!"
@@ -240,7 +173,7 @@ class PyPyVersions:
if "" in rf["platform"] == "darwin" and rf["arch"] == arch if "" in rf["platform"] == "darwin" and rf["arch"] == arch
) )
return ConfigUrl( return ConfigMacOS(
identifier=identifier, identifier=identifier,
version=f"{version.major}.{version.minor}", version=f"{version.major}.{version.minor}",
url=url, url=url,
@@ -258,21 +191,22 @@ class CPythonVersions:
self.versions_dict: dict[Version, int] = {} self.versions_dict: dict[Version, int] = {}
for release in releases_info: for release in releases_info:
# Skip the pymanager releases # Removing the prefix, Python 3.9 would use: release["name"].removeprefix("Python ")
if not release["slug"].startswith("python"): version = Version(release["name"][7:])
continue
# Removing the prefix
version = Version(release["name"].removeprefix("Python "))
uri = int(release["resource_uri"].rstrip("/").split("/")[-1]) uri = int(release["resource_uri"].rstrip("/").split("/")[-1])
self.versions_dict[version] = uri self.versions_dict[version] = uri
def update_version(self, identifier: str, spec: Specifier, file_ident: str) -> ConfigUrl | None: def update_version_macos(
self, identifier: str, version: Version, spec: Specifier
) -> ConfigMacOS | None:
# see note above on Specifier.filter # see note above on Specifier.filter
unsorted_versions = spec.filter(self.versions_dict) unsorted_versions = spec.filter(self.versions_dict)
sorted_versions = sorted(unsorted_versions, reverse=True) sorted_versions = sorted(unsorted_versions, reverse=True)
macver = "x10.9" if version <= Version("3.8.9999") else "11"
file_ident = f"macos{macver}.pkg"
for new_version in sorted_versions: for new_version in sorted_versions:
# Find the first patch version that contains the requested file # Find the first patch version that contains the requested file
uri = self.versions_dict[new_version] uri = self.versions_dict[new_version]
@@ -284,7 +218,7 @@ class CPythonVersions:
urls = [rf["url"] for rf in file_info if file_ident in rf["url"]] urls = [rf["url"] for rf in file_info if file_ident in rf["url"]]
if urls: if urls:
return ConfigUrl( return ConfigMacOS(
identifier=identifier, identifier=identifier,
version=f"{new_version.major}.{new_version.minor}", version=f"{new_version.major}.{new_version.minor}",
url=urls[0], url=urls[0],
@@ -292,120 +226,6 @@ class CPythonVersions:
return None return None
def update_version_macos(
self, identifier: str, version: Version, spec: Specifier
) -> ConfigUrl | None:
macver = "x10.9" if version <= Version("3.8.9999") else "11"
return self.update_version(identifier, spec, f"macos{macver}.pkg")
def update_version_android(self, identifier: str, spec: Specifier) -> ConfigUrl | None:
return self.update_version(identifier, spec, android_triplet(identifier))
class MavenVersions:
MAVEN_URL = "https://repo.maven.apache.org/maven2/com/chaquo/python/python"
def __init__(self) -> None:
response = requests.get(f"{self.MAVEN_URL}/maven-metadata.xml")
response.raise_for_status()
root = ET.fromstring(response.text)
self.versions: list[Version] = []
for version_elem in root.findall("./versioning/versions/version"):
version_str = version_elem.text
assert isinstance(version_str, str), version_str
self.versions.append(Version(version_str))
def update_version_android(self, identifier: str, spec: Specifier) -> ConfigUrl | None:
sorted_versions = sorted(spec.filter(self.versions), reverse=True)
# Return a config using the highest version for the given specifier.
if sorted_versions:
max_version = sorted_versions[0]
triplet = android_triplet(identifier)
return ConfigUrl(
identifier=identifier,
version=f"{max_version.major}.{max_version.minor}",
url=f"{self.MAVEN_URL}/{max_version}/python-{max_version}-{triplet}.tar.gz",
)
else:
return None
class CPythonIOSVersions:
def __init__(self) -> None:
response = requests.get(
"https://api.github.com/repos/beeware/Python-Apple-support/releases",
headers={
"Accept": "application/vnd.github+json",
"X-Github-Api-Version": "2022-11-28",
},
)
response.raise_for_status()
releases_info = response.json()
self.versions_dict: dict[Version, dict[int, str]] = {}
# Each release has a name like "3.13-b4"
for release in releases_info:
py_version, build = release["name"].split("-")
version = Version(py_version)
self.versions_dict.setdefault(version, {})
# There are several release assets associated with each release;
# The name of the asset will be something like
# "Python-3.11-iOS-support.b4.tar.gz". Store all builds that are
# "-iOS-support" builds, retaining the download URL.
for asset in release["assets"]:
filename, build, _, _ = asset["name"].rsplit(".", 3)
if filename.endswith("-iOS-support"):
self.versions_dict[version][int(build[1:])] = asset["browser_download_url"]
def update_version_ios(self, identifier: str, version: Version) -> ConfigUrl | None:
# Return a config using the highest build number for the given version.
urls = [url for _, url in sorted(self.versions_dict.get(version, {}).items())]
if urls:
return ConfigUrl(
identifier=identifier,
version=str(version),
url=urls[-1],
)
return None
class PyodideVersions:
def __init__(self) -> None:
xbuildenv_info = get_pyodide_xbuildenv_info()
self.releases = xbuildenv_info["releases"]
def update_version_pyodide(
self, identifier: str, version: Version, spec: Specifier, node_version: str
) -> ConfigPyodide | None:
# get releases that match the python version
releases = [
r for r in self.releases.values() if spec.contains(Version(r["python_version"]))
]
# sort by version, latest first
releases.sort(key=lambda r: Version(r["version"]), reverse=True)
if not releases:
msg = f"Pyodide not found for {spec}!"
raise ValueError(msg)
final_releases = [r for r in releases if not Version(r["version"]).is_prerelease]
# prefer a final release if available, otherwise use the latest
# pre-release
release = final_releases[0] if final_releases else releases[0]
return ConfigPyodide(
identifier=identifier,
version=str(version),
default_pyodide_version=release["version"],
node_version=node_version,
)
# This is a universal interface to all the above Versions classes. Given an # This is a universal interface to all the above Versions classes. Given an
# identifier, it updates a config dict. # identifier, it updates a config dict.
@@ -413,77 +233,48 @@ class PyodideVersions:
class AllVersions: class AllVersions:
def __init__(self) -> None: def __init__(self) -> None:
self.windows_32 = WindowsVersions("32", False) self.windows_32 = WindowsVersions("32")
self.windows_t_32 = WindowsVersions("32", True) self.windows_64 = WindowsVersions("64")
self.windows_64 = WindowsVersions("64", False) self.windows_arm64 = WindowsVersions("ARM64")
self.windows_t_64 = WindowsVersions("64", True)
self.windows_arm64 = WindowsVersions("ARM64", False)
self.windows_t_arm64 = WindowsVersions("ARM64", True)
self.windows_pypy_64 = PyPyVersions("64") self.windows_pypy_64 = PyPyVersions("64")
self.cpython = CPythonVersions() self.macos_cpython = CPythonVersions()
self.macos_pypy = PyPyVersions("64") self.macos_pypy = PyPyVersions("64")
self.macos_pypy_arm64 = PyPyVersions("ARM64") self.macos_pypy_arm64 = PyPyVersions("ARM64")
self.maven = MavenVersions()
self.ios_cpython = CPythonIOSVersions()
self.graalpy = GraalPyVersions()
self.pyodide = PyodideVersions()
def update_config(self, config: MutableMapping[str, str]) -> None: def update_config(self, config: MutableMapping[str, str]) -> None:
identifier = config["identifier"] identifier = config["identifier"]
version = Version(config["version"]) version = Version(config["version"])
spec = Specifier(f"=={version.major}.{version.minor}.*") spec = Specifier(f"=={version.major}.{version.minor}.*")
log.info("Reading in %r -> %s @ %s", str(identifier), spec, version) log.info("Reading in %r -> %s @ %s", str(identifier), spec, version)
config_update: Config | None = None orig_config = copy.copy(config)
config_update: AnyConfig | None = None
# We need to use ** in update due to MyPy (probably a bug) # We need to use ** in update due to MyPy (probably a bug)
if "macosx" in identifier: if "macosx" in identifier:
if identifier.startswith("cp"): if identifier.startswith("cp"):
config_update = self.cpython.update_version_macos(identifier, version, spec) config_update = self.macos_cpython.update_version_macos(identifier, version, spec)
elif identifier.startswith("pp"): elif identifier.startswith("pp"):
if "macosx_x86_64" in identifier: if "macosx_x86_64" in identifier:
config_update = self.macos_pypy.update_version_macos(spec) config_update = self.macos_pypy.update_version_macos(spec)
elif "macosx_arm64" in identifier: elif "macosx_arm64" in identifier:
config_update = self.macos_pypy_arm64.update_version_macos(spec) config_update = self.macos_pypy_arm64.update_version_macos(spec)
elif identifier.startswith("gp"): elif "win32" in identifier:
config_update = self.graalpy.update_version(identifier, spec) if identifier.startswith("cp"):
elif "t-win32" in identifier and identifier.startswith("cp"): config_update = self.windows_32.update_version_windows(spec)
config_update = self.windows_t_32.update_version_windows(spec)
elif "win32" in identifier and identifier.startswith("cp"):
config_update = self.windows_32.update_version_windows(spec)
elif "t-win_amd64" in identifier and identifier.startswith("cp"):
config_update = self.windows_t_64.update_version_windows(spec)
elif "win_amd64" in identifier: elif "win_amd64" in identifier:
if identifier.startswith("cp"): if identifier.startswith("cp"):
config_update = self.windows_64.update_version_windows(spec) config_update = self.windows_64.update_version_windows(spec)
elif identifier.startswith("pp"): elif identifier.startswith("pp"):
config_update = self.windows_pypy_64.update_version_windows(spec) config_update = self.windows_pypy_64.update_version_windows(spec)
elif identifier.startswith("gp"):
config_update = self.graalpy.update_version(identifier, spec)
elif "t-win_arm64" in identifier and identifier.startswith("cp"):
config_update = self.windows_t_arm64.update_version_windows(spec)
elif "win_arm64" in identifier and identifier.startswith("cp"): elif "win_arm64" in identifier and identifier.startswith("cp"):
config_update = self.windows_arm64.update_version_windows(spec) config_update = self.windows_arm64.update_version_windows(spec)
elif "android" in identifier:
# Python 3.13 is released by Chaquopy on Maven Central.
# Python 3.14 and newer have official releases on python.org.
versions = self.maven if identifier.startswith("cp313") else self.cpython
config_update = versions.update_version_android(identifier, spec)
elif "ios" in identifier:
config_update = self.ios_cpython.update_version_ios(identifier, version)
elif "pyodide" in identifier:
config_update = self.pyodide.update_version_pyodide(
identifier, version, spec, config["node_version"]
)
assert config_update is not None, f"{identifier} not found!" assert config_update is not None, f"{identifier} not found!"
if config_update != config: config.update(**config_update)
log.info(" Updated %s to %s", config, config_update)
config.clear() if config != orig_config:
config.update(**config_update) log.info(" Updated %s to %s", orig_config, config)
@click.command() @click.command()
@@ -507,9 +298,11 @@ def update_pythons(force: bool, level: str) -> None:
with toml_file_path.open("rb") as f: with toml_file_path.open("rb") as f:
configs = tomllib.load(f) configs = tomllib.load(f)
for platform in ["windows", "macos", "android", "ios", "pyodide"]: for config in configs["windows"]["python_configurations"]:
for config in configs[platform]["python_configurations"]: all_versions.update_config(config)
all_versions.update_config(config)
for config in configs["macos"]["python_configurations"]:
all_versions.update_config(config)
result_toml = dump_python_configurations(configs) result_toml = dump_python_configurations(configs)
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
from __future__ import annotations
import re
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent / ".."
CHANGELOG_FILE = PROJECT_ROOT / "docs" / "changelog.md"
README_FILE = PROJECT_ROOT / "README.md"
# https://regexr.com/622ds
FIRST_5_CHANGELOG_ENTRIES_REGEX = re.compile(r"""(^###.*?(?=###)){5}""", re.DOTALL | re.MULTILINE)
# https://regexr.com/622e5
README_CHANGELOG_SECTION = re.compile(
r"""(?<=<!-- START bin\/update_readme_changelog.py -->\n).*(?=<!-- END bin\/update_readme_changelog.py -->)""",
re.DOTALL,
)
def main():
changelog_text = CHANGELOG_FILE.read_text()
readme_text = README_FILE.read_text()
mini_changelog_match = FIRST_5_CHANGELOG_ENTRIES_REGEX.search(changelog_text)
assert mini_changelog_match, "Failed to find the first few changelog entries"
mini_changelog = "\n".join(
[
"",
"<!-- this section was generated by bin/update_readme_changelog.py -- do not edit manually -->",
"",
mini_changelog_match.group(0).strip(),
"",
"",
]
)
if not re.search(README_CHANGELOG_SECTION, readme_text):
sys.exit("Changelog section not found in README")
readme_text = re.sub(README_CHANGELOG_SECTION, mini_changelog, readme_text)
README_FILE.write_text(readme_text)
if __name__ == "__main__":
main()
+44 -44
View File
@@ -1,20 +1,21 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import dataclasses
import difflib import difflib
import logging import logging
import tomllib import subprocess
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Final from typing import Final
import click import click
import rich import rich
from packaging.version import Version from packaging.version import InvalidVersion, Version
from rich.logging import RichHandler from rich.logging import RichHandler
from rich.syntax import Syntax from rich.syntax import Syntax
from cibuildwheel.extra import github_api_request from cibuildwheel._compat import tomllib
log = logging.getLogger("cibw") log = logging.getLogger("cibw")
@@ -24,33 +25,43 @@ DIR: Final[Path] = Path(__file__).parent.parent.resolve()
RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources" RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"
GET_VIRTUALENV_GITHUB: Final[str] = "https://github.com/pypa/get-virtualenv" GET_VIRTUALENV_GITHUB: Final[str] = "https://github.com/pypa/get-virtualenv"
GET_VIRTUALENV_URL_TEMPLATE: Final[str] = ( GET_VIRTUALENV_URL_TEMPLATE: Final[
f"{GET_VIRTUALENV_GITHUB}/blob/{{version}}/public/virtualenv.pyz?raw=true" str
) ] = f"{GET_VIRTUALENV_GITHUB}/blob/{{version}}/public/virtualenv.pyz?raw=true"
@dataclasses.dataclass(frozen=True, order=True) @dataclass(frozen=True, order=True)
class VersionTuple: class VersionTuple:
name: str
download_url: str
version: Version version: Version
version_string: str
def get_latest_virtualenv_release() -> VersionTuple: def git_ls_remote_versions(url) -> list[VersionTuple]:
response = github_api_request("repos/pypa/get-virtualenv/releases/latest") versions: list[VersionTuple] = []
tag_name = response["tag_name"] tags = subprocess.run(
["git", "ls-remote", "--tags", url], check=True, text=True, capture_output=True
asset = next( ).stdout.splitlines()
(asset for asset in response["assets"] if asset["name"] == "virtualenv.pyz"), for tag in tags:
None, _, ref = tag.split()
) assert ref.startswith("refs/tags/")
if not asset: version_string = ref[10:]
msg = "No asset named 'virtualenv.pyz' found in the latest release of get-virtualenv." try:
raise RuntimeError(msg) version = Version(version_string)
if version.is_devrelease:
return VersionTuple( log.info("Ignoring development release %r", str(version))
version=Version(tag_name), name=tag_name, download_url=asset["browser_download_url"] continue
) if version.is_prerelease:
log.info("Ignoring pre-release %r", str(version))
continue
# Do not upgrade past 20.22.0 to keep python 3.6 compat
if version >= Version("20.22.0"):
log.info("Ignoring %r which is not compatible with python 3.6", str(version))
continue
versions.append(VersionTuple(version, version_string))
except InvalidVersion:
log.warning("Ignoring ref %r", ref)
versions.sort(reverse=True)
return versions
@click.command() @click.command()
@@ -71,26 +82,15 @@ def update_virtualenv(force: bool, level: str) -> None:
original_toml = toml_file_path.read_text() original_toml = toml_file_path.read_text()
with toml_file_path.open("rb") as f: with toml_file_path.open("rb") as f:
configurations = tomllib.load(f) loaded_file = tomllib.load(f)
default = configurations.pop("default") version = str(loaded_file["version"])
local_version = str(default["version"]) versions = git_ls_remote_versions(GET_VIRTUALENV_GITHUB)
if versions[0].version > Version(version):
version = versions[0].version_string
latest_release = get_latest_virtualenv_release() result_toml = (
f'version = "{version}"\n'
if latest_release.version > Version(local_version): f'url = "{GET_VIRTUALENV_URL_TEMPLATE.format(version=version)}"\n'
version = latest_release.name
url = latest_release.download_url
else:
version = local_version
url = default["url"]
configurations["default"] = {
"version": version,
"url": url,
}
result_toml = "".join(
f'{key} = {{ version = "{value["version"]}", url = "{value["url"]}" }}\n'
for key, value in configurations.items()
) )
rich.print() # spacer rich.print() # spacer
+3 -1
View File
@@ -1 +1,3 @@
__version__ = "3.3.1" from __future__ import annotations
__version__ = "2.16.5"
+139 -310
View File
@@ -1,110 +1,58 @@
from __future__ import annotations
import argparse import argparse
import contextlib
import dataclasses
import functools
import os import os
import shutil import shutil
import sys import sys
import tarfile import tarfile
import textwrap import textwrap
import traceback
import typing import typing
from collections.abc import Generator, Iterable, Sequence from collections.abc import Iterable, Sequence, Set
from pathlib import Path from pathlib import Path
from tempfile import mkdtemp from tempfile import mkdtemp
from typing import Any, Literal, TextIO from typing import Protocol
import cibuildwheel import cibuildwheel
import cibuildwheel.linux
import cibuildwheel.macos
import cibuildwheel.util import cibuildwheel.util
from cibuildwheel import errors import cibuildwheel.windows
from cibuildwheel._compat.typing import assert_never
from cibuildwheel.architecture import Architecture, allowed_architectures_check from cibuildwheel.architecture import Architecture, allowed_architectures_check
from cibuildwheel.ci import CIProvider, detect_ci_provider, fix_ansi_codes_for_github_actions
from cibuildwheel.logger import log from cibuildwheel.logger import log
from cibuildwheel.options import CommandLineArguments, Options, compute_options from cibuildwheel.options import CommandLineArguments, Options, compute_options
from cibuildwheel.platforms import ALL_PLATFORM_MODULES, get_build_identifiers, native_platform from cibuildwheel.typing import PLATFORMS, GenericPythonConfiguration, PlatformName
from cibuildwheel.selector import BuildSelector, EnableGroup, selector_matches from cibuildwheel.util import (
from cibuildwheel.typing import PLATFORMS, PlatformName CIBW_CACHE_PATH,
from cibuildwheel.util.file import CIBW_CACHE_PATH, ensure_cache_sentinel BuildSelector,
from cibuildwheel.util.helpers import strtobool CIProvider,
from cibuildwheel.util.resources import read_all_configs Unbuffered,
chdir,
detect_ci_provider,
@dataclasses.dataclass fix_ansi_codes_for_github_actions,
class GlobalOptions: )
print_traceback_on_error: bool = True # decides what happens when errors are hit.
@dataclasses.dataclass(frozen=True)
class FileReport:
name: str
size: str
# Taken from https://stackoverflow.com/a/107717
class Unbuffered:
def __init__(self, stream: TextIO) -> None:
self.stream = stream
def write(self, data: str) -> None:
self.stream.write(data)
self.stream.flush()
def writelines(self, data: Iterable[str]) -> None:
self.stream.writelines(data)
self.stream.flush()
def __getattr__(self, attr: str) -> Any:
return getattr(self.stream, attr)
def main() -> None: def main() -> None:
global_options = GlobalOptions() parser = argparse.ArgumentParser(
try:
main_inner(global_options)
except errors.FatalError as e:
message = e.args[0]
if log.step_active:
log.step_end_with_error(message)
else:
log.error(message)
if global_options.print_traceback_on_error:
traceback.print_exc(file=sys.stderr)
sys.exit(e.return_code)
def main_inner(global_options: GlobalOptions) -> None:
"""
`main_inner` is the same as `main`, but it raises FatalError exceptions
rather than exiting directly.
"""
# Default in 3.15+, only needed on 3.14
if sys.version_info >= (3, 14):
arg_parser = functools.partial(argparse.ArgumentParser, suggest_on_error=True)
else:
arg_parser = argparse.ArgumentParser
parser = arg_parser(
description="Build wheels for all the platforms.", description="Build wheels for all the platforms.",
epilog=""" epilog="""
Most options are supplied via environment variables or in Most options are supplied via environment variables or in
--config-file (pyproject.toml usually). See --config-file (pyproject.toml usually). See
https://github.com/pypa/cibuildwheel#options for info. https://github.com/pypa/cibuildwheel#options for info.
""", """,
allow_abbrev=False,
) )
parser.add_argument( parser.add_argument(
"--platform", "--platform",
choices=["auto", "linux", "macos", "windows", "pyodide", "android", "ios"], choices=["auto", "linux", "macos", "windows"],
default=None, default=None,
help=""" help="""
Platform to build for. Use this option to override the auto-detected Platform to build for. Use this option to override the
platform. Specifying "macos" or "windows" only works on that auto-detected platform or to run cibuildwheel on your development
operating system. "linux" works on any desktop OS, as long as machine. Specifying "macos" or "windows" only works on that
Docker/Podman is installed. "pyodide" and "android" only work on operating system, but "linux" works on all three, as long as
Linux and macOS. "ios" only works on macOS. Default: auto. Docker/Podman is installed. Default: auto.
""", """,
) )
@@ -122,22 +70,9 @@ def main_inner(global_options: GlobalOptions) -> None:
""", """,
) )
enable_groups_str = ", ".join(g.value for g in EnableGroup)
parser.add_argument(
"--enable",
action="append",
default=[],
metavar="GROUP",
help=f"""
Enable an additional category of builds. Use multiple times to select multiple groups. Choices: {enable_groups_str}.
""",
)
parser.add_argument( parser.add_argument(
"--only", "--only",
default=None, default=None,
choices=[v["identifier"] for vv in read_all_configs().values() for v in vv],
metavar="IDENTIFIER",
help=""" help="""
Force a single wheel build when given an identifier. Overrides Force a single wheel build when given an identifier. Overrides
CIBW_BUILD/CIBW_SKIP. --platform and --arch cannot be specified CIBW_BUILD/CIBW_SKIP. --platform and --arch cannot be specified
@@ -165,7 +100,7 @@ def main_inner(global_options: GlobalOptions) -> None:
parser.add_argument( parser.add_argument(
"package_dir", "package_dir",
metavar="PACKAGE", metavar="PACKAGE",
default=Path(), default=Path("."),
type=Path, type=Path,
nargs="?", nargs="?",
help=""" help="""
@@ -185,12 +120,6 @@ def main_inner(global_options: GlobalOptions) -> None:
help="Print the build identifiers matched by the current invocation and exit.", help="Print the build identifiers matched by the current invocation and exit.",
) )
parser.add_argument(
"--clean-cache",
action="store_true",
help="Clear the cibuildwheel cache and exit.",
)
parser.add_argument( parser.add_argument(
"--allow-empty", "--allow-empty",
action="store_true", action="store_true",
@@ -198,58 +127,13 @@ def main_inner(global_options: GlobalOptions) -> None:
) )
parser.add_argument( parser.add_argument(
"--debug-traceback", "--prerelease-pythons",
action="store_true", action="store_true",
default=strtobool(os.environ.get("CIBW_DEBUG_TRACEBACK", "0")), help="Enable pre-release Python versions if available.",
help="Print a full traceback for all errors",
) )
args = CommandLineArguments(**vars(parser.parse_args())) args = CommandLineArguments(**vars(parser.parse_args()))
global_options.print_traceback_on_error = args.debug_traceback
if args.clean_cache:
if not CIBW_CACHE_PATH.exists():
print(f"Cache directory does not exist: {CIBW_CACHE_PATH}")
sys.exit(0)
sentinel_file = CIBW_CACHE_PATH / "CACHEDIR.TAG"
if not sentinel_file.exists():
print(
f"Error: {CIBW_CACHE_PATH} does not appear to be a cibuildwheel cache directory.",
"Only directories with a CACHEDIR.TAG sentinel file can be cleaned.",
sep="\n",
file=sys.stderr,
)
sys.exit(1)
# Verify signature to ensure it's a proper cache dir
# See https://bford.info/cachedir/ for more
try:
sentinel_content = sentinel_file.read_text(encoding="utf-8")
except OSError as e:
print(f"Error reading cache directory tag: {e}", file=sys.stderr)
sys.exit(1)
if not sentinel_content.startswith("Signature: 8a477f597d28d172789f06886806bc55"):
print(
f"Error: {sentinel_file} does not contain a valid cache directory signature.",
"For safety, only properly signed cache directories can be cleaned.",
sep="\n",
file=sys.stderr,
)
sys.exit(1)
print(f"Clearing cache directory: {CIBW_CACHE_PATH}")
try:
shutil.rmtree(CIBW_CACHE_PATH)
print("Cache cleared successfully.")
except OSError as e:
print(f"Error clearing cache: {e}", file=sys.stderr)
sys.exit(1)
sys.exit(0)
args.package_dir = args.package_dir.resolve() args.package_dir = args.package_dir.resolve()
# This are always relative to the base directory, even in SDist builds # This are always relative to the base directory, even in SDist builds
@@ -276,7 +160,7 @@ def main_inner(global_options: GlobalOptions) -> None:
# This is now the new package dir # This is now the new package dir
args.package_dir = project_dir.resolve() args.package_dir = project_dir.resolve()
with contextlib.chdir(project_dir): with chdir(project_dir):
build_in_directory(args) build_in_directory(args)
finally: finally:
# avoid https://github.com/python/cpython/issues/86962 by performing # avoid https://github.com/python/cpython/issues/86962 by performing
@@ -293,44 +177,95 @@ def _compute_platform_only(only: str) -> PlatformName:
return "macos" return "macos"
if "win_" in only or "win32" in only: if "win_" in only or "win32" in only:
return "windows" return "windows"
if "pyodide_" in only: print(
return "pyodide" f"Invalid --only='{only}', must be a build selector with a known platform",
if "android_" in only: file=sys.stderr,
return "android" )
if "ios_" in only: sys.exit(2)
return "ios"
msg = f"Invalid --only='{only}', must be a build selector with a known platform"
raise errors.ConfigurationError(msg) def _compute_platform_ci() -> PlatformName:
if detect_ci_provider() is None:
print(
textwrap.dedent(
"""
cibuildwheel: Unable to detect platform. cibuildwheel should run on your CI server;
Travis CI, AppVeyor, Azure Pipelines, GitHub Actions, CircleCI, Gitlab, and Cirrus CI
are supported. You can run on your development machine or other CI providers
using the --platform argument. Check --help output for more information.
"""
),
file=sys.stderr,
)
sys.exit(2)
if sys.platform.startswith("linux"):
return "linux"
elif sys.platform == "darwin":
return "macos"
elif sys.platform == "win32":
return "windows"
else:
print(
'cibuildwheel: Unable to detect platform from "sys.platform" in a CI environment. You can run '
"cibuildwheel using the --platform argument. Check --help output for more information.",
file=sys.stderr,
)
sys.exit(2)
def _compute_platform(args: CommandLineArguments) -> PlatformName: def _compute_platform(args: CommandLineArguments) -> PlatformName:
platform_option_value = args.platform or os.environ.get("CIBW_PLATFORM", "") or "auto" platform_option_value = args.platform or os.environ.get("CIBW_PLATFORM", "auto")
if args.only and args.platform is not None: if args.only and args.platform is not None:
msg = "--platform cannot be specified with --only, it is computed from --only" print(
raise errors.ConfigurationError(msg) "--platform cannot be specified with --only, it is computed from --only",
file=sys.stderr,
)
sys.exit(2)
if args.only and args.archs is not None: if args.only and args.archs is not None:
msg = "--arch cannot be specified with --only, it is computed from --only" print(
raise errors.ConfigurationError(msg) "--arch cannot be specified with --only, it is computed from --only",
file=sys.stderr,
)
sys.exit(2)
if platform_option_value not in PLATFORMS | {"auto"}: if platform_option_value not in PLATFORMS | {"auto"}:
msg = f"Unsupported platform: {platform_option_value}" print(f"cibuildwheel: Unsupported platform: {platform_option_value}", file=sys.stderr)
raise errors.ConfigurationError(msg) sys.exit(2)
if args.only: if args.only:
return _compute_platform_only(args.only) return _compute_platform_only(args.only)
elif platform_option_value != "auto": elif platform_option_value != "auto":
return typing.cast("PlatformName", platform_option_value) return typing.cast(PlatformName, platform_option_value)
return native_platform() return _compute_platform_ci()
class PlatformModule(Protocol):
# note that as per PEP544, the self argument is ignored when the protocol
# is applied to a module
def get_python_configurations(
self, build_selector: BuildSelector, architectures: Set[Architecture]
) -> Sequence[GenericPythonConfiguration]:
...
def build(self, options: Options, tmp_path: Path) -> None:
...
# pylint: disable-next=inconsistent-return-statements
def get_platform_module(platform: PlatformName) -> PlatformModule:
if platform == "linux":
return cibuildwheel.linux
if platform == "windows":
return cibuildwheel.windows
if platform == "macos":
return cibuildwheel.macos
assert_never(platform)
def build_in_directory(args: CommandLineArguments) -> None: def build_in_directory(args: CommandLineArguments) -> None:
platform: PlatformName = _compute_platform(args) platform: PlatformName = _compute_platform(args)
if platform == "pyodide" and sys.platform == "win32":
msg = "Building for pyodide is not supported on Windows"
raise errors.ConfigurationError(msg)
options = compute_options(platform=platform, command_line_arguments=args, env=os.environ) options = compute_options(platform=platform, command_line_arguments=args, env=os.environ)
package_dir = options.globals.package_dir package_dir = options.globals.package_dir
@@ -338,10 +273,11 @@ def build_in_directory(args: CommandLineArguments) -> None:
if not any(package_dir.joinpath(name).exists() for name in package_files): if not any(package_dir.joinpath(name).exists() for name in package_files):
names = ", ".join(sorted(package_files, reverse=True)) names = ", ".join(sorted(package_files, reverse=True))
msg = f"Could not find any of {{{names}}} at root of package" msg = f"cibuildwheel: Could not find any of {{{names}}} at root of package"
raise errors.ConfigurationError(msg) print(msg, file=sys.stderr)
sys.exit(2)
platform_module = ALL_PLATFORM_MODULES[platform] platform_module = get_platform_module(platform)
identifiers = get_build_identifiers( identifiers = get_build_identifiers(
platform_module=platform_module, platform_module=platform_module,
build_selector=options.globals.build_selector, build_selector=options.globals.build_selector,
@@ -356,14 +292,11 @@ def build_in_directory(args: CommandLineArguments) -> None:
# Add CIBUILDWHEEL environment variable # Add CIBUILDWHEEL environment variable
os.environ["CIBUILDWHEEL"] = "1" os.environ["CIBUILDWHEEL"] = "1"
# Python is buffering by default when running on the CI platforms, giving # Python is buffering by default when running on the CI platforms, giving problems interleaving subprocess call output with unflushed calls to 'print'
# problems interleaving subprocess call output with unflushed calls to sys.stdout = Unbuffered(sys.stdout) # type: ignore[assignment]
# 'print'
sys.stdout = Unbuffered(sys.stdout)
# create the cache dir before it gets printed & builds performed # create the cache dir before it gets printed & builds performed
CIBW_CACHE_PATH.mkdir(parents=True, exist_ok=True) CIBW_CACHE_PATH.mkdir(parents=True, exist_ok=True)
ensure_cache_sentinel(CIBW_CACHE_PATH)
print_preamble(platform=platform, options=options, identifiers=identifiers) print_preamble(platform=platform, options=options, identifiers=identifiers)
@@ -371,36 +304,28 @@ def build_in_directory(args: CommandLineArguments) -> None:
options.check_for_invalid_configuration(identifiers) options.check_for_invalid_configuration(identifiers)
allowed_architectures_check(platform, options.globals.architectures) allowed_architectures_check(platform, options.globals.architectures)
except ValueError as err: except ValueError as err:
raise errors.DeprecationError(*err.args) from err print("cibuildwheel:", *err.args, file=sys.stderr)
sys.exit(4)
if not identifiers: if not identifiers:
message = f"No build identifiers selected: {options.globals.build_selector}" print(
if options.globals.allow_empty: f"cibuildwheel: No build identifiers selected: {options.globals.build_selector}",
print(f"cibuildwheel: {message}", file=sys.stderr) file=sys.stderr,
else: )
raise errors.NothingToDoError(message) if not args.allow_empty:
sys.exit(3)
output_dir = options.globals.output_dir output_dir = options.globals.output_dir
output_dir.mkdir(parents=True, exist_ok=True) if not output_dir.exists():
output_dir.mkdir(parents=True)
# Snapshot wheels before build to detect newly built ones
wheels_before = {p.name for p in output_dir.glob("*.whl")}
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True) tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
try: try:
with log.print_summary(options=options): with cibuildwheel.util.print_new_wheels(
"\n{n} wheels produced in {m:.0f} minutes:", output_dir
):
platform_module.build(options, tmp_path) platform_module.build(options, tmp_path)
# Run audit step after all builds complete
if options.globals.audit_command:
from cibuildwheel.audit import run_audit
run_audit(
audit_command=options.globals.audit_command,
output_dir=output_dir,
wheels_before=wheels_before,
)
finally: finally:
# avoid https://github.com/python/cpython/issues/86962 by performing # avoid https://github.com/python/cpython/issues/86962 by performing
# cleanup manually # cleanup manually
@@ -432,132 +357,36 @@ def print_preamble(platform: str, options: Options, identifiers: Sequence[str])
print() print()
print(f"Cache folder: {CIBW_CACHE_PATH}") print(f"Cache folder: {CIBW_CACHE_PATH}")
print()
warnings = detect_warnings(options=options) warnings = detect_warnings(options=options, identifiers=identifiers)
for warning in warnings: if warnings:
log.warning(warning) print("\nWarnings:")
for warning in warnings:
print(" " + warning)
error_list = list(detect_errors(options=options, identifiers=identifiers)) print("\nHere we go!\n")
if error_list:
for error in error_list:
log.error(error)
msg = "\n".join(error_list)
raise errors.ConfigurationError(msg)
print("Here we go!\n")
def detect_errors(*, options: Options, identifiers: Iterable[str]) -> Generator[str, None, None]: def get_build_identifiers(
# Check for deprecated CIBW_FREE_THREADED_SUPPORT environment variable platform_module: PlatformModule, build_selector: BuildSelector, architectures: Set[Architecture]
if "CIBW_FREE_THREADED_SUPPORT" in os.environ: ) -> list[str]:
yield ( python_configurations = platform_module.get_python_configurations(build_selector, architectures)
"CIBW_FREE_THREADED_SUPPORT environment variable is no longer supported. " return [config.identifier for config in python_configurations]
'Use tool.cibuildwheel.enable = ["cpython-freethreading"] in pyproject.toml '
"or set CIBW_ENABLE=cpython-freethreading instead."
)
# Deprecated {python} and {pip}
def detect_warnings(*, options: Options, identifiers: Iterable[str]) -> list[str]:
warnings = []
# warn about deprecated {python} and {pip}
for option_name in ["test_command", "before_build"]: for option_name in ["test_command", "before_build"]:
option_values = [getattr(options.build_options(i), option_name) for i in identifiers] option_values = [getattr(options.build_options(i), option_name) for i in identifiers]
if any(o and ("{python}" in o or "{pip}" in o) for o in option_values): if any(o and ("{python}" in o or "{pip}" in o) for o in option_values):
# Reminder: in an f-string, double braces means literal single brace # Reminder: in an f-string, double braces means literal single brace
yield ( msg = (
f"{option_name}: '{{python}}' and '{{pip}}' are no longer supported " f"{option_name}: '{{python}}' and '{{pip}}' are no longer needed, "
"and have been removed in cibuildwheel 3. Simply use 'python' or 'pip' instead." "and will be removed in a future release. Simply use 'python' or 'pip' instead."
) )
def detect_warnings(*, options: Options) -> Generator[str, None, None]:
python_version_deprecation = ((3, 11), 3)
if sys.version_info[:2] < python_version_deprecation[0]:
python_version = ".".join(map(str, python_version_deprecation[0]))
yield (
f"cibuildwheel {python_version_deprecation[1]} will require Python {python_version}+, "
"please upgrade the Python version used to run cibuildwheel. "
"This does not affect the versions you can target when building wheels. See: https://cibuildwheel.pypa.io/en/stable/#what-does-it-do"
)
build_selector = options.globals.build_selector
test_selector = options.globals.test_selector
all_valid_identifiers = [
config.identifier
for module in ALL_PLATFORM_MODULES.values()
for config in module.all_python_configurations()
]
enabled_selector = BuildSelector(
build_config="*", skip_config="", enable=options.globals.build_selector.enable
)
all_enabled_identifiers = [
identifier for identifier in all_valid_identifiers if enabled_selector(identifier)
]
yield from check_for_invalid_selectors(
selector_name="build",
selector_value=build_selector.build_config,
all_valid_identifiers=all_valid_identifiers,
all_enabled_identifiers=all_enabled_identifiers,
)
yield from check_for_invalid_selectors(
selector_name="skip",
selector_value=build_selector.skip_config,
all_valid_identifiers=all_valid_identifiers,
all_enabled_identifiers=all_enabled_identifiers,
)
yield from check_for_invalid_selectors(
selector_name="test_skip",
selector_value=test_selector.skip_config,
all_valid_identifiers=all_valid_identifiers,
all_enabled_identifiers=all_enabled_identifiers,
)
def check_for_invalid_selectors(
*,
selector_name: Literal["build", "skip", "test_skip"],
selector_value: str,
all_valid_identifiers: Sequence[str],
all_enabled_identifiers: Sequence[str],
) -> list[str]:
warnings = []
for selector in selector_value.split():
selector_ = selector
if selector_name == "test_skip":
# macosx_universal2 uses an additional identifier for tests which ends with ":{arch}"
values = selector.split(":")
universal2_identifiers = filter(
lambda x: x.endswith("-macosx_universal2"), all_valid_identifiers
)
if len(values) == 2 and any(
selector_matches(selector_, f"{i}:{arch}")
for i in universal2_identifiers
for arch in ["arm64", "x86_64"]
):
# just ignore the arch part in the rest of the check
selector_ = values[0]
if not any(selector_matches(selector_, i) for i in all_enabled_identifiers):
msg = f"Invalid {selector_name} selector: {selector!r}. "
error_type: type = errors.ConfigurationError
if any(selector_matches(selector_, i) for i in all_valid_identifiers):
msg += "This selector matches a group that wasn't enabled. Enable it using the `enable` option or remove this selector. "
if "p2" in selector_ or "p35" in selector_:
msg += f"cibuildwheel 3.x no longer supports Python < 3.8. Please use the 1.x series or update `{selector_name}`. "
error_type = errors.DeprecationError
if "p36" in selector_ or "p37" in selector_:
msg += f"cibuildwheel 3.x no longer supports Python < 3.8. Please use the 2.x series or update `{selector_name}`. "
error_type = errors.DeprecationError
if selector_name == "build":
raise error_type(msg)
msg += "This selector will have no effect. "
warnings.append(msg) warnings.append(msg)
return warnings return warnings
+1
View File
@@ -0,0 +1 @@
from __future__ import annotations
+10
View File
@@ -0,0 +1,10 @@
from __future__ import annotations
import sys
if sys.version_info >= (3, 11):
from tomllib import load, loads
else:
from tomli import load, loads
__all__ = ["load", "loads"]
+14
View File
@@ -0,0 +1,14 @@
from __future__ import annotations
import sys
if sys.version_info < (3, 11):
from typing_extensions import NotRequired, Self, assert_never
else:
from typing import NotRequired, Self, assert_never
__all__ = (
"assert_never",
"NotRequired",
"Self",
)
+64 -144
View File
@@ -1,116 +1,79 @@
from __future__ import annotations
import functools
import platform as platform_module import platform as platform_module
import re import re
import shutil
import subprocess
import sys import sys
import typing
from collections.abc import Set from collections.abc import Set
from enum import StrEnum, auto from enum import Enum
from typing import Final, Literal from typing import Final, Literal
from cibuildwheel import errors from ._compat.typing import assert_never
from .typing import PlatformName from .typing import PlatformName
PRETTY_NAMES: Final[dict[PlatformName, str]] = { PRETTY_NAMES: Final[dict[PlatformName, str]] = {
"linux": "Linux", "linux": "Linux",
"macos": "macOS", "macos": "macOS",
"windows": "Windows", "windows": "Windows",
"pyodide": "Pyodide",
"android": "Android",
"ios": "iOS",
} }
ARCH_SYNONYMS: Final[list[dict[PlatformName, str | None]]] = [ ARCH_SYNONYMS: Final[list[dict[PlatformName, str | None]]] = [
{"linux": "x86_64", "macos": "x86_64", "windows": "AMD64", "android": "x86_64"}, {"linux": "x86_64", "macos": "x86_64", "windows": "AMD64"},
{"linux": "i686", "macos": None, "windows": "x86"}, {"linux": "i686", "macos": None, "windows": "x86"},
{"linux": "aarch64", "macos": "arm64", "windows": "ARM64", "android": "arm64_v8a"}, {"linux": "aarch64", "macos": "arm64", "windows": "ARM64"},
] ]
def arch_synonym(arch: str, from_platform: PlatformName, to_platform: PlatformName) -> str | None: @functools.total_ordering
for arch_synonym_ in ARCH_SYNONYMS: class Architecture(Enum):
if arch == arch_synonym_.get(from_platform): value: str
return arch_synonym_.get(to_platform, arch)
return arch # mac/linux archs
x86_64 = "x86_64"
def _check_aarch32_el0() -> bool:
"""Check if running armv7l natively on aarch64 is supported"""
if not sys.platform.startswith("linux"):
return False
if platform_module.machine() != "aarch64":
return False
executable = shutil.which("linux32")
if executable is None:
return False
check = subprocess.run([executable, "uname", "-m"], check=False, capture_output=True, text=True)
return check.returncode == 0 and check.stdout.startswith("armv")
@typing.final
class Architecture(StrEnum):
# mac/linux/android archs
x86_64 = auto()
# linux archs # linux archs
i686 = auto() i686 = "i686"
aarch64 = auto() aarch64 = "aarch64"
ppc64le = auto() ppc64le = "ppc64le"
s390x = auto() s390x = "s390x"
armv7l = auto()
riscv64 = auto()
# mac archs # mac archs
universal2 = auto() universal2 = "universal2"
arm64 = auto() arm64 = "arm64"
# windows archs # windows archs
x86 = auto() x86 = "x86"
AMD64 = "AMD64" AMD64 = "AMD64"
ARM64 = "ARM64" ARM64 = "ARM64"
# WebAssembly # Allow this to be sorted
wasm32 = auto() def __lt__(self, other: Architecture) -> bool:
return self.value < other.value
# android archs def __str__(self) -> str:
arm64_v8a = auto() return self.name
# iOS "multiarch" architectures that include both
# the CPU architecture and the ABI.
arm64_iphoneos = auto()
arm64_iphonesimulator = auto()
x86_64_iphonesimulator = auto()
@staticmethod @staticmethod
def parse_config(config: str, platform: PlatformName) -> "set[Architecture]": def parse_config(config: str, platform: PlatformName) -> set[Architecture]:
result = set() result = set()
for arch_str in re.split(r"[\s,]+", config): for arch_str in re.split(r"[\s,]+", config):
match arch_str: if arch_str == "auto":
case "auto": result |= Architecture.auto_archs(platform=platform)
result |= Architecture.auto_archs(platform=platform) elif arch_str == "native":
case "native": result.add(Architecture(platform_module.machine()))
if native_arch := Architecture.native_arch(platform=platform): elif arch_str == "all":
result.add(native_arch) result |= Architecture.all_archs(platform=platform)
case "all": elif arch_str == "auto64":
result |= Architecture.all_archs(platform=platform) result |= Architecture.bitness_archs(platform=platform, bitness="64")
case "auto64": elif arch_str == "auto32":
result |= Architecture.bitness_archs(platform=platform, bitness="64") result |= Architecture.bitness_archs(platform=platform, bitness="32")
case "auto32": else:
result |= Architecture.bitness_archs(platform=platform, bitness="32") result.add(Architecture(arch_str))
case _:
try:
result.add(Architecture(arch_str))
except ValueError as e:
msg = f"Invalid architecture '{arch_str}'"
raise errors.ConfigurationError(msg) from e
return result return result
@staticmethod @staticmethod
def native_arch(platform: PlatformName) -> "Architecture | None": def auto_archs(platform: PlatformName) -> set[Architecture]:
native_machine = platform_module.machine() native_machine = platform_module.machine()
native_architecture = Architecture(native_machine)
# Cross-platform support. Used for --print-build-identifiers or docker builds. # Cross-platform support. Used for --print-build-identifiers or docker builds.
host_platform: PlatformName = ( host_platform: PlatformName = (
@@ -119,49 +82,34 @@ class Architecture(StrEnum):
else ("macos" if sys.platform.startswith("darwin") else "linux") else ("macos" if sys.platform.startswith("darwin") else "linux")
) )
if platform == "pyodide": native_architecture = Architecture(native_machine)
return Architecture.wasm32
elif platform == "ios":
# Can only build for iOS on macOS. The "native" architecture is the
# simulator for the macOS native platform.
if host_platform == "macos":
if native_architecture == Architecture.x86_64:
return Architecture.x86_64_iphonesimulator
else:
return Architecture.arm64_iphonesimulator
else:
return None
# we might need to rename the native arch to the machine we're running # we might need to rename the native arch to the machine we're running
# on, as the same arch can have different names on different platforms # on, as the same arch can have different names on different platforms
if host_platform != platform: if host_platform != platform:
synonym = arch_synonym(native_machine, host_platform, platform) for arch_synonym in ARCH_SYNONYMS:
if synonym is None: if native_machine == arch_synonym.get(host_platform):
# can't build anything on this platform synonym = arch_synonym[platform]
return None
native_architecture = Architecture(synonym) if synonym is None:
# can't build anything on this platform
return set()
return native_architecture native_architecture = Architecture(synonym)
@staticmethod result = {native_architecture}
def auto_archs(platform: PlatformName) -> "set[Architecture]":
native_arch = Architecture.native_arch(platform)
if native_arch is None:
return set() # can't build anything on this platform
result = {native_arch}
match platform: if platform == "linux" and Architecture.x86_64 in result:
case "windows" if Architecture.AMD64 in result: # x86_64 machines can run i686 containers
result.add(Architecture.x86) result.add(Architecture.i686)
case "ios" if native_arch == Architecture.arm64_iphonesimulator:
# Also build the device wheel if we're on ARM64. if platform == "windows" and Architecture.AMD64 in result:
result.add(Architecture.arm64_iphoneos) result.add(Architecture.x86)
return result return result
@staticmethod @staticmethod
def all_archs(platform: PlatformName) -> "set[Architecture]": def all_archs(platform: PlatformName) -> set[Architecture]:
all_archs_map = { all_archs_map = {
"linux": { "linux": {
Architecture.x86_64, Architecture.x86_64,
@@ -169,52 +117,24 @@ class Architecture(StrEnum):
Architecture.aarch64, Architecture.aarch64,
Architecture.ppc64le, Architecture.ppc64le,
Architecture.s390x, Architecture.s390x,
Architecture.armv7l,
Architecture.riscv64,
}, },
"macos": {Architecture.x86_64, Architecture.arm64, Architecture.universal2}, "macos": {Architecture.x86_64, Architecture.arm64, Architecture.universal2},
"windows": {Architecture.x86, Architecture.AMD64, Architecture.ARM64}, "windows": {Architecture.x86, Architecture.AMD64, Architecture.ARM64},
"pyodide": {Architecture.wasm32},
"android": {Architecture.x86_64, Architecture.arm64_v8a},
"ios": {
Architecture.x86_64_iphonesimulator,
Architecture.arm64_iphonesimulator,
Architecture.arm64_iphoneos,
},
} }
return all_archs_map[platform] return all_archs_map[platform]
@staticmethod @staticmethod
def bitness_archs(platform: PlatformName, bitness: Literal["64", "32"]) -> "set[Architecture]": # pylint: disable-next=inconsistent-return-statements
# This map maps 64-bit architectures to their 32-bit equivalents. def bitness_archs(platform: PlatformName, bitness: Literal["64", "32"]) -> set[Architecture]:
archs_map = { archs_32 = {Architecture.i686, Architecture.x86}
Architecture.x86_64: Architecture.i686, auto_archs = Architecture.auto_archs(platform)
Architecture.AMD64: Architecture.x86,
Architecture.aarch64: Architecture.armv7l,
}
native_arch = Architecture.native_arch(platform)
if native_arch is None: if bitness == "64":
return set() # can't build anything on this platform return auto_archs - archs_32
elif bitness == "32":
if native_arch == Architecture.wasm32: return auto_archs & archs_32
return {native_arch} if bitness == "32" else set() else:
assert_never(bitness)
match bitness:
case "64":
return {native_arch} if native_arch not in archs_map.values() else set()
case "32":
if native_arch in archs_map.values():
return {native_arch}
elif native_arch in archs_map and platform in {"linux", "windows"}:
if native_arch == Architecture.aarch64 and not _check_aarch32_el0():
# If we're on aarch64, skip if we cannot build armv7l wheels.
return set()
return {archs_map[native_arch]}
else:
return set()
case _:
typing.assert_never(bitness)
def allowed_architectures_check( def allowed_architectures_check(
-89
View File
@@ -1,89 +0,0 @@
"""
Audit step for wheels built by cibuildwheel.
This module provides functionality to run audit commands (like abi3audit)
on built wheels after all platform builds are complete.
"""
import os
import subprocess
from pathlib import Path
from packaging.utils import parse_wheel_filename
from .logger import log
from .util.helpers import format_safe
def is_abi3_wheel(wheel_path: Path) -> bool:
"""Check if a wheel is an abi3 wheel by parsing its filename."""
_, _, _, tags = parse_wheel_filename(wheel_path.name)
return any(t.abi == "abi3" for t in tags)
def run_audit(
audit_command: str,
output_dir: Path,
wheels_before: set[str],
) -> None:
"""
Run the audit command on wheels built in this run.
The audit command supports the following placeholders:
- {wheel}: expands to each wheel path, runs the command once per wheel
- {abi3_wheel}: same as {wheel}, but only for abi3 wheels
If the command contains {abi3_wheel} but no abi3 wheels were produced,
the audit step is skipped.
Args:
audit_command: The command template to run
output_dir: Directory where wheels were output
wheels_before: Set of wheel filenames that existed before the build
"""
if not audit_command:
return
# Find wheels built in this run (new wheels that weren't there before)
all_wheels = sorted(output_dir.glob("*.whl"))
just_built = [w for w in all_wheels if w.name not in wheels_before]
if not just_built:
return
# Determine if we're auditing abi3 wheels only
abi3_only = "{abi3_wheel}" in audit_command
# Filter wheels if needed
if abi3_only:
wheels_to_audit = [w for w in just_built if is_abi3_wheel(w)]
if not wheels_to_audit:
log.step("Skipping audit step (no abi3 wheels produced)")
return
else:
wheels_to_audit = just_built
log.step("Running audit...")
for wheel in wheels_to_audit:
# Prepare command with placeholders
prepared = format_safe(
audit_command,
wheel=wheel,
abi3_wheel=wheel,
)
log.step(f" Auditing {wheel.name}...")
env = os.environ.copy()
try:
subprocess.run(
prepared,
shell=True,
check=True,
env=env,
cwd=output_dir,
)
except subprocess.CalledProcessError as e:
log.error(f"Audit command failed for {wheel.name}")
raise SystemExit(e.returncode) from e
+7 -9
View File
@@ -1,23 +1,21 @@
import dataclasses from __future__ import annotations
import subprocess import subprocess
from collections.abc import ( from collections.abc import Iterable, Mapping, Sequence
Callable, from dataclasses import dataclass
Iterable, from typing import Callable, Dict, List # noqa: TID251
Mapping,
Sequence,
)
import bashlex import bashlex
# a function that takes a command and the environment, and returns the result # a function that takes a command and the environment, and returns the result
EnvironmentExecutor = Callable[[list[str], dict[str, str]], str] EnvironmentExecutor = Callable[[List[str], Dict[str, str]], str]
def local_environment_executor(command: Sequence[str], env: Mapping[str, str]) -> str: def local_environment_executor(command: Sequence[str], env: Mapping[str, str]) -> str:
return subprocess.run(command, env=env, text=True, stdout=subprocess.PIPE, check=True).stdout return subprocess.run(command, env=env, text=True, stdout=subprocess.PIPE, check=True).stdout
@dataclasses.dataclass(frozen=True, kw_only=True) @dataclass(frozen=True)
class NodeExecutionContext: class NodeExecutionContext:
environment: dict[str, str] environment: dict[str, str]
input: str input: str
-79
View File
@@ -1,79 +0,0 @@
import os
import re
from enum import Enum
from .util.helpers import strtobool
ANSI_CODE_REGEX = re.compile(r"(\033\[[0-9;]*m)")
class CIProvider(Enum):
# official support
travis_ci = "travis"
circle_ci = "circle_ci"
azure_pipelines = "azure_pipelines"
github_actions = "github_actions"
gitlab = "gitlab"
cirrus_ci = "cirrus_ci"
# unofficial support
appveyor = "appveyor"
other = "other"
def detect_ci_provider() -> CIProvider | None:
if "TRAVIS" in os.environ:
return CIProvider.travis_ci
elif "APPVEYOR" in os.environ:
return CIProvider.appveyor
elif "CIRCLECI" in os.environ:
return CIProvider.circle_ci
elif "AZURE_HTTP_USER_AGENT" in os.environ:
return CIProvider.azure_pipelines
elif "GITHUB_ACTIONS" in os.environ:
return CIProvider.github_actions
elif "GITLAB_CI" in os.environ:
return CIProvider.gitlab
elif "CIRRUS_CI" in os.environ:
return CIProvider.cirrus_ci
elif strtobool(os.environ.get("CI", "false")):
return CIProvider.other
else:
return None
def fix_ansi_codes_for_github_actions(text: str) -> str:
"""
Github Actions forgets the current ANSI style on every new line. This
function repeats the current ANSI style on every new line.
"""
ansi_codes: list[str] = []
output = ""
for line in text.splitlines(keepends=True):
# add the current ANSI codes to the beginning of the line
output += "".join(ansi_codes) + line
# split the line at each ANSI code
parts = ANSI_CODE_REGEX.split(line)
# if there are any ANSI codes, save them
if len(parts) > 1:
# iterate over the ANSI codes in this line
for code in parts[1::2]:
if code == "\033[0m":
# reset the list of ANSI codes when the clear code is found
ansi_codes = []
else:
ansi_codes.append(code)
return output
def filter_ansi_codes(text: str, /) -> str:
"""
Remove ANSI codes from text.
"""
return ANSI_CODE_REGEX.sub("", text)
+8 -5
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import dataclasses import dataclasses
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from typing import Any, Protocol from typing import Any, Protocol
@@ -73,16 +75,12 @@ class EnvironmentAssignmentRaw:
return self.value return self.value
@dataclasses.dataclass
class EnvironmentAssignmentBash: class EnvironmentAssignmentBash:
""" """
An environment variable, in bash syntax. The value can use bash constructs An environment variable, in bash syntax. The value can use bash constructs
like "$OTHER_VAR" and "$(command arg1 arg2)". like "$OTHER_VAR" and "$(command arg1 arg2)".
""" """
name: str
value: str
def __init__(self, assignment: str): def __init__(self, assignment: str):
name, equals, value = assignment.partition("=") name, equals, value = assignment.partition("=")
if not equals: if not equals:
@@ -100,8 +98,13 @@ class EnvironmentAssignmentBash:
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.name}={self.value}" return f"{self.name}={self.value}"
def __eq__(self, other: object) -> bool:
if isinstance(other, EnvironmentAssignmentBash):
return self.name == other.name and self.value == other.value
return False
@dataclasses.dataclass(kw_only=True)
@dataclasses.dataclass
class ParsedEnvironment: class ParsedEnvironment:
assignments: list[EnvironmentAssignment] assignments: list[EnvironmentAssignment]
-105
View File
@@ -1,105 +0,0 @@
"""
Errors that can cause the build to fail. Each subclass of FatalError has
a different return code, by defining them all here, we can ensure that they're
semantically clear and unique.
"""
import textwrap
class FatalError(BaseException):
"""
Raising an error of this type will cause the message to be printed to
stderr and the process to be terminated. Within cibuildwheel, raising this
exception produces a better error message, and optional traceback.
"""
return_code: int = 1
class ConfigurationError(FatalError):
return_code = 2
class NothingToDoError(FatalError):
return_code = 3
class DeprecationError(FatalError):
return_code = 4
class NonPlatformWheelError(FatalError):
def __init__(self) -> None:
message = textwrap.dedent(
"""
Build failed because a pure Python wheel was generated.
If you intend to build a pure-Python wheel, you don't need
cibuildwheel - use `pip wheel .`, `pipx run build --wheel`, `uv
build --wheel`, etc. instead. You only need cibuildwheel if you
have compiled (not Python) code in your wheels making them depend
on the platform.
If you expected a platform wheel, check your project configuration,
or run cibuildwheel with CIBW_BUILD_VERBOSITY=1 to view build logs.
"""
)
super().__init__(message)
self.return_code = 5
class AlreadyBuiltWheelError(FatalError):
def __init__(self, wheel_name: str) -> None:
message = textwrap.dedent(
f"""
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)
self.return_code = 6
class OCIEngineTooOldError(FatalError):
def __init__(self, message: str) -> None:
super().__init__(message)
self.return_code = 7
class RepairStepProducedNoWheelError(FatalError):
def __init__(self) -> None:
message = textwrap.dedent(
"""
Build failed because the repair step completed successfully but
did not produce a wheel.
Your `repair-wheel-command` is expected to place the repaired
wheel in the {dest_dir} directory. See the documentation for
example configurations:
https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command
"""
)
super().__init__(message)
self.return_code = 8
class RepairStepProducedMultipleWheelsError(FatalError):
def __init__(self, wheels: list[str]) -> None:
message = textwrap.dedent(
f"""
Build failed because the repair step completed successfully but
produced multiple wheels: {wheels}
Your `repair-wheel-command` is expected to place one repaired
wheel in the {{dest_dir}} directory. See the documentation for
example configurations:
https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command
"""
)
super().__init__(message)
self.return_code = 8
+5 -71
View File
@@ -2,22 +2,18 @@
These are utilities for the `/bin` scripts, not for the `cibuildwheel` program. These are utilities for the `/bin` scripts, not for the `cibuildwheel` program.
""" """
import json from __future__ import annotations
import time
import typing
import urllib.error
import urllib.request
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from io import StringIO from io import StringIO
from typing import Any, NotRequired, Protocol from typing import Protocol
from cibuildwheel import __version__ as cibw_version
__all__ = ("Printable", "dump_python_configurations") __all__ = ("Printable", "dump_python_configurations")
class Printable(Protocol): class Printable(Protocol):
def __str__(self) -> str: ... def __str__(self) -> str:
...
def dump_python_configurations( def dump_python_configurations(
@@ -37,65 +33,3 @@ def dump_python_configurations(
output.write("\n") output.write("\n")
# Strip the final newline, to avoid two blank lines at the end. # Strip the final newline, to avoid two blank lines at the end.
return output.getvalue()[:-1] return output.getvalue()[:-1]
def _json_request(request: urllib.request.Request, timeout: int = 30) -> dict[str, Any]:
with urllib.request.urlopen(request, timeout=timeout) as response:
return typing.cast("dict[str, Any]", json.load(response))
def github_api_request(path: str, *, max_retries: int = 3) -> dict[str, Any]:
"""
Makes a GitHub API request to the given path and returns the JSON response.
"""
api_url = f"https://api.github.com/{path}"
headers = {
"Accept": "application/vnd.github.v3+json",
"User-Agent": f"cibuildwheel/{cibw_version}",
}
request = urllib.request.Request(api_url, headers=headers)
for retry_count in range(max_retries):
try:
return _json_request(request)
except (urllib.error.URLError, TimeoutError) as e:
# pylint: disable=E1101
if (
isinstance(e, urllib.error.HTTPError)
and (e.code == 403 or e.code == 429)
and e.headers.get("x-ratelimit-remaining") == "0"
):
reset_time = int(e.headers.get("x-ratelimit-reset", 0))
wait_time = max(0, reset_time - int(e.headers.get("date", 0)))
print(f"Github rate limit exceeded. Waiting for {wait_time} seconds.")
time.sleep(wait_time)
else:
print(f"Retrying GitHub API request due to error: {e}")
if retry_count == max_retries - 1:
print(f"GitHub API request failed (Network error: {e}). Check network connection.")
raise e
# Should never be reached but to keep the type checker happy
msg = "Unexpected execution path in github_api_request"
raise RuntimeError(msg)
class PyodideXBuildEnvRelease(typing.TypedDict):
version: str
python_version: str
emscripten_version: str
min_pyodide_build_version: NotRequired[str]
max_pyodide_build_version: NotRequired[str]
class PyodideXBuildEnvInfo(typing.TypedDict):
releases: dict[str, PyodideXBuildEnvRelease]
def get_pyodide_xbuildenv_info() -> PyodideXBuildEnvInfo:
xbuildenv_info_url = (
"https://pyodide.github.io/pyodide/api/pyodide-cross-build-environments.json"
)
with urllib.request.urlopen(xbuildenv_info_url) as response:
return typing.cast("PyodideXBuildEnvInfo", json.loads(response.read().decode("utf-8")))
-84
View File
@@ -1,84 +0,0 @@
import dataclasses
import shlex
import typing
from collections.abc import Sequence
from typing import Literal, Self, get_args
from .logger import log
from .util.helpers import parse_key_value_string
BuildFrontendName = Literal["pip", "build", "build[uv]"]
@dataclasses.dataclass(frozen=True)
class BuildFrontendConfig:
name: BuildFrontendName
args: Sequence[str] = ()
@classmethod
def from_config_string(cls, config_string: str) -> Self:
config_dict = parse_key_value_string(config_string, ["name"], ["args"])
name = " ".join(config_dict["name"])
if name not in get_args(BuildFrontendName):
names = ", ".join(repr(n) for n in get_args(BuildFrontendName))
msg = f"Unrecognised build frontend {name!r}, must be one of {names}"
raise ValueError(msg)
name = typing.cast("BuildFrontendName", name)
args = config_dict.get("args") or []
return cls(name=name, args=args)
def options_summary(self) -> str | dict[str, str]:
if not self.args:
return self.name
else:
return {"name": self.name, "args": repr(self.args)}
def _get_verbosity_flags(level: int, frontend: BuildFrontendName) -> list[str]:
if level < 0:
if frontend == "pip":
return ["-" + -level * "q"]
msg = f"build_verbosity {level} is not supported for {frontend} frontend. Ignoring."
log.warning(msg)
if level > 0:
if frontend == "pip":
return ["-" + level * "v"]
if level > 1:
return ["-" + (level - 1) * "v"]
return []
def _split_config_settings(config_settings: str) -> list[str]:
config_settings_list = shlex.split(config_settings)
return [f"-C{setting}" for setting in config_settings_list]
# Based on build.__main__.main.
def parse_config_settings(config_settings_str: str) -> dict[str, str | list[str]]:
config_settings: dict[str, str | list[str]] = {}
for arg in shlex.split(config_settings_str):
setting, _, value = arg.partition("=")
existing_value = config_settings.get(setting)
if existing_value is None:
config_settings[setting] = value
elif isinstance(existing_value, str):
config_settings[setting] = [existing_value, value]
else:
existing_value.append(value)
return config_settings
def get_build_frontend_extra_flags(
build_frontend: BuildFrontendConfig, verbosity_level: int, config_settings: str
) -> list[str]:
return [
*_split_config_settings(config_settings),
*build_frontend.args,
*_get_verbosity_flags(verbosity_level, build_frontend.name),
]
@@ -1,40 +1,35 @@
import contextlib from __future__ import annotations
import dataclasses
import subprocess import subprocess
import sys import sys
import textwrap import textwrap
from collections import OrderedDict
from collections.abc import Iterable, Iterator, Sequence, Set from collections.abc import Iterable, Iterator, Sequence, Set
from dataclasses import dataclass
from pathlib import Path, PurePath, PurePosixPath from pathlib import Path, PurePath, PurePosixPath
from typing import TYPE_CHECKING, assert_never from typing import OrderedDict, Tuple
from .. import errors from ._compat.typing import assert_never
from ..architecture import Architecture from .architecture import Architecture
from ..frontend import get_build_frontend_extra_flags from .logger import log
from ..logger import log from .oci_container import OCIContainer
from ..oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform from .options import Options
from ..options import BuildOptions, Options from .typing import PathOrStr
from ..selector import BuildSelector from .util import (
from ..util import resources AlreadyBuiltWheelError,
from ..util.file import copy_test_sources BuildFrontendConfig,
from ..util.helpers import prepare_command, unwrap BuildSelector,
from ..util.packaging import find_compatible_wheel NonPlatformWheelError,
find_compatible_wheel,
if TYPE_CHECKING: get_build_verbosity_extra_flags,
from ..typing import PathOrStr prepare_command,
read_python_configs,
ARCHITECTURE_OCI_PLATFORM_MAP = { split_config_settings,
Architecture.x86_64: OCIPlatform.AMD64, test_fail_cwd_file,
Architecture.i686: OCIPlatform.i386, unwrap,
Architecture.aarch64: OCIPlatform.ARM64, )
Architecture.ppc64le: OCIPlatform.PPC64LE,
Architecture.s390x: OCIPlatform.S390X,
Architecture.armv7l: OCIPlatform.ARMV7,
Architecture.riscv64: OCIPlatform.RISCV64,
}
@dataclasses.dataclass(frozen=True, kw_only=True) @dataclass(frozen=True)
class PythonConfiguration: class PythonConfiguration:
version: str version: str
identifier: str identifier: str
@@ -45,24 +40,20 @@ class PythonConfiguration:
return PurePosixPath(self.path_str) return PurePosixPath(self.path_str)
@dataclasses.dataclass(frozen=True, kw_only=True) @dataclass(frozen=True)
class BuildStep: class BuildStep:
platform_configs: list[PythonConfiguration] platform_configs: list[PythonConfiguration]
platform_tag: str platform_tag: str
container_engine: OCIContainerEngineConfig
container_image: str container_image: str
def all_python_configurations() -> list[PythonConfiguration]:
config_dicts = resources.read_python_configs("linux")
return [PythonConfiguration(**item) for item in config_dicts]
def get_python_configurations( def get_python_configurations(
build_selector: BuildSelector, build_selector: BuildSelector,
architectures: Set[Architecture], architectures: Set[Architecture],
) -> list[PythonConfiguration]: ) -> list[PythonConfiguration]:
python_configurations = all_python_configurations() full_python_configs = read_python_configs("linux")
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
# return all configurations whose arch is in our `architectures` set, # return all configurations whose arch is in our `architectures` set,
# and match the build/skip rules # and match the build/skip rules
@@ -74,9 +65,8 @@ def get_python_configurations(
] ]
def container_image_for_python_configuration( def container_image_for_python_configuration(config: PythonConfiguration, options: Options) -> str:
config: PythonConfiguration, build_options: BuildOptions build_options = options.build_options(config.identifier)
) -> str:
# e.g # e.g
# identifier is 'cp310-manylinux_x86_64' # identifier is 'cp310-manylinux_x86_64'
# platform_tag is 'manylinux_x86_64' # platform_tag is 'manylinux_x86_64'
@@ -101,18 +91,15 @@ def get_build_steps(
Groups PythonConfigurations into BuildSteps. Each BuildStep represents a Groups PythonConfigurations into BuildSteps. Each BuildStep represents a
separate container instance. separate container instance.
""" """
steps = OrderedDict[tuple[str, str, str, OCIContainerEngineConfig], BuildStep]() steps = OrderedDict[Tuple[str, str, str], BuildStep]()
for config in python_configurations: for config in python_configurations:
_, platform_tag = config.identifier.split("-", 1) _, platform_tag = config.identifier.split("-", 1)
build_options = options.build_options(config.identifier) before_all = options.build_options(config.identifier).before_all
container_image = container_image_for_python_configuration(config, options)
before_all = build_options.before_all step_key = (platform_tag, container_image, before_all)
container_image = container_image_for_python_configuration(config, build_options)
container_engine = build_options.container_engine
step_key = (platform_tag, container_image, before_all, container_engine)
if step_key in steps: if step_key in steps:
steps[step_key].platform_configs.append(config) steps[step_key].platform_configs.append(config)
@@ -120,7 +107,6 @@ def get_build_steps(
steps[step_key] = BuildStep( steps[step_key] = BuildStep(
platform_configs=[config], platform_configs=[config],
platform_tag=platform_tag, platform_tag=platform_tag,
container_engine=container_engine,
container_image=container_image, container_image=container_image,
) )
@@ -131,36 +117,33 @@ def check_all_python_exist(
*, platform_configs: Iterable[PythonConfiguration], container: OCIContainer *, platform_configs: Iterable[PythonConfiguration], container: OCIContainer
) -> None: ) -> None:
exist = True exist = True
has_manylinux_interpreters = False has_manylinux_interpreters = True
messages = [] messages = []
with contextlib.suppress(subprocess.CalledProcessError): try:
# use capture_output to keep quiet # use capture_output to keep quiet
container.call(["manylinux-interpreters", "--help"], capture_output=True) container.call(["manylinux-interpreters", "--help"], capture_output=True)
has_manylinux_interpreters = True except subprocess.CalledProcessError:
has_manylinux_interpreters = False
for config in platform_configs: for config in platform_configs:
python_path = config.path / "bin" / "python" python_path = config.path / "bin" / "python"
if has_manylinux_interpreters: try:
try: if has_manylinux_interpreters:
container.call(["manylinux-interpreters", "ensure", config.path.name]) container.call(["manylinux-interpreters", "ensure", config.path.name])
except subprocess.CalledProcessError: container.call(["test", "-x", python_path])
messages.append( except subprocess.CalledProcessError:
f" 'manylinux-interpreters ensure {config.path.name}' needed to build '{config.identifier}' failed in container running image '{container.image}'." messages.append(
" Either the installation failed or this interpreter is not available in that image. Please check the logs." f" '{python_path}' executable doesn't exist in image '{container.image}' to build '{config.identifier}'."
) )
exist = False exist = False
else:
try:
container.call(["test", "-x", python_path])
except subprocess.CalledProcessError:
messages.append(
f" '{python_path}' executable doesn't exist in image '{container.image}' to build '{config.identifier}'."
)
exist = False
if not exist: if not exist:
message = "\n".join(messages) message = "\n".join(messages)
raise errors.FatalError(message) print(
f"cibuildwheel:\n{message}",
file=sys.stderr,
)
sys.exit(1)
def build_in_container( def build_in_container(
@@ -170,7 +153,6 @@ def build_in_container(
container: OCIContainer, container: OCIContainer,
container_project_path: PurePath, container_project_path: PurePath,
container_package_dir: PurePath, container_package_dir: PurePath,
local_tmp_dir: Path,
) -> None: ) -> None:
container_output_dir = PurePosixPath("/output") container_output_dir = PurePosixPath("/output")
@@ -186,7 +168,7 @@ def build_in_container(
log.step("Running before_all...") log.step("Running before_all...")
env = container.get_environment() env = container.get_environment()
env["PATH"] = f"/opt/python/cp39-cp39/bin:{env['PATH']}" env["PATH"] = f'/opt/python/cp38-cp38/bin:{env["PATH"]}'
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
env["PIP_ROOT_USER_ACTION"] = "ignore" env["PIP_ROOT_USER_ACTION"] = "ignore"
env = before_all_options.environment.as_dictionary( env = before_all_options.environment.as_dictionary(
@@ -204,50 +186,48 @@ def build_in_container(
for config in platform_configs: for config in platform_configs:
log.build_start(config.identifier) log.build_start(config.identifier)
local_identifier_tmp_dir = local_tmp_dir / config.identifier
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
use_uv = build_frontend.name == "build[uv]"
pip = ["uv", "pip"] if use_uv else ["pip"]
log.step("Setting up build environment...")
dependency_constraint_flags: list[PathOrStr] = [] dependency_constraint_flags: list[PathOrStr] = []
local_constraints_file = build_options.dependency_constraints.get_for_python_version(
version=config.version, if build_options.dependency_constraints:
tmp_dir=local_identifier_tmp_dir, constraints_file = build_options.dependency_constraints.get_for_python_version(
) config.version
if local_constraints_file: )
container_constraints_file = PurePosixPath("/constraints.txt") container_constraints_file = PurePosixPath("/constraints.txt")
container.copy_into(local_constraints_file, container_constraints_file)
container.copy_into(constraints_file, container_constraints_file)
dependency_constraint_flags = ["-c", container_constraints_file] dependency_constraint_flags = ["-c", container_constraints_file]
log.step("Setting up build environment...")
env = container.get_environment() env = container.get_environment()
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
env["PIP_ROOT_USER_ACTION"] = "ignore" env["PIP_ROOT_USER_ACTION"] = "ignore"
# put this config's python top of the list # put this config's python top of the list
python_bin = config.path / "bin" python_bin = config.path / "bin"
env["PATH"] = f"{python_bin}:{env['PATH']}" env["PATH"] = f'{python_bin}:{env["PATH"]}'
env = build_options.environment.as_dictionary(env, executor=container.environment_executor) env = build_options.environment.as_dictionary(env, executor=container.environment_executor)
# check config python is still on PATH # check config python is still on PATH
which_python = container.call(["which", "python"], env=env, capture_output=True).strip() which_python = container.call(["which", "python"], env=env, capture_output=True).strip()
if PurePosixPath(which_python) != python_bin / "python": if PurePosixPath(which_python) != python_bin / "python":
msg = "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." print(
raise errors.FatalError(msg) "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)
if use_uv: which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip()
which_uv = container.call(["which", "uv"], env=env, capture_output=True).strip() if PurePosixPath(which_pip) != python_bin / "pip":
if not which_uv: print(
msg = "uv not found on PATH. You must use a supported manylinux or musllinux environment with uv." "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.",
raise errors.FatalError(msg) file=sys.stderr,
else: )
which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip() sys.exit(1)
if PurePosixPath(which_pip) != python_bin / "pip":
msg = "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."
raise errors.FatalError(msg)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier) compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel: if compatible_wheel:
@@ -255,7 +235,7 @@ def build_in_container(
print( print(
f"\nFound previously built wheel {compatible_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 = compatible_wheel repaired_wheels = [compatible_wheel]
else: else:
if build_options.before_build: if build_options.before_build:
log.step("Running before_build...") log.step("Running before_build...")
@@ -273,42 +253,42 @@ def build_in_container(
container.call(["rm", "-rf", built_wheel_dir]) container.call(["rm", "-rf", built_wheel_dir])
container.call(["mkdir", "-p", built_wheel_dir]) container.call(["mkdir", "-p", built_wheel_dir])
extra_flags = get_build_frontend_extra_flags( extra_flags = split_config_settings(build_options.config_settings, build_frontend.name)
build_frontend, build_options.build_verbosity, build_options.config_settings extra_flags += build_frontend.args
)
match build_frontend.name: if build_frontend.name == "pip":
case "pip": extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
container.call( container.call(
[ [
"python", "python",
"-m", "-m",
"pip", "pip",
"wheel", "wheel",
container_package_dir, container_package_dir,
f"--wheel-dir={built_wheel_dir}", f"--wheel-dir={built_wheel_dir}",
"--no-deps", "--no-deps",
*extra_flags, *extra_flags,
], ],
env=env, env=env,
) )
case "build" | "build[uv]": elif build_frontend.name == "build":
if use_uv and "--no-isolation" not in extra_flags and "-n" not in extra_flags: if not 0 <= build_options.build_verbosity < 2:
extra_flags += ["--installer=uv"] msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
container.call( log.warning(msg)
[ container.call(
"python", [
"-m", "python",
"build", "-m",
container_package_dir, "build",
"--wheel", container_package_dir,
f"--outdir={built_wheel_dir}", "--wheel",
*extra_flags, f"--outdir={built_wheel_dir}",
], *extra_flags,
env=env, ],
) env=env,
case _: )
assert_never(build_frontend) else:
assert_never(build_frontend)
built_wheel = container.glob(built_wheel_dir, "*.whl")[0] built_wheel = container.glob(built_wheel_dir, "*.whl")[0]
@@ -317,62 +297,39 @@ def build_in_container(
container.call(["mkdir", "-p", repaired_wheel_dir]) container.call(["mkdir", "-p", repaired_wheel_dir])
if built_wheel.name.endswith("none-any.whl"): if built_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError() raise NonPlatformWheelError()
if build_options.repair_command: if build_options.repair_command:
log.step("Repairing wheel...") log.step("Repairing wheel...")
repair_command_prepared = prepare_command( repair_command_prepared = prepare_command(
build_options.repair_command, build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
wheel=built_wheel,
dest_dir=repaired_wheel_dir,
package=container_package_dir,
project=container_project_path,
) )
container.call(["sh", "-c", repair_command_prepared], env=env) container.call(["sh", "-c", repair_command_prepared], env=env)
else: else:
container.call(["mv", built_wheel, repaired_wheel_dir]) container.call(["mv", built_wheel, repaired_wheel_dir])
match container.glob(repaired_wheel_dir, "*.whl"): repaired_wheels = container.glob(repaired_wheel_dir, "*.whl")
case []:
raise errors.RepairStepProducedNoWheelError()
case [repaired_wheel]:
pass
case too_many:
raise errors.RepairStepProducedMultipleWheelsError([p.name for p in too_many])
if repaired_wheel.name in {wheel.name for wheel in built_wheels}: for repaired_wheel in repaired_wheels:
raise errors.AlreadyBuiltWheelError(repaired_wheel.name) 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): if build_options.test_command and build_options.test_selector(config.identifier):
log.step("Testing wheel...") log.step("Testing wheel...")
# set up a virtual environment to install and test from, to make sure # set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time. # there are no dependencies that were pulled in at build time.
if not use_uv: container.call(["pip", "install", "virtualenv", *dependency_constraint_flags], env=env)
container.call(
["pip", "install", "virtualenv", *dependency_constraint_flags], env=env
)
testing_temp_dir = PurePosixPath( testing_temp_dir = PurePosixPath(
container.call(["mktemp", "-d"], capture_output=True).strip() container.call(["mktemp", "-d"], capture_output=True).strip()
) )
venv_dir = testing_temp_dir / "venv" venv_dir = testing_temp_dir / "venv"
if use_uv: container.call(["python", "-m", "virtualenv", "--no-download", venv_dir], env=env)
container.call(["uv", "venv", venv_dir, "--python", python_bin / "python"], env=env)
else:
# Use embedded dependencies from virtualenv to ensure determinism
venv_args = ["--no-periodic-update", "--pip=embed", "--no-setuptools"]
if "38" in config.identifier:
venv_args.append("--no-wheel")
container.call(["python", "-m", "virtualenv", *venv_args, venv_dir], env=env)
virtualenv_env = env.copy() virtualenv_env = env.copy()
virtualenv_env["PATH"] = f"{venv_dir / 'bin'}:{virtualenv_env['PATH']}" virtualenv_env["PATH"] = f"{venv_dir / 'bin'}:{virtualenv_env['PATH']}"
virtualenv_env["VIRTUAL_ENV"] = str(venv_dir)
virtualenv_env = build_options.test_environment.as_dictionary(
prev_environment=virtualenv_env
)
if build_options.before_test: if build_options.before_test:
before_test_prepared = prepare_command( before_test_prepared = prepare_command(
@@ -383,52 +340,46 @@ def build_in_container(
container.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 # Install the wheel we just built
# Note: If auditwheel produced two wheels, it's because the earlier produced wheel
# conforms to multiple manylinux standards. These multiple versions of the wheel are
# functionally the same, differing only in name, wheel metadata, and possibly include
# 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]
container.call( container.call(
[*pip, "install", str(repaired_wheel) + build_options.test_extras], ["pip", "install", str(wheel_to_test) + build_options.test_extras],
env=virtualenv_env, env=virtualenv_env,
) )
# Install any requirements to run the tests # Install any requirements to run the tests
if build_options.test_requires: if build_options.test_requires:
container.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 # Run the tests from a different directory
test_command_prepared = prepare_command( test_command_prepared = prepare_command(
build_options.test_command, build_options.test_command,
project=container_project_path, project=container_project_path,
package=container_package_dir, package=container_package_dir,
wheel=repaired_wheel, wheel=wheel_to_test,
) )
test_cwd = testing_temp_dir / "test_cwd" test_cwd = testing_temp_dir / "test_cwd"
container.call(["mkdir", "-p", test_cwd]) container.call(["mkdir", "-p", test_cwd])
container.copy_into(test_fail_cwd_file, test_cwd / "test_fail.py")
if build_options.test_sources:
copy_test_sources(
build_options.test_sources,
Path.cwd(),
test_cwd,
copy_into=container.copy_into,
)
else:
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
container.copy_into(resources.TEST_FAIL_CWD_FILE, test_cwd / "test_fail.py")
container.call(["sh", "-c", test_command_prepared], cwd=test_cwd, env=virtualenv_env) container.call(["sh", "-c", test_command_prepared], cwd=test_cwd, env=virtualenv_env)
# clean up test environment # clean up test environment
container.call(["rm", "-rf", testing_temp_dir]) container.call(["rm", "-rf", testing_temp_dir])
# move repaired wheel to output # move repaired wheels to output
output_wheel: Path | None = None
if compatible_wheel is None: if compatible_wheel is None:
container.call(["mkdir", "-p", container_output_dir]) container.call(["mkdir", "-p", container_output_dir])
container.call(["mv", repaired_wheel, container_output_dir]) container.call(["mv", *repaired_wheels, container_output_dir])
built_wheels.append(container_output_dir / repaired_wheel.name) built_wheels.extend(
output_wheel = options.globals.output_dir / repaired_wheel.name container_output_dir / repaired_wheel.name for repaired_wheel in repaired_wheels
)
log.build_end(output_wheel) log.build_end()
log.step("Copying wheels back to host...") log.step("Copying wheels back to host...")
# copy the output back into the host # copy the output back into the host
@@ -436,7 +387,30 @@ def build_in_container(
log.step_end() log.step_end()
def build(options: Options, tmp_path: Path) -> None: def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
try:
# check the container engine is installed
subprocess.run(
[options.globals.container_engine.name, "--version"],
check=True,
stdout=subprocess.DEVNULL,
)
except subprocess.CalledProcessError:
print(
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.
If you're building on Cirrus CI, use `docker_builder` task.
"""
),
file=sys.stderr,
)
sys.exit(2)
python_configurations = get_python_configurations( python_configurations = get_python_configurations(
options.globals.build_selector, options.globals.architectures options.globals.build_selector, options.globals.architectures
) )
@@ -451,38 +425,17 @@ def build(options: Options, tmp_path: Path) -> None:
container_package_dir = container_project_path / abs_package_dir.relative_to(cwd) container_package_dir = container_project_path / abs_package_dir.relative_to(cwd)
for build_step in get_build_steps(options, python_configurations): for build_step in get_build_steps(options, python_configurations):
try:
# check the container engine is installed
subprocess.run(
[build_step.container_engine.name, "--version"],
check=True,
stdout=subprocess.DEVNULL,
)
except subprocess.CalledProcessError as error:
msg = unwrap(
f"""
{build_step.container_engine.name} 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. If
you're building on Cirrus CI, use `docker_builder` task.
"""
)
raise errors.ConfigurationError(msg) from error
try: try:
ids_to_build = [x.identifier for x in build_step.platform_configs] ids_to_build = [x.identifier for x in build_step.platform_configs]
log.step(f"Starting container image {build_step.container_image}...") log.step(f"Starting container image {build_step.container_image}...")
print(f"info: This container will host the build for {', '.join(ids_to_build)}...") print(f"info: This container will host the build for {', '.join(ids_to_build)}...")
architecture = Architecture(build_step.platform_tag.split("_", 1)[1])
with OCIContainer( with OCIContainer(
image=build_step.container_image, image=build_step.container_image,
oci_platform=ARCHITECTURE_OCI_PLATFORM_MAP[architecture], enforce_32_bit=build_step.platform_tag.endswith("i686"),
cwd=container_project_path, cwd=container_project_path,
engine=build_step.container_engine, engine=options.globals.container_engine,
) as container: ) as container:
build_in_container( build_in_container(
options=options, options=options,
@@ -490,13 +443,14 @@ def build(options: Options, tmp_path: Path) -> None:
container=container, container=container,
container_project_path=container_project_path, container_project_path=container_project_path,
container_package_dir=container_package_dir, container_package_dir=container_package_dir,
local_tmp_dir=tmp_path,
) )
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
log.step_end_with_error(
f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}"
)
troubleshoot(options, error) troubleshoot(options, error)
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}" sys.exit(1)
raise errors.FatalError(msg) from error
def _matches_prepared_command(error_cmd: Sequence[str], command_template: str) -> bool: def _matches_prepared_command(error_cmd: Sequence[str], command_template: str) -> bool:
@@ -515,6 +469,7 @@ def troubleshoot(options: Options, error: Exception) -> None:
) # TODO allow matching of overrides too? ) # TODO allow matching of overrides too?
): ):
# the wheel build step or the repair step failed # the wheel build step or the repair step failed
print("Checking for common errors...")
so_files = list(options.globals.package_dir.glob("**/*.so")) so_files = list(options.globals.package_dir.glob("**/*.so"))
if so_files: if so_files:
+58 -209
View File
@@ -1,26 +1,15 @@
from __future__ import annotations
import codecs import codecs
import contextlib
import dataclasses
import functools
import hashlib
import io
import os import os
import re import re
import sys import sys
import textwrap
import time import time
from collections.abc import Generator from typing import IO, AnyStr, Final, Tuple
from pathlib import Path
from typing import IO, TYPE_CHECKING, AnyStr, Final, Literal
import humanize from .util import CIProvider, detect_ci_provider
from .ci import CIProvider, detect_ci_provider, filter_ansi_codes FoldPattern = Tuple[str, str]
if TYPE_CHECKING:
from .options import Options
FoldPattern = tuple[str, str]
DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "") DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "")
FOLD_PATTERNS: Final[dict[str, FoldPattern]] = { FOLD_PATTERNS: Final[dict[str, FoldPattern]] = {
"azure": ("##[group]{name}", "##[endgroup]"), "azure": ("##[group]{name}", "##[endgroup]"),
@@ -34,92 +23,28 @@ PLATFORM_IDENTIFIER_DESCRIPTIONS: Final[dict[str, str]] = {
"manylinux_aarch64": "manylinux aarch64", "manylinux_aarch64": "manylinux aarch64",
"manylinux_ppc64le": "manylinux ppc64le", "manylinux_ppc64le": "manylinux ppc64le",
"manylinux_s390x": "manylinux s390x", "manylinux_s390x": "manylinux s390x",
"manylinux_armv7l": "manylinux armv7l",
"manylinux_riscv64": "manylinux riscv64",
"musllinux_x86_64": "musllinux x86_64", "musllinux_x86_64": "musllinux x86_64",
"musllinux_i686": "musllinux i686", "musllinux_i686": "musllinux i686",
"musllinux_aarch64": "musllinux aarch64", "musllinux_aarch64": "musllinux aarch64",
"musllinux_ppc64le": "musllinux ppc64le", "musllinux_ppc64le": "musllinux ppc64le",
"musllinux_s390x": "musllinux s390x", "musllinux_s390x": "manylinux s390x",
"musllinux_armv7l": "musllinux armv7l",
"musllinux_riscv64": "musllinux riscv64",
"win32": "Windows 32bit", "win32": "Windows 32bit",
"win_amd64": "Windows 64bit", "win_amd64": "Windows 64bit",
"win_arm64": "Windows on ARM 64bit", "win_arm64": "Windows on ARM 64bit",
"macosx_x86_64": "macOS x86_64", "macosx_x86_64": "macOS x86_64",
"macosx_universal2": "macOS Universal 2 - x86_64 and arm64", "macosx_universal2": "macOS Universal 2 - x86_64 and arm64",
"macosx_arm64": "macOS arm64 - Apple Silicon", "macosx_arm64": "macOS arm64 - Apple Silicon",
"pyodide_wasm32": "Pyodide",
"android_arm64_v8a": "Android arm64_v8a",
"android_x86_64": "Android x86_64",
"ios_arm64_iphoneos": "iOS Device (ARM64)",
"ios_arm64_iphonesimulator": "iOS Simulator (ARM64)",
"ios_x86_64_iphonesimulator": "iOS Simulator (x86_64)",
} }
class Colors:
def __init__(self, *, enabled: bool) -> None:
self.red = "\033[31m" if enabled else ""
self.green = "\033[32m" if enabled else ""
self.yellow = "\033[33m" if enabled else ""
self.blue = "\033[34m" if enabled else ""
self.cyan = "\033[36m" if enabled else ""
self.bright_red = "\033[91m" if enabled else ""
self.bright_green = "\033[92m" if enabled else ""
self.white = "\033[37m\033[97m" if enabled else ""
self.gray = "\033[38;5;244m" if enabled else ""
self.bg_grey = "\033[48;5;235m" if enabled else ""
self.bold = "\033[1m" if enabled else ""
self.faint = "\033[2m" if enabled else ""
self.end = "\033[0m" if enabled else ""
class Symbols:
def __init__(self, *, unicode: bool) -> None:
self.done = "" if unicode else "done"
self.error = "" if unicode else "failed"
@dataclasses.dataclass(kw_only=True, frozen=True)
class BuildInfo:
identifier: str
filename: Path | None
duration: float
@functools.cached_property
def size(self) -> str | None:
if self.filename is None:
return None
return humanize.naturalsize(self.filename.stat().st_size)
@functools.cached_property
def sha256(self) -> str | None:
if self.filename is None:
return None
with self.filename.open("rb") as f:
digest = hashlib.file_digest(f, "sha256")
return digest.hexdigest()
def __str__(self) -> str:
duration = humanize.naturaldelta(self.duration)
if self.filename:
return f"{self.identifier}: {self.filename.name} {self.size} in {duration}, SHA256={self.sha256}"
return f"{self.identifier}: {duration} (test only)"
class Logger: class Logger:
fold_mode: Literal["azure", "github", "travis", "disabled"] fold_mode: str
colors_enabled: bool colors_enabled: bool
unicode_enabled: bool unicode_enabled: bool
active_build_identifier: str | None = None active_build_identifier: str | None = None
build_start_time: float | None = None build_start_time: float | None = None
step_start_time: float | None = None step_start_time: float | None = None
active_fold_group_name: str | None = None active_fold_group_name: str | None = None
summary: list[BuildInfo]
def __init__(self) -> None: def __init__(self) -> None:
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"): if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
@@ -131,28 +56,25 @@ class Logger:
ci_provider = detect_ci_provider() ci_provider = detect_ci_provider()
match ci_provider: if ci_provider == CIProvider.azure_pipelines:
case CIProvider.azure_pipelines: self.fold_mode = "azure"
self.fold_mode = "azure" self.colors_enabled = True
self.colors_enabled = True
case CIProvider.github_actions: elif ci_provider == CIProvider.github_actions:
self.fold_mode = "github" self.fold_mode = "github"
self.colors_enabled = True self.colors_enabled = True
case CIProvider.travis_ci: elif ci_provider == CIProvider.travis_ci:
self.fold_mode = "travis" self.fold_mode = "travis"
self.colors_enabled = True self.colors_enabled = True
case CIProvider.appveyor: elif ci_provider == CIProvider.appveyor:
self.fold_mode = "disabled" self.fold_mode = "disabled"
self.colors_enabled = True self.colors_enabled = True
case _: else:
self.fold_mode = "disabled" self.fold_mode = "disabled"
self.colors_enabled = file_supports_color(sys.stdout) self.colors_enabled = file_supports_color(sys.stdout)
self.summary = []
def build_start(self, identifier: str) -> None: def build_start(self, identifier: str) -> None:
self.step_end() self.step_end()
@@ -166,7 +88,7 @@ class Logger:
self.build_start_time = time.time() self.build_start_time = time.time()
self.active_build_identifier = identifier self.active_build_identifier = identifier
def build_end(self, filename: Path | None) -> None: def build_end(self) -> None:
assert self.build_start_time is not None assert self.build_start_time is not None
assert self.active_build_identifier is not None assert self.active_build_identifier is not None
self.step_end() self.step_end()
@@ -174,14 +96,11 @@ class Logger:
c = self.colors c = self.colors
s = self.symbols s = self.symbols
duration = time.time() - self.build_start_time duration = time.time() - self.build_start_time
duration_str = humanize.naturaldelta(duration, minimum_unit="milliseconds")
print() print()
print(f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration_str}") print(
self.summary.append( f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration:.2f}s"
BuildInfo(identifier=self.active_build_identifier, filename=filename, duration=duration)
) )
self.build_start_time = None self.build_start_time = None
self.active_build_identifier = None self.active_build_identifier = None
@@ -196,7 +115,6 @@ class Logger:
c = self.colors c = self.colors
s = self.symbols s = self.symbols
duration = time.time() - self.step_start_time duration = time.time() - self.step_start_time
if success: if success:
print(f"{c.green}{s.done} {c.end}{duration:.2f}s".rjust(78)) print(f"{c.green}{s.done} {c.end}{duration:.2f}s".rjust(78))
else: else:
@@ -214,48 +132,24 @@ class Logger:
def notice(self, message: str) -> None: def notice(self, message: str) -> None:
if self.fold_mode == "github": if self.fold_mode == "github":
print(f"::notice::cibuildwheel: {message}\n", file=sys.stderr) print(f"::notice::{message}\n", file=sys.stderr)
else: else:
c = self.colors c = self.colors
print(f"cibuildwheel: {c.bold}note{c.end}: {message}\n", file=sys.stderr) print(f"{c.bold}Note{c.end}: {message}\n", file=sys.stderr)
def warning(self, message: str) -> None: def warning(self, message: str) -> None:
if self.fold_mode == "github": if self.fold_mode == "github":
print(f"::warning::cibuildwheel: {message}\n", file=sys.stderr) print(f"::warning::{message}\n", file=sys.stderr)
else: else:
c = self.colors c = self.colors
print(f"cibuildwheel: {c.yellow}warning{c.end}: {message}\n", file=sys.stderr) print(f"{c.yellow}Warning{c.end}: {message}\n", file=sys.stderr)
def error(self, error: BaseException | str) -> None: def error(self, error: BaseException | str) -> None:
if self.fold_mode == "github": if self.fold_mode == "github":
print(f"::error::cibuildwheel: {error}\n", file=sys.stderr) print(f"::error::{error}\n", file=sys.stderr)
else: else:
c = self.colors c = self.colors
print(f"cibuildwheel: {c.bright_red}error{c.end}: {error}\n", file=sys.stderr) print(f"{c.bright_red}Error{c.end}: {error}\n", file=sys.stderr)
@contextlib.contextmanager
def print_summary(self, *, options: "Options") -> Generator[None, None, None]:
start = time.time()
yield
duration = time.time() - start
if summary_path := os.environ.get("GITHUB_STEP_SUMMARY"):
github_summary = self._github_step_summary(duration=duration, options=options)
Path(summary_path).write_text(filter_ansi_codes(github_summary), encoding="utf-8")
n_wheels = len([info for info in self.summary if info.filename])
s = "s" if n_wheels > 1 else ""
duration_str = humanize.naturaldelta(duration)
print()
self._start_fold_group(f"{n_wheels} wheel{s} produced in {duration_str}")
for build_info in self.summary:
print(" ", build_info)
self._end_fold_group()
self.summary = []
@property
def step_active(self) -> bool:
return self.step_start_time is not None
def _start_fold_group(self, name: str) -> None: def _start_fold_group(self, name: str) -> None:
self._end_fold_group() self._end_fold_group()
@@ -292,72 +186,6 @@ class Logger:
# lowercase, shorten # lowercase, shorten
return identifier.lower()[:20] return identifier.lower()[:20]
def _github_step_summary(self, duration: float, options: "Options") -> str:
"""
Returns the GitHub step summary, in markdown format.
"""
out = io.StringIO()
options_summary = options.summary(
identifiers=[bi.identifier for bi in self.summary], skip_unset=True
)
out.write(
textwrap.dedent("""\
### 🎡 cibuildwheel
<details>
<summary>
Build options
</summary>
```yaml
{options_summary}
```
</details>
""").format(options_summary=options_summary)
)
n_wheels = len([b for b in self.summary if b.filename])
wheel_rows = "\n".join(
"<tr>"
f"<td nowrap>{'<samp>' + b.filename.name + '</samp>' if b.filename else '*Test only*'}</td>"
f"<td nowrap>{b.size or 'N/A'}</td>"
f"<td nowrap><samp>{b.identifier}</samp></td>"
f"<td nowrap>{humanize.naturaldelta(b.duration)}</td>"
f"<td nowrap><samp>{b.sha256 or 'N/A'}</samp></td>"
"</tr>"
for b in self.summary
)
out.write(
textwrap.dedent("""\
<table>
<thead>
<tr>
<th align="left">Wheel</th>
<th align="left">Size</th>
<th align="left">Build identifier</th>
<th align="left">Time</th>
<th align="left">SHA256</th>
</tr>
</thead>
<tbody>
{wheel_rows}
</tbody>
</table>
<div align="right"><sup>{n} wheel{s} created in {duration_str}</sup></div>
""").format(
wheel_rows=wheel_rows,
n=n_wheels,
duration_str=humanize.naturaldelta(duration),
s="s" if n_wheels > 1 else "",
)
)
out.write("\n")
out.write("---")
out.write("\n")
return out.getvalue()
@property @property
def colors(self) -> Colors: def colors(self) -> Colors:
return Colors(enabled=self.colors_enabled) return Colors(enabled=self.colors_enabled)
@@ -373,22 +201,17 @@ def build_description_from_identifier(identifier: str) -> str:
build_description = "" build_description = ""
python_interpreter = python_identifier[0:2] python_interpreter = python_identifier[0:2]
version_parts = python_identifier[2:].split("_") python_version = python_identifier[2:]
python_version = version_parts[0]
if python_interpreter == "cp": if python_interpreter == "cp":
build_description += "CPython" build_description += "CPython"
elif python_interpreter == "pp": elif python_interpreter == "pp":
build_description += "PyPy" build_description += "PyPy"
elif python_interpreter == "gp":
build_description += "GraalPy"
else: else:
msg = f"unknown python {python_interpreter!r}" msg = f"unknown python {python_interpreter!r}"
raise Exception(msg) raise Exception(msg)
build_description += f" {python_version[0]}.{python_version[1:]} " build_description += f" {python_version[0]}.{python_version[1:]} "
if len(version_parts) > 1:
build_description += f"(ABI {version_parts[1]}) "
try: try:
build_description += PLATFORM_IDENTIFIER_DESCRIPTIONS[platform_identifier] build_description += PLATFORM_IDENTIFIER_DESCRIPTIONS[platform_identifier]
@@ -399,6 +222,32 @@ def build_description_from_identifier(identifier: str) -> str:
return build_description return build_description
class Colors:
def __init__(self, *, enabled: bool) -> None:
self.red = "\033[31m" if enabled else ""
self.green = "\033[32m" if enabled else ""
self.yellow = "\033[33m" if enabled else ""
self.blue = "\033[34m" if enabled else ""
self.cyan = "\033[36m" if enabled else ""
self.bright_red = "\033[91m" if enabled else ""
self.bright_green = "\033[92m" if enabled else ""
self.white = "\033[37m\033[97m" if enabled else ""
self.gray = "\033[38;5;244m" if enabled else ""
self.bg_grey = "\033[48;5;235m" if enabled else ""
self.bold = "\033[1m" if enabled else ""
self.faint = "\033[2m" if enabled else ""
self.end = "\033[0m" if enabled else ""
class Symbols:
def __init__(self, *, unicode: bool) -> None:
self.done = "" if unicode else "done"
self.error = "" if unicode else "failed"
def file_supports_color(file_obj: IO[AnyStr]) -> bool: def file_supports_color(file_obj: IO[AnyStr]) -> bool:
""" """
Returns True if the running system's terminal supports color. Returns True if the running system's terminal supports color.
@@ -1,6 +1,7 @@
import dataclasses from __future__ import annotations
import contextlib
import functools import functools
import inspect
import os import os
import platform import platform
import re import re
@@ -8,35 +9,43 @@ import shutil
import subprocess import subprocess
import sys import sys
import typing import typing
from collections.abc import Set from collections.abc import Sequence, Set
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Literal, assert_never from typing import Literal, Tuple
from filelock import FileLock from filelock import FileLock
from packaging.version import Version
from .. import errors from ._compat.typing import assert_never
from ..architecture import Architecture from .architecture import Architecture
from ..ci import detect_ci_provider from .environment import ParsedEnvironment
from ..environment import ParsedEnvironment from .logger import log
from ..frontend import BuildFrontendName, get_build_frontend_extra_flags from .options import Options
from ..logger import log from .typing import PathOrStr
from ..options import Options from .util import (
from ..selector import BuildSelector
from ..util import resources
from ..util.cmd import call, shell
from ..util.file import (
CIBW_CACHE_PATH, CIBW_CACHE_PATH,
copy_test_sources, AlreadyBuiltWheelError,
BuildFrontendConfig,
BuildFrontendName,
BuildSelector,
NonPlatformWheelError,
call,
detect_ci_provider,
download, download,
move_file, find_compatible_wheel,
get_build_verbosity_extra_flags,
get_pip_version,
install_certifi_script,
prepare_command,
read_python_configs,
shell,
split_config_settings,
test_fail_cwd_file,
unwrap,
virtualenv,
) )
from ..util.helpers import prepare_command, unwrap
from ..util.packaging import find_compatible_wheel, get_pip_version
from ..venv import constraint_flags, find_uv, virtualenv
@functools.cache
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) Returns the macOS major/minor version, as a tuple, e.g. (10, 15) or (11, 0)
@@ -48,27 +57,7 @@ def get_macos_version() -> tuple[int, int]:
""" """
version_str, _, _ = platform.mac_ver() version_str, _, _ = platform.mac_ver()
version = tuple(map(int, version_str.split(".")[:2])) version = tuple(map(int, version_str.split(".")[:2]))
if (10, 15) < version < (11, 0): return typing.cast(Tuple[int, int], version)
# When built against an older macOS SDK, Python will report macOS 10.16
# instead of the real version.
version_str = call(
sys.executable,
"-sS",
"-c",
"import platform; print(platform.mac_ver()[0])",
env={"SYSTEM_VERSION_COMPAT": "0"},
capture_stdout=True,
)
version = tuple(map(int, version_str.split(".")[:2]))
return typing.cast("tuple[int, int]", version)
@functools.cache
def get_test_macosx_deployment_target() -> str:
version = get_macos_version()
if version >= (11, 0):
return f"{version[0]}.0"
return f"{version[0]}.{version[1]}"
def get_macos_sdks() -> list[str]: def get_macos_sdks() -> list[str]:
@@ -76,22 +65,19 @@ def get_macos_sdks() -> list[str]:
return [m.group(1) for m in re.finditer(r"-sdk (macosx\S+)", output)] return [m.group(1) for m in re.finditer(r"-sdk (macosx\S+)", output)]
@dataclasses.dataclass(frozen=True, kw_only=True) @dataclass(frozen=True)
class PythonConfiguration: class PythonConfiguration:
version: str version: str
identifier: str identifier: str
url: str url: str
def all_python_configurations() -> list[PythonConfiguration]:
config_dicts = resources.read_python_configs("macos")
return [PythonConfiguration(**item) for item in config_dicts]
def get_python_configurations( def get_python_configurations(
build_selector: BuildSelector, architectures: Set[Architecture] build_selector: BuildSelector, architectures: Set[Architecture]
) -> list[PythonConfiguration]: ) -> list[PythonConfiguration]:
python_configurations = all_python_configurations() full_python_configs = read_python_configs("macos")
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
# filter out configs that don't match any of the selected architectures # filter out configs that don't match any of the selected architectures
python_configurations = [ python_configurations = [
@@ -103,7 +89,7 @@ def get_python_configurations(
# skip builds as required by BUILD/SKIP # skip builds as required by BUILD/SKIP
python_configurations = [c for c in python_configurations if build_selector(c.identifier)] python_configurations = [c for c in python_configurations if build_selector(c.identifier)]
# filter-out some cross-compilation configs with PyPy and GraalPy: # filter-out some cross-compilation configs with PyPy:
# can't build arm64 on x86_64 # can't build arm64 on x86_64
# rosetta allows to build x86_64 on arm64 # rosetta allows to build x86_64 on arm64
if platform.machine() == "x86_64": if platform.machine() == "x86_64":
@@ -111,7 +97,7 @@ def get_python_configurations(
python_configurations = [ python_configurations = [
c c
for c in python_configurations for c in python_configurations
if not (c.identifier.startswith(("pp", "gp")) and c.identifier.endswith("arm64")) if not (c.identifier.startswith("pp") and c.identifier.endswith("arm64"))
] ]
removed_elements = python_configurations_before - set(python_configurations) removed_elements = python_configurations_before - set(python_configurations)
if removed_elements: if removed_elements:
@@ -119,7 +105,7 @@ def get_python_configurations(
log.quiet( log.quiet(
unwrap( unwrap(
f""" f"""
Note: {ids} {"was" if len(removed_elements) == 1 else "were"} Note: {ids} {'was' if len(removed_elements) == 1 else 'were'}
selected, but can't be built on x86_64 so will be skipped automatically. selected, but can't be built on x86_64 so will be skipped automatically.
""" """
) )
@@ -128,55 +114,35 @@ def get_python_configurations(
return python_configurations return python_configurations
def install_cpython(_tmp: Path, version: str, url: str, free_threading: bool) -> Path: def install_cpython(tmp: Path, version: str, url: str) -> Path:
ft = "T" if free_threading else "" installation_path = Path(f"/Library/Frameworks/Python.framework/Versions/{version}")
installation_path = Path(f"/Library/Frameworks/Python{ft}.framework/Versions/{version}")
with FileLock(CIBW_CACHE_PATH / f"cpython{version}.lock"): with FileLock(CIBW_CACHE_PATH / f"cpython{version}.lock"):
installed_system_packages = call("pkgutil", "--pkgs", capture_stdout=True).splitlines() installed_system_packages = call("pkgutil", "--pkgs", capture_stdout=True).splitlines()
# if this version of python isn't installed, get it from python.org and install # if this version of python isn't installed, get it from python.org and install
python_package_identifier = f"org.python.Python.Python{ft}Framework-{version}" python_package_identifier = f"org.python.Python.PythonFramework-{version}"
if python_package_identifier not in installed_system_packages: if python_package_identifier not in installed_system_packages:
if detect_ci_provider() is None: if detect_ci_provider() is None:
# if running locally, we don't want to install CPython with sudo # if running locally, we don't want to install CPython with sudo
# let the user know & provide a link to the installer # let the user know & provide a link to the installer
msg = inspect.cleandoc( print(
f""" f"Error: CPython {version} is not installed.\n"
Framework CPython {version} not detected as installed in: "cibuildwheel will not perform system-wide installs when running outside of CI.\n"
{installation_path} f"To build locally, install CPython {version} on this machine, or, disable this version of Python using CIBW_SKIP=cp{version.replace('.', '')}-macosx_*\n"
cibuildwheel will not perform system-wide installs when running outside of CI. f"\nDownload link: {url}",
To build locally, install CPython {version} on this machine, or, disable this file=sys.stderr,
version of Python using CIBW_SKIP=cp{version.replace(".", "")}-macosx_*
For portable builds, cibuildwheel needs the official builds from python.org.
Download link: {url}
"""
) )
raise errors.FatalError(msg) raise SystemExit(1)
python_filename = url.split("/")[-1] pkg_path = tmp / "Python.pkg"
pkg_path = CIBW_CACHE_PATH / "cpython-installer" / python_filename # download the pkg
if not pkg_path.exists(): download(url, pkg_path)
download(url, pkg_path) # install
args = [] call("sudo", "installer", "-pkg", pkg_path, "-target", "/")
if version.startswith("3.13"):
# Python 3.13 is the first version to have a free-threading option
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_313.resolve())]
elif version.startswith("3.14"):
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_314.resolve())]
call("sudo", "installer", "-pkg", pkg_path, *args, "-target", "/")
pkg_path.unlink() pkg_path.unlink()
env = os.environ.copy() env = os.environ.copy()
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
call(installation_path / "bin" / "python3", install_certifi_script, env=env)
if free_threading: return installation_path / "bin" / "python3"
call(installation_path / f"bin/python{version}t", "-m", "ensurepip", env=env)
call(
installation_path / f"bin/python{version}t",
resources.INSTALL_CERTIFI_SCRIPT,
env=env,
)
else:
call(installation_path / "bin/python3", resources.INSTALL_CERTIFI_SCRIPT, env=env)
return installation_path / "bin" / (f"python{version}t" if free_threading else "python3")
def install_pypy(tmp: Path, url: str) -> Path: def install_pypy(tmp: Path, url: str) -> Path:
@@ -194,61 +160,28 @@ def install_pypy(tmp: Path, url: str) -> Path:
return installation_path / "bin" / "pypy3" return installation_path / "bin" / "pypy3"
def install_graalpy(tmp: Path, url: str) -> Path:
graalpy_archive = url.rsplit("/", 1)[-1]
extension = ".tar.gz"
assert graalpy_archive.endswith(extension)
installation_path = CIBW_CACHE_PATH / graalpy_archive[: -len(extension)]
with FileLock(str(installation_path) + ".lock"):
if not installation_path.exists():
downloaded_archive = tmp / graalpy_archive
download(url, downloaded_archive)
installation_path.mkdir(parents=True)
# GraalPy top-folder name is inconsistent with archive name
call("tar", "-C", installation_path, "--strip-components=1", "-xzf", downloaded_archive)
downloaded_archive.unlink()
return installation_path / "bin" / "graalpy"
def setup_python( def setup_python(
tmp: Path, tmp: Path,
python_configuration: PythonConfiguration, python_configuration: PythonConfiguration,
dependency_constraint: Path | None, dependency_constraint_flags: Sequence[PathOrStr],
environment: ParsedEnvironment, environment: ParsedEnvironment,
build_frontend: BuildFrontendName, build_frontend: BuildFrontendName,
) -> tuple[Path, dict[str, str]]: ) -> dict[str, str]:
uv_path = find_uv()
use_uv = build_frontend == "build[uv]"
tmp.mkdir() tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0] implementation_id = python_configuration.identifier.split("-")[0]
log.step(f"Installing Python {implementation_id}...") log.step(f"Installing Python {implementation_id}...")
if implementation_id.startswith("cp"): if implementation_id.startswith("cp"):
free_threading = "t-macos" in python_configuration.identifier base_python = install_cpython(tmp, python_configuration.version, python_configuration.url)
base_python = install_cpython(
tmp, python_configuration.version, python_configuration.url, free_threading
)
elif implementation_id.startswith("pp"): elif implementation_id.startswith("pp"):
base_python = install_pypy(tmp, python_configuration.url) base_python = install_pypy(tmp, python_configuration.url)
elif implementation_id.startswith("gp"):
base_python = install_graalpy(tmp, python_configuration.url)
else: else:
msg = "Unknown Python implementation" msg = "Unknown Python implementation"
raise ValueError(msg) raise ValueError(msg)
assert base_python.exists(), ( assert base_python.exists()
f"{base_python.name} not found, has {list(base_python.parent.iterdir())}"
)
log.step("Setting up build environment...") log.step("Setting up build environment...")
venv_path = tmp / "venv" venv_path = tmp / "venv"
env = virtualenv( env = virtualenv(base_python, venv_path, dependency_constraint_flags)
python_configuration.version,
base_python,
venv_path,
dependency_constraint,
use_uv=use_uv,
)
venv_bin_path = venv_path / "bin" venv_bin_path = venv_path / "bin"
assert venv_bin_path.exists() assert venv_bin_path.exists()
# Fix issue with site.py setting the wrong `sys.prefix`, `sys.exec_prefix`, # Fix issue with site.py setting the wrong `sys.prefix`, `sys.exec_prefix`,
@@ -262,26 +195,45 @@ def setup_python(
# we version pip ourselves, so we don't care about pip version checking # we version pip ourselves, so we don't care about pip version checking
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
# upgrade pip to the version matching our constraints
# if necessary, reinstall it to ensure that it's available on PATH as 'pip'
call(
"python",
"-m",
"pip",
"install",
"--upgrade",
"pip",
*dependency_constraint_flags,
env=env,
cwd=venv_path,
)
# Apply our environment after pip is ready # Apply our environment after pip is ready
env = environment.as_dictionary(prev_environment=env) env = environment.as_dictionary(prev_environment=env)
# check what Python version we're on
which_python = call("which", "python", env=env, capture_stdout=True).strip()
print(which_python)
if which_python != str(venv_bin_path / "python"):
msg = "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."
raise errors.FatalError(msg)
call("python", "--version", env=env)
# check what pip version we're on # check what pip version we're on
if not use_uv: assert (venv_bin_path / "pip").exists()
assert (venv_bin_path / "pip").exists() call("which", "pip", env=env)
which_pip = call("which", "pip", env=env, capture_stdout=True).strip() call("pip", "--version", env=env)
print(which_pip) which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
if which_pip != str(venv_bin_path / "pip"): if which_pip != str(venv_bin_path / "pip"):
msg = "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." print(
raise errors.FatalError(msg) "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.",
call("pip", "--version", env=env) file=sys.stderr,
)
sys.exit(1)
# check what Python version we're on
call("which", "python", env=env)
call("python", "--version", env=env)
which_python = call("which", "python", env=env, capture_stdout=True).strip()
if which_python != str(venv_bin_path / "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)
config_is_arm64 = python_configuration.identifier.endswith("arm64") config_is_arm64 = python_configuration.identifier.endswith("arm64")
config_is_universal2 = python_configuration.identifier.endswith("universal2") config_is_universal2 = python_configuration.identifier.endswith("universal2")
@@ -289,42 +241,23 @@ def setup_python(
# Set MACOSX_DEPLOYMENT_TARGET, if the user didn't set it. # Set MACOSX_DEPLOYMENT_TARGET, if the user didn't set it.
# For arm64, the minimal deployment target is 11.0. # For arm64, the minimal deployment target is 11.0.
# On x86_64 (or universal2), use 10.9 as a default. # On x86_64 (or universal2), use 10.9 as a default.
# CPython 3.12.6+ needs 10.13. # PyPy defaults to 10.7, causing inconsistencies if it's left unset.
# CPython 3.14.0 needs 10.15. env.setdefault("MACOSX_DEPLOYMENT_TARGET", "11.0" if config_is_arm64 else "10.9")
if config_is_arm64:
default_target = "11.0"
elif Version(python_configuration.version) >= Version("3.14"):
default_target = "10.15"
elif Version(python_configuration.version) >= Version("3.12"):
default_target = "10.13"
elif python_configuration.identifier.startswith("pp") and Version(
python_configuration.version
) >= Version("3.9"):
default_target = "10.15"
else:
default_target = "10.9"
env.setdefault("MACOSX_DEPLOYMENT_TARGET", default_target)
# This is a floor, it can't be set lower than the default_target. if python_configuration.version not in {"3.6", "3.7"}:
if Version(env["MACOSX_DEPLOYMENT_TARGET"]) < Version(default_target): if config_is_arm64:
log.warning( # macOS 11 is the first OS with arm64 support, so the wheels
f"Bumping MACOSX_DEPLOYMENT_TARGET ({env['MACOSX_DEPLOYMENT_TARGET']}) to the minimum required ({default_target})." # have that as a minimum.
) env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-11.0-arm64")
env["MACOSX_DEPLOYMENT_TARGET"] = default_target env.setdefault("ARCHFLAGS", "-arch arm64")
elif config_is_universal2:
if config_is_arm64: env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-universal2")
# macOS 11 is the first OS with arm64 support, so the wheels env.setdefault("ARCHFLAGS", "-arch arm64 -arch x86_64")
# have that as a minimum. elif python_configuration.identifier.endswith("x86_64"):
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-11.0-arm64") # even on the macos11.0 Python installer, on the x86_64 side it's
env.setdefault("ARCHFLAGS", "-arch arm64") # compatible back to 10.9.
elif config_is_universal2: env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-x86_64")
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-universal2") env.setdefault("ARCHFLAGS", "-arch x86_64")
env.setdefault("ARCHFLAGS", "-arch arm64 -arch x86_64")
elif python_configuration.identifier.endswith("x86_64"):
# even on the macos11.0 Python installer, on the x86_64 side it's
# compatible back to 10.9.
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-x86_64")
env.setdefault("ARCHFLAGS", "-arch x86_64")
building_arm64 = config_is_arm64 or config_is_universal2 building_arm64 = config_is_arm64 or config_is_universal2
if building_arm64 and get_macos_version() < (10, 16) and "SDKROOT" not in env: if building_arm64 and get_macos_version() < (10, 16) and "SDKROOT" not in env:
@@ -349,42 +282,31 @@ def setup_python(
env.setdefault("SDKROOT", arm64_compatible_sdks[0]) env.setdefault("SDKROOT", arm64_compatible_sdks[0])
log.step("Installing build tools...") log.step("Installing build tools...")
match build_frontend: if build_frontend == "pip":
case "pip": call(
call( "pip",
"pip", "install",
"install", "--upgrade",
"--upgrade", "setuptools",
"delocate", "wheel",
*constraint_flags(dependency_constraint), "delocate",
env=env, *dependency_constraint_flags,
) env=env,
case "build": )
call( elif build_frontend == "build":
"pip", call(
"install", "pip",
"--upgrade", "install",
"delocate", "--upgrade",
"build[virtualenv]", "delocate",
*constraint_flags(dependency_constraint), "build[virtualenv]",
env=env, *dependency_constraint_flags,
) env=env,
case "build[uv]": )
assert uv_path is not None else:
call( assert_never(build_frontend)
uv_path,
"pip",
"install",
"--upgrade",
"delocate",
"build[virtualenv, uv]",
*constraint_flags(dependency_constraint),
env=env,
)
case _:
assert_never(build_frontend)
return base_python, env return env
def build(options: Options, tmp_path: Path) -> None: def build(options: Options, tmp_path: Path) -> None:
@@ -412,13 +334,7 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations: for config in python_configurations:
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
use_uv = build_frontend.name == "build[uv]"
uv_path = find_uv()
if use_uv and uv_path is None:
msg = "uv not found"
raise AssertionError(msg)
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
log.build_start(config.identifier) log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier identifier_tmp_dir = tmp_path / config.identifier
@@ -429,18 +345,20 @@ def build(options: Options, tmp_path: Path) -> None:
config_is_arm64 = config.identifier.endswith("arm64") config_is_arm64 = config.identifier.endswith("arm64")
config_is_universal2 = config.identifier.endswith("universal2") config_is_universal2 = config.identifier.endswith("universal2")
constraints_path = build_options.dependency_constraints.get_for_python_version( dependency_constraint_flags: Sequence[PathOrStr] = []
version=config.version, tmp_dir=identifier_tmp_dir if build_options.dependency_constraints:
) dependency_constraint_flags = [
"-c",
build_options.dependency_constraints.get_for_python_version(config.version),
]
base_python, env = setup_python( env = setup_python(
identifier_tmp_dir / "build", identifier_tmp_dir / "build",
config, config,
constraints_path, dependency_constraint_flags,
build_options.environment, build_options.environment,
build_frontend.name, build_frontend.name,
) )
pip_version = None if use_uv else get_pip_version(env)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier) compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel: if compatible_wheel:
@@ -460,53 +378,58 @@ def build(options: Options, tmp_path: Path) -> None:
log.step("Building wheel...") log.step("Building wheel...")
built_wheel_dir.mkdir() built_wheel_dir.mkdir()
extra_flags = get_build_frontend_extra_flags( extra_flags = split_config_settings(
build_frontend, build_options.build_verbosity, build_options.config_settings build_options.config_settings, build_frontend.name
) )
extra_flags += build_frontend.args
build_env = env.copy() if build_frontend.name == "pip":
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
match build_frontend.name: # Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
case "pip": # see https://github.com/pypa/cibuildwheel/pull/369
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org call(
# see https://github.com/pypa/cibuildwheel/pull/369 "python",
call( "-m",
"python", "pip",
"-m", "wheel",
"pip", build_options.package_dir.resolve(),
"wheel", f"--wheel-dir={built_wheel_dir}",
build_options.package_dir.resolve(), "--no-deps",
f"--wheel-dir={built_wheel_dir}", *extra_flags,
"--no-deps", env=env,
*extra_flags, )
env=build_env, elif build_frontend.name == "build":
if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg)
build_env = env.copy()
if build_options.dependency_constraints:
constraint_path = (
build_options.dependency_constraints.get_for_python_version(
config.version
)
) )
case "build" | "build[uv]": build_env["PIP_CONSTRAINT"] = constraint_path.as_uri()
if ( build_env["VIRTUALENV_PIP"] = get_pip_version(env)
use_uv call(
and "--no-isolation" not in extra_flags "python",
and "-n" not in extra_flags "-m",
): "build",
extra_flags.append("--installer=uv") build_options.package_dir,
call( "--wheel",
"python", f"--outdir={built_wheel_dir}",
"-m", *extra_flags,
"build", env=build_env,
build_options.package_dir, )
"--wheel", else:
f"--outdir={built_wheel_dir}", assert_never(build_frontend)
*extra_flags,
env=build_env,
)
case _:
assert_never(build_frontend)
built_wheel = next(built_wheel_dir.glob("*.whl")) built_wheel = next(built_wheel_dir.glob("*.whl"))
repaired_wheel_dir.mkdir() repaired_wheel_dir.mkdir()
if built_wheel.name.endswith("none-any.whl"): if built_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError() raise NonPlatformWheelError()
if build_options.repair_command: if build_options.repair_command:
log.step("Repairing wheel...") log.step("Repairing wheel...")
@@ -523,20 +446,15 @@ def build(options: Options, tmp_path: Path) -> None:
wheel=built_wheel, wheel=built_wheel,
dest_dir=repaired_wheel_dir, dest_dir=repaired_wheel_dir,
delocate_archs=delocate_archs, delocate_archs=delocate_archs,
package=build_options.package_dir,
project=".",
) )
shell(repair_command_prepared, env=env) shell(repair_command_prepared, env=env)
else: else:
shutil.move(str(built_wheel), repaired_wheel_dir) shutil.move(str(built_wheel), repaired_wheel_dir)
try: repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
except StopIteration:
raise errors.RepairStepProducedNoWheelError() from None
if repaired_wheel.name in {wheel.name for wheel in built_wheels}: if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
raise errors.AlreadyBuiltWheelError(repaired_wheel.name) raise AlreadyBuiltWheelError(repaired_wheel.name)
log.step_end() log.step_end()
@@ -620,13 +538,17 @@ def build(options: Options, tmp_path: Path) -> None:
else f"Testing wheel on {testing_arch}..." else f"Testing wheel on {testing_arch}..."
) )
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
venv_dir = identifier_tmp_dir / "venv-test"
arch_prefix = [] arch_prefix = []
uv_arch_args = []
if testing_arch != machine_arch: if testing_arch != machine_arch:
if machine_arch == "arm64" and testing_arch == "x86_64": if machine_arch == "arm64" and testing_arch == "x86_64":
# rosetta2 will provide the emulation with just the arch prefix. # rosetta2 will provide the emulation with just the arch prefix.
arch_prefix = ["arch", "-x86_64"] arch_prefix = ["arch", "-x86_64"]
uv_arch_args = ["--python-platform", "x86_64-apple-darwin"]
else: else:
msg = f"don't know how to emulate {testing_arch} on {machine_arch}" msg = f"don't know how to emulate {testing_arch} on {machine_arch}"
raise RuntimeError(msg) raise RuntimeError(msg)
@@ -635,27 +557,16 @@ def build(options: Options, tmp_path: Path) -> None:
call_with_arch = functools.partial(call, *arch_prefix) call_with_arch = functools.partial(call, *arch_prefix)
shell_with_arch = functools.partial(call, *arch_prefix, "/bin/sh", "-c") shell_with_arch = functools.partial(call, *arch_prefix, "/bin/sh", "-c")
# set up a virtual environment to install and test from, to make sure # Use --no-download to ensure determinism by using seed libraries
# there are no dependencies that were pulled in at build time. # built into virtualenv
venv_dir = identifier_tmp_dir / f"venv-test-{testing_arch}" call_with_arch("python", "-m", "virtualenv", "--no-download", venv_dir, env=env)
virtualenv_env = virtualenv(
config.version,
base_python,
venv_dir,
None,
use_uv=use_uv,
env=env,
pip_version=pip_version,
)
if use_uv:
pip_install = functools.partial(call, *pip, "install", *uv_arch_args)
else:
pip_install = functools.partial(call_with_arch, *pip, "install")
virtualenv_env["MACOSX_DEPLOYMENT_TARGET"] = get_test_macosx_deployment_target() virtualenv_env = env.copy()
virtualenv_env["PATH"] = os.pathsep.join(
virtualenv_env = build_options.test_environment.as_dictionary( [
prev_environment=virtualenv_env str(venv_dir / "bin"),
virtualenv_env["PATH"],
]
) )
# check that we are using the Python from the virtual environment # check that we are using the Python from the virtual environment
@@ -670,33 +581,17 @@ def build(options: Options, tmp_path: Path) -> None:
shell_with_arch(before_test_prepared, env=virtualenv_env) shell_with_arch(before_test_prepared, env=virtualenv_env)
# install the wheel # install the wheel
if is_cp38 and python_arch == "x86_64": call_with_arch(
virtualenv_env_install_wheel = virtualenv_env.copy() "pip",
virtualenv_env_install_wheel["SYSTEM_VERSION_COMPAT"] = "0" "install",
log.notice(
unwrap(
"""
Setting SYSTEM_VERSION_COMPAT=0 to ensure CPython 3.8 can get
correct macOS version and allow installation of wheels with
MACOSX_DEPLOYMENT_TARGET >= 11.0.
See https://github.com/pypa/cibuildwheel/issues/1767 for the
details.
"""
)
)
else:
virtualenv_env_install_wheel = virtualenv_env
pip_install(
f"{repaired_wheel}{build_options.test_extras}", f"{repaired_wheel}{build_options.test_extras}",
env=virtualenv_env_install_wheel, env=virtualenv_env,
) )
# test the wheel # test the wheel
if build_options.test_requires: if build_options.test_requires:
pip_install( call_with_arch(
*build_options.test_requires, "pip", "install", *build_options.test_requires, env=virtualenv_env
env=virtualenv_env_install_wheel,
) )
# run the tests from a temp dir, with an absolute path in the command # run the tests from a temp dir, with an absolute path in the command
@@ -704,48 +599,31 @@ def build(options: Options, tmp_path: Path) -> None:
# and not the repo code) # and not the repo code)
test_command_prepared = prepare_command( test_command_prepared = prepare_command(
build_options.test_command, build_options.test_command,
project=Path.cwd(), project=Path(".").resolve(),
package=build_options.package_dir.resolve(), package=build_options.package_dir.resolve(),
wheel=repaired_wheel, wheel=repaired_wheel,
) )
test_cwd = identifier_tmp_dir / "test_cwd" test_cwd = identifier_tmp_dir / "test_cwd"
test_cwd.mkdir(exist_ok=True)
if build_options.test_sources: (test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
# only create test_cwd if it doesn't already exist - it
# may have been created during a previous `testing_arch`
if not test_cwd.exists():
test_cwd.mkdir()
copy_test_sources(
build_options.test_sources,
Path.cwd(),
test_cwd,
)
else:
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
test_cwd.mkdir(exist_ok=True)
(test_cwd / "test_fail.py").write_text(
resources.TEST_FAIL_CWD_FILE.read_text()
)
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env) shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
# we're all done here; move it to output (overwrite existing) # we're all done here; move it to output (overwrite existing)
output_wheel = None
if compatible_wheel is None: if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name) with contextlib.suppress(FileNotFoundError):
moved_wheel = move_file(repaired_wheel, output_wheel) (build_options.output_dir / repaired_wheel.name).unlink()
if moved_wheel != output_wheel.resolve():
log.warning( shutil.move(str(repaired_wheel), build_options.output_dir)
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}" built_wheels.append(build_options.output_dir / repaired_wheel.name)
)
built_wheels.append(output_wheel)
# clean up # clean up
shutil.rmtree(identifier_tmp_dir) shutil.rmtree(identifier_tmp_dir)
log.build_end(output_wheel) log.build_end()
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}" log.step_end_with_error(
raise errors.FatalError(msg) from error f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}"
)
sys.exit(1)
+64 -184
View File
@@ -1,4 +1,5 @@
import dataclasses from __future__ import annotations
import io import io
import json import json
import os import os
@@ -7,67 +8,35 @@ import shlex
import shutil import shutil
import subprocess import subprocess
import sys import sys
import textwrap
import typing import typing
import uuid import uuid
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from enum import Enum from dataclasses import dataclass
from pathlib import Path, PurePath, PurePosixPath from pathlib import Path, PurePath, PurePosixPath
from types import TracebackType from types import TracebackType
from typing import IO, Literal, Self, assert_never from typing import IO, Dict, Literal
from .ci import CIProvider, detect_ci_provider from ._compat.typing import Self
from .errors import OCIEngineTooOldError from .typing import PathOrStr, PopenBytes
from .logger import log from .util import (
from .typing import PathOrStr CIProvider,
from .util.cmd import call call,
from .util.helpers import FlexibleVersion, parse_key_value_string, strtobool detect_ci_provider,
parse_key_value_string,
strtobool,
)
ContainerEngineName = Literal["docker", "podman"] ContainerEngineName = Literal["docker", "podman"]
# Order of the enum matters for tests. 386 shall appear before amd64. @dataclass(frozen=True)
class OCIPlatform(Enum):
i386 = "linux/386"
AMD64 = "linux/amd64"
ARMV7 = "linux/arm/v7"
ARM64 = "linux/arm64"
PPC64LE = "linux/ppc64le"
RISCV64 = "linux/riscv64"
S390X = "linux/s390x"
@classmethod
def native(cls) -> "OCIPlatform":
"""Return the current OCI platform, or raise ValueError if unknown."""
arch = platform.machine().lower()
mapping = {
"i386": cls.i386,
"i686": cls.i386,
"x86_64": cls.AMD64,
"amd64": cls.AMD64,
"armv7l": cls.ARMV7,
"aarch64": cls.ARM64,
"arm64": cls.ARM64,
"ppc64le": cls.PPC64LE,
"riscv64": cls.RISCV64,
"s390x": cls.S390X,
}
try:
return mapping[arch]
except KeyError as ex:
msg = f"Unsupported platform architecture: {arch}"
raise OSError(msg) from ex
@dataclasses.dataclass(frozen=True)
class OCIContainerEngineConfig: class OCIContainerEngineConfig:
name: ContainerEngineName name: ContainerEngineName
_: dataclasses.KW_ONLY create_args: Sequence[str] = ()
create_args: tuple[str, ...] = dataclasses.field(default_factory=tuple)
disable_host_mount: bool = False disable_host_mount: bool = False
@classmethod @staticmethod
def from_config_string(cls, config_string: str) -> Self: def from_config_string(config_string: str) -> OCIContainerEngineConfig:
config_dict = parse_key_value_string( config_dict = parse_key_value_string(
config_string, config_string,
["name"], ["name"],
@@ -78,7 +47,7 @@ class OCIContainerEngineConfig:
msg = f"unknown container engine {name}" msg = f"unknown container engine {name}"
raise ValueError(msg) raise ValueError(msg)
name = typing.cast("ContainerEngineName", name) name = typing.cast(ContainerEngineName, name)
# some flexibility in the option names to cope with TOML conventions # some flexibility in the option names to cope with TOML conventions
create_args = config_dict.get("create_args") or config_dict.get("create-args") or [] create_args = config_dict.get("create_args") or config_dict.get("create-args") or []
disable_host_mount_options = ( disable_host_mount_options = (
@@ -87,17 +56,10 @@ class OCIContainerEngineConfig:
disable_host_mount = ( disable_host_mount = (
strtobool(disable_host_mount_options[-1]) if disable_host_mount_options else False strtobool(disable_host_mount_options[-1]) if disable_host_mount_options else False
) )
if "--platform" in create_args or any(arg.startswith("--platform=") for arg in create_args):
msg = "Using '--platform' in 'container-engine::create_args' is deprecated. It will be ignored."
log.warning(msg)
if "--platform" in create_args:
index = create_args.index("--platform")
create_args.pop(index)
create_args.pop(index)
else:
create_args = [arg for arg in create_args if not arg.startswith("--platform=")]
return cls(name=name, create_args=tuple(create_args), disable_host_mount=disable_host_mount) return OCIContainerEngineConfig(
name=name, create_args=create_args, disable_host_mount=disable_host_mount
)
def options_summary(self) -> str | dict[str, str]: def options_summary(self) -> str | dict[str, str]:
if not self.create_args: if not self.create_args:
@@ -113,58 +75,6 @@ class OCIContainerEngineConfig:
DEFAULT_ENGINE = OCIContainerEngineConfig("docker") DEFAULT_ENGINE = OCIContainerEngineConfig("docker")
def _check_engine_version(engine: OCIContainerEngineConfig) -> None:
try:
version_string = call(engine.name, "version", "-f", "{{json .}}", capture_stdout=True)
# We are using lowercase keys for all dicts so we are not affected by casing of keys
version_info = json.loads(
version_string.strip(), object_pairs_hook=lambda inp: {k.lower(): v for k, v in inp}
)
match engine.name:
case "docker":
client_api_version = FlexibleVersion(version_info["client"]["apiversion"])
server_api_version = FlexibleVersion(version_info["server"]["apiversion"])
# --platform support was introduced in 1.32 as experimental, 1.41 removed the experimental flag
version = min(client_api_version, server_api_version)
minimum_version = FlexibleVersion("1.41")
minimum_version_str = "20.10.0" # docker version
error_msg = textwrap.dedent(
f"""
Build failed because {engine.name} is too old.
cibuildwheel requires {engine.name}>={minimum_version_str} running API version {minimum_version}.
The API version found by cibuildwheel is {version}.
"""
)
case "podman":
# podman uses the same version string for "Version" & "ApiVersion"
client_version = FlexibleVersion(version_info["client"]["version"])
if "server" in version_info:
server_version = FlexibleVersion(version_info["server"]["version"])
else:
server_version = client_version
# --platform support was introduced in v3
version = min(client_version, server_version)
minimum_version = FlexibleVersion("3")
error_msg = textwrap.dedent(
f"""
Build failed because {engine.name} is too old.
cibuildwheel requires {engine.name}>={minimum_version}.
The version found by cibuildwheel is {version}.
"""
)
case _:
assert_never(engine.name)
if version < minimum_version:
raise OCIEngineTooOldError(error_msg) from None
except (subprocess.CalledProcessError, KeyError, ValueError) as e:
msg = f"Build failed because {engine.name} is too old or is not working properly."
raise OCIEngineTooOldError(msg) from e
class OCIContainer: class OCIContainer:
""" """
An object that represents a running OCI (e.g. Docker) container. An object that represents a running OCI (e.g. Docker) container.
@@ -177,28 +87,20 @@ class OCIContainer:
back to cibuildwheel. back to cibuildwheel.
Example: Example:
>>> # xdoctest: +REQUIRES(LINUX)
>>> from cibuildwheel.oci_container import * # NOQA >>> from cibuildwheel.oci_container import * # NOQA
>>> from cibuildwheel.options import _get_pinned_container_images >>> from cibuildwheel.options import _get_pinned_container_images
>>> import pytest
>>> try:
... oci_platform = OCIPlatform.native()
... except OSError as ex:
... pytest.skip(str(ex))
>>> if oci_platform != OCIPlatform.AMD64:
... pytest.skip('only runs on amd64')
>>> image = _get_pinned_container_images()['x86_64']['manylinux2014'] >>> image = _get_pinned_container_images()['x86_64']['manylinux2014']
>>> # Test the default container >>> # Test the default container
>>> with OCIContainer(image=image, oci_platform=oci_platform) as self: >>> with OCIContainer(image=image) as self:
... self.call(["echo", "hello world"]) ... self.call(["echo", "hello world"])
... self.call(["cat", "/proc/1/cgroup"]) ... self.call(["cat", "/proc/1/cgroup"])
... print(self.get_environment()) ... print(self.get_environment())
... print(self.debug_info()) ... print(self.debug_info())
""" """
UTILITY_PYTHON = "/opt/python/cp39-cp39/bin/python" UTILITY_PYTHON = "/opt/python/cp38-cp38/bin/python"
process: subprocess.Popen[bytes] process: PopenBytes
bash_stdin: IO[bytes] bash_stdin: IO[bytes]
bash_stdout: IO[bytes] bash_stdout: IO[bytes]
@@ -206,7 +108,7 @@ class OCIContainer:
self, self,
*, *,
image: str, image: str,
oci_platform: OCIPlatform, enforce_32_bit: bool = False,
cwd: PathOrStr | None = None, cwd: PathOrStr | None = None,
engine: OCIContainerEngineConfig = DEFAULT_ENGINE, engine: OCIContainerEngineConfig = DEFAULT_ENGINE,
): ):
@@ -215,48 +117,14 @@ class OCIContainer:
raise ValueError(msg) raise ValueError(msg)
self.image = image self.image = image
self.oci_platform = oci_platform self.enforce_32_bit = enforce_32_bit
self.cwd = cwd self.cwd = cwd
self.name: str | None = None self.name: str | None = None
self.engine = engine self.engine = engine
self.host_tar_format = ""
if sys.platform.startswith("darwin"):
self.host_tar_format = "--format gnutar"
def _get_platform_args(self, *, oci_platform: OCIPlatform | None = None) -> tuple[str, str]:
if oci_platform is None:
oci_platform = self.oci_platform
# we need '--pull=always' otherwise some images with the wrong platform get reused (e.g. 386 image for amd64)
# c.f. https://github.com/moby/moby/issues/48197#issuecomment-2282802313
pull = "always"
try:
image_platform = call(
self.engine.name,
"image",
"inspect",
self.image,
"--format",
(
"{{.Os}}/{{.Architecture}}/{{.Variant}}"
if len(oci_platform.value.split("/")) == 3
else "{{.Os}}/{{.Architecture}}"
),
capture_stdout=True,
).strip()
if image_platform == oci_platform.value:
# in case the correct image is already present, don't pull
# this allows to run local only images
pull = "never"
except subprocess.CalledProcessError:
pass
return f"--platform={oci_platform.value}", f"--pull={pull}"
def __enter__(self) -> Self: def __enter__(self) -> Self:
self.name = f"cibuildwheel-{uuid.uuid4()}" self.name = f"cibuildwheel-{uuid.uuid4()}"
_check_engine_version(self.engine)
# work-around for Travis-CI PPC64le Docker runs since 2021: # work-around for Travis-CI PPC64le Docker runs since 2021:
# this avoids network splits # this avoids network splits
# https://github.com/pypa/cibuildwheel/issues/904 # https://github.com/pypa/cibuildwheel/issues/904
@@ -265,30 +133,15 @@ class OCIContainer:
if detect_ci_provider() == CIProvider.travis_ci and platform.machine() == "ppc64le": if detect_ci_provider() == CIProvider.travis_ci and platform.machine() == "ppc64le":
network_args = ["--network=host"] network_args = ["--network=host"]
platform_args = self._get_platform_args()
simulate_32_bit = False simulate_32_bit = False
if self.oci_platform in {OCIPlatform.i386, OCIPlatform.ARMV7}: if self.enforce_32_bit:
# If the architecture running the image is already the right one # If the architecture running the image is already the right one
# or the image entrypoint takes care of enforcing this, then we don't need to # or the image entrypoint takes care of enforcing this, then we don't need to
# simulate this # simulate this
run_cmd = [self.engine.name, "run", "--rm"] container_machine = call(
ctr_cmd = ["uname", "-m"] self.engine.name, "run", "--rm", self.image, "uname", "-m", capture_stdout=True
try: ).strip()
container_machine = call( simulate_32_bit = container_machine != "i686"
*run_cmd, *platform_args, self.image, *ctr_cmd, capture_stdout=True
).strip()
except subprocess.CalledProcessError:
if self.oci_platform == OCIPlatform.i386:
# The image might have been built with amd64 architecture
# Let's try that
platform_args = self._get_platform_args(oci_platform=OCIPlatform.AMD64)
container_machine = call(
*run_cmd, *platform_args, self.image, *ctr_cmd, capture_stdout=True
).strip()
else:
raise
simulate_32_bit = container_machine not in {"i686", "armv7l", "armv8l"}
shell_args = ["linux32", "/bin/bash"] if simulate_32_bit else ["/bin/bash"] shell_args = ["linux32", "/bin/bash"] if simulate_32_bit else ["/bin/bash"]
@@ -302,7 +155,6 @@ class OCIContainer:
"--interactive", "--interactive",
*(["--volume=/:/host"] if not self.engine.disable_host_mount else []), *(["--volume=/:/host"] if not self.engine.disable_host_mount else []),
*network_args, *network_args,
*platform_args,
*self.engine.create_args, *self.engine.create_args,
self.image, self.image,
*shell_args, *shell_args,
@@ -369,17 +221,21 @@ class OCIContainer:
self.name = None self.name = None
def copy_into(self, from_path: Path, to_path: PurePath) -> None: def copy_into(self, from_path: Path, to_path: PurePath) -> None:
# `docker cp` causes 'no space left on device' error when
# a container is running and the host filesystem is
# mounted. https://github.com/moby/moby/issues/38995
# Use `docker exec` instead.
if from_path.is_dir(): if from_path.is_dir():
self.call(["mkdir", "-p", to_path]) self.call(["mkdir", "-p", to_path])
subprocess.run( subprocess.run(
f"tar -c {self.host_tar_format} -f - . | {self.engine.name} exec -i {self.name} tar --no-same-owner -xC {shell_quote(to_path)} -f -", f"tar cf - . | {self.engine.name} exec -i {self.name} tar --no-same-owner -xC {shell_quote(to_path)} -f -",
shell=True, shell=True,
check=True, check=True,
cwd=from_path, cwd=from_path,
) )
else: else:
exec_process: subprocess.Popen[bytes] exec_process: subprocess.Popen[bytes]
self.call(["mkdir", "-p", to_path.parent])
with subprocess.Popen( with subprocess.Popen(
[ [
self.engine.name, self.engine.name,
@@ -394,7 +250,8 @@ class OCIContainer:
) as exec_process: ) as exec_process:
assert exec_process.stdin assert exec_process.stdin
with open(from_path, "rb") as from_file: with open(from_path, "rb") as from_file:
shutil.copyfileobj(from_file, exec_process.stdin) # Bug in mypy, https://github.com/python/mypy/issues/15031
shutil.copyfileobj(from_file, exec_process.stdin) # type: ignore[misc]
exec_process.stdin.close() exec_process.stdin.close()
exec_process.wait() exec_process.wait()
@@ -407,7 +264,30 @@ class OCIContainer:
def copy_out(self, from_path: PurePath, to_path: Path) -> None: def copy_out(self, from_path: PurePath, to_path: Path) -> None:
# note: we assume from_path is a dir # note: we assume from_path is a dir
to_path.mkdir(parents=True, exist_ok=True) to_path.mkdir(parents=True, exist_ok=True)
call(self.engine.name, "cp", f"{self.name}:{from_path}/.", to_path)
if self.engine.name == "podman":
subprocess.run(
[
self.engine.name,
"cp",
f"{self.name}:{from_path}/.",
str(to_path),
],
check=True,
cwd=to_path,
)
elif self.engine.name == "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.name} 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.name)
def glob(self, path: PurePosixPath, pattern: str) -> list[PurePosixPath]: def glob(self, path: PurePosixPath, pattern: str) -> list[PurePosixPath]:
glob_pattern = path.joinpath(pattern) glob_pattern = path.joinpath(pattern)
@@ -518,7 +398,7 @@ class OCIContainer:
capture_output=True, capture_output=True,
) )
) )
return typing.cast("dict[str, str]", env) return typing.cast(Dict[str, str], env)
def environment_executor(self, command: Sequence[str], environment: dict[str, str]) -> str: def environment_executor(self, command: Sequence[str], environment: dict[str, str]) -> str:
# used as an EnvironmentExecutor to evaluate commands and capture output # used as an EnvironmentExecutor to evaluate commands and capture output
+227 -534
View File
File diff suppressed because it is too large Load Diff
-63
View File
@@ -1,63 +0,0 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING, Final, Protocol
from cibuildwheel import errors
from cibuildwheel.platforms import android, ios, linux, macos, pyodide, windows
if TYPE_CHECKING:
from collections.abc import Sequence
from pathlib import Path
from cibuildwheel.architecture import Architecture
from cibuildwheel.options import Options
from cibuildwheel.selector import BuildSelector
from cibuildwheel.typing import GenericPythonConfiguration, PlatformName
class PlatformModule(Protocol):
# note that as per PEP544, the self argument is ignored when the protocol
# is applied to a module
def all_python_configurations(self) -> Sequence[GenericPythonConfiguration]: ...
def get_python_configurations(
self, build_selector: BuildSelector, architectures: set[Architecture]
) -> Sequence[GenericPythonConfiguration]: ...
def build(self, options: Options, tmp_path: Path) -> None: ...
ALL_PLATFORM_MODULES: Final[dict[PlatformName, PlatformModule]] = {
"linux": linux,
"windows": windows,
"macos": macos,
"pyodide": pyodide,
"android": android,
"ios": ios,
}
def native_platform() -> PlatformName:
if sys.platform.startswith("linux"):
return "linux"
elif sys.platform == "darwin":
return "macos"
elif sys.platform == "win32":
return "windows"
else:
msg = (
'Unable to detect platform from "sys.platform". cibuildwheel doesn\'t '
"support building wheels for this platform. You might be able to build for a different "
"platform using the --platform argument. Check --help output for more information."
)
raise errors.ConfigurationError(msg)
def get_build_identifiers(
platform_module: PlatformModule,
build_selector: BuildSelector,
architectures: set[Architecture],
) -> list[str]:
python_configurations = platform_module.get_python_configurations(build_selector, architectures)
return [config.identifier for config in python_configurations]
-733
View File
@@ -1,733 +0,0 @@
import csv
import hashlib
import os
import platform
import re
import shlex
import shutil
import subprocess
import sysconfig
from collections.abc import Iterable, Iterator, MutableMapping
from dataclasses import dataclass
from os.path import relpath
from pathlib import Path
from pprint import pprint
from runpy import run_path
from textwrap import dedent
from typing import Any
from build import ProjectBuilder
from build.env import IsolatedEnv
from elftools.common.exceptions import ELFError
from elftools.elf.elffile import ELFFile
from filelock import FileLock
from .. import errors, platforms # pylint: disable=cyclic-import
from ..architecture import Architecture, arch_synonym
from ..frontend import get_build_frontend_extra_flags, parse_config_settings
from ..logger import log
from ..options import BuildOptions, Options
from ..selector import BuildSelector
from ..util import resources
from ..util.cmd import call, shell
from ..util.file import CIBW_CACHE_PATH, copy_test_sources, download, move_file
from ..util.helpers import prepare_command
from ..util.packaging import find_compatible_wheel
from ..util.python_build_standalone import create_python_build_standalone_environment
from ..venv import constraint_flags, find_uv, virtualenv
ANDROID_TRIPLET = {
"arm64_v8a": "aarch64-linux-android",
"x86_64": "x86_64-linux-android",
}
def parse_identifier(identifier: str) -> tuple[str, str]:
match = re.fullmatch(r"cp(\d)(\d+)-android_(.+)", identifier)
if not match:
msg = f"invalid Android identifier: '{identifier}'"
raise ValueError(msg)
major, minor, arch = match.groups()
return (f"{major}.{minor}", arch)
def android_triplet(identifier: str) -> str:
return ANDROID_TRIPLET[parse_identifier(identifier)[1]]
@dataclass(frozen=True)
class PythonConfiguration:
version: str
identifier: str
url: str
@property
def arch(self) -> str:
return parse_identifier(self.identifier)[1]
def all_python_configurations() -> list[PythonConfiguration]:
return [PythonConfiguration(**item) for item in resources.read_python_configs("android")]
def get_python_configurations(
build_selector: BuildSelector, architectures: set[Architecture]
) -> list[PythonConfiguration]:
return [
c
for c in all_python_configurations()
if c.arch in architectures and build_selector(c.identifier)
]
def shell_prepared(command: str, *, build_options: BuildOptions, env: dict[str, str]) -> None:
shell(
prepare_command(command, project=".", package=build_options.package_dir),
env=env,
)
def before_all(options: Options, python_configurations: list[PythonConfiguration]) -> None:
before_all_options = options.build_options(python_configurations[0].identifier)
if before_all_options.before_all:
log.step("Running before_all...")
shell_prepared(
before_all_options.before_all,
build_options=before_all_options,
env=before_all_options.environment.as_dictionary(os.environ),
)
@dataclass(frozen=True)
class BuildState:
config: PythonConfiguration
options: BuildOptions
build_path: Path
python_dir: Path
build_env: dict[str, str]
android_env: dict[str, str]
def build(options: Options, tmp_path: Path) -> None:
if "ANDROID_HOME" not in os.environ:
msg = (
"ANDROID_HOME environment variable is not set. For instructions, see "
"https://cibuildwheel.pypa.io/en/stable/platforms/#android"
)
raise errors.FatalError(msg)
configs = get_python_configurations(
options.globals.build_selector, options.globals.architectures
)
if not configs:
return
try:
before_all(options, configs)
built_wheels: list[Path] = []
for config in configs:
log.build_start(config.identifier)
build_options = options.build_options(config.identifier)
build_path = tmp_path / config.identifier
build_path.mkdir()
python_dir = setup_target_python(config, build_path)
build_env, android_env = setup_env(config, build_options, build_path, python_dir)
state = BuildState(
config, build_options, build_path, python_dir, build_env, android_env
)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
print(
f"\nFound previously built wheel {compatible_wheel.name} that is "
f"compatible with {config.identifier}. Skipping build step..."
)
repaired_wheel = compatible_wheel
else:
before_build(state)
built_wheel = build_wheel(state)
repaired_wheel = repair_wheel(state, built_wheel)
test_wheel(state, repaired_wheel, build_frontend=build_options.build_frontend.name)
output_wheel: Path | None = None
if compatible_wheel is None:
output_wheel = move_file(
repaired_wheel, build_options.output_dir / repaired_wheel.name
)
built_wheels.append(output_wheel)
shutil.rmtree(build_path)
log.build_end(output_wheel)
except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error
def setup_target_python(config: PythonConfiguration, build_path: Path) -> Path:
log.step("Installing target Python...")
python_tgz = CIBW_CACHE_PATH / config.url.rpartition("/")[-1]
with FileLock(f"{python_tgz}.lock"):
if not python_tgz.exists():
download(config.url, python_tgz)
python_dir = build_path / "python"
python_dir.mkdir()
shutil.unpack_archive(python_tgz, python_dir)
return python_dir
def setup_env(
config: PythonConfiguration, build_options: BuildOptions, build_path: Path, python_dir: Path
) -> tuple[dict[str, str], dict[str, str]]:
"""
Returns two environment dicts, both pointing at the same virtual environment:
* build_env, which uses the environment normally.
* android_env, which uses the environment while simulating running on Android.
"""
log.step("Setting up build environment...")
build_frontend = build_options.build_frontend.name
use_uv = build_frontend == "build[uv]"
uv_path = find_uv()
if use_uv and uv_path is None:
msg = "uv not found"
raise AssertionError(msg)
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
# Create virtual environment
python_exe = create_python_build_standalone_environment(
config.version, build_path, CIBW_CACHE_PATH
)
venv_dir = build_path / "venv"
dependency_constraint = build_options.dependency_constraints.get_for_python_version(
version=config.version, tmp_dir=build_path
)
build_env = virtualenv(
config.version, python_exe, venv_dir, dependency_constraint, use_uv=use_uv
)
create_cmake_toolchain(config, build_path, python_dir, build_env)
# Apply custom environment variables, and check environment is still valid
build_env = build_options.environment.as_dictionary(build_env)
build_env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
for command in ["python"] if use_uv else ["python", "pip"]:
command_path = call("which", command, env=build_env, capture_stdout=True).strip()
if command_path != f"{venv_dir}/bin/{command}":
msg = (
f"{command} available on PATH doesn't match our installed instance. If you "
f"have modified PATH, ensure that you don't overwrite cibuildwheel's entry "
f"or insert {command} above it."
)
raise errors.FatalError(msg)
call(command, "--version", env=build_env)
# Construct an altered environment which simulates running on Android.
android_env = setup_android_env(config, python_dir, venv_dir, build_env)
# Install build tools
if build_frontend not in {"build", "build[uv]"}:
msg = "Android requires the build frontend to be 'build'"
raise errors.FatalError(msg)
call(*pip, "install", "build", *constraint_flags(dependency_constraint), env=build_env)
# Build-time requirements must be queried within android_env, because
# `get_requires_for_build` can run arbitrary code in setup.py scripts, which may be
# affected by the target platform. However, the requirements must be installed
# within build_env, because they're going to run on the build machine.
#
# The `build` CLI doesn't support this combination, so we use its API to query the
# requirements, and then install them ourselves with pip. We'll later run `build` in
# the same environment, passing the `--no-isolation` option.
class AndroidEnv(IsolatedEnv):
@property
def python_executable(self) -> str:
return f"{venv_dir}/bin/python"
def make_extra_environ(self) -> dict[str, str]:
return android_env
pb = ProjectBuilder.from_isolated_env(AndroidEnv(), build_options.package_dir)
if pb.build_system_requires:
call(*pip, "install", *pb.build_system_requires, env=build_env)
requires_for_build = pb.get_requires_for_build(
"wheel", parse_config_settings(build_options.config_settings)
)
if requires_for_build:
call(*pip, "install", *requires_for_build, env=build_env)
return build_env, android_env
def create_cmake_toolchain(
config: PythonConfiguration, build_path: Path, python_dir: Path, build_env: dict[str, str]
) -> None:
toolchain_path = build_path / "toolchain.cmake"
build_env["CMAKE_TOOLCHAIN_FILE"] = str(toolchain_path)
with open(toolchain_path, "w", encoding="UTF-8") as toolchain_file:
print(
dedent(
f"""\
# To support as many build systems as possible, we use environment
# variables as the single source of truth for compiler flags and paths,
# so they don't need to be specified here.
set(CMAKE_SYSTEM_NAME Android)
set(CMAKE_SYSTEM_PROCESSOR {android_triplet(config.identifier).split("-")[0]})
# Inhibit all of CMake's own NDK handling code.
set(CMAKE_SYSTEM_VERSION 1)
# Tell CMake where to look for headers and libraries.
set(CMAKE_FIND_ROOT_PATH "{python_dir}/prefix")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
# Allow CMake to run Python in the simulated Android environment when
# policy CMP0190 is active.
set(CMAKE_CROSSCOMPILING_EMULATOR /bin/sh -c [["$0" "$@"]])
"""
),
file=toolchain_file,
)
def localize_sysconfigdata(
python_dir: Path, build_env: dict[str, str], sysconfigdata_path: Path
) -> dict[str, Any]:
sysconfigdata: dict[str, Any] = run_path(str(sysconfigdata_path))["build_time_vars"]
with sysconfigdata_path.open("w", encoding="UTF-8") as f:
f.write("# Generated by cibuildwheel\n")
f.write("build_time_vars = ")
sysconfigdata = localized_vars(build_env, sysconfigdata, python_dir / "prefix")
pprint(sysconfigdata, stream=f, compact=True)
return sysconfigdata
def localized_vars(
build_env: dict[str, str], orig_vars: dict[str, Any], prefix: Path
) -> dict[str, Any]:
orig_prefix = orig_vars["prefix"]
localized_vars_ = {}
for key, value in orig_vars.items():
# The host's sysconfigdata will include references to build-time paths.
# Update these to refer to the current prefix.
final = value
if isinstance(final, str):
final = final.replace(orig_prefix, str(prefix))
if key == "ANDROID_API_LEVEL":
if api_level := build_env.get(key):
final = int(api_level)
# Build systems vary in whether FLAGS variables are read from sysconfig, and if so,
# whether they're replaced by environment variables or combined with them. Even
# setuptools has changed its behavior here
# (https://github.com/pypa/setuptools/issues/4836).
#
# Ensure consistency by clearing the sysconfig variables and letting the environment
# variables take effect alone. This will also work for any non-Python build systems
# which the build script may call.
elif key in ["CFLAGS", "CXXFLAGS", "LDFLAGS"]:
final = ""
# These variables contain an embedded copy of LDFLAGS.
elif key in ["LDSHARED", "LDCXXSHARED"]:
final = final.removesuffix(" " + orig_vars["LDFLAGS"])
localized_vars_[key] = final
return localized_vars_
def setup_android_env(
config: PythonConfiguration, python_dir: Path, venv_dir: Path, build_env: dict[str, str]
) -> dict[str, str]:
site_packages = next(venv_dir.glob("lib/python*/site-packages"))
for suffix in ["pth", "py"]:
shutil.copy(resources.PATH / f"_cross_venv.{suffix}", site_packages)
sysconfigdata_path = Path(
shutil.copy(
next(python_dir.glob("prefix/lib/python*/_sysconfigdata_*.py")),
site_packages,
)
)
sysconfigdata = localize_sysconfigdata(python_dir, build_env, sysconfigdata_path)
# Activate the code in _cross_venv.py.
android_env = build_env.copy()
android_env["CIBW_HOST_TRIPLET"] = android_triplet(config.identifier)
# Get the environment variables needed to build for Android (CC, CFLAGS, etc). These are
# generated by https://github.com/python/cpython/blob/main/Android/android-env.sh.
env_output = call(python_dir / "android.py", "env", env=build_env, capture_stdout=True)
# shlex.split should produce a sequence alternating between:
# * the word "export"
# * a key=value string, without quotes
for i, token in enumerate(shlex.split(env_output)):
if i % 2 == 0:
assert token == "export", token
else:
key, sep, value = token.partition("=")
assert sep == "=", token
android_env[key] = value
# localized_vars cleared the CFLAGS and CXXFLAGS in the sysconfigdata, but most
# packages take their optimization flags from these variables. Pass these flags via
# environment variables instead.
#
# We don't enable debug information, because it significantly increases binary size,
# and most Android app developers don't have the NDK installed, so they would have no
# way to strip it.
opt = " ".join(word for word in sysconfigdata["OPT"].split() if not word.startswith("-g"))
for key in ["CFLAGS", "CXXFLAGS"]:
android_env[key] += " " + opt
# Cargo target linker needs to be specified after CC is set
setup_rust(config, python_dir, android_env)
# Format the environment so it can be pasted into a shell when debugging.
for key, value in sorted(android_env.items()):
if os.environ.get(key) != value:
print(f"export {key}={shlex.quote(value)}")
return android_env
def setup_rust(
config: PythonConfiguration,
python_dir: Path,
env: MutableMapping[str, str],
) -> None:
cargo_target = android_triplet(config.identifier)
# CARGO_BUILD_TARGET is the variable used by Cargo and setuptools_rust
env["CARGO_BUILD_TARGET"] = cargo_target
# The linker needs to be specified after CC is set by android-env.sh
cargo_target_linker_env_name = f"CARGO_TARGET_{cargo_target.upper().replace('-', '_')}_LINKER"
# CC has already been set by calling android.py (it calls android-env.sh)
env[cargo_target_linker_env_name] = env["CC"]
# All Python extension modules must be explicitly linked against libpython3.x.so when building for Android.
# See: https://peps.python.org/pep-0738/#linkage
# For projects using PyO3, this requires setting PYO3_CROSS_LIB_DIR to the directory containing libpython3.x.so.
# See: https://pyo3.rs/v0.27.1/building-and-distribution.html#cross-compiling
env["PYO3_CROSS_LIB_DIR"] = str(python_dir / "prefix" / "lib")
venv_bin = Path(env["VIRTUAL_ENV"]) / "bin"
for tool in ["cargo", "rustup"]:
shim_path = venv_bin / tool
shutil.copy(resources.PATH / "_rust_shim.py", shim_path)
shim_path.chmod(0o755)
def before_build(state: BuildState) -> None:
if state.options.before_build:
log.step("Running before_build...")
shell_prepared(
state.options.before_build,
build_options=state.options,
env=state.build_env,
)
def build_wheel(state: BuildState) -> Path:
log.step("Building wheel...")
built_wheel_dir = state.build_path / "built_wheel"
call(
"python",
"-m",
"build",
state.options.package_dir,
"--wheel",
"--no-isolation",
"--skip-dependency-check",
f"--outdir={built_wheel_dir}",
*get_build_frontend_extra_flags(
state.options.build_frontend,
state.options.build_verbosity,
state.options.config_settings,
),
env=state.android_env,
)
built_wheels = list(built_wheel_dir.glob("*.whl"))
if len(built_wheels) != 1:
msg = f"{built_wheel_dir} contains {len(built_wheels)} wheels; expected 1"
raise errors.FatalError(msg)
built_wheel = built_wheels[0]
if built_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError()
return built_wheel
def repair_wheel(state: BuildState, built_wheel: Path) -> Path:
log.step("Repairing wheel...")
repaired_wheel_dir = state.build_path / "repaired_wheel"
repaired_wheel_dir.mkdir()
if state.options.repair_command:
shell(
prepare_command(
state.options.repair_command,
wheel=built_wheel,
dest_dir=repaired_wheel_dir,
package=state.options.package_dir,
project=".",
),
env=state.build_env,
)
else:
repair_default(state.android_env, built_wheel, repaired_wheel_dir)
repaired_wheels = list(repaired_wheel_dir.glob("*.whl"))
if len(repaired_wheels) == 0:
raise errors.RepairStepProducedNoWheelError()
if len(repaired_wheels) != 1:
msg = f"{repaired_wheel_dir} contains {len(repaired_wheels)} wheels; expected 1"
raise errors.FatalError(msg)
repaired_wheel = repaired_wheels[0]
if repaired_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError()
return repaired_wheel
def repair_default(
android_env: dict[str, str], built_wheel: Path, repaired_wheel_dir: Path
) -> None:
"""
Adds libc++ to the wheel if anything links against it. In the future this should be
moved to auditwheel and generalized to support more libraries.
"""
if (match := re.search(r"^(.+?)-", built_wheel.name)) is None:
msg = f"Failed to parse wheel filename: {built_wheel.name}"
raise errors.FatalError(msg)
wheel_name = match[1]
unpacked_dir = repaired_wheel_dir / "unpacked"
unpacked_dir.mkdir()
shutil.unpack_archive(built_wheel, unpacked_dir, format="zip")
# Some build systems are inconsistent about name normalization, so don't assume the
# dist-info name is identical to the wheel name.
record_paths = list(unpacked_dir.glob("*.dist-info/RECORD"))
if len(record_paths) != 1:
msg = f"{built_wheel.name} contains {len(record_paths)} dist-info/RECORD files; expected 1"
raise errors.FatalError(msg)
old_soname = "libc++_shared.so"
paths_to_patch = []
for path, elffile in elf_file_filter(
unpacked_dir / filename
for filename, *_ in csv.reader(record_paths[0].read_text().splitlines())
):
if (dynamic := elffile.get_section_by_name(".dynamic")) and any( # type: ignore[no-untyped-call]
tag.entry.d_tag == "DT_NEEDED" and tag.needed == old_soname
for tag in dynamic.iter_tags()
):
paths_to_patch.append(path)
if not paths_to_patch:
shutil.copyfile(built_wheel, repaired_wheel_dir / built_wheel.name)
else:
# Android doesn't support DT_RPATH, but supports DT_RUNPATH since API level 24
# (https://github.com/aosp-mirror/platform_bionic/blob/master/android-changes-for-ndk-developers.md).
if int(sysconfig_print('get_config_vars()["ANDROID_API_LEVEL"]', android_env)) < 24:
msg = f"Adding {old_soname} requires ANDROID_API_LEVEL to be at least 24"
raise errors.FatalError(msg)
toolchain = Path(android_env["CC"]).parent.parent
src_path = toolchain / f"sysroot/usr/lib/{android_env['CIBW_HOST_TRIPLET']}/{old_soname}"
# Use the same library location as auditwheel would.
libs_dir = unpacked_dir / (wheel_name + ".libs")
libs_dir.mkdir()
new_soname = soname_with_hash(src_path)
dst_path = libs_dir / new_soname
shutil.copyfile(src_path, dst_path)
call(which("patchelf"), "--set-soname", new_soname, dst_path)
for path in paths_to_patch:
call(which("patchelf"), "--replace-needed", old_soname, new_soname, path)
call(
which("patchelf"),
"--set-rpath",
f"${{ORIGIN}}/{relpath(libs_dir, path.parent)}",
path,
)
call(which("wheel"), "pack", unpacked_dir, "-d", repaired_wheel_dir)
# If cibuildwheel was called without activating its environment, its scripts directory
# will not be on the PATH.
def which(cmd: str) -> str:
scripts_dir = sysconfig.get_path("scripts")
result = shutil.which(cmd, path=scripts_dir + os.pathsep + os.environ["PATH"])
if result is None:
msg = f"Couldn't find {cmd!r} in {scripts_dir} or on the PATH"
raise errors.FatalError(msg)
return result
def elf_file_filter(paths: Iterable[Path]) -> Iterator[tuple[Path, ELFFile]]:
"""Filter through an iterator of filenames and load up only ELF files"""
for path in paths:
if not path.name.endswith(".py"):
try:
with open(path, "rb") as f:
candidate = ELFFile(f) # type: ignore[no-untyped-call]
yield path, candidate
except ELFError:
pass # Not an ELF file
def soname_with_hash(src_path: Path) -> str:
"""Return the same library filename as auditwheel would"""
shorthash = hashlib.sha256(src_path.read_bytes()).hexdigest()[:8]
src_name = src_path.name
base, ext = src_name.split(".", 1)
if not base.endswith(f"-{shorthash}"):
return f"{base}-{shorthash}.{ext}"
else:
return src_name
def test_wheel(state: BuildState, wheel: Path, *, build_frontend: str) -> None:
test_command = state.options.test_command
if not (test_command and state.options.test_selector(state.config.identifier)):
return
log.step("Testing wheel...")
use_uv = build_frontend == "build[uv]"
uv_path = find_uv()
if use_uv and uv_path is None:
msg = "uv not found"
raise AssertionError(msg)
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
native_arch = arch_synonym(platform.machine(), platforms.native_platform(), "android")
if state.config.arch != native_arch:
log.warning(
f"Skipping tests for {state.config.arch}, as the build machine only "
f"supports {native_arch}"
)
return
if state.options.before_test:
shell_prepared(
state.options.before_test,
build_options=state.options,
env=state.build_env,
)
platform_args = (
["--python-platform", android_triplet(state.config.identifier)]
if use_uv
else [
"--platform",
sysconfig_print("get_platform()", state.android_env).replace("-", "_"),
]
)
# Install the wheel and test-requires.
site_packages_dir = state.build_path / "site-packages"
site_packages_dir.mkdir()
call(
*pip,
"install",
"--only-binary=:all:",
*platform_args,
"--target",
site_packages_dir,
f"{wheel}{state.options.test_extras}",
*state.options.test_requires,
env=state.android_env,
)
# Copy test-sources.
cwd_dir = state.build_path / "cwd"
cwd_dir.mkdir()
if state.options.test_sources:
copy_test_sources(state.options.test_sources, Path.cwd(), cwd_dir)
else:
(cwd_dir / "test_fail.py").write_text(
resources.TEST_FAIL_CWD_FILE.read_text(),
)
# Android doesn't support placeholders in the test command.
if any(("{" + placeholder + "}") in test_command for placeholder in ["project", "package"]):
msg = (
f"Test command {test_command!r} with a "
"'{project}' or '{package}' placeholder is not supported on Android, "
"because the source directory is not visible on the emulator."
)
raise errors.FatalError(msg)
# Parse test-command.
test_args = shlex.split(test_command)
if test_args[0] in ["python", "python3"] and any(arg in test_args for arg in ["-c", "-m"]):
# Forward the args to the CPython testbed script. We require '-c' or '-m'
# to be in the command, because without those flags, the testbed script
# will prepend '-m test', which will run Python's own test suite.
del test_args[0]
elif test_args[0] in ["pytest"]:
# We transform some commands into the `python -m` form, but this is deprecated.
msg = (
f"Test command {test_command!r} is not supported on Android. "
"cibuildwheel will try to execute it as if it started with 'python -m'. "
"If this works, all you need to do is add that to your test command."
)
log.warning(msg)
test_args.insert(0, "-m")
else:
msg = (
f"Test command {test_command!r} is not supported on Android. "
f"Command must begin with 'python' or 'python3', and contain '-m' or '-c'."
)
raise errors.FatalError(msg)
# By default, run on a testbed managed emulator running the newest supported
# Android version. However, if the user specifies a --managed or --connected
# test execution argument, that argument takes precedence.
test_runtime_args = state.options.test_runtime.args
if any(arg.startswith(("--managed", "--connected")) for arg in test_runtime_args):
emulator_args = []
else:
emulator_args = ["--managed", "maxVersion"]
# Run the test app.
call(
state.python_dir / "android.py",
"test",
"--site-packages",
site_packages_dir,
"--cwd",
cwd_dir,
*emulator_args,
*(["-v"] if state.options.build_verbosity > 0 else []),
*test_runtime_args,
"--",
*test_args,
env=state.build_env,
)
def sysconfig_print(method_call: str, env: dict[str, str]) -> str:
return call(
"python",
"-c",
f'import sysconfig; print(sysconfig.{method_call}, end="")',
env=env,
capture_stdout=True,
)
-709
View File
@@ -1,709 +0,0 @@
from __future__ import annotations
import dataclasses
import os
import platform
import shlex
import shutil
import subprocess
import sys
import textwrap
from pathlib import Path
from typing import TYPE_CHECKING, assert_never
from filelock import FileLock
from .. import errors
from ..frontend import (
BuildFrontendName,
get_build_frontend_extra_flags,
)
from ..logger import log
from ..util import resources
from ..util.cmd import call, shell, split_command
from ..util.file import (
CIBW_CACHE_PATH,
copy_test_sources,
download,
move_file,
)
from ..util.helpers import prepare_command, unwrap_preserving_paragraphs
from ..util.packaging import (
find_compatible_wheel,
)
from ..venv import constraint_flags, virtualenv
from .macos import install_cpython as install_build_cpython
if TYPE_CHECKING:
from collections.abc import Sequence, Set
from ..architecture import Architecture
from ..environment import ParsedEnvironment
from ..options import Options
from ..selector import BuildSelector
@dataclasses.dataclass(frozen=True, kw_only=True)
class PythonConfiguration:
version: str
identifier: str
url: str
build_url: str
@property
def sdk(self) -> str:
return self.multiarch.rsplit("-", 1)[1]
@property
def arch(self) -> str:
return self.multiarch.rsplit("-", 1)[0]
@property
def multiarch(self) -> str:
# The multiarch identifier, as reported by `sys.implementation._multiarch`
return "-".join(self.identifier.split("-ios_")[1].rsplit("_", 1))
@property
def is_simulator(self) -> bool:
return self.identifier.endswith("_iphonesimulator")
@property
def xcframework_slice(self) -> str:
"XCframeworks include binaries for multiple ABIs; which ABI section should be used?"
return "ios-arm64_x86_64-simulator" if self.is_simulator else "ios-arm64"
def all_python_configurations() -> list[PythonConfiguration]:
# iOS builds are always cross builds; we need to install a macOS Python as
# well. Rather than duplicate the location of the URL of macOS installers,
# load the macos configurations, determine the macOS configuration that
# matches the platform we're building, and embed that URL in the parsed iOS
# configuration.
macos_python_configs = resources.read_python_configs("macos")
def build_url(config_dict: dict[str, str]) -> str:
# The iOS identifier will be something like cp313-ios_arm64_iphoneos.
# Drop the iphoneos suffix, then replace ios with macosx to yield
# cp313-macosx_arm64, which will be a macOS build identifier.
modified_ios_identifier = config_dict["identifier"].rsplit("_", 1)[0]
macos_identifier = modified_ios_identifier.replace("ios", "macosx")
matching = [
config for config in macos_python_configs if config["identifier"] == macos_identifier
]
return matching[0]["url"]
# Load the platform configuration
full_python_configs = resources.read_python_configs("ios")
# Build the configurations, annotating with macOS URL details.
return [
PythonConfiguration(
**item,
build_url=build_url(item),
)
for item in full_python_configs
]
def get_python_configurations(
build_selector: BuildSelector,
architectures: Set[Architecture],
) -> list[PythonConfiguration]:
python_configurations = all_python_configurations()
# Filter out configs that don't match any of the selected architectures
python_configurations = [
c
for c in python_configurations
if any(c.identifier.endswith(f"-ios_{a.value}") for a in architectures)
]
# Skip builds as required by BUILD/SKIP
python_configurations = [c for c in python_configurations if build_selector(c.identifier)]
return python_configurations
def install_target_cpython(tmp: Path, config: PythonConfiguration, free_threading: bool) -> Path:
if free_threading:
msg = "Free threading builds aren't available for iOS (yet)"
raise errors.FatalError(msg)
# Install an iOS build of CPython
ios_python_tar_gz = config.url.rsplit("/", 1)[-1]
extension = ".tar.gz"
assert ios_python_tar_gz.endswith(extension)
installation_path = CIBW_CACHE_PATH / ios_python_tar_gz[: -len(extension)]
with FileLock(str(installation_path) + ".lock"):
if not installation_path.exists():
downloaded_tar_gz = tmp / ios_python_tar_gz
download(config.url, downloaded_tar_gz)
installation_path.mkdir(parents=True, exist_ok=True)
call("tar", "-C", installation_path, "-xf", downloaded_tar_gz)
downloaded_tar_gz.unlink()
return installation_path
def cross_virtualenv(
*,
py_version: str,
target_python: Path,
multiarch: str,
build_python: Path,
venv_path: Path,
dependency_constraint: Path | None,
xbuild_tools: Sequence[str] | None,
) -> dict[str, str]:
"""Create a cross-compilation virtual environment.
In a cross-compilation environment, the *target* is the platform where the
code will ultimately run, and the *build* is the platform where you're
running the compilation. When building iOS wheels, iOS is the target machine
and macOS is the build machine. The terminology around these machines varies
between build tools (configure uses "host" and "build"; cmake uses "target" and
"build host").
A cross-compilation virtualenv is an environment that is based on the
*build* python (so that binaries can execute); but it modifies the
environment at startup so that any request for platform details (such as
`sys.platform` or `sysconfig.get_platform()`) return details of the target
platform. It also applies a loader patch so that any virtualenv created by
the cross-compilation environment will also be a cross-compilation
environment.
:param py_version: The Python version (major.minor) in use
:param target_python: The path to the python binary for the target platform
:param multiarch: The multiarch tag for the target platform (i.e., the value
of `sys.implementation._multiarch`)
:param build_python: The path to the python binary for the build platform
:param venv_path: The path where the cross virtual environment should be
created.
:param dependency_constraint: A path to a constraint file that should be
used when constraining dependencies in the environment.
:param xbuild_tools: A list of executable names (without paths) that are
on the path, but must be preserved in the cross environment.
"""
# Create an initial macOS virtual environment
env = virtualenv(
py_version,
build_python,
venv_path,
dependency_constraint,
use_uv=False,
)
# Convert the macOS virtual environment into an iOS virtual environment
# using the cross-platform conversion script in the iOS distribution.
# target_python is the path to the Python binary;
# determine the root of the XCframework slice that is being used.
slice_path = target_python.parent.parent
call(
"python",
str(slice_path / f"platform-config/{multiarch}/make_cross_venv.py"),
str(venv_path),
env=env,
cwd=venv_path,
)
# When running on macOS, it's easy for the build environment to leak into
# the target environment, especially when building for ARM64 (because the
# build architecture is the same as the target architecture). The primary
# culprit for this is Homebrew libraries leaking in as dependencies for iOS
# libraries.
#
# To prevent problems, set the PATH to isolate the build environment from
# sources that could introduce incompatible binaries.
#
# However, there may be some tools on the path that are needed for the
# build. Find their location on the path, and link the underlying binaries
# (fully resolving symlinks) to a "safe" location that will *only* contain
# those tools. This avoids needing to add *all* of Homebrew to the path just
# to get access to (for example) cmake for build purposes. A value of None
# means the user hasn't provided a list of xbuild tools.
xbuild_tools_path = venv_path / "cibw_xbuild_tools"
xbuild_tools_path.mkdir()
if xbuild_tools is None:
log.warning(
textwrap.dedent(
"""
Your project configuration does not define any cross-build tools.
iOS builds use an isolated build environment; if your build process requires any
third-party tools (such as cmake, ninja, or rustc), you must explicitly declare
that those tools are required using xbuild-tools/CIBW_XBUILD_TOOLS. This will
likely manifest as a "somebuildtool: command not found" error.
If the build succeeds, you can silence this warning by setting adding
`xbuild-tools = []` to your pyproject.toml configuration, or exporting
CIBW_XBUILD_TOOLS as an empty string into your environment.
"""
)
)
else:
for tool in xbuild_tools:
tool_path = shutil.which(tool)
if tool_path is None:
msg = f"Could not find a {tool!r} executable on the path."
raise errors.FatalError(msg)
# Link the binary into the safe tools directory
original = Path(tool_path).resolve()
print(f"{tool!r} will be included in the cross-build environment (using {original})")
(xbuild_tools_path / tool).symlink_to(original)
env["PATH"] = os.pathsep.join(
[
# The target python's binary directory
str(target_python.parent),
# The cross-platform environment's binary directory
str(venv_path / "bin"),
# The directory of cross-build tools
str(xbuild_tools_path),
# The bare minimum Apple system paths.
"/usr/bin",
"/bin",
"/usr/sbin",
"/sbin",
"/Library/Apple/usr/bin",
]
)
# Also unset DYLD_LIBRARY_PATH to ensure that no macOS libraries will be
# found and linked.
env.pop("DYLD_LIBRARY_PATH", None)
return env
def setup_python(
tmp: Path,
*,
python_configuration: PythonConfiguration,
dependency_constraint: Path | None,
environment: ParsedEnvironment,
build_frontend: BuildFrontendName,
xbuild_tools: Sequence[str] | None,
) -> tuple[Path, dict[str, str]]:
if build_frontend == "build[uv]":
msg = "uv doesn't support iOS"
raise errors.FatalError(msg)
# An iOS environment requires 2 python installs - one for the build machine
# (macOS), and one for the target (iOS). We'll only ever interact with the
# *target* python, but the build Python needs to exist to act as the base
# for a cross venv.
tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0]
log.step(f"Installing Build Python {implementation_id}...")
if implementation_id.startswith("cp"):
free_threading = "t-ios" in python_configuration.identifier
build_python = install_build_cpython(
tmp,
python_configuration.version,
python_configuration.build_url,
free_threading,
)
else:
msg = f"Unknown Python implementation: {implementation_id}"
raise errors.FatalError(msg)
assert build_python.exists(), (
f"{build_python.name} not found, has {list(build_python.parent.iterdir())}"
)
log.step(f"Installing Target Python {implementation_id}...")
target_install_path = install_target_cpython(tmp, python_configuration, free_threading)
target_python = (
target_install_path
/ "Python.xcframework"
/ python_configuration.xcframework_slice
/ "bin"
/ f"python{python_configuration.version}"
)
assert target_python.exists(), (
f"{target_python.name} not found, has {list(target_install_path.iterdir())}"
)
log.step("Creating cross build environment...")
venv_path = tmp / "venv"
env = cross_virtualenv(
py_version=python_configuration.version,
target_python=target_python,
multiarch=python_configuration.multiarch,
build_python=build_python,
venv_path=venv_path,
dependency_constraint=dependency_constraint,
xbuild_tools=xbuild_tools,
)
venv_bin_path = venv_path / "bin"
assert venv_bin_path.exists()
# We version pip ourselves, so we don't care about pip version checking
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
# upgrade pip to the version matching our constraints
# if necessary, reinstall it to ensure that it's available on PATH as 'pip'
pip = ["python", "-m", "pip"]
call(
*pip,
"install",
"--upgrade",
"pip",
*constraint_flags(dependency_constraint),
env=env,
cwd=venv_path,
)
# Apply our environment after pip is ready
env = environment.as_dictionary(prev_environment=env)
# Check what Python version we're on
which_python = call("which", "python", env=env, capture_stdout=True).strip()
print(which_python)
if which_python != str(venv_bin_path / "python"):
msg = (
"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."
)
raise errors.FatalError(msg)
call("python", "--version", env=env)
# Check what pip version we're on
assert (venv_bin_path / "pip").exists()
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
print(which_pip)
if which_pip != str(venv_bin_path / "pip"):
msg = (
"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."
)
raise errors.FatalError(msg)
call("pip", "--version", env=env)
# Ensure that IPHONEOS_DEPLOYMENT_TARGET is set in the environment
env.setdefault("IPHONEOS_DEPLOYMENT_TARGET", "13.0")
log.step("Installing build tools...")
match build_frontend:
case "pip":
# No additional build tools required
pass
case "build":
call(
"pip",
"install",
"--upgrade",
"build[virtualenv]",
*constraint_flags(dependency_constraint),
env=env,
)
case _:
assert_never(build_frontend)
return target_install_path, env
def build(options: Options, tmp_path: Path) -> None:
if sys.platform != "darwin":
msg = "iOS binaries can only be built on macOS"
raise errors.FatalError(msg)
python_configurations = get_python_configurations(
build_selector=options.globals.build_selector,
architectures=options.globals.architectures,
)
if not python_configurations:
return
try:
before_all_options_identifier = python_configurations[0].identifier
before_all_options = options.build_options(before_all_options_identifier)
if before_all_options.before_all:
log.step("Running before_all...")
env = before_all_options.environment.as_dictionary(prev_environment=os.environ)
env.setdefault("IPHONEOS_DEPLOYMENT_TARGET", "13.0")
before_all_prepared = prepare_command(
before_all_options.before_all,
project=".",
package=before_all_options.package_dir,
)
shell(before_all_prepared, env=env)
built_wheels: list[Path] = []
for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend
# uv doesn't support iOS
if build_frontend.name == "build[uv]":
msg = "uv doesn't support iOS"
raise errors.FatalError(msg)
log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier
identifier_tmp_dir.mkdir()
built_wheel_dir = identifier_tmp_dir / "built_wheel"
constraints_path = build_options.dependency_constraints.get_for_python_version(
version=config.version, tmp_dir=identifier_tmp_dir
)
target_install_path, env = setup_python(
identifier_tmp_dir / "build",
python_configuration=config,
dependency_constraint=constraints_path,
environment=build_options.environment,
build_frontend=build_frontend.name,
xbuild_tools=build_options.xbuild_tools,
)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
log.step_end()
print(
f"\nFound previously built wheel {compatible_wheel.name} "
f"that is compatible with {config.identifier}. "
"Skipping build step..."
)
test_wheel = compatible_wheel
else:
if build_options.before_build:
log.step("Running before_build...")
before_build_prepared = prepare_command(
build_options.before_build,
project=".",
package=build_options.package_dir,
)
shell(before_build_prepared, env=env)
log.step("Building wheel...")
built_wheel_dir.mkdir()
extra_flags = get_build_frontend_extra_flags(
build_frontend, build_options.build_verbosity, build_options.config_settings
)
match build_frontend.name:
case "pip":
# Path.resolve() is needed. Without it pip wheel may try to
# fetch package from pypi.org. See
# https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"pip",
"wheel",
build_options.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*extra_flags,
env=env,
)
case "build":
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
env=env,
)
case _:
assert_never(build_frontend)
test_wheel = built_wheel = next(built_wheel_dir.glob("*.whl"))
if built_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError()
log.step_end()
if build_options.test_command and build_options.test_selector(config.identifier):
if not config.is_simulator:
log.step("Skipping tests on non-simulator SDK")
elif config.arch != os.uname().machine:
log.step("Skipping tests on non-native simulator architecture")
else:
test_env = build_options.test_environment.as_dictionary(prev_environment=env)
if build_options.before_test:
before_test_prepared = prepare_command(
build_options.before_test,
project=".",
package=build_options.package_dir,
)
shell(before_test_prepared, env=test_env)
log.step("Setting up test harness...")
# Clone the testbed project into the build directory
testbed_path = identifier_tmp_dir / "testbed"
call(
"python",
target_install_path / "testbed",
"clone",
testbed_path,
env=test_env,
)
testbed_app_path = testbed_path / "iOSTestbed" / "app"
# Copy the test sources to the testbed app
if build_options.test_sources:
copy_test_sources(
build_options.test_sources,
Path.cwd(),
testbed_app_path,
)
else:
(testbed_app_path / "test_fail.py").write_text(
resources.TEST_FAIL_CWD_FILE.read_text()
)
log.step("Installing test requirements...")
# Install the compiled wheel (with any test extras), plus
# the test requirements. Use the --platform tag to force
# the installation of iOS wheels; this requires the use of
# --only-binary=:all:
ios_version = test_env["IPHONEOS_DEPLOYMENT_TARGET"]
platform_tag = f"ios_{ios_version.replace('.', '_')}_{config.arch}_{config.sdk}"
call(
"python",
"-m",
"pip",
"install",
"--only-binary=:all:",
"--platform",
platform_tag,
"--target",
testbed_path / "iOSTestbed" / "app_packages",
f"{test_wheel}{build_options.test_extras}",
*build_options.test_requires,
env=test_env,
)
log.step("Running test suite...")
# iOS doesn't support placeholders in the test command,
# because the source dir isn't visible on the simulator.
if (
"{project}" in build_options.test_command
or "{package}" in build_options.test_command
):
msg = unwrap_preserving_paragraphs(
f"""
iOS tests configured with a test command that uses the "{{project}}" or
"{{package}}" placeholder. iOS tests cannot use placeholders, because the
source directory is not visible on the simulator.
In addition, iOS tests must run as a Python module, so the test command
must begin with 'python -m'.
Test command: {build_options.test_command!r}
"""
)
raise errors.FatalError(msg)
test_command_list = shlex.split(build_options.test_command)
try:
for test_command_parts in split_command(test_command_list):
match test_command_parts:
case ["python", "-m", *rest]:
final_command = rest
case ["pytest", *rest]:
# pytest works exactly the same as a module, so we
# can just run it as a module.
msg = unwrap_preserving_paragraphs(f"""
iOS tests configured with a test command which doesn't start
with 'python -m'. iOS tests must execute python modules - other
entrypoints are not supported.
cibuildwheel will try to execute it as if it started with
'python -m'. If this works, all you need to do is add that to
your test command.
Test command: {build_options.test_command!r}
""")
log.warning(msg)
final_command = ["pytest", *rest]
case _:
msg = unwrap_preserving_paragraphs(
f"""
iOS tests configured with a test command which doesn't start
with 'python -m'. iOS tests must execute python modules - other
entrypoints are not supported.
Test command: {build_options.test_command!r}
"""
)
raise errors.FatalError(msg)
test_runtime_args = build_options.test_runtime.args
# 2025-10: The GitHub Actions macos-15 runner has a known issue where
# the default simulator won't start due to a disk performance issue;
# see https://github.com/actions/runner-images/issues/12777 for details.
# In the meantime, if it looks like we're running on a GitHub Actions
# macos-15 runner, use a simulator that is known to work, unless the
# user explicitly specifies a simulator.
os_version, _, arch = platform.mac_ver()
if (
"GITHUB_ACTIONS" in os.environ
and os_version.startswith("15.")
and arch == "arm64"
and not any(
arg.startswith("--simulator") for arg in test_runtime_args
)
):
test_runtime_args = [
"--simulator",
"iPhone 16e,OS=18.5",
*test_runtime_args,
]
call(
"python",
testbed_path,
"run",
*(["--verbose"] if build_options.build_verbosity > 0 else []),
*test_runtime_args,
"--",
*final_command,
env=test_env,
)
except subprocess.CalledProcessError:
# catches the first test command failure in the loop,
# implementing short-circuiting
log.step_end(success=False)
log.error(f"Test suite failed on {config.identifier}")
sys.exit(1)
log.step_end()
# We're all done here; move it to output (overwrite existing)
output_wheel: Path | None = None
if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(built_wheel.name)
moved_wheel = move_file(built_wheel, output_wheel)
if moved_wheel != output_wheel.resolve():
log.warning(
f"{built_wheel} was moved to {moved_wheel} instead of {output_wheel}"
)
built_wheels.append(output_wheel)
# Clean up
shutil.rmtree(identifier_tmp_dir)
log.build_end(output_wheel)
except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error
-553
View File
@@ -1,553 +0,0 @@
import dataclasses
import functools
import json
import os
import shutil
import subprocess
import sys
import tomllib
import typing
from collections.abc import Set
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Final, TypedDict
from filelock import FileLock
from .. import errors
from ..architecture import Architecture
from ..environment import ParsedEnvironment
from ..frontend import get_build_frontend_extra_flags
from ..logger import log
from ..options import Options
from ..selector import BuildSelector
from ..util import resources
from ..util.cmd import call, shell
from ..util.file import (
CIBW_CACHE_PATH,
copy_test_sources,
download,
extract_tar,
extract_zip,
move_file,
)
from ..util.helpers import prepare_command, unwrap, unwrap_preserving_paragraphs
from ..util.packaging import find_compatible_wheel, get_pip_version
from ..util.python_build_standalone import (
PythonBuildStandaloneError,
create_python_build_standalone_environment,
)
from ..venv import constraint_flags, virtualenv
IS_WIN: Final[bool] = sys.platform.startswith("win")
@dataclasses.dataclass(frozen=True, kw_only=True)
class PythonConfiguration:
version: str
identifier: str
default_pyodide_version: str
node_version: str
class PyodideXBuildEnvInfoVersionRange(TypedDict):
min: str | None
max: str | None
class PyodideXBuildEnvInfo(TypedDict):
version: str
python: str
emscripten: str
pyodide_build: PyodideXBuildEnvInfoVersionRange
compatible: bool
@functools.cache
def ensure_node(major_version: str) -> Path:
with resources.NODEJS.open("rb") as f:
loaded_file = tomllib.load(f)
version = str(loaded_file[major_version])
base_url = str(loaded_file["url"])
ext = "zip" if IS_WIN else "tar.xz"
platform = "win" if IS_WIN else ("darwin" if sys.platform.startswith("darwin") else "linux")
linux_arch = Architecture.native_arch("linux")
assert linux_arch is not None
arch = {"x86_64": "x64", "i686": "x86", "aarch64": "arm64"}.get(
linux_arch.value, linux_arch.value
)
name = f"node-{version}-{platform}-{arch}"
path = CIBW_CACHE_PATH / name
with FileLock(str(path) + ".lock"):
if not path.exists():
url = f"{base_url}{version}/{name}.{ext}"
with TemporaryDirectory() as tmp_path:
archive = Path(tmp_path) / f"{name}.{ext}"
download(url, archive)
if ext == "zip":
extract_zip(archive, path.parent)
else:
extract_tar(archive, path.parent)
assert path.exists()
if not IS_WIN:
return path / "bin"
return path
def install_emscripten(tmp: Path, version: str) -> Path:
url = f"https://github.com/emscripten-core/emsdk/archive/refs/tags/{version}.zip"
installation_path = CIBW_CACHE_PATH / f"emsdk-{version}"
emsdk_path = installation_path / f"emsdk-{version}/emsdk"
emcc_path = installation_path / f"emsdk-{version}/upstream/emscripten/emcc"
with FileLock(f"{installation_path}.lock"):
if installation_path.exists():
return emcc_path
emsdk_zip = tmp / "emsdk.zip"
download(url, emsdk_zip)
installation_path.mkdir()
extract_zip(emsdk_zip, installation_path)
call(emsdk_path, "install", version)
call(emsdk_path, "activate", version)
return emcc_path
def get_all_xbuildenv_version_info(env: dict[str, str]) -> list[PyodideXBuildEnvInfo]:
xbuildenvs_info_str = call(
"pyodide",
"xbuildenv",
"search",
"--json",
"--all",
env=env,
cwd=CIBW_CACHE_PATH,
capture_stdout=True,
).strip()
xbuildenvs_info = json.loads(xbuildenvs_info_str)
if "environments" not in xbuildenvs_info:
msg = f"Invalid xbuildenvs info, got {xbuildenvs_info}"
raise ValueError(msg)
return typing.cast("list[PyodideXBuildEnvInfo]", xbuildenvs_info["environments"])
def get_xbuildenv_version_info(
env: dict[str, str], version: str, pyodide_build_version: str
) -> PyodideXBuildEnvInfo:
xbuildenvs_info = get_all_xbuildenv_version_info(env)
for xbuildenv_info in xbuildenvs_info:
if xbuildenv_info["version"] == version:
return xbuildenv_info
msg = unwrap(f"""
Could not find Pyodide cross-build environment version {version} in the available
versions as reported by pyodide-build v{pyodide_build_version}.
Available pyodide xbuildenv versions are:
{", ".join(e["version"] for e in xbuildenvs_info if e["compatible"])}
""")
raise errors.FatalError(msg)
# The default pyodide xbuildenv version that's specified in
# build-platforms.toml is compatible with the pyodide-build version that's
# pinned in the bundled constraints file. But if the user changes
# pyodide-version and/or dependency-constraints in the cibuildwheel config, we
# need to check if the xbuildenv version is compatible with the pyodide-build
# version.
def validate_pyodide_build_version(
xbuildenv_info: PyodideXBuildEnvInfo, pyodide_build_version: str
) -> None:
"""
Validate the Pyodide version is compatible with the installed
pyodide-build version.
"""
pyodide_version = xbuildenv_info["version"]
if not xbuildenv_info["compatible"]:
msg = unwrap_preserving_paragraphs(f"""
The Pyodide xbuildenv version {pyodide_version} is not compatible
with the pyodide-build version {pyodide_build_version}. Please use
the 'pyodide xbuildenv search --all' command to find a compatible
version.
Set the pyodide-build version using the `dependency-constraints`
option, or set the Pyodide xbuildenv version using the
`pyodide-version` option.
""")
raise errors.FatalError(msg)
def install_xbuildenv(env: dict[str, str], pyodide_build_version: str, pyodide_version: str) -> str:
"""Install a particular Pyodide xbuildenv version and set a path to the Pyodide root."""
# Since pyodide-build was unvendored from Pyodide v0.27.0, the versions of
# pyodide-build are uncoupled from the versions of Pyodide. So, we specify
# both the pyodide-build version and the Pyodide version in the temp path.
xbuildenv_cache_path = CIBW_CACHE_PATH / f"pyodide-build-{pyodide_build_version}"
pyodide_root = xbuildenv_cache_path / pyodide_version / "xbuildenv" / "pyodide-root"
with FileLock(CIBW_CACHE_PATH / "xbuildenv.lock"):
if pyodide_root.exists():
return str(pyodide_root)
# We don't want to mutate env but we need to delete any existing
# PYODIDE_ROOT so copy it first.
env = dict(env)
env.pop("PYODIDE_ROOT", None)
# Install the xbuildenv
call(
"pyodide",
"xbuildenv",
"install",
"--path",
str(xbuildenv_cache_path),
pyodide_version,
env=env,
cwd=CIBW_CACHE_PATH,
)
assert pyodide_root.exists()
return str(pyodide_root)
def get_base_python(tmp: Path, python_configuration: PythonConfiguration) -> Path:
try:
return create_python_build_standalone_environment(
python_version=python_configuration.version,
temp_dir=tmp,
cache_dir=CIBW_CACHE_PATH,
)
except PythonBuildStandaloneError as e:
msg = unwrap(f"""
Failed to create a Python build environment:
{e}
""")
raise errors.FatalError(msg) from e
def setup_python(
tmp: Path,
python_configuration: PythonConfiguration,
constraints_path: Path | None,
environment: ParsedEnvironment,
user_pyodide_version: str | None,
) -> dict[str, str]:
log.step("Installing a base python environment...")
base_python = get_base_python(tmp / "base", python_configuration)
log.step("Setting up build environment...")
pyodide_version = user_pyodide_version or python_configuration.default_pyodide_version
venv_path = tmp / "venv"
env = virtualenv(python_configuration.version, base_python, venv_path, None, use_uv=False)
venv_bin_path = venv_path / "bin"
assert venv_bin_path.exists()
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
# upgrade pip to the version matching our constraints
# if necessary, reinstall it to ensure that it's available on PATH as 'pip'
call(
"python",
"-m",
"pip",
"install",
"--upgrade",
"pip",
*constraint_flags(constraints_path),
env=env,
cwd=venv_path,
)
env = environment.as_dictionary(prev_environment=env)
# check what Python version we're on
which_python = call("which", "python", env=env, capture_stdout=True).strip()
print(which_python)
if which_python != str(venv_bin_path / "python"):
msg = "python available on PATH doesn't match our venv instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it."
raise errors.FatalError(msg)
call("python", "--version", env=env)
# check what pip version we're on
assert (venv_bin_path / "pip").exists()
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
print(which_pip)
if which_pip != str(venv_bin_path / "pip"):
msg = "pip available on PATH doesn't match our venv instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it."
raise errors.FatalError(msg)
call("pip", "--version", env=env)
log.step("Installing build tools...")
call(
"pip",
"install",
"--upgrade",
"pyodide-build",
"build[virtualenv]",
*constraint_flags(constraints_path),
env=env,
)
pyodide_build_version = call(
"python",
"-c",
"from importlib.metadata import version; print(version('pyodide-build'))",
env=env,
capture_stdout=True,
).strip()
xbuildenv_info = get_xbuildenv_version_info(env, pyodide_version, pyodide_build_version)
validate_pyodide_build_version(
xbuildenv_info=xbuildenv_info,
pyodide_build_version=pyodide_build_version,
)
emscripten_version = xbuildenv_info["emscripten"]
log.step(f"Installing Emscripten version: {emscripten_version} ...")
emcc_path = install_emscripten(tmp, emscripten_version)
env["PATH"] = os.pathsep.join([str(emcc_path.parent), env["PATH"]])
log.step(f"Installing Pyodide xbuildenv version: {pyodide_version} ...")
env["PYODIDE_ROOT"] = install_xbuildenv(env, pyodide_build_version, pyodide_version)
return env
def all_python_configurations() -> list[PythonConfiguration]:
full_python_configs = resources.read_python_configs("pyodide")
return [PythonConfiguration(**item) for item in full_python_configs]
def get_python_configurations(
build_selector: BuildSelector,
architectures: Set[Architecture], # noqa: ARG001
) -> list[PythonConfiguration]:
return [c for c in all_python_configurations() if build_selector(c.identifier)]
def build(options: Options, tmp_path: Path) -> None:
python_configurations = get_python_configurations(
options.globals.build_selector, options.globals.architectures
)
if not python_configurations:
return
try:
before_all_options_identifier = python_configurations[0].identifier
before_all_options = options.build_options(before_all_options_identifier)
if before_all_options.before_all:
log.step("Running before_all...")
env = before_all_options.environment.as_dictionary(prev_environment=os.environ)
before_all_prepared = prepare_command(
before_all_options.before_all, project=".", package=before_all_options.package_dir
)
shell(before_all_prepared, env=env)
built_wheels: list[Path] = []
for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend
if build_frontend.name == "pip":
msg = "The pyodide platform doesn't support pip frontend"
raise errors.FatalError(msg)
log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier
built_wheel_dir = identifier_tmp_dir / "built_wheel"
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
identifier_tmp_dir.mkdir()
built_wheel_dir.mkdir()
repaired_wheel_dir.mkdir()
constraints_path = build_options.dependency_constraints.get_for_python_version(
version=config.version, variant="pyodide", tmp_dir=identifier_tmp_dir
)
env = setup_python(
tmp=identifier_tmp_dir / "build",
python_configuration=config,
constraints_path=constraints_path,
environment=build_options.environment,
user_pyodide_version=build_options.pyodide_version,
)
pip_version = get_pip_version(env)
# The Pyodide command line runner mounts all directories in the host
# filesystem into the Pyodide file system, except for the custom
# file systems /dev, /lib, /proc, and /tmp. Mounting the mount
# points for alternate file systems causes some mysterious failure
# of the process (it just quits without any clear error).
#
# Because of this, by default Pyodide can't see anything under /tmp.
# This environment variable tells it also to mount our temp
# directory.
oldmounts = ""
extra_mounts = [str(identifier_tmp_dir)]
if Path.cwd().is_relative_to("/tmp"):
extra_mounts.append(str(Path.cwd()))
if "_PYODIDE_EXTRA_MOUNTS" in env:
oldmounts = env["_PYODIDE_EXTRA_MOUNTS"] + ":"
env["_PYODIDE_EXTRA_MOUNTS"] = oldmounts + ":".join(extra_mounts)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
log.step_end()
print(
f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
)
built_wheel = compatible_wheel
else:
if build_options.before_build:
log.step("Running before_build...")
before_build_prepared = prepare_command(
build_options.before_build, project=".", package=build_options.package_dir
)
shell(before_build_prepared, env=env)
log.step("Building wheel...")
extra_flags = get_build_frontend_extra_flags(
build_frontend, build_options.build_verbosity, build_options.config_settings
)
call(
"pyodide",
"build",
build_options.package_dir,
f"--outdir={built_wheel_dir}",
*extra_flags,
env=env,
)
built_wheel = next(built_wheel_dir.glob("*.whl"))
if built_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError()
if build_options.repair_command:
log.step("Repairing wheel...")
repair_command_prepared = prepare_command(
build_options.repair_command,
wheel=built_wheel,
dest_dir=repaired_wheel_dir,
package=build_options.package_dir,
project=".",
)
shell(repair_command_prepared, env=env)
log.step_end()
else:
shutil.move(str(built_wheel), repaired_wheel_dir)
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
if build_options.test_command and build_options.test_selector(config.identifier):
log.step("Testing wheel...")
venv_dir = identifier_tmp_dir / "venv-test"
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
virtualenv_env = env.copy()
virtualenv_env["PATH"] = os.pathsep.join(
[
str(ensure_node(config.node_version)),
virtualenv_env["PATH"],
]
)
# pyodide venv uses virtualenv under the hood
# use the pip embedded with virtualenv & disable network updates
virtualenv_create_env = virtualenv_env.copy()
virtualenv_create_env["VIRTUALENV_PIP"] = pip_version
virtualenv_create_env["VIRTUALENV_NO_PERIODIC_UPDATE"] = "1"
call("pyodide", "venv", venv_dir, env=virtualenv_create_env)
virtualenv_env["PATH"] = os.pathsep.join(
[
str(venv_dir / "bin"),
virtualenv_env["PATH"],
]
)
virtualenv_env["VIRTUAL_ENV"] = str(venv_dir)
virtualenv_env = build_options.test_environment.as_dictionary(
prev_environment=virtualenv_env
)
# check that we are using the Python from the virtual environment
call("which", "python", env=virtualenv_env)
if build_options.before_test:
before_test_prepared = prepare_command(
build_options.before_test,
project=".",
package=build_options.package_dir,
wheel=repaired_wheel,
)
shell(before_test_prepared, env=virtualenv_env)
# install the wheel
call(
"pip",
"install",
f"{repaired_wheel}{build_options.test_extras}",
env=virtualenv_env,
)
# test the wheel
if build_options.test_requires:
call("pip", "install", *build_options.test_requires, env=virtualenv_env)
# run the tests from a temp dir, with an absolute path in the command
# (this ensures that Python runs the tests against the installed wheel
# and not the repo code)
test_command_prepared = prepare_command(
build_options.test_command,
project=Path.cwd(),
package=build_options.package_dir.resolve(),
)
test_cwd = identifier_tmp_dir / "test_cwd"
test_cwd.mkdir(exist_ok=True)
if build_options.test_sources:
copy_test_sources(
build_options.test_sources,
Path.cwd(),
test_cwd,
)
else:
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
(test_cwd / "test_fail.py").write_text(resources.TEST_FAIL_CWD_FILE.read_text())
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
# we're all done here; move it to output (overwrite existing)
output_wheel: Path | None = None
if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
moved_wheel = move_file(repaired_wheel, output_wheel)
if moved_wheel != output_wheel.resolve():
log.warning(
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
)
built_wheels.append(output_wheel)
log.build_end(output_wheel)
except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error
+43 -42
View File
@@ -1,10 +1,11 @@
from __future__ import annotations
import ast import ast
import configparser import configparser
import contextlib import contextlib
from pathlib import Path from pathlib import Path
from typing import Any
import dependency_groups from ._compat import tomllib
def get_parent(node: ast.AST | None, depth: int = 1) -> ast.AST | None: def get_parent(node: ast.AST | None, depth: int = 1) -> ast.AST | None:
@@ -14,16 +15,34 @@ def get_parent(node: ast.AST | None, depth: int = 1) -> ast.AST | None:
def is_main(parent: ast.AST | None) -> bool: def is_main(parent: ast.AST | None) -> bool:
match parent: if parent is None:
case ast.If(test=ast.Compare(left=left, ops=[ast.Eq()], comparators=[comp])): return False
values = {left, comp}
mains = {x for x in values if isinstance(x, ast.Constant) and x.value == "__main__"} # This would be much nicer with 3.10's pattern matching!
if len(mains) != 1: if not isinstance(parent, ast.If):
return False return False
consts = {x for x in values if isinstance(x, ast.Name) and x.id == "__name__"} if not isinstance(parent.test, ast.Compare):
return len(consts) == 1 return False
case _:
return False try:
(op,) = parent.test.ops
(comp,) = parent.test.comparators
except ValueError:
return False
if not isinstance(op, ast.Eq):
return False
values = {comp, parent.test.left}
mains = {x for x in values if isinstance(x, ast.Constant) and x.value == "__main__"}
if len(mains) != 1:
return False
consts = {x for x in values if isinstance(x, ast.Name) and x.id == "__name__"}
if len(consts) != 1:
return False
return True
class Analyzer(ast.NodeVisitor): class Analyzer(ast.NodeVisitor):
@@ -49,10 +68,12 @@ class Analyzer(ast.NodeVisitor):
parent is not None and get_parent(parent) is None and is_main(get_parent(node, 3)) parent is not None and get_parent(parent) is None and is_main(get_parent(node, 3))
) )
match node: if (
case ast.keyword(arg="python_requires", value=ast.Constant(value=str() as version)): node.arg == "python_requires"
if unnested or name_main_unnested: and isinstance(node.value, ast.Constant)
self.requires_python = version and (unnested or name_main_unnested)
):
self.requires_python = node.value.value
def setup_py_python_requires(content: str) -> str | None: def setup_py_python_requires(content: str) -> str | None:
@@ -65,12 +86,15 @@ def setup_py_python_requires(content: str) -> str | None:
return None return None
def get_requires_python_str(package_dir: Path, pyproject_toml: dict[str, Any] | None) -> str | None: def get_requires_python_str(package_dir: Path) -> str | None:
"""Return the python requires string from the most canonical source available, or None""" """Return the python requires string from the most canonical source available, or None"""
# Read in from pyproject.toml:project.requires-python # Read in from pyproject.toml:project.requires-python
with contextlib.suppress(KeyError, IndexError, TypeError): with contextlib.suppress(FileNotFoundError):
return str((pyproject_toml or {})["project"]["requires-python"]) with (package_dir / "pyproject.toml").open("rb") as f1:
info = tomllib.load(f1)
with contextlib.suppress(KeyError, IndexError, TypeError):
return str(info["project"]["requires-python"])
# Read in from setup.cfg:options.python_requires # Read in from setup.cfg:options.python_requires
config = configparser.ConfigParser() config = configparser.ConfigParser()
@@ -84,26 +108,3 @@ def get_requires_python_str(package_dir: Path, pyproject_toml: dict[str, Any] |
return setup_py_python_requires(f2.read()) return setup_py_python_requires(f2.read())
return None return None
def resolve_dependency_groups(
pyproject_toml: dict[str, Any] | None, *groups: str
) -> tuple[str, ...]:
"""
Get the packages in dependency-groups for a package.
"""
if not groups:
return ()
if pyproject_toml is None:
msg = f"Didn't find a pyproject.toml, so can't read [dependency-groups] {groups!r} from it!"
raise FileNotFoundError(msg)
try:
dependency_groups_toml = pyproject_toml["dependency-groups"]
except KeyError:
msg = f"Didn't find [dependency-groups] in pyproject.toml, which is needed to resolve {groups!r}."
raise KeyError(msg) from None
return dependency_groups.resolve(dependency_groups_toml, *groups)
-1
View File
@@ -1 +0,0 @@
import _cross_venv; _cross_venv.initialize()
-87
View File
@@ -1,87 +0,0 @@
# This module is copied into the site-packages directory of an Android build environment, and
# activated via a .pth file when we want the environment to simulate Android.
import os
import platform
import re
import sys
import sysconfig
from pathlib import Path
from typing import Any
def initialize() -> None:
if not (host_triplet := os.environ.get("CIBW_HOST_TRIPLET")):
return
# os ######################################################################
def cross_os_uname() -> os.uname_result:
return os.uname_result(
(
"Linux",
"localhost",
# The Linux kernel version and release are unlikely to be significant, but return
# realistic values anyway (from an API level 24 emulator).
"3.18.91+",
"#1 SMP PREEMPT Tue Jan 9 20:35:43 UTC 2018",
host_triplet.split("-")[0],
)
)
os.name = "posix"
os.uname = cross_os_uname
# platform ################################################################
#
# We can't determine the user-visible Android version number from the API level, so return a
# string which will work fine for display, but will fail to parse as a version number.
def cross_android_ver(*args: Any, **kwargs: Any) -> platform.AndroidVer:
return platform.AndroidVer(
release=f"API level {cross_getandroidapilevel()}",
api_level=cross_getandroidapilevel(),
manufacturer="Google",
model="sdk_gphone64",
device="emu64",
is_emulator=True,
)
# platform.uname, platform.system etc. are all implemented in terms of platform.android_ver.
platform.android_ver = cross_android_ver
# sys #####################################################################
def cross_getandroidapilevel() -> int:
api_level = sysconfig.get_config_var("ANDROID_API_LEVEL")
assert isinstance(api_level, int)
return api_level
# Some packages may recognize sys.cross_compiling from the crossenv tool.
sys.cross_compiling = True # type: ignore[attr-defined]
sys.getandroidapilevel = cross_getandroidapilevel # type: ignore[attr-defined]
sys.implementation._multiarch = host_triplet # type: ignore[attr-defined]
sys.platform = "android"
# Determine the abiflags from the sysconfigdata filename.
sysconfigdata_path = next(Path(__file__).parent.glob("_sysconfigdata_*.py"))
abiflags_match = re.match(r"_sysconfigdata_(.*?)_", sysconfigdata_path.name)
assert abiflags_match is not None
sys.abiflags = abiflags_match[1]
# sysconfig ###############################################################
#
# Load the sysconfigdata file, generating its name from sys.abiflags,
# sys.platform, and sys.implementation._multiarch.
sysconfig._init_config_vars() # type: ignore[attr-defined]
# We don't change the actual sys.base_prefix and base_exec_prefix, because that
# could have unpredictable effects. Instead, we change the sysconfig variables
# used by sysconfig.get_paths().
vars = sysconfig.get_config_vars()
try:
host_prefix = vars["host_prefix"] # This variable was added in Python 3.14.
except KeyError:
host_prefix = vars["exec_prefix"]
vars["installed_base"] = vars["installed_platbase"] = host_prefix
# sysconfig.get_platform, which determines the wheel tag, is implemented in terms of
# sys.platform, sysconfig.get_config_var("ANDROID_API_LEVEL") (see localized_vars in
# android.py), and os.uname.
-57
View File
@@ -1,57 +0,0 @@
#!/usr/bin/env python3
import os
import shutil
import subprocess
import sys
from pathlib import Path
def main() -> None:
# CIBW_HOST_TRIPLET is set in the android_env to the Android target triplet.
target = os.environ.get("CIBW_HOST_TRIPLET")
cmd_name = Path(sys.argv[0]).name
# Find the real command in PATH, excluding the current script's directory
path_env = os.environ.get("PATH", "")
script_dir = Path(__file__).resolve().parent
paths = path_env.split(os.pathsep)
# Filter out the script directory to avoid recursion
filtered_paths = [p for p in paths if Path(p).resolve() != script_dir]
filtered_path_env = os.pathsep.join(filtered_paths)
real_cmd = shutil.which(cmd_name, path=filtered_path_env)
if not real_cmd:
sys.stderr.write(f"cibuildwheel: Error: Could not find system {cmd_name}\n")
sys.exit(1)
# If we have a target (i.e. we are in the android_env), try to install it.
if target:
# Check if rustup is available to install the target
rustup_path = shutil.which("rustup", path=filtered_path_env)
if rustup_path:
try:
# We call rustup to ensure the target is installed.
subprocess.run(
[rustup_path, "target", "add", target],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
)
except subprocess.CalledProcessError as e:
sys.stderr.write(
f"cibuildwheel: Error: Failed to install Rust target {target}: {e.stderr}\n"
)
sys.exit(1)
# Execute the real command
os.execv(real_cmd, [real_cmd, *sys.argv[1:]])
if __name__ == "__main__":
main()
+58 -167
View File
@@ -1,151 +1,93 @@
[linux] [linux]
python_configurations = [ python_configurations = [
{ identifier = "cp36-manylinux_x86_64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/cp38-cp38" }, { identifier = "cp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/cp39-cp39" }, { identifier = "cp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_x86_64", version = "3.10", path_str = "/opt/python/cp310-cp310" }, { identifier = "cp310-manylinux_x86_64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-manylinux_x86_64", version = "3.11", path_str = "/opt/python/cp311-cp311" }, { identifier = "cp311-manylinux_x86_64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-manylinux_x86_64", version = "3.12", path_str = "/opt/python/cp312-cp312" }, { identifier = "cp312-manylinux_x86_64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313" }, { identifier = "cp36-manylinux_i686", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp313t-manylinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313t" }, { identifier = "cp37-manylinux_i686", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-manylinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-manylinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" }, { identifier = "cp38-manylinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_i686", version = "3.9", path_str = "/opt/python/cp39-cp39" }, { identifier = "cp39-manylinux_i686", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_i686", version = "3.10", path_str = "/opt/python/cp310-cp310" }, { identifier = "cp310-manylinux_i686", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-manylinux_i686", version = "3.11", path_str = "/opt/python/cp311-cp311" }, { identifier = "cp311-manylinux_i686", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-manylinux_i686", version = "3.12", path_str = "/opt/python/cp312-cp312" }, { identifier = "cp312-manylinux_i686", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313" }, { identifier = "pp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
{ identifier = "cp313t-manylinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp314-manylinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "pp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" }, { identifier = "pp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
{ identifier = "pp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" }, { identifier = "pp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
{ identifier = "pp310-manylinux_x86_64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" }, { identifier = "pp310-manylinux_x86_64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
{ identifier = "pp311-manylinux_x86_64", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" }, { identifier = "cp36-manylinux_aarch64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "gp311_242-manylinux_x86_64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" }, { identifier = "cp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "gp312_250-manylinux_x86_64", version = "3.12", path_str = "/opt/python/graalpy312-graalpy250_312_native" },
{ identifier = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" }, { identifier = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" }, { identifier = "cp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/cp310-cp310" }, { identifier = "cp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-manylinux_aarch64", version = "3.11", path_str = "/opt/python/cp311-cp311" }, { identifier = "cp311-manylinux_aarch64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-manylinux_aarch64", version = "3.12", path_str = "/opt/python/cp312-cp312" }, { identifier = "cp312-manylinux_aarch64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313" }, { identifier = "cp36-manylinux_ppc64le", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp313t-manylinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313t" }, { identifier = "cp37-manylinux_ppc64le", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-manylinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-manylinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" }, { identifier = "cp38-manylinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_ppc64le", version = "3.9", path_str = "/opt/python/cp39-cp39" }, { identifier = "cp39-manylinux_ppc64le", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_ppc64le", version = "3.10", path_str = "/opt/python/cp310-cp310" }, { identifier = "cp310-manylinux_ppc64le", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-manylinux_ppc64le", version = "3.11", path_str = "/opt/python/cp311-cp311" }, { identifier = "cp311-manylinux_ppc64le", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-manylinux_ppc64le", version = "3.12", path_str = "/opt/python/cp312-cp312" }, { identifier = "cp312-manylinux_ppc64le", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313" }, { identifier = "cp36-manylinux_s390x", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp313t-manylinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313t" }, { identifier = "cp37-manylinux_s390x", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-manylinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-manylinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" }, { identifier = "cp38-manylinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_s390x", version = "3.9", path_str = "/opt/python/cp39-cp39" }, { identifier = "cp39-manylinux_s390x", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_s390x", version = "3.10", path_str = "/opt/python/cp310-cp310" }, { identifier = "cp310-manylinux_s390x", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-manylinux_s390x", version = "3.11", path_str = "/opt/python/cp311-cp311" }, { identifier = "cp311-manylinux_s390x", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-manylinux_s390x", version = "3.12", path_str = "/opt/python/cp312-cp312" }, { identifier = "cp312-manylinux_s390x", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313" }, { identifier = "pp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
{ identifier = "cp313t-manylinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp314-manylinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-manylinux_armv7l", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_armv7l", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_armv7l", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-manylinux_armv7l", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-manylinux_armv7l", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-manylinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp314-manylinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-manylinux_riscv64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_riscv64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_riscv64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-manylinux_riscv64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-manylinux_riscv64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-manylinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp314-manylinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "pp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" }, { identifier = "pp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
{ identifier = "pp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" }, { identifier = "pp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
{ identifier = "pp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" }, { identifier = "pp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
{ identifier = "pp311-manylinux_aarch64", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" }, { identifier = "pp37-manylinux_i686", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
{ identifier = "gp311_242-manylinux_aarch64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" },
{ identifier = "gp312_250-manylinux_aarch64", version = "3.12", path_str = "/opt/python/graalpy312-graalpy250_312_native" },
{ identifier = "pp38-manylinux_i686", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" }, { identifier = "pp38-manylinux_i686", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
{ identifier = "pp39-manylinux_i686", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" }, { identifier = "pp39-manylinux_i686", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
{ identifier = "pp310-manylinux_i686", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" }, { identifier = "pp310-manylinux_i686", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
{ identifier = "pp311-manylinux_i686", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" }, { identifier = "cp36-musllinux_x86_64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-musllinux_x86_64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp38-musllinux_x86_64", version = "3.8", path_str = "/opt/python/cp38-cp38" }, { identifier = "cp38-musllinux_x86_64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_x86_64", version = "3.9", path_str = "/opt/python/cp39-cp39" }, { identifier = "cp39-musllinux_x86_64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_x86_64", version = "3.10", path_str = "/opt/python/cp310-cp310" }, { identifier = "cp310-musllinux_x86_64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-musllinux_x86_64", version = "3.11", path_str = "/opt/python/cp311-cp311" }, { identifier = "cp311-musllinux_x86_64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-musllinux_x86_64", version = "3.12", path_str = "/opt/python/cp312-cp312" }, { identifier = "cp312-musllinux_x86_64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313" }, { identifier = "cp36-musllinux_i686", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp313t-musllinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313t" }, { identifier = "cp37-musllinux_i686", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-musllinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" }, { identifier = "cp38-musllinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_i686", version = "3.9", path_str = "/opt/python/cp39-cp39" }, { identifier = "cp39-musllinux_i686", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_i686", version = "3.10", path_str = "/opt/python/cp310-cp310" }, { identifier = "cp310-musllinux_i686", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-musllinux_i686", version = "3.11", path_str = "/opt/python/cp311-cp311" }, { identifier = "cp311-musllinux_i686", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-musllinux_i686", version = "3.12", path_str = "/opt/python/cp312-cp312" }, { identifier = "cp312-musllinux_i686", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313" }, { identifier = "cp36-musllinux_aarch64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp313t-musllinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313t" }, { identifier = "cp37-musllinux_aarch64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-musllinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" }, { identifier = "cp38-musllinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" }, { identifier = "cp39-musllinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_aarch64", version = "3.10", path_str = "/opt/python/cp310-cp310" }, { identifier = "cp310-musllinux_aarch64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-musllinux_aarch64", version = "3.11", path_str = "/opt/python/cp311-cp311" }, { identifier = "cp311-musllinux_aarch64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-musllinux_aarch64", version = "3.12", path_str = "/opt/python/cp312-cp312" }, { identifier = "cp312-musllinux_aarch64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313" }, { identifier = "cp36-musllinux_ppc64le", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp313t-musllinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313t" }, { identifier = "cp37-musllinux_ppc64le", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-musllinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" }, { identifier = "cp38-musllinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_ppc64le", version = "3.9", path_str = "/opt/python/cp39-cp39" }, { identifier = "cp39-musllinux_ppc64le", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_ppc64le", version = "3.10", path_str = "/opt/python/cp310-cp310" }, { identifier = "cp310-musllinux_ppc64le", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-musllinux_ppc64le", version = "3.11", path_str = "/opt/python/cp311-cp311" }, { identifier = "cp311-musllinux_ppc64le", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-musllinux_ppc64le", version = "3.12", path_str = "/opt/python/cp312-cp312" }, { identifier = "cp312-musllinux_ppc64le", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313" }, { identifier = "cp36-musllinux_s390x", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp313t-musllinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313t" }, { identifier = "cp37-musllinux_s390x", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-musllinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" }, { identifier = "cp38-musllinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_s390x", version = "3.9", path_str = "/opt/python/cp39-cp39" }, { identifier = "cp39-musllinux_s390x", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_s390x", version = "3.10", path_str = "/opt/python/cp310-cp310" }, { identifier = "cp310-musllinux_s390x", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-musllinux_s390x", version = "3.11", path_str = "/opt/python/cp311-cp311" }, { identifier = "cp311-musllinux_s390x", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-musllinux_s390x", version = "3.12", path_str = "/opt/python/cp312-cp312" }, { identifier = "cp312-musllinux_s390x", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-musllinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp314-musllinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_armv7l", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_armv7l", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_armv7l", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-musllinux_armv7l", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-musllinux_armv7l", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-musllinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp314-musllinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_riscv64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_riscv64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_riscv64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-musllinux_riscv64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-musllinux_riscv64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-musllinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp314-musllinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
] ]
[macos] [macos]
python_configurations = [ python_configurations = [
{ identifier = "cp36-macosx_x86_64", version = "3.6", url = "https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg" },
{ identifier = "cp37-macosx_x86_64", version = "3.7", url = "https://www.python.org/ftp/python/3.7.9/python-3.7.9-macosx10.9.pkg" },
{ identifier = "cp38-macosx_x86_64", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" }, { identifier = "cp38-macosx_x86_64", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" },
{ identifier = "cp38-macosx_arm64", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" }, { identifier = "cp38-macosx_arm64", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" },
{ identifier = "cp38-macosx_universal2", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" }, { identifier = "cp38-macosx_universal2", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" },
@@ -155,94 +97,43 @@ python_configurations = [
{ identifier = "cp310-macosx_x86_64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" }, { identifier = "cp310-macosx_x86_64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" },
{ identifier = "cp310-macosx_arm64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" }, { identifier = "cp310-macosx_arm64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" },
{ identifier = "cp310-macosx_universal2", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" }, { identifier = "cp310-macosx_universal2", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" },
{ identifier = "cp311-macosx_x86_64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg" }, { identifier = "cp311-macosx_x86_64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.7/python-3.11.7-macos11.pkg" },
{ identifier = "cp311-macosx_arm64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg" }, { identifier = "cp311-macosx_arm64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.7/python-3.11.7-macos11.pkg" },
{ identifier = "cp311-macosx_universal2", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg" }, { identifier = "cp311-macosx_universal2", version = "3.11", url = "https://www.python.org/ftp/python/3.11.7/python-3.11.7-macos11.pkg" },
{ identifier = "cp312-macosx_x86_64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" }, { identifier = "cp312-macosx_x86_64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.1/python-3.12.1-macos11.pkg" },
{ identifier = "cp312-macosx_arm64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" }, { identifier = "cp312-macosx_arm64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.1/python-3.12.1-macos11.pkg" },
{ identifier = "cp312-macosx_universal2", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" }, { identifier = "cp312-macosx_universal2", version = "3.12", url = "https://www.python.org/ftp/python/3.12.1/python-3.12.1-macos11.pkg" },
{ identifier = "cp313-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.11/python-3.13.11-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 = "cp313-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.11/python-3.13.11-macos11.pkg" },
{ identifier = "cp313-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.11/python-3.13.11-macos11.pkg" },
{ identifier = "cp313t-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.11/python-3.13.11-macos11.pkg" },
{ identifier = "cp313t-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.11/python-3.13.11-macos11.pkg" },
{ identifier = "cp313t-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.11/python-3.13.11-macos11.pkg" },
{ identifier = "cp314-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.2/python-3.14.2-macos11.pkg" },
{ identifier = "cp314-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.2/python-3.14.2-macos11.pkg" },
{ identifier = "cp314-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.2/python-3.14.2-macos11.pkg" },
{ identifier = "cp314t-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.2/python-3.14.2-macos11.pkg" },
{ identifier = "cp314t-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.2/python-3.14.2-macos11.pkg" },
{ identifier = "cp314t-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.2/python-3.14.2-macos11.pkg" },
{ identifier = "pp38-macosx_x86_64", version = "3.8", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_x86_64.tar.bz2" }, { identifier = "pp38-macosx_x86_64", version = "3.8", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_x86_64.tar.bz2" },
{ identifier = "pp38-macosx_arm64", version = "3.8", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_arm64.tar.bz2" }, { identifier = "pp38-macosx_arm64", version = "3.8", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_arm64.tar.bz2" },
{ identifier = "pp39-macosx_x86_64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_x86_64.tar.bz2" }, { identifier = "pp39-macosx_x86_64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.15-macos_x86_64.tar.bz2" },
{ identifier = "pp39-macosx_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_arm64.tar.bz2" }, { identifier = "pp39-macosx_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.15-macos_arm64.tar.bz2" },
{ identifier = "pp310-macosx_x86_64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_x86_64.tar.bz2" }, { identifier = "pp310-macosx_x86_64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.15-macos_x86_64.tar.bz2" },
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_arm64.tar.bz2" }, { identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.15-macos_arm64.tar.bz2" },
{ identifier = "pp311-macosx_x86_64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.20-macos_x86_64.tar.bz2" },
{ identifier = "pp311-macosx_arm64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.20-macos_arm64.tar.bz2" },
{ identifier = "gp311_242-macosx_x86_64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-macos-amd64.tar.gz" },
{ identifier = "gp311_242-macosx_arm64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-macos-aarch64.tar.gz" },
{ identifier = "gp312_250-macosx_x86_64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.1/graalpy-25.0.1-macos-amd64.tar.gz" },
{ identifier = "gp312_250-macosx_arm64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.1/graalpy-25.0.1-macos-aarch64.tar.gz" },
] ]
[windows] [windows]
python_configurations = [ python_configurations = [
{ identifier = "cp38-win32", version = "3.8.10" }, { identifier = "cp36-win32", version = "3.6.8", arch = "32" },
{ identifier = "cp38-win_amd64", version = "3.8.10" }, { identifier = "cp36-win_amd64", version = "3.6.8", arch = "64" },
{ identifier = "cp39-win32", version = "3.9.13" }, { identifier = "cp37-win32", version = "3.7.9", arch = "32" },
{ identifier = "cp39-win_amd64", version = "3.9.13" }, { identifier = "cp37-win_amd64", version = "3.7.9", arch = "64" },
{ identifier = "cp310-win32", version = "3.10.11" }, { identifier = "cp38-win32", version = "3.8.10", arch = "32" },
{ identifier = "cp310-win_amd64", version = "3.10.11" }, { identifier = "cp38-win_amd64", version = "3.8.10", arch = "64" },
{ identifier = "cp311-win32", version = "3.11.9" }, { identifier = "cp39-win32", version = "3.9.13", arch = "32" },
{ identifier = "cp311-win_amd64", version = "3.11.9" }, { identifier = "cp39-win_amd64", version = "3.9.13", arch = "64" },
{ identifier = "cp312-win32", version = "3.12.10" }, { identifier = "cp310-win32", version = "3.10.11", arch = "32" },
{ identifier = "cp312-win_amd64", version = "3.12.10" }, { identifier = "cp310-win_amd64", version = "3.10.11", arch = "64" },
{ identifier = "cp313-win32", version = "3.13.11" }, { identifier = "cp311-win32", version = "3.11.7", arch = "32" },
{ identifier = "cp313t-win32", version = "3.13.11" }, { identifier = "cp311-win_amd64", version = "3.11.7", arch = "64" },
{ identifier = "cp313-win_amd64", version = "3.13.11" }, { identifier = "cp312-win32", version = "3.12.1", arch = "32" },
{ identifier = "cp313t-win_amd64", version = "3.13.11" }, { identifier = "cp312-win_amd64", version = "3.12.1", arch = "64" },
{ identifier = "cp314-win32", version = "3.14.2" }, { identifier = "cp39-win_arm64", version = "3.9.10", arch = "ARM64" },
{ identifier = "cp314t-win32", version = "3.14.2" }, { identifier = "cp310-win_arm64", version = "3.10.11", arch = "ARM64" },
{ identifier = "cp314-win_amd64", version = "3.14.2" }, { identifier = "cp311-win_arm64", version = "3.11.7", arch = "ARM64" },
{ identifier = "cp314t-win_amd64", version = "3.14.2" }, { identifier = "cp312-win_arm64", version = "3.12.1", arch = "ARM64" },
{ identifier = "cp39-win_arm64", version = "3.9.10" }, { identifier = "pp37-win_amd64", version = "3.7", arch = "64", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.9-win64.zip" },
{ identifier = "cp310-win_arm64", version = "3.10.11" }, { identifier = "pp38-win_amd64", version = "3.8", arch = "64", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip" },
{ identifier = "cp311-win_arm64", version = "3.11.9" }, { identifier = "pp39-win_amd64", version = "3.9", arch = "64", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.15-win64.zip" },
{ identifier = "cp312-win_arm64", version = "3.12.10" }, { identifier = "pp310-win_amd64", version = "3.10", arch = "64", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.15-win64.zip" },
{ identifier = "cp313-win_arm64", version = "3.13.11" },
{ identifier = "cp313t-win_arm64", version = "3.13.11" },
{ identifier = "cp314-win_arm64", version = "3.14.2" },
{ identifier = "cp314t-win_arm64", version = "3.14.2" },
{ identifier = "pp38-win_amd64", version = "3.8", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip" },
{ identifier = "pp39-win_amd64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip" },
{ identifier = "pp310-win_amd64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-win64.zip" },
{ identifier = "pp311-win_amd64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.20-win64.zip" },
{ identifier = "gp311_242-win_amd64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-windows-amd64.zip" },
{ identifier = "gp312_250-win_amd64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.1/graalpy-25.0.1-windows-amd64.zip" },
]
[pyodide]
python_configurations = [
{ identifier = "cp312-pyodide_wasm32", version = "3.12", default_pyodide_version = "0.27.7", node_version = "v22" },
{ identifier = "cp313-pyodide_wasm32", version = "3.13", default_pyodide_version = "0.29.2", node_version = "v22" },
]
[android]
python_configurations = [
{ identifier = "cp313-android_arm64_v8a", version = "3.13", url = "https://repo.maven.apache.org/maven2/com/chaquo/python/python/3.13.11/python-3.13.11-aarch64-linux-android.tar.gz" },
{ identifier = "cp313-android_x86_64", version = "3.13", url = "https://repo.maven.apache.org/maven2/com/chaquo/python/python/3.13.11/python-3.13.11-x86_64-linux-android.tar.gz" },
{ identifier = "cp314-android_arm64_v8a", version = "3.14", url = "https://www.python.org/ftp/python/3.14.2/python-3.14.2-aarch64-linux-android.tar.gz" },
{ identifier = "cp314-android_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.2/python-3.14.2-x86_64-linux-android.tar.gz" },
]
[ios]
python_configurations = [
{ identifier = "cp313-ios_arm64_iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b12/Python-3.13-iOS-support.b12.tar.gz" },
{ identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b12/Python-3.13-iOS-support.b12.tar.gz" },
{ identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b12/Python-3.13-iOS-support.b12.tar.gz" },
{ identifier = "cp314-ios_arm64_iphoneos", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b8/Python-3.14-iOS-support.b8.tar.gz" },
{ identifier = "cp314-ios_x86_64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b8/Python-3.14-iOS-support.b8.tar.gz" },
{ identifier = "cp314-ios_arm64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b8/Python-3.14-iOS-support.b8.tar.gz" },
] ]
+4 -563
View File
@@ -1,47 +1,10 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json", "$id": "https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json",
"$defs": { "$schema": "http://json-schema.org/draft-07/schema",
"inherit": {
"enum": [
"none",
"prepend",
"append"
],
"default": "none",
"description": "How to inherit the parent's value."
},
"enable": {
"enum": [
"cpython-freethreading",
"cpython-prerelease",
"graalpy",
"pyodide-prerelease",
"pypy",
"pypy-eol"
]
},
"description": "A Python version or flavor to enable."
},
"additionalProperties": false, "additionalProperties": false,
"description": "cibuildwheel's settings.", "description": "cibuildwheel's settings.",
"type": "object", "type": "object",
"properties": { "properties": {
"audit": {
"description": "Execute a shell command to audit each wheel after all builds complete. Use {wheel} for each wheel path, or {abi3_wheel} to only audit abi3 wheels.",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"title": "CIBW_AUDIT"
},
"archs": { "archs": {
"description": "Change the architectures built on your machine by default.", "description": "Change the architectures built on your machine by default.",
"oneOf": [ "oneOf": [
@@ -122,13 +85,12 @@
}, },
"build-frontend": { "build-frontend": {
"default": "default", "default": "default",
"description": "Set the tool to use to build, either \"build\" (default), \"build[uv]\", or \"pip\"", "description": "Set the tool to use to build, either \"pip\" (default for now) or \"build\"",
"oneOf": [ "oneOf": [
{ {
"enum": [ "enum": [
"pip", "pip",
"build", "build",
"build[uv]",
"default" "default"
] ]
}, },
@@ -140,10 +102,6 @@
"type": "string", "type": "string",
"pattern": "^build; ?args:" "pattern": "^build; ?args:"
}, },
{
"type": "string",
"pattern": "^build\\[uv\\]; ?args:"
},
{ {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -154,8 +112,7 @@
"name": { "name": {
"enum": [ "enum": [
"pip", "pip",
"build", "build"
"build[uv]"
] ]
}, },
"args": { "args": {
@@ -251,62 +208,9 @@
"dependency-versions": { "dependency-versions": {
"default": "pinned", "default": "pinned",
"description": "Specify how cibuildwheel controls the versions of the tools it uses", "description": "Specify how cibuildwheel controls the versions of the tools it uses",
"oneOf": [ "type": "string",
{
"enum": [
"pinned",
"latest"
]
},
{
"type": "string",
"description": "Path to a file containing dependency versions, or inline package specifications, starting with \"packages:\"",
"not": {
"enum": [
"pinned",
"latest"
]
}
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"file": {
"type": "string"
}
}
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"packages": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
],
"title": "CIBW_DEPENDENCY_VERSIONS" "title": "CIBW_DEPENDENCY_VERSIONS"
}, },
"enable": {
"description": "Enable or disable certain builds.",
"oneOf": [
{
"$ref": "#/$defs/enable"
},
{
"type": "array",
"items": {
"$ref": "#/$defs/enable"
}
}
],
"title": "CIBW_ENABLE"
},
"environment": { "environment": {
"description": "Set environment variables needed during the build.", "description": "Set environment variables needed during the build.",
"oneOf": [ "oneOf": [
@@ -345,11 +249,6 @@
"description": "Specify alternative manylinux / musllinux container images", "description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MANYLINUX_AARCH64_IMAGE" "title": "CIBW_MANYLINUX_AARCH64_IMAGE"
}, },
"manylinux-armv7l-image": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MANYLINUX_ARMV7L_IMAGE"
},
"manylinux-i686-image": { "manylinux-i686-image": {
"type": "string", "type": "string",
"description": "Specify alternative manylinux / musllinux container images", "description": "Specify alternative manylinux / musllinux container images",
@@ -375,11 +274,6 @@
"description": "Specify alternative manylinux / musllinux container images", "description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MANYLINUX_PYPY_X86_64_IMAGE" "title": "CIBW_MANYLINUX_PYPY_X86_64_IMAGE"
}, },
"manylinux-riscv64-image": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MANYLINUX_RISCV64_IMAGE"
},
"manylinux-s390x-image": { "manylinux-s390x-image": {
"type": "string", "type": "string",
"description": "Specify alternative manylinux / musllinux container images", "description": "Specify alternative manylinux / musllinux container images",
@@ -395,11 +289,6 @@
"description": "Specify alternative manylinux / musllinux container images", "description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MUSLLINUX_AARCH64_IMAGE" "title": "CIBW_MUSLLINUX_AARCH64_IMAGE"
}, },
"musllinux-armv7l-image": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MUSLLINUX_ARMV7L_IMAGE"
},
"musllinux-i686-image": { "musllinux-i686-image": {
"type": "string", "type": "string",
"description": "Specify alternative manylinux / musllinux container images", "description": "Specify alternative manylinux / musllinux container images",
@@ -410,11 +299,6 @@
"description": "Specify alternative manylinux / musllinux container images", "description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MUSLLINUX_PPC64LE_IMAGE" "title": "CIBW_MUSLLINUX_PPC64LE_IMAGE"
}, },
"musllinux-riscv64-image": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MUSLLINUX_RISCV64_IMAGE"
},
"musllinux-s390x-image": { "musllinux-s390x-image": {
"type": "string", "type": "string",
"description": "Specify alternative manylinux / musllinux container images", "description": "Specify alternative manylinux / musllinux container images",
@@ -425,26 +309,6 @@
"description": "Specify alternative manylinux / musllinux container images", "description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MUSLLINUX_X86_64_IMAGE" "title": "CIBW_MUSLLINUX_X86_64_IMAGE"
}, },
"xbuild-tools": {
"description": "Binaries on the path that should be included in an isolated cross-build environment",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"title": "CIBW_XBUILD_TOOLS"
},
"pyodide-version": {
"type": "string",
"description": "Specify the version of Pyodide to use",
"title": "CIBW_PYODIDE_VERSION"
},
"repair-wheel-command": { "repair-wheel-command": {
"description": "Execute a shell command to repair each built wheel.", "description": "Execute a shell command to repair each built wheel.",
"oneOf": [ "oneOf": [
@@ -505,36 +369,6 @@
], ],
"title": "CIBW_TEST_EXTRAS" "title": "CIBW_TEST_EXTRAS"
}, },
"test-sources": {
"description": "Test files that are required by the test environment",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"title": "CIBW_TEST_SOURCES"
},
"test-groups": {
"description": "Install extra groups when testing",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"title": "CIBW_TEST_GROUPS"
},
"test-requires": { "test-requires": {
"description": "Install Python dependencies before running the tests", "description": "Install Python dependencies before running the tests",
"oneOf": [ "oneOf": [
@@ -565,57 +399,6 @@
], ],
"title": "CIBW_TEST_SKIP" "title": "CIBW_TEST_SKIP"
}, },
"test-environment": {
"description": "Set environment variables for the test environment",
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"additionalProperties": false,
"patternProperties": {
".+": {
"type": "string"
}
}
}
],
"title": "CIBW_TEST_ENVIRONMENT"
},
"test-runtime": {
"description": "Additional configuration for the test runner",
"oneOf": [
{
"type": "string",
"pattern": "^$"
},
{
"type": "object",
"additionalProperties": false
},
{
"type": "string",
"pattern": "args:"
},
{
"type": "object",
"additionalProperties": false,
"required": [
"args"
],
"properties": {
"args": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
],
"title": "CIBW_TEST_RUNTIME"
},
"overrides": { "overrides": {
"type": "array", "type": "array",
"description": "An overrides array", "description": "An overrides array",
@@ -640,57 +423,6 @@
} }
] ]
}, },
"inherit": {
"type": "object",
"additionalProperties": false,
"properties": {
"before-all": {
"$ref": "#/$defs/inherit"
},
"before-build": {
"$ref": "#/$defs/inherit"
},
"xbuild-tools": {
"$ref": "#/$defs/inherit"
},
"before-test": {
"$ref": "#/$defs/inherit"
},
"config-settings": {
"$ref": "#/$defs/inherit"
},
"container-engine": {
"$ref": "#/$defs/inherit"
},
"environment": {
"$ref": "#/$defs/inherit"
},
"environment-pass": {
"$ref": "#/$defs/inherit"
},
"repair-wheel-command": {
"$ref": "#/$defs/inherit"
},
"test-command": {
"$ref": "#/$defs/inherit"
},
"test-extras": {
"$ref": "#/$defs/inherit"
},
"test-sources": {
"$ref": "#/$defs/inherit"
},
"test-requires": {
"$ref": "#/$defs/inherit"
},
"test-environment": {
"$ref": "#/$defs/inherit"
},
"test-runtime": {
"$ref": "#/$defs/inherit"
}
}
},
"before-all": { "before-all": {
"$ref": "#/properties/before-all" "$ref": "#/properties/before-all"
}, },
@@ -709,9 +441,6 @@
"config-settings": { "config-settings": {
"$ref": "#/properties/config-settings" "$ref": "#/properties/config-settings"
}, },
"container-engine": {
"$ref": "#/properties/container-engine"
},
"dependency-versions": { "dependency-versions": {
"$ref": "#/properties/dependency-versions" "$ref": "#/properties/dependency-versions"
}, },
@@ -724,9 +453,6 @@
"manylinux-aarch64-image": { "manylinux-aarch64-image": {
"$ref": "#/properties/manylinux-aarch64-image" "$ref": "#/properties/manylinux-aarch64-image"
}, },
"manylinux-armv7l-image": {
"$ref": "#/properties/manylinux-armv7l-image"
},
"manylinux-i686-image": { "manylinux-i686-image": {
"$ref": "#/properties/manylinux-i686-image" "$ref": "#/properties/manylinux-i686-image"
}, },
@@ -742,9 +468,6 @@
"manylinux-pypy_x86_64-image": { "manylinux-pypy_x86_64-image": {
"$ref": "#/properties/manylinux-pypy_x86_64-image" "$ref": "#/properties/manylinux-pypy_x86_64-image"
}, },
"manylinux-riscv64-image": {
"$ref": "#/properties/manylinux-riscv64-image"
},
"manylinux-s390x-image": { "manylinux-s390x-image": {
"$ref": "#/properties/manylinux-s390x-image" "$ref": "#/properties/manylinux-s390x-image"
}, },
@@ -754,30 +477,18 @@
"musllinux-aarch64-image": { "musllinux-aarch64-image": {
"$ref": "#/properties/musllinux-aarch64-image" "$ref": "#/properties/musllinux-aarch64-image"
}, },
"musllinux-armv7l-image": {
"$ref": "#/properties/musllinux-armv7l-image"
},
"musllinux-i686-image": { "musllinux-i686-image": {
"$ref": "#/properties/musllinux-i686-image" "$ref": "#/properties/musllinux-i686-image"
}, },
"musllinux-ppc64le-image": { "musllinux-ppc64le-image": {
"$ref": "#/properties/musllinux-ppc64le-image" "$ref": "#/properties/musllinux-ppc64le-image"
}, },
"musllinux-riscv64-image": {
"$ref": "#/properties/musllinux-riscv64-image"
},
"musllinux-s390x-image": { "musllinux-s390x-image": {
"$ref": "#/properties/musllinux-s390x-image" "$ref": "#/properties/musllinux-s390x-image"
}, },
"musllinux-x86_64-image": { "musllinux-x86_64-image": {
"$ref": "#/properties/musllinux-x86_64-image" "$ref": "#/properties/musllinux-x86_64-image"
}, },
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": { "repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command" "$ref": "#/properties/repair-wheel-command"
}, },
@@ -787,20 +498,8 @@
"test-extras": { "test-extras": {
"$ref": "#/properties/test-extras" "$ref": "#/properties/test-extras"
}, },
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": { "test-requires": {
"$ref": "#/properties/test-requires" "$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
},
"test-runtime": {
"$ref": "#/properties/test-runtime"
} }
} }
} }
@@ -830,9 +529,6 @@
"config-settings": { "config-settings": {
"$ref": "#/properties/config-settings" "$ref": "#/properties/config-settings"
}, },
"container-engine": {
"$ref": "#/properties/container-engine"
},
"environment": { "environment": {
"$ref": "#/properties/environment" "$ref": "#/properties/environment"
}, },
@@ -842,9 +538,6 @@
"manylinux-aarch64-image": { "manylinux-aarch64-image": {
"$ref": "#/properties/manylinux-aarch64-image" "$ref": "#/properties/manylinux-aarch64-image"
}, },
"manylinux-armv7l-image": {
"$ref": "#/properties/manylinux-armv7l-image"
},
"manylinux-i686-image": { "manylinux-i686-image": {
"$ref": "#/properties/manylinux-i686-image" "$ref": "#/properties/manylinux-i686-image"
}, },
@@ -860,9 +553,6 @@
"manylinux-pypy_x86_64-image": { "manylinux-pypy_x86_64-image": {
"$ref": "#/properties/manylinux-pypy_x86_64-image" "$ref": "#/properties/manylinux-pypy_x86_64-image"
}, },
"manylinux-riscv64-image": {
"$ref": "#/properties/manylinux-riscv64-image"
},
"manylinux-s390x-image": { "manylinux-s390x-image": {
"$ref": "#/properties/manylinux-s390x-image" "$ref": "#/properties/manylinux-s390x-image"
}, },
@@ -872,30 +562,18 @@
"musllinux-aarch64-image": { "musllinux-aarch64-image": {
"$ref": "#/properties/musllinux-aarch64-image" "$ref": "#/properties/musllinux-aarch64-image"
}, },
"musllinux-armv7l-image": {
"$ref": "#/properties/musllinux-armv7l-image"
},
"musllinux-i686-image": { "musllinux-i686-image": {
"$ref": "#/properties/musllinux-i686-image" "$ref": "#/properties/musllinux-i686-image"
}, },
"musllinux-ppc64le-image": { "musllinux-ppc64le-image": {
"$ref": "#/properties/musllinux-ppc64le-image" "$ref": "#/properties/musllinux-ppc64le-image"
}, },
"musllinux-riscv64-image": {
"$ref": "#/properties/musllinux-riscv64-image"
},
"musllinux-s390x-image": { "musllinux-s390x-image": {
"$ref": "#/properties/musllinux-s390x-image" "$ref": "#/properties/musllinux-s390x-image"
}, },
"musllinux-x86_64-image": { "musllinux-x86_64-image": {
"$ref": "#/properties/musllinux-x86_64-image" "$ref": "#/properties/musllinux-x86_64-image"
}, },
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": { "repair-wheel-command": {
"description": "Execute a shell command to repair each built wheel.", "description": "Execute a shell command to repair each built wheel.",
"oneOf": [ "oneOf": [
@@ -918,20 +596,8 @@
"test-extras": { "test-extras": {
"$ref": "#/properties/test-extras" "$ref": "#/properties/test-extras"
}, },
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": { "test-requires": {
"$ref": "#/properties/test-requires" "$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
},
"test-runtime": {
"$ref": "#/properties/test-runtime"
} }
} }
}, },
@@ -966,12 +632,6 @@
"environment": { "environment": {
"$ref": "#/properties/environment" "$ref": "#/properties/environment"
}, },
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": { "repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command" "$ref": "#/properties/repair-wheel-command"
}, },
@@ -981,20 +641,8 @@
"test-extras": { "test-extras": {
"$ref": "#/properties/test-extras" "$ref": "#/properties/test-extras"
}, },
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": { "test-requires": {
"$ref": "#/properties/test-requires" "$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
},
"test-runtime": {
"$ref": "#/properties/test-runtime"
} }
} }
}, },
@@ -1029,12 +677,6 @@
"environment": { "environment": {
"$ref": "#/properties/environment" "$ref": "#/properties/environment"
}, },
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": { "repair-wheel-command": {
"description": "Execute a shell command to repair each built wheel.", "description": "Execute a shell command to repair each built wheel.",
"oneOf": [ "oneOf": [
@@ -1057,209 +699,8 @@
"test-extras": { "test-extras": {
"$ref": "#/properties/test-extras" "$ref": "#/properties/test-extras"
}, },
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": { "test-requires": {
"$ref": "#/properties/test-requires" "$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
},
"test-runtime": {
"$ref": "#/properties/test-runtime"
}
}
},
"pyodide": {
"type": "object",
"additionalProperties": false,
"properties": {
"archs": {
"$ref": "#/properties/archs"
},
"before-all": {
"$ref": "#/properties/before-all"
},
"before-build": {
"$ref": "#/properties/before-build"
},
"before-test": {
"$ref": "#/properties/before-test"
},
"build-frontend": {
"$ref": "#/properties/build-frontend"
},
"build-verbosity": {
"$ref": "#/properties/build-verbosity"
},
"config-settings": {
"$ref": "#/properties/config-settings"
},
"dependency-versions": {
"$ref": "#/properties/dependency-versions"
},
"environment": {
"$ref": "#/properties/environment"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command"
},
"test-command": {
"$ref": "#/properties/test-command"
},
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
},
"test-runtime": {
"$ref": "#/properties/test-runtime"
}
}
},
"android": {
"type": "object",
"additionalProperties": false,
"properties": {
"archs": {
"$ref": "#/properties/archs"
},
"before-all": {
"$ref": "#/properties/before-all"
},
"before-build": {
"$ref": "#/properties/before-build"
},
"before-test": {
"$ref": "#/properties/before-test"
},
"build-frontend": {
"$ref": "#/properties/build-frontend"
},
"build-verbosity": {
"$ref": "#/properties/build-verbosity"
},
"config-settings": {
"$ref": "#/properties/config-settings"
},
"dependency-versions": {
"$ref": "#/properties/dependency-versions"
},
"environment": {
"$ref": "#/properties/environment"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command"
},
"test-command": {
"$ref": "#/properties/test-command"
},
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
},
"test-runtime": {
"$ref": "#/properties/test-runtime"
}
}
},
"ios": {
"type": "object",
"additionalProperties": false,
"properties": {
"archs": {
"$ref": "#/properties/archs"
},
"before-all": {
"$ref": "#/properties/before-all"
},
"before-build": {
"$ref": "#/properties/before-build"
},
"before-test": {
"$ref": "#/properties/before-test"
},
"build-frontend": {
"$ref": "#/properties/build-frontend"
},
"build-verbosity": {
"$ref": "#/properties/build-verbosity"
},
"config-settings": {
"$ref": "#/properties/config-settings"
},
"dependency-versions": {
"$ref": "#/properties/dependency-versions"
},
"environment": {
"$ref": "#/properties/environment"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command"
},
"test-command": {
"$ref": "#/properties/test-command"
},
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
},
"test-runtime": {
"$ref": "#/properties/test-runtime"
} }
} }
} }
@@ -1,114 +0,0 @@
# This file was autogenerated by uv via the following command:
# nox -s update_constraints
annotated-types==0.7.0
# via pydantic
anyio==4.12.1
# via httpx
auditwheel-emscripten==0.2.1
# via pyodide-build
build==1.2.2.post1
# via
# -r .nox/update_constraints/tmp/constraints-pyodide.in
# pyodide-build
certifi==2026.1.4
# via
# httpcore
# httpx
# requests
charset-normalizer==3.4.4
# via requests
click==8.1.8
# via
# -r .nox/update_constraints/tmp/constraints-pyodide.in
# typer
distlib==0.4.0
# via virtualenv
filelock==3.20.3
# via virtualenv
h11==0.16.0
# via httpcore
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via unearth
idna==3.11
# via
# anyio
# httpx
# requests
leb128==1.0.9
# via auditwheel-emscripten
markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
packaging==26.0
# via
# auditwheel-emscripten
# build
# pyodide-build
# unearth
# wheel
pip==25.3
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
platformdirs==4.5.1
# via
# pyodide-build
# virtualenv
pydantic==2.12.5
# via
# pyodide-build
# pyodide-lock
pydantic-core==2.41.5
# via pydantic
pygments==2.19.2
# via rich
pyodide-build==0.31.2
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
pyodide-cli==0.4.2
# via
# auditwheel-emscripten
# pyodide-build
pyodide-lock==0.1.1
# via pyodide-build
pyproject-hooks==1.2.0
# via build
requests==2.32.5
# via pyodide-build
resolvelib==1.2.1
# via pyodide-build
rich==14.3.1
# via
# pyodide-build
# pyodide-cli
# typer
ruamel-yaml==0.19.1
# via pyodide-build
shellingham==1.5.4
# via typer
typer==0.21.1
# via
# auditwheel-emscripten
# pyodide-build
# pyodide-cli
typing-extensions==4.15.0
# via
# anyio
# pydantic
# pydantic-core
# typer
# typing-inspection
typing-inspection==0.4.2
# via pydantic
unearth==0.18.2
# via pyodide-build
urllib3==2.6.3
# via requests
virtualenv==20.36.1
# via
# build
# pyodide-build
wheel==0.46.3
# via
# auditwheel-emscripten
# pyodide-build
@@ -1,113 +0,0 @@
# This file was autogenerated by uv via the following command:
# nox -s update_constraints
annotated-types==0.7.0
# via pydantic
anyio==4.12.1
# via httpx
auditwheel-emscripten==0.2.1
# via pyodide-build
build==1.2.2.post1
# via
# -r .nox/update_constraints/tmp/constraints-pyodide.in
# pyodide-build
certifi==2026.1.4
# via
# httpcore
# httpx
# requests
charset-normalizer==3.4.4
# via requests
click==8.1.8
# via
# -r .nox/update_constraints/tmp/constraints-pyodide.in
# typer
distlib==0.4.0
# via virtualenv
filelock==3.20.3
# via virtualenv
h11==0.16.0
# via httpcore
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via unearth
idna==3.11
# via
# anyio
# httpx
# requests
leb128==1.0.9
# via auditwheel-emscripten
markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
packaging==26.0
# via
# auditwheel-emscripten
# build
# pyodide-build
# unearth
# wheel
pip==25.3
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
platformdirs==4.5.1
# via
# pyodide-build
# virtualenv
pydantic==2.12.5
# via
# pyodide-build
# pyodide-lock
pydantic-core==2.41.5
# via pydantic
pygments==2.19.2
# via rich
pyodide-build==0.31.2
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
pyodide-cli==0.4.2
# via
# auditwheel-emscripten
# pyodide-build
pyodide-lock==0.1.1
# via pyodide-build
pyproject-hooks==1.2.0
# via build
requests==2.32.5
# via pyodide-build
resolvelib==1.2.1
# via pyodide-build
rich==14.3.1
# via
# pyodide-build
# pyodide-cli
# typer
ruamel-yaml==0.19.1
# via pyodide-build
shellingham==1.5.4
# via typer
typer==0.21.1
# via
# auditwheel-emscripten
# pyodide-build
# pyodide-cli
typing-extensions==4.15.0
# via
# pydantic
# pydantic-core
# typer
# typing-inspection
typing-inspection==0.4.2
# via pydantic
unearth==0.18.2
# via pyodide-build
urllib3==2.6.3
# via requests
virtualenv==20.36.1
# via
# build
# pyodide-build
wheel==0.46.3
# via
# auditwheel-emscripten
# pyodide-build
@@ -1,36 +1,28 @@
# This file was autogenerated by uv via the following command: #
# nox -s update_constraints # This file is autogenerated by pip-compile with Python 3.10
altgraph==0.17.5 # by the following command:
# via macholib #
build==1.4.0 # nox -s update_constraints-3.10
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
delocate==0.13.0 distlib==0.3.8
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
# via virtualenv # via virtualenv
filelock==3.20.3 filelock==3.13.1
# via virtualenv # via virtualenv
importlib-metadata==8.7.1 packaging==23.2
# via build
macholib==1.16.4
# via delocate # via delocate
packaging==26.0 platformdirs==4.1.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.5.1
# via virtualenv # via virtualenv
pyproject-hooks==1.2.0 typing-extensions==4.9.0
# via build # via delocate
tomli==2.4.0 virtualenv==20.25.0
# via build # via -r cibuildwheel/resources/constraints.in
typing-extensions==4.15.0 wheel==0.42.0
# via # via -r cibuildwheel/resources/constraints.in
# delocate
# virtualenv # The following packages are considered to be unsafe in a requirements file:
virtualenv==20.36.1 pip==23.3.2
# via -r cibuildwheel/resources/constraints.in
setuptools==69.0.3
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
zipp==3.23.0
# via importlib-metadata
@@ -1,28 +1,28 @@
# This file was autogenerated by uv via the following command: #
# nox -s update_constraints # This file is autogenerated by pip-compile with Python 3.11
altgraph==0.17.5 # by the following command:
# via macholib #
build==1.4.0 # nox -s update_constraints-3.11
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
delocate==0.13.0 distlib==0.3.8
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
# via virtualenv # via virtualenv
filelock==3.20.3 filelock==3.13.1
# via virtualenv # via virtualenv
macholib==1.16.4 packaging==23.2
# via delocate # via delocate
packaging==26.0 platformdirs==4.1.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.5.1
# via virtualenv # via virtualenv
pyproject-hooks==1.2.0 typing-extensions==4.9.0
# via build
typing-extensions==4.15.0
# via delocate # via delocate
virtualenv==20.36.1 virtualenv==20.25.0
# via -r cibuildwheel/resources/constraints.in
wheel==0.42.0
# via -r cibuildwheel/resources/constraints.in
# The following packages are considered to be unsafe in a requirements file:
pip==23.3.2
# via -r cibuildwheel/resources/constraints.in
setuptools==69.0.3
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
@@ -1,28 +1,28 @@
# This file was autogenerated by uv via the following command: #
# nox -s update_constraints # This file is autogenerated by pip-compile with Python 3.12
altgraph==0.17.5 # by the following command:
# via macholib #
build==1.4.0 # nox -s update_constraints-3.12
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
delocate==0.13.0 distlib==0.3.8
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
# via virtualenv # via virtualenv
filelock==3.20.3 filelock==3.13.1
# via virtualenv # via virtualenv
macholib==1.16.4 packaging==23.2
# via delocate # via delocate
packaging==26.0 platformdirs==4.1.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.5.1
# via virtualenv # via virtualenv
pyproject-hooks==1.2.0 typing-extensions==4.9.0
# via build
typing-extensions==4.15.0
# via delocate # via delocate
virtualenv==20.36.1 virtualenv==20.25.0
# via -r cibuildwheel/resources/constraints.in
wheel==0.42.0
# via -r cibuildwheel/resources/constraints.in
# The following packages are considered to be unsafe in a requirements file:
pip==23.3.2
# via -r cibuildwheel/resources/constraints.in
setuptools==69.0.3
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
@@ -1,28 +0,0 @@
# This file was autogenerated by uv via the following command:
# nox -s update_constraints
altgraph==0.17.5
# via macholib
build==1.4.0
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
# via virtualenv
filelock==3.20.3
# via virtualenv
macholib==1.16.4
# via delocate
packaging==26.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.5.1
# via virtualenv
pyproject-hooks==1.2.0
# via build
typing-extensions==4.15.0
# via delocate
virtualenv==20.36.1
# via -r cibuildwheel/resources/constraints.in
@@ -1,28 +0,0 @@
# This file was autogenerated by uv via the following command:
# nox -s update_constraints
altgraph==0.17.5
# via macholib
build==1.4.0
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
# via virtualenv
filelock==3.20.3
# via virtualenv
macholib==1.16.4
# via delocate
packaging==26.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.5.1
# via virtualenv
pyproject-hooks==1.2.0
# via build
typing-extensions==4.15.0
# via delocate
virtualenv==20.36.1
# via -r cibuildwheel/resources/constraints.in
@@ -0,0 +1,38 @@
#
# This file is autogenerated by pip-compile with python 3.6
# To update, run:
#
# nox -s update_constraints-3.6
#
delocate==0.10.2
# via -r cibuildwheel/resources/constraints.in
distlib==0.3.6
# via virtualenv
filelock==3.4.1
# via virtualenv
importlib-metadata==4.8.3
# via virtualenv
importlib-resources==5.4.0
# via virtualenv
platformdirs==2.4.0
# via virtualenv
typing-extensions==4.1.1
# via
# delocate
# importlib-metadata
virtualenv==20.17.1
# via -r cibuildwheel/resources/constraints.in
wheel==0.37.1
# via
# -r cibuildwheel/resources/constraints.in
# delocate
zipp==3.6.0
# via
# importlib-metadata
# importlib-resources
# The following packages are considered to be unsafe in a requirements file:
pip==21.3.1
# via -r cibuildwheel/resources/constraints.in
setuptools==59.6.0
# via -r cibuildwheel/resources/constraints.in
@@ -0,0 +1,35 @@
#
# This file is autogenerated by pip-compile with Python 3.7
# by the following command:
#
# nox -s update_constraints-3.7
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in
distlib==0.3.8
# via virtualenv
filelock==3.12.2
# via virtualenv
importlib-metadata==6.7.0
# via virtualenv
packaging==23.2
# via delocate
platformdirs==4.0.0
# via virtualenv
typing-extensions==4.7.1
# via
# delocate
# importlib-metadata
# platformdirs
virtualenv==20.25.0
# via -r cibuildwheel/resources/constraints.in
wheel==0.42.0
# via -r cibuildwheel/resources/constraints.in
zipp==3.15.0
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
pip==23.3.2
# via -r cibuildwheel/resources/constraints.in
setuptools==68.0.0
# via -r cibuildwheel/resources/constraints.in
+22 -30
View File
@@ -1,36 +1,28 @@
# This file was autogenerated by uv via the following command: #
# nox -s update_constraints # This file is autogenerated by pip-compile with Python 3.8
altgraph==0.17.5 # by the following command:
# via macholib #
build==1.2.2.post1 # nox -s update_constraints-3.8
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
delocate==0.12.0 distlib==0.3.8
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
# via virtualenv # via virtualenv
filelock==3.16.1 filelock==3.13.1
# via virtualenv # via virtualenv
importlib-metadata==8.5.0 packaging==23.2
# via build
macholib==1.16.4
# via delocate # via delocate
packaging==26.0 platformdirs==4.1.0
# via
# build
# delocate
pip==25.0.1
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.3.6
# via virtualenv # via virtualenv
pyproject-hooks==1.2.0 typing-extensions==4.9.0
# via build # via delocate
tomli==2.4.0 virtualenv==20.25.0
# via build # via -r cibuildwheel/resources/constraints.in
typing-extensions==4.13.2 wheel==0.42.0
# via # via -r cibuildwheel/resources/constraints.in
# delocate
# virtualenv # The following packages are considered to be unsafe in a requirements file:
virtualenv==20.36.1 pip==23.3.2
# via -r cibuildwheel/resources/constraints.in
setuptools==69.0.3
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
zipp==3.20.2
# via importlib-metadata
+22 -30
View File
@@ -1,36 +1,28 @@
# This file was autogenerated by uv via the following command: #
# nox -s update_constraints # This file is autogenerated by pip-compile with Python 3.9
altgraph==0.17.5 # by the following command:
# via macholib #
build==1.4.0 # nox -s update_constraints-3.9
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
delocate==0.13.0 distlib==0.3.8
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
# via virtualenv # via virtualenv
filelock==3.19.1 filelock==3.13.1
# via virtualenv # via virtualenv
importlib-metadata==8.7.1 packaging==23.2
# via build
macholib==1.16.4
# via delocate # via delocate
packaging==26.0 platformdirs==4.1.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.4.0
# via virtualenv # via virtualenv
pyproject-hooks==1.2.0 typing-extensions==4.9.0
# via build # via delocate
tomli==2.4.0 virtualenv==20.25.0
# via build # via -r cibuildwheel/resources/constraints.in
typing-extensions==4.15.0 wheel==0.42.0
# via # via -r cibuildwheel/resources/constraints.in
# delocate
# virtualenv # The following packages are considered to be unsafe in a requirements file:
virtualenv==20.36.1 pip==23.3.2
# via -r cibuildwheel/resources/constraints.in
setuptools==69.0.3
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
zipp==3.23.0
# via importlib-metadata
+2 -1
View File
@@ -1,4 +1,5 @@
pip pip
build setuptools
wheel
delocate delocate
virtualenv virtualenv
+21 -21
View File
@@ -1,28 +1,28 @@
# This file was autogenerated by uv via the following command: #
# nox -s update_constraints # This file is autogenerated by pip-compile with Python 3.12
altgraph==0.17.5 # by the following command:
# via macholib #
build==1.4.0 # nox -s update_constraints-3.12
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
delocate==0.13.0 distlib==0.3.8
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
# via virtualenv # via virtualenv
filelock==3.20.3 filelock==3.13.1
# via virtualenv # via virtualenv
macholib==1.16.4 packaging==23.2
# via delocate # via delocate
packaging==26.0 platformdirs==4.1.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.5.1
# via virtualenv # via virtualenv
pyproject-hooks==1.2.0 typing-extensions==4.9.0
# via build
typing-extensions==4.15.0
# via delocate # via delocate
virtualenv==20.36.1 virtualenv==20.25.0
# via -r cibuildwheel/resources/constraints.in
wheel==0.42.0
# via -r cibuildwheel/resources/constraints.in
# The following packages are considered to be unsafe in a requirements file:
pip==23.3.2
# via -r cibuildwheel/resources/constraints.in
setuptools==69.0.3
# via -r cibuildwheel/resources/constraints.in # via -r cibuildwheel/resources/constraints.in
+13 -33
View File
@@ -2,10 +2,8 @@
build = "*" build = "*"
skip = "" skip = ""
test-skip = "" test-skip = ""
enable = []
archs = ["auto"] archs = ["auto"]
audit = ""
build-frontend = "default" build-frontend = "default"
config-settings = {} config-settings = {}
dependency-versions = "pinned" dependency-versions = "pinned"
@@ -15,41 +13,29 @@ build-verbosity = 0
before-all = "" before-all = ""
before-build = "" before-build = ""
# TOML doesn't support explicit NULLs; use ["\u0000"] as a sentinel value.
xbuild-tools = ["\u0000"]
repair-wheel-command = "" repair-wheel-command = ""
test-command = "" test-command = ""
before-test = "" before-test = ""
test-sources = []
test-requires = [] test-requires = []
test-extras = [] test-extras = []
test-groups = []
test-environment = {}
test-runtime = {}
container-engine = "docker" container-engine = "docker"
pyodide-version = "" manylinux-x86_64-image = "manylinux2014"
manylinux-i686-image = "manylinux2014"
manylinux-aarch64-image = "manylinux2014"
manylinux-ppc64le-image = "manylinux2014"
manylinux-s390x-image = "manylinux2014"
manylinux-pypy_x86_64-image = "manylinux2014"
manylinux-pypy_i686-image = "manylinux2014"
manylinux-pypy_aarch64-image = "manylinux2014"
manylinux-x86_64-image = "manylinux_2_28" musllinux-x86_64-image = "musllinux_1_1"
manylinux-i686-image = "manylinux_2_28" musllinux-i686-image = "musllinux_1_1"
manylinux-aarch64-image = "manylinux_2_28" musllinux-aarch64-image = "musllinux_1_1"
manylinux-ppc64le-image = "manylinux_2_28" musllinux-ppc64le-image = "musllinux_1_1"
manylinux-s390x-image = "manylinux_2_28" musllinux-s390x-image = "musllinux_1_1"
manylinux-armv7l-image = "manylinux_2_31"
manylinux-riscv64-image = "manylinux_2_39"
manylinux-pypy_x86_64-image = "manylinux_2_28"
manylinux-pypy_i686-image = "manylinux_2_28"
manylinux-pypy_aarch64-image = "manylinux_2_28"
musllinux-x86_64-image = "musllinux_1_2"
musllinux-i686-image = "musllinux_1_2"
musllinux-aarch64-image = "musllinux_1_2"
musllinux-ppc64le-image = "musllinux_1_2"
musllinux-s390x-image = "musllinux_1_2"
musllinux-armv7l-image = "musllinux_1_2"
musllinux-riscv64-image = "musllinux_1_2"
[tool.cibuildwheel.linux] [tool.cibuildwheel.linux]
@@ -59,9 +45,3 @@ repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}"
repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"
[tool.cibuildwheel.windows] [tool.cibuildwheel.windows]
[tool.cibuildwheel.android]
[tool.cibuildwheel.ios]
[tool.cibuildwheel.pyodide]
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>attributeSetting</key>
<integer>1</integer>
<key>choiceAttribute</key>
<string>selected</string>
<key>choiceIdentifier</key>
<string>org.python.Python.PythonTFramework-3.13</string>
</dict>
</array>
</plist>
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>attributeSetting</key>
<integer>1</integer>
<key>choiceAttribute</key>
<string>selected</string>
<key>choiceIdentifier</key>
<string>org.python.Python.PythonTFramework-3.14</string>
</dict>
</array>
</plist>
+2 -2
View File
@@ -26,14 +26,14 @@ STAT_0o775 = (
) )
def main() -> None: def main():
openssl_dir, openssl_cafile = os.path.split(ssl.get_default_verify_paths().openssl_cafile) openssl_dir, openssl_cafile = os.path.split(ssl.get_default_verify_paths().openssl_cafile)
print(" -- pip install --upgrade certifi") print(" -- pip install --upgrade certifi")
subprocess.check_call( subprocess.check_call(
[sys.executable, "-E", "-s", "-m", "pip", "install", "--upgrade", "certifi"] [sys.executable, "-E", "-s", "-m", "pip", "install", "--upgrade", "certifi"]
) )
import certifi # noqa: PLC0415 import certifi
# change working directory to the default SSL directory # change working directory to the default SSL directory
os.chdir(openssl_dir) os.chdir(openssl_dir)
-4
View File
@@ -1,4 +0,0 @@
url = "https://nodejs.org/dist/"
v24 = "v24.12.0"
v22 = "v22.21.1"
v20 = "v20.19.6"
+44 -44
View File
@@ -1,54 +1,54 @@
[x86_64] [x86_64]
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2026.01.04-1 manylinux1 = quay.io/pypa/manylinux1_x86_64:2024-01-28-7b6687a
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2026.01.04-1 manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-08-05-4535177
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2026.01.04-1 manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2024-01-23-12ffabc
musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2026.01.04-1 manylinux_2_24 = quay.io/pypa/manylinux_2_24_x86_64:2022-12-26-0d38463
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2024-01-23-12ffabc
musllinux_1_1 = quay.io/pypa/musllinux_1_1_x86_64:2024-01-23-12ffabc
musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2024-01-23-12ffabc
[i686] [i686]
manylinux2014 = quay.io/pypa/manylinux2014_i686:2026.01.04-1 manylinux1 = quay.io/pypa/manylinux1_i686:2024-01-28-7b6687a
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2026.01.04-1 manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-08-05-4535177
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2026.01.04-1 manylinux2014 = quay.io/pypa/manylinux2014_i686:2024-01-23-12ffabc
musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686:2026.01.04-1 manylinux_2_24 = quay.io/pypa/manylinux_2_24_i686:2022-12-26-0d38463
musllinux_1_1 = quay.io/pypa/musllinux_1_1_i686:2024-01-23-12ffabc
[aarch64] musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686:2024-01-23-12ffabc
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2026.01.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2026.01.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2026.01.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64:2026.01.04-1
[ppc64le]
manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2026.01.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_ppc64le:2026.01.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_ppc64le:2026.01.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_ppc64le:2026.01.04-1
[s390x]
manylinux2014 = quay.io/pypa/manylinux2014_s390x:2026.01.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_s390x:2026.01.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_s390x:2026.01.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_s390x:2026.01.04-1
[pypy_x86_64] [pypy_x86_64]
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2026.01.04-1 manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-08-05-4535177
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2026.01.04-1 manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2024-01-23-12ffabc
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2026.01.04-1 manylinux_2_24 = quay.io/pypa/manylinux_2_24_x86_64:2022-12-26-0d38463
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2024-01-23-12ffabc
[pypy_i686] [pypy_i686]
manylinux2014 = quay.io/pypa/manylinux2014_i686:2026.01.04-1 manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-08-05-4535177
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2026.01.04-1 manylinux2014 = quay.io/pypa/manylinux2014_i686:2024-01-23-12ffabc
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2026.01.04-1 manylinux_2_24 = quay.io/pypa/manylinux_2_24_i686:2022-12-26-0d38463
[aarch64]
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2024-01-23-12ffabc
manylinux_2_24 = quay.io/pypa/manylinux_2_24_aarch64:2022-12-26-0d38463
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2024-01-23-12ffabc
musllinux_1_1 = quay.io/pypa/musllinux_1_1_aarch64:2024-01-23-12ffabc
musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64:2024-01-23-12ffabc
[ppc64le]
manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2024-01-23-12ffabc
manylinux_2_24 = quay.io/pypa/manylinux_2_24_ppc64le:2022-12-26-0d38463
manylinux_2_28 = quay.io/pypa/manylinux_2_28_ppc64le:2024-01-23-12ffabc
musllinux_1_1 = quay.io/pypa/musllinux_1_1_ppc64le:2024-01-23-12ffabc
musllinux_1_2 = quay.io/pypa/musllinux_1_2_ppc64le:2024-01-23-12ffabc
[s390x]
manylinux2014 = quay.io/pypa/manylinux2014_s390x:2024-01-23-12ffabc
manylinux_2_24 = quay.io/pypa/manylinux_2_24_s390x:2022-12-26-0d38463
manylinux_2_28 = quay.io/pypa/manylinux_2_28_s390x:2024-01-23-12ffabc
musllinux_1_1 = quay.io/pypa/musllinux_1_1_s390x:2024-01-23-12ffabc
musllinux_1_2 = quay.io/pypa/musllinux_1_2_s390x:2024-01-23-12ffabc
[pypy_aarch64] [pypy_aarch64]
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2026.01.04-1 manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2024-01-23-12ffabc
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2026.01.04-1 manylinux_2_24 = quay.io/pypa/manylinux_2_24_aarch64:2022-12-26-0d38463
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2026.01.04-1 manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2024-01-23-12ffabc
[armv7l]
manylinux_2_31 = quay.io/pypa/manylinux_2_31_armv7l:2026.01.04-1
manylinux_2_35 = quay.io/pypa/manylinux_2_35_armv7l:2026.01.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_armv7l:2026.01.04-1
[riscv64]
manylinux_2_39 = quay.io/pypa/manylinux_2_39_riscv64:2026.01.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_riscv64:2026.01.04-1
@@ -1,485 +0,0 @@
{
"releases": [
{
"tag": "20251217",
"assets": [
{
"name": "cpython-3.10.19+20251217-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-aarch64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-aarch64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.19+20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.10.19%2B20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-aarch64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-aarch64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-aarch64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-aarch64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.14+20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.11.14%2B20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-aarch64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-aarch64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-aarch64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-aarch64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.12+20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12%2B20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-aarch64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-aarch64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-aarch64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-aarch64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.11+20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.13.11%2B20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-aarch64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-aarch64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-aarch64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-aarch64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.2+20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.14.2%2B20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-aarch64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-aarch64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-aarch64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-aarch64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.15.0a3+20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.15.0a3%2B20251217-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
}
]
}
]
}
+10 -29
View File
@@ -1,36 +1,17 @@
# this file is copied to the testing cwd, to raise the below error message if # this file is copied to the testing cwd, to raise the below error message if
# pytest/unittest is run from there. # pytest/unittest is run from there.
import sys
import unittest import unittest
from typing import NoReturn
class TestStringMethods(unittest.TestCase): class TestStringMethods(unittest.TestCase):
def test_fail(self) -> NoReturn: def test_fail(self):
if sys.platform in ["android", "ios"]: self.fail(
msg = ( "cibuildwheel executes tests from a different working directory to "
"You tried to run tests from the testbed app's working " "your project. This ensures only your wheel is imported, preventing "
"directory, without specifying `test-sources`. " "Python from accessing files that haven't been packaged into the "
"On this platform, you must copy your test files to the testbed app by " "wheel. Please specify a path to your tests when invoking pytest "
"setting the `test-sources` option in your cibuildwheel " "using the {project} placeholder, e.g. `pytest {project}` or "
"configuration." "`pytest {project}/tests`. cibuildwheel will replace {project} with "
) "the path to your project."
else: )
msg = (
"cibuildwheel executes tests from a different working directory to "
"your project. This ensures only your wheel is imported, preventing "
"Python from accessing files that haven't been packaged into the "
"wheel. "
"\n\n"
"Please specify a path to your tests when invoking pytest "
"using the {project} placeholder, e.g. `pytest {project}` or "
"`pytest {project}/tests`. cibuildwheel will replace {project} with "
"the path to your project. "
"\n\n"
"Alternatively, you can specify your test files using the "
"`test-sources` option, and cibuildwheel will copy them to the "
"working directory for testing."
)
self.fail(msg)
+2 -1
View File
@@ -1 +1,2 @@
default = { version = "20.35.4", url = "https://github.com/pypa/get-virtualenv/releases/download/20.35.4/virtualenv.pyz" } version = "20.21.1"
url = "https://github.com/pypa/get-virtualenv/blob/20.21.1/public/virtualenv.pyz?raw=true"
+5 -2
View File
@@ -1,12 +1,15 @@
from __future__ import annotations
import json import json
from pathlib import Path
from typing import Any from typing import Any
from .util import resources DIR = Path(__file__).parent.resolve()
def get_schema(tool_name: str = "cibuildwheel") -> dict[str, Any]: def get_schema(tool_name: str = "cibuildwheel") -> dict[str, Any]:
"Get the stored complete schema for cibuildwheel settings." "Get the stored complete schema for cibuildwheel settings."
assert tool_name == "cibuildwheel", "Only cibuildwheel is supported." assert tool_name == "cibuildwheel", "Only cibuildwheel is supported."
with resources.CIBUILDWHEEL_SCHEMA.open(encoding="utf-8") as f: with DIR.joinpath("resources/cibuildwheel.schema.json").open(encoding="utf-8") as f:
return json.load(f) # type: ignore[no-any-return] return json.load(f) # type: ignore[no-any-return]
-138
View File
@@ -1,138 +0,0 @@
import dataclasses
import itertools
from enum import StrEnum
from fnmatch import fnmatch
from typing import Any
import bracex
from packaging.specifiers import SpecifierSet
from packaging.version import Version
def selector_matches(patterns: str, string: str) -> bool:
"""
Returns True if `string` is matched by any of the wildcard patterns in
`patterns`.
Matching is according to fnmatch, but with shell-like curly brace
expansion. For example, 'cp{36,37}-*' would match either of 'cp36-*' or
'cp37-*'.
"""
patterns_list = patterns.split()
expanded_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in patterns_list)
return any(fnmatch(string, pat) for pat in expanded_patterns)
class EnableGroup(StrEnum):
"""
Groups of build selectors that are not enabled by default.
"""
CPythonFreeThreading = "cpython-freethreading"
CPythonPrerelease = "cpython-prerelease"
GraalPy = "graalpy"
PyPy = "pypy"
PyPyEoL = "pypy-eol"
PyodidePrerelease = "pyodide-prerelease"
@classmethod
def all_groups(cls) -> frozenset["EnableGroup"]:
return frozenset(cls)
@classmethod
def parse_option_value(cls, value: str) -> frozenset["EnableGroup"]:
"""
Parses a string of space-separated values into a set of EnableGroup
members. The string may contain group names or "all".
"""
result = set()
for group in value.strip().split():
if group == "all":
return cls.all_groups()
try:
result.add(cls(group))
except ValueError:
msg = f"Unknown enable group: {group}"
raise ValueError(msg) from None
return frozenset(result)
@dataclasses.dataclass(frozen=True, kw_only=True)
class BuildSelector:
"""
This class holds a set of build/skip patterns. You call an instance with a
build identifier, and it returns True if that identifier should be
included. Only call this on valid identifiers, ones that have at least 2
numeric digits before the first dash.
"""
build_config: str
skip_config: str
requires_python: SpecifierSet | None = None
enable: frozenset[EnableGroup] = frozenset()
def __call__(self, build_id: str) -> bool:
# Filter build selectors by python_requires if set
if self.requires_python is not None:
py_ver_str = build_id.split("-")[0].split("_")[0]
py_ver_str = py_ver_str.removesuffix("t")
major = int(py_ver_str[2])
minor = int(py_ver_str[3:])
version = Version(f"{major}.{minor}.99")
if not self.requires_python.contains(version):
return False
# filter out groups that are not enabled
if EnableGroup.CPythonFreeThreading not in self.enable and fnmatch(build_id, "cp313t-*"):
return False
if EnableGroup.CPythonPrerelease not in self.enable and fnmatch(build_id, "cp315*"):
return False
is_pypy_eol = fnmatch(build_id, "pp3?-*") or fnmatch(build_id, "pp310-*")
is_pypy = fnmatch(build_id, "pp*") and not is_pypy_eol
if EnableGroup.PyPy not in self.enable and is_pypy:
return False
if EnableGroup.PyPyEoL not in self.enable and is_pypy_eol:
return False
if EnableGroup.GraalPy not in self.enable and fnmatch(build_id, "gp*"):
return False
# TODO: Re-enable this when we have Pyodide prereleases again (e.g., 0.29.0a1+)
# Python 3.13 support became stable in Pyodide 0.28.0, so it no longer needs a prerelease
# flag.
# Also update Pyodide tests in unit_test/build_selector_test.py accordingly.
# When re-enabling, update the pattern to match the experimental Python version in case
# it is bumped to Python 3.14 (likely cp314-pyodide_* but could remain as 3.13 as well).
# This depends on the CPython version being used in the Pyodide runtime at the time.
# if EnableGroup.PyodidePrerelease not in self.enable and fnmatch(
# build_id, "cp313-pyodide_*"
# ):
# return False
should_build = selector_matches(self.build_config, build_id)
should_skip = selector_matches(self.skip_config, build_id)
return should_build and not should_skip
def options_summary(self) -> Any:
return {
"build_config": self.build_config,
"skip_config": self.skip_config,
"requires_python": str(self.requires_python),
"enable": sorted(group.value for group in self.enable),
}
@dataclasses.dataclass(frozen=True, kw_only=True)
class TestSelector:
"""
A build selector that can only skip tests according to a skip pattern.
"""
skip_config: str
def __call__(self, build_id: str) -> bool:
should_skip = selector_matches(self.skip_config, build_id)
return not should_skip
def options_summary(self) -> Any:
return {"skip_config": self.skip_config}
+16 -5
View File
@@ -1,21 +1,32 @@
from __future__ import annotations
import os import os
import subprocess
import typing import typing
from typing import Final, Literal, Protocol from typing import Final, Literal, Protocol, Union
__all__ = ( __all__ = (
"PLATFORMS", "PLATFORMS",
"PathOrStr", "PathOrStr",
"PlatformName", "PlatformName",
"PLATFORMS",
"PopenBytes",
) )
PathOrStr = str | os.PathLike[str] if typing.TYPE_CHECKING:
PopenBytes = subprocess.Popen[bytes]
PathOrStr = Union[str, os.PathLike[str]]
else:
PopenBytes = subprocess.Popen
PathOrStr = Union[str, "os.PathLike[str]"]
PlatformName = Literal["linux", "macos", "windows", "pyodide", "android", "ios"] PlatformName = Literal["linux", "macos", "windows"]
PLATFORMS: Final[frozenset[PlatformName]] = frozenset(typing.get_args(PlatformName)) PLATFORMS: Final[set[PlatformName]] = {"linux", "macos", "windows"}
class GenericPythonConfiguration(Protocol): class GenericPythonConfiguration(Protocol):
@property @property
def identifier(self) -> str: ... def identifier(self) -> str:
...
+762
View File
@@ -0,0 +1,762 @@
from __future__ import annotations
import contextlib
import fnmatch
import itertools
import os
import re
import shlex
import ssl
import subprocess
import sys
import textwrap
import time
import typing
import urllib.request
from collections import defaultdict
from collections.abc import Generator, Iterable, Mapping, Sequence
from dataclasses import dataclass
from enum import Enum
from functools import cached_property, lru_cache
from pathlib import Path, PurePath
from time import sleep
from typing import Any, ClassVar, Final, Literal, TextIO, TypeVar
import bracex
import certifi
from filelock import FileLock
from packaging.requirements import InvalidRequirement, Requirement
from packaging.specifiers import SpecifierSet
from packaging.utils import parse_wheel_filename
from packaging.version import Version
from platformdirs import user_cache_path
from ._compat import tomllib
from .typing import PathOrStr, PlatformName
__all__ = [
"resources_dir",
"MANYLINUX_ARCHS",
"call",
"shell",
"find_compatible_wheel",
"format_safe",
"prepare_command",
"get_build_verbosity_extra_flags",
"read_python_configs",
"selector_matches",
"strtobool",
"cached_property",
"chdir",
"split_config_settings",
]
resources_dir: Final[Path] = Path(__file__).parent / "resources"
install_certifi_script: Final[Path] = resources_dir / "install_certifi.py"
test_fail_cwd_file: Final[Path] = resources_dir / "testing_temp_dir_file.py"
MANYLINUX_ARCHS: Final[tuple[str, ...]] = (
"x86_64",
"i686",
"pypy_x86_64",
"aarch64",
"ppc64le",
"s390x",
"pypy_aarch64",
"pypy_i686",
)
MUSLLINUX_ARCHS: Final[tuple[str, ...]] = (
"x86_64",
"i686",
"aarch64",
"ppc64le",
"s390x",
)
DEFAULT_CIBW_CACHE_PATH: Final[Path] = user_cache_path(appname="cibuildwheel", appauthor="pypa")
CIBW_CACHE_PATH: Final[Path] = Path(
os.environ.get("CIBW_CACHE_PATH", DEFAULT_CIBW_CACHE_PATH)
).resolve()
IS_WIN: Final[bool] = sys.platform.startswith("win")
@typing.overload
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: Literal[False] = ...,
) -> None:
...
@typing.overload
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: Literal[True],
) -> str:
...
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: bool = False,
) -> 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
Paths to strings, due to Windows behavior at least on older Pythons.
https://bugs.python.org/issue8557
"""
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] = {}
if capture_stdout:
kwargs["universal_newlines"] = True
kwargs["stdout"] = subprocess.PIPE
result = subprocess.run(args_, check=True, shell=IS_WIN, env=env, cwd=cwd, **kwargs)
if not capture_stdout:
return None
return typing.cast(str, result.stdout)
def shell(
*commands: str, env: Mapping[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)
def format_safe(template: str, **kwargs: str | os.PathLike[str]) -> str:
"""
Works similarly to `template.format(**kwargs)`, except that unmatched
fields in `template` are passed through untouched.
>>> format_safe('{a} {b}', a='123')
'123 {b}'
>>> format_safe('{a} {b[4]:3f}', a='123')
'123 {b[4]:3f}'
To avoid variable expansion, precede with a single backslash e.g.
>>> format_safe('\\{a} {b}', a='123')
'{a} {b}'
"""
result = template
for key, value in kwargs.items():
find_pattern = re.compile(
rf"""
(?<!\#) # don't match if preceded by a hash
{{ # literal open curly bracket
{re.escape(key)} # the field name
}} # literal close curly bracket
""",
re.VERBOSE,
)
result = re.sub(
pattern=find_pattern,
repl=str(value).replace("\\", r"\\"),
string=result,
)
# transform escaped sequences into their literal equivalents
result = result.replace(f"#{{{key}}}", f"{{{key}}}")
return result
def prepare_command(command: str, **kwargs: PathOrStr) -> str:
"""
Preprocesses a command by expanding variables like {python}.
For example, used in the test_command option to specify the path to the
project's root. Unmatched syntax will mostly be allowed through.
"""
return format_safe(command, python="python", pip="pip", **kwargs)
def get_build_verbosity_extra_flags(level: int) -> list[str]:
if level > 0:
return ["-" + level * "v"]
elif level < 0:
return ["-" + -level * "q"]
else:
return []
def split_config_settings(config_settings: str, frontend: Literal["pip", "build"]) -> list[str]:
config_settings_list = shlex.split(config_settings)
s = "s" if frontend == "pip" else ""
return [f"--config-setting{s}={setting}" for setting in config_settings_list]
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"])
return results
def selector_matches(patterns: str, string: str) -> bool:
"""
Returns True if `string` is matched by any of the wildcard patterns in
`patterns`.
Matching is according to fnmatch, but with shell-like curly brace
expansion. For example, 'cp{36,37}-*' would match either of 'cp36-*' or
'cp37-*'.
"""
patterns_list = patterns.split()
expanded_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in patterns_list)
return any(fnmatch.fnmatch(string, pat) for pat in expanded_patterns)
# Once we require Python 3.10+, we can add kw_only=True
@dataclass(frozen=True)
class BuildSelector:
"""
This class holds a set of build/skip patterns. You call an instance with a
build identifier, and it returns True if that identifier should be
included. Only call this on valid identifiers, ones that have at least 2
numeric digits before the first dash.
"""
build_config: str
skip_config: str
requires_python: SpecifierSet | None = None
# a pattern that skips prerelease versions, when include_prereleases is False.
PRERELEASE_SKIP: ClassVar[str] = ""
prerelease_pythons: bool = False
def __call__(self, build_id: str) -> bool:
# Filter build selectors by python_requires if set
if self.requires_python is not None:
py_ver_str = build_id.split("-")[0]
major = int(py_ver_str[2])
minor = int(py_ver_str[3:])
version = Version(f"{major}.{minor}.99")
if not self.requires_python.contains(version):
return False
# filter out the prerelease pythons if self.prerelease_pythons is False
if not self.prerelease_pythons and selector_matches(self.PRERELEASE_SKIP, build_id):
return False
should_build = selector_matches(self.build_config, build_id)
should_skip = selector_matches(self.skip_config, build_id)
return should_build and not should_skip
def options_summary(self) -> Any:
return {
"build_config": self.build_config,
"skip_config": self.skip_config,
"requires_python": str(self.requires_python),
"prerelease_pythons": self.prerelease_pythons,
}
@dataclass(frozen=True)
class TestSelector:
"""
A build selector that can only skip tests according to a skip pattern.
"""
skip_config: str
def __call__(self, build_id: str) -> bool:
should_skip = selector_matches(self.skip_config, build_id)
return not should_skip
def options_summary(self) -> Any:
return {"skip_config": self.skip_config}
# Taken from https://stackoverflow.com/a/107717
class Unbuffered:
def __init__(self, stream: TextIO) -> None:
self.stream = stream
def write(self, data: str) -> None:
self.stream.write(data)
self.stream.flush()
def writelines(self, data: Iterable[str]) -> None:
self.stream.writelines(data)
self.stream.flush()
def __getattr__(self, attr: str) -> Any:
return getattr(self.stream, attr)
def download(url: str, dest: Path) -> None:
print(f"+ Download {url} to {dest}")
dest_dir = dest.parent
if not dest_dir.exists():
dest_dir.mkdir(parents=True)
# we've had issues when relying on the host OS' CA certificates on Windows,
# so we use certifi (this sounds odd but requests also does this by default)
cafile = os.environ.get("SSL_CERT_FILE", certifi.where())
context = ssl.create_default_context(cafile=cafile)
repeat_num = 3
for i in range(repeat_num):
try:
with urllib.request.urlopen(url, context=context) as response:
dest.write_bytes(response.read())
return
except OSError:
if i == repeat_num - 1:
raise
sleep(3)
class DependencyConstraints:
def __init__(self, base_file_path: Path):
assert base_file_path.exists()
self.base_file_path = base_file_path.resolve()
@staticmethod
def with_defaults() -> DependencyConstraints:
return DependencyConstraints(base_file_path=resources_dir / "constraints.txt")
def get_for_python_version(self, version: str) -> Path:
version_parts = version.split(".")
# try to find a version-specific dependency file e.g. if
# ./constraints.txt is the base, look for ./constraints-python36.txt
specific_stem = self.base_file_path.stem + f"-python{version_parts[0]}{version_parts[1]}"
specific_name = specific_stem + self.base_file_path.suffix
specific_file_path = self.base_file_path.with_name(specific_name)
if specific_file_path.exists():
return specific_file_path
else:
return self.base_file_path
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.base_file_path!r})"
def __eq__(self, o: object) -> bool:
if not isinstance(o, DependencyConstraints):
return False
return self.base_file_path == o.base_file_path
def options_summary(self) -> Any:
if self == DependencyConstraints.with_defaults():
return "pinned"
else:
return self.base_file_path.name
BuildFrontendName = Literal["pip", "build"]
@dataclass(frozen=True)
class BuildFrontendConfig:
name: BuildFrontendName
args: Sequence[str] = ()
@staticmethod
def from_config_string(config_string: str) -> BuildFrontendConfig:
config_dict = parse_key_value_string(config_string, ["name"], ["args"])
name = " ".join(config_dict["name"])
if name not in {"pip", "build"}:
msg = f"Unrecognised build frontend {name}, only 'pip' and 'build' are supported"
raise ValueError(msg)
name = typing.cast(BuildFrontendName, name)
args = config_dict.get("args") or []
return BuildFrontendConfig(name=name, args=args)
def options_summary(self) -> str | dict[str, str]:
if not self.args:
return self.name
else:
return {"name": self.name, "args": repr(self.args)}
class NonPlatformWheelError(Exception):
def __init__(self) -> None:
message = textwrap.dedent(
"""
cibuildwheel: Build failed because a pure Python wheel was generated.
If you intend to build a pure-Python wheel, you don't need cibuildwheel - use
`pip wheel -w DEST_DIR .` instead.
If you expected a platform wheel, check your project configuration, or run
cibuildwheel with CIBW_BUILD_VERBOSITY=1 to view build logs.
"""
)
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"}
class CIProvider(Enum):
travis_ci = "travis"
appveyor = "appveyor"
circle_ci = "circle_ci"
azure_pipelines = "azure_pipelines"
github_actions = "github_actions"
gitlab = "gitlab"
cirrus_ci = "cirrus_ci"
other = "other"
def detect_ci_provider() -> CIProvider | None:
if "TRAVIS" in os.environ:
return CIProvider.travis_ci
elif "APPVEYOR" in os.environ:
return CIProvider.appveyor
elif "CIRCLECI" in os.environ:
return CIProvider.circle_ci
elif "AZURE_HTTP_USER_AGENT" in os.environ:
return CIProvider.azure_pipelines
elif "GITHUB_ACTIONS" in os.environ:
return CIProvider.github_actions
elif "GITLAB_CI" in os.environ:
return CIProvider.gitlab
elif "CIRRUS_CI" in os.environ:
return CIProvider.cirrus_ci
elif strtobool(os.environ.get("CI", "false")):
return CIProvider.other
else:
return None
def unwrap(text: str) -> str:
"""
Unwraps multi-line text to a single line
"""
# remove initial line indent
text = textwrap.dedent(text)
# remove leading/trailing whitespace
text = text.strip()
# remove consecutive whitespace
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]:
"""
Prints the new items in a directory upon exiting. The message to display
can include {n} for number of wheels, {s} for total number of seconds,
and/or {m} for total number of minutes. Does not print anything if this
exits via exception.
"""
start_time = time.time()
existing_contents = set(output_dir.iterdir())
yield
final_contents = set(output_dir.iterdir())
new_contents = [
FileReport(wheel.name, f"{(wheel.stat().st_size + 1023) // 1024:,d}")
for wheel in final_contents - existing_contents
]
if not new_contents:
return
max_name_len = max(len(f.name) for f in new_contents)
max_size_len = max(len(f.size) for f in new_contents)
n = len(new_contents)
s = time.time() - start_time
m = s / 60
print(
msg.format(n=n, s=s, m=m),
*sorted(
f" {f.name:<{max_name_len}s} {f.size:>{max_size_len}s} kB" for f in new_contents
),
sep="\n",
)
def get_pip_version(env: Mapping[str, str]) -> str:
versions_output_text = call(
"python", "-m", "pip", "freeze", "--all", capture_stdout=True, env=env
)
(pip_version,) = (
version[5:]
for version in versions_output_text.strip().splitlines()
if version.startswith("pip==")
)
return pip_version
@lru_cache(maxsize=None)
def _ensure_virtualenv() -> Path:
input_file = resources_dir / "virtualenv.toml"
with input_file.open("rb") as f:
loaded_file = tomllib.load(f)
version = str(loaded_file["version"])
url = str(loaded_file["url"])
path = CIBW_CACHE_PATH / f"virtualenv-{version}.pyz"
with FileLock(str(path) + ".lock"):
if not path.exists():
download(url, path)
return path
def _parse_constraints_for_virtualenv(
dependency_constraint_flags: Sequence[PathOrStr],
) -> 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.
If a package version cannot be found, its value is "embed" meaning that virtualenv will install
its bundled version, already available locally.
The function does not try to be too smart and just handles basic constraints.
If it can't get an exact version, the real constraint will be handled by the
{macos|windows}.setup_python function.
"""
assert len(dependency_constraint_flags) in {0, 2}
packages = ["pip", "setuptools", "wheel"]
constraints_dict = {package: "embed" for package in packages}
if len(dependency_constraint_flags) == 2:
assert dependency_constraint_flags[0] == "-c"
constraint_path = Path(dependency_constraint_flags[1])
assert constraint_path.exists()
with constraint_path.open(encoding="utf-8") as constraint_file:
for line_ in constraint_file:
line = line_.strip()
if not line:
continue
if line.startswith("#"):
continue
try:
requirement = Requirement(line)
package = requirement.name
if (
package not in packages
or requirement.url is not None
or requirement.marker is not None
or len(requirement.extras) != 0
or len(requirement.specifier) != 1
):
continue
specifier = next(iter(requirement.specifier))
if specifier.operator != "==":
continue
constraints_dict[package] = specifier.version
except InvalidRequirement:
continue
return constraints_dict
def virtualenv(
python: Path, venv_path: Path, dependency_constraint_flags: Sequence[PathOrStr]
) -> dict[str, str]:
assert python.exists()
virtualenv_app = _ensure_virtualenv()
constraints = _parse_constraints_for_virtualenv(dependency_constraint_flags)
additional_flags = [f"--{package}={version}" for package, version in constraints.items()]
# Using symlinks to pre-installed seed packages is really the fastest way to get a virtual
# environment. The initial cost is a bit higher but reusing is much faster.
# Windows does not always allow symlinks so just disabling for now.
# Requires pip>=19.3 so disabling for "embed" because this means we don't know what's the
# version of pip that will end-up installed.
# c.f. https://virtualenv.pypa.io/en/latest/cli_interface.html#section-seeder
if (
not IS_WIN
and constraints["pip"] != "embed"
and Version(constraints["pip"]) >= Version("19.3")
):
additional_flags.append("--symlink-app-data")
call(
sys.executable,
"-sS", # just the stdlib, https://github.com/pypa/virtualenv/issues/2133#issuecomment-1003710125
virtualenv_app,
"--activators=",
"--no-periodic-update",
*additional_flags,
"--python",
python,
venv_path,
)
paths = [str(venv_path), str(venv_path / "Scripts")] if IS_WIN else [str(venv_path / "bin")]
env = os.environ.copy()
env["PATH"] = os.pathsep.join([*paths, env["PATH"]])
return env
T = TypeVar("T", bound=PurePath)
def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None:
"""
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("-")
for wheel in wheels:
_, _, _, tags = parse_wheel_filename(wheel.name)
for tag in tags:
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 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 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(f"_{arch}"):
continue
else:
# Windows should exactly match
if tag.platform != platform:
continue
# If all the filters above pass, then the wheel is a previously built compatible wheel.
return wheel
return None
# Can be replaced by contextlib.chdir in Python 3.11
@contextlib.contextmanager
def chdir(new_path: Path | str) -> Generator[None, None, None]:
"""Non thread-safe context manager to change the current working directory."""
cwd = os.getcwd()
try:
os.chdir(new_path)
yield
finally:
os.chdir(cwd)
def fix_ansi_codes_for_github_actions(text: str) -> str:
"""
Github Actions forgets the current ANSI style on every new line. This
function repeats the current ANSI style on every new line.
"""
ansi_code_regex = re.compile(r"(\033\[[0-9;]*m)")
ansi_codes: list[str] = []
output = ""
for line in text.splitlines(keepends=True):
# add the current ANSI codes to the beginning of the line
output += "".join(ansi_codes) + line
# split the line at each ANSI code
parts = ansi_code_regex.split(line)
# if there are any ANSI codes, save them
if len(parts) > 1:
# iterate over the ANSI codes in this line
for code in parts[1::2]:
if code == "\033[0m":
# reset the list of ANSI codes when the clear code is found
ansi_codes = []
else:
ansi_codes.append(code)
return output
def parse_key_value_string(
key_value_string: str,
positional_arg_names: Sequence[str] | None = None,
kw_arg_names: Sequence[str] | None = None,
) -> dict[str, list[str]]:
"""
Parses a string like "docker; create_args: --some-option=value another-option"
"""
if positional_arg_names is None:
positional_arg_names = []
if kw_arg_names is None:
kw_arg_names = []
all_field_names = [*positional_arg_names, *kw_arg_names]
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";")
shlexer.commenters = ""
shlexer.whitespace_split = True
parts = list(shlexer)
# parts now looks like
# ['docker', ';', 'create_args:', '--some-option=value', 'another-option']
# split by semicolon
fields = [list(group) for k, group in itertools.groupby(parts, lambda x: x == ";") if not k]
result: defaultdict[str, list[str]] = defaultdict(list)
for field_i, field in enumerate(fields):
# check to see if the option name is specified
field_name, sep, first_value = field[0].partition(":")
if sep:
if field_name not in all_field_names:
msg = f"Failed to parse {key_value_string!r}. Unknown field name {field_name!r}"
raise ValueError(msg)
values = ([first_value] if first_value else []) + field[1:]
else:
try:
field_name = positional_arg_names[field_i]
except IndexError:
msg = f"Failed to parse {key_value_string!r}. Too many positional arguments - expected a maximum of {len(positional_arg_names)}"
raise ValueError(msg) from None
values = field
result[field_name] += values
return dict(result)
View File
-98
View File
@@ -1,98 +0,0 @@
import os
import shlex
import shutil
import subprocess
import sys
import typing
from collections.abc import Iterator, Mapping
from typing import Final, Literal
from ..errors import FatalError
from ..typing import PathOrStr
_IS_WIN: Final[bool] = sys.platform.startswith("win")
@typing.overload
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: Literal[False] = ...,
) -> None: ...
@typing.overload
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: Literal[True],
) -> str: ...
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: bool = False,
) -> 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
Paths to strings, due to Windows behavior at least on older Pythons.
https://bugs.python.org/issue8557
"""
args_ = [str(arg) for arg in args]
# print the command executing for the logs
print("+ " + " ".join(shlex.quote(a) for a in args_))
# workaround platform behaviour differences outlined
# in https://github.com/python/cpython/issues/52803
path_env = env if env is not None else os.environ
path = path_env.get("PATH", None)
executable = shutil.which(args_[0], path=path)
if executable is None:
msg = f"Couldn't find {args_[0]!r} in PATH {path!r}"
raise FatalError(msg)
args_[0] = executable
try:
result = subprocess.run(
args_,
check=True,
shell=_IS_WIN,
env=env,
cwd=cwd,
capture_output=capture_stdout,
text=capture_stdout,
)
except subprocess.CalledProcessError as e:
if capture_stdout:
sys.stderr.write(e.stderr)
raise
if not capture_stdout:
return None
sys.stderr.write(result.stderr)
return typing.cast("str", result.stdout)
def shell(
*commands: str, env: Mapping[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)
def split_command(lst: list[str]) -> Iterator[list[str]]:
"""
Split a shell-style command, as returned by shlex.split, into a sequence
of commands, separated by '&&'.
"""
items = list[str]()
for item in lst:
if item == "&&":
yield items
items = []
else:
items.append(item)
yield items
-154
View File
@@ -1,154 +0,0 @@
import os
import shutil
import ssl
import tarfile
import time
import urllib.request
from collections.abc import Callable
from pathlib import Path, PurePath
from typing import Final
from zipfile import ZipFile
import certifi
from platformdirs import user_cache_path
from ..errors import FatalError
DEFAULT_CIBW_CACHE_PATH: Final[Path] = user_cache_path(appname="cibuildwheel", appauthor="pypa")
CIBW_CACHE_PATH: Final[Path] = Path(
os.environ.get("CIBW_CACHE_PATH", DEFAULT_CIBW_CACHE_PATH)
).resolve()
def ensure_cache_sentinel(cache_path: Path) -> None:
"""Create a sentinel file to mark a cibuildwheel cache directory.
We help prevent accidental deletion of non-cache directories with this.
Args:
cache_path: The cache directory path where the sentinel should be created
"""
if cache_path.exists():
sentinel_file = cache_path / "CACHEDIR.TAG"
if not sentinel_file.exists():
sentinel_file.write_text(
"Signature: 8a477f597d28d172789f06886806bc55\n"
"# This file is a cache directory tag created by cibuildwheel.\n"
"# For information about cache directory tags, see:\n"
"# https://www.brynosaurus.com/cachedir/",
encoding="utf-8",
)
def download(url: str, dest: Path) -> None:
print(f"+ Download {url} to {dest}")
dest_dir = dest.parent
dest_dir.mkdir(parents=True, exist_ok=True)
# we've had issues when relying on the host OS' CA certificates on Windows,
# so we use certifi (this sounds odd but requests also does this by default)
cafile = os.environ.get("SSL_CERT_FILE", certifi.where())
context = ssl.create_default_context(cafile=cafile)
repeat_num = 3
for i in range(repeat_num):
try:
with urllib.request.urlopen(url, context=context) as response:
dest.write_bytes(response.read())
return
except OSError:
if i == repeat_num - 1:
raise
time.sleep(3)
def extract_zip(zip_src: Path, dest: Path) -> None:
"""Extracts a zip and correctly sets permissions on extracted files.
Notes:
- sets permissions to the same values as they were set in the archive
- files with no clear permissions in `external_attr` will be extracted with default values
"""
with ZipFile(zip_src) as zip_:
for zinfo in zip_.filelist:
zip_.extract(zinfo, dest)
# Set permissions to the same values as they were set in the archive
# We have to do this manually due to https://github.com/python/cpython/issues/59999
permissions = (zinfo.external_attr >> 16) & 0o777
if permissions != 0:
dest.joinpath(zinfo.filename).chmod(permissions)
def extract_tar(tar_src: Path, dest: Path) -> None:
"""Extracts a tar file using the stdlib 'tar' filter.
See: https://docs.python.org/3/library/tarfile.html#tarfile.tar_filter for filter details
"""
with tarfile.open(tar_src) as tar_:
tar_.extraction_filter = getattr(tarfile, "tar_filter", (lambda member, _: member))
tar_.extractall(dest)
def move_file(src_file: Path, dst_file: Path) -> Path:
"""Moves a file safely while avoiding potential semantic confusion:
1. `dst_file` must point to the target filename, not a directory
2. `dst_file` will be overwritten if it already exists
3. any missing parent directories will be created
Returns the fully resolved Path of the resulting file.
Raises:
NotADirectoryError: If any part of the intermediate path to `dst_file` is an existing file
IsADirectoryError: If `dst_file` points directly to an existing directory
"""
src_file = src_file.resolve(strict=True)
dst_file = dst_file.resolve()
if dst_file.is_dir():
msg = "dst_file must be a valid target filename, not an existing directory."
raise IsADirectoryError(msg)
dst_file.unlink(missing_ok=True)
dst_file.parent.mkdir(parents=True, exist_ok=True)
# using shutil.move() as Path.rename() is not guaranteed to work across filesystem boundaries
# explicit str() needed for Python 3.8
resulting_file = shutil.move(str(src_file), str(dst_file))
return Path(resulting_file).resolve(strict=True)
def copy_into_local(src: Path, dst: PurePath) -> None:
"""Copy a path from src to dst, regardless of whether it's a file or a directory."""
# Ensure the target folder location exists
Path(dst.parent).mkdir(exist_ok=True, parents=True)
if src.is_dir():
shutil.copytree(src, dst)
else:
shutil.copy(src, dst)
def copy_test_sources(
test_sources: list[str],
project_dir: Path,
test_dir: PurePath,
copy_into: Callable[[Path, PurePath], None] = copy_into_local,
) -> None:
"""Copy the list of test sources from the package to the test directory.
:param test_sources: A list of test paths, relative to the project_dir.
:param project_dir: The root of the project.
:param test_dir: The folder where test sources should be placed.
:param copy_into: The copy function to use. By default, does a local
filesystem copy; but an OCIContainer.copy_info method (or equivalent)
can be provided.
"""
for test_path in test_sources:
source = project_dir.resolve() / test_path
if not source.exists():
msg = f"Test source {test_path} does not exist."
raise FatalError(msg)
copy_into(source, test_dir / test_path)
-174
View File
@@ -1,174 +0,0 @@
import dataclasses
import itertools
import os
import re
import shlex
import textwrap
from collections import defaultdict
from collections.abc import Sequence
from ..typing import PathOrStr
def format_safe(template: str, **kwargs: str | os.PathLike[str]) -> str:
"""
Works similarly to `template.format(**kwargs)`, except that unmatched
fields in `template` are passed through untouched.
>>> format_safe('{a} {b}', a='123')
'123 {b}'
>>> format_safe('{a} {b[4]:3f}', a='123')
'123 {b[4]:3f}'
To avoid variable expansion, precede with a single hash e.g.
>>> format_safe('#{a} {b}', a='123')
'{a} {b}'
"""
result = template
for key, value in kwargs.items():
find_pattern = re.compile(
rf"""
(?<!\#) # don't match if preceded by a hash
{{ # literal open curly bracket
{re.escape(key)} # the field name
}} # literal close curly bracket
""",
re.VERBOSE,
)
result = re.sub(
pattern=find_pattern,
repl=str(value).replace("\\", r"\\"),
string=result,
)
# transform escaped sequences into their literal equivalents
result = result.replace(f"#{{{key}}}", f"{{{key}}}")
return result
def prepare_command(command: str, **kwargs: PathOrStr) -> str:
"""
Preprocesses a command by expanding variables like {project}.
For example, used in the test_command option to specify the path to the
project's root. Unmatched syntax will mostly be allowed through.
"""
return format_safe(command, **kwargs)
def strtobool(val: str) -> bool:
return val.lower() in {"y", "yes", "t", "true", "on", "1"}
def unwrap(text: str) -> str:
"""
Unwraps multi-line text to a single line
"""
# remove initial line indent
text = textwrap.dedent(text)
# remove leading/trailing whitespace
text = text.strip()
# remove consecutive whitespace
return re.sub(r"\s+", " ", text)
def unwrap_preserving_paragraphs(text: str) -> str:
"""
Unwraps multi-line text to a single line, but preserves paragraphs
"""
# remove initial line indent
text = textwrap.dedent(text)
# remove leading/trailing whitespace
text = text.strip()
paragraphs = text.split("\n\n")
# remove consecutive whitespace
paragraphs = [re.sub(r"\s+", " ", paragraph) for paragraph in paragraphs]
return "\n\n".join(paragraphs)
def parse_key_value_string(
key_value_string: str,
positional_arg_names: Sequence[str] | None = None,
kw_arg_names: Sequence[str] | None = None,
) -> dict[str, list[str]]:
"""
Parses a string like "docker; create_args: --some-option=value another-option"
"""
if positional_arg_names is None:
positional_arg_names = []
if kw_arg_names is None:
kw_arg_names = []
all_field_names = [*positional_arg_names, *kw_arg_names]
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";")
shlexer.commenters = ""
shlexer.whitespace_split = True
parts = list(shlexer)
# parts now looks like
# ['docker', ';', 'create_args:', '--some-option=value', 'another-option']
# split by semicolon
fields = [list(group) for k, group in itertools.groupby(parts, lambda x: x == ";") if not k]
result: defaultdict[str, list[str]] = defaultdict(list)
for field_i, field in enumerate(fields):
# check to see if the option name is specified
field_name, sep, first_value = field[0].partition(":")
if sep:
if field_name not in all_field_names:
msg = f"Failed to parse {key_value_string!r}. Unknown field name {field_name!r}"
raise ValueError(msg)
values = ([first_value] if first_value else []) + field[1:]
else:
try:
field_name = positional_arg_names[field_i]
except IndexError:
msg = f"Failed to parse {key_value_string!r}. Too many positional arguments - expected a maximum of {len(positional_arg_names)}"
raise ValueError(msg) from None
values = field
result[field_name] += values
return dict(result)
@dataclasses.dataclass(order=True)
class FlexibleVersion:
version_parts: tuple[int, ...] = dataclasses.field(init=False, repr=False)
suffix: str = dataclasses.field(init=False, repr=False)
version_str: str = dataclasses.field(compare=False)
def __post_init__(self) -> None:
# Split into numeric parts and the optional suffix
match = re.match(r"^[v]?(\d+(\.\d+)*)(.*)$", self.version_str)
if not match:
msg = f"Invalid version string: {self.version_str}"
raise ValueError(msg)
version_part, _, suffix = match.groups()
# Convert numeric version part into a tuple of integers
self.version_parts = tuple(map(int, version_part.split(".")))
self.suffix = suffix.strip() if suffix else ""
# Normalize by removing trailing zeros
self.version_parts = self._remove_trailing_zeros(self.version_parts)
@staticmethod
def _remove_trailing_zeros(parts: tuple[int, ...]) -> tuple[int, ...]:
# Remove trailing zeros for accurate comparisons
# without this, "3.0" would be considered greater than "3"
while parts and parts[-1] == 0:
parts = parts[:-1]
return parts
def __str__(self) -> str:
return self.version_str
-180
View File
@@ -1,180 +0,0 @@
import shlex
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from pathlib import Path, PurePath
from typing import Any, Literal, Self, TypeVar
from packaging.utils import parse_wheel_filename
from . import resources
from .cmd import call
from .helpers import parse_key_value_string, unwrap
@dataclass(kw_only=True)
class DependencyConstraints:
base_file_path: Path | None = None
packages: list[str] = field(default_factory=list)
def __post_init__(self) -> None:
if self.packages and self.base_file_path is not None:
msg = "Cannot specify both a file and packages in the dependency constraints"
raise ValueError(msg)
if self.base_file_path is not None:
if not self.base_file_path.exists():
msg = f"Dependency constraints file not found: {self.base_file_path}"
raise FileNotFoundError(msg)
self.base_file_path = self.base_file_path.resolve()
@classmethod
def pinned(cls) -> Self:
return cls(base_file_path=resources.CONSTRAINTS)
@classmethod
def latest(cls) -> Self:
return cls()
@classmethod
def from_config_string(cls, config_string: str) -> Self:
if config_string == "pinned":
return cls.pinned()
if config_string == "latest" or not config_string:
return cls.latest()
if config_string.startswith(("file:", "packages:")):
# we only do the table-style parsing if it looks like a table,
# because this option used to be only a file path. We don't want
# to break existing configurations, whose file paths might include
# special characters like ':' or ' ', which would require quoting
# if they were to be passed as a parse_key_value_string positional
# argument.
return cls.from_table_style_config_string(config_string)
return cls(base_file_path=Path(config_string))
@classmethod
def from_table_style_config_string(cls, config_string: str) -> Self:
config_dict = parse_key_value_string(config_string, kw_arg_names=["file", "packages"])
files = config_dict.get("file")
packages = config_dict.get("packages") or []
if files and packages:
msg = "Cannot specify both a file and packages in dependency-versions"
raise ValueError(msg)
if files:
if len(files) > 1:
msg = unwrap("""
Only one file can be specified in dependency-versions.
If you intended to pass only one, perhaps you need to quote the path?
""")
raise ValueError(msg)
return cls(base_file_path=Path(files[0]))
return cls(packages=packages)
def get_for_python_version(
self, *, version: str, variant: Literal["python", "pyodide"] = "python", tmp_dir: Path
) -> Path | None:
if self.packages:
constraint_file = tmp_dir / "constraints.txt"
constraint_file.write_text("\n".join(self.packages))
return constraint_file
if self.base_file_path is not None:
version_parts = version.split(".")
# try to find a version-specific dependency file e.g. if
# ./constraints.txt is the base, look for ./constraints-python36.txt
specific_stem = (
self.base_file_path.stem + f"-{variant}{version_parts[0]}{version_parts[1]}"
)
specific_name = specific_stem + self.base_file_path.suffix
specific_file_path = self.base_file_path.with_name(specific_name)
if specific_file_path.exists():
return specific_file_path
else:
return self.base_file_path
return None
def options_summary(self) -> Any:
if self == DependencyConstraints.pinned():
return "pinned"
elif self.packages:
return {"packages": " ".join(shlex.quote(p) for p in self.packages)}
elif self.base_file_path is not None:
return self.base_file_path.name
else:
return "latest"
def get_pip_version(env: Mapping[str, str]) -> str:
versions_output_text = call(
"python", "-m", "pip", "freeze", "--all", capture_stdout=True, env=env
)
(pip_version,) = (
version[5:]
for version in versions_output_text.strip().splitlines()
if version.startswith("pip==")
)
return pip_version
T = TypeVar("T", bound=PurePath)
def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None:
"""
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("-", 1)
interpreter = interpreter.split("_")[0]
free_threaded = interpreter.endswith("t")
if free_threaded:
interpreter = interpreter[:-1]
for wheel in wheels:
_, _, _, tags = parse_wheel_filename(wheel.name)
for tag in tags:
if tag.abi == "abi3" and not free_threaded:
# 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 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", "android", "ios")):
# On these platforms the wheel tag includes a platform version number, which we
# should ignore.
os_, arch = platform.split("_", 1)
if not tag.platform.startswith(os_):
continue
if not tag.platform.endswith(f"_{arch}"):
continue
elif platform.startswith("pyodide"):
# each Pyodide version has its own platform tag
continue
else:
# Windows should exactly match
if tag.platform != platform:
continue
# If all the filters above pass, then the wheel is a previously built compatible wheel.
return wheel
return None
@@ -1,181 +0,0 @@
import fnmatch
import functools
import json
import platform
import typing
from pathlib import Path
from filelock import FileLock
from cibuildwheel.util.file import download, extract_tar
from cibuildwheel.util.resources import PYTHON_BUILD_STANDALONE_RELEASES
class PythonBuildStandaloneAsset(typing.TypedDict):
name: str
url: str
class PythonBuildStandaloneRelease(typing.TypedDict):
tag: str
assets: list[PythonBuildStandaloneAsset]
class PythonBuildStandaloneReleaseData(typing.TypedDict):
releases: list[PythonBuildStandaloneRelease]
@functools.cache
def get_python_build_standalone_release_data() -> PythonBuildStandaloneReleaseData:
with open(PYTHON_BUILD_STANDALONE_RELEASES, "rb") as f:
return typing.cast("PythonBuildStandaloneReleaseData", json.load(f))
class PythonBuildStandaloneError(Exception):
"""Errors related to python-build-standalone."""
def _get_platform_identifiers() -> tuple[str, str, str | None]:
"""
Detects the current platform and returns architecture, platform, and libc
identifiers.
"""
system = platform.system()
machine = platform.machine()
machine_lower = machine.lower()
arch_identifier: str
platform_identifier: str
libc_identifier: str | None = None
# Map Architecture
if machine_lower in ["x86_64", "amd64"]:
arch_identifier = "x86_64"
elif machine_lower in ["aarch64", "arm64"]:
arch_identifier = "aarch64"
else:
msg = f"Unsupported architecture: {system} {machine}. Cannot download appropriate Python build."
raise PythonBuildStandaloneError(msg)
# Map OS + Libc
if system == "Linux":
platform_identifier = "unknown-linux"
libc_identifier = "musl" if "musl" in (platform.libc_ver() or ("", "")) else "gnu"
elif system == "Darwin":
platform_identifier = "apple-darwin"
elif system == "Windows":
platform_identifier = "pc-windows-msvc"
else:
msg = f"Unsupported operating system: {system}. Cannot download appropriate Python build."
raise PythonBuildStandaloneError(msg)
print(
f"Detected platform: arch='{arch_identifier}', platform='{platform_identifier}', libc='{libc_identifier}'"
)
return arch_identifier, platform_identifier, libc_identifier
def _get_pbs_asset(
*,
python_version: str,
arch_identifier: str,
platform_identifier: str,
libc_identifier: str | None,
) -> tuple[str, str, str]:
"""Finds the asset, returning (tag, filename, url)."""
release_data = get_python_build_standalone_release_data()
expected_suffix = f"{arch_identifier}-{platform_identifier}"
if libc_identifier:
expected_suffix += f"-{libc_identifier}"
expected_suffix += "-install_only.tar.gz"
asset_pattern = f"cpython-{python_version}.*-{expected_suffix}"
print(f"Looking for file with pattern {asset_pattern}")
for release in release_data["releases"]:
for asset in release["assets"]:
asset_name = asset["name"]
if not fnmatch.fnmatch(asset_name, asset_pattern):
continue
asset_url = asset["url"]
return release["tag"], asset_url, asset_name
# If loop completes without finding a match
msg = f"Could not find python-build-standalone release asset matching {asset_pattern!r}."
raise PythonBuildStandaloneError(msg)
def _download_or_get_from_cache(asset_url: str, asset_filename: str, cache_dir: Path) -> Path:
with FileLock(cache_dir / (asset_filename + ".lock")):
asset_cache_path = cache_dir / asset_filename
if asset_cache_path.is_file():
print(f"Using cached python_build_standalone: {asset_cache_path}")
return asset_cache_path
print(f"Downloading python_build_standalone: {asset_url} to {asset_cache_path}")
download(asset_url, asset_cache_path)
return asset_cache_path
def _find_python_executable(extracted_dir: Path) -> Path:
"""Finds the python executable within the extracted directory structure."""
# Structure is typically 'python/bin/python' or 'python/python.exe'
base_install_dir = extracted_dir / "python"
if platform.system() == "Windows":
executable_path = base_install_dir / "python.exe"
else:
executable_path = base_install_dir / "bin" / "python"
if not executable_path.is_file():
msg = f"Could not locate python executable at expected path {executable_path} within {extracted_dir}."
raise PythonBuildStandaloneError(msg)
print(f"Found python executable: {executable_path}")
return executable_path.resolve() # Return absolute path
def create_python_build_standalone_environment(
python_version: str, temp_dir: Path, cache_dir: Path
) -> Path:
"""
Returns a Python environment from python-build-standalone, downloading it
if necessary using a cache, and expanding it into a fresh base path.
Args:
python_version: The Python version string (e.g., "3.12").
temp_dir: A directory where the Python environment will be created.
cache_dir: A directory to store/retrieve downloaded archives.
Returns:
The absolute path to the python executable within the created environment (in temp_dir).
Raises:
PythonBuildStandaloneError: If the platform is unsupported, the build cannot be found,
download/extraction fails, or configuration is invalid.
"""
print(f"Creating python-build-standalone environment: version={python_version}")
arch_id, platform_id, libc_id = _get_platform_identifiers()
pbs_tag, asset_url, asset_filename = _get_pbs_asset(
python_version=python_version,
arch_identifier=arch_id,
platform_identifier=platform_id,
libc_identifier=libc_id,
)
print(f"Using python-build-standalone release: {pbs_tag}")
archive_path = _download_or_get_from_cache(
asset_url=asset_url, asset_filename=asset_filename, cache_dir=cache_dir
)
python_base_dir = temp_dir / "pbs"
assert not python_base_dir.exists()
extract_tar(archive_path, python_base_dir)
return _find_python_executable(python_base_dir)

Some files were not shown because too many files have changed in this diff Show More