Skip to content

Commit bfc5e50

Browse files
committed
Generate godot compat for dual build
generate compat generate compat Update ci.yml Update binding_generator.py generate compat generate compat lint python files Update compat_generator.py update docs Update binding_generator.py Update module_converter.py also collect defines Add module converter file that converts module based projects to godot_compat Update ci.yml update docs Update compat_generator.py lint python files generate compat generate compat generate compat generate compat Update ci.yml fix path issue when caling from outside Update binding_generator.py update to also take missing classes/structs Update binding_generator.py Generate godot compat for dual build generate compat generate compat Update ci.yml Update binding_generator.py generate compat generate compat lint python files Update compat_generator.py update docs Update binding_generator.py Update module_converter.py also collect defines Add module converter file that converts module based projects to godot_compat Update ci.yml update docs Update compat_generator.py lint python files generate compat generate compat generate compat generate compat Update ci.yml fix path issue when caling from outside Add support for build profiles. Allow enabling or disabling specific classes (which will not be built). Allow forwarding from `ClassDB` to `ClassDBSingleton` to support enumerations update to also take missing classes/structs Update binding_generator.py update update naming of files add godot mappings. update and run output_header_mapping.json
1 parent 1cce4d1 commit bfc5e50

5 files changed

+25782
-9
lines changed

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,23 @@ first-party `godot-cpp` extension.
6464
> for a list of known issues, and be sure to provide feedback on issues and PRs
6565
> which affect your use of this extension.
6666
67+
## Godot and Godot Cpp Compatibility
68+
69+
If you intend to target both building as a GDExtension and as a module using godot repo, you can generate compatibility includes that will target either GDExtension or module, based on the GODOT_MODULE_COMPAT define.
70+
71+
If you want such a thing built, when running the build command, `scons`, make sure you have a file called `output_header_mapping_godot_cpp.json` at root level of this repo. This file needs to have the mappings from `godot` repo. The mappings can be generated by running the `compat_generator.py` script.
72+
73+
Example of how to run `compat_generator.py`:
74+
75+
```
76+
git clone godotengine/godot
77+
python compat_generator.py godot
78+
```
79+
80+
The first argument of `compat_generator.py` is the folder where the repo is (can be godot or godot-cpp repo). If this folder is not given, the current directory is assumed. The output of this is either `output_header_mapping_godot.json` or `output_header_mapping_godot_cpp.json`
81+
82+
Then run the SConstruct build command as usual, and in the `gen/` folder you will now have a new folder, `include/godot_compat` which mirrors the `include/godot_cpp` includes, but have ifdef inside them and either include godot header or godot_cpp header.
83+
6784
## Contributing
6885

6986
We greatly appreciate help in maintaining and extending this project. If you

binding_generator.py

+53-9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import json
44
import re
55
import shutil
6+
import os
7+
from compat_generator import map_header_files
8+
from header_matcher import match_headers
69
from pathlib import Path
710

811

@@ -207,6 +210,7 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False, profil
207210

208211
core_gen_folder = Path(output_dir) / "gen" / "include" / "godot_cpp" / "core"
209212
include_gen_folder = Path(output_dir) / "gen" / "include" / "godot_cpp"
213+
include_gen_compat_folder = Path(output_dir) / "gen" / "include" / "godot_compat"
210214
source_gen_folder = Path(output_dir) / "gen" / "src"
211215

212216
files.append(str((core_gen_folder / "ext_wrappers.gen.inc").as_posix()))
@@ -220,9 +224,11 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False, profil
220224
continue
221225

222226
header_filename = include_gen_folder / "variant" / (camel_to_snake(builtin_class["name"]) + ".hpp")
227+
header_compat_filename = include_gen_compat_folder / "variant" / (camel_to_snake(builtin_class["name"]) + ".hpp")
223228
source_filename = source_gen_folder / "variant" / (camel_to_snake(builtin_class["name"]) + ".cpp")
224229
if headers:
225230
files.append(str(header_filename.as_posix()))
231+
files.append(str(header_compat_filename.as_posix()))
226232
if sources:
227233
files.append(str(source_filename.as_posix()))
228234

@@ -232,9 +238,11 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False, profil
232238
engine_class["name"] = "ClassDBSingleton"
233239
engine_class["alias_for"] = "ClassDB"
234240
header_filename = include_gen_folder / "classes" / (camel_to_snake(engine_class["name"]) + ".hpp")
241+
header_compat_filename = include_gen_compat_folder / "classes" / (camel_to_snake(engine_class["name"]) + ".hpp")
235242
source_filename = source_gen_folder / "classes" / (camel_to_snake(engine_class["name"]) + ".cpp")
236243
if headers:
237244
files.append(str(header_filename.as_posix()))
245+
files.append(str(header_compat_filename.as_posix()))
238246
if sources and is_class_included(engine_class["name"], build_profile):
239247
files.append(str(source_filename.as_posix()))
240248

@@ -245,8 +253,10 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False, profil
245253
snake_struct_name = camel_to_snake(struct_name)
246254

247255
header_filename = include_gen_folder / "classes" / (snake_struct_name + ".hpp")
256+
header_compat_filename = include_gen_compat_folder / "classes" / (snake_struct_name + ".hpp")
248257
if headers:
249258
files.append(str(header_filename.as_posix()))
259+
files.append(str(header_compat_filename.as_posix()))
250260

251261
if headers:
252262
for path in [
@@ -402,6 +412,7 @@ def generate_bindings(api_filepath, use_template_get_node, bits="64", precision=
402412
generate_builtin_bindings(api, target_dir, real_t + "_" + bits)
403413
generate_engine_classes_bindings(api, target_dir, use_template_get_node)
404414
generate_utility_functions(api, target_dir)
415+
generate_compat_includes(Path(output_dir), target_dir)
405416

406417

407418
CLASS_ALIASES = {
@@ -1545,6 +1556,46 @@ def generate_engine_classes_bindings(api, output_dir, use_template_get_node):
15451556
header_file.write("\n".join(result))
15461557

15471558

1559+
def generate_compat_includes(output_dir: Path, target_dir: Path):
1560+
file_types_mapping_godot_cpp_gen = map_header_files(target_dir / "include")
1561+
godot_compat = Path("output_header_mapping_godot.json")
1562+
levels_to_look_back = 3
1563+
while not godot_compat.exists():
1564+
godot_compat = ".." / godot_compat
1565+
levels_to_look_back -= 1
1566+
if levels_to_look_back == 0:
1567+
print("Skipping godot_compat")
1568+
return
1569+
with godot_compat.open() as file:
1570+
mapping2 = json.load(file)
1571+
# Match the headers
1572+
file_types_mapping = match_headers(file_types_mapping_godot_cpp_gen, mapping2)
1573+
1574+
include_gen_folder = Path(target_dir) / "include"
1575+
for file_godot_cpp_name, file_godot_names in file_types_mapping.items():
1576+
header_filename = file_godot_cpp_name.replace("godot_cpp", "godot_compat")
1577+
header_filepath = include_gen_folder / header_filename
1578+
Path(os.path.dirname(header_filepath)).mkdir(parents=True, exist_ok=True)
1579+
result = []
1580+
snake_header_name = camel_to_snake(header_filename)
1581+
add_header(f"{snake_header_name}.hpp", result)
1582+
1583+
header_guard = f"GODOT_COMPAT_{os.path.splitext(os.path.basename(header_filepath).upper())[0]}_HPP"
1584+
result.append(f"#ifndef {header_guard}")
1585+
result.append(f"#define {header_guard}")
1586+
result.append("")
1587+
result.append(f"#ifdef GODOT_MODULE_COMPAT")
1588+
for file_godot_name in file_godot_names:
1589+
result.append(f"#include <{file_godot_name}>")
1590+
result.append(f"#else")
1591+
result.append(f"#include <{file_godot_cpp_name}>")
1592+
result.append(f"#endif")
1593+
result.append("")
1594+
result.append(f"#endif // ! {header_guard}")
1595+
with header_filepath.open("w+", encoding="utf-8") as header_file:
1596+
header_file.write("\n".join(result))
1597+
1598+
15481599
def generate_engine_class_header(class_api, used_classes, fully_used_classes, use_template_get_node):
15491600
global singletons
15501601
result = []
@@ -1771,12 +1822,7 @@ def generate_engine_class_header(class_api, used_classes, fully_used_classes, us
17711822
if "is_static" in method and method["is_static"]:
17721823
continue
17731824

1774-
vararg = "is_vararg" in method and method["is_vararg"]
1775-
if vararg:
1776-
method_signature = "\ttemplate <typename... Args> static "
1777-
else:
1778-
method_signature = "\tstatic "
1779-
1825+
method_signature = "\tstatic "
17801826
return_type = None
17811827
if "return_type" in method:
17821828
return_type = correct_type(method["return_type"].replace("ClassDBSingleton", "ClassDB"), None, False)
@@ -1787,9 +1833,7 @@ def generate_engine_class_header(class_api, used_classes, fully_used_classes, us
17871833
False,
17881834
)
17891835
if return_type is not None:
1790-
method_signature += return_type
1791-
if not method_signature.endswith("*"):
1792-
method_signature += " "
1836+
method_signature += return_type + " "
17931837
else:
17941838
method_signature += "void "
17951839

compat_generator.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env python
2+
3+
import re
4+
import os
5+
import json
6+
import sys
7+
8+
9+
def parse_header_file(file_path):
10+
types = {"classes": [], "structs": [], "defines": []}
11+
12+
with open(file_path, "r", encoding="utf-8") as file:
13+
content = file.read()
14+
15+
# Regular expressions to match different types
16+
struct_pattern = r"struct\s+[\w\s]*?([a-zA-Z_]\w*)\s*[:{]"
17+
class_pattern = r"class\s+[\w\s]*?([a-zA-Z_]\w*)\s*[:{]"
18+
define_pattern = r"#define\s+([a-zA-Z_]\w*)"
19+
20+
# Extract classes
21+
types["classes"] += re.findall(class_pattern, content)
22+
23+
# Extract structs
24+
types["structs"] += re.findall(struct_pattern, content)
25+
26+
# Extract defines
27+
define_matches = re.findall(define_pattern, content)
28+
types["defines"] += define_matches
29+
30+
# Debug the case where no classes or structs are found
31+
#if len(types["classes"]) == 0 and len(types["structs"]) == 0 and len(types["defines"]) == 0:
32+
# print(f"{file_path} missing things")
33+
return types
34+
35+
36+
def map_header_files(directory):
37+
file_types_mapping = {}
38+
39+
for root, dirs, files in os.walk(directory):
40+
if "thirdparty" in dirs:
41+
dirs.remove("thirdparty")
42+
if "tests" in dirs:
43+
dirs.remove("tests")
44+
if "test" in dirs:
45+
dirs.remove("test")
46+
if "misc" in dirs:
47+
dirs.remove("misc")
48+
for file in files:
49+
if file.endswith(".h") or file.endswith(".hpp"):
50+
relative_path = os.path.relpath(root, directory)
51+
file_path = os.path.join(root, file)
52+
file_types_mapping[f"{relative_path}/{file}"] = parse_header_file(file_path)
53+
54+
return file_types_mapping
55+
56+
57+
if __name__ == "__main__":
58+
# Get current directory
59+
current_directory = os.getcwd()
60+
mapping_name = ""
61+
if len(sys.argv) > 1:
62+
mapping_name = f"_godot"
63+
current_directory = os.path.join(os.getcwd(), sys.argv[1])
64+
65+
file_types_mapping = map_header_files(current_directory)
66+
with open(f"output_header_mapping{mapping_name}.json", "w") as json_file:
67+
json.dump(file_types_mapping, json_file, indent=4)

header_matcher.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import json
2+
from compat_generator import map_header_files
3+
4+
def match_headers(mapping1, mapping2):
5+
matches = {}
6+
for header_file, data1 in mapping1.items():
7+
for header_file2, data2 in mapping2.items():
8+
# Check if classes/defines/structs in header_file1 are present in header_file2
9+
if header_file not in matches:
10+
matches[header_file] = []
11+
if (any(class_name in data2["classes"] for class_name in data1["classes"]) or
12+
any(define_name in data2["defines"] for define_name in data1["defines"]) or
13+
any(define_name in data2["structs"] for define_name in data1["structs"])):
14+
matches[header_file].append(header_file2)
15+
return matches
16+
17+
18+
if __name__ == "__main__":
19+
# Load the two header mappings
20+
with open("output_header_mapping_godot.json", "r") as file:
21+
mapping_godot = json.load(file)
22+
file_types_mapping_godot_cpp_gen = map_header_files("gen/include")
23+
matches = match_headers(file_types_mapping_godot_cpp_gen, mapping_godot)
24+
25+
# Optionally, you can save the matches to a file
26+
with open("header_matches.json", "w") as outfile:
27+
json.dump(matches, outfile, indent=4)

0 commit comments

Comments
 (0)