From b9335a6247978d4d0644613c64da0c89add09820 Mon Sep 17 00:00:00 2001 From: Patrick Wuttke Date: Sat, 20 Sep 2025 14:01:31 +0200 Subject: [PATCH] Added command line tool and removed lib folder from gitignore. --- .gitignore | 2 +- SConscript | 82 ++++++++++++--------------- addons/config_cache.py | 35 ++++++++++++ lib/spp.py | 55 ++++++++++++++++++ util/__init__.py | 0 util/python_module/sppcmd/__init__.py | 24 ++++++++ util/python_module/sppcmd/ccjson.py | 18 ++++++ util/python_module/sppcmd/common.py | 51 +++++++++++++++++ util/spp_cmd.py | 10 ++++ 9 files changed, 230 insertions(+), 47 deletions(-) create mode 100644 addons/config_cache.py create mode 100644 lib/spp.py create mode 100644 util/__init__.py create mode 100644 util/python_module/sppcmd/__init__.py create mode 100644 util/python_module/sppcmd/ccjson.py create mode 100644 util/python_module/sppcmd/common.py create mode 100755 util/spp_cmd.py diff --git a/.gitignore b/.gitignore index ca5de20..d04378e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ dist/ downloads/ eggs/ .eggs/ -lib/ +# lib/ lib64/ parts/ sdist/ diff --git a/SConscript b/SConscript index 168d8f4..a77e68a 100644 --- a/SConscript +++ b/SConscript @@ -23,7 +23,7 @@ from SCons.Script import * sys.path.append(os.path.join(Dir('.').abspath, 'lib')) -from spp import _init_interface +from spp import _init_interface, Module, Target, TargetType _init_interface(globals=globals()) @@ -46,12 +46,6 @@ _GCC_CPU_FEATURES_MAP = { 'avx2': '-mavx2' } -class TargetType(enum.Enum): - PROGRAM = 0 - STATIC_LIBRARY = 1 - SHARED_LIBRARY = 2 - MISC = 3 - class _VersionSpec: minimum_version = None maximum_version = None @@ -73,23 +67,6 @@ class _Dependency: depdeps: list = [] cook_result: dict = {} -@dataclass -class _Module: - name: str - folder: str - description: str - cxx_namespace: str - -class _Target: - name: str - target_type: TargetType - builder = None - args: list = [] - kwargs: dict = {} - dependencies: list = [] - target = None - module: _Module = None - def _find_recipe(env: Environment, recipe_name: str): if recipe_name in env['SPP_RECIPES']: return env['SPP_RECIPES'][recipe_name] @@ -150,7 +127,7 @@ def _module(env: Environment, file: str): folder = _normalize_module_path(env, env.File(file).dir.abspath) if folder is not None: # only include modules inside the source tree dirname = os.path.basename(folder) - env.Append(SPP_MODULES = {folder: _Module( + env.Append(SPP_MODULES = {folder: Module( name=dirname, folder=folder, description='', @@ -202,7 +179,7 @@ def _inject_dependency(dependency, kwargs: dict, add_sources: bool = True) -> No _inject_list(kwargs, dependency.cook_result, 'LINKFLAGS') for depdep in dependency.depdeps: _inject_dependency(depdep, kwargs) - elif isinstance(dependency, _Target): + elif isinstance(dependency, Target): _inject_list(kwargs, dependency.kwargs, 'CPPPATH') _inject_list(kwargs, dependency.kwargs, 'CPPDEFINES') _inject_list(kwargs, dependency.kwargs, 'LIBPATH') @@ -489,7 +466,7 @@ def _wrap_builder(builder, target_type: TargetType): if 'source' in kwargs: kwargs['source'] = _fix_filearg(kwargs['source']) - target = _Target() + target = Target() if 'name' in kwargs: target.name = kwargs['name'] else: @@ -518,7 +495,7 @@ def _wrap_builder(builder, target_type: TargetType): def _wrap_default(default): def _wrapped(env, arg): - if isinstance(arg, _Target): + if isinstance(arg, Target): env.Append(SPP_DEFAULT_TARGETS = [arg]) elif isinstance(arg, dict) and '_target' in arg: default(arg['_target']) @@ -528,7 +505,7 @@ def _wrap_default(default): def _wrap_depends(depends): def _wrapped(env, dependant, dependency): - if isinstance(dependant, _Target) or isinstance(dependency, _Target): + if isinstance(dependant, Target) or isinstance(dependency, Target): env.Append(SPP_TARGET_DEPENDENCIES = [(dependant, dependency, depends)]) return elif isinstance(dependant, dict) and '_target' in dependant: @@ -538,7 +515,7 @@ def _wrap_depends(depends): depends(dependant, dependency) return _wrapped -def _build_target(target: _Target): +def _build_target(target: Target): for dependency in target.dependencies: _inject_dependency(dependency, target.kwargs) if 'LIBS' in target.kwargs: @@ -548,7 +525,7 @@ def _build_target(target: _Target): target.kwargs['LIBS'].remove(lib) target.kwargs['LIBS'].append(env.File(lib)) pass - elif isinstance(lib, _Target): + elif isinstance(lib, Target): if not lib.target: _build_target(lib) target.kwargs['LIBS'].remove(lib) @@ -568,6 +545,7 @@ def _finalize(env: Environment): _generate_project(generate_project) Exit(0) + _hook_pre_finalize.invoke() version_requirements = {dep.name: { 'min': dep.version_spec.minimum_version and _version_to_string(dep.version_spec.minimum_version), 'max': dep.version_spec.maximum_version and _version_to_string(dep.version_spec.maximum_version), @@ -594,13 +572,15 @@ def _finalize(env: Environment): for target in env['SPP_DEFAULT_TARGETS']: env.Default(target.target) for dependant, dependency, depends in env['SPP_TARGET_DEPENDENCIES']: - if isinstance(dependant, _Target): + if isinstance(dependant, Target): dependant = dependant.target - if isinstance(dependency, _Target): + if isinstance(dependency, Target): dependency = dependency.target depends(dependant, dependency) -def _find_target(env: Environment, target_name: str) -> '_Target|None': + _hook_post_finalize.invoke() + +def _find_target(env: Environment, target_name: str) -> 'Target|None': for target in env['SPP_TARGETS']: if target.name == target_name: return target @@ -668,9 +648,9 @@ def _generate_project(project_type: str) -> None: if ms_style: return f'{{{result.upper()}}}' return result - + root_path = pathlib.Path(env.Dir('#').abspath) - + def _make_entry(target, type, prefix, suffix) -> str: def _full_path(build_type) -> str: trgt = _target_entry(target.kwargs['target']) @@ -719,8 +699,8 @@ def _generate_project(project_type: str) -> None: return '' path = path[pos+1:] return path - - + + def _folder_list(file_list: list[str], skip_eles: int = 0) -> list[str]: result = {} for file in file_list: @@ -739,7 +719,7 @@ def _generate_project(project_type: str) -> None: def _get_sources(target_dict: dict) -> list[str]: - target : _Target = target_dict['target'] + target : Target = target_dict['target'] sources = target.kwargs.get('source') return [str(pathlib.Path(source.abspath).relative_to(root_path)) for source in sources] @@ -955,6 +935,9 @@ class _Hook: _hook_pre_environment = _Hook() _hook_post_environment = _Hook() +_hook_config_complete = _Hook() +_hook_pre_finalize = _Hook() +_hook_post_finalize = _Hook() def _load_addon(modname: str, modpath: pathlib.Path) -> None: _debug('addons', f'Loading addon {modname} from {modpath}.') @@ -966,12 +949,17 @@ def _load_addon(modname: str, modpath: pathlib.Path) -> None: if hasattr(module, 'available') and not module.available(): _debug('addons', f'Addon {modname} is not available and will not be loaded.') return - if hasattr(module, 'pre_environment'): - _hook_pre_environment.add_func(module.pre_environment) - _debug('addons', f'Addon {modname} registered a pre_environment hook.') - if hasattr(module, 'post_environment'): - _hook_post_environment.add_func(module.post_environment) - _debug('addons', f'Addon {modname} registered a post_environment hook.') + + def _add_hook(func_name: str, hook: _Hook) -> None: + if hasattr(module, func_name): + hook.add_func(getattr(module, func_name)) + _debug('addons', f'Addon {modname} registered a {func_name} hook.') + + _add_hook('pre_environment', _hook_pre_environment) + _add_hook('post_environment', _hook_post_environment) + _add_hook('config_complete', _hook_config_complete) + _add_hook('pre_finalize', _hook_pre_finalize) + _add_hook('post_finalize', _hook_post_finalize) def _load_addons(folder: pathlib.Path) -> None: _debug('addons', f'Loading addons from {folder}.') @@ -1235,7 +1223,7 @@ env['SPP_DEFAULT_TARGETS'] = [] env['SPP_TARGET_DEPENDENCIES'] = [] env['SPP_DEPENDENCIES'] = {} env['SPP_RECIPES'] = {} -env['SPP_MODULES'] = {} # maps from folder to _Module +env['SPP_MODULES'] = {} # maps from folder to Module env['SPP_CPU_FEATURES'] = config.get('USE_CPU_FEATURES', []) env['OBJSUFFIX'] = f".{env['BUILD_TYPE']}{env['OBJSUFFIX']}" @@ -1437,6 +1425,8 @@ env.AddMethod(_find_target, 'FindTarget') if hasattr(env, 'Gch'): env.AddMethod(_wrap_builder(env.Gch, TargetType.STATIC_LIBRARY), 'Gch') +_hook_config_complete.invoke() + for addon_file in env.Glob('addons/old/*.py'): env = SConscript(addon_file, exports = 'env') diff --git a/addons/config_cache.py b/addons/config_cache.py new file mode 100644 index 0000000..33c967c --- /dev/null +++ b/addons/config_cache.py @@ -0,0 +1,35 @@ + +import json +from pathlib import Path +from spp import get_spp, TargetType + +spp = get_spp() + +def _should_generate() -> bool: + # check if any program or library target has been built + for target in spp.targets: + if target.target_type in (TargetType.PROGRAM, TargetType.STATIC_LIBRARY, TargetType.SHARED_LIBRARY): + return True + return False + +def post_finalize(**kwargs) -> None: + if not _should_generate(): + return + + cache_file = Path(spp.env['CACHE_DIR']) / 'config_cache.json' + + cache = {} + if cache_file.exists(): + try: + with cache_file.open('r') as f: + cache = json.load(f) + except Exception as e: + spp.env.Warn(f'Error while loading config cache: {e}.') + + cache['build_type'] = spp.env['BUILD_TYPE'] + + try: + with cache_file.open('w') as f: + json.dump(cache, f) + except Exception as e: + spp.env.Warn(f'Error while saving config cache: {e}.') diff --git a/lib/spp.py b/lib/spp.py new file mode 100644 index 0000000..c769d80 --- /dev/null +++ b/lib/spp.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass +import enum +from typing import TYPE_CHECKING +from SCons.Script import * + +if TYPE_CHECKING: + class SPPEnvironment(Environment): + def Info(self, message: str): ... + def Warn(self, message: str): ... + def Error(self, message: str): ... +else: + SPPEnvironment = Environment + +@dataclass +class Module: + name: str + folder: str + description: str + cxx_namespace: str + +class TargetType(enum.Enum): + PROGRAM = 0 + STATIC_LIBRARY = 1 + SHARED_LIBRARY = 2 + MISC = 3 + +class Target: + name: str + target_type: TargetType + builder = None + args: list = [] + kwargs: dict = {} + dependencies: list = [] + target = None + module: Module = None + +@dataclass(frozen=True) +class SPPInterface: + globals: dict + + @property + def env(self) -> SPPEnvironment: + return self.globals['env'] + + @property + def targets(self) -> list[Target]: + return self.env['SPP_TARGETS'] + +_spp: SPPInterface +def _init_interface(**kwargs) -> None: + global _spp + _spp = SPPInterface(**kwargs) + +def get_spp() -> SPPInterface: + return _spp diff --git a/util/__init__.py b/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/util/python_module/sppcmd/__init__.py b/util/python_module/sppcmd/__init__.py new file mode 100644 index 0000000..e730369 --- /dev/null +++ b/util/python_module/sppcmd/__init__.py @@ -0,0 +1,24 @@ +""" +Scons++ Command Line Interface +""" + +import argparse +import logging + +from .ccjson import make_ccjson_parser + +_STDOUT_LOG_FORMAT = '%(message)s' + +def run_spp_cmd() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('--verbose', '-v', action='store_true') + subparsers = parser.add_subparsers(required=True) + + make_ccjson_parser(subparsers) + + args = parser.parse_args() + + logging.basicConfig(format=_STDOUT_LOG_FORMAT, level=logging.DEBUG if args.verbose else logging.INFO) + args.handler(args) + + return 0 diff --git a/util/python_module/sppcmd/ccjson.py b/util/python_module/sppcmd/ccjson.py new file mode 100644 index 0000000..f63703c --- /dev/null +++ b/util/python_module/sppcmd/ccjson.py @@ -0,0 +1,18 @@ + +import argparse +from .common import exec_spp, get_config_cache, require_project_file + +def _cmd(args: argparse.Namespace) -> None: + require_project_file() + + build_type = args.build_type + if build_type == 'auto': + cache = get_config_cache() + build_type = cache.get('build_type', 'debug') + + exec_spp((f'--build_type={build_type}', '--unity=disable', 'compile_commands.json')) + +def make_ccjson_parser(subparsers) -> None: + parser : argparse.ArgumentParser = subparsers.add_parser('ccjson', help='Generate compile_commands.json') + parser.set_defaults(handler=_cmd) + parser.add_argument('--build_type', choices=('auto', 'debug', 'release_debug', 'release', 'profile'), default='auto') diff --git a/util/python_module/sppcmd/common.py b/util/python_module/sppcmd/common.py new file mode 100644 index 0000000..fa1af6a --- /dev/null +++ b/util/python_module/sppcmd/common.py @@ -0,0 +1,51 @@ + + +import json +import logging +from pathlib import Path +import shlex +import subprocess +import sys +from typing import Sequence + +_project_root = Path('.').absolute() + +def get_project_root() -> Path: + return _project_root + +def set_project_root(path: Path) -> None: + global _project_root + _project_root = path + +def get_config_cache() -> dict: + cache_file = get_project_root() / 'cache' / 'config_cache.json' + if not cache_file.exists(): + return {} + + try: + with cache_file.open('r') as f: + cache = json.load(f) + if not isinstance(cache, dict): + logging.warning('Config cache is not a dictionary, ignoring it.') + return {} + return cache + except Exception as e: + logging.error(f'Error while reading config cache: {e}.') + return {} + +def require_project_file() -> None: + if not (get_project_root() / 'SConstruct').exists(): + logging.error('This command has to be run inside an existing S++ project folder. Exiting.') + sys.exit(1) + +def exec_checked(args: Sequence[str], **kwargs) -> None: + logging.debug('exec_checked: "%s"', shlex.join(args)) + subprocess.run(args, stdout=sys.stdout, stderr=sys.stderr, check=True, **kwargs) + +def exec_get_output(args: Sequence[str], **kwargs) -> str: + logging.debug('exec_get_output: "%s"', shlex.join(args)) + return subprocess.run(args, text=True, check=True, capture_output=True, **kwargs).stdout + +def exec_spp(args: Sequence[str], **kwargs): + full_cmd = ('scons', '-s', '--disable_auto_update', *args) + exec_checked(full_cmd, **kwargs) diff --git a/util/spp_cmd.py b/util/spp_cmd.py new file mode 100755 index 0000000..92127cd --- /dev/null +++ b/util/spp_cmd.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 + +import os +import sys + +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'python_module')) + + from sppcmd import run_spp_cmd + sys.exit(run_spp_cmd())