mirror of
https://github.com/Redot-Engine/redot-engine.git
synced 2025-12-05 23:07:42 -05:00
Merge commit godotengine/godot@2d3bdcac35
This commit is contained in:
382
methods.py
382
methods.py
@@ -6,16 +6,20 @@ import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import zlib
|
||||
from collections import OrderedDict
|
||||
from io import StringIO, TextIOBase
|
||||
from pathlib import Path
|
||||
from typing import Generator, List, Optional, Union, cast
|
||||
|
||||
from misc.utility.color import print_error, print_info, print_warning
|
||||
from platform_methods import detect_arch
|
||||
|
||||
# Get the "Redot" folder name ahead of time
|
||||
base_folder_path = str(os.path.abspath(Path(__file__).parent)) + "/"
|
||||
base_folder_only = os.path.basename(os.path.normpath(base_folder_path))
|
||||
# Get the "Godot" folder name ahead of time
|
||||
base_folder = Path(__file__).resolve().parent
|
||||
|
||||
compiler_version_cache = None
|
||||
|
||||
# Listing all the folders we have converted
|
||||
# for SCU in scu_builders.py
|
||||
@@ -79,34 +83,45 @@ def add_source_files(self, sources, files, allow_gen=False):
|
||||
return True
|
||||
|
||||
|
||||
def redirect_emitter(target, source, env):
|
||||
"""
|
||||
Emitter to automatically redirect object/library build files to the `bin/obj` directory,
|
||||
retaining subfolder structure. External build files will attempt to retain subfolder
|
||||
structure relative to their environment's parent directory, sorted under `bin/obj/external`.
|
||||
If `redirect_build_objects` is `False`, or an external build file isn't relative to the
|
||||
passed environment, this emitter does nothing.
|
||||
"""
|
||||
if not env["redirect_build_objects"]:
|
||||
return target, source
|
||||
|
||||
redirected_targets = []
|
||||
for item in target:
|
||||
if base_folder in (path := Path(item.get_abspath()).resolve()).parents:
|
||||
item = env.File(f"#bin/obj/{path.relative_to(base_folder)}")
|
||||
elif (alt_base := Path(env.Dir(".").get_abspath()).resolve().parent) in path.parents:
|
||||
item = env.File(f"#bin/obj/external/{path.relative_to(alt_base)}")
|
||||
else:
|
||||
print_warning(f'Failed to redirect "{path}"')
|
||||
redirected_targets.append(item)
|
||||
return redirected_targets, source
|
||||
|
||||
|
||||
def disable_warnings(self):
|
||||
# 'self' is the environment
|
||||
if self.msvc and not using_clang(self):
|
||||
# We have to remove existing warning level defines before appending /w,
|
||||
# otherwise we get: "warning D9025 : overriding '/W3' with '/w'"
|
||||
WARN_FLAGS = ["/Wall", "/W4", "/W3", "/W2", "/W1", "/W0"]
|
||||
self["CCFLAGS"] = [x for x in self["CCFLAGS"] if x not in WARN_FLAGS]
|
||||
self["CFLAGS"] = [x for x in self["CFLAGS"] if x not in WARN_FLAGS]
|
||||
self["CXXFLAGS"] = [x for x in self["CXXFLAGS"] if x not in WARN_FLAGS]
|
||||
self.AppendUnique(CCFLAGS=["/w"])
|
||||
self["WARNLEVEL"] = "/w"
|
||||
else:
|
||||
self.AppendUnique(CCFLAGS=["-w"])
|
||||
self["WARNLEVEL"] = "-w"
|
||||
|
||||
|
||||
def force_optimization_on_debug(self):
|
||||
# 'self' is the environment
|
||||
if self["target"] == "template_release":
|
||||
return
|
||||
|
||||
if self.msvc:
|
||||
# We have to remove existing optimization level defines before appending /O2,
|
||||
# otherwise we get: "warning D9025 : overriding '/0d' with '/02'"
|
||||
self["CCFLAGS"] = [x for x in self["CCFLAGS"] if not x.startswith("/O")]
|
||||
self["CFLAGS"] = [x for x in self["CFLAGS"] if not x.startswith("/O")]
|
||||
self["CXXFLAGS"] = [x for x in self["CXXFLAGS"] if not x.startswith("/O")]
|
||||
self.AppendUnique(CCFLAGS=["/O2"])
|
||||
elif self.msvc:
|
||||
self["OPTIMIZELEVEL"] = "/O2"
|
||||
else:
|
||||
self.AppendUnique(CCFLAGS=["-O3"])
|
||||
self["OPTIMIZELEVEL"] = "-O3"
|
||||
|
||||
|
||||
def add_module_version_string(self, s):
|
||||
@@ -154,30 +169,36 @@ def get_version_info(module_version_string="", silent=False):
|
||||
f"Using version status '{version_info['status']}.{version_info['status_version']}', overriding the original '{version.status}.{version.status_version}'."
|
||||
)
|
||||
|
||||
return version_info
|
||||
|
||||
|
||||
def get_git_info():
|
||||
os.chdir(base_folder)
|
||||
|
||||
# Parse Git hash if we're in a Git repo.
|
||||
githash = ""
|
||||
gitfolder = ".git"
|
||||
git_hash = ""
|
||||
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: "):
|
||||
gitfolder = module_folder[8:]
|
||||
git_folder = module_folder[8:]
|
||||
|
||||
if os.path.isfile(os.path.join(gitfolder, "HEAD")):
|
||||
with open(os.path.join(gitfolder, "HEAD"), "r", encoding="utf8") as file:
|
||||
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 = gitfolder.split("/")
|
||||
parts = git_folder.split("/")
|
||||
if len(parts) > 2 and parts[-2] == "worktrees":
|
||||
gitfolder = "/".join(parts[0:-2])
|
||||
head = os.path.join(gitfolder, ref)
|
||||
packedrefs = os.path.join(gitfolder, "packed-refs")
|
||||
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:
|
||||
githash = file.readline().strip()
|
||||
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
|
||||
@@ -186,26 +207,26 @@ def get_version_info(module_version_string="", silent=False):
|
||||
continue
|
||||
(line_hash, line_ref) = line.split(" ")
|
||||
if ref == line_ref:
|
||||
githash = line_hash
|
||||
git_hash = line_hash
|
||||
break
|
||||
else:
|
||||
githash = head
|
||||
|
||||
version_info["git_hash"] = githash
|
||||
# Fallback to 0 as a timestamp (will be treated as "unknown" in the engine).
|
||||
version_info["git_timestamp"] = 0
|
||||
git_hash = head
|
||||
|
||||
# Get the UNIX timestamp of the build commit.
|
||||
git_timestamp = 0
|
||||
if os.path.exists(".git"):
|
||||
try:
|
||||
version_info["git_timestamp"] = subprocess.check_output(
|
||||
["git", "log", "-1", "--pretty=format:%ct", "--no-show-signature", githash]
|
||||
).decode("utf-8")
|
||||
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 version_info
|
||||
return {
|
||||
"git_hash": git_hash,
|
||||
"git_timestamp": git_timestamp,
|
||||
}
|
||||
|
||||
|
||||
def get_cmdline_bool(option, default):
|
||||
@@ -408,9 +429,9 @@ def use_windows_spawn_fix(self, platform=None):
|
||||
|
||||
|
||||
def no_verbose(env):
|
||||
from misc.utility.color import Ansi
|
||||
from misc.utility.color import Ansi, is_stdout_color
|
||||
|
||||
colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET]
|
||||
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.
|
||||
@@ -565,7 +586,7 @@ def glob_recursive(pattern, node="."):
|
||||
|
||||
|
||||
def precious_program(env, program, sources, **args):
|
||||
program = env.ProgramOriginal(program, sources, **args)
|
||||
program = env.Program(program, sources, **args)
|
||||
env.Precious(program)
|
||||
return program
|
||||
|
||||
@@ -647,6 +668,11 @@ def get_compiler_version(env):
|
||||
- metadata1, metadata2: Extra information
|
||||
- date: Date of the build
|
||||
"""
|
||||
|
||||
global compiler_version_cache
|
||||
if compiler_version_cache is not None:
|
||||
return compiler_version_cache
|
||||
|
||||
import shlex
|
||||
|
||||
ret = {
|
||||
@@ -693,7 +719,7 @@ def get_compiler_version(env):
|
||||
ret["metadata1"] = split[1]
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
print_warning("Couldn't find vswhere to determine compiler version.")
|
||||
return ret
|
||||
return update_compiler_version_cache(ret)
|
||||
|
||||
# Not using -dumpversion as some GCC distros only return major, and
|
||||
# Clang used to return hardcoded 4.2.1: # https://reviews.llvm.org/D56803
|
||||
@@ -703,7 +729,7 @@ def get_compiler_version(env):
|
||||
).strip()
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
print_warning("Couldn't parse CXX environment variable to infer compiler version.")
|
||||
return ret
|
||||
return update_compiler_version_cache(ret)
|
||||
|
||||
match = re.search(
|
||||
r"(?:(?<=version )|(?<=\) )|(?<=^))"
|
||||
@@ -746,7 +772,13 @@ def get_compiler_version(env):
|
||||
"apple_patch3",
|
||||
]:
|
||||
ret[key] = int(ret[key] or -1)
|
||||
return ret
|
||||
return update_compiler_version_cache(ret)
|
||||
|
||||
|
||||
def update_compiler_version_cache(value):
|
||||
global compiler_version_cache
|
||||
compiler_version_cache = value
|
||||
return value
|
||||
|
||||
|
||||
def using_gcc(env):
|
||||
@@ -766,7 +798,7 @@ def show_progress(env):
|
||||
if env["ninja"]:
|
||||
return
|
||||
|
||||
NODE_COUNT_FILENAME = f"{base_folder_path}.scons_node_count"
|
||||
NODE_COUNT_FILENAME = base_folder / ".scons_node_count"
|
||||
|
||||
class ShowProgress:
|
||||
def __init__(self):
|
||||
@@ -780,9 +812,10 @@ def show_progress(env):
|
||||
|
||||
# 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, self.max and env["progress"] and sys.stdout.isatty())
|
||||
self.display = cast(bool, env["progress"] and sys.stdout.isatty())
|
||||
if self.display and not self.max:
|
||||
print_info("Performing initial build, progress percentage unavailable!")
|
||||
self.display = False
|
||||
|
||||
def __call__(self, node, *args, **kw):
|
||||
self.count += 1
|
||||
@@ -865,9 +898,6 @@ def clean_cache(cache_path: str, cache_limit: int, verbose: bool) -> None:
|
||||
|
||||
|
||||
def prepare_cache(env) -> None:
|
||||
if env.GetOption("clean"):
|
||||
return
|
||||
|
||||
cache_path = ""
|
||||
if env["cache_path"]:
|
||||
cache_path = cast(str, env["cache_path"])
|
||||
@@ -890,11 +920,9 @@ def prepare_cache(env) -> None:
|
||||
# Convert GiB to bytes; treat negative numbers as 0 (unlimited).
|
||||
cache_limit = max(0, int(cache_limit * 1024 * 1024 * 1024))
|
||||
if env["verbose"]:
|
||||
print(
|
||||
"Current cache limit is {} (used: {})".format(
|
||||
convert_size(cache_limit) if cache_limit else "∞",
|
||||
convert_size(get_size(cache_path)),
|
||||
)
|
||||
print_info(
|
||||
f"Current cache size is {convert_size(get_size(cache_path))}"
|
||||
+ (f" (limit: {convert_size(cache_limit)})" if cache_limit else "")
|
||||
)
|
||||
|
||||
atexit.register(clean_cache, cache_path, cache_limit, env["verbose"])
|
||||
@@ -919,21 +947,19 @@ def prepare_timer():
|
||||
def print_elapsed_time(time_at_start: float):
|
||||
time_elapsed = time.time() - time_at_start
|
||||
time_formatted = time.strftime("%H:%M:%S", time.gmtime(time_elapsed))
|
||||
time_centiseconds = round((time_elapsed % 1) * 100)
|
||||
print_info(f"Time elapsed: {time_formatted}.{time_centiseconds}")
|
||||
time_centiseconds = (time_elapsed % 1) * 100
|
||||
print_info(f"Time elapsed: {time_formatted}.{time_centiseconds:02.0f}")
|
||||
|
||||
atexit.register(print_elapsed_time, time.time())
|
||||
|
||||
|
||||
def dump(env):
|
||||
# Dumps latest build information for debugging purposes and external tools.
|
||||
from json import dump
|
||||
"""
|
||||
Dumps latest build information for debugging purposes and external tools.
|
||||
"""
|
||||
|
||||
def non_serializable(obj):
|
||||
return "<<non-serializable: %s>>" % (type(obj).__qualname__)
|
||||
|
||||
with open(".scons_env.json", "w", encoding="utf-8", newline="\n") as f:
|
||||
dump(env.Dictionary(), f, indent=4, default=non_serializable)
|
||||
with open(".scons_env.json", "w", encoding="utf-8", newline="\n") as file:
|
||||
file.write(env.Dump(format="json"))
|
||||
|
||||
|
||||
# Custom Visual Studio project generation logic that supports any platform that has a msvs.py
|
||||
@@ -1042,8 +1068,22 @@ def generate_vs_project(env, original_args, project_name="redot"):
|
||||
platform = env["platform"]
|
||||
target = env["target"]
|
||||
arch = env["arch"]
|
||||
host_arch = detect_arch()
|
||||
|
||||
host_platform = "windows"
|
||||
if (
|
||||
sys.platform.startswith("linux")
|
||||
or sys.platform.startswith("dragonfly")
|
||||
or sys.platform.startswith("freebsd")
|
||||
or sys.platform.startswith("netbsd")
|
||||
or sys.platform.startswith("openbsd")
|
||||
):
|
||||
host_platform = "linuxbsd"
|
||||
elif sys.platform == "darwin":
|
||||
host_platform = "macos"
|
||||
|
||||
vs_configuration = {}
|
||||
host_vs_configuration = {}
|
||||
common_build_prefix = []
|
||||
confs = []
|
||||
for x in sorted(glob.glob("platform/*")):
|
||||
@@ -1072,6 +1112,12 @@ def generate_vs_project(env, original_args, project_name="redot"):
|
||||
if platform == platform_name:
|
||||
common_build_prefix = msvs.get_build_prefix(env)
|
||||
vs_configuration = vsconf
|
||||
if platform_name == host_platform:
|
||||
host_vs_configuration = vsconf
|
||||
for a in vsconf["arches"]:
|
||||
if host_arch == a["architecture"]:
|
||||
host_arch = a["platform"]
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -1229,29 +1275,29 @@ def generate_vs_project(env, original_args, project_name="redot"):
|
||||
properties.append(
|
||||
"<ActiveProjectItemList_%s>;%s;</ActiveProjectItemList_%s>" % (x, ";".join(itemlist[x]), x)
|
||||
)
|
||||
output = f"bin\\redot{env['PROGSUFFIX']}"
|
||||
output = os.path.join("bin", f"redot{env['PROGSUFFIX']}")
|
||||
|
||||
with open("misc/msvs/props.template", "r", encoding="utf-8") as file:
|
||||
props_template = file.read()
|
||||
|
||||
props_template = props_template.replace("%%VSCONF%%", vsconf)
|
||||
props_template = props_template.replace("%%CONDITION%%", condition)
|
||||
props_template = props_template.replace("%%PROPERTIES%%", "\n ".join(properties))
|
||||
props_template = props_template.replace("%%EXTRA_ITEMS%%", "\n ".join(extraItems))
|
||||
|
||||
props_template = props_template.replace("%%OUTPUT%%", output)
|
||||
|
||||
proplist = [format_key_value(v) for v in list(env["CPPDEFINES"])]
|
||||
proplist = [format_key_value(j) for j in list(env["CPPDEFINES"])]
|
||||
proplist += [format_key_value(j) for j in env.get("VSHINT_DEFINES", [])]
|
||||
props_template = props_template.replace("%%DEFINES%%", ";".join(proplist))
|
||||
|
||||
proplist = [str(j) for j in env["CPPPATH"]]
|
||||
proplist += [str(j) for j in env.get("VSHINT_INCLUDES", [])]
|
||||
proplist += [str(j) for j in get_default_include_paths(env)]
|
||||
props_template = props_template.replace("%%INCLUDES%%", ";".join(proplist))
|
||||
|
||||
proplist = env["CCFLAGS"]
|
||||
proplist += [x for x in env["CXXFLAGS"] if not x.startswith("$")]
|
||||
proplist += [str(j) for j in env.get("VSHINT_OPTIONS", [])]
|
||||
proplist = [env.subst("$CCFLAGS")]
|
||||
proplist += [env.subst("$CXXFLAGS")]
|
||||
proplist += [env.subst("$VSHINT_OPTIONS")]
|
||||
props_template = props_template.replace("%%OPTIONS%%", " ".join(proplist))
|
||||
|
||||
# Windows allows us to have spaces in paths, so we need
|
||||
@@ -1282,17 +1328,17 @@ def generate_vs_project(env, original_args, project_name="redot"):
|
||||
|
||||
commands = "scons"
|
||||
if len(common_build_prefix) == 0:
|
||||
commands = "echo Starting SCons && cmd /V /C " + commands
|
||||
commands = "echo Starting SCons & " + commands
|
||||
else:
|
||||
common_build_prefix[0] = "echo Starting SCons && cmd /V /C " + common_build_prefix[0]
|
||||
common_build_prefix[0] = "echo Starting SCons & " + common_build_prefix[0]
|
||||
|
||||
cmd = " ^& ".join(common_build_prefix + [" ".join([commands] + common_build_postfix)])
|
||||
cmd = " ".join(common_build_prefix + [" ".join([commands] + common_build_postfix)])
|
||||
props_template = props_template.replace("%%BUILD%%", cmd)
|
||||
|
||||
cmd = " ^& ".join(common_build_prefix + [" ".join([commands] + cmd_rebuild)])
|
||||
cmd = " ".join(common_build_prefix + [" ".join([commands] + cmd_rebuild)])
|
||||
props_template = props_template.replace("%%REBUILD%%", cmd)
|
||||
|
||||
cmd = " ^& ".join(common_build_prefix + [" ".join([commands] + cmd_clean)])
|
||||
cmd = " ".join(common_build_prefix + [" ".join([commands] + cmd_clean)])
|
||||
props_template = props_template.replace("%%CLEAN%%", cmd)
|
||||
|
||||
with open(
|
||||
@@ -1323,18 +1369,45 @@ def generate_vs_project(env, original_args, project_name="redot"):
|
||||
section2 = []
|
||||
for conf in confs:
|
||||
redot_platform = conf["platform"]
|
||||
has_editor = "editor" in conf["targets"]
|
||||
|
||||
# Skip any platforms that can build the editor and don't match the host platform.
|
||||
#
|
||||
# When both Windows and Mac define an editor target, it's defined as platform+target+arch (windows+editor+x64 for example).
|
||||
# VS only supports two attributes, a "Configuration" and a "Platform", and we currently map our target to the Configuration
|
||||
# (i.e. editor/template_debug/template_release), and our architecture to the "Platform" (i.e. x64, arm64, etc).
|
||||
# Those two are not enough to disambiguate multiple godot targets for different godot platforms with the same architecture,
|
||||
# i.e. editor|x64 would currently match both windows editor intel 64 and linux editor intel 64.
|
||||
#
|
||||
# TODO: More work is needed in order to support generating VS projects that unambiguously support all platform+target+arch variations.
|
||||
# The VS "Platform" has to be a known architecture that VS recognizes, so we can only play around with the "Configuration" part of the combo.
|
||||
if has_editor and redot_platform != host_vs_configuration["platform"]:
|
||||
continue
|
||||
|
||||
for p in conf["arches"]:
|
||||
sln_plat = p["platform"]
|
||||
proj_plat = sln_plat
|
||||
redot_arch = p["architecture"]
|
||||
|
||||
# Redirect editor configurations for non-Windows platforms to the Windows one, so the solution has all the permutations
|
||||
# and VS doesn't complain about missing project configurations.
|
||||
# Redirect editor configurations for platforms that don't support the editor target to the default editor target on the
|
||||
# active host platform, so the solution has all the permutations and VS doesn't complain about missing project configurations.
|
||||
# These configurations are disabled, so they show up but won't build.
|
||||
if redot_platform != "windows":
|
||||
if not has_editor:
|
||||
section1 += [f"editor|{sln_plat} = editor|{proj_plat}"]
|
||||
section2 += [
|
||||
f"{{{proj_uuid}}}.editor|{proj_plat}.ActiveCfg = editor|{proj_plat}",
|
||||
section2 += [f"{{{proj_uuid}}}.editor|{proj_plat}.ActiveCfg = editor|{host_arch}"]
|
||||
|
||||
configurations += [
|
||||
f'<ProjectConfiguration Include="editor|{proj_plat}">',
|
||||
" <Configuration>editor</Configuration>",
|
||||
f" <Platform>{proj_plat}</Platform>",
|
||||
"</ProjectConfiguration>",
|
||||
]
|
||||
|
||||
properties += [
|
||||
f"<PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='editor|{proj_plat}'\">",
|
||||
" <GodotConfiguration>editor</GodotConfiguration>",
|
||||
f" <GodotPlatform>{proj_plat}</GodotPlatform>",
|
||||
"</PropertyGroup>",
|
||||
]
|
||||
|
||||
for t in conf["targets"]:
|
||||
@@ -1358,21 +1431,6 @@ def generate_vs_project(env, original_args, project_name="redot"):
|
||||
"</PropertyGroup>",
|
||||
]
|
||||
|
||||
if redot_platform != "windows":
|
||||
configurations += [
|
||||
f'<ProjectConfiguration Include="editor|{proj_plat}">',
|
||||
" <Configuration>editor</Configuration>",
|
||||
f" <Platform>{proj_plat}</Platform>",
|
||||
"</ProjectConfiguration>",
|
||||
]
|
||||
|
||||
properties += [
|
||||
f"<PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='editor|{proj_plat}'\">",
|
||||
" <RedotConfiguration>editor</RedotConfiguration>",
|
||||
f" <RedotPlatform>{proj_plat}</RedotPlatform>",
|
||||
"</PropertyGroup>",
|
||||
]
|
||||
|
||||
p = f"{project_name}.{redot_platform}.{redot_target}.{redot_arch}.generated.props"
|
||||
imports += [
|
||||
f'<Import Project="$(MSBuildProjectDirectory)\\{p}" Condition="Exists(\'$(MSBuildProjectDirectory)\\{p}\')"/>'
|
||||
@@ -1420,6 +1478,11 @@ def generate_vs_project(env, original_args, project_name="redot"):
|
||||
sys.exit()
|
||||
|
||||
|
||||
############################################################
|
||||
# FILE GENERATION & FORMATTING
|
||||
############################################################
|
||||
|
||||
|
||||
def generate_copyright_header(filename: str) -> str:
|
||||
MARGIN = 70
|
||||
TEMPLATE = """\
|
||||
@@ -1455,81 +1518,87 @@ def generate_copyright_header(filename: str) -> str:
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
"""
|
||||
filename = filename.split("/")[-1].ljust(MARGIN)
|
||||
if len(filename) > MARGIN:
|
||||
if len(filename := os.path.basename(filename).ljust(MARGIN)) > MARGIN:
|
||||
print_warning(f'Filename "{filename}" too large for copyright header.')
|
||||
return TEMPLATE % filename
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def generated_wrapper(
|
||||
path, # FIXME: type with `Union[str, Node, List[Node]]` when pytest conflicts are resolved
|
||||
path: str,
|
||||
guard: Optional[bool] = None,
|
||||
prefix: str = "",
|
||||
suffix: str = "",
|
||||
) -> Generator[TextIOBase, None, None]:
|
||||
"""
|
||||
Wrapper class to automatically handle copyright headers and header guards
|
||||
for generated scripts. Meant to be invoked via `with` statement similar to
|
||||
creating a file.
|
||||
|
||||
- `path`: The path of the file to be created. Can be passed a raw string, an
|
||||
isolated SCons target, or a full SCons target list. If a target list contains
|
||||
multiple entries, produces a warning & only creates the first entry.
|
||||
- `guard`: Optional bool to determine if a header guard should be added. If
|
||||
unassigned, header guards are determined by the file extension.
|
||||
- `prefix`: Custom prefix to prepend to a header guard. Produces a warning if
|
||||
provided a value when `guard` evaluates to `False`.
|
||||
- `suffix`: Custom suffix to append to a header guard. Produces a warning if
|
||||
provided a value when `guard` evaluates to `False`.
|
||||
- `path`: The path of the file to be created.
|
||||
- `guard`: Optional bool to determine if `#pragma once` should be added. If
|
||||
unassigned, the value is determined by file extension.
|
||||
"""
|
||||
|
||||
# Handle unfiltered SCons target[s] passed as path.
|
||||
if not isinstance(path, str):
|
||||
if isinstance(path, list):
|
||||
if len(path) > 1:
|
||||
print_warning(
|
||||
f"Attempting to use generated wrapper with multiple targets; will only use first entry: {path[0]}"
|
||||
)
|
||||
path = path[0]
|
||||
if not hasattr(path, "get_abspath"):
|
||||
raise TypeError(f'Expected type "str", "Node" or "List[Node]"; was passed {type(path)}.')
|
||||
path = path.get_abspath()
|
||||
|
||||
path = str(path).replace("\\", "/")
|
||||
if guard is None:
|
||||
guard = path.endswith((".h", ".hh", ".hpp", ".inc"))
|
||||
if not guard and (prefix or suffix):
|
||||
print_warning(f'Trying to assign header guard prefix/suffix while `guard` is disabled: "{path}".')
|
||||
|
||||
header_guard = ""
|
||||
if guard:
|
||||
if prefix:
|
||||
prefix += "_"
|
||||
if suffix:
|
||||
suffix = f"_{suffix}"
|
||||
split = path.split("/")[-1].split(".")
|
||||
header_guard = (f"{prefix}{split[0]}{suffix}.{'.'.join(split[1:])}".upper()
|
||||
.replace(".", "_").replace("-", "_").replace(" ", "_").replace("__", "_")) # fmt: skip
|
||||
|
||||
with open(path, "wt", encoding="utf-8", newline="\n") as file:
|
||||
file.write(generate_copyright_header(path))
|
||||
file.write("\n/* THIS FILE IS GENERATED. EDITS WILL BE LOST. */\n\n")
|
||||
if not path.endswith(".out"): # For test output, we only care about the content.
|
||||
file.write(generate_copyright_header(path))
|
||||
file.write("\n/* THIS FILE IS GENERATED. EDITS WILL BE LOST. */\n\n")
|
||||
|
||||
if guard:
|
||||
file.write(f"#ifndef {header_guard}\n")
|
||||
file.write(f"#define {header_guard}\n\n")
|
||||
if guard is None:
|
||||
guard = path.endswith((".h", ".hh", ".hpp", ".hxx", ".inc"))
|
||||
if guard:
|
||||
file.write("#pragma once\n\n")
|
||||
|
||||
with StringIO(newline="\n") as str_io:
|
||||
yield str_io
|
||||
file.write(str_io.getvalue().strip() or "/* NO CONTENT */")
|
||||
|
||||
if guard:
|
||||
file.write(f"\n\n#endif // {header_guard}")
|
||||
|
||||
file.write("\n")
|
||||
|
||||
|
||||
def get_buffer(path: str) -> bytes:
|
||||
with open(path, "rb") as file:
|
||||
return file.read()
|
||||
|
||||
|
||||
def compress_buffer(buffer: bytes) -> bytes:
|
||||
# Use maximum zlib compression level to further reduce file size
|
||||
# (at the cost of initial build times).
|
||||
return zlib.compress(buffer, zlib.Z_BEST_COMPRESSION)
|
||||
|
||||
|
||||
def format_buffer(buffer: bytes, indent: int = 0, width: int = 120, initial_indent: bool = False) -> str:
|
||||
return textwrap.fill(
|
||||
", ".join(str(byte) for byte in buffer),
|
||||
width=width,
|
||||
initial_indent="\t" * indent if initial_indent else "",
|
||||
subsequent_indent="\t" * indent,
|
||||
tabsize=4,
|
||||
)
|
||||
|
||||
|
||||
############################################################
|
||||
# CSTRING PARSING
|
||||
############################################################
|
||||
|
||||
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 to_raw_cstring(value: Union[str, List[str]]) -> str:
|
||||
MAX_LITERAL = 16 * 1024
|
||||
|
||||
@@ -1567,4 +1636,23 @@ def to_raw_cstring(value: Union[str, List[str]]) -> str:
|
||||
|
||||
split += [segment]
|
||||
|
||||
return " ".join(f'R"<!>({x.decode()})<!>"' for x in split)
|
||||
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))
|
||||
|
||||
|
||||
def get_default_include_paths(env):
|
||||
if env.msvc:
|
||||
return []
|
||||
compiler = env.subst("$CXX")
|
||||
target = os.path.join(env.Dir("#main").abspath, "main.cpp")
|
||||
args = [compiler, target, "-x", "c++", "-v"]
|
||||
ret = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
output = ret.stdout
|
||||
match = re.search(r"#include <\.\.\.> search starts here:([\S\s]*)End of search list.", output)
|
||||
if not match:
|
||||
print_warning("Failed to find the include paths in the compiler output.")
|
||||
return []
|
||||
return [x.strip() for x in match[1].strip().splitlines()]
|
||||
|
||||
Reference in New Issue
Block a user