diff --git a/tools/shuriken.py b/tools/shuriken.py index e5a1943..f65d5be 100755 --- a/tools/shuriken.py +++ b/tools/shuriken.py @@ -10,7 +10,6 @@ ############################################ import argparse -import copy import json import shlex import subprocess @@ -18,7 +17,7 @@ import sys from collections import namedtuple from dataclasses import asdict, dataclass, field from pathlib import Path -from typing import Any +from typing import Any, Self import yaml @@ -60,6 +59,38 @@ class TargetConfig: return f"{self.linker_args or ''} {self.linker_args_extra or ''}".strip() +@dataclass(kw_only=True) +class MergedTargetConfig(TargetConfig): + cpp_compiler: str = '' + cpp_standard: str = '' + cpp_flags: str = '' + cpp_flags_extra: str = '' + + linker_flags: str = '' + linker_flags_extra: str = '' + linker_args: str = '' + linker_args_extra: str = '' + + output_file: str = '' + run_command: str = '' + + @classmethod + def create_from(cls, target_defaults: TargetConfig, target_config: TargetConfig) -> Self: + merged_config = cls() + merged_config.update(asdict(target_defaults)) + merged_config.update(asdict(target_config)) + return merged_config + + def validate(self) -> None: + # Validate that all required fields are set + if not self.cpp_compiler: + raise ConfigValidationException('cpp_compiler must be set') + if not self.cpp_standard: + raise ConfigValidationException('cpp_standard must be set') + if not self.output_file: + raise ConfigValidationException('output_file must be set') + + @dataclass(kw_only=True) class ShurikenConfig: __path_fields = ['source_dir', 'build_dir'] @@ -72,9 +103,6 @@ class ShurikenConfig: defaults: TargetConfig = field(default_factory=lambda: TargetConfig( cpp_compiler='clang++', cpp_standard='c++20', - cpp_flags='', - linker_flags='', - linker_args='', )) enable_import_std: bool = False @@ -82,6 +110,9 @@ class ShurikenConfig: default_target: str = '' targets: dict[str, TargetConfig] = field(default_factory=dict) + # Merged target configs, generated from defaults and targets + _merged_target_configs = {} + def update(self, data: dict) -> None: for key, value in data.items(): # Ignore unknown config keys @@ -116,6 +147,10 @@ class ShurikenConfig: else: setattr(self, key, str(value)) + # Generate merged target configs (we generate all of them so that we can validate the full config) + for target_name, target_config in self.targets.items(): + self._merged_target_configs[target_name] = MergedTargetConfig.create_from(self.defaults, target_config) + def update_from_yaml(self, file_path: Path) -> None: with file_path.open('r') as file: self.update(yaml.safe_load(file)) @@ -137,23 +172,18 @@ class ShurikenConfig: raise ConfigValidationException('default_target is not defined in targets') # Validate individual build targets - for target_name, target_config in self.targets.items(): - if not target_config.cpp_compiler and not self.defaults.cpp_compiler: - raise ConfigValidationException(f'Target "{target_name}": cpp_compiler must be set') - if not target_config.cpp_standard and not self.defaults.cpp_standard: - raise ConfigValidationException(f'Target "{target_name}": cpp_standard must be set') - if not target_config.output_file and not self.defaults.output_file: - raise ConfigValidationException(f'Target "{target_name}": output_file must be set') + for target_name, merged_target_config in self._merged_target_configs.items(): + try: + merged_target_config.validate() + except ConfigValidationException as exc: + raise ConfigValidationException(f'Target "{target_name}": {str(exc)}') def get_ninja_file_path(self, target: str) -> Path: return self.build_dir / f'build.{target}.ninja' - def get_merged_target_config(self, target: str) -> TargetConfig: + def get_merged_target_config(self, target: str) -> MergedTargetConfig: assert target in self.targets, f'Target "{target}" not defined!' - - merged_config = copy.deepcopy(self.defaults) - merged_config.update(asdict(self.targets[target])) - return merged_config + return self._merged_target_configs[target] class ShurikenArgumentParser: