// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.Core; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using OpenTracing.Util; using UnrealBuildBase; using Microsoft.Extensions.Logging; namespace UnrealBuildTool { internal static class ActionGraph { /// /// Enum describing why an Action is in conflict with another Action /// [Flags] private 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 /// Logger for output public static void CheckForConflicts(IEnumerable Actions, ILogger Logger) { 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, Logger); } else { ItemToProducingAction.Add(ProducedItem, Action); } } } if (!bResult) { throw new BuildException("Action graph is invalid; unable to continue. See log for additional details."); } } /// /// Finds conflicts between two actions, and prints them to the log /// /// The first action /// The second action /// Logger for output /// True if no conflicts were found, false otherwise. private static bool CheckForConflicts(IExternalAction A, IExternalAction B, ILogger Logger) { ActionConflictReasonFlags Reason = ActionConflictReasonFlags.None; if (A.ActionType != B.ActionType) { Reason |= ActionConflictReasonFlags.ActionType; } if (!A.PrerequisiteItems.SequenceEqual(B.PrerequisiteItems)) { Reason |= ActionConflictReasonFlags.PrerequisiteItems; } if (!A.DeleteItems.SequenceEqual(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, Logger); return false; } return true; } private 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()); } } private 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); } } private 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); } } private 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 /// Logger for output static void LogConflict(IExternalAction A, IExternalAction B, ActionConflictReasonFlags Reason, ILogger Logger) { // Convert some complex types in IExternalAction to strings when printing json JsonSerializerOptions Options = new JsonSerializerOptions { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 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); Logger.LogError("Unable to merge actions '{StatusA}' and '{StatusB}': {Reason} are different", A.StatusDescription, B.StatusDescription, Reason); Logger.LogInformation(" First Action: {AJson}", AJson); Logger.LogInformation(" Second Action: {BJson}", BJson); Logger.LogInformation(" First Action json written to '{AJsonPath}'", AJsonPath); Logger.LogInformation(" Second Action json written to '{BJsonPath}'", 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, ILogger Logger) { using (GlobalTracer.Instance.BuildSpan("ActionGraph.GetActionsToExecute()").StartActive()) { // For all targets, build a set of all actions that are outdated. Dictionary OutdatedActionDictionary = new Dictionary(); GatherAllOutdatedActions(Actions, History, OutdatedActionDictionary, CppDependencies, bIgnoreOutdatedImportLibraries, Logger); // Build a list of actions that are both needed for this target and outdated. return Actions.Where(Action => 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, ILogger Logger) { 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.Append($"The following output paths are longer than {MAX_PATH} characters. Please move the engine to a directory with a shorter path."); foreach (FileReference Path in FailPaths) { Message.Append($"\n[{Path.FullName.Length.ToString()} characters] {Path}"); } throw new BuildException(Message.ToString()); } if (WarnPaths.Count > 0) { StringBuilder Message = new StringBuilder(); Message.Append($"Detected paths more than {BuildConfiguration.MaxNestedPathLength.ToString()} characters below UE root directory. This may cause portability issues due to the {MAX_PATH.ToString()} character maximum path length on Windows:\n"); foreach (FileReference Path in WarnPaths) { string RelativePath = Path.MakeRelativeTo(Unreal.RootDirectory); Message.Append($"\n[{RelativePath.Length.ToString()} characters] {RelativePath}"); } Message.Append($"\n\nConsider setting {nameof(ModuleRules.ShortName)} = ... in module *.Build.cs files to use alternative names for intermediate paths."); Logger.LogWarning("{Message}", Message.ToString()); } } } /// /// Selects an ActionExecutor /// private static ActionExecutor SelectExecutor(BuildConfiguration BuildConfiguration, int ActionCount, List TargetDescriptors, ILogger Logger) { if (ActionCount > ParallelExecutor.GetDefaultNumParallelProcesses(Logger)) { if (BuildConfiguration.bAllowHybridExecutor && HybridExecutor.IsAvailable(Logger)) { return new HybridExecutor(TargetDescriptors, BuildConfiguration.MaxParallelActions, BuildConfiguration.bCompactOutput, Logger); } else if (BuildConfiguration.bAllowXGE && XGE.IsAvailable(Logger) && ActionCount >= XGE.MinActions) { return new XGE(); } else if (BuildConfiguration.bAllowFASTBuild && FASTBuild.IsAvailable(Logger)) { return new FASTBuild(BuildConfiguration.MaxParallelActions, BuildConfiguration.bCompactOutput, Logger); } else if (BuildConfiguration.bAllowSNDBS && SNDBS.IsAvailable(Logger)) { return new SNDBS(TargetDescriptors); } else if (BuildConfiguration.bAllowHordeCompute && HordeExecutor.IsAvailable()) { return new HordeExecutor(BuildConfiguration.MaxParallelActions, BuildConfiguration.bCompactOutput, Logger); } } return new ParallelExecutor(BuildConfiguration.MaxParallelActions, BuildConfiguration.bCompactOutput, Logger); } /// /// Executes a list of actions. /// public static void ExecuteActions(BuildConfiguration BuildConfiguration, List ActionsToExecute, List TargetDescriptors, ILogger Logger) { if (ActionsToExecute.Count == 0) { Logger.LogInformation("Target is up to date"); } else { // Figure out which executor to use ActionExecutor Executor = SelectExecutor(BuildConfiguration, ActionsToExecute.Count, TargetDescriptors, Logger); // Execute the build Stopwatch Timer = Stopwatch.StartNew(); if (!Executor.ExecuteActions(ActionsToExecute, Logger)) { throw new CompilationResultException(CompilationResult.OtherCompilationError); } Logger.LogInformation("Total time in {ExecutorName} executor: {TotalSeconds: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: {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 CyclicPrerequisite = CyclicActions[Action]; if (!CyclicPrerequisite.Contains(ProducingAction)) { CyclicPrerequisite.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 += $"Action #{ActionToIndex[Action].ToString()}: {Action.CommandPath}\n"; CycleDescription += $"\twith arguments: {Action.CommandArguments}\n"; foreach (FileItem PrerequisiteItem in Action.PrerequisiteItems) { CycleDescription += $"\tdepends on: {PrerequisiteItem.AbsolutePath}\n"; } foreach (FileItem ProducedItem in Action.ProducedItems) { CycleDescription += $"\tproduces: {ProducedItem.AbsolutePath}\n"; } CycleDescription += "\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 += $"\t\t{ActionToIndex[CyclicPrerequisiteAction].ToString()} (produces: {CyclicProducedItems[0].AbsolutePath})\n"; } else { CycleDescription += $"\t\t{ActionToIndex[CyclicPrerequisiteAction].ToString()}\n"; foreach (FileItem CyclicProducedItem in CyclicProducedItems) { CycleDescription += $"\t\t\tproduces: {CyclicProducedItem.AbsolutePath}\n"; } } } } CycleDescription += "\n"; } else { CycleDescription += "\t\tNone?? Coding error!\n"; } CycleDescription += "\n\n"; } } throw new BuildException($"Action graph contains cycle!\n\n{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(OutputItems.Contains)) { 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, without considering the full set of prerequisites. /// Writes to OutdatedActionDictionary iff the action is found to be outdated. /// Safe to run in parallel, but only with different RootActions. /// /// - The action being considered. /// - /// /// /// /// /// /// true if outdated private static void IsIndividualActionOutdated(LinkedAction RootAction, Dictionary OutdatedActionDictionary, ReaderWriterLockSlim OutdatedActionLock, ActionHistory ActionHistory, CppDependencyCache CppDependencies, bool bIgnoreOutdatedImportLibraries, ILogger Logger) { // Only compute the outdated-ness for actions that don't aren't cached in the outdated action dictionary. bool bIsOutdated = false; { // OutdatedActionDictionary may have already been populated for RootAction as part of a previously processed target OutdatedActionLock.EnterReadLock(); bool bPresent = OutdatedActionDictionary.ContainsKey(RootAction); OutdatedActionLock.ExitReadLock(); if (bPresent) { return; } } // 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 = $"{RootAction.CommandPath.FullName} {RootAction.CommandArguments} (ver {RootAction.CommandVersion})"; if (ActionHistory.UpdateProducingAttributes(ProducedItem, NewProducingAttributes, Logger) && RootAction.bUseActionHistory) { if (ProducedItem.Exists) { Logger.LogDebug("{StatusDescription}: Produced item \"{ProducedItem}\" was produced by outdated attributes.", RootAction.StatusDescription, ProducedItem.Location); Logger.LogDebug(" New attributes: {NewProducingAttributes}", 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. Logger.LogDebug("{StatusDescription}: Produced item \"{ProducedItem}\" doesn't exist.", RootAction.StatusDescription, ProducedItem.Location); bIsOutdated = true; } } // 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)) { Logger.LogDebug("{StatusDescription}: Prerequisite {PrerequisiteItem} is newer than the last execution of the action: {Message}", RootAction.StatusDescription, PrerequisiteItem.Location, $"{PrerequisiteItem.LastWriteTimeUtc.ToLocalTime().ToString(CultureInfo.CurrentCulture)} vs {LastExecutionTimeUtc.LocalDateTime.ToString(CultureInfo.CurrentCulture)}"); bIsOutdated = true; break; } } } } } // Check the dependency list if (!bIsOutdated && RootAction.DependencyListFile != null) { if (!CppDependencies.TryGetDependencies(RootAction.DependencyListFile, Logger, out List? DependencyFiles)) { Logger.LogDebug("{StatusDescription}: Missing dependency list file \"{DependencyListFile}\"", RootAction.StatusDescription, RootAction.DependencyListFile); bIsOutdated = true; } else { foreach (FileItem DependencyFile in DependencyFiles) { if (!DependencyFile.Exists || DependencyFile.LastWriteTimeUtc > LastExecutionTimeUtc) { Logger.LogDebug("{RootAction.StatusDescription}: Dependency {DependencyFile} is newer than the last execution of the action: {Message}", RootAction.StatusDescription, DependencyFile.AbsolutePath, $"{DependencyFile.LastWriteTimeUtc.ToLocalTime().ToString(CultureInfo.CurrentCulture)} vs {LastExecutionTimeUtc.LocalDateTime.ToString(CultureInfo.CurrentCulture)}"); bIsOutdated = true; break; } } } } // if the action is known to be out of date, record that fact // We don't yet know that the action is up-to-date - to determine that requires traversal of the graph of prerequisites. if (bIsOutdated) { OutdatedActionLock.EnterWriteLock(); OutdatedActionDictionary.Add(RootAction, bIsOutdated); OutdatedActionLock.ExitWriteLock(); } } /// /// Determines whether an action is outdated by examining the up-to-date state of all of its prerequisites, recursively. /// Not thread safe. Typically very fast. /// /// - The action being considered. /// - /// /// Logger instance /// true if outdated private static bool IsActionOutdatedDueToPrerequisites(LinkedAction RootAction, Dictionary OutdatedActionDictionary, bool bIgnoreOutdatedImportLibraries, ILogger Logger) { // Only compute the outdated-ness for actions that aren't already cached in the outdated action dictionary. if (OutdatedActionDictionary.TryGetValue(RootAction, out bool bIsOutdated)) { return bIsOutdated; } // Check if any of the prerequisite actions are out of date foreach (LinkedAction PrerequisiteAction in RootAction.PrerequisiteActions) { if (IsActionOutdatedDueToPrerequisites(PrerequisiteAction, OutdatedActionDictionary, bIgnoreOutdatedImportLibraries, Logger)) { // 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)) { Logger.LogDebug("{StatusDescription}: Prerequisite {PrereqStatusDescription} is produced by outdated action.", RootAction.StatusDescription, PrerequisiteAction.StatusDescription); bIsOutdated = true; break; } } } // Cache the outdated-ness of this action. 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(I => IsLibraryFile(I.Location) || !RootAction.PrerequisiteItems.Contains(I)); } 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 (IsLibraryFile(PrerequisiteItem.Location)) { foreach (LinkedAction PrerequisiteAction in RootAction.PrerequisiteActions) { if (PrerequisiteAction.bProducesImportLibrary && PrerequisiteAction.ProducedItems.Contains(PrerequisiteItem)) { return true; } } } return false; } /// /// Test to see if the given file is a library file (or in the case of linux/mac, a dynamic library) /// /// File to test /// True if the file is a library file static bool IsLibraryFile(FileReference Location) { return Location.HasExtension(".lib") || Location.HasExtension(".so") || Location.HasExtension(".dylib"); } /// /// Builds a dictionary containing the actions from AllActions that are outdated by calling /// IsActionOutdated. /// public static void GatherAllOutdatedActions(IReadOnlyList Actions, ActionHistory ActionHistory, Dictionary OutdatedActions, CppDependencyCache CppDependencies, bool bIgnoreOutdatedImportLibraries, ILogger Logger) { using (GlobalTracer.Instance.BuildSpan("Prefetching include dependencies").StartActive()) { List Dependencies = new List(); foreach (LinkedAction Action in Actions) { if (Action.DependencyListFile != null) { Dependencies.Add(Action.DependencyListFile); } } Parallel.ForEach(Dependencies, File => { CppDependencies.TryGetDependencies(File, Logger, out _); }); } using (GlobalTracer.Instance.BuildSpan("Cache individual outdated actions").StartActive()) { ReaderWriterLockSlim OutdatedActionsLock = new ReaderWriterLockSlim(); Parallel.ForEach(Actions, Action => IsIndividualActionOutdated(Action, OutdatedActions, OutdatedActionsLock, ActionHistory, CppDependencies, bIgnoreOutdatedImportLibraries, Logger)); } using (GlobalTracer.Instance.BuildSpan("Cache outdated actions based on recursive prerequisites").StartActive()) { foreach (var Action in Actions) { IsActionOutdatedDueToPrerequisites(Action, OutdatedActions, bIgnoreOutdatedImportLibraries, Logger); } } } /// /// Deletes all the items produced by actions in the provided outdated action dictionary. /// /// List of outdated actions /// Logger for output public static void DeleteOutdatedProducedItems(List OutdatedActions, ILogger Logger) { foreach (LinkedAction OutdatedAction in OutdatedActions) { foreach (FileItem DeleteItem in OutdatedAction.DeleteItems) { if (DeleteItem.Exists) { Logger.LogDebug("Deleting outdated item: {DeleteItem}", DeleteItem.Location); DeleteItem.Delete(Logger); } } } } /// /// 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(IReadOnlyList 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(); } } }