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 | |
51 | void 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 | |
62 | Vector<EditorExportPlatformIOS::ExportArchitecture> EditorExportPlatformIOS::_get_supported_architectures() const { |
63 | Vector<ExportArchitecture> archs; |
64 | archs.push_back(ExportArchitecture("arm64" , true)); |
65 | return archs; |
66 | } |
67 | |
68 | struct 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 | |
78 | static 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 | |
108 | struct 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 | |
116 | static 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 | |
131 | String 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 | |
149 | bool 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 | |
159 | void 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 | |
250 | void 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 | |
550 | String 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 | |
559 | String 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 | |
576 | String 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 | |
585 | void 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 | |
618 | Error 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 | |
707 | Error 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 | |
775 | Error 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 | |
863 | Error 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 | |
897 | struct 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 | |
907 | Error 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 | |
933 | struct PbxId { |
934 | private: |
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 | |
952 | public: |
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 | |
974 | struct ExportLibsData { |
975 | Vector<String> lib_paths; |
976 | String dest_dir; |
977 | }; |
978 | |
979 | void 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 | |
1067 | Error 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 | |
1213 | Error 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 | |
1229 | Error 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 | |
1262 | Vector<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 | |
1274 | Error 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 = "// 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 | |
1485 | Error 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 | |
1489 | Error 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 | |
1929 | bool 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 | |
1969 | bool 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 | |
2000 | int EditorExportPlatformIOS::get_options_count() const { |
2001 | MutexLock lock(device_lock); |
2002 | return devices.size(); |
2003 | } |
2004 | |
2005 | String EditorExportPlatformIOS::get_options_tooltip() const { |
2006 | return TTR("Select device from the list" ); |
2007 | } |
2008 | |
2009 | Ref<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 | |
2028 | String 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 | |
2034 | String 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 | |
2040 | bool 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 |
2064 | void 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 | |
2201 | Error 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 | |
2390 | EditorExportPlatformIOS::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 | |
2411 | EditorExportPlatformIOS::~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 | |