feat: updated engine version to 4.4-rc1

This commit is contained in:
Sara 2025-02-23 14:38:14 +01:00
parent ee00efde1f
commit 21ba8e33af
5459 changed files with 1128836 additions and 198305 deletions

View file

@ -1,25 +1,25 @@
import atexit
import contextlib
import glob
import math
import os
import re
import subprocess
import sys
from collections import OrderedDict
from enum import Enum
from io import StringIO, TextIOWrapper
from io import StringIO, TextIOBase
from pathlib import Path
from typing import Generator, Optional
from typing import Generator, List, Optional, Union, cast
from misc.utility.color import print_error, print_info, print_warning
# Get the "Godot" 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))
# Listing all the folders we have converted
# for SCU in scu_builders.py
_scu_folders = set()
# Colors are disabled in non-TTY environments such as pipes. This means
# that if output is redirected to a file, it won't contain color codes.
# Colors are always enabled on continuous integration.
_colorize = bool(sys.stdout.isatty() or os.environ.get("CI"))
def set_scu_folders(scu_folders):
@ -27,67 +27,15 @@ def set_scu_folders(scu_folders):
_scu_folders = scu_folders
class ANSI(Enum):
"""
Enum class for adding ansi colorcodes directly into strings.
Automatically converts values to strings representing their
internal value, or an empty string in a non-colorized scope.
"""
RESET = "\x1b[0m"
BOLD = "\x1b[1m"
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"
PURPLE = "\x1b[38;5;93m"
PINK = "\x1b[38;5;206m"
ORANGE = "\x1b[38;5;214m"
GRAY = "\x1b[38;5;244m"
def __str__(self) -> str:
global _colorize
return str(self.value) if _colorize else ""
def print_warning(*values: object) -> None:
"""Prints a warning message with formatting."""
print(f"{ANSI.YELLOW}{ANSI.BOLD}WARNING:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr)
def print_error(*values: object) -> None:
"""Prints an error message with formatting."""
print(f"{ANSI.RED}{ANSI.BOLD}ERROR:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr)
def add_source_files_orig(self, sources, files, allow_gen=False):
# Convert string to list of absolute paths (including expanding wildcard)
if isinstance(files, (str, bytes)):
# Keep SCons project-absolute path as they are (no wildcard support)
if files.startswith("#"):
if "*" in files:
print_error("Wildcards can't be expanded in SCons project-absolute path: '{}'".format(files))
return
files = [files]
else:
# Exclude .gen.cpp files from globbing, to avoid including obsolete ones.
# They should instead be added manually.
skip_gen_cpp = "*" in files
dir_path = self.Dir(".").abspath
files = sorted(glob.glob(dir_path + "/" + files))
if skip_gen_cpp and not allow_gen:
files = [f for f in files if not f.endswith(".gen.cpp")]
if isinstance(files, str):
# Exclude .gen.cpp files from globbing, to avoid including obsolete ones.
# They should instead be added manually.
skip_gen_cpp = "*" in files
files = self.Glob(files)
if skip_gen_cpp and not allow_gen:
files = [f for f in files if not str(f).endswith(".gen.cpp")]
# Add each path as compiled Object following environment (self) configuration
for path in files:
@ -98,35 +46,6 @@ def add_source_files_orig(self, sources, files, allow_gen=False):
sources.append(obj)
# The section name is used for checking
# the hash table to see whether the folder
# is included in the SCU build.
# It will be something like "core/math".
def _find_scu_section_name(subdir):
section_path = os.path.abspath(subdir) + "/"
folders = []
folder = ""
for i in range(8):
folder = os.path.dirname(section_path)
folder = os.path.basename(folder)
if folder == base_folder_only:
break
folders += [folder]
section_path += "../"
section_path = os.path.abspath(section_path) + "/"
section_name = ""
for n in range(len(folders)):
# section_name += folders[len(folders) - n - 1] + " "
section_name += folders[len(folders) - n - 1]
if n != (len(folders) - 1):
section_name += "/"
return section_name
def add_source_files_scu(self, sources, files, allow_gen=False):
if self["scu_build"] and isinstance(files, str):
if "*." not in files:
@ -135,10 +54,9 @@ def add_source_files_scu(self, sources, files, allow_gen=False):
# If the files are in a subdirectory, we want to create the scu gen
# files inside this subdirectory.
subdir = os.path.dirname(files)
if subdir != "":
subdir += "/"
section_name = _find_scu_section_name(subdir)
subdir = subdir if subdir == "" else subdir + "/"
section_name = self.Dir(subdir).tpath
section_name = section_name.replace("\\", "/") # win32
# if the section name is in the hash table?
# i.e. is it part of the SCU build?
global _scu_folders
@ -163,12 +81,13 @@ def add_source_files(self, sources, files, allow_gen=False):
def disable_warnings(self):
# 'self' is the environment
if self.msvc:
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'"
self["CCFLAGS"] = [x for x in self["CCFLAGS"] if not (x.startswith("/W") or x.startswith("/w"))]
self["CFLAGS"] = [x for x in self["CFLAGS"] if not (x.startswith("/W") or x.startswith("/w"))]
self["CXXFLAGS"] = [x for x in self["CXXFLAGS"] if not (x.startswith("/W") or x.startswith("/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"])
else:
self.AppendUnique(CCFLAGS=["-w"])
@ -199,7 +118,7 @@ def get_version_info(module_version_string="", silent=False):
if os.getenv("BUILD_NAME") is not None:
build_name = str(os.getenv("BUILD_NAME"))
if not silent:
print(f"Using custom build name: '{build_name}'.")
print_info(f"Using custom build name: '{build_name}'.")
import version
@ -221,7 +140,7 @@ def get_version_info(module_version_string="", silent=False):
if os.getenv("GODOT_VERSION_STATUS") is not None:
version_info["status"] = str(os.getenv("GODOT_VERSION_STATUS"))
if not silent:
print(f"Using version status '{version_info['status']}', overriding the original '{version.status}'.")
print_info(f"Using version status '{version_info['status']}', overriding the original '{version.status}'.")
# Parse Git hash if we're in a Git repo.
githash = ""
@ -277,34 +196,6 @@ def get_version_info(module_version_string="", silent=False):
return version_info
def parse_cg_file(fname, uniforms, sizes, conditionals):
with open(fname, "r", encoding="utf-8") as fs:
line = fs.readline()
while line:
if re.match(r"^\s*uniform", line):
res = re.match(r"uniform ([\d\w]*) ([\d\w]*)")
type = res.groups(1)
name = res.groups(2)
uniforms.append(name)
if type.find("texobj") != -1:
sizes.append(1)
else:
t = re.match(r"float(\d)x(\d)", type)
if t:
sizes.append(int(t.groups(1)) * int(t.groups(2)))
else:
t = re.match(r"float(\d)", type)
sizes.append(int(t.groups(1)))
if line.find("[branch]") != -1:
conditionals.append(name)
line = fs.readline()
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.
@ -404,10 +295,6 @@ def convert_custom_modules_path(path):
return path
def disable_module(self):
self.disabled_modules.append(self.current_module)
def module_add_dependencies(self, module, dependencies, optional=False):
"""
Adds dependencies for a given module.
@ -428,19 +315,21 @@ def module_check_dependencies(self, module):
Meant to be used in module `can_build` methods.
Returns a boolean (True if dependencies are satisfied).
"""
missing_deps = []
missing_deps = set()
required_deps = self.module_dependencies[module][0] if module in self.module_dependencies else []
for dep in required_deps:
opt = "module_{}_enabled".format(dep)
if opt not in self or not self[opt]:
missing_deps.append(dep)
if opt not in self or not self[opt] or not module_check_dependencies(self, dep):
missing_deps.add(dep)
if missing_deps != []:
print_warning(
"Disabling '{}' module as the following dependencies are not satisfied: {}".format(
module, ", ".join(missing_deps)
if missing_deps:
if module not in self.disabled_modules:
print_warning(
"Disabling '{}' module as the following dependencies are not satisfied: {}".format(
module, ", ".join(missing_deps)
)
)
)
self.disabled_modules.add(module)
return False
else:
return True
@ -467,16 +356,6 @@ def use_windows_spawn_fix(self, platform=None):
if os.name != "nt":
return # not needed, only for windows
# On Windows, due to the limited command line length, when creating a static library
# from a very high number of objects SCons will invoke "ar" once per object file;
# that makes object files with same names to be overwritten so the last wins and
# the library loses symbols defined by overwritten objects.
# By enabling quick append instead of the default mode (replacing), libraries will
# got built correctly regardless the invocation strategy.
# Furthermore, since SCons will rebuild the library from scratch when an object file
# changes, no multiple versions of the same object file will be present.
self.Replace(ARFLAGS="q")
def mySubProcess(cmdline, env):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
@ -488,29 +367,28 @@ def use_windows_spawn_fix(self, platform=None):
"shell": False,
"env": env,
}
if sys.version_info >= (3, 7, 0):
popen_args["text"] = True
popen_args["text"] = True
proc = subprocess.Popen(cmdline, **popen_args)
_, err = proc.communicate()
rv = proc.wait()
if rv:
print_error(err)
elif len(err) > 0 and not err.isspace():
print(err)
return rv
def mySpawn(sh, escape, cmd, args, env):
# Used by TEMPFILE.
if cmd == "del":
os.remove(args[1])
return 0
newargs = " ".join(args[1:])
cmdline = cmd + " " + newargs
rv = 0
env = {str(key): str(value) for key, value in iter(env.items())}
if len(cmdline) > 32000 and cmd.endswith("ar"):
cmdline = cmd + " " + args[1] + " " + args[2] + " "
for i in range(3, len(args)):
rv = mySubProcess(cmdline + args[i], env)
if rv:
break
else:
rv = mySubProcess(cmdline, env)
rv = mySubProcess(cmdline, env)
return rv
@ -518,7 +396,9 @@ def use_windows_spawn_fix(self, platform=None):
def no_verbose(env):
colors = [ANSI.BLUE, ANSI.BOLD, ANSI.REGULAR, ANSI.RESET]
from misc.utility.color import Ansi
colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET]
# There is a space before "..." to ensure that source file names can be
# Ctrl + clicked in the VS Code terminal.
@ -575,40 +455,7 @@ def detect_visual_c_compiler_version(tools_env):
vc_chosen_compiler_index = -1
vc_chosen_compiler_str = ""
# Start with Pre VS 2017 checks which uses VCINSTALLDIR:
if "VCINSTALLDIR" in tools_env:
# print("Checking VCINSTALLDIR")
# find() works with -1 so big ifs below are needed... the simplest solution, in fact
# First test if amd64 and amd64_x86 compilers are present in the path
vc_amd64_compiler_detection_index = tools_env["PATH"].find(tools_env["VCINSTALLDIR"] + "BIN\\amd64;")
if vc_amd64_compiler_detection_index > -1:
vc_chosen_compiler_index = vc_amd64_compiler_detection_index
vc_chosen_compiler_str = "amd64"
vc_amd64_x86_compiler_detection_index = tools_env["PATH"].find(tools_env["VCINSTALLDIR"] + "BIN\\amd64_x86;")
if vc_amd64_x86_compiler_detection_index > -1 and (
vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_amd64_x86_compiler_detection_index
):
vc_chosen_compiler_index = vc_amd64_x86_compiler_detection_index
vc_chosen_compiler_str = "amd64_x86"
# Now check the 32 bit compilers
vc_x86_compiler_detection_index = tools_env["PATH"].find(tools_env["VCINSTALLDIR"] + "BIN;")
if vc_x86_compiler_detection_index > -1 and (
vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_x86_compiler_detection_index
):
vc_chosen_compiler_index = vc_x86_compiler_detection_index
vc_chosen_compiler_str = "x86"
vc_x86_amd64_compiler_detection_index = tools_env["PATH"].find(tools_env["VCINSTALLDIR"] + "BIN\\x86_amd64;")
if vc_x86_amd64_compiler_detection_index > -1 and (
vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_x86_amd64_compiler_detection_index
):
vc_chosen_compiler_index = vc_x86_amd64_compiler_detection_index
vc_chosen_compiler_str = "x86_amd64"
# and for VS 2017 and newer we check VCTOOLSINSTALLDIR:
# VS 2017 and newer should set VCTOOLSINSTALLDIR
if "VCTOOLSINSTALLDIR" in tools_env:
# Newer versions have a different path available
vc_amd64_compiler_detection_index = (
@ -705,23 +552,6 @@ def glob_recursive(pattern, node="."):
return results
def add_to_vs_project(env, sources):
for x in sources:
fname = env.File(x).path if isinstance(x, str) else env.File(x)[0].path
pieces = fname.split(".")
if len(pieces) > 0:
basename = pieces[0]
basename = basename.replace("\\\\", "/")
if os.path.isfile(basename + ".h"):
env.vs_incs += [basename + ".h"]
elif os.path.isfile(basename + ".hpp"):
env.vs_incs += [basename + ".hpp"]
if os.path.isfile(basename + ".c"):
env.vs_srcs += [basename + ".c"]
elif os.path.isfile(basename + ".cpp"):
env.vs_srcs += [basename + ".cpp"]
def precious_program(env, program, sources, **args):
program = env.ProgramOriginal(program, sources, **args)
env.Precious(program)
@ -782,15 +612,19 @@ def detect_darwin_sdk_path(platform, env):
raise
def is_vanilla_clang(env):
def is_apple_clang(env):
import shlex
if env["platform"] not in ["macos", "ios"]:
return False
if not using_clang(env):
return False
try:
version = subprocess.check_output([env.subst(env["CXX"]), "--version"]).strip().decode("utf-8")
version = subprocess.check_output(shlex.split(env.subst(env["CXX"])) + ["--version"]).strip().decode("utf-8")
except (subprocess.CalledProcessError, OSError):
print_warning("Couldn't parse CXX environment variable to infer compiler version.")
return False
return not version.startswith("Apple")
return version.startswith("Apple")
def get_compiler_version(env):
@ -801,13 +635,15 @@ def get_compiler_version(env):
- metadata1, metadata2: Extra information
- date: Date of the build
"""
import shlex
ret = {
"major": -1,
"minor": -1,
"patch": -1,
"metadata1": None,
"metadata2": None,
"date": None,
"metadata1": "",
"metadata2": "",
"date": "",
"apple_major": -1,
"apple_minor": -1,
"apple_patch1": -1,
@ -815,21 +651,48 @@ def get_compiler_version(env):
"apple_patch3": -1,
}
if not env.msvc:
# Not using -dumpversion as some GCC distros only return major, and
# Clang used to return hardcoded 4.2.1: # https://reviews.llvm.org/D56803
if env.msvc and not using_clang(env):
try:
version = (
subprocess.check_output([env.subst(env["CXX"]), "--version"], shell=(os.name == "nt"))
.strip()
.decode("utf-8")
)
# FIXME: `-latest` works for most cases, but there are edge-cases where this would
# benefit from a more nuanced search.
# https://github.com/godotengine/godot/pull/91069#issuecomment-2358956731
# https://github.com/godotengine/godot/pull/91069#issuecomment-2380836341
args = [
env["VSWHERE"],
"-latest",
"-prerelease",
"-products",
"*",
"-requires",
"Microsoft.Component.MSBuild",
"-utf8",
]
version = subprocess.check_output(args, encoding="utf-8").strip()
for line in version.splitlines():
split = line.split(":", 1)
if split[0] == "catalog_productDisplayVersion":
sem_ver = split[1].split(".")
ret["major"] = int(sem_ver[0])
ret["minor"] = int(sem_ver[1])
ret["patch"] = int(sem_ver[2].split()[0])
# Could potentially add section for determining preview version, but
# that can wait until metadata is actually used for something.
if split[0] == "catalog_buildVersion":
ret["metadata1"] = split[1]
except (subprocess.CalledProcessError, OSError):
print_warning("Couldn't parse CXX environment variable to infer compiler version.")
return ret
else:
# TODO: Implement for MSVC
print_warning("Couldn't find vswhere to determine compiler version.")
return 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
try:
version = subprocess.check_output(
shlex.split(env.subst(env["CXX"])) + ["--version"], shell=(os.name == "nt"), encoding="utf-8"
).strip()
except (subprocess.CalledProcessError, OSError):
print_warning("Couldn't parse CXX environment variable to infer compiler version.")
return ret
match = re.search(
r"(?:(?<=version )|(?<=\) )|(?<=^))"
r"(?P<major>\d+)"
@ -887,135 +750,167 @@ def using_emcc(env):
def show_progress(env):
# Ninja has its own progress/tracking tool that clashes with ours.
if env["ninja"]:
# Has its own progress/tracking tool that clashes with ours
return
import sys
NODE_COUNT_FILENAME = f"{base_folder_path}.scons_node_count"
from SCons.Script import AlwaysBuild, Command, Progress
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
screen = sys.stdout
# Progress reporting is not available in non-TTY environments since it
# messes with the output (for example, when writing to a file)
show_progress = env["progress"] and sys.stdout.isatty()
node_count = 0
node_count_max = 0
node_count_interval = 1
node_count_fname = str(env.Dir("#")) + "/.scons_node_count"
import math
import time
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 is not 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())
# 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())
if self.display and not self.max:
print_info("Performing initial build, progress percentage unavailable!")
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", encoding="utf-8", newline="\n") 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, "r", encoding="utf-8") as f:
node_count_max = int(f.readline())
except Exception:
pass
atexit.register(progress_finish)
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)
def convert_size(size_bytes: int) -> str:
if size_bytes == 0:
return "0 bytes"
SIZE_NAMES = ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
index = math.floor(math.log(size_bytes, 1024))
power = math.pow(1024, index)
size = round(size_bytes / power, 2)
return f"{size} {SIZE_NAMES[index]}"
def get_size(start_path: str = ".") -> int:
total_size = 0
for dirpath, _, filenames in os.walk(start_path):
for file in filenames:
path = os.path.join(dirpath, file)
total_size += os.path.getsize(path)
return total_size
def clean_cache(cache_path: str, cache_limit: int, verbose: bool) -> None:
if not cache_limit:
return
files = glob.glob(os.path.join(cache_path, "*", "*"))
if not files:
return
# Store files in list of (filename, size, atime).
stats = []
for file in files:
try:
stats.append((file, *os.stat(file)[6:8]))
except OSError:
print_error(f'Failed to access cache file "{file}"; skipping.')
# Sort by most recent access (most sensible to keep) first. Search for the first entry where
# the cache limit is reached.
stats.sort(key=lambda x: x[2], reverse=True)
sum = 0
for index, stat in enumerate(stats):
sum += stat[1]
if sum > cache_limit:
purge = [x[0] for x in stats[index:]]
count = len(purge)
for file in purge:
try:
os.remove(file)
except OSError:
print_error(f'Failed to remove cache file "{file}"; skipping.')
count -= 1
if verbose and count:
print_info(f"Purged {count} file{'s' if count else ''} from cache.")
break
def prepare_cache(env) -> None:
if env.GetOption("clean"):
return
cache_path = ""
if env["cache_path"]:
cache_path = cast(str, env["cache_path"])
elif os.environ.get("SCONS_CACHE"):
print_warning("Environment variable `SCONS_CACHE` is deprecated; use `cache_path` argument instead.")
cache_path = cast(str, os.environ.get("SCONS_CACHE"))
if not cache_path:
return
env.CacheDir(cache_path)
print(f'SCons cache enabled... (path: "{cache_path}")')
if env["cache_limit"]:
cache_limit = float(env["cache_limit"])
elif os.environ.get("SCONS_CACHE_LIMIT"):
print_warning("Environment variable `SCONS_CACHE_LIMIT` is deprecated; use `cache_limit` argument instead.")
cache_limit = float(os.getenv("SCONS_CACHE_LIMIT", "0")) / 1024 # Old method used MiB, convert to GiB
# 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)),
)
)
atexit.register(clean_cache, cache_path, cache_limit, env["verbose"])
def prepare_purge(env):
from SCons.Script.Main import GetBuildFailures
def purge_flaky_files():
paths_to_keep = [env["ninja_file"]]
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.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}")
atexit.register(print_elapsed_time, time.time())
def dump(env):
@ -1086,6 +981,30 @@ def generate_vs_project(env, original_args, project_name="godot"):
return v[0] if len(v) == 1 else f"{v[0]}={v[1]}"
return v
def get_dependencies(file, env, exts, headers, sources, others):
for child in file.children():
if isinstance(child, str):
child = env.File(x)
fname = ""
try:
fname = child.path
except AttributeError:
# It's not a file.
pass
if fname:
parts = os.path.splitext(fname)
if len(parts) > 1:
ext = parts[1].lower()
if ext in exts["sources"]:
sources += [fname]
elif ext in exts["headers"]:
headers += [fname]
elif ext in exts["others"]:
others += [fname]
get_dependencies(child, env, exts, headers, sources, others)
filtered_args = original_args.copy()
# Ignore the "vsproj" option to not regenerate the VS project on every build
@ -1147,35 +1066,35 @@ def generate_vs_project(env, original_args, project_name="godot"):
sys.path.remove(tmppath)
sys.modules.pop("msvs")
extensions = {}
extensions["headers"] = [".h", ".hh", ".hpp", ".hxx", ".inc"]
extensions["sources"] = [".c", ".cc", ".cpp", ".cxx", ".m", ".mm", ".java"]
extensions["others"] = [".natvis", ".glsl", ".rc"]
headers = []
headers_dirs = []
for file in glob_recursive_2("*.h", headers_dirs):
headers.append(str(file).replace("/", "\\"))
for file in glob_recursive_2("*.hpp", headers_dirs):
headers.append(str(file).replace("/", "\\"))
for ext in extensions["headers"]:
for file in glob_recursive_2("*" + ext, headers_dirs):
headers.append(str(file).replace("/", "\\"))
sources = []
sources_dirs = []
for file in glob_recursive_2("*.cpp", sources_dirs):
sources.append(str(file).replace("/", "\\"))
for file in glob_recursive_2("*.c", sources_dirs):
sources.append(str(file).replace("/", "\\"))
for ext in extensions["sources"]:
for file in glob_recursive_2("*" + ext, sources_dirs):
sources.append(str(file).replace("/", "\\"))
others = []
others_dirs = []
for file in glob_recursive_2("*.natvis", others_dirs):
others.append(str(file).replace("/", "\\"))
for file in glob_recursive_2("*.glsl", others_dirs):
others.append(str(file).replace("/", "\\"))
for ext in extensions["others"]:
for file in glob_recursive_2("*" + ext, others_dirs):
others.append(str(file).replace("/", "\\"))
skip_filters = False
import hashlib
import json
md5 = hashlib.md5(
json.dumps(headers + headers_dirs + sources + sources_dirs + others + others_dirs, sort_keys=True).encode(
"utf-8"
)
json.dumps(sorted(headers + headers_dirs + sources + sources_dirs + others + others_dirs)).encode("utf-8")
).hexdigest()
if os.path.exists(f"{project_name}.vcxproj.filters"):
@ -1232,58 +1151,13 @@ def generate_vs_project(env, original_args, project_name="godot"):
with open(f"{project_name}.vcxproj.filters", "w", encoding="utf-8", newline="\r\n") as f:
f.write(filters_template)
envsources = []
envsources += env.core_sources
envsources += env.drivers_sources
envsources += env.main_sources
envsources += env.modules_sources
envsources += env.scene_sources
envsources += env.servers_sources
if env.editor_build:
envsources += env.editor_sources
envsources += env.platform_sources
headers_active = []
sources_active = []
others_active = []
for x in envsources:
fname = ""
if isinstance(x, str):
fname = env.File(x).path
else:
# Some object files might get added directly as a File object and not a list.
try:
fname = env.File(x)[0].path
except Exception:
fname = x.path
pass
if fname:
fname = fname.replace("\\\\", "/")
parts = os.path.splitext(fname)
basename = parts[0]
ext = parts[1]
idx = fname.find(env["OBJSUFFIX"])
if ext in [".h", ".hpp"]:
headers_active += [fname]
elif ext in [".c", ".cpp"]:
sources_active += [fname]
elif idx > 0:
basename = fname[:idx]
if os.path.isfile(basename + ".h"):
headers_active += [basename + ".h"]
elif os.path.isfile(basename + ".hpp"):
headers_active += [basename + ".hpp"]
elif basename.endswith(".gen") and os.path.isfile(basename[:-4] + ".h"):
headers_active += [basename[:-4] + ".h"]
if os.path.isfile(basename + ".c"):
sources_active += [basename + ".c"]
elif os.path.isfile(basename + ".cpp"):
sources_active += [basename + ".cpp"]
else:
fname = os.path.relpath(os.path.abspath(fname), env.Dir("").abspath)
others_active += [fname]
get_dependencies(
env.File(f"#bin/godot{env['PROGSUFFIX']}"), env, extensions, headers_active, sources_active, others_active
)
all_items = []
properties = []
@ -1327,7 +1201,7 @@ def generate_vs_project(env, original_args, project_name="godot"):
vsconf = ""
for a in vs_configuration["arches"]:
if arch == a["architecture"]:
vsconf = f'{target}|{a["platform"]}'
vsconf = f"{target}|{a['platform']}"
break
condition = "'$(GodotConfiguration)|$(GodotPlatform)'=='" + vsconf + "'"
@ -1343,7 +1217,7 @@ def generate_vs_project(env, original_args, project_name="godot"):
properties.append(
"<ActiveProjectItemList_%s>;%s;</ActiveProjectItemList_%s>" % (x, ";".join(itemlist[x]), x)
)
output = f'bin\\godot{env["PROGSUFFIX"]}'
output = f"bin\\godot{env['PROGSUFFIX']}"
with open("misc/msvs/props.template", "r", encoding="utf-8") as file:
props_template = file.read()
@ -1569,7 +1443,7 @@ def generate_copyright_header(filename: str) -> str:
"""
filename = filename.split("/")[-1].ljust(MARGIN)
if len(filename) > MARGIN:
print(f'WARNING: Filename "{filename}" too large for copyright header.')
print_warning(f'Filename "{filename}" too large for copyright header.')
return TEMPLATE % filename
@ -1579,7 +1453,7 @@ def generated_wrapper(
guard: Optional[bool] = None,
prefix: str = "",
suffix: str = "",
) -> Generator[TextIOWrapper, None, None]:
) -> 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
@ -1601,8 +1475,7 @@ def generated_wrapper(
if isinstance(path, list):
if len(path) > 1:
print_warning(
"Attempting to use generated wrapper with multiple targets; "
f"will only use first entry: {path[0]}"
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"):
@ -1641,3 +1514,43 @@ def generated_wrapper(
file.write(f"\n\n#endif // {header_guard}")
file.write("\n")
def to_raw_cstring(value: Union[str, List[str]]) -> str:
MAX_LITERAL = 16 * 1024
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]
return " ".join(f'R"<!>({x.decode()})<!>"' for x in split)