Skip to content

Commit f9f4469

Browse files
committed
Allow creating GDExtension plugins from inside the Godot editor
1 parent b5bdb88 commit f9f4469

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1669
-6
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
@@ -79,6 +79,7 @@ class EditorValidationPanel : public PanelContainer {
7979

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

8485
EditorValidationPanel();

editor/icons/LicensesDialog.svg

Lines changed: 1 addition & 1 deletion
Loading

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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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("#pragma once\n\n")
28+
s.write('#include "core/string/ustring.h"\n')
29+
parsed_template_string = ""
30+
for file in source:
31+
filepath = str(file)
32+
if os.path.isfile(filepath):
33+
parsed_template_string += parse_template(filepath)
34+
s.write(parsed_template_string)
35+
s.write("\n")
36+
with open(dst, "w", encoding="utf-8", newline="\n") as f:
37+
f.write(s.getvalue())
38+
39+
40+
env["BUILDERS"]["MakeGDExtTemplateBuilder"] = Builder(
41+
action=env.Run(make_templates),
42+
suffix=".h",
43+
)
44+
45+
# Template files
46+
templates_sources = Glob("template/*") + Glob("template/*/*") + Glob("template/*/*/*")
47+
48+
dest_file = "gdextension_template_files.gen.h"
49+
env.Alias("editor_template_gdext", [env.MakeGDExtTemplateBuilder(dest_file, templates_sources)])
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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+
if (!ret.ends_with("\n")) {
100+
ret = ret + "\n";
101+
}
102+
return ret;
103+
}
104+
105+
void CppSconsGDExtensionCreator::_write_file(const String &p_file_path, const String &p_contents) {
106+
Error err;
107+
Ref<FileAccess> file = FileAccess::open(p_file_path, FileAccess::WRITE, &err);
108+
ERR_FAIL_COND_MSG(err != OK, "Couldn't write file at path: " + p_file_path + ".");
109+
file->store_string(_process_template(p_contents));
110+
file->close();
111+
}
112+
113+
void CppSconsGDExtensionCreator::_ensure_file_contains(const String &p_file_path, const String &p_new_contents) {
114+
Error err;
115+
Ref<FileAccess> file = FileAccess::open(p_file_path, FileAccess::READ_WRITE, &err);
116+
if (err != OK) {
117+
_write_file(p_file_path, p_new_contents);
118+
return;
119+
}
120+
String new_contents = _process_template(p_new_contents);
121+
String existing_contents = file->get_as_text();
122+
if (existing_contents.is_empty()) {
123+
file->store_string(new_contents);
124+
} else {
125+
file->seek_end();
126+
PackedStringArray lines = new_contents.split("\n", false);
127+
for (const String &line : lines) {
128+
if (!existing_contents.contains(line)) {
129+
file->store_string(line + "\n");
130+
}
131+
}
132+
}
133+
file->close();
134+
}
135+
136+
void CppSconsGDExtensionCreator::_write_common_files_and_dirs() {
137+
DirAccess::make_dir_recursive_absolute(res_path.path_join("doc_classes"));
138+
DirAccess::make_dir_recursive_absolute(res_path.path_join("icons"));
139+
DirAccess::make_dir_recursive_absolute(res_path.path_join("src"));
140+
_ensure_file_contains("res://SConstruct", SCONSTRUCT_TOP_LEVEL);
141+
_write_file(res_path.path_join("doc_classes/" + example_node_name + ".xml"), EXAMPLENODE_XML);
142+
_write_file(res_path.path_join("icons/" + example_node_name + ".svg"), EXAMPLENODE_SVG);
143+
_write_file(res_path.path_join("icons/" + example_node_name + ".svg.import"), EXAMPLENODE_SVG_IMPORT);
144+
_write_file(res_path.path_join("src/.gdignore"), "");
145+
_write_file(res_path.path_join(".gitignore"), GDEXT_GITIGNORE + "\n*.obj");
146+
_write_file(res_path.path_join(library_name + ".gdextension"), LIBRARY_NAME_GDEXTENSION);
147+
}
148+
149+
void CppSconsGDExtensionCreator::_write_gdext_only_files() {
150+
_ensure_file_contains("res://.gitignore", "*.dblite");
151+
_write_file(res_path.path_join("src/example_node.cpp"), EXAMPLE_NODE_CPP);
152+
_write_file(res_path.path_join("src/example_node.h"), EXAMPLE_NODE_H);
153+
_write_file(res_path.path_join("src/register_types.cpp"), REGISTER_TYPES_CPP);
154+
_write_file(res_path.path_join("src/register_types.h"), REGISTER_TYPES_H);
155+
_write_file(res_path.path_join("src/" + library_name + "_defines.h"), GDEXT_DEFINES_H);
156+
_write_file(res_path.path_join("src/initialize_gdextension.cpp"), INITIALIZE_GDEXTENSION_CPP.replace("#include \"__UPDIR_DOTS__/../", "#include \""));
157+
_write_file(res_path.path_join("SConstruct"), SCONSTRUCT_ADDON.replace(" + Glob(\"__UPDIR_DOTS__/*.cpp\")", "").replace(", \"__UPDIR_DOTS__/\"", "").replace("__UPDIR_DOTS__/editor", "src/editor"));
158+
}
159+
160+
void CppSconsGDExtensionCreator::_write_gdext_module_files() {
161+
_ensure_file_contains("res://.gitignore", GDEXT_GITIGNORE);
162+
DirAccess::make_dir_recursive_absolute("res://tests/nodes");
163+
_write_file("res://SCsub", SCSUB);
164+
_write_file("res://config.py", CONFIG_PY);
165+
_write_file("res://example_node.cpp", EXAMPLE_NODE_CPP);
166+
_write_file("res://example_node.h", EXAMPLE_NODE_H);
167+
_write_file("res://register_types.cpp", REGISTER_TYPES_CPP);
168+
_write_file("res://register_types.h", REGISTER_TYPES_H);
169+
_write_file("res://" + library_name + "_defines.h", SHARED_DEFINES_H);
170+
_write_file("res://tests/test_" + base_name + ".h", TEST_BASE_NAME_H);
171+
_write_file("res://tests/nodes/test_example_node.h", TEST_EXAMPLE_NODE_H);
172+
_write_file(res_path.path_join("src/initialize_gdextension.cpp"), INITIALIZE_GDEXTENSION_CPP);
173+
_write_file(res_path.path_join("SConstruct"), SCONSTRUCT_ADDON);
174+
}
175+
176+
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) {
177+
res_path = p_path;
178+
base_name = p_base_name;
179+
library_name = p_library_name;
180+
updir_dots = String("../").repeat(p_path.count("/", 6)) + "..";
181+
strip_module_defines = p_variation_index == LANG_VAR_GDEXT_ONLY;
182+
if (ClassDB::class_exists("ExampleNode")) {
183+
int discriminator = 2;
184+
example_node_name = "ExampleNode2";
185+
while (ClassDB::class_exists(example_node_name)) {
186+
discriminator++;
187+
example_node_name = "ExampleNode" + itos(discriminator);
188+
}
189+
}
190+
_write_common_files_and_dirs();
191+
if (p_variation_index == LANG_VAR_GDEXT_ONLY) {
192+
_write_gdext_only_files();
193+
} else {
194+
_write_gdext_module_files();
195+
}
196+
if (does_git_exist) {
197+
_git_clone_godot_cpp(p_path.path_join("src"), p_compile);
198+
}
199+
}
200+
201+
void CppSconsGDExtensionCreator::setup_creator() {
202+
// Check for Git and SCons.
203+
List<String> args;
204+
args.push_back("--version");
205+
String output;
206+
OS::get_singleton()->execute("git", args, &output);
207+
if (output.is_empty()) {
208+
does_git_exist = false;
209+
} else {
210+
does_git_exist = true;
211+
output = "";
212+
OS::get_singleton()->execute("scons", args, &output);
213+
does_scons_exist = !output.is_empty();
214+
}
215+
}
216+
217+
PackedStringArray CppSconsGDExtensionCreator::get_language_variations() const {
218+
PackedStringArray variants;
219+
// Keep this in sync with enum LanguageVariation.
220+
variants.push_back("C++ with SCons, GDExtension only");
221+
variants.push_back("C++ with SCons, GDExtension and engine module");
222+
return variants;
223+
}
224+
225+
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) {
226+
Dictionary messages;
227+
// Check for Git and SCons.
228+
MessageType compile_consequence = p_compile ? MSG_ERROR : MSG_WARNING;
229+
if (does_git_exist) {
230+
if (does_scons_exist) {
231+
#ifdef WINDOWS_ENABLED
232+
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;
233+
#else
234+
messages[TTR("Both Git and SCons were found. You also need a C++17-compatible compiler, such as GCC or Clang/LLVM.")] = MSG_OK;
235+
#endif
236+
} else {
237+
messages[TTR("Cannot compile now, SCons was not found.")] = compile_consequence;
238+
}
239+
} else {
240+
messages[TTR("Cannot compile now, Git was not found.")] = compile_consequence;
241+
}
242+
// Check for existing engine module.
243+
if (p_variation_index == LANG_VAR_GDEXT_MODULE) {
244+
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_RESOURCES);
245+
if (dir->file_exists("SCsub")) {
246+
messages[TTR("This project already contains a C++ engine module.")] = MSG_ERROR;
247+
} else {
248+
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;
249+
messages[TTR("Warning: This will turn the root of your project into an engine module!")] = MSG_WARNING;
250+
}
251+
}
252+
return messages;
253+
}

0 commit comments

Comments
 (0)