// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.BuildGraph; 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 EpicGames.Core; using OpenTracing; using UnrealBuildBase; using UnrealBuildTool; namespace AutomationTool { /// /// 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; } } /// /// Proxy to handle executing multiple tasks simultaneously (such as compile tasks). If a task supports simultaneous execution, it can return a separate /// executor an executor instance from GetExecutor() callback. If not, it must implement Execute(). /// public interface ITaskExecutor { /// /// Adds another task to this executor /// /// Task to add /// True if the task could be added, false otherwise bool Add(BgTaskImpl Task); /// /// Execute all the tasks added to this executor. /// /// 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. Task ExecuteAsync(JobContext Job, HashSet BuildProducts, Dictionary> TagNameToFileSet); } /// /// Base class for all custom build tasks /// public abstract class BgTaskImpl { /// /// Line number in a source file that this task was declared. Optional; used for log messages. /// public BgScriptLocation SourceLocation { get; set; } /// /// 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 Task ExecuteAsync(JobContext Job, HashSet BuildProducts, Dictionary> TagNameToFileSet); /// /// Creates a proxy to execute this node. /// /// New proxy instance if one is available to execute this task, otherwise null. public virtual ITaskExecutor GetExecutor() { return null; } /// /// Output this task out to an XML writer. /// public abstract void Write(XmlWriter Writer); /// /// Writes this task to an XML writer, using the given parameters object. /// /// Writer for the XML schema /// Parameters object that this task is constructed with protected void Write(XmlWriter Writer, object Parameters) { TaskElementAttribute Element = GetType().GetCustomAttribute(); Writer.WriteStartElement(Element.Name); foreach (FieldInfo Field in Parameters.GetType().GetFields()) { if (Field.MemberType == MemberTypes.Field) { TaskParameterAttribute ParameterAttribute = Field.GetCustomAttribute(); if (ParameterAttribute != null) { object Value = Field.GetValue(Parameters); if (Value != null) { Writer.WriteAttributeString(Field.Name, Value.ToString()); } } } } Writer.WriteEndElement(); } /// /// Returns a string used for trace messages /// public string GetTraceString() { StringBuilder Builder = new StringBuilder(); using (XmlWriter Writer = XmlWriter.Create(new StringWriter(Builder), new XmlWriterSettings() { OmitXmlDeclaration = true })) { Write(Writer); } return Builder.ToString(); } /// /// Gets the name of this task for tracing /// /// The trace name public virtual string GetTraceName() { TaskElementAttribute TaskElement = GetType().GetCustomAttribute(); return (TaskElement != null)? TaskElement.Name : "unknown"; } /// /// Get properties to include in tracing info /// /// The scope to add properties to /// Prefix for metadata entries public virtual void GetTraceMetadata(ITraceSpan Span, string Prefix) { Span.AddMetadata(Prefix + "source.file", SourceLocation.File); Span.AddMetadata(Prefix + "source.line", SourceLocation.LineNumber.ToString()); } /// /// Get properties to include in tracing info /// /// The scope to add properties to /// Prefix for metadata entries public virtual void GetTraceMetadata(ISpan Span, string Prefix) { Span.SetTag(Prefix + "source.file", SourceLocation.File); Span.SetTag(Prefix + "source.line", SourceLocation.LineNumber); } /// /// Find all the tags which are used as inputs to this task /// /// The tag names which are read by this task public abstract IEnumerable FindConsumedTagNames(); /// /// Find all the tags which are modified by this task /// /// The tag names which are modified by this task public abstract IEnumerable FindProducedTagNames(); /// /// Adds tag names from a filespec /// /// A filespec, as can be passed to ResolveFilespec /// Tag names from this filespec protected IEnumerable FindTagNamesFromFilespec(string Filespec) { if(!String.IsNullOrEmpty(Filespec)) { foreach(string Pattern in SplitDelimitedList(Filespec)) { if(Pattern.StartsWith("#")) { yield return Pattern; } } } } /// /// Enumerates tag names from a list /// /// List of tags separated by semicolons /// Tag names from this filespec protected IEnumerable FindTagNamesFromList(string TagList) { if(!String.IsNullOrEmpty(TagList)) { foreach(string TagName in SplitDelimitedList(TagList)) { yield return TagName; } } } /// /// 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 Unreal.RootDirectory; } else if(Path.IsPathRooted(Name)) { return new DirectoryReference(Name); } else { return DirectoryReference.Combine(Unreal.RootDirectory, Name); } } /// /// Finds or adds a set containing files with the given tag /// /// Map of tag names to the set of files they contain /// The tag name to return a set for. A leading '#' character is required. /// Set of files public static HashSet FindOrAddTagSet(Dictionary> TagNameToFileSet, string TagName) { // Make sure the tag name contains a single leading hash if (TagName.LastIndexOf('#') != 0) { throw new AutomationException("Tag name '{0}' is not valid - should contain a single leading '#' character", TagName); } // Any spaces should be later than the second char - most likely to be a typo if directly after the # character if (TagName.IndexOf(' ') == 1) { throw new AutomationException("Tag name '{0}' is not valid - spaces should only be used to separate words", TagName); } // 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.", TagName); 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) { List ExcludePatterns = new List(); return ResolveFilespecWithExcludePatterns(DefaultDirectory, DelimitedPatterns, ExcludePatterns, TagNameToFileSet); } /// /// 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 patterns to apply to directory searches. This can greatly speed up enumeration by earlying out of recursive directory searches if large directories are excluded (eg. .../Intermediate/...). /// Mapping of tag name to fileset, as passed to the Execute() method /// Set of matching files. public static HashSet ResolveFilespecWithExcludePatterns(DirectoryReference DefaultDirectory, string DelimitedPatterns, List ExcludePatterns, Dictionary> TagNameToFileSet) { // Split the argument into a list of patterns List Patterns = SplitDelimitedList(DelimitedPatterns); return ResolveFilespecWithExcludePatterns(DefaultDirectory, Patterns, ExcludePatterns, TagNameToFileSet); } /// /// Resolve a list of files, tag names or file specifications 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 patterns to apply to directory searches. This can greatly speed up enumeration by earlying out of recursive directory searches if large directories are excluded (eg. .../Intermediate/...). /// Mapping of tag name to fileset, as passed to the Execute() method /// Set of matching files. public static HashSet ResolveFilespecWithExcludePatterns(DirectoryReference DefaultDirectory, List FilePatterns, List ExcludePatterns, Dictionary> TagNameToFileSet) { // Parse each of the patterns, and add the results into the given sets HashSet Files = new HashSet(); foreach(string Pattern in FilePatterns) { // Check if it's a tag name if(Pattern.StartsWith("#")) { Files.UnionWith(FindOrAddTagSet(TagNameToFileSet, Pattern)); continue; } // If it doesn't contain any wildcards, just add the pattern directly int WildcardIdx = FileFilter.FindWildcardIndex(Pattern); if(WildcardIdx == -1) { Files.Add(FileReference.Combine(DefaultDirectory, Pattern)); continue; } // Find the base directory for the search. We construct this in a very deliberate way including the directory separator itself, so matches // against the OS root directory will resolve correctly both on Mac (where / is the filesystem root) and Windows (where / refers to the current drive). int LastDirectoryIdx = Pattern.LastIndexOfAny(new char[]{ Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, WildcardIdx); DirectoryReference BaseDir = DirectoryReference.Combine(DefaultDirectory, Pattern.Substring(0, LastDirectoryIdx + 1)); // Construct the absolute include pattern to match against, re-inserting the resolved base directory to construct a canonical path. string IncludePattern = BaseDir.FullName.TrimEnd(new char[]{ Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }) + "/" + Pattern.Substring(LastDirectoryIdx + 1); // Construct a filter and apply it to the directory if(DirectoryReference.Exists(BaseDir)) { FileFilter Filter = new FileFilter(); Filter.AddRule(IncludePattern, FileFilterType.Include); if(ExcludePatterns != null && ExcludePatterns.Count > 0) { Filter.AddRules(ExcludePatterns, FileFilterType.Exclude); } Files.UnionWith(Filter.ApplyToDirectory(BaseDir, BaseDir.FullName, true)); } } // If we have exclude rules, create and run a filter against all the output files to catch things that weren't added from an include if(ExcludePatterns != null && ExcludePatterns.Count > 0) { FileFilter Filter = new FileFilter(FileFilterType.Include); Filter.AddRules(ExcludePatterns, FileFilterType.Exclude); Files.RemoveWhere(x => !Filter.Matches(x.FullName)); } return Files; } /// /// Splits a string separated by semicolons into a list, removing empty entries /// /// The input string /// Array of the parsed items public static List SplitDelimitedList(string Text) { return Text.Split(';').Select(x => x.Trim()).Where(x => x.Length > 0).ToList(); } } /// /// Legacy implementation of which operates synchronously /// public abstract class CustomTask : BgTaskImpl { /// public sealed override Task ExecuteAsync(JobContext Job, HashSet BuildProducts, Dictionary> TagNameToFileSet) { Execute(Job, BuildProducts, TagNameToFileSet); return Task.CompletedTask; } /// /// 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 void Execute(JobContext Job, HashSet BuildProducts, Dictionary> TagNameToFileSet); } }