From c4f1abe3f99343c4731bfcf3057297b3e38498b8 Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Fri, 20 Dec 2024 03:31:59 +0100 Subject: [PATCH] [Bindings] Build profile now strips methods and skip files This allows removing dependencies that are not explicitly unused by the gdextension being built and is implemented using an intermediate json API file with the methods and classes stripped (i.e. without touching the file generators). --- binding_generator.py | 150 ++------------ build_profile.py | 183 ++++++++++++++++++ .../godot_cpp/variant/variant_internal.hpp | 1 - test/build_profile.json | 6 +- tools/godotcpp.py | 34 +++- 5 files changed, 236 insertions(+), 138 deletions(-) create mode 100644 build_profile.py diff --git a/binding_generator.py b/binding_generator.py index b8b76ba9..3d050d32 100644 --- a/binding_generator.py +++ b/binding_generator.py @@ -197,13 +197,16 @@ def generate_virtuals(target): f.write(txt) -def get_file_list(api_filepath, output_dir, headers=False, sources=False, profile_filepath=""): +def get_file_list(api_filepath, output_dir, headers=False, sources=False): api = {} - files = [] with open(api_filepath, encoding="utf-8") as api_file: api = json.load(api_file) - build_profile = parse_build_profile(profile_filepath, api) + return _get_file_list(api, output_dir, headers, sources) + + +def _get_file_list(api, output_dir, headers=False, sources=False): + files = [] core_gen_folder = Path(output_dir) / "gen" / "include" / "godot_cpp" / "core" include_gen_folder = Path(output_dir) / "gen" / "include" / "godot_cpp" @@ -235,7 +238,7 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False, profil source_filename = source_gen_folder / "classes" / (camel_to_snake(engine_class["name"]) + ".cpp") if headers: files.append(str(header_filename.as_posix())) - if sources and is_class_included(engine_class["name"], build_profile): + if sources: files.append(str(source_filename.as_posix())) for native_struct in api["native_structures"]: @@ -267,128 +270,19 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False, profil return files -def print_file_list(api_filepath, output_dir, headers=False, sources=False, profile_filepath=""): - print(*get_file_list(api_filepath, output_dir, headers, sources, profile_filepath), sep=";", end=None) - - -def parse_build_profile(profile_filepath, api): - if profile_filepath == "": - return {} - print("Using feature build profile: " + profile_filepath) - - with open(profile_filepath, encoding="utf-8") as profile_file: - profile = json.load(profile_file) - - api_dict = {} - parents = {} - children = {} - for engine_class in api["classes"]: - api_dict[engine_class["name"]] = engine_class - parent = engine_class.get("inherits", "") - child = engine_class["name"] - parents[child] = parent - if parent == "": - continue - children[parent] = children.get(parent, []) - children[parent].append(child) - - # Parse methods dependencies - deps = {} - reverse_deps = {} - for name, engine_class in api_dict.items(): - ref_cls = set() - for method in engine_class.get("methods", []): - rtype = method.get("return_value", {}).get("type", "") - args = [a["type"] for a in method.get("arguments", [])] - if rtype in api_dict: - ref_cls.add(rtype) - elif is_enum(rtype) and get_enum_class(rtype) in api_dict: - ref_cls.add(get_enum_class(rtype)) - for arg in args: - if arg in api_dict: - ref_cls.add(arg) - elif is_enum(arg) and get_enum_class(arg) in api_dict: - ref_cls.add(get_enum_class(arg)) - deps[engine_class["name"]] = set(filter(lambda x: x != name, ref_cls)) - for acls in ref_cls: - if acls == name: - continue - reverse_deps[acls] = reverse_deps.get(acls, set()) - reverse_deps[acls].add(name) - - included = [] - front = list(profile.get("enabled_classes", [])) - if front: - # These must always be included - front.append("WorkerThreadPool") - front.append("ClassDB") - front.append("ClassDBSingleton") - while front: - cls = front.pop() - if cls in included: - continue - included.append(cls) - parent = parents.get(cls, "") - if parent: - front.append(parent) - for rcls in deps.get(cls, set()): - if rcls in included or rcls in front: - continue - front.append(rcls) - - excluded = [] - front = list(profile.get("disabled_classes", [])) - while front: - cls = front.pop() - if cls in excluded: - continue - excluded.append(cls) - front += children.get(cls, []) - for rcls in reverse_deps.get(cls, set()): - if rcls in excluded or rcls in front: - continue - front.append(rcls) - - if included and excluded: - print( - "WARNING: Cannot specify both 'enabled_classes' and 'disabled_classes' in build profile. 'disabled_classes' will be ignored." - ) - - return { - "enabled_classes": included, - "disabled_classes": excluded, - } - - -def scons_emit_files(target, source, env): - profile_filepath = env.get("build_profile", "") - if profile_filepath and not Path(profile_filepath).is_absolute(): - profile_filepath = str((Path(env.Dir("#").abspath) / profile_filepath).as_posix()) - - files = [env.File(f) for f in get_file_list(str(source[0]), target[0].abspath, True, True, profile_filepath)] - env.Clean(target, files) - env["godot_cpp_gen_dir"] = target[0].abspath - return files, source - - -def scons_generate_bindings(target, source, env): - generate_bindings( - str(source[0]), - env["generate_template_get_node"], - "32" if "32" in env["arch"] else "64", - env["precision"], - env["godot_cpp_gen_dir"], - ) - return None +def print_file_list(api_filepath, output_dir, headers=False, sources=False): + print(*get_file_list(api_filepath, output_dir, headers, sources), sep=";", end=None) def generate_bindings(api_filepath, use_template_get_node, bits="64", precision="single", output_dir="."): - api = None - - target_dir = Path(output_dir) / "gen" - + api = {} with open(api_filepath, encoding="utf-8") as api_file: api = json.load(api_file) + _generate_bindings(api, use_template_get_node, bits, precision, output_dir) + + +def _generate_bindings(api, use_template_get_node, bits="64", precision="single", output_dir="."): + target_dir = Path(output_dir) / "gen" shutil.rmtree(target_dir, ignore_errors=True) target_dir.mkdir(parents=True) @@ -2766,20 +2660,6 @@ def is_refcounted(type_name): return type_name in engine_classes and engine_classes[type_name] -def is_class_included(class_name, build_profile): - """ - Check if an engine class should be included. - This removes classes according to a build profile of enabled or disabled classes. - """ - included = build_profile.get("enabled_classes", []) - excluded = build_profile.get("disabled_classes", []) - if included: - return class_name in included - if excluded: - return class_name not in excluded - return True - - def is_included(type_name, current_type): """ Check if a builtin type should be included. diff --git a/build_profile.py b/build_profile.py new file mode 100644 index 00000000..b4d19ded --- /dev/null +++ b/build_profile.py @@ -0,0 +1,183 @@ +import json +import sys + + +def parse_build_profile(profile_filepath, api): + if profile_filepath == "": + return {} + + with open(profile_filepath, encoding="utf-8") as profile_file: + profile = json.load(profile_file) + + api_dict = {} + parents = {} + children = {} + for engine_class in api["classes"]: + api_dict[engine_class["name"]] = engine_class + parent = engine_class.get("inherits", "") + child = engine_class["name"] + parents[child] = parent + if parent == "": + continue + children[parent] = children.get(parent, []) + children[parent].append(child) + + included = [] + front = list(profile.get("enabled_classes", [])) + if front: + # These must always be included + front.append("WorkerThreadPool") + front.append("ClassDB") + front.append("ClassDBSingleton") + # In src/classes/low_level.cpp + front.append("FileAccess") + front.append("Image") + front.append("XMLParser") + # In include/godot_cpp/templates/thread_work_pool.hpp + front.append("Semaphore") + while front: + cls = front.pop() + if cls in included: + continue + included.append(cls) + parent = parents.get(cls, "") + if parent: + front.append(parent) + + excluded = [] + front = list(profile.get("disabled_classes", [])) + while front: + cls = front.pop() + if cls in excluded: + continue + excluded.append(cls) + front += children.get(cls, []) + + if included and excluded: + print( + "WARNING: Cannot specify both 'enabled_classes' and 'disabled_classes' in build profile. 'disabled_classes' will be ignored." + ) + + return { + "enabled_classes": included, + "disabled_classes": excluded, + } + + +def generate_trimmed_api(source_api_filepath, profile_filepath): + with open(source_api_filepath, encoding="utf-8") as api_file: + api = json.load(api_file) + + if profile_filepath == "": + return api + + build_profile = parse_build_profile(profile_filepath, api) + + engine_classes = {} + for class_api in api["classes"]: + engine_classes[class_api["name"]] = class_api["is_refcounted"] + for native_struct in api["native_structures"]: + if native_struct["name"] == "ObjectID": + continue + engine_classes[native_struct["name"]] = False + + classes = [] + for class_api in api["classes"]: + if not is_class_included(class_api["name"], build_profile): + continue + if "methods" in class_api: + methods = [] + for method in class_api["methods"]: + if not is_method_included(method, build_profile, engine_classes): + continue + methods.append(method) + class_api["methods"] = methods + classes.append(class_api) + api["classes"] = classes + + return api + + +def is_class_included(class_name, build_profile): + """ + Check if an engine class should be included. + This removes classes according to a build profile of enabled or disabled classes. + """ + included = build_profile.get("enabled_classes", []) + excluded = build_profile.get("disabled_classes", []) + if included: + return class_name in included + if excluded: + return class_name not in excluded + return True + + +def is_method_included(method, build_profile, engine_classes): + """ + Check if an engine class method should be included. + This removes methods according to a build profile of enabled or disabled classes. + """ + included = build_profile.get("enabled_classes", []) + excluded = build_profile.get("disabled_classes", []) + ref_cls = set() + rtype = get_base_type(method.get("return_value", {}).get("type", "")) + args = [get_base_type(a["type"]) for a in method.get("arguments", [])] + if rtype in engine_classes: + ref_cls.add(rtype) + elif is_enum(rtype) and get_enum_class(rtype) in engine_classes: + ref_cls.add(get_enum_class(rtype)) + for arg in args: + if arg in engine_classes: + ref_cls.add(arg) + elif is_enum(arg) and get_enum_class(arg) in engine_classes: + ref_cls.add(get_enum_class(arg)) + for acls in ref_cls: + if len(included) > 0 and acls not in included: + return False + elif len(excluded) > 0 and acls in excluded: + return False + return True + + +def is_enum(type_name): + return type_name.startswith("enum::") or type_name.startswith("bitfield::") + + +def get_enum_class(enum_name: str): + if "." in enum_name: + if is_bitfield(enum_name): + return enum_name.replace("bitfield::", "").split(".")[0] + else: + return enum_name.replace("enum::", "").split(".")[0] + else: + return "GlobalConstants" + + +def get_base_type(type_name): + if type_name.startswith("const "): + type_name = type_name[6:] + if type_name.endswith("*"): + type_name = type_name[:-1] + if type_name.startswith("typedarray::"): + type_name = type_name.replace("typedarray::", "") + return type_name + + +def is_bitfield(type_name): + return type_name.startswith("bitfield::") + + +if __name__ == "__main__": + if len(sys.argv) < 3 or len(sys.argv) > 4: + print("Usage: %s BUILD_PROFILE INPUT_JSON [OUTPUT_JSON]" % (sys.argv[0])) + sys.exit(1) + profile = sys.argv[1] + infile = sys.argv[2] + outfile = sys.argv[3] if len(sys.argv) > 3 else "" + api = generate_trimmed_api(infile, profile) + + if outfile: + with open(outfile, "w", encoding="utf-8") as f: + json.dump(api, f) + else: + json.dump(api, sys.stdout) diff --git a/include/godot_cpp/variant/variant_internal.hpp b/include/godot_cpp/variant/variant_internal.hpp index 56b73038..623810bb 100644 --- a/include/godot_cpp/variant/variant_internal.hpp +++ b/include/godot_cpp/variant/variant_internal.hpp @@ -32,7 +32,6 @@ #define GODOT_VARIANT_INTERNAL_HPP #include -#include #include namespace godot { diff --git a/test/build_profile.json b/test/build_profile.json index 3587651c..57d847a1 100644 --- a/test/build_profile.json +++ b/test/build_profile.json @@ -1,9 +1,13 @@ { "enabled_classes": [ "Control", + "InputEventKey", "Label", + "MultiplayerAPI", + "MultiplayerPeer", "OS", "TileMap", - "InputEventKey" + "TileSet", + "Viewport" ] } diff --git a/tools/godotcpp.py b/tools/godotcpp.py index 77a0740f..58392e3a 100644 --- a/tools/godotcpp.py +++ b/tools/godotcpp.py @@ -10,7 +10,8 @@ from SCons.Tool import Tool from SCons.Variables import BoolVariable, EnumVariable, PathVariable from SCons.Variables.BoolVariable import _text2bool -from binding_generator import scons_emit_files, scons_generate_bindings +from binding_generator import _generate_bindings, _get_file_list, get_file_list +from build_profile import generate_trimmed_api def add_sources(sources, dir, extension): @@ -129,6 +130,37 @@ def no_verbose(env): env.Append(GENCOMSTR=[generated_file_message]) +def scons_emit_files(target, source, env): + profile_filepath = env.get("build_profile", "") + if profile_filepath: + profile_filepath = normalize_path(profile_filepath, env) + + # Always clean all files + env.Clean(target, [env.File(f) for f in get_file_list(str(source[0]), target[0].abspath, True, True)]) + + api = generate_trimmed_api(str(source[0]), profile_filepath) + files = [env.File(f) for f in _get_file_list(api, target[0].abspath, True, True)] + env["godot_cpp_gen_dir"] = target[0].abspath + return files, source + + +def scons_generate_bindings(target, source, env): + profile_filepath = env.get("build_profile", "") + if profile_filepath: + profile_filepath = normalize_path(profile_filepath, env) + + api = generate_trimmed_api(str(source[0]), profile_filepath) + + _generate_bindings( + api, + env["generate_template_get_node"], + "32" if "32" in env["arch"] else "64", + env["precision"], + env["godot_cpp_gen_dir"], + ) + return None + + platforms = ["linux", "macos", "windows", "android", "ios", "web"] # CPU architecture options.