YREA-SLS/SLS_C/build_system/targets/base.py

328 lines
9.3 KiB
Python

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