diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp index 2d7457289..b8dc0a542 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp @@ -3,17 +3,36 @@ #if PLATFORM_TOOLS_MAC #include "MacPlatformTools.h" +#include "Engine/Platform/File.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Platform/Mac/MacPlatformSettings.h" #include "Engine/Core/Config/GameSettings.h" +#include "Engine/Core/Config/BuildSettings.h" #include "Editor/Utilities/EditorUtilities.h" +#include "Engine/Graphics/Textures/TextureData.h" #include "Engine/Content/Content.h" #include "Engine/Content/JsonAsset.h" +#include "Engine/Engine/Globals.h" #include "Editor/Editor.h" #include "Editor/ProjectInfo.h" +#include +using namespace pugi; IMPLEMENT_SETTINGS_GETTER(MacPlatformSettings, MacPlatform); +namespace +{ + String GetAppName() + { + const auto gameSettings = GameSettings::Get(); + String productName = gameSettings->ProductName; + productName.Replace(TEXT(" "), TEXT("")); + productName.Replace(TEXT("."), TEXT("")); + productName.Replace(TEXT("-"), TEXT("")); + return productName; + } +} + MacPlatformTools::MacPlatformTools(ArchitectureType arch) : _arch(arch) { @@ -39,17 +58,19 @@ ArchitectureType MacPlatformTools::GetArchitecture() const return _arch; } +bool MacPlatformTools::IsNativeCodeFile(CookingData& data, const String& file) +{ + String extension = FileSystem::GetExtension(file); + return extension.IsEmpty() || extension == TEXT("dylib"); +} + void MacPlatformTools::OnBuildStarted(CookingData& data) { // Adjust the cooking output folders for packaging app - const auto gameSettings = GameSettings::Get(); - String productName = gameSettings->ProductName; - productName.Replace(TEXT(" "), TEXT("")); - productName.Replace(TEXT("."), TEXT("")); - productName.Replace(TEXT("-"), TEXT("")); - String contents = productName + TEXT(".app/Contents/"); + const auto appName = GetAppName(); + String contents = appName + TEXT(".app/Contents/"); data.DataOutputPath /= contents; - data.NativeCodeOutputPath /= contents; + data.NativeCodeOutputPath /= contents / TEXT("MacOS"); data.ManagedCodeOutputPath /= contents; PlatformTools::OnBuildStarted(data); @@ -61,6 +82,7 @@ bool MacPlatformTools::OnPostProcess(CookingData& data) const auto platformSettings = MacPlatformSettings::Get(); const auto platformDataPath = data.GetPlatformBinariesRoot(); const auto projectVersion = Editor::Project->Version.ToString(); + const auto appName = GetAppName(); // Setup package name (eg. com.company.project) String appIdentifier = platformSettings->AppIdentifier; @@ -92,6 +114,120 @@ bool MacPlatformTools::OnPostProcess(CookingData& data) } } + // Find executable + String executableName; + { + Array files; + FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly); + for (auto& file : files) + { + if (FileSystem::GetExtension(file).IsEmpty()) + { + executableName = StringUtils::GetFileName(file); + break; + } + } + } + + // Deploy app icon + TextureData iconData; + if (!EditorUtilities::GetApplicationImage(platformSettings->OverrideIcon, iconData)) + { + String iconFolderPath = data.DataOutputPath / TEXT("Resources"); + String tmpFolderPath = iconFolderPath / TEXT("icon.iconset"); + if (!FileSystem::DirectoryExists(tmpFolderPath)) + FileSystem::CreateDirectory(tmpFolderPath); + String srcIconPath = tmpFolderPath / TEXT("icon_1024x1024.png"); + if (EditorUtilities::ExportApplicationImage(iconData, 1024, 1024, PixelFormat::R8G8B8A8_UNorm, srcIconPath)) + { + LOG(Error, "Failed to export application icon."); + return true; + } + bool failed = Platform::RunProcess(TEXT("sips -z 16 16 icon_1024x1024.png --out icon_16x16.png"), tmpFolderPath); + failed |= Platform::RunProcess(TEXT("sips -z 32 32 icon_1024x1024.png --out icon_32x32.png"), tmpFolderPath); + failed |= Platform::RunProcess(TEXT("sips -z 64 64 icon_1024x1024.png --out icon_64x64.png"), tmpFolderPath); + failed |= Platform::RunProcess(TEXT("sips -z 128 128 icon_1024x1024.png --out icon_128x128.png"), tmpFolderPath); + failed |= Platform::RunProcess(TEXT("sips -z 256 256 icon_1024x1024.png --out icon_256x512.png"), tmpFolderPath); + failed |= Platform::RunProcess(TEXT("sips -z 512 512 icon_1024x1024.png --out icon_512x512.png"), tmpFolderPath); + failed |= Platform::RunProcess(TEXT("iconutil -c icns icon.iconset"), iconFolderPath); + if (failed) + { + LOG(Error, "Failed to export application icon."); + return true; + } + FileSystem::DeleteDirectory(tmpFolderPath); + } + + // Create PkgInfo file + const String pkgInfoPath = data.DataOutputPath / TEXT("PkgInfo"); + File::WriteAllText(pkgInfoPath, TEXT("APPL???"), Encoding::ANSI); + + // Create Info.plist file with package description + const String plistPath = data.DataOutputPath / TEXT("Info.plist"); + { + xml_document doc; + xml_node plist = doc.child_or_append(PUGIXML_TEXT("plist")); + plist.append_attribute(PUGIXML_TEXT("version")).set_value(PUGIXML_TEXT("1.0")); + xml_node dict = plist.child_or_append(PUGIXML_TEXT("dict")); + +#define ADD_ENTRY(key, value) \ + dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT(key)); \ + dict.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT(value)) +#define ADD_ENTRY_STR(key, value) \ + dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT(key)); \ + { std::u16string valueStr(value.GetText()); \ + dict.append_child(PUGIXML_TEXT("string")).set_child_value(pugi::string_t(valueStr.begin(), valueStr.end()).c_str()); } + + ADD_ENTRY("CFBundleDevelopmentRegion", "English"); + ADD_ENTRY("CFBundlePackageType", "APPL"); + ADD_ENTRY("NSPrincipalClass", "NSApplication"); + ADD_ENTRY("LSApplicationCategoryType", "public.app-category.games"); + ADD_ENTRY("LSMinimumSystemVersion", "10.14"); + ADD_ENTRY("CFBundleIconFile", "icon.icns"); + ADD_ENTRY_STR("CFBundleExecutable", executableName); + ADD_ENTRY_STR("CFBundleIdentifier", appIdentifier); + ADD_ENTRY_STR("CFBundleGetInfoString", gameSettings->ProductName); + ADD_ENTRY_STR("CFBundleVersion", projectVersion); + ADD_ENTRY_STR("NSHumanReadableCopyright", gameSettings->CopyrightNotice); + + dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("CFBundleSupportedPlatforms")); + xml_node CFBundleSupportedPlatforms = dict.append_child(PUGIXML_TEXT("array")); + CFBundleSupportedPlatforms.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT("MacOSX")); + + dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("LSMinimumSystemVersionByArchitecture")); + xml_node LSMinimumSystemVersionByArchitecture = dict.append_child(PUGIXML_TEXT("dict")); + LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("x86_64")); + LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT("10.14")); + +#undef ADD_ENTRY + + if (!doc.save_file(*StringAnsi(plistPath))) + { + LOG(Error, "Failed to save {0}", plistPath); + return true; + } + } + + // TODO: sign binaries + + // TODO: expose event to inject custom post-processing before app packaging (eg. third-party plugins) + + // Package application + const auto buildSettings = BuildSettings::Get(); + if (buildSettings->SkipPackaging) + return false; + LOG(Info, "Building app package..."); + const String dmgPath = data.OriginalOutputPath / appName + TEXT(".dmg"); + const String dmgCommand = String::Format(TEXT("hdiutil create {0}.dmg -volname {0} -fs HFS+ -srcfolder {0}.app"), appName); + const int32 result = Platform::RunProcess(dmgCommand, data.OriginalOutputPath); + if (result != 0) + { + data.Error(TEXT("Failed to package app (result code: {0}). See log for more info."), result); + return true; + } + // TODO: sign dmg + LOG(Info, "Output application package: {0} (size: {1} MB)", dmgPath, FileSystem::GetFileSize(dmgPath) / 1024 / 1024); + return false; } diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h index aa2ced94f..e50caae0f 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h @@ -23,6 +23,7 @@ public: const Char* GetName() const override; PlatformType GetPlatform() const override; ArchitectureType GetArchitecture() const override; + bool IsNativeCodeFile(CookingData& data, const String& file) override; void OnBuildStarted(CookingData& data) override; bool OnPostProcess(CookingData& data) override; }; diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp index 0ccbf3417..72667358b 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp @@ -1084,7 +1084,11 @@ GPUDevice* GPUDeviceVulkan::Create() if (result == VK_ERROR_INCOMPATIBLE_DRIVER) { // Missing driver - Platform::Fatal(TEXT("Cannot find a compatible Vulkan driver.\nPlease look at the Getting Started guide for additional information.")); +#if PLATFORM_APPLE_FAMILY + Platform::Fatal(TEXT("Cannot find a compatible Metal driver.")); +#else + Platform::Fatal(TEXT("Cannot find a compatible Vulkan driver.")); +#endif return nullptr; } if (result == VK_ERROR_EXTENSION_NOT_PRESENT) diff --git a/Source/Engine/Platform/Mac/MacPlatform.cpp b/Source/Engine/Platform/Mac/MacPlatform.cpp index 76c846d57..b3050154e 100644 --- a/Source/Engine/Platform/Mac/MacPlatform.cpp +++ b/Source/Engine/Platform/Mac/MacPlatform.cpp @@ -535,7 +535,13 @@ Rectangle MacPlatform::GetVirtualDesktopBounds() String MacPlatform::GetMainDirectory() { - return StringUtils::GetDirectoryName(GetExecutableFilePath()); + String path = StringUtils::GetDirectoryName(GetExecutableFilePath()); + if (path.EndsWith(TEXT("/Contents/MacOS"))) + { + // If running from executable in a package, go up to the Contents + path = StringUtils::GetDirectoryName(path); + } + return path; } String MacPlatform::GetExecutableFilePath() @@ -589,18 +595,26 @@ bool MacPlatform::SetEnvironmentVariable(const String& name, const String& value int32 MacProcess(const StringView& cmdLine, const StringView& workingDir, const Dictionary& environment, bool waitForEnd, bool logOutput) { LOG(Info, "Command: {0}", cmdLine); + String cwd; if (workingDir.Length() != 0) + { LOG(Info, "Working directory: {0}", workingDir); + cwd = Platform::GetWorkingDirectory(); + Platform::SetWorkingDirectory(workingDir); + } StringAsANSI<> cmdLineAnsi(*cmdLine, cmdLine.Length()); FILE* pipe = popen(cmdLineAnsi.Get(), "r"); + if (cwd.Length() != 0) + { + Platform::SetWorkingDirectory(cwd); + } if (!pipe) { LOG(Warning, "Failed to start process, errno={}", errno); return -1; } - // TODO: workingDir // TODO: environment int32 returnCode = 0; diff --git a/Source/Engine/Platform/Mac/MacPlatformSettings.h b/Source/Engine/Platform/Mac/MacPlatformSettings.h index 3cc8c6f61..1dd611104 100644 --- a/Source/Engine/Platform/Mac/MacPlatformSettings.h +++ b/Source/Engine/Platform/Mac/MacPlatformSettings.h @@ -6,6 +6,9 @@ #include "Engine/Core/Config/PlatformSettingsBase.h" #include "Engine/Core/Types/String.h" +#include "Engine/Scripting/SoftObjectReference.h" + +class Texture; /// /// Mac platform settings. @@ -20,6 +23,12 @@ API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API API_FIELD(Attributes="EditorOrder(0), EditorDisplay(\"General\")") String AppIdentifier = TEXT("com.${COMPANY_NAME}.${PROJECT_NAME}"); + /// + /// Custom icon texture to use for the application (overrides the default one). + /// + API_FIELD(Attributes="EditorOrder(1000), EditorDisplay(\"Other\")") + SoftObjectReference OverrideIcon; + public: /// /// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use. @@ -30,6 +39,7 @@ public: void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override { DESERIALIZE(AppIdentifier); + DESERIALIZE(OverrideIcon); } };