// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Text; using System.Threading; using System.Threading.Tasks; using EpicGames.Core; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using UnrealBuildBase; namespace UnrealBuildTool.Artifacts { /// /// Helper class for directory mapping /// internal class ArtifactDirectoryMapping : IArtifactDirectoryMapping { /// /// Creating cache /// public IActionArtifactCache? Cache; /// /// Root of the project /// public DirectoryReference? ProjectRoot; /// public string GetDirectory(ArtifactDirectoryTree tree) { switch (tree) { case ArtifactDirectoryTree.Absolute: return String.Empty; case ArtifactDirectoryTree.Engine: if (Cache == null || Cache.EngineRoot == null) { throw new ApplicationException("Attempt to get engine root when not set"); } return Cache.EngineRoot.FullName; case ArtifactDirectoryTree.Project: if (ProjectRoot == null) { throw new ApplicationException("Attempt to get project root when not set"); } return ProjectRoot.FullName; default: throw new NotImplementedException("Unexpected directory tree value"); } } /// /// Given a file, return the artifact structure for it. This routine tests to see if the /// file is under any of the well known directories. /// /// Action requesting the artifact /// File in question /// Hash value used to populate artifact /// Created artifact public ArtifactFile GetArtifact(LinkedAction action, FileItem file, IoHash hash) { if (action.ArtifactMode.HasFlag(ArtifactMode.AbsolutePath)) { return CreateArtifact(ArtifactDirectoryTree.Absolute, file.AbsolutePath, hash); } if (Cache != null && Cache.EngineRoot != null && file.Location.IsUnderDirectory(Cache.EngineRoot)) { return CreateArtifact(ArtifactDirectoryTree.Engine, file.Location.MakeRelativeTo(Cache.EngineRoot), hash); } else if (ProjectRoot != null && file.Location.IsUnderDirectory(ProjectRoot)) { return CreateArtifact(ArtifactDirectoryTree.Project, file.Location.MakeRelativeTo(ProjectRoot), hash); } else { return CreateArtifact(ArtifactDirectoryTree.Absolute, file.AbsolutePath, hash); } } /// /// Create an artifact with the given settings /// /// Directory tree /// Path to artifact /// Hash of the artifact /// The artifact private static ArtifactFile CreateArtifact(ArtifactDirectoryTree tree, string path, IoHash hash) { return new(tree, new Utf8String(path), hash); } } /// /// Generic handler for artifacts /// internal class ActionArtifactCache : IActionArtifactCache { /// public bool EnableReads { get; set; } = true; /// public bool EnableWrites { get; set; } = true; /// public bool LogCacheMisses { get; set; } = false; /// public IArtifactCache ArtifactCache { get; init; } /// public DirectoryReference? EngineRoot { get; set; } = null; /// public DirectoryReference[]? DirectoryRoots { get; set; } = null; /// /// Logging device /// private readonly ILogger _logger; /// /// Cache of dependency files. /// private readonly CppDependencyCache _cppDependencyCache; /// /// Cache for file hashes /// private readonly FileHasher _fileHasher; /// /// Directory mapper to be used for targets without projects /// private readonly ArtifactDirectoryMapping _projectlessMapper; /// /// Artifact mappers for all targets /// private readonly ConcurrentDictionary _mappings = new(); /// /// Construct a new artifact handler object /// /// Artifact cache instance /// Previously created dependency cache /// Logging device private ActionArtifactCache(IArtifactCache artifactCache, CppDependencyCache cppDependencyCache, ILogger logger) { _logger = logger; _cppDependencyCache = cppDependencyCache; ArtifactCache = artifactCache; _fileHasher = new(NullLogger.Instance); _projectlessMapper = new() { Cache = this }; } /// /// Create a new action artifact cache using horde file based storage /// /// Directory for the cache /// Previously created dependency cache /// Cache for memory mapped files /// Logging device /// Action artifact cache object public static IActionArtifactCache CreateHordeFileCache(DirectoryReference directory, CppDependencyCache cppDependencyCache, MemoryMappedFileCache memoryMappedFileCache, ILogger logger) { IArtifactCache artifactCache = HordeStorageArtifactCache.CreateFileCache(directory, memoryMappedFileCache, NullLogger.Instance, false); return new ActionArtifactCache(artifactCache, cppDependencyCache, logger); } /// /// Create a new action artifact cache using horde memory based storage /// /// Previously created dependency cache /// Logging device /// Action artifact cache object public static IActionArtifactCache CreateHordeMemoryCache(CppDependencyCache cppDependencyCache, ILogger logger) { IArtifactCache artifactCache = HordeStorageArtifactCache.CreateMemoryCache(logger); return new ActionArtifactCache(artifactCache, cppDependencyCache, logger); } /// public async Task CompleteActionFromCacheAsync(LinkedAction action, CancellationToken cancellationToken) { if (!EnableReads) { return new ActionArtifactResult(false, new List()); } if (!action.ArtifactMode.HasFlag(ArtifactMode.Enabled)) { return new ActionArtifactResult(false, new List()); } ArtifactDirectoryMapping directoryMapping = GetDirectoryMapping(action); (List inputs, _) = CollectInputs(action, false); IoHash key = await GetKeyAsync(directoryMapping, action, inputs); ArtifactAction[] artifactActions = await ArtifactCache.QueryArtifactActionsAsync(new IoHash[] { key }, cancellationToken); string actionDescription = String.Empty; if (LogCacheMisses) { actionDescription = $"{(action.CommandDescription ?? action.CommandPath.GetFileNameWithoutExtension())} {action.StatusDescription}".Trim(); } if (artifactActions.Length == 0) { if (LogCacheMisses) { _logger.LogInformation("Artifact Cache Miss: No artifact actions found for {ActionDescription}", actionDescription); } return new ActionArtifactResult(false, new List()); } foreach (ArtifactAction artifactAction in artifactActions) { bool match = true; foreach (ArtifactFile input in artifactAction.Inputs) { string name = input.GetFullPath(directoryMapping); FileItem item = FileItem.GetItemByPath(name); if (!item.Exists) { if (LogCacheMisses) { _logger.LogInformation("Artifact Cache Miss: Input file missing {ActionDescription}/{File}", actionDescription, item.FullName); } match = false; break; } if (input.ContentHash != await _fileHasher.GetDigestAsync(item, cancellationToken)) { if (LogCacheMisses) { _logger.LogInformation("Artifact Cache Miss: Content hash different {ActionDescription}/{File}", actionDescription, item.FullName); } match = false; break; } } if (match) { ArtifactAction artifactActionCopy = artifactAction; artifactActionCopy.DirectoryMapping = directoryMapping; bool[]? readResults = await ArtifactCache.QueryArtifactOutputsAsync(new[] { artifactActionCopy }, cancellationToken); if (readResults == null || readResults.Length == 0 || !readResults[0]) { return new ActionArtifactResult(false, new List()); } else { foreach (ArtifactFile output in artifactAction.Outputs) { string outputName = output.GetFullPath(directoryMapping); FileItem item = FileItem.GetItemByPath(outputName); item.ResetCachedInfo(); // newly created outputs need refreshing _fileHasher.SetDigest(item, output.ContentHash); } return new ActionArtifactResult(true, new List()); } } } return new ActionArtifactResult(false, new List()); } /// public async Task ActionCompleteAsync(LinkedAction action, CancellationToken cancellationToken) { if (!EnableWrites) { return; } if (!action.ArtifactMode.HasFlag(ArtifactMode.Enabled)) { return; } ArtifactAction artifactAction = await CreateArtifactActionAsync(action, cancellationToken); if (artifactAction.Key != IoHash.Zero) { await ArtifactCache.SaveArtifactActionsAsync(new ArtifactAction[] { (ArtifactAction)artifactAction }, cancellationToken); } return; } /// public async Task FlushChangesAsync(CancellationToken cancellationToken) { await ArtifactCache.FlushChangesAsync(cancellationToken); } /// /// Create a new artifact action that represents the input and output of the action /// /// Source action /// Cancellation token /// Artifact action private async Task CreateArtifactActionAsync(LinkedAction action, CancellationToken cancellationToken) { ArtifactDirectoryMapping directoryMapping = GetDirectoryMapping(action); (List inputs, List? dependencies) = CollectInputs(action, false); (IoHash key, IoHash actionKey) = await GetKeyAndActionKeyAsync(directoryMapping, action, inputs, dependencies); // We gather the output files first to make sure that all the generated files (including the dependency file) gets // their cached FileInfo reset. List outputs = new(); foreach (FileItem output in action.ProducedItems) { // Outputs can not be a directory. if (output.Attributes.HasFlag(System.IO.FileAttributes.Directory)) { return new(IoHash.Zero, IoHash.Zero, Array.Empty(), Array.Empty()); } IoHash hash = await _fileHasher.GetDigestAsync(output, cancellationToken); ArtifactFile artifact = directoryMapping.GetArtifact(action, output, hash); outputs.Add(artifact); } List inputArtifacts = new(); foreach (FileItem input in inputs) { IoHash hash = await _fileHasher.GetDigestAsync(input, cancellationToken); ArtifactFile artifact = directoryMapping.GetArtifact(action, input, hash); inputArtifacts.Add(artifact); } if (dependencies != null) { foreach (FileItem dependency in dependencies) { IoHash hash = await _fileHasher.GetDigestAsync(dependency, cancellationToken); ArtifactFile artifact = directoryMapping.GetArtifact(action, dependency, hash); inputArtifacts.Add(artifact); } } return new(key, actionKey, inputArtifacts.ToArray(), outputs.ToArray()) { DirectoryMapping = directoryMapping }; } /// /// Get the key has for the action /// /// Directory mapping object /// Source action /// Inputs used to construct the key /// Task returning the key private async Task GetKeyAsync(ArtifactDirectoryMapping directoryMapping, LinkedAction action, List inputs) { StringBuilder builder = new(); await AppendKeyAsync(builder, directoryMapping, action, inputs); IoHash key = IoHash.Compute(new Utf8String(builder.ToString())); return key; } /// /// Generate the key and action key hashes for the action /// /// Directory mapping object /// Source action /// Inputs used to construct the key /// Dependencies used to construct the key /// Task object with the key and action key private async Task<(IoHash, IoHash)> GetKeyAndActionKeyAsync(ArtifactDirectoryMapping directoryMapping, LinkedAction action, List inputs, List? dependencies) { StringBuilder builder = new(); await AppendKeyAsync(builder, directoryMapping, action, inputs); IoHash key = IoHash.Compute(new Utf8String(builder.ToString())); await AppendActionKeyAsync(builder, directoryMapping, action, dependencies); IoHash actionKey = IoHash.Compute(new Utf8String(builder.ToString())); return (key, actionKey); } /// /// Generate the lookup key. This key is generated from the action's inputs. /// /// Destination builder /// Directory mapping object /// Source action /// Inputs used to construct the key /// Task object private async Task AppendKeyAsync(StringBuilder builder, ArtifactDirectoryMapping directoryMapping, LinkedAction action, List inputs) { builder.AppendLine(action.CommandVersion); builder.AppendLine(action.CommandArguments); await AppendFilesAsync(builder, directoryMapping, action, inputs); } /// /// Generate the full action key. This contains the hashes for the action's inputs and the dependent files. /// /// Destination builder /// Directory mapping object /// Source action /// Dependencies used to construct the key /// Task object private async Task AppendActionKeyAsync(StringBuilder builder, ArtifactDirectoryMapping directoryMapping, LinkedAction action, List? dependencies) { await AppendFilesAsync(builder, directoryMapping, action, dependencies); } /// /// Append the file information for a given list of files /// /// Destination builder /// Directory mapping object /// Source action /// Collection of files /// Task object private async Task AppendFilesAsync(StringBuilder builder, ArtifactDirectoryMapping directoryMapping, LinkedAction action, List? files) { if (files != null) { Task[] waits = new Task[files.Count]; for (int index = 0; index < files.Count; index++) { waits[index] = _fileHasher.GetDigestAsync(files[index]); } await Task.WhenAll(waits); string[] lines = new string[files.Count]; for (int index = 0; index < files.Count; index++) { ArtifactFile artifact = directoryMapping.GetArtifact(action, files[index], await waits[index]); lines[index] = $"{GetArtifactTreeName(artifact)} {artifact.Tree} {artifact.Name} {artifact.ContentHash}"; } Array.Sort(lines, StringComparer.Ordinal); foreach (string line in lines) { builder.AppendLine(line); } } } /// /// Given an action, collect the list of inputs and dependencies. This includes any processing required /// for disabled actions that propagate their inputs to dependent actions. /// /// Action in question /// If true, collect the dependencies too /// Collection of inputs and dependencies private (List, List?) CollectInputs(LinkedAction action, bool collectDependencies) { // Search for any prerequisite action that is set to propagate inputs (i.e. PCH). Collect those // inputs and we will be inserting those inputs in our list Dictionary> substitutions = new(); foreach (LinkedAction prereq in action.PrerequisiteActions) { if (prereq.ArtifactMode.HasFlag(ArtifactMode.PropagateInputs)) { (List prereqInputs, List? prereqDependencies) = CollectInputs(prereq, collectDependencies); if (prereqDependencies != null) { prereqInputs.AddRange(prereqDependencies); } foreach (FileItem output in prereq.ProducedItems) { substitutions.TryAdd(output, prereqInputs); } } } HashSet uniques = new(); List inputs = new(); AddFileItems(uniques, substitutions, inputs, action.PrerequisiteItems); List? dependencies = null; if (collectDependencies && action.DependencyListFile != null) { if (_cppDependencyCache.TryGetDependencies(action.DependencyListFile, _logger, out List? cppDeps)) { dependencies = new(); AddFileItems(uniques, substitutions, dependencies, cppDeps); } } return (inputs, dependencies); } /// /// Add a list of file items to the collection /// /// Hash set used to detect already included file items /// Substitutions when a given input is found /// Destination list /// Source inputs private static void AddFileItems(HashSet uniques, Dictionary>? substitutions, List outputs, IEnumerable inputs) { if (substitutions != null) { foreach (FileItem input in inputs) { if (substitutions.TryGetValue(input, out List? substituteFileItems)) { AddFileItems(uniques, null, outputs, substituteFileItems); } else if (uniques.Add(input)) { outputs.Add(input); } } } else { foreach (FileItem input in inputs) { if (uniques.Add(input)) { outputs.Add(input); } } } } /// /// Return the tree name of the artifact /// /// Artifact in question /// Tree name private static string GetArtifactTreeName(ArtifactFile artifact) { switch (artifact.Tree) { case ArtifactDirectoryTree.Absolute: return "Absolute"; case ArtifactDirectoryTree.Engine: return "Engine"; case ArtifactDirectoryTree.Project: return "Project"; default: throw new NotImplementedException("Unexpected artifact directory tree type"); } } /// /// Return an artifact mapper for the given action /// /// Action in question /// Artifact mapper specific to the target's project directory private ArtifactDirectoryMapping GetDirectoryMapping(LinkedAction action) { DirectoryReference? projectDirectory = action.Target != null && action.Target.ProjectFile != null ? action.Target.ProjectFile.Directory : null; if (projectDirectory == null) { return _projectlessMapper; } return _mappings.GetOrAdd(projectDirectory, x => { return new() { Cache = this, ProjectRoot = x, }; }); } } }