// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using UnrealBuildBase;
namespace UnrealBuildTool
{
static class ActionGraph
{
///
/// Enum describing why an Action is in conflict with another Action
///
[Flags]
internal enum ActionConflictReasonFlags : byte
{
None = 0,
ActionType = 1 << 0,
PrerequisiteItems = 1 << 1,
DeleteItems = 1 << 2,
DependencyListFile = 1 << 3,
WorkingDirectory = 1 << 4,
CommandPath = 1 << 5,
CommandArguments = 1 << 6,
};
///
/// Links the actions together and sets up their dependencies
///
/// List of actions in the graph
public static void Link(List Actions)
{
// Build a map from item to its producing action
Dictionary ItemToProducingAction = new Dictionary();
foreach (LinkedAction Action in Actions)
{
foreach (FileItem ProducedItem in Action.ProducedItems)
{
ItemToProducingAction[ProducedItem] = Action;
}
}
// Check for cycles
DetectActionGraphCycles(Actions, ItemToProducingAction);
// Use this map to add all the prerequisite actions
foreach (LinkedAction Action in Actions)
{
Action.PrerequisiteActions = new HashSet();
foreach(FileItem PrerequisiteItem in Action.PrerequisiteItems)
{
if (ItemToProducingAction.TryGetValue(PrerequisiteItem, out LinkedAction? PrerequisiteAction))
{
Action.PrerequisiteActions.Add(PrerequisiteAction);
}
}
}
// Sort the action graph
SortActionList(Actions);
}
///
/// Checks a set of actions for conflicts (ie. different actions producing the same output items)
///
/// The set of actions to check
public static void CheckForConflicts(IEnumerable Actions)
{
bool bResult = true;
Dictionary ItemToProducingAction = new Dictionary();
foreach(IExternalAction Action in Actions)
{
foreach(FileItem ProducedItem in Action.ProducedItems)
{
if (ItemToProducingAction.TryGetValue(ProducedItem, out IExternalAction? ExistingAction))
{
bResult &= CheckForConflicts(ExistingAction, Action);
}
else
{
ItemToProducingAction.Add(ProducedItem, Action);
}
}
}
if(!bResult)
{
throw new BuildException("Action graph is invalid; unable to continue. See log for additional details.");
}
}
///
/// Finds conflicts betwee two actions, and prints them to the log
///
/// The first action
/// The second action
/// True if no conflicts were found, false otherwise.
public static bool CheckForConflicts(IExternalAction A, IExternalAction B)
{
ActionConflictReasonFlags Reason = ActionConflictReasonFlags.None;
if (A.ActionType != B.ActionType)
{
Reason |= ActionConflictReasonFlags.ActionType;
}
if (!Enumerable.SequenceEqual(A.PrerequisiteItems, B.PrerequisiteItems))
{
Reason |= ActionConflictReasonFlags.PrerequisiteItems;
}
if (!Enumerable.SequenceEqual(A.DeleteItems, B.DeleteItems))
{
Reason |= ActionConflictReasonFlags.DeleteItems;
}
if (A.DependencyListFile != B.DependencyListFile)
{
Reason |= ActionConflictReasonFlags.DependencyListFile;
}
if (A.WorkingDirectory != B.WorkingDirectory)
{
Reason |= ActionConflictReasonFlags.WorkingDirectory;
}
if (A.CommandPath != B.CommandPath)
{
Reason |= ActionConflictReasonFlags.CommandPath;
}
if (A.CommandArguments != B.CommandArguments)
{
Reason |= ActionConflictReasonFlags.CommandArguments;
}
if (Reason != ActionConflictReasonFlags.None)
{
LogConflict(A, B, Reason);
return false;
}
return true;
}
internal class LogActionActionTypeConverter : JsonConverter
{
public override ActionType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, ActionType value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
internal class LogActionFileItemConverter : JsonConverter
{
public override FileItem Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, FileItem value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.FullName);
}
}
internal class LogActionDirectoryReferenceConverter : JsonConverter
{
public override DirectoryReference Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, DirectoryReference value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.FullName);
}
}
internal class LogActionFileReferenceConverter : JsonConverter
{
public override FileReference Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, FileReference value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.FullName);
}
}
///
/// Adds the description of a merge error to an output message
///
/// The first action with the conflict
/// The second action with the conflict
/// Enum flags for which properties are in conflict
static void LogConflict(IExternalAction A, IExternalAction B, ActionConflictReasonFlags Reason)
{
// Convert some complex types in IExternalAction to strings when printing json
JsonSerializerOptions Options = new JsonSerializerOptions
{
WriteIndented = true,
IgnoreNullValues = true,
Converters =
{
new LogActionActionTypeConverter(),
new LogActionFileItemConverter(),
new LogActionDirectoryReferenceConverter(),
new LogActionFileReferenceConverter(),
},
};
string AJson = JsonSerializer.Serialize(A, Options);
string BJson = JsonSerializer.Serialize(B, Options);
string AJsonPath = Path.Combine(Path.GetTempPath(), "UnrealBuildTool", Path.ChangeExtension(Path.GetRandomFileName(), "json"));
string BJsonPath = Path.Combine(Path.GetTempPath(), "UnrealBuildTool", Path.ChangeExtension(Path.GetRandomFileName(), "json"));
Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "UnrealBuildTool"));
File.WriteAllText(AJsonPath, AJson);
File.WriteAllText(BJsonPath, BJson);
Log.TraceError($"Unable to merge actions '{A.StatusDescription}' and '{B.StatusDescription}': {Reason} are different");
Log.TraceInformation($" First Action: {AJson}");
Log.TraceInformation($" Second Action: {BJson}");
Log.TraceInformation($" First Action json written to '{AJsonPath}'");
Log.TraceInformation($" Second Action json written to '{BJsonPath}'");
}
///
/// Builds a list of actions that need to be executed to produce the specified output items.
///
public static List GetActionsToExecute(List Actions, CppDependencyCache CppDependencies, ActionHistory History, bool bIgnoreOutdatedImportLibraries)
{
using (Timeline.ScopeEvent("ActionGraph.GetActionsToExecute()"))
{
// For all targets, build a set of all actions that are outdated.
Dictionary OutdatedActionDictionary = new Dictionary();
GatherAllOutdatedActions(Actions, History, OutdatedActionDictionary, CppDependencies, bIgnoreOutdatedImportLibraries);
// Build a list of actions that are both needed for this target and outdated.
return Actions.Where(Action => Action.CommandPath != null && OutdatedActionDictionary[Action]).ToList();
}
}
///
/// Checks that there aren't any intermediate files longer than the max allowed path length
///
/// The build configuration
/// List of actions in the graph
public static void CheckPathLengths(BuildConfiguration BuildConfiguration, IEnumerable Actions)
{
if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64)
{
const int MAX_PATH = 260;
List FailPaths = new List();
List WarnPaths = new List();
foreach (IExternalAction Action in Actions)
{
foreach (FileItem PrerequisiteItem in Action.PrerequisiteItems)
{
if (PrerequisiteItem.Location.FullName.Length >= MAX_PATH)
{
FailPaths.Add(PrerequisiteItem.Location);
}
}
foreach (FileItem ProducedItem in Action.ProducedItems)
{
if (ProducedItem.Location.FullName.Length >= MAX_PATH)
{
FailPaths.Add(ProducedItem.Location);
}
if (ProducedItem.Location.FullName.Length > Unreal.RootDirectory.FullName.Length + BuildConfiguration.MaxNestedPathLength && ProducedItem.Location.IsUnderDirectory(Unreal.RootDirectory))
{
WarnPaths.Add(ProducedItem.Location);
}
}
}
if (FailPaths.Count > 0)
{
StringBuilder Message = new StringBuilder();
Message.AppendFormat("The following output paths are longer than {0} characters. Please move the engine to a directory with a shorter path.", MAX_PATH);
foreach (FileReference Path in FailPaths)
{
Message.AppendFormat("\n[{0} characters] {1}", Path.FullName.Length, Path);
}
throw new BuildException(Message.ToString());
}
if (WarnPaths.Count > 0)
{
StringBuilder Message = new StringBuilder();
Message.AppendFormat("Detected paths more than {0} characters below UE root directory. This may cause portability issues due to the {1} character maximum path length on Windows:\n", BuildConfiguration.MaxNestedPathLength, MAX_PATH);
foreach (FileReference Path in WarnPaths)
{
string RelativePath = Path.MakeRelativeTo(Unreal.RootDirectory);
Message.AppendFormat("\n[{0} characters] {1}", RelativePath.Length, RelativePath);
}
Message.AppendFormat("\n\nConsider setting {0} = ... in module *.Build.cs files to use alternative names for intermediate paths.", nameof(ModuleRules.ShortName));
Log.TraceWarning(Message.ToString());
}
}
}
///
/// Executes a list of actions.
///
public static void ExecuteActions(BuildConfiguration BuildConfiguration, List ActionsToExecute)
{
if(ActionsToExecute.Count == 0)
{
Log.TraceInformation("Target is up to date");
}
else
{
// Figure out which executor to use
ActionExecutor Executor;
if (BuildConfiguration.bAllowHybridExecutor && HybridExecutor.IsAvailable())
{
Executor = new HybridExecutor(BuildConfiguration.MaxParallelActions);
}
else if (BuildConfiguration.bAllowXGE && XGE.IsAvailable())
{
Executor = new XGE();
}
else if (BuildConfiguration.bAllowFASTBuild && FASTBuild.IsAvailable())
{
Executor = new FASTBuild(BuildConfiguration.MaxParallelActions);
}
else if(BuildConfiguration.bAllowSNDBS && SNDBS.IsAvailable())
{
Executor = new SNDBS();
}
else if (BuildConfiguration.bAllowTaskExecutor && TaskExecutor.IsAvailable())
{
Executor = new TaskExecutor(BuildConfiguration.MaxParallelActions);
}
else
{
Executor = new ParallelExecutor(BuildConfiguration.MaxParallelActions);
}
// Execute the build
Stopwatch Timer = Stopwatch.StartNew();
if(!Executor.ExecuteActions(ActionsToExecute))
{
throw new CompilationResultException(CompilationResult.OtherCompilationError);
}
Log.TraceInformation("Total time in {0} executor: {1:0.00} seconds", Executor.Name, Timer.Elapsed.TotalSeconds);
// Reset the file info for all the produced items
foreach (LinkedAction BuildAction in ActionsToExecute)
{
foreach(FileItem ProducedItem in BuildAction.ProducedItems)
{
ProducedItem.ResetCachedInfo();
}
}
// Verify the link outputs were created (seems to happen with Win64 compiles)
foreach (LinkedAction BuildAction in ActionsToExecute)
{
if (BuildAction.ActionType == ActionType.Link)
{
foreach (FileItem Item in BuildAction.ProducedItems)
{
if(!Item.Exists)
{
throw new BuildException("Failed to produce item: {0}", Item.AbsolutePath);
}
}
}
}
}
}
///
/// Sorts the action list for improved parallelism with local execution.
///
static void SortActionList(List Actions)
{
// Clear the current dependent count
foreach(LinkedAction Action in Actions)
{
Action.NumTotalDependentActions = 0;
}
// Increment all the dependencies
foreach(LinkedAction Action in Actions)
{
Action.IncrementDependentCount(new HashSet());
}
// Sort actions by number of actions depending on them, descending. Secondary sort criteria is file size.
Actions.Sort(LinkedAction.Compare);
}
///
/// Checks for cycles in the action graph.
///
static void DetectActionGraphCycles(List Actions, Dictionary ItemToProducingAction)
{
// Starting with actions that only depend on non-produced items, iteratively expand a set of actions that are only dependent on
// non-cyclical actions.
Dictionary ActionIsNonCyclical = new Dictionary();
Dictionary> CyclicActions = new Dictionary>();
while (true)
{
bool bFoundNewNonCyclicalAction = false;
foreach (LinkedAction Action in Actions)
{
if (!ActionIsNonCyclical.ContainsKey(Action))
{
// Determine if the action depends on only actions that are already known to be non-cyclical.
bool bActionOnlyDependsOnNonCyclicalActions = true;
foreach (FileItem PrerequisiteItem in Action.PrerequisiteItems)
{
if (ItemToProducingAction.TryGetValue(PrerequisiteItem, out LinkedAction? ProducingAction))
{
if (!ActionIsNonCyclical.ContainsKey(ProducingAction))
{
bActionOnlyDependsOnNonCyclicalActions = false;
if (!CyclicActions.ContainsKey(Action))
{
CyclicActions.Add(Action, new List());
}
List CyclicPrereq = CyclicActions[Action];
if (!CyclicPrereq.Contains(ProducingAction))
{
CyclicPrereq.Add(ProducingAction);
}
}
}
}
// If the action only depends on known non-cyclical actions, then add it to the set of known non-cyclical actions.
if (bActionOnlyDependsOnNonCyclicalActions)
{
ActionIsNonCyclical.Add(Action, true);
bFoundNewNonCyclicalAction = true;
if (CyclicActions.ContainsKey(Action))
{
CyclicActions.Remove(Action);
}
}
}
}
// If this iteration has visited all actions without finding a new non-cyclical action, then all non-cyclical actions have
// been found.
if (!bFoundNewNonCyclicalAction)
{
break;
}
}
// If there are any cyclical actions, throw an exception.
if (ActionIsNonCyclical.Count < Actions.Count)
{
// Find the index of each action
Dictionary ActionToIndex = new Dictionary();
for(int Idx = 0; Idx < Actions.Count; Idx++)
{
ActionToIndex[Actions[Idx]] = Idx;
}
// Describe the cyclical actions.
string CycleDescription = "";
foreach (LinkedAction Action in Actions)
{
if (!ActionIsNonCyclical.ContainsKey(Action))
{
CycleDescription += string.Format("Action #{0}: {1}\n", ActionToIndex[Action], Action.CommandPath);
CycleDescription += string.Format("\twith arguments: {0}\n", Action.CommandArguments);
foreach (FileItem PrerequisiteItem in Action.PrerequisiteItems)
{
CycleDescription += string.Format("\tdepends on: {0}\n", PrerequisiteItem.AbsolutePath);
}
foreach (FileItem ProducedItem in Action.ProducedItems)
{
CycleDescription += string.Format("\tproduces: {0}\n", ProducedItem.AbsolutePath);
}
CycleDescription += string.Format("\tDepends on cyclic actions:\n");
if (CyclicActions.ContainsKey(Action))
{
foreach (LinkedAction CyclicPrerequisiteAction in CyclicActions[Action])
{
if (CyclicActions.ContainsKey(CyclicPrerequisiteAction))
{
List CyclicProducedItems = CyclicPrerequisiteAction.ProducedItems.ToList();
if (CyclicProducedItems.Count == 1)
{
CycleDescription += string.Format("\t\t{0} (produces: {1})\n", ActionToIndex[CyclicPrerequisiteAction], CyclicProducedItems[0].AbsolutePath);
}
else
{
CycleDescription += string.Format("\t\t{0}\n", ActionToIndex[CyclicPrerequisiteAction]);
foreach (FileItem CyclicProducedItem in CyclicProducedItems)
{
CycleDescription += string.Format("\t\t\tproduces: {0}\n", CyclicProducedItem.AbsolutePath);
}
}
}
}
CycleDescription += "\n";
}
else
{
CycleDescription += string.Format("\t\tNone?? Coding error!\n");
}
CycleDescription += "\n\n";
}
}
throw new BuildException("Action graph contains cycle!\n\n{0}", CycleDescription);
}
}
///
/// Determines the full set of actions that must be built to produce an item.
///
/// All the actions in the graph
/// Set of output items to be built
/// Set of prerequisite actions
public static List GatherPrerequisiteActions(List Actions, HashSet OutputItems)
{
HashSet PrerequisiteActions = new HashSet();
foreach(LinkedAction Action in Actions)
{
if(Action.ProducedItems.Any(x => OutputItems.Contains(x)))
{
GatherPrerequisiteActions(Action, PrerequisiteActions);
}
}
return PrerequisiteActions.ToList();
}
///
/// Determines the full set of actions that must be built to produce an item.
///
/// The root action to scan
/// Set of prerequisite actions
private static void GatherPrerequisiteActions(LinkedAction Action, HashSet PrerequisiteActions)
{
if(PrerequisiteActions.Add(Action))
{
foreach(LinkedAction PrerequisiteAction in Action.PrerequisiteActions)
{
GatherPrerequisiteActions(PrerequisiteAction, PrerequisiteActions);
}
}
}
///
/// Determines whether an action is outdated based on the modification times for its prerequisite
/// and produced items.
///
/// - The action being considered.
/// -
///
///
///
/// true if outdated
public static bool IsActionOutdated(LinkedAction RootAction, Dictionary OutdatedActionDictionary, ActionHistory ActionHistory, CppDependencyCache CppDependencies, bool bIgnoreOutdatedImportLibraries)
{
// Only compute the outdated-ness for actions that don't aren't cached in the outdated action dictionary.
bool bIsOutdated = false;
lock(OutdatedActionDictionary)
{
if (OutdatedActionDictionary.TryGetValue(RootAction, out bIsOutdated))
{
return bIsOutdated;
}
}
// Determine the last time the action was run based on the write times of its produced files.
DateTimeOffset LastExecutionTimeUtc = DateTimeOffset.MaxValue;
foreach (FileItem ProducedItem in RootAction.ProducedItems)
{
// Check if the command-line of the action previously used to produce the item is outdated.
string NewProducingAttributes = string.Format("{0} {1} (ver {2})", RootAction.CommandPath.FullName, RootAction.CommandArguments, RootAction.CommandVersion);
if (ActionHistory.UpdateProducingAttributes(ProducedItem, NewProducingAttributes))
{
if(ProducedItem.Exists)
{
Log.TraceLog(
"{0}: Produced item \"{1}\" was produced by outdated attributes.\n New attributes: {2}",
RootAction.StatusDescription,
Path.GetFileName(ProducedItem.AbsolutePath),
NewProducingAttributes
);
}
bIsOutdated = true;
}
// If the produced file doesn't exist or has zero size, consider it outdated. The zero size check is to detect cases
// where aborting an earlier compile produced invalid zero-sized obj files, but that may cause actions where that's
// legitimate output to always be considered outdated.
if (ProducedItem.Exists && (RootAction.ActionType != ActionType.Compile || ProducedItem.Length > 0 || (!ProducedItem.Location.HasExtension(".obj") && !ProducedItem.Location.HasExtension(".o"))))
{
// Use the oldest produced item's time as the last execution time.
if (ProducedItem.LastWriteTimeUtc < LastExecutionTimeUtc)
{
LastExecutionTimeUtc = ProducedItem.LastWriteTimeUtc;
}
}
else
{
// If any of the produced items doesn't exist, the action is outdated.
Log.TraceLog(
"{0}: Produced item \"{1}\" doesn't exist.",
RootAction.StatusDescription,
Path.GetFileName(ProducedItem.AbsolutePath)
);
bIsOutdated = true;
}
}
// Check if any of the prerequisite actions are out of date
if (!bIsOutdated)
{
foreach (LinkedAction PrerequisiteAction in RootAction.PrerequisiteActions)
{
if (IsActionOutdated(PrerequisiteAction, OutdatedActionDictionary, ActionHistory, CppDependencies, bIgnoreOutdatedImportLibraries))
{
// Only check for outdated import libraries if we were configured to do so. Often, a changed import library
// won't affect a dependency unless a public header file was also changed, in which case we would be forced
// to recompile anyway. This just allows for faster iteration when working on a subsystem in a DLL, as we
// won't have to wait for dependent targets to be relinked after each change.
if(!bIgnoreOutdatedImportLibraries || !IsImportLibraryDependency(RootAction, PrerequisiteAction))
{
Log.TraceLog("{0}: Prerequisite {1} is produced by outdated action.", RootAction.StatusDescription, PrerequisiteAction.StatusDescription);
bIsOutdated = true;
break;
}
}
}
}
// Check if any prerequisite item has a newer timestamp than the last execution time of this action
if(!bIsOutdated)
{
foreach (FileItem PrerequisiteItem in RootAction.PrerequisiteItems)
{
if (PrerequisiteItem.Exists)
{
// allow a 1 second slop for network copies
TimeSpan TimeDifference = PrerequisiteItem.LastWriteTimeUtc - LastExecutionTimeUtc;
bool bPrerequisiteItemIsNewerThanLastExecution = TimeDifference.TotalSeconds > 1;
if (bPrerequisiteItemIsNewerThanLastExecution)
{
// Need to check for import libraries here too
if(!bIgnoreOutdatedImportLibraries || !IsImportLibraryDependency(RootAction, PrerequisiteItem))
{
Log.TraceLog("{0}: Prerequisite {1} is newer than the last execution of the action: {2} vs {3}", RootAction.StatusDescription, Path.GetFileName(PrerequisiteItem.AbsolutePath), PrerequisiteItem.LastWriteTimeUtc.ToLocalTime(), LastExecutionTimeUtc.LocalDateTime);
bIsOutdated = true;
break;
}
}
}
}
}
// Check the dependency list
if(!bIsOutdated && RootAction.DependencyListFile != null)
{
if (!CppDependencies.TryGetDependencies(RootAction.DependencyListFile, out List? DependencyFiles))
{
Log.TraceLog("{0}: Missing dependency list file \"{1}\"", RootAction.StatusDescription, RootAction.DependencyListFile);
bIsOutdated = true;
}
else
{
foreach (FileItem DependencyFile in DependencyFiles)
{
if (!DependencyFile.Exists || DependencyFile.LastWriteTimeUtc > LastExecutionTimeUtc)
{
Log.TraceLog(
"{0}: Dependency {1} is newer than the last execution of the action: {2} vs {3}",
RootAction.StatusDescription,
Path.GetFileName(DependencyFile.AbsolutePath),
DependencyFile.LastWriteTimeUtc.ToLocalTime(),
LastExecutionTimeUtc.LocalDateTime
);
bIsOutdated = true;
break;
}
}
}
}
// Cache the outdated-ness of this action.
lock(OutdatedActionDictionary)
{
if(!OutdatedActionDictionary.ContainsKey(RootAction))
{
OutdatedActionDictionary.Add(RootAction, bIsOutdated);
}
}
return bIsOutdated;
}
///
/// Determines if the dependency between two actions is only for an import library
///
/// The action to check
/// The action that it depends on
/// True if the only dependency between two actions is for an import library
static bool IsImportLibraryDependency(LinkedAction RootAction, LinkedAction PrerequisiteAction)
{
if(PrerequisiteAction.bProducesImportLibrary)
{
return PrerequisiteAction.ProducedItems.All(x => x.Location.HasExtension(".lib") || !RootAction.PrerequisiteItems.Contains(x));
}
else
{
return false;
}
}
///
/// Determines if the dependency on a between two actions is only for an import library
///
/// The action to check
/// The dependency that is out of date
/// True if the only dependency between two actions is for an import library
static bool IsImportLibraryDependency(LinkedAction RootAction, FileItem PrerequisiteItem)
{
if(PrerequisiteItem.Location.HasExtension(".lib"))
{
foreach(LinkedAction PrerequisiteAction in RootAction.PrerequisiteActions)
{
if(PrerequisiteAction.bProducesImportLibrary && PrerequisiteAction.ProducedItems.Contains(PrerequisiteItem))
{
return true;
}
}
}
return false;
}
///
/// Builds a dictionary containing the actions from AllActions that are outdated by calling
/// IsActionOutdated.
///
public static void GatherAllOutdatedActions(IEnumerable Actions, ActionHistory ActionHistory, Dictionary OutdatedActions, CppDependencyCache CppDependencies, bool bIgnoreOutdatedImportLibraries)
{
using(Timeline.ScopeEvent("Prefetching include dependencies"))
{
List Dependencies = new List();
foreach(LinkedAction Action in Actions)
{
if(Action.DependencyListFile != null)
{
Dependencies.Add(Action.DependencyListFile);
}
}
Parallel.ForEach(Dependencies, File => { CppDependencies.TryGetDependencies(File, out _); });
}
using(Timeline.ScopeEvent("Cache outdated actions"))
{
Parallel.ForEach(Actions, Action => IsActionOutdated(Action, OutdatedActions, ActionHistory, CppDependencies, bIgnoreOutdatedImportLibraries));
}
}
///
/// Deletes all the items produced by actions in the provided outdated action dictionary.
///
/// List of outdated actions
public static void DeleteOutdatedProducedItems(List OutdatedActions)
{
foreach(LinkedAction OutdatedAction in OutdatedActions)
{
foreach (FileItem DeleteItem in OutdatedAction.DeleteItems)
{
if (DeleteItem.Exists)
{
Log.TraceLog("Deleting outdated item: {0}", DeleteItem.AbsolutePath);
DeleteItem.Delete();
}
}
}
}
///
/// Creates directories for all the items produced by actions in the provided outdated action
/// dictionary.
///
public static void CreateDirectoriesForProducedItems(List OutdatedActions)
{
HashSet OutputDirectories = new HashSet();
foreach(LinkedAction OutdatedAction in OutdatedActions)
{
foreach(FileItem ProducedItem in OutdatedAction.ProducedItems)
{
OutputDirectories.Add(ProducedItem.Location.Directory);
}
}
foreach(DirectoryReference OutputDirectory in OutputDirectories)
{
if(!DirectoryReference.Exists(OutputDirectory))
{
DirectoryReference.CreateDirectory(OutputDirectory);
}
}
}
///
/// Imports an action graph from a JSON file
///
/// The file to read from
/// List of actions
public static List ImportJson(FileReference InputFile)
{
JsonObject Object = JsonObject.Read(InputFile);
JsonObject EnvironmentObject = Object.GetObjectField("Environment");
foreach(string KeyName in EnvironmentObject.KeyNames)
{
Environment.SetEnvironmentVariable(KeyName, EnvironmentObject.GetStringField(KeyName));
}
List Actions = new List();
foreach (JsonObject ActionObject in Object.GetObjectArrayField("Actions"))
{
Actions.Add(Action.ImportJson(ActionObject));
}
return Actions;
}
///
/// Exports an action graph to a JSON file
///
/// The actions to write
/// Output file to write the actions to
public static void ExportJson(IEnumerable Actions, FileReference OutputFile)
{
DirectoryReference.CreateDirectory(OutputFile.Directory);
using JsonWriter Writer = new JsonWriter(OutputFile);
Writer.WriteObjectStart();
Writer.WriteObjectStart("Environment");
foreach (object? Object in Environment.GetEnvironmentVariables())
{
System.Collections.DictionaryEntry Pair = (System.Collections.DictionaryEntry)Object!;
if (!UnrealBuildTool.InitialEnvironment!.Contains(Pair.Key) || (string)(UnrealBuildTool.InitialEnvironment![Pair.Key]!) != (string)(Pair.Value!))
{
Writer.WriteValue((string)Pair!.Key, (string)Pair.Value!);
}
}
Writer.WriteObjectEnd();
Dictionary ActionToId = new Dictionary();
foreach (LinkedAction Action in Actions)
{
ActionToId[Action] = ActionToId.Count;
}
Writer.WriteArrayStart("Actions");
foreach (LinkedAction Action in Actions)
{
Writer.WriteObjectStart();
Action.ExportJson(ActionToId, Writer);
Writer.WriteObjectEnd();
}
Writer.WriteArrayEnd();
Writer.WriteObjectEnd();
}
}
}