clang-tool-chain 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of clang-tool-chain might be problematic. Click here for more details.

@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Expand tar.zst archive created with hard links.
4
+
5
+ This script:
6
+ 1. Decompresses zstd archive
7
+ 2. Extracts tar (tar automatically restores hard links as regular files)
8
+ 3. Copies/moves binaries to target location
9
+
10
+ The tar format preserves hard links, but when extracted, they become
11
+ regular files (duplicates) which is what we want for distribution.
12
+ """
13
+
14
+ import shutil
15
+ import sys
16
+ import tarfile
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+
21
+ def expand_zst_archive(archive_path: Path | str, output_dir: Path | str, keep_hardlinks: bool = False) -> Path:
22
+ """
23
+ Expand a tar.zst archive.
24
+
25
+ Args:
26
+ archive_path: Path to .tar.zst file
27
+ output_dir: Directory to extract to
28
+ keep_hardlinks: If True, preserve hard links; if False, copy to separate files
29
+ """
30
+ try:
31
+ import zstandard as zstd
32
+ except ImportError:
33
+ print("Error: zstandard module not installed")
34
+ print("Install with: pip install zstandard")
35
+ sys.exit(1)
36
+
37
+ archive_path = Path(archive_path)
38
+ output_dir = Path(output_dir)
39
+
40
+ if not archive_path.exists():
41
+ print(f"Error: Archive not found: {archive_path}")
42
+ sys.exit(1)
43
+
44
+ print("=" * 70)
45
+ print("EXPANDING ARCHIVE")
46
+ print("=" * 70)
47
+ print(f"Archive: {archive_path}")
48
+ print(f"Output: {output_dir}")
49
+ print(f"Size: {archive_path.stat().st_size / (1024*1024):.2f} MB")
50
+ print()
51
+
52
+ # Step 1: Decompress zstd
53
+ print("Step 1: Decompressing zstd...")
54
+ import time
55
+
56
+ start = time.time()
57
+
58
+ with open(archive_path, "rb") as f:
59
+ compressed_data = f.read()
60
+
61
+ dctx = zstd.ZstdDecompressor()
62
+ tar_data = dctx.decompress(compressed_data)
63
+
64
+ elapsed = time.time() - start
65
+ print(
66
+ f" Decompressed {len(compressed_data) / (1024*1024):.2f} MB -> {len(tar_data) / (1024*1024):.2f} MB in {elapsed:.2f}s"
67
+ )
68
+
69
+ # Step 2: Extract tar
70
+ print("\nStep 2: Extracting tar archive...")
71
+ import io
72
+
73
+ tar_buffer = io.BytesIO(tar_data)
74
+
75
+ output_dir.mkdir(parents=True, exist_ok=True)
76
+
77
+ with tarfile.open(fileobj=tar_buffer, mode="r") as tar:
78
+ # Get list of members
79
+ members = tar.getmembers()
80
+ print(f" Archive contains {len(members)} items")
81
+
82
+ # Extract
83
+ tar.extractall(path=output_dir)
84
+
85
+ print(f" Extracted to: {output_dir}")
86
+
87
+ # Step 3: Check for hard links
88
+ print("\nStep 3: Analyzing extracted files...")
89
+
90
+ extracted_root = output_dir / "win_hardlinked"
91
+ if not extracted_root.exists():
92
+ # Find the actual extraction root
93
+ subdirs = list(output_dir.iterdir())
94
+ if len(subdirs) == 1 and subdirs[0].is_dir():
95
+ extracted_root = subdirs[0]
96
+
97
+ bin_dir = extracted_root / "bin"
98
+ if bin_dir.exists():
99
+ exe_files = list(bin_dir.glob("*.exe"))
100
+ print(f" Found {len(exe_files)} .exe files")
101
+
102
+ # Check if hard links were preserved
103
+ inode_to_files = {}
104
+ for exe_file in exe_files:
105
+ stat = exe_file.stat()
106
+ inode = stat.st_ino
107
+ nlink = stat.st_nlink
108
+
109
+ if inode not in inode_to_files:
110
+ inode_to_files[inode] = []
111
+ inode_to_files[inode].append((exe_file.name, nlink))
112
+
113
+ hardlink_count = sum(1 for files in inode_to_files.values() if len(files) > 1)
114
+
115
+ if hardlink_count > 0:
116
+ print(f" ✓ Hard links preserved: {hardlink_count} groups")
117
+
118
+ if not keep_hardlinks:
119
+ print("\n Converting hard links to independent files...")
120
+ convert_hardlinks_to_files(bin_dir, inode_to_files)
121
+ else:
122
+ print(" ✓ Files are independent copies")
123
+
124
+ print("\n" + "=" * 70)
125
+ print("EXTRACTION COMPLETE")
126
+ print("=" * 70)
127
+ print(f"Location: {extracted_root}")
128
+
129
+ return extracted_root
130
+
131
+
132
+ def convert_hardlinks_to_files(bin_dir: Path, inode_to_files: dict[int, list[Any]]) -> None:
133
+ """Convert hard links to independent file copies."""
134
+ import tempfile
135
+
136
+ for _inode, files in inode_to_files.items():
137
+ if len(files) <= 1:
138
+ continue # Not a hard link group
139
+
140
+ print(f"\n Processing hard link group ({len(files)} files):")
141
+
142
+ # Keep the first file as-is, copy the rest
143
+ first_file = bin_dir / files[0][0]
144
+
145
+ for filename, _nlink in files[1:]:
146
+ target_file = bin_dir / filename
147
+
148
+ # Copy to temp, delete original, move temp to original
149
+ # This breaks the hard link
150
+ with tempfile.NamedTemporaryFile(delete=False, dir=bin_dir) as tmp:
151
+ tmp_path = Path(tmp.name)
152
+
153
+ shutil.copy2(first_file, tmp_path)
154
+ target_file.unlink()
155
+ tmp_path.rename(target_file)
156
+
157
+ print(f" - {filename} (converted to independent file)")
158
+
159
+
160
+ def verify_extraction(extracted_dir: Path | str, original_dir: Path | str | None = None) -> bool:
161
+ """Verify extracted files."""
162
+ import hashlib
163
+
164
+ extracted_dir = Path(extracted_dir)
165
+ bin_dir = extracted_dir / "bin"
166
+
167
+ if not bin_dir.exists():
168
+ print(f"Warning: bin directory not found in {extracted_dir}")
169
+ return False
170
+
171
+ print("\n" + "=" * 70)
172
+ print("VERIFICATION")
173
+ print("=" * 70)
174
+
175
+ exe_files = sorted(bin_dir.glob("*.exe"))
176
+ print(f"Extracted {len(exe_files)} .exe files:")
177
+
178
+ hashes = {}
179
+ for exe_file in exe_files:
180
+ md5 = hashlib.md5()
181
+ with open(exe_file, "rb") as f:
182
+ for chunk in iter(lambda: f.read(8192), b""):
183
+ md5.update(chunk)
184
+
185
+ file_hash = md5.hexdigest()
186
+ hashes[exe_file.name] = file_hash
187
+ size_mb = exe_file.stat().st_size / (1024 * 1024)
188
+ print(f" {exe_file.name:<25} {size_mb:6.1f} MB {file_hash[:16]}...")
189
+
190
+ # Compare with original if provided
191
+ if original_dir:
192
+ print("\n" + "=" * 70)
193
+ print("COMPARING WITH ORIGINAL")
194
+ print("=" * 70)
195
+
196
+ original_dir = Path(original_dir)
197
+ original_bin = original_dir / "bin"
198
+
199
+ if not original_bin.exists():
200
+ print(f"Warning: Original bin directory not found: {original_bin}")
201
+ return False
202
+
203
+ all_match = True
204
+ for filename, extracted_hash in sorted(hashes.items()):
205
+ original_file = original_bin / filename
206
+
207
+ if not original_file.exists():
208
+ print(f" ✗ {filename}: NOT FOUND in original")
209
+ all_match = False
210
+ continue
211
+
212
+ # Calculate original hash
213
+ md5 = hashlib.md5()
214
+ with open(original_file, "rb") as f:
215
+ for chunk in iter(lambda: f.read(8192), b""):
216
+ md5.update(chunk)
217
+ original_hash = md5.hexdigest()
218
+
219
+ if original_hash == extracted_hash:
220
+ print(f" ✓ {filename}")
221
+ else:
222
+ print(f" ✗ {filename}: HASH MISMATCH")
223
+ print(f" Original: {original_hash}")
224
+ print(f" Extracted: {extracted_hash}")
225
+ all_match = False
226
+
227
+ print()
228
+ if all_match:
229
+ print("✅ ALL FILES MATCH ORIGINAL!")
230
+ else:
231
+ print("❌ SOME FILES DO NOT MATCH")
232
+
233
+ return all_match
234
+
235
+ return True
236
+
237
+
238
+ def main() -> None:
239
+ import argparse
240
+
241
+ parser = argparse.ArgumentParser(description="Expand tar.zst archive")
242
+ parser.add_argument("archive", help="Path to .tar.zst archive")
243
+ parser.add_argument("output_dir", help="Output directory")
244
+ parser.add_argument("--verify", help="Original directory to verify against")
245
+ parser.add_argument(
246
+ "--keep-hardlinks", action="store_true", help="Keep hard links instead of converting to independent files"
247
+ )
248
+
249
+ args = parser.parse_args()
250
+
251
+ extracted_root = expand_zst_archive(args.archive, args.output_dir, args.keep_hardlinks)
252
+
253
+ if args.verify:
254
+ verify_extraction(extracted_root, args.verify)
255
+ else:
256
+ verify_extraction(extracted_root)
257
+
258
+
259
+ if __name__ == "__main__":
260
+ main()
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Extract MinGW sysroot from LLVM-MinGW release.
4
+
5
+ This script downloads LLVM-MinGW and extracts only the sysroot directory
6
+ (x86_64-w64-mingw32/) which contains headers and libraries for GNU ABI support.
7
+ """
8
+
9
+ import argparse
10
+ import hashlib
11
+ import json
12
+ import shutil
13
+ import sys
14
+ import tarfile
15
+ import urllib.request
16
+ import zipfile
17
+ from pathlib import Path
18
+
19
+ # LLVM-MinGW version and download URLs
20
+ LLVM_MINGW_VERSION = "20251104" # Release date format (latest as of Nov 2024)
21
+ LLVM_VERSION = "21.1.5"
22
+
23
+ LLVM_MINGW_URLS = {
24
+ "x86_64": f"https://github.com/mstorsjo/llvm-mingw/releases/download/"
25
+ f"{LLVM_MINGW_VERSION}/llvm-mingw-{LLVM_MINGW_VERSION}-ucrt-x86_64.zip",
26
+ "arm64": f"https://github.com/mstorsjo/llvm-mingw/releases/download/"
27
+ f"{LLVM_MINGW_VERSION}/llvm-mingw-{LLVM_MINGW_VERSION}-ucrt-aarch64.zip",
28
+ }
29
+
30
+ # Expected SHA256 checksums (to be updated after first download)
31
+ CHECKSUMS = {
32
+ "x86_64": "TBD", # Update after first download
33
+ "arm64": "TBD", # Update after first download
34
+ }
35
+
36
+
37
+ def download_llvm_mingw(arch: str, output_dir: Path) -> Path:
38
+ """Download LLVM-MinGW release."""
39
+ url = LLVM_MINGW_URLS.get(arch)
40
+ if not url:
41
+ raise ValueError(f"Unsupported architecture: {arch}")
42
+
43
+ output_dir.mkdir(parents=True, exist_ok=True)
44
+ filename = Path(url).name
45
+ output_path = output_dir / filename
46
+
47
+ if output_path.exists():
48
+ print(f"Already downloaded: {output_path}")
49
+ return output_path
50
+
51
+ print(f"Downloading: {url}")
52
+ print(f"To: {output_path}")
53
+
54
+ try:
55
+ urllib.request.urlretrieve(url, output_path)
56
+ print(f"Downloaded: {output_path.stat().st_size / (1024*1024):.2f} MB")
57
+ except Exception as e:
58
+ print(f"Error downloading: {e}")
59
+ if output_path.exists():
60
+ output_path.unlink()
61
+ raise
62
+
63
+ return output_path
64
+
65
+
66
+ def extract_sysroot(archive_path: Path, extract_dir: Path, arch: str) -> Path:
67
+ """Extract only the sysroot directory from LLVM-MinGW."""
68
+ print(f"\nExtracting sysroot from: {archive_path}")
69
+
70
+ # Determine target triple based on architecture
71
+ if arch == "x86_64":
72
+ sysroot_name = "x86_64-w64-mingw32"
73
+ elif arch == "arm64":
74
+ sysroot_name = "aarch64-w64-mingw32"
75
+ else:
76
+ raise ValueError(f"Unknown architecture: {arch}")
77
+
78
+ # Extract entire archive first (LLVM-MinGW structure)
79
+ temp_extract = extract_dir / "temp"
80
+ temp_extract.mkdir(parents=True, exist_ok=True)
81
+
82
+ print("Extracting archive...")
83
+ try:
84
+ # Check if it's a ZIP or tar.xz archive
85
+ if archive_path.suffix == ".zip":
86
+ with zipfile.ZipFile(archive_path, "r") as zf:
87
+ zf.extractall(path=temp_extract)
88
+ else:
89
+ with tarfile.open(archive_path, "r:xz") as tar:
90
+ tar.extractall(path=temp_extract)
91
+ except Exception as e:
92
+ print(f"Error extracting archive: {e}")
93
+ raise
94
+
95
+ # Find the llvm-mingw root directory
96
+ llvm_mingw_root = None
97
+ for item in temp_extract.iterdir():
98
+ if item.is_dir() and item.name.startswith("llvm-mingw"):
99
+ llvm_mingw_root = item
100
+ break
101
+
102
+ if not llvm_mingw_root:
103
+ raise RuntimeError(f"Could not find llvm-mingw root directory in {temp_extract}")
104
+
105
+ print(f"Found LLVM-MinGW root: {llvm_mingw_root}")
106
+
107
+ # Copy sysroot directory
108
+ sysroot_src = llvm_mingw_root / sysroot_name
109
+ if not sysroot_src.exists():
110
+ raise RuntimeError(f"Sysroot not found: {sysroot_src}")
111
+
112
+ sysroot_dst = extract_dir / sysroot_name
113
+ print(f"Copying sysroot: {sysroot_src} -> {sysroot_dst}")
114
+
115
+ if sysroot_dst.exists():
116
+ shutil.rmtree(sysroot_dst)
117
+
118
+ shutil.copytree(sysroot_src, sysroot_dst, symlinks=True)
119
+
120
+ # Copy top-level include directory (contains C/C++ headers)
121
+ include_src = llvm_mingw_root / "include"
122
+ if include_src.exists():
123
+ include_dst = extract_dir / "include"
124
+ print(f"Copying headers: {include_src} -> {include_dst}")
125
+ if include_dst.exists():
126
+ shutil.rmtree(include_dst)
127
+ shutil.copytree(include_src, include_dst, symlinks=True)
128
+
129
+ # Also copy generic headers if they exist
130
+ generic_headers = llvm_mingw_root / "generic-w64-mingw32"
131
+ if generic_headers.exists():
132
+ generic_dst = extract_dir / "generic-w64-mingw32"
133
+ print(f"Copying generic headers: {generic_headers} -> {generic_dst}")
134
+ if generic_dst.exists():
135
+ shutil.rmtree(generic_dst)
136
+ shutil.copytree(generic_headers, generic_dst, symlinks=True)
137
+
138
+ # Copy clang resource headers (mm_malloc.h, intrinsics, etc.)
139
+ # These are compiler builtin headers needed for compilation
140
+ clang_resource_src = llvm_mingw_root / "lib" / "clang"
141
+ if clang_resource_src.exists():
142
+ # Find the version directory (e.g., "21")
143
+ version_dirs = [d for d in clang_resource_src.iterdir() if d.is_dir()]
144
+ if version_dirs:
145
+ clang_version_dir = version_dirs[0] # Should only be one
146
+ resource_include_src = clang_version_dir / "include"
147
+ if resource_include_src.exists():
148
+ # Copy to lib/clang/<version>/include in sysroot
149
+ resource_dst = extract_dir / "lib" / "clang" / clang_version_dir.name / "include"
150
+ print(f"Copying clang resource headers: {resource_include_src} -> {resource_dst}")
151
+ resource_dst.parent.mkdir(parents=True, exist_ok=True)
152
+ if resource_dst.exists():
153
+ shutil.rmtree(resource_dst)
154
+ shutil.copytree(resource_include_src, resource_dst, symlinks=True)
155
+ print(f"Copied {len(list(resource_dst.glob('*.h')))} resource headers")
156
+
157
+ # Copy compiler-rt runtime libraries (libclang_rt.builtins.a, etc.)
158
+ # These are needed for linking to provide runtime support functions
159
+ resource_lib_src = clang_version_dir / "lib"
160
+ if resource_lib_src.exists():
161
+ resource_lib_dst = extract_dir / "lib" / "clang" / clang_version_dir.name / "lib"
162
+ print(f"Copying compiler-rt libraries: {resource_lib_src} -> {resource_lib_dst}")
163
+ resource_lib_dst.parent.mkdir(parents=True, exist_ok=True)
164
+ if resource_lib_dst.exists():
165
+ shutil.rmtree(resource_lib_dst)
166
+ shutil.copytree(resource_lib_src, resource_lib_dst, symlinks=True)
167
+
168
+ # Count library files
169
+ lib_count = len(list(resource_lib_dst.glob("**/*.a")))
170
+ print(f"Copied {lib_count} compiler-rt library files")
171
+
172
+ # Clean up temp directory
173
+ print("Cleaning up temporary files...")
174
+ shutil.rmtree(temp_extract)
175
+
176
+ print(f"\n✓ Sysroot extracted to: {sysroot_dst}")
177
+ return sysroot_dst
178
+
179
+
180
+ def create_archive(sysroot_dir: Path, output_dir: Path, arch: str) -> Path:
181
+ """Create compressed archive of sysroot."""
182
+ try:
183
+ import pyzstd
184
+ except ImportError:
185
+ print("Error: pyzstd module not installed")
186
+ print("Install with: pip install pyzstd")
187
+ sys.exit(1)
188
+
189
+ archive_name = f"mingw-sysroot-{LLVM_VERSION}-win-{arch}.tar.zst"
190
+ archive_path = output_dir / archive_name
191
+
192
+ print(f"\nCreating archive: {archive_path}")
193
+
194
+ # Create tar archive in memory, then compress
195
+ import io
196
+
197
+ tar_buffer = io.BytesIO()
198
+
199
+ with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
200
+ # Determine what to archive
201
+ sysroot_name = "x86_64-w64-mingw32" if arch == "x86_64" else "aarch64-w64-mingw32"
202
+
203
+ sysroot_path = sysroot_dir.parent / sysroot_name
204
+ include_path = sysroot_dir.parent / "include"
205
+ generic_path = sysroot_dir.parent / "generic-w64-mingw32"
206
+ lib_clang_path = sysroot_dir.parent / "lib" / "clang"
207
+
208
+ if sysroot_path.exists():
209
+ print(f"Adding to archive: {sysroot_name}/")
210
+ tar.add(sysroot_path, arcname=sysroot_name)
211
+
212
+ if include_path.exists():
213
+ print("Adding to archive: include/")
214
+ tar.add(include_path, arcname="include")
215
+
216
+ if generic_path.exists():
217
+ print("Adding to archive: generic-w64-mingw32/")
218
+ tar.add(generic_path, arcname="generic-w64-mingw32")
219
+
220
+ if lib_clang_path.exists():
221
+ print("Adding to archive: lib/clang/ (resource headers)")
222
+ tar.add(lib_clang_path, arcname="lib/clang")
223
+
224
+ tar_data = tar_buffer.getvalue()
225
+ tar_size = len(tar_data)
226
+ print(f"Tar size: {tar_size / (1024*1024):.2f} MB")
227
+
228
+ # Compress with zstd level 22
229
+ print("Compressing with zstd level 22...")
230
+ compressed_data = pyzstd.compress(tar_data, level_or_option=22)
231
+
232
+ with open(archive_path, "wb") as f:
233
+ f.write(compressed_data)
234
+
235
+ compressed_size = archive_path.stat().st_size
236
+ ratio = (1 - compressed_size / tar_size) * 100
237
+
238
+ print(f"Compressed size: {compressed_size / (1024*1024):.2f} MB")
239
+ print(f"Compression ratio: {ratio:.1f}%")
240
+
241
+ return archive_path
242
+
243
+
244
+ def generate_checksums(archive_path: Path) -> dict[str, str]:
245
+ """Generate SHA256 and MD5 checksums."""
246
+ print("\nGenerating checksums...")
247
+
248
+ sha256_hash = hashlib.sha256()
249
+ md5_hash = hashlib.md5()
250
+
251
+ with open(archive_path, "rb") as f:
252
+ while True:
253
+ chunk = f.read(8192)
254
+ if not chunk:
255
+ break
256
+ sha256_hash.update(chunk)
257
+ md5_hash.update(chunk)
258
+
259
+ checksums = {
260
+ "sha256": sha256_hash.hexdigest(),
261
+ "md5": md5_hash.hexdigest(),
262
+ }
263
+
264
+ # Write checksum files
265
+ sha256_file = Path(str(archive_path) + ".sha256")
266
+ md5_file = Path(str(archive_path) + ".md5")
267
+
268
+ with open(sha256_file, "w") as f:
269
+ f.write(f"{checksums['sha256']} {archive_path.name}\n")
270
+
271
+ with open(md5_file, "w") as f:
272
+ f.write(f"{checksums['md5']} {archive_path.name}\n")
273
+
274
+ print(f"SHA256: {checksums['sha256']}")
275
+ print(f"MD5: {checksums['md5']}")
276
+
277
+ return checksums
278
+
279
+
280
+ def main() -> None:
281
+ """Main entry point."""
282
+ parser = argparse.ArgumentParser(description="Extract MinGW sysroot from LLVM-MinGW release")
283
+ parser.add_argument("--arch", required=True, choices=["x86_64", "arm64"], help="Target architecture")
284
+ parser.add_argument(
285
+ "--work-dir",
286
+ type=Path,
287
+ default=Path("work"),
288
+ help="Working directory for downloads and extraction",
289
+ )
290
+ parser.add_argument(
291
+ "--output-dir",
292
+ type=Path,
293
+ default=Path("downloads-bins/assets/mingw/win"),
294
+ help="Output directory for final archives",
295
+ )
296
+
297
+ args = parser.parse_args()
298
+
299
+ work_dir = args.work_dir / args.arch
300
+ output_dir = args.output_dir / args.arch
301
+ output_dir.mkdir(parents=True, exist_ok=True)
302
+
303
+ print("=" * 70)
304
+ print("MINGW SYSROOT EXTRACTION")
305
+ print("=" * 70)
306
+ print(f"Architecture: {args.arch}")
307
+ print(f"Work directory: {work_dir}")
308
+ print(f"Output directory: {output_dir}")
309
+ print()
310
+
311
+ # Step 1: Download
312
+ archive_path = download_llvm_mingw(args.arch, work_dir)
313
+
314
+ # Step 2: Extract sysroot
315
+ sysroot_dir = extract_sysroot(archive_path, work_dir / "extracted", args.arch)
316
+
317
+ # Step 3: Create compressed archive
318
+ final_archive = create_archive(sysroot_dir, output_dir, args.arch)
319
+
320
+ # Step 4: Generate checksums
321
+ checksums = generate_checksums(final_archive)
322
+
323
+ # Step 5: Update manifest
324
+ manifest_path = output_dir / "manifest.json"
325
+ manifest_data = {
326
+ "latest": LLVM_VERSION,
327
+ "versions": {
328
+ LLVM_VERSION: {
329
+ "version": LLVM_VERSION,
330
+ "href": f"./mingw-sysroot-{LLVM_VERSION}-win-{args.arch}.tar.zst",
331
+ "sha256": checksums["sha256"],
332
+ }
333
+ },
334
+ }
335
+
336
+ with open(manifest_path, "w") as f:
337
+ json.dump(manifest_data, f, indent=2)
338
+
339
+ print(f"\n✓ Manifest written to: {manifest_path}")
340
+ print("\n" + "=" * 70)
341
+ print("COMPLETE")
342
+ print("=" * 70)
343
+ print(f"Archive: {final_archive}")
344
+ print(f"Size: {final_archive.stat().st_size / (1024*1024):.2f} MB")
345
+ print(f"SHA256: {checksums['sha256']}")
346
+
347
+
348
+ if __name__ == "__main__":
349
+ main()