Worked on build system
This commit is contained in:
parent
a66bce0041
commit
d7c23225f3
|
|
@ -0,0 +1,293 @@
|
|||
"""
|
||||
Build system configuration.
|
||||
|
||||
This module centralizes all build configuration including paths,
|
||||
compiler flags, and build settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
Centralized build configuration.
|
||||
|
||||
All paths, flags, and settings are defined here for easy modification.
|
||||
"""
|
||||
|
||||
def __init__(self, project_root: Optional[Path] = None):
|
||||
"""
|
||||
Initialize build configuration.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory (default: current directory)
|
||||
"""
|
||||
# Project structure
|
||||
self.project_root = project_root or Path.cwd()
|
||||
self.src_dir = self.project_root / "src"
|
||||
self.test_dir = self.project_root / "tests"
|
||||
self.include_dir = self.project_root / "include"
|
||||
self.obj_dir = self.project_root / "obj"
|
||||
self.bin_dir = self.project_root / "bin"
|
||||
self.build_dir = self.project_root / "build"
|
||||
|
||||
# Target executables
|
||||
self.main_target = self.bin_dir / "sls"
|
||||
self.test_target = self.bin_dir / "sls_tests"
|
||||
|
||||
# Build settings
|
||||
self.parallel_jobs = os.cpu_count() or 1
|
||||
self.verbose = False
|
||||
self.debug = True
|
||||
|
||||
# Git integration
|
||||
self.git_hash = self._get_git_hash()
|
||||
|
||||
# Compiler flags by category
|
||||
self._init_compiler_flags()
|
||||
|
||||
# Platform-specific settings
|
||||
self._init_platform_settings()
|
||||
|
||||
def _get_git_hash(self) -> str:
|
||||
"""Get git commit hash for version info."""
|
||||
from .utils import git_commit_hash
|
||||
return git_commit_hash()
|
||||
|
||||
def _init_compiler_flags(self):
|
||||
"""Initialize compiler flag sets."""
|
||||
|
||||
# Common GCC/Clang flags
|
||||
self.gcc_common_flags = [
|
||||
"-std=c99",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-g", # Debug symbols
|
||||
]
|
||||
|
||||
self.gcc_test_flags = [
|
||||
"-std=c99",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wno-unused-function",
|
||||
"-Werror",
|
||||
"-g",
|
||||
"-O0", # No optimization for tests
|
||||
]
|
||||
|
||||
# MSVC flags
|
||||
self.msvc_common_flags = [
|
||||
"/std:c11",
|
||||
"/W4",
|
||||
"/WX",
|
||||
"/Zi",
|
||||
]
|
||||
|
||||
self.msvc_test_flags = [
|
||||
"/std:c11",
|
||||
"/W4",
|
||||
"/WX",
|
||||
"/Zi",
|
||||
"/Od", # No optimization
|
||||
]
|
||||
|
||||
# ARM GCC flags for RP2040
|
||||
self.arm_gcc_flags = [
|
||||
"-mcpu=cortex-m0plus",
|
||||
"-mthumb",
|
||||
]
|
||||
|
||||
def _init_platform_settings(self):
|
||||
"""Initialize platform-specific settings."""
|
||||
|
||||
# Pico SDK settings
|
||||
self.pico_sdk_path = Path(os.environ.get(
|
||||
"PICO_SDK_PATH",
|
||||
Path.home() / "pico" / "pico-sdk"
|
||||
))
|
||||
self.pico_build_dir = self.project_root / "build_pico"
|
||||
self.pico_toolchain_file = self.project_root / "pico_arm_gcc_toolchain.cmake"
|
||||
|
||||
# macOS settings
|
||||
self.macos_min_version = "10.13"
|
||||
|
||||
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)
|
||||
"""
|
||||
sources = self.get_source_files(
|
||||
exclude=["main.c", "repl.c", "file.c", "test"]
|
||||
)
|
||||
|
||||
# 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 __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:
|
||||
"""
|
||||
Get the global configuration instance.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory (only used on first call)
|
||||
|
||||
Returns:
|
||||
Config instance
|
||||
"""
|
||||
global _config
|
||||
if _config is None:
|
||||
_config = Config(project_root)
|
||||
return _config
|
||||
|
||||
|
||||
def reset_config():
|
||||
"""Reset the global configuration (mainly for testing)."""
|
||||
global _config
|
||||
_config = None
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
"""
|
||||
Platform detection and information module.
|
||||
|
||||
This module provides utilities for detecting the current platform
|
||||
and getting platform-specific information.
|
||||
"""
|
||||
|
||||
import platform
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
class Platform:
|
||||
"""Platform information and detection."""
|
||||
|
||||
# Supported platforms
|
||||
LINUX = "linux"
|
||||
WINDOWS = "windows"
|
||||
MACOS = "macos"
|
||||
RP2040 = "rp2040"
|
||||
|
||||
# Platform aliases
|
||||
ALIASES = {
|
||||
"darwin": MACOS,
|
||||
"win32": WINDOWS,
|
||||
"cygwin": WINDOWS,
|
||||
"msys": WINDOWS,
|
||||
"pico": RP2040,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize platform detection."""
|
||||
self._system = None
|
||||
self._machine = None
|
||||
self._detected = None
|
||||
|
||||
@property
|
||||
def system(self) -> str:
|
||||
"""Get the system name (Linux, Windows, Darwin, etc.)."""
|
||||
if self._system is None:
|
||||
self._system = platform.system()
|
||||
return self._system
|
||||
|
||||
@property
|
||||
def machine(self) -> str:
|
||||
"""Get the machine architecture (x86_64, arm, etc.)."""
|
||||
if self._machine is None:
|
||||
self._machine = platform.machine()
|
||||
return self._machine
|
||||
|
||||
def detect(self) -> str:
|
||||
"""
|
||||
Detect the current platform.
|
||||
|
||||
Returns:
|
||||
Platform string ('linux', 'windows', 'macos')
|
||||
"""
|
||||
if self._detected is not None:
|
||||
return self._detected
|
||||
|
||||
system = self.system.lower()
|
||||
|
||||
# Check aliases
|
||||
for alias, platform_name in self.ALIASES.items():
|
||||
if alias in system:
|
||||
self._detected = platform_name
|
||||
return self._detected
|
||||
|
||||
# Direct mapping
|
||||
if system == "linux":
|
||||
self._detected = self.LINUX
|
||||
elif system == "darwin":
|
||||
self._detected = self.MACOS
|
||||
elif system == "windows":
|
||||
self._detected = self.WINDOWS
|
||||
else:
|
||||
# Unknown platform, default to linux
|
||||
self._detected = self.LINUX
|
||||
|
||||
return self._detected
|
||||
|
||||
def is_linux(self) -> bool:
|
||||
"""Check if current platform is Linux."""
|
||||
return self.detect() == self.LINUX
|
||||
|
||||
def is_windows(self) -> bool:
|
||||
"""Check if current platform is Windows."""
|
||||
return self.detect() == self.WINDOWS
|
||||
|
||||
def is_macos(self) -> bool:
|
||||
"""Check if current platform is macOS."""
|
||||
return self.detect() == self.MACOS
|
||||
|
||||
def is_64bit(self) -> bool:
|
||||
"""Check if running on 64-bit architecture."""
|
||||
return sys.maxsize > 2**32
|
||||
|
||||
def get_cpu_count(self) -> int:
|
||||
"""Get number of CPU cores."""
|
||||
import os
|
||||
return os.cpu_count() or 1
|
||||
|
||||
def get_exe_extension(self) -> str:
|
||||
"""
|
||||
Get the executable file extension for this platform.
|
||||
|
||||
Returns:
|
||||
'.exe' on Windows, '' on Unix-like systems
|
||||
"""
|
||||
return ".exe" if self.is_windows() else ""
|
||||
|
||||
def get_shared_lib_extension(self) -> str:
|
||||
"""
|
||||
Get the shared library extension for this platform.
|
||||
|
||||
Returns:
|
||||
'.dll' on Windows, '.dylib' on macOS, '.so' on Linux
|
||||
"""
|
||||
if self.is_windows():
|
||||
return ".dll"
|
||||
elif self.is_macos():
|
||||
return ".dylib"
|
||||
else:
|
||||
return ".so"
|
||||
|
||||
def normalize_platform_name(self, name: str) -> str:
|
||||
"""
|
||||
Normalize a platform name to canonical form.
|
||||
|
||||
Args:
|
||||
name: Platform name (possibly an alias)
|
||||
|
||||
Returns:
|
||||
Canonical platform name
|
||||
"""
|
||||
name = name.lower()
|
||||
|
||||
# Check if it's already canonical
|
||||
if name in [self.LINUX, self.WINDOWS, self.MACOS, self.RP2040]:
|
||||
return name
|
||||
|
||||
# Check aliases
|
||||
for alias, platform_name in self.ALIASES.items():
|
||||
if alias in name:
|
||||
return platform_name
|
||||
|
||||
# Return as-is if unknown
|
||||
return name
|
||||
|
||||
def get_platform_info(self) -> dict:
|
||||
"""
|
||||
Get detailed platform information.
|
||||
|
||||
Returns:
|
||||
Dictionary with platform details
|
||||
"""
|
||||
return {
|
||||
'platform': self.detect(),
|
||||
'system': self.system,
|
||||
'machine': self.machine,
|
||||
'architecture': platform.architecture()[0],
|
||||
'python_version': platform.python_version(),
|
||||
'is_64bit': self.is_64bit(),
|
||||
'cpu_count': self.get_cpu_count(),
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of platform."""
|
||||
return self.detect()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Detailed representation."""
|
||||
return f"Platform(system='{self.system}', detected='{self.detect()}')"
|
||||
|
||||
|
||||
# Global platform instance
|
||||
_platform = Platform()
|
||||
|
||||
|
||||
def detect_platform() -> str:
|
||||
"""
|
||||
Detect the current platform.
|
||||
|
||||
Returns:
|
||||
Platform string ('linux', 'windows', 'macos')
|
||||
"""
|
||||
return _platform.detect()
|
||||
|
||||
|
||||
def get_platform() -> Platform:
|
||||
"""
|
||||
Get the global Platform instance.
|
||||
|
||||
Returns:
|
||||
Platform instance
|
||||
"""
|
||||
return _platform
|
||||
|
||||
|
||||
def is_linux() -> bool:
|
||||
"""Check if current platform is Linux."""
|
||||
return _platform.is_linux()
|
||||
|
||||
|
||||
def is_windows() -> bool:
|
||||
"""Check if current platform is Windows."""
|
||||
return _platform.is_windows()
|
||||
|
||||
|
||||
def is_macos() -> bool:
|
||||
"""Check if current platform is macOS."""
|
||||
return _platform.is_macos()
|
||||
|
||||
|
||||
def normalize_target(target: Optional[str]) -> str:
|
||||
"""
|
||||
Normalize a target platform name.
|
||||
|
||||
Args:
|
||||
target: Target name or 'self' for current platform
|
||||
|
||||
Returns:
|
||||
Normalized platform name
|
||||
"""
|
||||
if target is None or target.lower() == "self":
|
||||
return detect_platform()
|
||||
|
||||
return _platform.normalize_platform_name(target)
|
||||
|
||||
|
||||
def print_platform_info():
|
||||
"""Print detailed platform information."""
|
||||
info = _platform.get_platform_info()
|
||||
print("Platform Information:")
|
||||
print(f" Platform: {info['platform']}")
|
||||
print(f" System: {info['system']}")
|
||||
print(f" Machine: {info['machine']}")
|
||||
print(f" Architecture: {info['architecture']}")
|
||||
print(f" Python: {info['python_version']}")
|
||||
print(f" 64-bit: {info['is_64bit']}")
|
||||
print(f" CPU Cores: {info['cpu_count']}")
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
"""
|
||||
Abstract base class for build targets.
|
||||
|
||||
This module defines the interface that all build target implementations
|
||||
must follow. A target represents a platform/architecture combination
|
||||
(e.g., Linux, Windows, RP2040) and knows how to build for that platform.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from ..config import Config
|
||||
from ..compilers.base import Compiler
|
||||
|
||||
|
||||
class Target(ABC):
|
||||
"""
|
||||
Abstract base class for all build targets.
|
||||
|
||||
A target encapsulates the knowledge of how to build for a specific
|
||||
platform, including which compiler to use, what flags to set, and
|
||||
what build steps to execute.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Config, compiler: Optional[Compiler] = None):
|
||||
"""
|
||||
Initialize the target.
|
||||
|
||||
Args:
|
||||
config: Build configuration
|
||||
compiler: Compiler to use (if None, target will auto-detect)
|
||||
"""
|
||||
self.config = config
|
||||
self.compiler = compiler or self._get_default_compiler()
|
||||
self._compiled_objects = []
|
||||
|
||||
@abstractmethod
|
||||
def _get_default_compiler(self) -> Compiler:
|
||||
"""
|
||||
Get the default compiler for this target.
|
||||
|
||||
Returns:
|
||||
Compiler instance appropriate for this target
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_name(self) -> str:
|
||||
"""
|
||||
Get the target name.
|
||||
|
||||
Returns:
|
||||
Target name (e.g., 'linux', 'windows', 'rp2040')
|
||||
"""
|
||||
pass
|
||||
|
||||
def build(self):
|
||||
"""
|
||||
Build the main executable for this target.
|
||||
|
||||
This is the main entry point for building. It compiles all sources
|
||||
and links them into an executable.
|
||||
"""
|
||||
print(f"\n=== Building for {self.get_name()} ===\n")
|
||||
|
||||
# Get source files
|
||||
sources = self.get_sources()
|
||||
|
||||
# Compile each source file
|
||||
objects = []
|
||||
for source in sources:
|
||||
obj = self.compile_source(source)
|
||||
objects.append(obj)
|
||||
|
||||
# Link into executable
|
||||
output = self.get_output_path()
|
||||
self.link_executable(objects, output)
|
||||
|
||||
print(f"\n✓ Build 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"\n✓ Test 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("\n✓ Clean complete\n")
|
||||
|
||||
def compile_source(self, source: Path, is_test: bool = False) -> Path:
|
||||
"""
|
||||
Compile a single source file.
|
||||
|
||||
Args:
|
||||
source: Source file path
|
||||
is_test: Whether this is a test compilation
|
||||
|
||||
Returns:
|
||||
Path to compiled object file
|
||||
"""
|
||||
from ..utils import needs_rebuild, mkdir
|
||||
|
||||
# Determine output path
|
||||
mkdir(self.config.obj_dir)
|
||||
obj_ext = self.compiler.get_object_extension()
|
||||
obj = self.config.obj_dir / (source.stem + obj_ext)
|
||||
deps = self.compiler.get_dependency_file(obj)
|
||||
|
||||
# Check if rebuild is needed
|
||||
if not needs_rebuild(source, obj, deps):
|
||||
return obj
|
||||
|
||||
# Get compilation parameters
|
||||
includes = self.config.get_includes()
|
||||
defines = self.config.get_defines()
|
||||
flags = self.get_compile_flags(is_test)
|
||||
|
||||
# Compile
|
||||
self.compiler.compile(
|
||||
source,
|
||||
obj,
|
||||
includes=includes,
|
||||
defines=defines,
|
||||
flags=flags,
|
||||
generate_deps=True,
|
||||
is_test=is_test
|
||||
)
|
||||
|
||||
return obj
|
||||
|
||||
def link_executable(self, objects: List[Path], output: Path):
|
||||
"""
|
||||
Link object files into an executable.
|
||||
|
||||
Args:
|
||||
objects: List of object files
|
||||
output: Output executable path
|
||||
"""
|
||||
from ..utils import mkdir
|
||||
|
||||
mkdir(output.parent)
|
||||
|
||||
libraries = self.config.get_libraries(self.get_name())
|
||||
flags = self.get_link_flags()
|
||||
|
||||
self.compiler.link(
|
||||
objects,
|
||||
output,
|
||||
libraries=libraries,
|
||||
flags=flags
|
||||
)
|
||||
|
||||
def get_sources(self) -> List[Path]:
|
||||
"""
|
||||
Get list of source files for this target.
|
||||
|
||||
Returns:
|
||||
List of source file paths
|
||||
"""
|
||||
return self.config.get_source_files()
|
||||
|
||||
def get_output_path(self) -> Path:
|
||||
"""
|
||||
Get the output executable path.
|
||||
|
||||
Returns:
|
||||
Path to output executable
|
||||
"""
|
||||
exe_ext = self.compiler.get_executable_extension()
|
||||
return self.config.main_target.with_suffix(exe_ext)
|
||||
|
||||
def get_test_output_path(self) -> Path:
|
||||
"""
|
||||
Get the test executable path.
|
||||
|
||||
Returns:
|
||||
Path to test executable
|
||||
"""
|
||||
exe_ext = self.compiler.get_executable_extension()
|
||||
return self.config.test_target.with_suffix(exe_ext)
|
||||
|
||||
def get_compile_flags(self, is_test: bool = False) -> List[str]:
|
||||
"""
|
||||
Get compilation flags for this target.
|
||||
|
||||
Args:
|
||||
is_test: Whether this is a test compilation
|
||||
|
||||
Returns:
|
||||
List of compiler flags
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_link_flags(self) -> List[str]:
|
||||
"""
|
||||
Get linker flags for this target.
|
||||
|
||||
Returns:
|
||||
List of linker flags
|
||||
"""
|
||||
return []
|
||||
|
||||
def generate_tests(self):
|
||||
"""
|
||||
Generate test code if needed.
|
||||
|
||||
Override this to implement test generation logic.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of target."""
|
||||
return f"{self.__class__.__name__}(compiler={self.compiler})"
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
"""
|
||||
Linux build target.
|
||||
|
||||
This module implements the build target for native Linux builds.
|
||||
Uses GCC as the default compiler.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from .base import Target
|
||||
from ..compilers import GCCCompiler, detect_compiler
|
||||
from ..config import Config
|
||||
|
||||
|
||||
class LinuxTarget(Target):
|
||||
"""
|
||||
Linux native build target.
|
||||
|
||||
Builds executables for Linux using GCC or Clang.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Config, compiler: Optional[GCCCompiler] = None):
|
||||
"""
|
||||
Initialize Linux target.
|
||||
|
||||
Args:
|
||||
config: Build configuration
|
||||
compiler: Compiler to use (default: auto-detect GCC/Clang)
|
||||
"""
|
||||
super().__init__(config, compiler)
|
||||
|
||||
def _get_default_compiler(self) -> GCCCompiler:
|
||||
"""
|
||||
Get the default compiler for Linux.
|
||||
|
||||
Returns:
|
||||
GCC or Clang compiler instance
|
||||
"""
|
||||
return detect_compiler("linux") # type: ignore
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""Get the target name."""
|
||||
return "linux"
|
||||
|
||||
def get_compile_flags(self, is_test: bool = False) -> List[str]:
|
||||
"""
|
||||
Get compilation flags for Linux.
|
||||
|
||||
Args:
|
||||
is_test: Whether this is a test compilation
|
||||
|
||||
Returns:
|
||||
List of compiler flags
|
||||
"""
|
||||
if is_test:
|
||||
return self.config.gcc_test_flags.copy()
|
||||
else:
|
||||
return self.config.gcc_common_flags.copy()
|
||||
|
||||
def get_link_flags(self) -> List[str]:
|
||||
"""
|
||||
Get linker flags for Linux.
|
||||
|
||||
Returns:
|
||||
List of linker flags
|
||||
"""
|
||||
return []
|
||||
|
||||
def generate_tests(self):
|
||||
"""Generate test code from YAML if available."""
|
||||
from ..utils import detect_python, run_command
|
||||
|
||||
script = Path("../SLS_Tests/yaml_to_c_tests.py")
|
||||
yaml = Path("../SLS_Tests/cases.yaml")
|
||||
out = self.config.test_dir / "lexer_tests.c"
|
||||
|
||||
if not script.exists() or not yaml.exists():
|
||||
if self.config.verbose:
|
||||
print("Test generation skipped (missing files).")
|
||||
return
|
||||
|
||||
# Check if regeneration is needed
|
||||
if out.exists() and out.stat().st_mtime > yaml.stat().st_mtime:
|
||||
return
|
||||
|
||||
print("Generating tests from YAML...")
|
||||
python = detect_python()
|
||||
run_command([python, str(script), str(yaml), str(out)])
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
"""
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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