Skip to content

Commit 5d8c20f

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 Update binding_generator.py use files from include too Update README.md lint lint lint Update CMakeLists.txt update to use all. fix linting a bit update wip fix posix path Update CMakeLists.txt Update binding_generator.py add using namespace godot; everywhere to includes fix includes Try fixes.
1 parent 1cce4d1 commit 5d8c20f

File tree

6 files changed

+234
-2
lines changed

6 files changed

+234
-2
lines changed

CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ execute_process(COMMAND "${Python3_EXECUTABLE}" "-c" "import binding_generator;
143143
)
144144

145145
add_custom_command(OUTPUT ${GENERATED_FILES_LIST}
146-
COMMAND "${Python3_EXECUTABLE}" "-c" "import binding_generator; binding_generator.generate_bindings(\"${GODOT_GDEXTENSION_API_FILE}\", \"${GENERATE_BINDING_PARAMETERS}\", \"${BITS}\", \"${FLOAT_PRECISION}\", \"${CMAKE_CURRENT_BINARY_DIR}\")"
146+
COMMAND "${Python3_EXECUTABLE}" "-c" "import binding_generator; binding_generator.generate_bindings(\"${GODOT_GDEXTENSION_API_FILE}\", \"${GENERATE_BINDING_PARAMETERS}\", \"${BITS}\", \"${FLOAT_PRECISION}\", \"${CMAKE_CURRENT_BINARY_DIR}\", \"${GODOT_REPO}\")"
147147
VERBATIM
148148
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
149149
MAIN_DEPENDENCY ${GODOT_GDEXTENSION_API_FILE}

README.md

+61
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 writing 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

+56-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import re
55
import shutil
66
from pathlib import Path
7+
from compat_generator import map_header_files
8+
from header_matcher import match_headers
79

810

911
def generate_mod_version(argcount, const=False, returns=False):
@@ -378,11 +380,14 @@ def scons_generate_bindings(target, source, env):
378380
"32" if "32" in env["arch"] else "64",
379381
env["precision"],
380382
env["godot_cpp_gen_dir"],
383+
env["godot_repo"],
381384
)
382385
return None
383386

384387

385-
def generate_bindings(api_filepath, use_template_get_node, bits="64", precision="single", output_dir="."):
388+
def generate_bindings(
389+
api_filepath, use_template_get_node, bits="64", precision="single", output_dir=".", godot_repo=""
390+
):
386391
api = None
387392

388393
target_dir = Path(output_dir) / "gen"
@@ -402,6 +407,8 @@ def generate_bindings(api_filepath, use_template_get_node, bits="64", precision=
402407
generate_builtin_bindings(api, target_dir, real_t + "_" + bits)
403408
generate_engine_classes_bindings(api, target_dir, use_template_get_node)
404409
generate_utility_functions(api, target_dir)
410+
if godot_repo != "":
411+
generate_compat_includes(godot_repo, output_dir, target_dir)
405412

406413

407414
CLASS_ALIASES = {
@@ -1545,6 +1552,54 @@ def generate_engine_classes_bindings(api, output_dir, use_template_get_node):
15451552
header_file.write("\n".join(result))
15461553

15471554

1555+
def generate_compat_includes(godot_repo: Path, output_dir: Path, target_dir: Path):
1556+
# Uncomment this to change include files
1557+
# target_dir = output_dir
1558+
file_types_mapping_godot_cpp_gen = map_header_files(target_dir)
1559+
file_types_mapping_godot = map_header_files(godot_repo)
1560+
# Match the headers
1561+
file_types_mapping = match_headers(file_types_mapping_godot_cpp_gen, file_types_mapping_godot)
1562+
for file_godot_cpp_name, file_godot_names in file_types_mapping.items():
1563+
result = []
1564+
with (Path(target_dir) / Path(file_godot_cpp_name)).open("r", encoding="utf-8") as header_file:
1565+
content = header_file.readlines()
1566+
# Find the last line (#endif guard)
1567+
last_endif_idx = len(content) - 1
1568+
# Look for the marker for the header guard (usually #define)
1569+
marker_pos = next((i for i, line in enumerate(content) if line.startswith("#define")), -1)
1570+
1571+
if marker_pos != -1:
1572+
# Add content before the last #endif
1573+
before_marker = content[: marker_pos + 1] # Include up to the marker line
1574+
after_marker = content[marker_pos + 1 : last_endif_idx] # Content excluding the final #endif
1575+
1576+
result.extend(before_marker) # Append original content up to marker
1577+
result.append("\n") # Blank line for separation
1578+
1579+
# Insert the #ifdef block
1580+
result.append("#ifdef GODOT_MODULE_COMPAT\n")
1581+
if len(file_godot_names) == 0:
1582+
print("No header found for", file_godot_cpp_name)
1583+
for file_godot_name in file_godot_names:
1584+
result.append(f"#include <{Path(file_godot_name).as_posix()}>\n")
1585+
result.append("#else\n")
1586+
1587+
for line in after_marker:
1588+
if line.strip() not in {"namespace godot {", "} // namespace godot"}:
1589+
result.append(line)
1590+
1591+
# Add the namespace and endif
1592+
result.append("#endif\n")
1593+
1594+
# Finally, append the original final #endif line
1595+
result.append(content[last_endif_idx].strip())
1596+
1597+
else:
1598+
print(f"Marker not found in {file_godot_cpp_name}")
1599+
with (Path(target_dir) / Path(file_godot_cpp_name)).open("w+", encoding="utf-8") as header_file:
1600+
header_file.write("".join(result))
1601+
1602+
15481603
def generate_engine_class_header(class_api, used_classes, fully_used_classes, use_template_get_node):
15491604
global singletons
15501605
result = []

compat_generator.py

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

header_matcher.py

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

tools/godotcpp.py

+8
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)