// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using UnrealBuildBase;
namespace UnrealBuildTool
{
class UEDeployIOS : UEBuildDeploy
{
public UEDeployIOS(ILogger InLogger)
: base(InLogger)
{
}
protected UnrealPluginLanguage? UPL = null;
protected delegate bool FilenameFilter(string InFilename);
public bool ForDistribution
{
get => bForDistribution;
set => bForDistribution = value;
}
bool bForDistribution = false;
private static readonly DirectoryReference MobileProvisionDirRef = AppleExports.GetProvisionDirectory();
protected class VersionUtilities
{
public static string? BuildDirectory
{
get;
set;
}
public static string? GameName
{
get;
set;
}
public static bool bCustomLaunchscreenStoryboard = false;
static string RunningVersionFilename => Path.Combine(BuildDirectory!, GameName + ".PackageVersionCounter");
///
/// Reads the GameName.PackageVersionCounter from disk and bumps the minor version number in it
///
///
public static string ReadRunningVersion()
{
string CurrentVersion = "0.0";
if (File.Exists(RunningVersionFilename))
{
CurrentVersion = File.ReadAllText(RunningVersionFilename);
}
return CurrentVersion;
}
///
/// Pulls apart a version string of one of the two following formats:
/// "7301.15 11-01 10:28" (Major.Minor Date Time)
/// "7486.0" (Major.Minor)
///
///
///
///
///
public static void PullApartVersion(string CFBundleVersion, out int VersionMajor, out int VersionMinor, out string TimeStamp)
{
// Expecting source to be like "7301.15 11-01 10:28" or "7486.0"
string[] Parts = CFBundleVersion.Split(new char[] { ' ' });
// Parse the version string
string[] VersionParts = Parts[0].Split(new char[] { '.' });
if (!Int32.TryParse(VersionParts[0], out VersionMajor))
{
VersionMajor = 0;
}
if ((VersionParts.Length < 2) || (!Int32.TryParse(VersionParts[1], out VersionMinor)))
{
VersionMinor = 0;
}
TimeStamp = "";
if (Parts.Length > 1)
{
TimeStamp = String.Join(" ", Parts, 1, Parts.Length - 1);
}
}
public static string ConstructVersion(int MajorVersion, int MinorVersion)
{
return String.Format("{0}.{1}", MajorVersion, MinorVersion);
}
///
/// Parses the version string (expected to be of the form major.minor or major)
/// Also parses the major.minor from the running version file and increments it's minor by 1.
///
/// If the running version major matches and the running version minor is newer, then the bundle version is updated.
///
/// In either case, the running version is set to the current bundle version number and written back out.
///
/// The (possibly updated) bundle version
public static string CalculateUpdatedMinorVersionString(string CFBundleVersion)
{
// Read the running version and bump it
int RunningMajorVersion;
int RunningMinorVersion;
string DummyDate;
string RunningVersion = ReadRunningVersion();
PullApartVersion(RunningVersion, out RunningMajorVersion, out RunningMinorVersion, out DummyDate);
RunningMinorVersion++;
// Read the passed in version and bump it
int MajorVersion;
int MinorVersion;
PullApartVersion(CFBundleVersion, out MajorVersion, out MinorVersion, out DummyDate);
MinorVersion++;
// Combine them if the stub time is older
if ((RunningMajorVersion == MajorVersion) && (RunningMinorVersion > MinorVersion))
{
// A subsequent cook on the same sync, the only time that we stomp on the stub version
MinorVersion = RunningMinorVersion;
}
// Combine them together
string ResultVersionString = ConstructVersion(MajorVersion, MinorVersion);
// Update the running version file
Directory.CreateDirectory(Path.GetDirectoryName(RunningVersionFilename)!);
File.WriteAllText(RunningVersionFilename, ResultVersionString);
return ResultVersionString;
}
///
/// Updates the minor version in the CFBundleVersion key of the specified PList if this is a new package.
/// Also updates the key EpicAppVersion with the bundle version and the current date/time (no year)
///
public static string UpdateBundleVersion(string OldPList, string EngineDirectory)
{
string CFBundleVersion = "-1";
if (!Unreal.IsBuildMachine())
{
int Index = OldPList.IndexOf("CFBundleVersion");
if (Index != -1)
{
int Start = OldPList.IndexOf("", Index) + ("").Length;
CFBundleVersion = OldPList.Substring(Start, OldPList.IndexOf("", Index) - Start);
CFBundleVersion = CalculateUpdatedMinorVersionString(CFBundleVersion);
}
else
{
CFBundleVersion = "0.0";
}
}
else
{
// get the changelist
CFBundleVersion = ReadOnlyBuildVersion.Current.Changelist.ToString();
}
return CFBundleVersion;
}
}
protected virtual string GetTargetPlatformName()
{
return "IOS";
}
public static string EncodeBundleName(string PlistValue, string ProjectName)
{
string result = PlistValue.Replace("[PROJECT_NAME]", ProjectName).Replace("_", "");
result = result.Replace("&", "&");
result = result.Replace("\"", """);
result = result.Replace("\'", "'");
result = result.Replace("<", "<");
result = result.Replace(">", ">");
return result;
}
public static string GetMinimumOSVersion(string MinVersion, ILogger Logger)
{
string MinVersionToReturn = "";
switch (MinVersion)
{
case "":
case "IOS_15":
case "IOS_Minimum":
MinVersionToReturn = "15.0";
break;
case "IOS_16":
MinVersionToReturn = "16.0";
break;
case "IOS_17":
MinVersionToReturn = "17.0";
break;
default:
MinVersionToReturn = "15.0";
Logger.LogInformation("MinimumiOSVersion {MinVersion} specified in ini file is no longer supported, defaulting to {MinVersionToReturn}", MinVersion, MinVersionToReturn);
break;
}
return MinVersionToReturn;
}
public static void WritePlistFile(FileReference PlistFile, DirectoryReference? ProjectLocation, UnrealPluginLanguage? UPL, UnrealTargetConfiguration Config, string GameName, bool bIsUnrealGame, string ProjectName, ILogger Logger)
{
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, ProjectLocation, UnrealTargetPlatform.IOS);
// required capabilities
List RequiredCaps = new() { "arm64", "metal" };
// orientations
string InterfaceOrientation = "";
string PreferredLandscapeOrientation = "";
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "PreferredLandscapeOrientation", out PreferredLandscapeOrientation);
string SupportedOrientations = "";
bool bSupported = true;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsPortraitOrientation", out bSupported);
SupportedOrientations += bSupported ? "\t\tUIInterfaceOrientationPortrait\n" : "";
bool bSupportsPortrait = bSupported;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsUpsideDownOrientation", out bSupported);
SupportedOrientations += bSupported ? "\t\tUIInterfaceOrientationPortraitUpsideDown\n" : "";
bSupportsPortrait |= bSupported;
bool bSupportsLandscapeLeft = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsLandscapeLeftOrientation", out bSupportsLandscapeLeft);
bool bSupportsLandscapeRight = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsLandscapeRightOrientation", out bSupportsLandscapeRight);
bool bSupportsLandscape = bSupportsLandscapeLeft || bSupportsLandscapeRight;
if (bSupportsLandscapeLeft && bSupportsLandscapeRight)
{
// if both landscape orientations are present, set the UIInterfaceOrientation key
// in the orientation list, the preferred orientation should be first
if (PreferredLandscapeOrientation == "LandscapeLeft")
{
InterfaceOrientation = "\tUIInterfaceOrientation\n\tUIInterfaceOrientationLandscapeLeft\n";
SupportedOrientations += "\t\tUIInterfaceOrientationLandscapeLeft\n\t\tUIInterfaceOrientationLandscapeRight\n";
}
else
{
// by default, landscape right is the preferred orientation - Apple's UI guidlines
InterfaceOrientation = "\tUIInterfaceOrientation\n\tUIInterfaceOrientationLandscapeRight\n";
SupportedOrientations += "\t\tUIInterfaceOrientationLandscapeRight\n\t\tUIInterfaceOrientationLandscapeLeft\n";
}
}
else
{
// max one landscape orientation is supported
SupportedOrientations += bSupportsLandscapeRight ? "\t\tUIInterfaceOrientationLandscapeRight\n" : "";
SupportedOrientations += bSupportsLandscapeLeft ? "\t\tUIInterfaceOrientationLandscapeLeft\n" : "";
}
// ITunes file sharing
bool bSupportsITunesFileSharing = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsITunesFileSharing", out bSupportsITunesFileSharing);
bool bSupportsFilesApp = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsFilesApp", out bSupportsFilesApp);
// disable https requirement
bool bDisableHTTPS;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bDisableHTTPS", out bDisableHTTPS);
bool bUseZenStore = false;
if (Config != UnrealTargetConfiguration.Shipping)
{
ConfigHierarchy PlatformGameConfig = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, ProjectLocation, UnrealTargetPlatform.IOS);
PlatformGameConfig.GetBool("/Script/UnrealEd.ProjectPackagingSettings", "bUseZenStore", out bUseZenStore);
}
// bundle display name
string BundleDisplayName;
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "BundleDisplayName", out BundleDisplayName);
// short version string
string BundleShortVersion;
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "VersionInfo", out BundleShortVersion);
// Get Google Support details
bool bEnableGoogleSupport = true;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bEnableGoogleSupport", out bEnableGoogleSupport);
// Write the Google iOS URL Scheme if we need it.
string GoogleReversedClientId = "";
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "GoogleReversedClientId", out GoogleReversedClientId);
bEnableGoogleSupport = bEnableGoogleSupport && !String.IsNullOrWhiteSpace(GoogleReversedClientId);
// Add remote-notifications as background mode
bool bRemoteNotificationsSupported = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bEnableRemoteNotificationsSupport", out bRemoteNotificationsSupported);
// Add audio as background mode
bool bBackgroundAudioSupported = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsBackgroundAudio", out bBackgroundAudioSupported);
// Add background fetch as background mode
bool bBackgroundFetch = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bEnableBackgroundFetch", out bBackgroundFetch);
// Get any Location Services permission descriptions added
string LocationAlwaysUsageDescription = "";
string LocationWhenInUseDescription = "";
Ini.GetString("/Script/LocationServicesIOSEditor.LocationServicesIOSSettings", "LocationAlwaysUsageDescription", out LocationAlwaysUsageDescription);
Ini.GetString("/Script/LocationServicesIOSEditor.LocationServicesIOSSettings", "LocationWhenInUseDescription", out LocationWhenInUseDescription);
// extra plist data
string ExtraData = "";
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "AdditionalPlistData", out ExtraData);
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bCustomLaunchscreenStoryboard", out VersionUtilities.bCustomLaunchscreenStoryboard);
// generate the plist file
StringBuilder Text = new StringBuilder();
Text.AppendLine("");
Text.AppendLine("");
Text.AppendLine("");
Text.AppendLine("");
Text.AppendLine("\tCFBundleURLTypes");
Text.AppendLine("\t");
Text.AppendLine("\t\t");
Text.AppendLine("\t\t\tCFBundleURLName");
Text.AppendLine("\t\t\tcom.Epic.Unreal");
Text.AppendLine("\t\t\tCFBundleURLSchemes");
Text.AppendLine("\t\t\t");
Text.AppendLine(String.Format("\t\t\t\t{0}", bIsUnrealGame ? "UnrealGame" : GameName));
if (bEnableGoogleSupport)
{
Text.AppendLine(String.Format("\t\t\t\t{0}", GoogleReversedClientId));
}
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t");
Text.AppendLine("\t");
Text.AppendLine("\tUIStatusBarHidden");
Text.AppendLine("\t");
Text.AppendLine("\tUIFileSharingEnabled");
Text.AppendLine(String.Format("\t<{0}/>", bSupportsITunesFileSharing ? "true" : "false"));
if (bSupportsFilesApp)
{
Text.AppendLine("\tLSSupportsOpeningDocumentsInPlace");
Text.AppendLine("\t");
}
Text.AppendLine("\tCFBundleDisplayName");
Text.AppendLine(String.Format("\t{0}", EncodeBundleName(BundleDisplayName, ProjectName)));
Text.AppendLine("\tUIRequiresFullScreen");
Text.AppendLine("\t");
Text.AppendLine("\tUIViewControllerBasedStatusBarAppearance");
Text.AppendLine("\t");
if (!String.IsNullOrEmpty(InterfaceOrientation))
{
Text.AppendLine(InterfaceOrientation);
}
Text.AppendLine("\tUIRequiredDeviceCapabilities");
Text.AppendLine("\t");
foreach (string Cap in RequiredCaps)
{
Text.AppendLine($"\t\t{Cap}\n");
}
Text.AppendLine("\t");
Text.AppendLine("\tUILaunchStoryboardName");
Text.AppendLine("\tLaunchScreen");
// Support high refresh rates (iPhone only)
// https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro
bool bSupportHighRefreshRates = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportHighRefreshRates", out bSupportHighRefreshRates);
if (bSupportHighRefreshRates)
{
Text.AppendLine("\tCADisableMinimumFrameDurationOnPhone");
}
// set exempt encryption
bool bUsesNonExemptEncryption = false;
string ITSEncryptionExportComplianceCode = "";
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bUsesNonExemptEncryption", out bUsesNonExemptEncryption);
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "ITSEncryptionExportComplianceCode", out ITSEncryptionExportComplianceCode);
Text.AppendLine("\tITSAppUsesNonExemptEncryption");
Text.AppendLine(String.Format("\t<{0}/>", bUsesNonExemptEncryption ? "true" : "false"));
if (bUsesNonExemptEncryption && !String.IsNullOrWhiteSpace(ITSEncryptionExportComplianceCode))
{
Text.AppendLine("\tITSEncryptionExportComplianceCode");
Text.AppendLine(String.Format("\t{0}", ITSEncryptionExportComplianceCode));
}
// add location services descriptions if used
if (!String.IsNullOrWhiteSpace(LocationAlwaysUsageDescription))
{
Text.AppendLine("\tNSLocationAlwaysAndWhenInUseUsageDescription");
Text.AppendLine(String.Format("\t{0}", LocationAlwaysUsageDescription));
}
if (!String.IsNullOrWhiteSpace(LocationWhenInUseDescription))
{
Text.AppendLine("\tNSLocationWhenInUseUsageDescription");
Text.AppendLine(String.Format("\t{0}", LocationWhenInUseDescription));
}
// disable HTTPS requirement
if (bDisableHTTPS || bUseZenStore)
{
Text.AppendLine("\tNSAppTransportSecurity");
Text.AppendLine("\t\t");
Text.AppendLine("\t\t\tNSAllowsArbitraryLoads");
Text.AppendLine("\t\t\tNSAllowsArbitraryLoadsInWebContent");
Text.AppendLine("\t\t\tNSAllowsLocalNetworking");
if (bUseZenStore)
{
Text.AppendLine("\t\t\tNSExceptionDomains");
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t\t\t169.245.0.0/16");
Text.AppendLine("\t\t\t\t");
Text.AppendLine("\t\t\t\t\tNSIncludesSubdomains");
Text.AppendLine("\t\t\t\t\tNSExceptionAllowsInsecureHTTPLoads");
Text.AppendLine("\t\t\t\t");
Text.AppendLine("\t\t\t");
}
Text.AppendLine("\t\t");
}
// add a TVOS setting since they share this file
Text.AppendLine("\tTVTopShelfImage");
Text.AppendLine("\t");
Text.AppendLine("\t\tTVTopShelfPrimaryImageWide");
Text.AppendLine("\t\tTop Shelf Image Wide");
Text.AppendLine("\t");
if (!String.IsNullOrEmpty(ExtraData))
{
ExtraData = ExtraData.Replace("\\n", "\n");
foreach (string Line in ExtraData.Split("\r\n".ToCharArray()))
{
if (!String.IsNullOrWhiteSpace(Line))
{
Text.AppendLine("\t" + Line);
}
}
}
// Add remote-notifications as background mode
if (bRemoteNotificationsSupported || bBackgroundFetch || bBackgroundAudioSupported)
{
Text.AppendLine("\tUIBackgroundModes");
Text.AppendLine("\t");
if (bBackgroundAudioSupported)
{
Text.AppendLine("\t\taudio");
}
if (bRemoteNotificationsSupported)
{
Text.AppendLine("\t\tremote-notification");
}
if (bBackgroundFetch)
{
Text.AppendLine("\t\tfetch");
}
Text.AppendLine("\t");
}
Text.AppendLine("");
Text.AppendLine("");
DirectoryReference.CreateDirectory(PlistFile.Directory);
if (UPL != null)
{
// Allow UPL to modify the plist here
XDocument XDoc;
try
{
XDoc = XDocument.Parse(Text.ToString());
}
catch (Exception e)
{
throw new BuildException("plist is invalid {0}\n{1}", e, Text.ToString());
}
XDoc.DocumentType!.InternalSubset = "";
UPL.ProcessPluginNode("None", "iosPListUpdates", "", ref XDoc);
string result = XDoc.Declaration?.ToString() + "\n" + XDoc.ToString().Replace("", "");
File.WriteAllText(PlistFile.FullName, result);
Text = new StringBuilder(result);
}
Text = Text.Replace("[PROJECT_NAME]", "$(UE_PROJECT_NAME)");
File.WriteAllText(PlistFile.FullName, Text.ToString());
}
public static bool GenerateIOSPList(FileReference? ProjectFile, UnrealTargetConfiguration Config, string ProjectDirectory, bool bIsUnrealGame, string GameName, bool bIsClient, string ProjectName, string InEngineDir, string AppDirectory, UnrealPluginLanguage? UPL, string? BundleID, bool bBuildAsFramework, ILogger Logger)
{
// get the settings from the ini file
// plist replacements
DirectoryReference? DirRef = bIsUnrealGame ? (!String.IsNullOrEmpty(UnrealBuildTool.GetRemoteIniPath()) ? new DirectoryReference(UnrealBuildTool.GetRemoteIniPath()!) : null) : new DirectoryReference(ProjectDirectory);
if (!AppleExports.UseModernXcode(ProjectFile))
{
return GenerateLegacyIOSPList(ProjectFile, Config, ProjectDirectory, bIsUnrealGame, GameName, bIsClient, ProjectName, InEngineDir, AppDirectory, UPL, BundleID, bBuildAsFramework, Logger);
}
// generate the Info.plist for future use
string BuildDirectory = ProjectDirectory + "/Build/IOS";
string IntermediateDirectory = ProjectDirectory + "/Intermediate/IOS";
string PListFile = IntermediateDirectory + "/" + GameName + "-Info.plist";
;
ProjectName = !String.IsNullOrEmpty(ProjectName) ? ProjectName : GameName;
VersionUtilities.BuildDirectory = BuildDirectory;
VersionUtilities.GameName = GameName;
WritePlistFile(new FileReference(PListFile), DirRef, UPL, Config, GameName, bIsUnrealGame, ProjectName, Logger);
if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac && !bBuildAsFramework)
{
// need to make sure touching this plist is in the same lock as FinalizeAppWithXcode()
string MutexName = GlobalSingleInstanceMutex.GetUniqueMutexForPath("UnrealBuildTool_XcodeBuild", Unreal.RootDirectory);
using (new GlobalSingleInstanceMutex(MutexName, true))
{
FileReference FinalPlistFile;
FinalPlistFile = new FileReference($"{ProjectDirectory}/Build/IOS/UBTGenerated/Info.Template.plist");
DirectoryReference.CreateDirectory(FinalPlistFile.Directory);
// @todo: writeifdifferent is better
FileReference.Delete(FinalPlistFile);
File.Copy(PListFile, FinalPlistFile.FullName);
}
}
return true;
}
public static bool GenerateLegacyIOSPList(FileReference? ProjectFile, UnrealTargetConfiguration Config, string ProjectDirectory, bool bIsUnrealGame, string GameName, bool bIsClient, string ProjectName, string InEngineDir, string AppDirectory, UnrealPluginLanguage? UPL, string? BundleID, bool bBuildAsFramework, ILogger Logger)
{
// generate the Info.plist for future use
string BuildDirectory = ProjectDirectory + "/Build/IOS";
string IntermediateDirectory = (bIsUnrealGame ? InEngineDir : ProjectDirectory) + "/Intermediate/IOS";
string PListFile = IntermediateDirectory + "/" + GameName + "-Info.plist";
ProjectName = !String.IsNullOrEmpty(ProjectName) ? ProjectName : GameName;
VersionUtilities.BuildDirectory = BuildDirectory;
VersionUtilities.GameName = GameName;
// read the old file
string OldPListData = File.Exists(PListFile) ? File.ReadAllText(PListFile) : "";
// get the settings from the ini file
// plist replacements
DirectoryReference? DirRef = bIsUnrealGame ? (!String.IsNullOrEmpty(UnrealBuildTool.GetRemoteIniPath()) ? new DirectoryReference(UnrealBuildTool.GetRemoteIniPath()!) : null) : new DirectoryReference(ProjectDirectory);
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirRef, UnrealTargetPlatform.IOS);
// orientations
string InterfaceOrientation = "";
string PreferredLandscapeOrientation = "";
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "PreferredLandscapeOrientation", out PreferredLandscapeOrientation);
string SupportedOrientations = "";
bool bSupported = true;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsPortraitOrientation", out bSupported);
SupportedOrientations += bSupported ? "\t\tUIInterfaceOrientationPortrait\n" : "";
bool bSupportsPortrait = bSupported;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsUpsideDownOrientation", out bSupported);
SupportedOrientations += bSupported ? "\t\tUIInterfaceOrientationPortraitUpsideDown\n" : "";
bSupportsPortrait |= bSupported;
bool bSupportsLandscapeLeft = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsLandscapeLeftOrientation", out bSupportsLandscapeLeft);
bool bSupportsLandscapeRight = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsLandscapeRightOrientation", out bSupportsLandscapeRight);
bool bSupportsLandscape = bSupportsLandscapeLeft || bSupportsLandscapeRight;
if (bSupportsLandscapeLeft && bSupportsLandscapeRight)
{
// if both landscape orientations are present, set the UIInterfaceOrientation key
// in the orientation list, the preferred orientation should be first
if (PreferredLandscapeOrientation == "LandscapeLeft")
{
InterfaceOrientation = "\tUIInterfaceOrientation\n\tUIInterfaceOrientationLandscapeLeft\n";
SupportedOrientations += "\t\tUIInterfaceOrientationLandscapeLeft\n\t\tUIInterfaceOrientationLandscapeRight\n";
}
else
{
// by default, landscape right is the preferred orientation - Apple's UI guidlines
InterfaceOrientation = "\tUIInterfaceOrientation\n\tUIInterfaceOrientationLandscapeRight\n";
SupportedOrientations += "\t\tUIInterfaceOrientationLandscapeRight\n\t\tUIInterfaceOrientationLandscapeLeft\n";
}
}
else
{
// max one landscape orientation is supported
SupportedOrientations += bSupportsLandscapeRight ? "\t\tUIInterfaceOrientationLandscapeRight\n" : "";
SupportedOrientations += bSupportsLandscapeLeft ? "\t\tUIInterfaceOrientationLandscapeLeft\n" : "";
}
// ITunes file sharing
bool bSupportsITunesFileSharing = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsITunesFileSharing", out bSupportsITunesFileSharing);
bool bSupportsFilesApp = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsFilesApp", out bSupportsFilesApp);
// bundle display name
string BundleDisplayName;
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "BundleDisplayName", out BundleDisplayName);
// bundle identifier
string BundleIdentifier;
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "BundleIdentifier", out BundleIdentifier);
if (!String.IsNullOrEmpty(BundleID))
{
BundleIdentifier = BundleID; // overriding bundle ID
}
// bundle name
string BundleName;
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "BundleName", out BundleName);
// disable https requirement
bool bDisableHTTPS;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bDisableHTTPS", out bDisableHTTPS);
// short version string
string BundleShortVersion;
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "VersionInfo", out BundleShortVersion);
// required capabilities (arm64 always required)
string RequiredCaps = "\t\tarm64\n";
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsMetal", out bSupported);
RequiredCaps += bSupported ? "\t\tmetal\n" : "";
// minimum iOS version
string MinVersionSetting = "";
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "MinimumiOSVersion", out MinVersionSetting);
string MinVersion = GetMinimumOSVersion(MinVersionSetting, Logger);
// Get Google Support details
bool bEnableGoogleSupport = true;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bEnableGoogleSupport", out bEnableGoogleSupport);
// Write the Google iOS URL Scheme if we need it.
string GoogleReversedClientId = "";
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "GoogleReversedClientId", out GoogleReversedClientId);
bEnableGoogleSupport = bEnableGoogleSupport && !String.IsNullOrWhiteSpace(GoogleReversedClientId);
// Add remote-notifications as background mode
bool bRemoteNotificationsSupported = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bEnableRemoteNotificationsSupport", out bRemoteNotificationsSupported);
// Add audio as background mode
bool bBackgroundAudioSupported = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsBackgroundAudio", out bBackgroundAudioSupported);
// Add background fetch as background mode
bool bBackgroundFetch = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bEnableBackgroundFetch", out bBackgroundFetch);
// Get any Location Services permission descriptions added
string LocationAlwaysUsageDescription = "";
string LocationWhenInUseDescription = "";
Ini.GetString("/Script/LocationServicesIOSEditor.LocationServicesIOSSettings", "LocationAlwaysUsageDescription", out LocationAlwaysUsageDescription);
Ini.GetString("/Script/LocationServicesIOSEditor.LocationServicesIOSSettings", "LocationWhenInUseDescription", out LocationWhenInUseDescription);
// extra plist data
string ExtraData = "";
Ini.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "AdditionalPlistData", out ExtraData);
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bCustomLaunchscreenStoryboard", out VersionUtilities.bCustomLaunchscreenStoryboard);
// generate the plist file
StringBuilder Text = new StringBuilder();
Text.AppendLine("");
Text.AppendLine("");
Text.AppendLine("");
Text.AppendLine("");
Text.AppendLine("\tCFBundleURLTypes");
Text.AppendLine("\t");
Text.AppendLine("\t\t");
Text.AppendLine("\t\t\tCFBundleURLName");
Text.AppendLine("\t\t\tcom.Epic.Unreal");
Text.AppendLine("\t\t\tCFBundleURLSchemes");
Text.AppendLine("\t\t\t");
Text.AppendLine(String.Format("\t\t\t\t{0}", bIsUnrealGame ? "UnrealGame" : GameName));
if (bEnableGoogleSupport)
{
Text.AppendLine(String.Format("\t\t\t\t{0}", GoogleReversedClientId));
}
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t");
Text.AppendLine("\t");
Text.AppendLine("\tCFBundleDevelopmentRegion");
Text.AppendLine("\tEnglish");
Text.AppendLine("\tCFBundleDisplayName");
Text.AppendLine(String.Format("\t{0}", EncodeBundleName(BundleDisplayName, ProjectName)));
Text.AppendLine("\tCFBundleExecutable");
string BundleExecutable = bIsUnrealGame ?
(bIsClient ? "UnrealClient" : "UnrealGame") :
(bIsClient ? GameName + "Client" : GameName);
Text.AppendLine(String.Format("\t{0}", BundleExecutable));
Text.AppendLine("\tCFBundleIdentifier");
Text.AppendLine(String.Format("\t{0}", BundleIdentifier.Replace("[PROJECT_NAME]", ProjectName).Replace("_", "")));
Text.AppendLine("\tCFBundleInfoDictionaryVersion");
Text.AppendLine("\t6.0");
Text.AppendLine("\tCFBundleName");
Text.AppendLine(String.Format("\t{0}", EncodeBundleName(BundleName, ProjectName)));
Text.AppendLine("\tCFBundlePackageType");
Text.AppendLine("\tAPPL");
Text.AppendLine("\tCFBundleSignature");
Text.AppendLine("\t????");
Text.AppendLine("\tCFBundleVersion");
Text.AppendLine(String.Format("\t{0}", VersionUtilities.UpdateBundleVersion(OldPListData, InEngineDir)));
Text.AppendLine("\tCFBundleShortVersionString");
Text.AppendLine(String.Format("\t{0}", BundleShortVersion));
Text.AppendLine("\tLSRequiresIPhoneOS");
Text.AppendLine("\t");
Text.AppendLine("\tUIStatusBarHidden");
Text.AppendLine("\t");
Text.AppendLine("\tUIFileSharingEnabled");
Text.AppendLine(String.Format("\t<{0}/>", bSupportsITunesFileSharing ? "true" : "false"));
if (bSupportsFilesApp)
{
Text.AppendLine("\tLSSupportsOpeningDocumentsInPlace");
Text.AppendLine("\t");
}
Text.AppendLine("\tUIRequiresFullScreen");
Text.AppendLine("\t");
Text.AppendLine("\tUIViewControllerBasedStatusBarAppearance");
Text.AppendLine("\t");
if (!String.IsNullOrEmpty(InterfaceOrientation))
{
Text.AppendLine(InterfaceOrientation);
}
Text.AppendLine("\tUISupportedInterfaceOrientations");
Text.AppendLine("\t");
foreach (string Line in SupportedOrientations.Split("\r\n".ToCharArray()))
{
if (!String.IsNullOrWhiteSpace(Line))
{
Text.AppendLine(Line);
}
}
Text.AppendLine("\t");
Text.AppendLine("\tUIRequiredDeviceCapabilities");
Text.AppendLine("\t");
foreach (string Line in RequiredCaps.Split("\r\n".ToCharArray()))
{
if (!String.IsNullOrWhiteSpace(Line))
{
Text.AppendLine(Line);
}
}
Text.AppendLine("\t");
Text.AppendLine("\tCFBundleIcons");
Text.AppendLine("\t");
Text.AppendLine("\t\tCFBundlePrimaryIcon");
Text.AppendLine("\t\t");
Text.AppendLine("\t\t\tCFBundleIconFiles");
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t\t\tAppIcon60x60");
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t\tCFBundleIconName");
Text.AppendLine("\t\t\tAppIcon");
Text.AppendLine("\t\t\tUIPrerenderedIcon");
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t");
Text.AppendLine("\t");
Text.AppendLine("\tCFBundleIcons~ipad");
Text.AppendLine("\t");
Text.AppendLine("\t\tCFBundlePrimaryIcon");
Text.AppendLine("\t\t");
Text.AppendLine("\t\t\tCFBundleIconFiles");
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t\t\tAppIcon60x60");
Text.AppendLine("\t\t\t\tAppIcon76x76");
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t\tCFBundleIconName");
Text.AppendLine("\t\t\tAppIcon");
Text.AppendLine("\t\t\tUIPrerenderedIcon");
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t");
Text.AppendLine("\t");
Text.AppendLine("\tUILaunchStoryboardName");
Text.AppendLine("\tLaunchScreen");
if (File.Exists(DirectoryReference.FromFile(ProjectFile) + "/Build/IOS/Resources/Interface/LaunchScreen.storyboard") && VersionUtilities.bCustomLaunchscreenStoryboard)
{
string LaunchStoryboard = DirectoryReference.FromFile(ProjectFile) + "/Build/IOS/Resources/Interface/LaunchScreen.storyboard";
if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac)
{
string outputStoryboard = LaunchStoryboard + "c";
string argsStoryboard = "--compile " + outputStoryboard + " " + LaunchStoryboard;
string stdOutLaunchScreen = Utils.RunLocalProcessAndReturnStdOut("ibtool", argsStoryboard, Logger);
Logger.LogInformation("LaunchScreen Storyboard compilation results : {Results}", stdOutLaunchScreen);
}
else
{
Logger.LogWarning("Custom Launchscreen compilation storyboard only compatible on Mac for now");
}
}
// Support high refresh rates (iPhone only)
// https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro
bool bSupportHighRefreshRates = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportHighRefreshRates", out bSupportHighRefreshRates);
if (bSupportHighRefreshRates)
{
Text.AppendLine("\tCADisableMinimumFrameDurationOnPhone");
}
Text.AppendLine("\tCFBundleSupportedPlatforms");
Text.AppendLine("\t");
Text.AppendLine("\t\tiPhoneOS");
Text.AppendLine("\t");
Text.AppendLine("\tMinimumOSVersion");
Text.AppendLine(String.Format("\t{0}", MinVersion));
// disable exempt encryption
Text.AppendLine("\tITSAppUsesNonExemptEncryption");
Text.AppendLine("\t");
// add location services descriptions if used
if (!String.IsNullOrWhiteSpace(LocationAlwaysUsageDescription))
{
Text.AppendLine("\tNSLocationAlwaysAndWhenInUseUsageDescription");
Text.AppendLine(String.Format("\t{0}", LocationAlwaysUsageDescription));
}
if (!String.IsNullOrWhiteSpace(LocationWhenInUseDescription))
{
Text.AppendLine("\tNSLocationWhenInUseUsageDescription");
Text.AppendLine(String.Format("\t{0}", LocationWhenInUseDescription));
}
// disable HTTPS requirement
if (bDisableHTTPS)
{
Text.AppendLine("\tNSAppTransportSecurity");
Text.AppendLine("\t\t");
Text.AppendLine("\t\t\tNSAllowsArbitraryLoads");
Text.AppendLine("\t\t");
}
if (!String.IsNullOrEmpty(ExtraData))
{
ExtraData = ExtraData.Replace("\\n", "\n");
foreach (string Line in ExtraData.Split("\r\n".ToCharArray()))
{
if (!String.IsNullOrWhiteSpace(Line))
{
Text.AppendLine("\t" + Line);
}
}
}
// Add remote-notifications as background mode
if (bRemoteNotificationsSupported || bBackgroundFetch || bBackgroundAudioSupported)
{
Text.AppendLine("\tUIBackgroundModes");
Text.AppendLine("\t");
if (bBackgroundAudioSupported)
{
Text.AppendLine("\t\taudio");
}
if (bRemoteNotificationsSupported)
{
Text.AppendLine("\t\tremote-notification");
}
if (bBackgroundFetch)
{
Text.AppendLine("\t\tfetch");
}
Text.AppendLine("\t");
}
// write the iCloud container identifier, if present in the old file
if (!String.IsNullOrEmpty(OldPListData))
{
int index = OldPListData.IndexOf("ICloudContainerIdentifier");
if (index > 0)
{
index = OldPListData.IndexOf("", index) + 8;
int length = OldPListData.IndexOf("", index) - index;
string ICloudContainerIdentifier = OldPListData.Substring(index, length);
Text.AppendLine("\tICloudContainerIdentifier");
Text.AppendLine(String.Format("\t{0}", ICloudContainerIdentifier));
}
}
Text.AppendLine("");
Text.AppendLine("");
// Create the intermediate directory if needed
if (!Directory.Exists(IntermediateDirectory))
{
Directory.CreateDirectory(IntermediateDirectory);
}
if (UPL != null)
{
// Allow UPL to modify the plist here
XDocument XDoc;
try
{
XDoc = XDocument.Parse(Text.ToString());
}
catch (Exception e)
{
throw new BuildException("plist is invalid {0}\n{1}", e, Text.ToString());
}
XDoc.DocumentType!.InternalSubset = "";
UPL.ProcessPluginNode("None", "iosPListUpdates", "", ref XDoc);
string result = XDoc.Declaration?.ToString() + "\n" + XDoc.ToString().Replace("", "");
File.WriteAllText(PListFile, result);
Text = new StringBuilder(result);
}
File.WriteAllText(PListFile, Text.ToString());
if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac && !bBuildAsFramework)
{
if (!Directory.Exists(AppDirectory))
{
Directory.CreateDirectory(AppDirectory);
}
File.WriteAllText(AppDirectory + "/Info.plist", Text.ToString());
}
return true;
}
public static VersionNumber? GetSdkVersion(TargetReceipt Receipt)
{
VersionNumber? SdkVersion = null;
if (Receipt != null)
{
ReceiptProperty? SdkVersionProperty = Receipt.AdditionalProperties.FirstOrDefault(x => x.Name == "SDK");
if (SdkVersionProperty != null)
{
VersionNumber.TryParse(SdkVersionProperty.Value, out SdkVersion);
}
}
return SdkVersion;
}
public static bool GetCompileAsDll(TargetReceipt? Receipt)
{
if (Receipt != null)
{
ReceiptProperty? CompileAsDllProperty = Receipt.AdditionalProperties.FirstOrDefault(x => x.Name == "CompileAsDll");
if (CompileAsDllProperty != null && CompileAsDllProperty.Value == "true")
{
return true;
}
}
return false;
}
public bool GeneratePList(FileReference ProjectFile, UnrealTargetConfiguration Config, string ProjectDirectory, bool bIsUnrealGame, string GameName, bool bIsClient, string ProjectName, string InEngineDir, string AppDirectory, TargetReceipt Receipt)
{
List UPLScripts = CollectPluginDataPaths(Receipt.AdditionalProperties, Logger);
VersionNumber? SdkVersion = GetSdkVersion(Receipt);
bool bBuildAsFramework = GetCompileAsDll(Receipt);
return GeneratePList(ProjectFile, Config, ProjectDirectory, bIsUnrealGame, GameName, bIsClient, ProjectName, InEngineDir, AppDirectory, UPLScripts, "", bBuildAsFramework);
}
public virtual bool GeneratePList(FileReference? ProjectFile, UnrealTargetConfiguration Config, string ProjectDirectory, bool bIsUnrealGame, string GameName, bool bIsClient, string ProjectName, string InEngineDir, string AppDirectory, List UPLScripts, string? BundleID, bool bBuildAsFramework)
{
// remember name with -IOS-Shipping, etc
// string ExeName = GameName;
// strip out the markup
GameName = GameName.Split("-".ToCharArray())[0];
List<(string UEArch, string NativeArch)> ProjectArches = new() { ("None", "None") };
string BundlePath;
// get the receipt
if (bIsUnrealGame)
{
// ReceiptFilename = TargetReceipt.GetDefaultPath(Unreal.EngineDirectory, "UnrealGame", UnrealTargetPlatform.IOS, Config, "");
BundlePath = Path.Combine(Unreal.EngineDirectory.ToString(), "Intermediate", "IOS-Deploy", "UnrealGame", Config.ToString(), "Payload", "UnrealGame.app");
}
else
{
// ReceiptFilename = TargetReceipt.GetDefaultPath(new DirectoryReference(ProjectDirectory), GameName, UnrealTargetPlatform.IOS, Config, "");
BundlePath = AppDirectory;//Path.Combine(ProjectDirectory, "Binaries", "IOS", "Payload", ProjectName + ".app");
}
string RelativeEnginePath = Unreal.EngineDirectory.MakeRelativeTo(DirectoryReference.GetCurrentDirectory());
UnrealPluginLanguage UPL = new UnrealPluginLanguage(ProjectFile, UPLScripts, ProjectArches, null, UnrealTargetPlatform.IOS, Logger);
// Passing in true for distribution is not ideal here but given the way that ios packaging happens and this call chain it seems unavoidable for now, maybe there is a way to correctly pass it in that I can't find?
UPL.Init(ProjectArches.Select(x => x.NativeArch), true, RelativeEnginePath, BundlePath, ProjectDirectory, Config.ToString(), false);
return GenerateIOSPList(ProjectFile, Config, ProjectDirectory, bIsUnrealGame, GameName, bIsClient, ProjectName, InEngineDir, AppDirectory, UPL, BundleID, bBuildAsFramework, Logger);
}
protected virtual void CopyCloudResources(string InEngineDir, string AppDirectory)
{
CopyFiles(InEngineDir + "/Build/IOS/Cloud", AppDirectory, "*.*", true);
}
protected virtual void CopyCustomLaunchScreenResources(string InEngineDir, string AppDirectory, string BuildDirectory, ILogger Logger)
{
if (Directory.Exists(BuildDirectory + "/Resources/Interface/LaunchScreen.storyboardc"))
{
CopyFolder(BuildDirectory + "/Resources/Interface/LaunchScreen.storyboardc", AppDirectory + "/LaunchScreen.storyboardc", true);
CopyFiles(BuildDirectory + "/Resources/Interface/Assets", AppDirectory, "*", true);
}
else
{
Logger.LogWarning("Custom LaunchScreen Storyboard is checked but no compiled Storyboard could be found. Custom Storyboard compilation is only Mac compatible for now. Fallback to default Launchscreen");
CopyStandardLaunchScreenResources(InEngineDir, AppDirectory, BuildDirectory);
}
}
protected virtual void CopyStandardLaunchScreenResources(string InEngineDir, string AppDirectory, string BuildDirectory)
{
CopyFolder(InEngineDir + "/Build/IOS/Resources/Interface/LaunchScreen.storyboardc", AppDirectory + "/LaunchScreen.storyboardc", true);
if (File.Exists(BuildDirectory + "/Resources/Graphics/LaunchScreenIOS.png"))
{
CopyFiles(BuildDirectory + "/Resources/Graphics", AppDirectory, "LaunchScreenIOS.png", true);
}
else
{
CopyFiles(InEngineDir + "/Build/IOS/Resources/Graphics", AppDirectory, "LaunchScreenIOS.png", true);
}
}
protected virtual void CopyLaunchScreenResources(string InEngineDir, string AppDirectory, string BuildDirectory, ILogger Logger)
{
if (VersionUtilities.bCustomLaunchscreenStoryboard)
{
CopyCustomLaunchScreenResources(InEngineDir, AppDirectory, BuildDirectory, Logger);
}
else
{
CopyStandardLaunchScreenResources(InEngineDir, AppDirectory, BuildDirectory);
}
if (!File.Exists(AppDirectory + "/LaunchScreen.storyboardc/LaunchScreen.nib"))
{
Logger.LogError("Launchscreen.storyboard ViewController needs an ID named LaunchScreen");
}
}
public bool PrepForUATPackageOrDeploy(UnrealTargetConfiguration Config, FileReference ProjectFile, string InProjectName, string InProjectDirectory, FileReference Executable, string InEngineDir, bool bForDistribution, string CookFlavor, bool bIsDataDeploy, bool bCreateStubIPA, TargetReceipt Receipt)
{
List UPLScripts = CollectPluginDataPaths(Receipt.AdditionalProperties, Logger);
VersionNumber? SdkVersion = GetSdkVersion(Receipt);
bool bBuildAsFramework = GetCompileAsDll(Receipt);
return PrepForUATPackageOrDeploy(Config, ProjectFile, InProjectName, InProjectDirectory, Executable, InEngineDir, bForDistribution, CookFlavor, bIsDataDeploy, bCreateStubIPA, UPLScripts, "", bBuildAsFramework);
}
void CopyAllProvisions(string ProvisionDir, ILogger Logger)
{
try
{
FileInfo DestFileInfo;
if (!Directory.Exists(ProvisionDir))
{
throw new DirectoryNotFoundException(String.Format("Provision Directory {0} not found.", ProvisionDir), null);
}
string LocalProvisionFolder = MobileProvisionDirRef.FullName;
if (!Directory.Exists(LocalProvisionFolder))
{
Logger.LogDebug("Local Provision Folder {LocalProvisionFolder} not found, attempting to create...", LocalProvisionFolder);
Directory.CreateDirectory(LocalProvisionFolder);
if (Directory.Exists(LocalProvisionFolder))
{
Logger.LogDebug("Local Provision Folder {LocalProvisionFolder} created successfully.", LocalProvisionFolder);
}
else
{
throw new DirectoryNotFoundException(String.Format("Local Provision Folder {0} could not be created.", LocalProvisionFolder), null);
}
}
foreach (string Provision in Directory.EnumerateFiles(ProvisionDir, "*.mobileprovision", SearchOption.AllDirectories))
{
string LocalProvisionFile = Path.Combine(LocalProvisionFolder, Path.GetFileName(Provision));
bool LocalFileExists = File.Exists(LocalProvisionFile);
if (!LocalFileExists || File.GetLastWriteTime(LocalProvisionFile) < File.GetLastWriteTime(Provision))
{
if (LocalFileExists)
{
DestFileInfo = new FileInfo(LocalProvisionFile);
DestFileInfo.Attributes &= ~FileAttributes.ReadOnly;
}
File.Copy(Provision, LocalProvisionFile, true);
DestFileInfo = new FileInfo(LocalProvisionFile);
DestFileInfo.Attributes &= ~FileAttributes.ReadOnly;
}
}
}
catch (Exception Ex)
{
Logger.LogError("{Message}", Ex.ToString());
throw;
}
}
public bool PrepForUATPackageOrDeploy(UnrealTargetConfiguration Config, FileReference? ProjectFile, string InProjectName, string InProjectDirectory, FileReference Executable, string InEngineDir, bool bForDistribution, string CookFlavor, bool bIsDataDeploy, bool bCreateStubIPA, List UPLScripts, string? BundleID, bool bBuildAsFramework)
{
if (BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac)
{
throw new BuildException("UEDeployIOS.PrepForUATPackageOrDeploy only supports running on the Mac");
}
// If we are building as a framework, we don't need to do all of this.
if (bBuildAsFramework)
{
return false;
}
string SubDir = GetTargetPlatformName();
bool bIsUnrealGame = Executable.FullName.Contains("UnrealGame");
DirectoryReference BinaryPath = Executable.Directory!;
string GameExeName = Executable.GetFileName();
string GameName = bIsUnrealGame ? "UnrealGame" : GameExeName.Split("-".ToCharArray())[0];
string PayloadDirectory = BinaryPath + "/Payload";
string AppDirectory = PayloadDirectory + "/" + GameName + ".app";
string CookedContentDirectory = AppDirectory + "/cookeddata";
string BuildDirectory = InProjectDirectory + "/Build/" + SubDir;
string BuildDirectory_NFL = InProjectDirectory + "/Restricted/NotForLicensees/Build/" + SubDir;
string IntermediateDirectory = (bIsUnrealGame ? InEngineDir : InProjectDirectory) + "/Intermediate/" + SubDir;
if (AppleExports.UseModernXcode(ProjectFile))
{
Logger.LogInformation("Generating plist (only step needed when deploying with Modern Xcode)");
AppDirectory = BinaryPath + "/" + GameExeName + ".app";
GeneratePList(ProjectFile, Config, InProjectDirectory, bIsUnrealGame, GameExeName, false, InProjectName, InEngineDir, AppDirectory, UPLScripts, BundleID, bBuildAsFramework);
//// for now, copy the executable into the .app
//if (File.Exists(AppDirectory + "/" + GameName))
//{
// FileInfo GameFileInfo = new FileInfo(AppDirectory + "/" + GameName);
// GameFileInfo.Attributes = GameFileInfo.Attributes & ~FileAttributes.ReadOnly;
//}
//// copy the GameName binary
//File.Copy(BinaryPath + "/" + GameExeName, AppDirectory + "/" + GameName, true);
// none of this is needed with modern xcode
return false;
}
DirectoryReference.CreateDirectory(BinaryPath);
Directory.CreateDirectory(PayloadDirectory);
Directory.CreateDirectory(AppDirectory);
Directory.CreateDirectory(BuildDirectory);
Directory.CreateDirectory(MobileProvisionDirRef.FullName);
// create the entitlements file
// delete some old files if they exist
if (Directory.Exists(AppDirectory + "/_CodeSignature"))
{
Directory.Delete(AppDirectory + "/_CodeSignature", true);
}
if (File.Exists(AppDirectory + "/CustomResourceRules.plist"))
{
File.Delete(AppDirectory + "/CustomResourceRules.plist");
}
if (File.Exists(AppDirectory + "/embedded.mobileprovision"))
{
File.Delete(AppDirectory + "/embedded.mobileprovision");
}
if (File.Exists(AppDirectory + "/PkgInfo"))
{
File.Delete(AppDirectory + "/PkgInfo");
}
// install the provision
FileInfo DestFileInfo;
// always look for provisions in the IOS dir, even for TVOS
string ProvisionWithPrefix = InEngineDir + "/Build/IOS/UnrealGame.mobileprovision";
string ProjectProvision = InProjectName + ".mobileprovision";
if (File.Exists(Path.Combine(BuildDirectory, ProjectProvision)))
{
ProvisionWithPrefix = Path.Combine(BuildDirectory, ProjectProvision);
}
else
{
if (File.Exists(Path.Combine(BuildDirectory_NFL, ProjectProvision)))
{
ProvisionWithPrefix = Path.Combine(BuildDirectory_NFL, BuildDirectory, ProjectProvision);
}
else if (!File.Exists(ProvisionWithPrefix))
{
ProvisionWithPrefix = Path.Combine(InEngineDir, "Restricted/NotForLicensees/Build", SubDir, "UnrealGame.mobileprovision");
}
}
if (File.Exists(ProvisionWithPrefix))
{
Directory.CreateDirectory(MobileProvisionDirRef.FullName);
string ProjectProvisionPath = DirectoryReference.Combine(MobileProvisionDirRef, ProjectProvision).FullName;
if (File.Exists(ProjectProvisionPath))
{
DestFileInfo = new FileInfo(ProjectProvisionPath);
DestFileInfo.Attributes &= ~FileAttributes.ReadOnly;
}
File.Copy(ProvisionWithPrefix, ProjectProvisionPath, true);
DestFileInfo = new FileInfo(ProjectProvisionPath);
DestFileInfo.Attributes &= ~FileAttributes.ReadOnly;
}
if (!File.Exists(ProvisionWithPrefix) || Unreal.IsBuildMachine())
{
// copy all provisions from the game directory, the engine directory, notforlicensees directory, and, if defined, the ProvisionDirectory.
CopyAllProvisions(BuildDirectory, Logger);
CopyAllProvisions(InEngineDir + "/Build/IOS", Logger);
string? ProvisionDirectory = Environment.GetEnvironmentVariable("ProvisionDirectory");
if (!String.IsNullOrWhiteSpace(ProvisionDirectory))
{
CopyAllProvisions(ProvisionDirectory, Logger);
}
}
// install the distribution provision
ProvisionWithPrefix = InEngineDir + "/Build/IOS/UnrealGame_Distro.mobileprovision";
string ProjectDistroProvision = InProjectName + "_Distro.mobileprovision";
if (File.Exists(Path.Combine(BuildDirectory, ProjectDistroProvision)))
{
ProvisionWithPrefix = Path.Combine(BuildDirectory, ProjectDistroProvision);
}
else
{
if (File.Exists(Path.Combine(BuildDirectory_NFL, ProjectDistroProvision)))
{
ProvisionWithPrefix = Path.Combine(BuildDirectory_NFL, ProjectDistroProvision);
}
else if (!File.Exists(ProvisionWithPrefix))
{
ProvisionWithPrefix = Path.Combine(InEngineDir, "Restricted/NotForLicensees/Build", SubDir, "UnrealGame_Distro.mobileprovision");
}
}
if (File.Exists(ProvisionWithPrefix))
{
Directory.CreateDirectory(MobileProvisionDirRef.FullName);
string InProjectProvisionPath = DirectoryReference.Combine(MobileProvisionDirRef, InProjectName, "_Distro.mobileprovision").FullName;
if (File.Exists(InProjectProvisionPath))
{
DestFileInfo = new FileInfo(InProjectProvisionPath);
DestFileInfo.Attributes &= ~FileAttributes.ReadOnly;
}
File.Copy(ProvisionWithPrefix, InProjectProvisionPath, true);
DestFileInfo = new FileInfo(InProjectProvisionPath);
DestFileInfo.Attributes &= ~FileAttributes.ReadOnly;
}
GeneratePList(ProjectFile, Config, InProjectDirectory, bIsUnrealGame, GameExeName, false, InProjectName, InEngineDir, AppDirectory, UPLScripts, BundleID, bBuildAsFramework);
// ensure the destination is writable
if (File.Exists(AppDirectory + "/" + GameName))
{
FileInfo GameFileInfo = new FileInfo(AppDirectory + "/" + GameName);
GameFileInfo.Attributes &= ~FileAttributes.ReadOnly;
}
// copy the GameName binary
File.Copy(BinaryPath + "/" + GameExeName, AppDirectory + "/" + GameName, true);
//tvos support
if (SubDir == GetTargetPlatformName())
{
string BuildDirectoryFortvOS = InProjectDirectory + "/Build/IOS";
CopyLaunchScreenResources(InEngineDir, AppDirectory, BuildDirectoryFortvOS, Logger);
}
else
{
CopyLaunchScreenResources(InEngineDir, AppDirectory, BuildDirectory, Logger);
}
if (!bCreateStubIPA)
{
CopyCloudResources(InProjectDirectory, AppDirectory);
// copy additional engine framework assets in
// @todo tvos: TVOS probably needs its own assets?
string FrameworkAssetsPath = InEngineDir + "/Intermediate/IOS/FrameworkAssets";
// Let project override assets if they exist
if (Directory.Exists(InProjectDirectory + "/Intermediate/IOS/FrameworkAssets"))
{
FrameworkAssetsPath = InProjectDirectory + "/Intermediate/IOS/FrameworkAssets";
}
if (Directory.Exists(FrameworkAssetsPath))
{
CopyFolder(FrameworkAssetsPath, AppDirectory, true);
}
Directory.CreateDirectory(CookedContentDirectory);
}
return true;
}
public override bool PrepTargetForDeployment(TargetReceipt Receipt)
{
List UPLScripts = CollectPluginDataPaths(Receipt.AdditionalProperties, Logger);
bool bBuildAsFramework = GetCompileAsDll(Receipt);
return PrepTargetForDeployment(Receipt.ProjectFile, Receipt.TargetName, Receipt.BuildProducts.First(x => x.Type == BuildProductType.Executable).Path, Receipt.Platform, Receipt.Configuration, UPLScripts, false, "", bBuildAsFramework);
}
public bool PrepTargetForDeployment(FileReference? ProjectFile, string TargetName, FileReference Executable, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, List UPLScripts, bool bCreateStubIPA, string? BundleID, bool bBuildAsFramework)
{
string GameName = TargetName;
string ProjectDirectory = (DirectoryReference.FromFile(ProjectFile) ?? Unreal.EngineDirectory).FullName;
bool bIsUnrealGame = GameName.Contains("UnrealGame");
if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac && Environment.GetEnvironmentVariable("UBT_NO_POST_DEPLOY") != "true")
{
return PrepForUATPackageOrDeploy(Configuration, ProjectFile, GameName, ProjectDirectory, Executable, "../../Engine", bForDistribution, "", false, bCreateStubIPA, UPLScripts, BundleID, bBuildAsFramework);
}
else
{
// @todo tvos merge: This used to copy the bundle back - where did that code go? It needs to be fixed up for TVOS directories
GeneratePList(ProjectFile, Configuration, ProjectDirectory, bIsUnrealGame, GameName, false, (ProjectFile == null) ? "" : Path.GetFileNameWithoutExtension(ProjectFile.FullName), "../../Engine", "", UPLScripts, BundleID, bBuildAsFramework);
}
return true;
}
public static List CollectPluginDataPaths(List ReceiptProperties, ILogger Logger)
{
List PluginExtras = new List();
if (ReceiptProperties == null)
{
Logger.LogInformation("Receipt is NULL");
//Logger.LogInformation("Receipt is NULL");
return PluginExtras;
}
// collect plugin extra data paths from target receipt
IEnumerable Results = ReceiptProperties.Where(x => x.Name == "IOSPlugin");
foreach (ReceiptProperty Property in Results)
{
// Keep only unique paths
string PluginPath = Property.Value;
if (PluginExtras.FirstOrDefault(x => x == PluginPath) == null)
{
PluginExtras.Add(PluginPath);
Logger.LogInformation("IOSPlugin: {PluginPath}", PluginPath);
}
}
return PluginExtras;
}
public static void SafeFileCopy(FileInfo SourceFile, string DestinationPath, bool bOverwrite)
{
FileInfo DI = new FileInfo(DestinationPath);
if (DI.Exists && bOverwrite)
{
DI.IsReadOnly = false;
DI.Delete();
}
Directory.CreateDirectory(Path.GetDirectoryName(DestinationPath)!);
SourceFile.CopyTo(DestinationPath, bOverwrite);
FileInfo DI2 = new FileInfo(DestinationPath);
if (DI2.Exists)
{
DI2.IsReadOnly = false;
}
}
protected void CopyFiles(string SourceDirectory, string DestinationDirectory, string TargetFiles, bool bOverwrite = false)
{
DirectoryInfo SourceFolderInfo = new DirectoryInfo(SourceDirectory);
if (SourceFolderInfo.Exists)
{
FileInfo[] SourceFiles = SourceFolderInfo.GetFiles(TargetFiles);
foreach (FileInfo SourceFile in SourceFiles)
{
string DestinationPath = Path.Combine(DestinationDirectory, SourceFile.Name);
SafeFileCopy(SourceFile, DestinationPath, bOverwrite);
}
}
}
protected void CopyFolder(string SourceDirectory, string DestinationDirectory, bool bOverwrite = false, FilenameFilter? Filter = null)
{
Directory.CreateDirectory(DestinationDirectory);
RecursiveFolderCopy(new DirectoryInfo(SourceDirectory), new DirectoryInfo(DestinationDirectory), bOverwrite, Filter);
}
private static void RecursiveFolderCopy(DirectoryInfo SourceFolderInfo, DirectoryInfo DestFolderInfo, bool bOverwrite = false, FilenameFilter? Filter = null)
{
foreach (FileInfo SourceFileInfo in SourceFolderInfo.GetFiles())
{
string DestinationPath = Path.Combine(DestFolderInfo.FullName, SourceFileInfo.Name);
if (Filter != null && !Filter(DestinationPath))
{
continue;
}
SafeFileCopy(SourceFileInfo, DestinationPath, bOverwrite);
}
foreach (DirectoryInfo SourceSubFolderInfo in SourceFolderInfo.GetDirectories())
{
string DestFolderName = Path.Combine(DestFolderInfo.FullName, SourceSubFolderInfo.Name);
Directory.CreateDirectory(DestFolderName);
RecursiveFolderCopy(SourceSubFolderInfo, new DirectoryInfo(DestFolderName), bOverwrite);
}
}
}
}