// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; namespace UnrealBuildTool { class MacToolChainSettings : AppleToolChainSettings { public override string GetTargetVersionForTargetType(TargetType Type) => ((ApplePlatformSDK)UEBuildPlatformSDK.GetSDKForPlatform("Mac")!).GetBuildTargetVersion(Type); /// /// Which version of the Mac OS X to allow at run time /// public static string MinMacBuildVersion(TargetType TargetType) { return ((ApplePlatformSDK)UEBuildPlatformSDK.GetSDKForPlatform("Mac")!).GetBuildTargetVersion(TargetType); } /// /// Minimum version of Mac OS X to actually run on, running on earlier versions will display the system minimum version error dialog and exit. /// public static string MinMacDeploymentVersion(TargetType TargetType) { return ((ApplePlatformSDK)UEBuildPlatformSDK.GetSDKForPlatform("Mac")!).GetDeploymentTargetVersion(TargetType); } /// /// Constructor /// /// Whether to output verbose logging /// Logger for output public MacToolChainSettings(bool bVerbose, ILogger Logger) // passing 0.0 for target version because we have per-target versions when making the Tuple : base("MacOSX", null, "macos", "0.0", bVerbose, Logger) { } } /// /// Mac toolchain wrapper /// class MacToolChain : AppleToolChain { public MacToolChain(ReadOnlyTargetRules? Target, ClangToolChainOptions InOptions, ILogger InLogger) : base(Target, () => new MacToolChainSettings(true, InLogger), InOptions, InLogger) { } /// /// Which compiler\linker frontend to use /// private const string MacCompiler = "clang++"; /// /// Which archiver to use /// private const string MacArchiver = "libtool"; protected override ClangToolChainInfo GetToolChainInfo() { FileReference CompilerPath = FileReference.Combine(Settings.ToolchainDir, MacCompiler); FileReference ArchiverPath = FileReference.Combine(Settings.ToolchainDir, MacArchiver); return new AppleToolChainInfo(UnrealTargetPlatform.Mac, ApplePlatformSDK.DeveloperDir, CompilerPath, ArchiverPath, Logger); } public static DirectoryReference FindProductDirectory(FileReference? ProjectFile, DirectoryReference BinaryDir, string? NameIfProgram) { // a project file is always used if there is one if (ProjectFile != null) { return ProjectFile.Directory; } // look to see if this is a program, and hunt down where the Programs directory is if (NameIfProgram != null) { return FindProgramDirectoryFromSource(BinaryDir, NameIfProgram, true) ?? Unreal.EngineDirectory; } return Unreal.EngineDirectory; } public static DirectoryReference? FindProgramDirectoryFromSource(DirectoryReference StartingDir, string ProgramName, bool bCreateIfNotFound) { DirectoryReference? ProgramFinder = StartingDir; while (ProgramFinder != null && !String.Equals(ProgramFinder.GetDirectoryName(), "Source", StringComparison.CurrentCultureIgnoreCase) && !String.Equals(ProgramFinder.GetDirectoryName(), "Intermediate", StringComparison.CurrentCultureIgnoreCase) && !String.Equals(ProgramFinder.GetDirectoryName(), "Binaries", StringComparison.CurrentCultureIgnoreCase)) { ProgramFinder = ProgramFinder.ParentDirectory; } // we are now at Source or similar directory, go up one more, then into Programs, and finally the "project" directory if (ProgramFinder != null) { DirectoryReference ProgramsDir = DirectoryReference.Combine(ProgramFinder, "../Programs"); // if it doesn't exist, something went wrong, and we can't use this. throw an exception to catch it, for now if (!DirectoryReference.Exists(ProgramsDir)) { throw new BuildException($"Unable to find Programs directory for {ProgramName}, when starting in {StartingDir}"); } ProgramFinder = DirectoryReference.Combine(ProgramsDir, ProgramName); // if it exists, we have a ProductDir we can use for plists, icons, etc if (!DirectoryReference.Exists(ProgramFinder)) { if (bCreateIfNotFound) { DirectoryReference.CreateDirectory(ProgramFinder); return ProgramFinder; } } else { return ProgramFinder; } } return null; } /// protected override void GetCompileArguments_WarningsAndErrors(CppCompileEnvironment CompileEnvironment, List Arguments) { base.GetCompileArguments_WarningsAndErrors(CompileEnvironment, Arguments); //Arguments.Add("-Wsign-compare"); // fed up of not seeing the signed/unsigned warnings we get on Windows - lets enable them here too. } /// protected override void GetCompileArguments_Debugging(CppCompileEnvironment CompileEnvironment, List Arguments) { base.GetCompileArguments_Debugging(CompileEnvironment, Arguments); // TODO: Mac always enables exceptions, is this correct? if (!CompileEnvironment.bEnableExceptions) { Arguments.Remove("-fno-exceptions"); Arguments.Remove("-DPLATFORM_EXCEPTIONS_DISABLED=1"); } Arguments.Add("-fexceptions"); Arguments.Add("-DPLATFORM_EXCEPTIONS_DISABLED=0"); if (CompileEnvironment.bHideSymbolsByDefault) { Arguments.Add("-fvisibility-ms-compat"); Arguments.Add("-fvisibility-inlines-hidden"); } } /// protected override void GetCompileArguments_AdditionalArgs(CppCompileEnvironment CompileEnvironment, List Arguments) { if (!String.IsNullOrWhiteSpace(CompileEnvironment.AdditionalArguments)) { string EscapedAdditionalArgs = String.Empty; foreach (string AdditionalArg in CompileEnvironment.AdditionalArguments.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) { Match DefinitionMatch = Regex.Match(AdditionalArg, "-D\"?(?.*)=(?.*)\"?"); if (DefinitionMatch.Success) { EscapedAdditionalArgs += String.Format(" -D{0}=\"{1}\"", DefinitionMatch.Groups["Name"].Value, DefinitionMatch.Groups["Value"].Value); } else { EscapedAdditionalArgs += " " + AdditionalArg; } } if (!String.IsNullOrWhiteSpace(EscapedAdditionalArgs)) { Arguments.Add(EscapedAdditionalArgs); } } } /// protected override void GetCompileArguments_Global(CppCompileEnvironment CompileEnvironment, List Arguments) { base.GetCompileArguments_Global(CompileEnvironment, Arguments); CppRootPaths RootPaths = CompileEnvironment.RootPaths; Arguments.Add("-fasm-blocks"); // Disable FMA contraction on arm builds in order to preserve consistency with x64 // derived data builds. // This is also added to the x64 builds in case someone ever adds -mfma. Arguments.Add("-ffp-contract=off"); if (CompileEnvironment.bEnableOSX109Support) { Arguments.Add("-faligned-new"); // aligned operator new is supported only on macOS 10.14 and above } // Pass through architecture and OS info Arguments.Add("" + FormatArchitectureArg(CompileEnvironment.Architectures)); Arguments.Add($"-isysroot \"{NormalizeCommandLinePath(Settings.GetSDKPath(CompileEnvironment.Architecture), RootPaths)}\""); List FrameworksSearchPaths = new List(); foreach (UEBuildFramework Framework in CompileEnvironment.AdditionalFrameworks) { FileReference FrameworkPath = new FileReference(Path.GetFullPath(Framework.Name)); if (!FrameworksSearchPaths.Contains(FrameworkPath.Directory.FullName)) { Arguments.Add($"-F \"{NormalizeCommandLinePath(FrameworkPath.Directory, RootPaths)}\""); FrameworksSearchPaths.Add(FrameworkPath.Directory.FullName); } } } void AddFrameworkToLinkCommand(List LinkArguments, string FrameworkName, string Arg = "-framework") { if (FrameworkName.EndsWith(".framework")) { LinkArguments.Add("-F \"" + Path.GetDirectoryName(Path.GetFullPath(FrameworkName)) + "\""); FrameworkName = Path.GetFileNameWithoutExtension(FrameworkName); } LinkArguments.Add(Arg + " \"" + FrameworkName + "\""); } protected override void GetLinkArguments_Global(LinkEnvironment LinkEnvironment, List Arguments) { base.GetLinkArguments_Global(LinkEnvironment, Arguments); // Pass through architecture and OS info Arguments.Add(FormatArchitectureArg(LinkEnvironment.Architectures)); Arguments.Add(String.Format("-isysroot \"{0}\"", NormalizeCommandLinePath(Settings.GetSDKPath(LinkEnvironment.Architecture), LinkEnvironment.RootPaths))); Arguments.Add($"-target {Settings.GetTargetTuple(LinkEnvironment.Architecture, Target!.Platform, Target!.Type)}"); Arguments.Add("-dead_strip"); // Temporary workaround for linker warning with Xcode 14: // 'ld: warning: could not create compact unwind for _inflate_fast: registers 27 not saved contiguously in frame' if (CompilerVersionLessThan(14, 0, 0)) { Arguments.Add("-Wl,-fatal_warnings"); } if (Options.HasFlag(ClangToolChainOptions.EnableAddressSanitizer) || Options.HasFlag(ClangToolChainOptions.EnableThreadSanitizer) || Options.HasFlag(ClangToolChainOptions.EnableUndefinedBehaviorSanitizer) || Options.HasFlag(ClangToolChainOptions.EnableLibFuzzer)) { Arguments.Add("-g"); if (Options.HasFlag(ClangToolChainOptions.EnableAddressSanitizer)) { Arguments.Add("-fsanitize=address"); } else if (Options.HasFlag(ClangToolChainOptions.EnableThreadSanitizer)) { Arguments.Add("-fsanitize=thread"); } else if (Options.HasFlag(ClangToolChainOptions.EnableUndefinedBehaviorSanitizer)) { Arguments.Add("-fsanitize=undefined"); } if (Options.HasFlag(ClangToolChainOptions.EnableLibFuzzer)) { Arguments.Add("-fsanitize=fuzzer"); } } if (LinkEnvironment.bIsBuildingDLL) { Arguments.Add("-dynamiclib"); } if (LinkEnvironment.Configuration == CppConfiguration.Debug) { // Apple's Clang is not supposed to run the de-duplication pass when linking in debug configs. Xcode adds this flag automatically, we need it as well, otherwise linking would take very long Arguments.Add("-Wl,-no_deduplicate"); } // Needed to make sure install_name_tool will be able to update paths in Mach-O headers Arguments.Add("-headerpad_max_install_names"); } void GetArchiveArguments_Global(LinkEnvironment LinkEnvironment, List Arguments) { Arguments.Add("-static"); } private void AppendMacLine(StreamWriter Writer, string Format, params object[] Arg) { string PreLine = String.Format(Format, Arg); Writer.Write(PreLine + "\n"); } private int LoadEngineCL() { BuildVersion? Version; if (BuildVersion.TryRead(BuildVersion.GetDefaultFileName(), out Version)) { return Version.Changelist; } else { return 0; } } public static string LoadEngineDisplayVersion(bool bIgnorePatchVersion = false) { BuildVersion? Version; if (BuildVersion.TryRead(BuildVersion.GetDefaultFileName(), out Version)) { return $"{Version.MajorVersion}.{Version.MinorVersion}.{(bIgnorePatchVersion ? 0 : Version.PatchVersion)}"; } else { return "4.0.0"; } } private int LoadBuiltFromChangelistValue() { return LoadEngineCL(); } private string LoadEngineAPIVersion() { if (BuildVersion.TryRead(BuildVersion.GetDefaultFileName(), out BuildVersion? Version)) { if ((Target?.bPrecompile == true || Version.IsPromotedBuild) && Version.EffectiveCompatibleChangelist > 0) { int CL = Version.EffectiveCompatibleChangelist; return $"{CL / (100 * 100)}.{(CL / 100) % 100}.{CL % 100}"; } } return LoadEngineDisplayVersion(true); } private string GetGameNameFromExecutablePath(string ExePath) { string ExeName = Path.GetFileName(ExePath); string[] ExeNameParts = ExeName.Split('-'); string GameName = ExeNameParts[0]; if (GameName == "EpicGamesBootstrapLauncher") { GameName = "EpicGamesLauncher"; } else if (GameName == "UE5" && ProjectFile != null) { GameName = ProjectFile.GetFileNameWithoutAnyExtensions(); } return GameName; } private void AddLibraryPathToRPaths(string Library, string ExeAbsolutePath, ref List RPaths, List LinkArguments, bool bIsBuildingAppBundle, ILogger Logger) { string LibraryFullPath = Path.GetFullPath(Library); string LibraryDir = Path.GetDirectoryName(LibraryFullPath)!; string ExeDir = Path.GetDirectoryName(ExeAbsolutePath)!; // Only dylibs and frameworks, and only those that are outside of Engine/Binaries/Mac and Engine/Source/ThirdParty, and outside of the folder where the executable is need an additional RPATH entry if ((Library.EndsWith("dylib") || Library.EndsWith(".framework")) && !LibraryFullPath.Contains("/Engine/Source/ThirdParty/") && LibraryDir != ExeDir && !RPaths.Contains(LibraryDir)) { // macOS gatekeeper erroneously complains about not seeing the CEF3 framework in the codesigned Launcher because it's only present in one of the folders specified in RPATHs. // To work around this we will only add a single RPATH entry for it, for the framework stored in .app/Contents/UE/ subfolder of the packaged app bundle bool bCanUseMultipleRPATHs = !ExeAbsolutePath.Contains("EpicGamesLauncher-Mac-Shipping") || !Library.Contains("CEF3"); // First, add a path relative to the executable. string FinalExeDir = ExeDir; if (bIsBuildingAppBundle) { FinalExeDir = ExeAbsolutePath + ".app/Contents/MacOS"; } string RelativePath = Utils.MakePathRelativeTo(LibraryDir, FinalExeDir).Replace("\\", "/"); if (bCanUseMultipleRPATHs) { LinkArguments.Add("-rpath \"@loader_path/" + RelativePath + "\""); // We are referencing plugins that need to be relative to the engine not the dylib we are building // To be safe leave the previous relative loader_path in place and add a relative engine executable path if (!bIsBuildingAppBundle && LibraryDir.Contains("/Engine/Plugins/") && ExeAbsolutePath.EndsWith("dylib")) { int EngineDirStrIndex = LibraryDir.IndexOf("Engine"); if (EngineDirStrIndex >= 0) { LinkArguments.Add("-rpath \"@executable_path/../../../../../../" + LibraryDir.Substring(EngineDirStrIndex) + "\""); } } } // If building an app bundle, we also need an RPATH for use in packaged game and a separate one for staged builds if (bIsBuildingAppBundle) { string EngineDir = Unreal.RootDirectory.ToString(); string? ProjectDir = ProjectFile?.Directory.FullName; // In packaged games dylibs are stored in Contents/UE subfolders, for example in GameName.app/Contents/UE/Engine/Binaries/ThirdParty/PhysX/Mac string BundleUEDir = Path.GetFullPath(ExeDir + "/../../Contents/UE"); string? BundleLibraryDir = null; if (LibraryDir.StartsWith(EngineDir + "/")) { BundleLibraryDir = LibraryDir.Replace(EngineDir, BundleUEDir); } else if (ProjectDir != null && LibraryDir.StartsWith(ProjectDir + "/")) { string GameName = GetGameNameFromExecutablePath(ExeAbsolutePath); BundleLibraryDir = LibraryDir.Replace(ProjectDir, BundleUEDir + "/" + GameName); } if (BundleLibraryDir != null) { string BundleRelativeDir = Utils.MakePathRelativeTo(BundleLibraryDir, ExeDir).Replace("\\", "/"); LinkArguments.Add("-rpath \"@loader_path/" + BundleRelativeDir + "\""); } else { Logger.LogWarning("Unexpected third party dylib location when generating RPATH entries: {LibraryFullPath}. Skipping.", LibraryFullPath); } // For staged code-based games we need additional entry if the game is not stored directly in the engine's root directory if (bCanUseMultipleRPATHs) { string StagedUEDir = Path.GetFullPath(ExeDir + "/../../../../../.."); string StagedLibraryDir = LibraryDir.Replace(EngineDir, StagedUEDir); string StagedRelativeDir = Utils.MakePathRelativeTo(StagedLibraryDir, ExeDir).Replace("\\", "/"); if (StagedRelativeDir != RelativePath) { LinkArguments.Add("-rpath \"@loader_path/" + StagedRelativeDir + "\""); } } } RPaths.Add(LibraryDir); } } public override FileItem[] LinkImportLibrary(LinkEnvironment LinkEnvironment, IActionGraphBuilder Graph) { // we actually create Actions for every dylib, but we never return a FileItem. Instead depend on a Link action // linking to the stub dylib to make thie action take place. If we returned FileItems here, UBT would link a // stub lib for every dylub even if not needed by anything if (LinkEnvironment.bIsBuildingDLL) { LinkAllFiles(LinkEnvironment, true, Graph); } return Array.Empty(); } public override FileItem? LinkFiles(LinkEnvironment LinkEnvironment, bool bBuildImportLibraryOnly, IActionGraphBuilder Graph) { return null; } public override FileItem[] LinkAllFiles(LinkEnvironment LinkEnvironment, bool bBuildImportLibraryOnly, IActionGraphBuilder Graph) { List OutputFiles = new(); // To solve the problem with cross dependencies, for now we create a broken dylib that does not link with other engine dylibs. // This is fixed in later step, FixDylibDependencies. For this and to know what libraries to copy whilst creating an app bundle, // we gather the list of engine dylibs. List EngineAndGameLibraries = new List(); bool bIsBuildingLibrary = LinkEnvironment.bIsBuildingLibrary || bBuildImportLibraryOnly; bool bIsBuildingAppBundle = !LinkEnvironment.bIsBuildingDLL && !LinkEnvironment.bIsBuildingLibrary && !LinkEnvironment.bIsBuildingConsoleApplication; FileReference LinkerPath = bIsBuildingLibrary ? Info.Archiver : Info.Clang; if (LinkEnvironment.Architectures.bIsMultiArch) { List PerArchOutputFiles = new(); foreach (UnrealArch Arch in LinkEnvironment.Architectures.Architectures) { LinkEnvironment ArchEnvironment = new LinkEnvironment(LinkEnvironment, Arch); if (!bBuildImportLibraryOnly) { ArchEnvironment.OutputFilePaths = LinkEnvironment.OutputFilePaths.Select(x => new FileReference($"{LinkEnvironment.IntermediateDirectory}/{Path.GetFileName(x.FullName)}_{Arch}")).ToList(); } PerArchOutputFiles.Add(LinkArchitectureFiles(ArchEnvironment, LinkEnvironment.OutputFilePath, bBuildImportLibraryOnly, Graph)); } if (bBuildImportLibraryOnly) { return PerArchOutputFiles.ToArray(); } FileItem OutputFileItem; if (bBuildImportLibraryOnly) { OutputFileItem = MakeStubItem(LinkEnvironment, LinkEnvironment.OutputFilePath.FullName); } else { OutputFileItem = FileItem.GetItemByFileReference(LinkEnvironment.OutputFilePath); } OutputFiles.Add(OutputFileItem); Action LipoAction = Graph.CreateAction(ActionType.Link); LipoAction.PrerequisiteItems.UnionWith(PerArchOutputFiles); LipoAction.ProducedItems.Add(OutputFileItem); LipoAction.WorkingDirectory = GetMacDevSrcRoot(); LipoAction.CommandPath = FileReference.Combine(Info.Clang.Directory, "lipo"); LipoAction.CommandDescription = (bBuildImportLibraryOnly ? "LipoStub" : "Lipo"); LipoAction.CommandVersion = Info.ClangVersionString; LipoAction.CommandArguments = String.Join(" ", PerArchOutputFiles.Select(x => $"\"{x}\"")) + $" -create -output \"{OutputFileItem.AbsolutePath}\""; LipoAction.StatusDescription = Path.GetFileName(OutputFileItem.AbsolutePath); LipoAction.bCanExecuteRemotely = false; } else { OutputFiles.Add(LinkArchitectureFiles(LinkEnvironment, LinkEnvironment.OutputFilePath, bBuildImportLibraryOnly, Graph)); } if (!DirectoryReference.Exists(LinkEnvironment.IntermediateDirectory!)) { DirectoryReference.CreateDirectory(LinkEnvironment.IntermediateDirectory!); } // For non-console application, prepare a script that will create the app bundle. It'll be run by FinalizeAppBundle action if (bIsBuildingAppBundle && !bUseModernXcode) { FileReference FinalizeAppBundleScriptPath = FileReference.Combine(LinkEnvironment.IntermediateDirectory!, "FinalizeAppBundle.sh"); StreamWriter FinalizeAppBundleScript = File.CreateText(FinalizeAppBundleScriptPath.FullName); AppendMacLine(FinalizeAppBundleScript, "#!/bin/sh"); string BinariesPath = Path.GetDirectoryName(OutputFiles[0].AbsolutePath)!; BinariesPath = Path.GetDirectoryName(BinariesPath.Substring(0, BinariesPath.IndexOf(".app")))!; AppendMacLine(FinalizeAppBundleScript, "cd \"{0}\"", BinariesPath.Replace("$", "\\$")); string BundleVersion = LinkEnvironment.BundleVersion!; BundleVersion ??= LoadEngineDisplayVersion(); string ExeName = Path.GetFileName(OutputFiles[0].AbsolutePath); bool bIsLauncherProduct = ExeName.StartsWith("EpicGamesLauncher") || ExeName.StartsWith("EpicGamesBootstrapLauncher"); string[] ExeNameParts = ExeName.Split('-'); string GameName = GetGameNameFromExecutablePath(OutputFiles[0].AbsolutePath); // bundle identifier // plist replacements DirectoryReference? DirRef = (!String.IsNullOrEmpty(UnrealBuildTool.GetRemoteIniPath()) ? new DirectoryReference(UnrealBuildTool.GetRemoteIniPath()!) : (ProjectFile?.Directory)); ConfigHierarchy IOSIni = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirRef, UnrealTargetPlatform.IOS); string BundleIdentifier; IOSIni.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "BundleIdentifier", out BundleIdentifier!); string ProjectName = GameName; FileReference? UProjectFilePath = ProjectFile; if (UProjectFilePath != null) { ProjectName = UProjectFilePath.GetFileNameWithoutAnyExtensions(); } AppendMacLine(FinalizeAppBundleScript, "mkdir -p \"{0}.app/Contents/MacOS\"", ExeName); AppendMacLine(FinalizeAppBundleScript, "mkdir -p \"{0}.app/Contents/Resources\"", ExeName); string IconName = "UnrealEngine"; string EngineSourcePath = Directory.GetCurrentDirectory().Replace("$", "\\$"); string CustomResourcesPath = ""; string CustomBuildPath = ""; if (UProjectFilePath == null) { string[] TargetFiles = Directory.GetFiles(Directory.GetCurrentDirectory(), GameName + ".Target.cs", SearchOption.AllDirectories); if (TargetFiles.Length == 1) { CustomResourcesPath = Path.GetDirectoryName(TargetFiles[0]) + "/Resources/Mac"; CustomBuildPath = Path.GetDirectoryName(TargetFiles[0]) + "../Build/Mac"; } else { Logger.LogWarning("Found {NumFiles} Target.cs files for {GameName} in alldir search of directory {Dir}", TargetFiles.Length, GameName, Directory.GetCurrentDirectory()); } } else { string ResourceParentFolderName = bIsLauncherProduct ? "Application" : GameName; CustomResourcesPath = Path.GetDirectoryName(UProjectFilePath.FullName) + "/Source/" + ResourceParentFolderName + "/Resources/Mac"; if (!Directory.Exists(CustomResourcesPath)) { CustomResourcesPath = Path.GetDirectoryName(UProjectFilePath.FullName) + "/Source/" + ProjectName + "/Resources/Mac"; } CustomBuildPath = Path.GetDirectoryName(UProjectFilePath.FullName) + "/Build/Mac"; } bool bBuildingEditor = GameName.EndsWith("Editor"); // Copy resources string DefaultIcon = EngineSourcePath + "/Runtime/Launch/Resources/Mac/" + IconName + ".icns"; string CustomIcon = ""; if (bBuildingEditor) { CustomIcon = DefaultIcon; } else { CustomIcon = CustomBuildPath + "/Application.icns"; if (!File.Exists(CustomIcon)) { CustomIcon = CustomResourcesPath + "/" + GameName + ".icns"; if (!File.Exists(CustomIcon)) { CustomIcon = DefaultIcon; } } } AppendMacLine(FinalizeAppBundleScript, FormatCopyCommand(CustomIcon, String.Format("{0}.app/Contents/Resources/{1}.icns", ExeName, GameName))); if (ExeName.StartsWith("UnrealEditor")) { AppendMacLine(FinalizeAppBundleScript, FormatCopyCommand(String.Format("{0}/Runtime/Launch/Resources/Mac/UProject.icns", EngineSourcePath), String.Format("{0}.app/Contents/Resources/UProject.icns", ExeName))); } string InfoPlistFile = CustomResourcesPath + (bBuildingEditor ? "/Info-Editor.plist" : "/Info.plist"); if (!File.Exists(InfoPlistFile)) { InfoPlistFile = EngineSourcePath + "/Runtime/Launch/Resources/Mac/" + (bBuildingEditor ? "Info-Editor.plist" : "Info.plist"); } string TempInfoPlist = "$TMPDIR/TempInfo.plist"; AppendMacLine(FinalizeAppBundleScript, FormatCopyCommand(InfoPlistFile, TempInfoPlist)); // Fix contents of Info.plist AppendMacLine(FinalizeAppBundleScript, "/usr/bin/sed -i \"\" -e \"s/\\${0}/{1}/g\" \"{2}\"", "{EXECUTABLE_NAME}", ExeName, TempInfoPlist); AppendMacLine(FinalizeAppBundleScript, "/usr/bin/sed -i \"\" -e \"s/\\${0}/{1}/g\" \"{2}\"", "{APP_NAME}", bBuildingEditor ? ("com.epicgames." + GameName) : (BundleIdentifier.Replace("[PROJECT_NAME]", GameName).Replace("_", "")), TempInfoPlist); AppendMacLine(FinalizeAppBundleScript, "/usr/bin/sed -i \"\" -e \"s/\\${0}/{1}/g\" \"{2}\"", "{MACOSX_DEPLOYMENT_TARGET}", MacToolChainSettings.MinMacDeploymentVersion(Target!.Type), TempInfoPlist); AppendMacLine(FinalizeAppBundleScript, "/usr/bin/sed -i \"\" -e \"s/\\${0}/{1}/g\" \"{2}\"", "{ICON_NAME}", GameName, TempInfoPlist); AppendMacLine(FinalizeAppBundleScript, "/usr/bin/sed -i \"\" -e \"s/\\${0}/{1}/g\" \"{2}\"", "{BUNDLE_VERSION}", BundleVersion, TempInfoPlist); // Copy it into place AppendMacLine(FinalizeAppBundleScript, FormatCopyCommand(TempInfoPlist, String.Format("{0}.app/Contents/Info.plist", ExeName))); AppendMacLine(FinalizeAppBundleScript, "chmod 644 \"{0}.app/Contents/Info.plist\"", ExeName); // Also copy it to where Xcode will look for it, now that Xcode 14 requires we have one set up - it can't point into the .app because that // is where it will copy to, so it will error with reading and writing to the same location string IntermediateDirectory = (ProjectFile == null ? Unreal.EngineDirectory : ProjectFile.Directory) + "/Intermediate/Mac"; string XcodeInputPListFile = IntermediateDirectory + "/" + ExeName + "-Info.plist"; AppendMacLine(FinalizeAppBundleScript, "mkdir -p \"{0}\"", IntermediateDirectory); AppendMacLine(FinalizeAppBundleScript, FormatCopyCommand(TempInfoPlist, XcodeInputPListFile)); AppendMacLine(FinalizeAppBundleScript, "chmod 644 \"{0}\"", XcodeInputPListFile); // Generate PkgInfo file string TempPkgInfo = "$TMPDIR/TempPkgInfo"; AppendMacLine(FinalizeAppBundleScript, "echo 'echo -n \"APPL????\"' | bash > \"{0}\"", TempPkgInfo); AppendMacLine(FinalizeAppBundleScript, FormatCopyCommand(TempPkgInfo, String.Format("{0}.app/Contents/PkgInfo", ExeName))); // Make sure OS X knows the bundle was updated AppendMacLine(FinalizeAppBundleScript, "touch -c \"{0}.app\"", ExeName); // codesign with ad-hoc signature for when building outside of Xcode the there will be at least some signature // (and it can be a BuildProduct down in ModifyBuildProducts) AppendMacLine(FinalizeAppBundleScript, "codesign -f -s - \"{0}.app\"", ExeName); AppendMacLine(FinalizeAppBundleScript, "codesign -f -s \"Developer ID Application\" \"{0}.app\" 2> /dev/null", ExeName); AppendMacLine(FinalizeAppBundleScript, "echo done > /dev/null"); FinalizeAppBundleScript.Close(); } return OutputFiles.ToArray(); } private FileItem MakeStubItem(LinkEnvironment LinkEnvironment, string DylibPath) { FileReference DylibPathRef = new FileReference(DylibPath); bool bIsEngineDylib = DylibPathRef.IsUnderDirectory(Unreal.EngineDirectory); string IntermediateDirectory = (ProjectFile == null || LinkEnvironment.OutputFilePaths.All(x => x.IsUnderDirectory(Unreal.EngineDirectory)) || bIsEngineDylib ? Unreal.EngineDirectory : ProjectFile.Directory) + "/Intermediate/Mac"; return FileItem.GetItemByPath(Path.Combine(IntermediateDirectory, "Stubs", LinkEnvironment.Architecture.ToString(), LinkEnvironment.Configuration.ToString(), Path.GetFileName(DylibPath))); } private FileItem LinkArchitectureFiles(LinkEnvironment LinkEnvironment, FileReference MultiArchOutputFile, bool bBuildImportLibraryOnly, IActionGraphBuilder Graph) { List InputFiles = LinkEnvironment.InputFiles; // with precompiled builds, we end up trying to link the arm64 and x64 versions of the engine .o files. Remove the wrong one // until we can separate out the .o files by architecture string ArchToRemove = LinkEnvironment.Architecture == UnrealArch.Arm64 ? "x64" : "arm64"; InputFiles.RemoveAll(x => x.Location.ContainsName(ArchToRemove, 0)); // Create an action that invokes the linker. Action LinkAction = Graph.CreateAction(ActionType.Link); LinkAction.RootPaths = LinkEnvironment.RootPaths; FileReference LinkerPath = LinkEnvironment.bIsBuildingLibrary ? Info.Archiver : Info.Clang; LinkAction.WorkingDirectory = GetMacDevSrcRoot(); LinkAction.CommandPath = LinkerPath; string Arch = UnrealArchitectureConfig.ForPlatform(UnrealTargetPlatform.Mac).ConvertToReadableArchitecture(LinkEnvironment.Architecture); LinkAction.CommandDescription = (bBuildImportLibraryOnly ? "LinkStub" : "Link") + $" [{Arch}]"; LinkAction.CommandVersion = Info.ClangVersionString; LinkAction.bProducesImportLibrary = bBuildImportLibraryOnly || LinkEnvironment.bIsBuildingDLL; LinkAction.bCanExecuteInUBA = true; LinkAction.CacheBucket = GetCacheBucket(Target, null); LinkAction.ArtifactMode = ArtifactMode.Enabled; List LinkArguments = new(); if (LinkEnvironment.bIsBuildingLibrary) { GetArchiveArguments_Global(LinkEnvironment, LinkArguments); } else { GetLinkArguments_Global(LinkEnvironment, LinkArguments); } if (LinkEnvironment.bIsBuildingDLL) { LinkArguments.Add("-current_version " + LoadEngineAPIVersion()); LinkArguments.Add("-compatibility_version " + LoadEngineDisplayVersion(true)); } // Tell the action that we're building an import library here and it should conditionally be // ignored as a prerequisite for other actions LinkAction.bProducesImportLibrary = bBuildImportLibraryOnly || LinkEnvironment.bIsBuildingDLL; // Add the output file as a production of the link action. FileItem OutputFile; if (bBuildImportLibraryOnly) { OutputFile = MakeStubItem(LinkEnvironment, LinkEnvironment.OutputFilePath.FullName); } else { OutputFile = FileItem.GetItemByFileReference(LinkEnvironment.OutputFilePath); } string DylibsPath = "@rpath"; string AbsolutePath = MultiArchOutputFile.FullName.Replace("\\", "/"); if (!LinkEnvironment.bIsBuildingLibrary) { LinkArguments.Add("-rpath @loader_path/ -rpath @executable_path/"); } bool bIsBuildingAppBundle = !LinkEnvironment.bIsBuildingDLL && !LinkEnvironment.bIsBuildingLibrary && !LinkEnvironment.bIsBuildingConsoleApplication; if (bIsBuildingAppBundle) { LinkArguments.Add("-rpath @executable_path/../../../"); } List RPaths = new List(); // static libs and stub dylibs don't need to look other libs if (bBuildImportLibraryOnly) { LinkArguments.Add("-undefined dynamic_lookup"); LinkArguments.Add("-Wl,-no_fixup_chains"); } else if (!LinkEnvironment.bIsBuildingLibrary) { IEnumerable AdditionalLibraries = Enumerable.Concat(LinkEnvironment.SystemLibraries, LinkEnvironment.Libraries.Select(x => x.FullName)); // @todo set this to false if we have any rpath issues, or maybe set it to false when making Monolithic // we will remove the old path entirely after a lot of wide testing bool bCanUseSimpleRPaths = LinkEnvironment.LinkType == TargetLinkType.Monolithic; if (bCanUseSimpleRPaths) { string StagedBundlePrefix = bIsBuildingAppBundle ? "../UE/" : ""; string UnstagedBundlePrefix = bIsBuildingAppBundle ? "../../../" : ""; HashSet UsedDestDirs = new(); HashSet UsedSourceLibs = new(); var ProcessLib = (FileReference SourceLib, FileReference DestLib, FileReference ReferencingFile) => { if (string.Equals(SourceLib.GetExtension(), ".dylib", StringComparison.InvariantCultureIgnoreCase)) { DirectoryReference DestDir = DestLib.Directory; if (UsedSourceLibs.Contains(SourceLib) || UsedDestDirs.Contains(DestDir)) { return; } UsedDestDirs.Add(DestDir); UsedSourceLibs.Add(SourceLib); string RPathEntry; // handle staged rpaths if (ProjectFile != null && DestDir.IsUnderDirectory(ProjectFile.Directory)) { RPathEntry = StagedBundlePrefix + ProjectFile.GetFileNameWithoutAnyExtensions() + "/" + DestDir.MakeRelativeTo(ProjectFile.Directory); } else { RPathEntry = StagedBundlePrefix + DestDir.MakeRelativeTo(Unreal.EngineDirectory.ParentDirectory!); } LinkArguments.Add($"-rpath \"@executable_path/{RPathEntry}\""); // handle unstaged paths RPathEntry = UnstagedBundlePrefix + DestDir.MakeRelativeTo(ReferencingFile.Directory); LinkArguments.Add($"-rpath \"@loader_path/{RPathEntry}\""); } }; // first look in the RuntimeDependency list to look for remapped locations, anything not remapped we will deal with // after this loop foreach (ModuleRules.RuntimeDependency SrcDep in LinkEnvironment.RuntimeDependencies) { FileReference DestPath = new(SrcDep.Path); FileReference SourcePath = SrcDep.SourcePath == null ? DestPath : new FileReference(SrcDep.SourcePath); ProcessLib(SourcePath, DestPath, MultiArchOutputFile); } foreach (string AdditionalLibrary in AdditionalLibraries) { FileReference LibPath = new(AdditionalLibrary); ProcessLib(LibPath, LibPath, MultiArchOutputFile); } } // Add the additional libraries to the argument list. foreach (string AdditionalLibrary in AdditionalLibraries) { if (!String.IsNullOrEmpty(Path.GetDirectoryName(AdditionalLibrary)) && (Path.GetDirectoryName(AdditionalLibrary)!.Contains("Binaries/Mac") || Path.GetDirectoryName(AdditionalLibrary)!.Contains("Binaries\\Mac"))) { // It's an engine or game dylib. Save it for later FileItem LinkedLib; if (LinkEnvironment.bIsCrossReferenced) { LinkedLib = MakeStubItem(LinkEnvironment, AdditionalLibrary); } else { LinkedLib = FileItem.GetItemByPath(AdditionalLibrary); } LinkAction.PrerequisiteItems.Add(LinkedLib); LinkArguments.Add($"\"{NormalizeCommandLinePath(LinkedLib, LinkEnvironment.RootPaths)}\""); } else if (AdditionalLibrary.Contains(".framework/")) { LinkArguments.Add(String.Format("\"{0}\"", AdditionalLibrary)); } else if (String.IsNullOrEmpty(Path.GetDirectoryName(AdditionalLibrary)) && String.IsNullOrEmpty(Path.GetExtension(AdditionalLibrary))) { LinkArguments.Add(String.Format("-l\"{0}\"", AdditionalLibrary)); } else { LinkArguments.Add($"\"{NormalizeCommandLinePath(FileItem.GetItemByPath(AdditionalLibrary), LinkEnvironment.RootPaths)}\""); } if (!bCanUseSimpleRPaths) { AddLibraryPathToRPaths(AdditionalLibrary, AbsolutePath, ref RPaths, LinkArguments, bIsBuildingAppBundle, Logger); } } foreach (string AdditionalLibrary in LinkEnvironment.DelayLoadDLLs) { LinkArguments.Add(String.Format("-weak_library \"{0}\"", Path.GetFullPath(AdditionalLibrary))); if (!bCanUseSimpleRPaths) { AddLibraryPathToRPaths(AdditionalLibrary, AbsolutePath, ref RPaths, LinkArguments, bIsBuildingAppBundle, Logger); } } // Add frameworks Dictionary AllFrameworks = new Dictionary(); foreach (string Framework in LinkEnvironment.Frameworks) { if (!AllFrameworks.ContainsKey(Framework)) { AllFrameworks.Add(Framework, false); } } foreach (UEBuildFramework Framework in LinkEnvironment.AdditionalFrameworks) { if (Framework.ZipFile != null) { FileItem ExtractedTokenFile = ExtractFramework(Framework, Graph, Logger); LinkAction.PrerequisiteItems.Add(ExtractedTokenFile); } if (Framework.bLinkFramework && !AllFrameworks.ContainsKey(Framework.Name)) { AllFrameworks.Add(Framework.Name, false); } } foreach (string Framework in LinkEnvironment.WeakFrameworks) { if (!AllFrameworks.ContainsKey(Framework)) { AllFrameworks.Add(Framework, true); } } foreach (KeyValuePair Framework in AllFrameworks) { AddFrameworkToLinkCommand(LinkArguments, Framework.Key, Framework.Value ? "-weak_framework" : "-framework"); AddLibraryPathToRPaths(Framework.Key, AbsolutePath, ref RPaths, LinkArguments, bIsBuildingAppBundle, Logger); } // Write the MAP file to the output directory. if (LinkEnvironment.bCreateMapFile) { string MapFileBaseName = OutputFile.AbsolutePath; int AppIdx = MapFileBaseName.IndexOf(".app/Contents/MacOS"); if (AppIdx != -1) { MapFileBaseName = MapFileBaseName.Substring(0, AppIdx); } FileReference MapFilePath = new FileReference(MapFileBaseName + ".map"); FileItem MapFile = FileItem.GetItemByFileReference(MapFilePath); LinkArguments.Add(String.Format("-Wl,-map,\"{0}\"", MapFilePath)); LinkAction.ProducedItems.Add(MapFile); } } if (LinkEnvironment.bIsBuildingDLL) { // Add the output file to the command-line. string? InstallName = LinkEnvironment.InstallName; InstallName ??= String.Format("{0}/{1}", DylibsPath, Path.GetFileName(OutputFile.AbsolutePath).Replace($".dylib_{LinkEnvironment.Architecture}", ".dylib")); LinkArguments.Add(String.Format("-install_name \"{0}\"", InstallName)); } List InputFileNames = new List(); foreach (FileItem InputFile in InputFiles) { InputFileNames.Add(String.Format("\"{0}\"", NormalizeCommandLinePath(InputFile, LinkEnvironment.RootPaths))); LinkAction.PrerequisiteItems.Add(InputFile); } foreach (string Filename in InputFileNames) { LinkArguments.Add(Filename); } // Add the output file to the command-line. LinkArguments.Add(String.Format("-o \"{0}\"", NormalizeCommandLinePath(OutputFile, LinkEnvironment.RootPaths))); // Add the additional arguments specified by the environment. LinkArguments.Add(LinkEnvironment.AdditionalArguments); FileReference ResponseFileName; if (bBuildImportLibraryOnly) { ResponseFileName = FileReference.FromString(OutputFile.FullName + ResponseExt); } else { ResponseFileName = GetResponseFileName(LinkEnvironment, OutputFile); } FileItem ResponseFileItem = Graph.CreateIntermediateTextFile(ResponseFileName, LinkArguments); string ResponsFileArgument = GetResponseFileArgument(ResponseFileItem, LinkEnvironment.RootPaths); LinkAction.PrerequisiteItems.Add(ResponseFileItem); LinkAction.CommandArguments = ResponsFileArgument; // Only execute linking on the local Mac. LinkAction.bCanExecuteRemotely = false; LinkAction.StatusDescription = Path.GetFileName(OutputFile.AbsolutePath); LinkAction.ProducedItems.Add(OutputFile); // Delete all items we produce LinkAction.DeleteItems.UnionWith(LinkAction.ProducedItems); return OutputFile; } static string FormatCopyCommand(string SourceFile, string TargetFile) { return String.Format("rsync --checksum \"{0}\" \"{1}\"", SourceFile, TargetFile); } /// /// Generates debug info for a given executable /// /// FileItem describing the executable or dylib to generate debug info for /// /// List of actions to be executed. Additional actions will be added to this list. /// Logger for output public FileItem GenerateDebugInfo(FileItem MachOBinary, LinkEnvironment LinkEnvironment, IActionGraphBuilder Graph, ILogger Logger) { string BinaryPath = MachOBinary.AbsolutePath; if (BinaryPath.Contains(".app")) { while (BinaryPath.Contains(".app")) { BinaryPath = Path.GetDirectoryName(BinaryPath)!; } BinaryPath = Path.Combine(BinaryPath, Path.GetFileName(Path.ChangeExtension(MachOBinary.AbsolutePath, ".dSYM"))); } else { BinaryPath = Path.ChangeExtension(BinaryPath, ".dSYM"); } FileItem OutputFile = FileItem.GetItemByPath(BinaryPath); // Delete on the local machine if (Directory.Exists(OutputFile.AbsolutePath)) { Directory.Delete(OutputFile.AbsolutePath, true); } // Make the compile action Action GenDebugAction = Graph.CreateAction(ActionType.GenerateDebugInfo); GenDebugAction.WorkingDirectory = GetMacDevSrcRoot(); GenDebugAction.CommandPath = BuildHostPlatform.Current.Shell; // Deletes ay existing file on the building machine. Also, waits 30 seconds, if needed, for the input file to be created in an attempt to work around // a problem where dsymutil would exit with an error saying the input file did not exist. // Note that the source and dest are switched from a copy command string DsymutilPath = "/usr/bin/dsymutil"; string UniversalDsymutilScriptPath = FileReference.Combine(Unreal.EngineDirectory, "Build/BatchFiles/Mac/GenerateUniversalDSYM.sh").FullName; string ArgumentString = "-c \""; ArgumentString += String.Format("for i in {{1..30}}; "); ArgumentString += String.Format("do if [ -f \\\"{0}\\\" ] ; ", MachOBinary.AbsolutePath); ArgumentString += String.Format("then "); ArgumentString += String.Format("break; "); ArgumentString += String.Format("else "); ArgumentString += String.Format("sleep 1; "); ArgumentString += String.Format("fi; "); ArgumentString += String.Format("done; "); ArgumentString += String.Format("if [ ! -f \\\"{1}\\\" ] || [ \\\"{0}\\\" -nt \\\"{1}\\\" ] ; ", MachOBinary.AbsolutePath, OutputFile.AbsolutePath); ArgumentString += String.Format("then "); ArgumentString += String.Format("rm -rf \\\"{0}\\\"; ", OutputFile.AbsolutePath); // use the new script for monolthic (ie large) targets if (LinkEnvironment.LinkType == TargetLinkType.Monolithic) { ArgumentString += String.Format(" \\\"{0}\\\" \\\"{1}\\\" \\\"{2}\\\"; ", UniversalDsymutilScriptPath, MachOBinary.AbsolutePath, OutputFile.AbsolutePath); } else { ArgumentString += String.Format(" \\\"{0}\\\" -f \\\"{1}\\\" -o \\\"{2}\\\"; ", DsymutilPath, MachOBinary.AbsolutePath, OutputFile.AbsolutePath); } ArgumentString += String.Format("fi; "); ArgumentString += "\""; GenDebugAction.CommandArguments = ArgumentString; GenDebugAction.PrerequisiteItems.Add(MachOBinary); GenDebugAction.ProducedItems.Add(OutputFile); GenDebugAction.CommandDescription = ""; GenDebugAction.StatusDescription = "Generating " + Path.GetFileName(BinaryPath); GenDebugAction.bCanExecuteRemotely = false; return OutputFile; } /// /// Creates app bundle for a given executable /// /// /// /// FileItem describing the executable to generate app bundle for /// List of actions to be executed. Additional actions will be added to this list. FileItem FinalizeAppBundle(ReadOnlyTargetRules Target, LinkEnvironment LinkEnvironment, FileItem Executable, IActionGraphBuilder Graph) { // Make a file item for the source and destination files string FullDestPath = Executable.AbsolutePath.Substring(0, Executable.AbsolutePath.IndexOf(".app") + 4); FileItem DestFile = FileItem.GetItemByPath(FullDestPath); // Make the compile action Action FinalizeAppBundleAction = Graph.CreateAction(ActionType.CreateAppBundle); FinalizeAppBundleAction.WorkingDirectory = GetMacDevSrcRoot(); // Path.GetFullPath("."); FinalizeAppBundleAction.CommandPath = BuildHostPlatform.Current.Shell; FinalizeAppBundleAction.CommandDescription = ""; // make path to the script FileItem BundleScript = FileItem.GetItemByFileReference(FileReference.Combine(LinkEnvironment.IntermediateDirectory!, "FinalizeAppBundle.sh")); FinalizeAppBundleAction.CommandArguments = "\"" + BundleScript.AbsolutePath + "\""; FinalizeAppBundleAction.PrerequisiteItems.Add(Executable); FinalizeAppBundleAction.ProducedItems.Add(DestFile); FinalizeAppBundleAction.StatusDescription = String.Format("Finalizing app bundle: {0}.app", Path.GetFileName(Executable.AbsolutePath)); FinalizeAppBundleAction.bCanExecuteRemotely = false; return DestFile; } FileItem CopyBundleResource(UEBuildBundleResource Resource, FileItem Executable, DirectoryReference BundleDirectory, IActionGraphBuilder Graph) { Action CopyAction = Graph.CreateAction(ActionType.CreateAppBundle); CopyAction.WorkingDirectory = GetMacDevSrcRoot(); // Path.GetFullPath("."); CopyAction.CommandPath = BuildHostPlatform.Current.Shell; CopyAction.CommandDescription = ""; string BundlePath = BundleDirectory.FullName; string SourcePath = Path.Combine(Path.GetFullPath("."), Resource.ResourcePath!); string TargetPath = Path.Combine(BundlePath, "Contents", Resource.BundleContentsSubdir!, Path.GetFileName(Resource.ResourcePath)!); FileItem TargetItem = FileItem.GetItemByPath(TargetPath); CopyAction.CommandArguments = String.Format("-c \"cp -f -R \\\"{0}\\\" \\\"{1}\\\"; touch -c \\\"{2}\\\"\"", SourcePath, Path.GetDirectoryName(TargetPath)!.Replace('\\', '/') + "/", TargetPath.Replace('\\', '/')); CopyAction.PrerequisiteItems.Add(Executable); CopyAction.ProducedItems.Add(TargetItem); CopyAction.bShouldOutputStatusDescription = Resource.bShouldLog; CopyAction.StatusDescription = String.Format("Copying {0} to app bundle", Path.GetFileName(Resource.ResourcePath)); CopyAction.bCanExecuteRemotely = false; return TargetItem; } private static Dictionary BundleContentsDirectories = new(); public override void ModifyBuildProducts(ReadOnlyTargetRules Target, UEBuildBinary Binary, IEnumerable Libraries, IEnumerable BundleResources, Dictionary BuildProducts) { if (Target.bUsePDBFiles == true) { KeyValuePair[] BuildProductsArray = BuildProducts.ToArray(); foreach (KeyValuePair BuildProductPair in BuildProductsArray) { string[] DebugExtensions = Array.Empty(); switch (BuildProductPair.Value) { case BuildProductType.Executable: DebugExtensions = UEBuildPlatform.GetBuildPlatform(Target.Platform).GetDebugInfoExtensions(Target, UEBuildBinaryType.Executable); break; case BuildProductType.DynamicLibrary: DebugExtensions = UEBuildPlatform.GetBuildPlatform(Target.Platform).GetDebugInfoExtensions(Target, UEBuildBinaryType.DynamicLinkLibrary); break; } string? DSYMExtension = Array.Find(DebugExtensions, element => element == ".dSYM"); if (!String.IsNullOrEmpty(DSYMExtension)) { string BinaryPath = BuildProductPair.Key.FullName; if (BinaryPath.Contains(".app")) { while (BinaryPath.Contains(".app")) { BinaryPath = Path.GetDirectoryName(BinaryPath)!; } BinaryPath = Path.Combine(BinaryPath, BuildProductPair.Key.GetFileName()); BinaryPath = Path.ChangeExtension(BinaryPath, DSYMExtension); FileReference Ref = new FileReference(BinaryPath); BuildProducts[Ref] = BuildProductType.SymbolFile; } } else if (BuildProductPair.Value == BuildProductType.SymbolFile && BuildProductPair.Key.FullName.Contains(".app")) { BuildProducts.Remove(BuildProductPair.Key); } if (BuildProductPair.Value == BuildProductType.DynamicLibrary && Target.bCreateMapFile) { BuildProducts.Add(new FileReference(BuildProductPair.Key.FullName + ".map"), BuildProductType.MapFile); } } } if (Target.bIsBuildingConsoleApplication) { return; } if (!BundleContentsDirectories.ContainsKey(Target) && Binary.Type == UEBuildBinaryType.Executable) { // For Mac binary executables, we may build it outside of app, but expect it to be inside of .app after modern Xcode does its thing // We still keep the executable outside of app as RequiredResource, so that Horde will copy in-between agents FileReference FinalBinaryPath = Binary.OutputFilePath; if (!FinalBinaryPath.Directory.FullName.EndsWith(".app/Contents/MacOS", StringComparison.OrdinalIgnoreCase)) { FinalBinaryPath = FileReference.Combine(FinalBinaryPath.Directory, FinalBinaryPath.GetFileName() + ".app", "Contents", "MacOS", FinalBinaryPath.GetFileName()); BuildProducts[Binary.OutputFilePath] = BuildProductType.RequiredResource; BuildProducts.Add(FinalBinaryPath, BuildProductType.Executable); } BundleContentsDirectories.Add(Target, FinalBinaryPath.Directory.ParentDirectory!); } DirectoryReference? BundleContentsDirectory = BundleContentsDirectories.GetValueOrDefault(Target); // We need to know what third party dylibs would be copied to the bundle if (Binary.Type != UEBuildBinaryType.StaticLibrary) { foreach (UEBuildBundleResource Resource in BundleResources) { if (Directory.Exists(Resource.ResourcePath)) { foreach (string ResourceFile in Directory.GetFiles(Resource.ResourcePath, "*", SearchOption.AllDirectories)) { BuildProducts.Add(FileReference.Combine(BundleContentsDirectory!, Resource.BundleContentsSubdir!, ResourceFile.Substring(Path.GetDirectoryName(Resource.ResourcePath)!.Length + 1)), BuildProductType.RequiredResource); } } else if (BundleContentsDirectory != null) { BuildProducts.Add(FileReference.Combine(BundleContentsDirectory, Resource.BundleContentsSubdir!, Path.GetFileName(Resource.ResourcePath)!), BuildProductType.RequiredResource); } } } if (Binary.Type == UEBuildBinaryType.Executable) { // And we also need all the resources BuildProducts.Add(FileReference.Combine(BundleContentsDirectory!, "Info.plist"), BuildProductType.RequiredResource); BuildProducts.Add(FileReference.Combine(BundleContentsDirectory!, "PkgInfo"), BuildProductType.RequiredResource); // when we codesign, we need to copy the signature around on build machines, etc. this will put the CodeResources (that were created by post-build signing) // is in the .target receipt file BuildProducts.Add(FileReference.Combine(BundleContentsDirectory!, "_CodeSignature", "CodeResources"), BuildProductType.RequiredResource); // modern xcode doesn't use the bootstrap launcher because it can make full .app with staged data inside it if (!bUseModernXcode) { if (Target.Type == TargetType.Editor) { BuildProducts.Add(FileReference.Combine(BundleContentsDirectory!, "Resources/UnrealEditor.icns"), BuildProductType.RequiredResource); BuildProducts.Add(FileReference.Combine(BundleContentsDirectory!, "Resources/UProject.icns"), BuildProductType.RequiredResource); } else { string IconName = Target.Name; if (IconName == "EpicGamesBootstrapLauncher") { IconName = "EpicGamesLauncher"; } BuildProducts.Add(FileReference.Combine(BundleContentsDirectory!, "Resources/" + IconName + ".icns"), BuildProductType.RequiredResource); } } } } private List DebugInfoFiles = new List(); public override void FinalizeOutput(ReadOnlyTargetRules Target, TargetMakefileBuilder MakefileBuilder) { base.FinalizeOutput(Target, MakefileBuilder); TargetMakefile Makefile = MakefileBuilder.Makefile; // Re-add any .dSYM files that may have been stripped out. List OutputFiles = Makefile.OutputItems.Select(Item => Path.ChangeExtension(Item.FullName, ".dSYM")).Distinct().ToList(); foreach (FileItem DebugItem in DebugInfoFiles) { if (OutputFiles.Any(Item => String.Equals(Item, DebugItem.FullName, StringComparison.InvariantCultureIgnoreCase))) { Makefile.OutputItems.Add(DebugItem); } } } public override ICollection PostBuild(ReadOnlyTargetRules Target, FileItem Executable, LinkEnvironment BinaryLinkEnvironment, IActionGraphBuilder Graph) { ICollection OutputFiles = base.PostBuild(Target, Executable, BinaryLinkEnvironment, Graph); if (BinaryLinkEnvironment.bIsBuildingLibrary) { return OutputFiles; } // modern handle bundles via xcode if (!bUseModernXcode) { if (BinaryLinkEnvironment.BundleDirectory != null) { foreach (UEBuildBundleResource Resource in BinaryLinkEnvironment.AdditionalBundleResources) { OutputFiles.Add(CopyBundleResource(Resource, Executable, BinaryLinkEnvironment.BundleDirectory, Graph)); } } } // For Mac, generate the dSYM file if the config file is set to do so if (BinaryLinkEnvironment.bUsePDBFiles == true) { DebugInfoFiles.Add(GenerateDebugInfo(Executable, BinaryLinkEnvironment, Graph, Logger)); } if ((BinaryLinkEnvironment.bIsBuildingDLL && (Options & ClangToolChainOptions.OutputDylib) == 0) || (BinaryLinkEnvironment.bIsBuildingConsoleApplication && Executable.Name.EndsWith("-Cmd"))) { return OutputFiles; } bool bIsBuildingAppBundle = !BinaryLinkEnvironment.bIsBuildingDLL && !BinaryLinkEnvironment.bIsBuildingLibrary && !BinaryLinkEnvironment.bIsBuildingConsoleApplication; if (bIsBuildingAppBundle) { if (!bUseModernXcode) { OutputFiles.Add(FinalizeAppBundle(Target, BinaryLinkEnvironment, Executable, Graph)); } } return OutputFiles; } public void StripSymbols(FileReference SourceFile, FileReference TargetFile) { StripSymbolsWithXcode(SourceFile, TargetFile, Settings.ToolchainDir); } }; }