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