using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnrealBuildTool;
using AutomationTool;
using System.Xml;
namespace AutomationTool
{
///
/// Reference to a node's output
///
class NodeOutput
{
///
/// The node which produces the given output
///
public Node ProducingNode;
///
/// Name of the output
///
public string Name;
///
/// Constructor
///
/// Node which produces the given output
/// Name of the output
public NodeOutput(Node InProducingNode, string InName)
{
ProducingNode = InProducingNode;
Name = InName;
}
///
/// Returns a string representation of this output for debugging purposes
///
/// The name of this output
public override string ToString()
{
return (ProducingNode.Name == Name)? Name : String.Format("{0}: {1}", ProducingNode.Name, Name);
}
}
///
/// 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 output names 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;
///
/// 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 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
public Node(string InName, NodeOutput[] InInputs, string[] InOutputNames, Node[] InInputDependencies, Node[] InOrderDependencies, ManualTrigger InControllingTrigger)
{
Name = InName;
Inputs = InInputs;
Outputs = InOutputNames.Select(x => new NodeOutput(this, x)).ToArray();
InputDependencies = InInputDependencies;
OrderDependencies = InOrderDependencies;
ControllingTrigger = InControllingTrigger;
}
///
/// Build all the tasks for this node
///
/// Information about the current job
/// Set of build products produced by this node.
/// 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)
{
bool bResult = true;
// Allow tasks to merge together
MergeTasks();
// Build everything
HashSet BuildProducts = new HashSet();
foreach(CustomTask Task in Tasks)
{
if(!Task.Execute(Job, BuildProducts, TagNameToFileSet))
{
CommandUtils.Log("Failed to execute task.");
return false;
}
}
// Build a mapping of build product to the outputs it belongs to, using the filesets created by the tasks.
Dictionary FileToOutput = new Dictionary();
foreach(NodeOutput Output in Outputs)
{
HashSet FileSet = TagNameToFileSet[Output.Name];
foreach(FileReference File in FileSet)
{
NodeOutput ExistingOutput;
if(FileToOutput.TryGetValue(File, out ExistingOutput))
{
CommandUtils.LogError("Build product is added to multiple outputs; {0} added to {1} and {2}", File.MakeRelativeTo(new DirectoryReference(CommandUtils.CmdEnv.LocalRoot)), ExistingOutput.Name, Output.Name);
bResult = false;
continue;
}
FileToOutput.Add(File, Output);
}
}
// Add any remaining valid build products into the output channel for this node. Since it's a catch-all output whose build products were not explicitly specified by the user, we can remove
// those which are outside the root directory or which no longer exist (they may have been removed by downstream tasks).
HashSet DefaultOutputs = TagNameToFileSet[Name];
foreach(FileReference BuildProduct in BuildProducts)
{
if(!FileToOutput.ContainsKey(BuildProduct) && BuildProduct.IsUnderDirectory(CommandUtils.RootDirectory) && BuildProduct.Exists())
{
DefaultOutputs.Add(BuildProduct);
}
}
return bResult;
}
///
/// Merge tasks which can be combined together
///
void MergeTasks()
{
List MergedTasks = new List();
while(Tasks.Count > 0)
{
CustomTask NextTask = Tasks[0];
Tasks.RemoveAt(0);
NextTask.Merge(Tasks);
MergedTasks.Add(NextTask);
}
Tasks = MergedTasks;
}
///
/// 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 downstream of the given trigger
///
/// The trigger to check
/// True if the node is downstream of the trigger, false otherwise
public bool IsControlledBy(ManualTrigger Trigger)
{
return Trigger == null || ControllingTrigger == Trigger || (ControllingTrigger != null && ControllingTrigger.IsDownstreamFrom(Trigger));
}
///
/// 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);
Node[] RequireNodes = Inputs.Where(x => x.Name == x.ProducingNode.Name).Select(x => x.ProducingNode).ToArray();
string[] RequireNames = RequireNodes.Select(x => x.Name).Union(Inputs.Where(x => !RequireNodes.Contains(x.ProducingNode)).Select(x => "#" + x.Name)).ToArray();
if (RequireNames.Length > 0)
{
Writer.WriteAttributeString("Requires", String.Join(";", RequireNames));
}
string[] ProducesNames = Outputs.Where(x => x.Name != Name).Select(x => "#" + x.Name).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());
}
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;
}
}
}