godot/platform/macos/export/export_plugin.cpp

1922 lines
84 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*************************************************************************/
/* export_plugin.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#include "export_plugin.h"
#include "codesign.h"
#include "lipo.h"
#include "macho.h"
#include "core/string/translation.h"
#include "editor/editor_node.h"
#include "editor/editor_paths.h"
#include "modules/modules_enabled.gen.h" // For regex.
void EditorExportPlatformMacOS::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {
if (p_preset->get("texture_format/s3tc")) {
r_features->push_back("s3tc");
}
if (p_preset->get("texture_format/etc")) {
r_features->push_back("etc");
}
if (p_preset->get("texture_format/etc2")) {
r_features->push_back("etc2");
}
r_features->push_back(p_preset->get("binary_format/architecture"));
}
bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option, const HashMap<StringName, Variant> &p_options) const {
// Hide irrelevant code signing options.
if (p_preset) {
int codesign_tool = p_preset->get("codesign/codesign");
switch (codesign_tool) {
case 1: { // built-in ad-hoc
if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options") {
return false;
}
} break;
case 2: { // "rcodesign"
if (p_option == "codesign/identity") {
return false;
}
} break;
#ifdef MACOS_ENABLED
case 3: { // "codesign"
if (p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password") {
return false;
}
} break;
#endif
default: { // disabled
if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options" || p_option.begins_with("codesign/entitlements")) {
return false;
}
} break;
}
// Hide irrelevant notarization options.
int notary_tool = p_preset->get("notarization/notarization");
switch (notary_tool) {
case 1: { // "rcodesign"
if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password" || p_option == "notarization/apple_team_id") {
return false;
}
} break;
case 2: { // "altool"
// All options are visible.
} break;
default: { // disabled
if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password" || p_option == "notarization/apple_team_id" || p_option == "notarization/api_uuid" || p_option == "notarization/api_key") {
return false;
}
} break;
}
}
// These entitlements are required to run managed code, and are always enabled in Mono builds.
if (Engine::get_singleton()->has_singleton("GodotSharp")) {
if (p_option == "codesign/entitlements/allow_jit_code_execution" || p_option == "codesign/entitlements/allow_unsigned_executable_memory" || p_option == "codesign/entitlements/allow_dyld_environment_variables") {
return false;
}
}
return true;
}
void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options) {
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "binary_format/architecture", PROPERTY_HINT_ENUM, "universal,x86_64,arm64", PROPERTY_USAGE_STORAGE), "universal"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "debug/export_console_script", PROPERTY_HINT_ENUM, "No,Debug Only,Debug and Release"), 1));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/icon", PROPERTY_HINT_FILE, "*.png,*.icns"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/signature"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/app_category", PROPERTY_HINT_ENUM, "Business,Developer-tools,Education,Entertainment,Finance,Games,Action-games,Adventure-games,Arcade-games,Board-games,Card-games,Casino-games,Dice-games,Educational-games,Family-games,Kids-games,Music-games,Puzzle-games,Racing-games,Role-playing-games,Simulation-games,Sports-games,Strategy-games,Trivia-games,Word-games,Graphics-design,Healthcare-fitness,Lifestyle,Medical,Music,News,Photography,Productivity,Reference,Social-networking,Sports,Travel,Utilities,Video,Weather"), "Games"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version"), "1.0"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "application/copyright_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "display/high_res"), false));
#ifdef MACOS_ENABLED
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/codesign", PROPERTY_HINT_ENUM, "Disabled,Built-in (ad-hoc only),PyOxidizer rcodesign,Xcode codesign"), 3, true));
#else
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/codesign", PROPERTY_HINT_ENUM, "Disabled,Built-in (ad-hoc only),PyOxidizer rcodesign"), 1, true));
#endif
// "codesign" only options:
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "Type: Name (ID)"), ""));
// "rcodesign" only options:
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_file", PROPERTY_HINT_GLOBAL_FILE, "*.pfx,*.p12"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_password", PROPERTY_HINT_PASSWORD), ""));
// "codesign" and "rcodesign" only options:
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/custom_file", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_jit_code_execution"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_unsigned_executable_memory"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_dyld_environment_variables"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/disable_library_validation"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/audio_input"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/camera"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/location"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/address_book"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/calendars"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/photos_library"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/apple_events"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/debugging"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/enabled"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/network_server"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/network_client"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/device_usb"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/device_bluetooth"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_downloads", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_pictures", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_music", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array()));
r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray()));
#ifdef MACOS_ENABLED
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "notarization/notarization", PROPERTY_HINT_ENUM, "Disabled,PyOxidizer rcodesign,Xcode altool"), 0, true));
#else
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "notarization/notarization", PROPERTY_HINT_ENUM, "Disabled,PyOxidizer rcodesign"), 0, true));
#endif
// "altool" only options:
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Apple ID email"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_password", PROPERTY_HINT_PASSWORD, "Enable two-factor authentication and provide app-specific password"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_team_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide team ID if your Apple ID belongs to multiple teams"), ""));
// "altool" and "rcodesign" only options:
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_uuid", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect issuer ID"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_key", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect API key ID"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/microphone_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/camera_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/location_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the location information"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/location_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/address_book_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the address book"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/address_book_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/calendar_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the calendar"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/calendar_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photos_library_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the photo library"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photos_library_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/desktop_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Desktop folder"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/desktop_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/documents_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Documents folder"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/documents_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/downloads_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Downloads folder"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/downloads_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/network_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use network volumes"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/network_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/removable_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use removable volumes"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/removable_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "texture_format/s3tc"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "texture_format/etc"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "texture_format/etc2"), false));
}
void _rgba8_to_packbits_encode(int p_ch, int p_size, Vector<uint8_t> &p_source, Vector<uint8_t> &p_dest) {
int src_len = p_size * p_size;
Vector<uint8_t> result;
result.resize(src_len * 1.25); //temp vector for rle encoded data, make it 25% larger for worst case scenario
int res_size = 0;
uint8_t buf[128];
int buf_size = 0;
int i = 0;
while (i < src_len) {
uint8_t cur = p_source.ptr()[i * 4 + p_ch];
if (i < src_len - 2) {
if ((p_source.ptr()[(i + 1) * 4 + p_ch] == cur) && (p_source.ptr()[(i + 2) * 4 + p_ch] == cur)) {
if (buf_size > 0) {
result.write[res_size++] = (uint8_t)(buf_size - 1);
memcpy(&result.write[res_size], &buf, buf_size);
res_size += buf_size;
buf_size = 0;
}
uint8_t lim = i + 130 >= src_len ? src_len - i - 1 : 130;
bool hit_lim = true;
for (int j = 3; j <= lim; j++) {
if (p_source.ptr()[(i + j) * 4 + p_ch] != cur) {
hit_lim = false;
i = i + j - 1;
result.write[res_size++] = (uint8_t)(j - 3 + 0x80);
result.write[res_size++] = cur;
break;
}
}
if (hit_lim) {
result.write[res_size++] = (uint8_t)(lim - 3 + 0x80);
result.write[res_size++] = cur;
i = i + lim;
}
} else {
buf[buf_size++] = cur;
if (buf_size == 128) {
result.write[res_size++] = (uint8_t)(buf_size - 1);
memcpy(&result.write[res_size], &buf, buf_size);
res_size += buf_size;
buf_size = 0;
}
}
} else {
buf[buf_size++] = cur;
result.write[res_size++] = (uint8_t)(buf_size - 1);
memcpy(&result.write[res_size], &buf, buf_size);
res_size += buf_size;
buf_size = 0;
}
i++;
}
int ofs = p_dest.size();
p_dest.resize(p_dest.size() + res_size);
memcpy(&p_dest.write[ofs], result.ptr(), res_size);
}
void EditorExportPlatformMacOS::_make_icon(const Ref<Image> &p_icon, Vector<uint8_t> &p_data) {
Ref<ImageTexture> it = memnew(ImageTexture);
Vector<uint8_t> data;
data.resize(8);
data.write[0] = 'i';
data.write[1] = 'c';
data.write[2] = 'n';
data.write[3] = 's';
struct MacOSIconInfo {
const char *name;
const char *mask_name;
bool is_png;
int size;
};
static const MacOSIconInfo icon_infos[] = {
{ "ic10", "", true, 1024 }, //1024×1024 32-bit PNG and 512×512@2x 32-bit "retina" PNG
{ "ic09", "", true, 512 }, //512×512 32-bit PNG
{ "ic14", "", true, 512 }, //256×256@2x 32-bit "retina" PNG
{ "ic08", "", true, 256 }, //256×256 32-bit PNG
{ "ic13", "", true, 256 }, //128×128@2x 32-bit "retina" PNG
{ "ic07", "", true, 128 }, //128×128 32-bit PNG
{ "ic12", "", true, 64 }, //32×32@2× 32-bit "retina" PNG
{ "ic11", "", true, 32 }, //16×16@2× 32-bit "retina" PNG
{ "il32", "l8mk", false, 32 }, //32×32 24-bit RLE + 8-bit uncompressed mask
{ "is32", "s8mk", false, 16 } //16×16 24-bit RLE + 8-bit uncompressed mask
};
for (uint64_t i = 0; i < (sizeof(icon_infos) / sizeof(icon_infos[0])); ++i) {
Ref<Image> copy = p_icon; // does this make sense? doesn't this just increase the reference count instead of making a copy? Do we even need a copy?
copy->convert(Image::FORMAT_RGBA8);
copy->resize(icon_infos[i].size, icon_infos[i].size);
if (icon_infos[i].is_png) {
// Encode PNG icon.
it->set_image(copy);
String path = EditorPaths::get_singleton()->get_cache_dir().path_join("icon.png");
ResourceSaver::save(it, path);
{
Ref<FileAccess> f = FileAccess::open(path, FileAccess::READ);
if (f.is_null()) {
// Clean up generated file.
DirAccess::remove_file_or_error(path);
add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not open icon file \"%s\"."), path));
return;
}
int ofs = data.size();
uint64_t len = f->get_length();
data.resize(data.size() + len + 8);
f->get_buffer(&data.write[ofs + 8], len);
len += 8;
len = BSWAP32(len);
memcpy(&data.write[ofs], icon_infos[i].name, 4);
encode_uint32(len, &data.write[ofs + 4]);
}
// Clean up generated file.
DirAccess::remove_file_or_error(path);
} else {
Vector<uint8_t> src_data = copy->get_data();
//encode 24bit RGB RLE icon
{
int ofs = data.size();
data.resize(data.size() + 8);
_rgba8_to_packbits_encode(0, icon_infos[i].size, src_data, data); // encode R
_rgba8_to_packbits_encode(1, icon_infos[i].size, src_data, data); // encode G
_rgba8_to_packbits_encode(2, icon_infos[i].size, src_data, data); // encode B
int len = data.size() - ofs;
len = BSWAP32(len);
memcpy(&data.write[ofs], icon_infos[i].name, 4);
encode_uint32(len, &data.write[ofs + 4]);
}
//encode 8bit mask uncompressed icon
{
int ofs = data.size();
int len = copy->get_width() * copy->get_height();
data.resize(data.size() + len + 8);
for (int j = 0; j < len; j++) {
data.write[ofs + 8 + j] = src_data.ptr()[j * 4 + 3];
}
len += 8;
len = BSWAP32(len);
memcpy(&data.write[ofs], icon_infos[i].mask_name, 4);
encode_uint32(len, &data.write[ofs + 4]);
}
}
}
uint32_t total_len = data.size();
total_len = BSWAP32(total_len);
encode_uint32(total_len, &data.write[4]);
p_data = data;
}
void EditorExportPlatformMacOS::_fix_plist(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &plist, const String &p_binary) {
String str;
String strnew;
str.parse_utf8((const char *)plist.ptr(), plist.size());
Vector<String> lines = str.split("\n");
for (int i = 0; i < lines.size(); i++) {
if (lines[i].find("$binary") != -1) {
strnew += lines[i].replace("$binary", p_binary) + "\n";
} else if (lines[i].find("$name") != -1) {
strnew += lines[i].replace("$name", ProjectSettings::get_singleton()->get("application/config/name")) + "\n";
} else if (lines[i].find("$bundle_identifier") != -1) {
strnew += lines[i].replace("$bundle_identifier", p_preset->get("application/bundle_identifier")) + "\n";
} else if (lines[i].find("$short_version") != -1) {
strnew += lines[i].replace("$short_version", p_preset->get("application/short_version")) + "\n";
} else if (lines[i].find("$version") != -1) {
strnew += lines[i].replace("$version", p_preset->get("application/version")) + "\n";
} else if (lines[i].find("$signature") != -1) {
strnew += lines[i].replace("$signature", p_preset->get("application/signature")) + "\n";
} else if (lines[i].find("$app_category") != -1) {
String cat = p_preset->get("application/app_category");
strnew += lines[i].replace("$app_category", cat.to_lower()) + "\n";
} else if (lines[i].find("$copyright") != -1) {
strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n";
} else if (lines[i].find("$highres") != -1) {
strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "\t<true/>" : "\t<false/>") + "\n";
} else if (lines[i].find("$usage_descriptions") != -1) {
String descriptions;
if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
descriptions += "\t<key>NSMicrophoneUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/microphone_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/camera_usage_description")).is_empty()) {
descriptions += "\t<key>NSCameraUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/camera_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/location_usage_description")).is_empty()) {
descriptions += "\t<key>NSLocationUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/location_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) {
descriptions += "\t<key>NSContactsUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/address_book_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) {
descriptions += "\t<key>NSCalendarsUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/calendar_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) {
descriptions += "\t<key>NSPhotoLibraryUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/photos_library_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/desktop_folder_usage_description")).is_empty()) {
descriptions += "\t<key>NSDesktopFolderUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/desktop_folder_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/documents_folder_usage_description")).is_empty()) {
descriptions += "\t<key>NSDocumentsFolderUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/documents_folder_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/downloads_folder_usage_description")).is_empty()) {
descriptions += "\t<key>NSDownloadsFolderUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/downloads_folder_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/network_volumes_usage_description")).is_empty()) {
descriptions += "\t<key>NSNetworkVolumesUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/network_volumes_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/removable_volumes_usage_description")).is_empty()) {
descriptions += "\t<key>NSRemovableVolumesUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/removable_volumes_usage_description") + "</string>\n";
}
if (!descriptions.is_empty()) {
strnew += lines[i].replace("$usage_descriptions", descriptions);
}
} else {
strnew += lines[i] + "\n";
}
}
CharString cs = strnew.utf8();
plist.resize(cs.size() - 1);
for (int i = 0; i < cs.size() - 1; i++) {
plist.write[i] = cs[i];
}
}
/**
* If we're running the macOS version of the Godot editor we'll:
* - export our application bundle to a temporary folder
* - attempt to code sign it
* - and then wrap it up in a DMG
*/
Error EditorExportPlatformMacOS::_notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path) {
int notary_tool = p_preset->get("notarization/notarization");
switch (notary_tool) {
case 1: { // "rcodesign"
print_verbose("using rcodesign notarization...");
String rcodesign = EditorSettings::get_singleton()->get("export/macos/rcodesign").operator String();
if (rcodesign.is_empty()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign)."));
return Error::FAILED;
}
List<String> args;
args.push_back("notary-submit");
if (p_preset->get("notarization/api_uuid") == "") {
add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect issuer ID name not specified."));
return Error::FAILED;
}
if (p_preset->get("notarization/api_key") == "") {
add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified."));
return Error::FAILED;
}
args.push_back("--api-issuer");
args.push_back(p_preset->get("notarization/api_uuid"));
args.push_back("--api-key");
args.push_back(p_preset->get("notarization/api_key"));
args.push_back(p_path);
String str;
int exitcode = 0;
Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true);
if (err != OK) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start rcodesign executable."));
return err;
}
int rq_offset = str.find("created submission ID:");
if (exitcode != 0 || rq_offset == -1) {
print_line("rcodesign (" + p_path + "):\n" + str);
add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details."));
return Error::FAILED;
} else {
print_verbose("rcodesign (" + p_path + "):\n" + str);
int next_nl = str.find("\n", rq_offset);
String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 14, -1) : str.substr(rq_offset + 14, next_nl - rq_offset - 14);
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid));
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour."));
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check progress manually by opening a Terminal and running the following command:"));
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign notary-log --api-issuer <api uuid> --api-key <api key> <request uuid>\"");
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):"));
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign staple <app path>\"");
}
} break;
#ifdef MACOS_ENABLED
case 2: { // "altool"
print_verbose("using altool notarization...");
if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Xcode command line tools are not installed."));
return Error::FAILED;
}
List<String> args;
args.push_back("altool");
args.push_back("--notarize-app");
args.push_back("--primary-bundle-id");
args.push_back(p_preset->get("application/bundle_identifier"));
if (p_preset->get("notarization/apple_id_name") == "" && p_preset->get("notarization/api_uuid") == "") {
add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified."));
return Error::FAILED;
}
if (p_preset->get("notarization/apple_id_name") != "" && p_preset->get("notarization/api_uuid") != "") {
add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time."));
return Error::FAILED;
}
if (p_preset->get("notarization/apple_id_name") != "") {
if (p_preset->get("notarization/apple_id_password") == "") {
add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Apple ID password not specified."));
return Error::FAILED;
}
args.push_back("--username");
args.push_back(p_preset->get("notarization/apple_id_name"));
args.push_back("--password");
args.push_back(p_preset->get("notarization/apple_id_password"));
} else {
if (p_preset->get("notarization/api_key") == "") {
add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified."));
return Error::FAILED;
}
args.push_back("--apiIssuer");
args.push_back(p_preset->get("notarization/api_uuid"));
args.push_back("--apiKey");
args.push_back(p_preset->get("notarization/api_key"));
}
args.push_back("--type");
args.push_back("osx");
if (p_preset->get("notarization/apple_team_id")) {
args.push_back("--asc-provider");
args.push_back(p_preset->get("notarization/apple_team_id"));
}
args.push_back("--file");
args.push_back(p_path);
String str;
int exitcode = 0;
Error err = OS::get_singleton()->execute("xcrun", args, &str, &exitcode, true);
if (err != OK) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start xcrun executable."));
return err;
}
int rq_offset = str.find("RequestUUID");
if (exitcode != 0 || rq_offset == -1) {
print_line("xcrun altool (" + p_path + "):\n" + str);
add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details."));
return Error::FAILED;
} else {
print_verbose("xcrun altool (" + p_path + "):\n" + str);
int next_nl = str.find("\n", rq_offset);
String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 14, -1) : str.substr(rq_offset + 14, next_nl - rq_offset - 14);
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid));
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour. When the process is completed, you'll receive an email."));
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check progress manually by opening a Terminal and running the following command:"));
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\"");
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):"));
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun stapler staple <app path>\"");
}
} break;
#endif
default: {
};
}
return OK;
}
Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn) {
int codesign_tool = p_preset->get("codesign/codesign");
switch (codesign_tool) {
case 1: { // built-in ad-hoc
print_verbose("using built-in codesign...");
#ifdef MODULE_REGEX_ENABLED
String error_msg;
Error err = CodeSign::codesign(false, true, p_path, p_ent_path, error_msg);
if (err != OK) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Built-in CodeSign failed with error \"%s\"."), error_msg));
return Error::FAILED;
}
#else
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Built-in CodeSign require regex module."));
#endif
} break;
case 2: { // "rcodesign"
print_verbose("using rcodesign codesign...");
String rcodesign = EditorSettings::get_singleton()->get("export/macos/rcodesign").operator String();
if (rcodesign.is_empty()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xrcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign)."));
return Error::FAILED;
}
List<String> args;
args.push_back("sign");
if (p_path.get_extension() != "dmg") {
args.push_back("--entitlements-xml-path");
args.push_back(p_ent_path);
}
String certificate_file = p_preset->get("codesign/certificate_file");
String certificate_pass = p_preset->get("codesign/certificate_password");
if (!certificate_file.is_empty() && !certificate_file.is_empty()) {
args.push_back("--p12-file");
args.push_back(certificate_file);
args.push_back("--p12-password");
args.push_back(certificate_pass);
}
args.push_back("-v"); /* provide some more feedback */
args.push_back(p_path);
String str;
int exitcode = 0;
Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true);
if (err != OK) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start rcodesign executable."));
return err;
}
if (exitcode != 0) {
print_line("rcodesign (" + p_path + "):\n" + str);
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details."));
return Error::FAILED;
} else {
print_verbose("rcodesign (" + p_path + "):\n" + str);
}
} break;
#ifdef MACOS_ENABLED
case 3: { // "codesign"
print_verbose("using xcode codesign...");
if (!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xcode command line tools are not installed."));
return Error::FAILED;
}
bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");
List<String> args;
if (!ad_hoc) {
args.push_back("--timestamp");
args.push_back("--options");
args.push_back("runtime");
}
if (p_path.get_extension() != "dmg") {
args.push_back("--entitlements");
args.push_back(p_ent_path);
}
PackedStringArray user_args = p_preset->get("codesign/custom_options");
for (int i = 0; i < user_args.size(); i++) {
String user_arg = user_args[i].strip_edges();
if (!user_arg.is_empty()) {
args.push_back(user_arg);
}
}
args.push_back("-s");
if (ad_hoc) {
args.push_back("-");
} else {
args.push_back(p_preset->get("codesign/identity"));
}
args.push_back("-v"); /* provide some more feedback */
args.push_back("-f");
args.push_back(p_path);
String str;
int exitcode = 0;
Error err = OS::get_singleton()->execute("codesign", args, &str, nullptr, true);
if (err != OK) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start codesign executable, make sure Xcode command line tools are installed."));
return err;
}
if (exitcode != 0) {
print_line("codesign (" + p_path + "):\n" + str);
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details."));
return Error::FAILED;
} else {
print_verbose("codesign (" + p_path + "):\n" + str);
}
} break;
#endif
default: {
};
}
return OK;
}
Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path,
const String &p_ent_path, bool p_should_error_on_non_code) {
#ifdef MACOS_ENABLED
static Vector<String> extensions_to_sign;
if (extensions_to_sign.is_empty()) {
extensions_to_sign.push_back("dylib");
extensions_to_sign.push_back("framework");
extensions_to_sign.push_back("");
}
Error dir_access_error;
Ref<DirAccess> dir_access{ DirAccess::open(p_path, &dir_access_error) };
if (dir_access_error != OK) {
return dir_access_error;
}
dir_access->list_dir_begin();
String current_file{ dir_access->get_next() };
while (!current_file.is_empty()) {
String current_file_path{ p_path.path_join(current_file) };
if (current_file == ".." || current_file == ".") {
current_file = dir_access->get_next();
continue;
}
if (extensions_to_sign.find(current_file.get_extension()) > -1) {
Error code_sign_error{ _code_sign(p_preset, current_file_path, p_ent_path, false) };
if (code_sign_error != OK) {
return code_sign_error;
}
if (is_executable(current_file_path)) {
// chmod with 0755 if the file is executable.
FileAccess::set_unix_permissions(current_file_path, 0755);
}
} else if (dir_access->current_is_dir()) {
Error code_sign_error{ _code_sign_directory(p_preset, current_file_path, p_ent_path, p_should_error_on_non_code) };
if (code_sign_error != OK) {
return code_sign_error;
}
} else if (p_should_error_on_non_code) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign file %s."), current_file));
return Error::FAILED;
}
current_file = dir_access->get_next();
}
#endif
return OK;
}
Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path,
const String &p_in_app_path, bool p_sign_enabled,
const Ref<EditorExportPreset> &p_preset, const String &p_ent_path,
bool p_should_error_on_non_code_sign) {
static Vector<String> extensions_to_sign;
if (extensions_to_sign.is_empty()) {
extensions_to_sign.push_back("dylib");
extensions_to_sign.push_back("framework");
extensions_to_sign.push_back("");
}
Error err{ OK };
if (dir_access->dir_exists(p_src_path)) {
#ifndef UNIX_ENABLED
add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Relative symlinks are not supported, exported \"%s\" might be broken!"), p_src_path.get_file()));
#endif
print_verbose("export framework: " + p_src_path + " -> " + p_in_app_path);
err = dir_access->make_dir_recursive(p_in_app_path);
if (err == OK) {
err = dir_access->copy_dir(p_src_path, p_in_app_path, -1, true);
}
} else {
print_verbose("export dylib: " + p_src_path + " -> " + p_in_app_path);
err = dir_access->copy(p_src_path, p_in_app_path);
}
if (err == OK && p_sign_enabled) {
if (dir_access->dir_exists(p_src_path) && p_src_path.get_extension().is_empty()) {
// If it is a directory, find and sign all dynamic libraries.
err = _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_should_error_on_non_code_sign);
} else {
if (extensions_to_sign.find(p_in_app_path.get_extension()) > -1) {
err = _code_sign(p_preset, p_in_app_path, p_ent_path, false);
}
if (is_executable(p_in_app_path)) {
// chmod with 0755 if the file is executable.
FileAccess::set_unix_permissions(p_in_app_path, 0755);
}
}
}
return err;
}
Error EditorExportPlatformMacOS::_export_macos_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin,
const String &p_app_path_name, Ref<DirAccess> &dir_access,
bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset,
const String &p_ent_path) {
Error error{ OK };
const Vector<String> &macos_plugins{ p_editor_export_plugin->get_macos_plugin_files() };
for (int i = 0; i < macos_plugins.size(); ++i) {
String src_path{ ProjectSettings::get_singleton()->globalize_path(macos_plugins[i]) };
String path_in_app{ p_app_path_name + "/Contents/PlugIns/" + src_path.get_file() };
error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, false);
if (error != OK) {
break;
}
}
return error;
}
Error EditorExportPlatformMacOS::_create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name) {
List<String> args;
if (FileAccess::exists(p_dmg_path)) {
OS::get_singleton()->move_to_trash(p_dmg_path);
}
args.push_back("create");
args.push_back(p_dmg_path);
args.push_back("-volname");
args.push_back(p_pkg_name);
args.push_back("-fs");
args.push_back("HFS+");
args.push_back("-srcfolder");
args.push_back(p_app_path_name);
String str;
Error err = OS::get_singleton()->execute("hdiutil", args, &str, nullptr, true);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("Could not start hdiutil executable."));
return err;
}
print_verbose("hdiutil returned: " + str);
if (str.find("create failed") != -1) {
if (str.find("File exists") != -1) {
add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed - file exists."));
} else {
add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed."));
}
return FAILED;
}
return OK;
}
bool EditorExportPlatformMacOS::is_shbang(const String &p_path) const {
Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("Can't open file: \"%s\".", p_path));
uint16_t magic = fb->get_16();
return (magic == 0x2123);
}
bool EditorExportPlatformMacOS::is_executable(const String &p_path) const {
return MachO::is_macho(p_path) || LipO::is_lipo(p_path) || is_shbang(p_path);
}
Error EditorExportPlatformMacOS::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) {
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
if (f.is_null()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path));
return ERR_CANT_CREATE;
}
f->store_line("#!/bin/sh");
f->store_line("echo -ne '\\033c\\033]0;" + p_app_name + "\\a'");
f->store_line("");
f->store_line("function app_realpath() {");
f->store_line(" SOURCE=$1");
f->store_line(" while [ -h \"$SOURCE\" ]; do");
f->store_line(" DIR=$(dirname \"$SOURCE\")");
f->store_line(" SOURCE=$(readlink \"$SOURCE\")");
f->store_line(" [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE");
f->store_line(" done");
f->store_line(" echo \"$( cd -P \"$( dirname \"$SOURCE\" )\" >/dev/null 2>&1 && pwd )\"");
f->store_line("}");
f->store_line("");
f->store_line("BASE_PATH=\"$(app_realpath \"${BASH_SOURCE[0]}\")\"");
f->store_line("\"$BASE_PATH/" + p_pkg_name + "\" \"$@\"");
f->store_line("");
return OK;
}
Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
String src_pkg_name;
EditorProgress ep("export", "Exporting for macOS", 3, true);
if (p_debug) {
src_pkg_name = p_preset->get("custom_template/debug");
} else {
src_pkg_name = p_preset->get("custom_template/release");
}
if (src_pkg_name.is_empty()) {
String err;
src_pkg_name = find_export_template("macos.zip", &err);
if (src_pkg_name.is_empty()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("Export template not found."));
return ERR_FILE_NOT_FOUND;
}
}
if (!DirAccess::exists(p_path.get_base_dir())) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("The given export path doesn't exist."));
return ERR_FILE_BAD_PATH;
}
Ref<FileAccess> io_fa;
zlib_filefunc_def io = zipio_create_io(&io_fa);
if (ep.step(TTR("Creating app bundle"), 0)) {
return ERR_SKIP;
}
unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io);
if (!src_pkg_zip) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not find template app to export: \"%s\"."), src_pkg_name));
return ERR_FILE_NOT_FOUND;
}
int ret = unzGoToFirstFile(src_pkg_zip);
String architecture = p_preset->get("binary_format/architecture");
String binary_to_use = "godot_macos_" + String(p_debug ? "debug" : "release") + "." + architecture;
String pkg_name;
if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") {
pkg_name = String(ProjectSettings::get_singleton()->get("application/config/name"));
} else {
pkg_name = "Unnamed";
}
pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);
String export_format;
if (use_dmg() && p_path.ends_with("dmg")) {
export_format = "dmg";
} else if (p_path.ends_with("zip")) {
export_format = "zip";
} else if (p_path.ends_with("app")) {
export_format = "app";
} else {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid export format."));
return ERR_CANT_CREATE;
}
// Create our application bundle.
String tmp_app_dir_name = pkg_name + ".app";
String tmp_base_path_name;
String tmp_app_path_name;
String scr_path;
if (export_format == "app") {
tmp_base_path_name = p_path.get_base_dir();
tmp_app_path_name = p_path;
scr_path = p_path.get_basename() + ".command";
} else {
tmp_base_path_name = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name);
tmp_app_path_name = tmp_base_path_name.path_join(tmp_app_dir_name);
scr_path = tmp_base_path_name.path_join(pkg_name + ".command");
}
print_verbose("Exporting to " + tmp_app_path_name);
Error err = OK;
Ref<DirAccess> tmp_app_dir = DirAccess::create_for_path(tmp_base_path_name);
if (tmp_app_dir.is_null()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory: \"%s\"."), tmp_base_path_name));
err = ERR_CANT_CREATE;
}
if (FileAccess::exists(scr_path)) {
DirAccess::remove_file_or_error(scr_path);
}
if (DirAccess::exists(tmp_app_path_name)) {
String old_dir = tmp_app_dir->get_current_dir();
if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) {
tmp_app_dir->erase_contents_recursive();
tmp_app_dir->change_dir(old_dir);
}
}
Array helpers = p_preset->get("codesign/entitlements/app_sandbox/helper_executables");
// Create our folder structure.
if (err == OK) {
print_verbose("Creating " + tmp_app_path_name + "/Contents/MacOS");
err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/MacOS");
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/MacOS"));
}
}
if (err == OK) {
print_verbose("Creating " + tmp_app_path_name + "/Contents/Frameworks");
err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks");
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Frameworks"));
}
}
if ((err == OK) && helpers.size() > 0) {
print_line("Creating " + tmp_app_path_name + "/Contents/Helpers");
err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Helpers");
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Helpers"));
}
}
if (err == OK) {
print_verbose("Creating " + tmp_app_path_name + "/Contents/Resources");
err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Resources");
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Resources"));
}
}
Dictionary appnames = ProjectSettings::get_singleton()->get("application/config/name_localized");
Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized");
Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized");
Dictionary location_usage_descriptions = p_preset->get("privacy/location_usage_description_localized");
Dictionary address_book_usage_descriptions = p_preset->get("privacy/address_book_usage_description_localized");
Dictionary calendar_usage_descriptions = p_preset->get("privacy/calendar_usage_description_localized");
Dictionary photos_library_usage_descriptions = p_preset->get("privacy/photos_library_usage_description_localized");
Dictionary desktop_folder_usage_descriptions = p_preset->get("privacy/desktop_folder_usage_description_localized");
Dictionary documents_folder_usage_descriptions = p_preset->get("privacy/documents_folder_usage_description_localized");
Dictionary downloads_folder_usage_descriptions = p_preset->get("privacy/downloads_folder_usage_description_localized");
Dictionary network_volumes_usage_descriptions = p_preset->get("privacy/network_volumes_usage_description_localized");
Dictionary removable_volumes_usage_descriptions = p_preset->get("privacy/removable_volumes_usage_description_localized");
Dictionary copyrights = p_preset->get("application/copyright_localized");
Vector<String> translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations");
if (translations.size() > 0) {
{
String fname = tmp_app_path_name + "/Contents/Resources/en.lproj";
tmp_app_dir->make_dir_recursive(fname);
Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
f->store_line("/* Localized versions of Info.plist keys */");
f->store_line("");
f->store_line("CFBundleDisplayName = \"" + ProjectSettings::get_singleton()->get("application/config/name").operator String() + "\";");
if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
f->store_line("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_usage_description").operator String() + "\";");
}
if (!((String)p_preset->get("privacy/camera_usage_description")).is_empty()) {
f->store_line("NSCameraUsageDescription = \"" + p_preset->get("privacy/camera_usage_description").operator String() + "\";");
}
if (!((String)p_preset->get("privacy/location_usage_description")).is_empty()) {
f->store_line("NSLocationUsageDescription = \"" + p_preset->get("privacy/location_usage_description").operator String() + "\";");
}
if (!((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) {
f->store_line("NSContactsUsageDescription = \"" + p_preset->get("privacy/address_book_usage_description").operator String() + "\";");
}
if (!((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) {
f->store_line("NSCalendarsUsageDescription = \"" + p_preset->get("privacy/calendar_usage_description").operator String() + "\";");
}
if (!((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) {
f->store_line("NSPhotoLibraryUsageDescription = \"" + p_preset->get("privacy/photos_library_usage_description").operator String() + "\";");
}
if (!((String)p_preset->get("privacy/desktop_folder_usage_description")).is_empty()) {
f->store_line("NSDesktopFolderUsageDescription = \"" + p_preset->get("privacy/desktop_folder_usage_description").operator String() + "\";");
}
if (!((String)p_preset->get("privacy/documents_folder_usage_description")).is_empty()) {
f->store_line("NSDocumentsFolderUsageDescription = \"" + p_preset->get("privacy/documents_folder_usage_description").operator String() + "\";");
}
if (!((String)p_preset->get("privacy/downloads_folder_usage_description")).is_empty()) {
f->store_line("NSDownloadsFolderUsageDescription = \"" + p_preset->get("privacy/downloads_folder_usage_description").operator String() + "\";");
}
if (!((String)p_preset->get("privacy/network_volumes_usage_description")).is_empty()) {
f->store_line("NSNetworkVolumesUsageDescription = \"" + p_preset->get("privacy/network_volumes_usage_description").operator String() + "\";");
}
if (!((String)p_preset->get("privacy/removable_volumes_usage_description")).is_empty()) {
f->store_line("NSRemovableVolumesUsageDescription = \"" + p_preset->get("privacy/removable_volumes_usage_description").operator String() + "\";");
}
f->store_line("NSHumanReadableCopyright = \"" + p_preset->get("application/copyright").operator String() + "\";");
}
HashSet<String> languages;
for (const String &E : translations) {
Ref<Translation> tr = ResourceLoader::load(E);
if (tr.is_valid() && tr->get_locale() != "en") {
languages.insert(tr->get_locale());
}
}
for (const String &lang : languages) {
String fname = tmp_app_path_name + "/Contents/Resources/" + lang + ".lproj";
tmp_app_dir->make_dir_recursive(fname);
Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
f->store_line("/* Localized versions of Info.plist keys */");
f->store_line("");
if (appnames.has(lang)) {
f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";");
}
if (microphone_usage_descriptions.has(lang)) {
f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";");
}
if (camera_usage_descriptions.has(lang)) {
f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";");
}
if (location_usage_descriptions.has(lang)) {
f->store_line("NSLocationUsageDescription = \"" + location_usage_descriptions[lang].operator String() + "\";");
}
if (address_book_usage_descriptions.has(lang)) {
f->store_line("NSContactsUsageDescription = \"" + address_book_usage_descriptions[lang].operator String() + "\";");
}
if (calendar_usage_descriptions.has(lang)) {
f->store_line("NSCalendarsUsageDescription = \"" + calendar_usage_descriptions[lang].operator String() + "\";");
}
if (photos_library_usage_descriptions.has(lang)) {
f->store_line("NSPhotoLibraryUsageDescription = \"" + photos_library_usage_descriptions[lang].operator String() + "\";");
}
if (desktop_folder_usage_descriptions.has(lang)) {
f->store_line("NSDesktopFolderUsageDescription = \"" + desktop_folder_usage_descriptions[lang].operator String() + "\";");
}
if (documents_folder_usage_descriptions.has(lang)) {
f->store_line("NSDocumentsFolderUsageDescription = \"" + documents_folder_usage_descriptions[lang].operator String() + "\";");
}
if (downloads_folder_usage_descriptions.has(lang)) {
f->store_line("NSDownloadsFolderUsageDescription = \"" + downloads_folder_usage_descriptions[lang].operator String() + "\";");
}
if (network_volumes_usage_descriptions.has(lang)) {
f->store_line("NSNetworkVolumesUsageDescription = \"" + network_volumes_usage_descriptions[lang].operator String() + "\";");
}
if (removable_volumes_usage_descriptions.has(lang)) {
f->store_line("NSRemovableVolumesUsageDescription = \"" + removable_volumes_usage_descriptions[lang].operator String() + "\";");
}
if (copyrights.has(lang)) {
f->store_line("NSHumanReadableCopyright = \"" + copyrights[lang].operator String() + "\";");
}
}
}
// Now process our template.
bool found_binary = false;
while (ret == UNZ_OK && err == OK) {
// Get filename.
unz_file_info info;
char fname[16384];
ret = unzGetCurrentFileInfo(src_pkg_zip, &info, fname, 16384, nullptr, 0, nullptr, 0);
if (ret != UNZ_OK) {
break;
}
String file = String::utf8(fname);
Vector<uint8_t> data;
data.resize(info.uncompressed_size);
// Read.
unzOpenCurrentFile(src_pkg_zip);
unzReadCurrentFile(src_pkg_zip, data.ptrw(), data.size());
unzCloseCurrentFile(src_pkg_zip);
// Write.
file = file.replace_first("macos_template.app/", "");
if (((info.external_fa >> 16L) & 0120000) == 0120000) {
#ifndef UNIX_ENABLED
add_message(EXPORT_MESSAGE_INFO, TTR("Export"), TTR("Relative symlinks are not supported on this OS, the exported project might be broken!"));
#endif
// Handle symlinks in the archive.
file = tmp_app_path_name.path_join(file);
if (err == OK) {
err = tmp_app_dir->make_dir_recursive(file.get_base_dir());
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir()));
}
}
if (err == OK) {
String lnk_data = String::utf8((const char *)data.ptr(), data.size());
err = tmp_app_dir->create_link(lnk_data, file);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not created symlink \"%s\" -> \"%s\"."), lnk_data, file));
}
print_verbose(vformat("ADDING SYMLINK %s => %s\n", file, lnk_data));
}
ret = unzGoToNextFile(src_pkg_zip);
continue; // next
}
if (file == "Contents/Info.plist") {
_fix_plist(p_preset, data, pkg_name);
}
if (file.begins_with("Contents/MacOS/godot_")) {
if (file != "Contents/MacOS/" + binary_to_use) {
ret = unzGoToNextFile(src_pkg_zip);
continue; // skip
}
found_binary = true;
file = "Contents/MacOS/" + pkg_name;
}
if (file == "Contents/Resources/icon.icns") {
// See if there is an icon.
String iconpath;
if (p_preset->get("application/icon") != "") {
iconpath = p_preset->get("application/icon");
} else {
iconpath = ProjectSettings::get_singleton()->get("application/config/icon");
}
if (!iconpath.is_empty()) {
if (iconpath.get_extension() == "icns") {
Ref<FileAccess> icon = FileAccess::open(iconpath, FileAccess::READ);
if (icon.is_valid()) {
data.resize(icon->get_length());
icon->get_buffer(&data.write[0], icon->get_length());
}
} else {
Ref<Image> icon;
icon.instantiate();
icon->load(iconpath);
if (!icon->is_empty()) {
_make_icon(icon, data);
}
}
}
}
if (data.size() > 0) {
print_verbose("ADDING: " + file + " size: " + itos(data.size()));
// Write it into our application bundle.
file = tmp_app_path_name.path_join(file);
if (err == OK) {
err = tmp_app_dir->make_dir_recursive(file.get_base_dir());
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir()));
}
}
if (err == OK) {
Ref<FileAccess> f = FileAccess::open(file, FileAccess::WRITE);
if (f.is_valid()) {
f->store_buffer(data.ptr(), data.size());
f.unref();
if (is_executable(file)) {
// chmod with 0755 if the file is executable.
FileAccess::set_unix_permissions(file, 0755);
}
} else {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not open \"%s\"."), file));
err = ERR_CANT_CREATE;
}
}
}
ret = unzGoToNextFile(src_pkg_zip);
}
// We're done with our source zip.
unzClose(src_pkg_zip);
if (!found_binary) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Requested template binary \"%s\" not found. It might be missing from your template archive."), binary_to_use));
err = ERR_FILE_NOT_FOUND;
}
// Save console script.
if (err == OK) {
int con_scr = p_preset->get("debug/export_console_script");
if ((con_scr == 1 && p_debug) || (con_scr == 2)) {
err = _export_debug_script(p_preset, pkg_name, tmp_app_path_name.get_file() + "/Contents/MacOS/" + pkg_name, scr_path);
FileAccess::set_unix_permissions(scr_path, 0755);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not create console script."));
}
}
}
if (err == OK) {
if (ep.step(TTR("Making PKG"), 1)) {
return ERR_SKIP;
}
// See if we can code sign our new package.
bool sign_enabled = (p_preset->get("codesign/codesign").operator int() > 0);
bool ad_hoc = false;
int codesign_tool = p_preset->get("codesign/codesign");
switch (codesign_tool) {
case 1: { // built-in ad-hoc
ad_hoc = true;
} break;
case 2: { // "rcodesign"
ad_hoc = p_preset->get("codesign/certificate_file").operator String().is_empty() || p_preset->get("codesign/certificate_password").operator String().is_empty();
} break;
#ifdef MACOS_ENABLED
case 3: { // "codesign"
ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");
} break;
#endif
default: {
};
}
String pack_path = tmp_app_path_name + "/Contents/Resources/" + pkg_name + ".pck";
Vector<SharedObject> shared_objects;
err = save_pack(p_preset, p_debug, pack_path, &shared_objects);
bool lib_validation = p_preset->get("codesign/entitlements/disable_library_validation");
if (!shared_objects.is_empty() && sign_enabled && ad_hoc && !lib_validation) {
add_message(EXPORT_MESSAGE_INFO, TTR("Entitlements Modified"), TTR("Ad-hoc signed applications require the 'Disable Library Validation' entitlement to load dynamic libraries."));
lib_validation = true;
}
String ent_path = p_preset->get("codesign/entitlements/custom_file");
String hlp_ent_path = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name + "_helper.entitlements");
if (sign_enabled && (ent_path.is_empty())) {
ent_path = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name + ".entitlements");
Ref<FileAccess> ent_f = FileAccess::open(ent_path, FileAccess::WRITE);
if (ent_f.is_valid()) {
ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
ent_f->store_line("<plist version=\"1.0\">");
ent_f->store_line("<dict>");
if (Engine::get_singleton()->has_singleton("GodotSharp")) {
// These entitlements are required to run managed code, and are always enabled in Mono builds.
ent_f->store_line("<key>com.apple.security.cs.allow-jit</key>");
ent_f->store_line("<true/>");
ent_f->store_line("<key>com.apple.security.cs.allow-unsigned-executable-memory</key>");
ent_f->store_line("<true/>");
ent_f->store_line("<key>com.apple.security.cs.allow-dyld-environment-variables</key>");
ent_f->store_line("<true/>");
} else {
if ((bool)p_preset->get("codesign/entitlements/allow_jit_code_execution")) {
ent_f->store_line("<key>com.apple.security.cs.allow-jit</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/allow_unsigned_executable_memory")) {
ent_f->store_line("<key>com.apple.security.cs.allow-unsigned-executable-memory</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/allow_dyld_environment_variables")) {
ent_f->store_line("<key>com.apple.security.cs.allow-dyld-environment-variables</key>");
ent_f->store_line("<true/>");
}
}
if (lib_validation) {
ent_f->store_line("<key>com.apple.security.cs.disable-library-validation</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/audio_input")) {
ent_f->store_line("<key>com.apple.security.device.audio-input</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/camera")) {
ent_f->store_line("<key>com.apple.security.device.camera</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/location")) {
ent_f->store_line("<key>com.apple.security.personal-information.location</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/address_book")) {
ent_f->store_line("<key>com.apple.security.personal-information.addressbook</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/calendars")) {
ent_f->store_line("<key>com.apple.security.personal-information.calendars</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/photos_library")) {
ent_f->store_line("<key>com.apple.security.personal-information.photos-library</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/apple_events")) {
ent_f->store_line("<key>com.apple.security.automation.apple-events</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/debugging")) {
ent_f->store_line("<key>com.apple.security.get-task-allow</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/app_sandbox/enabled")) {
ent_f->store_line("<key>com.apple.security.app-sandbox</key>");
ent_f->store_line("<true/>");
if ((bool)p_preset->get("codesign/entitlements/app_sandbox/network_server")) {
ent_f->store_line("<key>com.apple.security.network.server</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/app_sandbox/network_client")) {
ent_f->store_line("<key>com.apple.security.network.client</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/app_sandbox/device_usb")) {
ent_f->store_line("<key>com.apple.security.device.usb</key>");
ent_f->store_line("<true/>");
}
if ((bool)p_preset->get("codesign/entitlements/app_sandbox/device_bluetooth")) {
ent_f->store_line("<key>com.apple.security.device.bluetooth</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_downloads") == 1) {
ent_f->store_line("<key>com.apple.security.files.downloads.read-only</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_downloads") == 2) {
ent_f->store_line("<key>com.apple.security.files.downloads.read-write</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_pictures") == 1) {
ent_f->store_line("<key>com.apple.security.files.pictures.read-only</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_pictures") == 2) {
ent_f->store_line("<key>com.apple.security.files.pictures.read-write</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_music") == 1) {
ent_f->store_line("<key>com.apple.security.files.music.read-only</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_music") == 2) {
ent_f->store_line("<key>com.apple.security.files.music.read-write</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_movies") == 1) {
ent_f->store_line("<key>com.apple.security.files.movies.read-only</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_movies") == 2) {
ent_f->store_line("<key>com.apple.security.files.movies.read-write</key>");
ent_f->store_line("<true/>");
}
}
ent_f->store_line("</dict>");
ent_f->store_line("</plist>");
} else {
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create entitlements file."));
err = ERR_CANT_CREATE;
}
if ((err == OK) && helpers.size() > 0) {
ent_f = FileAccess::open(hlp_ent_path, FileAccess::WRITE);
if (ent_f.is_valid()) {
ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
ent_f->store_line("<plist version=\"1.0\">");
ent_f->store_line("<dict>");
ent_f->store_line("<key>com.apple.security.app-sandbox</key>");
ent_f->store_line("<true/>");
ent_f->store_line("<key>com.apple.security.inherit</key>");
ent_f->store_line("<true/>");
ent_f->store_line("</dict>");
ent_f->store_line("</plist>");
} else {
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create helper entitlements file."));
err = ERR_CANT_CREATE;
}
}
}
if ((err == OK) && helpers.size() > 0) {
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
for (int i = 0; i < helpers.size(); i++) {
String hlp_path = helpers[i];
err = da->copy(hlp_path, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file());
if (err == OK && sign_enabled) {
err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false);
}
FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755);
}
}
if (err == OK) {
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
for (int i = 0; i < shared_objects.size(); i++) {
String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path);
if (shared_objects[i].target.is_empty()) {
String path_in_app = tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file();
err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, true);
} else {
String path_in_app = tmp_app_path_name.path_join(shared_objects[i].target);
tmp_app_dir->make_dir_recursive(path_in_app);
err = _copy_and_sign_files(da, src_path, path_in_app.path_join(src_path.get_file()), sign_enabled, p_preset, ent_path, false);
}
if (err != OK) {
break;
}
}
Vector<Ref<EditorExportPlugin>> export_plugins{ EditorExport::get_singleton()->get_export_plugins() };
for (int i = 0; i < export_plugins.size(); ++i) {
err = _export_macos_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path);
if (err != OK) {
break;
}
}
}
if (err == OK && sign_enabled) {
if (ep.step(TTR("Code signing bundle"), 2)) {
return ERR_SKIP;
}
err = _code_sign(p_preset, tmp_app_path_name, ent_path);
}
if (export_format == "dmg") {
// Create a DMG.
if (err == OK) {
if (ep.step(TTR("Making DMG"), 3)) {
return ERR_SKIP;
}
err = _create_dmg(p_path, pkg_name, tmp_base_path_name);
}
// Sign DMG.
if (err == OK && sign_enabled && !ad_hoc) {
if (ep.step(TTR("Code signing DMG"), 3)) {
return ERR_SKIP;
}
err = _code_sign(p_preset, p_path, ent_path, false);
}
} else if (export_format == "zip") {
// Create ZIP.
if (err == OK) {
if (ep.step(TTR("Making ZIP"), 3)) {
return ERR_SKIP;
}
if (FileAccess::exists(p_path)) {
OS::get_singleton()->move_to_trash(p_path);
}
Ref<FileAccess> io_fa_dst;
zlib_filefunc_def io_dst = zipio_create_io(&io_fa_dst);
zipFile zip = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io_dst);
_zip_folder_recursive(zip, tmp_base_path_name, "", pkg_name);
zipClose(zip, nullptr);
}
}
bool noto_enabled = (p_preset->get("notarization/notarization").operator int() > 0);
if (err == OK && noto_enabled) {
if (export_format == "app") {
add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("Notarization requires the app to be archived first, select the DMG or ZIP export format instead."));
} else {
if (ep.step(TTR("Sending archive for notarization"), 4)) {
return ERR_SKIP;
}
err = _notarize(p_preset, p_path);
}
}
// Clean up temporary entitlements files.
if (FileAccess::exists(hlp_ent_path)) {
DirAccess::remove_file_or_error(hlp_ent_path);
}
// Clean up temporary .app dir and generated entitlements.
if ((String)(p_preset->get("codesign/entitlements/custom_file")) == "") {
tmp_app_dir->remove(ent_path);
}
if (export_format != "app") {
if (tmp_app_dir->change_dir(tmp_base_path_name) == OK) {
tmp_app_dir->erase_contents_recursive();
tmp_app_dir->change_dir("..");
tmp_app_dir->remove(pkg_name);
}
}
}
return err;
}
void EditorExportPlatformMacOS::_zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name) {
String dir = p_folder.is_empty() ? p_root_path : p_root_path.path_join(p_folder);
Ref<DirAccess> da = DirAccess::open(dir);
da->list_dir_begin();
String f = da->get_next();
while (!f.is_empty()) {
if (f == "." || f == "..") {
f = da->get_next();
continue;
}
if (da->is_link(f)) {
OS::DateTime dt = OS::get_singleton()->get_datetime();
zip_fileinfo zipfi;
zipfi.tmz_date.tm_year = dt.year;
zipfi.tmz_date.tm_mon = dt.month - 1; // Note: "tm" month range - 0..11, Godot month range - 1..12, https://www.cplusplus.com/reference/ctime/tm/
zipfi.tmz_date.tm_mday = dt.day;
zipfi.tmz_date.tm_hour = dt.hour;
zipfi.tmz_date.tm_min = dt.minute;
zipfi.tmz_date.tm_sec = dt.second;
zipfi.dosDate = 0;
// 0120000: symbolic link type
// 0000755: permissions rwxr-xr-x
// 0000644: permissions rw-r--r--
uint32_t _mode = 0120644;
zipfi.external_fa = (_mode << 16L) | !(_mode & 0200);
zipfi.internal_fa = 0;
zipOpenNewFileInZip4(p_zip,
p_folder.path_join(f).utf8().get_data(),
&zipfi,
nullptr,
0,
nullptr,
0,
nullptr,
Z_DEFLATED,
Z_DEFAULT_COMPRESSION,
0,
-MAX_WBITS,
DEF_MEM_LEVEL,
Z_DEFAULT_STRATEGY,
nullptr,
0,
0x0314, // "version made by", 0x03 - Unix, 0x14 - ZIP specification version 2.0, required to store Unix file permissions
0);
String target = da->read_link(f);
zipWriteInFileInZip(p_zip, target.utf8().get_data(), target.utf8().size());
zipCloseFileInZip(p_zip);
} else if (da->current_is_dir()) {
_zip_folder_recursive(p_zip, p_root_path, p_folder.path_join(f), p_pkg_name);
} else {
OS::DateTime dt = OS::get_singleton()->get_datetime();
zip_fileinfo zipfi;
zipfi.tmz_date.tm_year = dt.year;
zipfi.tmz_date.tm_mon = dt.month - 1; // Note: "tm" month range - 0..11, Godot month range - 1..12, https://www.cplusplus.com/reference/ctime/tm/
zipfi.tmz_date.tm_mday = dt.day;
zipfi.tmz_date.tm_hour = dt.hour;
zipfi.tmz_date.tm_min = dt.minute;
zipfi.tmz_date.tm_sec = dt.second;
zipfi.dosDate = 0;
// 0100000: regular file type
// 0000755: permissions rwxr-xr-x
// 0000644: permissions rw-r--r--
uint32_t _mode = (is_executable(dir.path_join(f)) ? 0100755 : 0100644);
zipfi.external_fa = (_mode << 16L) | !(_mode & 0200);
zipfi.internal_fa = 0;
zipOpenNewFileInZip4(p_zip,
p_folder.path_join(f).utf8().get_data(),
&zipfi,
nullptr,
0,
nullptr,
0,
nullptr,
Z_DEFLATED,
Z_DEFAULT_COMPRESSION,
0,
-MAX_WBITS,
DEF_MEM_LEVEL,
Z_DEFAULT_STRATEGY,
nullptr,
0,
0x0314, // "version made by", 0x03 - Unix, 0x14 - ZIP specification version 2.0, required to store Unix file permissions
0);
Ref<FileAccess> fa = FileAccess::open(dir.path_join(f), FileAccess::READ);
if (fa.is_null()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("ZIP Creation"), vformat(TTR("Could not open file to read from path \"%s\"."), dir.path_join(f)));
return;
}
const int bufsize = 16384;
uint8_t buf[bufsize];
while (true) {
uint64_t got = fa->get_buffer(buf, bufsize);
if (got == 0) {
break;
}
zipWriteInFileInZip(p_zip, buf, got);
}
zipCloseFileInZip(p_zip);
}
f = da->get_next();
}
da->list_dir_end();
}
bool EditorExportPlatformMacOS::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const {
String err;
bool valid = false;
// Look for export templates (custom templates).
bool dvalid = false;
bool rvalid = false;
if (p_preset->get("custom_template/debug") != "") {
dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
if (!dvalid) {
err += TTR("Custom debug template not found.") + "\n";
}
}
if (p_preset->get("custom_template/release") != "") {
rvalid = FileAccess::exists(p_preset->get("custom_template/release"));
if (!rvalid) {
err += TTR("Custom release template not found.") + "\n";
}
}
// Look for export templates (official templates, check only is custom templates are not set).
if (!dvalid || !rvalid) {
dvalid = exists_export_template("macos.zip", &err);
rvalid = dvalid; // Both in the same ZIP.
}
valid = dvalid || rvalid;
r_missing_templates = !valid;
if (!err.is_empty()) {
r_error = err;
}
return valid;
}
bool EditorExportPlatformMacOS::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
String err;
bool valid = true;
String identifier = p_preset->get("application/bundle_identifier");
String pn_err;
if (!is_package_name_valid(identifier, &pn_err)) {
err += TTR("Invalid bundle identifier:") + " " + pn_err + "\n";
valid = false;
}
bool ad_hoc = false;
int codesign_tool = p_preset->get("codesign/codesign");
switch (codesign_tool) {
case 1: { // built-in ad-hoc
ad_hoc = true;
} break;
case 2: { // "rcodesign"
ad_hoc = p_preset->get("codesign/certificate_file").operator String().is_empty() || p_preset->get("codesign/certificate_password").operator String().is_empty();
} break;
#ifdef MACOS_ENABLED
case 3: { // "codesign"
ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");
} break;
#endif
default: {
};
}
int notary_tool = p_preset->get("notarization/notarization");
if (notary_tool > 0) {
if (ad_hoc) {
err += TTR("Notarization: Notarization with an ad-hoc signature is not supported.") + "\n";
valid = false;
}
if (codesign_tool == 0) {
err += TTR("Notarization: Code signing is required for notarization.") + "\n";
valid = false;
}
if (notary_tool == 2) {
if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) {
err += TTR("Notarization: Xcode command line tools are not installed.") + "\n";
valid = false;
}
if (p_preset->get("notarization/apple_id_name") == "" && p_preset->get("notarization/api_uuid") == "") {
err += TTR("Notarization: Neither Apple ID name nor App Store Connect issuer ID name not specified.") + "\n";
valid = false;
} else if (p_preset->get("notarization/apple_id_name") != "" && p_preset->get("notarization/api_uuid") != "") {
err += TTR("Notarization: Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time.") + "\n";
valid = false;
} else {
if (p_preset->get("notarization/apple_id_name") != "") {
if (p_preset->get("notarization/apple_id_password") == "") {
err += TTR("Notarization: Apple ID password not specified.") + "\n";
}
valid = false;
}
if (p_preset->get("notarization/api_uuid") != "") {
if (p_preset->get("notarization/api_key") == "") {
err += TTR("Notarization: App Store Connect API key ID not specified.") + "\n";
valid = false;
}
}
}
} else if (notary_tool == 1) {
if (p_preset->get("notarization/api_uuid") == "") {
err += TTR("Notarization: App Store Connect issuer ID name not specified.") + "\n";
valid = false;
}
if (p_preset->get("notarization/api_key") == "") {
err += TTR("Notarization: App Store Connect API key ID not specified.") + "\n";
valid = false;
}
String rcodesign = EditorSettings::get_singleton()->get("export/macos/rcodesign").operator String();
if (rcodesign.is_empty()) {
err += TTR("Notarization: rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).") + "\n";
valid = false;
}
}
} else {
err += TTR("Warning: Notarization is disabled. The exported project will be blocked by Gatekeeper if it's downloaded from an unknown source.") + "\n";
if (codesign_tool == 0) {
err += TTR("Code signing is disabled. The exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n";
}
}
if (codesign_tool > 0) {
if (ad_hoc) {
err += TTR("Code signing: Using ad-hoc signature. The exported project will be blocked by Gatekeeper") + "\n";
}
if (codesign_tool == 3) {
if (!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) {
err += TTR("Code signing: Xcode command line tools are not installed.") + "\n";
valid = false;
}
} else if (codesign_tool == 2) {
String rcodesign = EditorSettings::get_singleton()->get("export/macos/rcodesign").operator String();
if (rcodesign.is_empty()) {
err += TTR("Code signing: rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).") + "\n";
valid = false;
}
}
if ((bool)p_preset->get("codesign/entitlements/audio_input") && ((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
err += TTR("Privacy: Microphone access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
if ((bool)p_preset->get("codesign/entitlements/camera") && ((String)p_preset->get("privacy/camera_usage_description")).is_empty()) {
err += TTR("Privacy: Camera access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
if ((bool)p_preset->get("codesign/entitlements/location") && ((String)p_preset->get("privacy/location_usage_description")).is_empty()) {
err += TTR("Privacy: Location information access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
if ((bool)p_preset->get("codesign/entitlements/address_book") && ((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) {
err += TTR("Privacy: Address book access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
if ((bool)p_preset->get("codesign/entitlements/calendars") && ((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) {
err += TTR("Privacy: Calendar access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
if ((bool)p_preset->get("codesign/entitlements/photos_library") && ((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) {
err += TTR("Privacy: Photo library access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
}
if (!err.is_empty()) {
r_error = err;
}
return valid;
}
EditorExportPlatformMacOS::EditorExportPlatformMacOS() {
logo = ImageTexture::create_from_image(memnew(Image(_macos_logo)));
}
EditorExportPlatformMacOS::~EditorExportPlatformMacOS() {
}