Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63fd63b352 | ||
|
|
f4fe311fc9 | ||
|
|
f6c810852d | ||
|
|
ccbae30b53 | ||
|
|
1337e50f74 | ||
|
|
720f8e266f | ||
|
|
4c7f3696b1 | ||
|
|
e1baa60eb0 | ||
|
|
1f2f8b28d5 | ||
|
|
8c5b02f42c | ||
|
|
997a6e1931 | ||
|
|
4fe7630d9c | ||
|
|
c53e541c2d | ||
|
|
58d113ad2b | ||
|
|
de0262ea87 | ||
|
|
41f3304a86 | ||
|
|
a3af13a337 | ||
|
|
168717b5c5 | ||
|
|
a592c06add | ||
|
|
bbe00a21e3 | ||
|
|
362020b1c7 | ||
|
|
8165422c41 | ||
|
|
a6acd2a2d5 | ||
|
|
372dd68cf6 | ||
|
|
cb5496943f | ||
|
|
ad74d64a16 | ||
|
|
be214efadb | ||
|
|
9c00cb4f6b | ||
|
|
ae65c7d85b | ||
|
|
86c3857999 | ||
|
|
68b1a81bb2 | ||
|
|
f131cd7935 | ||
|
|
8602e864d4 | ||
|
|
8ccc2650de | ||
|
|
c0c1dea51f | ||
|
|
7c619efba9 | ||
|
|
bbebb68339 | ||
|
|
5bbeaf830f | ||
|
|
7a67c6b5a9 | ||
|
|
b8a4fbc237 | ||
|
|
70de033a7f | ||
|
|
ce4c16f88f | ||
|
|
66490f0536 | ||
|
|
77b9e35d7c | ||
|
|
39e9da06c5 | ||
|
|
9b8ce87f0a | ||
|
|
3a524160c3 | ||
|
|
c294bab5aa | ||
|
|
5a4e49a734 | ||
|
|
4aa10a9378 | ||
|
|
c8fb268a1b | ||
|
|
841e718dee | ||
|
|
0826975570 | ||
|
|
3f076ba487 | ||
|
|
dac57ccc94 | ||
|
|
1d31ef4ab2 | ||
|
|
03ce2f2bc7 | ||
|
|
6331390c93 | ||
|
|
4b92d69e1b | ||
|
|
c923d83ad9 | ||
|
|
55ee8e49bf | ||
|
|
afe486c21f | ||
|
|
1c1ba8a22e | ||
|
|
33b75f491a | ||
|
|
c59c2fe70d | ||
|
|
24b64c5cd1 | ||
|
|
6425e48fed | ||
|
|
0264b4d03a | ||
|
|
68f9762657 | ||
|
|
95e827ece1 | ||
|
|
ff9a284f21 | ||
|
|
cade3864de | ||
|
|
63bdd4f043 | ||
|
|
5952330610 | ||
|
|
8db3538534 | ||
|
|
e5caaec292 | ||
|
|
5a1712daed | ||
|
|
a4c6d16811 | ||
|
|
78a246a901 | ||
|
|
47c5a31c1b | ||
|
|
3ebb6bd942 | ||
|
|
352e01339f | ||
|
|
c463e56ba2 | ||
|
|
8c5c738023 | ||
|
|
feeb3992a7 | ||
|
|
60b9cc95db | ||
|
|
e2c7102ed7 | ||
|
|
9e4e50bd76 | ||
|
|
8ef9414f60 | ||
|
|
1953c04972 | ||
|
|
46a6d27995 | ||
|
|
0dc0b48e4d | ||
|
|
b887417e0e | ||
|
|
36c5079ce5 | ||
|
|
68c03665d3 | ||
|
|
1c38fac9c8 | ||
|
|
140bc1a96a | ||
|
|
55fcb25edc | ||
|
|
c89609aaf2 | ||
|
|
13b086e3c6 | ||
|
|
302af02853 | ||
|
|
9361af3204 | ||
|
|
47c5a35421 | ||
|
|
e6de07ed39 | ||
|
|
2ca692b1e5 | ||
|
|
1ac7fa7f00 | ||
|
|
ffd835cef1 | ||
|
|
3e2a9aa6e8 | ||
|
|
10c727eed9 | ||
|
|
f628c9dd23 | ||
|
|
0f487ee2cb | ||
|
|
e2e24882d8 | ||
|
|
a8bff94dbb | ||
|
|
fbf37256a5 | ||
|
|
9613645abc | ||
|
|
7247e3887c | ||
|
|
d0e4aded3e | ||
|
|
a2c263d4ab | ||
|
|
576ebccc78 | ||
|
|
cd9b48cb74 | ||
|
|
5e245905b3 | ||
|
|
157d81eddb | ||
|
|
75b1860702 | ||
|
|
807cd81699 | ||
|
|
834ea40510 | ||
|
|
46ef6044e2 | ||
|
|
dafc3545eb | ||
|
|
a6ec930f15 | ||
|
|
95d2f3a92f | ||
|
|
40de3fe510 | ||
|
|
920081014e | ||
|
|
94fe0a212f | ||
|
|
405ddd5315 | ||
|
|
34b4f1e86e | ||
|
|
e69b5532ab | ||
|
|
3e86452449 | ||
|
|
e73749579b | ||
|
|
588dee0e0c | ||
|
|
9c53c4ee46 | ||
|
|
5f22145df4 | ||
|
|
a73177515a | ||
|
|
ff86a64577 | ||
|
|
6f5e480fec | ||
|
|
3c5ff09888 | ||
|
|
1b9a56e014 | ||
|
|
aa9fe2a24e | ||
|
|
e188d9e260 | ||
|
|
3fa7bd1e72 | ||
|
|
c6368be701 | ||
|
|
dcecda6962 | ||
|
|
adc58e93d1 | ||
|
|
487154abbb | ||
|
|
0fe82c5882 | ||
|
|
0b6c7d0aff | ||
|
|
38dab4b641 | ||
|
|
2edf578499 | ||
|
|
70c6aba901 | ||
|
|
31f536bdf3 | ||
|
|
7601bfcb45 | ||
|
|
b5c33b7a09 | ||
|
|
4e8d0757df | ||
|
|
a76b6fc338 | ||
|
|
ef60c822df | ||
|
|
4aa7d7e9c1 | ||
|
|
03a2ba0bec | ||
|
|
4aaa823c8e | ||
|
|
e6b4e2d355 | ||
|
|
c90accef51 | ||
|
|
2346c5f6e0 | ||
|
|
2125d7117e | ||
|
|
ea08120e25 | ||
|
|
f8168e2a89 | ||
|
|
3431adedba | ||
|
|
733573feb3 | ||
|
|
c5a3ced8c8 | ||
|
|
6f2d68febf | ||
|
|
e8a7a374ee | ||
|
|
cf078b0954 | ||
|
|
b9f1942e8d | ||
|
|
2d6c328457 | ||
|
|
0e8f4f777d | ||
|
|
7e099863cd | ||
|
|
c41b47f7c2 | ||
|
|
6379772957 | ||
|
|
7298563b25 | ||
|
|
db2d59be22 | ||
|
|
e18ab66cca | ||
|
|
b8814b8148 | ||
|
|
e92147f52a | ||
|
|
3bbc772778 | ||
|
|
2b4a8535d9 | ||
|
|
2d6b40d669 | ||
|
|
b1d944060a | ||
|
|
e2b6431822 | ||
|
|
44d6ccfade | ||
|
|
dded469d60 | ||
|
|
1bfec0e7c9 | ||
|
|
327f5c327d | ||
|
|
163864158d | ||
|
|
c3c260dff7 | ||
|
|
fcdbed860e | ||
|
|
9dd7192d89 | ||
|
|
bd641d11de | ||
|
|
90a0ddeff0 | ||
|
|
e46e9516d9 | ||
|
|
1fe8e55c67 | ||
|
|
898f0a45c1 | ||
|
|
1bc7e904b8 | ||
|
|
166465b56d | ||
|
|
c2cffa3d1e | ||
|
|
cab1ff4076 | ||
|
|
d193a8eff5 | ||
|
|
e7dcb31aa7 | ||
|
|
5622cc6988 | ||
|
|
54a4dccd62 | ||
|
|
5b65230f64 | ||
|
|
04c9427a20 | ||
|
|
edbd234b82 | ||
|
|
143ec07556 | ||
|
|
048ff03577 | ||
|
|
e91560e8a1 | ||
|
|
020a91baa8 | ||
|
|
5f8d06ff55 | ||
|
|
0463d26066 | ||
|
|
4449d7b5fa | ||
|
|
e9a8781000 | ||
|
|
faf86a6ed7 | ||
|
|
4241f37b2c | ||
|
|
4e5b8ee8e8 | ||
|
|
03f1905bb9 | ||
|
|
99183d68ac | ||
|
|
95e3a6575f | ||
|
|
e9fae06abe | ||
|
|
1fcca39f6d | ||
|
|
625240d4ff | ||
|
|
1d34f85c63 | ||
|
|
74a7d8a68e | ||
|
|
93314a8d65 | ||
|
|
8e2cc78913 | ||
|
|
08cbc6f8d0 | ||
|
|
1172caba2b | ||
|
|
830e79e11c | ||
|
|
2aaa489371 | ||
|
|
6c426a3a17 | ||
|
|
55bd91a92a | ||
|
|
279221e0d6 | ||
|
|
0dd98e95f1 | ||
|
|
4ef7b3770f | ||
|
|
094782a41d | ||
|
|
d04cacbc98 | ||
|
|
5f4e019684 | ||
|
|
2bb8b1e73d | ||
|
|
e6eff86c98 | ||
|
|
141e702872 | ||
|
|
4db404ca50 | ||
|
|
2efa648f38 | ||
|
|
059d8ea25a | ||
|
|
3b9c8d4d22 | ||
|
|
462c99589a | ||
|
|
fccd32f9b2 | ||
|
|
be2a707315 | ||
|
|
21888b531f | ||
|
|
54a0cd0b22 | ||
|
|
1826f70682 | ||
|
|
42728e866b | ||
|
|
6e1527b153 | ||
|
|
c25fe60385 | ||
|
|
a880bf5105 | ||
|
|
c087d85b69 | ||
|
|
572f2a72b3 | ||
|
|
26e12290b5 | ||
|
|
eefd48e266 | ||
|
|
915d15cc32 | ||
|
|
25f2d3f313 | ||
|
|
f5e502aa88 | ||
|
|
849921caef | ||
|
|
8c51c26bf7 | ||
|
|
c136aefe20 | ||
|
|
bddd438a88 | ||
|
|
ee94e7fdf8 | ||
|
|
ea26d56b5a | ||
|
|
efa1bedd1f | ||
|
|
6cccd09a31 | ||
|
|
0b052a43b7 | ||
|
|
aa1534d2a3 | ||
|
|
9e72cd9b54 | ||
|
|
708cf1d32b | ||
|
|
1b63074d76 | ||
|
|
783be9bc2f | ||
|
|
3e37148923 | ||
|
|
183a7c3f1d | ||
|
|
2f62696400 | ||
|
|
63a7c44b7f | ||
|
|
88ef8dbd29 | ||
|
|
4d37797aa6 | ||
|
|
7e5810c550 | ||
|
|
ee23dd147b | ||
|
|
55037e9716 | ||
|
|
8874ee2048 | ||
|
|
f76d345e81 | ||
|
|
5a5e514fbd | ||
|
|
017abbba68 | ||
|
|
bb1fd3320e | ||
|
|
98a6f3bf64 | ||
|
|
e061af0b82 | ||
|
|
6578d1a4f9 | ||
|
|
b08f1ba966 | ||
|
|
34315ff651 | ||
|
|
c93d51ec54 | ||
|
|
c738289583 | ||
|
|
a0f457675d | ||
|
|
a787e5d7c9 | ||
|
|
9cca4ce881 | ||
|
|
91b09c1c85 | ||
|
|
fad71e8d6d | ||
|
|
3805787fe7 | ||
|
|
9bfd0800fe | ||
|
|
3c5c62cf8c | ||
|
|
0dcc90f3e4 | ||
|
|
36259f4582 | ||
|
|
b656a829c8 | ||
|
|
e43288dd77 | ||
|
|
b605f8fec8 | ||
|
|
318a963cc3 | ||
|
|
3f123af8a1 | ||
|
|
d134edc652 | ||
|
|
730b64249f | ||
|
|
526df2f696 | ||
|
|
9c2ab266a6 | ||
|
|
c65b088000 | ||
|
|
a96545b729 | ||
|
|
1608f7567e | ||
|
|
25e87d8558 | ||
|
|
66295e07b9 | ||
|
|
6a3c9552c5 | ||
|
|
0b82613d6f | ||
|
|
4d0d9f6fd0 | ||
|
|
34dbe0e952 | ||
|
|
9358823214 | ||
|
|
148b87f6fe | ||
|
|
9c75ea15c2 | ||
|
|
7e66fea502 | ||
|
|
38f0901d38 | ||
|
|
6df11f0f28 | ||
|
|
3a0db72c17 | ||
|
|
116bf7490d | ||
|
|
648448b46a | ||
|
|
ee63bf16da | ||
|
|
d3eeba7967 | ||
|
|
109020ed2d | ||
|
|
fd990007be | ||
|
|
e158f22d51 | ||
|
|
8f21eb17be | ||
|
|
aac31ae2fb | ||
|
|
b882b84e47 | ||
|
|
243085f047 | ||
|
|
df6f886390 | ||
|
|
6f21f84a71 | ||
|
|
393e0352e8 | ||
|
|
9459533408 | ||
|
|
d9f987df66 | ||
|
|
f97c1fb99b | ||
|
|
5cdddb2d46 | ||
|
|
e18a6e9eb0 | ||
|
|
c42b1cecf1 | ||
|
|
c940bed415 | ||
|
|
530f52dc9c | ||
|
|
cc1d977359 | ||
|
|
d2c7614c3a | ||
|
|
66d45de432 | ||
|
|
eaa861622a | ||
|
|
7c3b0fbee8 | ||
|
|
b98602705f | ||
|
|
b4d1e17765 | ||
|
|
4e93d9153e | ||
|
|
1001c837de | ||
|
|
18f54c08e2 | ||
|
|
5b7119ecef | ||
|
|
ddbd67cbde | ||
|
|
84ac771415 | ||
|
|
e792bb9705 | ||
|
|
7940a4c0e7 | ||
|
|
3d3159b37b | ||
|
|
0319c431de | ||
|
|
80ae9a50c9 | ||
|
|
006f1e6ec6 | ||
|
|
f185952832 | ||
|
|
d5c6a83aeb | ||
|
|
2e8e511106 | ||
|
|
ec591cdf47 | ||
|
|
ee7fa96f6d | ||
|
|
d625d18b91 | ||
|
|
895eae340a | ||
|
|
9fad66fcfb | ||
|
|
dfd01af9aa | ||
|
|
735e88deba | ||
|
|
bb2f6652b7 | ||
|
|
6dcd7dd34b | ||
|
|
1327af5077 | ||
|
|
311f64987b | ||
|
|
08e5b97e57 | ||
|
|
d4a2945fcc | ||
|
|
9913c03128 | ||
|
|
c0e28d3401 | ||
|
|
8c42e7980c | ||
|
|
01ecd4e011 | ||
|
|
33da1f7d14 | ||
|
|
79b0dd3287 | ||
|
|
0787a44d99 | ||
|
|
fd11286290 | ||
|
|
22dc864d60 | ||
|
|
9d2f46ce4e | ||
|
|
b1ed7bb3bc | ||
|
|
ca08f5fbcf | ||
|
|
229029e112 | ||
|
|
3294577c17 | ||
|
|
8f51efbc3e | ||
|
|
ebdbb3cbd5 | ||
|
|
3a30bec9ef | ||
|
|
126b81916d | ||
|
|
9abea5b73b | ||
|
|
170e7aaeb9 | ||
|
|
5b570a2451 | ||
|
|
ffd7dddcc7 | ||
|
|
b0b13d3a16 | ||
|
|
b1781e4606 | ||
|
|
3580374b18 | ||
|
|
c6dd39bd83 | ||
|
|
dd8fe86308 | ||
|
|
2e12f05d5e | ||
|
|
bd033a4447 | ||
|
|
9d023cf77a | ||
|
|
8a69dbd9d4 | ||
|
|
147de6f4f7 | ||
|
|
e217e44661 | ||
|
|
871ef6b506 | ||
|
|
fac72f024a | ||
|
|
d4c332104d | ||
|
|
5af5df4e4f | ||
|
|
6c5cf87871 | ||
|
|
46f4108f23 | ||
|
|
1da439f8dc | ||
|
|
7af9d039fb | ||
|
|
de84624d70 | ||
|
|
cac121a67d | ||
|
|
604da878cb | ||
|
|
eaf0051162 | ||
|
|
8dcf71e02d | ||
|
|
92f0142a3e | ||
|
|
6c8347f465 | ||
|
|
a4d24056ef |
+34
-29
@@ -1,7 +1,30 @@
|
||||
version: 2
|
||||
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
|
||||
|
||||
jobs:
|
||||
osx-python3.12:
|
||||
osx-python312:
|
||||
macos:
|
||||
xcode: 15.4.0
|
||||
resource_class: macos.m1.medium.gen1
|
||||
@@ -9,16 +32,10 @@ jobs:
|
||||
PYTHON: python3
|
||||
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
|
||||
|
||||
linux-python3.12:
|
||||
linux-python312:
|
||||
docker:
|
||||
- image: cimg/python:3.12
|
||||
environment:
|
||||
@@ -29,14 +46,8 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
|
||||
- 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
|
||||
- cibw_prepare_environment
|
||||
- cibw_run_tests
|
||||
|
||||
linux-aarch64:
|
||||
machine:
|
||||
@@ -49,19 +60,13 @@ jobs:
|
||||
PYTEST_ADDOPTS: -k "unit_test or main_tests or test_0_basic or test_docker_images"
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- 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
|
||||
- cibw_prepare_environment
|
||||
- cibw_run_tests
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
all-tests:
|
||||
jobs:
|
||||
- osx-python3.12
|
||||
- linux-python3.12
|
||||
- osx-python312
|
||||
- linux-python312
|
||||
- linux-aarch64
|
||||
|
||||
@@ -4,11 +4,13 @@ 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 ".[dev]"
|
||||
venv/bin/python -m pip install -e. --group test
|
||||
venv/bin/python -m pip freeze
|
||||
venv/bin/python --version
|
||||
|
||||
+37
-20
@@ -1,12 +1,12 @@
|
||||
only_if: changesInclude('.cirrus.yml') || ($CIRRUS_BRANCH == "main" && !changesIncludeOnly('docs/*', '.pre-commit-config.yaml')) || $CIRRUS_BRANCH =~ 'cirrus.*'
|
||||
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.*'
|
||||
|
||||
run_tests: &RUN_TESTS
|
||||
install_cibuildwheel_script:
|
||||
- python -m pip install -e ".[dev]" pytest-custom-exit-code
|
||||
- python -m pip install -U pip
|
||||
- python -m pip install -e. --group test
|
||||
run_cibuildwheel_tests_script:
|
||||
- python ./bin/run_tests.py
|
||||
|
||||
|
||||
linux_x86_task:
|
||||
timeout_in: 120m
|
||||
compute_engine_instance:
|
||||
@@ -15,9 +15,15 @@ linux_x86_task:
|
||||
platform: linux
|
||||
cpu: 8
|
||||
memory: 8G
|
||||
|
||||
env:
|
||||
VENV_ROOT: ${HOME}/venv-cibuildwheel
|
||||
PATH: ${VENV_ROOT}/bin:${PATH}
|
||||
install_pre_requirements_script:
|
||||
- apt install -y python3-venv python-is-python3
|
||||
- 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}
|
||||
<<: *RUN_TESTS
|
||||
|
||||
linux_aarch64_task:
|
||||
@@ -28,14 +34,20 @@ linux_aarch64_task:
|
||||
platform: linux
|
||||
cpu: 4
|
||||
memory: 4G
|
||||
|
||||
env:
|
||||
VENV_ROOT: ${HOME}/venv-cibuildwheel
|
||||
PATH: ${VENV_ROOT}/bin:${PATH}
|
||||
install_pre_requirements_script:
|
||||
- apt install -y python3-venv python-is-python3
|
||||
- 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}
|
||||
<<: *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
|
||||
@@ -43,30 +55,35 @@ windows_x86_task:
|
||||
memory: 8G
|
||||
|
||||
install_pre_requirements_script:
|
||||
- choco install -y --no-progress python3 --version 3.10.6
|
||||
- certutil -generateSSTFromWU roots.sst
|
||||
- certutil -addstore -f root roots.sst
|
||||
- del roots.sst
|
||||
- choco install -y --no-progress python3 --version 3.12.4
|
||||
- refreshenv
|
||||
- echo PATH=%PATH% >> "%CIRRUS_ENV%"
|
||||
- powershell -Command "$cleanPath = $env:PATH -replace ';+$', ''; Add-Content -Path $env:CIRRUS_ENV -Value ('PATH=' + $cleanPath)"
|
||||
<<: *RUN_TESTS
|
||||
|
||||
macos_arm64_task:
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-sonoma-xcode
|
||||
|
||||
image: ghcr.io/cirruslabs/macos-runner:sequoia
|
||||
env:
|
||||
PATH: /opt/homebrew/opt/python@3.10/libexec/bin:$PATH
|
||||
VENV_ROOT: ${HOME}/venv-cibuildwheel
|
||||
PATH: ${VENV_ROOT}/bin:${PATH}
|
||||
install_pre_requirements_script:
|
||||
- brew install python@3.10
|
||||
- brew install python@3.12
|
||||
- python3.12 -m venv ${VENV_ROOT}
|
||||
<<: *RUN_TESTS
|
||||
|
||||
macos_arm64_cp38_task:
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-sonoma-xcode
|
||||
|
||||
image: ghcr.io/cirruslabs/macos-runner:sequoia
|
||||
env:
|
||||
PATH: /opt/homebrew/opt/python@3.10/libexec/bin:$PATH
|
||||
PYTEST_ADDOPTS: --run-cp38-universal2 -k 'test_cp38_arm64_testing_universal2_installer or test_arch_auto'
|
||||
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'
|
||||
install_pre_requirements_script:
|
||||
- brew install python@3.10
|
||||
- brew install python@3.12
|
||||
- python3.12 -m venv ${VENV_ROOT}
|
||||
- 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
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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
|
||||
@@ -11,18 +11,9 @@ jobs:
|
||||
dist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- 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/*
|
||||
- uses: hynek/build-and-inspect-python-package@v2
|
||||
|
||||
publish:
|
||||
needs: [dist]
|
||||
@@ -36,15 +27,16 @@ jobs:
|
||||
attestations: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: cibw-*
|
||||
name: Packages
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate artifact attestation for sdist and wheel
|
||||
uses: actions/attest-build-provenance@173725a1209d09b31f9d30a3890cf2757ebbff0d # v1.1.2
|
||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||
with:
|
||||
subject-path: "dist/cibuildwheel-*"
|
||||
|
||||
- uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
attestations: true
|
||||
|
||||
+139
-57
@@ -4,10 +4,27 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 2.x
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- labeled
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- .ci*
|
||||
- bin/*
|
||||
- docs/**
|
||||
- examples/azure-pipelines-*
|
||||
- examples/ci*
|
||||
- examples/travis-ci-*
|
||||
- .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
|
||||
|
||||
@@ -17,65 +34,120 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Linters (mypy, flake8, etc.)
|
||||
name: Linters (mypy, ruff, etc.)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
id: python
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
- name: Check manifest
|
||||
run: pipx run nox -s check_manifest
|
||||
- name: PyLint checks
|
||||
run: pipx run nox -s pylint -- --output-format=github
|
||||
run: pipx run --python "${{ steps.python.outputs.python-path }}" nox -s pylint -- --output-format=github
|
||||
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (${{ matrix.python_version }})
|
||||
name: Test on ${{ matrix.os }} (${{ matrix.python_version }}) ${{ matrix.test_select }}
|
||||
needs: lint
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-13, macos-14]
|
||||
python_version: ['3.12']
|
||||
include:
|
||||
# Min Python
|
||||
- os: ubuntu-latest
|
||||
python_version: '3.8'
|
||||
python_version: '3.11'
|
||||
# Max Python
|
||||
- os: ubuntu-latest
|
||||
python_version: '3.14'
|
||||
- os: ubuntu-latest
|
||||
python_version: '3.13'
|
||||
test_select: android
|
||||
- os: ubuntu-24.04-arm
|
||||
python_version: '3.13'
|
||||
- os: windows-latest
|
||||
python_version: '3.13'
|
||||
- os: windows-11-arm
|
||||
python_version: '3.13'
|
||||
- os: macos-15-intel
|
||||
python_version: '3.13'
|
||||
- os: macos-15
|
||||
python_version: '3.13'
|
||||
- os: macos-15-intel
|
||||
python_version: '3.13'
|
||||
test_select: ios
|
||||
- os: macos-15
|
||||
python_version: '3.13'
|
||||
test_select: ios
|
||||
# Exercise iOS on a non-default simulator.
|
||||
test_runtime: 'args: --simulator "iPhone 16e,OS=18.5"'
|
||||
- os: macos-15-intel
|
||||
python_version: '3.13'
|
||||
test_select: android
|
||||
# Exercise Android on a non-default simulator
|
||||
test_runtime: 'args: --managed minVersion'
|
||||
- os: macos-15
|
||||
python_version: '3.13'
|
||||
test_select: android
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
name: Install Python ${{ matrix.python_version }}
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
allow-prereleases: true
|
||||
|
||||
- uses: yezz123/setup-uv@v4
|
||||
- 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
|
||||
|
||||
# free some space to prevent reaching GHA disk space limits
|
||||
- name: Clean docker images
|
||||
- name: Free up disk space
|
||||
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 pip install --system ".[test]"
|
||||
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
|
||||
|
||||
- name: Generate a sample project
|
||||
run: |
|
||||
python -m test.test_projects test.test_0_basic.basic_project sample_proj
|
||||
uv run --no-sync -m test.test_projects test.test_0_basic.basic_project sample_proj
|
||||
|
||||
- name: Run a sample build (GitHub Action)
|
||||
uses: ./
|
||||
@@ -84,51 +156,62 @@ jobs:
|
||||
output-dir: wheelhouse
|
||||
env:
|
||||
CIBW_ARCHS_MACOS: x86_64 universal2 arm64
|
||||
CIBW_BUILD_FRONTEND: 'build[uv]'
|
||||
CIBW_FREE_THREADED_SUPPORT: 1
|
||||
CIBW_PRERELEASE_PYTHONS: 1
|
||||
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: cp312-${{ runner.os == 'Linux' && 'manylinux_x86_64' || (runner.os == 'Windows' && 'win_amd64' || 'macosx_x86_64') }}
|
||||
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.12 on native arch
|
||||
# Only build on CPython 3.13 on native arch
|
||||
archs = ["native"]
|
||||
build = "cp312-*"
|
||||
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 artefacts
|
||||
- name: Check Action artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
test $(find wheelhouse -name '*.whl' | wc -l) -ge 1
|
||||
test $(find wheelhouse_only -name '*.whl' | wc -l) -eq 1
|
||||
test $(find wheelhouse_config_file -name '*.whl' | wc -l) -eq 1
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- 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
|
||||
with:
|
||||
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
|
||||
path: wheelhouse/*.whl
|
||||
|
||||
- name: Test cibuildwheel
|
||||
env:
|
||||
CIBW_TEST_RUNTIME: ${{ matrix.test_runtime }}
|
||||
run: |
|
||||
python ./bin/run_tests.py --run-podman
|
||||
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
|
||||
@@ -137,16 +220,17 @@ jobs:
|
||||
outputs:
|
||||
archs: ${{ steps.archs.outputs.archs }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- name: Install dependencies
|
||||
run: python -m pip install ".[test]"
|
||||
run: uv sync --no-dev --group test
|
||||
- name: Get qemu emulated architectures
|
||||
id: archs
|
||||
run: |
|
||||
OUTPUT=$(python -c "from json import dumps; from test.utils import EMULATED_ARCHS; print(dumps(EMULATED_ARCHS))")
|
||||
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"
|
||||
|
||||
@@ -159,41 +243,39 @@ jobs:
|
||||
matrix:
|
||||
arch: ${{ fromJSON(needs.emulated-archs.outputs.archs) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- name: Install dependencies
|
||||
run: python -m pip install ".[test,uv]"
|
||||
run: uv sync --no-dev --group test
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Run the emulation tests
|
||||
run: pytest --run-emulation ${{ matrix.arch }} test/test_emulation.py
|
||||
run: uv run --no-sync pytest --run-emulation ${{ matrix.arch }} test/test_emulation.py
|
||||
|
||||
test-pyodide:
|
||||
name: Test cibuildwheel building pyodide wheels
|
||||
name: Test pyodide
|
||||
needs: lint
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- 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: |
|
||||
python -m pip install ".[test]"
|
||||
run: uv sync --no-dev --group test
|
||||
|
||||
- name: Generate a sample project
|
||||
run: |
|
||||
python -m test.test_projects test.test_0_basic.basic_project sample_proj
|
||||
uv run --no-sync -m test.test_projects test.test_0_basic.basic_project sample_proj
|
||||
|
||||
- name: Run a sample build (GitHub Action)
|
||||
uses: ./
|
||||
@@ -205,6 +287,6 @@ jobs:
|
||||
|
||||
- name: Run tests with 'CIBW_PLATFORM' set to 'pyodide'
|
||||
run: |
|
||||
python ./bin/run_tests.py
|
||||
uv run --no-sync ./bin/run_tests.py
|
||||
env:
|
||||
CIBW_PLATFORM: pyodide
|
||||
|
||||
@@ -20,11 +20,19 @@ jobs:
|
||||
if: github.repository_owner == 'pypa' || github.event_name != 'schedule'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: wntrblm/nox@2024.04.15
|
||||
# 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'
|
||||
with:
|
||||
python-versions: "3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13"
|
||||
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
|
||||
|
||||
- name: "Run update: dependencies"
|
||||
run: nox --force-color -s update_constraints
|
||||
@@ -33,18 +41,9 @@ 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@v6
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
commit-message: Update dependencies
|
||||
title: '[Bot] Update dependencies'
|
||||
@@ -53,7 +52,10 @@ jobs:
|
||||
|
||||
PR generated by "Update dependencies" [workflow](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}).
|
||||
branch: update-dependencies-pr
|
||||
committer: "cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>"
|
||||
author: "cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>"
|
||||
sign-commits: true
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
delete-branch: true
|
||||
labels: |
|
||||
CI: GraalPy
|
||||
CI: PyPy
|
||||
dependencies
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Update the ${{ env.TAG_NAME }} tag
|
||||
id: update-major-minor-tag
|
||||
|
||||
+1
-6
@@ -84,9 +84,7 @@ celerybeat-schedule
|
||||
|
||||
# virtualenv
|
||||
.venv
|
||||
venv/
|
||||
venv3/
|
||||
venv2/
|
||||
venv*/
|
||||
ENV/
|
||||
env/
|
||||
env2/
|
||||
@@ -112,8 +110,5 @@ all_known_setup.yaml
|
||||
# mkdocs
|
||||
site/
|
||||
|
||||
# Virtual environments
|
||||
venv*
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
+20
-4
@@ -13,9 +13,15 @@ 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
|
||||
- python -m pip install -e ".[dev]" pytest-custom-exit-code
|
||||
- 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 ./bin/run_tests.py
|
||||
|
||||
windows:
|
||||
@@ -23,9 +29,14 @@ windows:
|
||||
variables:
|
||||
PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
|
||||
before_script:
|
||||
- choco install python -y --version 3.12.4
|
||||
- choco install python -y --allow-downgrade --version 3.12.4
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
variables:
|
||||
CIBW_ENABLE: "all"
|
||||
script:
|
||||
- py -m pip install -e ".[dev]" pytest-custom-exit-code
|
||||
- py -m pip install -U pip
|
||||
- py -m pip install -e. pytest-custom-exit-code --group test
|
||||
- py bin\run_tests.py
|
||||
tags:
|
||||
- saas-windows-medium-amd64
|
||||
@@ -34,8 +45,13 @@ 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 -e ".[dev]" pytest-custom-exit-code
|
||||
- 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
|
||||
|
||||
+30
-23
@@ -1,7 +1,7 @@
|
||||
repos:
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
@@ -14,42 +14,47 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.0
|
||||
rev: v0.14.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-check
|
||||
args: ["--fix", "--show-fixes"]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.10.1
|
||||
rev: v1.18.2
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy 3.8 on cibuildwheel/
|
||||
exclude: ^cibuildwheel/resources/.*py|bin/generate_schema.py$
|
||||
args: ["--python-version=3.8"]
|
||||
name: mypy 3.11 on cibuildwheel/
|
||||
args: ["--python-version=3.11"]
|
||||
exclude: ^cibuildwheel/resources/_cross_venv.py$ # Requires Python 3.13 or later
|
||||
additional_dependencies: &mypy-dependencies
|
||||
- bracex
|
||||
- nox
|
||||
- build
|
||||
- dependency-groups>=1.2
|
||||
- humanize
|
||||
- nox>=2025.2.9
|
||||
- orjson
|
||||
- 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.12
|
||||
exclude: ^cibuildwheel/resources/.*py$
|
||||
args: ["--python-version=3.12"]
|
||||
name: mypy 3.13
|
||||
args: ["--python-version=3.13"]
|
||||
additional_dependencies: *mypy-dependencies
|
||||
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: v0.10.0.1
|
||||
rev: v0.11.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
|
||||
@@ -59,31 +64,33 @@ repos:
|
||||
name: Disallow improper capitalization
|
||||
language: pygrep
|
||||
entry: PyBind|Numpy|Cmake|Github|PyTest
|
||||
types:
|
||||
- markdown
|
||||
types: [markdown]
|
||||
exclude: ^docs/working-examples\.md$ # Autogenerated
|
||||
- id: update-readme-changelog
|
||||
name: Update README changelog
|
||||
- id: cog
|
||||
name: Cog the README
|
||||
language: python
|
||||
entry: bin/update_readme_changelog.py
|
||||
files: ^docs/changelog.md$
|
||||
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]
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
args: ["-L", "sur,assertin", "-w"]
|
||||
args: ["-w"]
|
||||
exclude: ^docs/working-examples\.md$ # Autogenerated
|
||||
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.28.6
|
||||
rev: 0.34.1
|
||||
hooks:
|
||||
- id: check-dependabot
|
||||
- id: check-github-actions
|
||||
- id: check-github-workflows
|
||||
- id: check-gitlab-ci
|
||||
- id: check-readthedocs
|
||||
# Disabled due to schema issue for now:
|
||||
# - id: check-readthedocs
|
||||
- id: check-travis
|
||||
- id: check-jsonschema
|
||||
name: Check projects
|
||||
|
||||
+2
-6
@@ -3,13 +3,9 @@
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
os: ubuntu-24.04
|
||||
commands:
|
||||
- asdf plugin add uv
|
||||
- asdf install uv latest
|
||||
- asdf global uv latest
|
||||
- uv venv
|
||||
- uv pip install -e.[docs]
|
||||
- NO_COLOR=1 .venv/bin/mkdocs build --strict --site-dir $READTHEDOCS_OUTPUT/html
|
||||
- NO_COLOR=1 uv run --python 3.14 --managed-python --no-dev --group docs mkdocs build --strict --site-dir $READTHEDOCS_OUTPUT/html
|
||||
|
||||
+39
-20
@@ -1,28 +1,34 @@
|
||||
os: linux
|
||||
dist: focal
|
||||
dist: jammy
|
||||
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.9
|
||||
python: 3.9
|
||||
- name: Linux | x86_64 + i686 | Python 3.12
|
||||
python: 3.12
|
||||
services: docker
|
||||
env: PYTHON=python
|
||||
env:
|
||||
- PYTHON=python
|
||||
# a build using CIBW_ENABLE=all does not fit into Travis' time limit,
|
||||
# so only the defaults are tested
|
||||
|
||||
- name: Linux | arm64 | Python 3.9
|
||||
python: 3.9
|
||||
- name: Linux | arm64 | Python 3.12
|
||||
python: 3.12
|
||||
services: docker
|
||||
arch: arm64-graviton2
|
||||
group: edge
|
||||
virt: vm
|
||||
env: PYTHON=python
|
||||
arch: arm64
|
||||
env:
|
||||
- PYTHON=python
|
||||
- CIBW_ENABLE=all
|
||||
|
||||
- name: Linux | ppc64le | Python 3.9
|
||||
python: 3.9
|
||||
- name: Linux | ppc64le | Python 3.12
|
||||
python: 3.12
|
||||
services: docker
|
||||
arch: ppc64le
|
||||
allow_failure: True
|
||||
@@ -31,24 +37,35 @@ 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.9
|
||||
- name: Windows | x86_64 | Python 3.12
|
||||
os: windows
|
||||
language: shell
|
||||
before_install:
|
||||
- choco upgrade python3 -y --version 3.9.13 --limit-output --params "/InstallDir:C:\\Python39"
|
||||
# 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"
|
||||
env:
|
||||
- PYTHON=C:\\Python39\\python
|
||||
- PYTHON=C:\\Python312\\python
|
||||
# a build using CIBW_ENABLE=all does not fit into Travis' time limit,
|
||||
# so only the defaults are tested
|
||||
|
||||
- name: Linux | s390x | Python 3.9
|
||||
python: 3.9
|
||||
- name: Linux | s390x | Python 3.12
|
||||
python: 3.12
|
||||
services: docker
|
||||
arch: s390x
|
||||
env: PYTHON=python
|
||||
allow_failure: True
|
||||
env:
|
||||
- PYTHON=python
|
||||
- CIBW_ENABLE=all
|
||||
|
||||
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 ".[dev]" pytest-custom-exit-code
|
||||
- $PYTHON -m pip install -e. --group test
|
||||
|
||||
script: |
|
||||
# travis_wait disable the output while waiting
|
||||
@@ -56,5 +73,7 @@ script: |
|
||||
(while true; do echo "travis_keep_alive"; sleep 300; done) &
|
||||
SPINNER_PID=$!
|
||||
disown
|
||||
$PYTHON ./bin/run_tests.py --num-processes 2
|
||||
result=0
|
||||
$PYTHON ./bin/run_tests.py --num-processes 2 || result=1
|
||||
kill -9 ${SPINNER_PID}
|
||||
test ${result} -eq 0
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
This is a summary of the host Python versions and platforms covered by the different CI platforms:
|
||||
|
||||
| | 3.8 | 3.9 | 3.10 | 3.11 | 3.12 |
|
||||
|---------|----------------------------------------------|-----------|-----------|---------|--------------------------------------|
|
||||
| Linux | AppVeyor¹ / Azure Pipelines / GitHub Actions | Travis CI | Cirrus CI | | CircleCI¹ / GitHub Actions / GitLab¹ |
|
||||
| macOS | AppVeyor¹ / Azure Pipelines | | Cirrus CI | GitLab¹ | CircleCI¹ / GitHub Actions |
|
||||
| Windows | AppVeyor¹ / Azure Pipelines | Travis CI | Cirrus CI | | GitHub Actions / GitLab¹ |
|
||||
| | 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 |
|
||||
|
||||
> ¹ Runs a reduced set of tests to reduce CI load
|
||||
|
||||
Non-x86 architectures are covered on Travis CI using Python 3.9.
|
||||
Non-x86 architectures are covered on Travis CI using Python 3.12.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
This project is licensed under the 'BSD 2-clause license'.
|
||||
|
||||
Copyright (c) 2017-2023, Joe Rickerby and contributors. All rights reserved.
|
||||
Copyright (c) 2017-2025, 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:
|
||||
|
||||
@@ -4,8 +4,7 @@ cibuildwheel
|
||||
[](https://pypi.python.org/pypi/cibuildwheel)
|
||||
[](https://cibuildwheel.pypa.io/en/stable/?badge=stable)
|
||||
[](https://github.com/pypa/cibuildwheel/actions)
|
||||
[](https://travis-ci.com/pypa/cibuildwheel)
|
||||
[](https://ci.appveyor.com/project/joerick/cibuildwheel/branch/main)
|
||||
[](https://travis-ci.com/github/pypa/cibuildwheel)
|
||||
[](https://circleci.com/gh/pypa/cibuildwheel)
|
||||
[](https://dev.azure.com/joerick0429/cibuildwheel/_build/latest?definitionId=4&branchName=main)
|
||||
|
||||
@@ -16,34 +15,38 @@ cibuildwheel
|
||||
|
||||
Python wheels are great. Building them across **Mac, Linux, Windows**, on **multiple versions of Python**, is not.
|
||||
|
||||
`cibuildwheel` is here to help. `cibuildwheel` runs on your CI server - currently it supports GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, 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, CircleCI, and GitLab CI - and it builds and tests your wheels across all of your platforms.
|
||||
|
||||
|
||||
What does it do?
|
||||
----------------
|
||||
|
||||
| | 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 | Pyodide |
|
||||
|----------------|----|-----|-----|-----|-----|----|-----|----|-----|-----|-----|
|
||||
| CPython 3.6 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | N/A |
|
||||
| CPython 3.7 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | N/A |
|
||||
| CPython 3.8 | ✅ | ✅ | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | N/A |
|
||||
| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | N/A |
|
||||
| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | N/A |
|
||||
| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | N/A |
|
||||
| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁴ |
|
||||
| CPython 3.13³ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | N/A |
|
||||
| PyPy 3.7 v7.3 | ✅ | N/A | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A |
|
||||
| PyPy 3.8 v7.3 | ✅ | ✅ | ✅ | 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 |
|
||||
| PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A |
|
||||
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:
|
||||
|
||||
<sup>¹ PyPy is only supported for manylinux wheels.</sup><br>
|
||||
| | 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>² Windows arm64 support is experimental.</sup><br>
|
||||
<sup>³ CPython 3.13 is available using the [`CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.pypa.io/en/stable/options/#prerelease-pythons) option. Free-threaded mode requires opt-in.</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 10.9+, and Windows wheels for CPython and PyPy
|
||||
- Works on GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, GitLab CI, and Cirrus CI
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -54,18 +57,19 @@ 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 |
|
||||
|-----------------|-------|-------|---------|-----------|-----------|-------------|
|
||||
| GitHub Actions | ✅ | ✅ | ✅ | ✅¹ | ✅ | ✅² |
|
||||
| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² |
|
||||
| Travis CI | ✅ | | ✅ | ✅ | | |
|
||||
| AppVeyor | ✅ | ✅ | ✅ | | ✅ | ✅² |
|
||||
| CircleCI | ✅ | ✅ | | ✅ | ✅ | |
|
||||
| Gitlab CI | ✅ | ✅ | ✅ | ✅¹ | ✅ | |
|
||||
| Cirrus CI | ✅ | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM | Android | iOS |
|
||||
|-----------------|-------|-------|---------|-----------|-----------|-------------|---------|-----|
|
||||
| GitHub Actions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅⁴ | ✅³ |
|
||||
| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² | ✅⁴ | ✅³ |
|
||||
| Travis CI | ✅ | | ✅ | ✅ | | | ✅⁴ | |
|
||||
| CircleCI | ✅ | ✅ | | ✅ | ✅ | | ✅⁴ | ✅³ |
|
||||
| Gitlab CI | ✅ | ✅ | ✅ | ✅¹ | ✅ | | ✅⁴ | ✅³ |
|
||||
| Cirrus CI | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅⁴ | |
|
||||
|
||||
<sup>¹ [Requires emulation](https://cibuildwheel.pypa.io/en/stable/faq/#emulation), distributed separately. Other services may also support Linux ARM through emulation or third-party build hosts, but these are not tested in our CI.</sup><br>
|
||||
<sup>² [Uses cross-compilation](https://cibuildwheel.pypa.io/en/stable/faq/#windows-arm64). It is not possible to test `arm64` on this CI platform.</sup>
|
||||
<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>
|
||||
|
||||
<!--intro-end-->
|
||||
|
||||
@@ -74,6 +78,7 @@ 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
|
||||
|
||||
@@ -85,28 +90,30 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-13, macos-14]
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-15-intel, macos-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
# Used to host cibuildwheel
|
||||
- uses: actions/setup-python@v5
|
||||
|
||||
- name: Install cibuildwheel
|
||||
run: python -m pip install cibuildwheel==2.19.2
|
||||
run: python -m pip install cibuildwheel==3.3.0
|
||||
|
||||
- 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).
|
||||
|
||||
@@ -119,33 +126,48 @@ The following diagram summarises the steps that cibuildwheel takes on each platf
|
||||
|
||||
<sup>Explore an interactive version of this diagram [in the docs](https://cibuildwheel.pypa.io/en/stable/#how-it-works).</sup>
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
<!--[[[cog from readme_options_table import get_table; print(get_table()) ]]]-->
|
||||
|
||||
<!-- This table is auto-generated from docs/options.md by bin/readme_options_table.py -->
|
||||
|
||||
| | Option | Description |
|
||||
|---|--------|-------------|
|
||||
| **Build selection** | [`CIBW_PLATFORM`](https://cibuildwheel.pypa.io/en/stable/options/#platform) | Override the auto-detected target platform |
|
||||
| | [`CIBW_BUILD`](https://cibuildwheel.pypa.io/en/stable/options/#build-skip) <br> [`CIBW_SKIP`](https://cibuildwheel.pypa.io/en/stable/options/#build-skip) | Choose the Python versions to build |
|
||||
| | [`CIBW_ARCHS`](https://cibuildwheel.pypa.io/en/stable/options/#archs) | Change the architectures built on your machine by default. |
|
||||
| | [`CIBW_PROJECT_REQUIRES_PYTHON`](https://cibuildwheel.pypa.io/en/stable/options/#requires-python) | Manually set the Python compatibility of your project |
|
||||
| | [`CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.pypa.io/en/stable/options/#prerelease-pythons) | Enable building with pre-release versions of Python if available |
|
||||
| **Build customization** | [`CIBW_BUILD_FRONTEND`](https://cibuildwheel.pypa.io/en/stable/options/#build-frontend) | Set the tool to use to build, either "pip" (default for now) or "build" |
|
||||
| | [`CIBW_ENVIRONMENT`](https://cibuildwheel.pypa.io/en/stable/options/#environment) | Set environment variables needed during the build |
|
||||
| | [`CIBW_ENVIRONMENT_PASS_LINUX`](https://cibuildwheel.pypa.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.pypa.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
|
||||
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.pypa.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
|
||||
| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.pypa.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.pypa.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images |
|
||||
| | [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.pypa.io/en/stable/options/#container-engine) | Specify which container engine to use when building Linux wheels |
|
||||
| | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses |
|
||||
| **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.pypa.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel |
|
||||
| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.pypa.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
|
||||
| | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.pypa.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
|
||||
| | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.pypa.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require |
|
||||
| | [`CIBW_TEST_SKIP`](https://cibuildwheel.pypa.io/en/stable/options/#test-skip) | Skip running tests on some builds |
|
||||
| **Other** | [`CIBW_BUILD_VERBOSITY`](https://cibuildwheel.pypa.io/en/stable/options/#build-verbosity) | Increase/decrease the output of pip wheel |
|
||||
|---|---|---|
|
||||
| **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 |
|
||||
|
||||
These options can be specified in a pyproject.toml file, as well; see [configuration](https://cibuildwheel.pypa.io/en/stable/options/#configuration).
|
||||
|
||||
<!--[[[end]]] (sum: dbfwOkj/k/) -->
|
||||
|
||||
These options can be specified in a pyproject.toml file, or as environment variables, see [configuration docs](https://cibuildwheel.pypa.io/en/latest/configuration/).
|
||||
|
||||
Working examples
|
||||
----------------
|
||||
@@ -159,28 +181,27 @@ 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. |
|
||||
| [duckdb][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | DuckDB is an analytical in-process SQL database management system |
|
||||
| [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 |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [Kivy][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Open source UI framework written in Python, running on Windows, Linux, macOS, Android and iOS |
|
||||
|
||||
[scikit-learn]: https://github.com/scikit-learn/scikit-learn
|
||||
[duckdb]: https://github.com/duckdb/duckdb
|
||||
[pytorch-fairseq]: https://github.com/facebookresearch/fairseq
|
||||
[NumPy]: https://github.com/numpy/numpy
|
||||
[Tornado]: https://github.com/tornadoweb/tornado
|
||||
[duckdb]: https://github.com/duckdb/duckdb
|
||||
[NCNN]: https://github.com/Tencent/ncnn
|
||||
[Matplotlib]: https://github.com/matplotlib/matplotlib
|
||||
[Prophet]: https://github.com/facebook/prophet
|
||||
[MyPy]: https://github.com/mypyc/mypy_mypyc-wheels
|
||||
[Prophet]: https://github.com/facebook/prophet
|
||||
[Kivy]: https://github.com/kivy/kivy
|
||||
|
||||
[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
|
||||
@@ -207,79 +228,69 @@ This is similar to static linking, so it might have some license implications. C
|
||||
Changelog
|
||||
=========
|
||||
|
||||
<!-- START bin/update_readme_changelog.py -->
|
||||
<!-- [[[cog from readme_changelog import mini_changelog; print(mini_changelog()) ]]] -->
|
||||
|
||||
<!-- this section was generated by bin/update_readme_changelog.py -- do not edit manually -->
|
||||
### v3.3.0
|
||||
|
||||
### v2.19.2
|
||||
_12 November 2025_
|
||||
|
||||
- 🐛 Update manylinux2014 pins to versions that support past-EoL CentOS 7 mirrors. (#1917)
|
||||
- 🐛 Support `--no-isolation` with `build[uv]` build-frontend. (#1889)
|
||||
- 🛠 Provide attestations for releases at <https://github.com/pypa/cibuildwheel/attestations>. (#1916)
|
||||
- 🛠 Provide CPython 3.13.0b3. (#1913)
|
||||
- 🛠 Remove some workarounds now that pip 21.1 is available. (#1891, #1892)
|
||||
- 📚 Remove nosetest from our docs. (#1821)
|
||||
- 📚 Document the macOS ARM workaround for 3.8 on GHA. (#1971)
|
||||
- 📚 GitLab CI + macOS is now a supported platform with an example. (#1911)
|
||||
- 🐛 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)
|
||||
|
||||
### v3.2.1
|
||||
|
||||
_12 October 2025_
|
||||
|
||||
- 🛠 Update to CPython 3.14.0 final (#2614)
|
||||
- 🐛 Fix the default MACOSX_DEPLOYMENT_TARGET on Python 3.14 (#2613)
|
||||
- 📚 Docs improvements (#2617)
|
||||
|
||||
### v3.2.0
|
||||
|
||||
_22 September 2025_
|
||||
|
||||
- ✨ 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)
|
||||
|
||||
### v3.1.4
|
||||
|
||||
_19 August 2025_
|
||||
|
||||
- ✨ 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)
|
||||
|
||||
|
||||
### v2.19.1
|
||||
### v3.1.3
|
||||
|
||||
- 🐛 Don't require setup-python on GHA for Pyodide (#1868)
|
||||
- 🐛 Specify full python path for uv (fixes issue in 0.2.10 & 0.2.11) (#1881)
|
||||
- 🛠 Update for pip 24.1b2 on CPython 3.13. (#1879)
|
||||
- 🛠 Fix a warning in our schema generation script. (#1866)
|
||||
- 🛠 Cleaner output on pytest 8-8.2. (#1865)
|
||||
_1 August 2025_
|
||||
|
||||
- 🐛 Fix bug where "latest" dependencies couldn't update to pip 25.2 on Windows (#2537)
|
||||
- 🧪 Use pytest-rerunfailures to improve some of our iOS/Android tests (#2527, #2539)
|
||||
- 🧪 Remove some GraalPy Windows workarounds in our tests (#2501)
|
||||
|
||||
### v2.19.0
|
||||
|
||||
See the [release post](https://iscinumpy.dev/post/cibuildwheel-2-19-0/) for more info on new features!
|
||||
|
||||
- 🌟 Add Pyodide platform. Set with `--platform pyodide` or `CIBW_PLATFORM: pyodide` on Linux with a host Python 3.12 to build WebAssembly wheels. Not accepted on PyPI currently, but usable directly in a website using Pyodide, for live docs, etc. (#1456, #1859)
|
||||
- 🌟 Add `build[uv]` backend, which will take a pre-existing uv install (or install `cibuildwheel[uv]`) and use `uv` for all environment setup and installs on Python 3.8+. This is significantly faster in most cases. (#1856)
|
||||
- ✨ Add free-threaded macOS builds and update CPython to 3.13.0b2. (#1854)
|
||||
- 🐛 Issue copying a wheel to a non-existent output dir fixed. (#1851, #1862)
|
||||
- 🐛 Better determinism for the test environment seeding. (#1835)
|
||||
- 🛠 `VIRTUAL_ENV` variable now set. (#1842)
|
||||
- 🛠 Remove a pip<21.3 workaround. (#1842)
|
||||
- 🛠 Error handling was refactored to use exceptions. (#1719)
|
||||
- 🛠 Hardcoded paths in tests avoided. (#1834)
|
||||
- 🛠 Single Python tests made more generic. (#1835)
|
||||
- 🛠 Sped up our ci by splitting up emulation tests. (#1839)
|
||||
|
||||
|
||||
|
||||
### v2.18.1
|
||||
|
||||
- 🌟 Add free-threaded Linux and Windows builds for 3.13. New identifiers `cp313t-*`, new option `CIBW_FREE_THREADED_SUPPORT`/`tool.cibuildwheel.free-threaded-support` required to opt-in. [See the docs](https://cibuildwheel.pypa.io/en/stable/options/#free-threaded-support) for more information. (#1831)
|
||||
- ✨ The `container-engine` is now a build (non-global) option. (#1792)
|
||||
- 🛠 The build backend for cibuildwheel is now hatchling. (#1297)
|
||||
- 🛠 Significant improvements and modernization to our noxfile. (#1823)
|
||||
- 🛠 Use pylint's new GitHub Actions reporter instead of a custom matcher. (#1823)
|
||||
- 🛠 Unpin virtualenv updates for Python 3.7+ (#1830)
|
||||
- 🐛 Fix running linux tests from Windows or macOS ARM. (#1788)
|
||||
- 📚 Fix our documentation build. (#1821)
|
||||
|
||||
|
||||
### v2.18.0
|
||||
|
||||
_12 May 2024_
|
||||
|
||||
- ✨ Adds CPython 3.13 support, under the prerelease flag [CIBW_PRERELEASE_PYTHONS](https://cibuildwheel.pypa.io/en/stable/options/#prerelease-pythons). This version of cibuildwheel uses 3.13.0b1. Free-threading mode is not available yet (#1657), waiting on official binaries (planned for beta 2) and pip support.
|
||||
|
||||
_While CPython is in beta, the ABI can change, so your wheels might not be compatible with the final release. For this reason, we don't recommend distributing wheels until RC1, at which point 3.13 will be available in cibuildwheel without the flag._ (#1815)
|
||||
|
||||
- ✨ Musllinux now defaults to `musllinux_1_2`. You can set the older `musllinux_1_1` via config if needed. (#1817)
|
||||
- 🛠 No longer pre-seed setuptools/wheel in virtual environments (#1819)
|
||||
- 🛠 Respect the constraints file when building with pip, matching build (#1818)
|
||||
- 🛠 Use uv to compile our pinned dependencies, 10x faster and doesn't require special setup (#1778)
|
||||
- 🐛 Fix an issue with the schema (#1788)
|
||||
- 📚 Document the new delocate error checking macOS versions (#1766)
|
||||
- 📚 Document Rust builds (#1816)
|
||||
- 📚 Speed up our readthedocs builds with uv, 26 seconds -> 6 seconds to install dependencies (#1816)
|
||||
|
||||
<!-- END bin/update_readme_changelog.py -->
|
||||
<!-- [[[end]]] (sum: vS54piNR4b) -->
|
||||
|
||||
---
|
||||
|
||||
@@ -299,12 +310,23 @@ Everyone interacting with the cibuildwheel project via codebase, issue tracker,
|
||||
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
|
||||
-------
|
||||
|
||||
|
||||
+35
-8
@@ -17,6 +17,10 @@ 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
|
||||
@@ -24,11 +28,10 @@ branding:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# Set up the version of Python that supports pyodide
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
id: python
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.11 - 3.13"
|
||||
update-environment: false
|
||||
|
||||
- id: cibw
|
||||
@@ -43,6 +46,10 @@ runs:
|
||||
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):
|
||||
@@ -54,7 +61,10 @@ runs:
|
||||
def post_setup(self, context):
|
||||
super().post_setup(context)
|
||||
self.bin_path = Path(context.env_exe).parent
|
||||
run([sys.executable, "-m", "pip", "--python", context.env_exe, "install", r"${{ github.action_path }}"], check=True)
|
||||
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")
|
||||
@@ -63,30 +73,47 @@ runs:
|
||||
shutil.rmtree(venv_path)
|
||||
builder = EnvBuilder()
|
||||
builder.create(venv_path)
|
||||
cibw_path = [path for path in builder.bin_path.glob("cibuildwheel*") if path.stem == "cibuildwheel"][0]
|
||||
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"cibw-path={cibw_path}\n")
|
||||
f.write(f"updated-path={full_path}\n")
|
||||
print("::endgroup::")
|
||||
EOF
|
||||
shell: bash
|
||||
|
||||
# Redirecting stderr to stdout to fix interleaving issue in Actions.
|
||||
- run: >
|
||||
"${{ steps.cibw.outputs.cibw-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) || ''}}
|
||||
2>&1
|
||||
env:
|
||||
PATH: "${{ steps.cibw.outputs.updated-path }}"
|
||||
shell: bash
|
||||
if: runner.os != 'Windows'
|
||||
|
||||
# Windows needs powershell to interact nicely with Meson
|
||||
- run: >
|
||||
& "${{ steps.cibw.outputs.cibw-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 }}"
|
||||
shell: pwsh
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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/*
|
||||
+65
-27
@@ -1,38 +1,76 @@
|
||||
pr:
|
||||
paths:
|
||||
exclude:
|
||||
- docs/*
|
||||
- .github/**
|
||||
- bin/*
|
||||
- docs/**
|
||||
- examples/github-*
|
||||
- examples/ci*
|
||||
- examples/travis-ci-*
|
||||
- .ci*
|
||||
- .pre-commit-config.yaml
|
||||
- .readthedocs.yml
|
||||
- .travis.yml
|
||||
- README.md
|
||||
- mkdocs.yml
|
||||
- noxfile.py
|
||||
|
||||
jobs:
|
||||
- job: linux_38
|
||||
timeoutInMinutes: 120
|
||||
pool: {vmImage: 'Ubuntu-20.04'}
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- bash: |
|
||||
python -m pip install -e ".[dev]"
|
||||
python ./bin/run_tests.py
|
||||
|
||||
- job: macos_38
|
||||
pool: {vmImage: 'macOS-12'}
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- bash: |
|
||||
python -m pip install -e ".[dev]"
|
||||
python ./bin/run_tests.py --num-processes 2
|
||||
|
||||
- job: windows_38
|
||||
pool: {vmImage: 'windows-2019'}
|
||||
- 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)
|
||||
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
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)
|
||||
|
||||
- bash: |
|
||||
python -m pip install -e ".[dev]"
|
||||
python ./bin/run_tests.py
|
||||
echo "CIBW_ENABLE = $CIBW_ENABLE"
|
||||
python ./bin/run_tests.py --test-select $(testSelect)
|
||||
displayName: 'Run tests'
|
||||
|
||||
+7
-15
@@ -1,27 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# /// script
|
||||
# dependencies = ["click", "packaging", "tomli; python_version<'3.11'"]
|
||||
# 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
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
import tomli as tomllib
|
||||
else:
|
||||
import tomllib
|
||||
|
||||
config = [
|
||||
# file path, version find/replace format
|
||||
("pyproject.toml", 'version = "{}"'),
|
||||
@@ -29,7 +22,6 @@ config = [
|
||||
("cibuildwheel/__init__.py", '__version__ = "{}"'),
|
||||
("docs/faq.md", "cibuildwheel=={}"),
|
||||
("docs/faq.md", "cibuildwheel@v{}"),
|
||||
("docs/setup.md", "cibuildwheel=={}"),
|
||||
("examples/*", "cibuildwheel=={}"),
|
||||
("examples/*", "cibuildwheel@v{}"),
|
||||
]
|
||||
@@ -76,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:
|
||||
@@ -90,7 +82,7 @@ def bump_version() -> None:
|
||||
actions = []
|
||||
|
||||
for path_pattern, version_pattern in config:
|
||||
paths = [Path(p) for p in glob.glob(path_pattern)]
|
||||
paths = list(Path().glob(path_pattern))
|
||||
|
||||
if not paths:
|
||||
print(f"error: Pattern {path_pattern} didn't match any files")
|
||||
@@ -137,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 the changelog as part of this")
|
||||
print("commit, do that now.")
|
||||
print("Files updated. If you want to update docs/changelog.md as part of")
|
||||
print("this commit, do that now.")
|
||||
print()
|
||||
|
||||
while input('Type "done" to continue: ').strip().lower() != "done":
|
||||
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/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()
|
||||
+94
-14
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env -S uv run -q
|
||||
|
||||
# /// script
|
||||
# dependencies = ["pyyaml"]
|
||||
@@ -11,12 +11,12 @@ from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
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
|
||||
$schema: http://json-schema.org/draft-07/schema#
|
||||
$id: https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json
|
||||
$defs:
|
||||
inherit:
|
||||
@@ -26,6 +26,16 @@ $defs:
|
||||
- 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.
|
||||
additionalProperties: false
|
||||
description: cibuildwheel's settings.
|
||||
type: object
|
||||
@@ -48,7 +58,7 @@ properties:
|
||||
type: string_array
|
||||
build-frontend:
|
||||
default: default
|
||||
description: Set the tool to use to build, either "pip" (default for now), "build", or "build[uv]"
|
||||
description: Set the tool to use to build, either "build" (default), "build[uv]", or "pip"
|
||||
oneOf:
|
||||
- enum: [pip, build, "build[uv]", default]
|
||||
- type: string
|
||||
@@ -98,7 +108,31 @@ properties:
|
||||
dependency-versions:
|
||||
default: pinned
|
||||
description: Specify how cibuildwheel controls the versions of the tools it uses
|
||||
type: string
|
||||
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"
|
||||
environment:
|
||||
description: Set environment variables needed during the build.
|
||||
type: string_table
|
||||
@@ -106,13 +140,12 @@ properties:
|
||||
description: Set environment variables on the host to pass-through to the container
|
||||
during the build.
|
||||
type: string_array
|
||||
free-threaded-support:
|
||||
type: boolean
|
||||
default: false
|
||||
description: The project supports free-threaded builds of Python (PEP703)
|
||||
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
|
||||
@@ -128,6 +161,9 @@ 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
|
||||
@@ -137,21 +173,33 @@ 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
|
||||
repair-wheel-command:
|
||||
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
|
||||
skip:
|
||||
description: Choose the Python versions to skip.
|
||||
type: string_array
|
||||
@@ -161,12 +209,39 @@ 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)
|
||||
@@ -235,6 +310,7 @@ items:
|
||||
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"}
|
||||
@@ -243,18 +319,21 @@ items:
|
||||
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"]}
|
||||
del non_global_options["build"]
|
||||
del non_global_options["skip"]
|
||||
del non_global_options["test-skip"]
|
||||
del non_global_options["free-threaded-support"]
|
||||
del non_global_options["enable"]
|
||||
|
||||
overrides["items"]["properties"]["select"]["oneOf"] = string_array
|
||||
overrides["items"]["properties"] |= non_global_options.copy()
|
||||
@@ -283,6 +362,8 @@ oses = {
|
||||
"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"] = {
|
||||
@@ -301,9 +382,8 @@ schema["properties"] |= oses
|
||||
|
||||
if args.schemastore:
|
||||
schema["$id"] = "https://json.schemastore.org/partial-cibuildwheel.json"
|
||||
schema["$schema"] = "http://json-schema.org/draft-07/schema#"
|
||||
schema["description"] = (
|
||||
"cibuildwheel's toml file, generated with ./bin/generate_schema.py --schemastore from cibuildwheel."
|
||||
"cibuildwheel's settings. Generated with ./bin/generate_schema.py --schemastore from cibuildwheel."
|
||||
)
|
||||
|
||||
print(json.dumps(schema, indent=2))
|
||||
|
||||
@@ -11,8 +11,6 @@ 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
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
#!/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, *, 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():
|
||||
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
|
||||
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
|
||||
)
|
||||
return unstaged_changes or staged_changes
|
||||
|
||||
|
||||
@click.command()
|
||||
def main():
|
||||
def main() -> None:
|
||||
project_root = Path(__file__).parent / ".."
|
||||
os.chdir(project_root)
|
||||
|
||||
@@ -54,10 +55,10 @@ def main():
|
||||
f"""
|
||||
Update the versions of our dependencies.
|
||||
|
||||
PR generated by `{os.path.basename(__file__)}`.
|
||||
PR generated by `{Path(__file__).name}`.
|
||||
"""
|
||||
)
|
||||
run(
|
||||
subprocess.run(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
|
||||
+13
-13
@@ -17,25 +17,22 @@ 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 datetime
|
||||
from datetime import UTC, datetime
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import Any, TextIO
|
||||
from typing import Any, Self, TextIO
|
||||
|
||||
import click
|
||||
import yaml
|
||||
from github import Github, GithubException
|
||||
|
||||
ICONS = (
|
||||
"appveyor",
|
||||
"github",
|
||||
"azurepipelines",
|
||||
"circleci",
|
||||
@@ -80,12 +77,12 @@ class Project:
|
||||
self.notes = notes
|
||||
else:
|
||||
self.num_stars = 0
|
||||
self.pushed_at = datetime.utcnow()
|
||||
self.pushed_at = datetime.now(UTC)
|
||||
|
||||
name_len = len(self.name) + 4
|
||||
self.__class__.NAME = max(self.__class__.NAME, name_len)
|
||||
|
||||
def __lt__(self, other: Project) -> bool:
|
||||
def __lt__(self, other: Self) -> bool:
|
||||
if self.online:
|
||||
return self.num_stars < other.num_stars
|
||||
else:
|
||||
@@ -95,8 +92,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
|
||||
@@ -127,7 +124,7 @@ class Project:
|
||||
return f"[{self.name}]: {self.url}"
|
||||
|
||||
def info(self) -> str:
|
||||
days = (datetime.utcnow() - self.pushed_at).days
|
||||
days = (datetime.now(UTC) - self.pushed_at).days
|
||||
return f"<!-- {self.name}: {self.num_stars}, last pushed {days} days ago -->"
|
||||
|
||||
|
||||
@@ -138,6 +135,7 @@ 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")
|
||||
@@ -172,7 +170,9 @@ 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):
|
||||
def render_projects(
|
||||
projects: Sequence[Project], *, dest_path: Path, include_info: bool = True
|
||||
) -> str:
|
||||
io = StringIO()
|
||||
print = functools.partial(builtins.print, file=io)
|
||||
|
||||
@@ -203,7 +203,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 +217,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)
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/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())
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/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())
|
||||
@@ -1,34 +1,35 @@
|
||||
#!/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
|
||||
|
||||
|
||||
def shell(cmd, *, check: bool, **kwargs):
|
||||
return run([cmd], shell=True, check=check, **kwargs)
|
||||
DIR = Path(__file__).parent.parent.resolve()
|
||||
|
||||
|
||||
def git_repo_has_changes():
|
||||
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:
|
||||
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):
|
||||
def generate_basic_project(path: Path) -> None:
|
||||
sys.path.insert(0, "")
|
||||
from test.test_projects.c import new_c_project
|
||||
from test.test_projects.c import new_c_project # noqa: PLC0415
|
||||
|
||||
project = new_c_project()
|
||||
project.generate(path)
|
||||
@@ -38,14 +39,32 @@ 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="[](https://ci.appveyor.com/project/joerick/cibuildwheel/branch/{branch})",
|
||||
),
|
||||
CIService(
|
||||
name="azure-pipelines",
|
||||
dst_config_path="azure-pipelines.yml",
|
||||
@@ -59,7 +78,8 @@ services = [
|
||||
CIService(
|
||||
name="github",
|
||||
dst_config_path=".github/workflows/example.yml",
|
||||
badge_md="[](https://github.com/pypa/cibuildwheel/actions)",
|
||||
badge_md="[](https://github.com/pypa/cibuildwheel/actions?query=branch%3A{branch})",
|
||||
config_file_transform=github_config_file_transform,
|
||||
),
|
||||
CIService(
|
||||
name="travis-ci",
|
||||
@@ -79,9 +99,8 @@ services = [
|
||||
]
|
||||
|
||||
|
||||
def ci_service_for_config_file(config_file):
|
||||
filename = Path(config_file).name
|
||||
|
||||
def ci_service_for_config_file(config_file: Path) -> CIService:
|
||||
filename = config_file.name
|
||||
try:
|
||||
return next(s for s in services if filename.startswith(s.name))
|
||||
except StopIteration:
|
||||
@@ -98,16 +117,18 @@ def run_example_ci_configs(config_files=None):
|
||||
"""
|
||||
|
||||
if len(config_files) == 0:
|
||||
config_files = glob("examples/*-minimal.yml")
|
||||
config_files = Path("examples").glob("*-minimal.yml")
|
||||
else:
|
||||
config_files = [Path(f) for f in config_files]
|
||||
|
||||
# check each CI service has at most 1 config file
|
||||
configs_by_service = {}
|
||||
configs_by_service = set()
|
||||
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[service.name] = config_file
|
||||
configs_by_service.add(service.name)
|
||||
|
||||
if git_repo_has_changes():
|
||||
print("Your git repo has uncommitted changes. Commit or stash before continuing.")
|
||||
@@ -128,23 +149,25 @@ 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)
|
||||
|
||||
run(["git", "add", example_project], check=True)
|
||||
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)
|
||||
message = textwrap.dedent(
|
||||
f"""\
|
||||
Test example minimal configs
|
||||
Test example CI configs
|
||||
|
||||
Testing files: {config_files}
|
||||
Testing files: {[str(f) for f in config_files]}
|
||||
Generated from branch: {previous_branch}
|
||||
Time: {timestamp}
|
||||
"""
|
||||
)
|
||||
run(["git", "commit", "--no-verify", "--message", message], check=True)
|
||||
subprocess.run(["git", "commit", "--no-verify", "--message", message], check=True)
|
||||
shell(f"git subtree --prefix={example_project} push origin {branch_name}", check=True)
|
||||
|
||||
print("---")
|
||||
@@ -174,6 +197,5 @@ def run_example_ci_configs(config_files=None):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
os.chdir("..")
|
||||
os.chdir(DIR)
|
||||
run_example_ci_configs(standalone_mode=True)
|
||||
|
||||
+75
-5
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
@@ -9,8 +8,12 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
if __name__ == "__main__":
|
||||
default_cpu_count = os.cpu_count() or 2
|
||||
parser = argparse.ArgumentParser()
|
||||
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.add_argument(
|
||||
"--run-podman", action="store_true", default=False, help="run podman tests (linux only)"
|
||||
)
|
||||
@@ -20,15 +23,46 @@ if __name__ == "__main__":
|
||||
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"):
|
||||
if (
|
||||
sys.platform.startswith("linux")
|
||||
and os.environ.get("CIBW_PLATFORM", "linux") == "linux"
|
||||
and args.test_select in ["all", "native"]
|
||||
):
|
||||
# run the docker unit tests only on Linux
|
||||
unit_test_args += ["--run-docker"]
|
||||
|
||||
@@ -37,17 +71,53 @@ if __name__ == "__main__":
|
||||
|
||||
subprocess.run(unit_test_args, check=True)
|
||||
|
||||
# integration tests
|
||||
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_test_args = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pytest",
|
||||
"-m",
|
||||
f"{' and '.join(['not serial', *marks])}",
|
||||
f"--numprocesses={args.num_processes}",
|
||||
"-x",
|
||||
"--durations",
|
||||
"0",
|
||||
"--timeout=2400",
|
||||
"test",
|
||||
"-vv",
|
||||
]
|
||||
|
||||
if sys.platform.startswith("linux") and args.run_podman:
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
@@ -13,7 +12,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")
|
||||
parser = argparse.ArgumentParser(description="Runs a sample build", allow_abbrev=False)
|
||||
parser.add_argument("project_python_path", nargs="?", default="test.test_0_basic.basic_project")
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
+83
-51
@@ -1,70 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
from dataclasses import dataclass
|
||||
import dataclasses
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from packaging.version import Version
|
||||
|
||||
DIR = Path(__file__).parent.resolve()
|
||||
RESOURCES = DIR.parent / "cibuildwheel/resources"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Image:
|
||||
manylinux_version: str
|
||||
platform: str
|
||||
platforms: list[str]
|
||||
image_name: str
|
||||
tag: str | None # Set this to pin the image
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
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),
|
||||
PyPAImage(
|
||||
"manylinux2014",
|
||||
[
|
||||
"x86_64",
|
||||
"i686",
|
||||
"aarch64",
|
||||
"ppc64le",
|
||||
"s390x",
|
||||
"pypy_x86_64",
|
||||
"pypy_i686",
|
||||
"pypy_aarch64",
|
||||
],
|
||||
),
|
||||
# manylinux_2_28 images
|
||||
Image("manylinux_2_28", "x86_64", "quay.io/pypa/manylinux_2_28_x86_64", None),
|
||||
Image("manylinux_2_28", "aarch64", "quay.io/pypa/manylinux_2_28_aarch64", None),
|
||||
Image("manylinux_2_28", "ppc64le", "quay.io/pypa/manylinux_2_28_ppc64le", None),
|
||||
Image("manylinux_2_28", "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),
|
||||
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"]),
|
||||
# musllinux_1_2 images
|
||||
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),
|
||||
PyPAImage(
|
||||
"musllinux_1_2", ["x86_64", "i686", "aarch64", "ppc64le", "s390x", "armv7l", "riscv64"]
|
||||
),
|
||||
]
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
@@ -90,6 +104,21 @@ 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()
|
||||
@@ -105,10 +134,13 @@ for image in images:
|
||||
)
|
||||
tag_name = pinned_tag["name"]
|
||||
|
||||
if not config.has_section(image.platform):
|
||||
config[image.platform] = {}
|
||||
|
||||
config[image.platform][image.manylinux_version] = f"{image.image_name}:{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}"
|
||||
|
||||
with open(RESOURCES / "pinned_docker_images.cfg", "w") as f:
|
||||
config.write(f)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -18,7 +17,7 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
subprocess.run(["mkdocs", "build"], check=True)
|
||||
|
||||
hti = Html2Image(custom_flags=["--force-device-scale-factor=2"])
|
||||
@@ -42,8 +41,7 @@ def main():
|
||||
)
|
||||
|
||||
dest_path = Path("docs/data/how-it-works.png")
|
||||
if dest_path.exists():
|
||||
dest_path.unlink()
|
||||
dest_path.unlink(missing_ok=True)
|
||||
|
||||
Path(screenshot).rename(dest_path)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import difflib
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
@@ -16,8 +16,6 @@ from packaging.version import InvalidVersion, Version
|
||||
from rich.logging import RichHandler
|
||||
from rich.syntax import Syntax
|
||||
|
||||
from cibuildwheel._compat import tomllib
|
||||
|
||||
log = logging.getLogger("cibw")
|
||||
|
||||
# Looking up the dir instead of using utils.resources_dir
|
||||
@@ -29,7 +27,7 @@ NODEJS_DIST: Final[str] = "https://nodejs.org/dist/"
|
||||
NODEJS_INDEX: Final[str] = f"{NODEJS_DIST}index.json"
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
@dataclasses.dataclass(frozen=True, order=True)
|
||||
class VersionTuple:
|
||||
version: Version
|
||||
version_string: str
|
||||
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/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()
|
||||
+244
-50
@@ -1,13 +1,15 @@
|
||||
#!/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, Union
|
||||
from typing import Any, Final, Literal, TypedDict
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
import click
|
||||
import requests
|
||||
@@ -17,8 +19,8 @@ from packaging.version import Version
|
||||
from rich.logging import RichHandler
|
||||
from rich.syntax import Syntax
|
||||
|
||||
from cibuildwheel._compat import tomllib
|
||||
from cibuildwheel.extra import dump_python_configurations
|
||||
from cibuildwheel.extra import dump_python_configurations, get_pyodide_xbuildenv_info
|
||||
from cibuildwheel.platforms.android import android_triplet
|
||||
|
||||
log = logging.getLogger("cibw")
|
||||
|
||||
@@ -31,26 +33,18 @@ RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"
|
||||
ArchStr = Literal["32", "64", "ARM64"]
|
||||
|
||||
|
||||
class ConfigWinCP(TypedDict):
|
||||
class Config(TypedDict):
|
||||
identifier: str
|
||||
version: str
|
||||
arch: str
|
||||
|
||||
|
||||
class ConfigWinPP(TypedDict):
|
||||
identifier: str
|
||||
version: str
|
||||
arch: str
|
||||
class ConfigUrl(Config):
|
||||
url: str
|
||||
|
||||
|
||||
class ConfigMacOS(TypedDict):
|
||||
identifier: str
|
||||
version: str
|
||||
url: str
|
||||
|
||||
|
||||
AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigMacOS]
|
||||
class ConfigPyodide(Config):
|
||||
default_pyodide_version: str
|
||||
node_version: str
|
||||
|
||||
|
||||
# The following set of "Versions" classes allow the initial call to the APIs to
|
||||
@@ -84,7 +78,7 @@ class WindowsVersions:
|
||||
|
||||
self.version_dict = {Version(v): v for v in cp_info["versions"]}
|
||||
|
||||
def update_version_windows(self, spec: Specifier) -> ConfigWinCP | None:
|
||||
def update_version_windows(self, spec: Specifier) -> Config | 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)
|
||||
@@ -99,10 +93,79 @@ class WindowsVersions:
|
||||
flags = "t" if self.free_threaded else ""
|
||||
version = versions[0]
|
||||
identifier = f"cp{version.major}{version.minor}{flags}-{self.arch}"
|
||||
return ConfigWinCP(
|
||||
return Config(
|
||||
identifier=identifier,
|
||||
version=self.version_dict[version],
|
||||
arch=self.arch_str,
|
||||
)
|
||||
|
||||
|
||||
class GraalPyVersions:
|
||||
def __init__(self) -> None:
|
||||
response = requests.get("https://api.github.com/repos/oracle/graalpython/releases")
|
||||
response.raise_for_status()
|
||||
|
||||
releases = response.json()
|
||||
gp_version_re = re.compile(r"-(\d+\.\d+\.\d+)$")
|
||||
cp_version_re = re.compile(r"Python (\d+\.\d+(?:\.\d+)?)")
|
||||
for release in releases:
|
||||
m = gp_version_re.search(release["tag_name"])
|
||||
if m:
|
||||
release["graalpy_version"] = Version(m.group(1))
|
||||
m = cp_version_re.search(release["body"])
|
||||
if m:
|
||||
release["python_version"] = Version(m.group(1))
|
||||
|
||||
self.releases = [r for r in releases if "graalpy_version" in r and "python_version" in r]
|
||||
|
||||
def update_version(self, identifier: str, spec: Specifier) -> ConfigUrl:
|
||||
if "x86_64" in identifier or "amd64" in identifier:
|
||||
arch = "x86_64"
|
||||
elif "arm64" in identifier or "aarch64" in identifier:
|
||||
arch = "aarch64"
|
||||
else:
|
||||
msg = f"{identifier} not supported yet on GraalPy"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
gpspec_str = identifier.split("-")[0].split("_")[1]
|
||||
if "." not in gpspec_str and len(gpspec_str) == 3:
|
||||
gpspec_str = gpspec_str[:2] + "." + gpspec_str[-1]
|
||||
gpspec = Specifier(f"=={gpspec_str}.*")
|
||||
|
||||
releases = [r for r in self.releases if spec.contains(r["python_version"])]
|
||||
releases = [r for r in self.releases if gpspec.contains(r["graalpy_version"])]
|
||||
releases = sorted(releases, key=lambda r: r["graalpy_version"])
|
||||
|
||||
if not releases:
|
||||
msg = f"GraalPy {arch} not found for {spec}!"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
release = releases[-1]
|
||||
version = release["python_version"]
|
||||
gpversion = release["graalpy_version"]
|
||||
|
||||
if "macosx" in identifier:
|
||||
arch = "x86_64" if "x86_64" in identifier else "arm64"
|
||||
platform = "macos"
|
||||
elif "win" in identifier:
|
||||
arch = "aarch64" if "arm64" in identifier else "x86_64"
|
||||
platform = "windows"
|
||||
else:
|
||||
msg = "GraalPy provides downloads for macOS and Windows and is included for manylinux"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
arch = "amd64" if arch == "x86_64" else "aarch64"
|
||||
ext = "zip" if "win" in identifier else "tar.gz"
|
||||
(url,) = (
|
||||
rf["browser_download_url"]
|
||||
for rf in release["assets"]
|
||||
if rf["name"].endswith(f"{platform}-{arch}.{ext}")
|
||||
and rf["name"].startswith(f"graalpy-{gpversion.major}")
|
||||
)
|
||||
|
||||
return ConfigUrl(
|
||||
identifier=identifier,
|
||||
version=f"{version.major}.{version.minor}",
|
||||
url=url,
|
||||
)
|
||||
|
||||
|
||||
@@ -131,9 +194,9 @@ class PyPyVersions:
|
||||
]
|
||||
return urls[0] if urls else ""
|
||||
|
||||
def update_version_windows(self, spec: Specifier) -> ConfigWinCP:
|
||||
def update_version_windows(self, spec: Specifier) -> ConfigUrl:
|
||||
releases = [r for r in self.releases if spec.contains(r["python_version"])]
|
||||
releases = sorted(releases, key=lambda r: r["pypy_version"])
|
||||
releases = sorted(releases, key=operator.itemgetter("pypy_version"))
|
||||
releases = [r for r in releases if self.get_arch_file(r)]
|
||||
|
||||
if not releases:
|
||||
@@ -147,20 +210,19 @@ class PyPyVersions:
|
||||
identifier = f"pp{version.major}{version.minor}-{version_arch}"
|
||||
url = self.get_arch_file(release)
|
||||
|
||||
return ConfigWinPP(
|
||||
return ConfigUrl(
|
||||
identifier=identifier,
|
||||
version=f"{version.major}.{version.minor}",
|
||||
arch=self.arch,
|
||||
url=url,
|
||||
)
|
||||
|
||||
def update_version_macos(self, spec: Specifier) -> ConfigMacOS:
|
||||
def update_version_macos(self, spec: Specifier) -> ConfigUrl:
|
||||
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=lambda r: r["pypy_version"])
|
||||
releases = sorted(releases, key=operator.itemgetter("pypy_version"))
|
||||
|
||||
if not releases:
|
||||
msg = f"PyPy macOS {self.arch} not found for {spec}!"
|
||||
@@ -178,7 +240,7 @@ class PyPyVersions:
|
||||
if "" in rf["platform"] == "darwin" and rf["arch"] == arch
|
||||
)
|
||||
|
||||
return ConfigMacOS(
|
||||
return ConfigUrl(
|
||||
identifier=identifier,
|
||||
version=f"{version.major}.{version.minor}",
|
||||
url=url,
|
||||
@@ -196,22 +258,21 @@ class CPythonVersions:
|
||||
|
||||
self.versions_dict: dict[Version, int] = {}
|
||||
for release in releases_info:
|
||||
# Removing the prefix, Python 3.9 would use: release["name"].removeprefix("Python ")
|
||||
version = Version(release["name"][7:])
|
||||
# Skip the pymanager releases
|
||||
if not release["slug"].startswith("python"):
|
||||
continue
|
||||
|
||||
# Removing the prefix
|
||||
version = Version(release["name"].removeprefix("Python "))
|
||||
|
||||
uri = int(release["resource_uri"].rstrip("/").split("/")[-1])
|
||||
self.versions_dict[version] = uri
|
||||
|
||||
def update_version_macos(
|
||||
self, identifier: str, version: Version, spec: Specifier
|
||||
) -> ConfigMacOS | None:
|
||||
def update_version(self, identifier: str, spec: Specifier, file_ident: str) -> ConfigUrl | 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]
|
||||
@@ -223,7 +284,7 @@ class CPythonVersions:
|
||||
|
||||
urls = [rf["url"] for rf in file_info if file_ident in rf["url"]]
|
||||
if urls:
|
||||
return ConfigMacOS(
|
||||
return ConfigUrl(
|
||||
identifier=identifier,
|
||||
version=f"{new_version.major}.{new_version.minor}",
|
||||
url=urls[0],
|
||||
@@ -231,6 +292,120 @@ 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.
|
||||
@@ -246,27 +421,35 @@ class AllVersions:
|
||||
self.windows_t_arm64 = WindowsVersions("ARM64", True)
|
||||
self.windows_pypy_64 = PyPyVersions("64")
|
||||
|
||||
self.macos_cpython = CPythonVersions()
|
||||
self.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)
|
||||
orig_config = copy.copy(config)
|
||||
config_update: AnyConfig | None = None
|
||||
config_update: Config | 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.macos_cpython.update_version_macos(identifier, version, spec)
|
||||
config_update = self.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"):
|
||||
@@ -278,16 +461,29 @@ class AllVersions:
|
||||
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!"
|
||||
config.update(**config_update)
|
||||
|
||||
if config != orig_config:
|
||||
log.info(" Updated %s to %s", orig_config, config)
|
||||
if config_update != config:
|
||||
log.info(" Updated %s to %s", config, config_update)
|
||||
config.clear()
|
||||
config.update(**config_update)
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -311,11 +507,9 @@ def update_pythons(force: bool, level: str) -> None:
|
||||
with toml_file_path.open("rb") as f:
|
||||
configs = tomllib.load(f)
|
||||
|
||||
for config in configs["windows"]["python_configurations"]:
|
||||
all_versions.update_config(config)
|
||||
|
||||
for config in configs["macos"]["python_configurations"]:
|
||||
all_versions.update_config(config)
|
||||
for platform in ["windows", "macos", "android", "ios", "pyodide"]:
|
||||
for config in configs[platform]["python_configurations"]:
|
||||
all_versions.update_config(config)
|
||||
|
||||
result_toml = dump_python_configurations(configs)
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/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()
|
||||
+33
-34
@@ -1,21 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import difflib
|
||||
import logging
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import click
|
||||
import rich
|
||||
from packaging.version import InvalidVersion, Version
|
||||
from packaging.version import Version
|
||||
from rich.logging import RichHandler
|
||||
from rich.syntax import Syntax
|
||||
|
||||
from cibuildwheel._compat import tomllib
|
||||
from cibuildwheel.extra import github_api_request
|
||||
|
||||
log = logging.getLogger("cibw")
|
||||
|
||||
@@ -30,34 +29,28 @@ GET_VIRTUALENV_URL_TEMPLATE: Final[str] = (
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
@dataclasses.dataclass(frozen=True, order=True)
|
||||
class VersionTuple:
|
||||
name: str
|
||||
download_url: str
|
||||
version: Version
|
||||
version_string: str
|
||||
|
||||
|
||||
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
|
||||
versions.append(VersionTuple(version, version_string))
|
||||
except InvalidVersion:
|
||||
log.warning("Ignoring ref %r", ref)
|
||||
versions.sort(reverse=True)
|
||||
return versions
|
||||
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"]
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -80,14 +73,20 @@ def update_virtualenv(force: bool, level: str) -> None:
|
||||
with toml_file_path.open("rb") as f:
|
||||
configurations = tomllib.load(f)
|
||||
default = configurations.pop("default")
|
||||
version = str(default["version"])
|
||||
versions = git_ls_remote_versions(GET_VIRTUALENV_GITHUB)
|
||||
if versions[0].version > Version(version):
|
||||
version = versions[0].version_string
|
||||
local_version = str(default["version"])
|
||||
|
||||
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": GET_VIRTUALENV_URL_TEMPLATE.format(version=version),
|
||||
"url": url,
|
||||
}
|
||||
result_toml = "".join(
|
||||
f'{key} = {{ version = "{value["version"]}", url = "{value["url"]}" }}\n'
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "2.19.2"
|
||||
__version__ = "3.3.0"
|
||||
|
||||
+239
-106
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import os
|
||||
import shutil
|
||||
@@ -9,33 +8,24 @@ import tarfile
|
||||
import textwrap
|
||||
import traceback
|
||||
import typing
|
||||
from collections.abc import Iterable, Sequence, Set
|
||||
from collections.abc import Generator, Iterable, Sequence
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
from typing import Protocol
|
||||
from typing import Any, Literal, TextIO
|
||||
|
||||
import cibuildwheel
|
||||
import cibuildwheel.linux
|
||||
import cibuildwheel.macos
|
||||
import cibuildwheel.pyodide
|
||||
import cibuildwheel.util
|
||||
import cibuildwheel.windows
|
||||
from cibuildwheel import errors
|
||||
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.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,
|
||||
strtobool,
|
||||
)
|
||||
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
|
||||
@@ -43,6 +33,29 @@ class GlobalOptions:
|
||||
print_traceback_on_error: bool = True # decides what happens when errors are hit.
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class FileReport:
|
||||
name: str
|
||||
size: str
|
||||
|
||||
|
||||
# Taken from https://stackoverflow.com/a/107717
|
||||
class Unbuffered:
|
||||
def __init__(self, stream: TextIO) -> None:
|
||||
self.stream = stream
|
||||
|
||||
def write(self, data: str) -> None:
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
|
||||
def writelines(self, data: Iterable[str]) -> None:
|
||||
self.stream.writelines(data)
|
||||
self.stream.flush()
|
||||
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
global_options = GlobalOptions()
|
||||
try:
|
||||
@@ -52,7 +65,7 @@ def main() -> None:
|
||||
if log.step_active:
|
||||
log.step_end_with_error(message)
|
||||
else:
|
||||
print(f"cibuildwheel: {message}", file=sys.stderr)
|
||||
log.error(message)
|
||||
|
||||
if global_options.print_traceback_on_error:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
@@ -73,17 +86,19 @@ 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"],
|
||||
choices=["auto", "linux", "macos", "windows", "pyodide", "android", "ios"],
|
||||
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, but "linux" works on all three, as long
|
||||
as Docker/Podman is installed. Default: auto.
|
||||
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.
|
||||
""",
|
||||
)
|
||||
|
||||
@@ -101,9 +116,22 @@ 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
|
||||
@@ -131,7 +159,7 @@ def main_inner(global_options: GlobalOptions) -> None:
|
||||
parser.add_argument(
|
||||
"package_dir",
|
||||
metavar="PACKAGE",
|
||||
default=Path("."),
|
||||
default=Path(),
|
||||
type=Path,
|
||||
nargs="?",
|
||||
help="""
|
||||
@@ -152,15 +180,15 @@ def main_inner(global_options: GlobalOptions) -> None:
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--allow-empty",
|
||||
"--clean-cache",
|
||||
action="store_true",
|
||||
help="Do not report an error code if the build does not match any wheels.",
|
||||
help="Clear the cibuildwheel cache and exit.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--prerelease-pythons",
|
||||
"--allow-empty",
|
||||
action="store_true",
|
||||
help="Enable pre-release Python versions if available.",
|
||||
help="Do not report an error code if the build does not match any wheels.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
@@ -174,6 +202,48 @@ def main_inner(global_options: GlobalOptions) -> None:
|
||||
|
||||
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
|
||||
@@ -200,7 +270,7 @@ def main_inner(global_options: GlobalOptions) -> None:
|
||||
# This is now the new package dir
|
||||
args.package_dir = project_dir.resolve()
|
||||
|
||||
with chdir(project_dir):
|
||||
with contextlib.chdir(project_dir):
|
||||
build_in_directory(args)
|
||||
finally:
|
||||
# avoid https://github.com/python/cpython/issues/86962 by performing
|
||||
@@ -219,28 +289,16 @@ def _compute_platform_only(only: str) -> PlatformName:
|
||||
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)
|
||||
|
||||
|
||||
def _compute_platform_auto() -> PlatformName:
|
||||
if sys.platform.startswith("linux"):
|
||||
return "linux"
|
||||
elif sys.platform == "darwin":
|
||||
return "macos"
|
||||
elif sys.platform == "win32":
|
||||
return "windows"
|
||||
else:
|
||||
msg = (
|
||||
'cibuildwheel: 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 _compute_platform(args: CommandLineArguments) -> PlatformName:
|
||||
platform_option_value = args.platform or os.environ.get("CIBW_PLATFORM", "auto")
|
||||
platform_option_value = args.platform or os.environ.get("CIBW_PLATFORM", "") or "auto"
|
||||
|
||||
if args.only and args.platform is not None:
|
||||
msg = "--platform cannot be specified with --only, it is computed from --only"
|
||||
@@ -256,40 +314,16 @@ def _compute_platform(args: CommandLineArguments) -> PlatformName:
|
||||
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 _compute_platform_auto()
|
||||
|
||||
|
||||
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
|
||||
if platform == "pyodide":
|
||||
return cibuildwheel.pyodide
|
||||
assert_never(platform)
|
||||
return native_platform()
|
||||
|
||||
|
||||
def build_in_directory(args: CommandLineArguments) -> None:
|
||||
platform: PlatformName = _compute_platform(args)
|
||||
if platform == "pyodide" and sys.platform == "win32":
|
||||
msg = "cibuildwheel: Building for pyodide is not supported on Windows"
|
||||
print(msg, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
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)
|
||||
|
||||
@@ -301,7 +335,7 @@ def build_in_directory(args: CommandLineArguments) -> None:
|
||||
msg = f"Could not find any of {{{names}}} at root of package"
|
||||
raise errors.ConfigurationError(msg)
|
||||
|
||||
platform_module = get_platform_module(platform)
|
||||
platform_module = ALL_PLATFORM_MODULES[platform]
|
||||
identifiers = get_build_identifiers(
|
||||
platform_module=platform_module,
|
||||
build_selector=options.globals.build_selector,
|
||||
@@ -319,10 +353,11 @@ def build_in_directory(args: CommandLineArguments) -> None:
|
||||
# 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]
|
||||
sys.stdout = Unbuffered(sys.stdout)
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -334,21 +369,18 @@ def build_in_directory(args: CommandLineArguments) -> None:
|
||||
|
||||
if not identifiers:
|
||||
message = f"No build identifiers selected: {options.globals.build_selector}"
|
||||
if args.allow_empty:
|
||||
if options.globals.allow_empty:
|
||||
print(f"cibuildwheel: {message}", file=sys.stderr)
|
||||
else:
|
||||
raise errors.NothingToDoError(message)
|
||||
|
||||
output_dir = options.globals.output_dir
|
||||
|
||||
if not output_dir.exists():
|
||||
output_dir.mkdir(parents=True)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
|
||||
try:
|
||||
with cibuildwheel.util.print_new_wheels(
|
||||
"\n{n} wheels produced in {m:.0f} minutes:", output_dir
|
||||
):
|
||||
with log.print_summary(options=options):
|
||||
platform_module.build(options, tmp_path)
|
||||
finally:
|
||||
# avoid https://github.com/python/cpython/issues/86962 by performing
|
||||
@@ -381,38 +413,139 @@ def print_preamble(platform: str, options: Options, identifiers: Sequence[str])
|
||||
|
||||
print()
|
||||
print(f"Cache folder: {CIBW_CACHE_PATH}")
|
||||
print()
|
||||
|
||||
warnings = detect_warnings(options=options, identifiers=identifiers)
|
||||
if warnings:
|
||||
print("\nWarnings:")
|
||||
for warning in warnings:
|
||||
print(" " + warning)
|
||||
warnings = detect_warnings(options=options)
|
||||
for warning in warnings:
|
||||
log.warning(warning)
|
||||
|
||||
print("\nHere we go!\n")
|
||||
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")
|
||||
|
||||
|
||||
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]
|
||||
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 detect_warnings(*, options: Options, identifiers: Iterable[str]) -> list[str]:
|
||||
warnings = []
|
||||
|
||||
# warn about deprecated {python} and {pip}
|
||||
# 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
|
||||
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."
|
||||
yield (
|
||||
f"{option_name}: '{{python}}' and '{{pip}}' are no longer supported "
|
||||
"and have been removed in cibuildwheel 3. 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 +0,0 @@
|
||||
from __future__ import annotations
|
||||
@@ -1,10 +0,0 @@
|
||||
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"]
|
||||
@@ -1,14 +0,0 @@
|
||||
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",
|
||||
)
|
||||
+135
-69
@@ -1,14 +1,15 @@
|
||||
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 Enum
|
||||
from enum import StrEnum, auto
|
||||
from typing import Final, Literal
|
||||
|
||||
from ._compat.typing import assert_never
|
||||
from cibuildwheel import errors
|
||||
|
||||
from .typing import PlatformName
|
||||
|
||||
PRETTY_NAMES: Final[dict[PlatformName, str]] = {
|
||||
@@ -16,69 +17,100 @@ PRETTY_NAMES: Final[dict[PlatformName, str]] = {
|
||||
"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"},
|
||||
{"linux": "x86_64", "macos": "x86_64", "windows": "AMD64", "android": "x86_64"},
|
||||
{"linux": "i686", "macos": None, "windows": "x86"},
|
||||
{"linux": "aarch64", "macos": "arm64", "windows": "ARM64"},
|
||||
{"linux": "aarch64", "macos": "arm64", "windows": "ARM64", "android": "arm64_v8a"},
|
||||
]
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Architecture(Enum):
|
||||
value: str
|
||||
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)
|
||||
|
||||
# mac/linux archs
|
||||
x86_64 = "x86_64"
|
||||
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()
|
||||
|
||||
# linux archs
|
||||
i686 = "i686"
|
||||
aarch64 = "aarch64"
|
||||
ppc64le = "ppc64le"
|
||||
s390x = "s390x"
|
||||
i686 = auto()
|
||||
aarch64 = auto()
|
||||
ppc64le = auto()
|
||||
s390x = auto()
|
||||
armv7l = auto()
|
||||
riscv64 = auto()
|
||||
|
||||
# mac archs
|
||||
universal2 = "universal2"
|
||||
arm64 = "arm64"
|
||||
universal2 = auto()
|
||||
arm64 = auto()
|
||||
|
||||
# windows archs
|
||||
x86 = "x86"
|
||||
x86 = auto()
|
||||
AMD64 = "AMD64"
|
||||
ARM64 = "ARM64"
|
||||
|
||||
# WebAssembly
|
||||
wasm32 = "wasm32"
|
||||
wasm32 = auto()
|
||||
|
||||
# Allow this to be sorted
|
||||
def __lt__(self, other: Architecture) -> bool:
|
||||
return self.value < other.value
|
||||
# android archs
|
||||
arm64_v8a = auto()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
# iOS "multiarch" architectures that include both
|
||||
# the CPU architecture and the ABI.
|
||||
arm64_iphoneos = auto()
|
||||
arm64_iphonesimulator = auto()
|
||||
x86_64_iphonesimulator = auto()
|
||||
|
||||
@staticmethod
|
||||
def parse_config(config: str, platform: PlatformName) -> set[Architecture]:
|
||||
def parse_config(config: str, platform: PlatformName) -> "set[Architecture]":
|
||||
result = set()
|
||||
for arch_str in re.split(r"[\s,]+", config):
|
||||
if arch_str == "auto":
|
||||
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))
|
||||
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
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def native_arch(platform: PlatformName) -> Architecture | None:
|
||||
if platform == "pyodide":
|
||||
return Architecture.wasm32
|
||||
def native_arch(platform: PlatformName) -> "Architecture | None":
|
||||
native_machine = platform_module.machine()
|
||||
native_architecture = Architecture(native_machine)
|
||||
|
||||
# Cross-platform support. Used for --print-build-identifiers or docker builds.
|
||||
host_platform: PlatformName = (
|
||||
@@ -87,42 +119,49 @@ class Architecture(Enum):
|
||||
else ("macos" if sys.platform.startswith("darwin") else "linux")
|
||||
)
|
||||
|
||||
native_machine = platform_module.machine()
|
||||
native_architecture = Architecture(native_machine)
|
||||
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
|
||||
|
||||
# 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:
|
||||
for arch_synonym in ARCH_SYNONYMS:
|
||||
if native_machine == arch_synonym.get(host_platform):
|
||||
synonym = arch_synonym[platform]
|
||||
synonym = arch_synonym(native_machine, host_platform, platform)
|
||||
if synonym is None:
|
||||
# can't build anything on this platform
|
||||
return None
|
||||
|
||||
if synonym is None:
|
||||
# can't build anything on this platform
|
||||
return None
|
||||
|
||||
native_architecture = Architecture(synonym)
|
||||
native_architecture = Architecture(synonym)
|
||||
|
||||
return native_architecture
|
||||
|
||||
@staticmethod
|
||||
def auto_archs(platform: PlatformName) -> set[Architecture]:
|
||||
def auto_archs(platform: PlatformName) -> "set[Architecture]":
|
||||
native_arch = Architecture.native_arch(platform)
|
||||
if native_arch is None:
|
||||
return set() # can't build anything on this platform
|
||||
result = {native_arch}
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def all_archs(platform: PlatformName) -> set[Architecture]:
|
||||
def all_archs(platform: PlatformName) -> "set[Architecture]":
|
||||
all_archs_map = {
|
||||
"linux": {
|
||||
Architecture.x86_64,
|
||||
@@ -130,25 +169,52 @@ class Architecture(Enum):
|
||||
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
|
||||
# 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)
|
||||
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)
|
||||
|
||||
if bitness == "64":
|
||||
return auto_archs - archs_32
|
||||
elif bitness == "32":
|
||||
return auto_archs & archs_32
|
||||
else:
|
||||
assert_never(bitness)
|
||||
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)
|
||||
|
||||
|
||||
def allowed_architectures_check(
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import subprocess
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Dict, List # noqa: TID251
|
||||
from collections.abc import (
|
||||
Callable,
|
||||
Iterable,
|
||||
Mapping,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
class NodeExecutionContext:
|
||||
environment: dict[str, str]
|
||||
input: str
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
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)
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, Protocol
|
||||
@@ -75,12 +73,16 @@ 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:
|
||||
@@ -98,13 +100,8 @@ 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
|
||||
@dataclasses.dataclass(kw_only=True)
|
||||
class ParsedEnvironment:
|
||||
assignments: list[EnvironmentAssignment]
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ a different return code, by defining them all here, we can ensure that they're
|
||||
semantically clear and unique.
|
||||
"""
|
||||
|
||||
import textwrap
|
||||
|
||||
|
||||
class FatalError(BaseException):
|
||||
"""
|
||||
@@ -25,3 +27,79 @@ class NothingToDoError(FatalError):
|
||||
|
||||
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
|
||||
|
||||
+70
-3
@@ -2,11 +2,16 @@
|
||||
These are utilities for the `/bin` scripts, not for the `cibuildwheel` program.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import typing
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from collections.abc import Mapping, Sequence
|
||||
from io import StringIO
|
||||
from typing import Protocol
|
||||
from typing import Any, NotRequired, Protocol
|
||||
|
||||
from cibuildwheel import __version__ as cibw_version
|
||||
|
||||
__all__ = ("Printable", "dump_python_configurations")
|
||||
|
||||
@@ -32,3 +37,65 @@ 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")))
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
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),
|
||||
]
|
||||
+204
-58
@@ -1,15 +1,26 @@
|
||||
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 typing import IO, AnyStr, Final, Tuple
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
from typing import IO, TYPE_CHECKING, AnyStr, Final, Literal
|
||||
|
||||
from .util import CIProvider, detect_ci_provider
|
||||
import humanize
|
||||
|
||||
FoldPattern = Tuple[str, str]
|
||||
from .ci import CIProvider, detect_ci_provider, filter_ansi_codes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .options import Options
|
||||
|
||||
FoldPattern = tuple[str, str]
|
||||
DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "")
|
||||
FOLD_PATTERNS: Final[dict[str, FoldPattern]] = {
|
||||
"azure": ("##[group]{name}", "##[endgroup]"),
|
||||
@@ -23,11 +34,15 @@ 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": "manylinux s390x",
|
||||
"musllinux_s390x": "musllinux s390x",
|
||||
"musllinux_armv7l": "musllinux armv7l",
|
||||
"musllinux_riscv64": "musllinux riscv64",
|
||||
"win32": "Windows 32bit",
|
||||
"win_amd64": "Windows 64bit",
|
||||
"win_arm64": "Windows on ARM 64bit",
|
||||
@@ -35,17 +50,76 @@ PLATFORM_IDENTIFIER_DESCRIPTIONS: Final[dict[str, str]] = {
|
||||
"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: str
|
||||
fold_mode: Literal["azure", "github", "travis", "disabled"]
|
||||
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"):
|
||||
@@ -57,25 +131,28 @@ class Logger:
|
||||
|
||||
ci_provider = detect_ci_provider()
|
||||
|
||||
if ci_provider == CIProvider.azure_pipelines:
|
||||
self.fold_mode = "azure"
|
||||
self.colors_enabled = True
|
||||
match ci_provider:
|
||||
case CIProvider.azure_pipelines:
|
||||
self.fold_mode = "azure"
|
||||
self.colors_enabled = True
|
||||
|
||||
elif ci_provider == CIProvider.github_actions:
|
||||
self.fold_mode = "github"
|
||||
self.colors_enabled = True
|
||||
case CIProvider.github_actions:
|
||||
self.fold_mode = "github"
|
||||
self.colors_enabled = True
|
||||
|
||||
elif ci_provider == CIProvider.travis_ci:
|
||||
self.fold_mode = "travis"
|
||||
self.colors_enabled = True
|
||||
case CIProvider.travis_ci:
|
||||
self.fold_mode = "travis"
|
||||
self.colors_enabled = True
|
||||
|
||||
elif ci_provider == CIProvider.appveyor:
|
||||
self.fold_mode = "disabled"
|
||||
self.colors_enabled = True
|
||||
case CIProvider.appveyor:
|
||||
self.fold_mode = "disabled"
|
||||
self.colors_enabled = True
|
||||
|
||||
else:
|
||||
self.fold_mode = "disabled"
|
||||
self.colors_enabled = file_supports_color(sys.stdout)
|
||||
case _:
|
||||
self.fold_mode = "disabled"
|
||||
self.colors_enabled = file_supports_color(sys.stdout)
|
||||
|
||||
self.summary = []
|
||||
|
||||
def build_start(self, identifier: str) -> None:
|
||||
self.step_end()
|
||||
@@ -89,7 +166,7 @@ class Logger:
|
||||
self.build_start_time = time.time()
|
||||
self.active_build_identifier = identifier
|
||||
|
||||
def build_end(self) -> None:
|
||||
def build_end(self, filename: Path | None) -> None:
|
||||
assert self.build_start_time is not None
|
||||
assert self.active_build_identifier is not None
|
||||
self.step_end()
|
||||
@@ -97,11 +174,14 @@ 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:.2f}s"
|
||||
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)
|
||||
)
|
||||
|
||||
self.build_start_time = None
|
||||
self.active_build_identifier = None
|
||||
|
||||
@@ -116,6 +196,7 @@ 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:
|
||||
@@ -133,24 +214,44 @@ class Logger:
|
||||
|
||||
def notice(self, message: str) -> None:
|
||||
if self.fold_mode == "github":
|
||||
print(f"::notice::{message}\n", file=sys.stderr)
|
||||
print(f"::notice::cibuildwheel: {message}\n", file=sys.stderr)
|
||||
else:
|
||||
c = self.colors
|
||||
print(f"{c.bold}Note{c.end}: {message}\n", file=sys.stderr)
|
||||
print(f"cibuildwheel: {c.bold}note{c.end}: {message}\n", file=sys.stderr)
|
||||
|
||||
def warning(self, message: str) -> None:
|
||||
if self.fold_mode == "github":
|
||||
print(f"::warning::{message}\n", file=sys.stderr)
|
||||
print(f"::warning::cibuildwheel: {message}\n", file=sys.stderr)
|
||||
else:
|
||||
c = self.colors
|
||||
print(f"{c.yellow}Warning{c.end}: {message}\n", file=sys.stderr)
|
||||
print(f"cibuildwheel: {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::{error}\n", file=sys.stderr)
|
||||
print(f"::error::cibuildwheel: {error}\n", file=sys.stderr)
|
||||
else:
|
||||
c = self.colors
|
||||
print(f"{c.bright_red}Error{c.end}: {error}\n", file=sys.stderr)
|
||||
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:
|
||||
@@ -191,6 +292,72 @@ 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)
|
||||
@@ -206,17 +373,22 @@ def build_description_from_identifier(identifier: str) -> str:
|
||||
build_description = ""
|
||||
|
||||
python_interpreter = python_identifier[0:2]
|
||||
python_version = python_identifier[2:]
|
||||
version_parts = python_identifier[2:].split("_")
|
||||
python_version = version_parts[0]
|
||||
|
||||
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]
|
||||
@@ -227,32 +399,6 @@ 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.
|
||||
|
||||
+184
-64
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
@@ -8,35 +7,67 @@ import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import typing
|
||||
import uuid
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path, PurePath, PurePosixPath
|
||||
from types import TracebackType
|
||||
from typing import IO, Dict, Literal
|
||||
from typing import IO, Literal, Self, assert_never
|
||||
|
||||
from ._compat.typing import Self
|
||||
from .typing import PathOrStr, PopenBytes
|
||||
from .util import (
|
||||
CIProvider,
|
||||
call,
|
||||
detect_ci_provider,
|
||||
parse_key_value_string,
|
||||
strtobool,
|
||||
)
|
||||
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
|
||||
|
||||
ContainerEngineName = Literal["docker", "podman"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
# 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)
|
||||
class OCIContainerEngineConfig:
|
||||
name: ContainerEngineName
|
||||
create_args: tuple[str, ...] = field(default_factory=tuple)
|
||||
_: dataclasses.KW_ONLY
|
||||
create_args: tuple[str, ...] = dataclasses.field(default_factory=tuple)
|
||||
disable_host_mount: bool = False
|
||||
|
||||
@staticmethod
|
||||
def from_config_string(config_string: str) -> OCIContainerEngineConfig:
|
||||
@classmethod
|
||||
def from_config_string(cls, config_string: str) -> Self:
|
||||
config_dict = parse_key_value_string(
|
||||
config_string,
|
||||
["name"],
|
||||
@@ -47,7 +78,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 = (
|
||||
@@ -56,10 +87,17 @@ 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 OCIContainerEngineConfig(
|
||||
name=name, create_args=tuple(create_args), disable_host_mount=disable_host_mount
|
||||
)
|
||||
return cls(name=name, create_args=tuple(create_args), disable_host_mount=disable_host_mount)
|
||||
|
||||
def options_summary(self) -> str | dict[str, str]:
|
||||
if not self.create_args:
|
||||
@@ -75,6 +113,58 @@ 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.
|
||||
@@ -87,20 +177,28 @@ 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) as self:
|
||||
>>> with OCIContainer(image=image, oci_platform=oci_platform) as self:
|
||||
... self.call(["echo", "hello world"])
|
||||
... self.call(["cat", "/proc/1/cgroup"])
|
||||
... print(self.get_environment())
|
||||
... print(self.debug_info())
|
||||
"""
|
||||
|
||||
UTILITY_PYTHON = "/opt/python/cp38-cp38/bin/python"
|
||||
UTILITY_PYTHON = "/opt/python/cp39-cp39/bin/python"
|
||||
|
||||
process: PopenBytes
|
||||
process: subprocess.Popen[bytes]
|
||||
bash_stdin: IO[bytes]
|
||||
bash_stdout: IO[bytes]
|
||||
|
||||
@@ -108,7 +206,7 @@ class OCIContainer:
|
||||
self,
|
||||
*,
|
||||
image: str,
|
||||
enforce_32_bit: bool = False,
|
||||
oci_platform: OCIPlatform,
|
||||
cwd: PathOrStr | None = None,
|
||||
engine: OCIContainerEngineConfig = DEFAULT_ENGINE,
|
||||
):
|
||||
@@ -117,14 +215,48 @@ class OCIContainer:
|
||||
raise ValueError(msg)
|
||||
|
||||
self.image = image
|
||||
self.enforce_32_bit = enforce_32_bit
|
||||
self.oci_platform = oci_platform
|
||||
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
|
||||
@@ -133,15 +265,30 @@ 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.enforce_32_bit:
|
||||
if self.oci_platform in {OCIPlatform.i386, OCIPlatform.ARMV7}:
|
||||
# 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
|
||||
container_machine = call(
|
||||
self.engine.name, "run", "--rm", self.image, "uname", "-m", capture_stdout=True
|
||||
).strip()
|
||||
simulate_32_bit = container_machine != "i686"
|
||||
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"}
|
||||
|
||||
shell_args = ["linux32", "/bin/bash"] if simulate_32_bit else ["/bin/bash"]
|
||||
|
||||
@@ -155,6 +302,7 @@ 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,
|
||||
@@ -221,21 +369,17 @@ 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 cf - . | {self.engine.name} exec -i {self.name} tar --no-same-owner -xC {shell_quote(to_path)} -f -",
|
||||
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 -",
|
||||
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,
|
||||
@@ -250,8 +394,7 @@ class OCIContainer:
|
||||
) as exec_process:
|
||||
assert exec_process.stdin
|
||||
with open(from_path, "rb") as from_file:
|
||||
# Bug in mypy, https://github.com/python/mypy/issues/15031
|
||||
shutil.copyfileobj(from_file, exec_process.stdin) # type: ignore[misc]
|
||||
shutil.copyfileobj(from_file, exec_process.stdin)
|
||||
|
||||
exec_process.stdin.close()
|
||||
exec_process.wait()
|
||||
@@ -264,30 +407,7 @@ 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)
|
||||
|
||||
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)
|
||||
call(self.engine.name, "cp", f"{self.name}:{from_path}/.", to_path)
|
||||
|
||||
def glob(self, path: PurePosixPath, pattern: str) -> list[PurePosixPath]:
|
||||
glob_pattern = path.joinpath(pattern)
|
||||
@@ -398,7 +518,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
|
||||
|
||||
+435
-219
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
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]
|
||||
@@ -0,0 +1,702 @@
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,709 @@
|
||||
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
|
||||
@@ -1,38 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import dataclasses
|
||||
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 OrderedDict, Tuple
|
||||
from typing import TYPE_CHECKING, assert_never
|
||||
|
||||
from packaging.version import Version
|
||||
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
|
||||
|
||||
from . import errors
|
||||
from ._compat.typing import assert_never
|
||||
from .architecture import Architecture
|
||||
from .logger import log
|
||||
from .oci_container import OCIContainer, OCIContainerEngineConfig
|
||||
from .options import BuildOptions, 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,
|
||||
)
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
class PythonConfiguration:
|
||||
version: str
|
||||
identifier: str
|
||||
@@ -43,7 +45,7 @@ class PythonConfiguration:
|
||||
return PurePosixPath(self.path_str)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
class BuildStep:
|
||||
platform_configs: list[PythonConfiguration]
|
||||
platform_tag: str
|
||||
@@ -51,13 +53,16 @@ class BuildStep:
|
||||
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]:
|
||||
full_python_configs = read_python_configs("linux")
|
||||
|
||||
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
|
||||
python_configurations = all_python_configurations()
|
||||
|
||||
# return all configurations whose arch is in our `architectures` set,
|
||||
# and match the build/skip rules
|
||||
@@ -96,7 +101,7 @@ 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, OCIContainerEngineConfig], BuildStep]()
|
||||
|
||||
for config in python_configurations:
|
||||
_, platform_tag = config.identifier.split("-", 1)
|
||||
@@ -126,26 +131,33 @@ def check_all_python_exist(
|
||||
*, platform_configs: Iterable[PythonConfiguration], container: OCIContainer
|
||||
) -> None:
|
||||
exist = True
|
||||
has_manylinux_interpreters = True
|
||||
has_manylinux_interpreters = False
|
||||
messages = []
|
||||
|
||||
try:
|
||||
with contextlib.suppress(subprocess.CalledProcessError):
|
||||
# use capture_output to keep quiet
|
||||
container.call(["manylinux-interpreters", "--help"], capture_output=True)
|
||||
except subprocess.CalledProcessError:
|
||||
has_manylinux_interpreters = False
|
||||
has_manylinux_interpreters = True
|
||||
|
||||
for config in platform_configs:
|
||||
python_path = config.path / "bin" / "python"
|
||||
try:
|
||||
if has_manylinux_interpreters:
|
||||
if has_manylinux_interpreters:
|
||||
try:
|
||||
container.call(["manylinux-interpreters", "ensure", config.path.name])
|
||||
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
|
||||
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
|
||||
if not exist:
|
||||
message = "\n".join(messages)
|
||||
raise errors.FatalError(message)
|
||||
@@ -158,6 +170,7 @@ def build_in_container(
|
||||
container: OCIContainer,
|
||||
container_project_path: PurePath,
|
||||
container_package_dir: PurePath,
|
||||
local_tmp_dir: Path,
|
||||
) -> None:
|
||||
container_output_dir = PurePosixPath("/output")
|
||||
|
||||
@@ -173,7 +186,7 @@ def build_in_container(
|
||||
log.step("Running before_all...")
|
||||
|
||||
env = container.get_environment()
|
||||
env["PATH"] = f'/opt/python/cp38-cp38/bin:{env["PATH"]}'
|
||||
env["PATH"] = f"/opt/python/cp39-cp39/bin:{env['PATH']}"
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
env["PIP_ROOT_USER_ACTION"] = "ignore"
|
||||
env = before_all_options.environment.as_dictionary(
|
||||
@@ -191,31 +204,31 @@ 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 or BuildFrontendConfig("pip")
|
||||
use_uv = build_frontend.name == "build[uv]" and Version(config.version) >= Version("3.8")
|
||||
build_frontend = build_options.build_frontend
|
||||
use_uv = build_frontend.name == "build[uv]"
|
||||
pip = ["uv", "pip"] if use_uv else ["pip"]
|
||||
|
||||
dependency_constraint_flags: list[PathOrStr] = []
|
||||
|
||||
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(constraints_file, container_constraints_file)
|
||||
dependency_constraint_flags = ["-c", container_constraints_file]
|
||||
|
||||
log.step("Setting up build environment...")
|
||||
|
||||
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:
|
||||
container_constraints_file = PurePosixPath("/constraints.txt")
|
||||
container.copy_into(local_constraints_file, container_constraints_file)
|
||||
dependency_constraint_flags = ["-c", container_constraints_file]
|
||||
|
||||
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)
|
||||
|
||||
@@ -242,7 +255,7 @@ def build_in_container(
|
||||
print(
|
||||
f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
|
||||
)
|
||||
repaired_wheels = [compatible_wheel]
|
||||
repaired_wheel = compatible_wheel
|
||||
else:
|
||||
if build_options.before_build:
|
||||
log.step("Running before_build...")
|
||||
@@ -260,44 +273,42 @@ def build_in_container(
|
||||
container.call(["rm", "-rf", built_wheel_dir])
|
||||
container.call(["mkdir", "-p", built_wheel_dir])
|
||||
|
||||
extra_flags = split_config_settings(build_options.config_settings, build_frontend.name)
|
||||
extra_flags += build_frontend.args
|
||||
extra_flags = get_build_frontend_extra_flags(
|
||||
build_frontend, build_options.build_verbosity, build_options.config_settings
|
||||
)
|
||||
|
||||
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" or build_frontend.name == "build[uv]":
|
||||
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)
|
||||
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,
|
||||
)
|
||||
else:
|
||||
assert_never(build_frontend)
|
||||
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)
|
||||
|
||||
built_wheel = container.glob(built_wheel_dir, "*.whl")[0]
|
||||
|
||||
@@ -306,22 +317,31 @@ def build_in_container(
|
||||
container.call(["mkdir", "-p", repaired_wheel_dir])
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise NonPlatformWheelError()
|
||||
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
|
||||
build_options.repair_command,
|
||||
wheel=built_wheel,
|
||||
dest_dir=repaired_wheel_dir,
|
||||
package=container_package_dir,
|
||||
project=container_project_path,
|
||||
)
|
||||
container.call(["sh", "-c", repair_command_prepared], env=env)
|
||||
else:
|
||||
container.call(["mv", built_wheel, repaired_wheel_dir])
|
||||
|
||||
repaired_wheels = container.glob(repaired_wheel_dir, "*.whl")
|
||||
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])
|
||||
|
||||
for repaired_wheel in repaired_wheels:
|
||||
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
|
||||
raise AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
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...")
|
||||
@@ -339,18 +359,20 @@ def build_in_container(
|
||||
venv_dir = testing_temp_dir / "venv"
|
||||
|
||||
if use_uv:
|
||||
container.call(["uv", "venv", venv_dir], env=env)
|
||||
container.call(["uv", "venv", venv_dir, "--python", python_bin / "python"], env=env)
|
||||
else:
|
||||
# Use embedded dependencies from virtualenv to ensure determinism
|
||||
venv_args = ["--no-periodic-update", "--pip=embed"]
|
||||
# In Python<3.12, setuptools & wheel are installed as well
|
||||
if Version(config.version) < Version("3.12"):
|
||||
venv_args.extend(("--setuptools=embed", "--wheel=embed"))
|
||||
venv_args = ["--no-periodic-update", "--pip=embed", "--no-setuptools"]
|
||||
if "38" in config.identifier:
|
||||
venv_args.append("--no-wheel")
|
||||
container.call(["python", "-m", "virtualenv", *venv_args, venv_dir], env=env)
|
||||
|
||||
virtualenv_env = env.copy()
|
||||
virtualenv_env["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(
|
||||
@@ -361,14 +383,8 @@ 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(wheel_to_test) + build_options.test_extras],
|
||||
[*pip, "install", str(repaired_wheel) + build_options.test_extras],
|
||||
env=virtualenv_env,
|
||||
)
|
||||
|
||||
@@ -381,26 +397,38 @@ def build_in_container(
|
||||
build_options.test_command,
|
||||
project=container_project_path,
|
||||
package=container_package_dir,
|
||||
wheel=wheel_to_test,
|
||||
wheel=repaired_wheel,
|
||||
)
|
||||
|
||||
test_cwd = testing_temp_dir / "test_cwd"
|
||||
container.call(["mkdir", "-p", test_cwd])
|
||||
container.copy_into(test_fail_cwd_file, test_cwd / "test_fail.py")
|
||||
|
||||
if build_options.test_sources:
|
||||
copy_test_sources(
|
||||
build_options.test_sources,
|
||||
Path.cwd(),
|
||||
test_cwd,
|
||||
copy_into=container.copy_into,
|
||||
)
|
||||
else:
|
||||
# Use the test_fail.py file to raise a nice error if the user
|
||||
# tries to run tests in the cwd
|
||||
container.copy_into(resources.TEST_FAIL_CWD_FILE, test_cwd / "test_fail.py")
|
||||
|
||||
container.call(["sh", "-c", test_command_prepared], cwd=test_cwd, env=virtualenv_env)
|
||||
|
||||
# clean up test environment
|
||||
container.call(["rm", "-rf", testing_temp_dir])
|
||||
|
||||
# move repaired wheels to output
|
||||
# move repaired wheel to output
|
||||
output_wheel: Path | None = None
|
||||
if compatible_wheel is None:
|
||||
container.call(["mkdir", "-p", container_output_dir])
|
||||
container.call(["mv", *repaired_wheels, container_output_dir])
|
||||
built_wheels.extend(
|
||||
container_output_dir / repaired_wheel.name for repaired_wheel in repaired_wheels
|
||||
)
|
||||
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
|
||||
|
||||
log.build_end()
|
||||
log.build_end(output_wheel)
|
||||
|
||||
log.step("Copying wheels back to host...")
|
||||
# copy the output back into the host
|
||||
@@ -408,7 +436,7 @@ def build_in_container(
|
||||
log.step_end()
|
||||
|
||||
|
||||
def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
|
||||
def build(options: Options, tmp_path: Path) -> None:
|
||||
python_configurations = get_python_configurations(
|
||||
options.globals.build_selector, options.globals.architectures
|
||||
)
|
||||
@@ -433,12 +461,12 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
|
||||
except subprocess.CalledProcessError as error:
|
||||
msg = unwrap(
|
||||
f"""
|
||||
cibuildwheel: {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.
|
||||
{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
|
||||
@@ -448,10 +476,11 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
|
||||
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,
|
||||
enforce_32_bit=build_step.platform_tag.endswith("i686"),
|
||||
oci_platform=ARCHITECTURE_OCI_PLATFORM_MAP[architecture],
|
||||
cwd=container_project_path,
|
||||
engine=build_step.container_engine,
|
||||
) as container:
|
||||
@@ -461,6 +490,7 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
|
||||
container=container,
|
||||
container_project_path=container_project_path,
|
||||
container_package_dir=container_package_dir,
|
||||
local_tmp_dir=tmp_path,
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError as error:
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
@@ -8,50 +8,35 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
from collections.abc import Sequence, Set
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Set
|
||||
from pathlib import Path
|
||||
from typing import Literal, Tuple
|
||||
from typing import Literal, assert_never
|
||||
|
||||
from filelock import FileLock
|
||||
from packaging.version import Version
|
||||
|
||||
from . import errors
|
||||
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 (
|
||||
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 (
|
||||
CIBW_CACHE_PATH,
|
||||
AlreadyBuiltWheelError,
|
||||
BuildFrontendConfig,
|
||||
BuildFrontendName,
|
||||
BuildSelector,
|
||||
NonPlatformWheelError,
|
||||
call,
|
||||
combine_constraints,
|
||||
detect_ci_provider,
|
||||
copy_test_sources,
|
||||
download,
|
||||
find_compatible_wheel,
|
||||
find_uv,
|
||||
free_thread_enable_313,
|
||||
get_build_verbosity_extra_flags,
|
||||
get_pip_version,
|
||||
install_certifi_script,
|
||||
move_file,
|
||||
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.lru_cache(maxsize=None)
|
||||
@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)
|
||||
@@ -75,10 +60,10 @@ def get_macos_version() -> tuple[int, int]:
|
||||
capture_stdout=True,
|
||||
)
|
||||
version = tuple(map(int, version_str.split(".")[:2]))
|
||||
return typing.cast(Tuple[int, int], version)
|
||||
return typing.cast("tuple[int, int]", version)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
@functools.cache
|
||||
def get_test_macosx_deployment_target() -> str:
|
||||
version = get_macos_version()
|
||||
if version >= (11, 0):
|
||||
@@ -91,19 +76,22 @@ def get_macos_sdks() -> list[str]:
|
||||
return [m.group(1) for m in re.finditer(r"-sdk (macosx\S+)", output)]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass(frozen=True, kw_only=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]:
|
||||
full_python_configs = read_python_configs("macos")
|
||||
|
||||
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
|
||||
python_configurations = all_python_configurations()
|
||||
|
||||
# filter out configs that don't match any of the selected architectures
|
||||
python_configurations = [
|
||||
@@ -115,7 +103,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:
|
||||
# filter-out some cross-compilation configs with PyPy and GraalPy:
|
||||
# can't build arm64 on x86_64
|
||||
# rosetta allows to build x86_64 on arm64
|
||||
if platform.machine() == "x86_64":
|
||||
@@ -123,7 +111,7 @@ def get_python_configurations(
|
||||
python_configurations = [
|
||||
c
|
||||
for c in python_configurations
|
||||
if not (c.identifier.startswith("pp") and c.identifier.endswith("arm64"))
|
||||
if not (c.identifier.startswith(("pp", "gp")) and c.identifier.endswith("arm64"))
|
||||
]
|
||||
removed_elements = python_configurations_before - set(python_configurations)
|
||||
if removed_elements:
|
||||
@@ -131,7 +119,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.
|
||||
"""
|
||||
)
|
||||
@@ -140,7 +128,7 @@ def get_python_configurations(
|
||||
return python_configurations
|
||||
|
||||
|
||||
def install_cpython(tmp: Path, version: str, url: str, free_threading: bool) -> Path:
|
||||
def install_cpython(_tmp: Path, version: str, url: str, free_threading: bool) -> Path:
|
||||
ft = "T" if free_threading else ""
|
||||
installation_path = Path(f"/Library/Frameworks/Python{ft}.framework/Versions/{version}")
|
||||
with FileLock(CIBW_CACHE_PATH / f"cpython{version}.lock"):
|
||||
@@ -151,21 +139,27 @@ def install_cpython(tmp: Path, version: str, url: str, free_threading: bool) ->
|
||||
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 = (
|
||||
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}"
|
||||
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}
|
||||
"""
|
||||
)
|
||||
raise errors.FatalError(msg)
|
||||
pkg_path = tmp / "Python.pkg"
|
||||
# download the pkg
|
||||
download(url, pkg_path)
|
||||
# install
|
||||
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(free_thread_enable_313.resolve())]
|
||||
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_313.resolve())]
|
||||
elif version.startswith("3.14"):
|
||||
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_314.resolve())]
|
||||
call("sudo", "installer", "-pkg", pkg_path, *args, "-target", "/")
|
||||
pkg_path.unlink()
|
||||
env = os.environ.copy()
|
||||
@@ -173,9 +167,13 @@ def install_cpython(tmp: Path, version: str, url: str, free_threading: bool) ->
|
||||
|
||||
if free_threading:
|
||||
call(installation_path / f"bin/python{version}t", "-m", "ensurepip", env=env)
|
||||
call(installation_path / f"bin/python{version}t", install_certifi_script, env=env)
|
||||
call(
|
||||
installation_path / f"bin/python{version}t",
|
||||
resources.INSTALL_CERTIFI_SCRIPT,
|
||||
env=env,
|
||||
)
|
||||
else:
|
||||
call(installation_path / "bin/python3", install_certifi_script, env=env)
|
||||
call(installation_path / "bin/python3", resources.INSTALL_CERTIFI_SCRIPT, env=env)
|
||||
|
||||
return installation_path / "bin" / (f"python{version}t" if free_threading else "python3")
|
||||
|
||||
@@ -195,20 +193,31 @@ 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_flags: Sequence[PathOrStr],
|
||||
dependency_constraint: Path | None,
|
||||
environment: ParsedEnvironment,
|
||||
build_frontend: BuildFrontendName,
|
||||
) -> tuple[Path, dict[str, str]]:
|
||||
if build_frontend == "build[uv]" and Version(python_configuration.version) < Version("3.8"):
|
||||
build_frontend = "build"
|
||||
|
||||
uv_path = find_uv()
|
||||
use_uv = build_frontend == "build[uv]" and Version(python_configuration.version) >= Version(
|
||||
"3.8"
|
||||
)
|
||||
use_uv = build_frontend == "build[uv]"
|
||||
|
||||
tmp.mkdir()
|
||||
implementation_id = python_configuration.identifier.split("-")[0]
|
||||
@@ -221,12 +230,14 @@ def setup_python(
|
||||
|
||||
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(), (
|
||||
f"{base_python.name} not found, has {list(base_python.parent.iterdir())}"
|
||||
)
|
||||
|
||||
log.step("Setting up build environment...")
|
||||
venv_path = tmp / "venv"
|
||||
@@ -234,7 +245,7 @@ def setup_python(
|
||||
python_configuration.version,
|
||||
base_python,
|
||||
venv_path,
|
||||
dependency_constraint_flags,
|
||||
dependency_constraint,
|
||||
use_uv=use_uv,
|
||||
)
|
||||
venv_bin_path = venv_path / "bin"
|
||||
@@ -250,45 +261,26 @@ 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'
|
||||
if build_frontend == "build[uv]":
|
||||
assert uv_path is not None
|
||||
pip = [str(uv_path), "pip"]
|
||||
else:
|
||||
pip = ["python", "-m", "pip"]
|
||||
|
||||
if not use_uv:
|
||||
call(
|
||||
*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()
|
||||
call("which", "pip", env=env)
|
||||
call("pip", "--version", env=env)
|
||||
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."
|
||||
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)
|
||||
|
||||
# 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"):
|
||||
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("pip", "--version", env=env)
|
||||
|
||||
config_is_arm64 = python_configuration.identifier.endswith("arm64")
|
||||
config_is_universal2 = python_configuration.identifier.endswith("universal2")
|
||||
@@ -296,10 +288,13 @@ 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.13 needs 10.13.
|
||||
# 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.13"):
|
||||
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
|
||||
@@ -316,20 +311,19 @@ def setup_python(
|
||||
)
|
||||
env["MACOSX_DEPLOYMENT_TARGET"] = default_target
|
||||
|
||||
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")
|
||||
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:
|
||||
@@ -354,39 +348,40 @@ def setup_python(
|
||||
env.setdefault("SDKROOT", arm64_compatible_sdks[0])
|
||||
|
||||
log.step("Installing build tools...")
|
||||
if build_frontend == "pip":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"delocate",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
)
|
||||
elif build_frontend == "build":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"delocate",
|
||||
"build[virtualenv]",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
)
|
||||
elif build_frontend == "build[uv]":
|
||||
assert uv_path is not None
|
||||
call(
|
||||
uv_path,
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"delocate",
|
||||
"build[virtualenv, uv]",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
)
|
||||
else:
|
||||
assert_never(build_frontend)
|
||||
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)
|
||||
|
||||
return base_python, env
|
||||
|
||||
@@ -416,10 +411,8 @@ 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 or BuildFrontendConfig("pip")
|
||||
use_uv = build_frontend.name == "build[uv]" and Version(config.version) >= Version(
|
||||
"3.8"
|
||||
)
|
||||
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"
|
||||
@@ -435,22 +428,18 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
config_is_arm64 = config.identifier.endswith("arm64")
|
||||
config_is_universal2 = config.identifier.endswith("universal2")
|
||||
|
||||
dependency_constraint_flags: Sequence[PathOrStr] = []
|
||||
if build_options.dependency_constraints:
|
||||
dependency_constraint_flags = [
|
||||
"-c",
|
||||
build_options.dependency_constraints.get_for_python_version(config.version),
|
||||
]
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version, tmp_dir=identifier_tmp_dir
|
||||
)
|
||||
|
||||
base_python, env = setup_python(
|
||||
identifier_tmp_dir / "build",
|
||||
config,
|
||||
dependency_constraint_flags,
|
||||
constraints_path,
|
||||
build_options.environment,
|
||||
build_frontend.name,
|
||||
)
|
||||
if not use_uv:
|
||||
pip_version = get_pip_version(env)
|
||||
pip_version = None if use_uv else get_pip_version(env)
|
||||
|
||||
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
|
||||
if compatible_wheel:
|
||||
@@ -470,62 +459,53 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
log.step("Building wheel...")
|
||||
built_wheel_dir.mkdir()
|
||||
|
||||
extra_flags = split_config_settings(
|
||||
build_options.config_settings, build_frontend.name
|
||||
extra_flags = get_build_frontend_extra_flags(
|
||||
build_frontend, build_options.build_verbosity, build_options.config_settings
|
||||
)
|
||||
extra_flags += build_frontend.args
|
||||
|
||||
build_env = env.copy()
|
||||
if not use_uv:
|
||||
build_env["VIRTUALENV_PIP"] = pip_version
|
||||
if build_options.dependency_constraints:
|
||||
constraint_path = build_options.dependency_constraints.get_for_python_version(
|
||||
config.version
|
||||
)
|
||||
combine_constraints(
|
||||
build_env, constraint_path, identifier_tmp_dir if use_uv else None
|
||||
)
|
||||
|
||||
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=build_env,
|
||||
)
|
||||
elif build_frontend.name == "build" or build_frontend.name == "build[uv]":
|
||||
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)
|
||||
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,
|
||||
)
|
||||
else:
|
||||
assert_never(build_frontend)
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
||||
|
||||
repaired_wheel_dir.mkdir()
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise NonPlatformWheelError()
|
||||
raise errors.NonPlatformWheelError()
|
||||
|
||||
if build_options.repair_command:
|
||||
log.step("Repairing wheel...")
|
||||
@@ -542,15 +522,20 @@ 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)
|
||||
|
||||
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
|
||||
try:
|
||||
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
|
||||
except StopIteration:
|
||||
raise errors.RepairStepProducedNoWheelError() from None
|
||||
|
||||
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
|
||||
raise AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
|
||||
log.step_end()
|
||||
|
||||
@@ -634,13 +619,6 @@ 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.
|
||||
if not use_uv:
|
||||
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
|
||||
|
||||
venv_dir = identifier_tmp_dir / f"venv-test-{testing_arch}"
|
||||
|
||||
arch_prefix = []
|
||||
uv_arch_args = []
|
||||
if testing_arch != machine_arch:
|
||||
@@ -656,27 +634,28 @@ 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)
|
||||
call("uv", "venv", venv_dir, f"--python={base_python}", env=env)
|
||||
else:
|
||||
pip_install = functools.partial(call_with_arch, *pip, "install")
|
||||
# Use pip version from the initial env to ensure determinism
|
||||
venv_args = ["--no-periodic-update", f"--pip={pip_version}"]
|
||||
# In Python<3.12, setuptools & wheel are installed as well, use virtualenv embedded ones
|
||||
if Version(config.version) < Version("3.12"):
|
||||
venv_args.extend(("--setuptools=embed", "--wheel=embed"))
|
||||
call_with_arch("python", "-m", "virtualenv", *venv_args, venv_dir, env=env)
|
||||
|
||||
virtualenv_env = env.copy()
|
||||
virtualenv_env["MACOSX_DEPLOYMENT_TARGET"] = get_test_macosx_deployment_target()
|
||||
virtualenv_env["PATH"] = os.pathsep.join(
|
||||
[
|
||||
str(venv_dir / "bin"),
|
||||
virtualenv_env["PATH"],
|
||||
]
|
||||
|
||||
virtualenv_env = build_options.test_environment.as_dictionary(
|
||||
prev_environment=virtualenv_env
|
||||
)
|
||||
virtualenv_env["VIRTUAL_ENV"] = str(venv_dir)
|
||||
|
||||
# check that we are using the Python from the virtual environment
|
||||
call_with_arch("which", "python", env=virtualenv_env)
|
||||
@@ -724,31 +703,48 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
# and not the repo code)
|
||||
test_command_prepared = prepare_command(
|
||||
build_options.test_command,
|
||||
project=Path(".").resolve(),
|
||||
project=Path.cwd(),
|
||||
package=build_options.package_dir.resolve(),
|
||||
wheel=repaired_wheel,
|
||||
)
|
||||
|
||||
test_cwd = identifier_tmp_dir / "test_cwd"
|
||||
test_cwd.mkdir(exist_ok=True)
|
||||
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
|
||||
|
||||
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()
|
||||
)
|
||||
|
||||
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(
|
||||
"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
|
||||
f"{repaired_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()
|
||||
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
|
||||
@@ -1,55 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Sequence, Set
|
||||
from dataclasses import dataclass
|
||||
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 .logger import log
|
||||
from .options import Options
|
||||
from .typing import PathOrStr
|
||||
from .util import (
|
||||
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,
|
||||
AlreadyBuiltWheelError,
|
||||
BuildFrontendConfig,
|
||||
BuildSelector,
|
||||
NonPlatformWheelError,
|
||||
call,
|
||||
combine_constraints,
|
||||
copy_test_sources,
|
||||
download,
|
||||
ensure_node,
|
||||
extract_tar,
|
||||
extract_zip,
|
||||
find_compatible_wheel,
|
||||
get_pip_version,
|
||||
move_file,
|
||||
prepare_command,
|
||||
read_python_configs,
|
||||
shell,
|
||||
split_config_settings,
|
||||
test_fail_cwd_file,
|
||||
virtualenv,
|
||||
)
|
||||
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")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
class PythonConfiguration:
|
||||
version: str
|
||||
identifier: str
|
||||
pyodide_version: str
|
||||
emscripten_version: 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:
|
||||
# We don't need to match the emsdk version to the version we install, but
|
||||
# we do for stability
|
||||
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"
|
||||
@@ -67,11 +112,82 @@ def install_emscripten(tmp: Path, version: str) -> Path:
|
||||
return emcc_path
|
||||
|
||||
|
||||
def install_xbuildenv(env: dict[str, str], pyodide_version: str) -> str:
|
||||
pyodide_root = (
|
||||
CIBW_CACHE_PATH
|
||||
/ f".pyodide-xbuildenv-{pyodide_version}/{pyodide_version}/xbuildenv/pyodide-root"
|
||||
)
|
||||
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)
|
||||
@@ -80,44 +196,52 @@ def install_xbuildenv(env: dict[str, str], pyodide_version: str) -> str:
|
||||
# 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(identifier: str) -> Path:
|
||||
implementation_id = identifier.split("-")[0]
|
||||
majorminor = implementation_id[len("cp") :]
|
||||
version_info = (int(majorminor[0]), int(majorminor[1:]))
|
||||
if version_info == sys.version_info[:2]:
|
||||
return Path(sys.executable)
|
||||
|
||||
major_minor = ".".join(str(v) for v in version_info)
|
||||
python_name = f"python{major_minor}"
|
||||
which_python = shutil.which(python_name)
|
||||
if which_python is None:
|
||||
msg = f"CPython {major_minor} is not installed."
|
||||
raise errors.FatalError(msg)
|
||||
return Path(which_python)
|
||||
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,
|
||||
dependency_constraint_flags: Sequence[PathOrStr],
|
||||
constraints_path: Path | None,
|
||||
environment: ParsedEnvironment,
|
||||
user_pyodide_version: str | None,
|
||||
) -> dict[str, str]:
|
||||
base_python = get_base_python(python_configuration.identifier)
|
||||
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, [], use_uv=False)
|
||||
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"
|
||||
@@ -131,62 +255,77 @@ def setup_python(
|
||||
"install",
|
||||
"--upgrade",
|
||||
"pip",
|
||||
*dependency_constraint_flags,
|
||||
*constraint_flags(constraints_path),
|
||||
env=env,
|
||||
cwd=venv_path,
|
||||
)
|
||||
|
||||
env = environment.as_dictionary(prev_environment=env)
|
||||
|
||||
# check what pip version we're on
|
||||
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"):
|
||||
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)
|
||||
|
||||
# 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()
|
||||
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",
|
||||
"auditwheel-emscripten",
|
||||
"build[virtualenv]",
|
||||
"pyodide-build",
|
||||
*dependency_constraint_flags,
|
||||
"build[virtualenv]",
|
||||
*constraint_flags(constraints_path),
|
||||
env=env,
|
||||
)
|
||||
|
||||
log.step("Installing emscripten...")
|
||||
emcc_path = install_emscripten(tmp, python_configuration.emscripten_version)
|
||||
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("Installing Pyodide xbuildenv...")
|
||||
env["PYODIDE_ROOT"] = install_xbuildenv(env, python_configuration.pyodide_version)
|
||||
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]:
|
||||
full_python_configs = read_python_configs("pyodide")
|
||||
|
||||
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
|
||||
python_configurations = [c for c in python_configurations if build_selector(c.identifier)]
|
||||
return python_configurations
|
||||
return [c for c in all_python_configurations() if build_selector(c.identifier)]
|
||||
|
||||
|
||||
def build(options: Options, tmp_path: Path) -> None:
|
||||
@@ -213,7 +352,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 or BuildFrontendConfig("build")
|
||||
build_frontend = build_options.build_frontend
|
||||
|
||||
if build_frontend.name == "pip":
|
||||
msg = "The pyodide platform doesn't support pip frontend"
|
||||
@@ -222,24 +361,23 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
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()
|
||||
|
||||
dependency_constraint_flags: Sequence[PathOrStr] = []
|
||||
if build_options.dependency_constraints:
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
config.version, variant="pyodide"
|
||||
)
|
||||
dependency_constraint_flags = ["-c", constraints_path]
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version, variant="pyodide", tmp_dir=identifier_tmp_dir
|
||||
)
|
||||
|
||||
env = setup_python(
|
||||
identifier_tmp_dir / "build",
|
||||
config,
|
||||
dependency_constraint_flags,
|
||||
build_options.environment,
|
||||
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
|
||||
@@ -253,8 +391,8 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
# directory.
|
||||
oldmounts = ""
|
||||
extra_mounts = [str(identifier_tmp_dir)]
|
||||
if str(Path(".").resolve()).startswith("/tmp"):
|
||||
extra_mounts.append(str(Path(".").resolve()))
|
||||
if Path.cwd().is_relative_to("/tmp"):
|
||||
extra_mounts.append(str(Path.cwd()))
|
||||
|
||||
if "_PYODIDE_EXTRA_MOUNTS" in env:
|
||||
oldmounts = env["_PYODIDE_EXTRA_MOUNTS"] + ":"
|
||||
@@ -277,29 +415,22 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
|
||||
log.step("Building wheel...")
|
||||
|
||||
extra_flags = split_config_settings(build_options.config_settings, "build")
|
||||
extra_flags += build_frontend.args
|
||||
extra_flags = get_build_frontend_extra_flags(
|
||||
build_frontend, build_options.build_verbosity, build_options.config_settings
|
||||
)
|
||||
|
||||
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:
|
||||
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
|
||||
build_env["VIRTUALENV_PIP"] = pip_version
|
||||
call(
|
||||
"pyodide",
|
||||
"build",
|
||||
build_options.package_dir,
|
||||
f"--outdir={built_wheel_dir}",
|
||||
*extra_flags,
|
||||
env=build_env,
|
||||
env=env,
|
||||
)
|
||||
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise NonPlatformWheelError()
|
||||
raise errors.NonPlatformWheelError()
|
||||
|
||||
if build_options.repair_command:
|
||||
log.step("Repairing wheel...")
|
||||
@@ -308,15 +439,18 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
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 AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
|
||||
if build_options.test_command and build_options.test_selector(config.identifier):
|
||||
log.step("Testing wheel...")
|
||||
@@ -349,6 +483,10 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -357,6 +495,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
build_options.before_test,
|
||||
project=".",
|
||||
package=build_options.package_dir,
|
||||
wheel=repaired_wheel,
|
||||
)
|
||||
shell(before_test_prepared, env=virtualenv_env)
|
||||
|
||||
@@ -377,25 +516,38 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
# and not the repo code)
|
||||
test_command_prepared = prepare_command(
|
||||
build_options.test_command,
|
||||
project=Path(".").resolve(),
|
||||
project=Path.cwd(),
|
||||
package=build_options.package_dir.resolve(),
|
||||
)
|
||||
|
||||
test_cwd = identifier_tmp_dir / "test_cwd"
|
||||
test_cwd.mkdir(exist_ok=True)
|
||||
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
|
||||
|
||||
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(
|
||||
"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
|
||||
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
|
||||
)
|
||||
built_wheels.append(output_wheel)
|
||||
log.build_end(output_wheel)
|
||||
|
||||
finally:
|
||||
pass
|
||||
except subprocess.CalledProcessError as error:
|
||||
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
||||
raise errors.FatalError(msg) from error
|
||||
@@ -1,49 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
import platform as platform_module
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
from collections.abc import MutableMapping, Sequence, Set
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from collections.abc import MutableMapping, Set
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
from typing import assert_never
|
||||
|
||||
from filelock import FileLock
|
||||
from packaging.version import Version
|
||||
|
||||
from . import errors
|
||||
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,
|
||||
AlreadyBuiltWheelError,
|
||||
BuildFrontendConfig,
|
||||
BuildFrontendName,
|
||||
BuildSelector,
|
||||
NonPlatformWheelError,
|
||||
call,
|
||||
combine_constraints,
|
||||
download,
|
||||
extract_zip,
|
||||
find_compatible_wheel,
|
||||
find_uv,
|
||||
get_build_verbosity_extra_flags,
|
||||
get_pip_version,
|
||||
move_file,
|
||||
prepare_command,
|
||||
read_python_configs,
|
||||
shell,
|
||||
split_config_settings,
|
||||
test_fail_cwd_file,
|
||||
unwrap,
|
||||
virtualenv,
|
||||
)
|
||||
from .. import errors
|
||||
from ..architecture import Architecture
|
||||
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 CIBW_CACHE_PATH, copy_test_sources, download, extract_zip, move_file
|
||||
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
|
||||
|
||||
|
||||
def get_nuget_args(
|
||||
@@ -70,21 +51,31 @@ def get_nuget_args(
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
class PythonConfiguration:
|
||||
version: str
|
||||
arch: str
|
||||
identifier: str
|
||||
url: str | None = None
|
||||
|
||||
@property
|
||||
def arch(self) -> str:
|
||||
return {
|
||||
"win32": "32",
|
||||
"win_amd64": "64",
|
||||
"win_arm64": "ARM64",
|
||||
}[self.identifier.split("-")[-1]]
|
||||
|
||||
|
||||
def all_python_configurations() -> list[PythonConfiguration]:
|
||||
config_dicts = resources.read_python_configs("windows")
|
||||
return [PythonConfiguration(**item) for item in config_dicts]
|
||||
|
||||
|
||||
def get_python_configurations(
|
||||
build_selector: BuildSelector,
|
||||
architectures: Set[Architecture],
|
||||
) -> list[PythonConfiguration]:
|
||||
full_python_configs = read_python_configs("windows")
|
||||
|
||||
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
|
||||
python_configurations = all_python_configurations()
|
||||
|
||||
map_arch = {"32": Architecture.x86, "64": Architecture.AMD64, "ARM64": Architecture.ARM64}
|
||||
|
||||
@@ -98,7 +89,7 @@ def get_python_configurations(
|
||||
return python_configurations
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
@cache
|
||||
def _ensure_nuget() -> Path:
|
||||
nuget = CIBW_CACHE_PATH / "nuget.exe"
|
||||
with FileLock(str(nuget) + ".lock"):
|
||||
@@ -140,6 +131,20 @@ def install_pypy(tmp: Path, arch: str, url: str) -> Path:
|
||||
return installation_path / "python.exe"
|
||||
|
||||
|
||||
def install_graalpy(tmp: Path, url: str) -> Path:
|
||||
zip_filename = url.rsplit("/", 1)[-1]
|
||||
extension = ".zip"
|
||||
assert zip_filename.endswith(extension)
|
||||
installation_path = CIBW_CACHE_PATH / zip_filename[: -len(extension)]
|
||||
with FileLock(str(installation_path) + ".lock"):
|
||||
if not installation_path.exists():
|
||||
graalpy_zip = tmp / zip_filename
|
||||
download(url, graalpy_zip)
|
||||
# Extract to the parent directory because the zip file still contains a directory
|
||||
extract_zip(graalpy_zip, installation_path.parent)
|
||||
return installation_path / "bin" / "graalpy.exe"
|
||||
|
||||
|
||||
def setup_setuptools_cross_compile(
|
||||
tmp: Path,
|
||||
python_configuration: PythonConfiguration,
|
||||
@@ -154,6 +159,16 @@ def setup_setuptools_cross_compile(
|
||||
# set the platform name
|
||||
map_plat = {"32": "win32", "64": "win-amd64", "ARM64": "win-arm64"}
|
||||
plat_name = map_plat[python_configuration.arch]
|
||||
|
||||
# Set environment variable so that setuptools._distutils.get_platform()
|
||||
# identifies the target, not the host
|
||||
vscmd_arg_tgt_arch = {"32": "x86", "64": "x64", "ARM64": "arm64"}
|
||||
current_tgt_arch = vscmd_arg_tgt_arch[python_configuration.arch]
|
||||
if (env.get("VSCMD_ARG_TGT_ARCH") or current_tgt_arch) != current_tgt_arch:
|
||||
msg = f"VSCMD_ARG_TGT_ARCH must be set to {current_tgt_arch!r}, got {env['VSCMD_ARG_TGT_ARCH']!r}. If you're setting up MSVC yourself (e.g. using vcvarsall.bat or msvc-dev-cmd), make sure to target the right architecture. Alternatively, run cibuildwheel without configuring MSVC, and let the build backend handle it."
|
||||
raise errors.FatalError(msg)
|
||||
env["VSCMD_ARG_TGT_ARCH"] = current_tgt_arch
|
||||
|
||||
# (This file must be default/locale encoding, so we can't pass 'encoding')
|
||||
distutils_cfg.write_text(
|
||||
textwrap.dedent(
|
||||
@@ -217,10 +232,15 @@ def setup_rust_cross_compile(
|
||||
)
|
||||
|
||||
|
||||
def can_use_uv(python_configuration: PythonConfiguration) -> bool:
|
||||
conditions = (not python_configuration.identifier.startswith("pp38-"),)
|
||||
return all(conditions)
|
||||
|
||||
|
||||
def setup_python(
|
||||
tmp: Path,
|
||||
python_configuration: PythonConfiguration,
|
||||
dependency_constraint_flags: Sequence[PathOrStr],
|
||||
dependency_constraint: Path | None,
|
||||
environment: ParsedEnvironment,
|
||||
build_frontend: BuildFrontendName,
|
||||
) -> tuple[Path, dict[str, str]]:
|
||||
@@ -241,14 +261,17 @@ def setup_python(
|
||||
elif implementation_id.startswith("pp"):
|
||||
assert python_configuration.url is not None
|
||||
base_python = install_pypy(tmp, python_configuration.arch, python_configuration.url)
|
||||
elif implementation_id.startswith("gp"):
|
||||
base_python = install_graalpy(tmp, python_configuration.url or "")
|
||||
else:
|
||||
msg = "Unknown Python implementation"
|
||||
raise ValueError(msg)
|
||||
assert base_python.exists()
|
||||
|
||||
use_uv = build_frontend == "build[uv]" and Version(python_configuration.version) >= Version(
|
||||
"3.8"
|
||||
)
|
||||
if build_frontend == "build[uv]" and not can_use_uv(python_configuration):
|
||||
build_frontend = "build"
|
||||
|
||||
use_uv = build_frontend == "build[uv]"
|
||||
uv_path = find_uv()
|
||||
|
||||
log.step("Setting up build environment...")
|
||||
@@ -257,7 +280,7 @@ def setup_python(
|
||||
python_configuration.version,
|
||||
base_python,
|
||||
venv_path,
|
||||
dependency_constraint_flags,
|
||||
dependency_constraint,
|
||||
use_uv=use_uv,
|
||||
)
|
||||
|
||||
@@ -266,70 +289,92 @@ def setup_python(
|
||||
env["PYTHON_ARCH"] = python_configuration.arch
|
||||
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.exe'
|
||||
if not use_uv:
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"pip",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
cwd=venv_path,
|
||||
)
|
||||
|
||||
# update env with results from CIBW_ENVIRONMENT
|
||||
env = environment.as_dictionary(prev_environment=env)
|
||||
|
||||
# check what Python version we're on
|
||||
call("where", "python", env=env)
|
||||
call("python", "--version", env=env)
|
||||
call("python", "-c", "\"import struct; print(struct.calcsize('P') * 8)\"", env=env)
|
||||
where_python = call("where", "python", env=env, capture_stdout=True).splitlines()[0].strip()
|
||||
print(where_python)
|
||||
if where_python != str(venv_path / "Scripts" / "python.exe"):
|
||||
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)
|
||||
call("python", "-c", "\"import struct; print(struct.calcsize('P') * 8)\"", env=env)
|
||||
|
||||
# check what pip version we're on
|
||||
if not use_uv:
|
||||
assert (venv_path / "Scripts" / "pip.exe").exists()
|
||||
where_pip = call("where", "pip", env=env, capture_stdout=True).splitlines()[0].strip()
|
||||
print(where_pip)
|
||||
if where_pip.strip() != str(venv_path / "Scripts" / "pip.exe"):
|
||||
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)
|
||||
|
||||
log.step("Installing build tools...")
|
||||
if build_frontend == "build":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"build[virtualenv]",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
)
|
||||
elif build_frontend == "build[uv]":
|
||||
assert uv_path is not None
|
||||
call(
|
||||
uv_path,
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"build[virtualenv]",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
)
|
||||
match build_frontend:
|
||||
case "build":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"build[virtualenv]",
|
||||
*constraint_flags(dependency_constraint),
|
||||
env=env,
|
||||
)
|
||||
case "build[uv]":
|
||||
assert uv_path is not None
|
||||
call(
|
||||
uv_path,
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"build[virtualenv]",
|
||||
*constraint_flags(dependency_constraint),
|
||||
env=env,
|
||||
)
|
||||
|
||||
if python_libs_base:
|
||||
# Set up the environment for various backends to enable cross-compilation
|
||||
setup_setuptools_cross_compile(tmp, python_configuration, python_libs_base, env)
|
||||
setup_rust_cross_compile(tmp, python_configuration, python_libs_base, env)
|
||||
|
||||
if implementation_id.startswith("gp311"):
|
||||
# GraalPy 24 fails to discover compilers, setup the relevant environment
|
||||
# variables. Adapted from
|
||||
# https://github.com/microsoft/vswhere/wiki/Start-Developer-Command-Prompt
|
||||
# Remove when GraalPy 24.x is dropped.
|
||||
vcpath = call(
|
||||
Path(os.environ["PROGRAMFILES(X86)"])
|
||||
/ "Microsoft Visual Studio"
|
||||
/ "Installer"
|
||||
/ "vswhere.exe",
|
||||
"-products",
|
||||
"*",
|
||||
"-latest",
|
||||
"-property",
|
||||
"installationPath",
|
||||
capture_stdout=True,
|
||||
).strip()
|
||||
log.notice(f"Discovering Visual Studio for GraalPy at {vcpath}")
|
||||
vcvars_file = tmp / "vcvars.json"
|
||||
call(
|
||||
f"{vcpath}\\Common7\\Tools\\vsdevcmd.bat",
|
||||
"-no_logo",
|
||||
"-arch=amd64",
|
||||
"-host_arch=amd64",
|
||||
"&&",
|
||||
"python",
|
||||
"-c",
|
||||
# this command needs to be one line for Windows reasons
|
||||
"import sys, json, pathlib, os; pathlib.Path(sys.argv[1]).write_text(json.dumps(dict(os.environ)))",
|
||||
vcvars_file,
|
||||
env=env,
|
||||
)
|
||||
with open(vcvars_file, encoding="utf-8") as f:
|
||||
vcvars = json.load(f)
|
||||
env.update(vcvars)
|
||||
|
||||
return base_python, env
|
||||
|
||||
|
||||
@@ -357,10 +402,9 @@ 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 or BuildFrontendConfig("pip")
|
||||
use_uv = build_frontend.name == "build[uv]" and Version(config.version) >= Version(
|
||||
"3.8"
|
||||
)
|
||||
build_frontend = build_options.build_frontend
|
||||
|
||||
use_uv = build_frontend.name == "build[uv]" and can_use_uv(config)
|
||||
log.build_start(config.identifier)
|
||||
|
||||
identifier_tmp_dir = tmp_path / config.identifier
|
||||
@@ -368,23 +412,20 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
built_wheel_dir = identifier_tmp_dir / "built_wheel"
|
||||
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
|
||||
|
||||
dependency_constraint_flags: Sequence[PathOrStr] = []
|
||||
if build_options.dependency_constraints:
|
||||
dependency_constraint_flags = [
|
||||
"-c",
|
||||
build_options.dependency_constraints.get_for_python_version(config.version),
|
||||
]
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version,
|
||||
tmp_dir=identifier_tmp_dir,
|
||||
)
|
||||
|
||||
# install Python
|
||||
base_python, env = setup_python(
|
||||
identifier_tmp_dir / "build",
|
||||
config,
|
||||
dependency_constraint_flags,
|
||||
constraints_path,
|
||||
build_options.environment,
|
||||
build_frontend.name,
|
||||
)
|
||||
if not use_uv:
|
||||
pip_version = get_pip_version(env)
|
||||
pip_version = None if use_uv else get_pip_version(env)
|
||||
|
||||
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
|
||||
if compatible_wheel:
|
||||
@@ -407,54 +448,61 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
log.step("Building wheel...")
|
||||
built_wheel_dir.mkdir()
|
||||
|
||||
extra_flags = split_config_settings(
|
||||
build_options.config_settings, build_frontend.name
|
||||
extra_flags = get_build_frontend_extra_flags(
|
||||
build_frontend, build_options.build_verbosity, build_options.config_settings
|
||||
)
|
||||
extra_flags += build_frontend.args
|
||||
|
||||
build_env = env.copy()
|
||||
if not use_uv:
|
||||
build_env["VIRTUALENV_PIP"] = pip_version
|
||||
if (
|
||||
config.identifier.startswith("gp311")
|
||||
and build_frontend.name == "build"
|
||||
and "--no-isolation" not in extra_flags
|
||||
and "-n" not in extra_flags
|
||||
):
|
||||
# GraalPy 24 fails to discover its standard library when a venv is created
|
||||
# from a virtualenv seeded executable. See
|
||||
# https://github.com/oracle/graalpython/issues/491 and remove this once
|
||||
# GraalPy 24 is dropped.
|
||||
log.notice(
|
||||
"Disabling build isolation to workaround GraalPy bug. If the build fails, consider using pip or build[uv] as build frontend."
|
||||
)
|
||||
shell("graalpy -m pip install setuptools wheel", env=env)
|
||||
extra_flags = [*extra_flags, "-n"]
|
||||
|
||||
if build_options.dependency_constraints:
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
config.version
|
||||
)
|
||||
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
|
||||
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",
|
||||
options.globals.package_dir.resolve(),
|
||||
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.append("--installer=uv")
|
||||
|
||||
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",
|
||||
options.globals.package_dir.resolve(),
|
||||
f"--wheel-dir={built_wheel_dir}",
|
||||
"--no-deps",
|
||||
*extra_flags,
|
||||
env=build_env,
|
||||
)
|
||||
elif build_frontend.name == "build" or build_frontend.name == "build[uv]":
|
||||
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)
|
||||
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,
|
||||
)
|
||||
else:
|
||||
assert_never(build_frontend)
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"build",
|
||||
build_options.package_dir,
|
||||
"--wheel",
|
||||
f"--outdir={built_wheel_dir}",
|
||||
*extra_flags,
|
||||
env=env,
|
||||
)
|
||||
case _:
|
||||
assert_never(build_frontend)
|
||||
|
||||
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
||||
|
||||
@@ -462,7 +510,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
repaired_wheel_dir.mkdir()
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise NonPlatformWheelError()
|
||||
raise errors.NonPlatformWheelError()
|
||||
|
||||
if build_options.repair_command:
|
||||
log.step("Repairing wheel...")
|
||||
@@ -470,15 +518,20 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
build_options.repair_command,
|
||||
wheel=built_wheel,
|
||||
dest_dir=repaired_wheel_dir,
|
||||
package=build_options.package_dir,
|
||||
project=".",
|
||||
)
|
||||
shell(repair_command_prepared, env=env)
|
||||
else:
|
||||
shutil.move(str(built_wheel), repaired_wheel_dir)
|
||||
|
||||
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
|
||||
try:
|
||||
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
|
||||
except StopIteration:
|
||||
raise errors.RepairStepProducedNoWheelError() from None
|
||||
|
||||
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
|
||||
raise AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
|
||||
test_selected = options.globals.test_selector(config.identifier)
|
||||
if test_selected and config.arch == "ARM64" != platform_module.machine():
|
||||
@@ -496,29 +549,20 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
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:
|
||||
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
|
||||
|
||||
venv_dir = identifier_tmp_dir / "venv-test"
|
||||
virtualenv_env = virtualenv(
|
||||
config.version,
|
||||
base_python,
|
||||
venv_dir,
|
||||
None,
|
||||
use_uv=use_uv,
|
||||
env=env,
|
||||
pip_version=pip_version,
|
||||
)
|
||||
|
||||
if use_uv:
|
||||
call("uv", "venv", venv_dir, f"--python={base_python}", env=env)
|
||||
else:
|
||||
# Use pip version from the initial env to ensure determinism
|
||||
venv_args = ["--no-periodic-update", f"--pip={pip_version}"]
|
||||
# In Python<3.12, setuptools & wheel are installed as well, use virtualenv embedded ones
|
||||
if Version(config.version) < Version("3.12"):
|
||||
venv_args.extend(("--setuptools=embed", "--wheel=embed"))
|
||||
call("python", "-m", "virtualenv", *venv_args, venv_dir, env=env)
|
||||
|
||||
virtualenv_env = env.copy()
|
||||
virtualenv_env["PATH"] = os.pathsep.join(
|
||||
[
|
||||
str(venv_dir / "Scripts"),
|
||||
virtualenv_env["PATH"],
|
||||
]
|
||||
virtualenv_env = build_options.test_environment.as_dictionary(
|
||||
prev_environment=virtualenv_env
|
||||
)
|
||||
virtualenv_env["VIRTUAL_ENV"] = str(venv_dir)
|
||||
|
||||
# check that we are using the Python from the virtual environment
|
||||
call("where", "python", env=virtualenv_env)
|
||||
@@ -548,25 +592,36 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
# 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_cwd = identifier_tmp_dir / "test_cwd"
|
||||
test_cwd.mkdir()
|
||||
|
||||
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())
|
||||
|
||||
test_command_prepared = prepare_command(
|
||||
build_options.test_command,
|
||||
project=Path(".").resolve(),
|
||||
project=Path.cwd(),
|
||||
package=options.globals.package_dir.resolve(),
|
||||
wheel=repaired_wheel,
|
||||
)
|
||||
test_cwd = identifier_tmp_dir / "test_cwd"
|
||||
test_cwd.mkdir()
|
||||
(test_cwd / "test_fail.py").write_text(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 (remove if already exists)
|
||||
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(
|
||||
"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
|
||||
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
|
||||
)
|
||||
built_wheels.append(output_wheel)
|
||||
|
||||
@@ -575,7 +630,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
# don't want to abort a build because of that)
|
||||
shutil.rmtree(identifier_tmp_dir, ignore_errors=True)
|
||||
|
||||
log.build_end()
|
||||
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
|
||||
@@ -1,11 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import configparser
|
||||
import contextlib
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ._compat import tomllib
|
||||
import dependency_groups
|
||||
|
||||
|
||||
def get_parent(node: ast.AST | None, depth: int = 1) -> ast.AST | None:
|
||||
@@ -15,32 +14,16 @@ def get_parent(node: ast.AST | None, depth: int = 1) -> ast.AST | None:
|
||||
|
||||
|
||||
def is_main(parent: ast.AST | None) -> bool:
|
||||
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__"}
|
||||
|
||||
return len(consts) == 1
|
||||
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
|
||||
|
||||
|
||||
class Analyzer(ast.NodeVisitor):
|
||||
@@ -66,12 +49,10 @@ class Analyzer(ast.NodeVisitor):
|
||||
parent is not None and get_parent(parent) is None and is_main(get_parent(node, 3))
|
||||
)
|
||||
|
||||
if (
|
||||
node.arg == "python_requires"
|
||||
and isinstance(node.value, ast.Constant)
|
||||
and (unnested or name_main_unnested)
|
||||
):
|
||||
self.requires_python = node.value.value
|
||||
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
|
||||
|
||||
|
||||
def setup_py_python_requires(content: str) -> str | None:
|
||||
@@ -84,15 +65,12 @@ def setup_py_python_requires(content: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def get_requires_python_str(package_dir: Path) -> str | None:
|
||||
def get_requires_python_str(package_dir: Path, pyproject_toml: dict[str, Any] | None) -> 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(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"])
|
||||
with contextlib.suppress(KeyError, IndexError, TypeError):
|
||||
return str((pyproject_toml or {})["project"]["requires-python"])
|
||||
|
||||
# Read in from setup.cfg:options.python_requires
|
||||
config = configparser.ConfigParser()
|
||||
@@ -106,3 +84,26 @@ def get_requires_python_str(package_dir: Path) -> str | None:
|
||||
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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import _cross_venv; _cross_venv.initialize()
|
||||
@@ -0,0 +1,87 @@
|
||||
# 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.
|
||||
@@ -1,7 +1,5 @@
|
||||
[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" },
|
||||
@@ -9,8 +7,8 @@ python_configurations = [
|
||||
{ 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 = "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 = "cp314-manylinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-manylinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "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" },
|
||||
@@ -18,12 +16,14 @@ python_configurations = [
|
||||
{ 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 = "pp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
|
||||
{ identifier = "cp314-manylinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "pp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
|
||||
{ identifier = "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 = "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 = "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 = "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" },
|
||||
@@ -31,8 +31,8 @@ python_configurations = [
|
||||
{ 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 = "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 = "cp314-manylinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-manylinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "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" },
|
||||
@@ -40,8 +40,8 @@ python_configurations = [
|
||||
{ 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 = "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 = "cp314-manylinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-manylinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "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" },
|
||||
@@ -49,16 +49,36 @@ python_configurations = [
|
||||
{ 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 = "pp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
|
||||
{ identifier = "cp314-manylinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-manylinux_armv7l", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-manylinux_armv7l", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-manylinux_armv7l", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-manylinux_armv7l", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-manylinux_armv7l", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "cp313-manylinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-manylinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-manylinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-manylinux_riscv64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-manylinux_riscv64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-manylinux_riscv64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-manylinux_riscv64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-manylinux_riscv64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "cp313-manylinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-manylinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-manylinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "pp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
|
||||
{ identifier = "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 = "pp37-manylinux_i686", version = "3.7", path_str = "/opt/python/pp37-pypy37_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 = "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 = "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 = "pp311-manylinux_i686", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
|
||||
{ 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" },
|
||||
@@ -66,8 +86,8 @@ python_configurations = [
|
||||
{ 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 = "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 = "cp314-musllinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-musllinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-musllinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "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" },
|
||||
@@ -75,8 +95,8 @@ python_configurations = [
|
||||
{ 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 = "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 = "cp314-musllinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-musllinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-musllinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "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" },
|
||||
@@ -84,8 +104,8 @@ python_configurations = [
|
||||
{ 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 = "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 = "cp314-musllinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-musllinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-musllinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "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" },
|
||||
@@ -93,8 +113,8 @@ python_configurations = [
|
||||
{ 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 = "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 = "cp314-musllinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-musllinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-musllinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "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" },
|
||||
@@ -102,12 +122,30 @@ python_configurations = [
|
||||
{ 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" },
|
||||
@@ -120,57 +158,91 @@ python_configurations = [
|
||||
{ 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.4/python-3.12.4-macos11.pkg" },
|
||||
{ identifier = "cp312-macosx_arm64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.4/python-3.12.4-macos11.pkg" },
|
||||
{ identifier = "cp312-macosx_universal2", version = "3.12", url = "https://www.python.org/ftp/python/3.12.4/python-3.12.4-macos11.pkg" },
|
||||
{ identifier = "cp313-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.0/python-3.13.0b3-macos11.pkg" },
|
||||
{ identifier = "cp313-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.0/python-3.13.0b3-macos11.pkg" },
|
||||
{ identifier = "cp313-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.0/python-3.13.0b3-macos11.pkg" },
|
||||
{ identifier = "cp313t-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.0/python-3.13.0b3-macos11.pkg" },
|
||||
{ identifier = "cp313t-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.0/python-3.13.0b3-macos11.pkg" },
|
||||
{ identifier = "cp313t-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.0/python-3.13.0b3-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 = "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.9/python-3.13.9-macos11.pkg" },
|
||||
{ identifier = "cp313-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.9/python-3.13.9-macos11.pkg" },
|
||||
{ identifier = "cp313-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.9/python-3.13.9-macos11.pkg" },
|
||||
{ identifier = "cp313t-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.9/python-3.13.9-macos11.pkg" },
|
||||
{ identifier = "cp313t-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.9/python-3.13.9-macos11.pkg" },
|
||||
{ identifier = "cp313t-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.9/python-3.13.9-macos11.pkg" },
|
||||
{ identifier = "cp314-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0-macos11.pkg" },
|
||||
{ identifier = "cp314-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0-macos11.pkg" },
|
||||
{ identifier = "cp314-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0-macos11.pkg" },
|
||||
{ identifier = "cp314t-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0-macos11.pkg" },
|
||||
{ identifier = "cp314t-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0-macos11.pkg" },
|
||||
{ identifier = "cp314t-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0-macos11.pkg" },
|
||||
{ identifier = "pp38-macosx_x86_64", version = "3.8", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_x86_64.tar.bz2" },
|
||||
{ identifier = "pp38-macosx_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.16-macos_x86_64.tar.bz2" },
|
||||
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-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" },
|
||||
]
|
||||
|
||||
[windows]
|
||||
python_configurations = [
|
||||
{ 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.9", arch = "32" },
|
||||
{ identifier = "cp311-win_amd64", version = "3.11.9", arch = "64" },
|
||||
{ identifier = "cp312-win32", version = "3.12.4", arch = "32" },
|
||||
{ identifier = "cp312-win_amd64", version = "3.12.4", arch = "64" },
|
||||
{ identifier = "cp313-win32", version = "3.13.0-b3", arch = "32" },
|
||||
{ identifier = "cp313t-win32", version = "3.13.0-b3", arch = "32" },
|
||||
{ identifier = "cp313-win_amd64", version = "3.13.0-b3", arch = "64" },
|
||||
{ identifier = "cp313t-win_amd64", version = "3.13.0-b3", 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.9", arch = "ARM64" },
|
||||
{ identifier = "cp312-win_arm64", version = "3.12.4", arch = "ARM64" },
|
||||
{ identifier = "cp313-win_arm64", version = "3.13.0-b3", arch = "ARM64" },
|
||||
{ identifier = "cp313t-win_arm64", version = "3.13.0-b3", 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.16-win64.zip" },
|
||||
{ identifier = "pp310-win_amd64", version = "3.10", arch = "64", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.16-win64.zip" },
|
||||
{ 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.9" },
|
||||
{ identifier = "cp313t-win32", version = "3.13.9" },
|
||||
{ identifier = "cp313-win_amd64", version = "3.13.9" },
|
||||
{ identifier = "cp313t-win_amd64", version = "3.13.9" },
|
||||
{ identifier = "cp314-win32", version = "3.14.0" },
|
||||
{ identifier = "cp314t-win32", version = "3.14.0" },
|
||||
{ identifier = "cp314-win_amd64", version = "3.14.0" },
|
||||
{ identifier = "cp314t-win_amd64", version = "3.14.0" },
|
||||
{ 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.9" },
|
||||
{ identifier = "cp313t-win_arm64", version = "3.13.9" },
|
||||
{ identifier = "cp314-win_arm64", version = "3.14.0" },
|
||||
{ identifier = "cp314t-win_arm64", version = "3.14.0" },
|
||||
{ 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.1", pyodide_version = "0.26.1", emscripten_version = "3.1.58", node_version = "v20" },
|
||||
{ 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.8/python-3.13.8-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.8/python-3.13.8-x86_64-linux-android.tar.gz" },
|
||||
{ identifier = "cp314-android_arm64_v8a", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0-aarch64-linux-android.tar.gz" },
|
||||
{ identifier = "cp314-android_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0-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" },
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json",
|
||||
"$defs": {
|
||||
"inherit": {
|
||||
@@ -10,7 +10,19 @@
|
||||
],
|
||||
"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."
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "cibuildwheel's settings.",
|
||||
@@ -96,7 +108,7 @@
|
||||
},
|
||||
"build-frontend": {
|
||||
"default": "default",
|
||||
"description": "Set the tool to use to build, either \"pip\" (default for now), \"build\", or \"build[uv]\"",
|
||||
"description": "Set the tool to use to build, either \"build\" (default), \"build[uv]\", or \"pip\"",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
@@ -225,9 +237,62 @@
|
||||
"dependency-versions": {
|
||||
"default": "pinned",
|
||||
"description": "Specify how cibuildwheel controls the versions of the tools it uses",
|
||||
"type": "string",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"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": [
|
||||
@@ -261,17 +326,16 @@
|
||||
],
|
||||
"title": "CIBW_ENVIRONMENT_PASS"
|
||||
},
|
||||
"free-threaded-support": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "The project supports free-threaded builds of Python (PEP703)",
|
||||
"title": "CIBW_FREE_THREADED_SUPPORT"
|
||||
},
|
||||
"manylinux-aarch64-image": {
|
||||
"type": "string",
|
||||
"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",
|
||||
@@ -297,6 +361,11 @@
|
||||
"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",
|
||||
@@ -312,6 +381,11 @@
|
||||
"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",
|
||||
@@ -322,6 +396,11 @@
|
||||
"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",
|
||||
@@ -332,6 +411,26 @@
|
||||
"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": [
|
||||
@@ -392,6 +491,36 @@
|
||||
],
|
||||
"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": [
|
||||
@@ -422,6 +551,57 @@
|
||||
],
|
||||
"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",
|
||||
@@ -456,6 +636,9 @@
|
||||
"before-build": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"before-test": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
@@ -480,8 +663,17 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -518,6 +710,9 @@
|
||||
"manylinux-aarch64-image": {
|
||||
"$ref": "#/properties/manylinux-aarch64-image"
|
||||
},
|
||||
"manylinux-armv7l-image": {
|
||||
"$ref": "#/properties/manylinux-armv7l-image"
|
||||
},
|
||||
"manylinux-i686-image": {
|
||||
"$ref": "#/properties/manylinux-i686-image"
|
||||
},
|
||||
@@ -533,6 +728,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -542,18 +740,30 @@
|
||||
"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"
|
||||
},
|
||||
@@ -563,8 +773,20 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,6 +828,9 @@
|
||||
"manylinux-aarch64-image": {
|
||||
"$ref": "#/properties/manylinux-aarch64-image"
|
||||
},
|
||||
"manylinux-armv7l-image": {
|
||||
"$ref": "#/properties/manylinux-armv7l-image"
|
||||
},
|
||||
"manylinux-i686-image": {
|
||||
"$ref": "#/properties/manylinux-i686-image"
|
||||
},
|
||||
@@ -621,6 +846,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -630,18 +858,30 @@
|
||||
"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": [
|
||||
@@ -664,8 +904,20 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -700,6 +952,12 @@
|
||||
"environment": {
|
||||
"$ref": "#/properties/environment"
|
||||
},
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/properties/xbuild-tools"
|
||||
},
|
||||
"pyodide-version": {
|
||||
"$ref": "#/properties/pyodide-version"
|
||||
},
|
||||
"repair-wheel-command": {
|
||||
"$ref": "#/properties/repair-wheel-command"
|
||||
},
|
||||
@@ -709,8 +967,20 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -745,6 +1015,12 @@
|
||||
"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": [
|
||||
@@ -767,8 +1043,20 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -803,6 +1091,12 @@
|
||||
"environment": {
|
||||
"$ref": "#/properties/environment"
|
||||
},
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/properties/xbuild-tools"
|
||||
},
|
||||
"pyodide-version": {
|
||||
"$ref": "#/properties/pyodide-version"
|
||||
},
|
||||
"repair-wheel-command": {
|
||||
"$ref": "#/properties/repair-wheel-command"
|
||||
},
|
||||
@@ -812,8 +1106,146 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,122 +2,116 @@
|
||||
# nox -s update_constraints
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
anyio==4.4.0
|
||||
anyio==4.11.0
|
||||
# via httpx
|
||||
auditwheel-emscripten==0.0.15
|
||||
auditwheel-emscripten==0.2.0
|
||||
# via pyodide-build
|
||||
build==1.2.1
|
||||
build==1.2.2.post1
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# pyodide-build
|
||||
certifi==2024.6.2
|
||||
certifi==2025.10.5
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
# requests
|
||||
charset-normalizer==3.3.2
|
||||
charset-normalizer==3.4.4
|
||||
# via requests
|
||||
click==8.1.7
|
||||
# via typer
|
||||
cloudpickle==3.0.0
|
||||
# via loky
|
||||
cmake==3.29.6
|
||||
# via pyodide-build
|
||||
distlib==0.3.8
|
||||
click==8.1.8
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# typer
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.15.4
|
||||
filelock==3.20.0
|
||||
# via virtualenv
|
||||
h11==0.14.0
|
||||
h11==0.16.0
|
||||
# via httpcore
|
||||
httpcore==1.0.5
|
||||
httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.27.0
|
||||
httpx==0.28.1
|
||||
# via unearth
|
||||
idna==3.7
|
||||
idna==3.11
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
# requests
|
||||
leb128==1.0.8
|
||||
# via auditwheel-emscripten
|
||||
loky==3.4.1
|
||||
# via pyodide-build
|
||||
markdown-it-py==3.0.0
|
||||
markdown-it-py==4.0.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==24.1
|
||||
packaging==25.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# build
|
||||
# pyodide-build
|
||||
# unearth
|
||||
pip==24.1.1
|
||||
pip==25.3
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
platformdirs==4.2.2
|
||||
# via virtualenv
|
||||
pydantic==2.8.0
|
||||
platformdirs==4.5.0
|
||||
# via
|
||||
# pyodide-build
|
||||
# virtualenv
|
||||
pydantic==2.12.4
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-lock
|
||||
pydantic-core==2.20.0
|
||||
pydantic-core==2.41.5
|
||||
# via pydantic
|
||||
pygments==2.18.0
|
||||
pygments==2.19.2
|
||||
# via rich
|
||||
pyodide-build==0.26.1
|
||||
pyodide-build==0.30.8
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
pyodide-cli==0.2.4
|
||||
pyodide-cli==0.4.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
pyodide-lock==0.1.0a6
|
||||
pyodide-lock==0.1.0
|
||||
# via pyodide-build
|
||||
pyproject-hooks==1.1.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
pyyaml==6.0.1
|
||||
requests==2.32.5
|
||||
# via pyodide-build
|
||||
requests==2.32.3
|
||||
resolvelib==1.2.1
|
||||
# via pyodide-build
|
||||
resolvelib==1.0.1
|
||||
# via pyodide-build
|
||||
rich==13.7.1
|
||||
rich==14.2.0
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
# typer
|
||||
ruamel-yaml==0.18.6
|
||||
ruamel-yaml==0.18.16
|
||||
# via pyodide-build
|
||||
ruamel-yaml-clib==0.2.8
|
||||
ruamel-yaml-clib==0.2.14
|
||||
# via ruamel-yaml
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
sniffio==1.3.1
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
typer==0.12.3
|
||||
# via anyio
|
||||
typer==0.20.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
types-requests==2.32.0.20240622
|
||||
# via pyodide-build
|
||||
typing-extensions==4.12.2
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# anyio
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# typer
|
||||
unearth==0.15.5
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.2
|
||||
# via pydantic
|
||||
unearth==0.18.1
|
||||
# via pyodide-build
|
||||
urllib3==2.2.2
|
||||
# via
|
||||
# requests
|
||||
# types-requests
|
||||
virtualenv==20.26.3
|
||||
urllib3==2.5.0
|
||||
# via requests
|
||||
virtualenv==20.35.4
|
||||
# via
|
||||
# build
|
||||
# pyodide-build
|
||||
wheel==0.43.0
|
||||
wheel==0.45.1
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
anyio==4.11.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==2025.10.5
|
||||
# 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.0
|
||||
# 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.0
|
||||
# via
|
||||
# pyodide-build
|
||||
# virtualenv
|
||||
pydantic==2.12.4
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-lock
|
||||
pydantic-core==2.41.5
|
||||
# via pydantic
|
||||
pygments==2.19.2
|
||||
# via rich
|
||||
pyodide-build==0.30.8
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
pyodide-cli==0.4.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
pyodide-lock==0.1.0
|
||||
# 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.18.16
|
||||
# via pyodide-build
|
||||
ruamel-yaml-clib==0.2.14
|
||||
# via ruamel-yaml
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
typer==0.20.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.1
|
||||
# via pyodide-build
|
||||
urllib3==2.5.0
|
||||
# via requests
|
||||
virtualenv==20.35.4
|
||||
# via
|
||||
# build
|
||||
# pyodide-build
|
||||
wheel==0.45.1
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
@@ -2,33 +2,35 @@
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.1
|
||||
build==1.3.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delocate==0.11.0
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.15.4
|
||||
filelock==3.20.0
|
||||
# via virtualenv
|
||||
importlib-metadata==8.0.0
|
||||
importlib-metadata==8.7.0
|
||||
# via build
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
packaging==24.1
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==24.1.1
|
||||
pip==25.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.2.2
|
||||
platformdirs==4.5.0
|
||||
# via virtualenv
|
||||
pyproject-hooks==1.1.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
tomli==2.0.1
|
||||
tomli==2.3.0
|
||||
# via build
|
||||
typing-extensions==4.12.2
|
||||
# via delocate
|
||||
virtualenv==20.26.3
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# delocate
|
||||
# virtualenv
|
||||
virtualenv==20.35.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
zipp==3.19.2
|
||||
zipp==3.23.0
|
||||
# via importlib-metadata
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.1
|
||||
build==1.3.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delocate==0.11.0
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.15.4
|
||||
filelock==3.20.0
|
||||
# via virtualenv
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
packaging==24.1
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==24.1.1
|
||||
pip==25.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.2.2
|
||||
platformdirs==4.5.0
|
||||
# via virtualenv
|
||||
pyproject-hooks==1.1.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
typing-extensions==4.12.2
|
||||
typing-extensions==4.15.0
|
||||
# via delocate
|
||||
virtualenv==20.26.3
|
||||
virtualenv==20.35.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.1
|
||||
build==1.3.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delocate==0.11.0
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.15.4
|
||||
filelock==3.20.0
|
||||
# via virtualenv
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
packaging==24.1
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==24.1.1
|
||||
pip==25.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.2.2
|
||||
platformdirs==4.5.0
|
||||
# via virtualenv
|
||||
pyproject-hooks==1.1.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
typing-extensions==4.12.2
|
||||
typing-extensions==4.15.0
|
||||
# via delocate
|
||||
virtualenv==20.26.3
|
||||
virtualenv==20.35.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.1
|
||||
build==1.3.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delocate==0.11.0
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.15.4
|
||||
filelock==3.20.0
|
||||
# via virtualenv
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
packaging==24.1
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==24.1.1
|
||||
pip==25.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.2.2
|
||||
platformdirs==4.5.0
|
||||
# via virtualenv
|
||||
pyproject-hooks==1.1.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
typing-extensions==4.12.2
|
||||
typing-extensions==4.15.0
|
||||
# via delocate
|
||||
virtualenv==20.26.3
|
||||
virtualenv==20.35.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
+11
-22
@@ -2,38 +2,27 @@
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.1.1
|
||||
build==1.3.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delocate==0.11.0
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.12.2
|
||||
filelock==3.20.0
|
||||
# via virtualenv
|
||||
importlib-metadata==6.7.0
|
||||
# via
|
||||
# build
|
||||
# virtualenv
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
packaging==24.0
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==24.0
|
||||
pip==25.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.0.0
|
||||
platformdirs==4.5.0
|
||||
# via virtualenv
|
||||
pyproject-hooks==1.1.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
tomli==2.0.1
|
||||
# via build
|
||||
typing-extensions==4.7.1
|
||||
# via
|
||||
# delocate
|
||||
# importlib-metadata
|
||||
# platformdirs
|
||||
virtualenv==20.26.3
|
||||
typing-extensions==4.15.0
|
||||
# via delocate
|
||||
virtualenv==20.35.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
zipp==3.15.0
|
||||
# via importlib-metadata
|
||||
@@ -1,52 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with python 3.6
|
||||
# To update, run:
|
||||
#
|
||||
# nox -s update_constraints-3.6
|
||||
#
|
||||
build==0.9.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delocate==0.10.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
# via virtualenv
|
||||
filelock==3.4.1
|
||||
# via virtualenv
|
||||
importlib-metadata==4.8.3
|
||||
# via
|
||||
# build
|
||||
# pep517
|
||||
# virtualenv
|
||||
importlib-resources==5.4.0
|
||||
# via virtualenv
|
||||
packaging==21.3
|
||||
# via build
|
||||
pep517==0.13.1
|
||||
# via build
|
||||
platformdirs==2.4.0
|
||||
# via virtualenv
|
||||
pyparsing==3.1.1
|
||||
# via packaging
|
||||
tomli==1.2.3
|
||||
# via
|
||||
# build
|
||||
# pep517
|
||||
typing-extensions==4.1.1
|
||||
# via
|
||||
# delocate
|
||||
# importlib-metadata
|
||||
virtualenv==20.17.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.37.1
|
||||
# via delocate
|
||||
zipp==3.6.0
|
||||
# via
|
||||
# importlib-metadata
|
||||
# importlib-resources
|
||||
# pep517
|
||||
|
||||
# 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
|
||||
@@ -2,33 +2,35 @@
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.1
|
||||
build==1.2.2.post1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delocate==0.11.0
|
||||
delocate==0.12.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.15.4
|
||||
filelock==3.16.1
|
||||
# via virtualenv
|
||||
importlib-metadata==8.0.0
|
||||
importlib-metadata==8.5.0
|
||||
# via build
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
packaging==24.1
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==24.1.1
|
||||
pip==25.0.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.2.2
|
||||
platformdirs==4.3.6
|
||||
# via virtualenv
|
||||
pyproject-hooks==1.1.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
tomli==2.0.1
|
||||
tomli==2.3.0
|
||||
# via build
|
||||
typing-extensions==4.12.2
|
||||
# via delocate
|
||||
virtualenv==20.26.3
|
||||
typing-extensions==4.13.2
|
||||
# via
|
||||
# delocate
|
||||
# virtualenv
|
||||
virtualenv==20.35.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
zipp==3.19.2
|
||||
zipp==3.20.2
|
||||
# via importlib-metadata
|
||||
|
||||
@@ -2,33 +2,35 @@
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.1
|
||||
build==1.3.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delocate==0.11.0
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.15.4
|
||||
filelock==3.19.1
|
||||
# via virtualenv
|
||||
importlib-metadata==8.0.0
|
||||
importlib-metadata==8.7.0
|
||||
# via build
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
packaging==24.1
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==24.1.1
|
||||
pip==25.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.2.2
|
||||
platformdirs==4.4.0
|
||||
# via virtualenv
|
||||
pyproject-hooks==1.1.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
tomli==2.0.1
|
||||
tomli==2.3.0
|
||||
# via build
|
||||
typing-extensions==4.12.2
|
||||
# via delocate
|
||||
virtualenv==20.26.3
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# delocate
|
||||
# virtualenv
|
||||
virtualenv==20.35.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
zipp==3.19.2
|
||||
zipp==3.23.0
|
||||
# via importlib-metadata
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.1
|
||||
build==1.3.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delocate==0.11.0
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.15.4
|
||||
filelock==3.20.0
|
||||
# via virtualenv
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
packaging==24.1
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==24.1.1
|
||||
pip==25.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.2.2
|
||||
platformdirs==4.5.0
|
||||
# via virtualenv
|
||||
pyproject-hooks==1.1.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
typing-extensions==4.12.2
|
||||
typing-extensions==4.15.0
|
||||
# via delocate
|
||||
virtualenv==20.26.3
|
||||
virtualenv==20.35.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
build = "*"
|
||||
skip = ""
|
||||
test-skip = ""
|
||||
free-threaded-support = false
|
||||
enable = []
|
||||
|
||||
archs = ["auto"]
|
||||
build-frontend = "default"
|
||||
@@ -14,29 +14,41 @@ 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"
|
||||
|
||||
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"
|
||||
pyodide-version = ""
|
||||
|
||||
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"
|
||||
|
||||
|
||||
[tool.cibuildwheel.linux]
|
||||
@@ -47,4 +59,8 @@ repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest
|
||||
|
||||
[tool.cibuildwheel.windows]
|
||||
|
||||
[tool.cibuildwheel.android]
|
||||
|
||||
[tool.cibuildwheel.ios]
|
||||
|
||||
[tool.cibuildwheel.pyodide]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?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>
|
||||
@@ -26,14 +26,14 @@ STAT_0o775 = (
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
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
|
||||
import certifi # noqa: PLC0415
|
||||
|
||||
# change working directory to the default SSL directory
|
||||
os.chdir(openssl_dir)
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
url = "https://nodejs.org/dist/"
|
||||
v20 = "v20.15.0"
|
||||
v24 = "v24.11.0"
|
||||
v22 = "v22.21.1"
|
||||
v20 = "v20.19.5"
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
[x86_64]
|
||||
manylinux1 = quay.io/pypa/manylinux1_x86_64:2024-04-29-76807b8
|
||||
manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-08-05-4535177
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2024.07.02-0
|
||||
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.07.02-0
|
||||
musllinux_1_1 = quay.io/pypa/musllinux_1_1_x86_64:2024.07.02-0
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2024.07.02-0
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2025.11.09-2
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2025.11.09-2
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2025.11.09-2
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2025.11.09-2
|
||||
|
||||
[i686]
|
||||
manylinux1 = quay.io/pypa/manylinux1_i686:2024-04-29-76807b8
|
||||
manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-08-05-4535177
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_i686:2024.07.02-0
|
||||
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.07.02-0
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686:2024.07.02-0
|
||||
|
||||
[pypy_x86_64]
|
||||
manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-08-05-4535177
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2024.07.02-0
|
||||
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.07.02-0
|
||||
|
||||
[pypy_i686]
|
||||
manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-08-05-4535177
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_i686:2024.07.02-0
|
||||
manylinux_2_24 = quay.io/pypa/manylinux_2_24_i686:2022-12-26-0d38463
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_i686:2025.11.09-2
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2025.11.09-2
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2025.11.09-2
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686:2025.11.09-2
|
||||
|
||||
[aarch64]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2024.07.02-0
|
||||
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.07.02-0
|
||||
musllinux_1_1 = quay.io/pypa/musllinux_1_1_aarch64:2024.07.02-0
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64:2024.07.02-0
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2025.11.09-2
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2025.11.09-2
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2025.11.09-2
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64:2025.11.09-2
|
||||
|
||||
[ppc64le]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2024.07.02-0
|
||||
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.07.02-0
|
||||
musllinux_1_1 = quay.io/pypa/musllinux_1_1_ppc64le:2024.07.02-0
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_ppc64le:2024.07.02-0
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2025.11.09-2
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_ppc64le:2025.11.09-2
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_ppc64le:2025.11.09-2
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_ppc64le:2025.11.09-2
|
||||
|
||||
[s390x]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_s390x:2024.07.02-0
|
||||
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.07.02-0
|
||||
musllinux_1_1 = quay.io/pypa/musllinux_1_1_s390x:2024.07.02-0
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_s390x:2024.07.02-0
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_s390x:2025.11.09-2
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_s390x:2025.11.09-2
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_s390x:2025.11.09-2
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_s390x:2025.11.09-2
|
||||
|
||||
[pypy_x86_64]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2025.11.09-2
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2025.11.09-2
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2025.11.09-2
|
||||
|
||||
[pypy_i686]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_i686:2025.11.09-2
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2025.11.09-2
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2025.11.09-2
|
||||
|
||||
[pypy_aarch64]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2024.07.02-0
|
||||
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.07.02-0
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2025.11.09-2
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2025.11.09-2
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2025.11.09-2
|
||||
|
||||
[armv7l]
|
||||
manylinux_2_31 = quay.io/pypa/manylinux_2_31_armv7l:2025.11.09-2
|
||||
manylinux_2_35 = quay.io/pypa/manylinux_2_35_armv7l:2025.11.09-2
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_armv7l:2025.11.09-2
|
||||
|
||||
[riscv64]
|
||||
manylinux_2_39 = quay.io/pypa/manylinux_2_39_riscv64:2025.11.09-2
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_riscv64:2025.11.09-2
|
||||
|
||||
|
||||
@@ -0,0 +1,561 @@
|
||||
{
|
||||
"releases": [
|
||||
{
|
||||
"tag": "20251031",
|
||||
"assets": [
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-aarch64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-aarch64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.19+20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.10.19%2B20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-aarch64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-aarch64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-aarch64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-aarch64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.14+20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.11.14%2B20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-aarch64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-aarch64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-aarch64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-aarch64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.12+20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.12.12%2B20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-aarch64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-aarch64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-aarch64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-aarch64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.9+20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.13.9%2B20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-aarch64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-aarch64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-aarch64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-aarch64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0+20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.14.0%2B20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-aarch64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-aarch64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-aarch64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-aarch64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.15.0a1+20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.15.0a1%2B20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-aarch64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-aarch64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.25+20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25%2B20251031-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,17 +1,36 @@
|
||||
# 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):
|
||||
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."
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
py36 = { version = "20.21.1", url = "https://github.com/pypa/get-virtualenv/blob/20.21.1/public/virtualenv.pyz?raw=true" }
|
||||
default = { version = "20.26.3", url = "https://github.com/pypa/get-virtualenv/blob/20.26.3/public/virtualenv.pyz?raw=true" }
|
||||
default = { version = "20.35.4", url = "https://github.com/pypa/get-virtualenv/releases/download/20.35.4/virtualenv.pyz" }
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
DIR = Path(__file__).parent.resolve()
|
||||
from .util import resources
|
||||
|
||||
|
||||
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 DIR.joinpath("resources/cibuildwheel.schema.json").open(encoding="utf-8") as f:
|
||||
with resources.CIBUILDWHEEL_SCHEMA.open(encoding="utf-8") as f:
|
||||
return json.load(f) # type: ignore[no-any-return]
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
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}
|
||||
+4
-14
@@ -1,29 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import typing
|
||||
from typing import Final, Literal, Protocol, Union
|
||||
from typing import Final, Literal, Protocol
|
||||
|
||||
__all__ = (
|
||||
"PLATFORMS",
|
||||
"PathOrStr",
|
||||
"PlatformName",
|
||||
"PLATFORMS",
|
||||
"PopenBytes",
|
||||
)
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
PopenBytes = subprocess.Popen[bytes]
|
||||
PathOrStr = Union[str, os.PathLike[str]]
|
||||
else:
|
||||
PopenBytes = subprocess.Popen
|
||||
PathOrStr = Union[str, "os.PathLike[str]"]
|
||||
PathOrStr = str | os.PathLike[str]
|
||||
|
||||
|
||||
PlatformName = Literal["linux", "macos", "windows", "pyodide"]
|
||||
PLATFORMS: Final[set[PlatformName]] = {"linux", "macos", "windows", "pyodide"}
|
||||
PlatformName = Literal["linux", "macos", "windows", "pyodide", "android", "ios"]
|
||||
PLATFORMS: Final[frozenset[PlatformName]] = frozenset(typing.get_args(PlatformName))
|
||||
|
||||
|
||||
class GenericPythonConfiguration(Protocol):
|
||||
|
||||
@@ -1,932 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import fnmatch
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import textwrap
|
||||
import time
|
||||
import typing
|
||||
import urllib.request
|
||||
from collections import defaultdict
|
||||
from collections.abc import Generator, Iterable, Mapping, MutableMapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path, PurePath
|
||||
from tempfile import TemporaryDirectory
|
||||
from time import sleep
|
||||
from typing import Any, ClassVar, Final, Literal, TextIO, TypeVar
|
||||
from zipfile import ZipFile
|
||||
|
||||
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 .architecture import Architecture
|
||||
from .typing import PathOrStr, PlatformName
|
||||
|
||||
__all__ = [
|
||||
"MANYLINUX_ARCHS",
|
||||
"call",
|
||||
"chdir",
|
||||
"combine_constraints",
|
||||
"find_compatible_wheel",
|
||||
"find_uv",
|
||||
"format_safe",
|
||||
"get_build_verbosity_extra_flags",
|
||||
"prepare_command",
|
||||
"read_python_configs",
|
||||
"resources_dir",
|
||||
"selector_matches",
|
||||
"shell",
|
||||
"split_config_settings",
|
||||
"strtobool",
|
||||
]
|
||||
|
||||
resources_dir: Final[Path] = Path(__file__).parent / "resources"
|
||||
|
||||
install_certifi_script: Final[Path] = resources_dir / "install_certifi.py"
|
||||
|
||||
free_thread_enable_313: Final[Path] = resources_dir / "free-threaded-enable-313.xml"
|
||||
|
||||
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", "build[uv]"]
|
||||
) -> 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] = "cp313-* cp313t-*"
|
||||
prerelease_pythons: bool = False
|
||||
|
||||
free_threaded_support: 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]
|
||||
if py_ver_str.endswith("t"):
|
||||
py_ver_str = py_ver_str[:-1]
|
||||
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
|
||||
|
||||
# filter out free threaded pythons if self.free_threaded_support is False
|
||||
if not self.free_threaded_support and selector_matches("*t-*", 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,
|
||||
"free_threaded_support": self.free_threaded_support,
|
||||
}
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
def extract_zip(zip_src: Path, dest: Path) -> None:
|
||||
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
|
||||
# But some files in the zipfile seem to have external_attr with 0
|
||||
# permissions. In that case just use the default value???
|
||||
permissions = (zinfo.external_attr >> 16) & 0o777
|
||||
if permissions != 0:
|
||||
dest.joinpath(zinfo.filename).chmod(permissions)
|
||||
|
||||
|
||||
def extract_tar(tar_src: Path, dest: Path) -> None:
|
||||
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)
|
||||
|
||||
|
||||
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, *, variant: Literal["python", "pyodide"] = "python"
|
||||
) -> 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"-{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
|
||||
|
||||
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", "build[uv]"]
|
||||
|
||||
|
||||
@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", "build[uv]"}:
|
||||
msg = f"Unrecognised build frontend {name!r}, only 'pip', 'build', and 'build[uv]' 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_node(major_version: str) -> Path:
|
||||
input_file = resources_dir / "nodejs.toml"
|
||||
with input_file.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
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _ensure_virtualenv(version: str) -> Path:
|
||||
version_parts = version.split(".")
|
||||
key = f"py{version_parts[0]}{version_parts[1]}"
|
||||
input_file = resources_dir / "virtualenv.toml"
|
||||
with input_file.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
|
||||
|
||||
|
||||
def _parse_constraints_for_virtualenv(
|
||||
seed_packages: list[str],
|
||||
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}
|
||||
# only seed pip if other seed packages do not appear in a constraint file
|
||||
constraints_dict = {"pip": "embed"}
|
||||
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 seed_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(
|
||||
version: str,
|
||||
python: Path,
|
||||
venv_path: Path,
|
||||
dependency_constraint_flags: Sequence[PathOrStr],
|
||||
*,
|
||||
use_uv: bool,
|
||||
) -> 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, and setuptools + wheel if Python < 3.12.
|
||||
"""
|
||||
assert python.exists()
|
||||
|
||||
if use_uv:
|
||||
call("uv", "venv", venv_path, "--python", python)
|
||||
else:
|
||||
virtualenv_app = _ensure_virtualenv(version)
|
||||
allowed_seed_packages = ["pip", "setuptools", "wheel"]
|
||||
constraints = _parse_constraints_for_virtualenv(
|
||||
allowed_seed_packages, dependency_constraint_flags
|
||||
)
|
||||
additional_flags: list[str] = []
|
||||
for package in allowed_seed_packages:
|
||||
if package in constraints:
|
||||
additional_flags.append(f"--{package}={constraints[package]}")
|
||||
else:
|
||||
additional_flags.append(f"--no-{package}")
|
||||
|
||||
# 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"]])
|
||||
env["VIRTUAL_ENV"] = str(venv_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("-")
|
||||
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")):
|
||||
# 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)
|
||||
|
||||
|
||||
def find_uv() -> Path | None:
|
||||
# Prefer uv in our environment
|
||||
with contextlib.suppress(ImportError, FileNotFoundError):
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from uv import find_uv_bin
|
||||
|
||||
return Path(find_uv_bin())
|
||||
|
||||
uv_on_path = shutil.which("uv")
|
||||
return Path(uv_on_path) if uv_on_path else None
|
||||
|
||||
|
||||
def combine_constraints(
|
||||
env: MutableMapping[str, str], /, constraints_path: Path, tmp_dir: Path | None
|
||||
) -> None:
|
||||
"""
|
||||
This will workaround a bug in pip<=21.1.1 or uv<=0.2.0 if a tmp_dir is given.
|
||||
If set to None, this will use the modern URI method.
|
||||
"""
|
||||
|
||||
if tmp_dir:
|
||||
if " " in str(constraints_path):
|
||||
assert " " not in str(tmp_dir)
|
||||
tmp_file = tmp_dir / "constraints.txt"
|
||||
tmp_file.write_bytes(constraints_path.read_bytes())
|
||||
constraints_path = tmp_file
|
||||
our_constraints = str(constraints_path)
|
||||
else:
|
||||
our_constraints = (
|
||||
constraints_path.as_uri() if " " in str(constraints_path) else str(constraints_path)
|
||||
)
|
||||
|
||||
user_constraints = env.get("PIP_CONSTRAINT")
|
||||
|
||||
env["UV_CONSTRAINT"] = env["PIP_CONSTRAINT"] = " ".join(
|
||||
c for c in [our_constraints, user_constraints] if c
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
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
|
||||
@@ -0,0 +1,154 @@
|
||||
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)
|
||||
@@ -0,0 +1,174 @@
|
||||
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
|
||||
@@ -0,0 +1,180 @@
|
||||
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
|
||||
@@ -0,0 +1,181 @@
|
||||
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)
|
||||
@@ -0,0 +1,32 @@
|
||||
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]
|
||||
@@ -0,0 +1,167 @@
|
||||
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
|
||||
+256
-3
@@ -4,20 +4,269 @@ title: Changelog
|
||||
|
||||
# Changelog
|
||||
|
||||
### v3.3.0
|
||||
|
||||
_12 November 2025_
|
||||
|
||||
- 🐛 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)
|
||||
|
||||
### v3.2.1
|
||||
|
||||
_12 October 2025_
|
||||
|
||||
- 🛠 Update to CPython 3.14.0 final (#2614)
|
||||
- 🐛 Fix the default MACOSX_DEPLOYMENT_TARGET on Python 3.14 (#2613)
|
||||
- 📚 Docs improvements (#2617)
|
||||
|
||||
### v3.2.0
|
||||
|
||||
_22 September 2025_
|
||||
|
||||
- ✨ 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)
|
||||
|
||||
### v3.1.4
|
||||
|
||||
_19 August 2025_
|
||||
|
||||
- ✨ 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)
|
||||
|
||||
|
||||
### v3.1.3
|
||||
|
||||
_1 August 2025_
|
||||
|
||||
- 🐛 Fix bug where "latest" dependencies couldn't update to pip 25.2 on Windows (#2537)
|
||||
- 🧪 Use pytest-rerunfailures to improve some of our iOS/Android tests (#2527, #2539)
|
||||
- 🧪 Remove some GraalPy Windows workarounds in our tests (#2501)
|
||||
|
||||
|
||||
### v3.1.2
|
||||
|
||||
_29 July 2025_
|
||||
|
||||
- ⚠️ Add an error if `CIBW_FREE_THREADING_SUPPORT` is set; you are likely missing 3.13t wheels, please use the `enable`/`CIBW_ENABLE` (#2520)
|
||||
- 🛠 `riscv64` now enabled if you target that architecture, it's now supported on PyPI (#2509)
|
||||
- 🛠 Add warning when using `cpython-experimental-riscv64` (no longer needed) (#2526, #2528)
|
||||
- 🛠 iOS versions bumped, fixing issues with 3.14 (now RC 1) (#2530)
|
||||
- 🐛 Fix bug in Android running wheel from our GitHub Action (#2517)
|
||||
- 🐛 Fix warning when using `test-skip` of `"*-macosx_universal2:arm64"` (#2522)
|
||||
- 🐛 Fix incorrect number of wheels reported in logs, again (#2517)
|
||||
- 📚 We welcome our Android platform maintainer (#2516)
|
||||
|
||||
|
||||
### v3.1.1
|
||||
|
||||
_24 July 2025_
|
||||
|
||||
- 🐛 Fix a bug showing an incorrect wheel count at the end of execution, and misrepresenting test-only runs in the GitHub Action summary (#2512)
|
||||
- 📚 Docs fix (#2510)
|
||||
|
||||
### v3.1.0
|
||||
|
||||
_23 July 2025_
|
||||
|
||||
|
||||
- 🌟 CPython 3.14 wheels are now built by default - without the `"cpython-prerelease"` `enable` set. It's time to build and upload these wheels to PyPI! This release includes CPython 3.14.0rc1, which is guaranteed to be ABI compatible with the final release. (#2507) Free-threading is no longer experimental in 3.14, so you have to skip it explicitly with `'cp31?t-*'` if you don't support it yet. (#2503)
|
||||
- 🌟 Adds the ability to [build wheels for Android](https://cibuildwheel.pypa.io/en/stable/platforms/#android)! Set the [`platform` option](https://cibuildwheel.pypa.io/en/stable/options/#platform) to `android` on Linux or macOS to try it out! (#2349)
|
||||
- 🌟 Adds Pyodide 0.28, which builds 3.13 wheels (#2487)
|
||||
- ✨ Support for 32-bit `manylinux_2_28` (now a consistent default) and `manylinux_2_34` added (#2500)
|
||||
- 🛠 Improved summary, will also use markdown summary output on GHA (#2469)
|
||||
- 🛠 The riscv64 images now have a working default (as they are now part of pypy/manylinux), but are still experimental (and behind an `enable`) since you can't push them to PyPI yet (#2506)
|
||||
- 🛠 Fixed a typo in the 3.9 MUSL riscv64 identifier (`cp39-musllinux_ricv64` -> `cp39-musllinux_riscv64`) (#2490)
|
||||
- 🛠 Mistyping `--only` now shows the correct possibilities, and even suggests near matches on Python 3.14+ (#2499)
|
||||
- 🛠 Only support one output from the repair step on linux like other platforms; auditwheel fixed this over four years ago! (#2478)
|
||||
- 💼 We now use pattern matching extensively (#2434)
|
||||
- 📚 We now have platform maintainers for our special platforms and interpreters! (#2481)
|
||||
|
||||
|
||||
|
||||
### v3.0.1
|
||||
|
||||
_5 July 2025_
|
||||
|
||||
- 🛠 Updates CPython 3.14 prerelease to 3.14.0b3 (#2471)
|
||||
- ✨ Adds a CPython 3.14 prerelease iOS build (only when prerelease builds are [enabled](https://cibuildwheel.pypa.io/en/stable/options/#enable)) (#2475)
|
||||
|
||||
### v3.0.0
|
||||
|
||||
_11 June 2025_
|
||||
|
||||
See @henryiii's [release post](https://iscinumpy.dev/post/cibuildwheel-3-0-0/) for more info on new features!
|
||||
|
||||
- 🌟 Adds the ability to [build wheels for iOS](https://cibuildwheel.pypa.io/en/stable/platforms/#ios)! Set the [`platform` option](https://cibuildwheel.pypa.io/en/stable/options/#platform) to `ios` on a Mac with the iOS toolchain to try it out! (#2286, #2363, #2432)
|
||||
- 🌟 Adds support for the GraalPy interpreter! Enable for your project using the [`enable` option](https://cibuildwheel.pypa.io/en/stable/options/#enable). (#1538, #2411, #2414)
|
||||
- ✨ Adds CPython 3.14 support, under the [`enable` option](https://cibuildwheel.pypa.io/en/stable/options/#enable) `cpython-prerelease`. This version of cibuildwheel uses 3.14.0b2. (#2390)
|
||||
|
||||
_While CPython is in beta, the ABI can change, so your wheels might not be compatible with the final release. For this reason, we don't recommend distributing wheels until RC1, at which point 3.14 will be available in cibuildwheel without the flag._ (#2390)
|
||||
|
||||
- ✨ Adds the [test-sources option](https://cibuildwheel.pypa.io/en/stable/options/#test-sources), which copies files and folders into the temporary working directory we run tests from. (#2062, #2284, #2420, #2437)
|
||||
|
||||
This is particularly important for iOS builds, which do not support placeholders in the `test-command`, but can also be useful for other platforms.
|
||||
|
||||
- ✨ Adds [`dependency-versions`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) inline syntax (#2122)
|
||||
- ✨ Improves support for Pyodide builds and adds the experimental [`pyodide-version`](https://cibuildwheel.pypa.io/en/stable/options/#pyodide-version) option, which allows you to specify the version of Pyodide to use for builds. (#2002)
|
||||
- ✨ Add `pyodide-prerelease` [enable](https://cibuildwheel.pypa.io/en/stable/options/#enable) option, with an early build of 0.28 (Python 3.13). (#2431)
|
||||
- ✨ Adds the [`test-environment`](https://cibuildwheel.pypa.io/en/stable/options/#test-environment) option, which allows you to set environment variables for the test command. (#2388)
|
||||
- ✨ Adds the [`xbuild-tools`](https://cibuildwheel.pypa.io/en/stable/options/#xbuild-tools) option, which allows you to specify tools safe for cross-compilation. Currently only used on iOS; will be useful for Android in the future. (#2317)
|
||||
- 🛠 The default [manylinux image](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) has changed from `manylinux2014` to `manylinux_2_28`. (#2330)
|
||||
- 🛠 EOL images `manylinux1`, `manylinux2010`, `manylinux_2_24` and `musllinux_1_1` can no longer be specified by their shortname. The full OCI name can still be used for these images, if you wish. (#2316)
|
||||
- 🛠 Invokes `build` rather than `pip wheel` to build wheels by default. You can control this via the [`build-frontend`](https://cibuildwheel.pypa.io/en/stable/options/#build-frontend) option. You might notice that you can see your build log output now! (#2321)
|
||||
- 🛠 Build verbosity settings have been reworked to have consistent meanings between build backends when non-zero. (#2339)
|
||||
- 🛠 Removed the `CIBW_PRERELEASE_PYTHONS` and `CIBW_FREE_THREADED_SUPPORT` options - these have been folded into the [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable) option instead. (#2095)
|
||||
- 🛠 Build environments no longer have setuptools and wheel preinstalled. (#2329)
|
||||
- 🛠 Use the standard Schema line for the integrated JSONSchema. (#2433)
|
||||
- ⚠️ Dropped support for building Python 3.6 and 3.7 wheels. If you need to build wheels for these versions, use cibuildwheel v2.23.3 or earlier. (#2282)
|
||||
- ⚠️ The minimum Python version required to run cibuildwheel is now Python 3.11. You can still build wheels for Python 3.8 and newer. (#1912)
|
||||
- ⚠️ 32-bit Linux wheels no longer built by default - the [arch](https://cibuildwheel.pypa.io/en/stable/options/#archs) was removed from `"auto"`. It now requires explicit `"auto32"`. Note that modern manylinux images (like the new default, `manylinux_2_28`) do not have 32-bit versions. (#2458)
|
||||
- ⚠️ PyPy wheels no longer built by default, due to a change to our options system. To continue building PyPy wheels, you'll now need to set the [`enable` option](https://cibuildwheel.pypa.io/en/stable/options/#enable) to `pypy` or `pypy-eol`. (#2095)
|
||||
- ⚠️ Dropped official support for Appveyor. If it was working for you before, it will probably continue to do so, but we can't be sure, because our CI doesn't run there anymore. (#2386)
|
||||
- 📚 A reorganisation of the docs, and numerous updates. (#2280)
|
||||
- 📚 Use Python 3.14 color output in docs CLI output. (#2407)
|
||||
- 📚 Docs now primarily use the pyproject.toml name of options, rather than the environment variable name. (#2389)
|
||||
- 📚 README table now matches docs and auto-updates. (#2427, #2428)
|
||||
|
||||
### v2.23.3
|
||||
|
||||
_26 April 2025_
|
||||
|
||||
- 🛠 Dependency updates, including Python 3.13.3 (#2371)
|
||||
|
||||
### v2.23.2
|
||||
|
||||
_24 March 2025_
|
||||
|
||||
- 🐛 Workaround an issue with pyodide builds when running cibuildwheel with a Python that was installed via UV (#2328 via #2331)
|
||||
- 🛠 Dependency updates, including a manylinux update that fixes an ['undefined symbol' error](https://github.com/pypa/manylinux/issues/1760) in gcc-toolset (#2334)
|
||||
|
||||
### v2.23.1
|
||||
|
||||
_15 March 2025_
|
||||
|
||||
- ⚠️ Added warnings when the shorthand values `manylinux1`, `manylinux2010`, `manylinux_2_24`, and `musllinux_1_1` are used to specify the images in linux builds. The shorthand to these (unmaintainted) images will be removed in v3.0. If you want to keep using these images, explicitly opt-in using the full image URL, which can be found in [this file](https://github.com/pypa/cibuildwheel/blob/v2.23.1/cibuildwheel/resources/pinned_docker_images.cfg). (#2312)
|
||||
- 🛠 Dependency updates, including a manylinux update which fixes an [issue with rustup](https://github.com/pypa/cibuildwheel/issues/2303). (#2315)
|
||||
|
||||
### v2.23.0
|
||||
|
||||
_1 March 2025_
|
||||
|
||||
- ✨ Adds official support for the new GitHub Actions Arm runners. In fact these worked out-of-the-box, now we include them in our tests and example configs. (#2135 via #2281)
|
||||
- ✨ Adds support for building PyPy 3.11 wheels (#2268 via #2281)
|
||||
- 🛠 Adopts the beta pypa/manylinux image for armv7l builds (#2269 via #2281)
|
||||
- 🛠 Dependency updates, including Pyodide 0.27 (#2117 and #2281)
|
||||
|
||||
### v2.22.0
|
||||
|
||||
_23 November 2024_
|
||||
|
||||
- 🌟 Added a new `CIBW_ENABLE`/`enable` feature that replaces `CIBW_FREETHREADED_SUPPORT`/`free-threaded-support` and `CIBW_PRERELEASE_PYTHONS` with a system that supports both. In cibuildwheel 3, this will also include a PyPy setting and the deprecated options will be removed. (#2048)
|
||||
- 🌟 [Dependency groups](https://peps.python.org/pep-0735/) are now supported for tests. Use `CIBW_TEST_GROUPS`/`test-groups` to specify groups in `[dependency-groups]` for testing. (#2063)
|
||||
- 🌟 Support for the experimental Ubuntu-based ARMv7l manylinux image (#2052)
|
||||
- ✨ Show a warning when cibuildwheel is run from Python 3.10 or older; cibuildwheel 3.0 will require Python 3.11 or newer as host (#2050)
|
||||
- 🐛 Fix issue with stderr interfering with checking the docker version (#2074)
|
||||
- 🛠 Python 3.9 is now used in `CIBW_BEFORE_ALL`/`before-all` on linux, replacing 3.8, which is now EoL (#2043)
|
||||
- 🛠 Error messages for producing a pure-Python wheel are slightly more informative (#2044)
|
||||
- 🛠 Better error when `uname -m` fails on ARM (#2049)
|
||||
- 🛠 Better error when repair fails and docs for abi3audit on Windows (#2058)
|
||||
- 🛠 Better error when `manylinux-interpreters ensure` fails (#2066)
|
||||
- 🛠 Update Pyodide to 0.26.4, and adapt to the unbundled pyodide-build (now 0.29) (#2090)
|
||||
- 🛠 Now cibuildwheel uses dependency-groups for development dependencies (#2064, #2085)
|
||||
- 📚 Docs updates and tidy ups (#2061, #2067, #2072)
|
||||
|
||||
|
||||
### v2.21.3
|
||||
|
||||
_9 October 2024_
|
||||
|
||||
- 🛠 Update CPython 3.13 to 3.13.0 final release (#2032)
|
||||
- 📚 Docs updates and tidy ups (#2035)
|
||||
|
||||
### v2.21.2
|
||||
|
||||
_2 October 2024_
|
||||
|
||||
- ✨ Adds support for building 32-bit armv7l wheels on musllinux. On a Linux system with emulation set up, set [CIBW_ARCHS](https://cibuildwheel.pypa.io/en/stable/options/#archs) to `armv7l` on Linux to try it out if you're interested! (#2017)
|
||||
- 🐛 Fix Linux Podman builds on some systems (#2016)
|
||||
- ✨ Adds official support for running on Python 3.13 (#2026)
|
||||
- 🛠 Update CPython 3.13 to 3.13.0rc3 (#2029)
|
||||
|
||||
Note: the default [manylinux image](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) is **scheduled to change** from `manylinux2014` to `manylinux_2_28` in a cibuildwheel release on or after **6th May 2025** - you can set the value now to avoid getting upgraded if you want. (#1992)
|
||||
|
||||
### v2.21.1
|
||||
|
||||
_16 September 2024_
|
||||
|
||||
- 🐛 Fix a bug in the Linux build, where files copied to the container would have invalid ownership permissions (#2007)
|
||||
- 🐛 Fix a bug on Windows where cibuildwheel would call upon `uv` to install dependencies for versions of CPython that it does not support (#2005)
|
||||
- 🐛 Fix a bug where `uv 0.4.10` would not use the right Python when testing on Linux. (#2008)
|
||||
- 🛠 Bump our documentation pins, fixes an issue with a missing package (#2011)
|
||||
|
||||
### v2.21.0
|
||||
|
||||
_13 September 2024_
|
||||
|
||||
- ⚠️ Update CPython 3.12 to 3.12.6, which changes the macOS minimum deployment target on CPython 3.12 from macOS 10.9 to macOS 10.13 (#1998)
|
||||
- 🛠 Changes the behaviour when inheriting `config-settings` in TOML overrides - rather than extending each key, which is rarely useful, individual keys will override previously set values. (#1803)
|
||||
- 🛠 Update CPython 3.13 to 3.13.0rc2 (#1998)
|
||||
- ✨ Adds support for multiarch OCI images (#1961)
|
||||
- 🐛 Fixes some bugs building Linux wheels on macOS. (#1961)
|
||||
- ⚠️ Changes the minimum version of Docker/Podman to Docker API version 1.43, Podman API version 3. The only mainstream runner this should affect is Travis Graviton2 runners - if so you can [upgrade your version of Docker](https://github.com/pypa/cibuildwheel/pull/1961#issuecomment-2304060019). (#1961)
|
||||
|
||||
|
||||
### v2.20.0
|
||||
|
||||
_4 August 2024_
|
||||
|
||||
- 🌟 CPython 3.13 wheels are now built by default - without the `CIBW_PRERELEASE_PYTHONS` flag. It's time to build and upload these wheels to PyPI! This release includes CPython 3.13.0rc1, which is guaranteed to be ABI compatible with the final release. Free-threading is still behind a flag/config option. (#1950)
|
||||
- ✨ Provide a `CIBW_ALLOW_EMPTY` environment variable as an alternative to the command line flag. (#1937)
|
||||
- 🐛 Don't use uv on PyPy3.8 on Windows, it stopped working starting in 0.2.25. Note that PyPy 3.8 is EoL. (#1868)
|
||||
- 🛠 Set the `VSCMD_ARG_TGT_ARCH` variable based on target arch. (#1876)
|
||||
- 🛠 Undo cleaner output on pytest 8-8.2 now that 8.3 is out. (#1943)
|
||||
- 📚 Update examples to use Python 3.12 on host (cibuildwheel will require Python 3.11+ on the host machine starting in October 2024) (#1919)
|
||||
|
||||
|
||||
### v2.19.2
|
||||
|
||||
_2 July 2024_
|
||||
|
||||
- 🐛 Update manylinux2014 pins to versions that support past-EoL CentOS 7 mirrors. (#1917)
|
||||
- 🐛 Support `--no-isolation` with `build[uv]` build-frontend. (#1889)
|
||||
- 🛠 Provide attestations for releases at <https://github.com/pypa/cibuildwheel/attestations>. (#1916)
|
||||
- 🛠 Provide CPython 3.13.0b3. (#1913)
|
||||
- 🛠 Remove some workarounds now that pip 21.1 is available. (#1891, #1892)
|
||||
- 📚 Remove nosetest from our docs. (#1821)
|
||||
- 📚 Document the macOS ARM workaround for 3.8 on GHA. (#1971)
|
||||
- 📚 Document the macOS ARM workaround for 3.8 on GHA. (#1871)
|
||||
- 📚 GitLab CI + macOS is now a supported platform with an example. (#1911)
|
||||
|
||||
|
||||
### v2.19.1
|
||||
|
||||
_13 June 2024_
|
||||
|
||||
- 🐛 Don't require setup-python on GHA for Pyodide (#1868)
|
||||
- 🐛 Specify full python path for uv (fixes issue in 0.2.10 & 0.2.11) (#1881)
|
||||
- 🛠 Update for pip 24.1b2 on CPython 3.13. (#1879)
|
||||
@@ -27,6 +276,8 @@ title: Changelog
|
||||
|
||||
### v2.19.0
|
||||
|
||||
_10 June 2024_
|
||||
|
||||
See the [release post](https://iscinumpy.dev/post/cibuildwheel-2-19-0/) for more info on new features!
|
||||
|
||||
- 🌟 Add Pyodide platform. Set with `--platform pyodide` or `CIBW_PLATFORM: pyodide` on Linux with a host Python 3.12 to build WebAssembly wheels. Not accepted on PyPI currently, but usable directly in a website using Pyodide, for live docs, etc. (#1456, #1859)
|
||||
@@ -45,6 +296,8 @@ See the [release post](https://iscinumpy.dev/post/cibuildwheel-2-19-0/) for more
|
||||
|
||||
### v2.18.1
|
||||
|
||||
_20 May 2024_
|
||||
|
||||
- 🌟 Add free-threaded Linux and Windows builds for 3.13. New identifiers `cp313t-*`, new option `CIBW_FREE_THREADED_SUPPORT`/`tool.cibuildwheel.free-threaded-support` required to opt-in. [See the docs](https://cibuildwheel.pypa.io/en/stable/options/#free-threaded-support) for more information. (#1831)
|
||||
- ✨ The `container-engine` is now a build (non-global) option. (#1792)
|
||||
- 🛠 The build backend for cibuildwheel is now hatchling. (#1297)
|
||||
@@ -371,7 +624,7 @@ _14 December 2021_
|
||||
_26 November 2021_
|
||||
|
||||
- 📈 cibuildwheel now defaults to manylinux2014 image for linux builds, rather than manylinux2010. If you want to stick with manylinux2010, it's simple to set this using [the image options](https://cibuildwheel.pypa.io/en/stable/options/#linux-image). (#926)
|
||||
- ✨ You can now pass environment variables from the host machine into the Docker container during a Linux build. Check out [the docs for `CIBW_ENVIRONMENT_PASS_LINUX `](https://cibuildwheel.pypa.io/en/latest/options/#environment-pass) for the details. (#914)
|
||||
- ✨ You can now pass environment variables from the host machine into the Docker container during a Linux build. Check out [the docs for `CIBW_ENVIRONMENT_PASS_LINUX `](https://cibuildwheel.pypa.io/en/stable/options/#environment-pass) for the details. (#914)
|
||||
- ✨ Added support for building PyPy 3.8 wheels. (#881)
|
||||
- ✨ Added support for building Windows arm64 CPython wheels on a Windows arm64 runner. We can't test this in CI yet, so for now, this is experimental. (#920)
|
||||
- 📚 Improved the deployment documentation (#911)
|
||||
@@ -402,7 +655,7 @@ _22 October 2021_
|
||||
|
||||
- 🌟 TOML option overrides! This provides much greater flexibility in configuration via pyproject.toml. (#854)
|
||||
|
||||
You can now set build options for any subset of your builds using a match pattern. So, for example, you can customise CPython 3.8 builds with an override on `cp38-*` or musllinux builds by selecting `*musllinux*`. Check out [the docs](https://cibuildwheel.pypa.io/en/latest/options/#overrides) for more info on the specifics.
|
||||
You can now set build options for any subset of your builds using a match pattern. So, for example, you can customise CPython 3.8 builds with an override on `cp38-*` or musllinux builds by selecting `*musllinux*`. Check out [the docs](https://cibuildwheel.pypa.io/en/stable/options/#overrides) for more info on the specifics.
|
||||
|
||||
- 🛠 Added support for building PyPy wheels on macOS 11 CI runners. (#875)
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
title: Configuring a CI service
|
||||
---
|
||||
|
||||
## Configuring a CI service
|
||||
|
||||
cibuildwheel works on many popular CI services. Others may work, but it will depend on the software installed on the CI machine/image. See the [platforms page](platforms.md) for details.
|
||||
|
||||
### GitHub Actions [linux/mac/windows] {: #github-actions}
|
||||
|
||||
To build Linux, macOS, and Windows wheels using GitHub Actions, create a `.github/workflows/build_wheels.yml` file in your repo.
|
||||
|
||||
!!! tab "Action"
|
||||
For GitHub Actions, `cibuildwheel` provides an action you can use. This is
|
||||
concise and enables easier auto updating via GitHub's Dependabot; see
|
||||
[Automatic updates](faq.md#automatic-updates).
|
||||
|
||||
> .github/workflows/build_wheels.yml
|
||||
|
||||
```yaml
|
||||
{% include "../examples/github-minimal.yml" %}
|
||||
```
|
||||
|
||||
Use `env:` to pass [build options](options.md) and `with:` to set
|
||||
`package-dir: .`, `output-dir: wheelhouse` and `config-file: ''`
|
||||
locations (those values are the defaults). You can also pass a
|
||||
comma-separated list of extras to install additional packages.
|
||||
For example, `extras: "uv"` to install UV into the virtual environment.
|
||||
|
||||
!!! tab "pipx"
|
||||
The GitHub Actions runners have pipx installed, so you can easily build in
|
||||
just one line. This is internally how the action works; the main benefit of
|
||||
the action form is easy updates via GitHub's Dependabot.
|
||||
|
||||
> .github/workflows/build_wheels.yml
|
||||
|
||||
```yaml
|
||||
{% include "../examples/github-pipx.yml" %}
|
||||
```
|
||||
|
||||
!!! tab "Generic"
|
||||
This is the most generic form using setup-python and pip; it looks the most
|
||||
like the other CI examples. If you want to avoid having setup that takes
|
||||
advantage of GitHub Actions features or pipx being preinstalled, this might
|
||||
appeal to you.
|
||||
|
||||
> .github/workflows/build_wheels.yml
|
||||
{%
|
||||
include-markdown "../README.md"
|
||||
start="<!--generic-github-start-->"
|
||||
end="<!--generic-github-end-->"
|
||||
%}
|
||||
|
||||
Commit this file, and push to GitHub - either to your default branch, or to a PR branch. The build should start automatically.
|
||||
|
||||
For more info on this file, check out the [docs](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions).
|
||||
|
||||
[`examples/github-deploy.yml`](https://github.com/pypa/cibuildwheel/blob/main/examples/github-deploy.yml) extends this minimal example to include Android, iOS and Pyodide builds, and a demonstration of how to automatically upload the built wheels to PyPI.
|
||||
|
||||
|
||||
### Azure Pipelines [linux/mac/windows] {: #azure-pipelines}
|
||||
|
||||
To build Linux, Mac, and Windows wheels on Azure Pipelines, create a `azure-pipelines.yml` file in your repo.
|
||||
|
||||
> azure-pipelines.yml
|
||||
|
||||
```yaml
|
||||
{% include "../examples/azure-pipelines-minimal.yml" %}
|
||||
```
|
||||
|
||||
Commit this file, enable building of your repo on Azure Pipelines, and push.
|
||||
|
||||
Wheels will be stored for you and available through the Pipelines interface. For more info on this file, check out the [docs](https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema).
|
||||
|
||||
### Travis CI [linux/windows] {: #travis-ci}
|
||||
|
||||
To build Linux and Windows wheels on Travis CI, create a `.travis.yml` file in your repo.
|
||||
|
||||
> .travis.yml
|
||||
|
||||
```yaml
|
||||
{% include "../examples/travis-ci-minimal.yml" %}
|
||||
```
|
||||
|
||||
Commit this file, enable building of your repo on Travis CI, and push.
|
||||
|
||||
Then setup a deployment method by following the [Travis CI deployment docs](https://docs.travis-ci.com/user/deployment/), or see [Delivering to PyPI](deliver-to-pypi.md). For more info on `.travis.yml`, check out the [docs](https://docs.travis-ci.com/).
|
||||
|
||||
[`examples/travis-ci-deploy.yml`](https://github.com/pypa/cibuildwheel/blob/main/examples/travis-ci-deploy.yml) extends this minimal example with a demonstration of how to automatically upload the built wheels to PyPI.
|
||||
|
||||
### CircleCI [linux/mac] {: #circleci}
|
||||
|
||||
To build Linux and Mac wheels on CircleCI, create a `.circleci/config.yml` file in your repo,
|
||||
|
||||
> .circleci/config.yml
|
||||
|
||||
```yaml
|
||||
{% include "../examples/circleci-minimal.yml" %}
|
||||
```
|
||||
|
||||
Commit this file, enable building of your repo on CircleCI, and push.
|
||||
|
||||
!!! note
|
||||
CircleCI doesn't enable free macOS containers for open source by default, but you can ask for access. See [here](https://circleci.com/docs/2.0/oss/#overview) for more information.
|
||||
|
||||
CircleCI will store the built wheels for you - you can access them from the project console. Check out the CircleCI [docs](https://circleci.com/docs/2.0/configuration-reference/#section=configuration) for more info on this config file.
|
||||
|
||||
### Gitlab CI [linux] {: #gitlab-ci}
|
||||
|
||||
To build Linux wheels on Gitlab CI, create a `.gitlab-ci.yml` file in your repo,
|
||||
|
||||
> .gitlab-ci.yml
|
||||
|
||||
```yaml
|
||||
{% include "../examples/gitlab-minimal.yml" %}
|
||||
```
|
||||
|
||||
Commit this file, and push to Gitlab. The pipeline should start automatically.
|
||||
|
||||
Gitlab will store the built wheels for you - you can access them from the Pipelines view. Check out the Gitlab [docs](https://docs.gitlab.com/ee/ci/yaml/) for more info on this config file.
|
||||
|
||||
### Cirrus CI [linux/mac/windows] {: #cirrus-ci}
|
||||
|
||||
To build Linux, Mac, and Windows wheels on Cirrus CI, create a `.cirrus.yml` file in your repo,
|
||||
|
||||
> .cirrus.yml
|
||||
|
||||
```yaml
|
||||
{% include "../examples/cirrus-ci-minimal.yml" %}
|
||||
```
|
||||
|
||||
Commit this file, enable building of your repo on Cirrus CI, and push.
|
||||
|
||||
Cirrus CI will store the built wheels for you - you can access them from the individual task view. Check out the Cirrus CI [docs](https://cirrus-ci.org/guide/writing-tasks/) for more info on this config file.
|
||||
|
||||
> ⚠️ Got an error? Check the [FAQ](faq.md).
|
||||
|
||||
### Other CI services
|
||||
|
||||
#### AppVeyor {: #appveyor}
|
||||
|
||||
Appveyor official support was dropped in cibuildwheel v3.0, due to a lack of CI credits. However, it can probably still be used as-is. Check the Appveyor example from the cibuildwheel v2.0 branch: [appveyor-minimal.yml](https://github.com/pypa/cibuildwheel/blob/v2.23.3/examples/appveyor-minimal.yml).
|
||||
|
||||
## Next steps
|
||||
|
||||
Once you've got the wheel building successfully, you might want to set up [testing](options.md#test-command) or [automatic releases to PyPI](deliver-to-pypi.md#automatic-method).
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
$('.toctree-l2>a, .rst-content h3').each(function(i, el) {
|
||||
var text = $(el).text()
|
||||
var match = text.match(/(.*) \[([a-z/]+)\]/);
|
||||
|
||||
if (match) {
|
||||
var iconHTML = $.map(match[2].split('/'), function(ident) {
|
||||
switch (ident) {
|
||||
case 'linux':
|
||||
return '<i class="fa fa-linux" aria-hidden="true"></i>'
|
||||
case 'windows':
|
||||
return '<i class="fa fa-windows" aria-hidden="true"></i>'
|
||||
case 'mac':
|
||||
return '<i class="fa fa-apple" aria-hidden="true"></i>'
|
||||
}
|
||||
}).join(' ');
|
||||
|
||||
$(el).append(
|
||||
$('<div>')
|
||||
.append(iconHTML)
|
||||
.css({float: 'right'})
|
||||
)
|
||||
$(el).contents()
|
||||
.filter(function(){ return this.nodeType == 3; }).first()
|
||||
.replaceWith(match[1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,230 @@
|
||||
# Configuration methods
|
||||
|
||||
cibuildwheel can either be configured using environment variables, or from
|
||||
config file such as `pyproject.toml`.
|
||||
|
||||
This page describes how to set options. For a full list of available options, see the [options reference](options.md).
|
||||
|
||||
## Environment variables {: #environment-variables}
|
||||
|
||||
Environment variables can be set in your CI config. For example, to configure
|
||||
cibuildwheel to run tests, add the following YAML to your CI config file:
|
||||
|
||||
!!! tab "GitHub Actions"
|
||||
|
||||
> .github/workflows/*.yml ([docs](https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables)) (can be global, in job, or in step)
|
||||
|
||||
```yaml
|
||||
env:
|
||||
CIBW_TEST_REQUIRES: pytest
|
||||
CIBW_TEST_COMMAND: "pytest {project}/tests"
|
||||
```
|
||||
|
||||
!!! tab "Azure Pipelines"
|
||||
|
||||
> azure-pipelines.yml ([docs](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables))
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
CIBW_TEST_REQUIRES: pytest
|
||||
CIBW_TEST_COMMAND: "pytest {project}/tests"
|
||||
```
|
||||
|
||||
!!! tab "Travis CI"
|
||||
|
||||
> .travis.yml ([docs](https://docs.travis-ci.com/user/environment-variables/))
|
||||
|
||||
```yaml
|
||||
env:
|
||||
global:
|
||||
- CIBW_TEST_REQUIRES=pytest
|
||||
- CIBW_TEST_COMMAND="pytest {project}/tests"
|
||||
```
|
||||
|
||||
!!! tab "CircleCI"
|
||||
|
||||
> .circleci/config.yml ([docs](https://circleci.com/docs/2.0/configuration-reference/#environment))
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
job_name:
|
||||
environment:
|
||||
CIBW_TEST_REQUIRES: pytest
|
||||
CIBW_TEST_COMMAND: "pytest {project}/tests"
|
||||
```
|
||||
|
||||
!!! tab "Gitlab CI"
|
||||
|
||||
> .gitlab-ci.yml ([docs](https://docs.gitlab.com/ci/yaml/#variables))
|
||||
|
||||
```yaml
|
||||
linux:
|
||||
variables:
|
||||
CIBW_TEST_REQUIRES: pytest
|
||||
CIBW_TEST_COMMAND: "pytest {project}/tests"
|
||||
```
|
||||
|
||||
!!! tab "Cirrus CI"
|
||||
|
||||
> .cirrus.yml ([docs](https://cirrus-ci.org/guide/writing-tasks/#environment-variables))
|
||||
|
||||
```yaml
|
||||
env:
|
||||
CIBW_TEST_REQUIRES: pytest
|
||||
CIBW_TEST_COMMAND: "pytest {project}/tests"
|
||||
```
|
||||
|
||||
## Configuration file {: #configuration-file}
|
||||
|
||||
You can configure cibuildwheel with a config file, such as `pyproject.toml`.
|
||||
Options have the same names as the environment variable overrides, but are
|
||||
placed in `[tool.cibuildwheel]` and are lower case, with dashes, following
|
||||
common [TOML](https://toml.io) practice. Anything placed in subsections `linux`, `windows`,
|
||||
`macos`, or `pyodide` will only affect those platforms. Lists can be used
|
||||
instead of strings for items that are naturally a list. Multiline strings also
|
||||
work just like in the environment variables. Environment variables will take
|
||||
precedence if defined.
|
||||
|
||||
The example above using environment variables could have been written like this:
|
||||
|
||||
```toml
|
||||
[tool.cibuildwheel]
|
||||
test-requires = "pytest"
|
||||
test-command = "pytest ./tests"
|
||||
```
|
||||
|
||||
The complete set of defaults for the current version of cibuildwheel are shown below:
|
||||
|
||||
```toml
|
||||
{% include "../cibuildwheel/resources/defaults.toml" %}
|
||||
```
|
||||
|
||||
|
||||
!!! tip
|
||||
Static configuration works across all CI systems, and can be used locally if
|
||||
you run `cibuildwheel --platform linux`. This is preferred, but environment
|
||||
variables are better if you need to change per-matrix element
|
||||
(`CIBW_BUILD` is often in this category, for example), or if you cannot or do
|
||||
not want to change a `pyproject.toml` file. You can specify a different file to
|
||||
use with `--config-file` on the command line, as well.
|
||||
|
||||
## Configuration overrides {: #overrides }
|
||||
|
||||
One feature specific to the configuration files is the ability to override
|
||||
settings based on selectors. To use, add a ``tool.cibuildwheel.overrides``
|
||||
array, and specify a ``select`` string, referencing the [build identifier](options.md#build-skip) (not wheel name!).
|
||||
Then any options you set will only
|
||||
apply to items that match that selector. These are applied in order, with later
|
||||
matches overriding earlier ones if multiple selectors match. Environment
|
||||
variables always override static configuration.
|
||||
|
||||
A few of the options below have special handling in overrides. A different
|
||||
`before-all` will trigger a new container to launch on Linux, and cannot be
|
||||
overridden on macOS or Windows. Overriding the image on linux will also
|
||||
trigger new containers, one per image.
|
||||
|
||||
!!! note "Some commands are not supported"
|
||||
|
||||
The ``output-dir``, ``build``, ``skip``, ``test_skip`` selectors, and architectures cannot be overridden.
|
||||
|
||||
You can specify a table of overrides in `inherit={}`, any list or table in this
|
||||
list will inherit from previous overrides or the main configuration. The valid
|
||||
options are `"none"` (the default), `"append"`, and `"prepend"`.
|
||||
|
||||
#### Examples:
|
||||
|
||||
```toml
|
||||
[tool.cibuildwheel.linux]
|
||||
before-all = "yum install mylib"
|
||||
test-command = "echo 'installed'"
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "*-musllinux*"
|
||||
before-all = "apk add mylib"
|
||||
```
|
||||
|
||||
This example will override the before-all command on musllinux only, but will
|
||||
still run the test-command. Note the double brackets, this is an array in TOML,
|
||||
which means it can be given multiple times.
|
||||
|
||||
```toml
|
||||
[tool.cibuildwheel]
|
||||
# Normal options, etc.
|
||||
manylinux-x86_64-image = "manylinux_2_34"
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp38-*"
|
||||
manylinux-x86_64-image = "manylinux2014"
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp3{9,10}-*"
|
||||
manylinux-x86_64-image = "manylinux_2_28"
|
||||
```
|
||||
|
||||
This example will build CPython 3.8 wheels on manylinux2014, CPython 3.9-3.10
|
||||
wheels on manylinux_2_28, and manylinux_2_34 wheels for any newer Python
|
||||
(like 3.10).
|
||||
|
||||
```toml
|
||||
[tool.cibuildwheel]
|
||||
environment = {FOO="BAR", "HAM"="EGGS"}
|
||||
test-command = ["pyproject"]
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp311*"
|
||||
|
||||
inherit.test-command = "prepend"
|
||||
test-command = ["pyproject-before"]
|
||||
|
||||
inherit.environment="append"
|
||||
environment = {FOO="BAZ", "PYTHON"="MONTY"}
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp311*"
|
||||
inherit.test-command = "append"
|
||||
test-command = ["pyproject-after"]
|
||||
```
|
||||
|
||||
This example will provide the command `"pyproject-before && pyproject && pyproject-after"`
|
||||
on Python 3.11, and will have `environment = {FOO="BAZ", "PYTHON"="MONTY", "HAM"="EGGS"}`.
|
||||
|
||||
|
||||
## Extending existing options {: #inherit }
|
||||
|
||||
In the TOML configuration, you can choose how tables and lists are inherited.
|
||||
By default, all values are overridden completely (`"none"`) but sometimes you'd
|
||||
rather `"append"` or `"prepend"` to an existing list or table. You can do this
|
||||
with the `inherit` table in overrides. For example, if you want to add an environment
|
||||
variable for CPython 3.11, without `inherit` you'd have to repeat all the
|
||||
original environment variables in the override. With `inherit`, it's just:
|
||||
|
||||
```toml
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp311*"
|
||||
inherit.environment = "append"
|
||||
environment.NEWVAR = "Added!"
|
||||
```
|
||||
|
||||
For a table, `"append"` will replace a key if it exists, while `"prepend"` will
|
||||
only add a new key, older keys take precedence.
|
||||
|
||||
Lists are also supported (and keep in mind that commands are lists). For
|
||||
example, you can print a message before and after a wheel is repaired:
|
||||
|
||||
```toml
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "*"
|
||||
inherit.repair-wheel-command = "prepend"
|
||||
repair-wheel-command = "echo 'Before repair'"
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "*"
|
||||
inherit.repair-wheel-command = "append"
|
||||
repair-wheel-command = "echo 'After repair'"
|
||||
```
|
||||
|
||||
As seen in this example, you can have multiple overrides match - they match top
|
||||
to bottom, with the config being accumulated. If you need platform-specific
|
||||
inheritance, you can use `select = "*-????linux_*"` for Linux, `select =
|
||||
"*-win_*"` for Windows, and `select = "*-macosx_*"` for macOS. As always,
|
||||
environment variables will completely override any TOML configuration.
|
||||
+34
-14
@@ -16,12 +16,12 @@ Everyone contributing to the cibuildwheel project is expected to follow the [PSF
|
||||
|
||||
- `cibuildwheel` should wrap the complexity of wheel building.
|
||||
- The user interface to `cibuildwheel` is the build script (e.g. `.travis.yml`). Feature additions should not increase the complexity of this script.
|
||||
- Options should be environment variables (these lend themselves better to YML config files). They should be prefixed with `CIBW_`.
|
||||
- Options should be generalise to all platforms. If platform-specific options are required, they should be namespaced e.g. `CIBW_TEST_COMMAND_MACOS`
|
||||
- Options should be environment variables (these lend themselves better to YAML config files). They should be prefixed with `CIBW_`.
|
||||
- Options should be generalised to all platforms. If platform-specific options are required, they should be namespaced e.g. `CIBW_TEST_COMMAND_MACOS`
|
||||
|
||||
Other notes:
|
||||
|
||||
- The platforms are very similar, until they're not. I'd rather have straight-forward code than totally DRY code, so let's keep airy platform abstractions to a minimum.
|
||||
- The platforms are very similar, until they're not. I'd rather have straightforward code than totally DRY code, so let's keep airy platform abstractions to a minimum.
|
||||
- I might want to break the options into a shared config file one day, so that config is more easily shared. That has motivated some of the design decisions.
|
||||
|
||||
### cibuildwheel's relationship with build errors
|
||||
@@ -36,7 +36,7 @@ So, if we can, I'd like to improve the experience on errors as well. In [this](h
|
||||
|
||||
### Running the tests
|
||||
|
||||
When making a change to the codebase, you can run tests locally for quicker feedback than the CI runs on a PR. You can [run them directly](#making-a-venv), but the easiest way to run tests is using [nox](https://nox.thea.codes/).
|
||||
When making a change to the codebase, you can run tests locally for quicker feedback than the CI runs on a PR. You can run them directly, but the easiest way to run tests is using [nox](https://nox.thea.codes/).
|
||||
|
||||
You can run all the tests locally by doing:
|
||||
|
||||
@@ -78,18 +78,17 @@ nox -s tests -- test -k before_build
|
||||
|
||||
A few notes-
|
||||
|
||||
- Because they run inside a container, Linux tests can run on all platforms where Docker is installed, so they're convenient for running integration tests locally. Set CIBW_PLATFORM to do this: `CIBW_PLATFORM=linux nox -s tests -- test`.
|
||||
- Because they run inside a container, Linux tests can run on all platforms where Docker is installed, so they're convenient for running integration tests locally. Set the `--platform` flag on pytest to do this: `nox -s tests -- test --platform linux`.
|
||||
|
||||
- Running the macOS integration tests requires _system installs_ of Python from python.org for all the versions that are tested. We won't attempt to install these when running locally, but you can do so manually using the URL in the error message that is printed when the install is not found.
|
||||
|
||||
#### Making a venv
|
||||
- The ['enable groups'](options.md#enable) run by default are just 'cpython-prerelease' and 'cpython-freethreading'. You can add other groups like pypy or graalpy by passing the `--enable` argument to pytest, i.e. `nox -s tests -- test --enable pypy`. On GitHub PRs, you can add a label to the PR to enable these groups.
|
||||
|
||||
More advanced users might prefer to invoke pytest directly-
|
||||
#### Running pytest directly
|
||||
|
||||
More advanced users might prefer to invoke pytest directly. Set up a [dev environment](#setting-up-a-dev-environment), then,
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -e .[dev]
|
||||
# run the unit tests
|
||||
pytest unit_test
|
||||
# run the whole integration test suite
|
||||
@@ -102,23 +101,44 @@ CIBW_PLATFORM=linux pytest test -k test_build_frontend_args
|
||||
|
||||
### Linting, docs
|
||||
|
||||
Most developer tasks have a nox interface. This allows you to very simply run tasks without worrying about setting up a development environment (as shown below). This is a slower than setting up a development environment and reusing it, but has the (important) benefit of being highly reproducible; an earlier run does not affect a current run, or anything else on your machine.
|
||||
Most developer tasks have a nox interface. This allows you to very simply run tasks without worrying about setting up a development environment (as shown below). This is slower than setting up a development environment and reusing it, but has the (important) benefit of being highly reproducible; an earlier run does not affect a current run, or anything else on your machine.
|
||||
|
||||
You can see a list of sessions by typing `nox -l`; here are a few common ones:
|
||||
|
||||
```console
|
||||
nox -s lint # Run the linters (default)
|
||||
nox -s tests [-- PYTEST-ARGS] # Run the tests (default)
|
||||
nox -s docs -- serve # Build and serve the documentation
|
||||
nox -s docs # Build and serve the documentation
|
||||
nox -s build # Make SDist and wheel
|
||||
```
|
||||
|
||||
More advanced users can run the update scripts. `update_pins` should work directly, but `update_constraints` needs all versions of Python installed. If you don't want to do that locally, a fast way to run it to use docker to run nox:
|
||||
More advanced users can run the update scripts:
|
||||
|
||||
```console
|
||||
docker run --rm -itv $PWD:/src -w /src quay.io/pypa/manylinux_2_24_x86_64:latest pipx run nox -s update_constraints
|
||||
nox -s update_constraints # update all constraints files in cibuildwheel/resources
|
||||
nox -s update_pins # update tools, python interpreters & docker images used by cibuildwheel
|
||||
```
|
||||
|
||||
### Setting up a dev environment
|
||||
|
||||
A dev environment isn't required for any of the `nox` tasks above. However, a dev environment is still useful, to be able to point an editor at, and a few other jobs.
|
||||
|
||||
cibuildwheel uses dependency groups. Set up a dev environment with UV by doing
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
Or, if you're not using `uv`, you can do:
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pipx run dependency-groups dev | xargs pip install -e.
|
||||
```
|
||||
|
||||
Your virtualenv is at `.venv`.
|
||||
|
||||
## Maintainer notes
|
||||
|
||||
### Testing sample configs
|
||||
|
||||
@@ -7,20 +7,19 @@ title: Modern C++ standards
|
||||
Building Python wheels with modern C++ standards (C++11 and later) requires a few tricks.
|
||||
|
||||
|
||||
## manylinux1 and C++14
|
||||
The old `manylinux1` image (based on CentOS 5) contains a version of GCC and libstdc++ that only supports C++11 and earlier standards. There are however ways to compile wheels with the C++14 standard (and later): https://github.com/pypa/manylinux/issues/118
|
||||
## manylinux2014 and C++20
|
||||
|
||||
`manylinux2010` and `manylinux2014` are newer and support all C++ standards (up to C++17).
|
||||
The past end-of-life `manylinux2014` image (based on CentOS 7) contains a version of GCC and libstdc++ that only supports C++17 and earlier standards.
|
||||
|
||||
`manylinux_2_28` are newer and support all C++ standards (up to C++20).
|
||||
|
||||
## macOS and deployment target versions
|
||||
|
||||
OS X/macOS allows you to specify a so-called "deployment target" version that will ensure backwards compatibility with older versions of macOS. One way to do this is by setting the `MACOSX_DEPLOYMENT_TARGET` environment variable.
|
||||
The [`MACOSX_DEPLOYMENT_TARGET` environment variable](platforms.md#macos-version-compatibility) is used to set the minimum deployment target for macOS.
|
||||
|
||||
However, to enable modern C++ standards, the deployment target needs to be set high enough (since older OS X/macOS versions did not have the necessary modern C++ standard library).
|
||||
|
||||
To get C++11 and C++14 support, `MACOSX_DEPLOYMENT_TARGET` needs to be set to (at least) `"10.9"`. By default, `cibuildwheel` already does this, building 64-bit-only wheels for macOS 10.9 and later.
|
||||
|
||||
To get C++17 support, Xcode 9.3+ is needed, requiring at least macOS 10.13 on the build machine. To use C++17 library features and link against the C++ runtime library, set `MACOSX_DEPLOYMENT_TARGET` to `"10.13"` or `"10.14"` (or higher) - macOS 10.13 offers partial C++17 support (e.g., the filesystem header is in experimental, offering `#include <experimental/filesystem>` instead of `#include <filesystem>`); macOS 10.14 has full C++17 support.
|
||||
To get C++17 support, Xcode 9.3+ is needed, requiring at least macOS 10.13 on the build machine. To use C++17 library features and link against the C++ runtime library, set `MACOSX_DEPLOYMENT_TARGET` to `"10.13"` or `"10.14"` (or higher) - macOS 10.13 offers partial C++17 support (e.g., the filesystem header is in experimental, offering `#include <experimental/filesystem>` instead of `#include <filesystem>`); macOS 10.14 has full C++17 support. CPython 3.12+ require 10.13+ anyway.
|
||||
|
||||
However, if only C++17 compiler and standard template library (STL) features are used (not needing a C++17 runtime) it might be possible to set `MACOSX_DEPLOYMENT_TARGET` to a lower value, such as `"10.9"`. To find out if this is the case, try compiling and running with a lower `MACOSX_DEPLOYMENT_TARGET`: if C++17 features are used that require a more recent deployment target, building the wheel should fail.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user