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