328 lines
9.3 KiB
Python
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"\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})"
|