// 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(); } } }