From f72e7fb7757113404f73e9bf01a7ff86370ec594 Mon Sep 17 00:00:00 2001 From: Spartan322 Date: Tue, 30 Jun 2026 00:27:43 -0400 Subject: [PATCH] Refactor SCons build into more modular scripts Rework the SCons build system into a modular toolset Simplify SConstruct to delegate options/generation to a new build/scripts.py Correct error module imports of parent build modules: This was caused by importing build modules inside the tools directory Add build_dir script import for internal VariantDir separation Add gen_dir script import for delegating generated files (for Author, License, and Git builders) Add AddLibraryIncludes method for simplified include configuration and exposure Add AddLibrarySources method for simplified source configuration Add BuildBaseLibrary method for specifying static library file name and target install location Add BuildHeadlessProgram method for specifying headless program construction Add BuildUnitTest method for specifying unit test program construction Add BuildDependencyLibrary method for specifying dependency library construction Change command strings to be less verbose by default with build/no_verbose.py Add color utilities in build/color.py Extract string utilities to build/string.py Change GlobRecursive functions to use cwd over specifying directory in pattern Change GetGitInfo to method Add name_prefix attribute getter inside GetGitInfo method Move functionality of build/common_compiler_flags to tools/scripts_flags.py Update platform tools to use tools/script_flags.py Remove obsolete platform tools Add .gitignore entries for scons artifacts Improve cache progress reporting Add GCC/Clang PCH support via GCH/GCHSH builders Remove custom MSVC build/pch.py Add SCons configuration through project tools directory Add platform override support via the project tools directory Update ruff check to Python 3.10 --- .gitignore | 4 +- SConstruct | 412 ++------------ build/author_info.py | 5 +- build/cache.py | 161 ++---- build/color.py | 153 ++++++ build/gch.py | 93 ++++ build/git_info.py | 3 +- build/glob_recursive.py | 5 +- build/library_builders.py | 240 ++++++++ build/license_info.py | 12 +- build/no_verbose.py | 37 ++ build/pch.py | 25 - build/scripts.py | 520 ++++++++++++++++++ build/string.py | 64 +++ pyproject.toml | 2 +- tools/linux.py | 10 +- tools/macos.py | 6 +- tools/macos_osxcross.py | 29 - .../scripts_flags.py | 19 +- tools/targets.py | 126 ----- tools/windows.py | 8 +- 21 files changed, 1256 insertions(+), 678 deletions(-) create mode 100644 build/color.py create mode 100644 build/gch.py create mode 100644 build/library_builders.py create mode 100644 build/no_verbose.py delete mode 100644 build/pch.py create mode 100644 build/scripts.py create mode 100644 build/string.py delete mode 100644 tools/macos_osxcross.py rename build/common_compiler_flags.py => tools/scripts_flags.py (89%) delete mode 100644 tools/targets.py diff --git a/.gitignore b/.gitignore index 96e935d..1a25300 100644 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,6 @@ cython_debug/ # When configure fails, SCons outputs these config.log -.sconf_temp \ No newline at end of file +.sconsign*.dblite +.scons_env.json +.scons_node_count diff --git a/SConstruct b/SConstruct index 8f766ab..08f311c 100644 --- a/SConstruct +++ b/SConstruct @@ -1,375 +1,83 @@ #!/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 +# This file is heavily based on https://github.com/godotengine/godot-cpp/blob/ba0edfed90512ec64aba51d4295a3e7e30112f86/SConstruct 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) +import build.scripts as scripts_tool -# 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=") +# Add scripts folder to sys.path, so that we can import local modules. +sys.path.append(Dir(".").srcnode().abspath) is_standalone = SCons.Script.sconscript_reading == 2 try: Import("env") - old_env = env - env = old_env.Clone() -except: + parent_env = env + env = Environment(tools=["default"], PLATFORM="") + env.parent_env = parent_env +except Exception: # 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 +try: + Import("build_dir") + env["build_dir"] = build_dir +except Exception: + pass -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)) +try: + Import("gen_dir") + env["gen_dir"] = gen_dir +except Exception: + pass - env.extra_suffix = "" +env.is_standalone = is_standalone - if env["platform"] in unsupported_known_platforms: - print("Unsupported platform: " + env["platform"]+". Only supports " + ", ".join(set(platforms) - set(unsupported_known_platforms))) - Exit() +env.PrependENVPath("PATH", os.getenv("PATH")) - # 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" +if env.get("parent_env", None) is not None: + skip_parent_item = True + for key in parent_env.Dictionary(): + if skip_parent_item: + if key == "arch": + skip_parent_item = False 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() + continue + env[key] = parent_env[key] - 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 +# Custom options and profile flags. +customs = ["custom.py"] +try: + customs += Import("customs") +except Exception: + pass +profile = ARGUMENTS.get("profile", "") +if profile: + if os.path.isfile(profile): + customs.append(profile) + elif os.path.isfile(profile + ".py"): + customs.append(profile + ".py") + +if "custom_tools" not in env: + try: + Import("custom_tools") + env["custom_tools"] = custom_tools + except Exception: + pass + +opts = Variables(customs, ARGUMENTS) +scripts_tool.options(opts, env) +opts.Update(env) + +Help(opts.GenerateHelpText(env)) + +scripts_tool.generate(env) + +# Prevent this scripts' from polluting later module imports. +sys.path.remove(Dir(".").srcnode().abspath) + +Return("env") diff --git a/build/author_info.py b/build/author_info.py index 93175a6..8225e77 100644 --- a/build/author_info.py +++ b/build/author_info.py @@ -1,3 +1,6 @@ +import build.string + + def author_builder(target, source, env): name_prefix = env.get("name_prefix", "project") prefix_upper = name_prefix.upper() @@ -29,7 +32,7 @@ def close_section(): for line in buffer.decode().splitlines(): if line.startswith(" ") and reading: - file.write(f'\t\t"{env.to_escaped_cstring(line).strip()}",\n') + file.write(f'\t\t"{build.string.to_escaped_cstring(line).strip()}",\n') elif line.startswith("## "): if reading: close_section() diff --git a/build/cache.py b/build/cache.py index d48b0e0..54209f9 100644 --- a/build/cache.py +++ b/build/cache.py @@ -1,127 +1,52 @@ -# 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" +# Copied from https://github.com/godotengine/godot/blob/77c0879cffe8cb7336fab2b7c18dcad2d9a0176c/methods.py#L815-L861 +import atexit +import sys +from typing import cast - 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 show_progress(env): + # Ninja has its own progress/tracking tool that clashes with ours. + if env.get("ninja", False): + return + + NODE_COUNT_FILENAME = env.Dir("#").srcnode().abspath + "/.scons_node_count" + + class ShowProgress: + def __init__(self): + self.count = 0 + self.max = 0 + try: + with open(NODE_COUNT_FILENAME, "r", encoding="utf-8") as f: + self.max = int(f.readline()) + except OSError: + pass + + # Progress reporting is not available in non-TTY environments since it + # messes with the output (for example, when writing to a file). + self.display = cast(bool, env["progress"] and sys.stdout.isatty()) + if self.display and not self.max: + print("Performing initial build, progress percentage unavailable!") + self.display = False 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() + self.count += 1 + if self.display: + percent = int(min(self.count * 100 / self.max, 100)) + sys.stdout.write(f"\r[{percent:3d}%] ") + sys.stdout.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] + from SCons.Script import Progress + from SCons.Script.Main import GetBuildFailures - 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:]] + progressor = ShowProgress() + Progress(progressor) - 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 + def progress_finish(): + if GetBuildFailures() or not progressor.count: + return try: - with open(node_count_fname, "w") as f: - f.write("%d\n" % node_count) - progressor.delete(progressor.file_list()) - except Exception: + with open(NODE_COUNT_FILENAME, "w", encoding="utf-8", newline="\n") as f: + f.write(f"{progressor.count}\n") + except OSError: 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) + atexit.register(progress_finish) diff --git a/build/color.py b/build/color.py new file mode 100644 index 0000000..6b5bc95 --- /dev/null +++ b/build/color.py @@ -0,0 +1,153 @@ +# Based on https://github.com/godotengine/godot/blob/f4c57c2824951a5df945392923bdfdc1c4055395/misc/utility/color.py +from __future__ import annotations + +import os +import re +import sys +from enum import Enum +from typing import Final + +# Colors are disabled in non-TTY environments such as pipes. This means if output is redirected +# to a file, it won't contain color codes. Colors are enabled by default on continuous integration. + +IS_CI: Final[bool] = bool(os.environ.get("CI")) +NO_COLOR: Final[bool] = bool(os.environ.get("NO_COLOR")) +CLICOLOR_FORCE: Final[bool] = bool(os.environ.get("CLICOLOR_FORCE")) +STDOUT_TTY: Final[bool] = bool(sys.stdout.isatty()) +STDERR_TTY: Final[bool] = bool(sys.stderr.isatty()) + + +_STDOUT_ORIGINAL: Final[bool] = False if NO_COLOR else CLICOLOR_FORCE or IS_CI or STDOUT_TTY +_STDERR_ORIGINAL: Final[bool] = False if NO_COLOR else CLICOLOR_FORCE or IS_CI or STDERR_TTY +_stdout_override: bool = _STDOUT_ORIGINAL +_stderr_override: bool = _STDERR_ORIGINAL + + +def is_stdout_color() -> bool: + return _stdout_override + + +def is_stderr_color() -> bool: + return _stderr_override + + +def force_stdout_color(value: bool) -> None: + """ + Explicitly set `stdout` support for ANSI escape codes. + If environment overrides exist, does nothing. + """ + if not NO_COLOR or not CLICOLOR_FORCE: + global _stdout_override + _stdout_override = value + + +def force_stderr_color(value: bool) -> None: + """ + Explicitly set `stderr` support for ANSI escape codes. + If environment overrides exist, does nothing. + """ + if not NO_COLOR or not CLICOLOR_FORCE: + global _stderr_override + _stderr_override = value + + +class Ansi(Enum): + """ + Enum class for adding ANSI codepoints directly into strings. Automatically converts values to + strings representing their internal value. + """ + + RESET = "\x1b[0m" + + BOLD = "\x1b[1m" + DIM = "\x1b[2m" + ITALIC = "\x1b[3m" + UNDERLINE = "\x1b[4m" + STRIKETHROUGH = "\x1b[9m" + REGULAR = "\x1b[22;23;24;29m" + + BLACK = "\x1b[30m" + RED = "\x1b[31m" + GREEN = "\x1b[32m" + YELLOW = "\x1b[33m" + BLUE = "\x1b[34m" + MAGENTA = "\x1b[35m" + CYAN = "\x1b[36m" + WHITE = "\x1b[37m" + GRAY = "\x1b[90m" + + def __str__(self) -> str: + return self.value + + +RE_ANSI = re.compile(r"\x1b\[[=\?]?[;\d]+[a-zA-Z]") + + +def color_print(*values: object, sep: str | None = " ", end: str | None = "\n", flush: bool = False) -> None: + """Prints a colored message to `stdout`. If disabled, ANSI codes are automatically stripped.""" + if is_stdout_color(): + print(*values, sep=sep, end=f"{Ansi.RESET}{end}", flush=flush) + else: + print(RE_ANSI.sub("", (sep or " ").join(map(str, values))), sep="", end=end, flush=flush) + + +def color_printerr(*values: object, sep: str | None = " ", end: str | None = "\n", flush: bool = False) -> None: + """Prints a colored message to `stderr`. If disabled, ANSI codes are automatically stripped.""" + if is_stderr_color(): + print(*values, sep=sep, end=f"{Ansi.RESET}{end}", flush=flush, file=sys.stderr) + else: + print(RE_ANSI.sub("", (sep or " ").join(map(str, values))), sep="", end=end, flush=flush, file=sys.stderr) + + +def print_info(*values: object) -> None: + """Prints a informational message with formatting.""" + color_print(f"{Ansi.GRAY}{Ansi.BOLD}INFO:{Ansi.REGULAR}", *values) + + +def print_warning(*values: object) -> None: + """Prints a warning message with formatting.""" + color_printerr(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR}", *values) + + +def print_error(*values: object) -> None: + """Prints an error message with formatting.""" + color_printerr(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR}", *values) + + +if sys.platform == "win32": + + def _win_color_fix(): + """Attempts to enable ANSI escape code support on Windows 10 and later.""" + from ctypes import POINTER, WINFUNCTYPE, WinError, windll + from ctypes.wintypes import BOOL, DWORD, HANDLE + + STDOUT_HANDLE = -11 + STDERR_HANDLE = -12 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + + def err_handler(result, func, args): + if not result: + raise WinError() + return args + + GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),)) + GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( + ("GetConsoleMode", windll.kernel32), + ((1, "hConsoleHandle"), (2, "lpMode")), + ) + GetConsoleMode.errcheck = err_handler + SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( + ("SetConsoleMode", windll.kernel32), + ((1, "hConsoleHandle"), (1, "dwMode")), + ) + SetConsoleMode.errcheck = err_handler + + for handle_id in [STDOUT_HANDLE, STDERR_HANDLE]: + try: + handle = GetStdHandle(handle_id) + flags = GetConsoleMode(handle) + SetConsoleMode(handle, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + except OSError: + pass + + _win_color_fix() diff --git a/build/gch.py b/build/gch.py new file mode 100644 index 0000000..45f1e33 --- /dev/null +++ b/build/gch.py @@ -0,0 +1,93 @@ +import SCons.Action +import SCons.Builder +import SCons.Errors +import SCons.Tool +import SCons.Util + + +def gch_emitter(target, source, env): + """Ensure proper suffix and dependency tracking.""" + + gch = None + + for t in target: + if SCons.Util.splitext(str(t))[1] == ".gch": + gch = t + + target = [gch] + return (target, source) + + +gch_action = SCons.Action.Action("$GCHCOM", "$GCHCOMSTR") +gch_builder = SCons.Builder.Builder( + action=gch_action, suffix=".gch", emitter=gch_emitter, source_scanner=SCons.Tool.SourceFileScanner +) + +gchsh_action = SCons.Action.Action("$GCHSHCOM", "$GCHSHCOMSTR") +gchsh_builder = SCons.Builder.Builder( + action=gchsh_action, suffix=".gch", emitter=gch_emitter, source_scanner=SCons.Tool.SourceFileScanner +) + + +def get_gch_node(env, target, source): + """ + Get the actual GCH file node + """ + gch_subst = env.get("GCH", False) and env.subst("$GCH", target=target, source=source, conv=lambda x: x) + + if not gch_subst: + return "" + + if SCons.Util.is_String(gch_subst): + gch_subst = target[0].dir.File(gch_subst) + + return gch_subst + + +def add_gch_builder(env): + """Add Gch / GchSh builders to the environment.""" + + if not env.Detect(["g++", "gcc", "clang++"]): + return + + # Command to build a gch from a header + env["GCHCOM"] = "$CXX -x c++-header -o $TARGET $CXXFLAGS $CCFLAGS $_CCCOMCOM $SOURCE" + env["BUILDERS"]["GCH"] = gch_builder + + env["GCHSHCOM"] = "$CXX -x c++-header -o $TARGET $SHCXXFLAGS $SHCCFLAGS $_CCCOMCOM $SOURCE" + env["BUILDERS"]["GCHSH"] = gchsh_builder + + def gch_dependent_emitter(target, source, env, parent_emitter): + parent_emitter(target, source, env) + + if not env.get("GCH"): + return target, source + + gch = get_gch_node(env, target, source) + if gch: + if str(target[0]) not in [SCons.Util.splitext(str(gch))[0] + s for s in [".so", ".os", ".o"]]: + env.Depends(target, gch) + + return target, source + + def gch_dependent_static_emitter(target, source, env): + return gch_dependent_emitter(target, source, env, SCons.Defaults.StaticObjectEmitter) + + def gch_dependent_shared_emitter(target, source, env): + return gch_dependent_emitter(target, source, env, SCons.Defaults.SharedObjectEmitter) + + modify_builders = [ + ("StaticObject", gch_dependent_static_emitter), + ("SharedObject", gch_dependent_shared_emitter), + ("Object", gch_dependent_static_emitter), + ] + + for builder_tuple in modify_builders: + if builder_tuple[0] not in env["BUILDERS"]: + continue + + bld = env["BUILDERS"][builder_tuple[0]] + bld.add_emitter(".cpp", builder_tuple[1]) + bld.add_emitter(".cc", builder_tuple[1]) + bld.add_emitter(".cxx", builder_tuple[1]) + bld.add_emitter(".c", builder_tuple[1]) diff --git a/build/git_info.py b/build/git_info.py index cf4d465..b028c47 100644 --- a/build/git_info.py +++ b/build/git_info.py @@ -101,7 +101,8 @@ def get_git_hash(): } -def get_git_info(name_prefix="project"): +def get_git_info(env, name_prefix=None): + name_prefix = env.get("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)} diff --git a/build/glob_recursive.py b/build/glob_recursive.py index d4fe65d..1ef3305 100644 --- a/build/glob_recursive.py +++ b/build/glob_recursive.py @@ -6,6 +6,7 @@ def GlobRecursive(pattern, nodes=["."], exclude=None): fs = SCons.Node.FS.get_default_fs() Glob = fs.Glob + Dir = fs.Dir if isinstance(exclude, str): exclude = [exclude] @@ -14,12 +15,12 @@ def GlobRecursive(pattern, nodes=["."], exclude=None): for node in nodes: node_str = str(node) - for f in Glob(node_str + "/*", source=True): + for f in Glob("*", cwd=Dir(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) + results += Glob(pattern, cwd=Dir(node_str)) if isinstance(exclude, list): diff --git a/build/library_builders.py b/build/library_builders.py new file mode 100644 index 0000000..5e8f0b5 --- /dev/null +++ b/build/library_builders.py @@ -0,0 +1,240 @@ +import os + +from build.glob_recursive import GlobRecursiveVariant + + +def add_library_includes(env, include_dir, expose_includes=True, add_build_dir=False, build_dir=None): + """ + include_dir is the location(s) to add to the CPPPATH + expose_includes determines whether location(s) are added to env.exposed_includes + add_build_dir determines whether the include automatically includes env["build_dir"] (does nothing if build_dir is specified) + build_dir is the VariantDir to include alongside include_dir as env["build_dir"]/include_dir + """ + + if add_build_dir and not build_dir: + build_dir = env["build_dir"] + + if isinstance(include_dir, list): + result = [] + for d in include_dir: + result.append( + add_library_includes( + env, d, expose_includes=expose_includes, add_build_dir=add_build_dir, build_dir=build_dir + ) + ) + return result + + if isinstance(include_dir, str): + include_dir = env.Dir(include_dir) + + if build_dir: + return add_library_includes( + env, [os.path.join(build_dir, include_dir), include_dir], expose_includes=expose_includes + ) + + env.AppendUnique(CPPPATH=[include_dir]) + + try: + env["INCPATH"] + except Exception: + env["INCPATH"] = [] + env["INCPATH"] += [include_dir] + + if expose_includes: + try: + env.exposed_includes + except Exception: + env.exposed_includes = [] + env.exposed_includes += [include_dir] + + return include_dir + + +def add_library_sources(env, src_dir, build_dir=None, glob="*.cpp", exclude=None): + """ + src_dir is the root directory to glob for files from + build_dir is the VariantDir to source from and build files into + glob is a directory recursive glob over files inside src_dir + exclude are the files or list of files to exclude from the glob + """ + + if isinstance(src_dir, list): + result = [] + for d in src_dir: + result.append(add_library_sources(env, src_dir, build_dir, glob, exclude)) + return result + + if not isinstance(src_dir, str): + src_dir = src_dir.srcnode() + + if not build_dir: + build_dir = env["build_dir"] + + try: + env.variant_paths + except Exception: + env.variant_paths = [] + + variant_path = os.path.join(build_dir, src_dir) + if variant_path not in env.variant_paths: + env.VariantDir(variant_path, src_dir, duplicate=False) + + env.variant_paths.append(variant_path) + + env.AppendUnique(CPPPATH=[env.Dir(d) for d in [variant_path, src_dir]]) + + try: + env.sources + except Exception: + env.sources = [] + env.sources += GlobRecursiveVariant(env, glob, src_dir, variant_path, exclude) + + return variant_path + + +def build_base_library(env, target, build_dir=None): + """ + target is the target to build + build_dir is the VariantDir to build the library into + """ + + if not build_dir: + build_dir = env["build_dir"] + + library = env.StaticLibrary(target=os.path.join(build_dir, target), source=env.sources) + env.NoCache(library) + env.Default(library) + env.Clean(library, env.variant_paths) + if env.is_standalone: + env.Default(env.Install(env.Dir(env.File(target).dir), library)) + + env.AppendUnique(LIBPATH=[env.Dir(f.dir) for f in env.File(library)]) + env.PrependUnique(LIBS=library) + return library + + +def build_headless_program(env, target, src_dir, defines_prefix, include_lib_src, build_dir=None): + """ + target is the target to build + src_dir is the headless source directory + defines_prefix is the prefix (to be uppercased) for the HEADLESS define + include_lib_src determines whether the headless environment includes previous sources + build_dir is the VariantDir to build the program into + """ + + headless_env = env.Clone() + headless_env.Append(CPPDEFINES=[f"{defines_prefix.upper()}_HEADLESS"]) + + if not include_lib_src: + headless_env.sources = [] + + variant_path = add_library_sources(headless_env, src_dir, build_dir=build_dir) + + headless_program = headless_env.Program( + target=os.path.join(build_dir, target), + source=headless_env.sources, + PROGSUFFIX=".headless" + headless_env["PROGSUFFIX"], + LIBSUFFIX=".headless" + headless_env["LIBSUFFIX"], + OBJSUFFIX=".headless" + headless_env["OBJSUFFIX"], + ) + env.NoCache(headless_program) + env.Default(headless_program) + env.Clean(headless_program, variant_path) + if env.is_standalone: + env.Default(env.Install(env.Dir(env.File(target).dir), headless_program)) + + return headless_program + + +def build_unit_test(env, target, src_dir, defines_prefix, build_dir=None, **kwargs): + """ + target is the target to build + src_dir is the unit test source directory + defines_prefix is the prefix (to be uppercased) for the TESTS define + build_dir is the VariantDir to build the program into + kwargs are passed to env.Program as is + """ + + if not build_dir: + build_dir = env["build_dir"] + + env.Append(CPPDEFINES=[f"{defines_prefix.lower()}_TESTS"]) + + env.sources = [] + variant_path = add_library_sources(env, src_dir, build_dir=os.path.join(build_dir, "tests")) + + env.unit_test = env.Program( + target=os.path.join(build_dir, target), + source=env.sources, + PROGSUFFIX=".tests" + env["PROGSUFFIX"], + LIBSUFFIX=".tests" + env["LIBSUFFIX"], + OBJSUFFIX=".tests" + env["OBJSUFFIX"], + **kwargs, + ) + env.NoCache(env.unit_test) + env.Default(env.unit_test) + env.Clean(env.unit_test, variant_path) + if env.is_standalone: + env.Default(env.Install(env.Dir(env.File(target).dir), env.unit_test)) + + def run_unit_test(env): + def run_unit_test_post_action(target=None, source=None, env=None): + import subprocess + + print() + return subprocess.run([target[0].path]).returncode + + unit_test_action = env.Action(run_unit_test_post_action, None) + test_post_action = env.AddPostAction(env.unit_test, unit_test_action) + env.AlwaysBuild(test_post_action) + + env.AddMethod(run_unit_test, "RunUnitTest") + return env + + +def build_dependency_library( + parent_env, target, src_dir, include_dir, build_dir=None, glob="*.cpp", exclude=None, shared_library=False +): + """ + target is the target to build + src_dir is the library source directory + include_dir is library include directory + build_dir is the VariantDir to build the library into + glob is a directory recursive glob over files inside src_dir + exclude are the files or list of files to exclude from the glob + shared_library determines whether the library is built as a shared library + """ + + add_library_includes(parent_env, include_dir) + env = parent_env.Clone() + + if not build_dir: + build_dir = env["build_dir"] + + if exclude is None: + pass + elif isinstance(exclude, str): + exclude = [os.path.join(src_dir, exclude)] + else: + exclude = [os.path.join(src_dir, e) for e in exclude] + + env.sources = [] + variant_path = add_library_sources(env, src_dir, build_dir=build_dir, glob=glob, exclude=exclude) + + if shared_library: + library = env.SharedLibrary(target=os.path.join(build_dir, target), source=env.sources) + else: + library = env.StaticLibrary(target=os.path.join(build_dir, target), source=env.sources) + env.NoCache(library) + env.Default(library) + env.Clean(library, variant_path) + + parent_env.AppendUnique(CPPPATH=[env.Dir(include_dir)]) + if parent_env.get("is_msvc", False): + parent_env.Append(CXXFLAGS=["/external:I", env.Dir(include_dir), "/external:W0"]) + else: + parent_env.Append(CXXFLAGS=["-isystem", env.Dir(include_dir)]) + parent_env.AppendUnique(LIBPATH=[env.Dir(build_dir)]) + parent_env.PrependUnique(LIBS=library) + + return env, library diff --git a/build/license_info.py b/build/license_info.py index 7652328..daa3056 100644 --- a/build/license_info.py +++ b/build/license_info.py @@ -1,6 +1,8 @@ from collections import OrderedDict from io import TextIOWrapper +import build.string + def get_license_info(src_copyright): class LicenseReader: @@ -95,7 +97,7 @@ def copyright_part_str() -> str: part_indexes[project_name] = part_index for part in project: result += ( - f'\t\t{{ "{env.to_escaped_cstring(part["License"][0])}", ' + f'\t\t{{ "{build.string.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" ) @@ -106,7 +108,7 @@ 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'\t\t{{ "{build.string.to_escaped_cstring(project_name)}", ' + f"{{ &{copyright_parts_name}[{part_indexes[project_name]}], {len(project)} }} }},\n" ) return result @@ -115,8 +117,8 @@ 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' + f'\t\t{{ "{build.string.to_escaped_cstring(license[0])}",' + + f"\n\t\t {build.string.to_raw_cstring([line if line != '.' else '' for line in license[1:]])} }}, \n" ) return result @@ -132,7 +134,7 @@ def license_list_str() -> str: namespace OpenVic {{ static constexpr std::string_view {license_text_name} = {{ - {env.to_raw_cstring(license_text)} + {build.string.to_raw_cstring(license_text)} }}; struct {component_copyright_part_name} {{ diff --git a/build/no_verbose.py b/build/no_verbose.py new file mode 100644 index 0000000..3d53148 --- /dev/null +++ b/build/no_verbose.py @@ -0,0 +1,37 @@ +def no_verbose(env): + from build.color import Ansi, is_stdout_color + + colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET] if is_stdout_color() else ["", "", "", ""] + + # There is a space before "..." to ensure that source file names can be + # Ctrl + clicked in the VS Code terminal. + compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(*colors) + link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(*colors) + link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(*colors) + ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(*colors) + link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(*colors) + java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(*colors) + compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format(*colors) + zip_archive_message = "{}Archiving {}$TARGET{} ...{}".format(*colors) + precompile_header_message = "{}Precompiling {}$SOURCE{} ...{}".format(*colors) + generated_file_message = "{}Generating {}$TARGET{} ...{}".format(*colors) + + env["CXXCOMSTR"] = compile_source_message + env["CCCOMSTR"] = compile_source_message + env["SWIFTCOMSTR"] = compile_source_message + env["SHCCCOMSTR"] = compile_shared_source_message + env["SHCXXCOMSTR"] = compile_shared_source_message + env["ARCOMSTR"] = link_library_message + env["RANLIBCOMSTR"] = ranlib_library_message + env["SHLINKCOMSTR"] = link_shared_library_message + env["LINKCOMSTR"] = link_program_message + env["JARCOMSTR"] = java_library_message + env["JAVACCOMSTR"] = java_compile_source_message + env["RCCOMSTR"] = compiled_resource_message + env["ZIPCOMSTR"] = zip_archive_message + env["PCHCOMSTR"] = precompile_header_message + env["GCHCOMSTR"] = precompile_header_message + env["GCHSHCOMSTR"] = precompile_header_message + env["GENCOMSTR"] = generated_file_message 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/build/scripts.py b/build/scripts.py new file mode 100644 index 0000000..d852f3b --- /dev/null +++ b/build/scripts.py @@ -0,0 +1,520 @@ +# Based on https://github.com/godotengine/godot-cpp/blob/ba0edfed90512ec64aba51d4295a3e7e30112f86/tools/godotcpp.py +import atexit +import os +import platform +import sys + +from SCons import __version__ as scons_raw_version +from SCons.Builder import Builder +from SCons.Errors import UserError +from SCons.Script import ARGUMENTS +from SCons.Tool import Tool +from SCons.Variables import BoolVariable, EnumVariable, PathVariable +from SCons.Variables.BoolVariable import _text2bool + +from build.author_info import author_builder +from build.cache import show_progress +from build.gch import add_gch_builder +from build.git_info import get_git_info, git_builder +from build.glob_recursive import GlobRecursive, GlobRecursiveVariant +from build.library_builders import ( + add_library_includes, + add_library_sources, + build_base_library, + build_dependency_library, + build_headless_program, + build_unit_test, +) +from build.license_info import license_builder +from build.no_verbose import no_verbose + + +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 normalize_path(val, env): + """Normalize a path that was provided by the user on the command line + and is thus either an absolute path, or relative to the top level directory (#) + where the command was run. + """ + # If val is an absolute path, it will not be joined. + return os.path.join(env.Dir("#").abspath, val) + + +def validate_file(key, val, env): + if not os.path.isfile(normalize_path(val, env)): + raise UserError("'%s' is not a file: %s" % (key, val)) + + +def validate_dir(key, val, env): + if not os.path.isdir(normalize_path(val, env)): + raise UserError("'%s' is not a directory: %s" % (key, 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))) + + +def get_platform_tools_paths(env): + result = [env.Dir("tools").srcnode().abspath] + + project_tools_dir = env.Dir("../tools") + if project_tools_dir.exists(): + result.insert(0, project_tools_dir.srcnode().abspath) + + custom_tools_path = env.get("custom_tools", None) + if custom_tools_path is not None: + result.insert(0, normalize_path(custom_tools_path, env)) + + return result + + +def get_project_platforms(env): + path = env.Dir("../tools") + if not path.exists(): + return [] + platforms = [] + for x in os.listdir(path.srcnode().abspath): + if not x.endswith(".py"): + continue + platforms.append(x.removesuffix(".py")) + return platforms + + +def get_custom_platforms(env): + path = env.get("custom_tools", None) + if path is None: + return [] + platforms = [] + for x in os.listdir(normalize_path(path, env)): + if not x.endswith(".py"): + continue + platforms.append(x.removesuffix(".py")) + return platforms + + +def project_info_generator_emitter(target, source, env): + for i in range(len(target)): + target[i] = os.path.join(env["gen_dir"], target[i]) + return target, source + + +def dump(env): + """ + Dumps latest build information for debugging purposes and external tools. + """ + + with open(".scons_env.json", "w", encoding="utf-8", newline="\n") as file: + file.write(env.Dump(format="json")) + + +def prepare_purge(env): + from SCons.Script.Main import GetBuildFailures + + def purge_flaky_files(): + paths_to_keep = [] + for build_failure in GetBuildFailures(): + path = build_failure.node.path + if os.path.isfile(path) and path not in paths_to_keep: + os.remove(path) + + atexit.register(purge_flaky_files) + + +def prepare_timer(): + import time + + def print_elapsed_time(time_at_start: float): + time_elapsed = time.monotonic() - time_at_start + time_formatted = time.strftime("%Hh %Mm %Ss", time.gmtime(time_elapsed)) + time_centiseconds = (time_elapsed % 1) * 100 + print(f"[BUILD TIMING] elapsed: {time_formatted} {time_centiseconds:02.0f}cs ({time_elapsed:.1f}s total)") + + atexit.register(print_elapsed_time, time.monotonic()) + + +# 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"] + + +def exists(env): + return True + + +def options(opts, env): + # 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=") + + opts.Add( + PathVariable( + key="custom_tools", + help="Path to directory containing custom tools", + default=env.get("custom_tools", None), + validator=validate_dir, + ) + ) + + opts.Update(env) + + project_platforms = get_project_platforms(env) + custom_platforms = get_custom_platforms(env) + + opts.Add( + EnumVariable( + key="platform", + help="Target platform", + default=env.get("platform", default_platform), + allowed_values=platforms + custom_platforms, + ignorecase=2, + ) + ) + + # Editor and template_debug are compatible (i.e. you can use the same binary for editor builds and debug templates). + # Release templates are only compatible with "template_release" builds. + # For this reason, we default to template_debug builds. + 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( + 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( + key="use_hot_reload", + help="Enable the extra accounting required to support hot reload.", + default=env.get("use_hot_reload", False), + ) + ) + + opts.Add( + BoolVariable( + "disable_exceptions", "Force disabling exception handling code", default=env.get("disable_exceptions", True) + ) + ) + + opts.Add( + BoolVariable("disable_rtti", "Force disabling 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"], + ) + ) + + opts.Add( + EnumVariable( + "optimize", + "The desired optimization flags. Inferred from 'target' and 'dev_build' by default.", + "auto", + ("auto", "none", "custom", "debug", "speed", "speed_trace", "size"), + ) + ) + opts.Add( + EnumVariable( + "lto", + "Link-time optimization", + "none", + ("none", "auto", "thin", "full"), + ) + ) + opts.Add( + EnumVariable( + "harden_memory", + "Library memory hardening. Inferred from 'dev_build' by default.", + "auto", + ["auto", "none", "fast"], + ignorecase=2, + ) + ) + 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(BoolVariable("verbose", "Enable verbose output for the compilation", False)) + 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)) + + # Add platform options (custom tools can override platforms) + for pl in sorted(set(platforms + project_platforms + custom_platforms)): + tool = Tool(pl, toolpath=get_platform_tools_paths(env)) + if hasattr(tool, "options"): + tool.options(opts) + + +def generate(env): + env.scons_version = env._get_major_minor_revision(scons_raw_version) + + # 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) + + # 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"]) + + # These defaults may be needed by platform tools + env.use_hot_reload = env["use_hot_reload"] + env.editor_build = env["target"] == "editor" + env.dev_build = env["dev_build"] + env.debug_features = env["target"] in ["editor", "template_debug"] + + opt_level = env["optimize"] + 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) + + harden_level = env["harden_memory"] + 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) + + tool = Tool(env["platform"], toolpath=get_platform_tools_paths(env)) + + if tool is None or not tool.exists(env): + raise ValueError("Required toolchain not found for platform " + env["platform"]) + + tool.generate(env) + + project_tools_dir = env.Dir("../tools") + if project_tools_dir.exists(): + for pl in sorted(set(get_project_platforms(env)) - set(platforms)): + tool = Tool(pl, toolpath=[project_tools_dir.srcnode().abspath]) + if hasattr(tool, "generate"): + tool.generate(env) + + env.Decider("MD5-timestamp") + + scons_cache_path = os.environ.get("SCONS_CACHE") + if scons_cache_path is not None: + env.CacheDir(scons_cache_path) + print("Scons cache enabled... (path: '" + scons_cache_path + "')") + + # Always presume godot-cpp thread=yes + env.Append(CPPDEFINES=["THREADS_ENABLED"]) + + 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 development. + env.Append(CPPDEFINES=["DEBUG_ENABLED"]) + + if env.dev_build: + # DEV_ENABLED enables *developer* code which should only be compiled for those + # working on the project itself. + env.Append(CPPDEFINES=["DEV_ENABLED"]) + else: + # Disable assert() for production targets. + env.Append(CPPDEFINES=["NDEBUG"]) + + if env["precision"] == "double": + env.Append(CPPDEFINES=["REAL_T_IS_DOUBLE"]) + + # Suffix + suffix = ".{}.{}".format(env["platform"], env["target"]) + if env.dev_build: + suffix += ".dev" + if env["precision"] == "double": + suffix += ".double" + suffix += "." + env["arch"] + if env["platform"] == "windows": + if env.get("debug_crt", False): + suffix += ".mdd" + elif env.get("use_static_cpp", False): + suffix += ".mt" + else: + suffix += ".md" + if env.get("use_asan", False): + suffix += ".san" + + env["suffix"] = suffix # Exposed when included from another project + env["LIBSUFFIX"] = suffix + env["LIBSUFFIX"] + env["OBJSUFFIX"] = suffix + env["OBJSUFFIX"] + env["PROGSUFFIX"] = suffix + env["PROGSUFFIX"] + + if "build_dir" not in env: + env["build_dir"] = env.Dir( + os.path.join(env.get("build_path", "#build"), env["suffix"].lstrip(".")) + ).abspath.replace("\\", "/") + + if "gen_dir" in env: + env["gen_dir"] = os.path.join(env["build_dir"], env["gen_dir"]) + else: + env["gen_dir"] = env["build_dir"] + + if env["compiledb"] and env.is_standalone: + # compile_commands.json + env.Tool("compilation_db") + env.Alias("compiledb", env.CompilationDatabase(normalize_path(env["compiledb_file"], env))) + env.Default("compiledb") + if not env["verbose"]: + env["COMPILATIONDB_COMSTR"] = "$GENCOMSTR" + + # Formatting + if not env["verbose"]: + no_verbose(env) + + # Builders + env.Append( + BUILDERS={ + "License": Builder(action=license_builder, emitter=project_info_generator_emitter), + "Git": Builder(action=git_builder, emitter=project_info_generator_emitter), + "Author": Builder(action=author_builder, emitter=project_info_generator_emitter), + } + ) + env.GlobRecursive = GlobRecursive + env.AddMethod(get_git_info, "GetGitInfo") + env.AddMethod(GlobRecursiveVariant, "GlobRecursiveVariant") + + env.AddMethod(add_library_includes, "AddLibraryIncludes") + env.AddMethod(add_library_sources, "AddLibrarySources") + + env.AddMethod(build_base_library, "BuildBaseLibrary") + env.AddMethod(build_headless_program, "BuildHeadlessProgram") + env.AddMethod(build_unit_test, "BuildUnitTest") + env.AddMethod(build_dependency_library, "BuildDependencyLibrary") + + add_gch_builder(env) + + if not env.GetOption("clean") and not env.GetOption("help") and env.is_standalone: + dump(env) + show_progress(env) + prepare_purge(env) + prepare_timer() diff --git a/build/string.py b/build/string.py new file mode 100644 index 0000000..b32f481 --- /dev/null +++ b/build/string.py @@ -0,0 +1,64 @@ +from typing import List, Union + + +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) diff --git a/pyproject.toml b/pyproject.toml index 383a144..2dcb92e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ explicit_package_bases = true [tool.ruff] extend-include = ["SConstruct", "SCsub"] line-length = 120 -target-version = "py37" +target-version = "py310" [tool.ruff.lint] extend-select = [ diff --git a/tools/linux.py b/tools/linux.py index f776bf9..e5e0cbd 100644 --- a/tools/linux.py +++ b/tools/linux.py @@ -1,5 +1,5 @@ -# Based on https://github.com/godotengine/godot-cpp/blob/e83fd0904c13356ed1d4c3d09f8bb9132bdc6b77/tools/linux.py -from build import common_compiler_flags +# Based on https://github.com/godotengine/godot-cpp/blob/ba0edfed90512ec64aba51d4295a3e7e30112f86/tools/linux.py +import scripts_flags from SCons.Variables import BoolVariable from SCons.Tool import clang, clangxx @@ -26,6 +26,10 @@ def generate(env): # Required for hot reload support. env.Append(CXXFLAGS=["-fno-gnu-unique"]) + if env.use_hot_reload: + # Reload won't work with "use_static_cpp", so disable it. + env["use_static_cpp"] = False + env.Append(CCFLAGS=["-fPIC", "-Wwrite-strings"]) env.Append(LINKFLAGS=["-Wl,-R,'$$ORIGIN'"]) @@ -91,4 +95,4 @@ def generate(env): if env["lto"] == "auto": env["lto"] = "full" - common_compiler_flags.generate(env) + scripts_flags.generate(env) diff --git a/tools/macos.py b/tools/macos.py index 35b96b5..cc8cce4 100644 --- a/tools/macos.py +++ b/tools/macos.py @@ -1,8 +1,8 @@ -# Based on https://github.com/godotengine/godot-cpp/blob/98ea2f60bb3846d6ae410d8936137d1b099cd50b/tools/macos.py +# Based on https://github.com/godotengine/godot-cpp/blob/ba0edfed90512ec64aba51d4295a3e7e30112f86/tools/macos.py import os import sys -from build import common_compiler_flags +import scripts_flags from SCons.Variables import BoolVariable @@ -102,4 +102,4 @@ def generate(env): if env["lto"] == "auto": env["lto"] = "none" - common_compiler_flags.generate(env) + scripts_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/build/common_compiler_flags.py b/tools/scripts_flags.py similarity index 89% rename from build/common_compiler_flags.py rename to tools/scripts_flags.py index 67e5ac1..0a602ca 100644 --- a/build/common_compiler_flags.py +++ b/tools/scripts_flags.py @@ -1,8 +1,12 @@ -# Based on https://github.com/godotengine/godot-cpp/blob/98ea2f60bb3846d6ae410d8936137d1b099cd50b/tools/common_compiler_flags.py +# Based on https://github.com/godotengine/godot-cpp/blob/ba0edfed90512ec64aba51d4295a3e7e30112f86/tools/common_compiler_flags.py import os import subprocess +def using_emcc(env): + return "emcc" in os.path.basename(env["CC"]) + + def using_clang(env): return "clang" in os.path.basename(env["CC"]) @@ -33,10 +37,7 @@ def generate(env): 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 + # Disable exception handling. We 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): @@ -108,7 +109,13 @@ def generate(env): # 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: + if using_emcc(env): + # Emscripten only produces dwarf symbols when using "-g3". + env.AppendUnique(CCFLAGS=["-g3"]) + # Emscripten linker needs debug symbols options too. + env.AppendUnique(LINKFLAGS=["-gdwarf-4"]) + env.AppendUnique(LINKFLAGS=["-g3"]) + elif env.dev_build: env.Append(CCFLAGS=["-g3"]) else: env.Append(CCFLAGS=["-g2"]) 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 index dbe7222..36f35ee 100644 --- a/tools/windows.py +++ b/tools/windows.py @@ -1,8 +1,8 @@ -# Based on https://github.com/godotengine/godot-cpp/blob/98ea2f60bb3846d6ae410d8936137d1b099cd50b/tools/windows.py +# Based on https://github.com/godotengine/godot-cpp/blob/ba0edfed90512ec64aba51d4295a3e7e30112f86/tools/windows.py import os import sys -from build import common_compiler_flags +import scripts_flags import my_spawn from SCons.Tool import mingw, msvc from SCons.Variables import BoolVariable @@ -90,8 +90,6 @@ def exists(env): def generate(env): - base = None - msvc_found = msvc.exists(env) mingw_found = mingw.exists(env) @@ -230,4 +228,4 @@ def generate(env): else: # Release env["lto"] = "full" - common_compiler_flags.generate(env) + scripts_flags.generate(env)