// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using EpicGames.Core; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using OpenTracing.Util; using UnrealBuildBase; namespace UnrealBuildTool { // TODO: Namespacing? enum QueryType { Capabilities, AvailableTargets, TargetDetails, TargetModuleReferences, } internal class TargetIntellisenseInfo { internal class CompileSettings { public List IncludePaths { get; set; } = new(); public List Defines { get; set; } = new(); public string? Standard { get; set; } public List ForcedIncludes { get; set; } = new(); public string? CompilerPath { get; set; } public List CompilerArgs { get; set; } = new(); public Dictionary> CompilerAdditionalArgs { get; set; } = new(); public string? WindowsSdkVersion { get; set; } } public ConcurrentDictionary ModuleToCompileSettings = new(); public ConcurrentDictionary DirToModule = new(); public UEBuildModule? FindModuleForFile(FileReference File) { DirectoryReference? Dir = File.Directory; while (Dir != null) { if (DirToModule.TryGetValue(Dir, out UEBuildModule? Module)) { return Module; } Dir = Dir.ParentDirectory; } return null; } public UEBuildModule? FindModuleForDirectory(DirectoryReference Directory) { DirectoryReference? Dir = Directory; while (Dir != null) { if (DirToModule.TryGetValue(Dir, out UEBuildModule? Module)) { return Module; } Dir = Dir.ParentDirectory; } return null; } } internal class LaunchSettings { public string? Description { get; set; } public string? BinaryPath { get; set; } public List Arguments { get; set; } = new(); } internal class TargetConfigs { public string TargetPath { get; set; } = String.Empty; public string ProjectPath { get; set; } = String.Empty; public string TargetType { get; set; } = String.Empty; public List Platforms { get; set; } = new(); public List Configurations { get; set; } = new(); } [ToolMode("Query", ToolModeOptions.BuildPlatforms | ToolModeOptions.XmlConfig | ToolModeOptions.UseStartupTraceListener)] class QueryMode : ToolMode { [CommandLine("-LogDirectory=")] public DirectoryReference? LogDirectory = null; [CommandLine("-OutputPath=")] public FileReference? OutputPath = null; [CommandLine("-Query=")] public QueryType? Query = null; [CommandLine("-LoadTargets")] public bool bLoadTargets = false; [CommandLine("-IncludeEngineSource=")] public bool bIncludeEngineSource = true; [CommandLine("-Target=")] public string? TargetName; [CommandLine("-Configuration=")] public string? TargetConfiguration; [CommandLine("-Platform=")] public string? TargetPlatform; [CommandLine("-Indented")] public bool bIndented; [CommandLine("-NoIntellisense")] public bool bNoIntellisense = false; private BuildConfiguration BuildConfiguration = new(); public override Task ExecuteAsync(CommandLineArguments Arguments, ILogger Logger) { Arguments.ApplyTo(this); if (LogDirectory == null) { LogDirectory = DirectoryReference.Combine(Unreal.EngineProgramSavedDirectory, "UnrealBuildTool", "QueryMode"); } DirectoryReference.CreateDirectory(LogDirectory); string LogFileName; switch (Query) { case QueryType.TargetDetails: LogFileName = $"Log_{TargetName}_{TargetConfiguration}_{TargetPlatform}.txt"; break; default: LogFileName = $"Log_{Query}.txt"; break; } FileReference LogFile = FileReference.Combine(LogDirectory, LogFileName); Log.AddFileWriter("DefaultLogTraceListener", LogFile); XmlConfig.ApplyTo(BuildConfiguration); Arguments.ApplyTo(BuildConfiguration); // TODO: Document this hack ProjectFileGenerator.bGenerateProjectFiles = true; if (Query == null) { return Task.FromResult(0); } JsonSerializerOptions ResponseOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = bIndented, }; switch (Query) { case QueryType.Capabilities: Logger.LogInformation("QueryCapabilities"); return QueryCapabilities(Arguments, Logger, ResponseOptions); case QueryType.AvailableTargets: Logger.LogInformation("QueryAvailableTargets"); return QueryAvailableTargetsAsync(Arguments, Logger, ResponseOptions); case QueryType.TargetDetails: Logger.LogInformation("QueryTargetDetails"); return QueryTargetDetails(Arguments, Logger, ResponseOptions); case QueryType.TargetModuleReferences: Logger.LogInformation("QueryTargetModuleReferences"); return QueryTargetModuleReferences(Arguments, Logger, ResponseOptions); } return Task.FromResult(0); } private async Task WriteResultsAsync(object? Value, JsonSerializerOptions JsonOptions, ILogger Logger) { if (OutputPath == null) { Console.WriteLine(JsonSerializer.Serialize(Value, JsonOptions)); return; } using FileStream Stream = new FileStream(OutputPath.FullName, FileMode.Create, FileAccess.Write); Logger.LogInformation("Writing {File}...", OutputPath.FullName); await JsonSerializer.SerializeAsync(Stream, Value, JsonOptions); } private async Task QueryCapabilities(CommandLineArguments Arguments, ILogger Logger, JsonSerializerOptions JsonOptions) { var Reply = new { Queries = new List { QueryType.Capabilities.ToString(), QueryType.AvailableTargets.ToString(), QueryType.TargetDetails.ToString(), QueryType.TargetModuleReferences.ToString() } }; await WriteResultsAsync(Reply, JsonOptions, Logger); return 0; } private async Task QueryAvailableTargetsAsync(CommandLineArguments Arguments, ILogger Logger, JsonSerializerOptions JsonOptions) { try { Utils.TryParseProjectFileArgument(Arguments, Logger, out FileReference? ProjectFileArg); List Platforms = new(); foreach (UnrealTargetPlatform Platform in UnrealTargetPlatform.GetValidPlatforms()) { // If there is a build platform present, add it to the SupportedPlatforms list UEBuildPlatform? BuildPlatform; if (UEBuildPlatform.TryGetBuildPlatform(Platform, out BuildPlatform)) { if (InstalledPlatformInfo.IsValidPlatform(Platform, EProjectType.Code)) { Platforms.Add(Platform); } } } List AllowedTargetConfigurations = new List(); AllowedTargetConfigurations = Enum.GetValues(typeof(UnrealTargetConfiguration)).Cast().ToList(); List Configurations = new(); foreach (UnrealTargetConfiguration CurConfiguration in AllowedTargetConfigurations) { if (CurConfiguration != UnrealTargetConfiguration.Unknown) { if (InstalledPlatformInfo.IsValidConfiguration(CurConfiguration, EProjectType.Code)) { Configurations.Add(CurConfiguration); } } } List Projects = ProjectFileArg != null ? new List(new[] { ProjectFileArg }) : NativeProjects.EnumerateProjectFiles(Logger).ToList(); List AllTargetFiles = ProjectFileGenerator.DiscoverTargets(Projects, Logger, null, Platforms, bIncludeEngineSource: bIncludeEngineSource, bIncludeTempTargets: false); Dictionary Targets = new(); string? DefaultTarget = null; foreach (FileReference TargetFilePath in AllTargetFiles) { try { FileReference? ProjectPath = Projects.FirstOrDefault(p => TargetFilePath.IsUnderDirectory(p.Directory)); string TargetType = String.Empty; List SupportedConfigurations = new(Configurations); List SupportedPlatforms = new(Platforms); if (bLoadTargets) { List RawArgs = new List { TargetFilePath.GetFileNameWithoutAnyExtensions(), UnrealTargetConfiguration.Development.ToString(), UnrealTargetPlatform.Win64.ToString() }; if (ProjectPath != null) { RawArgs.Add($"-Project={ProjectPath.FullName}"); } CommandLineArguments Args = new CommandLineArguments(RawArgs.ToArray()); List TargetDescriptors = new(); TargetDescriptor.ParseSingleCommandLine(Args, false, false, false, TargetDescriptors, NullLogger.Instance); // Ensure the intermediate environment does not conflict with normal builds TargetDescriptors[0].IntermediateEnvironment = UnrealIntermediateEnvironment.Query; UEBuildTarget CurrentTarget; using (GlobalTracer.Instance.BuildSpan("UEBuildTarget.Create()").StartActive()) { bool bUsePrecompiled = false; // Prevent multiple conflicting processes building TargetRules at the same time string MutexName = GlobalSingleInstanceMutex.GetUniqueMutexForPath("UnrealBuildTool_QueryMode_UEBuildTarget-Create", Unreal.RootDirectory); using (new GlobalSingleInstanceMutex(MutexName, true)) { CurrentTarget = UEBuildTarget.Create(TargetDescriptors[0], false, false, bUsePrecompiled, TargetDescriptors[0].IntermediateEnvironment, NullLogger.Instance); } } TargetType = CurrentTarget.Rules.Type.ToString(); SupportedConfigurations = CurrentTarget.Rules.SupportedConfigurations.ToList(); SupportedPlatforms = CurrentTarget.Rules.SupportedPlatforms.ToList(); } string TargetName = TargetFilePath.GetFileNameWithoutAnyExtensions(); Targets.Add(TargetName, new TargetConfigs() { TargetPath = TargetFilePath.FullName, ProjectPath = ProjectPath?.ToString() ?? String.Empty, TargetType = TargetType, Configurations = SupportedConfigurations.Select(x => x.ToString()).ToList(), Platforms = SupportedPlatforms.Select(x => x.ToString()).ToList() }); if (DefaultTarget == null || TargetName == "UnrealEditor") { DefaultTarget = TargetName; } } catch (Exception) { continue; } } var Reply = new { Targets, DefaultTarget, DefaultPlatform = Platforms[0].ToString(), DefaultConfiguration = UnrealTargetConfiguration.Development.ToString(), }; await WriteResultsAsync(Reply, JsonOptions, Logger); return 0; } catch (Exception Ex) { Logger.LogError("Failed to query available targets: {Error}", Ex.Message); Logger.LogDebug(Ex, "Unhandled exception: {Ex}", ExceptionUtils.FormatExceptionDetails(Ex)); return 1; } } private async Task QueryTargetDetails(CommandLineArguments Arguments, ILogger Logger, JsonSerializerOptions JsonOptions) { if (TargetName == null) { Logger.LogError("Missing argument Target"); return 1; } else if (TargetConfiguration == null) { Logger.LogError("Missing argument Configuration"); return 1; } else if (TargetPlatform == null) { Logger.LogError("Missing argument Platform"); return 1; } Utils.TryParseProjectFileArgument(Arguments, Logger, out FileReference? ProjectFileArg); List RawArgs = new List { TargetName, TargetConfiguration, TargetPlatform }; if (ProjectFileArg != null) { RawArgs.Add(ProjectFileArg.ToString()); } RawArgs.AddRange(Arguments.GetUnusedArguments()); RawArgs.Add("-NoPCHChain"); // Currently unsupported CommandLineArguments Args = new CommandLineArguments(RawArgs.ToArray()); List TargetDescriptors = new(); TargetDescriptor.ParseSingleCommandLine(Args, false, false, false, TargetDescriptors, Logger); if (TargetDescriptors.Count == 0) { // TOOD: Error return 1; } // Ensure the intermediate environment does not conflict with normal builds TargetDescriptors[0].IntermediateEnvironment = UnrealIntermediateEnvironment.Query; try { UEBuildTarget CurrentTarget; using (GlobalTracer.Instance.BuildSpan("UEBuildTarget.Create()").StartActive()) { bool bUsePrecompiled = false; // Prevent multiple conflicting processes building TargetRules at the same time string MutexName = GlobalSingleInstanceMutex.GetUniqueMutexForPath("UnrealBuildTool_QueryMode_UEBuildTarget-Create", Unreal.RootDirectory); using (new GlobalSingleInstanceMutex(MutexName, true)) { CurrentTarget = UEBuildTarget.Create(TargetDescriptors[0], false, false, bUsePrecompiled, TargetDescriptors[0].IntermediateEnvironment, Logger); } } UEBuildBinary? LaunchBinary = CurrentTarget.Binaries.FirstOrDefault(Binary => Binary.Modules.Any(Module => Module.Name == CurrentTarget.Rules.LaunchModuleName)); if (LaunchBinary == null) { throw new BuildException("Unable to find launch binary for target"); } LaunchSettings CurrentLaunchSettings = new LaunchSettings(); CurrentLaunchSettings.Description = $"{CurrentTarget.TargetName} {CurrentTarget.Configuration} {CurrentTarget.Platform}"; CurrentLaunchSettings.BinaryPath = LaunchBinary.OutputFilePath.FullName; if (CurrentTarget.ProjectFile != null && CurrentTarget.TargetType != TargetType.Program) { CurrentLaunchSettings.Arguments.Add(CurrentTarget.ProjectFile.FullName); } TargetIntellisenseInfo? CurrentTargetIntellisenseInfo = null; if (!bNoIntellisense) { // Create the makefile for the target and export the module information { using ISourceFileWorkingSet WorkingSet = new EmptySourceFileWorkingSet(); TargetMakefile Makefile; try { Makefile = await BuildMode.CreateMakefileAsync(BuildConfiguration, TargetDescriptors[0], WorkingSet, Logger); } finally { SourceFileMetadataCache.SaveAll(); } } CurrentTargetIntellisenseInfo = new TargetIntellisenseInfo(); // Partially duplicated from UEBuildTarget.Build because we just want to get C++ compile actions without running UHT // or generating link actions / full dependency graph CppConfiguration CppConfiguration = UEBuildTarget.GetCppConfiguration(CurrentTarget.Configuration); SourceFileMetadataCache MetadataCache = SourceFileMetadataCache.CreateHierarchy(null, Logger); CppCompileEnvironment GlobalCompileEnvironment = new CppCompileEnvironment(CurrentTarget.Platform, CppConfiguration, CurrentTarget.Architectures, MetadataCache); LinkEnvironment GlobalLinkEnvironment = new LinkEnvironment(GlobalCompileEnvironment.Platform, GlobalCompileEnvironment.Configuration, GlobalCompileEnvironment.Architectures); UEToolChain TargetToolChain = CurrentTarget.CreateToolchain(CurrentTarget.Platform, Logger); TargetToolChain.SetEnvironmentVariables(); CurrentTarget.SetupGlobalEnvironment(TargetToolChain, GlobalCompileEnvironment, GlobalLinkEnvironment); if (CurrentTarget.Rules.bUseSharedPCHs) { // Find all the shared PCHs. CurrentTarget.FindSharedPCHs(CurrentTarget.Binaries, GlobalCompileEnvironment, Logger); // Create all the shared PCH instances before processing the modules CurrentTarget.CreateSharedPCHInstances(CurrentTarget.Rules, TargetToolChain, CurrentTarget.Binaries, GlobalCompileEnvironment, new NullActionGraphBuilder(Logger), Logger); } await Parallel.ForEachAsync(CurrentTarget.Binaries, async (Binary, CancellationToken) => { CppCompileEnvironment BinaryCompileEnvironment = Binary.CreateBinaryCompileEnvironment(GlobalCompileEnvironment); IEnumerable Modules = Binary.Modules.OfType().Where(x => x.Binary == Binary); await Parallel.ForEachAsync(Modules, (Module, CancellationToken) => { if (CurrentTargetIntellisenseInfo.ModuleToCompileSettings.ContainsKey(Module)) { return ValueTask.CompletedTask; } foreach (DirectoryReference Dir in Module.ModuleDirectories) { CurrentTargetIntellisenseInfo.DirToModule.TryAdd(Dir, Module); } if (Module.GeneratedCodeDirectory != null) { CurrentTargetIntellisenseInfo.DirToModule.TryAdd(Module.GeneratedCodeDirectory, Module); } CppCompileEnvironment ModuleCompileEnvironment = Module.CreateCompileEnvironmentForIntellisense(CurrentTarget.Rules, BinaryCompileEnvironment, Logger); if (ModuleCompileEnvironment.PrecompiledHeaderAction == PrecompiledHeaderAction.Include) { FileItem IncludeHeader = FileItem.GetItemByFileReference(ModuleCompileEnvironment.PrecompiledHeaderIncludeFilename!); ModuleCompileEnvironment.ForceIncludeFiles.Insert(0, IncludeHeader); } // Remove include paths and defintiions from an environment used to get the command line args CppCompileEnvironment ModuleCompileEnvironmentForArgs = new CppCompileEnvironment(ModuleCompileEnvironment); ModuleCompileEnvironmentForArgs.SystemIncludePaths.Clear(); ModuleCompileEnvironmentForArgs.UserIncludePaths.Clear(); ModuleCompileEnvironmentForArgs.Definitions.Clear(); TargetIntellisenseInfo.CompileSettings Settings = new TargetIntellisenseInfo.CompileSettings(); Settings.IncludePaths.AddRange(ModuleCompileEnvironment.UserIncludePaths.Select(x => x.ToString())); Settings.IncludePaths.AddRange(ModuleCompileEnvironment.SystemIncludePaths.Select(x => x.ToString())); if (TargetToolChain is VCToolChain TargetVCToolChain) { Settings.IncludePaths.AddRange(TargetVCToolChain.GetVCIncludePaths().Select(x => x.ToString())); } Settings.Defines = ModuleCompileEnvironment.Definitions; Settings.Standard = ModuleCompileEnvironment.CppStandard.ToString(); Settings.ForcedIncludes = ModuleCompileEnvironment.ForceIncludeFiles.Select(x => x.ToString()).ToList(); Settings.CompilerPath = TargetToolChain.GetCppCompilerPath()?.ToString(); Settings.CompilerArgs = TargetToolChain.GetGlobalCommandLineArgs(ModuleCompileEnvironmentForArgs).ToList(); Settings.CompilerAdditionalArgs.Add("c", TargetToolChain.GetCCommandLineArgs(ModuleCompileEnvironmentForArgs).ToList()); Settings.CompilerAdditionalArgs.Add("cpp", TargetToolChain.GetCPPCommandLineArgs(ModuleCompileEnvironmentForArgs).ToList()); Settings.WindowsSdkVersion = CurrentTarget.Rules.WindowsPlatform.WindowsSdkVersion; CurrentTargetIntellisenseInfo.ModuleToCompileSettings.TryAdd(Module, Settings); return ValueTask.CompletedTask; }); }); } var Result = new { DirToModule = CurrentTargetIntellisenseInfo?.DirToModule.ToImmutableSortedDictionary(x => x.Key.ToString(), x => x.Value.Name), ModuleToCompileSettings = CurrentTargetIntellisenseInfo?.ModuleToCompileSettings.ToImmutableSortedDictionary(x => x.Key.Name, x => x.Value), LaunchSettings = CurrentLaunchSettings, IsTestTarget = CurrentTarget.Rules.IsTestTarget }; await WriteResultsAsync(Result, JsonOptions, Logger); return 0; } catch (Exception Ex) { Logger.LogError("Failed to query available targets: {Error}", Ex.Message); Logger.LogDebug(Ex, "Unhandled exception: {Ex}", ExceptionUtils.FormatExceptionDetails(Ex)); return 1; } } private async Task QueryTargetModuleReferences(CommandLineArguments Arguments, ILogger Logger, JsonSerializerOptions JsonOptions) { if (TargetName == null) { Logger.LogError("Missing argument Target"); return 1; } else if (TargetConfiguration == null) { Logger.LogError("Missing argument Configuration"); return 1; } else if (TargetPlatform == null) { Logger.LogError("Missing argument Platform"); return 1; } Utils.TryParseProjectFileArgument(Arguments, Logger, out FileReference? ProjectFileArg); List RawArgs = new List { TargetName, TargetConfiguration, TargetPlatform }; if (ProjectFileArg != null) { RawArgs.Add(ProjectFileArg.ToString()); } RawArgs.AddRange(Arguments.GetUnusedArguments()); RawArgs.Add("-NoPCHChain"); // Currently unsupported CommandLineArguments Args = new CommandLineArguments(RawArgs.ToArray()); List TargetDescriptors = new(); TargetDescriptor.ParseSingleCommandLine(Args, false, false, false, TargetDescriptors, Logger); if (TargetDescriptors.Count == 0) { // TOOD: Error return 1; } // Ensure the intermediate environment does not conflict with normal builds TargetDescriptors[0].IntermediateEnvironment = UnrealIntermediateEnvironment.Query; try { UEBuildTarget CurrentTarget; using (GlobalTracer.Instance.BuildSpan("UEBuildTarget.Create()").StartActive()) { bool bUsePrecompiled = false; // Prevent multiple conflicting processes building TargetRules at the same time string MutexName = GlobalSingleInstanceMutex.GetUniqueMutexForPath("UnrealBuildTool_QueryMode_UEBuildTarget-Create", Unreal.RootDirectory); using (new GlobalSingleInstanceMutex(MutexName, true)) { CurrentTarget = UEBuildTarget.Create(TargetDescriptors[0], false, false, bUsePrecompiled, TargetDescriptors[0].IntermediateEnvironment, Logger); } } UEBuildBinary? LaunchBinary = CurrentTarget.Binaries.FirstOrDefault(Binary => Binary.Modules.Any(Module => Module.Name == CurrentTarget.Rules.LaunchModuleName)); if (LaunchBinary == null) { throw new BuildException("Unable to find launch binary for target"); } // Create the makefile for the target and export the module information { using ISourceFileWorkingSet WorkingSet = new EmptySourceFileWorkingSet(); TargetMakefile Makefile; try { Makefile = await BuildMode.CreateMakefileAsync(BuildConfiguration, TargetDescriptors[0], WorkingSet, Logger); } finally { SourceFileMetadataCache.SaveAll(); } } ConcurrentDictionary ModuleInfo = []; await Parallel.ForEachAsync(CurrentTarget.Binaries, async (Binary, CancellationToken) => { IEnumerable Modules = Binary.Modules.OfType().Where(x => x.Binary == Binary); await Parallel.ForEachAsync(Modules, (Module, CancellationToken) => { if (ModuleInfo.ContainsKey(Module)) { return ValueTask.CompletedTask; } var Info = new { Public = new SortedSet(Module.PublicDependencyModules?.Where(x => x is UEBuildModuleCPP).Select(x => x.Name) ?? []), Private = new SortedSet(Module.PrivateDependencyModules?.Where(x => x is UEBuildModuleCPP).Select(x => x.Name) ?? []), PublicInclude = new SortedSet(Module.PublicIncludePathModules?.Where(x => x is UEBuildModuleCPP).Select(x => x.Name) ?? []), PrivateInclude = new SortedSet(Module.PrivateIncludePathModules?.Where(x => x is UEBuildModuleCPP).Select(x => x.Name) ?? []), DynamicallyLoaded = new SortedSet(Module.DynamicallyLoadedModules?.Where(x => x is UEBuildModuleCPP).Select(x => x.Name) ?? []), Circular = new SortedSet(Module.Rules.CircularlyReferencedDependentModules), }; ModuleInfo.TryAdd(Module, Info); return ValueTask.CompletedTask; }); }); var Result = new { Modules = ModuleInfo.ToImmutableSortedDictionary(x => x.Key.Name, x => x.Value) }; await WriteResultsAsync(Result, JsonOptions, Logger); return 0; } catch (Exception Ex) { Logger.LogError("Failed to query available targets: {Error}", Ex.Message); Logger.LogDebug(Ex, "Unhandled exception: {Ex}", ExceptionUtils.FormatExceptionDetails(Ex)); return 1; } } } }