// 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); } } }