Add app packaging for macOS
This commit is contained in:
@@ -3,17 +3,36 @@
|
|||||||
#if PLATFORM_TOOLS_MAC
|
#if PLATFORM_TOOLS_MAC
|
||||||
|
|
||||||
#include "MacPlatformTools.h"
|
#include "MacPlatformTools.h"
|
||||||
|
#include "Engine/Platform/File.h"
|
||||||
#include "Engine/Platform/FileSystem.h"
|
#include "Engine/Platform/FileSystem.h"
|
||||||
#include "Engine/Platform/Mac/MacPlatformSettings.h"
|
#include "Engine/Platform/Mac/MacPlatformSettings.h"
|
||||||
#include "Engine/Core/Config/GameSettings.h"
|
#include "Engine/Core/Config/GameSettings.h"
|
||||||
|
#include "Engine/Core/Config/BuildSettings.h"
|
||||||
#include "Editor/Utilities/EditorUtilities.h"
|
#include "Editor/Utilities/EditorUtilities.h"
|
||||||
|
#include "Engine/Graphics/Textures/TextureData.h"
|
||||||
#include "Engine/Content/Content.h"
|
#include "Engine/Content/Content.h"
|
||||||
#include "Engine/Content/JsonAsset.h"
|
#include "Engine/Content/JsonAsset.h"
|
||||||
|
#include "Engine/Engine/Globals.h"
|
||||||
#include "Editor/Editor.h"
|
#include "Editor/Editor.h"
|
||||||
#include "Editor/ProjectInfo.h"
|
#include "Editor/ProjectInfo.h"
|
||||||
|
#include <ThirdParty/pugixml/pugixml.hpp>
|
||||||
|
using namespace pugi;
|
||||||
|
|
||||||
IMPLEMENT_SETTINGS_GETTER(MacPlatformSettings, MacPlatform);
|
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)
|
MacPlatformTools::MacPlatformTools(ArchitectureType arch)
|
||||||
: _arch(arch)
|
: _arch(arch)
|
||||||
{
|
{
|
||||||
@@ -39,17 +58,19 @@ ArchitectureType MacPlatformTools::GetArchitecture() const
|
|||||||
return _arch;
|
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)
|
void MacPlatformTools::OnBuildStarted(CookingData& data)
|
||||||
{
|
{
|
||||||
// Adjust the cooking output folders for packaging app
|
// Adjust the cooking output folders for packaging app
|
||||||
const auto gameSettings = GameSettings::Get();
|
const auto appName = GetAppName();
|
||||||
String productName = gameSettings->ProductName;
|
String contents = appName + TEXT(".app/Contents/");
|
||||||
productName.Replace(TEXT(" "), TEXT(""));
|
|
||||||
productName.Replace(TEXT("."), TEXT(""));
|
|
||||||
productName.Replace(TEXT("-"), TEXT(""));
|
|
||||||
String contents = productName + TEXT(".app/Contents/");
|
|
||||||
data.DataOutputPath /= contents;
|
data.DataOutputPath /= contents;
|
||||||
data.NativeCodeOutputPath /= contents;
|
data.NativeCodeOutputPath /= contents / TEXT("MacOS");
|
||||||
data.ManagedCodeOutputPath /= contents;
|
data.ManagedCodeOutputPath /= contents;
|
||||||
|
|
||||||
PlatformTools::OnBuildStarted(data);
|
PlatformTools::OnBuildStarted(data);
|
||||||
@@ -61,6 +82,7 @@ bool MacPlatformTools::OnPostProcess(CookingData& data)
|
|||||||
const auto platformSettings = MacPlatformSettings::Get();
|
const auto platformSettings = MacPlatformSettings::Get();
|
||||||
const auto platformDataPath = data.GetPlatformBinariesRoot();
|
const auto platformDataPath = data.GetPlatformBinariesRoot();
|
||||||
const auto projectVersion = Editor::Project->Version.ToString();
|
const auto projectVersion = Editor::Project->Version.ToString();
|
||||||
|
const auto appName = GetAppName();
|
||||||
|
|
||||||
// Setup package name (eg. com.company.project)
|
// Setup package name (eg. com.company.project)
|
||||||
String appIdentifier = platformSettings->AppIdentifier;
|
String appIdentifier = platformSettings->AppIdentifier;
|
||||||
@@ -92,6 +114,120 @@ bool MacPlatformTools::OnPostProcess(CookingData& data)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find executable
|
||||||
|
String executableName;
|
||||||
|
{
|
||||||
|
Array<String> 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public:
|
|||||||
const Char* GetName() const override;
|
const Char* GetName() const override;
|
||||||
PlatformType GetPlatform() const override;
|
PlatformType GetPlatform() const override;
|
||||||
ArchitectureType GetArchitecture() const override;
|
ArchitectureType GetArchitecture() const override;
|
||||||
|
bool IsNativeCodeFile(CookingData& data, const String& file) override;
|
||||||
void OnBuildStarted(CookingData& data) override;
|
void OnBuildStarted(CookingData& data) override;
|
||||||
bool OnPostProcess(CookingData& data) override;
|
bool OnPostProcess(CookingData& data) override;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1084,7 +1084,11 @@ GPUDevice* GPUDeviceVulkan::Create()
|
|||||||
if (result == VK_ERROR_INCOMPATIBLE_DRIVER)
|
if (result == VK_ERROR_INCOMPATIBLE_DRIVER)
|
||||||
{
|
{
|
||||||
// Missing 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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
if (result == VK_ERROR_EXTENSION_NOT_PRESENT)
|
if (result == VK_ERROR_EXTENSION_NOT_PRESENT)
|
||||||
|
|||||||
@@ -535,7 +535,13 @@ Rectangle MacPlatform::GetVirtualDesktopBounds()
|
|||||||
|
|
||||||
String MacPlatform::GetMainDirectory()
|
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()
|
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<String, String>& environment, bool waitForEnd, bool logOutput)
|
int32 MacProcess(const StringView& cmdLine, const StringView& workingDir, const Dictionary<String, String>& environment, bool waitForEnd, bool logOutput)
|
||||||
{
|
{
|
||||||
LOG(Info, "Command: {0}", cmdLine);
|
LOG(Info, "Command: {0}", cmdLine);
|
||||||
|
String cwd;
|
||||||
if (workingDir.Length() != 0)
|
if (workingDir.Length() != 0)
|
||||||
|
{
|
||||||
LOG(Info, "Working directory: {0}", workingDir);
|
LOG(Info, "Working directory: {0}", workingDir);
|
||||||
|
cwd = Platform::GetWorkingDirectory();
|
||||||
|
Platform::SetWorkingDirectory(workingDir);
|
||||||
|
}
|
||||||
|
|
||||||
StringAsANSI<> cmdLineAnsi(*cmdLine, cmdLine.Length());
|
StringAsANSI<> cmdLineAnsi(*cmdLine, cmdLine.Length());
|
||||||
FILE* pipe = popen(cmdLineAnsi.Get(), "r");
|
FILE* pipe = popen(cmdLineAnsi.Get(), "r");
|
||||||
|
if (cwd.Length() != 0)
|
||||||
|
{
|
||||||
|
Platform::SetWorkingDirectory(cwd);
|
||||||
|
}
|
||||||
if (!pipe)
|
if (!pipe)
|
||||||
{
|
{
|
||||||
LOG(Warning, "Failed to start process, errno={}", errno);
|
LOG(Warning, "Failed to start process, errno={}", errno);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: workingDir
|
|
||||||
// TODO: environment
|
// TODO: environment
|
||||||
|
|
||||||
int32 returnCode = 0;
|
int32 returnCode = 0;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
#include "Engine/Core/Config/PlatformSettingsBase.h"
|
#include "Engine/Core/Config/PlatformSettingsBase.h"
|
||||||
#include "Engine/Core/Types/String.h"
|
#include "Engine/Core/Types/String.h"
|
||||||
|
#include "Engine/Scripting/SoftObjectReference.h"
|
||||||
|
|
||||||
|
class Texture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mac platform settings.
|
/// Mac platform settings.
|
||||||
@@ -20,6 +23,12 @@ API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API
|
|||||||
API_FIELD(Attributes="EditorOrder(0), EditorDisplay(\"General\")")
|
API_FIELD(Attributes="EditorOrder(0), EditorDisplay(\"General\")")
|
||||||
String AppIdentifier = TEXT("com.${COMPANY_NAME}.${PROJECT_NAME}");
|
String AppIdentifier = TEXT("com.${COMPANY_NAME}.${PROJECT_NAME}");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom icon texture to use for the application (overrides the default one).
|
||||||
|
/// </summary>
|
||||||
|
API_FIELD(Attributes="EditorOrder(1000), EditorDisplay(\"Other\")")
|
||||||
|
SoftObjectReference<Texture> OverrideIcon;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use.
|
/// 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
|
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override
|
||||||
{
|
{
|
||||||
DESERIALIZE(AppIdentifier);
|
DESERIALIZE(AppIdentifier);
|
||||||
|
DESERIALIZE(OverrideIcon);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user