// 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;
}
}
}