// 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 EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Describes all of the information needed to initialize a UEBuildTarget object /// class TargetDescriptor { public static string TEST_TARGETS_SUFFIX = "Tests"; public FileReference? ProjectFile; public string Name; public UnrealTargetPlatform Platform; public UnrealTargetConfiguration Configuration; public UnrealArchitectures Architectures; public CommandLineArguments AdditionalArguments; public bool IsTestsTarget = false; public static string GetTestedName(string Name) { if (Name.EndsWith(TEST_TARGETS_SUFFIX)) { return Name.Substring(0, Name.Length - TEST_TARGETS_SUFFIX.Length); } return Name; } /// /// Foreign plugin to compile against this target /// [CommandLine("-Plugin=")] public FileReference? ForeignPlugin = null; /// /// Whether we should treat the ForeignPlugin argument as a local plugin for building purposes /// [CommandLine("-BuildPluginAsLocal")] public bool bBuildPluginAsLocal = false; /// /// When building a foreign plugin, whether to build plugins it depends on as well. /// [CommandLine("-BuildDependantPlugins")] public bool bBuildDependantPlugins = false; /// /// 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(); /// /// Relative path to file(s) to compile /// [CommandLine("-Files=", ListSeparator = ';')] public List RelativePathsToSpecificFilesToCompile = new List(); /// /// Working directory when compiling with RelativePathsToSpecificFilesToCompile /// [CommandLine("-WorkingDir=")] public string? WorkingDir = null; /// /// Will build all files that directly include any of the files provided in -SingleFile /// [CommandLine("-SingleFileBuildDependents")] public bool bSingleFileBuildDependents; /// /// 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; /// /// Whether to unify C++ code into larger files for faster compilation. /// [CommandLine("-DisableUnity", Value = "false")] public bool bUseUnityBuild = true; /// /// Whether to force C++ source files to be combined into larger files for faster compilation. /// [CommandLine("-ForceUnity")] public bool bForceUnityBuild = false; /// /// Enables "include what you use" mode. /// [CommandLine("-IWYU")] public bool bIWYU = false; /// /// Intermediate environment. Determines if the intermediates end up in a different folder than normal. /// public UnrealIntermediateEnvironment IntermediateEnvironment { get { if (IntermediateEnvironmentOverride.HasValue) { return IntermediateEnvironmentOverride.Value; } if (bIWYU) { return UnrealIntermediateEnvironment.IWYU; } if (!bUseUnityBuild && !bForceUnityBuild) { return UnrealIntermediateEnvironment.NonUnity; } return UnrealIntermediateEnvironment.Default; } set => IntermediateEnvironmentOverride = value; } private UnrealIntermediateEnvironment? IntermediateEnvironmentOverride; /// /// Constructor /// /// Path to the project file /// Name of the target to build /// Platform to build for /// Configuration to build /// Architectures to build for /// Other command-line arguments for the target public TargetDescriptor(FileReference? ProjectFile, string TargetName, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, UnrealArchitectures Architectures, CommandLineArguments? Arguments) { this.ProjectFile = ProjectFile; Name = TargetName; this.Platform = Platform; this.Configuration = Configuration; if (Architectures == null) { this.Architectures = UnrealArchitectureConfig.ForPlatform(Platform).ActiveArchitectures(ProjectFile, TargetName); } else { this.Architectures = Architectures; } // 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.Where(x => !String.IsNullOrWhiteSpace(x))) { SpecificFilesToCompile.Add(FileReference.Combine(Unreal.RootDirectory, File)); } } // Create the full path for the files specified in RelativePathsToSpecificFilesToCompile DirectoryReference CurrentWorkingDir = Unreal.RootDirectory; if (WorkingDir != null) { CurrentWorkingDir = new DirectoryReference(WorkingDir); } foreach (string RelativeFilePath in RelativePathsToSpecificFilesToCompile) { SpecificFilesToCompile.Add(FileReference.Combine(CurrentWorkingDir, RelativeFilePath)); } // 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 TargetDescriptor Copy() { return (TargetDescriptor)MemberwiseClone(); } public static TargetDescriptor FromTargetInfo(TargetInfo Info) { return new TargetDescriptor(Info.ProjectFile, Info.Name, Info.Platform, Info.Configuration, Info.Architectures, Info.Arguments); } /// /// Parse a list of target descriptors from the command line /// /// Command-line arguments /// Build configuration to get common flags from /// Logger for output /// List of target descriptors public static List ParseCommandLine(CommandLineArguments Arguments, BuildConfiguration BuildConfiguration, ILogger Logger) { List TargetDescriptors = new List(); ParseCommandLine(Arguments, BuildConfiguration.bUsePrecompiled, BuildConfiguration.bSkipRulesCompile, BuildConfiguration.bForceRulesCompile, TargetDescriptors, Logger); // apply the intermediate environment from the build configuration if (BuildConfiguration.IntermediateEnvironment.HasValue) { TargetDescriptors.ForEach(x => x.IntermediateEnvironment = BuildConfiguration.IntermediateEnvironment.Value); } return TargetDescriptors; } /// /// Parse a list of target descriptors from the command line /// /// Command-line arguments /// Logger for output /// List of target descriptors public static List ParseCommandLine(CommandLineArguments Arguments, ILogger Logger) { List TargetDescriptors = new List(); ParseCommandLine(Arguments, false, false, false, TargetDescriptors, Logger); 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 all rules assemblies /// Logger for output /// List of target descriptors public static List ParseCommandLine(CommandLineArguments Arguments, bool bUsePrecompiled, bool bSkipRulesCompile, bool bForceRulesCompile, ILogger Logger) { List TargetDescriptors = new List(); ParseCommandLine(Arguments, bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile, TargetDescriptors, Logger); return TargetDescriptors; } public static IEnumerable ParseCommandLineForProjects(CommandLineArguments Arguments, ILogger Logger) { List SimplifiedTargetDescriptors = new List(); ParseCommandLine(Arguments, false, true, false, bCreateSimplifiedDescriptors: true, SimplifiedTargetDescriptors, Logger); return SimplifiedTargetDescriptors.Select(x => x.ProjectFile); } /// /// 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 /// Logger for output public static void ParseCommandLine(CommandLineArguments Arguments, bool bUsePrecompiled, bool bSkipRulesCompile, bool bForceRulesCompile, List TargetDescriptors, ILogger Logger) { // call the internal one ParseCommandLine(Arguments, bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile, bCreateSimplifiedDescriptors: false, TargetDescriptors, Logger); } private static void ParseCommandLine(CommandLineArguments Arguments, bool bUsePrecompiled, bool bSkipRulesCompile, bool bForceRulesCompile, bool bCreateSimplifiedDescriptors, List TargetDescriptors, ILogger Logger) { 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, bCreateSimplifiedDescriptors, TargetDescriptors, Logger); } } } foreach (string Target in Targets) { CommandLineArguments NewArguments = Arguments.Append(CommandLineArguments.Split(Target)); ParseCommandLine(NewArguments, bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile, bCreateSimplifiedDescriptors, TargetDescriptors, Logger); } } else { // Otherwise just process the whole command line together ParseSingleCommandLine(Arguments, bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile, bCreateSimplifiedDescriptors, TargetDescriptors, Logger); } } /// /// 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 /// Logger for output public static void ParseSingleCommandLine(CommandLineArguments Arguments, bool bUsePrecompiled, bool bSkipRulesCompile, bool bForceRulesCompile, List TargetDescriptors, ILogger Logger) { // call the internal one ParseSingleCommandLine(Arguments, bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile, bCreateSimplifiedDescriptors: false, TargetDescriptors, Logger); } private static void ParseSingleCommandLine(CommandLineArguments Arguments, bool bUsePrecompiled, bool bSkipRulesCompile, bool bForceRulesCompile, bool bCreateSimplifiedDescriptors, List TargetDescriptors, ILogger Logger) { 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 a single simple descriptor with out a lot of processing or rules assembly creation if (bCreateSimplifiedDescriptors) { // we expect either a targetname from the above loop, or -targetype combined with -project, so we should have -project or a TargetName already string? TargetName = TargetNames.FirstOrDefault(); if (TargetName == null) { if (ProjectFile == null) { Logger.LogInformation("When looking for per-project SDK overrides, we got a commandline that could not be used to find a target project ({CmdLine})", Arguments.ToString()); return; } // just assume Game target type, it doesn't matter TargetName = ProjectFile.GetFileNameWithoutAnyExtensions() + "Game"; } if (ProjectFile == null) { NativeProjects.TryGetProjectForTarget(TargetName, Logger, out ProjectFile); } TargetDescriptors.Add(new TargetDescriptor(ProjectFile, TargetName, Platforms.First(), Configurations.First(), new UnrealArchitectures(UnrealArch.X64), Arguments)); // we are done now with simple descriptors return; } // 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.Distinct()) { // 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); } } UEBuildPlatform BuildPlatform = UEBuildPlatform.GetBuildPlatform(Platform); // Parse the architecture parameter, or use null to look up platform defaults later string ParamArchitectureList = Arguments.GetStringOrDefault("-Architecture=", "") + Arguments.GetStringOrDefault("-Architectures=", ""); UnrealArchitectures? ParamArchitectures = UnrealArchitectures.FromString(ParamArchitectureList, Platform); foreach (UnrealTargetConfiguration Configuration in Configurations.Distinct()) { // 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, Logger).GetTargetNameByType(TargetType, Platform, Configuration, null, Logger)); } else { TargetNames.Add(RulesCompiler.CreateProjectRulesAssembly(ProjectFile, bUsePrecompiled, bSkipRulesCompile, bForceRulesCompile, Logger).GetTargetNameByType(TargetType, Platform, Configuration, ProjectFile, Logger)); } } // 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) { FileReference? TargetProjectFile = ProjectFile; UnrealArchitectures Architectures; // If a project file was not specified see if we can find one if (TargetProjectFile == null && NativeProjects.TryGetProjectForTarget(TargetName, Logger, out TargetProjectFile)) { Logger.LogDebug("Found project file for {TargetName} - {ProjectFile}", TargetName, TargetProjectFile); } // Programs can have a .uproject without finding a matching .Target (since the source and metadata directories are split up) if (TargetProjectFile == null) { // find one with a matching name TargetProjectFile = NativeProjects.EnumerateProjectFiles(Logger) .Where(x => x.GetFileNameWithoutAnyExtensions().Equals(TargetName, StringComparison.InvariantCultureIgnoreCase)) .FirstOrDefault(); } // make a temp target for hybrid content-as-code projects if (TargetProjectFile != null) { NativeProjects.ConditionalMakeTempTargetForHybridProject(TargetProjectFile, Logger); } if (ParamArchitectures != null) { Architectures = ParamArchitectures; } else { // ask the platform what achitectures it wants for this project Architectures = BuildPlatform.ArchitectureConfig.ActiveArchitectures(TargetProjectFile, TargetName); } // If the platform wants a target for each architecture, make a target descriptor for each architecture, otherwise one target for all architectures if (BuildPlatform.ArchitectureConfig.Mode == UnrealArchitectureMode.OneTargetPerArchitecture) { foreach (UnrealArch Architecture in Architectures.Architectures) { TargetDescriptors.Add(new TargetDescriptor(TargetProjectFile, TargetName, Platform, Configuration, new UnrealArchitectures(Architecture), Arguments)); } } else { TargetDescriptors.Add(new TargetDescriptor(TargetProjectFile, TargetName, Platform, Configuration, Architectures, Arguments)); } } } } // Register any found descriptors for telemetry, using the first as the primary target TelemetryService.Get().SetPrimaryTargetDetails(TargetDescriptors.FirstOrDefault()); TargetDescriptors.ForEach(x => TelemetryService.Get().AddEndpointsFromConfig(x.ProjectFile?.Directory)); } /// /// 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 (Unreal.IsProjectInstalled()) { ProjectFile = Unreal.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); Result.AppendFormat($" -Architecture={Architectures}"); 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() { #pragma warning disable RS1024 return String.GetHashCode(ProjectFile?.FullName) + Name.GetHashCode() + Platform.GetHashCode() + Configuration.GetHashCode() + Architectures.GetHashCode(); #pragma warning restore RE1024 } 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 && Architectures.Architectures.SequenceEqual(Other.Architectures.Architectures); } return false; } } }