2022-06-22 09:56:45 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
using System ;
using System.Collections.Generic ;
using System.ComponentModel ;
using System.Diagnostics.CodeAnalysis ;
using System.IO ;
using System.Linq ;
using System.Reflection ;
using System.Runtime.CompilerServices ;
using System.Text ;
using System.Text.RegularExpressions ;
using System.Threading.Tasks ;
using System.Xml ;
using System.Xml.Schema ;
using AutomationTool.Tasks ;
using EpicGames.BuildGraph ;
using EpicGames.BuildGraph.Expressions ;
using EpicGames.Core ;
using Microsoft.Extensions.Logging ;
using OpenTracing ;
using OpenTracing.Util ;
2022-06-24 15:18:50 -04:00
using UnrealBuildBase ;
using UnrealBuildTool ;
2022-06-22 09:56:45 -04:00
#nullable enable
namespace AutomationTool
{
/// <summary>
/// Base class for binding and executing nodes
/// </summary>
abstract class BgNodeExecutor
{
public abstract Task < bool > ExecuteAsync ( JobContext Job , Dictionary < string , HashSet < FileReference > > TagNameToFileSet ) ;
}
2022-06-24 15:18:50 -04:00
class BgBytecodeNodeExecutor : BgNodeExecutor
2022-06-22 09:56:45 -04:00
{
2022-06-24 15:18:50 -04:00
class BgContextImpl : BgContext
{
JobContext JobContext ;
2022-06-22 09:56:45 -04:00
2022-06-24 15:18:50 -04:00
public BgContextImpl ( JobContext JobContext , Dictionary < string , HashSet < FileReference > > TagNameToFileSet )
: base ( TagNameToFileSet . ToDictionary ( x = > x . Key , x = > FileSet . FromFiles ( Unreal . RootDirectory , x . Value ) ) )
{
this . JobContext = JobContext ;
}
public override string Stream = > CommandUtils . P4Enabled ? CommandUtils . P4Env . Branch : "" ;
public override int Change = > CommandUtils . P4Enabled ? CommandUtils . P4Env . Changelist : 0 ;
public override int CodeChange = > CommandUtils . P4Enabled ? CommandUtils . P4Env . CodeChangelist : 0 ;
public override ( int Major , int Minor , int Patch ) EngineVersion
{
get
{
ReadOnlyBuildVersion Current = ReadOnlyBuildVersion . Current ;
return ( Current . MajorVersion , Current . MinorVersion , Current . PatchVersion ) ;
}
}
public override bool IsBuildMachine = > CommandUtils . IsBuildMachine ;
}
2022-07-01 14:47:54 -04:00
readonly BgNodeDef Node ;
2022-06-24 15:18:50 -04:00
2022-07-01 14:47:54 -04:00
public BgBytecodeNodeExecutor ( BgNodeDef node )
2022-06-22 09:56:45 -04:00
{
Node = node ;
}
public bool Bind ( ILogger logger )
{
2022-06-24 15:18:50 -04:00
return true ;
2022-06-22 09:56:45 -04:00
}
/// <summary>
/// Execute the method given in the
/// </summary>
/// <param name="Job"></param>
/// <param name="TagNameToFileSet"></param>
/// <returns></returns>
public override async Task < bool > ExecuteAsync ( JobContext Job , Dictionary < string , HashSet < FileReference > > TagNameToFileSet )
{
2022-07-01 14:47:54 -04:00
BgThunkDef thunk = Node . Thunk ! ;
MethodInfo method = thunk . Method ;
2022-06-22 09:56:45 -04:00
HashSet < FileReference > BuildProducts = TagNameToFileSet [ Node . DefaultOutput . TagName ] ;
BgContextImpl Context = new BgContextImpl ( Job , TagNameToFileSet ) ;
ParameterInfo [ ] parameters = method . GetParameters ( ) ;
object? [ ] arguments = new object [ parameters . Length ] ;
for ( int Idx = 0 ; Idx < parameters . Length ; Idx + + )
{
Type ParameterType = parameters [ Idx ] . ParameterType ;
if ( ParameterType = = typeof ( BgContext ) )
{
arguments [ Idx ] = Context ;
}
else
{
2022-07-01 14:47:54 -04:00
arguments [ Idx ] = thunk . Arguments [ Idx ] ;
2022-06-22 09:56:45 -04:00
}
}
Task Task = ( Task ) method . Invoke ( null , arguments ) ! ;
await Task ;
if ( Node . Outputs . Count > 0 )
{
object? Result = null ;
if ( method . ReturnType . IsGenericType & & method . ReturnType . GetGenericTypeDefinition ( ) = = typeof ( Task < > ) )
{
Type TaskType = Task . GetType ( ) ;
PropertyInfo Property = TaskType . GetProperty ( nameof ( Task < int > . Result ) ) ! ;
Result = Property ! . GetValue ( Task ) ;
}
object? [ ] OutputValues ;
2022-06-24 15:18:50 -04:00
if ( Result is ITuple Tuple )
2022-06-22 09:56:45 -04:00
{
OutputValues = Enumerable . Range ( 0 , Tuple . Length ) . Select ( x = > Tuple [ x ] ) . ToArray ( ) ;
}
else
{
2022-06-24 15:18:50 -04:00
OutputValues = new [ ] { Result } ;
2022-06-22 09:56:45 -04:00
}
for ( int Idx = 0 ; Idx < OutputValues . Length ; Idx + + )
{
2022-06-24 15:18:50 -04:00
if ( OutputValues [ Idx ] is BgFileSetOutputExpr FileSet )
2022-06-22 09:56:45 -04:00
{
2022-06-24 15:18:50 -04:00
string TagName = Node . Outputs [ Idx + 1 ] . TagName ;
TagNameToFileSet [ TagName ] = new HashSet < FileReference > ( FileSet . Value . Flatten ( ) . Values ) ;
2022-06-22 09:56:45 -04:00
BuildProducts . UnionWith ( TagNameToFileSet [ TagName ] ) ;
}
}
}
return true ;
}
}
/// <summary>
2022-06-24 18:30:51 -04:00
/// Implementation of <see cref="BgNodeDef"/> for graphs defined through XML syntax
2022-06-22 09:56:45 -04:00
/// </summary>
class BgScriptNodeExecutor : BgNodeExecutor
{
/// <summary>
/// The script node
/// </summary>
public BgScriptNode Node { get ; }
/// <summary>
/// List of bound task implementations
/// </summary>
List < BgTaskImpl > _boundTasks = new List < BgTaskImpl > ( ) ;
/// <summary>
/// Constructor
/// </summary>
public BgScriptNodeExecutor ( BgScriptNode node )
{
Node = node ;
}
2022-06-22 17:14:28 -04:00
public bool Bind ( Dictionary < string , ScriptTaskBinding > nameToTask , Dictionary < string , BgNodeOutput > tagNameToNodeOutput , ILogger logger )
2022-06-22 09:56:45 -04:00
{
bool bResult = true ;
foreach ( BgTask TaskInfo in Node . Tasks )
{
2022-06-22 17:14:28 -04:00
BgTaskImpl ? boundTask = BindTask ( TaskInfo , nameToTask , tagNameToNodeOutput , logger ) ;
2022-06-22 09:56:45 -04:00
if ( boundTask = = null )
{
bResult = false ;
}
else
{
_boundTasks . Add ( boundTask ) ;
}
}
return bResult ;
}
2022-06-22 17:14:28 -04:00
BgTaskImpl ? BindTask ( BgTask TaskInfo , Dictionary < string , ScriptTaskBinding > NameToTask , IReadOnlyDictionary < string , BgNodeOutput > TagNameToNodeOutput , ILogger Logger )
2022-06-22 09:56:45 -04:00
{
// Get the reflection info for this element
ScriptTaskBinding ? Task ;
if ( ! NameToTask . TryGetValue ( TaskInfo . Name , out Task ) )
{
Logger . LogScriptError ( TaskInfo . Location , "Unknown task '{TaskName}'" , TaskInfo . Name ) ;
return null ;
}
// Check all the required parameters are present
bool bHasRequiredAttributes = true ;
foreach ( ScriptTaskParameterBinding Parameter in Task . NameToParameter . Values )
{
if ( ! Parameter . Optional & & ! TaskInfo . Arguments . ContainsKey ( Parameter . Name ) )
{
Logger . LogScriptError ( TaskInfo . Location , "Missing required attribute - {AttrName}" , Parameter . Name ) ;
bHasRequiredAttributes = false ;
}
}
// Read all the attributes into a parameters object for this task
object ParametersObject = Activator . CreateInstance ( Task . ParametersClass ) ! ;
foreach ( ( string Name , string Value ) in TaskInfo . Arguments )
{
// Get the field that this attribute should be written to in the parameters object
ScriptTaskParameterBinding ? Parameter ;
if ( ! Task . NameToParameter . TryGetValue ( Name , out Parameter ) )
{
Logger . LogScriptError ( TaskInfo . Location , "Unknown attribute '{AttrName}'" , Name ) ;
continue ;
}
// If it's a collection type, split it into separate values
2022-12-09 11:22:43 -05:00
try
2022-06-22 09:56:45 -04:00
{
2022-12-09 11:22:43 -05:00
if ( Parameter . CollectionType = = null )
2022-06-22 09:56:45 -04:00
{
2022-12-09 11:22:43 -05:00
// Parse it and assign it to the parameters object
object? FieldValue = ParseValue ( Value , Parameter . ValueType ) ;
2023-04-11 16:22:19 -04:00
if ( FieldValue ! = null )
{
Parameter . FieldInfo . SetValue ( ParametersObject , FieldValue ) ;
}
else if ( ! Parameter . Optional )
{
Logger . LogScriptError ( TaskInfo . Location , "Empty value for parameter '{AttrName}' is not allowed." , Name ) ;
}
2022-06-22 09:56:45 -04:00
}
2022-12-09 11:22:43 -05:00
else
{
// Get the collection, or create one if necessary
object? CollectionValue = Parameter . FieldInfo . GetValue ( ParametersObject ) ;
if ( CollectionValue = = null )
{
CollectionValue = Activator . CreateInstance ( Parameter . FieldInfo . FieldType ) ! ;
Parameter . FieldInfo . SetValue ( ParametersObject , CollectionValue ) ;
}
2022-06-22 09:56:45 -04:00
2022-12-09 11:22:43 -05:00
// Parse the values and add them to the collection
List < string > ValueStrings = BgTaskImpl . SplitDelimitedList ( Value ) ;
foreach ( string ValueString in ValueStrings )
{
2023-04-11 16:22:19 -04:00
object? ElementValue = ParseValue ( ValueString , Parameter . ValueType ) ;
if ( ElementValue ! = null )
{
Parameter . CollectionType . InvokeMember ( "Add" , BindingFlags . InvokeMethod | BindingFlags . Instance | BindingFlags . Public , null , CollectionValue , new object [ ] { ElementValue } ) ;
}
2022-12-09 11:22:43 -05:00
}
2022-06-22 09:56:45 -04:00
}
}
2022-12-09 11:22:43 -05:00
catch ( Exception ex )
{
Logger . LogScriptError ( TaskInfo . Location , "Unable to parse argument {Name} from {Value}" , Name , Value ) ;
Logger . LogDebug ( ex , "Exception while parsing argument {Name}" , Name ) ;
}
2022-06-22 09:56:45 -04:00
}
// Construct the task
if ( ! bHasRequiredAttributes )
{
return null ;
}
// Add it to the list
BgTaskImpl NewTask = ( BgTaskImpl ) Activator . CreateInstance ( Task . TaskClass , ParametersObject ) ! ;
// Set up the source location for diagnostics
NewTask . SourceLocation = TaskInfo . Location ;
// Make sure all the read tags are local or listed as a dependency
foreach ( string ReadTagName in NewTask . FindConsumedTagNames ( ) )
{
BgNodeOutput ? Output ;
if ( TagNameToNodeOutput . TryGetValue ( ReadTagName , out Output ) )
{
if ( Output ! = null & & Output . ProducingNode ! = Node & & ! Node . Inputs . Contains ( Output ) )
{
Logger . LogScriptError ( TaskInfo . Location , "The tag '{TagName}' is not a dependency of node '{Node}'" , ReadTagName , Node . Name ) ;
}
}
}
// Make sure all the written tags are local or listed as an output
foreach ( string ModifiedTagName in NewTask . FindProducedTagNames ( ) )
{
BgNodeOutput ? Output ;
if ( TagNameToNodeOutput . TryGetValue ( ModifiedTagName , out Output ) )
{
if ( Output ! = null & & ! Node . Outputs . Contains ( Output ) )
{
Logger . LogScriptError ( TaskInfo . Location , "The tag '{TagName}' is created by '{Node}', and cannot be modified downstream" , Output . TagName , Output . ProducingNode . Name ) ;
}
}
}
return NewTask ;
}
/// <summary>
/// Parse a value of the given type
/// </summary>
/// <param name="ValueText">The text to parse</param>
/// <param name="ValueType">Type of the value to parse</param>
/// <returns>Value that was parsed</returns>
2022-06-22 17:14:28 -04:00
static object? ParseValue ( string ValueText , Type ValueType )
2022-06-22 09:56:45 -04:00
{
// Parse it and assign it to the parameters object
if ( ValueType . IsEnum )
{
return Enum . Parse ( ValueType , ValueText ) ;
}
else if ( ValueType = = typeof ( Boolean ) )
{
2022-06-22 17:14:28 -04:00
return BgCondition . EvaluateAsync ( ValueText ) . Result ;
2022-06-22 09:56:45 -04:00
}
else if ( ValueType = = typeof ( FileReference ) )
{
2023-04-11 16:22:19 -04:00
if ( String . IsNullOrEmpty ( ValueText ) )
{
return null ;
}
else
{
return BgTaskImpl . ResolveFile ( ValueText ) ;
}
2022-06-22 09:56:45 -04:00
}
else if ( ValueType = = typeof ( DirectoryReference ) )
{
2023-04-11 16:22:19 -04:00
if ( String . IsNullOrEmpty ( ValueText ) )
{
return null ;
}
else
{
return BgTaskImpl . ResolveDirectory ( ValueText ) ;
}
2022-06-22 09:56:45 -04:00
}
TypeConverter Converter = TypeDescriptor . GetConverter ( ValueType ) ;
if ( Converter . CanConvertFrom ( typeof ( string ) ) )
{
return Converter . ConvertFromString ( ValueText ) ;
}
else
{
return Convert . ChangeType ( ValueText , ValueType ) ;
}
}
/// <summary>
/// Build all the tasks for this node
/// </summary>
/// <param name="Job">Information about the current job</param>
/// <param name="TagNameToFileSet">Mapping from tag names to the set of files they include. Should be set to contain the node inputs on entry.</param>
/// <returns>Whether the task succeeded or not. Exiting with an exception will be caught and treated as a failure.</returns>
public override async Task < bool > ExecuteAsync ( JobContext Job , Dictionary < string , HashSet < FileReference > > TagNameToFileSet )
{
// Run each of the tasks in order
HashSet < FileReference > BuildProducts = TagNameToFileSet [ Node . DefaultOutput . TagName ] ;
for ( int Idx = 0 ; Idx < _boundTasks . Count ; Idx + + )
{
using ( IScope Scope = GlobalTracer . Instance . BuildSpan ( "Task" ) . WithTag ( "resource" , _boundTasks [ Idx ] . GetTraceName ( ) ) . StartActive ( ) )
{
ITaskExecutor Executor = _boundTasks [ Idx ] . GetExecutor ( ) ;
if ( Executor = = null )
{
// Execute this task directly
try
{
_boundTasks [ Idx ] . GetTraceMetadata ( Scope . Span , "" ) ;
await _boundTasks [ Idx ] . ExecuteAsync ( Job , BuildProducts , TagNameToFileSet ) ;
}
catch ( Exception Ex )
{
ExceptionUtils . AddContext ( Ex , "while executing task {0}" , _boundTasks [ Idx ] . GetTraceString ( ) ) ;
if ( _boundTasks [ Idx ] . SourceLocation ! = null )
{
ExceptionUtils . AddContext ( Ex , "at {0}({1})" , _boundTasks [ Idx ] . SourceLocation . File , _boundTasks [ Idx ] . SourceLocation . LineNumber ) ;
}
throw ;
}
}
else
{
_boundTasks [ Idx ] . GetTraceMetadata ( Scope . Span , "1." ) ;
// 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 < Node . Tasks . Count & & Executor . Add ( _boundTasks [ Idx + 1 ] ) )
{
Idx + + ;
_boundTasks [ Idx ] . GetTraceMetadata ( Scope . Span , string . Format ( "{0}." , 1 + Idx - FirstIdx ) ) ;
}
try
{
await Executor . ExecuteAsync ( Job , BuildProducts , TagNameToFileSet ) ;
}
catch ( Exception Ex )
{
for ( int TaskIdx = FirstIdx ; TaskIdx < = Idx ; TaskIdx + + )
{
ExceptionUtils . AddContext ( Ex , "while executing {0}" , _boundTasks [ TaskIdx ] . GetTraceString ( ) ) ;
}
if ( _boundTasks [ FirstIdx ] . SourceLocation ! = null )
{
ExceptionUtils . AddContext ( Ex , "at {0}({1})" , _boundTasks [ FirstIdx ] . SourceLocation . File , _boundTasks [ FirstIdx ] . SourceLocation . LineNumber ) ;
}
throw ;
}
}
}
}
// Remove anything that doesn't exist, since these files weren't explicitly tagged
BuildProducts . RemoveWhere ( x = > ! FileReference . Exists ( x ) ) ;
return true ;
}
}
}