Selaa lähdekoodia

[Export] Add one-click deploy over SSH for the desktop exports.

Add one-click deploy over SSH for the desktop exports.
Add ZIP export option for Linux and Windows.
Change export plugin icons to SVG format.
bruvzg 3 vuotta sitten
vanhempi
commit
cebefc9f5d
43 muutettua tiedostoa jossa 1391 lisäystä ja 187 poistoa
  1. 1 0
      editor/editor_property_name_processor.cpp
  2. 3 1
      editor/editor_run_native.cpp
  3. 6 0
      editor/export/editor_export.cpp
  4. 233 0
      editor/export/editor_export_platform.cpp
  5. 9 1
      editor/export/editor_export_platform.h
  6. 0 1
      editor/export/editor_export_platform_pc.h
  7. 5 0
      editor/export/editor_export_plugin.cpp
  8. 18 18
      methods.py
  9. 19 4
      platform/android/export/export_plugin.cpp
  10. BIN
      platform/android/logo.png
  11. 1 0
      platform/android/logo.svg
  12. BIN
      platform/android/run_icon.png
  13. 1 0
      platform/android/run_icon.svg
  14. 16 1
      platform/ios/export/export_plugin.cpp
  15. 0 1
      platform/ios/export/export_plugin.h
  16. BIN
      platform/ios/logo.png
  17. 1 0
      platform/ios/logo.svg
  18. 0 1
      platform/linuxbsd/export/export.cpp
  19. 333 11
      platform/linuxbsd/export/export_plugin.cpp
  20. 42 3
      platform/linuxbsd/export/export_plugin.h
  21. BIN
      platform/linuxbsd/logo.png
  22. 1 0
      platform/linuxbsd/logo.svg
  23. 1 0
      platform/linuxbsd/run_icon.svg
  24. 263 121
      platform/macos/export/export_plugin.cpp
  25. 49 10
      platform/macos/export/export_plugin.h
  26. BIN
      platform/macos/logo.png
  27. 1 0
      platform/macos/logo.svg
  28. 0 0
      platform/macos/run_icon.svg
  29. 16 2
      platform/uwp/export/export_plugin.cpp
  30. BIN
      platform/uwp/logo.png
  31. 1 0
      platform/uwp/logo.svg
  32. 19 2
      platform/web/export/export_plugin.cpp
  33. 1 4
      platform/web/export/export_plugin.h
  34. BIN
      platform/web/logo.png
  35. 1 0
      platform/web/logo.svg
  36. BIN
      platform/web/run_icon.png
  37. 1 0
      platform/web/run_icon.svg
  38. 0 1
      platform/windows/export/export.cpp
  39. 312 4
      platform/windows/export/export_plugin.cpp
  40. 35 1
      platform/windows/export/export_plugin.h
  41. BIN
      platform/windows/logo.png
  42. 1 0
      platform/windows/logo.svg
  43. 1 0
      platform/windows/run_icon.svg

+ 1 - 0
editor/editor_property_name_processor.cpp

@@ -211,6 +211,7 @@ EditorPropertyNameProcessor::EditorPropertyNameProcessor() {
 	capitalize_string_remaps["rmb"] = "RMB";
 	capitalize_string_remaps["rpc"] = "RPC";
 	capitalize_string_remaps["s3tc"] = "S3TC";
+	capitalize_string_remaps["scp"] = "SCP";
 	capitalize_string_remaps["sdf"] = "SDF";
 	capitalize_string_remaps["sdfgi"] = "SDFGI";
 	capitalize_string_remaps["sdk"] = "SDK";

+ 3 - 1
editor/editor_run_native.cpp

@@ -154,7 +154,9 @@ Error EditorRunNative::run_native(int p_idx, int p_platform) {
 	Error err = eep->run(preset, p_idx, flags);
 	result_dialog_log->clear();
 	if (eep->fill_log_messages(result_dialog_log, err)) {
-		result_dialog->popup_centered_ratio(0.5);
+		if (eep->get_worst_message_type() >= EditorExportPlatform::EXPORT_MESSAGE_ERROR) {
+			result_dialog->popup_centered_ratio(0.5);
+		}
 	}
 	return err;
 }

+ 6 - 0
editor/export/editor_export.cpp

@@ -170,6 +170,12 @@ void EditorExport::_notification(int p_what) {
 		case NOTIFICATION_PROCESS: {
 			update_export_presets();
 		} break;
+
+		case NOTIFICATION_EXIT_TREE: {
+			for (int i = 0; i < export_platforms.size(); i++) {
+				export_platforms.write[i]->cleanup();
+			}
+		} break;
 	}
 }
 

+ 233 - 0
editor/export/editor_export_platform.cpp

@@ -1320,6 +1320,121 @@ Error EditorExportPlatform::_add_shared_object(void *p_userdata, const SharedObj
 	return OK;
 }
 
+void EditorExportPlatform::zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name) {
+	String dir = p_folder.is_empty() ? p_root_path : p_root_path.path_join(p_folder);
+
+	Ref<DirAccess> da = DirAccess::open(dir);
+	da->list_dir_begin();
+	String f = da->get_next();
+	while (!f.is_empty()) {
+		if (f == "." || f == "..") {
+			f = da->get_next();
+			continue;
+		}
+		if (da->is_link(f)) {
+			OS::DateTime dt = OS::get_singleton()->get_datetime();
+
+			zip_fileinfo zipfi;
+			zipfi.tmz_date.tm_year = dt.year;
+			zipfi.tmz_date.tm_mon = dt.month - 1; // Note: "tm" month range - 0..11, Godot month range - 1..12, https://www.cplusplus.com/reference/ctime/tm/
+			zipfi.tmz_date.tm_mday = dt.day;
+			zipfi.tmz_date.tm_hour = dt.hour;
+			zipfi.tmz_date.tm_min = dt.minute;
+			zipfi.tmz_date.tm_sec = dt.second;
+			zipfi.dosDate = 0;
+			// 0120000: symbolic link type
+			// 0000755: permissions rwxr-xr-x
+			// 0000644: permissions rw-r--r--
+			uint32_t _mode = 0120644;
+			zipfi.external_fa = (_mode << 16L) | !(_mode & 0200);
+			zipfi.internal_fa = 0;
+
+			zipOpenNewFileInZip4(p_zip,
+					p_folder.path_join(f).utf8().get_data(),
+					&zipfi,
+					nullptr,
+					0,
+					nullptr,
+					0,
+					nullptr,
+					Z_DEFLATED,
+					Z_DEFAULT_COMPRESSION,
+					0,
+					-MAX_WBITS,
+					DEF_MEM_LEVEL,
+					Z_DEFAULT_STRATEGY,
+					nullptr,
+					0,
+					0x0314, // "version made by", 0x03 - Unix, 0x14 - ZIP specification version 2.0, required to store Unix file permissions
+					0);
+
+			String target = da->read_link(f);
+			zipWriteInFileInZip(p_zip, target.utf8().get_data(), target.utf8().size());
+			zipCloseFileInZip(p_zip);
+		} else if (da->current_is_dir()) {
+			zip_folder_recursive(p_zip, p_root_path, p_folder.path_join(f), p_pkg_name);
+		} else {
+			bool _is_executable = is_executable(dir.path_join(f));
+
+			OS::DateTime dt = OS::get_singleton()->get_datetime();
+
+			zip_fileinfo zipfi;
+			zipfi.tmz_date.tm_year = dt.year;
+			zipfi.tmz_date.tm_mon = dt.month - 1; // Note: "tm" month range - 0..11, Godot month range - 1..12, https://www.cplusplus.com/reference/ctime/tm/
+			zipfi.tmz_date.tm_mday = dt.day;
+			zipfi.tmz_date.tm_hour = dt.hour;
+			zipfi.tmz_date.tm_min = dt.minute;
+			zipfi.tmz_date.tm_sec = dt.second;
+			zipfi.dosDate = 0;
+			// 0100000: regular file type
+			// 0000755: permissions rwxr-xr-x
+			// 0000644: permissions rw-r--r--
+			uint32_t _mode = (_is_executable ? 0100755 : 0100644);
+			zipfi.external_fa = (_mode << 16L) | !(_mode & 0200);
+			zipfi.internal_fa = 0;
+
+			zipOpenNewFileInZip4(p_zip,
+					p_folder.path_join(f).utf8().get_data(),
+					&zipfi,
+					nullptr,
+					0,
+					nullptr,
+					0,
+					nullptr,
+					Z_DEFLATED,
+					Z_DEFAULT_COMPRESSION,
+					0,
+					-MAX_WBITS,
+					DEF_MEM_LEVEL,
+					Z_DEFAULT_STRATEGY,
+					nullptr,
+					0,
+					0x0314, // "version made by", 0x03 - Unix, 0x14 - ZIP specification version 2.0, required to store Unix file permissions
+					0);
+
+			Ref<FileAccess> fa = FileAccess::open(dir.path_join(f), FileAccess::READ);
+			if (fa.is_null()) {
+				add_message(EXPORT_MESSAGE_ERROR, TTR("ZIP Creation"), vformat(TTR("Could not open file to read from path \"%s\"."), dir.path_join(f)));
+				return;
+			}
+			const int bufsize = 16384;
+			uint8_t buf[bufsize];
+
+			while (true) {
+				uint64_t got = fa->get_buffer(buf, bufsize);
+				if (got == 0) {
+					break;
+				}
+				zipWriteInFileInZip(p_zip, buf, got);
+			}
+
+			zipCloseFileInZip(p_zip);
+		}
+		f = da->get_next();
+	}
+	da->list_dir_end();
+}
+
 Error EditorExportPlatform::save_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *p_so_files, bool p_embed, int64_t *r_embedded_start, int64_t *r_embedded_size) {
 	EditorProgress ep("savepack", TTR("Packing"), 102, true);
 
@@ -1640,5 +1755,123 @@ bool EditorExportPlatform::can_export(const Ref<EditorExportPreset> &p_preset, S
 	return valid;
 }
 
+Error EditorExportPlatform::ssh_run_on_remote(const String &p_host, const String &p_port, const Vector<String> &p_ssh_args, const String &p_cmd_args, String *r_out, int p_port_fwd) const {
+	String ssh_path = EditorSettings::get_singleton()->get("export/ssh/ssh");
+	if (ssh_path.is_empty()) {
+		ssh_path = "ssh";
+	}
+
+	List<String> args;
+	args.push_back("-p");
+	args.push_back(p_port);
+	for (const String &E : p_ssh_args) {
+		args.push_back(E);
+	}
+	if (p_port_fwd > 0) {
+		args.push_back("-R");
+		args.push_back(vformat("%d:localhost:%d", p_port_fwd, p_port_fwd));
+	}
+	args.push_back(p_host);
+	args.push_back(p_cmd_args);
+
+	String out;
+	int exit_code = -1;
+
+	if (OS::get_singleton()->is_stdout_verbose()) {
+		OS::get_singleton()->print("Executing: %s", ssh_path.utf8().get_data());
+		for (const String &arg : args) {
+			OS::get_singleton()->print(" %s", arg.utf8().get_data());
+		}
+		OS::get_singleton()->print("\n");
+	}
+
+	Error err = OS::get_singleton()->execute(ssh_path, args, &out, &exit_code, true);
+	if (out.is_empty()) {
+		print_verbose(vformat("Exit code: %d", exit_code));
+	} else {
+		print_verbose(vformat("Exit code: %d, Output: %s", exit_code, out.replace("\r\n", "\n")));
+	}
+	if (r_out) {
+		*r_out = out.replace("\r\n", "\n").get_slice("\n", 0);
+	}
+	if (err != OK) {
+		return err;
+	} else if (exit_code != 0) {
+		if (!out.is_empty()) {
+			print_line(out);
+		}
+		return FAILED;
+	}
+	return OK;
+}
+
+Error EditorExportPlatform::ssh_run_on_remote_no_wait(const String &p_host, const String &p_port, const Vector<String> &p_ssh_args, const String &p_cmd_args, OS::ProcessID *r_pid, int p_port_fwd) const {
+	String ssh_path = EditorSettings::get_singleton()->get("export/ssh/ssh");
+	if (ssh_path.is_empty()) {
+		ssh_path = "ssh";
+	}
+
+	List<String> args;
+	args.push_back("-p");
+	args.push_back(p_port);
+	for (const String &E : p_ssh_args) {
+		args.push_back(E);
+	}
+	if (p_port_fwd > 0) {
+		args.push_back("-R");
+		args.push_back(vformat("%d:localhost:%d", p_port_fwd, p_port_fwd));
+	}
+	args.push_back(p_host);
+	args.push_back(p_cmd_args);
+
+	if (OS::get_singleton()->is_stdout_verbose()) {
+		OS::get_singleton()->print("Executing: %s", ssh_path.utf8().get_data());
+		for (const String &arg : args) {
+			OS::get_singleton()->print(" %s", arg.utf8().get_data());
+		}
+		OS::get_singleton()->print("\n");
+	}
+
+	return OS::get_singleton()->create_process(ssh_path, args, r_pid);
+}
+
+Error EditorExportPlatform::ssh_push_to_remote(const String &p_host, const String &p_port, const Vector<String> &p_scp_args, const String &p_src_file, const String &p_dst_file) const {
+	String scp_path = EditorSettings::get_singleton()->get("export/ssh/scp");
+	if (scp_path.is_empty()) {
+		scp_path = "scp";
+	}
+
+	List<String> args;
+	args.push_back("-P");
+	args.push_back(p_port);
+	for (const String &E : p_scp_args) {
+		args.push_back(E);
+	}
+	args.push_back(p_src_file);
+	args.push_back(vformat("%s:%s", p_host, p_dst_file));
+
+	String out;
+	int exit_code = -1;
+
+	if (OS::get_singleton()->is_stdout_verbose()) {
+		OS::get_singleton()->print("Executing: %s", scp_path.utf8().get_data());
+		for (const String &arg : args) {
+			OS::get_singleton()->print(" %s", arg.utf8().get_data());
+		}
+		OS::get_singleton()->print("\n");
+	}
+
+	Error err = OS::get_singleton()->execute(scp_path, args, &out, &exit_code, true);
+	if (err != OK) {
+		return err;
+	} else if (exit_code != 0) {
+		if (!out.is_empty()) {
+			print_line(out);
+		}
+		return FAILED;
+	}
+	return OK;
+}
+
 EditorExportPlatform::EditorExportPlatform() {
 }

+ 9 - 1
editor/export/editor_export_platform.h

@@ -35,6 +35,7 @@ class EditorFileSystemDirectory;
 struct EditorProgress;
 
 #include "core/io/dir_access.h"
+#include "core/io/zip_io.h"
 #include "editor_export_preset.h"
 #include "editor_export_shared_object.h"
 #include "scene/gui/rich_text_label.h"
@@ -92,7 +93,6 @@ private:
 	void _export_find_resources(EditorFileSystemDirectory *p_dir, HashSet<String> &p_paths);
 	void _export_find_dependencies(const String &p_path, HashSet<String> &p_paths);
 
-	void gen_debug_flags(Vector<String> &r_flags, int p_flags);
 	static Error _save_pack_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
 	static Error _save_zip_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
 
@@ -126,6 +126,13 @@ protected:
 	bool exists_export_template(String template_file_name, String *err) const;
 	String find_export_template(String template_file_name, String *err = nullptr) const;
 	void gen_export_flags(Vector<String> &r_flags, int p_flags);
+	void gen_debug_flags(Vector<String> &r_flags, int p_flags);
+
+	virtual void zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name);
+
+	Error ssh_run_on_remote(const String &p_host, const String &p_port, const Vector<String> &p_ssh_args, const String &p_cmd_args, String *r_out = nullptr, int p_port_fwd = -1) const;
+	Error ssh_run_on_remote_no_wait(const String &p_host, const String &p_port, const Vector<String> &p_ssh_args, const String &p_cmd_args, OS::ProcessID *r_pid = nullptr, int p_port_fwd = -1) const;
+	Error ssh_push_to_remote(const String &p_host, const String &p_port, const Vector<String> &p_scp_args, const String &p_src_file, const String &p_dst_file) const;
 
 public:
 	virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const = 0;
@@ -215,6 +222,7 @@ public:
 		DEBUG_FLAG_VIEW_NAVIGATION = 16,
 	};
 
+	virtual void cleanup() {}
 	virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) { return OK; }
 	virtual Ref<Texture2D> get_run_icon() const { return get_logo(); }
 

+ 0 - 1
editor/export/editor_export_platform_pc.h

@@ -62,7 +62,6 @@ public:
 	virtual Error modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { return OK; };
 	virtual Error export_project_data(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags);
 
-	void set_extension(const String &p_extension, const String &p_feature_key = "default");
 	void set_name(const String &p_name);
 	void set_os_name(const String &p_name);
 

+ 5 - 0
editor/export/editor_export_plugin.cpp

@@ -34,6 +34,7 @@
 #include "core/io/dir_access.h"
 #include "core/io/file_access.h"
 #include "editor/editor_paths.h"
+#include "editor/editor_settings.h"
 #include "editor/export/editor_export_platform.h"
 #include "scene/resources/resource_format_text.h"
 
@@ -226,4 +227,8 @@ void EditorExportPlugin::_bind_methods() {
 }
 
 EditorExportPlugin::EditorExportPlugin() {
+	GLOBAL_DEF("editor/export/convert_text_resources_to_binary", false);
+
+	EDITOR_DEF("export/ssh/ssh", "");
+	EDITOR_DEF("export/ssh/scp", "");
 }

+ 18 - 18
methods.py

@@ -484,29 +484,29 @@ def use_windows_spawn_fix(self, platform=None):
 def save_active_platforms(apnames, ap):
 
     for x in ap:
-        names = ["logo"]
-        if os.path.isfile(x + "/run_icon.png"):
-            names.append("run_icon")
-
-        for name in names:
-            pngf = open(x + "/" + name + ".png", "rb")
-            b = pngf.read(1)
-            str = " /* AUTOGENERATED FILE, DO NOT EDIT */ \n"
-            str += " static const unsigned char _" + x[9:] + "_" + name + "[]={"
+        svg_names = []
+        if os.path.isfile(x + "/logo.svg"):
+            svg_names.append("logo")
+        if os.path.isfile(x + "/run_icon.svg"):
+            svg_names.append("run_icon")
+
+        for name in svg_names:
+            svgf = open(x + "/" + name + ".svg", "rb")
+            b = svgf.read(1)
+            svg_str = " /* AUTOGENERATED FILE, DO NOT EDIT */ \n"
+            svg_str += " static const char *_" + x[9:] + "_" + name + '_svg = "'
             while len(b) == 1:
-                str += hex(ord(b))
-                b = pngf.read(1)
-                if len(b) == 1:
-                    str += ","
+                svg_str += "\\" + hex(ord(b))[1:]
+                b = svgf.read(1)
 
-            str += "};\n"
+            svg_str += '";\n'
 
-            pngf.close()
+            svgf.close()
 
             # NOTE: It is safe to generate this file here, since this is still executed serially
-            wf = x + "/" + name + ".gen.h"
-            with open(wf, "w") as pngw:
-                pngw.write(str)
+            wf = x + "/" + name + "_svg.gen.h"
+            with open(wf, "w") as svgw:
+                svgw.write(svg_str)
 
 
 def no_verbose(sys, env):

+ 19 - 4
platform/android/export/export_plugin.cpp

@@ -43,10 +43,16 @@
 #include "editor/editor_log.h"
 #include "editor/editor_node.h"
 #include "editor/editor_paths.h"
+#include "editor/editor_scale.h"
 #include "editor/editor_settings.h"
 #include "main/splash.gen.h"
-#include "platform/android/logo.gen.h"
-#include "platform/android/run_icon.gen.h"
+#include "platform/android/logo_svg.gen.h"
+#include "platform/android/run_icon_svg.gen.h"
+
+#include "modules/modules_enabled.gen.h" // For svg.
+#ifdef MODULE_SVG_ENABLED
+#include "modules/svg/image_loader_svg.h"
+#endif
 
 #include <string.h>
 
@@ -3234,8 +3240,17 @@ void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<
 }
 
 EditorExportPlatformAndroid::EditorExportPlatformAndroid() {
-	logo = ImageTexture::create_from_image(memnew(Image(_android_logo)));
-	run_icon = ImageTexture::create_from_image(memnew(Image(_android_run_icon)));
+#ifdef MODULE_SVG_ENABLED
+	Ref<Image> img = memnew(Image);
+	const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
+
+	ImageLoaderSVG img_loader;
+	img_loader.create_image_from_string(img, _android_logo_svg, EDSCALE, upsample, false);
+	logo = ImageTexture::create_from_image(img);
+
+	img_loader.create_image_from_string(img, _android_run_icon_svg, EDSCALE, upsample, false);
+	run_icon = ImageTexture::create_from_image(img);
+#endif
 
 	devices_changed.set();
 	plugins_changed.set();

BIN
platform/android/logo.png


+ 1 - 0
platform/android/logo.svg

@@ -0,0 +1 @@
+<svg height="32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="M22.904 20.192a1.25 1.25 0 1 1 1.25-1.25 1.25 1.25 0 0 1-1.25 1.25m-13.808 0a1.25 1.25 0 1 1 1.25-1.25 1.25 1.25 0 0 1-1.25 1.25m14.256-7.525 2.496-4.323a.52.52 0 1 0-.899-.52l-2.528 4.378a15.69 15.69 0 0 0-12.842 0L7.051 7.823a.52.52 0 1 0-.9.521l2.497 4.323C4.361 15 1.43 19.34 1 24.467h30c-.43-5.128-3.361-9.468-7.648-11.8" fill="#56d881"/></svg>

BIN
platform/android/run_icon.png


+ 1 - 0
platform/android/run_icon.svg

@@ -0,0 +1 @@
+<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M11.187 9.936a.577.577 0 1 1 .578-.578.577.577 0 0 1-.578.578m-6.374 0a.577.577 0 1 1 .577-.578.577.577 0 0 1-.577.578m6.581-3.475 1.153-1.996a.24.24 0 1 0-.415-.24l-1.167 2.021a7.244 7.244 0 0 0-5.93 0L3.868 4.225a.24.24 0 1 0-.415.24l1.153 1.996a6.806 6.806 0 0 0-3.532 5.448h13.852a6.807 6.807 0 0 0-3.532-5.448" fill="#56d881" style="fill:#e0e0e0;fill-opacity:1"/></svg>

+ 16 - 1
platform/ios/export/export_plugin.cpp

@@ -32,6 +32,13 @@
 
 #include "core/string/translation.h"
 #include "editor/editor_node.h"
+#include "editor/editor_scale.h"
+#include "platform/ios/logo_svg.gen.h"
+
+#include "modules/modules_enabled.gen.h" // For svg.
+#ifdef MODULE_SVG_ENABLED
+#include "modules/svg/image_loader_svg.h"
+#endif
 
 void EditorExportPlatformIOS::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {
 	// Vulkan and OpenGL ES 3.0 both mandate ETC2 support.
@@ -1914,7 +1921,15 @@ bool EditorExportPlatformIOS::has_valid_project_configuration(const Ref<EditorEx
 }
 
 EditorExportPlatformIOS::EditorExportPlatformIOS() {
-	logo = ImageTexture::create_from_image(memnew(Image(_ios_logo)));
+#ifdef MODULE_SVG_ENABLED
+	Ref<Image> img = memnew(Image);
+	const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
+
+	ImageLoaderSVG img_loader;
+	img_loader.create_image_from_string(img, _ios_logo_svg, EDSCALE, upsample, false);
+	logo = ImageTexture::create_from_image(img);
+#endif
+
 	plugins_changed.set();
 #ifndef ANDROID_ENABLED
 	check_for_changes_thread.start(_check_for_changes_poll_thread, this);

+ 0 - 1
platform/ios/export/export_plugin.h

@@ -43,7 +43,6 @@
 #include "editor/editor_settings.h"
 #include "editor/export/editor_export_platform.h"
 #include "main/splash.gen.h"
-#include "platform/ios/logo.gen.h"
 #include "string.h"
 
 #include "godot_plugin_config.h"

BIN
platform/ios/logo.png


+ 1 - 0
platform/ios/logo.svg

@@ -0,0 +1 @@
+<svg height="32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="M1 23.27h2.504V12.61H1zm1.247-12.057c.784 0 1.398-.603 1.398-1.358 0-.764-.614-1.367-1.398-1.367-.774 0-1.388.603-1.388 1.367 0 .755.614 1.358 1.388 1.358zm9.594-2.695c-4.233 0-6.888 2.886-6.888 7.502s2.654 7.492 6.888 7.492c4.224 0 6.88-2.876 6.88-7.492s-2.656-7.502-6.88-7.502zm0 2.212c2.585 0 4.234 2.052 4.234 5.29 0 3.228-1.649 5.28-4.234 5.28-2.594 0-4.233-2.052-4.233-5.28 0-3.238 1.639-5.29 4.233-5.29zm7.936 8.458c.11 2.675 2.303 4.324 5.641 4.324 3.51 0 5.723-1.73 5.723-4.485 0-2.162-1.247-3.379-4.194-4.053l-1.67-.382c-1.78-.422-2.513-.985-2.513-1.95 0-1.208 1.106-2.012 2.745-2.012 1.66 0 2.796.814 2.916 2.172H30.9c-.06-2.554-2.172-4.284-5.37-4.284-3.158 0-5.4 1.74-5.4 4.314 0 2.072 1.267 3.36 3.942 3.973l1.88.442c1.83.433 2.575 1.036 2.575 2.082 0 1.207-1.217 2.072-2.967 2.072-1.77 0-3.107-.875-3.268-2.213h-2.514z" fill="#bfbfbf"/></svg>

+ 0 - 1
platform/linuxbsd/export/export.cpp

@@ -36,7 +36,6 @@
 void register_linuxbsd_exporter() {
 	Ref<EditorExportPlatformLinuxBSD> platform;
 	platform.instantiate();
-	platform->set_logo(ImageTexture::create_from_image(memnew(Image(_linuxbsd_logo))));
 	platform->set_name("Linux/X11");
 	platform->set_os_name("Linux");
 	platform->set_chmod_flags(0755);

+ 333 - 11
platform/linuxbsd/export/export_plugin.cpp

@@ -32,6 +32,15 @@
 
 #include "core/config/project_settings.h"
 #include "editor/editor_node.h"
+#include "editor/editor_paths.h"
+#include "editor/editor_scale.h"
+#include "platform/linuxbsd/logo_svg.gen.h"
+#include "platform/linuxbsd/run_icon_svg.gen.h"
+
+#include "modules/modules_enabled.gen.h" // For svg.
+#ifdef MODULE_SVG_ENABLED
+#include "modules/svg/image_loader_svg.h"
+#endif
 
 Error EditorExportPlatformLinuxBSD::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) {
 	Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
@@ -49,26 +58,47 @@ Error EditorExportPlatformLinuxBSD::_export_debug_script(const Ref<EditorExportP
 }
 
 Error EditorExportPlatformLinuxBSD::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
-	Error err = EditorExportPlatformPC::export_project(p_preset, p_debug, p_path, p_flags);
-
-	if (err != OK) {
-		return err;
-	}
+	bool export_as_zip = p_path.ends_with("zip");
 
-	String app_name;
+	String pkg_name;
 	if (String(GLOBAL_GET("application/config/name")) != "") {
-		app_name = String(GLOBAL_GET("application/config/name"));
+		pkg_name = String(GLOBAL_GET("application/config/name"));
 	} else {
-		app_name = "Unnamed";
+		pkg_name = "Unnamed";
+	}
+
+	pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);
+
+	// Setup temp folder.
+	String path = p_path;
+	String tmp_dir_path = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name);
+
+	Ref<DirAccess> tmp_app_dir = DirAccess::create_for_path(tmp_dir_path);
+	if (export_as_zip) {
+		if (tmp_app_dir.is_null()) {
+			return ERR_CANT_CREATE;
+		}
+		if (DirAccess::exists(tmp_dir_path)) {
+			if (tmp_app_dir->change_dir(tmp_dir_path) == OK) {
+				tmp_app_dir->erase_contents_recursive();
+			}
+		}
+		tmp_app_dir->make_dir_recursive(tmp_dir_path);
+		path = tmp_dir_path.path_join(p_path.get_file().get_basename());
+	}
+
+	// Export project.
+	Error err = EditorExportPlatformPC::export_project(p_preset, p_debug, path, p_flags);
+	if (err != OK) {
+		return err;
 	}
-	app_name = OS::get_singleton()->get_safe_dir_name(app_name);
 
 	// Save console script.
 	if (err == OK) {
 		int con_scr = p_preset->get("debug/export_console_script");
 		if ((con_scr == 1 && p_debug) || (con_scr == 2)) {
-			String scr_path = p_path.get_basename() + ".sh";
-			err = _export_debug_script(p_preset, app_name, p_path.get_file(), scr_path);
+			String scr_path = path.get_basename() + ".sh";
+			err = _export_debug_script(p_preset, pkg_name, path.get_file(), scr_path);
 			FileAccess::set_unix_permissions(scr_path, 0755);
 			if (err != OK) {
 				add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), TTR("Could not create console script."));
@@ -76,6 +106,27 @@ Error EditorExportPlatformLinuxBSD::export_project(const Ref<EditorExportPreset>
 		}
 	}
 
+	// ZIP project.
+	if (export_as_zip) {
+		if (FileAccess::exists(p_path)) {
+			OS::get_singleton()->move_to_trash(p_path);
+		}
+
+		Ref<FileAccess> io_fa_dst;
+		zlib_filefunc_def io_dst = zipio_create_io(&io_fa_dst);
+		zipFile zip = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io_dst);
+
+		zip_folder_recursive(zip, tmp_dir_path, "", pkg_name);
+
+		zipClose(zip, nullptr);
+
+		if (tmp_app_dir->change_dir(tmp_dir_path) == OK) {
+			tmp_app_dir->erase_contents_recursive();
+			tmp_app_dir->change_dir("..");
+			tmp_app_dir->remove(pkg_name);
+		}
+	}
+
 	return err;
 }
 
@@ -86,12 +137,51 @@ String EditorExportPlatformLinuxBSD::get_template_file_name(const String &p_targ
 List<String> EditorExportPlatformLinuxBSD::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {
 	List<String> list;
 	list.push_back(p_preset->get("binary_format/architecture"));
+	list.push_back("zip");
+
 	return list;
 }
 
 void EditorExportPlatformLinuxBSD::get_export_options(List<ExportOption> *r_options) {
 	EditorExportPlatformPC::get_export_options(r_options);
+
 	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"));
+
+	String run_script = "#!/usr/bin/env bash\n"
+						"export DISPLAY=:0\n"
+						"unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"\n"
+						"\"{temp_dir}/{exe_name}\" {cmd_args}";
+
+	String cleanup_script = "#!/usr/bin/env bash\n"
+							"kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")\n"
+							"rm -rf \"{temp_dir}\"";
+
+	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/host"), "user@host_ip"));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/port"), "22"));
+
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_ssh", PROPERTY_HINT_MULTILINE_TEXT), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_scp", PROPERTY_HINT_MULTILINE_TEXT), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/run_script", PROPERTY_HINT_MULTILINE_TEXT), run_script));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/cleanup_script", PROPERTY_HINT_MULTILINE_TEXT), cleanup_script));
+}
+
+bool EditorExportPlatformLinuxBSD::is_elf(const String &p_path) const {
+	Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ);
+	ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("Can't open file: \"%s\".", p_path));
+	uint32_t magic = fb->get_32();
+	return (magic == 0x464c457f);
+}
+
+bool EditorExportPlatformLinuxBSD::is_shebang(const String &p_path) const {
+	Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ);
+	ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("Can't open file: \"%s\".", p_path));
+	uint16_t magic = fb->get_16();
+	return (magic == 0x2123);
+}
+
+bool EditorExportPlatformLinuxBSD::is_executable(const String &p_path) const {
+	return is_elf(p_path) || is_shebang(p_path);
 }
 
 Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) {
@@ -200,3 +290,235 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int
 	}
 	return OK;
 }
+
+Ref<Texture2D> EditorExportPlatformLinuxBSD::get_run_icon() const {
+	return run_icon;
+}
+
+bool EditorExportPlatformLinuxBSD::poll_export() {
+	Ref<EditorExportPreset> preset;
+
+	for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
+		Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i);
+		if (ep->is_runnable() && ep->get_platform() == this) {
+			preset = ep;
+			break;
+		}
+	}
+
+	int prev = menu_options;
+	menu_options = (preset.is_valid() && preset->get("ssh_remote_deploy/enabled").operator bool());
+	if (ssh_pid != 0 || !cleanup_commands.is_empty()) {
+		if (menu_options == 0) {
+			cleanup();
+		} else {
+			menu_options += 1;
+		}
+	}
+	return menu_options != prev;
+}
+
+Ref<ImageTexture> EditorExportPlatformLinuxBSD::get_option_icon(int p_index) const {
+	return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index);
+}
+
+int EditorExportPlatformLinuxBSD::get_options_count() const {
+	return menu_options;
+}
+
+String EditorExportPlatformLinuxBSD::get_option_label(int p_index) const {
+	return (p_index) ? TTR("Stop and uninstall") : TTR("Run on remote Linux/BSD system");
+}
+
+String EditorExportPlatformLinuxBSD::get_option_tooltip(int p_index) const {
+	return (p_index) ? TTR("Stop and uninstall running project from the remote system") : TTR("Run exported project on remote Linux/BSD system");
+}
+
+void EditorExportPlatformLinuxBSD::cleanup() {
+	if (ssh_pid != 0 && OS::get_singleton()->is_process_running(ssh_pid)) {
+		print_line("Terminating connection...");
+		OS::get_singleton()->kill(ssh_pid);
+		OS::get_singleton()->delay_usec(1000);
+	}
+
+	if (!cleanup_commands.is_empty()) {
+		print_line("Stopping and deleting previous version...");
+		for (const SSHCleanupCommand &cmd : cleanup_commands) {
+			if (cmd.wait) {
+				ssh_run_on_remote(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);
+			} else {
+				ssh_run_on_remote_no_wait(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);
+			}
+		}
+	}
+	ssh_pid = 0;
+	cleanup_commands.clear();
+}
+
+Error EditorExportPlatformLinuxBSD::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) {
+	cleanup();
+	if (p_device) { // Stop command, cleanup only.
+		return OK;
+	}
+
+	EditorProgress ep("run", TTR("Running..."), 5);
+
+	const String dest = EditorPaths::get_singleton()->get_cache_dir().path_join("linuxbsd");
+	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+	if (!da->dir_exists(dest)) {
+		Error err = da->make_dir_recursive(dest);
+		if (err != OK) {
+			EditorNode::get_singleton()->show_warning(TTR("Could not create temp directory:") + "\n" + dest);
+			return err;
+		}
+	}
+
+	String host = p_preset->get("ssh_remote_deploy/host").operator String();
+	String port = p_preset->get("ssh_remote_deploy/port").operator String();
+	if (port.is_empty()) {
+		port = "22";
+	}
+	Vector<String> extra_args_ssh = p_preset->get("ssh_remote_deploy/extra_args_ssh").operator String().split(" ");
+	Vector<String> extra_args_scp = p_preset->get("ssh_remote_deploy/extra_args_scp").operator String().split(" ");
+
+	const String basepath = dest.path_join("tmp_linuxbsd_export");
+
+#define CLEANUP_AND_RETURN(m_err)                      \
+	{                                                  \
+		if (da->file_exists(basepath + ".zip")) {      \
+			da->remove(basepath + ".zip");             \
+		}                                              \
+		if (da->file_exists(basepath + "_start.sh")) { \
+			da->remove(basepath + "_start.sh");        \
+		}                                              \
+		if (da->file_exists(basepath + "_clean.sh")) { \
+			da->remove(basepath + "_clean.sh");        \
+		}                                              \
+		return m_err;                                  \
+	}                                                  \
+	((void)0)
+
+	if (ep.step(TTR("Exporting project..."), 1)) {
+		return ERR_SKIP;
+	}
+	Error err = export_project(p_preset, true, basepath + ".zip", p_debug_flags);
+	if (err != OK) {
+		DirAccess::remove_file_or_error(basepath + ".zip");
+		return err;
+	}
+
+	String cmd_args;
+	{
+		Vector<String> cmd_args_list;
+		gen_debug_flags(cmd_args_list, p_debug_flags);
+		for (int i = 0; i < cmd_args_list.size(); i++) {
+			if (i != 0) {
+				cmd_args += " ";
+			}
+			cmd_args += cmd_args_list[i];
+		}
+	}
+
+	const bool use_remote = (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) || (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT);
+	int dbg_port = EditorSettings::get_singleton()->get("network/debug/remote_port");
+
+	print_line("Creating temporary directory...");
+	ep.step(TTR("Creating temporary directory..."), 2);
+	String temp_dir;
+	err = ssh_run_on_remote(host, port, extra_args_ssh, "mktemp -d", &temp_dir);
+	if (err != OK || temp_dir.is_empty()) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	print_line("Uploading archive...");
+	ep.step(TTR("Uploading archive..."), 3);
+	err = ssh_push_to_remote(host, port, extra_args_scp, basepath + ".zip", temp_dir);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	{
+		String run_script = p_preset->get("ssh_remote_deploy/run_script");
+		run_script = run_script.replace("{temp_dir}", temp_dir);
+		run_script = run_script.replace("{archive_name}", basepath.get_file() + ".zip");
+		run_script = run_script.replace("{exe_name}", basepath.get_file());
+		run_script = run_script.replace("{cmd_args}", cmd_args);
+
+		Ref<FileAccess> f = FileAccess::open(basepath + "_start.sh", FileAccess::WRITE);
+		if (f.is_null()) {
+			CLEANUP_AND_RETURN(err);
+		}
+
+		f->store_string(run_script);
+	}
+
+	{
+		String clean_script = p_preset->get("ssh_remote_deploy/cleanup_script");
+		clean_script = clean_script.replace("{temp_dir}", temp_dir);
+		clean_script = clean_script.replace("{archive_name}", basepath.get_file() + ".zip");
+		clean_script = clean_script.replace("{exe_name}", basepath.get_file());
+		clean_script = clean_script.replace("{cmd_args}", cmd_args);
+
+		Ref<FileAccess> f = FileAccess::open(basepath + "_clean.sh", FileAccess::WRITE);
+		if (f.is_null()) {
+			CLEANUP_AND_RETURN(err);
+		}
+
+		f->store_string(clean_script);
+	}
+
+	print_line("Uploading scripts...");
+	ep.step(TTR("Uploading scripts..."), 4);
+	err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_start.sh", temp_dir);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+	err = ssh_run_on_remote(host, port, extra_args_ssh, vformat("chmod +x \"%s/%s\"", temp_dir, basepath.get_file() + "_start.sh"));
+	if (err != OK || temp_dir.is_empty()) {
+		CLEANUP_AND_RETURN(err);
+	}
+	err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_clean.sh", temp_dir);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+	err = ssh_run_on_remote(host, port, extra_args_ssh, vformat("chmod +x \"%s/%s\"", temp_dir, basepath.get_file() + "_clean.sh"));
+	if (err != OK || temp_dir.is_empty()) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	print_line("Starting project...");
+	ep.step(TTR("Starting project..."), 5);
+	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);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	cleanup_commands.clear();
+	cleanup_commands.push_back(SSHCleanupCommand(host, port, extra_args_ssh, vformat("\"%s/%s\"", temp_dir, basepath.get_file() + "_clean.sh")));
+
+	print_line("Project started.");
+
+	CLEANUP_AND_RETURN(OK);
+#undef CLEANUP_AND_RETURN
+}
+
+EditorExportPlatformLinuxBSD::EditorExportPlatformLinuxBSD() {
+#ifdef MODULE_SVG_ENABLED
+	Ref<Image> img = memnew(Image);
+	const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
+
+	ImageLoaderSVG img_loader;
+	img_loader.create_image_from_string(img, _linuxbsd_logo_svg, EDSCALE, upsample, false);
+	set_logo(ImageTexture::create_from_image(img));
+
+	img_loader.create_image_from_string(img, _linuxbsd_run_icon_svg, EDSCALE, upsample, false);
+	run_icon = ImageTexture::create_from_image(img);
+#endif
+
+	Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
+	if (theme.is_valid()) {
+		stop_icon = theme->get_icon(SNAME("Stop"), SNAME("EditorIcons"));
+	} else {
+		stop_icon.instantiate();
+	}
+}

+ 42 - 3
platform/linuxbsd/export/export_plugin.h

@@ -34,19 +34,58 @@
 #include "core/io/file_access.h"
 #include "editor/editor_settings.h"
 #include "editor/export/editor_export_platform_pc.h"
-#include "platform/linuxbsd/logo.gen.h"
 #include "scene/resources/texture.h"
 
 class EditorExportPlatformLinuxBSD : public EditorExportPlatformPC {
+	HashMap<String, String> extensions;
+
+	struct SSHCleanupCommand {
+		String host;
+		String port;
+		Vector<String> ssh_args;
+		String cmd_args;
+		bool wait = false;
+
+		SSHCleanupCommand(){};
+		SSHCleanupCommand(const String &p_host, const String &p_port, const Vector<String> &p_ssh_arg, const String &p_cmd_args, bool p_wait = false) {
+			host = p_host;
+			port = p_port;
+			ssh_args = p_ssh_arg;
+			cmd_args = p_cmd_args;
+			wait = p_wait;
+		};
+	};
+
+	Ref<ImageTexture> run_icon;
+	Ref<ImageTexture> stop_icon;
+
+	Vector<SSHCleanupCommand> cleanup_commands;
+	OS::ProcessID ssh_pid = 0;
+	int menu_options = 0;
+
+	bool is_elf(const String &p_path) const;
+	bool is_shebang(const String &p_path) const;
+
 	Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path);
 
 public:
-	void set_extension(const String &p_extension, const String &p_feature_key = "default");
-	virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override;
 	virtual void get_export_options(List<ExportOption> *r_options) override;
+	virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override;
 	virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override;
 	virtual String get_template_file_name(const String &p_target, const String &p_arch) const override;
 	virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) override;
+	virtual bool is_executable(const String &p_path) const override;
+
+	virtual Ref<Texture2D> get_run_icon() const override;
+	virtual bool poll_export() override;
+	virtual Ref<ImageTexture> get_option_icon(int p_index) const override;
+	virtual int get_options_count() const override;
+	virtual String get_option_label(int p_index) const override;
+	virtual String get_option_tooltip(int p_index) const override;
+	virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override;
+	virtual void cleanup() override;
+
+	EditorExportPlatformLinuxBSD();
 };
 
 #endif // LINUXBSD_EXPORT_PLUGIN_H

BIN
platform/linuxbsd/logo.png


+ 1 - 0
platform/linuxbsd/logo.svg

@@ -0,0 +1 @@
+<svg height="32" width="32"><path d="M13 31h6s3 0 6-6c2.864-5.727-6-17-6-17h-6S3.775 18.55 7 25c3 6 6 6 6 6z" fill="#fff"/><path d="M15.876 28.636c-.05.322-.116.637-.204.941-.142.496-.35.993-.659 1.416.32.02.649.023.985.007a9.1 9.1 0 0 0 .985-.007c-.309-.423-.516-.92-.659-1.416a7.666 7.666 0 0 1-.203-.94l-.123.003c-.04 0-.081-.003-.122-.004z" fill="#333"/><path d="M21.693 21.916c-.629.01-.934.633-1.497.7-.694.08-1.128-.722-2.11-.123-.98.6-1.826 7.473.45 8.409 2.274.935 6.506-4.545 6.23-5.662-.275-1.116-1.146-.853-1.582-1.399-.436-.545.003-1.41-.995-1.82a1.246 1.246 0 0 0-.496-.105zm-11.461 0a1.315 1.315 0 0 0-.421.105c-.998.41-.56 1.275-.995 1.82-.436.546-1.31.283-1.586 1.4-.275 1.116 3.956 6.596 6.232 5.66 2.275-.935 1.429-7.808.448-8.408-.981-.6-1.415.204-2.11.122-.584-.068-.888-.739-1.568-.7z" fill="#f4bb37"/><path d="M15.998.99c-2.934 0-4.657 1.79-4.982 4.204-.324 2.414.198 2.856-.614 5.328-.813 2.472-4.456 6.71-4.37 10.62.026 1.217.166 2.27.41 3.192.3-.496.743-.846 1.066-.995.253-.117.375-.173.432-.194.008-.062.04-.205.098-.485.08-.386.387-.99.91-1.386-.005-.12-.01-.239-.013-.363-.06-3.033 3.073-6.318 3.65-8.236.577-1.917.326-2.114.421-2.59.096-.477.463-1.032.992-1.475a.23.23 0 0 1 .15-.06c.482-.005.965 1.75 1.898 1.752.933 0 1.419-2.141 1.956-1.692.529.443.896.998.992 1.474.095.477-.156.674.42 2.591.578 1.918 3.708 5.203 3.648 8.236-.003.123-.008.24-.014.36.526.396.834 1.002.914 1.389.058.28.09.423.098.485.057.021.18.08.432.197.323.15.764.499 1.063.995.244-.922.387-1.976.414-3.195.085-3.91-3.562-8.148-4.374-10.62-.813-2.472-.287-2.914-.611-5.328C20.659 2.78 18.933.99 15.998.99z" fill="#333"/></svg>

+ 1 - 0
platform/linuxbsd/run_icon.svg

@@ -0,0 +1 @@
+<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M7.941 13.966a3.62 3.62 0 0 1-.096.444 2.129 2.129 0 0 1-.31.668c.15.01.305.01.464.003.16.008.314.007.465-.003a2.129 2.129 0 0 1-.31-.668 3.62 3.62 0 0 1-.097-.444l-.058.001-.058-.001z" fill="#333" style="stroke-width:.472092;fill:#e0e0e0;fill-opacity:1"/><path d="M10.688 10.793c-.297.005-.441.299-.707.33-.328.038-.533-.34-.996-.058-.463.283-.862 3.528.212 3.97 1.074.442 3.072-2.146 2.942-2.673-.13-.527-.542-.403-.747-.66-.206-.258 0-.666-.47-.86a.588.588 0 0 0-.234-.05zm-5.411 0a.62.62 0 0 0-.199.05c-.47.193-.264.601-.47.859-.205.257-.618.133-.748.66s1.867 3.115 2.942 2.673c1.074-.442.674-3.687.211-3.97-.463-.283-.668.096-.995.058-.277-.032-.42-.349-.741-.33z" fill="#f4bb37" style="stroke-width:.472092;fill:#e0e0e0;fill-opacity:1"/><path d="M8 .914c-1.386 0-2.2.845-2.353 1.985-.153 1.14.094 1.348-.29 2.515s-2.103 3.168-2.063 5.013c.012.575.078 1.072.194 1.507a1.25 1.25 0 0 1 .503-.47 4.37 4.37 0 0 1 .204-.09c.004-.03.019-.098.046-.23.038-.182.183-.467.43-.654a4.773 4.773 0 0 1-.006-.172c-.029-1.431 1.45-2.982 1.723-3.888.272-.905.154-.998.199-1.223.045-.225.218-.487.468-.696a.11.11 0 0 1 .07-.028c.228-.003.456.826.897.827.44 0 .67-1.01.923-.799.25.21.423.471.468.696.045.225-.073.318.199 1.223.272.906 1.75 2.457 1.722 3.888-.001.058-.004.114-.007.17a1.2 1.2 0 0 1 .432.656c.027.132.042.2.046.23.027.01.085.037.204.092.153.07.36.236.502.47.115-.435.183-.933.195-1.509.04-1.845-1.681-3.846-2.065-5.013-.383-1.167-.135-1.376-.288-2.515C10.2 1.759 9.385.914 7.999.914Z" fill="#333" style="stroke-width:.472092;fill:#e0e0e0;fill-opacity:1"/></svg>

+ 263 - 121
platform/macos/export/export_plugin.cpp

@@ -38,8 +38,14 @@
 #include "core/string/translation.h"
 #include "editor/editor_node.h"
 #include "editor/editor_paths.h"
+#include "editor/editor_scale.h"
+#include "platform/macos/logo_svg.gen.h"
+#include "platform/macos/run_icon_svg.gen.h"
 
-#include "modules/modules_enabled.gen.h" // For regex.
+#include "modules/modules_enabled.gen.h" // For svg and regex.
+#ifdef MODULE_SVG_ENABLED
+#include "modules/svg/image_loader_svg.h"
+#endif
 
 void EditorExportPlatformMacOS::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {
 	if (p_preset->get("texture_format/s3tc")) {
@@ -207,6 +213,23 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "texture_format/s3tc"), true));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "texture_format/etc"), false));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "texture_format/etc2"), false));
+
+	String run_script = "#!/usr/bin/env bash\n"
+						"unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"\n"
+						"open \"{temp_dir}/{exe_name}.app\" --args {cmd_args}";
+
+	String cleanup_script = "#!/usr/bin/env bash\n"
+							"kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\")\n"
+							"rm -rf \"{temp_dir}\"";
+
+	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/host"), "user@host_ip"));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/port"), "22"));
+
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_ssh", PROPERTY_HINT_MULTILINE_TEXT), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_scp", PROPERTY_HINT_MULTILINE_TEXT), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/run_script", PROPERTY_HINT_MULTILINE_TEXT), run_script));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/cleanup_script", PROPERTY_HINT_MULTILINE_TEXT), cleanup_script));
 }
 
 void _rgba8_to_packbits_encode(int p_ch, int p_size, Vector<uint8_t> &p_source, Vector<uint8_t> &p_dest) {
@@ -993,7 +1016,7 @@ Error EditorExportPlatformMacOS::_create_dmg(const String &p_dmg_path, const Str
 	return OK;
 }
 
-bool EditorExportPlatformMacOS::is_shbang(const String &p_path) const {
+bool EditorExportPlatformMacOS::is_shebang(const String &p_path) const {
 	Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ);
 	ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("Can't open file: \"%s\".", p_path));
 	uint16_t magic = fb->get_16();
@@ -1001,7 +1024,7 @@ bool EditorExportPlatformMacOS::is_shbang(const String &p_path) const {
 }
 
 bool EditorExportPlatformMacOS::is_executable(const String &p_path) const {
-	return MachO::is_macho(p_path) || LipO::is_lipo(p_path) || is_shbang(p_path);
+	return MachO::is_macho(p_path) || LipO::is_lipo(p_path) || is_shebang(p_path);
 }
 
 Error EditorExportPlatformMacOS::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) {
@@ -1082,7 +1105,6 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
 	} else {
 		pkg_name = "Unnamed";
 	}
-
 	pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);
 
 	String export_format;
@@ -1684,7 +1706,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
 				zlib_filefunc_def io_dst = zipio_create_io(&io_fa_dst);
 				zipFile zip = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io_dst);
 
-				_zip_folder_recursive(zip, tmp_base_path_name, "", pkg_name);
+				zip_folder_recursive(zip, tmp_base_path_name, "", pkg_name);
 
 				zipClose(zip, nullptr);
 			}
@@ -1723,119 +1745,6 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
 	return err;
 }
 
-void EditorExportPlatformMacOS::_zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name) {
-	String dir = p_folder.is_empty() ? p_root_path : p_root_path.path_join(p_folder);
-
-	Ref<DirAccess> da = DirAccess::open(dir);
-	da->list_dir_begin();
-	String f = da->get_next();
-	while (!f.is_empty()) {
-		if (f == "." || f == "..") {
-			f = da->get_next();
-			continue;
-		}
-		if (da->is_link(f)) {
-			OS::DateTime dt = OS::get_singleton()->get_datetime();
-
-			zip_fileinfo zipfi;
-			zipfi.tmz_date.tm_year = dt.year;
-			zipfi.tmz_date.tm_mon = dt.month - 1; // Note: "tm" month range - 0..11, Godot month range - 1..12, https://www.cplusplus.com/reference/ctime/tm/
-			zipfi.tmz_date.tm_mday = dt.day;
-			zipfi.tmz_date.tm_hour = dt.hour;
-			zipfi.tmz_date.tm_min = dt.minute;
-			zipfi.tmz_date.tm_sec = dt.second;
-			zipfi.dosDate = 0;
-			// 0120000: symbolic link type
-			// 0000755: permissions rwxr-xr-x
-			// 0000644: permissions rw-r--r--
-			uint32_t _mode = 0120644;
-			zipfi.external_fa = (_mode << 16L) | !(_mode & 0200);
-			zipfi.internal_fa = 0;
-
-			zipOpenNewFileInZip4(p_zip,
-					p_folder.path_join(f).utf8().get_data(),
-					&zipfi,
-					nullptr,
-					0,
-					nullptr,
-					0,
-					nullptr,
-					Z_DEFLATED,
-					Z_DEFAULT_COMPRESSION,
-					0,
-					-MAX_WBITS,
-					DEF_MEM_LEVEL,
-					Z_DEFAULT_STRATEGY,
-					nullptr,
-					0,
-					0x0314, // "version made by", 0x03 - Unix, 0x14 - ZIP specification version 2.0, required to store Unix file permissions
-					0);
-
-			String target = da->read_link(f);
-			zipWriteInFileInZip(p_zip, target.utf8().get_data(), target.utf8().size());
-			zipCloseFileInZip(p_zip);
-		} else if (da->current_is_dir()) {
-			_zip_folder_recursive(p_zip, p_root_path, p_folder.path_join(f), p_pkg_name);
-		} else {
-			OS::DateTime dt = OS::get_singleton()->get_datetime();
-
-			zip_fileinfo zipfi;
-			zipfi.tmz_date.tm_year = dt.year;
-			zipfi.tmz_date.tm_mon = dt.month - 1; // Note: "tm" month range - 0..11, Godot month range - 1..12, https://www.cplusplus.com/reference/ctime/tm/
-			zipfi.tmz_date.tm_mday = dt.day;
-			zipfi.tmz_date.tm_hour = dt.hour;
-			zipfi.tmz_date.tm_min = dt.minute;
-			zipfi.tmz_date.tm_sec = dt.second;
-			zipfi.dosDate = 0;
-			// 0100000: regular file type
-			// 0000755: permissions rwxr-xr-x
-			// 0000644: permissions rw-r--r--
-			uint32_t _mode = (is_executable(dir.path_join(f)) ? 0100755 : 0100644);
-			zipfi.external_fa = (_mode << 16L) | !(_mode & 0200);
-			zipfi.internal_fa = 0;
-
-			zipOpenNewFileInZip4(p_zip,
-					p_folder.path_join(f).utf8().get_data(),
-					&zipfi,
-					nullptr,
-					0,
-					nullptr,
-					0,
-					nullptr,
-					Z_DEFLATED,
-					Z_DEFAULT_COMPRESSION,
-					0,
-					-MAX_WBITS,
-					DEF_MEM_LEVEL,
-					Z_DEFAULT_STRATEGY,
-					nullptr,
-					0,
-					0x0314, // "version made by", 0x03 - Unix, 0x14 - ZIP specification version 2.0, required to store Unix file permissions
-					0);
-
-			Ref<FileAccess> fa = FileAccess::open(dir.path_join(f), FileAccess::READ);
-			if (fa.is_null()) {
-				add_message(EXPORT_MESSAGE_ERROR, TTR("ZIP Creation"), vformat(TTR("Could not open file to read from path \"%s\"."), dir.path_join(f)));
-				return;
-			}
-			const int bufsize = 16384;
-			uint8_t buf[bufsize];
-
-			while (true) {
-				uint64_t got = fa->get_buffer(buf, bufsize);
-				if (got == 0) {
-					break;
-				}
-				zipWriteInFileInZip(p_zip, buf, got);
-			}
-
-			zipCloseFileInZip(p_zip);
-		}
-		f = da->get_next();
-	}
-	da->list_dir_end();
-}
-
 bool EditorExportPlatformMacOS::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const {
 	String err;
 	bool valid = false;
@@ -2008,9 +1917,242 @@ bool EditorExportPlatformMacOS::has_valid_project_configuration(const Ref<Editor
 	return valid;
 }
 
-EditorExportPlatformMacOS::EditorExportPlatformMacOS() {
-	logo = ImageTexture::create_from_image(memnew(Image(_macos_logo)));
+Ref<Texture2D> EditorExportPlatformMacOS::get_run_icon() const {
+	return run_icon;
+}
+
+bool EditorExportPlatformMacOS::poll_export() {
+	Ref<EditorExportPreset> preset;
+
+	for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
+		Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i);
+		if (ep->is_runnable() && ep->get_platform() == this) {
+			preset = ep;
+			break;
+		}
+	}
+
+	int prev = menu_options;
+	menu_options = (preset.is_valid() && preset->get("ssh_remote_deploy/enabled").operator bool());
+	if (ssh_pid != 0 || !cleanup_commands.is_empty()) {
+		if (menu_options == 0) {
+			cleanup();
+		} else {
+			menu_options += 1;
+		}
+	}
+	return menu_options != prev;
+}
+
+Ref<ImageTexture> EditorExportPlatformMacOS::get_option_icon(int p_index) const {
+	return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index);
+}
+
+int EditorExportPlatformMacOS::get_options_count() const {
+	return menu_options;
+}
+
+String EditorExportPlatformMacOS::get_option_label(int p_index) const {
+	return (p_index) ? TTR("Stop and uninstall") : TTR("Run on remote macOS system");
+}
+
+String EditorExportPlatformMacOS::get_option_tooltip(int p_index) const {
+	return (p_index) ? TTR("Stop and uninstall running project from the remote system") : TTR("Run exported project on remote macOS system");
+}
+
+void EditorExportPlatformMacOS::cleanup() {
+	if (ssh_pid != 0 && OS::get_singleton()->is_process_running(ssh_pid)) {
+		print_line("Terminating connection...");
+		OS::get_singleton()->kill(ssh_pid);
+		OS::get_singleton()->delay_usec(1000);
+	}
+
+	if (!cleanup_commands.is_empty()) {
+		print_line("Stopping and deleting previous version...");
+		for (const SSHCleanupCommand &cmd : cleanup_commands) {
+			if (cmd.wait) {
+				ssh_run_on_remote(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);
+			} else {
+				ssh_run_on_remote_no_wait(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);
+			}
+		}
+	}
+	ssh_pid = 0;
+	cleanup_commands.clear();
+}
+
+Error EditorExportPlatformMacOS::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) {
+	cleanup();
+	if (p_device) { // Stop command, cleanup only.
+		return OK;
+	}
+
+	EditorProgress ep("run", TTR("Running..."), 5);
+
+	const String dest = EditorPaths::get_singleton()->get_cache_dir().path_join("macos");
+	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+	if (!da->dir_exists(dest)) {
+		Error err = da->make_dir_recursive(dest);
+		if (err != OK) {
+			EditorNode::get_singleton()->show_warning(TTR("Could not create temp directory:") + "\n" + dest);
+			return err;
+		}
+	}
+
+	String pkg_name;
+	if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") {
+		pkg_name = String(ProjectSettings::get_singleton()->get("application/config/name"));
+	} else {
+		pkg_name = "Unnamed";
+	}
+	pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);
+
+	String host = p_preset->get("ssh_remote_deploy/host").operator String();
+	String port = p_preset->get("ssh_remote_deploy/port").operator String();
+	if (port.is_empty()) {
+		port = "22";
+	}
+	Vector<String> extra_args_ssh = p_preset->get("ssh_remote_deploy/extra_args_ssh").operator String().split(" ");
+	Vector<String> extra_args_scp = p_preset->get("ssh_remote_deploy/extra_args_scp").operator String().split(" ");
+
+	const String basepath = dest.path_join("tmp_macos_export");
+
+#define CLEANUP_AND_RETURN(m_err)                      \
+	{                                                  \
+		if (da->file_exists(basepath + ".zip")) {      \
+			da->remove(basepath + ".zip");             \
+		}                                              \
+		if (da->file_exists(basepath + "_start.sh")) { \
+			da->remove(basepath + "_start.sh");        \
+		}                                              \
+		if (da->file_exists(basepath + "_clean.sh")) { \
+			da->remove(basepath + "_clean.sh");        \
+		}                                              \
+		return m_err;                                  \
+	}                                                  \
+	((void)0)
+
+	if (ep.step(TTR("Exporting project..."), 1)) {
+		return ERR_SKIP;
+	}
+	Error err = export_project(p_preset, true, basepath + ".zip", p_debug_flags);
+	if (err != OK) {
+		DirAccess::remove_file_or_error(basepath + ".zip");
+		return err;
+	}
+
+	String cmd_args;
+	{
+		Vector<String> cmd_args_list;
+		gen_debug_flags(cmd_args_list, p_debug_flags);
+		for (int i = 0; i < cmd_args_list.size(); i++) {
+			if (i != 0) {
+				cmd_args += " ";
+			}
+			cmd_args += cmd_args_list[i];
+		}
+	}
+
+	const bool use_remote = (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) || (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT);
+	int dbg_port = EditorSettings::get_singleton()->get("network/debug/remote_port");
+
+	print_line("Creating temporary directory...");
+	ep.step(TTR("Creating temporary directory..."), 2);
+	String temp_dir;
+	err = ssh_run_on_remote(host, port, extra_args_ssh, "mktemp -d", &temp_dir);
+	if (err != OK || temp_dir.is_empty()) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	print_line("Uploading archive...");
+	ep.step(TTR("Uploading archive..."), 3);
+	err = ssh_push_to_remote(host, port, extra_args_scp, basepath + ".zip", temp_dir);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	{
+		String run_script = p_preset->get("ssh_remote_deploy/run_script");
+		run_script = run_script.replace("{temp_dir}", temp_dir);
+		run_script = run_script.replace("{archive_name}", basepath.get_file() + ".zip");
+		run_script = run_script.replace("{exe_name}", pkg_name);
+		run_script = run_script.replace("{cmd_args}", cmd_args);
+
+		Ref<FileAccess> f = FileAccess::open(basepath + "_start.sh", FileAccess::WRITE);
+		if (f.is_null()) {
+			CLEANUP_AND_RETURN(err);
+		}
+
+		f->store_string(run_script);
+	}
+
+	{
+		String clean_script = p_preset->get("ssh_remote_deploy/cleanup_script");
+		clean_script = clean_script.replace("{temp_dir}", temp_dir);
+		clean_script = clean_script.replace("{archive_name}", basepath.get_file() + ".zip");
+		clean_script = clean_script.replace("{exe_name}", pkg_name);
+		clean_script = clean_script.replace("{cmd_args}", cmd_args);
+
+		Ref<FileAccess> f = FileAccess::open(basepath + "_clean.sh", FileAccess::WRITE);
+		if (f.is_null()) {
+			CLEANUP_AND_RETURN(err);
+		}
+
+		f->store_string(clean_script);
+	}
+
+	print_line("Uploading scripts...");
+	ep.step(TTR("Uploading scripts..."), 4);
+	err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_start.sh", temp_dir);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+	err = ssh_run_on_remote(host, port, extra_args_ssh, vformat("chmod +x \"%s/%s\"", temp_dir, basepath.get_file() + "_start.sh"));
+	if (err != OK || temp_dir.is_empty()) {
+		CLEANUP_AND_RETURN(err);
+	}
+	err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_clean.sh", temp_dir);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+	err = ssh_run_on_remote(host, port, extra_args_ssh, vformat("chmod +x \"%s/%s\"", temp_dir, basepath.get_file() + "_clean.sh"));
+	if (err != OK || temp_dir.is_empty()) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	print_line("Starting project...");
+	ep.step(TTR("Starting project..."), 5);
+	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);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	cleanup_commands.clear();
+	cleanup_commands.push_back(SSHCleanupCommand(host, port, extra_args_ssh, vformat("\"%s/%s\"", temp_dir, basepath.get_file() + "_clean.sh")));
+
+	print_line("Project started.");
+
+	CLEANUP_AND_RETURN(OK);
+#undef CLEANUP_AND_RETURN
 }
 
-EditorExportPlatformMacOS::~EditorExportPlatformMacOS() {
+EditorExportPlatformMacOS::EditorExportPlatformMacOS() {
+#ifdef MODULE_SVG_ENABLED
+	Ref<Image> img = memnew(Image);
+	const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
+
+	ImageLoaderSVG img_loader;
+	img_loader.create_image_from_string(img, _macos_logo_svg, EDSCALE, upsample, false);
+	logo = ImageTexture::create_from_image(img);
+
+	img_loader.create_image_from_string(img, _macos_run_icon_svg, EDSCALE, upsample, false);
+	run_icon = ImageTexture::create_from_image(img);
+#endif
+
+	Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
+	if (theme.is_valid()) {
+		stop_icon = theme->get_icon(SNAME("Stop"), SNAME("EditorIcons"));
+	} else {
+		stop_icon.instantiate();
+	}
 }

+ 49 - 10
platform/macos/export/export_plugin.h

@@ -36,12 +36,10 @@
 #include "core/io/file_access.h"
 #include "core/io/marshalls.h"
 #include "core/io/resource_saver.h"
-#include "core/io/zip_io.h"
 #include "core/os/os.h"
 #include "core/version.h"
 #include "editor/editor_settings.h"
 #include "editor/export/editor_export.h"
-#include "platform/macos/logo.gen.h"
 
 #include <sys/stat.h>
 
@@ -52,6 +50,30 @@ class EditorExportPlatformMacOS : public EditorExportPlatform {
 
 	Ref<ImageTexture> logo;
 
+	struct SSHCleanupCommand {
+		String host;
+		String port;
+		Vector<String> ssh_args;
+		String cmd_args;
+		bool wait = false;
+
+		SSHCleanupCommand(){};
+		SSHCleanupCommand(const String &p_host, const String &p_port, const Vector<String> &p_ssh_arg, const String &p_cmd_args, bool p_wait = false) {
+			host = p_host;
+			port = p_port;
+			ssh_args = p_ssh_arg;
+			cmd_args = p_cmd_args;
+			wait = p_wait;
+		};
+	};
+
+	Ref<ImageTexture> run_icon;
+	Ref<ImageTexture> stop_icon;
+
+	Vector<SSHCleanupCommand> cleanup_commands;
+	OS::ProcessID ssh_pid = 0;
+	int menu_options = 0;
+
 	void _fix_plist(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &plist, const String &p_binary);
 	void _make_icon(const Ref<EditorExportPreset> &p_preset, const Ref<Image> &p_icon, Vector<uint8_t> &p_data);
 
@@ -65,14 +87,17 @@ class EditorExportPlatformMacOS : public EditorExportPlatform {
 			Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset,
 			const String &p_ent_path);
 	Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name);
-	void _zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name);
 	Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path);
 
 	bool use_codesign() const { return true; }
 #ifdef MACOS_ENABLED
-	bool use_dmg() const { return true; }
+	bool use_dmg() const {
+		return true;
+	}
 #else
-	bool use_dmg() const { return false; }
+	bool use_dmg() const {
+		return false;
+	}
 #endif
 
 	bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const {
@@ -97,7 +122,7 @@ class EditorExportPlatformMacOS : public EditorExportPlatform {
 
 		return true;
 	}
-	bool is_shbang(const String &p_path) const;
+	bool is_shebang(const String &p_path) const;
 
 protected:
 	virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override;
@@ -105,9 +130,15 @@ protected:
 	virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option, const HashMap<StringName, Variant> &p_options) const override;
 
 public:
-	virtual String get_name() const override { return "macOS"; }
-	virtual String get_os_name() const override { return "macOS"; }
-	virtual Ref<Texture2D> get_logo() const override { return logo; }
+	virtual String get_name() const override {
+		return "macOS";
+	}
+	virtual String get_os_name() const override {
+		return "macOS";
+	}
+	virtual Ref<Texture2D> get_logo() const override {
+		return logo;
+	}
 
 	virtual bool is_executable(const String &p_path) const override;
 	virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override {
@@ -133,8 +164,16 @@ public:
 	virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) override {
 	}
 
+	virtual Ref<Texture2D> get_run_icon() const override;
+	virtual bool poll_export() override;
+	virtual Ref<ImageTexture> get_option_icon(int p_index) const override;
+	virtual int get_options_count() const override;
+	virtual String get_option_label(int p_index) const override;
+	virtual String get_option_tooltip(int p_index) const override;
+	virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override;
+	virtual void cleanup() override;
+
 	EditorExportPlatformMacOS();
-	~EditorExportPlatformMacOS();
 };
 
 #endif // MACOS_EXPORT_PLUGIN_H

BIN
platform/macos/logo.png


+ 1 - 0
platform/macos/logo.svg

@@ -0,0 +1 @@
+<svg height="32" viewBox="0 0 25.6 25.6" width="32" xmlns="http://www.w3.org/2000/svg"><path d="M.948 19.474V6.126c0-2.767 2.42-5.178 5.178-5.178h6.904s-2.992 7.506-2.992 13.233c0 .346.337.69.69.69h2.877c0 6.648.906 8.436 1.266 9.781H6.126c-2.75 0-5.178-2.407-5.178-5.178z" fill="#1c9ef8"/><path d="M7.162 8.312v1.496" fill="none" stroke="#323232" stroke-linecap="round" stroke-width="1.036"/><path d="M10.729 14.871c-.352 0-.69-.344-.69-.69C10.038 8.414 13.03.948 13.03.948h6.444c2.77 0 5.178 2.416 5.178 5.178v13.348c0 2.754-2.407 5.178-5.178 5.178h-4.603c-.357-1.333-1.266-2.887-1.266-9.78z" fill="#e1e6ed"/><g fill="none" stroke="#323232" stroke-linecap="round"><path d="M17.575 8.255V9.75" stroke-width="1.036"/><path d="M5.55 16.943c1.037 1.794 3.907 2.876 6.79 2.876 2.877 0 5.745-1.068 6.789-2.876" stroke-width=".69"/></g></svg>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
platform/macos/run_icon.svg


+ 16 - 2
platform/uwp/export/export_plugin.cpp

@@ -30,8 +30,14 @@
 
 #include "export_plugin.h"
 
+#include "editor/editor_scale.h"
 #include "editor/editor_settings.h"
-#include "platform/uwp/logo.gen.h"
+#include "platform/uwp/logo_svg.gen.h"
+
+#include "modules/modules_enabled.gen.h" // For svg and regex.
+#ifdef MODULE_SVG_ENABLED
+#include "modules/svg/image_loader_svg.h"
+#endif
 
 String EditorExportPlatformUWP::get_name() const {
 	return "UWP";
@@ -504,5 +510,13 @@ void EditorExportPlatformUWP::resolve_platform_feature_priorities(const Ref<Edit
 }
 
 EditorExportPlatformUWP::EditorExportPlatformUWP() {
-	logo = ImageTexture::create_from_image(memnew(Image(_uwp_logo)));
+#ifdef MODULE_SVG_ENABLED
+	Ref<Image> img = memnew(Image);
+	const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
+
+	ImageLoaderSVG img_loader;
+	img_loader.create_image_from_string(img, _uwp_logo_svg, EDSCALE, upsample, false);
+
+	logo = ImageTexture::create_from_image(img);
+#endif
 }

BIN
platform/uwp/logo.png


+ 1 - 0
platform/uwp/logo.svg

@@ -0,0 +1 @@
+<svg height="32" width="32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(8.981 1.816)"><circle style="fill:#2f75bb;fill-opacity:1;stroke:none;stroke-width:.815427" cx="7.019" cy="14.184" r="13.825"/><path d="m-1.192 8.234 6.73-.927v6.503h-6.73Zm0 11.899 6.73.927v-6.422h-6.73Zm7.47 1.026 8.952 1.236v-7.757H6.278Zm0-13.951v6.602h8.952V5.973Z" fill="#00abed" style="fill:#fff;fill-opacity:1"/></g></svg>

+ 19 - 2
platform/web/export/export_plugin.cpp

@@ -31,7 +31,15 @@
 #include "export_plugin.h"
 
 #include "core/config/project_settings.h"
+#include "editor/editor_scale.h"
 #include "editor/editor_settings.h"
+#include "platform/web/logo_svg.gen.h"
+#include "platform/web/run_icon_svg.gen.h"
+
+#include "modules/modules_enabled.gen.h" // For svg.
+#ifdef MODULE_SVG_ENABLED
+#include "modules/svg/image_loader_svg.h"
+#endif
 
 Error EditorExportPlatformWeb::_extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa) {
 	Ref<FileAccess> io_fa;
@@ -651,8 +659,17 @@ EditorExportPlatformWeb::EditorExportPlatformWeb() {
 	server.instantiate();
 	server_thread.start(_server_thread_poll, this);
 
-	logo = ImageTexture::create_from_image(memnew(Image(_web_logo)));
-	run_icon = ImageTexture::create_from_image(memnew(Image(_web_run_icon)));
+#ifdef MODULE_SVG_ENABLED
+	Ref<Image> img = memnew(Image);
+	const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
+
+	ImageLoaderSVG img_loader;
+	img_loader.create_image_from_string(img, _web_logo_svg, EDSCALE, upsample, false);
+	logo = ImageTexture::create_from_image(img);
+
+	img_loader.create_image_from_string(img, _web_run_icon_svg, EDSCALE, upsample, false);
+	run_icon = ImageTexture::create_from_image(img);
+#endif
 
 	Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
 	if (theme.is_valid()) {

+ 1 - 4
platform/web/export/export_plugin.h

@@ -38,11 +38,8 @@
 #include "core/io/zip_io.h"
 #include "editor/editor_node.h"
 #include "editor/export/editor_export_platform.h"
-#include "main/splash.gen.h"
-#include "platform/web/logo.gen.h"
-#include "platform/web/run_icon.gen.h"
-
 #include "editor_http_server.h"
+#include "main/splash.gen.h"
 
 class EditorExportPlatformWeb : public EditorExportPlatform {
 	GDCLASS(EditorExportPlatformWeb, EditorExportPlatform);

BIN
platform/web/logo.png


+ 1 - 0
platform/web/logo.svg

@@ -0,0 +1 @@
+<svg height="32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="M7 5h18v21H7z" fill="#fff"/><path d="M3.143 1 5.48 27.504 15.967 31l10.553-3.496L28.857 1zM23.78 9.565H11.473l.275 3.308h11.759l-.911 9.937-6.556 1.808v.02h-.073l-6.61-1.828-.402-5.076h3.195l.234 2.552 3.583.97 3.595-.97.402-4.165H8.788L7.93 6.37h16.145z" fill="#eb6428"/></svg>

BIN
platform/web/run_icon.png


+ 1 - 0
platform/web/run_icon.svg

@@ -0,0 +1 @@
+<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M3.143 1 5.48 27.504 15.967 31l10.553-3.496L28.857 1ZM23.78 9.565H11.473l.275 3.308h11.759l-.911 9.937-6.556 1.808v.02h-.073l-6.61-1.828-.402-5.076h3.195l.234 2.552 3.583.97 3.595-.97.402-4.165H8.788L7.93 6.37h16.145Z" fill="#eb6428" style="fill:#e0e0e0;fill-opacity:1" transform="translate(.586 .586) scale(.46337)"/></svg>

+ 0 - 1
platform/windows/export/export.cpp

@@ -51,7 +51,6 @@ void register_windows_exporter() {
 
 	Ref<EditorExportPlatformWindows> platform;
 	platform.instantiate();
-	platform->set_logo(ImageTexture::create_from_image(memnew(Image(_windows_logo))));
 	platform->set_name("Windows Desktop");
 	platform->set_os_name("Windows");
 

+ 312 - 4
platform/windows/export/export_plugin.cpp

@@ -34,6 +34,14 @@
 #include "core/io/image_loader.h"
 #include "editor/editor_node.h"
 #include "editor/editor_paths.h"
+#include "editor/editor_scale.h"
+#include "platform/windows/logo_svg.gen.h"
+#include "platform/windows/run_icon_svg.gen.h"
+
+#include "modules/modules_enabled.gen.h" // For svg.
+#ifdef MODULE_SVG_ENABLED
+#include "modules/svg/image_loader_svg.h"
+#endif
 
 Error EditorExportPlatformWindows::_process_icon(const Ref<EditorExportPreset> &p_preset, const String &p_src_path, const String &p_dst_path) {
 	static const uint8_t icon_size[] = { 16, 32, 48, 64, 128, 0 /*256*/ };
@@ -168,9 +176,39 @@ Error EditorExportPlatformWindows::modify_template(const Ref<EditorExportPreset>
 }
 
 Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
-	String pck_path = p_path;
-	if (p_preset->get("binary_format/embed_pck")) {
-		pck_path = p_path.get_basename() + ".tmp";
+	bool export_as_zip = p_path.ends_with("zip");
+	bool embedded = p_preset->get("binary_format/embed_pck");
+
+	String pkg_name;
+	if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") {
+		pkg_name = String(ProjectSettings::get_singleton()->get("application/config/name"));
+	} else {
+		pkg_name = "Unnamed";
+	}
+
+	pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);
+
+	// Setup temp folder.
+	String path = p_path;
+	String tmp_dir_path = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name);
+	Ref<DirAccess> tmp_app_dir = DirAccess::create_for_path(tmp_dir_path);
+	if (export_as_zip) {
+		if (tmp_app_dir.is_null()) {
+			return ERR_CANT_CREATE;
+		}
+		if (DirAccess::exists(tmp_dir_path)) {
+			if (tmp_app_dir->change_dir(tmp_dir_path) == OK) {
+				tmp_app_dir->erase_contents_recursive();
+			}
+		}
+		tmp_app_dir->make_dir_recursive(tmp_dir_path);
+		path = tmp_dir_path.path_join(p_path.get_file().get_basename() + ".exe");
+	}
+
+	// Export project.
+	String pck_path = path;
+	if (embedded) {
+		pck_path = pck_path.get_basename() + ".tmp";
 	}
 	Error err = EditorExportPlatformPC::export_project(p_preset, p_debug, pck_path, p_flags);
 	if (p_preset->get("codesign/enable") && err == OK) {
@@ -181,7 +219,7 @@ Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset>
 		}
 	}
 
-	if (p_preset->get("binary_format/embed_pck") && err == OK) {
+	if (embedded && err == OK) {
 		Ref<DirAccess> tmp_dir = DirAccess::create_for_path(p_path.get_base_dir());
 		err = tmp_dir->rename(pck_path, p_path);
 		if (err != OK) {
@@ -189,6 +227,27 @@ Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset>
 		}
 	}
 
+	// ZIP project.
+	if (export_as_zip) {
+		if (FileAccess::exists(p_path)) {
+			OS::get_singleton()->move_to_trash(p_path);
+		}
+
+		Ref<FileAccess> io_fa_dst;
+		zlib_filefunc_def io_dst = zipio_create_io(&io_fa_dst);
+		zipFile zip = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io_dst);
+
+		zip_folder_recursive(zip, tmp_dir_path, "", pkg_name);
+
+		zipClose(zip, nullptr);
+
+		if (tmp_app_dir->change_dir(tmp_dir_path) == OK) {
+			tmp_app_dir->erase_contents_recursive();
+			tmp_app_dir->change_dir("..");
+			tmp_app_dir->remove(pkg_name);
+		}
+	}
+
 	return err;
 }
 
@@ -199,6 +258,7 @@ String EditorExportPlatformWindows::get_template_file_name(const String &p_targe
 List<String> EditorExportPlatformWindows::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {
 	List<String> list;
 	list.push_back("exe");
+	list.push_back("zip");
 	return list;
 }
 
@@ -212,6 +272,7 @@ bool EditorExportPlatformWindows::get_export_option_visibility(const EditorExpor
 
 void EditorExportPlatformWindows::get_export_options(List<ExportOption> *r_options) {
 	EditorExportPlatformPC::get_export_options(r_options);
+
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "binary_format/architecture", PROPERTY_HINT_ENUM, "x86_64,x86_32,arm64"), "x86_64"));
 
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/enable"), false));
@@ -235,6 +296,29 @@ void EditorExportPlatformWindows::get_export_options(List<ExportOption> *r_optio
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/file_description"), ""));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), ""));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/trademarks"), ""));
+
+	String run_script = "Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'\n"
+						"$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'\n"
+						"$trigger = New-ScheduledTaskTrigger -Once -At 00:00\n"
+						"$settings = New-ScheduledTaskSettingsSet\n"
+						"$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings\n"
+						"Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true\n"
+						"Start-ScheduledTask -TaskName godot_remote_debug\n"
+						"while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }\n"
+						"Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue";
+
+	String cleanup_script = "Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue\n"
+							"Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue\n"
+							"Remove-Item -Recurse -Force '{temp_dir}'";
+
+	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/host"), "user@host_ip"));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/port"), "22"));
+
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_ssh", PROPERTY_HINT_MULTILINE_TEXT), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_scp", PROPERTY_HINT_MULTILINE_TEXT), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/run_script", PROPERTY_HINT_MULTILINE_TEXT), run_script));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/cleanup_script", PROPERTY_HINT_MULTILINE_TEXT), cleanup_script));
 }
 
 Error EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path, bool p_console_icon) {
@@ -659,3 +743,227 @@ Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int6
 	}
 	return OK;
 }
+
+Ref<Texture2D> EditorExportPlatformWindows::get_run_icon() const {
+	return run_icon;
+}
+
+bool EditorExportPlatformWindows::poll_export() {
+	Ref<EditorExportPreset> preset;
+
+	for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
+		Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i);
+		if (ep->is_runnable() && ep->get_platform() == this) {
+			preset = ep;
+			break;
+		}
+	}
+
+	int prev = menu_options;
+	menu_options = (preset.is_valid() && preset->get("ssh_remote_deploy/enabled").operator bool());
+	if (ssh_pid != 0 || !cleanup_commands.is_empty()) {
+		if (menu_options == 0) {
+			cleanup();
+		} else {
+			menu_options += 1;
+		}
+	}
+	return menu_options != prev;
+}
+
+Ref<ImageTexture> EditorExportPlatformWindows::get_option_icon(int p_index) const {
+	return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index);
+}
+
+int EditorExportPlatformWindows::get_options_count() const {
+	return menu_options;
+}
+
+String EditorExportPlatformWindows::get_option_label(int p_index) const {
+	return (p_index) ? TTR("Stop and uninstall") : TTR("Run on remote Windows system");
+}
+
+String EditorExportPlatformWindows::get_option_tooltip(int p_index) const {
+	return (p_index) ? TTR("Stop and uninstall running project from the remote system") : TTR("Run exported project on remote Windows system");
+}
+
+void EditorExportPlatformWindows::cleanup() {
+	if (ssh_pid != 0 && OS::get_singleton()->is_process_running(ssh_pid)) {
+		print_line("Terminating connection...");
+		OS::get_singleton()->kill(ssh_pid);
+		OS::get_singleton()->delay_usec(1000);
+	}
+
+	if (!cleanup_commands.is_empty()) {
+		print_line("Stopping and deleting previous version...");
+		for (const SSHCleanupCommand &cmd : cleanup_commands) {
+			if (cmd.wait) {
+				ssh_run_on_remote(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);
+			} else {
+				ssh_run_on_remote_no_wait(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);
+			}
+		}
+	}
+	ssh_pid = 0;
+	cleanup_commands.clear();
+}
+
+Error EditorExportPlatformWindows::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) {
+	cleanup();
+	if (p_device) { // Stop command, cleanup only.
+		return OK;
+	}
+
+	EditorProgress ep("run", TTR("Running..."), 5);
+
+	const String dest = EditorPaths::get_singleton()->get_cache_dir().path_join("windows");
+	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+	if (!da->dir_exists(dest)) {
+		Error err = da->make_dir_recursive(dest);
+		if (err != OK) {
+			EditorNode::get_singleton()->show_warning(TTR("Could not create temp directory:") + "\n" + dest);
+			return err;
+		}
+	}
+
+	String host = p_preset->get("ssh_remote_deploy/host").operator String();
+	String port = p_preset->get("ssh_remote_deploy/port").operator String();
+	if (port.is_empty()) {
+		port = "22";
+	}
+	Vector<String> extra_args_ssh = p_preset->get("ssh_remote_deploy/extra_args_ssh").operator String().split(" ");
+	Vector<String> extra_args_scp = p_preset->get("ssh_remote_deploy/extra_args_scp").operator String().split(" ");
+
+	const String basepath = dest.path_join("tmp_windows_export");
+
+#define CLEANUP_AND_RETURN(m_err)                       \
+	{                                                   \
+		if (da->file_exists(basepath + ".zip")) {       \
+			da->remove(basepath + ".zip");              \
+		}                                               \
+		if (da->file_exists(basepath + "_start.ps1")) { \
+			da->remove(basepath + "_start.ps1");        \
+		}                                               \
+		if (da->file_exists(basepath + "_clean.ps1")) { \
+			da->remove(basepath + "_clean.ps1");        \
+		}                                               \
+		return m_err;                                   \
+	}                                                   \
+	((void)0)
+
+	if (ep.step(TTR("Exporting project..."), 1)) {
+		return ERR_SKIP;
+	}
+	Error err = export_project(p_preset, true, basepath + ".zip", p_debug_flags);
+	if (err != OK) {
+		DirAccess::remove_file_or_error(basepath + ".zip");
+		return err;
+	}
+
+	String cmd_args;
+	{
+		Vector<String> cmd_args_list;
+		gen_debug_flags(cmd_args_list, p_debug_flags);
+		for (int i = 0; i < cmd_args_list.size(); i++) {
+			if (i != 0) {
+				cmd_args += " ";
+			}
+			cmd_args += cmd_args_list[i];
+		}
+	}
+
+	const bool use_remote = (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) || (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT);
+	int dbg_port = EditorSettings::get_singleton()->get("network/debug/remote_port");
+
+	print_line("Creating temporary directory...");
+	ep.step(TTR("Creating temporary directory..."), 2);
+	String temp_dir;
+	err = ssh_run_on_remote(host, port, extra_args_ssh, "powershell -command \\\"\\$tmp = Join-Path \\$Env:Temp \\$(New-Guid); New-Item -Type Directory -Path \\$tmp | Out-Null; Write-Output \\$tmp\\\"", &temp_dir);
+	if (err != OK || temp_dir.is_empty()) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	print_line("Uploading archive...");
+	ep.step(TTR("Uploading archive..."), 3);
+	err = ssh_push_to_remote(host, port, extra_args_scp, basepath + ".zip", temp_dir);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	{
+		String run_script = p_preset->get("ssh_remote_deploy/run_script");
+		run_script = run_script.replace("{temp_dir}", temp_dir);
+		run_script = run_script.replace("{archive_name}", basepath.get_file() + ".zip");
+		run_script = run_script.replace("{exe_name}", basepath.get_file() + ".exe");
+		run_script = run_script.replace("{cmd_args}", cmd_args);
+
+		Ref<FileAccess> f = FileAccess::open(basepath + "_start.ps1", FileAccess::WRITE);
+		if (f.is_null()) {
+			CLEANUP_AND_RETURN(err);
+		}
+
+		f->store_string(run_script);
+	}
+
+	{
+		String clean_script = p_preset->get("ssh_remote_deploy/cleanup_script");
+		clean_script = clean_script.replace("{temp_dir}", temp_dir);
+		clean_script = clean_script.replace("{archive_name}", basepath.get_file() + ".zip");
+		clean_script = clean_script.replace("{exe_name}", basepath.get_file() + ".exe");
+		clean_script = clean_script.replace("{cmd_args}", cmd_args);
+
+		Ref<FileAccess> f = FileAccess::open(basepath + "_clean.ps1", FileAccess::WRITE);
+		if (f.is_null()) {
+			CLEANUP_AND_RETURN(err);
+		}
+
+		f->store_string(clean_script);
+	}
+
+	print_line("Uploading scripts...");
+	ep.step(TTR("Uploading scripts..."), 4);
+	err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_start.ps1", temp_dir);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+	err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_clean.ps1", temp_dir);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	print_line("Starting project...");
+	ep.step(TTR("Starting project..."), 5);
+	err = ssh_run_on_remote_no_wait(host, port, extra_args_ssh, vformat("powershell -file \"%s\\%s\"", temp_dir, basepath.get_file() + "_start.ps1"), &ssh_pid, (use_remote) ? dbg_port : -1);
+	if (err != OK) {
+		CLEANUP_AND_RETURN(err);
+	}
+
+	cleanup_commands.clear();
+	cleanup_commands.push_back(SSHCleanupCommand(host, port, extra_args_ssh, vformat("powershell -file \"%s\\%s\"", temp_dir, basepath.get_file() + "_clean.ps1")));
+
+	print_line("Project started.");
+
+	CLEANUP_AND_RETURN(OK);
+#undef CLEANUP_AND_RETURN
+}
+
+EditorExportPlatformWindows::EditorExportPlatformWindows() {
+#ifdef MODULE_SVG_ENABLED
+	Ref<Image> img = memnew(Image);
+	const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
+
+	ImageLoaderSVG img_loader;
+	img_loader.create_image_from_string(img, _windows_logo_svg, EDSCALE, upsample, false);
+	set_logo(ImageTexture::create_from_image(img));
+
+	img_loader.create_image_from_string(img, _windows_run_icon_svg, EDSCALE, upsample, false);
+	run_icon = ImageTexture::create_from_image(img);
+#endif
+
+	Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
+	if (theme.is_valid()) {
+		stop_icon = theme->get_icon(SNAME("Stop"), SNAME("EditorIcons"));
+	} else {
+		stop_icon.instantiate();
+	}
+}

+ 35 - 1
platform/windows/export/export_plugin.h

@@ -35,9 +35,32 @@
 #include "core/os/os.h"
 #include "editor/editor_settings.h"
 #include "editor/export/editor_export_platform_pc.h"
-#include "platform/windows/logo.gen.h"
 
 class EditorExportPlatformWindows : public EditorExportPlatformPC {
+	struct SSHCleanupCommand {
+		String host;
+		String port;
+		Vector<String> ssh_args;
+		String cmd_args;
+		bool wait = false;
+
+		SSHCleanupCommand(){};
+		SSHCleanupCommand(const String &p_host, const String &p_port, const Vector<String> &p_ssh_arg, const String &p_cmd_args, bool p_wait = false) {
+			host = p_host;
+			port = p_port;
+			ssh_args = p_ssh_arg;
+			cmd_args = p_cmd_args;
+			wait = p_wait;
+		};
+	};
+
+	Ref<ImageTexture> run_icon;
+	Ref<ImageTexture> stop_icon;
+
+	Vector<SSHCleanupCommand> cleanup_commands;
+	OS::ProcessID ssh_pid = 0;
+	int menu_options = 0;
+
 	Error _process_icon(const Ref<EditorExportPreset> &p_preset, const String &p_src_path, const String &p_dst_path);
 	Error _rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path, bool p_console_icon);
 	Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path);
@@ -53,6 +76,17 @@ public:
 	virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option, const HashMap<StringName, Variant> &p_options) const override;
 	virtual String get_template_file_name(const String &p_target, const String &p_arch) const override;
 	virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) override;
+
+	virtual Ref<Texture2D> get_run_icon() const override;
+	virtual bool poll_export() override;
+	virtual Ref<ImageTexture> get_option_icon(int p_index) const override;
+	virtual int get_options_count() const override;
+	virtual String get_option_label(int p_index) const override;
+	virtual String get_option_tooltip(int p_index) const override;
+	virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override;
+	virtual void cleanup() override;
+
+	EditorExportPlatformWindows();
 };
 
 #endif // WINDOWS_EXPORT_PLUGIN_H

BIN
platform/windows/logo.png


+ 1 - 0
platform/windows/logo.svg

@@ -0,0 +1 @@
+<svg height="32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m1 5.132 12.295-1.694v11.879H1zm0 21.736 12.295 1.695V16.83H1zm13.647 1.875L31 31V16.83H14.647zm0-25.486v12.06H31V1z" fill="#00abed"/></svg>

+ 1 - 0
platform/windows/run_icon.svg

@@ -0,0 +1 @@
+<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m1.095 2.997 5.66-.78v5.469h-5.66zm0 10.006 5.66.78v-5.4h-5.66zm6.282.863 7.528 1.04V8.381H7.377Zm0-11.732v5.552h7.528V1.095Z" fill="#00abed" style="stroke-width:.460341;fill:#e0e0e0;fill-opacity:1"/></svg>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä