Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
7e5a838a63 | ||
|
|
19e1b8a4b0 | ||
|
|
73581ae579 | ||
|
|
f21ff5e84c | ||
|
|
973946b172 | ||
|
|
5ea40e6ff3 | ||
|
|
6a36f6494b | ||
|
|
31de15bcd9 | ||
|
|
3179fd215a | ||
|
|
184d4e1280 | ||
|
|
08165ffeed | ||
|
|
9bb03835e5 | ||
|
|
2552303ff3 | ||
|
|
5b74582a72 | ||
|
|
4470eb0dd9 | ||
|
|
8bda260d3a | ||
|
|
5b5a95bdc2 | ||
|
|
561fa32683 | ||
|
|
70913f0a49 | ||
|
|
f49397f151 | ||
|
|
c1b8a12cd5 | ||
|
|
932529cab1 | ||
|
|
70fb1c4a1a | ||
|
|
405a475ac7 | ||
|
|
78da7bafcc | ||
|
|
8d86d3122b | ||
|
|
89a5cfe272 | ||
|
|
8d5d84e0fc | ||
|
|
4ada77dea4 | ||
|
|
829441f4a0 | ||
|
|
a8d190a111 | ||
|
|
bf817c6dc8 | ||
|
|
384c8d5c82 | ||
|
|
c37e5a2d13 | ||
|
|
130fdd2548 | ||
|
|
ed120540d9 | ||
|
|
6c6e0f6ba5 | ||
|
|
877d3bf649 | ||
|
|
21e9ef1800 | ||
|
|
c333b131d3 | ||
|
|
4c2ed1e632 | ||
|
|
9f2a3cb67c | ||
|
|
687406f664 | ||
|
|
2e6b11e10c | ||
|
|
b713086763 | ||
|
|
8f574d107b | ||
|
|
97da90432e | ||
|
|
67ee9dda92 | ||
|
|
ba8be0d988 | ||
|
|
90dd47640f | ||
|
|
b61324914b | ||
|
|
3992d5719c | ||
|
|
345467c6cf | ||
|
|
791e41cfec | ||
|
|
9d5f5e60b8 | ||
|
|
1b354cfa63 | ||
|
|
d1a4c9c074 | ||
|
|
78bca57cb4 | ||
|
|
f8894e67f5 | ||
|
|
666a2c3f1a | ||
|
|
15447bd591 | ||
|
|
678ef21647 | ||
|
|
e82b1bfc44 | ||
|
|
ced16da65f | ||
|
|
63daa02352 | ||
|
|
711a3d017d | ||
|
|
3873a1edf8 | ||
|
|
6dd4f15a0e | ||
|
|
44c4742477 | ||
|
|
c0217440e8 | ||
|
|
cf18014fce | ||
|
|
3ea0a6c2f0 | ||
|
|
30a0decb47 | ||
|
|
988d512fb3 | ||
|
|
98d57d9547 | ||
|
|
8daac05d63 | ||
|
|
950fbd0a60 | ||
|
|
d24ebf4125 | ||
|
|
8a0d1d59c7 | ||
|
|
208ca32050 | ||
|
|
9cf99e78bc | ||
|
|
2fb3205ff1 | ||
|
|
376414dc69 | ||
|
|
070e744d26 | ||
|
|
89e0a90bc5 | ||
|
|
312cf1573e | ||
|
|
6537b8f51f | ||
|
|
3f00a2fded | ||
|
|
a8d1defda9 | ||
|
|
510382886d | ||
|
|
b3c4d37885 | ||
|
|
3eb6dbbdd3 | ||
|
|
90c14a333f | ||
|
|
8d945475ac | ||
|
|
ca06deb26f | ||
|
|
f7e1922225 | ||
|
|
0d8e919dfc | ||
|
|
022de07dc1 | ||
|
|
920f574191 | ||
|
|
5c06f3c289 | ||
|
|
e2a0839555 | ||
|
|
87fff77282 | ||
|
|
8ef9486aab | ||
|
|
2a7fef07db | ||
|
|
f7de18e291 | ||
|
|
80095ac7af | ||
|
|
df2e35a016 | ||
|
|
f654071fe2 | ||
|
|
b71a83d411 | ||
|
|
a6ecded8c4 | ||
|
|
8f58f71c2e | ||
|
|
6940c77cd3 | ||
|
|
ae2451a199 | ||
|
|
1fe6b1aa44 | ||
|
|
554726e3c4 | ||
|
|
8742ff2ef8 | ||
|
|
75627e8ca4 | ||
|
|
3ae9aa1da5 | ||
|
|
f04a522d50 | ||
|
|
edd67e0d26 | ||
|
|
8a8c07d4f9 | ||
|
|
74afe329a1 | ||
|
|
7ada127bfc | ||
|
|
6b3122aff7 | ||
|
|
86ed421c1b | ||
|
|
b4a22b6d5e | ||
|
|
7c02fa721c | ||
|
|
88d60dce89 | ||
|
|
1dea9e4a14 | ||
|
|
53e2c74ab0 | ||
|
|
32343a3cf1 | ||
|
|
2e0ab0176c | ||
|
|
75d6d9d55b | ||
|
|
81b0a578df | ||
|
|
27d0942e1c | ||
|
|
68f1058d7e | ||
|
|
dfca0ce96a | ||
|
|
abd05a65ba | ||
|
|
d65d46ca24 | ||
|
|
88c113402d | ||
|
|
c7a653d6a9 | ||
|
|
6c93c131c4 | ||
|
|
d635c0eee6 | ||
|
|
a78facb861 | ||
|
|
3bf1908dcb | ||
|
|
ddc2c6e845 | ||
|
|
4db49f5485 | ||
|
|
4ca5d7cd2a | ||
|
|
9879937c63 | ||
|
|
2a1edad716 | ||
|
|
c2b03ead3e | ||
|
|
d198c90f7a |
+38
-32
@@ -1,25 +1,43 @@
|
||||
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.9:
|
||||
osx-python312:
|
||||
macos:
|
||||
xcode: 12.5.1
|
||||
xcode: 15.4.0
|
||||
resource_class: macos.m1.medium.gen1
|
||||
environment:
|
||||
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.9:
|
||||
linux-python312:
|
||||
docker:
|
||||
- image: circleci/python:3.9
|
||||
- image: cimg/python:3.12
|
||||
environment:
|
||||
PYTHON: python3
|
||||
# Temporarily restrict the tests that are run on CircleCI to prevent
|
||||
@@ -28,18 +46,12 @@ 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:
|
||||
image: ubuntu-2004:2022.04.1
|
||||
image: default
|
||||
resource_class: arm.medium
|
||||
environment:
|
||||
PYTHON: python3
|
||||
@@ -48,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.9
|
||||
- linux-python3.9
|
||||
- osx-python312
|
||||
- linux-python312
|
||||
- linux-aarch64
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
set -o errexit
|
||||
set -o xtrace
|
||||
|
||||
if [ "$(uname -s)" == "Darwin" ]; then
|
||||
sudo softwareupdate --install-rosetta --agree-to-license
|
||||
else
|
||||
docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
|
||||
fi
|
||||
|
||||
$PYTHON --version
|
||||
$PYTHON -m pip --version
|
||||
$PYTHON -m virtualenv -p "$PYTHON" venv
|
||||
venv/bin/python -m pip install -e ".[dev]"
|
||||
$PYTHON -m venv venv
|
||||
venv/bin/python -m pip install -U pip
|
||||
venv/bin/python -m pip install -e. --group test
|
||||
venv/bin/python -m pip freeze
|
||||
venv/bin/python --version
|
||||
|
||||
+37
-22
@@ -1,12 +1,12 @@
|
||||
only_if: changesInclude('.cirrus.yml') || ($BRANCH == "main" && !changesIncludeOnly('docs/*', '.pre-commit-config.yaml'))
|
||||
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,32 +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-monterey-xcode
|
||||
|
||||
image: ghcr.io/cirruslabs/macos-runner:sequoia
|
||||
env:
|
||||
PATH: /opt/homebrew/opt/python@3.10/bin:$PATH
|
||||
VENV_ROOT: ${HOME}/venv-cibuildwheel
|
||||
PATH: ${VENV_ROOT}/bin:${PATH}
|
||||
install_pre_requirements_script:
|
||||
- brew install python@3.10
|
||||
- ln -s python3 /opt/homebrew/opt/python@3.10/bin/python
|
||||
- brew install python@3.12
|
||||
- python3.12 -m venv ${VENV_ROOT}
|
||||
<<: *RUN_TESTS
|
||||
|
||||
macos_arm64_cp38_task:
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-monterey-xcode
|
||||
|
||||
image: ghcr.io/cirruslabs/macos-runner:sequoia
|
||||
env:
|
||||
PATH: /opt/homebrew/opt/python@3.10/bin:$PATH
|
||||
PYTEST_ADDOPTS: --run-cp38-universal2 -k 'test_cp38_arm64_testing_universal2_installer or test_arch_auto'
|
||||
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
|
||||
- ln -s python3 /opt/homebrew/opt/python@3.10/bin/python
|
||||
- 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,3 @@
|
||||
cibuildwheel/platforms/ios.py @freakboy3742
|
||||
cibuildwheel/platforms/pyodide.py @hoodmane @ryanking13 @agriyakhetarpal
|
||||
cibuildwheel/platforms/android.py @mhsmith
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"severity": "warning",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^([^:]+):(\\d+):(\\d+): ([A-DF-Z]\\d+): \\033\\[[\\d;]+m([^\\033]+).*$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"code": 4,
|
||||
"message": 5
|
||||
}
|
||||
],
|
||||
"owner": "pylint-warning"
|
||||
},
|
||||
{
|
||||
"severity": "error",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^([^:]+):(\\d+):(\\d+): (E\\d+): \\033\\[[\\d;]+m([^\\033]+).*$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"code": 4,
|
||||
"message": 5
|
||||
}
|
||||
],
|
||||
"owner": "pylint-error"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -13,30 +13,30 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
environment: pypi
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/cibuildwheel
|
||||
permissions:
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: cibw-*
|
||||
name: Packages
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate artifact attestation for sdist and wheel
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||
with:
|
||||
subject-path: "dist/cibuildwheel-*"
|
||||
|
||||
- uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
attestations: true
|
||||
|
||||
+169
-36
@@ -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,32 +34,32 @@ 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
|
||||
id: python
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- uses: pre-commit/action@v3.0.0
|
||||
- name: Check manifest
|
||||
run: pipx run nox -s check_manifest
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
- name: PyLint checks
|
||||
run: |
|
||||
echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json"
|
||||
pipx run nox -s pylint
|
||||
run: pipx run --python "${{ steps.python.outputs.python-path }}" nox -s pylint -- --output-format=github
|
||||
|
||||
test:
|
||||
name: Test cibuildwheel on ${{ matrix.os }}
|
||||
name: Test on ${{ matrix.os }} (${{ matrix.python_version }})
|
||||
needs: lint
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-11]
|
||||
python_version: ['3.12']
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-13, macos-15]
|
||||
python_version: ['3.13']
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
python_version: '3.8'
|
||||
- os: ubuntu-latest
|
||||
python_version: '3.11'
|
||||
- os: ubuntu-latest
|
||||
python_version: '3.14'
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -50,32 +67,60 @@ jobs:
|
||||
name: Install Python ${{ matrix.python_version }}
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
allow-prereleases: true
|
||||
|
||||
# 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
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
|
||||
# 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
|
||||
|
||||
# https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/
|
||||
- name: Enable KVM for Android emulator
|
||||
if: runner.os == 'Linux' && runner.arch == 'X64'
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
# for oci_container unit tests
|
||||
- name: Set up QEMU
|
||||
if: runner.os == 'Linux'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install ".[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,6 +129,40 @@ jobs:
|
||||
output-dir: wheelhouse
|
||||
env:
|
||||
CIBW_ARCHS_MACOS: x86_64 universal2 arm64
|
||||
CIBW_BUILD_FRONTEND: 'build[uv]'
|
||||
|
||||
- name: Run a sample build (GitHub Action, only)
|
||||
uses: ./
|
||||
with:
|
||||
package-dir: sample_proj
|
||||
output-dir: wheelhouse_only
|
||||
only: cp313-${{ runner.os == 'Linux' && (runner.arch == 'ARM64' && 'manylinux_aarch64' || 'manylinux_x86_64') || (runner.os == 'Windows' && 'win_amd64' || 'macosx_x86_64') }}
|
||||
|
||||
- name: Create custom configuration file
|
||||
shell: bash
|
||||
run: |
|
||||
cat > sample_proj/cibw.toml <<EOF
|
||||
[tool.cibuildwheel]
|
||||
# Only build on CPython 3.13 on native arch
|
||||
archs = ["native"]
|
||||
build = "cp313-*"
|
||||
# Skip musllinux
|
||||
skip = "*-musllinux*"
|
||||
EOF
|
||||
|
||||
- name: Run a sample build (GitHub Action, config-file)
|
||||
uses: ./
|
||||
with:
|
||||
package-dir: sample_proj
|
||||
output-dir: wheelhouse_config_file
|
||||
config-file: sample_proj/cibw.toml
|
||||
|
||||
- name: Check Action artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
test $(find wheelhouse -name '*.whl' | wc -l) -ge 1
|
||||
test $(find wheelhouse_only -name '*.whl' | wc -l) -eq 1
|
||||
test $(find wheelhouse_config_file -name '*.whl' | wc -l) -eq 1
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -92,28 +171,82 @@ jobs:
|
||||
|
||||
- name: Test cibuildwheel
|
||||
run: |
|
||||
python ./bin/run_tests.py --run-podman
|
||||
uv run --no-sync bin/run_tests.py ${{ (runner.os == 'Linux' && runner.arch == 'X64') && '--run-podman' || '' }}
|
||||
|
||||
test-emulated:
|
||||
name: Test emulated cibuildwheel using qemu
|
||||
emulated-archs:
|
||||
name: Get qemu emulated architectures
|
||||
needs: lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
outputs:
|
||||
archs: ${{ steps.archs.outputs.archs }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
- name: Install dependencies
|
||||
run: uv sync --no-dev --group test
|
||||
- name: Get qemu emulated architectures
|
||||
id: archs
|
||||
run: |
|
||||
python -m pip install ".[test]"
|
||||
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"
|
||||
|
||||
test-emulated:
|
||||
name: Test Linux ${{ matrix.arch }} using qemu
|
||||
needs: emulated-archs
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
matrix:
|
||||
arch: ${{ fromJSON(needs.emulated-archs.outputs.archs) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
- name: Install dependencies
|
||||
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: uv run --no-sync pytest --run-emulation ${{ matrix.arch }} test/test_emulation.py
|
||||
|
||||
test-pyodide:
|
||||
name: Test pyodide
|
||||
needs: lint
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
name: Install Python 3.12
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --no-dev --group test
|
||||
|
||||
- name: Generate a sample project
|
||||
run: |
|
||||
pytest --run-emulation test/test_emulation.py
|
||||
uv run --no-sync -m test.test_projects test.test_0_basic.basic_project sample_proj
|
||||
|
||||
- name: Run a sample build (GitHub Action)
|
||||
uses: ./
|
||||
with:
|
||||
package-dir: sample_proj
|
||||
output-dir: wheelhouse
|
||||
env:
|
||||
CIBW_PLATFORM: pyodide
|
||||
|
||||
- name: Run tests with 'CIBW_PLATFORM' set to 'pyodide'
|
||||
run: |
|
||||
uv run --no-sync ./bin/run_tests.py
|
||||
env:
|
||||
CIBW_PLATFORM: pyodide
|
||||
|
||||
@@ -18,13 +18,21 @@ jobs:
|
||||
update-dependencies:
|
||||
name: Update dependencies
|
||||
if: github.repository_owner == 'pypa' || github.event_name != 'schedule'
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
# 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:
|
||||
app-id: ${{ secrets.CIBUILDWHEEL_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.CIBUILDWHEEL_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: wntrblm/nox@2023.04.22
|
||||
with:
|
||||
python-versions: "3.7, 3.8, 3.9, 3.10, 3.11, 3.12"
|
||||
- uses: wntrblm/nox@2025.05.01
|
||||
|
||||
- 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@v5
|
||||
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
|
||||
|
||||
+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/
|
||||
|
||||
+31
-5
@@ -1,5 +1,5 @@
|
||||
linux:
|
||||
image: python:3.8
|
||||
image: python:3.12
|
||||
services:
|
||||
- name: docker:dind
|
||||
entrypoint: ["env", "-u", "DOCKER_HOST"]
|
||||
@@ -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,29 @@ windows:
|
||||
variables:
|
||||
PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
|
||||
before_script:
|
||||
- choco install python -y --version 3.8.6
|
||||
- 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:
|
||||
- windows
|
||||
- saas-windows-medium-amd64
|
||||
|
||||
macos:
|
||||
image: macos-14-xcode-15
|
||||
variables:
|
||||
PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
variables:
|
||||
CIBW_ENABLE: "all"
|
||||
script:
|
||||
- python3 -m pip install -U pip
|
||||
- python3 -m pip install -e. pytest-custom-exit-code --group test
|
||||
- python3 ./bin/run_tests.py
|
||||
tags:
|
||||
- saas-macos-medium-m1
|
||||
|
||||
+30
-28
@@ -1,7 +1,7 @@
|
||||
repos:
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
@@ -14,47 +14,47 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.14
|
||||
rev: v0.12.5
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix", "--show-fixes"]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.5.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: [--include-version-classifiers, --min-py-version=3.8, --max-py-version=3.12]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.8.0
|
||||
rev: v1.17.0
|
||||
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.9.0.6
|
||||
rev: v0.10.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
|
||||
@@ -64,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.2.6
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
args: ["-L", "sur", "-w"]
|
||||
args: ["-w"]
|
||||
exclude: ^docs/working-examples\.md$ # Autogenerated
|
||||
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.27.0
|
||||
rev: 0.33.2
|
||||
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
|
||||
|
||||
+6
-13
@@ -3,16 +3,9 @@
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
python:
|
||||
install:
|
||||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
|
||||
mkdocs:
|
||||
configuration: mkdocs.yml
|
||||
os: ubuntu-24.04
|
||||
commands:
|
||||
- asdf plugin add uv
|
||||
- asdf install uv latest
|
||||
- asdf global uv latest
|
||||
- NO_COLOR=1 uv run --python 3.14 --managed-python --no-dev --group docs mkdocs build --strict --site-dir $READTHEDOCS_OUTPUT/html
|
||||
|
||||
+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
|
||||
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.12 |
|
||||
|---------|--------------------------------------------------------|-----------------------|-----------|----------------|
|
||||
| Linux | AppVeyor¹ / Azure Pipelines / GitLab¹ / GitHub Actions | CircleCI¹ / Travis CI | Cirrus CI | GitHub Actions |
|
||||
| macOS | AppVeyor¹ / Azure Pipelines | CircleCI¹ / Travis CI | Cirrus CI | GitHub Actions |
|
||||
| Windows | AppVeyor¹ / Azure Pipelines / GitLab¹ | Travis CI | Cirrus CI | GitHub Actions |
|
||||
| | 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:
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
recursive-include cibuildwheel *.cfg
|
||||
recursive-include cibuildwheel *.in
|
||||
recursive-include cibuildwheel *.py
|
||||
recursive-include cibuildwheel *.toml
|
||||
recursive-include cibuildwheel *.txt
|
||||
recursive-include cibuildwheel *.typed
|
||||
@@ -2,73 +2,73 @@ cibuildwheel
|
||||
============
|
||||
|
||||
[](https://pypi.python.org/pypi/cibuildwheel)
|
||||
[](https://cibuildwheel.readthedocs.io/en/stable/?badge=stable)
|
||||
[](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)
|
||||
|
||||
|
||||
[Documentation](https://cibuildwheel.readthedocs.org)
|
||||
[Documentation](https://cibuildwheel.pypa.io)
|
||||
|
||||
<!--intro-start-->
|
||||
|
||||
Python wheels are great. Building them across **Mac, Linux, Windows**, on **multiple versions of Python**, is not.
|
||||
|
||||
`cibuildwheel` is here to help. `cibuildwheel` runs on your CI server - currently it supports GitHub Actions, Azure Pipelines, Travis CI, 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 |
|
||||
|----------------|----|-----|-----|-----|-----|----|-----|----|-----|-----|
|
||||
| CPython 3.6 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| CPython 3.7 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| CPython 3.8 | ✅ | ✅ | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅³ | ✅ | ✅ | ✅ | ✅ |
|
||||
| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| CPython 3.12⁵ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| PyPy 3.7 v7.3 | ✅ | N/A | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
|
||||
| PyPy 3.8 v7.3 | ✅ | ✅⁴ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
|
||||
| PyPy 3.9 v7.3 | ✅ | ✅⁴ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
|
||||
| PyPy 3.10 v7.3 | ✅ | ✅⁴ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
|
||||
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³ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | ✅ | ✅ | N/A |
|
||||
| CPython 3.14 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | 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 |
|
||||
|
||||
<sup>¹ PyPy & GraalPy are only supported for manylinux wheels.</sup><br>
|
||||
<sup>² Windows arm64 support is experimental.</sup><br>
|
||||
<sup>³ Alpine 3.14 and very briefly 3.15's default python3 [was not able to load](https://github.com/pypa/cibuildwheel/issues/934) musllinux wheels. This has been fixed; please upgrade the python package if using Alpine from before the fix.</sup><br>
|
||||
<sup>⁴ Cross-compilation not supported with PyPy - to build these wheels you need to run cibuildwheel on an Apple Silicon machine.</sup><br>
|
||||
<sup>⁵ CPython 3.12 is built by default using Python RCs, starting with cibuildwheel 2.15.</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 10.9+ (10.13+ for Python 3.12+), 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
|
||||
|
||||
See the [cibuildwheel 1 documentation](https://cibuildwheel.readthedocs.io/en/1.x/) if you need to build unsupported versions of Python, such as Python 2.
|
||||
See the [cibuildwheel 1 documentation](https://cibuildwheel.pypa.io/en/1.x/) if you need to build unsupported versions of Python, such as Python 2.
|
||||
|
||||
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 | ✅ | ✅³ | ✅ | ✅ | ✅ | |
|
||||
|
||||
<sup>¹ [Requires emulation](https://cibuildwheel.readthedocs.io/en/stable/faq/#emulation), distributed separately. Other services may also support Linux ARM through emulation or third-party build hosts, but these are not tested in our CI.</sup><br>
|
||||
<sup>² [Uses cross-compilation](https://cibuildwheel.readthedocs.io/en/stable/faq/#universal2). It is not possible to test `arm64` and the `arm64` part of a `universal2` wheel on this CI platform.</sup><br>
|
||||
<sup>³ [Uses cross-compilation](https://cibuildwheel.readthedocs.io/en/stable/faq/#universal2). Thanks to Rosetta 2 emulation, it is possible to test `x86_64` and both parts of a `universal2` wheel on this CI platform.</sup><br>
|
||||
<sup>⁴ [Uses cross-compilation](https://cibuildwheel.readthedocs.io/en/stable/faq/#windows-arm64). It is not possible to test `arm64` on this CI platform.</sup>
|
||||
| | 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><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-->
|
||||
|
||||
Example setup
|
||||
@@ -76,6 +76,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
|
||||
|
||||
@@ -87,30 +88,32 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-2019, macOS-11]
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-13, macos-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Used to host cibuildwheel
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/setup-python@v5
|
||||
|
||||
- name: Install cibuildwheel
|
||||
run: python -m pip install cibuildwheel==2.16.5
|
||||
run: python -m pip install cibuildwheel==3.1.3
|
||||
|
||||
- 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.readthedocs.org) and the [examples](https://github.com/pypa/cibuildwheel/tree/main/examples).
|
||||
For more information, including PyPI deployment, and the use of other CI services or the dedicated GitHub Action, check out the [documentation](https://cibuildwheel.pypa.io) and the [examples](https://github.com/pypa/cibuildwheel/tree/main/examples).
|
||||
|
||||
How it works
|
||||
------------
|
||||
@@ -119,35 +122,49 @@ 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.readthedocs.io/en/stable/#how-it-works).</sup>
|
||||
<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.readthedocs.io/en/stable/options/#platform) | Override the auto-detected target platform |
|
||||
| | [`CIBW_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) <br> [`CIBW_SKIP`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) | Choose the Python versions to build |
|
||||
| | [`CIBW_ARCHS`](https://cibuildwheel.readthedocs.io/en/stable/options/#archs) | Change the architectures built on your machine by default. |
|
||||
| | [`CIBW_PROJECT_REQUIRES_PYTHON`](https://cibuildwheel.readthedocs.io/en/stable/options/#requires-python) | Manually set the Python compatibility of your project |
|
||||
| | [`CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#prerelease-pythons) | Enable building with pre-release versions of Python if available |
|
||||
| **Build customization** | [`CIBW_BUILD_FRONTEND`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-frontend) | Set the tool to use to build, either "pip" (default for now) or "build" |
|
||||
| | [`CIBW_ENVIRONMENT`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment) | Set environment variables needed during the build |
|
||||
| | [`CIBW_ENVIRONMENT_PASS_LINUX`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment-pass) | Set environment variables on the host to pass-through to the container during the build. |
|
||||
| | [`CIBW_BEFORE_ALL`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
|
||||
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
|
||||
| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel |
|
||||
| | [`CIBW_MANYLINUX_*_IMAGE`<br/>`CIBW_MUSLLINUX_*_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images |
|
||||
| | [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.readthedocs.io/en/stable/options/#container-engine) | Specify which container engine to use when building Linux wheels |
|
||||
| | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses |
|
||||
| **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel |
|
||||
| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
|
||||
| | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
|
||||
| | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require |
|
||||
| | [`CIBW_TEST_SKIP`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-skip) | Skip running tests on some builds |
|
||||
| **Other** | [`CIBW_BUILD_VERBOSITY`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-verbosity) | Increase/decrease the output of pip wheel |
|
||||
|---|---|---|
|
||||
| **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 |
|
||||
| **Debugging** | [`debug-keep-container`](https://cibuildwheel.pypa.io/en/stable/options/#debug-keep-container) | Keep the container after running for debugging. |
|
||||
| | [`debug-traceback`](https://cibuildwheel.pypa.io/en/stable/options/#debug-traceback) | Print full traceback when errors occur. |
|
||||
| | [`build-verbosity`](https://cibuildwheel.pypa.io/en/stable/options/#build-verbosity) | Increase/decrease the output of the build |
|
||||
|
||||
These options can be specified in a pyproject.toml file, as well; see [configuration](https://cibuildwheel.readthedocs.io/en/stable/options/#configuration).
|
||||
|
||||
<!--[[[end]]] (sum: FxE3nIgFiY) -->
|
||||
|
||||
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
|
||||
----------------
|
||||
@@ -162,27 +179,26 @@ Here are some repos that use cibuildwheel.
|
||||
|-----------------------------------|----|----|:------|
|
||||
| [scikit-learn][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The machine learning library. A complex but clean config using many of cibuildwheel's features to build a large project with Cython and C++ extensions. |
|
||||
| [pytorch-fairseq][] | ![github icon][] | ![apple icon][] ![linux icon][] | Facebook AI Research Sequence-to-Sequence Toolkit written in Python. |
|
||||
| [duckdb][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | DuckDB is an analytical in-process SQL database management system |
|
||||
| [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. |
|
||||
| [Matplotlib][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The venerable Matplotlib, a Python library with C++ portions |
|
||||
| [NCNN][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | ncnn is a high-performance neural network inference framework optimized for the mobile platform |
|
||||
| [Prophet][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Tool for producing high quality forecasts for time series data that has multiple seasonality with linear or non-linear growth. |
|
||||
| [Matplotlib][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The venerable Matplotlib, a Python library with C++ portions |
|
||||
| [MyPy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | The compiled version of MyPy using MyPyC. |
|
||||
| [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 |
|
||||
| [duckdb][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | DuckDB is an in-process SQL OLAP Database Management System |
|
||||
|
||||
[scikit-learn]: https://github.com/scikit-learn/scikit-learn
|
||||
[pytorch-fairseq]: https://github.com/pytorch/fairseq
|
||||
[pytorch-fairseq]: https://github.com/facebookresearch/fairseq
|
||||
[duckdb]: https://github.com/duckdb/duckdb
|
||||
[NumPy]: https://github.com/numpy/numpy
|
||||
[Tornado]: https://github.com/tornadoweb/tornado
|
||||
[Matplotlib]: https://github.com/matplotlib/matplotlib
|
||||
[NCNN]: https://github.com/Tencent/ncnn
|
||||
[Prophet]: https://github.com/facebook/prophet
|
||||
[Matplotlib]: https://github.com/matplotlib/matplotlib
|
||||
[MyPy]: https://github.com/mypyc/mypy_mypyc-wheels
|
||||
[Prophet]: https://github.com/facebook/prophet
|
||||
[Kivy]: https://github.com/kivy/kivy
|
||||
[duckdb]: https://github.com/duckdb/duckdb
|
||||
|
||||
[appveyor icon]: docs/data/readme_icons/appveyor.svg
|
||||
[github icon]: docs/data/readme_icons/github.svg
|
||||
[azurepipelines icon]: docs/data/readme_icons/azurepipelines.svg
|
||||
[circleci icon]: docs/data/readme_icons/circleci.svg
|
||||
@@ -195,7 +211,7 @@ Here are some repos that use cibuildwheel.
|
||||
|
||||
<!-- END bin/projects.py -->
|
||||
|
||||
> ℹ️ That's just a handful, there are many more! Check out the [Working Examples](https://cibuildwheel.readthedocs.io/en/stable/working-examples) page in the docs.
|
||||
> ℹ️ That's just a handful, there are many more! Check out the [Working Examples](https://cibuildwheel.pypa.io/en/stable/working-examples) page in the docs.
|
||||
|
||||
Legal note
|
||||
----------
|
||||
@@ -209,75 +225,102 @@ 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.1.3
|
||||
|
||||
### v2.16.5
|
||||
_1 August 2025_
|
||||
|
||||
_30 January 2024_
|
||||
- 🐛 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)
|
||||
|
||||
- 🐛 Fix an incompatibility with the GitHub Action and new GitHub Runner images for Windows that bundle Powershell 7.3+ (#1741)
|
||||
- 🛠 Preliminary support for new `macos-14` arm64 runners (#1743)
|
||||
|
||||
### v2.16.4
|
||||
|
||||
_28 January 2024_
|
||||
### v3.1.2
|
||||
|
||||
- 🛠 Update manylinux pins to upgrade from a problematic PyPy version. (#1737)
|
||||
_29 July 2025_
|
||||
|
||||
### v2.16.3
|
||||
- ⚠️ 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)
|
||||
|
||||
_26 January 2024_
|
||||
|
||||
- 🐛 Fix a bug when building from sdist, where relative paths to files in the package didn't work because the working directory was wrong (#1687)
|
||||
- 🛠 Adds the ability to disable mounting the host filesystem in containers to `/host`, through the `disable_host_mount` suboption on [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.readthedocs.io/en/stable/options/#container-engine).
|
||||
- 📚 A lot of docs improvements! (#1708, #1705, #1686, #1679, #1667, #1665)
|
||||
### v3.1.1
|
||||
|
||||
### v2.16.2
|
||||
_24 July 2025_
|
||||
|
||||
_3 October 2023_
|
||||
- 🐛 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)
|
||||
|
||||
- 🛠 Updates CPython 3.12 version to 3.12.0, final release (#1635)
|
||||
- ✨ Adds a debug option [`CIBW_DEBUG_KEEP_CONTAINER`](https://cibuildwheel.readthedocs.io/en/stable/options/#cibw_debug_keep_container) to stop cibuildwheel deleting build containers after the build finishes. (#1620)
|
||||
- 📚 Adds support for `[tool.cibuildwheel]` checking by adding a schema compatible with the [validate-pyproject](https://github.com/abravalheri/validate-pyproject/) tool (#1622, #1628, #1629)
|
||||
- 🐛 Fix parsing of `CIBW_CONTAINER_ENGINE` and `CIBW_BUILD_FRONTEND` options to not break arguments on `:` characters (#1621)
|
||||
- 🐛 Fix the evaluation order of `CIBW_ENVIRONMENT` and `CIBW_ENVIRONMENT_PASS` so that `CIBW_ENVIRONMENT` assignments can reference environment variables passed through from the host machine. (#1617)
|
||||
- 🛠 Supports manylinux images' deferred installation of interpreters through the `manylinux-interpreters` tool (#1630)
|
||||
### v3.1.0
|
||||
|
||||
### v2.16.1
|
||||
_23 July 2025_
|
||||
|
||||
_26 September 2023_
|
||||
|
||||
- 🛠 Updates the prerelease CPython 3.12 version to 3.12.0rc3 (#1625)
|
||||
- 🛠 Only calls `linux32` in containers when necessary (#1599)
|
||||
- 🌟 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)
|
||||
|
||||
<!-- END bin/update_readme_changelog.py -->
|
||||
|
||||
|
||||
### 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)
|
||||
|
||||
<!-- [[[end]]] (sum: Fe+mD8YR4+) -->
|
||||
|
||||
---
|
||||
|
||||
That's the last few versions.
|
||||
|
||||
ℹ️ **Want more changelog? Head over to [the changelog page in the docs](https://cibuildwheel.readthedocs.io/en/stable/changelog/).**
|
||||
ℹ️ **Want more changelog? Head over to [the changelog page in the docs](https://cibuildwheel.pypa.io/en/stable/changelog/).**
|
||||
|
||||
---
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
For more info on how to contribute to cibuildwheel, see the [docs](https://cibuildwheel.readthedocs.io/en/latest/contributing/).
|
||||
For more info on how to contribute to cibuildwheel, see the [docs](https://cibuildwheel.pypa.io/en/latest/contributing/).
|
||||
|
||||
Everyone interacting with the cibuildwheel project via codebase, issue tracker, chat rooms, or otherwise is expected to follow the [PSF Code of Conduct](https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md).
|
||||
|
||||
Maintainers
|
||||
-----------
|
||||
|
||||
Core:
|
||||
|
||||
- Joe Rickerby [@joerick](https://github.com/joerick)
|
||||
- Yannick Jadoul [@YannickJadoul](https://github.com/YannickJadoul)
|
||||
- Matthieu Darbois [@mayeut](https://github.com/mayeut)
|
||||
- Henry Schreiner [@henryiii](https://github.com/henryiii)
|
||||
- Grzegorz Bokota [@Czaki](https://github.com/Czaki)
|
||||
|
||||
Platform maintainers:
|
||||
|
||||
- Russell Keith-Magee [@freakboy3742](https://github.com/freakboy3742) (iOS)
|
||||
- Agriya Khetarpal [@agriyakhetarpal](https://github.com/agriyakhetarpal) (Pyodide)
|
||||
- Hood Chatham [@hoodmane](https://github.com/hoodmane) (Pyodide)
|
||||
- Gyeongjae Choi [@ryanking13](https://github.com/ryanking13) (Pyodide)
|
||||
- Tim Felgentreff [@timfel](https://github.com/timfel) (GraalPy)
|
||||
- Malcolm Smith [@mhsmith](https://github.com/mhsmith) (Android)
|
||||
|
||||
Credits
|
||||
-------
|
||||
|
||||
@@ -300,4 +343,4 @@ See also
|
||||
|
||||
Another very similar tool to consider is [matthew-brett/multibuild](http://github.com/matthew-brett/multibuild). `multibuild` is a shell script toolbox for building a wheel on various platforms. It is used as a basis to build some of the big data science tools, like SciPy.
|
||||
|
||||
If you are building Rust wheels, you can get by without some of the tricks required to make GLIBC work via manylinux; this is especially relevant for cross-compiling, which is easy with Rust. See [maturin-action](https://github.com/messense/maturin-action) for a tool that is optimized for building Rust wheels and cross-compiling.
|
||||
If you are building Rust wheels, you can get by without some of the tricks required to make GLIBC work via manylinux; this is especially relevant for cross-compiling, which is easy with Rust. See [maturin-action](https://github.com/PyO3/maturin-action) for a tool that is optimized for building Rust wheels and cross-compiling.
|
||||
|
||||
+46
-23
@@ -24,45 +24,68 @@ branding:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# Set up a non-EOL, cibuildwheel & pipx supported Python version
|
||||
- uses: actions/setup-python@v5
|
||||
id: python
|
||||
with:
|
||||
python-version: "3.8 - 3.12"
|
||||
python-version: "3.11 - 3.13"
|
||||
update-environment: false
|
||||
|
||||
# macos-14 (M1) may be missing pipx (due to it not having CPython)
|
||||
- run: |
|
||||
"${{ steps.python.outputs.python-path }}" -m pip install pipx
|
||||
- id: cibw
|
||||
run: |
|
||||
# Install cibuildwheel
|
||||
"${{ steps.python.outputs.python-path }}" -u << "EOF"
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import venv
|
||||
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
|
||||
|
||||
class EnvBuilder(venv.EnvBuilder):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def setup_scripts(self, context):
|
||||
pass
|
||||
|
||||
def post_setup(self, context):
|
||||
super().post_setup(context)
|
||||
self.bin_path = Path(context.env_exe).parent
|
||||
run([sys.executable, "-m", "pip", "--python", context.env_exe, "install", r"${{ github.action_path }}"], check=True)
|
||||
|
||||
|
||||
print("::group::Install cibuildwheel")
|
||||
venv_path = Path(r"${{ runner.temp }}") / "cibw"
|
||||
if venv_path.exists():
|
||||
shutil.rmtree(venv_path)
|
||||
builder = EnvBuilder()
|
||||
builder.create(venv_path)
|
||||
cibw_path = [path for path in builder.bin_path.glob("cibuildwheel*") if path.stem == "cibuildwheel"][0]
|
||||
with open(os.environ["GITHUB_OUTPUT"], "at") as f:
|
||||
f.write(f"cibw-path={cibw_path}\n")
|
||||
print("::endgroup::")
|
||||
EOF
|
||||
shell: bash
|
||||
|
||||
# Redirecting stderr to stdout to fix interleaving issue in Actions.
|
||||
- run: >
|
||||
"${{ steps.python.outputs.python-path }}" -m pipx run
|
||||
--spec '${{ github.action_path }}'
|
||||
cibuildwheel
|
||||
"${{ steps.cibw.outputs.cibw-path }}"
|
||||
"${{ inputs.package-dir }}"
|
||||
--output-dir "${{ inputs.output-dir }}"
|
||||
--config-file "${{ inputs.config-file }}"
|
||||
--only "${{ inputs.only }}"
|
||||
${{ 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
|
||||
shell: bash
|
||||
if: runner.os != 'Windows'
|
||||
|
||||
# Windows needs powershell to interact nicely with Meson
|
||||
# $PSNativeCommandArgumentPassing was introduced in pwsh 7.3 and the
|
||||
# legacy behaviour is needed for backwards compatibility with how this
|
||||
# was called in the past.
|
||||
- run: >
|
||||
if ($PSNativeCommandArgumentPassing) {
|
||||
$PSNativeCommandArgumentPassing = 'Legacy'
|
||||
};
|
||||
& "${{ steps.python.outputs.python-path }}" -m pipx run
|
||||
--spec "${{ github.action_path }}"
|
||||
cibuildwheel
|
||||
& "${{ steps.cibw.outputs.cibw-path }}"
|
||||
"${{ inputs.package-dir }}"
|
||||
--output-dir '"${{ inputs.output-dir }}"'
|
||||
--config-file '"${{ inputs.config-file }}"'
|
||||
--only '"${{ inputs.only }}"'
|
||||
${{ 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) || ''}}
|
||||
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/*
|
||||
+58
-19
@@ -1,38 +1,77 @@
|
||||
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
|
||||
- job: linux_311
|
||||
timeoutInMinutes: 180
|
||||
pool: {vmImage: 'ubuntu-latest'}
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.11'
|
||||
- task: JavaToolInstaller@0
|
||||
inputs:
|
||||
versionSpec: '17'
|
||||
jdkArchitectureOption: 'x64'
|
||||
jdkSourceOption: 'PreInstalled'
|
||||
- bash: |
|
||||
docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
|
||||
python -m pip install -U pip
|
||||
python -m pip install -e. --group test
|
||||
if [ "$(Build.SourceBranch)" = "refs/heads/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 ($(Build.SourceBranch))."
|
||||
fi
|
||||
python ./bin/run_tests.py
|
||||
|
||||
- job: macos_311
|
||||
pool: {vmImage: 'macOS-latest'}
|
||||
timeoutInMinutes: 120
|
||||
pool: {vmImage: 'Ubuntu-20.04'}
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.11'
|
||||
- bash: |
|
||||
python -m pip install -e ".[dev]"
|
||||
python -m pip install -U pip
|
||||
python -m pip install -e. --group test
|
||||
if [ "$(Build.SourceBranch)" = "refs/heads/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 ($(Build.SourceBranch))."
|
||||
fi
|
||||
python ./bin/run_tests.py
|
||||
|
||||
- job: macos_38
|
||||
pool: {vmImage: 'macOS-11'}
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- bash: |
|
||||
python -m pip install -e ".[dev]"
|
||||
python ./bin/run_tests.py
|
||||
|
||||
- job: windows_38
|
||||
pool: {vmImage: 'windows-2019'}
|
||||
- job: windows_311
|
||||
pool: {vmImage: 'windows-latest'}
|
||||
timeoutInMinutes: 180
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.11'
|
||||
- bash: |
|
||||
python -m pip install -e ".[dev]"
|
||||
python -m pip install -U pip
|
||||
python -m pip install -e. --group test
|
||||
if [ "$(Build.SourceBranch)" = "refs/heads/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 ($(Build.SourceBranch))."
|
||||
fi
|
||||
python ./bin/run_tests.py
|
||||
|
||||
+11
-11
@@ -1,30 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# /// script
|
||||
# dependencies = ["click", "packaging"]
|
||||
# ///
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from packaging.version import InvalidVersion, Version
|
||||
|
||||
import cibuildwheel
|
||||
|
||||
config = [
|
||||
# file path, version find/replace format
|
||||
("pyproject.toml", 'version = "{}"'),
|
||||
("README.md", "cibuildwheel=={}"),
|
||||
("cibuildwheel/__init__.py", '__version__ = "{}"'),
|
||||
("docs/faq.md", "cibuildwheel=={}"),
|
||||
("docs/faq.md", "cibuildwheel@v{}"),
|
||||
("docs/setup.md", "cibuildwheel=={}"),
|
||||
("examples/*", "cibuildwheel=={}"),
|
||||
("examples/*", "cibuildwheel@v{}"),
|
||||
("setup.cfg", "version = {}"),
|
||||
]
|
||||
|
||||
RED = "\u001b[31m"
|
||||
@@ -34,7 +33,8 @@ OFF = "\u001b[0m"
|
||||
|
||||
@click.command()
|
||||
def bump_version() -> None:
|
||||
current_version = cibuildwheel.__version__
|
||||
with open("pyproject.toml", "rb") as f:
|
||||
current_version = tomllib.load(f)["project"]["version"]
|
||||
|
||||
try:
|
||||
commit_date_str = subprocess.run(
|
||||
@@ -82,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")
|
||||
@@ -129,8 +129,8 @@ def bump_version() -> None:
|
||||
contents = contents.replace(find, replace)
|
||||
path.write_text(contents, encoding="utf8")
|
||||
|
||||
print("Files updated. If you want to update 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":
|
||||
@@ -181,7 +181,7 @@ def bump_version() -> None:
|
||||
print()
|
||||
|
||||
print("Push the new version to GitHub with:")
|
||||
print(" git push && git push --tags")
|
||||
print(f" git push && git push origin v{new_version}")
|
||||
print()
|
||||
|
||||
release_url = "https://github.com/pypa/cibuildwheel/releases/new?" + urllib.parse.urlencode(
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
CIBW_PLATFORM=linux pytest "$@"
|
||||
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()
|
||||
+119
-43
@@ -1,19 +1,46 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env -S uv run -q
|
||||
|
||||
# /// script
|
||||
# dependencies = ["pyyaml"]
|
||||
# ///
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import functools
|
||||
import json
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
make_parser = functools.partial(argparse.ArgumentParser, allow_abbrev=False)
|
||||
if sys.version_info >= (3, 14):
|
||||
make_parser = functools.partial(make_parser, color=True, suggest_on_error=True)
|
||||
parser = make_parser()
|
||||
parser.add_argument("--schemastore", action="store_true", help="Generate schema_store version")
|
||||
args = parser.parse_args()
|
||||
|
||||
starter = """
|
||||
$schema: http://json-schema.org/draft-07/schema#
|
||||
$id: https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json
|
||||
$schema: http://json-schema.org/draft-07/schema
|
||||
$defs:
|
||||
inherit:
|
||||
enum:
|
||||
- none
|
||||
- prepend
|
||||
- append
|
||||
default: none
|
||||
description: How to inherit the parent's value.
|
||||
enable:
|
||||
enum:
|
||||
- cpython-experimental-riscv64
|
||||
- cpython-freethreading
|
||||
- cpython-prerelease
|
||||
- graalpy
|
||||
- pyodide-prerelease
|
||||
- pypy
|
||||
- pypy-eol
|
||||
description: A Python version or flavor to enable.
|
||||
additionalProperties: false
|
||||
description: cibuildwheel's settings.
|
||||
type: object
|
||||
@@ -36,19 +63,21 @@ properties:
|
||||
type: string_array
|
||||
build-frontend:
|
||||
default: default
|
||||
description: Set the tool to use to build, either "pip" (default for now) or "build"
|
||||
description: Set the tool to use to build, either "build" (default), "build[uv]", or "pip"
|
||||
oneOf:
|
||||
- enum: [pip, build, default]
|
||||
- enum: [pip, build, "build[uv]", default]
|
||||
- type: string
|
||||
pattern: '^pip; ?args:'
|
||||
- type: string
|
||||
pattern: '^build; ?args:'
|
||||
- type: string
|
||||
pattern: '^build\\[uv\\]; ?args:'
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required: [name]
|
||||
properties:
|
||||
name:
|
||||
enum: [pip, build]
|
||||
enum: [pip, build, "build[uv]"]
|
||||
args:
|
||||
type: array
|
||||
items:
|
||||
@@ -84,7 +113,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
|
||||
@@ -95,6 +148,9 @@ properties:
|
||||
manylinux-aarch64-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
manylinux-armv7l-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
manylinux-i686-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
@@ -110,6 +166,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
|
||||
@@ -119,21 +178,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
|
||||
@@ -143,19 +214,25 @@ 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
|
||||
"""
|
||||
|
||||
schema = yaml.safe_load(starter)
|
||||
|
||||
if args.schemastore:
|
||||
schema["$id"] += "#"
|
||||
|
||||
string_array = yaml.safe_load(
|
||||
"""
|
||||
- type: string
|
||||
@@ -214,23 +291,35 @@ items:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
select: {}
|
||||
inherit:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
before-all: {"$ref": "#/$defs/inherit"}
|
||||
before-build: {"$ref": "#/$defs/inherit"}
|
||||
xbuild-tools: {"$ref": "#/$defs/inherit"}
|
||||
before-test: {"$ref": "#/$defs/inherit"}
|
||||
config-settings: {"$ref": "#/$defs/inherit"}
|
||||
container-engine: {"$ref": "#/$defs/inherit"}
|
||||
environment: {"$ref": "#/$defs/inherit"}
|
||||
environment-pass: {"$ref": "#/$defs/inherit"}
|
||||
repair-wheel-command: {"$ref": "#/$defs/inherit"}
|
||||
test-command: {"$ref": "#/$defs/inherit"}
|
||||
test-extras: {"$ref": "#/$defs/inherit"}
|
||||
test-sources: {"$ref": "#/$defs/inherit"}
|
||||
test-requires: {"$ref": "#/$defs/inherit"}
|
||||
test-environment: {"$ref": "#/$defs/inherit"}
|
||||
"""
|
||||
)
|
||||
|
||||
for key, value in schema["properties"].items():
|
||||
value["title"] = f'CIBW_{key.replace("-", "_").upper()}'
|
||||
value["title"] = f"CIBW_{key.replace('-', '_').upper()}"
|
||||
|
||||
if args.schemastore:
|
||||
non_global_options = {
|
||||
k: {"$ref": f"#/properties/tool/properties/cibuildwheel/properties/{k}"}
|
||||
for k in schema["properties"]
|
||||
}
|
||||
else:
|
||||
non_global_options = {k: {"$ref": f"#/properties/{k}"} for k in schema["properties"]}
|
||||
non_global_options = {k: {"$ref": f"#/properties/{k}"} for k in schema["properties"]}
|
||||
del non_global_options["build"]
|
||||
del non_global_options["skip"]
|
||||
del non_global_options["container-engine"]
|
||||
del non_global_options["test-skip"]
|
||||
del non_global_options["enable"]
|
||||
|
||||
overrides["items"]["properties"]["select"]["oneOf"] = string_array
|
||||
overrides["items"]["properties"] |= non_global_options.copy()
|
||||
@@ -240,6 +329,7 @@ del overrides["items"]["properties"]["archs"]
|
||||
not_linux = non_global_options.copy()
|
||||
|
||||
del not_linux["environment-pass"]
|
||||
del not_linux["container-engine"]
|
||||
for key in list(not_linux):
|
||||
if "linux-" in key:
|
||||
del not_linux[key]
|
||||
@@ -257,6 +347,9 @@ oses = {
|
||||
"linux": as_object(non_global_options),
|
||||
"windows": as_object(not_linux),
|
||||
"macos": as_object(not_linux),
|
||||
"pyodide": as_object(not_linux),
|
||||
"android": as_object(not_linux),
|
||||
"ios": as_object(not_linux),
|
||||
}
|
||||
|
||||
oses["linux"]["properties"]["repair-wheel-command"] = {
|
||||
@@ -273,27 +366,10 @@ del oses["linux"]["properties"]["dependency-versions"]
|
||||
schema["properties"]["overrides"] = overrides
|
||||
schema["properties"] |= oses
|
||||
|
||||
if not args.schemastore:
|
||||
print(json.dumps(schema, indent=2))
|
||||
raise SystemExit(0)
|
||||
if args.schemastore:
|
||||
schema["$id"] = "https://json.schemastore.org/partial-cibuildwheel.json"
|
||||
schema["description"] = (
|
||||
"cibuildwheel's settings. Generated with ./bin/generate_schema.py --schemastore from cibuildwheel."
|
||||
)
|
||||
|
||||
schema_store_txt = """
|
||||
$id: https://json.schemastore.org/cibuildwheel.json
|
||||
$schema: http://json-schema.org/draft-07/schema#
|
||||
additionalProperties: false
|
||||
description: cibuildwheel's toml file, generated with ./bin/generate_schema.py --schemastore from cibuildwheel.
|
||||
type: object
|
||||
properties:
|
||||
tool:
|
||||
type: object
|
||||
properties:
|
||||
cibuildwheel:
|
||||
type: object
|
||||
"""
|
||||
schema_store = yaml.safe_load(schema_store_txt)
|
||||
|
||||
schema_store["properties"]["tool"]["properties"]["cibuildwheel"]["properties"] = schema[
|
||||
"properties"
|
||||
]
|
||||
|
||||
print(json.dumps(schema_store, indent=2))
|
||||
print(json.dumps(schema, indent=2))
|
||||
|
||||
@@ -11,9 +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",
|
||||
|
||||
+18
-10
@@ -1,5 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "click",
|
||||
# "pyyaml",
|
||||
# "pygithub",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
"""
|
||||
Convert a yaml project list into a nice table.
|
||||
|
||||
@@ -9,8 +17,6 @@ Suggested usage:
|
||||
git diff
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
import functools
|
||||
import textwrap
|
||||
@@ -20,14 +26,13 @@ from collections.abc import Iterable, Mapping, Sequence
|
||||
from datetime import 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",
|
||||
@@ -77,7 +82,7 @@ class Project:
|
||||
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:
|
||||
@@ -87,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
|
||||
@@ -130,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")
|
||||
@@ -164,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)
|
||||
|
||||
@@ -195,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)
|
||||
|
||||
@@ -209,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,34 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
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.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)
|
||||
@@ -41,11 +41,6 @@ class CIService(typing.NamedTuple):
|
||||
|
||||
|
||||
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",
|
||||
@@ -79,9 +74,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 +92,16 @@ 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")
|
||||
|
||||
# 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 +122,22 @@ 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)
|
||||
shutil.copyfile(config_file, dst_config_file)
|
||||
|
||||
run(["git", "add", example_project], check=True)
|
||||
subprocess.run(["git", "add", example_project], check=True)
|
||||
message = textwrap.dedent(
|
||||
f"""\
|
||||
Test example minimal 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 +167,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)
|
||||
|
||||
+51
-6
@@ -1,18 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
if __name__ == "__main__":
|
||||
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
|
||||
|
||||
make_parser = functools.partial(argparse.ArgumentParser, allow_abbrev=False)
|
||||
if sys.version_info >= (3, 14):
|
||||
make_parser = functools.partial(make_parser, color=True, suggest_on_error=True)
|
||||
parser = make_parser()
|
||||
parser.add_argument(
|
||||
"--run-podman", action="store_true", default=False, help="run podman tests (linux only)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-processes",
|
||||
action="store",
|
||||
default=default_cpu_count,
|
||||
help="number of processes to use for testing",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# move cwd to the project root
|
||||
@@ -21,29 +35,60 @@ if __name__ == "__main__":
|
||||
# unit tests
|
||||
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":
|
||||
# run the docker unit tests only on Linux
|
||||
unit_test_args += ["--run-docker"]
|
||||
|
||||
if args.run_podman:
|
||||
unit_test_args += ["--run-podman"]
|
||||
|
||||
print(
|
||||
"\n\n================================== UNIT TESTS ==================================",
|
||||
flush=True,
|
||||
)
|
||||
subprocess.run(unit_test_args, check=True)
|
||||
|
||||
# integration tests
|
||||
integration_test_args = [
|
||||
# Run the serial integration tests without multiple processes
|
||||
serial_integration_test_args = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pytest",
|
||||
"--numprocesses=2",
|
||||
"-m",
|
||||
"serial",
|
||||
"-x",
|
||||
"--durations",
|
||||
"0",
|
||||
"--timeout=2400",
|
||||
"test",
|
||||
"-vv",
|
||||
]
|
||||
print(
|
||||
"\n\n=========================== SERIAL INTEGRATION TESTS ===========================",
|
||||
flush=True,
|
||||
)
|
||||
subprocess.run(serial_integration_test_args, check=True)
|
||||
|
||||
# Non-serial integration tests
|
||||
integration_test_args = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pytest",
|
||||
"-m",
|
||||
"not serial",
|
||||
f"--numprocesses={args.num_processes}",
|
||||
"-x",
|
||||
"--durations",
|
||||
"0",
|
||||
"--timeout=2400",
|
||||
"test",
|
||||
"-vv",
|
||||
]
|
||||
|
||||
if sys.platform.startswith("linux") and args.run_podman:
|
||||
integration_test_args += ["--run-podman"]
|
||||
|
||||
print(
|
||||
"\n\n========================= NON-SERIAL INTEGRATION TESTS =========================",
|
||||
flush=True,
|
||||
)
|
||||
subprocess.run(integration_test_args, check=True)
|
||||
|
||||
+5
-2
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -13,7 +13,10 @@ if __name__ == "__main__":
|
||||
# move cwd to the project root
|
||||
os.chdir(Path(__file__).resolve().parents[1])
|
||||
|
||||
parser = argparse.ArgumentParser(description="Runs a sample build")
|
||||
make_parser = functools.partial(argparse.ArgumentParser, allow_abbrev=False)
|
||||
if sys.version_info >= (3, 14):
|
||||
make_parser = functools.partial(make_parser, color=True, suggest_on_error=True)
|
||||
parser = make_parser(description="Runs a sample build")
|
||||
parser.add_argument("project_python_path", nargs="?", default="test.test_0_basic.basic_project")
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
+81
-51
@@ -1,70 +1,82 @@
|
||||
#!/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_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 +102,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 +132,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)
|
||||
|
||||
|
||||
Executable
+146
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
import dataclasses
|
||||
import difflib
|
||||
import logging
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import click
|
||||
import packaging.specifiers
|
||||
import requests
|
||||
import rich
|
||||
from packaging.version import InvalidVersion, Version
|
||||
from rich.logging import RichHandler
|
||||
from rich.syntax import Syntax
|
||||
|
||||
log = logging.getLogger("cibw")
|
||||
|
||||
# Looking up the dir instead of using utils.resources_dir
|
||||
# since we want to write to it.
|
||||
DIR: Final[Path] = Path(__file__).parent.parent.resolve()
|
||||
RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"
|
||||
|
||||
NODEJS_DIST: Final[str] = "https://nodejs.org/dist/"
|
||||
NODEJS_INDEX: Final[str] = f"{NODEJS_DIST}index.json"
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, order=True)
|
||||
class VersionTuple:
|
||||
version: Version
|
||||
version_string: str
|
||||
|
||||
|
||||
def parse_nodejs_index() -> list[VersionTuple]:
|
||||
versions: list[VersionTuple] = []
|
||||
response = requests.get(NODEJS_INDEX)
|
||||
response.raise_for_status()
|
||||
versions_info = response.json()
|
||||
for version_info in versions_info:
|
||||
version_string = version_info.get("version", "???")
|
||||
if not version_info.get("lts", False):
|
||||
log.debug("Ignoring non LTS release %r", version_string)
|
||||
continue
|
||||
if "linux-x64" not in version_info.get("files", []):
|
||||
log.warning(
|
||||
"Ignoring release %r which does not include a linux-x64 binary", version_string
|
||||
)
|
||||
continue
|
||||
try:
|
||||
version = Version(version_string)
|
||||
if version.is_devrelease:
|
||||
log.info("Ignoring development release %r", str(version))
|
||||
continue
|
||||
if version.is_prerelease:
|
||||
log.info("Ignoring pre-release %r", str(version))
|
||||
continue
|
||||
versions.append(VersionTuple(version, version_string))
|
||||
except InvalidVersion:
|
||||
log.warning("Ignoring release %r", version_string)
|
||||
versions.sort(reverse=True)
|
||||
return versions
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--force", is_flag=True)
|
||||
@click.option(
|
||||
"--level", default="INFO", type=click.Choice(["WARNING", "INFO", "DEBUG"], case_sensitive=False)
|
||||
)
|
||||
def update_nodejs(force: bool, level: str) -> None:
|
||||
logging.basicConfig(
|
||||
level="INFO",
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[RichHandler(rich_tracebacks=True, markup=True)],
|
||||
)
|
||||
log.setLevel(level)
|
||||
|
||||
toml_file_path = RESOURCES_DIR / "nodejs.toml"
|
||||
|
||||
original_toml = toml_file_path.read_text()
|
||||
with toml_file_path.open("rb") as f:
|
||||
nodejs_data = tomllib.load(f)
|
||||
|
||||
nodejs_data.pop("url")
|
||||
|
||||
major_versions = [VersionTuple(Version(key), key) for key in nodejs_data]
|
||||
major_versions.sort(reverse=True)
|
||||
|
||||
versions = parse_nodejs_index()
|
||||
|
||||
# update existing versions, 1 per LTS
|
||||
for major_version in major_versions:
|
||||
current = Version(nodejs_data[major_version.version_string])
|
||||
specifier = packaging.specifiers.SpecifierSet(
|
||||
specifiers=f"=={major_version.version.major}.*"
|
||||
)
|
||||
for version in versions:
|
||||
if specifier.contains(version.version) and version.version > current:
|
||||
nodejs_data[major_version.version_string] = version.version_string
|
||||
break
|
||||
|
||||
# check for a new major LTS to insert
|
||||
if versions and versions[0].version.major > major_versions[0].version.major:
|
||||
major_versions.insert(
|
||||
0,
|
||||
VersionTuple(Version(str(versions[0].version.major)), f"v{versions[0].version.major}"),
|
||||
)
|
||||
nodejs_data[major_versions[0].version_string] = versions[0].version_string
|
||||
|
||||
versions_toml = "\n".join(
|
||||
f'{major_version.version_string} = "{nodejs_data[major_version.version_string]}"'
|
||||
for major_version in major_versions
|
||||
)
|
||||
result_toml = f'url = "{NODEJS_DIST}"\n{versions_toml}\n'
|
||||
|
||||
rich.print() # spacer
|
||||
|
||||
if original_toml == result_toml:
|
||||
rich.print("[green]Check complete, nodejs version unchanged.")
|
||||
return
|
||||
|
||||
rich.print("nodejs version updated.")
|
||||
rich.print("Changes:")
|
||||
rich.print()
|
||||
|
||||
toml_relpath = toml_file_path.relative_to(DIR).as_posix()
|
||||
diff_lines = difflib.unified_diff(
|
||||
original_toml.splitlines(keepends=True),
|
||||
result_toml.splitlines(keepends=True),
|
||||
fromfile=toml_relpath,
|
||||
tofile=toml_relpath,
|
||||
)
|
||||
rich.print(Syntax("".join(diff_lines), "diff", theme="ansi_light"))
|
||||
rich.print()
|
||||
|
||||
if force:
|
||||
toml_file_path.write_text(result_toml)
|
||||
rich.print("[green]TOML file updated.")
|
||||
else:
|
||||
rich.print("[yellow]File left unchanged. Use --force flag to update.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_nodejs()
|
||||
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()
|
||||
+261
-27
@@ -1,13 +1,16 @@
|
||||
#!/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 +20,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")
|
||||
|
||||
@@ -44,13 +47,32 @@ class ConfigWinPP(TypedDict):
|
||||
url: str
|
||||
|
||||
|
||||
class ConfigMacOS(TypedDict):
|
||||
class ConfigWinGP(TypedDict):
|
||||
identifier: str
|
||||
version: str
|
||||
url: str
|
||||
|
||||
|
||||
AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigMacOS]
|
||||
class ConfigApple(TypedDict):
|
||||
identifier: str
|
||||
version: str
|
||||
url: str
|
||||
|
||||
|
||||
class ConfigAndroid(TypedDict):
|
||||
identifier: str
|
||||
version: str
|
||||
url: str
|
||||
|
||||
|
||||
class ConfigPyodide(TypedDict):
|
||||
identifier: str
|
||||
version: str
|
||||
default_pyodide_version: str
|
||||
node_version: str
|
||||
|
||||
|
||||
AnyConfig = ConfigWinCP | ConfigWinPP | ConfigWinGP | ConfigApple | ConfigAndroid | ConfigPyodide
|
||||
|
||||
|
||||
# The following set of "Versions" classes allow the initial call to the APIs to
|
||||
@@ -58,7 +80,7 @@ AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigMacOS]
|
||||
|
||||
|
||||
class WindowsVersions:
|
||||
def __init__(self, arch_str: ArchStr) -> None:
|
||||
def __init__(self, arch_str: ArchStr, free_threaded: bool) -> None:
|
||||
response = requests.get("https://api.nuget.org/v3/index.json")
|
||||
response.raise_for_status()
|
||||
api_info = response.json()
|
||||
@@ -72,7 +94,11 @@ class WindowsVersions:
|
||||
|
||||
self.arch_str = arch_str
|
||||
self.arch = ARCH_DICT[arch_str]
|
||||
self.free_threaded = free_threaded
|
||||
|
||||
package = PACKAGE_DICT[arch_str]
|
||||
if free_threaded:
|
||||
package = f"{package}-freethreaded"
|
||||
|
||||
response = requests.get(f"{endpoint}{package}/index.json")
|
||||
response.raise_for_status()
|
||||
@@ -92,8 +118,9 @@ class WindowsVersions:
|
||||
if not versions:
|
||||
return None
|
||||
|
||||
flags = "t" if self.free_threaded else ""
|
||||
version = versions[0]
|
||||
identifier = f"cp{version.major}{version.minor}-{self.arch}"
|
||||
identifier = f"cp{version.major}{version.minor}{flags}-{self.arch}"
|
||||
return ConfigWinCP(
|
||||
identifier=identifier,
|
||||
version=self.version_dict[version],
|
||||
@@ -101,6 +128,76 @@ class WindowsVersions:
|
||||
)
|
||||
|
||||
|
||||
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) -> AnyConfig:
|
||||
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]
|
||||
gpspec = Specifier("==24.2.*") if gpspec_str == "242" else 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"
|
||||
config = ConfigApple
|
||||
platform = "macos"
|
||||
elif "win" in identifier:
|
||||
arch = "aarch64" if "arm64" in identifier else "x86_64"
|
||||
config = ConfigWinGP
|
||||
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 config(
|
||||
identifier=identifier,
|
||||
version=f"{version.major}.{version.minor}",
|
||||
url=url,
|
||||
)
|
||||
|
||||
|
||||
class PyPyVersions:
|
||||
def __init__(self, arch_str: ArchStr):
|
||||
response = requests.get("https://downloads.python.org/pypy/versions.json")
|
||||
@@ -128,7 +225,7 @@ class PyPyVersions:
|
||||
|
||||
def update_version_windows(self, spec: Specifier) -> ConfigWinCP:
|
||||
releases = [r for r in self.releases if spec.contains(r["python_version"])]
|
||||
releases = sorted(releases, key=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:
|
||||
@@ -149,13 +246,13 @@ class PyPyVersions:
|
||||
url=url,
|
||||
)
|
||||
|
||||
def update_version_macos(self, spec: Specifier) -> ConfigMacOS:
|
||||
def update_version_macos(self, spec: Specifier) -> ConfigApple:
|
||||
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}!"
|
||||
@@ -173,7 +270,7 @@ class PyPyVersions:
|
||||
if "" in rf["platform"] == "darwin" and rf["arch"] == arch
|
||||
)
|
||||
|
||||
return ConfigMacOS(
|
||||
return ConfigApple(
|
||||
identifier=identifier,
|
||||
version=f"{version.major}.{version.minor}",
|
||||
url=url,
|
||||
@@ -191,15 +288,19 @@ 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:
|
||||
) -> ConfigApple | None:
|
||||
# see note above on Specifier.filter
|
||||
unsorted_versions = spec.filter(self.versions_dict)
|
||||
sorted_versions = sorted(unsorted_versions, reverse=True)
|
||||
@@ -218,7 +319,7 @@ class CPythonVersions:
|
||||
|
||||
urls = [rf["url"] for rf in file_info if file_ident in rf["url"]]
|
||||
if urls:
|
||||
return ConfigMacOS(
|
||||
return ConfigApple(
|
||||
identifier=identifier,
|
||||
version=f"{new_version.major}.{new_version.minor}",
|
||||
url=urls[0],
|
||||
@@ -227,21 +328,139 @@ class CPythonVersions:
|
||||
return None
|
||||
|
||||
|
||||
class AndroidVersions:
|
||||
# This should be replaced with official python.org downloads once they're available.
|
||||
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, version: Version, spec: Specifier
|
||||
) -> ConfigAndroid | 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 ConfigAndroid(
|
||||
identifier=identifier,
|
||||
version=str(version),
|
||||
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) -> ConfigApple | 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 ConfigApple(
|
||||
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.
|
||||
|
||||
|
||||
class AllVersions:
|
||||
def __init__(self) -> None:
|
||||
self.windows_32 = WindowsVersions("32")
|
||||
self.windows_64 = WindowsVersions("64")
|
||||
self.windows_arm64 = WindowsVersions("ARM64")
|
||||
self.windows_32 = WindowsVersions("32", False)
|
||||
self.windows_t_32 = WindowsVersions("32", True)
|
||||
self.windows_64 = WindowsVersions("64", False)
|
||||
self.windows_t_64 = WindowsVersions("64", True)
|
||||
self.windows_arm64 = WindowsVersions("ARM64", False)
|
||||
self.windows_t_arm64 = WindowsVersions("ARM64", True)
|
||||
self.windows_pypy_64 = PyPyVersions("64")
|
||||
|
||||
self.macos_cpython = CPythonVersions()
|
||||
self.macos_pypy = PyPyVersions("64")
|
||||
self.macos_pypy_arm64 = PyPyVersions("ARM64")
|
||||
|
||||
self.android = AndroidVersions()
|
||||
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"])
|
||||
@@ -259,16 +478,33 @@ class AllVersions:
|
||||
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 "win32" in identifier:
|
||||
if identifier.startswith("cp"):
|
||||
config_update = self.windows_32.update_version_windows(spec)
|
||||
elif identifier.startswith("gp"):
|
||||
config_update = self.graalpy.update_version(identifier, spec)
|
||||
elif "t-win32" in identifier and identifier.startswith("cp"):
|
||||
config_update = self.windows_t_32.update_version_windows(spec)
|
||||
elif "win32" in identifier and identifier.startswith("cp"):
|
||||
config_update = self.windows_32.update_version_windows(spec)
|
||||
elif "t-win_amd64" in identifier and identifier.startswith("cp"):
|
||||
config_update = self.windows_t_64.update_version_windows(spec)
|
||||
elif "win_amd64" in identifier:
|
||||
if identifier.startswith("cp"):
|
||||
config_update = self.windows_64.update_version_windows(spec)
|
||||
elif identifier.startswith("pp"):
|
||||
config_update = self.windows_pypy_64.update_version_windows(spec)
|
||||
elif identifier.startswith("gp"):
|
||||
config_update = self.graalpy.update_version(identifier, spec)
|
||||
elif "t-win_arm64" in identifier and identifier.startswith("cp"):
|
||||
config_update = self.windows_t_arm64.update_version_windows(spec)
|
||||
elif "win_arm64" in identifier and identifier.startswith("cp"):
|
||||
config_update = self.windows_arm64.update_version_windows(spec)
|
||||
elif "android" in identifier:
|
||||
config_update = self.android.update_version_android(identifier, version, 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)
|
||||
@@ -298,11 +534,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()
|
||||
+17
-18
@@ -1,11 +1,11 @@
|
||||
#!/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
|
||||
|
||||
@@ -15,8 +15,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
|
||||
@@ -25,18 +23,18 @@ DIR: Final[Path] = Path(__file__).parent.parent.resolve()
|
||||
RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"
|
||||
|
||||
GET_VIRTUALENV_GITHUB: Final[str] = "https://github.com/pypa/get-virtualenv"
|
||||
GET_VIRTUALENV_URL_TEMPLATE: Final[
|
||||
str
|
||||
] = f"{GET_VIRTUALENV_GITHUB}/blob/{{version}}/public/virtualenv.pyz?raw=true"
|
||||
GET_VIRTUALENV_URL_TEMPLATE: Final[str] = (
|
||||
f"{GET_VIRTUALENV_GITHUB}/blob/{{version}}/public/virtualenv.pyz?raw=true"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
@dataclasses.dataclass(frozen=True, order=True)
|
||||
class VersionTuple:
|
||||
version: Version
|
||||
version_string: str
|
||||
|
||||
|
||||
def git_ls_remote_versions(url) -> list[VersionTuple]:
|
||||
def git_ls_remote_versions(url: str) -> list[VersionTuple]:
|
||||
versions: list[VersionTuple] = []
|
||||
tags = subprocess.run(
|
||||
["git", "ls-remote", "--tags", url], check=True, text=True, capture_output=True
|
||||
@@ -53,10 +51,6 @@ def git_ls_remote_versions(url) -> list[VersionTuple]:
|
||||
if version.is_prerelease:
|
||||
log.info("Ignoring pre-release %r", str(version))
|
||||
continue
|
||||
# Do not upgrade past 20.22.0 to keep python 3.6 compat
|
||||
if version >= Version("20.22.0"):
|
||||
log.info("Ignoring %r which is not compatible with python 3.6", str(version))
|
||||
continue
|
||||
versions.append(VersionTuple(version, version_string))
|
||||
except InvalidVersion:
|
||||
log.warning("Ignoring ref %r", ref)
|
||||
@@ -82,15 +76,20 @@ def update_virtualenv(force: bool, level: str) -> None:
|
||||
|
||||
original_toml = toml_file_path.read_text()
|
||||
with toml_file_path.open("rb") as f:
|
||||
loaded_file = tomllib.load(f)
|
||||
version = str(loaded_file["version"])
|
||||
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
|
||||
|
||||
result_toml = (
|
||||
f'version = "{version}"\n'
|
||||
f'url = "{GET_VIRTUALENV_URL_TEMPLATE.format(version=version)}"\n'
|
||||
configurations["default"] = {
|
||||
"version": version,
|
||||
"url": GET_VIRTUALENV_URL_TEMPLATE.format(version=version),
|
||||
}
|
||||
result_toml = "".join(
|
||||
f'{key} = {{ version = "{value["version"]}", url = "{value["url"]}" }}\n'
|
||||
for key, value in configurations.items()
|
||||
)
|
||||
|
||||
rich.print() # spacer
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "2.16.5"
|
||||
__version__ = "3.1.3"
|
||||
|
||||
+251
-138
@@ -1,40 +1,89 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import functools
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
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.util
|
||||
import cibuildwheel.windows
|
||||
from cibuildwheel._compat.typing import assert_never
|
||||
from cibuildwheel import errors
|
||||
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,
|
||||
)
|
||||
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
|
||||
from cibuildwheel.util.helpers import strtobool
|
||||
from cibuildwheel.util.resources import read_all_configs
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GlobalOptions:
|
||||
print_traceback_on_error: bool = True # decides what happens when errors are hit.
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class FileReport:
|
||||
name: str
|
||||
size: str
|
||||
|
||||
|
||||
# Taken from https://stackoverflow.com/a/107717
|
||||
class Unbuffered:
|
||||
def __init__(self, stream: TextIO) -> None:
|
||||
self.stream = stream
|
||||
|
||||
def write(self, data: str) -> None:
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
|
||||
def writelines(self, data: Iterable[str]) -> None:
|
||||
self.stream.writelines(data)
|
||||
self.stream.flush()
|
||||
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
global_options = GlobalOptions()
|
||||
try:
|
||||
main_inner(global_options)
|
||||
except errors.FatalError as e:
|
||||
message = e.args[0]
|
||||
if log.step_active:
|
||||
log.step_end_with_error(message)
|
||||
else:
|
||||
log.error(message)
|
||||
|
||||
if global_options.print_traceback_on_error:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
|
||||
sys.exit(e.return_code)
|
||||
|
||||
|
||||
def main_inner(global_options: GlobalOptions) -> None:
|
||||
"""
|
||||
`main_inner` is the same as `main`, but it raises FatalError exceptions
|
||||
rather than exiting directly.
|
||||
"""
|
||||
|
||||
make_parser = functools.partial(argparse.ArgumentParser, allow_abbrev=False)
|
||||
if sys.version_info >= (3, 14):
|
||||
make_parser = functools.partial(make_parser, color=True, suggest_on_error=True)
|
||||
parser = make_parser(
|
||||
description="Build wheels for all the platforms.",
|
||||
epilog="""
|
||||
Most options are supplied via environment variables or in
|
||||
@@ -45,14 +94,14 @@ def main() -> None:
|
||||
|
||||
parser.add_argument(
|
||||
"--platform",
|
||||
choices=["auto", "linux", "macos", "windows"],
|
||||
choices=["auto", "linux", "macos", "windows", "pyodide", "android", "ios"],
|
||||
default=None,
|
||||
help="""
|
||||
Platform to build for. Use this option to override the
|
||||
auto-detected platform or to run cibuildwheel on your development
|
||||
machine. Specifying "macos" or "windows" only works on that
|
||||
operating system, but "linux" works on all three, as long as
|
||||
Docker/Podman is installed. Default: auto.
|
||||
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.
|
||||
""",
|
||||
)
|
||||
|
||||
@@ -70,9 +119,22 @@ def main() -> 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
|
||||
@@ -100,7 +162,7 @@ def main() -> None:
|
||||
parser.add_argument(
|
||||
"package_dir",
|
||||
metavar="PACKAGE",
|
||||
default=Path("."),
|
||||
default=Path(),
|
||||
type=Path,
|
||||
nargs="?",
|
||||
help="""
|
||||
@@ -127,13 +189,16 @@ def main() -> None:
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--prerelease-pythons",
|
||||
"--debug-traceback",
|
||||
action="store_true",
|
||||
help="Enable pre-release Python versions if available.",
|
||||
default=strtobool(os.environ.get("CIBW_DEBUG_TRACEBACK", "0")),
|
||||
help="Print a full traceback for all errors",
|
||||
)
|
||||
|
||||
args = CommandLineArguments(**vars(parser.parse_args()))
|
||||
|
||||
global_options.print_traceback_on_error = args.debug_traceback
|
||||
|
||||
args.package_dir = args.package_dir.resolve()
|
||||
|
||||
# This are always relative to the base directory, even in SDist builds
|
||||
@@ -160,7 +225,7 @@ def main() -> 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
|
||||
@@ -177,95 +242,44 @@ def _compute_platform_only(only: str) -> PlatformName:
|
||||
return "macos"
|
||||
if "win_" in only or "win32" in only:
|
||||
return "windows"
|
||||
print(
|
||||
f"Invalid --only='{only}', must be a build selector with a known platform",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def _compute_platform_ci() -> PlatformName:
|
||||
if detect_ci_provider() is None:
|
||||
print(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
cibuildwheel: Unable to detect platform. cibuildwheel should run on your CI server;
|
||||
Travis CI, AppVeyor, Azure Pipelines, GitHub Actions, CircleCI, Gitlab, and Cirrus CI
|
||||
are supported. You can run on your development machine or other CI providers
|
||||
using the --platform argument. Check --help output for more information.
|
||||
"""
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
if sys.platform.startswith("linux"):
|
||||
return "linux"
|
||||
elif sys.platform == "darwin":
|
||||
return "macos"
|
||||
elif sys.platform == "win32":
|
||||
return "windows"
|
||||
else:
|
||||
print(
|
||||
'cibuildwheel: Unable to detect platform from "sys.platform" in a CI environment. You can run '
|
||||
"cibuildwheel using the --platform argument. Check --help output for more information.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
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(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:
|
||||
print(
|
||||
"--platform cannot be specified with --only, it is computed from --only",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
msg = "--platform cannot be specified with --only, it is computed from --only"
|
||||
raise errors.ConfigurationError(msg)
|
||||
if args.only and args.archs is not None:
|
||||
print(
|
||||
"--arch cannot be specified with --only, it is computed from --only",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
msg = "--arch cannot be specified with --only, it is computed from --only"
|
||||
raise errors.ConfigurationError(msg)
|
||||
|
||||
if platform_option_value not in PLATFORMS | {"auto"}:
|
||||
print(f"cibuildwheel: Unsupported platform: {platform_option_value}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
msg = f"Unsupported platform: {platform_option_value}"
|
||||
raise errors.ConfigurationError(msg)
|
||||
|
||||
if args.only:
|
||||
return _compute_platform_only(args.only)
|
||||
elif platform_option_value != "auto":
|
||||
return typing.cast(PlatformName, platform_option_value)
|
||||
|
||||
return _compute_platform_ci()
|
||||
|
||||
|
||||
class PlatformModule(Protocol):
|
||||
# note that as per PEP544, the self argument is ignored when the protocol
|
||||
# is applied to a module
|
||||
def get_python_configurations(
|
||||
self, build_selector: BuildSelector, architectures: Set[Architecture]
|
||||
) -> Sequence[GenericPythonConfiguration]:
|
||||
...
|
||||
|
||||
def build(self, options: Options, tmp_path: Path) -> None:
|
||||
...
|
||||
|
||||
|
||||
# pylint: disable-next=inconsistent-return-statements
|
||||
def get_platform_module(platform: PlatformName) -> PlatformModule:
|
||||
if platform == "linux":
|
||||
return cibuildwheel.linux
|
||||
if platform == "windows":
|
||||
return cibuildwheel.windows
|
||||
if platform == "macos":
|
||||
return cibuildwheel.macos
|
||||
assert_never(platform)
|
||||
return native_platform()
|
||||
|
||||
|
||||
def build_in_directory(args: CommandLineArguments) -> None:
|
||||
platform: PlatformName = _compute_platform(args)
|
||||
if platform == "pyodide" and sys.platform == "win32":
|
||||
msg = "Building for pyodide is not supported on Windows"
|
||||
raise errors.ConfigurationError(msg)
|
||||
|
||||
options = compute_options(platform=platform, command_line_arguments=args, env=os.environ)
|
||||
|
||||
package_dir = options.globals.package_dir
|
||||
@@ -273,11 +287,10 @@ def build_in_directory(args: CommandLineArguments) -> None:
|
||||
|
||||
if not any(package_dir.joinpath(name).exists() for name in package_files):
|
||||
names = ", ".join(sorted(package_files, reverse=True))
|
||||
msg = f"cibuildwheel: Could not find any of {{{names}}} at root of package"
|
||||
print(msg, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
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,
|
||||
@@ -292,8 +305,10 @@ def build_in_directory(args: CommandLineArguments) -> None:
|
||||
# Add CIBUILDWHEEL environment variable
|
||||
os.environ["CIBUILDWHEEL"] = "1"
|
||||
|
||||
# Python is buffering by default when running on the CI platforms, giving problems interleaving subprocess call output with unflushed calls to 'print'
|
||||
sys.stdout = Unbuffered(sys.stdout) # type: ignore[assignment]
|
||||
# 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)
|
||||
|
||||
# create the cache dir before it gets printed & builds performed
|
||||
CIBW_CACHE_PATH.mkdir(parents=True, exist_ok=True)
|
||||
@@ -304,27 +319,22 @@ def build_in_directory(args: CommandLineArguments) -> None:
|
||||
options.check_for_invalid_configuration(identifiers)
|
||||
allowed_architectures_check(platform, options.globals.architectures)
|
||||
except ValueError as err:
|
||||
print("cibuildwheel:", *err.args, file=sys.stderr)
|
||||
sys.exit(4)
|
||||
raise errors.DeprecationError(*err.args) from err
|
||||
|
||||
if not identifiers:
|
||||
print(
|
||||
f"cibuildwheel: No build identifiers selected: {options.globals.build_selector}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if not args.allow_empty:
|
||||
sys.exit(3)
|
||||
message = f"No build identifiers selected: {options.globals.build_selector}"
|
||||
if options.globals.allow_empty:
|
||||
print(f"cibuildwheel: {message}", file=sys.stderr)
|
||||
else:
|
||||
raise errors.NothingToDoError(message)
|
||||
|
||||
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
|
||||
@@ -357,36 +367,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",
|
||||
)
|
||||
+144
-64
@@ -1,79 +1,116 @@
|
||||
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]] = {
|
||||
"linux": "Linux",
|
||||
"macos": "macOS",
|
||||
"windows": "Windows",
|
||||
"pyodide": "Pyodide",
|
||||
"android": "Android",
|
||||
"ios": "iOS",
|
||||
}
|
||||
|
||||
ARCH_SYNONYMS: Final[list[dict[PlatformName, str | None]]] = [
|
||||
{"linux": "x86_64", "macos": "x86_64", "windows": "AMD64"},
|
||||
{"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"
|
||||
|
||||
# Allow this to be sorted
|
||||
def __lt__(self, other: Architecture) -> bool:
|
||||
return self.value < other.value
|
||||
# WebAssembly
|
||||
wasm32 = auto()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
# android archs
|
||||
arm64_v8a = auto()
|
||||
|
||||
# iOS "multiarch" architectures that include both
|
||||
# the CPU architecture and the ABI.
|
||||
arm64_iphoneos = auto()
|
||||
arm64_iphonesimulator = auto()
|
||||
x86_64_iphonesimulator = auto()
|
||||
|
||||
@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 auto_archs(platform: PlatformName) -> set[Architecture]:
|
||||
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 = (
|
||||
@@ -82,34 +119,49 @@ class Architecture(Enum):
|
||||
else ("macos" if sys.platform.startswith("darwin") else "linux")
|
||||
)
|
||||
|
||||
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 set()
|
||||
native_architecture = Architecture(synonym)
|
||||
|
||||
native_architecture = Architecture(synonym)
|
||||
return native_architecture
|
||||
|
||||
result = {native_architecture}
|
||||
@staticmethod
|
||||
def auto_archs(platform: PlatformName) -> "set[Architecture]":
|
||||
native_arch = Architecture.native_arch(platform)
|
||||
if native_arch is None:
|
||||
return set() # can't build anything on this platform
|
||||
result = {native_arch}
|
||||
|
||||
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,
|
||||
@@ -117,24 +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]
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Errors that can cause the build to fail. Each subclass of FatalError has
|
||||
a different return code, by defining them all here, we can ensure that they're
|
||||
semantically clear and unique.
|
||||
"""
|
||||
|
||||
import textwrap
|
||||
|
||||
|
||||
class FatalError(BaseException):
|
||||
"""
|
||||
Raising an error of this type will cause the message to be printed to
|
||||
stderr and the process to be terminated. Within cibuildwheel, raising this
|
||||
exception produces a better error message, and optional traceback.
|
||||
"""
|
||||
|
||||
return_code: int = 1
|
||||
|
||||
|
||||
class ConfigurationError(FatalError):
|
||||
return_code = 2
|
||||
|
||||
|
||||
class NothingToDoError(FatalError):
|
||||
return_code = 3
|
||||
|
||||
|
||||
class DeprecationError(FatalError):
|
||||
return_code = 4
|
||||
|
||||
|
||||
class NonPlatformWheelError(FatalError):
|
||||
def __init__(self) -> None:
|
||||
message = textwrap.dedent(
|
||||
"""
|
||||
Build failed because a pure Python wheel was generated.
|
||||
|
||||
If you intend to build a pure-Python wheel, you don't need
|
||||
cibuildwheel - use `pip wheel .`, `pipx run build --wheel`, `uv
|
||||
build --wheel`, etc. instead. You only need cibuildwheel if you
|
||||
have compiled (not Python) code in your wheels making them depend
|
||||
on the platform.
|
||||
|
||||
If you expected a platform wheel, check your project configuration,
|
||||
or run cibuildwheel with CIBW_BUILD_VERBOSITY=1 to view build logs.
|
||||
"""
|
||||
)
|
||||
super().__init__(message)
|
||||
self.return_code = 5
|
||||
|
||||
|
||||
class AlreadyBuiltWheelError(FatalError):
|
||||
def __init__(self, wheel_name: str) -> None:
|
||||
message = textwrap.dedent(
|
||||
f"""
|
||||
Build failed because a wheel named {wheel_name} was already generated in the current run.
|
||||
|
||||
If you expected another wheel to be generated, check your project configuration, or run
|
||||
cibuildwheel with CIBW_BUILD_VERBOSITY=1 to view build logs.
|
||||
"""
|
||||
)
|
||||
super().__init__(message)
|
||||
self.return_code = 6
|
||||
|
||||
|
||||
class OCIEngineTooOldError(FatalError):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.return_code = 7
|
||||
|
||||
|
||||
class RepairStepProducedNoWheelError(FatalError):
|
||||
def __init__(self) -> None:
|
||||
message = textwrap.dedent(
|
||||
"""
|
||||
Build failed because the repair step completed successfully but
|
||||
did not produce a wheel.
|
||||
|
||||
Your `repair-wheel-command` is expected to place the repaired
|
||||
wheel in the {dest_dir} directory. See the documentation for
|
||||
example configurations:
|
||||
|
||||
https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command
|
||||
"""
|
||||
)
|
||||
super().__init__(message)
|
||||
self.return_code = 8
|
||||
|
||||
|
||||
class RepairStepProducedMultipleWheelsError(FatalError):
|
||||
def __init__(self, wheels: list[str]) -> None:
|
||||
message = textwrap.dedent(
|
||||
f"""
|
||||
Build failed because the repair step completed successfully but
|
||||
produced multiple wheels: {wheels}
|
||||
|
||||
Your `repair-wheel-command` is expected to place one repaired
|
||||
wheel in the {{dest_dir}} directory. See the documentation for
|
||||
example configurations:
|
||||
|
||||
https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command
|
||||
"""
|
||||
)
|
||||
super().__init__(message)
|
||||
self.return_code = 8
|
||||
+71
-5
@@ -2,18 +2,22 @@
|
||||
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")
|
||||
|
||||
|
||||
class Printable(Protocol):
|
||||
def __str__(self) -> str:
|
||||
...
|
||||
def __str__(self) -> str: ...
|
||||
|
||||
|
||||
def dump_python_configurations(
|
||||
@@ -33,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),
|
||||
]
|
||||
+209
-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,28 +34,92 @@ 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",
|
||||
"macosx_x86_64": "macOS x86_64",
|
||||
"macosx_universal2": "macOS Universal 2 - x86_64 and arm64",
|
||||
"macosx_arm64": "macOS arm64 - Apple Silicon",
|
||||
"pyodide_wasm32": "Pyodide",
|
||||
"android_arm64_v8a": "Android arm64_v8a",
|
||||
"android_x86_64": "Android x86_64",
|
||||
"ios_arm64_iphoneos": "iOS Device (ARM64)",
|
||||
"ios_arm64_iphonesimulator": "iOS Simulator (ARM64)",
|
||||
"ios_x86_64_iphonesimulator": "iOS Simulator (x86_64)",
|
||||
}
|
||||
|
||||
|
||||
class Colors:
|
||||
def __init__(self, *, enabled: bool) -> None:
|
||||
self.red = "\033[31m" if enabled else ""
|
||||
self.green = "\033[32m" if enabled else ""
|
||||
self.yellow = "\033[33m" if enabled else ""
|
||||
self.blue = "\033[34m" if enabled else ""
|
||||
self.cyan = "\033[36m" if enabled else ""
|
||||
self.bright_red = "\033[91m" if enabled else ""
|
||||
self.bright_green = "\033[92m" if enabled else ""
|
||||
self.white = "\033[37m\033[97m" if enabled else ""
|
||||
self.gray = "\033[38;5;244m" if enabled else ""
|
||||
|
||||
self.bg_grey = "\033[48;5;235m" if enabled else ""
|
||||
|
||||
self.bold = "\033[1m" if enabled else ""
|
||||
self.faint = "\033[2m" if enabled else ""
|
||||
|
||||
self.end = "\033[0m" if enabled else ""
|
||||
|
||||
|
||||
class Symbols:
|
||||
def __init__(self, *, unicode: bool) -> None:
|
||||
self.done = "✓" if unicode else "done"
|
||||
self.error = "✕" if unicode else "failed"
|
||||
|
||||
|
||||
@dataclasses.dataclass(kw_only=True, frozen=True)
|
||||
class BuildInfo:
|
||||
identifier: str
|
||||
filename: Path | None
|
||||
duration: float
|
||||
|
||||
@functools.cached_property
|
||||
def size(self) -> str | None:
|
||||
if self.filename is None:
|
||||
return None
|
||||
return humanize.naturalsize(self.filename.stat().st_size)
|
||||
|
||||
@functools.cached_property
|
||||
def sha256(self) -> str | None:
|
||||
if self.filename is None:
|
||||
return None
|
||||
with self.filename.open("rb") as f:
|
||||
digest = hashlib.file_digest(f, "sha256")
|
||||
return digest.hexdigest()
|
||||
|
||||
def __str__(self) -> str:
|
||||
duration = humanize.naturaldelta(self.duration)
|
||||
if self.filename:
|
||||
return f"{self.identifier}: {self.filename.name} {self.size} in {duration}, SHA256={self.sha256}"
|
||||
return f"{self.identifier}: {duration} (test only)"
|
||||
|
||||
|
||||
class Logger:
|
||||
fold_mode: 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"):
|
||||
@@ -56,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()
|
||||
@@ -88,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()
|
||||
@@ -96,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
|
||||
|
||||
@@ -115,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:
|
||||
@@ -132,24 +214,48 @@ 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:
|
||||
return self.step_start_time is not None
|
||||
|
||||
def _start_fold_group(self, name: str) -> None:
|
||||
self._end_fold_group()
|
||||
@@ -186,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)
|
||||
@@ -201,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]
|
||||
@@ -222,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.
|
||||
|
||||
@@ -1,630 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
from collections.abc import Sequence, Set
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Literal, Tuple
|
||||
|
||||
from filelock import FileLock
|
||||
|
||||
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,
|
||||
detect_ci_provider,
|
||||
download,
|
||||
find_compatible_wheel,
|
||||
get_build_verbosity_extra_flags,
|
||||
get_pip_version,
|
||||
install_certifi_script,
|
||||
prepare_command,
|
||||
read_python_configs,
|
||||
shell,
|
||||
split_config_settings,
|
||||
test_fail_cwd_file,
|
||||
unwrap,
|
||||
virtualenv,
|
||||
)
|
||||
|
||||
|
||||
def get_macos_version() -> tuple[int, int]:
|
||||
"""
|
||||
Returns the macOS major/minor version, as a tuple, e.g. (10, 15) or (11, 0)
|
||||
|
||||
These tuples can be used in comparisons, e.g.
|
||||
(10, 14) <= (11, 0) == True
|
||||
(10, 14) <= (10, 16) == True
|
||||
(11, 2) <= (11, 0) != True
|
||||
"""
|
||||
version_str, _, _ = platform.mac_ver()
|
||||
version = tuple(map(int, version_str.split(".")[:2]))
|
||||
return typing.cast(Tuple[int, int], version)
|
||||
|
||||
|
||||
def get_macos_sdks() -> list[str]:
|
||||
output = call("xcodebuild", "-showsdks", capture_stdout=True)
|
||||
return [m.group(1) for m in re.finditer(r"-sdk (macosx\S+)", output)]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PythonConfiguration:
|
||||
version: str
|
||||
identifier: str
|
||||
url: str
|
||||
|
||||
|
||||
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]
|
||||
|
||||
# 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(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)]
|
||||
|
||||
# filter-out some cross-compilation configs with PyPy:
|
||||
# can't build arm64 on x86_64
|
||||
# rosetta allows to build x86_64 on arm64
|
||||
if platform.machine() == "x86_64":
|
||||
python_configurations_before = set(python_configurations)
|
||||
python_configurations = [
|
||||
c
|
||||
for c in python_configurations
|
||||
if not (c.identifier.startswith("pp") and c.identifier.endswith("arm64"))
|
||||
]
|
||||
removed_elements = python_configurations_before - set(python_configurations)
|
||||
if removed_elements:
|
||||
ids = ", ".join(c.identifier for c in removed_elements)
|
||||
log.quiet(
|
||||
unwrap(
|
||||
f"""
|
||||
Note: {ids} {'was' if len(removed_elements) == 1 else 'were'}
|
||||
selected, but can't be built on x86_64 so will be skipped automatically.
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
return python_configurations
|
||||
|
||||
|
||||
def install_cpython(tmp: Path, version: str, url: str) -> Path:
|
||||
installation_path = Path(f"/Library/Frameworks/Python.framework/Versions/{version}")
|
||||
with FileLock(CIBW_CACHE_PATH / f"cpython{version}.lock"):
|
||||
installed_system_packages = call("pkgutil", "--pkgs", capture_stdout=True).splitlines()
|
||||
# if this version of python isn't installed, get it from python.org and install
|
||||
python_package_identifier = f"org.python.Python.PythonFramework-{version}"
|
||||
if python_package_identifier not in installed_system_packages:
|
||||
if detect_ci_provider() is None:
|
||||
# if running locally, we don't want to install CPython with sudo
|
||||
# let the user know & provide a link to the installer
|
||||
print(
|
||||
f"Error: CPython {version} is not installed.\n"
|
||||
"cibuildwheel will not perform system-wide installs when running outside of CI.\n"
|
||||
f"To build locally, install CPython {version} on this machine, or, disable this version of Python using CIBW_SKIP=cp{version.replace('.', '')}-macosx_*\n"
|
||||
f"\nDownload link: {url}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
pkg_path = tmp / "Python.pkg"
|
||||
# download the pkg
|
||||
download(url, pkg_path)
|
||||
# install
|
||||
call("sudo", "installer", "-pkg", pkg_path, "-target", "/")
|
||||
pkg_path.unlink()
|
||||
env = os.environ.copy()
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
call(installation_path / "bin" / "python3", install_certifi_script, env=env)
|
||||
|
||||
return installation_path / "bin" / "python3"
|
||||
|
||||
|
||||
def install_pypy(tmp: Path, url: str) -> Path:
|
||||
pypy_tar_bz2 = url.rsplit("/", 1)[-1]
|
||||
extension = ".tar.bz2"
|
||||
assert pypy_tar_bz2.endswith(extension)
|
||||
installation_path = CIBW_CACHE_PATH / pypy_tar_bz2[: -len(extension)]
|
||||
with FileLock(str(installation_path) + ".lock"):
|
||||
if not installation_path.exists():
|
||||
downloaded_tar_bz2 = tmp / pypy_tar_bz2
|
||||
download(url, downloaded_tar_bz2)
|
||||
installation_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
call("tar", "-C", installation_path.parent, "-xf", downloaded_tar_bz2)
|
||||
downloaded_tar_bz2.unlink()
|
||||
return installation_path / "bin" / "pypy3"
|
||||
|
||||
|
||||
def setup_python(
|
||||
tmp: Path,
|
||||
python_configuration: PythonConfiguration,
|
||||
dependency_constraint_flags: Sequence[PathOrStr],
|
||||
environment: ParsedEnvironment,
|
||||
build_frontend: BuildFrontendName,
|
||||
) -> dict[str, str]:
|
||||
tmp.mkdir()
|
||||
implementation_id = python_configuration.identifier.split("-")[0]
|
||||
log.step(f"Installing Python {implementation_id}...")
|
||||
if implementation_id.startswith("cp"):
|
||||
base_python = install_cpython(tmp, python_configuration.version, python_configuration.url)
|
||||
elif implementation_id.startswith("pp"):
|
||||
base_python = install_pypy(tmp, python_configuration.url)
|
||||
else:
|
||||
msg = "Unknown Python implementation"
|
||||
raise ValueError(msg)
|
||||
assert base_python.exists()
|
||||
|
||||
log.step("Setting up build environment...")
|
||||
venv_path = tmp / "venv"
|
||||
env = virtualenv(base_python, venv_path, dependency_constraint_flags)
|
||||
venv_bin_path = venv_path / "bin"
|
||||
assert venv_bin_path.exists()
|
||||
# Fix issue with site.py setting the wrong `sys.prefix`, `sys.exec_prefix`,
|
||||
# `sys.path`, ... for PyPy: https://foss.heptapod.net/pypy/pypy/issues/3175
|
||||
# Also fix an issue with the shebang of installed scripts inside the
|
||||
# testing virtualenv- see https://github.com/theacodes/nox/issues/44 and
|
||||
# https://github.com/pypa/virtualenv/issues/620
|
||||
# Also see https://github.com/python/cpython/pull/9516
|
||||
env.pop("__PYVENV_LAUNCHER__", None)
|
||||
|
||||
# we version pip ourselves, so we don't care about pip version checking
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
|
||||
# upgrade pip to the version matching our constraints
|
||||
# if necessary, reinstall it to ensure that it's available on PATH as 'pip'
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"pip",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
cwd=venv_path,
|
||||
)
|
||||
|
||||
# Apply our environment after pip is ready
|
||||
env = environment.as_dictionary(prev_environment=env)
|
||||
|
||||
# check what 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"):
|
||||
print(
|
||||
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# check what Python version we're on
|
||||
call("which", "python", env=env)
|
||||
call("python", "--version", env=env)
|
||||
which_python = call("which", "python", env=env, capture_stdout=True).strip()
|
||||
if which_python != str(venv_bin_path / "python"):
|
||||
print(
|
||||
"cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
config_is_arm64 = python_configuration.identifier.endswith("arm64")
|
||||
config_is_universal2 = python_configuration.identifier.endswith("universal2")
|
||||
|
||||
# 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.
|
||||
# PyPy defaults to 10.7, causing inconsistencies if it's left unset.
|
||||
env.setdefault("MACOSX_DEPLOYMENT_TARGET", "11.0" if config_is_arm64 else "10.9")
|
||||
|
||||
if python_configuration.version not in {"3.6", "3.7"}:
|
||||
if config_is_arm64:
|
||||
# macOS 11 is the first OS with arm64 support, so the wheels
|
||||
# have that as a minimum.
|
||||
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-11.0-arm64")
|
||||
env.setdefault("ARCHFLAGS", "-arch arm64")
|
||||
elif config_is_universal2:
|
||||
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-universal2")
|
||||
env.setdefault("ARCHFLAGS", "-arch arm64 -arch x86_64")
|
||||
elif python_configuration.identifier.endswith("x86_64"):
|
||||
# even on the macos11.0 Python installer, on the x86_64 side it's
|
||||
# compatible back to 10.9.
|
||||
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-x86_64")
|
||||
env.setdefault("ARCHFLAGS", "-arch x86_64")
|
||||
|
||||
building_arm64 = config_is_arm64 or config_is_universal2
|
||||
if building_arm64 and get_macos_version() < (10, 16) and "SDKROOT" not in env:
|
||||
# xcode 12.2 or higher can build arm64 on macos 10.15 or below, but
|
||||
# needs the correct SDK selected.
|
||||
sdks = get_macos_sdks()
|
||||
|
||||
# Different versions of Xcode contain different SDK versions...
|
||||
# we're happy with anything newer than macOS 11.0
|
||||
arm64_compatible_sdks = [s for s in sdks if not s.startswith("macosx10.")]
|
||||
|
||||
if not arm64_compatible_sdks:
|
||||
log.warning(
|
||||
unwrap(
|
||||
"""
|
||||
SDK for building arm64-compatible wheels not found. You need Xcode 12.2 or later
|
||||
to build universal2 or arm64 wheels.
|
||||
"""
|
||||
)
|
||||
)
|
||||
else:
|
||||
env.setdefault("SDKROOT", arm64_compatible_sdks[0])
|
||||
|
||||
log.step("Installing build tools...")
|
||||
if build_frontend == "pip":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"setuptools",
|
||||
"wheel",
|
||||
"delocate",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
)
|
||||
elif build_frontend == "build":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"delocate",
|
||||
"build[virtualenv]",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
)
|
||||
else:
|
||||
assert_never(build_frontend)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def build(options: Options, tmp_path: Path) -> None:
|
||||
python_configurations = get_python_configurations(
|
||||
options.globals.build_selector, options.globals.architectures
|
||||
)
|
||||
|
||||
if not python_configurations:
|
||||
return
|
||||
|
||||
try:
|
||||
before_all_options_identifier = python_configurations[0].identifier
|
||||
before_all_options = options.build_options(before_all_options_identifier)
|
||||
|
||||
if before_all_options.before_all:
|
||||
log.step("Running before_all...")
|
||||
env = before_all_options.environment.as_dictionary(prev_environment=os.environ)
|
||||
env.setdefault("MACOSX_DEPLOYMENT_TARGET", "10.9")
|
||||
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 or BuildFrontendConfig("pip")
|
||||
log.build_start(config.identifier)
|
||||
|
||||
identifier_tmp_dir = tmp_path / config.identifier
|
||||
identifier_tmp_dir.mkdir()
|
||||
built_wheel_dir = identifier_tmp_dir / "built_wheel"
|
||||
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
|
||||
|
||||
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),
|
||||
]
|
||||
|
||||
env = setup_python(
|
||||
identifier_tmp_dir / "build",
|
||||
config,
|
||||
dependency_constraint_flags,
|
||||
build_options.environment,
|
||||
build_frontend.name,
|
||||
)
|
||||
|
||||
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
|
||||
if compatible_wheel:
|
||||
log.step_end()
|
||||
print(
|
||||
f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
|
||||
)
|
||||
repaired_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 = split_config_settings(
|
||||
build_options.config_settings, build_frontend.name
|
||||
)
|
||||
extra_flags += build_frontend.args
|
||||
|
||||
if build_frontend.name == "pip":
|
||||
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
|
||||
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
|
||||
# see https://github.com/pypa/cibuildwheel/pull/369
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"pip",
|
||||
"wheel",
|
||||
build_options.package_dir.resolve(),
|
||||
f"--wheel-dir={built_wheel_dir}",
|
||||
"--no-deps",
|
||||
*extra_flags,
|
||||
env=env,
|
||||
)
|
||||
elif build_frontend.name == "build":
|
||||
if not 0 <= build_options.build_verbosity < 2:
|
||||
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
|
||||
log.warning(msg)
|
||||
build_env = env.copy()
|
||||
if build_options.dependency_constraints:
|
||||
constraint_path = (
|
||||
build_options.dependency_constraints.get_for_python_version(
|
||||
config.version
|
||||
)
|
||||
)
|
||||
build_env["PIP_CONSTRAINT"] = constraint_path.as_uri()
|
||||
build_env["VIRTUALENV_PIP"] = get_pip_version(env)
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"build",
|
||||
build_options.package_dir,
|
||||
"--wheel",
|
||||
f"--outdir={built_wheel_dir}",
|
||||
*extra_flags,
|
||||
env=build_env,
|
||||
)
|
||||
else:
|
||||
assert_never(build_frontend)
|
||||
|
||||
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
||||
|
||||
repaired_wheel_dir.mkdir()
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise NonPlatformWheelError()
|
||||
|
||||
if build_options.repair_command:
|
||||
log.step("Repairing wheel...")
|
||||
|
||||
if config_is_universal2:
|
||||
delocate_archs = "x86_64,arm64"
|
||||
elif config_is_arm64:
|
||||
delocate_archs = "arm64"
|
||||
else:
|
||||
delocate_archs = "x86_64"
|
||||
|
||||
repair_command_prepared = prepare_command(
|
||||
build_options.repair_command,
|
||||
wheel=built_wheel,
|
||||
dest_dir=repaired_wheel_dir,
|
||||
delocate_archs=delocate_archs,
|
||||
)
|
||||
shell(repair_command_prepared, env=env)
|
||||
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)
|
||||
|
||||
log.step_end()
|
||||
|
||||
if build_options.test_command and build_options.test_selector(config.identifier):
|
||||
machine_arch = platform.machine()
|
||||
python_arch = call(
|
||||
"python",
|
||||
"-sSc",
|
||||
"import platform; print(platform.machine())",
|
||||
env=env,
|
||||
capture_stdout=True,
|
||||
).strip()
|
||||
testing_archs: list[Literal["x86_64", "arm64"]]
|
||||
|
||||
if config_is_arm64:
|
||||
testing_archs = ["arm64"]
|
||||
elif config_is_universal2:
|
||||
testing_archs = ["x86_64", "arm64"]
|
||||
else:
|
||||
testing_archs = ["x86_64"]
|
||||
|
||||
for testing_arch in testing_archs:
|
||||
if config_is_universal2:
|
||||
arch_specific_identifier = f"{config.identifier}:{testing_arch}"
|
||||
if not build_options.test_selector(arch_specific_identifier):
|
||||
continue
|
||||
|
||||
if machine_arch == "x86_64" and testing_arch == "arm64":
|
||||
if config_is_arm64:
|
||||
log.warning(
|
||||
unwrap(
|
||||
"""
|
||||
While arm64 wheels can be built on x86_64, they cannot be
|
||||
tested. The ability to test the arm64 wheels will be added in a
|
||||
future release of cibuildwheel, once Apple Silicon CI runners
|
||||
are widely available. To silence this warning, set
|
||||
`CIBW_TEST_SKIP: "*-macosx_arm64"`.
|
||||
"""
|
||||
)
|
||||
)
|
||||
elif config_is_universal2:
|
||||
log.warning(
|
||||
unwrap(
|
||||
"""
|
||||
While universal2 wheels can be built on x86_64, the arm64 part
|
||||
of them cannot currently be tested. The ability to test the
|
||||
arm64 part of a universal2 wheel will be added in a future
|
||||
release of cibuildwheel, once Apple Silicon CI runners are
|
||||
widely available. To silence this warning, set
|
||||
`CIBW_TEST_SKIP: "*-macosx_universal2:arm64"`.
|
||||
"""
|
||||
)
|
||||
)
|
||||
else:
|
||||
msg = "unreachable"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
# skip this test
|
||||
continue
|
||||
|
||||
is_cp38 = config.identifier.startswith("cp38-")
|
||||
if testing_arch == "arm64" and is_cp38 and python_arch != "arm64":
|
||||
log.warning(
|
||||
unwrap(
|
||||
"""
|
||||
While cibuildwheel can build CPython 3.8 universal2/arm64 wheels, we
|
||||
cannot test the arm64 part of them, even when running on an Apple
|
||||
Silicon machine. This is because we use the x86_64 installer of
|
||||
CPython 3.8. See the discussion in
|
||||
https://github.com/pypa/cibuildwheel/pull/1169 for the details. To
|
||||
silence this warning, set `CIBW_TEST_SKIP: "cp38-macosx_*:arm64"`.
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# skip this test
|
||||
continue
|
||||
|
||||
log.step(
|
||||
"Testing wheel..."
|
||||
if testing_arch == machine_arch
|
||||
else f"Testing wheel on {testing_arch}..."
|
||||
)
|
||||
|
||||
# set up a virtual environment to install and test from, to make sure
|
||||
# there are no dependencies that were pulled in at build time.
|
||||
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
|
||||
|
||||
venv_dir = identifier_tmp_dir / "venv-test"
|
||||
|
||||
arch_prefix = []
|
||||
if testing_arch != machine_arch:
|
||||
if machine_arch == "arm64" and testing_arch == "x86_64":
|
||||
# rosetta2 will provide the emulation with just the arch prefix.
|
||||
arch_prefix = ["arch", "-x86_64"]
|
||||
else:
|
||||
msg = f"don't know how to emulate {testing_arch} on {machine_arch}"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
# define a custom 'call' function that adds the arch prefix each time
|
||||
call_with_arch = functools.partial(call, *arch_prefix)
|
||||
shell_with_arch = functools.partial(call, *arch_prefix, "/bin/sh", "-c")
|
||||
|
||||
# Use --no-download to ensure determinism by using seed libraries
|
||||
# built into virtualenv
|
||||
call_with_arch("python", "-m", "virtualenv", "--no-download", venv_dir, env=env)
|
||||
|
||||
virtualenv_env = env.copy()
|
||||
virtualenv_env["PATH"] = os.pathsep.join(
|
||||
[
|
||||
str(venv_dir / "bin"),
|
||||
virtualenv_env["PATH"],
|
||||
]
|
||||
)
|
||||
|
||||
# check that we are using the Python from the virtual environment
|
||||
call_with_arch("which", "python", env=virtualenv_env)
|
||||
|
||||
if build_options.before_test:
|
||||
before_test_prepared = prepare_command(
|
||||
build_options.before_test,
|
||||
project=".",
|
||||
package=build_options.package_dir,
|
||||
)
|
||||
shell_with_arch(before_test_prepared, env=virtualenv_env)
|
||||
|
||||
# install the wheel
|
||||
call_with_arch(
|
||||
"pip",
|
||||
"install",
|
||||
f"{repaired_wheel}{build_options.test_extras}",
|
||||
env=virtualenv_env,
|
||||
)
|
||||
|
||||
# test the wheel
|
||||
if build_options.test_requires:
|
||||
call_with_arch(
|
||||
"pip", "install", *build_options.test_requires, env=virtualenv_env
|
||||
)
|
||||
|
||||
# run the tests from a temp dir, with an absolute path in the command
|
||||
# (this ensures that Python runs the tests against the installed wheel
|
||||
# and not the repo code)
|
||||
test_command_prepared = prepare_command(
|
||||
build_options.test_command,
|
||||
project=Path(".").resolve(),
|
||||
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())
|
||||
|
||||
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
||||
|
||||
# we're all done here; move it to output (overwrite existing)
|
||||
if compatible_wheel is None:
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
(build_options.output_dir / repaired_wheel.name).unlink()
|
||||
|
||||
shutil.move(str(repaired_wheel), build_options.output_dir)
|
||||
built_wheels.append(build_options.output_dir / repaired_wheel.name)
|
||||
|
||||
# clean up
|
||||
shutil.rmtree(identifier_tmp_dir)
|
||||
|
||||
log.build_end()
|
||||
except subprocess.CalledProcessError as error:
|
||||
log.step_end_with_error(
|
||||
f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}"
|
||||
)
|
||||
sys.exit(1)
|
||||
+149
-62
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
@@ -8,35 +7,45 @@ import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import typing
|
||||
import uuid
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
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"
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class OCIContainerEngineConfig:
|
||||
name: ContainerEngineName
|
||||
create_args: Sequence[str] = ()
|
||||
_: 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"],
|
||||
@@ -56,10 +65,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=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 +91,55 @@ 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)
|
||||
version_info = json.loads(version_string.strip())
|
||||
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.
|
||||
@@ -98,9 +163,9 @@ class OCIContainer:
|
||||
... 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 +173,7 @@ class OCIContainer:
|
||||
self,
|
||||
*,
|
||||
image: str,
|
||||
enforce_32_bit: bool = False,
|
||||
oci_platform: OCIPlatform,
|
||||
cwd: PathOrStr | None = None,
|
||||
engine: OCIContainerEngineConfig = DEFAULT_ENGINE,
|
||||
):
|
||||
@@ -117,14 +182,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 +232,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 +269,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 +336,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 +361,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 +374,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 +485,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
|
||||
|
||||
+498
-230
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import Final, Protocol
|
||||
|
||||
from cibuildwheel import errors
|
||||
from cibuildwheel.architecture import Architecture
|
||||
from cibuildwheel.options import Options
|
||||
from cibuildwheel.platforms import android, ios, linux, macos, pyodide, windows
|
||||
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,645 @@
|
||||
import csv
|
||||
import hashlib
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
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, virtualenv
|
||||
|
||||
|
||||
def android_triplet(identifier: str) -> str:
|
||||
return {
|
||||
"arm64_v8a": "aarch64-linux-android",
|
||||
"x86_64": "x86_64-linux-android",
|
||||
}[parse_identifier(identifier)[1]]
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
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...")
|
||||
|
||||
# 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=False
|
||||
)
|
||||
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", "pip"]:
|
||||
which = call("which", command, env=build_env, capture_stdout=True).strip()
|
||||
if which != 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
|
||||
build_frontend = build_options.build_frontend
|
||||
if build_frontend.name != "build":
|
||||
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.
|
||||
list(INSERT CMAKE_FIND_ROOT_PATH 0 {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 ONLY)
|
||||
"""
|
||||
),
|
||||
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
|
||||
),
|
||||
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("patchelf", "--set-soname", new_soname, dst_path)
|
||||
|
||||
for path in paths_to_patch:
|
||||
call("patchelf", "--replace-needed", old_soname, new_soname, path)
|
||||
call(
|
||||
"patchelf",
|
||||
"--set-rpath",
|
||||
f"${{ORIGIN}}/{relpath(libs_dir, path.parent)}",
|
||||
path,
|
||||
)
|
||||
call(sys.executable, "-m", "wheel", "pack", unpacked_dir, "-d", repaired_wheel_dir)
|
||||
|
||||
|
||||
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) -> None:
|
||||
test_command = state.options.test_command
|
||||
if not (test_command and state.options.test_selector(state.config.identifier)):
|
||||
return
|
||||
|
||||
log.step("Testing wheel...")
|
||||
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,
|
||||
)
|
||||
|
||||
# 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",
|
||||
sysconfig_print("get_platform()", state.android_env).replace("-", "_"),
|
||||
"--target",
|
||||
site_packages_dir,
|
||||
f"{wheel}{state.options.test_extras}",
|
||||
*state.options.test_requires,
|
||||
env=state.build_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[:2] in [["python", "-c"], ["python", "-m"]]:
|
||||
test_args[:3] = [test_args[1], test_args[2], "--"]
|
||||
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[:1] = ["-m", test_args[0], "--"]
|
||||
else:
|
||||
msg = (
|
||||
f"Test command {test_command!r} is not supported on Android. "
|
||||
f"Supported commands are 'python -m' and 'python -c'."
|
||||
)
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
# Run the test app.
|
||||
call(
|
||||
state.python_dir / "android.py",
|
||||
"test",
|
||||
"--managed",
|
||||
"maxVersion",
|
||||
"--site-packages",
|
||||
site_packages_dir,
|
||||
"--cwd",
|
||||
cwd_dir,
|
||||
*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,691 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from collections.abc import Sequence, Set
|
||||
from pathlib import Path
|
||||
from typing import assert_never
|
||||
|
||||
from filelock import FileLock
|
||||
|
||||
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, 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 (
|
||||
combine_constraints,
|
||||
find_compatible_wheel,
|
||||
get_pip_version,
|
||||
)
|
||||
from ..venv import constraint_flags, virtualenv
|
||||
from .macos import install_cpython as install_build_cpython
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
pip_version = get_pip_version(env)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
build_env = env.copy()
|
||||
build_env["VIRTUALENV_PIP"] = pip_version
|
||||
if constraints_path:
|
||||
combine_constraints(build_env, constraints_path, None)
|
||||
|
||||
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":
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"build",
|
||||
build_options.package_dir,
|
||||
"--wheel",
|
||||
f"--outdir={built_wheel_dir}",
|
||||
*extra_flags,
|
||||
env=build_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=build_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)
|
||||
|
||||
call(
|
||||
"python",
|
||||
testbed_path,
|
||||
"run",
|
||||
*(["--verbose"] if build_options.build_verbosity > 0 else []),
|
||||
"--",
|
||||
*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,35 +1,38 @@
|
||||
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 assert_never
|
||||
|
||||
from ._compat.typing import assert_never
|
||||
from .architecture import Architecture
|
||||
from .logger import log
|
||||
from .oci_container import OCIContainer
|
||||
from .options import Options
|
||||
from .typing import PathOrStr
|
||||
from .util import (
|
||||
AlreadyBuiltWheelError,
|
||||
BuildFrontendConfig,
|
||||
BuildSelector,
|
||||
NonPlatformWheelError,
|
||||
find_compatible_wheel,
|
||||
get_build_verbosity_extra_flags,
|
||||
prepare_command,
|
||||
read_python_configs,
|
||||
split_config_settings,
|
||||
test_fail_cwd_file,
|
||||
unwrap,
|
||||
)
|
||||
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 ..typing import PathOrStr
|
||||
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
|
||||
|
||||
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
|
||||
@@ -40,20 +43,24 @@ 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
|
||||
container_engine: OCIContainerEngineConfig
|
||||
container_image: str
|
||||
|
||||
|
||||
def all_python_configurations() -> list[PythonConfiguration]:
|
||||
config_dicts = resources.read_python_configs("linux")
|
||||
return [PythonConfiguration(**item) for item in config_dicts]
|
||||
|
||||
|
||||
def get_python_configurations(
|
||||
build_selector: BuildSelector,
|
||||
architectures: Set[Architecture],
|
||||
) -> list[PythonConfiguration]:
|
||||
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
|
||||
@@ -65,8 +72,9 @@ def get_python_configurations(
|
||||
]
|
||||
|
||||
|
||||
def container_image_for_python_configuration(config: PythonConfiguration, options: Options) -> str:
|
||||
build_options = options.build_options(config.identifier)
|
||||
def container_image_for_python_configuration(
|
||||
config: PythonConfiguration, build_options: BuildOptions
|
||||
) -> str:
|
||||
# e.g
|
||||
# identifier is 'cp310-manylinux_x86_64'
|
||||
# platform_tag is 'manylinux_x86_64'
|
||||
@@ -91,15 +99,18 @@ def get_build_steps(
|
||||
Groups PythonConfigurations into BuildSteps. Each BuildStep represents a
|
||||
separate container instance.
|
||||
"""
|
||||
steps = OrderedDict[Tuple[str, str, str], BuildStep]()
|
||||
steps = OrderedDict[tuple[str, str, str, OCIContainerEngineConfig], BuildStep]()
|
||||
|
||||
for config in python_configurations:
|
||||
_, platform_tag = config.identifier.split("-", 1)
|
||||
|
||||
before_all = options.build_options(config.identifier).before_all
|
||||
container_image = container_image_for_python_configuration(config, options)
|
||||
build_options = options.build_options(config.identifier)
|
||||
|
||||
step_key = (platform_tag, container_image, before_all)
|
||||
before_all = build_options.before_all
|
||||
container_image = container_image_for_python_configuration(config, build_options)
|
||||
container_engine = build_options.container_engine
|
||||
|
||||
step_key = (platform_tag, container_image, before_all, container_engine)
|
||||
|
||||
if step_key in steps:
|
||||
steps[step_key].platform_configs.append(config)
|
||||
@@ -107,6 +118,7 @@ def get_build_steps(
|
||||
steps[step_key] = BuildStep(
|
||||
platform_configs=[config],
|
||||
platform_tag=platform_tag,
|
||||
container_engine=container_engine,
|
||||
container_image=container_image,
|
||||
)
|
||||
|
||||
@@ -117,33 +129,36 @@ 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)
|
||||
print(
|
||||
f"cibuildwheel:\n{message}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
raise errors.FatalError(message)
|
||||
|
||||
|
||||
def build_in_container(
|
||||
@@ -153,6 +168,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")
|
||||
|
||||
@@ -168,7 +184,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(
|
||||
@@ -186,48 +202,50 @@ 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")
|
||||
|
||||
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]
|
||||
build_frontend = build_options.build_frontend
|
||||
use_uv = build_frontend.name == "build[uv]"
|
||||
pip = ["uv", "pip"] if use_uv else ["pip"]
|
||||
|
||||
log.step("Setting up build environment...")
|
||||
|
||||
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)
|
||||
|
||||
# check config python is still on PATH
|
||||
which_python = container.call(["which", "python"], env=env, capture_output=True).strip()
|
||||
if PurePosixPath(which_python) != python_bin / "python":
|
||||
print(
|
||||
"cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
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)
|
||||
|
||||
which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip()
|
||||
if PurePosixPath(which_pip) != python_bin / "pip":
|
||||
print(
|
||||
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if use_uv:
|
||||
which_uv = container.call(["which", "uv"], env=env, capture_output=True).strip()
|
||||
if not which_uv:
|
||||
msg = "uv not found on PATH. You must use a supported manylinux or musllinux environment with uv."
|
||||
raise errors.FatalError(msg)
|
||||
else:
|
||||
which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip()
|
||||
if PurePosixPath(which_pip) != python_bin / "pip":
|
||||
msg = "pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it."
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
|
||||
if compatible_wheel:
|
||||
@@ -235,7 +253,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...")
|
||||
@@ -253,42 +271,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":
|
||||
if not 0 <= build_options.build_verbosity < 2:
|
||||
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
|
||||
log.warning(msg)
|
||||
container.call(
|
||||
[
|
||||
"python",
|
||||
"-m",
|
||||
"build",
|
||||
container_package_dir,
|
||||
"--wheel",
|
||||
f"--outdir={built_wheel_dir}",
|
||||
*extra_flags,
|
||||
],
|
||||
env=env,
|
||||
)
|
||||
else:
|
||||
assert_never(build_frontend)
|
||||
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]
|
||||
|
||||
@@ -297,7 +315,7 @@ 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...")
|
||||
@@ -308,28 +326,47 @@ def build_in_container(
|
||||
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...")
|
||||
|
||||
# set up a virtual environment to install and test from, to make sure
|
||||
# there are no dependencies that were pulled in at build time.
|
||||
container.call(["pip", "install", "virtualenv", *dependency_constraint_flags], env=env)
|
||||
if not use_uv:
|
||||
container.call(
|
||||
["pip", "install", "virtualenv", *dependency_constraint_flags], env=env
|
||||
)
|
||||
|
||||
testing_temp_dir = PurePosixPath(
|
||||
container.call(["mktemp", "-d"], capture_output=True).strip()
|
||||
)
|
||||
venv_dir = testing_temp_dir / "venv"
|
||||
|
||||
container.call(["python", "-m", "virtualenv", "--no-download", venv_dir], env=env)
|
||||
if use_uv:
|
||||
container.call(["uv", "venv", venv_dir, "--python", python_bin / "python"], env=env)
|
||||
else:
|
||||
# Use embedded dependencies from virtualenv to ensure determinism
|
||||
venv_args = ["--no-periodic-update", "--pip=embed", "--no-setuptools"]
|
||||
if "38" in config.identifier:
|
||||
venv_args.append("--no-wheel")
|
||||
container.call(["python", "-m", "virtualenv", *venv_args, venv_dir], env=env)
|
||||
|
||||
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(
|
||||
@@ -340,46 +377,52 @@ 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,
|
||||
)
|
||||
|
||||
# Install any requirements to run the tests
|
||||
if build_options.test_requires:
|
||||
container.call(["pip", "install", *build_options.test_requires], env=virtualenv_env)
|
||||
container.call([*pip, "install", *build_options.test_requires], env=virtualenv_env)
|
||||
|
||||
# Run the tests from a different directory
|
||||
test_command_prepared = prepare_command(
|
||||
build_options.test_command,
|
||||
project=container_project_path,
|
||||
package=container_package_dir,
|
||||
wheel=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
|
||||
@@ -387,30 +430,7 @@ def build_in_container(
|
||||
log.step_end()
|
||||
|
||||
|
||||
def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
|
||||
try:
|
||||
# check the container engine is installed
|
||||
subprocess.run(
|
||||
[options.globals.container_engine.name, "--version"],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
print(
|
||||
unwrap(
|
||||
f"""
|
||||
cibuildwheel: {options.globals.container_engine} not found. An
|
||||
OCI exe like Docker or Podman is required to run Linux builds.
|
||||
If you're building on Travis CI, add `services: [docker]` to
|
||||
your .travis.yml. If you're building on Circle CI in Linux,
|
||||
add a `setup_remote_docker` step to your .circleci/config.yml.
|
||||
If you're building on Cirrus CI, use `docker_builder` task.
|
||||
"""
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
def build(options: Options, tmp_path: Path) -> None:
|
||||
python_configurations = get_python_configurations(
|
||||
options.globals.build_selector, options.globals.architectures
|
||||
)
|
||||
@@ -425,17 +445,38 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
|
||||
container_package_dir = container_project_path / abs_package_dir.relative_to(cwd)
|
||||
|
||||
for build_step in get_build_steps(options, python_configurations):
|
||||
try:
|
||||
# check the container engine is installed
|
||||
subprocess.run(
|
||||
[build_step.container_engine.name, "--version"],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
except subprocess.CalledProcessError as error:
|
||||
msg = unwrap(
|
||||
f"""
|
||||
{build_step.container_engine.name} not found. An OCI exe like
|
||||
Docker or Podman is required to run Linux builds. If you're
|
||||
building on Travis CI, add `services: [docker]` to your
|
||||
.travis.yml. If you're building on Circle CI in Linux, add a
|
||||
`setup_remote_docker` step to your .circleci/config.yml. If
|
||||
you're building on Cirrus CI, use `docker_builder` task.
|
||||
"""
|
||||
)
|
||||
raise errors.ConfigurationError(msg) from error
|
||||
|
||||
try:
|
||||
ids_to_build = [x.identifier for x in build_step.platform_configs]
|
||||
log.step(f"Starting container image {build_step.container_image}...")
|
||||
|
||||
print(f"info: This container will host the build for {', '.join(ids_to_build)}...")
|
||||
architecture = Architecture(build_step.platform_tag.split("_", 1)[1])
|
||||
|
||||
with OCIContainer(
|
||||
image=build_step.container_image,
|
||||
enforce_32_bit=build_step.platform_tag.endswith("i686"),
|
||||
oci_platform=ARCHITECTURE_OCI_PLATFORM_MAP[architecture],
|
||||
cwd=container_project_path,
|
||||
engine=options.globals.container_engine,
|
||||
engine=build_step.container_engine,
|
||||
) as container:
|
||||
build_in_container(
|
||||
options=options,
|
||||
@@ -443,14 +484,13 @@ 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:
|
||||
log.step_end_with_error(
|
||||
f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}"
|
||||
)
|
||||
troubleshoot(options, error)
|
||||
sys.exit(1)
|
||||
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
||||
raise errors.FatalError(msg) from error
|
||||
|
||||
|
||||
def _matches_prepared_command(error_cmd: Sequence[str], command_template: str) -> bool:
|
||||
@@ -469,7 +509,6 @@ def troubleshoot(options: Options, error: Exception) -> None:
|
||||
) # TODO allow matching of overrides too?
|
||||
):
|
||||
# the wheel build step or the repair step failed
|
||||
print("Checking for common errors...")
|
||||
so_files = list(options.globals.package_dir.glob("**/*.so"))
|
||||
|
||||
if so_files:
|
||||
@@ -0,0 +1,751 @@
|
||||
import dataclasses
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
from collections.abc import Set
|
||||
from pathlib import Path
|
||||
from typing import Literal, assert_never
|
||||
|
||||
from filelock import FileLock
|
||||
from packaging.version import Version
|
||||
|
||||
from .. import errors
|
||||
from ..architecture import Architecture
|
||||
from ..ci import detect_ci_provider
|
||||
from ..environment import ParsedEnvironment
|
||||
from ..frontend import BuildFrontendName, get_build_frontend_extra_flags
|
||||
from ..logger import log
|
||||
from ..options import Options
|
||||
from ..selector import BuildSelector
|
||||
from ..util import resources
|
||||
from ..util.cmd import call, shell
|
||||
from ..util.file import (
|
||||
CIBW_CACHE_PATH,
|
||||
copy_test_sources,
|
||||
download,
|
||||
move_file,
|
||||
)
|
||||
from ..util.helpers import prepare_command, unwrap
|
||||
from ..util.packaging import combine_constraints, find_compatible_wheel, get_pip_version
|
||||
from ..venv import constraint_flags, find_uv, virtualenv
|
||||
|
||||
|
||||
@functools.cache
|
||||
def get_macos_version() -> tuple[int, int]:
|
||||
"""
|
||||
Returns the macOS major/minor version, as a tuple, e.g. (10, 15) or (11, 0)
|
||||
|
||||
These tuples can be used in comparisons, e.g.
|
||||
(10, 14) <= (11, 0) == True
|
||||
(10, 14) <= (10, 16) == True
|
||||
(11, 2) <= (11, 0) != True
|
||||
"""
|
||||
version_str, _, _ = platform.mac_ver()
|
||||
version = tuple(map(int, version_str.split(".")[:2]))
|
||||
if (10, 15) < version < (11, 0):
|
||||
# When built against an older macOS SDK, Python will report macOS 10.16
|
||||
# instead of the real version.
|
||||
version_str = call(
|
||||
sys.executable,
|
||||
"-sS",
|
||||
"-c",
|
||||
"import platform; print(platform.mac_ver()[0])",
|
||||
env={"SYSTEM_VERSION_COMPAT": "0"},
|
||||
capture_stdout=True,
|
||||
)
|
||||
version = tuple(map(int, version_str.split(".")[:2]))
|
||||
return typing.cast(tuple[int, int], version)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def get_test_macosx_deployment_target() -> str:
|
||||
version = get_macos_version()
|
||||
if version >= (11, 0):
|
||||
return f"{version[0]}.0"
|
||||
return f"{version[0]}.{version[1]}"
|
||||
|
||||
|
||||
def get_macos_sdks() -> list[str]:
|
||||
output = call("xcodebuild", "-showsdks", capture_stdout=True)
|
||||
return [m.group(1) for m in re.finditer(r"-sdk (macosx\S+)", output)]
|
||||
|
||||
|
||||
@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]:
|
||||
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(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)]
|
||||
|
||||
# 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":
|
||||
python_configurations_before = set(python_configurations)
|
||||
python_configurations = [
|
||||
c
|
||||
for c in python_configurations
|
||||
if not (c.identifier.startswith(("pp", "gp")) and c.identifier.endswith("arm64"))
|
||||
]
|
||||
removed_elements = python_configurations_before - set(python_configurations)
|
||||
if removed_elements:
|
||||
ids = ", ".join(c.identifier for c in removed_elements)
|
||||
log.quiet(
|
||||
unwrap(
|
||||
f"""
|
||||
Note: {ids} {"was" if len(removed_elements) == 1 else "were"}
|
||||
selected, but can't be built on x86_64 so will be skipped automatically.
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
return python_configurations
|
||||
|
||||
|
||||
def install_cpython(_tmp: Path, version: str, url: str, free_threading: bool) -> Path:
|
||||
ft = "T" if free_threading else ""
|
||||
installation_path = Path(f"/Library/Frameworks/Python{ft}.framework/Versions/{version}")
|
||||
with FileLock(CIBW_CACHE_PATH / f"cpython{version}.lock"):
|
||||
installed_system_packages = call("pkgutil", "--pkgs", capture_stdout=True).splitlines()
|
||||
# if this version of python isn't installed, get it from python.org and install
|
||||
python_package_identifier = f"org.python.Python.Python{ft}Framework-{version}"
|
||||
if python_package_identifier not in installed_system_packages:
|
||||
if detect_ci_provider() is None:
|
||||
# if running locally, we don't want to install CPython with sudo
|
||||
# let the user know & provide a link to the installer
|
||||
msg = inspect.cleandoc(
|
||||
f"""
|
||||
Error: CPython {version} is not installed.
|
||||
cibuildwheel will not perform system-wide installs when running outside of CI.
|
||||
To build locally, install CPython {version} on this machine, or, disable this
|
||||
version of Python using CIBW_SKIP=cp{version.replace(".", "")}-macosx_*
|
||||
For portable builds, cibuildwheel needs the official builds from python.org.
|
||||
Download link: {url}
|
||||
"""
|
||||
)
|
||||
raise errors.FatalError(msg)
|
||||
python_filename = url.split("/")[-1]
|
||||
pkg_path = CIBW_CACHE_PATH / "cpython-installer" / python_filename
|
||||
if not pkg_path.exists():
|
||||
download(url, pkg_path)
|
||||
args = []
|
||||
if version.startswith("3.13"):
|
||||
# Python 3.13 is the first version to have a free-threading option
|
||||
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_313.resolve())]
|
||||
elif version.startswith("3.14"):
|
||||
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_314.resolve())]
|
||||
call("sudo", "installer", "-pkg", pkg_path, *args, "-target", "/")
|
||||
pkg_path.unlink()
|
||||
env = os.environ.copy()
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
|
||||
if free_threading:
|
||||
call(installation_path / f"bin/python{version}t", "-m", "ensurepip", env=env)
|
||||
call(
|
||||
installation_path / f"bin/python{version}t",
|
||||
resources.INSTALL_CERTIFI_SCRIPT,
|
||||
env=env,
|
||||
)
|
||||
else:
|
||||
call(installation_path / "bin/python3", resources.INSTALL_CERTIFI_SCRIPT, env=env)
|
||||
|
||||
return installation_path / "bin" / (f"python{version}t" if free_threading else "python3")
|
||||
|
||||
|
||||
def install_pypy(tmp: Path, url: str) -> Path:
|
||||
pypy_tar_bz2 = url.rsplit("/", 1)[-1]
|
||||
extension = ".tar.bz2"
|
||||
assert pypy_tar_bz2.endswith(extension)
|
||||
installation_path = CIBW_CACHE_PATH / pypy_tar_bz2[: -len(extension)]
|
||||
with FileLock(str(installation_path) + ".lock"):
|
||||
if not installation_path.exists():
|
||||
downloaded_tar_bz2 = tmp / pypy_tar_bz2
|
||||
download(url, downloaded_tar_bz2)
|
||||
installation_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
call("tar", "-C", installation_path.parent, "-xf", downloaded_tar_bz2)
|
||||
downloaded_tar_bz2.unlink()
|
||||
return installation_path / "bin" / "pypy3"
|
||||
|
||||
|
||||
def install_graalpy(tmp: Path, url: str) -> Path:
|
||||
graalpy_archive = url.rsplit("/", 1)[-1]
|
||||
extension = ".tar.gz"
|
||||
assert graalpy_archive.endswith(extension)
|
||||
installation_path = CIBW_CACHE_PATH / graalpy_archive[: -len(extension)]
|
||||
with FileLock(str(installation_path) + ".lock"):
|
||||
if not installation_path.exists():
|
||||
downloaded_archive = tmp / graalpy_archive
|
||||
download(url, downloaded_archive)
|
||||
installation_path.mkdir(parents=True)
|
||||
# GraalPy top-folder name is inconsistent with archive name
|
||||
call("tar", "-C", installation_path, "--strip-components=1", "-xzf", downloaded_archive)
|
||||
downloaded_archive.unlink()
|
||||
return installation_path / "bin" / "graalpy"
|
||||
|
||||
|
||||
def setup_python(
|
||||
tmp: Path,
|
||||
python_configuration: PythonConfiguration,
|
||||
dependency_constraint: Path | None,
|
||||
environment: ParsedEnvironment,
|
||||
build_frontend: BuildFrontendName,
|
||||
) -> tuple[Path, dict[str, str]]:
|
||||
uv_path = find_uv()
|
||||
use_uv = build_frontend == "build[uv]"
|
||||
|
||||
tmp.mkdir()
|
||||
implementation_id = python_configuration.identifier.split("-")[0]
|
||||
log.step(f"Installing Python {implementation_id}...")
|
||||
if implementation_id.startswith("cp"):
|
||||
free_threading = "t-macos" in python_configuration.identifier
|
||||
base_python = install_cpython(
|
||||
tmp, python_configuration.version, python_configuration.url, free_threading
|
||||
)
|
||||
|
||||
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())}"
|
||||
)
|
||||
|
||||
log.step("Setting up build environment...")
|
||||
venv_path = tmp / "venv"
|
||||
env = virtualenv(
|
||||
python_configuration.version,
|
||||
base_python,
|
||||
venv_path,
|
||||
dependency_constraint,
|
||||
use_uv=use_uv,
|
||||
)
|
||||
venv_bin_path = venv_path / "bin"
|
||||
assert venv_bin_path.exists()
|
||||
# Fix issue with site.py setting the wrong `sys.prefix`, `sys.exec_prefix`,
|
||||
# `sys.path`, ... for PyPy: https://foss.heptapod.net/pypy/pypy/issues/3175
|
||||
# Also fix an issue with the shebang of installed scripts inside the
|
||||
# testing virtualenv- see https://github.com/theacodes/nox/issues/44 and
|
||||
# https://github.com/pypa/virtualenv/issues/620
|
||||
# Also see https://github.com/python/cpython/pull/9516
|
||||
env.pop("__PYVENV_LAUNCHER__", None)
|
||||
|
||||
# we version pip ourselves, so we don't care about pip version checking
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
|
||||
# Apply our environment after pip is ready
|
||||
env = environment.as_dictionary(prev_environment=env)
|
||||
|
||||
# check what Python version we're on
|
||||
which_python = call("which", "python", env=env, capture_stdout=True).strip()
|
||||
print(which_python)
|
||||
if which_python != str(venv_bin_path / "python"):
|
||||
msg = "python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it."
|
||||
raise errors.FatalError(msg)
|
||||
call("python", "--version", env=env)
|
||||
|
||||
# check what pip version we're on
|
||||
if not use_uv:
|
||||
assert (venv_bin_path / "pip").exists()
|
||||
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
|
||||
print(which_pip)
|
||||
if which_pip != str(venv_bin_path / "pip"):
|
||||
msg = "pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it."
|
||||
raise errors.FatalError(msg)
|
||||
call("pip", "--version", env=env)
|
||||
|
||||
config_is_arm64 = python_configuration.identifier.endswith("arm64")
|
||||
config_is_universal2 = python_configuration.identifier.endswith("universal2")
|
||||
|
||||
# Set MACOSX_DEPLOYMENT_TARGET, if the user didn't set it.
|
||||
# For arm64, the minimal deployment target is 11.0.
|
||||
# On x86_64 (or universal2), use 10.9 as a default.
|
||||
# CPython 3.12.6+ needs 10.13.
|
||||
if config_is_arm64:
|
||||
default_target = "11.0"
|
||||
elif Version(python_configuration.version) >= Version("3.12"):
|
||||
default_target = "10.13"
|
||||
elif python_configuration.identifier.startswith("pp") and Version(
|
||||
python_configuration.version
|
||||
) >= Version("3.9"):
|
||||
default_target = "10.15"
|
||||
else:
|
||||
default_target = "10.9"
|
||||
env.setdefault("MACOSX_DEPLOYMENT_TARGET", default_target)
|
||||
|
||||
# This is a floor, it can't be set lower than the default_target.
|
||||
if Version(env["MACOSX_DEPLOYMENT_TARGET"]) < Version(default_target):
|
||||
log.warning(
|
||||
f"Bumping MACOSX_DEPLOYMENT_TARGET ({env['MACOSX_DEPLOYMENT_TARGET']}) to the minimum required ({default_target})."
|
||||
)
|
||||
env["MACOSX_DEPLOYMENT_TARGET"] = default_target
|
||||
|
||||
if config_is_arm64:
|
||||
# macOS 11 is the first OS with arm64 support, so the wheels
|
||||
# have that as a minimum.
|
||||
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-11.0-arm64")
|
||||
env.setdefault("ARCHFLAGS", "-arch arm64")
|
||||
elif config_is_universal2:
|
||||
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-universal2")
|
||||
env.setdefault("ARCHFLAGS", "-arch arm64 -arch x86_64")
|
||||
elif python_configuration.identifier.endswith("x86_64"):
|
||||
# even on the macos11.0 Python installer, on the x86_64 side it's
|
||||
# compatible back to 10.9.
|
||||
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-x86_64")
|
||||
env.setdefault("ARCHFLAGS", "-arch x86_64")
|
||||
|
||||
building_arm64 = config_is_arm64 or config_is_universal2
|
||||
if building_arm64 and get_macos_version() < (10, 16) and "SDKROOT" not in env:
|
||||
# xcode 12.2 or higher can build arm64 on macos 10.15 or below, but
|
||||
# needs the correct SDK selected.
|
||||
sdks = get_macos_sdks()
|
||||
|
||||
# Different versions of Xcode contain different SDK versions...
|
||||
# we're happy with anything newer than macOS 11.0
|
||||
arm64_compatible_sdks = [s for s in sdks if not s.startswith("macosx10.")]
|
||||
|
||||
if not arm64_compatible_sdks:
|
||||
log.warning(
|
||||
unwrap(
|
||||
"""
|
||||
SDK for building arm64-compatible wheels not found. You need Xcode 12.2 or later
|
||||
to build universal2 or arm64 wheels.
|
||||
"""
|
||||
)
|
||||
)
|
||||
else:
|
||||
env.setdefault("SDKROOT", arm64_compatible_sdks[0])
|
||||
|
||||
log.step("Installing build tools...")
|
||||
match build_frontend:
|
||||
case "pip":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"delocate",
|
||||
*constraint_flags(dependency_constraint),
|
||||
env=env,
|
||||
)
|
||||
case "build":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"delocate",
|
||||
"build[virtualenv]",
|
||||
*constraint_flags(dependency_constraint),
|
||||
env=env,
|
||||
)
|
||||
case "build[uv]":
|
||||
assert uv_path is not None
|
||||
call(
|
||||
uv_path,
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"delocate",
|
||||
"build[virtualenv, uv]",
|
||||
*constraint_flags(dependency_constraint),
|
||||
env=env,
|
||||
)
|
||||
case _:
|
||||
assert_never(build_frontend)
|
||||
|
||||
return base_python, env
|
||||
|
||||
|
||||
def build(options: Options, tmp_path: Path) -> None:
|
||||
python_configurations = get_python_configurations(
|
||||
options.globals.build_selector, options.globals.architectures
|
||||
)
|
||||
|
||||
if not python_configurations:
|
||||
return
|
||||
|
||||
try:
|
||||
before_all_options_identifier = python_configurations[0].identifier
|
||||
before_all_options = options.build_options(before_all_options_identifier)
|
||||
|
||||
if before_all_options.before_all:
|
||||
log.step("Running before_all...")
|
||||
env = before_all_options.environment.as_dictionary(prev_environment=os.environ)
|
||||
env.setdefault("MACOSX_DEPLOYMENT_TARGET", "10.9")
|
||||
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
|
||||
use_uv = build_frontend.name == "build[uv]"
|
||||
uv_path = find_uv()
|
||||
if use_uv and uv_path is None:
|
||||
msg = "uv not found"
|
||||
raise AssertionError(msg)
|
||||
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
|
||||
log.build_start(config.identifier)
|
||||
|
||||
identifier_tmp_dir = tmp_path / config.identifier
|
||||
identifier_tmp_dir.mkdir()
|
||||
built_wheel_dir = identifier_tmp_dir / "built_wheel"
|
||||
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
|
||||
|
||||
config_is_arm64 = config.identifier.endswith("arm64")
|
||||
config_is_universal2 = config.identifier.endswith("universal2")
|
||||
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version, tmp_dir=identifier_tmp_dir
|
||||
)
|
||||
|
||||
base_python, env = setup_python(
|
||||
identifier_tmp_dir / "build",
|
||||
config,
|
||||
constraints_path,
|
||||
build_options.environment,
|
||||
build_frontend.name,
|
||||
)
|
||||
pip_version = None if use_uv else get_pip_version(env)
|
||||
|
||||
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
|
||||
if compatible_wheel:
|
||||
log.step_end()
|
||||
print(
|
||||
f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
|
||||
)
|
||||
repaired_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
|
||||
)
|
||||
|
||||
build_env = env.copy()
|
||||
if pip_version is not None:
|
||||
build_env["VIRTUALENV_PIP"] = pip_version
|
||||
if constraints_path:
|
||||
combine_constraints(
|
||||
build_env, constraints_path, identifier_tmp_dir if use_uv else None
|
||||
)
|
||||
|
||||
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 errors.NonPlatformWheelError()
|
||||
|
||||
if build_options.repair_command:
|
||||
log.step("Repairing wheel...")
|
||||
|
||||
if config_is_universal2:
|
||||
delocate_archs = "x86_64,arm64"
|
||||
elif config_is_arm64:
|
||||
delocate_archs = "arm64"
|
||||
else:
|
||||
delocate_archs = "x86_64"
|
||||
|
||||
repair_command_prepared = prepare_command(
|
||||
build_options.repair_command,
|
||||
wheel=built_wheel,
|
||||
dest_dir=repaired_wheel_dir,
|
||||
delocate_archs=delocate_archs,
|
||||
)
|
||||
shell(repair_command_prepared, env=env)
|
||||
else:
|
||||
shutil.move(str(built_wheel), repaired_wheel_dir)
|
||||
|
||||
try:
|
||||
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
|
||||
except StopIteration:
|
||||
raise errors.RepairStepProducedNoWheelError() from None
|
||||
|
||||
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
|
||||
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
|
||||
log.step_end()
|
||||
|
||||
if build_options.test_command and build_options.test_selector(config.identifier):
|
||||
machine_arch = platform.machine()
|
||||
python_arch = call(
|
||||
"python",
|
||||
"-sSc",
|
||||
"import platform; print(platform.machine())",
|
||||
env=env,
|
||||
capture_stdout=True,
|
||||
).strip()
|
||||
testing_archs: list[Literal["x86_64", "arm64"]]
|
||||
|
||||
if config_is_arm64:
|
||||
testing_archs = ["arm64"]
|
||||
elif config_is_universal2:
|
||||
testing_archs = ["x86_64", "arm64"]
|
||||
else:
|
||||
testing_archs = ["x86_64"]
|
||||
|
||||
for testing_arch in testing_archs:
|
||||
if config_is_universal2:
|
||||
arch_specific_identifier = f"{config.identifier}:{testing_arch}"
|
||||
if not build_options.test_selector(arch_specific_identifier):
|
||||
continue
|
||||
|
||||
if machine_arch == "x86_64" and testing_arch == "arm64":
|
||||
if config_is_arm64:
|
||||
log.warning(
|
||||
unwrap(
|
||||
"""
|
||||
While arm64 wheels can be built on x86_64, they cannot be
|
||||
tested. Consider building arm64 wheels natively, if your CI
|
||||
provider offers this. To silence this warning, set
|
||||
`CIBW_TEST_SKIP: "*-macosx_arm64"`.
|
||||
"""
|
||||
)
|
||||
)
|
||||
elif config_is_universal2:
|
||||
log.warning(
|
||||
unwrap(
|
||||
"""
|
||||
While universal2 wheels can be built on x86_64, the arm64 part
|
||||
of the wheel cannot be tested on x86_64. Consider building
|
||||
universal2 wheels on an arm64 runner, if your CI provider offers
|
||||
this. Notably, an arm64 runner can also test the x86_64 part of
|
||||
the wheel, through Rosetta emulation. To silence this warning,
|
||||
set `CIBW_TEST_SKIP: "*-macosx_universal2:arm64"`.
|
||||
"""
|
||||
)
|
||||
)
|
||||
else:
|
||||
msg = "unreachable"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
# skip this test
|
||||
continue
|
||||
|
||||
is_cp38 = config.identifier.startswith("cp38-")
|
||||
if testing_arch == "arm64" and is_cp38 and python_arch != "arm64":
|
||||
log.warning(
|
||||
unwrap(
|
||||
"""
|
||||
While cibuildwheel can build CPython 3.8 universal2/arm64 wheels, we
|
||||
cannot test the arm64 part of them, even when running on an Apple
|
||||
Silicon machine. This is because we use the x86_64 installer of
|
||||
CPython 3.8. See the discussion in
|
||||
https://github.com/pypa/cibuildwheel/pull/1169 for the details. To
|
||||
silence this warning, set `CIBW_TEST_SKIP: "cp38-macosx_*:arm64"`.
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# skip this test
|
||||
continue
|
||||
|
||||
log.step(
|
||||
"Testing wheel..."
|
||||
if testing_arch == machine_arch
|
||||
else f"Testing wheel on {testing_arch}..."
|
||||
)
|
||||
|
||||
arch_prefix = []
|
||||
uv_arch_args = []
|
||||
if testing_arch != machine_arch:
|
||||
if machine_arch == "arm64" and testing_arch == "x86_64":
|
||||
# rosetta2 will provide the emulation with just the arch prefix.
|
||||
arch_prefix = ["arch", "-x86_64"]
|
||||
uv_arch_args = ["--python-platform", "x86_64-apple-darwin"]
|
||||
else:
|
||||
msg = f"don't know how to emulate {testing_arch} on {machine_arch}"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
# define a custom 'call' function that adds the arch prefix each time
|
||||
call_with_arch = functools.partial(call, *arch_prefix)
|
||||
shell_with_arch = functools.partial(call, *arch_prefix, "/bin/sh", "-c")
|
||||
|
||||
# set up a virtual environment to install and test from, to make sure
|
||||
# there are no dependencies that were pulled in at build time.
|
||||
venv_dir = identifier_tmp_dir / f"venv-test-{testing_arch}"
|
||||
virtualenv_env = virtualenv(
|
||||
config.version,
|
||||
base_python,
|
||||
venv_dir,
|
||||
None,
|
||||
use_uv=use_uv,
|
||||
env=env,
|
||||
pip_version=pip_version,
|
||||
)
|
||||
if use_uv:
|
||||
pip_install = functools.partial(call, *pip, "install", *uv_arch_args)
|
||||
else:
|
||||
pip_install = functools.partial(call_with_arch, *pip, "install")
|
||||
|
||||
virtualenv_env["MACOSX_DEPLOYMENT_TARGET"] = get_test_macosx_deployment_target()
|
||||
|
||||
virtualenv_env = build_options.test_environment.as_dictionary(
|
||||
prev_environment=virtualenv_env
|
||||
)
|
||||
|
||||
# check that we are using the Python from the virtual environment
|
||||
call_with_arch("which", "python", env=virtualenv_env)
|
||||
|
||||
if build_options.before_test:
|
||||
before_test_prepared = prepare_command(
|
||||
build_options.before_test,
|
||||
project=".",
|
||||
package=build_options.package_dir,
|
||||
)
|
||||
shell_with_arch(before_test_prepared, env=virtualenv_env)
|
||||
|
||||
# install the wheel
|
||||
if is_cp38 and python_arch == "x86_64":
|
||||
virtualenv_env_install_wheel = virtualenv_env.copy()
|
||||
virtualenv_env_install_wheel["SYSTEM_VERSION_COMPAT"] = "0"
|
||||
log.notice(
|
||||
unwrap(
|
||||
"""
|
||||
Setting SYSTEM_VERSION_COMPAT=0 to ensure CPython 3.8 can get
|
||||
correct macOS version and allow installation of wheels with
|
||||
MACOSX_DEPLOYMENT_TARGET >= 11.0.
|
||||
See https://github.com/pypa/cibuildwheel/issues/1767 for the
|
||||
details.
|
||||
"""
|
||||
)
|
||||
)
|
||||
else:
|
||||
virtualenv_env_install_wheel = virtualenv_env
|
||||
|
||||
pip_install(
|
||||
f"{repaired_wheel}{build_options.test_extras}",
|
||||
env=virtualenv_env_install_wheel,
|
||||
)
|
||||
|
||||
# test the wheel
|
||||
if build_options.test_requires:
|
||||
pip_install(
|
||||
*build_options.test_requires,
|
||||
env=virtualenv_env_install_wheel,
|
||||
)
|
||||
|
||||
# run the tests from a temp dir, with an absolute path in the command
|
||||
# (this ensures that Python runs the tests against the installed wheel
|
||||
# and not the repo code)
|
||||
test_command_prepared = prepare_command(
|
||||
build_options.test_command,
|
||||
project=Path.cwd(),
|
||||
package=build_options.package_dir.resolve(),
|
||||
wheel=repaired_wheel,
|
||||
)
|
||||
|
||||
test_cwd = identifier_tmp_dir / "test_cwd"
|
||||
|
||||
if build_options.test_sources:
|
||||
# only create test_cwd if it doesn't already exist - it
|
||||
# may have been created during a previous `testing_arch`
|
||||
if not test_cwd.exists():
|
||||
test_cwd.mkdir()
|
||||
copy_test_sources(
|
||||
build_options.test_sources,
|
||||
Path.cwd(),
|
||||
test_cwd,
|
||||
)
|
||||
else:
|
||||
# Use the test_fail.py file to raise a nice error if the user
|
||||
# tries to run tests in the cwd
|
||||
test_cwd.mkdir(exist_ok=True)
|
||||
(test_cwd / "test_fail.py").write_text(
|
||||
resources.TEST_FAIL_CWD_FILE.read_text()
|
||||
)
|
||||
|
||||
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
||||
|
||||
# we're all done here; move it to output (overwrite existing)
|
||||
output_wheel = None
|
||||
if compatible_wheel is None:
|
||||
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
|
||||
moved_wheel = move_file(repaired_wheel, output_wheel)
|
||||
if moved_wheel != output_wheel.resolve():
|
||||
log.warning(
|
||||
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
|
||||
)
|
||||
built_wheels.append(output_wheel)
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,556 @@
|
||||
import dataclasses
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
import typing
|
||||
from collections.abc import Set
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Final, TypedDict
|
||||
|
||||
from filelock import FileLock
|
||||
|
||||
from .. import errors
|
||||
from ..architecture import Architecture
|
||||
from ..environment import ParsedEnvironment
|
||||
from ..frontend import get_build_frontend_extra_flags
|
||||
from ..logger import log
|
||||
from ..options import Options
|
||||
from ..selector import BuildSelector
|
||||
from ..util import resources
|
||||
from ..util.cmd import call, shell
|
||||
from ..util.file import (
|
||||
CIBW_CACHE_PATH,
|
||||
copy_test_sources,
|
||||
download,
|
||||
extract_tar,
|
||||
extract_zip,
|
||||
move_file,
|
||||
)
|
||||
from ..util.helpers import prepare_command, unwrap, unwrap_preserving_paragraphs
|
||||
from ..util.packaging import combine_constraints, find_compatible_wheel, get_pip_version
|
||||
from ..util.python_build_standalone import (
|
||||
PythonBuildStandaloneError,
|
||||
create_python_build_standalone_environment,
|
||||
)
|
||||
from ..venv import constraint_flags, virtualenv
|
||||
|
||||
IS_WIN: Final[bool] = sys.platform.startswith("win")
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
class PythonConfiguration:
|
||||
version: str
|
||||
identifier: str
|
||||
default_pyodide_version: str
|
||||
node_version: str
|
||||
|
||||
|
||||
class PyodideXBuildEnvInfoVersionRange(TypedDict):
|
||||
min: str | None
|
||||
max: str | None
|
||||
|
||||
|
||||
class PyodideXBuildEnvInfo(TypedDict):
|
||||
version: str
|
||||
python: str
|
||||
emscripten: str
|
||||
pyodide_build: PyodideXBuildEnvInfoVersionRange
|
||||
compatible: bool
|
||||
|
||||
|
||||
@functools.cache
|
||||
def ensure_node(major_version: str) -> Path:
|
||||
with resources.NODEJS.open("rb") as f:
|
||||
loaded_file = tomllib.load(f)
|
||||
version = str(loaded_file[major_version])
|
||||
base_url = str(loaded_file["url"])
|
||||
ext = "zip" if IS_WIN else "tar.xz"
|
||||
platform = "win" if IS_WIN else ("darwin" if sys.platform.startswith("darwin") else "linux")
|
||||
linux_arch = Architecture.native_arch("linux")
|
||||
assert linux_arch is not None
|
||||
arch = {"x86_64": "x64", "i686": "x86", "aarch64": "arm64"}.get(
|
||||
linux_arch.value, linux_arch.value
|
||||
)
|
||||
name = f"node-{version}-{platform}-{arch}"
|
||||
path = CIBW_CACHE_PATH / name
|
||||
with FileLock(str(path) + ".lock"):
|
||||
if not path.exists():
|
||||
url = f"{base_url}{version}/{name}.{ext}"
|
||||
with TemporaryDirectory() as tmp_path:
|
||||
archive = Path(tmp_path) / f"{name}.{ext}"
|
||||
download(url, archive)
|
||||
if ext == "zip":
|
||||
extract_zip(archive, path.parent)
|
||||
else:
|
||||
extract_tar(archive, path.parent)
|
||||
assert path.exists()
|
||||
if not IS_WIN:
|
||||
return path / "bin"
|
||||
return path
|
||||
|
||||
|
||||
def install_emscripten(tmp: Path, version: str) -> Path:
|
||||
url = f"https://github.com/emscripten-core/emsdk/archive/refs/tags/{version}.zip"
|
||||
installation_path = CIBW_CACHE_PATH / f"emsdk-{version}"
|
||||
emsdk_path = installation_path / f"emsdk-{version}/emsdk"
|
||||
emcc_path = installation_path / f"emsdk-{version}/upstream/emscripten/emcc"
|
||||
with FileLock(f"{installation_path}.lock"):
|
||||
if installation_path.exists():
|
||||
return emcc_path
|
||||
emsdk_zip = tmp / "emsdk.zip"
|
||||
download(url, emsdk_zip)
|
||||
installation_path.mkdir()
|
||||
extract_zip(emsdk_zip, installation_path)
|
||||
call(emsdk_path, "install", version)
|
||||
call(emsdk_path, "activate", version)
|
||||
|
||||
return emcc_path
|
||||
|
||||
|
||||
def get_all_xbuildenv_version_info(env: dict[str, str]) -> list[PyodideXBuildEnvInfo]:
|
||||
xbuildenvs_info_str = call(
|
||||
"pyodide",
|
||||
"xbuildenv",
|
||||
"search",
|
||||
"--json",
|
||||
"--all",
|
||||
env=env,
|
||||
cwd=CIBW_CACHE_PATH,
|
||||
capture_stdout=True,
|
||||
).strip()
|
||||
|
||||
xbuildenvs_info = json.loads(xbuildenvs_info_str)
|
||||
|
||||
if "environments" not in xbuildenvs_info:
|
||||
msg = f"Invalid xbuildenvs info, got {xbuildenvs_info}"
|
||||
raise ValueError(msg)
|
||||
|
||||
return typing.cast(list[PyodideXBuildEnvInfo], xbuildenvs_info["environments"])
|
||||
|
||||
|
||||
def get_xbuildenv_version_info(
|
||||
env: dict[str, str], version: str, pyodide_build_version: str
|
||||
) -> PyodideXBuildEnvInfo:
|
||||
xbuildenvs_info = get_all_xbuildenv_version_info(env)
|
||||
for xbuildenv_info in xbuildenvs_info:
|
||||
if xbuildenv_info["version"] == version:
|
||||
return xbuildenv_info
|
||||
|
||||
msg = unwrap(f"""
|
||||
Could not find Pyodide cross-build environment version {version} in the available
|
||||
versions as reported by pyodide-build v{pyodide_build_version}.
|
||||
Available pyodide xbuildenv versions are:
|
||||
{", ".join(e["version"] for e in xbuildenvs_info if e["compatible"])}
|
||||
""")
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
|
||||
# The default pyodide xbuildenv version that's specified in
|
||||
# build-platforms.toml is compatible with the pyodide-build version that's
|
||||
# pinned in the bundled constraints file. But if the user changes
|
||||
# pyodide-version and/or dependency-constraints in the cibuildwheel config, we
|
||||
# need to check if the xbuildenv version is compatible with the pyodide-build
|
||||
# version.
|
||||
def validate_pyodide_build_version(
|
||||
xbuildenv_info: PyodideXBuildEnvInfo, pyodide_build_version: str
|
||||
) -> None:
|
||||
"""
|
||||
Validate the Pyodide version is compatible with the installed
|
||||
pyodide-build version.
|
||||
"""
|
||||
|
||||
pyodide_version = xbuildenv_info["version"]
|
||||
|
||||
if not xbuildenv_info["compatible"]:
|
||||
msg = unwrap_preserving_paragraphs(f"""
|
||||
The Pyodide xbuildenv version {pyodide_version} is not compatible
|
||||
with the pyodide-build version {pyodide_build_version}. Please use
|
||||
the 'pyodide xbuildenv search --all' command to find a compatible
|
||||
version.
|
||||
|
||||
Set the pyodide-build version using the `dependency-constraints`
|
||||
option, or set the Pyodide xbuildenv version using the
|
||||
`pyodide-version` option.
|
||||
""")
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
|
||||
def install_xbuildenv(env: dict[str, str], pyodide_build_version: str, pyodide_version: str) -> str:
|
||||
"""Install a particular Pyodide xbuildenv version and set a path to the Pyodide root."""
|
||||
# Since pyodide-build was unvendored from Pyodide v0.27.0, the versions of
|
||||
# pyodide-build are uncoupled from the versions of Pyodide. So, we specify
|
||||
# both the pyodide-build version and the Pyodide version in the temp path.
|
||||
xbuildenv_cache_path = CIBW_CACHE_PATH / f"pyodide-build-{pyodide_build_version}"
|
||||
pyodide_root = xbuildenv_cache_path / pyodide_version / "xbuildenv" / "pyodide-root"
|
||||
|
||||
with FileLock(CIBW_CACHE_PATH / "xbuildenv.lock"):
|
||||
if pyodide_root.exists():
|
||||
return str(pyodide_root)
|
||||
|
||||
# We don't want to mutate env but we need to delete any existing
|
||||
# PYODIDE_ROOT so copy it first.
|
||||
env = dict(env)
|
||||
env.pop("PYODIDE_ROOT", None)
|
||||
|
||||
# Install the xbuildenv
|
||||
call(
|
||||
"pyodide",
|
||||
"xbuildenv",
|
||||
"install",
|
||||
"--path",
|
||||
str(xbuildenv_cache_path),
|
||||
pyodide_version,
|
||||
env=env,
|
||||
cwd=CIBW_CACHE_PATH,
|
||||
)
|
||||
assert pyodide_root.exists()
|
||||
|
||||
return str(pyodide_root)
|
||||
|
||||
|
||||
def get_base_python(tmp: Path, python_configuration: PythonConfiguration) -> Path:
|
||||
try:
|
||||
return create_python_build_standalone_environment(
|
||||
python_version=python_configuration.version,
|
||||
temp_dir=tmp,
|
||||
cache_dir=CIBW_CACHE_PATH,
|
||||
)
|
||||
except PythonBuildStandaloneError as e:
|
||||
msg = unwrap(f"""
|
||||
Failed to create a Python build environment:
|
||||
{e}
|
||||
""")
|
||||
raise errors.FatalError(msg) from e
|
||||
|
||||
|
||||
def setup_python(
|
||||
tmp: Path,
|
||||
python_configuration: PythonConfiguration,
|
||||
constraints_path: Path | None,
|
||||
environment: ParsedEnvironment,
|
||||
user_pyodide_version: str | None,
|
||||
) -> dict[str, str]:
|
||||
log.step("Installing a base python environment...")
|
||||
base_python = get_base_python(tmp / "base", python_configuration)
|
||||
|
||||
log.step("Setting up build environment...")
|
||||
pyodide_version = user_pyodide_version or python_configuration.default_pyodide_version
|
||||
venv_path = tmp / "venv"
|
||||
env = virtualenv(python_configuration.version, base_python, venv_path, None, use_uv=False)
|
||||
venv_bin_path = venv_path / "bin"
|
||||
assert venv_bin_path.exists()
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
|
||||
# upgrade pip to the version matching our constraints
|
||||
# if necessary, reinstall it to ensure that it's available on PATH as 'pip'
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"pip",
|
||||
*constraint_flags(constraints_path),
|
||||
env=env,
|
||||
cwd=venv_path,
|
||||
)
|
||||
|
||||
env = environment.as_dictionary(prev_environment=env)
|
||||
|
||||
# check what Python version we're on
|
||||
which_python = call("which", "python", env=env, capture_stdout=True).strip()
|
||||
print(which_python)
|
||||
if which_python != str(venv_bin_path / "python"):
|
||||
msg = "python available on PATH doesn't match our venv instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it."
|
||||
raise errors.FatalError(msg)
|
||||
call("python", "--version", env=env)
|
||||
|
||||
# check what pip version we're on
|
||||
assert (venv_bin_path / "pip").exists()
|
||||
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
|
||||
print(which_pip)
|
||||
if which_pip != str(venv_bin_path / "pip"):
|
||||
msg = "pip available on PATH doesn't match our venv instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it."
|
||||
raise errors.FatalError(msg)
|
||||
call("pip", "--version", env=env)
|
||||
|
||||
log.step("Installing build tools...")
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"auditwheel-emscripten",
|
||||
"build[virtualenv]",
|
||||
"pyodide-build",
|
||||
*constraint_flags(constraints_path),
|
||||
env=env,
|
||||
)
|
||||
|
||||
pyodide_build_version = call(
|
||||
"python",
|
||||
"-c",
|
||||
"from importlib.metadata import version; print(version('pyodide-build'))",
|
||||
env=env,
|
||||
capture_stdout=True,
|
||||
).strip()
|
||||
|
||||
xbuildenv_info = get_xbuildenv_version_info(env, pyodide_version, pyodide_build_version)
|
||||
validate_pyodide_build_version(
|
||||
xbuildenv_info=xbuildenv_info,
|
||||
pyodide_build_version=pyodide_build_version,
|
||||
)
|
||||
|
||||
emscripten_version = xbuildenv_info["emscripten"]
|
||||
log.step(f"Installing Emscripten version: {emscripten_version} ...")
|
||||
emcc_path = install_emscripten(tmp, emscripten_version)
|
||||
|
||||
env["PATH"] = os.pathsep.join([str(emcc_path.parent), env["PATH"]])
|
||||
|
||||
log.step(f"Installing Pyodide xbuildenv version: {pyodide_version} ...")
|
||||
env["PYODIDE_ROOT"] = install_xbuildenv(env, pyodide_build_version, pyodide_version)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def all_python_configurations() -> list[PythonConfiguration]:
|
||||
full_python_configs = resources.read_python_configs("pyodide")
|
||||
return [PythonConfiguration(**item) for item in full_python_configs]
|
||||
|
||||
|
||||
def get_python_configurations(
|
||||
build_selector: BuildSelector,
|
||||
architectures: Set[Architecture], # noqa: ARG001
|
||||
) -> list[PythonConfiguration]:
|
||||
return [c for c in all_python_configurations() if build_selector(c.identifier)]
|
||||
|
||||
|
||||
def build(options: Options, tmp_path: Path) -> None:
|
||||
python_configurations = get_python_configurations(
|
||||
options.globals.build_selector, options.globals.architectures
|
||||
)
|
||||
|
||||
if not python_configurations:
|
||||
return
|
||||
|
||||
try:
|
||||
before_all_options_identifier = python_configurations[0].identifier
|
||||
before_all_options = options.build_options(before_all_options_identifier)
|
||||
|
||||
if before_all_options.before_all:
|
||||
log.step("Running before_all...")
|
||||
env = before_all_options.environment.as_dictionary(prev_environment=os.environ)
|
||||
before_all_prepared = prepare_command(
|
||||
before_all_options.before_all, project=".", package=before_all_options.package_dir
|
||||
)
|
||||
shell(before_all_prepared, env=env)
|
||||
|
||||
built_wheels: list[Path] = []
|
||||
|
||||
for config in python_configurations:
|
||||
build_options = options.build_options(config.identifier)
|
||||
build_frontend = build_options.build_frontend
|
||||
|
||||
if build_frontend.name == "pip":
|
||||
msg = "The pyodide platform doesn't support pip frontend"
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
log.build_start(config.identifier)
|
||||
|
||||
identifier_tmp_dir = tmp_path / config.identifier
|
||||
|
||||
built_wheel_dir = identifier_tmp_dir / "built_wheel"
|
||||
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
|
||||
identifier_tmp_dir.mkdir()
|
||||
built_wheel_dir.mkdir()
|
||||
repaired_wheel_dir.mkdir()
|
||||
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version, variant="pyodide", tmp_dir=identifier_tmp_dir
|
||||
)
|
||||
|
||||
env = setup_python(
|
||||
tmp=identifier_tmp_dir / "build",
|
||||
python_configuration=config,
|
||||
constraints_path=constraints_path,
|
||||
environment=build_options.environment,
|
||||
user_pyodide_version=build_options.pyodide_version,
|
||||
)
|
||||
pip_version = get_pip_version(env)
|
||||
# The Pyodide command line runner mounts all directories in the host
|
||||
# filesystem into the Pyodide file system, except for the custom
|
||||
# file systems /dev, /lib, /proc, and /tmp. Mounting the mount
|
||||
# points for alternate file systems causes some mysterious failure
|
||||
# of the process (it just quits without any clear error).
|
||||
#
|
||||
# Because of this, by default Pyodide can't see anything under /tmp.
|
||||
# This environment variable tells it also to mount our temp
|
||||
# directory.
|
||||
oldmounts = ""
|
||||
extra_mounts = [str(identifier_tmp_dir)]
|
||||
if Path.cwd().is_relative_to("/tmp"):
|
||||
extra_mounts.append(str(Path.cwd()))
|
||||
|
||||
if "_PYODIDE_EXTRA_MOUNTS" in env:
|
||||
oldmounts = env["_PYODIDE_EXTRA_MOUNTS"] + ":"
|
||||
env["_PYODIDE_EXTRA_MOUNTS"] = oldmounts + ":".join(extra_mounts)
|
||||
|
||||
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
|
||||
if compatible_wheel:
|
||||
log.step_end()
|
||||
print(
|
||||
f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
|
||||
)
|
||||
built_wheel = compatible_wheel
|
||||
else:
|
||||
if build_options.before_build:
|
||||
log.step("Running before_build...")
|
||||
before_build_prepared = prepare_command(
|
||||
build_options.before_build, project=".", package=build_options.package_dir
|
||||
)
|
||||
shell(before_build_prepared, env=env)
|
||||
|
||||
log.step("Building wheel...")
|
||||
|
||||
extra_flags = get_build_frontend_extra_flags(
|
||||
build_frontend, build_options.build_verbosity, build_options.config_settings
|
||||
)
|
||||
|
||||
build_env = env.copy()
|
||||
if constraints_path:
|
||||
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,
|
||||
)
|
||||
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise errors.NonPlatformWheelError()
|
||||
|
||||
if build_options.repair_command:
|
||||
log.step("Repairing wheel...")
|
||||
|
||||
repair_command_prepared = prepare_command(
|
||||
build_options.repair_command,
|
||||
wheel=built_wheel,
|
||||
dest_dir=repaired_wheel_dir,
|
||||
)
|
||||
shell(repair_command_prepared, env=env)
|
||||
log.step_end()
|
||||
else:
|
||||
shutil.move(str(built_wheel), repaired_wheel_dir)
|
||||
|
||||
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
|
||||
|
||||
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
|
||||
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
|
||||
if build_options.test_command and build_options.test_selector(config.identifier):
|
||||
log.step("Testing wheel...")
|
||||
|
||||
venv_dir = identifier_tmp_dir / "venv-test"
|
||||
# set up a virtual environment to install and test from, to make sure
|
||||
# there are no dependencies that were pulled in at build time.
|
||||
|
||||
virtualenv_env = env.copy()
|
||||
virtualenv_env["PATH"] = os.pathsep.join(
|
||||
[
|
||||
str(ensure_node(config.node_version)),
|
||||
virtualenv_env["PATH"],
|
||||
]
|
||||
)
|
||||
|
||||
# pyodide venv uses virtualenv under the hood
|
||||
# use the pip embedded with virtualenv & disable network updates
|
||||
virtualenv_create_env = virtualenv_env.copy()
|
||||
virtualenv_create_env["VIRTUALENV_PIP"] = pip_version
|
||||
virtualenv_create_env["VIRTUALENV_NO_PERIODIC_UPDATE"] = "1"
|
||||
|
||||
call("pyodide", "venv", venv_dir, env=virtualenv_create_env)
|
||||
|
||||
virtualenv_env["PATH"] = os.pathsep.join(
|
||||
[
|
||||
str(venv_dir / "bin"),
|
||||
virtualenv_env["PATH"],
|
||||
]
|
||||
)
|
||||
virtualenv_env["VIRTUAL_ENV"] = str(venv_dir)
|
||||
|
||||
virtualenv_env = build_options.test_environment.as_dictionary(
|
||||
prev_environment=virtualenv_env
|
||||
)
|
||||
|
||||
# check that we are using the Python from the virtual environment
|
||||
call("which", "python", env=virtualenv_env)
|
||||
|
||||
if build_options.before_test:
|
||||
before_test_prepared = prepare_command(
|
||||
build_options.before_test,
|
||||
project=".",
|
||||
package=build_options.package_dir,
|
||||
wheel=repaired_wheel,
|
||||
)
|
||||
shell(before_test_prepared, env=virtualenv_env)
|
||||
|
||||
# install the wheel
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
f"{repaired_wheel}{build_options.test_extras}",
|
||||
env=virtualenv_env,
|
||||
)
|
||||
|
||||
# test the wheel
|
||||
if build_options.test_requires:
|
||||
call("pip", "install", *build_options.test_requires, env=virtualenv_env)
|
||||
|
||||
# run the tests from a temp dir, with an absolute path in the command
|
||||
# (this ensures that Python runs the tests against the installed wheel
|
||||
# and not the repo code)
|
||||
test_command_prepared = prepare_command(
|
||||
build_options.test_command,
|
||||
project=Path.cwd(),
|
||||
package=build_options.package_dir.resolve(),
|
||||
)
|
||||
|
||||
test_cwd = identifier_tmp_dir / "test_cwd"
|
||||
test_cwd.mkdir(exist_ok=True)
|
||||
|
||||
if build_options.test_sources:
|
||||
copy_test_sources(
|
||||
build_options.test_sources,
|
||||
Path.cwd(),
|
||||
test_cwd,
|
||||
)
|
||||
else:
|
||||
# Use the test_fail.py file to raise a nice error if the user
|
||||
# tries to run tests in the cwd
|
||||
(test_cwd / "test_fail.py").write_text(resources.TEST_FAIL_CWD_FILE.read_text())
|
||||
|
||||
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
||||
|
||||
# we're all done here; move it to output (overwrite existing)
|
||||
output_wheel: Path | None = None
|
||||
if compatible_wheel is None:
|
||||
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
|
||||
moved_wheel = move_file(repaired_wheel, output_wheel)
|
||||
if moved_wheel != output_wheel.resolve():
|
||||
log.warning(
|
||||
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
|
||||
)
|
||||
built_wheels.append(output_wheel)
|
||||
log.build_end(output_wheel)
|
||||
|
||||
except subprocess.CalledProcessError as error:
|
||||
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
||||
raise errors.FatalError(msg) from error
|
||||
@@ -1,50 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
import platform as platform_module
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from collections.abc import MutableMapping, Sequence, Set
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from collections.abc import MutableMapping, Set
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
from typing import assert_never
|
||||
|
||||
from filelock import FileLock
|
||||
from packaging.version import Version
|
||||
|
||||
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,
|
||||
download,
|
||||
find_compatible_wheel,
|
||||
get_build_verbosity_extra_flags,
|
||||
get_pip_version,
|
||||
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 combine_constraints, find_compatible_wheel, get_pip_version
|
||||
from ..venv import constraint_flags, find_uv, virtualenv
|
||||
|
||||
|
||||
def get_nuget_args(version: str, arch: str, output_directory: Path) -> list[str]:
|
||||
def get_nuget_args(
|
||||
version: str, arch: str, free_threaded: bool, output_directory: Path
|
||||
) -> list[str]:
|
||||
package_name = {
|
||||
"32": "pythonx86",
|
||||
"64": "python",
|
||||
@@ -53,6 +38,8 @@ def get_nuget_args(version: str, arch: str, output_directory: Path) -> list[str]
|
||||
"x86": "pythonx86",
|
||||
"AMD64": "python",
|
||||
}[arch]
|
||||
if free_threaded:
|
||||
package_name = f"{package_name}-freethreaded"
|
||||
return [
|
||||
package_name,
|
||||
"-Version",
|
||||
@@ -64,7 +51,7 @@ def get_nuget_args(version: str, arch: str, output_directory: Path) -> list[str]
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
class PythonConfiguration:
|
||||
version: str
|
||||
arch: str
|
||||
@@ -72,13 +59,16 @@ class PythonConfiguration:
|
||||
url: str | None = None
|
||||
|
||||
|
||||
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}
|
||||
|
||||
@@ -92,12 +82,7 @@ def get_python_configurations(
|
||||
return python_configurations
|
||||
|
||||
|
||||
def extract_zip(zip_src: Path, dest: Path) -> None:
|
||||
with ZipFile(zip_src) as zip_:
|
||||
zip_.extractall(dest)
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
@cache
|
||||
def _ensure_nuget() -> Path:
|
||||
nuget = CIBW_CACHE_PATH / "nuget.exe"
|
||||
with FileLock(str(nuget) + ".lock"):
|
||||
@@ -106,11 +91,16 @@ def _ensure_nuget() -> Path:
|
||||
return nuget
|
||||
|
||||
|
||||
def install_cpython(version: str, arch: str) -> Path:
|
||||
def install_cpython(configuration: PythonConfiguration, arch: str | None = None) -> Path:
|
||||
version = configuration.version
|
||||
free_threaded = "t-" in configuration.identifier
|
||||
if arch is None:
|
||||
arch = configuration.arch
|
||||
base_output_dir = CIBW_CACHE_PATH / "nuget-cpython"
|
||||
nuget_args = get_nuget_args(version, arch, base_output_dir)
|
||||
nuget_args = get_nuget_args(version, arch, free_threaded, base_output_dir)
|
||||
installation_path = base_output_dir / (nuget_args[0] + "." + version) / "tools"
|
||||
with FileLock(str(base_output_dir) + f"-{version}-{arch}.lock"):
|
||||
free_threaded_str = "-freethreaded" if free_threaded else ""
|
||||
with FileLock(str(base_output_dir) + f"-{version}{free_threaded_str}-{arch}.lock"):
|
||||
if not installation_path.exists():
|
||||
nuget = _ensure_nuget()
|
||||
call(nuget, "install", *nuget_args)
|
||||
@@ -134,6 +124,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,
|
||||
@@ -148,6 +152,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(
|
||||
@@ -211,138 +225,150 @@ 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,
|
||||
) -> dict[str, str]:
|
||||
) -> tuple[Path, dict[str, str]]:
|
||||
tmp.mkdir()
|
||||
implementation_id = python_configuration.identifier.split("-")[0]
|
||||
python_libs_base = None
|
||||
log.step(f"Installing Python {implementation_id}...")
|
||||
if implementation_id.startswith("cp"):
|
||||
native_arch = platform_module.machine()
|
||||
base_python = install_cpython(python_configuration)
|
||||
if python_configuration.arch == "ARM64" != native_arch:
|
||||
# To cross-compile for ARM64, we need a native CPython to run the
|
||||
# build, and a copy of the ARM64 import libraries ('.\libs\*.lib')
|
||||
# for any extension modules.
|
||||
python_libs_base = install_cpython(
|
||||
python_configuration.version, python_configuration.arch
|
||||
)
|
||||
python_libs_base = python_libs_base.parent / "libs"
|
||||
python_libs_base = base_python.parent / "libs"
|
||||
log.step(f"Installing native Python {native_arch} for cross-compilation...")
|
||||
base_python = install_cpython(python_configuration.version, native_arch)
|
||||
else:
|
||||
base_python = install_cpython(python_configuration.version, python_configuration.arch)
|
||||
base_python = install_cpython(python_configuration, arch=native_arch)
|
||||
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()
|
||||
|
||||
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...")
|
||||
venv_path = tmp / "venv"
|
||||
env = virtualenv(base_python, venv_path, dependency_constraint_flags)
|
||||
env = virtualenv(
|
||||
python_configuration.version,
|
||||
base_python,
|
||||
venv_path,
|
||||
dependency_constraint,
|
||||
use_uv=use_uv,
|
||||
)
|
||||
|
||||
# set up environment variables for run_with_env
|
||||
env["PYTHON_VERSION"] = python_configuration.version
|
||||
env["PYTHON_ARCH"] = python_configuration.arch
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
|
||||
# pip older than 21.3 builds executables such as pip.exe for x64 platform.
|
||||
# The first re-install of pip updates pip module but builds pip.exe using
|
||||
# the old pip which still generates x64 executable. But the second
|
||||
# re-install uses updated pip and correctly builds pip.exe for the target.
|
||||
# This can be removed once ARM64 Pythons (currently 3.9 and 3.10) bundle
|
||||
# pip versions newer than 21.3.
|
||||
if python_configuration.arch == "ARM64" and Version(get_pip_version(env)) < Version("21.3"):
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--force-reinstall",
|
||||
"--upgrade",
|
||||
"pip",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
cwd=venv_path,
|
||||
)
|
||||
|
||||
# upgrade pip to the version matching our constraints
|
||||
# if necessary, reinstall it to ensure that it's available on PATH as 'pip.exe'
|
||||
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)
|
||||
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)
|
||||
where_python = call("where", "python", env=env, capture_stdout=True).splitlines()[0].strip()
|
||||
if where_python != str(venv_path / "Scripts" / "python.exe"):
|
||||
print(
|
||||
"cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# check what pip version we're on
|
||||
assert (venv_path / "Scripts" / "pip.exe").exists()
|
||||
where_pip = call("where", "pip", env=env, capture_stdout=True).splitlines()[0].strip()
|
||||
if where_pip.strip() != str(venv_path / "Scripts" / "pip.exe"):
|
||||
print(
|
||||
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
call("pip", "--version", env=env)
|
||||
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 == "pip":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"setuptools",
|
||||
"wheel",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
)
|
||||
elif build_frontend == "build":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"build[virtualenv]",
|
||||
*dependency_constraint_flags,
|
||||
env=env,
|
||||
)
|
||||
else:
|
||||
assert_never(build_frontend)
|
||||
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)
|
||||
|
||||
return env
|
||||
if implementation_id.startswith("gp"):
|
||||
# GraalPy fails to discover compilers, setup the relevant environment
|
||||
# variables. Adapted from
|
||||
# https://github.com/microsoft/vswhere/wiki/Start-Developer-Command-Prompt
|
||||
# Remove when https://github.com/oracle/graalpython/issues/492 is fixed.
|
||||
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
|
||||
|
||||
|
||||
def build(options: Options, tmp_path: Path) -> None:
|
||||
@@ -369,7 +395,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")
|
||||
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
|
||||
@@ -377,21 +405,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
|
||||
env = setup_python(
|
||||
base_python, env = setup_python(
|
||||
identifier_tmp_dir / "build",
|
||||
config,
|
||||
dependency_constraint_flags,
|
||||
constraints_path,
|
||||
build_options.environment,
|
||||
build_frontend.name,
|
||||
)
|
||||
pip_version = None if use_uv else get_pip_version(env)
|
||||
|
||||
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
|
||||
if compatible_wheel:
|
||||
@@ -414,49 +441,56 @@ 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
|
||||
|
||||
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=env,
|
||||
if (
|
||||
config.identifier.startswith("gp")
|
||||
and build_frontend.name == "build"
|
||||
and "--no-isolation" not in extra_flags
|
||||
and "-n" not in extra_flags
|
||||
):
|
||||
# GraalPy 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
|
||||
# fixed upstream.
|
||||
log.notice(
|
||||
"Disabling build isolation to workaround GraalPy bug. If the build fails, consider using pip or build[uv] as build frontend."
|
||||
)
|
||||
elif build_frontend.name == "build":
|
||||
if not 0 <= build_options.build_verbosity < 2:
|
||||
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
|
||||
log.warning(msg)
|
||||
build_env = env.copy()
|
||||
if build_options.dependency_constraints:
|
||||
constraints_path = (
|
||||
build_options.dependency_constraints.get_for_python_version(
|
||||
config.version
|
||||
)
|
||||
shell("graalpy -m pip install setuptools wheel", env=env)
|
||||
extra_flags = [*extra_flags, "-n"]
|
||||
|
||||
build_env = env.copy()
|
||||
if pip_version is not None:
|
||||
build_env["VIRTUALENV_PIP"] = pip_version
|
||||
|
||||
if constraints_path:
|
||||
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=build_env,
|
||||
)
|
||||
# Bug in pip <= 21.1.3 - we can't have a space in the
|
||||
# constraints file, and pip doesn't support drive letters
|
||||
# in uhi. After probably pip 21.2, we can use uri. For
|
||||
# now, use a temporary file.
|
||||
if " " in str(constraints_path):
|
||||
assert " " not in str(identifier_tmp_dir)
|
||||
tmp_file = identifier_tmp_dir / "constraints.txt"
|
||||
tmp_file.write_bytes(constraints_path.read_bytes())
|
||||
constraints_path = tmp_file
|
||||
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")
|
||||
|
||||
build_env["PIP_CONSTRAINT"] = str(constraints_path)
|
||||
build_env["VIRTUALENV_PIP"] = get_pip_version(env)
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
@@ -467,8 +501,8 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
*extra_flags,
|
||||
env=build_env,
|
||||
)
|
||||
else:
|
||||
assert_never(build_frontend)
|
||||
case _:
|
||||
assert_never(build_frontend)
|
||||
|
||||
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
||||
|
||||
@@ -476,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...")
|
||||
@@ -489,10 +523,13 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
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():
|
||||
@@ -510,19 +547,19 @@ 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.
|
||||
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,
|
||||
)
|
||||
|
||||
# Use --no-download to ensure determinism by using seed libraries
|
||||
# built into virtualenv
|
||||
call("python", "-m", "virtualenv", "--no-download", 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
|
||||
)
|
||||
|
||||
# check that we are using the Python from the virtual environment
|
||||
@@ -536,9 +573,11 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
)
|
||||
shell(before_test_prepared, env=virtualenv_env)
|
||||
|
||||
pip = ["uv", "pip"] if use_uv else ["pip"]
|
||||
|
||||
# install the wheel
|
||||
call(
|
||||
"pip",
|
||||
*pip,
|
||||
"install",
|
||||
str(repaired_wheel) + build_options.test_extras,
|
||||
env=virtualenv_env,
|
||||
@@ -546,39 +585,50 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
|
||||
# test the wheel
|
||||
if build_options.test_requires:
|
||||
call("pip", "install", *build_options.test_requires, env=virtualenv_env)
|
||||
call(*pip, "install", *build_options.test_requires, env=virtualenv_env)
|
||||
|
||||
# run the tests from a temp dir, with an absolute path in the command
|
||||
# (this ensures that Python runs the tests against the installed wheel
|
||||
# and not the repo code)
|
||||
test_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:
|
||||
with suppress(FileNotFoundError):
|
||||
(build_options.output_dir / repaired_wheel.name).unlink()
|
||||
|
||||
shutil.move(str(repaired_wheel), build_options.output_dir)
|
||||
built_wheels.append(build_options.output_dir / repaired_wheel.name)
|
||||
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
|
||||
moved_wheel = move_file(repaired_wheel, output_wheel)
|
||||
if moved_wheel != output_wheel.resolve():
|
||||
log.warning(
|
||||
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
|
||||
)
|
||||
built_wheels.append(output_wheel)
|
||||
|
||||
# clean up
|
||||
# (we ignore errors because occasionally Windows fails to unlink a file and we
|
||||
# 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:
|
||||
log.step_end_with_error(
|
||||
f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}"
|
||||
)
|
||||
sys.exit(1)
|
||||
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,34 +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__"}
|
||||
if len(consts) != 1:
|
||||
return False
|
||||
|
||||
return True
|
||||
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):
|
||||
@@ -68,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(version))):
|
||||
if unnested or name_main_unnested:
|
||||
self.requires_python = version
|
||||
|
||||
|
||||
def setup_py_python_requires(content: str) -> str | None:
|
||||
@@ -86,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()
|
||||
@@ -108,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,83 @@
|
||||
# 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 ###############################################################
|
||||
#
|
||||
# We don't change the actual sys.base_prefix and base_exec_prefix, because that
|
||||
# could have unpredictable effects. Instead, we change the internal variables
|
||||
# used to generate sysconfig.get_path("include").
|
||||
exec_prefix = sysconfig.get_config_var("exec_prefix")
|
||||
sysconfig._BASE_PREFIX = sysconfig._BASE_EXEC_PREFIX = exec_prefix # type: ignore[attr-defined]
|
||||
|
||||
# Reload the sysconfigdata file, generating its name from sys.abiflags,
|
||||
# sys.platform, and sys.implementation._multiarch.
|
||||
sysconfig._init_config_vars() # type: ignore[attr-defined]
|
||||
|
||||
# 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,93 +1,149 @@
|
||||
[linux]
|
||||
python_configurations = [
|
||||
{ identifier = "cp36-manylinux_x86_64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
|
||||
{ identifier = "cp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
|
||||
{ identifier = "cp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-manylinux_x86_64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-manylinux_x86_64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-manylinux_x86_64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "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 = "cp313-manylinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-manylinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-manylinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-manylinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-manylinux_i686", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-manylinux_i686", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-manylinux_i686", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-manylinux_i686", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "pp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
|
||||
{ identifier = "cp313-manylinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-manylinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-manylinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "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 = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-manylinux_aarch64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-manylinux_aarch64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "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 = "cp313-manylinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-manylinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-manylinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-manylinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-manylinux_ppc64le", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-manylinux_ppc64le", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-manylinux_ppc64le", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-manylinux_ppc64le", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "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 = "cp313-manylinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-manylinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-manylinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-manylinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-manylinux_s390x", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-manylinux_s390x", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-manylinux_s390x", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-manylinux_s390x", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "pp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
|
||||
{ identifier = "cp313-manylinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-manylinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-manylinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-manylinux_armv7l", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-manylinux_armv7l", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-manylinux_armv7l", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-manylinux_armv7l", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-manylinux_armv7l", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "cp313-manylinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-manylinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-manylinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-manylinux_riscv64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-manylinux_riscv64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-manylinux_riscv64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-manylinux_riscv64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-manylinux_riscv64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "cp313-manylinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-manylinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-manylinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-manylinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "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 = "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" },
|
||||
{ identifier = "cp311-musllinux_x86_64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-musllinux_x86_64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "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 = "cp313-musllinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-musllinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-musllinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-musllinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-musllinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-musllinux_i686", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-musllinux_i686", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-musllinux_i686", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-musllinux_i686", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "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 = "cp313-musllinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-musllinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-musllinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-musllinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-musllinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-musllinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-musllinux_aarch64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-musllinux_aarch64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-musllinux_aarch64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "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 = "cp313-musllinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-musllinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-musllinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-musllinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-musllinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-musllinux_ppc64le", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-musllinux_ppc64le", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-musllinux_ppc64le", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-musllinux_ppc64le", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "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 = "cp313-musllinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-musllinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-musllinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-musllinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-musllinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-musllinux_s390x", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-musllinux_s390x", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-musllinux_s390x", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-musllinux_s390x", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "cp313-musllinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-musllinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-musllinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-musllinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-musllinux_armv7l", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-musllinux_armv7l", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-musllinux_armv7l", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-musllinux_armv7l", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-musllinux_armv7l", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "cp313-musllinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-musllinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-musllinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-musllinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
{ identifier = "cp38-musllinux_riscv64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp39-musllinux_riscv64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
|
||||
{ identifier = "cp310-musllinux_riscv64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
|
||||
{ identifier = "cp311-musllinux_riscv64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
|
||||
{ identifier = "cp312-musllinux_riscv64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
|
||||
{ identifier = "cp313-musllinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ identifier = "cp313t-musllinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
|
||||
{ identifier = "cp314-musllinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
|
||||
{ identifier = "cp314t-musllinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
|
||||
]
|
||||
|
||||
[macos]
|
||||
python_configurations = [
|
||||
{ identifier = "cp36-macosx_x86_64", version = "3.6", url = "https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg" },
|
||||
{ identifier = "cp37-macosx_x86_64", version = "3.7", url = "https://www.python.org/ftp/python/3.7.9/python-3.7.9-macosx10.9.pkg" },
|
||||
{ identifier = "cp38-macosx_x86_64", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" },
|
||||
{ identifier = "cp38-macosx_arm64", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" },
|
||||
{ identifier = "cp38-macosx_universal2", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" },
|
||||
@@ -97,43 +153,89 @@ python_configurations = [
|
||||
{ identifier = "cp310-macosx_x86_64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" },
|
||||
{ identifier = "cp310-macosx_arm64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" },
|
||||
{ identifier = "cp310-macosx_universal2", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" },
|
||||
{ identifier = "cp311-macosx_x86_64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.7/python-3.11.7-macos11.pkg" },
|
||||
{ identifier = "cp311-macosx_arm64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.7/python-3.11.7-macos11.pkg" },
|
||||
{ identifier = "cp311-macosx_universal2", version = "3.11", url = "https://www.python.org/ftp/python/3.11.7/python-3.11.7-macos11.pkg" },
|
||||
{ identifier = "cp312-macosx_x86_64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.1/python-3.12.1-macos11.pkg" },
|
||||
{ identifier = "cp312-macosx_arm64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.1/python-3.12.1-macos11.pkg" },
|
||||
{ identifier = "cp312-macosx_universal2", version = "3.12", url = "https://www.python.org/ftp/python/3.12.1/python-3.12.1-macos11.pkg" },
|
||||
{ identifier = "pp37-macosx_x86_64", version = "3.7", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.9-osx64.tar.bz2" },
|
||||
{ identifier = "cp311-macosx_x86_64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg" },
|
||||
{ identifier = "cp311-macosx_arm64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg" },
|
||||
{ identifier = "cp311-macosx_universal2", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg" },
|
||||
{ identifier = "cp312-macosx_x86_64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" },
|
||||
{ identifier = "cp312-macosx_arm64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" },
|
||||
{ identifier = "cp312-macosx_universal2", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" },
|
||||
{ identifier = "cp313-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
|
||||
{ identifier = "cp313-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
|
||||
{ identifier = "cp313-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
|
||||
{ identifier = "cp313t-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
|
||||
{ identifier = "cp313t-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
|
||||
{ identifier = "cp313t-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
|
||||
{ identifier = "cp314-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-macos11.pkg" },
|
||||
{ identifier = "cp314-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-macos11.pkg" },
|
||||
{ identifier = "cp314-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-macos11.pkg" },
|
||||
{ identifier = "cp314t-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-macos11.pkg" },
|
||||
{ identifier = "cp314t-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-macos11.pkg" },
|
||||
{ identifier = "cp314t-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-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.15-macos_x86_64.tar.bz2" },
|
||||
{ identifier = "pp39-macosx_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.15-macos_arm64.tar.bz2" },
|
||||
{ identifier = "pp310-macosx_x86_64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.15-macos_x86_64.tar.bz2" },
|
||||
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.15-macos_arm64.tar.bz2" },
|
||||
{ identifier = "pp39-macosx_x86_64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_x86_64.tar.bz2" },
|
||||
{ identifier = "pp39-macosx_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_arm64.tar.bz2" },
|
||||
{ identifier = "pp310-macosx_x86_64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_x86_64.tar.bz2" },
|
||||
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_arm64.tar.bz2" },
|
||||
{ identifier = "pp311-macosx_x86_64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.20-macos_x86_64.tar.bz2" },
|
||||
{ identifier = "pp311-macosx_arm64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.20-macos_arm64.tar.bz2" },
|
||||
{ identifier = "gp311_242-macosx_x86_64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-macos-amd64.tar.gz" },
|
||||
{ identifier = "gp311_242-macosx_arm64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-macos-aarch64.tar.gz" },
|
||||
]
|
||||
|
||||
[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.7", arch = "32" },
|
||||
{ identifier = "cp311-win_amd64", version = "3.11.7", arch = "64" },
|
||||
{ identifier = "cp312-win32", version = "3.12.1", arch = "32" },
|
||||
{ identifier = "cp312-win_amd64", version = "3.12.1", arch = "64" },
|
||||
{ identifier = "cp311-win32", version = "3.11.9", arch = "32" },
|
||||
{ identifier = "cp311-win_amd64", version = "3.11.9", arch = "64" },
|
||||
{ identifier = "cp312-win32", version = "3.12.10", arch = "32" },
|
||||
{ identifier = "cp312-win_amd64", version = "3.12.10", arch = "64" },
|
||||
{ identifier = "cp313-win32", version = "3.13.5", arch = "32" },
|
||||
{ identifier = "cp313t-win32", version = "3.13.5", arch = "32" },
|
||||
{ identifier = "cp313-win_amd64", version = "3.13.5", arch = "64" },
|
||||
{ identifier = "cp313t-win_amd64", version = "3.13.5", arch = "64" },
|
||||
{ identifier = "cp314-win32", version = "3.14.0-rc1", arch = "32" },
|
||||
{ identifier = "cp314t-win32", version = "3.14.0-rc1", arch = "32" },
|
||||
{ identifier = "cp314-win_amd64", version = "3.14.0-rc1", arch = "64" },
|
||||
{ identifier = "cp314t-win_amd64", version = "3.14.0-rc1", arch = "64" },
|
||||
{ identifier = "cp39-win_arm64", version = "3.9.10", arch = "ARM64" },
|
||||
{ identifier = "cp310-win_arm64", version = "3.10.11", arch = "ARM64" },
|
||||
{ identifier = "cp311-win_arm64", version = "3.11.7", arch = "ARM64" },
|
||||
{ identifier = "cp312-win_arm64", version = "3.12.1", arch = "ARM64" },
|
||||
{ identifier = "pp37-win_amd64", version = "3.7", arch = "64", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.9-win64.zip" },
|
||||
{ identifier = "cp311-win_arm64", version = "3.11.9", arch = "ARM64" },
|
||||
{ identifier = "cp312-win_arm64", version = "3.12.10", arch = "ARM64" },
|
||||
{ identifier = "cp313-win_arm64", version = "3.13.5", arch = "ARM64" },
|
||||
{ identifier = "cp313t-win_arm64", version = "3.13.5", arch = "ARM64" },
|
||||
{ identifier = "cp314-win_arm64", version = "3.14.0-rc1", arch = "ARM64" },
|
||||
{ identifier = "cp314t-win_arm64", version = "3.14.0-rc1", arch = "ARM64" },
|
||||
{ identifier = "pp38-win_amd64", version = "3.8", arch = "64", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip" },
|
||||
{ identifier = "pp39-win_amd64", version = "3.9", arch = "64", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.15-win64.zip" },
|
||||
{ identifier = "pp310-win_amd64", version = "3.10", arch = "64", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.15-win64.zip" },
|
||||
{ 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.19-win64.zip" },
|
||||
{ identifier = "pp311-win_amd64", version = "3.11", arch = "64", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.20-win64.zip" },
|
||||
{ identifier = "gp311_242-win_amd64", version = "3.11", arch = "64", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-windows-amd64.zip" },
|
||||
]
|
||||
|
||||
[pyodide]
|
||||
python_configurations = [
|
||||
{ identifier = "cp312-pyodide_wasm32", version = "3.12", default_pyodide_version = "0.27.7", node_version = "v22" },
|
||||
{ identifier = "cp313-pyodide_wasm32", version = "3.13", default_pyodide_version = "0.28.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.5+20250722.214220/python-3.13.5+20250722.214220-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.5+20250722.214220/python-3.13.5+20250722.214220-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-b10/Python-3.13-iOS-support.b10.tar.gz" },
|
||||
{ identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b10/Python-3.13-iOS-support.b10.tar.gz" },
|
||||
{ identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b10/Python-3.13-iOS-support.b10.tar.gz" },
|
||||
{ identifier = "cp314-ios_arm64_iphoneos", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b6/Python-3.14-iOS-support.b6.tar.gz" },
|
||||
{ identifier = "cp314-ios_x86_64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b6/Python-3.14-iOS-support.b6.tar.gz" },
|
||||
{ identifier = "cp314-ios_arm64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b6/Python-3.14-iOS-support.b6.tar.gz" },
|
||||
]
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$defs": {
|
||||
"inherit": {
|
||||
"enum": [
|
||||
"none",
|
||||
"prepend",
|
||||
"append"
|
||||
],
|
||||
"default": "none",
|
||||
"description": "How to inherit the parent's value."
|
||||
},
|
||||
"enable": {
|
||||
"enum": [
|
||||
"cpython-experimental-riscv64",
|
||||
"cpython-freethreading",
|
||||
"cpython-prerelease",
|
||||
"graalpy",
|
||||
"pyodide-prerelease",
|
||||
"pypy",
|
||||
"pypy-eol"
|
||||
]
|
||||
},
|
||||
"description": "A Python version or flavor to enable."
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "cibuildwheel's settings.",
|
||||
"type": "object",
|
||||
@@ -85,12 +108,13 @@
|
||||
},
|
||||
"build-frontend": {
|
||||
"default": "default",
|
||||
"description": "Set the tool to use to build, either \"pip\" (default for now) or \"build\"",
|
||||
"description": "Set the tool to use to build, either \"build\" (default), \"build[uv]\", or \"pip\"",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"pip",
|
||||
"build",
|
||||
"build[uv]",
|
||||
"default"
|
||||
]
|
||||
},
|
||||
@@ -102,6 +126,10 @@
|
||||
"type": "string",
|
||||
"pattern": "^build; ?args:"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^build\\[uv\\]; ?args:"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -112,7 +140,8 @@
|
||||
"name": {
|
||||
"enum": [
|
||||
"pip",
|
||||
"build"
|
||||
"build",
|
||||
"build[uv]"
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
@@ -208,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": [
|
||||
@@ -249,6 +331,11 @@
|
||||
"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",
|
||||
@@ -274,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",
|
||||
@@ -289,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",
|
||||
@@ -299,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",
|
||||
@@ -309,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": [
|
||||
@@ -369,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": [
|
||||
@@ -399,6 +551,24 @@
|
||||
],
|
||||
"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"
|
||||
},
|
||||
"overrides": {
|
||||
"type": "array",
|
||||
"description": "An overrides array",
|
||||
@@ -423,6 +593,54 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"inherit": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"before-all": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"before-build": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"before-test": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"config-settings": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"container-engine": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"environment": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"environment-pass": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"repair-wheel-command": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"test-command": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"test-extras": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"test-sources": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"test-requires": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"test-environment": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"before-all": {
|
||||
"$ref": "#/properties/before-all"
|
||||
},
|
||||
@@ -441,6 +659,9 @@
|
||||
"config-settings": {
|
||||
"$ref": "#/properties/config-settings"
|
||||
},
|
||||
"container-engine": {
|
||||
"$ref": "#/properties/container-engine"
|
||||
},
|
||||
"dependency-versions": {
|
||||
"$ref": "#/properties/dependency-versions"
|
||||
},
|
||||
@@ -453,6 +674,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"
|
||||
},
|
||||
@@ -468,6 +692,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"
|
||||
},
|
||||
@@ -477,18 +704,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"
|
||||
},
|
||||
@@ -498,8 +737,17 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -529,6 +777,9 @@
|
||||
"config-settings": {
|
||||
"$ref": "#/properties/config-settings"
|
||||
},
|
||||
"container-engine": {
|
||||
"$ref": "#/properties/container-engine"
|
||||
},
|
||||
"environment": {
|
||||
"$ref": "#/properties/environment"
|
||||
},
|
||||
@@ -538,6 +789,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"
|
||||
},
|
||||
@@ -553,6 +807,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"
|
||||
},
|
||||
@@ -562,18 +819,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": [
|
||||
@@ -596,8 +865,17 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -632,6 +910,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"
|
||||
},
|
||||
@@ -641,8 +925,17 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -677,6 +970,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": [
|
||||
@@ -699,8 +998,197 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pyodide": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"archs": {
|
||||
"$ref": "#/properties/archs"
|
||||
},
|
||||
"before-all": {
|
||||
"$ref": "#/properties/before-all"
|
||||
},
|
||||
"before-build": {
|
||||
"$ref": "#/properties/before-build"
|
||||
},
|
||||
"before-test": {
|
||||
"$ref": "#/properties/before-test"
|
||||
},
|
||||
"build-frontend": {
|
||||
"$ref": "#/properties/build-frontend"
|
||||
},
|
||||
"build-verbosity": {
|
||||
"$ref": "#/properties/build-verbosity"
|
||||
},
|
||||
"config-settings": {
|
||||
"$ref": "#/properties/config-settings"
|
||||
},
|
||||
"dependency-versions": {
|
||||
"$ref": "#/properties/dependency-versions"
|
||||
},
|
||||
"environment": {
|
||||
"$ref": "#/properties/environment"
|
||||
},
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/properties/xbuild-tools"
|
||||
},
|
||||
"pyodide-version": {
|
||||
"$ref": "#/properties/pyodide-version"
|
||||
},
|
||||
"repair-wheel-command": {
|
||||
"$ref": "#/properties/repair-wheel-command"
|
||||
},
|
||||
"test-command": {
|
||||
"$ref": "#/properties/test-command"
|
||||
},
|
||||
"test-extras": {
|
||||
"$ref": "#/properties/test-extras"
|
||||
},
|
||||
"test-sources": {
|
||||
"$ref": "#/properties/test-sources"
|
||||
},
|
||||
"test-groups": {
|
||||
"$ref": "#/properties/test-groups"
|
||||
},
|
||||
"test-requires": {
|
||||
"$ref": "#/properties/test-requires"
|
||||
},
|
||||
"test-environment": {
|
||||
"$ref": "#/properties/test-environment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
anyio==4.9.0
|
||||
# via httpx
|
||||
auditwheel-emscripten==0.1.0
|
||||
# via pyodide-build
|
||||
build==1.2.2.post1
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# pyodide-build
|
||||
certifi==2025.7.14
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
# requests
|
||||
charset-normalizer==3.4.2
|
||||
# via requests
|
||||
click==8.1.8
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# typer
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.18.0
|
||||
# via virtualenv
|
||||
h11==0.16.0
|
||||
# via httpcore
|
||||
httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.28.1
|
||||
# via unearth
|
||||
idna==3.10
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
# requests
|
||||
leb128==1.0.8
|
||||
# via auditwheel-emscripten
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==25.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# build
|
||||
# pyodide-build
|
||||
# unearth
|
||||
pip==25.1.1
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
platformdirs==4.3.8
|
||||
# via
|
||||
# pyodide-build
|
||||
# virtualenv
|
||||
pydantic==2.11.7
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-lock
|
||||
pydantic-core==2.33.2
|
||||
# via pydantic
|
||||
pygments==2.19.2
|
||||
# via rich
|
||||
pyodide-build==0.30.5
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
pyodide-cli==0.3.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
pyodide-lock==0.1.0a7
|
||||
# via pyodide-build
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
requests==2.32.4
|
||||
# via pyodide-build
|
||||
resolvelib==1.2.0
|
||||
# via pyodide-build
|
||||
rich==14.1.0
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
# typer
|
||||
ruamel-yaml==0.18.14
|
||||
# via pyodide-build
|
||||
ruamel-yaml-clib==0.2.12
|
||||
# via ruamel-yaml
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
typer==0.16.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
typing-extensions==4.14.1
|
||||
# via
|
||||
# anyio
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# typer
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.1
|
||||
# via pydantic
|
||||
unearth==0.17.5
|
||||
# via pyodide-build
|
||||
urllib3==2.5.0
|
||||
# via requests
|
||||
virtualenv==20.32.0
|
||||
# via
|
||||
# build
|
||||
# pyodide-build
|
||||
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.9.0
|
||||
# via httpx
|
||||
auditwheel-emscripten==0.1.0
|
||||
# via pyodide-build
|
||||
build==1.2.2.post1
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# pyodide-build
|
||||
certifi==2025.7.14
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
# requests
|
||||
charset-normalizer==3.4.2
|
||||
# via requests
|
||||
click==8.1.8
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# typer
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.18.0
|
||||
# via virtualenv
|
||||
h11==0.16.0
|
||||
# via httpcore
|
||||
httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.28.1
|
||||
# via unearth
|
||||
idna==3.10
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
# requests
|
||||
leb128==1.0.8
|
||||
# via auditwheel-emscripten
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==25.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# build
|
||||
# pyodide-build
|
||||
# unearth
|
||||
pip==25.1.1
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
platformdirs==4.3.8
|
||||
# via
|
||||
# pyodide-build
|
||||
# virtualenv
|
||||
pydantic==2.11.7
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-lock
|
||||
pydantic-core==2.33.2
|
||||
# via pydantic
|
||||
pygments==2.19.2
|
||||
# via rich
|
||||
pyodide-build==0.30.5
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
pyodide-cli==0.3.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
pyodide-lock==0.1.0a7
|
||||
# via pyodide-build
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
requests==2.32.4
|
||||
# via pyodide-build
|
||||
resolvelib==1.2.0
|
||||
# via pyodide-build
|
||||
rich==14.1.0
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
# typer
|
||||
ruamel-yaml==0.18.14
|
||||
# via pyodide-build
|
||||
ruamel-yaml-clib==0.2.12
|
||||
# via ruamel-yaml
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
typer==0.16.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
typing-extensions==4.14.1
|
||||
# via
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# typer
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.1
|
||||
# via pydantic
|
||||
unearth==0.17.5
|
||||
# via pyodide-build
|
||||
urllib3==2.5.0
|
||||
# via requests
|
||||
virtualenv==20.32.0
|
||||
# via
|
||||
# build
|
||||
# pyodide-build
|
||||
wheel==0.45.1
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
@@ -1,28 +1,34 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.10
|
||||
#
|
||||
delocate==0.10.7
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.2.post1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.13.1
|
||||
filelock==3.18.0
|
||||
# via virtualenv
|
||||
packaging==23.2
|
||||
importlib-metadata==8.7.0
|
||||
# via build
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
platformdirs==4.1.0
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==25.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.3.8
|
||||
# via virtualenv
|
||||
typing-extensions==4.9.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
tomli==2.2.1
|
||||
# via build
|
||||
typing-extensions==4.14.1
|
||||
# via delocate
|
||||
virtualenv==20.25.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.42.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.3.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==69.0.3
|
||||
virtualenv==20.32.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
zipp==3.23.0
|
||||
# via importlib-metadata
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.11
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.11
|
||||
#
|
||||
delocate==0.10.7
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.2.post1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.13.1
|
||||
filelock==3.18.0
|
||||
# via virtualenv
|
||||
packaging==23.2
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
platformdirs==4.1.0
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==25.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.3.8
|
||||
# via virtualenv
|
||||
typing-extensions==4.9.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
typing-extensions==4.14.1
|
||||
# via delocate
|
||||
virtualenv==20.25.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.42.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.3.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==69.0.3
|
||||
virtualenv==20.32.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.12
|
||||
#
|
||||
delocate==0.10.7
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.2.post1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.13.1
|
||||
filelock==3.18.0
|
||||
# via virtualenv
|
||||
packaging==23.2
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
platformdirs==4.1.0
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==25.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.3.8
|
||||
# via virtualenv
|
||||
typing-extensions==4.9.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
typing-extensions==4.14.1
|
||||
# via delocate
|
||||
virtualenv==20.25.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.42.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.3.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==69.0.3
|
||||
virtualenv==20.32.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.2.post1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.18.0
|
||||
# via virtualenv
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==25.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.3.8
|
||||
# via virtualenv
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
typing-extensions==4.14.1
|
||||
# via delocate
|
||||
virtualenv==20.32.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
@@ -0,0 +1,28 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.2.post1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.18.0
|
||||
# via virtualenv
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==25.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.3.8
|
||||
# via virtualenv
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
typing-extensions==4.14.1
|
||||
# via delocate
|
||||
virtualenv==20.32.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
@@ -1,38 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with python 3.6
|
||||
# To update, run:
|
||||
#
|
||||
# nox -s update_constraints-3.6
|
||||
#
|
||||
delocate==0.10.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.6
|
||||
# via virtualenv
|
||||
filelock==3.4.1
|
||||
# via virtualenv
|
||||
importlib-metadata==4.8.3
|
||||
# via virtualenv
|
||||
importlib-resources==5.4.0
|
||||
# via virtualenv
|
||||
platformdirs==2.4.0
|
||||
# via virtualenv
|
||||
typing-extensions==4.1.1
|
||||
# via
|
||||
# delocate
|
||||
# importlib-metadata
|
||||
virtualenv==20.17.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.37.1
|
||||
# via
|
||||
# -r cibuildwheel/resources/constraints.in
|
||||
# delocate
|
||||
zipp==3.6.0
|
||||
# via
|
||||
# importlib-metadata
|
||||
# importlib-resources
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==21.3.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==59.6.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
@@ -1,35 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.7
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.7
|
||||
#
|
||||
delocate==0.10.7
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
# via virtualenv
|
||||
filelock==3.12.2
|
||||
# via virtualenv
|
||||
importlib-metadata==6.7.0
|
||||
# via virtualenv
|
||||
packaging==23.2
|
||||
# via delocate
|
||||
platformdirs==4.0.0
|
||||
# via virtualenv
|
||||
typing-extensions==4.7.1
|
||||
# via
|
||||
# delocate
|
||||
# importlib-metadata
|
||||
# platformdirs
|
||||
virtualenv==20.25.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.42.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
zipp==3.15.0
|
||||
# via importlib-metadata
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.3.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==68.0.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
@@ -1,28 +1,34 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.8
|
||||
#
|
||||
delocate==0.10.7
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.2.post1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
delocate==0.12.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.13.1
|
||||
filelock==3.16.1
|
||||
# via virtualenv
|
||||
packaging==23.2
|
||||
importlib-metadata==8.5.0
|
||||
# via build
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
platformdirs==4.1.0
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==25.0.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.3.6
|
||||
# via virtualenv
|
||||
typing-extensions==4.9.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
tomli==2.2.1
|
||||
# via build
|
||||
typing-extensions==4.13.2
|
||||
# via delocate
|
||||
virtualenv==20.25.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.42.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.3.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==69.0.3
|
||||
virtualenv==20.32.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
zipp==3.20.2
|
||||
# via importlib-metadata
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.9
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.9
|
||||
#
|
||||
delocate==0.10.7
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.2.post1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.13.1
|
||||
filelock==3.18.0
|
||||
# via virtualenv
|
||||
packaging==23.2
|
||||
importlib-metadata==8.7.0
|
||||
# via build
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
platformdirs==4.1.0
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==25.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.3.8
|
||||
# via virtualenv
|
||||
typing-extensions==4.9.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
tomli==2.2.1
|
||||
# via build
|
||||
typing-extensions==4.14.1
|
||||
# via delocate
|
||||
virtualenv==20.25.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.42.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.3.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==69.0.3
|
||||
virtualenv==20.32.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
zipp==3.23.0
|
||||
# via importlib-metadata
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pip
|
||||
setuptools
|
||||
wheel
|
||||
build
|
||||
delocate
|
||||
virtualenv
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.12
|
||||
#
|
||||
delocate==0.10.7
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
altgraph==0.17.4
|
||||
# via macholib
|
||||
build==1.2.2.post1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.8
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.13.1
|
||||
filelock==3.18.0
|
||||
# via virtualenv
|
||||
packaging==23.2
|
||||
macholib==1.16.3
|
||||
# via delocate
|
||||
platformdirs==4.1.0
|
||||
packaging==25.0
|
||||
# via
|
||||
# build
|
||||
# delocate
|
||||
pip==25.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.3.8
|
||||
# via virtualenv
|
||||
typing-extensions==4.9.0
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
typing-extensions==4.14.1
|
||||
# via delocate
|
||||
virtualenv==20.25.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.42.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.3.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==69.0.3
|
||||
virtualenv==20.32.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
build = "*"
|
||||
skip = ""
|
||||
test-skip = ""
|
||||
enable = []
|
||||
|
||||
archs = ["auto"]
|
||||
build-frontend = "default"
|
||||
@@ -13,29 +14,40 @@ 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 = {}
|
||||
|
||||
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 = ""
|
||||
|
||||
musllinux-x86_64-image = "musllinux_1_1"
|
||||
musllinux-i686-image = "musllinux_1_1"
|
||||
musllinux-aarch64-image = "musllinux_1_1"
|
||||
musllinux-ppc64le-image = "musllinux_1_1"
|
||||
musllinux-s390x-image = "musllinux_1_1"
|
||||
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]
|
||||
@@ -45,3 +57,9 @@ repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}"
|
||||
repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"
|
||||
|
||||
[tool.cibuildwheel.windows]
|
||||
|
||||
[tool.cibuildwheel.android]
|
||||
|
||||
[tool.cibuildwheel.ios]
|
||||
|
||||
[tool.cibuildwheel.pyodide]
|
||||
|
||||
@@ -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.13</string>
|
||||
</dict>
|
||||
</array>
|
||||
</plist>
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
url = "https://nodejs.org/dist/"
|
||||
v22 = "v22.17.1"
|
||||
v20 = "v20.19.4"
|
||||
@@ -1,54 +1,53 @@
|
||||
[x86_64]
|
||||
manylinux1 = quay.io/pypa/manylinux1_x86_64:2024-01-28-7b6687a
|
||||
manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-08-05-4535177
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2024-01-23-12ffabc
|
||||
manylinux_2_24 = quay.io/pypa/manylinux_2_24_x86_64:2022-12-26-0d38463
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2024-01-23-12ffabc
|
||||
musllinux_1_1 = quay.io/pypa/musllinux_1_1_x86_64:2024-01-23-12ffabc
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2024-01-23-12ffabc
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2025.07.27-1
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2025.07.27-1
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2025.07.27-1
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2025.07.27-1
|
||||
|
||||
[i686]
|
||||
manylinux1 = quay.io/pypa/manylinux1_i686:2024-01-28-7b6687a
|
||||
manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-08-05-4535177
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_i686:2024-01-23-12ffabc
|
||||
manylinux_2_24 = quay.io/pypa/manylinux_2_24_i686:2022-12-26-0d38463
|
||||
musllinux_1_1 = quay.io/pypa/musllinux_1_1_i686:2024-01-23-12ffabc
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686:2024-01-23-12ffabc
|
||||
|
||||
[pypy_x86_64]
|
||||
manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-08-05-4535177
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2024-01-23-12ffabc
|
||||
manylinux_2_24 = quay.io/pypa/manylinux_2_24_x86_64:2022-12-26-0d38463
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2024-01-23-12ffabc
|
||||
|
||||
[pypy_i686]
|
||||
manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-08-05-4535177
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_i686:2024-01-23-12ffabc
|
||||
manylinux_2_24 = quay.io/pypa/manylinux_2_24_i686:2022-12-26-0d38463
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_i686:2025.07.27-1
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2025.07.27-1
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2025.07.27-1
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686:2025.07.27-1
|
||||
|
||||
[aarch64]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2024-01-23-12ffabc
|
||||
manylinux_2_24 = quay.io/pypa/manylinux_2_24_aarch64:2022-12-26-0d38463
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2024-01-23-12ffabc
|
||||
musllinux_1_1 = quay.io/pypa/musllinux_1_1_aarch64:2024-01-23-12ffabc
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64:2024-01-23-12ffabc
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2025.07.27-1
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2025.07.27-1
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2025.07.27-1
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64:2025.07.27-1
|
||||
|
||||
[ppc64le]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2024-01-23-12ffabc
|
||||
manylinux_2_24 = quay.io/pypa/manylinux_2_24_ppc64le:2022-12-26-0d38463
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_ppc64le:2024-01-23-12ffabc
|
||||
musllinux_1_1 = quay.io/pypa/musllinux_1_1_ppc64le:2024-01-23-12ffabc
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_ppc64le:2024-01-23-12ffabc
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2025.07.27-1
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_ppc64le:2025.07.27-1
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_ppc64le:2025.07.27-1
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_ppc64le:2025.07.27-1
|
||||
|
||||
[s390x]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_s390x:2024-01-23-12ffabc
|
||||
manylinux_2_24 = quay.io/pypa/manylinux_2_24_s390x:2022-12-26-0d38463
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_s390x:2024-01-23-12ffabc
|
||||
musllinux_1_1 = quay.io/pypa/musllinux_1_1_s390x:2024-01-23-12ffabc
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_s390x:2024-01-23-12ffabc
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_s390x:2025.07.27-1
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_s390x:2025.07.27-1
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_s390x:2025.07.27-1
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_s390x:2025.07.27-1
|
||||
|
||||
[pypy_x86_64]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2025.07.27-1
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2025.07.27-1
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2025.07.27-1
|
||||
|
||||
[pypy_i686]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_i686:2025.07.27-1
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2025.07.27-1
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2025.07.27-1
|
||||
|
||||
[pypy_aarch64]
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2024-01-23-12ffabc
|
||||
manylinux_2_24 = quay.io/pypa/manylinux_2_24_aarch64:2022-12-26-0d38463
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2024-01-23-12ffabc
|
||||
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2025.07.27-1
|
||||
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2025.07.27-1
|
||||
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2025.07.27-1
|
||||
|
||||
[armv7l]
|
||||
manylinux_2_31 = quay.io/pypa/manylinux_2_31_armv7l:2025.07.27-1
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_armv7l:2025.07.27-1
|
||||
|
||||
[riscv64]
|
||||
manylinux_2_39 = quay.io/pypa/manylinux_2_39_riscv64:2025.07.27-1
|
||||
musllinux_1_2 = quay.io/pypa/musllinux_1_2_riscv64:2025.07.27-1
|
||||
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
{
|
||||
"releases": [
|
||||
{
|
||||
"tag": "20250723",
|
||||
"assets": [
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.10.18+20250723-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.10.18%2B20250723-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-aarch64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-aarch64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.11.13+20250723-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.11.13%2B20250723-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-aarch64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-aarch64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.12.11+20250723-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.11%2B20250723-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-aarch64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-aarch64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.13.5+20250723-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.13.5%2B20250723-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-aarch64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-aarch64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.14.0rc1+20250723-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.14.0rc1%2B20250723-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-aarch64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-aarch64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-aarch64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-armv7-unknown-linux-gnueabi-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-i686-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-i686-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-ppc64le-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-riscv64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-riscv64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-s390x-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-s390x-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-x86_64-apple-darwin-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-x86_64-apple-darwin-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-x86_64-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-x86_64-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
|
||||
},
|
||||
{
|
||||
"name": "cpython-3.9.23+20250723-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
|
||||
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.9.23%2B20250723-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 @@
|
||||
version = "20.21.1"
|
||||
url = "https://github.com/pypa/get-virtualenv/blob/20.21.1/public/virtualenv.pyz?raw=true"
|
||||
default = { version = "20.32.0", url = "https://github.com/pypa/get-virtualenv/blob/20.32.0/public/virtualenv.pyz?raw=true" }
|
||||
|
||||
@@ -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,137 @@
|
||||
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
|
||||
if EnableGroup.PyPy not in self.enable and fnmatch(build_id, "pp31*"):
|
||||
return False
|
||||
if EnableGroup.PyPyEoL not in self.enable and fnmatch(build_id, "pp3?-*"):
|
||||
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}
|
||||
+5
-16
@@ -1,32 +1,21 @@
|
||||
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"]
|
||||
PLATFORMS: Final[set[PlatformName]] = {"linux", "macos", "windows"}
|
||||
PlatformName = Literal["linux", "macos", "windows", "pyodide", "android", "ios"]
|
||||
PLATFORMS: Final[frozenset[PlatformName]] = frozenset(typing.get_args(PlatformName))
|
||||
|
||||
|
||||
class GenericPythonConfiguration(Protocol):
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
...
|
||||
def identifier(self) -> str: ...
|
||||
|
||||
@@ -1,762 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import fnmatch
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import time
|
||||
import typing
|
||||
import urllib.request
|
||||
from collections import defaultdict
|
||||
from collections.abc import Generator, Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from functools import cached_property, lru_cache
|
||||
from pathlib import Path, PurePath
|
||||
from time import sleep
|
||||
from typing import Any, ClassVar, Final, Literal, TextIO, TypeVar
|
||||
|
||||
import bracex
|
||||
import certifi
|
||||
from filelock import FileLock
|
||||
from packaging.requirements import InvalidRequirement, Requirement
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from packaging.utils import parse_wheel_filename
|
||||
from packaging.version import Version
|
||||
from platformdirs import user_cache_path
|
||||
|
||||
from ._compat import tomllib
|
||||
from .typing import PathOrStr, PlatformName
|
||||
|
||||
__all__ = [
|
||||
"resources_dir",
|
||||
"MANYLINUX_ARCHS",
|
||||
"call",
|
||||
"shell",
|
||||
"find_compatible_wheel",
|
||||
"format_safe",
|
||||
"prepare_command",
|
||||
"get_build_verbosity_extra_flags",
|
||||
"read_python_configs",
|
||||
"selector_matches",
|
||||
"strtobool",
|
||||
"cached_property",
|
||||
"chdir",
|
||||
"split_config_settings",
|
||||
]
|
||||
|
||||
resources_dir: Final[Path] = Path(__file__).parent / "resources"
|
||||
|
||||
install_certifi_script: Final[Path] = resources_dir / "install_certifi.py"
|
||||
|
||||
test_fail_cwd_file: Final[Path] = resources_dir / "testing_temp_dir_file.py"
|
||||
|
||||
|
||||
MANYLINUX_ARCHS: Final[tuple[str, ...]] = (
|
||||
"x86_64",
|
||||
"i686",
|
||||
"pypy_x86_64",
|
||||
"aarch64",
|
||||
"ppc64le",
|
||||
"s390x",
|
||||
"pypy_aarch64",
|
||||
"pypy_i686",
|
||||
)
|
||||
|
||||
MUSLLINUX_ARCHS: Final[tuple[str, ...]] = (
|
||||
"x86_64",
|
||||
"i686",
|
||||
"aarch64",
|
||||
"ppc64le",
|
||||
"s390x",
|
||||
)
|
||||
|
||||
DEFAULT_CIBW_CACHE_PATH: Final[Path] = user_cache_path(appname="cibuildwheel", appauthor="pypa")
|
||||
CIBW_CACHE_PATH: Final[Path] = Path(
|
||||
os.environ.get("CIBW_CACHE_PATH", DEFAULT_CIBW_CACHE_PATH)
|
||||
).resolve()
|
||||
|
||||
IS_WIN: Final[bool] = sys.platform.startswith("win")
|
||||
|
||||
|
||||
@typing.overload
|
||||
def call(
|
||||
*args: PathOrStr,
|
||||
env: Mapping[str, str] | None = None,
|
||||
cwd: PathOrStr | None = None,
|
||||
capture_stdout: Literal[False] = ...,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
|
||||
@typing.overload
|
||||
def call(
|
||||
*args: PathOrStr,
|
||||
env: Mapping[str, str] | None = None,
|
||||
cwd: PathOrStr | None = None,
|
||||
capture_stdout: Literal[True],
|
||||
) -> str:
|
||||
...
|
||||
|
||||
|
||||
def call(
|
||||
*args: PathOrStr,
|
||||
env: Mapping[str, str] | None = None,
|
||||
cwd: PathOrStr | None = None,
|
||||
capture_stdout: bool = False,
|
||||
) -> str | None:
|
||||
"""
|
||||
Run subprocess.run, but print the commands first. Takes the commands as
|
||||
*args. Uses shell=True on Windows due to a bug. Also converts to
|
||||
Paths to strings, due to Windows behavior at least on older Pythons.
|
||||
https://bugs.python.org/issue8557
|
||||
"""
|
||||
args_ = [str(arg) for arg in args]
|
||||
# print the command executing for the logs
|
||||
print("+ " + " ".join(shlex.quote(a) for a in args_))
|
||||
kwargs: dict[str, Any] = {}
|
||||
if capture_stdout:
|
||||
kwargs["universal_newlines"] = True
|
||||
kwargs["stdout"] = subprocess.PIPE
|
||||
result = subprocess.run(args_, check=True, shell=IS_WIN, env=env, cwd=cwd, **kwargs)
|
||||
if not capture_stdout:
|
||||
return None
|
||||
return typing.cast(str, result.stdout)
|
||||
|
||||
|
||||
def shell(
|
||||
*commands: str, env: Mapping[str, str] | None = None, cwd: PathOrStr | None = None
|
||||
) -> None:
|
||||
command = " ".join(commands)
|
||||
print(f"+ {command}")
|
||||
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
|
||||
|
||||
|
||||
def format_safe(template: str, **kwargs: str | os.PathLike[str]) -> str:
|
||||
"""
|
||||
Works similarly to `template.format(**kwargs)`, except that unmatched
|
||||
fields in `template` are passed through untouched.
|
||||
|
||||
>>> format_safe('{a} {b}', a='123')
|
||||
'123 {b}'
|
||||
>>> format_safe('{a} {b[4]:3f}', a='123')
|
||||
'123 {b[4]:3f}'
|
||||
|
||||
To avoid variable expansion, precede with a single backslash e.g.
|
||||
>>> format_safe('\\{a} {b}', a='123')
|
||||
'{a} {b}'
|
||||
"""
|
||||
|
||||
result = template
|
||||
|
||||
for key, value in kwargs.items():
|
||||
find_pattern = re.compile(
|
||||
rf"""
|
||||
(?<!\#) # don't match if preceded by a hash
|
||||
{{ # literal open curly bracket
|
||||
{re.escape(key)} # the field name
|
||||
}} # literal close curly bracket
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
result = re.sub(
|
||||
pattern=find_pattern,
|
||||
repl=str(value).replace("\\", r"\\"),
|
||||
string=result,
|
||||
)
|
||||
|
||||
# transform escaped sequences into their literal equivalents
|
||||
result = result.replace(f"#{{{key}}}", f"{{{key}}}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def prepare_command(command: str, **kwargs: PathOrStr) -> str:
|
||||
"""
|
||||
Preprocesses a command by expanding variables like {python}.
|
||||
|
||||
For example, used in the test_command option to specify the path to the
|
||||
project's root. Unmatched syntax will mostly be allowed through.
|
||||
"""
|
||||
return format_safe(command, python="python", pip="pip", **kwargs)
|
||||
|
||||
|
||||
def get_build_verbosity_extra_flags(level: int) -> list[str]:
|
||||
if level > 0:
|
||||
return ["-" + level * "v"]
|
||||
elif level < 0:
|
||||
return ["-" + -level * "q"]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def split_config_settings(config_settings: str, frontend: Literal["pip", "build"]) -> list[str]:
|
||||
config_settings_list = shlex.split(config_settings)
|
||||
s = "s" if frontend == "pip" else ""
|
||||
return [f"--config-setting{s}={setting}" for setting in config_settings_list]
|
||||
|
||||
|
||||
def read_python_configs(config: PlatformName) -> list[dict[str, str]]:
|
||||
input_file = resources_dir / "build-platforms.toml"
|
||||
with input_file.open("rb") as f:
|
||||
loaded_file = tomllib.load(f)
|
||||
results: list[dict[str, str]] = list(loaded_file[config]["python_configurations"])
|
||||
return results
|
||||
|
||||
|
||||
def selector_matches(patterns: str, string: str) -> bool:
|
||||
"""
|
||||
Returns True if `string` is matched by any of the wildcard patterns in
|
||||
`patterns`.
|
||||
|
||||
Matching is according to fnmatch, but with shell-like curly brace
|
||||
expansion. For example, 'cp{36,37}-*' would match either of 'cp36-*' or
|
||||
'cp37-*'.
|
||||
"""
|
||||
patterns_list = patterns.split()
|
||||
expanded_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in patterns_list)
|
||||
return any(fnmatch.fnmatch(string, pat) for pat in expanded_patterns)
|
||||
|
||||
|
||||
# Once we require Python 3.10+, we can add kw_only=True
|
||||
@dataclass(frozen=True)
|
||||
class BuildSelector:
|
||||
"""
|
||||
This class holds a set of build/skip patterns. You call an instance with a
|
||||
build identifier, and it returns True if that identifier should be
|
||||
included. Only call this on valid identifiers, ones that have at least 2
|
||||
numeric digits before the first dash.
|
||||
"""
|
||||
|
||||
build_config: str
|
||||
skip_config: str
|
||||
requires_python: SpecifierSet | None = None
|
||||
|
||||
# a pattern that skips prerelease versions, when include_prereleases is False.
|
||||
PRERELEASE_SKIP: ClassVar[str] = ""
|
||||
prerelease_pythons: bool = False
|
||||
|
||||
def __call__(self, build_id: str) -> bool:
|
||||
# Filter build selectors by python_requires if set
|
||||
if self.requires_python is not None:
|
||||
py_ver_str = build_id.split("-")[0]
|
||||
major = int(py_ver_str[2])
|
||||
minor = int(py_ver_str[3:])
|
||||
version = Version(f"{major}.{minor}.99")
|
||||
if not self.requires_python.contains(version):
|
||||
return False
|
||||
|
||||
# filter out the prerelease pythons if self.prerelease_pythons is False
|
||||
if not self.prerelease_pythons and selector_matches(self.PRERELEASE_SKIP, build_id):
|
||||
return False
|
||||
|
||||
should_build = selector_matches(self.build_config, build_id)
|
||||
should_skip = selector_matches(self.skip_config, build_id)
|
||||
|
||||
return should_build and not should_skip
|
||||
|
||||
def options_summary(self) -> Any:
|
||||
return {
|
||||
"build_config": self.build_config,
|
||||
"skip_config": self.skip_config,
|
||||
"requires_python": str(self.requires_python),
|
||||
"prerelease_pythons": self.prerelease_pythons,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestSelector:
|
||||
"""
|
||||
A build selector that can only skip tests according to a skip pattern.
|
||||
"""
|
||||
|
||||
skip_config: str
|
||||
|
||||
def __call__(self, build_id: str) -> bool:
|
||||
should_skip = selector_matches(self.skip_config, build_id)
|
||||
return not should_skip
|
||||
|
||||
def options_summary(self) -> Any:
|
||||
return {"skip_config": self.skip_config}
|
||||
|
||||
|
||||
# Taken from https://stackoverflow.com/a/107717
|
||||
class Unbuffered:
|
||||
def __init__(self, stream: TextIO) -> None:
|
||||
self.stream = stream
|
||||
|
||||
def write(self, data: str) -> None:
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
|
||||
def writelines(self, data: Iterable[str]) -> None:
|
||||
self.stream.writelines(data)
|
||||
self.stream.flush()
|
||||
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
|
||||
def download(url: str, dest: Path) -> None:
|
||||
print(f"+ Download {url} to {dest}")
|
||||
dest_dir = dest.parent
|
||||
if not dest_dir.exists():
|
||||
dest_dir.mkdir(parents=True)
|
||||
|
||||
# we've had issues when relying on the host OS' CA certificates on Windows,
|
||||
# so we use certifi (this sounds odd but requests also does this by default)
|
||||
cafile = os.environ.get("SSL_CERT_FILE", certifi.where())
|
||||
context = ssl.create_default_context(cafile=cafile)
|
||||
repeat_num = 3
|
||||
for i in range(repeat_num):
|
||||
try:
|
||||
with urllib.request.urlopen(url, context=context) as response:
|
||||
dest.write_bytes(response.read())
|
||||
return
|
||||
|
||||
except OSError:
|
||||
if i == repeat_num - 1:
|
||||
raise
|
||||
sleep(3)
|
||||
|
||||
|
||||
class DependencyConstraints:
|
||||
def __init__(self, base_file_path: Path):
|
||||
assert base_file_path.exists()
|
||||
self.base_file_path = base_file_path.resolve()
|
||||
|
||||
@staticmethod
|
||||
def with_defaults() -> DependencyConstraints:
|
||||
return DependencyConstraints(base_file_path=resources_dir / "constraints.txt")
|
||||
|
||||
def get_for_python_version(self, version: str) -> Path:
|
||||
version_parts = version.split(".")
|
||||
|
||||
# try to find a version-specific dependency file e.g. if
|
||||
# ./constraints.txt is the base, look for ./constraints-python36.txt
|
||||
specific_stem = self.base_file_path.stem + f"-python{version_parts[0]}{version_parts[1]}"
|
||||
specific_name = specific_stem + self.base_file_path.suffix
|
||||
specific_file_path = self.base_file_path.with_name(specific_name)
|
||||
|
||||
if specific_file_path.exists():
|
||||
return specific_file_path
|
||||
else:
|
||||
return self.base_file_path
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.base_file_path!r})"
|
||||
|
||||
def __eq__(self, o: object) -> bool:
|
||||
if not isinstance(o, DependencyConstraints):
|
||||
return False
|
||||
|
||||
return self.base_file_path == o.base_file_path
|
||||
|
||||
def options_summary(self) -> Any:
|
||||
if self == DependencyConstraints.with_defaults():
|
||||
return "pinned"
|
||||
else:
|
||||
return self.base_file_path.name
|
||||
|
||||
|
||||
BuildFrontendName = Literal["pip", "build"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BuildFrontendConfig:
|
||||
name: BuildFrontendName
|
||||
args: Sequence[str] = ()
|
||||
|
||||
@staticmethod
|
||||
def from_config_string(config_string: str) -> BuildFrontendConfig:
|
||||
config_dict = parse_key_value_string(config_string, ["name"], ["args"])
|
||||
name = " ".join(config_dict["name"])
|
||||
if name not in {"pip", "build"}:
|
||||
msg = f"Unrecognised build frontend {name}, only 'pip' and 'build' are supported"
|
||||
raise ValueError(msg)
|
||||
|
||||
name = typing.cast(BuildFrontendName, name)
|
||||
|
||||
args = config_dict.get("args") or []
|
||||
return BuildFrontendConfig(name=name, args=args)
|
||||
|
||||
def options_summary(self) -> str | dict[str, str]:
|
||||
if not self.args:
|
||||
return self.name
|
||||
else:
|
||||
return {"name": self.name, "args": repr(self.args)}
|
||||
|
||||
|
||||
class NonPlatformWheelError(Exception):
|
||||
def __init__(self) -> None:
|
||||
message = textwrap.dedent(
|
||||
"""
|
||||
cibuildwheel: Build failed because a pure Python wheel was generated.
|
||||
|
||||
If you intend to build a pure-Python wheel, you don't need cibuildwheel - use
|
||||
`pip wheel -w DEST_DIR .` instead.
|
||||
|
||||
If you expected a platform wheel, check your project configuration, or run
|
||||
cibuildwheel with CIBW_BUILD_VERBOSITY=1 to view build logs.
|
||||
"""
|
||||
)
|
||||
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AlreadyBuiltWheelError(Exception):
|
||||
def __init__(self, wheel_name: str) -> None:
|
||||
message = textwrap.dedent(
|
||||
f"""
|
||||
cibuildwheel: Build failed because a wheel named {wheel_name} was already generated in the current run.
|
||||
|
||||
If you expected another wheel to be generated, check your project configuration, or run
|
||||
cibuildwheel with CIBW_BUILD_VERBOSITY=1 to view build logs.
|
||||
"""
|
||||
)
|
||||
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
def strtobool(val: str) -> bool:
|
||||
return val.lower() in {"y", "yes", "t", "true", "on", "1"}
|
||||
|
||||
|
||||
class CIProvider(Enum):
|
||||
travis_ci = "travis"
|
||||
appveyor = "appveyor"
|
||||
circle_ci = "circle_ci"
|
||||
azure_pipelines = "azure_pipelines"
|
||||
github_actions = "github_actions"
|
||||
gitlab = "gitlab"
|
||||
cirrus_ci = "cirrus_ci"
|
||||
other = "other"
|
||||
|
||||
|
||||
def detect_ci_provider() -> CIProvider | None:
|
||||
if "TRAVIS" in os.environ:
|
||||
return CIProvider.travis_ci
|
||||
elif "APPVEYOR" in os.environ:
|
||||
return CIProvider.appveyor
|
||||
elif "CIRCLECI" in os.environ:
|
||||
return CIProvider.circle_ci
|
||||
elif "AZURE_HTTP_USER_AGENT" in os.environ:
|
||||
return CIProvider.azure_pipelines
|
||||
elif "GITHUB_ACTIONS" in os.environ:
|
||||
return CIProvider.github_actions
|
||||
elif "GITLAB_CI" in os.environ:
|
||||
return CIProvider.gitlab
|
||||
elif "CIRRUS_CI" in os.environ:
|
||||
return CIProvider.cirrus_ci
|
||||
elif strtobool(os.environ.get("CI", "false")):
|
||||
return CIProvider.other
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def unwrap(text: str) -> str:
|
||||
"""
|
||||
Unwraps multi-line text to a single line
|
||||
"""
|
||||
# remove initial line indent
|
||||
text = textwrap.dedent(text)
|
||||
# remove leading/trailing whitespace
|
||||
text = text.strip()
|
||||
# remove consecutive whitespace
|
||||
return re.sub(r"\s+", " ", text)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FileReport:
|
||||
name: str
|
||||
size: str
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
|
||||
"""
|
||||
Prints the new items in a directory upon exiting. The message to display
|
||||
can include {n} for number of wheels, {s} for total number of seconds,
|
||||
and/or {m} for total number of minutes. Does not print anything if this
|
||||
exits via exception.
|
||||
"""
|
||||
|
||||
start_time = time.time()
|
||||
existing_contents = set(output_dir.iterdir())
|
||||
yield
|
||||
final_contents = set(output_dir.iterdir())
|
||||
|
||||
new_contents = [
|
||||
FileReport(wheel.name, f"{(wheel.stat().st_size + 1023) // 1024:,d}")
|
||||
for wheel in final_contents - existing_contents
|
||||
]
|
||||
|
||||
if not new_contents:
|
||||
return
|
||||
|
||||
max_name_len = max(len(f.name) for f in new_contents)
|
||||
max_size_len = max(len(f.size) for f in new_contents)
|
||||
n = len(new_contents)
|
||||
s = time.time() - start_time
|
||||
m = s / 60
|
||||
print(
|
||||
msg.format(n=n, s=s, m=m),
|
||||
*sorted(
|
||||
f" {f.name:<{max_name_len}s} {f.size:>{max_size_len}s} kB" for f in new_contents
|
||||
),
|
||||
sep="\n",
|
||||
)
|
||||
|
||||
|
||||
def get_pip_version(env: Mapping[str, str]) -> str:
|
||||
versions_output_text = call(
|
||||
"python", "-m", "pip", "freeze", "--all", capture_stdout=True, env=env
|
||||
)
|
||||
(pip_version,) = (
|
||||
version[5:]
|
||||
for version in versions_output_text.strip().splitlines()
|
||||
if version.startswith("pip==")
|
||||
)
|
||||
return pip_version
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _ensure_virtualenv() -> Path:
|
||||
input_file = resources_dir / "virtualenv.toml"
|
||||
with input_file.open("rb") as f:
|
||||
loaded_file = tomllib.load(f)
|
||||
version = str(loaded_file["version"])
|
||||
url = str(loaded_file["url"])
|
||||
path = CIBW_CACHE_PATH / f"virtualenv-{version}.pyz"
|
||||
with FileLock(str(path) + ".lock"):
|
||||
if not path.exists():
|
||||
download(url, path)
|
||||
return path
|
||||
|
||||
|
||||
def _parse_constraints_for_virtualenv(
|
||||
dependency_constraint_flags: Sequence[PathOrStr],
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Parses the constraints file referenced by `dependency_constraint_flags` and returns a dict where
|
||||
the key is the package name, and the value is the constraint version.
|
||||
If a package version cannot be found, its value is "embed" meaning that virtualenv will install
|
||||
its bundled version, already available locally.
|
||||
The function does not try to be too smart and just handles basic constraints.
|
||||
If it can't get an exact version, the real constraint will be handled by the
|
||||
{macos|windows}.setup_python function.
|
||||
"""
|
||||
assert len(dependency_constraint_flags) in {0, 2}
|
||||
packages = ["pip", "setuptools", "wheel"]
|
||||
constraints_dict = {package: "embed" for package in packages}
|
||||
if len(dependency_constraint_flags) == 2:
|
||||
assert dependency_constraint_flags[0] == "-c"
|
||||
constraint_path = Path(dependency_constraint_flags[1])
|
||||
assert constraint_path.exists()
|
||||
with constraint_path.open(encoding="utf-8") as constraint_file:
|
||||
for line_ in constraint_file:
|
||||
line = line_.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
try:
|
||||
requirement = Requirement(line)
|
||||
package = requirement.name
|
||||
if (
|
||||
package not in packages
|
||||
or requirement.url is not None
|
||||
or requirement.marker is not None
|
||||
or len(requirement.extras) != 0
|
||||
or len(requirement.specifier) != 1
|
||||
):
|
||||
continue
|
||||
specifier = next(iter(requirement.specifier))
|
||||
if specifier.operator != "==":
|
||||
continue
|
||||
constraints_dict[package] = specifier.version
|
||||
except InvalidRequirement:
|
||||
continue
|
||||
return constraints_dict
|
||||
|
||||
|
||||
def virtualenv(
|
||||
python: Path, venv_path: Path, dependency_constraint_flags: Sequence[PathOrStr]
|
||||
) -> dict[str, str]:
|
||||
assert python.exists()
|
||||
virtualenv_app = _ensure_virtualenv()
|
||||
constraints = _parse_constraints_for_virtualenv(dependency_constraint_flags)
|
||||
additional_flags = [f"--{package}={version}" for package, version in constraints.items()]
|
||||
|
||||
# Using symlinks to pre-installed seed packages is really the fastest way to get a virtual
|
||||
# environment. The initial cost is a bit higher but reusing is much faster.
|
||||
# Windows does not always allow symlinks so just disabling for now.
|
||||
# Requires pip>=19.3 so disabling for "embed" because this means we don't know what's the
|
||||
# version of pip that will end-up installed.
|
||||
# c.f. https://virtualenv.pypa.io/en/latest/cli_interface.html#section-seeder
|
||||
if (
|
||||
not IS_WIN
|
||||
and constraints["pip"] != "embed"
|
||||
and Version(constraints["pip"]) >= Version("19.3")
|
||||
):
|
||||
additional_flags.append("--symlink-app-data")
|
||||
|
||||
call(
|
||||
sys.executable,
|
||||
"-sS", # just the stdlib, https://github.com/pypa/virtualenv/issues/2133#issuecomment-1003710125
|
||||
virtualenv_app,
|
||||
"--activators=",
|
||||
"--no-periodic-update",
|
||||
*additional_flags,
|
||||
"--python",
|
||||
python,
|
||||
venv_path,
|
||||
)
|
||||
paths = [str(venv_path), str(venv_path / "Scripts")] if IS_WIN else [str(venv_path / "bin")]
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = os.pathsep.join([*paths, env["PATH"]])
|
||||
return env
|
||||
|
||||
|
||||
T = TypeVar("T", bound=PurePath)
|
||||
|
||||
|
||||
def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None:
|
||||
"""
|
||||
Finds a wheel with an abi3 or a none ABI tag in `wheels` compatible with the Python interpreter
|
||||
specified by `identifier` that is previously built.
|
||||
"""
|
||||
|
||||
interpreter, platform = identifier.split("-")
|
||||
for wheel in wheels:
|
||||
_, _, _, tags = parse_wheel_filename(wheel.name)
|
||||
for tag in tags:
|
||||
if tag.abi == "abi3":
|
||||
# ABI3 wheels must start with cp3 for impl and tag
|
||||
if not (interpreter.startswith("cp3") and tag.interpreter.startswith("cp3")):
|
||||
continue
|
||||
elif tag.abi == "none":
|
||||
# CPythonless wheels must include py3 tag
|
||||
if tag.interpreter[:3] != "py3":
|
||||
continue
|
||||
else:
|
||||
# Other types of wheels are not detected, this is looking for previously built wheels.
|
||||
continue
|
||||
|
||||
if tag.interpreter != "py3" and int(tag.interpreter[3:]) > int(interpreter[3:]):
|
||||
# If a minor version number is given, it has to be lower than the current one.
|
||||
continue
|
||||
|
||||
if platform.startswith(("manylinux", "musllinux", "macosx")):
|
||||
# Linux, macOS require the beginning and ending match (macos/manylinux version doesn't need to)
|
||||
os_, arch = platform.split("_", 1)
|
||||
if not tag.platform.startswith(os_):
|
||||
continue
|
||||
if not tag.platform.endswith(f"_{arch}"):
|
||||
continue
|
||||
else:
|
||||
# Windows should exactly match
|
||||
if tag.platform != platform:
|
||||
continue
|
||||
|
||||
# If all the filters above pass, then the wheel is a previously built compatible wheel.
|
||||
return wheel
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Can be replaced by contextlib.chdir in Python 3.11
|
||||
@contextlib.contextmanager
|
||||
def chdir(new_path: Path | str) -> Generator[None, None, None]:
|
||||
"""Non thread-safe context manager to change the current working directory."""
|
||||
|
||||
cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(new_path)
|
||||
yield
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
|
||||
|
||||
def fix_ansi_codes_for_github_actions(text: str) -> str:
|
||||
"""
|
||||
Github Actions forgets the current ANSI style on every new line. This
|
||||
function repeats the current ANSI style on every new line.
|
||||
"""
|
||||
ansi_code_regex = re.compile(r"(\033\[[0-9;]*m)")
|
||||
ansi_codes: list[str] = []
|
||||
output = ""
|
||||
|
||||
for line in text.splitlines(keepends=True):
|
||||
# add the current ANSI codes to the beginning of the line
|
||||
output += "".join(ansi_codes) + line
|
||||
|
||||
# split the line at each ANSI code
|
||||
parts = ansi_code_regex.split(line)
|
||||
# if there are any ANSI codes, save them
|
||||
if len(parts) > 1:
|
||||
# iterate over the ANSI codes in this line
|
||||
for code in parts[1::2]:
|
||||
if code == "\033[0m":
|
||||
# reset the list of ANSI codes when the clear code is found
|
||||
ansi_codes = []
|
||||
else:
|
||||
ansi_codes.append(code)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def parse_key_value_string(
|
||||
key_value_string: str,
|
||||
positional_arg_names: Sequence[str] | None = None,
|
||||
kw_arg_names: Sequence[str] | None = None,
|
||||
) -> dict[str, list[str]]:
|
||||
"""
|
||||
Parses a string like "docker; create_args: --some-option=value another-option"
|
||||
"""
|
||||
if positional_arg_names is None:
|
||||
positional_arg_names = []
|
||||
if kw_arg_names is None:
|
||||
kw_arg_names = []
|
||||
|
||||
all_field_names = [*positional_arg_names, *kw_arg_names]
|
||||
|
||||
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";")
|
||||
shlexer.commenters = ""
|
||||
shlexer.whitespace_split = True
|
||||
parts = list(shlexer)
|
||||
# parts now looks like
|
||||
# ['docker', ';', 'create_args:', '--some-option=value', 'another-option']
|
||||
|
||||
# split by semicolon
|
||||
fields = [list(group) for k, group in itertools.groupby(parts, lambda x: x == ";") if not k]
|
||||
|
||||
result: defaultdict[str, list[str]] = defaultdict(list)
|
||||
for field_i, field in enumerate(fields):
|
||||
# check to see if the option name is specified
|
||||
field_name, sep, first_value = field[0].partition(":")
|
||||
if sep:
|
||||
if field_name not in all_field_names:
|
||||
msg = f"Failed to parse {key_value_string!r}. Unknown field name {field_name!r}"
|
||||
raise ValueError(msg)
|
||||
|
||||
values = ([first_value] if first_value else []) + field[1:]
|
||||
else:
|
||||
try:
|
||||
field_name = positional_arg_names[field_i]
|
||||
except IndexError:
|
||||
msg = f"Failed to parse {key_value_string!r}. Too many positional arguments - expected a maximum of {len(positional_arg_names)}"
|
||||
raise ValueError(msg) from None
|
||||
|
||||
values = field
|
||||
|
||||
result[field_name] += values
|
||||
|
||||
return dict(result)
|
||||
@@ -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,134 @@
|
||||
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 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 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 {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,207 @@
|
||||
import shlex
|
||||
from collections.abc import Mapping, MutableMapping, 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
|
||||
|
||||
|
||||
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,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]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user