// 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; using System.Text.RegularExpressions; using System.Threading.Tasks; using EpicGames.Core; using OpenTracing.Util; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Caches information about C++ source files; whether they contain reflection markup, what the first included header is, and so on. /// class SourceFileMetadataCache { /// /// Information about the first file included from a source file /// class IncludeInfo { /// /// Last write time of the file when the data was cached /// public long LastWriteTimeUtc; /// /// Contents of the include directive /// public string? IncludeText; } /// /// Information about whether a file contains reflection markup /// class ReflectionInfo { /// /// 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; } /// /// The current file version /// public const int CurrentVersion = 3; /// /// 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 FileToIncludeInfo = new ConcurrentDictionary(); /// /// Map from file item to header file info /// ConcurrentDictionary FileToReflectionInfo = new ConcurrentDictionary(); /// /// Map from file item to source file info /// ConcurrentDictionary FileToSourceFile = new ConcurrentDictionary(); /// /// Whether the cache has been modified and needs to be saved /// bool bModified; /// /// 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 | 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 Dictionary Caches = new Dictionary(); /// /// 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 private SourceFileMetadataCache(FileReference Location, DirectoryReference BaseDir, SourceFileMetadataCache? Parent) { this.Location = Location; this.BaseDirectory = BaseDir; this.Parent = Parent; if(FileReference.Exists(Location)) { using (GlobalTracer.Instance.BuildSpan("Reading source file metadata cache").StartActive()) { Read(); } } } /// /// 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) { if(Parent != null && !SourceFile.Location.IsUnderDirectory(BaseDirectory)) { return Parent.GetFirstInclude(SourceFile); } else { IncludeInfo? IncludeInfo; if(!FileToIncludeInfo.TryGetValue(SourceFile, out IncludeInfo) || SourceFile.LastWriteTimeUtc.Ticks > IncludeInfo.LastWriteTimeUtc) { IncludeInfo = new IncludeInfo(); IncludeInfo.LastWriteTimeUtc = SourceFile.LastWriteTimeUtc.Ticks; IncludeInfo.IncludeText = ParseFirstInclude(SourceFile.Location); FileToIncludeInfo[SourceFile] = IncludeInfo; bModified = true; } return IncludeInfo.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 { SourceFile? Result; if (!FileToSourceFile.TryGetValue(File, out Result) || File.LastWriteTimeUtc.Ticks > Result.LastWriteTimeUtc) { SourceFile NewSourceFile = new SourceFile(File); if (Result == null) { if (FileToSourceFile.TryAdd(File, NewSourceFile)) { Result = NewSourceFile; } else { Result = FileToSourceFile[File]; } } else { if (FileToSourceFile.TryUpdate(File, NewSourceFile, Result)) { Result = NewSourceFile; } else { Result = FileToSourceFile[File]; } } } return Result; } } /// /// Determines whether the given file contains reflection markup /// /// The source file to parse /// True if the file contains reflection markup public bool ContainsReflectionMarkup(FileItem SourceFile) { if(Parent != null && !SourceFile.Location.IsUnderDirectory(BaseDirectory)) { return Parent.ContainsReflectionMarkup(SourceFile); } else { ReflectionInfo? ReflectionInfo; if(!FileToReflectionInfo.TryGetValue(SourceFile, out ReflectionInfo) || SourceFile.LastWriteTimeUtc.Ticks > ReflectionInfo.LastWriteTimeUtc) { ReflectionInfo = new ReflectionInfo(); ReflectionInfo.LastWriteTimeUtc = SourceFile.LastWriteTimeUtc.Ticks; ReflectionInfo.bContainsMarkup = ReflectionMarkupRegex.IsMatch(FileReference.ReadAllText(SourceFile.Location)); FileToReflectionInfo[SourceFile] = ReflectionInfo; bModified = true; } return ReflectionInfo.bContainsMarkup; } } /// /// Parse the first include directive from a source file /// /// The source file to parse /// The first include directive static string? ParseFirstInclude(FileReference SourceFile) { bool bMatchImport = SourceFile.HasExtension(".m") || SourceFile.HasExtension(".mm"); using(StreamReader Reader = new StreamReader(SourceFile.FullName, true)) { for(;;) { string? Line = Reader.ReadLine(); 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 IncludeMatch.Groups[1].Value; } } } } } /// /// Creates a cache hierarchy for a particular target /// /// Project file for the target being built /// Dependency cache hierarchy for the given project public static SourceFileMetadataCache CreateHierarchy(FileReference? ProjectFile) { SourceFileMetadataCache? Cache = null; if(ProjectFile == null || !Unreal.IsEngineInstalled()) { FileReference EngineCacheLocation = FileReference.Combine(Unreal.EngineDirectory, "Intermediate", "Build", "SourceFileCache.bin"); Cache = FindOrAddCache(EngineCacheLocation, Unreal.EngineDirectory, Cache); } if(ProjectFile != null) { FileReference ProjectCacheLocation = FileReference.Combine(ProjectFile.Directory, "Intermediate", "Build", "SourceFileCache.bin"); Cache = FindOrAddCache(ProjectCacheLocation, ProjectFile.Directory, Cache); } 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) { lock(Caches) { SourceFileMetadataCache? Cache; if(Caches.TryGetValue(Location, out Cache)) { Debug.Assert(Cache.BaseDirectory == BaseDirectory); Debug.Assert(Cache.Parent == Parent); } else { Cache = new SourceFileMetadataCache(Location, BaseDirectory, Parent); Caches.Add(Location, Cache); } 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) { Log.TraceLog("Unable to read dependency cache from {0}; version {1} vs current {2}", Location, Version, CurrentVersion); return; } int FileToFirstIncludeCount = Reader.ReadInt(); for(int Idx = 0; Idx < FileToFirstIncludeCount; Idx++) { FileItem File = Reader.ReadCompactFileItem(); IncludeInfo IncludeInfo = new IncludeInfo(); IncludeInfo.LastWriteTimeUtc = Reader.ReadLong(); IncludeInfo.IncludeText = Reader.ReadString(); FileToIncludeInfo[File] = IncludeInfo; } int FileToMarkupFlagCount = Reader.ReadInt(); for(int Idx = 0; Idx < FileToMarkupFlagCount; Idx++) { FileItem File = Reader.ReadCompactFileItem(); ReflectionInfo ReflectionInfo = new ReflectionInfo(); ReflectionInfo.LastWriteTimeUtc = Reader.ReadLong(); ReflectionInfo.bContainsMarkup = Reader.ReadBool(); FileToReflectionInfo[File] = ReflectionInfo; } } } catch(Exception Ex) { Log.TraceWarning("Unable to read {0}. See log for additional information.", Location); Log.TraceLog("{0}", 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(FileToIncludeInfo.Count); foreach(KeyValuePair Pair in FileToIncludeInfo) { Writer.WriteCompactFileItem(Pair.Key); Writer.WriteLong(Pair.Value.LastWriteTimeUtc); Writer.WriteString(Pair.Value.IncludeText); } Writer.WriteInt(FileToReflectionInfo.Count); foreach(KeyValuePair Pair in FileToReflectionInfo) { Writer.WriteCompactFileItem(Pair.Key); Writer.WriteLong(Pair.Value.LastWriteTimeUtc); Writer.WriteBool(Pair.Value.bContainsMarkup); } } } bModified = false; } } }