1/**************************************************************************/
2/* export_plugin.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "export_plugin.h"
32
33#include "logo_svg.gen.h"
34#include "run_icon_svg.gen.h"
35
36#include "core/io/json.h"
37#include "core/string/translation.h"
38#include "editor/editor_node.h"
39#include "editor/editor_paths.h"
40#include "editor/editor_scale.h"
41#include "editor/editor_string_names.h"
42#include "editor/export/editor_export.h"
43#include "editor/import/resource_importer_texture_settings.h"
44#include "editor/plugins/script_editor_plugin.h"
45
46#include "modules/modules_enabled.gen.h" // For mono and svg.
47#ifdef MODULE_SVG_ENABLED
48#include "modules/svg/image_loader_svg.h"
49#endif
50
51void EditorExportPlatformIOS::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {
52 // Vulkan and OpenGL ES 3.0 both mandate ETC2 support.
53 r_features->push_back("etc2");
54 r_features->push_back("astc");
55
56 Vector<String> architectures = _get_preset_architectures(p_preset);
57 for (int i = 0; i < architectures.size(); ++i) {
58 r_features->push_back(architectures[i]);
59 }
60}
61
62Vector<EditorExportPlatformIOS::ExportArchitecture> EditorExportPlatformIOS::_get_supported_architectures() const {
63 Vector<ExportArchitecture> archs;
64 archs.push_back(ExportArchitecture("arm64", true));
65 return archs;
66}
67
68struct IconInfo {
69 const char *preset_key;
70 const char *idiom;
71 const char *export_name;
72 const char *actual_size_side;
73 const char *scale;
74 const char *unscaled_size;
75 const bool force_opaque;
76};
77
78static const IconInfo icon_infos[] = {
79 // Home screen on iPhone
80 { PNAME("icons/iphone_120x120"), "iphone", "Icon-120.png", "120", "2x", "60x60", false },
81 { PNAME("icons/iphone_120x120"), "iphone", "Icon-120.png", "120", "3x", "40x40", false },
82 { PNAME("icons/iphone_180x180"), "iphone", "Icon-180.png", "180", "3x", "60x60", false },
83
84 // Home screen on iPad
85 { PNAME("icons/ipad_76x76"), "ipad", "Icon-76.png", "76", "1x", "76x76", false },
86 { PNAME("icons/ipad_152x152"), "ipad", "Icon-152.png", "152", "2x", "76x76", false },
87 { PNAME("icons/ipad_167x167"), "ipad", "Icon-167.png", "167", "2x", "83.5x83.5", false },
88
89 // App Store
90 { PNAME("icons/app_store_1024x1024"), "ios-marketing", "Icon-1024.png", "1024", "1x", "1024x1024", true },
91
92 // Spotlight
93 { PNAME("icons/spotlight_40x40"), "ipad", "Icon-40.png", "40", "1x", "40x40", false },
94 { PNAME("icons/spotlight_80x80"), "iphone", "Icon-80.png", "80", "2x", "40x40", false },
95 { PNAME("icons/spotlight_80x80"), "ipad", "Icon-80.png", "80", "2x", "40x40", false },
96
97 // Settings
98 { PNAME("icons/settings_58x58"), "iphone", "Icon-58.png", "58", "2x", "29x29", false },
99 { PNAME("icons/settings_58x58"), "ipad", "Icon-58.png", "58", "2x", "29x29", false },
100 { PNAME("icons/settings_87x87"), "iphone", "Icon-87.png", "87", "3x", "29x29", false },
101
102 // Notification
103 { PNAME("icons/notification_40x40"), "iphone", "Icon-40.png", "40", "2x", "20x20", false },
104 { PNAME("icons/notification_40x40"), "ipad", "Icon-40.png", "40", "2x", "20x20", false },
105 { PNAME("icons/notification_60x60"), "iphone", "Icon-60.png", "60", "3x", "20x20", false }
106};
107
108struct LoadingScreenInfo {
109 const char *preset_key;
110 const char *export_name;
111 int width = 0;
112 int height = 0;
113 bool rotate = false;
114};
115
116static const LoadingScreenInfo loading_screen_infos[] = {
117 { PNAME("landscape_launch_screens/iphone_2436x1125"), "Default-Landscape-X.png", 2436, 1125, false },
118 { PNAME("landscape_launch_screens/iphone_2208x1242"), "Default-Landscape-736h@3x.png", 2208, 1242, false },
119 { PNAME("landscape_launch_screens/ipad_1024x768"), "Default-Landscape.png", 1024, 768, false },
120 { PNAME("landscape_launch_screens/ipad_2048x1536"), "Default-Landscape@2x.png", 2048, 1536, false },
121
122 { PNAME("portrait_launch_screens/iphone_640x960"), "Default-480h@2x.png", 640, 960, false },
123 { PNAME("portrait_launch_screens/iphone_640x1136"), "Default-568h@2x.png", 640, 1136, false },
124 { PNAME("portrait_launch_screens/iphone_750x1334"), "Default-667h@2x.png", 750, 1334, false },
125 { PNAME("portrait_launch_screens/iphone_1125x2436"), "Default-Portrait-X.png", 1125, 2436, false },
126 { PNAME("portrait_launch_screens/ipad_768x1024"), "Default-Portrait.png", 768, 1024, false },
127 { PNAME("portrait_launch_screens/ipad_1536x2048"), "Default-Portrait@2x.png", 1536, 2048, false },
128 { PNAME("portrait_launch_screens/iphone_1242x2208"), "Default-Portrait-736h@3x.png", 1242, 2208, false }
129};
130
131String EditorExportPlatformIOS::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const {
132 if (p_preset) {
133 if (p_name == "application/app_store_team_id") {
134 String team_id = p_preset->get("application/app_store_team_id");
135 if (team_id.is_empty()) {
136 return TTR("App Store Team ID not specified.") + "\n";
137 }
138 } else if (p_name == "application/bundle_identifier") {
139 String identifier = p_preset->get("application/bundle_identifier");
140 String pn_err;
141 if (!is_package_name_valid(identifier, &pn_err)) {
142 return TTR("Invalid Identifier:") + " " + pn_err;
143 }
144 }
145 }
146 return String();
147}
148
149bool EditorExportPlatformIOS::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {
150 if (p_preset) {
151 bool sb = p_preset->get("storyboard/use_launch_screen_storyboard");
152 if (!sb && p_option != "storyboard/use_launch_screen_storyboard" && p_option.begins_with("storyboard/")) {
153 return false;
154 }
155 }
156 return true;
157}
158
159void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) const {
160 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
161 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
162
163 Vector<ExportArchitecture> architectures = _get_supported_architectures();
164 for (int i = 0; i < architectures.size(); ++i) {
165 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("architectures"), architectures[i].name)), architectures[i].is_default));
166 }
167
168 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/app_store_team_id"), "", false, true));
169
170 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/provisioning_profile_uuid_debug", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
171 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/code_sign_identity_debug", PROPERTY_HINT_PLACEHOLDER_TEXT, "iPhone Developer"), ""));
172 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/export_method_debug", PROPERTY_HINT_ENUM, "App Store,Development,Ad-Hoc,Enterprise"), 1));
173 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/provisioning_profile_uuid_release", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
174 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/code_sign_identity_release", PROPERTY_HINT_PLACEHOLDER_TEXT, "iPhone Distribution"), ""));
175 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/export_method_release", PROPERTY_HINT_ENUM, "App Store,Development,Ad-Hoc,Enterprise"), 0));
176
177 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/targeted_device_family", PROPERTY_HINT_ENUM, "iPhone,iPad,iPhone & iPad"), 2));
178
179 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), "", false, true));
180 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/signature"), ""));
181 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version"), "1.0"));
182 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), ""));
183
184 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/icon_interpolation", PROPERTY_HINT_ENUM, "Nearest neighbor,Bilinear,Cubic,Trilinear,Lanczos"), 4));
185 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/launch_screens_interpolation", PROPERTY_HINT_ENUM, "Nearest neighbor,Bilinear,Cubic,Trilinear,Lanczos"), 4));
186
187 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/export_project_only"), false));
188
189 Vector<PluginConfigIOS> found_plugins = get_plugins();
190 for (int i = 0; i < found_plugins.size(); i++) {
191 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), found_plugins[i].name)), false));
192 }
193
194 HashSet<String> plist_keys;
195
196 for (int i = 0; i < found_plugins.size(); i++) {
197 // Editable plugin plist values
198 PluginConfigIOS plugin = found_plugins[i];
199
200 for (const KeyValue<String, PluginConfigIOS::PlistItem> &E : plugin.plist) {
201 switch (E.value.type) {
202 case PluginConfigIOS::PlistItemType::STRING_INPUT: {
203 String preset_name = "plugins_plist/" + E.key;
204 if (!plist_keys.has(preset_name)) {
205 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, preset_name), E.value.value));
206 plist_keys.insert(preset_name);
207 }
208 } break;
209 default:
210 continue;
211 }
212 }
213 }
214
215 plugins_changed.clear();
216 plugins = found_plugins;
217
218 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "capabilities/access_wifi"), false));
219 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "capabilities/push_notifications"), false));
220
221 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data/accessible_from_files_app"), false));
222 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data/accessible_from_itunes_sharing"), false));
223
224 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"), ""));
225 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/camera_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
226 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"), ""));
227 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/microphone_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
228 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photolibrary_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need access to the photo library"), ""));
229 r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photolibrary_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
230
231 HashSet<String> used_names;
232 for (uint64_t i = 0; i < sizeof(icon_infos) / sizeof(icon_infos[0]); ++i) {
233 if (!used_names.has(icon_infos[i].preset_key)) {
234 used_names.insert(icon_infos[i].preset_key);
235 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, icon_infos[i].preset_key, PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), ""));
236 }
237 }
238 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "storyboard/use_launch_screen_storyboard"), false, true));
239 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "storyboard/image_scale_mode", PROPERTY_HINT_ENUM, "Same as Logo,Center,Scale to Fit,Scale to Fill,Scale"), 0));
240 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@2x", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), ""));
241 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@3x", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), ""));
242 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "storyboard/use_custom_bg_color"), false));
243 r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "storyboard/custom_bg_color"), Color()));
244
245 for (uint64_t i = 0; i < sizeof(loading_screen_infos) / sizeof(loading_screen_infos[0]); ++i) {
246 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, loading_screen_infos[i].preset_key, PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), ""));
247 }
248}
249
250void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &pfile, const IOSConfigData &p_config, bool p_debug) {
251 static const String export_method_string[] = {
252 "app-store",
253 "development",
254 "ad-hoc",
255 "enterprise"
256 };
257 static const String storyboard_image_scale_mode[] = {
258 "center",
259 "scaleAspectFit",
260 "scaleAspectFill",
261 "scaleToFill"
262 };
263 String dbg_sign_id = p_preset->get("application/code_sign_identity_debug").operator String().is_empty() ? "iPhone Developer" : p_preset->get("application/code_sign_identity_debug");
264 String rel_sign_id = p_preset->get("application/code_sign_identity_release").operator String().is_empty() ? "iPhone Distribution" : p_preset->get("application/code_sign_identity_release");
265 bool dbg_manual = !p_preset->get_or_env("application/provisioning_profile_uuid_debug", ENV_IOS_PROFILE_UUID_DEBUG).operator String().is_empty() || (dbg_sign_id != "iPhone Developer");
266 bool rel_manual = !p_preset->get_or_env("application/provisioning_profile_uuid_release", ENV_IOS_PROFILE_UUID_RELEASE).operator String().is_empty() || (rel_sign_id != "iPhone Distribution");
267 String str;
268 String strnew;
269 str.parse_utf8((const char *)pfile.ptr(), pfile.size());
270 Vector<String> lines = str.split("\n");
271 for (int i = 0; i < lines.size(); i++) {
272 if (lines[i].find("$binary") != -1) {
273 strnew += lines[i].replace("$binary", p_config.binary_name) + "\n";
274 } else if (lines[i].find("$modules_buildfile") != -1) {
275 strnew += lines[i].replace("$modules_buildfile", p_config.modules_buildfile) + "\n";
276 } else if (lines[i].find("$modules_fileref") != -1) {
277 strnew += lines[i].replace("$modules_fileref", p_config.modules_fileref) + "\n";
278 } else if (lines[i].find("$modules_buildphase") != -1) {
279 strnew += lines[i].replace("$modules_buildphase", p_config.modules_buildphase) + "\n";
280 } else if (lines[i].find("$modules_buildgrp") != -1) {
281 strnew += lines[i].replace("$modules_buildgrp", p_config.modules_buildgrp) + "\n";
282 } else if (lines[i].find("$name") != -1) {
283 strnew += lines[i].replace("$name", p_config.pkg_name) + "\n";
284 } else if (lines[i].find("$bundle_identifier") != -1) {
285 strnew += lines[i].replace("$bundle_identifier", p_preset->get("application/bundle_identifier")) + "\n";
286 } else if (lines[i].find("$short_version") != -1) {
287 strnew += lines[i].replace("$short_version", p_preset->get("application/short_version")) + "\n";
288 } else if (lines[i].find("$version") != -1) {
289 strnew += lines[i].replace("$version", p_preset->get_version("application/version")) + "\n";
290 } else if (lines[i].find("$signature") != -1) {
291 strnew += lines[i].replace("$signature", p_preset->get("application/signature")) + "\n";
292 } else if (lines[i].find("$team_id") != -1) {
293 strnew += lines[i].replace("$team_id", p_preset->get("application/app_store_team_id")) + "\n";
294 } else if (lines[i].find("$default_build_config") != -1) {
295 strnew += lines[i].replace("$default_build_config", p_debug ? "Debug" : "Release") + "\n";
296 } else if (lines[i].find("$export_method") != -1) {
297 int export_method = p_preset->get(p_debug ? "application/export_method_debug" : "application/export_method_release");
298 strnew += lines[i].replace("$export_method", export_method_string[export_method]) + "\n";
299 } else if (lines[i].find("$provisioning_profile_uuid_release") != -1) {
300 strnew += lines[i].replace("$provisioning_profile_uuid_release", p_preset->get_or_env("application/provisioning_profile_uuid_release", ENV_IOS_PROFILE_UUID_RELEASE)) + "\n";
301 } else if (lines[i].find("$provisioning_profile_uuid_debug") != -1) {
302 strnew += lines[i].replace("$provisioning_profile_uuid_debug", p_preset->get_or_env("application/provisioning_profile_uuid_debug", ENV_IOS_PROFILE_UUID_DEBUG)) + "\n";
303 } else if (lines[i].find("$code_sign_style_debug") != -1) {
304 if (dbg_manual) {
305 strnew += lines[i].replace("$code_sign_style_debug", "Manual") + "\n";
306 } else {
307 strnew += lines[i].replace("$code_sign_style_debug", "Automatic") + "\n";
308 }
309 } else if (lines[i].find("$code_sign_style_release") != -1) {
310 if (rel_manual) {
311 strnew += lines[i].replace("$code_sign_style_release", "Manual") + "\n";
312 } else {
313 strnew += lines[i].replace("$code_sign_style_release", "Automatic") + "\n";
314 }
315 } else if (lines[i].find("$provisioning_profile_uuid") != -1) {
316 String uuid = p_debug ? p_preset->get_or_env("application/provisioning_profile_uuid_debug", ENV_IOS_PROFILE_UUID_DEBUG) : p_preset->get_or_env("application/provisioning_profile_uuid_release", ENV_IOS_PROFILE_UUID_RELEASE);
317 strnew += lines[i].replace("$provisioning_profile_uuid", uuid) + "\n";
318 } else if (lines[i].find("$code_sign_identity_debug") != -1) {
319 strnew += lines[i].replace("$code_sign_identity_debug", dbg_sign_id) + "\n";
320 } else if (lines[i].find("$code_sign_identity_release") != -1) {
321 strnew += lines[i].replace("$code_sign_identity_release", rel_sign_id) + "\n";
322 } else if (lines[i].find("$additional_plist_content") != -1) {
323 strnew += lines[i].replace("$additional_plist_content", p_config.plist_content) + "\n";
324 } else if (lines[i].find("$godot_archs") != -1) {
325 strnew += lines[i].replace("$godot_archs", p_config.architectures) + "\n";
326 } else if (lines[i].find("$linker_flags") != -1) {
327 strnew += lines[i].replace("$linker_flags", p_config.linker_flags) + "\n";
328 } else if (lines[i].find("$targeted_device_family") != -1) {
329 String xcode_value;
330 switch ((int)p_preset->get("application/targeted_device_family")) {
331 case 0: // iPhone
332 xcode_value = "1";
333 break;
334 case 1: // iPad
335 xcode_value = "2";
336 break;
337 case 2: // iPhone & iPad
338 xcode_value = "1,2";
339 break;
340 }
341 strnew += lines[i].replace("$targeted_device_family", xcode_value) + "\n";
342 } else if (lines[i].find("$cpp_code") != -1) {
343 strnew += lines[i].replace("$cpp_code", p_config.cpp_code) + "\n";
344 } else if (lines[i].find("$docs_in_place") != -1) {
345 strnew += lines[i].replace("$docs_in_place", ((bool)p_preset->get("user_data/accessible_from_files_app")) ? "<true/>" : "<false/>") + "\n";
346 } else if (lines[i].find("$docs_sharing") != -1) {
347 strnew += lines[i].replace("$docs_sharing", ((bool)p_preset->get("user_data/accessible_from_itunes_sharing")) ? "<true/>" : "<false/>") + "\n";
348 } else if (lines[i].find("$entitlements_push_notifications") != -1) {
349 bool is_on = p_preset->get("capabilities/push_notifications");
350 strnew += lines[i].replace("$entitlements_push_notifications", is_on ? "<key>aps-environment</key><string>development</string>" : "") + "\n";
351 } else if (lines[i].find("$required_device_capabilities") != -1) {
352 String capabilities;
353
354 // I've removed armv7 as we can run on 64bit only devices
355 // Note that capabilities listed here are requirements for the app to be installed.
356 // They don't enable anything.
357 Vector<String> capabilities_list = p_config.capabilities;
358
359 if ((bool)p_preset->get("capabilities/access_wifi") && !capabilities_list.has("wifi")) {
360 capabilities_list.push_back("wifi");
361 }
362
363 for (int idx = 0; idx < capabilities_list.size(); idx++) {
364 capabilities += "<string>" + capabilities_list[idx] + "</string>\n";
365 }
366
367 strnew += lines[i].replace("$required_device_capabilities", capabilities);
368 } else if (lines[i].find("$interface_orientations") != -1) {
369 String orientations;
370 const DisplayServer::ScreenOrientation screen_orientation =
371 DisplayServer::ScreenOrientation(int(GLOBAL_GET("display/window/handheld/orientation")));
372
373 switch (screen_orientation) {
374 case DisplayServer::SCREEN_LANDSCAPE:
375 orientations += "<string>UIInterfaceOrientationLandscapeLeft</string>\n";
376 break;
377 case DisplayServer::SCREEN_PORTRAIT:
378 orientations += "<string>UIInterfaceOrientationPortrait</string>\n";
379 break;
380 case DisplayServer::SCREEN_REVERSE_LANDSCAPE:
381 orientations += "<string>UIInterfaceOrientationLandscapeRight</string>\n";
382 break;
383 case DisplayServer::SCREEN_REVERSE_PORTRAIT:
384 orientations += "<string>UIInterfaceOrientationPortraitUpsideDown</string>\n";
385 break;
386 case DisplayServer::SCREEN_SENSOR_LANDSCAPE:
387 // Allow both landscape orientations depending on sensor direction.
388 orientations += "<string>UIInterfaceOrientationLandscapeLeft</string>\n";
389 orientations += "<string>UIInterfaceOrientationLandscapeRight</string>\n";
390 break;
391 case DisplayServer::SCREEN_SENSOR_PORTRAIT:
392 // Allow both portrait orientations depending on sensor direction.
393 orientations += "<string>UIInterfaceOrientationPortrait</string>\n";
394 orientations += "<string>UIInterfaceOrientationPortraitUpsideDown</string>\n";
395 break;
396 case DisplayServer::SCREEN_SENSOR:
397 // Allow all screen orientations depending on sensor direction.
398 orientations += "<string>UIInterfaceOrientationLandscapeLeft</string>\n";
399 orientations += "<string>UIInterfaceOrientationLandscapeRight</string>\n";
400 orientations += "<string>UIInterfaceOrientationPortrait</string>\n";
401 orientations += "<string>UIInterfaceOrientationPortraitUpsideDown</string>\n";
402 break;
403 }
404
405 strnew += lines[i].replace("$interface_orientations", orientations);
406 } else if (lines[i].find("$camera_usage_description") != -1) {
407 String description = p_preset->get("privacy/camera_usage_description");
408 strnew += lines[i].replace("$camera_usage_description", description) + "\n";
409 } else if (lines[i].find("$microphone_usage_description") != -1) {
410 String description = p_preset->get("privacy/microphone_usage_description");
411 strnew += lines[i].replace("$microphone_usage_description", description) + "\n";
412 } else if (lines[i].find("$photolibrary_usage_description") != -1) {
413 String description = p_preset->get("privacy/photolibrary_usage_description");
414 strnew += lines[i].replace("$photolibrary_usage_description", description) + "\n";
415 } else if (lines[i].find("$plist_launch_screen_name") != -1) {
416 bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard");
417 String value = is_on ? "<key>UILaunchStoryboardName</key>\n<string>Launch Screen</string>" : "";
418 strnew += lines[i].replace("$plist_launch_screen_name", value) + "\n";
419 } else if (lines[i].find("$pbx_launch_screen_file_reference") != -1) {
420 bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard");
421 String value = is_on ? "90DD2D9D24B36E8000717FE1 = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = \"Launch Screen.storyboard\"; sourceTree = \"<group>\"; };" : "";
422 strnew += lines[i].replace("$pbx_launch_screen_file_reference", value) + "\n";
423 } else if (lines[i].find("$pbx_launch_screen_copy_files") != -1) {
424 bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard");
425 String value = is_on ? "90DD2D9D24B36E8000717FE1 /* Launch Screen.storyboard */," : "";
426 strnew += lines[i].replace("$pbx_launch_screen_copy_files", value) + "\n";
427 } else if (lines[i].find("$pbx_launch_screen_build_phase") != -1) {
428 bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard");
429 String value = is_on ? "90DD2D9E24B36E8000717FE1 /* Launch Screen.storyboard in Resources */," : "";
430 strnew += lines[i].replace("$pbx_launch_screen_build_phase", value) + "\n";
431 } else if (lines[i].find("$pbx_launch_screen_build_reference") != -1) {
432 bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard");
433 String value = is_on ? "90DD2D9E24B36E8000717FE1 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 90DD2D9D24B36E8000717FE1 /* Launch Screen.storyboard */; };" : "";
434 strnew += lines[i].replace("$pbx_launch_screen_build_reference", value) + "\n";
435 } else if (lines[i].find("$pbx_launch_image_usage_setting") != -1) {
436 bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard");
437 String value = is_on ? "" : "ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;";
438 strnew += lines[i].replace("$pbx_launch_image_usage_setting", value) + "\n";
439 } else if (lines[i].find("$launch_screen_image_mode") != -1) {
440 int image_scale_mode = p_preset->get("storyboard/image_scale_mode");
441 String value;
442
443 switch (image_scale_mode) {
444 case 0: {
445 String logo_path = GLOBAL_GET("application/boot_splash/image");
446 bool is_on = GLOBAL_GET("application/boot_splash/fullsize");
447 // If custom logo is not specified, Godot does not scale default one, so we should do the same.
448 value = (is_on && logo_path.length() > 0) ? "scaleAspectFit" : "center";
449 } break;
450 default: {
451 value = storyboard_image_scale_mode[image_scale_mode - 1];
452 }
453 }
454
455 strnew += lines[i].replace("$launch_screen_image_mode", value) + "\n";
456 } else if (lines[i].find("$launch_screen_background_color") != -1) {
457 bool use_custom = p_preset->get("storyboard/use_custom_bg_color");
458 Color color = use_custom ? p_preset->get("storyboard/custom_bg_color") : GLOBAL_GET("application/boot_splash/bg_color");
459 const String value_format = "red=\"$red\" green=\"$green\" blue=\"$blue\" alpha=\"$alpha\"";
460
461 Dictionary value_dictionary;
462 value_dictionary["red"] = color.r;
463 value_dictionary["green"] = color.g;
464 value_dictionary["blue"] = color.b;
465 value_dictionary["alpha"] = color.a;
466 String value = value_format.format(value_dictionary, "$_");
467
468 strnew += lines[i].replace("$launch_screen_background_color", value) + "\n";
469 } else if (lines[i].find("$pbx_locale_file_reference") != -1) {
470 String locale_files;
471 Vector<String> translations = GLOBAL_GET("internationalization/locale/translations");
472 if (translations.size() > 0) {
473 HashSet<String> languages;
474 for (const String &E : translations) {
475 Ref<Translation> tr = ResourceLoader::load(E);
476 if (tr.is_valid() && tr->get_locale() != "en") {
477 languages.insert(tr->get_locale());
478 }
479 }
480
481 int index = 0;
482 for (const String &lang : languages) {
483 locale_files += "D0BCFE4518AEBDA2004A" + itos(index).pad_zeros(4) + " /* " + lang + " */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = " + lang + "; path = " + lang + ".lproj/InfoPlist.strings; sourceTree = \"<group>\"; };\n";
484 index++;
485 }
486 }
487 strnew += lines[i].replace("$pbx_locale_file_reference", locale_files);
488 } else if (lines[i].find("$pbx_locale_build_reference") != -1) {
489 String locale_files;
490 Vector<String> translations = GLOBAL_GET("internationalization/locale/translations");
491 if (translations.size() > 0) {
492 HashSet<String> languages;
493 for (const String &E : translations) {
494 Ref<Translation> tr = ResourceLoader::load(E);
495 if (tr.is_valid() && tr->get_locale() != "en") {
496 languages.insert(tr->get_locale());
497 }
498 }
499
500 int index = 0;
501 for (const String &lang : languages) {
502 locale_files += "D0BCFE4518AEBDA2004A" + itos(index).pad_zeros(4) + " /* " + lang + " */,\n";
503 index++;
504 }
505 }
506 strnew += lines[i].replace("$pbx_locale_build_reference", locale_files);
507 } else if (lines[i].find("$swift_runtime_migration") != -1) {
508 String value = !p_config.use_swift_runtime ? "" : "LastSwiftMigration = 1250;";
509 strnew += lines[i].replace("$swift_runtime_migration", value) + "\n";
510 } else if (lines[i].find("$swift_runtime_build_settings") != -1) {
511 String value = !p_config.use_swift_runtime ? "" : R"(
512 CLANG_ENABLE_MODULES = YES;
513 SWIFT_OBJC_BRIDGING_HEADER = "$binary/dummy.h";
514 SWIFT_VERSION = 5.0;
515 )";
516 value = value.replace("$binary", p_config.binary_name);
517 strnew += lines[i].replace("$swift_runtime_build_settings", value) + "\n";
518 } else if (lines[i].find("$swift_runtime_fileref") != -1) {
519 String value = !p_config.use_swift_runtime ? "" : R"(
520 90B4C2AA2680BC560039117A /* dummy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "dummy.h"; sourceTree = "<group>"; };
521 90B4C2B52680C7E90039117A /* dummy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "dummy.swift"; sourceTree = "<group>"; };
522 )";
523 strnew += lines[i].replace("$swift_runtime_fileref", value) + "\n";
524 } else if (lines[i].find("$swift_runtime_binary_files") != -1) {
525 String value = !p_config.use_swift_runtime ? "" : R"(
526 90B4C2AA2680BC560039117A /* dummy.h */,
527 90B4C2B52680C7E90039117A /* dummy.swift */,
528 )";
529 strnew += lines[i].replace("$swift_runtime_binary_files", value) + "\n";
530 } else if (lines[i].find("$swift_runtime_buildfile") != -1) {
531 String value = !p_config.use_swift_runtime ? "" : "90B4C2B62680C7E90039117A /* dummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B4C2B52680C7E90039117A /* dummy.swift */; };";
532 strnew += lines[i].replace("$swift_runtime_buildfile", value) + "\n";
533 } else if (lines[i].find("$swift_runtime_build_phase") != -1) {
534 String value = !p_config.use_swift_runtime ? "" : "90B4C2B62680C7E90039117A /* dummy.swift */,";
535 strnew += lines[i].replace("$swift_runtime_build_phase", value) + "\n";
536 } else {
537 strnew += lines[i] + "\n";
538 }
539 }
540
541 // !BAS! I'm assuming the 9 in the original code was a typo. I've added -1 or else it seems to also be adding our terminating zero...
542 // should apply the same fix in our macOS export.
543 CharString cs = strnew.utf8();
544 pfile.resize(cs.size() - 1);
545 for (int i = 0; i < cs.size() - 1; i++) {
546 pfile.write[i] = cs[i];
547 }
548}
549
550String EditorExportPlatformIOS::_get_additional_plist_content() {
551 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
552 String result;
553 for (int i = 0; i < export_plugins.size(); ++i) {
554 result += export_plugins[i]->get_ios_plist_content();
555 }
556 return result;
557}
558
559String EditorExportPlatformIOS::_get_linker_flags() {
560 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
561 String result;
562 for (int i = 0; i < export_plugins.size(); ++i) {
563 String flags = export_plugins[i]->get_ios_linker_flags();
564 if (flags.length() == 0) {
565 continue;
566 }
567 if (result.length() > 0) {
568 result += ' ';
569 }
570 result += flags;
571 }
572 // the flags will be enclosed in quotes, so need to escape them
573 return result.replace("\"", "\\\"");
574}
575
576String EditorExportPlatformIOS::_get_cpp_code() {
577 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
578 String result;
579 for (int i = 0; i < export_plugins.size(); ++i) {
580 result += export_plugins[i]->get_ios_cpp_code();
581 }
582 return result;
583}
584
585void EditorExportPlatformIOS::_blend_and_rotate(Ref<Image> &p_dst, Ref<Image> &p_src, bool p_rot) {
586 ERR_FAIL_COND(p_dst.is_null());
587 ERR_FAIL_COND(p_src.is_null());
588
589 int sw = p_rot ? p_src->get_height() : p_src->get_width();
590 int sh = p_rot ? p_src->get_width() : p_src->get_height();
591
592 int x_pos = (p_dst->get_width() - sw) / 2;
593 int y_pos = (p_dst->get_height() - sh) / 2;
594
595 int xs = (x_pos >= 0) ? 0 : -x_pos;
596 int ys = (y_pos >= 0) ? 0 : -y_pos;
597
598 if (sw + x_pos > p_dst->get_width()) {
599 sw = p_dst->get_width() - x_pos;
600 }
601 if (sh + y_pos > p_dst->get_height()) {
602 sh = p_dst->get_height() - y_pos;
603 }
604
605 for (int y = ys; y < sh; y++) {
606 for (int x = xs; x < sw; x++) {
607 Color sc = p_rot ? p_src->get_pixel(p_src->get_width() - y - 1, x) : p_src->get_pixel(x, y);
608 Color dc = p_dst->get_pixel(x_pos + x, y_pos + y);
609 dc.r = (double)(sc.a * sc.r + dc.a * (1.0 - sc.a) * dc.r);
610 dc.g = (double)(sc.a * sc.g + dc.a * (1.0 - sc.a) * dc.g);
611 dc.b = (double)(sc.a * sc.b + dc.a * (1.0 - sc.a) * dc.b);
612 dc.a = (double)(sc.a + dc.a * (1.0 - sc.a));
613 p_dst->set_pixel(x_pos + x, y_pos + y, dc);
614 }
615 }
616}
617
618Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_preset, const String &p_iconset_dir) {
619 String json_description = "{\"images\":[";
620 String sizes;
621
622 Ref<DirAccess> da = DirAccess::open(p_iconset_dir);
623 ERR_FAIL_COND_V_MSG(da.is_null(), ERR_CANT_OPEN, "Cannot open directory '" + p_iconset_dir + "'.");
624
625 Color boot_bg_color = GLOBAL_GET("application/boot_splash/bg_color");
626
627 for (uint64_t i = 0; i < (sizeof(icon_infos) / sizeof(icon_infos[0])); ++i) {
628 IconInfo info = icon_infos[i];
629 int side_size = String(info.actual_size_side).to_int();
630 String icon_path = p_preset->get(info.preset_key);
631 if (icon_path.length() == 0) {
632 // Resize main app icon
633 icon_path = GLOBAL_GET("application/config/icon");
634 Ref<Image> img = memnew(Image);
635 Error err = ImageLoader::load_image(icon_path, img);
636 if (err != OK) {
637 add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path));
638 return ERR_UNCONFIGURED;
639 } else if (info.force_opaque && img->detect_alpha() != Image::ALPHA_NONE) {
640 add_message(EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s) must be opaque.", info.preset_key));
641 img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));
642 Ref<Image> new_img = Image::create_empty(side_size, side_size, false, Image::FORMAT_RGBA8);
643 new_img->fill(boot_bg_color);
644 _blend_and_rotate(new_img, img, false);
645 err = new_img->save_png(p_iconset_dir + info.export_name);
646 } else {
647 img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));
648 err = img->save_png(p_iconset_dir + info.export_name);
649 }
650 if (err) {
651 add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path));
652 return err;
653 }
654 } else {
655 // Load custom icon and resize if required
656 Ref<Image> img = memnew(Image);
657 Error err = ImageLoader::load_image(icon_path, img);
658 if (err != OK) {
659 add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path));
660 return ERR_UNCONFIGURED;
661 } else if (info.force_opaque && img->detect_alpha() != Image::ALPHA_NONE) {
662 add_message(EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s) must be opaque.", info.preset_key));
663 img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));
664 Ref<Image> new_img = Image::create_empty(side_size, side_size, false, Image::FORMAT_RGBA8);
665 new_img->fill(boot_bg_color);
666 _blend_and_rotate(new_img, img, false);
667 err = new_img->save_png(p_iconset_dir + info.export_name);
668 } else if (img->get_width() != side_size || img->get_height() != side_size) {
669 add_message(EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s): '%s' has incorrect size %s and was automatically resized to %s.", info.preset_key, icon_path, img->get_size(), Vector2i(side_size, side_size)));
670 img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));
671 err = img->save_png(p_iconset_dir + info.export_name);
672 } else {
673 err = da->copy(icon_path, p_iconset_dir + info.export_name);
674 }
675
676 if (err) {
677 add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path));
678 return err;
679 }
680 }
681 sizes += String(info.actual_size_side) + "\n";
682 if (i > 0) {
683 json_description += ",";
684 }
685 json_description += String("{");
686 json_description += String("\"idiom\":") + "\"" + info.idiom + "\",";
687 json_description += String("\"size\":") + "\"" + info.unscaled_size + "\",";
688 json_description += String("\"scale\":") + "\"" + info.scale + "\",";
689 json_description += String("\"filename\":") + "\"" + info.export_name + "\"";
690 json_description += String("}");
691 }
692 json_description += "]}";
693
694 Ref<FileAccess> json_file = FileAccess::open(p_iconset_dir + "Contents.json", FileAccess::WRITE);
695 ERR_FAIL_COND_V(json_file.is_null(), ERR_CANT_CREATE);
696 CharString json_utf8 = json_description.utf8();
697 json_file->store_buffer((const uint8_t *)json_utf8.get_data(), json_utf8.length());
698
699 Ref<FileAccess> sizes_file = FileAccess::open(p_iconset_dir + "sizes", FileAccess::WRITE);
700 ERR_FAIL_COND_V(sizes_file.is_null(), ERR_CANT_CREATE);
701 CharString sizes_utf8 = sizes.utf8();
702 sizes_file->store_buffer((const uint8_t *)sizes_utf8.get_data(), sizes_utf8.length());
703
704 return OK;
705}
706
707Error EditorExportPlatformIOS::_export_loading_screen_file(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir) {
708 const String custom_launch_image_2x = p_preset->get("storyboard/custom_image@2x");
709 const String custom_launch_image_3x = p_preset->get("storyboard/custom_image@3x");
710
711 if (custom_launch_image_2x.length() > 0 && custom_launch_image_3x.length() > 0) {
712 Ref<Image> image;
713 String image_path = p_dest_dir.path_join("splash@2x.png");
714 image.instantiate();
715 Error err = ImageLoader::load_image(custom_launch_image_2x, image);
716
717 if (err) {
718 image.unref();
719 return err;
720 }
721
722 if (image->save_png(image_path) != OK) {
723 return ERR_FILE_CANT_WRITE;
724 }
725
726 image.unref();
727 image_path = p_dest_dir.path_join("splash@3x.png");
728 image.instantiate();
729 err = ImageLoader::load_image(custom_launch_image_3x, image);
730
731 if (err) {
732 image.unref();
733 return err;
734 }
735
736 if (image->save_png(image_path) != OK) {
737 return ERR_FILE_CANT_WRITE;
738 }
739 } else {
740 Ref<Image> splash;
741
742 const String splash_path = GLOBAL_GET("application/boot_splash/image");
743
744 if (!splash_path.is_empty()) {
745 splash.instantiate();
746 const Error err = ImageLoader::load_image(splash_path, splash);
747 if (err) {
748 splash.unref();
749 }
750 }
751
752 if (splash.is_null()) {
753 splash = Ref<Image>(memnew(Image(boot_splash_png)));
754 }
755
756 // Using same image for both @2x and @3x
757 // because Godot's own boot logo uses single image for all resolutions.
758 // Also not using @1x image, because devices using this image variant
759 // are not supported by iOS 9, which is minimal target.
760 const String splash_png_path_2x = p_dest_dir.path_join("splash@2x.png");
761 const String splash_png_path_3x = p_dest_dir.path_join("splash@3x.png");
762
763 if (splash->save_png(splash_png_path_2x) != OK) {
764 return ERR_FILE_CANT_WRITE;
765 }
766
767 if (splash->save_png(splash_png_path_3x) != OK) {
768 return ERR_FILE_CANT_WRITE;
769 }
770 }
771
772 return OK;
773}
774
775Error EditorExportPlatformIOS::_export_loading_screen_images(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir) {
776 Ref<DirAccess> da = DirAccess::open(p_dest_dir);
777 ERR_FAIL_COND_V_MSG(da.is_null(), ERR_CANT_OPEN, "Cannot open directory '" + p_dest_dir + "'.");
778
779 for (uint64_t i = 0; i < sizeof(loading_screen_infos) / sizeof(loading_screen_infos[0]); ++i) {
780 LoadingScreenInfo info = loading_screen_infos[i];
781 String loading_screen_file = p_preset->get(info.preset_key);
782
783 Color boot_bg_color = GLOBAL_GET("application/boot_splash/bg_color");
784 String boot_logo_path = GLOBAL_GET("application/boot_splash/image");
785 bool boot_logo_scale = GLOBAL_GET("application/boot_splash/fullsize");
786
787 if (loading_screen_file.size() > 0) {
788 // Load custom loading screens, and resize if required.
789 Ref<Image> img = memnew(Image);
790 Error err = ImageLoader::load_image(loading_screen_file, img);
791 if (err != OK) {
792 ERR_PRINT("Invalid loading screen (" + String(info.preset_key) + "): '" + loading_screen_file + "'.");
793 return ERR_UNCONFIGURED;
794 }
795 if (img->get_width() != info.width || img->get_height() != info.height) {
796 WARN_PRINT("Loading screen (" + String(info.preset_key) + "): '" + loading_screen_file + "' has incorrect size (" + String::num_int64(img->get_width()) + "x" + String::num_int64(img->get_height()) + ") and was automatically resized to " + String::num_int64(info.width) + "x" + String::num_int64(info.height) + ".");
797 float aspect_ratio = (float)img->get_width() / (float)img->get_height();
798 if (boot_logo_scale) {
799 if (info.height * aspect_ratio <= info.width) {
800 img->resize(info.height * aspect_ratio, info.height, (Image::Interpolation)(p_preset->get("application/launch_screens_interpolation").operator int()));
801 } else {
802 img->resize(info.width, info.width / aspect_ratio, (Image::Interpolation)(p_preset->get("application/launch_screens_interpolation").operator int()));
803 }
804 }
805 Ref<Image> new_img = Image::create_empty(info.width, info.height, false, Image::FORMAT_RGBA8);
806 new_img->fill(boot_bg_color);
807 _blend_and_rotate(new_img, img, false);
808 err = new_img->save_png(p_dest_dir + info.export_name);
809 } else {
810 err = da->copy(loading_screen_file, p_dest_dir + info.export_name);
811 }
812 if (err) {
813 String err_str = String("Failed to export loading screen (") + info.preset_key + ") from path '" + loading_screen_file + "'.";
814 ERR_PRINT(err_str.utf8().get_data());
815 return err;
816 }
817 } else {
818 // Generate loading screen from the splash screen
819 Ref<Image> img = Image::create_empty(info.width, info.height, false, Image::FORMAT_RGBA8);
820 img->fill(boot_bg_color);
821
822 Ref<Image> img_bs;
823
824 if (boot_logo_path.length() > 0) {
825 img_bs = Ref<Image>(memnew(Image));
826 ImageLoader::load_image(boot_logo_path, img_bs);
827 }
828 if (!img_bs.is_valid()) {
829 img_bs = Ref<Image>(memnew(Image(boot_splash_png)));
830 }
831 if (img_bs.is_valid()) {
832 float aspect_ratio = (float)img_bs->get_width() / (float)img_bs->get_height();
833 if (info.rotate) {
834 if (boot_logo_scale) {
835 if (info.width * aspect_ratio <= info.height) {
836 img_bs->resize(info.width * aspect_ratio, info.width, (Image::Interpolation)(p_preset->get("application/launch_screens_interpolation").operator int()));
837 } else {
838 img_bs->resize(info.height, info.height / aspect_ratio, (Image::Interpolation)(p_preset->get("application/launch_screens_interpolation").operator int()));
839 }
840 }
841 } else {
842 if (boot_logo_scale) {
843 if (info.height * aspect_ratio <= info.width) {
844 img_bs->resize(info.height * aspect_ratio, info.height, (Image::Interpolation)(p_preset->get("application/launch_screens_interpolation").operator int()));
845 } else {
846 img_bs->resize(info.width, info.width / aspect_ratio, (Image::Interpolation)(p_preset->get("application/launch_screens_interpolation").operator int()));
847 }
848 }
849 }
850 _blend_and_rotate(img, img_bs, info.rotate);
851 }
852 Error err = img->save_png(p_dest_dir + info.export_name);
853 if (err) {
854 String err_str = String("Failed to export loading screen (") + info.preset_key + ") from splash screen.";
855 WARN_PRINT(err_str.utf8().get_data());
856 }
857 }
858 }
859
860 return OK;
861}
862
863Error EditorExportPlatformIOS::_walk_dir_recursive(Ref<DirAccess> &p_da, FileHandler p_handler, void *p_userdata) {
864 Vector<String> dirs;
865 String current_dir = p_da->get_current_dir();
866 p_da->list_dir_begin();
867 String path = p_da->get_next();
868 while (!path.is_empty()) {
869 if (p_da->current_is_dir()) {
870 if (path != "." && path != "..") {
871 dirs.push_back(path);
872 }
873 } else {
874 Error err = p_handler(current_dir.path_join(path), p_userdata);
875 if (err) {
876 p_da->list_dir_end();
877 return err;
878 }
879 }
880 path = p_da->get_next();
881 }
882 p_da->list_dir_end();
883
884 for (int i = 0; i < dirs.size(); ++i) {
885 String dir = dirs[i];
886 p_da->change_dir(dir);
887 Error err = _walk_dir_recursive(p_da, p_handler, p_userdata);
888 p_da->change_dir("..");
889 if (err) {
890 return err;
891 }
892 }
893
894 return OK;
895}
896
897struct CodesignData {
898 const Ref<EditorExportPreset> &preset;
899 bool debug = false;
900
901 CodesignData(const Ref<EditorExportPreset> &p_preset, bool p_debug) :
902 preset(p_preset),
903 debug(p_debug) {
904 }
905};
906
907Error EditorExportPlatformIOS::_codesign(String p_file, void *p_userdata) {
908 if (p_file.ends_with(".dylib")) {
909 CodesignData *data = static_cast<CodesignData *>(p_userdata);
910 print_line(String("Signing ") + p_file);
911
912 String sign_id;
913 if (data->debug) {
914 sign_id = data->preset->get("application/code_sign_identity_debug").operator String().is_empty() ? "iPhone Developer" : data->preset->get("application/code_sign_identity_debug");
915 } else {
916 sign_id = data->preset->get("application/code_sign_identity_release").operator String().is_empty() ? "iPhone Distribution" : data->preset->get("application/code_sign_identity_release");
917 }
918
919 List<String> codesign_args;
920 codesign_args.push_back("-f");
921 codesign_args.push_back("-s");
922 codesign_args.push_back(sign_id);
923 codesign_args.push_back(p_file);
924 String str;
925 Error err = OS::get_singleton()->execute("codesign", codesign_args, &str, nullptr, true);
926 print_verbose("codesign (" + p_file + "):\n" + str);
927
928 return err;
929 }
930 return OK;
931}
932
933struct PbxId {
934private:
935 static char _hex_char(uint8_t four_bits) {
936 if (four_bits < 10) {
937 return ('0' + four_bits);
938 }
939 return 'A' + (four_bits - 10);
940 }
941
942 static String _hex_pad(uint32_t num) {
943 Vector<char> ret;
944 ret.resize(sizeof(num) * 2);
945 for (uint64_t i = 0; i < sizeof(num) * 2; ++i) {
946 uint8_t four_bits = (num >> (sizeof(num) * 8 - (i + 1) * 4)) & 0xF;
947 ret.write[i] = _hex_char(four_bits);
948 }
949 return String::utf8(ret.ptr(), ret.size());
950 }
951
952public:
953 uint32_t high_bits;
954 uint32_t mid_bits;
955 uint32_t low_bits;
956
957 String str() const {
958 return _hex_pad(high_bits) + _hex_pad(mid_bits) + _hex_pad(low_bits);
959 }
960
961 PbxId &operator++() {
962 low_bits++;
963 if (!low_bits) {
964 mid_bits++;
965 if (!mid_bits) {
966 high_bits++;
967 }
968 }
969
970 return *this;
971 }
972};
973
974struct ExportLibsData {
975 Vector<String> lib_paths;
976 String dest_dir;
977};
978
979void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_project_data, const Vector<IOSExportAsset> &p_additional_assets) {
980 // that is just a random number, we just need Godot IDs not to clash with
981 // existing IDs in the project.
982 PbxId current_id = { 0x58938401, 0, 0 };
983 String pbx_files;
984 String pbx_frameworks_build;
985 String pbx_frameworks_refs;
986 String pbx_resources_build;
987 String pbx_resources_refs;
988 String pbx_embeded_frameworks;
989
990 const String file_info_format = String("$build_id = {isa = PBXBuildFile; fileRef = $ref_id; };\n") +
991 "$ref_id = {isa = PBXFileReference; lastKnownFileType = $file_type; name = \"$name\"; path = \"$file_path\"; sourceTree = \"<group>\"; };\n";
992
993 for (int i = 0; i < p_additional_assets.size(); ++i) {
994 String additional_asset_info_format = file_info_format;
995
996 String build_id = (++current_id).str();
997 String ref_id = (++current_id).str();
998 String framework_id = "";
999
1000 const IOSExportAsset &asset = p_additional_assets[i];
1001
1002 String type;
1003 if (asset.exported_path.ends_with(".framework")) {
1004 if (asset.should_embed) {
1005 additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n";
1006 framework_id = (++current_id).str();
1007 pbx_embeded_frameworks += framework_id + ",\n";
1008 }
1009
1010 type = "wrapper.framework";
1011 } else if (asset.exported_path.ends_with(".xcframework")) {
1012 if (asset.should_embed) {
1013 additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n";
1014 framework_id = (++current_id).str();
1015 pbx_embeded_frameworks += framework_id + ",\n";
1016 }
1017
1018 type = "wrapper.xcframework";
1019 } else if (asset.exported_path.ends_with(".dylib")) {
1020 type = "compiled.mach-o.dylib";
1021 } else if (asset.exported_path.ends_with(".a")) {
1022 type = "archive.ar";
1023 } else {
1024 type = "file";
1025 }
1026
1027 String &pbx_build = asset.is_framework ? pbx_frameworks_build : pbx_resources_build;
1028 String &pbx_refs = asset.is_framework ? pbx_frameworks_refs : pbx_resources_refs;
1029
1030 if (pbx_build.length() > 0) {
1031 pbx_build += ",\n";
1032 pbx_refs += ",\n";
1033 }
1034 pbx_build += build_id;
1035 pbx_refs += ref_id;
1036
1037 Dictionary format_dict;
1038 format_dict["build_id"] = build_id;
1039 format_dict["ref_id"] = ref_id;
1040 format_dict["name"] = asset.exported_path.get_file();
1041 format_dict["file_path"] = asset.exported_path;
1042 format_dict["file_type"] = type;
1043 if (framework_id.length() > 0) {
1044 format_dict["framework_id"] = framework_id;
1045 }
1046 pbx_files += additional_asset_info_format.format(format_dict, "$_");
1047 }
1048
1049 // Note, frameworks like gamekit are always included in our project.pbxprof file
1050 // even if turned off in capabilities.
1051
1052 String str = String::utf8((const char *)p_project_data.ptr(), p_project_data.size());
1053 str = str.replace("$additional_pbx_files", pbx_files);
1054 str = str.replace("$additional_pbx_frameworks_build", pbx_frameworks_build);
1055 str = str.replace("$additional_pbx_frameworks_refs", pbx_frameworks_refs);
1056 str = str.replace("$additional_pbx_resources_build", pbx_resources_build);
1057 str = str.replace("$additional_pbx_resources_refs", pbx_resources_refs);
1058 str = str.replace("$pbx_embeded_frameworks", pbx_embeded_frameworks);
1059
1060 CharString cs = str.utf8();
1061 p_project_data.resize(cs.size() - 1);
1062 for (int i = 0; i < cs.size() - 1; i++) {
1063 p_project_data.write[i] = cs[i];
1064 }
1065}
1066
1067Error EditorExportPlatformIOS::_copy_asset(const String &p_out_dir, const String &p_asset, const String *p_custom_file_name, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_assets) {
1068 String binary_name = p_out_dir.get_file().get_basename();
1069
1070 Ref<DirAccess> da = DirAccess::create_for_path(p_asset);
1071 if (da.is_null()) {
1072 ERR_FAIL_V_MSG(ERR_CANT_CREATE, "Can't create directory: " + p_asset + ".");
1073 }
1074 bool file_exists = da->file_exists(p_asset);
1075 bool dir_exists = da->dir_exists(p_asset);
1076 if (!file_exists && !dir_exists) {
1077 return ERR_FILE_NOT_FOUND;
1078 }
1079
1080 String base_dir = p_asset.get_base_dir().replace("res://", "");
1081 String destination_dir;
1082 String destination;
1083 String asset_path;
1084
1085 bool create_framework = false;
1086
1087 if (p_is_framework && p_asset.ends_with(".dylib")) {
1088 // For iOS we need to turn .dylib into .framework
1089 // to be able to send application to AppStore
1090 asset_path = String("dylibs").path_join(base_dir);
1091
1092 String file_name;
1093
1094 if (!p_custom_file_name) {
1095 file_name = p_asset.get_basename().get_file();
1096 } else {
1097 file_name = *p_custom_file_name;
1098 }
1099
1100 String framework_name = file_name + ".framework";
1101
1102 asset_path = asset_path.path_join(framework_name);
1103 destination_dir = p_out_dir.path_join(asset_path);
1104 destination = destination_dir.path_join(file_name);
1105 create_framework = true;
1106 } else if (p_is_framework && (p_asset.ends_with(".framework") || p_asset.ends_with(".xcframework"))) {
1107 asset_path = String("dylibs").path_join(base_dir);
1108
1109 String file_name;
1110
1111 if (!p_custom_file_name) {
1112 file_name = p_asset.get_file();
1113 } else {
1114 file_name = *p_custom_file_name;
1115 }
1116
1117 asset_path = asset_path.path_join(file_name);
1118 destination_dir = p_out_dir.path_join(asset_path);
1119 destination = destination_dir;
1120 } else {
1121 asset_path = base_dir;
1122
1123 String file_name;
1124
1125 if (!p_custom_file_name) {
1126 file_name = p_asset.get_file();
1127 } else {
1128 file_name = *p_custom_file_name;
1129 }
1130
1131 destination_dir = p_out_dir.path_join(asset_path);
1132 asset_path = asset_path.path_join(file_name);
1133 destination = p_out_dir.path_join(asset_path);
1134 }
1135
1136 Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
1137 ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create DirAccess for path '" + p_out_dir + "'.");
1138
1139 if (!filesystem_da->dir_exists(destination_dir)) {
1140 Error make_dir_err = filesystem_da->make_dir_recursive(destination_dir);
1141 if (make_dir_err) {
1142 return make_dir_err;
1143 }
1144 }
1145
1146 Error err = dir_exists ? da->copy_dir(p_asset, destination) : da->copy(p_asset, destination);
1147 if (err) {
1148 return err;
1149 }
1150 IOSExportAsset exported_asset = { binary_name.path_join(asset_path), p_is_framework, p_should_embed };
1151 r_exported_assets.push_back(exported_asset);
1152
1153 if (create_framework) {
1154 String file_name;
1155
1156 if (!p_custom_file_name) {
1157 file_name = p_asset.get_basename().get_file();
1158 } else {
1159 file_name = *p_custom_file_name;
1160 }
1161
1162 String framework_name = file_name + ".framework";
1163
1164 // Performing `install_name_tool -id @rpath/{name}.framework/{name} ./{name}` on dylib
1165 {
1166 List<String> install_name_args;
1167 install_name_args.push_back("-id");
1168 install_name_args.push_back(String("@rpath").path_join(framework_name).path_join(file_name));
1169 install_name_args.push_back(destination);
1170
1171 OS::get_singleton()->execute("install_name_tool", install_name_args);
1172 }
1173
1174 // Creating Info.plist
1175 {
1176 String info_plist_format = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1177 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
1178 "<plist version=\"1.0\">\n"
1179 "<dict>\n"
1180 "<key>CFBundleShortVersionString</key>\n"
1181 "<string>1.0</string>\n"
1182 "<key>CFBundleIdentifier</key>\n"
1183 "<string>com.gdextension.framework.$name</string>\n"
1184 "<key>CFBundleName</key>\n"
1185 "<string>$name</string>\n"
1186 "<key>CFBundleExecutable</key>\n"
1187 "<string>$name</string>\n"
1188 "<key>DTPlatformName</key>\n"
1189 "<string>iphoneos</string>\n"
1190 "<key>CFBundleInfoDictionaryVersion</key>\n"
1191 "<string>6.0</string>\n"
1192 "<key>CFBundleVersion</key>\n"
1193 "<string>1</string>\n"
1194 "<key>CFBundlePackageType</key>\n"
1195 "<string>FMWK</string>\n"
1196 "<key>MinimumOSVersion</key>\n"
1197 "<string>10.0</string>\n"
1198 "</dict>\n"
1199 "</plist>";
1200
1201 String info_plist = info_plist_format.replace("$name", file_name);
1202
1203 Ref<FileAccess> f = FileAccess::open(destination_dir.path_join("Info.plist"), FileAccess::WRITE);
1204 if (f.is_valid()) {
1205 f->store_string(info_plist);
1206 }
1207 }
1208 }
1209
1210 return OK;
1211}
1212
1213Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_assets) {
1214 for (int f_idx = 0; f_idx < p_assets.size(); ++f_idx) {
1215 String asset = p_assets[f_idx];
1216 if (!asset.begins_with("res://")) {
1217 // either SDK-builtin or already a part of the export template
1218 IOSExportAsset exported_asset = { asset, p_is_framework, p_should_embed };
1219 r_exported_assets.push_back(exported_asset);
1220 } else {
1221 Error err = _copy_asset(p_out_dir, asset, nullptr, p_is_framework, p_should_embed, r_exported_assets);
1222 ERR_FAIL_COND_V(err, err);
1223 }
1224 }
1225
1226 return OK;
1227}
1228
1229Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<SharedObject> &p_libraries, Vector<IOSExportAsset> &r_exported_assets) {
1230 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
1231 for (int i = 0; i < export_plugins.size(); i++) {
1232 Vector<String> linked_frameworks = export_plugins[i]->get_ios_frameworks();
1233 Error err = _export_additional_assets(p_out_dir, linked_frameworks, true, false, r_exported_assets);
1234 ERR_FAIL_COND_V(err, err);
1235
1236 Vector<String> embedded_frameworks = export_plugins[i]->get_ios_embedded_frameworks();
1237 err = _export_additional_assets(p_out_dir, embedded_frameworks, true, true, r_exported_assets);
1238 ERR_FAIL_COND_V(err, err);
1239
1240 Vector<String> project_static_libs = export_plugins[i]->get_ios_project_static_libs();
1241 for (int j = 0; j < project_static_libs.size(); j++) {
1242 project_static_libs.write[j] = project_static_libs[j].get_file(); // Only the file name as it's copied to the project
1243 }
1244 err = _export_additional_assets(p_out_dir, project_static_libs, true, false, r_exported_assets);
1245 ERR_FAIL_COND_V(err, err);
1246
1247 Vector<String> ios_bundle_files = export_plugins[i]->get_ios_bundle_files();
1248 err = _export_additional_assets(p_out_dir, ios_bundle_files, false, false, r_exported_assets);
1249 ERR_FAIL_COND_V(err, err);
1250 }
1251
1252 Vector<String> library_paths;
1253 for (int i = 0; i < p_libraries.size(); ++i) {
1254 library_paths.push_back(p_libraries[i].path);
1255 }
1256 Error err = _export_additional_assets(p_out_dir, library_paths, true, true, r_exported_assets);
1257 ERR_FAIL_COND_V(err, err);
1258
1259 return OK;
1260}
1261
1262Vector<String> EditorExportPlatformIOS::_get_preset_architectures(const Ref<EditorExportPreset> &p_preset) const {
1263 Vector<ExportArchitecture> all_archs = _get_supported_architectures();
1264 Vector<String> enabled_archs;
1265 for (int i = 0; i < all_archs.size(); ++i) {
1266 bool is_enabled = p_preset->get("architectures/" + all_archs[i].name);
1267 if (is_enabled) {
1268 enabled_archs.push_back(all_archs[i].name);
1269 }
1270 }
1271 return enabled_archs;
1272}
1273
1274Error EditorExportPlatformIOS::_export_ios_plugins(const Ref<EditorExportPreset> &p_preset, IOSConfigData &p_config_data, const String &dest_dir, Vector<IOSExportAsset> &r_exported_assets, bool p_debug) {
1275 String plugin_definition_cpp_code;
1276 String plugin_initialization_cpp_code;
1277 String plugin_deinitialization_cpp_code;
1278
1279 Vector<String> plugin_linked_dependencies;
1280 Vector<String> plugin_embedded_dependencies;
1281 Vector<String> plugin_files;
1282
1283 Vector<PluginConfigIOS> enabled_plugins = get_enabled_plugins(p_preset);
1284
1285 Vector<String> added_linked_dependenciy_names;
1286 Vector<String> added_embedded_dependenciy_names;
1287 HashMap<String, String> plist_values;
1288
1289 HashSet<String> plugin_linker_flags;
1290
1291 Error err;
1292
1293 for (int i = 0; i < enabled_plugins.size(); i++) {
1294 PluginConfigIOS plugin = enabled_plugins[i];
1295
1296 // Export plugin binary.
1297 String plugin_main_binary = PluginConfigIOS::get_plugin_main_binary(plugin, p_debug);
1298 String plugin_binary_result_file = plugin.binary.get_file();
1299 // We shouldn't embed .xcframework that contains static libraries.
1300 // Static libraries are not embedded anyway.
1301 err = _copy_asset(dest_dir, plugin_main_binary, &plugin_binary_result_file, true, false, r_exported_assets);
1302
1303 ERR_FAIL_COND_V(err, err);
1304
1305 // Adding dependencies.
1306 // Use separate container for names to check for duplicates.
1307 for (int j = 0; j < plugin.linked_dependencies.size(); j++) {
1308 String dependency = plugin.linked_dependencies[j];
1309 String name = dependency.get_file();
1310
1311 if (added_linked_dependenciy_names.has(name)) {
1312 continue;
1313 }
1314
1315 added_linked_dependenciy_names.push_back(name);
1316 plugin_linked_dependencies.push_back(dependency);
1317 }
1318
1319 for (int j = 0; j < plugin.system_dependencies.size(); j++) {
1320 String dependency = plugin.system_dependencies[j];
1321 String name = dependency.get_file();
1322
1323 if (added_linked_dependenciy_names.has(name)) {
1324 continue;
1325 }
1326
1327 added_linked_dependenciy_names.push_back(name);
1328 plugin_linked_dependencies.push_back(dependency);
1329 }
1330
1331 for (int j = 0; j < plugin.embedded_dependencies.size(); j++) {
1332 String dependency = plugin.embedded_dependencies[j];
1333 String name = dependency.get_file();
1334
1335 if (added_embedded_dependenciy_names.has(name)) {
1336 continue;
1337 }
1338
1339 added_embedded_dependenciy_names.push_back(name);
1340 plugin_embedded_dependencies.push_back(dependency);
1341 }
1342
1343 plugin_files.append_array(plugin.files_to_copy);
1344
1345 // Capabilities
1346 // Also checking for duplicates.
1347 for (int j = 0; j < plugin.capabilities.size(); j++) {
1348 String capability = plugin.capabilities[j];
1349
1350 if (p_config_data.capabilities.has(capability)) {
1351 continue;
1352 }
1353
1354 p_config_data.capabilities.push_back(capability);
1355 }
1356
1357 // Linker flags
1358 // Checking duplicates
1359 for (int j = 0; j < plugin.linker_flags.size(); j++) {
1360 String linker_flag = plugin.linker_flags[j];
1361 plugin_linker_flags.insert(linker_flag);
1362 }
1363
1364 // Plist
1365 // Using hash map container to remove duplicates
1366
1367 for (const KeyValue<String, PluginConfigIOS::PlistItem> &E : plugin.plist) {
1368 String key = E.key;
1369 const PluginConfigIOS::PlistItem &item = E.value;
1370
1371 String value;
1372
1373 switch (item.type) {
1374 case PluginConfigIOS::PlistItemType::STRING_INPUT: {
1375 String preset_name = "plugins_plist/" + key;
1376 String input_value = p_preset->get(preset_name);
1377 value = "<string>" + input_value + "</string>";
1378 } break;
1379 default:
1380 value = item.value;
1381 break;
1382 }
1383
1384 if (key.is_empty() || value.is_empty()) {
1385 continue;
1386 }
1387
1388 String plist_key = "<key>" + key + "</key>";
1389
1390 plist_values[plist_key] = value;
1391 }
1392
1393 // CPP Code
1394 String definition_comment = "// Plugin: " + plugin.name + "\n";
1395 String initialization_method = plugin.initialization_method + "();\n";
1396 String deinitialization_method = plugin.deinitialization_method + "();\n";
1397
1398 plugin_definition_cpp_code += definition_comment +
1399 "extern void " + initialization_method +
1400 "extern void " + deinitialization_method + "\n";
1401
1402 plugin_initialization_cpp_code += "\t" + initialization_method;
1403 plugin_deinitialization_cpp_code += "\t" + deinitialization_method;
1404
1405 if (plugin.use_swift_runtime) {
1406 p_config_data.use_swift_runtime = true;
1407 }
1408 }
1409
1410 // Updating `Info.plist`
1411 {
1412 for (const KeyValue<String, String> &E : plist_values) {
1413 String key = E.key;
1414 String value = E.value;
1415
1416 if (key.is_empty() || value.is_empty()) {
1417 continue;
1418 }
1419
1420 p_config_data.plist_content += key + value + "\n";
1421 }
1422 }
1423
1424 // Export files
1425 {
1426 // Export linked plugin dependency
1427 err = _export_additional_assets(dest_dir, plugin_linked_dependencies, true, false, r_exported_assets);
1428 ERR_FAIL_COND_V(err, err);
1429
1430 // Export embedded plugin dependency
1431 err = _export_additional_assets(dest_dir, plugin_embedded_dependencies, true, true, r_exported_assets);
1432 ERR_FAIL_COND_V(err, err);
1433
1434 // Export plugin files
1435 err = _export_additional_assets(dest_dir, plugin_files, false, false, r_exported_assets);
1436 ERR_FAIL_COND_V(err, err);
1437 }
1438
1439 // Update CPP
1440 {
1441 Dictionary plugin_format;
1442 plugin_format["definition"] = plugin_definition_cpp_code;
1443 plugin_format["initialization"] = plugin_initialization_cpp_code;
1444 plugin_format["deinitialization"] = plugin_deinitialization_cpp_code;
1445
1446 String plugin_cpp_code = "\n// Godot Plugins\n"
1447 "void godot_ios_plugins_initialize();\n"
1448 "void godot_ios_plugins_deinitialize();\n"
1449 "// Exported Plugins\n\n"
1450 "$definition"
1451 "// Use Plugins\n"
1452 "void godot_ios_plugins_initialize() {\n"
1453 "$initialization"
1454 "}\n\n"
1455 "void godot_ios_plugins_deinitialize() {\n"
1456 "$deinitialization"
1457 "}\n";
1458
1459 p_config_data.cpp_code += plugin_cpp_code.format(plugin_format, "$_");
1460 }
1461
1462 // Update Linker Flag Values
1463 {
1464 String result_linker_flags = " ";
1465 for (const String &E : plugin_linker_flags) {
1466 const String &flag = E;
1467
1468 if (flag.length() == 0) {
1469 continue;
1470 }
1471
1472 if (result_linker_flags.length() > 0) {
1473 result_linker_flags += ' ';
1474 }
1475
1476 result_linker_flags += flag;
1477 }
1478 result_linker_flags = result_linker_flags.replace("\"", "\\\"");
1479 p_config_data.linker_flags += result_linker_flags;
1480 }
1481
1482 return OK;
1483}
1484
1485Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
1486 return _export_project_helper(p_preset, p_debug, p_path, p_flags, false, false);
1487}
1488
1489Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags, bool p_simulator, bool p_skip_ipa) {
1490 ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
1491
1492 String src_pkg_name;
1493 String dest_dir = p_path.get_base_dir() + "/";
1494 String binary_name = p_path.get_file().get_basename();
1495
1496 bool export_project_only = p_preset->get("application/export_project_only");
1497
1498 EditorProgress ep("export", export_project_only ? TTR("Exporting for iOS (Project Files Only)") : TTR("Exporting for iOS"), export_project_only ? 2 : 5, true);
1499
1500 String team_id = p_preset->get("application/app_store_team_id");
1501 ERR_FAIL_COND_V_MSG(team_id.length() == 0, ERR_CANT_OPEN, "App Store Team ID not specified - cannot configure the project.");
1502
1503 if (p_debug) {
1504 src_pkg_name = p_preset->get("custom_template/debug");
1505 } else {
1506 src_pkg_name = p_preset->get("custom_template/release");
1507 }
1508
1509 if (src_pkg_name.is_empty()) {
1510 String err;
1511 src_pkg_name = find_export_template("ios.zip", &err);
1512 if (src_pkg_name.is_empty()) {
1513 add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("Export template not found."));
1514 return ERR_FILE_NOT_FOUND;
1515 }
1516 }
1517
1518 if (!DirAccess::exists(dest_dir)) {
1519 return ERR_FILE_BAD_PATH;
1520 }
1521
1522 {
1523 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
1524 if (da.is_valid()) {
1525 String current_dir = da->get_current_dir();
1526
1527 // remove leftovers from last export so they don't interfere
1528 // in case some files are no longer needed
1529 if (da->change_dir(dest_dir + binary_name + ".xcodeproj") == OK) {
1530 da->erase_contents_recursive();
1531 }
1532 if (da->change_dir(dest_dir + binary_name) == OK) {
1533 da->erase_contents_recursive();
1534 }
1535
1536 da->change_dir(current_dir);
1537
1538 if (!da->dir_exists(dest_dir + binary_name)) {
1539 Error err = da->make_dir(dest_dir + binary_name);
1540 if (err) {
1541 return err;
1542 }
1543 }
1544 }
1545 }
1546
1547 if (ep.step("Making .pck", 0)) {
1548 return ERR_SKIP;
1549 }
1550 String pack_path = dest_dir + binary_name + ".pck";
1551 Vector<SharedObject> libraries;
1552 Error err = save_pack(p_preset, p_debug, pack_path, &libraries);
1553 if (err) {
1554 return err;
1555 }
1556
1557 if (ep.step("Extracting and configuring Xcode project", 1)) {
1558 return ERR_SKIP;
1559 }
1560
1561 String library_to_use = "libgodot.ios." + String(p_debug ? "debug" : "release") + ".xcframework";
1562
1563 print_line("Static framework: " + library_to_use);
1564 String pkg_name;
1565 if (String(GLOBAL_GET("application/config/name")) != "") {
1566 pkg_name = String(GLOBAL_GET("application/config/name"));
1567 } else {
1568 pkg_name = "Unnamed";
1569 }
1570
1571 bool found_library = false;
1572
1573 const String project_file = "godot_ios.xcodeproj/project.pbxproj";
1574 HashSet<String> files_to_parse;
1575 files_to_parse.insert("godot_ios/godot_ios-Info.plist");
1576 files_to_parse.insert(project_file);
1577 files_to_parse.insert("godot_ios/export_options.plist");
1578 files_to_parse.insert("godot_ios/dummy.cpp");
1579 files_to_parse.insert("godot_ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata");
1580 files_to_parse.insert("godot_ios.xcodeproj/xcshareddata/xcschemes/godot_ios.xcscheme");
1581 files_to_parse.insert("godot_ios/godot_ios.entitlements");
1582 files_to_parse.insert("godot_ios/Launch Screen.storyboard");
1583
1584 IOSConfigData config_data = {
1585 pkg_name,
1586 binary_name,
1587 _get_additional_plist_content(),
1588 String(" ").join(_get_preset_architectures(p_preset)),
1589 _get_linker_flags(),
1590 _get_cpp_code(),
1591 "",
1592 "",
1593 "",
1594 "",
1595 Vector<String>(),
1596 false
1597 };
1598
1599 Vector<IOSExportAsset> assets;
1600
1601 Ref<DirAccess> tmp_app_path = DirAccess::create_for_path(dest_dir);
1602 ERR_FAIL_COND_V(tmp_app_path.is_null(), ERR_CANT_CREATE);
1603
1604 print_line("Unzipping...");
1605 Ref<FileAccess> io_fa;
1606 zlib_filefunc_def io = zipio_create_io(&io_fa);
1607 unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io);
1608 if (!src_pkg_zip) {
1609 add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("Could not open export template (not a zip file?): \"%s\".", src_pkg_name));
1610 return ERR_CANT_OPEN;
1611 }
1612
1613 err = _export_ios_plugins(p_preset, config_data, dest_dir + binary_name, assets, p_debug);
1614 ERR_FAIL_COND_V(err, err);
1615
1616 //export rest of the files
1617 int ret = unzGoToFirstFile(src_pkg_zip);
1618 Vector<uint8_t> project_file_data;
1619 while (ret == UNZ_OK) {
1620#if defined(MACOS_ENABLED) || defined(LINUXBSD_ENABLED)
1621 bool is_execute = false;
1622#endif
1623
1624 //get filename
1625 unz_file_info info;
1626 char fname[16384];
1627 ret = unzGetCurrentFileInfo(src_pkg_zip, &info, fname, 16384, nullptr, 0, nullptr, 0);
1628 if (ret != UNZ_OK) {
1629 break;
1630 }
1631
1632 String file = String::utf8(fname);
1633
1634 print_line("READ: " + file);
1635 Vector<uint8_t> data;
1636 data.resize(info.uncompressed_size);
1637
1638 //read
1639 unzOpenCurrentFile(src_pkg_zip);
1640 unzReadCurrentFile(src_pkg_zip, data.ptrw(), data.size());
1641 unzCloseCurrentFile(src_pkg_zip);
1642
1643 //write
1644
1645 if (files_to_parse.has(file)) {
1646 _fix_config_file(p_preset, data, config_data, p_debug);
1647 } else if (file.begins_with("libgodot.ios")) {
1648 if (!file.begins_with(library_to_use) || file.ends_with(String("/empty"))) {
1649 ret = unzGoToNextFile(src_pkg_zip);
1650 continue; //ignore!
1651 }
1652 found_library = true;
1653#if defined(MACOS_ENABLED) || defined(LINUXBSD_ENABLED)
1654 is_execute = true;
1655#endif
1656 file = file.replace(library_to_use, binary_name + ".xcframework");
1657 }
1658
1659 if (file == project_file) {
1660 project_file_data = data;
1661 }
1662
1663 ///@TODO need to parse logo files
1664
1665 if (data.size() > 0) {
1666 file = file.replace("godot_ios", binary_name);
1667
1668 print_line("ADDING: " + file + " size: " + itos(data.size()));
1669
1670 /* write it into our folder structure */
1671 file = dest_dir + file;
1672
1673 /* make sure this folder exists */
1674 String dir_name = file.get_base_dir();
1675 if (!tmp_app_path->dir_exists(dir_name)) {
1676 print_line("Creating " + dir_name);
1677 Error dir_err = tmp_app_path->make_dir_recursive(dir_name);
1678 if (dir_err) {
1679 ERR_PRINT("Can't create '" + dir_name + "'.");
1680 unzClose(src_pkg_zip);
1681 return ERR_CANT_CREATE;
1682 }
1683 }
1684
1685 /* write the file */
1686 {
1687 Ref<FileAccess> f = FileAccess::open(file, FileAccess::WRITE);
1688 if (f.is_null()) {
1689 ERR_PRINT("Can't write '" + file + "'.");
1690 unzClose(src_pkg_zip);
1691 return ERR_CANT_CREATE;
1692 };
1693 f->store_buffer(data.ptr(), data.size());
1694 }
1695
1696#if defined(MACOS_ENABLED) || defined(LINUXBSD_ENABLED)
1697 if (is_execute) {
1698 // we need execute rights on this file
1699 chmod(file.utf8().get_data(), 0755);
1700 }
1701#endif
1702 }
1703
1704 ret = unzGoToNextFile(src_pkg_zip);
1705 }
1706
1707 /* we're done with our source zip */
1708 unzClose(src_pkg_zip);
1709
1710 if (!found_library) {
1711 ERR_PRINT("Requested template library '" + library_to_use + "' not found. It might be missing from your template archive.");
1712 return ERR_FILE_NOT_FOUND;
1713 }
1714
1715 Dictionary appnames = GLOBAL_GET("application/config/name_localized");
1716 Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized");
1717 Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized");
1718 Dictionary photolibrary_usage_descriptions = p_preset->get("privacy/photolibrary_usage_description_localized");
1719
1720 Vector<String> translations = GLOBAL_GET("internationalization/locale/translations");
1721 if (translations.size() > 0) {
1722 {
1723 String fname = dest_dir + binary_name + "/en.lproj";
1724 tmp_app_path->make_dir_recursive(fname);
1725 Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
1726 f->store_line("/* Localized versions of Info.plist keys */");
1727 f->store_line("");
1728 f->store_line("CFBundleDisplayName = \"" + GLOBAL_GET("application/config/name").operator String() + "\";");
1729 f->store_line("NSCameraUsageDescription = \"" + p_preset->get("privacy/camera_usage_description").operator String() + "\";");
1730 f->store_line("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_usage_description").operator String() + "\";");
1731 f->store_line("NSPhotoLibraryUsageDescription = \"" + p_preset->get("privacy/photolibrary_usage_description").operator String() + "\";");
1732 }
1733
1734 HashSet<String> languages;
1735 for (const String &E : translations) {
1736 Ref<Translation> tr = ResourceLoader::load(E);
1737 if (tr.is_valid() && tr->get_locale() != "en") {
1738 languages.insert(tr->get_locale());
1739 }
1740 }
1741
1742 for (const String &lang : languages) {
1743 String fname = dest_dir + binary_name + "/" + lang + ".lproj";
1744 tmp_app_path->make_dir_recursive(fname);
1745 Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
1746 f->store_line("/* Localized versions of Info.plist keys */");
1747 f->store_line("");
1748 if (appnames.has(lang)) {
1749 f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";");
1750 }
1751 if (camera_usage_descriptions.has(lang)) {
1752 f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";");
1753 }
1754 if (microphone_usage_descriptions.has(lang)) {
1755 f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";");
1756 }
1757 if (photolibrary_usage_descriptions.has(lang)) {
1758 f->store_line("NSPhotoLibraryUsageDescription = \"" + photolibrary_usage_descriptions[lang].operator String() + "\";");
1759 }
1760 }
1761 }
1762
1763 // Copy project static libs to the project
1764 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
1765 for (int i = 0; i < export_plugins.size(); i++) {
1766 Vector<String> project_static_libs = export_plugins[i]->get_ios_project_static_libs();
1767 for (int j = 0; j < project_static_libs.size(); j++) {
1768 const String &static_lib_path = project_static_libs[j];
1769 String dest_lib_file_path = dest_dir + static_lib_path.get_file();
1770 Error lib_copy_err = tmp_app_path->copy(static_lib_path, dest_lib_file_path);
1771 if (lib_copy_err != OK) {
1772 ERR_PRINT("Can't copy '" + static_lib_path + "'.");
1773 return lib_copy_err;
1774 }
1775 }
1776 }
1777
1778 String iconset_dir = dest_dir + binary_name + "/Images.xcassets/AppIcon.appiconset/";
1779 err = OK;
1780 if (!tmp_app_path->dir_exists(iconset_dir)) {
1781 err = tmp_app_path->make_dir_recursive(iconset_dir);
1782 }
1783 if (err) {
1784 return err;
1785 }
1786
1787 err = _export_icons(p_preset, iconset_dir);
1788 if (err) {
1789 return err;
1790 }
1791
1792 {
1793 bool use_storyboard = p_preset->get("storyboard/use_launch_screen_storyboard");
1794
1795 String launch_image_path = dest_dir + binary_name + "/Images.xcassets/LaunchImage.launchimage/";
1796 String splash_image_path = dest_dir + binary_name + "/Images.xcassets/SplashImage.imageset/";
1797
1798 Ref<DirAccess> launch_screen_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
1799 if (launch_screen_da.is_null()) {
1800 return ERR_CANT_CREATE;
1801 }
1802
1803 if (use_storyboard) {
1804 print_line("Using Launch Storyboard");
1805
1806 if (launch_screen_da->change_dir(launch_image_path) == OK) {
1807 launch_screen_da->erase_contents_recursive();
1808 launch_screen_da->remove(launch_image_path);
1809 }
1810
1811 err = _export_loading_screen_file(p_preset, splash_image_path);
1812 } else {
1813 print_line("Using Launch Images");
1814
1815 const String launch_screen_path = dest_dir + binary_name + "/Launch Screen.storyboard";
1816
1817 launch_screen_da->remove(launch_screen_path);
1818
1819 if (launch_screen_da->change_dir(splash_image_path) == OK) {
1820 launch_screen_da->erase_contents_recursive();
1821 launch_screen_da->remove(splash_image_path);
1822 }
1823
1824 err = _export_loading_screen_images(p_preset, launch_image_path);
1825 }
1826 }
1827
1828 if (err) {
1829 return err;
1830 }
1831
1832 print_line("Exporting additional assets");
1833 _export_additional_assets(dest_dir + binary_name, libraries, assets);
1834 _add_assets_to_project(p_preset, project_file_data, assets);
1835 String project_file_name = dest_dir + binary_name + ".xcodeproj/project.pbxproj";
1836 {
1837 Ref<FileAccess> f = FileAccess::open(project_file_name, FileAccess::WRITE);
1838 if (f.is_null()) {
1839 ERR_PRINT("Can't write '" + project_file_name + "'.");
1840 return ERR_CANT_CREATE;
1841 };
1842 f->store_buffer(project_file_data.ptr(), project_file_data.size());
1843 }
1844
1845#ifdef MACOS_ENABLED
1846 {
1847 if (ep.step("Code-signing dylibs", 2)) {
1848 return ERR_SKIP;
1849 }
1850 Ref<DirAccess> dylibs_dir = DirAccess::open(dest_dir + binary_name + "/dylibs");
1851 ERR_FAIL_COND_V(dylibs_dir.is_null(), ERR_CANT_OPEN);
1852 CodesignData codesign_data(p_preset, p_debug);
1853 err = _walk_dir_recursive(dylibs_dir, _codesign, &codesign_data);
1854 if (err != OK) {
1855 add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Code signing failed, see editor log for details."));
1856 return err;
1857 }
1858 }
1859
1860 if (export_project_only) {
1861 return OK;
1862 }
1863
1864 if (ep.step("Making .xcarchive", 3)) {
1865 return ERR_SKIP;
1866 }
1867 String archive_path = p_path.get_basename() + ".xcarchive";
1868 List<String> archive_args;
1869 archive_args.push_back("-project");
1870 archive_args.push_back(dest_dir + binary_name + ".xcodeproj");
1871 archive_args.push_back("-scheme");
1872 archive_args.push_back(binary_name);
1873 archive_args.push_back("-sdk");
1874 if (p_simulator) {
1875 archive_args.push_back("iphonesimulator");
1876 } else {
1877 archive_args.push_back("iphoneos");
1878 }
1879 archive_args.push_back("-configuration");
1880 archive_args.push_back(p_debug ? "Debug" : "Release");
1881 archive_args.push_back("-destination");
1882 if (p_simulator) {
1883 archive_args.push_back("generic/platform=iOS Simulator");
1884 } else {
1885 archive_args.push_back("generic/platform=iOS");
1886 }
1887 archive_args.push_back("archive");
1888 archive_args.push_back("-allowProvisioningUpdates");
1889 archive_args.push_back("-archivePath");
1890 archive_args.push_back(archive_path);
1891 String archive_str;
1892 err = OS::get_singleton()->execute("xcodebuild", archive_args, &archive_str, nullptr, true);
1893 ERR_FAIL_COND_V(err, err);
1894 print_line("xcodebuild (.xcarchive):\n" + archive_str);
1895 if (!archive_str.contains("** ARCHIVE SUCCEEDED **")) {
1896 add_message(EXPORT_MESSAGE_ERROR, TTR("Xcode Build"), TTR("Xcode project build failed, see editor log for details."));
1897 return FAILED;
1898 }
1899
1900 if (!p_skip_ipa) {
1901 if (ep.step("Making .ipa", 4)) {
1902 return ERR_SKIP;
1903 }
1904 List<String> export_args;
1905 export_args.push_back("-exportArchive");
1906 export_args.push_back("-archivePath");
1907 export_args.push_back(archive_path);
1908 export_args.push_back("-exportOptionsPlist");
1909 export_args.push_back(dest_dir + binary_name + "/export_options.plist");
1910 export_args.push_back("-allowProvisioningUpdates");
1911 export_args.push_back("-exportPath");
1912 export_args.push_back(dest_dir);
1913 String export_str;
1914 err = OS::get_singleton()->execute("xcodebuild", export_args, &export_str, nullptr, true);
1915 ERR_FAIL_COND_V(err, err);
1916 print_line("xcodebuild (.ipa):\n" + export_str);
1917 if (!export_str.contains("** EXPORT SUCCEEDED **")) {
1918 add_message(EXPORT_MESSAGE_ERROR, TTR("Xcode Build"), TTR(".ipa export failed, see editor log for details."));
1919 return FAILED;
1920 }
1921 }
1922#else
1923 add_message(EXPORT_MESSAGE_WARNING, TTR("Xcode Build"), TTR(".ipa can only be built on macOS. Leaving Xcode project without building the package."));
1924#endif
1925
1926 return OK;
1927}
1928
1929bool EditorExportPlatformIOS::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
1930#ifdef MODULE_MONO_ENABLED
1931 // Don't check for additional errors, as this particular error cannot be resolved.
1932 r_error += TTR("Exporting to iOS is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target iOS with C#/Mono instead.") + "\n";
1933 r_error += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n";
1934 return false;
1935#else
1936
1937 String err;
1938 bool valid = false;
1939
1940 // Look for export templates (first official, and if defined custom templates).
1941
1942 bool dvalid = exists_export_template("ios.zip", &err);
1943 bool rvalid = dvalid; // Both in the same ZIP.
1944
1945 if (p_preset->get("custom_template/debug") != "") {
1946 dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
1947 if (!dvalid) {
1948 err += TTR("Custom debug template not found.") + "\n";
1949 }
1950 }
1951 if (p_preset->get("custom_template/release") != "") {
1952 rvalid = FileAccess::exists(p_preset->get("custom_template/release"));
1953 if (!rvalid) {
1954 err += TTR("Custom release template not found.") + "\n";
1955 }
1956 }
1957
1958 valid = dvalid || rvalid;
1959 r_missing_templates = !valid;
1960
1961 if (!err.is_empty()) {
1962 r_error = err;
1963 }
1964
1965 return valid;
1966#endif // !MODULE_MONO_ENABLED
1967}
1968
1969bool EditorExportPlatformIOS::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
1970 String err;
1971 bool valid = true;
1972
1973 // Validate the project configuration.
1974
1975 List<ExportOption> options;
1976 get_export_options(&options);
1977 for (const EditorExportPlatform::ExportOption &E : options) {
1978 if (get_export_option_visibility(p_preset.ptr(), E.option.name)) {
1979 String warn = get_export_option_warning(p_preset.ptr(), E.option.name);
1980 if (!warn.is_empty()) {
1981 err += warn + "\n";
1982 if (E.required) {
1983 valid = false;
1984 }
1985 }
1986 }
1987 }
1988
1989 if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {
1990 valid = false;
1991 }
1992
1993 if (!err.is_empty()) {
1994 r_error = err;
1995 }
1996
1997 return valid;
1998}
1999
2000int EditorExportPlatformIOS::get_options_count() const {
2001 MutexLock lock(device_lock);
2002 return devices.size();
2003}
2004
2005String EditorExportPlatformIOS::get_options_tooltip() const {
2006 return TTR("Select device from the list");
2007}
2008
2009Ref<ImageTexture> EditorExportPlatformIOS::get_option_icon(int p_index) const {
2010 MutexLock lock(device_lock);
2011
2012 Ref<ImageTexture> icon;
2013 if (p_index >= 0 || p_index < devices.size()) {
2014 Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
2015 if (theme.is_valid()) {
2016 if (devices[p_index].simulator) {
2017 icon = theme->get_icon("IOSSimulator", EditorStringName(EditorIcons));
2018 } else if (devices[p_index].wifi) {
2019 icon = theme->get_icon("IOSDeviceWireless", EditorStringName(EditorIcons));
2020 } else {
2021 icon = theme->get_icon("IOSDeviceWired", EditorStringName(EditorIcons));
2022 }
2023 }
2024 }
2025 return icon;
2026}
2027
2028String EditorExportPlatformIOS::get_option_label(int p_index) const {
2029 ERR_FAIL_INDEX_V(p_index, devices.size(), "");
2030 MutexLock lock(device_lock);
2031 return devices[p_index].name;
2032}
2033
2034String EditorExportPlatformIOS::get_option_tooltip(int p_index) const {
2035 ERR_FAIL_INDEX_V(p_index, devices.size(), "");
2036 MutexLock lock(device_lock);
2037 return "UUID: " + devices[p_index].id;
2038}
2039
2040bool EditorExportPlatformIOS::is_package_name_valid(const String &p_package, String *r_error) const {
2041 String pname = p_package;
2042
2043 if (pname.length() == 0) {
2044 if (r_error) {
2045 *r_error = TTR("Identifier is missing.");
2046 }
2047 return false;
2048 }
2049
2050 for (int i = 0; i < pname.length(); i++) {
2051 char32_t c = pname[i];
2052 if (!(is_ascii_alphanumeric_char(c) || c == '-' || c == '.')) {
2053 if (r_error) {
2054 *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c));
2055 }
2056 return false;
2057 }
2058 }
2059
2060 return true;
2061}
2062
2063#ifdef MACOS_ENABLED
2064void EditorExportPlatformIOS::_check_for_changes_poll_thread(void *ud) {
2065 EditorExportPlatformIOS *ea = static_cast<EditorExportPlatformIOS *>(ud);
2066
2067 while (!ea->quit_request.is_set()) {
2068 // Nothing to do if we already know the plugins have changed.
2069 if (!ea->plugins_changed.is_set()) {
2070 MutexLock lock(ea->plugins_lock);
2071
2072 Vector<PluginConfigIOS> loaded_plugins = get_plugins();
2073
2074 if (ea->plugins.size() != loaded_plugins.size()) {
2075 ea->plugins_changed.set();
2076 } else {
2077 for (int i = 0; i < ea->plugins.size(); i++) {
2078 if (ea->plugins[i].name != loaded_plugins[i].name || ea->plugins[i].last_updated != loaded_plugins[i].last_updated) {
2079 ea->plugins_changed.set();
2080 break;
2081 }
2082 }
2083 }
2084 }
2085
2086 // Check for devices updates.
2087 Vector<Device> ldevices;
2088
2089 // Enum real devices.
2090 String idepl = EDITOR_GET("export/ios/ios_deploy");
2091 if (idepl.is_empty()) {
2092 idepl = "ios-deploy";
2093 }
2094 {
2095 String devices;
2096 List<String> args;
2097 args.push_back("-c");
2098 args.push_back("-timeout");
2099 args.push_back("1");
2100 args.push_back("-j");
2101 args.push_back("-u");
2102 args.push_back("-I");
2103
2104 int ec = 0;
2105 Error err = OS::get_singleton()->execute(idepl, args, &devices, &ec, true);
2106 if (err == OK && ec == 0) {
2107 Ref<JSON> json;
2108 json.instantiate();
2109 devices = "{ \"devices\":[" + devices.replace("}{", "},{") + "]}";
2110 err = json->parse(devices);
2111 if (err == OK) {
2112 Dictionary data = json->get_data();
2113 Array devices = data["devices"];
2114 for (int i = 0; i < devices.size(); i++) {
2115 Dictionary device_event = devices[i];
2116 if (device_event["Event"] == "DeviceDetected") {
2117 Dictionary device_info = device_event["Device"];
2118 Device nd;
2119 nd.id = device_info["DeviceIdentifier"];
2120 nd.name = device_info["DeviceName"].operator String() + " (connected through " + device_event["Interface"].operator String() + ")";
2121 nd.wifi = device_event["Interface"] == "WIFI";
2122 nd.simulator = false;
2123 ldevices.push_back(nd);
2124 }
2125 }
2126 }
2127 }
2128 }
2129
2130 // Enum simulators
2131 if (FileAccess::exists("/usr/bin/xcrun") || FileAccess::exists("/bin/xcrun")) {
2132 String devices;
2133 List<String> args;
2134 args.push_back("simctl");
2135 args.push_back("list");
2136 args.push_back("devices");
2137 args.push_back("-j");
2138
2139 int ec = 0;
2140 Error err = OS::get_singleton()->execute("xcrun", args, &devices, &ec, true);
2141 if (err == OK && ec == 0) {
2142 Ref<JSON> json;
2143 json.instantiate();
2144 err = json->parse(devices);
2145 if (err == OK) {
2146 Dictionary data = json->get_data();
2147 Dictionary devices = data["devices"];
2148 for (const Variant *key = devices.next(nullptr); key; key = devices.next(key)) {
2149 Array os_devices = devices[*key];
2150 for (int i = 0; i < os_devices.size(); i++) {
2151 Dictionary device_info = os_devices[i];
2152 if (device_info["isAvailable"].operator bool() && device_info["state"] == "Booted") {
2153 Device nd;
2154 nd.id = device_info["udid"];
2155 nd.name = device_info["name"].operator String() + " (simulator)";
2156 nd.simulator = true;
2157 ldevices.push_back(nd);
2158 }
2159 }
2160 }
2161 }
2162 }
2163 }
2164
2165 // Update device list.
2166 {
2167 MutexLock lock(ea->device_lock);
2168
2169 bool different = false;
2170
2171 if (ea->devices.size() != ldevices.size()) {
2172 different = true;
2173 } else {
2174 for (int i = 0; i < ea->devices.size(); i++) {
2175 if (ea->devices[i].id != ldevices[i].id) {
2176 different = true;
2177 break;
2178 }
2179 }
2180 }
2181
2182 if (different) {
2183 ea->devices = ldevices;
2184 ea->devices_changed.set();
2185 }
2186 }
2187
2188 uint64_t sleep = 200;
2189 uint64_t wait = 3000000;
2190 uint64_t time = OS::get_singleton()->get_ticks_usec();
2191 while (OS::get_singleton()->get_ticks_usec() - time < wait) {
2192 OS::get_singleton()->delay_usec(1000 * sleep);
2193 if (ea->quit_request.is_set()) {
2194 break;
2195 }
2196 }
2197 }
2198}
2199#endif
2200
2201Error EditorExportPlatformIOS::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) {
2202#ifdef MACOS_ENABLED
2203 ERR_FAIL_INDEX_V(p_device, devices.size(), ERR_INVALID_PARAMETER);
2204
2205 String can_export_error;
2206 bool can_export_missing_templates;
2207 if (!can_export(p_preset, can_export_error, can_export_missing_templates)) {
2208 add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error);
2209 return ERR_UNCONFIGURED;
2210 }
2211
2212 MutexLock lock(device_lock);
2213
2214 EditorProgress ep("run", vformat(TTR("Running on %s"), devices[p_device].name), 3);
2215
2216 String id = "tmpexport." + uitos(OS::get_singleton()->get_unix_time());
2217
2218 Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
2219 ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create DirAccess for path '" + EditorPaths::get_singleton()->get_cache_dir() + "'.");
2220 filesystem_da->make_dir_recursive(EditorPaths::get_singleton()->get_cache_dir().path_join(id));
2221 String tmp_export_path = EditorPaths::get_singleton()->get_cache_dir().path_join(id).path_join("export.ipa");
2222
2223#define CLEANUP_AND_RETURN(m_err) \
2224 { \
2225 if (filesystem_da->change_dir(EditorPaths::get_singleton()->get_cache_dir().path_join(id)) == OK) { \
2226 filesystem_da->erase_contents_recursive(); \
2227 filesystem_da->change_dir(".."); \
2228 filesystem_da->remove(id); \
2229 } \
2230 return m_err; \
2231 } \
2232 ((void)0)
2233
2234 Device dev = devices[p_device];
2235
2236 // Export before sending to device.
2237 Error err = _export_project_helper(p_preset, true, tmp_export_path, p_debug_flags, dev.simulator, true);
2238
2239 if (err != OK) {
2240 CLEANUP_AND_RETURN(err);
2241 }
2242
2243 Vector<String> cmd_args_list;
2244 String host = EDITOR_GET("network/debug/remote_host");
2245 int remote_port = (int)EDITOR_GET("network/debug/remote_port");
2246
2247 if (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST) {
2248 host = "localhost";
2249 }
2250
2251 if (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT) {
2252 int port = EDITOR_GET("filesystem/file_server/port");
2253 String passwd = EDITOR_GET("filesystem/file_server/password");
2254 cmd_args_list.push_back("--remote-fs");
2255 cmd_args_list.push_back(host + ":" + itos(port));
2256 if (!passwd.is_empty()) {
2257 cmd_args_list.push_back("--remote-fs-password");
2258 cmd_args_list.push_back(passwd);
2259 }
2260 }
2261
2262 if (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) {
2263 cmd_args_list.push_back("--remote-debug");
2264
2265 cmd_args_list.push_back(get_debug_protocol() + host + ":" + String::num(remote_port));
2266
2267 List<String> breakpoints;
2268 ScriptEditor::get_singleton()->get_breakpoints(&breakpoints);
2269
2270 if (breakpoints.size()) {
2271 cmd_args_list.push_back("--breakpoints");
2272 String bpoints;
2273 for (const List<String>::Element *E = breakpoints.front(); E; E = E->next()) {
2274 bpoints += E->get().replace(" ", "%20");
2275 if (E->next()) {
2276 bpoints += ",";
2277 }
2278 }
2279
2280 cmd_args_list.push_back(bpoints);
2281 }
2282 }
2283
2284 if (p_debug_flags & DEBUG_FLAG_VIEW_COLLISIONS) {
2285 cmd_args_list.push_back("--debug-collisions");
2286 }
2287
2288 if (p_debug_flags & DEBUG_FLAG_VIEW_NAVIGATION) {
2289 cmd_args_list.push_back("--debug-navigation");
2290 }
2291
2292 if (dev.simulator) {
2293 // Deploy and run on simulator.
2294 if (ep.step("Installing to simulator...", 3)) {
2295 CLEANUP_AND_RETURN(ERR_SKIP);
2296 } else {
2297 List<String> args;
2298 args.push_back("simctl");
2299 args.push_back("install");
2300 args.push_back(dev.id);
2301 args.push_back(EditorPaths::get_singleton()->get_cache_dir().path_join(id).path_join("export.xcarchive/Products/Applications/export.app"));
2302
2303 String log;
2304 int ec;
2305 err = OS::get_singleton()->execute("xcrun", args, &log, &ec, true);
2306 if (err != OK) {
2307 add_message(EXPORT_MESSAGE_WARNING, TTR("Run"), TTR("Could not start simctl executable."));
2308 CLEANUP_AND_RETURN(err);
2309 }
2310 if (ec != 0) {
2311 print_line("simctl install:\n" + log);
2312 add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Installation failed, see editor log for details."));
2313 CLEANUP_AND_RETURN(ERR_UNCONFIGURED);
2314 }
2315 }
2316
2317 if (ep.step("Running on simulator...", 4)) {
2318 CLEANUP_AND_RETURN(ERR_SKIP);
2319 } else {
2320 List<String> args;
2321 args.push_back("simctl");
2322 args.push_back("launch");
2323 args.push_back(dev.id);
2324 args.push_back(p_preset->get("application/bundle_identifier"));
2325 for (const String &E : cmd_args_list) {
2326 args.push_back(E);
2327 }
2328
2329 String log;
2330 int ec;
2331 err = OS::get_singleton()->execute("xcrun", args, &log, &ec, true);
2332 if (err != OK) {
2333 add_message(EXPORT_MESSAGE_WARNING, TTR("Run"), TTR("Could not start simctl executable."));
2334 CLEANUP_AND_RETURN(err);
2335 }
2336 if (ec != 0) {
2337 print_line("simctl launch:\n" + log);
2338 add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Running failed, see editor log for details."));
2339 }
2340 }
2341 } else {
2342 // Deploy and run on real device.
2343 if (ep.step("Installing and running on device...", 4)) {
2344 CLEANUP_AND_RETURN(ERR_SKIP);
2345 } else {
2346 List<String> args;
2347 args.push_back("-u");
2348 args.push_back("-I");
2349 args.push_back("--id");
2350 args.push_back(dev.id);
2351 args.push_back("--justlaunch");
2352 args.push_back("--bundle");
2353 args.push_back(EditorPaths::get_singleton()->get_cache_dir().path_join(id).path_join("export.xcarchive/Products/Applications/export.app"));
2354 String app_args;
2355 for (const String &E : cmd_args_list) {
2356 app_args += E + " ";
2357 }
2358 if (!app_args.is_empty()) {
2359 args.push_back("--args");
2360 args.push_back(app_args);
2361 }
2362
2363 String idepl = EDITOR_GET("export/ios/ios_deploy");
2364 if (idepl.is_empty()) {
2365 idepl = "ios-deploy";
2366 }
2367 String log;
2368 int ec;
2369 err = OS::get_singleton()->execute(idepl, args, &log, &ec, true);
2370 if (err != OK) {
2371 add_message(EXPORT_MESSAGE_WARNING, TTR("Run"), TTR("Could not start ios-deploy executable."));
2372 CLEANUP_AND_RETURN(err);
2373 }
2374 if (ec != 0) {
2375 print_line("ios-deploy:\n" + log);
2376 add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Installation/running failed, see editor log for details."));
2377 CLEANUP_AND_RETURN(ERR_UNCONFIGURED);
2378 }
2379 }
2380 }
2381
2382 CLEANUP_AND_RETURN(OK);
2383
2384#undef CLEANUP_AND_RETURN
2385#else
2386 return ERR_UNCONFIGURED;
2387#endif
2388}
2389
2390EditorExportPlatformIOS::EditorExportPlatformIOS() {
2391 if (EditorNode::get_singleton()) {
2392#ifdef MODULE_SVG_ENABLED
2393 Ref<Image> img = memnew(Image);
2394 const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
2395
2396 ImageLoaderSVG::create_image_from_string(img, _ios_logo_svg, EDSCALE, upsample, false);
2397 logo = ImageTexture::create_from_image(img);
2398
2399 ImageLoaderSVG::create_image_from_string(img, _ios_run_icon_svg, EDSCALE, upsample, false);
2400 run_icon = ImageTexture::create_from_image(img);
2401#endif
2402
2403 plugins_changed.set();
2404 devices_changed.set();
2405#ifdef MACOS_ENABLED
2406 check_for_changes_thread.start(_check_for_changes_poll_thread, this);
2407#endif
2408 }
2409}
2410
2411EditorExportPlatformIOS::~EditorExportPlatformIOS() {
2412#ifdef MACOS_ENABLED
2413 quit_request.set();
2414 if (check_for_changes_thread.is_started()) {
2415 check_for_changes_thread.wait_to_finish();
2416 }
2417#endif
2418}
2419