// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; 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.Tasks; using EpicGames.Core; using Microsoft.Extensions.Logging; using OpenTracing.Util; using UnrealBuildBase; using UnrealBuildTool.Artifacts; 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 /// Logger for output /// Optional sorting of actions public static void Link(List Actions, ILogger Logger, bool Sort = true) { // 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, Logger); // 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 if (Sort) { 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) { HashSet Conflicted = new HashSet(); foreach (FileItem ProducedItem in Action.ProducedItems) { if (ItemToProducingAction.TryGetValue(ProducedItem, out IExternalAction? ExistingAction)) { if (!Conflicted.Contains(ExistingAction)) { bResult &= CheckForConflicts(ExistingAction, Action, Logger); Conflicted.Add(ExistingAction); } } else { ItemToProducingAction.Add(ProducedItem, Action); } } } if (!bResult) { throw new CompilationResultException(CompilationResult.ActionGraphInvalid, "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", $"{IoHash.Compute(Encoding.Default.GetBytes(AJson)).GetHashCode():X}") + ".json"; string BJsonPath = Path.Combine(Path.GetTempPath(), "UnrealBuildTool", $"{IoHash.Compute(Encoding.Default.GetBytes(BJson)).GetHashCode():X}") + ".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. ConcurrentDictionary OutdatedActionDictionary = new ConcurrentDictionary(); 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; bool ShouldFail(FileItem item) { return item.Location.FullName.Length >= MAX_PATH && !(item.Location.ContainsName("Intermediate", 0) && item.Location.ContainsName("H", 1)); // Ignore -IncludeHeader items } bool ShouldWarn(FileItem item) { if (item.Location.FullName.Length <= Unreal.RootDirectory.FullName.Length + BuildConfiguration.MaxNestedPathLength) return false; if (!item.Location.IsUnderDirectory(Unreal.RootDirectory)) return false; if (!item.Location.FullName.Contains("/Restricted/NotForLicensees/", StringComparison.OrdinalIgnoreCase)) //Be more relaxed for internal only code return false; if (item.Location.FullName.Contains("/Intermediate/", StringComparison.OrdinalIgnoreCase)) { if (item.Location.ContainsName("H", 1)) // Ignore -IncludeHeader items return false; if (item.Location.FullName.EndsWith(".i.PVS-Studio.log")) // Ignore PVS Studio intermediate items return false; } return true; } HashSet FailPaths = new(); HashSet WarnPaths = new(); foreach (IExternalAction Action in Actions) { foreach (FileItem PrerequisiteItem in Action.PrerequisiteItems) { if (ShouldFail(PrerequisiteItem)) { FailPaths.Add(PrerequisiteItem.Location); } if (ShouldWarn(PrerequisiteItem)) { WarnPaths.Add(PrerequisiteItem.Location); } } foreach (FileItem ProducedItem in Action.ProducedItems) { if (ShouldFail(ProducedItem)) { FailPaths.Add(ProducedItem.Location); } if (ShouldWarn(ProducedItem)) { WarnPaths.Add(ProducedItem.Location); } } } if (FailPaths.Count > 0) { StringBuilder Message = new StringBuilder(); Message.Append($"The following action paths are longer than {MAX_PATH} characters. Please move the engine to a directory with a shorter path."); foreach (FileReference Path in FailPaths.OrderBy(x => x.FullName)) { 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.OrderBy(x => x.FullName)) { 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()); } } } private static ActionExecutor? GetRemoteExecutorByName(string Name, BuildConfiguration BuildConfiguration, int ActionCount, int MinActionsForRemote, List TargetDescriptors, ILogger Logger) { switch (Name) { case "XGE": { if (ActionCount >= MinActionsForRemote && BuildConfiguration.bAllowXGE && XGE.IsAvailable(Logger) && ActionCount >= XGE.MinActions) { return new XGE(Logger); } return null; } case "SNDBS": { if (ActionCount >= MinActionsForRemote && BuildConfiguration.bAllowSNDBS && SNDBS.IsAvailable(Logger)) { return new SNDBS(TargetDescriptors, Logger); } return null; } case "FASTBuild": { if (ActionCount >= MinActionsForRemote && BuildConfiguration.bAllowFASTBuild && FASTBuild.IsAvailable(Logger)) { return new FASTBuild(BuildConfiguration.MaxParallelActions, BuildConfiguration.bAllCores, BuildConfiguration.bCompactOutput, Logger); } return null; } case "UBA": { // Intentionally not checking MinActionsForRemote if (BuildConfiguration.bAllowUBAExecutor && UBAExecutor.IsAvailable()) { return new UBAExecutor(BuildConfiguration.MaxParallelActions, BuildConfiguration.bAllCores, BuildConfiguration.bCompactOutput, Logger, TargetDescriptors); } return null; } default: { Logger.LogWarning("Unknown remote executor {Name}", Name); return null; } } } /// /// Selects an ActionExecutor /// private static ActionExecutor SelectExecutor(BuildConfiguration BuildConfiguration, int ActionCount, List TargetDescriptors, ILogger Logger) { int MinActionsForRemote = ParallelExecutor.GetDefaultNumParallelProcesses(BuildConfiguration.MaxParallelActions, BuildConfiguration.bAllCores, Logger); foreach (string Name in BuildConfiguration.RemoteExecutorPriority) { ActionExecutor? Executor = GetRemoteExecutorByName(Name, BuildConfiguration, ActionCount, MinActionsForRemote, TargetDescriptors, Logger); if (Executor != null) { return Executor; } } return new ParallelExecutor(BuildConfiguration.MaxParallelActions, BuildConfiguration.bAllCores, BuildConfiguration.bCompactOutput, Logger); } /// /// Executes a list of actions. /// public static async Task ExecuteActionsAsync(BuildConfiguration BuildConfiguration, List ActionsToExecute, List TargetDescriptors, ILogger Logger, IActionArtifactCache? actionArtifactCache = null) { if (ActionsToExecute.Count == 0) { Logger.LogInformation("Target is up to date"); } else { // Figure out which executor to use using ActionExecutor Executor = SelectExecutor(BuildConfiguration, ActionsToExecute.Count, TargetDescriptors, Logger); Logger.LogInformation("Using {ExecutorName} executor to run {ActionCount} action(s)", Executor.Name, ActionsToExecute.Count); // Execute the build Stopwatch Timer = Stopwatch.StartNew(); bool Result = await Executor.ExecuteActionsAsync(ActionsToExecute, Logger, actionArtifactCache); Executor.PostTelemetryEvent(); Logger.LogInformation("Total time in {ExecutorName} executor: {TotalSeconds:0.00} seconds", Executor.Name, Timer.Elapsed.TotalSeconds); if (!Result) { throw new CompilationResultException(CompilationResult.OtherCompilationError); } // 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) if (Executor.VerifyOutputs) { 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 and SortIndex if there is any foreach (LinkedAction Action in Actions) { Action.NumTotalDependentActions = 0; Action.SortIndex = 0; } // Increment all the dependencies plus propagate high priority flag Parallel.ForEach(Actions, (Action) => { Action.IncrementDependentCount(new HashSet(), Action.bIsHighPriority); }); // Sort actions by number of actions depending on them, descending. Secondary sort criteria is file size. Actions.Sort(LinkedAction.Compare); // Now when everything is sorted we want to move link actions and actions that can't run remotely to as early as possible // Find the last prereq action and set the sort index to the same. since action has at least one more dependency it will end up after in the next sorting call int I = 0; foreach (LinkedAction Action in Actions) { Action.SortIndex = ++I; if ((!Action.bCanExecuteRemotely || Action.ActionType == ActionType.Link) && Action.PrerequisiteActions.Count > 0) { int LastSortIndex = 0; foreach (LinkedAction Prereq in Action.PrerequisiteActions) { LastSortIndex = Math.Max(LastSortIndex, Prereq.SortIndex); } Action.SortIndex = LastSortIndex; } } // Sort again now when we have set sorting index and will put link actions earlier Actions.Sort(LinkedAction.Compare); } /// /// Checks for cycles in the action graph. /// static void DetectActionGraphCycles(List Actions, Dictionary ItemToProducingAction, ILogger Logger) { // 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. foreach (LinkedAction Action in Actions) { if (!ActionIsNonCyclical.ContainsKey(Action)) { string CycleDescription = ""; CycleDescription += $"Action #{ActionToIndex[Action]}: {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]} (produces: {CyclicProducedItems[0].AbsolutePath})\n"; } else { CycleDescription += $"\t\t{ActionToIndex[CyclicPrerequisiteAction]}\n"; foreach (FileItem CyclicProducedItem in CyclicProducedItems) { CycleDescription += $"\t\t\tproduces: {CyclicProducedItem.AbsolutePath}\n"; } } } } CycleDescription += "\n"; } else { CycleDescription += "\t\tNone?? Coding error!\n"; } Logger.LogInformation(CycleDescription); } } throw new CompilationResultException(CompilationResult.ActionGraphInvalid, "Action graph contains cycle!"); } } /// /// 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, ConcurrentDictionary OutdatedActionDictionary, 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 if (OutdatedActionDictionary.ContainsKey(RootAction)) { 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")))) { // Find the newer of LastWriteTime and CreationTime, as copied files (such as from a build cache) can have a newer creation time in some cases DateTime ExecutionTimeUtc = ProducedItem.LastWriteTimeUtc > ProducedItem.CreationTimeUtc ? ProducedItem.LastWriteTimeUtc : ProducedItem.CreationTimeUtc; // Use the oldest produced item's time as the last execution time. if (ExecutionTimeUtc < LastExecutionTimeUtc) { LastExecutionTimeUtc = ExecutionTimeUtc; } } 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) { // Need to check for import libraries here too if (bIgnoreOutdatedImportLibraries && IsImportLibraryDependency(RootAction, PrerequisiteItem)) { continue; } if (PrerequisiteItem.Exists) { // allow a 1 second slop for network copies TimeSpan TimeDifference = PrerequisiteItem.LastWriteTimeUtc - LastExecutionTimeUtc; bool bPrerequisiteItemIsNewerThanLastExecution = TimeDifference.TotalSeconds > 1; if (bPrerequisiteItemIsNewerThanLastExecution) { 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; } } else { // If the PrerequisiteItem doesn't exist, it means it's being produced right now and we need to update the ProducedItem that // depends on it Logger.LogDebug("{StatusDescription}: Prerequisite {PrerequisiteItem} doesn't exist.", RootAction.StatusDescription, PrerequisiteItem.Location); 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) { Logger.LogDebug("{RootAction.StatusDescription}: Dependency {DependencyFile} doesn't exist", RootAction.StatusDescription, DependencyFile.AbsolutePath); bIsOutdated = true; break; } else if (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) { OutdatedActionDictionary.TryAdd(RootAction, bIsOutdated); } } /// /// 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, ConcurrentDictionary 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.TryAdd(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, ConcurrentDictionary 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()) { Parallel.ForEach(Actions, Action => IsIndividualActionOutdated(Action, OutdatedActions, ActionHistory, CppDependencies, bIgnoreOutdatedImportLibraries, Logger)); } using (GlobalTracer.Instance.BuildSpan("Cache outdated actions based on recursive prerequisites").StartActive()) { foreach (LinkedAction 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(); } } }