2022-08-10 16:03:37 +00:00
// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.BuildGraph ;
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text ;
using System.Threading.Tasks ;
using UnrealBuildTool ;
using AutomationTool ;
using System.Xml ;
using EpicGames.Core ;
using OpenTracing ;
namespace AutomationTool.Tasks
{
/// <summary>
/// Parameters for a compile task
/// </summary>
public class CompileTaskParameters
{
/// <summary>
/// The target to compile.
/// </summary>
[TaskParameter(Optional=true)]
public string Target ;
/// <summary>
/// The configuration to compile.
/// </summary>
[TaskParameter]
public UnrealTargetConfiguration Configuration ;
/// <summary>
/// The platform to compile for.
/// </summary>
[TaskParameter]
public UnrealTargetPlatform Platform ;
/// <summary>
/// The project to compile with.
/// </summary>
[TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.FileSpec)]
public string Project ;
/// <summary>
/// Additional arguments for UnrealBuildTool.
/// </summary>
[TaskParameter(Optional = true)]
public string Arguments ;
/// <summary>
/// Whether to allow using XGE for compilation.
/// </summary>
[TaskParameter(Optional = true)]
public bool AllowXGE = true ;
/// <summary>
2023-12-04 13:43:10 -05:00
/// No longer necessary as UnrealBuildTool is run to compile targets.
2022-08-10 16:03:37 +00:00
/// </summary>
[TaskParameter(Optional = true)]
2023-12-04 13:43:10 -05:00
[Obsolete]
2022-08-10 16:03:37 +00:00
public bool AllowParallelExecutor = true ;
2022-10-03 21:15:14 -04:00
/// <summary>
/// Whether to allow UBT to use all available cores, when AllowXGE is disabled.
/// </summary>
[TaskParameter(Optional = true)]
public bool AllowAllCores = false ;
2022-08-10 16:03:37 +00:00
/// <summary>
/// Whether to allow cleaning this target. If unspecified, targets are cleaned if the -Clean argument is passed on the command line.
/// </summary>
[TaskParameter(Optional = true)]
public bool? Clean = null ;
2024-03-25 18:31:19 -04:00
/// <summary>
/// Global flag passed to UBT that can be used to generate target files without fully compiling.
/// </summary>
[TaskParameter(Optional = true)]
public bool SkipBuild = false ;
2022-08-10 16:03:37 +00:00
/// <summary>
/// Tag to be applied to build products of this task.
/// </summary>
[TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.TagList)]
public string Tag ;
}
/// <summary>
/// Executor for compile tasks, which can compile multiple tasks simultaneously
/// </summary>
public class CompileTaskExecutor : ITaskExecutor
{
/// <summary>
/// List of targets to compile. As well as the target specifically added for this task, additional compile tasks may be merged with it.
/// </summary>
List < UnrealBuild . BuildTarget > Targets = new List < UnrealBuild . BuildTarget > ( ) ;
/// <summary>
/// Mapping of receipt filename to its corresponding tag name
/// </summary>
Dictionary < UnrealBuild . BuildTarget , string > TargetToTagName = new Dictionary < UnrealBuild . BuildTarget , string > ( ) ;
/// <summary>
/// Whether to allow using XGE for this job
/// </summary>
bool bAllowXGE = true ;
2022-10-03 21:15:14 -04:00
/// <summary>
/// Whether to allow using all available cores for this job, when bAllowXGE is false
/// </summary>
bool bAllowAllCores = false ;
2024-03-25 18:31:19 -04:00
/// <summary>
/// Should SkipBuild be passed to UBT so that only .target files are generated.
/// </summary>
bool bSkipBuild = false ;
2022-08-10 16:03:37 +00:00
/// <summary>
/// Constructor
/// </summary>
/// <param name="Task">Initial task to execute</param>
public CompileTaskExecutor ( CompileTask Task )
{
Add ( Task ) ;
}
/// <summary>
/// Adds another task to this executor
/// </summary>
/// <param name="Task">Task to add</param>
/// <returns>True if the task could be added, false otherwise</returns>
public bool Add ( BgTaskImpl Task )
{
CompileTask CompileTask = Task as CompileTask ;
if ( CompileTask = = null )
{
return false ;
}
2024-03-25 18:31:19 -04:00
CompileTaskParameters Parameters = CompileTask . Parameters ;
if ( Targets . Count > 0 )
2022-08-10 16:03:37 +00:00
{
2024-03-25 18:31:19 -04:00
if ( bAllowXGE ! = Parameters . AllowXGE
| | bSkipBuild ! = Parameters . SkipBuild )
2022-08-10 16:03:37 +00:00
{
return false ;
}
}
2024-03-25 18:31:19 -04:00
else
{
bAllowXGE = Parameters . AllowXGE ;
bAllowAllCores = Parameters . AllowAllCores ;
bSkipBuild = Parameters . SkipBuild ;
}
2022-08-10 16:03:37 +00:00
bAllowXGE & = Parameters . AllowXGE ;
2022-10-03 21:15:14 -04:00
bAllowAllCores & = Parameters . AllowAllCores ;
2022-08-10 16:03:37 +00:00
2023-01-18 08:01:18 -05:00
UnrealBuild . BuildTarget Target = new UnrealBuild . BuildTarget { TargetName = Parameters . Target , Platform = Parameters . Platform , Config = Parameters . Configuration , UprojectPath = CompileTask . FindProjectFile ( ) , UBTArgs = ( Parameters . Arguments ? ? "" ) , Clean = Parameters . Clean } ;
2022-08-10 16:03:37 +00:00
if ( ! String . IsNullOrEmpty ( Parameters . Tag ) )
{
TargetToTagName . Add ( Target , Parameters . Tag ) ;
}
Targets . Add ( Target ) ;
return true ;
}
/// <summary>
/// Execute all the tasks added to this executor.
/// </summary>
/// <param name="Job">Information about the current job</param>
/// <param name="BuildProducts">Set of build products produced by this node.</param>
/// <param name="TagNameToFileSet">Mapping from tag names to the set of files they include</param>
/// <returns>Whether the task succeeded or not. Exiting with an exception will be caught and treated as a failure.</returns>
public Task ExecuteAsync ( JobContext Job , HashSet < FileReference > BuildProducts , Dictionary < string , HashSet < FileReference > > TagNameToFileSet )
{
// Create the agenda
UnrealBuild . BuildAgenda Agenda = new UnrealBuild . BuildAgenda ( ) ;
Agenda . Targets . AddRange ( Targets ) ;
// Build everything
Dictionary < UnrealBuild . BuildTarget , BuildManifest > TargetToManifest = new Dictionary < UnrealBuild . BuildTarget , BuildManifest > ( ) ;
UnrealBuild Builder = new UnrealBuild ( Job . OwnerCommand ) ;
2024-03-25 18:31:19 -04:00
bool bAllCores = ( CommandUtils . IsBuildMachine | | bAllowAllCores ) ; // Enable using all cores if this is a build agent or the flag was passed in to the task and XGE is disabled.
Builder . Build ( Agenda , InDeleteBuildProducts : null , InUpdateVersionFiles : false , InForceNoXGE : ! bAllowXGE , InAllCores : bAllCores , InTargetToManifest : TargetToManifest , InSkipBuild : bSkipBuild ) ;
2022-08-10 16:03:37 +00:00
UnrealBuild . CheckBuildProducts ( Builder . BuildProductFiles ) ;
// Tag all the outputs
foreach ( KeyValuePair < UnrealBuild . BuildTarget , string > TargetTagName in TargetToTagName )
{
BuildManifest Manifest ;
if ( ! TargetToManifest . TryGetValue ( TargetTagName . Key , out Manifest ) )
{
throw new AutomationException ( "Missing manifest for target {0} {1} {2}" , TargetTagName . Key . TargetName , TargetTagName . Key . Platform , TargetTagName . Key . Config ) ;
}
2023-08-11 19:55:17 -04:00
HashSet < FileReference > ManifestBuildProducts = Manifest . BuildProducts . Select ( x = > new FileReference ( x ) ) . ToHashSet ( ) ;
// when we make a Mac/IOS build, Xcode will finalize the .app directory, adding files that UBT has no idea about, so now we recursively add any files in the .app
// as BuildProducts. look for any .apps that we have any files as BuildProducts, and expand to include all files in the .app
if ( BuildHostPlatform . Current . Platform = = UnrealTargetPlatform . Mac )
{
HashSet < string > AppBundleLocations = new ( ) ;
foreach ( FileReference File in ManifestBuildProducts )
{
// look for a ".app/" portion and chop off anything after it
int AppLocation = File . FullName . IndexOf ( ".app/" , StringComparison . InvariantCultureIgnoreCase ) ;
if ( AppLocation > 0 )
{
AppBundleLocations . Add ( File . FullName . Substring ( 0 , AppLocation + 4 ) ) ;
}
}
// now with a unique set of app bundles, add all files in them
foreach ( string AppBundleLocation in AppBundleLocations )
{
ManifestBuildProducts . UnionWith ( DirectoryReference . EnumerateFiles ( new DirectoryReference ( AppBundleLocation ) , "*" , System . IO . SearchOption . AllDirectories ) ) ;
}
}
foreach ( string TagName in CustomTask . SplitDelimitedList ( TargetTagName . Value ) )
2022-08-10 16:03:37 +00:00
{
HashSet < FileReference > FileSet = CustomTask . FindOrAddTagSet ( TagNameToFileSet , TagName ) ;
2023-08-11 19:55:17 -04:00
FileSet . UnionWith ( ManifestBuildProducts ) ;
2022-08-10 16:03:37 +00:00
}
}
// Add everything to the list of build products
BuildProducts . UnionWith ( Builder . BuildProductFiles . Select ( x = > new FileReference ( x ) ) ) ;
return Task . CompletedTask ;
}
2023-08-11 20:25:22 -04:00
}
2022-08-10 16:03:37 +00:00
/// <summary>
/// Compiles a target with UnrealBuildTool.
/// </summary>
[TaskElement("Compile", typeof(CompileTaskParameters))]
public class CompileTask : BgTaskImpl
{
/// <summary>
/// Parameters for this task
/// </summary>
public CompileTaskParameters Parameters ;
/// <summary>
/// Resolved path to Project file
/// </summary>
public FileReference ProjectFile = null ;
/// <summary>
/// Construct a compile task
/// </summary>
/// <param name="Parameters">Parameters for this task</param>
public CompileTask ( CompileTaskParameters Parameters )
{
this . Parameters = Parameters ;
}
/// <summary>
/// Resolve the path to the project file
/// </summary>
public FileReference FindProjectFile ( )
{
FileReference ProjectFile = null ;
// Resolve the full path to the project file
if ( ! String . IsNullOrEmpty ( Parameters . Project ) )
{
if ( Parameters . Project . EndsWith ( ".uproject" , StringComparison . OrdinalIgnoreCase ) )
{
ProjectFile = CustomTask . ResolveFile ( Parameters . Project ) ;
}
else
{
ProjectFile = NativeProjects . EnumerateProjectFiles ( Log . Logger ) . FirstOrDefault ( x = > x . GetFileNameWithoutExtension ( ) . Equals ( Parameters . Project , StringComparison . OrdinalIgnoreCase ) ) ;
}
if ( ProjectFile = = null | | ! FileReference . Exists ( ProjectFile ) )
{
throw new BuildException ( "Unable to resolve project '{0}'" , Parameters . Project ) ;
}
}
return ProjectFile ;
}
/// <summary>
/// Execute the task.
/// </summary>
/// <param name="Job">Information about the current job</param>
/// <param name="BuildProducts">Set of build products produced by this node.</param>
/// <param name="TagNameToFileSet">Mapping from tag names to the set of files they include</param>
public override Task ExecuteAsync ( JobContext Job , HashSet < FileReference > BuildProducts , Dictionary < string , HashSet < FileReference > > TagNameToFileSet )
{
//
// Don't do any logic here. You have to do it in the ctor or a getter
2023-12-04 13:43:10 -05:00
// otherwise you break the ITaskExecutor pathway, which doesn't call this function!
2022-08-10 16:03:37 +00:00
//
return GetExecutor ( ) . ExecuteAsync ( Job , BuildProducts , TagNameToFileSet ) ;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override ITaskExecutor GetExecutor ( )
{
return new CompileTaskExecutor ( this ) ;
}
/// <summary>
/// Get properties to include in tracing info
/// </summary>
/// <param name="Span">The span to add metadata to</param>
/// <param name="Prefix">Prefix for all metadata keys</param>
public override void GetTraceMetadata ( ITraceSpan Span , string Prefix )
{
base . GetTraceMetadata ( Span , Prefix ) ;
Span . AddMetadata ( Prefix + "target.name" , Parameters . Target ) ;
Span . AddMetadata ( Prefix + "target.config" , Parameters . Configuration . ToString ( ) ) ;
Span . AddMetadata ( Prefix + "target.platform" , Parameters . Platform . ToString ( ) ) ;
if ( Parameters . Project ! = null )
{
Span . AddMetadata ( Prefix + "target.project" , Parameters . Project ) ;
}
}
/// <summary>
/// Get properties to include in tracing info
/// </summary>
/// <param name="Span">The span to add metadata to</param>
/// <param name="Prefix">Prefix for all metadata keys</param>
public override void GetTraceMetadata ( ISpan Span , string Prefix )
{
base . GetTraceMetadata ( Span , Prefix ) ;
Span . SetTag ( Prefix + "target.name" , Parameters . Target ) ;
Span . SetTag ( Prefix + "target.config" , Parameters . Configuration . ToString ( ) ) ;
Span . SetTag ( Prefix + "target.platform" , Parameters . Platform . ToString ( ) ) ;
if ( Parameters . Project ! = null )
{
Span . SetTag ( Prefix + "target.project" , Parameters . Project ) ;
}
}
/// <summary>
/// Output this task out to an XML writer.
/// </summary>
public override void Write ( XmlWriter Writer )
{
Write ( Writer , Parameters ) ;
}
/// <summary>
/// Find all the tags which are used as inputs to this task
/// </summary>
/// <returns>The tag names which are read by this task</returns>
public override IEnumerable < string > FindConsumedTagNames ( )
{
yield break ;
}
/// <summary>
/// Find all the tags which are modified by this task
/// </summary>
/// <returns>The tag names which are modified by this task</returns>
public override IEnumerable < string > FindProducedTagNames ( )
{
return FindTagNamesFromList ( Parameters . Tag ) ;
}
}
public static partial class StandardTasks
{
/// <summary>
/// Compiles a target
/// </summary>
/// <param name="Target">The target to compile</param>
/// <param name="Configuration">The configuration to compile</param>
/// <param name="Platform">The platform to compile for</param>
/// <param name="Project">The project to compile with</param>
/// <param name="Arguments">Additional arguments for UnrealBuildTool</param>
/// <param name="AllowXGE">Whether to allow using XGE for compilation</param>
/// <param name="Clean">Whether to allow cleaning this target. If unspecified, targets are cleaned if the -Clean argument is passed on the command line</param>
/// <returns>Build products from the compile</returns>
2023-12-04 13:43:10 -05:00
public static async Task < FileSet > CompileAsync ( string Target , UnrealTargetPlatform Platform , UnrealTargetConfiguration Configuration , FileReference Project = null , string Arguments = null , bool AllowXGE = true , bool? Clean = null )
2022-08-10 16:03:37 +00:00
{
CompileTaskParameters Parameters = new CompileTaskParameters ( ) ;
Parameters . Target = Target ;
Parameters . Platform = Platform ;
Parameters . Configuration = Configuration ;
Parameters . Project = Project ? . FullName ;
Parameters . Arguments = Arguments ;
Parameters . AllowXGE = AllowXGE ;
Parameters . Clean = Clean ;
return await ExecuteAsync ( new CompileTask ( Parameters ) ) ;
}
}
}