/// <summary> /// Patch the action graph for hot reloading, mapping files according to the given dictionary. /// </summary> public static void PatchActionGraph(IEnumerable <Action> Actions, Dictionary <FileReference, FileReference> OriginalFileToHotReloadFile) { // Gather all of the response files for link actions. We're going to need to patch 'em up after we figure out new // names for all of the output files and import libraries List <string> ResponseFilePaths = new List <string>(); // Same as Response files but for all of the link.sh files for link actions. // Only used on BuildHostPlatform Linux List <string> LinkScriptFilePaths = new List <string>(); // Keep a map of the original file names and their new file names, so we can fix up response files after Dictionary <string, string> OriginalFileNameAndNewFileNameList_NoExtensions = new Dictionary <string, string>(); // Finally, we'll keep track of any file items that we had to create counterparts for change file names, so we can fix those up too Dictionary <FileItem, FileItem> AffectedOriginalFileItemAndNewFileItemMap = new Dictionary <FileItem, FileItem>(); foreach (Action Action in Actions.Where((Action) => Action.ActionType == ActionType.Link)) { // Assume that the first produced item (with no extension) is our output file name FileReference HotReloadFile; if (!OriginalFileToHotReloadFile.TryGetValue(Action.ProducedItems[0].Location, out HotReloadFile)) { continue; } string OriginalFileNameWithoutExtension = Utils.GetFilenameWithoutAnyExtensions(Action.ProducedItems[0].AbsolutePath); string NewFileNameWithoutExtension = Utils.GetFilenameWithoutAnyExtensions(HotReloadFile.FullName); // Find the response file in the command line. We'll need to make a copy of it with our new file name. string ResponseFileExtension = ".response"; int ResponseExtensionIndex = Action.CommandArguments.IndexOf(ResponseFileExtension, StringComparison.InvariantCultureIgnoreCase); if (ResponseExtensionIndex != -1) { int ResponseFilePathIndex = Action.CommandArguments.LastIndexOf("@\"", ResponseExtensionIndex); if (ResponseFilePathIndex == -1) { throw new BuildException("Couldn't find response file path in action's command arguments when hot reloading"); } string OriginalResponseFilePathWithoutExtension = Action.CommandArguments.Substring(ResponseFilePathIndex + 2, (ResponseExtensionIndex - ResponseFilePathIndex) - 2); string OriginalResponseFilePath = OriginalResponseFilePathWithoutExtension + ResponseFileExtension; string NewResponseFilePath = ReplaceBaseFileName(OriginalResponseFilePath, OriginalFileNameWithoutExtension, NewFileNameWithoutExtension); // Copy the old response file to the new path if (String.Compare(OriginalResponseFilePath, NewResponseFilePath, StringComparison.OrdinalIgnoreCase) != 0) { File.Copy(OriginalResponseFilePath, NewResponseFilePath, overwrite: true); } // Keep track of the new response file name. We'll have to do some edits afterwards. ResponseFilePaths.Add(NewResponseFilePath); } // Find the *.link.sh file in the command line. We'll need to make a copy of it with our new file name. // Only currently used on Linux if (UEBuildPlatform.IsPlatformInGroup(BuildHostPlatform.Current.Platform, UnrealPlatformGroup.Unix)) { string LinkScriptFileExtension = ".link.sh"; int LinkScriptExtensionIndex = Action.CommandArguments.IndexOf(LinkScriptFileExtension, StringComparison.InvariantCultureIgnoreCase); if (LinkScriptExtensionIndex != -1) { // We expect the script invocation to be quoted int LinkScriptFilePathIndex = Action.CommandArguments.LastIndexOf("\"", LinkScriptExtensionIndex); if (LinkScriptFilePathIndex == -1) { throw new BuildException("Couldn't find link script file path in action's command arguments when hot reloading. Is the path quoted?"); } string OriginalLinkScriptFilePathWithoutExtension = Action.CommandArguments.Substring(LinkScriptFilePathIndex + 1, (LinkScriptExtensionIndex - LinkScriptFilePathIndex) - 1); string OriginalLinkScriptFilePath = OriginalLinkScriptFilePathWithoutExtension + LinkScriptFileExtension; string NewLinkScriptFilePath = ReplaceBaseFileName(OriginalLinkScriptFilePath, OriginalFileNameWithoutExtension, NewFileNameWithoutExtension); // Copy the old response file to the new path File.Copy(OriginalLinkScriptFilePath, NewLinkScriptFilePath, overwrite: true); // Keep track of the new response file name. We'll have to do some edits afterwards. LinkScriptFilePaths.Add(NewLinkScriptFilePath); } // Update this action's list of prerequisite items too for (int ItemIndex = 0; ItemIndex < Action.PrerequisiteItems.Count; ++ItemIndex) { FileItem OriginalPrerequisiteItem = Action.PrerequisiteItems[ItemIndex]; string NewPrerequisiteItemFilePath = ReplaceBaseFileName(OriginalPrerequisiteItem.AbsolutePath, OriginalFileNameWithoutExtension, NewFileNameWithoutExtension); if (OriginalPrerequisiteItem.AbsolutePath != NewPrerequisiteItemFilePath) { // OK, the prerequisite item's file name changed so we'll update it to point to our new file FileItem NewPrerequisiteItem = FileItem.GetItemByPath(NewPrerequisiteItemFilePath); Action.PrerequisiteItems[ItemIndex] = NewPrerequisiteItem; // Keep track of it so we can fix up dependencies in a second pass afterwards AffectedOriginalFileItemAndNewFileItemMap.Add(OriginalPrerequisiteItem, NewPrerequisiteItem); ResponseExtensionIndex = OriginalPrerequisiteItem.AbsolutePath.IndexOf(ResponseFileExtension, StringComparison.InvariantCultureIgnoreCase); if (ResponseExtensionIndex != -1) { string OriginalResponseFilePathWithoutExtension = OriginalPrerequisiteItem.AbsolutePath.Substring(0, ResponseExtensionIndex); string OriginalResponseFilePath = OriginalResponseFilePathWithoutExtension + ResponseFileExtension; string NewResponseFilePath = ReplaceBaseFileName(OriginalResponseFilePath, OriginalFileNameWithoutExtension, NewFileNameWithoutExtension); // Copy the old response file to the new path File.Copy(OriginalResponseFilePath, NewResponseFilePath, overwrite: true); // Keep track of the new response file name. We'll have to do some edits afterwards. ResponseFilePaths.Add(NewResponseFilePath); } } } } // Update this action's list of produced items too for (int ItemIndex = 0; ItemIndex < Action.ProducedItems.Count; ++ItemIndex) { FileItem OriginalProducedItem = Action.ProducedItems[ItemIndex]; string NewProducedItemFilePath = ReplaceBaseFileName(OriginalProducedItem.AbsolutePath, OriginalFileNameWithoutExtension, NewFileNameWithoutExtension); if (OriginalProducedItem.AbsolutePath != NewProducedItemFilePath) { // OK, the produced item's file name changed so we'll update it to point to our new file FileItem NewProducedItem = FileItem.GetItemByPath(NewProducedItemFilePath); Action.ProducedItems[ItemIndex] = NewProducedItem; // Keep track of it so we can fix up dependencies in a second pass afterwards AffectedOriginalFileItemAndNewFileItemMap.Add(OriginalProducedItem, NewProducedItem); } } // Fix up the list of items to delete too for (int Idx = 0; Idx < Action.DeleteItems.Count; Idx++) { FileItem NewItem; if (AffectedOriginalFileItemAndNewFileItemMap.TryGetValue(Action.DeleteItems[Idx], out NewItem)) { Action.DeleteItems[Idx] = NewItem; } } // The status description of the item has the file name, so we'll update it too Action.StatusDescription = ReplaceBaseFileName(Action.StatusDescription, OriginalFileNameWithoutExtension, NewFileNameWithoutExtension); // Keep track of the file names, so we can fix up response files afterwards. if (!OriginalFileNameAndNewFileNameList_NoExtensions.ContainsKey(OriginalFileNameWithoutExtension)) { OriginalFileNameAndNewFileNameList_NoExtensions[OriginalFileNameWithoutExtension] = NewFileNameWithoutExtension; } else if (OriginalFileNameAndNewFileNameList_NoExtensions[OriginalFileNameWithoutExtension] != NewFileNameWithoutExtension) { throw new BuildException("Unexpected conflict in renaming files; {0} maps to {1} and {2}", OriginalFileNameWithoutExtension, OriginalFileNameAndNewFileNameList_NoExtensions[OriginalFileNameWithoutExtension], NewFileNameWithoutExtension); } } // Do another pass and update any actions that depended on the original file names that we changed foreach (Action Action in Actions) { for (int ItemIndex = 0; ItemIndex < Action.PrerequisiteItems.Count; ++ItemIndex) { FileItem OriginalFileItem = Action.PrerequisiteItems[ItemIndex]; FileItem NewFileItem; if (AffectedOriginalFileItemAndNewFileItemMap.TryGetValue(OriginalFileItem, out NewFileItem)) { // OK, looks like we need to replace this file item because we've renamed the file Action.PrerequisiteItems[ItemIndex] = NewFileItem; } } } if (OriginalFileNameAndNewFileNameList_NoExtensions.Count > 0) { // Update all the paths in link actions foreach (Action Action in Actions.Where((Action) => Action.ActionType == ActionType.Link)) { foreach (KeyValuePair <string, string> FileNameTuple in OriginalFileNameAndNewFileNameList_NoExtensions) { string OriginalFileNameWithoutExtension = FileNameTuple.Key; string NewFileNameWithoutExtension = FileNameTuple.Value; Action.CommandArguments = ReplaceBaseFileName(Action.CommandArguments, OriginalFileNameWithoutExtension, NewFileNameWithoutExtension); } } foreach (string ResponseFilePath in ResponseFilePaths) { // Load the file up string FileContents = Utils.ReadAllText(ResponseFilePath); // Replace all of the old file names with new ones foreach (KeyValuePair <string, string> FileNameTuple in OriginalFileNameAndNewFileNameList_NoExtensions) { string OriginalFileNameWithoutExtension = FileNameTuple.Key; string NewFileNameWithoutExtension = FileNameTuple.Value; FileContents = ReplaceBaseFileName(FileContents, OriginalFileNameWithoutExtension, NewFileNameWithoutExtension); } // Overwrite the original file File.WriteAllText(ResponseFilePath, FileContents, new System.Text.UTF8Encoding(false)); } if (UEBuildPlatform.IsPlatformInGroup(BuildHostPlatform.Current.Platform, UnrealPlatformGroup.Unix)) { foreach (string LinkScriptFilePath in LinkScriptFilePaths) { // Load the file up string FileContents = Utils.ReadAllText(LinkScriptFilePath); // Replace all of the old file names with new ones foreach (KeyValuePair <string, string> FileNameTuple in OriginalFileNameAndNewFileNameList_NoExtensions) { string OriginalFileNameWithoutExtension = FileNameTuple.Key; string NewFileNameWithoutExtension = FileNameTuple.Value; FileContents = ReplaceBaseFileName(FileContents, OriginalFileNameWithoutExtension, NewFileNameWithoutExtension); } // Overwrite the original file File.WriteAllText(LinkScriptFilePath, FileContents, new System.Text.UTF8Encoding(false)); } } } // Update the action that writes out the module manifests foreach (Action Action in Actions) { if (Action.ActionType == ActionType.WriteMetadata) { string Arguments = Action.CommandArguments; // Find the argument for the metadata file const string InputArgument = "-Input="; int InputIdx = Arguments.IndexOf(InputArgument); if (InputIdx == -1) { throw new Exception("Missing -Input= argument to WriteMetadata command when patching action graph."); } int FileNameIdx = InputIdx + InputArgument.Length; if (Arguments[FileNameIdx] == '\"') { FileNameIdx++; } int FileNameEndIdx = FileNameIdx; while (FileNameEndIdx < Arguments.Length && (Arguments[FileNameEndIdx] != ' ' || Arguments[FileNameIdx - 1] == '\"') && Arguments[FileNameEndIdx] != '\"') { FileNameEndIdx++; } // Read the metadata file FileReference TargetInfoFile = new FileReference(Arguments.Substring(FileNameIdx, FileNameEndIdx - FileNameIdx)); if (!FileReference.Exists(TargetInfoFile)) { throw new Exception(String.Format("Unable to find metadata file to patch action graph ({0})", TargetInfoFile)); } // Update the module names WriteMetadataTargetInfo TargetInfo = BinaryFormatterUtils.Load <WriteMetadataTargetInfo>(TargetInfoFile); foreach (KeyValuePair <FileReference, ModuleManifest> FileNameToVersionManifest in TargetInfo.FileToManifest) { KeyValuePair <string, string>[] ManifestEntries = FileNameToVersionManifest.Value.ModuleNameToFileName.ToArray(); foreach (KeyValuePair <string, string> Manifest in ManifestEntries) { FileReference OriginalFile = FileReference.Combine(FileNameToVersionManifest.Key.Directory, Manifest.Value); FileReference HotReloadFile; if (OriginalFileToHotReloadFile.TryGetValue(OriginalFile, out HotReloadFile)) { FileNameToVersionManifest.Value.ModuleNameToFileName[Manifest.Key] = HotReloadFile.GetFileName(); } } } // Write the hot-reload metadata file and update the argument list FileReference HotReloadTargetInfoFile = FileReference.Combine(TargetInfoFile.Directory, "Metadata-HotReload.dat"); BinaryFormatterUtils.SaveIfDifferent(HotReloadTargetInfoFile, TargetInfo); Action.PrerequisiteItems.RemoveAll(x => x.Location == TargetInfoFile); Action.PrerequisiteItems.Add(FileItem.GetItemByFileReference(HotReloadTargetInfoFile)); Action.CommandArguments = Arguments.Substring(0, FileNameIdx) + HotReloadTargetInfoFile + Arguments.Substring(FileNameEndIdx); } } }
/// <summary> /// Execute the command, having obtained the appropriate mutex /// </summary> /// <param name="Arguments">Command line arguments</param> /// <returns>Exit code</returns> private int ExecuteInternal(CommandLineArguments Arguments) { // Read the target info WriteMetadataTargetInfo TargetInfo = BinaryFormatterUtils.Load <WriteMetadataTargetInfo>(Arguments.GetFileReference("-Input=")); bool bNoManifestChanges = Arguments.HasOption("-NoManifestChanges"); int VersionNumber = Arguments.GetInteger("-Version="); Arguments.CheckAllArgumentsUsed(); // Make sure the version number is correct if (VersionNumber != CurrentVersionNumber) { throw new BuildException("Version number to WriteMetadataMode is incorrect (expected {0}, got {1})", CurrentVersionNumber, VersionNumber); } // Check if we need to set a build id TargetReceipt Receipt = TargetInfo.Receipt; if (String.IsNullOrEmpty(Receipt.Version.BuildId)) { // Check if there's an existing version file. If it exists, try to merge in any manifests that are valid (and reuse the existing build id) BuildVersion PreviousVersion; if (TargetInfo.VersionFile != null && BuildVersion.TryRead(TargetInfo.VersionFile, out PreviousVersion)) { // Check if we can reuse the existing manifests. This prevents unnecessary builds when switching between projects. Dictionary <FileReference, ModuleManifest> PreviousFileToManifest = new Dictionary <FileReference, ModuleManifest>(); if (TryRecyclingManifests(PreviousVersion.BuildId, TargetInfo.FileToManifest.Keys, PreviousFileToManifest)) { // Merge files from the existing manifests with the new ones foreach (KeyValuePair <FileReference, ModuleManifest> Pair in PreviousFileToManifest) { ModuleManifest TargetManifest = TargetInfo.FileToManifest[Pair.Key]; MergeManifests(Pair.Value, TargetManifest); } // Update the build id to use the current one Receipt.Version.BuildId = PreviousVersion.BuildId; } } // If the build id is still not set, generate a new one from a GUID if (String.IsNullOrEmpty(Receipt.Version.BuildId)) { Receipt.Version.BuildId = Guid.NewGuid().ToString(); } } else { // Read all the manifests and merge them into the new ones, if they have the same build id foreach (KeyValuePair <FileReference, ModuleManifest> Pair in TargetInfo.FileToManifest) { ModuleManifest SourceManifest; if (TryReadManifest(Pair.Key, out SourceManifest) && SourceManifest.BuildId == Receipt.Version.BuildId) { MergeManifests(SourceManifest, Pair.Value); } } } // Update the build id in all the manifests, and write them out foreach (KeyValuePair <FileReference, ModuleManifest> Pair in TargetInfo.FileToManifest) { FileReference ManifestFile = Pair.Key; if (!UnrealBuildTool.IsFileInstalled(ManifestFile)) { ModuleManifest Manifest = Pair.Value; Manifest.BuildId = Receipt.Version.BuildId; if (!FileReference.Exists(ManifestFile)) { // If the file doesn't already exist, just write it out DirectoryReference.CreateDirectory(ManifestFile.Directory); Manifest.Write(ManifestFile); } else { // Otherwise write it to a buffer first string OutputText; using (StringWriter Writer = new StringWriter()) { Manifest.Write(Writer); OutputText = Writer.ToString(); } // And only write it to disk if it's been modified. Note that if a manifest is out of date, we should have generated a new build id causing the contents to differ. string CurrentText = FileReference.ReadAllText(ManifestFile); if (CurrentText != OutputText) { if (bNoManifestChanges) { Log.TraceError("Build modifies {0}. This is not permitted. Before:\n {1}\nAfter:\n {2}", ManifestFile, CurrentText.Replace("\n", "\n "), OutputText.Replace("\n", "\n ")); } else { FileReference.WriteAllText(ManifestFile, OutputText); } } } } } // Write out the version file, if it's changed. Since this file is next to the executable, it may be used by multiple targets, and we should avoid modifying it unless necessary. if (TargetInfo.VersionFile != null && !UnrealBuildTool.IsFileInstalled(TargetInfo.VersionFile)) { DirectoryReference.CreateDirectory(TargetInfo.VersionFile.Directory); StringWriter Writer = new StringWriter(); Receipt.Version.Write(Writer); string Text = Writer.ToString(); if (!FileReference.Exists(TargetInfo.VersionFile) || File.ReadAllText(TargetInfo.VersionFile.FullName) != Text) { File.WriteAllText(TargetInfo.VersionFile.FullName, Text); } } // Write out the receipt if (!UnrealBuildTool.IsFileInstalled(TargetInfo.ReceiptFile)) { DirectoryReference.CreateDirectory(TargetInfo.ReceiptFile.Directory); Receipt.Write(TargetInfo.ReceiptFile); } return(0); }