1
1
#!/usr/bin/env python3
2
2
3
+ import logging
3
4
from itertools import groupby
4
5
from pathlib import Path
5
6
from typing import List
6
7
7
8
import click
8
9
import requests
10
+ import rich
9
11
import toml
12
+ from packaging .specifiers import SpecifierSet
10
13
from packaging .version import Version
14
+ from rich .logging import RichHandler
15
+ from rich .syntax import Syntax
11
16
12
17
from cibuildwheel .extra import InlineArrayDictEncoder
13
- from cibuildwheel .typing import PlatformName , TypedDict
18
+ from cibuildwheel .typing import Final , PlatformName , TypedDict
19
+
20
+ log = logging .getLogger ("cibw" )
14
21
15
22
# Looking up the dir instead of using utils.resources_dir
16
23
# since we want to write to it.
17
- DIR = Path (__file__ ).parent .parent .resolve ()
18
- RESOURCES_DIR = DIR / "cibuildwheel/resources"
24
+ DIR : Final [Path ] = Path (__file__ ).parent .parent .resolve ()
25
+ RESOURCES_DIR : Final [Path ] = DIR / "cibuildwheel/resources"
26
+
27
+ CIBW_SUPPORTED_PYTHONS : Final [SpecifierSet ] = SpecifierSet (">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" )
19
28
20
29
21
30
class AnyConfig (TypedDict ):
@@ -32,7 +41,12 @@ class ConfigWinPP(AnyConfig):
32
41
url : str
33
42
34
43
44
+ class ConfigMacOS (AnyConfig ):
45
+ url : str
46
+
47
+
35
48
def get_cpython_windows () -> List [ConfigWinCP ]:
49
+ log .info ("[bold]Collecting Windows CPython from nuget" )
36
50
ARCH_DICT = {"32" : "win32" , "64" : "win_amd64" }
37
51
38
52
response = requests .get ("https://api.nuget.org/v3/index.json" )
@@ -65,10 +79,12 @@ def get_cpython_windows() -> List[ConfigWinCP]:
65
79
arch = arch ,
66
80
)
67
81
)
82
+ log .debug (items [- 1 ])
68
83
return items
69
84
70
85
71
86
def get_pypy (platform : PlatformName ) -> List [AnyConfig ]:
87
+ log .info ("[bold]Collecting PyPy from python.org" )
72
88
73
89
response = requests .get ("https://downloads.python.org/pypy/versions.json" )
74
90
response .raise_for_status ()
@@ -98,11 +114,71 @@ def get_pypy(platform: PlatformName) -> List[AnyConfig]:
98
114
url = rf ["download_url" ],
99
115
)
100
116
)
117
+ log .debug (items [- 1 ])
118
+ break
119
+ elif platform == "macos" :
120
+ if rf ["platform" ] == "darwin" and rf ["arch" ] == "x64" :
121
+ identifier = f"pp{ version .major } { version .minor } -macosx_x86_64"
122
+ items .append (
123
+ ConfigMacOS (
124
+ identifier = identifier ,
125
+ version = Version (f"{ version .major } .{ version .minor } " ),
126
+ url = rf ["download_url" ],
127
+ )
128
+ )
129
+ log .debug (items [- 1 ])
101
130
break
102
131
103
132
return items
104
133
105
134
135
+ def _get_id (resource_uri : str ) -> int :
136
+ return int (resource_uri .rstrip ("/" ).split ("/" )[- 1 ])
137
+
138
+
139
+ def get_cpython (
140
+ plat_arch : str ,
141
+ file_ident : str ,
142
+ versions : SpecifierSet = CIBW_SUPPORTED_PYTHONS ,
143
+ ) -> List [ConfigMacOS ]:
144
+ log .info (f"[bold]Collecting { plat_arch } CPython from Python.org" )
145
+
146
+ response = requests .get ("https://www.python.org/api/v2/downloads/release/?is_published=true" )
147
+ response .raise_for_status ()
148
+
149
+ releases_info = response .json ()
150
+ # Removing the prefix, Python 3.9 would use: release["name"].removeprefix("Python ")
151
+ known_versions = {Version (release ["name" ][7 :]): _get_id (release ["resource_uri" ]) for release in releases_info }
152
+
153
+ items : List [ConfigMacOS ] = []
154
+
155
+ sorted_versions = sorted ((v for v in known_versions if versions .contains (v ) and not v .is_prerelease ), reverse = True )
156
+ # Group is a list of sorted patch versions
157
+ for pair , group in groupby (sorted_versions , lambda x : (x .major , x .minor )):
158
+ log .info (f"[bold]Working on { pair [0 ]} .{ pair [1 ]} " )
159
+ # Find the first patch version that contains the requested file
160
+ for version in group :
161
+ uri = known_versions [version ]
162
+
163
+ log .info (f" Checking { version } " )
164
+ response = requests .get (f"https://www.python.org/api/v2/downloads/release_file/?release={ uri } " )
165
+ response .raise_for_status ()
166
+ file_info = response .json ()
167
+
168
+ canidate_files = [rf ["url" ] for rf in file_info if file_ident in rf ["url" ]]
169
+ if canidate_files :
170
+ items .append (
171
+ ConfigMacOS (
172
+ identifier = f"cp{ version .major } { version .minor } -{ plat_arch } " ,
173
+ version = version ,
174
+ url = canidate_files [0 ],
175
+ )
176
+ )
177
+ log .info ("[green] Found!" )
178
+ break
179
+ return items
180
+
181
+
106
182
def sort_and_filter_configs (
107
183
orig_items : List [AnyConfig ],
108
184
* ,
@@ -154,28 +230,65 @@ def sort_and_filter_configs(
154
230
@click .command ()
155
231
@click .option ("--inplace" , is_flag = True )
156
232
@click .option ("--prereleases" , is_flag = True )
157
- @click .option ("--all" , is_flag = True )
158
- def update_pythons (inplace : bool , prereleases : bool , all : bool ) -> None :
233
+ @click .option ("--level" , default = "INFO" , type = click .Choice (["INFO" , "DEBUG" , "TRACE" ], case_sensitive = False ))
234
+ def update_pythons (inplace : bool , prereleases : bool , level : str ) -> None :
235
+
236
+ logging .basicConfig (
237
+ level = "INFO" ,
238
+ format = "%(message)s" ,
239
+ datefmt = "[%X]" ,
240
+ handlers = [RichHandler (rich_tracebacks = True , markup = True )],
241
+ )
242
+ log .setLevel (level )
243
+
159
244
windows_configs : List [AnyConfig ] = [
160
245
* CLASSIC_WINDOWS ,
161
246
* get_cpython_windows (),
162
247
* get_pypy ("windows" ),
163
248
]
164
249
165
- if not all :
166
- windows_configs = sort_and_filter_configs (
167
- windows_configs ,
168
- prereleases = prereleases ,
169
- )
250
+ windows_configs = sort_and_filter_configs (
251
+ windows_configs ,
252
+ prereleases = prereleases ,
253
+ )
254
+
255
+ macos_configs = [
256
+ * get_cpython (
257
+ plat_arch = "macosx_x86_64" ,
258
+ file_ident = "macosx10.9.pkg" ,
259
+ ),
260
+ * get_cpython (
261
+ plat_arch = "macosx_x86_64" ,
262
+ file_ident = "macosx10.6.pkg" ,
263
+ versions = SpecifierSet ("==3.5.*" ),
264
+ ),
265
+ * get_pypy ("macos" ),
266
+ ]
267
+
268
+ # For universal2:
269
+ # plat_arch="macosx_universal2",
270
+ # file_ident="macos11.0.pkg",
271
+ # versions=SpecifierSet(">=3.8"),
272
+
273
+ macos_configs = sort_and_filter_configs (
274
+ macos_configs ,
275
+ prereleases = prereleases ,
276
+ )
277
+
278
+ for config in macos_configs :
279
+ config ["version" ] = Version ("{0.major}.{0.minor}" .format (config ["version" ]))
170
280
171
281
configs = toml .load (RESOURCES_DIR / "build-platforms.toml" )
172
282
configs ["windows" ]["python_configurations" ] = windows_configs
283
+ configs ["macos" ]["python_configurations" ] = macos_configs
173
284
174
285
if inplace :
175
286
with open (RESOURCES_DIR / "build-platforms.toml" , "w" ) as f :
176
287
toml .dump (configs , f , encoder = InlineArrayDictEncoder ()) # type: ignore
177
288
else :
178
- print (toml .dumps (configs , encoder = InlineArrayDictEncoder ())) # type: ignore
289
+ output = toml .dumps (configs , encoder = InlineArrayDictEncoder ()) # type: ignore
290
+ rich .print (Syntax (output , "toml" , theme = "ansi_light" ))
291
+ log .info ("File not changed, use --inplace flag to update." )
179
292
180
293
181
294
if __name__ == "__main__" :
0 commit comments