diff options
author | Ignacio Etcheverry <ignalfonsore@gmail.com> | 2020-03-18 17:40:04 +0100 |
---|---|---|
committer | Ignacio Etcheverry <ignalfonsore@gmail.com> | 2020-03-31 09:37:16 +0200 |
commit | 77dd06134504ce68cc79b879c4a28d76a2c975f8 (patch) | |
tree | a4551344d3914ce93cfc25590f3c7261a27d4ab5 /modules/mono/editor | |
parent | fa08437694d09f0d5ee594cb4fbe1ff72b074742 (diff) |
Mono/C#: Add iOS support
Right now, games only work on devices when exported with FullAOT+Interpreter.
There are some issues left that need to addressed for FullAOT alone. Right now,
it's giving issues with the Godot.NativeCalls static constructor.
Diffstat (limited to 'modules/mono/editor')
7 files changed, 815 insertions, 358 deletions
diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs new file mode 100755 index 0000000000..f1765f7e19 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs @@ -0,0 +1,618 @@ +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<string, string> 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<string, string>(); + + 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<string, string> 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<string, string> 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.OSX ? ".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.OSX) + { + 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<string, string> assemblies, string bclDir) + { + var cppCode = new StringBuilder(); + var aotModuleInfoSymbols = new List<string>(assemblies.Count); + + // {arch: paths} + var objFilePathsForiOSArch = architectures.ToDictionary(arch => arch, arch => new List<string>(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<string>() + { + "-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>(); + 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<string>() + { + "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<string>(); + 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 supress 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<string> 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<string>(); + var optimizerOptions = new List<string>(); + + 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<string, string> + { + ["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<string> options) => string.Join(",", options.Select(EscapeOption)); + + var runtimeArgs = new List<string>(); + + // 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<string> compilerArgs, string bclDir) + { + // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead + string CmdLineArgsToString(IEnumerable<string> 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<string> GetEnablediOSArchs(string[] features) + { + var iosArchs = new[] + { + "armv7", + "arm64" + }; + + return iosArchs.Where(features.Contains); + } + + private static IEnumerable<string> 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<string, string> + { + ["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.OSX: + { + Debug.Assert(bits == null || bits == "64"); + string arch = "x86_64"; + return $"{platform}-{arch}"; + } + case OS.Platforms.X11: + 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}"); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index 022005ad0b..d782d4e61b 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -29,15 +29,13 @@ namespace GodotTools.Export All = CJK | MidEast | Other | Rare | West } - private void AddI18NAssemblies(Godot.Collections.Dictionary<string, string> assemblies, string platform) + private void AddI18NAssemblies(Godot.Collections.Dictionary<string, string> assemblies, string bclDir) { - var codesets = (I18NCodesets) ProjectSettings.GetSetting("mono/export/i18n_codesets"); + var codesets = (I18NCodesets)ProjectSettings.GetSetting("mono/export/i18n_codesets"); if (codesets == I18NCodesets.None) return; - string bclDir = DeterminePlatformBclDir(platform) ?? typeof(object).Assembly.Location.GetBaseDir(); - void AddI18NAssembly(string name) => assemblies.Add(name, Path.Combine(bclDir, $"{name}.dll")); AddI18NAssembly("I18N"); @@ -73,6 +71,7 @@ namespace GodotTools.Export GlobalDef("mono/export/aot/enabled", false); GlobalDef("mono/export/aot/full_aot", false); + GlobalDef("mono/export/aot/use_interpreter", true); // --aot or --aot=opt1,opt2 (use 'mono --aot=help AuxAssembly.dll' to list AOT options) GlobalDef("mono/export/aot/extra_aot_options", new string[] { }); @@ -86,9 +85,11 @@ namespace GodotTools.Export private void AddFile(string srcPath, string dstPath, bool remap = false) { + // Add file to the PCK AddFile(dstPath.Replace("\\", "/"), File.ReadAllBytes(srcPath), remap); } + // With this method we can override how a file is exported in the PCK public override void _ExportFile(string path, string type, string[] features) { base._ExportFile(path, type, features); @@ -110,6 +111,8 @@ namespace GodotTools.Export // Sadly, Godot prints errors when adding an empty file (nothing goes wrong, it's just noise). // Because of this, we add a file which contains a line break. AddFile(path, System.Text.Encoding.UTF8.GetBytes("\n"), remap: false); + + // Tell the Godot exporter that we already took care of the file Skip(); } } @@ -167,12 +170,7 @@ namespace GodotTools.Export var dependencies = new Godot.Collections.Dictionary<string, string>(); - var projectDllName = (string)ProjectSettings.GetSetting("application/config/name"); - if (projectDllName.Empty()) - { - projectDllName = "UnnamedProject"; - } - + string projectDllName = GodotSharpEditor.ProjectAssemblyName; string projectDllSrcDir = Path.Combine(GodotSharpDirs.ResTempAssembliesBaseDir, buildConfig); string projectDllSrcPath = Path.Combine(projectDllSrcDir, $"{projectDllName}.dll"); @@ -189,10 +187,12 @@ namespace GodotTools.Export dependencies["Mono.Android"] = monoAndroidAssemblyPath; } + string bclDir = DeterminePlatformBclDir(platform); + var initialDependencies = dependencies.Duplicate(); - internal_GetExportedAssemblyDependencies(initialDependencies, buildConfig, DeterminePlatformBclDir(platform), dependencies); + internal_GetExportedAssemblyDependencies(initialDependencies, buildConfig, bclDir, dependencies); - AddI18NAssemblies(dependencies, platform); + AddI18NAssemblies(dependencies, bclDir); string outputDataDir = null; @@ -227,11 +227,34 @@ namespace GodotTools.Export } } - // AOT + // AOT compilation + bool aotEnabled = platform == OS.Platforms.iOS || (bool)ProjectSettings.GetSetting("mono/export/aot/enabled"); - if ((bool)ProjectSettings.GetSetting("mono/export/aot/enabled")) + if (aotEnabled) { - AotCompileDependencies(features, platform, isDebug, outputDir, outputDataDir, dependencies); + string aotToolchainPath = null; + + if (platform == OS.Platforms.Android) + aotToolchainPath = (string)ProjectSettings.GetSetting("mono/export/aot/android_toolchain_path"); + + if (aotToolchainPath == string.Empty) + aotToolchainPath = null; // Don't risk it being used as current working dir + + // TODO: LLVM settings are hard-coded and disabled for now + var aotOpts = new AotOptions + { + EnableLLVM = false, + LLVMOnly = false, + LLVMPath = "", + LLVMOutputPath = "", + FullAot = platform == OS.Platforms.iOS || (bool)(ProjectSettings.GetSetting("mono/export/aot/full_aot") ?? false), + UseInterpreter = (bool)ProjectSettings.GetSetting("mono/export/aot/use_interpreter"), + ExtraAotOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_aot_options") ?? new string[] { }, + ExtraOptimizerOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_optimizer_options") ?? new string[] { }, + ToolchainPath = aotToolchainPath + }; + + AotBuilder.CompileAssemblies(this, aotOpts, features, platform, isDebug, bclDir, outputDir, outputDataDir, dependencies); } } @@ -258,7 +281,8 @@ namespace GodotTools.Export { string target = isDebug ? "release_debug" : "release"; - // NOTE: Bits is ok for now as all platforms with a data directory have it, but that may change in the future. + // NOTE: Bits is ok for now as all platforms with a data directory only have one or two architectures. + // However, this may change in the future if we add arm linux or windows desktop templates. string bits = features.Contains("64") ? "64" : "32"; string TemplateDirName() => $"data.mono.{platform}.{bits}.{target}"; @@ -284,7 +308,7 @@ namespace GodotTools.Export if (!validTemplatePathFound) throw new FileNotFoundException("Data template directory not found", templateDirPath); - string outputDataDir = Path.Combine(outputDir, DataDirName); + string outputDataDir = Path.Combine(outputDir, DetermineDataDirNameForProject()); if (Directory.Exists(outputDataDir)) Directory.Delete(outputDataDir, recursive: true); // Clean first @@ -304,333 +328,10 @@ namespace GodotTools.Export return outputDataDir; } - private void AotCompileDependencies(string[] features, string platform, bool isDebug, string outputDir, string outputDataDir, IDictionary<string, string> dependencies) - { - // TODO: WASM - - string bclDir = DeterminePlatformBclDir(platform) ?? typeof(object).Assembly.Location.GetBaseDir(); - - string aotTempDir = Path.Combine(Path.GetTempPath(), $"godot-aot-{Process.GetCurrentProcess().Id}"); - - if (!Directory.Exists(aotTempDir)) - Directory.CreateDirectory(aotTempDir); - - var assemblies = new Dictionary<string, string>(); - - foreach (var dependency in dependencies) - { - 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 - assemblies.Add(assemblyName, assemblyPathInBcl); - } - else - { - string tempAssemblyPath = Path.Combine(aotTempDir, assemblyName + ".dll"); - File.Copy(assemblyPath, tempAssemblyPath); - assemblies.Add(assemblyName, tempAssemblyPath); - } - } - - foreach (var assembly in assemblies) - { - string assemblyName = assembly.Key; - string assemblyPath = assembly.Value; - - string sharedLibExtension = platform == OS.Platforms.Windows ? ".dll" : - platform == OS.Platforms.OSX ? ".dylib" : - platform == OS.Platforms.HTML5 ? ".wasm" : - ".so"; - - string outputFileName = assemblyName + ".dll" + sharedLibExtension; - - if (platform == OS.Platforms.Android) - { - // 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. - outputFileName = "lib-aot-" + outputFileName; - } - - string outputFilePath = null; - string tempOutputFilePath; - - switch (platform) - { - case OS.Platforms.OSX: - tempOutputFilePath = Path.Combine(aotTempDir, outputFileName); - break; - case OS.Platforms.Android: - tempOutputFilePath = Path.Combine(aotTempDir, "%%ANDROID_ABI%%", outputFileName); - break; - case OS.Platforms.HTML5: - tempOutputFilePath = Path.Combine(aotTempDir, outputFileName); - outputFilePath = Path.Combine(outputDir, outputFileName); - break; - default: - tempOutputFilePath = Path.Combine(aotTempDir, outputFileName); - outputFilePath = Path.Combine(outputDataDir, "Mono", platform == OS.Platforms.Windows ? "bin" : "lib", outputFileName); - break; - } - - var data = new Dictionary<string, string>(); - var enabledAndroidAbis = platform == OS.Platforms.Android ? GetEnabledAndroidAbis(features).ToArray() : null; - - if (platform == OS.Platforms.Android) - { - Debug.Assert(enabledAndroidAbis != null); - - foreach (var abi in enabledAndroidAbis) - { - data["abi"] = abi; - var outputFilePathForThisAbi = tempOutputFilePath.Replace("%%ANDROID_ABI%%", abi); - - AotCompileAssembly(platform, isDebug, data, assemblyPath, outputFilePathForThisAbi); - - AddSharedObject(outputFilePathForThisAbi, tags: new[] { abi }); - } - } - else - { - string bits = features.Contains("64") ? "64" : features.Contains("64") ? "32" : null; - - if (bits != null) - data["bits"] = bits; - - AotCompileAssembly(platform, isDebug, data, assemblyPath, tempOutputFilePath); - - if (platform == OS.Platforms.OSX) - { - AddSharedObject(tempOutputFilePath, tags: null); - } - else - { - Debug.Assert(outputFilePath != null); - File.Copy(tempOutputFilePath, outputFilePath); - } - } - } - } - - private static void AotCompileAssembly(string platform, bool isDebug, Dictionary<string, string> data, string assemblyPath, string outputFilePath) - { - // Make sure the output directory exists - Directory.CreateDirectory(outputFilePath.GetBaseDir()); - - string exeExt = OS.IsWindows ? ".exe" : string.Empty; - - string monoCrossDirName = DetermineMonoCrossDirName(platform, data); - string monoCrossRoot = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", monoCrossDirName); - string monoCrossBin = Path.Combine(monoCrossRoot, "bin"); - - string toolPrefix = DetermineToolPrefix(monoCrossBin); - string monoExeName = System.IO.File.Exists(Path.Combine(monoCrossBin, $"{toolPrefix}mono{exeExt}")) ? "mono" : "mono-sgen"; - - string compilerCommand = Path.Combine(monoCrossBin, $"{toolPrefix}{monoExeName}{exeExt}"); - - bool fullAot = (bool)ProjectSettings.GetSetting("mono/export/aot/full_aot"); - - string EscapeOption(string option) => option.Contains(',') ? $"\"{option}\"" : option; - string OptionsToString(IEnumerable<string> options) => string.Join(",", options.Select(EscapeOption)); - - var aotOptions = new List<string>(); - var optimizerOptions = new List<string>(); - - if (fullAot) - aotOptions.Add("full"); - - aotOptions.Add(isDebug ? "soft-debug" : "nodebug"); - - if (platform == OS.Platforms.Android) - { - string abi = data["abi"]; - - string androidToolchain = (string)ProjectSettings.GetSetting("mono/export/aot/android_toolchain_path"); - - 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<string, string> - { - ["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}"); - } - - aotOptions.Add($"outfile={outputFilePath}"); - - var extraAotOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_aot_options"); - var extraOptimizerOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_optimizer_options"); - - if (extraAotOptions.Length > 0) - aotOptions.AddRange(extraAotOptions); - - if (extraOptimizerOptions.Length > 0) - optimizerOptions.AddRange(extraOptimizerOptions); - - var compilerArgs = new List<string>(); - - if (isDebug) - compilerArgs.Add("--debug"); // Required for --aot=soft-debug - - compilerArgs.Add(aotOptions.Count > 0 ? $"--aot={OptionsToString(aotOptions)}" : "--aot"); - - if (optimizerOptions.Count > 0) - compilerArgs.Add($"-O={OptionsToString(optimizerOptions)}"); - - compilerArgs.Add(ProjectSettings.GlobalizePath(assemblyPath)); - - // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead - string CmdLineArgsToString(IEnumerable<string> 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(compilerCommand, CmdLineArgsToString(compilerArgs)) - { - UseShellExecute = false - }; - - string platformBclDir = DeterminePlatformBclDir(platform); - process.StartInfo.EnvironmentVariables.Add("MONO_PATH", string.IsNullOrEmpty(platformBclDir) ? - typeof(object).Assembly.Location.GetBaseDir() : - platformBclDir); - - 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 error code: {process.ExitCode}"); - - if (!System.IO.File.Exists(outputFilePath)) - throw new Exception("Mono AOT compiler finished successfully but the output file is missing"); - } - } - - private static string DetermineMonoCrossDirName(string platform, IReadOnlyDictionary<string, string> data) - { - switch (platform) - { - case OS.Platforms.Windows: - case OS.Platforms.UWP: - { - string arch = data["bits"] == "64" ? "x86_64" : "i686"; - return $"windows-{arch}"; - } - case OS.Platforms.OSX: - { - string arch = "x86_64"; - return $"{platform}-{arch}"; - } - case OS.Platforms.X11: - case OS.Platforms.Server: - { - string arch = data["bits"] == "64" ? "x86_64" : "i686"; - return $"linux-{arch}"; - } - case OS.Platforms.Haiku: - { - string arch = data["bits"] == "64" ? "x86_64" : "i686"; - return $"{platform}-{arch}"; - } - case OS.Platforms.Android: - { - string abi = data["abi"]; - return $"{platform}-{abi}"; - } - case OS.Platforms.HTML5: - return "wasm-wasm32"; - default: - throw new NotSupportedException($"Platform not supported: {platform}"); - } - } - - private static string DetermineToolPrefix(string monoCrossBin) - { - string exeExt = OS.IsWindows ? ".exe" : string.Empty; - - if (System.IO.File.Exists(Path.Combine(monoCrossBin, $"mono{exeExt}"))) - return string.Empty; - - if (System.IO.File.Exists(Path.Combine(monoCrossBin, $"mono-sgen{exeExt}" + exeExt))) - return string.Empty; - - var files = new DirectoryInfo(monoCrossBin).GetFiles($"*mono{exeExt}" + exeExt, SearchOption.TopDirectoryOnly); - if (files.Length > 0) - { - string fileName = files[0].Name; - return fileName.Substring(0, fileName.Length - $"mono{exeExt}".Length); - } - - files = new DirectoryInfo(monoCrossBin).GetFiles($"*mono-sgen{exeExt}" + exeExt, SearchOption.TopDirectoryOnly); - if (files.Length > 0) - { - string fileName = files[0].Name; - return fileName.Substring(0, fileName.Length - $"mono-sgen{exeExt}".Length); - } - - throw new FileNotFoundException($"Cannot find the mono runtime executable in {monoCrossBin}"); - } - - private static IEnumerable<string> 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<string, string> - { - ["armeabi-v7a"] = "armv7", - ["arm64-v8a"] = "aarch64-v8a", - ["x86"] = "i686", - ["x86_64"] = "x86_64" - }; - - string arch = abiArchs[abi]; - - return $"{arch}-linux-android"; - } - private static bool PlatformHasTemplateDir(string platform) { // OSX export templates are contained in a zip, so we place our custom template inside it and let Godot do the rest. - return !new[] { OS.Platforms.OSX, OS.Platforms.Android, OS.Platforms.HTML5 }.Contains(platform); + return !new[] { OS.Platforms.OSX, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform); } private static string DeterminePlatformFromFeatures(IEnumerable<string> features) @@ -665,7 +366,7 @@ namespace GodotTools.Export if (PlatformRequiresCustomBcl(platform)) throw new FileNotFoundException($"Missing BCL (Base Class Library) for platform: {platform}"); - platformBclDir = null; // Use the one we're running on + platformBclDir = typeof(object).Assembly.Location; // Use the one we're running on } } @@ -678,7 +379,7 @@ namespace GodotTools.Export /// </summary> private static bool PlatformRequiresCustomBcl(string platform) { - if (new[] { OS.Platforms.Android, OS.Platforms.HTML5 }.Contains(platform)) + if (new[] { OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform)) return true; // The 'net_4_x' BCL is not compatible between Windows and the other platforms. @@ -707,6 +408,8 @@ namespace GodotTools.Export return "net_4_x"; case OS.Platforms.Android: return "monodroid"; + case OS.Platforms.iOS: + return "monotouch"; case OS.Platforms.HTML5: return "wasm"; default: @@ -714,14 +417,11 @@ namespace GodotTools.Export } } - private static string DataDirName + private static string DetermineDataDirNameForProject() { - get - { - var appName = (string)ProjectSettings.GetSetting("application/config/name"); - string appNameSafe = appName.ToSafeDirName(allowDirSeparator: false); - return $"data_{appNameSafe}"; - } + var appName = (string)ProjectSettings.GetSetting("application/config/name"); + string appNameSafe = appName.ToSafeDirName(allowDirSeparator: false); + return $"data_{appNameSafe}"; } [MethodImpl(MethodImplOptions.InternalCall)] diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs b/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs new file mode 100755 index 0000000000..219b7a698a --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; + +namespace GodotTools.Export +{ + public static class XcodeHelper + { + private static string _XcodePath = null; + + public static string XcodePath + { + get + { + if (_XcodePath == null) + { + _XcodePath = FindXcode(); + + if (_XcodePath == null) + throw new Exception("Could not find Xcode"); + } + + return _XcodePath; + } + } + + private static string FindSelectedXcode() + { + var outputWrapper = new Godot.Collections.Array(); + + int exitCode = Godot.OS.Execute("xcode-select", new string[] { "--print-path" }, blocking: true, output: outputWrapper); + + if (exitCode == 0) + { + string output = (string)outputWrapper[0]; + return output.Trim(); + } + + Console.Error.WriteLine($"'xcode-select --print-path' exited with code: {exitCode}"); + + return null; + } + + public static string FindXcode() + { + string selectedXcode = FindSelectedXcode(); + if (selectedXcode != null) + { + if (Directory.Exists(Path.Combine(selectedXcode, "Contents", "Developer"))) + return selectedXcode; + + // The path already pointed to Contents/Developer + var dirInfo = new DirectoryInfo(selectedXcode); + if (dirInfo.Name != "Developer" || dirInfo.Parent.Name != "Contents") + { + Console.WriteLine(Path.GetDirectoryName(selectedXcode)); + Console.WriteLine(System.IO.Directory.GetParent(selectedXcode).Name); + Console.Error.WriteLine("Unrecognized path for selected Xcode"); + } + else + { + return System.IO.Path.GetFullPath($"{selectedXcode}/../.."); + } + } + else + { + Console.Error.WriteLine("Could not find the selected Xcode; trying with a hint path"); + } + + const string XcodeHintPath = "/Applications/Xcode.app"; + + if (Directory.Exists(XcodeHintPath)) + { + if (Directory.Exists(Path.Combine(XcodeHintPath, "Contents", "Developer"))) + return XcodeHintPath; + + Console.Error.WriteLine($"Found Xcode at '{XcodeHintPath}' but it's missing the 'Contents/Developer' sub-directory"); + } + + return null; + } + + public static string FindXcodeTool(string toolName) + { + string XcodeDefaultToolchain = Path.Combine(XcodePath, "Contents", "Developer", "Toolchains", "XcodeDefault.xctoolchain"); + + string path = Path.Combine(XcodeDefaultToolchain, "usr", "bin", toolName); + if (File.Exists(path)) + return path; + + throw new FileNotFoundException($"Cannot find Xcode tool: {toolName}"); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index e6d5dd9895..afe7670165 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -36,6 +36,17 @@ namespace GodotTools public BottomPanel BottomPanel { get; private set; } + public static string ProjectAssemblyName + { + get + { + var projectAssemblyName = (string)ProjectSettings.GetSetting("application/config/name"); + if (string.IsNullOrEmpty(projectAssemblyName)) + projectAssemblyName = "UnnamedProject"; + return projectAssemblyName; + } + } + private bool CreateProjectSolution() { using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 3)) @@ -45,9 +56,7 @@ namespace GodotTools string resourceDir = ProjectSettings.GlobalizePath("res://"); string path = resourceDir; - string name = (string)ProjectSettings.GetSetting("application/config/name"); - if (name.Empty()) - name = "UnnamedProject"; + string name = ProjectAssemblyName; string guid = CsProjOperations.GenerateGameProject(path, name); diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index 379dfd9f7d..ac9379adf8 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -51,7 +51,9 @@ </ItemGroup> <ItemGroup> <Compile Include="Build\MsBuildFinder.cs" /> + <Compile Include="Export\AotBuilder.cs" /> <Compile Include="Export\ExportPlugin.cs" /> + <Compile Include="Export\XcodeHelper.cs" /> <Compile Include="ExternalEditorId.cs" /> <Compile Include="Ides\GodotIdeManager.cs" /> <Compile Include="Ides\GodotIdeServer.cs" /> diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index 11a4109d97..b057ac12c6 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using JetBrains.Annotations; namespace GodotTools.Utils { @@ -26,6 +27,7 @@ namespace GodotTools.Utils public const string UWP = "UWP"; public const string Haiku = "Haiku"; public const string Android = "Android"; + public const string iOS = "iOS"; public const string HTML5 = "HTML5"; } @@ -38,6 +40,7 @@ namespace GodotTools.Utils public const string UWP = "uwp"; public const string Haiku = "haiku"; public const string Android = "android"; + public const string iOS = "iphone"; public const string HTML5 = "javascript"; } @@ -50,6 +53,7 @@ namespace GodotTools.Utils [Names.UWP] = Platforms.UWP, [Names.Haiku] = Platforms.Haiku, [Names.Android] = Platforms.Android, + [Names.iOS] = Platforms.iOS, [Names.HTML5] = Platforms.HTML5 }; @@ -65,6 +69,7 @@ namespace GodotTools.Utils private static readonly Lazy<bool> _isUWP = new Lazy<bool>(() => IsOS(Names.UWP)); private static readonly Lazy<bool> _isHaiku = new Lazy<bool>(() => IsOS(Names.Haiku)); private static readonly Lazy<bool> _isAndroid = new Lazy<bool>(() => IsOS(Names.Android)); + private static readonly Lazy<bool> _isiOS = new Lazy<bool>(() => IsOS(Names.iOS)); private static readonly Lazy<bool> _isHTML5 = new Lazy<bool>(() => IsOS(Names.HTML5)); public static bool IsWindows => _isWindows.Value || IsUWP; @@ -74,10 +79,11 @@ namespace GodotTools.Utils public static bool IsUWP => _isUWP.Value; public static bool IsHaiku => _isHaiku.Value; public static bool IsAndroid => _isAndroid.Value; + public static bool IsiOS => _isiOS.Value; public static bool IsHTML5 => _isHTML5.Value; private static bool? _isUnixCache; - private static readonly string[] UnixLikePlatforms = { Names.OSX, Names.X11, Names.Server, Names.Haiku, Names.Android }; + private static readonly string[] UnixLikePlatforms = { Names.OSX, Names.X11, Names.Server, Names.Haiku, Names.Android, Names.iOS }; public static bool IsUnixLike() { @@ -91,12 +97,12 @@ namespace GodotTools.Utils public static char PathSep => IsWindows ? ';' : ':'; - public static string PathWhich(string name) + public static string PathWhich([NotNull] string name) { return IsWindows ? PathWhichWindows(name) : PathWhichUnix(name); } - private static string PathWhichWindows(string name) + private static string PathWhichWindows([NotNull] string name) { string[] windowsExts = Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) ?? new string[] { }; string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); @@ -121,7 +127,7 @@ namespace GodotTools.Utils select path + ext).FirstOrDefault(File.Exists); } - private static string PathWhichUnix(string name) + private static string PathWhichUnix([NotNull] string name) { string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); @@ -163,5 +169,33 @@ namespace GodotTools.Utils User32Dll.AllowSetForegroundWindow(process.Id); // allows application to focus itself } } + + public static int ExecuteCommand(string command, IEnumerable<string> arguments) + { + // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead + string CmdLineArgsToString(IEnumerable<string> args) + { + // Not perfect, but as long as we are careful... + return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg)); + } + + var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments)); + + Console.WriteLine($"Executing: \"{startInfo.FileName}\" {startInfo.Arguments}"); + + // Print the output + startInfo.RedirectStandardOutput = false; + startInfo.RedirectStandardError = false; + + startInfo.UseShellExecute = false; + + using (var process = new Process { StartInfo = startInfo }) + { + process.Start(); + process.WaitForExit(); + + return process.ExitCode; + } + } } } diff --git a/modules/mono/editor/godotsharp_export.cpp b/modules/mono/editor/godotsharp_export.cpp index ce0b6ad0e6..19b3bea5cf 100644 --- a/modules/mono/editor/godotsharp_export.cpp +++ b/modules/mono/editor/godotsharp_export.cpp @@ -92,7 +92,8 @@ Error get_assembly_dependencies(GDMonoAssembly *p_assembly, const Vector<String> ERR_FAIL_COND_V_MSG(!ref_assembly, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'."); - r_dependencies[ref_name] = ref_assembly->get_path(); + // Use the path we got from the search. Don't try to get the path from the loaded assembly as we can't trust it will be from the selected BCL dir. + r_dependencies[ref_name] = path; Error err = get_assembly_dependencies(ref_assembly, p_search_dirs, r_dependencies); ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot load one of the dependencies for the assembly: '" + ref_name + "'."); |