1/**************************************************************************/
2/* export_plugin.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "export_plugin.h"
32
33#include "codesign.h"
34#include "lipo.h"
35#include "logo_svg.gen.h"
36#include "macho.h"
37#include "run_icon_svg.gen.h"
38
39#include "core/io/image_loader.h"
40#include "core/string/translation.h"
41#include "editor/editor_node.h"
42#include "editor/editor_paths.h"
43#include "editor/editor_scale.h"
44#include "editor/editor_string_names.h"
45#include "editor/import/resource_importer_texture_settings.h"
46#include "scene/resources/image_texture.h"
47
48#include "modules/modules_enabled.gen.h" // For svg and regex.
49#ifdef MODULE_SVG_ENABLED
50#include "modules/svg/image_loader_svg.h"
51#endif
52
53void EditorExportPlatformMacOS::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {
54 r_features->push_back(p_preset->get("binary_format/architecture"));
55 String architecture = p_preset->get("binary_format/architecture");
56
57 if (architecture == "universal" || architecture == "x86_64") {
58 r_features->push_back("s3tc");
59 r_features->push_back("bptc");
60 } else if (architecture == "arm64") {
61 r_features->push_back("etc2");
62 r_features->push_back("astc");
63 } else {
64 ERR_PRINT("Invalid architecture");
65 }
66}
67
68String EditorExportPlatformMacOS::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const {
69 if (p_preset) {
70 int dist_type = p_preset->get("export/distribution_type");
71 bool ad_hoc = false;
72 int codesign_tool = p_preset->get("codesign/codesign");
73 int notary_tool = p_preset->get("notarization/notarization");
74 switch (codesign_tool) {
75 case 1: { // built-in ad-hoc
76 ad_hoc = true;
77 } break;
78 case 2: { // "rcodesign"
79 ad_hoc = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty() || p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty();
80 } break;
81#ifdef MACOS_ENABLED
82 case 3: { // "codesign"
83 ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");
84 } break;
85#endif
86 default: {
87 };
88 }
89
90 if (p_name == "application/bundle_identifier") {
91 String identifier = p_preset->get("application/bundle_identifier");
92 String pn_err;
93 if (!is_package_name_valid(identifier, &pn_err)) {
94 return TTR("Invalid bundle identifier:") + " " + pn_err;
95 }
96 }
97
98 if (p_name == "codesign/certificate_file" || p_name == "codesign/certificate_password" || p_name == "codesign/identity") {
99 if (dist_type == 2) {
100 if (ad_hoc) {
101 return TTR("App Store distribution with ad-hoc code signing is not supported.");
102 }
103 } else if (notary_tool > 0 && ad_hoc) {
104 return TTR("Notarization with an ad-hoc signature is not supported.");
105 }
106 }
107
108 if (p_name == "codesign/apple_team_id") {
109 String team_id = p_preset->get("codesign/apple_team_id");
110 if (team_id.is_empty()) {
111 if (dist_type == 2) {
112 return TTR("Apple Team ID is required for App Store distribution.");
113 } else if (notary_tool > 0) {
114 return TTR("Apple Team ID is required for notarization.");
115 }
116 }
117 }
118
119 if (p_name == "codesign/provisioning_profile" && dist_type == 2) {
120 String pprof = p_preset->get_or_env("codesign/provisioning_profile", ENV_MAC_CODESIGN_PROFILE);
121 if (pprof.is_empty()) {
122 return TTR("Provisioning profile is required for App Store distribution.");
123 }
124 }
125
126 if (p_name == "codesign/installer_identity" && dist_type == 2) {
127 String ident = p_preset->get("codesign/installer_identity");
128 if (ident.is_empty()) {
129 return TTR("Installer signing identity is required for App Store distribution.");
130 }
131 }
132
133 if (p_name == "codesign/entitlements/app_sandbox/enabled" && dist_type == 2) {
134 bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled");
135 if (!sandbox) {
136 return TTR("App sandbox is required for App Store distribution.");
137 }
138 }
139
140 if (p_name == "codesign/codesign") {
141 if (dist_type == 2) {
142 if (codesign_tool == 0) {
143 return TTR("Code signing is required for App Store distribution.");
144 }
145 if (codesign_tool == 1) {
146 return TTR("App Store distribution with ad-hoc code signing is not supported.");
147 }
148 } else if (notary_tool > 0) {
149 if (codesign_tool == 0) {
150 return TTR("Code signing is required for notarization.");
151 }
152 if (codesign_tool == 1) {
153 return TTR("Notarization with an ad-hoc signature is not supported.");
154 }
155 }
156 }
157
158 if (notary_tool == 2 || notary_tool == 3) {
159 if (p_name == "notarization/apple_id_name" || p_name == "notarization/api_uuid") {
160 String apple_id = p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID);
161 String api_uuid = p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID);
162 if (apple_id.is_empty() && api_uuid.is_empty()) {
163 return TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified.");
164 }
165 if (!apple_id.is_empty() && !api_uuid.is_empty()) {
166 return TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time.");
167 }
168 }
169 if (p_name == "notarization/apple_id_password") {
170 String apple_id = p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID);
171 String apple_pass = p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS);
172 if (!apple_id.is_empty() && apple_pass.is_empty()) {
173 return TTR("Apple ID password not specified.");
174 }
175 }
176 if (p_name == "notarization/api_key_id") {
177 String api_uuid = p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID);
178 String api_key = p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID);
179 if (!api_uuid.is_empty() && api_key.is_empty()) {
180 return TTR("App Store Connect API key ID not specified.");
181 }
182 }
183 } else if (notary_tool == 1) {
184 if (p_name == "notarization/api_uuid") {
185 String api_uuid = p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID);
186 if (api_uuid.is_empty()) {
187 return TTR("App Store Connect issuer ID name not specified.");
188 }
189 }
190 if (p_name == "notarization/api_key_id") {
191 String api_key = p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID);
192 if (api_key.is_empty()) {
193 return TTR("App Store Connect API key ID not specified.");
194 }
195 }
196 }
197
198 if (codesign_tool > 0) {
199 if (p_name == "privacy/microphone_usage_description") {
200 String discr = p_preset->get("privacy/microphone_usage_description");
201 bool enabled = p_preset->get("codesign/entitlements/audio_input");
202 if (enabled && discr.is_empty()) {
203 return TTR("Microphone access is enabled, but usage description is not specified.");
204 }
205 }
206 if (p_name == "privacy/camera_usage_description") {
207 String discr = p_preset->get("privacy/camera_usage_description");
208 bool enabled = p_preset->get("codesign/entitlements/camera");
209 if (enabled && discr.is_empty()) {
210 return TTR("Camera access is enabled, but usage description is not specified.");
211 }
212 }
213 if (p_name == "privacy/location_usage_description") {
214 String discr = p_preset->get("privacy/location_usage_description");
215 bool enabled = p_preset->get("codesign/entitlements/location");
216 if (enabled && discr.is_empty()) {
217 return TTR("Location information access is enabled, but usage description is not specified.");
218 }
219 }
220 if (p_name == "privacy/address_book_usage_description") {
221 String discr = p_preset->get("privacy/address_book_usage_description");
222 bool enabled = p_preset->get("codesign/entitlements/address_book");
223 if (enabled && discr.is_empty()) {
224 return TTR("Address book access is enabled, but usage description is not specified.");
225 }
226 }
227 if (p_name == "privacy/calendar_usage_description") {
228 String discr = p_preset->get("privacy/calendar_usage_description");
229 bool enabled = p_preset->get("codesign/entitlements/calendars");
230 if (enabled && discr.is_empty()) {
231 return TTR("Calendar access is enabled, but usage description is not specified.");
232 }
233 }
234 if (p_name == "privacy/photos_library_usage_description") {
235 String discr = p_preset->get("privacy/photos_library_usage_description");
236 bool enabled = p_preset->get("codesign/entitlements/photos_library");
237 if (enabled && discr.is_empty()) {
238 return TTR("Photo library access is enabled, but usage description is not specified.");
239 }
240 }
241 }
242 }
243 return String();
244}
245
246bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {
247 // Hide irrelevant code signing options.
248 if (p_preset) {
249 int codesign_tool = p_preset->get("codesign/codesign");
250 switch (codesign_tool) {
251 case 1: { // built-in ad-hoc
252 if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options" || p_option == "codesign/team_id") {
253 return false;
254 }
255 } break;
256 case 2: { // "rcodesign"
257 if (p_option == "codesign/identity") {
258 return false;
259 }
260 } break;
261#ifdef MACOS_ENABLED
262 case 3: { // "codesign"
263 if (p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password") {
264 return false;
265 }
266 } break;
267#endif
268 default: { // disabled
269 if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options" || p_option.begins_with("codesign/entitlements") || p_option == "codesign/team_id") {
270 return false;
271 }
272 } break;
273 }
274
275 // Distribution type.
276 int dist_type = p_preset->get("export/distribution_type");
277 if (dist_type != 2 && p_option == "codesign/installer_identity") {
278 return false;
279 }
280
281 if (dist_type == 2 && p_option.begins_with("notarization/")) {
282 return false;
283 }
284
285 if (dist_type != 2 && p_option == "codesign/provisioning_profile") {
286 return false;
287 }
288
289 String custom_prof = p_preset->get("codesign/entitlements/custom_file");
290 if (!custom_prof.is_empty() && p_option != "codesign/entitlements/custom_file" && p_option.begins_with("codesign/entitlements/")) {
291 return false;
292 }
293
294 // Hide sandbox entitlements.
295 bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled");
296 if (!sandbox && p_option != "codesign/entitlements/app_sandbox/enabled" && p_option.begins_with("codesign/entitlements/app_sandbox/")) {
297 return false;
298 }
299
300 // Hide SSH options.
301 bool ssh = p_preset->get("ssh_remote_deploy/enabled");
302 if (!ssh && p_option != "ssh_remote_deploy/enabled" && p_option.begins_with("ssh_remote_deploy/")) {
303 return false;
304 }
305
306 // Hide irrelevant notarization options.
307 int notary_tool = p_preset->get("notarization/notarization");
308 switch (notary_tool) {
309 case 1: { // "rcodesign"
310 if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password") {
311 return false;
312 }
313 } break;
314 case 2: { // "notarytool"
315 // All options are visible.
316 } break;
317 case 3: { // "altool"
318 // All options are visible.
319 } break;
320 default: { // disabled
321 if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password" || p_option == "notarization/api_uuid" || p_option == "notarization/api_key" || p_option == "notarization/api_key_id") {
322 return false;
323 }
324 } break;
325 }
326 }
327
328 // These entitlements are required to run managed code, and are always enabled in Mono builds.
329 if (Engine::get_singleton()->has_singleton("GodotSharp")) {
330 if (p_option == "codesign/entitlements/allow_jit_code_execution" || p_option == "codesign/entitlements/allow_unsigned_executable_memory" || p_option == "codesign/entitlements/allow_dyld_environment_variables") {
331 return false;
332 }
333 }
334 return true;
335}
336
337List<String> EditorExportPlatformMacOS::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {
338 List<String> list;
339
340 if (p_preset.is_valid()) {
341 int dist_type = p_preset->get("export/distribution_type");
342 if (dist_type == 0) {
343#ifdef MACOS_ENABLED
344 list.push_back("dmg");
345#endif
346 list.push_back("zip");
347#ifndef WINDOWS_ENABLED
348 list.push_back("app");
349#endif
350 } else if (dist_type == 1) {
351#ifdef MACOS_ENABLED
352 list.push_back("dmg");
353#endif
354 list.push_back("zip");
355 } else if (dist_type == 2) {
356#ifdef MACOS_ENABLED
357 list.push_back("pkg");
358#endif
359 }
360 }
361
362 return list;
363}
364
365void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options) const {
366#ifdef MACOS_ENABLED
367 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "export/distribution_type", PROPERTY_HINT_ENUM, "Testing,Distribution,App Store"), 1, true));
368#else
369 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "export/distribution_type", PROPERTY_HINT_ENUM, "Testing,Distribution"), 1, true));
370#endif
371
372 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "binary_format/architecture", PROPERTY_HINT_ENUM, "universal,x86_64,arm64", PROPERTY_USAGE_STORAGE), "universal"));
373 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
374 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
375
376 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "debug/export_console_wrapper", PROPERTY_HINT_ENUM, "No,Debug Only,Debug and Release"), 1));
377 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/icon", PROPERTY_HINT_FILE, "*.icns,*.png,*.webp,*.svg"), ""));
378 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/icon_interpolation", PROPERTY_HINT_ENUM, "Nearest neighbor,Bilinear,Cubic,Trilinear,Lanczos"), 4));
379 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), "", false, true));
380 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/signature"), ""));
381 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/app_category", PROPERTY_HINT_ENUM, "Business,Developer-tools,Education,Entertainment,Finance,Games,Action-games,Adventure-games,Arcade-games,Board-games,Card-games,Casino-games,Dice-games,Educational-games,Family-games,Kids-games,Music-games,Puzzle-games,Racing-games,Role-playing-games,Simulation-games,Sports-games,Strategy-games,Trivia-games,Word-games,Graphics-design,Healthcare-fitness,Lifestyle,Medical,Music,News,Photography,Productivity,Reference,Social-networking,Sports,Travel,Utilities,Video,Weather"), "Games"));
382 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version"), "1.0"));
383 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), ""));
384 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), ""));
385 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "application/copyright_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
386 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/min_macos_version"), "10.12"));
387 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "display/high_res"), true));
388
389 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/platform_build"), "14C18"));
390 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_version"), "13.1"));
391 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_build"), "22C55"));
392 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_name"), "macosx13.1"));
393 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/xcode_version"), "1420"));
394 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/xcode_build"), "14C18"));
395
396#ifdef MACOS_ENABLED
397 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/codesign", PROPERTY_HINT_ENUM, "Disabled,Built-in (ad-hoc only),rcodesign,Xcode codesign"), 3, true));
398#else
399 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/codesign", PROPERTY_HINT_ENUM, "Disabled,Built-in (ad-hoc only),rcodesign"), 1, true, true));
400#endif
401 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/installer_identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "3rd Party Mac Developer Installer: (ID)"), "", false, true));
402 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/apple_team_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "ID"), "", false, true));
403 // "codesign" only options:
404 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "Type: Name (ID)"), ""));
405 // "rcodesign" only options:
406 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_file", PROPERTY_HINT_GLOBAL_FILE, "*.pfx,*.p12", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
407 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
408 // "codesign" and "rcodesign" only options:
409 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/provisioning_profile", PROPERTY_HINT_GLOBAL_FILE, "*.provisionprofile", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));
410
411 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/custom_file", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), "", true));
412 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_jit_code_execution"), false));
413 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_unsigned_executable_memory"), false));
414 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_dyld_environment_variables"), false));
415 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/disable_library_validation"), false));
416 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/audio_input"), false));
417 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/camera"), false));
418 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/location"), false));
419 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/address_book"), false));
420 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/calendars"), false));
421 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/photos_library"), false));
422 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/apple_events"), false));
423 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/debugging"), false));
424 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/enabled"), false, true, true));
425 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/network_server"), false));
426 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/network_client"), false));
427 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/device_usb"), false));
428 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/device_bluetooth"), false));
429 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_downloads", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
430 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_pictures", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
431 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_music", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
432 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
433 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_user_selected", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
434 r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array()));
435 r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray()));
436
437#ifdef MACOS_ENABLED
438 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "notarization/notarization", PROPERTY_HINT_ENUM, "Disabled,rcodesign,Xcode notarytool,Xcode altool (deprecated)"), 0, true));
439#else
440 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "notarization/notarization", PROPERTY_HINT_ENUM, "Disabled,rcodesign"), 0, true));
441#endif
442 // "altool" and "notarytool" only options:
443 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Apple ID email", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));
444 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_password", PROPERTY_HINT_PASSWORD, "Enable two-factor authentication and provide app-specific password", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));
445 // "altool", "notarytool" and "rcodesign" only options:
446 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_uuid", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect issuer ID UUID", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));
447 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_key", PROPERTY_HINT_GLOBAL_FILE, "*.p8", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));
448 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_key_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect API key ID", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));
449
450 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), "", false, true));
451 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/microphone_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
452 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), "", false, true));
453 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/camera_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
454 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/location_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the location information"), "", false, true));
455 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/location_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
456 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/address_book_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the address book"), "", false, true));
457 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/address_book_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
458 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/calendar_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the calendar"), "", false, true));
459 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/calendar_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
460 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photos_library_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the photo library"), "", false, true));
461 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photos_library_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
462 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/desktop_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Desktop folder"), "", false, true));
463 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/desktop_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
464 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/documents_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Documents folder"), "", false, true));
465 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/documents_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
466 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/downloads_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Downloads folder"), "", false, true));
467 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/downloads_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
468 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/network_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use network volumes"), "", false, true));
469 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/network_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
470 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/removable_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use removable volumes"), "", false, true));
471 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/removable_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
472
473 String run_script = "#!/usr/bin/env bash\n"
474 "unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"\n"
475 "open \"{temp_dir}/{exe_name}.app\" --args {cmd_args}";
476
477 String cleanup_script = "#!/usr/bin/env bash\n"
478 "kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\")\n"
479 "rm -rf \"{temp_dir}\"";
480
481 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false, true));
482 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/host"), "user@host_ip"));
483 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/port"), "22"));
484
485 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_ssh", PROPERTY_HINT_MULTILINE_TEXT), ""));
486 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_scp", PROPERTY_HINT_MULTILINE_TEXT), ""));
487 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/run_script", PROPERTY_HINT_MULTILINE_TEXT), run_script));
488 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/cleanup_script", PROPERTY_HINT_MULTILINE_TEXT), cleanup_script));
489}
490
491void _rgba8_to_packbits_encode(int p_ch, int p_size, Vector<uint8_t> &p_source, Vector<uint8_t> &p_dest) {
492 int src_len = p_size * p_size;
493
494 Vector<uint8_t> result;
495 result.resize(src_len * 1.25); //temp vector for rle encoded data, make it 25% larger for worst case scenario
496 int res_size = 0;
497
498 uint8_t buf[128];
499 int buf_size = 0;
500
501 int i = 0;
502 while (i < src_len) {
503 uint8_t cur = p_source.ptr()[i * 4 + p_ch];
504
505 if (i < src_len - 2) {
506 if ((p_source.ptr()[(i + 1) * 4 + p_ch] == cur) && (p_source.ptr()[(i + 2) * 4 + p_ch] == cur)) {
507 if (buf_size > 0) {
508 result.write[res_size++] = (uint8_t)(buf_size - 1);
509 memcpy(&result.write[res_size], &buf, buf_size);
510 res_size += buf_size;
511 buf_size = 0;
512 }
513
514 uint8_t lim = i + 130 >= src_len ? src_len - i - 1 : 130;
515 bool hit_lim = true;
516
517 for (int j = 3; j <= lim; j++) {
518 if (p_source.ptr()[(i + j) * 4 + p_ch] != cur) {
519 hit_lim = false;
520 i = i + j - 1;
521 result.write[res_size++] = (uint8_t)(j - 3 + 0x80);
522 result.write[res_size++] = cur;
523 break;
524 }
525 }
526 if (hit_lim) {
527 result.write[res_size++] = (uint8_t)(lim - 3 + 0x80);
528 result.write[res_size++] = cur;
529 i = i + lim;
530 }
531 } else {
532 buf[buf_size++] = cur;
533 if (buf_size == 128) {
534 result.write[res_size++] = (uint8_t)(buf_size - 1);
535 memcpy(&result.write[res_size], &buf, buf_size);
536 res_size += buf_size;
537 buf_size = 0;
538 }
539 }
540 } else {
541 buf[buf_size++] = cur;
542 result.write[res_size++] = (uint8_t)(buf_size - 1);
543 memcpy(&result.write[res_size], &buf, buf_size);
544 res_size += buf_size;
545 buf_size = 0;
546 }
547
548 i++;
549 }
550
551 int ofs = p_dest.size();
552 p_dest.resize(p_dest.size() + res_size);
553 memcpy(&p_dest.write[ofs], result.ptr(), res_size);
554}
555
556void EditorExportPlatformMacOS::_make_icon(const Ref<EditorExportPreset> &p_preset, const Ref<Image> &p_icon, Vector<uint8_t> &p_data) {
557 Ref<ImageTexture> it = memnew(ImageTexture);
558
559 Vector<uint8_t> data;
560
561 data.resize(8);
562 data.write[0] = 'i';
563 data.write[1] = 'c';
564 data.write[2] = 'n';
565 data.write[3] = 's';
566
567 struct MacOSIconInfo {
568 const char *name;
569 const char *mask_name;
570 bool is_png;
571 int size;
572 };
573
574 static const MacOSIconInfo icon_infos[] = {
575 { "ic10", "", true, 1024 }, //1024×1024 32-bit PNG and 512×512@2x 32-bit "retina" PNG
576 { "ic09", "", true, 512 }, //512×512 32-bit PNG
577 { "ic14", "", true, 512 }, //256×256@2x 32-bit "retina" PNG
578 { "ic08", "", true, 256 }, //256×256 32-bit PNG
579 { "ic13", "", true, 256 }, //128×128@2x 32-bit "retina" PNG
580 { "ic07", "", true, 128 }, //128×128 32-bit PNG
581 { "ic12", "", true, 64 }, //32×32@2× 32-bit "retina" PNG
582 { "ic11", "", true, 32 }, //16×16@2× 32-bit "retina" PNG
583 { "il32", "l8mk", false, 32 }, //32×32 24-bit RLE + 8-bit uncompressed mask
584 { "is32", "s8mk", false, 16 } //16×16 24-bit RLE + 8-bit uncompressed mask
585 };
586
587 for (uint64_t i = 0; i < (sizeof(icon_infos) / sizeof(icon_infos[0])); ++i) {
588 Ref<Image> copy = p_icon; // does this make sense? doesn't this just increase the reference count instead of making a copy? Do we even need a copy?
589 copy->convert(Image::FORMAT_RGBA8);
590 copy->resize(icon_infos[i].size, icon_infos[i].size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));
591
592 if (icon_infos[i].is_png) {
593 // Encode PNG icon.
594 it->set_image(copy);
595 String path = EditorPaths::get_singleton()->get_cache_dir().path_join("icon.png");
596 ResourceSaver::save(it, path);
597
598 {
599 Ref<FileAccess> f = FileAccess::open(path, FileAccess::READ);
600 if (f.is_null()) {
601 // Clean up generated file.
602 DirAccess::remove_file_or_error(path);
603 add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not open icon file \"%s\"."), path));
604 return;
605 }
606
607 int ofs = data.size();
608 uint64_t len = f->get_length();
609 data.resize(data.size() + len + 8);
610 f->get_buffer(&data.write[ofs + 8], len);
611 len += 8;
612 len = BSWAP32(len);
613 memcpy(&data.write[ofs], icon_infos[i].name, 4);
614 encode_uint32(len, &data.write[ofs + 4]);
615 }
616
617 // Clean up generated file.
618 DirAccess::remove_file_or_error(path);
619
620 } else {
621 Vector<uint8_t> src_data = copy->get_data();
622
623 //encode 24bit RGB RLE icon
624 {
625 int ofs = data.size();
626 data.resize(data.size() + 8);
627
628 _rgba8_to_packbits_encode(0, icon_infos[i].size, src_data, data); // encode R
629 _rgba8_to_packbits_encode(1, icon_infos[i].size, src_data, data); // encode G
630 _rgba8_to_packbits_encode(2, icon_infos[i].size, src_data, data); // encode B
631
632 int len = data.size() - ofs;
633 len = BSWAP32(len);
634 memcpy(&data.write[ofs], icon_infos[i].name, 4);
635 encode_uint32(len, &data.write[ofs + 4]);
636 }
637
638 //encode 8bit mask uncompressed icon
639 {
640 int ofs = data.size();
641 int len = copy->get_width() * copy->get_height();
642 data.resize(data.size() + len + 8);
643
644 for (int j = 0; j < len; j++) {
645 data.write[ofs + 8 + j] = src_data.ptr()[j * 4 + 3];
646 }
647 len += 8;
648 len = BSWAP32(len);
649 memcpy(&data.write[ofs], icon_infos[i].mask_name, 4);
650 encode_uint32(len, &data.write[ofs + 4]);
651 }
652 }
653 }
654
655 uint32_t total_len = data.size();
656 total_len = BSWAP32(total_len);
657 encode_uint32(total_len, &data.write[4]);
658
659 p_data = data;
660}
661
662void EditorExportPlatformMacOS::_fix_plist(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &plist, const String &p_binary) {
663 String str;
664 String strnew;
665 str.parse_utf8((const char *)plist.ptr(), plist.size());
666 Vector<String> lines = str.split("\n");
667 for (int i = 0; i < lines.size(); i++) {
668 if (lines[i].find("$binary") != -1) {
669 strnew += lines[i].replace("$binary", p_binary) + "\n";
670 } else if (lines[i].find("$name") != -1) {
671 strnew += lines[i].replace("$name", GLOBAL_GET("application/config/name")) + "\n";
672 } else if (lines[i].find("$bundle_identifier") != -1) {
673 strnew += lines[i].replace("$bundle_identifier", p_preset->get("application/bundle_identifier")) + "\n";
674 } else if (lines[i].find("$short_version") != -1) {
675 strnew += lines[i].replace("$short_version", p_preset->get("application/short_version")) + "\n";
676 } else if (lines[i].find("$version") != -1) {
677 strnew += lines[i].replace("$version", p_preset->get_version("application/version")) + "\n";
678 } else if (lines[i].find("$signature") != -1) {
679 strnew += lines[i].replace("$signature", p_preset->get("application/signature")) + "\n";
680 } else if (lines[i].find("$app_category") != -1) {
681 String cat = p_preset->get("application/app_category");
682 strnew += lines[i].replace("$app_category", cat.to_lower()) + "\n";
683 } else if (lines[i].find("$copyright") != -1) {
684 strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n";
685 } else if (lines[i].find("$min_version") != -1) {
686 strnew += lines[i].replace("$min_version", p_preset->get("application/min_macos_version")) + "\n";
687 } else if (lines[i].find("$highres") != -1) {
688 strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "\t<true/>" : "\t<false/>") + "\n";
689 } else if (lines[i].find("$platfbuild") != -1) {
690 strnew += lines[i].replace("$platfbuild", p_preset->get("xcode/platform_build")) + "\n";
691 } else if (lines[i].find("$sdkver") != -1) {
692 strnew += lines[i].replace("$sdkver", p_preset->get("xcode/sdk_version")) + "\n";
693 } else if (lines[i].find("$sdkname") != -1) {
694 strnew += lines[i].replace("$sdkname", p_preset->get("xcode/sdk_name")) + "\n";
695 } else if (lines[i].find("$sdkbuild") != -1) {
696 strnew += lines[i].replace("$sdkbuild", p_preset->get("xcode/sdk_build")) + "\n";
697 } else if (lines[i].find("$xcodever") != -1) {
698 strnew += lines[i].replace("$xcodever", p_preset->get("xcode/xcode_version")) + "\n";
699 } else if (lines[i].find("$xcodebuild") != -1) {
700 strnew += lines[i].replace("$xcodebuild", p_preset->get("xcode/xcode_build")) + "\n";
701 } else if (lines[i].find("$usage_descriptions") != -1) {
702 String descriptions;
703 if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
704 descriptions += "\t<key>NSMicrophoneUsageDescription</key>\n";
705 descriptions += "\t<string>" + (String)p_preset->get("privacy/microphone_usage_description") + "</string>\n";
706 }
707 if (!((String)p_preset->get("privacy/camera_usage_description")).is_empty()) {
708 descriptions += "\t<key>NSCameraUsageDescription</key>\n";
709 descriptions += "\t<string>" + (String)p_preset->get("privacy/camera_usage_description") + "</string>\n";
710 }
711 if (!((String)p_preset->get("privacy/location_usage_description")).is_empty()) {
712 descriptions += "\t<key>NSLocationUsageDescription</key>\n";
713 descriptions += "\t<string>" + (String)p_preset->get("privacy/location_usage_description") + "</string>\n";
714 }
715 if (!((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) {
716 descriptions += "\t<key>NSContactsUsageDescription</key>\n";
717 descriptions += "\t<string>" + (String)p_preset->get("privacy/address_book_usage_description") + "</string>\n";
718 }
719 if (!((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) {
720 descriptions += "\t<key>NSCalendarsUsageDescription</key>\n";
721 descriptions += "\t<string>" + (String)p_preset->get("privacy/calendar_usage_description") + "</string>\n";
722 }
723 if (!((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) {
724 descriptions += "\t<key>NSPhotoLibraryUsageDescription</key>\n";
725 descriptions += "\t<string>" + (String)p_preset->get("privacy/photos_library_usage_description") + "</string>\n";
726 }
727 if (!((String)p_preset->get("privacy/desktop_folder_usage_description")).is_empty()) {
728 descriptions += "\t<key>NSDesktopFolderUsageDescription</key>\n";
729 descriptions += "\t<string>" + (String)p_preset->get("privacy/desktop_folder_usage_description") + "</string>\n";
730 }
731 if (!((String)p_preset->get("privacy/documents_folder_usage_description")).is_empty()) {
732 descriptions += "\t<key>NSDocumentsFolderUsageDescription</key>\n";
733 descriptions += "\t<string>" + (String)p_preset->get("privacy/documents_folder_usage_description") + "</string>\n";
734 }
735 if (!((String)p_preset->get("privacy/downloads_folder_usage_description")).is_empty()) {
736 descriptions += "\t<key>NSDownloadsFolderUsageDescription</key>\n";
737 descriptions += "\t<string>" + (String)p_preset->get("privacy/downloads_folder_usage_description") + "</string>\n";
738 }
739 if (!((String)p_preset->get("privacy/network_volumes_usage_description")).is_empty()) {
740 descriptions += "\t<key>NSNetworkVolumesUsageDescription</key>\n";
741 descriptions += "\t<string>" + (String)p_preset->get("privacy/network_volumes_usage_description") + "</string>\n";
742 }
743 if (!((String)p_preset->get("privacy/removable_volumes_usage_description")).is_empty()) {
744 descriptions += "\t<key>NSRemovableVolumesUsageDescription</key>\n";
745 descriptions += "\t<string>" + (String)p_preset->get("privacy/removable_volumes_usage_description") + "</string>\n";
746 }
747 if (!descriptions.is_empty()) {
748 strnew += lines[i].replace("$usage_descriptions", descriptions);
749 }
750 } else {
751 strnew += lines[i] + "\n";
752 }
753 }
754
755 CharString cs = strnew.utf8();
756 plist.resize(cs.size() - 1);
757 for (int i = 0; i < cs.size() - 1; i++) {
758 plist.write[i] = cs[i];
759 }
760}
761
762/**
763 * If we're running the macOS version of the Godot editor we'll:
764 * - export our application bundle to a temporary folder
765 * - attempt to code sign it
766 * - and then wrap it up in a DMG
767 */
768
769Error EditorExportPlatformMacOS::_notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path) {
770 int notary_tool = p_preset->get("notarization/notarization");
771 switch (notary_tool) {
772 case 1: { // "rcodesign"
773 print_verbose("using rcodesign notarization...");
774
775 String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String();
776 if (rcodesign.is_empty()) {
777 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign)."));
778 return Error::FAILED;
779 }
780
781 List<String> args;
782
783 args.push_back("notary-submit");
784
785 if (p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) == "") {
786 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect issuer ID name not specified."));
787 return Error::FAILED;
788 }
789 if (p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY) == "") {
790 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified."));
791 return Error::FAILED;
792 }
793
794 args.push_back("--api-issuer");
795 args.push_back(p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID));
796
797 args.push_back("--api-key");
798 args.push_back(p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID));
799
800 if (!p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY).operator String().is_empty()) {
801 args.push_back("--api-key-path");
802 args.push_back(p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY));
803 }
804
805 args.push_back(p_path);
806
807 String str;
808 int exitcode = 0;
809
810 Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true);
811 if (err != OK) {
812 add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start rcodesign executable."));
813 return err;
814 }
815
816 int rq_offset = str.find("created submission ID:");
817 if (exitcode != 0 || rq_offset == -1) {
818 print_line("rcodesign (" + p_path + "):\n" + str);
819 add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details."));
820 return Error::FAILED;
821 } else {
822 print_verbose("rcodesign (" + p_path + "):\n" + str);
823 int next_nl = str.find("\n", rq_offset);
824 String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 23, -1) : str.substr(rq_offset + 23, next_nl - rq_offset - 23);
825 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid));
826 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour."));
827 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check progress manually by opening a Terminal and running the following command:"));
828 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign notary-log --api-issuer <api uuid> --api-key <api key> <request uuid>\"");
829 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):"));
830 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign staple <app path>\"");
831 }
832 } break;
833#ifdef MACOS_ENABLED
834 case 2: { // "notarytool"
835 print_verbose("using notarytool notarization...");
836
837 if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) {
838 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Xcode command line tools are not installed."));
839 return Error::FAILED;
840 }
841
842 List<String> args;
843
844 args.push_back("notarytool");
845 args.push_back("submit");
846
847 args.push_back(p_path);
848
849 if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) == "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) == "") {
850 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified."));
851 return Error::FAILED;
852 }
853 if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) != "") {
854 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time."));
855 return Error::FAILED;
856 }
857
858 if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "") {
859 if (p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS) == "") {
860 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Apple ID password not specified."));
861 return Error::FAILED;
862 }
863 args.push_back("--apple-id");
864 args.push_back(p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID));
865
866 args.push_back("--password");
867 args.push_back(p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS));
868 } else {
869 if (p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID) == "") {
870 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified."));
871 return Error::FAILED;
872 }
873 args.push_back("--issuer");
874 args.push_back(p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID));
875
876 if (!p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY).operator String().is_empty()) {
877 args.push_back("--key");
878 args.push_back(p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY));
879 }
880
881 args.push_back("--key-id");
882 args.push_back(p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID));
883 }
884
885 args.push_back("--no-progress");
886
887 if (p_preset->get("codesign/apple_team_id")) {
888 args.push_back("--team-id");
889 args.push_back(p_preset->get("codesign/apple_team_id"));
890 }
891
892 String str;
893 int exitcode = 0;
894 Error err = OS::get_singleton()->execute("xcrun", args, &str, &exitcode, true);
895 if (err != OK) {
896 add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start xcrun executable."));
897 return err;
898 }
899
900 int rq_offset = str.find("id:");
901 if (exitcode != 0 || rq_offset == -1) {
902 print_line("notarytool (" + p_path + "):\n" + str);
903 add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details."));
904 return Error::FAILED;
905 } else {
906 print_verbose("notarytool (" + p_path + "):\n" + str);
907 int next_nl = str.find("\n", rq_offset);
908 String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 4, -1) : str.substr(rq_offset + 4, next_nl - rq_offset - 4);
909 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid));
910 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour."));
911 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check progress manually by opening a Terminal and running the following command:"));
912 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun notarytool log <request uuid> --issuer <api uuid> --key-id <api key id> --key <api key path>\" or");
913 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun notarytool log <request uuid> --apple-id <your email> --password <app-specific pwd>>\"");
914 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):"));
915 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun stapler staple <app path>\"");
916 }
917 } break;
918 case 3: { // "altool"
919 print_verbose("using altool notarization...");
920
921 if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) {
922 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Xcode command line tools are not installed."));
923 return Error::FAILED;
924 }
925
926 List<String> args;
927
928 args.push_back("altool");
929 args.push_back("--notarize-app");
930
931 args.push_back("--primary-bundle-id");
932 args.push_back(p_preset->get("application/bundle_identifier"));
933
934 if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) == "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) == "") {
935 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified."));
936 return Error::FAILED;
937 }
938 if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) != "") {
939 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time."));
940 return Error::FAILED;
941 }
942
943 if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "") {
944 if (p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS) == "") {
945 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Apple ID password not specified."));
946 return Error::FAILED;
947 }
948 args.push_back("--username");
949 args.push_back(p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID));
950
951 args.push_back("--password");
952 args.push_back(p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS));
953 } else {
954 if (p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY) == "") {
955 add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified."));
956 return Error::FAILED;
957 }
958 args.push_back("--apiIssuer");
959 args.push_back(p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID));
960
961 args.push_back("--apiKey");
962 args.push_back(p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID));
963 }
964
965 args.push_back("--type");
966 args.push_back("osx");
967
968 if (p_preset->get("codesign/apple_team_id")) {
969 args.push_back("--asc-provider");
970 args.push_back(p_preset->get("codesign/apple_team_id"));
971 }
972
973 args.push_back("--file");
974 args.push_back(p_path);
975
976 String str;
977 int exitcode = 0;
978 Error err = OS::get_singleton()->execute("xcrun", args, &str, &exitcode, true);
979 if (err != OK) {
980 add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start xcrun executable."));
981 return err;
982 }
983
984 int rq_offset = str.find("RequestUUID:");
985 if (exitcode != 0 || rq_offset == -1) {
986 print_line("xcrun altool (" + p_path + "):\n" + str);
987 add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details."));
988 return Error::FAILED;
989 } else {
990 print_verbose("xcrun altool (" + p_path + "):\n" + str);
991 int next_nl = str.find("\n", rq_offset);
992 String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 13, -1) : str.substr(rq_offset + 13, next_nl - rq_offset - 13);
993 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid));
994 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour. When the process is completed, you'll receive an email."));
995 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check progress manually by opening a Terminal and running the following command:"));
996 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\"");
997 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):"));
998 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun stapler staple <app path>\"");
999 }
1000 } break;
1001#endif
1002 default: {
1003 };
1004 }
1005 return OK;
1006}
1007
1008Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn) {
1009 int codesign_tool = p_preset->get("codesign/codesign");
1010 switch (codesign_tool) {
1011 case 1: { // built-in ad-hoc
1012 print_verbose("using built-in codesign...");
1013#ifdef MODULE_REGEX_ENABLED
1014 String error_msg;
1015 Error err = CodeSign::codesign(false, true, p_path, p_ent_path, error_msg);
1016 if (err != OK) {
1017 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Built-in CodeSign failed with error \"%s\"."), error_msg));
1018 return Error::FAILED;
1019 }
1020#else
1021 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Built-in CodeSign require regex module."));
1022#endif
1023 } break;
1024 case 2: { // "rcodesign"
1025 print_verbose("using rcodesign codesign...");
1026
1027 String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String();
1028 if (rcodesign.is_empty()) {
1029 add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xrcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign)."));
1030 return Error::FAILED;
1031 }
1032
1033 List<String> args;
1034 args.push_back("sign");
1035
1036 if (p_path.get_extension() != "dmg") {
1037 args.push_back("--entitlements-xml-path");
1038 args.push_back(p_ent_path);
1039 }
1040
1041 String certificate_file = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE);
1042 String certificate_pass = p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_PASS);
1043 if (!certificate_file.is_empty() && !certificate_pass.is_empty()) {
1044 args.push_back("--p12-file");
1045 args.push_back(certificate_file);
1046 args.push_back("--p12-password");
1047 args.push_back(certificate_pass);
1048 }
1049
1050 args.push_back("-v"); /* provide some more feedback */
1051
1052 args.push_back(p_path);
1053
1054 String str;
1055 int exitcode = 0;
1056
1057 Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true);
1058 if (err != OK) {
1059 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start rcodesign executable."));
1060 return err;
1061 }
1062
1063 if (exitcode != 0) {
1064 print_line("rcodesign (" + p_path + "):\n" + str);
1065 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details."));
1066 return Error::FAILED;
1067 } else {
1068 print_verbose("rcodesign (" + p_path + "):\n" + str);
1069 }
1070 } break;
1071#ifdef MACOS_ENABLED
1072 case 3: { // "codesign"
1073 print_verbose("using xcode codesign...");
1074
1075 if (!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) {
1076 add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xcode command line tools are not installed."));
1077 return Error::FAILED;
1078 }
1079
1080 bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");
1081
1082 List<String> args;
1083 if (!ad_hoc) {
1084 args.push_back("--timestamp");
1085 args.push_back("--options");
1086 args.push_back("runtime");
1087 }
1088
1089 if (p_path.get_extension() != "dmg") {
1090 args.push_back("--entitlements");
1091 args.push_back(p_ent_path);
1092 }
1093
1094 PackedStringArray user_args = p_preset->get("codesign/custom_options");
1095 for (int i = 0; i < user_args.size(); i++) {
1096 String user_arg = user_args[i].strip_edges();
1097 if (!user_arg.is_empty()) {
1098 args.push_back(user_arg);
1099 }
1100 }
1101
1102 args.push_back("-s");
1103 if (ad_hoc) {
1104 args.push_back("-");
1105 } else {
1106 args.push_back(p_preset->get("codesign/identity"));
1107 }
1108
1109 args.push_back("-v"); /* provide some more feedback */
1110 args.push_back("-f");
1111
1112 args.push_back(p_path);
1113
1114 String str;
1115 int exitcode = 0;
1116
1117 Error err = OS::get_singleton()->execute("codesign", args, &str, &exitcode, true);
1118 if (err != OK) {
1119 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start codesign executable, make sure Xcode command line tools are installed."));
1120 return err;
1121 }
1122
1123 if (exitcode != 0) {
1124 print_line("codesign (" + p_path + "):\n" + str);
1125 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details."));
1126 return Error::FAILED;
1127 } else {
1128 print_verbose("codesign (" + p_path + "):\n" + str);
1129 }
1130 } break;
1131#endif
1132 default: {
1133 };
1134 }
1135
1136 return OK;
1137}
1138
1139Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path,
1140 const String &p_ent_path, bool p_should_error_on_non_code) {
1141#ifdef MACOS_ENABLED
1142 static Vector<String> extensions_to_sign;
1143
1144 if (extensions_to_sign.is_empty()) {
1145 extensions_to_sign.push_back("dylib");
1146 extensions_to_sign.push_back("framework");
1147 extensions_to_sign.push_back("");
1148 }
1149
1150 Error dir_access_error;
1151 Ref<DirAccess> dir_access{ DirAccess::open(p_path, &dir_access_error) };
1152
1153 if (dir_access_error != OK) {
1154 return dir_access_error;
1155 }
1156
1157 dir_access->list_dir_begin();
1158 String current_file{ dir_access->get_next() };
1159 while (!current_file.is_empty()) {
1160 String current_file_path{ p_path.path_join(current_file) };
1161
1162 if (current_file == ".." || current_file == ".") {
1163 current_file = dir_access->get_next();
1164 continue;
1165 }
1166
1167 if (extensions_to_sign.find(current_file.get_extension()) > -1) {
1168 Error code_sign_error{ _code_sign(p_preset, current_file_path, p_ent_path, false) };
1169 if (code_sign_error != OK) {
1170 return code_sign_error;
1171 }
1172 if (is_executable(current_file_path)) {
1173 // chmod with 0755 if the file is executable.
1174 FileAccess::set_unix_permissions(current_file_path, 0755);
1175 }
1176 } else if (dir_access->current_is_dir()) {
1177 Error code_sign_error{ _code_sign_directory(p_preset, current_file_path, p_ent_path, p_should_error_on_non_code) };
1178 if (code_sign_error != OK) {
1179 return code_sign_error;
1180 }
1181 } else if (p_should_error_on_non_code) {
1182 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign file %s."), current_file));
1183 return Error::FAILED;
1184 }
1185
1186 current_file = dir_access->get_next();
1187 }
1188#endif
1189
1190 return OK;
1191}
1192
1193Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path,
1194 const String &p_in_app_path, bool p_sign_enabled,
1195 const Ref<EditorExportPreset> &p_preset, const String &p_ent_path,
1196 bool p_should_error_on_non_code_sign) {
1197 static Vector<String> extensions_to_sign;
1198
1199 if (extensions_to_sign.is_empty()) {
1200 extensions_to_sign.push_back("dylib");
1201 extensions_to_sign.push_back("framework");
1202 extensions_to_sign.push_back("");
1203 }
1204
1205 Error err{ OK };
1206 if (dir_access->dir_exists(p_src_path)) {
1207#ifndef UNIX_ENABLED
1208 add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Relative symlinks are not supported, exported \"%s\" might be broken!"), p_src_path.get_file()));
1209#endif
1210 print_verbose("export framework: " + p_src_path + " -> " + p_in_app_path);
1211 err = dir_access->make_dir_recursive(p_in_app_path);
1212 if (err == OK) {
1213 err = dir_access->copy_dir(p_src_path, p_in_app_path, -1, true);
1214 }
1215 } else {
1216 print_verbose("export dylib: " + p_src_path + " -> " + p_in_app_path);
1217 err = dir_access->copy(p_src_path, p_in_app_path);
1218 }
1219 if (err == OK && p_sign_enabled) {
1220 if (dir_access->dir_exists(p_src_path) && p_src_path.get_extension().is_empty()) {
1221 // If it is a directory, find and sign all dynamic libraries.
1222 err = _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_should_error_on_non_code_sign);
1223 } else {
1224 if (extensions_to_sign.find(p_in_app_path.get_extension()) > -1) {
1225 err = _code_sign(p_preset, p_in_app_path, p_ent_path, false);
1226 }
1227 if (is_executable(p_in_app_path)) {
1228 // chmod with 0755 if the file is executable.
1229 FileAccess::set_unix_permissions(p_in_app_path, 0755);
1230 }
1231 }
1232 }
1233 return err;
1234}
1235
1236Error EditorExportPlatformMacOS::_export_macos_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin,
1237 const String &p_app_path_name, Ref<DirAccess> &dir_access,
1238 bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset,
1239 const String &p_ent_path) {
1240 Error error{ OK };
1241 const Vector<String> &macos_plugins{ p_editor_export_plugin->get_macos_plugin_files() };
1242 for (int i = 0; i < macos_plugins.size(); ++i) {
1243 String src_path{ ProjectSettings::get_singleton()->globalize_path(macos_plugins[i]) };
1244 String path_in_app{ p_app_path_name + "/Contents/PlugIns/" + src_path.get_file() };
1245 error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, false);
1246 if (error != OK) {
1247 break;
1248 }
1249 }
1250 return error;
1251}
1252
1253Error EditorExportPlatformMacOS::_create_pkg(const Ref<EditorExportPreset> &p_preset, const String &p_pkg_path, const String &p_app_path_name) {
1254 List<String> args;
1255
1256 if (FileAccess::exists(p_pkg_path)) {
1257 OS::get_singleton()->move_to_trash(p_pkg_path);
1258 }
1259
1260 args.push_back("productbuild");
1261 args.push_back("--component");
1262 args.push_back(p_app_path_name);
1263 args.push_back("/Applications");
1264 String ident = p_preset->get("codesign/installer_identity");
1265 if (!ident.is_empty()) {
1266 args.push_back("--timestamp");
1267 args.push_back("--sign");
1268 args.push_back(ident);
1269 }
1270 args.push_back("--quiet");
1271 args.push_back(p_pkg_path);
1272
1273 String str;
1274 Error err = OS::get_singleton()->execute("xcrun", args, &str, nullptr, true);
1275 if (err != OK) {
1276 add_message(EXPORT_MESSAGE_ERROR, TTR("PKG Creation"), TTR("Could not start productbuild executable."));
1277 return err;
1278 }
1279
1280 print_verbose("productbuild returned: " + str);
1281 if (str.find("productbuild: error:") != -1) {
1282 add_message(EXPORT_MESSAGE_ERROR, TTR("PKG Creation"), TTR("`productbuild` failed."));
1283 return FAILED;
1284 }
1285
1286 return OK;
1287}
1288
1289Error EditorExportPlatformMacOS::_create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name) {
1290 List<String> args;
1291
1292 if (FileAccess::exists(p_dmg_path)) {
1293 OS::get_singleton()->move_to_trash(p_dmg_path);
1294 }
1295
1296 args.push_back("create");
1297 args.push_back(p_dmg_path);
1298 args.push_back("-volname");
1299 args.push_back(p_pkg_name);
1300 args.push_back("-fs");
1301 args.push_back("HFS+");
1302 args.push_back("-srcfolder");
1303 args.push_back(p_app_path_name);
1304
1305 String str;
1306 Error err = OS::get_singleton()->execute("hdiutil", args, &str, nullptr, true);
1307 if (err != OK) {
1308 add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("Could not start hdiutil executable."));
1309 return err;
1310 }
1311
1312 print_verbose("hdiutil returned: " + str);
1313 if (str.find("create failed") != -1) {
1314 if (str.find("File exists") != -1) {
1315 add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed - file exists."));
1316 } else {
1317 add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed."));
1318 }
1319 return FAILED;
1320 }
1321
1322 return OK;
1323}
1324
1325bool EditorExportPlatformMacOS::is_shebang(const String &p_path) const {
1326 Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ);
1327 ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("Can't open file: \"%s\".", p_path));
1328 uint16_t magic = fb->get_16();
1329 return (magic == 0x2123);
1330}
1331
1332bool EditorExportPlatformMacOS::is_executable(const String &p_path) const {
1333 return MachO::is_macho(p_path) || LipO::is_lipo(p_path) || is_shebang(p_path);
1334}
1335
1336Error EditorExportPlatformMacOS::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) {
1337 Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
1338 if (f.is_null()) {
1339 add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path));
1340 return ERR_CANT_CREATE;
1341 }
1342
1343 f->store_line("#!/bin/sh");
1344 f->store_line("echo -ne '\\033c\\033]0;" + p_app_name + "\\a'");
1345 f->store_line("");
1346 f->store_line("function app_realpath() {");
1347 f->store_line(" SOURCE=$1");
1348 f->store_line(" while [ -h \"$SOURCE\" ]; do");
1349 f->store_line(" DIR=$(dirname \"$SOURCE\")");
1350 f->store_line(" SOURCE=$(readlink \"$SOURCE\")");
1351 f->store_line(" [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE");
1352 f->store_line(" done");
1353 f->store_line(" echo \"$( cd -P \"$( dirname \"$SOURCE\" )\" >/dev/null 2>&1 && pwd )\"");
1354 f->store_line("}");
1355 f->store_line("");
1356 f->store_line("BASE_PATH=\"$(app_realpath \"${BASH_SOURCE[0]}\")\"");
1357 f->store_line("\"$BASE_PATH/" + p_pkg_name + "\" \"$@\"");
1358 f->store_line("");
1359
1360 return OK;
1361}
1362
1363Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
1364 ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
1365
1366 String src_pkg_name;
1367
1368 EditorProgress ep("export", TTR("Exporting for macOS"), 3, true);
1369
1370 if (p_debug) {
1371 src_pkg_name = p_preset->get("custom_template/debug");
1372 } else {
1373 src_pkg_name = p_preset->get("custom_template/release");
1374 }
1375
1376 if (src_pkg_name.is_empty()) {
1377 String err;
1378 src_pkg_name = find_export_template("macos.zip", &err);
1379 if (src_pkg_name.is_empty()) {
1380 add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("Export template not found."));
1381 return ERR_FILE_NOT_FOUND;
1382 }
1383 }
1384
1385 if (!DirAccess::exists(p_path.get_base_dir())) {
1386 add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("The given export path doesn't exist."));
1387 return ERR_FILE_BAD_PATH;
1388 }
1389
1390 Ref<FileAccess> io_fa;
1391 zlib_filefunc_def io = zipio_create_io(&io_fa);
1392
1393 if (ep.step(TTR("Creating app bundle"), 0)) {
1394 return ERR_SKIP;
1395 }
1396
1397 unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io);
1398 if (!src_pkg_zip) {
1399 add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not find template app to export: \"%s\"."), src_pkg_name));
1400 return ERR_FILE_NOT_FOUND;
1401 }
1402
1403 int ret = unzGoToFirstFile(src_pkg_zip);
1404
1405 String architecture = p_preset->get("binary_format/architecture");
1406 String binary_to_use = "godot_macos_" + String(p_debug ? "debug" : "release") + "." + architecture;
1407
1408 String pkg_name;
1409 if (String(GLOBAL_GET("application/config/name")) != "") {
1410 pkg_name = String(GLOBAL_GET("application/config/name"));
1411 } else {
1412 pkg_name = "Unnamed";
1413 }
1414 pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);
1415
1416 String export_format;
1417 if (p_path.ends_with("zip")) {
1418 export_format = "zip";
1419 } else if (p_path.ends_with("app")) {
1420 export_format = "app";
1421#ifdef MACOS_ENABLED
1422 } else if (p_path.ends_with("dmg")) {
1423 export_format = "dmg";
1424 } else if (p_path.ends_with("pkg")) {
1425 export_format = "pkg";
1426#endif
1427 } else {
1428 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid export format."));
1429 return ERR_CANT_CREATE;
1430 }
1431
1432 // Create our application bundle.
1433 String tmp_app_dir_name = pkg_name + ".app";
1434 String tmp_base_path_name;
1435 String tmp_app_path_name;
1436 String scr_path;
1437 if (export_format == "app") {
1438 tmp_base_path_name = p_path.get_base_dir();
1439 tmp_app_path_name = p_path;
1440 scr_path = p_path.get_basename() + ".command";
1441 } else {
1442 tmp_base_path_name = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name);
1443 tmp_app_path_name = tmp_base_path_name.path_join(tmp_app_dir_name);
1444 scr_path = tmp_base_path_name.path_join(pkg_name + ".command");
1445 }
1446
1447 print_verbose("Exporting to " + tmp_app_path_name);
1448
1449 Error err = OK;
1450
1451 Ref<DirAccess> tmp_app_dir = DirAccess::create_for_path(tmp_base_path_name);
1452 if (tmp_app_dir.is_null()) {
1453 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory: \"%s\"."), tmp_base_path_name));
1454 err = ERR_CANT_CREATE;
1455 }
1456
1457 if (FileAccess::exists(scr_path)) {
1458 DirAccess::remove_file_or_error(scr_path);
1459 }
1460 if (DirAccess::exists(tmp_app_path_name)) {
1461 String old_dir = tmp_app_dir->get_current_dir();
1462 if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) {
1463 tmp_app_dir->erase_contents_recursive();
1464 tmp_app_dir->change_dir(old_dir);
1465 }
1466 }
1467
1468 Array helpers = p_preset->get("codesign/entitlements/app_sandbox/helper_executables");
1469
1470 // Create our folder structure.
1471 if (err == OK) {
1472 print_verbose("Creating " + tmp_app_path_name + "/Contents/MacOS");
1473 err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/MacOS");
1474 if (err != OK) {
1475 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/MacOS"));
1476 }
1477 }
1478
1479 if (err == OK) {
1480 print_verbose("Creating " + tmp_app_path_name + "/Contents/Frameworks");
1481 err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks");
1482 if (err != OK) {
1483 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Frameworks"));
1484 }
1485 }
1486
1487 if ((err == OK) && helpers.size() > 0) {
1488 print_line("Creating " + tmp_app_path_name + "/Contents/Helpers");
1489 err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Helpers");
1490 if (err != OK) {
1491 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Helpers"));
1492 }
1493 }
1494
1495 if (err == OK) {
1496 print_verbose("Creating " + tmp_app_path_name + "/Contents/Resources");
1497 err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Resources");
1498 if (err != OK) {
1499 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Resources"));
1500 }
1501 }
1502
1503 Dictionary appnames = GLOBAL_GET("application/config/name_localized");
1504 Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized");
1505 Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized");
1506 Dictionary location_usage_descriptions = p_preset->get("privacy/location_usage_description_localized");
1507 Dictionary address_book_usage_descriptions = p_preset->get("privacy/address_book_usage_description_localized");
1508 Dictionary calendar_usage_descriptions = p_preset->get("privacy/calendar_usage_description_localized");
1509 Dictionary photos_library_usage_descriptions = p_preset->get("privacy/photos_library_usage_description_localized");
1510 Dictionary desktop_folder_usage_descriptions = p_preset->get("privacy/desktop_folder_usage_description_localized");
1511 Dictionary documents_folder_usage_descriptions = p_preset->get("privacy/documents_folder_usage_description_localized");
1512 Dictionary downloads_folder_usage_descriptions = p_preset->get("privacy/downloads_folder_usage_description_localized");
1513 Dictionary network_volumes_usage_descriptions = p_preset->get("privacy/network_volumes_usage_description_localized");
1514 Dictionary removable_volumes_usage_descriptions = p_preset->get("privacy/removable_volumes_usage_description_localized");
1515 Dictionary copyrights = p_preset->get("application/copyright_localized");
1516
1517 Vector<String> translations = GLOBAL_GET("internationalization/locale/translations");
1518 if (translations.size() > 0) {
1519 {
1520 String fname = tmp_app_path_name + "/Contents/Resources/en.lproj";
1521 tmp_app_dir->make_dir_recursive(fname);
1522 Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
1523 f->store_line("/* Localized versions of Info.plist keys */");
1524 f->store_line("");
1525 f->store_line("CFBundleDisplayName = \"" + GLOBAL_GET("application/config/name").operator String() + "\";");
1526 if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
1527 f->store_line("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_usage_description").operator String() + "\";");
1528 }
1529 if (!((String)p_preset->get("privacy/camera_usage_description")).is_empty()) {
1530 f->store_line("NSCameraUsageDescription = \"" + p_preset->get("privacy/camera_usage_description").operator String() + "\";");
1531 }
1532 if (!((String)p_preset->get("privacy/location_usage_description")).is_empty()) {
1533 f->store_line("NSLocationUsageDescription = \"" + p_preset->get("privacy/location_usage_description").operator String() + "\";");
1534 }
1535 if (!((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) {
1536 f->store_line("NSContactsUsageDescription = \"" + p_preset->get("privacy/address_book_usage_description").operator String() + "\";");
1537 }
1538 if (!((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) {
1539 f->store_line("NSCalendarsUsageDescription = \"" + p_preset->get("privacy/calendar_usage_description").operator String() + "\";");
1540 }
1541 if (!((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) {
1542 f->store_line("NSPhotoLibraryUsageDescription = \"" + p_preset->get("privacy/photos_library_usage_description").operator String() + "\";");
1543 }
1544 if (!((String)p_preset->get("privacy/desktop_folder_usage_description")).is_empty()) {
1545 f->store_line("NSDesktopFolderUsageDescription = \"" + p_preset->get("privacy/desktop_folder_usage_description").operator String() + "\";");
1546 }
1547 if (!((String)p_preset->get("privacy/documents_folder_usage_description")).is_empty()) {
1548 f->store_line("NSDocumentsFolderUsageDescription = \"" + p_preset->get("privacy/documents_folder_usage_description").operator String() + "\";");
1549 }
1550 if (!((String)p_preset->get("privacy/downloads_folder_usage_description")).is_empty()) {
1551 f->store_line("NSDownloadsFolderUsageDescription = \"" + p_preset->get("privacy/downloads_folder_usage_description").operator String() + "\";");
1552 }
1553 if (!((String)p_preset->get("privacy/network_volumes_usage_description")).is_empty()) {
1554 f->store_line("NSNetworkVolumesUsageDescription = \"" + p_preset->get("privacy/network_volumes_usage_description").operator String() + "\";");
1555 }
1556 if (!((String)p_preset->get("privacy/removable_volumes_usage_description")).is_empty()) {
1557 f->store_line("NSRemovableVolumesUsageDescription = \"" + p_preset->get("privacy/removable_volumes_usage_description").operator String() + "\";");
1558 }
1559 f->store_line("NSHumanReadableCopyright = \"" + p_preset->get("application/copyright").operator String() + "\";");
1560 }
1561
1562 HashSet<String> languages;
1563 for (const String &E : translations) {
1564 Ref<Translation> tr = ResourceLoader::load(E);
1565 if (tr.is_valid() && tr->get_locale() != "en") {
1566 languages.insert(tr->get_locale());
1567 }
1568 }
1569
1570 for (const String &lang : languages) {
1571 String fname = tmp_app_path_name + "/Contents/Resources/" + lang + ".lproj";
1572 tmp_app_dir->make_dir_recursive(fname);
1573 Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
1574 f->store_line("/* Localized versions of Info.plist keys */");
1575 f->store_line("");
1576 if (appnames.has(lang)) {
1577 f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";");
1578 }
1579 if (microphone_usage_descriptions.has(lang)) {
1580 f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";");
1581 }
1582 if (camera_usage_descriptions.has(lang)) {
1583 f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";");
1584 }
1585 if (location_usage_descriptions.has(lang)) {
1586 f->store_line("NSLocationUsageDescription = \"" + location_usage_descriptions[lang].operator String() + "\";");
1587 }
1588 if (address_book_usage_descriptions.has(lang)) {
1589 f->store_line("NSContactsUsageDescription = \"" + address_book_usage_descriptions[lang].operator String() + "\";");
1590 }
1591 if (calendar_usage_descriptions.has(lang)) {
1592 f->store_line("NSCalendarsUsageDescription = \"" + calendar_usage_descriptions[lang].operator String() + "\";");
1593 }
1594 if (photos_library_usage_descriptions.has(lang)) {
1595 f->store_line("NSPhotoLibraryUsageDescription = \"" + photos_library_usage_descriptions[lang].operator String() + "\";");
1596 }
1597 if (desktop_folder_usage_descriptions.has(lang)) {
1598 f->store_line("NSDesktopFolderUsageDescription = \"" + desktop_folder_usage_descriptions[lang].operator String() + "\";");
1599 }
1600 if (documents_folder_usage_descriptions.has(lang)) {
1601 f->store_line("NSDocumentsFolderUsageDescription = \"" + documents_folder_usage_descriptions[lang].operator String() + "\";");
1602 }
1603 if (downloads_folder_usage_descriptions.has(lang)) {
1604 f->store_line("NSDownloadsFolderUsageDescription = \"" + downloads_folder_usage_descriptions[lang].operator String() + "\";");
1605 }
1606 if (network_volumes_usage_descriptions.has(lang)) {
1607 f->store_line("NSNetworkVolumesUsageDescription = \"" + network_volumes_usage_descriptions[lang].operator String() + "\";");
1608 }
1609 if (removable_volumes_usage_descriptions.has(lang)) {
1610 f->store_line("NSRemovableVolumesUsageDescription = \"" + removable_volumes_usage_descriptions[lang].operator String() + "\";");
1611 }
1612 if (copyrights.has(lang)) {
1613 f->store_line("NSHumanReadableCopyright = \"" + copyrights[lang].operator String() + "\";");
1614 }
1615 }
1616 }
1617
1618 // Now process our template.
1619 bool found_binary = false;
1620
1621 while (ret == UNZ_OK && err == OK) {
1622 // Get filename.
1623 unz_file_info info;
1624 char fname[16384];
1625 ret = unzGetCurrentFileInfo(src_pkg_zip, &info, fname, 16384, nullptr, 0, nullptr, 0);
1626 if (ret != UNZ_OK) {
1627 break;
1628 }
1629
1630 String file = String::utf8(fname);
1631
1632 Vector<uint8_t> data;
1633 data.resize(info.uncompressed_size);
1634
1635 // Read.
1636 unzOpenCurrentFile(src_pkg_zip);
1637 unzReadCurrentFile(src_pkg_zip, data.ptrw(), data.size());
1638 unzCloseCurrentFile(src_pkg_zip);
1639
1640 // Write.
1641 file = file.replace_first("macos_template.app/", "");
1642
1643 if (((info.external_fa >> 16L) & 0120000) == 0120000) {
1644#ifndef UNIX_ENABLED
1645 add_message(EXPORT_MESSAGE_INFO, TTR("Export"), TTR("Relative symlinks are not supported on this OS, the exported project might be broken!"));
1646#endif
1647 // Handle symlinks in the archive.
1648 file = tmp_app_path_name.path_join(file);
1649 if (err == OK) {
1650 err = tmp_app_dir->make_dir_recursive(file.get_base_dir());
1651 if (err != OK) {
1652 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir()));
1653 }
1654 }
1655 if (err == OK) {
1656 String lnk_data = String::utf8((const char *)data.ptr(), data.size());
1657 err = tmp_app_dir->create_link(lnk_data, file);
1658 if (err != OK) {
1659 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not created symlink \"%s\" -> \"%s\"."), lnk_data, file));
1660 }
1661 print_verbose(vformat("ADDING SYMLINK %s => %s\n", file, lnk_data));
1662 }
1663
1664 ret = unzGoToNextFile(src_pkg_zip);
1665 continue; // next
1666 }
1667
1668 if (file == "Contents/Info.plist") {
1669 _fix_plist(p_preset, data, pkg_name);
1670 }
1671
1672 if (file.begins_with("Contents/MacOS/godot_")) {
1673 if (file != "Contents/MacOS/" + binary_to_use) {
1674 ret = unzGoToNextFile(src_pkg_zip);
1675 continue; // skip
1676 }
1677 found_binary = true;
1678 file = "Contents/MacOS/" + pkg_name;
1679 }
1680
1681 if (file == "Contents/Resources/icon.icns") {
1682 // See if there is an icon.
1683 String icon_path;
1684 if (p_preset->get("application/icon") != "") {
1685 icon_path = p_preset->get("application/icon");
1686 } else if (GLOBAL_GET("application/config/macos_native_icon") != "") {
1687 icon_path = GLOBAL_GET("application/config/macos_native_icon");
1688 } else {
1689 icon_path = GLOBAL_GET("application/config/icon");
1690 }
1691
1692 if (!icon_path.is_empty()) {
1693 if (icon_path.get_extension() == "icns") {
1694 Ref<FileAccess> icon = FileAccess::open(icon_path, FileAccess::READ);
1695 if (icon.is_valid()) {
1696 data.resize(icon->get_length());
1697 icon->get_buffer(&data.write[0], icon->get_length());
1698 }
1699 } else {
1700 Ref<Image> icon;
1701 icon.instantiate();
1702 err = ImageLoader::load_image(icon_path, icon);
1703 if (err == OK && !icon->is_empty()) {
1704 _make_icon(p_preset, icon, data);
1705 }
1706 }
1707 }
1708 }
1709
1710 if (data.size() > 0) {
1711 print_verbose("ADDING: " + file + " size: " + itos(data.size()));
1712
1713 // Write it into our application bundle.
1714 file = tmp_app_path_name.path_join(file);
1715 if (err == OK) {
1716 err = tmp_app_dir->make_dir_recursive(file.get_base_dir());
1717 if (err != OK) {
1718 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir()));
1719 }
1720 }
1721 if (err == OK) {
1722 Ref<FileAccess> f = FileAccess::open(file, FileAccess::WRITE);
1723 if (f.is_valid()) {
1724 f->store_buffer(data.ptr(), data.size());
1725 f.unref();
1726 if (is_executable(file)) {
1727 // chmod with 0755 if the file is executable.
1728 FileAccess::set_unix_permissions(file, 0755);
1729 }
1730 } else {
1731 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not open \"%s\"."), file));
1732 err = ERR_CANT_CREATE;
1733 }
1734 }
1735 }
1736
1737 ret = unzGoToNextFile(src_pkg_zip);
1738 }
1739
1740 // We're done with our source zip.
1741 unzClose(src_pkg_zip);
1742
1743 if (!found_binary) {
1744 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Requested template binary \"%s\" not found. It might be missing from your template archive."), binary_to_use));
1745 err = ERR_FILE_NOT_FOUND;
1746 }
1747
1748 // Save console wrapper.
1749 if (err == OK) {
1750 int con_scr = p_preset->get("debug/export_console_wrapper");
1751 if ((con_scr == 1 && p_debug) || (con_scr == 2)) {
1752 err = _export_debug_script(p_preset, pkg_name, tmp_app_path_name.get_file() + "/Contents/MacOS/" + pkg_name, scr_path);
1753 FileAccess::set_unix_permissions(scr_path, 0755);
1754 if (err != OK) {
1755 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not create console wrapper."));
1756 }
1757 }
1758 }
1759
1760 if (err == OK) {
1761 if (ep.step(TTR("Making PKG"), 1)) {
1762 return ERR_SKIP;
1763 }
1764
1765 // See if we can code sign our new package.
1766 bool sign_enabled = (p_preset->get("codesign/codesign").operator int() > 0);
1767 bool ad_hoc = false;
1768 int codesign_tool = p_preset->get("codesign/codesign");
1769 switch (codesign_tool) {
1770 case 1: { // built-in ad-hoc
1771 ad_hoc = true;
1772 } break;
1773 case 2: { // "rcodesign"
1774 ad_hoc = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty() || p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_PASS).operator String().is_empty();
1775 } break;
1776#ifdef MACOS_ENABLED
1777 case 3: { // "codesign"
1778 ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");
1779 } break;
1780#endif
1781 default: {
1782 };
1783 }
1784
1785 String pack_path = tmp_app_path_name + "/Contents/Resources/" + pkg_name + ".pck";
1786 Vector<SharedObject> shared_objects;
1787 err = save_pack(p_preset, p_debug, pack_path, &shared_objects);
1788
1789 bool lib_validation = p_preset->get("codesign/entitlements/disable_library_validation");
1790 if (!shared_objects.is_empty() && sign_enabled && ad_hoc && !lib_validation) {
1791 add_message(EXPORT_MESSAGE_INFO, TTR("Entitlements Modified"), TTR("Ad-hoc signed applications require the 'Disable Library Validation' entitlement to load dynamic libraries."));
1792 lib_validation = true;
1793 }
1794
1795 String ent_path = p_preset->get("codesign/entitlements/custom_file");
1796 String hlp_ent_path = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name + "_helper.entitlements");
1797 if (sign_enabled && (ent_path.is_empty())) {
1798 ent_path = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name + ".entitlements");
1799
1800 Ref<FileAccess> ent_f = FileAccess::open(ent_path, FileAccess::WRITE);
1801 if (ent_f.is_valid()) {
1802 ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
1803 ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
1804 ent_f->store_line("<plist version=\"1.0\">");
1805 ent_f->store_line("<dict>");
1806 if (Engine::get_singleton()->has_singleton("GodotSharp")) {
1807 // These entitlements are required to run managed code, and are always enabled in Mono builds.
1808 ent_f->store_line("<key>com.apple.security.cs.allow-jit</key>");
1809 ent_f->store_line("<true/>");
1810 ent_f->store_line("<key>com.apple.security.cs.allow-unsigned-executable-memory</key>");
1811 ent_f->store_line("<true/>");
1812 ent_f->store_line("<key>com.apple.security.cs.allow-dyld-environment-variables</key>");
1813 ent_f->store_line("<true/>");
1814 } else {
1815 if ((bool)p_preset->get("codesign/entitlements/allow_jit_code_execution")) {
1816 ent_f->store_line("<key>com.apple.security.cs.allow-jit</key>");
1817 ent_f->store_line("<true/>");
1818 }
1819 if ((bool)p_preset->get("codesign/entitlements/allow_unsigned_executable_memory")) {
1820 ent_f->store_line("<key>com.apple.security.cs.allow-unsigned-executable-memory</key>");
1821 ent_f->store_line("<true/>");
1822 }
1823 if ((bool)p_preset->get("codesign/entitlements/allow_dyld_environment_variables")) {
1824 ent_f->store_line("<key>com.apple.security.cs.allow-dyld-environment-variables</key>");
1825 ent_f->store_line("<true/>");
1826 }
1827 }
1828
1829 if (lib_validation) {
1830 ent_f->store_line("<key>com.apple.security.cs.disable-library-validation</key>");
1831 ent_f->store_line("<true/>");
1832 }
1833 if ((bool)p_preset->get("codesign/entitlements/audio_input")) {
1834 ent_f->store_line("<key>com.apple.security.device.audio-input</key>");
1835 ent_f->store_line("<true/>");
1836 }
1837 if ((bool)p_preset->get("codesign/entitlements/camera")) {
1838 ent_f->store_line("<key>com.apple.security.device.camera</key>");
1839 ent_f->store_line("<true/>");
1840 }
1841 if ((bool)p_preset->get("codesign/entitlements/location")) {
1842 ent_f->store_line("<key>com.apple.security.personal-information.location</key>");
1843 ent_f->store_line("<true/>");
1844 }
1845 if ((bool)p_preset->get("codesign/entitlements/address_book")) {
1846 ent_f->store_line("<key>com.apple.security.personal-information.addressbook</key>");
1847 ent_f->store_line("<true/>");
1848 }
1849 if ((bool)p_preset->get("codesign/entitlements/calendars")) {
1850 ent_f->store_line("<key>com.apple.security.personal-information.calendars</key>");
1851 ent_f->store_line("<true/>");
1852 }
1853 if ((bool)p_preset->get("codesign/entitlements/photos_library")) {
1854 ent_f->store_line("<key>com.apple.security.personal-information.photos-library</key>");
1855 ent_f->store_line("<true/>");
1856 }
1857 if ((bool)p_preset->get("codesign/entitlements/apple_events")) {
1858 ent_f->store_line("<key>com.apple.security.automation.apple-events</key>");
1859 ent_f->store_line("<true/>");
1860 }
1861 if ((bool)p_preset->get("codesign/entitlements/debugging")) {
1862 ent_f->store_line("<key>com.apple.security.get-task-allow</key>");
1863 ent_f->store_line("<true/>");
1864 }
1865
1866 int dist_type = p_preset->get("export/distribution_type");
1867 if (dist_type == 2) {
1868 String pprof = p_preset->get_or_env("codesign/provisioning_profile", ENV_MAC_CODESIGN_PROFILE);
1869 String teamid = p_preset->get("codesign/apple_team_id");
1870 String bid = p_preset->get("application/bundle_identifier");
1871 if (!pprof.is_empty() && !teamid.is_empty()) {
1872 ent_f->store_line("<key>com.apple.developer.team-identifier</key>");
1873 ent_f->store_line("<string>" + teamid + "</string>");
1874 ent_f->store_line("<key>com.apple.application-identifier</key>");
1875 ent_f->store_line("<string>" + teamid + "." + bid + "</string>");
1876 }
1877 }
1878
1879 if ((bool)p_preset->get("codesign/entitlements/app_sandbox/enabled")) {
1880 ent_f->store_line("<key>com.apple.security.app-sandbox</key>");
1881 ent_f->store_line("<true/>");
1882
1883 if ((bool)p_preset->get("codesign/entitlements/app_sandbox/network_server")) {
1884 ent_f->store_line("<key>com.apple.security.network.server</key>");
1885 ent_f->store_line("<true/>");
1886 }
1887 if ((bool)p_preset->get("codesign/entitlements/app_sandbox/network_client")) {
1888 ent_f->store_line("<key>com.apple.security.network.client</key>");
1889 ent_f->store_line("<true/>");
1890 }
1891 if ((bool)p_preset->get("codesign/entitlements/app_sandbox/device_usb")) {
1892 ent_f->store_line("<key>com.apple.security.device.usb</key>");
1893 ent_f->store_line("<true/>");
1894 }
1895 if ((bool)p_preset->get("codesign/entitlements/app_sandbox/device_bluetooth")) {
1896 ent_f->store_line("<key>com.apple.security.device.bluetooth</key>");
1897 ent_f->store_line("<true/>");
1898 }
1899 if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_downloads") == 1) {
1900 ent_f->store_line("<key>com.apple.security.files.downloads.read-only</key>");
1901 ent_f->store_line("<true/>");
1902 }
1903 if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_downloads") == 2) {
1904 ent_f->store_line("<key>com.apple.security.files.downloads.read-write</key>");
1905 ent_f->store_line("<true/>");
1906 }
1907 if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_pictures") == 1) {
1908 ent_f->store_line("<key>com.apple.security.files.pictures.read-only</key>");
1909 ent_f->store_line("<true/>");
1910 }
1911 if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_pictures") == 2) {
1912 ent_f->store_line("<key>com.apple.security.files.pictures.read-write</key>");
1913 ent_f->store_line("<true/>");
1914 }
1915 if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_music") == 1) {
1916 ent_f->store_line("<key>com.apple.security.files.music.read-only</key>");
1917 ent_f->store_line("<true/>");
1918 }
1919 if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_music") == 2) {
1920 ent_f->store_line("<key>com.apple.security.files.music.read-write</key>");
1921 ent_f->store_line("<true/>");
1922 }
1923 if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_movies") == 1) {
1924 ent_f->store_line("<key>com.apple.security.files.movies.read-only</key>");
1925 ent_f->store_line("<true/>");
1926 }
1927 if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_movies") == 2) {
1928 ent_f->store_line("<key>com.apple.security.files.movies.read-write</key>");
1929 ent_f->store_line("<true/>");
1930 }
1931 if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 1) {
1932 ent_f->store_line("<key>com.apple.security.files.user-selected.read-only</key>");
1933 ent_f->store_line("<true/>");
1934 }
1935 if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 2) {
1936 ent_f->store_line("<key>com.apple.security.files.user-selected.read-write</key>");
1937 ent_f->store_line("<true/>");
1938 }
1939 }
1940
1941 ent_f->store_line("</dict>");
1942 ent_f->store_line("</plist>");
1943 } else {
1944 add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create entitlements file."));
1945 err = ERR_CANT_CREATE;
1946 }
1947
1948 if ((err == OK) && helpers.size() > 0) {
1949 ent_f = FileAccess::open(hlp_ent_path, FileAccess::WRITE);
1950 if (ent_f.is_valid()) {
1951 ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
1952 ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
1953 ent_f->store_line("<plist version=\"1.0\">");
1954 ent_f->store_line("<dict>");
1955 ent_f->store_line("<key>com.apple.security.app-sandbox</key>");
1956 ent_f->store_line("<true/>");
1957 ent_f->store_line("<key>com.apple.security.inherit</key>");
1958 ent_f->store_line("<true/>");
1959 ent_f->store_line("</dict>");
1960 ent_f->store_line("</plist>");
1961 } else {
1962 add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create helper entitlements file."));
1963 err = ERR_CANT_CREATE;
1964 }
1965 }
1966 }
1967
1968 if ((err == OK) && helpers.size() > 0) {
1969 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
1970 for (int i = 0; i < helpers.size(); i++) {
1971 String hlp_path = helpers[i];
1972 err = da->copy(hlp_path, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file());
1973 if (err == OK && sign_enabled) {
1974 err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false);
1975 }
1976 FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755);
1977 }
1978 }
1979
1980 if (err == OK) {
1981 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
1982 for (int i = 0; i < shared_objects.size(); i++) {
1983 String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path);
1984 if (shared_objects[i].target.is_empty()) {
1985 String path_in_app = tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file();
1986 err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, true);
1987 } else {
1988 String path_in_app = tmp_app_path_name.path_join(shared_objects[i].target);
1989 tmp_app_dir->make_dir_recursive(path_in_app);
1990 err = _copy_and_sign_files(da, src_path, path_in_app.path_join(src_path.get_file()), sign_enabled, p_preset, ent_path, false);
1991 }
1992 if (err != OK) {
1993 break;
1994 }
1995 }
1996
1997 Vector<Ref<EditorExportPlugin>> export_plugins{ EditorExport::get_singleton()->get_export_plugins() };
1998 for (int i = 0; i < export_plugins.size(); ++i) {
1999 err = _export_macos_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path);
2000 if (err != OK) {
2001 break;
2002 }
2003 }
2004 }
2005
2006 if (err == OK && sign_enabled) {
2007 int dist_type = p_preset->get("export/distribution_type");
2008 if (dist_type == 2) {
2009 String pprof = p_preset->get_or_env("codesign/provisioning_profile", ENV_MAC_CODESIGN_PROFILE).operator String();
2010 if (!pprof.is_empty()) {
2011 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
2012 err = da->copy(pprof, tmp_app_path_name + "/Contents/embedded.provisionprofile");
2013 }
2014 }
2015
2016 if (ep.step(TTR("Code signing bundle"), 2)) {
2017 return ERR_SKIP;
2018 }
2019 err = _code_sign(p_preset, tmp_app_path_name, ent_path);
2020 }
2021
2022 if (export_format == "dmg") {
2023 // Create a DMG.
2024 if (err == OK) {
2025 if (ep.step(TTR("Making DMG"), 3)) {
2026 return ERR_SKIP;
2027 }
2028 err = _create_dmg(p_path, pkg_name, tmp_base_path_name);
2029 }
2030 // Sign DMG.
2031 if (err == OK && sign_enabled && !ad_hoc) {
2032 if (ep.step(TTR("Code signing DMG"), 3)) {
2033 return ERR_SKIP;
2034 }
2035 err = _code_sign(p_preset, p_path, ent_path, false);
2036 }
2037 } else if (export_format == "pkg") {
2038 // Create a Installer.
2039 if (err == OK) {
2040 if (ep.step(TTR("Making PKG installer"), 3)) {
2041 return ERR_SKIP;
2042 }
2043 err = _create_pkg(p_preset, p_path, tmp_app_path_name);
2044 }
2045 } else if (export_format == "zip") {
2046 // Create ZIP.
2047 if (err == OK) {
2048 if (ep.step(TTR("Making ZIP"), 3)) {
2049 return ERR_SKIP;
2050 }
2051 if (FileAccess::exists(p_path)) {
2052 OS::get_singleton()->move_to_trash(p_path);
2053 }
2054
2055 Ref<FileAccess> io_fa_dst;
2056 zlib_filefunc_def io_dst = zipio_create_io(&io_fa_dst);
2057 zipFile zip = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io_dst);
2058
2059 zip_folder_recursive(zip, tmp_base_path_name, "", pkg_name);
2060
2061 zipClose(zip, nullptr);
2062 }
2063 }
2064
2065 bool noto_enabled = (p_preset->get("notarization/notarization").operator int() > 0);
2066 if (err == OK && noto_enabled) {
2067 if (export_format == "app" || export_format == "pkg") {
2068 add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("Notarization requires the app to be archived first, select the DMG or ZIP export format instead."));
2069 } else {
2070 if (ep.step(TTR("Sending archive for notarization"), 4)) {
2071 return ERR_SKIP;
2072 }
2073 err = _notarize(p_preset, p_path);
2074 }
2075 }
2076
2077 // Clean up temporary entitlements files.
2078 if (FileAccess::exists(hlp_ent_path)) {
2079 DirAccess::remove_file_or_error(hlp_ent_path);
2080 }
2081
2082 // Clean up temporary .app dir and generated entitlements.
2083 if ((String)(p_preset->get("codesign/entitlements/custom_file")) == "") {
2084 tmp_app_dir->remove(ent_path);
2085 }
2086 if (export_format != "app") {
2087 if (tmp_app_dir->change_dir(tmp_base_path_name) == OK) {
2088 tmp_app_dir->erase_contents_recursive();
2089 tmp_app_dir->change_dir("..");
2090 tmp_app_dir->remove(pkg_name);
2091 }
2092 }
2093 }
2094
2095 return err;
2096}
2097
2098bool EditorExportPlatformMacOS::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
2099 String err;
2100 // Look for export templates (custom templates).
2101 bool dvalid = false;
2102 bool rvalid = false;
2103
2104 if (p_preset->get("custom_template/debug") != "") {
2105 dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
2106 if (!dvalid) {
2107 err += TTR("Custom debug template not found.") + "\n";
2108 }
2109 }
2110 if (p_preset->get("custom_template/release") != "") {
2111 rvalid = FileAccess::exists(p_preset->get("custom_template/release"));
2112 if (!rvalid) {
2113 err += TTR("Custom release template not found.") + "\n";
2114 }
2115 }
2116
2117 // Look for export templates (official templates, check only is custom templates are not set).
2118 if (!dvalid || !rvalid) {
2119 dvalid = exists_export_template("macos.zip", &err);
2120 rvalid = dvalid; // Both in the same ZIP.
2121 }
2122
2123 bool valid = dvalid || rvalid;
2124 r_missing_templates = !valid;
2125
2126 // Check the texture formats, which vary depending on the target architecture.
2127 String architecture = p_preset->get("binary_format/architecture");
2128 if (architecture == "universal" || architecture == "x86_64") {
2129 if (!ResourceImporterTextureSettings::should_import_s3tc_bptc()) {
2130 valid = false;
2131 }
2132 } else if (architecture == "arm64") {
2133 if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {
2134 valid = false;
2135 }
2136 } else {
2137 ERR_PRINT("Invalid architecture");
2138 }
2139
2140 if (!err.is_empty()) {
2141 r_error = err;
2142 }
2143 return valid;
2144}
2145
2146bool EditorExportPlatformMacOS::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
2147 String err;
2148 bool valid = true;
2149
2150 int dist_type = p_preset->get("export/distribution_type");
2151 bool ad_hoc = false;
2152 int codesign_tool = p_preset->get("codesign/codesign");
2153 int notary_tool = p_preset->get("notarization/notarization");
2154 switch (codesign_tool) {
2155 case 1: { // built-in ad-hoc
2156 ad_hoc = true;
2157 } break;
2158 case 2: { // "rcodesign"
2159 ad_hoc = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty() || p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_PASS).operator String().is_empty();
2160 } break;
2161#ifdef MACOS_ENABLED
2162 case 3: { // "codesign"
2163 ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");
2164 } break;
2165#endif
2166 default: {
2167 };
2168 }
2169
2170 List<ExportOption> options;
2171 get_export_options(&options);
2172 for (const EditorExportPlatform::ExportOption &E : options) {
2173 if (get_export_option_visibility(p_preset.ptr(), E.option.name)) {
2174 String warn = get_export_option_warning(p_preset.ptr(), E.option.name);
2175 if (!warn.is_empty()) {
2176 err += warn + "\n";
2177 if (E.required) {
2178 valid = false;
2179 }
2180 }
2181 }
2182 }
2183
2184 if (dist_type != 2) {
2185 if (notary_tool > 0) {
2186 if (notary_tool == 2 || notary_tool == 3) {
2187 if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) {
2188 err += TTR("Notarization: Xcode command line tools are not installed.") + "\n";
2189 valid = false;
2190 }
2191 } else if (notary_tool == 1) {
2192 String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String();
2193 if (rcodesign.is_empty()) {
2194 err += TTR("Notarization: rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).") + "\n";
2195 valid = false;
2196 }
2197 }
2198 } else {
2199 err += TTR("Warning: Notarization is disabled. The exported project will be blocked by Gatekeeper if it's downloaded from an unknown source.") + "\n";
2200 if (codesign_tool == 0) {
2201 err += TTR("Code signing is disabled. The exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n";
2202 }
2203 }
2204 }
2205
2206 if (codesign_tool > 0) {
2207 if (ad_hoc) {
2208 err += TTR("Code signing: Using ad-hoc signature. The exported project will be blocked by Gatekeeper") + "\n";
2209 }
2210 if (codesign_tool == 3) {
2211 if (!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) {
2212 err += TTR("Code signing: Xcode command line tools are not installed.") + "\n";
2213 valid = false;
2214 }
2215 } else if (codesign_tool == 2) {
2216 String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String();
2217 if (rcodesign.is_empty()) {
2218 err += TTR("Code signing: rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).") + "\n";
2219 valid = false;
2220 }
2221 }
2222 }
2223
2224 if (!err.is_empty()) {
2225 r_error = err;
2226 }
2227 return valid;
2228}
2229
2230Ref<Texture2D> EditorExportPlatformMacOS::get_run_icon() const {
2231 return run_icon;
2232}
2233
2234bool EditorExportPlatformMacOS::poll_export() {
2235 Ref<EditorExportPreset> preset;
2236
2237 for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
2238 Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i);
2239 if (ep->is_runnable() && ep->get_platform() == this) {
2240 preset = ep;
2241 break;
2242 }
2243 }
2244
2245 int prev = menu_options;
2246 menu_options = (preset.is_valid() && preset->get("ssh_remote_deploy/enabled").operator bool());
2247 if (ssh_pid != 0 || !cleanup_commands.is_empty()) {
2248 if (menu_options == 0) {
2249 cleanup();
2250 } else {
2251 menu_options += 1;
2252 }
2253 }
2254 return menu_options != prev;
2255}
2256
2257Ref<ImageTexture> EditorExportPlatformMacOS::get_option_icon(int p_index) const {
2258 return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index);
2259}
2260
2261int EditorExportPlatformMacOS::get_options_count() const {
2262 return menu_options;
2263}
2264
2265String EditorExportPlatformMacOS::get_option_label(int p_index) const {
2266 return (p_index) ? TTR("Stop and uninstall") : TTR("Run on remote macOS system");
2267}
2268
2269String EditorExportPlatformMacOS::get_option_tooltip(int p_index) const {
2270 return (p_index) ? TTR("Stop and uninstall running project from the remote system") : TTR("Run exported project on remote macOS system");
2271}
2272
2273void EditorExportPlatformMacOS::cleanup() {
2274 if (ssh_pid != 0 && OS::get_singleton()->is_process_running(ssh_pid)) {
2275 print_line("Terminating connection...");
2276 OS::get_singleton()->kill(ssh_pid);
2277 OS::get_singleton()->delay_usec(1000);
2278 }
2279
2280 if (!cleanup_commands.is_empty()) {
2281 print_line("Stopping and deleting previous version...");
2282 for (const SSHCleanupCommand &cmd : cleanup_commands) {
2283 if (cmd.wait) {
2284 ssh_run_on_remote(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);
2285 } else {
2286 ssh_run_on_remote_no_wait(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);
2287 }
2288 }
2289 }
2290 ssh_pid = 0;
2291 cleanup_commands.clear();
2292}
2293
2294Error EditorExportPlatformMacOS::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) {
2295 cleanup();
2296 if (p_device) { // Stop command, cleanup only.
2297 return OK;
2298 }
2299
2300 EditorProgress ep("run", TTR("Running..."), 5);
2301
2302 const String dest = EditorPaths::get_singleton()->get_cache_dir().path_join("macos");
2303 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
2304 if (!da->dir_exists(dest)) {
2305 Error err = da->make_dir_recursive(dest);
2306 if (err != OK) {
2307 EditorNode::get_singleton()->show_warning(TTR("Could not create temp directory:") + "\n" + dest);
2308 return err;
2309 }
2310 }
2311
2312 String pkg_name;
2313 if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") {
2314 pkg_name = String(ProjectSettings::get_singleton()->get("application/config/name"));
2315 } else {
2316 pkg_name = "Unnamed";
2317 }
2318 pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);
2319
2320 String host = p_preset->get("ssh_remote_deploy/host").operator String();
2321 String port = p_preset->get("ssh_remote_deploy/port").operator String();
2322 if (port.is_empty()) {
2323 port = "22";
2324 }
2325 Vector<String> extra_args_ssh = p_preset->get("ssh_remote_deploy/extra_args_ssh").operator String().split(" ", false);
2326 Vector<String> extra_args_scp = p_preset->get("ssh_remote_deploy/extra_args_scp").operator String().split(" ", false);
2327
2328 const String basepath = dest.path_join("tmp_macos_export");
2329
2330#define CLEANUP_AND_RETURN(m_err) \
2331 { \
2332 if (da->file_exists(basepath + ".zip")) { \
2333 da->remove(basepath + ".zip"); \
2334 } \
2335 if (da->file_exists(basepath + "_start.sh")) { \
2336 da->remove(basepath + "_start.sh"); \
2337 } \
2338 if (da->file_exists(basepath + "_clean.sh")) { \
2339 da->remove(basepath + "_clean.sh"); \
2340 } \
2341 return m_err; \
2342 } \
2343 ((void)0)
2344
2345 if (ep.step(TTR("Exporting project..."), 1)) {
2346 return ERR_SKIP;
2347 }
2348 Error err = export_project(p_preset, true, basepath + ".zip", p_debug_flags);
2349 if (err != OK) {
2350 DirAccess::remove_file_or_error(basepath + ".zip");
2351 return err;
2352 }
2353
2354 String cmd_args;
2355 {
2356 Vector<String> cmd_args_list;
2357 gen_debug_flags(cmd_args_list, p_debug_flags);
2358 for (int i = 0; i < cmd_args_list.size(); i++) {
2359 if (i != 0) {
2360 cmd_args += " ";
2361 }
2362 cmd_args += cmd_args_list[i];
2363 }
2364 }
2365
2366 const bool use_remote = (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) || (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT);
2367 int dbg_port = EditorSettings::get_singleton()->get("network/debug/remote_port");
2368
2369 print_line("Creating temporary directory...");
2370 ep.step(TTR("Creating temporary directory..."), 2);
2371 String temp_dir;
2372 err = ssh_run_on_remote(host, port, extra_args_ssh, "mktemp -d", &temp_dir);
2373 if (err != OK || temp_dir.is_empty()) {
2374 CLEANUP_AND_RETURN(err);
2375 }
2376
2377 print_line("Uploading archive...");
2378 ep.step(TTR("Uploading archive..."), 3);
2379 err = ssh_push_to_remote(host, port, extra_args_scp, basepath + ".zip", temp_dir);
2380 if (err != OK) {
2381 CLEANUP_AND_RETURN(err);
2382 }
2383
2384 {
2385 String run_script = p_preset->get("ssh_remote_deploy/run_script");
2386 run_script = run_script.replace("{temp_dir}", temp_dir);
2387 run_script = run_script.replace("{archive_name}", basepath.get_file() + ".zip");
2388 run_script = run_script.replace("{exe_name}", pkg_name);
2389 run_script = run_script.replace("{cmd_args}", cmd_args);
2390
2391 Ref<FileAccess> f = FileAccess::open(basepath + "_start.sh", FileAccess::WRITE);
2392 if (f.is_null()) {
2393 CLEANUP_AND_RETURN(err);
2394 }
2395
2396 f->store_string(run_script);
2397 }
2398
2399 {
2400 String clean_script = p_preset->get("ssh_remote_deploy/cleanup_script");
2401 clean_script = clean_script.replace("{temp_dir}", temp_dir);
2402 clean_script = clean_script.replace("{archive_name}", basepath.get_file() + ".zip");
2403 clean_script = clean_script.replace("{exe_name}", pkg_name);
2404 clean_script = clean_script.replace("{cmd_args}", cmd_args);
2405
2406 Ref<FileAccess> f = FileAccess::open(basepath + "_clean.sh", FileAccess::WRITE);
2407 if (f.is_null()) {
2408 CLEANUP_AND_RETURN(err);
2409 }
2410
2411 f->store_string(clean_script);
2412 }
2413
2414 print_line("Uploading scripts...");
2415 ep.step(TTR("Uploading scripts..."), 4);
2416 err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_start.sh", temp_dir);
2417 if (err != OK) {
2418 CLEANUP_AND_RETURN(err);
2419 }
2420 err = ssh_run_on_remote(host, port, extra_args_ssh, vformat("chmod +x \"%s/%s\"", temp_dir, basepath.get_file() + "_start.sh"));
2421 if (err != OK || temp_dir.is_empty()) {
2422 CLEANUP_AND_RETURN(err);
2423 }
2424 err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_clean.sh", temp_dir);
2425 if (err != OK) {
2426 CLEANUP_AND_RETURN(err);
2427 }
2428 err = ssh_run_on_remote(host, port, extra_args_ssh, vformat("chmod +x \"%s/%s\"", temp_dir, basepath.get_file() + "_clean.sh"));
2429 if (err != OK || temp_dir.is_empty()) {
2430 CLEANUP_AND_RETURN(err);
2431 }
2432
2433 print_line("Starting project...");
2434 ep.step(TTR("Starting project..."), 5);
2435 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);
2436 if (err != OK) {
2437 CLEANUP_AND_RETURN(err);
2438 }
2439
2440 cleanup_commands.clear();
2441 cleanup_commands.push_back(SSHCleanupCommand(host, port, extra_args_ssh, vformat("\"%s/%s\"", temp_dir, basepath.get_file() + "_clean.sh")));
2442
2443 print_line("Project started.");
2444
2445 CLEANUP_AND_RETURN(OK);
2446#undef CLEANUP_AND_RETURN
2447}
2448
2449EditorExportPlatformMacOS::EditorExportPlatformMacOS() {
2450 if (EditorNode::get_singleton()) {
2451#ifdef MODULE_SVG_ENABLED
2452 Ref<Image> img = memnew(Image);
2453 const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
2454
2455 ImageLoaderSVG::create_image_from_string(img, _macos_logo_svg, EDSCALE, upsample, false);
2456 logo = ImageTexture::create_from_image(img);
2457
2458 ImageLoaderSVG::create_image_from_string(img, _macos_run_icon_svg, EDSCALE, upsample, false);
2459 run_icon = ImageTexture::create_from_image(img);
2460#endif
2461
2462 Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
2463 if (theme.is_valid()) {
2464 stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons));
2465 } else {
2466 stop_icon.instantiate();
2467 }
2468 }
2469}
2470