Compare commits
2 Commits
df070b080e
...
1f0fccc866
| Author | SHA1 | Date |
|---|---|---|
|
|
1f0fccc866 | |
|
|
9f8d66ccae |
|
|
@ -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}")
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
@ -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}')"
|
||||
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -1,67 +1,91 @@
|
|||
{
|
||||
"paths": {
|
||||
"src_dir": "src",
|
||||
"test_dir": "tests",
|
||||
"include_dir": "include",
|
||||
"obj_dir": "obj",
|
||||
"bin_dir": "bin",
|
||||
"build_dir": "build"
|
||||
"directories": {
|
||||
"src": "src",
|
||||
"test": "tests",
|
||||
"obj": "obj",
|
||||
"bin": "bin",
|
||||
"include": "include"
|
||||
},
|
||||
"targets": {
|
||||
"main": "sls",
|
||||
"test": "sls_tests"
|
||||
"linux": {
|
||||
"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,
|
||||
"verbose": false,
|
||||
"debug": true,
|
||||
"gcc_common_flags": [
|
||||
"-std=c99",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-g"
|
||||
],
|
||||
"gcc_test_flags": [
|
||||
"-std=c99",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wno-unused-function",
|
||||
"-Werror",
|
||||
"-g",
|
||||
"-O0"
|
||||
],
|
||||
"msvc_common_flags": [
|
||||
"/std:c11",
|
||||
"/W4",
|
||||
"/WX",
|
||||
"/Zi"
|
||||
],
|
||||
"msvc_test_flags": [
|
||||
"/std:c11",
|
||||
"/W4",
|
||||
"/WX",
|
||||
"/Zi",
|
||||
"/Od"
|
||||
],
|
||||
"arm_gcc_flags": [
|
||||
"-mcpu=cortex-m0plus",
|
||||
"-mthumb"
|
||||
],
|
||||
"pico": {
|
||||
"sdk_path_env": "PICO_SDK_PATH",
|
||||
"sdk_path_default": "~/pico/pico-sdk",
|
||||
"build_dir": "build_pico",
|
||||
"toolchain_file": "pico_arm_gcc_toolchain.cmake"
|
||||
"compiler_flags": {
|
||||
"gcc": {
|
||||
"common": [
|
||||
"-std=c99",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-Iinclude",
|
||||
"-g"
|
||||
],
|
||||
"test": [
|
||||
"-std=c99",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wno-unused-function",
|
||||
"-Werror",
|
||||
"-Iinclude",
|
||||
"-g",
|
||||
"-O0"
|
||||
],
|
||||
"link": [
|
||||
"-lm"
|
||||
]
|
||||
},
|
||||
"msvc": {
|
||||
"common": [
|
||||
"/std:c11",
|
||||
"/Zi",
|
||||
"/Iinclude"
|
||||
],
|
||||
"test": [
|
||||
"/std:c11",
|
||||
"/Zi",
|
||||
"/Iinclude"
|
||||
],
|
||||
"link": []
|
||||
},
|
||||
"arm_gcc": {
|
||||
"common": [
|
||||
"-std=c99",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-mcpu=cortex-m0plus",
|
||||
"-mthumb"
|
||||
],
|
||||
"link": []
|
||||
}
|
||||
},
|
||||
"macos": {
|
||||
"min_version": "10.13"
|
||||
"test_generation": {
|
||||
"script_path": "../SLS_Tests/yaml_to_c_tests.py",
|
||||
"yaml_path": "../SLS_Tests/cases.yaml",
|
||||
"output_file": "tests/lexer_tests.c"
|
||||
},
|
||||
"source_excludes": {
|
||||
"rp2040": [
|
||||
"main.c",
|
||||
"repl.c",
|
||||
"file.c",
|
||||
"test"
|
||||
]
|
||||
"python_command": {
|
||||
"windows": "python",
|
||||
"unix": "python3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,301 +1,255 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build system configuration.
|
||||
|
||||
This module centralizes all build configuration including paths,
|
||||
compiler flags, and build settings. Configuration can be loaded from
|
||||
a default JSON file or overridden programmatically.
|
||||
Configuration management for the build system.
|
||||
Loads default configuration from config.json and provides
|
||||
access to build settings, paths, and compiler flags.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
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:
|
||||
"""
|
||||
Centralized build configuration.
|
||||
"""Central configuration manager for the build system."""
|
||||
|
||||
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):
|
||||
def __init__(self, config_override: Optional[Dict[str, Any]] = None):
|
||||
"""
|
||||
Initialize build configuration.
|
||||
Initialize configuration.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory (default: current directory)
|
||||
config_file: Custom config file path (default: uses built-in config.json)
|
||||
config_override: Optional dictionary to override default config values
|
||||
"""
|
||||
# Load default configuration
|
||||
self._load_default_config(config_file)
|
||||
self._config = self._load_default_config()
|
||||
if config_override:
|
||||
self._deep_update(self._config, config_override)
|
||||
|
||||
# Project structure
|
||||
self.project_root = project_root or Path.cwd()
|
||||
self._setup_paths()
|
||||
# Cache for computed values
|
||||
self._git_hash: Optional[str] = None
|
||||
self._python_cmd: Optional[str] = None
|
||||
|
||||
# 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)
|
||||
def _load_default_config(self) -> Dict[str, Any]:
|
||||
"""Load configuration from config.json."""
|
||||
try:
|
||||
# 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
|
||||
self.git_hash = self._get_git_hash()
|
||||
|
||||
# Compiler flags
|
||||
self.gcc_common_flags = self._data.get("gcc_common_flags", [])
|
||||
self.gcc_test_flags = self._data.get("gcc_test_flags", [])
|
||||
self.msvc_common_flags = self._data.get("msvc_common_flags", [])
|
||||
self.msvc_test_flags = self._data.get("msvc_test_flags", [])
|
||||
self.arm_gcc_flags = self._data.get("arm_gcc_flags", [])
|
||||
|
||||
# Platform-specific settings
|
||||
self._setup_platform_settings()
|
||||
|
||||
def _load_default_config(self, config_file: Optional[Path] = None):
|
||||
"""
|
||||
Load configuration from JSON file.
|
||||
|
||||
Args:
|
||||
config_file: Custom config file, or None to use default
|
||||
"""
|
||||
if config_file and config_file.exists():
|
||||
# Load custom config file
|
||||
with open(config_file, 'r') as f:
|
||||
self._data = json.load(f)
|
||||
else:
|
||||
# Load default config from package resources
|
||||
config_text = resources.files("build_system").joinpath("config.json").read_text()
|
||||
self._data = json.loads(config_text)
|
||||
|
||||
def _setup_paths(self):
|
||||
"""Setup directory paths from configuration."""
|
||||
paths = self._data.get("paths", {})
|
||||
|
||||
self.src_dir = self.project_root / paths.get("src_dir", "src")
|
||||
self.test_dir = self.project_root / paths.get("test_dir", "tests")
|
||||
self.include_dir = self.project_root / paths.get("include_dir", "include")
|
||||
self.obj_dir = self.project_root / paths.get("obj_dir", "obj")
|
||||
self.bin_dir = self.project_root / paths.get("bin_dir", "bin")
|
||||
self.build_dir = self.project_root / paths.get("build_dir", "build")
|
||||
|
||||
# Target executables
|
||||
targets = self._data.get("targets", {})
|
||||
self.main_target = self.bin_dir / targets.get("main", "sls")
|
||||
self.test_target = self.bin_dir / targets.get("test", "sls_tests")
|
||||
|
||||
def _setup_platform_settings(self):
|
||||
"""Setup platform-specific settings from configuration."""
|
||||
pico_config = self._data.get("pico", {})
|
||||
|
||||
# Pico SDK settings
|
||||
sdk_env = pico_config.get("sdk_path_env", "PICO_SDK_PATH")
|
||||
sdk_default = pico_config.get("sdk_path_default", "~/pico/pico-sdk")
|
||||
sdk_default = Path(sdk_default).expanduser()
|
||||
|
||||
self.pico_sdk_path = Path(os.environ.get(sdk_env, sdk_default))
|
||||
self.pico_build_dir = self.project_root / pico_config.get("build_dir", "build_pico")
|
||||
self.pico_toolchain_file = self.project_root / pico_config.get(
|
||||
"toolchain_file", "pico_arm_gcc_toolchain.cmake"
|
||||
)
|
||||
|
||||
# macOS settings
|
||||
macos_config = self._data.get("macos", {})
|
||||
self.macos_min_version = macos_config.get("min_version", "10.13")
|
||||
|
||||
def _get_git_hash(self) -> str:
|
||||
"""Get git commit hash for version info."""
|
||||
from .utils import git_commit_hash
|
||||
return git_commit_hash()
|
||||
|
||||
def get_defines(self, extra: Optional[Dict] = None) -> Dict:
|
||||
"""
|
||||
Get preprocessor defines.
|
||||
|
||||
Args:
|
||||
extra: Additional defines to include
|
||||
|
||||
Returns:
|
||||
Dictionary of defines
|
||||
"""
|
||||
defines = {
|
||||
"GIT_COMMIT_HASH": self.git_hash,
|
||||
def _minimal_default_config(self) -> Dict[str, Any]:
|
||||
"""Fallback configuration if config.json cannot be loaded."""
|
||||
return {
|
||||
"directories": {
|
||||
"src": "src",
|
||||
"test": "tests",
|
||||
"obj": "obj",
|
||||
"bin": "bin",
|
||||
"include": "include"
|
||||
},
|
||||
"targets": {},
|
||||
"compiler_flags": {},
|
||||
"test_generation": {},
|
||||
"python_command": {"windows": "python", "unix": "python3"}
|
||||
}
|
||||
|
||||
if extra:
|
||||
defines.update(extra)
|
||||
def _deep_update(self, base: Dict, update: Dict) -> None:
|
||||
"""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:
|
||||
extra: Additional include directories
|
||||
target_name: Name of the target (e.g., 'linux', 'windows', 'rp2040')
|
||||
|
||||
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:
|
||||
includes.extend(extra)
|
||||
def get_binary_name(self, target_name: str) -> str:
|
||||
"""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:
|
||||
target: Target platform
|
||||
compiler: Compiler name (e.g., 'gcc', 'msvc', 'arm_gcc')
|
||||
flag_type: Type of flags ('common', 'test', 'link')
|
||||
|
||||
Returns:
|
||||
List of library names
|
||||
List of compiler flags
|
||||
"""
|
||||
if target in ("linux", "macos"):
|
||||
return ["m"] # Math library
|
||||
elif target == "windows":
|
||||
return [] # No special libraries needed
|
||||
elif target == "rp2040":
|
||||
return [] # Handled by Pico SDK
|
||||
else:
|
||||
return []
|
||||
compiler_config = self._config.get("compiler_flags", {}).get(compiler, {})
|
||||
return compiler_config.get(flag_type, []).copy()
|
||||
|
||||
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:
|
||||
exclude: List of filenames to exclude
|
||||
target: Target name
|
||||
var_name: Environment variable name
|
||||
default: Default value if not found
|
||||
|
||||
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 []
|
||||
return find_sources(self.src_dir, "*.c", exclude)
|
||||
if env_key and env_key in os.environ:
|
||||
return os.environ[env_key]
|
||||
|
||||
def get_test_files(self) -> List[Path]:
|
||||
"""
|
||||
Get list of test source files.
|
||||
config_default = target_config.get(f"{var_name}_default")
|
||||
if config_default:
|
||||
# Expand ~ in paths
|
||||
return str(Path(config_default).expanduser())
|
||||
|
||||
Returns:
|
||||
List of test file paths
|
||||
"""
|
||||
from .utils import find_sources
|
||||
return find_sources(self.test_dir, "*.c")
|
||||
|
||||
def get_main_sources(self) -> List[Path]:
|
||||
"""
|
||||
Get main program source files (excludes main.c and pico_main.c).
|
||||
|
||||
Returns:
|
||||
List of source files for linking with tests
|
||||
"""
|
||||
return self.get_source_files(exclude=["main.c", "pico_main.c"])
|
||||
|
||||
def get_rp2040_sources(self) -> List[Path]:
|
||||
"""
|
||||
Get source files for RP2040 build.
|
||||
|
||||
Returns:
|
||||
List of source files (excludes main.c, repl.c, file.c, test files)
|
||||
"""
|
||||
excludes = self._data.get("source_excludes", {}).get("rp2040", [])
|
||||
sources = self.get_source_files(exclude=excludes)
|
||||
|
||||
# Add pico_main.c if it exists
|
||||
pico_main = self.src_dir / "pico_main.c"
|
||||
if pico_main.exists():
|
||||
sources.append(pico_main)
|
||||
|
||||
return sources
|
||||
|
||||
def set_verbose(self, verbose: bool = True):
|
||||
"""
|
||||
Enable/disable verbose output.
|
||||
|
||||
Args:
|
||||
verbose: Whether to enable verbose output
|
||||
"""
|
||||
self.verbose = verbose
|
||||
return self
|
||||
|
||||
def set_debug(self, debug: bool = True):
|
||||
"""
|
||||
Enable/disable debug build.
|
||||
|
||||
Args:
|
||||
debug: Whether to build with debug symbols
|
||||
"""
|
||||
self.debug = debug
|
||||
return self
|
||||
|
||||
def set_parallel_jobs(self, jobs: int):
|
||||
"""
|
||||
Set number of parallel build jobs.
|
||||
|
||||
Args:
|
||||
jobs: Number of parallel jobs (0 = auto-detect)
|
||||
"""
|
||||
if jobs <= 0:
|
||||
self.parallel_jobs = os.cpu_count() or 1
|
||||
else:
|
||||
self.parallel_jobs = jobs
|
||||
return self
|
||||
|
||||
def save_config(self, output_file: Path):
|
||||
"""
|
||||
Save current configuration to a JSON file.
|
||||
|
||||
Args:
|
||||
output_file: Path to output JSON file
|
||||
"""
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(self._data, f, indent=2)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of config."""
|
||||
return (
|
||||
f"Config(\n"
|
||||
f" project_root={self.project_root}\n"
|
||||
f" src_dir={self.src_dir}\n"
|
||||
f" bin_dir={self.bin_dir}\n"
|
||||
f" git_hash={self.git_hash}\n"
|
||||
f" parallel_jobs={self.parallel_jobs}\n"
|
||||
f")"
|
||||
)
|
||||
return default
|
||||
|
||||
|
||||
# Global configuration instance
|
||||
_config = None
|
||||
# Global config instance
|
||||
_config_instance: Optional[Config] = None
|
||||
|
||||
|
||||
def get_config(project_root: Optional[Path] = None, config_file: Optional[Path] = None) -> Config:
|
||||
"""
|
||||
Get the global configuration instance.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory (only used on first call)
|
||||
config_file: Custom config file (only used on first call)
|
||||
|
||||
Returns:
|
||||
Config instance
|
||||
"""
|
||||
global _config
|
||||
if _config is None:
|
||||
_config = Config(project_root, config_file)
|
||||
return _config
|
||||
def get_config() -> Config:
|
||||
"""Get the global configuration instance."""
|
||||
global _config_instance
|
||||
if _config_instance is None:
|
||||
_config_instance = Config()
|
||||
return _config_instance
|
||||
|
||||
|
||||
def reset_config():
|
||||
"""Reset the global configuration (mainly for testing)."""
|
||||
global _config
|
||||
_config = None
|
||||
def set_config(config: Config) -> None:
|
||||
"""Set the global configuration instance."""
|
||||
global _config_instance
|
||||
_config_instance = config
|
||||
|
|
|
|||
|
|
@ -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']}")
|
||||
|
|
@ -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})"
|
||||
|
|
@ -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)])
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue