e4dcf1d852
This method is used to generate headers for embedding files into the binary (think about the new `#embed` feature in C23 and C++26). While the stringification step itself was plenty fast, it then proceeded to wrap everything using the `textwrap` module. `textwrap` is *very* slow, as it's apparently optimized for human text. This patch reimplements the wrapping logic using a simple regex, resulting in a tremendous speed improvement (~6x), and switches to `map` for the stringification itself (thanks Rémi!) It also removes a (practically) unused argument, `initial_indent`. The generated files are pretty much the same, with a tiny difference in line length (for some reason the old logic overshot the requested line length)
292 lines
9.9 KiB
Python
292 lines
9.9 KiB
Python
"""Functions used to generate source files during build time"""
|
|
|
|
from collections import OrderedDict
|
|
from io import TextIOWrapper
|
|
|
|
import methods
|
|
|
|
|
|
# Generate disabled classes
|
|
def disabled_class_builder(target, source, env):
|
|
with methods.generated_wrapper(str(target[0])) as file:
|
|
for c in source[0].read():
|
|
if cs := c.strip():
|
|
file.write(f"class {cs}; template <> struct is_class_enabled<{cs}> : std::false_type {{}};\n")
|
|
|
|
|
|
# Generate version info
|
|
def version_info_builder(target, source, env):
|
|
with methods.generated_wrapper(str(target[0])) as file:
|
|
file.write(
|
|
"""\
|
|
#define GODOT_VERSION_SHORT_NAME "{short_name}"
|
|
#define GODOT_VERSION_NAME "{name}"
|
|
#define GODOT_VERSION_MAJOR {major}
|
|
#define GODOT_VERSION_MINOR {minor}
|
|
#define GODOT_VERSION_PATCH {patch}
|
|
#define GODOT_VERSION_STATUS "{status}"
|
|
#define GODOT_VERSION_BUILD "{build}"
|
|
#define GODOT_VERSION_MODULE_CONFIG "{module_config}"
|
|
#define GODOT_VERSION_WEBSITE "{website}"
|
|
#define GODOT_VERSION_DOCS_BRANCH "{docs_branch}"
|
|
#define GODOT_VERSION_DOCS_URL "https://docs.godotengine.org/en/" GODOT_VERSION_DOCS_BRANCH
|
|
""".format(**source[0].read())
|
|
)
|
|
|
|
|
|
def version_hash_builder(target, source, env):
|
|
with methods.generated_wrapper(str(target[0])) as file:
|
|
file.write(
|
|
"""\
|
|
#include "core/version.h"
|
|
|
|
const char *const GODOT_VERSION_HASH = "{git_hash}";
|
|
const unsigned long long GODOT_VERSION_TIMESTAMP = {git_timestamp};
|
|
""".format(**source[0].read())
|
|
)
|
|
|
|
|
|
def encryption_key_builder(target, source, env):
|
|
src = source[0].read() or "0" * 64
|
|
try:
|
|
buffer = bytes.fromhex(src)
|
|
if len(buffer) != 32:
|
|
raise ValueError
|
|
except ValueError:
|
|
methods.print_error(
|
|
f'Invalid AES256 encryption key, not 64 hexadecimal characters: "{src}".\n'
|
|
"Unset `SCRIPT_AES256_ENCRYPTION_KEY` in your environment "
|
|
"or make sure that it contains exactly 64 hexadecimal characters."
|
|
)
|
|
raise
|
|
|
|
with methods.generated_wrapper(str(target[0])) as file:
|
|
file.write(
|
|
f"""\
|
|
#include <cstdint>
|
|
|
|
uint8_t script_encryption_key[32] = {{
|
|
{methods.format_buffer(buffer, 1)}
|
|
}};"""
|
|
)
|
|
|
|
|
|
def make_certs_header(target, source, env):
|
|
buffer = methods.get_buffer(str(source[0]))
|
|
decomp_size = len(buffer)
|
|
buffer = methods.compress_buffer(buffer)
|
|
|
|
with methods.generated_wrapper(str(target[0])) as file:
|
|
# System certs path. Editor will use them if defined. (for package maintainers)
|
|
file.write('#define _SYSTEM_CERTS_PATH "{}"\n'.format(source[2].read() or ""))
|
|
if source[1].read():
|
|
# Defined here and not in env so changing it does not trigger a full rebuild.
|
|
file.write(f"""\
|
|
#define BUILTIN_CERTS_ENABLED
|
|
|
|
inline constexpr int _certs_compressed_size = {len(buffer)};
|
|
inline constexpr int _certs_uncompressed_size = {decomp_size};
|
|
inline constexpr unsigned char _certs_compressed[] = {{
|
|
{methods.format_buffer(buffer, 1)}
|
|
}};
|
|
""")
|
|
|
|
|
|
def make_authors_header(target, source, env):
|
|
SECTIONS = {
|
|
"Project Founders": "AUTHORS_FOUNDERS",
|
|
"Lead Developer": "AUTHORS_LEAD_DEVELOPERS",
|
|
"Project Manager": "AUTHORS_PROJECT_MANAGERS",
|
|
"Developers": "AUTHORS_DEVELOPERS",
|
|
}
|
|
buffer = methods.get_buffer(str(source[0]))
|
|
reading = False
|
|
|
|
with methods.generated_wrapper(str(target[0])) as file:
|
|
|
|
def close_section():
|
|
file.write("\tnullptr,\n};\n\n")
|
|
|
|
for line in buffer.decode().splitlines():
|
|
if line.startswith(" ") and reading:
|
|
file.write(f'\t"{methods.to_escaped_cstring(line).strip()}",\n')
|
|
elif line.startswith("## "):
|
|
if reading:
|
|
close_section()
|
|
reading = False
|
|
section = SECTIONS[line[3:].strip()]
|
|
if section:
|
|
file.write(f"inline constexpr const char *{section}[] = {{\n")
|
|
reading = True
|
|
|
|
if reading:
|
|
close_section()
|
|
|
|
|
|
def make_donors_header(target, source, env):
|
|
SECTIONS = {
|
|
"Patrons": "DONORS_PATRONS",
|
|
"Platinum sponsors": "DONORS_SPONSORS_PLATINUM",
|
|
"Gold sponsors": "DONORS_SPONSORS_GOLD",
|
|
"Silver sponsors": "DONORS_SPONSORS_SILVER",
|
|
"Diamond members": "DONORS_MEMBERS_DIAMOND",
|
|
"Titanium members": "DONORS_MEMBERS_TITANIUM",
|
|
"Platinum members": "DONORS_MEMBERS_PLATINUM",
|
|
"Gold members": "DONORS_MEMBERS_GOLD",
|
|
}
|
|
buffer = methods.get_buffer(str(source[0]))
|
|
reading = False
|
|
|
|
with methods.generated_wrapper(str(target[0])) as file:
|
|
|
|
def close_section():
|
|
file.write("\tnullptr,\n};\n\n")
|
|
|
|
for line in buffer.decode().splitlines():
|
|
if line.startswith(" ") and reading:
|
|
file.write(f'\t"{methods.to_escaped_cstring(line).strip()}",\n')
|
|
elif line.startswith("## "):
|
|
if reading:
|
|
close_section()
|
|
reading = False
|
|
section = SECTIONS.get(line[3:].strip())
|
|
if section:
|
|
file.write(f"inline constexpr const char *{section}[] = {{\n")
|
|
reading = True
|
|
|
|
if reading:
|
|
close_section()
|
|
|
|
|
|
def make_license_header(target, source, env):
|
|
src_copyright = str(source[0])
|
|
src_license = str(source[1])
|
|
|
|
class LicenseReader:
|
|
def __init__(self, license_file: TextIOWrapper):
|
|
self._license_file = license_file
|
|
self.line_num = 0
|
|
self.current = self.next_line()
|
|
|
|
def next_line(self):
|
|
line = self._license_file.readline()
|
|
self.line_num += 1
|
|
while line.startswith("#"):
|
|
line = self._license_file.readline()
|
|
self.line_num += 1
|
|
self.current = line
|
|
return line
|
|
|
|
def next_tag(self):
|
|
if ":" not in self.current:
|
|
return ("", [])
|
|
tag, line = self.current.split(":", 1)
|
|
lines = [line.strip()]
|
|
while self.next_line() and self.current.startswith(" "):
|
|
lines.append(self.current.strip())
|
|
return (tag, lines)
|
|
|
|
projects = OrderedDict()
|
|
license_list = []
|
|
|
|
with open(src_copyright, "r", encoding="utf-8") as copyright_file:
|
|
reader = LicenseReader(copyright_file)
|
|
part = {}
|
|
while reader.current:
|
|
tag, content = reader.next_tag()
|
|
if tag in ("Files", "Copyright", "License"):
|
|
part[tag] = content[:]
|
|
elif tag == "Comment" and part:
|
|
# attach non-empty part to named project
|
|
projects[content[0]] = projects.get(content[0], []) + [part]
|
|
|
|
if not tag or not reader.current:
|
|
# end of a paragraph start a new part
|
|
if "License" in part and "Files" not in part:
|
|
# no Files tag in this one, so assume standalone license
|
|
license_list.append(part["License"])
|
|
part = {}
|
|
reader.next_line()
|
|
|
|
data_list = []
|
|
for project in iter(projects.values()):
|
|
for part in project:
|
|
part["file_index"] = len(data_list)
|
|
data_list += part["Files"]
|
|
part["copyright_index"] = len(data_list)
|
|
data_list += part["Copyright"]
|
|
|
|
with open(src_license, "r", encoding="utf-8") as file:
|
|
license_text = file.read()
|
|
|
|
with methods.generated_wrapper(str(target[0])) as file:
|
|
file.write(f"""\
|
|
inline constexpr const char *GODOT_LICENSE_TEXT = {{
|
|
{methods.to_raw_cstring(license_text)}
|
|
}};
|
|
|
|
struct ComponentCopyrightPart {{
|
|
const char *license;
|
|
const char *const *files;
|
|
const char *const *copyright_statements;
|
|
int file_count;
|
|
int copyright_count;
|
|
}};
|
|
|
|
struct ComponentCopyright {{
|
|
const char *name;
|
|
const ComponentCopyrightPart *parts;
|
|
int part_count;
|
|
}};
|
|
|
|
""")
|
|
|
|
file.write("inline constexpr const char *COPYRIGHT_INFO_DATA[] = {\n")
|
|
for line in data_list:
|
|
file.write(f'\t"{methods.to_escaped_cstring(line)}",\n')
|
|
file.write("};\n\n")
|
|
|
|
file.write("inline constexpr ComponentCopyrightPart COPYRIGHT_PROJECT_PARTS[] = {\n")
|
|
part_index = 0
|
|
part_indexes = {}
|
|
for project_name, project in iter(projects.items()):
|
|
part_indexes[project_name] = part_index
|
|
for part in project:
|
|
file.write(
|
|
f'\t{{ "{methods.to_escaped_cstring(part["License"][0])}", '
|
|
+ f"©RIGHT_INFO_DATA[{part['file_index']}], "
|
|
+ f"©RIGHT_INFO_DATA[{part['copyright_index']}], "
|
|
+ f"{len(part['Files'])}, {len(part['Copyright'])} }},\n"
|
|
)
|
|
part_index += 1
|
|
file.write("};\n\n")
|
|
|
|
file.write(f"inline constexpr int COPYRIGHT_INFO_COUNT = {len(projects)};\n")
|
|
|
|
file.write("inline constexpr ComponentCopyright COPYRIGHT_INFO[] = {\n")
|
|
for project_name, project in iter(projects.items()):
|
|
file.write(
|
|
f'\t{{ "{methods.to_escaped_cstring(project_name)}", '
|
|
+ f"©RIGHT_PROJECT_PARTS[{part_indexes[project_name]}], "
|
|
+ f"{len(project)} }},\n"
|
|
)
|
|
file.write("};\n\n")
|
|
|
|
file.write(f"inline constexpr int LICENSE_COUNT = {len(license_list)};\n")
|
|
|
|
file.write("inline constexpr const char *LICENSE_NAMES[] = {\n")
|
|
for license in license_list:
|
|
file.write(f'\t"{methods.to_escaped_cstring(license[0])}",\n')
|
|
file.write("};\n\n")
|
|
|
|
file.write("inline constexpr const char *LICENSE_BODIES[] = {\n\n")
|
|
for license in license_list:
|
|
to_raw = []
|
|
for line in license[1:]:
|
|
if line == ".":
|
|
to_raw += [""]
|
|
else:
|
|
to_raw += [line]
|
|
file.write(f"{methods.to_raw_cstring(to_raw)},\n\n")
|
|
file.write("};\n\n")
|