From ef469e126854a7c2e1293ac008b2712d0eafad8e Mon Sep 17 00:00:00 2001 From: Justin Nolan Date: Tue, 7 Jul 2026 09:32:41 +0200 Subject: [PATCH] CMake support scripts --- .pre-commit-config.yaml | 7 - README.md | 50 ++- SConstruct | 375 ---------------- build/__init__.py | 0 build/author_info.py | 48 -- build/cache.py | 127 ------ build/common_compiler_flags.py | 145 ------ build/git_info.py | 131 ------ build/glob_recursive.py | 51 --- build/license_info.py | 167 ------- build/option_handler.py | 40 -- build/pch.py | 25 -- cmake/OpenVicCodegen.cmake | 146 ++++++ cmake/OpenVicDeps.cmake | 88 ++++ cmake/OpenVicFlags.cmake | 87 ++++ cmake/OpenVicScripts.cmake | 18 + gen/codegen.py | 781 +++++++++++++++++++++++++++++++++ gen/commit_info.gen.hpp.in | 13 + pyproject.toml | 7 - tools/linux.py | 94 ---- tools/macos.py | 105 ----- tools/macos_osxcross.py | 29 -- tools/my_spawn.py | 53 --- tools/targets.py | 126 ------ tools/windows.py | 233 ---------- 25 files changed, 1171 insertions(+), 1775 deletions(-) delete mode 100644 SConstruct delete mode 100644 build/__init__.py delete mode 100644 build/author_info.py delete mode 100644 build/cache.py delete mode 100644 build/common_compiler_flags.py delete mode 100644 build/git_info.py delete mode 100644 build/glob_recursive.py delete mode 100644 build/license_info.py delete mode 100644 build/option_handler.py delete mode 100644 build/pch.py create mode 100644 cmake/OpenVicCodegen.cmake create mode 100644 cmake/OpenVicDeps.cmake create mode 100644 cmake/OpenVicFlags.cmake create mode 100644 cmake/OpenVicScripts.cmake create mode 100644 gen/codegen.py create mode 100644 gen/commit_info.gen.hpp.in delete mode 100644 tools/linux.py delete mode 100644 tools/macos.py delete mode 100644 tools/macos_osxcross.py delete mode 100644 tools/my_spawn.py delete mode 100644 tools/targets.py delete mode 100644 tools/windows.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 13cd26c..d730549 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,6 @@ default_language_version: python: python3 -exclude: | - (?x)^( - tools/.*py| - build/cache.py| - build/common_compiler_flags.py - ) - repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.7.3 diff --git a/README.md b/README.md index d124dcc..3363c4e 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,39 @@ # OpenVicProject/scripts -Common Scons scripts repo for the [OpenVicProject repos](https://github.com/OpenVicProject) - -## Required -* [scons](https://scons.org/) - -## Usage -1. Call `env.SetupOptions()` and use the return value to add your options: - ```py - opts = env.SetupOptions() - opts.Add(BoolVariable("example", "Is an example", false)) - ``` -2. When options are finished call `env.FinalizeOptions()` then setup your scons script using env. + +Shared CMake helpers for the [OpenVicProject repos](https://github.com/OpenVicProject) +(OpenVic, openvic-simulation, openvic-dataloader, lexy-vdf). + + +Each consuming repo fetches this repo as a **pinned FetchContent tarball** + +```cmake +if(NOT COMMAND openvic_setup_base_flags) + include(FetchContent) + FetchContent_Declare( + openvic_scripts + URL "https://github.com/OpenVicProject/scripts/archive/.tar.gz" + URL_HASH SHA256= + ) + FetchContent_MakeAvailable(openvic_scripts) + include("${openvic_scripts_SOURCE_DIR}/cmake/OpenVicScripts.cmake") +endif() +``` + +## Bumping the pin in a consuming repo + +After a change lands on `master` here: + +1. Note the new commit SHA. +2. Compute the tarball hash: + ```sh + curl -sL https://github.com/OpenVicProject/scripts/archive/.tar.gz | sha256sum + ``` +3. In the consumer's root `CMakeLists.txt` bootstrap block, update the `URL` + SHA and the `URL_HASH SHA256=` value. + +To test uncommitted changes to this repo from a consumer, configure the +consumer with: + +```sh +cmake --preset -DFETCHCONTENT_SOURCE_DIR_OPENVIC_SCRIPTS= +``` diff --git a/SConstruct b/SConstruct deleted file mode 100644 index 8f766ab..0000000 --- a/SConstruct +++ /dev/null @@ -1,375 +0,0 @@ -#!/usr/bin/env python - -# This file is heavily based on https://github.com/godotengine/godot-cpp/blob/df5b1a9a692b0d972f5ac3c853371594cdec420b/SConstruct and https://github.com/godotengine/godot-cpp/blob/98ea2f60bb3846d6ae410d8936137d1b099cd50b/tools/godotcpp.py -import os -import platform -import sys -from typing import List, Union - -import SCons - -_SCRIPTS_DIR = Dir(".").abspath -if _SCRIPTS_DIR not in sys.path: - sys.path.insert(0, _SCRIPTS_DIR) - -# Local -from build.option_handler import OptionsClass -from build.glob_recursive import GlobRecursive, GlobRecursiveVariant -from build.git_info import get_git_info, git_builder -from build.license_info import license_builder -from build.author_info import author_builder -from build.cache import show_progress -from build.pch import setup_pch - -def normalize_path(val, env): - return val if os.path.isabs(val) else os.path.join(env.Dir("#").abspath, val) - -def validate_parent_dir(key, val, env): - if not os.path.isdir(normalize_path(os.path.dirname(val), env)): - raise UserError("'%s' is not a directory: %s" % (key, os.path.dirname(val))) - -# Try to detect the host platform automatically. -# This is used if no `platform` argument is passed -if sys.platform.startswith("linux"): - default_platform = "linux" -elif sys.platform == "darwin": - default_platform = "macos" -elif sys.platform == "win32" or sys.platform == "msys": - default_platform = "windows" -elif ARGUMENTS.get("platform", ""): - default_platform = ARGUMENTS.get("platform") -else: - raise ValueError("Could not detect platform automatically, please specify with platform=") - -is_standalone = SCons.Script.sconscript_reading == 2 - -try: - Import("env") - old_env = env - env = old_env.Clone() -except: - # Default tools with no platform defaults to gnu toolchain. - # We apply platform specific toolchains via our custom tools. - env = Environment(tools=["default"], PLATFORM="") - old_env = env - -env.TOOLPATH = [env.Dir("tools").abspath] -env.is_standalone = is_standalone -env.show_progress = show_progress - -env.PrependENVPath("PATH", os.getenv("PATH")) - -# CPU architecture options. -architecture_array = [ - "", - "universal", - "x86_32", - "x86_64", - "arm32", - "arm64", - "rv64", - "ppc32", - "ppc64", - "wasm32", -] -architecture_aliases = { - "x64": "x86_64", - "amd64": "x86_64", - "armv7": "arm32", - "armv8": "arm64", - "arm64v8": "arm64", - "aarch64": "arm64", - "rv": "rv64", - "riscv": "rv64", - "riscv64": "rv64", - "ppcle": "ppc32", - "ppc": "ppc32", - "ppc64le": "ppc64", -} - -platforms = ("linux", "macos", "windows", "android", "ios", "web") -unsupported_known_platforms = ("android", "ios", "web") - -def SetupOptions(): - # Default num_jobs to local cpu count if not user specified. - # SCons has a peculiarity where user-specified options won't be overridden - # by SetOption, so we can rely on this to know if we should use our default. - initial_num_jobs = env.GetOption("num_jobs") - altered_num_jobs = initial_num_jobs + 1 - env.SetOption("num_jobs", altered_num_jobs) - if env.GetOption("num_jobs") == altered_num_jobs: - cpu_count = os.cpu_count() - if cpu_count is None: - print("Couldn't auto-detect CPU count to configure build parallelism. Specify it with the -j argument.") - else: - safer_cpu_count = cpu_count if cpu_count <= 4 else cpu_count - 1 - print( - "Auto-detected %d CPU cores available for build parallelism. Using %d cores by default. You can override it with the -j argument." - % (cpu_count, safer_cpu_count) - ) - env.SetOption("num_jobs", safer_cpu_count) - - opts = OptionsClass(ARGUMENTS.copy()) - - opts.Add( - EnumVariable( - key="platform", - help="Target platform", - default=env.get("platform", default_platform), - allowed_values=platforms, - ignorecase=2, - ) - ) - - opts.Add( - EnumVariable( - key="target", - help="Compilation target", - default=env.get("target", "template_debug"), - allowed_values=("editor", "template_release", "template_debug"), - ) - ) - - opts.Add( - EnumVariable( - key="precision", - help="Set the floating-point precision level", - default=env.get("precision", "single"), - allowed_values=("single", "double"), - ) - ) - - opts.Add( - BoolVariable( - key="use_hot_reload", - help="Enable the extra accounting required to support hot reload.", - default=env.get("use_hot_reload", None), - ) - ) - - opts.Add( - BoolVariable( - "disable_exceptions", - "Force disabling exception handling code", - default=env.get("disable_exceptions", True), - ) - ) - - opts.Add( - BoolVariable( - "disable_rtti", - "Disabling of runtime type information", - default=env.get("disable_rtti", True) - ) - ) - - opts.Add( - EnumVariable( - key="symbols_visibility", - help="Symbols visibility on GNU platforms. Use 'auto' to apply the default value.", - default=env.get("symbols_visibility", "hidden"), - allowed_values=["auto", "visible", "hidden"], - ) - ) - - # Add platform options. Iterate deterministically with the current platform - # registered last so its defaults win - tools = {} - current_platform = env.get("platform", default_platform) - supported = sorted(set(platforms) - set(unsupported_known_platforms)) - if current_platform in supported: - supported = [pl for pl in supported if pl != current_platform] + [current_platform] - for pl in supported: - tool = Tool(pl, toolpath=env.TOOLPATH) - if hasattr(tool, "options"): - tool.options(opts) - tools[pl] = tool - - # CPU architecture options. - opts.Add( - EnumVariable( - key="arch", - help="CPU architecture", - default=env.get("arch", ""), - allowed_values=architecture_array, - map=architecture_aliases, - ) - ) - - # compiledb - opts.Add( - BoolVariable( - key="compiledb", - help="Generate compilation DB (`compile_commands.json`) for external tools", - default=env.get("compiledb", False), - ) - ) - opts.Add( - PathVariable( - key="compiledb_file", - help="Path to a custom `compile_commands.json` file", - default=env.get("compiledb_file", "compile_commands.json"), - validator=validate_parent_dir, - ) - ) - opts.Add(BoolVariable("verbose", "Enable verbose output for the compilation", False)) - opts.Add(BoolVariable("intermediate_delete", "Enables automatically deleting unassociated intermediate binary files.", True)) - opts.Add(BoolVariable("progress", "Show a progress indicator during compilation", True)) - opts.Add(BoolVariable("use_pch", "Enable precompiled headers when the toolchain supports it", True)) - - # Targets flags tool (optimizations, debug symbols) - target_tool = Tool("targets", toolpath=env.TOOLPATH) - target_tool.options(opts) - env._opts = opts - env._target_tool = target_tool - return opts - -def FinalizeOptions(): - opts = env._opts - target_tool = env._target_tool - # Custom options and profile flags. - opts.Make(["../custom.py"]) - opts.Finalize(env) - Help(opts.GenerateHelpText(env)) - - env.extra_suffix = "" - - if env["platform"] in unsupported_known_platforms: - print("Unsupported platform: " + env["platform"]+". Only supports " + ", ".join(set(platforms) - set(unsupported_known_platforms))) - Exit() - - # Process CPU architecture argument. - if env["arch"] == "": - # No architecture specified. Default to arm64 if building for Android, - # universal if building for macOS or iOS, wasm32 if building for web, - # otherwise default to the host architecture. - if env["platform"] in ["macos", "ios"]: - env["arch"] = "universal" - elif env["platform"] == "android": - env["arch"] = "arm64" - elif env["platform"] == "web": - env["arch"] = "wasm32" - else: - host_machine = platform.machine().lower() - if host_machine in architecture_array: - env["arch"] = host_machine - elif host_machine in architecture_aliases.keys(): - env["arch"] = architecture_aliases[host_machine] - elif "86" in host_machine: - # Catches x86, i386, i486, i586, i686, etc. - env["arch"] = "x86_32" - else: - print("Unsupported CPU architecture: " + host_machine) - env.Exit(1) - - print("Building for architecture " + env["arch"] + " on platform " + env["platform"]) - - tool = Tool(env["platform"], toolpath=env.TOOLPATH) - - if tool is None or not tool.exists(env): - raise ValueError("Required toolchain not found for platform " + env["platform"]) - - target_tool.generate(env) - tool.generate(env) - - Decider("MD5-timestamp") - - scons_cache_path = os.environ.get("SCONS_CACHE") - if scons_cache_path != None: - CacheDir(scons_cache_path) - print("Scons cache enabled... (path: '" + scons_cache_path + "')") - - if env["compiledb"] and is_standalone: - # compile_commands.json - env.Tool("compilation_db") - env.Alias("compiledb", env.CompilationDatabase(normalize_path(env["compiledb_file"], env))) - -env.SetupOptions = SetupOptions -env.FinalizeOptions = FinalizeOptions -env.GlobRecursive = GlobRecursive -env.GlobRecursiveVariant = lambda pattern, src, variant, exclude=None: GlobRecursiveVariant(env, pattern, src, variant, exclude) -env.get_git_info = get_git_info -env.license_builder = license_builder -env.git_builder = git_builder -env.author_builder = author_builder -env.SetupPCH = lambda header, source: setup_pch(env, header, source) - - -def to_raw_cstring(value: Union[str, List[str]]) -> str: - MAX_LITERAL = 16380 - - if isinstance(value, list): - value = "\n".join(value) + "\n" - - split: List[bytes] = [] - offset = 0 - encoded = value.encode() - - while offset <= len(encoded): - segment = encoded[offset : offset + MAX_LITERAL] - offset += MAX_LITERAL - if len(segment) == MAX_LITERAL: - # Try to segment raw strings at double newlines to keep readable. - pretty_break = segment.rfind(b"\n\n") - if pretty_break != -1: - segment = segment[: pretty_break + 1] - offset -= MAX_LITERAL - pretty_break - 1 - # If none found, ensure we end with valid utf8. - # https://github.com/halloleo/unicut/blob/master/truncate.py - elif segment[-1] & 0b10000000: - last_11xxxxxx_index = [i for i in range(-1, -5, -1) if segment[i] & 0b11000000 == 0b11000000][0] - last_11xxxxxx = segment[last_11xxxxxx_index] - if not last_11xxxxxx & 0b00100000: - last_char_length = 2 - elif not last_11xxxxxx & 0b0010000: - last_char_length = 3 - elif not last_11xxxxxx & 0b0001000: - last_char_length = 4 - - if last_char_length > -last_11xxxxxx_index: - segment = segment[:last_11xxxxxx_index] - offset += last_11xxxxxx_index - - split += [segment] - - if len(split) == 1: - return f'R"({split[0].decode()})"' - else: - # Wrap multiple segments in parenthesis to suppress `string-concatenation` warnings on clang. - return "({})".format(" ".join(f'R"({segment.decode()})"' for segment in split)) - - -C_ESCAPABLES = [ - ("\\", "\\\\"), - ("\a", "\\a"), - ("\b", "\\b"), - ("\f", "\\f"), - ("\n", "\\n"), - ("\r", "\\r"), - ("\t", "\\t"), - ("\v", "\\v"), - # ("'", "\\'"), # Skip, as we're only dealing with full strings. - ('"', '\\"'), - ] -C_ESCAPE_TABLE = str.maketrans(dict((x, y) for x, y in C_ESCAPABLES)) - -def to_escaped_cstring(value: str) -> str: - return value.translate(C_ESCAPE_TABLE) - -def Run(env, function, **kwargs): - return SCons.Action.Action(function, "$GENCOMSTR", **kwargs) - -def CommandNoCache(env, target, sources, command, **kwargs): - result = env.Command(target, sources, command, **kwargs) - env.NoCache(result) - for key, val in kwargs.items(): - env.Depends(result, env.Value({ key: val })) - return result - -env.to_raw_cstring = to_raw_cstring -env.to_escaped_cstring = to_escaped_cstring - -env.__class__.Run = Run -env.__class__.CommandNoCache = CommandNoCache - -Return("env") \ No newline at end of file diff --git a/build/__init__.py b/build/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/author_info.py b/build/author_info.py deleted file mode 100644 index 93175a6..0000000 --- a/build/author_info.py +++ /dev/null @@ -1,48 +0,0 @@ -def author_builder(target, source, env): - name_prefix = env.get("name_prefix", "project") - prefix_upper = name_prefix.upper() - sections = env.get("sections", {"Developers": "AUTHORS_DEVELOPERS"}) - - def get_buffer() -> bytes: - with open(str(source[0]), "rb") as file: - return file.read() - - buffer = get_buffer() - - reading = False - - with open(str(target[0]), "wt", encoding="utf-8", newline="\n") as file: - file.write("/* THIS FILE IS GENERATED. EDITS WILL BE LOST. */\n\n") - file.write( - """\ -#pragma once - -#include -#include - -namespace OpenVic { -""" - ) - - def close_section(): - file.write("\t});\n") - - for line in buffer.decode().splitlines(): - if line.startswith(" ") and reading: - file.write(f'\t\t"{env.to_escaped_cstring(line).strip()}",\n') - elif line.startswith("## "): - if reading: - close_section() - file.write("\n") - reading = False - section = sections.get(line[3:].strip(), None) - if section: - file.write( - f"\tstatic constexpr std::array {prefix_upper}_{section} = std::to_array({{\n" - ) - reading = True - - if reading: - close_section() - - file.write("}") diff --git a/build/cache.py b/build/cache.py deleted file mode 100644 index d48b0e0..0000000 --- a/build/cache.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copied from https://github.com/godotengine/godot/blob/c3b0a92c3cd9a219c1b1776b48c147f1d0602f07/methods.py#L1049-L1172 -def show_progress(env): - import os - import sys - import glob - from SCons.Script import Progress, Command, AlwaysBuild - - screen = sys.stdout - # Progress reporting is not available in non-TTY environments since it - # messes with the output (for example, when writing to a file) - show_progress = env["progress"] and sys.stdout.isatty() - node_count = 0 - node_count_max = 0 - node_count_interval = 1 - node_count_fname = str(env.Dir("#")) + "/.scons_node_count" - - import time, math - - class cache_progress: - # The default is 1 GB cache and 12 hours half life - def __init__(self, path=None, limit=1073741824, half_life=43200): - self.path = path - self.limit = limit - self.exponent_scale = math.log(2) / half_life - if env["verbose"] and path != None: - screen.write( - "Current cache limit is {} (used: {})\n".format( - self.convert_size(limit), self.convert_size(self.get_size(path)) - ) - ) - self.delete(self.file_list()) - - def __call__(self, node, *args, **kw): - nonlocal node_count, node_count_max, node_count_interval, node_count_fname, show_progress - if show_progress: - # Print the progress percentage - node_count += node_count_interval - if node_count_max > 0 and node_count <= node_count_max: - screen.write("\r[%3d%%] " % (node_count * 100 / node_count_max)) - screen.flush() - elif node_count_max > 0 and node_count > node_count_max: - screen.write("\r[100%] ") - screen.flush() - else: - screen.write("\r[Initial build] ") - screen.flush() - - def delete(self, files): - if len(files) == 0: - return - if env["verbose"]: - # Utter something - screen.write("\rPurging %d %s from cache...\n" % (len(files), len(files) > 1 and "files" or "file")) - [os.remove(f) for f in files] - - def file_list(self): - if self.path is None: - # Nothing to do - return [] - # Gather a list of (filename, (size, atime)) within the - # cache directory - file_stat = [(x, os.stat(x)[6:8]) for x in glob.glob(os.path.join(self.path, "*", "*"))] - if file_stat == []: - # Nothing to do - return [] - # Weight the cache files by size (assumed to be roughly - # proportional to the recompilation time) times an exponential - # decay since the ctime, and return a list with the entries - # (filename, size, weight). - current_time = time.time() - file_stat = [(x[0], x[1][0], (current_time - x[1][1])) for x in file_stat] - # Sort by the most recently accessed files (most sensible to keep) first - file_stat.sort(key=lambda x: x[2]) - # Search for the first entry where the storage limit is - # reached - sum, mark = 0, None - for i, x in enumerate(file_stat): - sum += x[1] - if sum > self.limit: - mark = i - break - if mark is None: - return [] - else: - return [x[0] for x in file_stat[mark:]] - - def convert_size(self, size_bytes): - if size_bytes == 0: - return "0 bytes" - size_name = ("bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") - i = int(math.floor(math.log(size_bytes, 1024))) - p = math.pow(1024, i) - s = round(size_bytes / p, 2) - return "%s %s" % (int(s) if i == 0 else s, size_name[i]) - - def get_size(self, start_path="."): - total_size = 0 - for dirpath, dirnames, filenames in os.walk(start_path): - for f in filenames: - fp = os.path.join(dirpath, f) - total_size += os.path.getsize(fp) - return total_size - - def progress_finish(target, source, env): - nonlocal node_count, progressor - try: - with open(node_count_fname, "w") as f: - f.write("%d\n" % node_count) - progressor.delete(progressor.file_list()) - except Exception: - pass - - try: - with open(node_count_fname) as f: - node_count_max = int(f.readline()) - except Exception: - pass - - cache_directory = os.environ.get("SCONS_CACHE") - # Simple cache pruning, attached to SCons' progress callback. Trim the - # cache directory to a size not larger than cache_limit. - cache_limit = float(os.getenv("SCONS_CACHE_LIMIT", 1024)) * 1024 * 1024 - progressor = cache_progress(cache_directory, cache_limit) - Progress(progressor, interval=node_count_interval) - - progress_finish_command = Command("progress_finish", [], progress_finish) - AlwaysBuild(progress_finish_command) diff --git a/build/common_compiler_flags.py b/build/common_compiler_flags.py deleted file mode 100644 index 67e5ac1..0000000 --- a/build/common_compiler_flags.py +++ /dev/null @@ -1,145 +0,0 @@ -# Based on https://github.com/godotengine/godot-cpp/blob/98ea2f60bb3846d6ae410d8936137d1b099cd50b/tools/common_compiler_flags.py -import os -import subprocess - - -def using_clang(env): - return "clang" in os.path.basename(env["CC"]) - - -def is_vanilla_clang(env): - if not using_clang(env): - return False - try: - version = subprocess.check_output([env.subst(env["CXX"]), "--version"]).strip().decode("utf-8") - except (subprocess.CalledProcessError, OSError): - print("Couldn't parse CXX environment variable to infer compiler version.") - return False - return not version.startswith("Apple") - - -def exists(env): - return True - - -def generate(env): - assert env["lto"] in ["thin", "full", "none"], "Unrecognized lto: {}".format(env["lto"]) - if env["lto"] != "none": - print("Using LTO: " + env["lto"]) - - # Require C++20 - if env.get("is_msvc", False): - env.Append(CXXFLAGS=["/std:c++20"]) - else: - env.Append(CXXFLAGS=["-std=c++20"]) - - if env["precision"] == "double": - env.Append(CPPDEFINES=["REAL_T_IS_DOUBLE"]) - - # Disable exception handling. Godot doesn't use exceptions anywhere, and this - # saves around 20% of binary size and very significant build time. - if env["disable_exceptions"]: - if env.get("is_msvc", False): - env.Append(CPPDEFINES=[("_HAS_EXCEPTIONS", 0)]) - else: - env.Append(CXXFLAGS=["-fno-exceptions"]) - elif env.get("is_msvc", False): - env.Append(CXXFLAGS=["/EHsc"]) - - if not env.get("is_msvc", False): - if env["symbols_visibility"] == "visible": - env.Append(CCFLAGS=["-fvisibility=default"]) - env.Append(LINKFLAGS=["-fvisibility=default"]) - elif env["symbols_visibility"] == "hidden": - env.Append(CCFLAGS=["-fvisibility=hidden"]) - env.Append(LINKFLAGS=["-fvisibility=hidden"]) - - if env["optimize"] == "speed": - env.Append(CPPDEFINES=["OPT_SPEED_ENABLED"]) - elif env["optimize"] == "speed_trace": - env.Append(CPPDEFINES=["OPT_SPEED_TRACE_ENABLED"]) - elif env["optimize"] == "size": - env.Append(CPPDEFINES=["OPT_SIZE_ENABLED"]) - elif env["optimize"] == "debug": - env.Append(CPPDEFINES=["OPT_DEBUG_ENABLED"]) - - if env["harden_memory"] == "fast": - env.Append(CPPDEFINES=["_GLIBCXX_ASSERTIONS", ("_LIBCPP_HARDENING_MODE", "_LIBCPP_HARDENING_MODE_FAST"), ("_MSVC_STL_HARDENING", 1)]) - - # Set optimize and debug_symbols flags. - # "custom" means do nothing and let users set their own optimization flags. - if env.get("is_msvc", False): - if env["debug_symbols"]: - env.Append(CCFLAGS=["/Zi", "/FS"]) - env.Append(LINKFLAGS=["/DEBUG:FULL"]) - - if env["disable_rtti"]: - env.Append(CCFLAGS=["/GR-"]) - - if env["optimize"] == "speed": - env.Append(CCFLAGS=["/O2"]) - env.Append(LINKFLAGS=["/OPT:REF"]) - elif env["optimize"] == "speed_trace": - env.Append(CCFLAGS=["/O2"]) - env.Append(LINKFLAGS=["/OPT:REF", "/OPT:NOICF"]) - elif env["optimize"] == "size": - env.Append(CCFLAGS=["/O1"]) - env.Append(LINKFLAGS=["/OPT:REF"]) - elif env["optimize"] == "debug" or env["optimize"] == "none": - env.Append(CCFLAGS=["/Od"]) - - if env["lto"] == "thin": - if not env["use_llvm"]: - print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") - env.Exit(255) - - env.Append(CCFLAGS=["-flto=thin"]) - env.Append(LINKFLAGS=["-flto=thin"]) - elif env["lto"] == "full": - if env["use_llvm"]: - env.Append(CCFLAGS=["-flto"]) - env.Append(LINKFLAGS=["-flto"]) - else: - env.AppendUnique(CCFLAGS=["/GL"]) - env.AppendUnique(ARFLAGS=["/LTCG"]) - env.AppendUnique(LINKFLAGS=["/LTCG"]) - else: - if env["debug_symbols"]: - # Adding dwarf-4 explicitly makes stacktraces work with clang builds, - # otherwise addr2line doesn't understand them. - env.Append(CCFLAGS=["-gdwarf-4"]) - if env.dev_build: - env.Append(CCFLAGS=["-g3"]) - else: - env.Append(CCFLAGS=["-g2"]) - else: - if using_clang(env) and not is_vanilla_clang(env) and not env["use_mingw"]: - # Apple Clang, its linker doesn't like -s. - env.Append(LINKFLAGS=["-Wl,-S", "-Wl,-x", "-Wl,-dead_strip"]) - else: - env.Append(LINKFLAGS=["-s"]) - - if env["disable_rtti"]: - env.Append(CCFLAGS=["-fno-rtti"]) - - if env["optimize"] == "speed": - env.Append(CCFLAGS=["-O3"]) - # `-O2` is friendlier to debuggers than `-O3`, leading to better crash backtraces. - elif env["optimize"] == "speed_trace": - env.Append(CCFLAGS=["-O2"]) - elif env["optimize"] == "size": - env.Append(CCFLAGS=["-Os"]) - elif env["optimize"] == "debug": - env.Append(CCFLAGS=["-Og"]) - elif env["optimize"] == "none": - env.Append(CCFLAGS=["-O0"]) - - if env["lto"] == "thin": - if (env["platform"] == "windows" or env["platform"] == "linux") and not env["use_llvm"]: - print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") - env.Exit(255) - env.Append(CCFLAGS=["-flto=thin"]) - env.Append(LINKFLAGS=["-flto=thin"]) - elif env["lto"] == "full": - env.Append(CCFLAGS=["-flto"]) - env.Append(LINKFLAGS=["-flto"]) diff --git a/build/git_info.py b/build/git_info.py deleted file mode 100644 index cf4d465..0000000 --- a/build/git_info.py +++ /dev/null @@ -1,131 +0,0 @@ -import os -import subprocess - - -def get_git_tag(prefix): - tag_var_name = f"{prefix}_TAG" - git_tag = "" - if tag_var_name in os.environ: - return os.environ[tag_var_name] - else: - git_tag = "" - - if os.path.exists(".git"): - try: - result = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"], encoding="utf-8").strip() - if result != "": - git_tag = result - except (subprocess.CalledProcessError, OSError): - # `git` not found in PATH. - pass - - return git_tag - - -def get_git_release(prefix): - release_var_name = f"{prefix}_RELEASE" - git_release = "" - if release_var_name in os.environ: - return os.environ[release_var_name] - else: - git_release = "" - - if os.path.exists(".git"): - try: - result = subprocess.check_output( - ["gh", "release", "list", "--json", "name", "-q", ".[0] | .name"], encoding="utf-8" - ).strip() - if result != "": - git_release = result - except (subprocess.CalledProcessError, OSError): - # `gh` not found in PATH. - git_tag = get_git_tag(prefix) - if git_tag != "": - git_release = git_tag - - return git_release - - -def get_git_hash(): - # Parse Git hash if we're in a Git repo. - git_hash = "0000000000000000000000000000000000000000" - git_folder = ".git" - - if os.path.isfile(".git"): - with open(".git", "r", encoding="utf-8") as file: - module_folder = file.readline().strip() - if module_folder.startswith("gitdir: "): - git_folder = module_folder[8:] - - if os.path.isfile(os.path.join(git_folder, "HEAD")): - with open(os.path.join(git_folder, "HEAD"), "r", encoding="utf8") as file: - head = file.readline().strip() - if head.startswith("ref: "): - ref = head[5:] - # If this directory is a Git worktree instead of a root clone. - parts = git_folder.split("/") - if len(parts) > 2 and parts[-2] == "worktrees": - git_folder = "/".join(parts[0:-2]) - head = os.path.join(git_folder, ref) - packedrefs = os.path.join(git_folder, "packed-refs") - if os.path.isfile(head): - with open(head, "r", encoding="utf-8") as file: - git_hash = file.readline().strip() - elif os.path.isfile(packedrefs): - # Git may pack refs into a single file. This code searches .git/packed-refs file for the current ref's hash. - # https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-pack-refs.html - for line in open(packedrefs, "r", encoding="utf-8").read().splitlines(): - if line.startswith("#"): - continue - (line_hash, line_ref) = line.split(" ") - if ref == line_ref: - git_hash = line_hash - break - else: - git_hash = head - - # Get the UNIX timestamp of the build commit. - git_timestamp = 0 - if os.path.exists(".git"): - try: - git_timestamp = subprocess.check_output( - ["git", "log", "-1", "--pretty=format:%ct", "--no-show-signature", git_hash], encoding="utf-8" - ) - except (subprocess.CalledProcessError, OSError): - # `git` not found in PATH. - pass - - return { - "git_hash": git_hash, - "git_timestamp": git_timestamp, - } - - -def get_git_info(name_prefix="project"): - prefix_upper = name_prefix.upper() - return {**get_git_hash(), "git_tag": get_git_tag(prefix_upper), "git_release": get_git_release(prefix_upper)} - - -def git_builder(target, source, env): - name_prefix = env.get("name_prefix", "project") - prefix_upper = name_prefix.upper() - - git_info = source[0].read() - - with open(str(target[0]), "wt", encoding="utf-8", newline="\n") as file: - file.write("/* THIS FILE IS GENERATED. EDITS WILL BE LOST. */\n\n") - file.write( - f"""\ -#pragma once - -#include -#include - -namespace OpenVic {{ - static constexpr std::string_view {prefix_upper}_TAG = "{git_info["git_tag"]}"; - static constexpr std::string_view {prefix_upper}_RELEASE = "{git_info["git_release"]}"; - static constexpr std::string_view {prefix_upper}_COMMIT_HASH = "{git_info["git_hash"]}"; - static constexpr const uint64_t {prefix_upper}_COMMIT_TIMESTAMP = {git_info["git_timestamp"]}ull; -}} -""" - ) diff --git a/build/glob_recursive.py b/build/glob_recursive.py deleted file mode 100644 index d4fe65d..0000000 --- a/build/glob_recursive.py +++ /dev/null @@ -1,51 +0,0 @@ -def GlobRecursive(pattern, nodes=["."], exclude=None): - import fnmatch - import os - - import SCons - - fs = SCons.Node.FS.get_default_fs() - Glob = fs.Glob - - if isinstance(exclude, str): - exclude = [exclude] - - results = [] - for node in nodes: - node_str = str(node) - - for f in Glob(node_str + "/*", source=True): - if type(f) is SCons.Node.FS.Dir: - child = node_str + "/" + os.path.basename(str(f)) - results += GlobRecursive(pattern, [child]) - - results += Glob(node_str + "/" + pattern) - - if isinstance(exclude, list): - - def norm(s): - return str(s).replace("\\", "/") - - norm_exclude = [norm(p) for p in exclude] - results = [r for r in results if not any(fnmatch.fnmatch(norm(r), p) for p in norm_exclude)] - return results - - -def GlobRecursiveVariant(env, pattern, src_root, variant_root, exclude=None): - src_nodes = GlobRecursive(pattern, [src_root]) - src_abs = env.Dir(src_root).abspath.replace("\\", "/").rstrip("/") + "/" - variant_prefix = env.Dir(variant_root).abspath.replace("\\", "/").rstrip("/") + "/" - if exclude is None: - exclude_abs = set() - else: - if isinstance(exclude, str): - exclude = [exclude] - exclude_abs = {env.File(e).abspath.replace("\\", "/") for e in exclude} - out = [] - for n in src_nodes: - p = n.abspath.replace("\\", "/") - if p in exclude_abs: - continue - assert p.startswith(src_abs), f"{p!r} not under {src_abs!r}" - out.append(env.File(variant_prefix + p[len(src_abs) :])) - return out diff --git a/build/license_info.py b/build/license_info.py deleted file mode 100644 index 7652328..0000000 --- a/build/license_info.py +++ /dev/null @@ -1,167 +0,0 @@ -from collections import OrderedDict -from io import TextIOWrapper - - -def get_license_info(src_copyright): - class LicenseReader: - def __init__(self, license_file: TextIOWrapper): - self._license_file = license_file - self.line_num = 0 - self.current = self.next_line() - - def next_line(self): - line = self._license_file.readline() - self.line_num += 1 - while line.startswith("#"): - line = self._license_file.readline() - self.line_num += 1 - self.current = line - return line - - def next_tag(self): - if ":" not in self.current: - return ("", []) - tag, line = self.current.split(":", 1) - lines = [line.strip()] - while self.next_line() and self.current.startswith(" "): - lines.append(self.current.strip()) - return (tag, lines) - - projects = OrderedDict() - license_list = [] - - with open(src_copyright, "r", encoding="utf-8") as copyright_file: - reader = LicenseReader(copyright_file) - part = {} - while reader.current: - tag, content = reader.next_tag() - if tag in ("Files", "Copyright", "License"): - part[tag] = content[:] - elif tag == "Comment" and part: - # attach non-empty part to named project - projects[content[0]] = projects.get(content[0], []) + [part] - - if not tag or not reader.current: - # end of a paragraph start a new part - if "License" in part and "Files" not in part: - # no Files tag in this one, so assume standalone license - license_list.append(part["License"]) - part = {} - reader.next_line() - - data_list: list = [] - for project in iter(projects.values()): - for part in project: - part["file_index"] = len(data_list) - data_list += part["Files"] - part["copyright_index"] = len(data_list) - data_list += part["Copyright"] - - return {"data": data_list, "projects": projects, "parts": part, "licenses": license_list} - - -def license_builder(target, source, env): - name_prefix = env.get("name_prefix", "project") - prefix_upper = name_prefix.upper() - prefix_capital = name_prefix.capitalize() - - license_text_name = f"{prefix_upper}_LICENSE_TEXT" - component_copyright_part_name = f"{prefix_capital}ComponentCopyrightPart" - component_copyright_name = f"{prefix_capital}ComponentCopyright" - copyright_data_name = f"{prefix_upper}_COPYRIGHT_DATA" - copyright_parts_name = f"{prefix_upper}_COPYRIGHT_PARTS" - copyright_info_name = f"{prefix_upper}_COPYRIGHT_INFO" - license_name = f"{prefix_capital}License" - licenses_name = f"{prefix_upper}_LICENSES" - - src_copyright = get_license_info(str(source[0])) - src_license = str(source[1]) - - with open(src_license, "r", encoding="utf-8") as file: - license_text = file.read() - - def copyright_data_str() -> str: - result = "" - for line in src_copyright["data"]: - result += f'\t\t"{line}",\n' - return result - - part_indexes = {} - - def copyright_part_str() -> str: - part_index = 0 - result = "" - for project_name, project in iter(src_copyright["projects"].items()): - part_indexes[project_name] = part_index - for part in project: - result += ( - f'\t\t{{ "{env.to_escaped_cstring(part["License"][0])}", ' - + f"{{ &{copyright_data_name}[{part['file_index']}], {len(part['Files'])} }}, " - + f"{{ &{copyright_data_name}[{part['copyright_index']}], {len(part['Copyright'])} }} }},\n" - ) - part_index += 1 - return result - - def copyright_info_str() -> str: - result = "" - for project_name, project in iter(src_copyright["projects"].items()): - result += ( - f'\t\t{{ "{env.to_escaped_cstring(project_name)}", ' - + f"{{ &{copyright_parts_name}[{part_indexes[project_name]}], {len(project)} }} }},\n" - ) - return result - - def license_list_str() -> str: - result = "" - for license in iter(src_copyright["licenses"]): - result += ( - f'\t\t{{ "{env.to_escaped_cstring(license[0])}",' - + f'\n\t\t {env.to_raw_cstring([line if line != "." else "" for line in license[1:]])} }}, \n' - ) - return result - - with open(str(target[0]), "wt", encoding="utf-8", newline="\n") as file: - file.write("/* THIS FILE IS GENERATED. EDITS WILL BE LOST. */\n\n") - file.write( - f"""\ -#pragma once - -#include -#include -#include - -namespace OpenVic {{ - static constexpr std::string_view {license_text_name} = {{ - {env.to_raw_cstring(license_text)} - }}; - - struct {component_copyright_part_name} {{ - std::string_view license; - std::span files; - std::span copyright_statements; - }}; - - struct {component_copyright_name} {{ - std::string_view name; - std::span parts; - }}; - - static constexpr std::array {copyright_data_name} = std::to_array({{ -{copyright_data_str()}\t}}); - - static constexpr std::array {copyright_parts_name} = std::to_array<{component_copyright_part_name}>({{ -{copyright_part_str()}\t}}); - - static constexpr std::array {copyright_info_name} = std::to_array<{component_copyright_name}>({{ -{copyright_info_str()}\t}}); - - struct {license_name} {{ - std::string_view license_name; - std::string_view license_body; - }}; - - static constexpr std::array {licenses_name} = std::to_array<{license_name}>({{ -{license_list_str()}\t}}); -}} -""" - ) diff --git a/build/option_handler.py b/build/option_handler.py deleted file mode 100644 index 223c12f..0000000 --- a/build/option_handler.py +++ /dev/null @@ -1,40 +0,0 @@ -import os - -from SCons.Variables import Variables - - -class OptionsClass: - def __init__(self, args): - self.opts = None - self.opt_list = [] - self.args = args - self.saved_args = args.copy() - - def Add(self, variableOrKey, *argv, **kwarg): - self.opt_list.append([variableOrKey, argv, kwarg]) - # Necessary to have our own build options without errors - if isinstance(variableOrKey, str): - self.args.pop(variableOrKey, True) - else: - self.args.pop(variableOrKey[0], True) - - def Make(self, customs): - self.args = self.saved_args - profile = self.args.get("profile", "") - if profile: - if os.path.isfile(profile): - customs.append(profile) - elif os.path.isfile(profile + ".py"): - customs.append(profile + ".py") - self.opts = Variables(customs, self.args) - for opt in self.opt_list: - if opt[1] is None and opt[2] is None: - self.opts.Add(opt[0]) - else: - self.opts.Add(opt[0], *opt[1], **opt[2]) - - def Finalize(self, env): - self.opts.Update(env) - - def GenerateHelpText(self, env): - return self.opts.GenerateHelpText(env) diff --git a/build/pch.py b/build/pch.py deleted file mode 100644 index a93271e..0000000 --- a/build/pch.py +++ /dev/null @@ -1,25 +0,0 @@ -def setup_pch(env, header_rel, source_cpp_variant): - if not env.get("use_pch", True): - return - - handler = _select_handler(env) - if handler is None: - return - - name = handler(env, header_rel, source_cpp_variant) - print(f"[PCH] enabled ({name}): {header_rel}") - - -def _select_handler(env): - if env.get("is_msvc", False): - return _msvc_pch - # TODO: GCC/Clang/MinGW - return None - - -def _msvc_pch(env, header_rel, source_cpp_variant): - env["PCHSTOP"] = header_rel - pch_pch, _pch_obj = env.PCH(source_cpp_variant) - env["PCH"] = pch_pch - env.Append(CCFLAGS=["/FI" + header_rel]) - return "msvc" diff --git a/cmake/OpenVicCodegen.cmake b/cmake/OpenVicCodegen.cmake new file mode 100644 index 0000000..0263e9a --- /dev/null +++ b/cmake/OpenVicCodegen.cmake @@ -0,0 +1,146 @@ +# Generated-header support shared by all OpenVic repos, driving gen/codegen.py +# (git/license/author info headers and foonathan/memory config headers). + +include_guard(GLOBAL) + +get_filename_component(_openvic_codegen_script "${CMAKE_CURRENT_LIST_DIR}/../gen/codegen.py" ABSOLUTE) +set(OPENVIC_CODEGEN_SCRIPT "${_openvic_codegen_script}" CACHE INTERNAL "Path to the OpenVic codegen CLI") + +# commit-info is filled in at configure time with configure_file() from local +# git metadata -- no codegen.py (Python startup) and no `gh release list` +# network call on the build's critical path. +get_filename_component(_openvic_commit_info_template "${CMAKE_CURRENT_LIST_DIR}/../gen/commit_info.gen.hpp.in" ABSOLUTE) +set(OPENVIC_COMMIT_INFO_TEMPLATE "${_openvic_commit_info_template}" CACHE INTERNAL "Path to the OpenVic commit-info header template") + +#[[ openvic_generate_commit_info(TARGET PREFIX REPO_DIR OUTPUT ) +Fills at configure time with configure_file(), substituting +_TAG/_RELEASE/_COMMIT_HASH/_COMMIT_TIMESTAMP from local git metadata (no +network). RELEASE mirrors TAG unless OPENVIC_RELEASE is set; OPENVIC_TAG likewise +forces TAG. The stamp is captured at configure time only -- it refreshes on the +next reconfigure, not on every build. Release/CI builds should configure fresh +(and typically set OPENVIC_RELEASE/OPENVIC_TAG) before building. TARGET is +accepted for call-site symmetry with the other generators but is unused (the +header exists on disk before the build starts). ]] +function(openvic_generate_commit_info) + cmake_parse_arguments(PARSE_ARGV 0 ARG "" "TARGET;PREFIX;REPO_DIR;OUTPUT" "") + + string(TOUPPER "${ARG_PREFIX}" PREFIX_UPPER) + + # --- tag --- + if(DEFINED ENV{OPENVIC_TAG}) + set(GIT_TAG "$ENV{OPENVIC_TAG}") + else() + execute_process( + COMMAND git describe --tags --abbrev=0 + WORKING_DIRECTORY "${ARG_REPO_DIR}" + OUTPUT_VARIABLE GIT_TAG OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET RESULT_VARIABLE _res + ) + if(NOT _res EQUAL 0 OR GIT_TAG STREQUAL "") + set(GIT_TAG "") + endif() + endif() + + # --- release: OPENVIC_RELEASE if set, otherwise the tag (no `gh`/network) --- + if(DEFINED ENV{OPENVIC_RELEASE}) + set(GIT_RELEASE "$ENV{OPENVIC_RELEASE}") + else() + set(GIT_RELEASE "${GIT_TAG}") + endif() + + # --- commit hash --- + execute_process( + COMMAND git rev-parse HEAD + WORKING_DIRECTORY "${ARG_REPO_DIR}" + OUTPUT_VARIABLE GIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET RESULT_VARIABLE _res + ) + if(NOT _res EQUAL 0 OR GIT_HASH STREQUAL "") + set(GIT_HASH "0000000000000000000000000000000000000000") + endif() + + # --- commit timestamp (UNIX seconds) --- + execute_process( + COMMAND git log -1 --pretty=format:%ct --no-show-signature + WORKING_DIRECTORY "${ARG_REPO_DIR}" + OUTPUT_VARIABLE GIT_TIMESTAMP OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET RESULT_VARIABLE _res + ) + if(NOT _res EQUAL 0 OR GIT_TIMESTAMP STREQUAL "") + set(GIT_TIMESTAMP "0") + endif() + + configure_file("${OPENVIC_COMMIT_INFO_TEMPLATE}" "${ARG_OUTPUT}" @ONLY NEWLINE_STYLE UNIX) +endfunction() + +#[[ openvic_generate_license_info(TARGET PREFIX

COPYRIGHT LICENSE OUTPUT ) ]] +function(openvic_generate_license_info) + cmake_parse_arguments(PARSE_ARGV 0 ARG "" "TARGET;PREFIX;COPYRIGHT;LICENSE;OUTPUT" "") + + add_custom_command( + OUTPUT ${ARG_OUTPUT} + COMMAND + ${Python3_EXECUTABLE} ${OPENVIC_CODEGEN_SCRIPT} license-info --prefix ${ARG_PREFIX} --copyright + ${ARG_COPYRIGHT} --license ${ARG_LICENSE} -o ${ARG_OUTPUT} + DEPENDS ${ARG_COPYRIGHT} ${ARG_LICENSE} ${OPENVIC_CODEGEN_SCRIPT} + COMMENT "Generating ${ARG_PREFIX} license_info.gen.hpp" + VERBATIM + ) + target_sources(${ARG_TARGET} PRIVATE ${ARG_OUTPUT}) +endfunction() + +#[[ openvic_generate_author_info(TARGET PREFIX

AUTHORS OUTPUT SECTIONS ...) ]] +function(openvic_generate_author_info) + cmake_parse_arguments(PARSE_ARGV 0 ARG "" "TARGET;PREFIX;AUTHORS;OUTPUT" "SECTIONS") + + set(section_args "") + foreach(section IN LISTS ARG_SECTIONS) + list(APPEND section_args --section "${section}") + endforeach() + + add_custom_command( + OUTPUT ${ARG_OUTPUT} + COMMAND + ${Python3_EXECUTABLE} ${OPENVIC_CODEGEN_SCRIPT} author-info --prefix ${ARG_PREFIX} --authors + ${ARG_AUTHORS} ${section_args} -o ${ARG_OUTPUT} + DEPENDS ${ARG_AUTHORS} ${OPENVIC_CODEGEN_SCRIPT} + COMMENT "Generating ${ARG_PREFIX} author_info.gen.hpp" + VERBATIM + ) + target_sources(${ARG_TARGET} PRIVATE ${ARG_OUTPUT}) +endfunction() + +#[[ openvic_generate_memory_headers(MEMORY_DIR DEFINES ...) +Writes config_impl.hpp and detail/container_node_sizes_impl.hpp into the +foonathan/memory source tree at configure time, byte-identical to the files +the SCons build generates (both build systems share these source-tree files, +and codegen.py leaves them untouched when the content already matches). ]] +function(openvic_generate_memory_headers) + cmake_parse_arguments(PARSE_ARGV 0 ARG "" "MEMORY_DIR" "DEFINES") + + set(define_args "") + foreach(define IN LISTS ARG_DEFINES) + list(APPEND define_args --define "${define}") + endforeach() + + set(inner_include "${ARG_MEMORY_DIR}/include/foonathan/memory") + execute_process( + COMMAND + ${Python3_EXECUTABLE} ${OPENVIC_CODEGEN_SCRIPT} memory-config ${define_args} -o + "${inner_include}/config_impl.hpp" + RESULT_VARIABLE result + ) + if(NOT result EQUAL 0) + message(FATAL_ERROR "codegen.py memory-config failed (${result})") + endif() + + execute_process( + COMMAND + ${Python3_EXECUTABLE} ${OPENVIC_CODEGEN_SCRIPT} memory-node-sizes -o + "${inner_include}/detail/container_node_sizes_impl.hpp" + RESULT_VARIABLE result + ) + if(NOT result EQUAL 0) + message(FATAL_ERROR "codegen.py memory-node-sizes failed (${result})") + endif() +endfunction() diff --git a/cmake/OpenVicDeps.cmake b/cmake/OpenVicDeps.cmake new file mode 100644 index 0000000..6d0561d --- /dev/null +++ b/cmake/OpenVicDeps.cmake @@ -0,0 +1,88 @@ +# Third-party dependency fetching shared by all OpenVic repos. +# +# Deps are fetched as pinned GitHub source tarballs into the build tree's +# `_deps/` dir (via FetchContent). The SHA/SHA256 pins in each repo's +# openvic_declare_dep() calls are the single source of truth for third-party +# versions; there are no third-party git submodules. Fetched sources live +# outside the working tree, so branch switches and CI checkouts never touch +# their mtimes and returning to an already-built pin is a no-op. +# +# First-party code (openvic-simulation, openvic-dataloader, lexy-vdf) is NOT +# fetched -- it is the code under development and must rebuild on branch +# switches, so it builds from the submodule tree via add_subdirectory(). + +include_guard(GLOBAL) + +include(FetchContent) + +# CMP0168 (CMake >= 3.30): FetchContent populates directly during configure +# with no per-dep sub-build -- a meaningful configure-time win across the ~15 +# fetched deps. Guarded so it is a no-op on 3.28/3.29. Set at include (directory) +# scope so it propagates to every repo's MakeAvailable call sites. +if(POLICY CMP0168) + cmake_policy(SET CMP0168 NEW) +endif() + +#[[ openvic_declare_dep( + REPO GitHub owner/repo the tarball comes from + SHA commit the tarball is pinned to + SHA256 tarball URL_HASH (integrity + download-skip) + [DOWNLOAD_ONLY] populate the source but do NOT add_subdirectory + it (for deps whose CMakeLists we don't use) + [SOURCE_SUBDIR

] add_subdirectory this subdir of the tarball + [SOURCE_DIR ] override the extraction directory + ) + +Declares a dep with FetchContent. The caller follows with +FetchContent_MakeAvailable() (after any option-setting the dep needs), +then uses ${_SOURCE_DIR} for DOWNLOAD_ONLY deps. + +Declaring the same FetchContent name twice keeps the first declaration +(standard FetchContent behavior). This is expected for the two vendored lexy +copies: openvic-dataloader's pin wins in composed builds, lexy-vdf's own pin +applies only when it builds standalone. + +Fetched as a source tarball rather than a git clone: a full-SHA GIT_TAG cannot +use a shallow clone, so git-based fetches of the larger repos (godot-cpp, +range-v3, spdlog) would pull full history. None of these deps need nested git +submodules at their pinned commit, so tarballs are strictly cheaper. + +Local-dev escape hatch: -DFETCHCONTENT_SOURCE_DIR_= points a +single dep back at a working-tree checkout (this reintroduces mtime coupling +for that one dep -- it is a debugging tool, not the default). +]] +function(openvic_declare_dep name) + cmake_parse_arguments( + PARSE_ARGV 1 ARG + "DOWNLOAD_ONLY" + "REPO;SHA;SHA256;SOURCE_SUBDIR;SOURCE_DIR" + "" + ) + + if(NOT ARG_REPO OR NOT ARG_SHA OR NOT ARG_SHA256) + message(FATAL_ERROR "openvic_declare_dep(${name}) requires REPO, SHA and SHA256") + endif() + + set(declare_args "") + if(ARG_DOWNLOAD_ONLY) + # SOURCE_SUBDIR pointing at a nonexistent dir makes MakeAvailable + # populate the source but skip add_subdirectory (documented behavior, + # CMake >= 3.28). Used for deps whose upstream CMakeLists we replace + # with a hand-rolled target (or whose CMakeLists pollutes global state). + list(APPEND declare_args SOURCE_SUBDIR "_openvic_do_not_add") + elseif(ARG_SOURCE_SUBDIR) + list(APPEND declare_args SOURCE_SUBDIR "${ARG_SOURCE_SUBDIR}") + endif() + if(ARG_SOURCE_DIR) + list(APPEND declare_args SOURCE_DIR "${ARG_SOURCE_DIR}") + endif() + + FetchContent_Declare( + ${name} + URL "https://github.com/${ARG_REPO}/archive/${ARG_SHA}.tar.gz" + URL_HASH SHA256=${ARG_SHA256} + SYSTEM + EXCLUDE_FROM_ALL + ${declare_args} + ) +endfunction() diff --git a/cmake/OpenVicFlags.cmake b/cmake/OpenVicFlags.cmake new file mode 100644 index 0000000..24a87a0 --- /dev/null +++ b/cmake/OpenVicFlags.cmake @@ -0,0 +1,87 @@ +# Compiler/define setup shared by all OpenVic repos. +# +# The Godot build axis reuses godot-cpp's cache variable names so one setting +# drives both godot-cpp and the OpenVic libraries; the declarations below also +# make them available in standalone builds that don't include godot-cpp. + +include_guard(GLOBAL) + +set(GODOTCPP_TARGET + "template_debug" + CACHE STRING + "Which Godot target to build for: editor, template_debug, template_release" +) +set_property(CACHE GODOTCPP_TARGET PROPERTY STRINGS "template_debug;template_release;editor") +option(GODOTCPP_DEV_BUILD "Developer build with dev-only debugging code (DEV_ENABLED)" OFF) + +if(NOT GODOTCPP_TARGET MATCHES "^(editor|template_debug|template_release)$") + message(FATAL_ERROR "GODOTCPP_TARGET must be editor, template_debug or template_release, got '${GODOTCPP_TARGET}'") +endif() + +#[[ Directory-scope flags every OpenVic repo builds with. Guarded by an +inherited (non-cache) variable so nested repos in a composed build inherit the +flags from their parent directory without re-adding them. Callers must invoke +this BEFORE add_subdirectory() of any code that should build with these flags +(and the OpenVic root invokes it AFTER add_subdirectory(godot-cpp), which owns +its own flag setup). ]] +macro(openvic_setup_base_flags) + if(NOT OPENVIC_BASE_FLAGS_APPLIED) + set(OPENVIC_BASE_FLAGS_APPLIED TRUE) + + # Third-party deps declare old cmake_minimum_required versions; give + # them modern policy behavior (option() and set(CACHE) honor normal + # variables, MSVC runtime via CMAKE_MSVC_RUNTIME_LIBRARY, and accept + # minimums older than CMake 4 supports). + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0091 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) + if(NOT DEFINED CMAKE_POLICY_VERSION_MINIMUM) + set(CMAKE_POLICY_VERSION_MINIMUM 3.5) + endif() + + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + + # No C++20 modules are used anywhere in the OpenVic sources, so skip the + # per-translation-unit module-import scan CMake adds for C++20 targets + # (CMP0155, on since 3.28). Each scan is a separate preprocessor pass + # per file -- pure overhead here, felt most on incremental builds. + set(CMAKE_CXX_SCAN_FOR_MODULES OFF) + + if(MSVC) + # /Zc:preprocessor is required: the code uses __VA_OPT__. + add_compile_options(/utf-8 /Zc:preprocessor) + # Godot doesn't use exceptions anywhere; build without them. + # /EHsc (a CMake default) must go too: it defines __cpp_exceptions, + # which would flip exception-detection macros (e.g. vmcontainer's) + # onto code paths that assume exceptions can propagate. + string(REPLACE "/EHsc" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + add_compile_definitions(NOMINMAX _HAS_EXCEPTIONS=0) + else() + add_compile_options(-fno-exceptions -fvisibility=hidden) + endif() + + if(NOT GODOTCPP_TARGET STREQUAL "template_release") + add_compile_definitions(DEBUG_ENABLED) + endif() + if(GODOTCPP_DEV_BUILD) + add_compile_definitions(DEV_ENABLED) + endif() + endif() +endmacro() + +#[[ Directory-scope RTTI disable. The simulation stack (openvic-simulation, +openvic-dataloader, lexy-vdf) builds without RTTI; the extension itself keeps +RTTI on, so the OpenVic root must NOT call this. ]] +macro(openvic_disable_rtti) + if(NOT OPENVIC_RTTI_DISABLED) + set(OPENVIC_RTTI_DISABLED TRUE) + if(MSVC) + add_compile_options(/GR-) + else() + add_compile_options(-fno-rtti) + endif() + endif() +endmacro() diff --git a/cmake/OpenVicScripts.cmake b/cmake/OpenVicScripts.cmake new file mode 100644 index 0000000..3db9774 --- /dev/null +++ b/cmake/OpenVicScripts.cmake @@ -0,0 +1,18 @@ +# Shared CMake support for OpenVic projects (OpenVic, openvic-simulation, +# openvic-dataloader, lexy-vdf). Each repo fetches this `scripts` repo as a +# pinned FetchContent tarball in its root CMakeLists.txt bootstrap block and +# then includes this module, guarded by: +# +# if(NOT COMMAND openvic_setup_base_flags) +# +# In a composed build (a parent already loaded its own copy) the fetch and +# include are skipped, so the first-loaded (outermost) module version wins. +# To bump the pin in a consuming repo, see the procedure in README.md. + +include_guard(GLOBAL) + +find_package(Python3 COMPONENTS Interpreter REQUIRED) + +include("${CMAKE_CURRENT_LIST_DIR}/OpenVicFlags.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/OpenVicCodegen.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/OpenVicDeps.cmake") diff --git a/gen/codegen.py b/gen/codegen.py new file mode 100644 index 0000000..3733523 --- /dev/null +++ b/gen/codegen.py @@ -0,0 +1,781 @@ +#!/usr/bin/env python +"""Standalone code generators for the OpenVic CMake builds. + +Stdlib-only CLI generating the git/license/author info headers and the +foonathan/memory config headers. Outputs are written only when their content +changes so build systems don't see spurious mtime bumps. + +Subcommands: + commit-info gen/commit_info.gen.hpp (git tag/release/hash/timestamp) + license-info gen/license_info.gen.hpp (COPYRIGHT + LICENSE.md) + author-info gen/author_info.gen.hpp (AUTHORS.md sections) + memory-config foonathan/memory config_impl.hpp + memory-node-sizes foonathan/memory detail/container_node_sizes_impl.hpp +""" + +import argparse +import os +import subprocess +import sys +from collections import OrderedDict +from typing import List, Union + +# --- shared helpers --- + + +def to_raw_cstring(value: Union[str, List[str]]) -> str: + MAX_LITERAL = 16380 + + if isinstance(value, list): + value = "\n".join(value) + "\n" + + split: List[bytes] = [] + offset = 0 + encoded = value.encode() + + while offset <= len(encoded): + segment = encoded[offset : offset + MAX_LITERAL] + offset += MAX_LITERAL + if len(segment) == MAX_LITERAL: + # Try to segment raw strings at double newlines to keep readable. + pretty_break = segment.rfind(b"\n\n") + if pretty_break != -1: + segment = segment[: pretty_break + 1] + offset -= MAX_LITERAL - pretty_break - 1 + # If none found, ensure we end with valid utf8. + # https://github.com/halloleo/unicut/blob/master/truncate.py + elif segment[-1] & 0b10000000: + last_11xxxxxx_index = [i for i in range(-1, -5, -1) if segment[i] & 0b11000000 == 0b11000000][0] + last_11xxxxxx = segment[last_11xxxxxx_index] + if not last_11xxxxxx & 0b00100000: + last_char_length = 2 + elif not last_11xxxxxx & 0b0010000: + last_char_length = 3 + elif not last_11xxxxxx & 0b0001000: + last_char_length = 4 + + if last_char_length > -last_11xxxxxx_index: + segment = segment[:last_11xxxxxx_index] + offset += last_11xxxxxx_index + + split += [segment] + + if len(split) == 1: + return f'R"({split[0].decode()})"' + else: + # Wrap multiple segments in parenthesis to suppress `string-concatenation` warnings on clang. + return "({})".format(" ".join(f'R"({segment.decode()})"' for segment in split)) + + +C_ESCAPABLES = [ + ("\\", "\\\\"), + ("\a", "\\a"), + ("\b", "\\b"), + ("\f", "\\f"), + ("\n", "\\n"), + ("\r", "\\r"), + ("\t", "\\t"), + ("\v", "\\v"), + ('"', '\\"'), +] +C_ESCAPE_TABLE = str.maketrans(dict((x, y) for x, y in C_ESCAPABLES)) + + +def to_escaped_cstring(value: str) -> str: + return value.translate(C_ESCAPE_TABLE) + + +def write_if_changed(path: str, text: str, platform_newlines: bool = False) -> bool: + """Write text to path (creating parent dirs), skipping the write when the + on-disk bytes already match. Returns True when the file was (re)written.""" + if platform_newlines: + data = text.replace("\n", os.linesep).encode("utf-8") + else: + data = text.encode("utf-8") + + try: + with open(path, "rb") as file: + if file.read() == data: + return False + except OSError: + pass + + os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) + with open(path, "wb") as file: + file.write(data) + return True + + +# --- commit-info (ported from build/git_info.py) --- + + +def get_git_tag(prefix): + tag_var_name = f"{prefix}_TAG" + git_tag = "" + if tag_var_name in os.environ: + return os.environ[tag_var_name] + else: + git_tag = "" + + if os.path.exists(".git"): + try: + result = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"], encoding="utf-8").strip() + if result != "": + git_tag = result + except (subprocess.CalledProcessError, OSError): + # `git` not found in PATH. + pass + + return git_tag + + +def get_git_release(prefix): + release_var_name = f"{prefix}_RELEASE" + git_release = "" + if release_var_name in os.environ: + return os.environ[release_var_name] + else: + git_release = "" + + if os.path.exists(".git"): + try: + result = subprocess.check_output( + ["gh", "release", "list", "--json", "name", "-q", ".[0] | .name"], encoding="utf-8" + ).strip() + if result != "": + git_release = result + except (subprocess.CalledProcessError, OSError): + # `gh` not found in PATH. + git_tag = get_git_tag(prefix) + if git_tag != "": + git_release = git_tag + + return git_release + + +def get_git_hash(): + # Parse Git hash if we're in a Git repo. + git_hash = "0000000000000000000000000000000000000000" + git_folder = ".git" + + if os.path.isfile(".git"): + with open(".git", "r", encoding="utf-8") as file: + module_folder = file.readline().strip() + if module_folder.startswith("gitdir: "): + git_folder = module_folder[8:] + + if os.path.isfile(os.path.join(git_folder, "HEAD")): + with open(os.path.join(git_folder, "HEAD"), "r", encoding="utf8") as file: + head = file.readline().strip() + if head.startswith("ref: "): + ref = head[5:] + # If this directory is a Git worktree instead of a root clone. + parts = git_folder.split("/") + if len(parts) > 2 and parts[-2] == "worktrees": + git_folder = "/".join(parts[0:-2]) + head = os.path.join(git_folder, ref) + packedrefs = os.path.join(git_folder, "packed-refs") + if os.path.isfile(head): + with open(head, "r", encoding="utf-8") as file: + git_hash = file.readline().strip() + elif os.path.isfile(packedrefs): + # Git may pack refs into a single file. This code searches .git/packed-refs file for the current ref's hash. + # https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-pack-refs.html + for line in open(packedrefs, "r", encoding="utf-8").read().splitlines(): + if line.startswith("#"): + continue + (line_hash, line_ref) = line.split(" ") + if ref == line_ref: + git_hash = line_hash + break + else: + git_hash = head + + # Get the UNIX timestamp of the build commit. + git_timestamp = 0 + if os.path.exists(".git"): + try: + git_timestamp = subprocess.check_output( + ["git", "log", "-1", "--pretty=format:%ct", "--no-show-signature", git_hash], encoding="utf-8" + ) + except (subprocess.CalledProcessError, OSError): + # `git` not found in PATH. + pass + + return { + "git_hash": git_hash, + "git_timestamp": git_timestamp, + } + + +def generate_commit_info(args) -> bool: + os.chdir(args.repo_dir) + prefix_upper = args.prefix.upper() + git_info = {**get_git_hash(), "git_tag": get_git_tag(prefix_upper), "git_release": get_git_release(prefix_upper)} + + text = "/* THIS FILE IS GENERATED. EDITS WILL BE LOST. */\n\n" + text += f"""\ +#pragma once + +#include +#include + +namespace OpenVic {{ + static constexpr std::string_view {prefix_upper}_TAG = "{git_info["git_tag"]}"; + static constexpr std::string_view {prefix_upper}_RELEASE = "{git_info["git_release"]}"; + static constexpr std::string_view {prefix_upper}_COMMIT_HASH = "{git_info["git_hash"]}"; + static constexpr const uint64_t {prefix_upper}_COMMIT_TIMESTAMP = {git_info["git_timestamp"]}ull; +}} +""" + return write_if_changed(args.output, text) + + +# --- license-info (ported from build/license_info.py) --- + + +def get_license_info(src_copyright): + class LicenseReader: + def __init__(self, license_file): + self._license_file = license_file + self.line_num = 0 + self.current = self.next_line() + + def next_line(self): + line = self._license_file.readline() + self.line_num += 1 + while line.startswith("#"): + line = self._license_file.readline() + self.line_num += 1 + self.current = line + return line + + def next_tag(self): + if ":" not in self.current: + return ("", []) + tag, line = self.current.split(":", 1) + lines = [line.strip()] + while self.next_line() and self.current.startswith(" "): + lines.append(self.current.strip()) + return (tag, lines) + + projects = OrderedDict() + license_list = [] + + with open(src_copyright, "r", encoding="utf-8") as copyright_file: + reader = LicenseReader(copyright_file) + part = {} + while reader.current: + tag, content = reader.next_tag() + if tag in ("Files", "Copyright", "License"): + part[tag] = content[:] + elif tag == "Comment" and part: + # attach non-empty part to named project + projects[content[0]] = projects.get(content[0], []) + [part] + + if not tag or not reader.current: + # end of a paragraph start a new part + if "License" in part and "Files" not in part: + # no Files tag in this one, so assume standalone license + license_list.append(part["License"]) + part = {} + reader.next_line() + + data_list: list = [] + for project in iter(projects.values()): + for part in project: + part["file_index"] = len(data_list) + data_list += part["Files"] + part["copyright_index"] = len(data_list) + data_list += part["Copyright"] + + return {"data": data_list, "projects": projects, "parts": part, "licenses": license_list} + + +def generate_license_info(args) -> bool: + name_prefix = args.prefix + prefix_upper = name_prefix.upper() + prefix_capital = name_prefix.capitalize() + + license_text_name = f"{prefix_upper}_LICENSE_TEXT" + component_copyright_part_name = f"{prefix_capital}ComponentCopyrightPart" + component_copyright_name = f"{prefix_capital}ComponentCopyright" + copyright_data_name = f"{prefix_upper}_COPYRIGHT_DATA" + copyright_parts_name = f"{prefix_upper}_COPYRIGHT_PARTS" + copyright_info_name = f"{prefix_upper}_COPYRIGHT_INFO" + license_name = f"{prefix_capital}License" + licenses_name = f"{prefix_upper}_LICENSES" + + src_copyright = get_license_info(args.copyright) + + with open(args.license, "r", encoding="utf-8") as file: + license_text = file.read() + + def copyright_data_str() -> str: + result = "" + for line in src_copyright["data"]: + result += f'\t\t"{line}",\n' + return result + + part_indexes = {} + + def copyright_part_str() -> str: + part_index = 0 + result = "" + for project_name, project in iter(src_copyright["projects"].items()): + part_indexes[project_name] = part_index + for part in project: + result += ( + f'\t\t{{ "{to_escaped_cstring(part["License"][0])}", ' + + f"{{ &{copyright_data_name}[{part['file_index']}], {len(part['Files'])} }}, " + + f"{{ &{copyright_data_name}[{part['copyright_index']}], {len(part['Copyright'])} }} }},\n" + ) + part_index += 1 + return result + + def copyright_info_str() -> str: + result = "" + for project_name, project in iter(src_copyright["projects"].items()): + result += ( + f'\t\t{{ "{to_escaped_cstring(project_name)}", ' + + f"{{ &{copyright_parts_name}[{part_indexes[project_name]}], {len(project)} }} }},\n" + ) + return result + + def license_list_str() -> str: + result = "" + for license in iter(src_copyright["licenses"]): + result += ( + f'\t\t{{ "{to_escaped_cstring(license[0])}",' + + f'\n\t\t {to_raw_cstring([line if line != "." else "" for line in license[1:]])} }}, \n' + ) + return result + + text = "/* THIS FILE IS GENERATED. EDITS WILL BE LOST. */\n\n" + text += f"""\ +#pragma once + +#include +#include +#include + +namespace OpenVic {{ + static constexpr std::string_view {license_text_name} = {{ + {to_raw_cstring(license_text)} + }}; + + struct {component_copyright_part_name} {{ + std::string_view license; + std::span files; + std::span copyright_statements; + }}; + + struct {component_copyright_name} {{ + std::string_view name; + std::span parts; + }}; + + static constexpr std::array {copyright_data_name} = std::to_array({{ +{copyright_data_str()}\t}}); + + static constexpr std::array {copyright_parts_name} = std::to_array<{component_copyright_part_name}>({{ +{copyright_part_str()}\t}}); + + static constexpr std::array {copyright_info_name} = std::to_array<{component_copyright_name}>({{ +{copyright_info_str()}\t}}); + + struct {license_name} {{ + std::string_view license_name; + std::string_view license_body; + }}; + + static constexpr std::array {licenses_name} = std::to_array<{license_name}>({{ +{license_list_str()}\t}}); +}} +""" + return write_if_changed(args.output, text) + + +# --- author-info (ported from build/author_info.py) --- + + +def generate_author_info(args) -> bool: + prefix_upper = args.prefix.upper() + sections = OrderedDict() + for section in args.sections or ["Developers=AUTHORS_DEVELOPERS"]: + heading, _, macro = section.partition("=") + sections[heading] = macro + + with open(args.authors, "rb") as file: + buffer = file.read() + + reading = False + lines_out = [] + + text = "/* THIS FILE IS GENERATED. EDITS WILL BE LOST. */\n\n" + text += """\ +#pragma once + +#include +#include + +namespace OpenVic { +""" + + def close_section(): + lines_out.append("\t});\n") + + for line in buffer.decode().splitlines(): + if line.startswith(" ") and reading: + lines_out.append(f'\t\t"{to_escaped_cstring(line).strip()}",\n') + elif line.startswith("## "): + if reading: + close_section() + lines_out.append("\n") + reading = False + section = sections.get(line[3:].strip(), None) + if section: + lines_out.append( + f"\tstatic constexpr std::array {prefix_upper}_{section} = std::to_array({{\n" + ) + reading = True + + if reading: + close_section() + + text += "".join(lines_out) + text += "}" + return write_if_changed(args.output, text) + + +# --- memory-config / memory-node-sizes (ported from openvic-simulation deps/methods.py) --- + + +def generate_memory_config(args) -> bool: + header = [] + + header.append("// THIS FILE IS GENERATED. EDITS WILL BE LOST.") + header.append("") + + header += """ +// Copyright (C) 2015-2025 Jonathan Müller and foonathan/memory contributors +// SPDX-License-Identifier: Zlib + +#ifndef FOONATHAN_MEMORY_IMPL_IN_CONFIG_HPP +#error "do not include this file directly, use config.hpp" +#endif + +#include + +//=== options ===// +// clang-format off +""".split("\n") + + for define in args.defines: + key, _, val = define.partition("=") + header.append(f"#define {key} {val}") + + header.append("// clang-format on") + header.append("") + + return write_if_changed(args.output, "\n".join(header), platform_newlines=True) + + +def generate_memory_node_sizes(args) -> bool: + header = [] + + header.append("// THIS FILE IS GENERATED. EDITS WILL BE LOST.") + header.append("") + + header += """ +namespace detail +{ + template + struct alignment_type + { + using type = void; + static_assert(Alignment == Alignment); + }; + + template <> + struct alignment_type<1> + { + using type = char; + static_assert(alignof(type) == 1); + }; + + template <> + struct alignment_type<2> + { + using type = short; + static_assert(alignof(type) == 2); + }; + + template <> + struct alignment_type<4> + { + using type = int; + static_assert(alignof(type) == 4); + }; + + template <> + struct alignment_type<8> + { + using type = std::conditional_t; + static_assert(alignof(type) == 8); + }; + + template <> + struct alignment_type<16> + { + using type = + std::conditional_t::type>; + }; + + template + using alignment_type_t = alignment_type::type; + + template + static consteval std::size_t calculate_node_size_by_type() + { + static_assert(!std::is_same::value && (sizeof(InitialType) != sizeof(T))); + static_assert(sizeof(T) > sizeof(InitialType)); + + return sizeof(T) - (SubtractTSize ? sizeof(InitialType) : 0); + } + + template + static consteval std::size_t calculate_node_size() + { + return calculate_node_size_by_type, T, SubtractTSize>(); + } + + template + using allocator_type = std::allocator>; +} +""".split("\n") + + containers = { + "forward_list": [ + "calculate_node_size, void*>>()", + "calculate_node_size>>()", + "calculate_node_size, void*>>()", + ], + "list": [ + "calculate_node_size, void*>>()", + "calculate_node_size>>()", + "calculate_node_size, void*>>()", + ], + "set": [ + "calculate_node_size, void*>>()", + "calculate_node_size>>()", + "calculate_node_size, void*>>()", + ], + "multiset": [], + "unordered_set": [ + "calculate_node_size, void*>>()", + "calculate_node_size, true>>()", + "calculate_node_size, void*>>()", + ], + "unordered_multiset": [], + "map": [ + """ +calculate_node_size_by_type< + std::__1::pair, alignment_type_t>, + std::__1::__tree_node< + std::__1::__value_type, alignment_type_t>, + void*>>() +""", + """ +calculate_node_size_by_type< + std::pair, alignment_type_t>, + std::_Rb_tree_node< + std::pair, alignment_type_t>>>() +""", + """ +calculate_node_size_by_type< + std::pair, alignment_type_t>, + std::_Tree_node< + std::pair, alignment_type_t>, + void*>>() +""", + ], + "multimap": [], + "unordered_map": [ + """ +calculate_node_size_by_type< + std::__1::pair, alignment_type_t>, + std::__1::__hash_node, + alignment_type_t>, + void*>>() +""", + """ +calculate_node_size_by_type< + std::pair, alignment_type_t>, + std::__detail::_Hash_node< + std::pair, alignment_type_t>, + true>>() +""", + """ +calculate_node_size_by_type< + std::pair, alignment_type_t>, + std::_List_node< + std::pair, alignment_type_t>, + void*>>() +""", + ], + "unordered_multimap": [], + "shared_ptr_stateless": [ + """ +calculate_node_size, + std::allocator>>, + false>() +""", + """ +calculate_node_size, + std::allocator>, + __gnu_cxx::_S_atomic>, + false>() +""", + """ +calculate_node_size< + Alignment, + std::_Ref_count_obj_alloc3>, + allocator_type>, + false>() +""", + ], + "shared_ptr_stateful": [ + """ +calculate_node_size, + std::pmr::polymorphic_allocator>>, + false>() +""", + """ +calculate_node_size, + std::pmr::polymorphic_allocator>, + __gnu_cxx::_S_atomic>, + false>() +""", + """ +calculate_node_size>, + std::pmr::polymorphic_allocator>>, + false>() +""", + ], + } + + container_names = list(containers.keys()) + for i, container_name in enumerate(container_names): + data = containers[container_name] + if len(data) == 0: + data = containers[container_names[i - 1]] + + header += f"""namespace detail +{{ +#ifdef _LIBCPP_VERSION + template + struct {container_name}_node_size + : std::integral_constant< + std::size_t,{data[0]}> + {{ + }}; +#elif defined(__GLIBCXX__) + template + struct {container_name}_node_size + : std::integral_constant< + std::size_t,{data[1]}> + {{ + }}; +#elif defined(_MSC_VER) + template + struct {container_name}_node_size + : std::integral_constant< + std::size_t,{data[2]}> + {{ + }}; +#else + template + struct {container_name}_node_size + : std::integral_constant< + std::size_t,0> + {{ + static_assert(Alignment == Alignment, "{container_name}_node_size not supported."); + }}; +#endif +}} + +template +struct {container_name}_node_size +: std::integral_constant::value + sizeof(T), alignof(void*))> +{{}}; +""".split("\n") + + return write_if_changed(args.output, "\n".join(header), platform_newlines=True) + + +# --- CLI --- + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + subparsers = parser.add_subparsers(dest="command", required=True) + + p = subparsers.add_parser("commit-info", help="Generate commit_info.gen.hpp") + p.add_argument("--prefix", required=True, help="Macro name prefix, e.g. game or sim") + p.add_argument("--repo-dir", required=True, help="Repository root to read git metadata from") + p.add_argument("-o", "--output", required=True) + p.set_defaults(func=generate_commit_info) + + p = subparsers.add_parser("license-info", help="Generate license_info.gen.hpp") + p.add_argument("--prefix", required=True) + p.add_argument("--copyright", required=True, help="Debian-style COPYRIGHT file") + p.add_argument("--license", required=True, help="LICENSE.md file") + p.add_argument("-o", "--output", required=True) + p.set_defaults(func=generate_license_info) + + p = subparsers.add_parser("author-info", help="Generate author_info.gen.hpp") + p.add_argument("--prefix", required=True) + p.add_argument("--authors", required=True, help="AUTHORS.md file") + p.add_argument( + "--section", + dest="sections", + action="append", + metavar="HEADING=MACRO", + help="Markdown section to macro suffix mapping, in order (repeatable)", + ) + p.add_argument("-o", "--output", required=True) + p.set_defaults(func=generate_author_info) + + p = subparsers.add_parser("memory-config", help="Generate foonathan/memory config_impl.hpp") + p.add_argument( + "--define", + dest="defines", + action="append", + required=True, + metavar="KEY=VALUE", + help="Config define (repeatable)", + ) + p.add_argument("-o", "--output", required=True) + p.set_defaults(func=generate_memory_config) + + p = subparsers.add_parser("memory-node-sizes", help="Generate foonathan/memory container_node_sizes_impl.hpp") + p.add_argument("-o", "--output", required=True) + p.set_defaults(func=generate_memory_node_sizes) + + args = parser.parse_args() + args.func(args) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/gen/commit_info.gen.hpp.in b/gen/commit_info.gen.hpp.in new file mode 100644 index 0000000..6428412 --- /dev/null +++ b/gen/commit_info.gen.hpp.in @@ -0,0 +1,13 @@ +/* THIS FILE IS GENERATED. EDITS WILL BE LOST. */ + +#pragma once + +#include +#include + +namespace OpenVic { + static constexpr std::string_view @PREFIX_UPPER@_TAG = "@GIT_TAG@"; + static constexpr std::string_view @PREFIX_UPPER@_RELEASE = "@GIT_RELEASE@"; + static constexpr std::string_view @PREFIX_UPPER@_COMMIT_HASH = "@GIT_HASH@"; + static constexpr const uint64_t @PREFIX_UPPER@_COMMIT_TIMESTAMP = @GIT_TIMESTAMP@ull; +} diff --git a/pyproject.toml b/pyproject.toml index 383a144..705d8e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ namespace_packages = true explicit_package_bases = true [tool.ruff] -extend-include = ["SConstruct", "SCsub"] line-length = 120 target-version = "py37" @@ -20,12 +19,6 @@ extend-select = [ "I", # isort ] -[tool.ruff.lint.per-file-ignores] -"{SConstruct,SCsub}" = [ - "E402", # Module level import not at top of file - "F821", # Undefined name -] - [tool.typos] files.extend-exclude = [".mailmap", "*.gitignore", "*.po", "*.pot", "*.rc"] default.extend-ignore-re = [ diff --git a/tools/linux.py b/tools/linux.py deleted file mode 100644 index f776bf9..0000000 --- a/tools/linux.py +++ /dev/null @@ -1,94 +0,0 @@ -# Based on https://github.com/godotengine/godot-cpp/blob/e83fd0904c13356ed1d4c3d09f8bb9132bdc6b77/tools/linux.py -from build import common_compiler_flags -from SCons.Variables import BoolVariable -from SCons.Tool import clang, clangxx - - -def options(opts): - opts.Add(BoolVariable("use_llvm", "Use the LLVM compiler - only effective when targeting Linux", False)) - opts.Add(BoolVariable("use_static_cpp", "Link libgcc and libstdc++ statically for better portability", True)) - opts.Add(BoolVariable("use_ubsan", "Use LLVM/GCC compiler undefined behavior sanitizer (UBSAN)", False)) - opts.Add(BoolVariable("use_asan", "Use LLVM/GCC compiler address sanitizer (ASAN)", False)) - opts.Add(BoolVariable("use_lsan", "Use LLVM/GCC compiler leak sanitizer (LSAN)", False)) - opts.Add(BoolVariable("use_tsan", "Use LLVM/GCC compiler thread sanitizer (TSAN)", False)) - opts.Add(BoolVariable("use_msan", "Use LLVM compiler memory sanitizer (MSAN)", False)) - - -def exists(env): - return True - - -def generate(env): - if env["use_llvm"]: - clang.generate(env) - clangxx.generate(env) - elif env.use_hot_reload: - # Required for hot reload support. - env.Append(CXXFLAGS=["-fno-gnu-unique"]) - - env.Append(CCFLAGS=["-fPIC", "-Wwrite-strings"]) - env.Append(LINKFLAGS=["-Wl,-R,'$$ORIGIN'"]) - - if env["arch"] == "x86_64": - # -m64 and -m32 are x86-specific already, but it doesn't hurt to - # be clear and also specify -march=x86-64. Similar with 32-bit. - env.Append(CCFLAGS=["-m64", "-march=x86-64"]) - env.Append(LINKFLAGS=["-m64", "-march=x86-64"]) - elif env["arch"] == "x86_32": - env.Append(CCFLAGS=["-m32", "-march=i686"]) - env.Append(LINKFLAGS=["-m32", "-march=i686"]) - elif env["arch"] == "arm64": - env.Append(CCFLAGS=["-march=armv8-a"]) - env.Append(LINKFLAGS=["-march=armv8-a"]) - elif env["arch"] == "rv64": - env.Append(CCFLAGS=["-march=rv64gc"]) - env.Append(LINKFLAGS=["-march=rv64gc"]) - - # Link statically for portability - if env["use_static_cpp"]: - env.Append(LINKFLAGS=["-static-libgcc", "-static-libstdc++"]) - - if env["use_ubsan"] or env["use_asan"] or env["use_lsan"] or env["use_tsan"] or env["use_msan"]: - env.extra_suffix += ".san" - env.Append(CCFLAGS=["-DSANITIZERS_ENABLED"]) - - if env["use_ubsan"]: - env.Append( - CCFLAGS=[ - "-fsanitize=undefined,shift,shift-exponent,integer-divide-by-zero,unreachable,vla-bound,null,return,signed-integer-overflow,bounds,float-divide-by-zero,float-cast-overflow,nonnull-attribute,returns-nonnull-attribute,bool,enum,vptr,pointer-overflow,builtin" - ] - ) - env.Append(LINKFLAGS=["-fsanitize=undefined"]) - if env["use_llvm"]: - env.Append( - CCFLAGS=[ - "-fsanitize=nullability-return,nullability-arg,function,nullability-assign,implicit-integer-sign-change" - ] - ) - else: - env.Append(CCFLAGS=["-fsanitize=bounds-strict"]) - - if env["use_asan"]: - env.Append(CCFLAGS=["-fsanitize=address,pointer-subtract,pointer-compare"]) - env.Append(LINKFLAGS=["-fsanitize=address"]) - - if env["use_lsan"]: - env.Append(CCFLAGS=["-fsanitize=leak"]) - env.Append(LINKFLAGS=["-fsanitize=leak"]) - - if env["use_tsan"]: - env.Append(CCFLAGS=["-fsanitize=thread"]) - env.Append(LINKFLAGS=["-fsanitize=thread"]) - - if env["use_msan"] and env["use_llvm"]: - env.Append(CCFLAGS=["-fsanitize=memory"]) - env.Append(CCFLAGS=["-fsanitize-memory-track-origins"]) - env.Append(CCFLAGS=["-fsanitize-recover=memory"]) - env.Append(LINKFLAGS=["-fsanitize=memory"]) - - env.Append(CPPDEFINES=["LINUX_ENABLED", "UNIX_ENABLED"]) - - if env["lto"] == "auto": - env["lto"] = "full" - - common_compiler_flags.generate(env) diff --git a/tools/macos.py b/tools/macos.py deleted file mode 100644 index 35b96b5..0000000 --- a/tools/macos.py +++ /dev/null @@ -1,105 +0,0 @@ -# Based on https://github.com/godotengine/godot-cpp/blob/98ea2f60bb3846d6ae410d8936137d1b099cd50b/tools/macos.py -import os -import sys - -from build import common_compiler_flags -from SCons.Variables import BoolVariable - - -def has_osxcross(): - return "OSXCROSS_ROOT" in os.environ - - -def options(opts): - opts.Add("macos_deployment_target", "macOS deployment target", "default") - opts.Add("macos_sdk_path", "macOS SDK path", "") - if has_osxcross(): - opts.Add("osxcross_sdk", "OSXCross SDK version", "darwin16") - opts.Add(BoolVariable("use_ubsan", "Use LLVM/GCC compiler undefined behavior sanitizer (UBSAN)", False)) - opts.Add(BoolVariable("use_asan", "Use LLVM/GCC compiler address sanitizer (ASAN)", False)) - opts.Add(BoolVariable("use_tsan", "Use LLVM/GCC compiler thread sanitizer (TSAN)", False)) - - -def exists(env): - return sys.platform == "darwin" or has_osxcross() - - -def generate(env): - if env["arch"] not in ("universal", "arm64", "x86_64"): - print("Only universal, arm64, and x86_64 are supported on macOS. Exiting.") - env.Exit(1) - - if sys.platform == "darwin": - # Use clang on macOS by default - env["CXX"] = "clang++" - env["CC"] = "clang" - else: - # OSXCross - root = os.environ.get("OSXCROSS_ROOT", "") - if env["arch"] == "arm64": - basecmd = root + "/target/bin/arm64-apple-" + env["osxcross_sdk"] + "-" - else: - basecmd = root + "/target/bin/x86_64-apple-" + env["osxcross_sdk"] + "-" - - env["CC"] = basecmd + "clang" - env["CXX"] = basecmd + "clang++" - env["AR"] = basecmd + "ar" - env["RANLIB"] = basecmd + "ranlib" - env["AS"] = basecmd + "as" - - binpath = os.path.join(root, "target", "bin") - if binpath not in env["ENV"]["PATH"]: - # Add OSXCROSS bin folder to PATH (required for linking). - env.PrependENVPath("PATH", binpath) - - # Common flags - if env["arch"] == "universal": - env.Append(LINKFLAGS=["-arch", "x86_64", "-arch", "arm64"]) - env.Append(CCFLAGS=["-arch", "x86_64", "-arch", "arm64"]) - else: - env.Append(LINKFLAGS=["-arch", env["arch"]]) - env.Append(CCFLAGS=["-arch", env["arch"]]) - - if env["macos_deployment_target"] != "default": - env.Append(CCFLAGS=["-mmacosx-version-min=" + env["macos_deployment_target"]]) - env.Append(LINKFLAGS=["-mmacosx-version-min=" + env["macos_deployment_target"]]) - - if env["macos_sdk_path"]: - env.Append(CCFLAGS=["-isysroot", env["macos_sdk_path"]]) - env.Append(LINKFLAGS=["-isysroot", env["macos_sdk_path"]]) - - env.Append( - LINKFLAGS=[ - "-framework", - "Foundation", - "-Wl,-undefined,dynamic_lookup", - ] - ) - - if env["use_ubsan"] or env["use_asan"] or env["use_tsan"]: - env.extra_suffix += ".san" - env.Append(CCFLAGS=["-DSANITIZERS_ENABLED"]) - - if env["use_ubsan"]: - env.Append( - CCFLAGS=[ - "-fsanitize=undefined,shift,shift-exponent,integer-divide-by-zero,unreachable,vla-bound,null,return,signed-integer-overflow,bounds,float-divide-by-zero,float-cast-overflow,nonnull-attribute,returns-nonnull-attribute,bool,enum,vptr,pointer-overflow,builtin" - ] - ) - env.Append(LINKFLAGS=["-fsanitize=undefined"]) - env.Append(CCFLAGS=["-fsanitize=nullability-return,nullability-arg,function,nullability-assign"]) - - if env["use_asan"]: - env.Append(CCFLAGS=["-fsanitize=address,pointer-subtract,pointer-compare"]) - env.Append(LINKFLAGS=["-fsanitize=address"]) - - if env["use_tsan"]: - env.Append(CCFLAGS=["-fsanitize=thread"]) - env.Append(LINKFLAGS=["-fsanitize=thread"]) - - env.Append(CPPDEFINES=["MACOS_ENABLED", "UNIX_ENABLED"]) - - if env["lto"] == "auto": - env["lto"] = "none" - - common_compiler_flags.generate(env) diff --git a/tools/macos_osxcross.py b/tools/macos_osxcross.py deleted file mode 100644 index 8ed9a5d..0000000 --- a/tools/macos_osxcross.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copied from https://github.com/godotengine/godot-cpp/blob/0ee980abae91c481009152cdccab8e61c9625303/tools/macos_osxcross.py -import os - - -def options(opts): - opts.Add("osxcross_sdk", "OSXCross SDK version", "darwin16") - - -def exists(env): - return "OSXCROSS_ROOT" in os.environ - - -def generate(env): - root = os.environ.get("OSXCROSS_ROOT", "") - if env["arch"] == "arm64": - basecmd = root + "/target/bin/arm64-apple-" + env["osxcross_sdk"] + "-" - else: - basecmd = root + "/target/bin/x86_64-apple-" + env["osxcross_sdk"] + "-" - - env["CC"] = basecmd + "clang" - env["CXX"] = basecmd + "clang++" - env["AR"] = basecmd + "ar" - env["RANLIB"] = basecmd + "ranlib" - env["AS"] = basecmd + "as" - - binpath = os.path.join(root, "target", "bin") - if binpath not in env["ENV"]["PATH"]: - # Add OSXCROSS bin folder to PATH (required for linking). - env["ENV"]["PATH"] = "%s:%s" % (binpath, env["ENV"]["PATH"]) diff --git a/tools/my_spawn.py b/tools/my_spawn.py deleted file mode 100644 index 915f972..0000000 --- a/tools/my_spawn.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copied from https://github.com/godotengine/godot-cpp/blob/93f2091185ff4390ca8fc8901ebc68ebc35a218f/tools/my_spawn.py -import os - - -def exists(env): - return os.name == "nt" - - -# Workaround for MinGW. See: -# http://www.scons.org/wiki/LongCmdLinesOnWin32 -def configure(env): - import subprocess - - def mySubProcess(cmdline, env): - # print "SPAWNED : " + cmdline - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - proc = subprocess.Popen( - cmdline, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - startupinfo=startupinfo, - shell=False, - env=env, - ) - data, err = proc.communicate() - rv = proc.wait() - if rv: - print("=====") - print(err.decode("utf-8")) - print("=====") - return rv - - def mySpawn(sh, escape, cmd, args, env): - - newargs = " ".join(args[1:]) - cmdline = cmd + " " + newargs - - rv = 0 - if len(cmdline) > 32000 and cmd.endswith("ar"): - cmdline = cmd + " " + args[1] + " " + args[2] + " " - for i in range(3, len(args)): - rv = mySubProcess(cmdline + args[i], env) - if rv: - break - else: - rv = mySubProcess(cmdline, env) - - return rv - - env["SPAWN"] = mySpawn - env.Replace(ARFLAGS=["q"]) diff --git a/tools/targets.py b/tools/targets.py deleted file mode 100644 index 1a562aa..0000000 --- a/tools/targets.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copied from https://github.com/godotengine/godot-cpp/blob/df5b1a9a692b0d972f5ac3c853371594cdec420b/tools/targets.py -import os -import subprocess -import sys -from SCons.Script import ARGUMENTS -from SCons.Variables import EnumVariable, BoolVariable -from SCons.Variables.BoolVariable import _text2bool - - -# Helper methods - - -def get_cmdline_bool(option, default): - """We use `ARGUMENTS.get()` to check if options were manually overridden on the command line, - and SCons' _text2bool helper to convert them to booleans, otherwise they're handled as strings. - """ - cmdline_val = ARGUMENTS.get(option) - if cmdline_val is not None: - return _text2bool(cmdline_val) - else: - return default - - -def using_clang(env): - return "clang" in os.path.basename(env["CC"]) - - -def is_vanilla_clang(env): - if not using_clang(env): - return False - try: - version = subprocess.check_output([env.subst(env["CXX"]), "--version"]).strip().decode("utf-8") - except (subprocess.CalledProcessError, OSError): - print("Couldn't parse CXX environment variable to infer compiler version.") - return False - return not version.startswith("Apple") - - -# Main tool definition - - -def options(opts): - opts.Add( - EnumVariable( - "optimize", - "Optimization level (by default inferred from 'target' and 'dev_build')", - "auto", - ["auto", "none", "custom", "debug", "speed", "speed_trace", "size"], - ignorecase=2, - ) - ) - opts.Add( - EnumVariable( - "lto", - "Link-time optimization", - "none", - ("none", "auto", "thin", "full"), - ) - ) - opts.Add(BoolVariable("debug_symbols", "Build with debugging symbols", True)) - opts.Add(BoolVariable("dev_build", "Developer build with dev-only debugging code (DEV_ENABLED)", False)) - opts.Add(EnumVariable( - "harden_memory", - "Library memory hardening (by default inferred from 'dev_build')", - "auto", - ["auto", "none", "fast"], - ignorecase=2 - ) - ) - - -def exists(env): - return True - - -def generate(env): - # Configuration of build targets: - # - Editor or template - # - Debug features (DEBUG_ENABLED code) - # - Dev only code (DEV_ENABLED code) - # - Optimization level - # - Debug symbols for crash traces / debuggers - - # Keep this configuration in sync with SConstruct in upstream Godot. - - env.use_hot_reload = env.get("use_hot_reload", env["target"] != "template_release") - env.editor_build = env["target"] == "editor" - env.dev_build = env["dev_build"] - env.debug_features = env["target"] in ["editor", "template_debug"] - - if env["optimize"] == "auto": - if env.dev_build: - opt_level = "none" - elif env.debug_features: - opt_level = "speed_trace" - else: # Release - opt_level = "speed" - env["optimize"] = ARGUMENTS.get("optimize", opt_level) - - if env["harden_memory"] == "auto": - if env.dev_build: - harden_level = "none" - else: - harden_level = "fast" - env["harden_memory"] = ARGUMENTS.get("harden_memory", harden_level) - - env["debug_symbols"] = get_cmdline_bool("debug_symbols", env.dev_build) - - if env.use_hot_reload: - env.Append(CPPDEFINES=["HOT_RELOAD_ENABLED"]) - - if env.editor_build: - env.Append(CPPDEFINES=["TOOLS_ENABLED"]) - - if env.debug_features: - # DEBUG_ENABLED enables debugging *features* and debug-only code, which is intended - # to give *users* extra debugging information for their game development. - env.Append(CPPDEFINES=["DEBUG_ENABLED"]) - - if env.dev_build: - # DEV_ENABLED enables *engine developer* code which should only be compiled for those - # working on the engine itself. - env.Append(CPPDEFINES=["DEV_ENABLED"]) - else: - # Disable assert() for production targets (only used in thirdparty code). - env.Append(CPPDEFINES=["NDEBUG"]) diff --git a/tools/windows.py b/tools/windows.py deleted file mode 100644 index dbe7222..0000000 --- a/tools/windows.py +++ /dev/null @@ -1,233 +0,0 @@ -# Based on https://github.com/godotengine/godot-cpp/blob/98ea2f60bb3846d6ae410d8936137d1b099cd50b/tools/windows.py -import os -import sys - -from build import common_compiler_flags -import my_spawn -from SCons.Tool import mingw, msvc -from SCons.Variables import BoolVariable - - -def silence_msvc(env): - import os - import re - import tempfile - - # Ensure we have a location to write captured output to, in case of false positives. - capture_path = os.path.join(os.path.dirname(__file__), "..", "msvc_capture.log") - with open(capture_path, "wt", encoding="utf-8"): - pass - - old_spawn = env["SPAWN"] - re_redirect_stream = re.compile(r"^[12]?>") - re_cl_capture = re.compile(r"^.+\.(c|cc|cpp|cxx|c[+]{2})$", re.IGNORECASE) - re_link_capture = re.compile(r'\s{3}\S.+\s(?:"[^"]+.lib"|\S+.lib)\s.+\s(?:"[^"]+.exp"|\S+.exp)') - - def spawn_capture(sh, escape, cmd, args, env): - # We only care about cl/link, process everything else as normal. - if args[0] not in ["cl", "link"]: - return old_spawn(sh, escape, cmd, args, env) - - # Process as normal if the user is manually rerouting output. - for arg in args: - if re_redirect_stream.match(arg): - return old_spawn(sh, escape, cmd, args, env) - - tmp_stdout, tmp_stdout_name = tempfile.mkstemp() - os.close(tmp_stdout) - args.append(f">{tmp_stdout_name}") - ret = old_spawn(sh, escape, cmd, args, env) - - try: - with open(tmp_stdout_name, "r", encoding=sys.stdout.encoding, errors="replace") as tmp_stdout: - lines = tmp_stdout.read().splitlines() - os.remove(tmp_stdout_name) - except OSError: - pass - - # Early process no lines (OSError) - if not lines: - return ret - - is_cl = args[0] == "cl" - content = "" - caught = False - for line in lines: - # These conditions are far from all-encompassing, but are specialized - # for what can be reasonably expected to show up in the repository. - if not caught and (is_cl and re_cl_capture.match(line)) or (not is_cl and re_link_capture.match(line)): - caught = True - try: - with open(capture_path, "a", encoding=sys.stdout.encoding) as log: - log.write(line + "\n") - except OSError: - print(f'WARNING: Failed to log captured line: "{line}".') - continue - content += line + "\n" - # Content remaining assumed to be an error/warning. - if content: - sys.stderr.write(content) - - return ret - - env["SPAWN"] = spawn_capture - - -def options(opts): - mingw = os.getenv("MINGW_PREFIX", "") - - opts.Add(BoolVariable("use_mingw", "Use the MinGW compiler instead of MSVC - only effective on Windows", False)) - opts.Add(BoolVariable("use_static_cpp", "Link MinGW/MSVC C++ runtime libraries statically", False)) - opts.Add(BoolVariable("silence_msvc", "Silence MSVC's cl/link stdout bloat, redirecting errors to stderr.", True)) - opts.Add(BoolVariable("debug_crt", "Compile with MSVC's debug CRT (/MDd)", False)) - opts.Add(BoolVariable("use_llvm", "Use the LLVM compiler (MVSC or MinGW depending on the use_mingw flag)", False)) - opts.Add("mingw_prefix", "MinGW prefix", mingw) - opts.Add(BoolVariable("use_asan", "Use address sanitizer (ASAN)", False)) - - -def exists(env): - return True - - -def generate(env): - base = None - - msvc_found = msvc.exists(env) - mingw_found = mingw.exists(env) - - if not msvc_found and not mingw_found: - print("Could not find installation of msvc or mingw, please properly install (or reinstall) MSVC with C++ or Mingw first.") - env.Exit(1) - - if not env["use_mingw"] and msvc_found: - if env["arch"] == "x86_64": - env["TARGET_ARCH"] = "amd64" - elif env["arch"] == "arm64": - env["TARGET_ARCH"] = "arm64" - elif env["arch"] == "arm32": - env["TARGET_ARCH"] = "arm" - elif env["arch"] == "x86_32": - env["TARGET_ARCH"] = "x86" - - env["MSVC_SETUP_RUN"] = False # Need to set this to re-run the tool - env["MSVS_VERSION"] = None - env["MSVC_VERSION"] = None - - env["is_msvc"] = True - - # MSVC, linker, and archiver. - msvc.generate(env) - env.Tool("msvc") - env.Tool("mslib") - env.Tool("mslink") - - env.Append(CPPDEFINES=["TYPED_METHOD_BIND", "NOMINMAX"]) - env.Append(CCFLAGS=["/utf-8", "/Zc:preprocessor"]) - env.Append(LINKFLAGS=["/WX"]) - - if env["use_llvm"]: - env["CC"] = "clang-cl" - env["CXX"] = "clang-cl" - - if env["debug_crt"]: - # Always use dynamic runtime, static debug CRT breaks thread_local. - env.AppendUnique(CCFLAGS=["/MDd"]) - else: - if env["use_static_cpp"]: - env.AppendUnique(CCFLAGS=["/MT"]) - else: - env.AppendUnique(CCFLAGS=["/MD"]) - - if env["silence_msvc"] and not env.GetOption("clean"): - silence_msvc(env) - - elif (sys.platform == "win32" or sys.platform == "msys") and not env["mingw_prefix"]: - env["use_mingw"] = True - mingw.generate(env) - # Don't want lib prefixes - env["IMPLIBPREFIX"] = "" - env["SHLIBPREFIX"] = "" - # Want dll suffix - env["SHLIBSUFFIX"] = ".dll" - - env.Append(CCFLAGS=["-Wwrite-strings"]) - env.Append(LINKFLAGS=["-Wl,--no-undefined"]) - if env["use_static_cpp"]: - env.Append( - LINKFLAGS=[ - "-static", - "-static-libgcc", - "-static-libstdc++", - ] - ) - - # Long line hack. Use custom spawn, quick AR append (to avoid files with the same names to override each other). - my_spawn.configure(env) - - else: - env["use_mingw"] = True - # Cross-compilation using MinGW - prefix = "" - if env["mingw_prefix"]: - prefix = env["mingw_prefix"] + "/bin/" - - if env["arch"] == "x86_64": - prefix += "x86_64" - elif env["arch"] == "arm64": - prefix += "aarch64" - elif env["arch"] == "arm32": - prefix += "armv7" - elif env["arch"] == "x86_32": - prefix += "i686" - - if env["use_llvm"]: - env["CXX"] = prefix + "-w64-mingw32-clang++" - env["CC"] = prefix + "-w64-mingw32-clang" - env["AR"] = prefix + "-w64-mingw32-llvm-ar" - env["RANLIB"] = prefix + "-w64-mingw32-ranlib" - env["LINK"] = prefix + "-w64-mingw32-clang" - else: - env["CXX"] = prefix + "-w64-mingw32-g++" - env["CC"] = prefix + "-w64-mingw32-gcc" - env["AR"] = prefix + "-w64-mingw32-gcc-ar" - env["RANLIB"] = prefix + "-w64-mingw32-ranlib" - env["LINK"] = prefix + "-w64-mingw32-g++" - - # Want dll suffix - env["SHLIBSUFFIX"] = ".dll" - - env.Append(CCFLAGS=["-Wwrite-strings"]) - env.Append(LINKFLAGS=["-Wl,--no-undefined"]) - if env["use_static_cpp"]: - env.Append( - LINKFLAGS=[ - "-static", - "-static-libgcc", - "-static-libstdc++", - ] - ) - if env["use_llvm"]: - env.Append(LINKFLAGS=["-lstdc++"]) - - if sys.platform == "win32" or sys.platform == "msys": - my_spawn.configure(env) - - if env["use_mingw"] and not mingw_found: - print("'use_mingw' set but Mingw is not installed, please install Mingw first.") - env.Exit(1) - - if env["use_asan"]: - env.extra_suffix += ".san" - env.Append(LINKFLAGS=["/INFERASANLIBS"]) - env.Append(CCFLAGS=["/fsanitize=address"]) - - env.Append(CPPDEFINES=["WINDOWS_ENABLED"]) - - if env["lto"] == "auto": - if env.get("is_msvc", False): - # No LTO by default for MSVC, doesn't help. - env["lto"] = "none" - else: # Release - env["lto"] = "full" - - common_compiler_flags.generate(env)