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

542 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()