// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Xml; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; namespace AutomationTool.Tasks { /// /// Parameters for a copy task /// public class CopyTaskParameters { /// /// Optional filter to be applied to the list of input files. /// [TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.FileSpec)] public string Files { get; set; } /// /// The pattern(s) to copy from (for example, Engine/*.txt). /// [TaskParameter(ValidationType = TaskParameterValidationType.FileSpec)] public string From { get; set; } /// /// The directory to copy to. /// [TaskParameter(ValidationType = TaskParameterValidationType.FileSpec)] public string To { get; set; } /// /// Optional pattern(s) to exclude from the copy for example, Engine/NoCopy*.txt) /// [TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.FileSpec)] public string Exclude { get; set; } /// /// Whether or not to overwrite existing files. /// [TaskParameter(Optional = true)] public bool Overwrite { get; set; } = true; /// /// Tag to be applied to build products of this task. /// [TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.TagList)] public string Tag { get; set; } /// /// Whether or not to throw an error if no files were found to copy /// [TaskParameter(Optional = true)] public bool ErrorIfNotFound { get; set; } = false; } /// /// Copies files from one directory to another. /// [TaskElement("Copy", typeof(CopyTaskParameters))] public class CopyTask : BgTaskImpl { readonly CopyTaskParameters _parameters; /// /// Constructor /// /// Parameters for this task public CopyTask(CopyTaskParameters parameters) { _parameters = parameters; } /// /// ExecuteAsync the task. /// /// Information about the current job /// Set of build products produced by this node. /// Mapping from tag names to the set of files they include public override async Task ExecuteAsync(JobContext job, HashSet buildProducts, Dictionary> tagNameToFileSet) { // Parse all the source patterns FilePattern sourcePattern = new FilePattern(Unreal.RootDirectory, _parameters.From); // Parse the target pattern FilePattern targetPattern = new FilePattern(Unreal.RootDirectory, _parameters.To); // Apply the filter to the source files HashSet files = null; if (!String.IsNullOrEmpty(_parameters.Files)) { sourcePattern = sourcePattern.AsDirectoryPattern(); files = ResolveFilespec(sourcePattern.BaseDirectory, _parameters.Files, tagNameToFileSet); } try { // Build the file mapping Dictionary targetFileToSourceFile = FilePattern.CreateMapping(files, ref sourcePattern, ref targetPattern); if (!String.IsNullOrEmpty(_parameters.Exclude)) { FileFilter excludeFilter = new FileFilter(); foreach (string excludeRule in SplitDelimitedList(_parameters.Exclude)) { // use "Include" because we want to match against the files we will explicitly exclude later excludeFilter.Include(excludeRule); } List excludedFiles = new List(); foreach (KeyValuePair filePair in targetFileToSourceFile) { if (excludeFilter.Matches(filePair.Value.ToString())) { excludedFiles.Add(filePair.Key); } } foreach (FileReference excludedFile in excludedFiles) { targetFileToSourceFile.Remove(excludedFile); } } // Check we got some files if (targetFileToSourceFile.Count == 0) { if (_parameters.ErrorIfNotFound) { throw new AutomationException("No files found matching '{0}'", sourcePattern); } else { Logger.LogInformation("No files found matching '{SourcePattern}'", sourcePattern); } return; } // Run the copy Logger.LogInformation("Copying {Arg0} file{Arg1} from {Arg2} to {Arg3}...", targetFileToSourceFile.Count, (targetFileToSourceFile.Count == 1) ? "" : "s", sourcePattern.BaseDirectory, targetPattern.BaseDirectory); await ExecuteAsync(targetFileToSourceFile, _parameters.Overwrite); // Update the list of build products buildProducts.UnionWith(targetFileToSourceFile.Keys); // Apply the optional output tag to them foreach (string tagName in FindTagNamesFromList(_parameters.Tag)) { FindOrAddTagSet(tagNameToFileSet, tagName).UnionWith(targetFileToSourceFile.Keys); } } catch (FilePatternSourceFileMissingException ex) { if (_parameters.ErrorIfNotFound) { throw new AutomationException(ex, "Error while trying to create file pattern match for '{0}': {1}", sourcePattern, ex.Message); } else { Logger.LogInformation(ex, "Error while trying to create file pattern match for '{SourcePattern}': {Message}", sourcePattern, ex.Message); } } } /// /// /// /// /// /// public static async Task ExecuteAsync(Dictionary targetFileToSourceFile, bool overwrite) { // If we're not overwriting, remove any files where the destination file already exists. if (!overwrite) { Dictionary filteredTargetToSourceFile = new Dictionary(); foreach (KeyValuePair file in targetFileToSourceFile) { if (FileReference.Exists(file.Key)) { Logger.LogInformation("Not copying existing file {Arg0}", file.Key); continue; } filteredTargetToSourceFile.Add(file.Key, file.Value); } if (filteredTargetToSourceFile.Count == 0) { Logger.LogInformation("All files already exist, exiting early."); return; } targetFileToSourceFile = filteredTargetToSourceFile; } // If the target is on a network share, retry creating the first directory until it succeeds DirectoryReference firstTargetDirectory = targetFileToSourceFile.First().Key.Directory; if (!DirectoryReference.Exists(firstTargetDirectory)) { const int MaxNumRetries = 15; for (int numRetries = 0; ; numRetries++) { try { DirectoryReference.CreateDirectory(firstTargetDirectory); if (numRetries == 1) { Logger.LogInformation("Created target directory {FirstTargetDirectory} after 1 retry.", firstTargetDirectory); } else if (numRetries > 1) { Logger.LogInformation("Created target directory {FirstTargetDirectory} after {NumRetries} retries.", firstTargetDirectory, numRetries); } break; } catch (Exception ex) { #pragma warning disable CA1508 // False positive about NumRetries alwayws being zero if (numRetries == 0) { Logger.LogInformation("Unable to create directory '{FirstTargetDirectory}' on first attempt. Retrying {MaxNumRetries} times...", firstTargetDirectory, MaxNumRetries); } #pragma warning restore CA1508 Logger.LogDebug(" {Ex}", ex); if (numRetries >= 15) { throw new AutomationException(ex, "Unable to create target directory '{0}' after {1} retries.", firstTargetDirectory, numRetries); } await Task.Delay(2000); } } } // Copy them all KeyValuePair[] filePairs = targetFileToSourceFile.ToArray(); foreach (KeyValuePair filePair in filePairs) { Logger.LogDebug(" {Arg0} -> {Arg1}", filePair.Value, filePair.Key); } CommandUtils.ThreadedCopyFiles(filePairs.Select(x => x.Value.FullName).ToList(), filePairs.Select(x => x.Key.FullName).ToList(), bQuiet: true, bRetry: true); } /// /// Output this task out to an XML writer. /// public override void Write(XmlWriter writer) { Write(writer, _parameters); } /// /// Find all the tags which are used as inputs to this task /// /// The tag names which are read by this task public override IEnumerable FindConsumedTagNames() { foreach (string tagName in FindTagNamesFromFilespec(_parameters.Files)) { yield return tagName; } } /// /// Find all the tags which are modified by this task /// /// The tag names which are modified by this task public override IEnumerable FindProducedTagNames() { return FindTagNamesFromList(_parameters.Tag); } } /// /// Extension methods /// public static partial class BgStateExtensions { /// /// Copy files from one location to another /// /// The files to copy /// /// Whether or not to overwrite existing files. public static async Task CopyToAsync(this FileSet files, DirectoryReference targetDir, bool? overwrite = null) { // Run the copy Dictionary targetFileToSourceFile = files.Flatten(targetDir); if (targetFileToSourceFile.Count == 0) { return FileSet.Empty; } Log.Logger.LogInformation("Copying {NumFiles} file(s) to {TargetDir}...", targetFileToSourceFile.Count, targetDir); await CopyTask.ExecuteAsync(targetFileToSourceFile, overwrite ?? true); return FileSet.FromFiles(targetFileToSourceFile.Keys.Select(x => (x.MakeRelativeTo(targetDir), x))); } } }