// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Text; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Mac-specific target settings /// public class MacTargetRules { /// /// Enables address sanitizer (ASan). /// [CommandLine("-EnableASan")] [XmlConfigFile(Category = "BuildConfiguration", Name = "bEnableAddressSanitizer")] public bool bEnableAddressSanitizer = false; /// /// Enables LibFuzzer. /// [CommandLine("-EnableLibFuzzer")] [XmlConfigFile(Category = "BuildConfiguration", Name = "bEnableLibFuzzer")] public bool bEnableLibFuzzer = false; /// /// Enables thread sanitizer (TSan). /// [CommandLine("-EnableTSan")] [XmlConfigFile(Category = "BuildConfiguration", Name = "bEnableThreadSanitizer")] public bool bEnableThreadSanitizer = false; /// /// Enables undefined behavior sanitizer (UBSan). /// [CommandLine("-EnableUBSan")] [XmlConfigFile(Category = "BuildConfiguration", Name = "bEnableUndefinedBehaviorSanitizer")] public bool bEnableUndefinedBehaviorSanitizer = false; /// /// Enables the generation of .dsym files. This can be disabled to enable faster iteration times during development. /// [CommandLine("-EnableDSYM", Value = "true")] [XmlConfigFile(Category = "BuildConfiguration", Name = "bUseDSYMFiles")] public bool bUseDSYMFiles = false; /// /// Disables clang build verification checks on static libraries /// [CommandLine("-skipclangvalidation", Value = "true")] [XmlConfigFile(Category = "BuildConfiguration", Name = "bSkipClangValidation")] public bool bSkipClangValidation = false; /// /// Enables runtime ray tracing support. /// [ConfigFile(ConfigHierarchyType.Engine, "/Script/MacTargetPlatform.MacTargetSettings", "bEnableRayTracing")] public bool bEnableRayTracing = false; /// /// Constructor /// public MacTargetRules() { string? AddressSanitizer = Environment.GetEnvironmentVariable("ENABLE_ADDRESS_SANITIZER"); if (AddressSanitizer != null && AddressSanitizer == "YES") { bEnableAddressSanitizer = true; } string? ThreadSanitizer = Environment.GetEnvironmentVariable("ENABLE_THREAD_SANITIZER"); if (ThreadSanitizer != null && ThreadSanitizer == "YES") { bEnableThreadSanitizer = true; } string? UndefSanitizerMode = Environment.GetEnvironmentVariable("ENABLE_UNDEFINED_BEHAVIOR_SANITIZER"); if (UndefSanitizerMode != null && UndefSanitizerMode == "YES") { bEnableUndefinedBehaviorSanitizer = true; } } } /// /// Read-only wrapper for Mac-specific target settings /// public class ReadOnlyMacTargetRules { /// /// The private mutable settings object /// private MacTargetRules Inner; /// /// Constructor /// /// The settings object to wrap public ReadOnlyMacTargetRules(MacTargetRules Inner) { this.Inner = Inner; } /// /// Accessors for fields on the inner TargetRules instance /// #region Read-only accessor properties #pragma warning disable CS1591 public bool bEnableAddressSanitizer => Inner.bEnableAddressSanitizer; public bool bEnableLibFuzzer => Inner.bEnableLibFuzzer; public bool bEnableThreadSanitizer => Inner.bEnableThreadSanitizer; public bool bEnableUndefinedBehaviorSanitizer => Inner.bEnableUndefinedBehaviorSanitizer; public bool bSkipClangValidation => Inner.bSkipClangValidation; public bool bEnableRayTracing => Inner.bEnableRayTracing; #pragma warning restore CS1591 #endregion } class MacArchitectureConfig : UnrealArchitectureConfig { public MacArchitectureConfig() : base(UnrealArchitectureMode.SingleTargetCompileSeparately, new[] { UnrealArch.X64, UnrealArch.Arm64 }) { } public override UnrealArch GetHostArchitecture() { return MacExports.IsRunningOnAppleArchitecture ? UnrealArch.Arm64 : UnrealArch.X64; } public override string ConvertToReadableArchitecture(UnrealArch Architecture) { if (Architecture == UnrealArch.X64) { return "Intel"; } if (Architecture == UnrealArch.Arm64) { return "Apple"; } return base.ConvertToReadableArchitecture(Architecture); } public override UnrealArchitectures ActiveArchitectures(FileReference? ProjectFile, string? TargetName) { return GetProjectArchitectures(ProjectFile, TargetName, false, false); } public override UnrealArchitectures DistributionArchitectures(FileReference? ProjectFile, string? TargetName) { return GetProjectArchitectures(ProjectFile, TargetName, false, true); } public override UnrealArchitectures ProjectSupportedArchitectures(FileReference? ProjectFile, string? TargetName = null) { return GetProjectArchitectures(ProjectFile, TargetName, true, false); } private static Dictionary ProjectArchitectureCache = new(); private UnrealArchitectures GetProjectArchitectures(FileReference? ProjectFile, string? TargetName, bool bGetAllSupported, bool bIsDistributionMode) { string Key = $"{ProjectFile}{TargetName}{bGetAllSupported}{bIsDistributionMode}"; lock (ProjectArchitectureCache) { UnrealArchitectures? CachedArches; if (ProjectArchitectureCache.TryGetValue(Key, out CachedArches)) { return CachedArches; } } bool bIsEditor = false; bool bIsBuildMachine = Unreal.IsBuildMachine(); // get project ini from ProjetFile, or if null, then try to get it from the target rules if (TargetName != null) { RulesAssembly RulesAsm; if (ProjectFile == null) { RulesAsm = RulesCompiler.CreateEngineRulesAssembly(Unreal.IsEngineInstalled(), false, false, Log.Logger); } else { RulesAsm = RulesCompiler.CreateProjectRulesAssembly(ProjectFile, Unreal.IsEngineInstalled(), false, false, Log.Logger); } try { // CreateTargetRules here needs to have an UnrealArchitectures object, because otherwise with 'null', it will call // back to this function to get the ActiveArchitectures! in this case the arch is unimportant UnrealArchitectures DummyArchitectures = new(UnrealArch.X64); TargetRules? Rules = RulesAsm.CreateTargetRules(TargetName, UnrealTargetPlatform.Mac, UnrealTargetConfiguration.Development, DummyArchitectures, ProjectFile, null, Log.Logger, ValidationOptions: TargetRulesValidationOptions.ValidateNothing); bIsEditor = Rules.Type == TargetType.Editor; // the projectfile passed in may be a game's uproject file that we are compiling a program in the context of, // but we still want the settings for the program if (Rules.Type == TargetType.Program) { ProjectFile = Rules.ProjectFile; } } catch (Exception) { // do nothing if it fails, assume no project } } ConfigHierarchy EngineIni = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, ProjectFile?.Directory, UnrealTargetPlatform.Mac); // get values from project ini string SupportKey = bIsEditor ? "EditorTargetArchitecture" : "TargetArchitecture"; string DefaultKey = bIsEditor ? "EditorDefaultArchitecture" : "DefaultArchitecture"; string SupportedArchitecture; string DefaultArchitecture; bool bBuildAllSupportedOnBuildMachine; EngineIni.GetString("/Script/MacTargetPlatform.MacTargetSettings", SupportKey, out SupportedArchitecture); EngineIni.GetString("/Script/MacTargetPlatform.MacTargetSettings", DefaultKey, out DefaultArchitecture); EngineIni.GetBool("/Script/MacTargetPlatform.MacTargetSettings", "bBuildAllSupportedOnBuildMachine", out bBuildAllSupportedOnBuildMachine); SupportedArchitecture = SupportedArchitecture.ToLower(); DefaultArchitecture = DefaultArchitecture.ToLower(); bool bSupportsArm64 = SupportedArchitecture.Contains("universal") || SupportedArchitecture.Contains("apple"); bool bSupportsX86 = SupportedArchitecture.Contains("universal") || SupportedArchitecture.Contains("intel"); // make sure we found a good value if (!bSupportsArm64 && !bSupportsX86) { throw new BuildException($"Unknown {SupportKey} value found ('{SupportedArchitecture}') in .ini"); } // choose a supported architecture(s) based on desired type List Architectures = new(); // return all supported if getting supported, compiling for distribution, or we want active, and "all" is selected if (bGetAllSupported || bIsDistributionMode || DefaultArchitecture.Equals("all", StringComparison.InvariantCultureIgnoreCase) || (bIsBuildMachine && bBuildAllSupportedOnBuildMachine)) { if (bSupportsArm64) { Architectures.Add(UnrealArch.Arm64); } if (bSupportsX86) { Architectures.Add(UnrealArch.X64); } } else if (DefaultArchitecture.Contains("host")) { // if we don't support Arm, then always use X64, otherwise use whatever the host arch is Architectures.Add(bSupportsArm64 ? UnrealArch.Host.Value : UnrealArch.X64); } else if (DefaultArchitecture.Contains("apple")) { if (!bSupportsArm64) { throw new BuildException($"{DefaultKey} is set to {DefaultArchitecture}, but AppleSilicon is not a supported architecture"); } Architectures.Add(UnrealArch.Arm64); } else if (DefaultArchitecture.Contains("intel")) { if (!bSupportsX86) { throw new BuildException($"{DefaultKey} is set to {DefaultArchitecture}, but Intel is not a supported architecture"); } Architectures.Add(UnrealArch.X64); } else { throw new BuildException($"Unknown {DefaultKey} value found ('{DefaultArchitecture}') in .ini"); } UnrealArchitectures Result = new UnrealArchitectures(Architectures); lock (ProjectArchitectureCache) { // Another thread could have beaten us to the add ProjectArchitectureCache.TryAdd(Key, Result); } return Result; } } abstract class AppleBuildPlatform : UEBuildPlatform { public AppleBuildPlatform(UnrealTargetPlatform Platform, UEBuildPlatformSDK SDK, UnrealArchitectureConfig ArchitectureConfig, ILogger Logger) : base(Platform, SDK, ArchitectureConfig, Logger) { } public override void GetExternalBuildMetadata(FileReference? ProjectFile, StringBuilder Metadata) { base.GetExternalBuildMetadata(ProjectFile, Metadata); Metadata.AppendLine("xcode-select: {0}", ApplePlatformSDK.DeveloperDir); } public override void SetUpEnvironment(ReadOnlyTargetRules Target, CppCompileEnvironment CompileEnvironment, LinkEnvironment LinkEnvironment) { // shared stuff for all apple platforms CompileEnvironment.Definitions.Add("PLATFORM_APPLE=1"); bool bUseSwiftUIMain; AppleExports.GetSwiftIntegrationSettings(Target.ProjectFile, Target.Type, Platform, out bUseSwiftUIMain, out _); CompileEnvironment.Definitions.Add("UE_USE_SWIFT_UI_MAIN=" + (bUseSwiftUIMain ? "1" : "0")); } } class MacPlatform : AppleBuildPlatform { public MacPlatform(UEBuildPlatformSDK InSDK, ILogger InLogger) : base(UnrealTargetPlatform.Mac, InSDK, new MacArchitectureConfig(), InLogger) { } public override bool CanUseFASTBuild() { return true; } public override void ResetTarget(TargetRules Target) { } public override void ValidateTarget(TargetRules Target) { base.ValidateTarget(Target); if (!String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CLANG_STATIC_ANALYZER_MODE"))) { Target.StaticAnalyzer = StaticAnalyzer.Default; Target.StaticAnalyzerOutputType = (Environment.GetEnvironmentVariable("CLANG_ANALYZER_OUTPUT")?.Contains("html", StringComparison.OrdinalIgnoreCase) == true) ? StaticAnalyzerOutputType.Html : StaticAnalyzerOutputType.Text; Target.StaticAnalyzerMode = String.Equals(Environment.GetEnvironmentVariable("CLANG_STATIC_ANALYZER_MODE"), "shallow", StringComparison.OrdinalIgnoreCase) ? StaticAnalyzerMode.Shallow : StaticAnalyzerMode.Deep; } else if (Target.StaticAnalyzer == StaticAnalyzer.Clang) { Target.StaticAnalyzer = StaticAnalyzer.Default; } // Disable linking and ignore build outputs if we're using a static analyzer if (Target.StaticAnalyzer == StaticAnalyzer.Default) { Target.bDisableLinking = true; Target.bIgnoreBuildOutputs = true; // Clang static analysis requires non unity builds Target.bUseUnityBuild = false; if (Target.bStaticAnalyzerIncludeGenerated) { Target.bAlwaysUseUnityForGeneratedFiles = false; } // Disable chaining PCHs for the moment because it is crashing clang Target.bChainPCHs = false; } if (BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac) { // @todo: Temporarily disable precompiled header files when building remotely due to errors Target.bUsePCHFiles = false; } // Mac-Arm todo - Remove this all when we feel confident no more x86-only plugins will come around bool bCompilingForArm = Target.Architectures.Contains(UnrealArch.Arm64); if (bCompilingForArm && Target.Name != "UnrealHeaderTool") { Target.DisablePlugins.AddRange(Array.Empty()); } if (Target.bCompileAgainstEngine) { Target.GlobalDefinitions.Add("HAS_METAL=1"); Target.ExtraModuleNames.Add("MetalRHI"); } else { Target.GlobalDefinitions.Add("HAS_METAL=0"); } if (Target.StaticAllocator == StaticAllocatorType.None) { Target.StaticAllocator = StaticAllocatorType.Binned3; } // Force using the ANSI allocator if ASan is enabled if (Target.MacPlatform.bEnableAddressSanitizer) { Target.StaticAllocator = StaticAllocatorType.Ansi; } Target.GlobalDefinitions.Add("GL_SILENCE_DEPRECATION=1"); Target.bUsePDBFiles = Target.DebugInfo != DebugInfoMode.None && ShouldCreateDebugInfo(new ReadOnlyTargetRules(Target)); Target.bUsePDBFiles &= Target.MacPlatform.bUseDSYMFiles; // we always deploy - the build machines need to be able to copy the files back, which needs the full bundle Target.bDeployAfterCompile = true; Target.bCheckSystemHeadersForModification = BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac; } /// /// Determines if the given name is a build product for a target. /// /// The name to check /// Target or application names that may appear at the start of the build product name (eg. "UnrealEditor", "ShooterGameEditor") /// Suffixes which may appear at the end of the build product name /// True if the string matches the name of a build product, false otherwise public override bool IsBuildProduct(string FileName, string[] NamePrefixes, string[] NameSuffixes) { return IsBuildProductName(FileName, NamePrefixes, NameSuffixes, "") || IsBuildProductName(FileName, NamePrefixes, NameSuffixes, ".dsym") || IsBuildProductName(FileName, NamePrefixes, NameSuffixes, ".dylib") || IsBuildProductName(FileName, NamePrefixes, NameSuffixes, ".a") || IsBuildProductName(FileName, NamePrefixes, NameSuffixes, ".app"); } /// /// Get the extension to use for the given binary type /// /// The binrary type being built /// string The binary extenstion (ie 'exe' or 'dll') public override string GetBinaryExtension(UEBuildBinaryType InBinaryType) { switch (InBinaryType) { case UEBuildBinaryType.DynamicLinkLibrary: return ".dylib"; case UEBuildBinaryType.Executable: return ""; case UEBuildBinaryType.StaticLibrary: return ".a"; } return base.GetBinaryExtension(InBinaryType); } /// /// Get the extensions to use for debug info for the given binary type /// /// Rules for the target being built /// The binary type being built /// string[] The debug info extensions (i.e. 'pdb') public override string[] GetDebugInfoExtensions(ReadOnlyTargetRules Target, UEBuildBinaryType InBinaryType) { switch (InBinaryType) { case UEBuildBinaryType.DynamicLinkLibrary: case UEBuildBinaryType.Executable: return Target.bUsePDBFiles ? new string[] { ".dSYM" } : Array.Empty(); case UEBuildBinaryType.StaticLibrary: default: return Array.Empty(); } } /// /// Modify the rules for a newly created module, where the target is a different host platform. /// This is not required - but allows for hiding details of a particular platform. /// /// The name of the module /// The module rules /// The target being build public override void ModifyModuleRulesForOtherPlatform(string ModuleName, ModuleRules Rules, ReadOnlyTargetRules Target) { // don't do any target platform stuff if SDK is not available bool bIsPlatformAvailableForTarget = UEBuildPlatform.IsPlatformAvailableForTarget(Platform, Target, bIgnoreSDKCheck: true); bool bIsPlatformAvailableForTargetWithSDK = UEBuildPlatform.IsPlatformAvailableForTarget(Platform, Target); if (!bIsPlatformAvailableForTarget) { return; } if (Target.Platform == UnrealTargetPlatform.Win64 && Target.Type == TargetType.Editor) { // because remote IOS building needs the new XcodeProject Settings to show up in the editor, we bring in the Mac bits that expose it if (ModuleName == "Engine") { Rules.DynamicallyLoadedModuleNames.Add("MacTargetPlatformSettings"); if (bIsPlatformAvailableForTargetWithSDK) { Rules.DynamicallyLoadedModuleNames.AddAll("MacTargetPlatform", "MacTargetPlatformControls", "MacPlatformEditor"); } } } } public override DirectoryReference? GetBundleDirectory(ReadOnlyTargetRules Rules, List OutputFiles) { if (Rules.bIsBuildingConsoleApplication) { return null; } else { return OutputFiles[0].Directory.ParentDirectory!.ParentDirectory; } } /// /// For platforms that need to output multiple files per binary (ie Android "fat" binaries) /// this will emit multiple paths. By default, it simply makes an array from the input /// public override List FinalizeBinaryPaths(FileReference BinaryName, FileReference? ProjectFile, ReadOnlyTargetRules Target) { List BinaryPaths = new List(); // ModernXcode now builds binary outside of .app, instead Xcode will be responsible of generating .app if (AppleExports.UseModernXcode(ProjectFile) || (Target.bIsBuildingConsoleApplication || !String.IsNullOrEmpty(BinaryName.GetExtension()))) { BinaryPaths.Add(BinaryName); } else { BinaryPaths.Add(new FileReference(BinaryName.FullName + ".app/Contents/MacOS/" + BinaryName.GetFileName())); } return BinaryPaths; } /// /// Modify the rules for a newly created module, in a target that's being built for this platform. /// This is not required - but allows for hiding details of a particular platform. /// /// The name of the module /// The module rules /// The target being build public override void ModifyModuleRulesForActivePlatform(string ModuleName, ModuleRules Rules, ReadOnlyTargetRules Target) { bool bBuildShaderFormats = Target.bForceBuildShaderFormats; if (!Target.bBuildRequiresCookedData) { if (ModuleName == "TargetPlatform") { bBuildShaderFormats = true; } } // allow standalone tools to use target platform modules, without needing Engine if (ModuleName == "TargetPlatform") { if (Target.bForceBuildTargetPlatforms) { Rules.DynamicallyLoadedModuleNames.Add("MacTargetPlatform"); Rules.DynamicallyLoadedModuleNames.Add("MacTargetPlatformSettings"); Rules.DynamicallyLoadedModuleNames.Add("MacTargetPlatformControls"); } if (bBuildShaderFormats) { // Rules.DynamicallyLoadedModuleNames.Add("ShaderFormatD3D"); Rules.DynamicallyLoadedModuleNames.Add("ShaderFormatOpenGL"); Rules.DynamicallyLoadedModuleNames.Add("MetalShaderFormat"); Rules.DynamicallyLoadedModuleNames.Add("ShaderFormatVectorVM"); Rules.DynamicallyLoadedModuleNames.Remove("VulkanRHI"); Rules.DynamicallyLoadedModuleNames.Add("VulkanShaderFormat"); } } } /// /// Setup the target environment for building /// /// Settings for the target being compiled /// The compile environment for this target /// The link environment for this target public override void SetUpEnvironment(ReadOnlyTargetRules Target, CppCompileEnvironment CompileEnvironment, LinkEnvironment LinkEnvironment) { base.SetUpEnvironment(Target, CompileEnvironment, LinkEnvironment); CompileEnvironment.Definitions.Add("PLATFORM_MAC=1"); if (Target.MacPlatform.bEnableRayTracing && Target.Type != TargetType.Server) { CompileEnvironment.Definitions.Add("RHI_RAYTRACING=1"); } CompileEnvironment.Definitions.Add("WITH_TTS=0"); CompileEnvironment.Definitions.Add("WITH_SPEECH_RECOGNITION=0"); } /// /// Whether this platform should create debug information or not /// /// The target being built /// true if debug info should be generated, false if not public override bool ShouldCreateDebugInfo(ReadOnlyTargetRules Target) { // Always generate debug symbols on the build machines. bool IsBuildMachine = Unreal.IsBuildMachine(); switch (Target.Configuration) { case UnrealTargetConfiguration.Development: case UnrealTargetConfiguration.Shipping: case UnrealTargetConfiguration.Test: return !Target.bOmitPCDebugInfoInDevelopment || IsBuildMachine; case UnrealTargetConfiguration.DebugGame: case UnrealTargetConfiguration.Debug: default: return true; } } /// /// Creates a toolchain instance for the given platform. /// /// The target being built /// New toolchain instance. public override UEToolChain CreateToolChain(ReadOnlyTargetRules Target) { ClangToolChainOptions Options = ClangToolChainOptions.None; if (Target.MacPlatform.bEnableAddressSanitizer) { Options |= ClangToolChainOptions.EnableAddressSanitizer; } if (Target.MacPlatform.bEnableThreadSanitizer) { Options |= ClangToolChainOptions.EnableThreadSanitizer; } if (Target.MacPlatform.bEnableUndefinedBehaviorSanitizer) { Options |= ClangToolChainOptions.EnableUndefinedBehaviorSanitizer; } if (Target.bShouldCompileAsDLL) { Options |= ClangToolChainOptions.OutputDylib; } return new MacToolChain(Target, Options, Logger); } /// public override void Deploy(TargetReceipt Receipt) { new UEDeployMac(Logger).PrepTargetForDeployment(Receipt); } } class MacPlatformFactory : UEBuildPlatformFactory { public override UnrealTargetPlatform TargetPlatform => UnrealTargetPlatform.Mac; /// /// Register the platform with the UEBuildPlatform class /// public override void RegisterBuildPlatforms(ILogger Logger) { ApplePlatformSDK SDK = new ApplePlatformSDK(Logger); // Register this build platform for Mac UEBuildPlatform.RegisterBuildPlatform(new MacPlatform(SDK, Logger), Logger); UEBuildPlatform.RegisterPlatformWithGroup(UnrealTargetPlatform.Mac, UnrealPlatformGroup.Apple); UEBuildPlatform.RegisterPlatformWithGroup(UnrealTargetPlatform.Mac, UnrealPlatformGroup.Desktop); UEBuildPlatform.RegisterPlatformWithGroup(UnrealTargetPlatform.Mac, UnrealPlatformGroup.PosixOS); } } }