Skip to content

Commit 17de6f7

Browse files
committed
Allow creating GDExtension plugins from inside the Godot editor
1 parent 01545c9 commit 17de6f7

40 files changed

+1687
-5
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ repos:
143143
exclude: |
144144
(?x)^(
145145
core/math/bvh_.*\.inc|
146+
editor/plugins/gdextension/cpp_scons/template/.*|
146147
platform/(?!android|ios|linuxbsd|macos|web|windows)\w+/.*|
147148
platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView\.java|
148149
platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper\.java|

editor/editor_node.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
#include "editor/plugins/editor_preview_plugins.h"
145145
#include "editor/plugins/editor_resource_conversion_plugin.h"
146146
#include "editor/plugins/game_view_plugin.h"
147-
#include "editor/plugins/gdextension_export_plugin.h"
147+
#include "editor/plugins/gdextension/gdextension_export_plugin.h"
148148
#include "editor/plugins/material_editor_plugin.h"
149149
#include "editor/plugins/mesh_library_editor_plugin.h"
150150
#include "editor/plugins/node_3d_editor_plugin.h"

editor/gui/editor_validation_panel.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ void EditorValidationPanel::add_line(int p_id, const String &p_valid_message) {
6464
ERR_FAIL_COND(valid_messages.has(p_id));
6565

6666
Label *label = memnew(Label);
67-
message_container->add_child(label);
6867
label->set_custom_minimum_size(Size2(200 * EDSCALE, 0));
6968
label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
7069
label->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
70+
message_container->add_child(label);
7171

7272
valid_messages[p_id] = p_valid_message;
7373
labels[p_id] = label;
@@ -124,6 +124,10 @@ void EditorValidationPanel::set_message(int p_id, const String &p_text, MessageT
124124
}
125125
}
126126

127+
int EditorValidationPanel::get_message_count() const {
128+
return valid_messages.size();
129+
}
130+
127131
bool EditorValidationPanel::is_valid() const {
128132
return valid;
129133
}

editor/gui/editor_validation_panel.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class EditorValidationPanel : public PanelContainer {
8080

8181
void update();
8282
void set_message(int p_id, const String &p_text, MessageType p_type, bool p_auto_prefix = true);
83+
int get_message_count() const;
8384
bool is_valid() const;
8485

8586
EditorValidationPanel();

editor/plugins/SCsub

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ Import("env")
55

66
env.add_source_files(env.editor_sources, "*.cpp")
77

8+
SConscript("gdextension/SCsub")
89
SConscript("gizmos/SCsub")
910
SConscript("tiles/SCsub")

editor/plugins/gdextension/SCsub

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env python
2+
from misc.utility.scons_hints import *
3+
4+
Import("env")
5+
6+
env.add_source_files(env.editor_sources, "*.cpp")
7+
8+
SConscript("cpp_scons/SCsub")
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python
2+
from misc.utility.scons_hints import *
3+
4+
import os
5+
6+
Import("env")
7+
8+
env.add_source_files(env.editor_sources, "*.cpp")
9+
10+
11+
def parse_template(source):
12+
with open(source) as file:
13+
lines = file.readlines()
14+
script_template = ""
15+
for line in lines:
16+
script_template += line
17+
if env["precision"] != "double":
18+
script_template = script_template.replace('ARGUMENTS.setdefault("precision", "double")', "")
19+
name = os.path.basename(source).upper().replace(".", "_")
20+
return "\nconst String " + name + ' = R"(' + script_template.rstrip() + ')";\n'
21+
22+
23+
def make_templates(target, source, env):
24+
dst = str(target[0])
25+
with StringIO() as s:
26+
s.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n\n")
27+
s.write("#ifndef GDEXTENSION_TEMPLATE_FILES_GEN_H\n")
28+
s.write("#define GDEXTENSION_TEMPLATE_FILES_GEN_H\n\n")
29+
s.write('#include "core/string/ustring.h"\n')
30+
parsed_template_string = ""
31+
for file in source:
32+
filepath = str(file)
33+
if os.path.isfile(filepath):
34+
parsed_template_string += parse_template(filepath)
35+
s.write(parsed_template_string)
36+
s.write("\n#endif // GDEXTENSION_TEMPLATE_FILES_GEN_H\n")
37+
with open(dst, "w", encoding="utf-8", newline="\n") as f:
38+
f.write(s.getvalue())
39+
40+
41+
env["BUILDERS"]["MakeGDExtTemplateBuilder"] = Builder(
42+
action=env.Run(make_templates),
43+
suffix=".h",
44+
)
45+
46+
# Template files
47+
templates_sources = Glob("template/*") + Glob("template/*/*") + Glob("template/*/*/*")
48+
49+
dest_file = "gdextension_template_files.gen.h"
50+
env.Alias("editor_template_gdext", [env.MakeGDExtTemplateBuilder(dest_file, templates_sources)])
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/**************************************************************************/
2+
/* cpp_scons_gdext_creator.cpp */
3+
/**************************************************************************/
4+
/* This file is part of: */
5+
/* GODOT ENGINE */
6+
/* https://godotengine.org */
7+
/**************************************************************************/
8+
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9+
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10+
/* */
11+
/* Permission is hereby granted, free of charge, to any person obtaining */
12+
/* a copy of this software and associated documentation files (the */
13+
/* "Software"), to deal in the Software without restriction, including */
14+
/* without limitation the rights to use, copy, modify, merge, publish, */
15+
/* distribute, sublicense, and/or sell copies of the Software, and to */
16+
/* permit persons to whom the Software is furnished to do so, subject to */
17+
/* the following conditions: */
18+
/* */
19+
/* The above copyright notice and this permission notice shall be */
20+
/* included in all copies or substantial portions of the Software. */
21+
/* */
22+
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23+
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24+
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25+
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26+
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27+
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28+
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29+
/**************************************************************************/
30+
31+
#include "cpp_scons_gdext_creator.h"
32+
33+
#include "core/core_bind.h"
34+
#include "core/io/dir_access.h"
35+
#include "core/string/string_builder.h"
36+
#include "core/version.h"
37+
#include "gdextension_template_files.gen.h"
38+
39+
#include "editor/editor_node.h"
40+
41+
void CppSconsGDExtensionCreator::_git_clone_godot_cpp(const String &p_parent_path, bool p_compile) {
42+
EditorProgress ep("Preparing GDExtension C++ plugin", "Preparing GDExtension C++ plugin", 3);
43+
List<String> args = { "clone", "--single-branch", "--branch", VERSION_BRANCH, "https://github.com/godotengine/godot-cpp" };
44+
const String godot_cpp_path = p_parent_path.trim_prefix("res://").path_join("godot-cpp");
45+
args.push_back(godot_cpp_path);
46+
ep.step(TTR("Cloning godot-cpp..."), 1);
47+
String output = "";
48+
int result = OS::get_singleton()->execute("git", args, &output);
49+
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_RESOURCES);
50+
if (result != 0 || !dir->dir_exists(godot_cpp_path)) {
51+
args.get(3) = "master";
52+
output = "";
53+
result = OS::get_singleton()->execute("git", args, &output);
54+
}
55+
ERR_FAIL_COND_MSG(result != 0 || !dir->dir_exists(godot_cpp_path), "Failed to clone godot-cpp. Please clone godot-cpp manually in order to have a working GDExtension plugin.");
56+
if (p_compile) {
57+
ep.step(TTR("Performing initial compile... (this may take several minutes)"), 2);
58+
result = OS::get_singleton()->execute("scons", List<String>());
59+
ERR_FAIL_COND_MSG(result != 0, "Failed to compile godot-cpp. Please ensure SCons is installed, then run the `scons` command in your project.");
60+
}
61+
ep.step(TTR("Done!"), 3);
62+
}
63+
64+
String CppSconsGDExtensionCreator::_process_template(const String &p_contents) {
65+
String ret;
66+
if (strip_module_defines) {
67+
StringBuilder builder;
68+
bool keep = true;
69+
PackedStringArray lines = p_contents.split("\n");
70+
for (const String &line : lines) {
71+
if (line == "#if GDEXTENSION" || line == "#else") {
72+
continue;
73+
} else if (line == "#elif GODOT_MODULE") {
74+
keep = false;
75+
continue;
76+
} else if (line == "#endif") {
77+
keep = true;
78+
continue;
79+
}
80+
if (keep) {
81+
builder += line;
82+
builder += "\n";
83+
}
84+
}
85+
ret = builder.as_string();
86+
} else {
87+
ret = p_contents;
88+
}
89+
if (ClassDB::class_exists("ExampleNode")) {
90+
ret = ret.replace("ExampleNode", example_node_name);
91+
}
92+
ret = ret.replace("__BASE_NAME__", base_name);
93+
ret = ret.replace("__BASE_NAME_UPPER__", base_name.to_upper());
94+
ret = ret.replace("__LIBRARY_NAME__", library_name);
95+
ret = ret.replace("__LIBRARY_NAME_UPPER__", library_name.to_upper());
96+
ret = ret.replace("__GODOT_VERSION__", VERSION_BRANCH);
97+
ret = ret.replace("__BASE_PATH__", res_path.trim_prefix("res://"));
98+
ret = ret.replace("__UPDIR_DOTS__", updir_dots);
99+
return ret;
100+
}
101+
102+
void CppSconsGDExtensionCreator::_write_file(const String &p_file_path, const String &p_contents) {
103+
Error err;
104+
Ref<FileAccess> file = FileAccess::open(p_file_path, FileAccess::WRITE, &err);
105+
ERR_FAIL_COND_MSG(err != OK, "Couldn't write file at path: " + p_file_path + ".");
106+
file->store_string(_process_template(p_contents));
107+
file->close();
108+
}
109+
110+
void CppSconsGDExtensionCreator::_ensure_file_contains(const String &p_file_path, const String &p_new_contents) {
111+
Error err;
112+
Ref<FileAccess> file = FileAccess::open(p_file_path, FileAccess::READ_WRITE, &err);
113+
if (err != OK) {
114+
_write_file(p_file_path, p_new_contents);
115+
return;
116+
}
117+
String new_contents = _process_template(p_new_contents);
118+
String existing_contents = file->get_as_text();
119+
if (existing_contents.is_empty()) {
120+
file->store_string(new_contents);
121+
} else {
122+
file->seek_end();
123+
PackedStringArray lines = new_contents.split("\n", false);
124+
for (const String &line : lines) {
125+
if (!existing_contents.contains(line)) {
126+
file->store_string(line + "\n");
127+
}
128+
}
129+
}
130+
file->close();
131+
}
132+
133+
void CppSconsGDExtensionCreator::_write_common_files_and_dirs() {
134+
DirAccess::make_dir_recursive_absolute(res_path.path_join("doc_classes"));
135+
DirAccess::make_dir_recursive_absolute(res_path.path_join("icons"));
136+
DirAccess::make_dir_recursive_absolute(res_path.path_join("src"));
137+
_ensure_file_contains("res://SConstruct", SCONSTRUCT_TOP_LEVEL);
138+
_write_file(res_path.path_join("doc_classes/" + example_node_name + ".xml"), EXAMPLENODE_XML);
139+
_write_file(res_path.path_join("icons/" + example_node_name + ".svg"), EXAMPLENODE_SVG);
140+
_write_file(res_path.path_join("icons/" + example_node_name + ".svg.import"), EXAMPLENODE_SVG_IMPORT);
141+
_write_file(res_path.path_join("src/.gdignore"), "");
142+
_write_file(res_path.path_join(".gitignore"), GDEXT_GITIGNORE + "\n*.obj");
143+
_write_file(res_path.path_join(library_name + ".gdextension"), LIBRARY_NAME_GDEXTENSION);
144+
}
145+
146+
void CppSconsGDExtensionCreator::_write_gdext_only_files() {
147+
_ensure_file_contains("res://.gitignore", "*.dblite");
148+
_write_file(res_path.path_join("src/example_node.cpp"), EXAMPLE_NODE_CPP);
149+
_write_file(res_path.path_join("src/example_node.h"), EXAMPLE_NODE_H);
150+
_write_file(res_path.path_join("src/register_types.cpp"), REGISTER_TYPES_CPP);
151+
_write_file(res_path.path_join("src/register_types.h"), REGISTER_TYPES_H);
152+
_write_file(res_path.path_join("src/" + library_name + "_defines.h"), GDEXT_DEFINES_H);
153+
_write_file(res_path.path_join("src/initialize_gdextension.cpp"), INITIALIZE_GDEXTENSION_CPP.replace("#include \"__UPDIR_DOTS__/../", "#include \""));
154+
_write_file(res_path.path_join("SConstruct"), SCONSTRUCT_ADDON.replace(" + Glob(\"__UPDIR_DOTS__/*.cpp\")", "").replace(", \"__UPDIR_DOTS__/\"", "").replace("__UPDIR_DOTS__/editor", "src/editor"));
155+
}
156+
157+
void CppSconsGDExtensionCreator::_write_gdext_module_files() {
158+
_ensure_file_contains("res://.gitignore", GDEXT_GITIGNORE);
159+
DirAccess::make_dir_recursive_absolute("res://tests/nodes");
160+
_write_file("res://SCsub", SCSUB);
161+
_write_file("res://config.py", CONFIG_PY);
162+
_write_file("res://example_node.cpp", EXAMPLE_NODE_CPP);
163+
_write_file("res://example_node.h", EXAMPLE_NODE_H);
164+
_write_file("res://register_types.cpp", REGISTER_TYPES_CPP);
165+
_write_file("res://register_types.h", REGISTER_TYPES_H);
166+
_write_file("res://" + library_name + "_defines.h", SHARED_DEFINES_H);
167+
_write_file("res://tests/test_" + base_name + ".h", TEST_BASE_NAME_H);
168+
_write_file("res://tests/nodes/test_example_node.h", TEST_EXAMPLE_NODE_H);
169+
_write_file(res_path.path_join("src/initialize_gdextension.cpp"), INITIALIZE_GDEXTENSION_CPP);
170+
_write_file(res_path.path_join("SConstruct"), SCONSTRUCT_ADDON);
171+
}
172+
173+
void CppSconsGDExtensionCreator::create_gdextension(const String &p_path, const String &p_base_name, const String &p_library_name, int p_variation_index, bool p_compile) {
174+
res_path = p_path;
175+
base_name = p_base_name;
176+
library_name = p_library_name;
177+
updir_dots = String("../").repeat(p_path.count("/", 6)) + "..";
178+
strip_module_defines = p_variation_index == LANG_VAR_GDEXT_ONLY;
179+
if (ClassDB::class_exists("ExampleNode")) {
180+
int discriminator = 2;
181+
example_node_name = "ExampleNode2";
182+
while (ClassDB::class_exists(example_node_name)) {
183+
discriminator++;
184+
example_node_name = "ExampleNode" + itos(discriminator);
185+
}
186+
}
187+
_write_common_files_and_dirs();
188+
if (p_variation_index == LANG_VAR_GDEXT_ONLY) {
189+
_write_gdext_only_files();
190+
} else {
191+
_write_gdext_module_files();
192+
}
193+
if (does_git_exist) {
194+
_git_clone_godot_cpp(p_path.path_join("src"), p_compile);
195+
}
196+
}
197+
198+
void CppSconsGDExtensionCreator::setup_creator() {
199+
// Check for Git and SCons.
200+
List<String> args;
201+
args.push_back("--version");
202+
String output;
203+
OS::get_singleton()->execute("git", args, &output);
204+
if (output.is_empty()) {
205+
does_git_exist = false;
206+
} else {
207+
does_git_exist = true;
208+
output = "";
209+
OS::get_singleton()->execute("scons", args, &output);
210+
does_scons_exist = !output.is_empty();
211+
}
212+
}
213+
214+
PackedStringArray CppSconsGDExtensionCreator::get_language_variations() const {
215+
PackedStringArray variants;
216+
// Keep this in sync with enum LanguageVariation.
217+
variants.push_back("C++ with SCons, GDExtension only");
218+
variants.push_back("C++ with SCons, GDExtension and engine module");
219+
return variants;
220+
}
221+
222+
Dictionary CppSconsGDExtensionCreator::get_validation_messages(const String &p_path, const String &p_base_name, const String &p_library_name, int p_variation_index, bool p_compile) {
223+
Dictionary messages;
224+
// Check for Git and SCons.
225+
MessageType compile_consequence = p_compile ? MSG_ERROR : MSG_WARNING;
226+
if (does_git_exist) {
227+
if (does_scons_exist) {
228+
#ifdef WINDOWS_ENABLED
229+
messages[TTR("Both Git and SCons were found. You also need a C++17-compatible compiler, such as GCC, Clang/LLVM, or MSVC from Visual Studio.")] = MSG_OK;
230+
#else
231+
messages[TTR("Both Git and SCons were found. You also need a C++17-compatible compiler, such as GCC or Clang/LLVM.")] = MSG_OK;
232+
#endif
233+
} else {
234+
messages[TTR("Cannot compile now, SCons was not found.")] = compile_consequence;
235+
}
236+
} else {
237+
messages[TTR("Cannot compile now, Git was not found.")] = compile_consequence;
238+
}
239+
// Check for existing engine module.
240+
if (p_variation_index == LANG_VAR_GDEXT_MODULE) {
241+
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_RESOURCES);
242+
if (dir->file_exists("SCsub")) {
243+
messages[TTR("This project already contains a C++ engine module.")] = MSG_ERROR;
244+
} else {
245+
messages[TTR("Able to create engine module in this Godot project. Note that the base name should match the project's folder name when used as a module.")] = MSG_OK;
246+
messages[TTR("Warning: This will turn the root of your project into an engine module!")] = MSG_WARNING;
247+
}
248+
}
249+
return messages;
250+
}

0 commit comments

Comments
 (0)