// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Text.RegularExpressions; using EpicGames.Core; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Builds a reverse lookup table for finding all source files that includes a set of headers /// class CppIncludeLookup { [DebuggerDisplay("{File}")] class CppIncludeFileInfo { public FileItem File { get; } public List? IncludedNames { get; set; } public DateTime LastWriteTimeUtc { get; set; } public CppIncludeFileInfo(FileItem File) => this.File = File; } [DebuggerDisplay("{Name}")] class CppIncludeNameInfo { public string Name { get; } public List Files { get; set; } = new List(); public HashSet IncludedByNames { get; set; } = new HashSet(); public int Index { get; set; } public CppIncludeNameInfo(string Name) => this.Name = Name; } const int CurrentVersion = 1; static Regex SourceFileRegex = new Regex(@"\.(?:c|cpp|cxx)$"); static Regex SourceOrHeaderFileRegex = new Regex(@"\.(?:c|h|cpp|hpp|cxx|hxx)$"); FileReference Location; Dictionary NameToInfo = new Dictionary(StringComparer.OrdinalIgnoreCase); public CppIncludeLookup(FileReference Location) { this.Location = Location; } public void Load() { if (FileReference.Exists(Location)) { using (BinaryArchiveReader Reader = new BinaryArchiveReader(Location)) { int Version = Reader.ReadInt(); if (Version == CurrentVersion) { CppIncludeNameInfo[] NameInfos = Reader.ReadArray(() => new CppIncludeNameInfo(Reader.ReadString()!))!; foreach (CppIncludeNameInfo NameInfo in NameInfos) { int NumFiles = Reader.ReadInt(); for (int Idx = 0; Idx < NumFiles; Idx++) { CppIncludeFileInfo FileInfo = new CppIncludeFileInfo(Reader.ReadCompactFileItem()!); int[]? NameIndexes = Reader.ReadIntArray(); if (NameIndexes != null) { FileInfo.IncludedNames = NameIndexes.Select(x => NameInfos[x]).ToList(); FileInfo.LastWriteTimeUtc = new DateTime(Reader.ReadLong(), DateTimeKind.Utc); } NameInfo.Files.Add(FileInfo); } } NameToInfo = NameInfos.ToDictionary(x => x.Name, x => x, StringComparer.OrdinalIgnoreCase); } } } } public void Save() { using (BinaryArchiveWriter Writer = new BinaryArchiveWriter(Location)) { List NameInfos = NameToInfo.Values.ToList(); Writer.WriteInt(CurrentVersion); Dictionary NameToIndex = new Dictionary(NameInfos.Count); for (int Idx = 0; Idx < NameInfos.Count; Idx++) { NameToIndex[NameInfos[Idx]] = Idx; } Writer.WriteList(NameInfos, x => Writer.WriteString(x.Name)); foreach (CppIncludeNameInfo NameInfo in NameInfos) { Writer.WriteInt(NameInfo.Files.Count); foreach (CppIncludeFileInfo FileInfo in NameInfo.Files) { Writer.WriteCompactFileItem(FileInfo.File); if (FileInfo.IncludedNames == null) { Writer.WriteIntArray(null); } else { Writer.WriteIntArray(FileInfo.IncludedNames.Select(x => NameToIndex[x]).ToArray()); Writer.WriteLong(FileInfo.LastWriteTimeUtc.Ticks); } } } } } public IEnumerable FindFiles(IEnumerable Names) { List NameInfoList = new List(); foreach (string Name in Names) { CppIncludeNameInfo? NameInfo; if (NameToInfo.TryGetValue(Name, out NameInfo)) { NameInfoList.Add(NameInfo); } } HashSet NameInfoSet = new HashSet(NameInfoList); for (int Idx = 0; Idx < NameInfoList.Count; Idx++) { CppIncludeNameInfo NameInfo = NameInfoList[Idx]; foreach (CppIncludeNameInfo IncludedByName in NameInfo.IncludedByNames) { if (NameInfoSet.Add(IncludedByName)) { NameInfoList.Add(IncludedByName); } } } return NameInfoSet.SelectMany(x => x.Files).Select(x => x.File); } public void Update(IEnumerable RootDirs, IEnumerable AdditionalFiles) { // Find all the source files in the given root directory ConcurrentBag KeepFileItems = new ConcurrentBag(AdditionalFiles); using (ThreadPoolWorkQueue Queue = new ThreadPoolWorkQueue()) { foreach (DirectoryReference RootDir in RootDirs) { DirectoryItem RootDirItem = DirectoryItem.GetItemByDirectoryReference(RootDir); Queue.Enqueue(() => ScanRootDir(RootDirItem, KeepFileItems, Queue)); } } // Remove any files which no longer exist HashSet FileItemsSet = new HashSet(KeepFileItems); foreach (CppIncludeNameInfo NameInfo in NameToInfo.Values) { NameInfo.Files.RemoveAll(x => !FileItemsSet.Contains(x.File)); } FileItemsSet.ExceptWith(NameToInfo.Values.SelectMany(x => x.Files).Select(x => x.File)); // Add any new items foreach (FileItem FileItem in FileItemsSet) { CppIncludeNameInfo NameInfo = FindOrAddNameInfo(FileItem.Name); NameInfo.Files.Add(new CppIncludeFileInfo(FileItem)); } // Update all the dependencies HashSet Files = new HashSet(); using (ThreadPoolWorkQueue Queue = new ThreadPoolWorkQueue()) { List RootFiles = NameToInfo.Values.Where(x => SourceFileRegex.IsMatch(x.Name)).SelectMany(x => x.Files).ToList(); foreach (CppIncludeFileInfo FileInfo in RootFiles) { Queue.Enqueue(() => ScanDependencies(FileInfo, Files, Queue)); } } // Build the inverse lookup from name to files foreach (CppIncludeNameInfo NameInfo in NameToInfo.Values) { foreach (CppIncludeFileInfo FileInfo in NameInfo.Files) { if (FileInfo.IncludedNames != null) { foreach (CppIncludeNameInfo IncludeNameInfo in FileInfo.IncludedNames) { IncludeNameInfo.IncludedByNames.Add(NameInfo); } } } } } void ScanDependencies(CppIncludeFileInfo FileInfo, HashSet Files, ThreadPoolWorkQueue Queue) { if (!FileInfo.File.Exists) { return; } // Update names for this file if (FileInfo.IncludedNames == null || FileInfo.File.LastWriteTimeUtc != FileInfo.LastWriteTimeUtc) { byte[] Data = FileReference.ReadAllBytes(FileInfo.File.Location); List Names = new List(); FindIncludes(Data, Names); lock (NameToInfo) { FileInfo.IncludedNames = Names.ConvertAll(x => FindOrAddNameInfo(x)); FileInfo.LastWriteTimeUtc = FileInfo.File.LastWriteTimeUtc; } } // Update all the files that lock (Files) { foreach (CppIncludeFileInfo IncludedFile in FileInfo.IncludedNames.SelectMany(x => x.Files)) { if (Files.Add(IncludedFile)) { Queue.Enqueue(() => ScanDependencies(IncludedFile, Files, Queue)); } } } } static void ScanRootDir(DirectoryItem DirItem, ConcurrentBag FileItems, ThreadPoolWorkQueue Queue) { foreach (DirectoryReference ExtensionDir in Unreal.GetExtensionDirs(DirItem.Location)) { DirectoryItem ExtensionDirItem = DirectoryItem.GetItemByDirectoryReference(ExtensionDir); DirectoryItem PluginsDirItem = DirectoryItem.Combine(ExtensionDirItem, "Plugins"); if (PluginsDirItem.Exists) { Queue.Enqueue(() => ScanPluginDir(PluginsDirItem, FileItems, Queue)); } DirectoryItem SourceDirItem = DirectoryItem.Combine(ExtensionDirItem, "Source"); if (SourceDirItem.Exists) { Queue.Enqueue(() => ScanSourceDir(SourceDirItem, FileItems, Queue)); } } } static void ScanPluginDir(DirectoryItem DirItem, ConcurrentBag FileItems, ThreadPoolWorkQueue Queue) { if (DirItem.EnumerateFiles().Any(x => x.HasExtension(".uplugin"))) { DirectoryItem SourceDirItem = DirectoryItem.Combine(DirItem, "Source"); if (SourceDirItem.Exists) { Queue.Enqueue(() => ScanSourceDir(SourceDirItem, FileItems, Queue)); } } else { foreach (DirectoryItem SubDirItem in DirItem.EnumerateDirectories()) { Queue.Enqueue(() => ScanPluginDir(SubDirItem, FileItems, Queue)); } } } static void ScanSourceDir(DirectoryItem DirItem, ConcurrentBag FileItems, ThreadPoolWorkQueue Queue) { foreach (DirectoryItem SubDirItem in DirItem.EnumerateDirectories()) { Queue.Enqueue(() => ScanSourceDir(SubDirItem, FileItems, Queue)); } foreach (FileItem FileItem in DirItem.EnumerateFiles().Where(x => SourceOrHeaderFileRegex.IsMatch(x.Name))) { FileItems.Add(FileItem); } } CppIncludeNameInfo FindOrAddNameInfo(string Name) { CppIncludeNameInfo? NameInfo; if (!NameToInfo.TryGetValue(Name, out NameInfo)) { NameInfo = new CppIncludeNameInfo(Name); NameToInfo.Add(Name, NameInfo); } return NameInfo; } const string IncludeText = "include"; static ReadOnlyMemory IncludeBytes = Encoding.UTF8.GetBytes(IncludeText); static sbyte[] SkipTable = CreateSkipTable(); static sbyte[] CreateSkipTable() { sbyte[] Table = new sbyte[256]; for (int Idx = 0; Idx < 256; Idx++) { Table[Idx] = (sbyte)IncludeText.Length; } for (int Idx = IncludeText.Length - 1; Idx >= 0; Idx--) { Table[IncludeText[Idx]] = (sbyte)-Idx; } return Table; } static void FindIncludes(ReadOnlySpan Span, List Names) { ReadOnlySpan IncludeSpan = IncludeBytes.Span; int Idx = IncludeText.Length; while (Idx < Span.Length) { int Skip = SkipTable[Span[Idx]]; Idx += Skip; if (Skip > 0) { continue; } byte[]? Name = ParseIncludeName(Span, Idx); if (Name != null) { Names.Add(Encoding.UTF8.GetString(Name)); } Idx += IncludeSpan.Length; } } static byte[]? ParseIncludeName(ReadOnlySpan Span, int Idx) { ReadOnlySpan IncludeSpan = IncludeBytes.Span; if (!Span.Slice(Idx).StartsWith(IncludeSpan)) { return null; } // Check it's an include directive int MinIdx = Idx; for (; ; ) { byte Char = Span[--MinIdx]; if (Char == '#') { break; } if (Char != ' ' && Char != '\t') { return null; } if (MinIdx == 1) { return null; } } for (; MinIdx > 0;) { byte Char = Span[--MinIdx]; if (Char == '\n') { break; } if (Char != ' ' && Char != '\t') { return null; } } // Scan to the start of the filename int MinNameIdx = Idx + IncludeSpan.Length; if (MinNameIdx + 1 >= Span.Length) { return null; } for (; ; ) { byte Char = Span[MinNameIdx++]; if (Char == '\"' || Char == '<') { break; } if (Char != ' ' && Char != '\t') { return null; } if (MinNameIdx + 1 >= Span.Length) { return null; } } // Scan to the end of the included file. Update MinNameIndex to the start of the filename. int MaxNameIdx = MinNameIdx; for (; ; MaxNameIdx++) { byte Char = Span[MaxNameIdx]; if (Char == '\"' || Char == '>') { break; } if (Char == '/' || Char == '\\') { MinNameIdx = MaxNameIdx + 1; } if (MaxNameIdx + 1 >= Span.Length) { return null; } } // Normalize the filename byte[] Name = new byte[MaxNameIdx - MinNameIdx]; Span.Slice(MinNameIdx, MaxNameIdx - MinNameIdx).CopyTo(Name); return Name; } } }