// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnrealBuildTool; using AutomationTool; using System.Xml; using Tools.DotNETCommon; namespace AutomationTool { /// /// Reference to an output tag from a particular node /// class NodeOutput { /// /// The node which produces the given output /// public Node ProducingNode; /// /// Name of the tag /// public string TagName; /// /// Constructor /// /// Node which produces the given output /// Name of the tag public NodeOutput(Node InProducingNode, string InTagName) { ProducingNode = InProducingNode; TagName = InTagName; } /// /// Returns a string representation of this output for debugging purposes /// /// The name of this output public override string ToString() { return String.Format("{0}: {1}", ProducingNode.Name, TagName); } } /// /// Defines a node, a container for tasks and the smallest unit of execution that can be run as part of a build graph. /// class Node { /// /// The node's name /// public string Name; /// /// Array of inputs which this node requires to run /// public NodeOutput[] Inputs; /// /// Array of outputs produced by this node /// public NodeOutput[] Outputs; /// /// Nodes which this node has input dependencies on /// public Node[] InputDependencies; /// /// Nodes which this node needs to run after /// public Node[] OrderDependencies; /// /// The trigger which controls whether this node will be executed /// public ManualTrigger ControllingTrigger; /// /// Tokens which must be acquired for this node to run /// public FileReference[] RequiredTokens; /// /// List of tasks to execute /// public List Tasks = new List(); /// /// List of email addresses to notify if this node fails. /// public HashSet NotifyUsers = new HashSet(StringComparer.InvariantCultureIgnoreCase); /// /// If set, anyone that has submitted to one of the given paths will be notified on failure of this node /// public HashSet NotifySubmitters = new HashSet(StringComparer.InvariantCultureIgnoreCase); /// /// Whether to start this node as soon as its dependencies are satisfied, rather than waiting for all of its agent's dependencies to be met. /// public bool bRunEarly = false; /// /// Whether to ignore warnings produced by this node /// public bool bNotifyOnWarnings = true; /// /// Constructor /// /// The name of this node /// Inputs that this node depends on /// Names of the outputs that this node produces /// Nodes which this node is dependent on for its inputs /// Nodes which this node needs to run after. Should include all input dependencies. /// The trigger which this node is behind /// Optional tokens which must be required for this node to run public Node(string InName, NodeOutput[] InInputs, string[] InOutputNames, Node[] InInputDependencies, Node[] InOrderDependencies, ManualTrigger InControllingTrigger, FileReference[] InRequiredTokens) { Name = InName; Inputs = InInputs; List AllOutputs = new List(); AllOutputs.Add(new NodeOutput(this, "#" + Name)); AllOutputs.AddRange(InOutputNames.Where(x => String.Compare(x, Name, StringComparison.InvariantCultureIgnoreCase) != 0).Select(x => new NodeOutput(this, x))); Outputs = AllOutputs.ToArray(); InputDependencies = InInputDependencies; OrderDependencies = InOrderDependencies; ControllingTrigger = InControllingTrigger; RequiredTokens = InRequiredTokens; } /// /// Returns the default output for this node, which includes all build products /// public NodeOutput DefaultOutput { get { return Outputs[0]; } } /// /// Build all the tasks for this node /// /// Information about the current job /// Mapping from tag names to the set of files they include. Should be set to contain the node inputs on entry. /// Whether the task succeeded or not. Exiting with an exception will be caught and treated as a failure. public bool Build(JobContext Job, Dictionary> TagNameToFileSet) { // Run each of the tasks in order HashSet BuildProducts = TagNameToFileSet[DefaultOutput.TagName]; for(int Idx = 0; Idx < Tasks.Count; Idx++) { ITaskExecutor Executor = Tasks[Idx].GetExecutor(); if (Executor == null) { // Execute this task directly try { Tasks[Idx].Execute(Job, BuildProducts, TagNameToFileSet); } catch (Exception Ex) { ExceptionUtils.AddContext(Ex, "while executing task {0}", Tasks[Idx].GetTraceString()); if(Tasks[Idx].SourceLocation != null) { ExceptionUtils.AddContext(Ex, "at {0}({1})", GetReadablePathForDiagnostics(Tasks[Idx].SourceLocation.Item1), Tasks[Idx].SourceLocation.Item2); } throw; } } else { // The task has a custom executor, which may be able to execute several tasks simultaneously. Try to add the following tasks. int FirstIdx = Idx; while (Idx + 1 < Tasks.Count && Executor.Add(Tasks[Idx + 1])) { Idx++; } try { Executor.Execute(Job, BuildProducts, TagNameToFileSet); } catch (Exception Ex) { for(int TaskIdx = FirstIdx; TaskIdx <= Idx; TaskIdx++) { ExceptionUtils.AddContext(Ex, "while executing {0}", Tasks[TaskIdx].GetTraceString()); } if (Tasks[FirstIdx].SourceLocation != null) { ExceptionUtils.AddContext(Ex, "at {0}({1})", GetReadablePathForDiagnostics(Tasks[FirstIdx].SourceLocation.Item1), Tasks[FirstIdx].SourceLocation.Item2); } throw; } } } // Remove anything that doesn't exist, since these files weren't explicitly tagged BuildProducts.RemoveWhere(x => !FileReference.Exists(x)); return true; } /// /// Converts a file into a more readable form for diangostic messages /// /// Path to the file /// Readable form of the path static string GetReadablePathForDiagnostics(FileReference File) { if(File.IsUnderDirectory(CommandUtils.RootDirectory)) { return File.MakeRelativeTo(CommandUtils.RootDirectory); } else { return File.FullName; } } /// /// Determines the minimal set of direct input dependencies for this node to run /// /// Sequence of nodes that are direct inputs to this node public IEnumerable GetDirectInputDependencies() { HashSet DirectDependencies = new HashSet(InputDependencies); foreach(Node InputDependency in InputDependencies) { DirectDependencies.ExceptWith(InputDependency.InputDependencies); } return DirectDependencies; } /// /// Determines the minimal set of direct order dependencies for this node to run /// /// Sequence of nodes that are direct order dependencies of this node public IEnumerable GetDirectOrderDependencies() { HashSet DirectDependencies = new HashSet(OrderDependencies); foreach(Node OrderDependency in OrderDependencies) { DirectDependencies.ExceptWith(OrderDependency.OrderDependencies); } return DirectDependencies; } /// /// Checks whether this node is behind the given trigger /// /// The trigger to check /// True if the node is directly or indirectly behind the given trigger, false otherwise public bool IsBehind(ManualTrigger Trigger) { for(ManualTrigger OtherTrigger = ControllingTrigger; OtherTrigger != Trigger; OtherTrigger = OtherTrigger.Parent) { if(OtherTrigger == null) { return false; } } return true; } /// /// Write this node to an XML writer /// /// The writer to output the node to public void Write(XmlWriter Writer) { Writer.WriteStartElement("Node"); Writer.WriteAttributeString("Name", Name); string[] RequireNames = Inputs.Select(x => x.TagName).ToArray(); if (RequireNames.Length > 0) { Writer.WriteAttributeString("Requires", String.Join(";", RequireNames)); } string[] ProducesNames = Outputs.Where(x => x != DefaultOutput).Select(x => x.TagName).ToArray(); if (ProducesNames.Length > 0) { Writer.WriteAttributeString("Produces", String.Join(";", ProducesNames)); } string[] AfterNames = GetDirectOrderDependencies().Except(InputDependencies).Select(x => x.Name).ToArray(); if (AfterNames.Length > 0) { Writer.WriteAttributeString("After", String.Join(";", AfterNames)); } if (!bNotifyOnWarnings) { Writer.WriteAttributeString("NotifyOnWarnings", bNotifyOnWarnings.ToString()); } if(bRunEarly) { Writer.WriteAttributeString("RunEarly", bRunEarly.ToString()); } foreach (CustomTask Task in Tasks) { Task.Write(Writer); } Writer.WriteEndElement(); } /// /// Returns the name of this node /// /// The name of this node public override string ToString() { return Name; } } }