using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Xml; using UnrealBuildTool; namespace AutomationTool { /// /// Specifies validation that should be performed on a task parameter. /// public enum TaskParameterValidationType { /// /// Allow any valid values for the field type. /// Default, /// /// A standard name; alphanumeric characters, plus underscore and space. Spaces at the start or end, or more than one in a row are prohibited. /// Name, /// /// A list of names separated by semicolons /// NameList, /// /// A tag name (a regular name with an optional '#' prefix) /// Tag, /// /// A list of tag names separated by semicolons /// TagList, } /// /// Attribute to mark parameters to a task, which should be read as XML attributes from the script file. /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public class TaskParameterAttribute : Attribute { /// /// Whether the parameter can be omitted /// public bool Optional { get; set; } /// /// Sets additional restrictions on how this field is validated in the schema. Default is to allow any valid field type. /// public TaskParameterValidationType ValidationType { get; set; } } /// /// Attribute used to associate an XML element name with a parameter block that can be used to construct tasks /// [AttributeUsage(AttributeTargets.Class)] public class TaskElementAttribute : Attribute { /// /// Name of the XML element that can be used to denote this class /// public string Name; /// /// Type to be constructed from the deserialized element /// public Type ParametersType; /// /// Constructor /// /// Name of the XML element used to denote this object /// Type to be constructed from this object public TaskElementAttribute(string InName, Type InParametersType) { Name = InName; ParametersType = InParametersType; } } /// /// Base class for all custom build tasks /// public abstract class CustomTask { /// /// Allow this task to merge with other tasks within the same node if it can. This can be useful to allow tasks to execute in parallel. /// /// Other tasks that this task can merge with. If a merge takes place, the other tasks should be removed from the list. public virtual void Merge(List OtherTasks) { } /// /// Execute this node. /// /// Information about the current job /// Set of build products produced by this node. /// Mapping from tag names to the set of files they include /// Whether the task succeeded or not. Exiting with an exception will be caught and treated as a failure. public abstract bool Execute(JobContext Job, HashSet BuildProducts, Dictionary> TagNameToFileSet); /// /// Resolves a single name to a file reference, resolving relative paths to the root of the current path. /// /// Name of the file /// Fully qualified file reference public static FileReference ResolveFile(string Name) { if(Path.IsPathRooted(Name)) { return new FileReference(Name); } else { return new FileReference(Path.Combine(CommandUtils.CmdEnv.LocalRoot, Name)); } } /// /// Resolves a directory reference from the given string. Assumes the root directory is the root of the current branch. /// /// Name of the directory. May be null or empty. /// The resolved directory public static DirectoryReference ResolveDirectory(string Name) { if(String.IsNullOrEmpty(Name)) { return CommandUtils.RootDirectory; } else if(Path.IsPathRooted(Name)) { return new DirectoryReference(Name); } else { return DirectoryReference.Combine(CommandUtils.RootDirectory, Name); } } /// /// Finds or adds a set containing files with the given tag /// /// The tag name to return a set for. An leading '#' character is optional. /// Set of files public static HashSet FindOrAddTagSet(Dictionary> TagNameToFileSet, string Name) { // Get the clean tag name, without the leading '#' character string TagName = Name.StartsWith("#")? Name.Substring(1) : Name; // Find the files which match this tag HashSet Files; if(!TagNameToFileSet.TryGetValue(TagName, out Files)) { Files = new HashSet(); TagNameToFileSet.Add(TagName, Files); } // If we got a null reference, it's because the tag is not listed as an input for this node (see RunGraph.BuildSingleNode). Fill it in, but only with an error. if(Files == null) { CommandUtils.LogError("Attempt to reference tag '{0}', which is not listed as a dependency of this node.", Name); Files = new HashSet(); TagNameToFileSet.Add(TagName, Files); } return Files; } /// /// Resolve a list of files, tag names or file specifications separated by semicolons. Supported entries may be: /// a) The name of a tag set (eg. #CompiledBinaries) /// b) Relative or absolute filenames /// c) A simple file pattern (eg. Foo/*.cpp) /// d) A full directory wildcard (eg. Engine/...) /// Note that wildcards may only match the last fragment in a pattern, so matches like "/*/Foo.txt" and "/.../Bar.txt" are illegal. /// /// The default directory to resolve relative paths to /// List of files, tag names, or file specifications to include separated by semicolons. /// Mapping of tag name to fileset, as passed to the Execute() method /// Set of matching files. public static HashSet ResolveFilespec(DirectoryReference DefaultDirectory, string DelimitedPatterns, Dictionary> TagNameToFileSet) { // Find all the files and directories that are referenced HashSet Directories = new HashSet(); HashSet Files = ResolveFilespec(DefaultDirectory, DelimitedPatterns, Directories, TagNameToFileSet); // Include all the files underneath the directories foreach(DirectoryReference Directory in Directories) { Files.UnionWith(Directory.EnumerateFileReferences("*", SearchOption.AllDirectories)); } return Files; } /// /// Resolve a list of files, tag names or file specifications separated by semicolons as above, but preserves any directory references for further processing. /// /// The default directory to resolve relative paths to /// List of files, tag names, or file specifications to include separated by semicolons. /// Set of directories which are referenced using directory wildcards. Files under these directories are not added to the output set. /// Mapping of tag name to fileset, as passed to the Execute() method /// Set of matching files. public static HashSet ResolveFilespec(DirectoryReference DefaultDirectory, string DelimitedPatterns, HashSet Directories, Dictionary> TagNameToFileSet) { // Split the argument into a list of patterns string[] Patterns = SplitDelimitedList(DelimitedPatterns); // Parse each of the patterns, and add the results into the given sets HashSet Files = new HashSet(); foreach(string Pattern in Patterns) { // Check if it's a tag name if(Pattern.StartsWith("#")) { Files.UnionWith(FindOrAddTagSet(TagNameToFileSet, Pattern.Substring(1))); continue; } // List of all wildcards string[] Wildcards = { "?", "*", "..." }; // Find the first index of a wildcard in the given pattern int WildcardIdx = -1; foreach(string Wildcard in Wildcards) { int Idx = Pattern.IndexOf(Wildcard); if(Idx != -1 && (WildcardIdx == -1 || Idx < WildcardIdx)) { WildcardIdx = Idx; } } // If we didn't find any wildcards, we can just add the pattern directly. if(WildcardIdx == -1) { Files.Add(FileReference.Combine(DefaultDirectory, Pattern)); continue; } // Check the wildcard is in the last fragment of the path. int LastDirectoryIdx = Pattern.LastIndexOfAny(new char[]{ Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); if(LastDirectoryIdx != -1 && LastDirectoryIdx > WildcardIdx) { CommandUtils.LogWarning("Invalid pattern '{0}': Path wildcard can only match files"); continue; } // Check if it's a directory reference (ends with ...) or file reference. int PathWildcardIdx = Pattern.IndexOf("..."); if(PathWildcardIdx != -1) { // Add the base directory to the list of results if(PathWildcardIdx + 3 != Pattern.Length) { CommandUtils.LogWarning("Invalid pattern '{0}': Path wildcard must appear at end of pattern."); } else if(PathWildcardIdx != LastDirectoryIdx + 1) { CommandUtils.LogWarning("Invalid pattern '{0}': Path wildcard cannot partially match a name."); } else { Directories.Add(DirectoryReference.Combine(DefaultDirectory, Pattern.Substring(0, PathWildcardIdx))); } } else { // Construct a file filter and apply it to files in the directory. Don't use the default file enumeration logic for consistency; search patterns // passed to Directory.EnumerateFiles et al have special cases for backwards compatibility that we don't want. FileFilter Filter = new FileFilter(); Filter.AddRule("/" + Pattern.Substring(LastDirectoryIdx + 1), FileFilterType.Include); DirectoryReference BaseDir = DirectoryReference.Combine(DefaultDirectory, Pattern.Substring(0, LastDirectoryIdx + 1)); Files.UnionWith(Filter.ApplyToDirectory(BaseDir, true)); } } return Files; } /// /// Splits a string separated by semicolons into a list, removing empty entries /// /// The input string /// Array of the parsed items public static string[] SplitDelimitedList(string Text) { return Text.Split(';').Select(x => x.Trim()).Where(x => x.Length > 0).ToArray(); } } }