// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.IO; using AutomationTool; using UnrealBuildTool; using UnrealBuildBase; using EpicGames.Core; using Microsoft.Extensions.Logging; using System.Security.Cryptography; public class MacPlatform : ApplePlatform { /// /// Default architecture to build projects for. Defaults to Intel /// protected UnrealArchitectures ProjectTargetArchitectures = new(UnrealArch.X64); public MacPlatform() : base(UnrealTargetPlatform.Mac) { } public override void PlatformSetupParams(ref ProjectParams ProjParams) { base.PlatformSetupParams(ref ProjParams); string ConfigTargetArchicture = ""; ConfigHierarchy PlatformEngineConfig = null; if (ProjParams.EngineConfigs.TryGetValue(PlatformType, out PlatformEngineConfig)) { PlatformEngineConfig.GetString("/Script/MacTargetPlatform.MacTargetSettings", "TargetArchitecture", out ConfigTargetArchicture); if (ConfigTargetArchicture.ToLower().Contains("intel")) { ProjectTargetArchitectures = new UnrealArchitectures(UnrealArch.X64); } else if (ConfigTargetArchicture.ToLower().Contains("apple")) { ProjectTargetArchitectures = new UnrealArchitectures(UnrealArch.Arm64); } else if (ConfigTargetArchicture.ToLower().Contains("universal")) { ProjectTargetArchitectures = new UnrealArchitectures(new[] { UnrealArch.X64, UnrealArch.Arm64 }); } } } public override void PersistSdkRootVar() { string UeSdksRoot = Environment.GetEnvironmentVariable("UE_SDKS_ROOT"); if (UeSdksRoot != null) { base.PersistSdkRootVar(); string AutoSdkFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ".autosdk"); if (!File.Exists(AutoSdkFile)) { using (StreamWriter wr = new StreamWriter(AutoSdkFile)) { wr.WriteLine(UeSdksRoot); } } } } public override DeviceInfo[] GetDevices() { List Devices = new List(); if (HostPlatform.Current.HostEditorPlatform == TargetPlatformType) { DeviceInfo LocalMachine = new DeviceInfo(TargetPlatformType, Unreal.MachineName, Unreal.MachineName, Environment.OSVersion.Version.ToString(), "Computer", true, true); Devices.Add(LocalMachine); } return Devices.ToArray(); } public override string GetCookPlatform(bool bDedicatedServer, bool bIsClientOnly) { const string NoEditorCookPlatform = "Mac"; const string ServerCookPlatform = "MacServer"; const string ClientCookPlatform = "MacClient"; if (bDedicatedServer) { return ServerCookPlatform; } else if (bIsClientOnly) { return ClientCookPlatform; } else { return NoEditorCookPlatform; } } public override string GetEditorCookPlatform() { return "MacEditor"; } /// /// Override PreBuildAgenda so we can control the architecture that targets are built for based on /// project settings and the current user environment /// /// /// /// public override void PreBuildAgenda(UnrealBuild Build, UnrealBuild.BuildAgenda Agenda, ProjectParams Params) { base.PreBuildAgenda(Build, Agenda, Params); // Go through the agenda for all targets and set the architecture if needed foreach (UnrealBuild.BuildTarget Target in Agenda.Targets) { // if building for Distribution, and no arch is already specified, then get the distro architectures and use that for this build // editors aren't usually distributed, so if we are doing distribution, it's probably not for the editor target, and we don't want to make universal // editors just to make a distribution client if (Params.Distribution && !Target.TargetName.Contains("Editor") && !Target.UBTArgs.ToLower().Contains("-architecture=")) { UnrealArchitectures DistroArches = UnrealArchitectureConfig.ForPlatform(UnrealTargetPlatform.Mac).DistributionArchitectures(Params.RawProjectPath, Target.TargetName); Target.UBTArgs += " -architecture=" + DistroArches.ToString(); } } } private void StageAppBundle(DeploymentContext SC, DirectoryReference InPath, StagedDirectoryReference NewName) { // Files with DebugFileExtensions should always be DebugNonUFS List DebugExtensions = GetDebugFileExtensions(); if(DirectoryExists(InPath.FullName)) { foreach (FileReference InputFile in DirectoryReference.EnumerateFiles(InPath, "*", SearchOption.AllDirectories)) { StagedFileReference OutputFile = StagedFileReference.Combine(NewName, InputFile.MakeRelativeTo(InPath)); StagedFileType FileType = DebugExtensions.Any(x => InputFile.HasExtension(x)) ? StagedFileType.DebugNonUFS : StagedFileType.NonUFS; SC.StageFile(FileType, InputFile, OutputFile); } } } public override void GetFilesToDeployOrStage(ProjectParams Params, DeploymentContext SC) { // Stage all the build products foreach (StageTarget Target in SC.StageTargets) { SC.StageBuildProductsFromReceipt(Target.Receipt, Target.RequireFilesExist, Params.bTreatNonShippingBinariesAsDebugFiles); } if (SC.bStageCrashReporter) { StagedDirectoryReference CrashReportClientPath = StagedDirectoryReference.Combine("Engine/Binaries", SC.PlatformDir, "CrashReportClient.app"); StageAppBundle(SC, DirectoryReference.Combine(SC.LocalRoot, "Engine/Binaries", SC.PlatformDir, "CrashReportClient.app"), CrashReportClientPath); } // Find the app bundle path List Exes = GetExecutableNames(SC); foreach (var Exe in Exes) { StagedDirectoryReference AppBundlePath = null; if (Exe.IsUnderDirectory(DirectoryReference.Combine(SC.RuntimeProjectRootDir, "Binaries", SC.PlatformDir))) { AppBundlePath = StagedDirectoryReference.Combine(SC.ShortProjectName, "Binaries", SC.PlatformDir, Path.GetFileNameWithoutExtension(Exe.FullName) + ".app"); } else if (Exe.IsUnderDirectory(DirectoryReference.Combine(SC.RuntimeRootDir, "Engine/Binaries", SC.PlatformDir))) { AppBundlePath = StagedDirectoryReference.Combine("Engine/Binaries", SC.PlatformDir, Path.GetFileNameWithoutExtension(Exe.FullName) + ".app"); } // Copy the custom icon and Steam dylib, if needed if (AppBundlePath != null) { FileReference AppIconsFile = FileReference.Combine(SC.ProjectRoot, "Build", "Mac", "Application.icns"); if(FileReference.Exists(AppIconsFile)) { SC.StageFile(StagedFileType.NonUFS, AppIconsFile, StagedFileReference.Combine(AppBundlePath, "Contents", "Resources", "Application.icns")); } } } // Copy the splash screen, Mac specific FileReference SplashImage = FileReference.Combine(SC.ProjectRoot, "Content", "Splash", "Splash.bmp"); if(FileReference.Exists(SplashImage)) { SC.StageFile(StagedFileType.NonUFS, SplashImage); } // Stage the bootstrap executable (modern doesn't need it with full .apps) if (!Params.NoBootstrapExe && !AppleExports.UseModernXcode(Params.RawProjectPath)) { foreach (StageTarget Target in SC.StageTargets) { BuildProduct Executable = Target.Receipt.BuildProducts.FirstOrDefault(x => x.Type == BuildProductType.Executable); if (Executable != null) { // only create bootstraps for executables List StagedFiles = SC.FilesToStage.NonUFSFiles.Where(x => x.Value == Executable.Path).Select(x => x.Key).ToList(); if (StagedFiles.Count > 0 && Executable.Path.FullName.Replace("\\", "/").Contains("/" + TargetPlatformType.ToString() + "/")) { string BootstrapArguments = ""; if (!ShouldStageCommandLine(Params, SC)) { if (!SC.IsCodeBasedProject) { BootstrapArguments = String.Format("../../../{0}/{0}.uproject", SC.ShortProjectName); } else { BootstrapArguments = SC.ShortProjectName; } } string BootstrapExeName; if (SC.StageTargetConfigurations.Count > 1) { BootstrapExeName = Path.GetFileName(Executable.Path.FullName) + ".app"; } else if (Params.IsCodeBasedProject) { // We want Mac-Shipping etc in the bundle name BootstrapExeName = Path.GetFileName(Executable.Path.FullName) + ".app"; } else { BootstrapExeName = SC.ShortProjectName + ".app"; } string AppSuffix = ".app" + Path.DirectorySeparatorChar; string AppPath = Executable.Path.FullName.Substring(0, Executable.Path.FullName.LastIndexOf(AppSuffix) + AppSuffix.Length); foreach (var DestPath in StagedFiles) { string AppRelativePath = DestPath.Name.Substring(0, DestPath.Name.LastIndexOf(AppSuffix) + AppSuffix.Length); StageBootstrapExecutable(SC, BootstrapExeName, AppPath, AppRelativePath, BootstrapArguments); } } } } } // Copy the ShaderCache files, if they exist FileReference DrawCacheFile = FileReference.Combine(SC.ProjectRoot, "Content", "DrawCache.ushadercache"); if(FileReference.Exists(DrawCacheFile)) { SC.StageFile(StagedFileType.UFS, DrawCacheFile); } FileReference ByteCodeCacheFile = FileReference.Combine(SC.ProjectRoot, "Content", "ByteCodeCache.ushadercode"); if(FileReference.Exists(ByteCodeCacheFile)) { SC.StageFile(StagedFileType.UFS, ByteCodeCacheFile); } { // Get the final output directory for cooked data DirectoryReference CookOutputDir; if(!String.IsNullOrEmpty(Params.CookOutputDir)) { CookOutputDir = DirectoryReference.Combine(new DirectoryReference(Params.CookOutputDir), SC.CookPlatform); } else if(Params.CookInEditor) { CookOutputDir = DirectoryReference.Combine(SC.ProjectRoot, "Saved", "EditorCooked", SC.CookPlatform); } else { CookOutputDir = DirectoryReference.Combine(SC.ProjectRoot, "Saved", "Cooked", SC.CookPlatform); } } } string GetValueFromInfoPlist(string InfoPlist, string Key, string DefaultValue = "") { string Value = DefaultValue; string KeyString = "" + Key + ""; int KeyIndex = InfoPlist.IndexOf(KeyString); if (KeyIndex > 0) { int ValueStartIndex = InfoPlist.IndexOf("", KeyIndex + KeyString.Length) + "".Length; int ValueEndIndex = InfoPlist.IndexOf("", ValueStartIndex); if (ValueStartIndex > 0 && ValueEndIndex > ValueStartIndex) { Value = InfoPlist.Substring(ValueStartIndex, ValueEndIndex - ValueStartIndex); } } return Value; } void StageBootstrapExecutable(DeploymentContext SC, string ExeName, string TargetFile, string StagedRelativeTargetPath, string StagedArguments) { DirectoryReference InputApp = DirectoryReference.Combine(SC.LocalRoot, "Engine", "Binaries", SC.PlatformDir, "BootstrapPackagedGame.app"); if (InternalUtils.SafeDirectoryExists(InputApp.FullName)) { // Create the new bootstrap program DirectoryReference IntermediateDir = DirectoryReference.Combine(SC.ProjectRoot, "Intermediate", "Staging"); InternalUtils.SafeCreateDirectory(IntermediateDir.FullName); DirectoryReference IntermediateApp = DirectoryReference.Combine(IntermediateDir, ExeName); if (DirectoryReference.Exists(IntermediateApp)) { DirectoryReference.Delete(IntermediateApp, true); } CloneDirectory(InputApp.FullName, IntermediateApp.FullName); // Rename the executable string GameName = Path.GetFileNameWithoutExtension(ExeName); FileReference.Move(FileReference.Combine(IntermediateApp, "Contents", "MacOS", "BootstrapPackagedGame"), FileReference.Combine(IntermediateApp, "Contents", "MacOS", GameName)); // Copy the icon string SrcInfoPlistPath = CombinePaths(TargetFile, "Contents", "Info.plist"); string SrcInfoPlist = File.ReadAllText(SrcInfoPlistPath); string IconName = GetValueFromInfoPlist(SrcInfoPlist, "CFBundleIconFile"); if (!string.IsNullOrEmpty(IconName)) { string IconPath = CombinePaths(TargetFile, "Contents", "Resources", IconName + ".icns"); InternalUtils.SafeCreateDirectory(CombinePaths(IntermediateApp.FullName, "Contents", "Resources")); File.Copy(IconPath, CombinePaths(IntermediateApp.FullName, "Contents", "Resources", IconName + ".icns")); } // Update Info.plist contents string DestInfoPlistPath = CombinePaths(IntermediateApp.FullName, "Contents", "Info.plist"); string DestInfoPlist = File.ReadAllText(DestInfoPlistPath); string AppIdentifier = GetValueFromInfoPlist(SrcInfoPlist, "CFBundleIdentifier"); if (AppIdentifier == "com.epicgames.UnrealGame") { AppIdentifier = ""; } string Copyright = GetValueFromInfoPlist(SrcInfoPlist, "NSHumanReadableCopyright"); string BundleVersion = GetValueFromInfoPlist(SrcInfoPlist, "CFBundleVersion", "1"); string ShortVersion = GetValueFromInfoPlist(SrcInfoPlist, "CFBundleShortVersionString", "1.0"); DestInfoPlist = DestInfoPlist.Replace("com.epicgames.BootstrapPackagedGame", string.IsNullOrEmpty(AppIdentifier) ? "com.epicgames." + GameName + "_bootstrap" : AppIdentifier + "_bootstrap"); DestInfoPlist = DestInfoPlist.Replace("BootstrapPackagedGame", GameName); DestInfoPlist = DestInfoPlist.Replace("__UE4_ICON_FILE__", IconName); DestInfoPlist = DestInfoPlist.Replace("__UE4_APP_TO_LAUNCH__", StagedRelativeTargetPath); DestInfoPlist = DestInfoPlist.Replace("__UE4_COMMANDLINE__", StagedArguments); DestInfoPlist = DestInfoPlist.Replace("__UE4_COPYRIGHT__", Copyright); DestInfoPlist = DestInfoPlist.Replace("__UE4_BUNDLE_VERSION__", BundleVersion); DestInfoPlist = DestInfoPlist.Replace("__UE4_SHORT_VERSION__", ShortVersion); File.WriteAllText(DestInfoPlistPath, DestInfoPlist); StageAppBundle(SC, IntermediateApp, new StagedDirectoryReference(ExeName)); } } private void RemoveExtraRPaths(ProjectParams Params, DeploymentContext SC) { // When we link the executable we add RPATH entries for all possible places where dylibs can be loaded from, so that the same executable can be used from Binaries/Mac // as well as in a packaged, self-contained application. In recent versions of macOS, Gatekeeper doesn't allow RPATHs pointing to folders that don't exist, // so we remove these based on the type of packaging (Params.CreateAppBundle). List Exes = GetExecutableNames(SC); foreach (var ExePath in Exes) { IProcessResult CommandResult = Run("otool", "-l \"" + ExePath + "\"", null, ERunOptions.None); if (CommandResult.ExitCode == 0) { StringReader Reader = new StringReader(CommandResult.Output); Regex RPathPattern = new Regex(@"^\s+path (?.+)\s\(offset"); string ToRemovePattern = Params.CreateAppBundle ? "/../../../" : "@loader_path/../UE4/"; string OutputLine; while ((OutputLine = Reader.ReadLine()) != null) { if (OutputLine.EndsWith("cmd LC_RPATH")) { OutputLine = Reader.ReadLine(); OutputLine = Reader.ReadLine(); Match RPathMatch = RPathPattern.Match(OutputLine); if (RPathMatch.Success) { string RPath = RPathMatch.Groups["rpath"].Value; if (RPath.Contains(ToRemovePattern)) { Run("xcrun", "install_name_tool -delete_rpath \"" + RPath + "\" \"" + ExePath + "\"", null, ERunOptions.NoStdOutCapture); } } } } } } } private void FixupFrameworks(string TargetPath) { DirectoryReference TargetCEFDir = DirectoryReference.Combine(new DirectoryReference(TargetPath), "Engine/Binaries/ThirdParty/CEF3/Mac"); DirectoryReference X86Framework = DirectoryReference.Combine(TargetCEFDir, "Chromium Embedded Framework x86.framework"); DirectoryReference X86Versions = DirectoryReference.Combine(X86Framework, "Versions"); DirectoryReference Arm64Framework = DirectoryReference.Combine(TargetCEFDir, "Chromium Embedded Framework arm64.framework"); DirectoryReference Arm64Versions = DirectoryReference.Combine(Arm64Framework, "Versions"); DirectoryReference EngineCEFDir = DirectoryReference.Combine(Unreal.EngineDirectory, "Binaries/ThirdParty/CEF3/Mac"); FileReference X86Zip = FileReference.Combine(EngineCEFDir, "Chromium Embedded Framework x86.framework.zip"); FileReference Arm64Zip = FileReference.Combine(EngineCEFDir, "Chromium Embedded Framework arm64.framework.zip"); // if the archive has a framework without Versions directory, it won't be allowed for App Store submission, so replace it with the zipped version // that has the proper symlinks if (DirectoryReference.Exists(X86Framework) && !DirectoryReference.Exists(X86Versions)) { Logger.LogInformation($"Replacing {X86Framework} with {X86Zip}..."); DirectoryReference.Delete(X86Framework, true); Utils.RunLocalProcessAndLogOutput("/usr/bin/unzip", $"-q -o \"{X86Zip}\" -d \"{TargetCEFDir}\" -x \"__MACOSX/*\" \"*.DS_Store\"", Logger); } if (DirectoryReference.Exists(Arm64Framework) && !DirectoryReference.Exists(Arm64Versions)) { Logger.LogInformation($"Replacing {Arm64Framework} with {Arm64Zip}..."); DirectoryReference.Delete(Arm64Framework, true); Utils.RunLocalProcessAndLogOutput("/usr/bin/unzip", $"-q -o \"{Arm64Zip}\" -d \"{TargetCEFDir}\" -x \"__MACOSX/*\" \"*.DS_Store\"", Logger); } } public override void ProcessArchivedProject(ProjectParams Params, DeploymentContext SC) { // nothing to do with modern if (AppleExports.UseModernXcode(Params.RawProjectPath)) { return; } if (Params.CreateAppBundle) { string ExeName = SC.StageExecutables[0]; string BundlePath = SC.IsCodeBasedProject ? CombinePaths(SC.ArchiveDirectory.FullName, ExeName + ".app") : CombinePaths(SC.ArchiveDirectory.FullName, SC.ShortProjectName + ".app"); if (SC.bIsCombiningMultiplePlatforms) { // when combining multiple platforms, don't merge the content into the .app, use the one in the Binaries directory BundlePath = CombinePaths(SC.ArchiveDirectory.FullName, SC.ShortProjectName, "Binaries", "Mac", ExeName + ".app"); if (!DirectoryExists(BundlePath)) { // if the .app wasn't there, just skip out (we don't require executables when combining) return; } } string TargetPath = CombinePaths(BundlePath, "Contents", "UE"); if (!SC.bIsCombiningMultiplePlatforms) { DeleteDirectory(true, BundlePath); string SourceBundlePath = CombinePaths(SC.ArchiveDirectory.FullName, SC.ShortProjectName, "Binaries", "Mac", ExeName + ".app"); if (!DirectoryExists(SourceBundlePath)) { SourceBundlePath = CombinePaths(SC.ArchiveDirectory.FullName, "Engine", "Binaries", "Mac", ExeName + ".app"); if (!DirectoryExists(SourceBundlePath)) { SourceBundlePath = CombinePaths(SC.ArchiveDirectory.FullName, "Engine", "Binaries", "Mac", "UE4.app"); } } RenameDirectory(SourceBundlePath, BundlePath, true); DeleteDirectory(true, TargetPath); string[] StagedFiles = Directory.GetFiles(SC.ArchiveDirectory.FullName, "*", SearchOption.TopDirectoryOnly); foreach (string FilePath in StagedFiles) { string TargetFilePath = CombinePaths(TargetPath, Path.GetFileName(FilePath)); CreateDirectory(Path.GetDirectoryName(TargetFilePath)); RenameFile(FilePath, TargetFilePath, true); } string[] StagedDirectories = Directory.GetDirectories(SC.ArchiveDirectory.FullName, "*", SearchOption.TopDirectoryOnly); foreach (string DirPath in StagedDirectories) { string DirName = Path.GetFileName(DirPath); if (!DirName.EndsWith(".app")) { string TargetDirPath = CombinePaths(TargetPath, DirName); CreateDirectory(Path.GetDirectoryName(TargetDirPath)); RenameDirectory(DirPath, TargetDirPath, true); } } FixupFrameworks(TargetPath); } // Update executable name, icon and entry in Info.plist string UE4GamePath = CombinePaths(BundlePath, "Contents", "MacOS", ExeName); if (!SC.IsCodeBasedProject && ExeName != SC.ShortProjectName && FileExists(UE4GamePath)) { string GameExePath = CombinePaths(BundlePath, "Contents", "MacOS", SC.ShortProjectName); DeleteFile(GameExePath); RenameFile(UE4GamePath, GameExePath); string DefaultIconPath = CombinePaths(BundlePath, "Contents", "Resources", "UnrealGame.icns"); string CustomIconSrcPath = CombinePaths(BundlePath, "Contents", "Resources", "Application.icns"); string CustomIconDestPath = CombinePaths(BundlePath, "Contents", "Resources", SC.ShortProjectName + ".icns"); if (FileExists(CustomIconSrcPath)) { DeleteFile(DefaultIconPath); DeleteFile(CustomIconDestPath); RenameFile(CustomIconSrcPath, CustomIconDestPath, true); } else if (FileExists(DefaultIconPath)) { DeleteFile(CustomIconDestPath); RenameFile(DefaultIconPath, CustomIconDestPath, true); } string InfoPlistPath = CombinePaths(BundlePath, "Contents", "Info.plist"); string InfoPlistContents = File.ReadAllText(InfoPlistPath); InfoPlistContents = InfoPlistContents.Replace(ExeName, SC.ShortProjectName); InfoPlistContents = InfoPlistContents.Replace("UnrealGame", "" + SC.ShortProjectName + ""); DeleteFile(InfoPlistPath); WriteAllText(InfoPlistPath, InfoPlistContents); // we now need to re-sign the .app because we modified the .plist // we codesign with ad-hoc, and if the Developer ID Application cert exists, attempt to use it, ignore any errors Utils.RunLocalProcessAndReturnStdOut("/usr/bin/codesign", $"-f -s - \"{BundlePath}\"", null); Utils.RunLocalProcessAndReturnStdOut("/usr/bin/codesign", $"-f -s \"Developer ID Application\" \"{BundlePath}\"", null); } // we now need to re-sign the .app because we modified the .plist // we codesign with ad-hoc, and if the Developer ID Application cert exists, attempt to use it, ignore any errors Utils.RunLocalProcessAndReturnStdOut("/usr/bin/codesign", $"-f -s - \"{BundlePath}\"", null); Utils.RunLocalProcessAndReturnStdOut("/usr/bin/codesign", $"-f -s \"Developer ID Application\" \"{BundlePath}\"", null); if (!SC.bIsCombiningMultiplePlatforms) { // creating these directories when the content isn't moved into the application causes it // to fail to load, and isn't needed CreateDirectory(CombinePaths(TargetPath, "Engine", "Binaries", "Mac")); CreateDirectory(CombinePaths(TargetPath, SC.ShortProjectName, "Binaries", "Mac")); } // Find any dSYM files in the Manifest_DebugFiles_Mac file, move them to the archive directory, and remove them from the manifest. string[] DebugManifests = FindFiles("Manifest_DebugFiles_Mac.txt", true, SC.ArchiveDirectory.FullName); if ( DebugManifests.Count() > 0 ) { string DebugManifest=DebugManifests[0]; List ManifestLines = new List(File.ReadAllLines(DebugManifest)); bool ModifyManifest = false; for (int ManifestLineIndex = ManifestLines.Count - 1; ManifestLineIndex >= 0; ManifestLineIndex--) { string ManifestLine = ManifestLines[ManifestLineIndex]; int TabIndex = ManifestLine.IndexOf('\t'); if (TabIndex > 0) { string FoundDebugFile = ManifestLine.Substring(0, TabIndex); if (FoundDebugFile.Contains(".dSYM")) { FoundDebugFile = CombinePaths(TargetPath, FoundDebugFile); string MovedDebugFile = CombinePaths(SC.ArchiveDirectory.FullName, Path.GetFileName(FoundDebugFile)); RenameFile(FoundDebugFile, MovedDebugFile); Logger.LogInformation("Moving debug file: '{FoundDebugFile}')", FoundDebugFile); ManifestLines.RemoveAt(ManifestLineIndex); ModifyManifest = true; } } } if (ModifyManifest) { File.WriteAllLines(DebugManifest, ManifestLines.ToArray()); } } // If there is a dSYM matching the exe name rename it so it matches the project name string ExeDSYMName = CombinePaths(SC.ArchiveDirectory.FullName, ExeName + ".dSYM"); string ProjectDSYMName = CombinePaths(SC.ArchiveDirectory.FullName, SC.ShortProjectName + ".dSYM"); if (ExeDSYMName != ProjectDSYMName) { if (FileExists(ExeDSYMName)) { RenameFile(ExeDSYMName, ProjectDSYMName); } } Run("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister", "-f " + BundlePath, null, ERunOptions.Default); } } public override IProcessResult RunClient(ERunOptions ClientRunFlags, string ClientApp, string ClientCmdLine, ProjectParams Params) { if (AppleExports.UseModernXcode(Params.RawProjectPath)) { // moden creates a full .app in the root of the Staged dir, but ClientApp as passed in is in Staged/Project/Binaries/Mac, which exists, but is not a full app string ExeName = Path.GetFileNameWithoutExtension(ClientApp); Int32 BaseDirLen = Params.BaseStageDirectory.Length; string StageSubDir = ClientApp.Substring(BaseDirLen, ClientApp.IndexOf("/", BaseDirLen + 1) - BaseDirLen); ClientApp = CombinePaths(Params.BaseStageDirectory, StageSubDir, $"{ExeName}.app/Contents/MacOS/{ExeName}"); if (!File.Exists(ClientApp)) { // Could be blueprint only projects which ClientApp would be pointing at non-existing UnrealGame/UnrealClient ExeName = Params.RawProjectPath.GetFileNameWithoutAnyExtensions(); ClientApp = CombinePaths(Params.BaseStageDirectory, StageSubDir, $"{ExeName}.app/Contents/MacOS/{ExeName}"); } } else if (!File.Exists(ClientApp)) { if (Directory.Exists(ClientApp + ".app")) { ClientApp += ".app/Contents/MacOS/" + Path.GetFileName(ClientApp); } else { Int32 BaseDirLen = Params.BaseStageDirectory.Length; string StageSubDir = ClientApp.Substring(BaseDirLen, ClientApp.IndexOf("/", BaseDirLen + 1) - BaseDirLen); ClientApp = CombinePaths(Params.BaseStageDirectory, StageSubDir, Params.ShortProjectName + ".app/Contents/MacOS/" + Params.ShortProjectName); } } PushDir(Path.GetDirectoryName(ClientApp)); // Always start client process and don't wait for exit. IProcessResult ClientProcess = Run(ClientApp, ClientCmdLine, null, ClientRunFlags | ERunOptions.NoWaitForExit); PopDir(); return ClientProcess; } public override bool IsSupported { get { return true; } } public override List GetDebugFileExtensions() { return new List { ".dSYM" }; } public override bool CanHostPlatform(UnrealTargetPlatform Platform) { if (Platform == UnrealTargetPlatform.IOS || Platform == UnrealTargetPlatform.Mac || Platform == UnrealTargetPlatform.TVOS) { return true; } return false; } public override bool ShouldStageCommandLine(ProjectParams Params, DeploymentContext SC) { // modern mode doesn't use the Bootstrap wrapper app, so we always insert the commandline file into the .app so double-clicking the .app works return AppleExports.UseModernXcode(Params.RawProjectPath); // !String.IsNullOrEmpty(Params.StageCommandline) || !String.IsNullOrEmpty(Params.RunCommandline) || (!Params.IsCodeBasedProject && Params.NoBootstrapExe); } public override bool SignExecutables(DeploymentContext SC, ProjectParams Params) { if (UnrealBuildTool.BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac) { if (Params.Archive) { // Remove extra RPATHs if we will be archiving the project Logger.LogInformation("Removing extraneous rpath entries"); RemoveExtraRPaths(Params, SC); } // with modern, the .app we'd want to sign is in the root of the staging directory, so this doesn't do anything, // and that one is already signed. note this is only done when Staging, so it doesn't affect codesigning the editor if (!AppleExports.UseModernXcode(Params.RawProjectPath)) { // Sign everything we built List FilesToSign = GetExecutableNames(SC); Logger.LogInformation("{Text}", "RuntimeProjectRootDir: " + SC.RuntimeProjectRootDir); foreach (var Exe in FilesToSign) { Logger.LogInformation("{Text}", "Signing: " + Exe); string AppBundlePath = ""; if (Exe.IsUnderDirectory(DirectoryReference.Combine(SC.RuntimeProjectRootDir, "Binaries", SC.PlatformDir))) { Logger.LogInformation("Starts with Binaries"); AppBundlePath = CombinePaths(SC.RuntimeProjectRootDir.FullName, "Binaries", SC.PlatformDir, Path.GetFileNameWithoutExtension(Exe.FullName) + ".app"); } else if (Exe.IsUnderDirectory(DirectoryReference.Combine(SC.RuntimeRootDir, "Engine/Binaries", SC.PlatformDir))) { Logger.LogInformation("Starts with Engine/Binaries"); AppBundlePath = CombinePaths("Engine/Binaries", SC.PlatformDir, Path.GetFileNameWithoutExtension(Exe.FullName) + ".app"); } Logger.LogInformation("{Text}", "Signing: " + AppBundlePath); CodeSign.SignMacFileOrFolder(AppBundlePath); } } } return true; } public override void StripSymbols(FileReference SourceFile, FileReference TargetFile) { MacExports.StripSymbols(SourceFile, TargetFile, Log.Logger); } }