diff --git a/platform/macos/export/codesign.cpp b/editor/export/codesign.cpp similarity index 100% rename from platform/macos/export/codesign.cpp rename to editor/export/codesign.cpp diff --git a/platform/macos/export/codesign.h b/editor/export/codesign.h similarity index 99% rename from platform/macos/export/codesign.h rename to editor/export/codesign.h index 49d53b376e7..9a858c49ac1 100644 --- a/platform/macos/export/codesign.h +++ b/editor/export/codesign.h @@ -28,8 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#ifndef MACOS_CODESIGN_H -#define MACOS_CODESIGN_H +#ifndef CODESIGN_H +#define CODESIGN_H // macOS code signature creation utility. // @@ -364,4 +364,4 @@ public: #endif // MODULE_REGEX_ENABLED -#endif // MACOS_CODESIGN_H +#endif // CODESIGN_H diff --git a/platform/macos/export/lipo.cpp b/editor/export/lipo.cpp similarity index 69% rename from platform/macos/export/lipo.cpp rename to editor/export/lipo.cpp index 2d77e5960de..9038994b06e 100644 --- a/platform/macos/export/lipo.cpp +++ b/editor/export/lipo.cpp @@ -37,7 +37,7 @@ bool LipO::is_lipo(const String &p_path) { return (magic == 0xbebafeca || magic == 0xcafebabe || magic == 0xbfbafeca || magic == 0xcafebabf); } -bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_files) { +bool LipO::create_file(const String &p_output_path, const Vector &p_files) { close(); fa = FileAccess::open(p_output_path, FileAccess::WRITE); @@ -125,6 +125,100 @@ bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_f return true; } +bool LipO::create_file(const String &p_output_path, const Vector &p_files, const Vector &p_cputypes) { + close(); + + fa = FileAccess::open(p_output_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("LipO: Can't open file: \"%s\".", p_output_path)); + ERR_FAIL_COND_V(p_files.size() != p_cputypes.size(), false); + + uint64_t max_size = 0; + for (int i = 0; i < p_files.size(); i++) { + Ref fb = FileAccess::open(p_files[i], FileAccess::READ); + if (fb.is_null()) { + close(); + ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s\".", p_files[i])); + } + + { + FatArch arch; + MachO mh; + if (MachO::is_macho(p_files[i]) && mh.open_file(p_files[i])) { + arch.cputype = mh.get_cputype(); + arch.cpusubtype = mh.get_cpusubtype(); + arch.offset = 0; + arch.size = mh.get_size(); + arch.align = mh.get_align(); + ERR_FAIL_V_MSG(arch.cputype != (uint32_t)p_cputypes[i].x || arch.cpusubtype != (uint32_t)p_cputypes[i].y, vformat("Mismatching MachO architecture: \"%s\".", p_files[i])); + } else { + arch.cputype = (uint32_t)p_cputypes[i].x; + arch.cpusubtype = (uint32_t)p_cputypes[i].y; + arch.offset = 0; + arch.size = fb->get_length(); + arch.align = 0x03; + } + max_size += arch.size; + + archs.push_back(arch); + } + } + + // Write header. + bool is_64 = (max_size >= std::numeric_limits::max()); + if (is_64) { + fa->store_32(0xbfbafeca); + } else { + fa->store_32(0xbebafeca); + } + fa->store_32(BSWAP32(archs.size())); + uint64_t offset = archs.size() * (is_64 ? 32 : 20) + 8; + for (int i = 0; i < archs.size(); i++) { + archs.write[i].offset = offset + PAD(offset, uint64_t(1) << archs[i].align); + if (is_64) { + fa->store_32(BSWAP32(archs[i].cputype)); + fa->store_32(BSWAP32(archs[i].cpusubtype)); + fa->store_64(BSWAP64(archs[i].offset)); + fa->store_64(BSWAP64(archs[i].size)); + fa->store_32(BSWAP32(archs[i].align)); + fa->store_32(0); + } else { + fa->store_32(BSWAP32(archs[i].cputype)); + fa->store_32(BSWAP32(archs[i].cpusubtype)); + fa->store_32(BSWAP32(archs[i].offset)); + fa->store_32(BSWAP32(archs[i].size)); + fa->store_32(BSWAP32(archs[i].align)); + } + offset = archs[i].offset + archs[i].size; + } + + // Write files and padding. + for (int i = 0; i < archs.size(); i++) { + Ref fb = FileAccess::open(p_files[i], FileAccess::READ); + if (fb.is_null()) { + close(); + ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s\".", p_files[i])); + } + uint64_t cur = fa->get_position(); + for (uint64_t j = cur; j < archs[i].offset; j++) { + fa->store_8(0); + } + int pages = archs[i].size / 4096; + int remain = archs[i].size % 4096; + unsigned char step[4096]; + for (int j = 0; j < pages; j++) { + uint64_t br = fb->get_buffer(step, 4096); + if (br > 0) { + fa->store_buffer(step, br); + } + } + uint64_t br = fb->get_buffer(step, remain); + if (br > 0) { + fa->store_buffer(step, br); + } + } + return true; +} + bool LipO::open_file(const String &p_path) { close(); @@ -198,6 +292,18 @@ int LipO::get_arch_count() const { return archs.size(); } +uint32_t LipO::get_arch_cputype(int p_index) const { + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "LipO: File not opened."); + ERR_FAIL_INDEX_V(p_index, archs.size(), 0); + return archs[p_index].cputype; +} + +uint32_t LipO::get_arch_cpusubtype(int p_index) const { + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "LipO: File not opened."); + ERR_FAIL_INDEX_V(p_index, archs.size(), 0); + return archs[p_index].cpusubtype; +} + bool LipO::extract_arch(int p_index, const String &p_path) { ERR_FAIL_COND_V_MSG(fa.is_null(), false, "LipO: File not opened."); ERR_FAIL_INDEX_V(p_index, archs.size(), false); diff --git a/platform/macos/export/lipo.h b/editor/export/lipo.h similarity index 89% rename from platform/macos/export/lipo.h rename to editor/export/lipo.h index e375fc5a66c..0b65959bc53 100644 --- a/platform/macos/export/lipo.h +++ b/editor/export/lipo.h @@ -28,8 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#ifndef MACOS_LIPO_H -#define MACOS_LIPO_H +#ifndef LIPO_H +#define LIPO_H // Universal / Universal 2 fat binary file creator and extractor. @@ -57,10 +57,13 @@ class LipO : public RefCounted { public: static bool is_lipo(const String &p_path); - bool create_file(const String &p_output_path, const PackedStringArray &p_files); + bool create_file(const String &p_output_path, const Vector &p_files); + bool create_file(const String &p_output_path, const Vector &p_files, const Vector &p_cputypes); bool open_file(const String &p_path); int get_arch_count() const; + uint32_t get_arch_cputype(int p_index) const; + uint32_t get_arch_cpusubtype(int p_index) const; bool extract_arch(int p_index, const String &p_path); void close(); @@ -68,4 +71,4 @@ public: ~LipO(); }; -#endif // MACOS_LIPO_H +#endif // LIPO_H diff --git a/platform/macos/export/macho.cpp b/editor/export/macho.cpp similarity index 100% rename from platform/macos/export/macho.cpp rename to editor/export/macho.cpp diff --git a/platform/macos/export/macho.h b/editor/export/macho.h similarity index 94% rename from platform/macos/export/macho.h rename to editor/export/macho.h index a84de7de60b..e7393a2feed 100644 --- a/platform/macos/export/macho.h +++ b/editor/export/macho.h @@ -28,8 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#ifndef MACOS_MACHO_H -#define MACOS_MACHO_H +#ifndef MACHO_H +#define MACHO_H // Mach-O binary object file format parser and editor. @@ -39,6 +39,7 @@ #include "core/object/ref_counted.h" class MachO : public RefCounted { +public: struct MachHeader { uint32_t cputype; uint32_t cpusubtype; @@ -98,6 +99,21 @@ class MachO : public RefCounted { LC_LINKER_OPTIMIZATION_HINT = 0x0000002e, LC_VERSION_MIN_TVOS = 0x0000002f, LC_VERSION_MIN_WATCHOS = 0x00000030, + LC_BUILD_VERSION = 0x00000032, + }; + + enum PlatformID { + PLATFORM_UNKNOWN = 0, + PLATFORM_MACOS = 1, + PLATFORM_IOS = 2, + PLATFORM_TVOS = 3, + PLATFORM_WATCHOS = 4, + PLATFORM_BRIDGEOS = 5, + PLATFORM_MACCATALYST = 6, + PLATFORM_IOSSIMULATOR = 7, + PLATFORM_TVOSSIMULATOR = 8, + PLATFORM_WATCHOSSIMULATOR = 9, + PLATFORM_DRIVERKIT = 10, }; struct LoadCommandHeader { @@ -158,6 +174,7 @@ class MachO : public RefCounted { uint32_t reserved3; }; +private: Ref fa; bool swap = false; @@ -208,4 +225,4 @@ public: bool set_signature_size(uint64_t p_size); }; -#endif // MACOS_MACHO_H +#endif // MACHO_H diff --git a/platform/ios/doc_classes/EditorExportPlatformIOS.xml b/platform/ios/doc_classes/EditorExportPlatformIOS.xml index 20c16478430..87994ef22bf 100644 --- a/platform/ios/doc_classes/EditorExportPlatformIOS.xml +++ b/platform/ios/doc_classes/EditorExportPlatformIOS.xml @@ -41,6 +41,9 @@ If [code]true[/code], exports iOS project files without building an XCArchive or [code].ipa[/code] file. If [code]false[/code], exports iOS project files and builds an XCArchive and [code].ipa[/code] file at the same time. When combining Godot with Fastlane or other build pipelines, you may want to set this to [code]true[/code]. + + If [code]true[/code], and ARM64 simulator library is missing from the export template, it is automatically generated from ARM64 device library. + Interpolation method used to resize application icon. diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp index 2194804b3b8..490f73a36da 100644 --- a/platform/ios/export/export_plugin.cpp +++ b/platform/ios/export/export_plugin.cpp @@ -40,6 +40,8 @@ #include "editor/editor_paths.h" #include "editor/editor_string_names.h" #include "editor/export/editor_export.h" +#include "editor/export/lipo.h" +#include "editor/export/macho.h" #include "editor/import/resource_importer_texture_settings.h" #include "editor/plugins/script_editor_plugin.h" #include "editor/themes/editor_scale.h" @@ -248,7 +250,7 @@ bool EditorExportPlatformIOS::get_export_option_visibility(const EditorExportPre } bool advanced_options_enabled = p_preset->are_advanced_options_enabled(); - if (p_option.begins_with("privacy")) { + if (p_option.begins_with("privacy") || p_option == "application/generate_simulator_library_if_missing") { return advanced_options_enabled; } @@ -288,6 +290,7 @@ void EditorExportPlatformIOS::get_export_options(List *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/export_project_only"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/delete_old_export_files_unconditionally"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/generate_simulator_library_if_missing"), true)); Vector found_plugins = get_plugins(); for (int i = 0; i < found_plugins.size(); i++) { @@ -1150,6 +1153,167 @@ struct ExportLibsData { String dest_dir; }; +bool EditorExportPlatformIOS::_archive_has_arm64(const String &p_path, uint32_t *r_cputype, uint32_t *r_cpusubtype) const { + bool has_arm64_image = false; + if (FileAccess::exists(p_path)) { + if (LipO::is_lipo(p_path)) { + // LipO. + Ref lipo; + lipo.instantiate(); + if (lipo->open_file(p_path)) { + for (int i = 0; i < lipo->get_arch_count(); i++) { + if (lipo->get_arch_cputype(i) == 0x100000c && lipo->get_arch_cpusubtype(i) == 0) { + has_arm64_image = true; + break; + } + } + } + lipo->close(); + } else { + // Single architecture archive. + Ref sim_f = FileAccess::open(p_path, FileAccess::READ); + if (sim_f.is_valid()) { + char magic[9] = {}; + sim_f->get_buffer((uint8_t *)&magic[0], 8); + if (String(magic) == String("!\n")) { + while (!sim_f->eof_reached()) { + // Read file metadata. + char name_short[17] = {}; + char size_short[11] = {}; + sim_f->get_buffer((uint8_t *)&name_short[0], 16); + sim_f->seek(sim_f->get_position() + 12 + 6 + 6 + 8); // Skip modification time, owner ID, group ID, file mode. + sim_f->get_buffer((uint8_t *)&size_short[0], 10); + sim_f->seek(sim_f->get_position() + 2); // Skip end marker. + + int64_t file_size = String(size_short).to_int(); + int64_t next_off = sim_f->get_position() + file_size; + + String name = String(name_short); // Skip extended name. + if (name.is_empty() || file_size == 0) { + break; + } + if (name.begins_with("#1/")) { + int64_t name_len = String(name_short).replace("#1/", "").to_int(); + sim_f->seek(sim_f->get_position() + name_len); + } + + // Read file content. + uint32_t obj_magic = sim_f->get_32(); + + bool swap = (obj_magic == 0xcffaedfe || obj_magic == 0xcefaedfe); + if (obj_magic == 0xcefaedfe || obj_magic == 0xfeedface || obj_magic == 0xcffaedfe || obj_magic == 0xfeedfacf) { + uint32_t cputype = sim_f->get_32(); + uint32_t cpusubtype = sim_f->get_32(); + if (swap) { + cputype = BSWAP32(cputype); + cpusubtype = BSWAP32(cpusubtype); + } + if (r_cputype) { + *r_cputype = cputype; + } + if (r_cpusubtype) { + *r_cpusubtype = cpusubtype; + } + if (cputype == 0x100000c && cpusubtype == 0) { + has_arm64_image = true; + } + break; + } + sim_f->seek(next_off); + } + } + sim_f->close(); + } + } + } + return has_arm64_image; +} + +int EditorExportPlatformIOS::_archive_convert_to_simulator(const String &p_path) const { + int commands_patched = 0; + Ref sim_f = FileAccess::open(p_path, FileAccess::READ_WRITE); + if (sim_f.is_valid()) { + char magic[9] = {}; + sim_f->get_buffer((uint8_t *)&magic[0], 8); + if (String(magic) == String("!\n")) { + while (!sim_f->eof_reached()) { + // Read file metadata. + char name_short[17] = {}; + char size_short[11] = {}; + sim_f->get_buffer((uint8_t *)&name_short[0], 16); + sim_f->seek(sim_f->get_position() + 12 + 6 + 6 + 8); // Skip modification time, owner ID, group ID, file mode. + sim_f->get_buffer((uint8_t *)&size_short[0], 10); + sim_f->seek(sim_f->get_position() + 2); // Skip end marker. + + int64_t file_size = String(size_short).to_int(); + int64_t next_off = sim_f->get_position() + file_size; + + String name = String(name_short); // Skip extended name. + if (name.is_empty() || file_size == 0) { + break; + } + if (name.begins_with("#1/")) { + int64_t name_len = String(name_short).replace("#1/", "").to_int(); + sim_f->seek(sim_f->get_position() + name_len); + } + + // Read file content. + uint32_t obj_magic = sim_f->get_32(); + + bool swap = (obj_magic == 0xcffaedfe || obj_magic == 0xcefaedfe); + if (obj_magic == 0xcefaedfe || obj_magic == 0xfeedface || obj_magic == 0xcffaedfe || obj_magic == 0xfeedfacf) { + uint32_t cputype = sim_f->get_32(); + uint32_t cpusubtype = sim_f->get_32(); + uint32_t filetype = sim_f->get_32(); + uint32_t ncmds = sim_f->get_32(); + sim_f->get_32(); // Commands total size. + sim_f->get_32(); // Commands flags. + if (obj_magic == 0xcffaedfe || obj_magic == 0xfeedfacf) { + sim_f->get_32(); // Reserved, 64-bit only. + } + if (swap) { + ncmds = BSWAP32(ncmds); + cputype = BSWAP32(cputype); + cpusubtype = BSWAP32(cpusubtype); + filetype = BSWAP32(filetype); + } + if (cputype == 0x100000C && cpusubtype == 0 && filetype == 1) { + // ARM64, object file. + for (uint32_t i = 0; i < ncmds; i++) { + int64_t cmdofs = sim_f->get_position(); + uint32_t cmdid = sim_f->get_32(); + uint32_t cmdsize = sim_f->get_32(); + if (swap) { + cmdid = BSWAP32(cmdid); + cmdsize = BSWAP32(cmdsize); + } + if (cmdid == MachO::LoadCommandID::LC_BUILD_VERSION) { + int64_t platform = sim_f->get_32(); + if (swap) { + platform = BSWAP32(platform); + } + if (platform == MachO::PlatformID::PLATFORM_IOS) { + sim_f->seek(cmdofs + 4 + 4); + uint32_t new_id = MachO::PlatformID::PLATFORM_IOSSIMULATOR; + if (swap) { + new_id = BSWAP32(new_id); + } + sim_f->store_32(new_id); + commands_patched++; + } + } + sim_f->seek(cmdofs + cmdsize); + } + } + } + sim_f->seek(next_off); + } + } + sim_f->close(); + } + return commands_patched; +} + void EditorExportPlatformIOS::_check_xcframework_content(const String &p_path, int &r_total_libs, int &r_static_libs, int &r_dylibs, int &r_frameworks) const { Ref plist; plist.instantiate(); @@ -2146,7 +2310,7 @@ Error EditorExportPlatformIOS::_export_project_helper(const Refget("application/generate_simulator_library_if_missing").operator bool()) { + String sim_lib_path = dest_dir + String(binary_name + ".xcframework").path_join("ios-arm64_x86_64-simulator").path_join("libgodot.a"); + String dev_lib_path = dest_dir + String(binary_name + ".xcframework").path_join("ios-arm64").path_join("libgodot.a"); + String tmp_lib_path = EditorPaths::get_singleton()->get_cache_dir().path_join(binary_name + "_lipo_"); + uint32_t cputype = 0; + uint32_t cpusubtype = 0; + if (!_archive_has_arm64(sim_lib_path, &cputype, &cpusubtype) && _archive_has_arm64(dev_lib_path) && FileAccess::exists(dev_lib_path)) { + add_message(EXPORT_MESSAGE_INFO, TTR("Export"), TTR("ARM64 simulator library, generating from device library.")); + + Vector tmp_lib_files; + Vector tmp_lib_cputypes; + // Extract/copy simulator lib. + if (FileAccess::exists(sim_lib_path)) { + if (LipO::is_lipo(sim_lib_path)) { + Ref lipo; + lipo.instantiate(); + if (lipo->open_file(sim_lib_path)) { + for (int i = 0; i < lipo->get_arch_count(); i++) { + const String &f_name = tmp_lib_path + itos(tmp_lib_files.size()); + lipo->extract_arch(i, f_name); + tmp_lib_files.push_back(f_name); + tmp_lib_cputypes.push_back(Vector2i(lipo->get_arch_cputype(i), lipo->get_arch_cpusubtype(i))); + } + } + } else { + const String &f_name = tmp_lib_path + itos(tmp_lib_files.size()); + tmp_app_path->copy(sim_lib_path, f_name); + tmp_lib_files.push_back(f_name); + tmp_lib_cputypes.push_back(Vector2i(cputype, cpusubtype)); + } + } + // Copy device lib. + if (LipO::is_lipo(dev_lib_path)) { + Ref lipo; + lipo.instantiate(); + if (lipo->open_file(dev_lib_path)) { + for (int i = 0; i < lipo->get_arch_count(); i++) { + if (lipo->get_arch_cputype(i) == 0x100000c && lipo->get_arch_cpusubtype(i) == 0) { + const String &f_name = tmp_lib_path + itos(tmp_lib_files.size()); + lipo->extract_arch(i, f_name); + tmp_lib_files.push_back(f_name); + tmp_lib_cputypes.push_back(Vector2i(0x100000c, 0)); // ARM64. + break; + } + } + } + } else { + const String &f_name = tmp_lib_path + itos(tmp_lib_files.size()); + tmp_app_path->copy(dev_lib_path, f_name); + tmp_lib_files.push_back(f_name); + tmp_lib_cputypes.push_back(Vector2i(0x100000c, 0)); // ARM64. + } + + // Patch device lib. + int patch_count = _archive_convert_to_simulator(tmp_lib_path + itos(tmp_lib_files.size() - 1)); + if (patch_count == 0) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Export"), TTR("Unable to generate ARM64 simulator library.")); + } else { + // Repack. + Ref lipo; + lipo.instantiate(); + lipo->create_file(sim_lib_path, tmp_lib_files, tmp_lib_cputypes); + } + + // Cleanup. + for (const String &E : tmp_lib_files) { + tmp_app_path->remove(E); + } + } + } + + // Generate translations files. + Dictionary appnames = GLOBAL_GET("application/config/name_localized"); Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized"); Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized"); diff --git a/platform/ios/export/export_plugin.h b/platform/ios/export/export_plugin.h index 197f27da763..1964906c276 100644 --- a/platform/ios/export/export_plugin.h +++ b/platform/ios/export/export_plugin.h @@ -135,6 +135,8 @@ class EditorExportPlatformIOS : public EditorExportPlatform { Vector _get_supported_architectures() const; Vector _get_preset_architectures(const Ref &p_preset) const; + bool _archive_has_arm64(const String &p_path, uint32_t *r_cputype = nullptr, uint32_t *r_cpusubtype = nullptr) const; + int _archive_convert_to_simulator(const String &p_path) const; void _check_xcframework_content(const String &p_path, int &r_total_libs, int &r_static_libs, int &r_dylibs, int &r_frameworks) const; Error _convert_to_framework(const String &p_source, const String &p_destination, const String &p_id) const; diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp index 32278ba4a1b..73e2f2d45b2 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -30,10 +30,7 @@ #include "export_plugin.h" -#include "codesign.h" -#include "lipo.h" #include "logo_svg.gen.h" -#include "macho.h" #include "run_icon_svg.gen.h" #include "core/io/image_loader.h" @@ -43,6 +40,9 @@ #include "editor/editor_node.h" #include "editor/editor_paths.h" #include "editor/editor_string_names.h" +#include "editor/export/codesign.h" +#include "editor/export/lipo.h" +#include "editor/export/macho.h" #include "editor/import/resource_importer_texture_settings.h" #include "editor/themes/editor_scale.h" #include "scene/resources/image_texture.h"