Added command line tool and removed lib folder from gitignore.

This commit is contained in:
Patrick 2025-09-20 14:01:31 +02:00
parent 79366c9098
commit b9335a6247
9 changed files with 230 additions and 47 deletions

2
.gitignore vendored
View File

@ -17,7 +17,7 @@ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ # lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/

View File

@ -23,7 +23,7 @@ from SCons.Script import *
sys.path.append(os.path.join(Dir('.').abspath, 'lib')) 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()) _init_interface(globals=globals())
@ -46,12 +46,6 @@ _GCC_CPU_FEATURES_MAP = {
'avx2': '-mavx2' 'avx2': '-mavx2'
} }
class TargetType(enum.Enum):
PROGRAM = 0
STATIC_LIBRARY = 1
SHARED_LIBRARY = 2
MISC = 3
class _VersionSpec: class _VersionSpec:
minimum_version = None minimum_version = None
maximum_version = None maximum_version = None
@ -73,23 +67,6 @@ class _Dependency:
depdeps: list = [] depdeps: list = []
cook_result: dict = {} 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): def _find_recipe(env: Environment, recipe_name: str):
if recipe_name in env['SPP_RECIPES']: if recipe_name in env['SPP_RECIPES']:
return env['SPP_RECIPES'][recipe_name] 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) folder = _normalize_module_path(env, env.File(file).dir.abspath)
if folder is not None: # only include modules inside the source tree if folder is not None: # only include modules inside the source tree
dirname = os.path.basename(folder) dirname = os.path.basename(folder)
env.Append(SPP_MODULES = {folder: _Module( env.Append(SPP_MODULES = {folder: Module(
name=dirname, name=dirname,
folder=folder, folder=folder,
description='', description='',
@ -202,7 +179,7 @@ def _inject_dependency(dependency, kwargs: dict, add_sources: bool = True) -> No
_inject_list(kwargs, dependency.cook_result, 'LINKFLAGS') _inject_list(kwargs, dependency.cook_result, 'LINKFLAGS')
for depdep in dependency.depdeps: for depdep in dependency.depdeps:
_inject_dependency(depdep, kwargs) _inject_dependency(depdep, kwargs)
elif isinstance(dependency, _Target): elif isinstance(dependency, Target):
_inject_list(kwargs, dependency.kwargs, 'CPPPATH') _inject_list(kwargs, dependency.kwargs, 'CPPPATH')
_inject_list(kwargs, dependency.kwargs, 'CPPDEFINES') _inject_list(kwargs, dependency.kwargs, 'CPPDEFINES')
_inject_list(kwargs, dependency.kwargs, 'LIBPATH') _inject_list(kwargs, dependency.kwargs, 'LIBPATH')
@ -489,7 +466,7 @@ def _wrap_builder(builder, target_type: TargetType):
if 'source' in kwargs: if 'source' in kwargs:
kwargs['source'] = _fix_filearg(kwargs['source']) kwargs['source'] = _fix_filearg(kwargs['source'])
target = _Target() target = Target()
if 'name' in kwargs: if 'name' in kwargs:
target.name = kwargs['name'] target.name = kwargs['name']
else: else:
@ -518,7 +495,7 @@ def _wrap_builder(builder, target_type: TargetType):
def _wrap_default(default): def _wrap_default(default):
def _wrapped(env, arg): def _wrapped(env, arg):
if isinstance(arg, _Target): if isinstance(arg, Target):
env.Append(SPP_DEFAULT_TARGETS = [arg]) env.Append(SPP_DEFAULT_TARGETS = [arg])
elif isinstance(arg, dict) and '_target' in arg: elif isinstance(arg, dict) and '_target' in arg:
default(arg['_target']) default(arg['_target'])
@ -528,7 +505,7 @@ def _wrap_default(default):
def _wrap_depends(depends): def _wrap_depends(depends):
def _wrapped(env, dependant, dependency): 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)]) env.Append(SPP_TARGET_DEPENDENCIES = [(dependant, dependency, depends)])
return return
elif isinstance(dependant, dict) and '_target' in dependant: elif isinstance(dependant, dict) and '_target' in dependant:
@ -538,7 +515,7 @@ def _wrap_depends(depends):
depends(dependant, dependency) depends(dependant, dependency)
return _wrapped return _wrapped
def _build_target(target: _Target): def _build_target(target: Target):
for dependency in target.dependencies: for dependency in target.dependencies:
_inject_dependency(dependency, target.kwargs) _inject_dependency(dependency, target.kwargs)
if 'LIBS' in target.kwargs: if 'LIBS' in target.kwargs:
@ -548,7 +525,7 @@ def _build_target(target: _Target):
target.kwargs['LIBS'].remove(lib) target.kwargs['LIBS'].remove(lib)
target.kwargs['LIBS'].append(env.File(lib)) target.kwargs['LIBS'].append(env.File(lib))
pass pass
elif isinstance(lib, _Target): elif isinstance(lib, Target):
if not lib.target: if not lib.target:
_build_target(lib) _build_target(lib)
target.kwargs['LIBS'].remove(lib) target.kwargs['LIBS'].remove(lib)
@ -568,6 +545,7 @@ def _finalize(env: Environment):
_generate_project(generate_project) _generate_project(generate_project)
Exit(0) Exit(0)
_hook_pre_finalize.invoke()
version_requirements = {dep.name: { version_requirements = {dep.name: {
'min': dep.version_spec.minimum_version and _version_to_string(dep.version_spec.minimum_version), '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), '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']: for target in env['SPP_DEFAULT_TARGETS']:
env.Default(target.target) env.Default(target.target)
for dependant, dependency, depends in env['SPP_TARGET_DEPENDENCIES']: for dependant, dependency, depends in env['SPP_TARGET_DEPENDENCIES']:
if isinstance(dependant, _Target): if isinstance(dependant, Target):
dependant = dependant.target dependant = dependant.target
if isinstance(dependency, _Target): if isinstance(dependency, Target):
dependency = dependency.target dependency = dependency.target
depends(dependant, dependency) 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']: for target in env['SPP_TARGETS']:
if target.name == target_name: if target.name == target_name:
return target return target
@ -739,7 +719,7 @@ def _generate_project(project_type: str) -> None:
def _get_sources(target_dict: dict) -> list[str]: def _get_sources(target_dict: dict) -> list[str]:
target : _Target = target_dict['target'] target : Target = target_dict['target']
sources = target.kwargs.get('source') sources = target.kwargs.get('source')
return [str(pathlib.Path(source.abspath).relative_to(root_path)) for source in sources] 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_pre_environment = _Hook()
_hook_post_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: def _load_addon(modname: str, modpath: pathlib.Path) -> None:
_debug('addons', f'Loading addon {modname} from {modpath}.') _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(): if hasattr(module, 'available') and not module.available():
_debug('addons', f'Addon {modname} is not available and will not be loaded.') _debug('addons', f'Addon {modname} is not available and will not be loaded.')
return return
if hasattr(module, 'pre_environment'):
_hook_pre_environment.add_func(module.pre_environment) def _add_hook(func_name: str, hook: _Hook) -> None:
_debug('addons', f'Addon {modname} registered a pre_environment hook.') if hasattr(module, func_name):
if hasattr(module, 'post_environment'): hook.add_func(getattr(module, func_name))
_hook_post_environment.add_func(module.post_environment) _debug('addons', f'Addon {modname} registered a {func_name} hook.')
_debug('addons', f'Addon {modname} registered a post_environment 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: def _load_addons(folder: pathlib.Path) -> None:
_debug('addons', f'Loading addons from {folder}.') _debug('addons', f'Loading addons from {folder}.')
@ -1235,7 +1223,7 @@ env['SPP_DEFAULT_TARGETS'] = []
env['SPP_TARGET_DEPENDENCIES'] = [] env['SPP_TARGET_DEPENDENCIES'] = []
env['SPP_DEPENDENCIES'] = {} env['SPP_DEPENDENCIES'] = {}
env['SPP_RECIPES'] = {} 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['SPP_CPU_FEATURES'] = config.get('USE_CPU_FEATURES', [])
env['OBJSUFFIX'] = f".{env['BUILD_TYPE']}{env['OBJSUFFIX']}" env['OBJSUFFIX'] = f".{env['BUILD_TYPE']}{env['OBJSUFFIX']}"
@ -1437,6 +1425,8 @@ env.AddMethod(_find_target, 'FindTarget')
if hasattr(env, 'Gch'): if hasattr(env, 'Gch'):
env.AddMethod(_wrap_builder(env.Gch, TargetType.STATIC_LIBRARY), 'Gch') env.AddMethod(_wrap_builder(env.Gch, TargetType.STATIC_LIBRARY), 'Gch')
_hook_config_complete.invoke()
for addon_file in env.Glob('addons/old/*.py'): for addon_file in env.Glob('addons/old/*.py'):
env = SConscript(addon_file, exports = 'env') env = SConscript(addon_file, exports = 'env')

35
addons/config_cache.py Normal file
View File

@ -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}.')

55
lib/spp.py Normal file
View File

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

0
util/__init__.py Normal file
View File

View File

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

View File

@ -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')

View File

@ -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)

10
util/spp_cmd.py Executable file
View File

@ -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())