// Copyright Epic Games, Inc. All Rights Reserved. using AutomationTool; using EpicGames.BuildGraph; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using EpicGames.Core; using UnrealBuildTool; using UnrealBuildBase; using Microsoft.Extensions.Logging; 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; /// /// The pattern(s) to copy from (for example, Engine/*.txt). /// [TaskParameter(ValidationType = TaskParameterValidationType.FileSpec)] public string From; /// /// The directory to copy to. /// [TaskParameter(ValidationType = TaskParameterValidationType.FileSpec)] public string To; /// /// Whether or not to overwrite existing files. /// [TaskParameter(Optional = true)] public bool Overwrite = true; /// /// Tag to be applied to build products of this task. /// [TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.TagList)] public string Tag; /// /// Whether or not to throw an error if no files were found to copy /// [TaskParameter(Optional = true)] public bool ErrorIfNotFound = false; } /// /// Copies files from one directory to another. /// [TaskElement("Copy", typeof(CopyTaskParameters))] public class CopyTask : BgTaskImpl { /// /// Parameters for this task /// CopyTaskParameters Parameters; /// /// Constructor /// /// Parameters for this task public CopyTask(CopyTaskParameters InParameters) { Parameters = InParameters; } /// /// Execute 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); } // Build the file mapping Dictionary TargetFileToSourceFile = FilePattern.CreateMapping(Files, ref SourcePattern, ref TargetPattern); // Check we got some files if (TargetFileToSourceFile.Count == 0) { if (Parameters.ErrorIfNotFound) { CommandUtils.LogError("No files found matching '{0}'", SourcePattern); } else { CommandUtils.LogInformation("No files found matching '{0}'", SourcePattern); } return; } // Run the copy CommandUtils.LogInformation("Copying {0} file{1} from {2} to {3}...", 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); } } public static Task ExecuteAsync(Dictionary TargetFileToSourceFile, bool Overwrite) { // If we're not overwriting, remove any files where the destination file already exists. if (!Overwrite) { TargetFileToSourceFile = TargetFileToSourceFile.Where(File => { if (FileReference.Exists(File.Key)) { CommandUtils.LogInformation("Not copying existing file {0}", File.Key); return false; } return true; }).ToDictionary(Pair => Pair.Key, Pair => Pair.Value); } // 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) { Log.TraceInformation("Created target directory {0} after 1 retry.", FirstTargetDirectory); } else if(NumRetries > 1) { Log.TraceInformation("Created target directory {0} after {1} retries.", FirstTargetDirectory, NumRetries); } break; } catch(Exception Ex) { if(NumRetries == 0) { Log.TraceInformation("Unable to create directory '{0}' on first attempt. Retrying {1} times...", FirstTargetDirectory, MaxNumRetries); } Log.TraceLog(" {0}", Ex); if(NumRetries >= 15) { throw new AutomationException(Ex, "Unable to create target directory '{0}' after {1} retries.", FirstTargetDirectory, NumRetries); } Thread.Sleep(2000); } } } // Copy them all KeyValuePair[] FilePairs = TargetFileToSourceFile.ToArray(); foreach(KeyValuePair FilePair in FilePairs) { CommandUtils.LogLog(" {0} -> {1}", FilePair.Value, FilePair.Key); } CommandUtils.ThreadedCopyFiles(FilePairs.Select(x => x.Value.FullName).ToList(), FilePairs.Select(x => x.Key.FullName).ToList(), bQuiet: true, bRetry: true); return Task.CompletedTask; } /// /// 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); } } 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))); } } }