2929
3030SELF_DIR = Path (__file__ ).parent
3131REPO_ROOT = SELF_DIR .parent
32- VERSIONS_FILE = REPO_ROOT / "crates" / "uv-python" / "download-metadata.json"
32+ DOWNLOAD_METADATA_FILENAME = "download-metadata.json"
33+ VERSIONS_FILE = REPO_ROOT / "crates" / "uv-python" / DOWNLOAD_METADATA_FILENAME
3334PREFIXES = [
3435 "https://github.com/astral-sh/python-build-standalone/releases/download/" ,
3536 "https://downloads.python.org/pypy/" ,
3637]
38+ NONE_SPECIFIED = "NONE_SPECIFIED"
3739
3840
3941logging .basicConfig (
@@ -62,7 +64,7 @@ def sha256_checksum(file_path: Path) -> str:
6264 return hasher .hexdigest ()
6365
6466
65- def collect_metadata_from_git_history () -> List [Dict ]:
67+ def collect_metadata_from_git_history () -> List [Tuple [ str , Dict ] ]:
6668 """Collect all metadata entries from the history of the VERSIONS_FILE."""
6769 metadata = []
6870 try :
@@ -74,7 +76,7 @@ def collect_metadata_from_git_history() -> List[Dict]:
7476 blob = commit .tree / str (VERSIONS_FILE .relative_to (REPO_ROOT ))
7577 content = blob .data_stream .read ().decode ()
7678 data = json .loads (content )
77- metadata .extend (data .values ())
79+ metadata .extend (data .items ())
7880 except KeyError :
7981 logger .warning (
8082 f"File { VERSIONS_FILE } not found in commit { commit .hexsha } . Skipping."
@@ -102,33 +104,33 @@ def check_arch(entry, arch):
102104def match_version (entry , pattern ):
103105 """Checks whether pattern matches against the entries version."""
104106 vers = f"{ entry ['major' ]} .{ entry ['minor' ]} .{ entry ['patch' ]} "
105- if entry ["prerelease" ] != "" :
107+ if "prerelease" in entry and entry ["prerelease" ] != "" :
106108 vers += f"-{ entry ['prerelease' ]} "
107109 return pattern .match (vers ) is not None
108110
109111
110112def filter_metadata (
111- metadata : List [Dict ],
113+ metadata : List [Tuple [ str , Dict ] ],
112114 name : Optional [str ],
113115 arch : Optional [str ],
114116 os : Optional [str ],
115117 version : Optional [re .Pattern ],
116- ) -> List [Dict ]:
118+ ) -> List [Tuple [ str , Dict ] ]:
117119 """Filter the metadata based on name, architecture, and OS, ensuring unique URLs."""
118120 filtered = [
119121 entry
120122 for entry in metadata
121- if (not name or entry ["name" ] == name )
122- and (not arch or check_arch (entry ["arch" ], arch ))
123- and (not os or entry ["os" ] == os )
124- and (not version or match_version (entry , version ))
123+ if (not name or entry [1 ][ "name" ] == name )
124+ and (not arch or check_arch (entry [1 ][ "arch" ], arch ))
125+ and (not os or entry [1 ][ "os" ] == os )
126+ and (not version or match_version (entry [ 1 ] , version ))
125127 ]
126128 # Use a set to ensure unique URLs
127129 unique_urls = set ()
128130 unique_filtered = []
129131 for entry in filtered :
130- if entry ["url" ] not in unique_urls :
131- unique_urls .add (entry ["url" ])
132+ if entry [1 ][ "url" ] not in unique_urls :
133+ unique_urls .add (entry [1 ][ "url" ])
132134 unique_filtered .append (entry )
133135 return unique_filtered
134136
@@ -222,6 +224,28 @@ async def sem_download(url, sha256):
222224 return success_count , errors
223225
224226
227+ def update_mirror_url (mirror_url : str , filtered_metadata : List [Tuple [str , Dict ]]):
228+ """Updates the given filtered_metadata to point at the given mirror_url"""
229+ if not mirror_url .endswith ("/" ):
230+ mirror_url += "/"
231+ for entry in filtered_metadata :
232+ for prefix in PREFIXES :
233+ entry [1 ]["url" ] = entry [1 ]["url" ].replace (prefix , mirror_url )
234+
235+
236+ def write_metadata (
237+ save_metadata : str , target : str , filtered_metadata : List [Tuple [str , Dict ]]
238+ ):
239+ """Save the filtered_metadata to the given save_metadata path or to target if NONE_SPECIFIED"""
240+
241+ metadata_output = Path (target ) / DOWNLOAD_METADATA_FILENAME
242+ if save_metadata != NONE_SPECIFIED :
243+ metadata_output = Path (save_metadata )
244+ Path (metadata_output ).parent .mkdir (parents = True , exist_ok = True )
245+ with open (metadata_output , "w" ) as f :
246+ json .dump (dict (filtered_metadata ), f , indent = 2 )
247+
248+
225249def parse_arguments ():
226250 """Parse command-line arguments using argparse."""
227251 parser = argparse .ArgumentParser (description = "Download and mirror Python builds." )
@@ -247,6 +271,15 @@ def parse_arguments():
247271 default = SELF_DIR / "mirror" ,
248272 help = "Directory to store the downloaded files." ,
249273 )
274+ parser .add_argument (
275+ "--save-metadata" ,
276+ nargs = "?" ,
277+ const = NONE_SPECIFIED ,
278+ help = "File to save metadata. Defaults to 'target' if no value provided." ,
279+ )
280+ parser .add_argument (
281+ "--mirror-url" , help = "Saved metadata has URLs updated to this root path."
282+ )
250283 return parser .parse_args ()
251284
252285
@@ -258,13 +291,13 @@ def main():
258291 metadata = collect_metadata_from_git_history ()
259292 else :
260293 with open (VERSIONS_FILE ) as f :
261- metadata = list (json .load (f ).values ())
294+ metadata : List [ Tuple [ str , Dict ]] = list (json .load (f ).items ())
262295
263296 version = re .compile (args .version ) if args .version else None
264297 filtered_metadata = filter_metadata (
265298 metadata , args .name , args .arch , args .os , version
266299 )
267- urls = {(entry ["url" ], entry ["sha256" ]) for entry in filtered_metadata }
300+ urls = {(entry [1 ][ "url" ], entry [ 1 ] ["sha256" ]) for entry in filtered_metadata }
268301
269302 if not urls :
270303 logger .error ("No URLs found." )
@@ -281,8 +314,17 @@ def main():
281314 print ("Failed downloads:" )
282315 for url , error in errors :
283316 print (f"- { url } : { error } " )
317+
318+ example_mirror = f"file://{ target .absolute ()} "
319+ if args .mirror_url :
320+ update_mirror_url (args .mirror_url , filtered_metadata )
321+ example_mirror = args .mirror_url
322+
323+ if args .save_metadata is not None :
324+ write_metadata (args .save_metadata , args .target , filtered_metadata )
325+
284326 print (
285- f"Example usage: `UV_PYTHON_INSTALL_MIRROR='file:// { target . absolute () } ' uv python install 3.13`"
327+ f"Example usage: `UV_PYTHON_INSTALL_MIRROR='{ example_mirror } ' uv python install 3.13`"
286328 )
287329 except Exception as e :
288330 logger .error (f"Error during download: { e } " )
0 commit comments