// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Xml.Linq; using EpicGames.Core; using Microsoft.Extensions.Logging; namespace UnrealBuildTool { /// /// Public IOS functions exposed to UAT /// public static class IOSExports { /// /// /// /// /// /// /// /// /// public static void GetProvisioningData(FileReference InProject, bool Distribution, out string? MobileProvision, out string? SigningCertificate, out string? TeamUUID, out bool bAutomaticSigning) { IOSProjectSettings ProjectSettings = ((IOSPlatform)UEBuildPlatform.GetBuildPlatform(UnrealTargetPlatform.IOS)).ReadProjectSettings(InProject); if (ProjectSettings == null) { MobileProvision = null; SigningCertificate = null; TeamUUID = null; bAutomaticSigning = false; return; } if (ProjectSettings.bAutomaticSigning) { MobileProvision = null; SigningCertificate = null; TeamUUID = ProjectSettings.TeamID; bAutomaticSigning = true; } else { IOSProvisioningData Data = ((IOSPlatform)UEBuildPlatform.GetBuildPlatform(UnrealTargetPlatform.IOS)).ReadProvisioningData(ProjectSettings, Distribution); if (Data == null) { // no provisioning, swith to automatic MobileProvision = null; SigningCertificate = null; TeamUUID = ProjectSettings.TeamID; bAutomaticSigning = true; } else { MobileProvision = Data.MobileProvision; SigningCertificate = Data.SigningCertificate; TeamUUID = Data.TeamUUID; bAutomaticSigning = false; } } } /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// Logger for output /// public static bool PrepForUATPackageOrDeploy(UnrealTargetConfiguration Config, FileReference ProjectFile, string InProjectName, DirectoryReference InProjectDirectory, FileReference Executable, DirectoryReference InEngineDir, bool bForDistribution, string CookFlavor, bool bIsDataDeploy, bool bCreateStubIPA, FileReference BuildReceiptFileName, ILogger Logger) { TargetReceipt Receipt = TargetReceipt.Read(BuildReceiptFileName); return new UEDeployIOS(Logger).PrepForUATPackageOrDeploy(Config, ProjectFile, InProjectName, InProjectDirectory.FullName, Executable, InEngineDir.FullName, bForDistribution, CookFlavor, bIsDataDeploy, bCreateStubIPA, Receipt); } /// /// /// /// /// /// /// /// /// /// /// /// /// /// Logger for output /// public static bool GeneratePList(FileReference ProjectFile, UnrealTargetConfiguration Config, DirectoryReference ProjectDirectory, bool bIsUnrealGame, string GameName, bool bIsClient, string ProjectName, DirectoryReference InEngineDir, DirectoryReference AppDirectory, TargetReceipt Receipt, ILogger Logger) { return new UEDeployIOS(Logger).GeneratePList(ProjectFile, Config, ProjectDirectory.FullName, bIsUnrealGame, GameName, bIsClient, ProjectName, InEngineDir.FullName, AppDirectory.FullName, Receipt); } /// /// /// /// /// /// /// public static void StripSymbols(UnrealTargetPlatform PlatformType, FileReference SourceFile, FileReference TargetFile, ILogger Logger) { IOSProjectSettings ProjectSettings = ((IOSPlatform)UEBuildPlatform.GetBuildPlatform(PlatformType)).ReadProjectSettings(null); IOSToolChain ToolChain = new IOSToolChain(null, ProjectSettings, ClangToolChainOptions.None, Logger); ToolChain.StripSymbols(SourceFile, TargetFile); } /// /// /// /// /// /// /// /// public static void GenerateAssetCatalog(FileReference ProjectFile, FileReference Executable, DirectoryReference StageDirectory, UnrealTargetPlatform Platform, ILogger Logger) { // Determine whether the user has modified icons that require a remote Mac to build. bool bUserImagesExist = false; DirectoryReference ResourcesDir = IOSToolChain.GenerateAssetCatalog(ProjectFile, Platform, ref bUserImagesExist); // Don't attempt to do anything remotely if the user is using the default UE images. if (!bUserImagesExist) { return; } // Also don't attempt to use a remote Mac if packaging for TVOS on PC. if (Platform == UnrealTargetPlatform.TVOS && BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac) { return; } // Compile the asset catalog immediately if (BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac) { FileReference OutputFile = FileReference.Combine(StageDirectory, "Assets.car"); RemoteMac Remote = new RemoteMac(ProjectFile, Logger); Remote.RunAssetCatalogTool(Platform, ResourcesDir, OutputFile, Logger); } else { // Get the output file FileReference OutputFile = IOSToolChain.GetAssetCatalogFile(Platform, Executable); // Delete the Assets.car file to force the asset catalog to build every time, because // removals of files or copies of icons (for instance) with a timestamp earlier than // the last generated Assets.car will result in nothing built. if (FileReference.Exists(OutputFile)) { FileReference.Delete(OutputFile); } // Run the process locally using (Process Process = new Process()) { Process.StartInfo.FileName = "/usr/bin/xcrun"; Process.StartInfo.Arguments = IOSToolChain.GetAssetCatalogArgs(Platform, ResourcesDir.FullName, OutputFile.Directory.FullName); ; Process.StartInfo.UseShellExecute = false; Utils.RunLocalProcess(Process); } } } /// /// Set a secondary remote Mac to retrieve built data on a remote Mac. /// /// public static void SetSecondaryRemoteMac(string ClientPlatform, FileReference ProjectFile, ILogger Logger) { RemoteMac Remote = new RemoteMac(ProjectFile, Logger, null, true, true); Remote.RetrieveFilesGeneratedOnPrimaryMac(ProjectFile, Logger, ClientPlatform); RemoteMac SecondaryRemote = new RemoteMac(ProjectFile, Logger, null, false); SecondaryRemote.UploadToSecondaryMac(ProjectFile, Logger); } /// /// Prepare the build and project on a Remote Mac to be able to debug an iOS or tvOS package built remotely /// /// TargetPlatform, iOS or tvOS /// Location of .uproject file /// A logger /// public static void PrepareRemoteMacForDebugging(string ClientPlatform, FileReference ProjectFile, ILogger Logger) { RemoteMac Remote = new RemoteMac(ProjectFile, Logger); Remote.PrepareToDebug(ClientPlatform, ProjectFile, Logger); } /// /// /// /// /// /// /// /// /// public static void WriteEntitlements(UnrealTargetPlatform Platform, ConfigHierarchy PlatformGameConfig, string AppName, FileReference? MobileProvisionFile, bool bForDistribution, string IntermediateDir) { // get some info from the mobileprovisioning file // the iCloud identifier and the bundle id may differ string iCloudContainerIdentifier = ""; string iCloudContainerIdentifiersXML = "iCloud.$(CFBundleIdentifier)"; string UbiquityContainerIdentifiersXML = "iCloud.$(CFBundleIdentifier)"; string iCloudServicesXML = "CloudKitCloudDocuments"; string UbiquityKVStoreIdentifiersXML = "\t$(TeamIdentifierPrefix)$(CFBundleIdentifier)"; string OutputFileName = Path.Combine(IntermediateDir, AppName + ".entitlements"); if (MobileProvisionFile != null && File.Exists(MobileProvisionFile.FullName)) { Console.WriteLine("Write entitlements from provisioning file {0}", MobileProvisionFile); MobileProvisionContents MobileProvisionContent = MobileProvisionContents.Read(MobileProvisionFile); iCloudContainerIdentifier = MobileProvisionContent.GetNodeValueByName("com.apple.developer.icloud-container-identifiers"); iCloudContainerIdentifiersXML = MobileProvisionContent.GetNodeXMLValueByName("com.apple.developer.icloud-container-identifiers"); string entitlementXML = MobileProvisionContent.GetNodeXMLValueByName("com.apple.developer.icloud-services"); if (!entitlementXML.Contains('*') || Platform == UnrealTargetPlatform.TVOS) { // for iOS, replace the generic value (*) with the default iCloudServicesXML = entitlementXML; } entitlementXML = MobileProvisionContent.GetNodeXMLValueByName("com.apple.developer.ubiquity-container-identifiers"); if (!entitlementXML.Contains('*') || !bForDistribution) { // for distribution, replace the generic value (*) with the default UbiquityContainerIdentifiersXML = entitlementXML; } entitlementXML = MobileProvisionContent.GetNodeXMLValueByName("com.apple.developer.ubiquity-kvstore-identifier"); if (!entitlementXML.Contains('*') || !bForDistribution) { // for distribution, replace the generic value (*) with the default UbiquityKVStoreIdentifiersXML = entitlementXML; } } else { Console.WriteLine("Couldn't locate the mobile provisioning file {0}", MobileProvisionFile); } // write the entitlements file { bool bCloudKitSupported = false; PlatformGameConfig.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bEnableCloudKitSupport", out bCloudKitSupported); Directory.CreateDirectory(Path.GetDirectoryName(OutputFileName)!); // we need to have something so Xcode will compile, so we just set the get-task-allow, since we know the value, // which is based on distribution or not (true means debuggable) StringBuilder Text = new StringBuilder(); Text.AppendLine(""); Text.AppendLine(""); Text.AppendLine(""); Text.AppendLine(""); Text.AppendLine("\tget-task-allow"); Text.AppendLine(String.Format("\t<{0}/>", bForDistribution ? "false" : "true")); if (bCloudKitSupported) { if (!String.IsNullOrEmpty(iCloudContainerIdentifiersXML)) { Text.AppendLine("\tcom.apple.developer.icloud-container-identifiers"); Text.AppendLine(iCloudContainerIdentifiersXML); } if (!String.IsNullOrEmpty(iCloudServicesXML)) { Text.AppendLine("\tcom.apple.developer.icloud-services"); Text.AppendLine(iCloudServicesXML); } if (!String.IsNullOrEmpty(UbiquityContainerIdentifiersXML)) { Text.AppendLine("\tcom.apple.developer.ubiquity-container-identifiers"); Text.AppendLine(UbiquityContainerIdentifiersXML); } if (!String.IsNullOrEmpty(UbiquityKVStoreIdentifiersXML)) { Text.AppendLine("\tcom.apple.developer.ubiquity-kvstore-identifier"); Text.AppendLine(UbiquityKVStoreIdentifiersXML); } Text.AppendLine("\tcom.apple.developer.icloud-container-environment"); Text.AppendLine(String.Format("\t{0}", bForDistribution ? "Production" : "Development")); } bool bRemoteNotificationsSupported = false; PlatformGameConfig.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bEnableRemoteNotificationsSupport", out bRemoteNotificationsSupported); // for TVOS we need push notifications when building for distribution with CloudKit if (bCloudKitSupported && bForDistribution && Platform == UnrealTargetPlatform.TVOS) { bRemoteNotificationsSupported = true; } if (bRemoteNotificationsSupported) { Text.AppendLine("\taps-environment"); Text.AppendLine(String.Format("\t{0}", bForDistribution ? "production" : "development")); } // for Sign in with Apple bool bSignInWithAppleSupported = false; PlatformGameConfig.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bEnableSignInWithAppleSupport", out bSignInWithAppleSupported); if (bSignInWithAppleSupported) { Text.AppendLine("\tcom.apple.developer.applesignin"); Text.AppendLine("\tDefault"); } // Add Multi-user support for tvOS bool bUserSwitching = false; PlatformGameConfig.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bUserSwitching", out bUserSwitching); if (bUserSwitching && Platform == UnrealTargetPlatform.TVOS) { Text.AppendLine("\tcom.apple.developer.user-management"); Text.AppendLine("\truns-as-current-user"); } // As of iOS15, to support GameCenter, "com.apple.developer.game-center" entitlement must be set bool bEnableGameCenterSupport = false; PlatformGameConfig.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bEnableGameCenterSupport", out bEnableGameCenterSupport); if (bEnableGameCenterSupport) { Text.AppendLine("\tcom.apple.developer.game-center"); Text.AppendLine("\t"); } // End of entitlements Text.AppendLine(""); Text.AppendLine(""); if (File.Exists(OutputFileName)) { // read existing file string ExisitingFileContents = File.ReadAllText(OutputFileName); bool bFileChanged = !ExisitingFileContents.Equals(Text.ToString(), StringComparison.Ordinal); // overwrite file if there are content changes if (bFileChanged) { File.WriteAllText(OutputFileName, Text.ToString()); } } else { File.WriteAllText(OutputFileName, Text.ToString()); } } // create a pList key named ICloudContainerIdentifier // to be used at run-time when intializing the CloudKit services if (!String.IsNullOrEmpty(iCloudContainerIdentifier)) { string PListFile = IntermediateDir + "/" + AppName + "-Info.plist"; if (File.Exists(PListFile)) { string OldPListData = File.ReadAllText(PListFile); XDocument XDoc; try { XDoc = XDocument.Parse(OldPListData); if (XDoc.DocumentType != null) { XDoc.DocumentType.InternalSubset = null; } XElement? dictElement = XDoc.Root?.Element("dict"); if (dictElement != null) { XElement containerIdKeyNew = new XElement("key", "ICloudContainerIdentifier"); XElement containerIdValueNew = new XElement("string", iCloudContainerIdentifier); XElement? containerIdKey = dictElement.Elements("key").FirstOrDefault(x => x.Value == "ICloudContainerIdentifier"); if (containerIdKey != null) { // if ICloudContainerIdentifier already exists in the pList file, update its value XElement? containerIdValue = containerIdKey.ElementsAfterSelf("string").FirstOrDefault(); if (containerIdValue != null) { containerIdValue.Value = iCloudContainerIdentifier; } else { containerIdKey.AddAfterSelf(containerIdValueNew); } } else { // add ICloudContainerIdentifier to the pList dictElement.Add(containerIdKeyNew); dictElement.Add(containerIdValueNew); } XDoc.Save(PListFile); } } catch (Exception e) { throw new BuildException("plist is invalid {0}\n{1}", e, OldPListData); } } } } } }