Files

469 lines
16 KiB
C#
Raw Permalink Normal View History

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);
}
}
}