YREA-SLS/SLS_C/build_system/config.py

302 lines
9.3 KiB
Python

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