summaryrefslogtreecommitdiff
path: root/platform
diff options
context:
space:
mode:
Diffstat (limited to 'platform')
-rw-r--r--platform/javascript/SCsub62
-rw-r--r--platform/javascript/detect.py25
-rw-r--r--platform/javascript/export/export.cpp106
-rw-r--r--platform/javascript/godot_audio.h2
-rw-r--r--platform/javascript/http_request.h2
-rw-r--r--platform/javascript/javascript_main.cpp2
-rw-r--r--platform/javascript/javascript_runtime.cpp35
-rw-r--r--platform/javascript/js/dynlink.pre.js1
-rw-r--r--platform/javascript/js/engine/engine.js13
-rw-r--r--platform/javascript/js/engine/utils.js2
-rw-r--r--platform/javascript/js/libs/library_godot_audio.js12
-rw-r--r--platform/javascript/js/libs/library_godot_display.js19
-rw-r--r--platform/javascript/js/libs/library_godot_editor_tools.js1
-rw-r--r--platform/javascript/js/libs/library_godot_eval.js1
-rw-r--r--platform/javascript/js/libs/library_godot_http_request.js15
-rw-r--r--platform/javascript/js/libs/library_godot_os.js9
-rw-r--r--platform/javascript/os_javascript.cpp24
-rw-r--r--platform/javascript/os_javascript.h1
-rw-r--r--platform/linuxbsd/os_linuxbsd.cpp146
19 files changed, 391 insertions, 87 deletions
diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub
index 627ae778b1..59f3dce3ad 100644
--- a/platform/javascript/SCsub
+++ b/platform/javascript/SCsub
@@ -12,13 +12,8 @@ javascript_files = [
"api/javascript_tools_editor_plugin.cpp",
]
-build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"]
-if env["threads_enabled"]:
- build_targets.append("#bin/godot${PROGSUFFIX}.worker.js")
-
-build = env.add_program(build_targets, javascript_files)
-
-env.AddJSLibraries(
+sys_env = env.Clone()
+sys_env.AddJSLibraries(
[
"js/libs/library_godot_audio.js",
"js/libs/library_godot_display.js",
@@ -29,12 +24,48 @@ env.AddJSLibraries(
)
if env["tools"]:
- env.AddJSLibraries(["js/libs/library_godot_editor_tools.js"])
+ sys_env.AddJSLibraries(["js/libs/library_godot_editor_tools.js"])
if env["javascript_eval"]:
- env.AddJSLibraries(["js/libs/library_godot_eval.js"])
-for lib in env["JS_LIBS"]:
- env.Append(LINKFLAGS=["--js-library", lib])
-env.Depends(build, env["JS_LIBS"])
+ sys_env.AddJSLibraries(["js/libs/library_godot_eval.js"])
+for lib in sys_env["JS_LIBS"]:
+ sys_env.Append(LINKFLAGS=["--js-library", lib])
+
+build = []
+if env["gdnative_enabled"]:
+ build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"]
+ # Reset libraries. The main runtime will only link emscripten libraries, not godot ones.
+ sys_env["LIBS"] = []
+ # We use IDBFS. Since Emscripten 1.39.1 it needs to be linked explicitly.
+ sys_env.Append(LIBS=["idbfs.js"])
+ # JS prepended to the module code loading the side library.
+ sys_env.Append(LINKFLAGS=["--pre-js", sys_env.File("js/dynlink.pre.js")])
+ # Configure it as a main module (dynamic linking support).
+ sys_env.Append(CCFLAGS=["-s", "MAIN_MODULE=1"])
+ sys_env.Append(LINKFLAGS=["-s", "MAIN_MODULE=1"])
+ sys_env.Append(CCFLAGS=["-s", "EXPORT_ALL=1"])
+ sys_env.Append(LINKFLAGS=["-s", "EXPORT_ALL=1"])
+ # Force exporting the standard library (printf, malloc, etc.)
+ sys_env["ENV"]["EMCC_FORCE_STDLIBS"] = "libc,libc++,libc++abi"
+ # The main emscripten runtime, with exported standard libraries.
+ sys = sys_env.Program(build_targets, ["javascript_runtime.cpp"])
+ sys_env.Depends(sys, "js/dynlink.pre.js")
+
+ # The side library, containing all Godot code.
+ wasm_env = env.Clone()
+ wasm_env.Append(CPPDEFINES=["WASM_GDNATIVE"]) # So that OS knows it can run GDNative libraries.
+ wasm_env.Append(CCFLAGS=["-s", "SIDE_MODULE=2"])
+ wasm_env.Append(LINKFLAGS=["-s", "SIDE_MODULE=2"])
+ wasm = wasm_env.add_program("#bin/godot.side${PROGSUFFIX}.wasm", javascript_files)
+ build = [sys[0], sys[1], wasm[0]]
+else:
+ build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"]
+ if env["threads_enabled"]:
+ build_targets.append("#bin/godot${PROGSUFFIX}.worker.js")
+ # We use IDBFS. Since Emscripten 1.39.1 it needs to be linked explicitly.
+ sys_env.Append(LIBS=["idbfs.js"])
+ build = sys_env.Program(build_targets, javascript_files + ["javascript_runtime.cpp"])
+
+sys_env.Depends(build[0], sys_env["JS_LIBS"])
engine = [
"js/engine/preloader.js",
@@ -61,8 +92,11 @@ out_files = [
]
html_file = "#misc/dist/html/editor.html" if env["tools"] else "#misc/dist/html/full-size.html"
in_files = [js_wrapped, build[1], html_file, "#platform/javascript/js/libs/audio.worklet.js"]
-if env["threads_enabled"]:
- in_files.append(build[2])
+if env["gdnative_enabled"]:
+ in_files.append(build[2]) # Runtime
+ out_files.append(zip_dir.File(binary_name + ".side.wasm"))
+elif env["threads_enabled"]:
+ in_files.append(build[2]) # Worker
out_files.append(zip_dir.File(binary_name + ".worker.js"))
zip_files = env.InstallAs(out_files, in_files)
diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py
index 9f584d0899..f4fa5fb218 100644
--- a/platform/javascript/detect.py
+++ b/platform/javascript/detect.py
@@ -23,6 +23,7 @@ def get_opts():
# eval() can be a security concern, so it can be disabled.
BoolVariable("javascript_eval", "Enable JavaScript eval interface", True),
BoolVariable("threads_enabled", "Enable WebAssembly Threads support (limited browser support)", False),
+ BoolVariable("gdnative_enabled", "Enable WebAssembly GDNative support (produces bigger binaries)", False),
BoolVariable("use_closure_compiler", "Use closure compiler to minimize JavaScript code", False),
]
@@ -85,10 +86,8 @@ def configure(env):
# LTO
if env["use_lto"]:
- env.Append(CCFLAGS=["-s", "WASM_OBJECT_FILES=0"])
- env.Append(LINKFLAGS=["-s", "WASM_OBJECT_FILES=0"])
- env.Append(CCFLAGS=["-flto"])
- env.Append(LINKFLAGS=["-flto"])
+ env.Append(CCFLAGS=["-flto=full"])
+ env.Append(LINKFLAGS=["-flto=full"])
# Closure compiler
if env["use_closure_compiler"]:
@@ -135,6 +134,9 @@ def configure(env):
if env["javascript_eval"]:
env.Append(CPPDEFINES=["JAVASCRIPT_EVAL_ENABLED"])
+ if env["threads_enabled"] and env["gdnative_enabled"]:
+ raise Exception("Threads and GDNative support can't be both enabled due to WebAssembly limitations")
+
# Thread support (via SharedArrayBuffer).
if env["threads_enabled"]:
env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"])
@@ -146,14 +148,15 @@ def configure(env):
else:
env.Append(CPPDEFINES=["NO_THREADS"])
+ if env["gdnative_enabled"]:
+ env.Append(CCFLAGS=["-s", "RELOCATABLE=1"])
+ env.Append(LINKFLAGS=["-s", "RELOCATABLE=1"])
+ env.extra_suffix = ".gdnative" + env.extra_suffix
+
# Reduce code size by generating less support code (e.g. skip NodeJS support).
env.Append(LINKFLAGS=["-s", "ENVIRONMENT=web,worker"])
- # We use IDBFS in javascript_main.cpp. Since Emscripten 1.39.1 it needs to
- # be linked explicitly.
- env.Append(LIBS=["idbfs.js"])
-
- env.Append(LINKFLAGS=["-s", "BINARYEN=1"])
+ # Wrap the JavaScript support code around a closure named Godot.
env.Append(LINKFLAGS=["-s", "MODULARIZE=1", "-s", "EXPORT_NAME='Godot'"])
# Allow increasing memory buffer size during runtime. This is efficient
@@ -164,12 +167,14 @@ def configure(env):
# This setting just makes WebGL 2 APIs available, it does NOT disable WebGL 1.
env.Append(LINKFLAGS=["-s", "USE_WEBGL2=1"])
+ # Do not call main immediately when the support code is ready.
env.Append(LINKFLAGS=["-s", "INVOKE_RUN=0"])
# Allow use to take control of swapping WebGL buffers.
env.Append(LINKFLAGS=["-s", "OFFSCREEN_FRAMEBUFFER=1"])
- # callMain for manual start, FS for preloading, PATH and ERRNO_CODES for BrowserFS.
+ # callMain for manual start.
env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain']"])
+
# Add code that allow exiting runtime.
env.Append(LINKFLAGS=["-s", "EXIT_RUNTIME=1"])
diff --git a/platform/javascript/export/export.cpp b/platform/javascript/export/export.cpp
index c3b7e0304e..f77bf6ab97 100644
--- a/platform/javascript/export/export.cpp
+++ b/platform/javascript/export/export.cpp
@@ -85,36 +85,44 @@ public:
// Wrong protocol
ERR_FAIL_COND_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", "Invalid method or HTTP version.");
- String filepath = EditorSettings::get_singleton()->get_cache_dir().plus_file("tmp_js_export");
+ const String cache_path = EditorSettings::get_singleton()->get_cache_dir();
const String basereq = "/tmp_js_export";
- String ctype = "";
+ String filepath;
+ String ctype;
if (req[1] == basereq + ".html") {
- filepath += ".html";
+ filepath = cache_path.plus_file(req[1].get_file());
ctype = "text/html";
} else if (req[1] == basereq + ".js") {
- filepath += ".js";
+ filepath = cache_path.plus_file(req[1].get_file());
ctype = "application/javascript";
} else if (req[1] == basereq + ".audio.worklet.js") {
- filepath += ".audio.worklet.js";
+ filepath = cache_path.plus_file(req[1].get_file());
ctype = "application/javascript";
} else if (req[1] == basereq + ".worker.js") {
- filepath += ".worker.js";
+ filepath = cache_path.plus_file(req[1].get_file());
ctype = "application/javascript";
} else if (req[1] == basereq + ".pck") {
- filepath += ".pck";
+ filepath = cache_path.plus_file(req[1].get_file());
ctype = "application/octet-stream";
} else if (req[1] == basereq + ".png" || req[1] == "/favicon.png") {
// Also allow serving the generated favicon for a smoother loading experience.
if (req[1] == "/favicon.png") {
filepath = EditorSettings::get_singleton()->get_cache_dir().plus_file("favicon.png");
} else {
- filepath += ".png";
+ filepath = basereq + ".png";
}
ctype = "image/png";
+ } else if (req[1] == basereq + ".side.wasm") {
+ filepath = cache_path.plus_file(req[1].get_file());
+ ctype = "application/wasm";
} else if (req[1] == basereq + ".wasm") {
- filepath += ".wasm";
+ filepath = cache_path.plus_file(req[1].get_file());
ctype = "application/wasm";
- } else {
+ } else if (req[1].ends_with(".wasm")) {
+ filepath = cache_path.plus_file(req[1].get_file()); // TODO dangerous?
+ ctype = "application/wasm";
+ }
+ if (filepath.empty() || !FileAccess::exists(filepath)) {
String s = "HTTP/1.1 404 Not Found\r\n";
s += "Connection: Close\r\n";
s += "\r\n";
@@ -205,7 +213,33 @@ class EditorExportPlatformJavaScript : public EditorExportPlatform {
Ref<ImageTexture> stop_icon;
int menu_options;
- void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags);
+ enum ExportMode {
+ EXPORT_MODE_NORMAL = 0,
+ EXPORT_MODE_THREADS = 1,
+ EXPORT_MODE_GDNATIVE = 2,
+ };
+
+ String _get_template_name(ExportMode p_mode, bool p_debug) const {
+ String name = "webassembly";
+ switch (p_mode) {
+ case EXPORT_MODE_THREADS:
+ name += "_threads";
+ break;
+ case EXPORT_MODE_GDNATIVE:
+ name += "_gdnative";
+ break;
+ default:
+ break;
+ }
+ if (p_debug) {
+ name += "_debug.zip";
+ } else {
+ name += "_release.zip";
+ }
+ return name;
+ }
+
+ void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects);
private:
Ref<EditorHTTPServer> server;
@@ -250,7 +284,7 @@ public:
~EditorExportPlatformJavaScript();
};
-void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags) {
+void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects) {
String str_template = String::utf8(reinterpret_cast<const char *>(p_html.ptr()), p_html.size());
String str_export;
Vector<String> lines = str_template.split("\n");
@@ -258,6 +292,10 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re
String flags_json;
gen_export_flags(flags, p_flags);
flags_json = JSON::print(flags);
+ String libs;
+ for (int i = 0; i < p_shared_objects.size(); i++) {
+ libs += "\"" + p_shared_objects[i].path.get_file() + "\",";
+ }
for (int i = 0; i < lines.size(); i++) {
String current_line = lines[i];
@@ -265,6 +303,7 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re
current_line = current_line.replace("$GODOT_PROJECT_NAME", ProjectSettings::get_singleton()->get_setting("application/config/name"));
current_line = current_line.replace("$GODOT_HEAD_INCLUDE", p_preset->get("html/head_include"));
current_line = current_line.replace("$GODOT_FULL_WINDOW", p_preset->get("html/full_window_size") ? "true" : "false");
+ current_line = current_line.replace("$GODOT_GDNATIVE_LIBS", libs);
current_line = current_line.replace("$GODOT_DEBUG_ENABLED", p_debug ? "true" : "false");
current_line = current_line.replace("$GODOT_ARGS", flags_json);
str_export += current_line + "\n";
@@ -291,12 +330,19 @@ void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportP
r_features->push_back("etc2");
}
}
+ ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
+ if (mode == EXPORT_MODE_THREADS) {
+ r_features->push_back("threads");
+ } else if (mode == EXPORT_MODE_GDNATIVE) {
+ r_features->push_back("wasm32");
+ }
}
void EditorExportPlatformJavaScript::get_export_options(List<ExportOption> *r_options) {
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "variant/export_type", PROPERTY_HINT_ENUM, "Regular,Threads,GDNative"), 0)); // Export type.
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer
@@ -320,11 +366,11 @@ Ref<Texture2D> EditorExportPlatformJavaScript::get_logo() const {
bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const {
String err;
bool valid = false;
+ ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
// Look for export templates (first official, and if defined custom templates).
-
- bool dvalid = exists_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG, &err);
- bool rvalid = exists_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE, &err);
+ bool dvalid = exists_export_template(_get_template_name(mode, true), &err);
+ bool rvalid = exists_export_template(_get_template_name(mode, false), &err);
if (p_preset->get("custom_template/debug") != "") {
dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
@@ -377,11 +423,8 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
template_path = template_path.strip_edges();
if (template_path == String()) {
- if (p_debug) {
- template_path = find_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG);
- } else {
- template_path = find_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE);
- }
+ ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
+ template_path = find_export_template(_get_template_name(mode, p_debug));
}
if (!DirAccess::exists(p_path.get_base_dir())) {
@@ -393,12 +436,24 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
return ERR_FILE_NOT_FOUND;
}
+ Vector<SharedObject> shared_objects;
String pck_path = p_path.get_basename() + ".pck";
- Error error = save_pack(p_preset, pck_path);
+ Error error = save_pack(p_preset, pck_path, &shared_objects);
if (error != OK) {
EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + pck_path);
return error;
}
+ DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ for (int i = 0; i < shared_objects.size(); i++) {
+ String dst = p_path.get_base_dir().plus_file(shared_objects[i].path.get_file());
+ error = da->copy(shared_objects[i].path, dst);
+ if (error != OK) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + shared_objects[i].path.get_file());
+ memdelete(da);
+ return error;
+ }
+ }
+ memdelete(da);
FileAccess *src_f = nullptr;
zlib_filefunc_def io = zipio_create_io_from_file(&src_f);
@@ -437,14 +492,18 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
if (!custom_html.empty()) {
continue;
}
- _fix_html(data, p_preset, p_path.get_file().get_basename(), p_debug, p_flags);
+ _fix_html(data, p_preset, p_path.get_file().get_basename(), p_debug, p_flags, shared_objects);
file = p_path.get_file();
} else if (file == "godot.js") {
file = p_path.get_file().get_basename() + ".js";
+
} else if (file == "godot.worker.js") {
file = p_path.get_file().get_basename() + ".worker.js";
+ } else if (file == "godot.side.wasm") {
+ file = p_path.get_file().get_basename() + ".side.wasm";
+
} else if (file == "godot.audio.worklet.js") {
file = p_path.get_file().get_basename() + ".audio.worklet.js";
@@ -475,7 +534,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
buf.resize(f->get_len());
f->get_buffer(buf.ptrw(), buf.size());
memdelete(f);
- _fix_html(buf, p_preset, p_path.get_file().get_basename(), p_debug, p_flags);
+ _fix_html(buf, p_preset, p_path.get_file().get_basename(), p_debug, p_flags, shared_objects);
f = FileAccess::open(p_path, FileAccess::WRITE);
if (!f) {
@@ -577,6 +636,7 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese
DirAccess::remove_file_or_error(basepath + ".audio.worklet.js");
DirAccess::remove_file_or_error(basepath + ".pck");
DirAccess::remove_file_or_error(basepath + ".png");
+ DirAccess::remove_file_or_error(basepath + ".side.wasm");
DirAccess::remove_file_or_error(basepath + ".wasm");
DirAccess::remove_file_or_error(EditorSettings::get_singleton()->get_cache_dir().plus_file("favicon.png"));
return err;
diff --git a/platform/javascript/godot_audio.h b/platform/javascript/godot_audio.h
index 7ebda3ad39..0ba6849715 100644
--- a/platform/javascript/godot_audio.h
+++ b/platform/javascript/godot_audio.h
@@ -48,7 +48,7 @@ extern void godot_audio_capture_stop();
typedef int32_t GodotAudioState[4];
extern void godot_audio_worklet_create(int p_channels);
extern void godot_audio_worklet_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, GodotAudioState p_state);
-extern void godot_audio_worklet_state_add(GodotAudioState p_state, int p_idx, int p_value);
+extern int godot_audio_worklet_state_add(GodotAudioState p_state, int p_idx, int p_value);
extern int godot_audio_worklet_state_get(GodotAudioState p_state, int p_idx);
extern int godot_audio_worklet_state_wait(int32_t *p_state, int p_idx, int32_t p_expected, int p_timeout);
diff --git a/platform/javascript/http_request.h b/platform/javascript/http_request.h
index 54e98c1927..41e4749216 100644
--- a/platform/javascript/http_request.h
+++ b/platform/javascript/http_request.h
@@ -47,7 +47,7 @@ typedef enum {
extern int godot_xhr_new();
extern void godot_xhr_reset(int p_xhr_id);
-extern bool godot_xhr_free(int p_xhr_id);
+extern void godot_xhr_free(int p_xhr_id);
extern int godot_xhr_open(int p_xhr_id, const char *p_method, const char *p_url, const char *p_user = nullptr, const char *p_password = nullptr);
diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp
index 2d28a63566..b4985a4f36 100644
--- a/platform/javascript/javascript_main.cpp
+++ b/platform/javascript/javascript_main.cpp
@@ -75,7 +75,7 @@ void main_loop_callback() {
}
/// When calling main, it is assumed FS is setup and synced.
-int main(int argc, char *argv[]) {
+extern EMSCRIPTEN_KEEPALIVE int godot_js_main(int argc, char *argv[]) {
os = new OS_JavaScript();
// We must override main when testing is enabled
diff --git a/platform/javascript/javascript_runtime.cpp b/platform/javascript/javascript_runtime.cpp
new file mode 100644
index 0000000000..bfe9fbd1bc
--- /dev/null
+++ b/platform/javascript/javascript_runtime.cpp
@@ -0,0 +1,35 @@
+/*************************************************************************/
+/* javascript_runtime.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+extern int godot_js_main(int argc, char *argv[]);
+
+int main(int argc, char *argv[]) {
+ return godot_js_main(argc, argv);
+}
diff --git a/platform/javascript/js/dynlink.pre.js b/platform/javascript/js/dynlink.pre.js
new file mode 100644
index 0000000000..34bc371ea9
--- /dev/null
+++ b/platform/javascript/js/dynlink.pre.js
@@ -0,0 +1 @@
+Module['dynamicLibraries'] = [Module['thisProgram'] + '.side.wasm'].concat(Module['dynamicLibraries'] ? Module['dynamicLibraries'] : []);
diff --git a/platform/javascript/js/engine/engine.js b/platform/javascript/js/engine/engine.js
index 74153b672a..4b8a7dde69 100644
--- a/platform/javascript/js/engine/engine.js
+++ b/platform/javascript/js/engine/engine.js
@@ -34,6 +34,7 @@ const Engine = (function () {
this.onExecute = null;
this.onExit = null;
this.persistentPaths = ['/userfs'];
+ this.gdnativeLibs = [];
}
Engine.prototype.init = /** @param {string=} basePath */ function (basePath) {
@@ -58,6 +59,10 @@ const Engine = (function () {
initPromise = new Promise(function (resolve, reject) {
config['locateFile'] = Utils.createLocateRewrite(loadPath);
config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise);
+ // Emscripten configuration.
+ config['thisProgram'] = me.executableName;
+ config['noExitRuntime'] = true;
+ config['dynamicLibraries'] = me.gdnativeLibs;
Godot(config).then(function (module) {
module['initFS'](me.persistentPaths).then(function (fs_err) {
me.rtenv = module;
@@ -119,9 +124,6 @@ const Engine = (function () {
locale = navigator.languages ? navigator.languages[0] : navigator.language;
locale = locale.split('.')[0];
}
- // Emscripten configuration.
- me.rtenv['thisProgram'] = me.executableName;
- me.rtenv['noExitRuntime'] = true;
// Godot configuration.
me.rtenv['initConfig']({
'resizeCanvasOnStart': me.resizeCanvasOnStart,
@@ -249,6 +251,10 @@ const Engine = (function () {
this.persistentPaths = persistentPaths;
};
+ Engine.prototype.setGDNativeLibraries = function (gdnativeLibs) {
+ this.gdnativeLibs = gdnativeLibs;
+ };
+
Engine.prototype.requestQuit = function () {
if (this.rtenv) {
this.rtenv['request_quit']();
@@ -277,6 +283,7 @@ const Engine = (function () {
Engine.prototype['setOnExit'] = Engine.prototype.setOnExit;
Engine.prototype['copyToFS'] = Engine.prototype.copyToFS;
Engine.prototype['setPersistentPaths'] = Engine.prototype.setPersistentPaths;
+ Engine.prototype['setGDNativeLibraries'] = Engine.prototype.setGDNativeLibraries;
Engine.prototype['requestQuit'] = Engine.prototype.requestQuit;
return Engine;
}());
diff --git a/platform/javascript/js/engine/utils.js b/platform/javascript/js/engine/utils.js
index d0fca4e1cb..9273bbad42 100644
--- a/platform/javascript/js/engine/utils.js
+++ b/platform/javascript/js/engine/utils.js
@@ -8,6 +8,8 @@ const Utils = { // eslint-disable-line no-unused-vars
return `${execName}.audio.worklet.js`;
} else if (path.endsWith('.js')) {
return `${execName}.js`;
+ } else if (path.endsWith('.side.wasm')) {
+ return `${execName}.side.wasm`;
} else if (path.endsWith('.wasm')) {
return `${execName}.wasm`;
}
diff --git a/platform/javascript/js/libs/library_godot_audio.js b/platform/javascript/js/libs/library_godot_audio.js
index 0c1f477f34..416e987513 100644
--- a/platform/javascript/js/libs/library_godot_audio.js
+++ b/platform/javascript/js/libs/library_godot_audio.js
@@ -137,6 +137,7 @@ const GodotAudio = {
},
},
+ godot_audio_is_available__sig: 'i',
godot_audio_is_available__proxy: 'sync',
godot_audio_is_available: function () {
if (!(window.AudioContext || window.webkitAudioContext)) {
@@ -145,12 +146,14 @@ const GodotAudio = {
return 1;
},
+ godot_audio_init__sig: 'iiiii',
godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) {
const statechange = GodotRuntime.get_func(p_state_change);
const latencyupdate = GodotRuntime.get_func(p_latency_update);
return GodotAudio.init(p_mix_rate, p_latency, statechange, latencyupdate);
},
+ godot_audio_resume__sig: 'v',
godot_audio_resume: function () {
if (GodotAudio.ctx && GodotAudio.ctx.state !== 'running') {
GodotAudio.ctx.resume();
@@ -158,6 +161,7 @@ const GodotAudio = {
},
godot_audio_capture_start__proxy: 'sync',
+ godot_audio_capture_start__sig: 'v',
godot_audio_capture_start: function () {
if (GodotAudio.input) {
return; // Already started.
@@ -168,6 +172,7 @@ const GodotAudio = {
},
godot_audio_capture_stop__proxy: 'sync',
+ godot_audio_capture_stop__sig: 'v',
godot_audio_capture_stop: function () {
if (GodotAudio.input) {
const tracks = GodotAudio.input['mediaStream']['getTracks']();
@@ -241,10 +246,12 @@ const GodotAudioWorklet = {
},
},
+ godot_audio_worklet_create__sig: 'vi',
godot_audio_worklet_create: function (channels) {
GodotAudioWorklet.create(channels);
},
+ godot_audio_worklet_start__sig: 'viiiii',
godot_audio_worklet_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) {
const out_buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);
const in_buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);
@@ -252,15 +259,18 @@ const GodotAudioWorklet = {
GodotAudioWorklet.start(in_buffer, out_buffer, state);
},
+ godot_audio_worklet_state_wait__sig: 'iiii',
godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) {
Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout);
return Atomics.load(HEAP32, (p_state >> 2) + p_idx);
},
+ godot_audio_worklet_state_add__sig: 'iiii',
godot_audio_worklet_state_add: function (p_state, p_idx, p_value) {
return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value);
},
+ godot_audio_worklet_state_get__sig: 'iii',
godot_audio_worklet_state_get: function (p_state, p_idx) {
return Atomics.load(HEAP32, (p_state >> 2) + p_idx);
},
@@ -330,10 +340,12 @@ const GodotAudioScript = {
},
},
+ godot_audio_script_create__sig: 'iii',
godot_audio_script_create: function (buffer_length, channel_count) {
return GodotAudioScript.create(buffer_length, channel_count);
},
+ godot_audio_script_start__sig: 'viiiii',
godot_audio_script_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_cb) {
const onprocess = GodotRuntime.get_func(p_cb);
GodotAudioScript.start(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess);
diff --git a/platform/javascript/js/libs/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js
index 9651b48952..800d6f414f 100644
--- a/platform/javascript/js/libs/library_godot_display.js
+++ b/platform/javascript/js/libs/library_godot_display.js
@@ -280,6 +280,7 @@ const GodotDisplay = {
window_icon: '',
},
+ godot_js_display_is_swap_ok_cancel__sig: 'i',
godot_js_display_is_swap_ok_cancel: function () {
const win = (['Windows', 'Win64', 'Win32', 'WinCE']);
const plat = navigator.platform || '';
@@ -289,10 +290,12 @@ const GodotDisplay = {
return 0;
},
+ godot_js_display_alert__sig: 'vi',
godot_js_display_alert: function (p_text) {
window.alert(GodotRuntime.parseString(p_text)); // eslint-disable-line no-alert
},
+ godot_js_display_pixel_ratio_get__sig: 'f',
godot_js_display_pixel_ratio_get: function () {
return window.devicePixelRatio || 1;
},
@@ -300,14 +303,17 @@ const GodotDisplay = {
/*
* Canvas
*/
+ godot_js_display_canvas_focus__sig: 'v',
godot_js_display_canvas_focus: function () {
GodotConfig.canvas.focus();
},
+ godot_js_display_canvas_is_focused__sig: 'i',
godot_js_display_canvas_is_focused: function () {
return document.activeElement === GodotConfig.canvas;
},
+ godot_js_display_canvas_bounding_rect_position_get__sig: 'vii',
godot_js_display_canvas_bounding_rect_position_get: function (r_x, r_y) {
const brect = GodotConfig.canvas.getBoundingClientRect();
GodotRuntime.setHeapValue(r_x, brect.x, 'i32');
@@ -317,6 +323,7 @@ const GodotDisplay = {
/*
* Touchscreen
*/
+ godot_js_display_touchscreen_is_available__sig: 'i',
godot_js_display_touchscreen_is_available: function () {
return 'ontouchstart' in window;
},
@@ -324,6 +331,7 @@ const GodotDisplay = {
/*
* Clipboard
*/
+ godot_js_display_clipboard_set__sig: 'ii',
godot_js_display_clipboard_set: function (p_text) {
const text = GodotRuntime.parseString(p_text);
if (!navigator.clipboard || !navigator.clipboard.writeText) {
@@ -336,6 +344,7 @@ const GodotDisplay = {
return 0;
},
+ godot_js_display_clipboard_get__sig: 'ii',
godot_js_display_clipboard_get: function (callback) {
const func = GodotRuntime.get_func(callback);
try {
@@ -354,6 +363,7 @@ const GodotDisplay = {
/*
* Window
*/
+ godot_js_display_window_request_fullscreen__sig: 'v',
godot_js_display_window_request_fullscreen: function () {
const canvas = GodotConfig.canvas;
(canvas.requestFullscreen || canvas.msRequestFullscreen
@@ -362,10 +372,12 @@ const GodotDisplay = {
).call(canvas);
},
+ godot_js_display_window_title_set__sig: 'vi',
godot_js_display_window_title_set: function (p_data) {
document.title = GodotRuntime.parseString(p_data);
},
+ godot_js_display_window_icon_set__sig: 'vii',
godot_js_display_window_icon_set: function (p_ptr, p_len) {
let link = document.getElementById('-gd-engine-icon');
if (link === null) {
@@ -386,6 +398,7 @@ const GodotDisplay = {
/*
* Cursor
*/
+ godot_js_display_cursor_set_visible__sig: 'vi',
godot_js_display_cursor_set_visible: function (p_visible) {
const visible = p_visible !== 0;
if (visible === GodotDisplayCursor.visible) {
@@ -399,14 +412,17 @@ const GodotDisplay = {
}
},
+ godot_js_display_cursor_is_hidden__sig: 'i',
godot_js_display_cursor_is_hidden: function () {
return !GodotDisplayCursor.visible;
},
+ godot_js_display_cursor_set_shape__sig: 'vi',
godot_js_display_cursor_set_shape: function (p_string) {
GodotDisplayCursor.set_shape(GodotRuntime.parseString(p_string));
},
+ godot_js_display_cursor_set_custom_shape__sig: 'viiiii',
godot_js_display_cursor_set_custom_shape: function (p_shape, p_ptr, p_len, p_hotspot_x, p_hotspot_y) {
const shape = GodotRuntime.parseString(p_shape);
const old_shape = GodotDisplayCursor.cursors[shape];
@@ -432,6 +448,7 @@ const GodotDisplay = {
/*
* Listeners
*/
+ godot_js_display_notification_cb__sig: 'viiiii',
godot_js_display_notification_cb: function (callback, p_enter, p_exit, p_in, p_out) {
const canvas = GodotConfig.canvas;
const func = GodotRuntime.get_func(callback);
@@ -443,6 +460,7 @@ const GodotDisplay = {
});
},
+ godot_js_display_paste_cb__sig: 'vi',
godot_js_display_paste_cb: function (callback) {
const func = GodotRuntime.get_func(callback);
GodotDisplayListeners.add(window, 'paste', function (evt) {
@@ -453,6 +471,7 @@ const GodotDisplay = {
}, false);
},
+ godot_js_display_drop_files_cb__sig: 'vi',
godot_js_display_drop_files_cb: function (callback) {
const func = GodotRuntime.get_func(callback);
const dropFiles = function (files) {
diff --git a/platform/javascript/js/libs/library_godot_editor_tools.js b/platform/javascript/js/libs/library_godot_editor_tools.js
index f39fed04a8..12edc8e733 100644
--- a/platform/javascript/js/libs/library_godot_editor_tools.js
+++ b/platform/javascript/js/libs/library_godot_editor_tools.js
@@ -30,6 +30,7 @@
const GodotEditorTools = {
godot_js_editor_download_file__deps: ['$FS'],
+ godot_js_editor_download_file__sig: 'viii',
godot_js_editor_download_file: function (p_path, p_name, p_mime) {
const path = GodotRuntime.parseString(p_path);
const name = GodotRuntime.parseString(p_name);
diff --git a/platform/javascript/js/libs/library_godot_eval.js b/platform/javascript/js/libs/library_godot_eval.js
index 33ff231726..1a2440dd24 100644
--- a/platform/javascript/js/libs/library_godot_eval.js
+++ b/platform/javascript/js/libs/library_godot_eval.js
@@ -30,6 +30,7 @@
const GodotEval = {
godot_js_eval__deps: ['$GodotRuntime'],
+ godot_js_eval__sig: 'iiiiiii',
godot_js_eval: function (p_js, p_use_global_ctx, p_union_ptr, p_byte_arr, p_byte_arr_write, p_callback) {
const js_code = GodotRuntime.parseString(p_js);
let eval_ret = null;
diff --git a/platform/javascript/js/libs/library_godot_http_request.js b/platform/javascript/js/libs/library_godot_http_request.js
index 2b9aa88208..d4468bd5aa 100644
--- a/platform/javascript/js/libs/library_godot_http_request.js
+++ b/platform/javascript/js/libs/library_godot_http_request.js
@@ -50,6 +50,7 @@ const GodotHTTPRequest = {
},
},
+ godot_xhr_new__sig: 'i',
godot_xhr_new: function () {
const newId = GodotHTTPRequest.getUnusedRequestId();
GodotHTTPRequest.requests[newId] = new XMLHttpRequest();
@@ -57,30 +58,36 @@ const GodotHTTPRequest = {
return newId;
},
+ godot_xhr_reset__sig: 'vi',
godot_xhr_reset: function (xhrId) {
GodotHTTPRequest.requests[xhrId] = new XMLHttpRequest();
GodotHTTPRequest.setupRequest(GodotHTTPRequest.requests[xhrId]);
},
+ godot_xhr_free__sig: 'vi',
godot_xhr_free: function (xhrId) {
GodotHTTPRequest.requests[xhrId].abort();
GodotHTTPRequest.requests[xhrId] = null;
},
+ godot_xhr_open__sig: 'viiiii',
godot_xhr_open: function (xhrId, method, url, p_user, p_password) {
const user = p_user > 0 ? GodotRuntime.parseString(p_user) : null;
const password = p_password > 0 ? GodotRuntime.parseString(p_password) : null;
GodotHTTPRequest.requests[xhrId].open(GodotRuntime.parseString(method), GodotRuntime.parseString(url), true, user, password);
},
+ godot_xhr_set_request_header__sig: 'viii',
godot_xhr_set_request_header: function (xhrId, header, value) {
GodotHTTPRequest.requests[xhrId].setRequestHeader(GodotRuntime.parseString(header), GodotRuntime.parseString(value));
},
+ godot_xhr_send_null__sig: 'vi',
godot_xhr_send_null: function (xhrId) {
GodotHTTPRequest.requests[xhrId].send();
},
+ godot_xhr_send_string__sig: 'vii',
godot_xhr_send_string: function (xhrId, strPtr) {
if (!strPtr) {
GodotRuntime.error('Failed to send string per XHR: null pointer');
@@ -89,6 +96,7 @@ const GodotHTTPRequest = {
GodotHTTPRequest.requests[xhrId].send(GodotRuntime.parseString(strPtr));
},
+ godot_xhr_send_data__sig: 'viii',
godot_xhr_send_data: function (xhrId, ptr, len) {
if (!ptr) {
GodotRuntime.error('Failed to send data per XHR: null pointer');
@@ -101,23 +109,28 @@ const GodotHTTPRequest = {
GodotHTTPRequest.requests[xhrId].send(HEAPU8.subarray(ptr, ptr + len));
},
+ godot_xhr_abort__sig: 'vi',
godot_xhr_abort: function (xhrId) {
GodotHTTPRequest.requests[xhrId].abort();
},
+ godot_xhr_get_status__sig: 'ii',
godot_xhr_get_status: function (xhrId) {
return GodotHTTPRequest.requests[xhrId].status;
},
+ godot_xhr_get_ready_state__sig: 'ii',
godot_xhr_get_ready_state: function (xhrId) {
return GodotHTTPRequest.requests[xhrId].readyState;
},
+ godot_xhr_get_response_headers_length__sig: 'ii',
godot_xhr_get_response_headers_length: function (xhrId) {
const headers = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders();
return headers === null ? 0 : GodotRuntime.strlen(headers);
},
+ godot_xhr_get_response_headers__sig: 'viii',
godot_xhr_get_response_headers: function (xhrId, dst, len) {
const str = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders();
if (str === null) {
@@ -126,11 +139,13 @@ const GodotHTTPRequest = {
GodotRuntime.stringToHeap(str, dst, len);
},
+ godot_xhr_get_response_length__sig: 'ii',
godot_xhr_get_response_length: function (xhrId) {
const body = GodotHTTPRequest.requests[xhrId].response;
return body === null ? 0 : body.byteLength;
},
+ godot_xhr_get_response__sig: 'viii',
godot_xhr_get_response: function (xhrId, dst, len) {
let buf = GodotHTTPRequest.requests[xhrId].response;
if (buf === null) {
diff --git a/platform/javascript/js/libs/library_godot_os.js b/platform/javascript/js/libs/library_godot_os.js
index 44b104922e..260cfbf97f 100644
--- a/platform/javascript/js/libs/library_godot_os.js
+++ b/platform/javascript/js/libs/library_godot_os.js
@@ -75,14 +75,17 @@ const GodotConfig = {
},
},
+ godot_js_config_canvas_id_get__sig: 'vii',
godot_js_config_canvas_id_get: function (p_ptr, p_ptr_max) {
GodotRuntime.stringToHeap(`#${GodotConfig.canvas.id}`, p_ptr, p_ptr_max);
},
+ godot_js_config_locale_get__sig: 'vii',
godot_js_config_locale_get: function (p_ptr, p_ptr_max) {
GodotRuntime.stringToHeap(GodotConfig.locale, p_ptr, p_ptr_max);
},
+ godot_js_config_is_resize_on_start__sig: 'i',
godot_js_config_is_resize_on_start: function () {
return GodotConfig.resize_on_start ? 1 : 0;
},
@@ -239,19 +242,23 @@ const GodotOS = {
},
},
+ godot_js_os_finish_async__sig: 'vi',
godot_js_os_finish_async: function (p_callback) {
const func = GodotRuntime.get_func(p_callback);
GodotOS.finish_async(func);
},
+ godot_js_os_request_quit_cb__sig: 'vi',
godot_js_os_request_quit_cb: function (p_callback) {
GodotOS.request_quit = GodotRuntime.get_func(p_callback);
},
+ godot_js_os_fs_is_persistent__sig: 'i',
godot_js_os_fs_is_persistent: function () {
return GodotFS.is_persistent();
},
+ godot_js_os_fs_sync__sig: 'vi',
godot_js_os_fs_sync: function (callback) {
const func = GodotRuntime.get_func(callback);
GodotOS._fs_sync_promise = GodotFS.sync();
@@ -260,6 +267,7 @@ const GodotOS = {
});
},
+ godot_js_os_execute__sig: 'ii',
godot_js_os_execute: function (p_json) {
const json_args = GodotRuntime.parseString(p_json);
const args = JSON.parse(json_args);
@@ -270,6 +278,7 @@ const GodotOS = {
return 1;
},
+ godot_js_os_shell_open__sig: 'vi',
godot_js_os_shell_open: function (p_uri) {
window.open(GodotRuntime.parseString(p_uri), '_blank');
},
diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp
index 80723d54fc..ebfcd7293e 100644
--- a/platform/javascript/os_javascript.cpp
+++ b/platform/javascript/os_javascript.cpp
@@ -43,6 +43,7 @@
#include "modules/websocket/remote_debugger_peer_websocket.h"
#endif
+#include <dlfcn.h>
#include <emscripten.h>
#include <stdlib.h>
@@ -127,12 +128,24 @@ int OS_JavaScript::get_process_id() const {
}
bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) {
- if (p_feature == "HTML5" || p_feature == "web")
+ if (p_feature == "HTML5" || p_feature == "web") {
return true;
+ }
#ifdef JAVASCRIPT_EVAL_ENABLED
- if (p_feature == "JavaScript")
+ if (p_feature == "JavaScript") {
+ return true;
+ }
+#endif
+#ifndef NO_THREADS
+ if (p_feature == "threads") {
return true;
+ }
+#endif
+#if WASM_GDNATIVE
+ if (p_feature == "wasm32") {
+ return true;
+ }
#endif
return false;
@@ -187,6 +200,13 @@ bool OS_JavaScript::is_userfs_persistent() const {
return idb_available;
}
+Error OS_JavaScript::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) {
+ String path = p_path.get_file();
+ p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW);
+ ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ". Error: " + dlerror());
+ return OK;
+}
+
OS_JavaScript *OS_JavaScript::get_singleton() {
return static_cast<OS_JavaScript *>(OS::get_singleton());
}
diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h
index 03a3053367..6682f8fd85 100644
--- a/platform/javascript/os_javascript.h
+++ b/platform/javascript/os_javascript.h
@@ -87,6 +87,7 @@ public:
String get_user_data_dir() const override;
bool is_userfs_persistent() const override;
+ Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) override;
void resume_audio();
diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp
index e569aa03d7..ac88d457a7 100644
--- a/platform/linuxbsd/os_linuxbsd.cpp
+++ b/platform/linuxbsd/os_linuxbsd.cpp
@@ -303,65 +303,147 @@ static String get_mountpoint(const String &p_path) {
}
Error OS_LinuxBSD::move_to_trash(const String &p_path) {
- String trash_can = "";
+ int err_code;
+ List<String> args;
+ args.push_back(p_path);
+ args.push_front("trash"); // The command is `gio trash <file_name>` so we need to add it to args.
+ Error result = execute("gio", args, true, nullptr, nullptr, &err_code); // For GNOME based machines.
+ if (result == OK && !err_code) {
+ return OK;
+ } else if (err_code == 2) {
+ return ERR_FILE_NOT_FOUND;
+ }
+
+ args.pop_front();
+ args.push_front("move");
+ args.push_back("trash:/"); // The command is `kioclient5 move <file_name> trash:/`.
+ result = execute("kioclient5", args, true, nullptr, nullptr, &err_code); // For KDE based machines.
+ if (result == OK && !err_code) {
+ return OK;
+ } else if (err_code == 2) {
+ return ERR_FILE_NOT_FOUND;
+ }
+
+ args.pop_front();
+ args.pop_back();
+ result = execute("gvfs-trash", args, true, nullptr, nullptr, &err_code); // For older Linux machines.
+ if (result == OK && !err_code) {
+ return OK;
+ } else if (err_code == 2) {
+ return ERR_FILE_NOT_FOUND;
+ }
+
+ // If the commands `kioclient5`, `gio` or `gvfs-trash` don't exist on the system we do it manually.
+ String trash_path = "";
String mnt = get_mountpoint(p_path);
- // If there is a directory "[Mountpoint]/.Trash-[UID]/files", use it as the trash can.
+ // If there is a directory "[Mountpoint]/.Trash-[UID], use it as the trash can.
if (mnt != "") {
- String path(mnt + "/.Trash-" + itos(getuid()) + "/files");
+ String path(mnt + "/.Trash-" + itos(getuid()));
struct stat s;
if (!stat(path.utf8().get_data(), &s)) {
- trash_can = path;
+ trash_path = path;
}
}
- // Otherwise, if ${XDG_DATA_HOME} is defined, use "${XDG_DATA_HOME}/Trash/files" as the trash can.
- if (trash_can == "") {
+ // Otherwise, if ${XDG_DATA_HOME} is defined, use "${XDG_DATA_HOME}/Trash" as the trash can.
+ if (trash_path == "") {
char *dhome = getenv("XDG_DATA_HOME");
if (dhome) {
- trash_can = String(dhome) + "/Trash/files";
+ trash_path = String(dhome) + "/Trash";
}
}
- // Otherwise, if ${HOME} is defined, use "${HOME}/.local/share/Trash/files" as the trash can.
- if (trash_can == "") {
+ // Otherwise, if ${HOME} is defined, use "${HOME}/.local/share/Trash" as the trash can.
+ if (trash_path == "") {
char *home = getenv("HOME");
if (home) {
- trash_can = String(home) + "/.local/share/Trash/files";
+ trash_path = String(home) + "/.local/share/Trash";
}
}
// Issue an error if none of the previous locations is appropriate for the trash can.
- if (trash_can == "") {
- ERR_PRINT("move_to_trash: Could not determine the trash can location");
- return FAILED;
- }
+ ERR_FAIL_COND_V_MSG(trash_path == "", FAILED, "Could not determine the trash can location");
// Create needed directories for decided trash can location.
- DirAccess *dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- Error err = dir_access->make_dir_recursive(trash_can);
- memdelete(dir_access);
+ {
+ DirAccess *dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ Error err = dir_access->make_dir_recursive(trash_path);
+
+ // Issue an error if trash can is not created proprely.
+ ERR_FAIL_COND_V_MSG(err != OK, err, "Could not create the trash path \"" + trash_path + "\"");
+ err = dir_access->make_dir_recursive(trash_path + "/files");
+ ERR_FAIL_COND_V_MSG(err != OK, err, "Could not create the trash path \"" + trash_path + "\"/files");
+ err = dir_access->make_dir_recursive(trash_path + "/info");
+ ERR_FAIL_COND_V_MSG(err != OK, err, "Could not create the trash path \"" + trash_path + "\"/info");
+ memdelete(dir_access);
+ }
- // Issue an error if trash can is not created proprely.
- if (err != OK) {
- ERR_PRINT("move_to_trash: Could not create the trash can \"" + trash_can + "\"");
- return err;
+ // The trash can is successfully created, now we check that we don't exceed our file name length limit.
+ // If the file name is too long trim it so we can add the identifying number and ".trashinfo".
+ // Assumes that the file name length limit is 255 characters.
+ String file_name = basename(p_path.utf8().get_data());
+ if (file_name.length() > 240) {
+ file_name = file_name.substr(0, file_name.length() - 15);
+ }
+
+ String dest_path = trash_path + "/files/" + file_name;
+ struct stat buff;
+ int id_number = 0;
+ String fn = file_name;
+
+ // Checks if a resource with the same name already exist in the trash can,
+ // if there is, add an identifying number to our resource's name.
+ while (stat(dest_path.utf8().get_data(), &buff) == 0) {
+ id_number++;
+
+ // Added a limit to check for identically named files already on the trash can
+ // if there are too many it could make the editor unresponsive.
+ ERR_FAIL_COND_V_MSG(id_number > 99, FAILED, "Too many identically named resources already in the trash can.");
+ fn = file_name + "." + itos(id_number);
+ dest_path = trash_path + "/files/" + fn;
+ }
+ file_name = fn;
+
+ // Generates the .trashinfo file
+ OS::Date date = OS::get_singleton()->get_date(false);
+ OS::Time time = OS::get_singleton()->get_time(false);
+ String timestamp = vformat("%04d-%02d-%02dT%02d:%02d:", date.year, date.month, date.day, time.hour, time.min);
+ timestamp = vformat("%s%02d", timestamp, time.sec); // vformat only supports up to 6 arguments.
+ String trash_info = "[Trash Info]\nPath=" + p_path.http_escape() + "\nDeletionDate=" + timestamp + "\n";
+ {
+ Error err;
+ FileAccess *file = FileAccess::open(trash_path + "/info/" + file_name + ".trashinfo", FileAccess::WRITE, &err);
+ ERR_FAIL_COND_V_MSG(err != OK, err, "Can't create trashinfo file:" + trash_path + "/info/" + file_name + ".trashinfo");
+ file->store_string(trash_info);
+ file->close();
+
+ // Rename our resource before moving it to the trash can.
+ DirAccess *dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ err = dir_access->rename(p_path, p_path.get_base_dir() + "/" + file_name);
+ ERR_FAIL_COND_V_MSG(err != OK, err, "Can't rename file \"" + p_path + "\"");
+ memdelete(dir_access);
}
- // The trash can is successfully created, now move the given resource to it.
+ // Move the given resource to the trash can.
// Do not use DirAccess:rename() because it can't move files across multiple mountpoints.
List<String> mv_args;
- mv_args.push_back(p_path);
- mv_args.push_back(trash_can);
- int retval;
- err = execute("mv", mv_args, true, nullptr, nullptr, &retval);
-
- // Issue an error if "mv" failed to move the given resource to the trash can.
- if (err != OK || retval != 0) {
- ERR_PRINT("move_to_trash: Could not move the resource \"" + p_path + "\" to the trash can \"" + trash_can + "\"");
- return FAILED;
+ mv_args.push_back(p_path.get_base_dir() + "/" + file_name);
+ mv_args.push_back(trash_path + "/files");
+ {
+ int retval;
+ Error err = execute("mv", mv_args, true, nullptr, nullptr, &retval);
+
+ // Issue an error if "mv" failed to move the given resource to the trash can.
+ if (err != OK || retval != 0) {
+ ERR_PRINT("move_to_trash: Could not move the resource \"" + p_path + "\" to the trash can \"" + trash_path + "/files\"");
+ DirAccess *dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ err = dir_access->rename(p_path.get_base_dir() + "/" + file_name, p_path);
+ memdelete(dir_access);
+ ERR_FAIL_COND_V_MSG(err != OK, err, "Could not rename " + p_path.get_base_dir() + "/" + file_name + " back to its original name:" + p_path);
+ return FAILED;
+ }
}
-
return OK;
}