Skip to content

Commit c4f1abe

Browse files
committed
[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).
1 parent 47f11bc commit c4f1abe

File tree

5 files changed

+236
-138
lines changed

5 files changed

+236
-138
lines changed

binding_generator.py

+15-135
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,16 @@ def generate_virtuals(target):
197197
f.write(txt)
198198

199199

200-
def get_file_list(api_filepath, output_dir, headers=False, sources=False, profile_filepath=""):
200+
def get_file_list(api_filepath, output_dir, headers=False, sources=False):
201201
api = {}
202-
files = []
203202
with open(api_filepath, encoding="utf-8") as api_file:
204203
api = json.load(api_file)
205204

206-
build_profile = parse_build_profile(profile_filepath, api)
205+
return _get_file_list(api, output_dir, headers, sources)
206+
207+
208+
def _get_file_list(api, output_dir, headers=False, sources=False):
209+
files = []
207210

208211
core_gen_folder = Path(output_dir) / "gen" / "include" / "godot_cpp" / "core"
209212
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
235238
source_filename = source_gen_folder / "classes" / (camel_to_snake(engine_class["name"]) + ".cpp")
236239
if headers:
237240
files.append(str(header_filename.as_posix()))
238-
if sources and is_class_included(engine_class["name"], build_profile):
241+
if sources:
239242
files.append(str(source_filename.as_posix()))
240243

241244
for native_struct in api["native_structures"]:
@@ -267,128 +270,19 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False, profil
267270
return files
268271

269272

270-
def print_file_list(api_filepath, output_dir, headers=False, sources=False, profile_filepath=""):
271-
print(*get_file_list(api_filepath, output_dir, headers, sources, profile_filepath), sep=";", end=None)
272-
273-
274-
def parse_build_profile(profile_filepath, api):
275-
if profile_filepath == "":
276-
return {}
277-
print("Using feature build profile: " + profile_filepath)
278-
279-
with open(profile_filepath, encoding="utf-8") as profile_file:
280-
profile = json.load(profile_file)
281-
282-
api_dict = {}
283-
parents = {}
284-
children = {}
285-
for engine_class in api["classes"]:
286-
api_dict[engine_class["name"]] = engine_class
287-
parent = engine_class.get("inherits", "")
288-
child = engine_class["name"]
289-
parents[child] = parent
290-
if parent == "":
291-
continue
292-
children[parent] = children.get(parent, [])
293-
children[parent].append(child)
294-
295-
# Parse methods dependencies
296-
deps = {}
297-
reverse_deps = {}
298-
for name, engine_class in api_dict.items():
299-
ref_cls = set()
300-
for method in engine_class.get("methods", []):
301-
rtype = method.get("return_value", {}).get("type", "")
302-
args = [a["type"] for a in method.get("arguments", [])]
303-
if rtype in api_dict:
304-
ref_cls.add(rtype)
305-
elif is_enum(rtype) and get_enum_class(rtype) in api_dict:
306-
ref_cls.add(get_enum_class(rtype))
307-
for arg in args:
308-
if arg in api_dict:
309-
ref_cls.add(arg)
310-
elif is_enum(arg) and get_enum_class(arg) in api_dict:
311-
ref_cls.add(get_enum_class(arg))
312-
deps[engine_class["name"]] = set(filter(lambda x: x != name, ref_cls))
313-
for acls in ref_cls:
314-
if acls == name:
315-
continue
316-
reverse_deps[acls] = reverse_deps.get(acls, set())
317-
reverse_deps[acls].add(name)
318-
319-
included = []
320-
front = list(profile.get("enabled_classes", []))
321-
if front:
322-
# These must always be included
323-
front.append("WorkerThreadPool")
324-
front.append("ClassDB")
325-
front.append("ClassDBSingleton")
326-
while front:
327-
cls = front.pop()
328-
if cls in included:
329-
continue
330-
included.append(cls)
331-
parent = parents.get(cls, "")
332-
if parent:
333-
front.append(parent)
334-
for rcls in deps.get(cls, set()):
335-
if rcls in included or rcls in front:
336-
continue
337-
front.append(rcls)
338-
339-
excluded = []
340-
front = list(profile.get("disabled_classes", []))
341-
while front:
342-
cls = front.pop()
343-
if cls in excluded:
344-
continue
345-
excluded.append(cls)
346-
front += children.get(cls, [])
347-
for rcls in reverse_deps.get(cls, set()):
348-
if rcls in excluded or rcls in front:
349-
continue
350-
front.append(rcls)
351-
352-
if included and excluded:
353-
print(
354-
"WARNING: Cannot specify both 'enabled_classes' and 'disabled_classes' in build profile. 'disabled_classes' will be ignored."
355-
)
356-
357-
return {
358-
"enabled_classes": included,
359-
"disabled_classes": excluded,
360-
}
361-
362-
363-
def scons_emit_files(target, source, env):
364-
profile_filepath = env.get("build_profile", "")
365-
if profile_filepath and not Path(profile_filepath).is_absolute():
366-
profile_filepath = str((Path(env.Dir("#").abspath) / profile_filepath).as_posix())
367-
368-
files = [env.File(f) for f in get_file_list(str(source[0]), target[0].abspath, True, True, profile_filepath)]
369-
env.Clean(target, files)
370-
env["godot_cpp_gen_dir"] = target[0].abspath
371-
return files, source
372-
373-
374-
def scons_generate_bindings(target, source, env):
375-
generate_bindings(
376-
str(source[0]),
377-
env["generate_template_get_node"],
378-
"32" if "32" in env["arch"] else "64",
379-
env["precision"],
380-
env["godot_cpp_gen_dir"],
381-
)
382-
return None
273+
def print_file_list(api_filepath, output_dir, headers=False, sources=False):
274+
print(*get_file_list(api_filepath, output_dir, headers, sources), sep=";", end=None)
383275

384276

385277
def generate_bindings(api_filepath, use_template_get_node, bits="64", precision="single", output_dir="."):
386-
api = None
387-
388-
target_dir = Path(output_dir) / "gen"
389-
278+
api = {}
390279
with open(api_filepath, encoding="utf-8") as api_file:
391280
api = json.load(api_file)
281+
_generate_bindings(api, use_template_get_node, bits, precision, output_dir)
282+
283+
284+
def _generate_bindings(api, use_template_get_node, bits="64", precision="single", output_dir="."):
285+
target_dir = Path(output_dir) / "gen"
392286

393287
shutil.rmtree(target_dir, ignore_errors=True)
394288
target_dir.mkdir(parents=True)
@@ -2766,20 +2660,6 @@ def is_refcounted(type_name):
27662660
return type_name in engine_classes and engine_classes[type_name]
27672661

27682662

2769-
def is_class_included(class_name, build_profile):
2770-
"""
2771-
Check if an engine class should be included.
2772-
This removes classes according to a build profile of enabled or disabled classes.
2773-
"""
2774-
included = build_profile.get("enabled_classes", [])
2775-
excluded = build_profile.get("disabled_classes", [])
2776-
if included:
2777-
return class_name in included
2778-
if excluded:
2779-
return class_name not in excluded
2780-
return True
2781-
2782-
27832663
def is_included(type_name, current_type):
27842664
"""
27852665
Check if a builtin type should be included.

build_profile.py

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import json
2+
import sys
3+
4+
5+
def parse_build_profile(profile_filepath, api):
6+
if profile_filepath == "":
7+
return {}
8+
9+
with open(profile_filepath, encoding="utf-8") as profile_file:
10+
profile = json.load(profile_file)
11+
12+
api_dict = {}
13+
parents = {}
14+
children = {}
15+
for engine_class in api["classes"]:
16+
api_dict[engine_class["name"]] = engine_class
17+
parent = engine_class.get("inherits", "")
18+
child = engine_class["name"]
19+
parents[child] = parent
20+
if parent == "":
21+
continue
22+
children[parent] = children.get(parent, [])
23+
children[parent].append(child)
24+
25+
included = []
26+
front = list(profile.get("enabled_classes", []))
27+
if front:
28+
# These must always be included
29+
front.append("WorkerThreadPool")
30+
front.append("ClassDB")
31+
front.append("ClassDBSingleton")
32+
# In src/classes/low_level.cpp
33+
front.append("FileAccess")
34+
front.append("Image")
35+
front.append("XMLParser")
36+
# In include/godot_cpp/templates/thread_work_pool.hpp
37+
front.append("Semaphore")
38+
while front:
39+
cls = front.pop()
40+
if cls in included:
41+
continue
42+
included.append(cls)
43+
parent = parents.get(cls, "")
44+
if parent:
45+
front.append(parent)
46+
47+
excluded = []
48+
front = list(profile.get("disabled_classes", []))
49+
while front:
50+
cls = front.pop()
51+
if cls in excluded:
52+
continue
53+
excluded.append(cls)
54+
front += children.get(cls, [])
55+
56+
if included and excluded:
57+
print(
58+
"WARNING: Cannot specify both 'enabled_classes' and 'disabled_classes' in build profile. 'disabled_classes' will be ignored."
59+
)
60+
61+
return {
62+
"enabled_classes": included,
63+
"disabled_classes": excluded,
64+
}
65+
66+
67+
def generate_trimmed_api(source_api_filepath, profile_filepath):
68+
with open(source_api_filepath, encoding="utf-8") as api_file:
69+
api = json.load(api_file)
70+
71+
if profile_filepath == "":
72+
return api
73+
74+
build_profile = parse_build_profile(profile_filepath, api)
75+
76+
engine_classes = {}
77+
for class_api in api["classes"]:
78+
engine_classes[class_api["name"]] = class_api["is_refcounted"]
79+
for native_struct in api["native_structures"]:
80+
if native_struct["name"] == "ObjectID":
81+
continue
82+
engine_classes[native_struct["name"]] = False
83+
84+
classes = []
85+
for class_api in api["classes"]:
86+
if not is_class_included(class_api["name"], build_profile):
87+
continue
88+
if "methods" in class_api:
89+
methods = []
90+
for method in class_api["methods"]:
91+
if not is_method_included(method, build_profile, engine_classes):
92+
continue
93+
methods.append(method)
94+
class_api["methods"] = methods
95+
classes.append(class_api)
96+
api["classes"] = classes
97+
98+
return api
99+
100+
101+
def is_class_included(class_name, build_profile):
102+
"""
103+
Check if an engine class should be included.
104+
This removes classes according to a build profile of enabled or disabled classes.
105+
"""
106+
included = build_profile.get("enabled_classes", [])
107+
excluded = build_profile.get("disabled_classes", [])
108+
if included:
109+
return class_name in included
110+
if excluded:
111+
return class_name not in excluded
112+
return True
113+
114+
115+
def is_method_included(method, build_profile, engine_classes):
116+
"""
117+
Check if an engine class method should be included.
118+
This removes methods according to a build profile of enabled or disabled classes.
119+
"""
120+
included = build_profile.get("enabled_classes", [])
121+
excluded = build_profile.get("disabled_classes", [])
122+
ref_cls = set()
123+
rtype = get_base_type(method.get("return_value", {}).get("type", ""))
124+
args = [get_base_type(a["type"]) for a in method.get("arguments", [])]
125+
if rtype in engine_classes:
126+
ref_cls.add(rtype)
127+
elif is_enum(rtype) and get_enum_class(rtype) in engine_classes:
128+
ref_cls.add(get_enum_class(rtype))
129+
for arg in args:
130+
if arg in engine_classes:
131+
ref_cls.add(arg)
132+
elif is_enum(arg) and get_enum_class(arg) in engine_classes:
133+
ref_cls.add(get_enum_class(arg))
134+
for acls in ref_cls:
135+
if len(included) > 0 and acls not in included:
136+
return False
137+
elif len(excluded) > 0 and acls in excluded:
138+
return False
139+
return True
140+
141+
142+
def is_enum(type_name):
143+
return type_name.startswith("enum::") or type_name.startswith("bitfield::")
144+
145+
146+
def get_enum_class(enum_name: str):
147+
if "." in enum_name:
148+
if is_bitfield(enum_name):
149+
return enum_name.replace("bitfield::", "").split(".")[0]
150+
else:
151+
return enum_name.replace("enum::", "").split(".")[0]
152+
else:
153+
return "GlobalConstants"
154+
155+
156+
def get_base_type(type_name):
157+
if type_name.startswith("const "):
158+
type_name = type_name[6:]
159+
if type_name.endswith("*"):
160+
type_name = type_name[:-1]
161+
if type_name.startswith("typedarray::"):
162+
type_name = type_name.replace("typedarray::", "")
163+
return type_name
164+
165+
166+
def is_bitfield(type_name):
167+
return type_name.startswith("bitfield::")
168+
169+
170+
if __name__ == "__main__":
171+
if len(sys.argv) < 3 or len(sys.argv) > 4:
172+
print("Usage: %s BUILD_PROFILE INPUT_JSON [OUTPUT_JSON]" % (sys.argv[0]))
173+
sys.exit(1)
174+
profile = sys.argv[1]
175+
infile = sys.argv[2]
176+
outfile = sys.argv[3] if len(sys.argv) > 3 else ""
177+
api = generate_trimmed_api(infile, profile)
178+
179+
if outfile:
180+
with open(outfile, "w", encoding="utf-8") as f:
181+
json.dump(api, f)
182+
else:
183+
json.dump(api, sys.stdout)

include/godot_cpp/variant/variant_internal.hpp

-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
#define GODOT_VARIANT_INTERNAL_HPP
3333

3434
#include <gdextension_interface.h>
35-
#include <godot_cpp/classes/gpu_particles3d.hpp>
3635
#include <godot_cpp/variant/variant.hpp>
3736

3837
namespace godot {

test/build_profile.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
{
22
"enabled_classes": [
33
"Control",
4+
"InputEventKey",
45
"Label",
6+
"MultiplayerAPI",
7+
"MultiplayerPeer",
58
"OS",
69
"TileMap",
7-
"InputEventKey"
10+
"TileSet",
11+
"Viewport"
812
]
913
}

0 commit comments

Comments
 (0)