mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
542 lines
23 KiB
Python
542 lines
23 KiB
Python
"""Main CLI entry point for CE Properties Wizard."""
|
||
|
||
import os
|
||
import shlex
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
import click
|
||
import inquirer
|
||
from colorama import Fore, Style, init
|
||
|
||
from .compiler_detector import LANGUAGE_CONFIGS, CompilerDetector, get_supported_compiler_types
|
||
from .config_manager import ConfigManager
|
||
from .models import CompilerInfo
|
||
from .utils import find_ce_config_directory
|
||
|
||
# Initialize colorama for cross-platform color support
|
||
init(autoreset=True)
|
||
|
||
|
||
def print_success(message: str):
|
||
"""Print success message in green."""
|
||
click.echo(f"{Fore.GREEN}✓ {message}{Style.RESET_ALL}")
|
||
|
||
|
||
def print_error(message: str):
|
||
"""Print error message in red."""
|
||
click.echo(f"{Fore.RED}✗ {message}{Style.RESET_ALL}", err=True)
|
||
|
||
|
||
def print_info(message: str):
|
||
"""Print info message in blue."""
|
||
click.echo(f"{Fore.BLUE}ℹ {message}{Style.RESET_ALL}")
|
||
|
||
|
||
def print_warning(message: str):
|
||
"""Print warning message in yellow."""
|
||
click.echo(f"{Fore.YELLOW}⚠ {message}{Style.RESET_ALL}")
|
||
|
||
|
||
def format_compiler_options(options_input: str) -> str:
|
||
"""Format compiler options properly.
|
||
|
||
Takes space-separated options and quotes any that contain spaces.
|
||
|
||
Args:
|
||
options_input: Raw options string from user input
|
||
|
||
Returns:
|
||
Properly formatted options string with quoted options containing spaces
|
||
"""
|
||
if not options_input or not options_input.strip():
|
||
return ""
|
||
|
||
# Split by spaces but respect quoted strings
|
||
try:
|
||
options = shlex.split(options_input)
|
||
except ValueError:
|
||
# If shlex fails (unmatched quotes), fall back to simple split
|
||
options = options_input.split()
|
||
|
||
# Format each option - quote it if it contains spaces
|
||
formatted_options = []
|
||
for opt in options:
|
||
opt = opt.strip()
|
||
if opt:
|
||
if " " in opt and not (opt.startswith('"') and opt.endswith('"')):
|
||
formatted_options.append(f'"{opt}"')
|
||
else:
|
||
formatted_options.append(opt)
|
||
|
||
return " ".join(formatted_options)
|
||
|
||
|
||
@click.command()
|
||
@click.argument("compiler_path", required=False)
|
||
@click.option("--id", "compiler_id", help="Compiler ID (auto-generated if not specified)")
|
||
@click.option("--name", "display_name", help="Display name for the compiler")
|
||
@click.option("--group", help="Compiler group to add to")
|
||
@click.option("--options", help="Default compiler options")
|
||
@click.option("--language", help="Programming language (auto-detected if not specified)")
|
||
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts")
|
||
@click.option("--non-interactive", is_flag=True, help="Run in non-interactive mode with auto-detected values")
|
||
@click.option("--config-dir", type=click.Path(exists=True), help="Path to etc/config directory")
|
||
@click.option("--verify-only", is_flag=True, help="Only detect and display compiler information without making changes")
|
||
@click.option("--list-types", is_flag=True, help="List all supported compiler types and exit")
|
||
@click.option("--reorganize", help="Reorganize an existing properties file for the specified language")
|
||
@click.option(
|
||
"--validate-discovery",
|
||
is_flag=True,
|
||
help="Run discovery validation to verify the compiler is detected (default for local environment)",
|
||
)
|
||
@click.option("--env", default="local", help="Environment to target (local, amazon, etc.)")
|
||
@click.option("--debug", is_flag=True, help="Enable debug output including subprocess commands")
|
||
def cli(
|
||
compiler_path: Optional[str],
|
||
compiler_id: Optional[str],
|
||
display_name: Optional[str],
|
||
group: Optional[str],
|
||
options: Optional[str],
|
||
language: Optional[str],
|
||
yes: bool,
|
||
non_interactive: bool,
|
||
config_dir: Optional[str],
|
||
verify_only: bool,
|
||
list_types: bool,
|
||
reorganize: Optional[str],
|
||
validate_discovery: bool,
|
||
env: str,
|
||
debug: bool,
|
||
):
|
||
"""CE Properties Wizard - Add compilers to your Compiler Explorer installation.
|
||
|
||
Examples:
|
||
ce-props-wizard # Interactive mode (local environment)
|
||
ce-props-wizard /usr/bin/g++-13 # Path-first mode
|
||
ce-props-wizard /usr/bin/g++-13 --yes # Automated mode
|
||
ce-props-wizard --env amazon /usr/bin/g++ # Target amazon environment
|
||
ce-props-wizard --list-types # List all supported compiler types
|
||
ce-props-wizard /usr/bin/g++ --verify-only # Just detect compiler info
|
||
"""
|
||
# Handle --list-types flag
|
||
if list_types:
|
||
try:
|
||
supported_types = get_supported_compiler_types()
|
||
click.echo(f"Found {len(supported_types)} supported compiler types:\n")
|
||
for compiler_type in sorted(supported_types):
|
||
click.echo(compiler_type)
|
||
sys.exit(0)
|
||
except Exception as e:
|
||
print_error(f"Error reading compiler types: {e}")
|
||
sys.exit(1)
|
||
|
||
# Handle --reorganize flag
|
||
if reorganize:
|
||
try:
|
||
# Find config directory
|
||
if config_dir:
|
||
config_mgr = ConfigManager(Path(config_dir), env, debug=debug)
|
||
else:
|
||
config_mgr = ConfigManager(find_ce_config_directory(), env, debug=debug)
|
||
|
||
print_info(f"Reorganizing {reorganize} properties file...")
|
||
|
||
# Check if language is valid
|
||
if reorganize not in LANGUAGE_CONFIGS:
|
||
print_error(f"Unknown language: {reorganize}")
|
||
print_info(f"Available languages: {', '.join(LANGUAGE_CONFIGS.keys())}")
|
||
sys.exit(1)
|
||
|
||
file_path = config_mgr.get_properties_path(reorganize)
|
||
if not file_path.exists():
|
||
print_error(f"No {env} properties file found for {reorganize}: {file_path}")
|
||
sys.exit(1)
|
||
|
||
config_mgr.reorganize_existing_file(reorganize)
|
||
print_success(f"Reorganized {file_path}")
|
||
|
||
# Validate with propscheck
|
||
print_info("Validating with propscheck.py...")
|
||
valid, message = config_mgr.validate_with_propscheck(reorganize)
|
||
if valid:
|
||
print_success(message)
|
||
else:
|
||
print_error(message)
|
||
|
||
sys.exit(0)
|
||
except Exception as e:
|
||
print_error(f"Error reorganizing file: {e}")
|
||
sys.exit(1)
|
||
|
||
# Skip banner in verify-only mode
|
||
if not verify_only:
|
||
click.echo(f"{Fore.CYAN}{'='*60}{Style.RESET_ALL}")
|
||
click.echo(f"{Fore.CYAN}Compiler Explorer Properties Wizard{Style.RESET_ALL}")
|
||
click.echo(f"{Fore.CYAN}{'='*60}{Style.RESET_ALL}\n")
|
||
|
||
try:
|
||
# Find config directory only if needed
|
||
if not verify_only:
|
||
if config_dir:
|
||
config_path = Path(config_dir)
|
||
else:
|
||
config_path = find_ce_config_directory()
|
||
print_info(f"Using config directory: {config_path}")
|
||
print_info(f"Targeting environment: {env}")
|
||
config_mgr = ConfigManager(config_path, env, debug=debug)
|
||
else:
|
||
config_mgr = None
|
||
|
||
# Initialize detector
|
||
detector = CompilerDetector(debug=debug)
|
||
|
||
# Get compiler path if not provided
|
||
if not compiler_path:
|
||
questions = [
|
||
inquirer.Text(
|
||
"compiler_path",
|
||
message="Enter the full path to the compiler executable",
|
||
validate=lambda _, x: os.path.isfile(x),
|
||
)
|
||
]
|
||
answers = inquirer.prompt(questions)
|
||
if not answers:
|
||
print_error("Cancelled by user")
|
||
sys.exit(1)
|
||
compiler_path = answers["compiler_path"]
|
||
|
||
# Validate compiler path
|
||
compiler_path = os.path.abspath(compiler_path)
|
||
if not os.path.isfile(compiler_path):
|
||
print_error(f"Compiler not found: {compiler_path}")
|
||
sys.exit(1)
|
||
|
||
if not os.access(compiler_path, os.X_OK):
|
||
print_error(f"File is not executable: {compiler_path}")
|
||
sys.exit(1)
|
||
|
||
# Detect compiler information
|
||
print_info("Detecting compiler type and language...")
|
||
try:
|
||
detected_info = detector.detect_from_path(compiler_path)
|
||
|
||
if detected_info.compiler_type:
|
||
print_success(f"Detected: {detected_info.name} ({LANGUAGE_CONFIGS[detected_info.language].name})")
|
||
else:
|
||
print_warning("Could not detect compiler type")
|
||
|
||
except Exception as e:
|
||
print_error(f"Detection failed: {e}")
|
||
# Create minimal info
|
||
detected_info = CompilerInfo(
|
||
id="custom-compiler",
|
||
name=os.path.basename(compiler_path),
|
||
exe=compiler_path,
|
||
language=language or "c++",
|
||
)
|
||
|
||
# Override with command-line options
|
||
if language:
|
||
detected_info.language = language
|
||
|
||
# Suggest appropriate group if not already set
|
||
if not detected_info.group:
|
||
if not verify_only and config_mgr is not None:
|
||
# Normal mode - create config manager and suggest group
|
||
suggested_group = config_mgr.suggest_appropriate_group(detected_info)
|
||
if suggested_group:
|
||
detected_info.group = suggested_group
|
||
else:
|
||
# Verify-only mode - create a temporary config manager just for suggestion
|
||
temp_config_mgr = ConfigManager(find_ce_config_directory(), env, debug=debug)
|
||
suggested_group = temp_config_mgr.suggest_appropriate_group(detected_info)
|
||
if suggested_group:
|
||
detected_info.group = suggested_group
|
||
|
||
# Initialize flag for forcing custom ID/name
|
||
force_custom_id_name = False
|
||
|
||
# Check for existing compiler by path early (before prompts)
|
||
if not verify_only and config_mgr is not None:
|
||
existing_compiler_id = config_mgr.check_existing_compiler_by_path(compiler_path, detected_info.language)
|
||
if existing_compiler_id:
|
||
file_path = config_mgr.get_properties_path(detected_info.language)
|
||
print_warning(f"Compiler already exists in {env} environment!")
|
||
print_info(f"Existing compiler ID: {existing_compiler_id}")
|
||
print_info(f"Executable path: {compiler_path}")
|
||
print_info(f"Properties file: {file_path}")
|
||
|
||
# If automated mode (-y), exit immediately
|
||
if yes or non_interactive:
|
||
print_info("No changes were made.")
|
||
sys.exit(0)
|
||
|
||
# In interactive mode, ask if user wants to continue with different ID/name
|
||
if not click.confirm("\nWould you like to add this compiler anyway with a different ID and name?"):
|
||
print_info("No changes were made.")
|
||
sys.exit(0)
|
||
|
||
print_info("You will need to provide a unique compiler ID and custom name.")
|
||
# Set flag to force custom ID and name prompts
|
||
force_custom_id_name = True
|
||
# Suggest the group from the existing duplicate compiler
|
||
if config_mgr is not None:
|
||
suggested_group = config_mgr.suggest_appropriate_group(detected_info, existing_compiler_id)
|
||
if suggested_group and not detected_info.group:
|
||
detected_info.group = suggested_group
|
||
|
||
# If verify-only mode, display info and exit
|
||
if verify_only:
|
||
click.echo("\nDetected compiler information:")
|
||
click.echo(f" Path: {compiler_path}")
|
||
lang_name = (
|
||
LANGUAGE_CONFIGS[detected_info.language].name
|
||
if detected_info.language in LANGUAGE_CONFIGS
|
||
else detected_info.language
|
||
)
|
||
click.echo(f" Language: {lang_name}")
|
||
click.echo(f" Compiler Type: {detected_info.compiler_type or 'unknown'}")
|
||
click.echo(f" Version: {detected_info.version or 'unknown'}")
|
||
click.echo(f" Semver: {detected_info.semver or 'unknown'}")
|
||
if detected_info.target:
|
||
click.echo(f" Target: {detected_info.target}")
|
||
click.echo(f" Cross-compiler: {'Yes' if detected_info.is_cross_compiler else 'No'}")
|
||
click.echo(f" Suggested ID: {detected_info.id}")
|
||
click.echo(f" Suggested Name: {detected_info.name}")
|
||
click.echo(f" Suggested Group: {detected_info.group or 'none'}")
|
||
sys.exit(0)
|
||
|
||
# Interactive prompts for missing information
|
||
if not yes and not non_interactive:
|
||
questions = []
|
||
|
||
# Windows SDK path prompt for MSVC compilers if auto-detection failed
|
||
if detected_info.needs_sdk_prompt:
|
||
print_info("Windows SDK auto-detection failed. You can optionally specify the Windows SDK path.")
|
||
print_info("Example: Z:/compilers/windows-kits-10 (leave empty to skip)")
|
||
sdk_question = inquirer.Text(
|
||
"windows_sdk_path",
|
||
message="Windows SDK base path (optional)",
|
||
default="",
|
||
validate=lambda _, x: x == "" or os.path.isdir(x.replace("\\", "/"))
|
||
)
|
||
sdk_answers = inquirer.prompt([sdk_question])
|
||
if sdk_answers and sdk_answers["windows_sdk_path"].strip():
|
||
# Apply the user-provided SDK path
|
||
detected_info = detector.set_windows_sdk_path(detected_info, sdk_answers["windows_sdk_path"].strip())
|
||
print_success(f"Windows SDK paths added from: {sdk_answers['windows_sdk_path']}")
|
||
|
||
# Language selection if needed
|
||
if not language and detected_info.language:
|
||
lang_choices = [(LANGUAGE_CONFIGS[k].name, k) for k in LANGUAGE_CONFIGS.keys()]
|
||
questions.append(
|
||
inquirer.List(
|
||
"language", message="Programming language", choices=lang_choices, default=detected_info.language
|
||
)
|
||
)
|
||
|
||
# Compiler ID - force custom if duplicate exists
|
||
if force_custom_id_name:
|
||
questions.append(
|
||
inquirer.Text(
|
||
"compiler_id",
|
||
message="Compiler ID (must be unique)",
|
||
default=compiler_id or "",
|
||
validate=lambda _, x: bool(x and x.strip() and x != detected_info.id),
|
||
)
|
||
)
|
||
else:
|
||
questions.append(
|
||
inquirer.Text(
|
||
"compiler_id",
|
||
message="Compiler ID",
|
||
default=compiler_id or detected_info.id,
|
||
validate=lambda _, x: bool(x and x.strip()),
|
||
)
|
||
)
|
||
|
||
# Display name - force custom if duplicate exists
|
||
if force_custom_id_name:
|
||
questions.append(
|
||
inquirer.Text(
|
||
"display_name",
|
||
message="Display name (must be custom)",
|
||
default=display_name or "",
|
||
validate=lambda _, x: bool(x and x.strip() and x != detected_info.name),
|
||
)
|
||
)
|
||
else:
|
||
questions.append(
|
||
inquirer.Text("display_name", message="Display name", default=display_name or detected_info.name)
|
||
)
|
||
|
||
# Compiler type (if not detected)
|
||
if not detected_info.compiler_type:
|
||
# Get all supported compiler types dynamically
|
||
supported_types = sorted(get_supported_compiler_types())
|
||
# Add 'other' as fallback option
|
||
type_choices = supported_types + ["other"]
|
||
|
||
questions.append(
|
||
inquirer.List("compiler_type", message="Compiler type", choices=type_choices, default="other")
|
||
)
|
||
|
||
# Group
|
||
questions.append(
|
||
inquirer.Text(
|
||
"group",
|
||
message="Add to group",
|
||
default=group or detected_info.group or detected_info.compiler_type or "",
|
||
)
|
||
)
|
||
|
||
# Options
|
||
questions.append(
|
||
inquirer.Text(
|
||
"options",
|
||
message="Additional options (space-separated, quote options with spaces)",
|
||
default=options or "",
|
||
)
|
||
)
|
||
|
||
if questions:
|
||
answers = inquirer.prompt(questions)
|
||
if not answers:
|
||
print_error("Cancelled by user")
|
||
sys.exit(1)
|
||
|
||
# Update detected info
|
||
if "language" in answers:
|
||
detected_info.language = answers["language"]
|
||
if "compiler_id" in answers:
|
||
detected_info.id = answers["compiler_id"]
|
||
if "display_name" in answers:
|
||
detected_info.name = answers["display_name"]
|
||
# If this is a duplicate override scenario, force the name to be included
|
||
if force_custom_id_name:
|
||
detected_info.force_name = True
|
||
if "compiler_type" in answers:
|
||
compiler_type = answers["compiler_type"]
|
||
# Validate compiler type against supported types
|
||
if compiler_type != "other":
|
||
supported_types = get_supported_compiler_types()
|
||
if compiler_type not in supported_types:
|
||
print_warning(f"'{compiler_type}' is not a recognized compiler type in Compiler Explorer")
|
||
detected_info.compiler_type = compiler_type
|
||
if "group" in answers and answers["group"]:
|
||
detected_info.group = answers["group"]
|
||
if "options" in answers and answers["options"]:
|
||
detected_info.options = format_compiler_options(answers["options"])
|
||
else:
|
||
# In automated mode, use command-line values
|
||
if compiler_id:
|
||
detected_info.id = compiler_id
|
||
if display_name:
|
||
detected_info.name = display_name
|
||
# If this is a duplicate override scenario, force the name to be included
|
||
if force_custom_id_name:
|
||
detected_info.force_name = True
|
||
if group:
|
||
detected_info.group = group
|
||
if options:
|
||
detected_info.options = format_compiler_options(options)
|
||
|
||
# Ensure unique ID (config_mgr should not be None at this point)
|
||
assert config_mgr is not None, "config_mgr should not be None in non-verify mode"
|
||
original_id = detected_info.id
|
||
detected_info.id = config_mgr.ensure_compiler_id_unique(detected_info.id, detected_info.language)
|
||
if detected_info.id != original_id:
|
||
print_warning(f"ID already exists, using: {detected_info.id}")
|
||
|
||
# Show configuration preview
|
||
print_info("\nConfiguration preview:")
|
||
normalized_exe_path = detected_info.exe.replace("\\", "/")
|
||
click.echo(f" compiler.{detected_info.id}.exe={normalized_exe_path}")
|
||
|
||
# Check if semver will be available (either detected or extracted)
|
||
semver_to_use = detected_info.semver
|
||
if not semver_to_use:
|
||
# Try to extract version like the config manager will do
|
||
try:
|
||
semver_to_use = config_mgr._extract_compiler_version(detected_info.exe)
|
||
except Exception:
|
||
pass
|
||
|
||
# Show semver if available
|
||
if semver_to_use:
|
||
click.echo(f" compiler.{detected_info.id}.semver={semver_to_use}")
|
||
|
||
# Show name if semver is not available OR if this is a duplicate override scenario
|
||
if detected_info.name and (not semver_to_use or force_custom_id_name):
|
||
click.echo(f" compiler.{detected_info.id}.name={detected_info.name}")
|
||
|
||
if detected_info.compiler_type:
|
||
click.echo(f" compiler.{detected_info.id}.compilerType={detected_info.compiler_type}")
|
||
if detected_info.options:
|
||
click.echo(f" compiler.{detected_info.id}.options={detected_info.options}")
|
||
if detected_info.java_home:
|
||
click.echo(f" compiler.{detected_info.id}.java_home={detected_info.java_home}")
|
||
if detected_info.runtime:
|
||
click.echo(f" compiler.{detected_info.id}.runtime={detected_info.runtime}")
|
||
if detected_info.execution_wrapper:
|
||
click.echo(f" compiler.{detected_info.id}.executionWrapper={detected_info.execution_wrapper}")
|
||
if detected_info.include_path:
|
||
click.echo(f" compiler.{detected_info.id}.includePath={detected_info.include_path}")
|
||
if detected_info.lib_path:
|
||
click.echo(f" compiler.{detected_info.id}.libPath={detected_info.lib_path}")
|
||
if detected_info.group:
|
||
click.echo(f" Will add to group: {detected_info.group}")
|
||
|
||
# Confirm
|
||
file_path = config_mgr.get_properties_path(detected_info.language)
|
||
if not yes and not non_interactive:
|
||
if not click.confirm(f"\nUpdate {file_path}?"):
|
||
print_error("Cancelled by user")
|
||
sys.exit(1)
|
||
|
||
# Add compiler
|
||
config_mgr.add_compiler(detected_info)
|
||
print_success("Configuration updated successfully!")
|
||
|
||
# Validate with propscheck
|
||
print_info("Validating with propscheck.py...")
|
||
valid, message = config_mgr.validate_with_propscheck(detected_info.language)
|
||
if valid:
|
||
print_success(message)
|
||
else:
|
||
print_error(message)
|
||
# Don't exit with error, as the file was written successfully
|
||
|
||
# Discovery validation (default for local environment, optional for others)
|
||
should_validate_discovery = validate_discovery or (env == "local")
|
||
if should_validate_discovery:
|
||
print_info("Validating with discovery...")
|
||
valid, message, discovered_semver = config_mgr.validate_with_discovery(
|
||
detected_info.language, detected_info.id
|
||
)
|
||
if valid:
|
||
print_success(message)
|
||
if discovered_semver:
|
||
print_info(f"Discovered semver: {discovered_semver}")
|
||
else:
|
||
print_error(message)
|
||
print_info(
|
||
"Note: Discovery validation failed, but the compiler was added to the properties file successfully."
|
||
)
|
||
|
||
click.echo(f"\n{Fore.GREEN}Compiler added successfully!{Style.RESET_ALL}")
|
||
click.echo("You may need to restart Compiler Explorer for changes to take effect.")
|
||
|
||
except KeyboardInterrupt:
|
||
print_error("\nCancelled by user")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print_error(f"Error: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
cli()
|