#!/usr/bin/env python3 import _thread import atexit import json import lzma import os import platform import shutil import subprocess import sys import tarfile import tempfile import urllib.error import urllib.request import zipfile from pathlib import Path from typing import Any, Dict, List, Optional, Protocol, Tuple, cast from ci.util.resumable_downloader import ResumableDownloader class _SysConf(Protocol): def short_os_name_and_version(self) -> str: ... architecture: str import sys_detection from llvm_installer import LlvmInstaller _HERE = Path(__file__).parent _PROJECT_ROOT = _HERE.parent # Global download directory - lazily initialized _download_dir: Optional[Path] = None def _get_download_dir() -> Path: """Get or create the download directory lazily.""" global _download_dir if _download_dir is None: _download_dir = _PROJECT_ROOT / ".cache" / "downloads" _download_dir.mkdir(parents=True, exist_ok=True) print(f"Created download directory: {_download_dir}") # Register cleanup on exit as fallback atexit.register(_cleanup_download_dir) return _download_dir def _cleanup_download_dir() -> None: """Clean up the download directory.""" global _download_dir if _download_dir and _download_dir.exists(): try: shutil.rmtree(_download_dir) print(f"Cleaned up download directory: {_download_dir}") except Exception as e: print( f"Warning: Failed to clean up download directory {_download_dir}: {e}" ) _download_dir = None def _download(url: str, filename: str) -> Path: """Download to the managed download directory. Args: url: URL to download filename: Filename to save as (not full path) Returns: Path to the downloaded file """ download_dir = _get_download_dir() file_path = download_dir / filename print(f"download {url} to {file_path}") # Use resumable downloader for large files downloader = ResumableDownloader(chunk_size=1024 * 1024) # 1MB chunks try: downloader.download(url, file_path) except Exception as e: # Print download failure banner url_truncated = url[:100] banner_width = max(35, len(url_truncated) + 6) banner_line = "#" * banner_width print(f"\n{banner_line}") print( f"# DOWNLOAD FAILED FOR {url_truncated}{'...' if len(url) > 100 else ''} #" ) print(f"{banner_line}") print(f"Error: {e}") print() raise return file_path def is_linux_or_unix() -> Tuple[bool, str]: """Check if running on Linux or Unix-like system.""" system = platform.system().lower() if system == "linux": return True, "linux" elif system in ["darwin", "freebsd", "openbsd", "netbsd"]: return True, system else: return False, system def get_tool_version(tool_name: str) -> Optional[str]: """Get version of a tool if it exists on system PATH.""" try: result = subprocess.run( [tool_name, "--version"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: return result.stdout.strip() return None except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError): return None def get_clang_version_number(version_output: str) -> Optional[int]: """Extract major version number from clang version output.""" try: # Look for "clang version X.Y.Z" pattern import re match = re.search(r"clang version (\d+)\.", version_output) if match: return int(match.group(1)) return None except (ValueError, AttributeError): return None def find_tool_path(tool_name: str) -> Optional[Path]: """Find the full path to a tool if it exists.""" tool_path = shutil.which(tool_name) return Path(tool_path) if tool_path else None def check_required_tools() -> Tuple[ Dict[str, Optional[Path]], Dict[str, Optional[Path]] ]: """Check for essential and extra LLVM/Clang tools on system PATH. On Windows, prefer lld-link as the linker front-end. """ system_name = platform.system().lower() essential_tools: List[str] = [ "clang", "clang++", "llvm-ar", "llvm-nm", "llvm-objdump", "llvm-addr2line", "llvm-strip", "llvm-objcopy", ] # Linker tools differ across platforms if system_name == "windows": essential_tools.append("lld-link") else: essential_tools.append("lld") # Useful developer tools (not strictly required for building) extra_tools: List[str] = [ "clangd", "clang-format", "clang-tidy", "lldb", ] found_essential: Dict[str, Optional[Path]] = {} for tool in essential_tools: found_essential[tool] = find_tool_path(tool) found_extras: Dict[str, Optional[Path]] = {} for tool in extra_tools: found_extras[tool] = find_tool_path(tool) return found_essential, found_extras def create_hard_links(source_tools: Dict[str, Path], target_dir: Path) -> None: """Create hard links from system tools to target directory.""" bin_dir = target_dir / "bin" bin_dir.mkdir(parents=True, exist_ok=True) linked_count: int = 0 copied_count: int = 0 failed_tools: list[str] = [] for tool_name, source_path in source_tools.items(): if source_path and source_path.exists(): # Preserve original filename to keep extensions on Windows (e.g., .exe) target_filename = source_path.name target_path = bin_dir / target_filename try: # Remove existing file if it exists if target_path.exists(): target_path.unlink() # Create hard link target_path.hardlink_to(source_path) print(f"[OK] Linked {tool_name}: {source_path} -> {target_path}") linked_count += 1 except (OSError, PermissionError) as e: print(f"[WARN] Failed to link {tool_name}: {e}") # Fallback to copy if hard link fails try: shutil.copy2(source_path, target_path) target_path.chmod(0o755) print(f"[OK] Copied {tool_name}: {source_path} -> {target_path}") copied_count += 1 except (OSError, PermissionError): print(f"[ERROR] Failed to copy {tool_name}") failed_tools.append(tool_name) total_prepared: int = linked_count + copied_count print( f"Summary: prepared {total_prepared} tools (linked: {linked_count}, copied: {copied_count})" ) if failed_tools: print(f"ERROR: Failed to mirror tools: {', '.join(failed_tools)}") def find_tools_in_directories( tool_names: List[str], directories: List[Path] ) -> Dict[str, Optional[Path]]: """Search for tools across a list of directories, returning first matches. On Windows, will match executables with .exe suffix. """ found: Dict[str, Optional[Path]] = {} for tool in tool_names: found[tool] = None for directory in directories: if not directory.exists(): continue for tool in tool_names: if found[tool] is not None: continue candidate: Path = directory / tool if candidate.exists(): found[tool] = candidate continue # Try platform-specific suffix if platform.system().lower() == "windows": exe_candidate: Path = directory / f"{tool}.exe" if exe_candidate.exists(): found[tool] = exe_candidate return found def get_breadcrumb_file(target_dir: Path) -> Path: """Get the path to the installation completion breadcrumb file.""" cache_dir = target_dir / ".cache" / "cc" cache_dir.mkdir(parents=True, exist_ok=True) return cache_dir / "done.txt" def create_breadcrumb(target_dir: Path) -> None: """Create breadcrumb file to mark successful installation.""" breadcrumb = get_breadcrumb_file(target_dir) breadcrumb.write_text(f"Installation completed at {os.path.basename(target_dir)}\n") print(f"Created installation breadcrumb: {breadcrumb}") def check_existing_installation(target_dir: Path) -> Tuple[bool, bool]: """Check if there's an existing installation and if it's complete. Returns: (has_artifacts, has_breadcrumb) - bool tuple indicating installation state """ breadcrumb = get_breadcrumb_file(target_dir) has_breadcrumb = breadcrumb.exists() # Check for any installation artifacts bin_dir = target_dir / "bin" has_artifacts = bin_dir.exists() and any(bin_dir.iterdir()) return has_artifacts, has_breadcrumb def verify_clang_installation(target_dir: Path) -> bool: """Test if clang is properly installed in target directory.""" bin_dir = target_dir / "bin" clang_exe = ( bin_dir / "clang.exe" if platform.system().lower() == "windows" else bin_dir / "clang" ) if not clang_exe.exists(): return False # Test that clang can run and report version try: result = subprocess.run( [str(clang_exe), "--version"], capture_output=True, text=True, timeout=5 ) return result.returncode == 0 except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError): return False def get_windows_default_llvm_bins() -> List[Path]: """Return common Windows LLVM bin directories to probe.""" roots: List[Path] = [] program_files: Optional[str] = os.environ.get("ProgramFiles") program_files_x86: Optional[str] = os.environ.get("ProgramFiles(x86)") chocolatey_lib: Optional[str] = os.environ.get("ChocolateyInstall") scoop: Optional[str] = os.environ.get("SCOOP") if program_files: roots.append(Path(program_files) / "LLVM" / "bin") if program_files_x86: roots.append(Path(program_files_x86) / "LLVM" / "bin") if chocolatey_lib: roots.append(Path(chocolatey_lib) / "lib" / "llvm" / "tools" / "llvm" / "bin") if scoop: roots.append(Path(scoop) / "apps" / "llvm" / "current" / "bin") # Also consider PATH entries explicitly path_entries: List[str] = os.environ.get("PATH", "").split(os.pathsep) for entry in path_entries: # Only consider entries that look like LLVM bin directories if "llvm" in entry.lower() and "bin" in entry.lower(): roots.append(Path(entry)) return roots def _run_subprocess(args: List[str], timeout_seconds: int) -> tuple[int, str, str]: """Run a subprocess command and return (returncode, stdout, stderr).""" try: result = subprocess.run( args, capture_output=True, text=True, timeout=timeout_seconds, ) return result.returncode, result.stdout, result.stderr except subprocess.TimeoutExpired as e: return 124, "", f"Timeout after {timeout_seconds}s: {e}" except subprocess.SubprocessError as e: return 1, "", f"Subprocess error: {e}" def install_llvm_windows_direct(target_dir: Path) -> Path: """Download and extract official LLVM Windows tar.xz archive into target_dir. Returns the extracted LLVM bin directory path on success. Fails fast with a descriptive error on failure. """ # Prefer dynamically discovered releases; as fallback, try a small curated list preferred_versions: List[str] = [ "20.1.8", "20.1.7", "20.1.6", "19.1.8", "19.1.7", "19.1.6", ] download_errors: List[str] = [] # First, try to discover the latest available Windows tar.xz for majors 20 and 19 for probe_major in [20, 19]: print(f"Attempting discovery for LLVM major version {probe_major}...") discovered_url = discover_latest_github_llvm_win_zip(probe_major) if discovered_url is not None: print(f"Discovered URL for LLVM {probe_major}: {discovered_url}") # Put discovered URL at the front of attempts target_dir.mkdir(parents=True, exist_ok=True) extract_root: Path = target_dir tmp_archive_path: Optional[Path] = None try: print(f"Attempting to download and extract LLVM {probe_major}...") # Download to managed directory tmp_archive_path = _download( discovered_url, f"llvm-discovered-{probe_major}.tar.xz" ) extract_root = target_dir with lzma.open(tmp_archive_path, "rb") as xz_file: with tarfile.open(fileobj=xz_file, mode="r") as tar: tar.extractall(extract_root, filter="data") except KeyboardInterrupt: print("Operation interrupted by user") _thread.interrupt_main() raise except Exception as e: print(f"Failed to download/extract LLVM {probe_major}: {e}") download_errors.append(f"discovered-{probe_major}: {e}") finally: # Clean up the downloaded file immediately if tmp_archive_path and tmp_archive_path.exists(): try: tmp_archive_path.unlink() print(f"Cleaned up: {tmp_archive_path}") except Exception as e: print(f"Warning: Failed to clean up {tmp_archive_path}: {e}") extracted_bin: Optional[Path] = None for entry in extract_root.iterdir(): if not entry.is_dir(): continue candidate = entry / "bin" / "clang.exe" if candidate.exists(): extracted_bin = entry / "bin" break if extracted_bin is not None: print( f"SUCCESS: LLVM {probe_major} installed successfully via discovery!" ) print("OK: Successfully installed LLVM (Windows tar.xz, discovered)") print(f" Installation directory: {extracted_bin.parent}") print(f" Bin directory: {extracted_bin}") create_breadcrumb(target_dir) return extracted_bin else: print( f"WARNING: LLVM {probe_major} downloaded but clang.exe not found in expected location" ) else: print(f"No URL discovered for LLVM major version {probe_major}") # If discovery did not succeed, try curated versions list print("\n" + "=" * 60) print("Discovery failed for all major versions. Falling back to curated list...") print("=" * 60 + "\n") for version in preferred_versions: base_url = ( "https://github.com/llvm/llvm-project/releases/download/" f"llvmorg-{version}/clang+llvm-{version}-x86_64-pc-windows-msvc.tar.xz" ) print(f"Attempting direct LLVM download for Windows: {base_url}") try: target_dir.mkdir(parents=True, exist_ok=True) tmp_archive_path: Optional[Path] = None try: # Download to managed directory tmp_archive_path = _download(base_url, f"llvm-{version}.tar.xz") except KeyboardInterrupt: print("Operation interrupted by user") _thread.interrupt_main() raise except Exception as e: download_errors.append(f"{version}: {e}") # Clean up on download error if tmp_archive_path and tmp_archive_path.exists(): try: tmp_archive_path.unlink() print(f"Cleaned up after error: {tmp_archive_path}") except Exception: pass continue extract_root = target_dir try: with lzma.open(tmp_archive_path, "rb") as xz_file: with tarfile.open(fileobj=xz_file, mode="r") as tar: tar.extractall(extract_root, filter="data") finally: # Clean up the downloaded file immediately if tmp_archive_path and tmp_archive_path.exists(): try: tmp_archive_path.unlink() print(f"Cleaned up: {tmp_archive_path}") except Exception as e: print(f"Warning: Failed to clean up {tmp_archive_path}: {e}") # Find extracted directory that contains bin/clang.exe extracted_bin: Optional[Path] = None for entry in extract_root.iterdir(): if not entry.is_dir(): continue candidate = entry / "bin" / "clang.exe" if candidate.exists(): extracted_bin = entry / "bin" break if extracted_bin is None: print( "ERROR: LLVM downloaded but clang.exe not found in expected location" ) # Try next version continue print("OK: Successfully installed LLVM (Windows tar.xz)") print(f" Installation directory: {extracted_bin.parent}") print(f" Bin directory: {extracted_bin}") create_breadcrumb(target_dir) return extracted_bin except KeyboardInterrupt: print("Operation interrupted by user") _thread.interrupt_main() raise except Exception as e: download_errors.append(f"{version}: {e}") continue # If we reach here, all attempts failed print("CRITICAL: Failed to download LLVM for Windows from official releases.") if download_errors: for err in download_errors: print(f" - {err}") print("Please ensure internet access is available and retry.") sys.exit(1) def discover_latest_github_llvm_win_zip(major: int) -> Optional[str]: """Find the latest LLVM Windows tar.xz asset for a given major version via GitHub API. Returns a browser_download_url string if found, otherwise None. """ try: per_page: int = 50 page: int = 1 max_pages: int = 3 while page <= max_pages: api_url: str = ( "https://api.github.com/repos/llvm/llvm-project/releases" f"?per_page={per_page}&page={page}" ) req = urllib.request.Request( api_url, headers={"User-Agent": "fastled-setup-llvm"} ) with urllib.request.urlopen(req, timeout=20) as resp: raw: str = resp.read().decode("utf-8") releases_any: Any = json.loads(raw) releases: list[dict[str, Any]] = [] if isinstance(releases_any, list): tmp_list: list[Any] = cast(list[Any], releases_any) filtered: list[dict[str, Any]] = [] k: int = 0 while k < len(tmp_list): item: Any = tmp_list[k] k += 1 if isinstance(item, dict): filtered.append(cast(dict[str, Any], item)) releases = filtered # Iterate releases idx: int = 0 while idx < len(releases): rel: dict[str, Any] = releases[idx] idx += 1 tag_val: Any = rel.get("tag_name") tag_name: Optional[str] = tag_val if isinstance(tag_val, str) else None if tag_name is None: continue # Expect tags like llvmorg-19.1.6 prefix: str = f"llvmorg-{major}." if not tag_name.startswith(prefix): continue assets_val: Any = rel.get("assets") assets: list[dict[str, Any]] = ( cast(list[dict[str, Any]], assets_val) if isinstance(assets_val, list) else [] ) filtered_assets: list[dict[str, Any]] = [ a for a in assets if isinstance(a, dict) ] j: int = 0 while j < len(filtered_assets): asset: dict[str, Any] = filtered_assets[j] j += 1 name_val: Any = asset.get("name") url_val: Any = asset.get("browser_download_url") name: Optional[str] = ( name_val if isinstance(name_val, str) else None ) url: Optional[str] = url_val if isinstance(url_val, str) else None if name is not None and url is not None: # Match e.g., clang+llvm-19.1.6-x86_64-pc-windows-msvc.tar.xz if ( name.startswith(f"clang+llvm-{major}.") and "x86_64-pc-windows-msvc.tar.xz" in name ): return url page += 1 return None except urllib.error.URLError: return None def download_and_install_llvm(target_dir: Path) -> None: """Download and install LLVM using llvm-installer package.""" print("Detecting system configuration...") try: sys_conf_obj2: Any = sys_detection.local_sys_conf() local_conf2: _SysConf = cast(_SysConf, sys_conf_obj2) detected_os: str = local_conf2.short_os_name_and_version() architecture: str = local_conf2.architecture print(f"Detected OS: {detected_os}") print(f"Architecture: {architecture}") # Use Ubuntu 24.04 as fallback since it's known to work # This package seems to be from YugabyteDB and has limited OS support working_os: str = "ubuntu24.04" if detected_os.startswith("ubuntu24") or detected_os.startswith("ubuntu22"): working_os = detected_os print(f"Using OS variant: {working_os}") llvm_installer: Any = LlvmInstaller( short_os_name_and_version=working_os, architecture=architecture ) # Prefer most recent supported major release for linux tarball path llvm_url: str = "" selected_version: Optional[int] = None for version in [20, 19]: try: print(f"Installing LLVM version {version}...") llvm_url = llvm_installer.get_llvm_url(major_llvm_version=version) print(f"Found LLVM {version} at: {llvm_url}") selected_version = version break except KeyboardInterrupt: print("Operation interrupted by user") _thread.interrupt_main() raise except Exception as e: print(f"WARN: Could not resolve LLVM {version}: {e}") if not llvm_url: raise RuntimeError( "Could not resolve a suitable LLVM download URL for Linux" ) # Download and extract print("Downloading LLVM package...") # Download to managed directory tmp_archive_path: Path = _download( llvm_url, f"llvm-linux-{selected_version or 'unknown'}.tar.gz" ) try: print("Extracting LLVM package...") with tarfile.open(tmp_archive_path, "r:gz") as tar: tar.extractall(target_dir, filter="data") finally: # Clean up immediately after extraction try: tmp_archive_path.unlink() print(f"Cleaned up: {tmp_archive_path}") except Exception as e: print(f"Warning: Failed to clean up {tmp_archive_path}: {e}") # Find the extracted directory and verify clang exists for extracted_dir in target_dir.iterdir(): if extracted_dir.is_dir(): clang_path = extracted_dir / "bin" / "clang" if clang_path.exists(): print( f"OK: Successfully installed LLVM {selected_version if selected_version is not None else 'unknown'}" ) print(f" Installation directory: {extracted_dir}") print(f" Clang binary: {clang_path}") # List available tools (first few) and provide PATH guidance bin_dir = extracted_dir / "bin" if bin_dir.exists(): tools = [ f.name for f in bin_dir.iterdir() if f.is_file() and not f.name.endswith(".dll") ] print( f" Available tools: {', '.join(sorted(tools[:10]))}{'...' if len(tools) > 10 else ''}" ) print("") print("Next steps:") print(f" - Add to PATH: {bin_dir}") print(" - Example:") print(f' export PATH="{bin_dir}:$PATH"') print(" - Verify:") print(" clang --version") return print("ERROR: LLVM downloaded but clang not found in expected location") sys.exit(1) except KeyboardInterrupt: print("Operation interrupted by user") _thread.interrupt_main() raise except Exception as e: print(f"Error during installation: {e}") import traceback traceback.print_exc() sys.exit(1) def main() -> None: """Setup LLVM toolchain - check system first, fallback to package installation.""" # Get target directory from command line (optional) if len(sys.argv) > 2: print("Usage: uv run python ci/setup-llvm.py [target_directory]") print(" target_directory: Optional. Defaults to .cache/cc in project root") sys.exit(1) if len(sys.argv) == 2: target_dir = Path(sys.argv[1]) else: # Default to project root .cache/cc directory target_dir = _PROJECT_ROOT / ".cache" / "cc" print(f"Using default target directory: {target_dir}") target_dir.mkdir(parents=True, exist_ok=True) # Check platform compatibility is_unix_like, platform_name = is_linux_or_unix() if platform_name == "windows": print("Checking for existing LLVM/Clang tools on system (Windows)...") # Check for existing installation in target directory has_artifacts, has_breadcrumb = check_existing_installation(target_dir) if has_artifacts and not has_breadcrumb: print("WARN: Found installation artifacts without completion marker") print(" This suggests an incomplete or corrupted installation") print(" Re-downloading LLVM to ensure proper installation...") elif has_artifacts and has_breadcrumb: if verify_clang_installation(target_dir): print("OK: Valid LLVM installation found in target directory") print(" Skipping download - clang installation verified") return else: print( "WARN: Installation breadcrumb exists but clang verification failed" ) print(" Re-downloading LLVM to fix installation...") clang_version_output = get_tool_version("clang") if clang_version_output: clang_version = get_clang_version_number(clang_version_output) print( f"Found system clang: version {clang_version if clang_version else 'unknown'}" ) else: clang_version = None found_essential, found_extras = check_required_tools() available_essential = { name: path for name, path in found_essential.items() if path } missing_essential = [name for name, path in found_essential.items() if not path] if len(available_essential) != len(found_essential): # Probe common install directories (e.g., C:\Program Files\LLVM\bin) probe_dirs = get_windows_default_llvm_bins() print( f"Probing default Windows LLVM locations: {', '.join(str(p) for p in probe_dirs)}" ) probed = find_tools_in_directories(list(found_essential.keys()), probe_dirs) for name, p in probed.items(): if p is not None and found_essential.get(name) is None: found_essential[name] = p available_essential = { name: path for name, path in found_essential.items() if path } missing_essential = [ name for name, path in found_essential.items() if not path ] if len(available_essential) == len(found_essential) and ( clang_version is None or clang_version < 19 ): print( "Note: Detected toolchain but clang < 19. Continuing; ensure compatibility with your build settings." ) # On Windows, require core tools including llvm-addr2line for stack traces core_required = ["clang", "clang++", "lld-link", "llvm-addr2line"] missing_core = [name for name in core_required if not found_essential.get(name)] if not missing_core: print("OK: Core tools present - proceeding with available tools") all_available_tools: Dict[str, Path] = {} for name, path in found_essential.items(): if path is not None: all_available_tools[name] = cast(Path, path) for name, path in found_extras.items(): if path is not None: all_available_tools[name] = cast(Path, path) print(f"Creating hard links in {target_dir}...") create_hard_links(all_available_tools, target_dir) version_file = target_dir / "VERSION" version_file.write_text( f"system-clang-{clang_version if clang_version else 'unknown'}\n" ) bin_dir = target_dir / "bin" existing = [p for p in bin_dir.iterdir() if p.is_file()] print(f"OK: Tools available in {bin_dir}: {len(existing)} files") create_breadcrumb(target_dir) # Warn about missing non-core tools missing_noncore = [ name for name in list(found_essential.keys()) if name not in core_required and not found_essential.get(name) ] if missing_noncore: print(f"WARN: Missing non-core tools: {', '.join(missing_noncore)}") missing_extras = [name for name, path in found_extras.items() if not path] if missing_extras: print(f"WARN: Missing extra tools: {', '.join(missing_extras)}") return # Try to auto-install on Windows via direct GitHub download print(f"ERROR: Missing core tools: {', '.join(missing_core)}") print("Downloading LLVM for Windows from GitHub releases...") extracted_bin_dir = install_llvm_windows_direct(target_dir) # Collect tools from the extracted bin directory and link/copy them tool_names: List[str] = [ "clang", "clang++", "lld-link", "llvm-addr2line", "llvm-ar", "llvm-nm", "llvm-objdump", "llvm-strip", "llvm-objcopy", "clangd", "clang-format", "clang-tidy", "lldb", ] discovered: Dict[str, Optional[Path]] = find_tools_in_directories( tool_names, [extracted_bin_dir] ) available_tools: Dict[str, Path] = {} for name, p in discovered.items(): if p is not None: available_tools[name] = cast(Path, p) if not available_tools: print( "CRITICAL: Extracted LLVM bin directory does not contain expected tools." ) sys.exit(1) print(f"Creating hard links in {target_dir}...") create_hard_links(available_tools, target_dir) version_file = target_dir / "VERSION" version_file.write_text("downloaded-llvm-windows\n") bin_dir = target_dir / "bin" existing = [p for p in bin_dir.iterdir() if p.is_file()] print(f"OK: Tools available in {bin_dir}: {len(existing)} files") create_breadcrumb(target_dir) return print("Checking for existing LLVM/Clang tools on system...") # Check if clang is available and get its version clang_version_output = get_tool_version("clang") if clang_version_output: clang_version = get_clang_version_number(clang_version_output) print( f"Found system clang: version {clang_version if clang_version else 'unknown'}" ) if clang_version and clang_version >= 19: print("OK: System clang 19+ detected - checking tools") # Check for essential and extra tools found_essential, found_extras = check_required_tools() available_essential = { name: path for name, path in found_essential.items() if path } missing_essential = [ name for name, path in found_essential.items() if not path ] available_extras = { name: path for name, path in found_extras.items() if path } missing_extras = [name for name, path in found_extras.items() if not path] # Check if all essential tools are present if len(available_essential) == len(found_essential): print("OK: All essential tools found - using system tools") # Combine available tools for linking all_available_tools = {**available_essential, **available_extras} print(f"Creating hard links in {target_dir}...") create_hard_links(all_available_tools, target_dir) # Create a simple version file version_file = target_dir / "VERSION" version_file.write_text(f"system-clang-{clang_version}\n") print( f"OK: Successfully linked {len(all_available_tools)} system tools" ) # Warn about missing extras in one line if missing_extras: print(f"WARN: Missing extra tools: {', '.join(missing_extras)}") return else: # Missing essential tools - need to install LLVM print(f"ERROR: Missing essential tools: {', '.join(missing_essential)}") if platform_name == "linux": print("Installing LLVM package to provide missing tools...") download_and_install_llvm(target_dir) return else: print( f"WARN: Cannot auto-install on {platform_name} - please install manually" ) return else: print(f"System clang version {clang_version} < 19") if platform_name == "linux": print("Installing LLVM 18 package...") download_and_install_llvm(target_dir) return else: print( f"WARN: Cannot auto-install on {platform_name} - please install manually" ) return else: print("No system clang found") if platform_name == "linux": print("Installing LLVM package...") download_and_install_llvm(target_dir) else: print( f"WARN: Cannot auto-install on {platform_name} - please install manually" ) if __name__ == "__main__": try: main() finally: # Clean up download directory at the end _cleanup_download_dir()