Started compilers

This commit is contained in:
Kyler Olsen 2025-12-16 22:24:09 -07:00
parent d775ab6067
commit a66bce0041
6 changed files with 982 additions and 0 deletions

View File

@ -0,0 +1,143 @@
"""
Compiler module for the build system.
This module provides compiler implementations for various toolchains:
- GCC (Unix/Linux)
- Clang (macOS/Unix)
- MSVC (Windows)
- ARM GCC (embedded ARM targets)
Usage:
from compilers import GCCCompiler, ClangCompiler, MSVCCompiler
compiler = GCCCompiler()
compiler.compile(source, output, includes=[...], defines={...})
"""
from typing import Optional
from .base import Compiler
from .gcc import GCCCompiler
from .clang import ClangCompiler
from .msvc import MSVCCompiler
from .arm_gcc import ARMGCCCompiler, RP2040Compiler
__all__ = [
'Compiler',
'GCCCompiler',
'ClangCompiler',
'MSVCCompiler',
'ARMGCCCompiler',
'RP2040Compiler',
]
def detect_compiler(platform: Optional[str] = None):
"""
Detect and return an appropriate compiler for the given platform.
Args:
platform: Target platform ('linux', 'windows', 'darwin'/'macos', 'rp2040', etc.)
If None, detects current platform.
Returns:
An instance of the appropriate Compiler subclass
Raises:
RuntimeError: If no suitable compiler is found
"""
import platform as plat
import shutil
# Detect current platform if not specified
if platform is None:
system = plat.system().lower()
if system == "darwin":
platform = "macos"
else:
platform = system
platform = platform.lower()
# Try to find appropriate compiler
if platform in ("linux", "unix"):
# Prefer GCC on Linux
if shutil.which("gcc"):
return GCCCompiler("gcc")
elif shutil.which("clang"):
return ClangCompiler("clang")
else:
raise RuntimeError("No suitable compiler found (tried gcc, clang)")
elif platform in ("macos", "darwin"):
# Prefer Clang on macOS (default compiler)
if shutil.which("clang"):
return ClangCompiler("clang")
elif shutil.which("gcc"):
return GCCCompiler("gcc")
else:
raise RuntimeError("No suitable compiler found (tried clang, gcc)")
elif platform == "windows":
# Try MSVC first, then MinGW GCC
if shutil.which("cl"):
return MSVCCompiler("cl")
elif shutil.which("gcc"):
return GCCCompiler("gcc")
else:
raise RuntimeError("No suitable compiler found (tried cl, gcc)")
elif platform == "rp2040":
# RP2040 needs ARM toolchain
if shutil.which("arm-none-eabi-gcc"):
return RP2040Compiler()
else:
raise RuntimeError(
"ARM GCC toolchain not found. Please install arm-none-eabi-gcc:\n"
" Ubuntu/Debian: sudo apt install gcc-arm-none-eabi\n"
" macOS: brew install arm-none-eabi-gcc"
)
else:
raise ValueError(f"Unknown platform: {platform}")
def get_available_compilers():
"""
Get a list of all compilers available on the current system.
Returns:
Dictionary mapping compiler names to Compiler instances
"""
import shutil
available = {}
# Check GCC
if shutil.which("gcc"):
available['gcc'] = GCCCompiler()
# Check Clang
if shutil.which("clang"):
available['clang'] = ClangCompiler()
# Check MSVC
if shutil.which("cl"):
available['msvc'] = MSVCCompiler()
# Check ARM GCC
if shutil.which("arm-none-eabi-gcc"):
available['arm-gcc'] = ARMGCCCompiler()
available['rp2040'] = RP2040Compiler()
return available
def print_available_compilers():
"""Print all available compilers on the system."""
compilers = get_available_compilers()
if not compilers:
print("No compilers found on this system.")
return
print("Available compilers:")
for name, compiler in compilers.items():
print(f" - {name}: {compiler.executable}")

View File

@ -0,0 +1,206 @@
"""
ARM GCC compiler implementation for embedded targets.
This module implements the Compiler interface for ARM GCC toolchain,
used for cross-compiling to ARM Cortex-M processors (e.g., RP2040).
"""
from pathlib import Path
from typing import List, Optional
from .gcc import GCCCompiler
class ARMGCCCompiler(GCCCompiler):
"""
ARM GCC compiler implementation for embedded targets.
Uses the arm-none-eabi-gcc toolchain for bare-metal ARM development.
Extends GCCCompiler with ARM Cortex-M specific flags.
"""
def __init__(
self,
executable: str = "arm-none-eabi-gcc",
cpu: str = "cortex-m0plus"
):
"""
Initialize ARM GCC compiler.
Args:
executable: ARM GCC executable (default: "arm-none-eabi-gcc")
cpu: Target CPU architecture (default: "cortex-m0plus" for RP2040)
"""
super().__init__(executable=executable, is_clang=False)
self.cpu = cpu
self.fpu = None
self.float_abi = None
self.additional_arch_flags = []
def set_cpu(self, cpu: str):
"""
Set target CPU architecture.
Args:
cpu: CPU name (e.g., "cortex-m0plus", "cortex-m4", "cortex-m7")
Returns:
Self for method chaining
"""
self.cpu = cpu
return self
def set_fpu(self, fpu: str, float_abi: str = "hard"):
"""
Set FPU configuration (for Cortex-M4F, M7, etc.).
Args:
fpu: FPU type (e.g., "fpv4-sp-d16", "fpv5-d16")
float_abi: Float ABI ("soft", "softfp", or "hard")
Returns:
Self for method chaining
"""
self.fpu = fpu
self.float_abi = float_abi
return self
def add_arch_flags(self, flags: List[str]):
"""
Add additional architecture-specific flags.
Args:
flags: List of flags to add
Returns:
Self for method chaining
"""
self.additional_arch_flags.extend(flags)
return self
def compile(
self,
source: Path,
output: Path,
*,
includes: Optional[List[Path]] = None,
defines: Optional[dict] = None,
flags: Optional[List[str]] = None,
generate_deps: bool = True,
is_test: bool = False
) -> Path:
"""
Compile using ARM GCC with embedded-specific flags.
Adds ARM Cortex-M architecture flags before calling parent compile.
"""
flags = flags or []
# ARM architecture flags (must come early)
arch_flags = [
f"-mcpu={self.cpu}",
"-mthumb", # Use Thumb instruction set
]
# FPU flags if configured
if self.fpu:
arch_flags.append(f"-mfpu={self.fpu}")
arch_flags.append(f"-mfloat-abi={self.float_abi}")
# Additional architecture flags
arch_flags.extend(self.additional_arch_flags)
# Prepend architecture flags (they need to come before other flags)
flags = arch_flags + flags
return super().compile(
source,
output,
includes=includes,
defines=defines,
flags=flags,
generate_deps=generate_deps,
is_test=is_test
)
def link(
self,
objects: List[Path],
output: Path,
*,
libraries: Optional[List[str]] = None,
library_paths: Optional[List[Path]] = None,
flags: Optional[List[str]] = None,
linker_script: Optional[Path] = None,
specs: Optional[str] = None
) -> Path:
"""
Link using ARM GCC with embedded-specific flags.
Args:
objects: Object files to link
output: Output executable path
libraries: Libraries to link
library_paths: Library search paths
flags: Additional linker flags
linker_script: Path to linker script (.ld file)
specs: Specs file (e.g., "nosys.specs", "nano.specs")
Returns:
Path to linked executable
"""
flags = flags or []
# ARM architecture flags (need to be passed to linker too)
arch_flags = [
f"-mcpu={self.cpu}",
"-mthumb",
]
# FPU flags
if self.fpu:
arch_flags.append(f"-mfpu={self.fpu}")
arch_flags.append(f"-mfloat-abi={self.float_abi}")
# Additional architecture flags
arch_flags.extend(self.additional_arch_flags)
# Linker script
if linker_script:
arch_flags.append(f"-T{linker_script}")
# Specs file (for selecting C library variant)
if specs:
arch_flags.append(f"--specs={specs}")
# Prepend architecture flags
flags = arch_flags + flags
return super().link(
objects,
output,
libraries=libraries,
library_paths=library_paths,
flags=flags
)
def get_executable_extension(self) -> str:
"""
Get executable extension for ARM embedded.
Returns .elf for embedded binaries (can be converted to .bin, .hex, .uf2)
"""
return ".elf"
class RP2040Compiler(ARMGCCCompiler):
"""
Specialized ARM GCC compiler for RP2040 (Raspberry Pi Pico).
Pre-configured for Cortex-M0+ with RP2040-specific settings.
"""
def __init__(self, executable: str = "arm-none-eabi-gcc"):
"""Initialize compiler for RP2040."""
super().__init__(executable=executable, cpu="cortex-m0plus")

View File

@ -0,0 +1,127 @@
"""
Abstract base class for compilers.
This module defines the interface that all compiler implementations must follow.
"""
from abc import ABC, abstractmethod
from pathlib import Path
from typing import List, Optional
class Compiler(ABC):
"""
Abstract base class for all compiler implementations.
Subclasses must implement compile() and link() methods with
compiler-specific logic.
"""
def __init__(self, executable: str):
"""
Initialize the compiler.
Args:
executable: Name or path to the compiler executable
"""
self.executable = executable
@abstractmethod
def compile(
self,
source: Path,
output: Path,
*,
includes: Optional[List[Path]] = None,
defines: Optional[dict] = None,
flags: Optional[List[str]] = None,
generate_deps: bool = True,
is_test: bool = False
) -> Path:
"""
Compile a single source file to an object file.
Args:
source: Path to source file (.c)
output: Path to output object file (.o or .obj)
includes: List of include directories
defines: Dictionary of preprocessor defines {name: value}
flags: Additional compiler flags
generate_deps: Whether to generate dependency files
is_test: Whether this is a test compilation (may affect optimization)
Returns:
Path to the compiled object file
"""
pass
@abstractmethod
def link(
self,
objects: List[Path],
output: Path,
*,
libraries: Optional[List[str]] = None,
library_paths: Optional[List[Path]] = None,
flags: Optional[List[str]] = None
) -> Path:
"""
Link object files into an executable.
Args:
objects: List of object files to link
output: Path to output executable
libraries: List of libraries to link against (e.g., ['m', 'pthread'])
library_paths: List of library search paths
flags: Additional linker flags
Returns:
Path to the linked executable
"""
pass
@abstractmethod
def get_object_extension(self) -> str:
"""
Get the object file extension for this compiler.
Returns:
Object file extension (e.g., '.o' or '.obj')
"""
pass
@abstractmethod
def get_executable_extension(self) -> str:
"""
Get the executable extension for this compiler/platform.
Returns:
Executable extension (e.g., '' for Unix, '.exe' for Windows)
"""
pass
def get_dependency_file(self, object_file: Path) -> Path:
"""
Get the dependency file path for an object file.
Args:
object_file: Path to object file
Returns:
Path to dependency file (usually .d extension)
"""
return object_file.with_suffix('.d')
def is_available(self) -> bool:
"""
Check if this compiler is available on the system.
Returns:
True if compiler executable exists in PATH
"""
import shutil
return shutil.which(self.executable) is not None
def __repr__(self) -> str:
return f"{self.__class__.__name__}(executable='{self.executable}')"

View File

@ -0,0 +1,128 @@
"""
Clang compiler implementation.
This module implements the Compiler interface specifically for Clang,
with any Clang-specific features or flags that differ from GCC.
"""
from pathlib import Path
from typing import List, Optional
from .gcc import GCCCompiler
class ClangCompiler(GCCCompiler):
"""
Clang compiler implementation.
Clang uses a GCC-compatible command-line interface, so most functionality
is inherited from GCCCompiler. This class adds Clang-specific features:
1. macOS version targeting
2. Sanitizer support
3. Future Clang-specific optimizations
"""
def __init__(self, executable: str = "clang"):
"""
Initialize Clang compiler.
Args:
executable: Clang executable name (default: "clang")
"""
# Call parent with is_clang=True
super().__init__(executable=executable, is_clang=True)
self.macos_min_version = None
self.sanitizers = []
def set_macos_version(self, min_version: str = "10.13"):
"""
Set minimum macOS version for cross-compilation or macOS builds.
This is a Clang-specific feature commonly used on macOS.
Args:
min_version: Minimum macOS version (e.g., "10.13")
Returns:
Self for method chaining
"""
self.macos_min_version = min_version
return self
def enable_sanitizers(self, sanitizers: List[str]):
"""
Enable Clang sanitizers (address, thread, undefined, memory, etc.).
Args:
sanitizers: List of sanitizers to enable (e.g., ['address', 'undefined'])
Returns:
Self for method chaining
"""
self.sanitizers = sanitizers
return self
def compile(
self,
source: Path,
output: Path,
*,
includes: Optional[List[Path]] = None,
defines: Optional[dict] = None,
flags: Optional[List[str]] = None,
generate_deps: bool = True,
is_test: bool = False
) -> Path:
"""
Compile using Clang with optional sanitizers and macOS flags.
Extends GCCCompiler.compile() with Clang-specific features.
"""
flags = flags or []
# Add macOS version flag if set
if self.macos_min_version:
flags.append(f"-mmacosx-version-min={self.macos_min_version}")
# Add sanitizers if enabled
for san in self.sanitizers:
flags.append(f"-fsanitize={san}")
return super().compile(
source,
output,
includes=includes,
defines=defines,
flags=flags,
generate_deps=generate_deps,
is_test=is_test
)
def link(
self,
objects: List[Path],
output: Path,
*,
libraries: Optional[List[str]] = None,
library_paths: Optional[List[Path]] = None,
flags: Optional[List[str]] = None
) -> Path:
"""
Link using Clang with optional sanitizer support.
Sanitizers need to be enabled at link time as well.
"""
flags = flags or []
# Add sanitizers at link time
for san in self.sanitizers:
flags.append(f"-fsanitize={san}")
return super().link(
objects,
output,
libraries=libraries,
library_paths=library_paths,
flags=flags
)

View File

@ -0,0 +1,185 @@
"""
GCC and Clang compiler implementation.
This module implements the Compiler interface for GCC and Clang,
which share similar command-line interfaces.
"""
from pathlib import Path
from typing import List, Optional
import subprocess
from .base import Compiler
class GCCCompiler(Compiler):
"""
GCC/Clang compiler implementation.
Works with both gcc and clang as they use compatible command-line syntax.
"""
def __init__(self, executable: str = "gcc", is_clang: bool = False):
"""
Initialize GCC/Clang compiler.
Args:
executable: Compiler executable name (default: "gcc")
is_clang: Whether this is clang (affects some flags)
"""
super().__init__(executable)
self.is_clang = is_clang
def compile(
self,
source: Path,
output: Path,
*,
includes: Optional[List[Path]] = None,
defines: Optional[dict] = None,
flags: Optional[List[str]] = None,
generate_deps: bool = True,
is_test: bool = False
) -> Path:
"""
Compile a source file using GCC/Clang.
Args:
source: Path to source file
output: Path to output object file
includes: Include directories
defines: Preprocessor defines
flags: Additional compiler flags
generate_deps: Generate dependency file
is_test: Test build (disables optimization)
Returns:
Path to compiled object file
"""
cmd = [self.executable]
# Add user flags first (can be overridden by later flags)
if flags:
cmd.extend(flags)
# Standard flags
cmd.extend([
"-std=c99",
"-Wall",
"-Wextra",
"-Werror",
"-g" # Debug symbols
])
# Test builds: disable optimization and allow unused functions
if is_test:
cmd.extend(["-O0", "-Wno-unused-function"])
# Include directories
if includes:
for inc in includes:
cmd.append(f"-I{inc}")
# Defines
if defines:
for name, value in defines.items():
if value is None:
cmd.append(f"-D{name}")
else:
# Properly escape string values
if isinstance(value, str):
cmd.append(f"-D{name}=\"{value}\"")
else:
cmd.append(f"-D{name}={value}")
# Dependency generation
if generate_deps:
cmd.extend(["-MMD", "-MP"])
# Compile only
cmd.extend(["-c", str(source)])
# Output file
cmd.extend(["-o", str(output)])
# Ensure output directory exists
output.parent.mkdir(parents=True, exist_ok=True)
# Run compilation
self._run(cmd)
return output
def link(
self,
objects: List[Path],
output: Path,
*,
libraries: Optional[List[str]] = None,
library_paths: Optional[List[Path]] = None,
flags: Optional[List[str]] = None
) -> Path:
"""
Link object files into an executable using GCC/Clang.
Args:
objects: Object files to link
output: Output executable path
libraries: Libraries to link (e.g., ['m', 'pthread'])
library_paths: Library search paths
flags: Additional linker flags
Returns:
Path to linked executable
"""
cmd = [self.executable]
# Add user flags first
if flags:
cmd.extend(flags)
# Object files
cmd.extend(str(obj) for obj in objects)
# Library paths
if library_paths:
for path in library_paths:
cmd.append(f"-L{path}")
# Libraries
if libraries:
for lib in libraries:
cmd.append(f"-l{lib}")
# Output file
cmd.extend(["-o", str(output)])
# Ensure output directory exists
output.parent.mkdir(parents=True, exist_ok=True)
# Run linker
self._run(cmd)
return output
def get_object_extension(self) -> str:
"""Get object file extension for GCC/Clang."""
return ".o"
def get_executable_extension(self) -> str:
"""Get executable extension (empty for Unix-like systems)."""
return ""
def _run(self, cmd: List[str]):
"""
Execute a command and handle errors.
Args:
cmd: Command to execute
Raises:
subprocess.CalledProcessError: If compilation/linking fails
"""
print(">>", " ".join(str(c) for c in cmd))
subprocess.check_call(cmd)

View File

@ -0,0 +1,193 @@
"""
MSVC (Microsoft Visual C++) compiler implementation.
This module implements the Compiler interface for Microsoft's MSVC compiler,
which has a completely different command-line syntax from GCC/Clang.
"""
from pathlib import Path
from typing import List, Optional
import subprocess
from .base import Compiler
class MSVCCompiler(Compiler):
"""
Microsoft Visual C++ compiler implementation.
MSVC uses different command-line syntax and conventions:
- Flags start with / instead of -
- Different flag names (/Fo vs -o, /Fe vs -o for linking)
- .obj instead of .o for object files
- .exe extension for executables
"""
def __init__(self, executable: str = "cl"):
"""
Initialize MSVC compiler.
Args:
executable: MSVC compiler executable (default: "cl")
"""
super().__init__(executable)
def compile(
self,
source: Path,
output: Path,
*,
includes: Optional[List[Path]] = None,
defines: Optional[dict] = None,
flags: Optional[List[str]] = None,
generate_deps: bool = True,
is_test: bool = False
) -> Path:
"""
Compile a source file using MSVC.
Args:
source: Path to source file
output: Path to output object file
includes: Include directories
defines: Preprocessor defines
flags: Additional compiler flags
generate_deps: Generate dependency file (not used by MSVC)
is_test: Test build (disables optimization)
Returns:
Path to compiled object file
"""
cmd = [self.executable]
# Add user flags first
if flags:
cmd.extend(flags)
# Standard flags
cmd.extend([
"/std:c11", # C11 standard (closest to c99)
"/W4", # Warning level 4 (similar to -Wall -Wextra)
"/WX", # Treat warnings as errors (like -Werror)
"/Zi", # Generate debug info (like -g)
])
# Test builds: disable optimization
if is_test:
cmd.append("/Od") # Disable optimization
# Include directories
if includes:
for inc in includes:
cmd.append(f"/I{inc}")
# Defines
if defines:
for name, value in defines.items():
if value is None:
cmd.append(f"/D{name}")
else:
# MSVC handles string escaping differently
if isinstance(value, str):
# Use backslash to escape quotes in MSVC
escaped = value.replace('"', '\\"')
cmd.append(f"/D{name}=\"{escaped}\"")
else:
cmd.append(f"/D{name}={value}")
# Compile only (don't link)
cmd.append("/c")
# Output file (note: /Fo with no space)
cmd.append(f"/Fo{output}")
# Source file
cmd.append(str(source))
# Ensure output directory exists
output.parent.mkdir(parents=True, exist_ok=True)
# Run compilation
self._run(cmd)
return output
def link(
self,
objects: List[Path],
output: Path,
*,
libraries: Optional[List[str]] = None,
library_paths: Optional[List[Path]] = None,
flags: Optional[List[str]] = None
) -> Path:
"""
Link object files into an executable using MSVC.
Args:
objects: Object files to link
output: Output executable path
libraries: Libraries to link (e.g., ['kernel32', 'user32'])
library_paths: Library search paths
flags: Additional linker flags
Returns:
Path to linked executable
"""
cmd = [self.executable]
# Add user flags first
if flags:
cmd.extend(flags)
# Object files
cmd.extend(str(obj) for obj in objects)
# Library paths
if library_paths:
for path in library_paths:
cmd.append(f"/LIBPATH:{path}")
# Libraries (MSVC uses .lib extension)
if libraries:
for lib in libraries:
# Add .lib if not present
if not lib.endswith('.lib'):
lib = f"{lib}.lib"
cmd.append(lib)
# Output file (note: /Fe with no space)
cmd.append(f"/Fe{output}")
# Link flag
cmd.append("/link")
# Ensure output directory exists
output.parent.mkdir(parents=True, exist_ok=True)
# Run linker
self._run(cmd)
return output
def get_object_extension(self) -> str:
"""Get object file extension for MSVC."""
return ".obj"
def get_executable_extension(self) -> str:
"""Get executable extension for Windows."""
return ".exe"
def _run(self, cmd: List[str]):
"""
Execute a command and handle errors.
Args:
cmd: Command to execute
Raises:
subprocess.CalledProcessError: If compilation/linking fails
"""
print(">>", " ".join(str(c) for c in cmd))
subprocess.check_call(cmd)