diff --git a/SConscript b/SConscript index 7a04d1d..1c65d41 100644 --- a/SConscript +++ b/SConscript @@ -1,26 +1,40 @@ import copy +import enum import os import psutil import sys import time -import SCons.Script -import SCons.Warnings + +class _VersionSpec: + minimum_version = None + maximum_version = None + + def __init__(self, minimum_version = None, maximum_version = None): + self.minimum_version = minimum_version + self.maximum_version = maximum_version + + def __str__(self): + return f'Min: {self.minimum_version}, Max: {self.maximum_version}' class _Dependency: name: str = '' - version: str = '' + version = None + version_spec: _VersionSpec + recipe = None + depdeps: list = [] class _Target: builder = None - target = None - source = None args: list = [] kwargs: dict = {} dependencies: list = [] + target = None -def _cook(env: Environment, recipe_name: str, *args, **kwargs): +def _find_recipe(env: Environment, recipe_name: str): + if recipe_name in env['SPP_RECIPES']: + return env['SPP_RECIPES'][recipe_name] import importlib.util source_file = None for folder in env['RECIPES_FOLDERS']: @@ -33,6 +47,11 @@ def _cook(env: Environment, recipe_name: str, *args, **kwargs): spec = importlib.util.spec_from_file_location(recipe_name, source_file) recipe = importlib.util.module_from_spec(spec) spec.loader.exec_module(recipe) + env['SPP_RECIPES'][recipe_name] = recipe + return recipe + +def _cook(env: Environment, recipe_name: str, *args, **kwargs): + recipe = _find_recipe(env, recipe_name) return recipe.cook(env, *args, **kwargs) def _module(env: Environment, file: str): @@ -63,6 +82,8 @@ def _inject_dependency(dependency, kwargs: dict, add_sources: bool = True) -> No if 'DEPENDENCIES' in dependency: for inner_dependency in dependency['DEPENDENCIES']: _inject_dependency(inner_dependency, kwargs, False) + elif isinstance(dependency, _Dependency): + pass def _rglob(env: Environment, root_path: str, pattern: str, **kwargs): result_nodes = [] @@ -102,23 +123,99 @@ def _error(env: Environment, message: str): print(message, file=sys.stderr) env.Exit(1) -def _build_action(target, source, env): - the_target = env['_target'] - the_target.builder.method(env=env, *the_target.args, **the_target.kwargs) +def _find_common_depenency_version(name: str, versionA: _VersionSpec, versionB: _VersionSpec) -> _VersionSpec: + result_version = _VersionSpec() + if versionA.minimum_version is not None: + if versionB.minimum_version is not None: + result_version.minimum_version = max(versionA.minimum_version, versionB.minimum_version) + else: + result_version.minimum_version = versionA.minimum_version + else: + result_version.minimum_version = versionB.minimum_version -_Builder = Builder(action=Action(_build_action, None)) + if versionA.maximum_version is not None: + if versionB.maximum_version is not None: + result_version.maximum_version = min(versionA.maximum_version, versionB.maximum_version) + else: + result_version.maximum_version = versionA.maximum_version + else: + result_version.maximum_version = versionB.maximum_version -def _add_dependency(name: str, version: str) -> _Dependency: + if result_version.minimum_version is not None and result_version.maximum_version is not None \ + and (result_version.minimum_version > result_version.maximum_version): + return None + return result_version + +def _parse_version_spec(version_spec: dict) -> _VersionSpec: + return _VersionSpec(version_spec.get('min'), version_spec.get('max')) + +def _can_add_dependency(env: Environment, name: str, version_spec: _VersionSpec) -> bool: + if name not in env['SPP_DEPENDENCIES']: + return True + dependency = env['SPP_DEPENDENCIES'][name] + common_version_spec = _find_common_depenency_version(name, dependency.version_spec, version_spec) + return common_version_spec is not None + +def _add_dependency(env: Environment, name: str, version_spec: _VersionSpec) -> _Dependency: + if name in env['SPP_DEPENDENCIES']: + dependency = env['SPP_DEPENDENCIES'][name] + common_version_spec = _find_common_depenency_version(name, dependency.version_spec, version_spec) + if common_version_spec is None: + raise Exception(f'Incompatible versions detected for {name}: {dependency.version_spec} and {version_spec}') + if dependency.version_spec != common_version_spec: + env['_SPP_DEPENDENCIES_OKAY'] = False + dependency.version_spec = common_version_spec + return dependency dependency = _Dependency() dependency.name = name - dependency.version = version + dependency.version_spec = version_spec + dependency.recipe = _find_recipe(env, name) + env['SPP_DEPENDENCIES'][name] = dependency + env['_SPP_DEPENDENCIES_OKAY'] = False return dependency +def _sort_versions(versions: list) -> None: + import functools + def _compare(left, right): + if left[0] != right[0]: + return right[0] - left[0] + elif left[1] != right[1]: + return right[1] - left[1] + else: + return right[2] - left[2] + versions.sort(key=functools.cmp_to_key(_compare)) + +def _version_matches(version, version_spec: _VersionSpec) -> bool: + if version_spec.minimum_version is not None and version < version_spec.minimum_version: + return False + if version_spec.maximum_version is not None and version > version_spec.maximum_version: + return False + return True + +def _find_version(env: Environment, dependency: _Dependency): + versions = dependency.recipe.versions(env, update=False) + _sort_versions(versions) + for version in versions: + if _version_matches(version, dependency.version_spec): + canadd = True + for depname, depspec in dependency.recipe.dependencies(env, version).items(): + if not _can_add_dependency(env, depname, _parse_version_spec(depspec)): + canadd = False + break + if canadd: + depdeps = [] + for depname, depspec in dependency.recipe.dependencies(env, version).items(): + depdeps.append(_add_dependency(env, depname, _parse_version_spec(depspec))) + dependency.version = version + dependency.depdeps = depdeps + return + raise Exception(f'Could not find a suitable version for dependency {dependency.name}.') + def _wrap_builder(builder, is_lib: bool = False): def _wrapped(env, dependencies = {}, *args, **kwargs): target_dependencies = [] - for name, version in dependencies.items(): - target_dependencies.append(_add_dependency(name, version)) + for name, version_spec in dependencies.items(): + target_dependencies.append(_add_dependency(env, name, _parse_version_spec(version_spec))) if 'CPPPATH' not in kwargs: kwargs['CPPPATH'] = copy.copy(env['CPPPATH']) @@ -134,22 +231,32 @@ def _wrap_builder(builder, is_lib: bool = False): if isinstance(lib, str) and os.path.isabs(lib): kwargs['LIBS'].remove(lib) kwargs['source'].append(lib) + if 'source' in kwargs: + source = kwargs['source'] + if not isinstance(source, list): + source = [source] + new_source = [] + for src in source: + if isinstance(src, str): + new_source.append(env.Entry(src)) + else: + new_source.append(src) + kwargs['source'] = new_source target = _Target() - target.target = kwargs.get('target', None) - target.source = kwargs.get('source', None) target.builder = builder target.args = args target.kwargs = kwargs target.dependencies = target_dependencies - # return _Builder(target=kwargs.get('target', None), source=kwargs.get('source', None), env=env, _target=target) - return builder(*args, **kwargs) + env.Append(SPP_TARGETS = [target]) + return target return _wrapped def _wrap_default(default): - print(default) def _wrapped(env, arg): - if isinstance(arg, dict) and '_target' in arg: + if isinstance(arg, _Target): + env.Append(SPP_DEFAULT_TARGETS = [arg]) + elif isinstance(arg, dict) and '_target' in arg: default(arg['_target']) else: default(arg) @@ -157,13 +264,28 @@ def _wrap_default(default): def _wrap_depends(depends): def _wrapped(env, dependant, dependency): - if isinstance(dependant, dict) and '_target' in dependant: + if isinstance(dependant, _Target) or isinstance(dependency, _Target): + env.Append(SPP_TARGET_DEPENDENCIES = [(dependant, dependency)]) + elif isinstance(dependant, dict) and '_target' in dependant: dependant = dependant['_target'] - if isinstance(dependency, dict) and '_target' in dependency: + elif isinstance(dependency, dict) and '_target' in dependency: dependency = dependency['_target'] depends(dependant, dependency) return _wrapped +def _finalize(env: Environment): + env['_SPP_DEPENDENCIES_OKAY'] = False + while not env['_SPP_DEPENDENCIES_OKAY']: + env['_SPP_DEPENDENCIES_OKAY'] = True + for dependency in list(env['SPP_DEPENDENCIES'].values()): + _find_version(env, dependency) + for target in env['SPP_TARGETS']: + for dependency in target.dependencies: + _inject_dependency(dependency, target.kwargs) + target.target = target.builder(*target.args, **target.kwargs) + for target in env['SPP_DEFAULT_TARGETS']: + env.Default(target.target) + def _get_fallback_cache_dir() -> str: return Dir('#cache').abspath @@ -333,6 +455,13 @@ env.Append(CPPPATH = []) env.Append(CPPDEFINES = []) env.Append(LINKFLAGS = []) +# init SPP environment variables +env['SPP_TARGETS'] = [] +env['SPP_DEFAULT_TARGETS'] = [] +env['SPP_TARGET_DEPENDENCIES'] = [] +env['SPP_DEPENDENCIES'] = {} +env['SPP_RECIPES'] = {} + # create the cache dir os.makedirs(env['CACHE_DIR'], exist_ok=True) cache_gitignore = f'{env["CACHE_DIR"]}/.gitignore' @@ -461,6 +590,7 @@ env.AddMethod(_wrap_builder(env.UnityLibrary, is_lib = True), 'UnityLibrary') env.AddMethod(_wrap_builder(env.UnityStaticLibrary, is_lib = True), 'UnityStaticLibrary') env.AddMethod(_wrap_builder(env.UnitySharedLibrary, is_lib = True), 'UnitySharedLibrary') env.AddMethod(_module, 'Module') +env.AddMethod(_finalize, 'Finalize') if hasattr(env, 'Gch'): env.AddMethod(_wrap_builder(env.Gch), 'Gch') @@ -473,30 +603,4 @@ if dump_env: print(env.Dump()) print('==== End Environment Dump =====') -_old_fn = SCons.Warnings.process_warn_strings - -import SCons.Util -class _FrameWrapper(SCons.Util.Proxy): - def __init__(self, subject): - super().__init__(subject) - - def __getattr__(self, name): - if name == 'retval': - print('YAY') - return super().__getattr__(name) - - -SCons.Script.call_stack[0] = _FrameWrapper(SCons.Script.call_stack[0]) - -print(SCons.Script.call_stack) -def _wrapped(*args, **kwargs): - for target in SCons.Script.BUILD_TARGETS: - if hasattr(target, 'abspath'): - print('Target: ', target.abspath) - else: - print('Target: ', target) - _old_fn(*args, **kwargs) - -SCons.Warnings.process_warn_strings = _wrapped - Return('env') diff --git a/addons/gitbranch.py b/addons/gitbranch.py index f4061a6..e399127 100644 --- a/addons/gitbranch.py +++ b/addons/gitbranch.py @@ -7,7 +7,7 @@ from SCons.Script import * Import('env') -def _gitbranch(env: Environment, repo_name: str, remote_url: str, git_ref: str = "main") -> dict: +def _clone(env: Environment, repo_name: str, remote_url: str): repo_dir = os.path.join(env['CLONE_DIR'], 'git', repo_name, '_bare') try: repo = Repo(repo_dir) @@ -16,6 +16,10 @@ def _gitbranch(env: Environment, repo_name: str, remote_url: str, git_ref: str = print(f'Initializing git repository at {repo_dir}.') repo = Repo.init(repo_dir, bare=True) origin = repo.create_remote('origin', remote_url) + return repo, origin + +def _gitbranch(env: Environment, repo_name: str, remote_url: str, git_ref: str = 'main') -> dict: + repo, origin = _clone(env, repo_name, remote_url) worktree_dir = os.path.join(env['CLONE_DIR'], 'git', repo_name, hashlib.shake_128(git_ref.encode('utf-8')).hexdigest(6)) # TODO: commit hash would be better, right? -> not if it's a branch! if not os.path.exists(worktree_dir): print(f'Checking out into {worktree_dir}.') @@ -34,6 +38,12 @@ def _gitbranch(env: Environment, repo_name: str, remote_url: str, git_ref: str = 'checkout_root': worktree_dir } +def _gittags(env: Environment, repo_name: str, remote_url: str, force_fetch: bool = False) -> 'list[str]': + repo, origin = _clone(env, repo_name, remote_url) + if force_fetch or env['UPDATE_REPOSITORIES']: + origin.fetch(tags=True) + return [t.name for t in repo.tags] env.AddMethod(_gitbranch, 'GitBranch') +env.AddMethod(_gittags, 'GitTags') Return('env') \ No newline at end of file diff --git a/recipes/libpng/recipe.py b/recipes/libpng/recipe.py index 0c9a353..751993a 100644 --- a/recipes/libpng/recipe.py +++ b/recipes/libpng/recipe.py @@ -1,6 +1,28 @@ +import re from SCons.Script import * +_REPO_NAME = 'libpng' +_REPO_URL = 'https://git.code.sf.net/p/libpng/code.git' +_TAG_PATTERN = re.compile(r'^v([0-9])+\.([0-9])+\.([0-9])$') + +def available(env: Environment) -> bool: + return hasattr(env, 'GitBranch') and hasattr(env, 'GitTags') + +def versions(env: Environment, update: bool = False) -> 'list[str]': + tags = env.GitTags(repo_name = _REPO_NAME, remote_url = _REPO_URL, force_fetch=update) + result = [] + for tag in tags: + match = _TAG_PATTERN.match(tag) + if match: + result.append((int(match.groups()[0]), int(match.groups()[1]), int(match.groups()[2]))) + return result + +def dependencies(env: Environment, version) -> 'list[dict]': + return { + 'zlib': {} + } + def cook(env: Environment, git_ref = 'master') -> dict: lib_z = env.Cook('zlib') repo = env.GitBranch(repo_name = 'libpng', remote_url = 'https://git.code.sf.net/p/libpng/code.git', git_ref = git_ref)