diff --git a/SConscript b/SConscript index 7a5750a..b652196 100644 --- a/SConscript +++ b/SConscript @@ -1,5 +1,6 @@ import copy +import glob import json import os import psutil @@ -108,12 +109,33 @@ def _rglob(env: Environment, root_path: str, pattern: str, **kwargs): result_nodes.extend(env.Glob(f'{path}/{pattern}', **kwargs)) return sorted(result_nodes) +def _safe_eval(condition: str, locals={}): + return eval(condition, { + '__builtins__': { + 'abs': abs, 'all': all, 'any': any, 'ascii': ascii, 'bin': bin, 'bool': bool, 'chr': chr, 'complex': complex, + 'dict': dict, 'divmod': divmod, 'enumerate': enumerate, 'filter': filter, 'float': float, 'format': format, + 'hasattr': hasattr, 'hash': hash, 'hex': hex, 'id': id, 'int': int, 'isinstance': isinstance, + 'issubclass': issubclass, 'len': len, 'list': list, 'map': map, 'max': max, 'min': min, 'next': next, + 'oct': oct, 'ord': ord, 'pow': pow, 'range': range, 'reversed': reversed, 'round': round, 'set': set, + 'slice': slice, 'sorted': sorted, 'str': str, 'sum': sum, 'tuple': tuple, 'type': type, 'zip': zip + } + }, locals) + def _deps_from_json(env: Environment, deps: dict) -> dict: - for _, dep in deps.items(): + to_remove = [] + for key, dep in deps.items(): + if 'condition' in dep: + if not _safe_eval(dep['condition'], { + 'compiler_family': env['COMPILER_FAMILY'] + }): + to_remove.append(key) + continue if 'min' in dep and isinstance(dep['min'], list): dep['min'] = tuple(dep['min']) if 'max' in dep and isinstance(dep['max'], list): dep['max'] = tuple(dep['max']) + for key in to_remove: + del deps[key] return deps def _make_interface(env: Environment, dependencies: list = []): @@ -126,21 +148,36 @@ def _make_interface(env: Environment, dependencies: list = []): } def _lib_filename(env: Environment, name: str, type: str = 'static') -> str: - # TODO: windows - ext = { - 'static': 'a', - 'shared': 'so' - }[type] - return f'lib{name}.{ext}' + if os.name == 'posix': + ext = { + 'static': 'a', + 'shared': 'so' + }[type] + return f'lib{name}.{ext}' + elif os.name == 'nt': + ext = { + 'static': 'lib', + 'shared': 'dll' + }[type] + return f'{name}.{ext}' + else: + raise Exception('What OS is this?') -def _find_lib(env: Environment, name: str, paths: 'list[str]', type : str = 'static', allow_fail: bool = False): +def _find_lib(env: Environment, name: str, paths: 'list[str]', type : str = 'static', allow_fail: bool = False, use_glob: bool = False): + fname = _lib_filename(env, name, type) for path in paths: - lib_path = os.path.join(path, _lib_filename(env, name, type)) - if os.path.exists(lib_path): + lib_path = os.path.join(path, fname) + if use_glob: + files = glob.glob(lib_path) + if len(files) == 1: + return files[0] + elif len(files) > 1: + raise Exception(f'Multiple candidates found for library with name {name} in paths: "{", ".join(paths)}" with name: "{", ".join(files)}".') + elif os.path.exists(lib_path): return lib_path if allow_fail: return None - raise Exception(f'Could not find library with name {name} in paths: "{",".join(paths)}".') + raise Exception(f'Could not find library with name {name} in paths: "{", ".join(paths)}" with name: "{fname}".') def _error(env: Environment, message: str): print(message, file=sys.stderr) @@ -216,22 +253,25 @@ def _version_matches(version, version_spec: _VersionSpec) -> bool: 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 update in (False, True): + versions = dependency.recipe.versions(env, update=update) + _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(): - depdeps.append(_add_dependency(env, depname, _parse_version_spec(depspec))) - dependency.version = version - dependency.depdeps = depdeps - return + 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 + print(f'Available versions: \n{versions}') + print(f'Required version: {dependency.version_spec}') raise Exception(f'Could not find a suitable version for dependency {dependency.name}.') def _wrap_builder(builder, is_lib: bool = False): @@ -466,6 +506,10 @@ 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['DOWNLOAD_DIR'] = os.path.join(env['SYSTEM_CACHE_DIR'], 'downloaded') env['UPDATE_REPOSITORIES'] = update_repositories +env['CXX_STANDARD'] = config['CXX_STANDARD'] # make it available to everyone +env['DEPS_CFLAGS'] = [] +env['DEPS_CXXFLAGS'] = [] +env['DEPS_LINKFLAGS'] = [] print(f'Detected system cache directory: {env["SYSTEM_CACHE_DIR"]}') try: @@ -580,12 +624,12 @@ if env['COMPILER_FAMILY'] == 'gcc' or env['COMPILER_FAMILY'] == 'clang': # -Wtautological-compare triggers in libfmt and doesn't seem too useful anyway env.Append(CCFLAGS = ['-Wno-missing-field-initializers', '-Wno-maybe-uninitialized']) env.Append(CXXFLAGS = ['-Wno-subobject-linkage', '-Wno-dangling-reference', '-Wno-init-list-lifetime', '-Wno-tautological-compare']) - 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']) + env.Append(DEPS_CXXFLAGS = ['-D_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': @@ -600,20 +644,35 @@ if env['COMPILER_FAMILY'] == 'gcc' or env['COMPILER_FAMILY'] == 'clang': if enable_asan: env.Append(CCFLAGS = ['-fsanitize=address', '-fno-omit-frame-pointer']) env.Append(LINKFLAGS = ['-fsanitize=address']) + env.Append(DEPS_CXXFLAGS = ['-fsanitize=address', '-fno-omit-frame-pointer']) + env.Append(DEPS_LINKFLAGS = ['-fsanitize=address']) elif env['COMPILER_FAMILY'] == 'cl': + cxx_version_name = { + 'c++14': 'c++14', + 'c++17': 'c++17', + 'c++20': 'c++20', + 'c++23': 'c++latest', + 'c++26': 'c++latest' + }.get(env['CXX_STANDARD'], 'c++14') # default to C++14 for older versions # 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? # C4702: unreachable code, issued after MIJIN_FATAL macro # C4251: missing dll-interface of some std types, yaml-cpp doesn't compile with this enabled # C4275: same as above - env.Append(CCFLAGS = ['/W4', '/WX', '/wd4201', '/wd4127', '/wd4702', '/wd4251', '/wd4275', '/bigobj', f'/std:{config["CXX_STANDARD"]}', '/permissive-', '/EHsc', '/FS', '/Zc:char8_t']) + env.Append(CCFLAGS = ['/W4', '/WX', '/wd4201', '/wd4127', '/wd4702', '/wd4251', '/wd4275', '/bigobj', '/vmg', + f'/std:{cxx_version_name}', '/permissive-', '/EHsc', '/FS', '/Zc:char8_t', '/utf-8']) env.Append(CPPDEFINES = ['_CRT_SECURE_NO_WARNINGS']) # I'd like to not use MSVC specific versions of functions because they are "safer" ... + env.Append(DEPS_CXXFLAGS = ['/EHsc', '/Zc:char8_t', '/utf-8', '/vmg']) if build_type == 'debug': env.Append(CCFLAGS = ['/Od', '/Zi', '/MDd'], LINKFLAGS = ' /DEBUG') env.Append(CPPDEFINES = ['_DEBUG', '_ITERATOR_DEBUG_LEVEL=2']) + env.Append(DEPS_CXXFLAGS = ['/MDd', '/Zi', '/D_DEBUG', '/D_ITERATOR_DEBUG_LEVEL=2']) + env.Append(DEPS_LINKFLAGS = ['/DEBUG']) elif build_type == 'release_debug' or build_type == 'profile': env.Append(CCFLAGS = ['/O2', '/Zi'], LINKFLAGS = ' /DEBUG') + env.Append(DEPS_CXXFLAGS = ['/Zi']) + env.Append(DEPS_LINKFLAGS = ['/DEBUG']) else: env.Append(CCFLAGS = ['/O2']) diff --git a/addons/cmake_project.py b/addons/cmake_project.py index e5cb7ab..a801178 100644 --- a/addons/cmake_project.py +++ b/addons/cmake_project.py @@ -6,7 +6,7 @@ import shutil from SCons.Script import * _BUILT_STAMPFILE = '.spp_built' -_VERSION = 0 # bump if you change how the projects are build to trigger a clean build +_VERSION = 2 # bump if you change how the projects are build to trigger a clean build Import('env') @@ -14,19 +14,31 @@ def cmd_quote(s: str) -> str: escaped = s.replace('\\', '\\\\') return f'"{escaped}"' -def _generate_cmake_c_flags(dependencies: 'list[dict]') -> str: - parts = [] +def _generate_cmake_c_flags(env, dependencies: 'list[dict]') -> str: + parts = env['DEPS_CFLAGS'].copy() for dependency in dependencies: for path in dependency.get('CPPPATH', []): - parts.append(cmd_quote(f'-I{path}')) - return ' '.join(parts) + parts.append(f'-I{path}') + return cmd_quote(' '.join(parts)) -def _generate_cmake_cxx_flags(dependencies: 'list[dict]') -> str: - parts = [] +def _generate_cmake_cxx_flags(env, dependencies: 'list[dict]') -> str: + parts = env['DEPS_CXXFLAGS'].copy() for dependency in dependencies: for path in dependency.get('CPPPATH', []): - parts.append(cmd_quote(f'-I{path}')) - return ' '.join(parts) + parts.append(f'-I{path}') + return cmd_quote(' '.join(parts)) + +def _get_cmake_cxx_standard(env: Environment) -> str: + return env['CXX_STANDARD'][3:] # we use "C++XX", CMake just "XX" + +def _generate_cmake_args(env: Environment, dependencies: 'list[dict]') -> 'list[str]': + args = [f'-DCMAKE_C_FLAGS={_generate_cmake_c_flags(env, dependencies)}', + f'-DCMAKE_CXX_FLAGS={_generate_cmake_cxx_flags(env, dependencies)}', + f'-DCMAKE_CXX_STANDARD={_get_cmake_cxx_standard(env)}'] + for dependency in dependencies: + for name, value in dependency.get('CMAKE_VARS', {}).items(): + args.append(f'-D{name}={cmd_quote(value)}') + return args def _calc_version_hash(dependencies: 'list[dict]') -> str: return json.dumps({ @@ -63,14 +75,14 @@ def _cmake_project(env: Environment, project_root: str, generate_args: 'list[str 'profile': 'RelWithDebInfo' }.get(env['BUILD_TYPE'], 'RelWithDebInfo') def run_cmd(args): - env.Execute(' '.join([str(s) for s in args])) + if env.Execute(' '.join([str(s) for s in args])): + Exit(1) # TODO: is this a problem? # environ = os.environ.copy() # environ['CXXFLAGS'] = ' '.join(f'-D{define}' for define in env['CPPDEFINES']) # TODO: who cares about windows? run_cmd(['cmake', '-G', 'Ninja', '-B', build_dir, f'-DCMAKE_BUILD_TYPE={build_type}', f'-DCMAKE_INSTALL_PREFIX={cmd_quote(install_dir)}', '-DBUILD_TESTING=OFF', - f'-DCMAKE_C_FLAGS={_generate_cmake_c_flags(dependencies)}', - f'-DCMAKE_CXX_FLAGS={_generate_cmake_cxx_flags(dependencies)}', *generate_args, project_root]) + *_generate_cmake_args(env, dependencies), *generate_args, project_root]) run_cmd(['cmake', '--build', *build_args, cmd_quote(build_dir)]) run_cmd(['cmake', '--install', *install_args, cmd_quote(build_dir)]) diff --git a/recipes/boost/recipe.py b/recipes/boost/recipe.py index 6f86201..002b727 100644 --- a/recipes/boost/recipe.py +++ b/recipes/boost/recipe.py @@ -1,6 +1,7 @@ import json +import os import re import requests from SCons.Script import * @@ -20,11 +21,15 @@ def versions(env: Environment, update: bool = False): continue result.append((int(match.groups()[0]), int(match.groups()[1]), int(match.groups()[2]))) with open(versions_file, 'w') as f: - json.dump(versions, f) + json.dump(result, f) return result else: - with open(versions_file, 'r') as f: - return [tuple(v) for v in json.load(f)] + try: + with open(versions_file, 'r') as f: + return [tuple(v) for v in json.load(f)] + except: + print('boost_versions.json is empty or broken, redownloading.') + return versions(env, update=True) def dependencies(env: Environment, version) -> 'dict': return {} @@ -50,7 +55,12 @@ def cook(env: Environment, version) -> dict: libs.append(fname) else: for lib in set(env['BOOST_LIBS']): - libs.append(env.FindLib(f'boost_{lib}', paths=build_result['LIBPATH'])) + if os.name == 'posix': + libs.append(env.FindLib(f'boost_{lib}', paths=build_result['LIBPATH'])) + elif os.name == 'nt': + libs.append(env.FindLib(f'libboost_{lib}-*', paths=build_result['LIBPATH'], use_glob=True)) + else: + raise Exception('Boost not supported on this platform.') return { 'CPPPATH': build_result['CPPPATH'], 'LIBS': libs diff --git a/recipes/libpng/recipe.py b/recipes/libpng/recipe.py index 8f87aa7..57b020d 100644 --- a/recipes/libpng/recipe.py +++ b/recipes/libpng/recipe.py @@ -1,14 +1,25 @@ +import os import re from SCons.Script import * +def _build_lib_name(env: Environment) -> str: + if os.name == 'posix': + return { + 'debug': 'png16d' + }.get(env['BUILD_TYPE'], 'png16') + elif os.name == 'nt': + return { + 'debug': 'libpng16_staticd' + }.get(env['BUILD_TYPE'], 'libpng16_static') + else: + raise Exception('libpng is not supported yet on this OS') + def _git_cook(env: Environment, repo: dict) -> dict: lib_zlib = env.Cook('zlib') checkout_root = repo['checkout_root'] build_result = env.CMakeProject(checkout_root, dependencies = [lib_zlib]) - lib_name = { - 'debug': 'png16d' - }.get(env['BUILD_TYPE'], 'png16') + lib_name = _build_lib_name(env) return { 'CPPPATH': build_result['CPPPATH'], 'LIBS': [env.FindLib(lib_name, paths=build_result['LIBPATH'])] diff --git a/recipes/mikktspace/recipe.py b/recipes/mikktspace/recipe.py index aa62cf8..c4826bb 100644 --- a/recipes/mikktspace/recipe.py +++ b/recipes/mikktspace/recipe.py @@ -11,7 +11,11 @@ def dependencies(env: Environment, version) -> 'dict': def cook(env: Environment, version) -> dict: repo = env.GitBranch(repo_name = 'mikktspace', remote_url = 'https://github.com/mmikk/MikkTSpace.git', git_ref = 'master') checkout_root = repo['checkout_root'] + ccflags = env['CCFLAGS'].copy() + if env['COMPILER_FAMILY'] == 'cl': + ccflags.append('/wd4456') lib_mikktspace = env.StaticLibrary( + CCFLAGS = ccflags, CPPPATH = [checkout_root], target = env['LIB_DIR'] + '/mikktspace', source = [os.path.join(repo['checkout_root'], 'mikktspace.c')] diff --git a/recipes/zlib/recipe.py b/recipes/zlib/recipe.py index c4d7ca8..1a849c1 100644 --- a/recipes/zlib/recipe.py +++ b/recipes/zlib/recipe.py @@ -1,4 +1,5 @@ +import os import re from SCons.Script import * @@ -6,6 +7,18 @@ _REPO_NAME = 'zlib' _REPO_URL = 'https://github.com/madler/zlib.git' _TAG_PATTERN = re.compile(r'^v([0-9]+)\.([0-9]+)(?:\.([0-9]+))?$') +def _build_lib_name(env: Environment) -> str: + if os.name == 'posix': + return { + 'debug': 'zd' + }.get(env['BUILD_TYPE'], 'z') + elif os.name == 'nt': + return { + 'debug': 'zlibstaticd' + }.get(env['BUILD_TYPE'], 'zlibstatic') + else: + raise Exception('libpng is not supported yet on this OS') + def versions(env: Environment, update: bool = False): tags = env.GitTags(repo_name = _REPO_NAME, remote_url = _REPO_URL, force_fetch=update) result = [] @@ -25,7 +38,14 @@ def cook(env: Environment, version) -> dict: repo = env.GitBranch(repo_name = _REPO_NAME, remote_url = _REPO_URL, git_ref = git_ref) checkout_root = repo['checkout_root'] build_result = env.CMakeProject(project_root=checkout_root) + include_dir = os.path.join(build_result['install_dir'], 'include') + lib_name = _build_lib_name(env) + lib_file = env.FindLib(lib_name, paths=build_result['LIBPATH']) return { - 'CPPPATH': [os.path.join(build_result['install_dir'], 'include')], - 'LIBS': [env.FindLib('z', paths=build_result['LIBPATH'])] + 'CPPPATH': [include_dir], + 'LIBS': [lib_file], + 'CMAKE_VARS': { + 'ZLIB_LIBRARY': lib_file, + 'ZLIB_INCLUDE_DIR': include_dir + } }