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