diff --git a/SLS_C/build_system/compilers/__init__.py b/SLS_C/build_system/compilers/__init__.py index 7953aa4..e69de29 100644 --- a/SLS_C/build_system/compilers/__init__.py +++ b/SLS_C/build_system/compilers/__init__.py @@ -1,143 +0,0 @@ -""" -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 index f882532..e69de29 100644 --- a/SLS_C/build_system/compilers/arm_gcc.py +++ b/SLS_C/build_system/compilers/arm_gcc.py @@ -1,206 +0,0 @@ -""" -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 index f1e94e5..e69de29 100644 --- a/SLS_C/build_system/compilers/base.py +++ b/SLS_C/build_system/compilers/base.py @@ -1,127 +0,0 @@ -""" -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 index c01da8d..e69de29 100644 --- a/SLS_C/build_system/compilers/clang.py +++ b/SLS_C/build_system/compilers/clang.py @@ -1,128 +0,0 @@ -""" -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 index 628128a..e69de29 100644 --- a/SLS_C/build_system/compilers/gcc.py +++ b/SLS_C/build_system/compilers/gcc.py @@ -1,185 +0,0 @@ -""" -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 index 687b5b6..e69de29 100644 --- a/SLS_C/build_system/compilers/msvc.py +++ b/SLS_C/build_system/compilers/msvc.py @@ -1,193 +0,0 @@ -""" -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) - diff --git a/SLS_C/build_system/config.json b/SLS_C/build_system/config.json index aa82cc1..e69de29 100644 --- a/SLS_C/build_system/config.json +++ b/SLS_C/build_system/config.json @@ -1,67 +0,0 @@ -{ - "paths": { - "src_dir": "src", - "test_dir": "tests", - "include_dir": "include", - "obj_dir": "obj", - "bin_dir": "bin", - "build_dir": "build" - }, - "targets": { - "main": "sls", - "test": "sls_tests" - }, - "parallel_jobs": 0, - "verbose": false, - "debug": true, - "gcc_common_flags": [ - "-std=c99", - "-Wall", - "-Wextra", - "-Werror", - "-g" - ], - "gcc_test_flags": [ - "-std=c99", - "-Wall", - "-Wextra", - "-Wno-unused-function", - "-Werror", - "-g", - "-O0" - ], - "msvc_common_flags": [ - "/std:c11", - "/W4", - "/WX", - "/Zi" - ], - "msvc_test_flags": [ - "/std:c11", - "/W4", - "/WX", - "/Zi", - "/Od" - ], - "arm_gcc_flags": [ - "-mcpu=cortex-m0plus", - "-mthumb" - ], - "pico": { - "sdk_path_env": "PICO_SDK_PATH", - "sdk_path_default": "~/pico/pico-sdk", - "build_dir": "build_pico", - "toolchain_file": "pico_arm_gcc_toolchain.cmake" - }, - "macos": { - "min_version": "10.13" - }, - "source_excludes": { - "rp2040": [ - "main.c", - "repl.c", - "file.c", - "test" - ] - } -} diff --git a/SLS_C/build_system/config.py b/SLS_C/build_system/config.py index 9225f32..e69de29 100644 --- a/SLS_C/build_system/config.py +++ b/SLS_C/build_system/config.py @@ -1,301 +0,0 @@ -""" -Build system configuration. - -This module centralizes all build configuration including paths, -compiler flags, and build settings. Configuration can be loaded from -a default JSON file or overridden programmatically. -""" - -import os -import json -from pathlib import Path -from typing import List, Dict, Optional - -from importlib import resources - - -# TODO: This should be utilized more throughout the build system. -class Config: - """ - Centralized build configuration. - - All paths, flags, and settings are defined here for easy modification. - Configuration is loaded from a default JSON file and can be overridden. - """ - - def __init__(self, project_root: Optional[Path] = None, config_file: Optional[Path] = None): - """ - Initialize build configuration. - - Args: - project_root: Project root directory (default: current directory) - config_file: Custom config file path (default: uses built-in config.json) - """ - # Load default configuration - self._load_default_config(config_file) - - # Project structure - self.project_root = project_root or Path.cwd() - self._setup_paths() - - # Build settings - self.parallel_jobs = self._data.get("parallel_jobs", os.cpu_count() or 1) - self.verbose = self._data.get("verbose", False) - self.debug = self._data.get("debug", True) - - # Git integration - self.git_hash = self._get_git_hash() - - # Compiler flags - self.gcc_common_flags = self._data.get("gcc_common_flags", []) - self.gcc_test_flags = self._data.get("gcc_test_flags", []) - self.msvc_common_flags = self._data.get("msvc_common_flags", []) - self.msvc_test_flags = self._data.get("msvc_test_flags", []) - self.arm_gcc_flags = self._data.get("arm_gcc_flags", []) - - # Platform-specific settings - self._setup_platform_settings() - - def _load_default_config(self, config_file: Optional[Path] = None): - """ - Load configuration from JSON file. - - Args: - config_file: Custom config file, or None to use default - """ - if config_file and config_file.exists(): - # Load custom config file - with open(config_file, 'r') as f: - self._data = json.load(f) - else: - # Load default config from package resources - config_text = resources.files("build_system").joinpath("config.json").read_text() - self._data = json.loads(config_text) - - def _setup_paths(self): - """Setup directory paths from configuration.""" - paths = self._data.get("paths", {}) - - self.src_dir = self.project_root / paths.get("src_dir", "src") - self.test_dir = self.project_root / paths.get("test_dir", "tests") - self.include_dir = self.project_root / paths.get("include_dir", "include") - self.obj_dir = self.project_root / paths.get("obj_dir", "obj") - self.bin_dir = self.project_root / paths.get("bin_dir", "bin") - self.build_dir = self.project_root / paths.get("build_dir", "build") - - # Target executables - targets = self._data.get("targets", {}) - self.main_target = self.bin_dir / targets.get("main", "sls") - self.test_target = self.bin_dir / targets.get("test", "sls_tests") - - def _setup_platform_settings(self): - """Setup platform-specific settings from configuration.""" - pico_config = self._data.get("pico", {}) - - # Pico SDK settings - sdk_env = pico_config.get("sdk_path_env", "PICO_SDK_PATH") - sdk_default = pico_config.get("sdk_path_default", "~/pico/pico-sdk") - sdk_default = Path(sdk_default).expanduser() - - self.pico_sdk_path = Path(os.environ.get(sdk_env, sdk_default)) - self.pico_build_dir = self.project_root / pico_config.get("build_dir", "build_pico") - self.pico_toolchain_file = self.project_root / pico_config.get( - "toolchain_file", "pico_arm_gcc_toolchain.cmake" - ) - - # macOS settings - macos_config = self._data.get("macos", {}) - self.macos_min_version = macos_config.get("min_version", "10.13") - - def _get_git_hash(self) -> str: - """Get git commit hash for version info.""" - from .utils import git_commit_hash - return git_commit_hash() - - def get_defines(self, extra: Optional[Dict] = None) -> Dict: - """ - Get preprocessor defines. - - Args: - extra: Additional defines to include - - Returns: - Dictionary of defines - """ - defines = { - "GIT_COMMIT_HASH": self.git_hash, - } - - if extra: - defines.update(extra) - - return defines - - def get_includes(self, extra: Optional[List[Path]] = None) -> List[Path]: - """ - Get include directories. - - Args: - extra: Additional include directories - - Returns: - List of include directory paths - """ - includes = [self.include_dir] - - if extra: - includes.extend(extra) - - return includes - - def get_libraries(self, target: str = "linux") -> List[str]: - """ - Get libraries to link based on target. - - Args: - target: Target platform - - Returns: - List of library names - """ - if target in ("linux", "macos"): - return ["m"] # Math library - elif target == "windows": - return [] # No special libraries needed - elif target == "rp2040": - return [] # Handled by Pico SDK - else: - return [] - - def get_source_files(self, exclude: Optional[List[str]] = None) -> List[Path]: - """ - Get list of source files. - - Args: - exclude: List of filenames to exclude - - Returns: - List of source file paths - """ - from .utils import find_sources - - exclude = exclude or [] - return find_sources(self.src_dir, "*.c", exclude) - - def get_test_files(self) -> List[Path]: - """ - Get list of test source files. - - Returns: - List of test file paths - """ - from .utils import find_sources - return find_sources(self.test_dir, "*.c") - - def get_main_sources(self) -> List[Path]: - """ - Get main program source files (excludes main.c and pico_main.c). - - Returns: - List of source files for linking with tests - """ - return self.get_source_files(exclude=["main.c", "pico_main.c"]) - - def get_rp2040_sources(self) -> List[Path]: - """ - Get source files for RP2040 build. - - Returns: - List of source files (excludes main.c, repl.c, file.c, test files) - """ - excludes = self._data.get("source_excludes", {}).get("rp2040", []) - sources = self.get_source_files(exclude=excludes) - - # Add pico_main.c if it exists - pico_main = self.src_dir / "pico_main.c" - if pico_main.exists(): - sources.append(pico_main) - - return sources - - def set_verbose(self, verbose: bool = True): - """ - Enable/disable verbose output. - - Args: - verbose: Whether to enable verbose output - """ - self.verbose = verbose - return self - - def set_debug(self, debug: bool = True): - """ - Enable/disable debug build. - - Args: - debug: Whether to build with debug symbols - """ - self.debug = debug - return self - - def set_parallel_jobs(self, jobs: int): - """ - Set number of parallel build jobs. - - Args: - jobs: Number of parallel jobs (0 = auto-detect) - """ - if jobs <= 0: - self.parallel_jobs = os.cpu_count() or 1 - else: - self.parallel_jobs = jobs - return self - - def save_config(self, output_file: Path): - """ - Save current configuration to a JSON file. - - Args: - output_file: Path to output JSON file - """ - with open(output_file, 'w') as f: - json.dump(self._data, f, indent=2) - - def __repr__(self) -> str: - """String representation of config.""" - return ( - f"Config(\n" - f" project_root={self.project_root}\n" - f" src_dir={self.src_dir}\n" - f" bin_dir={self.bin_dir}\n" - f" git_hash={self.git_hash}\n" - f" parallel_jobs={self.parallel_jobs}\n" - f")" - ) - - -# Global configuration instance -_config = None - - -def get_config(project_root: Optional[Path] = None, config_file: Optional[Path] = None) -> Config: - """ - Get the global configuration instance. - - Args: - project_root: Project root directory (only used on first call) - config_file: Custom config file (only used on first call) - - Returns: - Config instance - """ - global _config - if _config is None: - _config = Config(project_root, config_file) - return _config - - -def reset_config(): - """Reset the global configuration (mainly for testing).""" - global _config - _config = None diff --git a/SLS_C/build_system/platform.py b/SLS_C/build_system/platform.py index 1178d36..e69de29 100644 --- a/SLS_C/build_system/platform.py +++ b/SLS_C/build_system/platform.py @@ -1,241 +0,0 @@ -""" -Platform detection and information module. - -This module provides utilities for detecting the current platform -and getting platform-specific information. -""" - -import platform -import sys -from typing import Optional, Tuple - - -class Platform: - """Platform information and detection.""" - - # Supported platforms - LINUX = "linux" - WINDOWS = "windows" - MACOS = "macos" - RP2040 = "rp2040" - - # Platform aliases - ALIASES = { - "darwin": MACOS, - "win32": WINDOWS, - "cygwin": WINDOWS, - "msys": WINDOWS, - "pico": RP2040, - } - - def __init__(self): - """Initialize platform detection.""" - self._system = None - self._machine = None - self._detected = None - - @property - def system(self) -> str: - """Get the system name (Linux, Windows, Darwin, etc.).""" - if self._system is None: - self._system = platform.system() - return self._system - - @property - def machine(self) -> str: - """Get the machine architecture (x86_64, arm, etc.).""" - if self._machine is None: - self._machine = platform.machine() - return self._machine - - def detect(self) -> str: - """ - Detect the current platform. - - Returns: - Platform string ('linux', 'windows', 'macos') - """ - if self._detected is not None: - return self._detected - - system = self.system.lower() - - # Check aliases - for alias, platform_name in self.ALIASES.items(): - if alias in system: - self._detected = platform_name - return self._detected - - # Direct mapping - if system == "linux": - self._detected = self.LINUX - elif system == "darwin": - self._detected = self.MACOS - elif system == "windows": - self._detected = self.WINDOWS - else: - # Unknown platform, default to linux - self._detected = self.LINUX - - return self._detected - - def is_linux(self) -> bool: - """Check if current platform is Linux.""" - return self.detect() == self.LINUX - - def is_windows(self) -> bool: - """Check if current platform is Windows.""" - return self.detect() == self.WINDOWS - - def is_macos(self) -> bool: - """Check if current platform is macOS.""" - return self.detect() == self.MACOS - - def is_64bit(self) -> bool: - """Check if running on 64-bit architecture.""" - return sys.maxsize > 2**32 - - def get_cpu_count(self) -> int: - """Get number of CPU cores.""" - import os - return os.cpu_count() or 1 - - def get_exe_extension(self) -> str: - """ - Get the executable file extension for this platform. - - Returns: - '.exe' on Windows, '' on Unix-like systems - """ - return ".exe" if self.is_windows() else "" - - def get_shared_lib_extension(self) -> str: - """ - Get the shared library extension for this platform. - - Returns: - '.dll' on Windows, '.dylib' on macOS, '.so' on Linux - """ - if self.is_windows(): - return ".dll" - elif self.is_macos(): - return ".dylib" - else: - return ".so" - - def normalize_platform_name(self, name: str) -> str: - """ - Normalize a platform name to canonical form. - - Args: - name: Platform name (possibly an alias) - - Returns: - Canonical platform name - """ - name = name.lower() - - # Check if it's already canonical - if name in [self.LINUX, self.WINDOWS, self.MACOS, self.RP2040]: - return name - - # Check aliases - for alias, platform_name in self.ALIASES.items(): - if alias in name: - return platform_name - - # Return as-is if unknown - return name - - def get_platform_info(self) -> dict: - """ - Get detailed platform information. - - Returns: - Dictionary with platform details - """ - return { - 'platform': self.detect(), - 'system': self.system, - 'machine': self.machine, - 'architecture': platform.architecture()[0], - 'python_version': platform.python_version(), - 'is_64bit': self.is_64bit(), - 'cpu_count': self.get_cpu_count(), - } - - def __str__(self) -> str: - """String representation of platform.""" - return self.detect() - - def __repr__(self) -> str: - """Detailed representation.""" - return f"Platform(system='{self.system}', detected='{self.detect()}')" - - -# Global platform instance -_platform = Platform() - - -def detect_platform() -> str: - """ - Detect the current platform. - - Returns: - Platform string ('linux', 'windows', 'macos') - """ - return _platform.detect() - - -def get_platform() -> Platform: - """ - Get the global Platform instance. - - Returns: - Platform instance - """ - return _platform - - -def is_linux() -> bool: - """Check if current platform is Linux.""" - return _platform.is_linux() - - -def is_windows() -> bool: - """Check if current platform is Windows.""" - return _platform.is_windows() - - -def is_macos() -> bool: - """Check if current platform is macOS.""" - return _platform.is_macos() - - -def normalize_target(target: Optional[str]) -> str: - """ - Normalize a target platform name. - - Args: - target: Target name or 'self' for current platform - - Returns: - Normalized platform name - """ - if target is None or target.lower() == "self": - return detect_platform() - - return _platform.normalize_platform_name(target) - - -def print_platform_info(): - """Print detailed platform information.""" - info = _platform.get_platform_info() - print("Platform Information:") - print(f" Platform: {info['platform']}") - print(f" System: {info['system']}") - print(f" Machine: {info['machine']}") - print(f" Architecture: {info['architecture']}") - print(f" Python: {info['python_version']}") - print(f" 64-bit: {info['is_64bit']}") - print(f" CPU Cores: {info['cpu_count']}") diff --git a/SLS_C/build_system/targets/base.py b/SLS_C/build_system/targets/base.py index 301ede8..e69de29 100644 --- a/SLS_C/build_system/targets/base.py +++ b/SLS_C/build_system/targets/base.py @@ -1,327 +0,0 @@ -""" -Abstract base class for build targets. - -This module defines the interface that all build target implementations -must follow. A target represents a platform/architecture combination -(e.g., Linux, Windows, RP2040) and knows how to build for that platform. -""" - -from abc import ABC, abstractmethod -from pathlib import Path -from typing import List, Optional -from ..config import Config -from ..compilers.base import Compiler - - -class Target(ABC): - """ - Abstract base class for all build targets. - - A target encapsulates the knowledge of how to build for a specific - platform, including which compiler to use, what flags to set, and - what build steps to execute. - """ - - def __init__(self, config: Config, compiler: Optional[Compiler] = None): - """ - Initialize the target. - - Args: - config: Build configuration - compiler: Compiler to use (if None, target will auto-detect) - """ - self.config = config - self.compiler = compiler or self._get_default_compiler() - self._compiled_objects = [] - - @abstractmethod - def _get_default_compiler(self) -> Compiler: - """ - Get the default compiler for this target. - - Returns: - Compiler instance appropriate for this target - """ - pass - - @abstractmethod - def get_name(self) -> str: - """ - Get the target name. - - Returns: - Target name (e.g., 'linux', 'windows', 'rp2040') - """ - pass - - def build(self): - """ - Build the main executable for this target. - - This is the main entry point for building. It compiles all sources - and links them into an executable. - """ - print(f"\n=== Building for {self.get_name()} ===\n") - - # Get source files - sources = self.get_sources() - - # Compile each source file - objects = [] - for source in sources: - obj = self.compile_source(source) - objects.append(obj) - - # Link into executable - output = self.get_output_path() - self.link_executable(objects, output) - - print(f"\nBuild successful: {output}\n") - - def build_tests(self): - """ - Build the test executable for this target. - - Compiles test sources and links them with non-main sources. - """ - print(f"\n=== Building tests for {self.get_name()} ===\n") - - # Generate test code if needed - self.generate_tests() - - # Get test sources - test_sources = self.config.get_test_files() - - # Get main sources (exclude main.c) - main_sources = self.config.get_main_sources() - - # Compile test files - test_objects = [] - for source in test_sources: - obj = self.compile_source(source, is_test=True) - test_objects.append(obj) - - # Compile shared source files - shared_objects = [] - for source in main_sources: - obj = self.compile_source(source, is_test=False) - shared_objects.append(obj) - - # Link into test executable - output = self.get_test_output_path() - self.link_executable(test_objects + shared_objects, output) - - print(f"\nTest build successful: {output}\n") - - def run(self): - """ - Run the built executable. - - Raises: - NotImplementedError: If target doesn't support running - """ - import subprocess - - executable = self.get_output_path() - - if not executable.exists(): - print(f"Error: Executable not found: {executable}") - print("Run 'build' first to create the executable.") - return - - print(f"\n--- Running {executable} ---\n") - subprocess.call([str(executable)]) - - def run_tests(self): - """ - Run the test executable. - - Raises: - NotImplementedError: If target doesn't support running tests - """ - import subprocess - - executable = self.get_test_output_path() - - if not executable.exists(): - print(f"Error: Test executable not found: {executable}") - print("Run 'test' first to build tests.") - return - - print(f"\n--- Running tests ---\n") - subprocess.call([str(executable)]) - - def debug(self): - """ - Debug the test executable using GDB or platform debugger. - - Raises: - NotImplementedError: If target doesn't support debugging - """ - import subprocess - import shutil - import os - - executable = self.get_test_output_path() - - if not executable.exists(): - print(f"Error: Test executable not found: {executable}") - print("Run 'test' first to build tests.") - return - - if os.name == "nt": - print("Debugging on Windows requires Visual Studio debugger.") - print(f"Load {executable} in Visual Studio to debug.") - elif shutil.which("gdb"): - subprocess.call(["gdb", str(executable)]) - elif shutil.which("lldb"): - subprocess.call(["lldb", str(executable)]) - else: - print("No debugger found (tried gdb, lldb)") - - def clean(self): - """ - Clean build artifacts for this target. - - Removes object files, executables, and build directories. - """ - from ..utils import rm_tree - - print(f"\n=== Cleaning {self.get_name()} ===\n") - - # Remove object directory - if self.config.obj_dir.exists(): - rm_tree(self.config.obj_dir) - print(f"Removed {self.config.obj_dir}") - - # Remove binary directory - if self.config.bin_dir.exists(): - rm_tree(self.config.bin_dir) - print(f"Removed {self.config.bin_dir}") - - print("\nClean complete\n") - - def compile_source(self, source: Path, is_test: bool = False) -> Path: - """ - Compile a single source file. - - Args: - source: Source file path - is_test: Whether this is a test compilation - - Returns: - Path to compiled object file - """ - from ..utils import needs_rebuild, mkdir - - # Determine output path - mkdir(self.config.obj_dir) - obj_ext = self.compiler.get_object_extension() - obj = self.config.obj_dir / (source.stem + obj_ext) - deps = self.compiler.get_dependency_file(obj) - - # Check if rebuild is needed - if not needs_rebuild(source, obj, deps): - return obj - - # Get compilation parameters - includes = self.config.get_includes() - defines = self.config.get_defines() - flags = self.get_compile_flags(is_test) - - # Compile - self.compiler.compile( - source, - obj, - includes=includes, - defines=defines, - flags=flags, - generate_deps=True, - is_test=is_test - ) - - return obj - - def link_executable(self, objects: List[Path], output: Path): - """ - Link object files into an executable. - - Args: - objects: List of object files - output: Output executable path - """ - from ..utils import mkdir - - mkdir(output.parent) - - libraries = self.config.get_libraries(self.get_name()) - flags = self.get_link_flags() - - self.compiler.link( - objects, - output, - libraries=libraries, - flags=flags - ) - - def get_sources(self) -> List[Path]: - """ - Get list of source files for this target. - - Returns: - List of source file paths - """ - return self.config.get_source_files() - - def get_output_path(self) -> Path: - """ - Get the output executable path. - - Returns: - Path to output executable - """ - exe_ext = self.compiler.get_executable_extension() - return self.config.main_target.with_suffix(exe_ext) - - def get_test_output_path(self) -> Path: - """ - Get the test executable path. - - Returns: - Path to test executable - """ - exe_ext = self.compiler.get_executable_extension() - return self.config.test_target.with_suffix(exe_ext) - - def get_compile_flags(self, is_test: bool = False) -> List[str]: - """ - Get compilation flags for this target. - - Args: - is_test: Whether this is a test compilation - - Returns: - List of compiler flags - """ - return [] - - def get_link_flags(self) -> List[str]: - """ - Get linker flags for this target. - - Returns: - List of linker flags - """ - return [] - - def generate_tests(self): - """ - Generate test code if needed. - - Override this to implement test generation logic. - """ - pass - - def __repr__(self) -> str: - """String representation of target.""" - return f"{self.__class__.__name__}(compiler={self.compiler})" diff --git a/SLS_C/build_system/targets/linux.py b/SLS_C/build_system/targets/linux.py index 1c2c4d7..e69de29 100644 --- a/SLS_C/build_system/targets/linux.py +++ b/SLS_C/build_system/targets/linux.py @@ -1,88 +0,0 @@ -""" -Linux build target. - -This module implements the build target for native Linux builds. -Uses GCC as the default compiler. -""" - -from pathlib import Path -from typing import List, Optional -from .base import Target -from ..compilers import GCCCompiler, detect_compiler -from ..config import Config - - -class LinuxTarget(Target): - """ - Linux native build target. - - Builds executables for Linux using GCC or Clang. - """ - - def __init__(self, config: Config, compiler: Optional[GCCCompiler] = None): - """ - Initialize Linux target. - - Args: - config: Build configuration - compiler: Compiler to use (default: auto-detect GCC/Clang) - """ - super().__init__(config, compiler) - - def _get_default_compiler(self) -> GCCCompiler: - """ - Get the default compiler for Linux. - - Returns: - GCC or Clang compiler instance - """ - return detect_compiler("linux") # type: ignore - - def get_name(self) -> str: - """Get the target name.""" - return "linux" - - def get_compile_flags(self, is_test: bool = False) -> List[str]: - """ - Get compilation flags for Linux. - - Args: - is_test: Whether this is a test compilation - - Returns: - List of compiler flags - """ - if is_test: - return self.config.gcc_test_flags.copy() - else: - return self.config.gcc_common_flags.copy() - - def get_link_flags(self) -> List[str]: - """ - Get linker flags for Linux. - - Returns: - List of linker flags - """ - return [] - - def generate_tests(self): - """Generate test code from YAML if available.""" - from ..utils import detect_python, run_command - - script = Path("../SLS_Tests/yaml_to_c_tests.py") - yaml = Path("../SLS_Tests/cases.yaml") - out = self.config.test_dir / "lexer_tests.c" - - if not script.exists() or not yaml.exists(): - if self.config.verbose: - print("Test generation skipped (missing files).") - return - - # Check if regeneration is needed - if out.exists() and out.stat().st_mtime > yaml.stat().st_mtime: - return - - print("Generating tests from YAML...") - python = detect_python() - run_command([python, str(script), str(yaml), str(out)]) diff --git a/SLS_C/build_system/utils.py b/SLS_C/build_system/utils.py index 84d6551..e69de29 100644 --- a/SLS_C/build_system/utils.py +++ b/SLS_C/build_system/utils.py @@ -1,259 +0,0 @@ -""" -Utility functions for the build system. - -This module provides common utilities used across the build system: -- File and directory operations -- Git integration -- Dependency tracking -- Process execution helpers -""" - -import os -import subprocess -import shutil -from pathlib import Path -from typing import Optional - - -# TODO: Is this needed? -def mkdir(path: Path): - """ - Create a directory if it doesn't exist. - - Args: - path: Directory path to create - """ - path.mkdir(parents=True, exist_ok=True) - - -# TODO: Is this needed? -def rm_tree(path: Path): - """ - Remove a directory tree if it exists. - - Args: - path: Directory path to remove - """ - if path.exists(): - shutil.rmtree(path, ignore_errors=True) - - -# TODO: Is this needed? -def run_command(cmd: list, cwd: Optional[Path] = None, capture_output: bool = False): - """ - Run a command and optionally capture its output. - - Args: - cmd: Command and arguments as list - cwd: Working directory (default: current directory) - capture_output: If True, return output; if False, print to console - - Returns: - Command output as string if capture_output=True, else None - - Raises: - subprocess.CalledProcessError: If command fails - """ - if not capture_output: - print(">>", " ".join(str(c) for c in cmd)) - subprocess.check_call(cmd, cwd=cwd) - return None - else: - result = subprocess.check_output( - cmd, - cwd=cwd, - stderr=subprocess.DEVNULL, - text=True - ) - return result.strip() - - -def git_commit_hash() -> str: - """ - Get the current git commit hash and date. - - Returns: - String like "a1b2c3d 2024-12-18 10:30:45 -0700" or "unknown" if not a git repo - """ - try: - # Get short hash with dirty flag - hash_result = subprocess.check_output( - ["git", "describe", "--always", "--dirty", "--abbrev=7"], - stderr=subprocess.DEVNULL, - text=True - ).strip() - - # Get commit date - date_result = subprocess.check_output( - ["git", "show", "-s", "--format=%ci"], - stderr=subprocess.DEVNULL, - text=True - ).strip() - - return f"{hash_result} {date_result}" - except (subprocess.CalledProcessError, FileNotFoundError): - return "unknown" - - -def is_up_to_date(source: Path, output: Path, deps: Optional[Path] = None) -> bool: - """ - Check if an output file is up to date relative to its source. - - Args: - source: Source file path - output: Output file path - deps: Optional dependency file path - - Returns: - True if output is newer than source (and deps), False otherwise - """ - # Special case: always rebuild files with "meta" in name - if "meta" in source.stem or "meta" in output.stem: - return False - - # If output doesn't exist, not up to date - if not output.exists(): - return False - - # Check if dependency file is newer than output - if deps and deps.exists(): - if deps.stat().st_mtime > output.stat().st_mtime: - return False - - # Check if source is newer than output - return source.stat().st_mtime < output.stat().st_mtime - - -def find_sources(directory: Path, pattern: str = "*.c", exclude: Optional[list] = None) -> list: - """ - Find source files in a directory with optional exclusions. - - Args: - directory: Directory to search - pattern: Glob pattern for files (default: "*.c") - exclude: List of filenames or patterns to exclude - - Returns: - List of Path objects for matching files - """ - exclude = exclude or [] - sources = list(directory.glob(pattern)) - - # Filter out excluded files - filtered = [] - for src in sources: - # Check exact filename matches - if src.name in exclude: - continue - - # Check pattern matches in stem - if any(excl.lower() in src.stem.lower() for excl in exclude): - continue - - filtered.append(src) - - return filtered - - -def detect_python() -> str: - """ - Detect the appropriate Python executable. - - Returns: - 'python3' on Unix-like systems, 'python' on Windows - """ - if os.name == "nt": - return "python" - return "python3" - - -def file_hash(path: Path) -> Optional[str]: - """ - Calculate SHA256 hash of a file. - - Args: - path: File path - - Returns: - Hex string of file hash, or None if file doesn't exist - """ - import hashlib - - if not path.exists(): - return None - - sha256 = hashlib.sha256() - with open(path, 'rb') as f: - for chunk in iter(lambda: f.read(4096), b''): - sha256.update(chunk) - - return sha256.hexdigest() - - -def needs_rebuild(source: Path, output: Path, deps_file: Optional[Path] = None) -> bool: - """ - Advanced dependency checking including header dependencies. - - Args: - source: Source file - output: Output file - deps_file: Dependency file (.d) generated by compiler - - Returns: - True if rebuild is needed - """ - # Basic up-to-date check - if not is_up_to_date(source, output, deps_file): - return True - - # If we have a dependency file, check all dependencies - if deps_file and deps_file.exists(): - try: - with open(deps_file, 'r') as f: - content = f.read() - - # Parse Make-style dependencies - # Format: target: dep1 dep2 dep3 - if ':' in content: - _, deps_str = content.split(':', 1) - deps = deps_str.strip().split() - - output_mtime = output.stat().st_mtime - - # Check if any dependency is newer than output - for dep in deps: - dep_path = Path(dep.strip('\\').strip()) - if dep_path.exists(): - if dep_path.stat().st_mtime > output_mtime: - return True - except Exception: - # If we can't parse deps, rebuild to be safe - return True - - return False - - -def copy_file(src: Path, dst: Path): - """ - Copy a file, creating destination directory if needed. - - Args: - src: Source file - dst: Destination file - """ - mkdir(dst.parent) - shutil.copy2(src, dst) - - -def which(executable: str) -> Optional[Path]: - """ - Find an executable in PATH. - - Args: - executable: Name of executable - - Returns: - Path to executable if found, None otherwise - """ - result = shutil.which(executable) - return Path(result) if result else None