// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using EpicGames.Core;
using UnrealBuildBase;
namespace UnrealBuildTool
{
class Unity
{
///
/// Prefix used for all dynamically created Unity modules
///
public const string ModulePrefix = "Module.";
///
/// A class which represents a list of files and the sum of their lengths.
///
public class FileCollection
{
public List Files { get; private set; }
public List VirtualFiles { get; private set; }
public long TotalLength { get; private set; }
/// The length of this file collection, plus any additional virtual space needed for bUseAdapativeUnityBuild.
/// See the comment above AddVirtualFile() below for more information.
public long VirtualLength { get; private set; }
public FileCollection()
{
Files = new List();
VirtualFiles = new List();
TotalLength = 0;
VirtualLength = 0;
}
public void AddFile(FileItem File)
{
Files.Add(File);
long FileLength = File.Length;
TotalLength += FileLength;
VirtualLength += FileLength;
}
///
/// Doesn't actually add a file, but instead reserves space. This is used with "bUseAdaptiveUnityBuild", to prevent
/// other compiled unity blobs in the module's numbered set from having to be recompiled after we eject source files
/// one of that module's unity blobs. Basically, it can prevent dozens of files from being recompiled after the first
/// time building after your working set of source files changes
///
/// The virtual file to add to the collection
public void AddVirtualFile(FileItem File)
{
VirtualFiles.Add(File);
VirtualLength += File.Length;
}
}
///
/// A class for building up a set of unity files. You add files one-by-one using AddFile then call EndCurrentUnityFile to finish that one and
/// (perhaps) begin a new one.
///
public class UnityFileBuilder
{
private List UnityFiles;
private FileCollection CurrentCollection;
private int SplitLength;
///
/// Constructs a new UnityFileBuilder.
///
/// The accumulated length at which to automatically split a unity file, or -1 to disable automatic splitting.
public UnityFileBuilder(int InSplitLength)
{
UnityFiles = new List();
CurrentCollection = new FileCollection();
SplitLength = InSplitLength;
}
///
/// Adds a file to the current unity file. If splitting is required and the total size of the
/// unity file exceeds the split limit, then a new file is automatically started.
///
/// The file to add.
public void AddFile(FileItem File)
{
if (SplitLength != -1 && File.Length > SplitLength)
{
EndCurrentUnityFile();
}
CurrentCollection.AddFile(File);
if (SplitLength != -1 && CurrentCollection.VirtualLength > SplitLength)
{
EndCurrentUnityFile();
}
}
///
/// Doesn't actually add a file, but instead reserves space, then splits the unity blob normally as if it
/// was a real file that was added. See the comment above FileCollection.AddVirtualFile() for more info.
///
/// The file to add virtually. Only the size of the file is tracked.
public void AddVirtualFile(FileItem File)
{
if (SplitLength != -1 && File.Length > SplitLength)
{
EndCurrentUnityFile();
}
CurrentCollection.AddVirtualFile(File);
if (SplitLength != -1 && CurrentCollection.VirtualLength > SplitLength)
{
EndCurrentUnityFile();
}
}
///
/// Starts a new unity file. If the current unity file contains no files, this function has no effect, i.e. you will not get an empty unity file.
///
public void EndCurrentUnityFile()
{
if (CurrentCollection.Files.Count == 0)
{
return;
}
UnityFiles.Add(CurrentCollection);
CurrentCollection = new FileCollection();
}
///
/// Returns the list of built unity files. The UnityFileBuilder is unusable after this.
///
///
public List GetUnityFiles()
{
EndCurrentUnityFile();
List Result = UnityFiles;
// Null everything to ensure that failure will occur if you accidentally reuse this object.
CurrentCollection = null!;
UnityFiles = null!;
return Result;
}
}
///
/// Given a set of source files, generates another set of source files that #include all the original
/// files, the goal being to compile the same code in fewer translation units.
/// The "unity" files are written to the IntermediateDirectory.
///
/// The target we're building
/// The source files to #include.
/// The header files that might correspond to the source files.
/// The environment that is used to compile the source files.
/// Interface to query files which belong to the working set
/// Base name to use for the Unity files
/// Intermediate directory for unity source files
/// The makefile being built
/// Receives a mapping of source file to unity file
/// Receives the files to compile using the normal configuration.
/// Receives the files to compile using the adaptive unity configuration.
/// An approximate number of bytes of source code to target for inclusion in a single unified source file.
public static void GenerateUnitySource(
ReadOnlyTargetRules Target,
List SourceFiles,
List HeaderFiles,
CppCompileEnvironment CompileEnvironment,
ISourceFileWorkingSet WorkingSet,
string BaseName,
DirectoryReference IntermediateDirectory,
IActionGraphBuilder Graph,
Dictionary SourceFileToUnityFile,
out List NormalFiles,
out List AdaptiveFiles,
int NumIncludedBytesPerUnitySource)
{
List NewSourceFiles = new List();
// Figure out size of all input files combined. We use this to determine whether to use larger unity threshold or not.
long TotalBytesInSourceFiles = SourceFiles.Sum(F => F.Length);
// We have an increased threshold for unity file size if, and only if, all files fit into the same unity file. This
// is beneficial when dealing with PCH files. The default PCH creation limit is X unity files so if we generate < X
// this could be fairly slow and we'd rather bump the limit a bit to group them all into the same unity file.
// Optimization only makes sense if PCH files are enabled.
bool bForceIntoSingleUnityFile = Target.bStressTestUnity || (TotalBytesInSourceFiles < NumIncludedBytesPerUnitySource * 2 && Target.bUsePCHFiles);
// Even if every single file in the module appears in the working set adaptive unity should still be used even if it's slower.
GetAdaptiveFiles(Target, SourceFiles, HeaderFiles, CompileEnvironment, WorkingSet, BaseName, IntermediateDirectory, Graph, out NormalFiles, out AdaptiveFiles);
// Build the list of unity files.
List AllUnityFiles;
{
// Sort the incoming file paths lexicographically, so there will be consistency in unity blobs across multiple machines.
// Note that we're relying on this not only sorting files within each directory, but also the directories
// themselves, so the whole list of file paths is the same across computers.
// Case-insensitive file path compare, because you never know what is going on with local file systems.
List SortedSourceFiles = [.. SourceFiles];
SortedSourceFiles.Sort((A, B) =>
{
// Generated files from UHT need to be first in the list because they implement templated functions that aren't
// declared in the header but are required to link. If they are placed later in the list, you will see
// compile errors because the templated function is instantiated but is defined later in the same translation unit
// which results in 'error C2908: explicit specialization; '*****' has already been instantiated'
bool bAIsGenerated = A.AbsolutePath.EndsWith(".gen.cpp") || CompileEnvironment.FileMatchesExtraGeneratedCPPTypes(A.AbsolutePath);
bool bBIsGenerated = B.AbsolutePath.EndsWith(".gen.cpp") || CompileEnvironment.FileMatchesExtraGeneratedCPPTypes(B.AbsolutePath);
if (bAIsGenerated != bBIsGenerated)
{
return bAIsGenerated && !bBIsGenerated ? -1 : 1;
}
// Sort oversized files to the end of the list so they will be placed into their own unity file
if (!bForceIntoSingleUnityFile)
{
bool bAIsOversized = A.Length > NumIncludedBytesPerUnitySource;
bool bBIsOversized = B.Length > NumIncludedBytesPerUnitySource;
if (bAIsOversized != bBIsOversized)
{
return !bAIsOversized && bBIsOversized ? -1 : 1;
}
}
return String.Compare(A.AbsolutePath, B.AbsolutePath, StringComparison.OrdinalIgnoreCase);
});
HashSet AdaptiveFileSet = [.. AdaptiveFiles];
UnityFileBuilder SourceUnityFileBuilder = new(bForceIntoSingleUnityFile ? -1 : NumIncludedBytesPerUnitySource);
foreach (FileItem SourceFile in SortedSourceFiles)
{
if (!bForceIntoSingleUnityFile && SourceFile.AbsolutePath.Contains(".GeneratedWrapper.", StringComparison.InvariantCultureIgnoreCase))
{
NewSourceFiles.Add(SourceFile);
}
// When adaptive unity is enabled, go ahead and exclude any source files that we're actively working with
if (AdaptiveFileSet.Contains(SourceFile))
{
// Let the unity file builder know about the file, so that we can retain the existing size of the unity blobs.
// This won't actually make the source file part of the unity blob, but it will keep track of how big the
// file is so that other existing unity blobs from the same module won't be invalidated. This prevents much
// longer compile times the first time you build after your working file set changes.
SourceUnityFileBuilder.AddVirtualFile(SourceFile);
}
else
{
// Compile this file as part of the unity blob
SourceUnityFileBuilder.AddFile(SourceFile);
}
}
AllUnityFiles = SourceUnityFileBuilder.GetUnityFiles();
}
// Create a set of CPP files that combine smaller CPP files into larger compilation units, along with the corresponding
// actions to compile them.
int CurrentUnityFileCount = 0;
foreach (FileCollection UnityFile in AllUnityFiles)
{
++CurrentUnityFileCount;
FileItem FirstFile = UnityFile.Files.FirstOrDefault() ?? UnityFile.VirtualFiles.First();
string UnityExt = FirstFile.Location.GetExtension().ToLowerInvariant();
StringWriter OutputUnitySourceWriter = new();
OutputUnitySourceWriter.WriteLine($"// This file is automatically generated at compile-time to include some subset of the user-created {UnityExt.Trim('.')} files.");
// Determine unity file path name
string UnitySourceFileName = AllUnityFiles.Count > 1
? $"{ModulePrefix}{BaseName}.{CurrentUnityFileCount}{UnityExt}"
: $"{ModulePrefix}{BaseName}{UnityExt}";
FileReference UnitySourceFilePath = FileReference.Combine(IntermediateDirectory, UnitySourceFileName);
List InlinedGenCPPFilesInUnity = [];
// Add source files to the unity file
foreach (FileItem SourceFile in UnityFile.Files)
{
string SourceFileString = SourceFile.AbsolutePath;
if (CompileEnvironment.RootPaths.GetVfsOverlayPath(SourceFile.Location, out string? vfsPath))
{
SourceFileString = vfsPath;
}
else if (SourceFile.Location.IsUnderDirectory(Unreal.RootDirectory))
{
SourceFileString = SourceFile.Location.MakeRelativeTo(Unreal.EngineSourceDirectory);
}
OutputUnitySourceWriter.WriteLine("#include \"{0}\"", SourceFileString.Replace('\\', '/'));
List? InlinedGenCPPFiles;
if (CompileEnvironment.FileInlineGenCPPMap.TryGetValue(SourceFile, out InlinedGenCPPFiles))
{
InlinedGenCPPFilesInUnity.AddRange(InlinedGenCPPFiles);
}
}
// Write the unity file to the intermediate folder.
FileItem UnitySourceFile = Graph.CreateIntermediateTextFile(UnitySourceFilePath, OutputUnitySourceWriter.ToString());
NewSourceFiles.Add(UnitySourceFile);
// Store all the inlined gen.cpp files
CompileEnvironment.FileInlineGenCPPMap[UnitySourceFile] = InlinedGenCPPFilesInUnity;
// Store the mapping of source files to unity files in the makefile
foreach (FileItem SourceFile in UnityFile.Files)
{
SourceFileToUnityFile[SourceFile] = UnitySourceFile;
}
foreach (FileItem SourceFile in UnityFile.VirtualFiles)
{
SourceFileToUnityFile[SourceFile] = UnitySourceFile;
}
}
NormalFiles = NewSourceFiles;
}
public static void GetAdaptiveFiles(
ReadOnlyTargetRules Target,
List CPPFiles,
List HeaderFiles,
CppCompileEnvironment CompileEnvironment,
ISourceFileWorkingSet WorkingSet,
string BaseName,
DirectoryReference IntermediateDirectory,
IActionGraphBuilder Graph,
out List NormalFiles,
out List AdaptiveFiles)
{
NormalFiles = new List();
AdaptiveFiles = new List();
if (!Target.bUseAdaptiveUnityBuild || Target.bStressTestUnity)
{
NormalFiles = CPPFiles;
return;
}
HashSet HeaderFilesInWorkingSet = new HashSet(HeaderFiles.Where(WorkingSet.Contains));
// Figure out which uniquely-named header files are in the working set.
// Unique names are important to avoid ambiguity about which header a source file includes.
Dictionary NameToHeaderFileInWorkingSet = new Dictionary();
List DuplicateHeaderNames = new List();
HashSet HeaderNames = new HashSet();
foreach (FileItem HeaderFile in HeaderFiles)
{
string HeaderFileName = HeaderFile.Location.GetFileName();
if (!HeaderNames.Add(HeaderFileName))
{
DuplicateHeaderNames.Add(HeaderFileName);
}
else if (HeaderFilesInWorkingSet.Contains(HeaderFile))
{
NameToHeaderFileInWorkingSet[HeaderFileName] = HeaderFile;
}
}
foreach (string Name in DuplicateHeaderNames)
{
NameToHeaderFileInWorkingSet.Remove(Name);
}
HashSet UnhandledHeaderFilesInWorkingSet = new(HeaderFilesInWorkingSet);
// Add source files to the adaptive set if they or their first included header are in the working set.
foreach (FileItem CPPFile in CPPFiles)
{
bool bHeaderInWorkingSet = false;
if (CompileEnvironment.MetadataCache.GetFirstInclude(CPPFile) is string FirstInclude &&
NameToHeaderFileInWorkingSet.TryGetValue(Path.GetFileName(FirstInclude), out FileItem? HeaderFile))
{
bHeaderInWorkingSet = true;
UnhandledHeaderFilesInWorkingSet.Remove(HeaderFile);
}
bool bAdaptive = (bHeaderInWorkingSet || WorkingSet.Contains(CPPFile)) && !CompileEnvironment.FileMatchesExtraGeneratedCPPTypes(CPPFile.FullName);
List Files = bAdaptive ? AdaptiveFiles : NormalFiles;
Files.Add(CPPFile);
}
// Add adaptive files to the working set that will invalidate the makefile if it changes.
foreach (FileItem File in AdaptiveFiles)
{
Graph.AddFileToWorkingSet(File);
}
// We also need to add the headers since we don't want to invalidate makefile if they are changing
foreach (FileItem File in HeaderFilesInWorkingSet)
{
Graph.AddFileToWorkingSet(File);
}
// Add header files in the working set to the adaptive files if they are not the first include of a source file.
if (Target.bAdaptiveUnityCompilesHeaderFiles)
{
foreach (FileItem HeaderFile in UnhandledHeaderFilesInWorkingSet)
{
StringWriter OutputHeaderCPPWriter = new StringWriter();
OutputHeaderCPPWriter.WriteLine("// This file is automatically generated at compile-time to include a modified header file.");
OutputHeaderCPPWriter.WriteLine($"#include \"{HeaderFile.AbsolutePath.Replace('\\', '/')}\"");
string HeaderCPPFileName = $"{HeaderFile.Location.GetFileNameWithoutExtension()}.h.cpp";
FileReference HeaderCPPFilePath = FileReference.Combine(IntermediateDirectory, HeaderCPPFileName);
AdaptiveFiles.Add(Graph.CreateIntermediateTextFile(HeaderCPPFilePath, OutputHeaderCPPWriter.ToString()));
}
}
HashSet CandidateAdaptiveFiles = new HashSet();
CandidateAdaptiveFiles.UnionWith(CPPFiles);
CandidateAdaptiveFiles.UnionWith(HeaderFiles);
CandidateAdaptiveFiles.ExceptWith(AdaptiveFiles);
CandidateAdaptiveFiles.ExceptWith(HeaderFilesInWorkingSet);
foreach (FileItem File in CandidateAdaptiveFiles)
{
Graph.AddCandidateForWorkingSet(File);
}
}
}
}