1343 lines
50 KiB
C#
1343 lines
50 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
using EpicGames.Core;
|
|
using EpicGames.UHT.Utils;
|
|
using Microsoft.Extensions.Logging;
|
|
using OpenTracing;
|
|
using OpenTracing.Util;
|
|
using UnrealBuildBase;
|
|
using UnrealBuildTool.Modes;
|
|
|
|
namespace UnrealBuildTool
|
|
{
|
|
static class UHTModuleTypeExtensions
|
|
{
|
|
public static UHTModuleType? EngineModuleTypeFromHostType(ModuleHostType ModuleType)
|
|
{
|
|
switch (ModuleType)
|
|
{
|
|
case ModuleHostType.Program:
|
|
return UHTModuleType.Program;
|
|
case ModuleHostType.Runtime:
|
|
case ModuleHostType.RuntimeNoCommandlet:
|
|
case ModuleHostType.RuntimeAndProgram:
|
|
case ModuleHostType.CookedOnly:
|
|
case ModuleHostType.ServerOnly:
|
|
case ModuleHostType.ClientOnly:
|
|
case ModuleHostType.ClientOnlyNoCommandlet:
|
|
return UHTModuleType.EngineRuntime;
|
|
case ModuleHostType.Developer:
|
|
case ModuleHostType.DeveloperTool:
|
|
return UHTModuleType.EngineDeveloper;
|
|
case ModuleHostType.Editor:
|
|
case ModuleHostType.EditorNoCommandlet:
|
|
case ModuleHostType.EditorAndProgram:
|
|
return UHTModuleType.EngineEditor;
|
|
case ModuleHostType.UncookedOnly:
|
|
return UHTModuleType.EngineUncooked;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
public static UHTModuleType? GameModuleTypeFromHostType(ModuleHostType ModuleType)
|
|
{
|
|
switch (ModuleType)
|
|
{
|
|
case ModuleHostType.Program:
|
|
return UHTModuleType.Program;
|
|
case ModuleHostType.Runtime:
|
|
case ModuleHostType.RuntimeNoCommandlet:
|
|
case ModuleHostType.RuntimeAndProgram:
|
|
case ModuleHostType.CookedOnly:
|
|
case ModuleHostType.ServerOnly:
|
|
case ModuleHostType.ClientOnly:
|
|
case ModuleHostType.ClientOnlyNoCommandlet:
|
|
return UHTModuleType.GameRuntime;
|
|
case ModuleHostType.Developer:
|
|
case ModuleHostType.DeveloperTool:
|
|
return UHTModuleType.GameDeveloper;
|
|
case ModuleHostType.Editor:
|
|
case ModuleHostType.EditorNoCommandlet:
|
|
case ModuleHostType.EditorAndProgram:
|
|
return UHTModuleType.GameEditor;
|
|
case ModuleHostType.UncookedOnly:
|
|
return UHTModuleType.GameUncooked;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Information about a module that needs to be passed to UnrealHeaderTool for code generation
|
|
/// </summary>
|
|
class UHTModuleInfo
|
|
{
|
|
/// <summary>
|
|
/// Module name
|
|
/// </summary>
|
|
public string ModuleName;
|
|
|
|
/// <summary>
|
|
/// Path to the module rules file
|
|
/// </summary>
|
|
public FileReference ModuleRulesFile;
|
|
|
|
/// <summary>
|
|
/// Paths to all potential module source directories (with platform extension directories added in)
|
|
/// </summary>
|
|
public DirectoryReference[] ModuleDirectories;
|
|
|
|
/// <summary>
|
|
/// The include search path for generated headers to include other headers
|
|
/// </summary>
|
|
public DirectoryReference[] ModuleIncludePaths;
|
|
|
|
/// <summary>
|
|
/// Module type
|
|
/// </summary>
|
|
public string ModuleType;
|
|
|
|
/// <summary>
|
|
/// Overridden package module type to add more flags
|
|
/// </summary>
|
|
public string OverrideModuleType;
|
|
|
|
/// <summary>
|
|
/// Public UObject headers found in the Classes directory (legacy)
|
|
/// </summary>
|
|
public List<FileItem> PublicUObjectClassesHeaders;
|
|
|
|
/// <summary>
|
|
/// Public headers with UObjects
|
|
/// </summary>
|
|
public List<FileItem> PublicUObjectHeaders;
|
|
|
|
/// <summary>
|
|
/// Internal headers with UObjects
|
|
/// </summary>
|
|
public List<FileItem> InternalUObjectHeaders;
|
|
|
|
/// <summary>
|
|
/// Private headers with UObjects
|
|
/// </summary>
|
|
public List<FileItem> PrivateUObjectHeaders;
|
|
|
|
/// <summary>
|
|
/// Directory containing generated code
|
|
/// </summary>
|
|
public DirectoryItem GeneratedCodeDirectory;
|
|
|
|
/// <summary>
|
|
/// Collection of the module's public defines
|
|
/// </summary>
|
|
public List<string> PublicDefines;
|
|
|
|
/// <summary>
|
|
/// Path and base filename, without extension, of the .gen files
|
|
/// </summary>
|
|
public string? GeneratedCPPFilenameBase;
|
|
|
|
/// <summary>
|
|
/// Version of code generated by UHT
|
|
/// </summary>
|
|
public EGeneratedCodeVersion GeneratedCodeVersion;
|
|
|
|
/// <summary>
|
|
/// Whether this module is read-only
|
|
/// </summary>
|
|
public bool bIsReadOnly;
|
|
|
|
/// <summary>
|
|
/// Path of the verse generated types
|
|
/// </summary>
|
|
public string VersePath;
|
|
|
|
/// <summary>
|
|
/// Scope of the verse definitions
|
|
/// </summary>
|
|
public VerseScope VerseScope;
|
|
|
|
/// <summary>
|
|
/// Mount point for UE definitions
|
|
/// </summary>
|
|
public string VerseMountPoint;
|
|
|
|
/// <summary>
|
|
/// If true, verse scripts were found
|
|
/// </summary>
|
|
public bool HasVerse;
|
|
|
|
/// <summary>
|
|
/// If true, autogenerated functions of USTRUCTS are always exported
|
|
/// </summary>
|
|
public bool AlwaysExportStructs;
|
|
|
|
/// <summary>
|
|
/// If true, autogenerated functions of UENUMS are always exported
|
|
/// </summary>
|
|
public bool AlwaysExportEnums;
|
|
|
|
public UHTModuleInfo(
|
|
string ModuleName,
|
|
FileReference ModuleRulesFile,
|
|
DirectoryReference[] ModuleDirectories,
|
|
DirectoryReference[] ModuleIncludePaths,
|
|
UHTModuleType ModuleType,
|
|
DirectoryItem GeneratedCodeDirectory,
|
|
EGeneratedCodeVersion GeneratedCodeVersion,
|
|
bool bIsReadOnly,
|
|
ModuleRules.PackageOverrideType OverrideType,
|
|
string VersePath,
|
|
VerseScope VerseScope,
|
|
bool HasVerse,
|
|
string VerseMountPoint,
|
|
bool AlwaysExportStructs,
|
|
bool AlwaysExportEnums)
|
|
{
|
|
this.ModuleName = ModuleName;
|
|
this.ModuleRulesFile = ModuleRulesFile;
|
|
this.ModuleDirectories = ModuleDirectories;
|
|
this.ModuleIncludePaths = ModuleIncludePaths;
|
|
this.ModuleType = ModuleType.ToString();
|
|
OverrideModuleType = OverrideType.ToString();
|
|
PublicUObjectClassesHeaders = new List<FileItem>();
|
|
PublicUObjectHeaders = new List<FileItem>();
|
|
InternalUObjectHeaders = new List<FileItem>();
|
|
PrivateUObjectHeaders = new List<FileItem>();
|
|
PublicDefines = new List<string>();
|
|
this.GeneratedCodeDirectory = GeneratedCodeDirectory;
|
|
this.GeneratedCodeVersion = GeneratedCodeVersion;
|
|
this.bIsReadOnly = bIsReadOnly;
|
|
this.VersePath = VersePath;
|
|
this.VerseScope = VerseScope;
|
|
this.HasVerse = HasVerse;
|
|
this.VerseMountPoint = VerseMountPoint;
|
|
this.AlwaysExportStructs = AlwaysExportStructs;
|
|
this.AlwaysExportEnums = AlwaysExportEnums;
|
|
}
|
|
|
|
public UHTModuleInfo(BinaryArchiveReader Reader)
|
|
{
|
|
ModuleName = Reader.ReadString()!;
|
|
ModuleRulesFile = Reader.ReadFileReference();
|
|
ModuleDirectories = Reader.ReadArray<DirectoryReference>(Reader.ReadDirectoryReferenceNotNull)!;
|
|
ModuleIncludePaths = Reader.ReadArray<DirectoryReference>(Reader.ReadDirectoryReferenceNotNull)!;
|
|
ModuleType = Reader.ReadString()!;
|
|
OverrideModuleType = Reader.ReadString()!;
|
|
PublicUObjectClassesHeaders = Reader.ReadList(() => Reader.ReadFileItem())!;
|
|
PublicUObjectHeaders = Reader.ReadList(() => Reader.ReadFileItem())!;
|
|
InternalUObjectHeaders = Reader.ReadList(() => Reader.ReadFileItem())!;
|
|
PrivateUObjectHeaders = Reader.ReadList(() => Reader.ReadFileItem())!;
|
|
GeneratedCPPFilenameBase = Reader.ReadString();
|
|
GeneratedCodeDirectory = Reader.ReadDirectoryItem()!;
|
|
GeneratedCodeVersion = (EGeneratedCodeVersion)Reader.ReadInt();
|
|
bIsReadOnly = Reader.ReadBool();
|
|
PublicDefines = Reader.ReadList(() => Reader.ReadString())!;
|
|
VersePath = Reader.ReadString()!;
|
|
VerseScope = (VerseScope)Reader.ReadInt();
|
|
HasVerse = Reader.ReadBool();
|
|
VerseMountPoint = Reader.ReadString()!;
|
|
AlwaysExportStructs = Reader.ReadBool();
|
|
AlwaysExportEnums = Reader.ReadBool();
|
|
}
|
|
|
|
public void Write(BinaryArchiveWriter Writer)
|
|
{
|
|
Writer.WriteString(ModuleName);
|
|
Writer.WriteFileReference(ModuleRulesFile);
|
|
Writer.WriteArray<DirectoryReference>(ModuleDirectories, Writer.WriteDirectoryReference);
|
|
Writer.WriteArray<DirectoryReference>(ModuleIncludePaths, Writer.WriteDirectoryReference);
|
|
Writer.WriteString(ModuleType);
|
|
Writer.WriteString(OverrideModuleType);
|
|
Writer.WriteList(PublicUObjectClassesHeaders, Item => Writer.WriteFileItem(Item));
|
|
Writer.WriteList(PublicUObjectHeaders, Item => Writer.WriteFileItem(Item));
|
|
Writer.WriteList(InternalUObjectHeaders, Item => Writer.WriteFileItem(Item));
|
|
Writer.WriteList(PrivateUObjectHeaders, Item => Writer.WriteFileItem(Item));
|
|
Writer.WriteString(GeneratedCPPFilenameBase);
|
|
Writer.WriteDirectoryItem(GeneratedCodeDirectory);
|
|
Writer.WriteInt((int)GeneratedCodeVersion);
|
|
Writer.WriteBool(bIsReadOnly);
|
|
Writer.WriteList(PublicDefines, Item => Writer.WriteString(Item));
|
|
Writer.WriteString(VersePath);
|
|
Writer.WriteInt((int)VerseScope);
|
|
Writer.WriteBool(HasVerse);
|
|
Writer.WriteString(VerseMountPoint);
|
|
Writer.WriteBool(AlwaysExportStructs);
|
|
Writer.WriteBool(AlwaysExportEnums);
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return ModuleName;
|
|
}
|
|
}
|
|
|
|
class UHTModuleHeaderInfo
|
|
{
|
|
public DirectoryItem SourceFolder;
|
|
public List<FileItem> HeaderFiles;
|
|
public bool bUsePrecompiled;
|
|
|
|
public UHTModuleHeaderInfo(DirectoryItem SourceFolder, List<FileItem> HeaderFiles, bool bUsePrecompiled)
|
|
{
|
|
this.SourceFolder = SourceFolder;
|
|
this.HeaderFiles = HeaderFiles;
|
|
this.bUsePrecompiled = bUsePrecompiled;
|
|
}
|
|
|
|
public UHTModuleHeaderInfo(BinaryArchiveReader Reader)
|
|
{
|
|
SourceFolder = Reader.ReadDirectoryItem()!;
|
|
HeaderFiles = Reader.ReadList(() => Reader.ReadFileItem())!;
|
|
bUsePrecompiled = Reader.ReadBool();
|
|
}
|
|
|
|
public void Write(BinaryArchiveWriter Writer)
|
|
{
|
|
Writer.WriteDirectoryItem(SourceFolder);
|
|
Writer.WriteList(HeaderFiles, Item => Writer.WriteFileItem(Item));
|
|
Writer.WriteBool(bUsePrecompiled);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This handles all running of the UnrealHeaderTool
|
|
/// </summary>
|
|
class ExternalExecution
|
|
{
|
|
static UHTModuleType GetEngineModuleTypeFromDescriptor(ModuleDescriptor Module)
|
|
{
|
|
UHTModuleType? Type = UHTModuleTypeExtensions.EngineModuleTypeFromHostType(Module.Type);
|
|
if (Type == null)
|
|
{
|
|
throw new BuildException("Unhandled engine module type {0} for {1}", Module.Type.ToString(), Module.Name);
|
|
}
|
|
return Type.GetValueOrDefault();
|
|
}
|
|
|
|
static UHTModuleType GetGameModuleTypeFromDescriptor(ModuleDescriptor Module)
|
|
{
|
|
UHTModuleType? Type = UHTModuleTypeExtensions.GameModuleTypeFromHostType(Module.Type);
|
|
if (Type == null)
|
|
{
|
|
throw new BuildException("Unhandled game module type {0}", Module.Type.ToString());
|
|
}
|
|
return Type.GetValueOrDefault();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the mount point for the given module
|
|
/// </summary>
|
|
/// <param name="Module"></param>
|
|
/// <returns></returns>
|
|
/// <exception cref="BuildException"></exception>
|
|
public static string GetVerseMountPointForModule(UEBuildModule Module)
|
|
{
|
|
if (Module.Rules.Plugin != null)
|
|
{
|
|
if (Module.bHasVerse && !Module.Rules.Plugin.Descriptor.bCanContainVerse)
|
|
{
|
|
throw new BuildException("Module '{0}' has an associated Verse directory but its containing plugin '{1}' does not specify \"CanContainVerse\": true.", Module.Name, Module.Rules.Plugin.Name);
|
|
}
|
|
|
|
return Module.Rules.Plugin.Name;
|
|
}
|
|
// We currently allow Verse only in plugins and programs
|
|
if (Module.bHasVerse && Module.Rules.Context.DefaultUHTModuleType.GetValueOrDefault(UHTModuleType.Program) != UHTModuleType.Program)
|
|
{
|
|
throw new BuildException("Module '{0}' has an associated Verse directory but it is neither in a plugin nor program.", Module.Name);
|
|
}
|
|
|
|
return Module.Rules.Context.Scope.Name == "Project" ? "Game" : "Engine";
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gets the module type for a given rules object
|
|
/// </summary>
|
|
/// <param name="RulesObject">The rules object</param>
|
|
/// <param name="ProjectDescriptor">Descriptor for the project being built</param>
|
|
/// <returns>The module type</returns>
|
|
static UHTModuleType GetModuleType(ModuleRules RulesObject, ProjectDescriptor? ProjectDescriptor)
|
|
{
|
|
ModuleRulesContext Context = RulesObject.Context;
|
|
if (Context.bClassifyAsGameModuleForUHT)
|
|
{
|
|
if (RulesObject.Type == ModuleRules.ModuleType.External)
|
|
{
|
|
return UHTModuleType.GameThirdParty;
|
|
}
|
|
if (Context.DefaultUHTModuleType.HasValue)
|
|
{
|
|
return Context.DefaultUHTModuleType.Value;
|
|
}
|
|
if (RulesObject.Plugin != null)
|
|
{
|
|
ModuleDescriptor? Module = RulesObject.Plugin.Descriptor.Modules?.FirstOrDefault(x => x.Name == RulesObject.Name);
|
|
if (Module != null)
|
|
{
|
|
return GetGameModuleTypeFromDescriptor(Module);
|
|
}
|
|
}
|
|
if (ProjectDescriptor != null && ProjectDescriptor.Modules != null)
|
|
{
|
|
ModuleDescriptor? Module = ProjectDescriptor.Modules.FirstOrDefault(x => x.Name == RulesObject.Name);
|
|
if (Module != null)
|
|
{
|
|
return UHTModuleTypeExtensions.GameModuleTypeFromHostType(Module.Type) ?? UHTModuleType.GameRuntime;
|
|
}
|
|
}
|
|
return UHTModuleType.GameRuntime;
|
|
}
|
|
else
|
|
{
|
|
if (RulesObject.Type == ModuleRules.ModuleType.External)
|
|
{
|
|
return UHTModuleType.EngineThirdParty;
|
|
}
|
|
if (Context.DefaultUHTModuleType.HasValue)
|
|
{
|
|
return Context.DefaultUHTModuleType.Value;
|
|
}
|
|
if (RulesObject.Plugin != null)
|
|
{
|
|
string PluginName = !RulesObject.IsTestModule ? RulesObject.Name : TargetDescriptor.GetTestedName(RulesObject.Name);
|
|
ModuleDescriptor? Module = RulesObject.Plugin.Descriptor.Modules?.FirstOrDefault(x => x.Name == PluginName);
|
|
if (Module != null)
|
|
{
|
|
return GetEngineModuleTypeFromDescriptor(Module);
|
|
}
|
|
}
|
|
throw new BuildException("Unable to determine UHT module type for {0}", RulesObject.File);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find all the headers under the given base directory, excluding any other platform folders.
|
|
/// </summary>
|
|
/// <param name="BaseDirectory">Base directory to search</param>
|
|
/// <param name="ExcludeFolders">Array of folders to exclude</param>
|
|
/// <param name="Headers">Receives the list of headers that was found</param>
|
|
static void FindHeaders(DirectoryItem BaseDirectory, IReadOnlySet<string> ExcludeFolders, List<FileItem> Headers)
|
|
{
|
|
if (BaseDirectory.TryGetFile(".ubtignore", out FileItem? OutIgnoreFile))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Check for all the headers in this folder
|
|
Headers.AddRange(BaseDirectory.EnumerateFiles().Where((fi) => fi.HasExtension(".h")));
|
|
|
|
foreach (DirectoryItem SubDirectory in BaseDirectory.EnumerateDirectories())
|
|
{
|
|
if (!ExcludeFolders.Contains(SubDirectory.Name))
|
|
{
|
|
FindHeaders(SubDirectory, ExcludeFolders, Headers);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void SetupUObjectModules(
|
|
IEnumerable<UEBuildModuleCPP> ModulesToGenerateHeadersFor,
|
|
UnrealTargetPlatform Platform,
|
|
ProjectDescriptor? ProjectDescriptor,
|
|
List<UHTModuleInfo> UObjectModules,
|
|
List<UHTModuleHeaderInfo> UObjectModuleHeaders,
|
|
EGeneratedCodeVersion GeneratedCodeVersion,
|
|
SourceFileMetadataCache MetadataCache,
|
|
ILogger Logger)
|
|
{
|
|
// Find the type of each module
|
|
Dictionary<UEBuildModuleCPP, UHTModuleType> ModuleToType = new Dictionary<UEBuildModuleCPP, UHTModuleType>();
|
|
foreach (UEBuildModuleCPP Module in ModulesToGenerateHeadersFor)
|
|
{
|
|
ModuleToType[Module] = GetModuleType(Module.Rules, ProjectDescriptor);
|
|
}
|
|
|
|
// Sort modules by type, then by dependency
|
|
List<UEBuildModuleCPP> ModulesSortedByType = ModulesToGenerateHeadersFor.OrderBy(c => ModuleToType[c]).ToList();
|
|
ModulesSortedByType = UEBuildModule.StableTopologicalSort(ModulesSortedByType.Cast<UEBuildModule>().ToList()).Cast<UEBuildModuleCPP>().ToList();
|
|
|
|
// Create the info for each module in parallel
|
|
UHTModuleInfo[] ModuleInfoArray = new UHTModuleInfo[ModulesSortedByType.Count];
|
|
using (ThreadPoolWorkQueue Queue = new ThreadPoolWorkQueue())
|
|
{
|
|
IReadOnlySet<string> ExcludedFolders = UEBuildPlatform.GetBuildPlatform(Platform).GetExcludedFolderNames();
|
|
for (int Idx = 0; Idx < ModulesSortedByType.Count; Idx++)
|
|
{
|
|
UEBuildModuleCPP Module = ModulesSortedByType[Idx];
|
|
|
|
DirectoryItem GeneratedCodeDirectory = DirectoryItem.GetItemByDirectoryReference(Module.GeneratedCodeDirectoryUHT!);
|
|
|
|
DirectoryReference[] ModuleIncludePaths = Module.PublicIncludePaths.Union(Module.InternalIncludePaths).Union(Module.PrivateIncludePaths).ToArray();
|
|
|
|
UHTModuleInfo Info = new UHTModuleInfo(
|
|
Module.Name,
|
|
Module.RulesFile,
|
|
Module.ModuleDirectories,
|
|
ModuleIncludePaths,
|
|
ModuleToType[Module],
|
|
GeneratedCodeDirectory,
|
|
GeneratedCodeVersion,
|
|
Module.Rules.bUsePrecompiled,
|
|
Module.Rules.OverridePackageType,
|
|
Module.Rules.VersePath ?? "",
|
|
Module.Rules.VerseScope,
|
|
Module.bHasVerse,
|
|
Module.bHasVerse ? GetVerseMountPointForModule(Module) : string.Empty,
|
|
Module.Rules.bAlwaysExportStructs,
|
|
Module.Rules.bAlwaysExportEnums);
|
|
ModuleInfoArray[Idx] = Info;
|
|
|
|
Queue.Enqueue(() => SetupUObjectModule(Info, ExcludedFolders, MetadataCache, Queue));
|
|
}
|
|
}
|
|
|
|
// Filter out all the modules with reflection data
|
|
for (int Idx = 0; Idx < ModulesSortedByType.Count; Idx++)
|
|
{
|
|
UEBuildModuleCPP Module = ModulesSortedByType[Idx];
|
|
UHTModuleInfo Info = ModuleInfoArray[Idx];
|
|
Info.PublicDefines.AddRange(Module.PublicDefinitions);
|
|
|
|
if (Info.PublicUObjectClassesHeaders.Count > 0 || Info.PrivateUObjectHeaders.Count > 0 || Info.PublicUObjectHeaders.Count > 0 || Info.InternalUObjectHeaders.Count > 0)
|
|
{
|
|
Module.bHasUObjects = true;
|
|
|
|
// If we've got this far and there are no source files then it's likely we're installed and ignoring
|
|
// engine files, so we don't need a .gen.cpp either
|
|
DirectoryReference GeneratedCodeDirectoryUHT = Module.GeneratedCodeDirectoryUHT!;
|
|
Info.GeneratedCPPFilenameBase = Path.Combine(GeneratedCodeDirectoryUHT.FullName, Info.ModuleName) + ".gen";
|
|
if (!Module.Rules.bUsePrecompiled)
|
|
{
|
|
Module.GeneratedCppDirectories ??= new List<string>();
|
|
Module.GeneratedCppDirectories.Add(GeneratedCodeDirectoryUHT.FullName);
|
|
}
|
|
|
|
UObjectModules.Add(Info);
|
|
|
|
DirectoryItem ModuleDirectoryItem = DirectoryItem.GetItemByDirectoryReference(Module.ModuleDirectory);
|
|
|
|
List<FileItem> ReflectedHeaderFiles = new List<FileItem>();
|
|
ReflectedHeaderFiles.AddRange(Info.PublicUObjectClassesHeaders);
|
|
ReflectedHeaderFiles.AddRange(Info.PublicUObjectHeaders);
|
|
ReflectedHeaderFiles.AddRange(Info.InternalUObjectHeaders);
|
|
ReflectedHeaderFiles.AddRange(Info.PrivateUObjectHeaders);
|
|
UObjectModuleHeaders.Add(new UHTModuleHeaderInfo(ModuleDirectoryItem, ReflectedHeaderFiles, Module.Rules.bUsePrecompiled));
|
|
}
|
|
else
|
|
{
|
|
// Remove any stale generated code directory
|
|
if (Module.GeneratedCodeDirectoryUHT != null && !Module.Rules.bUsePrecompiled)
|
|
{
|
|
if (DirectoryReference.Exists(Module.GeneratedCodeDirectoryUHT))
|
|
{
|
|
Directory.Delete(Module.GeneratedCodeDirectoryUHT.FullName, true);
|
|
// Also delete parent directory if now empty
|
|
if (!Directory.EnumerateFileSystemEntries(Module.GeneratedCodeDirectory!.FullName).Any())
|
|
{
|
|
Directory.Delete(Module.GeneratedCodeDirectory!.FullName, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set Module.bHasUObjects for any IncludePathModules not already processed.
|
|
// This is necessary to keep include paths consistent between targets built with -AllModules and without
|
|
// Note that PublicIncludePathModules can be recursive so we need to traverse the entire chain
|
|
List<UEBuildModuleCPP> IncludePathModules = new();
|
|
CollectModulesOnlyIncluded(ModulesToGenerateHeadersFor, IncludePathModules);
|
|
if (IncludePathModules.Count > 0)
|
|
{
|
|
Dictionary<UEBuildModuleCPP, UHTModuleInfo> IncludePathInfo = new();
|
|
using (ThreadPoolWorkQueue Queue = new ThreadPoolWorkQueue())
|
|
{
|
|
IReadOnlySet<string> ExcludedFolders = UEBuildPlatform.GetBuildPlatform(Platform).GetExcludedFolderNames();
|
|
foreach (UEBuildModuleCPP Module in IncludePathModules)
|
|
{
|
|
Queue.Enqueue(() =>
|
|
{
|
|
foreach (DirectoryItem ModuleDirectoryItem in Module.ModuleDirectories.Select(x => DirectoryItem.GetItemByDirectoryReference(x)))
|
|
{
|
|
List<FileItem> HeaderFiles = new();
|
|
FindHeaders(ModuleDirectoryItem, ExcludedFolders, HeaderFiles);
|
|
|
|
if (HeaderFiles.Any(x => MetadataCache.ContainsReflectionMarkup(x)))
|
|
{
|
|
Module.bHasUObjects = true;
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void CollectModulesOnlyIncluded(HashSet<UEBuildModuleCPP> HandledModules, List<UEBuildModule> IncludedModules, List<UEBuildModuleCPP> OutList)
|
|
{
|
|
foreach (UEBuildModule pi in IncludedModules)
|
|
{
|
|
if (pi is UEBuildModuleCPP IncludedModule)
|
|
{
|
|
if (IncludedModule.bHasUObjects)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!HandledModules.Add(IncludedModule))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
OutList.Add(IncludedModule);
|
|
|
|
if (IncludedModule.PublicIncludePathModules != null)
|
|
{
|
|
CollectModulesOnlyIncluded(HandledModules, IncludedModule.PublicIncludePathModules, OutList);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void CollectModulesOnlyIncluded(IEnumerable<UEBuildModuleCPP> ModulesToGenerateHeadersFor, List<UEBuildModuleCPP> OutList)
|
|
{
|
|
HashSet<UEBuildModuleCPP> HandledModules = new();
|
|
foreach (UEBuildModuleCPP Module in ModulesToGenerateHeadersFor)
|
|
{
|
|
HandledModules.Add(Module);
|
|
}
|
|
|
|
foreach (UEBuildModuleCPP Module in ModulesToGenerateHeadersFor)
|
|
{
|
|
if (Module.PublicIncludePathModules != null)
|
|
{
|
|
CollectModulesOnlyIncluded(HandledModules, Module.PublicIncludePathModules, OutList);
|
|
}
|
|
if (Module.PrivateIncludePathModules != null)
|
|
{
|
|
CollectModulesOnlyIncluded(HandledModules, Module.PrivateIncludePathModules, OutList);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void SetupUObjectModule(UHTModuleInfo ModuleInfo, IReadOnlySet<string> ExcludedFolders, SourceFileMetadataCache MetadataCache, ThreadPoolWorkQueue Queue)
|
|
{
|
|
foreach (DirectoryReference ModuleDirectory in ModuleInfo.ModuleDirectories)
|
|
{
|
|
DirectoryItem ModuleDirectoryItem = DirectoryItem.GetItemByDirectoryReference(ModuleDirectory);
|
|
|
|
List<FileItem> HeaderFiles = new List<FileItem>();
|
|
FindHeaders(ModuleDirectoryItem, ExcludedFolders, HeaderFiles);
|
|
|
|
foreach (FileItem HeaderFile in HeaderFiles)
|
|
{
|
|
Queue.Enqueue(() => SetupUObjectModuleHeader(ModuleInfo, HeaderFile, MetadataCache));
|
|
}
|
|
}
|
|
}
|
|
|
|
static void SetupUObjectModuleHeader(UHTModuleInfo ModuleInfo, FileItem HeaderFile, SourceFileMetadataCache MetadataCache)
|
|
{
|
|
// Check to see if we know anything about this file. If we have up-to-date cached information about whether it has
|
|
// UObjects or not, we can skip doing a test here.
|
|
if (MetadataCache.ContainsReflectionMarkup(HeaderFile))
|
|
{
|
|
lock (ModuleInfo)
|
|
{
|
|
bool bFoundHeaderLocation = false;
|
|
foreach (DirectoryReference ModuleDirectory in ModuleInfo.ModuleDirectories)
|
|
{
|
|
if (HeaderFile.Location.IsUnderDirectory(DirectoryReference.Combine(ModuleDirectory, "Classes")))
|
|
{
|
|
ModuleInfo.PublicUObjectClassesHeaders.Add(HeaderFile);
|
|
bFoundHeaderLocation = true;
|
|
}
|
|
else if (HeaderFile.Location.IsUnderDirectory(DirectoryReference.Combine(ModuleDirectory, "Public")))
|
|
{
|
|
ModuleInfo.PublicUObjectHeaders.Add(HeaderFile);
|
|
bFoundHeaderLocation = true;
|
|
}
|
|
else if (HeaderFile.Location.IsUnderDirectory(DirectoryReference.Combine(ModuleDirectory, "Internal")))
|
|
{
|
|
ModuleInfo.InternalUObjectHeaders.Add(HeaderFile);
|
|
bFoundHeaderLocation = true;
|
|
}
|
|
}
|
|
if (!bFoundHeaderLocation)
|
|
{
|
|
ModuleInfo.PrivateUObjectHeaders.Add(HeaderFile);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets UnrealHeaderTool.exe path. Does not care if UnrealheaderTool was build as a monolithic exe or not.
|
|
/// </summary>
|
|
static FileReference GetHeaderToolPath(FileReference ReceiptFile)
|
|
{
|
|
TargetReceipt Receipt = TargetReceipt.Read(ReceiptFile);
|
|
if (Receipt.Launch == null)
|
|
{
|
|
throw new BuildException("'Launch' property not set in UHT receipt.");
|
|
}
|
|
return Receipt.Launch;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the latest write time of any of the UnrealHeaderTool binaries (including DLLs and Plugins) or DateTime.MaxValue if UnrealHeaderTool does not exist
|
|
/// </summary>
|
|
/// <returns>Latest timestamp of UHT binaries or DateTime.MaxValue if UnrealHeaderTool is out of date and needs to be rebuilt.</returns>
|
|
static bool GetHeaderToolTimestampUtc(FileReference ReceiptPath, ILogger Logger, out DateTime Timestamp)
|
|
{
|
|
using (ScopedTimer TimestampTimer = new ScopedTimer("GetHeaderToolTimestamp", Logger))
|
|
{
|
|
// Try to read the receipt for UHT.
|
|
FileItem ReceiptFile = FileItem.GetItemByFileReference(ReceiptPath);
|
|
if (!ReceiptFile.Exists)
|
|
{
|
|
Timestamp = DateTime.MaxValue;
|
|
return false;
|
|
}
|
|
|
|
// Don't check timestamps for individual binaries if we're using the installed version of UHT. It will always be up to date.
|
|
if (!Unreal.IsFileInstalled(ReceiptFile.Location))
|
|
{
|
|
TargetReceipt? Receipt;
|
|
if (!TargetReceipt.TryRead(ReceiptPath, out Receipt))
|
|
{
|
|
Timestamp = DateTime.MaxValue;
|
|
return false;
|
|
}
|
|
|
|
// Make sure all the build products exist, and that the receipt is newer
|
|
foreach (BuildProduct BuildProduct in Receipt.BuildProducts)
|
|
{
|
|
FileItem BuildProductItem = FileItem.GetItemByFileReference(BuildProduct.Path);
|
|
if (!BuildProductItem.Exists || BuildProductItem.LastWriteTimeUtc > ReceiptFile.LastWriteTimeUtc)
|
|
{
|
|
Timestamp = DateTime.MaxValue;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the timestamp for all the binaries
|
|
Timestamp = ReceiptFile.LastWriteTimeUtc;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the timestamp of CoreUObject.gen.cpp file.
|
|
/// </summary>
|
|
/// <returns>Last write time of CoreUObject.gen.cpp or DateTime.MaxValue if it doesn't exist.</returns>
|
|
private static DateTime GetCoreGeneratedTimestampUtc(string ModuleName, string ModuleGeneratedCodeDirectory)
|
|
{
|
|
// In Installed Builds, we don't check the timestamps on engine headers. Default to a very old date.
|
|
if (Unreal.IsEngineInstalled())
|
|
{
|
|
return DateTime.MinValue;
|
|
}
|
|
|
|
// Otherwise look for CoreUObject.init.gen.cpp
|
|
FileInfo CoreGeneratedFileInfo = new FileInfo(Path.Combine(ModuleGeneratedCodeDirectory, ModuleName + ".init.gen.cpp"));
|
|
if (CoreGeneratedFileInfo.Exists)
|
|
{
|
|
return CoreGeneratedFileInfo.LastWriteTimeUtc;
|
|
}
|
|
|
|
// Doesn't exist, so use a 'newer that everything' date to force rebuild headers.
|
|
return DateTime.MaxValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the timestamp of the Version.h header file. Throws an exception if not found.
|
|
/// </summary>
|
|
/// <returns>Last write time of Version.h</returns>
|
|
private static DateTime GetEngineVersionHeaderTimestampUtc()
|
|
{
|
|
// In Installed Builds, we don't check the timestamps on engine headers. Default to a very old date.
|
|
if (Unreal.IsEngineInstalled())
|
|
{
|
|
return DateTime.MinValue;
|
|
}
|
|
|
|
string EngineVersionHeaderPath = Path.Combine(Unreal.EngineSourceDirectory.FullName, "Runtime", "Launch", "Resources", "Version.h");
|
|
FileInfo EngineVersionHeaderFileInfo = new(EngineVersionHeaderPath);
|
|
if (EngineVersionHeaderFileInfo.Exists)
|
|
{
|
|
return EngineVersionHeaderFileInfo.LastWriteTimeUtc;
|
|
}
|
|
|
|
// The Version.h header doesn't exist somehow (maybe it was moved).
|
|
throw new BuildException("Could not find engine version header, expected: {0}", EngineVersionHeaderPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks the class header files and determines if generated UObject code files are out of date in comparison.
|
|
/// </summary>
|
|
/// <param name="BuildConfiguration">Build configuration</param>
|
|
/// <param name="UObjectModules">Modules that we generate headers for</param>
|
|
/// <param name="HeaderToolTimestampUtc">Timestamp for UHT</param>
|
|
/// <param name="Logger">Logger for output</param>
|
|
/// <returns>True if the code files are out of date</returns>
|
|
private static bool AreGeneratedCodeFilesOutOfDate(BuildConfiguration BuildConfiguration, List<UHTModuleInfo> UObjectModules, DateTime HeaderToolTimestampUtc, ILogger Logger)
|
|
{
|
|
// Get CoreUObject.init.gen.cpp timestamp. If the source files are older than the CoreUObject generated code, we'll
|
|
// need to regenerate code for the module
|
|
DateTime? CoreGeneratedTimestampUtc = null;
|
|
{
|
|
// Find the CoreUObject module
|
|
foreach (UHTModuleInfo Module in UObjectModules)
|
|
{
|
|
if (Module.ModuleName.Equals("CoreUObject", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
CoreGeneratedTimestampUtc = GetCoreGeneratedTimestampUtc(Module.ModuleName, Path.GetDirectoryName(Module.GeneratedCPPFilenameBase)!);
|
|
break;
|
|
}
|
|
}
|
|
if (CoreGeneratedTimestampUtc == null)
|
|
{
|
|
throw new BuildException("Could not find CoreUObject in list of all UObjectModules");
|
|
}
|
|
}
|
|
|
|
// Also get the Version.h timestamp. If it is newer than the last UHT run, it could affect preprocessor conditionals
|
|
// that check for the engine version
|
|
DateTime EngineVersionHeaderTimestampUtc = GetEngineVersionHeaderTimestampUtc();
|
|
|
|
foreach (UHTModuleInfo Module in UObjectModules)
|
|
{
|
|
// If we're using a precompiled engine, skip checking timestamps for modules that are under the engine directory
|
|
if (Module.bIsReadOnly)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Make sure we have an existing folder for generated code. If not, then we definitely need to generate code!
|
|
string GeneratedCodeDirectory = Path.GetDirectoryName(Module.GeneratedCPPFilenameBase)!;
|
|
FileSystemInfo TestDirectory = (FileSystemInfo)new DirectoryInfo(GeneratedCodeDirectory);
|
|
if (!TestDirectory.Exists)
|
|
{
|
|
// Generated code directory is missing entirely!
|
|
Logger.LogDebug("UnrealHeaderTool needs to run because no generated code directory was found for module {ModuleName}", Module.ModuleName);
|
|
return true;
|
|
}
|
|
|
|
// Grab our special "Timestamp" file that we saved after the last set of headers were generated. This file
|
|
// contains the list of source files which contained UObjects, so that we can compare to see if any
|
|
// UObject source files were deleted (or no longer contain UObjects), which means we need to run UHT even
|
|
// if no other source files were outdated
|
|
string TimestampFile = Path.Combine(GeneratedCodeDirectory, @"Timestamp");
|
|
FileSystemInfo SavedTimestampFileInfo = (FileSystemInfo)new FileInfo(TimestampFile);
|
|
if (!SavedTimestampFileInfo.Exists)
|
|
{
|
|
// Timestamp file was missing (possibly deleted/cleaned), so headers are out of date
|
|
Logger.LogDebug("UnrealHeaderTool needs to run because UHT Timestamp file did not exist for module {ModuleName}", Module.ModuleName);
|
|
return true;
|
|
}
|
|
|
|
// Make sure the last UHT run completed after UnrealHeaderTool.exe was compiled last, and after the CoreUObject headers were touched last.
|
|
DateTime SavedTimestampUtc = SavedTimestampFileInfo.LastWriteTimeUtc;
|
|
if (HeaderToolTimestampUtc > SavedTimestampUtc)
|
|
{
|
|
// Generated code is older than UnrealHeaderTool.exe. Out of date!
|
|
Logger.LogDebug("UnrealHeaderTool needs to run because UnrealHeaderTool timestamp ({Time}) is later than timestamp for module {ModuleName} ({ModuleTime})", HeaderToolTimestampUtc.ToLocalTime(), Module.ModuleName, SavedTimestampUtc.ToLocalTime());
|
|
return true;
|
|
}
|
|
if (CoreGeneratedTimestampUtc > SavedTimestampUtc)
|
|
{
|
|
// Generated code is older than CoreUObject headers. Out of date!
|
|
Logger.LogDebug("UnrealHeaderTool needs to run because CoreUObject timestamp ({Time}) is newer than timestamp for module {ModuleName} ({ModuleTime})", CoreGeneratedTimestampUtc.Value.ToLocalTime(), Module.ModuleName, SavedTimestampUtc.ToLocalTime());
|
|
return true;
|
|
}
|
|
if (EngineVersionHeaderTimestampUtc > SavedTimestampUtc)
|
|
{
|
|
// Generated code is older than engine version header. Out of date!
|
|
Logger.LogDebug("UnrealHeaderTool needs to run because engine version header timestamp ({Time}) is newer than timestamp for module {ModuleName} ({ModuleTime})", EngineVersionHeaderTimestampUtc.ToLocalTime(), Module.ModuleName, SavedTimestampUtc.ToLocalTime());
|
|
return true;
|
|
}
|
|
|
|
// Has the .build.cs file changed since we last generated headers successfully?
|
|
FileInfo ModuleRulesFile = new FileInfo(Module.ModuleRulesFile.FullName);
|
|
if (!ModuleRulesFile.Exists || ModuleRulesFile.LastWriteTimeUtc > SavedTimestampUtc)
|
|
{
|
|
Logger.LogDebug("UnrealHeaderTool needs to run because SavedTimestamp is older than the rules file ({ModuleModuleRulesFile}) for module {ModuleModuleName}", Module.ModuleRulesFile, Module.ModuleName);
|
|
return true;
|
|
}
|
|
|
|
// Iterate over our UObjects headers and figure out if any of them have changed
|
|
List<FileItem> AllUObjectHeaders = new List<FileItem>();
|
|
AllUObjectHeaders.AddRange(Module.PublicUObjectClassesHeaders);
|
|
AllUObjectHeaders.AddRange(Module.PublicUObjectHeaders);
|
|
AllUObjectHeaders.AddRange(Module.InternalUObjectHeaders);
|
|
AllUObjectHeaders.AddRange(Module.PrivateUObjectHeaders);
|
|
|
|
// Load up the old timestamp file and check to see if anything has changed
|
|
{
|
|
string[] UObjectFilesFromPreviousRun = File.ReadAllLines(TimestampFile);
|
|
if (AllUObjectHeaders.Count != UObjectFilesFromPreviousRun.Length)
|
|
{
|
|
Logger.LogDebug("UnrealHeaderTool needs to run because there are a different number of UObject source files in module {ModuleModuleName}", Module.ModuleName);
|
|
return true;
|
|
}
|
|
|
|
// Iterate over our UObjects headers and figure out if any of them have changed
|
|
HashSet<string> ObjectHeadersSet = new HashSet<string>(AllUObjectHeaders.Select(x => x.AbsolutePath), FileReference.Comparer);
|
|
foreach (string FileName in UObjectFilesFromPreviousRun)
|
|
{
|
|
if (!ObjectHeadersSet.Contains(FileName))
|
|
{
|
|
Logger.LogDebug("UnrealHeaderTool needs to run because the set of UObject source files in module {ModuleModuleName} has changed ({FileName})", Module.ModuleName, FileName);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (FileItem HeaderFile in AllUObjectHeaders)
|
|
{
|
|
DateTime HeaderFileTimestampUtc = HeaderFile.LastWriteTimeUtc;
|
|
|
|
// Has the source header changed since we last generated headers successfully?
|
|
if (HeaderFileTimestampUtc > SavedTimestampUtc)
|
|
{
|
|
Logger.LogDebug("UnrealHeaderTool needs to run because SavedTimestamp is older than HeaderFileTimestamp ({HeaderFileAbsolutePath}) for module {ModuleModuleName}", HeaderFile.AbsolutePath, Module.ModuleName);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if any external dependencies for generated code is out of date
|
|
/// </summary>
|
|
/// <param name="ExternalDependenciesFile">Path to the external dependencies file</param>
|
|
/// <returns>True if any external dependencies are out of date</returns>
|
|
private static bool AreExternalDependenciesOutOfDate(FileReference ExternalDependenciesFile)
|
|
{
|
|
if (!FileReference.Exists(ExternalDependenciesFile))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
DateTime LastWriteTime = File.GetLastWriteTimeUtc(ExternalDependenciesFile.FullName);
|
|
|
|
string[] Lines = File.ReadAllLines(ExternalDependenciesFile.FullName);
|
|
foreach (string Line in Lines)
|
|
{
|
|
string ExternalDependencyFile = Line.Trim();
|
|
if (ExternalDependencyFile.Length > 0)
|
|
{
|
|
if (!File.Exists(ExternalDependencyFile) || File.GetLastWriteTimeUtc(ExternalDependencyFile) > LastWriteTime)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the recorded timestamps of all the passed in UObject modules
|
|
/// </summary>
|
|
private static void UpdateTimestamps(List<UHTModuleInfo> UObjectModules)
|
|
{
|
|
Parallel.ForEach(UObjectModules, async Module =>
|
|
{
|
|
if (!Module.bIsReadOnly)
|
|
{
|
|
string GeneratedCodeDirectory = Path.GetDirectoryName(Module.GeneratedCPPFilenameBase)!;
|
|
DirectoryInfo GeneratedCodeDirectoryInfo = new DirectoryInfo(GeneratedCodeDirectory);
|
|
|
|
try
|
|
{
|
|
if (GeneratedCodeDirectoryInfo.Exists)
|
|
{
|
|
FileReference TimestampFile = FileReference.Combine(new DirectoryReference(GeneratedCodeDirectoryInfo.FullName), "Timestamp");
|
|
|
|
// Save all of the UObject files to a timestamp file. We'll load these on the next run to see if any new
|
|
// files with UObject classes were deleted, so that we'll know to run UHT even if the timestamps of all
|
|
// of the other source files were unchanged
|
|
{
|
|
List<string> AllUObjectFiles = new List<string>();
|
|
AllUObjectFiles.AddRange(Module.PublicUObjectClassesHeaders.ConvertAll(Item => Item.AbsolutePath));
|
|
AllUObjectFiles.AddRange(Module.PublicUObjectHeaders.ConvertAll(Item => Item.AbsolutePath));
|
|
AllUObjectFiles.AddRange(Module.InternalUObjectHeaders.ConvertAll(Item => Item.AbsolutePath));
|
|
AllUObjectFiles.AddRange(Module.PrivateUObjectHeaders.ConvertAll(Item => Item.AbsolutePath));
|
|
await FileReference.WriteAllLinesAsync(TimestampFile, AllUObjectFiles);
|
|
}
|
|
|
|
// Because new .cpp and .h files may have been generated by UHT, invalidate the DirectoryLookupCache
|
|
DirectoryLookupCache.InvalidateCachedDirectory(new DirectoryReference(GeneratedCodeDirectoryInfo.FullName));
|
|
}
|
|
}
|
|
catch (Exception Exception)
|
|
{
|
|
throw new BuildException(Exception, "Couldn't write Timestamp file: " + Exception.Message);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Run an external native executable (and capture the output), given the executable path and the commandline.
|
|
/// </summary>
|
|
public static int RunExternalNativeExecutable(FileReference ExePath, string Commandline, ILogger Logger)
|
|
{
|
|
Logger.LogDebug("RunExternalExecutable {ExePathFullName} {Commandline}", ExePath.FullName, Commandline);
|
|
using (LogEventParser Parser = new LogEventParser(Logger))
|
|
{
|
|
Parser.AddMatchersFromAssembly(Assembly.GetExecutingAssembly());
|
|
using (Process GameProcess = new Process())
|
|
{
|
|
GameProcess.StartInfo.FileName = ExePath.FullName;
|
|
GameProcess.StartInfo.Arguments = Commandline;
|
|
GameProcess.StartInfo.UseShellExecute = false;
|
|
GameProcess.StartInfo.RedirectStandardOutput = true;
|
|
GameProcess.OutputDataReceived += (s, e) => PrintProcessOutputAsync(s, e, Parser);
|
|
GameProcess.Start();
|
|
GameProcess.BeginOutputReadLine();
|
|
GameProcess.WaitForExit();
|
|
|
|
return GameProcess.ExitCode;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple function to pipe output asynchronously
|
|
/// </summary>
|
|
private static void PrintProcessOutputAsync(object Sender, DataReceivedEventArgs Event, LogEventParser Parser)
|
|
{
|
|
// DataReceivedEventHandler is fired with a null string when the output stream is closed. We don't want to
|
|
// print anything for that event.
|
|
if (!String.IsNullOrEmpty(Event.Data))
|
|
{
|
|
Parser.WriteLine(Event.Data);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds and runs the header tool and touches the header directories.
|
|
/// Performs any early outs if headers need no changes, given the UObject modules, tool path, game name, and configuration
|
|
/// </summary>
|
|
public static async Task ExecuteHeaderToolIfNecessaryAsync(BuildConfiguration BuildConfiguration, FileReference? ProjectFile, TargetMakefile Makefile, string TargetName, ISourceFileWorkingSet WorkingSet, ILogger Logger)
|
|
{
|
|
|
|
// No need to run UHT on itself
|
|
if (TargetName.Equals("UnrealHeaderTool", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
Logger.LogInformation("DEPRECATED: C++ UHT is being built by request. C++ UHT has been deprecated and will be removed in 5.2");
|
|
return;
|
|
}
|
|
|
|
await ExecuteHeaderToolIfNecessaryInternalAsync(BuildConfiguration, ProjectFile, Makefile, TargetName, WorkingSet, Logger);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the file reference for the UHT manifest file
|
|
/// </summary>
|
|
/// <param name="Makefile">Input makefile</param>
|
|
/// <param name="TargetName">Name of the target</param>
|
|
/// <returns>Manifest file name</returns>
|
|
public static FileReference GetUHTModuleInfoFileName(TargetMakefile Makefile, string TargetName)
|
|
{
|
|
return FileReference.Combine(Makefile.ProjectIntermediateDirectoryNoArch, TargetName + ".uhtmanifest");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the file reference for the UHT deps file
|
|
/// </summary>
|
|
/// <param name="ModuleInfoFileName">Manifest info file name</param>
|
|
/// <returns>UHT dependency file name</returns>
|
|
public static FileReference GetUHTDepsFileName(FileReference ModuleInfoFileName)
|
|
{
|
|
return ModuleInfoFileName.ChangeExtension(".deps");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert the makefile to a UHTManifest object
|
|
/// </summary>
|
|
/// <param name="Makefile">Input makefile</param>
|
|
/// <param name="TargetName">Name of the target</param>
|
|
/// <param name="DepsFileName">Name of the dependencies file.</param>
|
|
/// <returns>Output UHT manifest</returns>
|
|
public static UHTManifest CreateUHTManifest(TargetMakefile Makefile, string TargetName, FileReference DepsFileName)
|
|
{
|
|
List<UHTManifest.Module> Modules = new List<UHTManifest.Module>();
|
|
foreach (UHTModuleInfo UObjectModule in Makefile.UObjectModules)
|
|
{
|
|
Modules.Add(
|
|
new UHTManifest.Module
|
|
{
|
|
Name = UObjectModule.ModuleName,
|
|
ModuleType = (UHTModuleType)Enum.Parse(typeof(UHTModuleType), UObjectModule.ModuleType),
|
|
OverrideModuleType = (EPackageOverrideType)Enum.Parse(typeof(EPackageOverrideType), UObjectModule.OverrideModuleType),
|
|
BaseDirectory = UObjectModule.ModuleDirectories[0].FullName,
|
|
IncludePaths = UObjectModule.ModuleIncludePaths.Select(IncludePath => IncludePath.FullName).ToList(),
|
|
OutputDirectory = Path.GetDirectoryName(UObjectModule.GeneratedCPPFilenameBase)!,
|
|
ClassesHeaders = UObjectModule.PublicUObjectClassesHeaders.Select((Header) => Header.AbsolutePath).ToList(),
|
|
PublicHeaders = UObjectModule.PublicUObjectHeaders.Select((Header) => Header.AbsolutePath).ToList(),
|
|
InternalHeaders = UObjectModule.InternalUObjectHeaders.Select((Header) => Header.AbsolutePath).ToList(),
|
|
PrivateHeaders = UObjectModule.PrivateUObjectHeaders.Select((Header) => Header.AbsolutePath).ToList(),
|
|
PublicDefines = UObjectModule.PublicDefines,
|
|
GeneratedCPPFilenameBase = UObjectModule.GeneratedCPPFilenameBase,
|
|
SaveExportedHeaders = !UObjectModule.bIsReadOnly,
|
|
GeneratedCodeVersion = UObjectModule.GeneratedCodeVersion,
|
|
VersePath = UObjectModule.VersePath,
|
|
VerseScope = (UHTVerseScope)Enum.Parse(typeof(UHTVerseScope), UObjectModule.VerseScope.ToString()),
|
|
HasVerse = UObjectModule.HasVerse,
|
|
VerseMountPoint = UObjectModule.VerseMountPoint,
|
|
AlwaysExportStructs = UObjectModule.AlwaysExportStructs,
|
|
AlwaysExportEnums = UObjectModule.AlwaysExportEnums,
|
|
}); ;
|
|
}
|
|
|
|
List<string> UhtPlugins = new List<string>();
|
|
if (Makefile.EnabledUhtPlugins != null)
|
|
{
|
|
foreach (FileReference UhtPlugin in Makefile.EnabledUhtPlugins)
|
|
{
|
|
UhtPlugins.Add(UhtPlugin.FullName);
|
|
}
|
|
}
|
|
|
|
UHTManifest Manifest = new UHTManifest
|
|
{
|
|
TargetName = TargetName,
|
|
IsGameTarget = Makefile.TargetType != TargetType.Program,
|
|
RootLocalPath = Unreal.RootDirectory.FullName,
|
|
ExternalDependenciesFile = DepsFileName.FullName,
|
|
Modules = Modules,
|
|
UhtPlugins = UhtPlugins,
|
|
};
|
|
|
|
return Manifest;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write the manifest to disk
|
|
/// </summary>
|
|
/// <param name="Makefile">Input makefile</param>
|
|
/// <param name="TargetName">Name of the target</param>
|
|
/// <param name="ModuleInfoFileName">Destination file name. If not supplied, it will be generated.</param>
|
|
/// <param name="DepsFileName">Name of the dependencies file. If not supplied, it will be generated.</param>
|
|
public static void WriteUHTManifest(TargetMakefile Makefile, string TargetName, FileReference ModuleInfoFileName, FileReference DepsFileName)
|
|
{
|
|
// @todo ubtmake: Optimization: Ideally we could avoid having to generate this data in the case where UHT doesn't even need to run! Can't we use the existing copy? (see below use of Manifest)
|
|
UHTManifest Manifest = CreateUHTManifest(Makefile, TargetName, DepsFileName);
|
|
Directory.CreateDirectory(ModuleInfoFileName.Directory.FullName);
|
|
System.IO.File.WriteAllText(ModuleInfoFileName.FullName, JsonSerializer.Serialize(Manifest, new JsonSerializerOptions { WriteIndented = true }));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the latest of two date/times
|
|
/// </summary>
|
|
/// <param name="Lhs">First date/time to compare</param>
|
|
/// <param name="Rhs">Second date/time to compare</param>
|
|
/// <returns>Latest of the two dates</returns>
|
|
private static DateTime LatestDateTime(DateTime Lhs, DateTime Rhs)
|
|
{
|
|
return Lhs > Rhs ? Lhs : Rhs;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds and runs the header tool and touches the header directories.
|
|
/// Performs any early outs if headers need no changes, given the UObject modules, tool path, game name, and configuration
|
|
/// </summary>
|
|
private static async Task ExecuteHeaderToolIfNecessaryInternalAsync(BuildConfiguration BuildConfiguration, FileReference? ProjectFile,
|
|
TargetMakefile Makefile, string TargetName, ISourceFileWorkingSet WorkingSet, ILogger Logger)
|
|
{
|
|
if (ProgressWriter.bWriteMarkup)
|
|
{
|
|
Logger.LogInformation("@progress push 5%");
|
|
}
|
|
using (ProgressWriter Progress = new ProgressWriter("Generating code...", false, Logger))
|
|
{
|
|
string RootLocalPath = Unreal.RootDirectory.FullName;
|
|
|
|
// Get the path to the assemblies we care about
|
|
string UhtAssemblyPath = typeof(UhtSession).Assembly.Location;
|
|
|
|
// Get a composite date/time for cirtical assemblies being used
|
|
DateTime CompositeTimestamp = LatestDateTime(new FileInfo(Assembly.GetExecutingAssembly().Location).LastWriteTimeUtc, new FileInfo(UhtAssemblyPath).LastWriteTimeUtc);
|
|
if (Makefile.EnabledUhtPlugins != null)
|
|
{
|
|
foreach (FileReference Plugin in Makefile.EnabledUhtPlugins)
|
|
{
|
|
CompositeTimestamp = LatestDateTime(CompositeTimestamp, new FileInfo(Plugin.FullName).LastWriteTimeUtc);
|
|
}
|
|
}
|
|
|
|
// ensure the headers are up to date
|
|
bool bUHTNeedsToRun = false;
|
|
if (BuildConfiguration.bForceHeaderGeneration)
|
|
{
|
|
bUHTNeedsToRun = true;
|
|
}
|
|
else if (AreGeneratedCodeFilesOutOfDate(BuildConfiguration, Makefile.UObjectModules, CompositeTimestamp, Logger))
|
|
{
|
|
bUHTNeedsToRun = true;
|
|
}
|
|
|
|
// Check we're not using a different version of UHT
|
|
FileReference ModuleInfoFileName = GetUHTModuleInfoFileName(Makefile, TargetName);
|
|
FileReference ToolInfoFile = ModuleInfoFileName.ChangeExtension(".uhtpath");
|
|
if (!bUHTNeedsToRun)
|
|
{
|
|
if (!FileReference.Exists(ToolInfoFile))
|
|
{
|
|
bUHTNeedsToRun = true;
|
|
}
|
|
else if (FileReference.ReadAllText(ToolInfoFile) != UhtAssemblyPath)
|
|
{
|
|
bUHTNeedsToRun = true;
|
|
}
|
|
}
|
|
|
|
// Get the file containing dependencies for the generated code
|
|
FileReference ExternalDependenciesFile = GetUHTDepsFileName(ModuleInfoFileName);
|
|
if (AreExternalDependenciesOutOfDate(ExternalDependenciesFile))
|
|
{
|
|
bUHTNeedsToRun = true;
|
|
}
|
|
|
|
if (bUHTNeedsToRun)
|
|
{
|
|
Progress.Write(1, 3);
|
|
|
|
WriteUHTManifest(Makefile, TargetName, ModuleInfoFileName, ExternalDependenciesFile);
|
|
|
|
string ActualTargetName = String.IsNullOrEmpty(TargetName) ? "UE5" : TargetName;
|
|
Logger.LogInformation("Parsing headers for {ActualTargetName}", ActualTargetName);
|
|
|
|
// Generate the command line
|
|
List<string> CmdArgs = new List<string>();
|
|
CmdArgs.Add((ProjectFile != null) ? ProjectFile.FullName : TargetName);
|
|
CmdArgs.Add(ModuleInfoFileName.FullName);
|
|
CmdArgs.Add("-WarningsAsErrors");
|
|
|
|
if (Unreal.IsEngineInstalled())
|
|
{
|
|
CmdArgs.Add("-installed");
|
|
}
|
|
|
|
if (BuildConfiguration.bFailIfGeneratedCodeChanges)
|
|
{
|
|
CmdArgs.Add("-FailIfGeneratedCodeChanges");
|
|
}
|
|
|
|
if (Makefile.UHTAdditionalArguments != null)
|
|
{
|
|
foreach (string Arg in Makefile.UHTAdditionalArguments)
|
|
{
|
|
if (Arg[0] == '"' && Arg[^1] == '"')
|
|
{
|
|
CmdArgs.Add(Arg.Substring(1, Arg.Length - 2));
|
|
}
|
|
else
|
|
{
|
|
CmdArgs.Add(Arg);
|
|
}
|
|
}
|
|
}
|
|
CommandLineArguments Arguments = new CommandLineArguments(CmdArgs.ToArray());
|
|
|
|
StringBuilder CmdLine = new StringBuilder();
|
|
foreach (string Arg in CmdArgs)
|
|
{
|
|
CmdLine.AppendArgument(Arg);
|
|
}
|
|
|
|
Logger.LogInformation(" Running Internal UnrealHeaderTool {CmdLine}", CmdLine);
|
|
|
|
// Run UHT
|
|
Stopwatch s = new Stopwatch();
|
|
s.Start();
|
|
IScope Timer = GlobalTracer.Instance.BuildSpan("Executing UnrealHeaderTool").StartActive();
|
|
UnrealHeaderToolMode UHTTool = new UnrealHeaderToolMode();
|
|
CompilationResult UHTResult = (CompilationResult)await UHTTool.ExecuteAsync(Arguments, Logger);
|
|
Timer.Span.Finish();
|
|
s.Stop();
|
|
|
|
if (UHTResult != CompilationResult.Succeeded)
|
|
{
|
|
// On Linux and Mac, the shell will return 128+signal number exit codes if UHT gets a signal (e.g. crashes or is interrupted)
|
|
if ((BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Linux ||
|
|
BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac) &&
|
|
(int)(UHTResult) >= 128
|
|
)
|
|
{
|
|
// SIGINT is 2, so 128 + SIGINT is 130
|
|
UHTResult = ((int)(UHTResult) == 130) ? CompilationResult.Canceled : CompilationResult.CrashOrAssert;
|
|
}
|
|
|
|
if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64 &&
|
|
(int)(UHTResult) < 0)
|
|
{
|
|
Logger.LogError("UnrealHeaderTool failed with exit code 0x{Result:X} - check that Unreal Engine prerequisites are installed.", (int)UHTResult);
|
|
}
|
|
|
|
throw new CompilationResultException(UHTResult);
|
|
}
|
|
|
|
Logger.LogInformation("Reflection code generated for {TargetName} in {Time} seconds", ActualTargetName, s.Elapsed.TotalSeconds);
|
|
|
|
// Update the tool info file
|
|
DirectoryReference.CreateDirectory(ToolInfoFile.Directory);
|
|
FileReference.WriteAllText(ToolInfoFile, UhtAssemblyPath);
|
|
|
|
// Now that UHT has successfully finished generating code, we need to update all cached FileItems in case their last write time has changed.
|
|
// Otherwise UBT might not detect changes UHT made.
|
|
using (GlobalTracer.Instance.BuildSpan("ExternalExecution.ResetCachedHeaderInfo()").StartActive())
|
|
{
|
|
ResetCachedHeaderInfo(Makefile.UObjectModules);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogDebug("Generated code is up to date.");
|
|
}
|
|
|
|
Progress.Write(2, 3);
|
|
|
|
using (GlobalTracer.Instance.BuildSpan("ExternalExecution.UpdateTimestamps()").StartActive())
|
|
{
|
|
UpdateTimestamps(Makefile.UObjectModules);
|
|
}
|
|
|
|
Progress.Write(3, 3);
|
|
}
|
|
if (ProgressWriter.bWriteMarkup)
|
|
{
|
|
Logger.LogInformation("@progress pop");
|
|
}
|
|
}
|
|
|
|
static void ResetCachedHeaderInfo(List<UHTModuleInfo> UObjectModules)
|
|
{
|
|
foreach (UHTModuleInfo ModuleInfo in UObjectModules)
|
|
{
|
|
ModuleInfo.GeneratedCodeDirectory.ResetCachedInfo();
|
|
}
|
|
}
|
|
}
|
|
}
|