243 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
		
		
			
		
	
	
			243 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
|  | using System; | ||
|  | using System.Collections.Generic; | ||
|  | using System.IO; | ||
|  | using System.Linq; | ||
|  | using System.Threading; | ||
|  | using EpicGames.UHT.Types; | ||
|  | 
 | ||
|  | namespace UnrealSharpScriptGenerator.Utilities; | ||
|  | 
 | ||
|  | public readonly struct ProjectDirInfo | ||
|  | { | ||
|  |     private readonly string _projectName; | ||
|  |     private readonly string _projectDirectory; | ||
|  |     public HashSet<string>? Dependencies { get; } | ||
|  | 
 | ||
|  |     public ProjectDirInfo(string projectName, string projectDirectory, HashSet<string>? dependencies = null) | ||
|  |     { | ||
|  |         _projectName = projectName; | ||
|  |         _projectDirectory = projectDirectory; | ||
|  |         Dependencies = dependencies; | ||
|  |     } | ||
|  |      | ||
|  |     public string GlueProjectName => $"{_projectName}.Glue"; | ||
|  |     public string GlueProjectName_LEGACY => $"{_projectName}.PluginGlue"; | ||
|  |     public string GlueProjectFile => $"{GlueProjectName}.csproj"; | ||
|  |      | ||
|  |     public string ScriptDirectory => Path.Combine(_projectDirectory, "Script"); | ||
|  |      | ||
|  |     public string GlueCsProjPath => Path.Combine(GlueProjectDirectory, GlueProjectFile); | ||
|  | 
 | ||
|  |     public bool IsUProject => _projectDirectory.EndsWith(".uproject", StringComparison.OrdinalIgnoreCase); | ||
|  |      | ||
|  |     public bool IsPartOfEngine => _projectName == "Engine"; | ||
|  |      | ||
|  |     public string GlueProjectDirectory => Path.Combine(ScriptDirectory, GlueProjectName); | ||
|  |     public string GlueProjectDirectory_LEGACY => Path.Combine(ScriptDirectory, GlueProjectName_LEGACY); | ||
|  |      | ||
|  |     public string ProjectRoot => _projectDirectory; | ||
|  | } | ||
|  | 
 | ||
|  | public static class FileExporter | ||
|  | { | ||
|  |     private static readonly ReaderWriterLockSlim ReadWriteLock = new(); | ||
|  | 
 | ||
|  |     private static readonly List<string> ChangedFiles = new(); | ||
|  |     private static readonly List<string> UnchangedFiles = new(); | ||
|  | 
 | ||
|  |     public static void SaveGlueToDisk(UhtType type, GeneratorStringBuilder stringBuilder) | ||
|  |     { | ||
|  |         string directory = GetDirectoryPath(type.Package); | ||
|  |         SaveGlueToDisk(type.Package, directory, type.EngineName, stringBuilder.ToString()); | ||
|  |     } | ||
|  | 
 | ||
|  |     public static string GetFilePath(string typeName, string directory) | ||
|  |     { | ||
|  |         return Path.Combine(directory, $"{typeName}.generated.cs"); | ||
|  |     } | ||
|  | 
 | ||
|  |     public static void SaveGlueToDisk(UhtPackage package, string directory, string typeName, string text) | ||
|  |     { | ||
|  |         string absoluteFilePath = GetFilePath(typeName, directory); | ||
|  |         bool directoryExists = Directory.Exists(directory); | ||
|  |         bool glueExists = File.Exists(absoluteFilePath); | ||
|  | 
 | ||
|  |         ReadWriteLock.EnterWriteLock(); | ||
|  |         try | ||
|  |         { | ||
|  |             bool matchingGlue = glueExists && File.ReadAllText(absoluteFilePath) == text; | ||
|  | 
 | ||
|  |             // If the directory exists and the file exists with the same text, we can return early | ||
|  |             if (directoryExists && matchingGlue) | ||
|  |             { | ||
|  |                 UnchangedFiles.Add(absoluteFilePath); | ||
|  |                 return; | ||
|  |             } | ||
|  | 
 | ||
|  |             if (!directoryExists) | ||
|  |             { | ||
|  |                 Directory.CreateDirectory(directory); | ||
|  |             } | ||
|  | 
 | ||
|  |             File.WriteAllText(absoluteFilePath, text); | ||
|  |             ChangedFiles.Add(absoluteFilePath); | ||
|  | 
 | ||
|  |             if (package.IsPartOfEngine()) | ||
|  |             { | ||
|  |                 CSharpExporter.HasModifiedEngineGlue = true; | ||
|  |             } | ||
|  |         } | ||
|  |         finally | ||
|  |         { | ||
|  |             ReadWriteLock.ExitWriteLock(); | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     public static void AddUnchangedType(UhtType type) | ||
|  |     { | ||
|  |         string directory = GetDirectoryPath(type.Package); | ||
|  |         string filePath = GetFilePath(type.EngineName, directory); | ||
|  |         UnchangedFiles.Add(filePath); | ||
|  | 
 | ||
|  |         if (type is UhtStruct uhtStruct && uhtStruct.Functions.Any(f => f.HasMetadata("ExtensionMethod"))) | ||
|  |         { | ||
|  |             UnchangedFiles.Add(GetFilePath($"{type.EngineName}_Extensions", directory)); | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     public static string GetDirectoryPath(UhtPackage package) | ||
|  |     { | ||
|  |         if (package == null) | ||
|  |         { | ||
|  |             throw new InvalidOperationException("Package is null"); | ||
|  |         } | ||
|  | 
 | ||
|  |         string rootPath = GetGluePath(package); | ||
|  |         return Path.Combine(rootPath, package.GetShortName()); | ||
|  |     } | ||
|  | 
 | ||
|  |     public static string GetGluePath(UhtPackage package) | ||
|  |     { | ||
|  |         ProjectDirInfo projectDirInfo = package.FindOrAddProjectInfo(); | ||
|  |         return projectDirInfo.GlueProjectDirectory; | ||
|  |     } | ||
|  | 
 | ||
|  |     public static void CleanOldExportedFiles() | ||
|  |     { | ||
|  |         Console.WriteLine("Cleaning up old generated C# glue files..."); | ||
|  |         CleanFilesInDirectories(Program.EngineGluePath); | ||
|  |         CleanFilesInDirectories(Program.ProjectGluePath_LEGACY, true); | ||
|  |          | ||
|  |         foreach (ProjectDirInfo pluginDirectory in Program.PluginDirs) | ||
|  |         { | ||
|  |             CleanFilesInDirectories(pluginDirectory.GlueProjectDirectory, true); | ||
|  |             CleanFilesInDirectories(pluginDirectory.GlueProjectDirectory_LEGACY, true); | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     public static void CleanModuleFolders() | ||
|  |     { | ||
|  |         CleanGeneratedFolder(Program.EngineGluePath); | ||
|  |         CleanGeneratedFolder(Program.ProjectGluePath_LEGACY); | ||
|  |          | ||
|  |         foreach (ProjectDirInfo pluginDirectory in Program.PluginDirs) | ||
|  |         { | ||
|  |             CleanGeneratedFolder(pluginDirectory.GlueProjectDirectory); | ||
|  |             CleanGeneratedFolder(pluginDirectory.GlueProjectDirectory_LEGACY); | ||
|  |         } | ||
|  |     } | ||
|  |      | ||
|  |     public static void CleanGeneratedFolder(string path) | ||
|  |     { | ||
|  |         if (!Directory.Exists(path)) | ||
|  |         { | ||
|  |             return; | ||
|  |         } | ||
|  | 
 | ||
|  |         HashSet<string> ignoredDirectories = GetIgnoredDirectories(path); | ||
|  | 
 | ||
|  |         // TODO: Move runtime glue to a separate csproj. So we can fully clean the ProjectGlue folder. | ||
|  |         // Below is a temporary solution to not delete runtime glue that can cause compilation errors on editor startup, | ||
|  |         // and avoid having to restore nuget packages. | ||
|  |         string[] directories = Directory.GetDirectories(path); | ||
|  |         foreach (string directory in directories) | ||
|  |         { | ||
|  |             if (IsIntermediateDirectory(directory) || ignoredDirectories.Contains(Path.GetRelativePath(path, directory))) | ||
|  |             { | ||
|  |                 continue; | ||
|  |             } | ||
|  | 
 | ||
|  |             Directory.Delete(directory, true); | ||
|  |         } | ||
|  |     } | ||
|  |     private static HashSet<string> GetIgnoredDirectories(string path) | ||
|  |     { | ||
|  |         string glueIgnoreFileName = Path.Combine(path, ".glueignore"); | ||
|  |         if (!File.Exists(glueIgnoreFileName)) | ||
|  |         { | ||
|  |             return new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
|  |         } | ||
|  | 
 | ||
|  |         HashSet<string> ignoredDirectories = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
|  |         using StreamReader fileInput = File.OpenText(glueIgnoreFileName); | ||
|  |         while (!fileInput.EndOfStream) | ||
|  |         { | ||
|  |             string? line = fileInput.ReadLine(); | ||
|  |             if (string.IsNullOrWhiteSpace(line)) continue; | ||
|  | 
 | ||
|  |             ignoredDirectories.Add(line.Trim()); | ||
|  |         } | ||
|  |         return ignoredDirectories; | ||
|  |     } | ||
|  | 
 | ||
|  |     private static void CleanFilesInDirectories(string path, bool recursive = false) | ||
|  |     { | ||
|  |         if (!Directory.Exists(path)) | ||
|  |         { | ||
|  |             return; | ||
|  |         } | ||
|  | 
 | ||
|  |         string[] directories = Directory.GetDirectories(path); | ||
|  |         HashSet<string> ignoredDirectories = GetIgnoredDirectories(path); | ||
|  | 
 | ||
|  |         foreach (var directory in directories) | ||
|  |         { | ||
|  |             if (ignoredDirectories.Contains(Path.GetRelativePath(path, directory))) | ||
|  |             { | ||
|  |                 continue; | ||
|  |             } | ||
|  | 
 | ||
|  |             string moduleName = Path.GetFileName(directory); | ||
|  |             if (!CSharpExporter.HasBeenExported(moduleName)) | ||
|  |             { | ||
|  |                 continue; | ||
|  |             } | ||
|  | 
 | ||
|  |             int removedFiles = 0; | ||
|  |             string[] files = Directory.GetFiles(directory); | ||
|  | 
 | ||
|  |             foreach (var file in files) | ||
|  |             { | ||
|  |                 if (ChangedFiles.Contains(file) || UnchangedFiles.Contains(file)) | ||
|  |                 { | ||
|  |                     continue; | ||
|  |                 } | ||
|  | 
 | ||
|  |                 File.Delete(file); | ||
|  |                 removedFiles++; | ||
|  |             } | ||
|  | 
 | ||
|  |             if (removedFiles == files.Length) | ||
|  |             { | ||
|  |                 Directory.Delete(directory, recursive); | ||
|  |             } | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     static bool IsIntermediateDirectory(string path) | ||
|  |     { | ||
|  |         string directoryName = Path.GetFileName(path); | ||
|  |         return directoryName is "obj" or "bin" or "Properties"; | ||
|  |     } | ||
|  | } |