diff options
Diffstat (limited to 'modules/mono/editor/GodotTools')
11 files changed, 698 insertions, 252 deletions
diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 9a2b2e3a26..da90c960e5 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/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; diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs index eb2c2dd77c..ad8a6516ab 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs +++ b/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) { @@ -91,7 +91,7 @@ namespace GodotTools.Build { var result = new List<string>(); - if (OS.IsOSX()) + if (OS.IsOSX) { result.Add("/Library/Frameworks/Mono.framework/Versions/Current/bin/"); result.Add("/usr/local/var/homebrew/linked/mono/bin/"); @@ -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 diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs index ab37d89955..217bf5c144 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs +++ b/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}" }); diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs new file mode 100644 index 0000000000..f65d2a39f4 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -0,0 +1,615 @@ +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; +using Path = System.IO.Path; + +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); + } + + public override void _ExportFile(string path, string type, string[] features) + { + base._ExportFile(path, type, features); + + if (type != Internal.CSharpLanguageType) + return; + + if (Path.GetExtension(path) != $".{Internal.CSharpLanguageExtension}") + throw new ArgumentException($"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}", nameof(path)); + + // TODO What if the source file is not part of the game's C# project + + bool includeScriptsContent = (bool) ProjectSettings.GetSetting("mono/export/include_scripts_content"); + + if (!includeScriptsContent) + { + // 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(); + } + } + + public override void _ExportBegin(string[] features, bool isDebug, string path, int flags) + { + base._ExportBegin(features, isDebug, path, flags); + + try + { + _ExportBeginImpl(features, isDebug, path, flags); + } + 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 void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags) + { + if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) + return; + + string platform = DeterminePlatformFromFeatures(features); + + if (platform == null) + throw new NotSupportedException("Target platform not supported"); + + string outputDir = new FileInfo(path).Directory?.FullName ?? + throw new FileNotFoundException("Base directory not found"); + + string buildConfig = isDebug ? "Debug" : "Release"; + + string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}"); + CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath); + + AddFile(scriptsMetadataPath, scriptsMetadataPath); + + // Turn export features into defines + var godotDefines = features; + + if (!BuildManager.BuildProjectBlocking(buildConfig, godotDefines)) + throw new Exception("Failed to build project"); + + // Add dependency assemblies + + var dependencies = new Godot.Collections.Dictionary<string, string>(); + + var projectDllName = (string) ProjectSettings.GetSetting("application/config/name"); + if (projectDllName.Empty()) + { + projectDllName = "UnnamedProject"; + } + + string projectDllSrcDir = Path.Combine(GodotSharpDirs.ResTempAssembliesBaseDir, buildConfig); + string projectDllSrcPath = Path.Combine(projectDllSrcDir, $"{projectDllName}.dll"); + + dependencies[projectDllName] = projectDllSrcPath; + + { + string platformBclDir = DeterminePlatformBclDir(platform); + + internal_GetExportedAssemblyDependencies(projectDllName, projectDllSrcPath, buildConfig, platformBclDir, dependencies); + } + + string apiConfig = isDebug ? "Debug" : "Release"; + string resAssembliesDir = Path.Combine(GodotSharpDirs.ResAssembliesBaseDir, apiConfig); + + foreach (var dependency in dependencies) + { + string dependSrcPath = dependency.Value; + string dependDstPath = Path.Combine(resAssembliesDir, dependSrcPath.GetFile()); + AddFile(dependSrcPath, dependDstPath); + } + + // Mono specific export template extras (data dir) + string outputDataDir = null; + + if (PlatformHasTemplateDir(platform)) + outputDataDir = ExportDataDirectory(features, platform, isDebug, outputDir); + + // AOT + + if ((bool) ProjectSettings.GetSetting("mono/export/aot/enabled")) + { + AotCompileDependencies(features, platform, isDebug, outputDir, outputDataDir, dependencies); + } + } + + public override void _ExportEnd() + { + 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 TemplateDirName() => $"data.mono.{platform}.{bits}.{target}"; + + string templateDirPath = Path.Combine(Internal.FullTemplatesDir, TemplateDirName()); + + if (!Directory.Exists(templateDirPath)) + { + templateDirPath = null; + + if (isDebug) + { + target = "debug"; // Support both 'release_debug' and 'debug' for the template data directory name + templateDirPath = Path.Combine(Internal.FullTemplatesDir, TemplateDirName()); + + if (!Directory.Exists(templateDirPath)) + templateDirPath = null; + } + } + + if (templateDirPath == null) + throw new FileNotFoundException("Data template directory not found"); + + string outputDataDir = Path.Combine(outputDir, DataDirName); + + if (Directory.Exists(outputDataDir)) + Directory.Delete(outputDataDir, recursive: true); // Clean first + + Directory.CreateDirectory(outputDataDir); + + foreach (string dir in Directory.GetDirectories(templateDirPath, "*", SearchOption.AllDirectories)) + { + Directory.CreateDirectory(Path.Combine(outputDataDir, dir.Substring(templateDirPath.Length + 1))); + } + + foreach (string file in Directory.GetFiles(templateDirPath, "*", SearchOption.AllDirectories)) + { + 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) + { + // 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); + } + + private static string DeterminePlatformFromFeatures(IEnumerable<string> features) + { + foreach (var feature in features) + { + if (OS.PlatformNameMap.TryGetValue(feature, out string platform)) + return platform; + } + + 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 + { + var appName = (string) ProjectSettings.GetSetting("application/config/name"); + string appNameSafe = appName.ToSafeDirName(allowDirSeparator: false); + return $"data_{appNameSafe}"; + } + } + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void internal_GetExportedAssemblyDependencies(string projectDllName, string projectDllSrcPath, + string buildConfig, string customBclDir, Godot.Collections.Dictionary<string, string> dependencies); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 12edd651df..2a5d3de126 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -1,4 +1,5 @@ using Godot; +using GodotTools.Export; using GodotTools.Utils; using System; using System.Collections.Generic; @@ -225,7 +226,7 @@ namespace GodotTools bool osxAppBundleInstalled = false; - if (OS.IsOSX()) + if (OS.IsOSX) { // The package path is '/Applications/Visual Studio Code.app' const string vscodeBundleId = "com.microsoft.VSCode"; @@ -265,7 +266,7 @@ namespace GodotTools string command; - if (OS.IsOSX()) + if (OS.IsOSX) { if (!osxAppBundleInstalled && _vsCodePath.Empty()) { @@ -415,18 +416,18 @@ namespace GodotTools string settingsHintStr = "Disabled"; - if (OS.IsWindows()) + if (OS.IsWindows) { settingsHintStr += $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" + $",Visual Studio Code:{(int) ExternalEditorId.VsCode}"; } - else if (OS.IsOSX()) + else if (OS.IsOSX) { settingsHintStr += $",Visual Studio:{(int) ExternalEditorId.VisualStudioForMac}" + $",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}"; @@ -441,8 +442,9 @@ namespace GodotTools }); // Export plugin - var exportPlugin = new GodotSharpExport(); + var exportPlugin = new ExportPlugin(); AddExportPlugin(exportPlugin); + exportPlugin.RegisterExportSettings(); exportPluginWeak = WeakRef(exportPlugin); BuildManager.Initialize(); @@ -461,7 +463,7 @@ namespace GodotTools // Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid // will be freed after EditorSettings already was, and its device polling thread // will try to access the EditorSettings singleton, resulting in null dereferencing. - (exportPluginWeak.GetRef() as GodotSharpExport)?.Dispose(); + (exportPluginWeak.GetRef() as ExportPlugin)?.Dispose(); exportPluginWeak.Dispose(); } diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs deleted file mode 100644 index 4f93ef8530..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs +++ /dev/null @@ -1,197 +0,0 @@ -using Godot; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using GodotTools.Core; -using GodotTools.Internals; -using Directory = GodotTools.Utils.Directory; -using File = GodotTools.Utils.File; -using Path = System.IO.Path; - -namespace GodotTools -{ - public class GodotSharpExport : EditorExportPlugin - { - private void AddFile(string srcPath, string dstPath, bool remap = false) - { - AddFile(dstPath.Replace("\\", "/"), File.ReadAllBytes(srcPath), remap); - } - - public override void _ExportFile(string path, string type, string[] features) - { - base._ExportFile(path, type, features); - - if (type != Internal.CSharpLanguageType) - return; - - if (Path.GetExtension(path) != $".{Internal.CSharpLanguageExtension}") - throw new ArgumentException($"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}", nameof(path)); - - // TODO What if the source file is not part of the game's C# project - - bool includeScriptsContent = (bool) ProjectSettings.GetSetting("mono/export/include_scripts_content"); - - if (!includeScriptsContent) - { - // We don't want to include the source code on exported games - AddFile(path, new byte[] { }, remap: false); - Skip(); - } - } - - public override void _ExportBegin(string[] features, bool isDebug, string path, int flags) - { - base._ExportBegin(features, isDebug, path, flags); - - try - { - _ExportBeginImpl(features, isDebug, path, flags); - } - catch (Exception e) - { - GD.PushError($"Failed to export project. Exception message: {e.Message}"); - Console.Error.WriteLine(e); - } - } - - public void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags) - { - // TODO Right now there is no way to stop the export process with an error - - if (File.Exists(GodotSharpDirs.ProjectSlnPath)) - { - string buildConfig = isDebug ? "Debug" : "Release"; - - string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}"); - CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath); - - AddFile(scriptsMetadataPath, scriptsMetadataPath); - - // Turn export features into defines - var godotDefines = features; - - if (!BuildManager.BuildProjectBlocking(buildConfig, godotDefines)) - { - GD.PushError("Failed to build project"); - return; - } - - // Add dependency assemblies - - var dependencies = new Godot.Collections.Dictionary<string, string>(); - - var projectDllName = (string) ProjectSettings.GetSetting("application/config/name"); - if (projectDllName.Empty()) - { - projectDllName = "UnnamedProject"; - } - - string projectDllSrcDir = Path.Combine(GodotSharpDirs.ResTempAssembliesBaseDir, buildConfig); - string projectDllSrcPath = Path.Combine(projectDllSrcDir, $"{projectDllName}.dll"); - - dependencies[projectDllName] = projectDllSrcPath; - - { - string templatesDir = Internal.FullTemplatesDir; - string androidBclDir = Path.Combine(templatesDir, "android-bcl"); - - string customLibDir = features.Contains("Android") && Directory.Exists(androidBclDir) ? androidBclDir : string.Empty; - - GetExportedAssemblyDependencies(projectDllName, projectDllSrcPath, buildConfig, customLibDir, dependencies); - } - - string apiConfig = isDebug ? "Debug" : "Release"; - string resAssembliesDir = Path.Combine(GodotSharpDirs.ResAssembliesBaseDir, apiConfig); - - foreach (var dependency in dependencies) - { - string dependSrcPath = dependency.Value; - string dependDstPath = Path.Combine(resAssembliesDir, dependSrcPath.GetFile()); - AddFile(dependSrcPath, dependDstPath); - } - } - - // Mono specific export template extras (data dir) - ExportDataDirectory(features, isDebug, path); - } - - private static void ExportDataDirectory(IEnumerable<string> features, bool debug, string path) - { - var featureSet = new HashSet<string>(features); - - if (!PlatformHasTemplateDir(featureSet)) - return; - - string templateDirName = "data.mono"; - - if (featureSet.Contains("Windows")) - { - templateDirName += ".windows"; - templateDirName += featureSet.Contains("64") ? ".64" : ".32"; - } - else if (featureSet.Contains("X11")) - { - templateDirName += ".x11"; - templateDirName += featureSet.Contains("64") ? ".64" : ".32"; - } - else - { - throw new NotSupportedException("Target platform not supported"); - } - - templateDirName += debug ? ".release_debug" : ".release"; - - string templateDirPath = Path.Combine(Internal.FullTemplatesDir, templateDirName); - - if (!Directory.Exists(templateDirPath)) - 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)) - Directory.Delete(outputDataDir, recursive: true); // Clean first - - Directory.CreateDirectory(outputDataDir); - - foreach (string dir in Directory.GetDirectories(templateDirPath, "*", SearchOption.AllDirectories)) - { - Directory.CreateDirectory(Path.Combine(outputDataDir, dir.Substring(templateDirPath.Length + 1))); - } - - foreach (string file in Directory.GetFiles(templateDirPath, "*", SearchOption.AllDirectories)) - { - File.Copy(file, Path.Combine(outputDataDir, file.Substring(templateDirPath.Length + 1))); - } - } - - private static bool PlatformHasTemplateDir(IEnumerable<string> featureSet) - { - // OSX export templates are contained in a zip, so we place - // our custom template inside it and let Godot do the rest. - return !featureSet.Any(f => new[] {"OSX", "Android"}.Contains(f)); - } - - private static string DataDirName - { - get - { - var appName = (string) ProjectSettings.GetSetting("application/config/name"); - string appNameSafe = appName.ToSafeDirName(allowDirSeparator: false); - return $"data_{appNameSafe}"; - } - } - - private static void GetExportedAssemblyDependencies(string projectDllName, string projectDllSrcPath, - string buildConfig, string customLibDir, Godot.Collections.Dictionary<string, string> dependencies) => - internal_GetExportedAssemblyDependencies(projectDllName, projectDllSrcPath, buildConfig, customLibDir, dependencies); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_GetExportedAssemblyDependencies(string projectDllName, string projectDllSrcPath, - string buildConfig, string customLibDir, Godot.Collections.Dictionary<string, string> dependencies); - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index 3c57900873..d0c78d095b 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -40,6 +40,7 @@ </ItemGroup> <ItemGroup> <Compile Include="Build\MsBuildFinder.cs" /> + <Compile Include="Export\ExportPlugin.cs" /> <Compile Include="ExternalEditorId.cs" /> <Compile Include="Ides\GodotIdeManager.cs" /> <Compile Include="Ides\GodotIdeServer.cs" /> @@ -63,7 +64,6 @@ <Compile Include="BuildInfo.cs" /> <Compile Include="BuildTab.cs" /> <Compile Include="BottomPanel.cs" /> - <Compile Include="GodotSharpExport.cs" /> <Compile Include="CsProjOperations.cs" /> <Compile Include="Utils\CollectionExtensions.cs" /> </ItemGroup> diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs index 01aa0d0ab1..f94d6f998c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs @@ -79,7 +79,7 @@ namespace GodotTools.Ides { MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath) { - if (Utils.OS.IsOSX() && editor == ExternalEditorId.VisualStudioForMac) + if (Utils.OS.IsOSX && editor == ExternalEditorId.VisualStudioForMac) { vsForMacInstance = vsForMacInstance ?? new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac); diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs index 1fdccf5bbd..6026c109ad 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs @@ -24,7 +24,7 @@ namespace GodotTools.Ides.MonoDevelop string command; - if (OS.IsOSX()) + if (OS.IsOSX) { string bundleId = BundleIds[editorId]; @@ -81,7 +81,7 @@ namespace GodotTools.Ides.MonoDevelop public Instance(string solutionFile, EditorId editorId) { - if (editorId == EditorId.VisualStudioForMac && !OS.IsOSX()) + if (editorId == EditorId.VisualStudioForMac && !OS.IsOSX) throw new InvalidOperationException($"{nameof(EditorId.VisualStudioForMac)} not supported on this platform"); this.solutionFile = solutionFile; @@ -93,7 +93,7 @@ namespace GodotTools.Ides.MonoDevelop static Instance() { - if (OS.IsOSX()) + if (OS.IsOSX) { ExecutableNames = new Dictionary<EditorId, string> { @@ -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> { diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs index 836c9c11e4..de361ba844 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs +++ b/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 } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index e48b1115db..21ee85f2a9 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -1,72 +1,94 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.CompilerServices; namespace GodotTools.Utils { + [SuppressMessage("ReSharper", "InconsistentNaming")] public static class OS { [MethodImpl(MethodImplOptions.InternalCall)] - extern static string GetPlatformName(); + static extern string GetPlatformName(); - const string HaikuName = "Haiku"; - const string OSXName = "OSX"; - const string ServerName = "Server"; - const string UWPName = "UWP"; - const string WindowsName = "Windows"; - const string X11Name = "X11"; - - public static bool IsHaiku() + public static class Names { - return HaikuName.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase); + public const string Windows = "Windows"; + public const string OSX = "OSX"; + public const string X11 = "X11"; + public const string Server = "Server"; + public const string UWP = "UWP"; + public const string Haiku = "Haiku"; + public const string Android = "Android"; + public const string HTML5 = "HTML5"; } - public static bool IsOSX() + public static class Platforms { - return OSXName.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase); + public const string Windows = "windows"; + public const string OSX = "osx"; + public const string X11 = "x11"; + public const string Server = "server"; + public const string UWP = "uwp"; + public const string Haiku = "haiku"; + public const string Android = "android"; + public const string HTML5 = "javascript"; } - public static bool IsServer() + public static readonly Dictionary<string, string> PlatformNameMap = new Dictionary<string, string> { - return ServerName.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase); - } - - public static bool IsUWP() + [Names.Windows] = Platforms.Windows, + [Names.OSX] = Platforms.OSX, + [Names.X11] = Platforms.X11, + [Names.Server] = Platforms.Server, + [Names.UWP] = Platforms.UWP, + [Names.Haiku] = Platforms.Haiku, + [Names.Android] = Platforms.Android, + [Names.HTML5] = Platforms.HTML5 + }; + + private static bool IsOS(string name) { - return UWPName.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase); + return name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase); } - public static bool IsWindows() - { - return WindowsName.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase); - } + public static bool IsWindows => IsOS(Names.Windows); - public static bool IsX11() - { - return X11Name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase); - } + public static bool IsOSX => IsOS(Names.OSX); + + public static bool IsX11 => IsOS(Names.X11); + + public static bool IsServer => IsOS(Names.Server); + + public static bool IsUWP => IsOS(Names.UWP); + + public static bool IsHaiku => IsOS(Names.Haiku); + + public static bool IsAndroid => IsOS(Names.Android); + + public static bool IsHTML5 => IsOS(Names.HTML5); private static bool? _isUnixCache; - private static readonly string[] UnixPlatforms = {HaikuName, OSXName, ServerName, X11Name}; + 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>(); @@ -80,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) { @@ -102,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, |