// 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); } } } }