Path: blob/master/platform/macos/export/export_plugin.cpp
11353 views
/**************************************************************************/1/* export_plugin.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "export_plugin.h"3132#include "logo_svg.gen.h"33#include "run_icon_svg.gen.h"3435#include "core/io/image_loader.h"36#include "core/io/plist.h"37#include "core/string/translation.h"38#include "drivers/png/png_driver_common.h"39#include "editor/editor_node.h"40#include "editor/editor_string_names.h"41#include "editor/export/codesign.h"42#include "editor/export/lipo.h"43#include "editor/export/macho.h"44#include "editor/file_system/editor_paths.h"45#include "editor/import/resource_importer_texture_settings.h"46#include "editor/themes/editor_scale.h"47#include "scene/resources/image_texture.h"4849#include "modules/svg/image_loader_svg.h"5051void EditorExportPlatformMacOS::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {52r_features->push_back(p_preset->get("binary_format/architecture"));53String architecture = p_preset->get("binary_format/architecture");5455if (architecture == "universal" || architecture == "x86_64") {56r_features->push_back("s3tc");57r_features->push_back("bptc");58} else if (architecture == "arm64") {59r_features->push_back("etc2");60r_features->push_back("astc");61} else {62ERR_PRINT("Invalid architecture");63}6465if (p_preset->get("shader_baker/enabled")) {66r_features->push_back("shader_baker");67}6869if (architecture == "universal") {70r_features->push_back("x86_64");71r_features->push_back("arm64");72}73}7475String EditorExportPlatformMacOS::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const {76if (p_preset) {77int dist_type = p_preset->get("export/distribution_type");78bool ad_hoc = false;79int codesign_tool = p_preset->get("codesign/codesign");80int notary_tool = p_preset->get("notarization/notarization");81switch (codesign_tool) {82case 1: { // built-in ad-hoc83ad_hoc = true;84} break;85case 2: { // "rcodesign"86ad_hoc = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty() || p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty();87} break;88#ifdef MACOS_ENABLED89case 3: { // "codesign"90ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");91} break;92#endif93default: {94};95}9697if (p_name == "application/bundle_identifier") {98String identifier = p_preset->get("application/bundle_identifier");99String pn_err;100if (!is_package_name_valid(identifier, &pn_err)) {101return TTR("Invalid bundle identifier:") + " " + pn_err;102}103}104105if (p_name == "shader_baker/enabled" && bool(p_preset->get("shader_baker/enabled"))) {106String export_renderer = GLOBAL_GET("rendering/renderer/rendering_method");107if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") {108return TTR("\"Shader Baker\" is not supported when using the Compatibility renderer.");109} else if (OS::get_singleton()->get_current_rendering_method() != export_renderer) {110return vformat(TTR("The editor is currently using a different renderer than what the target platform will use. \"Shader Baker\" won't be able to include core shaders. Switch to the \"%s\" renderer temporarily to fix this."), export_renderer);111}112}113114if (p_name == "codesign/certificate_file" || p_name == "codesign/certificate_password" || p_name == "codesign/identity") {115if (dist_type == 2) {116if (ad_hoc) {117return TTR("App Store distribution with ad-hoc code signing is not supported.");118}119} else if (notary_tool > 0 && ad_hoc) {120return TTR("Notarization with an ad-hoc signature is not supported.");121}122}123124if (p_name == "codesign/apple_team_id") {125String team_id = p_preset->get("codesign/apple_team_id");126if (team_id.is_empty()) {127if (dist_type == 2) {128return TTR("Apple Team ID is required for App Store distribution.");129} else if (notary_tool > 0) {130return TTR("Apple Team ID is required for notarization.");131}132}133}134135if (p_name == "codesign/provisioning_profile" && dist_type == 2) {136String pprof = p_preset->get_or_env("codesign/provisioning_profile", ENV_MAC_CODESIGN_PROFILE);137if (pprof.is_empty()) {138return TTR("Provisioning profile is required for App Store distribution.");139}140}141142if (p_name == "codesign/installer_identity" && dist_type == 2) {143String ident = p_preset->get("codesign/installer_identity");144if (ident.is_empty()) {145return TTR("Installer signing identity is required for App Store distribution.");146}147}148149if (p_name == "codesign/entitlements/app_sandbox/enabled" && dist_type == 2) {150bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled");151if (!sandbox) {152return TTR("App sandbox is required for App Store distribution.");153}154}155156if (p_name == "codesign/codesign") {157if (dist_type == 2) {158if (codesign_tool == 2 && ClassDB::class_exists("CSharpScript")) {159return TTR("'rcodesign' doesn't support signing applications with embedded dynamic libraries (GDExtension or .NET).");160}161if (codesign_tool == 0) {162return TTR("Code signing is required for App Store distribution.");163}164if (codesign_tool == 1) {165return TTR("App Store distribution with ad-hoc code signing is not supported.");166}167} else if (notary_tool > 0) {168if (codesign_tool == 0) {169return TTR("Code signing is required for notarization.");170}171if (codesign_tool == 1) {172return TTR("Notarization with an ad-hoc signature is not supported.");173}174}175}176177if (notary_tool == 2 || notary_tool == 3) {178if (p_name == "notarization/apple_id_name" || p_name == "notarization/api_uuid") {179String apple_id = p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID);180String api_uuid = p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID);181if (apple_id.is_empty() && api_uuid.is_empty()) {182return TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified.");183}184if (!apple_id.is_empty() && !api_uuid.is_empty()) {185return TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time.");186}187}188if (p_name == "notarization/apple_id_password") {189String apple_id = p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID);190String apple_pass = p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS);191if (!apple_id.is_empty() && apple_pass.is_empty()) {192return TTR("Apple ID password not specified.");193}194}195if (p_name == "notarization/api_key_id") {196String api_uuid = p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID);197String api_key = p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID);198if (!api_uuid.is_empty() && api_key.is_empty()) {199return TTR("App Store Connect API key ID not specified.");200}201}202} else if (notary_tool == 1) {203if (p_name == "notarization/api_uuid") {204String api_uuid = p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID);205if (api_uuid.is_empty()) {206return TTR("App Store Connect issuer ID name not specified.");207}208}209if (p_name == "notarization/api_key_id") {210String api_key = p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID);211if (api_key.is_empty()) {212return TTR("App Store Connect API key ID not specified.");213}214}215}216217if (codesign_tool > 0) {218if (p_name == "privacy/microphone_usage_description") {219String discr = p_preset->get("privacy/microphone_usage_description");220bool enabled = p_preset->get("codesign/entitlements/audio_input");221if (enabled && discr.is_empty()) {222return TTR("Microphone access is enabled, but usage description is not specified.");223}224}225if (p_name == "privacy/camera_usage_description") {226String discr = p_preset->get("privacy/camera_usage_description");227bool enabled = p_preset->get("codesign/entitlements/camera");228if (enabled && discr.is_empty()) {229return TTR("Camera access is enabled, but usage description is not specified.");230}231}232if (p_name == "privacy/location_usage_description") {233String discr = p_preset->get("privacy/location_usage_description");234bool enabled = p_preset->get("codesign/entitlements/location");235if (enabled && discr.is_empty()) {236return TTR("Location information access is enabled, but usage description is not specified.");237}238}239if (p_name == "privacy/address_book_usage_description") {240String discr = p_preset->get("privacy/address_book_usage_description");241bool enabled = p_preset->get("codesign/entitlements/address_book");242if (enabled && discr.is_empty()) {243return TTR("Address book access is enabled, but usage description is not specified.");244}245}246if (p_name == "privacy/calendar_usage_description") {247String discr = p_preset->get("privacy/calendar_usage_description");248bool enabled = p_preset->get("codesign/entitlements/calendars");249if (enabled && discr.is_empty()) {250return TTR("Calendar access is enabled, but usage description is not specified.");251}252}253if (p_name == "privacy/photos_library_usage_description") {254String discr = p_preset->get("privacy/photos_library_usage_description");255bool enabled = p_preset->get("codesign/entitlements/photos_library");256if (enabled && discr.is_empty()) {257return TTR("Photo library access is enabled, but usage description is not specified.");258}259}260}261}262return String();263}264265bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {266// Hide irrelevant code signing options.267if (p_preset) {268int codesign_tool = p_preset->get("codesign/codesign");269switch (codesign_tool) {270case 1: { // built-in ad-hoc271if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options" || p_option == "codesign/team_id") {272return false;273}274} break;275case 2: { // "rcodesign"276if (p_option == "codesign/identity") {277return false;278}279} break;280#ifdef MACOS_ENABLED281case 3: { // "codesign"282if (p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password") {283return false;284}285} break;286#endif287default: { // disabled288if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options" || p_option.begins_with("codesign/entitlements") || p_option == "codesign/team_id") {289return false;290}291} break;292}293294// Distribution type.295int dist_type = p_preset->get("export/distribution_type");296if (dist_type != 2 && p_option == "codesign/installer_identity") {297return false;298}299300if (dist_type == 2 && p_option.begins_with("notarization/")) {301return false;302}303304if (dist_type != 2 && p_option == "codesign/provisioning_profile") {305return false;306}307308#ifndef MACOS_ENABLED309if (p_option == "application/liquid_glass_icon") {310return false;311}312#endif313314String custom_prof = p_preset->get("codesign/entitlements/custom_file");315if (!custom_prof.is_empty() && p_option != "codesign/entitlements/custom_file" && p_option.begins_with("codesign/entitlements/")) {316return false;317}318319// Hide sandbox entitlements.320bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled");321if (!sandbox && p_option != "codesign/entitlements/app_sandbox/enabled" && p_option.begins_with("codesign/entitlements/app_sandbox/")) {322return false;323}324325// Hide SSH options.326bool ssh = p_preset->get("ssh_remote_deploy/enabled");327if (!ssh && p_option != "ssh_remote_deploy/enabled" && p_option.begins_with("ssh_remote_deploy/")) {328return false;329}330331// Hide irrelevant notarization options.332int notary_tool = p_preset->get("notarization/notarization");333switch (notary_tool) {334case 1: { // "rcodesign"335if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password") {336return false;337}338} break;339case 2: { // "notarytool"340// All options are visible.341} break;342default: { // disabled343if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password" || p_option == "notarization/api_uuid" || p_option == "notarization/api_key" || p_option == "notarization/api_key_id") {344return false;345}346} break;347}348349bool advanced_options_enabled = p_preset->are_advanced_options_enabled();350if (p_option.begins_with("privacy") ||351p_option == "codesign/entitlements/additional" ||352p_option == "custom_template/debug" ||353p_option == "custom_template/release" ||354p_option == "application/additional_plist_content" ||355p_option == "application/export_angle" ||356p_option == "application/icon_interpolation" ||357p_option == "application/signature" ||358p_option == "display/high_res" ||359p_option == "xcode/platform_build" ||360p_option == "xcode/sdk_build" ||361p_option == "xcode/sdk_name" ||362p_option == "xcode/sdk_version" ||363p_option == "xcode/xcode_build" ||364p_option == "xcode/xcode_version") {365return advanced_options_enabled;366}367}368369// These entitlements are required to run managed code, and are always enabled in Mono builds.370if (ClassDB::class_exists("CSharpScript")) {371if (p_option == "codesign/entitlements/allow_jit_code_execution" || p_option == "codesign/entitlements/allow_unsigned_executable_memory" || p_option == "codesign/entitlements/allow_dyld_environment_variables") {372return false;373}374}375376// Hide unsupported .NET embedding option.377if (p_option == "dotnet/embed_build_outputs") {378return false;379}380381return true;382}383384List<String> EditorExportPlatformMacOS::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {385List<String> list;386387if (p_preset.is_valid()) {388int dist_type = p_preset->get("export/distribution_type");389if (dist_type == 0) {390#ifdef MACOS_ENABLED391list.push_back("dmg");392#endif393list.push_back("zip");394list.push_back("app");395} else if (dist_type == 1) {396#ifdef MACOS_ENABLED397list.push_back("dmg");398#endif399list.push_back("zip");400list.push_back("app");401} else if (dist_type == 2) {402#ifdef MACOS_ENABLED403list.push_back("pkg");404#endif405}406}407408return list;409}410411struct DataCollectionInfo {412String prop_name;413String type_name;414};415416static const DataCollectionInfo data_collect_type_info[] = {417{ "name", "NSPrivacyCollectedDataTypeName" },418{ "email_address", "NSPrivacyCollectedDataTypeEmailAddress" },419{ "phone_number", "NSPrivacyCollectedDataTypePhoneNumber" },420{ "physical_address", "NSPrivacyCollectedDataTypePhysicalAddress" },421{ "other_contact_info", "NSPrivacyCollectedDataTypeOtherUserContactInfo" },422{ "health", "NSPrivacyCollectedDataTypeHealth" },423{ "fitness", "NSPrivacyCollectedDataTypeFitness" },424{ "payment_info", "NSPrivacyCollectedDataTypePaymentInfo" },425{ "credit_info", "NSPrivacyCollectedDataTypeCreditInfo" },426{ "other_financial_info", "NSPrivacyCollectedDataTypeOtherFinancialInfo" },427{ "precise_location", "NSPrivacyCollectedDataTypePreciseLocation" },428{ "coarse_location", "NSPrivacyCollectedDataTypeCoarseLocation" },429{ "sensitive_info", "NSPrivacyCollectedDataTypeSensitiveInfo" },430{ "contacts", "NSPrivacyCollectedDataTypeContacts" },431{ "emails_or_text_messages", "NSPrivacyCollectedDataTypeEmailsOrTextMessages" },432{ "photos_or_videos", "NSPrivacyCollectedDataTypePhotosorVideos" },433{ "audio_data", "NSPrivacyCollectedDataTypeAudioData" },434{ "gameplay_content", "NSPrivacyCollectedDataTypeGameplayContent" },435{ "customer_support", "NSPrivacyCollectedDataTypeCustomerSupport" },436{ "other_user_content", "NSPrivacyCollectedDataTypeOtherUserContent" },437{ "browsing_history", "NSPrivacyCollectedDataTypeBrowsingHistory" },438{ "search_hhistory", "NSPrivacyCollectedDataTypeSearchHistory" },439{ "user_id", "NSPrivacyCollectedDataTypeUserID" },440{ "device_id", "NSPrivacyCollectedDataTypeDeviceID" },441{ "purchase_history", "NSPrivacyCollectedDataTypePurchaseHistory" },442{ "product_interaction", "NSPrivacyCollectedDataTypeProductInteraction" },443{ "advertising_data", "NSPrivacyCollectedDataTypeAdvertisingData" },444{ "other_usage_data", "NSPrivacyCollectedDataTypeOtherUsageData" },445{ "crash_data", "NSPrivacyCollectedDataTypeCrashData" },446{ "performance_data", "NSPrivacyCollectedDataTypePerformanceData" },447{ "other_diagnostic_data", "NSPrivacyCollectedDataTypeOtherDiagnosticData" },448{ "environment_scanning", "NSPrivacyCollectedDataTypeEnvironmentScanning" },449{ "hands", "NSPrivacyCollectedDataTypeHands" },450{ "head", "NSPrivacyCollectedDataTypeHead" },451{ "other_data_types", "NSPrivacyCollectedDataTypeOtherDataTypes" },452};453454static const DataCollectionInfo data_collect_purpose_info[] = {455{ "Analytics", "NSPrivacyCollectedDataTypePurposeAnalytics" },456{ "App Functionality", "NSPrivacyCollectedDataTypePurposeAppFunctionality" },457{ "Developer Advertising", "NSPrivacyCollectedDataTypePurposeDeveloperAdvertising" },458{ "Third-party Advertising", "NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising" },459{ "Product Personalization", "NSPrivacyCollectedDataTypePurposeProductPersonalization" },460{ "Other", "NSPrivacyCollectedDataTypePurposeOther" },461};462463void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options) const {464#ifdef MACOS_ENABLED465r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "export/distribution_type", PROPERTY_HINT_ENUM, "Testing,Distribution,App Store"), 1, true));466#else467r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "export/distribution_type", PROPERTY_HINT_ENUM, "Testing,Distribution"), 1, true));468#endif469470r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "binary_format/architecture", PROPERTY_HINT_ENUM, "universal,x86_64,arm64", PROPERTY_USAGE_STORAGE), "universal"));471r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));472r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));473474r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "debug/export_console_wrapper", PROPERTY_HINT_ENUM, "No,Debug Only,Debug and Release"), 1));475r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/liquid_glass_icon", PROPERTY_HINT_FILE, "*.icon"), ""));476r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/icon", PROPERTY_HINT_FILE, "*.icns,*.png,*.webp,*.svg"), ""));477r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/icon_interpolation", PROPERTY_HINT_ENUM, "Nearest neighbor,Bilinear,Cubic,Trilinear,Lanczos"), 4));478r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), "", false, true));479r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/signature"), ""));480r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/app_category", PROPERTY_HINT_ENUM, "Business,Developer-tools,Education,Entertainment,Finance,Games,Action-games,Adventure-games,Arcade-games,Board-games,Card-games,Casino-games,Dice-games,Educational-games,Family-games,Kids-games,Music-games,Puzzle-games,Racing-games,Role-playing-games,Simulation-games,Sports-games,Strategy-games,Trivia-games,Word-games,Graphics-design,Healthcare-fitness,Lifestyle,Medical,Music,News,Photography,Productivity,Reference,Social-networking,Sports,Travel,Utilities,Video,Weather"), "Games"));481r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), ""));482r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), ""));483r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), ""));484r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "application/copyright_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));485r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/min_macos_version_x86_64"), "10.12"));486r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/min_macos_version_arm64"), "11.00"));487r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/export_angle", PROPERTY_HINT_ENUM, "Auto,Yes,No"), 0, true));488r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "display/high_res"), true));489490r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "shader_baker/enabled"), false));491492r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/additional_plist_content", PROPERTY_HINT_MULTILINE_TEXT), ""));493494r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/platform_build"), "14C18"));495// TODO(sgc): Need to set appropriate version when using Metal496r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_version"), "13.1"));497r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_build"), "22C55"));498r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_name"), "macosx13.1"));499r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/xcode_version"), "1420"));500r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/xcode_build"), "14C18"));501502#ifdef MACOS_ENABLED503r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/codesign", PROPERTY_HINT_ENUM, "Disabled,Built-in (ad-hoc only),rcodesign,Xcode codesign"), 3, true));504#else505r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/codesign", PROPERTY_HINT_ENUM, "Disabled,Built-in (ad-hoc only),rcodesign"), 1, true, true));506#endif507r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/installer_identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "3rd Party Mac Developer Installer: (ID)"), "", false, true));508r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/apple_team_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "ID"), "", false, true));509// "codesign" only options:510r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "Type: Name (ID)"), ""));511// "rcodesign" only options:512r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_file", PROPERTY_HINT_GLOBAL_FILE, "*.pfx,*.p12", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));513r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));514// "codesign" and "rcodesign" only options:515r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/provisioning_profile", PROPERTY_HINT_GLOBAL_FILE, "*.provisionprofile", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));516517r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/custom_file", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), "", true));518r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_jit_code_execution"), false));519r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_unsigned_executable_memory"), false));520r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_dyld_environment_variables"), false));521r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/disable_library_validation"), false));522r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/audio_input"), false));523r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/camera"), false));524r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/location"), false));525r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/address_book"), false));526r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/calendars"), false));527r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/photos_library"), false));528r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/apple_events"), false));529r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/debugging"), false));530r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/enabled"), false, true, true));531r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/network_server"), false));532r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/network_client"), false));533r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/device_usb"), false));534r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/device_bluetooth"), false));535r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_downloads", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));536r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_pictures", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));537r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_music", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));538r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));539r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_user_selected", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));540r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array()));541r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/additional", PROPERTY_HINT_MULTILINE_TEXT), ""));542r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray()));543544#ifdef MACOS_ENABLED545r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "notarization/notarization", PROPERTY_HINT_ENUM, "Disabled,rcodesign,Xcode notarytool"), 0, true));546#else547r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "notarization/notarization", PROPERTY_HINT_ENUM, "Disabled,rcodesign"), 0, true));548#endif549// "notarytool" only options:550r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Apple ID email", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));551r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_password", PROPERTY_HINT_PASSWORD, "Enable two-factor authentication and provide app-specific password", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));552// "notarytool" and "rcodesign" only options:553r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_uuid", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect issuer ID UUID", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));554r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_key", PROPERTY_HINT_GLOBAL_FILE, "*.p8", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));555r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_key_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect API key ID", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true));556557r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), "", false, true));558r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/microphone_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));559r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), "", false, true));560r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/camera_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));561r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/location_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the location information"), "", false, true));562r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/location_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));563r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/address_book_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the address book"), "", false, true));564r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/address_book_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));565r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/calendar_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the calendar"), "", false, true));566r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/calendar_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));567r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photos_library_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the photo library"), "", false, true));568r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photos_library_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));569r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/desktop_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Desktop folder"), "", false, true));570r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/desktop_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));571r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/documents_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Documents folder"), "", false, true));572r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/documents_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));573r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/downloads_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Downloads folder"), "", false, true));574r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/downloads_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));575r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/network_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use network volumes"), "", false, true));576r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/network_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));577r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/removable_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use removable volumes"), "", false, true));578r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/removable_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));579580r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "privacy/tracking_enabled"), false));581r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "privacy/tracking_domains"), Vector<String>()));582583{584String hint;585for (uint64_t i = 0; i < std_size(data_collect_purpose_info); ++i) {586if (i != 0) {587hint += ",";588}589hint += vformat("%s:%d", data_collect_purpose_info[i].prop_name, (1 << i));590}591for (uint64_t i = 0; i < std_size(data_collect_type_info); ++i) {592r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/collected", data_collect_type_info[i].prop_name)), false));593r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/linked_to_user", data_collect_type_info[i].prop_name)), false));594r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/used_for_tracking", data_collect_type_info[i].prop_name)), false));595r_options->push_back(ExportOption(PropertyInfo(Variant::INT, vformat("privacy/collected_data/%s/collection_purposes", data_collect_type_info[i].prop_name), PROPERTY_HINT_FLAGS, hint), 0));596}597}598599String run_script = "#!/usr/bin/env bash\n"600"unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"\n"601"open \"{temp_dir}/{exe_name}.app\" --args {cmd_args}";602603String cleanup_script = "#!/usr/bin/env bash\n"604"kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\")\n"605"rm -rf \"{temp_dir}\"";606607r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false, true));608r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/host"), "user@host_ip"));609r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/port"), "22"));610611r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_ssh", PROPERTY_HINT_MULTILINE_TEXT), ""));612r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/extra_args_scp", PROPERTY_HINT_MULTILINE_TEXT), ""));613r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/run_script", PROPERTY_HINT_MULTILINE_TEXT), run_script));614r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/cleanup_script", PROPERTY_HINT_MULTILINE_TEXT), cleanup_script));615}616617void _rgba8_to_packbits_encode(int p_ch, int p_size, Vector<uint8_t> &p_source, Vector<uint8_t> &p_dest) {618int src_len = p_size * p_size;619620Vector<uint8_t> result;621622int i = 0;623const uint8_t *src = p_source.ptr();624while (i < src_len) {625Vector<uint8_t> seq;626627uint8_t count = 0;628while (count <= 0x7f && i < src_len) {629if (i + 2 < src_len && src[i * 4 + p_ch] == src[(i + 1) * 4 + p_ch] && src[i] == src[(i + 2) * 4 + p_ch]) {630break;631}632seq.push_back(src[i * 4 + p_ch]);633i++;634count++;635}636if (!seq.is_empty()) {637result.push_back(count - 1);638result.append_array(seq);639}640if (i >= src_len) {641break;642}643644uint8_t rep = src[i * 4 + p_ch];645count = 0;646while (count <= 0x7f && i < src_len && src[i * 4 + p_ch] == rep) {647i++;648count++;649}650if (count >= 3) {651result.push_back(0x80 + count - 3);652result.push_back(rep);653} else {654result.push_back(count - 1);655for (int j = 0; j < count; j++) {656result.push_back(rep);657}658}659}660661int ofs = p_dest.size();662p_dest.resize(p_dest.size() + result.size());663memcpy(&p_dest.write[ofs], result.ptr(), result.size());664}665666void EditorExportPlatformMacOS::_make_icon(const Ref<EditorExportPreset> &p_preset, const Ref<Image> &p_icon, Vector<uint8_t> &p_data) {667Vector<uint8_t> data;668669data.resize(8);670data.write[0] = 'i';671data.write[1] = 'c';672data.write[2] = 'n';673data.write[3] = 's';674675struct MacOSIconInfo {676const char *name;677const char *mask_name;678bool is_png;679int size;680};681682static const MacOSIconInfo icon_infos[] = {683{ "ic10", "", true, 1024 }, //1024×1024 32-bit PNG and 512×512@2x 32-bit "retina" PNG684{ "ic09", "", true, 512 }, //512×512 32-bit PNG685{ "ic14", "", true, 512 }, //256×256@2x 32-bit "retina" PNG686{ "ic08", "", true, 256 }, //256×256 32-bit PNG687{ "ic13", "", true, 256 }, //128×128@2x 32-bit "retina" PNG688{ "ic07", "", true, 128 }, //128×128 32-bit PNG689{ "ic12", "", true, 64 }, //32×32@2× 32-bit "retina" PNG690{ "ic11", "", true, 32 }, //16×16@2× 32-bit "retina" PNG691{ "il32", "l8mk", false, 32 }, //32×32 24-bit RLE + 8-bit uncompressed mask692{ "is32", "s8mk", false, 16 } //16×16 24-bit RLE + 8-bit uncompressed mask693};694695for (uint64_t i = 0; i < std_size(icon_infos); ++i) {696Ref<Image> copy = p_icon->duplicate();697copy->convert(Image::FORMAT_RGBA8);698copy->resize(icon_infos[i].size, icon_infos[i].size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));699700if (icon_infos[i].is_png) {701// Encode PNG icon.702Vector<uint8_t> png_buffer;703Error err = PNGDriverCommon::image_to_png(copy, png_buffer);704if (err == OK) {705int ofs = data.size();706uint64_t len = png_buffer.size();707data.resize(data.size() + len + 8);708memcpy(&data.write[ofs + 8], png_buffer.ptr(), len);709len += 8;710len = BSWAP32(len);711memcpy(&data.write[ofs], icon_infos[i].name, 4);712encode_uint32(len, &data.write[ofs + 4]);713}714} else {715Vector<uint8_t> src_data = copy->get_data();716717// Encode 24-bit RGB RLE icon.718{719int ofs = data.size();720data.resize(data.size() + 8);721722_rgba8_to_packbits_encode(0, icon_infos[i].size, src_data, data); // Encode R.723_rgba8_to_packbits_encode(1, icon_infos[i].size, src_data, data); // Encode G.724_rgba8_to_packbits_encode(2, icon_infos[i].size, src_data, data); // Encode B.725726// Note: workaround for macOS icon decoder bug corrupting last RLE encoded value.727data.push_back(0x00);728729int len = data.size() - ofs;730len = BSWAP32(len);731memcpy(&data.write[ofs], icon_infos[i].name, 4);732encode_uint32(len, &data.write[ofs + 4]);733}734735// Encode 8-bit mask uncompressed icon.736{737int ofs = data.size();738int len = copy->get_width() * copy->get_height();739data.resize(data.size() + len + 8);740741for (int j = 0; j < len; j++) {742data.write[ofs + 8 + j] = src_data.ptr()[j * 4 + 3];743}744len += 8;745len = BSWAP32(len);746memcpy(&data.write[ofs], icon_infos[i].mask_name, 4);747encode_uint32(len, &data.write[ofs + 4]);748}749}750}751752uint32_t total_len = data.size();753total_len = BSWAP32(total_len);754encode_uint32(total_len, &data.write[4]);755756p_data = data;757}758759void EditorExportPlatformMacOS::_fix_privacy_manifest(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &plist) {760String str = String::utf8((const char *)plist.ptr(), plist.size());761String strnew;762Vector<String> lines = str.split("\n");763for (int i = 0; i < lines.size(); i++) {764if (lines[i].find("$priv_collection") != -1) {765bool section_opened = false;766for (uint64_t j = 0; j < std_size(data_collect_type_info); ++j) {767bool data_collected = p_preset->get(vformat("privacy/collected_data/%s/collected", data_collect_type_info[j].prop_name));768bool linked = p_preset->get(vformat("privacy/collected_data/%s/linked_to_user", data_collect_type_info[j].prop_name));769bool tracking = p_preset->get(vformat("privacy/collected_data/%s/used_for_tracking", data_collect_type_info[j].prop_name));770int purposes = p_preset->get(vformat("privacy/collected_data/%s/collection_purposes", data_collect_type_info[j].prop_name));771if (data_collected) {772if (!section_opened) {773section_opened = true;774strnew += "\t<key>NSPrivacyCollectedDataTypes</key>\n";775strnew += "\t<array>\n";776}777strnew += "\t\t<dict>\n";778strnew += "\t\t\t<key>NSPrivacyCollectedDataType</key>\n";779strnew += vformat("\t\t\t<string>%s</string>\n", data_collect_type_info[j].type_name);780strnew += "\t\t\t\t<key>NSPrivacyCollectedDataTypeLinked</key>\n";781if (linked) {782strnew += "\t\t\t\t<true/>\n";783} else {784strnew += "\t\t\t\t<false/>\n";785}786strnew += "\t\t\t\t<key>NSPrivacyCollectedDataTypeTracking</key>\n";787if (tracking) {788strnew += "\t\t\t\t<true/>\n";789} else {790strnew += "\t\t\t\t<false/>\n";791}792if (purposes != 0) {793strnew += "\t\t\t\t<key>NSPrivacyCollectedDataTypePurposes</key>\n";794strnew += "\t\t\t\t<array>\n";795for (uint64_t k = 0; k < std_size(data_collect_purpose_info); ++k) {796if (purposes & (1 << k)) {797strnew += vformat("\t\t\t\t\t<string>%s</string>\n", data_collect_purpose_info[k].type_name);798}799}800strnew += "\t\t\t\t</array>\n";801}802strnew += "\t\t\t</dict>\n";803}804}805if (section_opened) {806strnew += "\t</array>\n";807}808} else if (lines[i].find("$priv_tracking") != -1) {809bool tracking = p_preset->get("privacy/tracking_enabled");810strnew += "\t<key>NSPrivacyTracking</key>\n";811if (tracking) {812strnew += "\t<true/>\n";813} else {814strnew += "\t<false/>\n";815}816Vector<String> tracking_domains = p_preset->get("privacy/tracking_domains");817if (!tracking_domains.is_empty()) {818strnew += "\t<key>NSPrivacyTrackingDomains</key>\n";819strnew += "\t<array>\n";820for (const String &E : tracking_domains) {821strnew += "\t\t<string>" + E + "</string>\n";822}823strnew += "\t</array>\n";824}825} else {826strnew += lines[i] + "\n";827}828}829830CharString cs = strnew.utf8();831plist.resize(cs.size() - 1);832for (int i = 0; i < cs.size() - 1; i++) {833plist.write[i] = cs[i];834}835}836837void EditorExportPlatformMacOS::_fix_plist(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &plist, const String &p_binary, bool p_lg_icon_exported, const String &p_lg_icon) {838String str = String::utf8((const char *)plist.ptr(), plist.size());839String strnew;840Vector<String> lines = str.split("\n");841for (int i = 0; i < lines.size(); i++) {842if (lines[i].contains("$binary")) {843strnew += lines[i].replace("$binary", p_binary) + "\n";844} else if (lines[i].contains("$name")) {845strnew += lines[i].replace("$name", get_project_setting(p_preset, "application/config/name")) + "\n";846} else if (lines[i].contains("$bundle_identifier")) {847strnew += lines[i].replace("$bundle_identifier", p_preset->get("application/bundle_identifier")) + "\n";848} else if (lines[i].contains("$short_version")) {849strnew += lines[i].replace("$short_version", p_preset->get_version("application/short_version")) + "\n";850} else if (lines[i].contains("$version")) {851strnew += lines[i].replace("$version", p_preset->get_version("application/version")) + "\n";852} else if (lines[i].contains("$signature")) {853strnew += lines[i].replace("$signature", p_preset->get("application/signature")) + "\n";854} else if (lines[i].contains("$app_category")) {855String cat = p_preset->get("application/app_category");856strnew += lines[i].replace("$app_category", cat.to_lower()) + "\n";857} else if (lines[i].contains("$copyright")) {858strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n";859} else if (lines[i].contains("$min_version_arm64")) {860strnew += lines[i].replace("$min_version_arm64", p_preset->get("application/min_macos_version_arm64")) + "\n";861} else if (lines[i].contains("$min_version_x86_64")) {862strnew += lines[i].replace("$min_version_x86_64", p_preset->get("application/min_macos_version_x86_64")) + "\n";863} else if (lines[i].contains("$min_version")) {864strnew += lines[i].replace("$min_version", p_preset->get("application/min_macos_version_x86_64")) + "\n"; // Old template, use x86-64 version for both.865} else if (lines[i].contains("$highres")) {866strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "\t<true/>" : "\t<false/>") + "\n";867} else if (lines[i].contains("$additional_plist_content")) {868strnew += lines[i].replace("$additional_plist_content", p_preset->get("application/additional_plist_content")) + "\n";869} else if (lines[i].contains("$platfbuild")) {870strnew += lines[i].replace("$platfbuild", p_preset->get("xcode/platform_build")) + "\n";871} else if (lines[i].contains("$sdkver")) {872strnew += lines[i].replace("$sdkver", p_preset->get("xcode/sdk_version")) + "\n";873} else if (lines[i].contains("$sdkname")) {874strnew += lines[i].replace("$sdkname", p_preset->get("xcode/sdk_name")) + "\n";875} else if (lines[i].contains("$sdkbuild")) {876strnew += lines[i].replace("$sdkbuild", p_preset->get("xcode/sdk_build")) + "\n";877} else if (lines[i].contains("$xcodever")) {878strnew += lines[i].replace("$xcodever", p_preset->get("xcode/xcode_version")) + "\n";879} else if (lines[i].contains("$xcodebuild")) {880strnew += lines[i].replace("$xcodebuild", p_preset->get("xcode/xcode_build")) + "\n";881} else if (lines[i].contains("$liquid_glass_icon")) {882if (p_lg_icon_exported) {883strnew += lines[i].replace("$liquid_glass_icon", "\t<key>CFBundleIconName</key>\n\t<string>" + p_lg_icon + "</string>\n");884} else {885strnew += lines[i].replace("$liquid_glass_icon", "");886}887} else if (lines[i].contains("$usage_descriptions")) {888String descriptions;889if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {890descriptions += "\t<key>NSMicrophoneUsageDescription</key>\n";891descriptions += "\t<string>" + (String)p_preset->get("privacy/microphone_usage_description") + "</string>\n";892}893if (!((String)p_preset->get("privacy/camera_usage_description")).is_empty()) {894descriptions += "\t<key>NSCameraUsageDescription</key>\n";895descriptions += "\t<string>" + (String)p_preset->get("privacy/camera_usage_description") + "</string>\n";896}897if (!((String)p_preset->get("privacy/location_usage_description")).is_empty()) {898descriptions += "\t<key>NSLocationUsageDescription</key>\n";899descriptions += "\t<string>" + (String)p_preset->get("privacy/location_usage_description") + "</string>\n";900}901if (!((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) {902descriptions += "\t<key>NSContactsUsageDescription</key>\n";903descriptions += "\t<string>" + (String)p_preset->get("privacy/address_book_usage_description") + "</string>\n";904}905if (!((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) {906descriptions += "\t<key>NSCalendarsUsageDescription</key>\n";907descriptions += "\t<string>" + (String)p_preset->get("privacy/calendar_usage_description") + "</string>\n";908}909if (!((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) {910descriptions += "\t<key>NSPhotoLibraryUsageDescription</key>\n";911descriptions += "\t<string>" + (String)p_preset->get("privacy/photos_library_usage_description") + "</string>\n";912}913if (!((String)p_preset->get("privacy/desktop_folder_usage_description")).is_empty()) {914descriptions += "\t<key>NSDesktopFolderUsageDescription</key>\n";915descriptions += "\t<string>" + (String)p_preset->get("privacy/desktop_folder_usage_description") + "</string>\n";916}917if (!((String)p_preset->get("privacy/documents_folder_usage_description")).is_empty()) {918descriptions += "\t<key>NSDocumentsFolderUsageDescription</key>\n";919descriptions += "\t<string>" + (String)p_preset->get("privacy/documents_folder_usage_description") + "</string>\n";920}921if (!((String)p_preset->get("privacy/downloads_folder_usage_description")).is_empty()) {922descriptions += "\t<key>NSDownloadsFolderUsageDescription</key>\n";923descriptions += "\t<string>" + (String)p_preset->get("privacy/downloads_folder_usage_description") + "</string>\n";924}925if (!((String)p_preset->get("privacy/network_volumes_usage_description")).is_empty()) {926descriptions += "\t<key>NSNetworkVolumesUsageDescription</key>\n";927descriptions += "\t<string>" + (String)p_preset->get("privacy/network_volumes_usage_description") + "</string>\n";928}929if (!((String)p_preset->get("privacy/removable_volumes_usage_description")).is_empty()) {930descriptions += "\t<key>NSRemovableVolumesUsageDescription</key>\n";931descriptions += "\t<string>" + (String)p_preset->get("privacy/removable_volumes_usage_description") + "</string>\n";932}933if (!descriptions.is_empty()) {934strnew += lines[i].replace("$usage_descriptions", descriptions);935}936} else {937strnew += lines[i] + "\n";938}939}940941CharString cs = strnew.utf8();942plist.resize(cs.size() - 1);943for (int i = 0; i < cs.size() - 1; i++) {944plist.write[i] = cs[i];945}946}947948Error EditorExportPlatformMacOS::_export_liquid_glass_icon(const Ref<EditorExportPreset> &p_preset, const String &p_app_path, const String &p_icon_path) {949String actool = EDITOR_GET("export/macos/actool").operator String();950if (actool.is_empty()) {951actool = "actool";952}953954List<String> args;955args.push_back("--version");956String str;957String err_str;958int exitcode = 0;959960Error err = OS::get_singleton()->execute(actool, args, &str, &exitcode, true);961if (err != OK) {962add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not start 'actool' executable."));963return err;964}965PList info_plist;966if (!info_plist.load_string(str, err_str)) {967print_verbose(str);968add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not read 'actool' version."));969return err;970}971if (info_plist.get_root()->data_type == PList::PLNodeType::PL_NODE_TYPE_DICT && info_plist.get_root()->data_dict.has("com.apple.actool.version")) {972Ref<PListNode> dict = info_plist.get_root()->data_dict["com.apple.actool.version"];973if (dict->data_type == PList::PLNodeType::PL_NODE_TYPE_DICT && dict->data_dict.has("short-bundle-version")) {974float version = String::utf8(dict->data_dict["short-bundle-version"]->data_string.get_data()).to_float();975if (version < 26.0) {976add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), vformat(TTR("At least version 26.0 of 'actool' is required (version %f found)."), version));977return ERR_UNAVAILABLE;978}979}980}981str.clear();982983String plist = EditorPaths::get_singleton()->get_temp_dir().path_join("assetcatalog.plist");984args.clear();985args.push_back(ProjectSettings::get_singleton()->globalize_path(p_icon_path));986args.push_back("--compile");987args.push_back(p_app_path + "/Contents/Resources/");988args.push_back("--output-format");989args.push_back("human-readable-text");990args.push_back("--lightweight-asset-runtime-mode");991args.push_back("enabled");992args.push_back("--app-icon");993args.push_back(p_icon_path.get_file().get_basename());994args.push_back("--include-all-app-icons");995args.push_back("--enable-on-demand-resources");996args.push_back("NO");997args.push_back("--development-region");998args.push_back("en");999args.push_back("--target-device");1000args.push_back("mac");1001args.push_back("--minimum-deployment-target");1002args.push_back("26");1003args.push_back("--platform");1004args.push_back("macosx");1005args.push_back("--output-partial-info-plist");1006args.push_back(plist);10071008err = OS::get_singleton()->execute(actool, args, &str, &exitcode, true);1009if (err != OK || str.contains("error:") || !FileAccess::exists(p_app_path + "/Contents/Resources/Assets.car") || !FileAccess::exists(plist)) {1010print_verbose(str);1011add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not export liquid glass icon:") + "\n" + str);1012return err;1013}10141015return OK;1016}10171018/**1019* If we're running the macOS version of the Godot editor we'll:1020* - export our application bundle to a temporary folder1021* - attempt to code sign it1022* - and then wrap it up in a DMG1023*/10241025Error EditorExportPlatformMacOS::_notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path) {1026int notary_tool = p_preset->get("notarization/notarization");1027switch (notary_tool) {1028case 1: { // "rcodesign"1029print_verbose("using rcodesign notarization...");10301031String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String();1032if (rcodesign.is_empty()) {1033add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign)."));1034return Error::FAILED;1035}10361037List<String> args;10381039args.push_back("notary-submit");10401041if (p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) == "") {1042add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect issuer ID name not specified."));1043return Error::FAILED;1044}1045if (p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY) == "") {1046add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified."));1047return Error::FAILED;1048}10491050args.push_back("--api-issuer");1051args.push_back(p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID));10521053args.push_back("--api-key");1054args.push_back(p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID));10551056if (!p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY).operator String().is_empty()) {1057args.push_back("--api-key-path");1058args.push_back(p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY));1059}10601061args.push_back(p_path);10621063String str;1064int exitcode = 0;10651066Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true);1067if (err != OK) {1068add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start rcodesign executable."));1069return err;1070}10711072int rq_offset = str.find("created submission ID:");1073if (exitcode != 0 || rq_offset == -1) {1074print_line("rcodesign (" + p_path + "):\n" + str);1075add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details."));1076return Error::FAILED;1077} else {1078print_verbose("rcodesign (" + p_path + "):\n" + str);1079int next_nl = str.find_char('\n', rq_offset);1080String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 23) : str.substr(rq_offset + 23, next_nl - rq_offset - 23);1081add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid));1082add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour."));1083add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check the progress manually by opening a Terminal and running the following command:"));1084add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign notary-log --api-issuer <API UUID> --api-key <API key> <request UUID>\"");1085add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):"));1086add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign staple <app path>\"");1087}1088} break;1089#ifdef MACOS_ENABLED1090case 2: { // "notarytool"1091print_verbose("using notarytool notarization...");10921093if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) {1094add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Xcode command line tools are not installed."));1095return Error::FAILED;1096}10971098List<String> args;10991100args.push_back("notarytool");1101args.push_back("submit");11021103args.push_back(p_path);11041105if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) == "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) == "") {1106add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified."));1107return Error::FAILED;1108}1109if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) != "") {1110add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time."));1111return Error::FAILED;1112}11131114if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "") {1115if (p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS) == "") {1116add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Apple ID password not specified."));1117return Error::FAILED;1118}1119args.push_back("--apple-id");1120args.push_back(p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID));11211122args.push_back("--password");1123args.push_back(p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS));1124} else {1125if (p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID) == "") {1126add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified."));1127return Error::FAILED;1128}1129args.push_back("--issuer");1130args.push_back(p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID));11311132if (!p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY).operator String().is_empty()) {1133args.push_back("--key");1134args.push_back(p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY));1135}11361137args.push_back("--key-id");1138args.push_back(p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID));1139}11401141args.push_back("--no-progress");11421143if (p_preset->get("codesign/apple_team_id")) {1144args.push_back("--team-id");1145args.push_back(p_preset->get("codesign/apple_team_id"));1146}11471148String str;1149int exitcode = 0;1150Error err = OS::get_singleton()->execute("xcrun", args, &str, &exitcode, true);1151if (err != OK) {1152add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start xcrun executable."));1153return err;1154}11551156int rq_offset = str.find("id:");1157if (exitcode != 0 || rq_offset == -1) {1158print_line("notarytool (" + p_path + "):\n" + str);1159add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details."));1160return Error::FAILED;1161} else {1162print_verbose("notarytool (" + p_path + "):\n" + str);1163int next_nl = str.find_char('\n', rq_offset);1164String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 4) : str.substr(rq_offset + 4, next_nl - rq_offset - 4);1165add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid));1166add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour."));1167add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("See instructions on finding your team ID: https://developer.apple.com/help/glossary/team-id"));1168add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check the progress manually by opening a Terminal and running the following command:"));1169add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun notarytool log <request UUID> --issuer <API UUID> --key-id <API key ID> --key <API key path>\" or");1170add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun notarytool log <request UUID> --team-id <team ID> --apple-id <your email> --password <app-specific password>\"");1171add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):"));1172add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun stapler staple <app path>\"");1173}1174} break;1175#endif1176default: {1177};1178}1179return OK;1180}11811182void EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn, bool p_set_id) {1183int codesign_tool = p_preset->get("codesign/codesign");1184switch (codesign_tool) {1185case 1: { // built-in ad-hoc1186print_verbose("using built-in codesign...");1187String error_msg;1188Error err = CodeSign::codesign(false, true, p_path, p_ent_path, error_msg);1189if (err != OK) {1190add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Built-in CodeSign failed with error \"%s\"."), error_msg));1191return;1192}1193} break;1194case 2: { // "rcodesign"1195print_verbose("using rcodesign codesign...");11961197String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String();1198if (rcodesign.is_empty()) {1199add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xrcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign)."));1200return;1201}12021203List<String> args;1204args.push_back("sign");12051206if (!p_ent_path.is_empty()) {1207args.push_back("--entitlements-xml-path");1208args.push_back(p_ent_path);1209}12101211String certificate_file = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE);1212String certificate_pass = p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_PASS);1213if (!certificate_file.is_empty() && !certificate_pass.is_empty()) {1214args.push_back("--p12-file");1215args.push_back(certificate_file);1216args.push_back("--p12-password");1217args.push_back(certificate_pass);1218}1219args.push_back("--code-signature-flags");1220args.push_back("runtime");12211222if (p_set_id) {1223String app_id = p_preset->get("application/bundle_identifier");1224args.push_back("--binary-identifier");1225args.push_back(app_id);1226}12271228args.push_back("-v"); /* provide some more feedback */12291230args.push_back(p_path);12311232String str;1233int exitcode = 0;12341235Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true);1236if (err != OK) {1237add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start rcodesign executable."));1238return;1239}12401241if (exitcode != 0) {1242print_line("rcodesign (" + p_path + "):\n" + str);1243add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details."));1244return;1245} else {1246print_verbose("rcodesign (" + p_path + "):\n" + str);1247}1248} break;1249#ifdef MACOS_ENABLED1250case 3: { // "codesign"1251print_verbose("using xcode codesign...");12521253if (!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) {1254add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xcode command line tools are not installed."));1255return;1256}12571258bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");12591260List<String> args;1261if (!ad_hoc) {1262args.push_back("--timestamp");1263args.push_back("--options");1264args.push_back("runtime");1265}12661267if (!p_ent_path.is_empty()) {1268args.push_back("--entitlements");1269args.push_back(p_ent_path);1270}12711272PackedStringArray user_args = p_preset->get("codesign/custom_options");1273for (int i = 0; i < user_args.size(); i++) {1274String user_arg = user_args[i].strip_edges();1275if (!user_arg.is_empty()) {1276args.push_back(user_arg);1277}1278}12791280args.push_back("-s");1281if (ad_hoc) {1282args.push_back("-");1283} else {1284args.push_back(p_preset->get("codesign/identity"));1285}12861287if (p_set_id) {1288String app_id = p_preset->get("application/bundle_identifier");1289args.push_back("-i");1290args.push_back(app_id);1291}12921293args.push_back("-v"); /* provide some more feedback */1294args.push_back("-f");12951296args.push_back(p_path);12971298String str;1299int exitcode = 0;13001301Error err = OS::get_singleton()->execute("codesign", args, &str, &exitcode, true);1302if (err != OK) {1303add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start codesign executable, make sure Xcode command line tools are installed."));1304return;1305}13061307if (exitcode != 0) {1308print_line("codesign (" + p_path + "):\n" + str);1309add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details."));1310return;1311} else {1312print_verbose("codesign (" + p_path + "):\n" + str);1313}1314} break;1315#endif1316default: {1317};1318}1319}13201321void EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path,1322const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code) {1323static Vector<String> extensions_to_sign;13241325bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled");1326if (extensions_to_sign.is_empty()) {1327extensions_to_sign.push_back("dylib");1328extensions_to_sign.push_back("framework");1329extensions_to_sign.push_back("");1330}13311332Error dir_access_error;1333Ref<DirAccess> dir_access{ DirAccess::open(p_path, &dir_access_error) };13341335if (dir_access_error != OK) {1336add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign directory %s."), p_path));1337return;1338}13391340dir_access->list_dir_begin();1341String current_file{ dir_access->get_next() };1342while (!current_file.is_empty()) {1343String current_file_path{ p_path.path_join(current_file) };13441345if (current_file == ".." || current_file == ".") {1346current_file = dir_access->get_next();1347continue;1348}13491350if (extensions_to_sign.has(current_file.get_extension())) {1351String ent_path;1352bool set_bundle_id = false;1353if (sandbox && FileAccess::exists(current_file_path)) {1354int ftype = MachO::get_filetype(current_file_path);1355if (ftype == 2 || ftype == 5) {1356ent_path = p_helper_ent_path;1357set_bundle_id = true;1358}1359}1360_code_sign(p_preset, current_file_path, ent_path, false, set_bundle_id);1361if (is_executable(current_file_path)) {1362// chmod with 0755 if the file is executable.1363FileAccess::set_unix_permissions(current_file_path, 0755);1364}1365} else if (dir_access->current_is_dir()) {1366_code_sign_directory(p_preset, current_file_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code);1367} else if (p_should_error_on_non_code) {1368add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign file %s."), current_file));1369}13701371current_file = dir_access->get_next();1372}1373}13741375Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path,1376const String &p_in_app_path, bool p_sign_enabled,1377const Ref<EditorExportPreset> &p_preset, const String &p_ent_path,1378const String &p_helper_ent_path,1379bool p_should_error_on_non_code_sign, bool p_sandbox) {1380static Vector<String> extensions_to_sign;13811382if (extensions_to_sign.is_empty()) {1383extensions_to_sign.push_back("dylib");1384extensions_to_sign.push_back("framework");1385extensions_to_sign.push_back("");1386}13871388Error err{ OK };1389if (dir_access->dir_exists(p_src_path)) {1390#ifndef UNIX_ENABLED1391add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Relative symlinks are not supported, exported \"%s\" might be broken!"), p_src_path.get_file()));1392#endif1393print_verbose("export framework: " + p_src_path + " -> " + p_in_app_path);13941395bool plist_missing = false;1396Ref<PList> plist;1397plist.instantiate();1398plist->load_file(p_src_path.path_join("Resources").path_join("Info.plist"));13991400Ref<PListNode> root_node = plist->get_root();1401if (root_node.is_null()) {1402plist_missing = true;1403} else {1404Dictionary root = root_node->get_value();1405if (!root.has("CFBundleExecutable") || !root.has("CFBundleIdentifier") || !root.has("CFBundlePackageType") || !root.has("CFBundleInfoDictionaryVersion") || !root.has("CFBundleName") || !root.has("CFBundleSupportedPlatforms")) {1406plist_missing = true;1407}1408}14091410err = dir_access->make_dir_recursive(p_in_app_path);1411if (err == OK) {1412err = dir_access->copy_dir(p_src_path, p_in_app_path, -1, true);1413}1414if (err == OK && plist_missing) {1415add_message(EXPORT_MESSAGE_WARNING, TTR("Export"), vformat(TTR("\"%s\": Info.plist missing or invalid, new Info.plist generated."), p_src_path.get_file()));1416// Generate Info.plist1417String lib_name = p_src_path.get_basename().get_file();1418String lib_id = p_preset->get("application/bundle_identifier");1419String lib_clean_name = lib_name;1420for (int i = 0; i < lib_clean_name.length(); i++) {1421if (!is_ascii_alphanumeric_char(lib_clean_name[i]) && lib_clean_name[i] != '.' && lib_clean_name[i] != '-') {1422lib_clean_name[i] = '-';1423}1424}14251426String info_plist_format = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"1427"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"1428"<plist version=\"1.0\">\n"1429" <dict>\n"1430" <key>CFBundleExecutable</key>\n"1431" <string>$name</string>\n"1432" <key>CFBundleIdentifier</key>\n"1433" <string>$id.framework.$cl_name</string>\n"1434" <key>CFBundleInfoDictionaryVersion</key>\n"1435" <string>6.0</string>\n"1436" <key>CFBundleName</key>\n"1437" <string>$name</string>\n"1438" <key>CFBundlePackageType</key>\n"1439" <string>FMWK</string>\n"1440" <key>CFBundleShortVersionString</key>\n"1441" <string>1.0.0</string>\n"1442" <key>CFBundleSupportedPlatforms</key>\n"1443" <array>\n"1444" <string>MacOSX</string>\n"1445" </array>\n"1446" <key>CFBundleVersion</key>\n"1447" <string>1.0.0</string>\n"1448" <key>LSMinimumSystemVersion</key>\n"1449" <string>10.12</string>\n"1450" </dict>\n"1451"</plist>";14521453String info_plist = info_plist_format.replace("$id", lib_id).replace("$name", lib_name).replace("$cl_name", lib_clean_name);14541455err = dir_access->make_dir_recursive(p_in_app_path.path_join("Resources"));1456Ref<FileAccess> f = FileAccess::open(p_in_app_path.path_join("Resources").path_join("Info.plist"), FileAccess::WRITE);1457if (f.is_valid()) {1458f->store_string(info_plist);1459}1460}1461} else {1462print_verbose("export dylib: " + p_src_path + " -> " + p_in_app_path);1463err = dir_access->copy(p_src_path, p_in_app_path);1464}1465if (err == OK && p_sign_enabled) {1466if (dir_access->dir_exists(p_src_path) && p_src_path.get_extension().is_empty()) {1467// If it is a directory, find and sign all dynamic libraries.1468_code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code_sign);1469} else {1470if (extensions_to_sign.has(p_in_app_path.get_extension())) {1471String ent_path;1472bool set_bundle_id = false;1473if (p_sandbox && FileAccess::exists(p_in_app_path)) {1474int ftype = MachO::get_filetype(p_in_app_path);1475if (ftype == 2 || ftype == 5) {1476ent_path = p_helper_ent_path;1477set_bundle_id = true;1478}1479}1480_code_sign(p_preset, p_in_app_path, ent_path, false, set_bundle_id);1481}1482if (dir_access->file_exists(p_in_app_path) && is_executable(p_in_app_path)) {1483// chmod with 0755 if the file is executable.1484FileAccess::set_unix_permissions(p_in_app_path, 0755);1485}1486}1487}1488return err;1489}14901491Error EditorExportPlatformMacOS::_export_macos_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin,1492const String &p_app_path_name, Ref<DirAccess> &dir_access,1493bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset,1494const String &p_ent_path, const String &p_helper_ent_path, bool p_sandbox) {1495Error error{ OK };1496const Vector<String> &macos_plugins{ p_editor_export_plugin->get_macos_plugin_files() };1497for (int i = 0; i < macos_plugins.size(); ++i) {1498String src_path{ ProjectSettings::get_singleton()->globalize_path(macos_plugins[i]) };1499String path_in_app{ p_app_path_name + "/Contents/PlugIns/" + src_path.get_file() };1500error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, p_helper_ent_path, false, p_sandbox);1501if (error != OK) {1502break;1503}1504}1505return error;1506}15071508Error EditorExportPlatformMacOS::_create_pkg(const Ref<EditorExportPreset> &p_preset, const String &p_pkg_path, const String &p_app_path_name) {1509List<String> args;15101511if (FileAccess::exists(p_pkg_path)) {1512OS::get_singleton()->move_to_trash(p_pkg_path);1513}15141515args.push_back("productbuild");1516args.push_back("--component");1517args.push_back(p_app_path_name);1518args.push_back("/Applications");1519String ident = p_preset->get("codesign/installer_identity");1520if (!ident.is_empty()) {1521args.push_back("--timestamp");1522args.push_back("--sign");1523args.push_back(ident);1524}1525args.push_back("--quiet");1526args.push_back(p_pkg_path);15271528String str;1529Error err = OS::get_singleton()->execute("xcrun", args, &str, nullptr, true);1530if (err != OK) {1531add_message(EXPORT_MESSAGE_ERROR, TTR("PKG Creation"), TTR("Could not start productbuild executable."));1532return err;1533}15341535print_verbose("productbuild returned: " + str);1536if (str.contains("productbuild: error:")) {1537add_message(EXPORT_MESSAGE_ERROR, TTR("PKG Creation"), TTR("`productbuild` failed."));1538return FAILED;1539}15401541return OK;1542}15431544Error EditorExportPlatformMacOS::_create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name) {1545List<String> args;15461547if (FileAccess::exists(p_dmg_path)) {1548OS::get_singleton()->move_to_trash(p_dmg_path);1549}15501551args.push_back("create");1552args.push_back(p_dmg_path);1553args.push_back("-volname");1554args.push_back(p_pkg_name);1555args.push_back("-fs");1556args.push_back("HFS+");1557args.push_back("-srcfolder");1558args.push_back(p_app_path_name);15591560String str;1561Error err = OS::get_singleton()->execute("hdiutil", args, &str, nullptr, true);1562if (err != OK) {1563add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("Could not start hdiutil executable."));1564return err;1565}15661567print_verbose("hdiutil returned: " + str);1568if (str.contains("create failed")) {1569if (str.contains("File exists")) {1570add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed - file exists."));1571} else {1572add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed."));1573}1574return FAILED;1575}15761577return OK;1578}15791580bool EditorExportPlatformMacOS::is_shebang(const String &p_path) const {1581Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ);1582ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("Can't open file: \"%s\".", p_path));1583uint16_t magic = fb->get_16();1584return (magic == 0x2123);1585}15861587bool EditorExportPlatformMacOS::is_executable(const String &p_path) const {1588return MachO::is_macho(p_path) || LipO::is_lipo(p_path) || is_shebang(p_path);1589}15901591Error EditorExportPlatformMacOS::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) {1592Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);1593if (f.is_null()) {1594add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path));1595return ERR_CANT_CREATE;1596}15971598f->store_line("#!/bin/sh");1599f->store_line("printf '\\033c\\033]0;%s\\a' " + p_app_name);1600f->store_line("");1601f->store_line("function app_realpath() {");1602f->store_line(" SOURCE=$1");1603f->store_line(" while [ -h \"$SOURCE\" ]; do");1604f->store_line(" DIR=$(dirname \"$SOURCE\")");1605f->store_line(" SOURCE=$(readlink \"$SOURCE\")");1606f->store_line(" [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE");1607f->store_line(" done");1608f->store_line(" echo \"$( cd -P \"$( dirname \"$SOURCE\" )\" >/dev/null 2>&1 && pwd )\"");1609f->store_line("}");1610f->store_line("");1611f->store_line("BASE_PATH=\"$(app_realpath \"${BASH_SOURCE[0]}\")\"");1612f->store_line("\"$BASE_PATH/" + p_pkg_name + "\" \"$@\"");1613f->store_line("");16141615return OK;1616}16171618Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) {1619ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);16201621const String base_dir = p_path.get_base_dir();16221623if (!DirAccess::exists(base_dir)) {1624add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir));1625return ERR_FILE_BAD_PATH;1626}16271628EditorProgress ep("export", TTR("Exporting for macOS"), 3, true);16291630String src_pkg_name;1631if (p_debug) {1632src_pkg_name = p_preset->get("custom_template/debug");1633} else {1634src_pkg_name = p_preset->get("custom_template/release");1635}16361637if (src_pkg_name.is_empty()) {1638String err;1639src_pkg_name = find_export_template("macos.zip", &err);1640if (src_pkg_name.is_empty()) {1641add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("Export template not found.") + "\n" + err);1642return ERR_FILE_NOT_FOUND;1643}1644}16451646Ref<FileAccess> io_fa;1647zlib_filefunc_def io = zipio_create_io(&io_fa);16481649if (ep.step(TTR("Creating app bundle"), 0)) {1650return ERR_SKIP;1651}16521653unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io);1654if (!src_pkg_zip) {1655add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not find template app to export: \"%s\"."), src_pkg_name));1656return ERR_FILE_NOT_FOUND;1657}16581659int ret = unzGoToFirstFile(src_pkg_zip);16601661String architecture = p_preset->get("binary_format/architecture");1662String binary_to_use = "godot_macos_" + String(p_debug ? "debug" : "release") + "." + architecture;16631664String pkg_name;1665if (String(get_project_setting(p_preset, "application/config/name")) != "") {1666pkg_name = String(get_project_setting(p_preset, "application/config/name"));1667} else {1668pkg_name = "Unnamed";1669}1670pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);16711672String export_format;1673if (p_path.ends_with("zip")) {1674export_format = "zip";1675} else if (p_path.ends_with("app")) {1676export_format = "app";1677#ifdef MACOS_ENABLED1678} else if (p_path.ends_with("dmg")) {1679export_format = "dmg";1680} else if (p_path.ends_with("pkg")) {1681export_format = "pkg";1682#endif1683} else {1684add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid export format."));1685return ERR_CANT_CREATE;1686}16871688// Create our application bundle.1689String tmp_app_dir_name = pkg_name + ".app";1690String tmp_base_path_name;1691String tmp_app_path_name;1692String scr_path;1693if (export_format == "app") {1694tmp_base_path_name = p_path.get_base_dir();1695tmp_app_path_name = p_path;1696scr_path = p_path.get_basename() + ".command";1697} else {1698tmp_base_path_name = EditorPaths::get_singleton()->get_temp_dir().path_join(pkg_name);1699tmp_app_path_name = tmp_base_path_name.path_join(tmp_app_dir_name);1700scr_path = tmp_base_path_name.path_join(pkg_name + ".command");1701}17021703print_verbose("Exporting to " + tmp_app_path_name);17041705Error err = OK;17061707Ref<DirAccess> tmp_app_dir = DirAccess::create_for_path(tmp_base_path_name);1708if (tmp_app_dir.is_null()) {1709add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory: \"%s\"."), tmp_base_path_name));1710err = ERR_CANT_CREATE;1711}17121713if (FileAccess::exists(scr_path)) {1714DirAccess::remove_file_or_error(scr_path);1715}1716if (DirAccess::exists(tmp_app_path_name)) {1717String old_dir = tmp_app_dir->get_current_dir();1718if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) {1719tmp_app_dir->erase_contents_recursive();1720tmp_app_dir->change_dir(old_dir);1721}1722}17231724Array helpers = p_preset->get("codesign/entitlements/app_sandbox/helper_executables");17251726// Create our folder structure.1727if (err == OK) {1728print_verbose("Creating " + tmp_app_path_name + "/Contents/MacOS");1729err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/MacOS");1730if (err != OK) {1731add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/MacOS"));1732}1733}17341735if (err == OK) {1736print_verbose("Creating " + tmp_app_path_name + "/Contents/Frameworks");1737err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks");1738if (err != OK) {1739add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Frameworks"));1740}1741}17421743if ((err == OK) && helpers.size() > 0) {1744print_line("Creating " + tmp_app_path_name + "/Contents/Helpers");1745err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Helpers");1746if (err != OK) {1747add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Helpers"));1748}1749}17501751if (err == OK) {1752print_verbose("Creating " + tmp_app_path_name + "/Contents/Resources");1753err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Resources");1754if (err != OK) {1755add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Resources"));1756}1757}17581759Dictionary appnames = get_project_setting(p_preset, "application/config/name_localized");1760Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized");1761Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized");1762Dictionary location_usage_descriptions = p_preset->get("privacy/location_usage_description_localized");1763Dictionary address_book_usage_descriptions = p_preset->get("privacy/address_book_usage_description_localized");1764Dictionary calendar_usage_descriptions = p_preset->get("privacy/calendar_usage_description_localized");1765Dictionary photos_library_usage_descriptions = p_preset->get("privacy/photos_library_usage_description_localized");1766Dictionary desktop_folder_usage_descriptions = p_preset->get("privacy/desktop_folder_usage_description_localized");1767Dictionary documents_folder_usage_descriptions = p_preset->get("privacy/documents_folder_usage_description_localized");1768Dictionary downloads_folder_usage_descriptions = p_preset->get("privacy/downloads_folder_usage_description_localized");1769Dictionary network_volumes_usage_descriptions = p_preset->get("privacy/network_volumes_usage_description_localized");1770Dictionary removable_volumes_usage_descriptions = p_preset->get("privacy/removable_volumes_usage_description_localized");1771Dictionary copyrights = p_preset->get("application/copyright_localized");17721773Vector<String> translations = get_project_setting(p_preset, "internationalization/locale/translations");1774if (translations.size() > 0) {1775{1776String fname = tmp_app_path_name + "/Contents/Resources/en.lproj";1777tmp_app_dir->make_dir_recursive(fname);1778Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);1779f->store_line("/* Localized versions of Info.plist keys */");1780f->store_line("");1781f->store_line("CFBundleDisplayName = \"" + get_project_setting(p_preset, "application/config/name").operator String() + "\";");1782if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {1783f->store_line("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_usage_description").operator String() + "\";");1784}1785if (!((String)p_preset->get("privacy/camera_usage_description")).is_empty()) {1786f->store_line("NSCameraUsageDescription = \"" + p_preset->get("privacy/camera_usage_description").operator String() + "\";");1787}1788if (!((String)p_preset->get("privacy/location_usage_description")).is_empty()) {1789f->store_line("NSLocationUsageDescription = \"" + p_preset->get("privacy/location_usage_description").operator String() + "\";");1790}1791if (!((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) {1792f->store_line("NSContactsUsageDescription = \"" + p_preset->get("privacy/address_book_usage_description").operator String() + "\";");1793}1794if (!((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) {1795f->store_line("NSCalendarsUsageDescription = \"" + p_preset->get("privacy/calendar_usage_description").operator String() + "\";");1796}1797if (!((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) {1798f->store_line("NSPhotoLibraryUsageDescription = \"" + p_preset->get("privacy/photos_library_usage_description").operator String() + "\";");1799}1800if (!((String)p_preset->get("privacy/desktop_folder_usage_description")).is_empty()) {1801f->store_line("NSDesktopFolderUsageDescription = \"" + p_preset->get("privacy/desktop_folder_usage_description").operator String() + "\";");1802}1803if (!((String)p_preset->get("privacy/documents_folder_usage_description")).is_empty()) {1804f->store_line("NSDocumentsFolderUsageDescription = \"" + p_preset->get("privacy/documents_folder_usage_description").operator String() + "\";");1805}1806if (!((String)p_preset->get("privacy/downloads_folder_usage_description")).is_empty()) {1807f->store_line("NSDownloadsFolderUsageDescription = \"" + p_preset->get("privacy/downloads_folder_usage_description").operator String() + "\";");1808}1809if (!((String)p_preset->get("privacy/network_volumes_usage_description")).is_empty()) {1810f->store_line("NSNetworkVolumesUsageDescription = \"" + p_preset->get("privacy/network_volumes_usage_description").operator String() + "\";");1811}1812if (!((String)p_preset->get("privacy/removable_volumes_usage_description")).is_empty()) {1813f->store_line("NSRemovableVolumesUsageDescription = \"" + p_preset->get("privacy/removable_volumes_usage_description").operator String() + "\";");1814}1815f->store_line("NSHumanReadableCopyright = \"" + p_preset->get("application/copyright").operator String() + "\";");1816}18171818HashSet<String> languages;1819for (const String &E : translations) {1820Ref<Translation> tr = ResourceLoader::load(E);1821if (tr.is_valid() && tr->get_locale() != "en") {1822languages.insert(tr->get_locale());1823}1824}18251826for (const String &lang : languages) {1827String fname = tmp_app_path_name + "/Contents/Resources/" + lang + ".lproj";1828tmp_app_dir->make_dir_recursive(fname);1829Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);1830f->store_line("/* Localized versions of Info.plist keys */");1831f->store_line("");1832if (appnames.has(lang)) {1833f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";");1834}1835if (microphone_usage_descriptions.has(lang)) {1836f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";");1837}1838if (camera_usage_descriptions.has(lang)) {1839f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";");1840}1841if (location_usage_descriptions.has(lang)) {1842f->store_line("NSLocationUsageDescription = \"" + location_usage_descriptions[lang].operator String() + "\";");1843}1844if (address_book_usage_descriptions.has(lang)) {1845f->store_line("NSContactsUsageDescription = \"" + address_book_usage_descriptions[lang].operator String() + "\";");1846}1847if (calendar_usage_descriptions.has(lang)) {1848f->store_line("NSCalendarsUsageDescription = \"" + calendar_usage_descriptions[lang].operator String() + "\";");1849}1850if (photos_library_usage_descriptions.has(lang)) {1851f->store_line("NSPhotoLibraryUsageDescription = \"" + photos_library_usage_descriptions[lang].operator String() + "\";");1852}1853if (desktop_folder_usage_descriptions.has(lang)) {1854f->store_line("NSDesktopFolderUsageDescription = \"" + desktop_folder_usage_descriptions[lang].operator String() + "\";");1855}1856if (documents_folder_usage_descriptions.has(lang)) {1857f->store_line("NSDocumentsFolderUsageDescription = \"" + documents_folder_usage_descriptions[lang].operator String() + "\";");1858}1859if (downloads_folder_usage_descriptions.has(lang)) {1860f->store_line("NSDownloadsFolderUsageDescription = \"" + downloads_folder_usage_descriptions[lang].operator String() + "\";");1861}1862if (network_volumes_usage_descriptions.has(lang)) {1863f->store_line("NSNetworkVolumesUsageDescription = \"" + network_volumes_usage_descriptions[lang].operator String() + "\";");1864}1865if (removable_volumes_usage_descriptions.has(lang)) {1866f->store_line("NSRemovableVolumesUsageDescription = \"" + removable_volumes_usage_descriptions[lang].operator String() + "\";");1867}1868if (copyrights.has(lang)) {1869f->store_line("NSHumanReadableCopyright = \"" + copyrights[lang].operator String() + "\";");1870}1871}1872}18731874// Now process our template.1875bool found_binary = false;18761877int export_angle = p_preset->get("application/export_angle");1878bool include_angle_libs = false;1879if (export_angle == 0) {1880include_angle_libs = String(get_project_setting(p_preset, "rendering/gl_compatibility/driver.macos")) == "opengl3_angle";1881} else if (export_angle == 1) {1882include_angle_libs = true;1883}18841885while (ret == UNZ_OK && err == OK) {1886// Get filename.1887unz_file_info info;1888char fname[16384];1889ret = unzGetCurrentFileInfo(src_pkg_zip, &info, fname, 16384, nullptr, 0, nullptr, 0);1890if (ret != UNZ_OK) {1891break;1892}18931894String file = String::utf8(fname);18951896Vector<uint8_t> data;1897data.resize(info.uncompressed_size);18981899// Read.1900unzOpenCurrentFile(src_pkg_zip);1901unzReadCurrentFile(src_pkg_zip, data.ptrw(), data.size());1902unzCloseCurrentFile(src_pkg_zip);19031904// Write.1905file = file.replace_first("macos_template.app/", "");19061907if (((info.external_fa >> 16L) & 0120000) == 0120000) {1908#ifndef UNIX_ENABLED1909add_message(EXPORT_MESSAGE_INFO, TTR("Export"), TTR("Relative symlinks are not supported on this OS, the exported project might be broken!"));1910#endif1911// Handle symlinks in the archive.1912file = tmp_app_path_name.path_join(file);1913if (err == OK) {1914err = tmp_app_dir->make_dir_recursive(file.get_base_dir());1915if (err != OK) {1916add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir()));1917}1918}1919if (err == OK) {1920String lnk_data = String::utf8((const char *)data.ptr(), data.size());1921err = tmp_app_dir->create_link(lnk_data, file);1922if (err != OK) {1923add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not created symlink \"%s\" -> \"%s\"."), lnk_data, file));1924}1925print_verbose(vformat("ADDING SYMLINK %s => %s\n", file, lnk_data));1926}19271928ret = unzGoToNextFile(src_pkg_zip);1929continue; // next1930}19311932if (file == "Contents/Frameworks/libEGL.dylib") {1933if (!include_angle_libs) {1934ret = unzGoToNextFile(src_pkg_zip);1935continue; // skip1936}1937}19381939if (file == "Contents/Frameworks/libGLESv2.dylib") {1940if (!include_angle_libs) {1941ret = unzGoToNextFile(src_pkg_zip);1942continue; // skip1943}1944}19451946if (file == "Contents/Info.plist") {1947bool lg_icon_expored = false;1948String lg_icon = p_preset->get("application/liquid_glass_icon");1949#ifdef MACOS_ENABLED1950// Export liquid glass.1951if (!lg_icon.is_empty()) {1952lg_icon_expored = (_export_liquid_glass_icon(p_preset, tmp_app_path_name, lg_icon) == OK);1953}1954#endif1955// Modify plist.1956_fix_plist(p_preset, data, pkg_name, lg_icon_expored, lg_icon.get_file().get_basename());1957}19581959if (file == "Contents/Resources/PrivacyInfo.xcprivacy") {1960_fix_privacy_manifest(p_preset, data);1961}19621963if (file.begins_with("Contents/MacOS/godot_")) {1964if (file != "Contents/MacOS/" + binary_to_use) {1965ret = unzGoToNextFile(src_pkg_zip);1966continue; // skip1967}1968found_binary = true;1969file = "Contents/MacOS/" + pkg_name;1970}19711972if (file == "Contents/Resources/icon.icns") {1973// See if there is an icon.1974String icon_path;1975if (p_preset->get("application/icon") != "") {1976icon_path = p_preset->get("application/icon");1977} else if (get_project_setting(p_preset, "application/config/macos_native_icon") != "") {1978icon_path = get_project_setting(p_preset, "application/config/macos_native_icon");1979} else {1980icon_path = get_project_setting(p_preset, "application/config/icon");1981}19821983if (!icon_path.is_empty()) {1984if (icon_path.get_extension() == "icns") {1985Ref<FileAccess> icon = FileAccess::open(icon_path, FileAccess::READ);1986if (icon.is_valid()) {1987data.resize(icon->get_length());1988icon->get_buffer(&data.write[0], icon->get_length());1989}1990} else {1991Ref<Image> icon = _load_icon_or_splash_image(icon_path, &err);1992if (err == OK && icon.is_valid() && !icon->is_empty()) {1993_make_icon(p_preset, icon, data);1994}1995}1996}1997}19981999if (data.size() > 0) {2000print_verbose("ADDING: " + file + " size: " + itos(data.size()));20012002// Write it into our application bundle.2003file = tmp_app_path_name.path_join(file);2004if (err == OK) {2005err = tmp_app_dir->make_dir_recursive(file.get_base_dir());2006if (err != OK) {2007add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir()));2008}2009}2010if (err == OK) {2011Ref<FileAccess> f = FileAccess::open(file, FileAccess::WRITE);2012if (f.is_valid()) {2013f->store_buffer(data.ptr(), data.size());2014f.unref();2015if (is_executable(file)) {2016// chmod with 0755 if the file is executable.2017FileAccess::set_unix_permissions(file, 0755);2018#ifndef UNIX_ENABLED2019if (export_format == "app") {2020add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), "Contents/MacOS/" + file.get_file()));2021}2022#endif2023}2024} else {2025add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not open \"%s\"."), file));2026err = ERR_CANT_CREATE;2027}2028}2029}20302031ret = unzGoToNextFile(src_pkg_zip);2032}20332034// We're done with our source zip.2035unzClose(src_pkg_zip);20362037if (!found_binary) {2038add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Requested template binary \"%s\" not found. It might be missing from your template archive."), binary_to_use));2039err = ERR_FILE_NOT_FOUND;2040}20412042// Save console wrapper.2043if (err == OK) {2044int con_scr = p_preset->get("debug/export_console_wrapper");2045if ((con_scr == 1 && p_debug) || (con_scr == 2)) {2046err = _export_debug_script(p_preset, pkg_name, tmp_app_path_name.get_file() + "/Contents/MacOS/" + pkg_name, scr_path);2047FileAccess::set_unix_permissions(scr_path, 0755);2048#ifndef UNIX_ENABLED2049if (export_format == "app") {2050add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), scr_path.get_file()));2051}2052#endif2053if (err != OK) {2054add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not create console wrapper."));2055}2056}2057}20582059if (err == OK) {2060if (ep.step(TTR("Making PKG"), 1)) {2061return ERR_SKIP;2062}20632064// See if we can code sign our new package.2065bool sign_enabled = (p_preset->get("codesign/codesign").operator int() > 0);2066bool ad_hoc = false;2067int codesign_tool = p_preset->get("codesign/codesign");2068switch (codesign_tool) {2069case 1: { // built-in ad-hoc2070ad_hoc = true;2071} break;2072case 2: { // "rcodesign"2073ad_hoc = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty() || p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_PASS).operator String().is_empty();2074} break;2075#ifdef MACOS_ENABLED2076case 3: { // "codesign"2077ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");2078} break;2079#endif2080default: {2081};2082}20832084String pack_path = tmp_app_path_name + "/Contents/Resources/" + pkg_name + ".pck";2085Vector<SharedObject> shared_objects;2086err = save_pack(p_preset, p_debug, pack_path, &shared_objects);20872088bool lib_validation = p_preset->get("codesign/entitlements/disable_library_validation");2089if (!shared_objects.is_empty() && sign_enabled && ad_hoc && !lib_validation) {2090add_message(EXPORT_MESSAGE_INFO, TTR("Entitlements Modified"), TTR("Ad-hoc signed applications require the 'Disable Library Validation' entitlement to load dynamic libraries."));2091lib_validation = true;2092}20932094if (!shared_objects.is_empty() && sign_enabled && codesign_tool == 2) {2095add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("'rcodesign' doesn't support signing applications with embedded dynamic libraries."));2096}20972098bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled");2099String ent_path = p_preset->get("codesign/entitlements/custom_file");2100String hlp_ent_path = sandbox ? EditorPaths::get_singleton()->get_temp_dir().path_join(pkg_name + "_helper.entitlements") : ent_path;2101if (sign_enabled && (ent_path.is_empty())) {2102ent_path = EditorPaths::get_singleton()->get_temp_dir().path_join(pkg_name + ".entitlements");21032104Ref<FileAccess> ent_f = FileAccess::open(ent_path, FileAccess::WRITE);2105if (ent_f.is_valid()) {2106ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");2107ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");2108ent_f->store_line("<plist version=\"1.0\">");2109ent_f->store_line("<dict>");2110if (ClassDB::class_exists("CSharpScript")) {2111// These entitlements are required to run managed code, and are always enabled in Mono builds.2112ent_f->store_line("<key>com.apple.security.cs.allow-jit</key>");2113ent_f->store_line("<true/>");2114ent_f->store_line("<key>com.apple.security.cs.allow-unsigned-executable-memory</key>");2115ent_f->store_line("<true/>");2116ent_f->store_line("<key>com.apple.security.cs.allow-dyld-environment-variables</key>");2117ent_f->store_line("<true/>");2118} else {2119if ((bool)p_preset->get("codesign/entitlements/allow_jit_code_execution")) {2120ent_f->store_line("<key>com.apple.security.cs.allow-jit</key>");2121ent_f->store_line("<true/>");2122}2123if ((bool)p_preset->get("codesign/entitlements/allow_unsigned_executable_memory")) {2124ent_f->store_line("<key>com.apple.security.cs.allow-unsigned-executable-memory</key>");2125ent_f->store_line("<true/>");2126}2127if ((bool)p_preset->get("codesign/entitlements/allow_dyld_environment_variables")) {2128ent_f->store_line("<key>com.apple.security.cs.allow-dyld-environment-variables</key>");2129ent_f->store_line("<true/>");2130}2131}21322133if (lib_validation) {2134ent_f->store_line("<key>com.apple.security.cs.disable-library-validation</key>");2135ent_f->store_line("<true/>");2136}2137if ((bool)p_preset->get("codesign/entitlements/audio_input")) {2138ent_f->store_line("<key>com.apple.security.device.audio-input</key>");2139ent_f->store_line("<true/>");2140}2141if ((bool)p_preset->get("codesign/entitlements/camera")) {2142ent_f->store_line("<key>com.apple.security.device.camera</key>");2143ent_f->store_line("<true/>");2144}2145if ((bool)p_preset->get("codesign/entitlements/location")) {2146ent_f->store_line("<key>com.apple.security.personal-information.location</key>");2147ent_f->store_line("<true/>");2148}2149if ((bool)p_preset->get("codesign/entitlements/address_book")) {2150ent_f->store_line("<key>com.apple.security.personal-information.addressbook</key>");2151ent_f->store_line("<true/>");2152}2153if ((bool)p_preset->get("codesign/entitlements/calendars")) {2154ent_f->store_line("<key>com.apple.security.personal-information.calendars</key>");2155ent_f->store_line("<true/>");2156}2157if ((bool)p_preset->get("codesign/entitlements/photos_library")) {2158ent_f->store_line("<key>com.apple.security.personal-information.photos-library</key>");2159ent_f->store_line("<true/>");2160}2161if ((bool)p_preset->get("codesign/entitlements/apple_events")) {2162ent_f->store_line("<key>com.apple.security.automation.apple-events</key>");2163ent_f->store_line("<true/>");2164}2165if ((bool)p_preset->get("codesign/entitlements/debugging")) {2166ent_f->store_line("<key>com.apple.security.get-task-allow</key>");2167ent_f->store_line("<true/>");2168}21692170int dist_type = p_preset->get("export/distribution_type");2171if (dist_type == 2) {2172String pprof = p_preset->get_or_env("codesign/provisioning_profile", ENV_MAC_CODESIGN_PROFILE);2173String teamid = p_preset->get("codesign/apple_team_id");2174String bid = p_preset->get("application/bundle_identifier");2175if (!pprof.is_empty() && !teamid.is_empty()) {2176ent_f->store_line("<key>com.apple.developer.team-identifier</key>");2177ent_f->store_line("<string>" + teamid + "</string>");2178ent_f->store_line("<key>com.apple.application-identifier</key>");2179ent_f->store_line("<string>" + teamid + "." + bid + "</string>");2180}2181}21822183if ((bool)p_preset->get("codesign/entitlements/app_sandbox/enabled")) {2184ent_f->store_line("<key>com.apple.security.app-sandbox</key>");2185ent_f->store_line("<true/>");21862187if ((bool)p_preset->get("codesign/entitlements/app_sandbox/network_server")) {2188ent_f->store_line("<key>com.apple.security.network.server</key>");2189ent_f->store_line("<true/>");2190}2191if ((bool)p_preset->get("codesign/entitlements/app_sandbox/network_client")) {2192ent_f->store_line("<key>com.apple.security.network.client</key>");2193ent_f->store_line("<true/>");2194}2195if ((bool)p_preset->get("codesign/entitlements/app_sandbox/device_usb")) {2196ent_f->store_line("<key>com.apple.security.device.usb</key>");2197ent_f->store_line("<true/>");2198}2199if ((bool)p_preset->get("codesign/entitlements/app_sandbox/device_bluetooth")) {2200ent_f->store_line("<key>com.apple.security.device.bluetooth</key>");2201ent_f->store_line("<true/>");2202}2203if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_downloads") == 1) {2204ent_f->store_line("<key>com.apple.security.files.downloads.read-only</key>");2205ent_f->store_line("<true/>");2206}2207if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_downloads") == 2) {2208ent_f->store_line("<key>com.apple.security.files.downloads.read-write</key>");2209ent_f->store_line("<true/>");2210}2211if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_pictures") == 1) {2212ent_f->store_line("<key>com.apple.security.files.pictures.read-only</key>");2213ent_f->store_line("<true/>");2214}2215if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_pictures") == 2) {2216ent_f->store_line("<key>com.apple.security.files.pictures.read-write</key>");2217ent_f->store_line("<true/>");2218}2219if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_music") == 1) {2220ent_f->store_line("<key>com.apple.security.files.music.read-only</key>");2221ent_f->store_line("<true/>");2222}2223if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_music") == 2) {2224ent_f->store_line("<key>com.apple.security.files.music.read-write</key>");2225ent_f->store_line("<true/>");2226}2227if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_movies") == 1) {2228ent_f->store_line("<key>com.apple.security.files.movies.read-only</key>");2229ent_f->store_line("<true/>");2230}2231if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_movies") == 2) {2232ent_f->store_line("<key>com.apple.security.files.movies.read-write</key>");2233ent_f->store_line("<true/>");2234}2235if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 1) {2236ent_f->store_line("<key>com.apple.security.files.user-selected.read-only</key>");2237ent_f->store_line("<true/>");2238}2239if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 2) {2240ent_f->store_line("<key>com.apple.security.files.user-selected.read-write</key>");2241ent_f->store_line("<true/>");2242}2243}22442245const String &additional_entitlements = p_preset->get("codesign/entitlements/additional");2246if (!additional_entitlements.is_empty()) {2247ent_f->store_line(additional_entitlements);2248}22492250ent_f->store_line("</dict>");2251ent_f->store_line("</plist>");2252} else {2253add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create entitlements file."));2254err = ERR_CANT_CREATE;2255}22562257if ((err == OK) && sandbox && (helpers.size() > 0 || shared_objects.size() > 0)) {2258ent_f = FileAccess::open(hlp_ent_path, FileAccess::WRITE);2259if (ent_f.is_valid()) {2260ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");2261ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");2262ent_f->store_line("<plist version=\"1.0\">");2263ent_f->store_line("<dict>");2264ent_f->store_line("<key>com.apple.security.app-sandbox</key>");2265ent_f->store_line("<true/>");2266ent_f->store_line("<key>com.apple.security.inherit</key>");2267ent_f->store_line("<true/>");2268ent_f->store_line("</dict>");2269ent_f->store_line("</plist>");2270} else {2271add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create helper entitlements file."));2272err = ERR_CANT_CREATE;2273}2274}2275}22762277if ((err == OK) && helpers.size() > 0) {2278Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);2279for (int i = 0; i < helpers.size(); i++) {2280String hlp_path = helpers[i];2281err = da->copy(hlp_path, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file());2282if (err == OK && sign_enabled) {2283_code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false, true);2284}2285FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755);2286#ifndef UNIX_ENABLED2287if (export_format == "app") {2288add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), "Contents/Helpers/" + hlp_path.get_file()));2289}2290#endif2291}2292}22932294if (err == OK) {2295Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);2296for (int i = 0; i < shared_objects.size(); i++) {2297String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path);2298if (shared_objects[i].target.is_empty()) {2299String path_in_app = tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file();2300err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, hlp_ent_path, true, sandbox);2301} else {2302String path_in_app = tmp_app_path_name.path_join(shared_objects[i].target);2303tmp_app_dir->make_dir_recursive(path_in_app);2304err = _copy_and_sign_files(da, src_path, path_in_app.path_join(src_path.get_file()), sign_enabled, p_preset, ent_path, hlp_ent_path, false, sandbox);2305}2306if (err != OK) {2307break;2308}2309}23102311Vector<Ref<EditorExportPlugin>> export_plugins{ EditorExport::get_singleton()->get_export_plugins() };2312for (int i = 0; i < export_plugins.size(); ++i) {2313err = _export_macos_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path, hlp_ent_path, sandbox);2314if (err != OK) {2315break;2316}2317}2318}23192320if (err == OK && sign_enabled) {2321int dist_type = p_preset->get("export/distribution_type");2322if (dist_type == 2) {2323String pprof = p_preset->get_or_env("codesign/provisioning_profile", ENV_MAC_CODESIGN_PROFILE).operator String();2324if (!pprof.is_empty()) {2325Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);2326err = da->copy(pprof, tmp_app_path_name + "/Contents/embedded.provisionprofile");2327}2328}23292330if (ep.step(TTR("Code signing bundle"), 2)) {2331return ERR_SKIP;2332}2333_code_sign(p_preset, tmp_app_path_name, ent_path, true, false);2334}23352336String noto_path = p_path;2337bool noto_enabled = (p_preset->get("notarization/notarization").operator int() > 0);2338if (export_format == "dmg") {2339// Create a DMG.2340if (err == OK) {2341if (ep.step(TTR("Making DMG"), 3)) {2342return ERR_SKIP;2343}2344err = _create_dmg(p_path, pkg_name, tmp_base_path_name);2345}2346// Sign DMG.2347if (err == OK && sign_enabled && !ad_hoc) {2348if (ep.step(TTR("Code signing DMG"), 3)) {2349return ERR_SKIP;2350}2351_code_sign(p_preset, p_path, ent_path, false, false);2352}2353} else if (export_format == "pkg") {2354// Create a Installer.2355if (err == OK) {2356if (ep.step(TTR("Making PKG installer"), 3)) {2357return ERR_SKIP;2358}2359err = _create_pkg(p_preset, p_path, tmp_app_path_name);2360}2361} else if (export_format == "zip") {2362// Create ZIP.2363if (err == OK) {2364if (ep.step(TTR("Making ZIP"), 3)) {2365return ERR_SKIP;2366}2367if (FileAccess::exists(p_path)) {2368OS::get_singleton()->move_to_trash(p_path);2369}23702371Ref<FileAccess> io_fa_dst;2372zlib_filefunc_def io_dst = zipio_create_io(&io_fa_dst);2373zipFile zip = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io_dst);23742375zip_folder_recursive(zip, tmp_base_path_name, "", pkg_name);23762377zipClose(zip, nullptr);2378}2379} else if (export_format == "app" && noto_enabled) {2380// Create temporary ZIP.2381if (err == OK) {2382noto_path = EditorPaths::get_singleton()->get_temp_dir().path_join(pkg_name + ".zip");23832384if (ep.step(TTR("Making ZIP"), 3)) {2385return ERR_SKIP;2386}2387if (FileAccess::exists(noto_path)) {2388OS::get_singleton()->move_to_trash(noto_path);2389}23902391Ref<FileAccess> io_fa_dst;2392zlib_filefunc_def io_dst = zipio_create_io(&io_fa_dst);2393zipFile zip = zipOpen2(noto_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io_dst);23942395zip_folder_recursive(zip, tmp_base_path_name, tmp_app_dir_name, pkg_name);23962397zipClose(zip, nullptr);2398}2399}24002401if (err == OK && noto_enabled) {2402if (export_format == "pkg") {2403add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("Notarization requires the app to be archived first, select the DMG or ZIP export format instead."));2404} else {2405if (ep.step(TTR("Sending archive for notarization"), 4)) {2406return ERR_SKIP;2407}2408err = _notarize(p_preset, noto_path);2409}2410}24112412if (FileAccess::exists(ent_path)) {2413print_verbose("entitlements:\n" + FileAccess::get_file_as_string(ent_path));2414}24152416if (FileAccess::exists(hlp_ent_path)) {2417print_verbose("helper entitlements:\n" + FileAccess::get_file_as_string(hlp_ent_path));2418}24192420// Clean up temporary entitlements files.2421if (FileAccess::exists(hlp_ent_path)) {2422DirAccess::remove_file_or_error(hlp_ent_path);2423}24242425// Clean up temporary .app dir and generated entitlements.2426if ((String)(p_preset->get("codesign/entitlements/custom_file")) == "") {2427tmp_app_dir->remove(ent_path);2428}2429if (export_format != "app") {2430if (tmp_app_dir->change_dir(tmp_base_path_name) == OK) {2431tmp_app_dir->erase_contents_recursive();2432tmp_app_dir->change_dir("..");2433tmp_app_dir->remove(pkg_name);2434}2435} else if (noto_path != p_path) {2436if (FileAccess::exists(noto_path)) {2437DirAccess::remove_file_or_error(noto_path);2438}2439}2440}24412442return err;2443}24442445bool EditorExportPlatformMacOS::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {2446String err;2447// Look for export templates (official templates first, then custom).2448bool dvalid = exists_export_template("macos.zip", &err);2449bool rvalid = dvalid; // Both in the same ZIP.24502451if (p_preset->get("custom_template/debug") != "") {2452dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));2453if (!dvalid) {2454err += TTR("Custom debug template not found.") + "\n";2455}2456}2457if (p_preset->get("custom_template/release") != "") {2458rvalid = FileAccess::exists(p_preset->get("custom_template/release"));2459if (!rvalid) {2460err += TTR("Custom release template not found.") + "\n";2461}2462}24632464bool valid = dvalid || rvalid;2465r_missing_templates = !valid;24662467// Check the texture formats, which vary depending on the target architecture.2468String architecture = p_preset->get("binary_format/architecture");2469if (architecture == "universal" || architecture == "x86_64") {2470if (!ResourceImporterTextureSettings::should_import_s3tc_bptc()) {2471err += TTR("Cannot export for universal or x86_64 if S3TC BPTC texture format is disabled. Enable it in the Project Settings (Rendering > Textures > VRAM Compression > Import S3TC BPTC).") + "\n";2472valid = false;2473}2474}2475if (architecture == "universal" || architecture == "arm64") {2476if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {2477err += TTR("Cannot export for universal or arm64 if ETC2 ASTC texture format is disabled. Enable it in the Project Settings (Rendering > Textures > VRAM Compression > Import ETC2 ASTC).") + "\n";2478valid = false;2479}2480}2481if (architecture != "universal" && architecture != "x86_64" && architecture != "arm64") {2482ERR_PRINT("Invalid architecture");2483}24842485if (!err.is_empty()) {2486r_error = err;2487}2488return valid;2489}24902491bool EditorExportPlatformMacOS::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {2492String err;2493bool valid = true;24942495int dist_type = p_preset->get("export/distribution_type");2496bool ad_hoc = false;2497int codesign_tool = p_preset->get("codesign/codesign");2498int notary_tool = p_preset->get("notarization/notarization");2499switch (codesign_tool) {2500case 1: { // built-in ad-hoc2501ad_hoc = true;2502} break;2503case 2: { // "rcodesign"2504ad_hoc = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty() || p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_PASS).operator String().is_empty();2505} break;2506#ifdef MACOS_ENABLED2507case 3: { // "codesign"2508ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");2509} break;2510#endif2511default: {2512};2513}25142515const String &additional_plist_content = p_preset->get("application/additional_plist_content");2516if (!additional_plist_content.is_empty()) {2517const String &plist = vformat("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"2518"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"2519"<plist version=\"1.0\">"2520"<dict>\n"2521"%s\n"2522"</dict>\n"2523"</plist>\n",2524additional_plist_content);25252526String plist_err;2527Ref<PList> plist_parser;2528plist_parser.instantiate();2529if (!plist_parser->load_string(plist, plist_err)) {2530err += TTR("Invalid additional PList content: ") + plist_err + "\n";2531valid = false;2532}2533}25342535List<ExportOption> options;2536get_export_options(&options);2537for (const EditorExportPlatform::ExportOption &E : options) {2538if (get_export_option_visibility(p_preset.ptr(), E.option.name)) {2539String warn = get_export_option_warning(p_preset.ptr(), E.option.name);2540if (!warn.is_empty()) {2541err += warn + "\n";2542if (E.required) {2543valid = false;2544}2545}2546}2547}25482549if (dist_type != 2) {2550if (notary_tool > 0) {2551if (notary_tool == 2 || notary_tool == 3) {2552if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) {2553err += TTR("Notarization: Xcode command line tools are not installed.") + "\n";2554valid = false;2555}2556} else if (notary_tool == 1) {2557String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String();2558if (rcodesign.is_empty()) {2559err += TTR("Notarization: rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).") + "\n";2560valid = false;2561}2562}2563} else {2564err += TTR("Warning: Notarization is disabled. The exported project will be blocked by Gatekeeper if it's downloaded from an unknown source.") + "\n";2565if (codesign_tool == 0) {2566err += TTR("Code signing is disabled. The exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n";2567}2568}2569}25702571if (codesign_tool > 0) {2572if (ad_hoc) {2573err += TTR("Code signing: Using ad-hoc signature. The exported project will be blocked by Gatekeeper") + "\n";2574}2575if (codesign_tool == 3) {2576if (!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) {2577err += TTR("Code signing: Xcode command line tools are not installed.") + "\n";2578valid = false;2579}2580} else if (codesign_tool == 2) {2581String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String();2582if (rcodesign.is_empty()) {2583err += TTR("Code signing: rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).") + "\n";2584valid = false;2585}2586}2587}25882589if (!err.is_empty()) {2590r_error = err;2591}2592return valid;2593}25942595Ref<Texture2D> EditorExportPlatformMacOS::get_run_icon() const {2596return run_icon;2597}25982599bool EditorExportPlatformMacOS::poll_export() {2600Ref<EditorExportPreset> preset;26012602for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {2603Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i);2604if (ep->is_runnable() && ep->get_platform() == this) {2605preset = ep;2606break;2607}2608}26092610int prev = menu_options;2611menu_options = (preset.is_valid() && preset->get("ssh_remote_deploy/enabled").operator bool());2612if (ssh_pid != 0 || !cleanup_commands.is_empty()) {2613if (menu_options == 0) {2614cleanup();2615} else {2616menu_options += 1;2617}2618}2619return menu_options != prev;2620}26212622Ref<Texture2D> EditorExportPlatformMacOS::get_option_icon(int p_index) const {2623if (p_index == 1) {2624return stop_icon;2625} else {2626return EditorExportPlatform::get_option_icon(p_index);2627}2628}26292630int EditorExportPlatformMacOS::get_options_count() const {2631return menu_options;2632}26332634String EditorExportPlatformMacOS::get_option_label(int p_index) const {2635return (p_index) ? TTR("Stop and uninstall") : TTR("Run on remote macOS system");2636}26372638String EditorExportPlatformMacOS::get_option_tooltip(int p_index) const {2639return (p_index) ? TTR("Stop and uninstall running project from the remote system") : TTR("Run exported project on remote macOS system");2640}26412642void EditorExportPlatformMacOS::cleanup() {2643if (ssh_pid != 0 && OS::get_singleton()->is_process_running(ssh_pid)) {2644print_line("Terminating connection...");2645OS::get_singleton()->kill(ssh_pid);2646OS::get_singleton()->delay_usec(1000);2647}26482649if (!cleanup_commands.is_empty()) {2650print_line("Stopping and deleting previous version...");2651for (const SSHCleanupCommand &cmd : cleanup_commands) {2652if (cmd.wait) {2653ssh_run_on_remote(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);2654} else {2655ssh_run_on_remote_no_wait(cmd.host, cmd.port, cmd.ssh_args, cmd.cmd_args);2656}2657}2658}2659ssh_pid = 0;2660cleanup_commands.clear();2661}26622663Error EditorExportPlatformMacOS::run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) {2664cleanup();2665if (p_device) { // Stop command, cleanup only.2666return OK;2667}26682669EditorProgress ep("run", TTR("Running..."), 5);26702671const String dest = EditorPaths::get_singleton()->get_temp_dir().path_join("macos");2672Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);2673if (!da->dir_exists(dest)) {2674Error err = da->make_dir_recursive(dest);2675if (err != OK) {2676EditorNode::get_singleton()->show_warning(TTR("Could not create temp directory:") + "\n" + dest);2677return err;2678}2679}26802681String pkg_name;2682if (String(get_project_setting(p_preset, "application/config/name")) != "") {2683pkg_name = String(get_project_setting(p_preset, "application/config/name"));2684} else {2685pkg_name = "Unnamed";2686}2687pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);26882689String host = p_preset->get("ssh_remote_deploy/host").operator String();2690String port = p_preset->get("ssh_remote_deploy/port").operator String();2691if (port.is_empty()) {2692port = "22";2693}2694Vector<String> extra_args_ssh = p_preset->get("ssh_remote_deploy/extra_args_ssh").operator String().split(" ", false);2695Vector<String> extra_args_scp = p_preset->get("ssh_remote_deploy/extra_args_scp").operator String().split(" ", false);26962697const String basepath = dest.path_join("tmp_macos_export");26982699#define CLEANUP_AND_RETURN(m_err) \2700{ \2701if (da->file_exists(basepath + ".zip")) { \2702da->remove(basepath + ".zip"); \2703} \2704if (da->file_exists(basepath + "_start.sh")) { \2705da->remove(basepath + "_start.sh"); \2706} \2707if (da->file_exists(basepath + "_clean.sh")) { \2708da->remove(basepath + "_clean.sh"); \2709} \2710return m_err; \2711} \2712((void)0)27132714if (ep.step(TTR("Exporting project..."), 1)) {2715return ERR_SKIP;2716}2717Error err = export_project(p_preset, true, basepath + ".zip", p_debug_flags);2718if (err != OK) {2719DirAccess::remove_file_or_error(basepath + ".zip");2720return err;2721}27222723String cmd_args;2724{2725Vector<String> cmd_args_list = gen_export_flags(p_debug_flags);2726for (int i = 0; i < cmd_args_list.size(); i++) {2727if (i != 0) {2728cmd_args += " ";2729}2730cmd_args += cmd_args_list[i];2731}2732}27332734const bool use_remote = p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT);2735int dbg_port = EDITOR_GET("network/debug/remote_port");27362737print_line("Creating temporary directory...");2738ep.step(TTR("Creating temporary directory..."), 2);2739String temp_dir;2740err = ssh_run_on_remote(host, port, extra_args_ssh, "mktemp -d", &temp_dir);2741if (err != OK || temp_dir.is_empty()) {2742CLEANUP_AND_RETURN(err);2743}27442745print_line("Uploading archive...");2746ep.step(TTR("Uploading archive..."), 3);2747err = ssh_push_to_remote(host, port, extra_args_scp, basepath + ".zip", temp_dir);2748if (err != OK) {2749CLEANUP_AND_RETURN(err);2750}27512752{2753String run_script = p_preset->get("ssh_remote_deploy/run_script");2754run_script = run_script.replace("{temp_dir}", temp_dir);2755run_script = run_script.replace("{archive_name}", basepath.get_file() + ".zip");2756run_script = run_script.replace("{exe_name}", pkg_name);2757run_script = run_script.replace("{cmd_args}", cmd_args);27582759Ref<FileAccess> f = FileAccess::open(basepath + "_start.sh", FileAccess::WRITE);2760if (f.is_null()) {2761CLEANUP_AND_RETURN(err);2762}27632764f->store_string(run_script);2765}27662767{2768String clean_script = p_preset->get("ssh_remote_deploy/cleanup_script");2769clean_script = clean_script.replace("{temp_dir}", temp_dir);2770clean_script = clean_script.replace("{archive_name}", basepath.get_file() + ".zip");2771clean_script = clean_script.replace("{exe_name}", pkg_name);2772clean_script = clean_script.replace("{cmd_args}", cmd_args);27732774Ref<FileAccess> f = FileAccess::open(basepath + "_clean.sh", FileAccess::WRITE);2775if (f.is_null()) {2776CLEANUP_AND_RETURN(err);2777}27782779f->store_string(clean_script);2780}27812782print_line("Uploading scripts...");2783ep.step(TTR("Uploading scripts..."), 4);2784err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_start.sh", temp_dir);2785if (err != OK) {2786CLEANUP_AND_RETURN(err);2787}2788err = ssh_run_on_remote(host, port, extra_args_ssh, vformat("chmod +x \"%s/%s\"", temp_dir, basepath.get_file() + "_start.sh"));2789if (err != OK || temp_dir.is_empty()) {2790CLEANUP_AND_RETURN(err);2791}2792err = ssh_push_to_remote(host, port, extra_args_scp, basepath + "_clean.sh", temp_dir);2793if (err != OK) {2794CLEANUP_AND_RETURN(err);2795}2796err = ssh_run_on_remote(host, port, extra_args_ssh, vformat("chmod +x \"%s/%s\"", temp_dir, basepath.get_file() + "_clean.sh"));2797if (err != OK || temp_dir.is_empty()) {2798CLEANUP_AND_RETURN(err);2799}28002801print_line("Starting project...");2802ep.step(TTR("Starting project..."), 5);2803err = ssh_run_on_remote_no_wait(host, port, extra_args_ssh, vformat("\"%s/%s\"", temp_dir, basepath.get_file() + "_start.sh"), &ssh_pid, (use_remote) ? dbg_port : -1);2804if (err != OK) {2805CLEANUP_AND_RETURN(err);2806}28072808cleanup_commands.clear();2809cleanup_commands.push_back(SSHCleanupCommand(host, port, extra_args_ssh, vformat("\"%s/%s\"", temp_dir, basepath.get_file() + "_clean.sh")));28102811print_line("Project started.");28122813CLEANUP_AND_RETURN(OK);2814#undef CLEANUP_AND_RETURN2815}28162817void EditorExportPlatformMacOS::initialize() {2818if (EditorNode::get_singleton()) {2819Ref<Image> img = memnew(Image);2820const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);28212822ImageLoaderSVG::create_image_from_string(img, _macos_logo_svg, EDSCALE, upsample, false);2823logo = ImageTexture::create_from_image(img);28242825ImageLoaderSVG::create_image_from_string(img, _macos_run_icon_svg, EDSCALE, upsample, false);2826run_icon = ImageTexture::create_from_image(img);28272828Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();2829if (theme.is_valid()) {2830stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons));2831} else {2832stop_icon.instantiate();2833}2834}2835}283628372838