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
199 changed files with 6192 additions and 17345 deletions
+32 -38
View File
@@ -1,43 +1,25 @@
version: 2.1
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
version: 2
jobs:
osx-python312:
osx-python3.9:
macos:
xcode: 15.4.0
resource_class: macos.m1.medium.gen1
xcode: 12.5.1
environment:
PYTHON: python3
steps:
- 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:
- image: cimg/python:3.12
- image: circleci/python:3.9
environment:
PYTHON: python3
# Temporarily restrict the tests that are run on CircleCI to prevent
@@ -46,12 +28,18 @@ jobs:
steps:
- checkout
- 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:
machine:
image: default
image: ubuntu-2004:2022.04.1
resource_class: arm.medium
environment:
PYTHON: python3
@@ -60,13 +48,19 @@ jobs:
PYTEST_ADDOPTS: -k "unit_test or main_tests or test_0_basic or test_docker_images"
steps:
- 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:
version: 2
all-tests:
jobs:
- osx-python312
- linux-python312
- osx-python3.9
- linux-python3.9
- linux-aarch64
+3 -9
View File
@@ -2,15 +2,9 @@
set -o errexit
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 -m venv venv
venv/bin/python -m pip install -U pip
venv/bin/python -m pip install -e. --group test
$PYTHON -m pip --version
$PYTHON -m virtualenv -p "$PYTHON" venv
venv/bin/python -m pip install -e ".[dev]"
venv/bin/python -m pip freeze
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
install_cibuildwheel_script:
- python -m pip install -U pip
- python -m pip install -e. --group test
- python -m pip install -e ".[dev]" pytest-custom-exit-code
run_cibuildwheel_tests_script:
- python ./bin/run_tests.py
linux_x86_task:
timeout_in: 120m
compute_engine_instance:
@@ -15,15 +15,9 @@ linux_x86_task:
platform: linux
cpu: 8
memory: 8G
env:
VENV_ROOT: ${HOME}/venv-cibuildwheel
PATH: ${VENV_ROOT}/bin:${PATH}
install_pre_requirements_script:
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
- add-apt-repository -y ppa:deadsnakes/ppa
- apt-get update
- apt-get install -y python3.12-venv
- python3.12 -m venv ${VENV_ROOT}
- apt install -y python3-venv python-is-python3
<<: *RUN_TESTS
linux_aarch64_task:
@@ -34,20 +28,14 @@ linux_aarch64_task:
platform: linux
cpu: 4
memory: 4G
env:
VENV_ROOT: ${HOME}/venv-cibuildwheel
PATH: ${VENV_ROOT}/bin:${PATH}
install_pre_requirements_script:
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
- add-apt-repository -y ppa:deadsnakes/ppa
- apt-get update
- apt-get install -y python3.12-venv
- python3.12 -m venv ${VENV_ROOT}
- apt install -y python3-venv python-is-python3
<<: *RUN_TESTS
windows_x86_task:
# The task takes ~55 minutes while the timeout happens after 60 minutes by default,
# let's allow some wiggle room.
# The task takes ~55 minutes while the timeout happens
# after 60 minutes by default, let's allow some wiggle room.
timeout_in: 120m
windows_container:
image: cirrusci/windowsservercore:visualstudio2022
@@ -55,35 +43,32 @@ windows_x86_task:
memory: 8G
install_pre_requirements_script:
- certutil -generateSSTFromWU roots.sst
- certutil -addstore -f root roots.sst
- del roots.sst
- choco install -y --no-progress python3 --version 3.12.4
- choco install -y --no-progress python3 --version 3.10.6
- refreshenv
- powershell -Command "$cleanPath = $env:PATH -replace ';+$', ''; Add-Content -Path $env:CIRRUS_ENV -Value ('PATH=' + $cleanPath)"
- echo PATH=%PATH% >> "%CIRRUS_ENV%"
<<: *RUN_TESTS
macos_arm64_task:
macos_instance:
image: ghcr.io/cirruslabs/macos-runner:sequoia
image: ghcr.io/cirruslabs/macos-monterey-xcode
env:
VENV_ROOT: ${HOME}/venv-cibuildwheel
PATH: ${VENV_ROOT}/bin:${PATH}
PATH: /opt/homebrew/opt/python@3.10/bin:$PATH
install_pre_requirements_script:
- brew install python@3.12
- python3.12 -m venv ${VENV_ROOT}
- brew install python@3.10
- ln -s python3 /opt/homebrew/opt/python@3.10/bin/python
<<: *RUN_TESTS
macos_arm64_cp38_task:
macos_instance:
image: ghcr.io/cirruslabs/macos-runner:sequoia
image: ghcr.io/cirruslabs/macos-monterey-xcode
env:
VENV_ROOT: ${HOME}/venv-cibuildwheel
PATH: ${VENV_ROOT}/bin:${PATH}
PYTEST_ADDOPTS: --run-cp38-universal2 -k 'test_cp38_arm64_testing_universal2_installer or test_arch_auto or test_dummy_serial'
PATH: /opt/homebrew/opt/python@3.10/bin:$PATH
PYTEST_ADDOPTS: --run-cp38-universal2 -k 'test_cp38_arm64_testing_universal2_installer or test_arch_auto'
install_pre_requirements_script:
- brew install python@3.12
- python3.12 -m venv ${VENV_ROOT}
- brew install python@3.10
- 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
- sudo installer -pkg python-3.8.10-macos11.pkg -target /
- 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:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- 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:
needs: [dist]
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'published'
environment:
name: pypi
url: https://pypi.org/p/cibuildwheel
environment: pypi
permissions:
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v4
with:
name: Packages
pattern: cibw-*
path: dist
- name: Generate artifact attestation for sdist and wheel
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-path: "dist/cibuildwheel-*"
merge-multiple: true
- uses: pypa/gh-action-pypi-publish@release/v1
with:
attestations: true
+43 -216
View File
@@ -4,27 +4,10 @@ on:
push:
branches:
- main
- 2.x
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
paths-ignore:
- .ci*
- bin/*
- docs/**
- examples/azure-pipelines-*
- examples/ci*
- examples/travis-ci-*
- 'docs/**'
- .pre-commit-config.yaml
- .readthedocs.yml
- .travis.yml
- README.md
- azure-pipelines.yml
- mkdocs.yml
- noxfile.py
workflow_dispatch:
# allow manual runs on branches without a PR
@@ -34,120 +17,65 @@ concurrency:
jobs:
lint:
name: Linters (mypy, ruff, etc.)
name: Linters (mypy, flake8, etc.)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
id: python
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- uses: pre-commit/action@v3.0.1
- uses: pre-commit/action@v3.0.0
- name: Check manifest
run: pipx run nox -s check_manifest
- 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"
pipx run nox -s pylint
test:
name: Test on ${{ matrix.os }} (${{ matrix.python_version }}) ${{ matrix.test_select }}
name: Test cibuildwheel on ${{ matrix.os }}
needs: lint
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, windows-latest, macos-13, macos-14]
python_version: ['3.12']
include:
# Min Python
- os: ubuntu-latest
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
- os: ubuntu-22.04
python_version: '3.8'
timeout-minutes: 180
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
name: Install Python ${{ matrix.python_version }}
with:
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: Install latest podman
if: runner.os == 'Linux'
run: |
. /etc/os-release
echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
curl -sSfL "https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/Release.key" | sudo apt-key add -
sudo apt-get update
sudo apt-get -y install podman
- name: Free up disk space
# free some space to prevent reaching GHA disk space limits
- name: Clean docker images
if: runner.os == 'Linux'
run: |
docker system prune -a -f
sudo rm -rf $ANDROID_HOME/ndk/{26,28}.* /opt/hostedtoolcache/CodeQL \
/usr/local/lib/node_modules /usr/local/share/chromium \
/usr/local/share/powershell
df -h
# for oci_container unit tests
- name: Set up QEMU
if: runner.os == 'Linux'
uses: docker/setup-qemu-action@v3
- name: Set up Xcode
if: ${{ startsWith(matrix.os, 'macos-15') }}
run: |
# GitHub recommends explicitly selecting the desired Xcode version
sudo xcode-select --switch /Applications/Xcode_16.4.app
- 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: |
if [[ "${{ github.ref_name }}" == "main" ]]; then
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
if [[ -n "$GITHUB_PR_LABEL_CI_PYPY" ]]; then
CIBW_ENABLE+=" pypy"
fi
if [[ -n "$GITHUB_PR_LABEL_CI_GRAALPY" ]]; then
CIBW_ENABLE+=" graalpy"
fi
fi
echo "CIBW_ENABLE=${CIBW_ENABLE}" >> $GITHUB_ENV
python -m pip install ".[test]"
- name: Generate a sample project
run: |
uv run --no-sync -m test.test_projects test.test_0_basic.basic_project sample_proj
python -m test.test_projects test.test_0_basic.basic_project sample_proj
- name: Run a sample build (GitHub Action)
uses: ./
@@ -156,137 +84,36 @@ jobs:
output-dir: wheelhouse
env:
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: ./
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@v5
- uses: actions/upload-artifact@v4
with:
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: wheelhouse/*.whl
- 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@v5
- 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: |
OUTPUT=$(uv run --no-sync python -c "from json import dumps; from test.utils import EMULATED_ARCHS; print(dumps(EMULATED_ARCHS))")
echo "${OUTPUT}"
echo "archs=${OUTPUT}" >> "$GITHUB_OUTPUT"
python ./bin/run_tests.py --run-podman
test-emulated:
name: Test Linux ${{ matrix.arch }} using qemu
needs: emulated-archs
name: Test emulated cibuildwheel using qemu
needs: lint
runs-on: ubuntu-latest
timeout-minutes: 180
strategy:
matrix:
arch: ${{ fromJSON(needs.emulated-archs.outputs.archs) }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: uv sync --no-dev --group test
run: |
python -m pip install ".[test]"
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Run the emulation tests
run: uv run --no-sync pytest --run-emulation ${{ matrix.arch }} test/test_emulation.py
test-pyodide:
name: Test pyodide
needs: lint
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- uses: actions/checkout@v5
- 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: Generate a sample project
run: |
uv run --no-sync -m test.test_projects test.test_0_basic.basic_project 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
pytest --run-emulation test/test_emulation.py
+16 -18
View File
@@ -18,21 +18,13 @@ jobs:
update-dependencies:
name: Update dependencies
if: github.repository_owner == 'pypa' || github.event_name != 'schedule'
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
# we use this step to grab a Github App auth token, so that PRs generated by this workflow
# run the GHA tests.
- uses: actions/create-github-app-token@v2
id: generate-token
if: github.ref == 'refs/heads/main' && github.repository == 'pypa/cibuildwheel'
- uses: wntrblm/nox@2023.04.22
with:
app-id: ${{ secrets.CIBUILDWHEEL_BOT_APP_ID }}
private-key: ${{ secrets.CIBUILDWHEEL_BOT_APP_PRIVATE_KEY }}
- uses: actions/checkout@v5
- uses: wntrblm/nox@2025.10.16
python-versions: "3.7, 3.8, 3.9, 3.10, 3.11, 3.12"
- name: "Run update: dependencies"
run: nox --force-color -s update_constraints
@@ -41,9 +33,18 @@ jobs:
- name: "Run update: docs user projects"
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
if: github.ref == 'refs/heads/main' && github.repository == 'pypa/cibuildwheel'
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@v5
with:
commit-message: 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}}).
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 }}
delete-branch: true
labels: |
CI: GraalPy
CI: PyPy
dependencies
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Update the ${{ env.TAG_NAME }} tag
id: update-major-minor-tag
+6 -1
View File
@@ -84,7 +84,9 @@ celerybeat-schedule
# virtualenv
.venv
venv*/
venv/
venv3/
venv2/
ENV/
env/
env2/
@@ -110,5 +112,8 @@ all_known_setup.yaml
# mkdocs
site/
# Virtual environments
venv*
# PyCharm
.idea/
+5 -31
View File
@@ -1,5 +1,5 @@
linux:
image: python:3.12
image: python:3.8
services:
- name: docker:dind
entrypoint: ["env", "-u", "DOCKER_HOST"]
@@ -13,15 +13,9 @@ linux:
# skip all but the basic tests
# (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
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
CIBW_ENABLE: "all"
script:
- curl -sSL https://get.docker.com/ | sh
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
- python -m pip install -U pip
- python -m pip install -e. pytest-custom-exit-code --group test
- python -m pip install -e ".[dev]" pytest-custom-exit-code
- python ./bin/run_tests.py
windows:
@@ -29,29 +23,9 @@ windows:
variables:
PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
before_script:
- choco install python -y --allow-downgrade --version 3.12.4
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
CIBW_ENABLE: "all"
- choco install python -y --version 3.8.6
script:
- py -m pip install -U pip
- py -m pip install -e. pytest-custom-exit-code --group test
- py -m pip install -e ".[dev]" pytest-custom-exit-code
- py bin\run_tests.py
tags:
- saas-windows-medium-amd64
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
- windows
+29 -31
View File
@@ -1,7 +1,7 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: v4.5.0
hooks:
- id: check-case-conflict
- id: check-merge-conflict
@@ -14,47 +14,47 @@ repos:
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.4
rev: v0.1.14
hooks:
- id: ruff-check
- id: ruff
args: ["--fix", "--show-fixes"]
- 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
rev: v1.18.2
rev: v1.8.0
hooks:
- id: mypy
name: mypy 3.11 on cibuildwheel/
args: ["--python-version=3.11"]
exclude: ^cibuildwheel/resources/_cross_venv.py$ # Requires Python 3.13 or later
name: mypy 3.8 on cibuildwheel/
exclude: ^cibuildwheel/resources/.*py|bin/generate_schema.py$
args: ["--python-version=3.8"]
additional_dependencies: &mypy-dependencies
- bracex
- build
- dependency-groups>=1.2
- humanize
- nox>=2025.2.9
- orjson
- nox
- packaging
- pyelftools
- pygithub
- pytest
- rich
- tomli
- tomli_w
- types-certifi
- types-click
- types-jinja2
- types-pyyaml
- types-requests
- types-setuptools
- uv
- validate-pyproject
- id: mypy
name: mypy 3.13
args: ["--python-version=3.13"]
name: mypy 3.12
exclude: ^cibuildwheel/resources/.*py$
args: ["--python-version=3.12"]
additional_dependencies: *mypy-dependencies
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
rev: v0.9.0.6
hooks:
- id: shellcheck
@@ -64,33 +64,31 @@ repos:
name: Disallow improper capitalization
language: pygrep
entry: PyBind|Numpy|Cmake|Github|PyTest
types: [markdown]
types:
- markdown
exclude: ^docs/working-examples\.md$ # Autogenerated
- id: cog
name: Cog the README
- id: update-readme-changelog
name: Update README changelog
language: python
pass_filenames: false
entry: cog -c -P -r -I ./bin README.md
files: '^(README\.md|docs/changelog\.md|docs/options\.md|bin/readme.*)$'
additional_dependencies: [cogapp>=3.5]
entry: bin/update_readme_changelog.py
files: ^docs/changelog.md$
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
rev: v2.2.6
hooks:
- id: codespell
args: ["-w"]
args: ["-L", "sur", "-w"]
exclude: ^docs/working-examples\.md$ # Autogenerated
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.34.1
rev: 0.27.0
hooks:
- id: check-dependabot
- id: check-github-actions
- id: check-github-workflows
- id: check-gitlab-ci
# Disabled due to schema issue for now:
# - id: check-readthedocs
- id: check-readthedocs
- id: check-travis
- id: check-jsonschema
name: Check projects
+13 -6
View File
@@ -3,9 +3,16 @@
version: 2
build:
os: ubuntu-24.04
commands:
- asdf plugin add uv
- asdf install uv latest
- asdf global uv latest
- NO_COLOR=1 uv run --python 3.14 --managed-python --no-dev --group docs mkdocs build --strict --site-dir $READTHEDOCS_OUTPUT/html
os: ubuntu-22.04
tools:
python: "3.11"
python:
install:
- method: pip
path: .
extra_requirements:
- docs
mkdocs:
configuration: mkdocs.yml
+20 -39
View File
@@ -1,34 +1,28 @@
os: linux
dist: jammy
dist: focal
language: python
branches:
only:
- main
# only build the main branch or PR that explicitely want to test with Travis CI
if: (type = "push") OR (commit_message =~ /travis-ci/)
jobs:
include:
- name: Linux | x86_64 + i686 | Python 3.12
python: 3.12
- name: Linux | x86_64 + i686 | Python 3.9
python: 3.9
services: docker
env:
- PYTHON=python
# a build using CIBW_ENABLE=all does not fit into Travis' time limit,
# so only the defaults are tested
env: PYTHON=python
- name: Linux | arm64 | Python 3.12
python: 3.12
- name: Linux | arm64 | Python 3.9
python: 3.9
services: docker
arch: arm64
env:
- PYTHON=python
- CIBW_ENABLE=all
arch: arm64-graviton2
group: edge
virt: vm
env: PYTHON=python
- name: Linux | ppc64le | Python 3.12
python: 3.12
- name: Linux | ppc64le | Python 3.9
python: 3.9
services: docker
arch: ppc64le
allow_failure: True
@@ -37,35 +31,24 @@ jobs:
# 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"'
- CIBW_ENABLE=all
- name: Windows | x86_64 | Python 3.12
- name: Windows | x86_64 | Python 3.9
os: windows
language: shell
before_install:
# http://woshub.com/updating-trusted-root-certificates-in-windows-10
- certutil -generateSSTFromWU roots.sst
- powershell -Command 'Get-ChildItem -Path roots.sst | Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root'
- rm -f roots.sst
- choco upgrade python3 -y --version 3.12.8 --limit-output --params "/InstallDir:C:\\Python312"
- choco upgrade python3 -y --version 3.9.13 --limit-output --params "/InstallDir:C:\\Python39"
env:
- PYTHON=C:\\Python312\\python
# a build using CIBW_ENABLE=all does not fit into Travis' time limit,
# so only the defaults are tested
- PYTHON=C:\\Python39\\python
- name: Linux | s390x | Python 3.12
python: 3.12
- name: Linux | s390x | Python 3.9
python: 3.9
services: docker
arch: s390x
allow_failure: True
env:
- PYTHON=python
- CIBW_ENABLE=all
env: PYTHON=python
install:
- if [ "${TRAVIS_OS_NAME}" == "linux" ]; then docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all; fi
- $PYTHON -m pip install -U pip
- $PYTHON -m pip install -e. --group test
- $PYTHON -m pip install -e ".[dev]" pytest-custom-exit-code
script: |
# travis_wait disable the output while waiting
@@ -73,7 +56,5 @@ script: |
(while true; do echo "travis_keep_alive"; sleep 300; done) &
SPINNER_PID=$!
disown
result=0
$PYTHON ./bin/run_tests.py --num-processes 2 || result=1
$PYTHON ./bin/run_tests.py
kill -9 ${SPINNER_PID}
test ${result} -eq 0
+6 -6
View File
@@ -1,11 +1,11 @@
This is a summary of the host Python versions and platforms covered by the different CI platforms:
| | 3.11 | 3.12 | 3.13 |
|---------|----------------------------------|---------------------------------------------|----------------|
| Linux | Azure Pipelines / GitHub Actions | CircleCI¹ / Cirrus CI / GitLab¹ / Travis CI | GitHub Actions |
| macOS | Azure Pipelines | CircleCI¹ / Cirrus CI / GitLab¹ | GitHub Actions |
| Windows | Azure Pipelines | Cirrus CI / GitLab¹ / Travis CI | GitHub Actions |
| | 3.8 | 3.9 | 3.10 | 3.12 |
|---------|--------------------------------------------------------|-----------------------|-----------|----------------|
| Linux | AppVeyor¹ / Azure Pipelines / GitLab¹ / GitHub Actions | CircleCI¹ / Travis CI | Cirrus CI | GitHub Actions |
| macOS | AppVeyor¹ / Azure Pipelines | CircleCI¹ / Travis CI | Cirrus CI | GitHub Actions |
| Windows | AppVeyor¹ / Azure Pipelines / GitLab¹ | Travis CI | Cirrus CI | GitHub Actions |
> ¹ Runs a reduced set of tests to reduce CI load
Non-x86 architectures are covered on Travis CI using Python 3.12.
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'.
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
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 -154
View File
@@ -2,74 +2,67 @@ 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)
[![Travis Status](https://img.shields.io/travis/com/pypa/cibuildwheel/main?logo=travis)](https://travis-ci.com/github/pypa/cibuildwheel)
[![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)
[![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-->
Python wheels are great. Building them across **Mac, Linux, Windows**, on **multiple versions of Python**, is not.
`cibuildwheel` is here to help. `cibuildwheel` runs on your CI server - currently it supports GitHub Actions, Azure Pipelines, Travis CI, 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?
----------------
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 |
|--------------------|----|-----|----|-----|-----|----|-----|----|-----|-----|---|-----|-----|-----|
| 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>¹ PyPy is only supported for manylinux wheels.</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
- Works on GitHub Actions, Azure Pipelines, Travis CI, CircleCI, GitLab CI, and Cirrus CI
- Builds manylinux, musllinux, macOS 10.9+, and Windows wheels for CPython and PyPy
- 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)
- 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
-----
`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 |
|-----------------|-------|-------|---------|-----------|-----------|-------------|---------|-----|
| GitHub Actions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅⁴ | ✅³ |
| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² | ✅⁴ | ✅³ |
| Travis CI | ✅ | | ✅ | ✅ | | | ✅⁴ | |
| CircleCI | ✅ | ✅ | | ✅ | ✅ | | ✅⁴ | ✅³ |
| Gitlab CI | ✅ | ✅ | | ✅¹ | ✅ | | ✅⁴ | ✅³ |
| Cirrus CI | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅⁴ | |
| | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM |
|-----------------|-------|-------|---------|-----------|-----------|-------------|
| GitHub Actions | ✅ | ✅ | ✅ | ✅¹ | ✅ | ✅² |
| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² |
| Travis CI | ✅ | | ✅ | ✅ | | |
| AppVeyor | ✅ | ✅ | | | ✅ | ✅² |
| 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>² [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>³ 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>
<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.readthedocs.io/en/stable/faq/#windows-arm64). It is not possible to test `arm64` on this CI platform.</sup>
<!--intro-end-->
@@ -78,7 +71,6 @@ Example setup
To build manylinux, musllinux, macOS, and Windows wheels on GitHub Actions, you could use this `.github/workflows/wheels.yml`:
<!--generic-github-start-->
```yaml
name: Build
@@ -90,32 +82,30 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
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:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
# Used to host cibuildwheel
- uses: actions/setup-python@v5
- uses: actions/setup-python@v3
- name: Install cibuildwheel
run: python -m pip install cibuildwheel==3.3.1
run: python -m pip install cibuildwheel==2.16.5
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
# to supply options, put them in 'env', like:
# env:
# CIBW_SOME_OPTION: value
# ...
- uses: actions/upload-artifact@v4
with:
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
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
------------
@@ -124,50 +114,35 @@ The following diagram summarises the steps that cibuildwheel takes on each platf
![](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>
<!--[[[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 -->
Options
-------
| | Option | Description |
|---|---|---|
| **Build selection** | [`platform`](https://cibuildwheel.pypa.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 |
| | [`archs`](https://cibuildwheel.pypa.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 |
| | [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable) | Enable building with extra categories of selectors present. |
| | [`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** | [`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" |
| | [`config-settings`](https://cibuildwheel.pypa.io/en/stable/options/#config-settings) | Specify config-settings for the build backend. |
| | [`environment`](https://cibuildwheel.pypa.io/en/stable/options/#environment) | Set environment variables |
| | [`environment-pass`](https://cibuildwheel.pypa.io/en/stable/options/#environment-pass) | Set environment variables on the host to pass-through to the container. |
| | [`before-all`](https://cibuildwheel.pypa.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
| | [`before-build`](https://cibuildwheel.pypa.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
| | [`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. |
| | [`repair-wheel-command`](https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel |
| | [`manylinux-*-image`<br>`musllinux-*-image`](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) | Specify manylinux / musllinux container images |
| | [`container-engine`](https://cibuildwheel.pypa.io/en/stable/options/#container-engine) | Specify the container engine to use when building Linux wheels |
| | [`dependency-versions`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) | Control the versions of the tools cibuildwheel uses |
| | [`pyodide-version`](https://cibuildwheel.pypa.io/en/stable/options/#pyodide-version) | Specify the Pyodide version to use for `pyodide` platform builds |
| **Testing** | [`test-command`](https://cibuildwheel.pypa.io/en/stable/options/#test-command) | The command to test each built wheel |
| | [`before-test`](https://cibuildwheel.pypa.io/en/stable/options/#before-test) | Execute a shell command before testing each 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 |
|---|--------|-------------|
| **Build selection** | [`CIBW_PLATFORM`](https://cibuildwheel.readthedocs.io/en/stable/options/#platform) | Override the auto-detected target platform |
| | [`CIBW_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) <br> [`CIBW_SKIP`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) | Choose the Python versions to build |
| | [`CIBW_ARCHS`](https://cibuildwheel.readthedocs.io/en/stable/options/#archs) | Change the architectures built on your machine by default. |
| | [`CIBW_PROJECT_REQUIRES_PYTHON`](https://cibuildwheel.readthedocs.io/en/stable/options/#requires-python) | Manually set the Python compatibility of your project |
| | [`CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#prerelease-pythons) | Enable building with pre-release versions of Python if available |
| **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" |
| | [`CIBW_ENVIRONMENT`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment) | Set environment variables needed during the build |
| | [`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. |
| | [`CIBW_BEFORE_ALL`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel |
| | [`CIBW_MANYLINUX_*_IMAGE`<br/>`CIBW_MUSLLINUX_*_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images |
| | [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.readthedocs.io/en/stable/options/#container-engine) | Specify which container engine to use when building Linux wheels |
| | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses |
| **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel |
| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
| | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
| | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require |
| | [`CIBW_TEST_SKIP`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-skip) | Skip running tests on some builds |
| **Other** | [`CIBW_BUILD_VERBOSITY`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-verbosity) | Increase/decrease the output of pip wheel |
<!--[[[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/).
These options can be specified in a pyproject.toml file, as well; see [configuration](https://cibuildwheel.readthedocs.io/en/stable/options/#configuration).
Working examples
----------------
@@ -181,27 +156,28 @@ Here are some repos that use cibuildwheel.
| 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. |
| [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. |
| [NumPy][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The fundamental package for scientific computing with Python. |
| [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. |
| [NCNN][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | ncnn is a high-performance neural network inference framework optimized for the mobile platform |
| [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. |
| [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 |
| [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
[duckdb]: https://github.com/duckdb/duckdb
[pytorch-fairseq]: https://github.com/facebookresearch/fairseq
[pytorch-fairseq]: https://github.com/pytorch/fairseq
[NumPy]: https://github.com/numpy/numpy
[Tornado]: https://github.com/tornadoweb/tornado
[NCNN]: https://github.com/Tencent/ncnn
[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
[MyPy]: https://github.com/mypyc/mypy_mypyc-wheels
[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
[azurepipelines icon]: docs/data/readme_icons/azurepipelines.svg
[circleci icon]: docs/data/readme_icons/circleci.svg
@@ -214,7 +190,7 @@ Here are some repos that use cibuildwheel.
<!-- 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
----------
@@ -228,102 +204,75 @@ This is similar to static linking, so it might have some license implications. C
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)
- ✨ 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)
_28 January 2024_
### 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)
- 🐛 Fix the default MACOSX_DEPLOYMENT_TARGET on Python 3.14 (#2613)
- 📚 Docs improvements (#2617)
_26 January 2024_
### 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)
- 🛠 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)
_3 October 2023_
### 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)
- 🛠 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)
_26 September 2023_
<!-- [[[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.
️ **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
============
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).
Maintainers
-----------
Core:
- Joe Rickerby [@joerick](https://github.com/joerick)
- Yannick Jadoul [@YannickJadoul](https://github.com/YannickJadoul)
- Matthieu Darbois [@mayeut](https://github.com/mayeut)
- Henry Schreiner [@henryiii](https://github.com/henryiii)
- 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
-------
@@ -346,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.
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 -73
View File
@@ -17,10 +17,6 @@ inputs:
description: 'Build a specific wheel only. No need for arch/platform if this is set'
required: false
default: ''
extras:
description: 'Comma-separated list of extras to install'
required: false
default: ''
branding:
icon: package
color: yellow
@@ -28,92 +24,45 @@ branding:
runs:
using: composite
steps:
- uses: actions/setup-python@v6
# Set up a non-EOL, cibuildwheel & pipx supported Python version
- uses: actions/setup-python@v5
id: python
with:
python-version: "3.11 - 3.13"
python-version: "3.8 - 3.12"
update-environment: false
- id: cibw
run: |
# Install cibuildwheel
"${{ 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())
if sys.platform == "linux":
EXTRAS.discard("uv")
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
# macos-14 (M1) may be missing pipx (due to it not having CPython)
- run: |
"${{ steps.python.outputs.python-path }}" -m pip install pipx
shell: bash
# Redirecting stderr to stdout to fix interleaving issue in Actions.
- run: >
"${{ steps.python.outputs.python-path }}" -m pipx run
--spec '${{ github.action_path }}'
cibuildwheel
"${{ inputs.package-dir }}"
${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}}
${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}}
${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}}
--output-dir "${{ inputs.output-dir }}"
--config-file "${{ inputs.config-file }}"
--only "${{ inputs.only }}"
2>&1
env:
PATH: "${{ steps.cibw.outputs.updated-path }}"
shell: bash
if: runner.os != 'Windows'
# 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: >
if ($PSNativeCommandArgumentPassing) {
$PSNativeCommandArgumentPassing = 'Legacy'
};
& "${{ steps.python.outputs.python-path }}" -m pipx run
--spec "${{ github.action_path }}"
cibuildwheel
"${{ inputs.package-dir }}"
${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}}
${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}}
${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}}
env:
PATH: "${{ steps.cibw.outputs.updated-path }}"
--output-dir '"${{ inputs.output-dir }}"'
--config-file '"${{ inputs.config-file }}"'
--only '"${{ inputs.only }}"'
shell: pwsh
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:
paths:
exclude:
- .github/**
- bin/*
- docs/**
- examples/github-*
- examples/ci*
- examples/travis-ci-*
- .ci*
- docs/*
- .pre-commit-config.yaml
- .readthedocs.yml
- .travis.yml
- README.md
- mkdocs.yml
- noxfile.py
jobs:
- job: tests
strategy:
matrix:
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)
- job: linux_38
timeoutInMinutes: 120
pool: {vmImage: 'Ubuntu-20.04'}
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: $(pythonVersion)
- 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)
versionSpec: '3.8'
- bash: |
echo "CIBW_ENABLE = $CIBW_ENABLE"
python ./bin/run_tests.py --test-select $(testSelect)
displayName: 'Run tests'
python -m pip install -e ".[dev]"
python ./bin/run_tests.py
- 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
+13 -13
View File
@@ -1,29 +1,30 @@
#!/usr/bin/env python3
# /// script
# dependencies = ["click", "packaging"]
# ///
from __future__ import annotations
import glob
import os
import subprocess
import sys
import tomllib
import urllib.parse
from pathlib import Path
import click
from packaging.version import InvalidVersion, Version
import cibuildwheel
config = [
# file path, version find/replace format
("pyproject.toml", 'version = "{}"'),
("README.md", "cibuildwheel=={}"),
("cibuildwheel/__init__.py", '__version__ = "{}"'),
("docs/faq.md", "cibuildwheel=={}"),
("docs/faq.md", "cibuildwheel@v{}"),
("docs/setup.md", "cibuildwheel=={}"),
("examples/*", "cibuildwheel=={}"),
("examples/*", "cibuildwheel@v{}"),
("setup.cfg", "version = {}"),
]
RED = "\u001b[31m"
@@ -33,8 +34,7 @@ OFF = "\u001b[0m"
@click.command()
def bump_version() -> None:
with open("pyproject.toml", "rb") as f:
current_version = tomllib.load(f)["project"]["version"]
current_version = cibuildwheel.__version__
try:
commit_date_str = subprocess.run(
@@ -68,8 +68,8 @@ def bump_version() -> None:
sys.exit(1)
# fmt: off
print( "Current version:", current_version)
new_version = input(" New version: ").strip()
print( 'Current version:', current_version)
new_version = input(' New version: ').strip()
# fmt: on
try:
@@ -82,7 +82,7 @@ def bump_version() -> None:
actions = []
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:
print(f"error: Pattern {path_pattern} didn't match any files")
@@ -129,8 +129,8 @@ def bump_version() -> None:
contents = contents.replace(find, replace)
path.write_text(contents, encoding="utf8")
print("Files updated. If you want to update docs/changelog.md as part of")
print("this commit, do that now.")
print("Files updated. If you want to update the changelog as part of this")
print("commit, do that now.")
print()
while input('Type "done" to continue: ').strip().lower() != "done":
@@ -181,7 +181,7 @@ def bump_version() -> None:
print()
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()
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 -133
View File
@@ -1,8 +1,4 @@
#!/usr/bin/env -S uv run -q
# /// script
# dependencies = ["pyyaml"]
# ///
#!/usr/bin/env python
import argparse
import copy
@@ -11,31 +7,13 @@ from typing import Any
import yaml
parser = argparse.ArgumentParser(allow_abbrev=False)
parser = argparse.ArgumentParser()
parser.add_argument("--schemastore", action="store_true", help="Generate schema_store version")
args = parser.parse_args()
starter = """
$schema: http://json-schema.org/draft-07/schema#
$id: https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json
$defs:
inherit:
enum:
- none
- prepend
- append
default: none
description: How to inherit the parent's value.
enable:
enum:
- cpython-experimental-riscv64
- cpython-freethreading
- cpython-prerelease
- graalpy
- pyodide-prerelease
- pypy
- pypy-eol
description: A Python version or flavor to enable.
$schema: http://json-schema.org/draft-07/schema
additionalProperties: false
description: cibuildwheel's settings.
type: object
@@ -58,21 +36,19 @@ properties:
type: string_array
build-frontend:
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:
- enum: [pip, build, "build[uv]", default]
- enum: [pip, build, default]
- type: string
pattern: '^pip; ?args:'
- type: string
pattern: '^build; ?args:'
- type: string
pattern: '^build\\[uv\\]; ?args:'
- type: object
additionalProperties: false
required: [name]
properties:
name:
enum: [pip, build, "build[uv]"]
enum: [pip, build]
args:
type: array
items:
@@ -108,31 +84,7 @@ properties:
dependency-versions:
default: pinned
description: Specify how cibuildwheel controls the versions of the tools it uses
oneOf:
- 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"
type: string
environment:
description: Set environment variables needed during the build.
type: string_table
@@ -143,9 +95,6 @@ properties:
manylinux-aarch64-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-armv7l-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-i686-image:
type: string
description: Specify alternative manylinux / musllinux container images
@@ -161,9 +110,6 @@ properties:
manylinux-pypy_x86_64-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-riscv64-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-s390x-image:
type: string
description: Specify alternative manylinux / musllinux container images
@@ -173,33 +119,21 @@ properties:
musllinux-aarch64-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-armv7l-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-i686-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-ppc64le-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-riscv64-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-s390x-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-x86_64-image:
type: string
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:
description: Execute a shell command to repair each built wheel.
type: string_array
description: Execute a shell command to repair each built wheel.
skip:
description: Choose the Python versions to skip.
type: string_array
@@ -209,43 +143,19 @@ properties:
test-extras:
description: Install your wheel for testing using `extras_require`
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:
description: Install Python dependencies before running the tests
type: string_array
test-skip:
description: Skip running tests on some builds.
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)
if args.schemastore:
schema["$id"] += "#"
string_array = yaml.safe_load(
"""
- type: string
@@ -304,36 +214,23 @@ items:
additionalProperties: false
properties:
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():
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["skip"]
del non_global_options["container-engine"]
del non_global_options["test-skip"]
del non_global_options["enable"]
overrides["items"]["properties"]["select"]["oneOf"] = string_array
overrides["items"]["properties"] |= non_global_options.copy()
@@ -343,7 +240,6 @@ del overrides["items"]["properties"]["archs"]
not_linux = non_global_options.copy()
del not_linux["environment-pass"]
del not_linux["container-engine"]
for key in list(not_linux):
if "linux-" in key:
del not_linux[key]
@@ -361,9 +257,6 @@ oses = {
"linux": as_object(non_global_options),
"windows": 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"] = {
@@ -380,10 +273,27 @@ del oses["linux"]["properties"]["dependency-versions"]
schema["properties"]["overrides"] = overrides
schema["properties"] |= oses
if args.schemastore:
schema["$id"] = "https://json.schemastore.org/partial-cibuildwheel.json"
schema["description"] = (
"cibuildwheel's settings. Generated with ./bin/generate_schema.py --schemastore from cibuildwheel."
)
if not args.schemastore:
print(json.dumps(schema, indent=2))
raise SystemExit(0)
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.
"""
from __future__ import annotations
import ast
from collections.abc import Iterable, Iterator
from pathlib import Path
+10 -11
View File
@@ -1,30 +1,29 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
import subprocess
import sys
import textwrap
import time
from pathlib import Path
from subprocess import run
import click
def shell(cmd: str, *, check: bool, **kwargs: object) -> subprocess.CompletedProcess[str]:
return subprocess.run([cmd], shell=True, check=check, **kwargs) # type: ignore[call-overload, no-any-return]
def shell(cmd, *, check: bool, **kwargs):
return run([cmd], shell=True, check=check, **kwargs)
def git_repo_has_changes() -> bool:
unstaged_changes: bool = shell("git diff-index --quiet HEAD --", check=False).returncode != 0
staged_changes: bool = (
shell("git diff-index --quiet --cached HEAD --", check=False).returncode != 0
)
def git_repo_has_changes():
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
return unstaged_changes or staged_changes
@click.command()
def main() -> None:
def main():
project_root = Path(__file__).parent / ".."
os.chdir(project_root)
@@ -55,10 +54,10 @@ def main() -> None:
f"""
Update the versions of our dependencies.
PR generated by `{Path(__file__).name}`.
PR generated by `{os.path.basename(__file__)}`.
"""
)
subprocess.run(
run(
[
"gh",
"pr",
+13 -21
View File
@@ -1,13 +1,5 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "click",
# "pyyaml",
# "pygithub",
# ]
# ///
"""
Convert a yaml project list into a nice table.
@@ -17,22 +9,25 @@ Suggested usage:
git diff
"""
from __future__ import annotations
import builtins
import functools
import textwrap
import urllib.request
import xml.dom.minidom
from collections.abc import Iterable, Mapping, Sequence
from datetime import UTC, datetime
from datetime import datetime
from io import StringIO
from pathlib import Path
from typing import Any, Self, TextIO
from typing import Any, TextIO
import click
import yaml
from github import Github, GithubException
ICONS = (
"appveyor",
"github",
"azurepipelines",
"circleci",
@@ -77,12 +72,12 @@ class Project:
self.notes = notes
else:
self.num_stars = 0
self.pushed_at = datetime.now(UTC)
self.pushed_at = datetime.utcnow()
name_len = len(self.name) + 4
self.__class__.NAME = max(self.__class__.NAME, name_len)
def __lt__(self, other: Self) -> bool:
def __lt__(self, other: Project) -> bool:
if self.online:
return self.num_stars < other.num_stars
else:
@@ -92,8 +87,8 @@ class Project:
def header(cls) -> str:
return textwrap.dedent(
f"""\
| {"Name":{cls.NAME}} | CI | OS | Notes |
|{"":-^{cls.NAME + 2}}|----|----|:------|"""
| {'Name':{cls.NAME}} | CI | OS | Notes |
|{'':-^{cls.NAME+2 }}|----|----|:------|"""
)
@property
@@ -124,7 +119,7 @@ class Project:
return f"[{self.name}]: {self.url}"
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 -->"
@@ -135,7 +130,6 @@ def fetch_icon(icon_name: str) -> None:
document = xml.dom.minidom.parseString(original_svg_data)
svgElement = document.documentElement
assert svgElement is not None
assert svgElement.nodeName == "svg"
svgElement.setAttribute("width", "16px")
svgElement.setAttribute("fill", "#606060")
@@ -170,9 +164,7 @@ def get_projects(
return sorted((Project(item, github) for item in config), reverse=online)
def render_projects(
projects: Sequence[Project], *, dest_path: Path, include_info: bool = True
) -> str:
def render_projects(projects: Sequence[Project], *, dest_path: Path, include_info: bool = True):
io = StringIO()
print = functools.partial(builtins.print, file=io)
@@ -203,7 +195,7 @@ def insert_projects_table(
projects: Sequence[Project],
input_filename: str,
include_info: bool = True,
) -> None:
):
text = file.read_text()
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} -->"
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)
-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
from __future__ import annotations
import os
import re
import shutil
import subprocess
import sys
import textwrap
import time
import typing
from glob import glob
from pathlib import Path
from subprocess import run
from urllib.parse import quote
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]:
return subprocess.run([cmd], shell=True, check=check, **kwargs) # type: ignore[call-overload, no-any-return]
def git_repo_has_changes() -> bool:
def git_repo_has_changes():
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
return unstaged_changes or staged_changes
def generate_basic_project(path: Path) -> None:
def generate_basic_project(path):
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.generate(path)
@@ -39,32 +38,14 @@ class CIService(typing.NamedTuple):
name: str
dst_config_path: 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 = [
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(
name="azure-pipelines",
dst_config_path="azure-pipelines.yml",
@@ -78,8 +59,7 @@ services = [
CIService(
name="github",
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})",
config_file_transform=github_config_file_transform,
badge_md="[![Build](https://github.com/pypa/cibuildwheel/workflows/Build/badge.svg?branch={branch})](https://github.com/pypa/cibuildwheel/actions)",
),
CIService(
name="travis-ci",
@@ -99,8 +79,9 @@ services = [
]
def ci_service_for_config_file(config_file: Path) -> CIService:
filename = config_file.name
def ci_service_for_config_file(config_file):
filename = Path(config_file).name
try:
return next(s for s in services if filename.startswith(s.name))
except StopIteration:
@@ -117,18 +98,16 @@ def run_example_ci_configs(config_files=None):
"""
if len(config_files) == 0:
config_files = Path("examples").glob("*-minimal.yml")
else:
config_files = [Path(f) for f in config_files]
config_files = glob("examples/*-minimal.yml")
# check each CI service has at most 1 config file
configs_by_service = set()
configs_by_service = {}
for config_file in config_files:
service = ci_service_for_config_file(config_file)
if service.name in configs_by_service:
msg = "You cannot specify more than one config per CI service"
raise Exception(msg)
configs_by_service.add(service.name)
configs_by_service[service.name] = config_file
if git_repo_has_changes():
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:
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.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(src_config_file, dst_config_file)
contents = config_file.read_text(encoding="utf8")
contents = service.config_file_transform(contents)
dst_config_file.write_text(contents, encoding="utf8")
subprocess.run(["git", "add", example_project], check=True)
run(["git", "add", example_project], check=True)
message = textwrap.dedent(
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}
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)
print("---")
@@ -197,5 +174,6 @@ def run_example_ci_configs(config_files=None):
if __name__ == "__main__":
os.chdir(DIR)
os.chdir(os.path.dirname(__file__))
os.chdir("..")
run_example_ci_configs(standalone_mode=True)
+5 -82
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
@@ -8,61 +9,19 @@ import sys
from pathlib import Path
if __name__ == "__main__":
if sys.version_info < (3, 13):
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 = argparse.ArgumentParser()
parser.add_argument(
"--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()
# move cwd to the project root
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
print(
"\n\n================================== UNIT TESTS ==================================",
flush=True,
)
unit_test_args = [sys.executable, "-m", "pytest", "unit_test"]
if (
sys.platform.startswith("linux")
and os.environ.get("CIBW_PLATFORM", "linux") == "linux"
and args.test_select in ["all", "native"]
):
if sys.platform.startswith("linux"):
# run the docker unit tests only on Linux
unit_test_args += ["--run-docker"]
@@ -71,53 +30,17 @@ if __name__ == "__main__":
subprocess.run(unit_test_args, check=True)
print(
"\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 tests
integration_test_args = [
sys.executable,
"-m",
"pytest",
"-m",
f"{' and '.join(['not serial', *marks])}",
f"--numprocesses={args.num_processes}",
"--numprocesses=2",
"-x",
"--durations",
"0",
"--timeout=2400",
"test",
"-vv",
]
if sys.platform.startswith("linux") and args.run_podman:
+2 -1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
@@ -12,7 +13,7 @@ if __name__ == "__main__":
# move cwd to the project root
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")
options = parser.parse_args()
+51 -83
View File
@@ -1,84 +1,70 @@
#!/usr/bin/env python3
from __future__ import annotations
import configparser
import dataclasses
from dataclasses import dataclass
from pathlib import Path
import requests
from packaging.version import Version
DIR = Path(__file__).parent.resolve()
RESOURCES = DIR.parent / "cibuildwheel/resources"
@dataclasses.dataclass(frozen=True)
@dataclass(frozen=True)
class Image:
manylinux_version: str
platforms: list[str]
platform: str
image_name: str
tag: str | None = 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)
tag: str | None # Set this to pin the image
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
PyPAImage(
"manylinux2014",
[
"x86_64",
"i686",
"aarch64",
"ppc64le",
"s390x",
"pypy_x86_64",
"pypy_i686",
"pypy_aarch64",
],
),
Image("manylinux2014", "x86_64", "quay.io/pypa/manylinux2014_x86_64", None),
Image("manylinux2014", "i686", "quay.io/pypa/manylinux2014_i686", None),
Image("manylinux2014", "aarch64", "quay.io/pypa/manylinux2014_aarch64", None),
Image("manylinux2014", "ppc64le", "quay.io/pypa/manylinux2014_ppc64le", None),
Image("manylinux2014", "s390x", "quay.io/pypa/manylinux2014_s390x", None),
Image("manylinux2014", "pypy_x86_64", "quay.io/pypa/manylinux2014_x86_64", None),
Image("manylinux2014", "pypy_i686", "quay.io/pypa/manylinux2014_i686", None),
Image("manylinux2014", "pypy_aarch64", "quay.io/pypa/manylinux2014_aarch64", None),
# manylinux_2_24 images
Image("manylinux_2_24", "x86_64", "quay.io/pypa/manylinux_2_24_x86_64", None),
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
PyPAImage(
"manylinux_2_28",
[
"x86_64",
"i686",
"aarch64",
"ppc64le",
"s390x",
"pypy_x86_64",
"pypy_i686",
"pypy_aarch64",
],
),
# 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"]),
Image("manylinux_2_28", "x86_64", "quay.io/pypa/manylinux_2_28_x86_64", None),
Image("manylinux_2_28", "aarch64", "quay.io/pypa/manylinux_2_28_aarch64", None),
Image("manylinux_2_28", "ppc64le", "quay.io/pypa/manylinux_2_28_ppc64le", None),
Image("manylinux_2_28", "s390x", "quay.io/pypa/manylinux_2_28_s390x", None),
Image("manylinux_2_28", "pypy_x86_64", "quay.io/pypa/manylinux_2_28_x86_64", None),
Image("manylinux_2_28", "pypy_aarch64", "quay.io/pypa/manylinux_2_28_aarch64", None),
# musllinux_1_1 images
Image("musllinux_1_1", "x86_64", "quay.io/pypa/musllinux_1_1_x86_64", None),
Image("musllinux_1_1", "i686", "quay.io/pypa/musllinux_1_1_i686", None),
Image("musllinux_1_1", "aarch64", "quay.io/pypa/musllinux_1_1_aarch64", None),
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),
# musllinux_1_2 images
PyPAImage(
"musllinux_1_2", ["x86_64", "i686", "aarch64", "ppc64le", "s390x", "armv7l", "riscv64"]
),
Image("musllinux_1_2", "x86_64", "quay.io/pypa/musllinux_1_2_x86_64", None),
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()
@@ -104,21 +90,6 @@ for image in images:
for (name, info) in tags_dict.items()
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:
response = requests.get(f"https://hub.docker.com/v2/repositories/{image.image_name}/tags")
response.raise_for_status()
@@ -134,13 +105,10 @@ for image in images:
)
tag_name = pinned_tag["name"]
for platform in image.platforms:
if not config.has_section(platform):
config[platform] = {}
suffix = ""
if image.use_platform_suffix:
suffix = f"_{platform.removeprefix('pypy_')}"
config[platform][image.manylinux_version] = f"{image.image_name}{suffix}:{tag_name}"
if not config.has_section(image.platform):
config[image.platform] = {}
config[image.platform][image.manylinux_version] = f"{image.image_name}:{tag_name}"
with open(RESOURCES / "pinned_docker_images.cfg", "w") as f:
config.write(f)
+4 -2
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
from __future__ import annotations
import subprocess
import sys
@@ -17,7 +18,7 @@ except ImportError:
)
def main() -> None:
def main():
subprocess.run(["mkdocs", "build"], check=True)
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.unlink(missing_ok=True)
if dest_path.exists():
dest_path.unlink()
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
from __future__ import annotations
import copy
import difflib
import logging
import operator
import re
import tomllib
from collections.abc import Mapping, MutableMapping
from pathlib import Path
from typing import Any, Final, Literal, TypedDict
from xml.etree import ElementTree as ET
from typing import Any, Final, Literal, TypedDict, Union
import click
import requests
@@ -19,8 +17,8 @@ from packaging.version import Version
from rich.logging import RichHandler
from rich.syntax import Syntax
from cibuildwheel.extra import dump_python_configurations, get_pyodide_xbuildenv_info
from cibuildwheel.platforms.android import android_triplet
from cibuildwheel._compat import tomllib
from cibuildwheel.extra import dump_python_configurations
log = logging.getLogger("cibw")
@@ -33,18 +31,26 @@ RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"
ArchStr = Literal["32", "64", "ARM64"]
class Config(TypedDict):
class ConfigWinCP(TypedDict):
identifier: str
version: str
arch: str
class ConfigUrl(Config):
class ConfigWinPP(TypedDict):
identifier: str
version: str
arch: str
url: str
class ConfigPyodide(Config):
default_pyodide_version: str
node_version: str
class ConfigMacOS(TypedDict):
identifier: str
version: str
url: str
AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigMacOS]
# The following set of "Versions" classes allow the initial call to the APIs to
@@ -52,7 +58,7 @@ class ConfigPyodide(Config):
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.raise_for_status()
api_info = response.json()
@@ -66,11 +72,7 @@ class WindowsVersions:
self.arch_str = arch_str
self.arch = ARCH_DICT[arch_str]
self.free_threaded = free_threaded
package = PACKAGE_DICT[arch_str]
if free_threaded:
package = f"{package}-freethreaded"
response = requests.get(f"{endpoint}{package}/index.json")
response.raise_for_status()
@@ -78,7 +80,7 @@ class WindowsVersions:
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,
# unless there are only pre-releases, then it selects pre-releases
# instead (like pip)
@@ -90,82 +92,12 @@ class WindowsVersions:
if not versions:
return None
flags = "t" if self.free_threaded else ""
version = versions[0]
identifier = f"cp{version.major}{version.minor}{flags}-{self.arch}"
return Config(
identifier = f"cp{version.major}{version.minor}-{self.arch}"
return ConfigWinCP(
identifier=identifier,
version=self.version_dict[version],
)
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,
arch=self.arch_str,
)
@@ -194,9 +126,9 @@ class PyPyVersions:
]
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 = 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)]
if not releases:
@@ -210,19 +142,20 @@ class PyPyVersions:
identifier = f"pp{version.major}{version.minor}-{version_arch}"
url = self.get_arch_file(release)
return ConfigUrl(
return ConfigWinPP(
identifier=identifier,
version=f"{version.major}.{version.minor}",
arch=self.arch,
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"}:
msg = f"'{self.arch}' arch not supported yet on macOS"
raise RuntimeError(msg)
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:
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
)
return ConfigUrl(
return ConfigMacOS(
identifier=identifier,
version=f"{version.major}.{version.minor}",
url=url,
@@ -258,21 +191,22 @@ class CPythonVersions:
self.versions_dict: dict[Version, int] = {}
for release in releases_info:
# Skip the pymanager releases
if not release["slug"].startswith("python"):
continue
# Removing the prefix
version = Version(release["name"].removeprefix("Python "))
# Removing the prefix, Python 3.9 would use: release["name"].removeprefix("Python ")
version = Version(release["name"][7:])
uri = int(release["resource_uri"].rstrip("/").split("/")[-1])
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
unsorted_versions = spec.filter(self.versions_dict)
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:
# Find the first patch version that contains the requested file
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"]]
if urls:
return ConfigUrl(
return ConfigMacOS(
identifier=identifier,
version=f"{new_version.major}.{new_version.minor}",
url=urls[0],
@@ -292,120 +226,6 @@ class CPythonVersions:
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
# identifier, it updates a config dict.
@@ -413,77 +233,48 @@ class PyodideVersions:
class AllVersions:
def __init__(self) -> None:
self.windows_32 = WindowsVersions("32", False)
self.windows_t_32 = WindowsVersions("32", True)
self.windows_64 = WindowsVersions("64", False)
self.windows_t_64 = WindowsVersions("64", True)
self.windows_arm64 = WindowsVersions("ARM64", False)
self.windows_t_arm64 = WindowsVersions("ARM64", True)
self.windows_32 = WindowsVersions("32")
self.windows_64 = WindowsVersions("64")
self.windows_arm64 = WindowsVersions("ARM64")
self.windows_pypy_64 = PyPyVersions("64")
self.cpython = CPythonVersions()
self.macos_cpython = CPythonVersions()
self.macos_pypy = PyPyVersions("64")
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:
identifier = config["identifier"]
version = Version(config["version"])
spec = Specifier(f"=={version.major}.{version.minor}.*")
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)
if "macosx" in identifier:
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"):
if "macosx_x86_64" in identifier:
config_update = self.macos_pypy.update_version_macos(spec)
elif "macosx_arm64" in identifier:
config_update = self.macos_pypy_arm64.update_version_macos(spec)
elif identifier.startswith("gp"):
config_update = self.graalpy.update_version(identifier, spec)
elif "t-win32" in identifier and identifier.startswith("cp"):
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 "win32" in identifier:
if identifier.startswith("cp"):
config_update = self.windows_32.update_version_windows(spec)
elif "win_amd64" in identifier:
if identifier.startswith("cp"):
config_update = self.windows_64.update_version_windows(spec)
elif identifier.startswith("pp"):
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"):
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!"
if config_update != config:
log.info(" Updated %s to %s", config, config_update)
config.clear()
config.update(**config_update)
config.update(**config_update)
if config != orig_config:
log.info(" Updated %s to %s", orig_config, config)
@click.command()
@@ -507,9 +298,11 @@ def update_pythons(force: bool, level: str) -> None:
with toml_file_path.open("rb") as f:
configs = tomllib.load(f)
for platform in ["windows", "macos", "android", "ios", "pyodide"]:
for config in configs[platform]["python_configurations"]:
all_versions.update_config(config)
for config in configs["windows"]["python_configurations"]:
all_versions.update_config(config)
for config in configs["macos"]["python_configurations"]:
all_versions.update_config(config)
result_toml = 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
from __future__ import annotations
import dataclasses
import difflib
import logging
import tomllib
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Final
import click
import rich
from packaging.version import Version
from packaging.version import InvalidVersion, Version
from rich.logging import RichHandler
from rich.syntax import Syntax
from cibuildwheel.extra import github_api_request
from cibuildwheel._compat import tomllib
log = logging.getLogger("cibw")
@@ -24,33 +25,43 @@ DIR: Final[Path] = Path(__file__).parent.parent.resolve()
RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"
GET_VIRTUALENV_GITHUB: Final[str] = "https://github.com/pypa/get-virtualenv"
GET_VIRTUALENV_URL_TEMPLATE: Final[str] = (
f"{GET_VIRTUALENV_GITHUB}/blob/{{version}}/public/virtualenv.pyz?raw=true"
)
GET_VIRTUALENV_URL_TEMPLATE: Final[
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:
name: str
download_url: str
version: Version
version_string: str
def get_latest_virtualenv_release() -> VersionTuple:
response = github_api_request("repos/pypa/get-virtualenv/releases/latest")
tag_name = response["tag_name"]
asset = next(
(asset for asset in response["assets"] if asset["name"] == "virtualenv.pyz"),
None,
)
if not asset:
msg = "No asset named 'virtualenv.pyz' found in the latest release of get-virtualenv."
raise RuntimeError(msg)
return VersionTuple(
version=Version(tag_name), name=tag_name, download_url=asset["browser_download_url"]
)
def git_ls_remote_versions(url) -> list[VersionTuple]:
versions: list[VersionTuple] = []
tags = subprocess.run(
["git", "ls-remote", "--tags", url], check=True, text=True, capture_output=True
).stdout.splitlines()
for tag in tags:
_, ref = tag.split()
assert ref.startswith("refs/tags/")
version_string = ref[10:]
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
# 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()
@@ -71,26 +82,15 @@ def update_virtualenv(force: bool, level: str) -> None:
original_toml = toml_file_path.read_text()
with toml_file_path.open("rb") as f:
configurations = tomllib.load(f)
default = configurations.pop("default")
local_version = str(default["version"])
loaded_file = tomllib.load(f)
version = str(loaded_file["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()
if latest_release.version > Version(local_version):
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()
result_toml = (
f'version = "{version}"\n'
f'url = "{GET_VIRTUALENV_URL_TEMPLATE.format(version=version)}"\n'
)
rich.print() # spacer
+3 -1
View File
@@ -1 +1,3 @@
__version__ = "3.3.1"
from __future__ import annotations
__version__ = "2.16.5"
+138 -297
View File
@@ -1,84 +1,39 @@
from __future__ import annotations
import argparse
import contextlib
import dataclasses
import os
import shutil
import sys
import tarfile
import textwrap
import traceback
import typing
from collections.abc import Generator, Iterable, Sequence
from collections.abc import Iterable, Sequence, Set
from pathlib import Path
from tempfile import mkdtemp
from typing import Any, Literal, TextIO
from typing import Protocol
import cibuildwheel
import cibuildwheel.linux
import cibuildwheel.macos
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.ci import CIProvider, detect_ci_provider, fix_ansi_codes_for_github_actions
from cibuildwheel.logger import log
from cibuildwheel.options import CommandLineArguments, Options, compute_options
from cibuildwheel.platforms import ALL_PLATFORM_MODULES, get_build_identifiers, native_platform
from cibuildwheel.selector import BuildSelector, EnableGroup, selector_matches
from cibuildwheel.typing import PLATFORMS, PlatformName
from cibuildwheel.util.file import CIBW_CACHE_PATH, ensure_cache_sentinel
from cibuildwheel.util.helpers import strtobool
from cibuildwheel.util.resources import read_all_configs
@dataclasses.dataclass
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)
from cibuildwheel.typing import PLATFORMS, GenericPythonConfiguration, PlatformName
from cibuildwheel.util import (
CIBW_CACHE_PATH,
BuildSelector,
CIProvider,
Unbuffered,
chdir,
detect_ci_provider,
fix_ansi_codes_for_github_actions,
)
def main() -> None:
global_options = GlobalOptions()
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.
"""
parser = argparse.ArgumentParser(
description="Build wheels for all the platforms.",
epilog="""
@@ -86,19 +41,18 @@ def main_inner(global_options: GlobalOptions) -> None:
--config-file (pyproject.toml usually). See
https://github.com/pypa/cibuildwheel#options for info.
""",
allow_abbrev=False,
)
parser.add_argument(
"--platform",
choices=["auto", "linux", "macos", "windows", "pyodide", "android", "ios"],
choices=["auto", "linux", "macos", "windows"],
default=None,
help="""
Platform to build for. Use this option to override the auto-detected
platform. Specifying "macos" or "windows" only works on that
operating system. "linux" works on any desktop OS, as long as
Docker/Podman is installed. "pyodide" and "android" only work on
Linux and macOS. "ios" only works on macOS. Default: auto.
Platform to build for. Use this option to override the
auto-detected platform or to run cibuildwheel on your development
machine. Specifying "macos" or "windows" only works on that
operating system, but "linux" works on all three, as long as
Docker/Podman is installed. Default: auto.
""",
)
@@ -116,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(
"--only",
default=None,
choices=[v["identifier"] for vv in read_all_configs().values() for v in vv],
metavar="IDENTIFIER",
help="""
Force a single wheel build when given an identifier. Overrides
CIBW_BUILD/CIBW_SKIP. --platform and --arch cannot be specified
@@ -159,7 +100,7 @@ def main_inner(global_options: GlobalOptions) -> None:
parser.add_argument(
"package_dir",
metavar="PACKAGE",
default=Path(),
default=Path("."),
type=Path,
nargs="?",
help="""
@@ -179,12 +120,6 @@ def main_inner(global_options: GlobalOptions) -> None:
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(
"--allow-empty",
action="store_true",
@@ -192,58 +127,13 @@ def main_inner(global_options: GlobalOptions) -> None:
)
parser.add_argument(
"--debug-traceback",
"--prerelease-pythons",
action="store_true",
default=strtobool(os.environ.get("CIBW_DEBUG_TRACEBACK", "0")),
help="Print a full traceback for all errors",
help="Enable pre-release Python versions if available.",
)
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()
# This are always relative to the base directory, even in SDist builds
@@ -270,7 +160,7 @@ def main_inner(global_options: GlobalOptions) -> None:
# This is now the new package dir
args.package_dir = project_dir.resolve()
with contextlib.chdir(project_dir):
with chdir(project_dir):
build_in_directory(args)
finally:
# avoid https://github.com/python/cpython/issues/86962 by performing
@@ -287,44 +177,95 @@ def _compute_platform_only(only: str) -> PlatformName:
return "macos"
if "win_" in only or "win32" in only:
return "windows"
if "pyodide_" in only:
return "pyodide"
if "android_" in only:
return "android"
if "ios_" in only:
return "ios"
msg = f"Invalid --only='{only}', must be a build selector with a known platform"
raise errors.ConfigurationError(msg)
print(
f"Invalid --only='{only}', must be a build selector with a known platform",
file=sys.stderr,
)
sys.exit(2)
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:
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:
msg = "--platform cannot be specified with --only, it is computed from --only"
raise errors.ConfigurationError(msg)
print(
"--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:
msg = "--arch cannot be specified with --only, it is computed from --only"
raise errors.ConfigurationError(msg)
print(
"--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"}:
msg = f"Unsupported platform: {platform_option_value}"
raise errors.ConfigurationError(msg)
print(f"cibuildwheel: Unsupported platform: {platform_option_value}", file=sys.stderr)
sys.exit(2)
if args.only:
return _compute_platform_only(args.only)
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:
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)
package_dir = options.globals.package_dir
@@ -332,10 +273,11 @@ def build_in_directory(args: CommandLineArguments) -> None:
if not any(package_dir.joinpath(name).exists() for name in package_files):
names = ", ".join(sorted(package_files, reverse=True))
msg = f"Could not find any of {{{names}}} at root of package"
raise errors.ConfigurationError(msg)
msg = f"cibuildwheel: Could not find any of {{{names}}} at root of package"
print(msg, file=sys.stderr)
sys.exit(2)
platform_module = ALL_PLATFORM_MODULES[platform]
platform_module = get_platform_module(platform)
identifiers = get_build_identifiers(
platform_module=platform_module,
build_selector=options.globals.build_selector,
@@ -350,14 +292,11 @@ def build_in_directory(args: CommandLineArguments) -> None:
# Add CIBUILDWHEEL environment variable
os.environ["CIBUILDWHEEL"] = "1"
# Python is buffering by default when running on the CI platforms, giving
# problems interleaving subprocess call output with unflushed calls to
# 'print'
sys.stdout = Unbuffered(sys.stdout)
# Python is buffering by default when running on the CI platforms, giving problems interleaving subprocess call output with unflushed calls to 'print'
sys.stdout = Unbuffered(sys.stdout) # type: ignore[assignment]
# create the cache dir before it gets printed & builds performed
CIBW_CACHE_PATH.mkdir(parents=True, exist_ok=True)
ensure_cache_sentinel(CIBW_CACHE_PATH)
print_preamble(platform=platform, options=options, identifiers=identifiers)
@@ -365,22 +304,27 @@ def build_in_directory(args: CommandLineArguments) -> None:
options.check_for_invalid_configuration(identifiers)
allowed_architectures_check(platform, options.globals.architectures)
except ValueError as err:
raise errors.DeprecationError(*err.args) from err
print("cibuildwheel:", *err.args, file=sys.stderr)
sys.exit(4)
if not identifiers:
message = f"No build identifiers selected: {options.globals.build_selector}"
if options.globals.allow_empty:
print(f"cibuildwheel: {message}", file=sys.stderr)
else:
raise errors.NothingToDoError(message)
print(
f"cibuildwheel: No build identifiers selected: {options.globals.build_selector}",
file=sys.stderr,
)
if not args.allow_empty:
sys.exit(3)
output_dir = options.globals.output_dir
output_dir.mkdir(parents=True, exist_ok=True)
if not output_dir.exists():
output_dir.mkdir(parents=True)
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
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)
finally:
# avoid https://github.com/python/cpython/issues/86962 by performing
@@ -413,139 +357,36 @@ def print_preamble(platform: str, options: Options, identifiers: Sequence[str])
print()
print(f"Cache folder: {CIBW_CACHE_PATH}")
print()
warnings = detect_warnings(options=options)
for warning in warnings:
log.warning(warning)
warnings = detect_warnings(options=options, identifiers=identifiers)
if warnings:
print("\nWarnings:")
for warning in warnings:
print(" " + warning)
error_list = list(detect_errors(options=options, identifiers=identifiers))
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")
print("\nHere we go!\n")
def detect_errors(*, options: Options, identifiers: Iterable[str]) -> Generator[str, None, None]:
# Check for deprecated CIBW_FREE_THREADED_SUPPORT environment variable
if "CIBW_FREE_THREADED_SUPPORT" in os.environ:
yield (
"CIBW_FREE_THREADED_SUPPORT environment variable is no longer supported. "
'Use tool.cibuildwheel.enable = ["cpython-freethreading"] in pyproject.toml '
"or set CIBW_ENABLE=cpython-freethreading instead."
)
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]
# 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"]:
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):
# Reminder: in an f-string, double braces means literal single brace
yield (
f"{option_name}: '{{python}}' and '{{pip}}' are no longer supported "
"and have been removed in cibuildwheel 3. Simply use 'python' or 'pip' instead."
msg = (
f"{option_name}: '{{python}}' and '{{pip}}' are no longer needed, "
"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
if EnableGroup.CPythonExperimentalRiscV64 in build_selector.enable:
yield (
"'cpython-experimental-riscv64' enable is deprecated and will be removed in a future version. "
"It should be removed from tool.cibuildwheel.enable in pyproject.toml "
"or CIBW_ENABLE environment variable."
)
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)
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 re
import shutil
import subprocess
import sys
import typing
from collections.abc import Set
from enum import StrEnum, auto
from enum import Enum
from typing import Final, Literal
from cibuildwheel import errors
from ._compat.typing import assert_never
from .typing import PlatformName
PRETTY_NAMES: Final[dict[PlatformName, str]] = {
"linux": "Linux",
"macos": "macOS",
"windows": "Windows",
"pyodide": "Pyodide",
"android": "Android",
"ios": "iOS",
}
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": "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:
for arch_synonym_ in ARCH_SYNONYMS:
if arch == arch_synonym_.get(from_platform):
return arch_synonym_.get(to_platform, arch)
@functools.total_ordering
class Architecture(Enum):
value: str
return arch
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()
# mac/linux archs
x86_64 = "x86_64"
# linux archs
i686 = auto()
aarch64 = auto()
ppc64le = auto()
s390x = auto()
armv7l = auto()
riscv64 = auto()
i686 = "i686"
aarch64 = "aarch64"
ppc64le = "ppc64le"
s390x = "s390x"
# mac archs
universal2 = auto()
arm64 = auto()
universal2 = "universal2"
arm64 = "arm64"
# windows archs
x86 = auto()
x86 = "x86"
AMD64 = "AMD64"
ARM64 = "ARM64"
# WebAssembly
wasm32 = auto()
# Allow this to be sorted
def __lt__(self, other: Architecture) -> bool:
return self.value < other.value
# android archs
arm64_v8a = auto()
# iOS "multiarch" architectures that include both
# the CPU architecture and the ABI.
arm64_iphoneos = auto()
arm64_iphonesimulator = auto()
x86_64_iphonesimulator = auto()
def __str__(self) -> str:
return self.name
@staticmethod
def parse_config(config: str, platform: PlatformName) -> "set[Architecture]":
def parse_config(config: str, platform: PlatformName) -> set[Architecture]:
result = set()
for arch_str in re.split(r"[\s,]+", config):
match arch_str:
case "auto":
result |= Architecture.auto_archs(platform=platform)
case "native":
if native_arch := Architecture.native_arch(platform=platform):
result.add(native_arch)
case "all":
result |= Architecture.all_archs(platform=platform)
case "auto64":
result |= Architecture.bitness_archs(platform=platform, bitness="64")
case "auto32":
result |= Architecture.bitness_archs(platform=platform, bitness="32")
case _:
try:
result.add(Architecture(arch_str))
except ValueError as e:
msg = f"Invalid architecture '{arch_str}'"
raise errors.ConfigurationError(msg) from e
if arch_str == "auto":
result |= Architecture.auto_archs(platform=platform)
elif arch_str == "native":
result.add(Architecture(platform_module.machine()))
elif arch_str == "all":
result |= Architecture.all_archs(platform=platform)
elif arch_str == "auto64":
result |= Architecture.bitness_archs(platform=platform, bitness="64")
elif arch_str == "auto32":
result |= Architecture.bitness_archs(platform=platform, bitness="32")
else:
result.add(Architecture(arch_str))
return result
@staticmethod
def native_arch(platform: PlatformName) -> "Architecture | None":
def auto_archs(platform: PlatformName) -> set[Architecture]:
native_machine = platform_module.machine()
native_architecture = Architecture(native_machine)
# Cross-platform support. Used for --print-build-identifiers or docker builds.
host_platform: PlatformName = (
@@ -119,49 +82,34 @@ class Architecture(StrEnum):
else ("macos" if sys.platform.startswith("darwin") else "linux")
)
if platform == "pyodide":
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
native_architecture = Architecture(native_machine)
# 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
if host_platform != platform:
synonym = arch_synonym(native_machine, host_platform, platform)
if synonym is None:
# can't build anything on this platform
return None
for arch_synonym in ARCH_SYNONYMS:
if native_machine == arch_synonym.get(host_platform):
synonym = arch_synonym[platform]
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
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}
result = {native_architecture}
match platform:
case "windows" if Architecture.AMD64 in result:
result.add(Architecture.x86)
case "ios" if native_arch == Architecture.arm64_iphonesimulator:
# Also build the device wheel if we're on ARM64.
result.add(Architecture.arm64_iphoneos)
if platform == "linux" and Architecture.x86_64 in result:
# x86_64 machines can run i686 containers
result.add(Architecture.i686)
if platform == "windows" and Architecture.AMD64 in result:
result.add(Architecture.x86)
return result
@staticmethod
def all_archs(platform: PlatformName) -> "set[Architecture]":
def all_archs(platform: PlatformName) -> set[Architecture]:
all_archs_map = {
"linux": {
Architecture.x86_64,
@@ -169,52 +117,24 @@ class Architecture(StrEnum):
Architecture.aarch64,
Architecture.ppc64le,
Architecture.s390x,
Architecture.armv7l,
Architecture.riscv64,
},
"macos": {Architecture.x86_64, Architecture.arm64, Architecture.universal2},
"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]
@staticmethod
def bitness_archs(platform: PlatformName, bitness: Literal["64", "32"]) -> "set[Architecture]":
# This map maps 64-bit architectures to their 32-bit equivalents.
archs_map = {
Architecture.x86_64: Architecture.i686,
Architecture.AMD64: Architecture.x86,
Architecture.aarch64: Architecture.armv7l,
}
native_arch = Architecture.native_arch(platform)
# pylint: disable-next=inconsistent-return-statements
def bitness_archs(platform: PlatformName, bitness: Literal["64", "32"]) -> set[Architecture]:
archs_32 = {Architecture.i686, Architecture.x86}
auto_archs = Architecture.auto_archs(platform)
if native_arch is None:
return set() # can't build anything on this platform
if native_arch == Architecture.wasm32:
return {native_arch} if bitness == "32" else set()
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)
if bitness == "64":
return auto_archs - archs_32
elif bitness == "32":
return auto_archs & archs_32
else:
assert_never(bitness)
def allowed_architectures_check(
+7 -9
View File
@@ -1,23 +1,21 @@
import dataclasses
from __future__ import annotations
import subprocess
from collections.abc import (
Callable,
Iterable,
Mapping,
Sequence,
)
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass
from typing import Callable, Dict, List # noqa: TID251
import bashlex
# 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:
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:
environment: dict[str, 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
from collections.abc import Mapping, Sequence
from typing import Any, Protocol
@@ -73,16 +75,12 @@ class EnvironmentAssignmentRaw:
return self.value
@dataclasses.dataclass
class EnvironmentAssignmentBash:
"""
An environment variable, in bash syntax. The value can use bash constructs
like "$OTHER_VAR" and "$(command arg1 arg2)".
"""
name: str
value: str
def __init__(self, assignment: str):
name, equals, value = assignment.partition("=")
if not equals:
@@ -100,8 +98,13 @@ class EnvironmentAssignmentBash:
def __repr__(self) -> str:
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:
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.
"""
import json
import time
import typing
import urllib.error
import urllib.request
from __future__ import annotations
from collections.abc import Mapping, Sequence
from io import StringIO
from typing import Any, NotRequired, Protocol
from cibuildwheel import __version__ as cibw_version
from typing import Protocol
__all__ = ("Printable", "dump_python_configurations")
class Printable(Protocol):
def __str__(self) -> str: ...
def __str__(self) -> str:
...
def dump_python_configurations(
@@ -37,65 +33,3 @@ def dump_python_configurations(
output.write("\n")
# Strip the final newline, to avoid two blank lines at the end.
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
import dataclasses
from __future__ import annotations
import subprocess
import sys
import textwrap
from collections import OrderedDict
from collections.abc import Iterable, Iterator, Sequence, Set
from dataclasses import dataclass
from pathlib import Path, PurePath, PurePosixPath
from typing import TYPE_CHECKING, assert_never
from typing import OrderedDict, Tuple
from .. import errors
from ..architecture import Architecture
from ..frontend import get_build_frontend_extra_flags
from ..logger import log
from ..oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform
from ..options import BuildOptions, Options
from ..selector import BuildSelector
from ..util import resources
from ..util.file import copy_test_sources
from ..util.helpers import prepare_command, unwrap
from ..util.packaging import find_compatible_wheel
if TYPE_CHECKING:
from ..typing import PathOrStr
ARCHITECTURE_OCI_PLATFORM_MAP = {
Architecture.x86_64: OCIPlatform.AMD64,
Architecture.i686: OCIPlatform.i386,
Architecture.aarch64: OCIPlatform.ARM64,
Architecture.ppc64le: OCIPlatform.PPC64LE,
Architecture.s390x: OCIPlatform.S390X,
Architecture.armv7l: OCIPlatform.ARMV7,
Architecture.riscv64: OCIPlatform.RISCV64,
}
from ._compat.typing import assert_never
from .architecture import Architecture
from .logger import log
from .oci_container import OCIContainer
from .options import Options
from .typing import PathOrStr
from .util import (
AlreadyBuiltWheelError,
BuildFrontendConfig,
BuildSelector,
NonPlatformWheelError,
find_compatible_wheel,
get_build_verbosity_extra_flags,
prepare_command,
read_python_configs,
split_config_settings,
test_fail_cwd_file,
unwrap,
)
@dataclasses.dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True)
class PythonConfiguration:
version: str
identifier: str
@@ -45,24 +40,20 @@ class PythonConfiguration:
return PurePosixPath(self.path_str)
@dataclasses.dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True)
class BuildStep:
platform_configs: list[PythonConfiguration]
platform_tag: str
container_engine: OCIContainerEngineConfig
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(
build_selector: BuildSelector,
architectures: Set[Architecture],
) -> 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,
# and match the build/skip rules
@@ -74,9 +65,8 @@ def get_python_configurations(
]
def container_image_for_python_configuration(
config: PythonConfiguration, build_options: BuildOptions
) -> str:
def container_image_for_python_configuration(config: PythonConfiguration, options: Options) -> str:
build_options = options.build_options(config.identifier)
# e.g
# identifier is 'cp310-manylinux_x86_64'
# platform_tag is 'manylinux_x86_64'
@@ -101,18 +91,15 @@ def get_build_steps(
Groups PythonConfigurations into BuildSteps. Each BuildStep represents a
separate container instance.
"""
steps = OrderedDict[tuple[str, str, str, OCIContainerEngineConfig], BuildStep]()
steps = OrderedDict[Tuple[str, str, str], BuildStep]()
for config in python_configurations:
_, 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
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)
step_key = (platform_tag, container_image, before_all)
if step_key in steps:
steps[step_key].platform_configs.append(config)
@@ -120,7 +107,6 @@ def get_build_steps(
steps[step_key] = BuildStep(
platform_configs=[config],
platform_tag=platform_tag,
container_engine=container_engine,
container_image=container_image,
)
@@ -131,36 +117,33 @@ def check_all_python_exist(
*, platform_configs: Iterable[PythonConfiguration], container: OCIContainer
) -> None:
exist = True
has_manylinux_interpreters = False
has_manylinux_interpreters = True
messages = []
with contextlib.suppress(subprocess.CalledProcessError):
try:
# use capture_output to keep quiet
container.call(["manylinux-interpreters", "--help"], capture_output=True)
has_manylinux_interpreters = True
except subprocess.CalledProcessError:
has_manylinux_interpreters = False
for config in platform_configs:
python_path = config.path / "bin" / "python"
if has_manylinux_interpreters:
try:
try:
if has_manylinux_interpreters:
container.call(["manylinux-interpreters", "ensure", config.path.name])
except subprocess.CalledProcessError:
messages.append(
f" 'manylinux-interpreters ensure {config.path.name}' needed to build '{config.identifier}' failed in container running image '{container.image}'."
" Either the installation failed or this interpreter is not available in that image. Please check the logs."
)
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
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:
message = "\n".join(messages)
raise errors.FatalError(message)
print(
f"cibuildwheel:\n{message}",
file=sys.stderr,
)
sys.exit(1)
def build_in_container(
@@ -170,7 +153,6 @@ def build_in_container(
container: OCIContainer,
container_project_path: PurePath,
container_package_dir: PurePath,
local_tmp_dir: Path,
) -> None:
container_output_dir = PurePosixPath("/output")
@@ -186,7 +168,7 @@ def build_in_container(
log.step("Running before_all...")
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_ROOT_USER_ACTION"] = "ignore"
env = before_all_options.environment.as_dictionary(
@@ -204,50 +186,48 @@ def build_in_container(
for config in platform_configs:
log.build_start(config.identifier)
local_identifier_tmp_dir = local_tmp_dir / config.identifier
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend
use_uv = build_frontend.name == "build[uv]"
pip = ["uv", "pip"] if use_uv else ["pip"]
log.step("Setting up build environment...")
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
dependency_constraint_flags: list[PathOrStr] = []
local_constraints_file = build_options.dependency_constraints.get_for_python_version(
version=config.version,
tmp_dir=local_identifier_tmp_dir,
)
if local_constraints_file:
if build_options.dependency_constraints:
constraints_file = build_options.dependency_constraints.get_for_python_version(
config.version
)
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]
log.step("Setting up build environment...")
env = container.get_environment()
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
env["PIP_ROOT_USER_ACTION"] = "ignore"
# put this config's python top of the list
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)
# check config python is still on PATH
which_python = container.call(["which", "python"], env=env, capture_output=True).strip()
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."
raise errors.FatalError(msg)
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)
if use_uv:
which_uv = container.call(["which", "uv"], env=env, capture_output=True).strip()
if not which_uv:
msg = "uv not found on PATH. You must use a supported manylinux or musllinux environment with uv."
raise errors.FatalError(msg)
else:
which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip()
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)
which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip()
if PurePosixPath(which_pip) != python_bin / "pip":
print(
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.",
file=sys.stderr,
)
sys.exit(1)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
@@ -255,7 +235,7 @@ def build_in_container(
print(
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:
if build_options.before_build:
log.step("Running before_build...")
@@ -273,42 +253,42 @@ def build_in_container(
container.call(["rm", "-rf", built_wheel_dir])
container.call(["mkdir", "-p", built_wheel_dir])
extra_flags = get_build_frontend_extra_flags(
build_frontend, build_options.build_verbosity, build_options.config_settings
)
extra_flags = split_config_settings(build_options.config_settings, build_frontend.name)
extra_flags += build_frontend.args
match build_frontend.name:
case "pip":
container.call(
[
"python",
"-m",
"pip",
"wheel",
container_package_dir,
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*extra_flags,
],
env=env,
)
case "build" | "build[uv]":
if use_uv and "--no-isolation" not in extra_flags and "-n" not in extra_flags:
extra_flags += ["--installer=uv"]
container.call(
[
"python",
"-m",
"build",
container_package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
],
env=env,
)
case _:
assert_never(build_frontend)
if build_frontend.name == "pip":
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
container.call(
[
"python",
"-m",
"pip",
"wheel",
container_package_dir,
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*extra_flags,
],
env=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)
container.call(
[
"python",
"-m",
"build",
container_package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
],
env=env,
)
else:
assert_never(build_frontend)
built_wheel = container.glob(built_wheel_dir, "*.whl")[0]
@@ -317,62 +297,39 @@ def build_in_container(
container.call(["mkdir", "-p", repaired_wheel_dir])
if built_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError()
raise 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=container_package_dir,
project=container_project_path,
build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
)
container.call(["sh", "-c", repair_command_prepared], env=env)
else:
container.call(["mv", built_wheel, repaired_wheel_dir])
match 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])
repaired_wheels = container.glob(repaired_wheel_dir, "*.whl")
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
for repaired_wheel in repaired_wheels:
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
raise AlreadyBuiltWheelError(repaired_wheel.name)
if build_options.test_command and build_options.test_selector(config.identifier):
log.step("Testing wheel...")
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
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(
container.call(["mktemp", "-d"], capture_output=True).strip()
)
venv_dir = testing_temp_dir / "venv"
if use_uv:
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)
container.call(["python", "-m", "virtualenv", "--no-download", venv_dir], env=env)
virtualenv_env = env.copy()
virtualenv_env["PATH"] = f"{venv_dir / 'bin'}:{virtualenv_env['PATH']}"
virtualenv_env["VIRTUAL_ENV"] = str(venv_dir)
virtualenv_env = build_options.test_environment.as_dictionary(
prev_environment=virtualenv_env
)
if build_options.before_test:
before_test_prepared = prepare_command(
@@ -383,52 +340,46 @@ def build_in_container(
container.call(["sh", "-c", before_test_prepared], env=virtualenv_env)
# Install the wheel we just built
# Note: If auditwheel produced two wheels, it's because the earlier produced wheel
# 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(
[*pip, "install", str(repaired_wheel) + build_options.test_extras],
["pip", "install", str(wheel_to_test) + build_options.test_extras],
env=virtualenv_env,
)
# Install any requirements to run the tests
if build_options.test_requires:
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
test_command_prepared = prepare_command(
build_options.test_command,
project=container_project_path,
package=container_package_dir,
wheel=repaired_wheel,
wheel=wheel_to_test,
)
test_cwd = testing_temp_dir / "test_cwd"
container.call(["mkdir", "-p", test_cwd])
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.copy_into(test_fail_cwd_file, test_cwd / "test_fail.py")
container.call(["sh", "-c", test_command_prepared], cwd=test_cwd, env=virtualenv_env)
# clean up test environment
container.call(["rm", "-rf", testing_temp_dir])
# move repaired wheel to output
output_wheel: Path | None = None
# move repaired wheels to output
if compatible_wheel is None:
container.call(["mkdir", "-p", container_output_dir])
container.call(["mv", repaired_wheel, container_output_dir])
built_wheels.append(container_output_dir / repaired_wheel.name)
output_wheel = options.globals.output_dir / repaired_wheel.name
container.call(["mv", *repaired_wheels, container_output_dir])
built_wheels.extend(
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...")
# copy the output back into the host
@@ -436,7 +387,30 @@ def build_in_container(
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(
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)
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:
ids_to_build = [x.identifier for x in build_step.platform_configs]
log.step(f"Starting container image {build_step.container_image}...")
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(
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,
engine=build_step.container_engine,
engine=options.globals.container_engine,
) as container:
build_in_container(
options=options,
@@ -490,13 +443,14 @@ def build(options: Options, tmp_path: Path) -> None:
container=container,
container_project_path=container_project_path,
container_package_dir=container_package_dir,
local_tmp_dir=tmp_path,
)
except subprocess.CalledProcessError as error:
log.step_end_with_error(
f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}"
)
troubleshoot(options, error)
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error
sys.exit(1)
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?
):
# the wheel build step or the repair step failed
print("Checking for common errors...")
so_files = list(options.globals.package_dir.glob("**/*.so"))
if so_files:
+58 -209
View File
@@ -1,26 +1,15 @@
from __future__ import annotations
import codecs
import contextlib
import dataclasses
import functools
import hashlib
import io
import os
import re
import sys
import textwrap
import time
from collections.abc import Generator
from pathlib import Path
from typing import IO, TYPE_CHECKING, AnyStr, Final, Literal
from typing import IO, AnyStr, Final, Tuple
import humanize
from .util import CIProvider, detect_ci_provider
from .ci import CIProvider, detect_ci_provider, filter_ansi_codes
if TYPE_CHECKING:
from .options import Options
FoldPattern = tuple[str, str]
FoldPattern = Tuple[str, str]
DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "")
FOLD_PATTERNS: Final[dict[str, FoldPattern]] = {
"azure": ("##[group]{name}", "##[endgroup]"),
@@ -34,92 +23,28 @@ PLATFORM_IDENTIFIER_DESCRIPTIONS: Final[dict[str, str]] = {
"manylinux_aarch64": "manylinux aarch64",
"manylinux_ppc64le": "manylinux ppc64le",
"manylinux_s390x": "manylinux s390x",
"manylinux_armv7l": "manylinux armv7l",
"manylinux_riscv64": "manylinux riscv64",
"musllinux_x86_64": "musllinux x86_64",
"musllinux_i686": "musllinux i686",
"musllinux_aarch64": "musllinux aarch64",
"musllinux_ppc64le": "musllinux ppc64le",
"musllinux_s390x": "musllinux s390x",
"musllinux_armv7l": "musllinux armv7l",
"musllinux_riscv64": "musllinux riscv64",
"musllinux_s390x": "manylinux s390x",
"win32": "Windows 32bit",
"win_amd64": "Windows 64bit",
"win_arm64": "Windows on ARM 64bit",
"macosx_x86_64": "macOS x86_64",
"macosx_universal2": "macOS Universal 2 - x86_64 and arm64",
"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:
fold_mode: Literal["azure", "github", "travis", "disabled"]
fold_mode: str
colors_enabled: bool
unicode_enabled: bool
active_build_identifier: str | None = None
build_start_time: float | None = None
step_start_time: float | None = None
active_fold_group_name: str | None = None
summary: list[BuildInfo]
def __init__(self) -> None:
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
@@ -131,28 +56,25 @@ class Logger:
ci_provider = detect_ci_provider()
match ci_provider:
case CIProvider.azure_pipelines:
self.fold_mode = "azure"
self.colors_enabled = True
if ci_provider == CIProvider.azure_pipelines:
self.fold_mode = "azure"
self.colors_enabled = True
case CIProvider.github_actions:
self.fold_mode = "github"
self.colors_enabled = True
elif ci_provider == CIProvider.github_actions:
self.fold_mode = "github"
self.colors_enabled = True
case CIProvider.travis_ci:
self.fold_mode = "travis"
self.colors_enabled = True
elif ci_provider == CIProvider.travis_ci:
self.fold_mode = "travis"
self.colors_enabled = True
case CIProvider.appveyor:
self.fold_mode = "disabled"
self.colors_enabled = True
elif ci_provider == CIProvider.appveyor:
self.fold_mode = "disabled"
self.colors_enabled = True
case _:
self.fold_mode = "disabled"
self.colors_enabled = file_supports_color(sys.stdout)
self.summary = []
else:
self.fold_mode = "disabled"
self.colors_enabled = file_supports_color(sys.stdout)
def build_start(self, identifier: str) -> None:
self.step_end()
@@ -166,7 +88,7 @@ class Logger:
self.build_start_time = time.time()
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.active_build_identifier is not None
self.step_end()
@@ -174,14 +96,11 @@ class Logger:
c = self.colors
s = self.symbols
duration = time.time() - self.build_start_time
duration_str = humanize.naturaldelta(duration, minimum_unit="milliseconds")
print()
print(f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration_str}")
self.summary.append(
BuildInfo(identifier=self.active_build_identifier, filename=filename, duration=duration)
print(
f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration:.2f}s"
)
self.build_start_time = None
self.active_build_identifier = None
@@ -196,7 +115,6 @@ class Logger:
c = self.colors
s = self.symbols
duration = time.time() - self.step_start_time
if success:
print(f"{c.green}{s.done} {c.end}{duration:.2f}s".rjust(78))
else:
@@ -214,48 +132,24 @@ class Logger:
def notice(self, message: str) -> None:
if self.fold_mode == "github":
print(f"::notice::cibuildwheel: {message}\n", file=sys.stderr)
print(f"::notice::{message}\n", file=sys.stderr)
else:
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:
if self.fold_mode == "github":
print(f"::warning::cibuildwheel: {message}\n", file=sys.stderr)
print(f"::warning::{message}\n", file=sys.stderr)
else:
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:
if self.fold_mode == "github":
print(f"::error::cibuildwheel: {error}\n", file=sys.stderr)
print(f"::error::{error}\n", file=sys.stderr)
else:
c = self.colors
print(f"cibuildwheel: {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
print(f"{c.bright_red}Error{c.end}: {error}\n", file=sys.stderr)
def _start_fold_group(self, name: str) -> None:
self._end_fold_group()
@@ -292,72 +186,6 @@ class Logger:
# lowercase, shorten
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
def colors(self) -> Colors:
return Colors(enabled=self.colors_enabled)
@@ -373,22 +201,17 @@ def build_description_from_identifier(identifier: str) -> str:
build_description = ""
python_interpreter = python_identifier[0:2]
version_parts = python_identifier[2:].split("_")
python_version = version_parts[0]
python_version = python_identifier[2:]
if python_interpreter == "cp":
build_description += "CPython"
elif python_interpreter == "pp":
build_description += "PyPy"
elif python_interpreter == "gp":
build_description += "GraalPy"
else:
msg = f"unknown python {python_interpreter!r}"
raise Exception(msg)
build_description += f" {python_version[0]}.{python_version[1:]} "
if len(version_parts) > 1:
build_description += f"(ABI {version_parts[1]}) "
try:
build_description += PLATFORM_IDENTIFIER_DESCRIPTIONS[platform_identifier]
@@ -399,6 +222,32 @@ def build_description_from_identifier(identifier: str) -> str:
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:
"""
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 inspect
import os
import platform
import re
@@ -8,35 +9,43 @@ import shutil
import subprocess
import sys
import typing
from collections.abc import Set
from collections.abc import Sequence, Set
from dataclasses import dataclass
from pathlib import Path
from typing import Literal, assert_never
from typing import Literal, Tuple
from filelock import FileLock
from packaging.version import Version
from .. import errors
from ..architecture import Architecture
from ..ci import detect_ci_provider
from ..environment import ParsedEnvironment
from ..frontend import BuildFrontendName, 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 (
from ._compat.typing import assert_never
from .architecture import Architecture
from .environment import ParsedEnvironment
from .logger import log
from .options import Options
from .typing import PathOrStr
from .util import (
CIBW_CACHE_PATH,
copy_test_sources,
AlreadyBuiltWheelError,
BuildFrontendConfig,
BuildFrontendName,
BuildSelector,
NonPlatformWheelError,
call,
detect_ci_provider,
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]:
"""
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 = tuple(map(int, version_str.split(".")[:2]))
if (10, 15) < version < (11, 0):
# 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]}"
return typing.cast(Tuple[int, int], version)
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)]
@dataclasses.dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True)
class PythonConfiguration:
version: str
identifier: 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(
build_selector: BuildSelector, architectures: Set[Architecture]
) -> 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
python_configurations = [
@@ -103,7 +89,7 @@ def get_python_configurations(
# skip builds as required by BUILD/SKIP
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
# rosetta allows to build x86_64 on arm64
if platform.machine() == "x86_64":
@@ -111,7 +97,7 @@ def get_python_configurations(
python_configurations = [
c
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)
if removed_elements:
@@ -119,7 +105,7 @@ def get_python_configurations(
log.quiet(
unwrap(
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.
"""
)
@@ -128,54 +114,35 @@ def get_python_configurations(
return python_configurations
def install_cpython(_tmp: Path, version: str, url: str, free_threading: bool) -> Path:
ft = "T" if free_threading else ""
installation_path = Path(f"/Library/Frameworks/Python{ft}.framework/Versions/{version}")
def install_cpython(tmp: Path, version: str, url: str) -> Path:
installation_path = Path(f"/Library/Frameworks/Python.framework/Versions/{version}")
with FileLock(CIBW_CACHE_PATH / f"cpython{version}.lock"):
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
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 detect_ci_provider() is None:
# if running locally, we don't want to install CPython with sudo
# let the user know & provide a link to the installer
msg = inspect.cleandoc(
f"""
Error: CPython {version} is not installed.
cibuildwheel will not perform system-wide installs when running outside of CI.
To build locally, install CPython {version} on this machine, or, disable this
version of Python using CIBW_SKIP=cp{version.replace(".", "")}-macosx_*
For portable builds, cibuildwheel needs the official builds from python.org.
Download link: {url}
"""
print(
f"Error: CPython {version} is not installed.\n"
"cibuildwheel will not perform system-wide installs when running outside of CI.\n"
f"To build locally, install CPython {version} on this machine, or, disable this version of Python using CIBW_SKIP=cp{version.replace('.', '')}-macosx_*\n"
f"\nDownload link: {url}",
file=sys.stderr,
)
raise errors.FatalError(msg)
python_filename = url.split("/")[-1]
pkg_path = CIBW_CACHE_PATH / "cpython-installer" / python_filename
if not pkg_path.exists():
download(url, pkg_path)
args = []
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", "/")
raise SystemExit(1)
pkg_path = tmp / "Python.pkg"
# download the pkg
download(url, pkg_path)
# install
call("sudo", "installer", "-pkg", pkg_path, "-target", "/")
pkg_path.unlink()
env = os.environ.copy()
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
call(installation_path / "bin" / "python3", install_certifi_script, env=env)
if free_threading:
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")
return installation_path / "bin" / "python3"
def install_pypy(tmp: Path, url: str) -> Path:
@@ -193,61 +160,28 @@ def install_pypy(tmp: Path, url: str) -> Path:
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(
tmp: Path,
python_configuration: PythonConfiguration,
dependency_constraint: Path | None,
dependency_constraint_flags: Sequence[PathOrStr],
environment: ParsedEnvironment,
build_frontend: BuildFrontendName,
) -> tuple[Path, dict[str, str]]:
uv_path = find_uv()
use_uv = build_frontend == "build[uv]"
) -> dict[str, str]:
tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0]
log.step(f"Installing Python {implementation_id}...")
if implementation_id.startswith("cp"):
free_threading = "t-macos" in python_configuration.identifier
base_python = install_cpython(
tmp, python_configuration.version, python_configuration.url, free_threading
)
base_python = install_cpython(tmp, python_configuration.version, python_configuration.url)
elif implementation_id.startswith("pp"):
base_python = install_pypy(tmp, python_configuration.url)
elif implementation_id.startswith("gp"):
base_python = install_graalpy(tmp, python_configuration.url)
else:
msg = "Unknown Python implementation"
raise ValueError(msg)
assert base_python.exists(), (
f"{base_python.name} not found, has {list(base_python.parent.iterdir())}"
)
assert base_python.exists()
log.step("Setting up build environment...")
venv_path = tmp / "venv"
env = virtualenv(
python_configuration.version,
base_python,
venv_path,
dependency_constraint,
use_uv=use_uv,
)
env = virtualenv(base_python, venv_path, dependency_constraint_flags)
venv_bin_path = venv_path / "bin"
assert venv_bin_path.exists()
# Fix issue with site.py setting the wrong `sys.prefix`, `sys.exec_prefix`,
@@ -261,26 +195,45 @@ def setup_python(
# 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'
call(
"python",
"-m",
"pip",
"install",
"--upgrade",
"pip",
*dependency_constraint_flags,
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 = "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
if not use_uv:
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 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)
assert (venv_bin_path / "pip").exists()
call("which", "pip", env=env)
call("pip", "--version", env=env)
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
if which_pip != str(venv_bin_path / "pip"):
print(
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.",
file=sys.stderr,
)
sys.exit(1)
# 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_universal2 = python_configuration.identifier.endswith("universal2")
@@ -288,42 +241,23 @@ def setup_python(
# Set MACOSX_DEPLOYMENT_TARGET, if the user didn't set it.
# For arm64, the minimal deployment target is 11.0.
# On x86_64 (or universal2), use 10.9 as a default.
# CPython 3.12.6+ needs 10.13.
# CPython 3.14.0 needs 10.15.
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)
# PyPy defaults to 10.7, causing inconsistencies if it's left unset.
env.setdefault("MACOSX_DEPLOYMENT_TARGET", "11.0" if config_is_arm64 else "10.9")
# This is a floor, it can't be set lower than the default_target.
if Version(env["MACOSX_DEPLOYMENT_TARGET"]) < Version(default_target):
log.warning(
f"Bumping MACOSX_DEPLOYMENT_TARGET ({env['MACOSX_DEPLOYMENT_TARGET']}) to the minimum required ({default_target})."
)
env["MACOSX_DEPLOYMENT_TARGET"] = default_target
if config_is_arm64:
# macOS 11 is the first OS with arm64 support, so the wheels
# have that as a minimum.
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-11.0-arm64")
env.setdefault("ARCHFLAGS", "-arch arm64")
elif config_is_universal2:
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-universal2")
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")
if python_configuration.version not in {"3.6", "3.7"}:
if config_is_arm64:
# macOS 11 is the first OS with arm64 support, so the wheels
# have that as a minimum.
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-11.0-arm64")
env.setdefault("ARCHFLAGS", "-arch arm64")
elif config_is_universal2:
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-universal2")
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
if building_arm64 and get_macos_version() < (10, 16) and "SDKROOT" not in env:
@@ -348,42 +282,31 @@ def setup_python(
env.setdefault("SDKROOT", arm64_compatible_sdks[0])
log.step("Installing build tools...")
match build_frontend:
case "pip":
call(
"pip",
"install",
"--upgrade",
"delocate",
*constraint_flags(dependency_constraint),
env=env,
)
case "build":
call(
"pip",
"install",
"--upgrade",
"delocate",
"build[virtualenv]",
*constraint_flags(dependency_constraint),
env=env,
)
case "build[uv]":
assert uv_path is not None
call(
uv_path,
"pip",
"install",
"--upgrade",
"delocate",
"build[virtualenv, uv]",
*constraint_flags(dependency_constraint),
env=env,
)
case _:
assert_never(build_frontend)
if build_frontend == "pip":
call(
"pip",
"install",
"--upgrade",
"setuptools",
"wheel",
"delocate",
*dependency_constraint_flags,
env=env,
)
elif build_frontend == "build":
call(
"pip",
"install",
"--upgrade",
"delocate",
"build[virtualenv]",
*dependency_constraint_flags,
env=env,
)
else:
assert_never(build_frontend)
return base_python, env
return env
def build(options: Options, tmp_path: Path) -> None:
@@ -411,13 +334,7 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend
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"]
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier
@@ -428,18 +345,20 @@ def build(options: Options, tmp_path: Path) -> None:
config_is_arm64 = config.identifier.endswith("arm64")
config_is_universal2 = config.identifier.endswith("universal2")
constraints_path = build_options.dependency_constraints.get_for_python_version(
version=config.version, tmp_dir=identifier_tmp_dir
)
dependency_constraint_flags: Sequence[PathOrStr] = []
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",
config,
constraints_path,
dependency_constraint_flags,
build_options.environment,
build_frontend.name,
)
pip_version = None if use_uv else get_pip_version(env)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
@@ -459,53 +378,58 @@ def build(options: Options, tmp_path: Path) -> None:
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
extra_flags = split_config_settings(
build_options.config_settings, build_frontend.name
)
extra_flags += build_frontend.args
build_env = env.copy()
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=build_env,
if build_frontend.name == "pip":
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
# 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,
)
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]":
if (
use_uv
and "--no-isolation" not in extra_flags
and "-n" not in extra_flags
):
extra_flags.append("--installer=uv")
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
env=build_env,
)
case _:
assert_never(build_frontend)
build_env["PIP_CONSTRAINT"] = constraint_path.as_uri()
build_env["VIRTUALENV_PIP"] = get_pip_version(env)
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
env=build_env,
)
else:
assert_never(build_frontend)
built_wheel = next(built_wheel_dir.glob("*.whl"))
repaired_wheel_dir.mkdir()
if built_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError()
raise NonPlatformWheelError()
if build_options.repair_command:
log.step("Repairing wheel...")
@@ -522,20 +446,15 @@ def build(options: Options, tmp_path: Path) -> None:
wheel=built_wheel,
dest_dir=repaired_wheel_dir,
delocate_archs=delocate_archs,
package=build_options.package_dir,
project=".",
)
shell(repair_command_prepared, env=env)
else:
shutil.move(str(built_wheel), repaired_wheel_dir)
try:
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
except StopIteration:
raise errors.RepairStepProducedNoWheelError() from None
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)
raise AlreadyBuiltWheelError(repaired_wheel.name)
log.step_end()
@@ -619,13 +538,17 @@ def build(options: Options, tmp_path: Path) -> None:
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 = []
uv_arch_args = []
if testing_arch != machine_arch:
if machine_arch == "arm64" and testing_arch == "x86_64":
# rosetta2 will provide the emulation with just the arch prefix.
arch_prefix = ["arch", "-x86_64"]
uv_arch_args = ["--python-platform", "x86_64-apple-darwin"]
else:
msg = f"don't know how to emulate {testing_arch} on {machine_arch}"
raise RuntimeError(msg)
@@ -634,27 +557,16 @@ def build(options: Options, tmp_path: Path) -> None:
call_with_arch = functools.partial(call, *arch_prefix)
shell_with_arch = functools.partial(call, *arch_prefix, "/bin/sh", "-c")
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
venv_dir = identifier_tmp_dir / f"venv-test-{testing_arch}"
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")
# Use --no-download to ensure determinism by using seed libraries
# built into virtualenv
call_with_arch("python", "-m", "virtualenv", "--no-download", venv_dir, env=env)
virtualenv_env["MACOSX_DEPLOYMENT_TARGET"] = get_test_macosx_deployment_target()
virtualenv_env = build_options.test_environment.as_dictionary(
prev_environment=virtualenv_env
virtualenv_env = env.copy()
virtualenv_env["PATH"] = os.pathsep.join(
[
str(venv_dir / "bin"),
virtualenv_env["PATH"],
]
)
# check that we are using the Python from the virtual environment
@@ -669,33 +581,17 @@ def build(options: Options, tmp_path: Path) -> None:
shell_with_arch(before_test_prepared, env=virtualenv_env)
# install the wheel
if is_cp38 and python_arch == "x86_64":
virtualenv_env_install_wheel = virtualenv_env.copy()
virtualenv_env_install_wheel["SYSTEM_VERSION_COMPAT"] = "0"
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(
call_with_arch(
"pip",
"install",
f"{repaired_wheel}{build_options.test_extras}",
env=virtualenv_env_install_wheel,
env=virtualenv_env,
)
# test the wheel
if build_options.test_requires:
pip_install(
*build_options.test_requires,
env=virtualenv_env_install_wheel,
call_with_arch(
"pip", "install", *build_options.test_requires, env=virtualenv_env
)
# run the tests from a temp dir, with an absolute path in the command
@@ -703,48 +599,31 @@ def build(options: Options, tmp_path: Path) -> None:
# and not the repo code)
test_command_prepared = prepare_command(
build_options.test_command,
project=Path.cwd(),
project=Path(".").resolve(),
package=build_options.package_dir.resolve(),
wheel=repaired_wheel,
)
test_cwd = identifier_tmp_dir / "test_cwd"
if build_options.test_sources:
# 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()
)
test_cwd.mkdir(exist_ok=True)
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
# we're all done here; move it to output (overwrite existing)
output_wheel = 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)
with contextlib.suppress(FileNotFoundError):
(build_options.output_dir / repaired_wheel.name).unlink()
shutil.move(str(repaired_wheel), build_options.output_dir)
built_wheels.append(build_options.output_dir / repaired_wheel.name)
# clean up
shutil.rmtree(identifier_tmp_dir)
log.build_end(output_wheel)
log.build_end()
except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error
log.step_end_with_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 json
import os
@@ -7,67 +8,35 @@ import shlex
import shutil
import subprocess
import sys
import textwrap
import typing
import uuid
from collections.abc import Mapping, Sequence
from enum import Enum
from dataclasses import dataclass
from pathlib import Path, PurePath, PurePosixPath
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 .errors import OCIEngineTooOldError
from .logger import log
from .typing import PathOrStr
from .util.cmd import call
from .util.helpers import FlexibleVersion, parse_key_value_string, strtobool
from ._compat.typing import Self
from .typing import PathOrStr, PopenBytes
from .util import (
CIProvider,
call,
detect_ci_provider,
parse_key_value_string,
strtobool,
)
ContainerEngineName = Literal["docker", "podman"]
# Order of the enum matters for tests. 386 shall appear before amd64.
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)
@dataclass(frozen=True)
class OCIContainerEngineConfig:
name: ContainerEngineName
_: dataclasses.KW_ONLY
create_args: tuple[str, ...] = dataclasses.field(default_factory=tuple)
create_args: Sequence[str] = ()
disable_host_mount: bool = False
@classmethod
def from_config_string(cls, config_string: str) -> Self:
@staticmethod
def from_config_string(config_string: str) -> OCIContainerEngineConfig:
config_dict = parse_key_value_string(
config_string,
["name"],
@@ -78,7 +47,7 @@ class OCIContainerEngineConfig:
msg = f"unknown container engine {name}"
raise ValueError(msg)
name = typing.cast("ContainerEngineName", name)
name = typing.cast(ContainerEngineName, name)
# 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 []
disable_host_mount_options = (
@@ -87,17 +56,10 @@ class OCIContainerEngineConfig:
disable_host_mount = (
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]:
if not self.create_args:
@@ -113,58 +75,6 @@ class OCIContainerEngineConfig:
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:
"""
An object that represents a running OCI (e.g. Docker) container.
@@ -177,28 +87,20 @@ class OCIContainer:
back to cibuildwheel.
Example:
>>> # xdoctest: +REQUIRES(LINUX)
>>> from cibuildwheel.oci_container import * # NOQA
>>> 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']
>>> # 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(["cat", "/proc/1/cgroup"])
... print(self.get_environment())
... 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_stdout: IO[bytes]
@@ -206,7 +108,7 @@ class OCIContainer:
self,
*,
image: str,
oci_platform: OCIPlatform,
enforce_32_bit: bool = False,
cwd: PathOrStr | None = None,
engine: OCIContainerEngineConfig = DEFAULT_ENGINE,
):
@@ -215,48 +117,14 @@ class OCIContainer:
raise ValueError(msg)
self.image = image
self.oci_platform = oci_platform
self.enforce_32_bit = enforce_32_bit
self.cwd = cwd
self.name: str | None = None
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:
self.name = f"cibuildwheel-{uuid.uuid4()}"
_check_engine_version(self.engine)
# work-around for Travis-CI PPC64le Docker runs since 2021:
# this avoids network splits
# https://github.com/pypa/cibuildwheel/issues/904
@@ -265,30 +133,15 @@ class OCIContainer:
if detect_ci_provider() == CIProvider.travis_ci and platform.machine() == "ppc64le":
network_args = ["--network=host"]
platform_args = self._get_platform_args()
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
# or the image entrypoint takes care of enforcing this, then we don't need to
# simulate this
run_cmd = [self.engine.name, "run", "--rm"]
ctr_cmd = ["uname", "-m"]
try:
container_machine = call(
*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"}
container_machine = call(
self.engine.name, "run", "--rm", self.image, "uname", "-m", capture_stdout=True
).strip()
simulate_32_bit = container_machine != "i686"
shell_args = ["linux32", "/bin/bash"] if simulate_32_bit else ["/bin/bash"]
@@ -302,7 +155,6 @@ class OCIContainer:
"--interactive",
*(["--volume=/:/host"] if not self.engine.disable_host_mount else []),
*network_args,
*platform_args,
*self.engine.create_args,
self.image,
*shell_args,
@@ -369,17 +221,21 @@ class OCIContainer:
self.name = 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():
self.call(["mkdir", "-p", to_path])
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,
check=True,
cwd=from_path,
)
else:
exec_process: subprocess.Popen[bytes]
self.call(["mkdir", "-p", to_path.parent])
with subprocess.Popen(
[
self.engine.name,
@@ -394,7 +250,8 @@ class OCIContainer:
) as exec_process:
assert exec_process.stdin
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.wait()
@@ -407,7 +264,30 @@ class OCIContainer:
def copy_out(self, from_path: PurePath, to_path: Path) -> None:
# note: we assume from_path is a dir
to_path.mkdir(parents=True, exist_ok=True)
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]:
glob_pattern = path.joinpath(pattern)
@@ -518,7 +398,7 @@ class OCIContainer:
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:
# used as an EnvironmentExecutor to evaluate commands and capture output
+228 -531
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]
-702
View File
@@ -1,702 +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
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
# 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 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 configparser
import contextlib
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:
@@ -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:
match parent:
case ast.If(test=ast.Compare(left=left, ops=[ast.Eq()], comparators=[comp])):
values = {left, comp}
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__"}
return len(consts) == 1
case _:
return False
if parent is None:
return False
# This would be much nicer with 3.10's pattern matching!
if not isinstance(parent, ast.If):
return False
if not isinstance(parent.test, ast.Compare):
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):
@@ -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))
)
match node:
case ast.keyword(arg="python_requires", value=ast.Constant(value=str() as version)):
if unnested or name_main_unnested:
self.requires_python = version
if (
node.arg == "python_requires"
and isinstance(node.value, ast.Constant)
and (unnested or name_main_unnested)
):
self.requires_python = node.value.value
def setup_py_python_requires(content: str) -> str | None:
@@ -65,12 +86,15 @@ def setup_py_python_requires(content: str) -> str | 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"""
# Read in from pyproject.toml:project.requires-python
with contextlib.suppress(KeyError, IndexError, TypeError):
return str((pyproject_toml or {})["project"]["requires-python"])
with contextlib.suppress(FileNotFoundError):
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
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 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.
+58 -167
View File
@@ -1,151 +1,93 @@
[linux]
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 = "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 = "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 = "cp313-manylinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-manylinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ 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 = "cp36-manylinux_i686", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_i686", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp38-manylinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_i686", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "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 = "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 = "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 = "pp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/pp37-pypy37_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 = "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 = "gp311_242-manylinux_x86_64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" },
{ identifier = "gp312_250-manylinux_x86_64", version = "3.12", path_str = "/opt/python/graalpy312-graalpy250_312_native" },
{ identifier = "cp36-manylinux_aarch64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "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 = "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 = "cp313t-manylinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ 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 = "cp36-manylinux_ppc64le", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_ppc64le", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp38-manylinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_ppc64le", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "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 = "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 = "cp313t-manylinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ 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 = "cp36-manylinux_s390x", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_s390x", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp38-manylinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_s390x", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ 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 = "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 = "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 = "pp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/pp37-pypy37_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 = "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 = "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 = "pp37-manylinux_i686", version = "3.7", path_str = "/opt/python/pp37-pypy37_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 = "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 = "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 = "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 = "cp313-musllinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-musllinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ 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 = "cp36-musllinux_i686", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-musllinux_i686", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ 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 = "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 = "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 = "cp313t-musllinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ 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 = "cp36-musllinux_aarch64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-musllinux_aarch64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ 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 = "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 = "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 = "cp313t-musllinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ 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 = "cp36-musllinux_ppc64le", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-musllinux_ppc64le", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ 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 = "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 = "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 = "cp313t-musllinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ 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 = "cp36-musllinux_s390x", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-musllinux_s390x", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ 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 = "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 = "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]
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_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" },
@@ -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_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 = "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_arm64", 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.9/python-3.11.9-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_arm64", 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.10/python-3.12.10-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 = "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 = "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.7/python-3.11.7-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.1/python-3.12.1-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.1/python-3.12.1-macos11.pkg" },
{ identifier = "pp37-macosx_x86_64", version = "3.7", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.9-osx64.tar.bz2" },
{ identifier = "pp38-macosx_x86_64", version = "3.8", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.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 = "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_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-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_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-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" },
{ 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.15-macos_arm64.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.15-macos_arm64.tar.bz2" },
]
[windows]
python_configurations = [
{ identifier = "cp38-win32", version = "3.8.10" },
{ identifier = "cp38-win_amd64", version = "3.8.10" },
{ identifier = "cp39-win32", version = "3.9.13" },
{ identifier = "cp39-win_amd64", version = "3.9.13" },
{ identifier = "cp310-win32", version = "3.10.11" },
{ identifier = "cp310-win_amd64", version = "3.10.11" },
{ identifier = "cp311-win32", version = "3.11.9" },
{ identifier = "cp311-win_amd64", version = "3.11.9" },
{ identifier = "cp312-win32", version = "3.12.10" },
{ identifier = "cp312-win_amd64", version = "3.12.10" },
{ identifier = "cp313-win32", version = "3.13.11" },
{ identifier = "cp313t-win32", version = "3.13.11" },
{ identifier = "cp313-win_amd64", version = "3.13.11" },
{ identifier = "cp313t-win_amd64", version = "3.13.11" },
{ identifier = "cp314-win32", version = "3.14.2" },
{ identifier = "cp314t-win32", version = "3.14.2" },
{ identifier = "cp314-win_amd64", version = "3.14.2" },
{ identifier = "cp314t-win_amd64", version = "3.14.2" },
{ identifier = "cp39-win_arm64", version = "3.9.10" },
{ identifier = "cp310-win_arm64", version = "3.10.11" },
{ identifier = "cp311-win_arm64", version = "3.11.9" },
{ identifier = "cp312-win_arm64", version = "3.12.10" },
{ 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.0", 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" },
{ identifier = "cp36-win32", version = "3.6.8", arch = "32" },
{ identifier = "cp36-win_amd64", version = "3.6.8", arch = "64" },
{ identifier = "cp37-win32", version = "3.7.9", arch = "32" },
{ identifier = "cp37-win_amd64", version = "3.7.9", arch = "64" },
{ identifier = "cp38-win32", version = "3.8.10", arch = "32" },
{ identifier = "cp38-win_amd64", version = "3.8.10", arch = "64" },
{ identifier = "cp39-win32", version = "3.9.13", arch = "32" },
{ identifier = "cp39-win_amd64", version = "3.9.13", arch = "64" },
{ identifier = "cp310-win32", version = "3.10.11", arch = "32" },
{ identifier = "cp310-win_amd64", version = "3.10.11", arch = "64" },
{ identifier = "cp311-win32", version = "3.11.7", arch = "32" },
{ identifier = "cp311-win_amd64", version = "3.11.7", arch = "64" },
{ identifier = "cp312-win32", version = "3.12.1", arch = "32" },
{ identifier = "cp312-win_amd64", version = "3.12.1", arch = "64" },
{ identifier = "cp39-win_arm64", version = "3.9.10", arch = "ARM64" },
{ identifier = "cp310-win_arm64", version = "3.10.11", arch = "ARM64" },
{ identifier = "cp311-win_arm64", version = "3.11.7", arch = "ARM64" },
{ identifier = "cp312-win_arm64", version = "3.12.1", arch = "ARM64" },
{ identifier = "pp37-win_amd64", version = "3.7", arch = "64", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.9-win64.zip" },
{ identifier = "pp38-win_amd64", version = "3.8", arch = "64", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip" },
{ identifier = "pp39-win_amd64", version = "3.9", arch = "64", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.15-win64.zip" },
{ identifier = "pp310-win_amd64", version = "3.10", arch = "64", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.15-win64.zip" },
]
+4 -549
View File
@@ -1,29 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json",
"$defs": {
"inherit": {
"enum": [
"none",
"prepend",
"append"
],
"default": "none",
"description": "How to inherit the parent's value."
},
"enable": {
"enum": [
"cpython-experimental-riscv64",
"cpython-freethreading",
"cpython-prerelease",
"graalpy",
"pyodide-prerelease",
"pypy",
"pypy-eol"
]
},
"description": "A Python version or flavor to enable."
},
"$schema": "http://json-schema.org/draft-07/schema",
"additionalProperties": false,
"description": "cibuildwheel's settings.",
"type": "object",
@@ -108,13 +85,12 @@
},
"build-frontend": {
"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": [
{
"enum": [
"pip",
"build",
"build[uv]",
"default"
]
},
@@ -126,10 +102,6 @@
"type": "string",
"pattern": "^build; ?args:"
},
{
"type": "string",
"pattern": "^build\\[uv\\]; ?args:"
},
{
"type": "object",
"additionalProperties": false,
@@ -140,8 +112,7 @@
"name": {
"enum": [
"pip",
"build",
"build[uv]"
"build"
]
},
"args": {
@@ -237,62 +208,9 @@
"dependency-versions": {
"default": "pinned",
"description": "Specify how cibuildwheel controls the versions of the tools it uses",
"oneOf": [
{
"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"
}
}
}
}
],
"type": "string",
"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": {
"description": "Set environment variables needed during the build.",
"oneOf": [
@@ -331,11 +249,6 @@
"description": "Specify alternative manylinux / musllinux container images",
"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": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
@@ -361,11 +274,6 @@
"description": "Specify alternative manylinux / musllinux container images",
"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": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
@@ -381,11 +289,6 @@
"description": "Specify alternative manylinux / musllinux container images",
"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": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
@@ -396,11 +299,6 @@
"description": "Specify alternative manylinux / musllinux container images",
"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": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
@@ -411,26 +309,6 @@
"description": "Specify alternative manylinux / musllinux container images",
"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": {
"description": "Execute a shell command to repair each built wheel.",
"oneOf": [
@@ -491,36 +369,6 @@
],
"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": {
"description": "Install Python dependencies before running the tests",
"oneOf": [
@@ -551,57 +399,6 @@
],
"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": {
"type": "array",
"description": "An overrides array",
@@ -626,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": {
"$ref": "#/properties/before-all"
},
@@ -695,9 +441,6 @@
"config-settings": {
"$ref": "#/properties/config-settings"
},
"container-engine": {
"$ref": "#/properties/container-engine"
},
"dependency-versions": {
"$ref": "#/properties/dependency-versions"
},
@@ -710,9 +453,6 @@
"manylinux-aarch64-image": {
"$ref": "#/properties/manylinux-aarch64-image"
},
"manylinux-armv7l-image": {
"$ref": "#/properties/manylinux-armv7l-image"
},
"manylinux-i686-image": {
"$ref": "#/properties/manylinux-i686-image"
},
@@ -728,9 +468,6 @@
"manylinux-pypy_x86_64-image": {
"$ref": "#/properties/manylinux-pypy_x86_64-image"
},
"manylinux-riscv64-image": {
"$ref": "#/properties/manylinux-riscv64-image"
},
"manylinux-s390x-image": {
"$ref": "#/properties/manylinux-s390x-image"
},
@@ -740,30 +477,18 @@
"musllinux-aarch64-image": {
"$ref": "#/properties/musllinux-aarch64-image"
},
"musllinux-armv7l-image": {
"$ref": "#/properties/musllinux-armv7l-image"
},
"musllinux-i686-image": {
"$ref": "#/properties/musllinux-i686-image"
},
"musllinux-ppc64le-image": {
"$ref": "#/properties/musllinux-ppc64le-image"
},
"musllinux-riscv64-image": {
"$ref": "#/properties/musllinux-riscv64-image"
},
"musllinux-s390x-image": {
"$ref": "#/properties/musllinux-s390x-image"
},
"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": {
"$ref": "#/properties/repair-wheel-command"
},
@@ -773,20 +498,8 @@
"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"
}
}
}
@@ -816,9 +529,6 @@
"config-settings": {
"$ref": "#/properties/config-settings"
},
"container-engine": {
"$ref": "#/properties/container-engine"
},
"environment": {
"$ref": "#/properties/environment"
},
@@ -828,9 +538,6 @@
"manylinux-aarch64-image": {
"$ref": "#/properties/manylinux-aarch64-image"
},
"manylinux-armv7l-image": {
"$ref": "#/properties/manylinux-armv7l-image"
},
"manylinux-i686-image": {
"$ref": "#/properties/manylinux-i686-image"
},
@@ -846,9 +553,6 @@
"manylinux-pypy_x86_64-image": {
"$ref": "#/properties/manylinux-pypy_x86_64-image"
},
"manylinux-riscv64-image": {
"$ref": "#/properties/manylinux-riscv64-image"
},
"manylinux-s390x-image": {
"$ref": "#/properties/manylinux-s390x-image"
},
@@ -858,30 +562,18 @@
"musllinux-aarch64-image": {
"$ref": "#/properties/musllinux-aarch64-image"
},
"musllinux-armv7l-image": {
"$ref": "#/properties/musllinux-armv7l-image"
},
"musllinux-i686-image": {
"$ref": "#/properties/musllinux-i686-image"
},
"musllinux-ppc64le-image": {
"$ref": "#/properties/musllinux-ppc64le-image"
},
"musllinux-riscv64-image": {
"$ref": "#/properties/musllinux-riscv64-image"
},
"musllinux-s390x-image": {
"$ref": "#/properties/musllinux-s390x-image"
},
"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": {
"description": "Execute a shell command to repair each built wheel.",
"oneOf": [
@@ -904,20 +596,8 @@
"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"
}
}
},
@@ -952,12 +632,6 @@
"environment": {
"$ref": "#/properties/environment"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command"
},
@@ -967,20 +641,8 @@
"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"
}
}
},
@@ -1015,12 +677,6 @@
"environment": {
"$ref": "#/properties/environment"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"description": "Execute a shell command to repair each built wheel.",
"oneOf": [
@@ -1043,209 +699,8 @@
"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"
}
}
},
"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,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.0
# via httpx
auditwheel-emscripten==0.2.0
# 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.2
# 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.8
# via auditwheel-emscripten
markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
packaging==25.0
# via
# auditwheel-emscripten
# build
# pyodide-build
# unearth
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.30.9
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
pyodide-cli==0.4.0
# 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.2.0
# via
# pyodide-build
# pyodide-cli
# typer
ruamel-yaml==0.19.1
# via pyodide-build
shellingham==1.5.4
# via typer
typer==0.21.0
# 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.2
# via requests
virtualenv==20.35.4
# via
# build
# pyodide-build
wheel==0.45.1
# via
# auditwheel-emscripten
# pyodide-build
@@ -1,112 +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.0
# via httpx
auditwheel-emscripten==0.2.0
# 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.2
# 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.8
# via auditwheel-emscripten
markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
packaging==25.0
# via
# auditwheel-emscripten
# build
# pyodide-build
# unearth
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.30.9
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
pyodide-cli==0.4.0
# 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.2.0
# via
# pyodide-build
# pyodide-cli
# typer
ruamel-yaml==0.19.1
# via pyodide-build
shellingham==1.5.4
# via typer
typer==0.21.0
# 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.2
# via requests
virtualenv==20.35.4
# via
# build
# pyodide-build
wheel==0.45.1
# via
# auditwheel-emscripten
# pyodide-build
@@ -1,36 +1,28 @@
# This file was autogenerated by uv via the following command:
# nox -s update_constraints
altgraph==0.17.5
# via macholib
build==1.3.0
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# nox -s update_constraints-3.10
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
distlib==0.3.8
# via virtualenv
filelock==3.20.2
filelock==3.13.1
# via virtualenv
importlib-metadata==8.7.1
# via build
macholib==1.16.4
packaging==23.2
# via delocate
packaging==25.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.5.1
platformdirs==4.1.0
# via virtualenv
pyproject-hooks==1.2.0
# via build
tomli==2.3.0
# via build
typing-extensions==4.15.0
# via
# delocate
# virtualenv
virtualenv==20.35.4
typing-extensions==4.9.0
# via delocate
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
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
altgraph==0.17.5
# via macholib
build==1.3.0
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# nox -s update_constraints-3.11
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
distlib==0.3.8
# via virtualenv
filelock==3.20.2
filelock==3.13.1
# via virtualenv
macholib==1.16.4
packaging==23.2
# via delocate
packaging==25.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.5.1
platformdirs==4.1.0
# via virtualenv
pyproject-hooks==1.2.0
# via build
typing-extensions==4.15.0
typing-extensions==4.9.0
# via delocate
virtualenv==20.35.4
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
@@ -1,28 +1,28 @@
# This file was autogenerated by uv via the following command:
# nox -s update_constraints
altgraph==0.17.5
# via macholib
build==1.3.0
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# nox -s update_constraints-3.12
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
distlib==0.3.8
# via virtualenv
filelock==3.20.2
filelock==3.13.1
# via virtualenv
macholib==1.16.4
packaging==23.2
# via delocate
packaging==25.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.5.1
platformdirs==4.1.0
# via virtualenv
pyproject-hooks==1.2.0
# via build
typing-extensions==4.15.0
typing-extensions==4.9.0
# via delocate
virtualenv==20.35.4
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
@@ -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.3.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.2
# via virtualenv
macholib==1.16.4
# via delocate
packaging==25.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.35.4
# 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.3.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.2
# via virtualenv
macholib==1.16.4
# via delocate
packaging==25.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.35.4
# 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
altgraph==0.17.5
# via macholib
build==1.2.2.post1
#
# This file is autogenerated by pip-compile with Python 3.8
# by the following command:
#
# nox -s update_constraints-3.8
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in
delocate==0.12.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
distlib==0.3.8
# via virtualenv
filelock==3.16.1
filelock==3.13.1
# via virtualenv
importlib-metadata==8.5.0
# via build
macholib==1.16.4
packaging==23.2
# via delocate
packaging==25.0
# via
# build
# delocate
pip==25.0.1
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.3.6
platformdirs==4.1.0
# via virtualenv
pyproject-hooks==1.2.0
# via build
tomli==2.3.0
# via build
typing-extensions==4.13.2
# via
# delocate
# virtualenv
virtualenv==20.35.4
typing-extensions==4.9.0
# via delocate
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
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
altgraph==0.17.5
# via macholib
build==1.3.0
#
# This file is autogenerated by pip-compile with Python 3.9
# by the following command:
#
# nox -s update_constraints-3.9
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
distlib==0.3.8
# via virtualenv
filelock==3.19.1
filelock==3.13.1
# via virtualenv
importlib-metadata==8.7.1
# via build
macholib==1.16.4
packaging==23.2
# via delocate
packaging==25.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.4.0
platformdirs==4.1.0
# via virtualenv
pyproject-hooks==1.2.0
# via build
tomli==2.3.0
# via build
typing-extensions==4.15.0
# via
# delocate
# virtualenv
virtualenv==20.35.4
typing-extensions==4.9.0
# via delocate
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
zipp==3.23.0
# via importlib-metadata
+2 -1
View File
@@ -1,4 +1,5 @@
pip
build
setuptools
wheel
delocate
virtualenv
+21 -21
View File
@@ -1,28 +1,28 @@
# This file was autogenerated by uv via the following command:
# nox -s update_constraints
altgraph==0.17.5
# via macholib
build==1.3.0
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# nox -s update_constraints-3.12
#
delocate==0.10.7
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.4.0
distlib==0.3.8
# via virtualenv
filelock==3.20.2
filelock==3.13.1
# via virtualenv
macholib==1.16.4
packaging==23.2
# via delocate
packaging==25.0
# via
# build
# delocate
pip==25.3
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.5.1
platformdirs==4.1.0
# via virtualenv
pyproject-hooks==1.2.0
# via build
typing-extensions==4.15.0
typing-extensions==4.9.0
# via delocate
virtualenv==20.35.4
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
+13 -32
View File
@@ -2,7 +2,6 @@
build = "*"
skip = ""
test-skip = ""
enable = []
archs = ["auto"]
build-frontend = "default"
@@ -14,41 +13,29 @@ build-verbosity = 0
before-all = ""
before-build = ""
# TOML doesn't support explicit NULLs; use ["\u0000"] as a sentinel value.
xbuild-tools = ["\u0000"]
repair-wheel-command = ""
test-command = ""
before-test = ""
test-sources = []
test-requires = []
test-extras = []
test-groups = []
test-environment = {}
test-runtime = {}
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"
manylinux-i686-image = "manylinux_2_28"
manylinux-aarch64-image = "manylinux_2_28"
manylinux-ppc64le-image = "manylinux_2_28"
manylinux-s390x-image = "manylinux_2_28"
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"
musllinux-x86_64-image = "musllinux_1_1"
musllinux-i686-image = "musllinux_1_1"
musllinux-aarch64-image = "musllinux_1_1"
musllinux-ppc64le-image = "musllinux_1_1"
musllinux-s390x-image = "musllinux_1_1"
[tool.cibuildwheel.linux]
@@ -58,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}"
[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)
print(" -- pip install --upgrade certifi")
subprocess.check_call(
[sys.executable, "-E", "-s", "-m", "pip", "install", "--upgrade", "certifi"]
)
import certifi # noqa: PLC0415
import certifi
# change working directory to the default SSL directory
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]
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2026.01.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2026.01.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2026.01.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2026.01.04-1
manylinux1 = quay.io/pypa/manylinux1_x86_64:2024-01-28-7b6687a
manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-08-05-4535177
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2024-01-23-12ffabc
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]
manylinux2014 = quay.io/pypa/manylinux2014_i686:2026.01.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2026.01.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2026.01.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686:2026.01.04-1
[aarch64]
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
manylinux1 = quay.io/pypa/manylinux1_i686:2024-01-28-7b6687a
manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-08-05-4535177
manylinux2014 = quay.io/pypa/manylinux2014_i686:2024-01-23-12ffabc
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
musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686:2024-01-23-12ffabc
[pypy_x86_64]
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2026.01.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2026.01.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2026.01.04-1
manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-08-05-4535177
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2024-01-23-12ffabc
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]
manylinux2014 = quay.io/pypa/manylinux2014_i686:2026.01.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2026.01.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2026.01.04-1
manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-08-05-4535177
manylinux2014 = quay.io/pypa/manylinux2014_i686:2024-01-23-12ffabc
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]
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
[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
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
@@ -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
# pytest/unittest is run from there.
import sys
import unittest
from typing import NoReturn
class TestStringMethods(unittest.TestCase):
def test_fail(self) -> NoReturn:
if sys.platform in ["android", "ios"]:
msg = (
"You tried to run tests from the testbed app's working "
"directory, without specifying `test-sources`. "
"On this platform, you must copy your test files to the testbed app by "
"setting the `test-sources` option in your cibuildwheel "
"configuration."
)
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)
def test_fail(self):
self.fail(
"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. 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."
)
+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
from pathlib import Path
from typing import Any
from .util import resources
DIR = Path(__file__).parent.resolve()
def get_schema(tool_name: str = "cibuildwheel") -> dict[str, Any]:
"Get the stored complete schema for cibuildwheel settings."
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]
-139
View File
@@ -1,139 +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.
"""
CPythonExperimentalRiscV64 = "cpython-experimental-riscv64"
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(set(cls) - {cls.CPythonExperimentalRiscV64})
@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 subprocess
import typing
from typing import Final, Literal, Protocol
from typing import Final, Literal, Protocol, Union
__all__ = (
"PLATFORMS",
"PathOrStr",
"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"]
PLATFORMS: Final[frozenset[PlatformName]] = frozenset(typing.get_args(PlatformName))
PlatformName = Literal["linux", "macos", "windows"]
PLATFORMS: Final[set[PlatformName]] = {"linux", "macos", "windows"}
class GenericPythonConfiguration(Protocol):
@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)
-32
View File
@@ -1,32 +0,0 @@
import functools
import tomllib
from pathlib import Path
from typing import Final
from ..typing import PlatformName
PATH: Final[Path] = Path(__file__).parent.parent / "resources"
INSTALL_CERTIFI_SCRIPT: Final[Path] = PATH / "install_certifi.py"
FREE_THREAD_ENABLE_313: Final[Path] = PATH / "free-threaded-enable-313.xml"
FREE_THREAD_ENABLE_314: Final[Path] = PATH / "free-threaded-enable-314.xml"
NODEJS: Final[Path] = PATH / "nodejs.toml"
DEFAULTS: Final[Path] = PATH / "defaults.toml"
PINNED_DOCKER_IMAGES: Final[Path] = PATH / "pinned_docker_images.cfg"
BUILD_PLATFORMS: Final[Path] = PATH / "build-platforms.toml"
CONSTRAINTS: Final[Path] = PATH / "constraints.txt"
VIRTUALENV: Final[Path] = PATH / "virtualenv.toml"
CIBUILDWHEEL_SCHEMA: Final[Path] = PATH / "cibuildwheel.schema.json"
PYTHON_BUILD_STANDALONE_RELEASES: Final[Path] = PATH / "python-build-standalone-releases.json"
TEST_FAIL_CWD_FILE: Final[Path] = PATH / "testing_temp_dir_file.py"
# this value is cached because it's used a lot in unit tests
@functools.cache
def read_all_configs() -> dict[str, list[dict[str, str]]]:
with BUILD_PLATFORMS.open("rb") as f:
loaded_file = tomllib.load(f)
return {k: list[dict[str, str]](v["python_configurations"]) for k, v in loaded_file.items()}
def read_python_configs(config: PlatformName) -> list[dict[str, str]]:
return read_all_configs()[config]
-167
View File
@@ -1,167 +0,0 @@
import contextlib
import functools
import os
import shutil
import sys
import tomllib
from collections.abc import Sequence
from pathlib import Path
from typing import Final
from filelock import FileLock
from packaging.requirements import InvalidRequirement, Requirement
from packaging.version import Version
from .util import resources
from .util.cmd import call
from .util.file import CIBW_CACHE_PATH, download
_IS_WIN: Final[bool] = sys.platform.startswith("win")
@functools.cache
def _ensure_virtualenv(version: str) -> tuple[Path, Version]:
version_parts = version.split(".")
key = f"py{version_parts[0]}{version_parts[1]}"
with resources.VIRTUALENV.open("rb") as f:
loaded_file = tomllib.load(f)
configuration = loaded_file.get(key, loaded_file["default"])
version = str(configuration["version"])
url = str(configuration["url"])
path = CIBW_CACHE_PATH / f"virtualenv-{version}.pyz"
with FileLock(str(path) + ".lock"):
if not path.exists():
download(url, path)
return (path, Version(version))
def constraint_flags(
dependency_constraint: Path | None,
) -> Sequence[str]:
"""
Returns the flags to pass to pip for the given dependency constraint.
"""
return ["-c", dependency_constraint.as_uri()] if dependency_constraint else []
def _parse_pip_constraint_for_virtualenv(
constraint_path: Path | None,
) -> 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.
"""
if constraint_path:
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 != "pip"
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
return specifier.version
except InvalidRequirement:
continue
return "embed"
def virtualenv(
version: str,
python: Path,
venv_path: Path,
dependency_constraint: Path | None,
*,
use_uv: bool,
env: dict[str, str] | None = None,
pip_version: str | None = None,
) -> dict[str, str]:
"""
Create a virtual environment. If `use_uv` is True,
dependency_constraint_flags are ignored since nothing is installed in the
venv. Otherwise, pip is installed.
"""
# virtualenv may fail if this is a symlink.
python = python.resolve()
assert python.exists()
if use_uv:
call("uv", "venv", venv_path, "--python", python)
else:
virtualenv_app, virtualenv_version = _ensure_virtualenv(version)
if pip_version is None:
pip_version = _parse_pip_constraint_for_virtualenv(dependency_constraint)
additional_flags = [f"--pip={pip_version}", "--no-setuptools"]
if virtualenv_version < Version("20.31") or Version(version) < Version("3.9"):
additional_flags.append("--no-wheel")
# 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 pip_version != "embed" and Version(pip_version) >= 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")]
venv_env = os.environ.copy() if env is None else env.copy()
venv_env["PATH"] = os.pathsep.join([*paths, venv_env["PATH"]])
venv_env["VIRTUAL_ENV"] = str(venv_path)
if not use_uv and pip_version == "embed":
call(
"python",
"-m",
"pip",
"install",
"--upgrade",
"pip",
*constraint_flags(dependency_constraint),
env=venv_env,
cwd=venv_path,
)
return venv_env
def find_uv() -> Path | None:
# Prefer uv in our environment
with contextlib.suppress(ImportError, FileNotFoundError):
from uv import find_uv_bin # noqa: PLC0415
return Path(find_uv_bin())
uv_on_path = shutil.which("uv")
return Path(uv_on_path) if uv_on_path else None

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