#!/usr/bin/env python3 """ Configuration management for the build system. Loads default configuration from config.json and provides access to build settings, paths, and compiler flags. """ import os import json import subprocess from pathlib import Path from typing import Dict, List, Any, Optional from importlib.resources import files class Config: """Central configuration manager for the build system.""" def __init__(self, config_override: Optional[Dict[str, Any]] = None): """ Initialize configuration. Args: config_override: Optional dictionary to override default config values """ self._config = self._load_default_config() if config_override: self._deep_update(self._config, config_override) # Cache for computed values self._git_hash: Optional[str] = None self._python_cmd: Optional[str] = None def _load_default_config(self) -> Dict[str, Any]: """Load configuration from config.json.""" try: # Load config.json from the same package config_file = files(__package__).joinpath('config.json') # type: ignore with config_file.open('r') as f: return json.load(f) except Exception as e: print(f"Warning: Could not load config.json: {e}") print("Using minimal default configuration") return self._minimal_default_config() def _minimal_default_config(self) -> Dict[str, Any]: """Fallback configuration if config.json cannot be loaded.""" return { "directories": { "src": "src", "test": "tests", "obj": "obj", "bin": "bin", "include": "include" }, "targets": {}, "compiler_flags": {}, "test_generation": {}, "python_command": {"windows": "python", "unix": "python3"} } def _deep_update(self, base: Dict, update: Dict) -> None: """Recursively update nested dictionaries.""" for key, value in update.items(): if isinstance(value, dict) and key in base and isinstance(base[key], dict): self._deep_update(base[key], value) else: base[key] = value # ------------------------------------------------------------------------- # Directory paths # ------------------------------------------------------------------------- @property def src_dir(self) -> Path: """Source directory path.""" return Path(self._config["directories"]["src"]) @property def test_dir(self) -> Path: """Test directory path.""" return Path(self._config["directories"]["test"]) @property def obj_dir(self) -> Path: """Object files directory path.""" return Path(self._config["directories"]["obj"]) @property def bin_dir(self) -> Path: """Binary output directory path.""" return Path(self._config["directories"]["bin"]) @property def include_dir(self) -> Path: """Include directory path.""" return Path(self._config["directories"]["include"]) # ------------------------------------------------------------------------- # Target configuration # ------------------------------------------------------------------------- def get_target_config(self, target_name: str) -> Dict[str, Any]: """ Get configuration for a specific target. Args: target_name: Name of the target (e.g., 'linux', 'windows', 'rp2040') Returns: Dictionary containing target-specific configuration """ return self._config.get("targets", {}).get(target_name, {}) def get_binary_name(self, target_name: str) -> str: """Get the output binary name for a target.""" target_config = self.get_target_config(target_name) return target_config.get("binary_name", "sls") def get_test_binary_name(self, target_name: str) -> str: """Get the test binary name for a target.""" target_config = self.get_target_config(target_name) return target_config.get("test_binary_name", "sls_tests") # ------------------------------------------------------------------------- # Compiler flags # ------------------------------------------------------------------------- def get_compiler_flags(self, compiler: str, flag_type: str = "common") -> List[str]: """ Get compiler flags for a specific compiler and flag type. Args: compiler: Compiler name (e.g., 'gcc', 'msvc', 'arm_gcc') flag_type: Type of flags ('common', 'test', 'link') Returns: List of compiler flags """ compiler_config = self._config.get("compiler_flags", {}).get(compiler, {}) return compiler_config.get(flag_type, []).copy() # ------------------------------------------------------------------------- # Test generation # ------------------------------------------------------------------------- @property def test_script_path(self) -> Path: """Path to the test generation script.""" path_str = self._config.get("test_generation", {}).get("script_path", "") return Path(path_str) if path_str else Path() @property def test_yaml_path(self) -> Path: """Path to the test cases YAML file.""" path_str = self._config.get("test_generation", {}).get("yaml_path", "") return Path(path_str) if path_str else Path() @property def test_output_file(self) -> Path: """Path to the generated test output file.""" path_str = self._config.get("test_generation", {}).get("output_file", "") return Path(path_str) if path_str else Path() # ------------------------------------------------------------------------- # Git information # ------------------------------------------------------------------------- @property def git_hash(self) -> str: """Get the current git commit hash with date.""" if self._git_hash is None: self._git_hash = self._compute_git_hash() return self._git_hash def _compute_git_hash(self) -> str: """Compute git commit hash and date.""" try: result_hash = subprocess.check_output( ["git", "describe", "--always", "--dirty", "--abbrev=7"], cwd=".", stderr=subprocess.DEVNULL, text=True ).strip() result_date = subprocess.check_output( ["git", "show", "-s", "--format=%ci"], cwd=".", stderr=subprocess.DEVNULL, text=True ).strip() return f"{result_hash} {result_date}" except Exception: return "unknown" # ------------------------------------------------------------------------- # Platform-specific settings # ------------------------------------------------------------------------- @property def python_command(self) -> str: """Get the appropriate Python command for the current platform.""" if self._python_cmd is None: if os.name == "nt": self._python_cmd = self._config.get("python_command", {}).get("windows", "python") else: self._python_cmd = self._config.get("python_command", {}).get("unix", "python3") return self._python_cmd # type: ignore # ------------------------------------------------------------------------- # Environment variables # ------------------------------------------------------------------------- def get_env_var(self, target: str, var_name: str, default: Optional[str] = None) -> Optional[str]: """ Get an environment variable value, with target-specific defaults. Args: target: Target name var_name: Environment variable name default: Default value if not found Returns: Environment variable value or default """ target_config = self.get_target_config(target) env_key = target_config.get(f"{var_name}_env") if env_key and env_key in os.environ: return os.environ[env_key] config_default = target_config.get(f"{var_name}_default") if config_default: # Expand ~ in paths return str(Path(config_default).expanduser()) return default # Global config instance _config_instance: Optional[Config] = None def get_config() -> Config: """Get the global configuration instance.""" global _config_instance if _config_instance is None: _config_instance = Config() return _config_instance def set_config(config: Config) -> None: """Set the global configuration instance.""" global _config_instance _config_instance = config