mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 07:04:04 -05:00
287 lines
11 KiB
Python
287 lines
11 KiB
Python
"""Shared utility functions for CE Properties Wizard."""
|
|
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Callable, List, Optional, Tuple
|
|
|
|
|
|
def find_ce_root_directory(search_targets: List[Tuple[str, Callable]], max_levels: int = 6) -> Optional[Path]:
|
|
"""Find CE root directory by looking for specific target paths.
|
|
|
|
Args:
|
|
search_targets: List of (relative_path, validation_function) tuples
|
|
max_levels: Maximum directory levels to traverse upward
|
|
|
|
Returns:
|
|
Path to CE root directory if found, None otherwise
|
|
"""
|
|
current_dir = Path(__file__).resolve().parent
|
|
|
|
for _ in range(max_levels):
|
|
for target_path, validator in search_targets:
|
|
target_dir = current_dir / target_path
|
|
if target_dir.exists() and validator(target_dir):
|
|
return current_dir
|
|
current_dir = current_dir.parent
|
|
|
|
return None
|
|
|
|
|
|
def find_ce_config_directory() -> Path:
|
|
"""Find the etc/config directory containing CE configuration files."""
|
|
|
|
def validate_config_dir(path: Path) -> bool:
|
|
return path.is_dir() and any(path.glob("*.defaults.properties"))
|
|
|
|
search_targets = [("etc/config", validate_config_dir)]
|
|
ce_root = find_ce_root_directory(search_targets)
|
|
|
|
if ce_root:
|
|
return ce_root / "etc" / "config"
|
|
|
|
# Fallback: check if we're already in the main CE directory
|
|
if Path("etc/config").exists() and Path("etc/config").is_dir():
|
|
config_dir = Path("etc/config").resolve()
|
|
if any(config_dir.glob("*.defaults.properties")):
|
|
return config_dir
|
|
|
|
raise FileNotFoundError("Could not find etc/config directory with CE configuration files")
|
|
|
|
|
|
def find_ce_lib_directory() -> Path:
|
|
"""Find the lib directory containing CE TypeScript files."""
|
|
|
|
def validate_lib_dir(path: Path) -> bool:
|
|
compilers_dir = path / "compilers"
|
|
return compilers_dir.exists() and compilers_dir.is_dir() and any(compilers_dir.glob("*.ts"))
|
|
|
|
search_targets = [("lib", validate_lib_dir)]
|
|
ce_root = find_ce_root_directory(search_targets)
|
|
|
|
if ce_root:
|
|
return ce_root / "lib"
|
|
|
|
# Fallback: assume we're in the main CE directory
|
|
lib_dir = Path("lib")
|
|
if validate_lib_dir(lib_dir):
|
|
return lib_dir.resolve()
|
|
|
|
raise FileNotFoundError("Could not find lib directory with TypeScript files")
|
|
|
|
|
|
def create_backup(file_path: Path) -> Path:
|
|
"""Create a backup of the file with .bak extension.
|
|
|
|
Args:
|
|
file_path: Path to the file to backup
|
|
|
|
Returns:
|
|
Path to the created backup file
|
|
"""
|
|
backup_path = file_path.with_suffix(".properties.bak")
|
|
if file_path.exists():
|
|
shutil.copy2(file_path, backup_path)
|
|
return backup_path
|
|
|
|
|
|
class SubprocessRunner:
|
|
"""Utility class for running subprocess commands with consistent error handling."""
|
|
|
|
@staticmethod
|
|
def run_with_timeout(
|
|
cmd: List[str], timeout: Optional[int] = 10, capture_output: bool = True, text: bool = True
|
|
) -> Optional[subprocess.CompletedProcess]:
|
|
"""Run a subprocess command with timeout and error handling.
|
|
|
|
Args:
|
|
cmd: Command and arguments to execute
|
|
timeout: Timeout in seconds (None for no timeout)
|
|
capture_output: Whether to capture stdout/stderr
|
|
text: Whether to return text output
|
|
|
|
Returns:
|
|
CompletedProcess result if successful, None if failed
|
|
"""
|
|
try:
|
|
# If timeout is None, run without timeout
|
|
if timeout is None:
|
|
result = subprocess.run(cmd, capture_output=capture_output, text=text)
|
|
else:
|
|
result = subprocess.run(cmd, capture_output=capture_output, text=text, timeout=timeout)
|
|
return result
|
|
except (subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
return None
|
|
|
|
|
|
class VersionExtractor:
|
|
"""Utility class for extracting version information from compiler output."""
|
|
|
|
# Regex patterns for different compiler types
|
|
PATTERNS = {
|
|
"gcc": [r"gcc.*?(\d+\.\d+\.\d+)", r"g\+\+.*?(\d+\.\d+\.\d+)"],
|
|
"clang": [r"clang version (\d+\.\d+\.\d+)"],
|
|
"intel": [r"(?:icc|icpc|icx|dpcpp).*?(\d+\.\d+\.\d+)", r"intel.*?compiler.*?(\d+\.\d+)"],
|
|
"intel_fortran": [r"(?:ifx|ifort)\s*\([^)]+\)\s*(\d+\.\d+\.\d+)", r"(?:ifx|ifort).*?(\d+\.\d+\.\d+)"],
|
|
"msvc": [r"compiler version (\d+\.\d+\.\d+)"],
|
|
"nvcc": [r"release (\d+\.\d+)"],
|
|
"rust": [r"rustc (\d+\.\d+\.\d+)"],
|
|
"go": [r"go\s*version\s+go(\d+\.\d+(?:\.\d+)?)", r"go(\d+\.\d+(?:\.\d+)?)"],
|
|
"tinygo": [r"(?:tinygo\s+)?version:?\s+(\d+\.\d+(?:\.\d+)?)"],
|
|
"python": [r"python (\d+\.\d+\.\d+)", r"pypy.*?(\d+\.\d+\.\d+)"],
|
|
"fpc": [r"Free Pascal Compiler version (\d+\.\d+\.\d+)", r"fpc.*?(\d+\.\d+\.\d+)"],
|
|
"z88dk": [r"z88dk.*?-\s*v([^-\s]+(?:-[^-\s]+)*)", r"v(\d+[^-\s]*(?:-[^-\s]*)*)"],
|
|
"kotlin": [r"kotlinc.*?(\d+\.\d+\.\d+)", r"kotlin.*?(\d+\.\d+\.\d+)"],
|
|
"zig": [r"zig (\d+\.\d+\.\d+)", r"zig.*?(\d+\.\d+\.\d+)"],
|
|
"dart": [r"Dart SDK version: (\d+\.\d+\.\d+)", r"dart.*?(\d+\.\d+\.\d+)"],
|
|
# Popular compiled languages
|
|
"dmd": [r"DMD.*?v(\d+\.\d+\.\d+)", r"dmd.*?(\d+\.\d+\.\d+)"],
|
|
"ldc": [r"LDC.*?(\d+\.\d+\.\d+)", r"ldc.*?(\d+\.\d+\.\d+)"],
|
|
"gdc": [r"gdc.*?(\d+\.\d+\.\d+)", r"GNU D compiler.*?(\d+\.\d+\.\d+)"],
|
|
"swiftc": [r"Swift version (\d+\.\d+(?:\.\d+)?)", r"swiftc.*?(\d+\.\d+(?:\.\d+)?)"],
|
|
"nim": [r"Nim Compiler Version (\d+\.\d+\.\d+)", r"nim.*?(\d+\.\d+\.\d+)"],
|
|
"crystal": [r"Crystal (\d+\.\d+\.\d+)", r"crystal.*?(\d+\.\d+\.\d+)"],
|
|
"v": [r"V (\d+\.\d+(?:\.\d+)?)", r"v.*?(\d+\.\d+(?:\.\d+)?)"],
|
|
# Functional languages
|
|
"ghc": [r"The Glorious Glasgow Haskell Compilation System, version (\d+\.\d+\.\d+)", r"ghc.*?(\d+\.\d+\.\d+)"],
|
|
"ocamlc": [r"OCaml version (\d+\.\d+\.\d+)", r"ocaml.*?(\d+\.\d+\.\d+)"],
|
|
"ocamlopt": [r"OCaml version (\d+\.\d+\.\d+)", r"ocaml.*?(\d+\.\d+\.\d+)"],
|
|
"scalac": [r"Scala compiler version (\d+\.\d+\.\d+)", r"scala.*?(\d+\.\d+\.\d+)"],
|
|
# .NET languages
|
|
"csharp": [r"Microsoft.*?C# Compiler version (\d+\.\d+\.\d+)", r"dotnet.*?(\d+\.\d+\.\d+)"],
|
|
"dotnet": [r"Microsoft.*?\.NET.*?(\d+\.\d+\.\d+)", r"dotnet.*?(\d+\.\d+\.\d+)"],
|
|
"fsharp": [r"F# Compiler.*?(\d+\.\d+\.\d+)", r"fsharpc.*?(\d+\.\d+\.\d+)"],
|
|
# Scripting/Dynamic languages
|
|
"ruby": [r"ruby (\d+\.\d+\.\d+)", r"ruby.*?(\d+\.\d+\.\d+)"],
|
|
"julia": [r"julia version (\d+\.\d+\.\d+)", r"julia.*?(\d+\.\d+\.\d+)"],
|
|
"elixir": [r"Elixir (\d+\.\d+\.\d+)", r"elixir.*?(\d+\.\d+\.\d+)"],
|
|
"erlc": [r"Erlang.*?(\d+(?:\.\d+)*)", r"erlc.*?(\d+(?:\.\d+)*)"],
|
|
# Assembly and low-level
|
|
"nasm": [r"NASM version (\d+\.\d+(?:\.\d+)?)", r"nasm.*?(\d+\.\d+(?:\.\d+)?)"],
|
|
"gas": [r"GNU assembler.*?(\d+\.\d+(?:\.\d+)?)", r"as.*?(\d+\.\d+(?:\.\d+)?)"],
|
|
"yasm": [r"yasm (\d+\.\d+\.\d+)", r"yasm.*?(\d+\.\d+\.\d+)"],
|
|
# Modern systems languages
|
|
"carbon": [r"Carbon.*?(\d+\.\d+(?:\.\d+)?)", r"carbon.*?(\d+\.\d+(?:\.\d+)?)"],
|
|
"mojo": [r"mojo (\d+\.\d+(?:\.\d+)?)", r"mojo.*?(\d+\.\d+(?:\.\d+)?)"],
|
|
"odin": [r"odin version (\d+\.\d+(?:\.\d+)?)", r"odin.*?(\d+\.\d+(?:\.\d+)?)"],
|
|
"gnatmake": [r"GNATMAKE.*?(\d+\.\d+(?:\.\d+)?)", r"gnat.*?(\d+\.\d+(?:\.\d+)?)"],
|
|
"gnucobol": [r"gnucobol.*?(\d+\.\d+(?:\.\d+)?)", r"cobol.*?(\d+\.\d+(?:\.\d+)?)"],
|
|
}
|
|
|
|
@classmethod
|
|
def extract_version(cls, compiler_type: str, output: str) -> Optional[str]:
|
|
"""Extract version string from compiler output.
|
|
|
|
Args:
|
|
compiler_type: Type of compiler (gcc, clang, etc.)
|
|
output: Raw output from compiler version command
|
|
|
|
Returns:
|
|
Extracted version string if found, None otherwise
|
|
"""
|
|
patterns = cls.PATTERNS.get(compiler_type, [])
|
|
|
|
for pattern in patterns:
|
|
match = re.search(pattern, output, re.IGNORECASE)
|
|
if match:
|
|
return match.group(1)
|
|
|
|
return None
|
|
|
|
@classmethod
|
|
def extract_semver(cls, version: Optional[str]) -> Optional[str]:
|
|
"""Extract semantic version from version string.
|
|
|
|
Args:
|
|
version: Version string to parse
|
|
|
|
Returns:
|
|
Semantic version (major.minor.patch) if found, None otherwise
|
|
"""
|
|
if not version:
|
|
return None
|
|
match = re.match(r"(\d+\.\d+(?:\.\d+)?)", version)
|
|
if match:
|
|
return match.group(1)
|
|
return None
|
|
|
|
|
|
class ArchitectureMapper:
|
|
"""Utility class for architecture and instruction set mapping."""
|
|
|
|
# Architecture mapping based on lib/instructionsets.ts
|
|
ARCH_MAPPINGS = {
|
|
"aarch64": "aarch64",
|
|
"arm64": "aarch64",
|
|
"arm": "arm32",
|
|
"avr": "avr",
|
|
"bpf": "ebpf",
|
|
"ez80": "ez80",
|
|
"kvx": "kvx",
|
|
"k1": "kvx",
|
|
"loongarch": "loongarch",
|
|
"m68k": "m68k",
|
|
"mips": "mips",
|
|
"mipsel": "mips",
|
|
"mips64": "mips",
|
|
"mips64el": "mips",
|
|
"nanomips": "mips",
|
|
"mrisc32": "mrisc32",
|
|
"msp430": "msp430",
|
|
"powerpc": "powerpc",
|
|
"ppc64": "powerpc",
|
|
"ppc": "powerpc",
|
|
"riscv64": "riscv64",
|
|
"rv64": "riscv64",
|
|
"riscv32": "riscv32",
|
|
"rv32": "riscv32",
|
|
"sh": "sh",
|
|
"sparc": "sparc",
|
|
"sparc64": "sparc",
|
|
"s390x": "s390x",
|
|
"vax": "vax",
|
|
"wasm32": "wasm32",
|
|
"wasm64": "wasm64",
|
|
"xtensa": "xtensa",
|
|
"z180": "z180",
|
|
"z80": "z80",
|
|
"x86_64": "amd64",
|
|
"x86-64": "amd64",
|
|
"amd64": "amd64",
|
|
"i386": "x86",
|
|
"i486": "x86",
|
|
"i586": "x86",
|
|
"i686": "x86",
|
|
}
|
|
|
|
@classmethod
|
|
def detect_instruction_set(cls, target: Optional[str], exe_path: str) -> str:
|
|
"""Detect instruction set from target platform or executable path.
|
|
|
|
Args:
|
|
target: Target platform string (e.g., from compiler -v output)
|
|
exe_path: Path to the compiler executable
|
|
|
|
Returns:
|
|
Instruction set name (defaults to "amd64" if not detected)
|
|
"""
|
|
if not target:
|
|
target = ""
|
|
|
|
target_lower = target.lower()
|
|
exe_lower = exe_path.lower()
|
|
|
|
# Check target first
|
|
for arch, instruction_set in cls.ARCH_MAPPINGS.items():
|
|
if arch in target_lower:
|
|
return instruction_set
|
|
|
|
# Check executable path as fallback
|
|
for arch, instruction_set in cls.ARCH_MAPPINGS.items():
|
|
if arch in exe_lower:
|
|
return instruction_set
|
|
|
|
# Default to amd64 if nothing detected
|
|
return "amd64"
|