Restart, emptied all files
This commit is contained in:
parent
df070b080e
commit
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 +0,0 @@
|
||||||
{
|
|
||||||
"paths": {
|
|
||||||
"src_dir": "src",
|
|
||||||
"test_dir": "tests",
|
|
||||||
"include_dir": "include",
|
|
||||||
"obj_dir": "obj",
|
|
||||||
"bin_dir": "bin",
|
|
||||||
"build_dir": "build"
|
|
||||||
},
|
|
||||||
"targets": {
|
|
||||||
"main": "sls",
|
|
||||||
"test": "sls_tests"
|
|
||||||
},
|
|
||||||
"parallel_jobs": 0,
|
|
||||||
"verbose": false,
|
|
||||||
"debug": true,
|
|
||||||
"gcc_common_flags": [
|
|
||||||
"-std=c99",
|
|
||||||
"-Wall",
|
|
||||||
"-Wextra",
|
|
||||||
"-Werror",
|
|
||||||
"-g"
|
|
||||||
],
|
|
||||||
"gcc_test_flags": [
|
|
||||||
"-std=c99",
|
|
||||||
"-Wall",
|
|
||||||
"-Wextra",
|
|
||||||
"-Wno-unused-function",
|
|
||||||
"-Werror",
|
|
||||||
"-g",
|
|
||||||
"-O0"
|
|
||||||
],
|
|
||||||
"msvc_common_flags": [
|
|
||||||
"/std:c11",
|
|
||||||
"/W4",
|
|
||||||
"/WX",
|
|
||||||
"/Zi"
|
|
||||||
],
|
|
||||||
"msvc_test_flags": [
|
|
||||||
"/std:c11",
|
|
||||||
"/W4",
|
|
||||||
"/WX",
|
|
||||||
"/Zi",
|
|
||||||
"/Od"
|
|
||||||
],
|
|
||||||
"arm_gcc_flags": [
|
|
||||||
"-mcpu=cortex-m0plus",
|
|
||||||
"-mthumb"
|
|
||||||
],
|
|
||||||
"pico": {
|
|
||||||
"sdk_path_env": "PICO_SDK_PATH",
|
|
||||||
"sdk_path_default": "~/pico/pico-sdk",
|
|
||||||
"build_dir": "build_pico",
|
|
||||||
"toolchain_file": "pico_arm_gcc_toolchain.cmake"
|
|
||||||
},
|
|
||||||
"macos": {
|
|
||||||
"min_version": "10.13"
|
|
||||||
},
|
|
||||||
"source_excludes": {
|
|
||||||
"rp2040": [
|
|
||||||
"main.c",
|
|
||||||
"repl.c",
|
|
||||||
"file.c",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,301 +0,0 @@
|
||||||
"""
|
|
||||||
Build system configuration.
|
|
||||||
|
|
||||||
This module centralizes all build configuration including paths,
|
|
||||||
compiler flags, and build settings. Configuration can be loaded from
|
|
||||||
a default JSON file or overridden programmatically.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List, Dict, Optional
|
|
||||||
|
|
||||||
from importlib import resources
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: This should be utilized more throughout the build system.
|
|
||||||
class Config:
|
|
||||||
"""
|
|
||||||
Centralized build configuration.
|
|
||||||
|
|
||||||
All paths, flags, and settings are defined here for easy modification.
|
|
||||||
Configuration is loaded from a default JSON file and can be overridden.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, project_root: Optional[Path] = None, config_file: Optional[Path] = None):
|
|
||||||
"""
|
|
||||||
Initialize build configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_root: Project root directory (default: current directory)
|
|
||||||
config_file: Custom config file path (default: uses built-in config.json)
|
|
||||||
"""
|
|
||||||
# Load default configuration
|
|
||||||
self._load_default_config(config_file)
|
|
||||||
|
|
||||||
# Project structure
|
|
||||||
self.project_root = project_root or Path.cwd()
|
|
||||||
self._setup_paths()
|
|
||||||
|
|
||||||
# Build settings
|
|
||||||
self.parallel_jobs = self._data.get("parallel_jobs", os.cpu_count() or 1)
|
|
||||||
self.verbose = self._data.get("verbose", False)
|
|
||||||
self.debug = self._data.get("debug", True)
|
|
||||||
|
|
||||||
# Git integration
|
|
||||||
self.git_hash = self._get_git_hash()
|
|
||||||
|
|
||||||
# Compiler flags
|
|
||||||
self.gcc_common_flags = self._data.get("gcc_common_flags", [])
|
|
||||||
self.gcc_test_flags = self._data.get("gcc_test_flags", [])
|
|
||||||
self.msvc_common_flags = self._data.get("msvc_common_flags", [])
|
|
||||||
self.msvc_test_flags = self._data.get("msvc_test_flags", [])
|
|
||||||
self.arm_gcc_flags = self._data.get("arm_gcc_flags", [])
|
|
||||||
|
|
||||||
# Platform-specific settings
|
|
||||||
self._setup_platform_settings()
|
|
||||||
|
|
||||||
def _load_default_config(self, config_file: Optional[Path] = None):
|
|
||||||
"""
|
|
||||||
Load configuration from JSON file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_file: Custom config file, or None to use default
|
|
||||||
"""
|
|
||||||
if config_file and config_file.exists():
|
|
||||||
# Load custom config file
|
|
||||||
with open(config_file, 'r') as f:
|
|
||||||
self._data = json.load(f)
|
|
||||||
else:
|
|
||||||
# Load default config from package resources
|
|
||||||
config_text = resources.files("build_system").joinpath("config.json").read_text()
|
|
||||||
self._data = json.loads(config_text)
|
|
||||||
|
|
||||||
def _setup_paths(self):
|
|
||||||
"""Setup directory paths from configuration."""
|
|
||||||
paths = self._data.get("paths", {})
|
|
||||||
|
|
||||||
self.src_dir = self.project_root / paths.get("src_dir", "src")
|
|
||||||
self.test_dir = self.project_root / paths.get("test_dir", "tests")
|
|
||||||
self.include_dir = self.project_root / paths.get("include_dir", "include")
|
|
||||||
self.obj_dir = self.project_root / paths.get("obj_dir", "obj")
|
|
||||||
self.bin_dir = self.project_root / paths.get("bin_dir", "bin")
|
|
||||||
self.build_dir = self.project_root / paths.get("build_dir", "build")
|
|
||||||
|
|
||||||
# Target executables
|
|
||||||
targets = self._data.get("targets", {})
|
|
||||||
self.main_target = self.bin_dir / targets.get("main", "sls")
|
|
||||||
self.test_target = self.bin_dir / targets.get("test", "sls_tests")
|
|
||||||
|
|
||||||
def _setup_platform_settings(self):
|
|
||||||
"""Setup platform-specific settings from configuration."""
|
|
||||||
pico_config = self._data.get("pico", {})
|
|
||||||
|
|
||||||
# Pico SDK settings
|
|
||||||
sdk_env = pico_config.get("sdk_path_env", "PICO_SDK_PATH")
|
|
||||||
sdk_default = pico_config.get("sdk_path_default", "~/pico/pico-sdk")
|
|
||||||
sdk_default = Path(sdk_default).expanduser()
|
|
||||||
|
|
||||||
self.pico_sdk_path = Path(os.environ.get(sdk_env, sdk_default))
|
|
||||||
self.pico_build_dir = self.project_root / pico_config.get("build_dir", "build_pico")
|
|
||||||
self.pico_toolchain_file = self.project_root / pico_config.get(
|
|
||||||
"toolchain_file", "pico_arm_gcc_toolchain.cmake"
|
|
||||||
)
|
|
||||||
|
|
||||||
# macOS settings
|
|
||||||
macos_config = self._data.get("macos", {})
|
|
||||||
self.macos_min_version = macos_config.get("min_version", "10.13")
|
|
||||||
|
|
||||||
def _get_git_hash(self) -> str:
|
|
||||||
"""Get git commit hash for version info."""
|
|
||||||
from .utils import git_commit_hash
|
|
||||||
return git_commit_hash()
|
|
||||||
|
|
||||||
def get_defines(self, extra: Optional[Dict] = None) -> Dict:
|
|
||||||
"""
|
|
||||||
Get preprocessor defines.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
extra: Additional defines to include
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary of defines
|
|
||||||
"""
|
|
||||||
defines = {
|
|
||||||
"GIT_COMMIT_HASH": self.git_hash,
|
|
||||||
}
|
|
||||||
|
|
||||||
if extra:
|
|
||||||
defines.update(extra)
|
|
||||||
|
|
||||||
return defines
|
|
||||||
|
|
||||||
def get_includes(self, extra: Optional[List[Path]] = None) -> List[Path]:
|
|
||||||
"""
|
|
||||||
Get include directories.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
extra: Additional include directories
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of include directory paths
|
|
||||||
"""
|
|
||||||
includes = [self.include_dir]
|
|
||||||
|
|
||||||
if extra:
|
|
||||||
includes.extend(extra)
|
|
||||||
|
|
||||||
return includes
|
|
||||||
|
|
||||||
def get_libraries(self, target: str = "linux") -> List[str]:
|
|
||||||
"""
|
|
||||||
Get libraries to link based on target.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
target: Target platform
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of library names
|
|
||||||
"""
|
|
||||||
if target in ("linux", "macos"):
|
|
||||||
return ["m"] # Math library
|
|
||||||
elif target == "windows":
|
|
||||||
return [] # No special libraries needed
|
|
||||||
elif target == "rp2040":
|
|
||||||
return [] # Handled by Pico SDK
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_source_files(self, exclude: Optional[List[str]] = None) -> List[Path]:
|
|
||||||
"""
|
|
||||||
Get list of source files.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
exclude: List of filenames to exclude
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of source file paths
|
|
||||||
"""
|
|
||||||
from .utils import find_sources
|
|
||||||
|
|
||||||
exclude = exclude or []
|
|
||||||
return find_sources(self.src_dir, "*.c", exclude)
|
|
||||||
|
|
||||||
def get_test_files(self) -> List[Path]:
|
|
||||||
"""
|
|
||||||
Get list of test source files.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of test file paths
|
|
||||||
"""
|
|
||||||
from .utils import find_sources
|
|
||||||
return find_sources(self.test_dir, "*.c")
|
|
||||||
|
|
||||||
def get_main_sources(self) -> List[Path]:
|
|
||||||
"""
|
|
||||||
Get main program source files (excludes main.c and pico_main.c).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of source files for linking with tests
|
|
||||||
"""
|
|
||||||
return self.get_source_files(exclude=["main.c", "pico_main.c"])
|
|
||||||
|
|
||||||
def get_rp2040_sources(self) -> List[Path]:
|
|
||||||
"""
|
|
||||||
Get source files for RP2040 build.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of source files (excludes main.c, repl.c, file.c, test files)
|
|
||||||
"""
|
|
||||||
excludes = self._data.get("source_excludes", {}).get("rp2040", [])
|
|
||||||
sources = self.get_source_files(exclude=excludes)
|
|
||||||
|
|
||||||
# Add pico_main.c if it exists
|
|
||||||
pico_main = self.src_dir / "pico_main.c"
|
|
||||||
if pico_main.exists():
|
|
||||||
sources.append(pico_main)
|
|
||||||
|
|
||||||
return sources
|
|
||||||
|
|
||||||
def set_verbose(self, verbose: bool = True):
|
|
||||||
"""
|
|
||||||
Enable/disable verbose output.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
verbose: Whether to enable verbose output
|
|
||||||
"""
|
|
||||||
self.verbose = verbose
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_debug(self, debug: bool = True):
|
|
||||||
"""
|
|
||||||
Enable/disable debug build.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
debug: Whether to build with debug symbols
|
|
||||||
"""
|
|
||||||
self.debug = debug
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_parallel_jobs(self, jobs: int):
|
|
||||||
"""
|
|
||||||
Set number of parallel build jobs.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
jobs: Number of parallel jobs (0 = auto-detect)
|
|
||||||
"""
|
|
||||||
if jobs <= 0:
|
|
||||||
self.parallel_jobs = os.cpu_count() or 1
|
|
||||||
else:
|
|
||||||
self.parallel_jobs = jobs
|
|
||||||
return self
|
|
||||||
|
|
||||||
def save_config(self, output_file: Path):
|
|
||||||
"""
|
|
||||||
Save current configuration to a JSON file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
output_file: Path to output JSON file
|
|
||||||
"""
|
|
||||||
with open(output_file, 'w') as f:
|
|
||||||
json.dump(self._data, f, indent=2)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
"""String representation of config."""
|
|
||||||
return (
|
|
||||||
f"Config(\n"
|
|
||||||
f" project_root={self.project_root}\n"
|
|
||||||
f" src_dir={self.src_dir}\n"
|
|
||||||
f" bin_dir={self.bin_dir}\n"
|
|
||||||
f" git_hash={self.git_hash}\n"
|
|
||||||
f" parallel_jobs={self.parallel_jobs}\n"
|
|
||||||
f")"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Global configuration instance
|
|
||||||
_config = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_config(project_root: Optional[Path] = None, config_file: Optional[Path] = None) -> Config:
|
|
||||||
"""
|
|
||||||
Get the global configuration instance.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_root: Project root directory (only used on first call)
|
|
||||||
config_file: Custom config file (only used on first call)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Config instance
|
|
||||||
"""
|
|
||||||
global _config
|
|
||||||
if _config is None:
|
|
||||||
_config = Config(project_root, config_file)
|
|
||||||
return _config
|
|
||||||
|
|
||||||
|
|
||||||
def reset_config():
|
|
||||||
"""Reset the global configuration (mainly for testing)."""
|
|
||||||
global _config
|
|
||||||
_config = None
|
|
||||||
|
|
@ -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