Skip to content

Commit fe2da72

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 Update README.md make godot_compat work without a file generated fix the test Update binding_generator.py Update binding_generator.py
1 parent 1cce4d1 commit fe2da72

File tree

5 files changed

+233
-16
lines changed

5 files changed

+233
-16
lines changed

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,64 @@ generic reusable template.
147147

148148
Or checkout the code for the [Summator example](https://github.com/paddy-exe/GDExtensionSummator)
149149
as shown in the [official documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/gdextension/gdextension_cpp_example.html).
150+
151+
## Godot and Godot Cpp Compatibility
152+
153+
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.
154+
155+
If you want such headers, when running the build command, `scons`, pass the `godot_repo` param with the path to the godot repository. Eg. if you have the godot repository cloned at path `../godot`, then do:
156+
157+
```sh
158+
scons godot_repo="../godot"
159+
```
160+
161+
This will generate something like this:
162+
```
163+
gen/include/godot_cpp/..
164+
gen/include/godot_compat/..
165+
```
166+
167+
Now, all you need to do is when writting your addon/module, replace includes like these:
168+
169+
```cpp
170+
#include <godot_cpp/classes/a_star_grid2d.hpp>
171+
```
172+
173+
with
174+
175+
```cpp
176+
#include <godot_compat/classes/a_star_grid2d.hpp>
177+
```
178+
179+
Inside, this file will have code for both godot and godot-cpp:
180+
181+
```cpp
182+
#ifdef GODOT_MODULE_COMPAT
183+
#include <core/math/a_star_grid_2d.h>
184+
#else
185+
#include <godot_cpp/classes/a_star_grid2d.hpp>
186+
#endif
187+
```
188+
189+
### Manually generate mapping files
190+
191+
The mappings can be manually generated by running the `compat_generator.py` script.
192+
193+
Example of how to run `compat_generator.py`:
194+
195+
```sh
196+
git clone godotengine/godot
197+
python compat_generator.py godot
198+
```
199+
200+
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`
201+
202+
### Manually match the mapping files
203+
204+
If you want to manually match the godot mapping file with the godot-cpp one, you can do that by running:
205+
206+
```sh
207+
python header_matcher.py
208+
```
209+
210+
This will generate the `header_matches.json` file with matches from godot and godot_cpp repo.

binding_generator.py

Lines changed: 69 additions & 16 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

@@ -197,7 +200,7 @@ def generate_virtuals(target):
197200
f.write(txt)
198201

199202

200-
def get_file_list(api_filepath, output_dir, headers=False, sources=False, profile_filepath=""):
203+
def get_file_list(api_filepath, output_dir, headers=False, sources=False, compat=False, profile_filepath=""):
201204
api = {}
202205
files = []
203206
with open(api_filepath, encoding="utf-8") as api_file:
@@ -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,12 @@ 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+
if compat:
232+
files.append(str(header_compat_filename.as_posix()))
226233
if sources:
227234
files.append(str(source_filename.as_posix()))
228235

@@ -232,9 +239,12 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False, profil
232239
engine_class["name"] = "ClassDBSingleton"
233240
engine_class["alias_for"] = "ClassDB"
234241
header_filename = include_gen_folder / "classes" / (camel_to_snake(engine_class["name"]) + ".hpp")
242+
header_compat_filename = include_gen_compat_folder / "classes" / (camel_to_snake(engine_class["name"]) + ".hpp")
235243
source_filename = source_gen_folder / "classes" / (camel_to_snake(engine_class["name"]) + ".cpp")
236244
if headers:
237245
files.append(str(header_filename.as_posix()))
246+
if compat:
247+
files.append(str(header_compat_filename.as_posix()))
238248
if sources and is_class_included(engine_class["name"], build_profile):
239249
files.append(str(source_filename.as_posix()))
240250

@@ -245,30 +255,39 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False, profil
245255
snake_struct_name = camel_to_snake(struct_name)
246256

247257
header_filename = include_gen_folder / "classes" / (snake_struct_name + ".hpp")
258+
header_compat_filename = include_gen_compat_folder / "classes" / (snake_struct_name + ".hpp")
248259
if headers:
249260
files.append(str(header_filename.as_posix()))
261+
if compat:
262+
files.append(str(header_compat_filename.as_posix()))
250263

251264
if headers:
252-
for path in [
253-
include_gen_folder / "variant" / "builtin_types.hpp",
254-
include_gen_folder / "variant" / "builtin_binds.hpp",
255-
include_gen_folder / "variant" / "utility_functions.hpp",
256-
include_gen_folder / "variant" / "variant_size.hpp",
257-
include_gen_folder / "variant" / "builtin_vararg_methods.hpp",
258-
include_gen_folder / "classes" / "global_constants.hpp",
259-
include_gen_folder / "classes" / "global_constants_binds.hpp",
260-
include_gen_folder / "core" / "version.hpp",
261-
]:
262-
files.append(str(path.as_posix()))
265+
relative_paths = [
266+
["variant", "builtin_types.hpp"],
267+
["variant", "builtin_binds.hpp"],
268+
["variant", "utility_functions.hpp"],
269+
["variant", "variant_size.hpp"],
270+
["variant", "builtin_vararg_methods.hpp"],
271+
["classes", "global_constants.hpp"],
272+
["classes", "global_constants_binds.hpp"],
273+
["core", "version.hpp"]
274+
]
275+
276+
for relative_path_parts in relative_paths:
277+
full_header_path = include_gen_folder.joinpath(*relative_path_parts)
278+
files.append(str(full_header_path.as_posix()))
279+
if compat:
280+
full_compat_path = include_gen_compat_folder.joinpath(*relative_path_parts)
281+
files.append(str(full_compat_path.as_posix()))
263282
if sources:
264283
utility_functions_source_path = source_gen_folder / "variant" / "utility_functions.cpp"
265284
files.append(str(utility_functions_source_path.as_posix()))
266285

267286
return files
268287

269288

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)
289+
def print_file_list(api_filepath, output_dir, headers=False, sources=False, compat=False, profile_filepath=""):
290+
print(*get_file_list(api_filepath, output_dir, headers, sources, compat, profile_filepath), sep=";", end=None)
272291

273292

274293
def parse_build_profile(profile_filepath, api):
@@ -365,7 +384,7 @@ def scons_emit_files(target, source, env):
365384
if profile_filepath and not Path(profile_filepath).is_absolute():
366385
profile_filepath = str((Path(env.Dir("#").abspath) / profile_filepath).as_posix())
367386

368-
files = [env.File(f) for f in get_file_list(str(source[0]), target[0].abspath, True, True, profile_filepath)]
387+
files = [env.File(f) for f in get_file_list(str(source[0]), target[0].abspath, True, True, env["godot_repo"] != "", profile_filepath)]
369388
env.Clean(target, files)
370389
env["godot_cpp_gen_dir"] = target[0].abspath
371390
return files, source
@@ -378,11 +397,12 @@ def scons_generate_bindings(target, source, env):
378397
"32" if "32" in env["arch"] else "64",
379398
env["precision"],
380399
env["godot_cpp_gen_dir"],
400+
env["godot_repo"],
381401
)
382402
return None
383403

384404

385-
def generate_bindings(api_filepath, use_template_get_node, bits="64", precision="single", output_dir="."):
405+
def generate_bindings(api_filepath, use_template_get_node, bits="64", precision="single", output_dir=".", godot_repo = ""):
386406
api = None
387407

388408
target_dir = Path(output_dir) / "gen"
@@ -402,6 +422,8 @@ def generate_bindings(api_filepath, use_template_get_node, bits="64", precision=
402422
generate_builtin_bindings(api, target_dir, real_t + "_" + bits)
403423
generate_engine_classes_bindings(api, target_dir, use_template_get_node)
404424
generate_utility_functions(api, target_dir)
425+
if godot_repo != "":
426+
generate_compat_includes(godot_repo, target_dir)
405427

406428

407429
CLASS_ALIASES = {
@@ -1545,6 +1567,37 @@ def generate_engine_classes_bindings(api, output_dir, use_template_get_node):
15451567
header_file.write("\n".join(result))
15461568

15471569

1570+
def generate_compat_includes(godot_repo: Path, target_dir: Path):
1571+
file_types_mapping_godot_cpp_gen = map_header_files(target_dir / "include")
1572+
file_types_mapping_godot = map_header_files(godot_repo)
1573+
# Match the headers
1574+
file_types_mapping = match_headers(file_types_mapping_godot_cpp_gen, file_types_mapping_godot)
1575+
1576+
include_gen_folder = Path(target_dir) / "include"
1577+
for file_godot_cpp_name, file_godot_names in file_types_mapping.items():
1578+
header_filename = file_godot_cpp_name.replace("godot_cpp", "godot_compat")
1579+
header_filepath = include_gen_folder / header_filename
1580+
Path(os.path.dirname(header_filepath)).mkdir(parents=True, exist_ok=True)
1581+
result = []
1582+
snake_header_name = camel_to_snake(header_filename)
1583+
add_header(f"{snake_header_name}.hpp", result)
1584+
1585+
header_guard = f"GODOT_COMPAT_{os.path.splitext(os.path.basename(header_filepath).upper())[0]}_HPP"
1586+
result.append(f"#ifndef {header_guard}")
1587+
result.append(f"#define {header_guard}")
1588+
result.append("")
1589+
result.append(f"#ifdef GODOT_MODULE_COMPAT")
1590+
for file_godot_name in file_godot_names:
1591+
result.append(f"#include <{file_godot_name}>")
1592+
result.append(f"#else")
1593+
result.append(f"#include <{file_godot_cpp_name}>")
1594+
result.append(f"#endif")
1595+
result.append("")
1596+
result.append(f"#endif // ! {header_guard}")
1597+
with header_filepath.open("w+", encoding="utf-8") as header_file:
1598+
header_file.write("\n".join(result))
1599+
1600+
15481601
def generate_engine_class_header(class_api, used_classes, fully_used_classes, use_template_get_node):
15491602
global singletons
15501603
result = []

compat_generator.py

Lines changed: 67 additions & 0 deletions
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

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

tools/godotcpp.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,14 @@ def options(opts, env):
228228
validator=validate_file,
229229
)
230230
)
231+
opts.Add(
232+
PathVariable(
233+
key="godot_repo",
234+
help="Path to a custom directory containing Godot repository. Used to generate godot_compat bindings.",
235+
default=env.get("godot_repo", ""),
236+
validator=validate_dir,
237+
)
238+
)
231239
opts.Add(
232240
BoolVariable(
233241
key="generate_bindings",

0 commit comments

Comments
 (0)