Compare commits
No commits in common. "1f0fccc86663e92ce1d053ea5d8349561a922ce3" and "df070b080ef56c2393e9ce3e014007ad4ee66fad" have entirely different histories.
1f0fccc866
...
df070b080e
|
|
@ -0,0 +1,143 @@
|
||||||
|
"""
|
||||||
|
Compiler module for the build system.
|
||||||
|
|
||||||
|
This module provides compiler implementations for various toolchains:
|
||||||
|
- GCC (Unix/Linux)
|
||||||
|
- Clang (macOS/Unix)
|
||||||
|
- MSVC (Windows)
|
||||||
|
- ARM GCC (embedded ARM targets)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from compilers import GCCCompiler, ClangCompiler, MSVCCompiler
|
||||||
|
|
||||||
|
compiler = GCCCompiler()
|
||||||
|
compiler.compile(source, output, includes=[...], defines={...})
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .base import Compiler
|
||||||
|
from .gcc import GCCCompiler
|
||||||
|
from .clang import ClangCompiler
|
||||||
|
from .msvc import MSVCCompiler
|
||||||
|
from .arm_gcc import ARMGCCCompiler, RP2040Compiler
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Compiler',
|
||||||
|
'GCCCompiler',
|
||||||
|
'ClangCompiler',
|
||||||
|
'MSVCCompiler',
|
||||||
|
'ARMGCCCompiler',
|
||||||
|
'RP2040Compiler',
|
||||||
|
]
|
||||||
|
|
||||||
|
def detect_compiler(platform: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Detect and return an appropriate compiler for the given platform.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
platform: Target platform ('linux', 'windows', 'darwin'/'macos', 'rp2040', etc.)
|
||||||
|
If None, detects current platform.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An instance of the appropriate Compiler subclass
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If no suitable compiler is found
|
||||||
|
"""
|
||||||
|
import platform as plat
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Detect current platform if not specified
|
||||||
|
if platform is None:
|
||||||
|
system = plat.system().lower()
|
||||||
|
if system == "darwin":
|
||||||
|
platform = "macos"
|
||||||
|
else:
|
||||||
|
platform = system
|
||||||
|
|
||||||
|
platform = platform.lower()
|
||||||
|
|
||||||
|
# Try to find appropriate compiler
|
||||||
|
if platform in ("linux", "unix"):
|
||||||
|
# Prefer GCC on Linux
|
||||||
|
if shutil.which("gcc"):
|
||||||
|
return GCCCompiler("gcc")
|
||||||
|
elif shutil.which("clang"):
|
||||||
|
return ClangCompiler("clang")
|
||||||
|
else:
|
||||||
|
raise RuntimeError("No suitable compiler found (tried gcc, clang)")
|
||||||
|
|
||||||
|
elif platform in ("macos", "darwin"):
|
||||||
|
# Prefer Clang on macOS (default compiler)
|
||||||
|
if shutil.which("clang"):
|
||||||
|
return ClangCompiler("clang")
|
||||||
|
elif shutil.which("gcc"):
|
||||||
|
return GCCCompiler("gcc")
|
||||||
|
else:
|
||||||
|
raise RuntimeError("No suitable compiler found (tried clang, gcc)")
|
||||||
|
|
||||||
|
elif platform == "windows":
|
||||||
|
# Try MSVC first, then MinGW GCC
|
||||||
|
if shutil.which("cl"):
|
||||||
|
return MSVCCompiler("cl")
|
||||||
|
elif shutil.which("gcc"):
|
||||||
|
return GCCCompiler("gcc")
|
||||||
|
else:
|
||||||
|
raise RuntimeError("No suitable compiler found (tried cl, gcc)")
|
||||||
|
|
||||||
|
elif platform == "rp2040":
|
||||||
|
# RP2040 needs ARM toolchain
|
||||||
|
if shutil.which("arm-none-eabi-gcc"):
|
||||||
|
return RP2040Compiler()
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"ARM GCC toolchain not found. Please install arm-none-eabi-gcc:\n"
|
||||||
|
" Ubuntu/Debian: sudo apt install gcc-arm-none-eabi\n"
|
||||||
|
" macOS: brew install arm-none-eabi-gcc"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown platform: {platform}")
|
||||||
|
|
||||||
|
def get_available_compilers():
|
||||||
|
"""
|
||||||
|
Get a list of all compilers available on the current system.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping compiler names to Compiler instances
|
||||||
|
"""
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
available = {}
|
||||||
|
|
||||||
|
# Check GCC
|
||||||
|
if shutil.which("gcc"):
|
||||||
|
available['gcc'] = GCCCompiler()
|
||||||
|
|
||||||
|
# Check Clang
|
||||||
|
if shutil.which("clang"):
|
||||||
|
available['clang'] = ClangCompiler()
|
||||||
|
|
||||||
|
# Check MSVC
|
||||||
|
if shutil.which("cl"):
|
||||||
|
available['msvc'] = MSVCCompiler()
|
||||||
|
|
||||||
|
# Check ARM GCC
|
||||||
|
if shutil.which("arm-none-eabi-gcc"):
|
||||||
|
available['arm-gcc'] = ARMGCCCompiler()
|
||||||
|
available['rp2040'] = RP2040Compiler()
|
||||||
|
|
||||||
|
return available
|
||||||
|
|
||||||
|
def print_available_compilers():
|
||||||
|
"""Print all available compilers on the system."""
|
||||||
|
compilers = get_available_compilers()
|
||||||
|
|
||||||
|
if not compilers:
|
||||||
|
print("No compilers found on this system.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Available compilers:")
|
||||||
|
for name, compiler in compilers.items():
|
||||||
|
print(f" - {name}: {compiler.executable}")
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
"""
|
||||||
|
ARM GCC compiler implementation for embedded targets.
|
||||||
|
|
||||||
|
This module implements the Compiler interface for ARM GCC toolchain,
|
||||||
|
used for cross-compiling to ARM Cortex-M processors (e.g., RP2040).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from .gcc import GCCCompiler
|
||||||
|
|
||||||
|
|
||||||
|
class ARMGCCCompiler(GCCCompiler):
|
||||||
|
"""
|
||||||
|
ARM GCC compiler implementation for embedded targets.
|
||||||
|
|
||||||
|
Uses the arm-none-eabi-gcc toolchain for bare-metal ARM development.
|
||||||
|
Extends GCCCompiler with ARM Cortex-M specific flags.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
executable: str = "arm-none-eabi-gcc",
|
||||||
|
cpu: str = "cortex-m0plus"
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize ARM GCC compiler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
executable: ARM GCC executable (default: "arm-none-eabi-gcc")
|
||||||
|
cpu: Target CPU architecture (default: "cortex-m0plus" for RP2040)
|
||||||
|
"""
|
||||||
|
super().__init__(executable=executable, is_clang=False)
|
||||||
|
self.cpu = cpu
|
||||||
|
self.fpu = None
|
||||||
|
self.float_abi = None
|
||||||
|
self.additional_arch_flags = []
|
||||||
|
|
||||||
|
def set_cpu(self, cpu: str):
|
||||||
|
"""
|
||||||
|
Set target CPU architecture.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cpu: CPU name (e.g., "cortex-m0plus", "cortex-m4", "cortex-m7")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self for method chaining
|
||||||
|
"""
|
||||||
|
self.cpu = cpu
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_fpu(self, fpu: str, float_abi: str = "hard"):
|
||||||
|
"""
|
||||||
|
Set FPU configuration (for Cortex-M4F, M7, etc.).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fpu: FPU type (e.g., "fpv4-sp-d16", "fpv5-d16")
|
||||||
|
float_abi: Float ABI ("soft", "softfp", or "hard")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self for method chaining
|
||||||
|
"""
|
||||||
|
self.fpu = fpu
|
||||||
|
self.float_abi = float_abi
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_arch_flags(self, flags: List[str]):
|
||||||
|
"""
|
||||||
|
Add additional architecture-specific flags.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
flags: List of flags to add
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self for method chaining
|
||||||
|
"""
|
||||||
|
self.additional_arch_flags.extend(flags)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def compile(
|
||||||
|
self,
|
||||||
|
source: Path,
|
||||||
|
output: Path,
|
||||||
|
*,
|
||||||
|
includes: Optional[List[Path]] = None,
|
||||||
|
defines: Optional[dict] = None,
|
||||||
|
flags: Optional[List[str]] = None,
|
||||||
|
generate_deps: bool = True,
|
||||||
|
is_test: bool = False
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Compile using ARM GCC with embedded-specific flags.
|
||||||
|
|
||||||
|
Adds ARM Cortex-M architecture flags before calling parent compile.
|
||||||
|
"""
|
||||||
|
flags = flags or []
|
||||||
|
|
||||||
|
# ARM architecture flags (must come early)
|
||||||
|
arch_flags = [
|
||||||
|
f"-mcpu={self.cpu}",
|
||||||
|
"-mthumb", # Use Thumb instruction set
|
||||||
|
]
|
||||||
|
|
||||||
|
# FPU flags if configured
|
||||||
|
if self.fpu:
|
||||||
|
arch_flags.append(f"-mfpu={self.fpu}")
|
||||||
|
arch_flags.append(f"-mfloat-abi={self.float_abi}")
|
||||||
|
|
||||||
|
# Additional architecture flags
|
||||||
|
arch_flags.extend(self.additional_arch_flags)
|
||||||
|
|
||||||
|
# Prepend architecture flags (they need to come before other flags)
|
||||||
|
flags = arch_flags + flags
|
||||||
|
|
||||||
|
return super().compile(
|
||||||
|
source,
|
||||||
|
output,
|
||||||
|
includes=includes,
|
||||||
|
defines=defines,
|
||||||
|
flags=flags,
|
||||||
|
generate_deps=generate_deps,
|
||||||
|
is_test=is_test
|
||||||
|
)
|
||||||
|
|
||||||
|
def link(
|
||||||
|
self,
|
||||||
|
objects: List[Path],
|
||||||
|
output: Path,
|
||||||
|
*,
|
||||||
|
libraries: Optional[List[str]] = None,
|
||||||
|
library_paths: Optional[List[Path]] = None,
|
||||||
|
flags: Optional[List[str]] = None,
|
||||||
|
linker_script: Optional[Path] = None,
|
||||||
|
specs: Optional[str] = None
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Link using ARM GCC with embedded-specific flags.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
objects: Object files to link
|
||||||
|
output: Output executable path
|
||||||
|
libraries: Libraries to link
|
||||||
|
library_paths: Library search paths
|
||||||
|
flags: Additional linker flags
|
||||||
|
linker_script: Path to linker script (.ld file)
|
||||||
|
specs: Specs file (e.g., "nosys.specs", "nano.specs")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to linked executable
|
||||||
|
"""
|
||||||
|
flags = flags or []
|
||||||
|
|
||||||
|
# ARM architecture flags (need to be passed to linker too)
|
||||||
|
arch_flags = [
|
||||||
|
f"-mcpu={self.cpu}",
|
||||||
|
"-mthumb",
|
||||||
|
]
|
||||||
|
|
||||||
|
# FPU flags
|
||||||
|
if self.fpu:
|
||||||
|
arch_flags.append(f"-mfpu={self.fpu}")
|
||||||
|
arch_flags.append(f"-mfloat-abi={self.float_abi}")
|
||||||
|
|
||||||
|
# Additional architecture flags
|
||||||
|
arch_flags.extend(self.additional_arch_flags)
|
||||||
|
|
||||||
|
# Linker script
|
||||||
|
if linker_script:
|
||||||
|
arch_flags.append(f"-T{linker_script}")
|
||||||
|
|
||||||
|
# Specs file (for selecting C library variant)
|
||||||
|
if specs:
|
||||||
|
arch_flags.append(f"--specs={specs}")
|
||||||
|
|
||||||
|
# Prepend architecture flags
|
||||||
|
flags = arch_flags + flags
|
||||||
|
|
||||||
|
return super().link(
|
||||||
|
objects,
|
||||||
|
output,
|
||||||
|
libraries=libraries,
|
||||||
|
library_paths=library_paths,
|
||||||
|
flags=flags
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_executable_extension(self) -> str:
|
||||||
|
"""
|
||||||
|
Get executable extension for ARM embedded.
|
||||||
|
|
||||||
|
Returns .elf for embedded binaries (can be converted to .bin, .hex, .uf2)
|
||||||
|
"""
|
||||||
|
return ".elf"
|
||||||
|
|
||||||
|
|
||||||
|
class RP2040Compiler(ARMGCCCompiler):
|
||||||
|
"""
|
||||||
|
Specialized ARM GCC compiler for RP2040 (Raspberry Pi Pico).
|
||||||
|
|
||||||
|
Pre-configured for Cortex-M0+ with RP2040-specific settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, executable: str = "arm-none-eabi-gcc"):
|
||||||
|
"""Initialize compiler for RP2040."""
|
||||||
|
super().__init__(executable=executable, cpu="cortex-m0plus")
|
||||||
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
"""
|
||||||
|
Abstract base class for compilers.
|
||||||
|
|
||||||
|
This module defines the interface that all compiler implementations must follow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Compiler(ABC):
|
||||||
|
"""
|
||||||
|
Abstract base class for all compiler implementations.
|
||||||
|
|
||||||
|
Subclasses must implement compile() and link() methods with
|
||||||
|
compiler-specific logic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, executable: str):
|
||||||
|
"""
|
||||||
|
Initialize the compiler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
executable: Name or path to the compiler executable
|
||||||
|
"""
|
||||||
|
self.executable = executable
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def compile(
|
||||||
|
self,
|
||||||
|
source: Path,
|
||||||
|
output: Path,
|
||||||
|
*,
|
||||||
|
includes: Optional[List[Path]] = None,
|
||||||
|
defines: Optional[dict] = None,
|
||||||
|
flags: Optional[List[str]] = None,
|
||||||
|
generate_deps: bool = True,
|
||||||
|
is_test: bool = False
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Compile a single source file to an object file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: Path to source file (.c)
|
||||||
|
output: Path to output object file (.o or .obj)
|
||||||
|
includes: List of include directories
|
||||||
|
defines: Dictionary of preprocessor defines {name: value}
|
||||||
|
flags: Additional compiler flags
|
||||||
|
generate_deps: Whether to generate dependency files
|
||||||
|
is_test: Whether this is a test compilation (may affect optimization)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to the compiled object file
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def link(
|
||||||
|
self,
|
||||||
|
objects: List[Path],
|
||||||
|
output: Path,
|
||||||
|
*,
|
||||||
|
libraries: Optional[List[str]] = None,
|
||||||
|
library_paths: Optional[List[Path]] = None,
|
||||||
|
flags: Optional[List[str]] = None
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Link object files into an executable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
objects: List of object files to link
|
||||||
|
output: Path to output executable
|
||||||
|
libraries: List of libraries to link against (e.g., ['m', 'pthread'])
|
||||||
|
library_paths: List of library search paths
|
||||||
|
flags: Additional linker flags
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to the linked executable
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_object_extension(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the object file extension for this compiler.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Object file extension (e.g., '.o' or '.obj')
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_executable_extension(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the executable extension for this compiler/platform.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Executable extension (e.g., '' for Unix, '.exe' for Windows)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_dependency_file(self, object_file: Path) -> Path:
|
||||||
|
"""
|
||||||
|
Get the dependency file path for an object file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
object_file: Path to object file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to dependency file (usually .d extension)
|
||||||
|
"""
|
||||||
|
return object_file.with_suffix('.d')
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if this compiler is available on the system.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if compiler executable exists in PATH
|
||||||
|
"""
|
||||||
|
import shutil
|
||||||
|
return shutil.which(self.executable) is not None
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}(executable='{self.executable}')"
|
||||||
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
"""
|
||||||
|
Clang compiler implementation.
|
||||||
|
|
||||||
|
This module implements the Compiler interface specifically for Clang,
|
||||||
|
with any Clang-specific features or flags that differ from GCC.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from .gcc import GCCCompiler
|
||||||
|
|
||||||
|
|
||||||
|
class ClangCompiler(GCCCompiler):
|
||||||
|
"""
|
||||||
|
Clang compiler implementation.
|
||||||
|
|
||||||
|
Clang uses a GCC-compatible command-line interface, so most functionality
|
||||||
|
is inherited from GCCCompiler. This class adds Clang-specific features:
|
||||||
|
1. macOS version targeting
|
||||||
|
2. Sanitizer support
|
||||||
|
3. Future Clang-specific optimizations
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, executable: str = "clang"):
|
||||||
|
"""
|
||||||
|
Initialize Clang compiler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
executable: Clang executable name (default: "clang")
|
||||||
|
"""
|
||||||
|
# Call parent with is_clang=True
|
||||||
|
super().__init__(executable=executable, is_clang=True)
|
||||||
|
self.macos_min_version = None
|
||||||
|
self.sanitizers = []
|
||||||
|
|
||||||
|
def set_macos_version(self, min_version: str = "10.13"):
|
||||||
|
"""
|
||||||
|
Set minimum macOS version for cross-compilation or macOS builds.
|
||||||
|
|
||||||
|
This is a Clang-specific feature commonly used on macOS.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
min_version: Minimum macOS version (e.g., "10.13")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self for method chaining
|
||||||
|
"""
|
||||||
|
self.macos_min_version = min_version
|
||||||
|
return self
|
||||||
|
|
||||||
|
def enable_sanitizers(self, sanitizers: List[str]):
|
||||||
|
"""
|
||||||
|
Enable Clang sanitizers (address, thread, undefined, memory, etc.).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sanitizers: List of sanitizers to enable (e.g., ['address', 'undefined'])
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self for method chaining
|
||||||
|
"""
|
||||||
|
self.sanitizers = sanitizers
|
||||||
|
return self
|
||||||
|
|
||||||
|
def compile(
|
||||||
|
self,
|
||||||
|
source: Path,
|
||||||
|
output: Path,
|
||||||
|
*,
|
||||||
|
includes: Optional[List[Path]] = None,
|
||||||
|
defines: Optional[dict] = None,
|
||||||
|
flags: Optional[List[str]] = None,
|
||||||
|
generate_deps: bool = True,
|
||||||
|
is_test: bool = False
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Compile using Clang with optional sanitizers and macOS flags.
|
||||||
|
|
||||||
|
Extends GCCCompiler.compile() with Clang-specific features.
|
||||||
|
"""
|
||||||
|
flags = flags or []
|
||||||
|
|
||||||
|
# Add macOS version flag if set
|
||||||
|
if self.macos_min_version:
|
||||||
|
flags.append(f"-mmacosx-version-min={self.macos_min_version}")
|
||||||
|
|
||||||
|
# Add sanitizers if enabled
|
||||||
|
for san in self.sanitizers:
|
||||||
|
flags.append(f"-fsanitize={san}")
|
||||||
|
|
||||||
|
return super().compile(
|
||||||
|
source,
|
||||||
|
output,
|
||||||
|
includes=includes,
|
||||||
|
defines=defines,
|
||||||
|
flags=flags,
|
||||||
|
generate_deps=generate_deps,
|
||||||
|
is_test=is_test
|
||||||
|
)
|
||||||
|
|
||||||
|
def link(
|
||||||
|
self,
|
||||||
|
objects: List[Path],
|
||||||
|
output: Path,
|
||||||
|
*,
|
||||||
|
libraries: Optional[List[str]] = None,
|
||||||
|
library_paths: Optional[List[Path]] = None,
|
||||||
|
flags: Optional[List[str]] = None
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Link using Clang with optional sanitizer support.
|
||||||
|
|
||||||
|
Sanitizers need to be enabled at link time as well.
|
||||||
|
"""
|
||||||
|
flags = flags or []
|
||||||
|
|
||||||
|
# Add sanitizers at link time
|
||||||
|
for san in self.sanitizers:
|
||||||
|
flags.append(f"-fsanitize={san}")
|
||||||
|
|
||||||
|
return super().link(
|
||||||
|
objects,
|
||||||
|
output,
|
||||||
|
libraries=libraries,
|
||||||
|
library_paths=library_paths,
|
||||||
|
flags=flags
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
"""
|
||||||
|
GCC and Clang compiler implementation.
|
||||||
|
|
||||||
|
This module implements the Compiler interface for GCC and Clang,
|
||||||
|
which share similar command-line interfaces.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from .base import Compiler
|
||||||
|
|
||||||
|
|
||||||
|
class GCCCompiler(Compiler):
|
||||||
|
"""
|
||||||
|
GCC/Clang compiler implementation.
|
||||||
|
|
||||||
|
Works with both gcc and clang as they use compatible command-line syntax.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, executable: str = "gcc", is_clang: bool = False):
|
||||||
|
"""
|
||||||
|
Initialize GCC/Clang compiler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
executable: Compiler executable name (default: "gcc")
|
||||||
|
is_clang: Whether this is clang (affects some flags)
|
||||||
|
"""
|
||||||
|
super().__init__(executable)
|
||||||
|
self.is_clang = is_clang
|
||||||
|
|
||||||
|
def compile(
|
||||||
|
self,
|
||||||
|
source: Path,
|
||||||
|
output: Path,
|
||||||
|
*,
|
||||||
|
includes: Optional[List[Path]] = None,
|
||||||
|
defines: Optional[dict] = None,
|
||||||
|
flags: Optional[List[str]] = None,
|
||||||
|
generate_deps: bool = True,
|
||||||
|
is_test: bool = False
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Compile a source file using GCC/Clang.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: Path to source file
|
||||||
|
output: Path to output object file
|
||||||
|
includes: Include directories
|
||||||
|
defines: Preprocessor defines
|
||||||
|
flags: Additional compiler flags
|
||||||
|
generate_deps: Generate dependency file
|
||||||
|
is_test: Test build (disables optimization)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to compiled object file
|
||||||
|
"""
|
||||||
|
cmd = [self.executable]
|
||||||
|
|
||||||
|
# Add user flags first (can be overridden by later flags)
|
||||||
|
if flags:
|
||||||
|
cmd.extend(flags)
|
||||||
|
|
||||||
|
# Standard flags
|
||||||
|
cmd.extend([
|
||||||
|
"-std=c99",
|
||||||
|
"-Wall",
|
||||||
|
"-Wextra",
|
||||||
|
"-Werror",
|
||||||
|
"-g" # Debug symbols
|
||||||
|
])
|
||||||
|
|
||||||
|
# Test builds: disable optimization and allow unused functions
|
||||||
|
if is_test:
|
||||||
|
cmd.extend(["-O0", "-Wno-unused-function"])
|
||||||
|
|
||||||
|
# Include directories
|
||||||
|
if includes:
|
||||||
|
for inc in includes:
|
||||||
|
cmd.append(f"-I{inc}")
|
||||||
|
|
||||||
|
# Defines
|
||||||
|
if defines:
|
||||||
|
for name, value in defines.items():
|
||||||
|
if value is None:
|
||||||
|
cmd.append(f"-D{name}")
|
||||||
|
else:
|
||||||
|
# Properly escape string values
|
||||||
|
if isinstance(value, str):
|
||||||
|
cmd.append(f"-D{name}=\"{value}\"")
|
||||||
|
else:
|
||||||
|
cmd.append(f"-D{name}={value}")
|
||||||
|
|
||||||
|
# Dependency generation
|
||||||
|
if generate_deps:
|
||||||
|
cmd.extend(["-MMD", "-MP"])
|
||||||
|
|
||||||
|
# Compile only
|
||||||
|
cmd.extend(["-c", str(source)])
|
||||||
|
|
||||||
|
# Output file
|
||||||
|
cmd.extend(["-o", str(output)])
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Run compilation
|
||||||
|
self._run(cmd)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def link(
|
||||||
|
self,
|
||||||
|
objects: List[Path],
|
||||||
|
output: Path,
|
||||||
|
*,
|
||||||
|
libraries: Optional[List[str]] = None,
|
||||||
|
library_paths: Optional[List[Path]] = None,
|
||||||
|
flags: Optional[List[str]] = None
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Link object files into an executable using GCC/Clang.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
objects: Object files to link
|
||||||
|
output: Output executable path
|
||||||
|
libraries: Libraries to link (e.g., ['m', 'pthread'])
|
||||||
|
library_paths: Library search paths
|
||||||
|
flags: Additional linker flags
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to linked executable
|
||||||
|
"""
|
||||||
|
cmd = [self.executable]
|
||||||
|
|
||||||
|
# Add user flags first
|
||||||
|
if flags:
|
||||||
|
cmd.extend(flags)
|
||||||
|
|
||||||
|
# Object files
|
||||||
|
cmd.extend(str(obj) for obj in objects)
|
||||||
|
|
||||||
|
# Library paths
|
||||||
|
if library_paths:
|
||||||
|
for path in library_paths:
|
||||||
|
cmd.append(f"-L{path}")
|
||||||
|
|
||||||
|
# Libraries
|
||||||
|
if libraries:
|
||||||
|
for lib in libraries:
|
||||||
|
cmd.append(f"-l{lib}")
|
||||||
|
|
||||||
|
# Output file
|
||||||
|
cmd.extend(["-o", str(output)])
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Run linker
|
||||||
|
self._run(cmd)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def get_object_extension(self) -> str:
|
||||||
|
"""Get object file extension for GCC/Clang."""
|
||||||
|
return ".o"
|
||||||
|
|
||||||
|
def get_executable_extension(self) -> str:
|
||||||
|
"""Get executable extension (empty for Unix-like systems)."""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _run(self, cmd: List[str]):
|
||||||
|
"""
|
||||||
|
Execute a command and handle errors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd: Command to execute
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
subprocess.CalledProcessError: If compilation/linking fails
|
||||||
|
"""
|
||||||
|
print(">>", " ".join(str(c) for c in cmd))
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
"""
|
||||||
|
MSVC (Microsoft Visual C++) compiler implementation.
|
||||||
|
|
||||||
|
This module implements the Compiler interface for Microsoft's MSVC compiler,
|
||||||
|
which has a completely different command-line syntax from GCC/Clang.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from .base import Compiler
|
||||||
|
|
||||||
|
|
||||||
|
class MSVCCompiler(Compiler):
|
||||||
|
"""
|
||||||
|
Microsoft Visual C++ compiler implementation.
|
||||||
|
|
||||||
|
MSVC uses different command-line syntax and conventions:
|
||||||
|
- Flags start with / instead of -
|
||||||
|
- Different flag names (/Fo vs -o, /Fe vs -o for linking)
|
||||||
|
- .obj instead of .o for object files
|
||||||
|
- .exe extension for executables
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, executable: str = "cl"):
|
||||||
|
"""
|
||||||
|
Initialize MSVC compiler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
executable: MSVC compiler executable (default: "cl")
|
||||||
|
"""
|
||||||
|
super().__init__(executable)
|
||||||
|
|
||||||
|
def compile(
|
||||||
|
self,
|
||||||
|
source: Path,
|
||||||
|
output: Path,
|
||||||
|
*,
|
||||||
|
includes: Optional[List[Path]] = None,
|
||||||
|
defines: Optional[dict] = None,
|
||||||
|
flags: Optional[List[str]] = None,
|
||||||
|
generate_deps: bool = True,
|
||||||
|
is_test: bool = False
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Compile a source file using MSVC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: Path to source file
|
||||||
|
output: Path to output object file
|
||||||
|
includes: Include directories
|
||||||
|
defines: Preprocessor defines
|
||||||
|
flags: Additional compiler flags
|
||||||
|
generate_deps: Generate dependency file (not used by MSVC)
|
||||||
|
is_test: Test build (disables optimization)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to compiled object file
|
||||||
|
"""
|
||||||
|
cmd = [self.executable]
|
||||||
|
|
||||||
|
# Add user flags first
|
||||||
|
if flags:
|
||||||
|
cmd.extend(flags)
|
||||||
|
|
||||||
|
# Standard flags
|
||||||
|
cmd.extend([
|
||||||
|
"/std:c11", # C11 standard (closest to c99)
|
||||||
|
"/W4", # Warning level 4 (similar to -Wall -Wextra)
|
||||||
|
"/WX", # Treat warnings as errors (like -Werror)
|
||||||
|
"/Zi", # Generate debug info (like -g)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Test builds: disable optimization
|
||||||
|
if is_test:
|
||||||
|
cmd.append("/Od") # Disable optimization
|
||||||
|
|
||||||
|
# Include directories
|
||||||
|
if includes:
|
||||||
|
for inc in includes:
|
||||||
|
cmd.append(f"/I{inc}")
|
||||||
|
|
||||||
|
# Defines
|
||||||
|
if defines:
|
||||||
|
for name, value in defines.items():
|
||||||
|
if value is None:
|
||||||
|
cmd.append(f"/D{name}")
|
||||||
|
else:
|
||||||
|
# MSVC handles string escaping differently
|
||||||
|
if isinstance(value, str):
|
||||||
|
# Use backslash to escape quotes in MSVC
|
||||||
|
escaped = value.replace('"', '\\"')
|
||||||
|
cmd.append(f"/D{name}=\"{escaped}\"")
|
||||||
|
else:
|
||||||
|
cmd.append(f"/D{name}={value}")
|
||||||
|
|
||||||
|
# Compile only (don't link)
|
||||||
|
cmd.append("/c")
|
||||||
|
|
||||||
|
# Output file (note: /Fo with no space)
|
||||||
|
cmd.append(f"/Fo{output}")
|
||||||
|
|
||||||
|
# Source file
|
||||||
|
cmd.append(str(source))
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Run compilation
|
||||||
|
self._run(cmd)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def link(
|
||||||
|
self,
|
||||||
|
objects: List[Path],
|
||||||
|
output: Path,
|
||||||
|
*,
|
||||||
|
libraries: Optional[List[str]] = None,
|
||||||
|
library_paths: Optional[List[Path]] = None,
|
||||||
|
flags: Optional[List[str]] = None
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Link object files into an executable using MSVC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
objects: Object files to link
|
||||||
|
output: Output executable path
|
||||||
|
libraries: Libraries to link (e.g., ['kernel32', 'user32'])
|
||||||
|
library_paths: Library search paths
|
||||||
|
flags: Additional linker flags
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to linked executable
|
||||||
|
"""
|
||||||
|
cmd = [self.executable]
|
||||||
|
|
||||||
|
# Add user flags first
|
||||||
|
if flags:
|
||||||
|
cmd.extend(flags)
|
||||||
|
|
||||||
|
# Object files
|
||||||
|
cmd.extend(str(obj) for obj in objects)
|
||||||
|
|
||||||
|
# Library paths
|
||||||
|
if library_paths:
|
||||||
|
for path in library_paths:
|
||||||
|
cmd.append(f"/LIBPATH:{path}")
|
||||||
|
|
||||||
|
# Libraries (MSVC uses .lib extension)
|
||||||
|
if libraries:
|
||||||
|
for lib in libraries:
|
||||||
|
# Add .lib if not present
|
||||||
|
if not lib.endswith('.lib'):
|
||||||
|
lib = f"{lib}.lib"
|
||||||
|
cmd.append(lib)
|
||||||
|
|
||||||
|
# Output file (note: /Fe with no space)
|
||||||
|
cmd.append(f"/Fe{output}")
|
||||||
|
|
||||||
|
# Link flag
|
||||||
|
cmd.append("/link")
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Run linker
|
||||||
|
self._run(cmd)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def get_object_extension(self) -> str:
|
||||||
|
"""Get object file extension for MSVC."""
|
||||||
|
return ".obj"
|
||||||
|
|
||||||
|
def get_executable_extension(self) -> str:
|
||||||
|
"""Get executable extension for Windows."""
|
||||||
|
return ".exe"
|
||||||
|
|
||||||
|
def _run(self, cmd: List[str]):
|
||||||
|
"""
|
||||||
|
Execute a command and handle errors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd: Command to execute
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
subprocess.CalledProcessError: If compilation/linking fails
|
||||||
|
"""
|
||||||
|
print(">>", " ".join(str(c) for c in cmd))
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
|
||||||
|
|
@ -1,91 +1,67 @@
|
||||||
{
|
{
|
||||||
"directories": {
|
"paths": {
|
||||||
"src": "src",
|
"src_dir": "src",
|
||||||
"test": "tests",
|
"test_dir": "tests",
|
||||||
"obj": "obj",
|
"include_dir": "include",
|
||||||
"bin": "bin",
|
"obj_dir": "obj",
|
||||||
"include": "include"
|
"bin_dir": "bin",
|
||||||
|
"build_dir": "build"
|
||||||
},
|
},
|
||||||
"targets": {
|
"targets": {
|
||||||
"linux": {
|
"main": "sls",
|
||||||
"binary_name": "sls",
|
"test": "sls_tests"
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"compiler_flags": {
|
"parallel_jobs": 0,
|
||||||
"gcc": {
|
"verbose": false,
|
||||||
"common": [
|
"debug": true,
|
||||||
"-std=c99",
|
"gcc_common_flags": [
|
||||||
"-Wall",
|
"-std=c99",
|
||||||
"-Wextra",
|
"-Wall",
|
||||||
"-Werror",
|
"-Wextra",
|
||||||
"-Iinclude",
|
"-Werror",
|
||||||
"-g"
|
"-g"
|
||||||
],
|
],
|
||||||
"test": [
|
"gcc_test_flags": [
|
||||||
"-std=c99",
|
"-std=c99",
|
||||||
"-Wall",
|
"-Wall",
|
||||||
"-Wextra",
|
"-Wextra",
|
||||||
"-Wno-unused-function",
|
"-Wno-unused-function",
|
||||||
"-Werror",
|
"-Werror",
|
||||||
"-Iinclude",
|
"-g",
|
||||||
"-g",
|
"-O0"
|
||||||
"-O0"
|
],
|
||||||
],
|
"msvc_common_flags": [
|
||||||
"link": [
|
"/std:c11",
|
||||||
"-lm"
|
"/W4",
|
||||||
]
|
"/WX",
|
||||||
},
|
"/Zi"
|
||||||
"msvc": {
|
],
|
||||||
"common": [
|
"msvc_test_flags": [
|
||||||
"/std:c11",
|
"/std:c11",
|
||||||
"/Zi",
|
"/W4",
|
||||||
"/Iinclude"
|
"/WX",
|
||||||
],
|
"/Zi",
|
||||||
"test": [
|
"/Od"
|
||||||
"/std:c11",
|
],
|
||||||
"/Zi",
|
"arm_gcc_flags": [
|
||||||
"/Iinclude"
|
"-mcpu=cortex-m0plus",
|
||||||
],
|
"-mthumb"
|
||||||
"link": []
|
],
|
||||||
},
|
"pico": {
|
||||||
"arm_gcc": {
|
"sdk_path_env": "PICO_SDK_PATH",
|
||||||
"common": [
|
"sdk_path_default": "~/pico/pico-sdk",
|
||||||
"-std=c99",
|
"build_dir": "build_pico",
|
||||||
"-Wall",
|
"toolchain_file": "pico_arm_gcc_toolchain.cmake"
|
||||||
"-Wextra",
|
|
||||||
"-mcpu=cortex-m0plus",
|
|
||||||
"-mthumb"
|
|
||||||
],
|
|
||||||
"link": []
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"test_generation": {
|
"macos": {
|
||||||
"script_path": "../SLS_Tests/yaml_to_c_tests.py",
|
"min_version": "10.13"
|
||||||
"yaml_path": "../SLS_Tests/cases.yaml",
|
|
||||||
"output_file": "tests/lexer_tests.c"
|
|
||||||
},
|
},
|
||||||
"python_command": {
|
"source_excludes": {
|
||||||
"windows": "python",
|
"rp2040": [
|
||||||
"unix": "python3"
|
"main.c",
|
||||||
|
"repl.c",
|
||||||
|
"file.c",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,255 +1,301 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
"""
|
||||||
Configuration management for the build system.
|
Build system configuration.
|
||||||
Loads default configuration from config.json and provides
|
|
||||||
access to build settings, paths, and compiler flags.
|
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 os
|
||||||
import json
|
import json
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Any, Optional
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
from importlib.resources import files
|
from importlib import resources
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: This should be utilized more throughout the build system.
|
||||||
class Config:
|
class Config:
|
||||||
"""Central configuration manager for the build system."""
|
"""
|
||||||
|
Centralized build configuration.
|
||||||
|
|
||||||
def __init__(self, config_override: Optional[Dict[str, Any]] = None):
|
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 configuration.
|
Initialize build configuration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config_override: Optional dictionary to override default config values
|
project_root: Project root directory (default: current directory)
|
||||||
|
config_file: Custom config file path (default: uses built-in config.json)
|
||||||
"""
|
"""
|
||||||
self._config = self._load_default_config()
|
# Load default configuration
|
||||||
if config_override:
|
self._load_default_config(config_file)
|
||||||
self._deep_update(self._config, config_override)
|
|
||||||
|
|
||||||
# Cache for computed values
|
# Project structure
|
||||||
self._git_hash: Optional[str] = None
|
self.project_root = project_root or Path.cwd()
|
||||||
self._python_cmd: Optional[str] = None
|
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) -> Dict[str, Any]:
|
def _load_default_config(self, config_file: Optional[Path] = None):
|
||||||
"""Load configuration from config.json."""
|
"""
|
||||||
try:
|
Load configuration from JSON file.
|
||||||
# Load config.json from the same package
|
|
||||||
config_file = files(__package__).joinpath('config.json') # type: ignore
|
Args:
|
||||||
with config_file.open('r') as f:
|
config_file: Custom config file, or None to use default
|
||||||
return json.load(f)
|
"""
|
||||||
except Exception as e:
|
if config_file and config_file.exists():
|
||||||
print(f"Warning: Could not load config.json: {e}")
|
# Load custom config file
|
||||||
print("Using minimal default configuration")
|
with open(config_file, 'r') as f:
|
||||||
return self._minimal_default_config()
|
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 _minimal_default_config(self) -> Dict[str, Any]:
|
def _setup_paths(self):
|
||||||
"""Fallback configuration if config.json cannot be loaded."""
|
"""Setup directory paths from configuration."""
|
||||||
return {
|
paths = self._data.get("paths", {})
|
||||||
"directories": {
|
|
||||||
"src": "src",
|
self.src_dir = self.project_root / paths.get("src_dir", "src")
|
||||||
"test": "tests",
|
self.test_dir = self.project_root / paths.get("test_dir", "tests")
|
||||||
"obj": "obj",
|
self.include_dir = self.project_root / paths.get("include_dir", "include")
|
||||||
"bin": "bin",
|
self.obj_dir = self.project_root / paths.get("obj_dir", "obj")
|
||||||
"include": "include"
|
self.bin_dir = self.project_root / paths.get("bin_dir", "bin")
|
||||||
},
|
self.build_dir = self.project_root / paths.get("build_dir", "build")
|
||||||
"targets": {},
|
|
||||||
"compiler_flags": {},
|
# Target executables
|
||||||
"test_generation": {},
|
targets = self._data.get("targets", {})
|
||||||
"python_command": {"windows": "python", "unix": "python3"}
|
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 _deep_update(self, base: Dict, update: Dict) -> None:
|
def get_includes(self, extra: Optional[List[Path]] = None) -> List[Path]:
|
||||||
"""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
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# Directory paths
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@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 configuration for a specific target.
|
Get include directories.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
target_name: Name of the target (e.g., 'linux', 'windows', 'rp2040')
|
extra: Additional include directories
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary containing target-specific configuration
|
List of include directory paths
|
||||||
"""
|
"""
|
||||||
return self._config.get("targets", {}).get(target_name, {})
|
includes = [self.include_dir]
|
||||||
|
|
||||||
|
if extra:
|
||||||
|
includes.extend(extra)
|
||||||
|
|
||||||
|
return includes
|
||||||
|
|
||||||
def get_binary_name(self, target_name: str) -> str:
|
def get_libraries(self, target: str = "linux") -> List[str]:
|
||||||
"""Get the output binary name for a target."""
|
|
||||||
target_config = self.get_target_config(target_name)
|
|
||||||
return target_config.get("binary_name", "sls")
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# Compiler flags
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_compiler_flags(self, compiler: str, flag_type: str = "common") -> List[str]:
|
|
||||||
"""
|
"""
|
||||||
Get compiler flags for a specific compiler and flag type.
|
Get libraries to link based on target.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
compiler: Compiler name (e.g., 'gcc', 'msvc', 'arm_gcc')
|
target: Target platform
|
||||||
flag_type: Type of flags ('common', 'test', 'link')
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of compiler flags
|
List of library names
|
||||||
"""
|
"""
|
||||||
compiler_config = self._config.get("compiler_flags", {}).get(compiler, {})
|
if target in ("linux", "macos"):
|
||||||
return compiler_config.get(flag_type, []).copy()
|
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]:
|
||||||
# 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 an environment variable value, with target-specific defaults.
|
Get list of source files.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
target: Target name
|
exclude: List of filenames to exclude
|
||||||
var_name: Environment variable name
|
|
||||||
default: Default value if not found
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Environment variable value or default
|
List of source file paths
|
||||||
"""
|
"""
|
||||||
target_config = self.get_target_config(target)
|
from .utils import find_sources
|
||||||
env_key = target_config.get(f"{var_name}_env")
|
|
||||||
|
|
||||||
if env_key and env_key in os.environ:
|
exclude = exclude or []
|
||||||
return os.environ[env_key]
|
return find_sources(self.src_dir, "*.c", exclude)
|
||||||
|
|
||||||
|
def get_test_files(self) -> List[Path]:
|
||||||
|
"""
|
||||||
|
Get list of test source files.
|
||||||
|
|
||||||
config_default = target_config.get(f"{var_name}_default")
|
Returns:
|
||||||
if config_default:
|
List of test file paths
|
||||||
# Expand ~ in paths
|
"""
|
||||||
return str(Path(config_default).expanduser())
|
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).
|
||||||
|
|
||||||
return default
|
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 config instance
|
# Global configuration instance
|
||||||
_config_instance: Optional[Config] = None
|
_config = None
|
||||||
|
|
||||||
|
|
||||||
def get_config() -> Config:
|
def get_config(project_root: Optional[Path] = None, config_file: Optional[Path] = None) -> Config:
|
||||||
"""Get the global configuration instance."""
|
"""
|
||||||
global _config_instance
|
Get the global configuration instance.
|
||||||
if _config_instance is None:
|
|
||||||
_config_instance = Config()
|
Args:
|
||||||
return _config_instance
|
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 set_config(config: Config) -> None:
|
def reset_config():
|
||||||
"""Set the global configuration instance."""
|
"""Reset the global configuration (mainly for testing)."""
|
||||||
global _config_instance
|
global _config
|
||||||
_config_instance = config
|
_config = None
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
"""
|
||||||
|
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']}")
|
||||||
|
|
@ -0,0 +1,327 @@
|
||||||
|
"""
|
||||||
|
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})"
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
"""
|
||||||
|
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)])
|
||||||
|
|
@ -0,0 +1,259 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
Loading…
Reference in New Issue