diff --git a/SLS_C/build_system/compilers/__init__.py b/SLS_C/build_system/compilers/__init__.py new file mode 100644 index 0000000..7953aa4 --- /dev/null +++ b/SLS_C/build_system/compilers/__init__.py @@ -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}") diff --git a/SLS_C/build_system/compilers/arm_gcc.py b/SLS_C/build_system/compilers/arm_gcc.py new file mode 100644 index 0000000..f882532 --- /dev/null +++ b/SLS_C/build_system/compilers/arm_gcc.py @@ -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") + diff --git a/SLS_C/build_system/compilers/base.py b/SLS_C/build_system/compilers/base.py new file mode 100644 index 0000000..f1e94e5 --- /dev/null +++ b/SLS_C/build_system/compilers/base.py @@ -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}')" + diff --git a/SLS_C/build_system/compilers/clang.py b/SLS_C/build_system/compilers/clang.py new file mode 100644 index 0000000..c01da8d --- /dev/null +++ b/SLS_C/build_system/compilers/clang.py @@ -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 + ) + diff --git a/SLS_C/build_system/compilers/gcc.py b/SLS_C/build_system/compilers/gcc.py new file mode 100644 index 0000000..628128a --- /dev/null +++ b/SLS_C/build_system/compilers/gcc.py @@ -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) + diff --git a/SLS_C/build_system/compilers/msvc.py b/SLS_C/build_system/compilers/msvc.py new file mode 100644 index 0000000..687b5b6 --- /dev/null +++ b/SLS_C/build_system/compilers/msvc.py @@ -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) +