// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.BuildGraph; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnrealBuildTool; using AutomationTool; using System.Xml; using EpicGames.Core; using OpenTracing; namespace AutomationTool.Tasks { /// /// Parameters for a compile task /// public class CompileTaskParameters { /// /// The target to compile. /// [TaskParameter(Optional=true)] public string Target; /// /// The configuration to compile. /// [TaskParameter] public UnrealTargetConfiguration Configuration; /// /// The platform to compile for. /// [TaskParameter] public UnrealTargetPlatform Platform; /// /// The project to compile with. /// [TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.FileSpec)] public string Project; /// /// Additional arguments for UnrealBuildTool. /// [TaskParameter(Optional = true)] public string Arguments; /// /// Whether to allow using XGE for compilation. /// [TaskParameter(Optional = true)] public bool AllowXGE = true; /// /// Whether to allow using the parallel executor for this compile. /// [TaskParameter(Optional = true)] public bool AllowParallelExecutor = true; /// /// Whether to allow cleaning this target. If unspecified, targets are cleaned if the -Clean argument is passed on the command line. /// [TaskParameter(Optional = true)] public bool? Clean = null; /// /// Tag to be applied to build products of this task. /// [TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.TagList)] public string Tag; } /// /// Executor for compile tasks, which can compile multiple tasks simultaneously /// public class CompileTaskExecutor : ITaskExecutor { /// /// List of targets to compile. As well as the target specifically added for this task, additional compile tasks may be merged with it. /// List Targets = new List(); /// /// Mapping of receipt filename to its corresponding tag name /// Dictionary TargetToTagName = new Dictionary(); /// /// Whether to allow using XGE for this job /// bool bAllowXGE = true; /// /// Whether to allow using the parallel executor for this job /// bool bAllowParallelExecutor = true; /// /// Constructor /// /// Initial task to execute public CompileTaskExecutor(CompileTask Task) { Add(Task); } /// /// Adds another task to this executor /// /// Task to add /// True if the task could be added, false otherwise public bool Add(BgTaskImpl Task) { CompileTask CompileTask = Task as CompileTask; if(CompileTask == null) { return false; } if(Targets.Count > 0) { if (bAllowXGE != CompileTask.Parameters.AllowXGE) { return false; } if (!bAllowParallelExecutor || !CompileTask.Parameters.AllowParallelExecutor) { return false; } } CompileTaskParameters Parameters = CompileTask.Parameters; bAllowXGE &= Parameters.AllowXGE; bAllowParallelExecutor &= Parameters.AllowParallelExecutor; UnrealBuild.BuildTarget Target = new UnrealBuild.BuildTarget { TargetName = Parameters.Target, Platform = Parameters.Platform, Config = Parameters.Configuration, UprojectPath = CompileTask.FindProjectFile(), UBTArgs = "-nobuilduht " + (Parameters.Arguments ?? ""), Clean = Parameters.Clean }; if(!String.IsNullOrEmpty(Parameters.Tag)) { TargetToTagName.Add(Target, Parameters.Tag); } Targets.Add(Target); return true; } /// /// Execute all the tasks added to this executor. /// /// Information about the current job /// Set of build products produced by this node. /// Mapping from tag names to the set of files they include /// Whether the task succeeded or not. Exiting with an exception will be caught and treated as a failure. public Task ExecuteAsync(JobContext Job, HashSet BuildProducts, Dictionary> TagNameToFileSet) { // Create the agenda UnrealBuild.BuildAgenda Agenda = new UnrealBuild.BuildAgenda(); Agenda.Targets.AddRange(Targets); // Build everything Dictionary TargetToManifest = new Dictionary(); UnrealBuild Builder = new UnrealBuild(Job.OwnerCommand); bool bCanUseParallelExecutor = (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64 && bAllowParallelExecutor); // parallel executor is only available on Windows as of 2016-09-22 Builder.Build(Agenda, InDeleteBuildProducts: null, InUpdateVersionFiles: false, InForceNoXGE: !bAllowXGE, InUseParallelExecutor: bCanUseParallelExecutor, InTargetToManifest: TargetToManifest); UnrealBuild.CheckBuildProducts(Builder.BuildProductFiles); // Tag all the outputs foreach(KeyValuePair TargetTagName in TargetToTagName) { BuildManifest Manifest; if(!TargetToManifest.TryGetValue(TargetTagName.Key, out Manifest)) { throw new AutomationException("Missing manifest for target {0} {1} {2}", TargetTagName.Key.TargetName, TargetTagName.Key.Platform, TargetTagName.Key.Config); } foreach(string TagName in CustomTask.SplitDelimitedList(TargetTagName.Value)) { HashSet FileSet = CustomTask.FindOrAddTagSet(TagNameToFileSet, TagName); FileSet.UnionWith(Manifest.BuildProducts.Select(x => new FileReference(x))); } } // Add everything to the list of build products BuildProducts.UnionWith(Builder.BuildProductFiles.Select(x => new FileReference(x))); return Task.CompletedTask; } } /// /// Compiles a target with UnrealBuildTool. /// [TaskElement("Compile", typeof(CompileTaskParameters))] public class CompileTask : BgTaskImpl { /// /// Parameters for this task /// public CompileTaskParameters Parameters; /// /// Resolved path to Project file /// public FileReference ProjectFile = null; /// /// Construct a compile task /// /// Parameters for this task public CompileTask(CompileTaskParameters Parameters) { this.Parameters = Parameters; } /// /// Resolve the path to the project file /// public FileReference FindProjectFile() { FileReference ProjectFile = null; // Resolve the full path to the project file if(!String.IsNullOrEmpty(Parameters.Project)) { if(Parameters.Project.EndsWith(".uproject", StringComparison.OrdinalIgnoreCase)) { ProjectFile = CustomTask.ResolveFile(Parameters.Project); } else { ProjectFile = NativeProjects.EnumerateProjectFiles().FirstOrDefault(x => x.GetFileNameWithoutExtension().Equals(Parameters.Project, StringComparison.OrdinalIgnoreCase)); } if(ProjectFile == null || !FileReference.Exists(ProjectFile)) { throw new BuildException("Unable to resolve project '{0}'", Parameters.Project); } } return ProjectFile; } /// /// Execute the task. /// /// Information about the current job /// Set of build products produced by this node. /// Mapping from tag names to the set of files they include public override Task ExecuteAsync(JobContext Job, HashSet BuildProducts, Dictionary> TagNameToFileSet) { // // Don't do any logic here. You have to do it in the ctor or a getter // otherwise you break the ParallelExecutor pathway, which doesn't call this function! // return GetExecutor().ExecuteAsync(Job, BuildProducts, TagNameToFileSet); } /// /// /// /// public override ITaskExecutor GetExecutor() { return new CompileTaskExecutor(this); } /// /// Get properties to include in tracing info /// /// The span to add metadata to /// Prefix for all metadata keys public override void GetTraceMetadata(ITraceSpan Span, string Prefix) { base.GetTraceMetadata(Span, Prefix); Span.AddMetadata(Prefix + "target.name", Parameters.Target); Span.AddMetadata(Prefix + "target.config", Parameters.Configuration.ToString()); Span.AddMetadata(Prefix + "target.platform", Parameters.Platform.ToString()); if (Parameters.Project != null) { Span.AddMetadata(Prefix + "target.project", Parameters.Project); } } /// /// Get properties to include in tracing info /// /// The span to add metadata to /// Prefix for all metadata keys public override void GetTraceMetadata(ISpan Span, string Prefix) { base.GetTraceMetadata(Span, Prefix); Span.SetTag(Prefix + "target.name", Parameters.Target); Span.SetTag(Prefix + "target.config", Parameters.Configuration.ToString()); Span.SetTag(Prefix + "target.platform", Parameters.Platform.ToString()); if (Parameters.Project != null) { Span.SetTag(Prefix + "target.project", Parameters.Project); } } /// /// Output this task out to an XML writer. /// public override void Write(XmlWriter Writer) { Write(Writer, Parameters); } /// /// Find all the tags which are used as inputs to this task /// /// The tag names which are read by this task public override IEnumerable FindConsumedTagNames() { yield break; } /// /// Find all the tags which are modified by this task /// /// The tag names which are modified by this task public override IEnumerable FindProducedTagNames() { return FindTagNamesFromList(Parameters.Tag); } } public static partial class StandardTasks { /// /// Compiles a target /// /// The target to compile /// The configuration to compile /// The platform to compile for /// The project to compile with /// Additional arguments for UnrealBuildTool /// Whether to allow using XGE for compilation /// Whether to allow using the parallel executor for this compile /// Whether to allow cleaning this target. If unspecified, targets are cleaned if the -Clean argument is passed on the command line /// Build products from the compile public static async Task CompileAsync(string Target, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, FileReference Project = null, string Arguments = null, bool AllowXGE = true, bool AllowParallelExecutor = true, bool? Clean = null) { CompileTaskParameters Parameters = new CompileTaskParameters(); Parameters.Target = Target; Parameters.Platform = Platform; Parameters.Configuration = Configuration; Parameters.Project = Project?.FullName; Parameters.Arguments = Arguments; Parameters.AllowXGE = AllowXGE; Parameters.AllowParallelExecutor = AllowParallelExecutor; Parameters.Clean = Clean; return await ExecuteAsync(new CompileTask(Parameters)); } } }