285 lines
13 KiB
C++
285 lines
13 KiB
C++
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
|
|
|
#if PLATFORM_TOOLS_IOS
|
|
|
|
#include "iOSPlatformTools.h"
|
|
#include "Engine/Platform/File.h"
|
|
#include "Engine/Platform/FileSystem.h"
|
|
#include "Engine/Platform/CreateProcessSettings.h"
|
|
#include "Engine/Platform/iOS/iOSPlatformSettings.h"
|
|
#include "Engine/Core/Config/GameSettings.h"
|
|
#include "Engine/Core/Config/BuildSettings.h"
|
|
#include "Engine/Core/Collections/Dictionary.h"
|
|
#include "Engine/Graphics/Textures/TextureData.h"
|
|
#include "Engine/Graphics/PixelFormatExtensions.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 "Editor/Cooker/GameCooker.h"
|
|
#include "Editor/Utilities/EditorUtilities.h"
|
|
|
|
IMPLEMENT_SETTINGS_GETTER(iOSPlatformSettings, iOSPlatform);
|
|
|
|
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;
|
|
}
|
|
|
|
const Char* GetExportMethod(iOSPlatformSettings::ExportMethods method)
|
|
{
|
|
switch (method)
|
|
{
|
|
case iOSPlatformSettings::ExportMethods::AppStore: return TEXT("app-store");
|
|
case iOSPlatformSettings::ExportMethods::Development: return TEXT("development");
|
|
case iOSPlatformSettings::ExportMethods::AdHoc: return TEXT("ad-hoc");
|
|
case iOSPlatformSettings::ExportMethods::Enterprise: return TEXT("enterprise");
|
|
default: return TEXT("");
|
|
}
|
|
}
|
|
|
|
String GetUIInterfaceOrientation(iOSPlatformSettings::UIInterfaceOrientations orientations)
|
|
{
|
|
String result;
|
|
if (EnumHasAnyFlags(orientations, iOSPlatformSettings::UIInterfaceOrientations::Portrait))
|
|
result += TEXT("UIInterfaceOrientationPortrait ");
|
|
if (EnumHasAnyFlags(orientations, iOSPlatformSettings::UIInterfaceOrientations::PortraitUpsideDown))
|
|
result += TEXT("UIInterfaceOrientationPortraitUpsideDown ");
|
|
if (EnumHasAnyFlags(orientations, iOSPlatformSettings::UIInterfaceOrientations::LandscapeLeft))
|
|
result += TEXT("UIInterfaceOrientationLandscapeLeft ");
|
|
if (EnumHasAnyFlags(orientations, iOSPlatformSettings::UIInterfaceOrientations::LandscapeRight))
|
|
result += TEXT("UIInterfaceOrientationLandscapeRight ");
|
|
result = result.TrimTrailing();
|
|
return result;
|
|
}
|
|
}
|
|
|
|
const Char* iOSPlatformTools::GetDisplayName() const
|
|
{
|
|
return TEXT("iOS");
|
|
}
|
|
|
|
const Char* iOSPlatformTools::GetName() const
|
|
{
|
|
return TEXT("iOS");
|
|
}
|
|
|
|
PlatformType iOSPlatformTools::GetPlatform() const
|
|
{
|
|
return PlatformType::iOS;
|
|
}
|
|
|
|
ArchitectureType iOSPlatformTools::GetArchitecture() const
|
|
{
|
|
return ArchitectureType::ARM64;
|
|
}
|
|
|
|
DotNetAOTModes iOSPlatformTools::UseAOT() const
|
|
{
|
|
return DotNetAOTModes::MonoAOTDynamic;
|
|
}
|
|
|
|
PixelFormat iOSPlatformTools::GetTextureFormat(CookingData& data, TextureBase* texture, PixelFormat format)
|
|
{
|
|
// TODO: add ETC compression support for iOS
|
|
// TODO: add ASTC compression support for iOS
|
|
|
|
if (PixelFormatExtensions::IsCompressedBC(format))
|
|
{
|
|
switch (format)
|
|
{
|
|
case PixelFormat::BC1_Typeless:
|
|
case PixelFormat::BC2_Typeless:
|
|
case PixelFormat::BC3_Typeless:
|
|
return PixelFormat::R8G8B8A8_Typeless;
|
|
case PixelFormat::BC1_UNorm:
|
|
case PixelFormat::BC2_UNorm:
|
|
case PixelFormat::BC3_UNorm:
|
|
return PixelFormat::R8G8B8A8_UNorm;
|
|
case PixelFormat::BC1_UNorm_sRGB:
|
|
case PixelFormat::BC2_UNorm_sRGB:
|
|
case PixelFormat::BC3_UNorm_sRGB:
|
|
return PixelFormat::R8G8B8A8_UNorm_sRGB;
|
|
case PixelFormat::BC4_Typeless:
|
|
return PixelFormat::R8_Typeless;
|
|
case PixelFormat::BC4_UNorm:
|
|
return PixelFormat::R8_UNorm;
|
|
case PixelFormat::BC4_SNorm:
|
|
return PixelFormat::R8_SNorm;
|
|
case PixelFormat::BC5_Typeless:
|
|
return PixelFormat::R16G16_Typeless;
|
|
case PixelFormat::BC5_UNorm:
|
|
return PixelFormat::R16G16_UNorm;
|
|
case PixelFormat::BC5_SNorm:
|
|
return PixelFormat::R16G16_SNorm;
|
|
case PixelFormat::BC7_Typeless:
|
|
case PixelFormat::BC6H_Typeless:
|
|
return PixelFormat::R16G16B16A16_Typeless;
|
|
case PixelFormat::BC7_UNorm:
|
|
case PixelFormat::BC6H_Uf16:
|
|
case PixelFormat::BC6H_Sf16:
|
|
return PixelFormat::R16G16B16A16_Float;
|
|
case PixelFormat::BC7_UNorm_sRGB:
|
|
return PixelFormat::R16G16B16A16_UNorm;
|
|
default:
|
|
return format;
|
|
}
|
|
}
|
|
|
|
return format;
|
|
}
|
|
|
|
bool iOSPlatformTools::IsNativeCodeFile(CookingData& data, const String& file)
|
|
{
|
|
String extension = FileSystem::GetExtension(file);
|
|
return extension.IsEmpty() || extension == TEXT("dylib");
|
|
}
|
|
|
|
void iOSPlatformTools::OnBuildStarted(CookingData& data)
|
|
{
|
|
// Adjust the cooking output folders for packaging app
|
|
const Char* subDir = TEXT("FlaxGame/Data");
|
|
data.DataOutputPath /= subDir;
|
|
data.NativeCodeOutputPath /= subDir;
|
|
data.ManagedCodeOutputPath /= subDir;
|
|
|
|
PlatformTools::OnBuildStarted(data);
|
|
}
|
|
|
|
bool iOSPlatformTools::OnPostProcess(CookingData& data)
|
|
{
|
|
const auto gameSettings = GameSettings::Get();
|
|
const auto platformSettings = iOSPlatformSettings::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;
|
|
if (EditorUtilities::FormatAppPackageName(appIdentifier))
|
|
return true;
|
|
|
|
// Copy fresh Gradle project template
|
|
if (FileSystem::CopyDirectory(data.OriginalOutputPath, platformDataPath / TEXT("Project"), true))
|
|
{
|
|
LOG(Error, "Failed to deploy XCode project to {0} from {1}", data.OriginalOutputPath, platformDataPath);
|
|
return true;
|
|
}
|
|
|
|
// Format project template files
|
|
Dictionary<String, String> configReplaceMap;
|
|
configReplaceMap[TEXT("${AppName}")] = appName;
|
|
configReplaceMap[TEXT("${AppIdentifier}")] = appIdentifier;
|
|
configReplaceMap[TEXT("${AppTeamId}")] = platformSettings->AppTeamId;
|
|
configReplaceMap[TEXT("${AppVersion}")] = platformSettings->AppVersion;
|
|
configReplaceMap[TEXT("${ProjectName}")] = gameSettings->ProductName;
|
|
configReplaceMap[TEXT("${ProjectVersion}")] = projectVersion;
|
|
configReplaceMap[TEXT("${HeaderSearchPaths}")] = Globals::StartupFolder;
|
|
configReplaceMap[TEXT("${ExportMethod}")] = GetExportMethod(platformSettings->ExportMethod);
|
|
configReplaceMap[TEXT("${UISupportedInterfaceOrientations_iPhone}")] = GetUIInterfaceOrientation(platformSettings->SupportedInterfaceOrientationsiPhone);
|
|
configReplaceMap[TEXT("${UISupportedInterfaceOrientations_iPad}")] = GetUIInterfaceOrientation(platformSettings->SupportedInterfaceOrientationsiPad);
|
|
{
|
|
// Initialize auto-generated areas as empty
|
|
configReplaceMap[TEXT("${PBXBuildFile}")] = String::Empty;
|
|
configReplaceMap[TEXT("${PBXCopyFilesBuildPhaseFiles}")] = String::Empty;
|
|
configReplaceMap[TEXT("${PBXFileReference}")] = String::Empty;
|
|
configReplaceMap[TEXT("${PBXFrameworksBuildPhase}")] = String::Empty;
|
|
configReplaceMap[TEXT("${PBXFrameworksGroup}")] = String::Empty;
|
|
configReplaceMap[TEXT("${PBXFilesGroup}")] = String::Empty;
|
|
configReplaceMap[TEXT("${PBXResourcesGroup}")] = String::Empty;
|
|
}
|
|
{
|
|
// Rename dotnet license files to not mislead the actual game licensing
|
|
FileSystem::MoveFile(data.DataOutputPath / TEXT("Dotnet/DOTNET-LICENSE.TXT"), data.DataOutputPath / TEXT("Dotnet/LICENSE.TXT"), true);
|
|
FileSystem::MoveFile(data.DataOutputPath / TEXT("Dotnet/DOTNET-THIRD-PARTY-NOTICES.TXT"), data.DataOutputPath / TEXT("Dotnet/THIRD-PARTY-NOTICES.TXT"), true);
|
|
}
|
|
Array<String> files;
|
|
FileSystem::DirectoryGetFiles(files, data.DataOutputPath, TEXT("*"), DirectorySearchOption::AllDirectories);
|
|
for (const String& file : files)
|
|
{
|
|
String name = StringUtils::GetFileName(file);
|
|
if (name == TEXT(".DS_Store") || name == TEXT("FlaxGame"))
|
|
continue;
|
|
String fileId = Guid::New().ToString(Guid::FormatType::N).Left(24);
|
|
String projectPath = FileSystem::ConvertAbsolutePathToRelative(data.DataOutputPath, file);
|
|
if (name.EndsWith(TEXT(".dylib")))
|
|
{
|
|
String frameworkId = Guid::New().ToString(Guid::FormatType::N).Left(24);
|
|
String frameworkEmbedId = Guid::New().ToString(Guid::FormatType::N).Left(24);
|
|
configReplaceMap[TEXT("${PBXBuildFile}")] += String::Format(TEXT("\t\t{0} /* {1} in Frameworks */ = {{isa = PBXBuildFile; fileRef = {2} /* {1} */; }};\n"), frameworkId, name, fileId);
|
|
configReplaceMap[TEXT("${PBXBuildFile}")] += String::Format(TEXT("\t\t{0} /* {1} in Embed Frameworks */ = {{isa = PBXBuildFile; fileRef = {2} /* {1} */; settings = {{ATTRIBUTES = (CodeSignOnCopy, ); }}; }};\n"), frameworkEmbedId, name, fileId);
|
|
configReplaceMap[TEXT("${PBXCopyFilesBuildPhaseFiles}")] += String::Format(TEXT("\t\t\t\t{0} /* {1} in Embed Frameworks */,\n"), frameworkEmbedId, name);
|
|
configReplaceMap[TEXT("${PBXFileReference}")] += String::Format(TEXT("\t\t{0} /* {1} */ = {{isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = \"{1}\"; path = \"FlaxGame/Data/{2}\"; sourceTree = \"<group>\"; }};\n"), fileId, name, projectPath);
|
|
configReplaceMap[TEXT("${PBXFrameworksBuildPhase}")] += String::Format(TEXT("\t\t\t\t{0} /* {1} in Frameworks */,\n"), frameworkId, name);
|
|
configReplaceMap[TEXT("${PBXFrameworksGroup}")] += String::Format(TEXT("\t\t\t\t{0} /* {1} */,\n"), fileId, name);
|
|
}
|
|
else
|
|
{
|
|
String fileRefId = Guid::New().ToString(Guid::FormatType::N).Left(24);
|
|
configReplaceMap[TEXT("${PBXBuildFile}")] += String::Format(TEXT("\t\t{0} /* {1} in Resources */ = {{isa = PBXBuildFile; fileRef = {2} /* {1} */; }};\n"), fileRefId, name, fileId);
|
|
configReplaceMap[TEXT("${PBXFileReference}")] += String::Format(TEXT("\t\t{0} /* {1} */ = {{isa = PBXFileReference; lastKnownFileType = file; name = \"{1}\"; path = \"Data/{2}\"; sourceTree = \"<group>\"; }};\n"), fileId, name, projectPath);
|
|
configReplaceMap[TEXT("${PBXFilesGroup}")] += String::Format(TEXT("\t\t\t\t{0} /* {1} */,\n"), fileId, name);
|
|
configReplaceMap[TEXT("${PBXResourcesGroup}")] += String::Format(TEXT("\t\t\t\t{0} /* {1} in Resources */,\n"), fileRefId, name);
|
|
}
|
|
}
|
|
bool failed = false;
|
|
failed |= EditorUtilities::ReplaceInFile(data.OriginalOutputPath / TEXT("FlaxGame.xcodeproj/project.pbxproj"), configReplaceMap);
|
|
failed |= EditorUtilities::ReplaceInFile(data.OriginalOutputPath / TEXT("ExportOptions.plist"), configReplaceMap);
|
|
if (failed)
|
|
{
|
|
LOG(Error, "Failed to format XCode project");
|
|
return true;
|
|
}
|
|
|
|
// Export images
|
|
// TODO: provide per-device icons (eg. iPad 1x, iPad 2x) instead of a single icon size
|
|
TextureData iconData;
|
|
if (!EditorUtilities::GetApplicationImage(platformSettings->OverrideIcon, iconData))
|
|
{
|
|
String outputPath = data.OriginalOutputPath / TEXT("FlaxGame/Assets.xcassets/AppIcon.appiconset/ios_store_icon.png");
|
|
if (EditorUtilities::ExportApplicationImage(iconData, 1024, 1024, PixelFormat::R8G8B8A8_UNorm, outputPath))
|
|
{
|
|
LOG(Error, "Failed to export application icon.");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Package application
|
|
const auto buildSettings = BuildSettings::Get();
|
|
if (buildSettings->SkipPackaging)
|
|
return false;
|
|
GameCooker::PackageFiles();
|
|
{
|
|
LOG(Info, "Building app package...");
|
|
const Char* configuration = data.Configuration == BuildConfiguration::Release ? TEXT("Release") : TEXT("Debug");
|
|
String command = String::Format(TEXT("xcodebuild -project FlaxGame.xcodeproj -configuration {} -scheme FlaxGame -archivePath FlaxGame.xcarchive archive"), configuration);
|
|
int32 result = Platform::RunProcess(command, data.OriginalOutputPath);
|
|
if (result != 0)
|
|
{
|
|
data.Error(String::Format(TEXT("Failed to package app (result code: {0}). See log for more info."), result));
|
|
return true;
|
|
}
|
|
command = TEXT("xcodebuild -exportArchive -archivePath FlaxGame.xcarchive -allowProvisioningUpdates -exportPath . -exportOptionsPlist ExportOptions.plist");
|
|
result = Platform::RunProcess(command, data.OriginalOutputPath);
|
|
if (result != 0)
|
|
{
|
|
data.Error(String::Format(TEXT("Failed to package app (result code: {0}). See log for more info."), result));
|
|
return true;
|
|
}
|
|
const String ipaPath = data.OriginalOutputPath / TEXT("FlaxGame.ipa");
|
|
LOG(Info, "Output application package: {0} (size: {1} MB)", ipaPath, FileSystem::GetFileSize(ipaPath) / 1024 / 1024);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#endif
|