import copy import os def _cook(env: Environment, recipe_name: str, *args, **kwargs): import importlib.util source_file = None for folder in env['RECIPES_FOLDERS']: try_source_file = f'{folder.abspath}/{recipe_name}/recipe.py' if os.path.exists(try_source_file): source_file = try_source_file break if not source_file: raise Exception(f'Could not find recipe {recipe_name}.') spec = importlib.util.spec_from_file_location(recipe_name, source_file) recipe = importlib.util.module_from_spec(spec) spec.loader.exec_module(recipe) return recipe.cook(env, *args, **kwargs) def _parse_lib_conf(env: Environment, lib_conf: dict) -> None: env.Append(CPPPATH = lib_conf.get('CPPPATH', []), CPPDEFINES = lib_conf.get('CPPDEFINES', []), LIBPATH = lib_conf.get('LIBPATH', []), LIBS = lib_conf.get('LIBS', [])) def _inject_list(kwargs: dict, dependency: dict, list_name: str) -> None: if list_name not in dependency: return if list_name not in kwargs: kwargs[list_name] = [] kwargs[list_name].extend(dependency[list_name]) # TODO: eliminiate duplicates? def _inject_dependency(dependency, kwargs: dict) -> None: if isinstance(dependency, dict): _inject_list(kwargs, dependency, 'CPPPATH') _inject_list(kwargs, dependency, 'CPPDEFINES') _inject_list(kwargs, dependency, 'LIBPATH') _inject_list(kwargs, dependency, 'LIBS') if 'DEPENDENCIES' in dependency: for inner_dependency in dependency['DEPENDENCIES']: _inject_dependency(inner_dependency, kwargs) def _rglob(env: Environment, root_path: str, pattern: str, **kwargs): result_nodes = [] paths = [root_path] while paths: path = paths.pop() all_nodes = env.Glob(f'{path}/*', **kwargs) paths.extend(entry for entry in all_nodes if entry.isdir() or (entry.srcnode() and entry.srcnode().isdir())) # `srcnode()` must be used because `isdir()` doesn't work for entries in variant dirs which haven't been copied yet. result_nodes.extend(env.Glob(f'{path}/{pattern}', **kwargs)) return sorted(result_nodes) def _wrap_builder(builder, is_lib: bool = False): def _wrapped(env, dependencies = [], *args, **kwargs): if 'CPPPATH' not in kwargs: kwargs['CPPPATH'] = copy.copy(env['CPPPATH']) if 'CPPDEFINES' not in kwargs: kwargs['CPPDEFINES'] = copy.copy(env['CPPDEFINES']) if 'LIBPATH' not in kwargs: kwargs['LIBPATH'] = copy.copy(env['LIBPATH']) if 'LIBS' not in kwargs and 'LIBS' in env: kwargs['LIBS'] = copy.copy(env['LIBS']) for dependency in dependencies: _inject_dependency(dependency, kwargs) result = builder(*args, **kwargs) if is_lib: # generate a new libconf return { 'CPPPATH': kwargs.get('CPPPATH', []), 'CPPDEFINES': kwargs.get('CPPDEFINES', []), 'LIBPATH': kwargs.get('LIBPATH', []), 'LIBS': result + kwargs.get('LIBS', []), '_target': result } return result return _wrapped def _wrap_default(default): def _wrapped(env, arg): if isinstance(arg, dict) and '_target' in arg: default(arg['_target']) else: default(arg) return _wrapped def _get_fallback_cache_dir() -> str: return Dir('#cache').abspath def _find_system_cache_dir() -> str: if os.name == 'posix': if os.environ.get('XDG_CACHE_HOME'): return os.environ['XDG_CACHE_HOME'] elif os.environ.get('HOME'): return os.path.join(os.environ['HOME'], '.cache') elif os.name == 'nt': # TODO: just guessing return os.environ['LocalAppData'] # fallback return _get_fallback_cache_dir() Import('config') if not config.get('PROJECT_NAME'): config['PROJECT_NAME'] = 'PROJECT' if not config.get('CXX_STANDARD'): config['CXX_STANDARD'] = 'c++20' if not config.get('PREPROCESSOR_PREFIX'): config['PREPROCESSOR_PREFIX'] = config['PROJECT_NAME'].upper() # TODO: may be nicer? AddOption( '--build_type', dest = 'build_type', type = 'choice', choices = ('debug', 'release_debug', 'release', 'profile'), nargs = 1, action = 'store', default = 'debug' ) AddOption( '--unity', dest = 'unity_mode', type = 'choice', choices = ('enable', 'disable', 'stress'), nargs = 1, action = 'store', default = 'enable' ) AddOption( '--variant', dest = 'variant', nargs = 1, action = 'store' ) AddOption( '--asan', dest = 'enable_asan', action = 'store_true' ) AddOption( '--config_file', dest = 'config_file', nargs = 1, action = 'store', default = 'config.py' ) AddOption( '--compiler', dest = 'compiler', type = 'choice', choices = ('auto', 'gcc', 'clang', 'msvc'), nargs = 1, action = 'store', default = 'auto' ) AddOption( '--update_repositories', dest = 'update_repositories', action = 'store_true' ) AddOption( '--dump_env', dest = 'dump_env', action = 'store_true' ) build_type = GetOption('build_type') unity_mode = GetOption('unity_mode') variant = GetOption('variant') enable_asan = GetOption('enable_asan') config_file = GetOption('config_file') compiler = GetOption('compiler') update_repositories = GetOption('update_repositories') dump_env = GetOption('dump_env') default_CC = { 'gcc': 'gcc', 'clang': 'clang', 'msvc': 'cl.exe' }.get(compiler, None) default_CXX = { 'gcc': 'g++', 'clang': 'clang++', 'msvc': 'cl.exe' }.get(compiler, None) if not os.path.isabs(config_file): config_file = os.path.join(Dir('#').abspath, config_file) vars = Variables(config_file) vars.Add('CC', 'The C Compiler', default_CC) vars.Add('CXX', 'The C++ Compiler', default_CXX) vars.Add('LINK', 'The Linker') vars.Add('CCFLAGS', 'C/C++ Compiler Flags') vars.Add('LINKFLAGS', 'Linker Flags') vars.Add('PYTHON', 'Python Executable', 'python') env = Environment(tools = ['default', 'compilation_db', 'unity_build'], variables = vars) env['RECIPES_FOLDERS'] = [Dir('recipes')] env['SYSTEM_CACHE_DIR'] = os.path.join(_find_system_cache_dir(), 'spp_cache') env['CLONE_DIR'] = os.path.join(env['SYSTEM_CACHE_DIR'], 'cloned') env['UPDATE_REPOSITORIES'] = update_repositories print(f'Detected system cache directory: {env["SYSTEM_CACHE_DIR"]}') try: os.makedirs(env['SYSTEM_CACHE_DIR'], exist_ok=True) except: env['SYSTEM_CACHE_DIR'] = os.path.join(_get_fallback_cache_dir(), 'spp_cache') env['CLONE_DIR'] = os.path.join(env['SYSTEM_CACHE_DIR'], 'cloned') print(f'Creating spp cache dir failed, using fallback: {env["SYSTEM_CACHE_DIR"]}.') os.makedirs(env['SYSTEM_CACHE_DIR'], exist_ok=True) # no more safeguards! # allow compiling to variant directories (each gets their own bin/lib/cache dirs) if variant: env['BIN_DIR'] = Dir(f'#bin_{variant}').abspath env['LIB_DIR'] = Dir(f'#lib_{variant}').abspath env['CACHE_DIR'] = Dir(f'#cache_{variant}').abspath env['VARIANT_DIR'] = f'{env["CACHE_DIR"]}/variant' env.Append(CPPDEFINES = [f'{config["PREPROCESSOR_PREFIX"]}_VARIANT={variant}']) else: env['VARIANT_DIR'] = None comp_db = env.CompilationDatabase(target = '#compile_commands.json') Default(comp_db) env['BIN_DIR'] = Dir('#bin').abspath env['LIB_DIR'] = Dir('#lib').abspath env['CACHE_DIR'] = Dir(f'#cache').abspath env['UNITY_CACHE_DIR'] = Dir(f'{env["CACHE_DIR"]}/unity') env['BUILD_TYPE'] = build_type env.Append(LIBPATH = [env['LIB_DIR']]) # to allow submodules to link to each other without hassle # make sure these are all defined in case someone wants to use/copy them env.Append(CCFLAGS = []) env.Append(CXXFLAGS = []) env.Append(CPPPATH = []) env.Append(CPPDEFINES = []) env.Append(LINKFLAGS = []) # create the cache dir os.makedirs(env['CACHE_DIR'], exist_ok=True) cache_gitignore = f'{env["CACHE_DIR"]}/.gitignore' if not os.path.exists(cache_gitignore): with open(cache_gitignore, 'w') as f: f.write('*\n') # create the clone and system cache dirs os.makedirs(env['CLONE_DIR'], exist_ok=True) # try to detect what compiler we are using compiler_exe = os.path.basename(env['CC']) if 'gcc' in compiler_exe: env['COMPILER_FAMILY'] = 'gcc' elif 'clang' in compiler_exe: env['COMPILER_FAMILY'] = 'clang' elif 'cl' in compiler_exe: env['COMPILER_FAMILY'] = 'cl' else: env['COMPILER_FAMILY'] = 'unknown' # setup unity build depending on mode if unity_mode == 'disable': env['UNITY_DISABLE'] = True elif unity_mode == 'stress': # compile everything in one single file to stress test the unity build env['UNITY_MAX_SOURCES'] = 100000 # I'll hopefully never reach this env['UNITY_MIN_FILES'] = 1 # setup compiler specific options if env['COMPILER_FAMILY'] == 'gcc' or env['COMPILER_FAMILY'] == 'clang': env.Append(CCFLAGS = ['-Wall', '-Wextra', '-Werror', '-Wstrict-aliasing', '-pedantic']) env.Append(CXXFLAGS = [f'-std={config["CXX_STANDARD"]}']) if build_type != 'release': env.Append(LINKFLAGS = [f'-Wl,-rpath,{env["LIB_DIR"]}']) env['LINKCOM'] = env['LINKCOM'].replace('$_LIBFLAGS', '-Wl,--start-group $_LIBFLAGS -Wl,--end-group') if env['COMPILER_FAMILY'] == 'gcc': # GCC complains about missing initializer for "" that doesn't exist :/ # also GCC complains about some (compiler generated) fields in coroutines not having any linkage # also -Wdangling-reference seems to produce a lot of false positives # also -Wmaybe-uninitialized seems to produce false positives (or a bug in the standard library?) env.Append(CCFLAGS = ['-Wno-missing-field-initializers', '-Wno-subobject-linkage', '-Wno-dangling-reference', '-Wno-maybe-uninitialized']) else: # no-gnu-anonymous-struct - we don't care env.Append(CCFLAGS = ['-Wno-gnu-anonymous-struct']) if build_type == 'debug': env.Append(CCFLAGS = ['-g', '-O0'], CPPDEFINES = ['_GLIBCXX_DEBUG']) elif build_type == 'release_debug' or build_type == 'profile': env.Append(CCFLAGS = ['-Wno-unused-variable', '-Wno-unused-parameter', '-Wno-unused-but-set-variable', '-Wno-unused-local-typedef', '-Wno-unused-local-typedefs', '-g', '-O2'], CPPDEFINES = [f'{config["PREPROCESSOR_PREFIX"]}_RELEASE', 'NDEBUG']) if build_type == 'profile': if env['COMPILER_FAMILY'] == 'gcc': env.Append(CPPDEFINES = [f'{config["PREPROCESSOR_PREFIX"]}_GCC_INSTRUMENTING=1']) env.Append(CCFLAGS = ['-finstrument-functions']) env.Append(LINKFLAGS = ['-rdynamic']) elif build_type == 'release': env.Append(CCFLAGS = ['-Wno-unused-variable', '-Wno-unused-parameter', '-Wno-unused-but-set-variable', '-Wno-unused-local-typedef', '-Wno-unused-local-typedefs', '-O2'], CPPDEFINES = [f'{config["PREPROCESSOR_PREFIX"]}_RELEASE', 'NDEBUG']) if enable_asan: env.Append(CCFLAGS = ['-fsanitize=address', '-fno-omit-frame-pointer']) env.Append(LINKFLAGS = ['-fsanitize=address']) elif env['COMPILER_FAMILY'] == 'cl': # C4201: nonstandard extension used : nameless struct/union - I use it and want to continue using it # C4127: conditional expression is constant - some libs (CRC, format) don't compile with this enabled # TODO: fix? env.Append(CCFLAGS = ['/W4', '/WX', '/wd4201', '/wd4127', f'/std:{config["CXX_STANDARD"]}', '/permissive-', '/EHsc', '/FS', '/Zc:char8_t']) env.Append(CPPDEFINES = ['_CRT_SECURE_NO_WARNINGS']) # I'd like to not use MSVC specific versions of functions because they are "safer" ... if build_type == 'debug': env.Append(CCFLAGS = ['/Od', '/Zi'], LINKFLAGS = ' /DEBUG') elif build_type == 'release_debug' or build_type == 'profile': env.Append(CCFLAGS = ['/O2', '/Zi'], LINKFLAGS = ' /DEBUG') else: env.Append(CCFLAGS = ['/O2']) if env['COMPILER_FAMILY'] == 'gcc': env.Append(CCFLAGS = ['-Wno-volatile']) elif env['COMPILER_FAMILY'] == 'clang': env.Append(CCFLAGS = ['-Wno-deprecated-volatile', '-Wno-nested-anon-types', '-Wno-unknown-warning-option']) env.AddMethod(_cook, 'Cook') env.AddMethod(_parse_lib_conf, 'ParseLibConf') env.AddMethod(_rglob, 'RGlob') env.AddMethod(_wrap_builder(env.Library, is_lib = True), 'Library') env.AddMethod(_wrap_builder(env.StaticLibrary, is_lib = True), 'StaticLibrary') env.AddMethod(_wrap_builder(env.SharedLibrary, is_lib = True), 'SharedLibrary') env.AddMethod(_wrap_builder(env.Program), 'Program') env.AddMethod(_wrap_default(env.Default), 'Default') env.AddMethod(_wrap_builder(env.UnityProgram), 'UnityProgram') 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') if dump_env: print('==== Begin Environment Dump =====') print(env.Dump()) print('==== End Environment Dump =====') Return('env')