// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using EpicGames.UHT.Tables;
using EpicGames.UHT.Tokenizer;
using EpicGames.UHT.Types;
using EpicGames.UHT.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using UnrealBuildBase;
using Microsoft.Extensions.Logging;
namespace UnrealBuildTool.Modes
{
///
/// Implement the UHT configuration interface. Due to the configuration system being fairly embedded into
/// UBT, the implementation must be part of UBT.
///
public class UhtConfigImpl : IUhtConfig
{
private readonly ConfigHierarchy _ini;
///
/// Types that have been renamed, treat the old deprecated name as the new name for code generation
///
private readonly IReadOnlyDictionary _typeRedirectMap;
///
/// Metadata that have been renamed, treat the old deprecated name as the new name for code generation
///
private readonly IReadOnlyDictionary _metaDataRedirectMap;
///
/// Supported units in the game
///
private readonly ReadOnlyHashSet _units;
///
/// Special parsed struct names that do not require a prefix
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "")]
private readonly ReadOnlyHashSet _structsWithNoPrefix;
///
/// Special parsed struct names that have a 'T' prefix
///
private readonly ReadOnlyHashSet _structsWithTPrefix;
///
/// Mapping from 'human-readable' macro substring to # of parameters for delegate declarations
/// Index 0 is 1 parameter, Index 1 is 2, etc...
///
private readonly IReadOnlyList _delegateParameterCountStrings;
///
/// Default version of generated code. Defaults to oldest possible, unless specified otherwise in config.
///
private readonly EGeneratedCodeVersion _defaultGeneratedCodeVersion = EGeneratedCodeVersion.V1;
///
/// Internal version of pointer warning for native pointers in the engine
///
private readonly UhtPointerMemberBehavior _engineNativePointerMemberBehavior = UhtPointerMemberBehavior.AllowSilently;
///
/// Internal version of pointer warning for object pointers in the engine
///
private readonly UhtPointerMemberBehavior _engineObjectPtrMemberBehavior = UhtPointerMemberBehavior.AllowSilently;
///
/// Internal version of pointer warning for native pointers outside the engine
///
private readonly UhtPointerMemberBehavior _nonEngineNativePointerMemberBehavior = UhtPointerMemberBehavior.AllowSilently;
///
/// Internal version of pointer warning for object pointers outside the engine
///
private readonly UhtPointerMemberBehavior _nonEngineObjectPtrMemberBehavior = UhtPointerMemberBehavior.AllowSilently;
///
/// If true, deprecation warnings should be shown
///
private readonly bool _showDeprecations = true;
///
/// If true, UObject properties are enabled in RigVM
///
private readonly bool _areRigVMUObjectProeprtiesEnabled = false;
///
/// If true, UInterface properties are enabled in RigVM
///
private readonly bool _areRigVMUInterfaceProeprtiesEnabled = false;
#region IUhtConfig Implementation
///
public EGeneratedCodeVersion DefaultGeneratedCodeVersion => this._defaultGeneratedCodeVersion;
///
public UhtPointerMemberBehavior EngineNativePointerMemberBehavior => this._engineNativePointerMemberBehavior;
///
public UhtPointerMemberBehavior EngineObjectPtrMemberBehavior => this._engineObjectPtrMemberBehavior;
///
public UhtPointerMemberBehavior NonEngineNativePointerMemberBehavior => this._nonEngineNativePointerMemberBehavior;
///
public UhtPointerMemberBehavior NonEngineObjectPtrMemberBehavior => this._nonEngineObjectPtrMemberBehavior;
///
/// If true, UObject properties are enabled in RigVM
///
public bool AreRigVMUObjectProeprtiesEnabled => this._areRigVMUObjectProeprtiesEnabled;
///
/// If true, UInterface properties are enabled in RigVM
///
public bool AreRigVMUInterfaceProeprtiesEnabled => this._areRigVMUInterfaceProeprtiesEnabled;
///
/// If true, deprecation warnings should be shown
///
public bool ShowDeprecations => this._showDeprecations;
///
public void RedirectTypeIdentifier(ref UhtToken Token)
{
if (!Token.IsIdentifier())
{
throw new Exception("Attempt to redirect type identifier when the token isn't an identifier.");
}
if (this._typeRedirectMap.TryGetValue(Token.Value, out StringView Redirect))
{
Token.Value = Redirect;
}
}
///
public bool RedirectMetaDataKey(string Key, out string NewKey)
{
if (this._metaDataRedirectMap.TryGetValue(Key, out string? Redirect))
{
NewKey = Redirect;
return Key != NewKey;
}
else
{
NewKey = Key;
return false;
}
}
///
public bool IsValidUnits(StringView Units)
{
return this._units.Contains(Units);
}
///
public bool IsStructWithTPrefix(StringView Name)
{
return this._structsWithTPrefix.Contains(Name);
}
///
public int FindDelegateParameterCount(StringView DelegateMacro)
{
for (int Index = 0, Count = this._delegateParameterCountStrings.Count; Index < Count; ++Index)
{
if (DelegateMacro.Span.Contains(this._delegateParameterCountStrings[Index].Span, StringComparison.Ordinal))
{
return Index;
}
}
return -1;
}
///
public StringView GetDelegateParameterCountString(int Index)
{
return Index >= 0 ? this._delegateParameterCountStrings[Index] : "";
}
///
public bool IsExporterEnabled(string Name)
{
this._ini.GetBool("UnrealHeaderTool", Name, out bool Value);
return Value;
}
#endregion
///
/// Read the UHT configuration
///
/// Extra command line arguments
public UhtConfigImpl(CommandLineArguments Args)
{
DirectoryReference ConfigDirectory = DirectoryReference.Combine(Unreal.EngineDirectory, "Programs", "UnrealHeaderTool");
this._ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, ConfigDirectory, BuildHostPlatform.Current.Platform, "", Args.GetRawArray());
this._typeRedirectMap = GetRedirectsStringView("UnrealHeaderTool", "TypeRedirects", "OldType", "NewType");
this._metaDataRedirectMap = GetRedirectsString("CoreUObject.Metadata", "MetadataRedirects", "OldKey", "NewKey");
this._structsWithNoPrefix = GetHashSet("UnrealHeaderTool", "StructsWithNoPrefix", StringViewComparer.Ordinal);
this._structsWithTPrefix = GetHashSet("UnrealHeaderTool", "StructsWithTPrefix", StringViewComparer.Ordinal);
this._units = GetHashSet("UnrealHeaderTool", "Units", StringViewComparer.OrdinalIgnoreCase);
this._delegateParameterCountStrings = GetList("UnrealHeaderTool", "DelegateParameterCountStrings");
this._defaultGeneratedCodeVersion = GetGeneratedCodeVersion("UnrealHeaderTool", "DefaultGeneratedCodeVersion", EGeneratedCodeVersion.V1);
this._engineNativePointerMemberBehavior = GetPointerMemberBehavior("UnrealHeaderTool", "EngineNativePointerMemberBehavior", UhtPointerMemberBehavior.AllowSilently);
this._engineObjectPtrMemberBehavior = GetPointerMemberBehavior("UnrealHeaderTool", "EngineObjectPtrMemberBehavior", UhtPointerMemberBehavior.AllowSilently);
this._nonEngineNativePointerMemberBehavior = GetPointerMemberBehavior("UnrealHeaderTool", "NonEngineNativePointerMemberBehavior", UhtPointerMemberBehavior.AllowSilently);
this._nonEngineObjectPtrMemberBehavior = GetPointerMemberBehavior("UnrealHeaderTool", "NonEngineObjectPtrMemberBehavior", UhtPointerMemberBehavior.AllowSilently);
this._areRigVMUObjectProeprtiesEnabled = GetBoolean("UnrealHeaderTool", "AreRigVMUObjectProeprtiesEnabled", false);
this._areRigVMUInterfaceProeprtiesEnabled = GetBoolean("UnrealHeaderTool", "AreRigVMUInterfaceProeprtiesEnabled", false);
this._showDeprecations = GetBoolean("UnrealHeaderTool", "ShowDeprecations", true);
}
private bool GetBoolean(string SectionName, string KeyName, bool bDefault)
{
if (this._ini.TryGetValue(SectionName, KeyName, out bool value))
{
return value;
}
return bDefault;
}
private UhtPointerMemberBehavior GetPointerMemberBehavior(string SectionName, string KeyName, UhtPointerMemberBehavior Default)
{
if (this._ini.TryGetValue(SectionName, KeyName, out string? BehaviorStr))
{
if (!Enum.TryParse(BehaviorStr, out UhtPointerMemberBehavior Value))
{
throw new Exception(string.Format("Unrecognized native pointer member behavior '{0}'", BehaviorStr));
}
return Value;
}
return Default;
}
private EGeneratedCodeVersion GetGeneratedCodeVersion(string SectionName, string KeyName, EGeneratedCodeVersion Default)
{
if (this._ini.TryGetValue(SectionName, KeyName, out string? BehaviorStr))
{
if (!Enum.TryParse(BehaviorStr, out EGeneratedCodeVersion Value))
{
throw new Exception(string.Format("Unrecognized generated code version '{0}'", BehaviorStr));
}
return Value;
}
return Default;
}
private IReadOnlyDictionary GetRedirectsStringView(string Section, string Key, string OldKeyName, string NewKeyName)
{
Dictionary Redirects = new();
if (this._ini.TryGetValues(Section, Key, out IReadOnlyList? StringList))
{
foreach (string Line in StringList)
{
if (ConfigHierarchy.TryParse(Line, out Dictionary? Properties))
{
if (!Properties.TryGetValue(OldKeyName, out string? OldKey))
{
throw new Exception(string.Format("Unable to get the {0} from the {1} value", OldKeyName, Key));
}
if (!Properties.TryGetValue(NewKeyName, out string? NewKey))
{
throw new Exception(string.Format("Unable to get the {0} from the {1} value", NewKeyName, Key));
}
Redirects.Add(OldKey, NewKey);
}
}
}
return Redirects;
}
private IReadOnlyDictionary GetRedirectsString(string Section, string Key, string OldKeyName, string NewKeyName)
{
Dictionary Redirects = new();
if (this._ini.TryGetValues(Section, Key, out IReadOnlyList? StringList))
{
foreach (string Line in StringList)
{
if (ConfigHierarchy.TryParse(Line, out Dictionary? Properties))
{
if (!Properties.TryGetValue(OldKeyName, out string? OldKey))
{
throw new Exception(string.Format("Unable to get the {0} from the {1} value", OldKeyName, Key));
}
if (!Properties.TryGetValue(NewKeyName, out string? NewKey))
{
throw new Exception(string.Format("Unable to get the {0} from the {1} value", NewKeyName, Key));
}
Redirects.Add(OldKey, NewKey);
}
}
}
return Redirects;
}
private IReadOnlyList GetList(string Section, string Key)
{
List List = new();
if (this._ini.TryGetValues(Section, Key, out IReadOnlyList? StringList))
{
foreach (string Value in StringList)
{
List.Add(new StringView(Value));
}
}
return List;
}
private ReadOnlyHashSet GetHashSet(string Section, string Key, StringViewComparer Comparer)
{
HashSet Set = new(Comparer);
if (this._ini.TryGetValues(Section, Key, out IReadOnlyList? StringList))
{
foreach (string Value in StringList)
{
Set.Add(new StringView(Value));
}
}
return Set;
}
}
///
/// Global options for UBT (any modes)
///
class UhtGlobalOptions
{
///
/// User asked for help
///
[CommandLine(Prefix = "-Help", Description = "Display this help.")]
[CommandLine(Prefix = "-h")]
[CommandLine(Prefix = "--help")]
public bool bGetHelp = false;
///
/// The amount of detail to write to the log
///
[CommandLine(Prefix = "-Verbose", Value = "Verbose", Description = "Increase output verbosity")]
[CommandLine(Prefix = "-VeryVerbose", Value = "VeryVerbose", Description = "Increase output verbosity more")]
public LogEventType LogOutputLevel = LogEventType.Log;
///
/// Specifies the path to a log file to write. Note that the default mode (eg. building, generating project files) will create a log file by default if this not specified.
///
[CommandLine(Prefix = "-Log", Description = "Specify a log file location instead of the default Engine/Programs/UnrealHeaderTool/Saved/Logs/UnrealHeaderTool.log")]
public FileReference? LogFileName = null;
///
/// Whether to include timestamps in the log
///
[CommandLine(Prefix = "-Timestamps", Description = "Include timestamps in the log")]
public bool bLogTimestamps = false;
///
/// Whether to format messages in MsBuild format
///
[CommandLine(Prefix = "-FromMsBuild", Description = "Format messages for msbuild")]
public bool bLogFromMsBuild = false;
///
/// Disables all logging including the default log location
///
[CommandLine(Prefix = "-NoLog", Description = "Disable log file creation including the default log file")]
public bool bNoLog = false;
[CommandLine(Prefix = "-Test", Description = "Run testing scripts")]
public bool bTest = false;
[CommandLine("-WarningsAsErrors", Description = "Treat warnings as errors")]
public bool bWarningsAsErrors = false;
[CommandLine("-NoGoWide", Description = "Disable concurrent parsing and code generation")]
public bool bNoGoWide = false;
[CommandLine("-WriteRef", Description = "Write all the output to a reference directory")]
public bool bWriteRef = false;
[CommandLine("-VerifyRef", Description = "Write all the output to a verification directory and compare to the reference output")]
public bool bVerifyRef = false;
[CommandLine("-FailIfGeneratedCodeChanges", Description = "Consider any changes to output files as being an error")]
public bool bFailIfGeneratedCodeChanges = false;
[CommandLine("-NoOutput", Description = "Do not save any output files other than reference output")]
public bool bNoOutput = false;
[CommandLine("-IncludeDebugOutput", Description = "Include extra content in generated output to assist with debugging")]
public bool bIncludeDebugOutput = false;
[CommandLine("-NoDefaultExporters", Description = "Disable all default exporters. Useful for when a specific exporter is to be run")]
public bool bNoDefaultExporters = false;
///
/// Initialize the options with the given command line arguments
///
///
public UhtGlobalOptions(CommandLineArguments Arguments)
{
Arguments.ApplyTo(this);
}
}
///
/// File manager for the test harness
///
public class UhtTestFileManager : IUhtFileManager
{
///
/// Collection of test fragments that can be read
///
public Dictionary SourceFragments = new();
///
/// All output segments generated by code gen
///
public SortedDictionary Outputs = new();
private readonly IUhtFileManager InnerManager;
private readonly string? RootDirectory;
///
/// Construct a new instance of the test file manager
///
/// Root directory of the UE
public UhtTestFileManager(string RootDirectory)
{
this.RootDirectory = RootDirectory;
this.InnerManager = new UhtStdFileManager();
}
///
public string GetFullFilePath(string FilePath)
{
if (this.RootDirectory == null)
{
return FilePath;
}
else
{
return Path.Combine(this.RootDirectory, FilePath);
}
}
///
public bool ReadSource(string FilePath, out UhtSourceFragment Fragment)
{
if (this.SourceFragments.TryGetValue(FilePath, out Fragment))
{
return true;
}
return InnerManager.ReadSource(GetFullFilePath(FilePath), out Fragment);
}
///
public UhtBuffer? ReadOutput(string FilePath)
{
return null;
}
///
public bool WriteOutput(string FilePath, ReadOnlySpan Contents)
{
lock (this.Outputs)
{
this.Outputs.Add(FilePath, Contents.ToString());
}
return true;
}
///
public bool RenameOutput(string OldFilePath, string NewFilePath)
{
lock (this.Outputs)
{
if (this.Outputs.TryGetValue(OldFilePath, out string? Content))
{
this.Outputs.Remove(OldFilePath);
this.Outputs.Add(NewFilePath, Content);
}
}
return true;
}
///
/// Add a source file fragment to the session. When requests are made to read sources, the
/// fragment list will be searched first.
///
/// Source file
/// The relative path to add
/// Starting line number
/// The data associated with the path
public void AddSourceFragment(UhtSourceFile SourceFile, string FilePath, int LineNumber, StringView Data)
{
this.SourceFragments.Add(FilePath, new UhtSourceFragment { SourceFile = SourceFile, FilePath = FilePath, LineNumber = LineNumber, Data = Data });
}
}
///
/// Testing harness to run the test scripts
///
class UhtTestHarness
{
private enum ScriptFragmentType
{
Unknown,
Manifest,
Header,
Console,
Output,
}
private struct ScriptFragment
{
public ScriptFragmentType Type;
public string Name;
public int LineNumber;
public StringView Header;
public StringView Body;
public bool External;
}
private static bool RunScriptTest(UhtTables Tables, IUhtConfig Config, UhtGlobalOptions Options, string TestDirectory, string TestOutputDirectory, string Script, ILogger Logger)
{
string InPath = Path.Combine(TestDirectory, Script);
string OutPath = Path.Combine(TestOutputDirectory, Script);
UhtTestFileManager TestFileManager = new(TestDirectory);
UhtSession Session = new()
{
Tables = Tables,
Config = Config,
FileManager = TestFileManager,
RootDirectory = TestDirectory,
WarningsAsErrors = Options.bWarningsAsErrors,
RelativePathInLog = true,
GoWide = !Options.bNoGoWide,
NoOutput = false,
CullOutput = false,
CacheMessages = true,
IncludeDebugOutput = true,
};
// Read the testing script
List ScriptFragments = new();
int ManifestIndex = -1;
int ConsoleIndex = -1;
UhtSourceFile ScriptSourceFile = new(Session, Script);
Dictionary OutputFragments = new();
Session.Try(ScriptSourceFile, () =>
{
ScriptSourceFile.Read();
UhtTokenBufferReader Reader = new(ScriptSourceFile, ScriptSourceFile.Data.Memory);
bool done = false;
while (!done)
{
// Scan for the fragment header
ScriptFragmentType Type = ScriptFragmentType.Unknown;
string Name = "";
int HeaderStartPos = Reader.InputPos;
int HeaderEndPos = HeaderStartPos;
int LineNumber = 1;
while (true)
{
using UhtTokenSaveState SaveState = new(Reader);
UhtToken Token = Reader.GetLine();
if (Token.TokenType == UhtTokenType.EndOfFile)
{
break;
}
if (Token.Value.Span.Length == 0 || (Token.Value.Span.Length > 0 && Token.Value.Span[0] != '!'))
{
break;
}
HeaderEndPos = Reader.InputPos;
int EndCommandPos = Token.Value.Span.IndexOf(' ');
if (EndCommandPos == -1)
{
EndCommandPos = Token.Value.Span.Length;
}
string ScriptFragmentTypeString = Token.Value.Span[1..EndCommandPos].Trim().ToString();
if (!System.Enum.TryParse(ScriptFragmentTypeString, true, out Type))
{
continue;
}
if (Type == ScriptFragmentType.Unknown)
{
continue;
}
Name = Token.Value.Span[EndCommandPos..].Trim().ToString();
LineNumber = Token.InputLine;
SaveState.AbandonState();
break;
}
// Scan for the fragment body
int BodyStartPos = Reader.InputPos;
int BodyEndPos = BodyStartPos;
while (true)
{
using var SaveState = new UhtTokenSaveState(Reader);
UhtToken Token = Reader.GetLine();
if (Token.TokenType == UhtTokenType.EndOfFile)
{
done = true;
break;
}
if (Token.Value.Span.Length > 0 && Token.Value.Span[0] == '!')
{
break;
}
BodyEndPos = Reader.InputPos;
SaveState.AbandonState();
}
ScriptFragments.Add(new ScriptFragment
{
Type = Type,
Name = Name.Replace("\\\\", "\\"), // Be kind to people cut/copy/paste escaped strings around
LineNumber = LineNumber,
Header = new StringView(ScriptSourceFile.Data.Memory[HeaderStartPos..HeaderEndPos]),
Body = new StringView(ScriptSourceFile.Data.Memory[BodyStartPos..BodyEndPos]),
External = false,
});
}
// Search for the manifest and any output. Add fragments to the session
for (int i = 0; i < ScriptFragments.Count; ++i)
{
switch (ScriptFragments[i].Type)
{
case ScriptFragmentType.Manifest:
if (ManifestIndex != -1)
{
ScriptSourceFile.LogError(ScriptFragments[i].LineNumber, "There can be only one manifest section in a test script");
break;
}
ManifestIndex = i;
if (ScriptFragments[i].Name.Length == 0)
{
ScriptSourceFile.LogError(ScriptFragments[i].LineNumber, "Manifest name can not be blank");
break;
}
TestFileManager.AddSourceFragment(ScriptSourceFile, ScriptFragments[i].Name, ScriptFragments[i].LineNumber, ScriptFragments[i].Body);
break;
case ScriptFragmentType.Console:
if (ConsoleIndex != -1)
{
ScriptSourceFile.LogError(ScriptFragments[i].LineNumber, "There can be only one console section in a test script");
break;
}
ConsoleIndex = i;
break;
case ScriptFragmentType.Header:
if (ScriptFragments[i].Name.Length == 0)
{
ScriptSourceFile.LogError(ScriptFragments[i].LineNumber, "Header name can not be blank");
break;
}
if (ScriptFragments[i].Body.Length == 0)
{
// Read the NoExportTypes.h file from the engine source so we don't have to keep a copy
if (Path.GetFileName(ScriptFragments[i].Name).Equals("NoExportTypes.h", StringComparison.OrdinalIgnoreCase))
{
string ExternalPath = Path.Combine(Unreal.EngineDirectory.FullName, ScriptFragments[i].Name);
if (File.Exists(ExternalPath))
{
ScriptFragment Copy = ScriptFragments[i];
Copy.Body = new StringView(File.ReadAllText(ExternalPath));
Copy.External = true;
ScriptFragments[i] = Copy;
}
}
}
TestFileManager.AddSourceFragment(ScriptSourceFile, ScriptFragments[i].Name, ScriptFragments[i].LineNumber, ScriptFragments[i].Body);
break;
case ScriptFragmentType.Output:
OutputFragments.Add(ScriptFragments[i].Name, i);
break;
}
}
if (ManifestIndex == -1)
{
ScriptSourceFile.LogError("There must be a manifest section in a test script");
}
if (ConsoleIndex == -1)
{
ScriptSourceFile.LogError("There must be a console section in a test script");
}
});
// Run UHT
if (!Session.HasErrors)
{
Session.Run(ScriptFragments[ManifestIndex].Name);
}
// If we have no console index, then there is nothing we can do. This is a fatal error than can not be tested
bool bSuccess = true;
if (ConsoleIndex == -1)
{
ScriptSourceFile.LogError("Unable to do any verification without a console section");
Session.LogMessages();
File.Copy(InPath, OutPath, true);
bSuccess = false;
}
else
{
// Generate the console block
List ConsoleLines = Session.CollectMessages();
StringBuilder SBConsole = new();
foreach (string Line in ConsoleLines)
{
SBConsole.AppendLine(Line);
}
// Verify the console block
// We trim the ends because it is too easy to leave off the ending CRLF in the script file.
if (ScriptFragments[ConsoleIndex].Body.ToString().TrimEnd() != SBConsole.ToString().TrimEnd())
{
Logger.LogError("Console output failed to match");
bSuccess = false;
}
// Check the output
foreach (KeyValuePair KVP in TestFileManager.Outputs)
{
if (OutputFragments.TryGetValue(KVP.Key, out int Index))
{
if (ScriptFragments[Index].Body.ToString().TrimEnd() != KVP.Value.TrimEnd())
{
Logger.LogError("Output \"{Key}\" failed to match", KVP.Key);
bSuccess = false;
}
OutputFragments.Remove(KVP.Key);
}
else
{
Logger.LogError("Output \"{Key}\" not found in test script", KVP.Key);
}
}
foreach (KeyValuePair KVP in OutputFragments)
{
Logger.LogError("Output \"{Key}\" in test script but not generated", KVP.Key);
}
// Create the complete output. Output includes all of the source fragments and console fragments
// and followed the output data sorted by file name.
StringBuilder SBTest = new();
for (int i = 0; i < ScriptFragments.Count; ++i)
{
if (ScriptFragments[i].Type != ScriptFragmentType.Output)
{
SBTest.Append(ScriptFragments[i].Header);
if (i == ConsoleIndex)
{
SBTest.Append(SBConsole);
}
else if (!ScriptFragments[i].External)
{
SBTest.Append(ScriptFragments[i].Body);
}
}
}
// Add the output
foreach (KeyValuePair KVP in TestFileManager.Outputs)
{
SBTest.Append($"!output {KVP.Key}\r\n");
SBTest.Append(KVP.Value);
}
// Write the final content
try
{
File.WriteAllText(OutPath, SBTest.ToString());
}
catch (Exception E)
{
Logger.LogError(E, "Unable to write test result to \"{Ex}\"", E.Message);
}
}
if (bSuccess)
{
Logger.LogInformation("Test {InPath} succeeded", InPath);
}
else
{
Logger.LogError("Test {InPath} failed", InPath);
}
return bSuccess;
}
private static bool RunScriptTests(UhtTables Tables, IUhtConfig Config, UhtGlobalOptions Options, string TestDirectory, string TestOutputDirectory, List Scripts, ILogger Logger)
{
bool bResult = true;
foreach (string Script in Scripts)
{
bResult &= RunScriptTest(Tables, Config, Options, TestDirectory, TestOutputDirectory, Script, Logger);
}
return bResult;
}
private static bool RunDirectoryTests(UhtTables Tables, IUhtConfig Config, UhtGlobalOptions Options, string TestDirectory, string TestOutputDirectory, List Directories, ILogger Logger)
{
bool bResult = true;
foreach (string Directory in Directories)
{
bResult &= RunTests(Tables, Config, Options, Path.Combine(TestDirectory, Directory), Path.Combine(TestOutputDirectory, Directory), Logger);
}
return bResult;
}
private static bool RunTests(UhtTables Tables, IUhtConfig Config, UhtGlobalOptions Options, string TestDirectory, string TestOutputDirectory, ILogger Logger)
{
// Create output directory
Directory.CreateDirectory(TestOutputDirectory);
List Scripts = new();
foreach (string Script in Directory.EnumerateFiles(TestDirectory, "*.uhttest"))
{
Scripts.Add(Path.GetFileName(Script));
}
Scripts.Sort(StringComparer.OrdinalIgnoreCase);
List Directories = new();
foreach (string Directory in Directory.EnumerateDirectories(TestDirectory))
{
Directories.Add(Path.GetFileName(Directory));
}
Directories.Sort(StringComparer.OrdinalIgnoreCase);
List Manifests = new();
foreach (string Manifest in Directory.EnumerateFiles(TestDirectory, "*.uhtmanifest"))
{
Manifests.Add(Path.GetFileName(Manifest));
}
Manifests.Sort(StringComparer.OrdinalIgnoreCase);
return
RunScriptTests(Tables, Config, Options, TestDirectory, TestOutputDirectory, Scripts, Logger) &&
RunDirectoryTests(Tables, Config, Options, TestDirectory, TestOutputDirectory, Directories, Logger);
}
public static bool RunTests(UhtTables Tables, IUhtConfig Config, UhtGlobalOptions Options, ILogger Logger)
{
DirectoryReference EngineSourceProgramDirectory = DirectoryReference.Combine(Unreal.EngineDirectory, "Source", "Programs");
string TestDirectory = FileReference.Combine(EngineSourceProgramDirectory, "UnrealBuildTool.Tests", "UHT").FullName;
string TestOutputDirectory = FileReference.Combine(EngineSourceProgramDirectory, "UnrealBuildTool.Tests", "UHT.Out").FullName;
// Clear the output directory
try
{
Directory.Delete(TestOutputDirectory, true);
}
catch (Exception)
{ }
// Collect a list of all the test scripts
Logger.LogInformation("Running tests in {TestDirectory}", TestDirectory);
Logger.LogInformation("Output can be compared in {TestOutputDirectory}", TestOutputDirectory);
// Run the tests on the directory
return RunTests(Tables, Config, Options, TestDirectory, TestOutputDirectory, Logger);
}
}
///
/// Invoke UHT
///
[ToolMode("UnrealHeaderTool", ToolModeOptions.XmlConfig | ToolModeOptions.BuildPlatforms | ToolModeOptions.ShowExecutionTime)]
class UnrealHeaderToolMode : ToolMode
{
///
/// Directory for saved application settings (typically Engine/Programs)
///
static DirectoryReference? CachedEngineProgramSavedDirectory;
///
/// The engine programs directory
///
public static DirectoryReference EngineProgramSavedDirectory
{
get
{
if (CachedEngineProgramSavedDirectory == null)
{
if (Unreal.IsEngineInstalled())
{
CachedEngineProgramSavedDirectory = Utils.GetUserSettingDirectory() ?? DirectoryReference.Combine(Unreal.EngineDirectory, "Programs");
}
else
{
CachedEngineProgramSavedDirectory = DirectoryReference.Combine(Unreal.EngineDirectory, "Programs");
}
}
return CachedEngineProgramSavedDirectory;
}
}
///
/// Print (incomplete) usage information
///
/// Defined exporters
/// Configuration
private static void PrintUsage(UhtExporterTable ExporterTable, IUhtConfig Config)
{
Console.WriteLine("UnrealBuildTool -Mode=UnrealHeaderTool [ProjectFile ManifestFile] -OR [\"-Target...\"] [Options]");
Console.WriteLine("");
Console.WriteLine("Options:");
int LongestPrefix = 0;
foreach (FieldInfo Info in typeof(UhtGlobalOptions).GetFields())
{
foreach (CommandLineAttribute Att in Info.GetCustomAttributes())
{
if (Att.Prefix != null && Att.Description != null)
{
LongestPrefix = Att.Prefix.Length > LongestPrefix ? Att.Prefix.Length : LongestPrefix;
}
}
}
foreach (UhtExporter Generator in ExporterTable)
{
LongestPrefix = Generator.Name.Length + 2 > LongestPrefix ? Generator.Name.Length + 2 : LongestPrefix;
}
foreach (FieldInfo Info in typeof(UhtGlobalOptions).GetFields())
{
foreach (CommandLineAttribute Att in Info.GetCustomAttributes())
{
if (Att.Prefix != null && Att.Description != null)
{
Console.WriteLine($" {Att.Prefix.PadRight(LongestPrefix)} : {Att.Description}");
}
}
}
Console.WriteLine("");
Console.WriteLine("Generators: Prefix with 'no' to disable a generator");
foreach (UhtExporter Generator in ExporterTable)
{
string IsDefault = Config.IsExporterEnabled(Generator.Name) || Generator.Options.HasAnyFlags(UhtExporterOptions.Default) ? " (Default)" : "";
Console.WriteLine($" -{Generator.Name.PadRight(LongestPrefix)} : {Generator.Description}{IsDefault}");
}
Console.WriteLine("");
}
///
/// Execute the command
///
/// Command line arguments
/// Exit code
///
public override int Execute(CommandLineArguments Arguments, ILogger Logger)
{
try
{
// Initialize the attributes
UhtTables Tables = new();
// Initialize the config
IUhtConfig Config = new UhtConfigImpl(Arguments);
// Parse the global options
UhtGlobalOptions Options = new(Arguments);
int TargetArgumentIndex = -1;
if (Arguments.GetPositionalArgumentCount() == 0)
{
for (int Index = 0; Index < Arguments.Count; ++Index)
{
if (Arguments[Index].StartsWith("-Target", StringComparison.OrdinalIgnoreCase))
{
TargetArgumentIndex = Index;
break;
}
}
}
int RequiredArgCount = TargetArgumentIndex >= 0 || Options.bTest ? 0 : 2;
if (Arguments.GetPositionalArgumentCount() != RequiredArgCount || Options.bGetHelp)
{
PrintUsage(Tables.ExporterTable, Config);
return Options.bGetHelp ? (int)CompilationResult.Succeeded : (int)CompilationResult.OtherCompilationError;
}
// Configure the log system
Log.OutputLevel = Options.LogOutputLevel;
Log.IncludeTimestamps = Options.bLogTimestamps;
Log.IncludeProgramNameWithSeverityPrefix = Options.bLogFromMsBuild;
// Add the log writer if requested. When building a target, we'll create the writer for the default log file later.
if (!Options.bNoLog)
{
if (Options.LogFileName != null)
{
Log.AddFileWriter("LogTraceListener", Options.LogFileName);
}
if (!Log.HasFileWriter())
{
string BaseLogFileName = FileReference.Combine(EngineProgramSavedDirectory, "UnrealHeaderTool", "Saved", "Logs", "UnrealHeaderTool.log").FullName;
FileReference LogFile = new(BaseLogFileName);
foreach (string LogSuffix in Arguments.GetValues("-LogSuffix="))
{
LogFile = LogFile.ChangeExtension(null) + "_" + LogSuffix + LogFile.GetExtension();
}
Log.AddFileWriter("DefaultLogTraceListener", LogFile);
}
}
// If we are running test scripts
if (Options.bTest)
{
return UhtTestHarness.RunTests(Tables, Config, Options, Logger) ? (int)CompilationResult.Succeeded : (int)CompilationResult.OtherCompilationError;
}
string? ProjectFile = null;
string? ManifestPath = null;
if (TargetArgumentIndex >= 0)
{
CommandLineArguments LocalArguments = new(new string[] { Arguments[TargetArgumentIndex] });
List TargetDescriptors = TargetDescriptor.ParseCommandLine(LocalArguments, false, false, false, Logger);
if (TargetDescriptors.Count == 0)
{
Logger.LogError("No target descriptors found.");
return (int)CompilationResult.OtherCompilationError;
}
TargetDescriptor TargetDesc = TargetDescriptors[0];
// Create the target
UEBuildTarget Target = UEBuildTarget.Create(TargetDesc, false, false, false, Logger);
// Create the makefile for the target and export the module information
using ISourceFileWorkingSet WorkingSet = new EmptySourceFileWorkingSet();
// Create the build configuration object, and read the settings
BuildConfiguration BuildConfiguration = new();
XmlConfig.ApplyTo(BuildConfiguration);
Arguments.ApplyTo(BuildConfiguration);
// Create the makefile
TargetMakefile Makefile = Target.Build(BuildConfiguration, WorkingSet, TargetDesc, Logger, true);
FileReference ModuleInfoFileName = ExternalExecution.GetUHTModuleInfoFileName(Makefile, Target.TargetName);
FileReference DepsFileName = ExternalExecution.GetUHTDepsFileName(ModuleInfoFileName);
ManifestPath = ModuleInfoFileName.FullName;
ExternalExecution.WriteUHTManifest(Makefile, Target.TargetName, ModuleInfoFileName, DepsFileName);
if (Target.ProjectFile != null)
{
ProjectFile = Target.ProjectFile.FullName;
}
}
else
{
ProjectFile = Arguments.GetPositionalArguments()[0];
ManifestPath = Arguments.GetPositionalArguments()[1];
}
string? ProjectPath = ProjectFile != null ? Path.GetDirectoryName(ProjectFile) : null;
UhtSession Session = new()
{
Tables = Tables,
Config = Config,
FileManager = new UhtStdFileManager(),
EngineDirectory = Unreal.EngineDirectory.FullName,
ProjectFile = ProjectFile,
ProjectDirectory = string.IsNullOrEmpty(ProjectPath) ? null : ProjectPath,
ReferenceDirectory = FileReference.Combine(EngineProgramSavedDirectory, "UnrealHeaderTool", "Saved", "ReferenceExports").FullName,
VerifyDirectory = FileReference.Combine(EngineProgramSavedDirectory, "UnrealHeaderTool", "Saved", "VerifyExports").FullName,
WarningsAsErrors = Options.bWarningsAsErrors,
GoWide = !Options.bNoGoWide,
FailIfGeneratedCodeChanges = Options.bFailIfGeneratedCodeChanges,
NoOutput = Options.bNoOutput,
IncludeDebugOutput = Options.bIncludeDebugOutput,
NoDefaultExporters = Options.bNoDefaultExporters,
};
if (Options.bWriteRef)
{
Session.ReferenceMode = UhtReferenceMode.Reference;
}
else if (Options.bVerifyRef)
{
Session.ReferenceMode = UhtReferenceMode.Verify;
}
foreach(UhtExporter Exporter in Session.ExporterTable)
{
if (Arguments.HasOption($"-{Exporter.Name}"))
{
Session.SetExporterStatus(Exporter.Name, true);
}
else if (Arguments.HasOption($"-no{Exporter.Name}"))
{
Session.SetExporterStatus(Exporter.Name, false);
}
}
// Read and parse
Session.Run(ManifestPath!);
Session.LogMessages();
return (int)(Session.HasErrors ? CompilationResult.OtherCompilationError : CompilationResult.Succeeded);
}
catch (Exception Ex)
{
// Unhandled exception.
Logger.LogError(Ex, "Unhandled exception: {Ex}", ExceptionUtils.FormatException(Ex));
Logger.LogDebug(Ex, "Unhandled exception: {Ex}", ExceptionUtils.FormatExceptionDetails(Ex));
return (int)CompilationResult.OtherCompilationError;
}
}
}
}