Compare commits
No commits in common. "df070b080ef56c2393e9ce3e014007ad4ee66fad" and "a66bce0041f26fa26258ed2f0eee7633957e0b90" have entirely different histories.
df070b080e
...
a66bce0041
|
|
@ -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