diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs index 90b7a682c5..d76b32d2a2 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs @@ -249,55 +249,72 @@ namespace GodotTools.Export var aotObjFilePaths = new List(assemblies.Count); - foreach (var assembly in assemblies) + string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", + $"{OS.Platforms.iOS}-arm64"); + string crossCompiler = FindCrossCompiler(compilerDirPath); + + string aotCacheDir = Path.Combine(ProjectSettings.GlobalizePath(GodotSharpDirs.ResTempDir), + "obj", isDebug ? "ExportDebug" : "ExportRelease", "godot-aot-cache"); + + if (!Directory.Exists(aotCacheDir)) + Directory.CreateDirectory(aotCacheDir); + + var aotCache = new AotCache(Path.Combine(aotCacheDir, "cache.json")); + + try { - string assemblyName = assembly.Key; - string assemblyPath = assembly.Value; - - string asmFileName = assemblyName + ".dll.S"; - string objFileName = assemblyName + ".dll.o"; - + foreach (var assembly in assemblies) { - string asmFilePath = Path.Combine(aotTempDir, asmFileName); + string assemblyName = assembly.Key; + string assemblyPath = assembly.Value; - var compilerArgs = GetAotCompilerArgs(OS.Platforms.iOS, isDebug, "arm64", aotOpts, assemblyPath, asmFilePath); + string asmFilePath = Path.Combine(aotCacheDir, assemblyName + ".dll.S"); + string objFilePath = Path.Combine(aotCacheDir, assemblyName + ".dll.o"); - string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", $"{OS.Platforms.iOS}-arm64"); - - ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir); - - // Assembling - const string iOSPlatformName = "iPhoneOS"; - const string versionMin = "10.0"; // TODO: Turn this hard-coded version into an exporter setting - string iOSSdkPath = Path.Combine(XcodeHelper.XcodePath, - $"Contents/Developer/Platforms/{iOSPlatformName}.platform/Developer/SDKs/{iOSPlatformName}.sdk"); - - string objFilePath = Path.Combine(aotTempDir, objFileName); - - var clangArgs = new List() + aotCache.RunCached(name: assemblyName, input: assemblyPath, output: objFilePath, () => { - "-isysroot", iOSSdkPath, - "-Qunused-arguments", - $"-miphoneos-version-min={versionMin}", - "-arch", "arm64", - "-c", - "-o", objFilePath, - "-x", "assembler" - }; + Console.WriteLine($"AOT compiler: Compiling '{assemblyName}'..."); - if (isDebug) - clangArgs.Add("-DDEBUG"); + var compilerArgs = GetAotCompilerArgs(OS.Platforms.iOS, isDebug, + "arm64", aotOpts, assemblyPath, asmFilePath); - clangArgs.Add(asmFilePath); + ExecuteCompiler(crossCompiler, compilerArgs, bclDir); - int clangExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("clang"), clangArgs); - if (clangExitCode != 0) - throw new Exception($"Command 'clang' exited with code: {clangExitCode}"); + // Assembling + const string iOSPlatformName = "iPhoneOS"; + const string versionMin = "10.0"; // TODO: Turn this hard-coded version into an exporter setting + string iOSSdkPath = Path.Combine(XcodeHelper.XcodePath, + $"Contents/Developer/Platforms/{iOSPlatformName}.platform/Developer/SDKs/{iOSPlatformName}.sdk"); + + var clangArgs = new List() + { + "-isysroot", iOSSdkPath, + "-Qunused-arguments", + $"-miphoneos-version-min={versionMin}", + "-arch", "arm64", + "-c", + "-o", objFilePath, + "-x", "assembler" + }; + + if (isDebug) + clangArgs.Add("-DDEBUG"); + + clangArgs.Add(asmFilePath); + + int clangExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("clang"), clangArgs); + if (clangExitCode != 0) + throw new Exception($"Command 'clang' exited with code: {clangExitCode}"); + }); aotObjFilePaths.Add(objFilePath); - } - aotModuleInfoSymbols.Add($"mono_aot_module_{AssemblyNameToAotSymbol(assemblyName)}_info"); + aotModuleInfoSymbols.Add($"mono_aot_module_{AssemblyNameToAotSymbol(assemblyName)}_info"); + } + } + finally + { + aotCache.SaveCache(); } RunAr(aotObjFilePaths, libAotFilePath); diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/AotCache.cs b/modules/mono/editor/GodotTools/GodotTools/Export/AotCache.cs new file mode 100644 index 0000000000..27402e0ec8 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Export/AotCache.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using Newtonsoft.Json; + +namespace GodotTools.Export +{ + public class AotCache + { + private readonly string _cacheFilePath; + private readonly Cache _cache = new Cache(); + private bool _hasUnsavedChanges = false; + + public AotCache(string cacheFilePath) + { + _cacheFilePath = cacheFilePath; + + if (File.Exists(_cacheFilePath)) + LoadCache(_cacheFilePath, out _cache); + } + + private static byte[] ComputeSha256Checksum(string filePath) + { + using (var sha256 = SHA256.Create()) + { + using (var streamReader = File.OpenRead(filePath)) + return sha256.ComputeHash(streamReader); + } + } + + private static bool CompareHashes(byte[] a, byte[] b) + { + if (a.Length != b.Length) + return false; + + for (int i = 0; i < a.Length; i++) + { + if (a[i] != b[i]) + return false; + } + + return true; + } + + private class Cache + { + [JsonProperty("assemblies")] + public Dictionary Assemblies { get; set; } = + new Dictionary(); + } + + private struct CachedChecksums + { + [JsonProperty("input_checksum")] public string InputChecksumBase64 { get; set; } + [JsonProperty("output_checksum")] public string OutputChecksumBase64 { get; set; } + } + + private static void LoadCache(string cacheFilePath, out Cache cache) + { + using (var streamReader = new StreamReader(cacheFilePath, Encoding.UTF8)) + using (var jsonReader = new JsonTextReader(streamReader)) + { + cache = new JsonSerializer().Deserialize(jsonReader); + } + } + + private static void SaveCache(string cacheFilePath, Cache cache) + { + using (var streamWriter = new StreamWriter(cacheFilePath, append: false, Encoding.UTF8)) + using (var jsonWriter = new JsonTextWriter(streamWriter)) + { + new JsonSerializer().Serialize(jsonWriter, cache); + } + } + + private bool TryGetCachedChecksums(string name, out CachedChecksums cachedChecksums) + => _cache.Assemblies.TryGetValue(name, out cachedChecksums); + + private void ChangeCache(string name, byte[] inputChecksum, byte[] outputChecksum) + { + _cache.Assemblies[name] = new CachedChecksums() + { + InputChecksumBase64 = Convert.ToBase64String(inputChecksum), + OutputChecksumBase64 = Convert.ToBase64String(outputChecksum) + }; + _hasUnsavedChanges = true; + } + + public void SaveCache() + { + if (!_hasUnsavedChanges) + return; + SaveCache(_cacheFilePath, _cache); + _hasUnsavedChanges = false; + } + + private bool IsCached(string name, byte[] inputChecksum, string output) + { + if (!File.Exists(output)) + { + return false; + } + + if (!TryGetCachedChecksums(name, out var cachedChecksums)) + return false; + + if (string.IsNullOrEmpty(cachedChecksums.InputChecksumBase64) || + string.IsNullOrEmpty(cachedChecksums.OutputChecksumBase64)) + return false; + + var cachedInputChecksum = Convert.FromBase64String(cachedChecksums.InputChecksumBase64); + + if (!CompareHashes(inputChecksum, cachedInputChecksum)) + return false; + + var outputChecksum = ComputeSha256Checksum(output); + var cachedOutputChecksum = Convert.FromBase64String(cachedChecksums.OutputChecksumBase64); + + if (!CompareHashes(outputChecksum, cachedOutputChecksum)) + return false; + + return true; + } + + public void RunCached(string name, string input, string output, Action action) + { + var inputChecksum = ComputeSha256Checksum(input); + + if (IsCached(name, inputChecksum, output)) + { + Console.WriteLine($"AOT compiler cache: '{name}' already compiled."); + return; + } + + action(); + + var outputChecksum = ComputeSha256Checksum(output); + + ChangeCache(name, inputChecksum, outputChecksum); + } + } +}