using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using GodotTools.Internals; using Directory = GodotTools.Utils.Directory; using File = GodotTools.Utils.File; using OS = GodotTools.Utils.OS; using Path = System.IO.Path; namespace GodotTools.Export { public struct AotOptions { public bool EnableLLVM; public bool LLVMOnly; public string LLVMPath; public string LLVMOutputPath; public bool FullAot; private bool _useInterpreter; public bool UseInterpreter { get => _useInterpreter && !LLVMOnly; set => _useInterpreter = value; } public string[] ExtraAotOptions; public string[] ExtraOptimizerOptions; public string ToolchainPath; } public static class AotBuilder { public static void CompileAssemblies(ExportPlugin exporter, AotOptions aotOpts, string[] features, string platform, bool isDebug, string bclDir, string outputDir, string outputDataDir, IDictionary assemblies) { // TODO: WASM string aotTempDir = Path.Combine(Path.GetTempPath(), $"godot-aot-{Process.GetCurrentProcess().Id}"); if (!Directory.Exists(aotTempDir)) Directory.CreateDirectory(aotTempDir); var assembliesPrepared = new Dictionary(); foreach (var dependency in assemblies) { string assemblyName = dependency.Key; string assemblyPath = dependency.Value; string assemblyPathInBcl = Path.Combine(bclDir, assemblyName + ".dll"); if (File.Exists(assemblyPathInBcl)) { // Don't create teporaries for assemblies from the BCL assembliesPrepared.Add(assemblyName, assemblyPathInBcl); } else { string tempAssemblyPath = Path.Combine(aotTempDir, assemblyName + ".dll"); File.Copy(assemblyPath, tempAssemblyPath); assembliesPrepared.Add(assemblyName, tempAssemblyPath); } } if (platform == OS.Platforms.iOS) { var architectures = GetEnablediOSArchs(features).ToArray(); CompileAssembliesForiOS(exporter, isDebug, architectures, aotOpts, aotTempDir, assembliesPrepared, bclDir); } else if (platform == OS.Platforms.Android) { var abis = GetEnabledAndroidAbis(features).ToArray(); CompileAssembliesForAndroid(exporter, isDebug, abis, aotOpts, aotTempDir, assembliesPrepared, bclDir); } else { string bits = features.Contains("64") ? "64" : features.Contains("32") ? "32" : null; CompileAssembliesForDesktop(exporter, platform, isDebug, bits, aotOpts, aotTempDir, outputDataDir, assembliesPrepared, bclDir); } } public static void CompileAssembliesForAndroid(ExportPlugin exporter, bool isDebug, string[] abis, AotOptions aotOpts, string aotTempDir, IDictionary assemblies, string bclDir) { foreach (var assembly in assemblies) { string assemblyName = assembly.Key; string assemblyPath = assembly.Value; // Not sure if the 'lib' prefix is an Android thing or just Godot being picky, // but we use '-aot-' as well just in case to avoid conflicts with other libs. string outputFileName = "lib-aot-" + assemblyName + ".dll.so"; foreach (string abi in abis) { string aotAbiTempDir = Path.Combine(aotTempDir, abi); string soFilePath = Path.Combine(aotAbiTempDir, outputFileName); var compilerArgs = GetAotCompilerArgs(OS.Platforms.Android, isDebug, abi, aotOpts, assemblyPath, soFilePath); // Make sure the output directory exists Directory.CreateDirectory(aotAbiTempDir); string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", $"{OS.Platforms.Android}-{abi}"); ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir); // The Godot exporter expects us to pass the abi in the tags parameter exporter.AddSharedObject(soFilePath, tags: new[] { abi }); } } } public static void CompileAssembliesForDesktop(ExportPlugin exporter, string platform, bool isDebug, string bits, AotOptions aotOpts, string aotTempDir, string outputDataDir, IDictionary assemblies, string bclDir) { foreach (var assembly in assemblies) { string assemblyName = assembly.Key; string assemblyPath = assembly.Value; string outputFileExtension = platform == OS.Platforms.Windows ? ".dll" : platform == OS.Platforms.MacOS ? ".dylib" : ".so"; string outputFileName = assemblyName + ".dll" + outputFileExtension; string tempOutputFilePath = Path.Combine(aotTempDir, outputFileName); var compilerArgs = GetAotCompilerArgs(platform, isDebug, bits, aotOpts, assemblyPath, tempOutputFilePath); string compilerDirPath = GetMonoCrossDesktopDirName(platform, bits); ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir); if (platform == OS.Platforms.MacOS) { exporter.AddSharedObject(tempOutputFilePath, tags: null); } else { string outputDataLibDir = Path.Combine(outputDataDir, "Mono", platform == OS.Platforms.Windows ? "bin" : "lib"); File.Copy(tempOutputFilePath, Path.Combine(outputDataLibDir, outputFileName)); } } } public static void CompileAssembliesForiOS(ExportPlugin exporter, bool isDebug, string[] architectures, AotOptions aotOpts, string aotTempDir, IDictionary assemblies, string bclDir) { var cppCode = new StringBuilder(); var aotModuleInfoSymbols = new List(assemblies.Count); // {arch: paths} var objFilePathsForiOSArch = architectures.ToDictionary(arch => arch, arch => new List(assemblies.Count)); foreach (var assembly in assemblies) { string assemblyName = assembly.Key; string assemblyPath = assembly.Value; string asmFileName = assemblyName + ".dll.S"; string objFileName = assemblyName + ".dll.o"; foreach (string arch in architectures) { string aotArchTempDir = Path.Combine(aotTempDir, arch); string asmFilePath = Path.Combine(aotArchTempDir, asmFileName); var compilerArgs = GetAotCompilerArgs(OS.Platforms.iOS, isDebug, arch, aotOpts, assemblyPath, asmFilePath); // Make sure the output directory exists Directory.CreateDirectory(aotArchTempDir); string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", $"{OS.Platforms.iOS}-{arch}"); ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir); // Assembling bool isSim = arch == "i386" || arch == "x86_64"; // Shouldn't really happen as we don't do AOT for the simulator string versionMinName = isSim ? "iphonesimulator" : "iphoneos"; string iOSPlatformName = isSim ? "iPhoneSimulator" : "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(aotArchTempDir, objFileName); var clangArgs = new List() { "-isysroot", iOSSdkPath, "-Qunused-arguments", $"-m{versionMinName}-version-min={versionMin}", "-arch", arch, "-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}"); objFilePathsForiOSArch[arch].Add(objFilePath); } aotModuleInfoSymbols.Add($"mono_aot_module_{AssemblyNameToAotSymbol(assemblyName)}_info"); } // Generate driver code cppCode.AppendLine("#if defined(__arm__) || defined(__arm64__) || defined(__aarch64__)"); cppCode.AppendLine("#define IOS_DEVICE"); cppCode.AppendLine("#endif"); cppCode.AppendLine("#ifdef IOS_DEVICE"); cppCode.AppendLine("extern \"C\" {"); cppCode.AppendLine("// Mono API"); cppCode.AppendLine(@" typedef enum { MONO_AOT_MODE_NONE, MONO_AOT_MODE_NORMAL, MONO_AOT_MODE_HYBRID, MONO_AOT_MODE_FULL, MONO_AOT_MODE_LLVMONLY, MONO_AOT_MODE_INTERP, MONO_AOT_MODE_INTERP_LLVMONLY, MONO_AOT_MODE_LLVMONLY_INTERP, MONO_AOT_MODE_LAST = 1000, } MonoAotMode;"); cppCode.AppendLine("void mono_jit_set_aot_mode(MonoAotMode);"); cppCode.AppendLine("void mono_aot_register_module(void *);"); if (aotOpts.UseInterpreter) { cppCode.AppendLine("void mono_ee_interp_init(const char *);"); cppCode.AppendLine("void mono_icall_table_init();"); cppCode.AppendLine("void mono_marshal_ilgen_init();"); cppCode.AppendLine("void mono_method_builder_ilgen_init();"); cppCode.AppendLine("void mono_sgen_mono_ilgen_init();"); } foreach (string symbol in aotModuleInfoSymbols) cppCode.AppendLine($"extern void *{symbol};"); cppCode.AppendLine("void gd_mono_setup_aot() {"); foreach (string symbol in aotModuleInfoSymbols) cppCode.AppendLine($"\tmono_aot_register_module({symbol});"); if (aotOpts.UseInterpreter) { cppCode.AppendLine("\tmono_icall_table_init();"); cppCode.AppendLine("\tmono_marshal_ilgen_init();"); cppCode.AppendLine("\tmono_method_builder_ilgen_init();"); cppCode.AppendLine("\tmono_sgen_mono_ilgen_init();"); cppCode.AppendLine("\tmono_ee_interp_init(0);"); } string aotModeStr = null; if (aotOpts.LLVMOnly) { aotModeStr = "MONO_AOT_MODE_LLVMONLY"; // --aot=llvmonly } else { if (aotOpts.UseInterpreter) aotModeStr = "MONO_AOT_MODE_INTERP"; // --aot=interp or --aot=interp,full else if (aotOpts.FullAot) aotModeStr = "MONO_AOT_MODE_FULL"; // --aot=full } // One of the options above is always set for iOS Debug.Assert(aotModeStr != null); cppCode.AppendLine($"\tmono_jit_set_aot_mode({aotModeStr});"); cppCode.AppendLine("} // gd_mono_setup_aot"); cppCode.AppendLine("} // extern \"C\""); cppCode.AppendLine("#endif // IOS_DEVICE"); // Add the driver code to the Xcode project exporter.AddIosCppCode(cppCode.ToString()); // Archive the AOT object files into a static library var arFilePathsForAllArchs = new List(); string projectAssemblyName = GodotSharpEditor.ProjectAssemblyName; foreach (var archPathsPair in objFilePathsForiOSArch) { string arch = archPathsPair.Key; var objFilePaths = archPathsPair.Value; string arOutputFilePath = Path.Combine(aotTempDir, $"lib-aot-{projectAssemblyName}.{arch}.a"); var arArgs = new List() { "cr", arOutputFilePath }; foreach (string objFilePath in objFilePaths) arArgs.Add(objFilePath); int arExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("ar"), arArgs); if (arExitCode != 0) throw new Exception($"Command 'ar' exited with code: {arExitCode}"); arFilePathsForAllArchs.Add(arOutputFilePath); } // It's lipo time string fatOutputFileName = $"lib-aot-{projectAssemblyName}.fat.a"; string fatOutputFilePath = Path.Combine(aotTempDir, fatOutputFileName); var lipoArgs = new List(); lipoArgs.Add("-create"); lipoArgs.AddRange(arFilePathsForAllArchs); lipoArgs.Add("-output"); lipoArgs.Add(fatOutputFilePath); int lipoExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("lipo"), lipoArgs); if (lipoExitCode != 0) throw new Exception($"Command 'lipo' exited with code: {lipoExitCode}"); // TODO: Add the AOT lib and interpreter libs as device only to suppress warnings when targeting the simulator // Add the fat AOT static library to the Xcode project exporter.AddIosProjectStaticLib(fatOutputFilePath); // Add the required Mono libraries to the Xcode project string MonoLibFile(string libFileName) => libFileName + ".iphone.fat.a"; string MonoLibFromTemplate(string libFileName) => Path.Combine(Internal.FullTemplatesDir, "iphone-mono-libs", MonoLibFile(libFileName)); exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmonosgen-2.0")); exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-native")); if (aotOpts.UseInterpreter) { exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-ee-interp")); exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-icall-table")); exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-ilgen")); } // TODO: Turn into an exporter option bool enableProfiling = false; if (enableProfiling) exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-profiler-log")); // Add frameworks required by Mono to the Xcode project exporter.AddIosFramework("libiconv.tbd"); exporter.AddIosFramework("GSS.framework"); exporter.AddIosFramework("CFNetwork.framework"); // Force load and export dynamic are needed for the linker to not strip required symbols. // In theory we shouldn't be relying on this for P/Invoked functions (as is the case with // functions in System.Native/libmono-native). Instead, we should use cecil to search for // DllImports in assemblies and pass them to 'ld' as '-u/--undefined {pinvoke_symbol}'. exporter.AddIosLinkerFlags("-rdynamic"); exporter.AddIosLinkerFlags($"-force_load \"$(SRCROOT)/{MonoLibFile("libmono-native")}\""); } /// Converts an assembly name to a valid symbol name in the same way the AOT compiler does private static string AssemblyNameToAotSymbol(string assemblyName) { var builder = new StringBuilder(); foreach (var charByte in Encoding.UTF8.GetBytes(assemblyName)) { char @char = (char)charByte; builder.Append(Char.IsLetterOrDigit(@char) || @char == '_' ? @char : '_'); } return builder.ToString(); } private static IEnumerable GetAotCompilerArgs(string platform, bool isDebug, string target, AotOptions aotOpts, string assemblyPath, string outputFilePath) { // TODO: LLVM bool aotSoftDebug = isDebug && !aotOpts.EnableLLVM; bool aotDwarfDebug = platform == OS.Platforms.iOS; var aotOptions = new List(); var optimizerOptions = new List(); if (aotOpts.LLVMOnly) { aotOptions.Add("llvmonly"); } else { // Can be both 'interp' and 'full' if (aotOpts.UseInterpreter) aotOptions.Add("interp"); if (aotOpts.FullAot) aotOptions.Add("full"); } aotOptions.Add(aotSoftDebug ? "soft-debug" : "nodebug"); if (aotDwarfDebug) aotOptions.Add("dwarfdebug"); if (platform == OS.Platforms.Android) { string abi = target; string androidToolchain = aotOpts.ToolchainPath; if (string.IsNullOrEmpty(androidToolchain)) { androidToolchain = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "android-toolchains", $"{abi}"); // TODO: $"{abi}-{apiLevel}{(clang?"clang":"")}" if (!Directory.Exists(androidToolchain)) throw new FileNotFoundException("Missing android toolchain. Specify one in the AOT export settings."); } else if (!Directory.Exists(androidToolchain)) { throw new FileNotFoundException("Android toolchain not found: " + androidToolchain); } var androidToolPrefixes = new Dictionary { ["armeabi-v7a"] = "arm-linux-androideabi-", ["arm64-v8a"] = "aarch64-linux-android-", ["x86"] = "i686-linux-android-", ["x86_64"] = "x86_64-linux-android-" }; aotOptions.Add("tool-prefix=" + Path.Combine(androidToolchain, "bin", androidToolPrefixes[abi])); string triple = GetAndroidTriple(abi); aotOptions.Add($"mtriple={triple}"); } else if (platform == OS.Platforms.iOS) { if (!aotOpts.LLVMOnly && !aotOpts.UseInterpreter) optimizerOptions.Add("gsharedvt"); aotOptions.Add("static"); // I couldn't get the Mono cross-compiler to do assembling, so we'll have to do it ourselves aotOptions.Add("asmonly"); aotOptions.Add("direct-icalls"); if (aotSoftDebug) aotOptions.Add("no-direct-calls"); if (aotOpts.LLVMOnly || !aotOpts.UseInterpreter) aotOptions.Add("direct-pinvoke"); string arch = target; aotOptions.Add($"mtriple={arch}-ios"); } aotOptions.Add($"outfile={outputFilePath}"); if (aotOpts.EnableLLVM) { aotOptions.Add($"llvm-path={aotOpts.LLVMPath}"); aotOptions.Add($"llvm-outfile={aotOpts.LLVMOutputPath}"); } if (aotOpts.ExtraAotOptions.Length > 0) aotOptions.AddRange(aotOpts.ExtraAotOptions); if (aotOpts.ExtraOptimizerOptions.Length > 0) optimizerOptions.AddRange(aotOpts.ExtraOptimizerOptions); string EscapeOption(string option) => option.Contains(',') ? $"\"{option}\"" : option; string OptionsToString(IEnumerable options) => string.Join(",", options.Select(EscapeOption)); var runtimeArgs = new List(); // The '--debug' runtime option is required when using the 'soft-debug' and 'dwarfdebug' AOT options if (aotSoftDebug || aotDwarfDebug) runtimeArgs.Add("--debug"); if (aotOpts.EnableLLVM) runtimeArgs.Add("--llvm"); runtimeArgs.Add(aotOptions.Count > 0 ? $"--aot={OptionsToString(aotOptions)}" : "--aot"); if (optimizerOptions.Count > 0) runtimeArgs.Add($"-O={OptionsToString(optimizerOptions)}"); runtimeArgs.Add(assemblyPath); return runtimeArgs; } private static void ExecuteCompiler(string compiler, IEnumerable compilerArgs, string bclDir) { // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead string CmdLineArgsToString(IEnumerable args) { // Not perfect, but as long as we are careful... return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg)); } using (var process = new Process()) { process.StartInfo = new ProcessStartInfo(compiler, CmdLineArgsToString(compilerArgs)) { UseShellExecute = false }; process.StartInfo.EnvironmentVariables.Remove("MONO_ENV_OPTIONS"); process.StartInfo.EnvironmentVariables.Remove("MONO_THREADS_SUSPEND"); process.StartInfo.EnvironmentVariables.Add("MONO_PATH", bclDir); Console.WriteLine($"Running: \"{process.StartInfo.FileName}\" {process.StartInfo.Arguments}"); if (!process.Start()) throw new Exception("Failed to start process for Mono AOT compiler"); process.WaitForExit(); if (process.ExitCode != 0) throw new Exception($"Mono AOT compiler exited with code: {process.ExitCode}"); } } private static IEnumerable GetEnablediOSArchs(string[] features) { var iosArchs = new[] { "armv7", "arm64" }; return iosArchs.Where(features.Contains); } private static IEnumerable GetEnabledAndroidAbis(string[] features) { var androidAbis = new[] { "armeabi-v7a", "arm64-v8a", "x86", "x86_64" }; return androidAbis.Where(features.Contains); } private static string GetAndroidTriple(string abi) { var abiArchs = new Dictionary { ["armeabi-v7a"] = "armv7", ["arm64-v8a"] = "aarch64-v8a", ["x86"] = "i686", ["x86_64"] = "x86_64" }; string arch = abiArchs[abi]; return $"{arch}-linux-android"; } private static string GetMonoCrossDesktopDirName(string platform, string bits) { switch (platform) { case OS.Platforms.Windows: case OS.Platforms.UWP: { string arch = bits == "64" ? "x86_64" : "i686"; return $"windows-{arch}"; } case OS.Platforms.MacOS: { Debug.Assert(bits == null || bits == "64"); string arch = "x86_64"; return $"{platform}-{arch}"; } case OS.Platforms.LinuxBSD: case OS.Platforms.Server: { string arch = bits == "64" ? "x86_64" : "i686"; return $"linux-{arch}"; } case OS.Platforms.Haiku: { string arch = bits == "64" ? "x86_64" : "i686"; return $"{platform}-{arch}"; } default: throw new NotSupportedException($"Platform not supported: {platform}"); } } // TODO: Replace this for a specific path for each platform private static string FindCrossCompiler(string monoCrossBin) { string exeExt = OS.IsWindows ? ".exe" : string.Empty; var files = new DirectoryInfo(monoCrossBin).GetFiles($"*mono-sgen{exeExt}", SearchOption.TopDirectoryOnly); if (files.Length > 0) return Path.Combine(monoCrossBin, files[0].Name); throw new FileNotFoundException($"Cannot find the mono runtime executable in {monoCrossBin}"); } } }