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