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? Dependencies { get; } public ProjectDirInfo(string projectName, string projectDirectory, HashSet? 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 ChangedFiles = new(); private static readonly List 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 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 GetIgnoredDirectories(string path) { string glueIgnoreFileName = Path.Combine(path, ".glueignore"); if (!File.Exists(glueIgnoreFileName)) { return new HashSet(StringComparer.OrdinalIgnoreCase); } HashSet ignoredDirectories = new HashSet(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 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"; } }