소스 검색

Mono/C#: Initial exporter support for AOT compilation

Ignacio Etcheverry 5 년 전
부모
커밋
2b67924a0b

+ 0 - 2
modules/mono/csharp_script.cpp

@@ -132,8 +132,6 @@ void CSharpLanguage::init() {
 
 #ifdef TOOLS_ENABLED
 	EditorNode::add_init_callback(&_editor_init_callback);
-
-	GLOBAL_DEF("mono/export/include_scripts_content", false);
 #endif
 }
 

+ 1 - 1
modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs

@@ -44,7 +44,7 @@ namespace GodotTools.Build
         {
             get
             {
-                if (OS.IsWindows())
+                if (OS.IsWindows)
                 {
                     return (BuildManager.BuildTool) EditorSettings.GetSetting("mono/builds/build_tool")
                            == BuildManager.BuildTool.MsBuildMono;

+ 3 - 3
modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs

@@ -21,7 +21,7 @@ namespace GodotTools.Build
             var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
             var buildTool = (BuildManager.BuildTool) editorSettings.GetSetting("mono/builds/build_tool");
 
-            if (OS.IsWindows())
+            if (OS.IsWindows)
             {
                 switch (buildTool)
                 {
@@ -59,7 +59,7 @@ namespace GodotTools.Build
                 }
             }
 
-            if (OS.IsUnix())
+            if (OS.IsUnixLike())
             {
                 if (buildTool == BuildManager.BuildTool.MsBuildMono)
                 {
@@ -128,7 +128,7 @@ namespace GodotTools.Build
 
         private static string FindMsBuildToolsPathOnWindows()
         {
-            if (!OS.IsWindows())
+            if (!OS.IsWindows)
                 throw new PlatformNotSupportedException();
 
             // Try to find 15.0 with vswhere

+ 2 - 2
modules/mono/editor/GodotTools/GodotTools/BuildManager.cs

@@ -246,7 +246,7 @@ namespace GodotTools
         {
             // Build tool settings
 
-            EditorDef("mono/builds/build_tool", OS.IsWindows() ? BuildTool.MsBuildVs : BuildTool.MsBuildMono);
+            EditorDef("mono/builds/build_tool", OS.IsWindows ? BuildTool.MsBuildVs : BuildTool.MsBuildMono);
 
             var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
 
@@ -255,7 +255,7 @@ namespace GodotTools
                 ["type"] = Godot.Variant.Type.Int,
                 ["name"] = "mono/builds/build_tool",
                 ["hint"] = Godot.PropertyHint.Enum,
-                ["hint_string"] = OS.IsWindows() ?
+                ["hint_string"] = OS.IsWindows ?
                     $"{PropNameMsbuildMono},{PropNameMsbuildVs}" :
                     $"{PropNameMsbuildMono}"
             });

+ 430 - 26
modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs

@@ -1,11 +1,13 @@
 using Godot;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Runtime.CompilerServices;
 using GodotTools.Core;
 using GodotTools.Internals;
+using static GodotTools.Internals.Globals;
 using Directory = GodotTools.Utils.Directory;
 using File = GodotTools.Utils.File;
 using OS = GodotTools.Utils.OS;
@@ -15,6 +17,25 @@ namespace GodotTools.Export
 {
     public class ExportPlugin : EditorExportPlugin
     {
+        public void RegisterExportSettings()
+        {
+            // TODO: These would be better as export preset options, but that doesn't seem to be supported yet
+
+            GlobalDef("mono/export/include_scripts_content", false);
+
+            GlobalDef("mono/export/aot/enabled", false);
+            GlobalDef("mono/export/aot/full_aot", false);
+
+            // --aot or --aot=opt1,opt2 (use 'mono --aot=help AuxAssembly.dll' to list AOT options)
+            GlobalDef("mono/export/aot/extra_aot_options", new string[] { });
+            // --optimize/-O=opt1,opt2 (use 'mono --list-opt'' to list optimize options)
+            GlobalDef("mono/export/aot/extra_optimizer_options", new string[] { });
+
+            GlobalDef("mono/export/aot/android_toolchain_path", "");
+        }
+
+        private string maybeLastExportError;
+
         private void AddFile(string srcPath, string dstPath, bool remap = false)
         {
             AddFile(dstPath, File.ReadAllBytes(srcPath), remap);
@@ -36,8 +57,11 @@ namespace GodotTools.Export
 
             if (!includeScriptsContent)
             {
-                // We don't want to include the source code on exported games
-                AddFile(path, new byte[] { }, remap: false);
+                // We don't want to include the source code on exported games.
+
+                // 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);
                 Skip();
             }
         }
@@ -49,27 +73,28 @@ namespace GodotTools.Export
             try
             {
                 _ExportBeginImpl(features, isDebug, path, flags);
-                // TODO: Handle _ExportBeginImpl return value. Do something on error once _ExportBegin supports failing.
             }
             catch (Exception e)
             {
+                maybeLastExportError = e.Message;
                 GD.PushError($"Failed to export project: {e.Message}");
                 Console.Error.WriteLine(e);
                 // TODO: Do something on error once _ExportBegin supports failing.
             }
         }
 
-        private bool _ExportBeginImpl(string[] features, bool isDebug, string path, int flags)
+        private void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags)
         {
             if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
-                return true;
+                return;
 
             string platform = DeterminePlatformFromFeatures(features);
 
             if (platform == null)
                 throw new NotSupportedException("Target platform not supported");
 
-            // TODO Right now there is no way to stop the export process with an error
+            string outputDir = new FileInfo(path).Directory?.FullName ??
+                               throw new FileNotFoundException("Base directory not found");
 
             string buildConfig = isDebug ? "Debug" : "Release";
 
@@ -82,10 +107,7 @@ namespace GodotTools.Export
             var godotDefines = features;
 
             if (!BuildManager.BuildProjectBlocking(buildConfig, godotDefines))
-            {
-                GD.PushError("Failed to build project");
-                return false;
-            }
+                throw new Exception("Failed to build project");
 
             // Add dependency assemblies
 
@@ -103,12 +125,9 @@ namespace GodotTools.Export
             dependencies[projectDllName] = projectDllSrcPath;
 
             {
-                string templatesDir = Internal.FullTemplatesDir;
-                string platformBclDir = Path.Combine(templatesDir, $"{platform}-bcl", platform);
+                string platformBclDir = DeterminePlatformBclDir(platform);
 
-                string customBclDir = Directory.Exists(platformBclDir) ? platformBclDir : string.Empty;
-
-                internal_GetExportedAssemblyDependencies(projectDllName, projectDllSrcPath, buildConfig, customBclDir, dependencies);
+                internal_GetExportedAssemblyDependencies(projectDllName, projectDllSrcPath, buildConfig, platformBclDir, dependencies);
             }
 
             string apiConfig = isDebug ? "Debug" : "Release";
@@ -122,18 +141,44 @@ namespace GodotTools.Export
             }
 
             // Mono specific export template extras (data dir)
-            ExportDataDirectory(features, platform, isDebug, path);
+            string outputDataDir = null;
+
+            if (PlatformHasTemplateDir(platform))
+                outputDataDir = ExportDataDirectory(features, platform, isDebug, outputDir);
+
+            // AOT
 
-            return true;
+            if ((bool) ProjectSettings.GetSetting("mono/export/aot/enabled"))
+            {
+                AotCompileDependencies(features, platform, isDebug, outputDir, outputDataDir, dependencies);
+            }
         }
 
-        private static void ExportDataDirectory(ICollection<string> features, string platform, bool debug, string path)
+        public override void _ExportEnd()
         {
-            if (!PlatformHasTemplateDir(platform))
-                return;
+            base._ExportEnd();
+
+            string aotTempDir = Path.Combine(Path.GetTempPath(), $"godot-aot-{Process.GetCurrentProcess().Id}");
+
+            if (Directory.Exists(aotTempDir))
+                Directory.Delete(aotTempDir, recursive: true);
 
+            // TODO: Just a workaround until the export plugins can be made to abort with errors
+            if (string.IsNullOrEmpty(maybeLastExportError)) // Check empty as well, because it's set to empty after hot-reloading
+            {
+                string lastExportError = maybeLastExportError;
+                maybeLastExportError = null;
+
+                GodotSharpEditor.Instance.ShowErrorDialog(lastExportError, "C# export failed");
+            }
+        }
+
+        private static string ExportDataDirectory(string[] features, string platform, bool isDebug, string outputDir)
+        {
+            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.
             string bits = features.Contains("64") ? "64" : "32";
-            string target = debug ? "release_debug" : "release";
 
             string TemplateDirName() => $"data.mono.{platform}.{bits}.{target}";
 
@@ -142,8 +187,8 @@ namespace GodotTools.Export
             if (!Directory.Exists(templateDirPath))
             {
                 templateDirPath = null;
-                
-                if (debug)
+
+                if (isDebug)
                 {
                     target = "debug"; // Support both 'release_debug' and 'debug' for the template data directory name
                     templateDirPath = Path.Combine(Internal.FullTemplatesDir, TemplateDirName());
@@ -156,9 +201,6 @@ namespace GodotTools.Export
             if (templateDirPath == null)
                 throw new FileNotFoundException("Data template directory not found");
 
-            string outputDir = new FileInfo(path).Directory?.FullName ??
-                               throw new FileNotFoundException("Base directory not found");
-
             string outputDataDir = Path.Combine(outputDir, DataDirName);
 
             if (Directory.Exists(outputDataDir))
@@ -175,6 +217,331 @@ namespace GodotTools.Export
             {
                 File.Copy(file, Path.Combine(outputDataDir, file.Substring(templateDirPath.Length + 1)));
             }
+
+            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();
+            }
+        }
+
+        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)
@@ -194,6 +561,43 @@ namespace GodotTools.Export
             return null;
         }
 
+        private static string DeterminePlatformBclDir(string platform)
+        {
+            string templatesDir = Internal.FullTemplatesDir;
+            string platformBclDir = Path.Combine(templatesDir, "bcl", platform);
+
+            if (!File.Exists(Path.Combine(platformBclDir, "mscorlib.dll")))
+            {
+                string profile = DeterminePlatformBclProfile(platform);
+                platformBclDir = Path.Combine(templatesDir, "bcl", profile);
+
+                if (!File.Exists(Path.Combine(platformBclDir, "mscorlib.dll")))
+                    platformBclDir = null; // Use the one we're running on
+            }
+
+            return platformBclDir;
+        }
+
+        private static string DeterminePlatformBclProfile(string platform)
+        {
+            switch (platform)
+            {
+                case OS.Platforms.Windows:
+                case OS.Platforms.UWP:
+                case OS.Platforms.OSX:
+                case OS.Platforms.X11:
+                case OS.Platforms.Server:
+                case OS.Platforms.Haiku:
+                    return "net_4_x";
+                case OS.Platforms.Android:
+                    return "monodroid";
+                case OS.Platforms.HTML5:
+                    return "wasm";
+                default:
+                    throw new NotSupportedException();
+            }
+        }
+
         private static string DataDirName
         {
             get

+ 3 - 2
modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs

@@ -416,7 +416,7 @@ namespace GodotTools
 
             string settingsHintStr = "Disabled";
 
-            if (OS.IsWindows())
+            if (OS.IsWindows)
             {
                 settingsHintStr += $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" +
                                    $",Visual Studio Code:{(int) ExternalEditorId.VsCode}";
@@ -427,7 +427,7 @@ namespace GodotTools
                                    $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" +
                                    $",Visual Studio Code:{(int) ExternalEditorId.VsCode}";
             }
-            else if (OS.IsUnix())
+            else if (OS.IsUnixLike())
             {
                 settingsHintStr += $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" +
                                    $",Visual Studio Code:{(int) ExternalEditorId.VsCode}";
@@ -444,6 +444,7 @@ namespace GodotTools
             // Export plugin
             var exportPlugin = new ExportPlugin();
             AddExportPlugin(exportPlugin);
+            exportPlugin.RegisterExportSettings();
             exportPluginWeak = WeakRef(exportPlugin);
 
             BuildManager.Initialize();

+ 2 - 2
modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs

@@ -107,7 +107,7 @@ namespace GodotTools.Ides.MonoDevelop
                     {EditorId.VisualStudioForMac, "com.microsoft.visual-studio"}
                 };
             }
-            else if (OS.IsWindows())
+            else if (OS.IsWindows)
             {
                 ExecutableNames = new Dictionary<EditorId, string>
                 {
@@ -118,7 +118,7 @@ namespace GodotTools.Ides.MonoDevelop
                     {EditorId.MonoDevelop, "MonoDevelop.exe"}
                 };
             }
-            else if (OS.IsUnix())
+            else if (OS.IsUnixLike())
             {
                 ExecutableNames = new Dictionary<EditorId, string>
                 {

+ 3 - 1
modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs

@@ -52,7 +52,7 @@ namespace GodotTools.Internals
 
         public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts();
 
-        // Internal Calls
+        #region Internal
 
         [MethodImpl(MethodImplOptions.InternalCall)]
         private static extern string internal_UpdateApiAssembliesFromPrebuilt(string config);
@@ -110,5 +110,7 @@ namespace GodotTools.Internals
 
         [MethodImpl(MethodImplOptions.InternalCall)]
         private static extern void internal_ScriptEditorDebugger_ReloadScripts();
+
+        #endregion
     }
 }

+ 10 - 8
modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs

@@ -55,7 +55,7 @@ namespace GodotTools.Utils
             return name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
         }
 
-        public static bool IsWindows() => IsOS(Names.Windows);
+        public static bool IsWindows => IsOS(Names.Windows);
 
         public static bool IsOSX => IsOS(Names.OSX);
 
@@ -72,23 +72,23 @@ namespace GodotTools.Utils
         public static bool IsHTML5 => IsOS(Names.HTML5);
 
         private static bool? _isUnixCache;
-        private static readonly string[] UnixPlatforms = {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};
 
-        public static bool IsUnix()
+        public static bool IsUnixLike()
         {
             if (_isUnixCache.HasValue)
                 return _isUnixCache.Value;
 
             string osName = GetPlatformName();
-            _isUnixCache = UnixPlatforms.Any(p => p.Equals(osName, StringComparison.OrdinalIgnoreCase));
+            _isUnixCache = UnixLikePlatforms.Any(p => p.Equals(osName, StringComparison.OrdinalIgnoreCase));
             return _isUnixCache.Value;
         }
 
-        public static char PathSep => IsWindows() ? ';' : ':';
+        public static char PathSep => IsWindows ? ';' : ':';
 
         public static string PathWhich(string name)
         {
-            string[] windowsExts = IsWindows() ? Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) : null;
+            string[] windowsExts = IsWindows ? Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) : null;
             string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep);
 
             var searchDirs = new List<string>();
@@ -102,7 +102,7 @@ namespace GodotTools.Utils
             {
                 string path = Path.Combine(dir, name);
 
-                if (IsWindows() && windowsExts != null)
+                if (IsWindows && windowsExts != null)
                 {
                     foreach (var extension in windowsExts)
                     {
@@ -124,12 +124,14 @@ namespace GodotTools.Utils
 
         public static void RunProcess(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));
             }
 
-            ProcessStartInfo startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments))
+            var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments))
             {
                 RedirectStandardOutput = true,
                 RedirectStandardError = true,

+ 6 - 6
modules/mono/godotsharp_dirs.cpp

@@ -39,8 +39,8 @@
 #include "editor/editor_settings.h"
 #endif
 
-#ifdef __ANDROID__
-#include "utils/android_utils.h"
+#ifdef ANDROID_ENABLED
+#include "mono_gd/gd_mono_android.h"
 #endif
 
 #include "mono_gd/gd_mono.h"
@@ -164,8 +164,8 @@ private:
 		String data_mono_root_dir = data_dir_root.plus_file("Mono");
 		data_mono_etc_dir = data_mono_root_dir.plus_file("etc");
 
-#if __ANDROID__
-		data_mono_lib_dir = GDMonoUtils::Android::get_app_native_lib_dir();
+#ifdef ANDROID_ENABLED
+		data_mono_lib_dir = GDMonoAndroid::get_app_native_lib_dir();
 #else
 		data_mono_lib_dir = data_mono_root_dir.plus_file("lib");
 #endif
@@ -201,8 +201,8 @@ private:
 		String data_mono_root_dir = data_dir_root.plus_file("Mono");
 		data_mono_etc_dir = data_mono_root_dir.plus_file("etc");
 
-#if __ANDROID__
-		data_mono_lib_dir = GDMonoUtils::Android::get_app_native_lib_dir();
+#ifdef ANDROID_ENABLED
+		data_mono_lib_dir = GDMonoAndroid::get_app_native_lib_dir();
 #else
 		data_mono_lib_dir = data_mono_root_dir.plus_file("lib");
 #endif

+ 116 - 0
modules/mono/mono_gd/gd_mono_android.cpp

@@ -0,0 +1,116 @@
+#include "gd_mono_android.h"
+
+#if defined(ANDROID_ENABLED)
+
+#include <dlfcn.h> // dlopen, dlsym
+#include <mono/utils/mono-dl-fallback.h>
+
+#include "core/os/os.h"
+#include "core/ustring.h"
+#include "platform/android/thread_jandroid.h"
+
+#include "../utils/path_utils.h"
+#include "../utils/string_utils.h"
+
+namespace GDMonoAndroid {
+
+String app_native_lib_dir_cache;
+
+String determine_app_native_lib_dir() {
+	JNIEnv *env = ThreadAndroid::get_env();
+
+	jclass activityThreadClass = env->FindClass("android/app/ActivityThread");
+	jmethodID currentActivityThread = env->GetStaticMethodID(activityThreadClass, "currentActivityThread", "()Landroid/app/ActivityThread;");
+	jobject activityThread = env->CallStaticObjectMethod(activityThreadClass, currentActivityThread);
+	jmethodID getApplication = env->GetMethodID(activityThreadClass, "getApplication", "()Landroid/app/Application;");
+	jobject ctx = env->CallObjectMethod(activityThread, getApplication);
+
+	jmethodID getApplicationInfo = env->GetMethodID(env->GetObjectClass(ctx), "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;");
+	jobject applicationInfo = env->CallObjectMethod(ctx, getApplicationInfo);
+	jfieldID nativeLibraryDirField = env->GetFieldID(env->GetObjectClass(applicationInfo), "nativeLibraryDir", "Ljava/lang/String;");
+	jstring nativeLibraryDir = (jstring)env->GetObjectField(applicationInfo, nativeLibraryDirField);
+
+	String result;
+
+	const char *const nativeLibraryDir_utf8 = env->GetStringUTFChars(nativeLibraryDir, NULL);
+	if (nativeLibraryDir_utf8) {
+		result.parse_utf8(nativeLibraryDir_utf8);
+		env->ReleaseStringUTFChars(nativeLibraryDir, nativeLibraryDir_utf8);
+	}
+
+	return result;
+}
+
+String get_app_native_lib_dir() {
+	if (app_native_lib_dir_cache.empty())
+		app_native_lib_dir_cache = determine_app_native_lib_dir();
+	return app_native_lib_dir_cache;
+}
+
+int gd_mono_convert_dl_flags(int flags) {
+	// from mono's runtime-bootstrap.c
+
+	int lflags = flags & MONO_DL_LOCAL ? 0 : RTLD_GLOBAL;
+
+	if (flags & MONO_DL_LAZY)
+		lflags |= RTLD_LAZY;
+	else
+		lflags |= RTLD_NOW;
+
+	return lflags;
+}
+
+void *gd_mono_android_dlopen(const char *p_name, int p_flags, char **r_err, void *p_user_data) {
+	String name = String::utf8(p_name);
+
+	if (name.ends_with(".dll.so") || name.ends_with(".exe.so")) {
+		String app_native_lib_dir = get_app_native_lib_dir();
+
+		String orig_so_name = name.get_file();
+		String so_name = "lib-aot-" + orig_so_name;
+		String so_path = path::join(app_native_lib_dir, so_name);
+
+		if (!FileAccess::exists(so_path)) {
+			if (OS::get_singleton()->is_stdout_verbose())
+				OS::get_singleton()->print("Cannot find shared library: '%s'\n", so_path.utf8().get_data());
+			return NULL;
+		}
+
+		int lflags = gd_mono_convert_dl_flags(p_flags);
+
+		void *handle = dlopen(so_path.utf8().get_data(), lflags);
+
+		if (!handle) {
+			if (OS::get_singleton()->is_stdout_verbose())
+				OS::get_singleton()->print("Failed to open shared library: '%s'. Error: '%s'\n", so_path.utf8().get_data(), dlerror());
+			return NULL;
+		}
+
+		if (OS::get_singleton()->is_stdout_verbose())
+			OS::get_singleton()->print("Successfully loaded AOT shared library: '%s'\n", so_path.utf8().get_data());
+
+		return handle;
+	}
+
+	return NULL;
+}
+
+void *gd_mono_android_dlsym(void *p_handle, const char *p_name, char **r_err, void *p_user_data) {
+	void *sym_addr = dlsym(p_handle, p_name);
+
+	if (sym_addr)
+		return sym_addr;
+
+	if (r_err)
+		*r_err = str_format_new("%s\n", dlerror());
+
+	return NULL;
+}
+
+void register_android_dl_fallback() {
+	mono_dl_fallback_register(gd_mono_android_dlopen, gd_mono_android_dlsym, NULL, NULL);
+}
+
+} // namespace GDMonoAndroid
+
+#endif

+ 18 - 0
modules/mono/mono_gd/gd_mono_android.h

@@ -0,0 +1,18 @@
+#ifndef GD_MONO_ANDROID_H
+#define GD_MONO_ANDROID_H
+
+#if defined(ANDROID_ENABLED)
+
+#include "core/ustring.h"
+
+namespace GDMonoAndroid {
+
+String get_app_native_lib_dir();
+
+void register_android_dl_fallback();
+
+} // namespace GDMonoAndroid
+
+#endif // ANDROID_ENABLED
+
+#endif // GD_MONO_ANDROID_H

+ 0 - 68
modules/mono/utils/android_utils.cpp

@@ -1,68 +0,0 @@
-/*************************************************************************/
-/*  android_utils.cpp                                                    */
-/*************************************************************************/
-/*                       This file is part of:                           */
-/*                           GODOT ENGINE                                */
-/*                      https://godotengine.org                          */
-/*************************************************************************/
-/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur.                 */
-/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md)    */
-/*                                                                       */
-/* Permission is hereby granted, free of charge, to any person obtaining */
-/* a copy of this software and associated documentation files (the       */
-/* "Software"), to deal in the Software without restriction, including   */
-/* without limitation the rights to use, copy, modify, merge, publish,   */
-/* distribute, sublicense, and/or sell copies of the Software, and to    */
-/* permit persons to whom the Software is furnished to do so, subject to */
-/* the following conditions:                                             */
-/*                                                                       */
-/* The above copyright notice and this permission notice shall be        */
-/* included in all copies or substantial portions of the Software.       */
-/*                                                                       */
-/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
-/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
-/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
-/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
-/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
-/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
-/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
-/*************************************************************************/
-
-#include "android_utils.h"
-
-#ifdef __ANDROID__
-
-#include "platform/android/thread_jandroid.h"
-
-namespace GDMonoUtils {
-namespace Android {
-
-String get_app_native_lib_dir() {
-	JNIEnv *env = ThreadAndroid::get_env();
-
-	jclass activityThreadClass = env->FindClass("android/app/ActivityThread");
-	jmethodID currentActivityThread = env->GetStaticMethodID(activityThreadClass, "currentActivityThread", "()Landroid/app/ActivityThread;");
-	jobject activityThread = env->CallStaticObjectMethod(activityThreadClass, currentActivityThread);
-	jmethodID getApplication = env->GetMethodID(activityThreadClass, "getApplication", "()Landroid/app/Application;");
-	jobject ctx = env->CallObjectMethod(activityThread, getApplication);
-
-	jmethodID getApplicationInfo = env->GetMethodID(env->GetObjectClass(ctx), "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;");
-	jobject applicationInfo = env->CallObjectMethod(ctx, getApplicationInfo);
-	jfieldID nativeLibraryDirField = env->GetFieldID(env->GetObjectClass(applicationInfo), "nativeLibraryDir", "Ljava/lang/String;");
-	jstring nativeLibraryDir = (jstring)env->GetObjectField(applicationInfo, nativeLibraryDirField);
-
-	String result;
-
-	const char *const nativeLibraryDir_utf8 = env->GetStringUTFChars(nativeLibraryDir, NULL);
-	if (nativeLibraryDir_utf8) {
-		result.parse_utf8(nativeLibraryDir_utf8);
-		env->ReleaseStringUTFChars(nativeLibraryDir, nativeLibraryDir_utf8);
-	}
-
-	return result;
-}
-
-} // namespace Android
-} // namespace GDMonoUtils
-
-#endif // __ANDROID__

+ 0 - 48
modules/mono/utils/android_utils.h

@@ -1,48 +0,0 @@
-/*************************************************************************/
-/*  android_utils.h                                                      */
-/*************************************************************************/
-/*                       This file is part of:                           */
-/*                           GODOT ENGINE                                */
-/*                      https://godotengine.org                          */
-/*************************************************************************/
-/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur.                 */
-/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md)    */
-/*                                                                       */
-/* Permission is hereby granted, free of charge, to any person obtaining */
-/* a copy of this software and associated documentation files (the       */
-/* "Software"), to deal in the Software without restriction, including   */
-/* without limitation the rights to use, copy, modify, merge, publish,   */
-/* distribute, sublicense, and/or sell copies of the Software, and to    */
-/* permit persons to whom the Software is furnished to do so, subject to */
-/* the following conditions:                                             */
-/*                                                                       */
-/* The above copyright notice and this permission notice shall be        */
-/* included in all copies or substantial portions of the Software.       */
-/*                                                                       */
-/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
-/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
-/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
-/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
-/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
-/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
-/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
-/*************************************************************************/
-
-#ifndef ANDROID_UTILS_H
-#define ANDROID_UTILS_H
-
-#ifdef __ANDROID__
-
-#include "core/ustring.h"
-
-namespace GDMonoUtils {
-namespace Android {
-
-String get_app_native_lib_dir();
-
-} // namespace Android
-} // namespace GDMonoUtils
-
-#endif // __ANDROID__
-
-#endif // ANDROID_UTILS_H

+ 20 - 4
modules/mono/utils/string_utils.cpp

@@ -216,6 +216,25 @@ String str_format(const char *p_format, ...) {
 #endif
 
 String str_format(const char *p_format, va_list p_list) {
+	char *buffer = str_format_new(p_format, p_list);
+
+	String res(buffer);
+	memdelete_arr(buffer);
+
+	return res;
+}
+
+char *str_format_new(const char *p_format, ...) {
+	va_list list;
+
+	va_start(list, p_format);
+	char *res = str_format_new(p_format, list);
+	va_end(list);
+
+	return res;
+}
+
+char *str_format_new(const char *p_format, va_list p_list) {
 	va_list list;
 
 	va_copy(list, p_list);
@@ -230,8 +249,5 @@ String str_format(const char *p_format, va_list p_list) {
 	gd_vsnprintf(buffer, len, p_format, list);
 	va_end(list);
 
-	String res(buffer);
-	memdelete_arr(buffer);
-
-	return res;
+	return buffer;
 }

+ 2 - 0
modules/mono/utils/string_utils.h

@@ -56,5 +56,7 @@ Error read_all_file_utf8(const String &p_path, String &r_content);
 
 String str_format(const char *p_format, ...) _PRINTF_FORMAT_ATTRIBUTE_1_2;
 String str_format(const char *p_format, va_list p_list) _PRINTF_FORMAT_ATTRIBUTE_1_0;
+char *str_format_new(const char *p_format, ...) _PRINTF_FORMAT_ATTRIBUTE_1_2;
+char *str_format_new(const char *p_format, va_list p_list) _PRINTF_FORMAT_ATTRIBUTE_1_0;
 
 #endif // STRING_FORMAT_H