// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Xml; using System.Diagnostics; using UnrealBuildTool; using EpicGames.Core; using System.Linq; using System.Reflection; using OpenTracing; using OpenTracing.Util; using UnrealBuildBase; using CommandLine = UnrealBuildBase.CommandLine; using Microsoft.Extensions.Logging; using static AutomationTool.CommandUtils; namespace AutomationTool { class UnrealBuildException : AutomationException { public UnrealBuildException(string Message) : base(ExitCode.Error_UBTFailure, "BUILD FAILED: " + Message) { OutputFormat = AutomationExceptionOutputFormat.MinimalError; } public UnrealBuildException(string Format, params object[] Args) : this(String.Format(Format, Args)) { } } [Help("ForceMonolithic", "Toggle to combined the result into one executable")] [Help("ForceDebugInfo", "Forces debug info even in development builds")] [Help("NoXGE", "Toggle to disable the distributed build process")] [Help("ForceNonUnity", "Toggle to disable the unity build system")] [Help("ForceUnity", "Toggle to force enable the unity build system")] [Help("Licensee", "If set, this build is being compiled by a licensee")] public class UnrealBuild { private BuildCommand OwnerCommand; /// /// If true we will let UBT build UHT /// [Obsolete] public bool AlwaysBuildUHT { get; set; } public void AddBuildProduct(string InFile) { string File = CommandUtils.CombinePaths(InFile); if (!CommandUtils.FileExists(File) && !CommandUtils.DirectoryExists(File)) { throw new UnrealBuildException("Specified file to AddBuildProduct {0} does not exist.", File); } BuildProductFiles.Add(File); } static bool IsBuildReceipt(string FileName) { return FileName.EndsWith(".version", StringComparison.InvariantCultureIgnoreCase) || FileName.EndsWith(".target", StringComparison.InvariantCultureIgnoreCase) || FileName.EndsWith(".modules", StringComparison.InvariantCultureIgnoreCase) || FileName.EndsWith("buildid.txt", StringComparison.InvariantCultureIgnoreCase); } BuildManifest AddBuildProductsFromManifest(FileReference ManifestFile) { if (!FileReference.Exists(ManifestFile)) { throw new UnrealBuildException("UBT Manifest {0} does not exist.", ManifestFile); } BuildManifest Manifest = CommandUtils.ReadManifest(ManifestFile); foreach (string Item in Manifest.BuildProducts) { if (!CommandUtils.FileExists_NoExceptions(Item) && !CommandUtils.DirectoryExists_NoExceptions(Item)) { throw new UnrealBuildException($"AddBuildProductsFromManifest: {Item} was in manifest \"{ManifestFile}\" but could not be found."); } AddBuildProduct(Item); } return Manifest; } private void PrepareUBT() { if (!FileReference.Exists(UnrealBuildToolDll)) { throw new UnrealBuildException($"UnrealBuildTool.dll does not exist at {UnrealBuildToolDll}"); } if (!FileReference.Exists(Unreal.DotnetPath)) { throw new UnrealBuildException($"dotnet executable does not exist at {Unreal.DotnetPath}"); } } public void CleanWithUBT(string TargetName, UnrealTargetPlatform Platform, UnrealTargetConfiguration Config, FileReference UprojectPath, string InAddArgs = "") { string AddArgs = ""; if (UprojectPath != null) { AddArgs += " " + CommandUtils.MakePathSafeToUseWithCommandLine(UprojectPath.FullName); } AddArgs += " -NoUBTMakefiles"; AddArgs += " " + InAddArgs; PrepareUBT(); using (IScope Scope = GlobalTracer.Instance.BuildSpan("Compile").StartActive()) { Scope.Span.SetTag("target", TargetName); Scope.Span.SetTag("platform", Platform.ToString()); Scope.Span.SetTag("config", Config.ToString()); CommandUtils.RunUBT(CommandUtils.CmdEnv, UnrealBuildToolDll: UnrealBuildToolDll, Project: UprojectPath, Target: TargetName, Platform: Platform, Config: Config, AdditionalArgs: "-Clean -NoHotReload" + AddArgs); } } List GetManifestFiles(List Targets) { List ManifestFiles = new List(); for (int Idx = 0; Idx < Targets.Count; Idx++) { BuildTarget Target = Targets[Idx]; DirectoryReference ManifestDir = GetManifestDir(Target.UprojectPath); ManifestFiles.Add(FileReference.Combine(ManifestDir, $"Manifest-{Idx + 1}-{Target.TargetName}-{Target.Platform}-{Target.Config}.xml")); } return ManifestFiles; } FileReference GetManifestFile(BuildTarget Target, int Index) { DirectoryReference ManifestDir = GetManifestDir(Target.UprojectPath); if (Index == -1) { return FileReference.Combine(ManifestDir, "Manifest.xml"); } else { return FileReference.Combine(ManifestDir, $"Manifest-{Index + 1}-{Target.TargetName}-{Target.Platform}-{Target.Config}.xml"); } } void BuildWithUBT(List Targets, Dictionary TargetToManifest, bool DisableXGE, bool AllCores, bool SkipBuild) { List ManifestFiles = new List(Targets.Count); StringBuilder FullCommandLine = new StringBuilder(); if (Targets.Count == 1) { ManifestFiles.Add(GetManifestFile(Targets[0], -1)); FullCommandLine.Append(GetTargetArguments(Targets[0], ManifestFiles[0])); } else { List Arguments = new List(); for (int Idx = 0; Idx < Targets.Count; Idx++) { ManifestFiles.Add(GetManifestFile(Targets[Idx], Idx)); Arguments.Add("-Target=" + GetTargetArguments(Targets[Idx], ManifestFiles[Idx])); } FullCommandLine.Append(CommandLine.FormatCommandLine(Arguments)); } if (DisableXGE) { FullCommandLine.Append(" -NoXGE"); } if (AllCores) { FullCommandLine.Append(" -AllCores"); } if (SkipBuild) { FullCommandLine.Append(" -SkipBuild"); } PrepareUBT(); for (int Idx = 0; Idx < ManifestFiles.Count; Idx++) { CommandUtils.DeleteFile(ManifestFiles[Idx]); } CommandUtils.RunUBT(CommandUtils.CmdEnv, UnrealBuildToolDll, FullCommandLine.ToString()); for (int Idx = 0; Idx < Targets.Count; Idx++) { FileReference ManifestFile = ManifestFiles[Idx]; BuildManifest Manifest = AddBuildProductsFromManifest(ManifestFile); CommandUtils.DeleteFile(ManifestFile); TargetToManifest?.Add(Targets[Idx], Manifest); } } string GetTargetArguments(BuildTarget Target, FileReference ManifestFile) { List Arguments = new List { Target.TargetName, Target.Platform.ToString(), Target.Config.ToString() }; if (Target.UprojectPath != null) { Arguments.Add($"-Project={Target.UprojectPath}"); } if (ManifestFile != null) { Arguments.Add($"-Manifest={ManifestFile}"); } string CmdLine = CommandLine.FormatCommandLine(Arguments); if (!String.IsNullOrEmpty(Target.UBTArgs)) { CmdLine = $"{CmdLine} {Target.UBTArgs}"; } return CmdLine; } string[] DotNetProductExtenstions() { return new string[] { ".dll", ".pdb", ".exe.config", ".exe", "exe.mdb" }; } string[] SwarmBuildProducts() { return new string[] { "AgentInterface", "SwarmAgent", "SwarmCoordinator", "SwarmCoordinatorInterface", "SwarmInterface", "SwarmCommonUtils" }; } void AddBuildProductsForCSharpProj(string CsProj) { string BaseOutput = CommandUtils.CmdEnv.LocalRoot + @"/Engine/Binaries/DotNET/" + Path.GetFileNameWithoutExtension(CsProj); foreach (var Ext in DotNetProductExtenstions()) { if (CommandUtils.FileExists(BaseOutput + Ext)) { AddBuildProduct(BaseOutput + Ext); } } } void AddIOSBuildProductsForCSharpProj(string CsProj) { string BaseOutput = CommandUtils.CmdEnv.LocalRoot + @"/Engine/Binaries/DotNET/IOS/" + Path.GetFileNameWithoutExtension(CsProj); foreach (var Ext in DotNetProductExtenstions()) { if (CommandUtils.FileExists(BaseOutput + Ext)) { AddBuildProduct(BaseOutput + Ext); } } } void AddSwarmBuildProducts() { foreach (var SwarmProduct in SwarmBuildProducts()) { string DotNETOutput = CommandUtils.CmdEnv.LocalRoot + @"/Engine/Binaries/DotNET/" + SwarmProduct; string Win64Output = CommandUtils.CmdEnv.LocalRoot + @"/Engine/Binaries/Win64/" + SwarmProduct; foreach (var Ext in DotNetProductExtenstions()) { if (CommandUtils.FileExists(DotNETOutput + Ext)) { AddBuildProduct(DotNETOutput + Ext); } } foreach (var Ext in DotNetProductExtenstions()) { if (CommandUtils.FileExists(Win64Output + Ext)) { AddBuildProduct(Win64Output + Ext); } } } } /// /// Updates the engine version files /// public List UpdateVersionFiles(bool ActuallyUpdateVersionFiles = true, int? ChangelistNumberOverride = null, int? CompatibleChangelistNumberOverride = null, string Build = null, string BuildURL = null, bool? IsPromotedOverride = null) { bool bIsLicenseeVersion = ParseParam("Licensee") || !FileReference.Exists(FileReference.Combine(Unreal.EngineDirectory, "Restricted", "NotForLicensees", "Build", "EpicInternal.txt")); bool bIsPromotedBuild = IsPromotedOverride.HasValue? IsPromotedOverride.Value : (ParseParamInt("Promoted", 1) != 0); bool bDoUpdateVersionFiles = CommandUtils.P4Enabled && ActuallyUpdateVersionFiles; int ChangelistNumber = 0; if (bDoUpdateVersionFiles) { ChangelistNumber = ChangelistNumberOverride.HasValue? ChangelistNumberOverride.Value : CommandUtils.P4Env.Changelist; } int CompatibleChangelistNumber = 0; if(bDoUpdateVersionFiles && CompatibleChangelistNumberOverride.HasValue) { CompatibleChangelistNumber = CompatibleChangelistNumberOverride.Value; } string Branch = OwnerCommand.ParseParamValue("Branch"); if (String.IsNullOrEmpty(Branch)) { Branch = CommandUtils.P4Enabled ? CommandUtils.EscapePath(CommandUtils.P4Env.Branch) : ""; } return StaticUpdateVersionFiles(ChangelistNumber, CompatibleChangelistNumber, Branch, Build, BuildURL, bIsLicenseeVersion, bIsPromotedBuild, bDoUpdateVersionFiles); } public static List StaticUpdateVersionFiles(int ChangelistNumber, int CompatibleChangelistNumber, string Branch, string Build, string BuildURL, bool bIsLicenseeVersion, bool bIsPromotedBuild, bool bDoUpdateVersionFiles) { FileReference BuildVersionFile = BuildVersion.GetDefaultFileName(); // Get the revision to sync files to before if(CommandUtils.P4Enabled && ChangelistNumber > 0 && !CommandUtils.IsBuildMachine) { CommandUtils.P4.Sync(String.Format("-f \"{0}@{1}\"", BuildVersionFile, ChangelistNumber), false, false); } BuildVersion Version; if(!BuildVersion.TryRead(BuildVersionFile, out Version)) { Version = new BuildVersion(); } List Result = new List(1); { if (bDoUpdateVersionFiles) { Logger.LogDebug("Updating {BuildVersionFile} with:", BuildVersionFile); Logger.LogDebug(" Changelist={ChangelistNumber}", ChangelistNumber); Logger.LogDebug(" CompatibleChangelist={CompatibleChangelistNumber}", CompatibleChangelistNumber); Logger.LogDebug(" IsLicenseeVersion={Arg0}", bIsLicenseeVersion? 1 : 0); Logger.LogDebug(" IsPromotedBuild={Arg0}", bIsPromotedBuild? 1 : 0); Logger.LogDebug(" BranchName={Branch}", Branch); Logger.LogDebug(" BuildVersion={Build}", Build); Logger.LogDebug(" BuildURL={BuildURL}", BuildURL); Version.Changelist = ChangelistNumber; if(CompatibleChangelistNumber > 0) { Version.CompatibleChangelist = CompatibleChangelistNumber; } else if(bIsLicenseeVersion != Version.IsLicenseeVersion) { Version.CompatibleChangelist = 0; // Clear out the compatible changelist number; it corresponds to a different P4 server. } Version.IsLicenseeVersion = bIsLicenseeVersion; Version.IsPromotedBuild = bIsPromotedBuild; Version.BranchName = Branch; Version.BuildVersionString = Build; Version.BuildURL = BuildURL; if (File.Exists(BuildVersionFile.FullName)) { VersionFileUpdater.MakeFileWriteable(BuildVersionFile.FullName, true); } Version.Write(BuildVersionFile); } else { Logger.LogDebug("{BuildVersionFile} will not be updated because P4 is not enabled.", BuildVersionFile); } Result.Add(BuildVersionFile); } return Result; } [DebuggerDisplay("{TargetName} {Platform} {Config}")] public class BuildTarget { /// /// Name of the target /// public string TargetName; /// /// For code-based projects with a .uproject, the TargetName isn't enough for UBT to find the target, this will point UBT to the target /// public FileReference UprojectPath; /// /// Platform to build /// public UnrealTargetPlatform Platform; /// /// Configuration to build /// public UnrealTargetConfiguration Config; /// /// Extra UBT args /// public string UBTArgs; /// /// Whether to clean this target. If not specified, the target will be cleaned if -Clean is on the command line. /// public bool? Clean; /// /// Format as string /// /// public override string ToString() { return string.Format("{0} {1} {2}", TargetName, Platform, Config); } } public class BuildAgenda { /// /// .NET .csproj files that will be compiled and included in the build. Currently we assume the output /// binary file names match the solution file base name, but with various different binary file extensions. /// public List DotNetProjects = new List(); public string SwarmAgentProject = ""; public string SwarmCoordinatorProject = ""; /// /// List of targets to build. These can be various Unreal projects, programs or libraries in various configurations /// public List Targets = new List(); /// /// Adds a target with the specified configuration. /// /// Name of the target /// Platform /// Configuration /// Path to optional uproject file /// Specifies additional arguments for UBT public void AddTarget(string TargetName, UnrealTargetPlatform InPlatform, UnrealTargetConfiguration InConfiguration, FileReference InUprojectPath = null, string InAddArgs = "") { // Is this platform a compilable target? if (!Platform.GetPlatform(InPlatform).CanBeCompiled()) { return; } Targets.Add(new BuildTarget() { TargetName = TargetName, Platform = InPlatform, Config = InConfiguration, UprojectPath = InUprojectPath, UBTArgs = InAddArgs, }); } /// /// Adds multiple targets with the specified configuration. /// /// List of targets. /// Platform /// Configuration /// Path to optional uproject file /// Specifies additional arguments for UBT public void AddTargets(string[] TargetNames, UnrealTargetPlatform InPlatform, UnrealTargetConfiguration InConfiguration, FileReference InUprojectPath = null, string InAddArgs = "") { // Is this platform a compilable target? if (!Platform.GetPlatform(InPlatform).CanBeCompiled()) { return; } foreach (var Target in TargetNames) { Targets.Add(new BuildTarget() { TargetName = Target, Platform = InPlatform, Config = InConfiguration, UprojectPath = InUprojectPath, UBTArgs = InAddArgs, }); } } } public UnrealBuild(BuildCommand Command) { OwnerCommand = Command; BuildProductFiles.Clear(); } private bool ParseParam(string Name) { return OwnerCommand != null && OwnerCommand.ParseParam(Name); } private string ParseParamValue(string Name) { return (OwnerCommand != null)? OwnerCommand.ParseParamValue(Name) : null; } private int ParseParamInt(string Name, int Default = 0) { return (OwnerCommand != null)? OwnerCommand.ParseParamInt(Name, Default) : Default; } /// /// Executes a build. /// /// Build agenda. /// if specified, determines if the build products will be deleted before building. If not specified -clean parameter will be used, /// True if the version files are to be updated /// If true will force XGE off /// If true AND XGE not present or not being used then ensure UBT uses all available cores /// /// /// public void Build(BuildAgenda InAgenda, bool? InDeleteBuildProducts = null, bool InUpdateVersionFiles = true, bool InForceNoXGE = false, bool InAllCores = false, int? InChangelistNumberOverride = null, Dictionary InTargetToManifest = null, bool InSkipBuild = false) { if (!CommandUtils.CmdEnv.HasCapabilityToCompile) { throw new UnrealBuildException("You are attempting to compile on a machine that does not have a supported compiler!"); } bool DeleteBuildProducts = InDeleteBuildProducts.HasValue ? InDeleteBuildProducts.Value : ParseParam("Clean"); if (InUpdateVersionFiles) { UpdateVersionFiles(ActuallyUpdateVersionFiles: true, ChangelistNumberOverride: InChangelistNumberOverride); } ////////////////////////////////////// // make a set of unique platforms involved var UniquePlatforms = new List(); foreach (var Target in InAgenda.Targets) { if (!UniquePlatforms.Contains(Target.Platform)) { UniquePlatforms.Add(Target.Platform); } } if (InAgenda.SwarmAgentProject != "") { string SwarmAgentSolution = Path.Combine(CommandUtils.CmdEnv.LocalRoot, InAgenda.SwarmAgentProject); CommandUtils.BuildSolution(CommandUtils.CmdEnv, SwarmAgentSolution, "Development", "Mixed Platforms"); AddSwarmBuildProducts(); } if (InAgenda.SwarmCoordinatorProject != "") { string SwarmCoordinatorSolution = Path.Combine(CommandUtils.CmdEnv.LocalRoot, InAgenda.SwarmCoordinatorProject); CommandUtils.BuildSolution(CommandUtils.CmdEnv, SwarmCoordinatorSolution, "Development", "Mixed Platforms"); AddSwarmBuildProducts(); } foreach (var DotNetProject in InAgenda.DotNetProjects) { string CsProj = Path.Combine(CommandUtils.CmdEnv.LocalRoot, DotNetProject); CommandUtils.BuildCSharpProject(CommandUtils.CmdEnv, CsProj); AddBuildProductsForCSharpProj(CsProj); } string XGEConsole = null; bool bDisableXGE = ParseParam("NoXGE") || InForceNoXGE; bool bCanUseXGE = !bDisableXGE && PlatformExports.TryGetXgConsoleExecutable(out XGEConsole); Logger.LogDebug("************************* UnrealBuild:"); Logger.LogDebug("************************* UseXGE: {bCanUseXGE}", bCanUseXGE); // Clean all the targets foreach (BuildTarget Target in InAgenda.Targets) { bool bClean = Target.Clean ?? DeleteBuildProducts; if (bClean) { CleanWithUBT(Target.TargetName, Target.Platform, Target.Config, Target.UprojectPath, Target.UBTArgs); } } if (InAgenda.Targets.Count == 0) { return; } // Build all the targets BuildWithUBT(InAgenda.Targets, InTargetToManifest, bDisableXGE, InAllCores, InSkipBuild); } /// /// Checks to make sure there was at least one build product, and that all files exist. Also, logs them all out. /// /// List of files public static void CheckBuildProducts(HashSet BuildProductFiles) { // Check build products { Logger.LogDebug("Build products *******"); if (BuildProductFiles.Count < 1) { Logger.LogInformation("No build products were made"); } else { foreach (var Product in BuildProductFiles) { if (!CommandUtils.FileExists(Product) && !CommandUtils.DirectoryExists(Product)) { throw new UnrealBuildException("{0} was a build product but no longer exists", Product); } Logger.LogDebug("{Text}", Product); } } Logger.LogDebug("End Build products *******"); } } /// /// Adds or edits existing files at head revision, expecting an exclusive lock, resolving by clobbering any existing version /// /// /// List of files to check out public static void AddBuildProductsToChangelist(int WorkingCL, IEnumerable Files) { Logger.LogInformation("Adding {Arg0} build products to changelist {WorkingCL}...", Files.Count(), WorkingCL); foreach (var File in Files) { CommandUtils.P4.Sync("-f -k " + CommandUtils.MakePathSafeToUseWithCommandLine(File) + "#head"); // sync the file without overwriting local one if (!CommandUtils.FileExists(File)) { throw new UnrealBuildException("{0} was a build product but no longer exists", File); } CommandUtils.P4.ReconcileNoDeletes(WorkingCL, CommandUtils.MakePathSafeToUseWithCommandLine(File)); // Change file type on binary files to be always writeable. var FileStats = CommandUtils.P4.FStat(File); if (CommandUtils.IsProbablyAMacOrIOSExe(File)) { if (FileStats.Type == P4FileType.Binary && (FileStats.Attributes & (P4FileAttributes.Executable | P4FileAttributes.Writeable)) != (P4FileAttributes.Executable | P4FileAttributes.Writeable)) { CommandUtils.P4.ChangeFileType(File, (P4FileAttributes.Executable | P4FileAttributes.Writeable)); } } else { if (IsBuildProduct(File, FileStats) && (FileStats.Attributes & P4FileAttributes.Writeable) != P4FileAttributes.Writeable) { CommandUtils.P4.ChangeFileType(File, P4FileAttributes.Writeable); } } } } /// /// Determines if this file is a build product. /// /// File path /// P4 file stats. /// True if this is a Windows build product. False otherwise. private static bool IsBuildProduct(string File, P4FileStat FileStats) { if(FileStats.Type == P4FileType.Binary || IsBuildReceipt(File)) { return true; } return FileStats.Type == P4FileType.Text && File.EndsWith(".exe.config", StringComparison.InvariantCultureIgnoreCase); } /// /// Add UBT files to build products /// public void AddUBTFilesToBuildProducts() { string UBTLocation = UnrealBuildToolDll.Directory.FullName; // copy all the files from the UBT output directory string[] UBTFiles = CommandUtils.FindFiles_NoExceptions("*.*", true, UBTLocation); foreach (string UBTFile in UBTFiles) { AddBuildProduct(UBTFile); } } /// /// Copy the UAT files to their precompiled location, and add them as build products /// public void AddUATFilesToBuildProducts() { // All scripts are expected to exist in DotNET/AutomationScripts subfolder. foreach (FileReference BuildProduct in ScriptManager.BuildProducts) { string UATScriptFilePath = BuildProduct.FullName; if (!CommandUtils.FileExists_NoExceptions(UATScriptFilePath)) { throw new UnrealBuildException("Cannot add UAT to the build products because {0} does not exist.", UATScriptFilePath); } AddBuildProduct(UATScriptFilePath); } } DirectoryReference GetManifestDir(FileReference ProjectFile) { // Can't write to Engine directory on installed builds if (Unreal.IsEngineInstalled() && ProjectFile != null) { return DirectoryReference.Combine(ProjectFile.Directory, "Intermediate", "Build"); } else { return DirectoryReference.Combine(Unreal.EngineDirectory, "Intermediate", "Build"); } } FileReference GetManifestFile(FileReference ProjectFile) { return FileReference.Combine(GetManifestDir(ProjectFile), "Manifest.xml"); } [Obsolete("Deprecated in UE5.1; use UnrealBuildToolDll")] public static string GetUBTExecutable() { return CommandUtils.CombinePaths(CommandUtils.CmdEnv.LocalRoot, @"Engine/Binaries/DotNET/UnrealBuildTool/UnrealBuildTool" + (OperatingSystem.IsWindows() ? ".exe" : "")); } [Obsolete("Deprecated in UE5.1; use UnrealBuildToolDll")] public string UBTExecutable { get { return GetUBTExecutable(); } } public static FileReference UnrealBuildToolDll => FileReference.FromString(CommandUtils.CombinePaths(CommandUtils.CmdEnv.LocalRoot, "Engine/Binaries/DotNET/UnrealBuildTool/UnrealBuildTool.dll")); // List of everything we built so far public readonly HashSet BuildProductFiles = new HashSet(StringComparer.InvariantCultureIgnoreCase); } }