469 lines
16 KiB
C#
469 lines
16 KiB
C#
|
|
using System.Text.Json;
|
||
|
|
using Mono.Cecil;
|
||
|
|
using Mono.Cecil.Pdb;
|
||
|
|
using UnrealSharpWeaver.MetaData;
|
||
|
|
using UnrealSharpWeaver.TypeProcessors;
|
||
|
|
using UnrealSharpWeaver.Utilities;
|
||
|
|
|
||
|
|
namespace UnrealSharpWeaver;
|
||
|
|
|
||
|
|
public static class Program
|
||
|
|
{
|
||
|
|
public static WeaverOptions WeaverOptions { get; private set; } = null!;
|
||
|
|
|
||
|
|
public static void Weave(WeaverOptions weaverOptions)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
WeaverOptions = weaverOptions;
|
||
|
|
LoadBindingsAssembly();
|
||
|
|
ProcessUserAssemblies();
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
WeaverImporter.Shutdown();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public static int Main(string[] args)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
WeaverOptions = WeaverOptions.ParseArguments(args);
|
||
|
|
LoadBindingsAssembly();
|
||
|
|
ProcessUserAssemblies();
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
Console.Error.WriteLine(ex);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void LoadBindingsAssembly()
|
||
|
|
{
|
||
|
|
DefaultAssemblyResolver resolver = new DefaultAssemblyResolver();
|
||
|
|
|
||
|
|
List<string> searchPaths = new();
|
||
|
|
foreach (string assemblyPath in WeaverOptions.AssemblyPaths)
|
||
|
|
{
|
||
|
|
string? directory = Path.GetDirectoryName(StripQuotes(assemblyPath));
|
||
|
|
|
||
|
|
if (string.IsNullOrEmpty(directory) || searchPaths.Contains(directory))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!Directory.Exists(directory))
|
||
|
|
{
|
||
|
|
throw new InvalidOperationException("Could not determine directory for assembly path.");
|
||
|
|
}
|
||
|
|
|
||
|
|
resolver.AddSearchDirectory(directory);
|
||
|
|
searchPaths.Add(directory);
|
||
|
|
}
|
||
|
|
|
||
|
|
WeaverImporter.Instance.AssemblyResolver = resolver;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void ProcessUserAssemblies()
|
||
|
|
{
|
||
|
|
DirectoryInfo outputDirInfo = new DirectoryInfo(StripQuotes(WeaverOptions.OutputDirectory));
|
||
|
|
|
||
|
|
if (!outputDirInfo.Exists)
|
||
|
|
{
|
||
|
|
outputDirInfo.Create();
|
||
|
|
}
|
||
|
|
|
||
|
|
DefaultAssemblyResolver resolver = GetAssemblyResolver();
|
||
|
|
List<AssemblyDefinition> assembliesToProcess = LoadInputAssemblies(resolver);
|
||
|
|
ICollection<AssemblyDefinition> orderedUserAssemblies = OrderInputAssembliesByReferences(assembliesToProcess);
|
||
|
|
WeaverImporter.Instance.AllProjectAssemblies = assembliesToProcess;
|
||
|
|
|
||
|
|
WriteUnrealSharpMetadataFile(orderedUserAssemblies, outputDirInfo);
|
||
|
|
ProcessOrderedAssemblies(orderedUserAssemblies, outputDirInfo);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void WriteUnrealSharpMetadataFile(ICollection<AssemblyDefinition> orderedAssemblies, DirectoryInfo outputDirectory)
|
||
|
|
{
|
||
|
|
UnrealSharpMetadata unrealSharpMetadata = new UnrealSharpMetadata
|
||
|
|
{
|
||
|
|
AssemblyLoadingOrder = orderedAssemblies
|
||
|
|
.Select(x => Path.GetFileNameWithoutExtension(x.MainModule.FileName)).ToList(),
|
||
|
|
};
|
||
|
|
|
||
|
|
string metaDataContent = JsonSerializer.Serialize(unrealSharpMetadata, new JsonSerializerOptions
|
||
|
|
{
|
||
|
|
WriteIndented = false,
|
||
|
|
});
|
||
|
|
|
||
|
|
string fileName = Path.Combine(outputDirectory.FullName, "UnrealSharp.assemblyloadorder.json");
|
||
|
|
File.WriteAllText(fileName, metaDataContent);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void ProcessOrderedAssemblies(ICollection<AssemblyDefinition> assemblies, DirectoryInfo outputDirectory)
|
||
|
|
{
|
||
|
|
Exception? exception = null;
|
||
|
|
|
||
|
|
foreach (AssemblyDefinition assembly in assemblies)
|
||
|
|
{
|
||
|
|
if (assembly.Name.Name.EndsWith("Glue"))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
string outputPath = Path.Combine(outputDirectory.FullName, Path.GetFileName(assembly.MainModule.FileName));
|
||
|
|
StartWeavingAssembly(assembly, outputPath);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
exception = ex;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (AssemblyDefinition assembly in assemblies)
|
||
|
|
{
|
||
|
|
assembly.Dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (exception != null)
|
||
|
|
{
|
||
|
|
throw new AggregateException("Assembly processing failed", exception);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static ICollection<AssemblyDefinition> OrderInputAssembliesByReferences(ICollection<AssemblyDefinition> assemblies)
|
||
|
|
{
|
||
|
|
HashSet<string> assemblyNames = new HashSet<string>();
|
||
|
|
|
||
|
|
foreach (AssemblyDefinition assembly in assemblies)
|
||
|
|
{
|
||
|
|
assemblyNames.Add(assembly.FullName);
|
||
|
|
}
|
||
|
|
|
||
|
|
List<AssemblyDefinition> result = new List<AssemblyDefinition>(assemblies.Count);
|
||
|
|
HashSet<AssemblyDefinition> remaining = new HashSet<AssemblyDefinition>(assemblies);
|
||
|
|
|
||
|
|
// Add assemblies with no references first between the user assemblies.
|
||
|
|
foreach (AssemblyDefinition assembly in assemblies)
|
||
|
|
{
|
||
|
|
bool hasReferenceToUserAssembly = false;
|
||
|
|
foreach (AssemblyNameReference? reference in assembly.MainModule.AssemblyReferences)
|
||
|
|
{
|
||
|
|
if (!assemblyNames.Contains(reference.FullName))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
hasReferenceToUserAssembly = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (hasReferenceToUserAssembly)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
result.Add(assembly);
|
||
|
|
remaining.Remove(assembly);
|
||
|
|
}
|
||
|
|
|
||
|
|
do
|
||
|
|
{
|
||
|
|
bool added = false;
|
||
|
|
|
||
|
|
foreach (AssemblyDefinition assembly in assemblies)
|
||
|
|
{
|
||
|
|
if (!remaining.Contains(assembly))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool allResolved = true;
|
||
|
|
foreach (AssemblyNameReference? reference in assembly.MainModule.AssemblyReferences)
|
||
|
|
{
|
||
|
|
if (assemblyNames.Contains(reference.FullName))
|
||
|
|
{
|
||
|
|
bool found = false;
|
||
|
|
foreach (AssemblyDefinition addedAssembly in result)
|
||
|
|
{
|
||
|
|
if (addedAssembly.FullName != reference.FullName)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
found = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (found)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
allResolved = false;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!allResolved)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
result.Add(assembly);
|
||
|
|
remaining.Remove(assembly);
|
||
|
|
added = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (added || remaining.Count <= 0)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (AssemblyDefinition asm in remaining)
|
||
|
|
{
|
||
|
|
result.Add(asm);
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
|
||
|
|
} while (remaining.Count > 0);
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static DefaultAssemblyResolver GetAssemblyResolver()
|
||
|
|
{
|
||
|
|
return WeaverImporter.Instance.AssemblyResolver;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static List<AssemblyDefinition> LoadInputAssemblies(IAssemblyResolver resolver)
|
||
|
|
{
|
||
|
|
ReaderParameters readerParams = new ReaderParameters
|
||
|
|
{
|
||
|
|
AssemblyResolver = resolver,
|
||
|
|
ReadSymbols = true,
|
||
|
|
SymbolReaderProvider = new PdbReaderProvider(),
|
||
|
|
};
|
||
|
|
|
||
|
|
List<AssemblyDefinition> result = new List<AssemblyDefinition>();
|
||
|
|
|
||
|
|
foreach (var assemblyPath in WeaverOptions.AssemblyPaths.Select(StripQuotes))
|
||
|
|
{
|
||
|
|
if (!File.Exists(assemblyPath))
|
||
|
|
{
|
||
|
|
throw new FileNotFoundException($"Could not find assembly at: {assemblyPath}");
|
||
|
|
}
|
||
|
|
|
||
|
|
AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(assemblyPath, readerParams);
|
||
|
|
result.Add(assembly);
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string StripQuotes(string value)
|
||
|
|
{
|
||
|
|
if (value.StartsWith('\"') && value.EndsWith('\"'))
|
||
|
|
{
|
||
|
|
return value.Substring(1, value.Length - 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void StartWeavingAssembly(AssemblyDefinition assembly, string assemblyOutputPath)
|
||
|
|
{
|
||
|
|
void CleanOldFilesAndMoveExistingFiles()
|
||
|
|
{
|
||
|
|
var pdbOutputFile = new FileInfo(Path.ChangeExtension(assemblyOutputPath, ".pdb"));
|
||
|
|
|
||
|
|
if (!pdbOutputFile.Exists)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var tmpDirectory = Path.Join(Path.GetTempPath(), assembly.Name.Name);
|
||
|
|
if (Path.GetPathRoot(tmpDirectory) != Path.GetPathRoot(pdbOutputFile.FullName)) //if the temp directory is on a different drive, move will not work as desired if file is locked since it does a copy for drive boundaries
|
||
|
|
{
|
||
|
|
tmpDirectory = Path.Join(Path.GetDirectoryName(assemblyOutputPath), "..", "_Temporary", assembly.Name.Name);
|
||
|
|
}
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
if (Directory.Exists(tmpDirectory))
|
||
|
|
{
|
||
|
|
foreach (var file in Directory.GetFiles(tmpDirectory))
|
||
|
|
{
|
||
|
|
File.Delete(file);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Directory.CreateDirectory(tmpDirectory);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
//no action needed
|
||
|
|
}
|
||
|
|
|
||
|
|
//move the file to an temp folder to prevent write locks in case a debugger is attached to UE which locks the pdb for writes (common strategy).
|
||
|
|
var tmpDestFileName = Path.Join(tmpDirectory, Path.GetFileName(Path.ChangeExtension(Path.GetTempFileName(), ".pdb")));
|
||
|
|
File.Move(pdbOutputFile.FullName, tmpDestFileName);
|
||
|
|
}
|
||
|
|
|
||
|
|
Task cleanupTask = Task.Run(CleanOldFilesAndMoveExistingFiles);
|
||
|
|
WeaverImporter.Instance.ImportCommonTypes(assembly);
|
||
|
|
|
||
|
|
ApiMetaData assemblyMetaData = new ApiMetaData(assembly.Name.Name);
|
||
|
|
StartProcessingAssembly(assembly, assemblyMetaData);
|
||
|
|
|
||
|
|
string sourcePath = Path.GetDirectoryName(assembly.MainModule.FileName)!;
|
||
|
|
CopyAssemblyDependencies(assemblyOutputPath, sourcePath);
|
||
|
|
|
||
|
|
Task.WaitAll(cleanupTask);
|
||
|
|
assembly.Write(assemblyOutputPath, new WriterParameters
|
||
|
|
{
|
||
|
|
SymbolWriterProvider = new PdbWriterProvider(),
|
||
|
|
});
|
||
|
|
|
||
|
|
WriteAssemblyMetaDataFile(assemblyMetaData, assemblyOutputPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void WriteAssemblyMetaDataFile(ApiMetaData metadata, string outputPath)
|
||
|
|
{
|
||
|
|
string metaDataContent = JsonSerializer.Serialize(metadata, new JsonSerializerOptions
|
||
|
|
{
|
||
|
|
WriteIndented = false,
|
||
|
|
});
|
||
|
|
|
||
|
|
string metadataFilePath = Path.ChangeExtension(outputPath, "metadata.json");
|
||
|
|
File.WriteAllText(metadataFilePath, metaDataContent);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void StartProcessingAssembly(AssemblyDefinition userAssembly, ApiMetaData metadata)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
List<TypeDefinition> classes = [];
|
||
|
|
List<TypeDefinition> structs = [];
|
||
|
|
List<TypeDefinition> enums = [];
|
||
|
|
List<TypeDefinition> interfaces = [];
|
||
|
|
List<TypeDefinition> multicastDelegates = [];
|
||
|
|
List<TypeDefinition> delegates = [];
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
void RegisterType(List<TypeDefinition> typeDefinitions, TypeDefinition typeDefinition)
|
||
|
|
{
|
||
|
|
typeDefinitions.Add(typeDefinition);
|
||
|
|
typeDefinition.AddGeneratedTypeAttribute();
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (ModuleDefinition? module in userAssembly.Modules)
|
||
|
|
{
|
||
|
|
foreach (TypeDefinition? type in module.Types)
|
||
|
|
{
|
||
|
|
if (type.IsUClass())
|
||
|
|
{
|
||
|
|
RegisterType(classes, type);
|
||
|
|
}
|
||
|
|
else if (type.IsUEnum())
|
||
|
|
{
|
||
|
|
RegisterType(enums, type);
|
||
|
|
}
|
||
|
|
else if (type.IsUStruct())
|
||
|
|
{
|
||
|
|
RegisterType(structs, type);
|
||
|
|
}
|
||
|
|
else if (type.IsUInterface())
|
||
|
|
{
|
||
|
|
RegisterType(interfaces, type);
|
||
|
|
}
|
||
|
|
else if (type.BaseType != null && type.BaseType.FullName.Contains("UnrealSharp.MulticastDelegate"))
|
||
|
|
{
|
||
|
|
RegisterType(multicastDelegates, type);
|
||
|
|
}
|
||
|
|
else if (type.BaseType != null && type.BaseType.FullName.Contains("UnrealSharp.Delegate"))
|
||
|
|
{
|
||
|
|
RegisterType(delegates, type);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
Console.Error.WriteLine($"Error enumerating types: {ex.Message}");
|
||
|
|
throw;
|
||
|
|
}
|
||
|
|
|
||
|
|
UnrealEnumProcessor.ProcessEnums(enums, metadata);
|
||
|
|
UnrealInterfaceProcessor.ProcessInterfaces(interfaces, metadata);
|
||
|
|
UnrealStructProcessor.ProcessStructs(structs, metadata, userAssembly);
|
||
|
|
UnrealClassProcessor.ProcessClasses(classes, metadata);
|
||
|
|
UnrealDelegateProcessor.ProcessDelegates(delegates, multicastDelegates, userAssembly, metadata.DelegateMetaData);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
Console.Error.WriteLine($"Error during assembly processing: {ex.Message}");
|
||
|
|
throw;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void RecursiveFileCopy(DirectoryInfo sourceDirectory, DirectoryInfo destinationDirectory)
|
||
|
|
{
|
||
|
|
// Early out of our search if the last updated timestamps match
|
||
|
|
if (sourceDirectory.LastWriteTimeUtc == destinationDirectory.LastWriteTimeUtc) return;
|
||
|
|
|
||
|
|
if (!destinationDirectory.Exists)
|
||
|
|
{
|
||
|
|
destinationDirectory.Create();
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (FileInfo sourceFile in sourceDirectory.GetFiles())
|
||
|
|
{
|
||
|
|
string destinationFilePath = Path.Combine(destinationDirectory.FullName, sourceFile.Name);
|
||
|
|
FileInfo destinationFile = new FileInfo(destinationFilePath);
|
||
|
|
|
||
|
|
if (!destinationFile.Exists || sourceFile.LastWriteTimeUtc > destinationFile.LastWriteTimeUtc)
|
||
|
|
{
|
||
|
|
sourceFile.CopyTo(destinationFilePath, true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update our write time to match source for faster copying
|
||
|
|
destinationDirectory.LastWriteTimeUtc = sourceDirectory.LastWriteTimeUtc;
|
||
|
|
|
||
|
|
foreach (DirectoryInfo subSourceDirectory in sourceDirectory.GetDirectories())
|
||
|
|
{
|
||
|
|
string subDestinationDirectoryPath = Path.Combine(destinationDirectory.FullName, subSourceDirectory.Name);
|
||
|
|
DirectoryInfo subDestinationDirectory = new DirectoryInfo(subDestinationDirectoryPath);
|
||
|
|
|
||
|
|
RecursiveFileCopy(subSourceDirectory, subDestinationDirectory);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void CopyAssemblyDependencies(string destinationPath, string sourcePath)
|
||
|
|
{
|
||
|
|
var directoryName = Path.GetDirectoryName(destinationPath) ?? throw new InvalidOperationException("Assembly path does not have a valid directory.");
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var destinationDirectory = new DirectoryInfo(directoryName);
|
||
|
|
var sourceDirectory = new DirectoryInfo(sourcePath);
|
||
|
|
|
||
|
|
RecursiveFileCopy(sourceDirectory, destinationDirectory);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
ErrorEmitter.Error("WeaverError", sourcePath, 0, "Failed to copy dependencies: " + ex.Message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|