Files
UnrealEngine/Engine/Source/Programs/UnrealBuildTool/Platform/Apple/AppleExports.cs
2025-05-18 13:04:45 +08:00

412 lines
18 KiB
C#

// 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
{
/// <summary>
/// Public Apple functions exposed to UAT
/// </summary>
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;
/// <summary>
/// True if we should force disable entitlements based on "-noEntitlements"
/// </summary>
/// <returns>bool</returns>
public static bool ForceNoEntitlements()
{
return bNoEntitlements;
}
/// <summary>
/// Is the current project using modern xcode?
/// </summary>
/// <param name="ProjectFile"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Get the given project's Swift settings
/// </summary>
/// <param name="ProjectFile"></param>
/// <param name="TargetType"></param>
/// <param name="Platform"></param>
/// <param name="bUseSwiftUIMain"></param>
/// <param name="bCreateBridgingHeader"></param>
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;
}
/// <summary>
/// This is a FilePath from UE settings, like /Game/Foo/Bar.txt
/// </summary>
/// <param name="ProductDirectory">Directory to use for /Game paths</param>
/// <param name="FilePath"></param>
/// <returns></returns>
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);
}
}
/// <summary>
/// Convert UnrealTargetPlatform to the platform that Xcode uses for -destination
/// </summary>
/// <param name="Platform"></param>
/// <param name="Architectures">The architecture we are targeting (really, this is for Simulators)</param>
/// <returns></returns>
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}");
}
/// <summary>
/// Different ways that xcodebuild is run, so the scripts can behave appropriately
/// </summary>
public enum XcodeBuildMode
{
/// <summary>
/// This is when hitting Build from in Xcode
/// </summary>
Default = 0,
/// <summary>
/// This runs after building when building on commandline directly with UBT
/// </summary>
PostBuildSync = 1,
/// <summary>
/// This runs when making a fully made .app in the Staged directory
/// </summary>
Stage = 2,
/// <summary>
/// This runs when packaging a full made .app into Project/Binaries
/// </summary>
Package = 3,
/// <summary>
/// This runs when packaging a .xcarchive for distribution
/// </summary>
Distribute = 4,
}
/// <summary>
/// Gets the AppStoreConnect auth options for a given project. Will return empty string if the project isn't set up to use ASC
/// </summary>
/// <param name="ProjectFile"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Generates a stub xcode project for the given project/platform/target combo, then builds or archives it
/// </summary>
/// <param name="ProjectFile">Project to build</param>
/// <param name="Platform">Platform to build</param>
/// <param name="Architectures">The architecture we are targeting (really, this is for Simulator to pass in the -destination field)</param>
/// <param name="Configuration">Configuration to build</param>
/// <param name="TargetName">Target to build</param>
/// <param name="BuildMode">Sets an envvar used inside the xcode project to control certain features</param>
/// <param name="Logger"></param>
/// <param name="ExtraOptions">Any extra options to pass to xcodebuild</param>
/// <param name="bForceDummySigning">If true, force signing with the - identity</param>
/// <param name="DestinationIds">Optional list of devices to target, mainly for adding to provision if needed</param>
/// <returns>xcode's exit code</returns>
public static int BuildWithStubXcodeProject(FileReference? ProjectFile, UnrealTargetPlatform Platform, UnrealArchitectures Architectures, UnrealTargetConfiguration Configuration,
string TargetName, XcodeBuildMode BuildMode, ILogger Logger, string ExtraOptions = "", bool bForceDummySigning = false, List<string>? 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);
}
/// <summary>
/// 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
/// </summary>
/// <param name="UProjectFile">Location of .uproject file (or null for the engine project</param>
/// <param name="Platform">The platform to generate a project for</param>
/// <param name="TargetName">The name of the target being built, so we can generate a more minimal project</param>
/// <param name="bForDistribution">True if this is making a bild for uploading to app store</param>
/// <param name="bNoEntitlements">True if we should disable entitlements</param>
/// <param name="Logger">Logging object</param>
/// <param name="GeneratedProjectFile">Returns the .xcworkspace that was made</param>
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);
}
/// <summary>
/// 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
/// </summary>
/// <param name="XcodeProject">The .xcworkspace file to build</param>
/// <param name="Platform">THe platform to make the .app for</param>
/// <param name="Architectures">The architecture we are targeting (really, this is for Simulator to pass in the -destination field)</param>
/// <param name="bUseAutomaticCodeSigning">True when using automatic codesigning via xcodebuild</param>
/// <param name="SchemeName">The name of the scheme (basically the target on the .xcworkspace)</param>
/// <param name="Configuration">Which configuration to make (Debug, etc)</param>
/// <param name="Action">Action (build, archive, etc)</param>
/// <param name="ExtraOptions">Extra options to pass to xcodebuild</param>
/// <param name="Logger">Logging object</param>
/// <param name="DestinationIds">Optional list of devices to target, mainly for adding to provision if needed</param>
/// <returns>xcode's exit code</returns>
public static int FinalizeAppWithXcode(DirectoryReference XcodeProject, UnrealTargetPlatform Platform, UnrealArchitectures Architectures, bool bUseAutomaticCodeSigning, string SchemeName, string Configuration, string Action, string ExtraOptions, ILogger Logger, List<string>? DestinationIds=null)
{
return AppleToolChain.FinalizeAppWithXcode(XcodeProject, Platform, Architectures, bUseAutomaticCodeSigning, SchemeName, Configuration, Action, ExtraOptions, Logger, DestinationIds);
}
/// <summary>
/// Pass along the call to UEBuildTarget.MakeBinaryFileName, but taking an extension instead of a binary type, since the type is hidden
/// </summary>
/// <param name="BinaryName"></param>
/// <param name="Separator"></param>
/// <param name="Platform"></param>
/// <param name="Configuration"></param>
/// <param name="Architectures"></param>
/// <param name="UndecoratedConfiguration"></param>
/// <param name="Extension"></param>
/// <returns></returns>
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);
}
/// <summary>
/// Finds the latest .xcarchive for a given Target name (the .xcarchive will start with this name then have a data appended)
/// </summary>
/// <param name="TargetName">Name of the target to look for, this will be the prefix for the .xcarchive to search for</param>
/// <param name="SearchPath">If null, look in Xcode Archives library, otherwiwse, look here</param>
/// <returns></returns>
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<DirectoryReference> 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<DirectoryReference> 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;
}
/// <summary>
/// Returns the location of the Mobile Provisioning directory. Handles Mac and Windows.
/// On mac, this value changed starting with Xcode16
/// </summary>
/// <returns>The location of the Mobile Provisioning directory</returns>
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!;
}
}
}
}