// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Generate a clang compile_commands file for a target /// [ToolMode("GenerateClangDatabase", ToolModeOptions.XmlConfig | ToolModeOptions.BuildPlatforms | ToolModeOptions.SingleInstance | ToolModeOptions.StartPrefetchingEngine | ToolModeOptions.ShowExecutionTime)] class GenerateClangDatabase : ToolMode { static Regex ArgumentRegex = new Regex(@"^(\-include|\-I|\/I|\/imsvc|\-isystem|\/FI|\/Fo)\s*(.*)"); /// /// Set of filters for files to include in the database. Relative to the root directory, or to the project file. /// [CommandLine("-Filter=")] List FilterRules = new List(); /// /// Execute any actions which result in code generation (eg. ISPC compilation) /// [CommandLine("-ExecCodeGenActions")] public bool bExecCodeGenActions = false; /// /// This ActionGraphBuilder captures the build output from a UEBuildModuleCPP so it can be consumed later. /// private class CaptureActionGraphBuilder : IActionGraphBuilder { private readonly ILogger Logger; public List CapturedActions = new List(); public List>> CapturedTextFiles = new List>>(); /// /// Constructor /// /// public CaptureActionGraphBuilder(ILogger InLogger) { Logger = InLogger; } /// public void AddAction(IExternalAction Action) { CapturedActions.Add(Action); } /// public void CreateIntermediateTextFile(FileItem FileItem, string Contents, bool AllowAsync) { Utils.WriteFileIfChanged(FileItem, Contents, Logger); } /// public void CreateIntermediateTextFile(FileItem FileItem, IEnumerable ContentLines, bool AllowAsync = true) { Utils.WriteFileIfChanged(FileItem, ContentLines, Logger); CapturedTextFiles.Add(new Tuple>(FileItem, ContentLines)); } /// public void AddSourceDir(DirectoryItem SourceDir) { } /// public void AddSourceFiles(DirectoryItem SourceDir, FileItem[] SourceFiles) { } /// public void AddHeaderFiles(FileItem[] HeaderFiles) { } /// public void AddFileToWorkingSet(FileItem File) { } /// public void AddCandidateForWorkingSet(FileItem File) { } /// public void AddDiagnostic(string Message) { } /// public void SetOutputItemsForModule(string ModuleName, FileItem[] OutputItems) { } } /// /// Execute the command /// /// Command line arguments /// Exit code /// public override async Task ExecuteAsync(CommandLineArguments Arguments, ILogger Logger) { Arguments.ApplyTo(this); // Create the build configuration object, and read the settings BuildConfiguration BuildConfiguration = new BuildConfiguration(); XmlConfig.ApplyTo(BuildConfiguration); Arguments.ApplyTo(BuildConfiguration); // Parse the filter argument FileFilter? FileFilter = null; if (FilterRules.Count > 0) { FileFilter = new FileFilter(FileFilterType.Exclude); foreach (string FilterRule in FilterRules) { FileFilter.AddRules(FilterRule.Split(';')); } } // Force C++ modules to always include their generated code directories UEBuildModuleCPP.bForceAddGeneratedCodeIncludePath = true; // Parse all the target descriptors List TargetDescriptors = TargetDescriptor.ParseCommandLine(Arguments, BuildConfiguration, Logger); // Generate the compile DB for each target using (ISourceFileWorkingSet WorkingSet = new EmptySourceFileWorkingSet()) { // Find the compile commands for each file in the target Dictionary FileToCommand = new Dictionary(); foreach (TargetDescriptor TargetDescriptor in TargetDescriptors) { // Disable PCHs and unity builds for the target TargetDescriptor.bUseUnityBuild = false; TargetDescriptor.AdditionalArguments = TargetDescriptor.AdditionalArguments.Append(new string[] { "-NoPCH" }); // Create a makefile for the target UEBuildTarget Target = UEBuildTarget.Create(TargetDescriptor, BuildConfiguration, Logger); UEToolChain TargetToolChain = Target.CreateToolchain(Target.Platform); // Execute code generation actions if (bExecCodeGenActions) { // Create the makefile TargetMakefile Makefile = await Target.BuildAsync(BuildConfiguration, WorkingSet, TargetDescriptor, Logger); List Actions = Makefile.Actions.ConvertAll(x => new LinkedAction(x, TargetDescriptor)); ActionGraph.Link(Actions, Logger); // Filter all the actions to execute HashSet PrerequisiteItems = new HashSet(Makefile.Actions.SelectMany(x => x.ProducedItems).Where(x => x.HasExtension(".h") || x.HasExtension(".cpp"))); List PrerequisiteActions = ActionGraph.GatherPrerequisiteActions(Actions, PrerequisiteItems); // Execute these actions if (PrerequisiteActions.Count > 0) { Logger.LogInformation("Executing actions that produce source files..."); await ActionGraph.ExecuteActionsAsync(BuildConfiguration, PrerequisiteActions, new List { TargetDescriptor }, Logger); } } // Create all the binaries and modules CppCompileEnvironment GlobalCompileEnvironment = Target.CreateCompileEnvironmentForProjectFiles(Logger); foreach (UEBuildBinary Binary in Target.Binaries) { CppCompileEnvironment BinaryCompileEnvironment = Binary.CreateBinaryCompileEnvironment(GlobalCompileEnvironment); foreach (UEBuildModuleCPP Module in Binary.Modules.OfType()) { if (!Module.Rules.bUsePrecompiled) { // Gather all the files we care about UEBuildModuleCPP.InputFileCollection InputFileCollection = Module.FindInputFiles(Target.Platform, new Dictionary(), Logger); List InputFiles = new List(); InputFiles.AddRange(InputFileCollection.CPPFiles); InputFiles.AddRange(InputFileCollection.CCFiles); HashSet FilterFileList = new(); foreach (FileItem InputFile in InputFiles) { if (FileFilter == null || FileFilter.Matches(InputFile.Location.MakeRelativeTo(Unreal.RootDirectory))) { FilterFileList.Add(InputFile); } } CaptureActionGraphBuilder ActionGraphBuilder = new CaptureActionGraphBuilder(Logger); Module.Compile(Target.Rules, TargetToolChain, BinaryCompileEnvironment, WorkingSet, ActionGraphBuilder, Logger); List ValidActions = new(); foreach (IExternalAction Action in ActionGraphBuilder.CapturedActions) { if (Action.ActionType == ActionType.Compile && Action.ProducedItems.Any()) { foreach (FileItem Prereq in Action.PrerequisiteItems) { if (FilterFileList.Contains(Prereq)) { ValidActions.Add(Action); } } } } if (ValidActions.Count != 0) { // convert any rsp files Dictionary UpdatedResFiles = new Dictionary(); foreach (Tuple> FileAndContents in ActionGraphBuilder.CapturedTextFiles) { if (FileAndContents.Item1.AbsolutePath.EndsWith(".rsp") || FileAndContents.Item1.AbsolutePath.EndsWith(".response")) { string NewResPath = ConvertResponseFile(FileAndContents.Item1, FileAndContents.Item2, Logger); UpdatedResFiles[FileAndContents.Item1.AbsolutePath] = NewResPath; } } foreach (IExternalAction Action in ValidActions) { // Create the command StringBuilder CommandBuilder = new StringBuilder(); string CommandArguments = Action.CommandArguments.Replace(".rsp", ".rsp.gcd").Replace(".response", ".response.gcd"); CommandBuilder.AppendFormat("\"{0}\" {1}", Action.CommandPath, CommandArguments); foreach (string ExtraArgument in GetExtraPlatformArguments(TargetToolChain)) { CommandBuilder.AppendFormat(" {0}", ExtraArgument); } // find source file FileItem? SourceFile = Action.PrerequisiteItems.FirstOrDefault(fi => fi.HasExtension(".cpp") || fi.HasExtension(".c") || fi.HasExtension(".c")); if (SourceFile != null) { FileToCommand[SourceFile.Location] = CommandBuilder.ToString(); } } } } } } } // Write the compile database DirectoryReference DatabaseDirectory = Arguments.GetDirectoryReferenceOrDefault("-OutputDir=", Unreal.RootDirectory); FileReference DatabaseFile = FileReference.Combine(DatabaseDirectory, "compile_commands.json"); using (JsonWriter Writer = new JsonWriter(DatabaseFile)) { Writer.WriteArrayStart(); foreach (KeyValuePair FileCommandPair in FileToCommand.OrderBy(x => x.Key.FullName)) { Writer.WriteObjectStart(); Writer.WriteValue("file", FileCommandPair.Key.FullName); Writer.WriteValue("command", FileCommandPair.Value); Writer.WriteValue("directory", Unreal.EngineSourceDirectory.ToString()); Writer.WriteObjectEnd(); } Writer.WriteArrayEnd(); } Logger.LogInformation($"ClangDatabase written to {DatabaseFile.FullName}"); } return 0; } private IEnumerable GetExtraPlatformArguments(UEToolChain TargetToolChain) { IList ExtraPlatformArguments = new List(); ClangToolChain? ClangToolChain = TargetToolChain as ClangToolChain; if (ClangToolChain != null) { ClangToolChain.AddExtraToolArguments(ExtraPlatformArguments); } return ExtraPlatformArguments; } private static string ConvertResponseFile(FileItem OriginalFileItem, IEnumerable FileContents, ILogger Logger) { List NewFileContents = new List(FileContents); FileItem NewFileItem = FileItem.GetItemByFileReference(new FileReference(OriginalFileItem.AbsolutePath + ".gcd")); for (int i = 0; i < NewFileContents.Count; i++) { string Line = NewFileContents[i].TrimStart(); // Ignore empty strings if (String.IsNullOrEmpty(Line)) { continue; } // The file that is going to compile else if (!Line.StartsWith("-") && !Line.StartsWith("/")) { Line = ConvertPath(Line, Line); } // Arguments else { int StrIndex = Line.IndexOf("..\\"); if (StrIndex != -1) { Line = ConvertPath(Line, Line.Substring(StrIndex)); } else { Match LineMatch = ArgumentRegex.Match(Line); if (LineMatch.Success) { Line = ConvertPath(Line, LineMatch.Groups[2].Value); } } } NewFileContents[i] = Line; } Utils.WriteFileIfChanged(NewFileItem, NewFileContents, Logger); return NewFileItem.AbsolutePath; } private static string ConvertPath(string Line, string OldPath) { OldPath = OldPath.Replace("\"", ""); if (OldPath[^1] == '\\' || OldPath[^1] == '/') { OldPath = OldPath.Remove(OldPath.Length - 1, 1); } FileReference FileReference = new FileReference(OldPath); return Line.Replace(OldPath, FileReference.FullName.Replace("\\", "/")); } } }