Compare commits

...

2 Commits

Author SHA1 Message Date
Kyler Olsen 1f0fccc866 Created config.py 2025-12-19 13:49:48 -07:00
Kyler Olsen 9f8d66ccae Restart, emptied all files 2025-12-19 13:47:33 -07:00
12 changed files with 289 additions and 2208 deletions

View File

@ -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}")

View File

@ -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")

View File

@ -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}')"

View File

@ -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
)

View File

@ -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)

View File

@ -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)

View File

@ -1,67 +1,91 @@
{ {
"paths": { "directories": {
"src_dir": "src", "src": "src",
"test_dir": "tests", "test": "tests",
"include_dir": "include", "obj": "obj",
"obj_dir": "obj", "bin": "bin",
"bin_dir": "bin", "include": "include"
"build_dir": "build"
}, },
"targets": { "targets": {
"main": "sls", "linux": {
"test": "sls_tests" "binary_name": "sls",
"test_binary_name": "sls_tests"
},
"windows": {
"binary_name": "sls.exe",
"test_binary_name": "sls_tests.exe"
},
"rp2040": {
"binary_name": "sls.elf",
"build_dir": "build_pico",
"toolchain_file": "pico_arm_gcc_toolchain.cmake",
"sdk_path_env": "PICO_SDK_PATH",
"sdk_path_default": "~/pico/pico-sdk",
"excluded_sources": [
"main.c",
"repl.c",
"file.c"
],
"required_sources": [
"pico_main.c"
]
}
}, },
"parallel_jobs": 0, "compiler_flags": {
"verbose": false, "gcc": {
"debug": true, "common": [
"gcc_common_flags": [ "-std=c99",
"-std=c99", "-Wall",
"-Wall", "-Wextra",
"-Wextra", "-Werror",
"-Werror", "-Iinclude",
"-g" "-g"
], ],
"gcc_test_flags": [ "test": [
"-std=c99", "-std=c99",
"-Wall", "-Wall",
"-Wextra", "-Wextra",
"-Wno-unused-function", "-Wno-unused-function",
"-Werror", "-Werror",
"-g", "-Iinclude",
"-O0" "-g",
], "-O0"
"msvc_common_flags": [ ],
"/std:c11", "link": [
"/W4", "-lm"
"/WX", ]
"/Zi" },
], "msvc": {
"msvc_test_flags": [ "common": [
"/std:c11", "/std:c11",
"/W4", "/Zi",
"/WX", "/Iinclude"
"/Zi", ],
"/Od" "test": [
], "/std:c11",
"arm_gcc_flags": [ "/Zi",
"-mcpu=cortex-m0plus", "/Iinclude"
"-mthumb" ],
], "link": []
"pico": { },
"sdk_path_env": "PICO_SDK_PATH", "arm_gcc": {
"sdk_path_default": "~/pico/pico-sdk", "common": [
"build_dir": "build_pico", "-std=c99",
"toolchain_file": "pico_arm_gcc_toolchain.cmake" "-Wall",
"-Wextra",
"-mcpu=cortex-m0plus",
"-mthumb"
],
"link": []
}
}, },
"macos": { "test_generation": {
"min_version": "10.13" "script_path": "../SLS_Tests/yaml_to_c_tests.py",
"yaml_path": "../SLS_Tests/cases.yaml",
"output_file": "tests/lexer_tests.c"
}, },
"source_excludes": { "python_command": {
"rp2040": [ "windows": "python",
"main.c", "unix": "python3"
"repl.c",
"file.c",
"test"
]
} }
} }

View File

@ -1,301 +1,255 @@
#!/usr/bin/env python3
""" """
Build system configuration. Configuration management for the build system.
Loads default configuration from config.json and provides
This module centralizes all build configuration including paths, access to build settings, paths, and compiler flags.
compiler flags, and build settings. Configuration can be loaded from
a default JSON file or overridden programmatically.
""" """
import os import os
import json import json
import subprocess
from pathlib import Path from pathlib import Path
from typing import List, Dict, Optional from typing import Dict, List, Any, Optional
from importlib import resources from importlib.resources import files
# TODO: This should be utilized more throughout the build system.
class Config: class Config:
""" """Central configuration manager for the build system."""
Centralized build configuration.
All paths, flags, and settings are defined here for easy modification. def __init__(self, config_override: Optional[Dict[str, Any]] = None):
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. Initialize configuration.
Args: Args:
project_root: Project root directory (default: current directory) config_override: Optional dictionary to override default config values
config_file: Custom config file path (default: uses built-in config.json)
""" """
# Load default configuration self._config = self._load_default_config()
self._load_default_config(config_file) if config_override:
self._deep_update(self._config, config_override)
# Project structure # Cache for computed values
self.project_root = project_root or Path.cwd() self._git_hash: Optional[str] = None
self._setup_paths() self._python_cmd: Optional[str] = None
# Build settings def _load_default_config(self) -> Dict[str, Any]:
self.parallel_jobs = self._data.get("parallel_jobs", os.cpu_count() or 1) """Load configuration from config.json."""
self.verbose = self._data.get("verbose", False) try:
self.debug = self._data.get("debug", True) # Load config.json from the same package
config_file = files(__package__).joinpath('config.json') # type: ignore
with config_file.open('r') as f:
return json.load(f)
except Exception as e:
print(f"Warning: Could not load config.json: {e}")
print("Using minimal default configuration")
return self._minimal_default_config()
# Git integration def _minimal_default_config(self) -> Dict[str, Any]:
self.git_hash = self._get_git_hash() """Fallback configuration if config.json cannot be loaded."""
return {
# Compiler flags "directories": {
self.gcc_common_flags = self._data.get("gcc_common_flags", []) "src": "src",
self.gcc_test_flags = self._data.get("gcc_test_flags", []) "test": "tests",
self.msvc_common_flags = self._data.get("msvc_common_flags", []) "obj": "obj",
self.msvc_test_flags = self._data.get("msvc_test_flags", []) "bin": "bin",
self.arm_gcc_flags = self._data.get("arm_gcc_flags", []) "include": "include"
},
# Platform-specific settings "targets": {},
self._setup_platform_settings() "compiler_flags": {},
"test_generation": {},
def _load_default_config(self, config_file: Optional[Path] = None): "python_command": {"windows": "python", "unix": "python3"}
"""
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: def _deep_update(self, base: Dict, update: Dict) -> None:
defines.update(extra) """Recursively update nested dictionaries."""
for key, value in update.items():
if isinstance(value, dict) and key in base and isinstance(base[key], dict):
self._deep_update(base[key], value)
else:
base[key] = value
return defines # -------------------------------------------------------------------------
# Directory paths
# -------------------------------------------------------------------------
def get_includes(self, extra: Optional[List[Path]] = None) -> List[Path]: @property
def src_dir(self) -> Path:
"""Source directory path."""
return Path(self._config["directories"]["src"])
@property
def test_dir(self) -> Path:
"""Test directory path."""
return Path(self._config["directories"]["test"])
@property
def obj_dir(self) -> Path:
"""Object files directory path."""
return Path(self._config["directories"]["obj"])
@property
def bin_dir(self) -> Path:
"""Binary output directory path."""
return Path(self._config["directories"]["bin"])
@property
def include_dir(self) -> Path:
"""Include directory path."""
return Path(self._config["directories"]["include"])
# -------------------------------------------------------------------------
# Target configuration
# -------------------------------------------------------------------------
def get_target_config(self, target_name: str) -> Dict[str, Any]:
""" """
Get include directories. Get configuration for a specific target.
Args: Args:
extra: Additional include directories target_name: Name of the target (e.g., 'linux', 'windows', 'rp2040')
Returns: Returns:
List of include directory paths Dictionary containing target-specific configuration
""" """
includes = [self.include_dir] return self._config.get("targets", {}).get(target_name, {})
if extra: def get_binary_name(self, target_name: str) -> str:
includes.extend(extra) """Get the output binary name for a target."""
target_config = self.get_target_config(target_name)
return target_config.get("binary_name", "sls")
return includes def get_test_binary_name(self, target_name: str) -> str:
"""Get the test binary name for a target."""
target_config = self.get_target_config(target_name)
return target_config.get("test_binary_name", "sls_tests")
def get_libraries(self, target: str = "linux") -> List[str]: # -------------------------------------------------------------------------
# Compiler flags
# -------------------------------------------------------------------------
def get_compiler_flags(self, compiler: str, flag_type: str = "common") -> List[str]:
""" """
Get libraries to link based on target. Get compiler flags for a specific compiler and flag type.
Args: Args:
target: Target platform compiler: Compiler name (e.g., 'gcc', 'msvc', 'arm_gcc')
flag_type: Type of flags ('common', 'test', 'link')
Returns: Returns:
List of library names List of compiler flags
""" """
if target in ("linux", "macos"): compiler_config = self._config.get("compiler_flags", {}).get(compiler, {})
return ["m"] # Math library return compiler_config.get(flag_type, []).copy()
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]: # -------------------------------------------------------------------------
# Test generation
# -------------------------------------------------------------------------
@property
def test_script_path(self) -> Path:
"""Path to the test generation script."""
path_str = self._config.get("test_generation", {}).get("script_path", "")
return Path(path_str) if path_str else Path()
@property
def test_yaml_path(self) -> Path:
"""Path to the test cases YAML file."""
path_str = self._config.get("test_generation", {}).get("yaml_path", "")
return Path(path_str) if path_str else Path()
@property
def test_output_file(self) -> Path:
"""Path to the generated test output file."""
path_str = self._config.get("test_generation", {}).get("output_file", "")
return Path(path_str) if path_str else Path()
# -------------------------------------------------------------------------
# Git information
# -------------------------------------------------------------------------
@property
def git_hash(self) -> str:
"""Get the current git commit hash with date."""
if self._git_hash is None:
self._git_hash = self._compute_git_hash()
return self._git_hash
def _compute_git_hash(self) -> str:
"""Compute git commit hash and date."""
try:
result_hash = subprocess.check_output(
["git", "describe", "--always", "--dirty", "--abbrev=7"],
cwd=".",
stderr=subprocess.DEVNULL,
text=True
).strip()
result_date = subprocess.check_output(
["git", "show", "-s", "--format=%ci"],
cwd=".",
stderr=subprocess.DEVNULL,
text=True
).strip()
return f"{result_hash} {result_date}"
except Exception:
return "unknown"
# -------------------------------------------------------------------------
# Platform-specific settings
# -------------------------------------------------------------------------
@property
def python_command(self) -> str:
"""Get the appropriate Python command for the current platform."""
if self._python_cmd is None:
if os.name == "nt":
self._python_cmd = self._config.get("python_command", {}).get("windows", "python")
else:
self._python_cmd = self._config.get("python_command", {}).get("unix", "python3")
return self._python_cmd # type: ignore
# -------------------------------------------------------------------------
# Environment variables
# -------------------------------------------------------------------------
def get_env_var(self, target: str, var_name: str, default: Optional[str] = None) -> Optional[str]:
""" """
Get list of source files. Get an environment variable value, with target-specific defaults.
Args: Args:
exclude: List of filenames to exclude target: Target name
var_name: Environment variable name
default: Default value if not found
Returns: Returns:
List of source file paths Environment variable value or default
""" """
from .utils import find_sources target_config = self.get_target_config(target)
env_key = target_config.get(f"{var_name}_env")
exclude = exclude or [] if env_key and env_key in os.environ:
return find_sources(self.src_dir, "*.c", exclude) return os.environ[env_key]
def get_test_files(self) -> List[Path]: config_default = target_config.get(f"{var_name}_default")
""" if config_default:
Get list of test source files. # Expand ~ in paths
return str(Path(config_default).expanduser())
Returns: return default
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 # Global config instance
_config = None _config_instance: Optional[Config] = None
def get_config(project_root: Optional[Path] = None, config_file: Optional[Path] = None) -> Config: def get_config() -> Config:
""" """Get the global configuration instance."""
Get the global configuration instance. global _config_instance
if _config_instance is None:
Args: _config_instance = Config()
project_root: Project root directory (only used on first call) return _config_instance
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(): def set_config(config: Config) -> None:
"""Reset the global configuration (mainly for testing).""" """Set the global configuration instance."""
global _config global _config_instance
_config = None _config_instance = config

View File

@ -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']}")

View File

@ -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})"

View File

@ -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)])

View File

@ -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