Skip to content

Commit 841edd2

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
1 parent b021245 commit 841edd2

File tree

6 files changed

+212
-0
lines changed

6 files changed

+212
-0
lines changed

.github/workflows/ci.yml

+11
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ jobs:
108108
with:
109109
python-version: '3.x'
110110

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

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ and the [godot-cpp issue tracker](https://github.com/godotengine/godot-cpp/issue
6262
for a list of known issues, and be sure to provide feedback on issues and PRs
6363
which affect your use of this extension.
6464

65+
## Godot and Godot Cpp Compatibility
66+
67+
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.
68+
69+
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.
70+
71+
Example of how to obtain them:
72+
73+
```
74+
git clone godotengine/godot
75+
python compat_generator.py godot
76+
```
77+
78+
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.
79+
6580
## Contributing
6681

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

binding_generator.py

+46
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

@@ -205,6 +208,7 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False):
205208

206209
core_gen_folder = Path(output_dir) / "gen" / "include" / "godot_cpp" / "core"
207210
include_gen_folder = Path(output_dir) / "gen" / "include" / "godot_cpp"
211+
include_gen_compat_folder = Path(output_dir) / "gen" / "include" / "godot_compat"
208212
source_gen_folder = Path(output_dir) / "gen" / "src"
209213

210214
files.append(str((core_gen_folder / "ext_wrappers.gen.inc").as_posix()))
@@ -307,6 +311,7 @@ def generate_bindings(api_filepath, use_template_get_node, bits="64", precision=
307311
generate_builtin_bindings(api, target_dir, real_t + "_" + bits)
308312
generate_engine_classes_bindings(api, target_dir, use_template_get_node)
309313
generate_utility_functions(api, target_dir)
314+
generate_compat_includes(Path(output_dir), target_dir)
310315

311316

312317
builtin_classes = []
@@ -1440,6 +1445,47 @@ def generate_engine_classes_bindings(api, output_dir, use_template_get_node):
14401445
header_file.write("\n".join(result))
14411446

14421447

1448+
def generate_compat_includes(output_dir: Path, target_dir: Path):
1449+
file_types_mapping_godot_cpp_gen = map_header_files(target_dir / "include")
1450+
file_types_mapping_godot_cpp = map_header_files(output_dir / "include") | file_types_mapping_godot_cpp_gen
1451+
godot_compat = Path("output_header_mapping_godot.json")
1452+
levels_to_look_back = 3
1453+
while not godot_compat.exists():
1454+
godot_compat = ".." / godot_compat
1455+
levels_to_look_back -= 1
1456+
if levels_to_look_back == 0:
1457+
print("Skipping godot_compat")
1458+
return
1459+
with godot_compat.open() as file:
1460+
mapping2 = json.load(file)
1461+
# Match the headers
1462+
file_types_mapping = match_headers(file_types_mapping_godot_cpp, mapping2)
1463+
1464+
include_gen_folder = Path(target_dir) / "include"
1465+
for file_godot_cpp_name, file_godot_names in file_types_mapping.items():
1466+
header_filename = file_godot_cpp_name.replace("godot_cpp", "godot_compat")
1467+
header_filepath = include_gen_folder / header_filename
1468+
Path(os.path.dirname(header_filepath)).mkdir(parents=True, exist_ok=True)
1469+
result = []
1470+
snake_header_name = camel_to_snake(header_filename)
1471+
add_header(f"{snake_header_name}.hpp", result)
1472+
1473+
header_guard = f"GODOT_COMPAT_{os.path.splitext(os.path.basename(header_filepath).upper())[0]}_HPP"
1474+
result.append(f"#ifndef {header_guard}")
1475+
result.append(f"#define {header_guard}")
1476+
result.append("")
1477+
result.append(f"#ifdef GODOT_MODULE_COMPAT")
1478+
for file_godot_name in file_godot_names:
1479+
result.append(f"#include <{file_godot_name}>")
1480+
result.append(f"#else")
1481+
result.append(f"#include <{file_godot_cpp_name}>")
1482+
result.append(f"#endif")
1483+
result.append("")
1484+
result.append(f"#endif // ! {header_guard}")
1485+
with header_filepath.open("w+", encoding="utf-8") as header_file:
1486+
header_file.write("\n".join(result))
1487+
1488+
14431489
def generate_engine_class_header(class_api, used_classes, fully_used_classes, use_template_get_node):
14441490
global singletons
14451491
result = []

compat_generator.py

+63
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+
class_pattern = r"class\s+([a-zA-Z_]\w*)\s*[:{]"
15+
struct_pattern = r"struct\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

+31
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

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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) > 2:
13+
current_directory = os.path.join(os.getcwd(), sys.argv[1])
14+
godot_cpp_directory = os.path.join(os.getcwd(), sys.argv[2])
15+
# Load the godot mappings
16+
with open(f"{godot_cpp_directory}/output_header_mapping.json", "r") as file:
17+
godot_mappings = json.load(file)
18+
19+
# Generate mappings for godot-cpp
20+
godot_cpp_mappings = map_header_files(godot_cpp_directory)
21+
matches = match_headers(godot_mappings, godot_cpp_mappings)
22+
# Save matches to a file
23+
with open("header_matches.json", "w") as outfile:
24+
json.dump(matches, outfile, indent=4)
25+
current_directory = os.getcwd()
26+
# Go through folder specified through all files with .cpp, .h or .hpp
27+
for root, dirs, files in os.walk(current_directory):
28+
for file in files:
29+
if file.endswith(".cpp") or file.endswith(".h") or file.endswith(".hpp"):
30+
with open(os.path.join(root, file), "r") as f:
31+
content = f.read()
32+
33+
# Replace imports to godot imports with godot_compat imports
34+
for match in matches:
35+
generate_imports = matches[match]
36+
godot_compat_imports = ""
37+
for generate_import in generate_imports:
38+
godot_compat_import = generate_import.replace("gen/include/godot_cpp/", "godot_compat/")
39+
godot_compat_import = godot_compat_import.replace("include/godot_cpp/", "godot_compat/")
40+
godot_compat_imports += f"#include <{godot_compat_import}>\n"
41+
# Remove last 'n from imports
42+
godot_compat_imports = godot_compat_imports[:-1]
43+
content = content.replace(f"#include \"{match}\"", godot_compat_imports)
44+
# Write the modified content back to the file
45+
with open(os.path.join(root, file), "w") as f:
46+
f.write(content)

0 commit comments

Comments
 (0)