1/**************************************************************************/
2/* editor_export_platform.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 "editor_export_platform.h"
32
33#include "core/config/project_settings.h"
34#include "core/crypto/crypto_core.h"
35#include "core/extension/gdextension.h"
36#include "core/io/file_access_encrypted.h"
37#include "core/io/file_access_pack.h" // PACK_HEADER_MAGIC, PACK_FORMAT_VERSION
38#include "core/io/zip_io.h"
39#include "core/version.h"
40#include "editor/editor_file_system.h"
41#include "editor/editor_node.h"
42#include "editor/editor_paths.h"
43#include "editor/editor_scale.h"
44#include "editor/editor_settings.h"
45#include "editor/editor_string_names.h"
46#include "editor/export/editor_export.h"
47#include "editor/plugins/script_editor_plugin.h"
48#include "editor_export_plugin.h"
49#include "scene/resources/image_texture.h"
50#include "scene/resources/packed_scene.h"
51
52static int _get_pad(int p_alignment, int p_n) {
53 int rest = p_n % p_alignment;
54 int pad = 0;
55 if (rest > 0) {
56 pad = p_alignment - rest;
57 };
58
59 return pad;
60}
61
62#define PCK_PADDING 16
63
64bool EditorExportPlatform::fill_log_messages(RichTextLabel *p_log, Error p_err) {
65 bool has_messages = false;
66
67 int msg_count = get_message_count();
68
69 p_log->add_text(TTR("Project export for platform:") + " ");
70 p_log->add_image(get_logo(), 16 * EDSCALE, 16 * EDSCALE, Color(1.0, 1.0, 1.0), INLINE_ALIGNMENT_CENTER);
71 p_log->add_text(" ");
72 p_log->add_text(get_name());
73 p_log->add_text(" - ");
74 if (p_err == OK) {
75 if (get_worst_message_type() >= EditorExportPlatform::EXPORT_MESSAGE_WARNING) {
76 p_log->add_image(p_log->get_editor_theme_icon(SNAME("StatusWarning")), 16 * EDSCALE, 16 * EDSCALE, Color(1.0, 1.0, 1.0), INLINE_ALIGNMENT_CENTER);
77 p_log->add_text(" ");
78 p_log->add_text(TTR("Completed with warnings."));
79 has_messages = true;
80 } else {
81 p_log->add_image(p_log->get_editor_theme_icon(SNAME("StatusSuccess")), 16 * EDSCALE, 16 * EDSCALE, Color(1.0, 1.0, 1.0), INLINE_ALIGNMENT_CENTER);
82 p_log->add_text(" ");
83 p_log->add_text(TTR("Completed successfully."));
84 if (msg_count > 0) {
85 has_messages = true;
86 }
87 }
88 } else {
89 p_log->add_image(p_log->get_editor_theme_icon(SNAME("StatusError")), 16 * EDSCALE, 16 * EDSCALE, Color(1.0, 1.0, 1.0), INLINE_ALIGNMENT_CENTER);
90 p_log->add_text(" ");
91 p_log->add_text(TTR("Failed."));
92 has_messages = true;
93 }
94 p_log->add_newline();
95
96 if (msg_count) {
97 p_log->push_table(2);
98 p_log->set_table_column_expand(0, false);
99 p_log->set_table_column_expand(1, true);
100 for (int m = 0; m < msg_count; m++) {
101 EditorExportPlatform::ExportMessage msg = get_message(m);
102 Color color = p_log->get_theme_color(SNAME("font_color"), SNAME("Label"));
103 Ref<Texture> icon;
104
105 switch (msg.msg_type) {
106 case EditorExportPlatform::EXPORT_MESSAGE_INFO: {
107 color = p_log->get_theme_color(SNAME("font_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.6);
108 } break;
109 case EditorExportPlatform::EXPORT_MESSAGE_WARNING: {
110 icon = p_log->get_editor_theme_icon(SNAME("Warning"));
111 color = p_log->get_theme_color(SNAME("warning_color"), EditorStringName(Editor));
112 } break;
113 case EditorExportPlatform::EXPORT_MESSAGE_ERROR: {
114 icon = p_log->get_editor_theme_icon(SNAME("Error"));
115 color = p_log->get_theme_color(SNAME("error_color"), EditorStringName(Editor));
116 } break;
117 default:
118 break;
119 }
120
121 p_log->push_cell();
122 p_log->add_text("\t");
123 if (icon.is_valid()) {
124 p_log->add_image(icon);
125 }
126 p_log->pop();
127
128 p_log->push_cell();
129 p_log->push_color(color);
130 p_log->add_text(vformat("[%s]: %s", msg.category, msg.text));
131 p_log->pop();
132 p_log->pop();
133 }
134 p_log->pop();
135 p_log->add_newline();
136 }
137 p_log->add_newline();
138 return has_messages;
139}
140
141void EditorExportPlatform::gen_debug_flags(Vector<String> &r_flags, int p_flags) {
142 String host = EDITOR_GET("network/debug/remote_host");
143 int remote_port = (int)EDITOR_GET("network/debug/remote_port");
144
145 if (EditorSettings::get_singleton()->has_setting("export/android/use_wifi_for_remote_debug") && EDITOR_GET("export/android/use_wifi_for_remote_debug")) {
146 host = EDITOR_GET("export/android/wifi_remote_debug_host");
147 } else if (p_flags & DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST) {
148 host = "localhost";
149 }
150
151 if (p_flags & DEBUG_FLAG_DUMB_CLIENT) {
152 int port = EDITOR_GET("filesystem/file_server/port");
153 String passwd = EDITOR_GET("filesystem/file_server/password");
154 r_flags.push_back("--remote-fs");
155 r_flags.push_back(host + ":" + itos(port));
156 if (!passwd.is_empty()) {
157 r_flags.push_back("--remote-fs-password");
158 r_flags.push_back(passwd);
159 }
160 }
161
162 if (p_flags & DEBUG_FLAG_REMOTE_DEBUG) {
163 r_flags.push_back("--remote-debug");
164
165 r_flags.push_back(get_debug_protocol() + host + ":" + String::num(remote_port));
166
167 List<String> breakpoints;
168 ScriptEditor::get_singleton()->get_breakpoints(&breakpoints);
169
170 if (breakpoints.size()) {
171 r_flags.push_back("--breakpoints");
172 String bpoints;
173 for (const List<String>::Element *E = breakpoints.front(); E; E = E->next()) {
174 bpoints += E->get().replace(" ", "%20");
175 if (E->next()) {
176 bpoints += ",";
177 }
178 }
179
180 r_flags.push_back(bpoints);
181 }
182 }
183
184 if (p_flags & DEBUG_FLAG_VIEW_COLLISIONS) {
185 r_flags.push_back("--debug-collisions");
186 }
187
188 if (p_flags & DEBUG_FLAG_VIEW_NAVIGATION) {
189 r_flags.push_back("--debug-navigation");
190 }
191}
192
193Error EditorExportPlatform::_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) {
194 ERR_FAIL_COND_V_MSG(p_total < 1, ERR_PARAMETER_RANGE_ERROR, "Must select at least one file to export.");
195
196 PackData *pd = (PackData *)p_userdata;
197
198 SavedData sd;
199 sd.path_utf8 = p_path.utf8();
200 sd.ofs = pd->f->get_position();
201 sd.size = p_data.size();
202 sd.encrypted = false;
203
204 for (int i = 0; i < p_enc_in_filters.size(); ++i) {
205 if (p_path.matchn(p_enc_in_filters[i]) || p_path.replace("res://", "").matchn(p_enc_in_filters[i])) {
206 sd.encrypted = true;
207 break;
208 }
209 }
210
211 for (int i = 0; i < p_enc_ex_filters.size(); ++i) {
212 if (p_path.matchn(p_enc_ex_filters[i]) || p_path.replace("res://", "").matchn(p_enc_ex_filters[i])) {
213 sd.encrypted = false;
214 break;
215 }
216 }
217
218 Ref<FileAccessEncrypted> fae;
219 Ref<FileAccess> ftmp = pd->f;
220
221 if (sd.encrypted) {
222 fae.instantiate();
223 ERR_FAIL_COND_V(fae.is_null(), ERR_SKIP);
224
225 Error err = fae->open_and_parse(ftmp, p_key, FileAccessEncrypted::MODE_WRITE_AES256, false);
226 ERR_FAIL_COND_V(err != OK, ERR_SKIP);
227 ftmp = fae;
228 }
229
230 // Store file content.
231 ftmp->store_buffer(p_data.ptr(), p_data.size());
232
233 if (fae.is_valid()) {
234 ftmp.unref();
235 fae.unref();
236 }
237
238 int pad = _get_pad(PCK_PADDING, pd->f->get_position());
239 for (int i = 0; i < pad; i++) {
240 pd->f->store_8(0);
241 }
242
243 // Store MD5 of original file.
244 {
245 unsigned char hash[16];
246 CryptoCore::md5(p_data.ptr(), p_data.size(), hash);
247 sd.md5.resize(16);
248 for (int i = 0; i < 16; i++) {
249 sd.md5.write[i] = hash[i];
250 }
251 }
252
253 pd->file_ofs.push_back(sd);
254
255 // TRANSLATORS: This is an editor progress label describing the storing of a file.
256 if (pd->ep->step(vformat(TTR("Storing File: %s"), p_path), 2 + p_file * 100 / p_total, false)) {
257 return ERR_SKIP;
258 }
259
260 return OK;
261}
262
263Error EditorExportPlatform::_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) {
264 ERR_FAIL_COND_V_MSG(p_total < 1, ERR_PARAMETER_RANGE_ERROR, "Must select at least one file to export.");
265
266 String path = p_path.replace_first("res://", "");
267
268 ZipData *zd = (ZipData *)p_userdata;
269
270 zipFile zip = (zipFile)zd->zip;
271
272 zipOpenNewFileInZip(zip,
273 path.utf8().get_data(),
274 nullptr,
275 nullptr,
276 0,
277 nullptr,
278 0,
279 nullptr,
280 Z_DEFLATED,
281 Z_DEFAULT_COMPRESSION);
282
283 zipWriteInFileInZip(zip, p_data.ptr(), p_data.size());
284 zipCloseFileInZip(zip);
285
286 if (zd->ep->step(TTR("Storing File:") + " " + p_path, 2 + p_file * 100 / p_total, false)) {
287 return ERR_SKIP;
288 }
289
290 return OK;
291}
292
293Ref<ImageTexture> EditorExportPlatform::get_option_icon(int p_index) const {
294 Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
295 ERR_FAIL_COND_V(theme.is_null(), Ref<ImageTexture>());
296 if (EditorNode::get_singleton()->get_main_screen_control()->is_layout_rtl()) {
297 return theme->get_icon(SNAME("PlayBackwards"), EditorStringName(EditorIcons));
298 } else {
299 return theme->get_icon(SNAME("Play"), EditorStringName(EditorIcons));
300 }
301}
302
303String EditorExportPlatform::find_export_template(String template_file_name, String *err) const {
304 String current_version = VERSION_FULL_CONFIG;
305 String template_path = EditorPaths::get_singleton()->get_export_templates_dir().path_join(current_version).path_join(template_file_name);
306
307 if (FileAccess::exists(template_path)) {
308 return template_path;
309 }
310
311 // Not found
312 if (err) {
313 *err += TTR("No export template found at the expected path:") + "\n" + template_path + "\n";
314 }
315 return String();
316}
317
318bool EditorExportPlatform::exists_export_template(String template_file_name, String *err) const {
319 return find_export_template(template_file_name, err) != "";
320}
321
322Ref<EditorExportPreset> EditorExportPlatform::create_preset() {
323 Ref<EditorExportPreset> preset;
324 preset.instantiate();
325 preset->platform = Ref<EditorExportPlatform>(this);
326
327 List<ExportOption> options;
328 get_export_options(&options);
329
330 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
331 for (int i = 0; i < export_plugins.size(); i++) {
332 export_plugins.write[i]->_get_export_options(Ref<EditorExportPlatform>(this), &options);
333 }
334
335 for (const ExportOption &E : options) {
336 StringName option_name = E.option.name;
337 preset->properties[option_name] = E.option;
338 preset->values[option_name] = E.default_value;
339 preset->update_visibility[option_name] = E.update_visibility;
340 }
341
342 return preset;
343}
344
345void EditorExportPlatform::_export_find_resources(EditorFileSystemDirectory *p_dir, HashSet<String> &p_paths) {
346 for (int i = 0; i < p_dir->get_subdir_count(); i++) {
347 _export_find_resources(p_dir->get_subdir(i), p_paths);
348 }
349
350 for (int i = 0; i < p_dir->get_file_count(); i++) {
351 if (p_dir->get_file_type(i) == "TextFile") {
352 continue;
353 }
354 p_paths.insert(p_dir->get_file_path(i));
355 }
356}
357
358void EditorExportPlatform::_export_find_customized_resources(const Ref<EditorExportPreset> &p_preset, EditorFileSystemDirectory *p_dir, EditorExportPreset::FileExportMode p_mode, HashSet<String> &p_paths) {
359 for (int i = 0; i < p_dir->get_subdir_count(); i++) {
360 EditorFileSystemDirectory *subdir = p_dir->get_subdir(i);
361 _export_find_customized_resources(p_preset, subdir, p_preset->get_file_export_mode(subdir->get_path(), p_mode), p_paths);
362 }
363
364 for (int i = 0; i < p_dir->get_file_count(); i++) {
365 if (p_dir->get_file_type(i) == "TextFile") {
366 continue;
367 }
368 String path = p_dir->get_file_path(i);
369 EditorExportPreset::FileExportMode file_mode = p_preset->get_file_export_mode(path, p_mode);
370 if (file_mode != EditorExportPreset::MODE_FILE_REMOVE) {
371 p_paths.insert(path);
372 }
373 }
374}
375
376void EditorExportPlatform::_export_find_dependencies(const String &p_path, HashSet<String> &p_paths) {
377 if (p_paths.has(p_path)) {
378 return;
379 }
380
381 p_paths.insert(p_path);
382
383 EditorFileSystemDirectory *dir;
384 int file_idx;
385 dir = EditorFileSystem::get_singleton()->find_file(p_path, &file_idx);
386 if (!dir) {
387 return;
388 }
389
390 Vector<String> deps = dir->get_file_deps(file_idx);
391
392 for (int i = 0; i < deps.size(); i++) {
393 _export_find_dependencies(deps[i], p_paths);
394 }
395}
396
397void EditorExportPlatform::_edit_files_with_filter(Ref<DirAccess> &da, const Vector<String> &p_filters, HashSet<String> &r_list, bool exclude) {
398 da->list_dir_begin();
399 String cur_dir = da->get_current_dir().replace("\\", "/");
400 if (!cur_dir.ends_with("/")) {
401 cur_dir += "/";
402 }
403 String cur_dir_no_prefix = cur_dir.replace("res://", "");
404
405 Vector<String> dirs;
406 String f = da->get_next();
407 while (!f.is_empty()) {
408 if (da->current_is_dir()) {
409 dirs.push_back(f);
410 } else {
411 String fullpath = cur_dir + f;
412 // Test also against path without res:// so that filters like `file.txt` can work.
413 String fullpath_no_prefix = cur_dir_no_prefix + f;
414 for (int i = 0; i < p_filters.size(); ++i) {
415 if (fullpath.matchn(p_filters[i]) || fullpath_no_prefix.matchn(p_filters[i])) {
416 if (!exclude) {
417 r_list.insert(fullpath);
418 } else {
419 r_list.erase(fullpath);
420 }
421 }
422 }
423 }
424 f = da->get_next();
425 }
426
427 da->list_dir_end();
428
429 for (int i = 0; i < dirs.size(); ++i) {
430 String dir = dirs[i];
431 if (dir.begins_with(".")) {
432 continue;
433 }
434
435 if (EditorFileSystem::_should_skip_directory(cur_dir + dir)) {
436 continue;
437 }
438
439 da->change_dir(dir);
440 _edit_files_with_filter(da, p_filters, r_list, exclude);
441 da->change_dir("..");
442 }
443}
444
445void EditorExportPlatform::_edit_filter_list(HashSet<String> &r_list, const String &p_filter, bool exclude) {
446 if (p_filter.is_empty()) {
447 return;
448 }
449 Vector<String> split = p_filter.split(",");
450 Vector<String> filters;
451 for (int i = 0; i < split.size(); i++) {
452 String f = split[i].strip_edges();
453 if (f.is_empty()) {
454 continue;
455 }
456 filters.push_back(f);
457 }
458
459 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
460 ERR_FAIL_COND(da.is_null());
461 _edit_files_with_filter(da, filters, r_list, exclude);
462}
463
464HashSet<String> EditorExportPlatform::get_features(const Ref<EditorExportPreset> &p_preset, bool p_debug) const {
465 Ref<EditorExportPlatform> platform = p_preset->get_platform();
466 List<String> feature_list;
467 platform->get_platform_features(&feature_list);
468 platform->get_preset_features(p_preset, &feature_list);
469
470 HashSet<String> result;
471 for (const String &E : feature_list) {
472 result.insert(E);
473 }
474
475 result.insert("template");
476 if (p_debug) {
477 result.insert("debug");
478 result.insert("template_debug");
479 } else {
480 result.insert("release");
481 result.insert("template_release");
482 }
483
484 if (!p_preset->get_custom_features().is_empty()) {
485 Vector<String> tmp_custom_list = p_preset->get_custom_features().split(",");
486
487 for (int i = 0; i < tmp_custom_list.size(); i++) {
488 String f = tmp_custom_list[i].strip_edges();
489 if (!f.is_empty()) {
490 result.insert(f);
491 }
492 }
493 }
494
495 return result;
496}
497
498EditorExportPlatform::ExportNotifier::ExportNotifier(EditorExportPlatform &p_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
499 HashSet<String> features = p_platform.get_features(p_preset, p_debug);
500 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
501 //initial export plugin callback
502 for (int i = 0; i < export_plugins.size(); i++) {
503 export_plugins.write[i]->set_export_preset(p_preset);
504 if (GDVIRTUAL_IS_OVERRIDDEN_PTR(export_plugins[i], _export_begin)) {
505 PackedStringArray features_psa;
506 for (const String &feature : features) {
507 features_psa.push_back(feature);
508 }
509 export_plugins.write[i]->_export_begin_script(features_psa, p_debug, p_path, p_flags);
510 } else {
511 export_plugins.write[i]->_export_begin(features, p_debug, p_path, p_flags);
512 }
513 }
514}
515
516EditorExportPlatform::ExportNotifier::~ExportNotifier() {
517 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
518 for (int i = 0; i < export_plugins.size(); i++) {
519 if (GDVIRTUAL_IS_OVERRIDDEN_PTR(export_plugins[i], _export_end)) {
520 export_plugins.write[i]->_export_end_script();
521 }
522 export_plugins.write[i]->_export_end();
523 export_plugins.write[i]->set_export_preset(Ref<EditorExportPlugin>());
524 }
525}
526
527bool EditorExportPlatform::_export_customize_dictionary(Dictionary &dict, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins) {
528 bool changed = false;
529
530 List<Variant> keys;
531 dict.get_key_list(&keys);
532 for (const Variant &K : keys) {
533 Variant v = dict[K];
534 switch (v.get_type()) {
535 case Variant::OBJECT: {
536 Ref<Resource> res = v;
537 if (res.is_valid()) {
538 for (Ref<EditorExportPlugin> &plugin : customize_resources_plugins) {
539 Ref<Resource> new_res = plugin->_customize_resource(res, "");
540 if (new_res.is_valid()) {
541 changed = true;
542 if (new_res != res) {
543 dict[K] = new_res;
544 res = new_res;
545 }
546 break;
547 }
548 }
549
550 // If it was not replaced, go through and see if there is something to replace.
551 if (res.is_valid() && !res->get_path().is_resource_file() && _export_customize_object(res.ptr(), customize_resources_plugins), true) {
552 changed = true;
553 }
554 }
555
556 } break;
557 case Variant::DICTIONARY: {
558 Dictionary d = v;
559 if (_export_customize_dictionary(d, customize_resources_plugins)) {
560 changed = true;
561 }
562 } break;
563 case Variant::ARRAY: {
564 Array a = v;
565 if (_export_customize_array(a, customize_resources_plugins)) {
566 changed = true;
567 }
568 } break;
569 default: {
570 }
571 }
572 }
573 return changed;
574}
575
576bool EditorExportPlatform::_export_customize_array(Array &arr, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins) {
577 bool changed = false;
578
579 for (int i = 0; i < arr.size(); i++) {
580 Variant v = arr.get(i);
581 switch (v.get_type()) {
582 case Variant::OBJECT: {
583 Ref<Resource> res = v;
584 if (res.is_valid()) {
585 for (Ref<EditorExportPlugin> &plugin : customize_resources_plugins) {
586 Ref<Resource> new_res = plugin->_customize_resource(res, "");
587 if (new_res.is_valid()) {
588 changed = true;
589 if (new_res != res) {
590 arr.set(i, new_res);
591 res = new_res;
592 }
593 break;
594 }
595 }
596
597 // If it was not replaced, go through and see if there is something to replace.
598 if (res.is_valid() && !res->get_path().is_resource_file() && _export_customize_object(res.ptr(), customize_resources_plugins), true) {
599 changed = true;
600 }
601 }
602 } break;
603 case Variant::DICTIONARY: {
604 Dictionary d = v;
605 if (_export_customize_dictionary(d, customize_resources_plugins)) {
606 changed = true;
607 }
608 } break;
609 case Variant::ARRAY: {
610 Array a = v;
611 if (_export_customize_array(a, customize_resources_plugins)) {
612 changed = true;
613 }
614 } break;
615 default: {
616 }
617 }
618 }
619 return changed;
620}
621
622bool EditorExportPlatform::_export_customize_object(Object *p_object, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins) {
623 bool changed = false;
624
625 List<PropertyInfo> props;
626 p_object->get_property_list(&props);
627 for (const PropertyInfo &E : props) {
628 switch (E.type) {
629 case Variant::OBJECT: {
630 Ref<Resource> res = p_object->get(E.name);
631 if (res.is_valid()) {
632 for (Ref<EditorExportPlugin> &plugin : customize_resources_plugins) {
633 Ref<Resource> new_res = plugin->_customize_resource(res, "");
634 if (new_res.is_valid()) {
635 changed = true;
636 if (new_res != res) {
637 p_object->set(E.name, new_res);
638 res = new_res;
639 }
640 break;
641 }
642 }
643
644 // If it was not replaced, go through and see if there is something to replace.
645 if (res.is_valid() && !res->get_path().is_resource_file() && _export_customize_object(res.ptr(), customize_resources_plugins), true) {
646 changed = true;
647 }
648 }
649
650 } break;
651 case Variant::DICTIONARY: {
652 Dictionary d = p_object->get(E.name);
653 if (_export_customize_dictionary(d, customize_resources_plugins)) {
654 // May have been generated, so set back just in case
655 p_object->set(E.name, d);
656 changed = true;
657 }
658 } break;
659 case Variant::ARRAY: {
660 Array a = p_object->get(E.name);
661 if (_export_customize_array(a, customize_resources_plugins)) {
662 // May have been generated, so set back just in case
663 p_object->set(E.name, a);
664 changed = true;
665 }
666 } break;
667 default: {
668 }
669 }
670 }
671 return changed;
672}
673
674bool EditorExportPlatform::_is_editable_ancestor(Node *p_root, Node *p_node) {
675 while (p_node != nullptr && p_node != p_root) {
676 if (p_root->is_editable_instance(p_node)) {
677 return true;
678 }
679 p_node = p_node->get_owner();
680 }
681 return false;
682}
683
684bool EditorExportPlatform::_export_customize_scene_resources(Node *p_root, Node *p_node, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins) {
685 bool changed = false;
686
687 if (p_root == p_node || p_node->get_owner() == p_root || _is_editable_ancestor(p_root, p_node)) {
688 if (_export_customize_object(p_node, customize_resources_plugins)) {
689 changed = true;
690 }
691 }
692
693 for (int i = 0; i < p_node->get_child_count(); i++) {
694 if (_export_customize_scene_resources(p_root, p_node->get_child(i), customize_resources_plugins)) {
695 changed = true;
696 }
697 }
698
699 return changed;
700}
701
702String EditorExportPlatform::_export_customize(const String &p_path, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins, LocalVector<Ref<EditorExportPlugin>> &customize_scenes_plugins, HashMap<String, FileExportCache> &export_cache, const String &export_base_path, bool p_force_save) {
703 if (!p_force_save && customize_resources_plugins.is_empty() && customize_scenes_plugins.is_empty()) {
704 return p_path; // do none
705 }
706
707 // Check if a cache exists
708 if (export_cache.has(p_path)) {
709 FileExportCache &fec = export_cache[p_path];
710
711 if (fec.saved_path.is_empty() || FileAccess::exists(fec.saved_path)) {
712 // Destination file exists (was not erased) or not needed
713
714 uint64_t mod_time = FileAccess::get_modified_time(p_path);
715 if (fec.source_modified_time == mod_time) {
716 // Cached (modified time matches).
717 fec.used = true;
718 return fec.saved_path.is_empty() ? p_path : fec.saved_path;
719 }
720
721 String md5 = FileAccess::get_md5(p_path);
722 if (FileAccess::exists(p_path + ".import")) {
723 // Also consider the import file in the string
724 md5 += FileAccess::get_md5(p_path + ".import");
725 }
726 if (fec.source_md5 == md5) {
727 // Cached (md5 matches).
728 fec.source_modified_time = mod_time;
729 fec.used = true;
730 return fec.saved_path.is_empty() ? p_path : fec.saved_path;
731 }
732 }
733 }
734
735 FileExportCache fec;
736 fec.used = true;
737 fec.source_modified_time = FileAccess::get_modified_time(p_path);
738
739 String md5 = FileAccess::get_md5(p_path);
740 if (FileAccess::exists(p_path + ".import")) {
741 // Also consider the import file in the string
742 md5 += FileAccess::get_md5(p_path + ".import");
743 }
744
745 fec.source_md5 = md5;
746
747 // Check if it should convert
748
749 String type = ResourceLoader::get_resource_type(p_path);
750
751 bool modified = false;
752
753 String save_path;
754
755 if (type == "PackedScene") { // Its a scene.
756 Ref<PackedScene> ps = ResourceLoader::load(p_path, "PackedScene", ResourceFormatLoader::CACHE_MODE_IGNORE);
757 ERR_FAIL_COND_V(ps.is_null(), p_path);
758 Node *node = ps->instantiate(PackedScene::GEN_EDIT_STATE_INSTANCE); // Make sure the child scene root gets the correct inheritance chain.
759 ERR_FAIL_NULL_V(node, p_path);
760 if (!customize_scenes_plugins.is_empty()) {
761 for (Ref<EditorExportPlugin> &plugin : customize_scenes_plugins) {
762 Node *customized = plugin->_customize_scene(node, p_path);
763 if (customized != nullptr) {
764 node = customized;
765 modified = true;
766 }
767 }
768 }
769 if (!customize_resources_plugins.is_empty()) {
770 if (_export_customize_scene_resources(node, node, customize_resources_plugins)) {
771 modified = true;
772 }
773 }
774
775 if (modified || p_force_save) {
776 // If modified, save it again. This is also used for TSCN -> SCN conversion on export.
777
778 String base_file = p_path.get_file().get_basename() + ".scn"; // use SCN for saving (binary) and repack (If conversting, TSCN PackedScene representation is inefficient, so repacking is also desired).
779 save_path = export_base_path.path_join("export-" + p_path.md5_text() + "-" + base_file);
780
781 Ref<PackedScene> s;
782 s.instantiate();
783 s->pack(node);
784 Error err = ResourceSaver::save(s, save_path);
785 ERR_FAIL_COND_V_MSG(err != OK, p_path, "Unable to save export scene file to: " + save_path);
786 }
787 } else {
788 Ref<Resource> res = ResourceLoader::load(p_path, "", ResourceFormatLoader::CACHE_MODE_IGNORE);
789 ERR_FAIL_COND_V(res.is_null(), p_path);
790
791 if (!customize_resources_plugins.is_empty()) {
792 for (Ref<EditorExportPlugin> &plugin : customize_resources_plugins) {
793 Ref<Resource> new_res = plugin->_customize_resource(res, p_path);
794 if (new_res.is_valid()) {
795 modified = true;
796 if (new_res != res) {
797 res = new_res;
798 }
799 break;
800 }
801 }
802
803 if (_export_customize_object(res.ptr(), customize_resources_plugins)) {
804 modified = true;
805 }
806 }
807
808 if (modified || p_force_save) {
809 // If modified, save it again. This is also used for TRES -> RES conversion on export.
810
811 String base_file = p_path.get_file().get_basename() + ".res"; // use RES for saving (binary)
812 save_path = export_base_path.path_join("export-" + p_path.md5_text() + "-" + base_file);
813
814 Error err = ResourceSaver::save(res, save_path);
815 ERR_FAIL_COND_V_MSG(err != OK, p_path, "Unable to save export resource file to: " + save_path);
816 }
817 }
818
819 fec.saved_path = save_path;
820
821 export_cache[p_path] = fec;
822
823 return save_path.is_empty() ? p_path : save_path;
824}
825
826String EditorExportPlatform::_get_script_encryption_key(const Ref<EditorExportPreset> &p_preset) const {
827 const String from_env = OS::get_singleton()->get_environment(ENV_SCRIPT_ENCRYPTION_KEY);
828 if (!from_env.is_empty()) {
829 return from_env.to_lower();
830 }
831 return p_preset->get_script_encryption_key().to_lower();
832}
833
834Vector<String> EditorExportPlatform::get_forced_export_files() {
835 Vector<String> files;
836
837 files.push_back(ProjectSettings::get_singleton()->get_global_class_list_path());
838
839 String icon = GLOBAL_GET("application/config/icon");
840 String splash = GLOBAL_GET("application/boot_splash/image");
841 if (!icon.is_empty() && FileAccess::exists(icon)) {
842 files.push_back(icon);
843 }
844 if (!splash.is_empty() && FileAccess::exists(splash) && icon != splash) {
845 files.push_back(splash);
846 }
847 String resource_cache_file = ResourceUID::get_cache_file();
848 if (FileAccess::exists(resource_cache_file)) {
849 files.push_back(resource_cache_file);
850 }
851
852 String extension_list_config_file = GDExtension::get_extension_list_config_file();
853 if (FileAccess::exists(extension_list_config_file)) {
854 files.push_back(extension_list_config_file);
855 }
856
857 // Store text server data if it is supported.
858 if (TS->has_feature(TextServer::FEATURE_USE_SUPPORT_DATA)) {
859 bool use_data = GLOBAL_GET("internationalization/locale/include_text_server_data");
860 if (use_data) {
861 // Try using user provided data file.
862 String ts_data = "res://" + TS->get_support_data_filename();
863 if (FileAccess::exists(ts_data)) {
864 files.push_back(ts_data);
865 } else {
866 // Use default text server data.
867 String icu_data_file = EditorPaths::get_singleton()->get_cache_dir().path_join("tmp_icu_data");
868 ERR_FAIL_COND_V(!TS->save_support_data(icu_data_file), files);
869 files.push_back(icu_data_file);
870 // Remove the file later.
871 MessageQueue::get_singleton()->push_callable(callable_mp_static(DirAccess::remove_absolute), icu_data_file);
872 }
873 }
874 }
875
876 return files;
877}
878
879Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> &p_preset, bool p_debug, EditorExportSaveFunction p_func, void *p_udata, EditorExportSaveSharedObject p_so_func) {
880 //figure out paths of files that will be exported
881 HashSet<String> paths;
882 Vector<String> path_remaps;
883
884 if (p_preset->get_export_filter() == EditorExportPreset::EXPORT_ALL_RESOURCES) {
885 //find stuff
886 _export_find_resources(EditorFileSystem::get_singleton()->get_filesystem(), paths);
887 } else if (p_preset->get_export_filter() == EditorExportPreset::EXCLUDE_SELECTED_RESOURCES) {
888 _export_find_resources(EditorFileSystem::get_singleton()->get_filesystem(), paths);
889 Vector<String> files = p_preset->get_files_to_export();
890 for (int i = 0; i < files.size(); i++) {
891 paths.erase(files[i]);
892 }
893 } else if (p_preset->get_export_filter() == EditorExportPreset::EXPORT_CUSTOMIZED) {
894 _export_find_customized_resources(p_preset, EditorFileSystem::get_singleton()->get_filesystem(), p_preset->get_file_export_mode("res://"), paths);
895 } else {
896 bool scenes_only = p_preset->get_export_filter() == EditorExportPreset::EXPORT_SELECTED_SCENES;
897
898 Vector<String> files = p_preset->get_files_to_export();
899 for (int i = 0; i < files.size(); i++) {
900 if (scenes_only && ResourceLoader::get_resource_type(files[i]) != "PackedScene") {
901 continue;
902 }
903
904 _export_find_dependencies(files[i], paths);
905 }
906
907 // Add autoload resources and their dependencies
908 List<PropertyInfo> props;
909 ProjectSettings::get_singleton()->get_property_list(&props);
910
911 for (const PropertyInfo &pi : props) {
912 if (!pi.name.begins_with("autoload/")) {
913 continue;
914 }
915
916 String autoload_path = GLOBAL_GET(pi.name);
917
918 if (autoload_path.begins_with("*")) {
919 autoload_path = autoload_path.substr(1);
920 }
921
922 _export_find_dependencies(autoload_path, paths);
923 }
924 }
925
926 //add native icons to non-resource include list
927 _edit_filter_list(paths, String("*.icns"), false);
928 _edit_filter_list(paths, String("*.ico"), false);
929
930 _edit_filter_list(paths, p_preset->get_include_filter(), false);
931 _edit_filter_list(paths, p_preset->get_exclude_filter(), true);
932
933 // Ignore import files, since these are automatically added to the jar later with the resources
934 _edit_filter_list(paths, String("*.import"), true);
935
936 // Get encryption filters.
937 bool enc_pck = p_preset->get_enc_pck();
938 Vector<String> enc_in_filters;
939 Vector<String> enc_ex_filters;
940 Vector<uint8_t> key;
941
942 if (enc_pck) {
943 Vector<String> enc_in_split = p_preset->get_enc_in_filter().split(",");
944 for (int i = 0; i < enc_in_split.size(); i++) {
945 String f = enc_in_split[i].strip_edges();
946 if (f.is_empty()) {
947 continue;
948 }
949 enc_in_filters.push_back(f);
950 }
951
952 Vector<String> enc_ex_split = p_preset->get_enc_ex_filter().split(",");
953 for (int i = 0; i < enc_ex_split.size(); i++) {
954 String f = enc_ex_split[i].strip_edges();
955 if (f.is_empty()) {
956 continue;
957 }
958 enc_ex_filters.push_back(f);
959 }
960
961 // Get encryption key.
962 String script_key = _get_script_encryption_key(p_preset);
963 key.resize(32);
964 if (script_key.length() == 64) {
965 for (int i = 0; i < 32; i++) {
966 int v = 0;
967 if (i * 2 < script_key.length()) {
968 char32_t ct = script_key[i * 2];
969 if (is_digit(ct)) {
970 ct = ct - '0';
971 } else if (ct >= 'a' && ct <= 'f') {
972 ct = 10 + ct - 'a';
973 }
974 v |= ct << 4;
975 }
976
977 if (i * 2 + 1 < script_key.length()) {
978 char32_t ct = script_key[i * 2 + 1];
979 if (is_digit(ct)) {
980 ct = ct - '0';
981 } else if (ct >= 'a' && ct <= 'f') {
982 ct = 10 + ct - 'a';
983 }
984 v |= ct;
985 }
986 key.write[i] = v;
987 }
988 }
989 }
990
991 Error err = OK;
992 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
993
994 struct SortByName {
995 bool operator()(const Ref<EditorExportPlugin> &left, const Ref<EditorExportPlugin> &right) const {
996 return left->get_name() < right->get_name();
997 }
998 };
999
1000 // Always sort by name, to so if for some reason they are re-arranged, it still works.
1001 export_plugins.sort_custom<SortByName>();
1002
1003 for (int i = 0; i < export_plugins.size(); i++) {
1004 if (p_so_func) {
1005 for (int j = 0; j < export_plugins[i]->shared_objects.size(); j++) {
1006 err = p_so_func(p_udata, export_plugins[i]->shared_objects[j]);
1007 if (err != OK) {
1008 return err;
1009 }
1010 }
1011 }
1012 for (int j = 0; j < export_plugins[i]->extra_files.size(); j++) {
1013 err = p_func(p_udata, export_plugins[i]->extra_files[j].path, export_plugins[i]->extra_files[j].data, 0, paths.size(), enc_in_filters, enc_ex_filters, key);
1014 if (err != OK) {
1015 return err;
1016 }
1017 }
1018
1019 export_plugins.write[i]->_clear();
1020 }
1021
1022 HashSet<String> features = get_features(p_preset, p_debug);
1023 PackedStringArray features_psa;
1024 for (const String &feature : features) {
1025 features_psa.push_back(feature);
1026 }
1027
1028 // Check if custom processing is needed
1029 uint32_t custom_resources_hash = HASH_MURMUR3_SEED;
1030 uint32_t custom_scene_hash = HASH_MURMUR3_SEED;
1031
1032 LocalVector<Ref<EditorExportPlugin>> customize_resources_plugins;
1033 LocalVector<Ref<EditorExportPlugin>> customize_scenes_plugins;
1034
1035 for (int i = 0; i < export_plugins.size(); i++) {
1036 if (export_plugins.write[i]->_begin_customize_resources(Ref<EditorExportPlatform>(this), features_psa)) {
1037 customize_resources_plugins.push_back(export_plugins[i]);
1038
1039 custom_resources_hash = hash_murmur3_one_64(export_plugins[i]->get_name().hash64(), custom_resources_hash);
1040 uint64_t hash = export_plugins[i]->_get_customization_configuration_hash();
1041 custom_resources_hash = hash_murmur3_one_64(hash, custom_resources_hash);
1042 }
1043 if (export_plugins.write[i]->_begin_customize_scenes(Ref<EditorExportPlatform>(this), features_psa)) {
1044 customize_scenes_plugins.push_back(export_plugins[i]);
1045
1046 custom_resources_hash = hash_murmur3_one_64(export_plugins[i]->get_name().hash64(), custom_resources_hash);
1047 uint64_t hash = export_plugins[i]->_get_customization_configuration_hash();
1048 custom_scene_hash = hash_murmur3_one_64(hash, custom_scene_hash);
1049 }
1050 }
1051
1052 HashMap<String, FileExportCache> export_cache;
1053 String export_base_path = ProjectSettings::get_singleton()->get_project_data_path().path_join("exported/") + itos(custom_resources_hash);
1054
1055 bool convert_text_to_binary = GLOBAL_GET("editor/export/convert_text_resources_to_binary");
1056
1057 if (convert_text_to_binary || !customize_resources_plugins.is_empty() || !customize_scenes_plugins.is_empty()) {
1058 // See if we have something to open
1059 Ref<FileAccess> f = FileAccess::open(export_base_path.path_join("file_cache"), FileAccess::READ);
1060 if (f.is_valid()) {
1061 String l = f->get_line();
1062 while (l != String()) {
1063 Vector<String> fields = l.split("::");
1064 if (fields.size() == 4) {
1065 FileExportCache fec;
1066 String path = fields[0];
1067 fec.source_md5 = fields[1].strip_edges();
1068 fec.source_modified_time = fields[2].strip_edges().to_int();
1069 fec.saved_path = fields[3];
1070 fec.used = false; // Assume unused until used.
1071 export_cache[path] = fec;
1072 }
1073 l = f->get_line();
1074 }
1075 } else {
1076 // create the path
1077 Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1078 d->change_dir(ProjectSettings::get_singleton()->get_project_data_path());
1079 d->make_dir_recursive("exported/" + itos(custom_resources_hash));
1080 }
1081 }
1082
1083 //store everything in the export medium
1084 int idx = 0;
1085 int total = paths.size();
1086
1087 for (const String &E : paths) {
1088 String path = E;
1089 String type = ResourceLoader::get_resource_type(path);
1090
1091 if (FileAccess::exists(path + ".import")) {
1092 // Before doing this, try to see if it can be customized.
1093
1094 String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, false);
1095
1096 if (export_path != path) {
1097 // It was actually customized.
1098 // Since the original file is likely not recognized, just use the import system.
1099
1100 Ref<ConfigFile> config;
1101 config.instantiate();
1102 err = config->load(path + ".import");
1103 if (err != OK) {
1104 ERR_PRINT("Could not parse: '" + path + "', not exported.");
1105 continue;
1106 }
1107 config->set_value("remap", "type", ResourceLoader::get_resource_type(export_path));
1108
1109 // Erase all Paths.
1110 List<String> keys;
1111 config->get_section_keys("remap", &keys);
1112 for (const String &K : keys) {
1113 if (K.begins_with("path")) {
1114 config->erase_section_key("remap", K);
1115 }
1116 }
1117 // Set actual converted path.
1118 config->set_value("remap", "path", export_path);
1119
1120 // Erase useless sections.
1121 config->erase_section("deps");
1122 config->erase_section("params");
1123
1124 String import_text = config->encode_to_text();
1125 CharString cs = import_text.utf8();
1126 Vector<uint8_t> sarr;
1127 sarr.resize(cs.size());
1128 memcpy(sarr.ptrw(), cs.ptr(), sarr.size());
1129
1130 err = p_func(p_udata, path + ".import", sarr, idx, total, enc_in_filters, enc_ex_filters, key);
1131 if (err != OK) {
1132 return err;
1133 }
1134 // Now actual remapped file:
1135 sarr = FileAccess::get_file_as_bytes(export_path);
1136 err = p_func(p_udata, export_path, sarr, idx, total, enc_in_filters, enc_ex_filters, key);
1137 if (err != OK) {
1138 return err;
1139 }
1140 } else {
1141 // File is imported and not customized, replace by what it imports.
1142 Ref<ConfigFile> config;
1143 config.instantiate();
1144 err = config->load(path + ".import");
1145 if (err != OK) {
1146 ERR_PRINT("Could not parse: '" + path + "', not exported.");
1147 continue;
1148 }
1149
1150 String importer_type = config->get_value("remap", "importer");
1151
1152 if (importer_type == "keep") {
1153 // Just keep file as-is.
1154 Vector<uint8_t> array = FileAccess::get_file_as_bytes(path);
1155 err = p_func(p_udata, path, array, idx, total, enc_in_filters, enc_ex_filters, key);
1156
1157 if (err != OK) {
1158 return err;
1159 }
1160
1161 continue;
1162 }
1163
1164 List<String> remaps;
1165 config->get_section_keys("remap", &remaps);
1166
1167 HashSet<String> remap_features;
1168
1169 for (const String &F : remaps) {
1170 String remap = F;
1171 String feature = remap.get_slice(".", 1);
1172 if (features.has(feature)) {
1173 remap_features.insert(feature);
1174 }
1175 }
1176
1177 if (remap_features.size() > 1) {
1178 this->resolve_platform_feature_priorities(p_preset, remap_features);
1179 }
1180
1181 err = OK;
1182
1183 for (const String &F : remaps) {
1184 String remap = F;
1185 if (remap == "path") {
1186 String remapped_path = config->get_value("remap", remap);
1187 Vector<uint8_t> array = FileAccess::get_file_as_bytes(remapped_path);
1188 err = p_func(p_udata, remapped_path, array, idx, total, enc_in_filters, enc_ex_filters, key);
1189 } else if (remap.begins_with("path.")) {
1190 String feature = remap.get_slice(".", 1);
1191
1192 if (remap_features.has(feature)) {
1193 String remapped_path = config->get_value("remap", remap);
1194 Vector<uint8_t> array = FileAccess::get_file_as_bytes(remapped_path);
1195 err = p_func(p_udata, remapped_path, array, idx, total, enc_in_filters, enc_ex_filters, key);
1196 } else {
1197 // Remove paths if feature not enabled.
1198 config->erase_section_key("remap", remap);
1199 }
1200 }
1201 }
1202
1203 if (err != OK) {
1204 return err;
1205 }
1206
1207 // Erase useless sections.
1208 config->erase_section("deps");
1209 config->erase_section("params");
1210
1211 String import_text = config->encode_to_text();
1212 CharString cs = import_text.utf8();
1213 Vector<uint8_t> sarr;
1214 sarr.resize(cs.size());
1215 memcpy(sarr.ptrw(), cs.ptr(), sarr.size());
1216
1217 err = p_func(p_udata, path + ".import", sarr, idx, total, enc_in_filters, enc_ex_filters, key);
1218
1219 if (err != OK) {
1220 return err;
1221 }
1222 }
1223
1224 } else {
1225 // Customize.
1226
1227 bool do_export = true;
1228 for (int i = 0; i < export_plugins.size(); i++) {
1229 if (GDVIRTUAL_IS_OVERRIDDEN_PTR(export_plugins[i], _export_file)) {
1230 export_plugins.write[i]->_export_file_script(path, type, features_psa);
1231 } else {
1232 export_plugins.write[i]->_export_file(path, type, features);
1233 }
1234 if (p_so_func) {
1235 for (int j = 0; j < export_plugins[i]->shared_objects.size(); j++) {
1236 err = p_so_func(p_udata, export_plugins[i]->shared_objects[j]);
1237 if (err != OK) {
1238 return err;
1239 }
1240 }
1241 }
1242
1243 for (int j = 0; j < export_plugins[i]->extra_files.size(); j++) {
1244 err = p_func(p_udata, export_plugins[i]->extra_files[j].path, export_plugins[i]->extra_files[j].data, idx, total, enc_in_filters, enc_ex_filters, key);
1245 if (err != OK) {
1246 return err;
1247 }
1248 if (export_plugins[i]->extra_files[j].remap) {
1249 do_export = false; //if remap, do not
1250 path_remaps.push_back(path);
1251 path_remaps.push_back(export_plugins[i]->extra_files[j].path);
1252 }
1253 }
1254
1255 if (export_plugins[i]->skipped) {
1256 do_export = false;
1257 }
1258 export_plugins.write[i]->_clear();
1259
1260 if (!do_export) {
1261 break; //apologies, not exporting
1262 }
1263 }
1264 //just store it as it comes
1265 if (do_export) {
1266 // Customization only happens if plugins did not take care of it before
1267 bool force_binary = convert_text_to_binary && (path.get_extension().to_lower() == "tres" || path.get_extension().to_lower() == "tscn");
1268 String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, force_binary);
1269
1270 if (export_path != path) {
1271 // Add a remap entry
1272 path_remaps.push_back(path);
1273 path_remaps.push_back(export_path);
1274 }
1275
1276 Vector<uint8_t> array = FileAccess::get_file_as_bytes(export_path);
1277 err = p_func(p_udata, export_path, array, idx, total, enc_in_filters, enc_ex_filters, key);
1278 if (err != OK) {
1279 return err;
1280 }
1281 }
1282 }
1283
1284 idx++;
1285 }
1286
1287 if (convert_text_to_binary || !customize_resources_plugins.is_empty() || !customize_scenes_plugins.is_empty()) {
1288 // End scene customization
1289
1290 String fcache = export_base_path.path_join("file_cache");
1291 Ref<FileAccess> f = FileAccess::open(fcache, FileAccess::WRITE);
1292
1293 if (f.is_valid()) {
1294 for (const KeyValue<String, FileExportCache> &E : export_cache) {
1295 if (E.value.used) { // May be old, unused
1296 String l = E.key + "::" + E.value.source_md5 + "::" + itos(E.value.source_modified_time) + "::" + E.value.saved_path;
1297 f->store_line(l);
1298 }
1299 }
1300 } else {
1301 ERR_PRINT("Error opening export file cache: " + fcache);
1302 }
1303
1304 for (Ref<EditorExportPlugin> &plugin : customize_resources_plugins) {
1305 plugin->_end_customize_resources();
1306 }
1307
1308 for (Ref<EditorExportPlugin> &plugin : customize_scenes_plugins) {
1309 plugin->_end_customize_scenes();
1310 }
1311 }
1312 //save config!
1313
1314 Vector<String> custom_list;
1315
1316 if (!p_preset->get_custom_features().is_empty()) {
1317 Vector<String> tmp_custom_list = p_preset->get_custom_features().split(",");
1318
1319 for (int i = 0; i < tmp_custom_list.size(); i++) {
1320 String f = tmp_custom_list[i].strip_edges();
1321 if (!f.is_empty()) {
1322 custom_list.push_back(f);
1323 }
1324 }
1325 }
1326 for (int i = 0; i < export_plugins.size(); i++) {
1327 custom_list.append_array(export_plugins[i]->_get_export_features(Ref<EditorExportPlatform>(this), p_debug));
1328 }
1329
1330 ProjectSettings::CustomMap custom_map;
1331 if (path_remaps.size()) {
1332 if (true) { //new remap mode, use always as it's friendlier with multiple .pck exports
1333 for (int i = 0; i < path_remaps.size(); i += 2) {
1334 String from = path_remaps[i];
1335 String to = path_remaps[i + 1];
1336 String remap_file = "[remap]\n\npath=\"" + to.c_escape() + "\"\n";
1337 CharString utf8 = remap_file.utf8();
1338 Vector<uint8_t> new_file;
1339 new_file.resize(utf8.length());
1340 for (int j = 0; j < utf8.length(); j++) {
1341 new_file.write[j] = utf8[j];
1342 }
1343
1344 err = p_func(p_udata, from + ".remap", new_file, idx, total, enc_in_filters, enc_ex_filters, key);
1345 if (err != OK) {
1346 return err;
1347 }
1348 }
1349 } else {
1350 //old remap mode, will still work, but it's unused because it's not multiple pck export friendly
1351 custom_map["path_remap/remapped_paths"] = path_remaps;
1352 }
1353 }
1354
1355 Vector<String> forced_export = get_forced_export_files();
1356 for (int i = 0; i < forced_export.size(); i++) {
1357 Vector<uint8_t> array = FileAccess::get_file_as_bytes(forced_export[i]);
1358 err = p_func(p_udata, forced_export[i], array, idx, total, enc_in_filters, enc_ex_filters, key);
1359 if (err != OK) {
1360 return err;
1361 }
1362 }
1363
1364 String config_file = "project.binary";
1365 String engine_cfb = EditorPaths::get_singleton()->get_cache_dir().path_join("tmp" + config_file);
1366 ProjectSettings::get_singleton()->save_custom(engine_cfb, custom_map, custom_list);
1367 Vector<uint8_t> data = FileAccess::get_file_as_bytes(engine_cfb);
1368 DirAccess::remove_file_or_error(engine_cfb);
1369
1370 return p_func(p_udata, "res://" + config_file, data, idx, total, enc_in_filters, enc_ex_filters, key);
1371}
1372
1373Error EditorExportPlatform::_add_shared_object(void *p_userdata, const SharedObject &p_so) {
1374 PackData *pack_data = (PackData *)p_userdata;
1375 if (pack_data->so_files) {
1376 pack_data->so_files->push_back(p_so);
1377 }
1378
1379 return OK;
1380}
1381
1382void EditorExportPlatform::zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name) {
1383 String dir = p_folder.is_empty() ? p_root_path : p_root_path.path_join(p_folder);
1384
1385 Ref<DirAccess> da = DirAccess::open(dir);
1386 da->list_dir_begin();
1387 String f = da->get_next();
1388 while (!f.is_empty()) {
1389 if (f == "." || f == "..") {
1390 f = da->get_next();
1391 continue;
1392 }
1393 if (da->is_link(f)) {
1394 OS::DateTime dt = OS::get_singleton()->get_datetime();
1395
1396 zip_fileinfo zipfi;
1397 zipfi.tmz_date.tm_year = dt.year;
1398 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/
1399 zipfi.tmz_date.tm_mday = dt.day;
1400 zipfi.tmz_date.tm_hour = dt.hour;
1401 zipfi.tmz_date.tm_min = dt.minute;
1402 zipfi.tmz_date.tm_sec = dt.second;
1403 zipfi.dosDate = 0;
1404 // 0120000: symbolic link type
1405 // 0000755: permissions rwxr-xr-x
1406 // 0000644: permissions rw-r--r--
1407 uint32_t _mode = 0120644;
1408 zipfi.external_fa = (_mode << 16L) | !(_mode & 0200);
1409 zipfi.internal_fa = 0;
1410
1411 zipOpenNewFileInZip4(p_zip,
1412 p_folder.path_join(f).utf8().get_data(),
1413 &zipfi,
1414 nullptr,
1415 0,
1416 nullptr,
1417 0,
1418 nullptr,
1419 Z_DEFLATED,
1420 Z_DEFAULT_COMPRESSION,
1421 0,
1422 -MAX_WBITS,
1423 DEF_MEM_LEVEL,
1424 Z_DEFAULT_STRATEGY,
1425 nullptr,
1426 0,
1427 0x0314, // "version made by", 0x03 - Unix, 0x14 - ZIP specification version 2.0, required to store Unix file permissions
1428 0);
1429
1430 String target = da->read_link(f);
1431 zipWriteInFileInZip(p_zip, target.utf8().get_data(), target.utf8().size());
1432 zipCloseFileInZip(p_zip);
1433 } else if (da->current_is_dir()) {
1434 zip_folder_recursive(p_zip, p_root_path, p_folder.path_join(f), p_pkg_name);
1435 } else {
1436 bool _is_executable = is_executable(dir.path_join(f));
1437
1438 OS::DateTime dt = OS::get_singleton()->get_datetime();
1439
1440 zip_fileinfo zipfi;
1441 zipfi.tmz_date.tm_year = dt.year;
1442 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/
1443 zipfi.tmz_date.tm_mday = dt.day;
1444 zipfi.tmz_date.tm_hour = dt.hour;
1445 zipfi.tmz_date.tm_min = dt.minute;
1446 zipfi.tmz_date.tm_sec = dt.second;
1447 zipfi.dosDate = 0;
1448 // 0100000: regular file type
1449 // 0000755: permissions rwxr-xr-x
1450 // 0000644: permissions rw-r--r--
1451 uint32_t _mode = (_is_executable ? 0100755 : 0100644);
1452 zipfi.external_fa = (_mode << 16L) | !(_mode & 0200);
1453 zipfi.internal_fa = 0;
1454
1455 zipOpenNewFileInZip4(p_zip,
1456 p_folder.path_join(f).utf8().get_data(),
1457 &zipfi,
1458 nullptr,
1459 0,
1460 nullptr,
1461 0,
1462 nullptr,
1463 Z_DEFLATED,
1464 Z_DEFAULT_COMPRESSION,
1465 0,
1466 -MAX_WBITS,
1467 DEF_MEM_LEVEL,
1468 Z_DEFAULT_STRATEGY,
1469 nullptr,
1470 0,
1471 0x0314, // "version made by", 0x03 - Unix, 0x14 - ZIP specification version 2.0, required to store Unix file permissions
1472 0);
1473
1474 Ref<FileAccess> fa = FileAccess::open(dir.path_join(f), FileAccess::READ);
1475 if (fa.is_null()) {
1476 add_message(EXPORT_MESSAGE_ERROR, TTR("ZIP Creation"), vformat(TTR("Could not open file to read from path \"%s\"."), dir.path_join(f)));
1477 return;
1478 }
1479 const int bufsize = 16384;
1480 uint8_t buf[bufsize];
1481
1482 while (true) {
1483 uint64_t got = fa->get_buffer(buf, bufsize);
1484 if (got == 0) {
1485 break;
1486 }
1487 zipWriteInFileInZip(p_zip, buf, got);
1488 }
1489
1490 zipCloseFileInZip(p_zip);
1491 }
1492 f = da->get_next();
1493 }
1494 da->list_dir_end();
1495}
1496
1497Error 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) {
1498 EditorProgress ep("savepack", TTR("Packing"), 102, true);
1499
1500 // Create the temporary export directory if it doesn't exist.
1501 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
1502 da->make_dir_recursive(EditorPaths::get_singleton()->get_cache_dir());
1503
1504 String tmppath = EditorPaths::get_singleton()->get_cache_dir().path_join("packtmp");
1505 Ref<FileAccess> ftmp = FileAccess::open(tmppath, FileAccess::WRITE);
1506 if (ftmp.is_null()) {
1507 add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), vformat(TTR("Cannot create file \"%s\"."), tmppath));
1508 return ERR_CANT_CREATE;
1509 }
1510
1511 PackData pd;
1512 pd.ep = &ep;
1513 pd.f = ftmp;
1514 pd.so_files = p_so_files;
1515
1516 Error err = export_project_files(p_preset, p_debug, _save_pack_file, &pd, _add_shared_object);
1517
1518 // Close temp file.
1519 pd.f.unref();
1520 ftmp.unref();
1521
1522 if (err != OK) {
1523 DirAccess::remove_file_or_error(tmppath);
1524 add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Failed to export project files."));
1525 return err;
1526 }
1527
1528 pd.file_ofs.sort(); //do sort, so we can do binary search later
1529
1530 Ref<FileAccess> f;
1531 int64_t embed_pos = 0;
1532 if (!p_embed) {
1533 // Regular output to separate PCK file
1534 f = FileAccess::open(p_path, FileAccess::WRITE);
1535 if (f.is_null()) {
1536 DirAccess::remove_file_or_error(tmppath);
1537 add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), vformat(TTR("Can't open file for writing at path \"%s\"."), p_path));
1538 return ERR_CANT_CREATE;
1539 }
1540 } else {
1541 // Append to executable
1542 f = FileAccess::open(p_path, FileAccess::READ_WRITE);
1543 if (f.is_null()) {
1544 DirAccess::remove_file_or_error(tmppath);
1545 add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), vformat(TTR("Can't open file for reading-writing at path \"%s\"."), p_path));
1546 return ERR_FILE_CANT_OPEN;
1547 }
1548
1549 f->seek_end();
1550 embed_pos = f->get_position();
1551
1552 if (r_embedded_start) {
1553 *r_embedded_start = embed_pos;
1554 }
1555
1556 // Ensure embedded PCK starts at a 64-bit multiple
1557 int pad = f->get_position() % 8;
1558 for (int i = 0; i < pad; i++) {
1559 f->store_8(0);
1560 }
1561 }
1562
1563 int64_t pck_start_pos = f->get_position();
1564
1565 f->store_32(PACK_HEADER_MAGIC);
1566 f->store_32(PACK_FORMAT_VERSION);
1567 f->store_32(VERSION_MAJOR);
1568 f->store_32(VERSION_MINOR);
1569 f->store_32(VERSION_PATCH);
1570
1571 uint32_t pack_flags = 0;
1572 bool enc_pck = p_preset->get_enc_pck();
1573 bool enc_directory = p_preset->get_enc_directory();
1574 if (enc_pck && enc_directory) {
1575 pack_flags |= PACK_DIR_ENCRYPTED;
1576 }
1577 f->store_32(pack_flags); // flags
1578
1579 uint64_t file_base_ofs = f->get_position();
1580 f->store_64(0); // files base
1581
1582 for (int i = 0; i < 16; i++) {
1583 //reserved
1584 f->store_32(0);
1585 }
1586
1587 f->store_32(pd.file_ofs.size()); //amount of files
1588
1589 Ref<FileAccessEncrypted> fae;
1590 Ref<FileAccess> fhead = f;
1591
1592 if (enc_pck && enc_directory) {
1593 String script_key = _get_script_encryption_key(p_preset);
1594 Vector<uint8_t> key;
1595 key.resize(32);
1596 if (script_key.length() == 64) {
1597 for (int i = 0; i < 32; i++) {
1598 int v = 0;
1599 if (i * 2 < script_key.length()) {
1600 char32_t ct = script_key[i * 2];
1601 if (is_digit(ct)) {
1602 ct = ct - '0';
1603 } else if (ct >= 'a' && ct <= 'f') {
1604 ct = 10 + ct - 'a';
1605 }
1606 v |= ct << 4;
1607 }
1608
1609 if (i * 2 + 1 < script_key.length()) {
1610 char32_t ct = script_key[i * 2 + 1];
1611 if (is_digit(ct)) {
1612 ct = ct - '0';
1613 } else if (ct >= 'a' && ct <= 'f') {
1614 ct = 10 + ct - 'a';
1615 }
1616 v |= ct;
1617 }
1618 key.write[i] = v;
1619 }
1620 }
1621 fae.instantiate();
1622 if (fae.is_null()) {
1623 add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Can't create encrypted file."));
1624 return ERR_CANT_CREATE;
1625 }
1626
1627 err = fae->open_and_parse(f, key, FileAccessEncrypted::MODE_WRITE_AES256, false);
1628 if (err != OK) {
1629 add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Can't open encrypted file to write."));
1630 return ERR_CANT_CREATE;
1631 }
1632
1633 fhead = fae;
1634 }
1635
1636 for (int i = 0; i < pd.file_ofs.size(); i++) {
1637 uint32_t string_len = pd.file_ofs[i].path_utf8.length();
1638 uint32_t pad = _get_pad(4, string_len);
1639
1640 fhead->store_32(string_len + pad);
1641 fhead->store_buffer((const uint8_t *)pd.file_ofs[i].path_utf8.get_data(), string_len);
1642 for (uint32_t j = 0; j < pad; j++) {
1643 fhead->store_8(0);
1644 }
1645
1646 fhead->store_64(pd.file_ofs[i].ofs);
1647 fhead->store_64(pd.file_ofs[i].size); // pay attention here, this is where file is
1648 fhead->store_buffer(pd.file_ofs[i].md5.ptr(), 16); //also save md5 for file
1649 uint32_t flags = 0;
1650 if (pd.file_ofs[i].encrypted) {
1651 flags |= PACK_FILE_ENCRYPTED;
1652 }
1653 fhead->store_32(flags);
1654 }
1655
1656 if (fae.is_valid()) {
1657 fhead.unref();
1658 fae.unref();
1659 }
1660
1661 int header_padding = _get_pad(PCK_PADDING, f->get_position());
1662 for (int i = 0; i < header_padding; i++) {
1663 f->store_8(0);
1664 }
1665
1666 uint64_t file_base = f->get_position();
1667 f->seek(file_base_ofs);
1668 f->store_64(file_base); // update files base
1669 f->seek(file_base);
1670
1671 // Save the rest of the data.
1672
1673 ftmp = FileAccess::open(tmppath, FileAccess::READ);
1674 if (ftmp.is_null()) {
1675 DirAccess::remove_file_or_error(tmppath);
1676 add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), vformat(TTR("Can't open file to read from path \"%s\"."), tmppath));
1677 return ERR_CANT_CREATE;
1678 }
1679
1680 const int bufsize = 16384;
1681 uint8_t buf[bufsize];
1682
1683 while (true) {
1684 uint64_t got = ftmp->get_buffer(buf, bufsize);
1685 if (got == 0) {
1686 break;
1687 }
1688 f->store_buffer(buf, got);
1689 }
1690
1691 ftmp.unref(); // Close temp file.
1692
1693 if (p_embed) {
1694 // Ensure embedded data ends at a 64-bit multiple
1695 uint64_t embed_end = f->get_position() - embed_pos + 12;
1696 uint64_t pad = embed_end % 8;
1697 for (uint64_t i = 0; i < pad; i++) {
1698 f->store_8(0);
1699 }
1700
1701 uint64_t pck_size = f->get_position() - pck_start_pos;
1702 f->store_64(pck_size);
1703 f->store_32(PACK_HEADER_MAGIC);
1704
1705 if (r_embedded_size) {
1706 *r_embedded_size = f->get_position() - embed_pos;
1707 }
1708 }
1709
1710 DirAccess::remove_file_or_error(tmppath);
1711
1712 return OK;
1713}
1714
1715Error EditorExportPlatform::save_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) {
1716 EditorProgress ep("savezip", TTR("Packing"), 102, true);
1717
1718 Ref<FileAccess> io_fa;
1719 zlib_filefunc_def io = zipio_create_io(&io_fa);
1720 zipFile zip = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io);
1721
1722 ZipData zd;
1723 zd.ep = &ep;
1724 zd.zip = zip;
1725
1726 Error err = export_project_files(p_preset, p_debug, _save_zip_file, &zd);
1727 if (err != OK && err != ERR_SKIP) {
1728 add_message(EXPORT_MESSAGE_ERROR, TTR("Save ZIP"), TTR("Failed to export project files."));
1729 }
1730
1731 zipClose(zip, nullptr);
1732
1733 return OK;
1734}
1735
1736Error EditorExportPlatform::export_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
1737 ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
1738 return save_pack(p_preset, p_debug, p_path);
1739}
1740
1741Error EditorExportPlatform::export_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
1742 ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
1743 return save_zip(p_preset, p_debug, p_path);
1744}
1745
1746void EditorExportPlatform::gen_export_flags(Vector<String> &r_flags, int p_flags) {
1747 String host = EDITOR_GET("network/debug/remote_host");
1748 int remote_port = (int)EDITOR_GET("network/debug/remote_port");
1749
1750 if (p_flags & DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST) {
1751 host = "localhost";
1752 }
1753
1754 if (p_flags & DEBUG_FLAG_DUMB_CLIENT) {
1755 int port = EDITOR_GET("filesystem/file_server/port");
1756 String passwd = EDITOR_GET("filesystem/file_server/password");
1757 r_flags.push_back("--remote-fs");
1758 r_flags.push_back(host + ":" + itos(port));
1759 if (!passwd.is_empty()) {
1760 r_flags.push_back("--remote-fs-password");
1761 r_flags.push_back(passwd);
1762 }
1763 }
1764
1765 if (p_flags & DEBUG_FLAG_REMOTE_DEBUG) {
1766 r_flags.push_back("--remote-debug");
1767
1768 r_flags.push_back(get_debug_protocol() + host + ":" + String::num(remote_port));
1769
1770 List<String> breakpoints;
1771 ScriptEditor::get_singleton()->get_breakpoints(&breakpoints);
1772
1773 if (breakpoints.size()) {
1774 r_flags.push_back("--breakpoints");
1775 String bpoints;
1776 for (List<String>::Element *E = breakpoints.front(); E; E = E->next()) {
1777 bpoints += E->get().replace(" ", "%20");
1778 if (E->next()) {
1779 bpoints += ",";
1780 }
1781 }
1782
1783 r_flags.push_back(bpoints);
1784 }
1785 }
1786
1787 if (p_flags & DEBUG_FLAG_VIEW_COLLISIONS) {
1788 r_flags.push_back("--debug-collisions");
1789 }
1790
1791 if (p_flags & DEBUG_FLAG_VIEW_NAVIGATION) {
1792 r_flags.push_back("--debug-navigation");
1793 }
1794}
1795
1796bool EditorExportPlatform::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
1797 bool valid = true;
1798
1799#ifndef ANDROID_ENABLED
1800 String templates_error;
1801 valid = valid && has_valid_export_configuration(p_preset, templates_error, r_missing_templates, p_debug);
1802
1803 if (!templates_error.is_empty()) {
1804 r_error += templates_error;
1805 }
1806
1807 String export_plugins_warning;
1808 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
1809 for (int i = 0; i < export_plugins.size(); i++) {
1810 Ref<EditorExportPlatform> export_platform = Ref<EditorExportPlatform>(this);
1811 if (!export_plugins[i]->supports_platform(export_platform)) {
1812 continue;
1813 }
1814
1815 String plugin_warning = export_plugins.write[i]->_has_valid_export_configuration(export_platform, p_preset);
1816 if (!plugin_warning.is_empty()) {
1817 export_plugins_warning += plugin_warning;
1818 }
1819 }
1820
1821 if (!export_plugins_warning.is_empty()) {
1822 r_error += export_plugins_warning;
1823 }
1824#endif
1825
1826 String project_configuration_error;
1827 valid = valid && has_valid_project_configuration(p_preset, project_configuration_error);
1828
1829 if (!project_configuration_error.is_empty()) {
1830 r_error += project_configuration_error;
1831 }
1832
1833 return valid;
1834}
1835
1836Error 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 {
1837 String ssh_path = EditorSettings::get_singleton()->get("export/ssh/ssh");
1838 if (ssh_path.is_empty()) {
1839 ssh_path = "ssh";
1840 }
1841
1842 List<String> args;
1843 args.push_back("-p");
1844 args.push_back(p_port);
1845 args.push_back("-q");
1846 args.push_back("-o");
1847 args.push_back("LogLevel=error");
1848 args.push_back("-o");
1849 args.push_back("BatchMode=yes");
1850 args.push_back("-o");
1851 args.push_back("StrictHostKeyChecking=no");
1852 for (const String &E : p_ssh_args) {
1853 args.push_back(E);
1854 }
1855 if (p_port_fwd > 0) {
1856 args.push_back("-R");
1857 args.push_back(vformat("%d:localhost:%d", p_port_fwd, p_port_fwd));
1858 }
1859 args.push_back(p_host);
1860 args.push_back(p_cmd_args);
1861
1862 String out;
1863 int exit_code = -1;
1864
1865 if (OS::get_singleton()->is_stdout_verbose()) {
1866 OS::get_singleton()->print("Executing: %s", ssh_path.utf8().get_data());
1867 for (const String &arg : args) {
1868 OS::get_singleton()->print(" %s", arg.utf8().get_data());
1869 }
1870 OS::get_singleton()->print("\n");
1871 }
1872
1873 Error err = OS::get_singleton()->execute(ssh_path, args, &out, &exit_code, true);
1874 if (out.is_empty()) {
1875 print_verbose(vformat("Exit code: %d", exit_code));
1876 } else {
1877 print_verbose(vformat("Exit code: %d, Output: %s", exit_code, out.replace("\r\n", "\n")));
1878 }
1879 if (r_out) {
1880 *r_out = out.replace("\r\n", "\n").get_slice("\n", 0);
1881 }
1882 if (err != OK) {
1883 return err;
1884 } else if (exit_code != 0) {
1885 if (!out.is_empty()) {
1886 print_line(out);
1887 }
1888 return FAILED;
1889 }
1890 return OK;
1891}
1892
1893Error 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 {
1894 String ssh_path = EditorSettings::get_singleton()->get("export/ssh/ssh");
1895 if (ssh_path.is_empty()) {
1896 ssh_path = "ssh";
1897 }
1898
1899 List<String> args;
1900 args.push_back("-p");
1901 args.push_back(p_port);
1902 args.push_back("-q");
1903 args.push_back("-o");
1904 args.push_back("LogLevel=error");
1905 args.push_back("-o");
1906 args.push_back("BatchMode=yes");
1907 args.push_back("-o");
1908 args.push_back("StrictHostKeyChecking=no");
1909 for (const String &E : p_ssh_args) {
1910 args.push_back(E);
1911 }
1912 if (p_port_fwd > 0) {
1913 args.push_back("-R");
1914 args.push_back(vformat("%d:localhost:%d", p_port_fwd, p_port_fwd));
1915 }
1916 args.push_back(p_host);
1917 args.push_back(p_cmd_args);
1918
1919 if (OS::get_singleton()->is_stdout_verbose()) {
1920 OS::get_singleton()->print("Executing: %s", ssh_path.utf8().get_data());
1921 for (const String &arg : args) {
1922 OS::get_singleton()->print(" %s", arg.utf8().get_data());
1923 }
1924 OS::get_singleton()->print("\n");
1925 }
1926
1927 return OS::get_singleton()->create_process(ssh_path, args, r_pid);
1928}
1929
1930Error 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 {
1931 String scp_path = EditorSettings::get_singleton()->get("export/ssh/scp");
1932 if (scp_path.is_empty()) {
1933 scp_path = "scp";
1934 }
1935
1936 List<String> args;
1937 args.push_back("-P");
1938 args.push_back(p_port);
1939 args.push_back("-q");
1940 args.push_back("-o");
1941 args.push_back("LogLevel=error");
1942 args.push_back("-o");
1943 args.push_back("BatchMode=yes");
1944 args.push_back("-o");
1945 args.push_back("StrictHostKeyChecking=no");
1946 for (const String &E : p_scp_args) {
1947 args.push_back(E);
1948 }
1949 args.push_back(p_src_file);
1950 args.push_back(vformat("%s:%s", p_host, p_dst_file));
1951
1952 String out;
1953 int exit_code = -1;
1954
1955 if (OS::get_singleton()->is_stdout_verbose()) {
1956 OS::get_singleton()->print("Executing: %s", scp_path.utf8().get_data());
1957 for (const String &arg : args) {
1958 OS::get_singleton()->print(" %s", arg.utf8().get_data());
1959 }
1960 OS::get_singleton()->print("\n");
1961 }
1962
1963 Error err = OS::get_singleton()->execute(scp_path, args, &out, &exit_code, true);
1964 if (err != OK) {
1965 return err;
1966 } else if (exit_code != 0) {
1967 if (!out.is_empty()) {
1968 print_line(out);
1969 }
1970 return FAILED;
1971 }
1972 return OK;
1973}
1974
1975void EditorExportPlatform::_bind_methods() {
1976 ClassDB::bind_method(D_METHOD("get_os_name"), &EditorExportPlatform::get_os_name);
1977}
1978
1979EditorExportPlatform::EditorExportPlatform() {
1980}
1981