// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.Serialization.Formatters.Binary; using System.Security.Cryptography; using System.Text; using EpicGames.Core; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Caches include dependency information to speed up preprocessing on subsequent runs. /// [DebuggerDisplay("{Location}")] class ActionHistoryLayer { /// /// Version number to check /// const int CurrentVersion = 2; /// /// Size of each hash value /// const int HashLength = 16; /// /// Path to store the cache data to. /// public FileReference Location { get; } /// /// The Attributes used to produce files, keyed by the absolute file paths. /// ConcurrentDictionary OutputItemToAttributeHash = new ConcurrentDictionary(); /// /// Whether the dependency cache is dirty and needs to be saved. /// bool bModified; /// /// Constructor /// /// File to store this history in public ActionHistoryLayer(FileReference Location) { this.Location = Location; if(FileReference.Exists(Location)) { Load(); } } /// /// Attempts to load this action history from disk /// void Load() { try { using(BinaryArchiveReader Reader = new BinaryArchiveReader(Location)) { int Version = Reader.ReadInt(); if(Version != CurrentVersion) { Log.TraceLog("Unable to read action history from {0}; version {1} vs current {2}", Location, Version, CurrentVersion); return; } OutputItemToAttributeHash = new ConcurrentDictionary(Reader.ReadDictionary(() => Reader.ReadFileItem(), () => Reader.ReadFixedSizeByteArray(HashLength))); } } catch(Exception Ex) { Log.TraceWarning("Unable to read {0}. See log for additional information.", Location); Log.TraceLog("{0}", ExceptionUtils.FormatExceptionDetails(Ex)); } } /// /// Saves this action history to disk /// public void Save() { if (bModified) { DirectoryReference.CreateDirectory(Location.Directory); using (BinaryArchiveWriter Writer = new BinaryArchiveWriter(Location)) { Writer.WriteInt(CurrentVersion); Writer.WriteDictionary(OutputItemToAttributeHash, Key => Writer.WriteFileItem(Key), Value => Writer.WriteFixedSizeByteArray(Value)); } bModified = false; } } /// /// Computes the case-invariant hash for a string /// /// The text to hash /// Hash of the string static byte[] ComputeHash(string Text) { string InvariantText = Text.ToUpperInvariant(); byte[] InvariantBytes = Encoding.Unicode.GetBytes(InvariantText); return new MD5CryptoServiceProvider().ComputeHash(InvariantBytes); } /// /// Compares two hashes for equality /// /// The first hash value /// The second hash value /// True if the hashes are equal static bool CompareHashes(byte[] A, byte[] B) { for(int Idx = 0; Idx < HashLength; Idx++) { if(A[Idx] != B[Idx]) { return false; } } return true; } /// /// Gets the producing attributes for the given file /// /// The output file to look for /// Receives the Attributes used to produce this file /// True if Attributes have changed and is updated, false otherwise public bool UpdateProducingCommandLine(FileItem File, string Attributes) { byte[] NewHash = ComputeHash(Attributes); for (;;) { if (OutputItemToAttributeHash.TryAdd(File, NewHash)) { // If this is a new entry we're done bModified = true; return true; } else { byte[]? OldHash; if (OutputItemToAttributeHash.TryGetValue(File, out OldHash)) { if (CompareHashes(NewHash, OldHash)) { // hashes are the same, no update needed return false; } else { // Try to update with the new value if (OutputItemToAttributeHash.TryUpdate(File, NewHash, OldHash)) { bModified = true; return true; } } } } } } /// /// Gets the location for the engine action history /// /// Target name being built /// The platform being built /// Type of the target being built /// The target architecture /// Path to the engine action history for this target public static FileReference GetEngineLocation(string TargetName, UnrealTargetPlatform Platform, TargetType TargetType, string Architecture) { string AppName; if(TargetType == TargetType.Program) { AppName = TargetName; } else { AppName = UEBuildTarget.GetAppNameForTargetType(TargetType); } return FileReference.Combine(Unreal.EngineDirectory, UEBuildTarget.GetPlatformIntermediateFolder(Platform, Architecture), AppName, "ActionHistory.bin"); } /// /// Gets the location of the project action history /// /// Path to the project file /// Platform being built /// Name of the target being built /// The target architecture /// Path to the project action history public static FileReference GetProjectLocation(FileReference ProjectFile, string TargetName, UnrealTargetPlatform Platform, string Architecture) { return FileReference.Combine(ProjectFile.Directory, UEBuildTarget.GetPlatformIntermediateFolder(Platform, Architecture), TargetName, "ActionHistory.dat"); } /// /// Enumerates all the locations of action history files for the given target /// /// Project file for the target being built /// Name of the target /// Platform being built /// The target type /// The target architecture /// Dependency cache hierarchy for the given project public static IEnumerable GetFilesToClean(FileReference ProjectFile, string TargetName, UnrealTargetPlatform Platform, TargetType TargetType, string Architecture) { if(ProjectFile == null || !Unreal.IsEngineInstalled()) { yield return GetEngineLocation(TargetName, Platform, TargetType, Architecture); } if(ProjectFile != null) { yield return GetProjectLocation(ProjectFile, TargetName, Platform, Architecture); } } } /// /// Information about actions producing artifacts under a particular directory /// [DebuggerDisplay("{BaseDir}")] class ActionHistoryPartition { /// /// The base directory for this partition /// public DirectoryReference BaseDir { get; } /// /// Used to ensure exclusive access to the layers list /// object LockObject = new object(); /// /// Map of filename to layer /// IReadOnlyList Layers = new List(); /// /// Construct a new partition /// /// The base directory for this partition public ActionHistoryPartition(DirectoryReference BaseDir) { this.BaseDir = BaseDir; } /// /// Attempt to update the producing commandline for the given file /// /// The file to update /// The new attributes /// True if the attributes were updated, false otherwise public bool UpdateProducingAttributes(FileItem File, string Attributes) { FileReference LayerLocation = GetLayerLocationForFile(File.Location); ActionHistoryLayer Layer = Layers.FirstOrDefault(x => x.Location == LayerLocation); if (Layer == null) { lock (LockObject) { Layer = Layers.FirstOrDefault(x => x.Location == LayerLocation); if(Layer == null) { Layer = new ActionHistoryLayer(LayerLocation); List NewLayers = new List(Layers); NewLayers.Add(Layer); Layers = NewLayers; } } } return Layer.UpdateProducingCommandLine(File, Attributes); } /// /// Get the path to the action history layer to use for the given file /// /// Path to the file to use /// Path to the file public FileReference GetLayerLocationForFile(FileReference Location) { int Offset = BaseDir.FullName.Length; for (; ; ) { int NameOffset = Offset + 1; // Get the next directory separator Offset = Location.FullName.IndexOf(Path.DirectorySeparatorChar, NameOffset + 1); if (Offset == -1) { break; } // Get the length of the name int NameLength = Offset - NameOffset; // Try to find Binaries// in the path if (MatchPathFragment(Location, NameOffset, NameLength, "Binaries")) { int PlatformOffset = Offset + 1; int PlatformEndOffset = Location.FullName.IndexOf(Path.DirectorySeparatorChar, PlatformOffset); if (PlatformEndOffset != -1) { string PlatformName = Location.FullName.Substring(PlatformOffset, PlatformEndOffset - PlatformOffset); return FileReference.Combine(BaseDir, "Intermediate", "Build", PlatformName, "ActionHistory.bin"); } } // Try to find /Intermediate/Build/// in the path if (MatchPathFragment(Location, NameOffset, NameLength, "Intermediate")) { int BuildOffset = Offset + 1; int BuildEndOffset = Location.FullName.IndexOf(Path.DirectorySeparatorChar, BuildOffset); if (BuildEndOffset != -1 && MatchPathFragment(Location, BuildOffset, BuildEndOffset - BuildOffset, "Build")) { // Skip the platform, target/app name, and configuration int EndOffset = BuildEndOffset; for (int Idx = 0; ; Idx++) { EndOffset = Location.FullName.IndexOf(Path.DirectorySeparatorChar, EndOffset + 1); if (EndOffset == -1) { break; } if (Idx == 2) { return FileReference.Combine(BaseDir, Location.FullName.Substring(NameOffset, EndOffset - NameOffset), "ActionHistory.bin"); } } } } } return FileReference.Combine(BaseDir, "Intermediate", "Build", "ActionHistory.bin"); } /// /// Attempts to match a substring of a path with the given fragment /// /// Path to match against /// Offset of the substring to match /// Length of the substring to match /// The path fragment /// True if the substring matches static bool MatchPathFragment(FileReference Location, int Offset, int Length, string Fragment) { return Length == Fragment.Length && String.Compare(Location.FullName, Offset, Fragment, 0, Fragment.Length, FileReference.Comparison) == 0; } /// /// Saves the modified layers /// public void Save() { foreach(ActionHistoryLayer Layer in Layers) { Layer.Save(); } } } /// /// A collection of ActionHistory layers /// class ActionHistory { /// /// The lock object for this history /// object LockObject = new object(); /// /// List of partitions /// List Partitions = new List(); /// /// Constructor /// public ActionHistory() { Partitions.Add(new ActionHistoryPartition(Unreal.EngineDirectory)); } /// /// Reads a cache from the given location, or creates it with the given settings /// /// Base directory for files that this cache should store data for /// Reference to a dependency cache with the given settings public void Mount(DirectoryReference BaseDir) { lock (LockObject) { ActionHistoryPartition Partition = Partitions.FirstOrDefault(x => x.BaseDir == BaseDir); if(Partition == null) { Partition = new ActionHistoryPartition(BaseDir); Partitions.Add(Partition); } } } /// /// Gets the producing command line for the given file /// /// The output file to look for /// Receives the Attributes used to produce this file /// True if the output item exists public bool UpdateProducingAttributes(FileItem File, string Attributes) { foreach (ActionHistoryPartition Partition in Partitions) { if (File.Location.IsUnderDirectory(Partition.BaseDir)) { return Partition.UpdateProducingAttributes(File, Attributes); } } Log.TraceWarning("File {0} is not under any action history root directory", File.Location); return false; } /// /// Saves all layers of this action history /// public void Save() { lock (LockObject) { foreach (ActionHistoryPartition Partition in Partitions) { Partition.Save(); } } } } }