// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using EpicGames.Core;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Microsoft.Extensions.Logging;
using UnrealBuildBase;
namespace UnrealBuildTool
{
///
/// Public Apple functions exposed to UAT
///
public static class AppleExports
{
private static bool bForceModernXcode = Environment.CommandLine.Contains("-modernxcode", StringComparison.OrdinalIgnoreCase);
private static bool bForceLegacyXcode = Environment.CommandLine.Contains("-legacyxcode", StringComparison.OrdinalIgnoreCase);
private static bool bNoEntitlements = Environment.CommandLine.Contains("-noEntitlements", StringComparison.OrdinalIgnoreCase);
private static DirectoryReference? _MobileProvisionDir;
///
/// True if we should force disable entitlements based on "-noEntitlements"
///
/// bool
public static bool ForceNoEntitlements()
{
return bNoEntitlements;
}
///
/// Is the current project using modern xcode?
///
///
///
public static bool UseModernXcode(FileReference? ProjectFile)
{
if (bForceModernXcode)
{
if (bForceLegacyXcode)
{
throw new BuildException("Both -modernxcode and -legacyxcode were specified, please use one or the other.");
}
Log.TraceInformationOnce("Forcing MODERN XCODE with -modernxcode");
return true;
}
if (bForceLegacyXcode)
{
Log.TraceInformationOnce("Forcing LEGACY XCODE with -legacyxcode");
return false;
}
// Modern Xcode mode does this now
bool bUseModernXcode = false;
if (OperatingSystem.IsMacOS())
{
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, ProjectFile?.Directory, UnrealTargetPlatform.Mac);
Ini.TryGetValue("/Script/MacTargetPlatform.XcodeProjectSettings", "bUseModernXcode", out bUseModernXcode);
Log.TraceInformationOnce("Choosing {0} XCODE based on .ini settings", bUseModernXcode ? "MODERN" : "LEGACY");
}
else
{
Log.TraceInformationOnce("Forcing LEGACY XCODE because host OS is not Mac");
}
return bUseModernXcode;
}
///
/// Get the given project's Swift settings
///
///
///
///
///
///
public static void GetSwiftIntegrationSettings(FileReference? ProjectFile, TargetType TargetType, UnrealTargetPlatform Platform, out bool bUseSwiftUIMain, out bool bCreateBridgingHeader)
{
if (TargetType == TargetType.Editor)
{
bUseSwiftUIMain = false;
}
else
{
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, ProjectFile?.Directory, Platform);
Ini.TryGetValue("/Script/MacTargetPlatform.XcodeProjectSettings", "bUseSwiftUIMain", out bUseSwiftUIMain);
}
// for now always create bridging headers (which let's cpp call into swift)
bCreateBridgingHeader = true;
}
///
/// This is a FilePath from UE settings, like /Game/Foo/Bar.txt
///
/// Directory to use for /Game paths
///
///
public static FileReference ConvertFilePath(DirectoryReference? ProductDirectory, string FilePath)
{
// for FilePath params, pull the path out of the struct
if (FilePath.StartsWith("(FilePath=", StringComparison.OrdinalIgnoreCase))
{
FilePath = ConfigHierarchy.GetStructEntry(FilePath, "FilePath", false)!;
}
if (FilePath.StartsWith("/Engine/", StringComparison.OrdinalIgnoreCase))
{
return FileReference.Combine(Unreal.EngineDirectory, FilePath.Substring(8));
}
else if (ProductDirectory != null && FilePath.StartsWith("/Game/", StringComparison.OrdinalIgnoreCase))
{
return FileReference.Combine(ProductDirectory, FilePath.Substring(6));
}
else if (FilePath.StartsWith("/", StringComparison.OrdinalIgnoreCase))
{
// Absolute path
return new FileReference(FilePath);
}
else
{
// UE-193103, using the file selector in UE will set the path relative to /Engine/Binaries/Mac
return FileReference.Combine(Unreal.EngineDirectory, "Binaries", "Mac", FilePath);
}
}
///
/// Convert UnrealTargetPlatform to the platform that Xcode uses for -destination
///
///
/// The architecture we are targeting (really, this is for Simulators)
///
public static string GetDestinationPlatform(UnrealTargetPlatform Platform, UnrealArchitectures Architectures)
{
if (Platform == UnrealTargetPlatform.Mac)
{
return "macOS";
}
else if (Platform == UnrealTargetPlatform.IOS)
{
return Architectures.SingleArchitecture == UnrealArch.IOSSimulator ? "iOS Simulator" : "iOS";
}
else if (Platform == UnrealTargetPlatform.TVOS)
{
return Architectures.SingleArchitecture == UnrealArch.TVOSSimulator ? "tvOS Simulator" : "tvOS";
}
else if (Platform == UnrealTargetPlatform.VisionOS)
{
return Architectures.SingleArchitecture == UnrealArch.IOSSimulator ? "visionOS Simulator" : "visionOS";
}
throw new BuildException($"Unknown plaform {Platform}");
}
///
/// Different ways that xcodebuild is run, so the scripts can behave appropriately
///
public enum XcodeBuildMode
{
///
/// This is when hitting Build from in Xcode
///
Default = 0,
///
/// This runs after building when building on commandline directly with UBT
///
PostBuildSync = 1,
///
/// This runs when making a fully made .app in the Staged directory
///
Stage = 2,
///
/// This runs when packaging a full made .app into Project/Binaries
///
Package = 3,
///
/// This runs when packaging a .xcarchive for distribution
///
Distribute = 4,
}
///
/// Gets the AppStoreConnect auth options for a given project. Will return empty string if the project isn't set up to use ASC
///
///
///
public static string GetXcodeBuildAuthOptions(FileReference? ProjectFile)
{
string Options = "";
// handle AppStore Connect settings
bool bUseAppStoreConnect;
ConfigHierarchy SharedPlatformIni = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, ProjectFile?.Directory, UnrealTargetPlatform.Mac);
SharedPlatformIni.TryGetValue("/Script/MacTargetPlatform.XcodeProjectSettings", "bUseAppStoreConnect", out bUseAppStoreConnect);
if (bUseAppStoreConnect)
{
string? IssuerID, KeyID, KeyPath;
if (SharedPlatformIni.TryGetValue("/Script/MacTargetPlatform.XcodeProjectSettings", "AppStoreConnectIssuerID", out IssuerID) &&
SharedPlatformIni.TryGetValue("/Script/MacTargetPlatform.XcodeProjectSettings", "AppStoreConnectKeyID", out KeyID) &&
SharedPlatformIni.TryGetValue("/Script/MacTargetPlatform.XcodeProjectSettings", "AppStoreConnectKeyPath", out KeyPath))
{
FileReference KeyFile = ConvertFilePath(ProjectFile?.Directory, KeyPath);
Options += $" -authenticationKeyIssuerID {IssuerID}";
Options += $" -authenticationKeyID {KeyID}";
Options += $" -authenticationKeyPath \"{KeyFile}\"";
}
}
return Options;
}
///
/// Generates a stub xcode project for the given project/platform/target combo, then builds or archives it
///
/// Project to build
/// Platform to build
/// The architecture we are targeting (really, this is for Simulator to pass in the -destination field)
/// Configuration to build
/// Target to build
/// Sets an envvar used inside the xcode project to control certain features
///
/// Any extra options to pass to xcodebuild
/// If true, force signing with the - identity
/// Optional list of devices to target, mainly for adding to provision if needed
/// xcode's exit code
public static int BuildWithStubXcodeProject(FileReference? ProjectFile, UnrealTargetPlatform Platform, UnrealArchitectures Architectures, UnrealTargetConfiguration Configuration,
string TargetName, XcodeBuildMode BuildMode, ILogger Logger, string ExtraOptions = "", bool bForceDummySigning = false, List? DestinationIds = null)
{
DirectoryReference? GeneratedProjectFile;
// we don't use distro flag when making a modern project
AppleExports.GenerateRunOnlyXcodeProject(ProjectFile, Platform, TargetName, bForDistribution: false, bNoEntitlements: bNoEntitlements, Logger, out GeneratedProjectFile);
if (GeneratedProjectFile == null)
{
return 1;
}
bool bUseAutomaticCodeSigning = false;
if (bForceDummySigning)
{
ExtraOptions += " CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY= PROVISIONING_PROFILE_SPECIFIER=";
}
else
{
// look for the special app store connect key information
ConfigHierarchy SharedPlatformIni = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, ProjectFile?.Directory, UnrealTargetPlatform.Mac);
SharedPlatformIni.TryGetValue("/Script/MacTargetPlatform.XcodeProjectSettings", "bUseAutomaticCodeSigning", out bUseAutomaticCodeSigning);
// disable automatic signing, if some extra options imply manual
if (ExtraOptions.Contains("CODE_SIGN_IDENTITY"))
{
bUseAutomaticCodeSigning = false;
}
if (bUseAutomaticCodeSigning)
{
ExtraOptions += " -allowProvisioningUpdates";
ExtraOptions += GetXcodeBuildAuthOptions(ProjectFile);
}
}
// run xcodebuild on the generated project to make the .app
string XcodeBuildAction = BuildMode == XcodeBuildMode.Distribute ? "archive" : "build";
return AppleExports.FinalizeAppWithXcode(GeneratedProjectFile!, Platform, Architectures, bUseAutomaticCodeSigning, TargetName, Configuration.ToString(), XcodeBuildAction, ExtraOptions + $" UE_XCODE_BUILD_MODE={BuildMode}", Logger, DestinationIds);
}
///
/// Genearate an run-only Xcode project, that is not meant to be used for anything else besides code-signing/running/etc of the native .app bundle
///
/// Location of .uproject file (or null for the engine project
/// The platform to generate a project for
/// The name of the target being built, so we can generate a more minimal project
/// True if this is making a bild for uploading to app store
/// True if we should disable entitlements
/// Logging object
/// Returns the .xcworkspace that was made
public static void GenerateRunOnlyXcodeProject(FileReference? UProjectFile, UnrealTargetPlatform Platform, string TargetName, bool bForDistribution, bool bNoEntitlements, ILogger Logger, out DirectoryReference? GeneratedProjectFile)
{
AppleToolChain.GenerateRunOnlyXcodeProject(UProjectFile, Platform, TargetName, bForDistribution, bNoEntitlements, Logger, out GeneratedProjectFile);
}
///
/// Version of FinalizeAppWithXcode that is meant for modern xcode mode, where we assume all codesigning is setup already in the project, so nothing else is needed
///
/// The .xcworkspace file to build
/// THe platform to make the .app for
/// The architecture we are targeting (really, this is for Simulator to pass in the -destination field)
/// True when using automatic codesigning via xcodebuild
/// The name of the scheme (basically the target on the .xcworkspace)
/// Which configuration to make (Debug, etc)
/// Action (build, archive, etc)
/// Extra options to pass to xcodebuild
/// Logging object
/// Optional list of devices to target, mainly for adding to provision if needed
/// xcode's exit code
public static int FinalizeAppWithXcode(DirectoryReference XcodeProject, UnrealTargetPlatform Platform, UnrealArchitectures Architectures, bool bUseAutomaticCodeSigning, string SchemeName, string Configuration, string Action, string ExtraOptions, ILogger Logger, List? DestinationIds=null)
{
return AppleToolChain.FinalizeAppWithXcode(XcodeProject, Platform, Architectures, bUseAutomaticCodeSigning, SchemeName, Configuration, Action, ExtraOptions, Logger, DestinationIds);
}
///
/// Pass along the call to UEBuildTarget.MakeBinaryFileName, but taking an extension instead of a binary type, since the type is hidden
///
///
///
///
///
///
///
///
///
public static string MakeBinaryFileName(string BinaryName, string Separator, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, UnrealArchitectures Architectures, UnrealTargetConfiguration UndecoratedConfiguration, string? Extension)
{
string StandardBinaryName = UEBuildTarget.MakeBinaryFileName(BinaryName, Separator, Platform, Configuration, Architectures, UndecoratedConfiguration, UEBuildBinaryType.Executable);
return System.IO.Path.ChangeExtension(StandardBinaryName, Extension);
}
///
/// Finds the latest .xcarchive for a given Target name (the .xcarchive will start with this name then have a data appended)
///
/// Name of the target to look for, this will be the prefix for the .xcarchive to search for
/// If null, look in Xcode Archives library, otherwiwse, look here
///
public static DirectoryReference? FindLatestXcArchive(string TargetName, DirectoryReference? SearchPath=null)
{
DirectoryReference UserDir = new DirectoryReference(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
DirectoryReference Library = SearchPath ?? DirectoryReference.Combine(UserDir, "Library/Developer/Xcode/Archives");
// order date named folders (use creating data, not name, but same thing)
List DateDirs = DirectoryReference.EnumerateDirectories(Library).ToList();
DateDirs.SortBy(x => Directory.GetCreationTime(x.FullName));
DateDirs.Reverse();
// go through each folder, starting at most recent, looking for an archive for the target
string Wildcard = $"{TargetName} *.xcarchive";
foreach (DirectoryReference DateDir in DateDirs)
{
List XcArchives = DirectoryReference.EnumerateDirectories(DateDir, Wildcard).ToList();
if (XcArchives.Count > 0)
{
XcArchives.SortBy(x => Directory.GetCreationTime(x.FullName));
DirectoryReference XcArchive = XcArchives.Last();
return XcArchive;
}
}
return null;
}
///
/// Returns the location of the Mobile Provisioning directory. Handles Mac and Windows.
/// On mac, this value changed starting with Xcode16
///
/// The location of the Mobile Provisioning directory
public static DirectoryReference GetProvisionDirectory()
{
if (!String.IsNullOrEmpty(_MobileProvisionDir?.FullName))
{
return _MobileProvisionDir!;
}
if (OperatingSystem.IsMacOS())
{
// Default to the Xcode15 and less location
string MobileProvisionDir = "/Library/MobileDevice/Provisioning Profiles/";
// Find out the version number in Xcode.app/Contents/Info.plist
int ExitCode;
string XcodeVersion = Utils.RunLocalProcessAndReturnStdOut("/bin/sh",
$"-c 'plutil -extract CFBundleShortVersionString raw $(xcode-select -p)/../Info.plist'", out ExitCode);
if (ExitCode == 0 && XcodeVersion != null)
{
// parse it into Major/Minor version numbers
int Major = 15;
int Minor = 0;
try
{
string[] Tokens = XcodeVersion!.Split(".".ToCharArray());
if (Tokens.Length >= 2)
{
Major = Int32.Parse(Tokens[0]);
Minor = Int32.Parse(Tokens[1]);
}
}
catch (Exception)
{
}
if (Major >= 16)
{
MobileProvisionDir = "/Library/Developer/Xcode/UserData/Provisioning Profiles/";
}
}
_MobileProvisionDir = new DirectoryReference(Environment.GetEnvironmentVariable("HOME") + MobileProvisionDir);
Console.WriteLine(" Setting Mobile Provision Profile Dir: {0}", _MobileProvisionDir?.FullName ?? "Not set");
return _MobileProvisionDir!;
}
else
{
_MobileProvisionDir = new DirectoryReference(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "/Apple Computer/MobileDevice/Provisioning Profiles/");
Console.WriteLine(" Setting Mobile Provision Profile Dir: {0}", _MobileProvisionDir?.FullName ?? "Not set");
return _MobileProvisionDir!;
}
}
}
}