// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; using System.Threading.Tasks; using EpicGames.Core; namespace EpicGames.BuildGraph.Expressions { /// /// Exception for constructing nodes /// public sealed class BgNodeException : Exception { /// /// Constructor /// /// public BgNodeException(string message) : base(message) { } } /// /// Speecifies the node name for a method. Parameters from the method may be embedded in the name using the {ParamName} syntax. /// [AttributeUsage(AttributeTargets.Method)] public sealed class BgNodeNameAttribute : Attribute { /// /// The format string /// public string Template { get; } /// /// Constructor /// /// Format string for the name public BgNodeNameAttribute(string template) { Template = template; } } /// /// Specification for a node to execute /// public class BgNode : BgExpr { /// /// Name of the node /// public BgString Name { get; } /// /// Thunk to native code to execute the node /// public BgThunk Thunk { get; } /// /// Number of outputs from this node /// public int OutputCount { get; protected set; } /// /// The default output of this node. Includes all other outputs. /// public BgFileSet DefaultOutput { get; } /// /// Agent for the node to be run on /// public BgAgent Agent { get; } /// /// Tokens for inputs of this node /// public BgList Inputs { get; private set; } = BgList.Empty(); /// /// Weak dependency on outputs that must be generated for the node to run, without making those dependencies inputs. /// public BgList Fences { get; private set; } = BgList.Empty(); /// /// Whether this node should start running as soon as its dependencies are ready, even if other nodes in the same agent are not. /// public BgBool RunEarly { get; private set; } = BgBool.False; /// /// Labels that this node contributes to /// public BgList Labels { get; private set; } = BgList.Empty(); /// /// Constructor /// public BgNode(BgThunk thunk, BgAgent agent) : base(BgExprFlags.ForceFragment) { Name = GetDefaultNodeName(thunk); Thunk = thunk; Agent = agent; DefaultOutput = new BgFileSetFromNodeOutputExpr(this, 0); } /// /// Copy constructor /// /// public BgNode(BgNode node) : base(BgExprFlags.ForceFragment) { Name = node.Name; Thunk = node.Thunk; Agent = node.Agent; DefaultOutput = new BgFileSetFromNodeOutputExpr(this, 0); Inputs = node.Inputs; Fences = node.Fences; RunEarly = node.RunEarly; Labels = node.Labels; } /// public override void Write(BgBytecodeWriter writer) { BgObject obj = BgObject.Empty; obj = obj.Set(x => x.Name, Name); obj = obj.Set(x => x.Agent, Agent); obj = obj.Set(x => x.Thunk, Thunk); obj = obj.Set(x => x.OutputCount, (BgInt)OutputCount); obj = obj.Set(x => x.InputExprs, Inputs); obj = obj.Set(x => x.OrderDependencies, Fences); obj = obj.Set(x => x.RunEarly, RunEarly); obj = obj.Set(x => x.Labels, Labels); writer.WriteExpr(obj); } /// /// Creates a copy of this node and updates the given parameters /// /// /// /// /// /// internal BgNode Modify(BgList? inputs = null, BgList? fences = null, BgBool? runEarly = null, BgList? labels = null) { BgNode node = Clone(); if (inputs is not null) { node.Inputs = inputs; } if (fences is not null) { node.Fences = fences; } if (runEarly is not null) { node.RunEarly = runEarly; } if (labels is not null) { node.Labels = labels; } return node; } /// /// Clone this node /// /// Clone of this node protected virtual BgNode Clone() => new BgNode(this); /// /// Gets the default tag name for the numbered output index /// /// Name of the node /// Index of the output. Index zero is the default, others are explicit. /// internal static string GetDefaultTagName(string name, int index) { return $"#{name}${index}"; } static BgString GetDefaultNodeName(BgThunk thunk) { // Check if it's got an attribute override for the node name BgNodeNameAttribute? nameAttr = thunk.Method.GetCustomAttribute(); if (nameAttr != null) { return GetNodeNameFromTemplate(nameAttr.Template, thunk.Method.GetParameters(), thunk.Arguments); } else { return GetNodeNameFromMethodName(thunk.Method.Name); } } static BgString GetNodeNameFromTemplate(string template, ParameterInfo[] parameters, IReadOnlyList arguments) { // Create a list of lazily computed string fragments which comprise the evaluated name List fragments = new List(); int lastIdx = 0; for (int nextIdx = 0; nextIdx < template.Length; nextIdx++) { if (template[nextIdx] == '{') { if (nextIdx + 1 < template.Length && template[nextIdx + 1] == '{') { fragments.Add(template.Substring(lastIdx, nextIdx - lastIdx)); lastIdx = ++nextIdx; } else { fragments.Add(template.Substring(lastIdx, nextIdx - lastIdx)); nextIdx++; int endIdx = template.IndexOf('}', nextIdx); if (endIdx == -1) { throw new BgNodeException($"Unterminated parameter expression for {nameof(BgNodeNameAttribute)} in {template}"); } StringView paramName = new StringView(template, nextIdx, endIdx - nextIdx); int paramIdx = Array.FindIndex(parameters, x => x.Name != null && paramName.Equals(x.Name, StringComparison.Ordinal)); if (paramIdx == -1) { throw new BgNodeException($"Unable to find parameter named {paramName} in {template}"); } object? arg = arguments[paramIdx]; if (typeof(BgExpr).IsAssignableFrom(parameters[paramIdx].ParameterType)) { fragments.Add(((BgExpr)arg!).ToBgString()); } else if (arg != null) { fragments.Add(arg.ToString() ?? String.Empty); } lastIdx = nextIdx = endIdx + 1; } } else if (template[nextIdx] == '}') { if (nextIdx + 1 < template.Length && template[nextIdx + 1] == '{') { fragments.Add(template.Substring(lastIdx, nextIdx - lastIdx)); lastIdx = ++nextIdx; } } } fragments.Add(template.Substring(lastIdx, template.Length - lastIdx)); if (fragments.Count == 1) { return fragments[0]; } else { return BgString.Join(BgString.Empty, fragments); } } /// /// Inserts spaces into a PascalCase method name to create a node name /// public static string GetNodeNameFromMethodName(string methodName) { StringBuilder name = new StringBuilder(); name.Append(methodName[0]); int length = methodName.Length; if (length > 5 && methodName.EndsWith("Async", StringComparison.Ordinal)) { length -= 5; } bool bIsAcronym = false; for (int idx = 1; idx < length; idx++) { bool bLastIsUpper = Char.IsUpper(methodName[idx - 1]); bool bNextIsUpper = Char.IsUpper(methodName[idx]); if (bLastIsUpper && bNextIsUpper) { bIsAcronym = true; } else if (bIsAcronym) { name.Insert(name.Length - 2, ' '); bIsAcronym = false; } else if (!bLastIsUpper && bNextIsUpper) { name.Append(' '); } name.Append(methodName[idx]); } return name.ToString(); } /// /// Implicit conversion to a fileset /// /// public static implicit operator BgFileSet(BgNode node) { return new BgFileSetFromNodeExpr(node); } /// /// Implicit conversion to a fileset /// /// public static implicit operator BgList(BgNode node) { return (BgFileSet)node; } /// public override BgString ToBgString() => Name ?? BgString.Empty; } /// /// Nodespec with a typed return value /// /// public class BgNode : BgNode { /// /// Output from this node /// public T Output { get; } /// /// Constructor /// public BgNode(BgThunk thunk, BgAgent agent) : base(thunk, agent) { Output = CreateOutput(); } /// /// Copy constructor /// public BgNode(BgNode other) : base(other) { Output = CreateOutput(); } /// /// Clone this node /// /// Clone of this node protected override BgNode Clone() => new BgNode(this); T CreateOutput() { Type type = typeof(T); if (IsValueTuple(type)) { BgExpr[] outputs = CreateOutputExprs(type.GetGenericArguments()); OutputCount = outputs.Length; return (T)Activator.CreateInstance(type, outputs)!; } else { BgExpr[] outputs = CreateOutputExprs(new[] { type }); OutputCount = outputs.Length; return (T)(object)outputs[0]; } } BgExpr[] CreateOutputExprs(Type[] types) { BgExpr[] outputs = new BgExpr[types.Length]; for (int idx = 0; idx < types.Length; idx++) { outputs[idx] = CreateOutputExpr(types[idx], idx); } return outputs; } BgExpr CreateOutputExpr(Type type, int index) { if (type == typeof(BgFileSet)) { return new BgFileSetFromNodeOutputExpr(this, index); } else { throw new NotImplementedException(); } } internal static bool IsValueTuple(Type returnType) { if (returnType.IsGenericType) { Type genericType = returnType.GetGenericTypeDefinition(); if (genericType.FullName != null && genericType.FullName.StartsWith("System.ValueTuple`", StringComparison.Ordinal)) { return true; } } return false; } } /// /// Extension methods for BgNode types /// public static class BgNodeExtensions { /// /// Creates a node builder for the given agent /// /// Agent to run the node /// Function to execute /// Node builder public static BgNode AddNode(this BgAgent agent, Expression> func) { BgThunk thunk = BgThunk.Create(func); return new BgNode(thunk, agent); } /// /// Creates a node builder for the given agent /// /// Agent to run the node /// Function to execute /// Node builder public static BgNode AddNode(this BgAgent agent, Expression>> func) { BgThunk thunk = BgThunk.Create(func); return new BgNode(thunk, agent); } /// /// Add dependencies onto other nodes or outputs. Outputs from the given tokens will be copied to the current machine before execution of the node. /// /// The node to modify /// Files to add as inputs /// The current node spec, to allow chaining calls public static T Requires(this T node, params BgNode[] inputs) where T : BgNode { return (T)node.Modify(inputs: node.Inputs.Add(inputs.Select(x => (BgFileSet)x))); } /// /// Add dependencies onto other nodes or outputs. Outputs from the given tokens will be copied to the current machine before execution of the node. /// /// The node to modify /// Files to add as inputs /// The current node spec, to allow chaining calls public static T Requires(this T node, params BgFileSet[] inputs) where T : BgNode { return (T)node.Modify(inputs: node.Inputs.Add(inputs)); } /// /// Add dependencies onto other nodes or outputs. Outputs from the given tokens will be copied to the current machine before execution of the node. /// /// The node to modify /// Files to add as inputs /// The current node spec, to allow chaining calls public static T Requires(this T node, BgList inputs) where T : BgNode { return (T)node.Modify(inputs: node.Inputs.Add(inputs)); } /// /// Add weak dependencies onto other nodes or outputs. The producing nodes must complete successfully if they are part of the graph, but outputs from them will not be /// transferred to the machine running this node. /// /// The node to modify /// Files to add as inputs /// The current node spec, to allow chaining calls public static T After(this T node, params BgNode[] inputs) where T : BgNode { return (T)node.Modify(fences: node.Fences.Add(inputs)); } /// /// Add a label to the node. /// /// The node to modify /// The label to add to the node. /// The current node spec, to allow chaining calls public static T AddLabel(this T node, BgLabel label) where T : BgNode { return (T)node.Modify(labels: node.Labels.Add(label)); } } }