Files
klubhaus-doorbell/libraries/FastLED/ci/setup-llvm.py
2026-02-12 00:45:31 -08:00

970 lines
36 KiB
Python

#!/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()