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