// 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.BuildGraph.Expressions; using EpicGames.Core; using Microsoft.Extensions.Logging; namespace EpicGames.BuildGraph { /// /// 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 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 BgNodeSpec { internal string _className; internal string _methodName; internal IBgExpr?[] _argumentExprs; internal BgFileSet[] ResultExprs { get; } /// /// Name of the node /// public BgString Name { get; set; } /// /// Tokens for inputs of this node /// public BgList InputDependencies { get; set; } = BgList.Empty; /// /// Weak dependency on outputs that must be generated for the node to run, without making those dependencies inputs. /// public BgList AfterDependencies { get; set; } = BgList.Empty; /// /// Token for the output of this node /// BgFileSet Output { get; } /// /// All inputs and outputs of this node /// BgList InputsAndOutputs => InputDependencies.Add(Output); /// /// 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 CanRunEarly { get; set; } = BgBool.False; /// /// Diagnostics for running this node /// public BgList Diagnostics { get; set; } = BgList.Empty; /// /// Constructor /// internal BgNodeSpec(MethodCallExpression call) { string methodSignature = $"{call.Method.DeclaringType?.Name}.{call.Method.Name}"; if (!call.Method.IsStatic) { throw new BgNodeException($"Node {methodSignature} must be static"); } _className = call.Method.DeclaringType!.AssemblyQualifiedName!; _methodName = call.Method.Name; try { _argumentExprs = CreateArgumentExprs(call); Name = GetDefaultNodeName(call.Method, _argumentExprs); ResultExprs = CreateReturnExprs(call.Method, Name, call.Method.ReturnType); } catch (Exception ex) { ExceptionUtils.AddContext(ex, $"while calling method {methodSignature}"); throw; } Output = new BgFileSetTagFromStringExpr(BgString.Format("#{0}", Name)); } /// /// Creates a node specification /// internal static BgNodeSpec Create(Expression> action) { MethodCallExpression call = (MethodCallExpression)action.Body; return new BgNodeSpec(call); } /// /// Creates a node specification /// internal static BgNodeSpec Create(Expression>> function) { MethodCallExpression call = (MethodCallExpression)function.Body; return new BgNodeSpec(call); } static IBgExpr?[] CreateArgumentExprs(MethodCallExpression call) { IBgExpr?[] args = new IBgExpr?[call.Arguments.Count]; for (int idx = 0; idx < call.Arguments.Count; idx++) { Expression expr = call.Arguments[idx]; if (expr is ParameterExpression parameterExpr) { if (parameterExpr.Type != typeof(BgContext)) { throw new BgNodeException($"Unable to determine type of parameter '{parameterExpr.Name}'"); } } else { Delegate compiled = Expression.Lambda(expr).Compile(); object? result = compiled.DynamicInvoke(); if (result is IBgExpr computable) { args[idx] = computable; } else { args[idx] = (BgString)(result?.ToString() ?? String.Empty); } } } return args; } static BgFileSet[] CreateReturnExprs(MethodInfo methodInfo, BgString name, Type type) { if (type == typeof(Task)) { return Array.Empty(); } else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)) { return CreateReturnExprsInner(methodInfo, name, type.GetGenericArguments()[0]); } else { throw new NotImplementedException(); } } static BgFileSet[] CreateReturnExprsInner(MethodInfo methodInfo, BgString name, Type type) { Type[] outputTypes; if (IsValueTuple(type)) { outputTypes = type.GetGenericArguments(); } else { outputTypes = new[] { type }; } BgFileSet[] outputExprs = new BgFileSet[outputTypes.Length]; for (int idx = 0; idx < outputTypes.Length; idx++) { outputExprs[idx] = CreateOutputExpr(methodInfo, name, idx, outputTypes[idx]); } return outputExprs; } static BgFileSet CreateOutputExpr(MethodInfo methodInfo, BgString name, int index, Type type) { if (type == typeof(BgFileSet)) { return new BgFileSetTagFromStringExpr(BgString.Format("#{0}${1}", name, index)); } else { throw new BgNodeException($"Unsupported return type for {methodInfo.Name}: {type.Name}"); } } 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; } static BgString GetDefaultNodeName(MethodInfo methodInfo, IBgExpr?[] args) { // Check if it's got an attribute override for the node name BgNodeNameAttribute? nameAttr = methodInfo.GetCustomAttribute(); if (nameAttr != null) { return GetNodeNameFromTemplate(nameAttr.Template, methodInfo.GetParameters(), args); } else { return GetNodeNameFromMethodName(methodInfo.Name); } } static BgString GetNodeNameFromTemplate(string template, ParameterInfo[] parameters, IBgExpr?[] args) { // 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}"); } IBgExpr? arg = args[paramIdx]; if (arg != null) { fragments.Add(arg.ToBgString()); } 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)); 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(); } /// /// Gets the signature for a method /// public static string GetSignature(MethodInfo methodInfo) { StringBuilder arguments = new StringBuilder(); foreach (ParameterInfo parameterInfo in methodInfo.GetParameters()) { arguments.AppendLine(parameterInfo.ParameterType.FullName); } return $"{methodInfo.Name}:{Digest.Compute(arguments.ToString())}"; } /// /// Creates a concrete node /// /// /// /// /// internal void AddToGraph(BgExprContext context, BgGraph graph, BgAgent agent) { HashSet inputTags = new HashSet(); inputTags.UnionWith(InputDependencies.ComputeTags(context)); inputTags.UnionWith(_argumentExprs.OfType().Select(x => x.ComputeTag(context))); inputTags.UnionWith(_argumentExprs.OfType>().SelectMany(x => x.GetEnumerable(context)).Select(x => x.ComputeTag(context))); HashSet afterTags = new HashSet(inputTags); afterTags.UnionWith(AfterDependencies.ComputeTags(context)); string name = Name.Compute(context); BgNodeOutput[] inputDependencies = inputTags.Select(x => graph.TagNameToNodeOutput[x]).Distinct().ToArray(); BgNodeOutput[] afterDependencies = afterTags.Select(x => graph.TagNameToNodeOutput[x]).Distinct().ToArray(); string[] outputNames = Array.ConvertAll(ResultExprs, x => x.ComputeTag(context)); BgNode[] inputNodes = inputDependencies.Select(x => x.ProducingNode).Distinct().ToArray(); BgNode[] afterNodes = afterDependencies.Select(x => x.ProducingNode).Distinct().ToArray(); bool runEarly = CanRunEarly.Compute(context); BgNode node = new BgNode(name, inputDependencies, outputNames, inputNodes, afterNodes, Array.Empty()); node.RunEarly = runEarly; BgScriptLocation location = new BgScriptLocation("(unknown)", "(unknown)", 1); BgTask task = new BgTask(location, "CallMethod"); task.Arguments["Class"] = _className; task.Arguments["Method"] = _methodName; for (int idx = 0; idx < _argumentExprs.Length; idx++) { IBgExpr? argumentExpr = _argumentExprs[idx]; if (argumentExpr != null) { task.Arguments[$"Arg{idx + 1}"] = BgType.Get(argumentExpr.GetType()).SerializeArgument(argumentExpr, context); } } if (outputNames != null) { task.Arguments["Tags"] = String.Join(";", outputNames); } node.Tasks.Add(task); agent.Nodes.Add(node); graph.NameToNode.Add(name, node); foreach (BgDiagnosticSpec precondition in Diagnostics.GetEnumerable(context)) { precondition.AddToGraph(context, graph, agent, node); } foreach (BgNodeOutput output in node.Outputs) { graph.TagNameToNodeOutput.Add(output.TagName, output); } } /// /// Allows using the /// /// public static implicit operator BgList(BgNodeSpec nodeSpec) { return nodeSpec.InputsAndOutputs.Add(nodeSpec.Output); } } /// /// Specification for a node to execute that returns one or more tagged outputs /// /// Return type from the node. Must consist of latent Bg* types (eg. BgToken) public class BgNodeSpec : BgNodeSpec { /// /// Output value from this node /// public T Output { get; } /// /// Constructor /// internal BgNodeSpec(MethodCallExpression call) : base(call) { Type returnType = call.Method.ReturnType; if (IsValueTuple(returnType)) { Output = (T)Activator.CreateInstance(returnType, ResultExprs)!; } else { Output = (T)(object)ResultExprs[0]; } } } /// /// Extension methods for BgNode types /// public static class BgNodeExtensions { /// /// 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 specification /// Tokens to add dependencies on /// The current node spec, to allow chaining calls public static T Requires(this T nodeSpec, params BgList[] tokens) where T : BgNodeSpec { foreach (BgList tokenSet in tokens) { nodeSpec.InputDependencies = nodeSpec.InputDependencies.Add(tokenSet); } return nodeSpec; } /// /// 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 specification /// Tokens to add dependencies on /// The current node spec, to allow chaining calls public static T After(this T nodeSpec, params BgList[] tokens) where T : BgNodeSpec { foreach (BgList tokenSet in tokens) { nodeSpec.AfterDependencies = nodeSpec.AfterDependencies.Add(tokenSet); } return nodeSpec; } /// /// Adds a warning when executing this node /// /// The node specification /// Condition for writing the message /// Message to output /// The current node spec, to allow chaining calls public static BgNodeSpec WarnIf(this T nodeSpec, BgBool condition, BgString message) where T : BgNodeSpec { nodeSpec.Diagnostics = nodeSpec.Diagnostics.AddIf(condition, new BgDiagnosticSpec(LogLevel.Warning, message)); return nodeSpec; } /// /// Adds an error when executing this node /// /// The node specification /// Condition for writing the message /// Message to output /// The current node spec, to allow chaining calls public static BgNodeSpec ErrorIf(this T nodeSpec, BgBool condition, BgString message) where T : BgNodeSpec { nodeSpec.Diagnostics = nodeSpec.Diagnostics.AddIf(condition, new BgDiagnosticSpec(LogLevel.Error, message)); return nodeSpec; } } }