// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using EpicGames.Core; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Describes all of the information needed to initialize a UEBuildTarget object /// class TargetDescriptor { public FileReference? ProjectFile; public string Name; public UnrealTargetPlatform Platform; public UnrealTargetConfiguration Configuration; public string Architecture; public CommandLineArguments AdditionalArguments; /// /// Foreign plugin to compile against this target /// [CommandLine("-Plugin=")] public FileReference? ForeignPlugin = null; /// /// Set of module names to compile. /// [CommandLine("-Module=")] public HashSet OnlyModuleNames = new HashSet(StringComparer.OrdinalIgnoreCase); /// /// Lists of files to compile /// [CommandLine("-FileList=")] public List FileLists = new List(); /// /// Individual file(s) to compile /// [CommandLine("-File=")] [CommandLine("-SingleFile=")] public List SpecificFilesToCompile = new List(); /// /// Whether to perform hot reload for this target /// [CommandLine("-NoHotReload", Value = nameof(HotReloadMode.Disabled))] [CommandLine("-ForceHotReload", Value = nameof(HotReloadMode.FromIDE))] [CommandLine("-LiveCoding", Value = nameof(HotReloadMode.LiveCoding))] public HotReloadMode HotReloadMode = HotReloadMode.Default; /// /// Map of module name to suffix for hot reloading from the editor /// public Dictionary HotReloadModuleNameToSuffix = new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// Path to a file containing a list of modules that may be modified for live coding. /// [CommandLine("-LiveCodingModules=")] public FileReference? LiveCodingModules = null; /// /// Path to the manifest for passing info about the output to live coding /// [CommandLine("-LiveCodingManifest=")] public FileReference? LiveCodingManifest = null; /// /// If a non-zero value, a live coding request will be terminated if more than the given number of actions are required. /// [CommandLine("-LiveCodingLimit=")] public uint LiveCodingLimit = 0; /// /// Suppress messages about building this target /// [CommandLine("-Quiet")] public bool bQuiet; /// /// Clean the target before trying to build it /// [CommandLine("-Rebuild")] public bool bRebuild; /// /// Constructor /// /// Path to the project file /// Name of the target to build /// Platform to build for /// Configuration to build /// Architecture to build for /// Other command-line arguments for the target public TargetDescriptor(FileReference? ProjectFile, string TargetName, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, string Architecture, CommandLineArguments? Arguments) { this.ProjectFile = ProjectFile; this.Name = TargetName; this.Platform = Platform; this.Configuration = Configuration; this.Architecture = Architecture; // If there are any additional command line arguments List AdditionalArguments = new List(); if(Arguments != null) { // Apply the arguments to this object Arguments.ApplyTo(this); // Read the file lists foreach (FileReference FileList in FileLists) { string[] Files = FileReference.ReadAllLines(FileList); foreach (string File in Files) { if (!String.IsNullOrWhiteSpace(File)) { SpecificFilesToCompile.Add(new FileReference(File)); } } } // Parse all the hot-reload module names foreach(string ModuleWithSuffix in Arguments.GetValues("-ModuleWithSuffix=")) { int SuffixIdx = ModuleWithSuffix.LastIndexOf(','); if(SuffixIdx == -1) { throw new BuildException("Missing suffix argument from -ModuleWithSuffix=Name,Suffix"); } string ModuleName = ModuleWithSuffix.Substring(0, SuffixIdx); int Suffix; if(!Int32.TryParse(ModuleWithSuffix.Substring(SuffixIdx + 1), out Suffix)) { throw new BuildException("Suffix for modules must be an integer"); } HotReloadModuleNameToSuffix[ModuleName] = Suffix; } // Pull out all the arguments that haven't been used so far for(int Idx = 0; Idx < Arguments.Count; Idx++) { if(!Arguments.HasBeenUsed(Idx)) { AdditionalArguments.Add(Arguments[Idx]); } } } this.AdditionalArguments = new CommandLineArguments(AdditionalArguments.ToArray()); } public static TargetDescriptor FromTargetInfo(TargetInfo Info) { return new TargetDescriptor(Info.ProjectFile, Info.Name, Info.Platform, Info.Configuration, Info.Architecture, Info.Arguments); } /// /// Parse a list of target descriptors from the command line /// /// Command-line arguments /// Whether to use a precompiled engine distribution /// Whether to skip compiling rules assemblies /// Whether to always compile all rules assemblies /// List of target descriptors public static List ParseCommandLine(CommandLineArguments Arguments, bool bUsePrecompiled, bool bSkipRulesCompile, bool bForceRulesCompile) { List TargetDescriptors = new List(); ParseCommandLine(Arguments, bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile, TargetDescriptors); return TargetDescriptors; } /// /// Parse a list of target descriptors from the command line /// /// Command-line arguments /// Whether to use a precompiled engine distribution /// Whether to skip compiling rules assemblies /// Whether to always compile rules assemblies /// Receives the list of parsed target descriptors public static void ParseCommandLine(CommandLineArguments Arguments, bool bUsePrecompiled, bool bSkipRulesCompile, bool bForceRulesCompile, List TargetDescriptors) { List TargetLists; Arguments = Arguments.Remove("-TargetList=", out TargetLists); List Targets; Arguments = Arguments.Remove("-Target=", out Targets); if(TargetLists.Count > 0 || Targets.Count > 0) { // Try to parse multiple arguments from a single command line foreach(string TargetList in TargetLists) { string[] Lines = File.ReadAllLines(TargetList); foreach(string Line in Lines) { string TrimLine = Line.Trim(); if(TrimLine.Length > 0 && TrimLine[0] != ';') { CommandLineArguments NewArguments = Arguments.Append(CommandLineArguments.Split(TrimLine)); ParseCommandLine(NewArguments, bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile, TargetDescriptors); } } } foreach(string Target in Targets) { CommandLineArguments NewArguments = Arguments.Append(CommandLineArguments.Split(Target)); ParseCommandLine(NewArguments, bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile, TargetDescriptors); } } else { // Otherwise just process the whole command line together ParseSingleCommandLine(Arguments, bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile, TargetDescriptors); } } /// /// Parse a list of target descriptors from the command line /// /// Command-line arguments /// Whether to use a precompiled engine distribution /// Whether to skip compiling rules assemblies /// Whether to always compile all rules assemblies /// List of target descriptors public static void ParseSingleCommandLine(CommandLineArguments Arguments, bool bUsePrecompiled, bool bSkipRulesCompile, bool bForceRulesCompile, List TargetDescriptors) { List Platforms = new List(); List Configurations = new List(); List TargetNames = new List(); FileReference? ProjectFile = Arguments.GetFileReferenceOrDefault("-Project=", null); // Settings for creating/using static libraries for the engine for (int ArgumentIndex = 0; ArgumentIndex < Arguments.Count; ArgumentIndex++) { string Argument = Arguments[ArgumentIndex]; if(Argument.Length > 0 && Argument[0] != '-') { // Mark this argument as used. We'll interpret it as one thing or another. Arguments.MarkAsUsed(ArgumentIndex); // Check if it's a project file argument if(Argument.EndsWith(".uproject", StringComparison.OrdinalIgnoreCase)) { FileReference NewProjectFile = new FileReference(Argument); if(ProjectFile != null && ProjectFile != NewProjectFile) { throw new BuildException("Multiple project files specified on command line (first {0}, then {1})", ProjectFile, NewProjectFile); } ProjectFile = new FileReference(Argument); continue; } // Split it into separate arguments string[] InlineArguments = Argument.Split('+'); // Try to parse them as platforms UnrealTargetPlatform ParsedPlatform; if(UnrealTargetPlatform.TryParse(InlineArguments[0], out ParsedPlatform)) { Platforms.Add(ParsedPlatform); for(int InlineArgumentIdx = 1; InlineArgumentIdx < InlineArguments.Length; InlineArgumentIdx++) { Platforms.Add(UnrealTargetPlatform.Parse(InlineArguments[InlineArgumentIdx])); } continue; } // Try to parse them as configurations UnrealTargetConfiguration ParsedConfiguration; if(Enum.TryParse(InlineArguments[0], true, out ParsedConfiguration)) { Configurations.Add(ParsedConfiguration); for(int InlineArgumentIdx = 1; InlineArgumentIdx < InlineArguments.Length; InlineArgumentIdx++) { string InlineArgument = InlineArguments[InlineArgumentIdx]; if(!Enum.TryParse(InlineArgument, true, out ParsedConfiguration)) { throw new BuildException("Invalid configuration '{0}'", InlineArgument); } Configurations.Add(ParsedConfiguration); } continue; } // Otherwise assume they are target names TargetNames.AddRange(InlineArguments); } } if (Platforms.Count == 0) { throw new BuildException("No platforms specified for target"); } if (Configurations.Count == 0) { throw new BuildException("No configurations specified for target"); } // Make sure the project file exists, and make sure we're using the correct case. if(ProjectFile != null) { FileInfo ProjectFileInfo = FileUtils.FindCorrectCase(ProjectFile.ToFileInfo()); if(!ProjectFileInfo.Exists) { throw new BuildException("Unable to find project '{0}'.", ProjectFile); } ProjectFile = new FileReference(ProjectFileInfo); } // Expand all the platforms, architectures and configurations foreach(UnrealTargetPlatform Platform in Platforms) { // Make sure the platform is valid if (!InstalledPlatformInfo.IsValid(null, Platform, null, EProjectType.Code, InstalledPlatformState.Downloaded)) { if (!InstalledPlatformInfo.IsValid(null, Platform, null, EProjectType.Code, InstalledPlatformState.Supported)) { throw new BuildException("The {0} platform is not supported from this engine distribution.", Platform); } else { throw new BuildException("Missing files required to build {0} targets. Enable {0} as an optional download component in the Epic Games Launcher.", Platform); } } // Parse the architecture parameter, or get the default for the platform List Architectures = new List(Arguments.GetValues("-Architecture=", '+')); if (Architectures.Count == 0) { Architectures.Add(UEBuildPlatform.GetBuildPlatform(Platform).GetDefaultArchitecture(ProjectFile)); } else { // If the platform can do these in a single pass then turn them back into a single + separate string. // It is now responsible for splitting them as necessary. if (UEBuildPlatform.GetBuildPlatform(Platform).CanBuildArchitecturesInSinglePass(Architectures)) { Architectures = new List { string.Join("+", Architectures.OrderBy(S => S)) }; } } foreach(string Architecture in Architectures) { foreach(UnrealTargetConfiguration Configuration in Configurations) { // Create all the target descriptors for targets specified by type foreach(string TargetTypeString in Arguments.GetValues("-TargetType=")) { TargetType TargetType; if(!Enum.TryParse(TargetTypeString, out TargetType)) { throw new BuildException("Invalid target type '{0}'", TargetTypeString); } if (ProjectFile == null) { TargetNames.Add(RulesCompiler.CreateEngineRulesAssembly(bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile).GetTargetNameByType(TargetType, Platform, Configuration, Architecture, null)); } else { TargetNames.Add(RulesCompiler.CreateProjectRulesAssembly(ProjectFile, bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile).GetTargetNameByType(TargetType, Platform, Configuration, Architecture, ProjectFile)); } } // Make sure we could parse something if (TargetNames.Count == 0) { throw new BuildException("No target name was specified on the command-line."); } // Create all the target descriptors foreach(string TargetName in TargetNames) { // If a project file was not specified see if we can find one if (ProjectFile == null && NativeProjects.TryGetProjectForTarget(TargetName, out ProjectFile)) { Log.TraceVerbose("Found project file for {0} - {1}", TargetName, ProjectFile); } // Create the target descriptor TargetDescriptors.Add(new TargetDescriptor(ProjectFile, TargetName, Platform, Configuration, Architecture, Arguments)); } } } } } /// /// Try to parse the project file from the command line /// /// The command line arguments /// The project file that was parsed /// True if the project file was parsed, false otherwise public static bool TryParseProjectFileArgument(CommandLineArguments Arguments, [NotNullWhen(true)] out FileReference? ProjectFile) { FileReference? ExplicitProjectFile; if(Arguments.TryGetValue("-Project=", out ExplicitProjectFile)) { ProjectFile = ExplicitProjectFile; return true; } for(int Idx = 0; Idx < Arguments.Count; Idx++) { if(Arguments[Idx][0] != '-' && Arguments[Idx].EndsWith(".uproject", StringComparison.OrdinalIgnoreCase)) { Arguments.MarkAsUsed(Idx); ProjectFile = new FileReference(Arguments[Idx]); return true; } } if(UnrealBuildTool.IsProjectInstalled()) { ProjectFile = UnrealBuildTool.GetInstalledProjectFile()!; return true; } ProjectFile = null; return false; } /// /// Format this object for the debugger /// /// String representation of this target descriptor public override string ToString() { StringBuilder Result = new StringBuilder(); Result.AppendFormat("{0} {1} {2}", Name, Platform, Configuration); if(!String.IsNullOrEmpty(Architecture)) { Result.AppendFormat(" -Architecture={0}", Architecture); } if(ProjectFile != null) { Result.AppendFormat(" -Project={0}", Utils.MakePathSafeToUseWithCommandLine(ProjectFile)); } if(AdditionalArguments != null && AdditionalArguments.Count > 0) { Result.AppendFormat(" {0}", AdditionalArguments); } return Result.ToString(); } public override int GetHashCode() { return String.GetHashCode(ProjectFile?.FullName) + Name.GetHashCode() + Platform.GetHashCode() + Configuration.GetHashCode() + Architecture.GetHashCode(); } public override bool Equals(object? Obj) { TargetDescriptor? Other = Obj as TargetDescriptor; if (Other != null) { return ProjectFile == Other.ProjectFile && Name == Other.Name && Platform == Other.Platform && Configuration == Other.Configuration && Architecture == Other.Architecture; } return false; } } }