Skip to content

Commit 6dc467a

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
1 parent 1cce4d1 commit 6dc467a

File tree

6 files changed

+223
-9
lines changed

6 files changed

+223
-9
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,17 @@ jobs:
110110
with:
111111
python-version: '3.x'
112112

113+
- name: Clone Godot
114+
uses: actions/checkout@v4
115+
with:
116+
repository: godotengine/godot
117+
path: godot
118+
#ref: TODO take tag
119+
120+
- name: Generate compat mappings for godot
121+
run: |
122+
python compat_generator.py godot
123+
113124
- name: Android dependencies
114125
if: ${{ matrix.platform == 'android' }}
115126
uses: nttld/setup-ndk@v1

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ 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.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 obtain them:
74+
75+
```
76+
git clone godotengine/godot
77+
python compat_generator.py godot
78+
```
79+
80+
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.
81+
6782
## Contributing
6883

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

binding_generator.py

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

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-
1826+
method_signature = "\tstatic "
17801827
return_type = None
17811828
if "return_type" in method:
17821829
return_type = correct_type(method["return_type"].replace("ClassDBSingleton", "ClassDB"), None, False)
@@ -1787,9 +1834,7 @@ def generate_engine_class_header(class_api, used_classes, fully_used_classes, us
17871834
False,
17881835
)
17891836
if return_type is not None:
1790-
method_signature += return_type
1791-
if not method_signature.endswith("*"):
1792-
method_signature += " "
1837+
method_signature += return_type + " "
17931838
else:
17941839
method_signature += "void "
17951840

compat_generator.py

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

header_matcher.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import json
2+
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 (any(class_name in data2["classes"] for class_name in data1["classes"]) or
10+
any(define_name in data2["defines"] for define_name in data1["defines"]) or
11+
any(define_name in data2["structs"] for define_name in data1["structs"])):
12+
if header_file not in matches:
13+
matches[header_file] = []
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.json", "r") as file:
21+
mapping1 = json.load(file)
22+
23+
with open("output_header_mapping_godot.json", "r") as file:
24+
mapping2 = json.load(file)
25+
26+
# Match the headers
27+
matches = match_headers(mapping1, mapping2)
28+
29+
# Optionally, you can save the matches to a file
30+
with open("header_matches.json", "w") as outfile:
31+
json.dump(matches, outfile, indent=4)

module_converter.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Using output_header_mapping.json convert all imports in specified source folder location from godot imports to godot-compat imports
2+
3+
4+
import json
5+
import os
6+
import sys
7+
8+
from compat_generator import map_header_files
9+
from header_matcher import match_headers
10+
11+
if __name__ == "__main__":
12+
if len(sys.argv) > 3:
13+
src_directory = os.path.join(os.getcwd(), sys.argv[1])
14+
godot_directory = os.path.join(os.getcwd(), sys.argv[2])
15+
godot_cpp_directory = os.path.join(os.getcwd(), sys.argv[3])
16+
else:
17+
raise Exception("Usage: python module_converter.py <source directory> <godot directory> <godot-cpp directory>")
18+
# Load the godot mappings
19+
with open(f"{godot_directory}/output_header_mapping_godot.json", "r") as file:
20+
godot_mappings = json.load(file)
21+
22+
# Generate mappings for godot-cpp
23+
godot_cpp_mappings = map_header_files(godot_cpp_directory)
24+
matches = match_headers(godot_mappings, godot_cpp_mappings)
25+
# Save matches to a file
26+
with open("header_matches.json", "w") as outfile:
27+
json.dump(matches, outfile, indent=4)
28+
src_directory = os.getcwd()
29+
# Go through folder specified through all files with .cpp, .h or .hpp
30+
for root, dirs, files in os.walk(src_directory):
31+
for file in files:
32+
if file.endswith(".cpp") or file.endswith(".h") or file.endswith(".hpp"):
33+
with open(os.path.join(root, file), "r") as f:
34+
content = f.read()
35+
36+
# Replace imports to godot imports with godot_compat imports
37+
for match in matches:
38+
generate_imports = matches[match]
39+
godot_compat_imports = ""
40+
for generate_import in generate_imports:
41+
godot_compat_import = generate_import.replace("gen/include/godot_cpp/", "godot_compat/")
42+
godot_compat_import = godot_compat_import.replace("include/godot_cpp/", "godot_compat/")
43+
godot_compat_imports += f"#include <{godot_compat_import}>\n"
44+
# Remove last 'n from imports
45+
godot_compat_imports = godot_compat_imports[:-1]
46+
content = content.replace(f"#include \"{match}\"", godot_compat_imports)
47+
# Write the modified content back to the file
48+
with open(os.path.join(root, file), "w") as f:
49+
f.write(content)

0 commit comments

Comments
 (0)