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