// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Xml;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using UnrealBuildBase;
namespace AutomationTool.Tasks
{
///
/// Parameters for a task that compiles a C# project
///
public class CsCompileTaskParameters
{
///
/// The C# project file to compile. Using semicolons, more than one project file can be specified.
///
[TaskParameter]
public string Project { get; set; }
///
/// The configuration to compile.
///
[TaskParameter(Optional = true)]
public string Configuration { get; set; }
///
/// The platform to compile.
///
[TaskParameter(Optional = true)]
public string Platform { get; set; }
///
/// The target to build.
///
[TaskParameter(Optional = true)]
public string Target { get; set; }
///
/// Properties for the command
///
[TaskParameter(Optional = true)]
public string Properties { get; set; }
///
/// Additional options to pass to the compiler.
///
[TaskParameter(Optional = true)]
public string Arguments { get; set; }
///
/// Only enumerate build products -- do not actually compile the projects.
///
[TaskParameter(Optional = true)]
public bool EnumerateOnly { get; set; }
///
/// Tag to be applied to build products of this task.
///
[TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.TagList)]
public string Tag { get; set; }
///
/// Tag to be applied to any non-private references the projects have.
/// (for example, those that are external and not copied into the output directory).
///
[TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.TagList)]
public string TagReferences { get; set; }
///
/// Whether to use the system toolchain rather than the bundled UE SDK
///
[TaskParameter(Optional = true)]
public bool UseSystemCompiler { get; set; }
}
///
/// Compiles C# project files, and their dependencies.
///
[TaskElement("CsCompile", typeof(CsCompileTaskParameters))]
public class CsCompileTask : BgTaskImpl
{
readonly CsCompileTaskParameters _parameters;
///
/// Constructor.
///
/// Parameters for this task
public CsCompileTask(CsCompileTaskParameters 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 Task ExecuteAsync(JobContext job, HashSet buildProducts, Dictionary> tagNameToFileSet)
{
// Get the project file
HashSet projectFiles = ResolveFilespec(Unreal.RootDirectory, _parameters.Project, tagNameToFileSet);
foreach (FileReference projectFile in projectFiles)
{
if (!FileReference.Exists(projectFile))
{
throw new AutomationException("Couldn't find project file '{0}'", projectFile.FullName);
}
if (!projectFile.HasExtension(".csproj"))
{
throw new AutomationException("File '{0}' is not a C# project", projectFile.FullName);
}
}
// 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;
}
if (!String.IsNullOrEmpty(_parameters.Properties))
{
foreach (string property in _parameters.Properties.Split(';'))
{
if (!String.IsNullOrWhiteSpace(property))
{
int equalsIdx = property.IndexOf('=', StringComparison.Ordinal);
if (equalsIdx == -1)
{
Logger.LogWarning("Missing '=' in property assignment");
}
else
{
properties[property.Substring(0, equalsIdx).Trim()] = property.Substring(equalsIdx + 1).Trim();
}
}
}
}
// 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);
}
if (!String.IsNullOrEmpty(_parameters.Target))
{
arguments.Add(String.Format("/target:{0}", CommandUtils.MakePathSafeToUseWithCommandLine(_parameters.Target)));
}
arguments.Add("/restore");
arguments.Add("/verbosity:minimal");
arguments.Add("/nologo");
string joinedArguments = String.Join(" ", arguments);
foreach (FileReference projectFile in projectFiles)
{
if (!FileReference.Exists(projectFile))
{
throw new AutomationException("Project {0} does not exist!", projectFile);
}
if (_parameters.UseSystemCompiler)
{
CommandUtils.MsBuild(CommandUtils.CmdEnv, projectFile.FullName, joinedArguments, null);
}
else
{
CommandUtils.RunAndLog(CommandUtils.CmdEnv, CommandUtils.CmdEnv.DotnetMsbuildPath, $"msbuild {CommandUtils.MakePathSafeToUseWithCommandLine(projectFile.FullName)} {joinedArguments}");
}
}
}
// Try to figure out the output files
HashSet projectBuildProducts;
HashSet projectReferences;
properties["EngineDirectory"] = Unreal.EngineDirectory.FullName;
FindBuildProductsAndReferences(projectFiles, properties, out projectBuildProducts, out projectReferences);
// Apply the optional tag to the produced archive
foreach (string tagName in FindTagNamesFromList(_parameters.Tag))
{
FindOrAddTagSet(tagNameToFileSet, tagName).UnionWith(projectBuildProducts);
}
// Apply the optional tag to any references
if (!String.IsNullOrEmpty(_parameters.TagReferences))
{
foreach (string tagName in FindTagNamesFromList(_parameters.TagReferences))
{
FindOrAddTagSet(tagNameToFileSet, tagName).UnionWith(projectReferences);
}
}
// Merge them into the standard set of build products
buildProducts.UnionWith(projectBuildProducts);
buildProducts.UnionWith(projectReferences);
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()
{
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
static void FindBuildProductsAndReferences(HashSet projectFiles, Dictionary initialProperties, out HashSet outBuildProducts, out HashSet outReferences)
{
// Find all the build products and references
outBuildProducts = new HashSet();
outReferences = new HashSet();
// Read all the project information into a dictionary
Dictionary fileToProjectInfo = new Dictionary();
foreach (FileReference projectFile in projectFiles)
{
// Read all the projects
ReadProjectsRecursively(projectFile, initialProperties, fileToProjectInfo);
// Find all the outputs for each project
foreach (KeyValuePair pair in fileToProjectInfo)
{
CsProjectInfo projectInfo = pair.Value;
// Add all the build projects from this project
DirectoryReference outputDir = projectInfo.GetOutputDir(pair.Key.Directory);
projectInfo.FindBuildProducts(outputDir, outBuildProducts, fileToProjectInfo);
// Add any files which are only referenced
foreach (KeyValuePair reference in projectInfo.References)
{
CsProjectInfo.AddReferencedAssemblyAndSupportFiles(reference.Key, outReferences);
}
}
}
outBuildProducts.RemoveWhere(x => !FileReference.Exists(x));
outReferences.RemoveWhere(x => !FileReference.Exists(x));
}
///
/// 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 void ReadProjectsRecursively(FileReference file, Dictionary initialProperties, Dictionary fileToProjectInfo)
{
// Early out if we've already read this project
if (!fileToProjectInfo.ContainsKey(file))
{
// Try to read this project
CsProjectInfo projectInfo;
if (!CsProjectInfo.TryRead(file, initialProperties, out projectInfo))
{
throw new AutomationException("Couldn't read project '{0}'", file.FullName);
}
// Add it to the project lookup, and try to read all the projects it references
fileToProjectInfo.Add(file, projectInfo);
foreach (FileReference projectReference in projectInfo.ProjectReferences.Keys)
{
if (!FileReference.Exists(projectReference))
{
throw new AutomationException("Unable to find project '{0}' referenced by '{1}'", projectReference, file);
}
ReadProjectsRecursively(projectReference, initialProperties, fileToProjectInfo);
}
}
}
}
///
/// Output from compiling a csproj file
///
public class CsCompileOutput
{
///
/// Empty instance of CsCompileOutput
///
public static CsCompileOutput Empty { get; } = new CsCompileOutput(FileSet.Empty, FileSet.Empty);
///
/// Output binaries
///
public FileSet Binaries { get; }
///
/// Referenced output
///
public FileSet References { get; }
///
/// Constructor
///
public CsCompileOutput(FileSet binaries, FileSet references)
{
Binaries = binaries;
References = references;
}
///
/// Merge all outputs from this project
///
///
public FileSet Merge()
{
return Binaries + References;
}
///
/// Merges two outputs together
///
public static CsCompileOutput operator +(CsCompileOutput lhs, CsCompileOutput rhs)
{
return new CsCompileOutput(lhs.Binaries + rhs.Binaries, lhs.References + rhs.References);
}
}
///
/// Extension methods for csproj compilation
///
public static class CsCompileOutputExtensions
{
///
///
///
///
///
public static async Task MergeAsync(this Task task)
{
return (await task).Merge();
}
}
public static partial class StandardTasks
{
///
/// Compile a C# project
///
/// The C# project files to compile.
/// The platform to compile.
/// The configuration to compile.
/// The target to build.
/// Properties for the command.
/// Additional options to pass to the compiler.
/// Only enumerate build products -- do not actually compile the projects.
public static async Task CsCompileAsync(FileReference project, string platform = null, string configuration = null, string target = null, string properties = null, string arguments = null, bool? enumerateOnly = null)
{
CsCompileTaskParameters parameters = new CsCompileTaskParameters();
parameters.Project = project.FullName;
parameters.Platform = platform;
parameters.Configuration = configuration;
parameters.Target = target;
parameters.Properties = properties;
parameters.Arguments = arguments;
parameters.EnumerateOnly = enumerateOnly ?? parameters.EnumerateOnly;
parameters.Tag = "#Out";
parameters.TagReferences = "#Refs";
HashSet buildProducts = new HashSet();
Dictionary> tagNameToFileSet = new Dictionary>();
await new CsCompileTask(parameters).ExecuteAsync(new JobContext(null!, null!), buildProducts, tagNameToFileSet);
FileSet binaries = FileSet.Empty;
FileSet references = FileSet.Empty;
if (tagNameToFileSet.TryGetValue(parameters.Tag, out HashSet binaryFiles))
{
binaries = FileSet.FromFiles(Unreal.RootDirectory, binaryFiles);
}
if (tagNameToFileSet.TryGetValue(parameters.TagReferences, out HashSet referenceFiles))
{
references = FileSet.FromFiles(Unreal.RootDirectory, referenceFiles);
}
return new CsCompileOutput(binaries, references);
}
}
}