// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using EpicGames.Core; using Microsoft.Extensions.Logging; using OpenTracing.Util; using UnrealBuildBase; namespace UnrealBuildTool { /// /// The header unit type markup is used for multiple things: /// 1. To figure out if a header can be compiled by itself or not. Many includes are included in the middle of other includes and those can never be compiled by themselves /// This markup is used by both msvc header unit feature and IWYU toolchain /// 2. Tell if the header even supports being part of a header unit. If it does not support it will also prevent all includes including it from producing a header unit /// This is how to markup in headers to provide above info: /// // HEADER_UNIT_SKIP - Here you can write why this file can't be compiled standalone /// // HEADER_UNIT_UNSUPPORTED - Here you can write why this file can't be part of header units. /// enum HeaderUnitType { Valid = 0, Unsupported = 1, Skip = 2, Ignore = 3 } /// /// Caches information about C++ source files; whether they contain reflection markup, what the first included header is, and so on. /// class SourceFileMetadataCache { /// /// Source(cpp/c) file info /// class SourceFileInfo { /// /// Last write time of the file when the data was cached /// public long LastWriteTimeUtc; /// /// Contents of the include directive /// public string? IncludeText; /// /// List of files this particular file is inlining /// public List InlinedFileNames = new List(); } /// /// Header file info /// class HeaderFileInfo { /// /// Last write time of the file when the data was cached /// public long LastWriteTimeUtc; /// /// Whether or not the file contains reflection markup /// public bool bContainsMarkup; /// /// Whether or not the file has types that use DLL export/import defines /// public bool bUsesAPIDefine; /// /// List of includes that header contains /// public List? Includes; public HeaderUnitType UnitType; } /// /// The current file version /// public const int CurrentVersion = 8; /// /// Location of this dependency cache /// FileReference Location; /// /// Directory for files to cache dependencies for. /// DirectoryReference BaseDirectory; /// /// The parent cache. /// SourceFileMetadataCache? Parent; /// /// Map from file item to source file info /// ConcurrentDictionary FileToSourceFileInfo = new ConcurrentDictionary(); /// /// Map from file item to header file info /// ConcurrentDictionary FileToHeaderFileInfo = new ConcurrentDictionary(); /// /// Map from file item to source file /// ConcurrentDictionary FileToSourceFile = new ConcurrentDictionary(); /// /// Whether the cache has been modified and needs to be saved /// bool bModified; /// /// Logger for output /// ILogger Logger; /// /// Regex that matches C++ code with UObject declarations which we will need to generated code for. /// static readonly Regex ReflectionMarkupRegex = new Regex("^\\s*U(CLASS|STRUCT|ENUM|INTERFACE|DELEGATE)\\b", RegexOptions.Compiled); /// /// Regex that matches #include UE_INLINE_GENERATED_CPP_BY_NAME(****) statements. /// static readonly Regex InlineReflectionMarkupRegex = new Regex("^\\s*#\\s*include\\s*UE_INLINE_GENERATED_CPP_BY_NAME\\((.+)\\)", RegexOptions.Compiled | RegexOptions.Multiline); /// /// Regex that matches #include statements. /// static readonly Regex IncludeRegex = new Regex("^[ \t]*#[ \t]*include[ \t]*[<\"](?[^\">]*)[\">]", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture); /// /// Regex that matches #import directives in mm files /// static readonly Regex ImportRegex = new Regex("^[ \t]*#[ \t]*import[ \t]*[<\"](?[^\">]*)[\">]", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture); /// /// Static cache of all constructed dependency caches /// static ConcurrentDictionary Caches = new ConcurrentDictionary(); /// /// Constructs a dependency cache. This method is private; call CppDependencyCache.Create() to create a cache hierarchy for a given project. /// /// File to store the cache /// Base directory for files that this cache should store data for /// The parent cache to use /// Logger for output private SourceFileMetadataCache(FileReference Location, DirectoryReference BaseDir, SourceFileMetadataCache? Parent, ILogger Logger) { this.Location = Location; BaseDirectory = BaseDir; this.Parent = Parent; this.Logger = Logger; if (FileReference.Exists(Location)) { using (GlobalTracer.Instance.BuildSpan("Reading source file metadata cache").StartActive()) { Read(); } } } /// /// Returns a SourceFileInfo struct for a file (and parse the file if not already cached) /// /// The file to parse /// SourceFileInfo for file SourceFileInfo GetSourceFileInfo(FileItem SourceFile) { if (Parent != null && !SourceFile.Location.IsUnderDirectory(BaseDirectory)) { return Parent.GetSourceFileInfo(SourceFile); } else { Func UpdateSourceFileInfo = (FileItem SourceFile) => { SourceFileInfo SourceFileInfo = new SourceFileInfo(); string FileText = FileReference.ReadAllText(SourceFile.Location); string[] FileTextLines = FileText.Split('\n'); SourceFileInfo.LastWriteTimeUtc = SourceFile.LastWriteTimeUtc.Ticks; // Inline reflection data MatchCollection FileMatches = InlineReflectionMarkupRegex.Matches(FileText); foreach (Match Match in FileMatches) { SourceFileInfo.InlinedFileNames.Add(Match.Groups[1].Value); } SourceFileInfo.IncludeText = ParseFirstInclude(SourceFile, FileTextLines); bModified = true; return SourceFileInfo; }; return FileToSourceFileInfo.AddOrUpdate(SourceFile, _ => { return UpdateSourceFileInfo(SourceFile); }, (k, v) => { if (SourceFile.LastWriteTimeUtc.Ticks > v.LastWriteTimeUtc) { return UpdateSourceFileInfo(SourceFile); } return v; } ); } } /// /// Parse the first include directive from a source file /// /// The source file to parse /// The source file contents /// The first include directive static string? ParseFirstInclude(FileItem SourceFile, string[] FileToSourceFileFileText) { bool bMatchImport = SourceFile.HasExtension(".m") || SourceFile.HasExtension(".mm"); foreach (string Line in FileToSourceFileFileText) { if (Line == null) { return null; } Match IncludeMatch = IncludeRegex.Match(Line); if (IncludeMatch.Success) { return IncludeMatch.Groups[1].Value; } if (bMatchImport) { Match ImportMatch = ImportRegex.Match(Line); if (ImportMatch.Success) { return ImportMatch.Groups[1].Value; } } } return null; } HeaderFileInfo GetHeaderFileInfo(FileItem HeaderFile) { if (Parent != null && !HeaderFile.Location.IsUnderDirectory(BaseDirectory)) { return Parent.GetHeaderFileInfo(HeaderFile); } else { Func UpdateHeaderFileInfo = (FileItem HeaderFile) => { HeaderFileInfo HeaderFileInfo = new HeaderFileInfo(); string FileText = FileReference.ReadAllText(HeaderFile.Location); string[] FileTextLines = FileText.Split('\n'); HeaderFileInfo.LastWriteTimeUtc = HeaderFile.LastWriteTimeUtc.Ticks; ParseHeader(HeaderFileInfo, FileTextLines); bModified = true; return HeaderFileInfo; }; return FileToHeaderFileInfo.AddOrUpdate(HeaderFile, _ => { return UpdateHeaderFileInfo(HeaderFile); }, (k, v) => { if (HeaderFile.LastWriteTimeUtc.Ticks > v.LastWriteTimeUtc) { return UpdateHeaderFileInfo(HeaderFile); } return v; } ); } } /// /// Read entire header file to find markup and includes /// /// A HeaderInfo struct containing information about header private void ParseHeader(HeaderFileInfo HeaderFileInfo, string[] FileText) { bool bContainsMarkup = false; bool bUsesAPIDefine = false; int InsideDeprecationScope = 0; SortedSet Includes = new(); foreach (string Line in FileText) { if (!bUsesAPIDefine) { bUsesAPIDefine = Line.Contains("_API", StringComparison.Ordinal); } if (!bContainsMarkup) { bContainsMarkup = ReflectionMarkupRegex.IsMatch(Line); } ReadOnlySpan LineSpan = Line.AsSpan().TrimStart(); if (LineSpan.StartsWith("#include") && InsideDeprecationScope == 0) { ReadOnlySpan IncludeSpan = LineSpan.Slice("#include".Length).TrimStart(); if (IncludeSpan.IsEmpty) { continue; } char EndChar; bool TrimQuotation = true; if (IncludeSpan[0] == '"') { EndChar = '"'; } else if (IncludeSpan[0] == '<') { EndChar = '>'; } else { EndChar = ')'; TrimQuotation = false; } if (TrimQuotation) { IncludeSpan = IncludeSpan.Slice(1); } if (IncludeSpan.Contains("HEADER_UNIT_IGNORE", StringComparison.OrdinalIgnoreCase)) { continue; } int EndIndex = IncludeSpan.IndexOf(EndChar); if (EndIndex == -1) { continue; } // This will include the ')' at the end if (!TrimQuotation) { EndIndex++; } IncludeSpan = IncludeSpan.Slice(0, EndIndex); Includes.Add(IncludeSpan.ToString()); } else if (InsideDeprecationScope != 0) { if (LineSpan.StartsWith("#endif")) { --InsideDeprecationScope; } else if (LineSpan.StartsWith("#if")) { ++InsideDeprecationScope; } } else if (LineSpan.StartsWith("#if")) { if (Line.Contains("UE_ENABLE_INCLUDE_ORDER_DEPRECATED_", StringComparison.CurrentCulture)) { ++InsideDeprecationScope; } } else if (LineSpan.StartsWith("UE_DEPRECATED_HEADER")) { HeaderFileInfo.UnitType = HeaderUnitType.Skip; } else { int HeaderUnitIndex = Line.IndexOf("HEADER_UNIT_"); if (HeaderUnitIndex != -1) { ReadOnlySpan Span = Line.AsSpan(HeaderUnitIndex + "HEADER_UNIT_".Length); if (Span.StartsWith("UNSUPPORTED")) { HeaderFileInfo.UnitType = HeaderUnitType.Unsupported; } else if (Span.StartsWith("SKIP")) { HeaderFileInfo.UnitType = HeaderUnitType.Skip; } } } } HeaderFileInfo.bContainsMarkup = bContainsMarkup; HeaderFileInfo.bUsesAPIDefine = bUsesAPIDefine; HeaderFileInfo.Includes = Includes.ToList(); } /// /// Gets the first included file from a source file /// /// The source file to parse /// Text from the first include directive. Null if the file did not contain any include directives. public string? GetFirstInclude(FileItem SourceFile) { return GetSourceFileInfo(SourceFile).IncludeText; } /// /// Finds or adds a SourceFile class for the given file /// /// File to fetch the source file data for /// SourceFile instance corresponding to the given source file public SourceFile GetSourceFile(FileItem File) { if (Parent != null && !File.Location.IsUnderDirectory(BaseDirectory)) { return Parent.GetSourceFile(File); } else { return FileToSourceFile.AddOrUpdate(File, _ => { return new SourceFile(File); }, (k, v) => { if (File.LastWriteTimeUtc.Ticks > v.LastWriteTimeUtc) { return new SourceFile(File); } return v; }); } } /// /// Returns a list of inlined generated cpps that this source file contains. /// /// The source file to parse /// List of marked files this source file contains public IList GetListOfInlinedGeneratedCppFiles(FileItem SourceFile) { return GetSourceFileInfo(SourceFile).InlinedFileNames; } /// /// Determines whether the given file contains reflection markup /// /// The source file to parse /// True if the file contains reflection markup public bool ContainsReflectionMarkup(FileItem HeaderFile) { return GetHeaderFileInfo(HeaderFile).bContainsMarkup; } /// /// Determines whether the given file uses the *_API define /// /// The source file to parse /// True if the file uses the *_API define public bool UsesAPIDefine(FileItem HeaderFile) { return GetHeaderFileInfo(HeaderFile).bUsesAPIDefine; } /// /// Returns header unit type for a header file (and parse the file if not already cached) /// /// The header file to parse /// Header unit type public HeaderUnitType GetHeaderUnitType(FileItem HeaderFile) { return GetHeaderFileInfo(HeaderFile).UnitType; } /// /// Returns all #includes existing inside a header file (and parse the file if not already cached) /// /// The header file to parse /// List of includes public List GetHeaderIncludes(FileItem HeaderFile) { return GetHeaderFileInfo(HeaderFile).Includes!; } /// /// Creates a cache hierarchy for a particular target /// /// Project file for the target being built /// Logger for output /// Dependency cache hierarchy for the given project public static SourceFileMetadataCache CreateHierarchy(FileReference? ProjectFile, ILogger Logger) { SourceFileMetadataCache? Cache = null; if (ProjectFile == null || !Unreal.IsEngineInstalled()) { FileReference EngineCacheLocation = FileReference.Combine(Unreal.EngineDirectory, "Intermediate", "Build", "SourceFileCache.bin"); Cache = FindOrAddCache(EngineCacheLocation, Unreal.EngineDirectory, Cache, Logger); } if (ProjectFile != null) { FileReference ProjectCacheLocation = FileReference.Combine(ProjectFile.Directory, "Intermediate", "Build", "SourceFileCache.bin"); Cache = FindOrAddCache(ProjectCacheLocation, ProjectFile.Directory, Cache, Logger); } return Cache!; } /// /// Enumerates all the locations of metadata caches for the given target /// /// Project file for the target being built /// Dependency cache hierarchy for the given project public static IEnumerable GetFilesToClean(FileReference? ProjectFile) { if (ProjectFile == null || !Unreal.IsEngineInstalled()) { yield return FileReference.Combine(Unreal.EngineDirectory, "Intermediate", "Build", "SourceFileCache.bin"); } if (ProjectFile != null) { yield return FileReference.Combine(ProjectFile.Directory, "Intermediate", "Build", "SourceFileCache.bin"); } } /// /// Reads a cache from the given location, or creates it with the given settings /// /// File to store the cache /// Base directory for files that this cache should store data for /// The parent cache to use /// /// Reference to a dependency cache with the given settings static SourceFileMetadataCache FindOrAddCache(FileReference Location, DirectoryReference BaseDirectory, SourceFileMetadataCache? Parent, ILogger Logger) { SourceFileMetadataCache Cache = Caches.GetOrAdd(Location, _ => { return new SourceFileMetadataCache(Location, BaseDirectory, Parent, Logger); ; }); Debug.Assert(Cache.BaseDirectory == BaseDirectory); Debug.Assert(Cache.Parent == Parent); return Cache; } /// /// Save all the caches that have been modified /// public static void SaveAll() { Parallel.ForEach(Caches.Values, Cache => { if (Cache.bModified) { Cache.Write(); } }); } /// /// Reads data for this dependency cache from disk /// private void Read() { try { using (BinaryArchiveReader Reader = new BinaryArchiveReader(Location)) { int Version = Reader.ReadInt(); if (Version != CurrentVersion) { Logger.LogDebug("Unable to read dependency cache from {File}; version {Version} vs current {CurrentVersion}", Location, Version, CurrentVersion); return; } int FileToMarkupFlagCount = Reader.ReadInt(); for (int Idx = 0; Idx < FileToMarkupFlagCount; Idx++) { FileItem File = Reader.ReadCompactFileItem(); HeaderFileInfo HeaderFileInfo = new HeaderFileInfo(); HeaderFileInfo.LastWriteTimeUtc = Reader.ReadLong(); HeaderFileInfo.bContainsMarkup = Reader.ReadBool(); HeaderFileInfo.bUsesAPIDefine = Reader.ReadBool(); HeaderFileInfo.UnitType = (HeaderUnitType)Reader.ReadByte(); HeaderFileInfo.Includes = Reader.ReadList(() => Reader.ReadString())!; FileToHeaderFileInfo[File] = HeaderFileInfo; } int FileToInlineMarkupFlagCount = Reader.ReadInt(); for (int Idx = 0; Idx < FileToInlineMarkupFlagCount; Idx++) { FileItem File = Reader.ReadCompactFileItem(); SourceFileInfo SourceFileInfo = new SourceFileInfo(); SourceFileInfo.LastWriteTimeUtc = Reader.ReadLong(); SourceFileInfo.IncludeText = Reader.ReadString(); SourceFileInfo.InlinedFileNames = Reader.ReadList(() => Reader.ReadString())!; FileToSourceFileInfo[File] = SourceFileInfo; } } } catch (Exception Ex) { Logger.LogWarning("Unable to read {Location}. See log for additional information.", Location); Logger.LogDebug(Ex, "{Ex}", ExceptionUtils.FormatExceptionDetails(Ex)); } } /// /// Writes data for this dependency cache to disk /// private void Write() { DirectoryReference.CreateDirectory(Location.Directory); using (FileStream Stream = File.Open(Location.FullName, FileMode.Create, FileAccess.Write, FileShare.Read)) { using (BinaryArchiveWriter Writer = new BinaryArchiveWriter(Stream)) { Writer.WriteInt(CurrentVersion); Writer.WriteInt(FileToHeaderFileInfo.Count); foreach (KeyValuePair Pair in FileToHeaderFileInfo) { Writer.WriteCompactFileItem(Pair.Key); Writer.WriteLong(Pair.Value.LastWriteTimeUtc); Writer.WriteBool(Pair.Value.bContainsMarkup); Writer.WriteBool(Pair.Value.bUsesAPIDefine); Writer.WriteByte((byte)Pair.Value.UnitType); Writer.WriteList(Pair.Value.Includes, Item => Writer.WriteString(Item)); } Writer.WriteInt(FileToSourceFileInfo.Count); foreach (KeyValuePair Pair in FileToSourceFileInfo) { Writer.WriteCompactFileItem(Pair.Key); Writer.WriteLong(Pair.Value.LastWriteTimeUtc); Writer.WriteString(Pair.Value.IncludeText); Writer.WriteList(Pair.Value.InlinedFileNames, Item => Writer.WriteString(Item)); } } } bModified = false; } } }