From be429eb4044f37d573d7d2de07d9daf30f0b31fd Mon Sep 17 00:00:00 2001 From: Thaddeus Crews Date: Tue, 29 Oct 2024 14:23:08 -0500 Subject: [PATCH] SCons: Make builders prettier, utilize `constexpr` --- SConstruct | 2 - core/SCsub | 42 +-- core/core_builders.py | 305 +++++++----------- core/extension/make_interface_dumper.py | 63 ++-- core/input/input_builders.py | 48 +-- editor/SCsub | 36 +-- editor/editor_builders.py | 160 ++++----- editor/editor_translation.cpp | 12 +- editor/icons/editor_icons_builders.py | 86 ++--- editor/template_builders.py | 69 ++-- editor/themes/SCsub | 11 +- editor/themes/editor_theme_builders.py | 34 +- main/SCsub | 3 - main/main_builders.py | 70 ++-- methods.py | 125 ++++--- modules/SCsub | 32 +- modules/text_server_adv/SCsub | 35 +- platform/SCsub | 6 +- scene/theme/SCsub | 1 - scene/theme/default_theme_builders.py | 35 +- .../icons/default_theme_icons_builders.py | 58 ++-- 21 files changed, 510 insertions(+), 723 deletions(-) diff --git a/SConstruct b/SConstruct index 020ec3ea2cd..87bf5f2f8b1 100644 --- a/SConstruct +++ b/SConstruct @@ -968,8 +968,6 @@ if env.editor_build: print_error("Not all modules required by editor builds are enabled.") Exit(255) -env.version_info = methods.get_version_info(env.module_version_string) - env["PROGSUFFIX_WRAP"] = suffix + env.module_version_string + ".console" + env["PROGSUFFIX"] env["PROGSUFFIX"] = suffix + env.module_version_string + env["PROGSUFFIX"] env["OBJSUFFIX"] = suffix + env["OBJSUFFIX"] diff --git a/core/SCsub b/core/SCsub index f055a75cb75..c847f50c404 100644 --- a/core/SCsub +++ b/core/SCsub @@ -167,10 +167,9 @@ env.add_source_files(env.core_sources, "*.cpp") # Generate disabled classes def disabled_class_builder(target, source, env): - with methods.generated_wrapper(target) as file: + with methods.generated_wrapper(str(target[0])) as file: for c in source[0].read(): - cs = c.strip() - if cs != "": + if cs := c.strip(): file.write(f"#define ClassDB_Disable_{cs} 1\n") @@ -179,7 +178,7 @@ env.CommandNoCache("disabled_classes.gen.h", env.Value(env.disabled_classes), en # Generate version info def version_info_builder(target, source, env): - with methods.generated_wrapper(target) as file: + with methods.generated_wrapper(str(target[0])) as file: file.write( """\ #define VERSION_SHORT_NAME "{short_name}" @@ -193,35 +192,37 @@ def version_info_builder(target, source, env): #define VERSION_WEBSITE "{website}" #define VERSION_DOCS_BRANCH "{docs_branch}" #define VERSION_DOCS_URL "https://docs.godotengine.org/en/" VERSION_DOCS_BRANCH -""".format(**env.version_info) +""".format(**source[0].read()) ) -env.CommandNoCache("version_generated.gen.h", env.Value(env.version_info), env.Run(version_info_builder)) +env.CommandNoCache( + "version_generated.gen.h", + env.Value(methods.get_version_info(env.module_version_string)), + env.Run(version_info_builder), +) # Generate version hash def version_hash_builder(target, source, env): - with methods.generated_wrapper(target) as file: + with methods.generated_wrapper(str(target[0])) as file: file.write( """\ #include "core/version.h" const char *const VERSION_HASH = "{git_hash}"; const uint64_t VERSION_TIMESTAMP = {git_timestamp}; -""".format(**env.version_info) +""".format(**source[0].read()) ) -gen_hash = env.CommandNoCache( - "version_hash.gen.cpp", env.Value(env.version_info["git_hash"]), env.Run(version_hash_builder) -) +gen_hash = env.CommandNoCache("version_hash.gen.cpp", env.Value(methods.get_git_info()), env.Run(version_hash_builder)) env.add_source_files(env.core_sources, gen_hash) # Generate AES256 script encryption key def encryption_key_builder(target, source, env): - with methods.generated_wrapper(target) as file: + with methods.generated_wrapper(str(target[0])) as file: file.write( f"""\ #include "core/config/project_settings.h" @@ -251,30 +252,21 @@ env.add_source_files(env.core_sources, gen_encrypt) # Certificates -env.Depends( - "#core/io/certs_compressed.gen.h", - ["#thirdparty/certs/ca-certificates.crt", env.Value(env["builtin_certs"]), env.Value(env["system_certs_path"])], -) env.CommandNoCache( "#core/io/certs_compressed.gen.h", - "#thirdparty/certs/ca-certificates.crt", + ["#thirdparty/certs/ca-certificates.crt", env.Value(env["builtin_certs"]), env.Value(env["system_certs_path"])], env.Run(core_builders.make_certs_header), ) # Authors -env.Depends("#core/authors.gen.h", "../AUTHORS.md") -env.CommandNoCache("#core/authors.gen.h", "../AUTHORS.md", env.Run(core_builders.make_authors_header)) +env.CommandNoCache("#core/authors.gen.h", "#AUTHORS.md", env.Run(core_builders.make_authors_header)) # Donors -env.Depends("#core/donors.gen.h", "../DONORS.md") -env.CommandNoCache("#core/donors.gen.h", "../DONORS.md", env.Run(core_builders.make_donors_header)) +env.CommandNoCache("#core/donors.gen.h", "#DONORS.md", env.Run(core_builders.make_donors_header)) # License -env.Depends("#core/license.gen.h", ["../COPYRIGHT.txt", "../LICENSE.txt"]) env.CommandNoCache( - "#core/license.gen.h", - ["../COPYRIGHT.txt", "../LICENSE.txt"], - env.Run(core_builders.make_license_header), + "#core/license.gen.h", ["#COPYRIGHT.txt", "#LICENSE.txt"], env.Run(core_builders.make_license_header) ) # Chain load SCsubs diff --git a/core/core_builders.py b/core/core_builders.py index d7c0603c8d7..5eaac237a62 100644 --- a/core/core_builders.py +++ b/core/core_builders.py @@ -1,171 +1,104 @@ """Functions used to generate source files during build time""" -import zlib +from collections import OrderedDict +from io import TextIOWrapper - -def escape_string(s): - def charcode_to_c_escapes(c): - rev_result = [] - while c >= 256: - c, low = (c // 256, c % 256) - rev_result.append("\\%03o" % low) - rev_result.append("\\%03o" % c) - return "".join(reversed(rev_result)) - - result = "" - if isinstance(s, str): - s = s.encode("utf-8") - for c in s: - if not (32 <= c < 127) or c in (ord("\\"), ord('"')): - result += charcode_to_c_escapes(c) - else: - result += chr(c) - return result +import methods def make_certs_header(target, source, env): - src = str(source[0]) - dst = str(target[0]) - with open(src, "rb") as f, open(dst, "w", encoding="utf-8", newline="\n") as g: - buf = f.read() - decomp_size = len(buf) - - # Use maximum zlib compression level to further reduce file size - # (at the cost of initial build times). - buf = zlib.compress(buf, zlib.Z_BEST_COMPRESSION) - - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write("#ifndef CERTS_COMPRESSED_GEN_H\n") - g.write("#define CERTS_COMPRESSED_GEN_H\n") + 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) - path = env["system_certs_path"] - g.write('#define _SYSTEM_CERTS_PATH "%s"\n' % str(path)) + file.write('#define _SYSTEM_CERTS_PATH "{}"\n'.format(env["system_certs_path"])) if env["builtin_certs"]: # Defined here and not in env so changing it does not trigger a full rebuild. - g.write("#define BUILTIN_CERTS_ENABLED\n") - g.write("static const int _certs_compressed_size = " + str(len(buf)) + ";\n") - g.write("static const int _certs_uncompressed_size = " + str(decomp_size) + ";\n") - g.write("static const unsigned char _certs_compressed[] = {\n") - for i in range(len(buf)): - g.write("\t" + str(buf[i]) + ",\n") - g.write("};\n") - g.write("#endif // CERTS_COMPRESSED_GEN_H") + 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", - "Lead Developer", - "Project Manager", - "Developers", - ] - sections_id = [ - "AUTHORS_FOUNDERS", - "AUTHORS_LEAD_DEVELOPERS", - "AUTHORS_PROJECT_MANAGERS", - "AUTHORS_DEVELOPERS", - ] + 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 - src = str(source[0]) - dst = str(target[0]) - with open(src, "r", encoding="utf-8") as f, open(dst, "w", encoding="utf-8", newline="\n") as g: - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write("#ifndef AUTHORS_GEN_H\n") - g.write("#define AUTHORS_GEN_H\n") - - reading = False + with methods.generated_wrapper(str(target[0])) as file: def close_section(): - g.write("\t0\n") - g.write("};\n") + file.write("\tnullptr,\n};\n\n") - for line in f: - if reading: - if line.startswith(" "): - g.write('\t"' + escape_string(line.strip()) + '",\n') - continue - if line.startswith("## "): + 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 - for section, section_id in zip(sections, sections_id): - if line.strip().endswith(section): - current_section = escape_string(section_id) - reading = True - g.write("const char *const " + current_section + "[] = {\n") - break + section = SECTIONS[line[3:].strip()] + if section: + file.write(f"inline constexpr const char *{section}[] = {{\n") + reading = True if reading: close_section() - g.write("#endif // AUTHORS_GEN_H\n") - def make_donors_header(target, source, env): - sections = [ - "Patrons", - "Platinum sponsors", - "Gold sponsors", - "Silver sponsors", - "Diamond members", - "Titanium members", - "Platinum members", - "Gold members", - ] - sections_id = [ - "DONORS_PATRONS", - "DONORS_SPONSORS_PLATINUM", - "DONORS_SPONSORS_GOLD", - "DONORS_SPONSORS_SILVER", - "DONORS_MEMBERS_DIAMOND", - "DONORS_MEMBERS_TITANIUM", - "DONORS_MEMBERS_PLATINUM", - "DONORS_MEMBERS_GOLD", - ] + 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 - src = str(source[0]) - dst = str(target[0]) - with open(src, "r", encoding="utf-8") as f, open(dst, "w", encoding="utf-8", newline="\n") as g: - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write("#ifndef DONORS_GEN_H\n") - g.write("#define DONORS_GEN_H\n") - - reading = False + with methods.generated_wrapper(str(target[0])) as file: def close_section(): - g.write("\t0\n") - g.write("};\n") + file.write("\tnullptr,\n};\n\n") - for line in f: - if reading >= 0: - if line.startswith(" "): - g.write('\t"' + escape_string(line.strip()) + '",\n') - continue - if line.startswith("## "): + 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 - for section, section_id in zip(sections, sections_id): - if line.strip().endswith(section): - current_section = escape_string(section_id) - reading = True - g.write("const char *const " + current_section + "[] = {\n") - break + section = SECTIONS.get(line[3:].strip()) + if section: + file.write(f"inline constexpr const char *{section}[] = {{\n") + reading = True if reading: close_section() - g.write("#endif // DONORS_GEN_H\n") - def make_license_header(target, source, env): src_copyright = str(source[0]) src_license = str(source[1]) - dst = str(target[0]) class LicenseReader: - def __init__(self, license_file): + def __init__(self, license_file: TextIOWrapper): self._license_file = license_file self.line_num = 0 self.current = self.next_line() @@ -188,9 +121,7 @@ def make_license_header(target, source, env): lines.append(self.current.strip()) return (tag, lines) - from collections import OrderedDict - - projects: dict = OrderedDict() + projects = OrderedDict() license_list = [] with open(src_copyright, "r", encoding="utf-8") as copyright_file: @@ -212,7 +143,7 @@ def make_license_header(target, source, env): part = {} reader.next_line() - data_list: list = [] + data_list = [] for project in iter(projects.values()): for part in project: part["file_index"] = len(data_list) @@ -220,96 +151,76 @@ def make_license_header(target, source, env): part["copyright_index"] = len(data_list) data_list += part["Copyright"] - with open(dst, "w", encoding="utf-8", newline="\n") as f: - f.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - f.write("#ifndef LICENSE_GEN_H\n") - f.write("#define LICENSE_GEN_H\n") - f.write("const char *const GODOT_LICENSE_TEXT =") + with open(src_license, "r", encoding="utf-8") as file: + license_text = file.read() - with open(src_license, "r", encoding="utf-8") as license_file: - for line in license_file: - escaped_string = escape_string(line.strip()) - f.write('\n\t\t"' + escaped_string + '\\n"') - f.write(";\n\n") + 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)} +}}; - f.write( - "struct ComponentCopyrightPart {\n" - "\tconst char *license;\n" - "\tconst char *const *files;\n" - "\tconst char *const *copyright_statements;\n" - "\tint file_count;\n" - "\tint copyright_count;\n" - "};\n\n" - ) +struct ComponentCopyrightPart {{ + const char *license; + const char *const *files; + const char *const *copyright_statements; + int file_count; + int copyright_count; +}}; - f.write( - "struct ComponentCopyright {\n" - "\tconst char *name;\n" - "\tconst ComponentCopyrightPart *parts;\n" - "\tint part_count;\n" - "};\n\n" - ) +struct ComponentCopyright {{ + const char *name; + const ComponentCopyrightPart *parts; + int part_count; +}}; - f.write("const char *const COPYRIGHT_INFO_DATA[] = {\n") +""") + + file.write("inline constexpr const char *COPYRIGHT_INFO_DATA[] = {\n") for line in data_list: - f.write('\t"' + escape_string(line) + '",\n') - f.write("};\n\n") + file.write(f'\t"{methods.to_escaped_cstring(line)}",\n') + file.write("};\n\n") - f.write("const ComponentCopyrightPart COPYRIGHT_PROJECT_PARTS[] = {\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: - f.write( - '\t{ "' - + escape_string(part["License"][0]) - + '", ' - + "©RIGHT_INFO_DATA[" - + str(part["file_index"]) - + "], " - + "©RIGHT_INFO_DATA[" - + str(part["copyright_index"]) - + "], " - + str(len(part["Files"])) - + ", " - + str(len(part["Copyright"])) - + " },\n" + 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 - f.write("};\n\n") + file.write("};\n\n") - f.write("const int COPYRIGHT_INFO_COUNT = " + str(len(projects)) + ";\n") + file.write(f"inline constexpr int COPYRIGHT_INFO_COUNT = {len(projects)};\n") - f.write("const ComponentCopyright COPYRIGHT_INFO[] = {\n") + file.write("inline constexpr ComponentCopyright COPYRIGHT_INFO[] = {\n") for project_name, project in iter(projects.items()): - f.write( - '\t{ "' - + escape_string(project_name) - + '", ' - + "©RIGHT_PROJECT_PARTS[" - + str(part_indexes[project_name]) - + "], " - + str(len(project)) - + " },\n" + file.write( + f'\t{{ "{methods.to_escaped_cstring(project_name)}", ' + + f"©RIGHT_PROJECT_PARTS[{part_indexes[project_name]}], " + + f"{len(project)} }},\n" ) - f.write("};\n\n") + file.write("};\n\n") - f.write("const int LICENSE_COUNT = " + str(len(license_list)) + ";\n") + file.write(f"inline constexpr int LICENSE_COUNT = {len(license_list)};\n") - f.write("const char *const LICENSE_NAMES[] = {\n") + file.write("inline constexpr const char *LICENSE_NAMES[] = {\n") for license in license_list: - f.write('\t"' + escape_string(license[0]) + '",\n') - f.write("};\n\n") + file.write(f'\t"{methods.to_escaped_cstring(license[0])}",\n') + file.write("};\n\n") - f.write("const char *const LICENSE_BODIES[] = {\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 == ".": - f.write('\t"\\n"\n') + to_raw += [""] else: - f.write('\t"' + escape_string(line) + '\\n"\n') - f.write('\t"",\n\n') - f.write("};\n\n") - - f.write("#endif // LICENSE_GEN_H\n") + to_raw += [line] + file.write(f"{methods.to_raw_cstring(to_raw)},\n\n") + file.write("};\n\n") diff --git a/core/extension/make_interface_dumper.py b/core/extension/make_interface_dumper.py index 6227c2efede..45f257a331a 100644 --- a/core/extension/make_interface_dumper.py +++ b/core/extension/make_interface_dumper.py @@ -1,52 +1,37 @@ -import zlib +import methods def run(target, source, env): - src = str(source[0]) - dst = str(target[0]) - with open(src, "rb") as f, open(dst, "w", encoding="utf-8", newline="\n") as g: - buf = f.read() - decomp_size = len(buf) - - # Use maximum zlib compression level to further reduce file size - # (at the cost of initial build times). - buf = zlib.compress(buf, zlib.Z_BEST_COMPRESSION) - - g.write( - """/* THIS FILE IS GENERATED DO NOT EDIT */ -#pragma once + 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: + file.write(f"""\ #ifdef TOOLS_ENABLED #include "core/io/compression.h" #include "core/io/file_access.h" #include "core/string/ustring.h" -""" - ) +inline constexpr int _gdextension_interface_data_compressed_size = {len(buffer)}; +inline constexpr int _gdextension_interface_data_uncompressed_size = {decomp_size}; +inline constexpr unsigned char _gdextension_interface_data_compressed[] = {{ + {methods.format_buffer(buffer, 1)} +}}; - g.write("static const int _gdextension_interface_data_compressed_size = " + str(len(buf)) + ";\n") - g.write("static const int _gdextension_interface_data_uncompressed_size = " + str(decomp_size) + ";\n") - g.write("static const unsigned char _gdextension_interface_data_compressed[] = {\n") - for i in range(len(buf)): - g.write("\t" + str(buf[i]) + ",\n") - g.write("};\n") - - g.write( - """ -class GDExtensionInterfaceDump { - public: - static void generate_gdextension_interface_file(const String &p_path) { - Ref fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_MSG(fa.is_null(), vformat("Cannot open file '%s' for writing.", p_path)); - Vector data; - data.resize(_gdextension_interface_data_uncompressed_size); - int ret = Compression::decompress(data.ptrw(), _gdextension_interface_data_uncompressed_size, _gdextension_interface_data_compressed, _gdextension_interface_data_compressed_size, Compression::MODE_DEFLATE); - ERR_FAIL_COND_MSG(ret == -1, "Compressed file is corrupt."); - fa->store_buffer(data.ptr(), data.size()); - }; -}; +class GDExtensionInterfaceDump {{ + public: + static void generate_gdextension_interface_file(const String &p_path) {{ + Ref fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_MSG(fa.is_null(), vformat("Cannot open file '%s' for writing.", p_path)); + Vector data; + data.resize(_gdextension_interface_data_uncompressed_size); + int ret = Compression::decompress(data.ptrw(), _gdextension_interface_data_uncompressed_size, _gdextension_interface_data_compressed, _gdextension_interface_data_compressed_size, Compression::MODE_DEFLATE); + ERR_FAIL_COND_MSG(ret == -1, "Compressed file is corrupt."); + fa->store_buffer(data.ptr(), data.size()); + }}; +}}; #endif // TOOLS_ENABLED -""" - ) +""") diff --git a/core/input/input_builders.py b/core/input/input_builders.py index 3685e726b46..c63f595fced 100644 --- a/core/input/input_builders.py +++ b/core/input/input_builders.py @@ -2,18 +2,22 @@ from collections import OrderedDict +import methods + def make_default_controller_mappings(target, source, env): - dst = str(target[0]) - with open(dst, "w", encoding="utf-8", newline="\n") as g: - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write('#include "core/typedefs.h"\n') - g.write('#include "core/input/default_controller_mappings.h"\n') + with methods.generated_wrapper(str(target[0])) as file: + file.write("""\ +#include "core/input/default_controller_mappings.h" + +#include "core/typedefs.h" + +""") # ensure mappings have a consistent order - platform_mappings: dict = OrderedDict() - for src_path in source: - with open(str(src_path), "r", encoding="utf-8") as f: + platform_mappings = OrderedDict() + for src_path in map(str, source): + with open(src_path, "r", encoding="utf-8") as f: # read mapping file and skip header mapping_file_lines = f.readlines()[2:] @@ -32,28 +36,28 @@ def make_default_controller_mappings(target, source, env): line_parts = line.split(",") guid = line_parts[0] if guid in platform_mappings[current_platform]: - g.write( + file.write( "// WARNING: DATABASE {} OVERWROTE PRIOR MAPPING: {} {}\n".format( src_path, current_platform, platform_mappings[current_platform][guid] ) ) platform_mappings[current_platform][guid] = line - platform_variables = { - "Linux": "#ifdef LINUXBSD_ENABLED", - "Windows": "#ifdef WINDOWS_ENABLED", - "Mac OS X": "#ifdef MACOS_ENABLED", - "Android": "#ifdef ANDROID_ENABLED", - "iOS": "#ifdef IOS_ENABLED", - "Web": "#ifdef WEB_ENABLED", + PLATFORM_VARIABLES = { + "Linux": "LINUXBSD", + "Windows": "WINDOWS", + "Mac OS X": "MACOS", + "Android": "ANDROID", + "iOS": "IOS", + "Web": "WEB", } - g.write("const char* DefaultControllerMappings::mappings[] = {\n") + file.write("const char *DefaultControllerMappings::mappings[] = {\n") for platform, mappings in platform_mappings.items(): - variable = platform_variables[platform] - g.write("{}\n".format(variable)) + variable = PLATFORM_VARIABLES[platform] + file.write(f"#ifdef {variable}_ENABLED\n") for mapping in mappings.values(): - g.write('\t"{}",\n'.format(mapping)) - g.write("#endif\n") + file.write(f'\t"{mapping}",\n') + file.write(f"#endif // {variable}_ENABLED\n") - g.write("\tnullptr\n};\n") + file.write("\tnullptr\n};\n") diff --git a/editor/SCsub b/editor/SCsub index 9fcaf612457..bcd6492d831 100644 --- a/editor/SCsub +++ b/editor/SCsub @@ -5,7 +5,6 @@ Import("env") env.editor_sources = [] -import glob import os import editor_builders @@ -17,17 +16,16 @@ if env.editor_build: def doc_data_class_path_builder(target, source, env): paths = dict(sorted(source[0].read().items())) data = "\n".join([f'\t{{"{key}", "{value}"}},' for key, value in paths.items()]) - with methods.generated_wrapper(target) as file: + with methods.generated_wrapper(str(target[0])) as file: file.write( f"""\ -static const int _doc_data_class_path_count = {len(paths)}; - struct _DocDataClassPath {{ const char *name; const char *path; }}; -static const _DocDataClassPath _doc_data_class_paths[{len(env.doc_class_path) + 1}] = {{ +inline constexpr int _doc_data_class_path_count = {len(paths)}; +inline constexpr _DocDataClassPath _doc_data_class_paths[{len(env.doc_class_path) + 1}] = {{ {data} {{nullptr, nullptr}}, }}; @@ -42,7 +40,7 @@ static const _DocDataClassPath _doc_data_class_paths[{len(env.doc_class_path) + exp_inc = "\n".join([f'#include "platform/{p}/export/export.h"' for p in platforms]) exp_reg = "\n".join([f"\tregister_{p}_exporter();" for p in platforms]) exp_type = "\n".join([f"\tregister_{p}_exporter_types();" for p in platforms]) - with methods.generated_wrapper(target) as file: + with methods.generated_wrapper(str(target[0])) as file: file.write( f"""\ #include "register_exporters.h" @@ -83,7 +81,6 @@ void register_exporter_types() {{ docs += Glob(d + "/*.xml") # Custom. docs = sorted(docs) - env.Depends("#editor/doc_data_compressed.gen.h", docs) env.CommandNoCache( "#editor/doc_data_compressed.gen.h", docs, @@ -97,40 +94,31 @@ void register_exporter_types() {{ # Generated with `make include-list` for each resource. # Editor translations - tlist = glob.glob(env.Dir("#editor/translations/editor").abspath + "/*.po") - env.Depends("#editor/editor_translations.gen.h", tlist) env.CommandNoCache( "#editor/editor_translations.gen.h", - tlist, - env.Run(editor_builders.make_editor_translations_header), + Glob("#editor/translations/editor/*"), + env.Run(editor_builders.make_translations_header), ) # Property translations - tlist = glob.glob(env.Dir("#editor/translations/properties").abspath + "/*.po") - env.Depends("#editor/property_translations.gen.h", tlist) env.CommandNoCache( "#editor/property_translations.gen.h", - tlist, - env.Run(editor_builders.make_property_translations_header), + Glob("#editor/translations/properties/*"), + env.Run(editor_builders.make_translations_header), ) # Documentation translations - tlist = glob.glob(env.Dir("#doc/translations").abspath + "/*.po") - env.Depends("#editor/doc_translations.gen.h", tlist) env.CommandNoCache( "#editor/doc_translations.gen.h", - tlist, - env.Run(editor_builders.make_doc_translations_header), + Glob("#doc/translations/*"), + env.Run(editor_builders.make_translations_header), ) # Extractable translations - tlist = glob.glob(env.Dir("#editor/translations/extractable").abspath + "/*.po") - tlist.extend(glob.glob(env.Dir("#editor/translations/extractable").abspath + "/extractable.pot")) - env.Depends("#editor/extractable_translations.gen.h", tlist) env.CommandNoCache( "#editor/extractable_translations.gen.h", - tlist, - env.Run(editor_builders.make_extractable_translations_header), + Glob("#editor/translations/extractable/*"), + env.Run(editor_builders.make_translations_header), ) env.add_source_files(env.editor_sources, "*.cpp") diff --git a/editor/editor_builders.py b/editor/editor_builders.py index 625d5706666..50bcd7632f1 100644 --- a/editor/editor_builders.py +++ b/editor/editor_builders.py @@ -2,141 +2,95 @@ import os import os.path -import shutil import subprocess import tempfile import uuid -import zlib -from methods import print_warning +import methods def make_doc_header(target, source, env): - dst = str(target[0]) - with open(dst, "w", encoding="utf-8", newline="\n") as g: - buf = "" - docbegin = "" - docend = "" - for src in source: - src = str(src) - if not src.endswith(".xml"): - continue - with open(src, "r", encoding="utf-8") as f: - content = f.read() - buf += content + buffer = b"".join([methods.get_buffer(src) for src in map(str, source)]) + decomp_size = len(buffer) + buffer = methods.compress_buffer(buffer) - buf = (docbegin + buf + docend).encode("utf-8") - decomp_size = len(buf) - - # Use maximum zlib compression level to further reduce file size - # (at the cost of initial build times). - buf = zlib.compress(buf, zlib.Z_BEST_COMPRESSION) - - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write("#ifndef _DOC_DATA_RAW_H\n") - g.write("#define _DOC_DATA_RAW_H\n") - g.write('static const char *_doc_data_hash = "' + str(hash(buf)) + '";\n') - g.write("static const int _doc_data_compressed_size = " + str(len(buf)) + ";\n") - g.write("static const int _doc_data_uncompressed_size = " + str(decomp_size) + ";\n") - g.write("static const unsigned char _doc_data_compressed[] = {\n") - for i in range(len(buf)): - g.write("\t" + str(buf[i]) + ",\n") - g.write("};\n") - - g.write("#endif") + with methods.generated_wrapper(str(target[0])) as file: + file.write(f"""\ +inline constexpr const char *_doc_data_hash = "{hash(buffer)}"; +inline constexpr int _doc_data_compressed_size = {len(buffer)}; +inline constexpr int _doc_data_uncompressed_size = {decomp_size}; +inline constexpr const unsigned char _doc_data_compressed[] = {{ + {methods.format_buffer(buffer, 1)} +}}; +""") -def make_translations_header(target, source, env, category): - dst = str(target[0]) +def make_translations_header(target, source, env): + category = os.path.basename(str(target[0])).split("_")[0] + sorted_paths = sorted([src.abspath for src in source], key=lambda path: os.path.splitext(os.path.basename(path))[0]) - with open(dst, "w", encoding="utf-8", newline="\n") as g: - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write("#ifndef _{}_TRANSLATIONS_H\n".format(category.upper())) - g.write("#define _{}_TRANSLATIONS_H\n".format(category.upper())) + xl_names = [] + msgfmt = env.Detect("msgfmt") + if not msgfmt: + methods.print_warning("msgfmt not found, using .po files instead of .mo") - sorted_paths = sorted([str(x) for x in source], key=lambda path: os.path.splitext(os.path.basename(path))[0]) - - msgfmt_available = shutil.which("msgfmt") is not None - - if not msgfmt_available: - print_warning("msgfmt is not found, using .po files instead of .mo") - - xl_names = [] - for i in range(len(sorted_paths)): - name = os.path.splitext(os.path.basename(sorted_paths[i]))[0] + with methods.generated_wrapper(str(target[0])) as file: + for path in sorted_paths: + name = os.path.splitext(os.path.basename(path))[0] # msgfmt erases non-translated messages, so avoid using it if exporting the POT. - if msgfmt_available and name != category: + if msgfmt and name != category: mo_path = os.path.join(tempfile.gettempdir(), uuid.uuid4().hex + ".mo") - cmd = "msgfmt " + sorted_paths[i] + " --no-hash -o " + mo_path + cmd = f"{msgfmt} {path} --no-hash -o {mo_path}" try: subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE).communicate() - with open(mo_path, "rb") as f: - buf = f.read() + buffer = methods.get_buffer(mo_path) except OSError as e: - print_warning( + methods.print_warning( "msgfmt execution failed, using .po file instead of .mo: path=%r; [%s] %s" - % (sorted_paths[i], e.__class__.__name__, e) + % (path, e.__class__.__name__, e) ) - with open(sorted_paths[i], "rb") as f: - buf = f.read() + buffer = methods.get_buffer(path) finally: try: - os.remove(mo_path) + if os.path.exists(mo_path): + os.remove(mo_path) except OSError as e: # Do not fail the entire build if it cannot delete a temporary file. - print_warning( + methods.print_warning( "Could not delete temporary .mo file: path=%r; [%s] %s" % (mo_path, e.__class__.__name__, e) ) else: - with open(sorted_paths[i], "rb") as f: - buf = f.read() - + buffer = methods.get_buffer(path) if name == category: name = "source" - decomp_size = len(buf) - # Use maximum zlib compression level to further reduce file size - # (at the cost of initial build times). - buf = zlib.compress(buf, zlib.Z_BEST_COMPRESSION) + decomp_size = len(buffer) + buffer = methods.compress_buffer(buffer) - g.write("static const unsigned char _{}_translation_{}_compressed[] = {{\n".format(category, name)) - for j in range(len(buf)): - g.write("\t" + str(buf[j]) + ",\n") + file.write(f"""\ +inline constexpr const unsigned char _{category}_translation_{name}_compressed[] = {{ + {methods.format_buffer(buffer, 1)} +}}; - g.write("};\n") +""") - xl_names.append([name, len(buf), str(decomp_size)]) + xl_names.append([name, len(buffer), decomp_size]) + + file.write(f"""\ +struct {category.capitalize()}TranslationList {{ + const char* lang; + int comp_size; + int uncomp_size; + const unsigned char* data; +}}; + +inline constexpr {category.capitalize()}TranslationList _{category}_translations[] = {{ +""") - g.write("struct {}TranslationList {{\n".format(category.capitalize())) - g.write("\tconst char* lang;\n") - g.write("\tint comp_size;\n") - g.write("\tint uncomp_size;\n") - g.write("\tconst unsigned char* data;\n") - g.write("};\n\n") - g.write("static {}TranslationList _{}_translations[] = {{\n".format(category.capitalize(), category)) for x in xl_names: - g.write( - '\t{{ "{}", {}, {}, _{}_translation_{}_compressed }},\n'.format( - x[0], str(x[1]), str(x[2]), category, x[0] - ) - ) - g.write("\t{nullptr, 0, 0, nullptr}\n") - g.write("};\n") + file.write(f'\t{{ "{x[0]}", {x[1]}, {x[2]}, _{category}_translation_{x[0]}_compressed }},\n') - g.write("#endif") - - -def make_editor_translations_header(target, source, env): - make_translations_header(target, source, env, "editor") - - -def make_property_translations_header(target, source, env): - make_translations_header(target, source, env, "property") - - -def make_doc_translations_header(target, source, env): - make_translations_header(target, source, env, "doc") - - -def make_extractable_translations_header(target, source, env): - make_translations_header(target, source, env, "extractable") + file.write("""\ + { nullptr, 0, 0, nullptr }, +}; +""") diff --git a/editor/editor_translation.cpp b/editor/editor_translation.cpp index 607d3aac3b6..fb59288e6ef 100644 --- a/editor/editor_translation.cpp +++ b/editor/editor_translation.cpp @@ -42,7 +42,7 @@ Vector get_editor_locales() { Vector locales; - EditorTranslationList *etl = _editor_translations; + const EditorTranslationList *etl = _editor_translations; while (etl->data) { const String &locale = etl->lang; locales.push_back(locale); @@ -56,7 +56,7 @@ Vector get_editor_locales() { void load_editor_translations(const String &p_locale) { const Ref domain = TranslationServer::get_singleton()->get_or_add_domain("godot.editor"); - EditorTranslationList *etl = _editor_translations; + const EditorTranslationList *etl = _editor_translations; while (etl->data) { if (etl->lang == p_locale) { Vector data; @@ -84,7 +84,7 @@ void load_editor_translations(const String &p_locale) { void load_property_translations(const String &p_locale) { const Ref domain = TranslationServer::get_singleton()->get_or_add_domain("godot.properties"); - PropertyTranslationList *etl = _property_translations; + const PropertyTranslationList *etl = _property_translations; while (etl->data) { if (etl->lang == p_locale) { Vector data; @@ -112,7 +112,7 @@ void load_property_translations(const String &p_locale) { void load_doc_translations(const String &p_locale) { const Ref domain = TranslationServer::get_singleton()->get_or_add_domain("godot.documentation"); - DocTranslationList *dtl = _doc_translations; + const DocTranslationList *dtl = _doc_translations; while (dtl->data) { if (dtl->lang == p_locale) { Vector data; @@ -140,7 +140,7 @@ void load_doc_translations(const String &p_locale) { void load_extractable_translations(const String &p_locale) { const Ref domain = TranslationServer::get_singleton()->get_or_add_domain("godot.editor"); - ExtractableTranslationList *etl = _extractable_translations; + const ExtractableTranslationList *etl = _extractable_translations; while (etl->data) { if (etl->lang == p_locale) { Vector data; @@ -166,7 +166,7 @@ void load_extractable_translations(const String &p_locale) { } Vector> get_extractable_message_list() { - ExtractableTranslationList *etl = _extractable_translations; + const ExtractableTranslationList *etl = _extractable_translations; Vector> list; while (etl->data) { diff --git a/editor/icons/editor_icons_builders.py b/editor/icons/editor_icons_builders.py index d3e89534835..24ae0b8029c 100644 --- a/editor/icons/editor_icons_builders.py +++ b/editor/icons/editor_icons_builders.py @@ -1,71 +1,47 @@ """Functions used to generate source files during build time""" import os -from io import StringIO -from methods import to_raw_cstring +import methods # See also `scene/theme/icons/default_theme_icons_builders.py`. def make_editor_icons_action(target, source, env): - dst = str(target[0]) - svg_icons = source + icons_names = [] + icons_raw = [] + icons_med = [] + icons_big = [] - with StringIO() as icons_string, StringIO() as s: - for svg in svg_icons: - with open(str(svg), "r") as svgf: - icons_string.write("\t%s,\n" % to_raw_cstring(svgf.read())) + for idx, svg in enumerate(source): + path = str(svg) + with open(path, encoding="utf-8", newline="\n") as file: + icons_raw.append(methods.to_raw_cstring(file.read())) - s.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - s.write("#ifndef _EDITOR_ICONS_H\n") - s.write("#define _EDITOR_ICONS_H\n") - s.write("static const int editor_icons_count = {};\n".format(len(svg_icons))) - s.write("static const char *editor_icons_sources[] = {\n") - s.write(icons_string.getvalue()) - s.write("};\n\n") - s.write("static const char *editor_icons_names[] = {\n") + name = os.path.splitext(os.path.basename(path))[0] + icons_names.append(f'"{name}"') - # this is used to store the indices of thumbnail icons - thumb_medium_indices = [] - thumb_big_indices = [] - index = 0 - for f in svg_icons: - fname = str(f) + if name.endswith("MediumThumb"): + icons_med.append(str(idx)) + elif name.endswith(("BigThumb", "GodotFile")): + icons_big.append(str(idx)) - # Trim the `.svg` extension from the string. - icon_name = os.path.basename(fname)[:-4] - # some special cases - if icon_name.endswith("MediumThumb"): # don't know a better way to handle this - thumb_medium_indices.append(str(index)) - if icon_name.endswith("BigThumb"): # don't know a better way to handle this - thumb_big_indices.append(str(index)) - if icon_name.endswith("GodotFile"): # don't know a better way to handle this - thumb_big_indices.append(str(index)) + icons_names_str = ",\n\t".join(icons_names) + icons_raw_str = ",\n\t".join(icons_raw) - s.write('\t"{0}"'.format(icon_name)) + with methods.generated_wrapper(str(target[0])) as file: + file.write(f"""\ +inline constexpr int editor_icons_count = {len(icons_names)}; +inline constexpr const char *editor_icons_sources[] = {{ + {icons_raw_str} +}}; - if fname != svg_icons[-1]: - s.write(",") - s.write("\n") +inline constexpr const char *editor_icons_names[] = {{ + {icons_names_str} +}}; - index += 1 +inline constexpr int editor_md_thumbs_count = {len(icons_med)}; +inline constexpr int editor_md_thumbs_indices[] = {{ {", ".join(icons_med)} }}; - s.write("};\n") - - if thumb_medium_indices: - s.write("\n\n") - s.write("static const int editor_md_thumbs_count = {};\n".format(len(thumb_medium_indices))) - s.write("static const int editor_md_thumbs_indices[] = {") - s.write(", ".join(thumb_medium_indices)) - s.write("};\n") - if thumb_big_indices: - s.write("\n\n") - s.write("static const int editor_bg_thumbs_count = {};\n".format(len(thumb_big_indices))) - s.write("static const int editor_bg_thumbs_indices[] = {") - s.write(", ".join(thumb_big_indices)) - s.write("};\n") - - s.write("#endif\n") - - with open(dst, "w", encoding="utf-8", newline="\n") as f: - f.write(s.getvalue()) +inline constexpr int editor_bg_thumbs_count = {len(icons_big)}; +inline constexpr int editor_bg_thumbs_indices[] = {{ {", ".join(icons_big)} }}; +""") diff --git a/editor/template_builders.py b/editor/template_builders.py index 6eee4e22435..4dbe03788a4 100644 --- a/editor/template_builders.py +++ b/editor/template_builders.py @@ -1,7 +1,8 @@ """Functions used to generate source files during build time""" import os -from io import StringIO + +import methods def parse_template(inherits, source, delimiter): @@ -36,54 +37,36 @@ def parse_template(inherits, source, delimiter): script_template["script"].replace('"', '\\"').lstrip().replace("\n", "\\n").replace("\t", "_TS_") ) return ( - '{ String("' - + script_template["inherits"] - + '"), String("' - + script_template["name"] - + '"), String("' - + script_template["description"] - + '"), String("' - + script_template["script"] - + '")' - + " },\n" + f'{{ String("{script_template["inherits"]}"), ' + + f'String("{script_template["name"]}"), ' + + f'String("{script_template["description"]}"), ' + + f'String("{script_template["script"]}") }},' ) def make_templates(target, source, env): - dst = str(target[0]) - with StringIO() as s: - s.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n\n") - s.write("#ifndef _CODE_TEMPLATES_H\n") - s.write("#define _CODE_TEMPLATES_H\n\n") - s.write('#include "core/object/object.h"\n') - s.write('#include "core/object/script_language.h"\n') + delimiter = "#" # GDScript single line comment delimiter by default. + if source: + ext = os.path.splitext(str(source[0]))[1] + if ext == ".cs": + delimiter = "//" - delimiter = "#" # GDScript single line comment delimiter by default. - if source: - ext = os.path.splitext(str(source[0]))[1] - if ext == ".cs": - delimiter = "//" + parsed_templates = [] - parsed_template_string = "" - number_of_templates = 0 + for filepath in source: + filepath = str(filepath) + node_name = os.path.basename(os.path.dirname(filepath)) + parsed_templates.append(parse_template(node_name, filepath, delimiter)) - for filepath in source: - filepath = str(filepath) - node_name = os.path.basename(os.path.dirname(filepath)) - parsed_template = parse_template(node_name, filepath, delimiter) - parsed_template_string += "\t" + parsed_template - number_of_templates += 1 + parsed_template_string = "\n\t".join(parsed_templates) - s.write("\nstatic const int TEMPLATES_ARRAY_SIZE = " + str(number_of_templates) + ";\n") - s.write( - "\nstatic const struct ScriptLanguage::ScriptTemplate TEMPLATES[" + str(number_of_templates) + "] = {\n" - ) + with methods.generated_wrapper(str(target[0])) as file: + file.write(f"""\ +#include "core/object/object.h" +#include "core/object/script_language.h" - s.write(parsed_template_string) - - s.write("};\n") - - s.write("\n#endif\n") - - with open(dst, "w", encoding="utf-8", newline="\n") as f: - f.write(s.getvalue()) +inline constexpr int TEMPLATES_ARRAY_SIZE = {len(parsed_templates)}; +static const struct ScriptLanguage::ScriptTemplate TEMPLATES[TEMPLATES_ARRAY_SIZE] = {{ + {parsed_template_string} +}}; +""") diff --git a/editor/themes/SCsub b/editor/themes/SCsub index 5a9949dfa76..ba5070e891c 100644 --- a/editor/themes/SCsub +++ b/editor/themes/SCsub @@ -3,17 +3,14 @@ from misc.utility.scons_hints import * Import("env") -import glob - import editor_theme_builders # Fonts -flist = glob.glob(env.Dir("#thirdparty").abspath + "/fonts/*.ttf") -flist.extend(glob.glob(env.Dir("#thirdparty").abspath + "/fonts/*.otf")) -flist.extend(glob.glob(env.Dir("#thirdparty").abspath + "/fonts/*.woff")) -flist.extend(glob.glob(env.Dir("#thirdparty").abspath + "/fonts/*.woff2")) +flist = Glob("#thirdparty/fonts/*.ttf") +flist.extend(Glob("#thirdparty/fonts/*.otf")) +flist.extend(Glob("#thirdparty/fonts/*.woff")) +flist.extend(Glob("#thirdparty/fonts/*.woff2")) flist.sort() -env.Depends("#editor/themes/builtin_fonts.gen.h", flist) env.CommandNoCache( "#editor/themes/builtin_fonts.gen.h", flist, diff --git a/editor/themes/editor_theme_builders.py b/editor/themes/editor_theme_builders.py index ec5653fb095..fc9f3f9dd02 100644 --- a/editor/themes/editor_theme_builders.py +++ b/editor/themes/editor_theme_builders.py @@ -2,28 +2,20 @@ import os +import methods + def make_fonts_header(target, source, env): - dst = str(target[0]) + with methods.generated_wrapper(str(target[0])) as file: + for src in map(str, source): + # Saving uncompressed, since FreeType will reference from memory pointer. + buffer = methods.get_buffer(src) + name = os.path.splitext(os.path.basename(src))[0] - with open(dst, "w", encoding="utf-8", newline="\n") as g: - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write("#ifndef _EDITOR_FONTS_H\n") - g.write("#define _EDITOR_FONTS_H\n") + file.write(f"""\ +inline constexpr int _font_{name}_size = {len(buffer)}; +inline constexpr unsigned char _font_{name}[] = {{ + {methods.format_buffer(buffer, 1)} +}}; - # Saving uncompressed, since FreeType will reference from memory pointer. - for i in range(len(source)): - file = str(source[i]) - with open(file, "rb") as f: - buf = f.read() - - name = os.path.splitext(os.path.basename(file))[0] - - g.write("static const int _font_" + name + "_size = " + str(len(buf)) + ";\n") - g.write("static const unsigned char _font_" + name + "[] = {\n") - for j in range(len(buf)): - g.write("\t" + str(buf[j]) + ",\n") - - g.write("};\n") - - g.write("#endif") +""") diff --git a/main/SCsub b/main/SCsub index 71bee465f59..18e6a7ba9d3 100644 --- a/main/SCsub +++ b/main/SCsub @@ -17,7 +17,6 @@ if env["steamapi"] and env.editor_build: if env["tests"]: env_main.Append(CPPDEFINES=["TESTS_ENABLED"]) -env_main.Depends("#main/splash.gen.h", "#main/splash.png") env_main.CommandNoCache( "#main/splash.gen.h", "#main/splash.png", @@ -25,14 +24,12 @@ env_main.CommandNoCache( ) if env_main.editor_build and not env_main["no_editor_splash"]: - env_main.Depends("#main/splash_editor.gen.h", "#main/splash_editor.png") env_main.CommandNoCache( "#main/splash_editor.gen.h", "#main/splash_editor.png", env.Run(main_builders.make_splash_editor), ) -env_main.Depends("#main/app_icon.gen.h", "#main/app_icon.png") env_main.CommandNoCache( "#main/app_icon.gen.h", "#main/app_icon.png", diff --git a/main/main_builders.py b/main/main_builders.py index b693df73fcb..a184dafddc6 100644 --- a/main/main_builders.py +++ b/main/main_builders.py @@ -1,60 +1,42 @@ """Functions used to generate source files during build time""" +import methods + def make_splash(target, source, env): - src = str(source[0]) - dst = str(target[0]) + buffer = methods.get_buffer(str(source[0])) - with open(src, "rb") as f: - buf = f.read() - - with open(dst, "w", encoding="utf-8", newline="\n") as g: - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write("#ifndef BOOT_SPLASH_H\n") - g.write("#define BOOT_SPLASH_H\n") + with methods.generated_wrapper(str(target[0])) as file: # Use a neutral gray color to better fit various kinds of projects. - g.write("static const Color boot_splash_bg_color = Color(0.14, 0.14, 0.14);\n") - g.write("static const unsigned char boot_splash_png[] = {\n") - for i in range(len(buf)): - g.write(str(buf[i]) + ",\n") - g.write("};\n") - g.write("#endif") + file.write(f"""\ +static const Color boot_splash_bg_color = Color(0.14, 0.14, 0.14); +inline constexpr const unsigned char boot_splash_png[] = {{ + {methods.format_buffer(buffer, 1)} +}}; +""") def make_splash_editor(target, source, env): - src = str(source[0]) - dst = str(target[0]) + buffer = methods.get_buffer(str(source[0])) - with open(src, "rb") as f: - buf = f.read() - - with open(dst, "w", encoding="utf-8", newline="\n") as g: - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write("#ifndef BOOT_SPLASH_EDITOR_H\n") - g.write("#define BOOT_SPLASH_EDITOR_H\n") + with methods.generated_wrapper(str(target[0])) as file: # The editor splash background color is taken from the default editor theme's background color. # This helps achieve a visually "smoother" transition between the splash screen and the editor. - g.write("static const Color boot_splash_editor_bg_color = Color(0.125, 0.145, 0.192);\n") - g.write("static const unsigned char boot_splash_editor_png[] = {\n") - for i in range(len(buf)): - g.write(str(buf[i]) + ",\n") - g.write("};\n") - g.write("#endif") + file.write(f"""\ +static const Color boot_splash_editor_bg_color = Color(0.125, 0.145, 0.192); +inline constexpr const unsigned char boot_splash_editor_png[] = {{ + {methods.format_buffer(buffer, 1)} +}}; +""") def make_app_icon(target, source, env): - src = str(source[0]) - dst = str(target[0]) + buffer = methods.get_buffer(str(source[0])) - with open(src, "rb") as f: - buf = f.read() - - with open(dst, "w", encoding="utf-8", newline="\n") as g: - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write("#ifndef APP_ICON_H\n") - g.write("#define APP_ICON_H\n") - g.write("static const unsigned char app_icon_png[] = {\n") - for i in range(len(buf)): - g.write(str(buf[i]) + ",\n") - g.write("};\n") - g.write("#endif") + with methods.generated_wrapper(str(target[0])) as file: + # Use a neutral gray color to better fit various kinds of projects. + file.write(f"""\ +inline constexpr const unsigned char app_icon_png[] = {{ + {methods.format_buffer(buffer, 1)} +}}; +""") diff --git a/methods.py b/methods.py index d09f9200071..697c5c815ca 100644 --- a/methods.py +++ b/methods.py @@ -6,6 +6,8 @@ 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 @@ -144,30 +146,36 @@ def get_version_info(module_version_string="", silent=False): if not silent: print_info(f"Using version status '{version_info['status']}', overriding the original '{version.status}'.") + return version_info + + +def get_git_info(): + os.chdir(base_folder_path) + # 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 @@ -176,26 +184,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): @@ -1417,6 +1425,11 @@ def generate_vs_project(env, original_args, project_name="godot"): sys.exit() +############################################################ +# FILE GENERATION & FORMATTING +############################################################ + + def generate_copyright_header(filename: str) -> str: MARGIN = 70 TEMPLATE = """\ @@ -1450,15 +1463,14 @@ 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, ) -> Generator[TextIOBase, None, None]: """ @@ -1466,26 +1478,11 @@ def generated_wrapper( 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. + - `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", ".hxx", ".inc")) @@ -1503,6 +1500,50 @@ def generated_wrapper( 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 @@ -1540,4 +1581,8 @@ 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)) diff --git a/modules/SCsub b/modules/SCsub index 09944241ea5..485dfe4abdc 100644 --- a/modules/SCsub +++ b/modules/SCsub @@ -17,8 +17,9 @@ Export("env_modules") # Header with MODULE_*_ENABLED defines. def modules_enabled_builder(target, source, env): - with methods.generated_wrapper(target) as file: - for module in source[0].read(): + modules = sorted(source[0].read()) + with methods.generated_wrapper(str(target[0])) as file: + for module in modules: file.write(f"#define MODULE_{module.upper()}_ENABLED\n") @@ -29,14 +30,26 @@ modules_enabled = env.CommandNoCache( def register_module_types_builder(target, source, env): modules = source[0].read() - mod_inc = "\n".join([f'#include "{p}/register_types.h"' for p in modules.values()]) + mod_inc = "\n".join([f'#include "{value}/register_types.h"' for value in modules.values()]) mod_init = "\n".join( - [f"#ifdef MODULE_{n.upper()}_ENABLED\n\tinitialize_{n}_module(p_level);\n#endif" for n in modules.keys()] + [ + f"""\ +#ifdef MODULE_{key.upper()}_ENABLED + initialize_{key}_module(p_level); +#endif""" + for key in modules.keys() + ] ) mod_uninit = "\n".join( - [f"#ifdef MODULE_{n.upper()}_ENABLED\n\tuninitialize_{n}_module(p_level);\n#endif" for n in modules.keys()] + [ + f"""\ +#ifdef MODULE_{key.upper()}_ENABLED + uninitialize_{key}_module(p_level); +#endif""" + for key in modules.keys() + ] ) - with methods.generated_wrapper(target) as file: + with methods.generated_wrapper(str(target[0])) as file: file.write( f"""\ #include "register_module_types.h" @@ -88,9 +101,10 @@ for name, path in env.module_list.items(): if env["tests"]: def modules_tests_builder(target, source, env): - with methods.generated_wrapper(target) as file: - for header in source: - file.write('#include "{}"\n'.format(os.path.normpath(header.path).replace("\\", "/"))) + headers = sorted([os.path.relpath(src.path, methods.base_folder_path).replace("\\", "/") for src in source]) + with methods.generated_wrapper(str(target[0])) as file: + for header in headers: + file.write(f'#include "{header}"\n') env.CommandNoCache("modules_tests.gen.h", test_headers, env.Run(modules_tests_builder)) diff --git a/modules/text_server_adv/SCsub b/modules/text_server_adv/SCsub index d05002944db..5bb753e9203 100644 --- a/modules/text_server_adv/SCsub +++ b/modules/text_server_adv/SCsub @@ -1,6 +1,8 @@ #!/usr/bin/env python from misc.utility.scons_hints import * +import methods + Import("env") Import("env_modules") @@ -8,28 +10,21 @@ env_text_server_adv = env_modules.Clone() def make_icu_data(target, source, env): - dst = target[0].srcnode().abspath + buffer = methods.get_buffer(str(source[0])) + with methods.generated_wrapper(str(target[0])) as file: + file.write(f"""\ +/* (C) 2016 and later: Unicode, Inc. and others. */ +/* License & terms of use: https://www.unicode.org/copyright.html */ - with open(dst, "w", encoding="utf-8", newline="\n") as g: - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write("/* (C) 2016 and later: Unicode, Inc. and others. */\n") - g.write("/* License & terms of use: https://www.unicode.org/copyright.html */\n") - g.write("#ifndef _ICU_DATA_H\n") - g.write("#define _ICU_DATA_H\n") - g.write('#include "unicode/utypes.h"\n') - g.write('#include "unicode/udata.h"\n') - g.write('#include "unicode/uversion.h"\n') +#include +#include +#include - with open(source[0].srcnode().abspath, "rb") as f: - buf = f.read() - - g.write('extern "C" U_EXPORT const size_t U_ICUDATA_SIZE = ' + str(len(buf)) + ";\n") - g.write('extern "C" U_EXPORT const unsigned char U_ICUDATA_ENTRY_POINT[] = {\n') - for i in range(len(buf)): - g.write("\t" + str(buf[i]) + ",\n") - - g.write("};\n") - g.write("#endif") +extern "C" U_EXPORT const size_t U_ICUDATA_SIZE = {len(buffer)}; +extern "C" U_EXPORT const unsigned char U_ICUDATA_ENTRY_POINT[] = {{ + {methods.format_buffer(buffer, 1)} +}}; +""") # Thirdparty source files diff --git a/platform/SCsub b/platform/SCsub index 3e384347b05..d4f35a2c298 100644 --- a/platform/SCsub +++ b/platform/SCsub @@ -18,10 +18,10 @@ def export_icon_builder(target, source, env): platform = src_path.parent.parent.stem with open(str(source[0]), "r") as file: svg = file.read() - with methods.generated_wrapper(target) as file: + with methods.generated_wrapper(str(target[0])) as file: file.write( f"""\ -static const char *_{platform}_{src_name}_svg = {methods.to_raw_cstring(svg)}; +inline constexpr const char *_{platform}_{src_name}_svg = {methods.to_raw_cstring(svg)}; """ ) @@ -37,7 +37,7 @@ def register_platform_apis_builder(target, source, env): api_inc = "\n".join([f'#include "{p}/api/api.h"' for p in platforms]) api_reg = "\n".join([f"\tregister_{p}_api();" for p in platforms]) api_unreg = "\n".join([f"\tunregister_{p}_api();" for p in platforms]) - with methods.generated_wrapper(target) as file: + with methods.generated_wrapper(str(target[0])) as file: file.write( f"""\ #include "register_platform_apis.h" diff --git a/scene/theme/SCsub b/scene/theme/SCsub index fb0914c0ee1..832ea075718 100644 --- a/scene/theme/SCsub +++ b/scene/theme/SCsub @@ -9,7 +9,6 @@ env.add_source_files(env.scene_sources, "*.cpp") SConscript("icons/SCsub") -env.Depends("#scene/theme/default_font.gen.h", "#thirdparty/fonts/OpenSans_SemiBold.woff2") env.CommandNoCache( "#scene/theme/default_font.gen.h", "#thirdparty/fonts/OpenSans_SemiBold.woff2", diff --git a/scene/theme/default_theme_builders.py b/scene/theme/default_theme_builders.py index 31272510ace..fc9f3f9dd02 100644 --- a/scene/theme/default_theme_builders.py +++ b/scene/theme/default_theme_builders.py @@ -1,30 +1,21 @@ """Functions used to generate source files during build time""" import os -import os.path + +import methods def make_fonts_header(target, source, env): - dst = str(target[0]) + with methods.generated_wrapper(str(target[0])) as file: + for src in map(str, source): + # Saving uncompressed, since FreeType will reference from memory pointer. + buffer = methods.get_buffer(src) + name = os.path.splitext(os.path.basename(src))[0] - with open(dst, "w", encoding="utf-8", newline="\n") as g: - g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") - g.write("#ifndef _DEFAULT_FONTS_H\n") - g.write("#define _DEFAULT_FONTS_H\n") + file.write(f"""\ +inline constexpr int _font_{name}_size = {len(buffer)}; +inline constexpr unsigned char _font_{name}[] = {{ + {methods.format_buffer(buffer, 1)} +}}; - # Saving uncompressed, since FreeType will reference from memory pointer. - for i in range(len(source)): - file = str(source[i]) - with open(file, "rb") as f: - buf = f.read() - - name = os.path.splitext(os.path.basename(file))[0] - - g.write("static const int _font_" + name + "_size = " + str(len(buf)) + ";\n") - g.write("static const unsigned char _font_" + name + "[] = {\n") - for j in range(len(buf)): - g.write("\t" + str(buf[j]) + ",\n") - - g.write("};\n") - - g.write("#endif") +""") diff --git a/scene/theme/icons/default_theme_icons_builders.py b/scene/theme/icons/default_theme_icons_builders.py index 3a673af92eb..8e29af7e682 100644 --- a/scene/theme/icons/default_theme_icons_builders.py +++ b/scene/theme/icons/default_theme_icons_builders.py @@ -1,51 +1,35 @@ """Functions used to generate source files during build time""" import os -from io import StringIO -from methods import to_raw_cstring +import methods # See also `editor/icons/editor_icons_builders.py`. def make_default_theme_icons_action(target, source, env): - dst = str(target[0]) - svg_icons = [str(x) for x in source] + icons_names = [] + icons_raw = [] - with StringIO() as icons_string, StringIO() as s: - for svg in svg_icons: - with open(svg, "r") as svgf: - icons_string.write("\t%s,\n" % to_raw_cstring(svgf.read())) + for src in map(str, source): + with open(src, encoding="utf-8", newline="\n") as file: + icons_raw.append(methods.to_raw_cstring(file.read())) - s.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n\n") - s.write('#include "modules/modules_enabled.gen.h"\n\n') - s.write("#ifndef _DEFAULT_THEME_ICONS_H\n") - s.write("#define _DEFAULT_THEME_ICONS_H\n") - s.write("static const int default_theme_icons_count = {};\n\n".format(len(svg_icons))) - s.write("#ifdef MODULE_SVG_ENABLED\n") - s.write("static const char *default_theme_icons_sources[] = {\n") - s.write(icons_string.getvalue()) - s.write("};\n") - s.write("#endif // MODULE_SVG_ENABLED\n\n") - s.write("static const char *default_theme_icons_names[] = {\n") + name = os.path.splitext(os.path.basename(src))[0] + icons_names.append(f'"{name}"') - index = 0 - for f in svg_icons: - fname = str(f) + icons_names_str = ",\n\t".join(icons_names) + icons_raw_str = ",\n\t".join(icons_raw) - # Trim the `.svg` extension from the string. - icon_name = os.path.basename(fname)[:-4] + with methods.generated_wrapper(str(target[0])) as file: + file.write(f"""\ +#include "modules/modules_enabled.gen.h" - s.write('\t"{0}"'.format(icon_name)) +inline constexpr int default_theme_icons_count = {len(icons_names)}; +inline constexpr const char *default_theme_icons_sources[] = {{ + {icons_raw_str} +}}; - if fname != svg_icons[-1]: - s.write(",") - s.write("\n") - - index += 1 - - s.write("};\n") - - s.write("#endif\n") - - with open(dst, "w", encoding="utf-8", newline="\n") as f: - f.write(s.getvalue()) +inline constexpr const char *default_theme_icons_names[] = {{ + {icons_names_str} +}}; +""")