This commit is contained in:
Spartan322
2025-04-19 07:12:16 -04:00
5209 changed files with 229124 additions and 66199 deletions

View File

@@ -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 &amp;&amp; cmd /V /C " + commands
commands = "echo Starting SCons &amp; " + commands
else:
common_build_prefix[0] = "echo Starting SCons &amp;&amp; cmd /V /C " + common_build_prefix[0]
common_build_prefix[0] = "echo Starting SCons &amp; " + common_build_prefix[0]
cmd = " ^&amp; ".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 = " ^&amp; ".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 = " ^&amp; ".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()]