Compare commits
935
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
892871796a | ||
|
|
39b605f6e0 | ||
|
|
3f6f9b3b1f | ||
|
|
f925e8ccef | ||
|
|
b597cb5aff | ||
|
|
97537fe937 | ||
|
|
e3e7cc9e07 | ||
|
|
2380f52783 | ||
|
|
8286363b4d | ||
|
|
a78478edc4 | ||
|
|
45cfffd3ee | ||
|
|
a7b3f178c3 | ||
|
|
525f538878 | ||
|
|
3a3f4dcfeb | ||
|
|
51b25dc0aa | ||
|
|
d08d9acda2 | ||
|
|
36b8e7e524 | ||
|
|
c274b4e02c | ||
|
|
02b64875a0 | ||
|
|
b6293e1712 | ||
|
|
54327ab9d3 | ||
|
|
efebd1ca62 | ||
|
|
79244d366c | ||
|
|
e04baff444 | ||
|
|
78869d0cf6 | ||
|
|
6d9f4c9545 | ||
|
|
ec0977e5f6 | ||
|
|
3617733645 | ||
|
|
8bdb703302 | ||
|
|
cbef1e8b89 | ||
|
|
aed06ac94d | ||
|
|
d2c0e59833 | ||
|
|
542335fb4a | ||
|
|
b425e9d2a7 | ||
|
|
a76fca878d | ||
|
|
d593457f60 | ||
|
|
f42b92f804 | ||
|
|
29d36dc17d | ||
|
|
a9f58eb656 | ||
|
|
545670cbf5 | ||
|
|
32b0ccc560 | ||
|
|
f86700db67 | ||
|
|
46558a4e24 | ||
|
|
023f45a968 | ||
|
|
1d5d9be23f | ||
|
|
a42e5d3563 | ||
|
|
d97b262640 | ||
|
|
58977c129d | ||
|
|
075b5c8881 | ||
|
|
3c2956fe9b | ||
|
|
3dcb54150e | ||
|
|
e2027266cd | ||
|
|
314d2268ab | ||
|
|
b7e8713e11 | ||
|
|
2f9518f182 | ||
|
|
8d75eb7a82 | ||
|
|
8d2b08b684 | ||
|
|
54b8a010d0 | ||
|
|
097806b6b1 | ||
|
|
643b30c796 | ||
|
|
fffe2ca07d | ||
|
|
611194896a | ||
|
|
e478767d76 | ||
|
|
caf433b137 | ||
|
|
a257a3f789 | ||
|
|
6df84da3e7 | ||
|
|
5d52226105 | ||
|
|
817878a5f9 | ||
|
|
cb1fdd0316 | ||
|
|
8a2ca0ce78 | ||
|
|
31f0d8a0db | ||
|
|
35452dcd42 | ||
|
|
741be9402c | ||
|
|
a94d4faa11 | ||
|
|
fd19ae7e2f | ||
|
|
e5486e0b83 | ||
|
|
ff9508a3e1 | ||
|
|
fd6b413212 | ||
|
|
4d4daefbd2 | ||
|
|
42c502093f | ||
|
|
bc6f42d805 | ||
|
|
eaf116e303 | ||
|
|
02c132e91e | ||
|
|
fd27532d12 | ||
|
|
1313a80d3a | ||
|
|
601935bfc3 | ||
|
|
880a211a08 | ||
|
|
ee02a1537c | ||
|
|
f08ce70d78 | ||
|
|
95b4b79b96 | ||
|
|
f046d0a94d | ||
|
|
f5a31ee79d | ||
|
|
35ec4565f5 | ||
|
|
51fed90270 | ||
|
|
b70562f37a | ||
|
|
0a85e29549 | ||
|
|
2d33864baf | ||
|
|
ade95b7be4 | ||
|
|
bb153041f0 | ||
|
|
27fd6ecc3c | ||
|
|
57e8757899 | ||
|
|
8879b0f013 | ||
|
|
9d4c80f487 | ||
|
|
e0af304d30 | ||
|
|
a2f965f62f | ||
|
|
0d2547bf6f | ||
|
|
1252e6249a | ||
|
|
42d1caec22 | ||
|
|
d31248fdbf | ||
|
|
d7dbf5a358 | ||
|
|
6088ea2c85 | ||
|
|
be32c5a2ed | ||
|
|
e8646fb903 | ||
|
|
6f8121fa47 | ||
|
|
7989c25bcd | ||
|
|
9a15d0851e | ||
|
|
dc4fc9c3fe | ||
|
|
dcd9f40825 | ||
|
|
90da3e21e2 | ||
|
|
f3ba8a83a5 | ||
|
|
3cefb9d38c | ||
|
|
298ed2fb2c | ||
|
|
f0ff944318 | ||
|
|
aae69a8e1d | ||
|
|
24bade1d15 | ||
|
|
a68c3b27fb | ||
|
|
99059b7442 | ||
|
|
d0e070e278 | ||
|
|
bb2f93202e | ||
|
|
11597ed249 | ||
|
|
90422f1824 | ||
|
|
986d80b3d7 | ||
|
|
5acadc2dfc | ||
|
|
6878a1baeb | ||
|
|
7288bbed76 | ||
|
|
0cb04c05db | ||
|
|
3c2d5961de | ||
|
|
6c7e3ee860 | ||
|
|
df2ce29908 | ||
|
|
74a3c45b74 | ||
|
|
9f991f2330 | ||
|
|
ea4b3eb6a5 | ||
|
|
6bc6700ab3 | ||
|
|
c314f9f07b | ||
|
|
dd81fa002c | ||
|
|
3658bf2f84 | ||
|
|
63fd63b352 | ||
|
|
f4fe311fc9 | ||
|
|
f6c810852d | ||
|
|
ccbae30b53 | ||
|
|
1337e50f74 | ||
|
|
720f8e266f | ||
|
|
4c7f3696b1 | ||
|
|
e1baa60eb0 | ||
|
|
1f2f8b28d5 | ||
|
|
8c5b02f42c | ||
|
|
997a6e1931 | ||
|
|
4fe7630d9c | ||
|
|
c53e541c2d | ||
|
|
58d113ad2b | ||
|
|
de0262ea87 | ||
|
|
41f3304a86 | ||
|
|
a3af13a337 | ||
|
|
168717b5c5 | ||
|
|
a592c06add | ||
|
|
bbe00a21e3 | ||
|
|
362020b1c7 | ||
|
|
8165422c41 | ||
|
|
a6acd2a2d5 | ||
|
|
372dd68cf6 | ||
|
|
cb5496943f | ||
|
|
ad74d64a16 | ||
|
|
be214efadb | ||
|
|
9c00cb4f6b | ||
|
|
ae65c7d85b | ||
|
|
86c3857999 | ||
|
|
68b1a81bb2 | ||
|
|
f131cd7935 | ||
|
|
8602e864d4 | ||
|
|
8ccc2650de | ||
|
|
c0c1dea51f | ||
|
|
7c619efba9 | ||
|
|
bbebb68339 | ||
|
|
5bbeaf830f | ||
|
|
7a67c6b5a9 | ||
|
|
b8a4fbc237 | ||
|
|
70de033a7f | ||
|
|
ce4c16f88f | ||
|
|
66490f0536 | ||
|
|
77b9e35d7c | ||
|
|
39e9da06c5 | ||
|
|
9b8ce87f0a | ||
|
|
3a524160c3 | ||
|
|
c294bab5aa | ||
|
|
5a4e49a734 | ||
|
|
4aa10a9378 | ||
|
|
c8fb268a1b | ||
|
|
841e718dee | ||
|
|
0826975570 | ||
|
|
3f076ba487 | ||
|
|
dac57ccc94 | ||
|
|
1d31ef4ab2 | ||
|
|
03ce2f2bc7 | ||
|
|
6331390c93 | ||
|
|
4b92d69e1b | ||
|
|
c923d83ad9 | ||
|
|
55ee8e49bf | ||
|
|
afe486c21f | ||
|
|
1c1ba8a22e | ||
|
|
33b75f491a | ||
|
|
c59c2fe70d | ||
|
|
24b64c5cd1 | ||
|
|
6425e48fed | ||
|
|
0264b4d03a | ||
|
|
68f9762657 | ||
|
|
95e827ece1 | ||
|
|
ff9a284f21 | ||
|
|
cade3864de | ||
|
|
63bdd4f043 | ||
|
|
5952330610 | ||
|
|
8db3538534 | ||
|
|
e5caaec292 | ||
|
|
5a1712daed | ||
|
|
a4c6d16811 | ||
|
|
78a246a901 | ||
|
|
47c5a31c1b | ||
|
|
3ebb6bd942 | ||
|
|
352e01339f | ||
|
|
c463e56ba2 | ||
|
|
8c5c738023 | ||
|
|
feeb3992a7 | ||
|
|
60b9cc95db | ||
|
|
e2c7102ed7 | ||
|
|
9e4e50bd76 | ||
|
|
8ef9414f60 | ||
|
|
1953c04972 | ||
|
|
46a6d27995 | ||
|
|
0dc0b48e4d | ||
|
|
b887417e0e | ||
|
|
36c5079ce5 | ||
|
|
68c03665d3 | ||
|
|
1c38fac9c8 | ||
|
|
140bc1a96a | ||
|
|
55fcb25edc | ||
|
|
c89609aaf2 | ||
|
|
13b086e3c6 | ||
|
|
302af02853 | ||
|
|
9361af3204 | ||
|
|
47c5a35421 | ||
|
|
e6de07ed39 | ||
|
|
2ca692b1e5 | ||
|
|
1ac7fa7f00 | ||
|
|
ffd835cef1 | ||
|
|
3e2a9aa6e8 | ||
|
|
10c727eed9 | ||
|
|
f628c9dd23 | ||
|
|
0f487ee2cb | ||
|
|
e2e24882d8 | ||
|
|
a8bff94dbb | ||
|
|
fbf37256a5 | ||
|
|
9613645abc | ||
|
|
7247e3887c | ||
|
|
d0e4aded3e | ||
|
|
a2c263d4ab | ||
|
|
576ebccc78 | ||
|
|
cd9b48cb74 | ||
|
|
5e245905b3 | ||
|
|
157d81eddb | ||
|
|
75b1860702 | ||
|
|
807cd81699 | ||
|
|
834ea40510 | ||
|
|
46ef6044e2 | ||
|
|
dafc3545eb | ||
|
|
a6ec930f15 | ||
|
|
95d2f3a92f | ||
|
|
40de3fe510 | ||
|
|
920081014e | ||
|
|
94fe0a212f | ||
|
|
405ddd5315 | ||
|
|
34b4f1e86e | ||
|
|
e69b5532ab | ||
|
|
3e86452449 | ||
|
|
e73749579b | ||
|
|
588dee0e0c | ||
|
|
9c53c4ee46 | ||
|
|
5f22145df4 | ||
|
|
a73177515a | ||
|
|
ff86a64577 | ||
|
|
6f5e480fec | ||
|
|
3c5ff09888 | ||
|
|
1b9a56e014 | ||
|
|
aa9fe2a24e | ||
|
|
e188d9e260 | ||
|
|
3fa7bd1e72 | ||
|
|
c6368be701 | ||
|
|
dcecda6962 | ||
|
|
adc58e93d1 | ||
|
|
487154abbb | ||
|
|
0fe82c5882 | ||
|
|
0b6c7d0aff | ||
|
|
38dab4b641 | ||
|
|
2edf578499 | ||
|
|
70c6aba901 | ||
|
|
31f536bdf3 | ||
|
|
7601bfcb45 | ||
|
|
b5c33b7a09 | ||
|
|
4e8d0757df | ||
|
|
a76b6fc338 | ||
|
|
ef60c822df | ||
|
|
4aa7d7e9c1 | ||
|
|
03a2ba0bec | ||
|
|
4aaa823c8e | ||
|
|
e6b4e2d355 | ||
|
|
c90accef51 | ||
|
|
2346c5f6e0 | ||
|
|
2125d7117e | ||
|
|
ea08120e25 | ||
|
|
f8168e2a89 | ||
|
|
3431adedba | ||
|
|
733573feb3 | ||
|
|
c5a3ced8c8 | ||
|
|
6f2d68febf | ||
|
|
e8a7a374ee | ||
|
|
cf078b0954 | ||
|
|
b9f1942e8d | ||
|
|
2d6c328457 | ||
|
|
0e8f4f777d | ||
|
|
7e099863cd | ||
|
|
c41b47f7c2 | ||
|
|
6379772957 | ||
|
|
7298563b25 | ||
|
|
db2d59be22 | ||
|
|
e18ab66cca | ||
|
|
b8814b8148 | ||
|
|
e92147f52a | ||
|
|
3bbc772778 | ||
|
|
2b4a8535d9 | ||
|
|
2d6b40d669 | ||
|
|
b1d944060a | ||
|
|
e2b6431822 | ||
|
|
44d6ccfade | ||
|
|
dded469d60 | ||
|
|
1bfec0e7c9 | ||
|
|
327f5c327d | ||
|
|
163864158d | ||
|
|
c3c260dff7 | ||
|
|
fcdbed860e | ||
|
|
9dd7192d89 | ||
|
|
bd641d11de | ||
|
|
90a0ddeff0 | ||
|
|
e46e9516d9 | ||
|
|
1fe8e55c67 | ||
|
|
898f0a45c1 | ||
|
|
1bc7e904b8 | ||
|
|
166465b56d | ||
|
|
c2cffa3d1e | ||
|
|
cab1ff4076 | ||
|
|
d193a8eff5 | ||
|
|
e7dcb31aa7 | ||
|
|
5622cc6988 | ||
|
|
54a4dccd62 | ||
|
|
5b65230f64 | ||
|
|
04c9427a20 | ||
|
|
edbd234b82 | ||
|
|
143ec07556 | ||
|
|
048ff03577 | ||
|
|
e91560e8a1 | ||
|
|
020a91baa8 | ||
|
|
5f8d06ff55 | ||
|
|
0463d26066 | ||
|
|
4449d7b5fa | ||
|
|
e9a8781000 | ||
|
|
faf86a6ed7 | ||
|
|
4241f37b2c | ||
|
|
4e5b8ee8e8 | ||
|
|
03f1905bb9 | ||
|
|
99183d68ac | ||
|
|
95e3a6575f | ||
|
|
e9fae06abe | ||
|
|
1fcca39f6d | ||
|
|
625240d4ff | ||
|
|
1d34f85c63 | ||
|
|
74a7d8a68e | ||
|
|
93314a8d65 | ||
|
|
8e2cc78913 | ||
|
|
08cbc6f8d0 | ||
|
|
1172caba2b | ||
|
|
830e79e11c | ||
|
|
2aaa489371 | ||
|
|
6c426a3a17 | ||
|
|
55bd91a92a | ||
|
|
279221e0d6 | ||
|
|
0dd98e95f1 | ||
|
|
4ef7b3770f | ||
|
|
094782a41d | ||
|
|
d04cacbc98 | ||
|
|
5f4e019684 | ||
|
|
2bb8b1e73d | ||
|
|
e6eff86c98 | ||
|
|
141e702872 | ||
|
|
4db404ca50 | ||
|
|
2efa648f38 | ||
|
|
059d8ea25a | ||
|
|
3b9c8d4d22 | ||
|
|
462c99589a | ||
|
|
fccd32f9b2 | ||
|
|
be2a707315 | ||
|
|
21888b531f | ||
|
|
54a0cd0b22 | ||
|
|
1826f70682 | ||
|
|
42728e866b | ||
|
|
6e1527b153 | ||
|
|
c25fe60385 | ||
|
|
a880bf5105 | ||
|
|
c087d85b69 | ||
|
|
572f2a72b3 | ||
|
|
26e12290b5 | ||
|
|
eefd48e266 | ||
|
|
915d15cc32 | ||
|
|
25f2d3f313 | ||
|
|
f5e502aa88 | ||
|
|
849921caef | ||
|
|
8c51c26bf7 | ||
|
|
c136aefe20 | ||
|
|
bddd438a88 | ||
|
|
ee94e7fdf8 | ||
|
|
ea26d56b5a | ||
|
|
efa1bedd1f | ||
|
|
6cccd09a31 | ||
|
|
0b052a43b7 | ||
|
|
aa1534d2a3 | ||
|
|
9e72cd9b54 | ||
|
|
708cf1d32b | ||
|
|
1b63074d76 | ||
|
|
783be9bc2f | ||
|
|
3e37148923 | ||
|
|
183a7c3f1d | ||
|
|
2f62696400 | ||
|
|
63a7c44b7f | ||
|
|
88ef8dbd29 | ||
|
|
4d37797aa6 | ||
|
|
7e5810c550 | ||
|
|
ee23dd147b | ||
|
|
55037e9716 | ||
|
|
8874ee2048 | ||
|
|
f76d345e81 | ||
|
|
5a5e514fbd | ||
|
|
017abbba68 | ||
|
|
bb1fd3320e | ||
|
|
98a6f3bf64 | ||
|
|
e061af0b82 | ||
|
|
6578d1a4f9 | ||
|
|
b08f1ba966 | ||
|
|
34315ff651 | ||
|
|
c93d51ec54 | ||
|
|
c738289583 | ||
|
|
a0f457675d | ||
|
|
a787e5d7c9 | ||
|
|
9cca4ce881 | ||
|
|
91b09c1c85 | ||
|
|
fad71e8d6d | ||
|
|
3805787fe7 | ||
|
|
9bfd0800fe | ||
|
|
3c5c62cf8c | ||
|
|
0dcc90f3e4 | ||
|
|
36259f4582 | ||
|
|
b656a829c8 | ||
|
|
e43288dd77 | ||
|
|
b605f8fec8 | ||
|
|
318a963cc3 | ||
|
|
3f123af8a1 | ||
|
|
d134edc652 | ||
|
|
730b64249f | ||
|
|
526df2f696 | ||
|
|
9c2ab266a6 | ||
|
|
c65b088000 | ||
|
|
a96545b729 | ||
|
|
1608f7567e | ||
|
|
25e87d8558 | ||
|
|
66295e07b9 | ||
|
|
6a3c9552c5 | ||
|
|
0b82613d6f | ||
|
|
4d0d9f6fd0 | ||
|
|
34dbe0e952 | ||
|
|
9358823214 | ||
|
|
148b87f6fe | ||
|
|
9c75ea15c2 | ||
|
|
7e66fea502 | ||
|
|
38f0901d38 | ||
|
|
6df11f0f28 | ||
|
|
3a0db72c17 | ||
|
|
116bf7490d | ||
|
|
648448b46a | ||
|
|
ee63bf16da | ||
|
|
d3eeba7967 | ||
|
|
109020ed2d | ||
|
|
fd990007be | ||
|
|
e158f22d51 | ||
|
|
8f21eb17be | ||
|
|
aac31ae2fb | ||
|
|
b882b84e47 | ||
|
|
243085f047 | ||
|
|
df6f886390 | ||
|
|
6f21f84a71 | ||
|
|
393e0352e8 | ||
|
|
9459533408 | ||
|
|
d9f987df66 | ||
|
|
f97c1fb99b | ||
|
|
5cdddb2d46 | ||
|
|
e18a6e9eb0 | ||
|
|
c42b1cecf1 | ||
|
|
c940bed415 | ||
|
|
530f52dc9c | ||
|
|
cc1d977359 | ||
|
|
d2c7614c3a | ||
|
|
66d45de432 | ||
|
|
eaa861622a | ||
|
|
7c3b0fbee8 | ||
|
|
b98602705f | ||
|
|
b4d1e17765 | ||
|
|
4e93d9153e | ||
|
|
1001c837de | ||
|
|
18f54c08e2 | ||
|
|
5b7119ecef | ||
|
|
ddbd67cbde | ||
|
|
84ac771415 | ||
|
|
e792bb9705 | ||
|
|
7940a4c0e7 | ||
|
|
3d3159b37b | ||
|
|
0319c431de | ||
|
|
80ae9a50c9 | ||
|
|
006f1e6ec6 | ||
|
|
f185952832 | ||
|
|
d5c6a83aeb | ||
|
|
2e8e511106 | ||
|
|
ec591cdf47 | ||
|
|
ee7fa96f6d | ||
|
|
d625d18b91 | ||
|
|
895eae340a | ||
|
|
9fad66fcfb | ||
|
|
dfd01af9aa | ||
|
|
735e88deba | ||
|
|
bb2f6652b7 | ||
|
|
6dcd7dd34b | ||
|
|
1327af5077 | ||
|
|
311f64987b | ||
|
|
08e5b97e57 | ||
|
|
d4a2945fcc | ||
|
|
9913c03128 | ||
|
|
c0e28d3401 | ||
|
|
8c42e7980c | ||
|
|
01ecd4e011 | ||
|
|
33da1f7d14 | ||
|
|
79b0dd3287 | ||
|
|
0787a44d99 | ||
|
|
fd11286290 | ||
|
|
22dc864d60 | ||
|
|
9d2f46ce4e | ||
|
|
b1ed7bb3bc | ||
|
|
ca08f5fbcf | ||
|
|
229029e112 | ||
|
|
3294577c17 | ||
|
|
8f51efbc3e | ||
|
|
ebdbb3cbd5 | ||
|
|
3a30bec9ef | ||
|
|
126b81916d | ||
|
|
9abea5b73b | ||
|
|
170e7aaeb9 | ||
|
|
5b570a2451 | ||
|
|
ffd7dddcc7 | ||
|
|
b0b13d3a16 | ||
|
|
b1781e4606 | ||
|
|
3580374b18 | ||
|
|
c6dd39bd83 | ||
|
|
dd8fe86308 | ||
|
|
2e12f05d5e | ||
|
|
bd033a4447 | ||
|
|
9d023cf77a | ||
|
|
8a69dbd9d4 | ||
|
|
147de6f4f7 | ||
|
|
e217e44661 | ||
|
|
871ef6b506 | ||
|
|
fac72f024a | ||
|
|
d4c332104d | ||
|
|
5af5df4e4f | ||
|
|
6c5cf87871 | ||
|
|
46f4108f23 | ||
|
|
1da439f8dc | ||
|
|
7af9d039fb | ||
|
|
de84624d70 | ||
|
|
cac121a67d | ||
|
|
604da878cb | ||
|
|
eaf0051162 | ||
|
|
8dcf71e02d | ||
|
|
92f0142a3e | ||
|
|
6c8347f465 | ||
|
|
a4d24056ef | ||
|
|
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 | ||
|
|
ce3fb78320 | ||
|
|
5b0b4583a8 | ||
|
|
a7ea5fb408 | ||
|
|
bc55e8bf0a | ||
|
|
c753cd282d | ||
|
|
07bd78c4e6 | ||
|
|
d7db575b96 | ||
|
|
7154e181e6 | ||
|
|
0b04ab1040 | ||
|
|
34b049f138 | ||
|
|
3bf1908dcb | ||
|
|
4a79413fe6 | ||
|
|
e250df5d5d | ||
|
|
ddc2c6e845 | ||
|
|
fd0aae31e3 | ||
|
|
2a83588552 | ||
|
|
a1e3efb81c | ||
|
|
c0e4b16ab9 | ||
|
|
40b63b834d | ||
|
|
4db49f5485 | ||
|
|
23a6e88b0c | ||
|
|
4ca5d7cd2a | ||
|
|
9879937c63 | ||
|
|
c18f6cba12 | ||
|
|
93542c397c | ||
|
|
7a5515e3e0 | ||
|
|
a6da0692ce | ||
|
|
c9765e9dc2 | ||
|
|
53a38b70b2 | ||
|
|
fab2a86a3f | ||
|
|
d0a527bfd5 | ||
|
|
f11f1b2ef9 | ||
|
|
056a543dd9 | ||
|
|
bf6eacdf25 | ||
|
|
909d53b9b6 | ||
|
|
2c4974286f | ||
|
|
6e2f4c00ab | ||
|
|
d371852317 | ||
|
|
c7b505db47 | ||
|
|
7ddddff94f | ||
|
|
4136cb5969 | ||
|
|
a3e5b541dc | ||
|
|
9195da8c1a | ||
|
|
606879f2d3 | ||
|
|
f2dc466d03 | ||
|
|
0618babf0c | ||
|
|
886ba0f662 | ||
|
|
8e9a703abc | ||
|
|
4dba644b8d | ||
|
|
4cedf1c603 | ||
|
|
796361d9f9 | ||
|
|
22aff1bc25 | ||
|
|
86ef5eed8a | ||
|
|
a4f8eb3544 | ||
|
|
270c626d6b | ||
|
|
ae0c33b395 | ||
|
|
2a1edad716 | ||
|
|
c2b03ead3e | ||
|
|
d198c90f7a | ||
|
|
6c6c51ef3b | ||
|
|
a6345190fa | ||
|
|
ebf48d4122 | ||
|
|
9213ca194d | ||
|
|
b371ae35d1 | ||
|
|
92ee0fda43 | ||
|
|
5baa3de6df | ||
|
|
a561ae0f9d | ||
|
|
7c99262c00 | ||
|
|
277abc5ffa | ||
|
|
a70a6346c5 | ||
|
|
859ebaf371 | ||
|
|
39d9b00eeb | ||
|
|
e577a9fbdf | ||
|
|
77c57bdfa8 | ||
|
|
81812d05a3 | ||
|
|
cc25f71674 | ||
|
|
6853b08120 | ||
|
|
c38a3d7026 | ||
|
|
2090f0ddce | ||
|
|
cec051f4b8 | ||
|
|
89d5bd58f5 | ||
|
|
4586062efb | ||
|
|
1efccd03b5 | ||
|
|
c4fdf66efc | ||
|
|
75f6b45a75 | ||
|
|
c2e19fb391 | ||
|
|
b29dda3b4d | ||
|
|
0ec6d97029 | ||
|
|
36e40805e8 | ||
|
|
fff9ec32ed | ||
|
|
f4291e7492 | ||
|
|
9da4ede616 | ||
|
|
77d3a5f98e | ||
|
|
9535d168fa | ||
|
|
70fae8d0b7 | ||
|
|
7699db3017 | ||
|
|
6dceb29008 | ||
|
|
035f5a2297 | ||
|
|
951d453a2e | ||
|
|
b05d91ee01 | ||
|
|
cbffaf76d2 | ||
|
|
46a7bf14dc | ||
|
|
1e2942dd68 | ||
|
|
de4fe44dc8 | ||
|
|
96e0efe076 | ||
|
|
84ffe27f78 | ||
|
|
129137d894 | ||
|
|
35a8d6486e | ||
|
|
74c99e0d93 | ||
|
|
86248929ed | ||
|
|
5fee29256d | ||
|
|
d4af0ac229 | ||
|
|
7da7df1efc | ||
|
|
9deb1b6794 | ||
|
|
271c5fe6e4 | ||
|
|
c716cfaeb8 | ||
|
|
28ad6e4b40 | ||
|
|
13599a1a10 | ||
|
|
a1bebe5362 | ||
|
|
687981060f | ||
|
|
29cb13893f | ||
|
|
099d397aee | ||
|
|
d7a6d04353 | ||
|
|
72222654f4 | ||
|
|
7a8b8012ff | ||
|
|
0ccf1dc016 | ||
|
|
ba11212a13 | ||
|
|
6d0890e7e0 | ||
|
|
cf7586ce7e | ||
|
|
137dc1a2cf | ||
|
|
1646b08659 | ||
|
|
a873dd9cbf | ||
|
|
e8ba0d49ed | ||
|
|
f0feaffbaa | ||
|
|
80a54b0226 | ||
|
|
76dba0b9ba | ||
|
|
391249a1ca | ||
|
|
0954ffaa65 | ||
|
|
753cbd1ca5 | ||
|
|
825d89818a | ||
|
|
62a06d28b5 | ||
|
|
647b509c74 | ||
|
|
f5e60d647f | ||
|
|
adc991c47b | ||
|
|
cc50337ecd | ||
|
|
db6d3095b9 | ||
|
|
f6a95d3d0b | ||
|
|
028455f481 | ||
|
|
05d5c9bada | ||
|
|
f34ae7731d | ||
|
|
4f888e27c3 | ||
|
|
e5d0cc0b0d | ||
|
|
a449142802 | ||
|
|
82a1f99268 | ||
|
|
1280565f7f | ||
|
|
386d36f5ad | ||
|
|
be0e07ac31 | ||
|
|
001f11c755 | ||
|
|
5f37a3bb02 | ||
|
|
5311f88684 | ||
|
|
81ef5fac43 | ||
|
|
53b641caaf | ||
|
|
ae6b3d40b5 | ||
|
|
061ea627e2 | ||
|
|
297e4a6099 | ||
|
|
e8bb208374 | ||
|
|
18ff2c57c2 | ||
|
|
98b83afe63 | ||
|
|
25428deff2 | ||
|
|
ce71f445de | ||
|
|
12d548865f | ||
|
|
9afd31017a | ||
|
|
7508943e06 | ||
|
|
26c5e18e62 | ||
|
|
c655dbdaab | ||
|
|
39a63b5912 | ||
|
|
47e5d3c2cd | ||
|
|
a63e11a1c4 | ||
|
|
df94b5db67 | ||
|
|
8946bfb2e8 | ||
|
|
36049d86a2 | ||
|
|
229f8570b4 | ||
|
|
5b32ee7d7b | ||
|
|
3f3bd9cff6 | ||
|
|
8df6d59adf | ||
|
|
4135c0a01d | ||
|
|
6282bc5a83 | ||
|
|
ddbcf561db | ||
|
|
f21bb8376a | ||
|
|
a5b2e543e5 | ||
|
|
bd9675d05b | ||
|
|
661ba2d0de | ||
|
|
f5960020cd | ||
|
|
c824c7e87a |
@@ -0,0 +1,122 @@
|
||||
---
|
||||
name: changelog-entry
|
||||
description: Generate a new changelog entry for cibuildwheel based on all changes since the last tag. Use when updating the changelog or preparing a release.
|
||||
---
|
||||
|
||||
# Generate Changelog Entry
|
||||
|
||||
Produce a new version section at the top of `docs/changelog.md` summarizing all changes since the last git tag.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Find the last tag** — run `git tag --sort=-version:refname | head -1` to get the most recent version tag.
|
||||
2. **Gather changes** — run `git log <last-tag>..HEAD --oneline` and `git log <last-tag>..HEAD --format="%H %s"` to see all commits since that tag.
|
||||
3. **Read each merge/commit** — for substantive commits, read the full message and any linked PRs to understand the change. Use `git log <last-tag>..HEAD --format="---%n%B"` for full messages. If a commit message is ambiguous, use `gh pr view <number>` to get richer context from GitHub.
|
||||
4. **Classify and draft entries** — assign each change an emoji and write a one-line description following the style rules below. Combine related small PRs of the same type (bot dependency updates, dependabot CI action bumps, pre-commit autoupdates) into a single entry listing all PR numbers. Skip meta-changelog PRs (e.g., "Add missing CHANGELOG entries") — they don't describe user-facing changes. If a bot/dependency PR contains a substantive fix mixed in (e.g., a pip revert inside a dependency update), split it: mention the fix separately under its own category.
|
||||
5. **Determine the new version number** — inspect commits for breaking changes or new features to decide patch/minor/major bump. Ask the user if unclear.
|
||||
6. **Determine the date** — use today's date.
|
||||
7. **Insert the new section** — add it at the top of `docs/changelog.md`, right after the `# Changelog` heading and a blank line, before any existing version sections.
|
||||
|
||||
## Style Rules
|
||||
|
||||
These are non-negotiable formatting conventions derived from the existing changelog.
|
||||
|
||||
### Version heading
|
||||
|
||||
```markdown
|
||||
### v3.4.2
|
||||
```
|
||||
|
||||
`###` heading, `v` prefix, full semver.
|
||||
|
||||
### Date line
|
||||
|
||||
```markdown
|
||||
_14 May 2026_
|
||||
```
|
||||
|
||||
Italic (underscore-wrapped), day without leading zero, full month name, 4-digit year. One blank line after the date.
|
||||
|
||||
### Entry format
|
||||
|
||||
```markdown
|
||||
- <emoji> <Description> (#<PR number>)
|
||||
```
|
||||
|
||||
- Each entry is a single bullet starting with `- `.
|
||||
- Emoji immediately after the dash-space.
|
||||
- Space between emoji and description text.
|
||||
- PR number(s) in parens at end: `(#1234)` or `(#1234, #5678)`.
|
||||
- No trailing period for single-sentence entries.
|
||||
- Period at end of multi-sentence entries only.
|
||||
|
||||
### Emoji categories
|
||||
|
||||
Use exactly one emoji per entry, chosen by category:
|
||||
|
||||
| Emoji | Category | Used for |
|
||||
|-------|----------|----------|
|
||||
| 🌟 | Major feature | New platforms, significant new capabilities |
|
||||
| ✨ | Feature | New features, additions, user-visible enhancements |
|
||||
| 🐛 | Bug fix | Bug fixes |
|
||||
| 🛠 | Maintenance | Dep updates, internal improvements, behavior tweaks |
|
||||
| ⚠️ | Warning | Deprecations, breaking changes, dropped support |
|
||||
| 📚 | Docs | Documentation changes |
|
||||
| 💼 | Internal | Non-user-facing infra/tooling changes |
|
||||
| 🧪 | Tests | Test changes |
|
||||
| 🔐 | Security | Security-related changes (used in past changelogs) |
|
||||
|
||||
### Category disambiguation
|
||||
|
||||
When a change could fit multiple categories, use these tiebreakers:
|
||||
|
||||
- **CI/workflow fixes** → 💼 (not 🧪) — they fix infra, not test logic.
|
||||
- **Test suite changes** → 🧪 — only for changes to the test code itself.
|
||||
- **Diagnostic output changes** (e.g., printing more info during builds) → 🛠 (not 🐛) — they're improvements, not bug fixes.
|
||||
- **A bug fix that also changes test code** → 🐛 — the user-facing fix takes priority; test changes are implicit.
|
||||
|
||||
### Writing style
|
||||
|
||||
- **Present tense**: "Adds", "Fixes", "Updates", not "Added", "Fixed".
|
||||
- **Sentence case**: capitalize only the first word after the emoji.
|
||||
- **Link option names to docs**: `[`option-name`](https://cibuildwheel.pypa.io/en/stable/options/#option-name)`. Option anchors match the option name — verify in `docs/options.md` by searching for `{: #option-name }`.
|
||||
- Be specific about what changed and why, not just that something changed.
|
||||
|
||||
### Ordering
|
||||
|
||||
Within a version section, order entries by importance:
|
||||
|
||||
1. 🌟 entries first
|
||||
2. ⚠️ entries next
|
||||
3. ✨ entries
|
||||
4. 🐛 entries
|
||||
5. 🛠 entries
|
||||
6. 📚 entries
|
||||
7. 💼 entries
|
||||
8. 🧪 entries last
|
||||
|
||||
### Multi-line entries
|
||||
|
||||
For complex features needing explanation, use an indented italic paragraph:
|
||||
|
||||
```markdown
|
||||
- ✨ Short summary here. (#1234)
|
||||
|
||||
_Longer explanation with details and caveats._ (#1234)
|
||||
```
|
||||
|
||||
Adding a new Python beta version always has a specific longer explanation, check for a previous addition (like 3.14) for the note to use.
|
||||
|
||||
### Blank lines
|
||||
|
||||
- One blank line between version sections.
|
||||
- No blank lines between bullets within a version.
|
||||
|
||||
## Validation
|
||||
|
||||
After inserting the new section:
|
||||
|
||||
1. Check that the new section follows all style rules above.
|
||||
2. Verify PR numbers match actual PRs in the commit log.
|
||||
3. Ensure no duplicate entries — multiple commits to the same PR should produce one entry.
|
||||
4. Run a final review of the file to confirm formatting is consistent with surrounding entries.
|
||||
+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
|
||||
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
run_tests: &RUN_TESTS
|
||||
install_cibuildwheel_script:
|
||||
- python -m pip install -e ".[dev]" pytest-custom-exit-code
|
||||
run_cibuildwheel_tests_script:
|
||||
- python ./bin/run_tests.py
|
||||
|
||||
|
||||
linux_x86_task:
|
||||
timeout_in: 120m
|
||||
compute_engine_instance:
|
||||
image_project: cirrus-images
|
||||
image: family/docker-builder
|
||||
platform: linux
|
||||
cpu: 8
|
||||
memory: 8G
|
||||
|
||||
install_pre_requirements_script:
|
||||
- apt install -y python3-venv python-is-python3
|
||||
<<: *RUN_TESTS
|
||||
|
||||
linux_aarch64_task:
|
||||
compute_engine_instance:
|
||||
image_project: cirrus-images
|
||||
image: family/docker-builder-arm64
|
||||
architecture: arm64
|
||||
platform: linux
|
||||
cpu: 4
|
||||
memory: 4G
|
||||
|
||||
install_pre_requirements_script:
|
||||
- apt install -y python3-venv python-is-python3
|
||||
<<: *RUN_TESTS
|
||||
|
||||
windows_x86_task:
|
||||
# The task takes ~55 minutes while the timeout happens
|
||||
# after 60 minutes by default, let's allow some wiggle room.
|
||||
timeout_in: 120m
|
||||
windows_container:
|
||||
image: cirrusci/windowsservercore:visualstudio2022
|
||||
cpu: 8
|
||||
memory: 8G
|
||||
|
||||
install_pre_requirements_script:
|
||||
- choco install -y --no-progress python3 --version 3.10.6
|
||||
- refreshenv
|
||||
- echo PATH=%PATH% >> "%CIRRUS_ENV%"
|
||||
<<: *RUN_TESTS
|
||||
|
||||
macos_arm64_task:
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-monterey-xcode
|
||||
|
||||
env:
|
||||
PATH: /opt/homebrew/opt/python@3.10/bin:$PATH
|
||||
install_pre_requirements_script:
|
||||
- brew install python@3.10
|
||||
- ln -s python3 /opt/homebrew/opt/python@3.10/bin/python
|
||||
<<: *RUN_TESTS
|
||||
|
||||
macos_arm64_cp38_task:
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-monterey-xcode
|
||||
|
||||
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'
|
||||
install_pre_requirements_script:
|
||||
- brew install python@3.10
|
||||
- ln -s python3 /opt/homebrew/opt/python@3.10/bin/python
|
||||
- curl -fsSLO https://www.python.org/ftp/python/3.8.10/python-3.8.10-macos11.pkg
|
||||
- sudo installer -pkg python-3.8.10-macos11.pkg -target /
|
||||
- rm python-3.8.10-macos11.pkg
|
||||
- sh "/Applications/Python 3.8/Install Certificates.command"
|
||||
<<: *RUN_TESTS
|
||||
@@ -0,0 +1,6 @@
|
||||
* text=auto
|
||||
|
||||
*.py diff=python
|
||||
*.md diff=markdown
|
||||
|
||||
*.svg -diff
|
||||
@@ -0,0 +1,7 @@
|
||||
# Specification files for platform maintainers
|
||||
cibuildwheel/platforms/ios.py @freakboy3742
|
||||
cibuildwheel/platforms/pyodide.py @hoodmane @ryanking13 @agriyakhetarpal
|
||||
cibuildwheel/platforms/android.py @mhsmith
|
||||
|
||||
# Constraints files
|
||||
cibuildwheel/resources/constraints-pyodide*.txt @hoodmane @ryanking13 @agriyakhetarpal
|
||||
@@ -5,3 +5,9 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
name: "Copilot Setup Steps"
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
|
||||
jobs:
|
||||
copilot-setup-steps:
|
||||
name: Copilot Setup Steps
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
allow-prereleases: true
|
||||
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
|
||||
- name: Install tooling
|
||||
run: |
|
||||
uv tool install nox
|
||||
uv tool install prek
|
||||
|
||||
- name: Pre-install checks
|
||||
run: |
|
||||
prek prepare-hooks
|
||||
@@ -7,34 +7,40 @@ on:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
dist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build SDist and wheel
|
||||
run: pipx run build
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
path: dist/*
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check metadata
|
||||
run: pipx run twine check dist/*
|
||||
- uses: hynek/build-and-inspect-python-package@d44ca7d91762de7a7d5436ddae667c6da6d1c3df # v2.18.0
|
||||
|
||||
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@v3
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: artifact
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- uses: pypa/gh-action-pypi-publish@release/v1
|
||||
- name: Generate artifact attestation for sdist and wheel
|
||||
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
||||
with:
|
||||
subject-path: "dist/cibuildwheel-*"
|
||||
|
||||
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
with:
|
||||
attestations: true
|
||||
|
||||
+306
-48
@@ -4,70 +4,178 @@ 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
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: test-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Linters (mypy, flake8, etc.)
|
||||
name: Linters (mypy, ruff, etc.)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- uses: pre-commit/action@v3.0.0
|
||||
- name: Check manifest
|
||||
run: pipx run nox -s check_manifest
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
- name: PyLint checks
|
||||
run: |
|
||||
echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json"
|
||||
pipx run nox -s pylint
|
||||
run: uvx nox -s pylint -- --output-format=github
|
||||
|
||||
sample:
|
||||
name: Generate a sample project
|
||||
needs: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- name: Install dependencies
|
||||
run: uv sync --no-dev --group test
|
||||
- name: Generate a sample project
|
||||
run: uv run --no-sync -m test.test_projects test.test_0_basic.basic_project sample_proj
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sample_proj
|
||||
path: sample_proj
|
||||
|
||||
test:
|
||||
name: Test cibuildwheel on ${{ matrix.os }}
|
||||
needs: lint
|
||||
name: Test on ${{ matrix.os }} (${{ matrix.python_version }}) ${{ matrix.test_select }}
|
||||
needs: sample
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-11]
|
||||
python_version: ['3.11']
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
python_version: '3.8'
|
||||
# Min Python
|
||||
- os: ubuntu-latest
|
||||
python_version: '3.11'
|
||||
# Max Python
|
||||
- os: ubuntu-latest
|
||||
python_version: '3.15'
|
||||
- os: ubuntu-latest
|
||||
python_version: '3.14'
|
||||
test_select: android
|
||||
- os: ubuntu-24.04-arm
|
||||
python_version: '3.13'
|
||||
- os: windows-latest
|
||||
python_version: '3.13'
|
||||
- os: windows-11-arm
|
||||
python_version: '3.13'
|
||||
- os: macos-15-intel
|
||||
python_version: '3.13'
|
||||
- os: macos-15
|
||||
python_version: '3.13'
|
||||
- os: macos-15-intel
|
||||
python_version: '3.13'
|
||||
test_select: ios
|
||||
- os: macos-15
|
||||
python_version: '3.13'
|
||||
test_select: ios
|
||||
# Exercise iOS on a non-default simulator.
|
||||
test_runtime: 'args: --simulator "iPhone 16e,OS=18.5"'
|
||||
- os: macos-15-intel
|
||||
python_version: '3.13'
|
||||
test_select: android
|
||||
# Exercise Android on a non-default simulator
|
||||
test_runtime: 'args: --managed minVersion'
|
||||
- os: macos-15
|
||||
python_version: '3.13'
|
||||
test_select: android
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
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'
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
|
||||
- name: Free up disk space
|
||||
if: runner.os == 'Linux' && matrix.test_select != 'android'
|
||||
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
|
||||
sudo rm -rf $ANDROID_HOME/ndk /opt/hostedtoolcache/CodeQL \
|
||||
/usr/local/lib/node_modules /usr/local/share/chromium \
|
||||
/usr/local/share/powershell
|
||||
df -m
|
||||
|
||||
# for oci_container unit tests
|
||||
- name: Set up QEMU
|
||||
if: runner.os == 'Linux'
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Set up Xcode
|
||||
if: ${{ startsWith(matrix.os, 'macos-15') }}
|
||||
run: |
|
||||
# GitHub recommends explicitly selecting the desired Xcode version
|
||||
sudo xcode-select --switch /Applications/Xcode_16.4.app
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install ".[test]"
|
||||
run: uv sync --no-dev --group test
|
||||
|
||||
- name: Generate a sample project
|
||||
- uses: joerick/pr-labels-action@0543b277721e852d821c6738d449f2f4dea03d5f # v1.0.9
|
||||
|
||||
- name: Set CIBW_ENABLE
|
||||
shell: bash
|
||||
run: |
|
||||
python -m test.test_projects test.test_0_basic.basic_project sample_proj
|
||||
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: Download a sample project
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: sample_proj
|
||||
path: sample_proj
|
||||
|
||||
- name: Run a sample build (GitHub Action)
|
||||
uses: ./
|
||||
@@ -76,36 +184,186 @@ jobs:
|
||||
output-dir: wheelhouse
|
||||
env:
|
||||
CIBW_ARCHS_MACOS: x86_64 universal2 arm64
|
||||
CIBW_BUILD_FRONTEND: ${{ matrix.test_select && 'build' || 'build[uv]' }}
|
||||
CIBW_PLATFORM: ${{ matrix.test_select }}
|
||||
CIBW_TEST_RUNTIME: ${{ matrix.test_runtime }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- name: Run a sample build (GitHub Action, only)
|
||||
uses: ./
|
||||
if: matrix.test_select == ''
|
||||
with:
|
||||
name: sample_wheels_action
|
||||
path: wheelhouse
|
||||
package-dir: sample_proj
|
||||
output-dir: wheelhouse_only
|
||||
only: cp313-${{ runner.os == 'Linux' && (runner.arch == 'ARM64' && 'manylinux_aarch64' || 'manylinux_x86_64') || (runner.os == 'Windows' && 'win_amd64' || 'macosx_x86_64') }}
|
||||
|
||||
- name: Create custom configuration file
|
||||
shell: bash
|
||||
run: |
|
||||
cat > sample_proj/cibw.toml <<EOF
|
||||
[tool.cibuildwheel]
|
||||
# Only build on CPython 3.13 on native arch
|
||||
archs = ["native"]
|
||||
build = "cp313-*"
|
||||
# Skip musllinux
|
||||
skip = "*-musllinux*"
|
||||
EOF
|
||||
|
||||
- name: Run a sample build (GitHub Action, config-file)
|
||||
uses: ./
|
||||
env:
|
||||
CIBW_PLATFORM: ${{ matrix.test_select }}
|
||||
CIBW_TEST_RUNTIME: ${{ matrix.test_runtime }}
|
||||
with:
|
||||
package-dir: sample_proj
|
||||
output-dir: wheelhouse_config_file
|
||||
config-file: sample_proj/cibw.toml
|
||||
|
||||
- name: Check Action artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
test $(find wheelhouse -name '*.whl' | wc -l) -ge 1
|
||||
test $(find wheelhouse_config_file -name '*.whl' | wc -l) -eq 1
|
||||
|
||||
- name: Check Action artifacts (native build only)
|
||||
if: matrix.test_select == ''
|
||||
shell: bash
|
||||
run: |
|
||||
test $(find wheelhouse_only -name '*.whl' | wc -l) -eq 1
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
|
||||
path: wheelhouse/*.whl
|
||||
|
||||
- name: Test cibuildwheel
|
||||
run: |
|
||||
python ./bin/run_tests.py --run-podman
|
||||
env:
|
||||
CIBW_TEST_RUNTIME: ${{ matrix.test_runtime }}
|
||||
run: uv run --no-sync bin/run_tests.py --test-select=${{ matrix.test_select || 'native' }} ${{ (runner.os == 'Linux' && runner.arch == 'X64') && '--run-podman' || '' }}
|
||||
|
||||
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@v3
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- name: Install dependencies
|
||||
run: uv sync --no-dev --group test
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: all
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Run the emulation tests
|
||||
run: |
|
||||
pytest --run-emulation test/test_emulation.py
|
||||
env:
|
||||
MATRIX_ARCH: ${{ matrix.arch }}
|
||||
run: uv run --no-sync pytest --run-emulation "$MATRIX_ARCH" test/test_emulation.py
|
||||
|
||||
test-pyodide:
|
||||
name: Test pyodide
|
||||
needs: sample
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
name: Install Python 3.12
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --no-dev --group test
|
||||
|
||||
- name: Download a sample project
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: sample_proj
|
||||
path: sample_proj
|
||||
|
||||
- name: Run a sample build (GitHub Action)
|
||||
uses: ./
|
||||
with:
|
||||
package-dir: sample_proj
|
||||
output-dir: wheelhouse
|
||||
env:
|
||||
CIBW_PLATFORM: pyodide
|
||||
CIBW_ENABLE: pyodide-prerelease
|
||||
|
||||
- name: Run tests with 'CIBW_PLATFORM' set to 'pyodide'
|
||||
run: uv run --no-sync ./bin/run_tests.py
|
||||
env:
|
||||
CIBW_PLATFORM: pyodide
|
||||
CIBW_ENABLE: pyodide-prerelease
|
||||
|
||||
test-uv-extras:
|
||||
name: Test uv extra on ${{ matrix.os }} ${{ matrix.test_select }}
|
||||
needs: sample
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
test_select: android
|
||||
- os: windows-latest
|
||||
- os: windows-11-arm
|
||||
- os: macos-15-intel
|
||||
- os: macos-15
|
||||
- os: macos-15-intel
|
||||
test_select: android
|
||||
- os: macos-15
|
||||
test_select: android
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download a sample project
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: sample_proj
|
||||
path: sample_proj
|
||||
- name: Run a sample build (GitHub Action)
|
||||
uses: ./
|
||||
with:
|
||||
extras: uv
|
||||
package-dir: sample_proj
|
||||
output-dir: wheelhouse
|
||||
env:
|
||||
CIBW_ARCHS_MACOS: x86_64 universal2 arm64
|
||||
CIBW_BUILD: cp314-*
|
||||
CIBW_BUILD_FRONTEND: 'build[uv]'
|
||||
CIBW_PLATFORM: ${{ matrix.test_select }}
|
||||
|
||||
@@ -14,37 +14,47 @@ on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # "At 06:00 on Monday."
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
update-dependencies:
|
||||
name: Update dependencies
|
||||
if: github.repository_owner == 'pypa' || github.event_name != 'schedule'
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
environment: ${{ github.ref == 'refs/heads/main' && github.repository == 'pypa/cibuildwheel' && 'update-dependencies-workflow' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: wntrblm/nox@2023.04.22
|
||||
with:
|
||||
python-versions: "3.7, 3.8, 3.9, 3.10, 3.11, 3.12-dev"
|
||||
|
||||
- name: "Run update: dependencies"
|
||||
run: nox --force-color -s update_constraints
|
||||
- name: "Run update: python configs"
|
||||
run: nox --force-color -s update_pins
|
||||
- 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@v1
|
||||
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
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 }}
|
||||
client-id: ${{ secrets.CIBUILDWHEEL_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.CIBUILDWHEEL_BOT_APP_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
|
||||
- name: "Run update: dependencies"
|
||||
run: uvx nox --force-color -s update_constraints
|
||||
- name: "Run update: python configs"
|
||||
run: uvx nox --force-color -s update_pins
|
||||
- name: "Run update: docs user projects"
|
||||
run: uvx nox --force-color -s update_proj -- --auth=${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
commit-message: Update dependencies
|
||||
title: '[Bot] Update dependencies'
|
||||
@@ -53,7 +63,10 @@ jobs:
|
||||
|
||||
PR generated by "Update dependencies" [workflow](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}).
|
||||
branch: update-dependencies-pr
|
||||
committer: "cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>"
|
||||
author: "cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>"
|
||||
sign-commits: true
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
delete-branch: true
|
||||
labels: |
|
||||
CI: GraalPy
|
||||
CI: PyPy
|
||||
dependencies
|
||||
|
||||
@@ -21,10 +21,12 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Update the ${{ env.TAG_NAME }} tag
|
||||
id: update-major-minor-tag
|
||||
uses: joerick/update-vX.Y-tag-action@v1.0
|
||||
uses: joerick/update-vX.Y-tag-action@c4cefb60c33b82e4aa73a469e4acf30ee48d5812 # v1.0.2
|
||||
with:
|
||||
source-tag: ${{ env.TAG_NAME }}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: zizmor - GitHub Actions Security Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Run zizmor 🌈
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
+11
-6
@@ -84,9 +84,7 @@ celerybeat-schedule
|
||||
|
||||
# virtualenv
|
||||
.venv
|
||||
venv/
|
||||
venv3/
|
||||
venv2/
|
||||
venv*/
|
||||
ENV/
|
||||
env/
|
||||
env2/
|
||||
@@ -112,8 +110,15 @@ all_known_setup.yaml
|
||||
# mkdocs
|
||||
site/
|
||||
|
||||
# Virtual environments
|
||||
venv*
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
# Lockfiles
|
||||
*.lock
|
||||
*.pylock
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
|
||||
# This file should be a symlink or contain "See @AGENTS.md"
|
||||
CLAUDE.md
|
||||
|
||||
+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
|
||||
|
||||
+54
-35
@@ -1,7 +1,10 @@
|
||||
repos:
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
exclude: "^cibuildwheel/resources/ios-support/(?!make_cross_venv\\.py)"
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
@@ -12,52 +15,50 @@ repos:
|
||||
exclude: (cibuildwheel/resources/pinned_docker_images.cfg)|(.svg$)
|
||||
- id: mixed-line-ending
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: ^cibuildwheel/resources/android/android.patch$
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.276
|
||||
rev: 0c7b6c989466a93942def1f84baf36ddfcd60c83 # frozen: v0.15.14
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix", "--show-fixes"]
|
||||
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: [--include-version-classifiers, --min-py-version=3.8, --max-py-version=3.11]
|
||||
- id: ruff-check
|
||||
args: ["--fix"]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.4.1
|
||||
rev: fc0f09a29bb495f4a91f00266155d6282d52485d # frozen: v1.20.2
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy 3.8 on cibuildwheel/
|
||||
exclude: ^cibuildwheel/resources/.*py$
|
||||
args: ["--python-version=3.8"]
|
||||
name: mypy 3.11 on cibuildwheel/
|
||||
args: ["--python-version=3.11"]
|
||||
exclude: ^cibuildwheel/resources/android/_cross_venv.py$ # Requires Python 3.13 or later
|
||||
additional_dependencies: &mypy-dependencies
|
||||
- nox
|
||||
- bracex
|
||||
- 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
|
||||
- bracex
|
||||
- types-setuptools
|
||||
- uv
|
||||
- validate-pyproject
|
||||
- id: mypy
|
||||
name: mypy 3.11
|
||||
exclude: ^cibuildwheel/resources/.*py$
|
||||
args: ["--python-version=3.11"]
|
||||
name: mypy 3.14
|
||||
args: ["--python-version=3.14"]
|
||||
additional_dependencies: *mypy-dependencies
|
||||
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: v0.9.0.5
|
||||
rev: 745eface02aef23e168a8afb6b5737818efbea95 # frozen: v0.11.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
|
||||
@@ -67,18 +68,36 @@ 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.5
|
||||
rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2
|
||||
hooks:
|
||||
- id: codespell
|
||||
args: ["-L", "sur"]
|
||||
args: ["-w"]
|
||||
exclude: ^docs/working-examples\.md$ # Autogenerated
|
||||
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 943377262562a12b57292fc98fabd7dbf81451fe # frozen: 0.37.2
|
||||
hooks:
|
||||
- id: check-dependabot
|
||||
- id: check-github-actions
|
||||
- id: check-github-workflows
|
||||
- id: check-gitlab-ci
|
||||
- id: check-readthedocs
|
||||
- id: check-travis
|
||||
- id: check-jsonschema
|
||||
name: Check projects
|
||||
args: [--schemafile, docs/data/projects.schema.json]
|
||||
files: '^docs/data/projects.yml$'
|
||||
- id: check-metaschema
|
||||
files: '^docs/data/projects.schema.json$'
|
||||
|
||||
+7
-10
@@ -2,13 +2,10 @@
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
version: 2
|
||||
|
||||
mkdocs:
|
||||
configuration: mkdocs.yml
|
||||
|
||||
python:
|
||||
version: 3.8
|
||||
install:
|
||||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
build:
|
||||
os: ubuntu-24.04
|
||||
commands:
|
||||
- asdf plugin add uv
|
||||
- asdf install uv latest
|
||||
- asdf global uv latest
|
||||
- NO_COLOR=1 uv run --python 3.14 --managed-python --no-dev --group docs mkdocs build --strict --site-dir $READTHEDOCS_OUTPUT/html
|
||||
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
os: linux
|
||||
dist: focal
|
||||
language: python
|
||||
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- name: Linux | x86_64 + i686 | Python 3.9
|
||||
python: 3.9
|
||||
services: docker
|
||||
env: PYTHON=python
|
||||
|
||||
- name: Linux | arm64 | Python 3.9
|
||||
python: 3.9
|
||||
services: docker
|
||||
arch: arm64-graviton2
|
||||
group: edge
|
||||
virt: vm
|
||||
env: PYTHON=python
|
||||
|
||||
- name: Linux | ppc64le | Python 3.9
|
||||
python: 3.9
|
||||
services: docker
|
||||
arch: ppc64le
|
||||
allow_failure: True
|
||||
env:
|
||||
- PYTHON=python
|
||||
# skip test_manylinuxXXXX_only, it uses too much disk space
|
||||
# c.f. https://travis-ci.community/t/running-out-of-disk-space-quota-when-using-docker-on-ppc64le/11634
|
||||
- PYTEST_ADDOPTS='-k "not test_manylinuxXXXX_only"'
|
||||
|
||||
- name: Windows | x86_64 | Python 3.9
|
||||
os: windows
|
||||
language: shell
|
||||
before_install:
|
||||
- choco upgrade python3 -y --version 3.9.13 --limit-output --params "/InstallDir:C:\\Python39"
|
||||
env:
|
||||
- PYTHON=C:\\Python39\\python
|
||||
|
||||
- name: Linux | s390x | Python 3.9
|
||||
python: 3.9
|
||||
services: docker
|
||||
arch: s390x
|
||||
env: PYTHON=python
|
||||
|
||||
install:
|
||||
- $PYTHON -m pip install -U pip
|
||||
- $PYTHON -m pip install -e ".[dev]" pytest-custom-exit-code
|
||||
|
||||
script: |
|
||||
# travis_wait disable the output while waiting
|
||||
# use the same kind of tricks as in multibuild
|
||||
(while true; do echo "travis_keep_alive"; sleep 300; done) &
|
||||
SPINNER_PID=$!
|
||||
disown
|
||||
$PYTHON ./bin/run_tests.py
|
||||
kill -9 ${SPINNER_PID}
|
||||
@@ -0,0 +1,57 @@
|
||||
# cibuildwheel — Agent Notes
|
||||
|
||||
## Always run
|
||||
|
||||
- `prek -a` should be run after changes to reformat, lint, and type check
|
||||
|
||||
## Developer commands
|
||||
- `uv run pytest unit_test` — Quick run for unit tests
|
||||
- `nox -s tests` — run doctests + unit tests + integration tests (slow, >30 min, requires system Python installs on macOS).
|
||||
- `nox -s tests -- unit_test` — fast unit tests only.
|
||||
- `nox -s tests -- test -k before_build` — single integration test/file via pytest `-k`.
|
||||
- `nox -s lint` — run all linters (pre-commit/prek).
|
||||
- `nox -s pylint` — run pylint separately (not in pre-commit).
|
||||
- `nox -s docs` — mkdocs serve (interactive) or build (non-interactive).
|
||||
- Set up local dev env at `.venv`: `uv sync` (dependency groups used).
|
||||
|
||||
## Project layout
|
||||
- `cibuildwheel/` — main package. Entry point: `cibuildwheel.__main__:main`.
|
||||
- `test/` — **integration tests** (expensive, run actual wheel builds).
|
||||
- `unit_test/` — **unit tests** (fast, no wheel builds).
|
||||
- `bin/` — maintainer scripts (update pins, generate README tables, schema, etc.).
|
||||
- `docs/` — mkdocs source.
|
||||
|
||||
## Testing specifics
|
||||
- Three test suites exist, run in this order by `bin/run_tests.py`:
|
||||
1. `pytest cibuildwheel` — doctests.
|
||||
2. `pytest unit_test [...]` — unit tests.
|
||||
3. `pytest test [...]` — integration tests (split into `serial` and `not serial` runs).
|
||||
- Serial integration tests **must not** run in parallel; non-serial use pytest-xdist by default.
|
||||
- Custom pytest options:
|
||||
- `--run-docker` (unit_test + test): run OCI container tests. Linux only.
|
||||
- `--run-podman`: run podman tests (Linux).
|
||||
- `--run-emulation` (test): run QEMU emulation tests (e.g., `--run-emulation aarch64`).
|
||||
- `--platform linux` (test): force integration tests to target Linux container builds even on macOS/Windows.
|
||||
- `--enable` (test): sets `CIBW_ENABLE` env var (e.g., `pypy`, `graalpy`).
|
||||
- Integration tests auto-set a default `CIBW_ENABLE` if the env var is absent.
|
||||
- The `build_frontend_env` fixture parameterizes over `pip`, `build`, `build[uv]`, `uv` and skips unsupported combos per platform.
|
||||
- Some integration tests require system Python.org installs on macOS; missing them prints a download URL in the error.
|
||||
- iOS/Android/pyodide tests have dedicated pytest marks (`ios`, `android`, `pyodide`) and need platform-specific runners/simulators.
|
||||
|
||||
## Lint / typecheck
|
||||
- Ruff (lint + format) and mypy run via pre-commit. Pylint runs separately via `nox -s pylint`.
|
||||
- Mypy is strict (`strict = true`) and targets Python 3.11 for the package, 3.14 for a second check in pre-commit.
|
||||
- Ruff config in `pyproject.toml` (`line-length = 100`).
|
||||
- Python 3.11 is the minimum supported version for the package itself.
|
||||
|
||||
## Generated / maintained files
|
||||
- `README.md` contains two **cog-generated** tables (options table, changelog preview). Pre-commit runs `cog -c -P -r -I ./bin README.md`. Edit the source scripts (`bin/readme_*.py`) or the upstream files (`docs/options.md`, `docs/changelog.md`) — do not hand-edit the generated blocks. Note the identifier is _not_ cog generated, and can be edited.
|
||||
- `cibuildwheel/resources/cibuildwheel.schema.json` is generated by `bin/generate_schema.py` (run via `nox -s generate_schema`).
|
||||
- `cibuildwheel/resources/constraints-*.txt` are generated via `nox -s update_constraints`.
|
||||
- `cibuildwheel/resources/pinned_docker_images.cfg` and other resource files are updated via `nox -s update_pins`.
|
||||
|
||||
## CI / release quirks
|
||||
- CI uses `uv sync --no-dev --group test` for test installs, then `uv run --no-sync` to execute.
|
||||
- The release workflow uses `hynek/build-and-inspect-python-package` for dist building.
|
||||
- `test.yml` skips unrelated paths to avoid burning CI time on docs-only changes.
|
||||
- A sample project artifact is built once and downloaded by downstream test jobs to avoid redundant work.
|
||||
@@ -1,11 +1,9 @@
|
||||
This is a summary of the Python versions and platforms covered by the different CI platforms:
|
||||
This is a summary of the host Python versions and platforms covered by the different CI platforms:
|
||||
|
||||
| | 3.8 | 3.9 | 3.10 | 3.11 |
|
||||
|---------|--------------------------------------------------------|-----------------------|-----------|----------------|
|
||||
| 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 | 3.14 |
|
||||
|---------|----------------------------------|-----------------------|----------------|----------------|
|
||||
| Linux | Azure Pipelines / GitHub Actions | CircleCI¹ / GitLab¹ | GitHub Actions | GitHub Actions |
|
||||
| macOS | Azure Pipelines | CircleCI¹ / GitLab¹ | GitHub Actions | |
|
||||
| Windows | Azure Pipelines | GitLab¹ | GitHub Actions | |
|
||||
|
||||
> ¹ Runs a reduced set of tests to reduce CI load
|
||||
|
||||
Non-x86 architectures are covered on Travis CI using Python 3.9.
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
This project is licensed under the 'BSD 2-clause license'.
|
||||
Copyright 2017, Joe Rickerby and contributors. All rights reserved.
|
||||
|
||||
Copyright (c) 2017-2019, 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:
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
@@ -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://circleci.com/gh/pypa/cibuildwheel)
|
||||
[](https://dev.azure.com/joerick0429/cibuildwheel/_build/latest?definitionId=4&branchName=main)
|
||||
[](https://cirrus-ci.com/github/pypa/cibuildwheel)
|
||||
|
||||
|
||||
[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, 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>
|
||||
<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 available using the [CIBW_PRERELEASE_PYTHONS](https://cibuildwheel.readthedocs.io/en/stable/options/#prerelease-pythons) option.</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.9 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | N/A |
|
||||
| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | N/A |
|
||||
| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | N/A |
|
||||
| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | ✅<sup>3</sup> |
|
||||
| CPython 3.13 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | ✅ | ✅ | ✅ |
|
||||
| CPython 3.14 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | ✅ | ✅ | ✅<sup>4</sup> |
|
||||
| CPython 3.15<sup>6</sup> | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | ✅ | ✅ | N/A |
|
||||
| PyPy 3.9 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅<sup>1</sup> | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅<sup>1</sup> | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| PyPy 3.11 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅<sup>1</sup> | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| GraalPy 3.11 v24.2 | ✅ | ✅ | ✅ | N/A | N/A | ✅<sup>1</sup> | N/A | ✅<sup>1</sup> | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| GraalPy 3.12 v25.0 | ✅ | ✅ | ✅ | N/A | N/A | ✅<sup>1</sup> | N/A | ✅<sup>1</sup> | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
|
||||
- Builds manylinux, musllinux, macOS 10.9+, and Windows wheels for CPython and PyPy
|
||||
- Works on GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, GitLab CI, and Cirrus CI
|
||||
- Bundles shared library dependencies on Linux and macOS through [auditwheel](https://github.com/pypa/auditwheel) and [delocate](https://github.com/matthew-brett/delocate)
|
||||
<sup>**1** PyPy & GraalPy are only supported for manylinux wheels.</sup><br>
|
||||
<sup>**2** Windows arm64 support is experimental.</sup><br>
|
||||
<sup>**3** Not supported on PyPI, uses old `pyodide` tag instead of `pyemscripten`. Requires `pyodide-eol` [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable).</sup><br>
|
||||
<sup>**4** Experimental alpha ABI, do not upload to PyPI yet.</sup><br>
|
||||
<sup>**5** 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>
|
||||
<sup>**6** Python 3.15 requires opt-in using [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable).</sup><br>
|
||||
|
||||
- Builds manylinux, musllinux, macOS, Windows, pyemscripten, iOS, and Android wheels
|
||||
- Supports CPython, PyPy, and GraalPy
|
||||
- Works on GitHub Actions, Azure Pipelines, CircleCI, and GitLab CI
|
||||
- Bundles shared library dependencies on Linux through [auditwheel](https://github.com/pypa/auditwheel), macOS through [delocate](https://github.com/matthew-brett/delocate), and Windows through [delvewheel](https://github.com/adang1345/delvewheel)
|
||||
- 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 | ✅ | ✅³ | ✅ | ✅ | ✅ | |
|
||||
| | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM | Android | iOS | Pyodide |
|
||||
| --------------- | ----- | ----- | ------- | -------------- | --------- | -------------- | -------------------------- | -------------------------- | -------------- |
|
||||
| GitHub Actions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅<sup>4</sup> | ✅<sup>3</sup> | ✅ |
|
||||
| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅<sup>2</sup> | ✅<sup>4</sup> | ✅<sup>3</sup> | ✅<sup>5</sup> |
|
||||
| CircleCI | ✅ | ✅ | | ✅ | ✅ | | ✅<sup>4</sup><sup>5</sup> | ✅<sup>3</sup><sup>5</sup> | ✅<sup>5</sup> |
|
||||
| GitLab CI | ✅ | ✅ | ✅ | ✅<sup>1</sup> | ✅ | | ✅<sup>4</sup><sup>5</sup> | ✅<sup>3</sup><sup>5</sup> | ✅<sup>5</sup> |
|
||||
|
||||
<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>
|
||||
<sup>**1** [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>**2** [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>**3** Requires a macOS runner; runs tests on the simulator for the runner's architecture. </sup><br>
|
||||
<sup>**4** Building for Android requires the runner to be Linux x86_64, macOS ARM64 or macOS x86_64. Testing has [additional requirements](https://cibuildwheel.pypa.io/en/stable/platforms/#android).</sup><br>
|
||||
<sup>**5** Builds may work, but are untested in cibuildwheel's CI.</sup><br>
|
||||
|
||||
<!--intro-end-->
|
||||
|
||||
@@ -77,6 +77,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
|
||||
|
||||
@@ -88,29 +89,34 @@ 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-15-intel, macos-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Used to host cibuildwheel
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/setup-python@v6
|
||||
|
||||
- name: Install cibuildwheel
|
||||
run: python -m pip install cibuildwheel==2.14.0
|
||||
run: python -m pip install cibuildwheel==4.0.0rc1
|
||||
|
||||
- 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@v3
|
||||
- uses: actions/upload-artifact@v6
|
||||
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 +125,57 @@ 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
|
||||
-------
|
||||
> [!WARNING]
|
||||
> Building and testing wheels executes arbitrary code from your project and its dependencies. Although cibuildwheel uses OCI containers and Pyodide for some builds, these provide no security guarantees - the code you're building and testing has full access to the environment that's invoking cibuildwheel.
|
||||
>
|
||||
> If you cannot trust all the code that's pulled in, maintain good security hygiene: keep the job that builds distributions separate from the job that uploads them to PyPI, handle secrets and credentials with care and rotate them regularly, and follow the principle of least privilege when granting permissions. Do not store sensitive data on CI runners.
|
||||
|
||||
|
||||
<!--[[[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 |
|
||||
| **Auditing** | [`audit-requires`](https://cibuildwheel.pypa.io/en/stable/options/#audit-requires) | Install Python dependencies for the audit step |
|
||||
| | [`audit-command`](https://cibuildwheel.pypa.io/en/stable/options/#audit-command) | Use a tool to check wheels before the end of the run |
|
||||
| **Testing** | [`test-command`](https://cibuildwheel.pypa.io/en/stable/options/#test-command) | The command to test each built wheel |
|
||||
| | [`before-test`](https://cibuildwheel.pypa.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
|
||||
| | [`test-sources`](https://cibuildwheel.pypa.io/en/stable/options/#test-sources) | Paths that are copied into the working directory of the tests |
|
||||
| | [`test-requires`](https://cibuildwheel.pypa.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
|
||||
| | [`test-extras`](https://cibuildwheel.pypa.io/en/stable/options/#test-extras) | Install your wheel for testing using `extras_require` |
|
||||
| | [`test-groups`](https://cibuildwheel.pypa.io/en/stable/options/#test-groups) | Specify test dependencies from your project's `dependency-groups` |
|
||||
| | [`test-skip`](https://cibuildwheel.pypa.io/en/stable/options/#test-skip) | Skip running tests on some builds |
|
||||
| | [`test-environment`](https://cibuildwheel.pypa.io/en/stable/options/#test-environment) | Set environment variables for the test environment |
|
||||
| | [`test-runtime`](https://cibuildwheel.pypa.io/en/stable/options/#test-runtime) | Controls how the tests will be executed. |
|
||||
| **Debugging** | [`debug-keep-container`](https://cibuildwheel.pypa.io/en/stable/options/#debug-keep-container) | Keep the container after running for debugging. |
|
||||
| | [`debug-traceback`](https://cibuildwheel.pypa.io/en/stable/options/#debug-traceback) | Print full traceback when errors occur. |
|
||||
| | [`build-verbosity`](https://cibuildwheel.pypa.io/en/stable/options/#build-verbosity) | Increase/decrease the output of the build |
|
||||
|
||||
These options can be specified in a pyproject.toml file, as well; see [configuration](https://cibuildwheel.readthedocs.io/en/stable/options/#configuration).
|
||||
|
||||
<!--[[[end]]] (sum: b7YIjCyCkf) -->
|
||||
|
||||
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
|
||||
----------------
|
||||
@@ -160,47 +188,48 @@ Here are some repos that use cibuildwheel.
|
||||
|
||||
| Name | CI | OS | Notes |
|
||||
|-----------------------------------|----|----|:------|
|
||||
| [scikit-learn][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The machine learning library. A complex but clean config using many of cibuildwheel's features to build a large project with Cython and C++ extensions. |
|
||||
| [scikit-learn][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] ![pyodide icon][] | The machine learning library. A complex but clean config using many of cibuildwheel's features to build a large project with Cython and C++ extensions. |
|
||||
| [duckdb][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | DuckDB is an analytical in-process SQL database management system |
|
||||
| [pytorch-fairseq][] | ![github icon][] | ![apple icon][] ![linux icon][] | Facebook AI Research Sequence-to-Sequence Toolkit written in Python. |
|
||||
| [NumPy][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The fundamental package for scientific computing with Python. |
|
||||
| [Tornado][] | ![travisci icon][] | ![apple icon][] ![linux icon][] | Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed. |
|
||||
| [Matplotlib][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The venerable Matplotlib, a Python library with C++ portions |
|
||||
| [NumPy][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] ![pyodide icon][] | The fundamental package for scientific computing with Python. |
|
||||
| [NCNN][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | ncnn is a high-performance neural network inference framework optimized for the mobile platform |
|
||||
| [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 |
|
||||
| [Matplotlib][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] ![pyodide icon][] | The venerable Matplotlib, a Python library with C++ portions |
|
||||
| [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. |
|
||||
| [MyPy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | The compiled version of MyPy using MyPyC. |
|
||||
| [duckdb][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | DuckDB is an in-process SQL OLAP Database Management System |
|
||||
| [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. |
|
||||
| [Triton][] | ![github icon][] | ![linux icon][] | Self hosted runners |
|
||||
|
||||
[scikit-learn]: https://github.com/scikit-learn/scikit-learn
|
||||
[pytorch-fairseq]: https://github.com/pytorch/fairseq
|
||||
[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
|
||||
[Kivy]: https://github.com/kivy/kivy
|
||||
[MyPy]: https://github.com/mypyc/mypy_mypyc-wheels
|
||||
[duckdb]: https://github.com/duckdb/duckdb
|
||||
[pytorch-fairseq]: https://github.com/facebookresearch/fairseq
|
||||
[NumPy]: https://github.com/numpy/numpy
|
||||
[NCNN]: https://github.com/Tencent/ncnn
|
||||
[Matplotlib]: https://github.com/matplotlib/matplotlib
|
||||
[Tornado]: https://github.com/tornadoweb/tornado
|
||||
[MyPy]: https://github.com/mypyc/mypy_mypyc-wheels
|
||||
[Prophet]: https://github.com/facebook/prophet
|
||||
[Triton]: https://github.com/openai/triton
|
||||
|
||||
[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
|
||||
[gitlab icon]: docs/data/readme_icons/gitlab.svg
|
||||
[travisci icon]: docs/data/readme_icons/travisci.svg
|
||||
[cirrusci icon]: docs/data/readme_icons/cirrusci.svg
|
||||
[windows icon]: docs/data/readme_icons/windows.svg
|
||||
[apple icon]: docs/data/readme_icons/apple.svg
|
||||
[linux icon]: docs/data/readme_icons/linux.svg
|
||||
[android icon]: docs/data/readme_icons/android.svg
|
||||
[ios icon]: docs/data/readme_icons/ios.svg
|
||||
[pyodide icon]: docs/data/readme_icons/pyodide.svg
|
||||
|
||||
<!-- 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
|
||||
----------
|
||||
|
||||
Since `cibuildwheel` repairs the wheel with `delocate` or `auditwheel`, it might automatically bundle dynamically linked libraries from the build machine.
|
||||
Since `cibuildwheel` repairs the wheel with `delocate`, `auditwheel`, or `delvewheel`, it might automatically bundle dynamically linked libraries from the build machine.
|
||||
|
||||
It helps ensure that the library can run without any dependencies outside of the pip toolchain.
|
||||
|
||||
@@ -209,76 +238,115 @@ 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 -->
|
||||
### v4.0.0rc1
|
||||
|
||||
### v2.14.0
|
||||
_14 May 2026_
|
||||
|
||||
_10 July 2023_
|
||||
- 🌟 Adds wheel auditing with `abi3audit` as a default after the repair step, with new [`audit-requires`](https://cibuildwheel.pypa.io/en/stable/options/#audit-requires) and [`audit-command`](https://cibuildwheel.pypa.io/en/stable/options/#audit-command) options (#2805)
|
||||
- 🌟 Adds `pyemscripten` platform tag support (PEP 783), updates Pyodide to 314.0.0a1, and adds a `pyodide-eol` [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable) flag for building end-of-life Pyodide versions (#2812, #2848)
|
||||
- 🌟 Sets up `delvewheel` as the default [`repair-wheel-command`](https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command) for Windows, so extension module DLLs are now bundled automatically. Skip by setting it to empty if not needed. (#2831)
|
||||
- ✨ Adds CPython 3.15 support, under the [`enable` option](https://cibuildwheel.pypa.io/en/stable/options/#enable) `cpython-prerelease`. This version of cibuildwheel uses 3.15.0b1. (#2833, #2850)
|
||||
|
||||
- ✨ Adds support for building PyPy 3.10 wheels. (#1525)
|
||||
- 🛠 Updates the prerelease CPython 3.12 version to 3.12.0b3.
|
||||
- ✨ Allow the use of the `{wheel}` placeholder in CIBW_TEST_COMMAND (#1533)
|
||||
- 📚 Docs & examples updates (#1532, #1416)
|
||||
- ⚠️ Removed support for running cibuildwheel in Python 3.7. Python 3.7 is EOL. However, cibuildwheel continues to build Python 3.7 wheels for the moment. (#1175)
|
||||
_While CPython is in beta, the ABI can change, so your wheels might not be compatible with the final release. For this reason, we don't recommend distributing wheels until RC1, at which point 3.14 will be available in cibuildwheel without the flag._ (#2390)
|
||||
- ✨ Adds `{project}` and `{package}` placeholders to [`config-settings`](https://cibuildwheel.pypa.io/en/stable/options/#config-settings) (#2827)
|
||||
- ⚠️ Drops support for Python 3.8 (#2686)
|
||||
- ⚠️ Removes the experimental CPython 3.13 free-threading builds and the `cpython-freethreading` [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable) option. CPython 3.14+ free-threading support remains available without the enable flag. (#2684)
|
||||
- ⚠️ Drops support for Cirrus CI, which is shutting down June 1, 2026 (#2817)
|
||||
- 🐛 Fixes `UV_PYTHON` not being set for [`before-build`](https://cibuildwheel.pypa.io/en/stable/options/#before-build) on Linux when using `uv` as the [`build-frontend`](https://cibuildwheel.pypa.io/en/stable/options/#build-frontend) (#2830)
|
||||
- 🛠 Updates Android to Python 3.13.13 and 3.14.4 (#2821)
|
||||
- 🛠 Applies Pyodide-specific patches to the Emscripten toolchain installation (#2800)
|
||||
- 🛠 Updates dependencies and container pins (#2845, #2837, #2821, #2818, #2810, #2838, #2813)
|
||||
- 🛠 Uses `python -V -V` for Windows build diagnostics (#2832)
|
||||
- 📚 Documents platform-specific [`before-build`](https://cibuildwheel.pypa.io/en/stable/options/#before-build) configuration (#2834)
|
||||
- 📚 Updates the "How it works" diagram with details of Android, iOS, and Pyodide builds (#2816)
|
||||
- 📚 Adds Pyodide icon and regenerates working examples data for Android, iOS, and Pyodide (#2815, #2811)
|
||||
- 📚 Links back to source in docs (#2806)
|
||||
- 💼 Adds PEP 723 metadata for `bin/` scripts and drops the `bin` dependency group (#2819)
|
||||
- 💼 Updates CI action pins and dev dependencies (#2851, #2843, #2826, #2823, #2820, #2807)
|
||||
- 🧪 Fixes Android tests using the `uv` frontend (#2809)
|
||||
- 🧪 Fixes the update-dependencies workflow to use `uv` to run `nox` (#2808)
|
||||
|
||||
### v2.13.1
|
||||
|
||||
_10 June 2023_
|
||||
### v3.4.1
|
||||
|
||||
- 🛠 Updates the prerelease CPython 3.12 version to 3.12.0b2. (#1516)
|
||||
- 🛠 Adds a moving `v<major>.<minor>` tag for use in GitHub Actions workflow files. If you use this, you'll get the latest patch release within a minor version. Additionally, Dependabot won't send you PRs for patch releases. (#1517)
|
||||
_2 April 2026_
|
||||
|
||||
### v2.13.0
|
||||
- ⚠️ Building for the experimental CPython 3.13 free-threading variant is now deprecated. That functionality will be removed in the next minor release. The [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable) option `cpython-freethreading` is therefore also deprecated. Builds specifying `enable = "all"` no longer select `cpython-freethreading`. CPython 3.14 free-threading support remains available without the `enable` flag. (#2787)
|
||||
- 🐛 iOS builds will no longer skip `repair-wheel-command` if it's defined in config (#2761)
|
||||
- 🐛 Fix bug causing `uv` to fail when environments define PYTHON_VERSION or UV_PYTHON, conflicting with our venvs (#2795)
|
||||
- ✨ cibuildwheel prints the selected build identifiers at the start of the build. (#2785)
|
||||
- 🔐 The GitHub Action now references other actions with a full SHA (#2744)
|
||||
|
||||
_28 May 2023_
|
||||
### v3.4.0
|
||||
|
||||
- ✨ Adds CPython 3.12 support, under the prerelease flag [CIBW_PRERELEASE_PYTHONS](https://cibuildwheel.readthedocs.io/en/stable/options/#prerelease-pythons). This version of cibuildwheel uses 3.12.0b1.
|
||||
_5 March 2026_
|
||||
|
||||
While CPython is in beta, the ABI can change, so your wheels might not be compatible with the final release. For this reason, we don't recommend distributing wheels until RC1, at which point 3.12 will be available in cibuildwheel without the flag. (#1507)
|
||||
- 🌟 You can now build wheels using `uv` as a build frontend. This should improve performance, especially if your project has lots of build dependencies. To use, set [`build-frontend`](https://cibuildwheel.pypa.io/en/stable/options/#build-frontend) to `uv`. (#2322)
|
||||
- ⚠️ We no longer support running on Travis CI. It may continue working but we don't run tests there anymore so we can't be sure. (#2682)
|
||||
- ✨ Improvements to building rust wheels on Android (#2650)
|
||||
- 🛠 Update Pyodide to 0.29.3 (#2719, #2733)
|
||||
- 🐛 Fix bug with the GitHub Action on Windows, where PATH was getting unnecessarily changed, causing issues with meson builds. (#2723)
|
||||
- ✨ Add support for quiet setting on `build` and `uv` from the cibuildwheel `build-verbosity` setting. (#2737)
|
||||
- 📚 Docs updates, including guidance on using Meson on Windows (#2718)
|
||||
|
||||
- ✨ Adds the ability to pass arguments to the container engine when the container is created, using the [CIBW_CONTAINER_ENGINE](https://cibuildwheel.readthedocs.io/en/stable/options/#container-engine) option. (#1499)
|
||||
### v3.3.1
|
||||
|
||||
### v2.12.3
|
||||
_5 January 2026_
|
||||
|
||||
_19 April 2023_
|
||||
- 🛠 Update dependencies and container pins, including updating to CPython 3.14.2. (#2708)
|
||||
|
||||
- 🐛 Fix an import error when running on Python 3.7. (#1479)
|
||||
### v3.3.0
|
||||
|
||||
### v2.12.2
|
||||
_12 November 2025_
|
||||
|
||||
_18 April 2023_
|
||||
- 🐛 Fix an incompatibility with Docker v29 (#2660)
|
||||
- ✨ Adds `test-runtime` option, to customise how tests on simulated/emulated environments are run (#2636)
|
||||
- ✨ Adds support for new `manylinux_2_35` images on 32-bit ARM `armv7l`, offering better C++20 compatibility (#2656)
|
||||
- ✨ `build[uv]` is now supported on Android (#2587)
|
||||
- ✨ You can now install extras (such as `uv`) with a simple option on the GitHub Action (#2630)
|
||||
- ✨ `{project}` and `{package}` placeholders are now supported in `repair-wheel-command` (#2589)
|
||||
- 🛠 The versions set with `dependency-versions` no longer constrain packages specified by your `build-system.requires`. Previously, on platforms other than Linux, the constraints in this option would remain in the environment during the build. This has been tidied up make behaviour more consistent between platforms, and to prevent version conflicts. (#2583)
|
||||
- 🛠 Improve the handling of `test-command` on Android, enabling more options to be passed (#2590)
|
||||
- 📚 Docs improvements (#2618)
|
||||
|
||||
- 🐛 Fix a bug that caused an extra empty config-setting to be passed to the backend when CIBW_BUILD_FRONTEND is set to `build`. (#1474)
|
||||
- 🐛 Fix a crash that occurred when overwriting an existing wheel on Windows. (#1464)
|
||||
- 🛠 Pinned version updates, including CPython 3.10.11, 3.11.3, pip 23.1 and wheel 0.40.0.
|
||||
|
||||
<!-- END bin/update_readme_changelog.py -->
|
||||
<!-- [[[end]]] (sum: ZD0EjcyC0B) -->
|
||||
|
||||
---
|
||||
|
||||
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)
|
||||
- Agriya Khetarpal [@agriyakhetarpal](https://github.com/agriyakhetarpal) (also Pyodide)
|
||||
|
||||
Platform maintainers:
|
||||
|
||||
- Russell Keith-Magee [@freakboy3742](https://github.com/freakboy3742) (iOS)
|
||||
- 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
|
||||
-------
|
||||
@@ -302,4 +370,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.
|
||||
|
||||
+110
-22
@@ -17,6 +17,10 @@ inputs:
|
||||
description: 'Build a specific wheel only. No need for arch/platform if this is set'
|
||||
required: false
|
||||
default: ''
|
||||
extras:
|
||||
description: 'Comma-separated list of extras to install'
|
||||
required: false
|
||||
default: ''
|
||||
branding:
|
||||
icon: package
|
||||
color: yellow
|
||||
@@ -24,36 +28,120 @@ branding:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# Set up a non-EOL, cibuildwheel & pipx supported Python version
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: "3.8 - 3.11"
|
||||
python-version: "3.11 - 3.14"
|
||||
update-environment: false
|
||||
|
||||
- id: cibw
|
||||
run: |
|
||||
# Install cibuildwheel and build the command line
|
||||
"$PYTHON" -u << "EOF"
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
import venv
|
||||
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
|
||||
EXTRAS = set(e.strip() for e in os.environ.get("INPUT_EXTRAS", "").split(",") if e.strip())
|
||||
|
||||
|
||||
class EnvBuilder(venv.EnvBuilder):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def setup_scripts(self, context):
|
||||
pass
|
||||
|
||||
def post_setup(self, context):
|
||||
super().post_setup(context)
|
||||
self.bin_path = Path(context.env_exe).parent
|
||||
install_spec = os.environ["GITHUB_ACTION_PATH"]
|
||||
if EXTRAS:
|
||||
install_spec += f"[{','.join(sorted(EXTRAS))}]"
|
||||
run([sys.executable, "-m", "pip", "--python", context.env_exe, "install", install_spec], check=True)
|
||||
|
||||
|
||||
print("::group::Install cibuildwheel")
|
||||
venv_path = Path(os.environ["RUNNER_TEMP"]) / "cibw"
|
||||
if venv_path.exists():
|
||||
shutil.rmtree(venv_path)
|
||||
|
||||
builder = EnvBuilder()
|
||||
builder.create(venv_path)
|
||||
exposed_binaries = {"cibuildwheel"}
|
||||
|
||||
if "uv" in EXTRAS:
|
||||
exposed_binaries.add("uv")
|
||||
clean_bin_path = builder.bin_path.parent / f"{builder.bin_path.name}.clean"
|
||||
clean_bin_path.mkdir()
|
||||
|
||||
for path in list(builder.bin_path.iterdir()):
|
||||
if path.stem in exposed_binaries:
|
||||
try:
|
||||
os.symlink(path, clean_bin_path / path.name)
|
||||
except OSError:
|
||||
shutil.copy2(path, clean_bin_path / path.name)
|
||||
|
||||
cibw_bin = [p for p in builder.bin_path.glob("cibuildwheel*") if p.stem == "cibuildwheel"][0]
|
||||
|
||||
# Build the command line
|
||||
cmd_args = [str(cibw_bin), os.environ["INPUT_PACKAGE_DIR"]]
|
||||
|
||||
if output_dir := os.environ.get("INPUT_OUTPUT_DIR"):
|
||||
cmd_args += ["--output-dir", output_dir]
|
||||
|
||||
if config_file := os.environ.get("INPUT_CONFIG_FILE"):
|
||||
cmd_args += ["--config-file", config_file]
|
||||
|
||||
if only := os.environ.get("INPUT_ONLY"):
|
||||
cmd_args += ["--only", only]
|
||||
|
||||
cmd_bash = shlex.join(cmd_args)
|
||||
|
||||
def pwsh_quote(text):
|
||||
# Wrap in single quotes and double-up any existing single quotes
|
||||
return "'" + str(text).replace("'", "''") + "'"
|
||||
|
||||
# Prepend '& ' so PowerShell executes the quoted binary path
|
||||
cmd_pwsh = "& " + " ".join(pwsh_quote(arg) for arg in cmd_args)
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "at") as f:
|
||||
f.write(f"prepend-path={clean_bin_path}\n")
|
||||
f.write(f"cmd-bash={cmd_bash}\n")
|
||||
f.write(f"cmd-pwsh={cmd_pwsh}\n")
|
||||
|
||||
print("::endgroup::")
|
||||
EOF
|
||||
shell: bash
|
||||
env:
|
||||
PYTHON: ${{ steps.python.outputs.python-path }}
|
||||
INPUT_PACKAGE_DIR: ${{ inputs.package-dir }}
|
||||
INPUT_OUTPUT_DIR: ${{ inputs.output-dir }}
|
||||
INPUT_CONFIG_FILE: ${{ inputs.config-file }}
|
||||
INPUT_ONLY: ${{ inputs.only }}
|
||||
INPUT_EXTRAS: ${{ inputs.extras }}
|
||||
|
||||
# Redirecting stderr to stdout to fix interleaving issue in Actions.
|
||||
- run: >
|
||||
pipx run
|
||||
--python '${{ steps.python.outputs.python-path }}'
|
||||
--spec '${{ github.action_path }}'
|
||||
cibuildwheel
|
||||
"${{ inputs.package-dir }}"
|
||||
--output-dir "${{ inputs.output-dir }}"
|
||||
--config-file "${{ inputs.config-file }}"
|
||||
--only "${{ inputs.only }}"
|
||||
2>&1
|
||||
- run: |
|
||||
export PATH="$CIBW_PREPEND_PATH:$PATH"
|
||||
eval "$CIBW_CMD_BASH" 2>&1
|
||||
shell: bash
|
||||
if: runner.os != 'Windows'
|
||||
env:
|
||||
CIBW_PREPEND_PATH: ${{ steps.cibw.outputs.prepend-path }}
|
||||
CIBW_CMD_BASH: ${{ steps.cibw.outputs.cmd-bash }}
|
||||
|
||||
# Windows needs powershell to interact nicely with Meson
|
||||
- run: >
|
||||
pipx run
|
||||
--python "${{ steps.python.outputs.python-path }}"
|
||||
--spec "${{ github.action_path }}"
|
||||
cibuildwheel
|
||||
"${{ inputs.package-dir }}"
|
||||
--output-dir '"${{ inputs.output-dir }}"'
|
||||
--config-file '"${{ inputs.config-file }}"'
|
||||
--only '"${{ inputs.only }}"'
|
||||
- run: |
|
||||
$env:PATH = "$env:CIBW_PREPEND_PATH;$env:PATH"
|
||||
Invoke-Expression $env:CIBW_CMD_PWSH
|
||||
shell: pwsh
|
||||
if: runner.os == 'Windows'
|
||||
env:
|
||||
CIBW_PREPEND_PATH: ${{ steps.cibw.outputs.prepend-path }}
|
||||
CIBW_CMD_PWSH: ${{ steps.cibw.outputs.cmd-pwsh }}
|
||||
|
||||
@@ -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/*
|
||||
+70
-26
@@ -1,32 +1,76 @@
|
||||
pr:
|
||||
paths:
|
||||
exclude:
|
||||
- .github/**
|
||||
- bin/*
|
||||
- docs/**
|
||||
- examples/github-*
|
||||
- examples/ci*
|
||||
- examples/travis-ci-*
|
||||
- .ci*
|
||||
- .pre-commit-config.yaml
|
||||
- .readthedocs.yml
|
||||
- .travis.yml
|
||||
- README.md
|
||||
- mkdocs.yml
|
||||
- noxfile.py
|
||||
|
||||
jobs:
|
||||
- job: linux_38
|
||||
timeoutInMinutes: 120
|
||||
pool: {vmImage: 'Ubuntu-20.04'}
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- bash: |
|
||||
python -m pip install -e ".[dev]"
|
||||
python ./bin/run_tests.py
|
||||
|
||||
- job: macos_38
|
||||
pool: {vmImage: 'macOS-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: tests
|
||||
strategy:
|
||||
matrix:
|
||||
linux_311:
|
||||
imageName: "ubuntu-latest"
|
||||
pythonVersion: "3.11"
|
||||
testSelect: "native"
|
||||
android_311:
|
||||
imageName: "ubuntu-latest"
|
||||
pythonVersion: "3.11"
|
||||
testSelect: "android"
|
||||
macos_311:
|
||||
imageName: "macos-latest"
|
||||
pythonVersion: "3.11"
|
||||
testSelect: "native"
|
||||
ios_311:
|
||||
imageName: "macos-14" # See https://github.com/actions/runner-images/issues/12777
|
||||
pythonVersion: "3.11"
|
||||
testSelect: "ios"
|
||||
android_macos_311:
|
||||
imageName: "macos-latest"
|
||||
pythonVersion: "3.11"
|
||||
testSelect: "android"
|
||||
windows_311:
|
||||
imageName: "windows-latest"
|
||||
pythonVersion: "3.11"
|
||||
testSelect: "native"
|
||||
timeoutInMinutes: 180
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: $(pythonVersion)
|
||||
|
||||
- task: JavaToolInstaller@0
|
||||
condition: and(eq(variables['testSelect'], 'android'), eq(variables['Agent.OS'], 'Linux'))
|
||||
inputs:
|
||||
versionSpec: '17'
|
||||
jdkArchitectureOption: 'x64'
|
||||
jdkSourceOption: 'PreInstalled'
|
||||
|
||||
- bash: docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
|
||||
condition: and(eq(variables['imageName'], 'ubuntu-latest'), eq(variables['testSelect'], 'native'))
|
||||
displayName: 'Install binfmt on Linux'
|
||||
|
||||
- bash: python -m pip install -U pip && python -m pip install -e. --group test
|
||||
displayName: 'Update pip and install cibuildwheel --group test'
|
||||
|
||||
- bash: echo "##vso[task.setvariable variable=CIBW_ENABLE;]all"
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
|
||||
displayName: Set CIBW_ENABLE to all (main branch)
|
||||
|
||||
- bash: |
|
||||
python -m pip install -e ".[dev]"
|
||||
python ./bin/run_tests.py
|
||||
echo "CIBW_ENABLE = $CIBW_ENABLE"
|
||||
python ./bin/run_tests.py --test-select $(testSelect)
|
||||
displayName: 'Run tests'
|
||||
|
||||
+20
-19
@@ -1,30 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# dependencies = ["click", "packaging", "prek"]
|
||||
# ///
|
||||
|
||||
|
||||
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(
|
||||
@@ -60,7 +60,7 @@ def bump_version() -> None:
|
||||
print(e)
|
||||
print("Failed to get previous version tag information.")
|
||||
|
||||
git_changes_result = subprocess.run(["git diff-index --quiet HEAD --"], shell=True)
|
||||
git_changes_result = subprocess.run(["git diff-index --quiet HEAD --"], shell=True, check=False)
|
||||
repo_has_uncommitted_changes = git_changes_result.returncode != 0
|
||||
|
||||
if repo_has_uncommitted_changes:
|
||||
@@ -68,8 +68,8 @@ def bump_version() -> None:
|
||||
sys.exit(1)
|
||||
|
||||
# fmt: off
|
||||
print( 'Current version:', current_version)
|
||||
new_version = input(' New version: ').strip()
|
||||
print( "Current version:", current_version)
|
||||
new_version = input(" New version: ").strip()
|
||||
# fmt: on
|
||||
|
||||
try:
|
||||
@@ -82,7 +82,7 @@ def bump_version() -> None:
|
||||
actions = []
|
||||
|
||||
for path_pattern, version_pattern in config:
|
||||
paths = [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":
|
||||
@@ -139,16 +139,17 @@ def bump_version() -> None:
|
||||
# run pre-commit to update the README changelog
|
||||
subprocess.run(
|
||||
[
|
||||
"pre-commit",
|
||||
"prek",
|
||||
"run",
|
||||
"--files=docs/changelog.md",
|
||||
]
|
||||
],
|
||||
check=False,
|
||||
)
|
||||
|
||||
# run pre-commit to check that no errors occurred on the second run
|
||||
subprocess.run(
|
||||
[
|
||||
"pre-commit",
|
||||
"prek",
|
||||
"run",
|
||||
"--files=docs/changelog.md",
|
||||
],
|
||||
@@ -180,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
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "click",
|
||||
# "cibuildwheel",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# cibuildwheel = { path = ".." }
|
||||
# ///
|
||||
|
||||
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()
|
||||
Executable
+397
@@ -0,0 +1,397 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# dependencies = ["pyyaml"]
|
||||
# ///
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
parser.add_argument("--schemastore", action="store_true", help="Generate schema_store version")
|
||||
args = parser.parse_args()
|
||||
|
||||
starter = """
|
||||
$schema: http://json-schema.org/draft-07/schema#
|
||||
$id: https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json
|
||||
$defs:
|
||||
inherit:
|
||||
enum:
|
||||
- none
|
||||
- prepend
|
||||
- append
|
||||
default: none
|
||||
description: How to inherit the parent's value.
|
||||
enable:
|
||||
enum:
|
||||
- cpython-prerelease
|
||||
- graalpy
|
||||
- pyodide-prerelease
|
||||
- pypy
|
||||
- pypy-eol
|
||||
description: A Python version or flavor to enable.
|
||||
additionalProperties: false
|
||||
description: cibuildwheel's settings.
|
||||
type: object
|
||||
properties:
|
||||
audit-command:
|
||||
description: Execute a shell command to audit each wheel after it is repaired. Use {wheel} for each wheel path, or {abi3_wheel} to only audit abi3 wheels.
|
||||
type: string_array
|
||||
audit-requires:
|
||||
description: Install Python dependencies for the audit step.
|
||||
type: string_array
|
||||
archs:
|
||||
description: Change the architectures built on your machine by default.
|
||||
type: string_array
|
||||
before-all:
|
||||
description: Execute a shell command on the build system before any wheels are built.
|
||||
type: string_array
|
||||
before-build:
|
||||
description: Execute a shell command preparing each wheel's build.
|
||||
type: string_array
|
||||
before-test:
|
||||
description: Execute a shell command before testing each wheel.
|
||||
type: string_array
|
||||
build:
|
||||
default: ['*']
|
||||
description: Choose the Python versions to build.
|
||||
type: string_array
|
||||
build-frontend:
|
||||
default: default
|
||||
description: Set the tool to use to build, either "build" (default), "build[uv]", "uv", or "pip"
|
||||
oneOf:
|
||||
- enum: [pip, build, "build[uv]", uv, default]
|
||||
- type: string
|
||||
pattern: '^pip; ?args:'
|
||||
- type: string
|
||||
pattern: '^build; ?args:'
|
||||
- type: string
|
||||
pattern: '^build\\[uv\\]; ?args:'
|
||||
- type: string
|
||||
pattern: '^uv; ?args:'
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required: [name]
|
||||
properties:
|
||||
name:
|
||||
enum: [pip, build, "build[uv]", uv]
|
||||
args:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
build-verbosity:
|
||||
type: integer
|
||||
minimum: -3
|
||||
maximum: 3
|
||||
default: 0
|
||||
description: Increase/decrease the output of pip wheel.
|
||||
config-settings:
|
||||
description: Specify config-settings for the build backend.
|
||||
type: string_table_array
|
||||
container-engine:
|
||||
oneOf:
|
||||
- enum: [docker, podman]
|
||||
- type: string
|
||||
pattern: '^docker; ?(create_args|disable_host_mount):'
|
||||
- type: string
|
||||
pattern: '^podman; ?(create_args|disable_host_mount):'
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required: [name]
|
||||
properties:
|
||||
name:
|
||||
enum: [docker, podman]
|
||||
create-args:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
disable-host-mount:
|
||||
type: boolean
|
||||
dependency-versions:
|
||||
default: pinned
|
||||
description: Specify how cibuildwheel controls the versions of the tools it uses
|
||||
oneOf:
|
||||
- enum: [pinned, latest]
|
||||
- type: string
|
||||
description: Path to a file containing dependency versions, or inline package specifications, starting with "packages:"
|
||||
not:
|
||||
enum: [pinned, latest]
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
packages:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enable:
|
||||
description: Enable or disable certain builds.
|
||||
oneOf:
|
||||
- $ref: "#/$defs/enable"
|
||||
- type: array
|
||||
items:
|
||||
$ref: "#/$defs/enable"
|
||||
environment:
|
||||
description: Set environment variables needed during the build.
|
||||
type: string_table
|
||||
environment-pass:
|
||||
description: Set environment variables on the host to pass-through to the container
|
||||
during the build.
|
||||
type: string_array
|
||||
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
|
||||
manylinux-ppc64le-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
manylinux-pypy_aarch64-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
manylinux-pypy_i686-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
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
|
||||
manylinux-x86_64-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
musllinux-aarch64-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
musllinux-armv7l-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
musllinux-i686-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
musllinux-ppc64le-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
musllinux-riscv64-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
musllinux-s390x-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
musllinux-x86_64-image:
|
||||
type: string
|
||||
description: Specify alternative manylinux / musllinux container images
|
||||
xbuild-tools:
|
||||
description: Binaries on the path that should be included in an isolated cross-build environment
|
||||
type: string_array
|
||||
pyodide-version:
|
||||
type: string
|
||||
description: Specify the version of Pyodide to use
|
||||
repair-wheel-command:
|
||||
description: Execute a shell command to repair each built wheel.
|
||||
type: string_array
|
||||
skip:
|
||||
description: Choose the Python versions to skip.
|
||||
type: string_array
|
||||
test-command:
|
||||
description: Execute a shell command to test each built wheel.
|
||||
type: string_array
|
||||
test-extras:
|
||||
description: Install your wheel for testing using `extras_require`
|
||||
type: string_array
|
||||
test-sources:
|
||||
description: Test files that are required by the test environment
|
||||
type: string_array
|
||||
test-groups:
|
||||
description: Install extra groups when testing
|
||||
type: string_array
|
||||
test-requires:
|
||||
description: Install Python dependencies before running the tests
|
||||
type: string_array
|
||||
test-skip:
|
||||
description: Skip running tests on some builds.
|
||||
type: string_array
|
||||
test-environment:
|
||||
description: Set environment variables for the test environment
|
||||
type: string_table
|
||||
test-runtime:
|
||||
description: Additional configuration for the test runner
|
||||
oneOf:
|
||||
- type: string
|
||||
pattern: '^$'
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
- type: string
|
||||
pattern: 'args:'
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required: [args]
|
||||
properties:
|
||||
args:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
"""
|
||||
|
||||
schema = yaml.safe_load(starter)
|
||||
|
||||
string_array = yaml.safe_load(
|
||||
"""
|
||||
- type: string
|
||||
- type: array
|
||||
items:
|
||||
type: string
|
||||
"""
|
||||
)
|
||||
|
||||
string_table_array = yaml.safe_load(
|
||||
"""
|
||||
- type: string
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
patternProperties:
|
||||
.+:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: array
|
||||
items:
|
||||
type: string
|
||||
"""
|
||||
)
|
||||
|
||||
string_table = yaml.safe_load(
|
||||
"""
|
||||
- type: string
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
patternProperties:
|
||||
.+:
|
||||
type: string
|
||||
"""
|
||||
)
|
||||
|
||||
for value in schema["properties"].values():
|
||||
match value:
|
||||
case {"type": "string_array"}:
|
||||
del value["type"]
|
||||
value["oneOf"] = string_array
|
||||
case {"type": "string_table"}:
|
||||
del value["type"]
|
||||
value["oneOf"] = string_table
|
||||
case {"type": "string_table_array"}:
|
||||
del value["type"]
|
||||
value["oneOf"] = string_table_array
|
||||
|
||||
overrides = yaml.safe_load(
|
||||
"""
|
||||
type: array
|
||||
description: An overrides array
|
||||
items:
|
||||
type: object
|
||||
required: ["select"]
|
||||
minProperties: 2
|
||||
additionalProperties: false
|
||||
properties:
|
||||
select: {}
|
||||
inherit:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
audit-command: {"$ref": "#/$defs/inherit"}
|
||||
audit-requires: {"$ref": "#/$defs/inherit"}
|
||||
before-all: {"$ref": "#/$defs/inherit"}
|
||||
before-build: {"$ref": "#/$defs/inherit"}
|
||||
xbuild-tools: {"$ref": "#/$defs/inherit"}
|
||||
before-test: {"$ref": "#/$defs/inherit"}
|
||||
config-settings: {"$ref": "#/$defs/inherit"}
|
||||
container-engine: {"$ref": "#/$defs/inherit"}
|
||||
environment: {"$ref": "#/$defs/inherit"}
|
||||
environment-pass: {"$ref": "#/$defs/inherit"}
|
||||
repair-wheel-command: {"$ref": "#/$defs/inherit"}
|
||||
test-command: {"$ref": "#/$defs/inherit"}
|
||||
test-extras: {"$ref": "#/$defs/inherit"}
|
||||
test-sources: {"$ref": "#/$defs/inherit"}
|
||||
test-requires: {"$ref": "#/$defs/inherit"}
|
||||
test-environment: {"$ref": "#/$defs/inherit"}
|
||||
test-runtime: {"$ref": "#/$defs/inherit"}
|
||||
"""
|
||||
)
|
||||
|
||||
for key, value in schema["properties"].items():
|
||||
value["title"] = f"CIBW_{key.replace('-', '_').upper()}"
|
||||
|
||||
non_global_options = {k: {"$ref": f"#/properties/{k}"} for k in schema["properties"]}
|
||||
del non_global_options["build"]
|
||||
del non_global_options["skip"]
|
||||
del non_global_options["test-skip"]
|
||||
del non_global_options["enable"]
|
||||
|
||||
overrides["items"]["properties"]["select"]["oneOf"] = string_array
|
||||
overrides["items"]["properties"] |= non_global_options.copy()
|
||||
|
||||
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]
|
||||
|
||||
|
||||
def as_object(d: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": copy.deepcopy(d),
|
||||
}
|
||||
|
||||
|
||||
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"] = {
|
||||
**schema["properties"]["repair-wheel-command"],
|
||||
"default": "auditwheel repair -w {dest_dir} {wheel}",
|
||||
}
|
||||
oses["macos"]["properties"]["repair-wheel-command"] = {
|
||||
**schema["properties"]["repair-wheel-command"],
|
||||
"default": "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}",
|
||||
}
|
||||
|
||||
del oses["linux"]["properties"]["dependency-versions"]
|
||||
|
||||
schema["properties"]["overrides"] = overrides
|
||||
schema["properties"] |= oses
|
||||
|
||||
if args.schemastore:
|
||||
schema["$id"] = "https://json.schemastore.org/partial-cibuildwheel.json"
|
||||
schema["description"] = (
|
||||
"cibuildwheel's settings. Generated with ./bin/generate_schema.py --schemastore from cibuildwheel."
|
||||
)
|
||||
|
||||
print(json.dumps(schema, indent=2))
|
||||
@@ -1,4 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "click",
|
||||
# "pygithub",
|
||||
# "pyyaml",
|
||||
# "rich",
|
||||
# "cibuildwheel",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# cibuildwheel = { path = ".." }
|
||||
# ///
|
||||
|
||||
"""
|
||||
Check known projects for usage of requires-python.
|
||||
@@ -11,11 +24,9 @@ This will cache the results to all_known_setup.yaml; you can reprint
|
||||
the results without the `--online` setting.
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from collections.abc import Iterable, Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
@@ -25,6 +36,10 @@ from rich import print
|
||||
|
||||
from cibuildwheel.projectfiles import Analyzer
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Iterator
|
||||
|
||||
DIR = Path(__file__).parent.resolve()
|
||||
|
||||
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "click",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
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, **kwargs):
|
||||
return run([cmd], shell=True, **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 --").returncode != 0
|
||||
staged_changes = shell("git diff-index --quiet --cached HEAD --").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 +61,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",
|
||||
@@ -73,7 +80,7 @@ def main():
|
||||
print("Done.")
|
||||
finally:
|
||||
# remove any local changes
|
||||
shell("git checkout -- .")
|
||||
shell("git checkout -- .", check=True)
|
||||
shell(f"git checkout {previous_branch}", check=True)
|
||||
shell(f"git branch -D --force {branch_name}", check=True)
|
||||
|
||||
|
||||
+39
-18
@@ -1,4 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "click",
|
||||
# "pyyaml",
|
||||
# "pygithub",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
"""
|
||||
Convert a yaml project list into a nice table.
|
||||
@@ -14,29 +22,34 @@ from __future__ import annotations
|
||||
import builtins
|
||||
import functools
|
||||
import textwrap
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import xml.dom.minidom
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import Any, TextIO
|
||||
from typing import Any, Self, TextIO
|
||||
|
||||
import click
|
||||
import yaml
|
||||
from github import Github, GithubException
|
||||
from github import Auth, Github, GithubException
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
|
||||
ICONS = (
|
||||
"appveyor",
|
||||
"github",
|
||||
"azurepipelines",
|
||||
"circleci",
|
||||
"gitlab",
|
||||
"travisci",
|
||||
"cirrusci",
|
||||
"windows",
|
||||
"apple",
|
||||
"linux",
|
||||
"android",
|
||||
"ios",
|
||||
"pyodide",
|
||||
)
|
||||
|
||||
|
||||
@@ -72,12 +85,12 @@ class Project:
|
||||
self.notes = notes
|
||||
else:
|
||||
self.num_stars = 0
|
||||
self.pushed_at = datetime.utcnow()
|
||||
self.pushed_at = datetime.now(UTC)
|
||||
|
||||
name_len = len(self.name) + 4
|
||||
self.__class__.NAME = max(self.__class__.NAME, name_len)
|
||||
|
||||
def __lt__(self, other: Project) -> bool:
|
||||
def __lt__(self, other: Self) -> bool:
|
||||
if self.online:
|
||||
return self.num_stars < other.num_stars
|
||||
else:
|
||||
@@ -87,8 +100,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
|
||||
@@ -119,17 +132,23 @@ class Project:
|
||||
return f"[{self.name}]: {self.url}"
|
||||
|
||||
def info(self) -> str:
|
||||
days = (datetime.utcnow() - self.pushed_at).days
|
||||
days = (datetime.now(UTC) - self.pushed_at).days
|
||||
return f"<!-- {self.name}: {self.num_stars}, last pushed {days} days ago -->"
|
||||
|
||||
|
||||
def fetch_icon(icon_name: str) -> None:
|
||||
url = f"https://cdn.jsdelivr.net/npm/simple-icons@v4/icons/{icon_name}.svg"
|
||||
with urllib.request.urlopen(url) as f:
|
||||
original_svg_data = f.read()
|
||||
try:
|
||||
with urllib.request.urlopen(url) as f:
|
||||
original_svg_data = f.read()
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404 and path_for_icon(icon_name).exists():
|
||||
return
|
||||
raise
|
||||
|
||||
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")
|
||||
@@ -159,12 +178,14 @@ def get_projects(
|
||||
for icon in ICONS:
|
||||
fetch_icon(icon)
|
||||
|
||||
github = Github(auth) if online else None
|
||||
github = Github(auth=Auth.Token(auth)) if (online and auth) else None
|
||||
|
||||
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 +216,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 +230,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 -S uv run --script
|
||||
|
||||
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 -S uv run --script
|
||||
|
||||
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())
|
||||
+104
-46
@@ -1,46 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "click",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from glob import glob
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
from urllib.parse import quote
|
||||
|
||||
import click
|
||||
|
||||
DIR = Path(__file__).parent.parent.resolve()
|
||||
|
||||
def shell(cmd, **kwargs):
|
||||
return run([cmd], shell=True, **kwargs)
|
||||
BuildBackend = typing.Literal["setuptools", "meson"]
|
||||
|
||||
|
||||
def git_repo_has_changes():
|
||||
unstaged_changes = shell("git diff-index --quiet HEAD --").returncode != 0
|
||||
staged_changes = shell("git diff-index --quiet --cached HEAD --").returncode != 0
|
||||
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_project(path: Path, build_backend: BuildBackend) -> None:
|
||||
sys.path.insert(0, "")
|
||||
from test.test_projects.c import new_c_project
|
||||
match build_backend:
|
||||
case "meson":
|
||||
from test.test_projects.meson import new_meson_project as new_project # noqa: PLC0415
|
||||
case "setuptools":
|
||||
from test.test_projects.setuptools import new_c_project as new_project # noqa: PLC0415
|
||||
case _:
|
||||
typing.assert_never(build_backend)
|
||||
|
||||
project = new_c_project()
|
||||
project = new_project()
|
||||
project.generate(path)
|
||||
|
||||
|
||||
CIService = namedtuple("CIService", "name dst_config_path badge_md")
|
||||
class CIService(typing.NamedTuple):
|
||||
name: str
|
||||
dst_config_path: str
|
||||
badge_md: str
|
||||
config_file_transform: typing.Callable[[str, str], str] = lambda x, _: x # identity by default
|
||||
|
||||
|
||||
def github_config_file_transform(content: str, git_ref: str) -> str:
|
||||
# one of the the github configs only builds on main, so we need to remove that restriction
|
||||
# so our example build will run on the test branch.
|
||||
#
|
||||
# replace:
|
||||
# """
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# """
|
||||
# with:
|
||||
# """
|
||||
# push:
|
||||
# """"
|
||||
content = re.sub(
|
||||
r"push:\n\s+branches:\n\s+- main",
|
||||
"push:",
|
||||
content,
|
||||
)
|
||||
|
||||
# use the version of cibuildwheel from the current commit, not the latest
|
||||
# release
|
||||
# replace:
|
||||
# """
|
||||
# uses: pypa/cibuildwheel@v3.3.1
|
||||
# """
|
||||
# with:
|
||||
# """
|
||||
# uses: pypa/cibuildwheel@<latest commit hash>
|
||||
# """
|
||||
content = re.sub(
|
||||
r"uses: pypa/cibuildwheel@v.*",
|
||||
f"uses: pypa/cibuildwheel@{git_ref}",
|
||||
content,
|
||||
)
|
||||
return content
|
||||
|
||||
|
||||
services = [
|
||||
CIService(
|
||||
name="appveyor",
|
||||
dst_config_path="appveyor.yml",
|
||||
badge_md="[](https://ci.appveyor.com/project/joerick/cibuildwheel/branch/{branch})",
|
||||
),
|
||||
CIService(
|
||||
name="azure-pipelines",
|
||||
dst_config_path="azure-pipelines.yml",
|
||||
@@ -54,7 +108,8 @@ services = [
|
||||
CIService(
|
||||
name="github",
|
||||
dst_config_path=".github/workflows/example.yml",
|
||||
badge_md="[](https://github.com/pypa/cibuildwheel/actions)",
|
||||
badge_md="[](https://github.com/pypa/cibuildwheel/actions?query=branch%3A{branch})",
|
||||
config_file_transform=github_config_file_transform,
|
||||
),
|
||||
CIService(
|
||||
name="travis-ci",
|
||||
@@ -66,17 +121,11 @@ services = [
|
||||
dst_config_path=".gitlab-ci.yml",
|
||||
badge_md="[](https://gitlab.com/joerick/cibuildwheel/-/commits/{branch})",
|
||||
),
|
||||
CIService(
|
||||
name="cirrus-ci",
|
||||
dst_config_path=".cirrus.yml",
|
||||
badge_md="[](https://cirrus-ci.com/github/pypa/cibuildwheel/{branch})",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
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:
|
||||
@@ -86,23 +135,28 @@ def ci_service_for_config_file(config_file):
|
||||
|
||||
@click.command()
|
||||
@click.argument("config_files", nargs=-1, type=click.Path())
|
||||
def run_example_ci_configs(config_files=None):
|
||||
@click.option("--build-backend", type=click.Choice(["setuptools", "meson"]), default="setuptools")
|
||||
def run_example_ci_configs(
|
||||
config_files: list[str], build_backend: BuildBackend = "setuptools"
|
||||
) -> None:
|
||||
"""
|
||||
Test the example configs. If no files are specified, will test
|
||||
examples/*-minimal.yml
|
||||
"""
|
||||
|
||||
if len(config_files) == 0:
|
||||
config_files = glob("examples/*-minimal.yml")
|
||||
config_file_paths = list(Path("examples").glob("*-minimal.yml"))
|
||||
else:
|
||||
config_file_paths = [Path(f) for f in config_files]
|
||||
|
||||
# check each CI service has at most 1 config file
|
||||
configs_by_service = {}
|
||||
for config_file in config_files:
|
||||
configs_by_service = set()
|
||||
for config_file in config_file_paths:
|
||||
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.")
|
||||
@@ -111,6 +165,9 @@ def run_example_ci_configs(config_files=None):
|
||||
previous_branch = shell(
|
||||
"git rev-parse --abbrev-ref HEAD", check=True, capture_output=True, encoding="utf8"
|
||||
).stdout.strip()
|
||||
git_ref = shell(
|
||||
"git rev-parse HEAD", check=True, capture_output=True, encoding="utf8"
|
||||
).stdout.strip()
|
||||
|
||||
timestamp = time.strftime("%Y-%m-%dT%H-%M-%S", time.gmtime())
|
||||
branch_name = f"example-config-test---{previous_branch}-{timestamp}"
|
||||
@@ -119,27 +176,29 @@ def run_example_ci_configs(config_files=None):
|
||||
shell(f"git checkout --orphan {branch_name}", check=True)
|
||||
|
||||
example_project = Path("example_root")
|
||||
generate_basic_project(example_project)
|
||||
generate_project(example_project, build_backend=build_backend)
|
||||
|
||||
for config_file in config_files:
|
||||
for config_file in config_file_paths:
|
||||
service = ci_service_for_config_file(config_file)
|
||||
src_config_file = Path(config_file)
|
||||
dst_config_file = example_project / service.dst_config_path
|
||||
|
||||
dst_config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(src_config_file, dst_config_file)
|
||||
|
||||
run(["git", "add", example_project], check=True)
|
||||
contents = config_file.read_text(encoding="utf8")
|
||||
contents = service.config_file_transform(contents, git_ref)
|
||||
dst_config_file.write_text(contents, encoding="utf8")
|
||||
|
||||
subprocess.run(["git", "add", example_project], check=True)
|
||||
message = textwrap.dedent(
|
||||
f"""\
|
||||
Test example minimal configs
|
||||
Test example CI configs
|
||||
|
||||
Testing files: {config_files}
|
||||
Testing files: {[str(f) for f in config_files]}
|
||||
Generated from branch: {previous_branch}
|
||||
Time: {timestamp}
|
||||
"""
|
||||
)
|
||||
run(["git", "commit", "--no-verify", "--message", message], check=True)
|
||||
subprocess.run(["git", "commit", "--no-verify", "--message", message], check=True)
|
||||
shell(f"git subtree --prefix={example_project} push origin {branch_name}", check=True)
|
||||
|
||||
print("---")
|
||||
@@ -150,25 +209,24 @@ def run_example_ci_configs(config_files=None):
|
||||
print("> ")
|
||||
print("> | Service | Config | Status |")
|
||||
print("> |---|---|---|")
|
||||
for config_file in config_files:
|
||||
for config_file in config_file_paths:
|
||||
service = ci_service_for_config_file(config_file)
|
||||
badge = service.badge_md.format(
|
||||
branch=branch_name, branch_escaped=quote(branch_name, safe="")
|
||||
)
|
||||
print(f"> | {service.name} | `{config_file}` | {badge} |")
|
||||
print("> ")
|
||||
print("> Generated by `bin/run_example_ci_config.py`")
|
||||
print(f"> Generated by `{' '.join(sys.argv)}`")
|
||||
print()
|
||||
print("---")
|
||||
finally:
|
||||
# remove any local changes
|
||||
shutil.rmtree(example_project, ignore_errors=True)
|
||||
shell("git checkout -- .")
|
||||
shell("git checkout -- .", check=True)
|
||||
shell(f"git checkout {previous_branch}", check=True)
|
||||
shell(f"git branch -D --force {branch_name}", check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
os.chdir("..")
|
||||
os.chdir(DIR)
|
||||
run_example_ci_configs(standalone_mode=True)
|
||||
|
||||
+84
-7
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
@@ -9,19 +8,61 @@ 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
|
||||
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
parser.add_argument(
|
||||
"--run-podman", action="store_true", default=False, help="run podman tests (linux only)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-processes",
|
||||
action="store",
|
||||
default=default_cpu_count,
|
||||
help="number of processes to use for testing",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test-select",
|
||||
choices={"all", "native", "android", "ios", "pyodide"},
|
||||
default="all",
|
||||
help="Either 'native' or 'android'/'ios'/'pyodide'",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# move cwd to the project root
|
||||
os.chdir(Path(__file__).resolve().parents[1])
|
||||
|
||||
# doc tests
|
||||
doc_test_args = [sys.executable, "-m", "pytest", "cibuildwheel"]
|
||||
|
||||
print(
|
||||
"\n\n================================== DOC TESTS ==================================",
|
||||
flush=True,
|
||||
)
|
||||
result = subprocess.run(doc_test_args, check=False)
|
||||
if result.returncode not in (0, 5):
|
||||
# Allow case where no doctests are collected (returncode 5) because
|
||||
# circleci sets an explicit "-k" filter that disables doctests. There
|
||||
# isn't a pattern that will only select doctests. This can be removed
|
||||
# and have check=True if the circleci PYTEST_ADDOPTS is removed.
|
||||
raise subprocess.CalledProcessError(
|
||||
result.returncode, result.args, output=result.stdout, stderr=result.stderr
|
||||
)
|
||||
|
||||
# unit tests
|
||||
print(
|
||||
"\n\n================================== UNIT TESTS ==================================",
|
||||
flush=True,
|
||||
)
|
||||
unit_test_args = [sys.executable, "-m", "pytest", "unit_test"]
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
if (
|
||||
sys.platform.startswith("linux")
|
||||
and os.environ.get("CIBW_PLATFORM", "linux") == "linux"
|
||||
and args.test_select in ["all", "native"]
|
||||
):
|
||||
# run the docker unit tests only on Linux
|
||||
unit_test_args += ["--run-docker"]
|
||||
|
||||
@@ -30,17 +71,53 @@ if __name__ == "__main__":
|
||||
|
||||
subprocess.run(unit_test_args, check=True)
|
||||
|
||||
# integration tests
|
||||
integration_test_args = [
|
||||
print(
|
||||
"\n\n=========================== SERIAL INTEGRATION TESTS ===========================",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
match args.test_select:
|
||||
case "all":
|
||||
marks = []
|
||||
case "native":
|
||||
marks = ["not pyodide", "not android", "not ios"]
|
||||
case mark:
|
||||
marks = [f"{mark}"]
|
||||
|
||||
# Run the serial integration tests without multiple processes
|
||||
serial_integration_test_args = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pytest",
|
||||
"--numprocesses=2",
|
||||
"-m",
|
||||
f"{' and '.join(['serial', *marks])}",
|
||||
"-x",
|
||||
"--durations",
|
||||
"0",
|
||||
"--timeout=2400",
|
||||
"test",
|
||||
"-vv",
|
||||
]
|
||||
|
||||
subprocess.run(serial_integration_test_args, check=True)
|
||||
|
||||
print(
|
||||
"\n\n========================= NON-SERIAL INTEGRATION TESTS =========================",
|
||||
flush=True,
|
||||
)
|
||||
integration_test_args = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pytest",
|
||||
"-m",
|
||||
f"{' and '.join(['not serial', *marks])}",
|
||||
f"--numprocesses={args.num_processes}",
|
||||
"-x",
|
||||
"--durations",
|
||||
"0",
|
||||
"--timeout=2400",
|
||||
"test",
|
||||
"-vv",
|
||||
]
|
||||
|
||||
if sys.platform.startswith("linux") and args.run_podman:
|
||||
|
||||
+7
-4
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
@@ -13,7 +12,7 @@ if __name__ == "__main__":
|
||||
# move cwd to the project root
|
||||
os.chdir(Path(__file__).resolve().parents[1])
|
||||
|
||||
parser = argparse.ArgumentParser(description="Runs a sample build")
|
||||
parser = argparse.ArgumentParser(description="Runs a sample build", allow_abbrev=False)
|
||||
parser.add_argument("project_python_path", nargs="?", default="test.test_0_basic.basic_project")
|
||||
|
||||
options = parser.parse_args()
|
||||
@@ -24,4 +23,8 @@ if __name__ == "__main__":
|
||||
check=True,
|
||||
)
|
||||
|
||||
sys.exit(subprocess.run([sys.executable, "-m", "cibuildwheel"], cwd=project_dir).returncode)
|
||||
sys.exit(
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "cibuildwheel"], cwd=project_dir, check=False
|
||||
).returncode
|
||||
)
|
||||
|
||||
+92
-47
@@ -1,64 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "packaging",
|
||||
# "requests",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import configparser
|
||||
from dataclasses import dataclass
|
||||
import dataclasses
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from packaging.version import Version
|
||||
|
||||
DIR = Path(__file__).parent.resolve()
|
||||
RESOURCES = DIR.parent / "cibuildwheel/resources"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Image:
|
||||
manylinux_version: str
|
||||
platform: str
|
||||
platforms: list[str]
|
||||
image_name: str
|
||||
tag: str | None # Set this to pin the image
|
||||
tag: str | None = None # Set this to pin the image
|
||||
use_platform_suffix: bool = False
|
||||
|
||||
|
||||
class PyPAImage(Image):
|
||||
def __init__(self, manylinux_version: str, platforms: list[str], tag: str | None = None):
|
||||
image_name = f"quay.io/pypa/{manylinux_version}"
|
||||
super().__init__(manylinux_version, platforms, image_name, tag, True)
|
||||
|
||||
|
||||
images = [
|
||||
# manylinux1 images
|
||||
Image("manylinux1", "x86_64", "quay.io/pypa/manylinux1_x86_64", None),
|
||||
Image("manylinux1", "i686", "quay.io/pypa/manylinux1_i686", None),
|
||||
# manylinux2010 images
|
||||
Image("manylinux2010", "x86_64", "quay.io/pypa/manylinux2010_x86_64", None),
|
||||
Image("manylinux2010", "i686", "quay.io/pypa/manylinux2010_i686", None),
|
||||
Image("manylinux2010", "pypy_x86_64", "quay.io/pypa/manylinux2010_x86_64", None),
|
||||
Image("manylinux2010", "pypy_i686", "quay.io/pypa/manylinux2010_i686", None),
|
||||
# manylinux2014 images
|
||||
Image("manylinux2014", "x86_64", "quay.io/pypa/manylinux2014_x86_64", None),
|
||||
Image("manylinux2014", "i686", "quay.io/pypa/manylinux2014_i686", None),
|
||||
Image("manylinux2014", "aarch64", "quay.io/pypa/manylinux2014_aarch64", None),
|
||||
Image("manylinux2014", "ppc64le", "quay.io/pypa/manylinux2014_ppc64le", None),
|
||||
Image("manylinux2014", "s390x", "quay.io/pypa/manylinux2014_s390x", None),
|
||||
Image("manylinux2014", "pypy_x86_64", "quay.io/pypa/manylinux2014_x86_64", None),
|
||||
Image("manylinux2014", "pypy_i686", "quay.io/pypa/manylinux2014_i686", None),
|
||||
Image("manylinux2014", "pypy_aarch64", "quay.io/pypa/manylinux2014_aarch64", None),
|
||||
# manylinux_2_24 images
|
||||
Image("manylinux_2_24", "x86_64", "quay.io/pypa/manylinux_2_24_x86_64", None),
|
||||
Image("manylinux_2_24", "i686", "quay.io/pypa/manylinux_2_24_i686", None),
|
||||
Image("manylinux_2_24", "aarch64", "quay.io/pypa/manylinux_2_24_aarch64", None),
|
||||
Image("manylinux_2_24", "ppc64le", "quay.io/pypa/manylinux_2_24_ppc64le", None),
|
||||
Image("manylinux_2_24", "s390x", "quay.io/pypa/manylinux_2_24_s390x", None),
|
||||
Image("manylinux_2_24", "pypy_x86_64", "quay.io/pypa/manylinux_2_24_x86_64", None),
|
||||
Image("manylinux_2_24", "pypy_i686", "quay.io/pypa/manylinux_2_24_i686", None),
|
||||
Image("manylinux_2_24", "pypy_aarch64", "quay.io/pypa/manylinux_2_24_aarch64", None),
|
||||
PyPAImage(
|
||||
"manylinux2014",
|
||||
[
|
||||
"x86_64",
|
||||
"i686",
|
||||
"aarch64",
|
||||
"ppc64le",
|
||||
"s390x",
|
||||
"pypy_x86_64",
|
||||
"pypy_i686",
|
||||
"pypy_aarch64",
|
||||
],
|
||||
),
|
||||
# manylinux_2_28 images
|
||||
Image("manylinux_2_28", "x86_64", "quay.io/pypa/manylinux_2_28_x86_64", None),
|
||||
Image("manylinux_2_28", "aarch64", "quay.io/pypa/manylinux_2_28_aarch64", None),
|
||||
Image("manylinux_2_28", "ppc64le", "quay.io/pypa/manylinux_2_28_ppc64le", None),
|
||||
Image("manylinux_2_28", "s390x", "quay.io/pypa/manylinux_2_28_s390x", None),
|
||||
Image("manylinux_2_28", "pypy_x86_64", "quay.io/pypa/manylinux_2_28_x86_64", None),
|
||||
Image("manylinux_2_28", "pypy_aarch64", "quay.io/pypa/manylinux_2_28_aarch64", None),
|
||||
# musllinux_1_1 images
|
||||
Image("musllinux_1_1", "x86_64", "quay.io/pypa/musllinux_1_1_x86_64", None),
|
||||
Image("musllinux_1_1", "i686", "quay.io/pypa/musllinux_1_1_i686", None),
|
||||
Image("musllinux_1_1", "aarch64", "quay.io/pypa/musllinux_1_1_aarch64", None),
|
||||
Image("musllinux_1_1", "ppc64le", "quay.io/pypa/musllinux_1_1_ppc64le", None),
|
||||
Image("musllinux_1_1", "s390x", "quay.io/pypa/musllinux_1_1_s390x", None),
|
||||
PyPAImage(
|
||||
"manylinux_2_28",
|
||||
[
|
||||
"x86_64",
|
||||
"i686",
|
||||
"aarch64",
|
||||
"ppc64le",
|
||||
"s390x",
|
||||
"pypy_x86_64",
|
||||
"pypy_i686",
|
||||
"pypy_aarch64",
|
||||
],
|
||||
),
|
||||
# manylinux_2_31 images
|
||||
PyPAImage("manylinux_2_31", ["armv7l"]),
|
||||
# manylinux_2_34 images
|
||||
PyPAImage(
|
||||
"manylinux_2_34",
|
||||
[
|
||||
"x86_64",
|
||||
"i686",
|
||||
"aarch64",
|
||||
"ppc64le",
|
||||
"s390x",
|
||||
"pypy_x86_64",
|
||||
"pypy_i686",
|
||||
"pypy_aarch64",
|
||||
],
|
||||
),
|
||||
# manylinux_2_35 images
|
||||
PyPAImage("manylinux_2_35", ["armv7l"]),
|
||||
# manylinux_2_39 images
|
||||
PyPAImage("manylinux_2_39", ["riscv64"]),
|
||||
# musllinux_1_2 images
|
||||
PyPAImage(
|
||||
"musllinux_1_2", ["x86_64", "i686", "aarch64", "ppc64le", "s390x", "armv7l", "riscv64"]
|
||||
),
|
||||
]
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
@@ -84,6 +111,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()
|
||||
@@ -99,10 +141,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,51 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
from __future__ import annotations
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "playwright",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from html2image import Html2Image # type: ignore[import]
|
||||
except ImportError:
|
||||
sys.exit(
|
||||
"""
|
||||
html2image not found. Ensure you have Chrome (on Mac/Windows) or
|
||||
Chromium (on Linux) installed, and then do:
|
||||
pip install html2image
|
||||
"""
|
||||
)
|
||||
from playwright.sync_api import sync_playwright # type: ignore[import-not-found]
|
||||
|
||||
CSS = """
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lato&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
html, body {
|
||||
font-family: Lato, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: #404040;
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
subprocess.run(["mkdocs", "build"])
|
||||
def main() -> None:
|
||||
subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=True)
|
||||
|
||||
hti = Html2Image(custom_flags=["--force-device-scale-factor=2"])
|
||||
html_str = Path("docs/diagram.html").read_text()
|
||||
html_str = f"<html><head>{CSS}</head><body>{html_str}</body></html>"
|
||||
|
||||
html_str = Path("docs/diagram.md").read_text()
|
||||
css_tags = f"""
|
||||
<style>{Path("site/css/theme.css").read_text()}</style>
|
||||
<style>{Path("site/css/theme_extra.css").read_text()}</style>
|
||||
<style>{Path("site/extra.css").read_text()}</style>
|
||||
<style>
|
||||
body {{
|
||||
background: white;
|
||||
}}
|
||||
</style>
|
||||
"""
|
||||
html_str = css_tags + html_str
|
||||
with tempfile.TemporaryDirectory() as tmp_dir_str:
|
||||
html_path = Path(tmp_dir_str) / "diagram_screenshot.html"
|
||||
html_path.write_text(html_str)
|
||||
|
||||
[screenshot, *_] = hti.screenshot(
|
||||
html_str=html_str,
|
||||
size=(830, 405),
|
||||
)
|
||||
dest_path = Path("docs/data/how-it-works.png")
|
||||
|
||||
dest_path = Path("docs/data/how-it-works.png")
|
||||
if dest_path.exists():
|
||||
dest_path.unlink()
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
page = browser.new_page(device_scale_factor=2, viewport={"width": 830, "height": 600})
|
||||
page.goto(html_path.as_uri())
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
Path(screenshot).rename(dest_path)
|
||||
height = page.evaluate("document.body.scrollHeight")
|
||||
page.set_viewport_size({"width": 830, "height": height})
|
||||
|
||||
page.screenshot(path=str(dest_path), full_page=True)
|
||||
browser.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "click",
|
||||
# "packaging",
|
||||
# "requests",
|
||||
# "rich",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
|
||||
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
+98
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "cibuildwheel",
|
||||
# "requests",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# cibuildwheel = { path = ".." }
|
||||
# ///
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import requests
|
||||
|
||||
from cibuildwheel.extra import github_api_request
|
||||
from cibuildwheel.util.python_build_standalone import (
|
||||
PythonBuildStandaloneAsset,
|
||||
PythonBuildStandaloneReleaseData,
|
||||
)
|
||||
|
||||
# Resolve path relative to this script so writes go to the source checkout,
|
||||
# not the uv-installed copy of the package.
|
||||
DIR: Final[Path] = Path(__file__).parent.parent.resolve()
|
||||
PYTHON_BUILD_STANDALONE_RELEASES: Final[Path] = (
|
||||
DIR / "cibuildwheel/resources/python-build-standalone-releases.json"
|
||||
)
|
||||
|
||||
|
||||
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"]
|
||||
|
||||
# Build a sha256 map from the SHA256SUMS file in the release
|
||||
sha256_sums_urls = [
|
||||
ga["browser_download_url"] for ga in github_assets if ga["name"] == "SHA256SUMS"
|
||||
]
|
||||
name_to_sha256: dict[str, str] = {}
|
||||
if sha256_sums_urls:
|
||||
response = requests.get(sha256_sums_urls[0])
|
||||
response.raise_for_status()
|
||||
for line in response.text.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) == 2:
|
||||
sha256_hex, filename = parts
|
||||
# The filename may have a leading "./" or spaces - strip it
|
||||
name_to_sha256[filename.lstrip("./")] = sha256_hex
|
||||
|
||||
assets = [
|
||||
PythonBuildStandaloneAsset(
|
||||
name=ga["name"],
|
||||
url=ga["browser_download_url"],
|
||||
sha256=name_to_sha256.get(ga["name"], ""),
|
||||
)
|
||||
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()
|
||||
+345
-70
@@ -1,13 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "click",
|
||||
# "packaging",
|
||||
# "requests",
|
||||
# "rich",
|
||||
# "cibuildwheel",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# cibuildwheel = { path = ".." }
|
||||
# ///
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import difflib
|
||||
import hashlib
|
||||
import logging
|
||||
from collections.abc import Mapping, MutableMapping
|
||||
import operator
|
||||
import re
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from typing import Any, Final, Literal, TypedDict, Union
|
||||
from typing import Any, Final, Literal, NotRequired, TypedDict
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
import click
|
||||
import requests
|
||||
@@ -17,8 +32,12 @@ 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
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping, MutableMapping
|
||||
|
||||
log = logging.getLogger("cibw")
|
||||
|
||||
@@ -31,26 +50,20 @@ RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"
|
||||
ArchStr = Literal["32", "64", "ARM64"]
|
||||
|
||||
|
||||
class ConfigWinCP(TypedDict):
|
||||
class Config(TypedDict):
|
||||
identifier: str
|
||||
version: str
|
||||
arch: str
|
||||
|
||||
|
||||
class ConfigWinPP(TypedDict):
|
||||
identifier: str
|
||||
version: str
|
||||
arch: str
|
||||
class ConfigUrl(Config):
|
||||
url: str
|
||||
sha256: NotRequired[str]
|
||||
|
||||
|
||||
class ConfigMacOS(TypedDict):
|
||||
identifier: str
|
||||
version: str
|
||||
url: str
|
||||
|
||||
|
||||
AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigMacOS]
|
||||
class ConfigPyodide(Config):
|
||||
default_pyodide_version: str
|
||||
node_version: str
|
||||
sha256: str
|
||||
|
||||
|
||||
# The following set of "Versions" classes allow the initial call to the APIs to
|
||||
@@ -58,7 +71,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 +85,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()
|
||||
@@ -80,7 +97,7 @@ class WindowsVersions:
|
||||
|
||||
self.version_dict = {Version(v): v for v in cp_info["versions"]}
|
||||
|
||||
def update_version_windows(self, spec: Specifier) -> ConfigWinCP | None:
|
||||
def update_version_windows(self, spec: Specifier) -> Config | None:
|
||||
# Specifier.filter selects all non pre-releases that match the spec,
|
||||
# unless there are only pre-releases, then it selects pre-releases
|
||||
# instead (like pip)
|
||||
@@ -92,15 +109,100 @@ 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}"
|
||||
return ConfigWinCP(
|
||||
identifier = f"cp{version.major}{version.minor}{flags}-{self.arch}"
|
||||
return Config(
|
||||
identifier=identifier,
|
||||
version=self.version_dict[version],
|
||||
arch=self.arch_str,
|
||||
)
|
||||
|
||||
|
||||
class GraalPyVersions:
|
||||
def __init__(self) -> None:
|
||||
response = requests.get("https://api.github.com/repos/oracle/graalpython/releases")
|
||||
response.raise_for_status()
|
||||
|
||||
releases = response.json()
|
||||
gp_version_re = re.compile(r"-(\d+\.\d+\.\d+)$")
|
||||
cp_version_re = re.compile(r"Python (\d+\.\d+(?:\.\d+)?)")
|
||||
for release in releases:
|
||||
m = gp_version_re.search(release["tag_name"])
|
||||
if m:
|
||||
release["graalpy_version"] = Version(m.group(1))
|
||||
m = cp_version_re.search(release["body"])
|
||||
if m:
|
||||
release["python_version"] = Version(m.group(1))
|
||||
|
||||
self.releases = [r for r in releases if "graalpy_version" in r and "python_version" in r]
|
||||
|
||||
def update_version(self, identifier: str, spec: Specifier) -> ConfigUrl | None:
|
||||
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("-", maxsplit=1)[0].split("_")[1]
|
||||
if "." not in gpspec_str and len(gpspec_str) == 3:
|
||||
gpspec_str = gpspec_str[:2] + "." + gpspec_str[-1]
|
||||
gpspec = Specifier(f"=={gpspec_str}.*")
|
||||
|
||||
releases_tmp = (r for r in self.releases if spec.contains(r["python_version"]))
|
||||
releases_tmp = (r for r in releases_tmp if gpspec.contains(r["graalpy_version"]))
|
||||
releases = sorted(releases_tmp, key=lambda r: r["graalpy_version"])
|
||||
|
||||
if not releases:
|
||||
msg = f"GraalPy {arch} not found for {spec}!"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
release = releases[-1]
|
||||
version = release["python_version"]
|
||||
gpversion = release["graalpy_version"]
|
||||
|
||||
if "macosx" in identifier:
|
||||
arch = "x86_64" if "x86_64" in identifier else "arm64"
|
||||
platform = "macos"
|
||||
elif "win" in identifier:
|
||||
arch = "aarch64" if "arm64" in identifier else "x86_64"
|
||||
platform = "windows"
|
||||
else:
|
||||
msg = "GraalPy provides downloads for macOS and Windows and is included for manylinux"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
arch = "amd64" if arch == "x86_64" else "aarch64"
|
||||
ext = "zip" if "win" in identifier else "tar.gz"
|
||||
urls = [
|
||||
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}")
|
||||
]
|
||||
if urls:
|
||||
(url,) = urls
|
||||
# Fetch sha256 from the ".sha256" sidecar asset in the same release
|
||||
sha256 = ""
|
||||
sha256_asset_name = url.rsplit("/", 1)[-1] + ".sha256"
|
||||
sha256_urls = [
|
||||
rf["browser_download_url"]
|
||||
for rf in release["assets"]
|
||||
if rf["name"] == sha256_asset_name
|
||||
]
|
||||
if sha256_urls:
|
||||
sha256_response = requests.get(sha256_urls[0])
|
||||
sha256_response.raise_for_status()
|
||||
sha256 = sha256_response.text.strip().split()[0]
|
||||
return ConfigUrl(
|
||||
identifier=identifier,
|
||||
version=f"{version.major}.{version.minor}",
|
||||
url=url,
|
||||
sha256=sha256,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class PyPyVersions:
|
||||
def __init__(self, arch_str: ArchStr):
|
||||
response = requests.get("https://downloads.python.org/pypy/versions.json")
|
||||
@@ -126,9 +228,9 @@ class PyPyVersions:
|
||||
]
|
||||
return urls[0] if urls else ""
|
||||
|
||||
def update_version_windows(self, spec: Specifier) -> ConfigWinCP:
|
||||
def update_version_windows(self, spec: Specifier) -> ConfigUrl:
|
||||
releases = [r for r in self.releases if spec.contains(r["python_version"])]
|
||||
releases = sorted(releases, key=lambda r: r["pypy_version"])
|
||||
releases = sorted(releases, key=operator.itemgetter("pypy_version"))
|
||||
releases = [r for r in releases if self.get_arch_file(r)]
|
||||
|
||||
if not releases:
|
||||
@@ -142,20 +244,19 @@ class PyPyVersions:
|
||||
identifier = f"pp{version.major}{version.minor}-{version_arch}"
|
||||
url = self.get_arch_file(release)
|
||||
|
||||
return ConfigWinPP(
|
||||
return ConfigUrl(
|
||||
identifier=identifier,
|
||||
version=f"{version.major}.{version.minor}",
|
||||
arch=self.arch,
|
||||
url=url,
|
||||
)
|
||||
|
||||
def update_version_macos(self, spec: Specifier) -> ConfigMacOS:
|
||||
def update_version_macos(self, spec: Specifier) -> ConfigUrl:
|
||||
if self.arch not in {"64", "ARM64"}:
|
||||
msg = f"'{self.arch}' arch not supported yet on macOS"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
releases = [r for r in self.releases if spec.contains(r["python_version"])]
|
||||
releases = sorted(releases, key=lambda r: r["pypy_version"])
|
||||
releases = sorted(releases, key=operator.itemgetter("pypy_version"))
|
||||
|
||||
if not releases:
|
||||
msg = f"PyPy macOS {self.arch} not found for {spec}!"
|
||||
@@ -173,7 +274,7 @@ class PyPyVersions:
|
||||
if "" in rf["platform"] == "darwin" and rf["arch"] == arch
|
||||
)
|
||||
|
||||
return ConfigMacOS(
|
||||
return ConfigUrl(
|
||||
identifier=identifier,
|
||||
version=f"{version.major}.{version.minor}",
|
||||
url=url,
|
||||
@@ -191,43 +292,155 @@ 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
|
||||
|
||||
uri = int(release["resource_uri"].rstrip("/").split("/")[-1])
|
||||
self.versions_dict[version] = uri
|
||||
# Removing the prefix
|
||||
version = Version(release["name"].removeprefix("Python "))
|
||||
self.versions_dict[version] = release["resource_uri"]
|
||||
|
||||
def update_version_macos(
|
||||
self, identifier: str, version: Version, spec: Specifier
|
||||
) -> ConfigMacOS | None:
|
||||
files_response = requests.get("https://www.python.org/api/v2/downloads/release_file/")
|
||||
files_response.raise_for_status()
|
||||
self.files_info = files_response.json()
|
||||
|
||||
def update_version(self, identifier: str, spec: Specifier, file_ident: str) -> ConfigUrl | None:
|
||||
# see note above on Specifier.filter
|
||||
unsorted_versions = spec.filter(self.versions_dict)
|
||||
sorted_versions = sorted(unsorted_versions, reverse=True)
|
||||
|
||||
if version <= Version("3.8.9999"):
|
||||
file_ident = "macosx10.9.pkg"
|
||||
else:
|
||||
file_ident = "macos11.pkg"
|
||||
|
||||
for new_version in sorted_versions:
|
||||
# Find the first patch version that contains the requested file
|
||||
uri = self.versions_dict[new_version]
|
||||
response = requests.get(
|
||||
f"https://www.python.org/api/v2/downloads/release_file/?release={uri}"
|
||||
)
|
||||
response.raise_for_status()
|
||||
file_info = response.json()
|
||||
files = [rf for rf in self.files_info if rf["release"] == uri]
|
||||
|
||||
urls = [rf["url"] for rf in file_info if file_ident in rf["url"]]
|
||||
if urls:
|
||||
return ConfigMacOS(
|
||||
matching = [rf for rf in files if file_ident in rf["url"]]
|
||||
if matching:
|
||||
rf = matching[0]
|
||||
return ConfigUrl(
|
||||
identifier=identifier,
|
||||
version=f"{new_version.major}.{new_version.minor}",
|
||||
url=urls[0],
|
||||
url=rf["url"],
|
||||
sha256=rf.get("sha256_sum", ""),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def update_version_macos(
|
||||
self, identifier: str, version: Version, spec: Specifier
|
||||
) -> ConfigUrl | None:
|
||||
macver = "x10.9" if version <= Version("3.8.9999") else "11"
|
||||
return self.update_version(identifier, spec, f"macos{macver}.pkg")
|
||||
|
||||
def update_version_android(self, identifier: str, spec: Specifier) -> ConfigUrl | None:
|
||||
return self.update_version(identifier, spec, android_triplet(identifier))
|
||||
|
||||
|
||||
class MavenVersions:
|
||||
MAVEN_URL = "https://repo.maven.apache.org/maven2/com/chaquo/python/python"
|
||||
|
||||
def __init__(self) -> None:
|
||||
response = requests.get(f"{self.MAVEN_URL}/maven-metadata.xml")
|
||||
response.raise_for_status()
|
||||
root = ET.fromstring(response.text)
|
||||
|
||||
self.versions: list[Version] = []
|
||||
for version_elem in root.findall("./versioning/versions/version"):
|
||||
version_str = version_elem.text
|
||||
assert isinstance(version_str, str), version_str
|
||||
self.versions.append(Version(version_str))
|
||||
|
||||
def update_version_android(self, identifier: str, spec: Specifier) -> ConfigUrl | None:
|
||||
sorted_versions = sorted(spec.filter(self.versions), reverse=True)
|
||||
|
||||
# Return a config using the highest version for the given specifier.
|
||||
if sorted_versions:
|
||||
max_version = sorted_versions[0]
|
||||
triplet = android_triplet(identifier)
|
||||
return ConfigUrl(
|
||||
identifier=identifier,
|
||||
version=f"{max_version.major}.{max_version.minor}",
|
||||
url=f"{self.MAVEN_URL}/{max_version}/python-{max_version}-{triplet}.tar.gz",
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class CPythonIOSVersions:
|
||||
def __init__(self) -> None:
|
||||
response = requests.get(
|
||||
"https://api.github.com/repos/beeware/Python-Apple-support/releases",
|
||||
headers={
|
||||
"Accept": "application/vnd.github+json",
|
||||
"X-Github-Api-Version": "2022-11-28",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
releases_info = response.json()
|
||||
self.versions_dict: dict[Version, dict[int, str]] = {}
|
||||
|
||||
# Each release has a name like "3.13-b4"
|
||||
for release in releases_info:
|
||||
py_version, build = release["name"].split("-")
|
||||
version = Version(py_version)
|
||||
self.versions_dict.setdefault(version, {})
|
||||
|
||||
# There are several release assets associated with each release;
|
||||
# The name of the asset will be something like
|
||||
# "Python-3.11-iOS-support.b4.tar.gz". Store all builds that are
|
||||
# "-iOS-support" builds, retaining the download URL.
|
||||
for asset in release["assets"]:
|
||||
filename, build, _, _ = asset["name"].rsplit(".", 3)
|
||||
if filename.endswith("-iOS-support"):
|
||||
self.versions_dict[version][int(build[1:])] = asset["browser_download_url"]
|
||||
|
||||
def update_version_ios(self, identifier: str, version: Version) -> ConfigUrl | None:
|
||||
# Return a config using the highest build number for the given version.
|
||||
urls = [url for _, url in sorted(self.versions_dict.get(version, {}).items())]
|
||||
if urls:
|
||||
return ConfigUrl(
|
||||
identifier=identifier,
|
||||
version=str(version),
|
||||
url=urls[-1],
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class PyodideVersions:
|
||||
def __init__(self) -> None:
|
||||
xbuildenv_info = get_pyodide_xbuildenv_info()
|
||||
self.releases = xbuildenv_info["releases"]
|
||||
|
||||
def update_version_pyodide(
|
||||
self, identifier: str, version: Version, spec: Specifier, node_version: str
|
||||
) -> ConfigPyodide | None:
|
||||
# get releases that match the python version
|
||||
releases = [
|
||||
r for r in self.releases.values() if spec.contains(Version(r["python_version"]))
|
||||
]
|
||||
# sort by version, latest first
|
||||
releases.sort(key=lambda r: Version(r["version"]), reverse=True)
|
||||
|
||||
if not releases:
|
||||
msg = f"Pyodide not found for {spec}!"
|
||||
raise ValueError(msg)
|
||||
|
||||
final_releases = [r for r in releases if not Version(r["version"]).is_prerelease]
|
||||
|
||||
# prefer a final release if available, otherwise use the latest
|
||||
# pre-release
|
||||
release = final_releases[0] if final_releases else releases[0]
|
||||
|
||||
return ConfigPyodide(
|
||||
identifier=identifier,
|
||||
version=str(version),
|
||||
default_pyodide_version=release["version"],
|
||||
node_version=node_version,
|
||||
sha256=release["sha256"],
|
||||
)
|
||||
|
||||
|
||||
# This is a universal interface to all the above Versions classes. Given an
|
||||
# identifier, it updates a config dict.
|
||||
@@ -235,48 +448,112 @@ class CPythonVersions:
|
||||
|
||||
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.cpython = CPythonVersions()
|
||||
self.macos_pypy = PyPyVersions("64")
|
||||
self.macos_pypy_arm64 = PyPyVersions("ARM64")
|
||||
|
||||
self.maven = MavenVersions()
|
||||
self.ios_cpython = CPythonIOSVersions()
|
||||
|
||||
self.graalpy = GraalPyVersions()
|
||||
|
||||
self.pyodide = PyodideVersions()
|
||||
|
||||
def _stream_sha256(self, url: str) -> str:
|
||||
"""Download a file (streaming) and return its SHA256 hex digest."""
|
||||
log.debug("Computing sha256 for %s by streaming download...", url)
|
||||
response = requests.get(url, stream=True)
|
||||
response.raise_for_status()
|
||||
hasher = hashlib.sha256()
|
||||
for chunk in response.iter_content(65536):
|
||||
hasher.update(chunk)
|
||||
return hasher.hexdigest()
|
||||
|
||||
def update_config(self, config: MutableMapping[str, str]) -> None:
|
||||
identifier = config["identifier"]
|
||||
version = Version(config["version"])
|
||||
spec = Specifier(f"=={version.major}.{version.minor}.*")
|
||||
log.info("Reading in %r -> %s @ %s", str(identifier), spec, version)
|
||||
orig_config = copy.copy(config)
|
||||
config_update: AnyConfig | None = None
|
||||
config_update: Config | None = None
|
||||
|
||||
# We need to use ** in update due to MyPy (probably a bug)
|
||||
if "macosx" in identifier:
|
||||
if identifier.startswith("cp"):
|
||||
config_update = self.macos_cpython.update_version_macos(identifier, version, spec)
|
||||
config_update = self.cpython.update_version_macos(identifier, version, spec)
|
||||
elif identifier.startswith("pp"):
|
||||
if "macosx_x86_64" in identifier:
|
||||
config_update = self.macos_pypy.update_version_macos(spec)
|
||||
elif "macosx_arm64" in identifier:
|
||||
config_update = self.macos_pypy_arm64.update_version_macos(spec)
|
||||
elif "win32" in identifier:
|
||||
if identifier.startswith("cp"):
|
||||
config_update = self.windows_32.update_version_windows(spec)
|
||||
elif identifier.startswith("gp"):
|
||||
if "macosx_x86_64" in identifier:
|
||||
return
|
||||
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:
|
||||
# Python 3.13 is released by Chaquopy on Maven Central.
|
||||
# Python 3.14 and newer have official releases on python.org.
|
||||
versions = self.maven if identifier.startswith("cp313") else self.cpython
|
||||
config_update = versions.update_version_android(identifier, spec)
|
||||
elif "ios" in identifier:
|
||||
# Python 3.13 and 3.14 are released by BeeWare on GitHub.
|
||||
# Python 3.15 and newer have official releases on python.org.
|
||||
if identifier.startswith(("cp313", "cp314")):
|
||||
config_update = self.ios_cpython.update_version_ios(identifier, version)
|
||||
else:
|
||||
config_update = self.cpython.update_version(
|
||||
identifier, spec, "iOS-XCframework.tar.gz"
|
||||
)
|
||||
elif "pyodide" in identifier:
|
||||
config_update = self.pyodide.update_version_pyodide(
|
||||
identifier, version, spec, config["node_version"]
|
||||
)
|
||||
|
||||
assert config_update is not None, f"{identifier} not found!"
|
||||
config.update(**config_update)
|
||||
|
||||
if config != orig_config:
|
||||
log.info(" Updated %s to %s", orig_config, config)
|
||||
# Fill in sha256 for URL-based configs if not already provided by the
|
||||
# update_version_* method (e.g. PyPy, BeeWare iOS, Maven have no sidecar).
|
||||
# Also fills in sha256 when the CPython API doesn't return a sha256_sum
|
||||
# (e.g. for older releases).
|
||||
# Widen the type to allow arbitrary key access on the underlying dict.
|
||||
config_update_dict: dict[str, str] = config_update # type: ignore[assignment]
|
||||
if "url" in config_update_dict and not config_update_dict.get("sha256"):
|
||||
url = config_update_dict["url"]
|
||||
existing_sha256 = config.get("sha256", "")
|
||||
if url == config.get("url") and existing_sha256:
|
||||
# URL unchanged — preserve the existing sha256
|
||||
config_update_dict["sha256"] = existing_sha256
|
||||
else:
|
||||
config_update_dict["sha256"] = self._stream_sha256(url)
|
||||
|
||||
if config_update != config:
|
||||
log.info(" Updated %s to %s", config, config_update)
|
||||
config.clear()
|
||||
config.update(**config_update)
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -300,11 +577,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()
|
||||
+73
-45
@@ -1,21 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
from __future__ import annotations
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "click",
|
||||
# "packaging",
|
||||
# "requests",
|
||||
# "rich",
|
||||
# "cibuildwheel",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# cibuildwheel = { path = ".." }
|
||||
# ///
|
||||
|
||||
|
||||
import dataclasses
|
||||
import difflib
|
||||
import hashlib
|
||||
import logging
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import click
|
||||
import requests
|
||||
import rich
|
||||
from packaging.version import InvalidVersion, Version
|
||||
from packaging.version import Version
|
||||
from rich.logging import RichHandler
|
||||
from rich.syntax import Syntax
|
||||
|
||||
from cibuildwheel._compat import tomllib
|
||||
from cibuildwheel.extra import github_api_request
|
||||
|
||||
log = logging.getLogger("cibw")
|
||||
|
||||
@@ -25,43 +39,33 @@ 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:
|
||||
name: str
|
||||
download_url: str
|
||||
version: Version
|
||||
version_string: str
|
||||
|
||||
|
||||
def git_ls_remote_versions(url) -> list[VersionTuple]:
|
||||
versions: list[VersionTuple] = []
|
||||
tags = subprocess.run(
|
||||
["git", "ls-remote", "--tags", url], check=True, text=True, capture_output=True
|
||||
).stdout.splitlines()
|
||||
for tag in tags:
|
||||
_, ref = tag.split()
|
||||
assert ref.startswith("refs/tags/")
|
||||
version_string = ref[10:]
|
||||
try:
|
||||
version = Version(version_string)
|
||||
if version.is_devrelease:
|
||||
log.info("Ignoring development release %r", str(version))
|
||||
continue
|
||||
if version.is_prerelease:
|
||||
log.info("Ignoring pre-release %r", str(version))
|
||||
continue
|
||||
# Do not upgrade past 20.22.0 to keep python 3.6 compat
|
||||
if version >= Version("20.22.0"):
|
||||
log.info("Ignoring %r which is not compatible with python 3.6", str(version))
|
||||
continue
|
||||
versions.append(VersionTuple(version, version_string))
|
||||
except InvalidVersion:
|
||||
log.warning("Ignoring ref %r", ref)
|
||||
versions.sort(reverse=True)
|
||||
return versions
|
||||
def get_latest_virtualenv_release() -> VersionTuple:
|
||||
response = github_api_request("repos/pypa/get-virtualenv/releases/latest")
|
||||
tag_name = response["tag_name"]
|
||||
|
||||
asset = next(
|
||||
(asset for asset in response["assets"] if asset["name"] == "virtualenv.pyz"),
|
||||
None,
|
||||
)
|
||||
if not asset:
|
||||
msg = "No asset named 'virtualenv.pyz' found in the latest release of get-virtualenv."
|
||||
raise RuntimeError(msg)
|
||||
|
||||
return VersionTuple(
|
||||
version=Version(tag_name), name=tag_name, download_url=asset["browser_download_url"]
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -82,15 +86,39 @@ 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"])
|
||||
versions = git_ls_remote_versions(GET_VIRTUALENV_GITHUB)
|
||||
if versions[0].version > Version(version):
|
||||
version = versions[0].version_string
|
||||
configurations = tomllib.load(f)
|
||||
default = configurations.pop("default")
|
||||
local_version = str(default["version"])
|
||||
|
||||
result_toml = (
|
||||
f'version = "{version}"\n'
|
||||
f'url = "{GET_VIRTUALENV_URL_TEMPLATE.format(version=version)}"\n'
|
||||
latest_release = get_latest_virtualenv_release()
|
||||
|
||||
if latest_release.version > Version(local_version):
|
||||
version = latest_release.name
|
||||
url = latest_release.download_url
|
||||
sha256 = "" # recomputed below
|
||||
else:
|
||||
version = local_version
|
||||
url = default["url"]
|
||||
sha256 = default.get("sha256", "")
|
||||
|
||||
# Compute sha256 if not already stored (new version or first-time population)
|
||||
if not sha256:
|
||||
log.info("Computing sha256 for %s...", url)
|
||||
response = requests.get(url, stream=True)
|
||||
response.raise_for_status()
|
||||
hasher = hashlib.sha256()
|
||||
for chunk in response.iter_content(65536):
|
||||
hasher.update(chunk)
|
||||
sha256 = hasher.hexdigest()
|
||||
|
||||
configurations["default"] = {
|
||||
"version": version,
|
||||
"url": url,
|
||||
"sha256": sha256,
|
||||
}
|
||||
result_toml = "".join(
|
||||
f'{key} = {{ version = "{value["version"]}", url = "{value["url"]}", sha256 = "{value.get("sha256", "")}" }}\n'
|
||||
for key, value in configurations.items()
|
||||
)
|
||||
|
||||
rich.print() # spacer
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "2.14.0"
|
||||
__version__ = "4.0.0rc1"
|
||||
|
||||
+309
-141
@@ -1,58 +1,114 @@
|
||||
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 pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
from typing import Protocol
|
||||
|
||||
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._compat.tarfile import TarFile, safe_extractall
|
||||
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, ensure_cache_sentinel
|
||||
from cibuildwheel.util.helpers import strtobool
|
||||
from cibuildwheel.util.resources import read_all_configs
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator, Iterable, Sequence
|
||||
from typing import Any, Literal, TextIO
|
||||
|
||||
|
||||
@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: # noqa: ANN401
|
||||
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.
|
||||
"""
|
||||
# Default in 3.15+, only needed on 3.14
|
||||
if sys.version_info >= (3, 14):
|
||||
arg_parser = functools.partial(argparse.ArgumentParser, suggest_on_error=True)
|
||||
else:
|
||||
arg_parser = argparse.ArgumentParser
|
||||
|
||||
parser = arg_parser(
|
||||
description="Build wheels for all the platforms.",
|
||||
epilog="""
|
||||
Most options are supplied via environment variables or in
|
||||
--config-file (pyproject.toml usually). See
|
||||
https://github.com/pypa/cibuildwheel#options for info.
|
||||
""",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--platform",
|
||||
choices=["auto", "linux", "macos", "windows"],
|
||||
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 +126,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 +169,7 @@ def main() -> None:
|
||||
parser.add_argument(
|
||||
"package_dir",
|
||||
metavar="PACKAGE",
|
||||
default=Path("."),
|
||||
default=Path(),
|
||||
type=Path,
|
||||
nargs="?",
|
||||
help="""
|
||||
@@ -120,6 +189,12 @@ def main() -> None:
|
||||
help="Print the build identifiers matched by the current invocation and exit.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--clean-cache",
|
||||
action="store_true",
|
||||
help="Clear the cibuildwheel cache and exit.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--allow-empty",
|
||||
action="store_true",
|
||||
@@ -127,13 +202,58 @@ 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
|
||||
|
||||
if args.clean_cache:
|
||||
if not CIBW_CACHE_PATH.exists():
|
||||
print(f"Cache directory does not exist: {CIBW_CACHE_PATH}")
|
||||
sys.exit(0)
|
||||
|
||||
sentinel_file = CIBW_CACHE_PATH / "CACHEDIR.TAG"
|
||||
if not sentinel_file.exists():
|
||||
print(
|
||||
f"Error: {CIBW_CACHE_PATH} does not appear to be a cibuildwheel cache directory.",
|
||||
"Only directories with a CACHEDIR.TAG sentinel file can be cleaned.",
|
||||
sep="\n",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Verify signature to ensure it's a proper cache dir
|
||||
# See https://bford.info/cachedir/ for more
|
||||
try:
|
||||
sentinel_content = sentinel_file.read_text(encoding="utf-8")
|
||||
except OSError as e:
|
||||
print(f"Error reading cache directory tag: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not sentinel_content.startswith("Signature: 8a477f597d28d172789f06886806bc55"):
|
||||
print(
|
||||
f"Error: {sentinel_file} does not contain a valid cache directory signature.",
|
||||
"For safety, only properly signed cache directories can be cleaned.",
|
||||
sep="\n",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Clearing cache directory: {CIBW_CACHE_PATH}")
|
||||
try:
|
||||
shutil.rmtree(CIBW_CACHE_PATH)
|
||||
print("Cache cleared successfully.")
|
||||
except OSError as e:
|
||||
print(f"Error clearing cache: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
args.package_dir = args.package_dir.resolve()
|
||||
|
||||
# This are always relative to the base directory, even in SDist builds
|
||||
@@ -147,8 +267,8 @@ def main() -> None:
|
||||
# Tarfile builds require extraction and changing the directory
|
||||
temp_dir = Path(mkdtemp(prefix="cibw-sdist-")).resolve(strict=True)
|
||||
try:
|
||||
with tarfile.open(args.package_dir) as tar:
|
||||
tar.extractall(path=temp_dir)
|
||||
with TarFile.open(args.package_dir) as tar:
|
||||
safe_extractall(tar, temp_dir)
|
||||
|
||||
# The extract directory is now the project dir
|
||||
try:
|
||||
@@ -160,7 +280,7 @@ def main() -> None:
|
||||
# This is now the new package dir
|
||||
args.package_dir = project_dir.resolve()
|
||||
|
||||
with chdir(temp_dir):
|
||||
with contextlib.chdir(project_dir):
|
||||
build_in_directory(args)
|
||||
finally:
|
||||
# avoid https://github.com/python/cpython/issues/86962 by performing
|
||||
@@ -177,95 +297,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 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 +342,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,11 +360,14 @@ 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)
|
||||
ensure_cache_sentinel(CIBW_CACHE_PATH)
|
||||
|
||||
print_preamble(platform=platform, options=options, identifiers=identifiers)
|
||||
|
||||
@@ -304,27 +375,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 +423,138 @@ 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)
|
||||
|
||||
n = len(identifiers)
|
||||
print(f"{n} build{'s' if n != 1 else ''} selected:")
|
||||
print(", ".join(identifiers))
|
||||
print()
|
||||
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."
|
||||
|
||||
|
||||
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
|
||||
|
||||
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 4.x no longer supports Python < 3.9. 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 4.x no longer supports Python < 3.9. Please use the 2.x series or update `{selector_name}`. "
|
||||
error_type = errors.DeprecationError
|
||||
if "p38" in selector_:
|
||||
msg += f"cibuildwheel 4.x no longer supports Python < 3.9. Please use the 3.x series or update `{selector_name}`. "
|
||||
error_type = errors.DeprecationError
|
||||
if "cp313t" in selector_:
|
||||
msg += f"cibuildwheel 4.x no longer supports Python 3.13 free-threading. Please use the 3.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
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Based on https://github.com/pypa/build/blob/f4ebd495cc0c2c74155bd4fe48b76399fb7927ac/src/build/_compat/tarfile.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
TarFile = tarfile.TarFile
|
||||
|
||||
# Per https://peps.python.org/pep-0706/, the "data" filter will become
|
||||
# the default in Python 3.14. The first series of releases with the filter
|
||||
# had a broken filter that could not process symlinks correctly.
|
||||
elif (3, 11, 5) <= sys.version_info < (3, 14):
|
||||
|
||||
class TarFile(tarfile.TarFile): # pragma: no cover
|
||||
extraction_filter = staticmethod(tarfile.data_filter)
|
||||
|
||||
else:
|
||||
TarFile = tarfile.TarFile # pragma: no cover
|
||||
|
||||
|
||||
# Same availability matrix as the TarFile subclass above. On runtimes that
|
||||
# ship the stdlib ``data`` filter we delegate to it; the fallback branch is
|
||||
# only reached on 3.10.0-3.10.12 / 3.11.0-3.11.4 and validates each member
|
||||
# manually before extraction.
|
||||
if sys.version_info >= (3, 11, 5):
|
||||
|
||||
def safe_extractall(tar: tarfile.TarFile, path: Path) -> None: # pragma: no cover
|
||||
"""Extract every member of ``tar`` into ``path`` via the PEP 706 ``data`` filter."""
|
||||
tar.extractall(path, filter="data")
|
||||
|
||||
else:
|
||||
|
||||
def safe_extractall(tar: tarfile.TarFile, path: Path) -> None: # pragma: no cover
|
||||
"""Validate every member of ``tar``, then extract into ``path``.
|
||||
|
||||
Reached on 3.10.0-3.10.12 / 3.11.0-3.11.4 where the stdlib ``data`` filter is missing. Device or special files,
|
||||
paths that escape ``path``, and symlinks/hardlinks whose targets resolve outside ``path`` are rejected before
|
||||
any write hits the disk.
|
||||
|
||||
"""
|
||||
base = path.resolve()
|
||||
for member in tar.getmembers():
|
||||
_validate_safe_member(member, base)
|
||||
tar.extractall(path)
|
||||
|
||||
|
||||
def _validate_safe_member(member: tarfile.TarInfo, base: Path) -> None:
|
||||
if member.ischr() or member.isblk() or member.isfifo():
|
||||
msg = f"refusing to extract special device file {member.name!r}"
|
||||
raise tarfile.TarError(msg)
|
||||
target = (base / member.name).resolve(strict=False)
|
||||
if not target.is_relative_to(base):
|
||||
msg = f"refusing to extract {member.name!r}: path escapes destination"
|
||||
raise tarfile.TarError(msg)
|
||||
if member.issym() or member.islnk():
|
||||
link_base = target.parent if member.issym() else base
|
||||
link_target = (link_base / member.linkname).resolve(strict=False)
|
||||
if not link_target.is_relative_to(base):
|
||||
msg = f"refusing to extract {member.name!r}: link target escapes destination"
|
||||
raise tarfile.TarError(msg)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TarFile",
|
||||
"safe_extractall",
|
||||
]
|
||||
@@ -1,10 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from tomllib import load # noqa: TID251
|
||||
else:
|
||||
from tomli import load # noqa: TID251
|
||||
|
||||
__all__ = ("load",)
|
||||
@@ -1,13 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from typing_extensions import NotRequired, assert_never
|
||||
else:
|
||||
from typing import NotRequired, assert_never # noqa: TID251
|
||||
|
||||
__all__ = (
|
||||
"assert_never",
|
||||
"NotRequired",
|
||||
)
|
||||
+161
-76
@@ -1,79 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import platform as platform_module
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Set
|
||||
from enum import Enum
|
||||
from typing import Final, Literal
|
||||
import typing
|
||||
from enum import StrEnum, auto
|
||||
|
||||
from ._compat.typing import assert_never
|
||||
from .typing import PlatformName
|
||||
from cibuildwheel import errors
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Set
|
||||
from typing import Final, Literal, Self
|
||||
|
||||
from cibuildwheel.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()
|
||||
|
||||
@staticmethod
|
||||
def parse_config(config: str, platform: PlatformName) -> set[Architecture]:
|
||||
# iOS "multiarch" architectures that include both
|
||||
# the CPU architecture and the ABI.
|
||||
arm64_iphoneos = auto()
|
||||
arm64_iphonesimulator = auto()
|
||||
x86_64_iphonesimulator = auto()
|
||||
|
||||
@classmethod
|
||||
def parse_config(cls, config: str, platform: PlatformName) -> set[Self]:
|
||||
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 |= cls.auto_archs(platform=platform)
|
||||
case "native":
|
||||
if native_arch := cls.native_arch(platform=platform):
|
||||
result.add(native_arch)
|
||||
case "all":
|
||||
result |= cls.all_archs(platform=platform)
|
||||
case "auto64":
|
||||
result |= cls.bitness_archs(platform=platform, bitness="64")
|
||||
case "auto32":
|
||||
result |= cls.bitness_archs(platform=platform, bitness="32")
|
||||
case _:
|
||||
try:
|
||||
result.add(cls(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]:
|
||||
@classmethod
|
||||
def native_arch(cls, platform: PlatformName) -> Self | None:
|
||||
native_machine = platform_module.machine()
|
||||
native_architecture = cls(native_machine)
|
||||
|
||||
# Cross-platform support. Used for --print-build-identifiers or docker builds.
|
||||
host_platform: PlatformName = (
|
||||
@@ -82,59 +124,102 @@ class Architecture(Enum):
|
||||
else ("macos" if sys.platform.startswith("darwin") else "linux")
|
||||
)
|
||||
|
||||
native_architecture = Architecture(native_machine)
|
||||
if platform == "pyodide":
|
||||
return cls.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 == cls.x86_64:
|
||||
return cls.x86_64_iphonesimulator
|
||||
else:
|
||||
return cls.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 = cls(synonym)
|
||||
|
||||
native_architecture = Architecture(synonym)
|
||||
return native_architecture
|
||||
|
||||
result = {native_architecture}
|
||||
@classmethod
|
||||
def auto_archs(cls, platform: PlatformName) -> set[Self]:
|
||||
native_arch = cls.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 cls.AMD64 in result:
|
||||
result.add(cls.x86)
|
||||
case "ios" if native_arch == cls.arm64_iphonesimulator:
|
||||
# Also build the device wheel if we're on ARM64.
|
||||
result.add(cls.arm64_iphoneos)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def all_archs(platform: PlatformName) -> set[Architecture]:
|
||||
@classmethod
|
||||
def all_archs(cls, platform: PlatformName) -> set[Self]:
|
||||
all_archs_map = {
|
||||
"linux": {
|
||||
Architecture.x86_64,
|
||||
Architecture.i686,
|
||||
Architecture.aarch64,
|
||||
Architecture.ppc64le,
|
||||
Architecture.s390x,
|
||||
cls.x86_64,
|
||||
cls.i686,
|
||||
cls.aarch64,
|
||||
cls.ppc64le,
|
||||
cls.s390x,
|
||||
cls.armv7l,
|
||||
cls.riscv64,
|
||||
},
|
||||
"macos": {cls.x86_64, cls.arm64, cls.universal2},
|
||||
"windows": {cls.x86, cls.AMD64, cls.ARM64},
|
||||
"pyodide": {cls.wasm32},
|
||||
"android": {cls.x86_64, cls.arm64_v8a},
|
||||
"ios": {
|
||||
cls.x86_64_iphonesimulator,
|
||||
cls.arm64_iphonesimulator,
|
||||
cls.arm64_iphoneos,
|
||||
},
|
||||
"macos": {Architecture.x86_64, Architecture.arm64, Architecture.universal2},
|
||||
"windows": {Architecture.x86, Architecture.AMD64, Architecture.ARM64},
|
||||
}
|
||||
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)
|
||||
@classmethod
|
||||
def bitness_archs(cls, platform: PlatformName, bitness: Literal["64", "32"]) -> set[Self]:
|
||||
# This map maps 64-bit architectures to their 32-bit equivalents.
|
||||
archs_map = {
|
||||
cls.x86_64: cls.i686,
|
||||
cls.AMD64: cls.x86,
|
||||
cls.aarch64: cls.armv7l,
|
||||
}
|
||||
native_arch = cls.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 == cls.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 == cls.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(
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from cibuildwheel import errors
|
||||
from cibuildwheel.logger import log
|
||||
from cibuildwheel.util.cmd import call, shell
|
||||
from cibuildwheel.util.helpers import prepare_command
|
||||
from cibuildwheel.util.packaging import is_abi3_wheel
|
||||
from cibuildwheel.venv import activate_virtualenv, find_uv, virtualenv
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from cibuildwheel.options import BuildOptions
|
||||
|
||||
|
||||
def run_audit(
|
||||
*,
|
||||
tmp_dir: Path,
|
||||
build_options: BuildOptions,
|
||||
wheel: Path,
|
||||
) -> None:
|
||||
"""
|
||||
Run the audit commands on a single wheel.
|
||||
|
||||
Creates a virtualenv (or reuses an existing one) and installs any
|
||||
audit requirements, then runs each audit command template against
|
||||
the wheel. Commands containing {abi3_wheel} are skipped for
|
||||
non-abi3 wheels.
|
||||
"""
|
||||
|
||||
if not needs_audit(build_options.audit_command, wheel.name):
|
||||
return
|
||||
|
||||
log.step("Auditing wheel...")
|
||||
|
||||
use_uv = build_options.build_frontend.name in {"build[uv]", "uv"}
|
||||
version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
dependency_constraint = build_options.dependency_constraints.get_for_python_version(
|
||||
version=version, tmp_dir=tmp_dir
|
||||
)
|
||||
|
||||
# Use the base interpreter, not the venv python, to avoid nested-venv
|
||||
# issues where pip can't be found (seen on Windows + Python 3.13).
|
||||
host_python = Path(getattr(sys, "_base_executable", sys.executable))
|
||||
|
||||
audit_venv_dir = tmp_dir / "audit_venv"
|
||||
if not (audit_venv_dir / "pyvenv.cfg").exists():
|
||||
env = virtualenv(
|
||||
version,
|
||||
host_python,
|
||||
audit_venv_dir,
|
||||
dependency_constraint=dependency_constraint,
|
||||
use_uv=use_uv,
|
||||
)
|
||||
else:
|
||||
env = activate_virtualenv(audit_venv_dir)
|
||||
|
||||
# install audit requirements. This is run every time in case the user has
|
||||
# defined overrides.
|
||||
audit_requires = build_options.audit_requires
|
||||
if audit_requires:
|
||||
print(f"Installing audit dependencies: {', '.join(audit_requires)}")
|
||||
|
||||
pip: list[str]
|
||||
if use_uv:
|
||||
uv_path = find_uv()
|
||||
assert uv_path is not None
|
||||
pip = [str(uv_path), "pip"]
|
||||
else:
|
||||
pip = ["pip"]
|
||||
# we pin if the audit-requires is left as the default "abi3audit"
|
||||
should_pin = audit_requires == ["abi3audit"] and dependency_constraint
|
||||
|
||||
call(
|
||||
*pip,
|
||||
"install",
|
||||
*(["--constraint", str(dependency_constraint)] if should_pin else []),
|
||||
*audit_requires,
|
||||
env=env,
|
||||
)
|
||||
|
||||
audit_command = build_options.audit_command
|
||||
|
||||
for command_template in audit_command:
|
||||
if "{abi3_wheel}" in command_template and "{wheel}" in command_template:
|
||||
msg = (
|
||||
f"Invalid audit command {command_template!r}: cannot contain both {{abi3_wheel}} "
|
||||
"and {{wheel}} placeholders"
|
||||
)
|
||||
raise errors.ConfigurationError(msg)
|
||||
|
||||
if "{abi3_wheel}" in command_template and not is_abi3_wheel(wheel.name):
|
||||
continue
|
||||
|
||||
prepared_command = prepare_command(
|
||||
command_template,
|
||||
abi3_wheel=wheel,
|
||||
wheel=wheel,
|
||||
project=".",
|
||||
package=build_options.package_dir,
|
||||
)
|
||||
|
||||
print(f"Running audit command: {prepared_command}")
|
||||
try:
|
||||
shell(prepared_command, env=env)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Audit command failed with exit code {e.returncode}")
|
||||
msg = f"Audit command failed: {prepared_command}"
|
||||
raise errors.AuditCommandFailedError(msg) from e
|
||||
|
||||
|
||||
def needs_audit(audit_commands: list[str], wheel_name: str) -> bool:
|
||||
saw_abi3_placeholder = False
|
||||
for audit_command in audit_commands:
|
||||
if "{abi3_wheel}" not in audit_command and "{wheel}" not in audit_command:
|
||||
msg = (
|
||||
f"Invalid audit command {audit_command!r}: must contain either "
|
||||
"{{abi3_wheel}} or {{wheel}} placeholder"
|
||||
)
|
||||
raise errors.ConfigurationError(msg)
|
||||
|
||||
if "{abi3_wheel}" in audit_command:
|
||||
saw_abi3_placeholder = True
|
||||
if is_abi3_wheel(wheel_name):
|
||||
return True
|
||||
elif "{wheel}" in audit_command:
|
||||
return True
|
||||
|
||||
if saw_abi3_placeholder:
|
||||
print("No audit required for this wheel, as it is not abi3")
|
||||
else:
|
||||
print("No audit configured")
|
||||
|
||||
return False
|
||||
@@ -1,21 +1,28 @@
|
||||
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
|
||||
|
||||
import bashlex
|
||||
|
||||
# a function that takes a command and the environment, and returns the result
|
||||
EnvironmentExecutor = Callable[[List[str], Dict[str, str]], str]
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import (
|
||||
Callable,
|
||||
Iterable,
|
||||
Mapping,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
# a function that takes a command and the environment, and returns the result
|
||||
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,76 @@
|
||||
import os
|
||||
import re
|
||||
from enum import Enum
|
||||
|
||||
from cibuildwheel.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"
|
||||
|
||||
# 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 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)
|
||||
+19
-12
@@ -1,13 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, Protocol
|
||||
from typing import Protocol
|
||||
|
||||
import bashlex
|
||||
import bashlex.errors
|
||||
|
||||
from . import bashlex_eval
|
||||
from cibuildwheel import bashlex_eval
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
|
||||
class EnvironmentParseError(Exception):
|
||||
@@ -75,12 +79,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 +106,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]
|
||||
|
||||
@@ -124,13 +127,17 @@ class ParsedEnvironment:
|
||||
|
||||
return environment
|
||||
|
||||
def add(self, name: str, value: str) -> None:
|
||||
self.assignments.append(EnvironmentAssignmentRaw(name=name, value=value))
|
||||
def add(self, name: str, value: str, prepend: bool = False) -> None:
|
||||
assignment = EnvironmentAssignmentRaw(name=name, value=value)
|
||||
if prepend:
|
||||
self.assignments.insert(0, assignment)
|
||||
else:
|
||||
self.assignments.append(assignment)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({[repr(a) for a in self.assignments]!r})"
|
||||
|
||||
def options_summary(self) -> Any:
|
||||
def options_summary(self) -> list[EnvironmentAssignment]:
|
||||
return self.assignments
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
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` must 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` must place exactly 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
|
||||
|
||||
|
||||
class AuditCommandFailedError(FatalError):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.return_code = 9
|
||||
+79
-5
@@ -4,20 +4,31 @@ These are utilities for the `/bin` scripts, not for the `cibuildwheel` program.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
import json
|
||||
import time
|
||||
import typing
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from io import StringIO
|
||||
from typing import Protocol
|
||||
from typing import NotRequired, Protocol
|
||||
|
||||
from cibuildwheel import __version__ as cibw_version
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
|
||||
__all__ = ("Printable", "dump_python_configurations")
|
||||
|
||||
|
||||
class Printable(Protocol):
|
||||
def __str__(self) -> str:
|
||||
...
|
||||
def __str__(self) -> str: ...
|
||||
|
||||
|
||||
def dump_python_configurations(
|
||||
inp: Mapping[str, Mapping[str, Sequence[Mapping[str, Printable]]]]
|
||||
inp: Mapping[str, Mapping[str, Sequence[Mapping[str, Printable]]]],
|
||||
) -> str:
|
||||
output = StringIO()
|
||||
for header, values in inp.items():
|
||||
@@ -33,3 +44,66 @@ 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 in {403, 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
|
||||
sha256: 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,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import shlex
|
||||
import typing
|
||||
from typing import Literal, get_args
|
||||
|
||||
from cibuildwheel.util.helpers import parse_key_value_string, prepare_command
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
from typing import Self
|
||||
|
||||
from cibuildwheel.typing import PathOrStr
|
||||
|
||||
BuildFrontendName = Literal["pip", "build", "build[uv]", "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:
|
||||
return ["-" + -level * "q"]
|
||||
|
||||
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]
|
||||
|
||||
|
||||
def prepare_config_settings(config_settings: str, *, project: PathOrStr, package: PathOrStr) -> str:
|
||||
return prepare_command(config_settings, project=project, package=package)
|
||||
|
||||
|
||||
# Based on build.__main__.main.
|
||||
def parse_config_settings(config_settings_str: str) -> dict[str, str | list[str]]:
|
||||
config_settings: dict[str, str | list[str]] = {}
|
||||
for arg in shlex.split(config_settings_str):
|
||||
setting, _, value = arg.partition("=")
|
||||
existing_value = config_settings.get(setting)
|
||||
if existing_value is None:
|
||||
config_settings[setting] = value
|
||||
elif isinstance(existing_value, str):
|
||||
config_settings[setting] = [existing_value, value]
|
||||
else:
|
||||
existing_value.append(value)
|
||||
|
||||
return config_settings
|
||||
|
||||
|
||||
def get_build_frontend_extra_flags(
|
||||
build_frontend: BuildFrontendConfig, verbosity_level: int, config_settings: str
|
||||
) -> list[str]:
|
||||
return [
|
||||
*_split_config_settings(config_settings),
|
||||
*build_frontend.args,
|
||||
*_get_verbosity_flags(verbosity_level, build_frontend.name),
|
||||
]
|
||||
@@ -1,489 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from collections.abc import Iterable, Iterator, Sequence, Set
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path, PurePath, PurePosixPath
|
||||
from typing import OrderedDict, Tuple
|
||||
|
||||
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,
|
||||
BuildSelector,
|
||||
NonPlatformWheelError,
|
||||
build_frontend_or_default,
|
||||
find_compatible_wheel,
|
||||
get_build_verbosity_extra_flags,
|
||||
prepare_command,
|
||||
read_python_configs,
|
||||
split_config_settings,
|
||||
test_fail_cwd_file,
|
||||
unwrap,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PythonConfiguration:
|
||||
version: str
|
||||
identifier: str
|
||||
path_str: str
|
||||
|
||||
@property
|
||||
def path(self) -> PurePosixPath:
|
||||
return PurePosixPath(self.path_str)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BuildStep:
|
||||
platform_configs: list[PythonConfiguration]
|
||||
platform_tag: str
|
||||
container_image: str
|
||||
|
||||
|
||||
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]
|
||||
|
||||
# return all configurations whose arch is in our `architectures` set,
|
||||
# and match the build/skip rules
|
||||
return [
|
||||
c
|
||||
for c in python_configurations
|
||||
if any(c.identifier.endswith(arch.value) for arch in architectures)
|
||||
and build_selector(c.identifier)
|
||||
]
|
||||
|
||||
|
||||
def container_image_for_python_configuration(config: PythonConfiguration, options: Options) -> str:
|
||||
build_options = options.build_options(config.identifier)
|
||||
# e.g
|
||||
# identifier is 'cp310-manylinux_x86_64'
|
||||
# platform_tag is 'manylinux_x86_64'
|
||||
# platform_arch is 'x86_64'
|
||||
_, platform_tag = config.identifier.split("-", 1)
|
||||
_, platform_arch = platform_tag.split("_", 1)
|
||||
|
||||
assert build_options.manylinux_images is not None
|
||||
assert build_options.musllinux_images is not None
|
||||
|
||||
return (
|
||||
build_options.manylinux_images[platform_arch]
|
||||
if platform_tag.startswith("manylinux")
|
||||
else build_options.musllinux_images[platform_arch]
|
||||
)
|
||||
|
||||
|
||||
def get_build_steps(
|
||||
options: Options, python_configurations: list[PythonConfiguration]
|
||||
) -> Iterator[BuildStep]:
|
||||
"""
|
||||
Groups PythonConfigurations into BuildSteps. Each BuildStep represents a
|
||||
separate container instance.
|
||||
"""
|
||||
steps = OrderedDict[Tuple[str, str, str], 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)
|
||||
|
||||
step_key = (platform_tag, container_image, before_all)
|
||||
|
||||
if step_key in steps:
|
||||
steps[step_key].platform_configs.append(config)
|
||||
else:
|
||||
steps[step_key] = BuildStep(
|
||||
platform_configs=[config],
|
||||
platform_tag=platform_tag,
|
||||
container_image=container_image,
|
||||
)
|
||||
|
||||
yield from steps.values()
|
||||
|
||||
|
||||
def check_all_python_exist(
|
||||
*, platform_configs: Iterable[PythonConfiguration], container: OCIContainer
|
||||
) -> None:
|
||||
exist = True
|
||||
messages = []
|
||||
for config in platform_configs:
|
||||
python_path = config.path / "bin" / "python"
|
||||
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)
|
||||
|
||||
|
||||
def build_in_container(
|
||||
*,
|
||||
options: Options,
|
||||
platform_configs: Sequence[PythonConfiguration],
|
||||
container: OCIContainer,
|
||||
container_project_path: PurePath,
|
||||
container_package_dir: PurePath,
|
||||
) -> None:
|
||||
container_output_dir = PurePosixPath("/output")
|
||||
|
||||
check_all_python_exist(platform_configs=platform_configs, container=container)
|
||||
|
||||
log.step("Copying project into container...")
|
||||
container.copy_into(Path.cwd(), container_project_path)
|
||||
|
||||
before_all_options_identifier = platform_configs[0].identifier
|
||||
before_all_options = options.build_options(before_all_options_identifier)
|
||||
|
||||
if before_all_options.before_all:
|
||||
log.step("Running before_all...")
|
||||
|
||||
env = container.get_environment()
|
||||
env["PATH"] = f'/opt/python/cp38-cp38/bin:{env["PATH"]}'
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
env["PIP_ROOT_USER_ACTION"] = "ignore"
|
||||
env = before_all_options.environment.as_dictionary(
|
||||
env, executor=container.environment_executor
|
||||
)
|
||||
|
||||
before_all_prepared = prepare_command(
|
||||
before_all_options.before_all,
|
||||
project=container_project_path,
|
||||
package=container_package_dir,
|
||||
)
|
||||
container.call(["sh", "-c", before_all_prepared], env=env)
|
||||
|
||||
built_wheels: list[PurePosixPath] = []
|
||||
|
||||
for config in platform_configs:
|
||||
log.build_start(config.identifier)
|
||||
build_options = options.build_options(config.identifier)
|
||||
build_frontend = build_frontend_or_default(build_options.build_frontend)
|
||||
|
||||
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 = PurePath("/constraints.txt")
|
||||
|
||||
container.copy_into(constraints_file, container_constraints_file)
|
||||
dependency_constraint_flags = ["-c", container_constraints_file]
|
||||
|
||||
log.step("Setting up build environment...")
|
||||
|
||||
env = container.get_environment()
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
env["PIP_ROOT_USER_ACTION"] = "ignore"
|
||||
|
||||
# put this config's python top of the list
|
||||
python_bin = config.path / "bin"
|
||||
env["PATH"] = f'{python_bin}:{env["PATH"]}'
|
||||
|
||||
env = 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)
|
||||
|
||||
which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip()
|
||||
if PurePosixPath(which_pip) != python_bin / "pip":
|
||||
print(
|
||||
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
|
||||
if compatible_wheel:
|
||||
log.step_end()
|
||||
print(
|
||||
f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
|
||||
)
|
||||
repaired_wheels = [compatible_wheel]
|
||||
else:
|
||||
if build_options.before_build:
|
||||
log.step("Running before_build...")
|
||||
before_build_prepared = prepare_command(
|
||||
build_options.before_build,
|
||||
project=container_project_path,
|
||||
package=container_package_dir,
|
||||
)
|
||||
container.call(["sh", "-c", before_build_prepared], env=env)
|
||||
|
||||
log.step("Building wheel...")
|
||||
|
||||
temp_dir = PurePosixPath("/tmp/cibuildwheel")
|
||||
built_wheel_dir = temp_dir / "built_wheel"
|
||||
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)
|
||||
|
||||
if build_frontend == "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 == "build":
|
||||
if not 0 <= build_options.build_verbosity < 2:
|
||||
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
|
||||
log.warning(msg)
|
||||
container.call(
|
||||
[
|
||||
"python",
|
||||
"-m",
|
||||
"build",
|
||||
container_package_dir,
|
||||
"--wheel",
|
||||
f"--outdir={built_wheel_dir}",
|
||||
*extra_flags,
|
||||
],
|
||||
env=env,
|
||||
)
|
||||
else:
|
||||
assert_never(build_frontend)
|
||||
|
||||
built_wheel = container.glob(built_wheel_dir, "*.whl")[0]
|
||||
|
||||
repaired_wheel_dir = temp_dir / "repaired_wheel"
|
||||
container.call(["rm", "-rf", repaired_wheel_dir])
|
||||
container.call(["mkdir", "-p", repaired_wheel_dir])
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise NonPlatformWheelError()
|
||||
|
||||
if build_options.repair_command:
|
||||
log.step("Repairing wheel...")
|
||||
repair_command_prepared = prepare_command(
|
||||
build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
|
||||
)
|
||||
container.call(["sh", "-c", repair_command_prepared], env=env)
|
||||
else:
|
||||
container.call(["mv", built_wheel, repaired_wheel_dir])
|
||||
|
||||
repaired_wheels = container.glob(repaired_wheel_dir, "*.whl")
|
||||
|
||||
for repaired_wheel in repaired_wheels:
|
||||
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
|
||||
raise AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
|
||||
if build_options.test_command and build_options.test_selector(config.identifier):
|
||||
log.step("Testing wheel...")
|
||||
|
||||
# set up a virtual environment to install and test from, to make sure
|
||||
# there are no dependencies that were pulled in at build time.
|
||||
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)
|
||||
|
||||
virtualenv_env = env.copy()
|
||||
virtualenv_env["PATH"] = f"{venv_dir / 'bin'}:{virtualenv_env['PATH']}"
|
||||
|
||||
if build_options.before_test:
|
||||
before_test_prepared = prepare_command(
|
||||
build_options.before_test,
|
||||
project=container_project_path,
|
||||
package=container_package_dir,
|
||||
)
|
||||
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],
|
||||
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)
|
||||
|
||||
# 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,
|
||||
)
|
||||
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")
|
||||
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
log.build_end()
|
||||
|
||||
log.step("Copying wheels back to host...")
|
||||
# copy the output back into the host
|
||||
container.copy_out(container_output_dir, options.globals.output_dir)
|
||||
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)
|
||||
|
||||
python_configurations = get_python_configurations(
|
||||
options.globals.build_selector, options.globals.architectures
|
||||
)
|
||||
|
||||
cwd = Path.cwd()
|
||||
abs_package_dir = options.globals.package_dir.resolve()
|
||||
if cwd != abs_package_dir and cwd not in abs_package_dir.parents:
|
||||
msg = "package_dir must be inside the working directory"
|
||||
raise Exception(msg)
|
||||
|
||||
container_project_path = PurePosixPath("/project")
|
||||
container_package_dir = container_project_path / abs_package_dir.relative_to(cwd)
|
||||
|
||||
for build_step in get_build_steps(options, python_configurations):
|
||||
try:
|
||||
ids_to_build = [x.identifier for x in build_step.platform_configs]
|
||||
log.step(f"Starting container image {build_step.container_image}...")
|
||||
|
||||
print(f"info: This container will host the build for {', '.join(ids_to_build)}...")
|
||||
|
||||
with OCIContainer(
|
||||
image=build_step.container_image,
|
||||
simulate_32_bit=build_step.platform_tag.endswith("i686"),
|
||||
cwd=container_project_path,
|
||||
engine=options.globals.container_engine,
|
||||
) as container:
|
||||
build_in_container(
|
||||
options=options,
|
||||
platform_configs=build_step.platform_configs,
|
||||
container=container,
|
||||
container_project_path=container_project_path,
|
||||
container_package_dir=container_package_dir,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def _matches_prepared_command(error_cmd: Sequence[str], command_template: str) -> bool:
|
||||
if len(error_cmd) < 3 or error_cmd[0:2] != ["sh", "-c"]:
|
||||
return False
|
||||
command_prefix = command_template.split("{", maxsplit=1)[0].strip()
|
||||
return error_cmd[2].startswith(command_prefix)
|
||||
|
||||
|
||||
def troubleshoot(options: Options, error: Exception) -> None:
|
||||
if isinstance(error, subprocess.CalledProcessError) and (
|
||||
error.cmd[0:4] == ["python", "-m", "pip", "wheel"]
|
||||
or error.cmd[0:3] == ["python", "-m", "build"]
|
||||
or _matches_prepared_command(
|
||||
error.cmd, options.build_options(None).repair_command
|
||||
) # 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:
|
||||
print(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
NOTE: Shared object (.so) files found in this project.
|
||||
|
||||
These files might be built against the wrong OS, causing problems with
|
||||
auditwheel. If possible, run cibuildwheel in a clean checkout.
|
||||
|
||||
If you're using Cython and have previously done an in-place build,
|
||||
remove those build files (*.so and *.c) before starting cibuildwheel.
|
||||
|
||||
setuptools uses the build/ folder to store its build cache. It
|
||||
may be necessary to remove those build files (*.so and *.o) before
|
||||
starting cibuildwheel.
|
||||
|
||||
Files that belong to a virtual environment are probably not an issue
|
||||
unless you used a custom command telling cibuildwheel to activate it.
|
||||
"""
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
print(" Files detected:")
|
||||
print("\n".join(f" {f}" for f in so_files))
|
||||
print()
|
||||
+212
-56
@@ -1,15 +1,31 @@
|
||||
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 pathlib import Path
|
||||
|
||||
from .util import CIProvider, detect_ci_provider
|
||||
import humanize
|
||||
|
||||
from cibuildwheel.ci import CIProvider, detect_ci_provider, filter_ansi_codes
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
from typing import IO, AnyStr, Final, Literal
|
||||
|
||||
from cibuildwheel.options import Options
|
||||
|
||||
FoldPattern = tuple[str, str]
|
||||
|
||||
FoldPattern = Tuple[str, str]
|
||||
DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "")
|
||||
FOLD_PATTERNS: Final[dict[str, FoldPattern]] = {
|
||||
"azure": ("##[group]{name}", "##[endgroup]"),
|
||||
@@ -23,28 +39,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 +136,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 +171,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 +179,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 +201,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 +219,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 +297,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 +378,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 +404,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,627 +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,
|
||||
BuildFrontend,
|
||||
BuildSelector,
|
||||
NonPlatformWheelError,
|
||||
build_frontend_or_default,
|
||||
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: BuildFrontend,
|
||||
) -> 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_frontend_or_default(build_options.build_frontend)
|
||||
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,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
if build_frontend == "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 == "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)
|
||||
+232
-67
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
@@ -8,48 +9,170 @@ 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 pathlib import Path, PurePath, PurePosixPath
|
||||
from types import TracebackType
|
||||
from typing import IO, Dict, Literal
|
||||
from enum import Enum
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Literal, assert_never
|
||||
|
||||
from .typing import PathOrStr, PopenBytes
|
||||
from .util import CIProvider, detect_ci_provider, parse_key_value_string
|
||||
from cibuildwheel.ci import CIProvider, detect_ci_provider
|
||||
from cibuildwheel.errors import OCIEngineTooOldError
|
||||
from cibuildwheel.logger import log
|
||||
from cibuildwheel.util.cmd import call
|
||||
from cibuildwheel.util.helpers import FlexibleVersion, parse_key_value_string, strtobool
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping, Sequence
|
||||
from pathlib import Path, PurePath
|
||||
from types import TracebackType
|
||||
from typing import IO, Self
|
||||
|
||||
from cibuildwheel.typing import PathOrStr
|
||||
|
||||
ContainerEngineName = Literal["docker", "podman"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
# Order of the enum matters for tests. 386 shall appear before amd64.
|
||||
class OCIPlatform(Enum):
|
||||
i386 = "linux/386"
|
||||
AMD64 = "linux/amd64"
|
||||
ARMV7 = "linux/arm/v7"
|
||||
ARM64 = "linux/arm64"
|
||||
PPC64LE = "linux/ppc64le"
|
||||
RISCV64 = "linux/riscv64"
|
||||
S390X = "linux/s390x"
|
||||
|
||||
@classmethod
|
||||
def native(cls) -> Self:
|
||||
"""Return the current OCI platform, or raise ValueError if unknown."""
|
||||
arch = platform.machine().lower()
|
||||
mapping = {
|
||||
"i386": cls.i386,
|
||||
"i686": cls.i386,
|
||||
"x86_64": cls.AMD64,
|
||||
"amd64": cls.AMD64,
|
||||
"armv7l": cls.ARMV7,
|
||||
"aarch64": cls.ARM64,
|
||||
"arm64": cls.ARM64,
|
||||
"ppc64le": cls.PPC64LE,
|
||||
"riscv64": cls.RISCV64,
|
||||
"s390x": cls.S390X,
|
||||
}
|
||||
try:
|
||||
return mapping[arch]
|
||||
except KeyError as ex:
|
||||
msg = f"Unsupported platform architecture: {arch}"
|
||||
raise OSError(msg) from ex
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class OCIContainerEngineConfig:
|
||||
name: ContainerEngineName
|
||||
create_args: 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:
|
||||
config_dict = parse_key_value_string(config_string, ["name"])
|
||||
@classmethod
|
||||
def from_config_string(cls, config_string: str) -> Self:
|
||||
config_dict = parse_key_value_string(
|
||||
config_string,
|
||||
["name"],
|
||||
["create_args", "create-args", "disable_host_mount", "disable-host-mount"],
|
||||
)
|
||||
name = " ".join(config_dict["name"])
|
||||
if name not in {"docker", "podman"}:
|
||||
msg = f"unknown container engine {name}"
|
||||
raise ValueError(msg)
|
||||
|
||||
name = typing.cast(ContainerEngineName, name)
|
||||
# some flexibility in the option name to cope with TOML conventions
|
||||
name = typing.cast("ContainerEngineName", name)
|
||||
# some flexibility in the option names to cope with TOML conventions
|
||||
create_args = config_dict.get("create_args") or config_dict.get("create-args") or []
|
||||
return OCIContainerEngineConfig(name=name, create_args=create_args)
|
||||
disable_host_mount_options = (
|
||||
config_dict.get("disable_host_mount") or config_dict.get("disable-host-mount") or []
|
||||
)
|
||||
disable_host_mount = (
|
||||
strtobool(disable_host_mount_options[-1]) if disable_host_mount_options else False
|
||||
)
|
||||
if "--platform" in create_args or any(arg.startswith("--platform=") for arg in create_args):
|
||||
msg = "Using '--platform' in 'container-engine::create_args' is deprecated. It will be ignored."
|
||||
log.warning(msg)
|
||||
if "--platform" in create_args:
|
||||
index = create_args.index("--platform")
|
||||
create_args.pop(index)
|
||||
create_args.pop(index)
|
||||
else:
|
||||
create_args = [arg for arg in create_args if not arg.startswith("--platform=")]
|
||||
|
||||
return cls(name=name, create_args=tuple(create_args), disable_host_mount=disable_host_mount)
|
||||
|
||||
def options_summary(self) -> str | dict[str, str]:
|
||||
if not self.create_args:
|
||||
return self.name
|
||||
else:
|
||||
return {"name": self.name, "create_args": repr(self.create_args)}
|
||||
return {
|
||||
"name": self.name,
|
||||
"create_args": repr(self.create_args),
|
||||
"disable_host_mount": str(self.disable_host_mount),
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_ENGINE = OCIContainerEngineConfig("docker")
|
||||
|
||||
|
||||
def _check_engine_version(engine: OCIContainerEngineConfig) -> None:
|
||||
try:
|
||||
version_string = call(engine.name, "version", "-f", "{{json .}}", capture_stdout=True)
|
||||
# We are using lowercase keys for all dicts so we are not affected by casing of keys
|
||||
version_info = json.loads(
|
||||
version_string.strip(), object_pairs_hook=lambda inp: {k.lower(): v for k, v in inp}
|
||||
)
|
||||
match engine.name:
|
||||
case "docker":
|
||||
client_api_version = FlexibleVersion(version_info["client"]["apiversion"])
|
||||
server_api_version = FlexibleVersion(version_info["server"]["apiversion"])
|
||||
# --platform support was introduced in 1.32 as experimental, 1.41 removed the experimental flag
|
||||
version = min(client_api_version, server_api_version)
|
||||
minimum_version = FlexibleVersion("1.41")
|
||||
minimum_version_str = "20.10.0" # docker version
|
||||
error_msg = textwrap.dedent(
|
||||
f"""
|
||||
Build failed because {engine.name} is too old.
|
||||
|
||||
cibuildwheel requires {engine.name}>={minimum_version_str} running API version {minimum_version}.
|
||||
The API version found by cibuildwheel is {version}.
|
||||
"""
|
||||
)
|
||||
case "podman":
|
||||
# podman uses the same version string for "Version" & "ApiVersion"
|
||||
client_version = FlexibleVersion(version_info["client"]["version"])
|
||||
if "server" in version_info:
|
||||
server_version = FlexibleVersion(version_info["server"]["version"])
|
||||
else:
|
||||
server_version = client_version
|
||||
# --platform support was introduced in v3
|
||||
version = min(client_version, server_version)
|
||||
minimum_version = FlexibleVersion("3")
|
||||
error_msg = textwrap.dedent(
|
||||
f"""
|
||||
Build failed because {engine.name} is too old.
|
||||
|
||||
cibuildwheel requires {engine.name}>={minimum_version}.
|
||||
The version found by cibuildwheel is {version}.
|
||||
"""
|
||||
)
|
||||
case _:
|
||||
assert_never(engine.name)
|
||||
|
||||
if version < minimum_version:
|
||||
raise OCIEngineTooOldError(error_msg) from None
|
||||
|
||||
except (subprocess.CalledProcessError, KeyError, ValueError) as e:
|
||||
msg = f"Build failed because {engine.name} is too old or is not working properly."
|
||||
raise OCIEngineTooOldError(msg) from e
|
||||
|
||||
|
||||
class OCIContainer:
|
||||
"""
|
||||
An object that represents a running OCI (e.g. Docker) container.
|
||||
@@ -62,20 +185,28 @@ class OCIContainer:
|
||||
back to cibuildwheel.
|
||||
|
||||
Example:
|
||||
>>> # xdoctest: +REQUIRES(LINUX)
|
||||
>>> from cibuildwheel.oci_container import * # NOQA
|
||||
>>> from cibuildwheel.options import _get_pinned_container_images
|
||||
>>> import pytest
|
||||
>>> try:
|
||||
... oci_platform = OCIPlatform.native()
|
||||
... except OSError as ex:
|
||||
... pytest.skip(str(ex))
|
||||
>>> if oci_platform != OCIPlatform.AMD64:
|
||||
... pytest.skip('only runs on amd64')
|
||||
>>> image = _get_pinned_container_images()['x86_64']['manylinux2014']
|
||||
>>> # Test the default container
|
||||
>>> with OCIContainer(image=image) as self:
|
||||
>>> with OCIContainer(image=image, oci_platform=oci_platform) as self:
|
||||
... self.call(["echo", "hello world"])
|
||||
... self.call(["cat", "/proc/1/cgroup"])
|
||||
... print(self.get_environment())
|
||||
... print(self.debug_info())
|
||||
"""
|
||||
|
||||
UTILITY_PYTHON = "/opt/python/cp38-cp38/bin/python"
|
||||
UTILITY_PYTHON = "/opt/python/cp39-cp39/bin/python"
|
||||
|
||||
process: PopenBytes
|
||||
process: subprocess.Popen[bytes]
|
||||
bash_stdin: IO[bytes]
|
||||
bash_stdout: IO[bytes]
|
||||
|
||||
@@ -83,7 +214,7 @@ class OCIContainer:
|
||||
self,
|
||||
*,
|
||||
image: str,
|
||||
simulate_32_bit: bool = False,
|
||||
oci_platform: OCIPlatform,
|
||||
cwd: PathOrStr | None = None,
|
||||
engine: OCIContainerEngineConfig = DEFAULT_ENGINE,
|
||||
):
|
||||
@@ -92,14 +223,48 @@ class OCIContainer:
|
||||
raise ValueError(msg)
|
||||
|
||||
self.image = image
|
||||
self.simulate_32_bit = simulate_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 __enter__(self) -> OCIContainer:
|
||||
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
|
||||
@@ -108,17 +273,44 @@ class OCIContainer:
|
||||
if detect_ci_provider() == CIProvider.travis_ci and platform.machine() == "ppc64le":
|
||||
network_args = ["--network=host"]
|
||||
|
||||
shell_args = ["linux32", "/bin/bash"] if self.simulate_32_bit else ["/bin/bash"]
|
||||
platform_args = self._get_platform_args()
|
||||
|
||||
simulate_32_bit = False
|
||||
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
|
||||
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"]
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
self.engine.name,
|
||||
"create",
|
||||
"--env=CIBUILDWHEEL",
|
||||
"--env=SOURCE_DATE_EPOCH",
|
||||
f"--name={self.name}",
|
||||
"--interactive",
|
||||
"--volume=/:/host", # ignored on CircleCI
|
||||
*(["--volume=/:/host"] if not self.engine.disable_host_mount else []),
|
||||
*network_args,
|
||||
*platform_args,
|
||||
*self.engine.create_args,
|
||||
self.image,
|
||||
*shell_args,
|
||||
@@ -175,29 +367,27 @@ class OCIContainer:
|
||||
|
||||
assert isinstance(self.name, str)
|
||||
|
||||
subprocess.run(
|
||||
[self.engine.name, "rm", "--force", "-v", self.name],
|
||||
stdout=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
self.name = None
|
||||
keep_container = strtobool(os.environ.get("CIBW_DEBUG_KEEP_CONTAINER", ""))
|
||||
if not keep_container:
|
||||
subprocess.run(
|
||||
[self.engine.name, "rm", "--force", "-v", self.name],
|
||||
stdout=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
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,
|
||||
@@ -212,8 +402,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()
|
||||
@@ -226,30 +415,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)
|
||||
@@ -360,20 +526,19 @@ 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
|
||||
return self.call(command, env=environment, capture_output=True)
|
||||
|
||||
def debug_info(self) -> str:
|
||||
command = [self.engine.name, "info"]
|
||||
if self.engine.name == "podman":
|
||||
command = f"{self.engine.name} info --debug"
|
||||
else:
|
||||
command = f"{self.engine.name} info"
|
||||
command.append("--debug")
|
||||
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
check=True,
|
||||
cwd=self.cwd,
|
||||
stdin=subprocess.PIPE,
|
||||
|
||||
+568
-238
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Protocol
|
||||
|
||||
from cibuildwheel import errors
|
||||
from cibuildwheel.platforms import android, ios, linux, macos, pyodide, windows
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from cibuildwheel.architecture import Architecture
|
||||
from cibuildwheel.options import Options
|
||||
from cibuildwheel.selector import BuildSelector
|
||||
from cibuildwheel.typing import GenericPythonConfiguration, PlatformName
|
||||
|
||||
|
||||
class PlatformModule(Protocol):
|
||||
# note that as per PEP544, the self argument is ignored when the protocol
|
||||
# is applied to a module
|
||||
def all_python_configurations(self) -> Sequence[GenericPythonConfiguration]: ...
|
||||
|
||||
def get_python_configurations(
|
||||
self, build_selector: BuildSelector, architectures: set[Architecture]
|
||||
) -> Sequence[GenericPythonConfiguration]: ...
|
||||
|
||||
def build(self, options: Options, tmp_path: Path) -> None: ...
|
||||
|
||||
|
||||
ALL_PLATFORM_MODULES: Final[dict[PlatformName, PlatformModule]] = {
|
||||
"linux": linux,
|
||||
"windows": windows,
|
||||
"macos": macos,
|
||||
"pyodide": pyodide,
|
||||
"android": android,
|
||||
"ios": ios,
|
||||
}
|
||||
|
||||
|
||||
def native_platform() -> PlatformName:
|
||||
if sys.platform.startswith("linux"):
|
||||
return "linux"
|
||||
elif sys.platform == "darwin":
|
||||
return "macos"
|
||||
elif sys.platform == "win32":
|
||||
return "windows"
|
||||
else:
|
||||
msg = (
|
||||
'Unable to detect platform from "sys.platform". cibuildwheel doesn\'t '
|
||||
"support building wheels for this platform. You might be able to build for a different "
|
||||
"platform using the --platform argument. Check --help output for more information."
|
||||
)
|
||||
raise errors.ConfigurationError(msg)
|
||||
|
||||
|
||||
def get_build_identifiers(
|
||||
platform_module: PlatformModule,
|
||||
build_selector: BuildSelector,
|
||||
architectures: set[Architecture],
|
||||
) -> list[str]:
|
||||
python_configurations = platform_module.get_python_configurations(build_selector, architectures)
|
||||
return [config.identifier for config in python_configurations]
|
||||
@@ -0,0 +1,785 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sysconfig
|
||||
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 cibuildwheel import errors, platforms # pylint: disable=cyclic-import
|
||||
from cibuildwheel.architecture import Architecture, arch_synonym
|
||||
from cibuildwheel.audit import run_audit
|
||||
from cibuildwheel.frontend import (
|
||||
get_build_frontend_extra_flags,
|
||||
parse_config_settings,
|
||||
prepare_config_settings,
|
||||
)
|
||||
from cibuildwheel.logger import log
|
||||
from cibuildwheel.util import resources
|
||||
from cibuildwheel.util.cmd import call, shell
|
||||
from cibuildwheel.util.file import CIBW_CACHE_PATH, copy_test_sources, download, move_file
|
||||
from cibuildwheel.util.helpers import prepare_command
|
||||
from cibuildwheel.util.packaging import find_compatible_wheel
|
||||
from cibuildwheel.util.python_build_standalone import create_python_build_standalone_environment
|
||||
from cibuildwheel.venv import constraint_flags, find_uv, virtualenv
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Iterator, MutableMapping
|
||||
|
||||
from cibuildwheel.options import BuildOptions, Options
|
||||
from cibuildwheel.selector import BuildSelector
|
||||
|
||||
RESOURCES_ANDROID = resources.PATH / "android"
|
||||
ANDROID_TRIPLET = {
|
||||
"arm64_v8a": "aarch64-linux-android",
|
||||
"x86_64": "x86_64-linux-android",
|
||||
}
|
||||
|
||||
|
||||
def parse_identifier(identifier: str) -> tuple[str, str]:
|
||||
match = re.fullmatch(r"cp(\d)(\d+)-android_(.+)", identifier)
|
||||
if not match:
|
||||
msg = f"invalid Android identifier: '{identifier}'"
|
||||
raise ValueError(msg)
|
||||
major, minor, arch = match.groups()
|
||||
return (f"{major}.{minor}", arch)
|
||||
|
||||
|
||||
def android_triplet(identifier: str) -> str:
|
||||
return ANDROID_TRIPLET[parse_identifier(identifier)[1]]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PythonConfiguration:
|
||||
version: str
|
||||
identifier: str
|
||||
url: str
|
||||
sha256: 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)
|
||||
run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel)
|
||||
|
||||
test_wheel(state, repaired_wheel, build_frontend=build_options.build_frontend.name)
|
||||
|
||||
output_wheel: Path | None = None
|
||||
if compatible_wheel is None:
|
||||
output_wheel = move_file(
|
||||
repaired_wheel, build_options.output_dir / repaired_wheel.name
|
||||
)
|
||||
built_wheels.append(output_wheel)
|
||||
|
||||
shutil.rmtree(build_path)
|
||||
log.build_end(output_wheel)
|
||||
|
||||
except subprocess.CalledProcessError as error:
|
||||
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
||||
raise errors.FatalError(msg) from error
|
||||
|
||||
|
||||
def setup_target_python(config: PythonConfiguration, build_path: Path) -> Path:
|
||||
log.step("Installing target Python...")
|
||||
python_tgz = CIBW_CACHE_PATH / config.url.rpartition("/")[-1]
|
||||
with FileLock(f"{python_tgz}.lock"):
|
||||
if not python_tgz.exists():
|
||||
download(config.url, python_tgz, sha256=config.sha256 or None)
|
||||
|
||||
python_dir = build_path / "python"
|
||||
python_dir.mkdir()
|
||||
shutil.unpack_archive(python_tgz, python_dir)
|
||||
|
||||
# Patch a testbed bug. This code and the patch file can both be removed once we've
|
||||
# updated to Python versions that include the fix.
|
||||
call("patch", "-p1", "-i", RESOURCES_ANDROID / "android.patch", cwd=python_dir)
|
||||
|
||||
return python_dir
|
||||
|
||||
|
||||
def setup_env(
|
||||
config: PythonConfiguration, build_options: BuildOptions, build_path: Path, python_dir: Path
|
||||
) -> tuple[dict[str, str], dict[str, str]]:
|
||||
"""
|
||||
Returns two environment dicts, both pointing at the same virtual environment:
|
||||
|
||||
* build_env, which uses the environment normally.
|
||||
* android_env, which uses the environment while simulating running on Android.
|
||||
"""
|
||||
log.step("Setting up build environment...")
|
||||
build_frontend = build_options.build_frontend.name
|
||||
use_uv = build_frontend in {"build[uv]", "uv"}
|
||||
uv_path = find_uv()
|
||||
if use_uv and uv_path is None:
|
||||
msg = "uv not found"
|
||||
raise AssertionError(msg)
|
||||
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
|
||||
|
||||
# Create virtual environment
|
||||
python_exe = create_python_build_standalone_environment(
|
||||
config.version, build_path, CIBW_CACHE_PATH
|
||||
)
|
||||
venv_dir = build_path / "venv"
|
||||
dependency_constraint = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version, tmp_dir=build_path
|
||||
)
|
||||
build_env = virtualenv(
|
||||
config.version, python_exe, venv_dir, dependency_constraint, use_uv=use_uv
|
||||
)
|
||||
create_cmake_toolchain(config, build_path, python_dir, build_env)
|
||||
|
||||
# Apply custom environment variables, and check environment is still valid
|
||||
build_env = build_options.environment.as_dictionary(build_env)
|
||||
build_env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
for command in ["python"] if use_uv else ["python", "pip"]:
|
||||
command_path = call("which", command, env=build_env, capture_stdout=True).strip()
|
||||
if command_path != f"{venv_dir}/bin/{command}":
|
||||
msg = (
|
||||
f"{command} available on PATH doesn't match our installed instance. If you "
|
||||
f"have modified PATH, ensure that you don't overwrite cibuildwheel's entry "
|
||||
f"or insert {command} above it."
|
||||
)
|
||||
raise errors.FatalError(msg)
|
||||
if command == "python":
|
||||
call(command, "-V", "-V", env=build_env)
|
||||
else:
|
||||
call(command, "--version", env=build_env)
|
||||
|
||||
# Construct an altered environment which simulates running on Android.
|
||||
android_env = setup_android_env(config, python_dir, venv_dir, build_env)
|
||||
|
||||
# Install build tools
|
||||
if build_frontend not in {"build", "build[uv]", "uv"}:
|
||||
msg = "Android requires the build frontend to be 'build' or 'uv'"
|
||||
raise errors.FatalError(msg)
|
||||
call(*pip, "install", "build", *constraint_flags(dependency_constraint), env=build_env)
|
||||
|
||||
# Build-time requirements must be queried within android_env, because
|
||||
# `get_requires_for_build` can run arbitrary code in setup.py scripts, which may be
|
||||
# affected by the target platform. However, the requirements must be installed
|
||||
# within build_env, because they're going to run on the build machine.
|
||||
#
|
||||
# The `build` CLI doesn't support this combination, so we use its API to query the
|
||||
# requirements, and then install them ourselves with pip. We'll later run `build` in
|
||||
# the same environment, passing the `--no-isolation` option.
|
||||
class AndroidEnv(IsolatedEnv):
|
||||
@property
|
||||
def python_executable(self) -> str:
|
||||
return f"{venv_dir}/bin/python"
|
||||
|
||||
def make_extra_environ(self) -> dict[str, str]:
|
||||
return android_env
|
||||
|
||||
pb = ProjectBuilder.from_isolated_env(AndroidEnv(), build_options.package_dir)
|
||||
if pb.build_system_requires:
|
||||
call(*pip, "install", *pb.build_system_requires, env=build_env)
|
||||
|
||||
requires_for_build = pb.get_requires_for_build(
|
||||
"wheel", parse_config_settings(build_options.config_settings)
|
||||
)
|
||||
if requires_for_build:
|
||||
call(*pip, "install", *requires_for_build, env=build_env)
|
||||
|
||||
return build_env, android_env
|
||||
|
||||
|
||||
def create_cmake_toolchain(
|
||||
config: PythonConfiguration, build_path: Path, python_dir: Path, build_env: dict[str, str]
|
||||
) -> None:
|
||||
toolchain_path = build_path / "toolchain.cmake"
|
||||
build_env["CMAKE_TOOLCHAIN_FILE"] = str(toolchain_path)
|
||||
with open(toolchain_path, "w", encoding="UTF-8") as toolchain_file:
|
||||
print(
|
||||
dedent(
|
||||
f"""\
|
||||
# To support as many build systems as possible, we use environment
|
||||
# variables as the single source of truth for compiler flags and paths,
|
||||
# so they don't need to be specified here.
|
||||
|
||||
set(CMAKE_SYSTEM_NAME Android)
|
||||
set(CMAKE_SYSTEM_PROCESSOR {android_triplet(config.identifier).split("-")[0]})
|
||||
|
||||
# Inhibit all of CMake's own NDK handling code.
|
||||
set(CMAKE_SYSTEM_VERSION 1)
|
||||
|
||||
# Tell CMake where to look for headers and libraries.
|
||||
set(CMAKE_FIND_ROOT_PATH "{python_dir}/prefix")
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
|
||||
|
||||
# Allow CMake to run Python in the simulated Android environment when
|
||||
# policy CMP0190 is active.
|
||||
set(CMAKE_CROSSCOMPILING_EMULATOR /bin/sh -c [["$0" "$@"]])
|
||||
"""
|
||||
),
|
||||
file=toolchain_file,
|
||||
)
|
||||
|
||||
|
||||
def localize_sysconfigdata(
|
||||
python_dir: Path, build_env: dict[str, str], sysconfigdata_path: Path
|
||||
) -> dict[str, Any]:
|
||||
sysconfigdata: dict[str, Any] = run_path(str(sysconfigdata_path))["build_time_vars"]
|
||||
with sysconfigdata_path.open("w", encoding="UTF-8") as f:
|
||||
f.write("# Generated by cibuildwheel\n")
|
||||
f.write("build_time_vars = ")
|
||||
sysconfigdata = localized_vars(build_env, sysconfigdata, python_dir / "prefix")
|
||||
pprint(sysconfigdata, stream=f, compact=True)
|
||||
return sysconfigdata
|
||||
|
||||
|
||||
def localized_vars(
|
||||
build_env: dict[str, str], orig_vars: dict[str, Any], prefix: Path
|
||||
) -> dict[str, Any]:
|
||||
orig_prefix = orig_vars["prefix"]
|
||||
localized_vars_ = {}
|
||||
for key, value in orig_vars.items():
|
||||
# The host's sysconfigdata will include references to build-time paths.
|
||||
# Update these to refer to the current prefix.
|
||||
final = value
|
||||
if isinstance(final, str):
|
||||
final = final.replace(orig_prefix, str(prefix))
|
||||
|
||||
if key == "ANDROID_API_LEVEL":
|
||||
if api_level := build_env.get(key):
|
||||
final = int(api_level)
|
||||
|
||||
# Build systems vary in whether FLAGS variables are read from sysconfig, and if so,
|
||||
# whether they're replaced by environment variables or combined with them. Even
|
||||
# setuptools has changed its behavior here
|
||||
# (https://github.com/pypa/setuptools/issues/4836).
|
||||
#
|
||||
# Ensure consistency by clearing the sysconfig variables and letting the environment
|
||||
# variables take effect alone. This will also work for any non-Python build systems
|
||||
# which the build script may call.
|
||||
elif key in ["CFLAGS", "CXXFLAGS", "LDFLAGS"]:
|
||||
final = ""
|
||||
|
||||
# These variables contain an embedded copy of LDFLAGS.
|
||||
elif key in ["LDSHARED", "LDCXXSHARED"]:
|
||||
final = final.removesuffix(" " + orig_vars["LDFLAGS"])
|
||||
|
||||
localized_vars_[key] = final
|
||||
|
||||
return localized_vars_
|
||||
|
||||
|
||||
def setup_android_env(
|
||||
config: PythonConfiguration, python_dir: Path, venv_dir: Path, build_env: dict[str, str]
|
||||
) -> dict[str, str]:
|
||||
site_packages = next(venv_dir.glob("lib/python*/site-packages"))
|
||||
for suffix in ["pth", "py"]:
|
||||
shutil.copy(RESOURCES_ANDROID / f"_cross_venv.{suffix}", site_packages)
|
||||
|
||||
sysconfigdata_path = Path(
|
||||
shutil.copy(
|
||||
next(python_dir.glob("prefix/lib/python*/_sysconfigdata_*.py")),
|
||||
site_packages,
|
||||
)
|
||||
)
|
||||
sysconfigdata = localize_sysconfigdata(python_dir, build_env, sysconfigdata_path)
|
||||
|
||||
# Activate the code in _cross_venv.py.
|
||||
android_env = build_env.copy()
|
||||
android_env["CIBW_HOST_TRIPLET"] = android_triplet(config.identifier)
|
||||
|
||||
# Get the environment variables needed to build for Android (CC, CFLAGS, etc). These are
|
||||
# generated by https://github.com/python/cpython/blob/main/Android/android-env.sh.
|
||||
env_output = call(python_dir / "android.py", "env", env=build_env, capture_stdout=True)
|
||||
|
||||
# shlex.split should produce a sequence alternating between:
|
||||
# * the word "export"
|
||||
# * a key=value string, without quotes
|
||||
for i, token in enumerate(shlex.split(env_output)):
|
||||
if i % 2 == 0:
|
||||
assert token == "export", token
|
||||
else:
|
||||
key, sep, value = token.partition("=")
|
||||
assert sep == "=", token
|
||||
android_env[key] = value
|
||||
|
||||
# localized_vars cleared the CFLAGS and CXXFLAGS in the sysconfigdata, but most
|
||||
# packages take their optimization flags from these variables. Pass these flags via
|
||||
# environment variables instead.
|
||||
#
|
||||
# We don't enable debug information, because it significantly increases binary size,
|
||||
# and most Android app developers don't have the NDK installed, so they would have no
|
||||
# way to strip it.
|
||||
opt = " ".join(word for word in sysconfigdata["OPT"].split() if not word.startswith("-g"))
|
||||
for key in ["CFLAGS", "CXXFLAGS"]:
|
||||
android_env[key] += " " + opt
|
||||
|
||||
# Cargo target linker needs to be specified after CC is set
|
||||
setup_rust(config, python_dir, android_env)
|
||||
|
||||
# Format the environment so it can be pasted into a shell when debugging.
|
||||
for key, value in sorted(android_env.items()):
|
||||
if os.environ.get(key) != value:
|
||||
print(f"export {key}={shlex.quote(value)}")
|
||||
|
||||
return android_env
|
||||
|
||||
|
||||
def setup_rust(
|
||||
config: PythonConfiguration,
|
||||
python_dir: Path,
|
||||
env: MutableMapping[str, str],
|
||||
) -> None:
|
||||
cargo_target = android_triplet(config.identifier)
|
||||
|
||||
# CARGO_BUILD_TARGET is the variable used by Cargo and setuptools_rust
|
||||
env["CARGO_BUILD_TARGET"] = cargo_target
|
||||
|
||||
# The linker needs to be specified after CC is set by android-env.sh
|
||||
cargo_target_linker_env_name = f"CARGO_TARGET_{cargo_target.upper().replace('-', '_')}_LINKER"
|
||||
# CC has already been set by calling android.py (it calls android-env.sh)
|
||||
env[cargo_target_linker_env_name] = env["CC"]
|
||||
|
||||
# All Python extension modules must be explicitly linked against libpython3.x.so when building for Android.
|
||||
# See: https://peps.python.org/pep-0738/#linkage
|
||||
# For projects using PyO3, this requires setting PYO3_CROSS_LIB_DIR to the directory containing libpython3.x.so.
|
||||
# See: https://pyo3.rs/v0.27.1/building-and-distribution.html#cross-compiling
|
||||
env["PYO3_CROSS_LIB_DIR"] = str(python_dir / "prefix" / "lib")
|
||||
|
||||
venv_bin = Path(env["VIRTUAL_ENV"]) / "bin"
|
||||
for tool in ["cargo", "rustup"]:
|
||||
shim_path = venv_bin / tool
|
||||
shutil.copy(RESOURCES_ANDROID / "rust_shim.py", shim_path)
|
||||
shim_path.chmod(0o755)
|
||||
|
||||
|
||||
def before_build(state: BuildState) -> None:
|
||||
if state.options.before_build:
|
||||
log.step("Running before_build...")
|
||||
shell_prepared(
|
||||
state.options.before_build,
|
||||
build_options=state.options,
|
||||
env=state.build_env,
|
||||
)
|
||||
|
||||
|
||||
def build_wheel(state: BuildState) -> Path:
|
||||
log.step("Building wheel...")
|
||||
built_wheel_dir = state.build_path / "built_wheel"
|
||||
match state.options.build_frontend.name:
|
||||
case "build" | "build[uv]":
|
||||
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,
|
||||
prepare_config_settings(
|
||||
state.options.config_settings,
|
||||
project=".",
|
||||
package=state.options.package_dir,
|
||||
),
|
||||
),
|
||||
env=state.android_env,
|
||||
)
|
||||
case "uv":
|
||||
uv_path = find_uv()
|
||||
assert uv_path is not None
|
||||
call(
|
||||
uv_path,
|
||||
"build",
|
||||
state.options.package_dir,
|
||||
"--wheel",
|
||||
"--no-build-isolation",
|
||||
f"--out-dir={built_wheel_dir}",
|
||||
*get_build_frontend_extra_flags(
|
||||
state.options.build_frontend,
|
||||
state.options.build_verbosity,
|
||||
prepare_config_settings(
|
||||
state.options.config_settings,
|
||||
project=".",
|
||||
package=state.options.package_dir,
|
||||
),
|
||||
),
|
||||
env=state.android_env,
|
||||
)
|
||||
case x:
|
||||
msg = f"Invalid build backend {x!r}"
|
||||
raise AssertionError(msg)
|
||||
|
||||
built_wheels = list(built_wheel_dir.glob("*.whl"))
|
||||
if len(built_wheels) != 1:
|
||||
msg = f"{built_wheel_dir} contains {len(built_wheels)} wheels; expected 1"
|
||||
raise errors.FatalError(msg)
|
||||
built_wheel = built_wheels[0]
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise errors.NonPlatformWheelError()
|
||||
return built_wheel
|
||||
|
||||
|
||||
def repair_wheel(state: BuildState, built_wheel: Path) -> Path:
|
||||
log.step("Repairing wheel...")
|
||||
repaired_wheel_dir = state.build_path / "repaired_wheel"
|
||||
repaired_wheel_dir.mkdir()
|
||||
|
||||
if state.options.repair_command:
|
||||
shell(
|
||||
prepare_command(
|
||||
state.options.repair_command,
|
||||
wheel=built_wheel,
|
||||
dest_dir=repaired_wheel_dir,
|
||||
package=state.options.package_dir,
|
||||
project=".",
|
||||
),
|
||||
env=state.build_env,
|
||||
)
|
||||
else:
|
||||
repair_default(state.android_env, built_wheel, repaired_wheel_dir)
|
||||
|
||||
repaired_wheels = list(repaired_wheel_dir.glob("*.whl"))
|
||||
if len(repaired_wheels) == 0:
|
||||
raise errors.RepairStepProducedNoWheelError()
|
||||
if len(repaired_wheels) != 1:
|
||||
msg = f"{repaired_wheel_dir} contains {len(repaired_wheels)} wheels; expected 1"
|
||||
raise errors.FatalError(msg)
|
||||
repaired_wheel = repaired_wheels[0]
|
||||
|
||||
if repaired_wheel.name.endswith("none-any.whl"):
|
||||
raise errors.NonPlatformWheelError()
|
||||
return repaired_wheel
|
||||
|
||||
|
||||
def repair_default(
|
||||
android_env: dict[str, str], built_wheel: Path, repaired_wheel_dir: Path
|
||||
) -> None:
|
||||
"""
|
||||
Adds libc++ to the wheel if anything links against it. In the future this should be
|
||||
moved to auditwheel and generalized to support more libraries.
|
||||
"""
|
||||
if (match := re.search(r"^(.+?)-", built_wheel.name)) is None:
|
||||
msg = f"Failed to parse wheel filename: {built_wheel.name}"
|
||||
raise errors.FatalError(msg)
|
||||
wheel_name = match[1]
|
||||
|
||||
unpacked_dir = repaired_wheel_dir / "unpacked"
|
||||
unpacked_dir.mkdir()
|
||||
shutil.unpack_archive(built_wheel, unpacked_dir, format="zip")
|
||||
|
||||
# Some build systems are inconsistent about name normalization, so don't assume the
|
||||
# dist-info name is identical to the wheel name.
|
||||
record_paths = list(unpacked_dir.glob("*.dist-info/RECORD"))
|
||||
if len(record_paths) != 1:
|
||||
msg = f"{built_wheel.name} contains {len(record_paths)} dist-info/RECORD files; expected 1"
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
old_soname = "libc++_shared.so"
|
||||
paths_to_patch = []
|
||||
for path, elffile in elf_file_filter(
|
||||
unpacked_dir / filename
|
||||
for filename, *_ in csv.reader(record_paths[0].read_text().splitlines())
|
||||
):
|
||||
if (dynamic := elffile.get_section_by_name(".dynamic")) and any( # type: ignore[no-untyped-call]
|
||||
tag.entry.d_tag == "DT_NEEDED" and tag.needed == old_soname
|
||||
for tag in dynamic.iter_tags()
|
||||
):
|
||||
paths_to_patch.append(path)
|
||||
|
||||
if not paths_to_patch:
|
||||
shutil.copyfile(built_wheel, repaired_wheel_dir / built_wheel.name)
|
||||
else:
|
||||
# Android doesn't support DT_RPATH, but supports DT_RUNPATH since API level 24
|
||||
# (https://github.com/aosp-mirror/platform_bionic/blob/master/android-changes-for-ndk-developers.md).
|
||||
if int(sysconfig_print('get_config_vars()["ANDROID_API_LEVEL"]', android_env)) < 24:
|
||||
msg = f"Adding {old_soname} requires ANDROID_API_LEVEL to be at least 24"
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
toolchain = Path(android_env["CC"]).parent.parent
|
||||
src_path = toolchain / f"sysroot/usr/lib/{android_env['CIBW_HOST_TRIPLET']}/{old_soname}"
|
||||
|
||||
# Use the same library location as auditwheel would.
|
||||
libs_dir = unpacked_dir / (wheel_name + ".libs")
|
||||
libs_dir.mkdir()
|
||||
new_soname = soname_with_hash(src_path)
|
||||
dst_path = libs_dir / new_soname
|
||||
shutil.copyfile(src_path, dst_path)
|
||||
call(which("patchelf"), "--set-soname", new_soname, dst_path)
|
||||
|
||||
for path in paths_to_patch:
|
||||
call(which("patchelf"), "--replace-needed", old_soname, new_soname, path)
|
||||
call(
|
||||
which("patchelf"),
|
||||
"--set-rpath",
|
||||
f"${{ORIGIN}}/{relpath(libs_dir, path.parent)}",
|
||||
path,
|
||||
)
|
||||
call(which("wheel"), "pack", unpacked_dir, "-d", repaired_wheel_dir)
|
||||
|
||||
|
||||
# If cibuildwheel was called without activating its environment, its scripts directory
|
||||
# will not be on the PATH.
|
||||
def which(cmd: str) -> str:
|
||||
scripts_dir = sysconfig.get_path("scripts")
|
||||
result = shutil.which(cmd, path=scripts_dir + os.pathsep + os.environ["PATH"])
|
||||
if result is None:
|
||||
msg = f"Couldn't find {cmd!r} in {scripts_dir} or on the PATH"
|
||||
raise errors.FatalError(msg)
|
||||
return result
|
||||
|
||||
|
||||
def elf_file_filter(paths: Iterable[Path]) -> Iterator[tuple[Path, ELFFile]]:
|
||||
"""Filter through an iterator of filenames and load up only ELF files"""
|
||||
for path in paths:
|
||||
if not path.name.endswith(".py"):
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
candidate = ELFFile(f) # type: ignore[no-untyped-call]
|
||||
yield path, candidate
|
||||
except ELFError:
|
||||
pass # Not an ELF file
|
||||
|
||||
|
||||
def soname_with_hash(src_path: Path) -> str:
|
||||
"""Return the same library filename as auditwheel would"""
|
||||
shorthash = hashlib.sha256(src_path.read_bytes()).hexdigest()[:8]
|
||||
src_name = src_path.name
|
||||
base, ext = src_name.split(".", 1)
|
||||
if not base.endswith(f"-{shorthash}"):
|
||||
return f"{base}-{shorthash}.{ext}"
|
||||
else:
|
||||
return src_name
|
||||
|
||||
|
||||
def test_wheel(state: BuildState, wheel: Path, *, build_frontend: str) -> None:
|
||||
test_command = state.options.test_command
|
||||
if not (test_command and state.options.test_selector(state.config.identifier)):
|
||||
return
|
||||
|
||||
log.step("Testing wheel...")
|
||||
use_uv = build_frontend in {"build[uv]", "uv"}
|
||||
uv_path = find_uv()
|
||||
if use_uv and uv_path is None:
|
||||
msg = "uv not found"
|
||||
raise AssertionError(msg)
|
||||
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
|
||||
|
||||
native_arch = arch_synonym(platform.machine(), platforms.native_platform(), "android")
|
||||
if state.config.arch != native_arch:
|
||||
log.warning(
|
||||
f"Skipping tests for {state.config.arch}, as the build machine only "
|
||||
f"supports {native_arch}"
|
||||
)
|
||||
return
|
||||
|
||||
if state.options.before_test:
|
||||
shell_prepared(
|
||||
state.options.before_test,
|
||||
build_options=state.options,
|
||||
env=state.build_env,
|
||||
)
|
||||
|
||||
platform_args = (
|
||||
["--python-platform", android_triplet(state.config.identifier)]
|
||||
if use_uv
|
||||
else [
|
||||
"--platform",
|
||||
sysconfig_print("get_platform()", state.android_env).replace("-", "_"),
|
||||
]
|
||||
)
|
||||
|
||||
# Install the wheel and test-requires.
|
||||
site_packages_dir = state.build_path / "site-packages"
|
||||
site_packages_dir.mkdir()
|
||||
call(
|
||||
*pip,
|
||||
"install",
|
||||
"--only-binary=:all:",
|
||||
*platform_args,
|
||||
"--target",
|
||||
site_packages_dir,
|
||||
f"{wheel}{state.options.test_extras}",
|
||||
*state.options.test_requires,
|
||||
env=state.android_env,
|
||||
)
|
||||
|
||||
# Copy test-sources.
|
||||
cwd_dir = state.build_path / "cwd"
|
||||
cwd_dir.mkdir()
|
||||
if state.options.test_sources:
|
||||
copy_test_sources(state.options.test_sources, Path.cwd(), cwd_dir)
|
||||
else:
|
||||
(cwd_dir / "test_fail.py").write_text(
|
||||
resources.TEST_FAIL_CWD_FILE.read_text(),
|
||||
)
|
||||
|
||||
# Android doesn't support placeholders in the test command.
|
||||
if any(("{" + placeholder + "}") in test_command for placeholder in ("project", "package")):
|
||||
msg = (
|
||||
f"Test command {test_command!r} with a "
|
||||
"'{project}' or '{package}' placeholder is not supported on Android, "
|
||||
"because the source directory is not visible on the emulator."
|
||||
)
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
# Parse test-command.
|
||||
test_args = shlex.split(test_command)
|
||||
if test_args[0] in {"python", "python3"} and any(arg in test_args for arg in ("-c", "-m")):
|
||||
# Forward the args to the CPython testbed script. We require '-c' or '-m'
|
||||
# to be in the command, because without those flags, the testbed script
|
||||
# will prepend '-m test', which will run Python's own test suite.
|
||||
del test_args[0]
|
||||
elif test_args[0] == "pytest":
|
||||
# We transform some commands into the `python -m` form, but this is deprecated.
|
||||
msg = (
|
||||
f"Test command {test_command!r} is not supported on Android. "
|
||||
"cibuildwheel will try to execute it as if it started with 'python -m'. "
|
||||
"If this works, all you need to do is add that to your test command."
|
||||
)
|
||||
log.warning(msg)
|
||||
test_args.insert(0, "-m")
|
||||
else:
|
||||
msg = (
|
||||
f"Test command {test_command!r} is not supported on Android. "
|
||||
f"Command must begin with 'python' or 'python3', and contain '-m' or '-c'."
|
||||
)
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
# By default, run on a testbed managed emulator running the newest supported
|
||||
# Android version. However, if the user specifies a --managed or --connected
|
||||
# test execution argument, that argument takes precedence.
|
||||
test_runtime_args = state.options.test_runtime.args
|
||||
|
||||
if any(arg.startswith(("--managed", "--connected")) for arg in test_runtime_args):
|
||||
emulator_args = []
|
||||
else:
|
||||
emulator_args = ["--managed", "maxVersion"]
|
||||
|
||||
# Run the test app.
|
||||
call(
|
||||
state.python_dir / "android.py",
|
||||
"test",
|
||||
"--site-packages",
|
||||
site_packages_dir,
|
||||
"--cwd",
|
||||
cwd_dir,
|
||||
*emulator_args,
|
||||
*(["-v"] if state.options.build_verbosity > 0 else []),
|
||||
*test_runtime_args,
|
||||
"--",
|
||||
*test_args,
|
||||
env=state.build_env,
|
||||
)
|
||||
|
||||
|
||||
def sysconfig_print(method_call: str, env: dict[str, str]) -> str:
|
||||
return call(
|
||||
"python",
|
||||
"-c",
|
||||
f'import sysconfig; print(sysconfig.{method_call}, end="")',
|
||||
env=env,
|
||||
capture_stdout=True,
|
||||
)
|
||||
@@ -0,0 +1,762 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
import platform
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from typing import assert_never
|
||||
|
||||
from filelock import FileLock
|
||||
from packaging.version import Version
|
||||
|
||||
from cibuildwheel import errors
|
||||
from cibuildwheel.audit import run_audit
|
||||
from cibuildwheel.frontend import (
|
||||
BuildFrontendName,
|
||||
get_build_frontend_extra_flags,
|
||||
prepare_config_settings,
|
||||
)
|
||||
from cibuildwheel.logger import log
|
||||
from cibuildwheel.platforms.macos import install_cpython as install_build_cpython
|
||||
from cibuildwheel.util import resources
|
||||
from cibuildwheel.util.cmd import call, shell, split_command
|
||||
from cibuildwheel.util.file import CIBW_CACHE_PATH, copy_test_sources, download, move_file
|
||||
from cibuildwheel.util.helpers import prepare_command, unwrap_preserving_paragraphs
|
||||
from cibuildwheel.util.packaging import find_compatible_wheel
|
||||
from cibuildwheel.venv import constraint_flags, virtualenv
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence, Set
|
||||
|
||||
from cibuildwheel.architecture import Architecture
|
||||
from cibuildwheel.environment import ParsedEnvironment
|
||||
from cibuildwheel.options import Options
|
||||
from cibuildwheel.selector import BuildSelector
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
class PythonConfiguration:
|
||||
version: str
|
||||
identifier: str
|
||||
url: str
|
||||
build_url: str
|
||||
sha256: 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, sha256=config.sha256 or None)
|
||||
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 make_cross_venv.py.
|
||||
|
||||
# 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
|
||||
if Version(py_version) >= Version("3.15"):
|
||||
# python.org 3.15+ distributions: sysconfig data lives in the
|
||||
# stdlib directory (lib-<arch>/python<version>); cross-venv scripts
|
||||
# come from cibuildwheel resources.
|
||||
arch = multiarch.split("-", maxsplit=1)[0]
|
||||
stdlib_path = slice_path / f"lib-{arch}" / f"python{py_version}"
|
||||
call(
|
||||
"python",
|
||||
str(resources.IOS_SUPPORT_FILES / "make_cross_venv.py"),
|
||||
str(venv_path),
|
||||
str(stdlib_path),
|
||||
str(resources.IOS_SUPPORT_FILES),
|
||||
env=env,
|
||||
cwd=venv_path,
|
||||
)
|
||||
else:
|
||||
# BeeWare distributions: platform-config already contains both
|
||||
# sysconfig data and the make_cross_venv.py script.
|
||||
platform_config_path = slice_path / f"platform-config/{multiarch}"
|
||||
call(
|
||||
"python",
|
||||
str(platform_config_path / "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]]:
|
||||
# Not using set because mypy can't narrow it
|
||||
if build_frontend == "build[uv]" or build_frontend == "uv": # noqa: PLR1714
|
||||
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.parent.exists(), (
|
||||
f"{target_python.parent} 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", "-V", "-V", 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
|
||||
# Not using set because mypy can't narrow it
|
||||
if build_frontend.name == "build[uv]" or build_frontend.name == "uv": # noqa: PLR1714
|
||||
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"
|
||||
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
|
||||
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version, tmp_dir=identifier_tmp_dir
|
||||
)
|
||||
|
||||
target_install_path, env = setup_python(
|
||||
identifier_tmp_dir / "build",
|
||||
python_configuration=config,
|
||||
dependency_constraint=constraints_path,
|
||||
environment=build_options.environment,
|
||||
build_frontend=build_frontend.name,
|
||||
xbuild_tools=build_options.xbuild_tools,
|
||||
)
|
||||
|
||||
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
|
||||
if compatible_wheel:
|
||||
log.step_end()
|
||||
print(
|
||||
f"\nFound previously built wheel {compatible_wheel.name} "
|
||||
f"that is compatible with {config.identifier}. "
|
||||
"Skipping build step..."
|
||||
)
|
||||
test_wheel = compatible_wheel
|
||||
else:
|
||||
if build_options.before_build:
|
||||
log.step("Running before_build...")
|
||||
before_build_prepared = prepare_command(
|
||||
build_options.before_build,
|
||||
project=".",
|
||||
package=build_options.package_dir,
|
||||
)
|
||||
shell(before_build_prepared, env=env)
|
||||
|
||||
log.step("Building wheel...")
|
||||
built_wheel_dir.mkdir()
|
||||
|
||||
extra_flags = get_build_frontend_extra_flags(
|
||||
build_frontend,
|
||||
build_options.build_verbosity,
|
||||
prepare_config_settings(
|
||||
build_options.config_settings,
|
||||
project=".",
|
||||
package=build_options.package_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",
|
||||
build_options.package_dir.resolve(),
|
||||
f"--wheel-dir={built_wheel_dir}",
|
||||
"--no-deps",
|
||||
*extra_flags,
|
||||
env=env,
|
||||
)
|
||||
case "build":
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"build",
|
||||
build_options.package_dir,
|
||||
"--wheel",
|
||||
f"--outdir={built_wheel_dir}",
|
||||
*extra_flags,
|
||||
env=env,
|
||||
)
|
||||
case _:
|
||||
assert_never(build_frontend)
|
||||
|
||||
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise errors.NonPlatformWheelError()
|
||||
|
||||
repaired_wheel_dir.mkdir()
|
||||
if build_options.repair_command:
|
||||
log.step("Repairing wheel...")
|
||||
|
||||
repair_command_prepared = prepare_command(
|
||||
build_options.repair_command,
|
||||
wheel=built_wheel,
|
||||
dest_dir=repaired_wheel_dir,
|
||||
package=build_options.package_dir,
|
||||
project=".",
|
||||
)
|
||||
shell(repair_command_prepared, env=env)
|
||||
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()
|
||||
|
||||
run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel)
|
||||
|
||||
test_wheel = repaired_wheel
|
||||
|
||||
if build_options.test_command and build_options.test_selector(config.identifier):
|
||||
if not config.is_simulator:
|
||||
log.step("Skipping tests on non-simulator SDK")
|
||||
elif config.arch != os.uname().machine:
|
||||
log.step("Skipping tests on non-native simulator architecture")
|
||||
else:
|
||||
test_env = build_options.test_environment.as_dictionary(prev_environment=env)
|
||||
|
||||
if build_options.before_test:
|
||||
before_test_prepared = prepare_command(
|
||||
build_options.before_test,
|
||||
project=".",
|
||||
package=build_options.package_dir,
|
||||
)
|
||||
shell(before_test_prepared, env=test_env)
|
||||
|
||||
log.step("Setting up test harness...")
|
||||
# Clone the testbed project into the build directory
|
||||
testbed_path = identifier_tmp_dir / "testbed"
|
||||
call(
|
||||
"python",
|
||||
target_install_path / "testbed",
|
||||
"clone",
|
||||
testbed_path,
|
||||
env=test_env,
|
||||
)
|
||||
|
||||
testbed_app_path = testbed_path / "iOSTestbed" / "app"
|
||||
|
||||
# Copy the test sources to the testbed app
|
||||
if build_options.test_sources:
|
||||
copy_test_sources(
|
||||
build_options.test_sources,
|
||||
Path.cwd(),
|
||||
testbed_app_path,
|
||||
)
|
||||
else:
|
||||
(testbed_app_path / "test_fail.py").write_text(
|
||||
resources.TEST_FAIL_CWD_FILE.read_text()
|
||||
)
|
||||
|
||||
log.step("Installing test requirements...")
|
||||
# Install the compiled wheel (with any test extras), plus
|
||||
# the test requirements. Use the --platform tag to force
|
||||
# the installation of iOS wheels; this requires the use of
|
||||
# --only-binary=:all:
|
||||
ios_version = test_env["IPHONEOS_DEPLOYMENT_TARGET"]
|
||||
platform_tag = f"ios_{ios_version.replace('.', '_')}_{config.arch}_{config.sdk}"
|
||||
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--only-binary=:all:",
|
||||
"--platform",
|
||||
platform_tag,
|
||||
"--target",
|
||||
testbed_path / "iOSTestbed" / "app_packages",
|
||||
f"{test_wheel}{build_options.test_extras}",
|
||||
*build_options.test_requires,
|
||||
env=test_env,
|
||||
)
|
||||
|
||||
log.step("Running test suite...")
|
||||
|
||||
# iOS doesn't support placeholders in the test command,
|
||||
# because the source dir isn't visible on the simulator.
|
||||
if (
|
||||
"{project}" in build_options.test_command
|
||||
or "{package}" in build_options.test_command
|
||||
):
|
||||
msg = unwrap_preserving_paragraphs(
|
||||
f"""
|
||||
iOS tests configured with a test command that uses the "{{project}}" or
|
||||
"{{package}}" placeholder. iOS tests cannot use placeholders, because the
|
||||
source directory is not visible on the simulator.
|
||||
|
||||
In addition, iOS tests must run as a Python module, so the test command
|
||||
must begin with 'python -m'.
|
||||
|
||||
Test command: {build_options.test_command!r}
|
||||
"""
|
||||
)
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
test_command_list = shlex.split(build_options.test_command)
|
||||
try:
|
||||
for test_command_parts in split_command(test_command_list):
|
||||
match test_command_parts:
|
||||
case ["python", "-m", *rest]:
|
||||
final_command = rest
|
||||
case ["pytest", *rest]:
|
||||
# pytest works exactly the same as a module, so we
|
||||
# can just run it as a module.
|
||||
msg = unwrap_preserving_paragraphs(f"""
|
||||
iOS tests configured with a test command which doesn't start
|
||||
with 'python -m'. iOS tests must execute python modules - other
|
||||
entrypoints are not supported.
|
||||
|
||||
cibuildwheel will try to execute it as if it started with
|
||||
'python -m'. If this works, all you need to do is add that to
|
||||
your test command.
|
||||
|
||||
Test command: {build_options.test_command!r}
|
||||
""")
|
||||
log.warning(msg)
|
||||
final_command = ["pytest", *rest]
|
||||
case _:
|
||||
msg = unwrap_preserving_paragraphs(
|
||||
f"""
|
||||
iOS tests configured with a test command which doesn't start
|
||||
with 'python -m'. iOS tests must execute python modules - other
|
||||
entrypoints are not supported.
|
||||
|
||||
Test command: {build_options.test_command!r}
|
||||
"""
|
||||
)
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
test_runtime_args = build_options.test_runtime.args
|
||||
|
||||
# 2025-10: The GitHub Actions macos-15 runner has a known issue where
|
||||
# the default simulator won't start due to a disk performance issue;
|
||||
# see https://github.com/actions/runner-images/issues/12777 for details.
|
||||
# In the meantime, if it looks like we're running on a GitHub Actions
|
||||
# macos-15 runner, use a simulator that is known to work, unless the
|
||||
# user explicitly specifies a simulator.
|
||||
os_version, _, arch = platform.mac_ver()
|
||||
if (
|
||||
"GITHUB_ACTIONS" in os.environ
|
||||
and os_version.startswith("15.")
|
||||
and arch == "arm64"
|
||||
and not any(
|
||||
arg.startswith("--simulator") for arg in test_runtime_args
|
||||
)
|
||||
):
|
||||
test_runtime_args = [
|
||||
"--simulator",
|
||||
"iPhone 16e,OS=18.5",
|
||||
*test_runtime_args,
|
||||
]
|
||||
|
||||
call(
|
||||
"python",
|
||||
testbed_path,
|
||||
"run",
|
||||
*(["--verbose"] if build_options.build_verbosity > 0 else []),
|
||||
*test_runtime_args,
|
||||
"--",
|
||||
*final_command,
|
||||
env=test_env,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
# catches the first test command failure in the loop,
|
||||
# implementing short-circuiting
|
||||
log.step_end(success=False)
|
||||
log.error(f"Test suite failed on {config.identifier}")
|
||||
sys.exit(1)
|
||||
|
||||
log.step_end()
|
||||
|
||||
# We're all done here; move it to output (overwrite existing)
|
||||
output_wheel: Path | None = None
|
||||
if compatible_wheel is None:
|
||||
output_wheel = build_options.output_dir.joinpath(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,592 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path, PurePath, PurePosixPath
|
||||
from typing import assert_never
|
||||
|
||||
from cibuildwheel import errors
|
||||
from cibuildwheel.architecture import Architecture
|
||||
from cibuildwheel.audit import needs_audit, run_audit
|
||||
from cibuildwheel.frontend import get_build_frontend_extra_flags, prepare_config_settings
|
||||
from cibuildwheel.logger import log
|
||||
from cibuildwheel.oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform
|
||||
from cibuildwheel.util import resources
|
||||
from cibuildwheel.util.file import copy_test_sources
|
||||
from cibuildwheel.util.helpers import prepare_command, unwrap
|
||||
from cibuildwheel.util.packaging import find_compatible_wheel
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Iterator, Sequence, Set
|
||||
|
||||
from cibuildwheel.options import BuildOptions, Options
|
||||
from cibuildwheel.selector import BuildSelector
|
||||
from cibuildwheel.typing import PathOrStr
|
||||
|
||||
ARCHITECTURE_OCI_PLATFORM_MAP = {
|
||||
Architecture.x86_64: OCIPlatform.AMD64,
|
||||
Architecture.i686: OCIPlatform.i386,
|
||||
Architecture.aarch64: OCIPlatform.ARM64,
|
||||
Architecture.ppc64le: OCIPlatform.PPC64LE,
|
||||
Architecture.s390x: OCIPlatform.S390X,
|
||||
Architecture.armv7l: OCIPlatform.ARMV7,
|
||||
Architecture.riscv64: OCIPlatform.RISCV64,
|
||||
}
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
class PythonConfiguration:
|
||||
version: str
|
||||
identifier: str
|
||||
path_str: str
|
||||
|
||||
@property
|
||||
def path(self) -> PurePosixPath:
|
||||
return PurePosixPath(self.path_str)
|
||||
|
||||
|
||||
@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]:
|
||||
python_configurations = all_python_configurations()
|
||||
|
||||
# return all configurations whose arch is in our `architectures` set,
|
||||
# and match the build/skip rules
|
||||
return [
|
||||
c
|
||||
for c in python_configurations
|
||||
if any(c.identifier.endswith(arch.value) for arch in architectures)
|
||||
and build_selector(c.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'
|
||||
# platform_arch is 'x86_64'
|
||||
_, platform_tag = config.identifier.split("-", 1)
|
||||
_, platform_arch = platform_tag.split("_", 1)
|
||||
|
||||
assert build_options.manylinux_images is not None
|
||||
assert build_options.musllinux_images is not None
|
||||
|
||||
return (
|
||||
build_options.manylinux_images[platform_arch]
|
||||
if platform_tag.startswith("manylinux")
|
||||
else build_options.musllinux_images[platform_arch]
|
||||
)
|
||||
|
||||
|
||||
def get_build_steps(
|
||||
options: Options, python_configurations: list[PythonConfiguration]
|
||||
) -> Iterator[BuildStep]:
|
||||
"""
|
||||
Groups PythonConfigurations into BuildSteps. Each BuildStep represents a
|
||||
separate container instance.
|
||||
"""
|
||||
steps = OrderedDict[tuple[str, str, str, OCIContainerEngineConfig], BuildStep]()
|
||||
|
||||
for config in python_configurations:
|
||||
_, platform_tag = config.identifier.split("-", 1)
|
||||
|
||||
build_options = options.build_options(config.identifier)
|
||||
|
||||
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)
|
||||
else:
|
||||
steps[step_key] = BuildStep(
|
||||
platform_configs=[config],
|
||||
platform_tag=platform_tag,
|
||||
container_engine=container_engine,
|
||||
container_image=container_image,
|
||||
)
|
||||
|
||||
yield from steps.values()
|
||||
|
||||
|
||||
def check_all_python_exist(
|
||||
*, platform_configs: Iterable[PythonConfiguration], container: OCIContainer
|
||||
) -> None:
|
||||
exist = True
|
||||
has_manylinux_interpreters = False
|
||||
messages = []
|
||||
|
||||
with contextlib.suppress(subprocess.CalledProcessError):
|
||||
# use capture_output to keep quiet
|
||||
container.call(["manylinux-interpreters", "--help"], capture_output=True)
|
||||
has_manylinux_interpreters = True
|
||||
|
||||
for config in platform_configs:
|
||||
python_path = config.path / "bin" / "python"
|
||||
if has_manylinux_interpreters:
|
||||
try:
|
||||
container.call(["manylinux-interpreters", "ensure", config.path.name])
|
||||
except subprocess.CalledProcessError:
|
||||
messages.append(
|
||||
f" 'manylinux-interpreters ensure {config.path.name}' needed to build '{config.identifier}' failed in container running image '{container.image}'."
|
||||
" Either the installation failed or this interpreter is not available in that image. Please check the logs."
|
||||
)
|
||||
exist = False
|
||||
else:
|
||||
try:
|
||||
container.call(["test", "-x", python_path])
|
||||
except subprocess.CalledProcessError:
|
||||
messages.append(
|
||||
f" '{python_path}' executable doesn't exist in image '{container.image}' to build '{config.identifier}'."
|
||||
)
|
||||
exist = False
|
||||
if not exist:
|
||||
message = "\n".join(messages)
|
||||
raise errors.FatalError(message)
|
||||
|
||||
|
||||
def build_in_container(
|
||||
*,
|
||||
options: Options,
|
||||
platform_configs: Sequence[PythonConfiguration],
|
||||
container: OCIContainer,
|
||||
container_project_path: PurePath,
|
||||
container_package_dir: PurePath,
|
||||
local_tmp_dir: Path,
|
||||
) -> None:
|
||||
container_output_dir = PurePosixPath("/output")
|
||||
|
||||
check_all_python_exist(platform_configs=platform_configs, container=container)
|
||||
|
||||
log.step("Copying project into container...")
|
||||
container.copy_into(Path.cwd(), container_project_path)
|
||||
|
||||
before_all_options_identifier = platform_configs[0].identifier
|
||||
before_all_options = options.build_options(before_all_options_identifier)
|
||||
|
||||
if before_all_options.before_all:
|
||||
log.step("Running before_all...")
|
||||
|
||||
env = container.get_environment()
|
||||
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(
|
||||
env, executor=container.environment_executor
|
||||
)
|
||||
|
||||
before_all_prepared = prepare_command(
|
||||
before_all_options.before_all,
|
||||
project=container_project_path,
|
||||
package=container_package_dir,
|
||||
)
|
||||
container.call(["sh", "-c", before_all_prepared], env=env)
|
||||
|
||||
built_wheels: list[PurePosixPath] = []
|
||||
|
||||
for config in platform_configs:
|
||||
log.build_start(config.identifier)
|
||||
local_identifier_tmp_dir = local_tmp_dir / config.identifier
|
||||
build_options = options.build_options(config.identifier)
|
||||
build_frontend = build_options.build_frontend
|
||||
use_uv = build_frontend.name in {"build[uv]", "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 = build_options.environment.as_dictionary(env, executor=container.environment_executor)
|
||||
|
||||
# check config python is still on PATH
|
||||
which_python = container.call(["which", "python"], env=env, capture_output=True).strip()
|
||||
if PurePosixPath(which_python) != python_bin / "python":
|
||||
msg = "python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it."
|
||||
raise errors.FatalError(msg)
|
||||
container.call(["python", "-V", "-V"], env=env)
|
||||
|
||||
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:
|
||||
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=container_project_path,
|
||||
package=container_package_dir,
|
||||
)
|
||||
before_build_env = env.copy()
|
||||
if use_uv:
|
||||
# On Linux, no virtualenv is created for the build environment
|
||||
# (unlike macOS/Windows, where one is set up before before_build
|
||||
# runs). uv requires either an active venv or an explicit Python
|
||||
# target to install packages. Pin UV_PYTHON to the exact interpreter
|
||||
# for this build so that `uv pip install` works in before_build
|
||||
# without requiring users to pass --system.
|
||||
before_build_env["UV_PYTHON"] = str(python_bin / "python")
|
||||
container.call(["sh", "-c", before_build_prepared], env=before_build_env)
|
||||
|
||||
log.step("Building wheel...")
|
||||
|
||||
temp_dir = PurePosixPath("/tmp/cibuildwheel")
|
||||
built_wheel_dir = temp_dir / "built_wheel"
|
||||
container.call(["rm", "-rf", built_wheel_dir])
|
||||
container.call(["mkdir", "-p", built_wheel_dir])
|
||||
|
||||
extra_flags = get_build_frontend_extra_flags(
|
||||
build_frontend,
|
||||
build_options.build_verbosity,
|
||||
prepare_config_settings(
|
||||
build_options.config_settings,
|
||||
project=container_project_path,
|
||||
package=container_package_dir,
|
||||
),
|
||||
)
|
||||
|
||||
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 "uv":
|
||||
container.call(
|
||||
[
|
||||
"uv",
|
||||
"build",
|
||||
f"--python={python_bin / 'python'}",
|
||||
container_package_dir,
|
||||
"--wheel",
|
||||
f"--out-dir={built_wheel_dir}",
|
||||
*extra_flags,
|
||||
],
|
||||
env=env,
|
||||
)
|
||||
case _:
|
||||
assert_never(build_frontend)
|
||||
|
||||
built_wheel = container.glob(built_wheel_dir, "*.whl")[0]
|
||||
|
||||
repaired_wheel_dir = temp_dir / "repaired_wheel"
|
||||
container.call(["rm", "-rf", repaired_wheel_dir])
|
||||
container.call(["mkdir", "-p", repaired_wheel_dir])
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise errors.NonPlatformWheelError()
|
||||
|
||||
if build_options.repair_command:
|
||||
log.step("Repairing wheel...")
|
||||
repair_command_prepared = prepare_command(
|
||||
build_options.repair_command,
|
||||
wheel=built_wheel,
|
||||
dest_dir=repaired_wheel_dir,
|
||||
package=container_package_dir,
|
||||
project=container_project_path,
|
||||
)
|
||||
container.call(["sh", "-c", repair_command_prepared], env=env)
|
||||
else:
|
||||
container.call(["mv", built_wheel, repaired_wheel_dir])
|
||||
|
||||
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])
|
||||
|
||||
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
|
||||
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
|
||||
log.step_end()
|
||||
|
||||
if needs_audit(build_options.audit_command, repaired_wheel.name):
|
||||
local_abi3audit_dir = local_identifier_tmp_dir / "audit"
|
||||
local_abi3audit_dir.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
container.copy_out(repaired_wheel_dir, local_abi3audit_dir)
|
||||
local_wheel = local_abi3audit_dir / repaired_wheel.name
|
||||
run_audit(tmp_dir=local_tmp_dir, build_options=build_options, wheel=local_wheel)
|
||||
finally:
|
||||
shutil.rmtree(local_abi3audit_dir, ignore_errors=True)
|
||||
|
||||
if build_options.test_command and build_options.test_selector(config.identifier):
|
||||
log.step("Testing wheel...")
|
||||
|
||||
# set up a virtual environment to install and test from, to make sure
|
||||
# there are no dependencies that were pulled in at build time.
|
||||
if not use_uv:
|
||||
container.call(
|
||||
["pip", "install", "virtualenv", *dependency_constraint_flags], env=env
|
||||
)
|
||||
|
||||
testing_temp_dir = PurePosixPath(
|
||||
container.call(["mktemp", "-d"], capture_output=True).strip()
|
||||
)
|
||||
venv_dir = testing_temp_dir / "venv"
|
||||
|
||||
if use_uv:
|
||||
container.call(["uv", "venv", venv_dir, "--python", python_bin / "python"], env=env)
|
||||
else:
|
||||
# Use embedded dependencies from virtualenv to ensure determinism
|
||||
venv_args = ["--no-periodic-update", "--pip=embed", "--no-setuptools"]
|
||||
if "38" in config.identifier:
|
||||
venv_args.append("--no-wheel")
|
||||
container.call(["python", "-m", "virtualenv", *venv_args, venv_dir], env=env)
|
||||
|
||||
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(
|
||||
build_options.before_test,
|
||||
project=container_project_path,
|
||||
package=container_package_dir,
|
||||
)
|
||||
container.call(["sh", "-c", before_test_prepared], env=virtualenv_env)
|
||||
|
||||
# Install the wheel we just built
|
||||
container.call(
|
||||
[*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)
|
||||
|
||||
# Run the tests from a different directory
|
||||
test_command_prepared = prepare_command(
|
||||
build_options.test_command,
|
||||
project=container_project_path,
|
||||
package=container_package_dir,
|
||||
wheel=repaired_wheel,
|
||||
)
|
||||
|
||||
test_cwd = testing_temp_dir / "test_cwd"
|
||||
container.call(["mkdir", "-p", test_cwd])
|
||||
|
||||
if build_options.test_sources:
|
||||
copy_test_sources(
|
||||
build_options.test_sources,
|
||||
Path.cwd(),
|
||||
test_cwd,
|
||||
copy_into=container.copy_into,
|
||||
)
|
||||
else:
|
||||
# Use the test_fail.py file to raise a nice error if the user
|
||||
# tries to run tests in the cwd
|
||||
container.copy_into(resources.TEST_FAIL_CWD_FILE, test_cwd / "test_fail.py")
|
||||
|
||||
container.call(["sh", "-c", test_command_prepared], cwd=test_cwd, env=virtualenv_env)
|
||||
|
||||
# clean up test environment
|
||||
container.call(["rm", "-rf", testing_temp_dir])
|
||||
|
||||
# move repaired wheel to output
|
||||
output_wheel: Path | None = None
|
||||
if compatible_wheel is None:
|
||||
container.call(["mkdir", "-p", container_output_dir])
|
||||
container.call(["mv", repaired_wheel, container_output_dir])
|
||||
built_wheels.append(container_output_dir / repaired_wheel.name)
|
||||
output_wheel = options.globals.output_dir / repaired_wheel.name
|
||||
|
||||
log.build_end(output_wheel)
|
||||
|
||||
log.step("Copying wheels back to host...")
|
||||
# copy the output back into the host
|
||||
container.copy_out(container_output_dir, options.globals.output_dir)
|
||||
log.step_end()
|
||||
|
||||
|
||||
def build(options: Options, tmp_path: Path) -> None:
|
||||
python_configurations = get_python_configurations(
|
||||
options.globals.build_selector, options.globals.architectures
|
||||
)
|
||||
|
||||
cwd = Path.cwd()
|
||||
abs_package_dir = options.globals.package_dir.resolve()
|
||||
if cwd != abs_package_dir and cwd not in abs_package_dir.parents:
|
||||
msg = "package_dir must be inside the working directory"
|
||||
raise Exception(msg)
|
||||
|
||||
container_project_path = PurePosixPath("/project")
|
||||
container_package_dir = container_project_path / abs_package_dir.relative_to(cwd)
|
||||
|
||||
for build_step in get_build_steps(options, python_configurations):
|
||||
try:
|
||||
# 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.
|
||||
"""
|
||||
)
|
||||
raise errors.ConfigurationError(msg) from error
|
||||
|
||||
try:
|
||||
ids_to_build = [x.identifier for x in build_step.platform_configs]
|
||||
log.step(f"Starting container image {build_step.container_image}...")
|
||||
|
||||
print(f"info: This container will host the build for {', '.join(ids_to_build)}...")
|
||||
architecture = Architecture(build_step.platform_tag.split("_", 1)[1])
|
||||
|
||||
with OCIContainer(
|
||||
image=build_step.container_image,
|
||||
oci_platform=ARCHITECTURE_OCI_PLATFORM_MAP[architecture],
|
||||
cwd=container_project_path,
|
||||
engine=build_step.container_engine,
|
||||
) as container:
|
||||
build_in_container(
|
||||
options=options,
|
||||
platform_configs=build_step.platform_configs,
|
||||
container=container,
|
||||
container_project_path=container_project_path,
|
||||
container_package_dir=container_package_dir,
|
||||
local_tmp_dir=tmp_path,
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError as error:
|
||||
troubleshoot(options, error)
|
||||
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:
|
||||
if len(error_cmd) < 3 or error_cmd[0:2] != ["sh", "-c"]:
|
||||
return False
|
||||
command_prefix = command_template.split("{", maxsplit=1)[0].strip()
|
||||
return error_cmd[2].startswith(command_prefix)
|
||||
|
||||
|
||||
def troubleshoot(options: Options, error: Exception) -> None:
|
||||
if isinstance(error, subprocess.CalledProcessError) and (
|
||||
error.cmd[0:4] == ["python", "-m", "pip", "wheel"]
|
||||
or error.cmd[0:2] == ["uv", "build"]
|
||||
or error.cmd[0:3] == ["python", "-m", "build"]
|
||||
or _matches_prepared_command(
|
||||
error.cmd, options.build_options(None).repair_command
|
||||
) # TODO allow matching of overrides too?
|
||||
):
|
||||
# the wheel build step or the repair step failed
|
||||
so_files = list(options.globals.package_dir.glob("**/*.so"))
|
||||
|
||||
if so_files:
|
||||
print(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
NOTE: Shared object (.so) files found in this project.
|
||||
|
||||
These files might be built against the wrong OS, causing problems with
|
||||
auditwheel. If possible, run cibuildwheel in a clean checkout.
|
||||
|
||||
If you're using Cython and have previously done an in-place build,
|
||||
remove those build files (*.so and *.c) before starting cibuildwheel.
|
||||
|
||||
setuptools uses the build/ folder to store its build cache. It
|
||||
may be necessary to remove those build files (*.so and *.o) before
|
||||
starting cibuildwheel.
|
||||
|
||||
Files that belong to a virtual environment are probably not an issue
|
||||
unless you used a custom command telling cibuildwheel to activate it.
|
||||
"""
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
print(" Files detected:")
|
||||
print("\n".join(f" {f}" for f in so_files))
|
||||
print()
|
||||
@@ -0,0 +1,762 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from typing import assert_never
|
||||
|
||||
from filelock import FileLock
|
||||
from packaging.version import Version
|
||||
|
||||
from cibuildwheel import errors
|
||||
from cibuildwheel.audit import run_audit
|
||||
from cibuildwheel.ci import detect_ci_provider
|
||||
from cibuildwheel.frontend import (
|
||||
BuildFrontendName,
|
||||
get_build_frontend_extra_flags,
|
||||
prepare_config_settings,
|
||||
)
|
||||
from cibuildwheel.logger import log
|
||||
from cibuildwheel.util import resources
|
||||
from cibuildwheel.util.cmd import call, shell
|
||||
from cibuildwheel.util.file import CIBW_CACHE_PATH, copy_test_sources, download, move_file
|
||||
from cibuildwheel.util.helpers import prepare_command, unwrap
|
||||
from cibuildwheel.util.packaging import find_compatible_wheel, get_pip_version
|
||||
from cibuildwheel.venv import constraint_flags, find_uv, target_marker_env, virtualenv
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Set
|
||||
from typing import Literal
|
||||
|
||||
from cibuildwheel.architecture import Architecture
|
||||
from cibuildwheel.environment import ParsedEnvironment
|
||||
from cibuildwheel.options import Options
|
||||
from cibuildwheel.selector import BuildSelector
|
||||
|
||||
|
||||
@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
|
||||
sha256: 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, sha256: str = ""
|
||||
) -> 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"""
|
||||
Framework CPython {version} not detected as installed in:
|
||||
{installation_path}
|
||||
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.rsplit("/", maxsplit=1)[-1]
|
||||
pkg_path = CIBW_CACHE_PATH / "cpython-installer" / python_filename
|
||||
if not pkg_path.exists():
|
||||
download(url, pkg_path, sha256=sha256 or None)
|
||||
args = []
|
||||
if version.startswith("3.14"):
|
||||
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_314.resolve())]
|
||||
elif version.startswith("3.15"):
|
||||
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_315.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, sha256: 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, sha256=sha256 or None)
|
||||
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, sha256: 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, sha256=sha256 or None)
|
||||
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 in {"build[uv]", "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,
|
||||
python_configuration.sha256,
|
||||
)
|
||||
|
||||
elif implementation_id.startswith("pp"):
|
||||
base_python = install_pypy(tmp, python_configuration.url, python_configuration.sha256)
|
||||
elif implementation_id.startswith("gp"):
|
||||
base_python = install_graalpy(tmp, python_configuration.url, python_configuration.sha256)
|
||||
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,
|
||||
marker_env=target_marker_env(implementation_id=implementation_id),
|
||||
)
|
||||
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)
|
||||
# uv uses this over the current environment's python, so remove it to avoid confusion
|
||||
env.pop("PYTHON_VERSION", None)
|
||||
env.pop("PYTHON_ARCH", None)
|
||||
env.pop("UV_PYTHON", 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", "-V", "-V", 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.
|
||||
# CPython 3.14.0 needs 10.15.
|
||||
if config_is_arm64:
|
||||
default_target = "11.0"
|
||||
elif Version(python_configuration.version) >= Version("3.14"):
|
||||
default_target = "10.15"
|
||||
elif Version(python_configuration.version) >= Version("3.12"):
|
||||
default_target = "10.13"
|
||||
elif python_configuration.identifier.startswith("pp") and Version(
|
||||
python_configuration.version
|
||||
) >= Version("3.9"):
|
||||
default_target = "10.15"
|
||||
else:
|
||||
default_target = "10.9"
|
||||
env.setdefault("MACOSX_DEPLOYMENT_TARGET", default_target)
|
||||
|
||||
# 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",
|
||||
"--python",
|
||||
which_python,
|
||||
"--upgrade",
|
||||
"delocate",
|
||||
"build[virtualenv, uv]",
|
||||
*constraint_flags(dependency_constraint),
|
||||
env=env,
|
||||
)
|
||||
case "uv":
|
||||
assert uv_path is not None
|
||||
call(
|
||||
uv_path,
|
||||
"pip",
|
||||
"install",
|
||||
"--python",
|
||||
which_python,
|
||||
"--upgrade",
|
||||
"delocate",
|
||||
*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 in {"build[uv]", "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,
|
||||
prepare_config_settings(
|
||||
build_options.config_settings,
|
||||
project=".",
|
||||
package=build_options.package_dir,
|
||||
),
|
||||
)
|
||||
|
||||
build_env = env.copy()
|
||||
|
||||
match build_frontend.name:
|
||||
case "pip":
|
||||
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
|
||||
# see https://github.com/pypa/cibuildwheel/pull/369
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"pip",
|
||||
"wheel",
|
||||
build_options.package_dir.resolve(),
|
||||
f"--wheel-dir={built_wheel_dir}",
|
||||
"--no-deps",
|
||||
*extra_flags,
|
||||
env=build_env,
|
||||
)
|
||||
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 "uv":
|
||||
assert uv_path is not None
|
||||
call(
|
||||
uv_path,
|
||||
"build",
|
||||
f"--python={base_python}",
|
||||
build_options.package_dir,
|
||||
"--wheel",
|
||||
f"--out-dir={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,
|
||||
package=build_options.package_dir,
|
||||
project=".",
|
||||
)
|
||||
shell(repair_command_prepared, env=env)
|
||||
else:
|
||||
shutil.move(str(built_wheel), repaired_wheel_dir)
|
||||
|
||||
try:
|
||||
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
|
||||
except StopIteration:
|
||||
raise errors.RepairStepProducedNoWheelError() from None
|
||||
|
||||
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
|
||||
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
|
||||
log.step_end()
|
||||
|
||||
run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel)
|
||||
|
||||
if build_options.test_command and build_options.test_selector(config.identifier):
|
||||
machine_arch = platform.machine()
|
||||
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
|
||||
|
||||
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
|
||||
pip_install(
|
||||
f"{repaired_wheel}{build_options.test_extras}",
|
||||
env=virtualenv_env,
|
||||
)
|
||||
|
||||
# test the wheel
|
||||
if build_options.test_requires:
|
||||
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(),
|
||||
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,574 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Final, TypedDict
|
||||
|
||||
from filelock import FileLock
|
||||
|
||||
from cibuildwheel import errors
|
||||
from cibuildwheel.architecture import Architecture
|
||||
from cibuildwheel.audit import run_audit
|
||||
from cibuildwheel.frontend import get_build_frontend_extra_flags, prepare_config_settings
|
||||
from cibuildwheel.logger import log
|
||||
from cibuildwheel.util import resources
|
||||
from cibuildwheel.util.cmd import call, shell
|
||||
from cibuildwheel.util.file import (
|
||||
CIBW_CACHE_PATH,
|
||||
copy_test_sources,
|
||||
download,
|
||||
extract_tar,
|
||||
extract_zip,
|
||||
move_file,
|
||||
)
|
||||
from cibuildwheel.util.helpers import prepare_command, unwrap, unwrap_preserving_paragraphs
|
||||
from cibuildwheel.util.packaging import find_compatible_wheel, get_pip_version
|
||||
from cibuildwheel.util.python_build_standalone import (
|
||||
PythonBuildStandaloneError,
|
||||
create_python_build_standalone_environment,
|
||||
)
|
||||
from cibuildwheel.venv import constraint_flags, virtualenv
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Set
|
||||
|
||||
from cibuildwheel.environment import ParsedEnvironment
|
||||
from cibuildwheel.options import Options
|
||||
from cibuildwheel.selector import BuildSelector
|
||||
|
||||
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
|
||||
sha256: 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(env: dict[str, str], version: str, xbuildenv_cache_path: Path) -> Path:
|
||||
"""Install Emscripten via pyodide-build, which also applies Pyodide-specific patches."""
|
||||
emscripten_dir = Path(
|
||||
call("pyodide", "config", "get", "emscripten_dir", env=env, capture_stdout=True).strip()
|
||||
)
|
||||
with FileLock(CIBW_CACHE_PATH / "emscripten.lock"):
|
||||
if emscripten_dir.exists():
|
||||
return emscripten_dir
|
||||
call(
|
||||
"pyodide",
|
||||
"xbuildenv",
|
||||
"install-emscripten",
|
||||
"--force",
|
||||
"--version",
|
||||
version,
|
||||
"--path",
|
||||
str(xbuildenv_cache_path),
|
||||
env=env,
|
||||
cwd=CIBW_CACHE_PATH,
|
||||
)
|
||||
assert emscripten_dir.exists()
|
||||
return emscripten_dir
|
||||
|
||||
|
||||
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], xbuildenv_cache_path: Path, pyodide_version: str) -> str:
|
||||
"""Install a particular Pyodide xbuildenv version and set a path to the Pyodide root."""
|
||||
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", "-V", "-V", env=env)
|
||||
|
||||
# check what pip version we're on
|
||||
assert (venv_bin_path / "pip").exists()
|
||||
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
|
||||
print(which_pip)
|
||||
if which_pip != str(venv_bin_path / "pip"):
|
||||
msg = "pip available on PATH doesn't match our venv instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it."
|
||||
raise errors.FatalError(msg)
|
||||
call("pip", "--version", env=env)
|
||||
|
||||
log.step("Installing build tools...")
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"pyodide-build",
|
||||
*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,
|
||||
)
|
||||
|
||||
xbuildenv_cache_path = CIBW_CACHE_PATH / f"pyodide-build-{pyodide_build_version}"
|
||||
|
||||
log.step(f"Installing Pyodide xbuildenv version: {pyodide_version} ...")
|
||||
env["PYODIDE_ROOT"] = install_xbuildenv(env, xbuildenv_cache_path, pyodide_version)
|
||||
|
||||
emscripten_version = xbuildenv_info["emscripten"]
|
||||
log.step(
|
||||
f"Installing Emscripten {emscripten_version} and applying Pyodide-specific patches ..."
|
||||
)
|
||||
emscripten_dir = install_emscripten(env, emscripten_version, xbuildenv_cache_path)
|
||||
|
||||
env["PATH"] = os.pathsep.join([str(emscripten_dir), env["PATH"]])
|
||||
|
||||
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,
|
||||
prepare_config_settings(
|
||||
build_options.config_settings,
|
||||
project=".",
|
||||
package=build_options.package_dir,
|
||||
),
|
||||
)
|
||||
|
||||
call(
|
||||
"pyodide",
|
||||
"build",
|
||||
build_options.package_dir,
|
||||
f"--outdir={built_wheel_dir}",
|
||||
*extra_flags,
|
||||
env=env,
|
||||
)
|
||||
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise errors.NonPlatformWheelError()
|
||||
|
||||
if build_options.repair_command:
|
||||
log.step("Repairing wheel...")
|
||||
|
||||
repair_command_prepared = prepare_command(
|
||||
build_options.repair_command,
|
||||
wheel=built_wheel,
|
||||
dest_dir=repaired_wheel_dir,
|
||||
package=build_options.package_dir,
|
||||
project=".",
|
||||
)
|
||||
shell(repair_command_prepared, env=env)
|
||||
log.step_end()
|
||||
else:
|
||||
shutil.move(str(built_wheel), repaired_wheel_dir)
|
||||
|
||||
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
|
||||
|
||||
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
|
||||
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
|
||||
|
||||
run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel)
|
||||
|
||||
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
|
||||
@@ -0,0 +1,707 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
import platform as platform_module
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
from typing import assert_never
|
||||
|
||||
from filelock import FileLock
|
||||
|
||||
from cibuildwheel import errors
|
||||
from cibuildwheel.architecture import Architecture
|
||||
from cibuildwheel.audit import run_audit
|
||||
from cibuildwheel.frontend import (
|
||||
BuildFrontendName,
|
||||
get_build_frontend_extra_flags,
|
||||
prepare_config_settings,
|
||||
)
|
||||
from cibuildwheel.logger import log
|
||||
from cibuildwheel.util import resources
|
||||
from cibuildwheel.util.cmd import call, shell
|
||||
from cibuildwheel.util.file import (
|
||||
CIBW_CACHE_PATH,
|
||||
copy_test_sources,
|
||||
download,
|
||||
extract_zip,
|
||||
move_file,
|
||||
)
|
||||
from cibuildwheel.util.helpers import prepare_command, unwrap
|
||||
from cibuildwheel.util.packaging import find_compatible_wheel, get_pip_version
|
||||
from cibuildwheel.venv import constraint_flags, find_uv, target_marker_env, virtualenv
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import MutableMapping, Sequence, Set
|
||||
|
||||
from cibuildwheel.environment import ParsedEnvironment
|
||||
from cibuildwheel.options import Options
|
||||
from cibuildwheel.selector import BuildSelector
|
||||
|
||||
|
||||
def get_nuget_args(
|
||||
version: str, arch: str, free_threaded: bool, output_directory: Path
|
||||
) -> list[str]:
|
||||
package_name = {
|
||||
"32": "pythonx86",
|
||||
"64": "python",
|
||||
"ARM64": "pythonarm64",
|
||||
# Aliases for platform.machine() return values
|
||||
"x86": "pythonx86",
|
||||
"AMD64": "python",
|
||||
}[arch]
|
||||
if free_threaded:
|
||||
package_name = f"{package_name}-freethreaded"
|
||||
return [
|
||||
package_name,
|
||||
"-Version",
|
||||
version,
|
||||
"-FallbackSource",
|
||||
"https://api.nuget.org/v3/index.json",
|
||||
"-OutputDirectory",
|
||||
str(output_directory),
|
||||
]
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
class PythonConfiguration:
|
||||
version: str
|
||||
identifier: str
|
||||
url: str | None = None
|
||||
sha256: str = ""
|
||||
|
||||
@property
|
||||
def arch(self) -> str:
|
||||
return {
|
||||
"win32": "32",
|
||||
"win_amd64": "64",
|
||||
"win_arm64": "ARM64",
|
||||
}[self.identifier.split("-")[-1]]
|
||||
|
||||
|
||||
def all_python_configurations() -> list[PythonConfiguration]:
|
||||
config_dicts = resources.read_python_configs("windows")
|
||||
return [PythonConfiguration(**item) for item in config_dicts]
|
||||
|
||||
|
||||
def get_python_configurations(
|
||||
build_selector: BuildSelector,
|
||||
architectures: Set[Architecture],
|
||||
) -> list[PythonConfiguration]:
|
||||
python_configurations = all_python_configurations()
|
||||
|
||||
map_arch = {"32": Architecture.x86, "64": Architecture.AMD64, "ARM64": Architecture.ARM64}
|
||||
|
||||
# skip builds as required
|
||||
python_configurations = [
|
||||
c
|
||||
for c in python_configurations
|
||||
if build_selector(c.identifier) and map_arch[c.arch] in architectures
|
||||
]
|
||||
|
||||
return python_configurations
|
||||
|
||||
|
||||
@cache
|
||||
def _ensure_nuget() -> Path:
|
||||
nuget = CIBW_CACHE_PATH / "nuget.exe"
|
||||
with FileLock(str(nuget) + ".lock"):
|
||||
if not nuget.exists():
|
||||
download("https://dist.nuget.org/win-x86-commandline/latest/nuget.exe", nuget)
|
||||
return nuget
|
||||
|
||||
|
||||
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, free_threaded, base_output_dir)
|
||||
installation_path = base_output_dir / (nuget_args[0] + "." + version) / "tools"
|
||||
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)
|
||||
return installation_path / "python.exe"
|
||||
|
||||
|
||||
def install_pypy(tmp: Path, arch: str, url: str, sha256: str = "") -> Path:
|
||||
assert arch == "64"
|
||||
assert "win64" in url
|
||||
# Inside the PyPy zip file is a directory with the same name
|
||||
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():
|
||||
pypy_zip = tmp / zip_filename
|
||||
download(url, pypy_zip, sha256=sha256 or None)
|
||||
# Extract to the parent directory because the zip file still contains a directory
|
||||
extract_zip(pypy_zip, installation_path.parent)
|
||||
return installation_path / "python.exe"
|
||||
|
||||
|
||||
def install_graalpy(tmp: Path, url: str, sha256: 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, sha256=sha256 or None)
|
||||
# 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,
|
||||
python_libs_base: Path,
|
||||
env: MutableMapping[str, str],
|
||||
) -> None:
|
||||
distutils_cfg = tmp / "extra-setup.cfg"
|
||||
env["DIST_EXTRA_CONFIG"] = str(distutils_cfg)
|
||||
log.notice(f"Setting DIST_EXTRA_CONFIG={distutils_cfg} for cross-compilation")
|
||||
|
||||
# Ensure our additional import libraries are made available, and explicitly
|
||||
# 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(
|
||||
f"""\
|
||||
[build]
|
||||
plat_name={plat_name}
|
||||
[build_ext]
|
||||
library_dirs={python_libs_base}
|
||||
plat_name={plat_name}
|
||||
[bdist_wheel]
|
||||
plat_name={plat_name}
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# setuptools builds require explicit override of PYD extension
|
||||
# This is because it always gets the extension from the running
|
||||
# interpreter, and has no logic to construct it. Currently, CPython's
|
||||
# extensions follow our identifiers, but if they ever diverge in the
|
||||
# future, we will need to store new data
|
||||
log.notice(
|
||||
f"Setting SETUPTOOLS_EXT_SUFFIX=.{python_configuration.identifier}.pyd for cross-compilation"
|
||||
)
|
||||
env["SETUPTOOLS_EXT_SUFFIX"] = f".{python_configuration.identifier}.pyd"
|
||||
|
||||
# Cross-compilation requires fixes that only exist in setuptools's copy of
|
||||
# distutils, so ensure that it is activated
|
||||
# Since not all projects can handle the newer distutils, display a warning
|
||||
# to help them figure out what may have gone wrong if this breaks for them
|
||||
log.notice("Setting SETUPTOOLS_USE_DISTUTILS=local as it is required for cross-compilation")
|
||||
env["SETUPTOOLS_USE_DISTUTILS"] = "local"
|
||||
|
||||
|
||||
# These cross-compile setup functions have the same signature by design
|
||||
def setup_rust_cross_compile(
|
||||
tmp: Path, # noqa: ARG001
|
||||
python_configuration: PythonConfiguration,
|
||||
python_libs_base: Path, # noqa: ARG001
|
||||
env: MutableMapping[str, str],
|
||||
) -> None:
|
||||
# Assume that MSVC will be used, because we already know that we are
|
||||
# cross-compiling. MinGW users can set CARGO_BUILD_TARGET themselves
|
||||
# and we will respect the existing value.
|
||||
cargo_target = {
|
||||
"64": "x86_64-pc-windows-msvc",
|
||||
"32": "i686-pc-windows-msvc",
|
||||
"ARM64": "aarch64-pc-windows-msvc",
|
||||
}.get(python_configuration.arch)
|
||||
|
||||
# CARGO_BUILD_TARGET is the variable used by Cargo and setuptools_rust
|
||||
if env.get("CARGO_BUILD_TARGET"):
|
||||
if env["CARGO_BUILD_TARGET"] != cargo_target:
|
||||
log.notice("Not overriding CARGO_BUILD_TARGET as it has already been set")
|
||||
# No message if it was set to what we were planning to set it to
|
||||
elif cargo_target:
|
||||
log.notice(f"Setting CARGO_BUILD_TARGET={cargo_target} for cross-compilation")
|
||||
env["CARGO_BUILD_TARGET"] = cargo_target
|
||||
else:
|
||||
log.warning(
|
||||
f"Unable to configure Rust cross-compilation for architecture {python_configuration.arch}"
|
||||
)
|
||||
|
||||
|
||||
def setup_python(
|
||||
tmp: Path,
|
||||
python_configuration: PythonConfiguration,
|
||||
dependency_constraint: Path | None,
|
||||
environment: ParsedEnvironment,
|
||||
build_frontend: BuildFrontendName,
|
||||
) -> 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 = base_python.parent / "libs"
|
||||
log.step(f"Installing native Python {native_arch} for cross-compilation...")
|
||||
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, python_configuration.sha256
|
||||
)
|
||||
elif implementation_id.startswith("gp"):
|
||||
base_python = install_graalpy(
|
||||
tmp, python_configuration.url or "", python_configuration.sha256
|
||||
)
|
||||
else:
|
||||
msg = "Unknown Python implementation"
|
||||
raise ValueError(msg)
|
||||
assert base_python.exists()
|
||||
|
||||
use_uv = build_frontend in {"build[uv]", "uv"}
|
||||
uv_path = find_uv()
|
||||
|
||||
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,
|
||||
marker_env=target_marker_env(implementation_id=implementation_id),
|
||||
)
|
||||
|
||||
# set up environment variables for run_with_env
|
||||
env["PYTHON_VERSION"] = python_configuration.version
|
||||
env["PYTHON_ARCH"] = python_configuration.arch
|
||||
env.pop("UV_PYTHON", None)
|
||||
if not use_uv:
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
|
||||
# update env with results from CIBW_ENVIRONMENT
|
||||
env = environment.as_dictionary(prev_environment=env)
|
||||
|
||||
# check what Python version we're on
|
||||
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", "-V", "-V", env=env)
|
||||
call("python", "-c", "\"import struct; print(struct.calcsize('P') * 8)\"", env=env)
|
||||
|
||||
# check what pip version we're on
|
||||
if not use_uv:
|
||||
assert (venv_path / "Scripts" / "pip.exe").exists()
|
||||
where_pip = call("where", "pip", env=env, capture_stdout=True).splitlines()[0].strip()
|
||||
print(where_pip)
|
||||
if where_pip.strip() != str(venv_path / "Scripts" / "pip.exe"):
|
||||
msg = "pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it."
|
||||
raise errors.FatalError(msg)
|
||||
call("pip", "--version", env=env)
|
||||
|
||||
log.step("Installing build tools...")
|
||||
match build_frontend:
|
||||
case "pip":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"delvewheel",
|
||||
*constraint_flags(dependency_constraint),
|
||||
env=env,
|
||||
)
|
||||
case "build":
|
||||
call(
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"build[virtualenv]",
|
||||
"delvewheel",
|
||||
*constraint_flags(dependency_constraint),
|
||||
env=env,
|
||||
)
|
||||
case "build[uv]":
|
||||
assert uv_path is not None
|
||||
call(
|
||||
uv_path,
|
||||
"pip",
|
||||
"install",
|
||||
"--python",
|
||||
where_python,
|
||||
"--upgrade",
|
||||
"build[virtualenv]",
|
||||
"delvewheel",
|
||||
*constraint_flags(dependency_constraint),
|
||||
env=env,
|
||||
)
|
||||
case "uv":
|
||||
assert uv_path is not None
|
||||
call(
|
||||
uv_path,
|
||||
"pip",
|
||||
"install",
|
||||
"--python",
|
||||
where_python,
|
||||
"--upgrade",
|
||||
"delvewheel",
|
||||
*constraint_flags(dependency_constraint),
|
||||
env=env,
|
||||
)
|
||||
case _:
|
||||
assert_never(build_frontend)
|
||||
|
||||
if python_libs_base:
|
||||
# Set up the environment for various backends to enable cross-compilation
|
||||
setup_setuptools_cross_compile(tmp, python_configuration, python_libs_base, env)
|
||||
setup_rust_cross_compile(tmp, python_configuration, python_libs_base, env)
|
||||
|
||||
if implementation_id.startswith("gp311"):
|
||||
# GraalPy 24 fails to discover compilers, setup the relevant environment
|
||||
# variables. Adapted from
|
||||
# https://github.com/microsoft/vswhere/wiki/Start-Developer-Command-Prompt
|
||||
# Remove when GraalPy 24.x is dropped.
|
||||
vcpath = call(
|
||||
Path(os.environ["PROGRAMFILES(X86)"])
|
||||
/ "Microsoft Visual Studio"
|
||||
/ "Installer"
|
||||
/ "vswhere.exe",
|
||||
"-products",
|
||||
"*",
|
||||
"-latest",
|
||||
"-property",
|
||||
"installationPath",
|
||||
capture_stdout=True,
|
||||
).strip()
|
||||
log.notice(f"Discovering Visual Studio for GraalPy at {vcpath}")
|
||||
vcvars_file = tmp / "vcvars.json"
|
||||
call(
|
||||
f"{vcpath}\\Common7\\Tools\\vsdevcmd.bat",
|
||||
"-no_logo",
|
||||
"-arch=amd64",
|
||||
"-host_arch=amd64",
|
||||
"&&",
|
||||
"python",
|
||||
"-c",
|
||||
# this command needs to be one line for Windows reasons
|
||||
"import sys, json, pathlib, os; pathlib.Path(sys.argv[1]).write_text(json.dumps(dict(os.environ)))",
|
||||
vcvars_file,
|
||||
env=env,
|
||||
)
|
||||
with open(vcvars_file, encoding="utf-8") as f:
|
||||
vcvars = json.load(f)
|
||||
env.update(vcvars)
|
||||
|
||||
return base_python, env
|
||||
|
||||
|
||||
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
|
||||
|
||||
uv_path = find_uv()
|
||||
|
||||
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=options.globals.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 in {"build[uv]", "uv"}
|
||||
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"
|
||||
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version,
|
||||
tmp_dir=identifier_tmp_dir,
|
||||
)
|
||||
|
||||
# install Python
|
||||
base_python, env = setup_python(
|
||||
identifier_tmp_dir / "build",
|
||||
config,
|
||||
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:
|
||||
# run the before_build command
|
||||
if build_options.before_build:
|
||||
log.step("Running before_build...")
|
||||
before_build_prepared = prepare_command(
|
||||
build_options.before_build,
|
||||
project=".",
|
||||
package=options.globals.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,
|
||||
prepare_config_settings(
|
||||
build_options.config_settings,
|
||||
project=".",
|
||||
package=options.globals.package_dir,
|
||||
),
|
||||
)
|
||||
|
||||
if (
|
||||
config.identifier.startswith("gp311")
|
||||
and build_frontend.name == "build"
|
||||
and "--no-isolation" not in extra_flags
|
||||
and "-n" not in extra_flags
|
||||
):
|
||||
# GraalPy 24 fails to discover its standard library when a venv is created
|
||||
# from a virtualenv seeded executable. See
|
||||
# https://github.com/oracle/graalpython/issues/491 and remove this once
|
||||
# GraalPy 24 is dropped.
|
||||
log.notice(
|
||||
"Disabling build isolation to workaround GraalPy bug. If the build fails, consider using pip or build[uv] as build frontend."
|
||||
)
|
||||
shell("graalpy -m pip install setuptools wheel", env=env)
|
||||
extra_flags = [*extra_flags, "-n"]
|
||||
|
||||
match build_frontend.name:
|
||||
case "pip":
|
||||
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
|
||||
# see https://github.com/pypa/cibuildwheel/pull/369
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"pip",
|
||||
"wheel",
|
||||
options.globals.package_dir.resolve(),
|
||||
f"--wheel-dir={built_wheel_dir}",
|
||||
"--no-deps",
|
||||
*extra_flags,
|
||||
env=env,
|
||||
)
|
||||
case "build" | "build[uv]":
|
||||
if (
|
||||
use_uv
|
||||
and "--no-isolation" not in extra_flags
|
||||
and "-n" not in extra_flags
|
||||
):
|
||||
extra_flags.append("--installer=uv")
|
||||
|
||||
call(
|
||||
"python",
|
||||
"-m",
|
||||
"build",
|
||||
build_options.package_dir,
|
||||
"--wheel",
|
||||
f"--outdir={built_wheel_dir}",
|
||||
*extra_flags,
|
||||
env=env,
|
||||
)
|
||||
case "uv":
|
||||
assert uv_path is not None
|
||||
call(
|
||||
uv_path,
|
||||
"build",
|
||||
f"--python={base_python}",
|
||||
build_options.package_dir,
|
||||
"--wheel",
|
||||
f"--out-dir={built_wheel_dir}",
|
||||
*extra_flags,
|
||||
env=env,
|
||||
)
|
||||
case _:
|
||||
assert_never(build_frontend)
|
||||
|
||||
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
||||
|
||||
# repair the wheel
|
||||
repaired_wheel_dir.mkdir()
|
||||
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise errors.NonPlatformWheelError()
|
||||
|
||||
if build_options.repair_command:
|
||||
log.step("Repairing wheel...")
|
||||
repair_command_prepared = prepare_command(
|
||||
build_options.repair_command,
|
||||
wheel=built_wheel,
|
||||
dest_dir=repaired_wheel_dir,
|
||||
package=build_options.package_dir,
|
||||
project=".",
|
||||
)
|
||||
shell(repair_command_prepared, env=env)
|
||||
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)
|
||||
|
||||
run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel)
|
||||
|
||||
test_selected = options.globals.test_selector(config.identifier)
|
||||
if test_selected and config.arch == "ARM64" != platform_module.machine():
|
||||
log.warning(
|
||||
unwrap(
|
||||
"""
|
||||
While arm64 wheels can be built on other platforms, they cannot
|
||||
be tested. An arm64 runner is required. To silence this warning,
|
||||
set `CIBW_TEST_SKIP: "*-win_arm64"`.
|
||||
"""
|
||||
)
|
||||
)
|
||||
# skip this test
|
||||
elif test_selected and build_options.test_command:
|
||||
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.
|
||||
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,
|
||||
)
|
||||
|
||||
virtualenv_env = build_options.test_environment.as_dictionary(
|
||||
prev_environment=virtualenv_env
|
||||
)
|
||||
|
||||
# check that we are using the Python from the virtual environment
|
||||
call("where", "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(before_test_prepared, env=virtualenv_env)
|
||||
|
||||
pip: Sequence[Path | str]
|
||||
if use_uv:
|
||||
assert uv_path is not None
|
||||
pip = [uv_path, "pip"]
|
||||
else:
|
||||
pip = ["pip"]
|
||||
|
||||
# install the wheel
|
||||
call(
|
||||
*pip,
|
||||
"install",
|
||||
str(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_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.cwd(),
|
||||
package=options.globals.package_dir.resolve(),
|
||||
wheel=repaired_wheel,
|
||||
)
|
||||
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
||||
|
||||
# we're all done here; move it to output (remove if already exists)
|
||||
output_wheel = None
|
||||
if compatible_wheel is None:
|
||||
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
|
||||
moved_wheel = move_file(repaired_wheel, output_wheel)
|
||||
if moved_wheel != output_wheel.resolve():
|
||||
log.warning(
|
||||
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(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
|
||||
@@ -3,9 +3,32 @@ from __future__ import annotations
|
||||
import ast
|
||||
import configparser
|
||||
import contextlib
|
||||
from pathlib import Path
|
||||
|
||||
from ._compat import tomllib
|
||||
import dependency_groups
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def get_parent(node: ast.AST | None, depth: int = 1) -> ast.AST | None:
|
||||
for _ in range(depth):
|
||||
node = getattr(node, "parent", None)
|
||||
return node
|
||||
|
||||
|
||||
def is_main(parent: ast.AST | None) -> bool:
|
||||
match parent:
|
||||
case ast.If(test=ast.Compare(left=left, ops=[ast.Eq()], comparators=[comp])):
|
||||
values = {left, comp}
|
||||
mains = {x for x in values if isinstance(x, ast.Constant) and x.value == "__main__"}
|
||||
if len(mains) != 1:
|
||||
return False
|
||||
consts = {x for x in values if isinstance(x, ast.Name) and x.id == "__name__"}
|
||||
return len(consts) == 1
|
||||
case _:
|
||||
return False
|
||||
|
||||
|
||||
class Analyzer(ast.NodeVisitor):
|
||||
@@ -19,15 +42,22 @@ class Analyzer(ast.NodeVisitor):
|
||||
super().visit(node)
|
||||
|
||||
def visit_keyword(self, node: ast.keyword) -> None:
|
||||
# Must not be nested except for if __name__ == "__main__"
|
||||
|
||||
self.generic_visit(node)
|
||||
# Must not be nested in an if or other structure
|
||||
# This will be Module -> Expr -> Call -> keyword
|
||||
if (
|
||||
node.arg == "python_requires"
|
||||
and not hasattr(node.parent.parent.parent, "parent") # type: ignore[attr-defined]
|
||||
and isinstance(node.value, ast.Constant)
|
||||
):
|
||||
self.requires_python = node.value.value
|
||||
parent = get_parent(node, 4)
|
||||
unnested = parent is None
|
||||
|
||||
# This will be Module -> If -> Expr -> Call -> keyword
|
||||
name_main_unnested = (
|
||||
parent is not None and get_parent(parent) is None and is_main(get_parent(node, 3))
|
||||
)
|
||||
|
||||
match node:
|
||||
case ast.keyword(arg="python_requires", value=ast.Constant(value=str() as version)):
|
||||
if unnested or name_main_unnested:
|
||||
self.requires_python = version
|
||||
|
||||
|
||||
def setup_py_python_requires(content: str) -> str | None:
|
||||
@@ -40,15 +70,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()
|
||||
@@ -62,3 +89,26 @@ def get_requires_python_str(package_dir: Path) -> str | None:
|
||||
return setup_py_python_requires(f2.read())
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_dependency_groups(
|
||||
pyproject_toml: dict[str, Any] | None, *groups: str
|
||||
) -> tuple[str, ...]:
|
||||
"""
|
||||
Get the packages in dependency-groups for a package.
|
||||
"""
|
||||
|
||||
if not groups:
|
||||
return ()
|
||||
|
||||
if pyproject_toml is None:
|
||||
msg = f"Didn't find a pyproject.toml, so can't read [dependency-groups] {groups!r} from it!"
|
||||
raise FileNotFoundError(msg)
|
||||
|
||||
try:
|
||||
dependency_groups_toml = pyproject_toml["dependency-groups"]
|
||||
except KeyError:
|
||||
msg = f"Didn't find [dependency-groups] in pyproject.toml, which is needed to resolve {groups!r}."
|
||||
raise KeyError(msg) from None
|
||||
|
||||
return dependency_groups.resolve(dependency_groups_toml, *groups)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import _cross_venv; _cross_venv.initialize()
|
||||
@@ -0,0 +1,87 @@
|
||||
# This module is copied into the site-packages directory of an Android build environment, and
|
||||
# activated via a .pth file when we want the environment to simulate Android.
|
||||
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def initialize() -> None:
|
||||
if not (host_triplet := os.environ.get("CIBW_HOST_TRIPLET")):
|
||||
return
|
||||
|
||||
# os ######################################################################
|
||||
def cross_os_uname() -> os.uname_result:
|
||||
return os.uname_result(
|
||||
(
|
||||
"Linux",
|
||||
"localhost",
|
||||
# The Linux kernel version and release are unlikely to be significant, but return
|
||||
# realistic values anyway (from an API level 24 emulator).
|
||||
"3.18.91+",
|
||||
"#1 SMP PREEMPT Tue Jan 9 20:35:43 UTC 2018",
|
||||
host_triplet.split("-")[0],
|
||||
)
|
||||
)
|
||||
|
||||
os.name = "posix"
|
||||
os.uname = cross_os_uname
|
||||
|
||||
# platform ################################################################
|
||||
#
|
||||
# We can't determine the user-visible Android version number from the API level, so return a
|
||||
# string which will work fine for display, but will fail to parse as a version number.
|
||||
def cross_android_ver(*args: Any, **kwargs: Any) -> platform.AndroidVer:
|
||||
return platform.AndroidVer(
|
||||
release=f"API level {cross_getandroidapilevel()}",
|
||||
api_level=cross_getandroidapilevel(),
|
||||
manufacturer="Google",
|
||||
model="sdk_gphone64",
|
||||
device="emu64",
|
||||
is_emulator=True,
|
||||
)
|
||||
|
||||
# platform.uname, platform.system etc. are all implemented in terms of platform.android_ver.
|
||||
platform.android_ver = cross_android_ver
|
||||
|
||||
# sys #####################################################################
|
||||
def cross_getandroidapilevel() -> int:
|
||||
api_level = sysconfig.get_config_var("ANDROID_API_LEVEL")
|
||||
assert isinstance(api_level, int)
|
||||
return api_level
|
||||
|
||||
# Some packages may recognize sys.cross_compiling from the crossenv tool.
|
||||
sys.cross_compiling = True # type: ignore[attr-defined]
|
||||
sys.getandroidapilevel = cross_getandroidapilevel # type: ignore[attr-defined]
|
||||
sys.implementation._multiarch = host_triplet # type: ignore[attr-defined]
|
||||
sys.platform = "android"
|
||||
|
||||
# Determine the abiflags from the sysconfigdata filename.
|
||||
sysconfigdata_path = next(Path(__file__).parent.glob("_sysconfigdata_*.py"))
|
||||
abiflags_match = re.match(r"_sysconfigdata_(.*?)_", sysconfigdata_path.name)
|
||||
assert abiflags_match is not None
|
||||
sys.abiflags = abiflags_match[1]
|
||||
|
||||
# sysconfig ###############################################################
|
||||
#
|
||||
# Load the sysconfigdata file, generating its name from sys.abiflags,
|
||||
# sys.platform, and sys.implementation._multiarch.
|
||||
sysconfig._init_config_vars() # type: ignore[attr-defined]
|
||||
|
||||
# We don't change the actual sys.base_prefix and base_exec_prefix, because that
|
||||
# could have unpredictable effects. Instead, we change the sysconfig variables
|
||||
# used by sysconfig.get_paths().
|
||||
vars = sysconfig.get_config_vars()
|
||||
try:
|
||||
host_prefix = vars["host_prefix"] # This variable was added in Python 3.14.
|
||||
except KeyError:
|
||||
host_prefix = vars["exec_prefix"]
|
||||
vars["installed_base"] = vars["installed_platbase"] = host_prefix
|
||||
|
||||
# sysconfig.get_platform, which determines the wheel tag, is implemented in terms of
|
||||
# sys.platform, sysconfig.get_config_var("ANDROID_API_LEVEL") (see localized_vars in
|
||||
# android.py), and os.uname.
|
||||
@@ -0,0 +1,11 @@
|
||||
--- a/android.py
|
||||
+++ b/android.py
|
||||
@@ -1021,7 +1021,7 @@ def main():
|
||||
context = parse_args()
|
||||
|
||||
# Set the CROSS_BUILD_DIR if an argument was provided
|
||||
- if context.cross_build_dir:
|
||||
+ if getattr(context, "cross_build_dir", None):
|
||||
global CROSS_BUILD_DIR
|
||||
CROSS_BUILD_DIR = context.cross_build_dir.resolve()
|
||||
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# CIBW_HOST_TRIPLET is set in the android_env to the Android target triplet.
|
||||
target = os.environ.get("CIBW_HOST_TRIPLET")
|
||||
|
||||
cmd_name = Path(sys.argv[0]).name
|
||||
|
||||
# Find the real command in PATH, excluding the current script's directory
|
||||
path_env = os.environ.get("PATH", "")
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
|
||||
paths = path_env.split(os.pathsep)
|
||||
# Filter out the script directory to avoid recursion
|
||||
filtered_paths = [p for p in paths if Path(p).resolve() != script_dir]
|
||||
filtered_path_env = os.pathsep.join(filtered_paths)
|
||||
|
||||
real_cmd = shutil.which(cmd_name, path=filtered_path_env)
|
||||
|
||||
if not real_cmd:
|
||||
sys.stderr.write(f"cibuildwheel: Error: Could not find system {cmd_name}\n")
|
||||
sys.exit(1)
|
||||
|
||||
# If we have a target (i.e. we are in the android_env), try to install it.
|
||||
if target:
|
||||
# Check if rustup is available to install the target
|
||||
rustup_path = shutil.which("rustup", path=filtered_path_env)
|
||||
|
||||
if rustup_path:
|
||||
try:
|
||||
# We call rustup to ensure the target is installed.
|
||||
subprocess.run(
|
||||
[rustup_path, "target", "add", target],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
sys.stderr.write(
|
||||
f"cibuildwheel: Error: Failed to install Rust target {target}: {e.stderr}\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Execute the real command
|
||||
os.execv(real_cmd, [real_cmd, *sys.argv[1:]])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,139 +1,249 @@
|
||||
[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 = "cp38-manylinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp313-manylinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ 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 = "cp315-manylinux_x86_64", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-manylinux_x86_64", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "pp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
|
||||
{ identifier = "cp313-manylinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ 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 = "cp315-manylinux_i686", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-manylinux_i686", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "pp311-manylinux_x86_64", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
|
||||
{ identifier = "gp311_242-manylinux_x86_64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" },
|
||||
{ identifier = "gp312_250-manylinux_x86_64", version = "3.12", path_str = "/opt/python/graalpy312-graalpy250_312_native" },
|
||||
{ identifier = "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 = "cp38-manylinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp313-manylinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ 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 = "cp315-manylinux_aarch64", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-manylinux_aarch64", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "cp38-manylinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp313-manylinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ 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 = "cp315-manylinux_ppc64le", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-manylinux_ppc64le", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "pp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
|
||||
{ identifier = "cp313-manylinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ 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 = "cp315-manylinux_s390x", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-manylinux_s390x", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "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 = "cp315-manylinux_armv7l", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-manylinux_armv7l", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "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 = "cp315-manylinux_riscv64", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-manylinux_riscv64", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "pp38-manylinux_i686", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
|
||||
{ identifier = "pp311-manylinux_aarch64", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
|
||||
{ identifier = "gp311_242-manylinux_aarch64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" },
|
||||
{ identifier = "gp312_250-manylinux_aarch64", version = "3.12", path_str = "/opt/python/graalpy312-graalpy250_312_native" },
|
||||
{ identifier = "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 = "cp38-musllinux_x86_64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "pp311-manylinux_i686", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
|
||||
{ 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 = "cp38-musllinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp313-musllinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ 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 = "cp315-musllinux_x86_64", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-musllinux_x86_64", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "cp38-musllinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp313-musllinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ 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 = "cp315-musllinux_i686", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-musllinux_i686", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "cp38-musllinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp313-musllinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ 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 = "cp315-musllinux_aarch64", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-musllinux_aarch64", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "cp38-musllinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
|
||||
{ identifier = "cp313-musllinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313" },
|
||||
{ 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 = "cp315-musllinux_ppc64le", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-musllinux_ppc64le", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "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 = "cp315-musllinux_s390x", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-musllinux_s390x", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "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 = "cp315-musllinux_armv7l", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-musllinux_armv7l", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
{ 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 = "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" },
|
||||
{ identifier = "cp315-musllinux_riscv64", version = "3.15", path_str = "/opt/python/cp315-cp315" },
|
||||
{ identifier = "cp315t-musllinux_riscv64", version = "3.15", path_str = "/opt/python/cp315-cp315t" },
|
||||
]
|
||||
|
||||
[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" },
|
||||
{ identifier = "cp39-macosx_x86_64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg" },
|
||||
{ identifier = "cp39-macosx_arm64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg" },
|
||||
{ identifier = "cp39-macosx_universal2", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg" },
|
||||
{ identifier = "cp310-macosx_x86_64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" },
|
||||
{ identifier = "cp310-macosx_arm64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" },
|
||||
{ identifier = "cp310-macosx_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.4/python-3.11.4-macos11.pkg" },
|
||||
{ identifier = "cp311-macosx_arm64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.4/python-3.11.4-macos11.pkg" },
|
||||
{ identifier = "cp311-macosx_universal2", version = "3.11", url = "https://www.python.org/ftp/python/3.11.4/python-3.11.4-macos11.pkg" },
|
||||
{ identifier = "cp312-macosx_x86_64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.0/python-3.12.0b3-macos11.pkg" },
|
||||
{ identifier = "cp312-macosx_arm64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.0/python-3.12.0b3-macos11.pkg" },
|
||||
{ identifier = "cp312-macosx_universal2", version = "3.12", url = "https://www.python.org/ftp/python/3.12.0/python-3.12.0b3-macos11.pkg" },
|
||||
{ identifier = "pp37-macosx_x86_64", version = "3.7", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.9-osx64.tar.bz2" },
|
||||
{ identifier = "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.12-macos_x86_64.tar.bz2" },
|
||||
{ identifier = "pp39-macosx_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.12-macos_arm64.tar.bz2" },
|
||||
{ identifier = "pp310-macosx_x86_64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_x86_64.tar.bz2" },
|
||||
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_arm64.tar.bz2" },
|
||||
{ identifier = "cp39-macosx_x86_64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg", sha256 = "351fe18f4fb03be7afac5e4012fc0a51345f43202af43ef620cf1eee5ee36578" },
|
||||
{ identifier = "cp39-macosx_arm64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg", sha256 = "351fe18f4fb03be7afac5e4012fc0a51345f43202af43ef620cf1eee5ee36578" },
|
||||
{ identifier = "cp39-macosx_universal2", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg", sha256 = "351fe18f4fb03be7afac5e4012fc0a51345f43202af43ef620cf1eee5ee36578" },
|
||||
{ identifier = "cp310-macosx_x86_64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg", sha256 = "767ed35ad688d28ea4494081ae96408a0318d0d5bb9ca0139d74d6247b231cfc" },
|
||||
{ identifier = "cp310-macosx_arm64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg", sha256 = "767ed35ad688d28ea4494081ae96408a0318d0d5bb9ca0139d74d6247b231cfc" },
|
||||
{ identifier = "cp310-macosx_universal2", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg", sha256 = "767ed35ad688d28ea4494081ae96408a0318d0d5bb9ca0139d74d6247b231cfc" },
|
||||
{ identifier = "cp311-macosx_x86_64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg", sha256 = "b6cfdee2571ca56ee895043ca1e7110fb78a878cee3eb0c21accb2de34d24b55" },
|
||||
{ identifier = "cp311-macosx_arm64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg", sha256 = "b6cfdee2571ca56ee895043ca1e7110fb78a878cee3eb0c21accb2de34d24b55" },
|
||||
{ identifier = "cp311-macosx_universal2", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg", sha256 = "b6cfdee2571ca56ee895043ca1e7110fb78a878cee3eb0c21accb2de34d24b55" },
|
||||
{ identifier = "cp312-macosx_x86_64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg", sha256 = "8373e58da4ea146b3eb1c1f9834f19a319440b6b679b06050b1f9ee3237aa8e4" },
|
||||
{ identifier = "cp312-macosx_arm64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg", sha256 = "8373e58da4ea146b3eb1c1f9834f19a319440b6b679b06050b1f9ee3237aa8e4" },
|
||||
{ identifier = "cp312-macosx_universal2", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg", sha256 = "8373e58da4ea146b3eb1c1f9834f19a319440b6b679b06050b1f9ee3237aa8e4" },
|
||||
{ identifier = "cp313-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.13/python-3.13.13-macos11.pkg", sha256 = "a909cb655af5db67d5a90b3603437a1d58bec3446d624e4034e278ac62023cc9" },
|
||||
{ identifier = "cp313-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.13/python-3.13.13-macos11.pkg", sha256 = "a909cb655af5db67d5a90b3603437a1d58bec3446d624e4034e278ac62023cc9" },
|
||||
{ identifier = "cp313-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.13/python-3.13.13-macos11.pkg", sha256 = "a909cb655af5db67d5a90b3603437a1d58bec3446d624e4034e278ac62023cc9" },
|
||||
{ identifier = "cp314-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||
{ identifier = "cp314-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||
{ identifier = "cp314-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||
{ identifier = "cp314t-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||
{ identifier = "cp314t-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||
{ identifier = "cp314t-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||
{ identifier = "cp315-macosx_x86_64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||
{ identifier = "cp315-macosx_arm64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||
{ identifier = "cp315-macosx_universal2", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||
{ identifier = "cp315t-macosx_x86_64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||
{ identifier = "cp315t-macosx_arm64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||
{ identifier = "cp315t-macosx_universal2", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||
{ identifier = "pp39-macosx_x86_64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_x86_64.tar.bz2", sha256 = "fda015431621e7e5aa16359d114f2c45a77ed936992c1efff86302e768a6b21c" },
|
||||
{ identifier = "pp39-macosx_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_arm64.tar.bz2", sha256 = "88f824e7a2d676440d09bc90fc959ae0fd3557d7e2f14bfbbe53d41d159a47fe" },
|
||||
{ identifier = "pp310-macosx_x86_64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_x86_64.tar.bz2", sha256 = "2c5e5c2a33ac882551d7f28b98d19d486b8995aa50824a73b4edcc6aeec35c63" },
|
||||
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_arm64.tar.bz2", sha256 = "3db8a03fc496164801646844d7f3b12baa0adb3ad9a0b7cb124521bc2e168a36" },
|
||||
{ identifier = "pp311-macosx_x86_64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.23-macos_x86_64.tar.bz2", sha256 = "c95363c4e87235d11a6cec8128239c291b1eb67a752778fbcfe029a71da82b5e" },
|
||||
{ identifier = "pp311-macosx_arm64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.23-macos_arm64.tar.bz2", sha256 = "4747b3aceba4c1c6104cddc0fe5ea302101d32955f0957347b9ecc4fbd7aed05" },
|
||||
{ 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", sha256 = "" },
|
||||
{ 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", sha256 = "f4a2ae01bae0fa53ec0d19f86d73c6dcc2a162d245552030183b84bfdd8f7635" },
|
||||
{ identifier = "gp312_250-macosx_x86_64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.1/graalpy-25.0.1-macos-amd64.tar.gz", sha256 = "" },
|
||||
{ identifier = "gp312_250-macosx_arm64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.3/graalpy-25.0.3-macos-aarch64.tar.gz", sha256 = "387d9f5b376860842bea4d55aae5820974f3f3b68fc36c77ae863a061a888857" },
|
||||
]
|
||||
|
||||
[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.4", arch = "32" },
|
||||
{ identifier = "cp311-win_amd64", version = "3.11.4", arch = "64" },
|
||||
{ identifier = "cp312-win32", version = "3.12.0-b3", arch = "32" },
|
||||
{ identifier = "cp312-win_amd64", version = "3.12.0-b3", arch = "64" },
|
||||
{ identifier = "cp39-win_arm64", version = "3.9.10", arch = "ARM64" },
|
||||
{ identifier = "cp310-win_arm64", version = "3.10.11", arch = "ARM64" },
|
||||
{ identifier = "cp311-win_arm64", version = "3.11.4", arch = "ARM64" },
|
||||
{ identifier = "cp312-win_arm64", version = "3.12.0-b3", arch = "ARM64" },
|
||||
{ identifier = "pp37-win_amd64", version = "3.7", arch = "64", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.9-win64.zip" },
|
||||
{ identifier = "pp38-win_amd64", version = "3.8", arch = "64", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip" },
|
||||
{ identifier = "pp39-win_amd64", version = "3.9", arch = "64", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.12-win64.zip" },
|
||||
{ identifier = "pp310-win_amd64", version = "3.10", arch = "64", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.12-win64.zip" },
|
||||
{ identifier = "cp39-win32", version = "3.9.13" },
|
||||
{ identifier = "cp39-win_amd64", version = "3.9.13" },
|
||||
{ identifier = "cp310-win32", version = "3.10.11" },
|
||||
{ identifier = "cp310-win_amd64", version = "3.10.11" },
|
||||
{ identifier = "cp311-win32", version = "3.11.9" },
|
||||
{ identifier = "cp311-win_amd64", version = "3.11.9" },
|
||||
{ identifier = "cp312-win32", version = "3.12.10" },
|
||||
{ identifier = "cp312-win_amd64", version = "3.12.10" },
|
||||
{ identifier = "cp313-win32", version = "3.13.13" },
|
||||
{ identifier = "cp313-win_amd64", version = "3.13.13" },
|
||||
{ identifier = "cp314-win32", version = "3.14.5" },
|
||||
{ identifier = "cp314t-win32", version = "3.14.5" },
|
||||
{ identifier = "cp314-win_amd64", version = "3.14.5" },
|
||||
{ identifier = "cp314t-win_amd64", version = "3.14.5" },
|
||||
{ identifier = "cp315-win32", version = "3.15.0-b1" },
|
||||
{ identifier = "cp315t-win32", version = "3.15.0-b1" },
|
||||
{ identifier = "cp315-win_amd64", version = "3.15.0-b1" },
|
||||
{ identifier = "cp315t-win_amd64", version = "3.15.0-b1" },
|
||||
{ identifier = "cp39-win_arm64", version = "3.9.10" },
|
||||
{ identifier = "cp310-win_arm64", version = "3.10.11" },
|
||||
{ identifier = "cp311-win_arm64", version = "3.11.9" },
|
||||
{ identifier = "cp312-win_arm64", version = "3.12.10" },
|
||||
{ identifier = "cp313-win_arm64", version = "3.13.13" },
|
||||
{ identifier = "cp314-win_arm64", version = "3.14.5" },
|
||||
{ identifier = "cp314t-win_arm64", version = "3.14.5" },
|
||||
{ identifier = "cp315-win_arm64", version = "3.15.0-b1" },
|
||||
{ identifier = "cp315t-win_arm64", version = "3.15.0-b1" },
|
||||
{ identifier = "pp39-win_amd64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip", sha256 = "06ec12a5e964dc0ad33e6f380185a4d295178dce6d6df512f508e7aee00a1323" },
|
||||
{ identifier = "pp310-win_amd64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-win64.zip", sha256 = "c0d07bba6c8fb4e5804f4a8b3f8ef07cc3d89f6ad1db42a45ffb9be60bbb7cc2" },
|
||||
{ identifier = "pp311-win_amd64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.23-win64.zip", sha256 = "948b8ea58dea5b9917210fe4afd242c788fbfaba1c3f1a25e696a404f703389a" },
|
||||
{ identifier = "gp311_242-win_amd64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-windows-amd64.zip", sha256 = "9606134284d4d95b2f9d69c3087cd3e9e488f46355b419f5e66588a3281df6a3" },
|
||||
{ identifier = "gp312_250-win_amd64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.3/graalpy-25.0.3-windows-amd64.zip", sha256 = "2ae5c42b93e08d9e106a017634a3442b272ccae6d08ace157244de0449e937d4" },
|
||||
]
|
||||
|
||||
[pyodide]
|
||||
python_configurations = [
|
||||
{ identifier = "cp312-pyodide_wasm32", version = "3.12", default_pyodide_version = "0.27.7", node_version = "v22", sha256 = "0c2e7db42efa5d1dac38b50f8b3d659a1e3885d0a233494831b8206281307d52" },
|
||||
{ identifier = "cp313-pyodide_wasm32", version = "3.13", default_pyodide_version = "0.29.4", node_version = "v22", sha256 = "a29fc4a076408a18fc29eb4b280f80c6dddc95c19514c874c29e590d0931c02a" },
|
||||
{ identifier = "cp314-pyodide_wasm32", version = "3.14", default_pyodide_version = "314.0.0a2", node_version = "v24", sha256 = "ac7bbcdf289ab3ae25621c436b1d013099ef6bec644ad0538dae5d65552c5c95" },
|
||||
]
|
||||
|
||||
[android]
|
||||
python_configurations = [
|
||||
{ identifier = "cp313-android_arm64_v8a", version = "3.13", url = "https://repo.maven.apache.org/maven2/com/chaquo/python/python/3.13.13/python-3.13.13-aarch64-linux-android.tar.gz", sha256 = "a21250c246b36eb704be096be51e40ab24cafa3ae1d7ca4c396e6bf780cf87cf" },
|
||||
{ identifier = "cp313-android_x86_64", version = "3.13", url = "https://repo.maven.apache.org/maven2/com/chaquo/python/python/3.13.13/python-3.13.13-x86_64-linux-android.tar.gz", sha256 = "b021b76df2c8d64e41c76d18a2d91f27652356bd932c06f9076bac1b664c0e37" },
|
||||
{ identifier = "cp314-android_arm64_v8a", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-aarch64-linux-android.tar.gz", sha256 = "f008321abf837fcaec569df143283ece0e764b18d8c75763200160553f906af1" },
|
||||
{ identifier = "cp314-android_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-x86_64-linux-android.tar.gz", sha256 = "36184e31464b2b959d92c81ded8d025834342cc080623bced282a2a9a57bd47c" },
|
||||
{ identifier = "cp315-android_arm64_v8a", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-aarch64-linux-android.tar.gz", sha256 = "9a25e5499d639d4f59bc766ad36c9eddd892232a9f9224c2fa4ef55dc1d49ff3" },
|
||||
{ identifier = "cp315-android_x86_64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-x86_64-linux-android.tar.gz", sha256 = "86f492e92340028c8c7efedde6760b40f5e645062d0ef17af0d0bf4d18b127f2" },
|
||||
]
|
||||
|
||||
[ios]
|
||||
python_configurations = [
|
||||
{ identifier = "cp313-ios_arm64_iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b13/Python-3.13-iOS-support.b13.tar.gz", sha256 = "d1f95f95137a4b91dc0cbe9b99ddfc0a78918dde883c7fbe7147074a5e715274" },
|
||||
{ identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b13/Python-3.13-iOS-support.b13.tar.gz", sha256 = "d1f95f95137a4b91dc0cbe9b99ddfc0a78918dde883c7fbe7147074a5e715274" },
|
||||
{ identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b13/Python-3.13-iOS-support.b13.tar.gz", sha256 = "d1f95f95137a4b91dc0cbe9b99ddfc0a78918dde883c7fbe7147074a5e715274" },
|
||||
{ identifier = "cp314-ios_arm64_iphoneos", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz", sha256 = "8e4123b543bf17fdae2e2c6c033434487752438431014eb12e6d833aa35927a8" },
|
||||
{ identifier = "cp314-ios_x86_64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz", sha256 = "8e4123b543bf17fdae2e2c6c033434487752438431014eb12e6d833aa35927a8" },
|
||||
{ identifier = "cp314-ios_arm64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz", sha256 = "8e4123b543bf17fdae2e2c6c033434487752438431014eb12e6d833aa35927a8" },
|
||||
{ identifier = "cp315-ios_arm64_iphoneos", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz", sha256 = "abd43fc362bb6e40a5233a499aa3fa96bf49306ca10b4cbcb27418778e429eb8" },
|
||||
{ identifier = "cp315-ios_x86_64_iphonesimulator", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz", sha256 = "abd43fc362bb6e40a5233a499aa3fa96bf49306ca10b4cbcb27418778e429eb8" },
|
||||
{ identifier = "cp315-ios_arm64_iphonesimulator", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz", sha256 = "abd43fc362bb6e40a5233a499aa3fa96bf49306ca10b4cbcb27418778e429eb8" },
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
auditwheel-emscripten==0.2.4
|
||||
# via pyodide-build
|
||||
build==1.5.0
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# pyodide-build
|
||||
certifi==2026.5.20
|
||||
# via requests
|
||||
charset-normalizer==3.4.7
|
||||
# via requests
|
||||
click==8.1.8
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.29.0
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
idna==3.16
|
||||
# via requests
|
||||
leb128==1.0.9
|
||||
# via auditwheel-emscripten
|
||||
markdown-it-py==4.2.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==26.2
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# build
|
||||
# pyodide-build
|
||||
# wheel
|
||||
pip==26.1.1
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# pyodide-build
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
pydantic==2.13.4
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-lock
|
||||
pydantic-core==2.46.4
|
||||
# via pydantic
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyodide-build==0.34.4
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
pyodide-cli==0.5.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
pyodide-lock==0.1.3
|
||||
# via pyodide-build
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
python-discovery==1.3.1
|
||||
# via virtualenv
|
||||
requests==2.34.2
|
||||
# via pyodide-build
|
||||
rich==15.0.0
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
ruamel-yaml==0.19.1
|
||||
# via pyodide-build
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.2
|
||||
# via pydantic
|
||||
urllib3==2.7.0
|
||||
# via requests
|
||||
virtualenv==21.3.3
|
||||
# via
|
||||
# build
|
||||
# pyodide-build
|
||||
wheel==0.47.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
@@ -0,0 +1,91 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
auditwheel-emscripten==0.2.4
|
||||
# via pyodide-build
|
||||
build==1.5.0
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# pyodide-build
|
||||
certifi==2026.5.20
|
||||
# via requests
|
||||
charset-normalizer==3.4.7
|
||||
# via requests
|
||||
click==8.1.8
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.29.0
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
idna==3.16
|
||||
# via requests
|
||||
leb128==1.0.9
|
||||
# via auditwheel-emscripten
|
||||
markdown-it-py==4.2.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==26.2
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# build
|
||||
# pyodide-build
|
||||
# wheel
|
||||
pip==26.1.1
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# pyodide-build
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
pydantic==2.13.4
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-lock
|
||||
pydantic-core==2.46.4
|
||||
# via pydantic
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyodide-build==0.34.4
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
pyodide-cli==0.5.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
pyodide-lock==0.1.3
|
||||
# via pyodide-build
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
python-discovery==1.3.1
|
||||
# via virtualenv
|
||||
requests==2.34.2
|
||||
# via pyodide-build
|
||||
rich==15.0.0
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
ruamel-yaml==0.19.1
|
||||
# via pyodide-build
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.2
|
||||
# via pydantic
|
||||
urllib3==2.7.0
|
||||
# via requests
|
||||
virtualenv==21.3.3
|
||||
# via
|
||||
# build
|
||||
# pyodide-build
|
||||
wheel==0.47.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
@@ -0,0 +1,91 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
auditwheel-emscripten==0.2.4
|
||||
# via pyodide-build
|
||||
build==1.5.0
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# pyodide-build
|
||||
certifi==2026.5.20
|
||||
# via requests
|
||||
charset-normalizer==3.4.7
|
||||
# via requests
|
||||
click==8.1.8
|
||||
# via
|
||||
# -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.29.0
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
idna==3.16
|
||||
# via requests
|
||||
leb128==1.0.9
|
||||
# via auditwheel-emscripten
|
||||
markdown-it-py==4.2.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==26.2
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# build
|
||||
# pyodide-build
|
||||
# wheel
|
||||
pip==26.1.1
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# pyodide-build
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
pydantic==2.13.4
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-lock
|
||||
pydantic-core==2.46.4
|
||||
# via pydantic
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyodide-build==0.34.4
|
||||
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
|
||||
pyodide-cli==0.5.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
pyodide-lock==0.1.3
|
||||
# via pyodide-build
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
python-discovery==1.3.1
|
||||
# via virtualenv
|
||||
requests==2.34.2
|
||||
# via pyodide-build
|
||||
rich==15.0.0
|
||||
# via
|
||||
# pyodide-build
|
||||
# pyodide-cli
|
||||
ruamel-yaml==0.19.1
|
||||
# via pyodide-build
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.2
|
||||
# via pydantic
|
||||
urllib3==2.7.0
|
||||
# via requests
|
||||
virtualenv==21.3.3
|
||||
# via
|
||||
# build
|
||||
# pyodide-build
|
||||
wheel==0.47.0
|
||||
# via
|
||||
# auditwheel-emscripten
|
||||
# pyodide-build
|
||||
@@ -1,28 +1,96 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.10
|
||||
#
|
||||
delocate==0.10.4
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
abi3audit==0.0.26
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.6
|
||||
abi3info==2025.11.29
|
||||
# via abi3audit
|
||||
altgraph==0.17.5
|
||||
# via macholib
|
||||
attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
# via requests-cache
|
||||
certifi==2026.5.20
|
||||
# via requests
|
||||
charset-normalizer==3.4.7
|
||||
# via requests
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delvewheel==1.12.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.12.2
|
||||
# via virtualenv
|
||||
packaging==23.1
|
||||
exceptiongroup==1.3.1
|
||||
# via cattrs
|
||||
filelock==3.29.0
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
idna==3.16
|
||||
# via
|
||||
# requests
|
||||
# url-normalize
|
||||
importlib-metadata==9.0.0
|
||||
# via build
|
||||
kaitaistruct==0.11
|
||||
# via abi3audit
|
||||
macholib==1.16.4
|
||||
# via delocate
|
||||
platformdirs==3.8.1
|
||||
markdown-it-py==4.2.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# build
|
||||
# delocate
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
python-discovery==1.3.1
|
||||
# via virtualenv
|
||||
typing-extensions==4.7.1
|
||||
# via delocate
|
||||
virtualenv==20.23.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.40.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.1.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==68.0.0
|
||||
requests==2.34.2
|
||||
# via
|
||||
# abi3audit
|
||||
# requests-cache
|
||||
requests-cache==1.3.2
|
||||
# via abi3audit
|
||||
rich==15.0.0
|
||||
# via abi3audit
|
||||
tomli==2.4.1
|
||||
# via build
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# cattrs
|
||||
# delocate
|
||||
# exceptiongroup
|
||||
# virtualenv
|
||||
url-normalize==3.0.0
|
||||
# via requests-cache
|
||||
urllib3==2.7.0
|
||||
# via
|
||||
# requests
|
||||
# requests-cache
|
||||
virtualenv==21.3.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
zipp==4.1.0
|
||||
# via importlib-metadata
|
||||
|
||||
@@ -1,28 +1,87 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.11
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.11
|
||||
#
|
||||
delocate==0.10.4
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
abi3audit==0.0.26
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.6
|
||||
abi3info==2025.11.29
|
||||
# via abi3audit
|
||||
altgraph==0.17.5
|
||||
# via macholib
|
||||
attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
# via requests-cache
|
||||
certifi==2026.5.20
|
||||
# via requests
|
||||
charset-normalizer==3.4.7
|
||||
# via requests
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delvewheel==1.12.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.12.2
|
||||
# via virtualenv
|
||||
packaging==23.1
|
||||
filelock==3.29.0
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
idna==3.16
|
||||
# via
|
||||
# requests
|
||||
# url-normalize
|
||||
kaitaistruct==0.11
|
||||
# via abi3audit
|
||||
macholib==1.16.4
|
||||
# via delocate
|
||||
platformdirs==3.8.1
|
||||
markdown-it-py==4.2.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# build
|
||||
# delocate
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows"
|
||||
pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows"
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
python-discovery==1.3.1
|
||||
# via virtualenv
|
||||
typing-extensions==4.7.1
|
||||
# via delocate
|
||||
virtualenv==20.23.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.40.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.1.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==68.0.0
|
||||
requests==2.34.2
|
||||
# via
|
||||
# abi3audit
|
||||
# requests-cache
|
||||
requests-cache==1.3.2
|
||||
# via abi3audit
|
||||
rich==15.0.0
|
||||
# via abi3audit
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# cattrs
|
||||
# delocate
|
||||
url-normalize==3.0.0
|
||||
# via requests-cache
|
||||
urllib3==2.7.0
|
||||
# via
|
||||
# requests
|
||||
# requests-cache
|
||||
virtualenv==21.3.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
@@ -1,28 +1,87 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.12
|
||||
#
|
||||
delocate==0.10.4
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
abi3audit==0.0.26
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.6
|
||||
abi3info==2025.11.29
|
||||
# via abi3audit
|
||||
altgraph==0.17.5
|
||||
# via macholib
|
||||
attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
# via requests-cache
|
||||
certifi==2026.5.20
|
||||
# via requests
|
||||
charset-normalizer==3.4.7
|
||||
# via requests
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delvewheel==1.12.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.12.2
|
||||
# via virtualenv
|
||||
packaging==23.1
|
||||
filelock==3.29.0
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
idna==3.16
|
||||
# via
|
||||
# requests
|
||||
# url-normalize
|
||||
kaitaistruct==0.11
|
||||
# via abi3audit
|
||||
macholib==1.16.4
|
||||
# via delocate
|
||||
platformdirs==3.8.1
|
||||
markdown-it-py==4.2.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# build
|
||||
# delocate
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows"
|
||||
pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows"
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
python-discovery==1.3.1
|
||||
# via virtualenv
|
||||
typing-extensions==4.7.1
|
||||
# via delocate
|
||||
virtualenv==20.23.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.40.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.1.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==68.0.0
|
||||
requests==2.34.2
|
||||
# via
|
||||
# abi3audit
|
||||
# requests-cache
|
||||
requests-cache==1.3.2
|
||||
# via abi3audit
|
||||
rich==15.0.0
|
||||
# via abi3audit
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# cattrs
|
||||
# delocate
|
||||
url-normalize==3.0.0
|
||||
# via requests-cache
|
||||
urllib3==2.7.0
|
||||
# via
|
||||
# requests
|
||||
# requests-cache
|
||||
virtualenv==21.3.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
abi3audit==0.0.26
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
abi3info==2025.11.29
|
||||
# via abi3audit
|
||||
altgraph==0.17.5
|
||||
# via macholib
|
||||
attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
# via requests-cache
|
||||
certifi==2026.5.20
|
||||
# via requests
|
||||
charset-normalizer==3.4.7
|
||||
# via requests
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delvewheel==1.12.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.29.0
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
idna==3.16
|
||||
# via
|
||||
# requests
|
||||
# url-normalize
|
||||
kaitaistruct==0.11
|
||||
# via abi3audit
|
||||
macholib==1.16.4
|
||||
# via delocate
|
||||
markdown-it-py==4.2.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# build
|
||||
# delocate
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
python-discovery==1.3.1
|
||||
# via virtualenv
|
||||
requests==2.34.2
|
||||
# via
|
||||
# abi3audit
|
||||
# requests-cache
|
||||
requests-cache==1.3.2
|
||||
# via abi3audit
|
||||
rich==15.0.0
|
||||
# via abi3audit
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# cattrs
|
||||
# delocate
|
||||
url-normalize==3.0.0
|
||||
# via requests-cache
|
||||
urllib3==2.7.0
|
||||
# via
|
||||
# requests
|
||||
# requests-cache
|
||||
virtualenv==21.3.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
@@ -0,0 +1,86 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
abi3audit==0.0.26
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
abi3info==2025.11.29
|
||||
# via abi3audit
|
||||
altgraph==0.17.5
|
||||
# via macholib
|
||||
attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
# via requests-cache
|
||||
certifi==2026.5.20
|
||||
# via requests
|
||||
charset-normalizer==3.4.7
|
||||
# via requests
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delvewheel==1.12.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.29.0
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
idna==3.16
|
||||
# via
|
||||
# requests
|
||||
# url-normalize
|
||||
kaitaistruct==0.11
|
||||
# via abi3audit
|
||||
macholib==1.16.4
|
||||
# via delocate
|
||||
markdown-it-py==4.2.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# build
|
||||
# delocate
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
python-discovery==1.3.1
|
||||
# via virtualenv
|
||||
requests==2.34.2
|
||||
# via
|
||||
# abi3audit
|
||||
# requests-cache
|
||||
requests-cache==1.3.2
|
||||
# via abi3audit
|
||||
rich==15.0.0
|
||||
# via abi3audit
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# cattrs
|
||||
# delocate
|
||||
url-normalize==3.0.0
|
||||
# via requests-cache
|
||||
urllib3==2.7.0
|
||||
# via
|
||||
# requests
|
||||
# requests-cache
|
||||
virtualenv==21.3.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
@@ -0,0 +1,86 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
abi3audit==0.0.26
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
abi3info==2025.11.29
|
||||
# via abi3audit
|
||||
altgraph==0.17.5
|
||||
# via macholib
|
||||
attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
# via requests-cache
|
||||
certifi==2026.5.20
|
||||
# via requests
|
||||
charset-normalizer==3.4.7
|
||||
# via requests
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delvewheel==1.12.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.29.0
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
idna==3.16
|
||||
# via
|
||||
# requests
|
||||
# url-normalize
|
||||
kaitaistruct==0.11
|
||||
# via abi3audit
|
||||
macholib==1.16.4
|
||||
# via delocate
|
||||
markdown-it-py==4.2.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# build
|
||||
# delocate
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
python-discovery==1.3.1
|
||||
# via virtualenv
|
||||
requests==2.34.2
|
||||
# via
|
||||
# abi3audit
|
||||
# requests-cache
|
||||
requests-cache==1.3.2
|
||||
# via abi3audit
|
||||
rich==15.0.0
|
||||
# via abi3audit
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# cattrs
|
||||
# delocate
|
||||
url-normalize==3.0.0
|
||||
# via requests-cache
|
||||
urllib3==2.7.0
|
||||
# via
|
||||
# requests
|
||||
# requests-cache
|
||||
virtualenv==21.3.3
|
||||
# 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.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.6
|
||||
# via virtualenv
|
||||
filelock==3.12.2
|
||||
# via virtualenv
|
||||
importlib-metadata==6.7.0
|
||||
# via virtualenv
|
||||
packaging==23.1
|
||||
# via delocate
|
||||
platformdirs==3.8.1
|
||||
# via virtualenv
|
||||
typing-extensions==4.7.1
|
||||
# via
|
||||
# delocate
|
||||
# importlib-metadata
|
||||
# platformdirs
|
||||
virtualenv==20.23.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.40.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.1.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==68.0.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
@@ -1,28 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.8
|
||||
#
|
||||
delocate==0.10.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.6
|
||||
# via virtualenv
|
||||
filelock==3.12.2
|
||||
# via virtualenv
|
||||
packaging==23.1
|
||||
# via delocate
|
||||
platformdirs==3.8.1
|
||||
# via virtualenv
|
||||
typing-extensions==4.7.1
|
||||
# via delocate
|
||||
virtualenv==20.23.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.40.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.1.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==68.0.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
@@ -1,28 +1,96 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.9
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.9
|
||||
#
|
||||
delocate==0.10.4
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
abi3audit==0.0.25
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.6
|
||||
abi3info==2025.4.29
|
||||
# via abi3audit
|
||||
altgraph==0.17.5
|
||||
# via macholib
|
||||
attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
build==1.4.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==25.3.0
|
||||
# via requests-cache
|
||||
certifi==2026.5.20
|
||||
# via requests
|
||||
charset-normalizer==3.4.7
|
||||
# via requests
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delvewheel==1.12.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.12.2
|
||||
# via virtualenv
|
||||
packaging==23.1
|
||||
exceptiongroup==1.3.1
|
||||
# via cattrs
|
||||
filelock==3.19.1
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
idna==3.16
|
||||
# via
|
||||
# requests
|
||||
# url-normalize
|
||||
importlib-metadata==8.7.1
|
||||
# via build
|
||||
kaitaistruct==0.11
|
||||
# via abi3audit
|
||||
macholib==1.16.4
|
||||
# via delocate
|
||||
platformdirs==3.8.1
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==25.0
|
||||
# via
|
||||
# abi3audit
|
||||
# build
|
||||
# delocate
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.0.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.4.0
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
python-discovery==1.3.1
|
||||
# via virtualenv
|
||||
typing-extensions==4.7.1
|
||||
# via delocate
|
||||
virtualenv==20.23.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.40.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.1.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==68.0.0
|
||||
requests==2.32.5
|
||||
# via
|
||||
# abi3audit
|
||||
# requests-cache
|
||||
requests-cache==1.2.1
|
||||
# via abi3audit
|
||||
rich==14.2.0
|
||||
# via abi3audit
|
||||
tomli==2.4.1
|
||||
# via build
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# cattrs
|
||||
# delocate
|
||||
# exceptiongroup
|
||||
# virtualenv
|
||||
url-normalize==2.2.1
|
||||
# via requests-cache
|
||||
urllib3==2.6.3
|
||||
# via
|
||||
# requests
|
||||
# requests-cache
|
||||
virtualenv==21.3.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
zipp==3.23.1
|
||||
# via importlib-metadata
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pip
|
||||
setuptools
|
||||
wheel
|
||||
build
|
||||
delocate
|
||||
delvewheel
|
||||
virtualenv
|
||||
abi3audit
|
||||
|
||||
@@ -1,28 +1,86 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# nox -s update_constraints-3.12
|
||||
#
|
||||
delocate==0.10.4
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# nox -s update_constraints
|
||||
abi3audit==0.0.26
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.3.6
|
||||
abi3info==2025.11.29
|
||||
# via abi3audit
|
||||
altgraph==0.17.5
|
||||
# via macholib
|
||||
attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
# via requests-cache
|
||||
certifi==2026.5.20
|
||||
# via requests
|
||||
charset-normalizer==3.4.7
|
||||
# via requests
|
||||
delocate==0.13.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
delvewheel==1.12.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
filelock==3.12.2
|
||||
# via virtualenv
|
||||
packaging==23.1
|
||||
filelock==3.29.0
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
idna==3.16
|
||||
# via
|
||||
# requests
|
||||
# url-normalize
|
||||
kaitaistruct==0.11
|
||||
# via abi3audit
|
||||
macholib==1.16.4
|
||||
# via delocate
|
||||
platformdirs==3.8.1
|
||||
markdown-it-py==4.2.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# build
|
||||
# delocate
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
python-discovery==1.3.1
|
||||
# via virtualenv
|
||||
typing-extensions==4.7.1
|
||||
# via delocate
|
||||
virtualenv==20.23.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
wheel==0.40.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==23.1.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
setuptools==68.0.0
|
||||
requests==2.34.2
|
||||
# via
|
||||
# abi3audit
|
||||
# requests-cache
|
||||
requests-cache==1.3.2
|
||||
# via abi3audit
|
||||
rich==15.0.0
|
||||
# via abi3audit
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# cattrs
|
||||
# delocate
|
||||
url-normalize==3.0.0
|
||||
# via requests-cache
|
||||
urllib3==2.7.0
|
||||
# via
|
||||
# requests
|
||||
# requests-cache
|
||||
virtualenv==21.3.3
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
|
||||
@@ -2,40 +2,55 @@
|
||||
build = "*"
|
||||
skip = ""
|
||||
test-skip = ""
|
||||
enable = []
|
||||
|
||||
archs = ["auto"]
|
||||
audit-requires = ["abi3audit"]
|
||||
audit-command = "abi3audit --strict --report {abi3_wheel}"
|
||||
build-frontend = "default"
|
||||
config-settings = {}
|
||||
dependency-versions = "pinned"
|
||||
environment = {}
|
||||
environment-pass = []
|
||||
build-verbosity = ""
|
||||
build-verbosity = 0
|
||||
|
||||
before-all = ""
|
||||
before-build = ""
|
||||
# TOML doesn't support explicit NULLs; use ["\u0000"] as a sentinel value.
|
||||
xbuild-tools = ["\u0000"]
|
||||
repair-wheel-command = ""
|
||||
|
||||
test-command = ""
|
||||
before-test = ""
|
||||
test-sources = []
|
||||
test-requires = []
|
||||
test-extras = []
|
||||
test-groups = []
|
||||
test-environment = {}
|
||||
test-runtime = {}
|
||||
|
||||
container-engine = "docker"
|
||||
|
||||
manylinux-x86_64-image = "manylinux2014"
|
||||
manylinux-i686-image = "manylinux2014"
|
||||
manylinux-aarch64-image = "manylinux2014"
|
||||
manylinux-ppc64le-image = "manylinux2014"
|
||||
manylinux-s390x-image = "manylinux2014"
|
||||
manylinux-pypy_x86_64-image = "manylinux2014"
|
||||
manylinux-pypy_i686-image = "manylinux2014"
|
||||
manylinux-pypy_aarch64-image = "manylinux2014"
|
||||
pyodide-version = ""
|
||||
|
||||
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 +60,11 @@ 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]
|
||||
repair-wheel-command = "delvewheel repair -w {dest_dir} -v {wheel}"
|
||||
|
||||
[tool.cibuildwheel.android]
|
||||
|
||||
[tool.cibuildwheel.ios]
|
||||
|
||||
[tool.cibuildwheel.pyodide]
|
||||
audit-command = ""
|
||||
|
||||
@@ -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>
|
||||
@@ -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.15</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,103 @@
|
||||
import shutil
|
||||
import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
|
||||
SITE_PACKAGE_PATH = Path(__file__).parent
|
||||
|
||||
###########################################################################
|
||||
# importlib module patches
|
||||
###########################################################################
|
||||
|
||||
|
||||
def patch_env_create(env):
|
||||
"""
|
||||
Patch the process of creating virtual environments to ensure that the cross
|
||||
environment modification files are also copied as part of environment
|
||||
creation.
|
||||
"""
|
||||
old_pip_env_create = env._PipBackend.create
|
||||
|
||||
def pip_env_create(self, path, *args, **kwargs):
|
||||
result = old_pip_env_create(self, path, *args, **kwargs)
|
||||
# Copy any _cross_*.pth or _cross_*.py file, plus the cross-platform
|
||||
# sysconfigdata module and sysconfig_vars JSON to the new environment.
|
||||
data_name = sysconfig._get_sysconfigdata_name()
|
||||
json_name = data_name.replace("_sysconfigdata", "_sysconfig_vars")
|
||||
for filename in [
|
||||
"_cross_venv.pth",
|
||||
"_cross_venv.py",
|
||||
f"_cross_{sys.implementation._multiarch.replace('-', '_')}.py",
|
||||
f"{data_name}.py",
|
||||
f"{json_name}.json",
|
||||
]:
|
||||
src = SITE_PACKAGE_PATH / filename
|
||||
target = Path(path) / src.relative_to(SITE_PACKAGE_PATH.parent.parent.parent)
|
||||
if not target.exists():
|
||||
shutil.copy(src, target)
|
||||
return result
|
||||
|
||||
env._PipBackend.create = pip_env_create
|
||||
|
||||
|
||||
# Import hook that patches the creation of virtual environments by `build`
|
||||
#
|
||||
# The approach used here is the same as the one used by virtualenv to patch
|
||||
# distutils (but without support for the older load_module API).
|
||||
# https://docs.python.org/3/library/importlib.html#setting-up-an-importer
|
||||
_BUILD_PATCH = ("build.env",)
|
||||
|
||||
|
||||
class _Finder:
|
||||
"""A meta path finder that allows patching the imported build modules."""
|
||||
|
||||
fullname = None
|
||||
|
||||
# lock[0] is threading.Lock(), but initialized lazily to avoid importing
|
||||
# threading very early at startup, because there are gevent-based
|
||||
# applications that need to be first to import threading by themselves.
|
||||
# See https://github.com/pypa/virtualenv/issues/1895 for details.
|
||||
lock = [] # noqa: RUF012
|
||||
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
if fullname in _BUILD_PATCH and self.fullname is None:
|
||||
# initialize lock[0] lazily
|
||||
if len(self.lock) == 0:
|
||||
import threading
|
||||
|
||||
lock = threading.Lock()
|
||||
# there is possibility that two threads T1 and T2 are
|
||||
# simultaneously running into find_spec, observing .lock as
|
||||
# empty, and further going into hereby initialization. However
|
||||
# due to the GIL, list.append() operation is atomic and this
|
||||
# way only one of the threads will "win" to put the lock
|
||||
# - that every thread will use - into .lock[0].
|
||||
# https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
|
||||
self.lock.append(lock)
|
||||
|
||||
from functools import partial
|
||||
from importlib.util import find_spec
|
||||
|
||||
with self.lock[0]:
|
||||
self.fullname = fullname
|
||||
try:
|
||||
spec = find_spec(fullname, path)
|
||||
if spec is not None:
|
||||
# https://www.python.org/dev/peps/pep-0451/#how-loading-will-work
|
||||
old = spec.loader.exec_module
|
||||
func = self.exec_module
|
||||
if old is not func:
|
||||
spec.loader.exec_module = partial(func, old)
|
||||
return spec
|
||||
finally:
|
||||
self.fullname = None
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def exec_module(old, module):
|
||||
old(module)
|
||||
if module.__name__ in _BUILD_PATCH:
|
||||
patch_env_create(module)
|
||||
|
||||
|
||||
sys.meta_path.insert(0, _Finder())
|
||||
@@ -0,0 +1,79 @@
|
||||
# A site package that turns a macOS virtual environment
|
||||
# into an arm64 iphoneos cross-platform virtual environment
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
###########################################################################
|
||||
# sys module patches
|
||||
###########################################################################
|
||||
sys.cross_compiling = True
|
||||
sys.platform = "ios"
|
||||
sys.implementation._multiarch = "arm64-iphoneos"
|
||||
sys.base_prefix = sysconfig._get_sysconfigdata()["prefix"]
|
||||
sys.base_exec_prefix = sysconfig._get_sysconfigdata()["prefix"]
|
||||
|
||||
###########################################################################
|
||||
# subprocess module patches
|
||||
###########################################################################
|
||||
subprocess._can_fork_exec = True
|
||||
|
||||
|
||||
###########################################################################
|
||||
# platform module patches
|
||||
###########################################################################
|
||||
|
||||
|
||||
def cross_system():
|
||||
return "iOS"
|
||||
|
||||
|
||||
def cross_uname():
|
||||
return platform.uname_result(
|
||||
system="iOS",
|
||||
node="build",
|
||||
release="13.0",
|
||||
version="",
|
||||
machine="arm64",
|
||||
)
|
||||
|
||||
|
||||
def cross_ios_ver(system="", release="", model="", is_simulator=False):
|
||||
if system == "":
|
||||
system = "iOS"
|
||||
if release == "":
|
||||
release = "13.0"
|
||||
if model == "":
|
||||
model = "iphoneos"
|
||||
|
||||
return platform.IOSVersionInfo(system, release, model, False)
|
||||
|
||||
|
||||
platform.system = cross_system
|
||||
platform.uname = cross_uname
|
||||
platform.ios_ver = cross_ios_ver
|
||||
|
||||
|
||||
###########################################################################
|
||||
# sysconfig module patches
|
||||
###########################################################################
|
||||
|
||||
|
||||
def cross_get_platform():
|
||||
return "ios-13.0-arm64-iphoneos"
|
||||
|
||||
|
||||
def cross_get_sysconfigdata_name():
|
||||
return "_sysconfigdata__ios_arm64-iphoneos"
|
||||
|
||||
|
||||
sysconfig.get_platform = cross_get_platform
|
||||
sysconfig._get_sysconfigdata_name = cross_get_sysconfigdata_name
|
||||
|
||||
# Ensure module-level values cached at time of import are updated.
|
||||
sysconfig._BASE_PREFIX = sys.base_prefix
|
||||
sysconfig._BASE_EXEC_PREFIX = sys.base_exec_prefix
|
||||
|
||||
# Force sysconfig data to be loaded (and cached).
|
||||
sysconfig._init_config_vars()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user