Files
compiler-explorer/etc/scripts/ce-properties-wizard/auto_discover_compilers.py

284 lines
9.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""
CE Compiler Auto-Discovery Tool
Automatically discovers compilers in PATH directories and adds them using
the CE Properties Wizard.
"""
import argparse
import os
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Set
# Compiler patterns for each language
COMPILER_PATTERNS = {
'c++': ['g++', 'g++-*', 'clang++', 'clang++-*', 'icpc*', 'icx*'],
'c': ['gcc', 'gcc-[0-9]*', 'clang', 'clang-[0-9]*', 'icc*', 'cc'],
'cuda': ['nvcc*'],
'rust': ['rustc*'],
'go': ['go', 'gccgo*'],
'python': ['python*', 'python3*', 'pypy*'],
'java': ['javac*', 'java'],
'fortran': ['gfortran*', 'ifort*', 'ifx*'],
'pascal': ['fpc'],
'kotlin': ['kotlin*', 'kotlinc*'],
'zig': ['zig'],
'dart': ['dart'],
'd': ['dmd*', 'ldc*', 'ldc2*', 'gdc*'],
'swift': ['swift*', 'swiftc*'],
'nim': ['nim'],
'crystal': ['crystal'],
'v': ['v'],
'haskell': ['ghc*'],
'ocaml': ['ocaml*'],
'scala': ['scala*', 'scalac*'],
'csharp': ['csc*', 'mcs*', 'dotnet'],
'fsharp': ['fsharpc*', 'dotnet'],
'ruby': ['ruby*'],
'julia': ['julia'],
'elixir': ['elixir*'],
'erlang': ['erlc*', 'erl'],
'assembly': ['nasm*', 'yasm*', 'as'],
'carbon': ['carbon*'],
'mojo': ['mojo*'],
'odin': ['odin*'],
'ada': ['gnatmake*', 'gprbuild*', 'gnat*'],
'cobol': ['cobc*', 'gnucobol*', 'gcobol*'],
}
# Default exclude patterns
DEFAULT_EXCLUDES = {
'wrapper', 'distcc', 'ccache', '-config', 'config-',
'-ar', '-nm', '-ranlib', '-strip', 'filt', 'format',
'calls', 'flow', 'stat', '-gdb', 'argcomplete', 'build',
'ldconfig', 'ldconfig.real', '-bpfcc', 'bpfcc', 'scalar',
'pythongc-bpfcc', 'pythonflow-bpfcc', 'pythoncalls-bpfcc', 'pythonstat-bpfcc'
}
def get_path_dirs() -> List[Path]:
"""Get all directories from PATH environment variable."""
path = os.environ.get('PATH', '')
return [Path(p) for p in path.split(':') if p.strip()]
def should_exclude(name: str, excludes: Set[str]) -> bool:
"""Check if a compiler name should be excluded."""
return any(exclude in name for exclude in excludes)
def find_compilers_in_dir(directory: Path, patterns: List[str], excludes: Set[str]) -> List[Path]:
"""Find compilers matching patterns in a directory."""
compilers = []
if not directory.exists() or not directory.is_dir():
return compilers
for pattern in patterns:
# Simple glob matching
for compiler in directory.glob(pattern):
if (compiler.is_file() or compiler.is_symlink()) and \
os.access(compiler, os.X_OK) and \
not should_exclude(compiler.name, excludes):
compilers.append(compiler)
return compilers
def resolve_duplicates(compilers: List[Path]) -> List[Path]:
"""Remove duplicate compilers (same resolved path)."""
seen = set()
unique_compilers = []
for compiler in compilers:
try:
resolved = compiler.resolve()
if resolved not in seen:
seen.add(resolved)
unique_compilers.append(compiler)
except (OSError, RuntimeError):
# If we can't resolve, keep the original
unique_compilers.append(compiler)
return unique_compilers
def discover_compilers(languages: List[str], search_dirs: List[Path] = None,
excludes: Set[str] = None) -> Dict[str, List[Path]]:
"""Discover compilers for specified languages."""
if search_dirs is None:
search_dirs = get_path_dirs()
if excludes is None:
excludes = DEFAULT_EXCLUDES
discovered = {}
for language in languages:
if language not in COMPILER_PATTERNS:
print(f"Warning: Unknown language '{language}'", file=sys.stderr)
continue
patterns = COMPILER_PATTERNS[language]
compilers = []
for directory in search_dirs:
compilers.extend(find_compilers_in_dir(directory, patterns, excludes))
if compilers:
# Remove duplicates and sort
unique_compilers = resolve_duplicates(compilers)
discovered[language] = sorted(unique_compilers, key=lambda x: x.name)
return discovered
def add_compiler_with_wizard(compiler: Path, language: str, script_dir: Path,
wizard_args: List[str], dry_run: bool) -> bool:
"""Add a compiler using the CE Properties Wizard."""
if dry_run:
return True
cmd = [
str(script_dir / 'run.sh'),
str(compiler),
'--yes',
'--language', language
] + wizard_args
try:
result = subprocess.run(cmd, capture_output=True, text=True)
return result.returncode == 0
except Exception:
return False
def main():
parser = argparse.ArgumentParser(
description='CE Compiler Auto-Discovery Tool',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s # Interactive discovery of all languages
%(prog)s --dry-run # Preview what would be discovered
%(prog)s --languages c++,rust,go # Only discover C++, Rust, and Go
%(prog)s --yes --languages c++,c # Non-interactive C/C++ discovery
""")
parser.add_argument('--languages',
help='Comma-separated list of languages to discover (default: all)')
parser.add_argument('--search-dirs',
help='Colon-separated search directories (default: PATH dirs)')
parser.add_argument('--exclude',
help='Comma-separated exclude patterns')
parser.add_argument('--dry-run', action='store_true',
help='Show what would be added without making changes')
parser.add_argument('--yes', '-y', action='store_true',
help='Skip confirmation prompts')
parser.add_argument('--config-dir',
help='Path to etc/config directory')
parser.add_argument('--env', default='local',
help='Environment to target (local, amazon, etc.)')
args = parser.parse_args()
# Get script directory
script_dir = Path(__file__).parent
wizard_script = script_dir / 'run.sh'
if not wizard_script.exists():
print(f"Error: CE Properties Wizard not found at {wizard_script}", file=sys.stderr)
sys.exit(1)
# Parse languages
if args.languages:
languages = [lang.strip() for lang in args.languages.split(',')]
else:
languages = list(COMPILER_PATTERNS.keys())
# Parse search directories
search_dirs = None
if args.search_dirs:
search_dirs = [Path(d) for d in args.search_dirs.split(':') if d.strip()]
# Parse excludes
excludes = DEFAULT_EXCLUDES.copy()
if args.exclude:
excludes.update(args.exclude.split(','))
# Discover compilers
print("CE Compiler Auto-Discovery Tool")
print("=" * 35)
print()
if args.dry_run:
print("DRY RUN MODE - No compilers will actually be added")
print()
discovered = discover_compilers(languages, search_dirs, excludes)
if not discovered:
print("No compilers found matching the specified criteria")
sys.exit(1)
# Show results
total_count = sum(len(compilers) for compilers in discovered.values())
print(f"Found {total_count} compilers:")
print()
for language, compilers in discovered.items():
print(f"{language.upper()} ({len(compilers)} compilers):")
for compiler in compilers:
print(f"{compiler}")
print()
# Confirm before adding
if not args.dry_run and not args.yes:
response = input("Add these compilers? [y/N] ")
if not response.lower().startswith('y'):
print("Operation cancelled")
sys.exit(0)
if args.dry_run:
print("Dry run complete - no changes made")
sys.exit(0)
# Add compilers
print("Adding compilers using CE Properties Wizard...")
print()
wizard_args = []
if args.config_dir:
wizard_args.extend(['--config-dir', args.config_dir])
if args.env != 'local':
wizard_args.extend(['--env', args.env])
added_count = 0
failed_count = 0
for language, compilers in discovered.items():
for compiler in compilers:
print(f"Adding {compiler} ({language})...", end=' ')
if add_compiler_with_wizard(compiler, language, script_dir, wizard_args, args.dry_run):
print("")
added_count += 1
else:
print("")
failed_count += 1
print()
print("Summary:")
print(f" ✓ Successfully added: {added_count} compilers")
if failed_count > 0:
print(f" ✗ Failed to add: {failed_count} compilers")
print()
print("Auto-discovery complete!")
if __name__ == '__main__':
main()