Skip to main content
Glama
compiler.py14.3 kB
"""Delphi compiler orchestration and execution.""" import subprocess import tempfile import time from pathlib import Path from typing import Optional from src.config import ConfigLoader from src.dproj_parser import DProjParser from src.models import CompilationResult, CompilationStatistics from src.output_parser import OutputParser class DelphiCompiler: """Orchestrates Delphi compilation process.""" def __init__(self, config_loader: Optional[ConfigLoader] = None): """Initialize compiler. Args: config_loader: Config loader instance. If None, creates a new one. """ self.config_loader = config_loader or ConfigLoader() self.config = None def compile_project( self, project_path: Path, force_build_all: bool = False, override_config: Optional[str] = None, override_platform: Optional[str] = None, additional_search_paths: Optional[list[str]] = None, additional_flags: Optional[list[str]] = None, ) -> CompilationResult: """Compile a Delphi project. Args: project_path: Path to .dpr or .dproj file force_build_all: Force rebuild all units (-B flag) override_config: Override active build config (Debug/Release) override_platform: Override active platform (Win32/Win64) additional_search_paths: Extra search paths to add additional_flags: Additional compiler flags Returns: CompilationResult with errors and statistics Raises: FileNotFoundError: If project file not found ValueError: If project file is invalid """ # Validate project file if not project_path.exists(): raise FileNotFoundError(f"Project file not found: {project_path}") if project_path.suffix.lower() not in [".dpr", ".dproj"]: raise ValueError(f"Invalid project file: {project_path}") # Load configuration if not self.config: self.config = self.config_loader.load() # Parse .dproj file if it exists dproj_path = self._get_dproj_path(project_path) dproj_settings = None if dproj_path and dproj_path.exists(): dproj_parser = DProjParser(dproj_path) dproj_settings = dproj_parser.parse(override_config, override_platform) platform = dproj_settings.active_platform else: # No .dproj, use override or default to Win32 platform = override_platform or "Win32" # Get the actual .dpr file to compile (not .dproj) dpr_path = self._get_dpr_path(project_path) if not dpr_path.exists(): raise FileNotFoundError(f"Delphi project source file not found: {dpr_path}") # Build compiler command compiler_path = self.config_loader.get_compiler_path(platform) command = self._build_command( compiler_path=compiler_path, project_path=dpr_path, # Compile the .dpr file, not .dproj dproj_settings=dproj_settings, force_build_all=force_build_all, additional_search_paths=additional_search_paths or [], additional_flags=additional_flags or [], ) # Execute compilation start_time = time.time() output, exit_code = self._execute_compiler(command, dpr_path.parent) compilation_time = time.time() - start_time # Parse output parser = OutputParser() errors, statistics = parser.parse(output) # Determine output executable path output_exe = None if exit_code == 0: output_exe = self._find_output_executable(project_path, dproj_settings) return CompilationResult( success=exit_code == 0, exit_code=exit_code, errors=errors, compilation_time_seconds=round(compilation_time, 2), output_executable=str(output_exe) if output_exe else None, statistics=statistics, ) def _get_dproj_path(self, project_path: Path) -> Optional[Path]: """Get .dproj path corresponding to .dpr file. Args: project_path: Path to .dpr or .dproj file Returns: Path to .dproj file, or None if not found """ if project_path.suffix.lower() == ".dproj": return project_path # Look for .dproj with same name dproj_path = project_path.with_suffix(".dproj") return dproj_path if dproj_path.exists() else None def _get_dpr_path(self, project_path: Path) -> Path: """Get .dpr path to actually compile. Args: project_path: Path to .dpr or .dproj file Returns: Path to .dpr file """ if project_path.suffix.lower() == ".dpr": return project_path # If given .dproj, return corresponding .dpr file return project_path.with_suffix(".dpr") def _build_command( self, compiler_path: Path, project_path: Path, dproj_settings: Optional[any], force_build_all: bool, additional_search_paths: list[str], additional_flags: list[str], ) -> list[str]: """Build the complete compiler command line. Args: compiler_path: Path to compiler executable project_path: Path to project file dproj_settings: Parsed .dproj settings (if available) force_build_all: Whether to force rebuild all additional_search_paths: Extra search paths additional_flags: Additional compiler flags Returns: Command as list of arguments """ command = [str(compiler_path)] # Add compiler flags from config file (extracted from build log) # These include essential flags like --no-config, -$O-, -$W+, etc. config_flags = self.config.compiler.flags.get("flags", []) for flag in config_flags: # Skip -B and -Q as we handle them separately if flag not in ["-B", "-Q"]: command.append(flag) # Add compiler flags from .dproj (like -$O-, -$R+, etc.) if dproj_settings: for flag in dproj_settings.compiler_flags: # Only add if not already present (avoid duplicates) if flag not in command: command.append(flag) # Add defines if dproj_settings.defines: defines_str = ";".join(dproj_settings.defines) command.append(f"-D{defines_str}") # Add build all flag if force_build_all: command.append("-B") # Add quiet flag command.append("-Q") # Build search paths - merge global config paths with .dproj paths all_search_paths = [] # Add config file paths (global paths from delphi_config.toml) all_search_paths.extend(self.config_loader.get_all_search_paths()) # Add .dproj search paths (project-specific paths) if dproj_settings: all_search_paths.extend(dproj_settings.unit_search_paths) all_search_paths.extend(dproj_settings.include_paths) all_search_paths.extend(dproj_settings.resource_paths) # Add additional search paths from caller all_search_paths.extend([Path(p) for p in additional_search_paths]) # Deduplicate paths while preserving order unique_paths = self._deduplicate_paths(all_search_paths) # Add search paths to command if unique_paths: search_path_str = ";".join(str(p) for p in unique_paths) command.append(f"-U{search_path_str}") command.append(f"-I{search_path_str}") command.append(f"-R{search_path_str}") # Add namespace prefixes - merge global config with .dproj namespaces namespace_prefixes = self._merge_namespaces( self.config.compiler.namespaces.get("prefixes", []), dproj_settings.namespace_prefixes if dproj_settings else [] ) if namespace_prefixes: ns_str = ";".join(namespace_prefixes) command.append(f"-NS{ns_str}") # Add unit aliases if self.config.compiler.aliases: alias_parts = [] for old, new in self.config.compiler.aliases.items(): alias_parts.append(f"{old}={new}") if alias_parts: alias_str = ";".join(alias_parts) command.append(f"-A{alias_str}") # Add output directories if dproj_settings: if dproj_settings.output_dir: command.append(f"-E{dproj_settings.output_dir}") if dproj_settings.dcu_output_dir: command.append(f"-NU{dproj_settings.dcu_output_dir}") # Add additional flags from caller command.extend(additional_flags) # Add project file (must be last) # Use just filename since we're running from project directory command.append(project_path.name) return command def _deduplicate_paths(self, paths: list[Path]) -> list[Path]: """Deduplicate paths while preserving order. Args: paths: List of paths to deduplicate Returns: Deduplicated list of paths """ unique_paths = [] seen = set() for path in paths: # Normalize path for comparison (case-insensitive on Windows) path_str = str(path).lower().replace("/", "\\") if path_str not in seen: seen.add(path_str) unique_paths.append(path) return unique_paths def _merge_namespaces(self, config_namespaces: list[str], dproj_namespaces: list[str]) -> list[str]: """Merge namespace lists without duplicates while preserving order. Config namespaces come first, then any additional dproj namespaces. Args: config_namespaces: Namespaces from config file dproj_namespaces: Namespaces from .dproj file Returns: Merged list of unique namespaces """ merged = [] seen = set() # Add config namespaces first for ns in config_namespaces: ns_lower = ns.lower() if ns_lower not in seen: seen.add(ns_lower) merged.append(ns) # Add dproj namespaces (only if not already present) for ns in dproj_namespaces: ns_lower = ns.lower() if ns_lower not in seen: seen.add(ns_lower) merged.append(ns) return merged def _execute_compiler(self, command: list[str], working_dir: Path) -> tuple[str, int]: """Execute the compiler and capture output. Uses a response file (@file.rsp) if command line is too long. Args: command: Compiler command as list working_dir: Working directory for execution Returns: Tuple of (output string, exit code) """ try: # Check if command line is too long (Windows limit is ~8191 characters) # Calculate full command line length command_line = " ".join(command) use_response_file = len(command_line) > 8000 if use_response_file: # Create a temporary response file response_file = working_dir / "delphi_compile.rsp" # Write all arguments (except compiler executable) to response file # Each argument on its own line with open(response_file, "w", encoding="utf-8") as f: for arg in command[1:]: # Skip compiler executable # Quote arguments that contain spaces if " " in arg and not arg.startswith('"'): f.write(f'"{arg}"\n') else: f.write(f"{arg}\n") # Build new command using response file actual_command = [command[0], f"@{response_file.name}"] else: actual_command = command response_file = None # Execute compilation result = subprocess.run( actual_command, cwd=str(working_dir), capture_output=True, text=True, encoding="utf-8", errors="replace", ) # Combine stdout and stderr output = result.stdout + "\n" + result.stderr # Clean up response file if we created one if response_file and response_file.exists(): response_file.unlink() return output, result.returncode except Exception as e: return f"Compiler execution failed: {e}", 1 def _find_output_executable( self, project_path: Path, dproj_settings: Optional[any] ) -> Optional[Path]: """Find the output executable after successful compilation. Args: project_path: Project file path dproj_settings: .dproj settings (if available) Returns: Path to output executable, or None if not found """ # Check if .dproj specifies output directory if dproj_settings and dproj_settings.output_dir: exe_name = project_path.stem + ".exe" exe_path = dproj_settings.output_dir / exe_name if exe_path.exists(): return exe_path # Check default location (same directory as project) exe_path = project_path.with_suffix(".exe") if exe_path.exists(): return exe_path # Check Win32/Debug and Win32/Release directories project_dir = project_path.parent exe_name = project_path.stem + ".exe" for subdir in ["Win32/Debug", "Win32/Release", "Win64/Debug", "Win64/Release"]: exe_path = project_dir / subdir / exe_name if exe_path.exists(): return exe_path return None

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Basti-Fantasti/delphi-build-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server