// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.BuildGraph; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml; using EpicGames.Core; using UnrealBuildBase; using UnrealBuildTool; using Microsoft.Extensions.Logging; 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; /// /// The configuration to compile. /// [TaskParameter(Optional = true)] public string Configuration; /// /// The platform to compile. /// [TaskParameter(Optional = true)] public string Platform; /// /// The target to build. /// [TaskParameter(Optional = true)] public string Target; /// /// Properties for the command /// [TaskParameter(Optional = true)] public string Properties; /// /// Additional options to pass to the compiler. /// [TaskParameter(Optional = true)] public string Arguments; /// /// 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. /// (for example, those that are external and not copied into the output directory). /// [TaskParameter(Optional = true, ValidationType = TaskParameterValidationType.TagList)] public string TagReferences; /// /// Whether to use the system toolchain rather than the bundled UE SDK /// [TaskParameter(Optional = true)] public bool UseSystemCompiler; } /// /// Compiles C# project files, and their dependencies. /// [TaskElement("CsCompile", typeof(CsCompileTaskParameters))] public class CsCompileTask : BgTaskImpl { /// /// 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 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('='); 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))); } if(!CommandUtils.CmdEnv.FrameworkMsbuildPath.Equals("xbuild")) { // not supported by xbuild 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; 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) { this.Binaries = Binaries; this.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!), 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); } } }