using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using UnrealBuildTool;
namespace AutomationTool.Tasks
{
///
/// Parameters for a task that compiles a C# project
///
public class CsCompileTaskParameters
{
///
/// The C# project file to be compile. More than one project file can be specified by separating with semicolons.
///
[TaskParameter]
public string Project;
///
/// The configuration to compile
///
[TaskParameter(Optional = true)]
public string Configuration;
///
/// The platform to compile
///
[TaskParameter(Optional = true)]
public string Platform;
///
/// Additional options to pass to the compiler
///
[TaskParameter(Optional = true)]
public string Arguments;
///
/// Directory containing output files
///
[TaskParameter(Optional = true)]
public string OutputDir;
///
/// Patterns for output files
///
[TaskParameter(Optional = true)]
public string OutputFiles;
///
/// Only enumerate build products; do not actually compile the projects.
///
[TaskParameter(Optional = true)]
public bool EnumerateOnly;
///
/// Tag to be applied to build products of this task
///
[TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.TagList)]
public string Tag;
///
/// Tag to be applied to any non-private references the projects have
/// (i.e. those that are external and not copied into the output dir)
///
[TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.TagList)]
public string TagReferences;
}
///
/// Compile a C# project file
///
[TaskElement("CsCompile", typeof(CsCompileTaskParameters))]
public class CsCompileTask : CustomTask
{
///
/// Parameters for the task
///
CsCompileTaskParameters Parameters;
///
/// Constructor.
///
/// Parameters for this task
public CsCompileTask(CsCompileTaskParameters 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
/// True if the task succeeded
public override bool Execute(JobContext Job, HashSet BuildProducts, Dictionary> TagNameToFileSet)
{
// Get the project file
HashSet ProjectFiles = ResolveFilespec(CommandUtils.RootDirectory, Parameters.Project, TagNameToFileSet);
foreach(FileReference ProjectFile in ProjectFiles)
{
if(!ProjectFile.Exists())
{
CommandUtils.LogError("Couldn't find project file '{0}'", ProjectFile.FullName);
return false;
}
if(!ProjectFile.HasExtension(".csproj"))
{
CommandUtils.LogError("File '{0}' is not a C# project", ProjectFile.FullName);
return false;
}
}
// Get the default properties
Dictionary Properties = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
if(!String.IsNullOrEmpty(Parameters.Platform))
{
Properties["Platform"] = Parameters.Platform;
}
if(!String.IsNullOrEmpty(Parameters.Configuration))
{
Properties["Configuration"] = Parameters.Configuration;
}
// Build the arguments and run the build
if(!Parameters.EnumerateOnly)
{
List Arguments = new List();
foreach(KeyValuePair PropertyPair in Properties)
{
Arguments.Add(String.Format("/property:{0}={1}", CommandUtils.MakePathSafeToUseWithCommandLine(PropertyPair.Key), CommandUtils.MakePathSafeToUseWithCommandLine(PropertyPair.Value)));
}
if(!String.IsNullOrEmpty(Parameters.Arguments))
{
Arguments.Add(Parameters.Arguments);
}
Arguments.Add("/verbosity:minimal");
Arguments.Add("/nologo");
foreach(FileReference ProjectFile in ProjectFiles)
{
CommandUtils.MsBuild(CommandUtils.CmdEnv, ProjectFile.FullName, String.Join(" ", Arguments), null);
}
}
// Try to figure out the output files
HashSet ProjectBuildProducts;
HashSet ProjectReferences;
if(!FindBuildProducts(ProjectFiles, Properties, out ProjectBuildProducts, out ProjectReferences))
{
return false;
}
// Apply the optional tag to the produced archive
foreach(string TagName in FindTagNamesFromList(Parameters.Tag))
{
FindOrAddTagSet(TagNameToFileSet, TagName).UnionWith(ProjectBuildProducts);
}
if (!String.IsNullOrEmpty(Parameters.TagReferences))
{
// Apply the optional tag to any references
foreach (string TagName in FindTagNamesFromList(Parameters.TagReferences))
{
FindOrAddTagSet(TagNameToFileSet, TagName).UnionWith(ProjectReferences);
}
}
// Merge them into the standard set of build products
BuildProducts.UnionWith(ProjectBuildProducts);
return 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()
{
return FindTagNamesFromFilespec(Parameters.Project);
}
///
/// Find all the tags which are modified by this task
///
/// The tag names which are modified by this task
public override IEnumerable FindProducedTagNames()
{
foreach (string TagName in FindTagNamesFromList(Parameters.Tag))
{
yield return TagName;
}
foreach (string TagName in FindTagNamesFromList(Parameters.TagReferences))
{
yield return TagName;
}
}
///
/// Find all the build products created by compiling the given project file
///
/// Initial project file to read. All referenced projects will also be read.
/// Mapping of property name to value
/// Receives a set of build products on success
/// Receives a set of non-private references on success
/// True if the build products were found, false otherwise.
static bool FindBuildProducts(HashSet ProjectFiles, Dictionary InitialProperties, out HashSet OutBuildProducts, out HashSet OutReferences)
{
// Read all the project information into a dictionary
Dictionary FileToProjectInfo = new Dictionary();
foreach(FileReference ProjectFile in ProjectFiles)
{
if(!ReadProjectsRecursively(ProjectFile, InitialProperties, FileToProjectInfo))
{
OutBuildProducts = null;
OutReferences = null;
return false;
}
}
// Find all the build products and references
HashSet BuildProducts = new HashSet();
HashSet References = new HashSet();
foreach(KeyValuePair Pair in FileToProjectInfo)
{
CsProjectInfo ProjectInfo = Pair.Value;
// Add the standard build products
DirectoryReference OutputDir = ProjectInfo.GetOutputDir(Pair.Key.Directory);
ProjectInfo.AddBuildProducts(OutputDir, BuildProducts);
// Add the referenced assemblies
foreach(KeyValuePair Reference in ProjectInfo.References)
{
FileReference OtherAssembly = Reference.Key;
if (Reference.Value)
{
// Add reference from the output dir
FileReference OutputFile = FileReference.Combine(OutputDir, OtherAssembly.GetFileName());
BuildProducts.Add(OutputFile);
FileReference OutputSymbolFile = OutputFile.ChangeExtension(".pdb");
if(OutputSymbolFile.Exists())
{
BuildProducts.Add(OutputSymbolFile);
}
}
else
{
// Add reference directly
References.Add(OtherAssembly);
FileReference SymbolFile = OtherAssembly.ChangeExtension(".pdb");
if(SymbolFile.Exists())
{
References.Add(SymbolFile);
}
}
}
// Add build products from all the referenced projects. MSBuild only copy the directly referenced build products, not recursive references or other assemblies.
foreach(CsProjectInfo OtherProjectInfo in ProjectInfo.ProjectReferences.Where(x => x.Value).Select(x => FileToProjectInfo[x.Key]))
{
OtherProjectInfo.AddBuildProducts(OutputDir, BuildProducts);
}
}
// Update the output set
OutBuildProducts = BuildProducts;
OutReferences = References;
return true;
}
///
/// Read a project file, plus all the project files it references.
///
/// Project file to read
/// Mapping of property name to value for the initial project
///
/// True if the projects were read correctly, false (and prints an error to the log) if not
static bool ReadProjectsRecursively(FileReference File, Dictionary InitialProperties, Dictionary FileToProjectInfo)
{
// Early out if we've already read this project, return succes
if(FileToProjectInfo.ContainsKey(File))
{
return true;
}
// Try to read this project
CsProjectInfo ProjectInfo;
if(!CsProjectInfo.TryRead(File, InitialProperties, out ProjectInfo))
{
CommandUtils.LogError("Couldn't read project '{0}'", File.FullName);
return false;
}
// Add it to the project lookup, and try to read all the projects it references
FileToProjectInfo.Add(File, ProjectInfo);
return ProjectInfo.ProjectReferences.Keys.All(x => ReadProjectsRecursively(x, new Dictionary(), FileToProjectInfo));
}
}
///
/// Basic information from a preprocessed C# project file. Supports reading a project file, expanding simple conditions in it, parsing property values, assembly references and references to other projects.
///
class CsProjectInfo
{
///
/// Evaluated properties from the project file
///
public Dictionary Properties;
///
/// Mapping of referenced assemblies to their 'CopyLocal' (aka 'Private') setting.
///
public Dictionary References = new Dictionary();
///
/// Mapping of referenced projects to their 'CopyLocal' (aka 'Private') setting.
///
public Dictionary ProjectReferences = new Dictionary();
///
/// Constructor
///
/// Initial mapping of property names to values
CsProjectInfo(Dictionary InProperties)
{
Properties = new Dictionary(InProperties);
}
///
/// Resolve the project's output directory
///
/// Base directory to resolve relative paths to
/// The configured output directory
public DirectoryReference GetOutputDir(DirectoryReference BaseDirectory)
{
string OutputPath;
if(Properties.TryGetValue("OutputPath", out OutputPath))
{
return DirectoryReference.Combine(BaseDirectory, OutputPath);
}
else
{
return BaseDirectory;
}
}
///
/// Adds build products from the project to the given set.
///
/// Output directory for the build products. May be different to the project's output directory in the case that we're copying local to another project.
/// Set to receive the list of build products
public bool AddBuildProducts(DirectoryReference OutputDir, HashSet BuildProducts)
{
string OutputType, AssemblyName;
if(Properties.TryGetValue("OutputType", out OutputType) && Properties.TryGetValue("AssemblyName", out AssemblyName))
{
switch(OutputType)
{
case "Exe":
case "WinExe":
BuildProducts.Add(FileReference.Combine(OutputDir, AssemblyName + ".exe"));
BuildProducts.Add(FileReference.Combine(OutputDir, AssemblyName + ".pdb"));
FileReference ExeConfig = FileReference.Combine(OutputDir, AssemblyName + ".exe.config");
if (ExeConfig.Exists()) { BuildProducts.Add(ExeConfig); }
FileReference ExeMDB = FileReference.Combine(OutputDir, AssemblyName + "exe.mdb");
if (ExeMDB.Exists()) { BuildProducts.Add(ExeMDB); }
return true;
case "Library":
BuildProducts.Add(FileReference.Combine(OutputDir, AssemblyName + ".dll"));
BuildProducts.Add(FileReference.Combine(OutputDir, AssemblyName + ".pdb"));
return true;
}
}
return false;
}
///
/// Attempts to read project information for the given file.
///
/// The project file to read
/// Initial set of property values
/// If successful, the parsed project info
/// True if the project was read successfully, false otherwise
public static bool TryRead(FileReference File, Dictionary Properties, out CsProjectInfo OutProjectInfo)
{
// Read the project file
XmlDocument Document = new XmlDocument();
Document.Load(File.FullName);
// Check the root element is the right type
// HashSet ProjectBuildProducts = new HashSet();
if(Document.DocumentElement.Name != "Project")
{
OutProjectInfo = null;
return false;
}
// Parse the basic structure of the document, updating properties and recursing into other referenced projects as we go
CsProjectInfo ProjectInfo = new CsProjectInfo(Properties);
foreach(XmlElement Element in Document.DocumentElement.ChildNodes.OfType())
{
switch(Element.Name)
{
case "PropertyGroup":
if(EvaluateCondition(Element, ProjectInfo.Properties))
{
ParsePropertyGroup(Element, ProjectInfo.Properties);
}
break;
case "ItemGroup":
if(EvaluateCondition(Element, ProjectInfo.Properties))
{
ParseItemGroup(File.Directory, Element, ProjectInfo);
}
break;
}
}
// Return the complete project
OutProjectInfo = ProjectInfo;
return true;
}
///
/// Parses a 'PropertyGroup' element.
///
/// The parent 'PropertyGroup' element
/// Dictionary mapping property names to values
static void ParsePropertyGroup(XmlElement ParentElement, Dictionary Properties)
{
// We need to know the overridden output type and output path for the selected configuration.
foreach(XmlElement Element in ParentElement.ChildNodes.OfType())
{
if(EvaluateCondition(Element, Properties))
{
Properties[Element.Name] = ExpandProperties(Element.InnerText, Properties);
}
}
}
///
/// Parses an 'ItemGroup' element.
///
/// Base directory to resolve relative paths against
/// The parent 'ItemGroup' element
/// Project info object to be updated
static void ParseItemGroup(DirectoryReference BaseDirectory, XmlElement ParentElement, CsProjectInfo ProjectInfo)
{
// Parse any external assembly references
foreach(XmlElement ItemElement in ParentElement.ChildNodes.OfType())
{
switch(ItemElement.Name)
{
case "Reference":
// Reference to an external assembly
if(EvaluateCondition(ItemElement, ProjectInfo.Properties))
{
ParseReference(BaseDirectory, ItemElement, ProjectInfo.References);
}
break;
case "ProjectReference":
// Reference to another project
if(EvaluateCondition(ItemElement, ProjectInfo.Properties))
{
ParseProjectReference(BaseDirectory, ItemElement, ProjectInfo.ProjectReferences);
}
break;
}
}
}
///
/// Parses an assembly reference from a given 'Reference' element
///
/// Directory to resolve relative paths against
/// The parent 'Reference' element
/// Dictionary of project files to a bool indicating whether the assembly should be copied locally to the referencing project.
static void ParseReference(DirectoryReference BaseDirectory, XmlElement ParentElement, Dictionary References)
{
string HintPath = GetChildElementString(ParentElement, "HintPath", null);
if(!String.IsNullOrEmpty(HintPath))
{
FileReference AssemblyFile = FileReference.Combine(BaseDirectory, HintPath);
bool bPrivate = GetChildElementBoolean(ParentElement, "Private", true);
References.Add(AssemblyFile, bPrivate);
}
}
///
/// Parses a project reference from a given 'ProjectReference' element
///
/// Directory to resolve relative paths against
/// The parent 'ProjectReference' element
/// Dictionary of project files to a bool indicating whether the outputs of the project should be copied locally to the referencing project.
static void ParseProjectReference(DirectoryReference BaseDirectory, XmlElement ParentElement, Dictionary ProjectReferences)
{
string IncludePath = ParentElement.GetAttribute("Include");
if(!String.IsNullOrEmpty(IncludePath))
{
FileReference ProjectFile = FileReference.Combine(BaseDirectory, IncludePath);
bool bPrivate = GetChildElementBoolean(ParentElement, "Private", true);
ProjectReferences[ProjectFile] = bPrivate;
}
}
///
/// Reads the inner text of a child XML element
///
/// The parent element to check
/// Name of the child element
/// Default value to return if the child element is missing
/// The contents of the child element, or default value if it's not present
static string GetChildElementString(XmlElement ParentElement, string Name, string DefaultValue)
{
XmlElement ChildElement = ParentElement.ChildNodes.OfType().FirstOrDefault(x => x.Name == Name);
if(ChildElement == null)
{
return DefaultValue;
}
else
{
return ChildElement.InnerText ?? DefaultValue;
}
}
///
/// Read a child XML element with the given name, and parse it as a boolean.
///
/// Parent element to check
/// Name of the child element to look for
/// Default value to return if the element is missing or not a valid bool
/// The parsed boolean, or the default value
static bool GetChildElementBoolean(XmlElement ParentElement, string Name, bool DefaultValue)
{
string Value = GetChildElementString(ParentElement, Name, null);
if(Value == null)
{
return DefaultValue;
}
else if(Value.Equals("True", StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
else if(Value.Equals("False", StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
else
{
return DefaultValue;
}
}
///
/// Evaluate whether the optional MSBuild condition on an XML element evaluates to true. Currently only supports 'ABC' == 'DEF' style expressions, but can be expanded as needed.
///
/// The XML element to check
/// Dictionary mapping from property names to values.
///
static bool EvaluateCondition(XmlElement Element, Dictionary Properties)
{
// Read the condition attribute. If it's not present, assume it evaluates to true.
string Condition = Element.GetAttribute("Condition");
if(String.IsNullOrEmpty(Condition))
{
return true;
}
// Expand all the properties
Condition = ExpandProperties(Condition, Properties);
// Tokenize the condition
string[] Tokens = Tokenize(Condition);
// Try to evaluate it. We only support a very limited class of condition expressions at the moment, but it's enough to parse standard projects
bool bResult;
if(Tokens.Length == 3 && Tokens[0].StartsWith("'") && Tokens[1] == "==" && Tokens[2].StartsWith("'"))
{
bResult = String.Compare(Tokens[0], Tokens[2], StringComparison.InvariantCultureIgnoreCase) == 0;
}
else
{
throw new AutomationException("Couldn't parse condition in project file");
}
return bResult;
}
///
/// Expand MSBuild properties within a string. If referenced properties are not in this dictionary, the process' environment variables are expanded. Unknown properties are expanded to an empty string.
///
/// The input string to expand
/// Dictionary mapping from property names to values.
/// String with all properties expanded.
static string ExpandProperties(string Text, Dictionary Properties)
{
string NewText = Text;
for (int Idx = NewText.IndexOf("$("); Idx != -1; Idx = NewText.IndexOf("$(", Idx))
{
// Find the end of the variable name
int EndIdx = NewText.IndexOf(')', Idx + 2);
if (EndIdx != -1)
{
// Extract the variable name from the string
string Name = NewText.Substring(Idx + 2, EndIdx - (Idx + 2));
// Find the value for it, either from the dictionary or the environment block
string Value;
if(!Properties.TryGetValue(Name, out Value))
{
Value = Environment.GetEnvironmentVariable(Name) ?? "";
}
// Replace the variable, or skip past it
NewText = NewText.Substring(0, Idx) + Value + NewText.Substring(EndIdx + 1);
// Make sure we skip over the expanded variable; we don't want to recurse on it.
Idx += Value.Length;
}
}
return NewText;
}
///
/// Split an MSBuild condition into tokens
///
/// The condition expression
/// Array of the parsed tokens
static string[] Tokenize(string Condition)
{
List Tokens = new List();
for(int Idx = 0; Idx < Condition.Length; Idx++)
{
if(Idx + 1 < Condition.Length && Condition[Idx] == '=' && Condition[Idx + 1] == '=')
{
// "==" operator
Idx++;
Tokens.Add("==");
}
else if(Condition[Idx] == '\'')
{
// Quoted string
int StartIdx = Idx++;
while(Idx + 1 < Condition.Length && Condition[Idx] != '\'')
{
Idx++;
}
Tokens.Add(Condition.Substring(StartIdx, Idx - StartIdx));
}
else if(!Char.IsWhiteSpace(Condition[Idx]))
{
// Other token; assume a single character.
string Token = Condition.Substring(Idx, 1);
Tokens.Add(Token);
}
}
return Tokens.ToArray();
}
}
}