1/**************************************************************************/
2/* export_plugin.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 "export_plugin.h"
32
33#include "logo_svg.gen.h"
34#include "run_icon_svg.gen.h"
35
36#include "core/config/project_settings.h"
37#include "editor/editor_node.h"
38#include "editor/editor_paths.h"
39#include "editor/editor_scale.h"
40#include "editor/editor_string_names.h"
41#include "editor/export/editor_export.h"
42
43#include "modules/modules_enabled.gen.h" // For svg.
44#ifdef MODULE_SVG_ENABLED
45#include "modules/svg/image_loader_svg.h"
46#endif
47
48Error EditorExportPlatformLinuxBSD::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) {
49 Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
50 if (f.is_null()) {
51 add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path));
52 return ERR_CANT_CREATE;
53 }
54
55 f->store_line("#!/bin/sh");
56 f->store_line("echo -ne '\\033c\\033]0;" + p_app_name + "\\a'");
57 f->store_line("base_path=\"$(dirname \"$(realpath \"$0\")\")\"");
58 f->store_line("\"$base_path/" + p_pkg_name + "\" \"$@\"");
59
60 return OK;
61}
62
63Error EditorExportPlatformLinuxBSD::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
64 bool export_as_zip = p_path.ends_with("zip");
65
66 String pkg_name;
67 if (String(GLOBAL_GET("application/config/name")) != "") {
68 pkg_name = String(GLOBAL_GET("application/config/name"));
69 } else {
70 pkg_name = "Unnamed";
71 }
72
73 pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);
74
75 // Setup temp folder.
76 String path = p_path;
77 String tmp_dir_path = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name);
78
79 Ref<DirAccess> tmp_app_dir = DirAccess::create_for_path(tmp_dir_path);
80 if (export_as_zip) {
81 if (tmp_app_dir.is_null()) {
82 return ERR_CANT_CREATE;
83 }
84 if (DirAccess::exists(tmp_dir_path)) {
85 if (tmp_app_dir->change_dir(tmp_dir_path) == OK) {
86 tmp_app_dir->erase_contents_recursive();
87 }
88 }
89 tmp_app_dir->make_dir_recursive(tmp_dir_path);
90 path = tmp_dir_path.path_join(p_path.get_file().get_basename());
91 }
92
93 // Export project.
94 Error err = EditorExportPlatformPC::export_project(p_preset, p_debug, path, p_flags);
95 if (err != OK) {
96 return err;
97 }
98
99 // Save console wrapper.
100 if (err == OK) {
101 int con_scr = p_preset->get("debug/export_console_wrapper");
102 if ((con_scr == 1 && p_debug) || (con_scr == 2)) {
103 String scr_path = path.get_basename() + ".sh";
104 err = _export_debug_script(p_preset, pkg_name, path.get_file(), scr_path);
105 FileAccess::set_unix_permissions(scr_path, 0755);
106 if (err != OK) {
107 add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Console Export"), TTR("Could not create console wrapper."));
108 }
109 }
110 }
111
112 // ZIP project.
113 if (export_as_zip) {
114 if (FileAccess::exists(p_path)) {
115 OS::get_singleton()->move_to_trash(p_path);
116 }
117
118 Ref<FileAccess> io_fa_dst;
119 zlib_filefunc_def io_dst = zipio_create_io(&io_fa_dst);
120 zipFile zip = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io_dst);
121
122 zip_folder_recursive(zip, tmp_dir_path, "", pkg_name);
123
124 zipClose(zip, nullptr);
125
126 if (tmp_app_dir->change_dir(tmp_dir_path) == OK) {
127 tmp_app_dir->erase_contents_recursive();
128 tmp_app_dir->change_dir("..");
129 tmp_app_dir->remove(pkg_name);
130 }
131 }
132
133 return err;
134}
135
136String EditorExportPlatformLinuxBSD::get_template_file_name(const String &p_target, const String &p_arch) const {
137 return "linux_" + p_target + "." + p_arch;
138}
139
140List<String> EditorExportPlatformLinuxBSD::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {
141 List<String> list;
142 list.push_back(p_preset->get("binary_format/architecture"));
143 list.push_back("zip");
144
145 return list;
146}
147
148bool EditorExportPlatformLinuxBSD::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {
149 if (p_preset) {
150 // Hide SSH options.
151 bool ssh = p_preset->get("ssh_remote_deploy/enabled");
152 if (!ssh && p_option != "ssh_remote_deploy/enabled" && p_option.begins_with("ssh_remote_deploy/")) {
153 return false;
154 }
155 }
156 return true;
157}
158
159void EditorExportPlatformLinuxBSD::get_export_options(List<ExportOption> *r_options) const {
160 EditorExportPlatformPC::get_export_options(r_options);
161
162 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "binary_format/architecture", PROPERTY_HINT_ENUM, "x86_64,x86_32,arm64,arm32,rv64,ppc64,ppc32"), "x86_64"));
163
164 String run_script = "#!/usr/bin/env bash\n"
165 "export DISPLAY=:0\n"
166 "unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"\n"
167 "\"{temp_dir}/{exe_name}\" {cmd_args}";
168
169 String cleanup_script = "#!/usr/bin/env bash\n"
170 "kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")\n"
171 "rm -rf \"{temp_dir}\"";
172
173 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false, true));
174 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/host"), "user@host_ip"));
175 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/port"), "22"));
176
177 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_ssh", PROPERTY_HINT_MULTILINE_TEXT), ""));
178 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_scp", PROPERTY_HINT_MULTILINE_TEXT), ""));
179 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/run_script", PROPERTY_HINT_MULTILINE_TEXT), run_script));
180 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/cleanup_script", PROPERTY_HINT_MULTILINE_TEXT), cleanup_script));
181}
182
183bool EditorExportPlatformLinuxBSD::is_elf(const String &p_path) const {
184 Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ);
185 ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("Can't open file: \"%s\".", p_path));
186 uint32_t magic = fb->get_32();
187 return (magic == 0x464c457f);
188}
189
190bool EditorExportPlatformLinuxBSD::is_shebang(const String &p_path) const {
191 Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ);
192 ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("Can't open file: \"%s\".", p_path));
193 uint16_t magic = fb->get_16();
194 return (magic == 0x2123);
195}
196
197bool EditorExportPlatformLinuxBSD::is_executable(const String &p_path) const {
198 return is_elf(p_path) || is_shebang(p_path);
199}
200
201Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) {
202 // Patch the header of the "pck" section in the ELF file so that it corresponds to the embedded data
203
204 Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ_WRITE);
205 if (f.is_null()) {
206 add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), vformat(TTR("Failed to open executable file \"%s\"."), p_path));
207 return ERR_CANT_OPEN;
208 }
209
210 // Read and check ELF magic number
211 {
212 uint32_t magic = f->get_32();
213 if (magic != 0x464c457f) { // 0x7F + "ELF"
214 add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable file header corrupted."));
215 return ERR_FILE_CORRUPT;
216 }
217 }
218
219 // Read program architecture bits from class field
220
221 int bits = f->get_8() * 32;
222
223 if (bits == 32 && p_embedded_size >= 0x100000000) {
224 add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("32-bit executables cannot have embedded data >= 4 GiB."));
225 }
226
227 // Get info about the section header table
228
229 int64_t section_table_pos;
230 int64_t section_header_size;
231 if (bits == 32) {
232 section_header_size = 40;
233 f->seek(0x20);
234 section_table_pos = f->get_32();
235 f->seek(0x30);
236 } else { // 64
237 section_header_size = 64;
238 f->seek(0x28);
239 section_table_pos = f->get_64();
240 f->seek(0x3c);
241 }
242 int num_sections = f->get_16();
243 int string_section_idx = f->get_16();
244
245 // Load the strings table
246 uint8_t *strings;
247 {
248 // Jump to the strings section header
249 f->seek(section_table_pos + string_section_idx * section_header_size);
250
251 // Read strings data size and offset
252 int64_t string_data_pos;
253 int64_t string_data_size;
254 if (bits == 32) {
255 f->seek(f->get_position() + 0x10);
256 string_data_pos = f->get_32();
257 string_data_size = f->get_32();
258 } else { // 64
259 f->seek(f->get_position() + 0x18);
260 string_data_pos = f->get_64();
261 string_data_size = f->get_64();
262 }
263
264 // Read strings data
265 f->seek(string_data_pos);
266 strings = (uint8_t *)memalloc(string_data_size);
267 if (!strings) {
268 return ERR_OUT_OF_MEMORY;
269 }
270 f->get_buffer(strings, string_data_size);
271 }
272
273 // Search for the "pck" section
274
275 bool found = false;
276 for (int i = 0; i < num_sections; ++i) {
277 int64_t section_header_pos = section_table_pos + i * section_header_size;
278 f->seek(section_header_pos);
279
280 uint32_t name_offset = f->get_32();
281 if (strcmp((char *)strings + name_offset, "pck") == 0) {
282 // "pck" section found, let's patch!
283
284 if (bits == 32) {
285 f->seek(section_header_pos + 0x10);
286 f->store_32(p_embedded_start);
287 f->store_32(p_embedded_size);
288 } else { // 64
289 f->seek(section_header_pos + 0x18);
290 f->store_64(p_embedded_start);
291 f->store_64(p_embedded_size);
292 }
293
294 found = true;
295 break;
296 }
297 }
298
299 memfree(strings);
300
301 if (!found) {
302 add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable \"pck\" section not found."));
303 return ERR_FILE_CORRUPT;
304 }
305 return OK;
306}
307
308Ref<Texture2D> EditorExportPlatformLinuxBSD::get_run_icon() const {
309 return run_icon;
310}
311
312bool EditorExportPlatformLinuxBSD::poll_export() {
313 Ref<EditorExportPreset> preset;
314
315 for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
316 Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i);
317 if (ep->is_runnable() && ep->get_platform() == this) {
318 preset = ep;
319 break;
320 }
321 }
322
323 int prev = menu_options;
324 menu_options = (preset.is_valid() && preset->get("ssh_remote_deploy/enabled").operator bool());
325 if (ssh_pid != 0 || !cleanup_commands.is_empty()) {
326 if (menu_options == 0) {
327 cleanup();
328 } else {
329 menu_options += 1;
330 }
331 }
332 return menu_options != prev;
333}
334
335Ref<ImageTexture> EditorExportPlatformLinuxBSD::get_option_icon(int p_index) const {
336 return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index);
337}
338
339int EditorExportPlatformLinuxBSD::get_options_count() const {
340 return menu_options;
341}
342
343String EditorExportPlatformLinuxBSD::get_option_label(int p_index) const {
344 return (p_index) ? TTR("Stop and uninstall") : TTR("Run on remote Linux/BSD system");
345}
346
347String EditorExportPlatformLinuxBSD::get_option_tooltip(int p_index) const {
348 return (p_index) ? TTR("Stop and uninstall running project from the remote system") : TTR("Run exported project on remote Linux/BSD system");
349}
350
351void EditorExportPlatformLinuxBSD::cleanup() {
352 if (ssh_pid != 0 && OS::get_singleton()->is_process_running(ssh_pid)) {
353 print_line("Terminating connection...");
354 OS::get_singleton()->kill(ssh_pid);
355 OS::get_singleton()->delay_usec(1000);
356 }
357
358 if (!cleanup_commands.is_empty()) {
359 print_line("Stopping and deleting previous version...");
360 for (const SSHCleanupCommand &cmd : cleanup_commands) {
361 if (cmd.wait) {
362 ssh_run_on_remote(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);
363 } else {
364 ssh_run_on_remote_no_wait(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);
365 }
366 }
367 }
368 ssh_pid = 0;
369 cleanup_commands.clear();
370}
371
372Error EditorExportPlatformLinuxBSD::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) {
373 cleanup();
374 if (p_device) { // Stop command, cleanup only.
375 return OK;
376 }
377
378 EditorProgress ep("run", TTR("Running..."), 5);
379
380 const String dest = EditorPaths::get_singleton()->get_cache_dir().path_join("linuxbsd");
381 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
382 if (!da->dir_exists(dest)) {
383 Error err = da->make_dir_recursive(dest);
384 if (err != OK) {
385 EditorNode::get_singleton()->show_warning(TTR("Could not create temp directory:") + "\n" + dest);
386 return err;
387 }
388 }
389
390 String host = p_preset->get("ssh_remote_deploy/host").operator String();
391 String port = p_preset->get("ssh_remote_deploy/port").operator String();
392 if (port.is_empty()) {
393 port = "22";
394 }
395 Vector<String> extra_args_ssh = p_preset->get("ssh_remote_deploy/extra_args_ssh").operator String().split(" ", false);
396 Vector<String> extra_args_scp = p_preset->get("ssh_remote_deploy/extra_args_scp").operator String().split(" ", false);
397
398 const String basepath = dest.path_join("tmp_linuxbsd_export");
399
400#define CLEANUP_AND_RETURN(m_err) \
401 { \
402 if (da->file_exists(basepath + ".zip")) { \
403 da->remove(basepath + ".zip"); \
404 } \
405 if (da->file_exists(basepath + "_start.sh")) { \
406 da->remove(basepath + "_start.sh"); \
407 } \
408 if (da->file_exists(basepath + "_clean.sh")) { \
409 da->remove(basepath + "_clean.sh"); \
410 } \
411 return m_err; \
412 } \
413 ((void)0)
414
415 if (ep.step(TTR("Exporting project..."), 1)) {
416 return ERR_SKIP;
417 }
418 Error err = export_project(p_preset, true, basepath + ".zip", p_debug_flags);
419 if (err != OK) {
420 DirAccess::remove_file_or_error(basepath + ".zip");
421 return err;
422 }
423
424 String cmd_args;
425 {
426 Vector<String> cmd_args_list;
427 gen_debug_flags(cmd_args_list, p_debug_flags);
428 for (int i = 0; i < cmd_args_list.size(); i++) {
429 if (i != 0) {
430 cmd_args += " ";
431 }
432 cmd_args += cmd_args_list[i];
433 }
434 }
435
436 const bool use_remote = (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) || (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT);
437 int dbg_port = EditorSettings::get_singleton()->get("network/debug/remote_port");
438
439 print_line("Creating temporary directory...");
440 ep.step(TTR("Creating temporary directory..."), 2);
441 String temp_dir;
442 err = ssh_run_on_remote(host, port, extra_args_ssh, "mktemp -d", &temp_dir);
443 if (err != OK || temp_dir.is_empty()) {
444 CLEANUP_AND_RETURN(err);
445 }
446
447 print_line("Uploading archive...");
448 ep.step(TTR("Uploading archive..."), 3);
449 err = ssh_push_to_remote(host, port, extra_args_scp, basepath + ".zip", temp_dir);
450 if (err != OK) {
451 CLEANUP_AND_RETURN(err);
452 }
453
454 {
455 String run_script = p_preset->get("ssh_remote_deploy/run_script");
456 run_script = run_script.replace("{temp_dir}", temp_dir);
457 run_script = run_script.replace("{archive_name}", basepath.get_file() + ".zip");
458 run_script = run_script.replace("{exe_name}", basepath.get_file());
459 run_script = run_script.replace("{cmd_args}", cmd_args);
460
461 Ref<FileAccess> f = FileAccess::open(basepath + "_start.sh", FileAccess::WRITE);
462 if (f.is_null()) {
463 CLEANUP_AND_RETURN(err);
464 }
465
466 f->store_string(run_script);
467 }
468
469 {
470 String clean_script = p_preset->get("ssh_remote_deploy/cleanup_script");
471 clean_script = clean_script.replace("{temp_dir}", temp_dir);
472 clean_script = clean_script.replace("{archive_name}", basepath.get_file() + ".zip");
473 clean_script = clean_script.replace("{exe_name}", basepath.get_file());
474 clean_script = clean_script.replace("{cmd_args}", cmd_args);
475
476 Ref<FileAccess> f = FileAccess::open(basepath + "_clean.sh", FileAccess::WRITE);
477 if (f.is_null()) {
478 CLEANUP_AND_RETURN(err);
479 }
480
481 f->store_string(clean_script);
482 }
483
484 print_line("Uploading scripts...");
485 ep.step(TTR("Uploading scripts..."), 4);
486 err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_start.sh", temp_dir);
487 if (err != OK) {
488 CLEANUP_AND_RETURN(err);
489 }
490 err = ssh_run_on_remote(host, port, extra_args_ssh, vformat("chmod +x \"%s/%s\"", temp_dir, basepath.get_file() + "_start.sh"));
491 if (err != OK || temp_dir.is_empty()) {
492 CLEANUP_AND_RETURN(err);
493 }
494 err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_clean.sh", temp_dir);
495 if (err != OK) {
496 CLEANUP_AND_RETURN(err);
497 }
498 err = ssh_run_on_remote(host, port, extra_args_ssh, vformat("chmod +x \"%s/%s\"", temp_dir, basepath.get_file() + "_clean.sh"));
499 if (err != OK || temp_dir.is_empty()) {
500 CLEANUP_AND_RETURN(err);
501 }
502
503 print_line("Starting project...");
504 ep.step(TTR("Starting project..."), 5);
505 err = ssh_run_on_remote_no_wait(host, port, extra_args_ssh, vformat("\"%s/%s\"", temp_dir, basepath.get_file() + "_start.sh"), &ssh_pid, (use_remote) ? dbg_port : -1);
506 if (err != OK) {
507 CLEANUP_AND_RETURN(err);
508 }
509
510 cleanup_commands.clear();
511 cleanup_commands.push_back(SSHCleanupCommand(host, port, extra_args_ssh, vformat("\"%s/%s\"", temp_dir, basepath.get_file() + "_clean.sh")));
512
513 print_line("Project started.");
514
515 CLEANUP_AND_RETURN(OK);
516#undef CLEANUP_AND_RETURN
517}
518
519EditorExportPlatformLinuxBSD::EditorExportPlatformLinuxBSD() {
520 if (EditorNode::get_singleton()) {
521#ifdef MODULE_SVG_ENABLED
522 Ref<Image> img = memnew(Image);
523 const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
524
525 ImageLoaderSVG::create_image_from_string(img, _linuxbsd_logo_svg, EDSCALE, upsample, false);
526 set_logo(ImageTexture::create_from_image(img));
527
528 ImageLoaderSVG::create_image_from_string(img, _linuxbsd_run_icon_svg, EDSCALE, upsample, false);
529 run_icon = ImageTexture::create_from_image(img);
530#endif
531
532 Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
533 if (theme.is_valid()) {
534 stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons));
535 } else {
536 stop_icon.instantiate();
537 }
538 }
539}
540