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,463 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Download pre-built LLVM/Clang binaries for multiple platforms.
4
+
5
+ This script downloads official LLVM releases from GitHub and prepares them
6
+ for packaging with the clang-tool-chain Python package.
7
+ """
8
+
9
+ import argparse
10
+ import hashlib
11
+ import platform
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ import tarfile
16
+ import urllib.request
17
+ from pathlib import Path
18
+
19
+ # Import checksum database
20
+ # Note: This import may fail when running the script standalone (before package installation)
21
+ try:
22
+ from clang_tool_chain.checksums import get_checksum, has_checksum
23
+ except ImportError:
24
+ # Fallback when running standalone
25
+ def get_checksum(version: str, platform: str) -> str | None:
26
+ return None
27
+
28
+ def has_checksum(version: str, platform: str) -> bool:
29
+ return False
30
+
31
+ def format_platform_key(os_name: str, arch: str) -> str:
32
+ return f"{os_name}-{arch}"
33
+
34
+
35
+ # Default LLVM version to download
36
+ DEFAULT_VERSION = "21.1.5"
37
+
38
+ # Base URLs for LLVM releases
39
+ GITHUB_RELEASE_URL = "https://github.com/llvm/llvm-project/releases/download"
40
+
41
+ # Platform-specific binary URLs and filenames
42
+ BINARY_CONFIGS: dict[str, dict[str, str]] = {
43
+ "win-x86_64": {
44
+ "filename": "LLVM-{version}-win64.exe",
45
+ "url": f"{GITHUB_RELEASE_URL}/llvmorg-{{version}}/LLVM-{{version}}-win64.exe",
46
+ "type": "installer",
47
+ "alt_filename": "clang+llvm-{version}-x86_64-pc-windows-msvc.tar.xz",
48
+ "alt_url": f"{GITHUB_RELEASE_URL}/llvmorg-{{version}}/clang+llvm-{{version}}-x86_64-pc-windows-msvc.tar.xz",
49
+ },
50
+ "linux-x86_64": {
51
+ "filename": "LLVM-{version}-Linux-X64.tar.xz",
52
+ "url": f"{GITHUB_RELEASE_URL}/llvmorg-{{version}}/LLVM-{{version}}-Linux-X64.tar.xz",
53
+ "type": "archive",
54
+ },
55
+ "linux-aarch64": {
56
+ "filename": "LLVM-{version}-Linux-ARM64.tar.xz",
57
+ "url": f"{GITHUB_RELEASE_URL}/llvmorg-{{version}}/LLVM-{{version}}-Linux-ARM64.tar.xz",
58
+ "type": "archive",
59
+ },
60
+ "darwin-x86_64": {
61
+ "filename": "clang+llvm-{version}-x86_64-apple-darwin.tar.xz",
62
+ "url": f"{GITHUB_RELEASE_URL}/llvmorg-{{version}}/clang+llvm-{{version}}-x86_64-apple-darwin.tar.xz",
63
+ "type": "archive",
64
+ },
65
+ "darwin-arm64": {
66
+ "filename": "clang+llvm-{version}-arm64-apple-darwin.tar.xz",
67
+ "url": f"{GITHUB_RELEASE_URL}/llvmorg-{{version}}/clang+llvm-{{version}}-arm64-apple-darwin.tar.xz",
68
+ "type": "archive",
69
+ },
70
+ }
71
+
72
+
73
+ class BinaryDownloader:
74
+ """Download and extract LLVM binaries for different platforms."""
75
+
76
+ def __init__(self, version: str = DEFAULT_VERSION, output_dir: str = "downloads", verify_checksums: bool = True):
77
+ """
78
+ Initialize the downloader.
79
+
80
+ Args:
81
+ version: LLVM version to download (e.g., "21.1.5")
82
+ output_dir: Directory to store downloaded files
83
+ verify_checksums: Whether to verify SHA256 checksums after download
84
+ """
85
+ self.version = version
86
+ self.output_dir = Path(output_dir)
87
+ self.output_dir.mkdir(parents=True, exist_ok=True)
88
+ self.verify_checksums = verify_checksums
89
+
90
+ def compute_sha256(self, file_path: Path) -> str:
91
+ """
92
+ Compute SHA256 checksum of a file.
93
+
94
+ Args:
95
+ file_path: Path to the file
96
+
97
+ Returns:
98
+ SHA256 checksum as hex string
99
+ """
100
+ sha256_hash = hashlib.sha256()
101
+ with open(file_path, "rb") as f:
102
+ # Read file in chunks to handle large files
103
+ for byte_block in iter(lambda: f.read(4096), b""):
104
+ sha256_hash.update(byte_block)
105
+ return sha256_hash.hexdigest()
106
+
107
+ def verify_checksum(self, file_path: Path, expected_checksum: str | None = None) -> bool:
108
+ """
109
+ Verify SHA256 checksum of a downloaded file.
110
+
111
+ Args:
112
+ file_path: Path to the file to verify
113
+ expected_checksum: Expected SHA256 checksum (optional)
114
+
115
+ Returns:
116
+ True if checksum matches or verification is skipped, False otherwise
117
+ """
118
+ if not self.verify_checksums:
119
+ return True
120
+
121
+ if expected_checksum is None:
122
+ print(f"Note: No checksum provided for {file_path.name}, skipping verification")
123
+ return True
124
+
125
+ try:
126
+ print(f"Verifying checksum for {file_path.name}...")
127
+ actual_checksum = self.compute_sha256(file_path)
128
+
129
+ if actual_checksum.lower() == expected_checksum.lower():
130
+ print(f"✓ Checksum verified: {actual_checksum[:16]}...")
131
+ return True
132
+ else:
133
+ print("✗ Checksum mismatch!")
134
+ print(f" Expected: {expected_checksum}")
135
+ print(f" Actual: {actual_checksum}")
136
+ return False
137
+
138
+ except Exception as e:
139
+ print(f"Error verifying checksum: {e}")
140
+ return False
141
+
142
+ def download_file(
143
+ self, url: str, destination: Path, show_progress: bool = True, expected_checksum: str | None = None
144
+ ) -> bool:
145
+ """
146
+ Download a file from URL to destination.
147
+
148
+ Args:
149
+ url: URL to download from
150
+ destination: Path to save the file
151
+ show_progress: Whether to show download progress
152
+ expected_checksum: Expected SHA256 checksum for verification (optional)
153
+
154
+ Returns:
155
+ True if download was successful, False otherwise
156
+ """
157
+ try:
158
+ print(f"Downloading {url}...")
159
+
160
+ # Check if file already exists
161
+ if destination.exists():
162
+ print(f"File already exists: {destination}")
163
+ # Verify existing file's checksum if provided
164
+ if expected_checksum and self.verify_checksums:
165
+ if self.verify_checksum(destination, expected_checksum):
166
+ print("Existing file verified, skipping download.")
167
+ return True
168
+ else:
169
+ print("Existing file failed verification, re-downloading...")
170
+ else:
171
+ response = input("Overwrite? (y/n): ").lower()
172
+ if response != "y":
173
+ print("Skipping download.")
174
+ return True
175
+
176
+ # Download with progress reporting
177
+ def reporthook(blocknum: int, blocksize: int, totalsize: int) -> None:
178
+ if show_progress and totalsize > 0:
179
+ downloaded = blocknum * blocksize
180
+ percent = min(100, downloaded * 100 / totalsize)
181
+ mb_downloaded = downloaded / (1024 * 1024)
182
+ mb_total = totalsize / (1024 * 1024)
183
+ print(f"\rProgress: {percent:.1f}% ({mb_downloaded:.1f}/{mb_total:.1f} MB)", end="")
184
+
185
+ urllib.request.urlretrieve(url, destination, reporthook if show_progress else None)
186
+
187
+ if show_progress:
188
+ print() # New line after progress
189
+
190
+ print(f"Successfully downloaded to {destination}")
191
+
192
+ # Verify checksum if provided
193
+ if expected_checksum and not self.verify_checksum(destination, expected_checksum):
194
+ print("Warning: Downloaded file failed checksum verification")
195
+ print("The file may be corrupted or tampered with")
196
+ # Don't return False to allow continuation, but warn user
197
+ return False
198
+
199
+ return True
200
+
201
+ except Exception as e:
202
+ print(f"Error downloading {url}: {e}")
203
+ return False
204
+
205
+ def extract_archive(self, archive_path: Path, extract_dir: Path) -> bool:
206
+ """
207
+ Extract a tar.xz archive.
208
+
209
+ Args:
210
+ archive_path: Path to the archive file
211
+ extract_dir: Directory to extract to
212
+
213
+ Returns:
214
+ True if extraction was successful, False otherwise
215
+ """
216
+ try:
217
+ print(f"Extracting {archive_path}...")
218
+ extract_dir.mkdir(parents=True, exist_ok=True)
219
+
220
+ with tarfile.open(archive_path, "r:xz") as tar:
221
+ tar.extractall(extract_dir)
222
+
223
+ print(f"Successfully extracted to {extract_dir}")
224
+ return True
225
+
226
+ except Exception as e:
227
+ print(f"Error extracting {archive_path}: {e}")
228
+ return False
229
+
230
+ def extract_windows_installer(self, installer_path: Path, extract_dir: Path) -> bool:
231
+ """
232
+ Extract Windows .exe installer using 7zip or fallback method.
233
+
234
+ Args:
235
+ installer_path: Path to the .exe installer
236
+ extract_dir: Directory to extract to
237
+
238
+ Returns:
239
+ True if extraction was successful, False otherwise
240
+ """
241
+ try:
242
+ print(f"Extracting Windows installer {installer_path}...")
243
+ extract_dir.mkdir(parents=True, exist_ok=True)
244
+
245
+ # Try using 7zip if available
246
+ if shutil.which("7z"):
247
+ result = subprocess.run(
248
+ ["7z", "x", str(installer_path), f"-o{extract_dir}", "-y"],
249
+ capture_output=True,
250
+ text=True,
251
+ )
252
+ if result.returncode == 0:
253
+ print(f"Successfully extracted to {extract_dir}")
254
+ return True
255
+ else:
256
+ print(f"7zip extraction failed: {result.stderr}")
257
+
258
+ # Fallback: suggest manual extraction
259
+ print("Warning: Could not automatically extract Windows installer.")
260
+ print(f"Please manually extract {installer_path} to {extract_dir}")
261
+ print("You can use 7-Zip or run the installer in extract-only mode.")
262
+ return False
263
+
264
+ except Exception as e:
265
+ print(f"Error extracting {installer_path}: {e}")
266
+ return False
267
+
268
+ def download_platform(self, platform_key: str, expected_checksum: str | None = None) -> Path | None:
269
+ """
270
+ Download binaries for a specific platform.
271
+
272
+ Args:
273
+ platform_key: Platform identifier (e.g., "linux-x86_64")
274
+ expected_checksum: Optional SHA256 checksum to verify download
275
+
276
+ Returns:
277
+ Path to the extracted directory, or None if download failed
278
+ """
279
+ if platform_key not in BINARY_CONFIGS:
280
+ print(f"Unknown platform: {platform_key}")
281
+ print(f"Available platforms: {', '.join(BINARY_CONFIGS.keys())}")
282
+ return None
283
+
284
+ config = BINARY_CONFIGS[platform_key]
285
+ filename = config["filename"].format(version=self.version)
286
+ url = config["url"].format(version=self.version)
287
+
288
+ # Try to get checksum from database if not provided
289
+ if expected_checksum is None and self.verify_checksums:
290
+ # Normalize platform key for checksum lookup
291
+ # Convert platform_key format to checksum format
292
+ # e.g., "darwin-x86_64" -> "mac-x86_64", "linux-aarch64" -> "linux-arm64"
293
+ checksum_platform = platform_key
294
+ if platform_key.startswith("darwin-"):
295
+ checksum_platform = platform_key.replace("darwin-", "mac-")
296
+ if "aarch64" in platform_key:
297
+ checksum_platform = checksum_platform.replace("aarch64", "arm64")
298
+
299
+ db_checksum = get_checksum(self.version, checksum_platform)
300
+ if db_checksum:
301
+ print(f"Using checksum from database for {checksum_platform}")
302
+ expected_checksum = db_checksum
303
+ elif has_checksum(self.version, checksum_platform):
304
+ # Checksum exists but is empty (should not happen with current implementation)
305
+ print(f"Warning: Empty checksum found in database for {checksum_platform}")
306
+ else:
307
+ print(f"Note: No checksum available in database for {checksum_platform} version {self.version}")
308
+ print(" The file will be downloaded but cannot be automatically verified.")
309
+ print(" To add checksum verification, see: src/clang_tool_chain/checksums.py")
310
+
311
+ # Download the file
312
+ download_path = self.output_dir / filename
313
+ if not self.download_file(url, download_path, expected_checksum=expected_checksum):
314
+ # Try alternative URL if available
315
+ if "alt_url" in config and "alt_filename" in config:
316
+ print("Trying alternative download URL...")
317
+ filename = config["alt_filename"].format(version=self.version)
318
+ url = config["alt_url"].format(version=self.version)
319
+ download_path = self.output_dir / filename
320
+ if not self.download_file(url, download_path, expected_checksum=expected_checksum):
321
+ return None
322
+ else:
323
+ return None
324
+
325
+ # Extract the archive
326
+ extract_dir = self.output_dir / f"{platform_key}-extracted"
327
+
328
+ if config["type"] == "installer":
329
+ if not self.extract_windows_installer(download_path, extract_dir):
330
+ print(f"Note: You may need to manually extract {download_path}")
331
+ else:
332
+ if not self.extract_archive(download_path, extract_dir):
333
+ return None
334
+
335
+ return extract_dir
336
+
337
+ def download_all(self, platforms: list[str] | None = None) -> dict[str, Path | None]:
338
+ """
339
+ Download binaries for all or specified platforms.
340
+
341
+ Args:
342
+ platforms: List of platform keys to download, or None for all
343
+
344
+ Returns:
345
+ Dictionary mapping platform keys to extracted directory paths
346
+ """
347
+ if platforms is None:
348
+ platforms = list(BINARY_CONFIGS.keys())
349
+
350
+ results = {}
351
+ for platform_key in platforms:
352
+ print(f"\n{'='*60}")
353
+ print(f"Downloading {platform_key}")
354
+ print(f"{'='*60}\n")
355
+
356
+ extract_dir = self.download_platform(platform_key)
357
+ results[platform_key] = extract_dir
358
+
359
+ if extract_dir:
360
+ print(f"✓ {platform_key}: Success")
361
+ else:
362
+ print(f"✗ {platform_key}: Failed")
363
+
364
+ return results
365
+
366
+
367
+ def get_current_platform() -> str | None:
368
+ """
369
+ Detect the current platform and return its key.
370
+
371
+ Returns:
372
+ Platform key string, or None if platform not supported
373
+ """
374
+ system = platform.system().lower()
375
+ machine = platform.machine().lower()
376
+
377
+ if system == "windows":
378
+ return "win-x86_64"
379
+ elif system == "linux":
380
+ if machine in ("x86_64", "amd64"):
381
+ return "linux-x86_64"
382
+ elif machine in ("aarch64", "arm64"):
383
+ return "linux-aarch64"
384
+ elif system == "darwin":
385
+ if machine == "x86_64":
386
+ return "darwin-x86_64"
387
+ elif machine in ("arm64", "aarch64"):
388
+ return "darwin-arm64"
389
+
390
+ return None
391
+
392
+
393
+ def main() -> None:
394
+ """Main entry point for the download script."""
395
+ parser = argparse.ArgumentParser(description="Download pre-built LLVM/Clang binaries for multiple platforms")
396
+ parser.add_argument(
397
+ "--version",
398
+ default=DEFAULT_VERSION,
399
+ help=f"LLVM version to download (default: {DEFAULT_VERSION})",
400
+ )
401
+ parser.add_argument(
402
+ "--output",
403
+ default="downloads",
404
+ help="Output directory for downloaded files (default: downloads)",
405
+ )
406
+ parser.add_argument(
407
+ "--platform",
408
+ action="append",
409
+ choices=list(BINARY_CONFIGS.keys()),
410
+ help="Platform to download (can specify multiple times, default: all)",
411
+ )
412
+ parser.add_argument(
413
+ "--current-only",
414
+ action="store_true",
415
+ help="Only download binaries for the current platform",
416
+ )
417
+ parser.add_argument(
418
+ "--no-verify",
419
+ action="store_true",
420
+ help="Skip SHA256 checksum verification (not recommended)",
421
+ )
422
+
423
+ args = parser.parse_args()
424
+
425
+ # Determine which platforms to download
426
+ platforms = None
427
+ if args.current_only:
428
+ current_platform = get_current_platform()
429
+ if current_platform:
430
+ platforms = [current_platform]
431
+ print(f"Detected current platform: {current_platform}")
432
+ else:
433
+ print("Error: Could not detect current platform")
434
+ sys.exit(1)
435
+ elif args.platform:
436
+ platforms = args.platform
437
+
438
+ # Download binaries
439
+ downloader = BinaryDownloader(version=args.version, output_dir=args.output, verify_checksums=not args.no_verify)
440
+ results = downloader.download_all(platforms=platforms)
441
+
442
+ # Print summary
443
+ print(f"\n{'='*60}")
444
+ print("Download Summary")
445
+ print(f"{'='*60}\n")
446
+
447
+ success_count = sum(1 for path in results.values() if path is not None)
448
+ total_count = len(results)
449
+
450
+ for platform_key, extract_dir in results.items():
451
+ status = "✓ Success" if extract_dir else "✗ Failed"
452
+ print(f"{platform_key:20s} {status}")
453
+ if extract_dir:
454
+ print(f" Location: {extract_dir}")
455
+
456
+ print(f"\nTotal: {success_count}/{total_count} successful")
457
+
458
+ if success_count < total_count:
459
+ sys.exit(1)
460
+
461
+
462
+ if __name__ == "__main__":
463
+ main()