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