Files
UnrealEngine/Engine/Source/Programs/Shared/EpicGames.UHT/Utils/UhtSession.cs
2025-05-18 13:04:45 +08:00

2722 lines
81 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Enumeration;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using EpicGames.UHT.Parsers;
using EpicGames.UHT.Tables;
using EpicGames.UHT.Tokenizer;
using EpicGames.UHT.Types;
using Microsoft.Extensions.Logging;
namespace EpicGames.UHT.Utils
{
/// <summary>
/// To support the testing framework, source files can be containing in other source files.
/// A source fragment represents this possibility.
/// </summary>
public struct UhtSourceFragment
{
/// <summary>
/// When not null, this source comes from another source file
/// </summary>
public UhtSourceFile? SourceFile { get; set; }
/// <summary>
/// The file path of the source
/// </summary>
public string FilePath { get; set; }
/// <summary>
/// The line number of the fragment in the containing source file.
/// </summary>
public int LineNumber { get; set; }
/// <summary>
/// Data of the source file
/// </summary>
public StringView Data { get; set; }
}
/// <summary>
/// A structure that represents an engine version.
/// </summary>
public struct EngineVersion
{
/// <summary>
/// The major engine version.
/// </summary>
public int MajorVersion { get; set; } = 0;
/// <summary>
/// The minor engine version.
/// </summary>
public int MinorVersion { get; set; } = 0;
/// <summary>
/// The patch engine version.
/// </summary>
public int PatchVersion { get; set; } = 0;
/// <summary>
/// Build a default engine version (0.0.0).
/// </summary>
public EngineVersion() { }
/// <summary>
/// Build a new engine version with the given numbers.
/// </summary>
public EngineVersion(int major, int minor, int patch)
{
MajorVersion = major;
MinorVersion = minor;
PatchVersion = patch;
}
/// <summary>
/// Compares this engine version to another one.
/// </summary>
/// <param name="other">The other engine version</param>
/// <returns>Returns -1 if this version is less than the other, 1 if it is greater than the other, and 0 if they're equal</returns>
public int CompareTo(EngineVersion other)
{
if (MajorVersion != other.MajorVersion)
{
return MajorVersion.CompareTo(other.MajorVersion);
}
if (MinorVersion != other.MinorVersion)
{
return MinorVersion.CompareTo(other.MinorVersion);
}
if (PatchVersion != other.PatchVersion)
{
return PatchVersion.CompareTo(other.PatchVersion);
}
return 0;
}
/// <summary>
/// Generates a string representation of the engine version.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return String.Format("{0}.{1}.{2}", MajorVersion, MinorVersion, PatchVersion);
}
}
/// <summary>
/// Implementation of the export factory
/// </summary>
class UhtExportFactory : IUhtExportFactory
{
public struct Output
{
public string FilePath { get; set; }
public string TempFilePath { get; set; }
public bool Saved { get; set; }
}
/// <summary>
/// UHT session
/// </summary>
private readonly UhtSession _session;
/// <summary>
/// Module associated with the plugin
/// </summary>
private readonly UHTManifest.Module? _pluginModule;
/// <summary>
/// Limiter for the number of files being saved to the reference directory.
/// The OS can get swamped on high core systems
/// </summary>
private static readonly Semaphore s_writeRefSemaphore = new(32, 32);
/// <summary>
/// Requesting exporter
/// </summary>
public readonly UhtExporter Exporter;
/// <summary>
/// UHT Session
/// </summary>
public UhtSession Session => _session;
/// <summary>
/// Plugin module
/// </summary>
public UHTManifest.Module? PluginModule => _pluginModule;
/// <summary>
/// Collection of error from mismatches with the reference files
/// </summary>
public Dictionary<string, bool> ReferenceErrorMessages { get; } = new Dictionary<string, bool>();
/// <summary>
/// List of export outputs
/// </summary>
public List<Output> Outputs { get; } = new List<Output>();
/// <summary>
/// Directory for the reference output
/// </summary>
public string ReferenceDirectory { get; set; } = String.Empty;
/// <summary>
/// Directory for the verify output
/// </summary>
public string VerifyDirectory { get; set; } = String.Empty;
/// <summary>
/// Collection of external dependencies
/// </summary>
public HashSet<string> ExternalDependencies { get; } = new HashSet<string>();
/// <summary>
/// Create a new instance of the export factory
/// </summary>
/// <param name="session">UHT session</param>
/// <param name="pluginModule">Plugin module</param>
/// <param name="exporter">Exporter being run</param>
public UhtExportFactory(UhtSession session, UHTManifest.Module? pluginModule, UhtExporter exporter)
{
_session = session;
_pluginModule = pluginModule;
Exporter = exporter;
if (Session.ReferenceMode != UhtReferenceMode.None)
{
ReferenceDirectory = Path.Combine(Session.ReferenceDirectory, Exporter.Name);
VerifyDirectory = Path.Combine(Session.VerifyDirectory, Exporter.Name);
Directory.CreateDirectory(Session.ReferenceMode == UhtReferenceMode.Reference ? ReferenceDirectory : VerifyDirectory);
}
}
/// <summary>
/// Commit the contents of the string builder as the output.
/// If you have a string builder, use this method so that a
/// temporary buffer can be used.
/// </summary>
/// <param name="filePath">Destination file path</param>
/// <param name="builder">Source for the content</param>
public void CommitOutput(string filePath, StringBuilder builder)
{
using UhtRentedPoolBuffer<char> borrowBuffer = builder.RentPoolBuffer();
string tempFilePath = filePath + ".tmp";
SaveIfChanged(filePath, tempFilePath, new StringView(borrowBuffer.Buffer.Memory));
}
/// <summary>
/// Commit the value of the string as the output
/// </summary>
/// <param name="filePath">Destination file path</param>
/// <param name="output">Output to commit</param>
public void CommitOutput(string filePath, StringView output)
{
string tempFilePath = filePath + ".tmp";
SaveIfChanged(filePath, tempFilePath, output);
}
/// <summary>
/// Create a task to export two files
/// </summary>
/// <param name="prereqs">Tasks that must be completed prior to this task running</param>
/// <param name="action">Action to be invoked to generate the output</param>
/// <returns>Task object or null if the task was immediately executed.</returns>
public Task? CreateTask(List<Task?>? prereqs, UhtExportTaskDelegate action)
{
if (Session.GoWide)
{
Task[]? prereqTasks = prereqs?.Where(x => x != null).Cast<Task>().ToArray();
if (prereqTasks != null && prereqTasks.Length > 0)
{
return Task.Factory.ContinueWhenAll(prereqTasks, (Task[] tasks) => { Session.TryNoErrorCheck(null, () => action(this)); });
}
else
{
return Task.Factory.StartNew(() => { Session.TryNoErrorCheck(null, () => action(this)); }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
}
}
else
{
Session.TryNoErrorCheck(null, () => action(this));
return null;
}
}
/// <summary>
/// Create a task to export two files
/// </summary>
/// <param name="action">Action to be invoked to generate the output</param>
/// <returns>Task object or null if the task was immediately executed.</returns>
public Task? CreateTask(UhtExportTaskDelegate action)
{
return CreateTask(null, action);
}
/// <summary>
/// Given a header file, generate the output file name.
/// </summary>
/// <param name="headerFile">Header file</param>
/// <param name="suffix">Suffix/extension to be added to the file name.</param>
/// <returns>Resulting file name</returns>
public string MakePath(UhtHeaderFile headerFile, string suffix)
{
return MakePath(headerFile.Module.Module, headerFile.FileNameWithoutExtension, suffix);
}
/// <summary>
/// Given a package file, generate the output file name
/// </summary>
/// <param name="module">Module</param>
/// <param name="suffix">Suffix/extension to be added to the file name.</param>
/// <returns>Resulting file name</returns>
public string MakePath(UhtModule module, string suffix)
{
return MakePath(module.Module, module.ShortName, suffix);
}
/// <summary>
/// Make a path for an output based on the package output directory.
/// </summary>
/// <param name="fileName">Name of the file</param>
/// <param name="extension">Extension to add to the file</param>
/// <returns>Output file path</returns>
public string MakePath(string fileName, string extension)
{
if (PluginModule == null)
{
throw new UhtIceException("MakePath with just a filename and extension can not be called from non-plugin exporters");
}
return MakePath(PluginModule, fileName, extension);
}
/// <summary>
/// Add an external dependency to the given file path
/// </summary>
/// <param name="filePath">External dependency to add</param>
public void AddExternalDependency(string filePath)
{
ExternalDependencies.Add(filePath);
}
/// <inheritdoc/>
public string GetModuleShortestIncludePath(UhtModule moduleObj, string filePath)
{
return GetShortestIncludePath(moduleObj.Module, filePath);
}
/// <inheritdoc/>
public string GetPluginShortestIncludePath(string filePath)
{
if (PluginModule == null)
{
throw new UhtIceException("Made a request for the plugin's shortest include path but there is no plugin");
}
return GetShortestIncludePath(PluginModule, filePath);
}
public string GetShortestIncludePath(UHTManifest.Module moduleObj, string filePath)
{
string? relativePath = null;
foreach (string includePath in moduleObj.IncludePaths)
{
if (filePath.StartsWith(includePath, StringComparison.Ordinal))
{
if (relativePath == null || (includePath.Length - filePath.Length - 1) < relativePath.Length)
{
relativePath = filePath.Substring(includePath.Length + 1);
}
}
}
// This will create a "../" path which is not great
if (relativePath == null)
{
string? includePath = moduleObj.IncludePaths.Find(includePath => includePath.StartsWith(moduleObj.BaseDirectory, StringComparison.Ordinal));
if (includePath == null)
{
Session.Logger.LogWarning("There are no include paths in module {ModuleName} for {FileName}. It will be included by a \"../\" path external to the module.", moduleObj.Name, filePath);
includePath = moduleObj.IncludePaths.FirstOrDefault(".");
}
relativePath = Path.GetRelativePath(includePath!, filePath);
}
return relativePath!.Replace("\\", "/", StringComparison.Ordinal);
}
private string MakePath(UHTManifest.Module module, string fileName, string suffix)
{
if (PluginModule != null)
{
module = PluginModule;
}
return Path.Combine(module.OutputDirectory, fileName) + suffix;
}
/// <summary>
/// Helper method to test to see if the output has changed.
/// </summary>
/// <param name="filePath">Name of the output file</param>
/// <param name="tempFilePath">Name of the temporary file</param>
/// <param name="exported">Exported contents of the file</param>
internal void SaveIfChanged(string filePath, string tempFilePath, StringView exported)
{
ReadOnlySpan<char> exportedSpan = exported.Span;
if (Session.ReferenceMode != UhtReferenceMode.None)
{
string fileName = Path.GetFileName(filePath);
// Writing billions of files to the same directory causes issues. Use ourselves to throttle reference writes
try
{
UhtExportFactory.s_writeRefSemaphore.WaitOne();
{
string outPath = Path.Combine(Session.ReferenceMode == UhtReferenceMode.Reference ? ReferenceDirectory : VerifyDirectory, fileName);
if (!Session.WriteSource(outPath, exported.Span))
{
new UhtSimpleFileMessageSite(Session, outPath).LogWarning($"Unable to write reference file {outPath}");
}
}
}
finally
{
UhtExportFactory.s_writeRefSemaphore.Release();
}
// If we are verifying, read the existing file and check the contents
if (Session.ReferenceMode == UhtReferenceMode.Verify)
{
string message = String.Empty;
string refPath = Path.Combine(ReferenceDirectory, fileName);
if (Session.ReadSource(refPath, out UhtPoolBuffer<char> existingRef))
{
ReadOnlySpan<char> existingSpan = existingRef.Memory.Span;
if (existingSpan.CompareTo(exportedSpan, StringComparison.Ordinal) != 0)
{
message = $"********************************* {fileName} has changed.";
}
UhtPoolBuffers.Return<char>(existingRef);
}
else
{
message = $"********************************* {fileName} appears to be a new generated file.";
}
if (!String.IsNullOrEmpty(message))
{
Session.Logger.LogInformation("{Message}", message);
Session.SignalError();
lock (ReferenceErrorMessages)
{
ReferenceErrorMessages.Add(message, true);
}
}
}
}
// Check to see if the contents have changed
bool save = false;
if (Session.ReadSource(filePath, out UhtPoolBuffer<char> original))
{
ReadOnlySpan<char> originalSpan = original.Memory.Span;
if (originalSpan.CompareTo(exportedSpan, StringComparison.Ordinal) != 0)
{
if (Session.FailIfGeneratedCodeChanges)
{
string conflictPath = filePath + ".conflict";
if (!Session.WriteSource(conflictPath, exported.Span))
{
new UhtSimpleFileMessageSite(Session, filePath).LogError($"Changes to generated code are not allowed - conflicts written to '{conflictPath}'");
}
}
save = true;
}
UhtPoolBuffers.Return<char>(original);
}
else
{
save = true;
}
// If changed of the original didn't exist, then save the new version
if (save && !Session.NoOutput)
{
if (!Session.WriteSource(tempFilePath, exported.Span))
{
new UhtSimpleFileMessageSite(Session, filePath).LogWarning($"Failed to save export file: '{tempFilePath}'");
}
}
else
{
save = false;
}
// Add this to the list of outputs
lock (Outputs)
{
Outputs.Add(new Output { FilePath = filePath, TempFilePath = tempFilePath, Saved = save });
}
}
/// <summary>
/// Run the output exporter
/// </summary>
public void Run()
{
// Invoke the exported via the delegate
Exporter.Delegate(this);
// These outputs are used to cull old outputs from the directories
Dictionary<string, HashSet<string>> outputsByDirectory = new(
Session.Modules.Where(x => x.Module.SaveExportedHeaders).Select(x => new KeyValuePair<string, HashSet<string>>(x.Module.OutputDirectory, new HashSet<string>(StringComparer.OrdinalIgnoreCase))),
StringComparer.OrdinalIgnoreCase
);
// If outputs were exported
if (Outputs.Count > 0)
{
List<UhtExportFactory.Output> saves = new();
// Collect information about the outputs
foreach (UhtExportFactory.Output output in Outputs)
{
// Add this output to the list of expected outputs by directory
string? fileDirectory = Path.GetDirectoryName(output.FilePath);
if (fileDirectory != null)
{
if (!outputsByDirectory.TryGetValue(fileDirectory, out HashSet<string>? files))
{
files = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
outputsByDirectory.Add(fileDirectory, files);
}
files.Add(Path.GetFileName(output.FilePath));
}
// Add the save task
if (output.Saved)
{
saves.Add(output);
}
}
// Perform the renames
if (Session.GoWide)
{
Parallel.ForEach(saves, (UhtExportFactory.Output output) =>
{
RenameSource(output);
});
}
else
{
foreach (UhtExportFactory.Output output in saves)
{
RenameSource(output);
}
}
}
// Perform the culling of the output directories
if (Session.CullOutput && !Session.NoOutput &&
(Exporter.CppFilters != null || Exporter.HeaderFilters != null || Exporter.OtherFilters != null))
{
if (Session.GoWide)
{
Parallel.ForEach(outputsByDirectory, (KeyValuePair<string, HashSet<string>> kvp) =>
{
CullOutputDirectory(kvp.Key, kvp.Value);
});
}
else
{
foreach (KeyValuePair<string, HashSet<string>> kvp in outputsByDirectory)
{
CullOutputDirectory(kvp.Key, kvp.Value);
}
}
}
}
/// <summary>
/// Given an output, rename the output file from the temporary file name to the final file name.
/// If there exists a current final file, it will be replaced.
/// </summary>
/// <param name="output">The output file to rename</param>
private void RenameSource(UhtExportFactory.Output output)
{
Session.RenameSource(output.TempFilePath, output.FilePath);
}
/// <summary>
/// Given a directory and a list of known files, delete any unknown file that matches of the supplied filters
/// </summary>
/// <param name="outputDirectory">Output directory to scan</param>
/// <param name="knownOutputs">Collection of known output files not to be deleted</param>
private void CullOutputDirectory(string outputDirectory, HashSet<string> knownOutputs)
{
foreach (string filePath in Directory.EnumerateFiles(outputDirectory))
{
string fileName = Path.GetFileName(filePath);
if (knownOutputs.Contains(fileName))
{
continue;
}
if (IsFilterMatch(fileName, Exporter.CppFilters) ||
IsFilterMatch(fileName, Exporter.HeaderFilters) ||
IsFilterMatch(fileName, Exporter.OtherFilters))
{
try
{
File.Delete(Path.Combine(outputDirectory, filePath));
}
catch (Exception)
{
}
}
}
}
/// <summary>
/// Test to see if the given filename (without directory), matches one of the given filters
/// </summary>
/// <param name="fileName">File name to test</param>
/// <param name="filters">List of wildcard filters</param>
/// <returns>True if there is a match</returns>
private static bool IsFilterMatch(string fileName, IEnumerable<string> filters)
{
foreach (string filter in filters)
{
if (FileSystemName.MatchesSimpleExpression(filter, fileName, true))
{
return true;
}
}
return false;
}
}
/// <summary>
/// UHT supports the exporting of two reference output directories for testing. The reference version can be used to test
/// modification to UHT and verify there are no output changes or just expected changes.
/// </summary>
public enum UhtReferenceMode
{
/// <summary>
/// Do not export any reference output files
/// </summary>
None,
/// <summary>
/// Export the reference copy
/// </summary>
Reference,
/// <summary>
/// Export the verify copy and compare to the reference copy
/// </summary>
Verify,
};
/// <summary>
/// Session object that represents a UHT run
/// </summary>
public partial class UhtSession : IUhtMessageSite, IUhtMessageSession
{
/// <summary>
/// Helper class for returning a sequence of auto-incrementing indices
/// </summary>
private class TypeCounter
{
/// <summary>
/// Current number of types
/// </summary>
private int _count = 0;
/// <summary>
/// Get the next type index
/// </summary>
/// <returns>Index starting at zero</returns>
public int GetNext()
{
return Interlocked.Increment(ref _count) - 1;
}
/// <summary>
/// The number of times GetNext was called.
/// </summary>
public int Count => Interlocked.Add(ref _count, 0) + 1;
}
/// <summary>
/// Pair that represents a specific value for an enumeration
/// </summary>
private struct EnumAndValue
{
public UhtEnum Enum { get; set; }
public long Value { get; set; }
}
/// <summary>
/// Collection of reserved names
/// </summary>
private static readonly HashSet<string> s_reservedNames = new() { "none" };
#region Configurable settings
/// <summary>
/// Logger interface.
/// </summary>
public ILogger Logger { get; }
/// <summary>
/// Interface used to read/write files
/// </summary>
public IUhtFileManager? FileManager { get; set; }
/// <summary>
/// Location of the engine code
/// </summary>
public string? EngineDirectory { get; set; }
/// <summary>
/// If set, the name of the project file.
/// </summary>
public string? ProjectFile { get; set; }
/// <summary>
/// Optional location of the project
/// </summary>
public string? ProjectDirectory { get; set; }
/// <summary>
/// Root directory for the engine. This is usually just EngineDirectory without the Engine directory.
/// </summary>
public string? RootDirectory { get; set; }
/// <summary>
/// Directory to store the reference output
/// </summary>
public string ReferenceDirectory { get; set; } = String.Empty;
/// <summary>
/// Directory to store the verification output
/// </summary>
public string VerifyDirectory { get; set; } = String.Empty;
/// <summary>
/// Mode for generating and/or testing reference output
/// </summary>
public UhtReferenceMode ReferenceMode { get; set; } = UhtReferenceMode.None;
/// <summary>
/// If true, warnings are considered to be errors
/// </summary>
public bool WarningsAsErrors { get; set; } = false;
/// <summary>
/// If true, include relative file paths in the log file
/// </summary>
public bool RelativePathInLog { get; set; } = false;
/// <summary>
/// If true, use concurrent tasks to run UHT
/// </summary>
public bool GoWide { get; set; } = true;
/// <summary>
/// If any output file mismatches existing outputs, an error will be generated
/// </summary>
public bool FailIfGeneratedCodeChanges { get; set; } = false;
/// <summary>
/// If true, no output files will be saved
/// </summary>
public bool NoOutput { get; set; } = false;
/// <summary>
/// If true, cull the output for any extra files
/// </summary>
public bool CullOutput { get; set; } = true;
/// <summary>
/// If true, include extra output in code generation
/// </summary>
public bool IncludeDebugOutput { get; set; } = false;
/// <summary>
/// If true, disable all exporters which would normally be run by default
/// </summary>
public bool NoDefaultExporters { get; set; } = false;
/// <summary>
/// If true, cache any error messages until the end of processing. This is used by the testing
/// harness to generate more stable console output.
/// </summary>
public bool CacheMessages { get; set; } = false;
/// <summary>
/// Collection of UHT tables
/// </summary>
public UhtTables? Tables { get; set; }
/// <summary>
/// Configuration for the session
/// </summary>
public IUhtConfig? Config { get; set; }
#endregion
/// <summary>
/// Manifest file
/// </summary>
public UhtManifestFile? ManifestFile { get; set; } = null;
/// <summary>
/// Manifest data from the manifest file
/// </summary>
public UHTManifest? Manifest => ManifestFile?.Manifest;
/// <summary>
/// Collection of modules from the manifest
/// </summary>
public IReadOnlyList<UhtModule> Modules => _modules;
/// <summary>
/// Collection of header files from the manifest. The header files will also appear as the children
/// of the packages
/// </summary>
public IReadOnlyList<UhtHeaderFile> HeaderFiles => _headerFiles;
/// <summary>
/// Collection of header files topologically sorted. This will not be populated until after header files
/// are parsed and resolved.
/// </summary>
public IReadOnlyList<UhtHeaderFile> SortedHeaderFiles => _sortedHeaderFiles;
/// <summary>
/// Dictionary of stripped file name to the header file
/// </summary>
public IReadOnlyDictionary<string, UhtHeaderFile> HeaderFileDictionary => _headerFileDictionary;
/// <summary>
/// Gets the version of the engine this session belongs to.
/// </summary>
public EngineVersion EngineVersion => _engineVersion.Value;
/// <summary>
/// After headers are parsed, returns the UObject class.
/// </summary>
public UhtClass UObject
{
get
{
if (_uobject == null)
{
throw new UhtIceException("UObject was not defined.");
}
return _uobject;
}
}
/// <summary>
/// After headers are parsed, returns the UClass class.
/// </summary>
public UhtClass UClass
{
get
{
if (_uclass == null)
{
throw new UhtIceException("UClass was not defined.");
}
return _uclass;
}
}
/// <summary>
/// After headers are parsed, returns the UInterface class.
/// </summary>
public UhtClass UInterface
{
get
{
if (_uinterface == null)
{
throw new UhtIceException("UInterface was not defined.");
}
return _uinterface;
}
}
/// <summary>
/// After headers are parsed, returns the IInterface class.
/// </summary>
public UhtClass IInterface
{
get
{
if (_iinterface == null)
{
throw new UhtIceException("IInterface was not defined.");
}
return _iinterface;
}
}
/// <summary>
/// After headers are parsed, returns the AActor class. Unlike such properties as "UObject", there
/// is no requirement for AActor to be defined. May be null.
/// </summary>
public UhtClass? AActor { get; set; } = null;
/// <summary>
/// After headers are parsed, return the INotifyFieldValueChanged interface. There is no requirement
/// that this interface be defined.
/// </summary>
public UhtClass? INotifyFieldValueChanged { get; set; } = null;
/// <summary>
/// After headers are parsed, returns the FInstancedStruct script struct. Unlike such properties as
/// "UObject", there is no requirement for FInstancedStruct to be defined. May be null.
/// </summary>
public UhtScriptStruct? FInstancedStruct { get; set; } = null;
/// <summary>
/// After headers are parsed, returns the FStateTreePropertyRef script struct.
/// There is no requirement for FStateTreePropertyRef to be defined. May be null.
/// </summary>
public UhtScriptStruct? FStateTreePropertyRef { get; set; } = null;
/// <summary>
/// After headers are parsed, returns the EVerseNativeCallResult enumeration.
/// There is no requirement for EVerseNativeCallResult to be defined. May be null.
/// </summary>
public UhtEnum? EVerseNativeCallResult { get; set; } = null;
private readonly List<UhtModule> _modules = new();
private readonly List<UhtHeaderFile> _headerFiles = new();
private readonly List<UhtHeaderFile> _sortedHeaderFiles = new();
private readonly Dictionary<string, UhtHeaderFile> _headerFileDictionary = new(StringComparer.OrdinalIgnoreCase);
private readonly Lazy<EngineVersion> _engineVersion;
private long _errorCount = 0;
private long _warningCount = 0;
private readonly List<UhtMessage> _messages = new();
private Task? _messageTask = null;
private UhtClass? _uobject = null;
private UhtClass? _uclass = null;
private UhtClass? _uinterface = null;
private UhtClass? _iinterface = null;
private readonly TypeCounter _typeCounter = new();
private readonly TypeCounter _packageTypeCount = new();
private readonly TypeCounter _headerFileTypeCount = new();
private readonly TypeCounter _objectTypeCount = new();
private UhtSymbolTable _sourceNameSymbolTable = new(0);
private UhtSymbolTable _engineNameSymbolTable = new(0);
private bool _symbolTablePopulated = false;
private Task? _referenceDeleteTask = null;
private readonly Dictionary<string, bool> _exporterStates = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, EnumAndValue> _fullEnumValueLookup = new();
private readonly Dictionary<string, UhtEnum> _shortEnumValueLookup = new();
private JsonDocument? _projectJson = null;
/// <summary>
/// The number of errors
/// </summary>
public long ErrorCount => Interlocked.Read(ref _errorCount);
/// <summary>
/// The number of warnings
/// </summary>
public long WarningCount => Interlocked.Read(ref _warningCount);
/// <summary>
/// True if any errors have occurred or warnings if warnings are to be treated as errors
/// </summary>
public bool HasErrors => ErrorCount > 0 || (WarningsAsErrors && WarningCount > 0);
#region IUHTMessageSession implementation
/// <inheritdoc/>
public IUhtMessageSession MessageSession => this;
/// <inheritdoc/>
public IUhtMessageSource? MessageSource => null;
/// <inheritdoc/>
public IUhtMessageLineNumber? MessageLineNumber => null;
#endregion
/// <summary>
/// Constructor
/// </summary>
/// <param name="logger">Logger for the session</param>
public UhtSession(ILogger logger)
{
Logger = logger;
_engineVersion = new Lazy<EngineVersion>(() => LoadEngineVersion(), true);
}
/// <summary>
/// Return the index for a newly defined type
/// </summary>
/// <returns>New index</returns>
public int GetNextTypeIndex()
{
return _typeCounter.GetNext();
}
/// <summary>
/// Return the number of types that have been defined. This includes all types.
/// </summary>
public int TypeCount => _typeCounter.Count;
/// <summary>
/// Return the index for a newly defined packaging
/// </summary>
/// <returns>New index</returns>
public int GetNextHeaderFileTypeIndex()
{
return _headerFileTypeCount.GetNext();
}
/// <summary>
/// Return the number of headers that have been defined
/// </summary>
public int HeaderFileTypeCount => _headerFileTypeCount.Count;
/// <summary>
/// Return the index for a newly defined package
/// </summary>
/// <returns>New index</returns>
public int GetNextPackageTypeIndex()
{
return _packageTypeCount.GetNext();
}
/// <summary>
/// Return the number of UPackage types that have been defined
/// </summary>
public int PackageTypeCount => _packageTypeCount.Count;
/// <summary>
/// Return the index for a newly defined object
/// </summary>
/// <returns>New index</returns>
public int GetNextObjectTypeIndex()
{
return _objectTypeCount.GetNext();
}
/// <summary>
/// Return the total number of UObject types that have been defined
/// </summary>
public int ObjectTypeCount => _objectTypeCount.Count;
/// <summary>
/// Return the collection of exporters
/// </summary>
public UhtExporterTable ExporterTable => Tables!.ExporterTable;
/// <summary>
/// Return the keyword table for the given table name
/// </summary>
/// <param name="tableName">Name of the table</param>
/// <returns>The requested table</returns>
public UhtKeywordTable GetKeywordTable(string tableName)
{
return Tables!.KeywordTables.Get(tableName);
}
/// <summary>
/// Return the specifier table for the given table name
/// </summary>
/// <param name="tableName">Name of the table</param>
/// <returns>The requested table</returns>
public UhtSpecifierTable GetSpecifierTable(string tableName)
{
return Tables!.SpecifierTables.Get(tableName);
}
/// <summary>
/// Return the specifier validator table for the given table name
/// </summary>
/// <param name="tableName">Name of the table</param>
/// <returns>The requested table</returns>
public UhtSpecifierValidatorTable GetSpecifierValidatorTable(string tableName)
{
return Tables!.SpecifierValidatorTables.Get(tableName);
}
/// <summary>
/// Generate an error for the given unhandled keyword
/// </summary>
/// <param name="tokenReader">Token reader</param>
/// <param name="token">Unhandled token</param>
public void LogUnhandledKeywordError(IUhtTokenReader tokenReader, UhtToken token)
{
Tables!.KeywordTables.LogUnhandledError(tokenReader, token);
}
/// <summary>
/// Test to see if the given class name is a property
/// </summary>
/// <param name="name">Name of the class without the prefix</param>
/// <returns>True if the class name is a property. False if the class name isn't a property or isn't an engine class.</returns>
public bool IsValidPropertyTypeName(StringView name)
{
return Tables!.EngineClassTable.IsValidPropertyTypeName(name);
}
/// <summary>
/// Return the loc text default value associated with the given name
/// </summary>
/// <param name="name"></param>
/// <param name="locTextDefaultValue">Loc text default value handler</param>
/// <returns></returns>
public bool TryGetLocTextDefaultValue(StringView name, out UhtLocTextDefaultValue locTextDefaultValue)
{
return Tables!.LocTextDefaultValueTable.TryGet(name, out locTextDefaultValue);
}
/// <summary>
/// Return the default processor
/// </summary>
public UhtPropertyType DefaultPropertyType => Tables!.PropertyTypeTable.Default;
/// <summary>
/// Return the property type associated with the given name
/// </summary>
/// <param name="name"></param>
/// <param name="propertyType">Property type if matched</param>
/// <returns></returns>
public bool TryGetPropertyType(StringView name, out UhtPropertyType propertyType)
{
return Tables!.PropertyTypeTable.TryGet(name, out propertyType);
}
/// <summary>
/// Fetch the default sanitizer
/// </summary>
public UhtStructDefaultValue DefaultStructDefaultValue => Tables!.StructDefaultValueTable.Default;
/// <summary>
/// Return the structure default value associated with the given name
/// </summary>
/// <param name="name"></param>
/// <param name="structDefaultValue">Structure default value handler</param>
/// <returns></returns>
public bool TryGetStructDefaultValue(StringView name, out UhtStructDefaultValue structDefaultValue)
{
return Tables!.StructDefaultValueTable.TryGet(name, out structDefaultValue);
}
/// <summary>
/// Run UHT on the given manifest. Use the bHasError property to see if process was successful.
/// </summary>
/// <param name="manifestFilePath">Path to the manifest file</param>
/// <param name="arguments">Any command line arguments</param>
public void Run(string manifestFilePath, CommandLineArguments? arguments = null)
{
if (FileManager == null)
{
SignalError();
Logger.LogError("No file manager supplied, aborting.");
return;
}
if (Config == null)
{
SignalError();
Logger.LogError("No configuration supplied, aborting.");
return;
}
if (Tables == null)
{
SignalError();
Logger.LogError("No parsing tables supplied, aborting.");
return;
}
switch (ReferenceMode)
{
case UhtReferenceMode.None:
break;
case UhtReferenceMode.Reference:
if (String.IsNullOrEmpty(ReferenceDirectory))
{
Logger.LogError("WRITEREF requested but directory not set, ignoring");
ReferenceMode = UhtReferenceMode.None;
}
break;
case UhtReferenceMode.Verify:
if (String.IsNullOrEmpty(ReferenceDirectory) || String.IsNullOrEmpty(VerifyDirectory))
{
Logger.LogError("VERIFYREF requested but directories not set, ignoring");
ReferenceMode = UhtReferenceMode.None;
}
break;
}
{
string defaultPolicyName = Config.DefaultDocumentationPolicy;
if (!String.IsNullOrEmpty(defaultPolicyName))
{
if (!Config.DocumentationPolicies.TryGetValue(defaultPolicyName, out UhtDocumentationPolicy? policy))
{
Logger.LogError("The default documentation policy '{DefaultPolicyName}' isn't known", defaultPolicyName);
return;
}
}
}
if (ReferenceMode != UhtReferenceMode.None)
{
string directoryToDelete = ReferenceMode == UhtReferenceMode.Reference ? ReferenceDirectory : VerifyDirectory;
_referenceDeleteTask = Task.Factory.StartNew(() =>
{
try
{
Directory.Delete(directoryToDelete, true);
}
catch (Exception)
{ }
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
}
StepReadManifestFile(manifestFilePath);
if (arguments != null)
{
foreach (UhtExporter Exporter in ExporterTable)
{
if (arguments.HasOption($"-{Exporter.Name}"))
{
SetExporterStatus(Exporter.Name, true);
}
else if (arguments.HasOption($"-no{Exporter.Name}"))
{
SetExporterStatus(Exporter.Name, false);
}
}
}
StepPrepareModules();
StepPrepareHeaders();
StepParseHeaders();
StepPopulateTypeTable();
StepResolveInvalidCheck();
StepBindSuperAndBases();
RecursiveStructCheck();
StepResolveBases();
StepResolveProperties();
StepResolveFinal();
StepResolveValidate();
StepCollectReferences();
TopologicalSortHeaderFiles();
// If we are deleting the reference directory, then wait for that task to complete
if (_referenceDeleteTask != null)
{
Logger.LogTrace("Step - Waiting for reference output to be cleared.");
_referenceDeleteTask.Wait();
}
Logger.LogTrace("Step - Starting exporters.");
StepExport();
}
/// <summary>
/// Try the given action. If an exception occurs that doesn't have the required
/// context, use the supplied context to generate the message. If a previous error
/// has occured, the action will not be executed
/// </summary>
/// <param name="messageSource">Message context for when the exception doesn't contain a context.</param>
/// <param name="action">The lambda to be invoked</param>
public void Try(IUhtMessageSource? messageSource, Action action)
{
if (!HasErrors)
{
TryNoErrorCheck(messageSource, action);
}
}
/// <summary>
/// Try the given action. If an exception occurs that doesn't have the required
/// context, use the supplied context to generate the message.
/// </summary>
/// <param name="messageSource">Message context for when the exception doesn't contain a context.</param>
/// <param name="action">The lambda to be invoked</param>
public void TryNoErrorCheck(IUhtMessageSource? messageSource, Action action)
{
try
{
action();
}
catch (Exception e)
{
HandleException(messageSource, e);
}
}
/// <summary>
/// Execute the given action on all the headers
/// </summary>
/// <param name="action">Action to be executed</param>
/// <param name="allowGoWide">If true, go wide if enabled. Otherwise single threaded</param>
public void ForEachHeader(Action<UhtHeaderFile> action, bool allowGoWide)
{
if (!HasErrors)
{
ForEachHeaderNoErrorCheck(action, allowGoWide);
}
}
/// <summary>
/// Execute the given action on all the headers
/// </summary>
/// <param name="action">Action to be executed</param>
/// <param name="allowGoWide">If true, go wide if enabled. Otherwise single threaded</param>
public void ForEachHeaderNoErrorCheck(Action<UhtHeaderFile> action, bool allowGoWide)
{
if (GoWide && allowGoWide)
{
Parallel.ForEach(_headerFiles, headerFile =>
{
TryHeader(action, headerFile);
});
}
else
{
foreach (UhtHeaderFile headerFile in _headerFiles)
{
TryHeader(action, headerFile);
}
}
}
/// <summary>
/// Read the given source file
/// </summary>
/// <param name="filePath">Full or relative file path</param>
/// <returns>Information about the read source</returns>
public UhtSourceFragment ReadSource(string filePath)
{
if (FileManager!.ReadSource(filePath, out UhtSourceFragment fragment))
{
return fragment;
}
throw new UhtException($"File not found '{filePath}'");
}
/// <summary>
/// Read the given source file
/// </summary>
/// <param name="filePath">Full or relative file path</param>
/// <returns>Buffer containing the read data or null if not found. The returned buffer must be returned to the cache via a call to UhtBuffer.Return</returns>
[Obsolete("Use the ReadSource method with the UhtPoolBuffer<char> output parameter")]
public UhtBuffer? ReadSourceToBuffer(string filePath)
{
return FileManager!.ReadOutput(filePath);
}
/// <summary>
/// Read the given source file
/// </summary>
/// <param name="filePath">Full or relative file path</param>
/// <param name="output">Read source file</param>
/// <returns>True if the file was read</returns>
public bool ReadSource(string filePath, out UhtPoolBuffer<char> output)
{
return FileManager!.ReadOutput(filePath, out output);
}
/// <summary>
/// Write the given contents to the file
/// </summary>
/// <param name="filePath">Path to write to</param>
/// <param name="contents">Contents to write</param>
/// <returns>True if the source was written</returns>
internal bool WriteSource(string filePath, ReadOnlySpan<char> contents)
{
return FileManager!.WriteOutput(filePath, contents);
}
/// <summary>
/// Rename the given file
/// </summary>
/// <param name="oldFilePath">Old file path name</param>
/// <param name="newFilePath">New file path name</param>
public void RenameSource(string oldFilePath, string newFilePath)
{
if (!FileManager!.RenameOutput(oldFilePath, newFilePath))
{
new UhtSimpleFileMessageSite(this, newFilePath).LogError($"Failed to rename exported file: '{oldFilePath}'");
}
}
/// <summary>
/// Given the name of a regular enum value, return the enum type
/// </summary>
/// <param name="name">Enum value</param>
/// <returns>Associated regular enum type or null if not found or enum isn't a regular enum.</returns>
public UhtEnum? FindRegularEnumValue(string name)
{
//COMPATIBILITY-TODO - See comment below on a more rebust version of the enum lookup
//if (RegularEnumValueLookup.TryGetValue(name, out UhtEnum? enumObj))
//{
// return enumObj;
//}
if (_fullEnumValueLookup.TryGetValue(name, out EnumAndValue value))
{
if (value.Value != -1)
{
return value.Enum;
}
}
if (!name.Contains("::", StringComparison.Ordinal) && _shortEnumValueLookup.TryGetValue(name, out UhtEnum? enumObj))
{
return enumObj;
}
return null;
}
/// <summary>
/// Find the given type in the type hierarchy
/// </summary>
/// <param name="startingType">Starting point for searches</param>
/// <param name="options">Options controlling what is searched</param>
/// <param name="name">Name of the type.</param>
/// <param name="messageSite">If supplied, then a error message will be generated if the type can not be found</param>
/// <param name="lineNumber">Source code line number requesting the lookup.</param>
/// <returns>The located type of null if not found</returns>
public UhtType? FindType(UhtType? startingType, UhtFindOptions options, string name, IUhtMessageSite? messageSite = null, int lineNumber = -1)
{
ValidateFindOptions(options);
UhtType? type = FindTypeInternal(startingType, options, name);
if (type == null && messageSite != null)
{
FindTypeError(messageSite, lineNumber, options, name);
}
return type;
}
/// <summary>
/// Find the given type in the type hierarchy
/// </summary>
/// <param name="startingType">Starting point for searches</param>
/// <param name="options">Options controlling what is searched</param>
/// <param name="name">Name of the type.</param>
/// <param name="messageSite">If supplied, then a error message will be generated if the type can not be found</param>
/// <returns>The located type of null if not found</returns>
public UhtType? FindType(UhtType? startingType, UhtFindOptions options, ref UhtToken name, IUhtMessageSite? messageSite = null)
{
ValidateFindOptions(options);
UhtType? type = FindTypeInternal(startingType, options, name.Value.ToString());
if (type == null && messageSite != null)
{
FindTypeError(messageSite, name.InputLine, options, name.Value.ToString());
}
return type;
}
/// <summary>
/// Find the given type in the type hierarchy
/// </summary>
/// <param name="startingType">If specified, this represents the starting type to use when searching base/owner chain for a match</param>
/// <param name="options">Options controlling what is searched</param>
/// <param name="identifiers">Enumeration of identifiers.</param>
/// <param name="messageSite">If supplied, then a error message will be generated if the type can not be found</param>
/// <param name="lineNumber">Source code line number requesting the lookup.</param>
/// <returns>The located type of null if not found</returns>
public UhtType? FindType(UhtType? startingType, UhtFindOptions options, UhtTokenList identifiers, IUhtMessageSite? messageSite = null, int lineNumber = -1)
{
ValidateFindOptions(options);
if (identifiers.Next != null && identifiers.Next.Next != null)
{
if (messageSite != null)
{
//messageSite.LogError(lineNumber, "UnrealHeaderTool only supports C++ identifiers of two or less identifiers");
string fullIdentifier = identifiers.Join("::");
FindTypeError(messageSite, lineNumber, options, fullIdentifier);
return null;
}
}
UhtType? type;
if (identifiers.Next != null)
{
type = FindTypeTwoNamesInternal(startingType, options, identifiers.Token.Value.ToString(), identifiers.Next.Token.Value.ToString());
}
else
{
type = FindTypeInternal(startingType, options, identifiers.Token.Value.ToString());
}
if (type == null && messageSite != null)
{
string fullIdentifier = identifiers.Join("::");
FindTypeError(messageSite, lineNumber, options, fullIdentifier);
}
return type;
}
/// <summary>
/// Find the given type in the type hierarchy
/// </summary>
/// <param name="startingType">If specified, this represents the starting type to use when searching base/owner chain for a match</param>
/// <param name="options">Options controlling what is searched</param>
/// <param name="identifiers">Enumeration of identifiers.</param>
/// <param name="messageSite">If supplied, then a error message will be generated if the type can not be found</param>
/// <param name="lineNumber">Source code line number requesting the lookup.</param>
/// <returns>The located type of null if not found</returns>
public UhtType? FindType(UhtType? startingType, UhtFindOptions options, UhtToken[] identifiers, IUhtMessageSite? messageSite = null, int lineNumber = -1)
{
ValidateFindOptions(options);
if (identifiers.Length == 0)
{
throw new UhtIceException("Empty identifier array");
}
if (identifiers.Length > 2)
{
if (messageSite != null)
{
//messageSite.LogError(lineNumber, "UnrealHeaderTool only supports C++ identifiers of two or less identifiers");
string fullIdentifier = String.Join("::", identifiers);
FindTypeError(messageSite, lineNumber, options, fullIdentifier);
return null;
}
}
UhtType? type;
if (identifiers.Length == 0)
{
type = FindTypeTwoNamesInternal(startingType, options, identifiers[0].Value.ToString(), identifiers[1].Value.ToString());
}
else
{
type = FindTypeInternal(startingType, options, identifiers[0].Value.ToString());
}
if (type == null && messageSite != null)
{
string fullIdentifier = String.Join("::", identifiers);
FindTypeError(messageSite, lineNumber, options, fullIdentifier);
}
return type;
}
/// <summary>
/// Find the given type in the type hierarchy
/// </summary>
/// <param name="startingType">Starting point for searches</param>
/// <param name="options">Options controlling what is searched</param>
/// <param name="firstName">First name of the type.</param>
/// <param name="secondName">Second name used by delegates in classes and namespace enumerations</param>
/// <returns>The located type of null if not found</returns>
private UhtType? FindTypeTwoNamesInternal(UhtType? startingType, UhtFindOptions options, string firstName, string secondName)
{
// If we have two names
if (secondName.Length > 0)
{
if (options.HasAnyFlags(UhtFindOptions.DelegateFunction | UhtFindOptions.Enum))
{
UhtFindOptions subOptions = UhtFindOptions.NoParents | (options & ~UhtFindOptions.TypesMask) | (options & UhtFindOptions.Enum);
if (options.HasAnyFlags(UhtFindOptions.DelegateFunction))
{
subOptions |= UhtFindOptions.Class;
}
UhtType? type = FindTypeInternal(startingType, subOptions, firstName);
if (type == null)
{
return null;
}
if (type is UhtEnum)
{
return type;
}
if (type is UhtClass)
{
//TODO - Old UHT compatibility. In UWidget, it references USlateAccessibleWidgetData::FGetText. However, since UWidget has a FGetText, that
// is returned first.
UhtType? compatType = FindTypeInternal(startingType, UhtFindOptions.DelegateFunction | UhtFindOptions.NoParents | (options & ~UhtFindOptions.TypesMask), secondName);
if (compatType != null)
{
return compatType;
}
return FindTypeInternal(type, UhtFindOptions.DelegateFunction | UhtFindOptions.NoParents | (options & ~UhtFindOptions.TypesMask), secondName);
}
}
// We can't match anything at this point
return null;
}
// Perform the lookup for just a single name
return FindTypeInternal(startingType, options, firstName);
}
/// <summary>
/// Find the given type in the type hierarchy
/// </summary>
/// <param name="startingType">Starting point for searches</param>
/// <param name="options">Options controlling what is searched</param>
/// <param name="name">Name of the type.</param>
/// <returns>The located type of null if not found</returns>
public UhtType? FindTypeInternal(UhtType? startingType, UhtFindOptions options, string name)
{
if (options.HasAnyFlags(UhtFindOptions.EngineName))
{
if (options.HasAnyFlags(UhtFindOptions.CaseCompare))
{
return _engineNameSymbolTable.FindCasedType(startingType, options, name);
}
else
{
return _engineNameSymbolTable.FindCaselessType(startingType, options, name);
}
}
else if (options.HasAnyFlags(UhtFindOptions.SourceName))
{
if (options.HasAnyFlags(UhtFindOptions.CaselessCompare))
{
return _sourceNameSymbolTable.FindCaselessType(startingType, options, name);
}
else
{
return _sourceNameSymbolTable.FindCasedType(startingType, options, name);
}
}
else
{
throw new UhtIceException("Either EngineName or SourceName must be specified in the options");
}
}
/// <summary>
/// Verify that the options are valid. Will also check to make sure the symbol table has been populated.
/// </summary>
/// <param name="options">Find options</param>
private void ValidateFindOptions(UhtFindOptions options)
{
if (!options.HasAnyFlags(UhtFindOptions.EngineName | UhtFindOptions.SourceName))
{
throw new UhtIceException("Either EngineName or SourceName must be specified in the options");
}
if (options.HasAnyFlags(UhtFindOptions.CaseCompare) && options.HasAnyFlags(UhtFindOptions.CaselessCompare))
{
throw new UhtIceException("Both CaseCompare and CaselessCompare can't be specified as FindType options");
}
UhtFindOptions typeOptions = options & UhtFindOptions.TypesMask;
if (typeOptions == 0)
{
throw new UhtIceException("No type options specified");
}
if (!_symbolTablePopulated)
{
throw new UhtIceException("Symbol table has not been populated, don't call FindType until headers are parsed.");
}
}
/// <summary>
/// Generate an error message for when a given symbol wasn't found. The text will contain the list of types that the symbol must be
/// </summary>
/// <param name="messageSite">Destination for the message</param>
/// <param name="lineNumber">Line number generating the error</param>
/// <param name="options">Collection of required types</param>
/// <param name="name">The name of the symbol</param>
private static void FindTypeError(IUhtMessageSite messageSite, int lineNumber, UhtFindOptions options, string name)
{
List<string> types = new();
if (options.HasAnyFlags(UhtFindOptions.Enum))
{
types.Add("'enum'");
}
if (options.HasAnyFlags(UhtFindOptions.ScriptStruct))
{
types.Add("'struct'");
}
if (options.HasAnyFlags(UhtFindOptions.Class))
{
types.Add("'class'");
}
if (options.HasAnyFlags(UhtFindOptions.DelegateFunction))
{
types.Add("'delegate'");
}
if (options.HasAnyFlags(UhtFindOptions.Function))
{
types.Add("'function'");
}
if (options.HasAnyFlags(UhtFindOptions.Property))
{
types.Add("'property'");
}
messageSite.LogError(lineNumber, $"Unable to find {UhtUtilities.MergeTypeNames(types, "or")} with name '{name}'");
}
/// <summary>
/// Search for the given header file by just the file name
/// </summary>
/// <param name="name">Name to be found</param>
/// <returns></returns>
public UhtHeaderFile? FindHeaderFile(string name)
{
if (_headerFileDictionary.TryGetValue(name, out UhtHeaderFile? headerFile))
{
return headerFile;
}
return null;
}
#region IUHTMessageSource implementation
/// <summary>
/// Add a message to the collection of output messages
/// </summary>
/// <param name="message">Message being added</param>
public void AddMessage(UhtMessage message)
{
if (message.MessageType == UhtMessageType.Deprecation && !Config!.ShowDeprecations)
{
return;
}
lock (_messages)
{
_messages.Add(message);
// If we aren't caching messages and this is the first message,
// start a task to flush the messages.
if (!CacheMessages && _messageTask == null)
{
_messageTask = Task.Factory.StartNew(() => FlushMessages(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
}
}
switch (message.MessageType)
{
case UhtMessageType.Error:
case UhtMessageType.Ice:
SignalError();
break;
case UhtMessageType.Warning:
Interlocked.Increment(ref _warningCount);
break;
case UhtMessageType.Info:
case UhtMessageType.Trace:
case UhtMessageType.Deprecation:
break;
}
}
#endregion
/// <summary>
/// Register than an error has happened
/// </summary>
public void SignalError()
{
Interlocked.Increment(ref _errorCount);
}
/// <summary>
/// Log all the collected messages to the log/console. If messages aren't being
/// cached, then this just waits until the flush task has completed. If messages
/// are being cached, they are sorted by file name and line number to ensure the
/// output is stable.
/// </summary>
public void LogMessages()
{
Task? messageTask = null;
lock (_messages)
{
messageTask = _messageTask;
}
messageTask?.Wait();
foreach (UhtMessage message in FetchOrderedMessages())
{
LogMessage(message);
}
}
/// <summary>
/// Flush all pending messages to the logger
/// </summary>
private void FlushMessages()
{
while (true)
{
UhtMessage[]? messageArray = null;
lock (_messages)
{
messageArray = _messages.ToArray();
_messages.Clear();
if (messageArray.Length == 0)
{
_messageTask = null;
return;
}
}
foreach (UhtMessage message in messageArray)
{
LogMessage(message);
}
}
}
/// <summary>
/// Log the given message
/// </summary>
/// <param name="msg">The message to be logged</param>
private void LogMessage(UhtMessage msg)
{
LogLevel logLevel;
switch (msg.MessageType)
{
default:
case UhtMessageType.Error:
case UhtMessageType.Ice:
logLevel = LogLevel.Error;
break;
case UhtMessageType.Warning:
logLevel = LogLevel.Warning;
break;
case UhtMessageType.Deprecation:
logLevel = LogLevel.Information;
break;
case UhtMessageType.Info:
logLevel = LogLevel.Information;
break;
case UhtMessageType.Trace:
logLevel = LogLevel.Trace;
break;
}
GetMessageParts(msg, out string filePath, out int lineNumber, out string fragmentPath, out string severity, out string message);
(Logger??Log.Logger).Log(logLevel, KnownLogEvents.UHT, "{File}({Line}){FileFragment}: {Severity}: {Message}",
new FileReference(filePath),
new LogValue(LogValueType.LineNumber, lineNumber.ToString()),
fragmentPath,
new LogValue(LogValueType.Severity, severity),
new LogValue(LogValueType.Message, message));
}
/// <summary>
/// Return all of the messages into a list
/// </summary>
/// <returns>List of all the messages</returns>
public List<string> CollectMessages()
{
List<string> messages = new();
foreach (UhtMessage msg in FetchOrderedMessages())
{
GetMessageParts(msg, out string filePath, out int lineNumber, out string fragmentPath, out string severity, out string message);
messages.Add($"{filePath}({lineNumber}){fragmentPath}: {severity}: {message}");
}
return messages;
}
/// <summary>
/// Given an existing and a new instance, replace the given type in the symbol table.
/// This is used by the property resolution system to replace properties created during
/// the parsing phase that couldn't be resoled until after all headers are parsed.
/// </summary>
/// <param name="oldType"></param>
/// <param name="newType"></param>
public void ReplaceTypeInSymbolTable(UhtType oldType, UhtType newType)
{
_sourceNameSymbolTable.Replace(oldType, newType, oldType.SourceName);
if (oldType.EngineType.HasEngineName())
{
_engineNameSymbolTable.Replace(oldType, newType, oldType.EngineName);
}
}
/// <summary>
/// Hide the given type in the symbol table
/// </summary>
/// <param name="typeToHide"></param>
public void HideTypeInSymbolTable(UhtType typeToHide)
{
_sourceNameSymbolTable.Hide(typeToHide, typeToHide.SourceName);
if (typeToHide.EngineType.HasEngineName())
{
_engineNameSymbolTable.Hide(typeToHide, typeToHide.EngineName);
}
}
/// <summary>
/// Return an ordered enumeration of all messages.
/// </summary>
/// <returns>Enumerator</returns>
private IOrderedEnumerable<UhtMessage> FetchOrderedMessages()
{
List<UhtMessage> messages = new();
lock (_messages)
{
messages.AddRange(_messages);
_messages.Clear();
}
return messages.OrderBy(context => context.FilePath).ThenBy(context => context.LineNumber + context.MessageSource?.MessageFragmentLineNumber);
}
/// <summary>
/// Extract the part of a message
/// </summary>
/// <param name="msg">Message being consider</param>
/// <param name="filePath">Full path of file where error happened</param>
/// <param name="lineNumber">Line number where error happened</param>
/// <param name="fragmentPath">For multi-part files (test harness), the inner file</param>
/// <param name="severity">Severity of the error</param>
/// <param name="message">Text of the message</param>
private void GetMessageParts(UhtMessage msg, out string filePath, out int lineNumber, out string fragmentPath, out string severity, out string message)
{
fragmentPath = "";
lineNumber = msg.LineNumber;
if (msg.FilePath != null)
{
filePath = msg.FilePath;
}
else if (msg.MessageSource != null)
{
if (msg.MessageSource.MessageIsFragment)
{
if (RelativePathInLog)
{
filePath = msg.MessageSource.MessageFragmentFilePath;
}
else
{
filePath = msg.MessageSource.MessageFragmentFullFilePath;
}
fragmentPath = $"[{msg.MessageSource.MessageFilePath}]";
lineNumber += msg.MessageSource.MessageFragmentLineNumber;
}
else
{
if (RelativePathInLog)
{
filePath = msg.MessageSource.MessageFilePath;
}
else
{
filePath = msg.MessageSource.MessageFullFilePath;
}
}
}
else
{
filePath = "UnknownSource";
}
switch (msg.MessageType)
{
case UhtMessageType.Error:
severity = "Error";
break;
case UhtMessageType.Warning:
severity = "Warning";
break;
case UhtMessageType.Info:
severity = "Info";
break;
case UhtMessageType.Trace:
severity = "Trace";
break;
case UhtMessageType.Deprecation:
severity = "Deprecation";
break;
default:
case UhtMessageType.Ice:
severity = "Internal Compiler Error";
break;
}
message = msg.Message;
}
/// <summary>
/// Handle the given exception with the provided message context
/// </summary>
/// <param name="messageSource">Context for the exception. Required to handled all exceptions other than UHTException</param>
/// <param name="e">Exception being handled</param>
private void HandleException(IUhtMessageSource? messageSource, Exception e)
{
switch (e)
{
case UhtException uhtException:
UhtMessage message = uhtException.UhtMessage;
message.MessageSource ??= messageSource;
AddMessage(message);
break;
case JsonException jsonException:
AddMessage(UhtMessage.MakeMessage(UhtMessageType.Error, messageSource, null, (int)(jsonException.LineNumber + 1 ?? 1), jsonException.Message));
break;
default:
//Log.TraceInformation("{0}", E.StackTrace);
AddMessage(UhtMessage.MakeMessage(UhtMessageType.Ice, messageSource, null, 1, $"{e.GetType()} - {e.Message}"));
break;
}
}
[GeneratedRegex(@"^\#define\s+ENGINE_(?<Name>[A-Z]+)_VERSION\s+(?<Number>\d+)\s*$")]
private static partial Regex GetEngineVersionFragmentRegex();
private EngineVersion LoadEngineVersion()
{
if (EngineDirectory == null)
{
throw new UhtException("Can't parse engine version without an engine directory");
}
string versionHeader = Path.Combine(EngineDirectory, "Source", "Runtime", "Launch", "Resources", "Version.h");
Log.Logger.LogDebug("Reading engine version from file: {VersionHeader}", versionHeader);
string[] lines;
try
{
lines = File.ReadAllLines(versionHeader);
}
catch (Exception ex)
{
throw new UhtException(String.Format("Error reading engine version header '{0}': {1}", versionHeader, ex));
}
EngineVersion loadedVersion = new EngineVersion();
bool gotMajor = false, gotMinor = false, gotPatch = false;
Regex versionRegex = GetEngineVersionFragmentRegex();
foreach (string line in lines)
{
Match match = versionRegex.Match(line);
if (match.Success)
{
int matchNumber = Int32.Parse(match.Groups["Number"].Value);
switch (match.Groups["Name"].Value)
{
case "MAJOR":
loadedVersion.MajorVersion = matchNumber;
gotMajor = true;
break;
case "MINOR":
loadedVersion.MinorVersion = matchNumber;
gotMinor = true;
break;
case "PATCH":
loadedVersion.PatchVersion = matchNumber;
gotPatch = true;
break;
}
if (gotMajor && gotMinor && gotPatch)
{
break;
}
}
}
if (!gotMajor || !gotMinor || !gotPatch)
{
throw new UhtException(String.Format("Got incomplete engine version {0}", loadedVersion));
}
Log.Logger.LogDebug("Got engine version {EngineVersion}", loadedVersion);
return loadedVersion;
}
#region Run steps
private void StepReadManifestFile(string manifestFilePath)
{
ManifestFile = new UhtManifestFile(this, manifestFilePath);
Try(ManifestFile.MessageSource, () =>
{
Log.Logger.LogTrace("Step - Read Manifest File");
ManifestFile.Read();
if (Manifest != null && Tables != null)
{
Tables.AddPlugins(Manifest.UhtPlugins);
}
});
}
private void StepPrepareModules()
{
if (ManifestFile == null || ManifestFile.Manifest == null)
{
return;
}
Try(ManifestFile.MessageSource, () =>
{
Log.Logger.LogTrace("Step - Prepare Modules");
foreach (UHTManifest.Module manifestModule in ManifestFile.Manifest.Modules)
{
UhtModule module = new(this, manifestModule);
_modules.Add(module);
}
});
}
private void StepPrepareHeaders()
{
if (ManifestFile == null)
{
return;
}
Try(ManifestFile.MessageSource, () =>
{
Log.Logger.LogTrace("Step - Prepare Headers");
foreach (UhtModule module in Modules)
{
module.PrepareHeaders(headerFile =>
{
_headerFiles.Add(headerFile);
_headerFileDictionary.Add(Path.GetFileName(headerFile.FilePath), headerFile);
});
}
// Locate the NoExportTypes.h file and add it to every other header file
if (_headerFileDictionary.TryGetValue("NoExportTypes.h", out UhtHeaderFile? noExportTypes))
{
foreach (UhtHeaderFile headerFile in _headerFiles)
{
if (headerFile != noExportTypes)
{
headerFile.AddReferencedHeader(noExportTypes, UhtHeaderReferenceType.Passive);
}
}
}
});
}
private void StepParseHeaders()
{
if (HasErrors)
{
return;
}
Log.Logger.LogTrace("Step - Parse Headers");
ForEachHeader(headerFile =>
{
headerFile.Read();
UhtHeaderFileParser.Parse(headerFile);
}, true);
}
private void StepPopulateTypeTable()
{
Try(null, () =>
{
Log.Logger.LogTrace("Step - Populate symbol table");
_sourceNameSymbolTable = new UhtSymbolTable(TypeCount);
_engineNameSymbolTable = new UhtSymbolTable(TypeCount);
PopulateSymbolTable();
_uobject = (UhtClass?)FindType(null, UhtFindOptions.SourceName | UhtFindOptions.Class, "UObject");
_uclass = (UhtClass?)FindType(null, UhtFindOptions.SourceName | UhtFindOptions.Class, "UClass");
_uinterface = (UhtClass?)FindType(null, UhtFindOptions.SourceName | UhtFindOptions.Class, "UInterface");
_iinterface = (UhtClass?)FindType(null, UhtFindOptions.SourceName | UhtFindOptions.Class, "IInterface");
EVerseNativeCallResult = (UhtEnum?)FindType(null, UhtFindOptions.SourceName | UhtFindOptions.Enum, "EVerseNativeCallResult");
AActor = (UhtClass?)FindType(null, UhtFindOptions.SourceName | UhtFindOptions.Class, "AActor");
INotifyFieldValueChanged = (UhtClass?)FindType(null, UhtFindOptions.SourceName | UhtFindOptions.Class, "INotifyFieldValueChanged");
FInstancedStruct = (UhtScriptStruct?)FindType(null, UhtFindOptions.SourceName | UhtFindOptions.ScriptStruct, "FInstancedStruct");
FStateTreePropertyRef = (UhtScriptStruct?)FindType(null, UhtFindOptions.SourceName | UhtFindOptions.ScriptStruct, "FStateTreePropertyRef");
});
}
private void StepBindSuperAndBases()
{
Log.Logger.LogTrace("Step - Bind super and bases");
ForEachHeader(headerFile => headerFile.BindSuperAndBases(), true);
}
private void StepResolveBases()
{
Log.Logger.LogTrace("Step - Resolve bases");
ResolveAllHeaders(UhtResolvePhase.Bases);
}
private void StepResolveInvalidCheck()
{
Log.Logger.LogTrace("Step - Resolve invalid check");
ResolveAllHeaders(UhtResolvePhase.InvalidCheck);
}
private void StepResolveProperties()
{
Log.Logger.LogTrace("Step - Resolve properties");
ResolveAllHeaders(UhtResolvePhase.Properties);
}
private void StepResolveFinal()
{
Log.Logger.LogTrace("Step - Resolve final");
ResolveAllHeaders(UhtResolvePhase.Final);
}
private void StepResolveValidate()
{
Log.Logger.LogTrace("Step - Resolve validate");
ForEachHeader(headerFile => headerFile.Validate(UhtValidationOptions.None), true);
}
private void StepCollectReferences()
{
Log.Logger.LogTrace("Step - Collect references");
ForEachHeader(headerFile => headerFile.CollectReferences(), true);
}
private void ResolveAllHeaders(UhtResolvePhase resolvePhase)
{
ForEachHeader(headerFile => headerFile.Resolve(resolvePhase), resolvePhase.IsMultiThreadedResolvePhase());
if (resolvePhase == UhtResolvePhase.InvalidCheck)
{
foreach (UhtModule module in Modules)
{
foreach (UhtPackage package in module.Packages)
{
package.RemoveInvalidChildren();
}
}
}
}
private void TryHeader(Action<UhtHeaderFile> action, UhtHeaderFile headerFile)
{
try
{
action(headerFile);
}
catch (Exception e)
{
HandleException(headerFile.MessageSource, e);
}
}
#endregion
#region Symbol table initialization
private void PopulateSymbolTable()
{
foreach (UhtHeaderFile headerFile in _headerFiles)
{
foreach (UhtType child in headerFile.Children)
{
if (child.Outer is UhtPackage package)
{
package.AddChildDirectly(child);
}
else
{
throw new UhtIceException($"Type \"{child.SourceName}\" is a root level type but does not have a package outer");
}
AddTypeToSymbolTable(child);
}
}
_symbolTablePopulated = true;
}
private void AddTypeToSymbolTable(UhtType type)
{
UhtEngineType engineExtendedType = type.EngineType;
if (type is UhtEnum enumObj)
{
//COMPATIBILITY-TODO: We can get more reliable results by only adding regular enums to the table
// and then in the lookup code in the property system to look for the '::' and just lookup
// the raw enum name. In UHT we only care about the enum and not the value.
//
// The current algorithm has issues with two cases:
//
// EnumNamespaceName::EnumTypeName::Value - Where the enum type name is included with a namespace enum
// EnumName::Value - Where the value is defined in terms that can't be parsed. The -1 check causes it
// to be kicked out.
//if (Enum.CppForm == UhtEnumCppForm.Regular)
//{
// foreach (UhtEnumValue Value in Enum.EnumValues)
// {
// RegularEnumValueLookup.Add(Value.Name.ToString(), Enum);
// }
//}
bool addShortNames = enumObj.CppForm == UhtEnumCppForm.Namespaced || enumObj.CppForm == UhtEnumCppForm.EnumClass;
string checkName = $"{enumObj.SourceName}::";
foreach (UhtEnumValue value in enumObj.EnumValues)
{
if (!_fullEnumValueLookup.TryAdd(value.Name, new EnumAndValue { Enum = enumObj, Value = value.Value }))
{
//TODO - add a warning
}
if (addShortNames)
{
if (value.Name.StartsWith(checkName, StringComparison.Ordinal))
{
_shortEnumValueLookup.TryAdd(value.Name[checkName.Length..], enumObj);
}
}
}
}
if (engineExtendedType.FindOptions() != 0)
{
if (engineExtendedType.MustNotBeReserved())
{
if (s_reservedNames.Contains(type.EngineName))
{
type.HeaderFile.LogError(type.LineNumber, $"{engineExtendedType.CapitalizedText()} '{type.EngineName}' uses a reserved type name.");
}
}
if (engineExtendedType.HasEngineName() && engineExtendedType.MustBeUnique())
{
UhtType? existingType = _engineNameSymbolTable.FindCaselessType(null, engineExtendedType.MustBeUniqueFindOptions(), type.EngineName);
if (existingType != null)
{
type.HeaderFile.LogError(type.LineNumber,
$"{engineExtendedType.CapitalizedText()} '{type.SourceName}' shares engine name '{type.EngineName}' with " +
$"{existingType.EngineType.LowercaseText()} '{existingType.SourceName}' in {existingType.HeaderFile.FilePath}({existingType.LineNumber})");
}
}
_sourceNameSymbolTable.Add(type, type.SourceName);
if (engineExtendedType.HasEngineName())
{
_engineNameSymbolTable.Add(type, type.EngineName);
}
}
if (engineExtendedType.AddChildrenToSymbolTable())
{
foreach (UhtType child in type.Children)
{
AddTypeToSymbolTable(child);
}
}
}
#endregion
#region Topological testing of headers and structs
private enum TopologicalState
{
Unmarked,
Temporary,
Permanent,
}
private void TopologicalHeaderVisit(List<TopologicalState> states, UhtHeaderFile visit, List<UhtHeaderFile> headerStack)
{
headerStack.Add(visit);
switch (states[visit.HeaderFileTypeIndex])
{
case TopologicalState.Unmarked:
states[visit.HeaderFileTypeIndex] = TopologicalState.Temporary;
foreach (UhtHeaderFile referenced in visit.ReferencedHeadersNoLock)
{
TopologicalHeaderVisit(states, referenced, headerStack);
}
states[visit.HeaderFileTypeIndex] = TopologicalState.Permanent;
_sortedHeaderFiles.Add(visit);
break;
case TopologicalState.Temporary:
{
int index = headerStack.IndexOf(visit);
if (index == -1 || index == headerStack.Count - 1)
{
throw new UhtIceException("Error locating include file loop");
}
index++;
visit.LogError("Circular dependency detected:");
UhtHeaderFile previous = visit;
for (int loopIndex = index; loopIndex < headerStack.Count; loopIndex++)
{
UhtHeaderFile next = headerStack[loopIndex];
previous.LogError($"includes/requires '{next.FilePath}'");
previous = next;
}
}
break;
case TopologicalState.Permanent:
break;
default:
throw new UhtIceException("Unknown topological state");
}
headerStack.RemoveAt(headerStack.Count - 1);
}
private void TopologicalSortHeaderFiles()
{
Try(null, () =>
{
Log.Logger.LogTrace("Step - Topological Sort Header Files");
// Initialize a scratch table for topological states
_sortedHeaderFiles.Capacity = HeaderFileTypeCount;
List<TopologicalState> states = new(HeaderFileTypeCount);
for (int index = 0; index < HeaderFileTypeCount; ++index)
{
states.Add(TopologicalState.Unmarked);
}
List<UhtHeaderFile> headerStack = new(32); // arbitrary capacity
foreach (UhtHeaderFile headerFile in HeaderFiles)
{
if (states[headerFile.HeaderFileTypeIndex] == TopologicalState.Unmarked)
{
TopologicalHeaderVisit(states, headerFile, headerStack);
}
}
});
}
private void TopologicalStructVisitChildren(List<TopologicalState> states, UhtHeaderFile visit, List<UhtStruct> structStack)
{
foreach (UhtType child in visit.Children)
{
if (child is UhtStruct childStruct)
{
TopologicalStructVisit(states, childStruct, structStack);
}
}
}
private void TopologicalStructVisitChildren(List<TopologicalState> states, UhtType visit, List<UhtStruct> structStack)
{
foreach (UhtType child in visit.Children)
{
if (child is UhtStruct childStruct)
{
TopologicalStructVisit(states, childStruct, structStack);
}
}
}
private void TopologicalStructVisit(List<TopologicalState> states, UhtStruct visit, List<UhtStruct> structStack)
{
structStack.Add(visit);
switch (states[visit.ObjectTypeIndex])
{
case TopologicalState.Unmarked:
states[visit.ObjectTypeIndex] = TopologicalState.Temporary;
if (visit.Super != null)
{
TopologicalStructVisit(states, visit.Super, structStack);
}
foreach (UhtStruct baseStruct in visit.Bases)
{
TopologicalStructVisit(states, baseStruct, structStack);
}
if (visit is UhtClass classObj)
{
foreach (UhtStruct verseInterface in classObj.VerseInterfaces)
{
TopologicalStructVisit(states, verseInterface, structStack);
}
}
TopologicalStructVisitChildren(states, visit, structStack);
states[visit.ObjectTypeIndex] = TopologicalState.Permanent;
break;
case TopologicalState.Temporary:
{
int index = structStack.IndexOf(visit);
if (index == -1 || index == structStack.Count - 1)
{
throw new UhtIceException("Error locating struct loop");
}
index++;
visit.LogError("Recursive class/struct definition:");
UhtStruct previous = visit;
for (int loopIndex = index; loopIndex < structStack.Count; loopIndex++)
{
UhtStruct next = structStack[loopIndex];
previous.LogError($"'{previous.SourceName}' inherits '{next.SourceName}'");
previous = next;
}
}
break;
case TopologicalState.Permanent:
break;
default:
throw new UhtIceException("Unknown topological state");
}
structStack.RemoveAt(structStack.Count - 1);
}
private void RecursiveStructCheck()
{
Try(null, () =>
{
Log.Logger.LogTrace("Step - Check for recursive structs");
// Initialize a scratch table for topological states
List<TopologicalState> states = new(ObjectTypeCount);
for (int index = 0; index < ObjectTypeCount; ++index)
{
states.Add(TopologicalState.Unmarked);
}
List<UhtStruct> structStack = new(32); // arbitrary capacity
foreach (UhtHeaderFile headerFile in HeaderFiles)
{
TopologicalStructVisitChildren(states, headerFile, structStack);
}
});
}
#endregion
#region Validation helpers
private readonly HashSet<UhtScriptStruct> _scriptStructsValidForNet = new();
/// <summary>
/// Validate that the given referenced script structure is valid for network operations. If the structure
/// is valid, then the result will be cached. It not valid, errors will be generated each time the structure
/// is referenced.
/// </summary>
/// <param name="referencingProperty">The property referencing a structure</param>
/// <param name="referencedScriptStruct">The script structure being referenced</param>
/// <returns></returns>
public bool ValidateScriptStructOkForNet(UhtProperty referencingProperty, UhtScriptStruct referencedScriptStruct)
{
// Check for existing value
lock (_scriptStructsValidForNet)
{
if (_scriptStructsValidForNet.Contains(referencedScriptStruct))
{
return true;
}
}
bool isStructValid = true;
// Check the super chain structure
UhtScriptStruct? superScriptStruct = referencedScriptStruct.SuperScriptStruct;
if (superScriptStruct != null)
{
if (!ValidateScriptStructOkForNet(referencingProperty, superScriptStruct))
{
isStructValid = false;
}
}
// Check the structure properties
foreach (UhtProperty property in referencedScriptStruct.Properties)
{
if (!property.ValidateStructPropertyOkForNet(referencingProperty))
{
isStructValid = false;
break;
}
}
// Save the results
if (isStructValid)
{
lock (_scriptStructsValidForNet)
{
_scriptStructsValidForNet.Add(referencedScriptStruct);
}
}
return isStructValid;
}
#endregion
#region Exporting
/// <summary>
/// Enable/Disable an exporter. This overrides the default state of the exporter.
/// </summary>
/// <param name="name">Name of the exporter</param>
/// <param name="enabled">If true, the exporter is to be enabled</param>
public void SetExporterStatus(string name, bool enabled)
{
_exporterStates[name] = enabled;
}
/// <summary>
/// Test to see if the given exporter plugin is enabled.
/// </summary>
/// <param name="pluginName">Name of the plugin</param>
/// <param name="includeTargetCheck">If true, include a target check</param>
/// <returns>True if enabled</returns>
public bool IsPluginEnabled(string pluginName, bool includeTargetCheck)
{
if (_projectJson == null && ProjectDirectory != null && ProjectFile != null)
{
if (ReadSource(ProjectFile, out UhtPoolBuffer<char> contents))
{
_projectJson = JsonDocument.Parse(contents.Memory);
UhtPoolBuffers.Return<char>(contents);
}
}
if (_projectJson == null)
{
return false;
}
JsonObject rootObject = new(_projectJson.RootElement);
if (rootObject.TryGetObjectArrayField("Plugins", out JsonObject[]? plugins))
{
foreach (JsonObject plugin in plugins)
{
if (!plugin.TryGetStringField("Name", out string? testPluginName) || !String.Equals(pluginName, testPluginName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!plugin.TryGetBoolField("Enabled", out bool enabled) || !enabled)
{
return false;
}
if (includeTargetCheck && Manifest != null)
{
if (plugin.TryGetStringArrayField("TargetAllowList", out string[]? allowList))
{
if (allowList.Contains(Manifest.TargetName, StringComparer.OrdinalIgnoreCase))
{
return true;
}
}
if (plugin.TryGetStringArrayField("TargetDenyList", out string[]? denyList))
{
if (denyList.Contains(Manifest.TargetName, StringComparer.OrdinalIgnoreCase))
{
return false;
}
}
}
return true;
}
}
return false;
}
private void StepExport()
{
HashSet<string> externalDependencies = new();
long totalWrittenFiles = 0;
Try(null, () =>
{
Logger.LogDebug("Step - Exports");
foreach (UhtExporter exporter in ExporterTable)
{
if (!_exporterStates.TryGetValue(exporter.Name, out bool run))
{
run = Config!.IsExporterEnabled(exporter.Name) ||
(exporter.Options.HasAnyFlags(UhtExporterOptions.Default) && !NoDefaultExporters);
}
UHTManifest.Module? pluginModule = null;
if (!String.IsNullOrEmpty(exporter.ModuleName))
{
foreach (UHTManifest.Module module in Manifest!.Modules)
{
if (String.Equals(module.Name, exporter.ModuleName, StringComparison.OrdinalIgnoreCase))
{
pluginModule = module;
break;
}
}
if (pluginModule == null)
{
Logger.LogWarning("Exporter \"{ExporterName}\" skipped because module \"{ModuleName}\" was not found in manifest", exporter.Name, exporter.ModuleName);
continue;
}
}
if (run)
{
Logger.LogDebug(" Running exporter {ExporterName}", exporter.Name);
UhtExportFactory factory = new(this, pluginModule, exporter);
factory.Run();
foreach (UhtExportFactory.Output output in factory.Outputs)
{
if (output.Saved)
{
totalWrittenFiles++;
}
}
foreach (string dep in factory.ExternalDependencies)
{
externalDependencies.Add(dep);
}
}
else
{
Logger.LogDebug(" Exporter {ExporterName} skipped", exporter.Name);
}
}
// Save the collected external dependencies
if (!String.IsNullOrEmpty(Manifest!.ExternalDependenciesFile))
{
using StreamWriter output = new(Manifest!.ExternalDependenciesFile);
foreach (string dep in externalDependencies)
{
output.WriteLine(dep);
}
}
});
Logger.LogInformation("Total of {NumFiles} written", totalWrittenFiles);
}
#endregion
#region Other helper methods
/// <summary>
/// Test to see if the given property is an incomplete return type
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
public bool IsIncompleteReturn(UhtProperty? property)
{
return property is UhtEnumProperty enumProperty && enumProperty.Enum == EVerseNativeCallResult;
}
#endregion
}
}