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