Refactored to use BaseGCCCompiler

This commit is contained in:
Kyler Olsen 2025-12-19 14:30:31 -07:00
parent a686c2c9e8
commit a6d867c31e
4 changed files with 279 additions and 234 deletions

View File

@ -6,11 +6,11 @@ Used primarily for RP2040 and other ARM Cortex-M microcontrollers.
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List, Optional
from .base import Compiler, CompileResult from .base_gcc import BaseGCCCompiler
from build_system.utils import run from .base import CompileResult
class ARMGCCCompiler(Compiler): class ARMGCCCompiler(BaseGCCCompiler):
"""ARM GCC compiler for embedded targets.""" """ARM GCC compiler for embedded targets."""
def __init__(self, executable: str = "arm-none-eabi-gcc", cpu: str = "cortex-m0plus"): def __init__(self, executable: str = "arm-none-eabi-gcc", cpu: str = "cortex-m0plus"):
@ -25,6 +25,18 @@ class ARMGCCCompiler(Compiler):
self.cpu = cpu self.cpu = cpu
self.thumb = True # Use Thumb instruction set by default self.thumb = True # Use Thumb instruction set by default
def _get_cpu_flags(self) -> List[str]:
"""
Get CPU-specific flags.
Returns:
List of CPU-specific compiler flags
"""
flags = [f"-mcpu={self.cpu}"]
if self.thumb:
flags.append("-mthumb")
return flags
def compile(self, def compile(self,
source: Path, source: Path,
output: Path, output: Path,
@ -49,51 +61,18 @@ class ARMGCCCompiler(Compiler):
Raises: Raises:
subprocess.CalledProcessError: If compilation fails subprocess.CalledProcessError: If compilation fails
""" """
# Ensure output directory exists # Prepend CPU-specific flags to user flags
output.parent.mkdir(parents=True, exist_ok=True) cpu_flags = self._get_cpu_flags()
combined_flags = cpu_flags + (flags or [])
# Build command # Call parent compile with combined flags
cmd = [self.executable] return super().compile(
source=source,
# Add CPU-specific flags output=output,
cmd.append(f"-mcpu={self.cpu}") flags=combined_flags,
if self.thumb: defines=defines,
cmd.append("-mthumb") include_dirs=include_dirs,
dependency_file=dependency_file
# Add flags
if flags:
cmd.extend(flags)
# Add compile-only flag
cmd.append(self.get_compile_only_flag())
# Add defines
if defines:
cmd.extend([self.get_define_flag(d) for d in defines])
# Add include directories
if include_dirs:
cmd.extend([self.get_include_flag(d) for d in include_dirs])
# Add dependency generation if requested
dep_file = None
if dependency_file:
cmd.extend(["-MMD", "-MP"])
dep_file = dependency_file
# Add source file
cmd.append(str(source))
# Add output file
cmd.extend(self.get_output_flag(output))
# Run compilation
run(cmd) # type: ignore
return CompileResult(
object_file=output,
dependency_file=dep_file,
success=True
) )
def link(self, def link(self,
@ -118,51 +97,18 @@ class ARMGCCCompiler(Compiler):
Raises: Raises:
subprocess.CalledProcessError: If linking fails subprocess.CalledProcessError: If linking fails
""" """
# Ensure output directory exists # Prepend CPU-specific flags to user flags
output.parent.mkdir(parents=True, exist_ok=True) cpu_flags = self._get_cpu_flags()
combined_flags = cpu_flags + (flags or [])
# Build command # Call parent link with combined flags
cmd = [self.executable] return super().link(
objects=objects,
# Add CPU-specific flags output=output,
cmd.append(f"-mcpu={self.cpu}") flags=combined_flags,
if self.thumb: libraries=libraries,
cmd.append("-mthumb") library_dirs=library_dirs
)
# Add object files
cmd.extend([str(obj) for obj in objects])
# Add linker flags
if flags:
cmd.extend(flags)
# Add library directories
if library_dirs:
cmd.extend([f"-L{lib_dir}" for lib_dir in library_dirs])
# Add libraries
if libraries:
cmd.extend([f"-l{lib}" for lib in libraries])
# Add output file
cmd.extend(["-o", str(output)])
# Run linker
run(cmd) # type: ignore
return True
def get_object_extension(self) -> str:
"""Get object file extension for ARM GCC."""
return ".o"
def get_dependency_extension(self) -> str:
"""Get dependency file extension for ARM GCC."""
return ".d"
def supports_dependency_generation(self) -> bool:
"""ARM GCC supports automatic dependency generation."""
return True
def set_cpu(self, cpu: str) -> None: def set_cpu(self, cpu: str) -> None:
""" """
@ -183,6 +129,22 @@ class ARMGCCCompiler(Compiler):
self.thumb = enabled self.thumb = enabled
def detect_arm_gcc_compiler() -> Optional[ARMGCCCompiler]:
"""
Detect and return an available ARM GCC compiler.
Returns:
ARMGCCCompiler instance if found, None otherwise
"""
from build_system.utils import which
# Try to find ARM GCC
if which("arm-none-eabi-gcc"):
return ARMGCCCompiler()
return None
def check_arm_toolchain() -> bool: def check_arm_toolchain() -> bool:
""" """
Check if ARM toolchain is available. Check if ARM toolchain is available.
@ -200,3 +162,4 @@ def check_arm_toolchain() -> bool:
" Ubuntu/Debian: sudo apt install gcc-arm-none-eabi\n" " Ubuntu/Debian: sudo apt install gcc-arm-none-eabi\n"
" macOS: brew install arm-none-eabi-gcc" " macOS: brew install arm-none-eabi-gcc"
) )
return False

View File

@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""
Base GCC-like compiler implementation.
Provides common functionality for GCC, Clang, and other GCC-compatible compilers.
"""
from pathlib import Path
from typing import List, Optional
from .base import Compiler, CompileResult
from build_system.utils import run
class BaseGCCCompiler(Compiler):
"""Base class for GCC-compatible compilers (GCC, Clang, etc.)."""
def __init__(self, name: str, executable: str):
"""
Initialize base GCC-compatible compiler.
Args:
name: Human-readable compiler name
executable: Compiler executable name
"""
super().__init__(name, executable)
def compile(self,
source: Path,
output: Path,
flags: Optional[List[str]] = None,
defines: Optional[List[str]] = None,
include_dirs: Optional[List[Path]] = None,
dependency_file: Optional[Path] = None) -> CompileResult:
"""
Compile a source file using GCC-compatible compiler.
Args:
source: Source file path
output: Output object file path
flags: Additional compiler flags
defines: Preprocessor defines
include_dirs: Additional include directories
dependency_file: Path for dependency file (.d file)
Returns:
CompileResult with object file and dependency file paths
Raises:
subprocess.CalledProcessError: If compilation fails
"""
# Ensure output directory exists
output.parent.mkdir(parents=True, exist_ok=True)
# Build command
cmd = [self.executable]
# Add flags
if flags:
cmd.extend(flags)
# Add compile-only flag
cmd.append(self.get_compile_only_flag())
# Add defines
if defines:
cmd.extend([self.get_define_flag(d) for d in defines])
# Add include directories
if include_dirs:
cmd.extend([self.get_include_flag(d) for d in include_dirs])
# Add dependency generation if requested
dep_file = None
if dependency_file:
cmd.extend(["-MMD", "-MP"])
dep_file = dependency_file
# Add source file
cmd.append(str(source))
# Add output file
cmd.extend(self.get_output_flag(output))
# Run compilation
run(cmd) # type: ignore
return CompileResult(
object_file=output,
dependency_file=dep_file,
success=True
)
def link(self,
objects: List[Path],
output: Path,
flags: Optional[List[str]] = None,
libraries: Optional[List[str]] = None,
library_dirs: Optional[List[Path]] = None) -> bool:
"""
Link object files into an executable using GCC-compatible compiler.
Args:
objects: List of object file paths
output: Output executable path
flags: Additional linker flags
libraries: Libraries to link against
library_dirs: Additional library search directories
Returns:
True if linking succeeded
Raises:
subprocess.CalledProcessError: If linking fails
"""
# Ensure output directory exists
output.parent.mkdir(parents=True, exist_ok=True)
# Build command
cmd = [self.executable]
# Add object files
cmd.extend([str(obj) for obj in objects])
# Add linker flags
if flags:
cmd.extend(flags)
# Add library directories
if library_dirs:
cmd.extend([f"-L{lib_dir}" for lib_dir in library_dirs])
# Add libraries
if libraries:
cmd.extend([f"-l{lib}" for lib in libraries])
# Add output file
cmd.extend(["-o", str(output)])
# Run linker
run(cmd) # type: ignore
return True
def get_object_extension(self) -> str:
"""Get object file extension for GCC-compatible compilers."""
return ".o"
def get_dependency_extension(self) -> str:
"""Get dependency file extension for GCC-compatible compilers."""
return ".d"
def supports_dependency_generation(self) -> bool:
"""GCC-compatible compilers support automatic dependency generation."""
return True

View File

@ -1,17 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Clang compiler implementation. Clang compiler implementation.
While Clang is compatible with GCC, this provides a dedicated implementation
for cases where Clang-specific features or flags are needed.
""" """
from pathlib import Path from typing import Optional
from typing import List, Optional from .base_gcc import BaseGCCCompiler
from .gcc import GCCCompiler import subprocess
class ClangCompiler(GCCCompiler): class ClangCompiler(BaseGCCCompiler):
"""Clang compiler implementation (inherits from GCC).""" """Clang compiler implementation."""
def __init__(self, executable: str = "clang"): def __init__(self, executable: str = "clang"):
""" """
@ -20,9 +18,7 @@ class ClangCompiler(GCCCompiler):
Args: Args:
executable: Compiler executable name ("clang") executable: Compiler executable name ("clang")
""" """
# Call parent constructor but override the name super().__init__("Clang", executable)
super().__init__(executable)
self.name = "Clang"
def get_version(self) -> Optional[str]: def get_version(self) -> Optional[str]:
""" """
@ -31,10 +27,13 @@ class ClangCompiler(GCCCompiler):
Returns: Returns:
Version string if available, None otherwise Version string if available, None otherwise
""" """
from build_system.utils import run_quiet
try: try:
result = run_quiet([self.executable, "--version"], check=False) result = subprocess.run(
[self.executable, "--version"],
capture_output=True,
text=True,
check=False
)
if result.returncode == 0 and result.stdout: if result.returncode == 0 and result.stdout:
# Parse first line of version output # Parse first line of version output
first_line = result.stdout.strip().split('\n')[0] first_line = result.stdout.strip().split('\n')[0]
@ -43,3 +42,22 @@ class ClangCompiler(GCCCompiler):
pass pass
return None return None
def detect_clang_compiler() -> Optional[ClangCompiler]:
"""
Detect and return an available Clang compiler.
Returns:
ClangCompiler instance if found, None otherwise
"""
from build_system.utils import which
# Try to find Clang
compilers = ["clang", "clang-18", "clang-17", "clang-16", "clang-15"]
for compiler in compilers:
if which(compiler):
return ClangCompiler(compiler)
return None

View File

@ -1,153 +1,64 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
GCC/Clang compiler implementation. GCC compiler implementation.
Handles both GCC and Clang as they use similar command-line interfaces.
""" """
from pathlib import Path from typing import Optional
from typing import List, Optional from .base_gcc import BaseGCCCompiler
from .base import Compiler, CompileResult
from build_system.utils import run
class GCCCompiler(Compiler): class GCCCompiler(BaseGCCCompiler):
"""GCC or Clang compiler implementation.""" """GCC compiler implementation."""
def __init__(self, executable: str = "gcc"): def __init__(self, executable: str = "gcc"):
""" """
Initialize GCC/Clang compiler. Initialize GCC compiler.
Args: Args:
executable: Compiler executable name ("gcc" or "clang") executable: Compiler executable name ("gcc")
""" """
name = "Clang" if "clang" in executable.lower() else "GCC" super().__init__("GCC", executable)
super().__init__(name, executable)
def compile(self, def get_version(self) -> Optional[str]:
source: Path,
output: Path,
flags: Optional[List[str]] = None,
defines: Optional[List[str]] = None,
include_dirs: Optional[List[Path]] = None,
dependency_file: Optional[Path] = None) -> CompileResult:
""" """
Compile a source file using GCC/Clang. Get the GCC version string.
Args:
source: Source file path
output: Output object file path
flags: Additional compiler flags
defines: Preprocessor defines
include_dirs: Additional include directories
dependency_file: Path for dependency file (.d file)
Returns: Returns:
CompileResult with object file and dependency file paths Version string if available, None otherwise
Raises:
subprocess.CalledProcessError: If compilation fails
""" """
# Ensure output directory exists import subprocess
output.parent.mkdir(parents=True, exist_ok=True)
# Build command try:
cmd = [self.executable] result = subprocess.run(
[self.executable, "--version"],
capture_output=True,
text=True,
check=False
)
if result.returncode == 0 and result.stdout:
# Parse first line of version output
first_line = result.stdout.strip().split('\n')[0]
return first_line
except Exception:
pass
# Add flags return None
if flags:
cmd.extend(flags)
def detect_gcc_compiler() -> Optional[GCCCompiler]:
# Add compile-only flag """
cmd.append(self.get_compile_only_flag()) Detect and return an available GCC compiler.
# Add defines
if defines:
cmd.extend([self.get_define_flag(d) for d in defines])
# Add include directories
if include_dirs:
cmd.extend([self.get_include_flag(d) for d in include_dirs])
# Add dependency generation if requested
dep_file = None
if dependency_file:
cmd.extend(["-MMD", "-MP"])
dep_file = dependency_file
# Add source file
cmd.append(str(source))
# Add output file
cmd.extend(self.get_output_flag(output))
# Run compilation
run(cmd) # type: ignore
return CompileResult(
object_file=output,
dependency_file=dep_file,
success=True
)
def link(self, Returns:
objects: List[Path], GCCCompiler instance if found, None otherwise
output: Path, """
flags: Optional[List[str]] = None, from build_system.utils import which
libraries: Optional[List[str]] = None,
library_dirs: Optional[List[Path]] = None) -> bool:
"""
Link object files into an executable using GCC/Clang.
Args:
objects: List of object file paths
output: Output executable path
flags: Additional linker flags
libraries: Libraries to link against
library_dirs: Additional library search directories
Returns:
True if linking succeeded
Raises:
subprocess.CalledProcessError: If linking fails
"""
# Ensure output directory exists
output.parent.mkdir(parents=True, exist_ok=True)
# Build command
cmd = [self.executable]
# Add object files
cmd.extend([str(obj) for obj in objects])
# Add linker flags
if flags:
cmd.extend(flags)
# Add library directories
if library_dirs:
cmd.extend([f"-L{lib_dir}" for lib_dir in library_dirs])
# Add libraries
if libraries:
cmd.extend([f"-l{lib}" for lib in libraries])
# Add output file
cmd.extend(["-o", str(output)])
# Run linker
run(cmd) # type: ignore
return True
def get_object_extension(self) -> str: # Try to find GCC
"""Get object file extension for GCC/Clang.""" compilers = ["gcc", "gcc-13", "gcc-12", "gcc-11", "cc"]
return ".o"
def get_dependency_extension(self) -> str: for compiler in compilers:
"""Get dependency file extension for GCC/Clang.""" if which(compiler):
return ".d" return GCCCompiler(compiler)
def supports_dependency_generation(self) -> bool: return None
"""GCC/Clang supports automatic dependency generation."""
return True