Created config.py

This commit is contained in:
Kyler Olsen 2025-12-19 13:49:48 -07:00
parent 9f8d66ccae
commit 1f0fccc866
2 changed files with 346 additions and 0 deletions

View File

@ -0,0 +1,91 @@
{
"directories": {
"src": "src",
"test": "tests",
"obj": "obj",
"bin": "bin",
"include": "include"
},
"targets": {
"linux": {
"binary_name": "sls",
"test_binary_name": "sls_tests"
},
"windows": {
"binary_name": "sls.exe",
"test_binary_name": "sls_tests.exe"
},
"rp2040": {
"binary_name": "sls.elf",
"build_dir": "build_pico",
"toolchain_file": "pico_arm_gcc_toolchain.cmake",
"sdk_path_env": "PICO_SDK_PATH",
"sdk_path_default": "~/pico/pico-sdk",
"excluded_sources": [
"main.c",
"repl.c",
"file.c"
],
"required_sources": [
"pico_main.c"
]
}
},
"compiler_flags": {
"gcc": {
"common": [
"-std=c99",
"-Wall",
"-Wextra",
"-Werror",
"-Iinclude",
"-g"
],
"test": [
"-std=c99",
"-Wall",
"-Wextra",
"-Wno-unused-function",
"-Werror",
"-Iinclude",
"-g",
"-O0"
],
"link": [
"-lm"
]
},
"msvc": {
"common": [
"/std:c11",
"/Zi",
"/Iinclude"
],
"test": [
"/std:c11",
"/Zi",
"/Iinclude"
],
"link": []
},
"arm_gcc": {
"common": [
"-std=c99",
"-Wall",
"-Wextra",
"-mcpu=cortex-m0plus",
"-mthumb"
],
"link": []
}
},
"test_generation": {
"script_path": "../SLS_Tests/yaml_to_c_tests.py",
"yaml_path": "../SLS_Tests/cases.yaml",
"output_file": "tests/lexer_tests.c"
},
"python_command": {
"windows": "python",
"unix": "python3"
}
}

View File

@ -0,0 +1,255 @@
#!/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