diff options
author | Ignacio Roldán Etcheverry <ignalfonsore@gmail.com> | 2021-12-28 23:25:16 +0100 |
---|---|---|
committer | Ignacio Roldán Etcheverry <ignalfonsore@gmail.com> | 2022-08-22 03:36:51 +0200 |
commit | 88e367a4066773a6fbfe2ea25dc2e81d2035d791 (patch) | |
tree | a219a4332cb7b4c05daacce718af76347774df77 /modules | |
parent | f88d8902cfc0d6a9441e794eb47611ef4ed0d46c (diff) |
C#/netcore: Add base desktop game export implementation
This base implementation is still very barebones but it defines the path
for how exporting will work (at least when embedding the .NET runtime).
Many manual steps are still needed, which should be automatized in the
future. For example, in addition to the API assemblies, now you also
need to copy the GodotPlugins assembly to each game project.
Diffstat (limited to 'modules')
51 files changed, 1536 insertions, 1192 deletions
diff --git a/modules/mono/SCsub b/modules/mono/SCsub index 5757917f56..ab7a2bf761 100644 --- a/modules/mono/SCsub +++ b/modules/mono/SCsub @@ -7,9 +7,6 @@ Import("env_modules") env_mono = env_modules.Clone() -if env_mono["tools"] or env_mono["target"] != "release": - env_mono.Append(CPPDEFINES=["GD_MONO_HOT_RELOAD"]) - # Configure Mono mono_configure.configure(env, env_mono) diff --git a/modules/mono/build_scripts/mono_configure.py b/modules/mono/build_scripts/mono_configure.py index 5d2030f756..39c9d58eee 100644 --- a/modules/mono/build_scripts/mono_configure.py +++ b/modules/mono/build_scripts/mono_configure.py @@ -25,7 +25,7 @@ def configure(env, env_mono): if tools_enabled and not module_supports_tools_on(env["platform"]): raise RuntimeError("This module does not currently support building for this platform with tools enabled") - if env["tools"] or env["target"] != "release": + if env["tools"]: env_mono.Append(CPPDEFINES=["GD_MONO_HOT_RELOAD"]) app_host_dir = find_dotnet_app_host_dir(env) @@ -47,29 +47,33 @@ def configure(env, env_mono): check_app_host_file_exists("hostfxr.h") check_app_host_file_exists("coreclr_delegates.h") - env.Append(LIBPATH=[app_host_dir]) env_mono.Prepend(CPPPATH=app_host_dir) - libnethost_path = os.path.join(app_host_dir, "libnethost.lib" if os.name == "nt" else "libnethost.a") + env.Append(LIBPATH=[app_host_dir]) + + # Only the editor build links nethost, which is needed to find hostfxr. + # Exported games don't need this logic as hostfxr is bundled with them. + if tools_enabled: + libnethost_path = os.path.join(app_host_dir, "libnethost.lib" if os.name == "nt" else "libnethost.a") - if env["platform"] == "windows": - env_mono.Append(CPPDEFINES=["NETHOST_USE_AS_STATIC"]) + if env["platform"] == "windows": + env_mono.Append(CPPDEFINES=["NETHOST_USE_AS_STATIC"]) - if env.msvc: - env.Append(LINKFLAGS="libnethost.lib") + if env.msvc: + env.Append(LINKFLAGS="libnethost.lib") + else: + env.Append(LINKFLAGS=["-Wl,-whole-archive", libnethost_path, "-Wl,-no-whole-archive"]) else: - env.Append(LINKFLAGS=["-Wl,-whole-archive", libnethost_path, "-Wl,-no-whole-archive"]) - else: - is_apple = env["platform"] in ["macos", "ios"] - # is_macos = is_apple and not is_ios + is_apple = env["platform"] in ["macos", "ios"] + # is_macos = is_apple and not is_ios - # if is_ios and not is_ios_sim: - # env_mono.Append(CPPDEFINES=["IOS_DEVICE"]) + # if is_ios and not is_ios_sim: + # env_mono.Append(CPPDEFINES=["IOS_DEVICE"]) - if is_apple: - env.Append(LINKFLAGS=["-Wl,-force_load," + libnethost_path]) - else: - env.Append(LINKFLAGS=["-Wl,-whole-archive", libnethost_path, "-Wl,-no-whole-archive"]) + if is_apple: + env.Append(LINKFLAGS=["-Wl,-force_load," + libnethost_path]) + else: + env.Append(LINKFLAGS=["-Wl,-whole-archive", libnethost_path, "-Wl,-no-whole-archive"]) def find_dotnet_app_host_dir(env): @@ -156,7 +160,8 @@ def find_app_host_version(dotnet_cmd, search_version_str): search_version = LooseVersion(search_version_str) try: - lines = subprocess.check_output([dotnet_cmd, "--list-runtimes"]).splitlines() + env = dict(os.environ, DOTNET_CLI_UI_LANGUAGE="en-US") + lines = subprocess.check_output([dotnet_cmd, "--list-runtimes"], env=env).splitlines() for line_bytes in lines: line = line_bytes.decode("utf-8") @@ -188,7 +193,8 @@ def find_dotnet_sdk(dotnet_cmd, search_version_str): search_version = LooseVersion(search_version_str) try: - lines = subprocess.check_output([dotnet_cmd, "--list-sdks"]).splitlines() + env = dict(os.environ, DOTNET_CLI_UI_LANGUAGE="en-US") + lines = subprocess.check_output([dotnet_cmd, "--list-sdks"], env=env).splitlines() for line_bytes in lines: line = line_bytes.decode("utf-8") diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 52b0e82c6e..ade436d3e0 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -107,7 +107,7 @@ Error CSharpLanguage::execute_file(const String &p_path) { extern void *godotsharp_pinvoke_funcs[178]; [[maybe_unused]] volatile void **do_not_strip_godotsharp_pinvoke_funcs; #ifdef TOOLS_ENABLED -extern void *godotsharp_editor_pinvoke_funcs[32]; +extern void *godotsharp_editor_pinvoke_funcs[30]; [[maybe_unused]] volatile void **do_not_strip_godotsharp_editor_pinvoke_funcs; #endif @@ -137,11 +137,11 @@ void CSharpLanguage::init() { gdmono = memnew(GDMono); gdmono->initialize(); +#ifdef TOOLS_ENABLED if (gdmono->is_runtime_initialized()) { gdmono->initialize_load_assemblies(); } -#ifdef TOOLS_ENABLED EditorNode::add_init_callback(&_editor_init_callback); #endif } @@ -788,6 +788,11 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { } #warning TODO ALCs after switching to .NET 6 + + // Try to load the project assembly if it was not yet loaded + // (while hot-reload is not yet implemented) + gdmono->initialize_load_assemblies(); + #if 0 // There is no soft reloading with Mono. It's always hard reloading. @@ -1230,7 +1235,7 @@ void CSharpLanguage::_on_scripts_domain_about_to_unload() { void CSharpLanguage::_editor_init_callback() { // Load GodotTools and initialize GodotSharpEditor - Object *editor_plugin_obj = GDMono::get_singleton()->plugin_callbacks.LoadToolsAssemblyCallback( + Object *editor_plugin_obj = GDMono::get_singleton()->get_plugin_callbacks().LoadToolsAssemblyCallback( GodotSharpDirs::get_data_editor_tools_dir().plus_file("GodotTools.dll").utf16()); CRASH_COND(editor_plugin_obj == nullptr); @@ -1836,11 +1841,8 @@ bool CSharpInstance::has_method(const StringName &p_method) const { return false; } - String method = p_method; - bool deep = true; - - return GDMonoCache::managed_callbacks.ScriptManagerBridge_HasMethodUnknownParams( - script.ptr(), &method, deep); + return GDMonoCache::managed_callbacks.CSharpInstanceBridge_HasMethodUnknownParams( + gchandle.get_intptr(), &p_method); } Variant CSharpInstance::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { @@ -2954,21 +2956,9 @@ void CSharpScript::get_script_method_list(List<MethodInfo> *p_list) const { } bool CSharpScript::has_method(const StringName &p_method) const { - if (!valid) { - return false; - } - - if (!GDMonoCache::godot_api_cache_updated) { - return false; - } - - String method = p_method; - bool deep = false; - - bool found = GDMonoCache::managed_callbacks.ScriptManagerBridge_HasMethodUnknownParams( - this, &method, deep); - - return found; + // The equivalent of this will be implemented once we switch to the GDExtension system + ERR_PRINT_ONCE("CSharpScript::has_method is not implemented"); + return false; } MethodInfo CSharpScript::get_method_info(const StringName &p_method) const { @@ -3004,6 +2994,9 @@ Error CSharpScript::reload(bool p_keep_state) { String script_path = get_path(); + // In case it was already added by a previous reload + GDMonoCache::managed_callbacks.ScriptManagerBridge_RemoveScriptBridge(this); + valid = GDMonoCache::managed_callbacks.ScriptManagerBridge_AddScriptBridge(this, &script_path); if (valid) { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props index 5a499742e9..9bc134818a 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props @@ -104,11 +104,9 @@ Godot scripting API is continuaslly breaking backwards compatibility even in patch releases. --> <Reference Include="GodotSharp"> - <Private>false</Private> <HintPath>$(GodotProjectDir).godot\mono\assemblies\$(GodotApiConfiguration)\GodotSharp.dll</HintPath> </Reference> <Reference Include="GodotSharpEditor" Condition=" '$(Configuration)' == 'Debug' "> - <Private>false</Private> <HintPath>$(GodotProjectDir).godot\mono\assemblies\$(GodotApiConfiguration)\GodotSharpEditor.dll</HintPath> </Reference> </ItemGroup> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs index 05723c8940..0c7328284e 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs @@ -1,6 +1,8 @@ +using System; + namespace Godot.SourceGenerators.Sample { - public partial class ScriptBoilerplate : Godot.Node + public partial class ScriptBoilerplate : Node { private NodePath _nodePath; private int _velocity; @@ -17,5 +19,18 @@ namespace Godot.SourceGenerators.Sample _ = name; return 1; } + + public void IgnoreThisMethodWithByRefParams(ref int a) + { + _ = a; + } + } + + partial struct OuterClass + { + public partial class NesterClass : RefCounted + { + public override object _Get(StringName property) => null; + } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs index 4dd67252ed..fa41c85322 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs @@ -28,5 +28,37 @@ namespace Godot.SourceGenerators cds.GetLocation(), cds.SyntaxTree.FilePath)); } + + public static void ReportNonPartialGodotScriptOuterClass( + GeneratorExecutionContext context, + TypeDeclarationSyntax outerTypeDeclSyntax + ) + { + var outerSymbol = context.Compilation + .GetSemanticModel(outerTypeDeclSyntax.SyntaxTree) + .GetDeclaredSymbol(outerTypeDeclSyntax); + + string fullQualifiedName = outerSymbol is INamedTypeSymbol namedTypeSymbol ? + namedTypeSymbol.FullQualifiedName() : + "type not found"; + + string message = + $"Missing partial modifier on declaration of type '{fullQualifiedName}', " + + $"which contains one or more subclasses of '{GodotClasses.Object}'"; + + string description = $"{message}. Subclasses of '{GodotClasses.Object}' and their " + + "containing types must be declared with the partial modifier."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GODOT-G0002", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + outerTypeDeclSyntax.GetLocation(), + outerTypeDeclSyntax.SyntaxTree.FilePath)); + } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs index f8c50e66c8..9586e71d02 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs @@ -19,6 +19,11 @@ namespace Godot.SourceGenerators toggle != null && toggle.Equals("disabled", StringComparison.OrdinalIgnoreCase); + public static bool IsGodotToolsProject(this GeneratorExecutionContext context) + => context.TryGetGlobalAnalyzerProperty("IsGodotToolsProject", out string? toggle) && + toggle != null && + toggle.Equals("true", StringComparison.OrdinalIgnoreCase); + private static bool InheritsFrom(this INamedTypeSymbol? symbol, string baseName) { if (symbol == null) @@ -75,9 +80,48 @@ namespace Godot.SourceGenerators } } - public static bool IsPartial(this ClassDeclarationSyntax cds) + public static bool IsNested(this TypeDeclarationSyntax cds) + => cds.Parent is TypeDeclarationSyntax; + + public static bool IsPartial(this TypeDeclarationSyntax cds) => cds.Modifiers.Any(SyntaxKind.PartialKeyword); + public static bool AreAllOuterTypesPartial( + this TypeDeclarationSyntax cds, + out TypeDeclarationSyntax? typeMissingPartial + ) + { + SyntaxNode? outerSyntaxNode = cds.Parent; + + while (outerSyntaxNode is TypeDeclarationSyntax outerTypeDeclSyntax) + { + if (!outerTypeDeclSyntax.IsPartial()) + { + typeMissingPartial = outerTypeDeclSyntax; + return false; + } + + outerSyntaxNode = outerSyntaxNode.Parent; + } + + typeMissingPartial = null; + return true; + } + + public static string GetDeclarationKeyword(this INamedTypeSymbol namedTypeSymbol) + { + string? keyword = namedTypeSymbol.DeclaringSyntaxReferences + .OfType<TypeDeclarationSyntax>().FirstOrDefault()? + .Keyword.Text; + + return keyword ?? namedTypeSymbol.TypeKind switch + { + TypeKind.Interface => "interface", + TypeKind.Struct => "struct", + _ => "class" + }; + } + private static SymbolDisplayFormat FullyQualifiedFormatOmitGlobal { get; } = SymbolDisplayFormat.FullyQualifiedFormat .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj index 11d8e0f72b..791ad85572 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj @@ -21,14 +21,14 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" /> - <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1" PrivateAssets="all" /> + <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" /> </ItemGroup> <ItemGroup> <!-- Package the generator in the analyzer directory of the nuget package --> <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> <!-- Package the props file --> - <None Include="Godot.SourceGenerators.props" Pack="true" PackagePath="build" Visible="false" /> + <None Include="Godot.SourceGenerators.props" Pack="true" PackagePath="build" Visible="true" /> </ItemGroup> <Target Name="CopyNupkgToSConsOutputDir" AfterTargets="Pack"> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props index 5025215d34..7881ed0a8c 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props @@ -2,6 +2,7 @@ <ItemGroup> <!-- $(GodotProjectDir) is defined by Godot.NET.Sdk --> <CompilerVisibleProperty Include="GodotProjectDir" /> - <CompilerVisibleProperty Include="EnableGodotGenerators" /> + <CompilerVisibleProperty Include="GodotSourceGenerators" /> + <CompilerVisibleProperty Include="IsGodotToolsProject" /> </ItemGroup> </Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs new file mode 100644 index 0000000000..497a1b908c --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs @@ -0,0 +1,58 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.SourceGenerators +{ + [Generator] + public class GodotPluginsInitializerGenerator : ISourceGenerator + { + public void Initialize(GeneratorInitializationContext context) + { + } + + public void Execute(GeneratorExecutionContext context) + { + if (context.IsGodotToolsProject()) + return; + + string source = + @"using System; +using System.Runtime.InteropServices; +using Godot.Bridge; +using Godot.NativeInterop; + +namespace GodotPlugins.Game +{ + internal static partial class Main + { + [UnmanagedCallersOnly] + private static godot_bool InitializeFromGameProject(IntPtr outManagedCallbacks) + { + try + { + var coreApiAssembly = typeof(Godot.Object).Assembly; + + NativeLibrary.SetDllImportResolver(coreApiAssembly, GodotDllImportResolver.OnResolveDllImport); + + ManagedCallbacks.Create(outManagedCallbacks); + + ScriptManagerBridge.LookupScriptsInAssembly(typeof(GodotPlugins.Game.Main).Assembly); + + return godot_bool.True; + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return false.ToGodotBool(); + } + } + } +} +"; + + context.AddSource("GodotPlugins.Game_Generated", + SourceText.From(source, Encoding.UTF8)); + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptBoilerplateGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptBoilerplateGenerator.cs index 5ace809d95..51e9406c15 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptBoilerplateGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptBoilerplateGenerator.cs @@ -16,8 +16,6 @@ namespace Godot.SourceGenerators if (context.AreGodotSourceGeneratorsDisabled()) return; - // False positive for RS1024. We're already using `SymbolEqualityComparer.Default`... -#pragma warning disable RS1024 INamedTypeSymbol[] godotClasses = context .Compilation.SyntaxTrees .SelectMany(tree => @@ -28,7 +26,16 @@ namespace Godot.SourceGenerators .Where(x => { if (x.cds.IsPartial()) + { + if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial)) + { + Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!); + return false; + } + return true; + } + Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol); return false; }) @@ -36,7 +43,6 @@ namespace Godot.SourceGenerators ) .Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default) .ToArray(); -#pragma warning restore RS1024 if (godotClasses.Length > 0) { @@ -63,6 +69,8 @@ namespace Godot.SourceGenerators string.Empty; bool hasNamespace = classNs.Length != 0; + bool isInnerClass = symbol.ContainingType != null; + string uniqueName = hasNamespace ? classNs + "." + className + "_ScriptBoilerplate_Generated" : className + "_ScriptBoilerplate_Generated"; @@ -80,6 +88,22 @@ namespace Godot.SourceGenerators source.Append(" {\n\n"); } + if (isInnerClass) + { + var containingType = symbol.ContainingType; + + while (containingType != null) + { + source.Append("partial "); + source.Append(containingType.GetDeclarationKeyword()); + source.Append(" "); + source.Append(containingType.Name); + source.Append("\n{\n"); + + containingType = containingType.ContainingType; + } + } + source.Append("partial class "); source.Append(className); source.Append("\n{\n"); @@ -102,15 +126,15 @@ namespace Godot.SourceGenerators .Cast<IFieldSymbol>() .Where(p => !p.IsImplicitlyDeclared); - var methods = WhereHasCompatibleGodotType(methodSymbols, typeCache).ToArray(); - var properties = WhereIsCompatibleGodotType(propertySymbols, typeCache).ToArray(); - var fields = WhereIsCompatibleGodotType(fieldSymbols, typeCache).ToArray(); + var godotClassMethods = WhereHasCompatibleGodotType(methodSymbols, typeCache).ToArray(); + var godotClassProperties = WhereIsCompatibleGodotType(propertySymbols, typeCache).ToArray(); + var godotClassFields = WhereIsCompatibleGodotType(fieldSymbols, typeCache).ToArray(); source.Append(" private class GodotInternal {\n"); // Generate cached StringNames for methods and properties, for fast lookup - foreach (var method in methods) + foreach (var method in godotClassMethods) { string methodName = method.Method.Name; source.Append(" public static readonly StringName MethodName_"); @@ -120,7 +144,7 @@ namespace Godot.SourceGenerators source.Append("\";\n"); } - foreach (var property in properties) + foreach (var property in godotClassProperties) { string propertyName = property.Property.Name; source.Append(" public static readonly StringName PropName_"); @@ -130,7 +154,7 @@ namespace Godot.SourceGenerators source.Append("\";\n"); } - foreach (var field in fields) + foreach (var field in godotClassFields) { string fieldName = field.Field.Name; source.Append(" public static readonly StringName PropName_"); @@ -140,14 +164,16 @@ namespace Godot.SourceGenerators source.Append("\";\n"); } - source.Append(" }\n"); + source.Append(" }\n"); // class GodotInternal + + // Generate InvokeGodotClassMethod - if (methods.Length > 0) + if (godotClassMethods.Length > 0) { source.Append(" protected override bool InvokeGodotClassMethod(in godot_string_name method, "); source.Append("NativeVariantPtrArgs args, int argCount, out godot_variant ret)\n {\n"); - foreach (var method in methods) + foreach (var method in godotClassMethods) { GenerateMethodInvoker(method, source); } @@ -157,42 +183,64 @@ namespace Godot.SourceGenerators source.Append(" }\n"); } - if (properties.Length > 0 || fields.Length > 0) + // Generate Set/GetGodotClassPropertyValue + + if (godotClassProperties.Length > 0 || godotClassFields.Length > 0) { + bool isFirstEntry; + // Setters - source.Append(" protected override bool SetGodotClassPropertyValue(in godot_string_name name, "); - source.Append("in godot_variant value)\n {\n"); + bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.Field.IsReadOnly) && + godotClassProperties.All(pi => pi.Property.IsReadOnly); - foreach (var property in properties) + if (!allPropertiesAreReadOnly) { - GeneratePropertySetter(property.Property.Name, - property.Property.Type.FullQualifiedName(), source); - } + source.Append(" protected override bool SetGodotClassPropertyValue(in godot_string_name name, "); + source.Append("in godot_variant value)\n {\n"); - foreach (var field in fields) - { - GeneratePropertySetter(field.Field.Name, - field.Field.Type.FullQualifiedName(), source); - } + isFirstEntry = true; + foreach (var property in godotClassProperties) + { + if (property.Property.IsReadOnly) + continue; - source.Append(" return base.SetGodotClassPropertyValue(name, value);\n"); + GeneratePropertySetter(property.Property.Name, + property.Property.Type.FullQualifiedName(), source, isFirstEntry); + isFirstEntry = false; + } - source.Append(" }\n"); + foreach (var field in godotClassFields) + { + if (field.Field.IsReadOnly) + continue; + + GeneratePropertySetter(field.Field.Name, + field.Field.Type.FullQualifiedName(), source, isFirstEntry); + isFirstEntry = false; + } + + source.Append(" return base.SetGodotClassPropertyValue(name, value);\n"); + + source.Append(" }\n"); + } // Getters source.Append(" protected override bool GetGodotClassPropertyValue(in godot_string_name name, "); source.Append("out godot_variant value)\n {\n"); - foreach (var property in properties) + isFirstEntry = true; + foreach (var property in godotClassProperties) { - GeneratePropertyGetter(property.Property.Name, source); + GeneratePropertyGetter(property.Property.Name, source, isFirstEntry); + isFirstEntry = false; } - foreach (var field in fields) + foreach (var field in godotClassFields) { - GeneratePropertyGetter(field.Field.Name, source); + GeneratePropertyGetter(field.Field.Name, source, isFirstEntry); + isFirstEntry = false; } source.Append(" return base.GetGodotClassPropertyValue(name, out value);\n"); @@ -200,7 +248,37 @@ namespace Godot.SourceGenerators source.Append(" }\n"); } - source.Append("}\n"); + // Generate HasGodotClassMethod + + if (godotClassMethods.Length > 0) + { + source.Append(" protected override bool HasGodotClassMethod(in godot_string_name method)\n {\n"); + + bool isFirstEntry = true; + foreach (var method in godotClassMethods) + { + GenerateHasMethodEntry(method, source, isFirstEntry); + isFirstEntry = false; + } + + source.Append(" return base.HasGodotClassMethod(method);\n"); + + source.Append(" }\n"); + } + + source.Append("}\n"); // partial class + + if (isInnerClass) + { + var containingType = symbol.ContainingType; + + while (containingType != null) + { + source.Append("}\n"); // outer class + + containingType = containingType.ContainingType; + } + } if (hasNamespace) { @@ -269,10 +347,14 @@ namespace Godot.SourceGenerators private static void GeneratePropertySetter( string propertyMemberName, string propertyTypeQualifiedName, - StringBuilder source + StringBuilder source, + bool isFirstEntry ) { - source.Append(" if (name == GodotInternal.PropName_"); + source.Append(" "); + if (!isFirstEntry) + source.Append("else "); + source.Append("if (name == GodotInternal.PropName_"); source.Append(propertyMemberName); source.Append(") {\n"); @@ -295,10 +377,14 @@ namespace Godot.SourceGenerators private static void GeneratePropertyGetter( string propertyMemberName, - StringBuilder source + StringBuilder source, + bool isFirstEntry ) { - source.Append(" if (name == GodotInternal.PropName_"); + source.Append(" "); + if (!isFirstEntry) + source.Append("else "); + source.Append("if (name == GodotInternal.PropName_"); source.Append(propertyMemberName); source.Append(") {\n"); @@ -312,6 +398,22 @@ namespace Godot.SourceGenerators source.Append(" }\n"); } + private static void GenerateHasMethodEntry( + GodotMethodInfo method, + StringBuilder source, + bool isFirstEntry + ) + { + string methodName = method.Method.Name; + + source.Append(" "); + if (!isFirstEntry) + source.Append("else "); + source.Append("if (method == GodotInternal.MethodName_"); + source.Append(methodName); + source.Append(") {\n return true;\n }\n"); + } + public void Initialize(GeneratorInitializationContext context) { } @@ -376,12 +478,17 @@ namespace Godot.SourceGenerators var parameters = method.Parameters; - var paramTypes = parameters.Select(p => - MarshalUtils.ConvertManagedTypeToVariantType(p.Type, typeCache)) + var paramTypes = parameters + // Currently we don't support `ref`, `out`, `in`, `ref readonly` parameters (and we never may) + .Where(p => p.RefKind == RefKind.None) + // Attempt to determine the variant type + .Select(p => MarshalUtils.ConvertManagedTypeToVariantType(p.Type, typeCache)) + // Discard parameter types that couldn't be determined (null entries) .Where(t => t != null).Cast<MarshalType>().ToImmutableArray(); + // If any parameter type was incompatible, it was discarded so the length won't match if (parameters.Length > paramTypes.Length) - continue; // Some param types weren't compatible + continue; // Ignore incompatible method yield return new GodotMethodInfo(method, paramTypes, parameters .Select(p => p.Type).ToImmutableArray(), retType); @@ -395,6 +502,10 @@ namespace Godot.SourceGenerators { foreach (var property in properties) { + // Ignore properties without a getter. Godot properties must be readable. + if (property.IsWriteOnly) + continue; + var marshalType = MarshalUtils.ConvertManagedTypeToVariantType(property.Type, typeCache); if (marshalType == null) diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs index f0b2758342..b625287087 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs @@ -17,6 +17,9 @@ namespace Godot.SourceGenerators if (context.AreGodotSourceGeneratorsDisabled()) return; + if (context.IsGodotToolsProject()) + return; + // NOTE: NotNullWhen diagnostics don't work on projects targeting .NET Standard 2.0 // ReSharper disable once ReplaceWithStringIsNullOrEmpty if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out string? godotProjectDir) @@ -31,7 +34,7 @@ namespace Godot.SourceGenerators tree.GetRoot().DescendantNodes() .OfType<ClassDeclarationSyntax>() // Ignore inner classes - .Where(cds => !(cds.Parent is ClassDeclarationSyntax)) + .Where(cds => !cds.IsNested()) .SelectGodotScriptClasses(context.Compilation) // Report and skip non-partial classes .Where(x => diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index c549cf5f12..d921e2f6a2 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.Text; using Microsoft.Build.Construction; @@ -45,7 +46,7 @@ namespace GodotTools.ProjectEditor // Save (without BOM) root.Save(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); - return Guid.NewGuid().ToString().ToUpper(); + return Guid.NewGuid().ToString().ToUpperInvariant(); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index 9b921c517c..7b1d5c228a 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -19,8 +19,15 @@ namespace GodotTools.ProjectEditor public static class ProjectUtils { - public static void MSBuildLocatorRegisterDefaults() - => Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults(); + public static void MSBuildLocatorRegisterDefaults(out Version version, out string path) + { + var instance = Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults(); + version = instance.Version; + path = instance.MSBuildPath; + } + + public static void MSBuildLocatorRegisterMSBuildPath(string msbuildPath) + => Microsoft.Build.Locator.MSBuildLocator.RegisterMSBuildPath(msbuildPath); public static MSBuildProject Open(string path) { diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln index 415e49b426..f3d2f90f39 100644 --- a/modules/mono/editor/GodotTools/GodotTools.sln +++ b/modules/mono/editor/GodotTools/GodotTools.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.OpenVisualStudio EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Shared", "GodotTools.Shared\GodotTools.Shared.csproj", "{2758FFAF-8237-4CF2-B569-66BF8B3587BB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators", "..\Godot.NET.Sdk\Godot.SourceGenerators\Godot.SourceGenerators.csproj", "{D8C421B2-8911-41EB-B983-F675C7141EB7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,5 +51,9 @@ Global {2758FFAF-8237-4CF2-B569-66BF8B3587BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {2758FFAF-8237-4CF2-B569-66BF8B3587BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {2758FFAF-8237-4CF2-B569-66BF8B3587BB}.Release|Any CPU.Build.0 = Release|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs index 28bf57dc21..18073d6492 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs @@ -4,26 +4,36 @@ using Godot.Collections; using GodotTools.Internals; using Path = System.IO.Path; +#nullable enable + namespace GodotTools.Build { [Serializable] - public sealed class BuildInfo : RefCounted // TODO Remove RefCounted once we have proper serialization + public sealed partial class BuildInfo : RefCounted // TODO Remove RefCounted once we have proper serialization { public string Solution { get; } - public string[] Targets { get; } public string Configuration { get; } + public string? RuntimeIdentifier { get; } + public string? PublishOutputDir { get; } public bool Restore { get; } + public bool Rebuild { get; } + public bool OnlyClean { get; } + // TODO Use List once we have proper serialization public Array<string> CustomProperties { get; } = new Array<string>(); - public string LogsDirPath => Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}"); + public string LogsDirPath => + Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}"); - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is BuildInfo other) - return other.Solution == Solution && other.Targets == Targets && - other.Configuration == Configuration && other.Restore == Restore && - other.CustomProperties == CustomProperties && other.LogsDirPath == LogsDirPath; + return other.Solution == Solution && + other.Configuration == Configuration && other.RuntimeIdentifier == RuntimeIdentifier && + other.PublishOutputDir == PublishOutputDir && other.Restore == Restore && + other.Rebuild == Rebuild && other.OnlyClean == OnlyClean && + other.CustomProperties == CustomProperties && + other.LogsDirPath == LogsDirPath; return false; } @@ -34,25 +44,37 @@ namespace GodotTools.Build { int hash = 17; hash = (hash * 29) + Solution.GetHashCode(); - hash = (hash * 29) + Targets.GetHashCode(); hash = (hash * 29) + Configuration.GetHashCode(); + hash = (hash * 29) + (RuntimeIdentifier?.GetHashCode() ?? 0); + hash = (hash * 29) + (PublishOutputDir?.GetHashCode() ?? 0); hash = (hash * 29) + Restore.GetHashCode(); + hash = (hash * 29) + Rebuild.GetHashCode(); + hash = (hash * 29) + OnlyClean.GetHashCode(); hash = (hash * 29) + CustomProperties.GetHashCode(); hash = (hash * 29) + LogsDirPath.GetHashCode(); return hash; } } - private BuildInfo() + public BuildInfo(string solution, string configuration, bool restore, bool rebuild, bool onlyClean) { + Solution = solution; + Configuration = configuration; + Restore = restore; + Rebuild = rebuild; + OnlyClean = onlyClean; } - public BuildInfo(string solution, string[] targets, string configuration, bool restore) + public BuildInfo(string solution, string configuration, string runtimeIdentifier, + string publishOutputDir, bool restore, bool rebuild, bool onlyClean) { Solution = solution; - Targets = targets; Configuration = configuration; + RuntimeIdentifier = runtimeIdentifier; + PublishOutputDir = publishOutputDir; Restore = restore; + Rebuild = rebuild; + OnlyClean = onlyClean; } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs index 58677625c6..43256953f5 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs @@ -2,11 +2,9 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; -using GodotTools.Ides.Rider; +using Godot; using GodotTools.Internals; -using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; -using OS = GodotTools.Utils.OS; namespace GodotTools.Build { @@ -14,13 +12,8 @@ namespace GodotTools.Build { private static BuildInfo _buildInProgress; - public const string PropNameMSBuildMono = "MSBuild (Mono)"; - public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)"; - public const string PropNameMSBuildJetBrains = "MSBuild (JetBrains Rider)"; - public const string PropNameDotnetCli = "dotnet CLI"; - public const string MsBuildIssuesFileName = "msbuild_issues.csv"; - public const string MsBuildLogFileName = "msbuild_log.txt"; + private const string MsBuildLogFileName = "msbuild_log.txt"; public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason); @@ -62,11 +55,11 @@ namespace GodotTools.Build private static void PrintVerbose(string text) { - if (Godot.OS.IsStdoutVerbose()) - Godot.GD.Print(text); + if (OS.IsStdoutVerbose()) + GD.Print(text); } - public static bool Build(BuildInfo buildInfo) + private static bool Build(BuildInfo buildInfo) { if (_buildInProgress != null) throw new InvalidOperationException("A build is already in progress"); @@ -103,7 +96,8 @@ namespace GodotTools.Build } catch (Exception e) { - BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); + BuildLaunchFailed?.Invoke(buildInfo, + $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); Console.Error.WriteLine(e); return false; } @@ -148,7 +142,8 @@ namespace GodotTools.Build } catch (Exception e) { - BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); + BuildLaunchFailed?.Invoke(buildInfo, + $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); Console.Error.WriteLine(e); return false; } @@ -159,18 +154,54 @@ namespace GodotTools.Build } } - public static bool BuildProjectBlocking(string config, [MaybeNull] string[] targets = null, [MaybeNull] string platform = null) + private static bool Publish(BuildInfo buildInfo) { - var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets ?? new[] {"Build"}, config, restore: true); + if (_buildInProgress != null) + throw new InvalidOperationException("A build is already in progress"); - // If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it. - if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform)) - buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}"); + _buildInProgress = buildInfo; - if (Internal.GodotIsRealTDouble()) - buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); + try + { + BuildStarted?.Invoke(buildInfo); + + // Required in order to update the build tasks list + Internal.GodotMainIteration(); - return BuildProjectBlocking(buildInfo); + try + { + RemoveOldIssuesFile(buildInfo); + } + catch (IOException e) + { + BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); + Console.Error.WriteLine(e); + } + + try + { + int exitCode = BuildSystem.Publish(buildInfo, StdOutputReceived, StdErrorReceived); + + if (exitCode != 0) + PrintVerbose( + $"dotnet publish exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); + + BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error); + + return exitCode == 0; + } + catch (Exception e) + { + BuildLaunchFailed?.Invoke(buildInfo, + $"The publish method threw an exception.\n{e.GetType().FullName}: {e.Message}"); + Console.Error.WriteLine(e); + return false; + } + } + finally + { + _buildInProgress = null; + } } private static bool BuildProjectBlocking(BuildInfo buildInfo) @@ -178,20 +209,109 @@ namespace GodotTools.Build if (!File.Exists(buildInfo.Solution)) return true; // No solution to build - using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1)) + using var pr = new EditorProgress("dotnet_build_project", "Building .NET project...", 1); + + pr.Step("Building project solution", 0); + + if (!Build(buildInfo)) { - pr.Step("Building project solution", 0); + ShowBuildErrorDialog("Failed to build project solution"); + return false; + } - if (!Build(buildInfo)) - { - ShowBuildErrorDialog("Failed to build project solution"); - return false; - } + return true; + } + + private static bool CleanProjectBlocking(BuildInfo buildInfo) + { + if (!File.Exists(buildInfo.Solution)) + return true; // No solution to clean + + using var pr = new EditorProgress("dotnet_clean_project", "Cleaning .NET project...", 1); + + pr.Step("Cleaning project solution", 0); + + if (!Build(buildInfo)) + { + ShowBuildErrorDialog("Failed to clean project solution"); + return false; } return true; } + private static bool PublishProjectBlocking(BuildInfo buildInfo) + { + using var pr = new EditorProgress("dotnet_publish_project", "Publishing .NET project...", 1); + + pr.Step("Running dotnet publish", 0); + + if (!Publish(buildInfo)) + { + ShowBuildErrorDialog("Failed to publish .NET project"); + return false; + } + + return true; + } + + private static BuildInfo CreateBuildInfo( + [DisallowNull] string configuration, + [AllowNull] string platform = null, + bool rebuild = false, + bool onlyClean = false + ) + { + var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, configuration, + restore: true, rebuild, onlyClean); + + // If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it. + if (platform != null || Utils.OS.PlatformNameMap.TryGetValue(OS.GetName(), out platform)) + buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}"); + + if (Internal.GodotIsRealTDouble()) + buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); + + return buildInfo; + } + + private static BuildInfo CreatePublishBuildInfo( + [DisallowNull] string configuration, + [DisallowNull] string platform, + [DisallowNull] string runtimeIdentifier, + [DisallowNull] string publishOutputDir + ) + { + var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, configuration, + runtimeIdentifier, publishOutputDir, restore: true, rebuild: false, onlyClean: false); + + buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}"); + + if (Internal.GodotIsRealTDouble()) + buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); + + return buildInfo; + } + + public static bool BuildProjectBlocking( + [DisallowNull] string configuration, + [AllowNull] string platform = null, + bool rebuild = false + ) => BuildProjectBlocking(CreateBuildInfo(configuration, platform, rebuild)); + + public static bool CleanProjectBlocking( + [DisallowNull] string configuration, + [AllowNull] string platform = null + ) => CleanProjectBlocking(CreateBuildInfo(configuration, platform, rebuild: false)); + + public static bool PublishProjectBlocking( + [DisallowNull] string configuration, + [DisallowNull] string platform, + [DisallowNull] string runtimeIdentifier, + string publishOutputDir + ) => PublishProjectBlocking(CreatePublishBuildInfo(configuration, + platform, runtimeIdentifier, publishOutputDir)); + public static bool EditorBuildCallback() { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) @@ -204,7 +324,7 @@ namespace GodotTools.Build } catch (Exception e) { - Godot.GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); + GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); } if (GodotSharpEditor.Instance.SkipBuildBeforePlaying) @@ -215,47 +335,6 @@ namespace GodotTools.Build public static void Initialize() { - // Build tool settings - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - - BuildTool msbuildDefault; - - if (OS.IsWindows) - { - if (RiderPathManager.IsExternalEditorSetToRider(editorSettings)) - msbuildDefault = BuildTool.JetBrainsMsBuild; - else - msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildVs; - } - else - { - msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildMono; - } - - EditorDef("mono/builds/build_tool", msbuildDefault); - - string hintString; - - if (OS.IsWindows) - { - hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," + - $"{PropNameMSBuildVs}:{(int)BuildTool.MsBuildVs}," + - $"{PropNameMSBuildJetBrains}:{(int)BuildTool.JetBrainsMsBuild}," + - $"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}"; - } - else - { - hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," + - $"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}"; - } - - editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary - { - ["type"] = Godot.Variant.Type.Int, - ["name"] = "mono/builds/build_tool", - ["hint"] = Godot.PropertyHint.Enum, - ["hint_string"] = hintString - }); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs index ed5ee10585..f7b8c6bffd 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs @@ -8,10 +8,10 @@ using Path = System.IO.Path; namespace GodotTools.Build { - public class BuildOutputView : VBoxContainer, ISerializationListener + public partial class BuildOutputView : VBoxContainer, ISerializationListener { [Serializable] - private class BuildIssue : RefCounted // TODO Remove RefCounted once we have proper serialization + private partial class BuildIssue : RefCounted // TODO Remove RefCounted once we have proper serialization { public bool Warning { get; set; } public string File { get; set; } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 0e793a44ba..d2e3ecff57 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -1,61 +1,93 @@ -using GodotTools.Core; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; +using System.Globalization; using System.IO; +using System.Text; using System.Threading.Tasks; using GodotTools.BuildLogger; -using GodotTools.Internals; using GodotTools.Utils; -using Directory = System.IO.Directory; namespace GodotTools.Build { public static class BuildSystem { - private static string MonoWindowsBinDir + private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, + Action<string> stdErrHandler) { - get - { - string monoWinBinDir = Path.Combine(Internal.MonoWindowsInstallRoot, "bin"); + string dotnetPath = DotNetFinder.FindDotNetExe(); - if (!Directory.Exists(monoWinBinDir)) - throw new FileNotFoundException("Cannot find the Windows Mono install bin directory."); + if (dotnetPath == null) + throw new FileNotFoundException("Cannot find the dotnet executable."); - return monoWinBinDir; - } + var startInfo = new ProcessStartInfo(dotnetPath); + + BuildArguments(buildInfo, startInfo.ArgumentList); + + string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Running: ")).ToString(); + stdOutHandler?.Invoke(launchMessage); + if (Godot.OS.IsStdoutVerbose()) + Console.WriteLine(launchMessage); + + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + + // Needed when running from Developer Command Prompt for VS + RemovePlatformVariable(startInfo.EnvironmentVariables); + + var process = new Process { StartInfo = startInfo }; + + if (stdOutHandler != null) + process.OutputDataReceived += (s, e) => stdOutHandler.Invoke(e.Data); + if (stdErrHandler != null) + process.ErrorDataReceived += (s, e) => stdErrHandler.Invoke(e.Data); + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + return process; } - private static Godot.EditorSettings EditorSettings => - GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); + public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + { + using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) + { + process.WaitForExit(); + + return process.ExitCode; + } + } - private static bool UsingMonoMsBuildOnWindows + public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, + Action<string> stdErrHandler) { - get + using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) { - if (OS.IsWindows) - { - return (BuildTool)EditorSettings.GetSetting("mono/builds/build_tool") - == BuildTool.MsBuildMono; - } + await process.WaitForExitAsync(); - return false; + return process.ExitCode; } } - private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + private static Process LaunchPublish(BuildInfo buildInfo, Action<string> stdOutHandler, + Action<string> stdErrHandler) { - (string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild(); + string dotnetPath = DotNetFinder.FindDotNetExe(); - if (msbuildPath == null) - throw new FileNotFoundException("Cannot find the MSBuild executable."); + if (dotnetPath == null) + throw new FileNotFoundException("Cannot find the dotnet executable."); - string compilerArgs = BuildArguments(buildTool, buildInfo); + var startInfo = new ProcessStartInfo(dotnetPath); - var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs); + BuildPublishArguments(buildInfo, startInfo.ArgumentList); - string launchMessage = $"Running: \"{startInfo.FileName}\" {startInfo.Arguments}"; + string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Running: ")).ToString(); stdOutHandler?.Invoke(launchMessage); if (Godot.OS.IsStdoutVerbose()) Console.WriteLine(launchMessage); @@ -63,22 +95,11 @@ namespace GodotTools.Build startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; startInfo.UseShellExecute = false; - startInfo.CreateNoWindow = true; - - if (UsingMonoMsBuildOnWindows) - { - // These environment variables are required for Mono's MSBuild to find the compilers. - // We use the batch files in Mono's bin directory to make sure the compilers are executed with mono. - string monoWinBinDir = MonoWindowsBinDir; - startInfo.EnvironmentVariables.Add("CscToolExe", Path.Combine(monoWinBinDir, "csc.bat")); - startInfo.EnvironmentVariables.Add("VbcToolExe", Path.Combine(monoWinBinDir, "vbc.bat")); - startInfo.EnvironmentVariables.Add("FscToolExe", Path.Combine(monoWinBinDir, "fsharpc.bat")); - } // Needed when running from Developer Command Prompt for VS RemovePlatformVariable(startInfo.EnvironmentVariables); - var process = new Process {StartInfo = startInfo}; + var process = new Process { StartInfo = startInfo }; if (stdOutHandler != null) process.OutputDataReceived += (s, e) => stdOutHandler.Invoke(e.Data); @@ -93,9 +114,9 @@ namespace GodotTools.Build return process; } - public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + public static int Publish(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) { - using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) + using (var process = LaunchPublish(buildInfo, stdOutHandler, stdErrHandler)) { process.WaitForExit(); @@ -103,46 +124,105 @@ namespace GodotTools.Build } } - public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + private static void BuildArguments(BuildInfo buildInfo, Collection<string> arguments) { - using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) + // `dotnet clean` / `dotnet build` commands + arguments.Add(buildInfo.OnlyClean ? "clean" : "build"); + + // Solution + arguments.Add(buildInfo.Solution); + + // `dotnet clean` doesn't recognize these options + if (!buildInfo.OnlyClean) { - await process.WaitForExitAsync(); + // Restore + // `dotnet build` restores by default, unless requested not to + if (!buildInfo.Restore) + arguments.Add("--no-restore"); + + // Incremental or rebuild + if (buildInfo.Rebuild) + arguments.Add("--no-incremental"); + } - return process.ExitCode; + // Configuration + arguments.Add("-c"); + arguments.Add(buildInfo.Configuration); + + // Verbosity + arguments.Add("-v"); + arguments.Add("normal"); + + // Logger + AddLoggerArgument(buildInfo, arguments); + + // Custom properties + foreach (string customProperty in buildInfo.CustomProperties) + { + arguments.Add("-p:" + customProperty); } } - private static string BuildArguments(BuildTool buildTool, BuildInfo buildInfo) + private static void BuildPublishArguments(BuildInfo buildInfo, Collection<string> arguments) { - string arguments = string.Empty; + arguments.Add("publish"); // `dotnet publish` command + + // Solution + arguments.Add(buildInfo.Solution); + + // Restore + // `dotnet publish` restores by default, unless requested not to + if (!buildInfo.Restore) + arguments.Add("--no-restore"); + + // Incremental or rebuild + if (buildInfo.Rebuild) + arguments.Add("--no-incremental"); - if (buildTool == BuildTool.DotnetCli) - arguments += "msbuild"; // `dotnet msbuild` command + // Configuration + arguments.Add("-c"); + arguments.Add(buildInfo.Configuration); - arguments += $@" ""{buildInfo.Solution}"""; + // Runtime Identifier + arguments.Add("-r"); + arguments.Add(buildInfo.RuntimeIdentifier!); - if (buildInfo.Restore) - arguments += " /restore"; + // Self-published + arguments.Add("--self-contained"); + arguments.Add("true"); - arguments += $@" /t:{string.Join(",", buildInfo.Targets)} " + - $@"""/p:{"Configuration=" + buildInfo.Configuration}"" /v:normal " + - $@"""{AddLoggerArgument(buildInfo)}"""; + // Verbosity + arguments.Add("-v"); + arguments.Add("normal"); + // Logger + AddLoggerArgument(buildInfo, arguments); + + // Trimming is not supported for dynamically loaded assemblies, as is our case with self hosting: + // https://github.com/dotnet/runtime/blob/main/docs/design/features/native-hosting.md#incompatible-with-trimming + arguments.Add("-p:PublishTrimmed=false"); + + // Custom properties foreach (string customProperty in buildInfo.CustomProperties) { - arguments += " /p:" + customProperty; + arguments.Add("-p:" + customProperty); } - return arguments; + // Publish output directory + if (buildInfo.PublishOutputDir != null) + { + arguments.Add("-o"); + arguments.Add(buildInfo.PublishOutputDir); + } } - private static string AddLoggerArgument(BuildInfo buildInfo) + private static void AddLoggerArgument(BuildInfo buildInfo, Collection<string> arguments) { - string buildLoggerPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, + string buildLoggerPath = Path.Combine(Internals.GodotSharpDirs.DataEditorToolsDir, "GodotTools.BuildLogger.dll"); - return $"/l:{typeof(GodotBuildLogger).FullName},{buildLoggerPath};{buildInfo.LogsDirPath}"; + arguments.Add( + $"-l:{typeof(GodotBuildLogger).FullName},{buildLoggerPath};{buildInfo.LogsDirPath}"); } private static void RemovePlatformVariable(StringDictionary environmentVariables) @@ -153,7 +233,7 @@ namespace GodotTools.Build foreach (string env in environmentVariables.Keys) { - if (env.ToUpper() == "PLATFORM") + if (env.ToUpperInvariant() == "PLATFORM") platformEnvironmentVariables.Add(env); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs deleted file mode 100644 index 837c8adddb..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace GodotTools.Build -{ - public enum BuildTool : long - { - MsBuildMono, - MsBuildVs, - JetBrainsMsBuild, - DotnetCli - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs new file mode 100644 index 0000000000..7bce53308c --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using JetBrains.Annotations; +using OS = GodotTools.Utils.OS; + +namespace GodotTools.Build +{ + public static class DotNetFinder + { + [CanBeNull] + public static string FindDotNetExe() + { + // In the future, this method may do more than just search in PATH. We could look in + // known locations or use Godot's linked nethost to search from the hostfxr location. + + return OS.PathWhich("dotnet"); + } + + public static bool TryFindDotNetSdk( + Version expectedVersion, + [NotNullWhen(true)] out Version version, + [NotNullWhen(true)] out string path + ) + { + version = null; + path = null; + + string dotNetExe = FindDotNetExe(); + + if (string.IsNullOrEmpty(dotNetExe)) + return false; + + using Process process = new Process(); + process.StartInfo = new ProcessStartInfo(dotNetExe, "--list-sdks") + { + UseShellExecute = false, + RedirectStandardOutput = true + }; + + process.StartInfo.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"] = "en-US"; + + var lines = new List<string>(); + + process.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrWhiteSpace(e.Data)) + lines.Add(e.Data); + }; + + try + { + process.Start(); + } + catch + { + return false; + } + + process.BeginOutputReadLine(); + process.WaitForExit(); + + Version latestVersionMatch = null; + string matchPath = null; + + foreach (var line in lines) + { + string[] sdkLineParts = line.Trim() + .Split(' ', 2, StringSplitOptions.TrimEntries); + + if (sdkLineParts.Length < 2) + continue; + + if (!Version.TryParse(sdkLineParts[0], out var lineVersion)) + continue; + + // We're looking for the exact same major version + if (lineVersion.Major != expectedVersion.Major) + continue; + + if (latestVersionMatch != null && lineVersion < latestVersionMatch) + continue; + + latestVersionMatch = lineVersion; + matchPath = sdkLineParts[1].TrimStart('[').TrimEnd(']'); + } + + if (latestVersionMatch == null) + return false; + + version = latestVersionMatch; + path = Path.Combine(matchPath!, version.ToString()); + + return true; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs index 13b3ab7da2..6dae0a3a0e 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs @@ -6,7 +6,7 @@ using File = GodotTools.Utils.File; namespace GodotTools.Build { - public class MSBuildPanel : VBoxContainer + public partial class MSBuildPanel : VBoxContainer { public BuildOutputView BuildOutputView { get; private set; } @@ -70,7 +70,7 @@ namespace GodotTools.Build GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); } - if (!BuildManager.BuildProjectBlocking("Debug", targets: new[] { "Rebuild" })) + if (!BuildManager.BuildProjectBlocking("Debug", rebuild: true)) return; // Build failed // Notify running game for hot-reload @@ -88,7 +88,7 @@ namespace GodotTools.Build if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) return; // No solution to build - BuildManager.BuildProjectBlocking("Debug", targets: new[] { "Clean" }); + _ = BuildManager.CleanProjectBlocking("Debug"); } private void ViewLogToggled(bool pressed) => BuildOutputView.LogVisible = pressed; diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs deleted file mode 100644 index a859c6f717..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Godot; -using GodotTools.Ides.Rider; -using GodotTools.Internals; -using Directory = System.IO.Directory; -using Environment = System.Environment; -using File = System.IO.File; -using Path = System.IO.Path; -using OS = GodotTools.Utils.OS; - -namespace GodotTools.Build -{ - public static class MsBuildFinder - { - private static string _msbuildToolsPath = string.Empty; - private static string _msbuildUnixPath = string.Empty; - - public static (string, BuildTool) FindMsBuild() - { - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); - - if (OS.IsWindows) - { - switch (buildTool) - { - case BuildTool.DotnetCli: - { - string dotnetCliPath = OS.PathWhich("dotnet"); - if (!string.IsNullOrEmpty(dotnetCliPath)) - return (dotnetCliPath, BuildTool.DotnetCli); - GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Visual Studio."); - goto case BuildTool.MsBuildVs; - } - case BuildTool.MsBuildVs: - { - if (string.IsNullOrEmpty(_msbuildToolsPath) || !File.Exists(_msbuildToolsPath)) - { - // Try to search it again if it wasn't found last time or if it was removed from its location - _msbuildToolsPath = FindMsBuildToolsPathOnWindows(); - - if (string.IsNullOrEmpty(_msbuildToolsPath)) - throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildVs}'."); - } - - if (!_msbuildToolsPath.EndsWith("\\")) - _msbuildToolsPath += "\\"; - - return (Path.Combine(_msbuildToolsPath, "MSBuild.exe"), BuildTool.MsBuildVs); - } - case BuildTool.MsBuildMono: - { - string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat"); - - if (!File.Exists(msbuildPath)) - throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildMono}'. Tried with path: {msbuildPath}"); - - return (msbuildPath, BuildTool.MsBuildMono); - } - case BuildTool.JetBrainsMsBuild: - { - string editorPath = (string)editorSettings.GetSetting(RiderPathManager.EditorPathSettingName); - - if (!File.Exists(editorPath)) - throw new FileNotFoundException($"Cannot find Rider executable. Tried with path: {editorPath}"); - - var riderDir = new FileInfo(editorPath).Directory?.Parent; - - string msbuildPath = Path.Combine(riderDir.FullName, @"tools\MSBuild\Current\Bin\MSBuild.exe"); - - if (!File.Exists(msbuildPath)) - throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildJetBrains}'. Tried with path: {msbuildPath}"); - - return (msbuildPath, BuildTool.JetBrainsMsBuild); - } - default: - throw new IndexOutOfRangeException("Invalid build tool in editor settings"); - } - } - - if (OS.IsUnixLike) - { - switch (buildTool) - { - case BuildTool.DotnetCli: - { - string dotnetCliPath = FindBuildEngineOnUnix("dotnet"); - if (!string.IsNullOrEmpty(dotnetCliPath)) - return (dotnetCliPath, BuildTool.DotnetCli); - GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Mono."); - goto case BuildTool.MsBuildMono; - } - case BuildTool.MsBuildMono: - { - if (string.IsNullOrEmpty(_msbuildUnixPath) || !File.Exists(_msbuildUnixPath)) - { - // Try to search it again if it wasn't found last time or if it was removed from its location - _msbuildUnixPath = FindBuildEngineOnUnix("msbuild"); - } - - if (string.IsNullOrEmpty(_msbuildUnixPath)) - throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMSBuildMono}'"); - - return (_msbuildUnixPath, BuildTool.MsBuildMono); - } - default: - throw new IndexOutOfRangeException("Invalid build tool in editor settings"); - } - } - - throw new PlatformNotSupportedException(); - } - - private static IEnumerable<string> MsBuildHintDirs - { - get - { - var result = new List<string>(); - - if (OS.IsMacOS) - { - result.Add("/Library/Frameworks/Mono.framework/Versions/Current/bin/"); - result.Add("/opt/local/bin/"); - result.Add("/usr/local/var/homebrew/linked/mono/bin/"); - result.Add("/usr/local/bin/"); - result.Add("/usr/local/bin/dotnet/"); - result.Add("/usr/local/share/dotnet/"); - } - - result.Add("/opt/novell/mono/bin/"); - - return result; - } - } - - private static string FindBuildEngineOnUnix(string name) - { - string ret = OS.PathWhich(name); - - if (!string.IsNullOrEmpty(ret)) - return ret; - - string retFallback = OS.PathWhich($"{name}.exe"); - - if (!string.IsNullOrEmpty(retFallback)) - return retFallback; - - foreach (string hintDir in MsBuildHintDirs) - { - string hintPath = Path.Combine(hintDir, name); - - if (File.Exists(hintPath)) - return hintPath; - } - - return string.Empty; - } - - private static string FindMsBuildToolsPathOnWindows() - { - if (!OS.IsWindows) - throw new PlatformNotSupportedException(); - - // Try to find 15.0 with vswhere - - string[] envNames = Internal.GodotIs32Bits() ? - envNames = new[] { "ProgramFiles", "ProgramW6432" } : - envNames = new[] { "ProgramFiles(x86)", "ProgramFiles" }; - - string vsWherePath = null; - foreach (var envName in envNames) - { - vsWherePath = Environment.GetEnvironmentVariable(envName); - if (!string.IsNullOrEmpty(vsWherePath)) - { - vsWherePath += "\\Microsoft Visual Studio\\Installer\\vswhere.exe"; - if (File.Exists(vsWherePath)) - break; - } - - vsWherePath = null; - } - - var vsWhereArgs = new[] {"-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"}; - - var outputArray = new Godot.Collections.Array<string>(); - int exitCode = Godot.OS.Execute(vsWherePath, vsWhereArgs, - output: (Godot.Collections.Array)outputArray); - - if (exitCode != 0) - return string.Empty; - - if (outputArray.Count == 0) - return string.Empty; - - var lines = outputArray[0].Split('\n'); - - foreach (string line in lines) - { - int sepIdx = line.IndexOf(':'); - - if (sepIdx <= 0) - continue; - - string key = line.Substring(0, sepIdx); // No need to trim - - if (key != "installationPath") - continue; - - string value = line.Substring(sepIdx + 1).StripEdges(); - - if (string.IsNullOrEmpty(value)) - throw new FormatException("installationPath value is empty"); - - if (!value.EndsWith("\\")) - value += "\\"; - - // Since VS2019, the directory is simply named "Current" - string msbuildDir = Path.Combine(value, "MSBuild\\Current\\Bin"); - - if (Directory.Exists(msbuildDir)) - return msbuildDir; - - // Directory name "15.0" is used in VS 2017 - return Path.Combine(value, "MSBuild\\15.0\\Bin"); - } - - return string.Empty; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs b/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs index 63b97e981e..fdb86c8f34 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; @@ -39,7 +40,8 @@ namespace GodotTools.Build // Since this can be considered pretty much a new NuGet.Config, add the default nuget.org source as well XmlElement nugetOrgSourceEntry = xmlDoc.CreateElement("add"); nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("key")).Value = "nuget.org"; - nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("value")).Value = "https://api.nuget.org/v3/index.json"; + nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("value")).Value = + "https://api.nuget.org/v3/index.json"; nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("protocolVersion")).Value = "3"; rootNode.AppendChild(xmlDoc.CreateElement("packageSources")).AppendChild(nugetOrgSourceEntry); } @@ -181,8 +183,8 @@ namespace GodotTools.Build // - The sha512 of the nupkg is base64 encoded. // - We can get the nuspec from the nupkg which is a Zip file. - string packageIdLower = packageId.ToLower(); - string packageVersionLower = packageVersion.ToLower(); + string packageIdLower = packageId.ToLowerInvariant(); + string packageVersionLower = packageVersion.ToLowerInvariant(); string destDir = Path.Combine(fallbackFolder, packageIdLower, packageVersionLower); string nupkgDestPath = Path.Combine(destDir, $"{packageIdLower}.{packageVersionLower}.nupkg"); @@ -227,9 +229,11 @@ namespace GodotTools.Build var nuspecEntry = archive.GetEntry(packageId + ".nuspec"); if (nuspecEntry == null) - throw new InvalidOperationException($"Failed to extract package {packageId}.{packageVersion}. Could not find the nuspec file."); + throw new InvalidOperationException( + $"Failed to extract package {packageId}.{packageVersion}. Could not find the nuspec file."); - nuspecEntry.ExtractToFile(Path.Combine(destDir, nuspecEntry.Name.ToLower().SimplifyGodotPath())); + nuspecEntry.ExtractToFile(Path.Combine(destDir, nuspecEntry.Name + .ToLowerInvariant().SimplifyGodotPath())); // Extract the other package files diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index d8ebe762e1..edf3eeb7fa 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -1,12 +1,9 @@ using Godot; -using Godot.NativeInterop; using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using GodotTools.Build; using GodotTools.Core; using GodotTools.Internals; @@ -18,61 +15,13 @@ using Path = System.IO.Path; namespace GodotTools.Export { - public class ExportPlugin : EditorExportPlugin + public partial class ExportPlugin : EditorExportPlugin { - [Flags] - private enum I18NCodesets : long - { - None = 0, - CJK = 1, - MidEast = 2, - Other = 4, - Rare = 8, - West = 16, - All = CJK | MidEast | Other | Rare | West - } - - private string _maybeLastExportError; - - private void AddI18NAssemblies(Godot.Collections.Dictionary<string, string> assemblies, string bclDir) - { - var codesets = (I18NCodesets)ProjectSettings.GetSetting("mono/export/i18n_codesets"); - - if (codesets == I18NCodesets.None) - return; - - void AddI18NAssembly(string name) => assemblies.Add(name, Path.Combine(bclDir, $"{name}.dll")); - - AddI18NAssembly("I18N"); - - if ((codesets & I18NCodesets.CJK) != 0) - AddI18NAssembly("I18N.CJK"); - if ((codesets & I18NCodesets.MidEast) != 0) - AddI18NAssembly("I18N.MidEast"); - if ((codesets & I18NCodesets.Other) != 0) - AddI18NAssembly("I18N.Other"); - if ((codesets & I18NCodesets.Rare) != 0) - AddI18NAssembly("I18N.Rare"); - if ((codesets & I18NCodesets.West) != 0) - AddI18NAssembly("I18N.West"); - } - 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/export_assemblies_inside_pck", true); - - GlobalDef("mono/export/i18n_codesets", I18NCodesets.All); - - ProjectSettings.AddPropertyInfo(new Godot.Collections.Dictionary - { - ["type"] = Variant.Type.Int, - ["name"] = "mono/export/i18n_codesets", - ["hint"] = PropertyHint.Flags, - ["hint_string"] = "CJK,MidEast,Other,Rare,West" - }); GlobalDef("mono/export/aot/enabled", false); GlobalDef("mono/export/aot/full_aot", false); @@ -86,11 +35,7 @@ namespace GodotTools.Export GlobalDef("mono/export/aot/android_toolchain_path", ""); } - private void AddFile(string srcPath, string dstPath, bool remap = false) - { - // Add file to the PCK - AddFile(dstPath.Replace("\\", "/"), File.ReadAllBytes(srcPath), remap); - } + private string _maybeLastExportError; // With this method we can override how a file is exported in the PCK public override void _ExportFile(string path, string type, string[] features) @@ -155,178 +100,84 @@ namespace GodotTools.Export if (!DeterminePlatformFromFeatures(features, out string platform)) throw new NotSupportedException("Target platform not supported"); + if (!new[] { OS.Platforms.Windows, OS.Platforms.LinuxBSD, OS.Platforms.MacOS, OS.Platforms.Server } + .Contains(platform)) + { + throw new NotImplementedException("Target platform not yet implemented"); + } + string outputDir = new FileInfo(path).Directory?.FullName ?? - throw new FileNotFoundException("Base directory not found"); + throw new FileNotFoundException("Output base directory not found"); string buildConfig = isDebug ? "ExportDebug" : "ExportRelease"; - if (!BuildManager.BuildProjectBlocking(buildConfig, platform: platform)) - throw new Exception("Failed to build project"); + // TODO: This works for now, as we only implemented support for x86 family desktop so far, but it needs to be fixed + string arch = features.Contains("64") ? "x86_64" : "x86"; - // Add dependency assemblies + string ridOS = DetermineRuntimeIdentifierOS(platform); + string ridArch = DetermineRuntimeIdentifierArch(arch); + string runtimeIdentifier = $"{ridOS}-{ridArch}"; - var assemblies = new Godot.Collections.Dictionary<string, string>(); + // Create temporary publish output directory - string projectDllName = GodotSharpEditor.ProjectAssemblyName; - string projectDllSrcDir = Path.Combine(GodotSharpDirs.ResTempAssembliesBaseDir, buildConfig); - string projectDllSrcPath = Path.Combine(projectDllSrcDir, $"{projectDllName}.dll"); + string publishOutputTempDir = Path.Combine(Path.GetTempPath(), "godot-publish-dotnet", + $"{Process.GetCurrentProcess().Id}-{buildConfig}-{runtimeIdentifier}"); - assemblies[projectDllName] = projectDllSrcPath; + if (!Directory.Exists(publishOutputTempDir)) + Directory.CreateDirectory(publishOutputTempDir); - string bclDir = DeterminePlatformBclDir(platform); + // Execute dotnet publish - if (platform == OS.Platforms.Android) + if (!BuildManager.PublishProjectBlocking(buildConfig, platform, + runtimeIdentifier, publishOutputTempDir)) { - string godotAndroidExtProfileDir = GetBclProfileDir("godot_android_ext"); - string monoAndroidAssemblyPath = Path.Combine(godotAndroidExtProfileDir, "Mono.Android.dll"); - - if (!File.Exists(monoAndroidAssemblyPath)) - throw new FileNotFoundException("Assembly not found: 'Mono.Android'", monoAndroidAssemblyPath); - - assemblies["Mono.Android"] = monoAndroidAssemblyPath; + throw new Exception("Failed to build project"); } - else if (platform == OS.Platforms.HTML5) + + if (!File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpEditor.ProjectAssemblyName}.dll"))) { - // Ideally these would be added automatically since they're referenced by the wasm BCL assemblies. - // However, at least in the case of 'WebAssembly.Net.Http' for some reason the BCL assemblies - // reference a different version even though the assembly is the same, for some weird reason. - - var wasmFrameworkAssemblies = new[] { "WebAssembly.Bindings", "WebAssembly.Net.WebSockets" }; - - foreach (string thisWasmFrameworkAssemblyName in wasmFrameworkAssemblies) - { - string thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName + ".dll"); - if (!File.Exists(thisWasmFrameworkAssemblyPath)) - throw new FileNotFoundException($"Assembly not found: '{thisWasmFrameworkAssemblyName}'", - thisWasmFrameworkAssemblyPath); - assemblies[thisWasmFrameworkAssemblyName] = thisWasmFrameworkAssemblyPath; - } - - // Assemblies that can have a different name in a newer version. Newer version must come first and it has priority. - (string newName, string oldName)[] wasmFrameworkAssembliesOneOf = new[] - { - ("System.Net.Http.WebAssemblyHttpHandler", "WebAssembly.Net.Http") - }; - - foreach (var thisWasmFrameworkAssemblyName in wasmFrameworkAssembliesOneOf) - { - string thisWasmFrameworkAssemblyPath = - Path.Combine(bclDir, thisWasmFrameworkAssemblyName.newName + ".dll"); - if (File.Exists(thisWasmFrameworkAssemblyPath)) - { - assemblies[thisWasmFrameworkAssemblyName.newName] = thisWasmFrameworkAssemblyPath; - } - else - { - thisWasmFrameworkAssemblyPath = - Path.Combine(bclDir, thisWasmFrameworkAssemblyName.oldName + ".dll"); - if (!File.Exists(thisWasmFrameworkAssemblyPath)) - { - throw new FileNotFoundException( - "Expected one of the following assemblies but none were found: " + - $"'{thisWasmFrameworkAssemblyName.newName}' / '{thisWasmFrameworkAssemblyName.oldName}'", - thisWasmFrameworkAssemblyPath); - } - - assemblies[thisWasmFrameworkAssemblyName.oldName] = thisWasmFrameworkAssemblyPath; - } - } + throw new NotSupportedException( + "Publish succeeded but project assembly not found in the output directory"); } - // var initialAssemblies = assemblies.Duplicate(); - // godot_dictionary initialAssembliesAux = ((Godot.Collections.Dictionary)initialAssemblies).NativeValue; - // using godot_string buildConfigAux = Marshaling.ConvertStringToNative(buildConfig); - // using godot_string bclDirAux = Marshaling.ConvertStringToNative(bclDir); - // godot_dictionary assembliesAux = ((Godot.Collections.Dictionary)assemblies).NativeValue; - // TODO - throw new NotImplementedException(); - //internal_GetExportedAssemblyDependencies(initialAssembliesAux, buildConfigAux, bclDirAux, ref assembliesAux); - - AddI18NAssemblies(assemblies, bclDir); - - string outputDataDir = null; + // Copy all files from the dotnet publish output directory to + // a data directory next to the Godot output executable. - if (PlatformHasTemplateDir(platform)) - outputDataDir = ExportDataDirectory(features, platform, isDebug, outputDir); + string outputDataDir = Path.Combine(outputDir, DetermineDataDirNameForProject()); - string apiConfig = isDebug ? "Debug" : "Release"; - // TODO - throw new NotImplementedException(); - string resAssembliesDir = null; // Path.Combine(GodotSharpDirs.ResAssembliesBaseDir, apiConfig); + if (Directory.Exists(outputDataDir)) + Directory.Delete(outputDataDir, recursive: true); // Clean first - bool assembliesInsidePck = (bool)ProjectSettings.GetSetting("mono/export/export_assemblies_inside_pck") || - outputDataDir == null; + Directory.CreateDirectory(outputDataDir); - if (!assembliesInsidePck) + foreach (string dir in Directory.GetDirectories(publishOutputTempDir, "*", SearchOption.AllDirectories)) { - string outputDataGameAssembliesDir = Path.Combine(outputDataDir, "Assemblies"); - if (!Directory.Exists(outputDataGameAssembliesDir)) - Directory.CreateDirectory(outputDataGameAssembliesDir); + Directory.CreateDirectory(Path.Combine(outputDataDir, dir.Substring(publishOutputTempDir.Length + 1))); } - foreach (var assembly in assemblies) + foreach (string file in Directory.GetFiles(publishOutputTempDir, "*", SearchOption.AllDirectories)) { - void AddToAssembliesDir(string fileSrcPath) - { - if (assembliesInsidePck) - { - string fileDstPath = Path.Combine(resAssembliesDir, fileSrcPath.GetFile()); - AddFile(fileSrcPath, fileDstPath); - } - else - { - Debug.Assert(outputDataDir != null); - string fileDstPath = Path.Combine(outputDataDir, "Assemblies", fileSrcPath.GetFile()); - File.Copy(fileSrcPath, fileDstPath); - } - } - - string assemblySrcPath = assembly.Value; - - string assemblyPathWithoutExtension = Path.ChangeExtension(assemblySrcPath, null); - string pdbSrcPath = assemblyPathWithoutExtension + ".pdb"; - - AddToAssembliesDir(assemblySrcPath); - - if (File.Exists(pdbSrcPath)) - AddToAssembliesDir(pdbSrcPath); + File.Copy(file, Path.Combine(outputDataDir, file.Substring(publishOutputTempDir.Length + 1))); } + } - // AOT compilation - bool aotEnabled = platform == OS.Platforms.iOS || - (bool)ProjectSettings.GetSetting("mono/export/aot/enabled"); + private string DetermineRuntimeIdentifierOS(string platform) + => OS.DotNetOSPlatformMap[platform]; - if (aotEnabled) + private string DetermineRuntimeIdentifierArch(string arch) + { + return arch switch { - 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") ?? - Array.Empty<string>(), - ExtraOptimizerOptions = - (string[])ProjectSettings.GetSetting("mono/export/aot/extra_optimizer_options") ?? - Array.Empty<string>(), - ToolchainPath = aotToolchainPath - }; - - AotBuilder.CompileAssemblies(this, aotOpts, features, platform, isDebug, bclDir, outputDir, - outputDataDir, assemblies); - } + "x86" => "x86", + "x86_32" => "x86", + "x64" => "x64", + "x86_64" => "x64", + "armeabi-v7a" => "arm", + "arm64-v8a" => "arm64", + "armv7" => "arm", + "arm64" => "arm64", + _ => throw new ArgumentOutOfRangeException(nameof(arch), arch, "Unexpected architecture") + }; } public override void _ExportEnd() @@ -350,70 +201,11 @@ namespace GodotTools.Export } } - [return: NotNull] - 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 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}"; - - string templateDirPath = Path.Combine(Internal.FullExportTemplatesDir, TemplateDirName()); - bool validTemplatePathFound = true; - - if (!Directory.Exists(templateDirPath)) - { - validTemplatePathFound = false; - - if (isDebug) - { - target = "debug"; // Support both 'release_debug' and 'debug' for the template data directory name - templateDirPath = Path.Combine(Internal.FullExportTemplatesDir, TemplateDirName()); - validTemplatePathFound = true; - - if (!Directory.Exists(templateDirPath)) - validTemplatePathFound = false; - } - } - - if (!validTemplatePathFound) - throw new FileNotFoundException("Data template directory not found", templateDirPath); - - string outputDataDir = Path.Combine(outputDir, DetermineDataDirNameForProject()); - - 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 static bool PlatformHasTemplateDir(string platform) - { - // macOS 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.MacOS, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 } - .Contains(platform); - } - private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, out string platform) { foreach (var feature in features) { - if (OS.PlatformNameMap.TryGetValue(feature, out platform)) + if (OS.PlatformFeatureMap.TryGetValue(feature, out platform)) return true; } @@ -421,78 +213,6 @@ namespace GodotTools.Export return false; } - private static string GetBclProfileDir(string profile) - { - string templatesDir = Internal.FullExportTemplatesDir; - return Path.Combine(templatesDir, "bcl", profile); - } - - private static string DeterminePlatformBclDir(string platform) - { - string templatesDir = Internal.FullExportTemplatesDir; - 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"))) - { - if (PlatformRequiresCustomBcl(platform)) - throw new FileNotFoundException($"Missing BCL (Base Class Library) for platform: {platform}"); - - platformBclDir = typeof(object).Assembly.Location.GetBaseDir(); // Use the one we're running on - } - } - - return platformBclDir; - } - - /// <summary> - /// Determines whether the BCL bundled with the Godot editor can be used for the target platform, - /// or if it requires a custom BCL that must be distributed with the export templates. - /// </summary> - private static bool PlatformRequiresCustomBcl(string 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. - // We use the names 'net_4_x_win' and 'net_4_x' to differentiate between the two. - - bool isWinOrUwp = new[] - { - OS.Platforms.Windows, - OS.Platforms.UWP - }.Contains(platform); - - return OS.IsWindows ? !isWinOrUwp : isWinOrUwp; - } - - private static string DeterminePlatformBclProfile(string platform) - { - switch (platform) - { - case OS.Platforms.Windows: - case OS.Platforms.UWP: - return "net_4_x_win"; - case OS.Platforms.MacOS: - case OS.Platforms.LinuxBSD: - case OS.Platforms.Server: - case OS.Platforms.Haiku: - return "net_4_x"; - case OS.Platforms.Android: - return "monodroid"; - case OS.Platforms.iOS: - return "monotouch"; - case OS.Platforms.HTML5: - return "wasm"; - default: - throw new NotSupportedException($"Platform not supported: {platform}"); - } - } - private static string DetermineDataDirNameForProject() { string appName = (string)ProjectSettings.GetSetting("application/config/name"); diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 2d85513766..4e27f4ed14 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -13,13 +13,14 @@ using GodotTools.Internals; using GodotTools.ProjectEditor; using JetBrains.Annotations; using static GodotTools.Internals.Globals; +using Environment = System.Environment; using File = GodotTools.Utils.File; using OS = GodotTools.Utils.OS; using Path = System.IO.Path; namespace GodotTools { - public class GodotSharpEditor : EditorPlugin, ISerializationListener + public partial class GodotSharpEditor : EditorPlugin, ISerializationListener { private EditorSettings _editorSettings; @@ -151,13 +152,6 @@ namespace GodotTools Instance.MSBuildPanel.BuildSolution(); } - public override void _Ready() - { - base._Ready(); - - MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged; - } - private enum MenuOptions { CreateSln, @@ -382,12 +376,37 @@ namespace GodotTools { base._EnablePlugin(); - ProjectUtils.MSBuildLocatorRegisterDefaults(); - if (Instance != null) throw new InvalidOperationException(); Instance = this; + var dotNetSdkSearchVersion = Environment.Version; + + // First we try to find the .NET Sdk ourselves to make sure we get the + // correct version first (`RegisterDefaults` always picks the latest). + if (DotNetFinder.TryFindDotNetSdk(dotNetSdkSearchVersion, out var sdkVersion, out string sdkPath)) + { + if (Godot.OS.IsStdoutVerbose()) + Console.WriteLine($"Found .NET Sdk version '{sdkVersion}': {sdkPath}"); + + ProjectUtils.MSBuildLocatorRegisterMSBuildPath(sdkPath); + } + else + { + try + { + ProjectUtils.MSBuildLocatorRegisterDefaults(out sdkVersion, out sdkPath); + if (Godot.OS.IsStdoutVerbose()) + Console.WriteLine($"Found .NET Sdk version '{sdkVersion}': {sdkPath}"); + } + catch (InvalidOperationException e) + { + if (Godot.OS.IsStdoutVerbose()) + GD.PrintErr(e.ToString()); + GD.PushError($".NET Sdk not found. The required version is '{dotNetSdkSearchVersion}'."); + } + } + var editorInterface = GetEditorInterface(); var editorBaseControl = editorInterface.GetBaseControl(); @@ -397,6 +416,8 @@ namespace GodotTools editorBaseControl.AddChild(_errorDialog); MSBuildPanel = new MSBuildPanel(); + MSBuildPanel.Ready += () => + MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged; _bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR()); AddChild(new HotReloadAssemblyWatcher { Name = "HotReloadAssemblyWatcher" }); diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index d0fae02d5d..92a40b9b8b 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -11,6 +11,13 @@ <GodotApiAssembliesDir>$(GodotOutputDataDir)/Api/$(GodotApiConfiguration)</GodotApiAssembliesDir> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> + <!-- Needed for our source generators to work despite this not being a Godot game project --> + <PropertyGroup> + <IsGodotToolsProject>true</IsGodotToolsProject> + </PropertyGroup> + <ItemGroup> + <CompilerVisibleProperty Include="IsGodotToolsProject" /> + </ItemGroup> <PropertyGroup Condition=" Exists('$(GodotApiAssembliesDir)/GodotSharp.dll') "> <!-- The project is part of the Godot source tree --> <!-- Use the Godot source tree output folder instead of '$(ProjectDir)/bin' --> @@ -34,6 +41,9 @@ </Reference> </ItemGroup> <ItemGroup> + <ProjectReference Include="..\..\Godot.NET.Sdk\Godot.SourceGenerators\Godot.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> + </ItemGroup> + <ItemGroup> <ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj" /> <ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" /> <ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj" /> diff --git a/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs b/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs index 414729b18e..260d13a714 100644 --- a/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs +++ b/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs @@ -5,7 +5,7 @@ using static GodotTools.Internals.Globals; namespace GodotTools { - public class HotReloadAssemblyWatcher : Node + public partial class HotReloadAssemblyWatcher : Node { private Timer _watchTimer; diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs index 23339fe50b..9acdeb4183 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs @@ -8,7 +8,7 @@ using GodotTools.Internals; namespace GodotTools.Ides { - public sealed class GodotIdeManager : Node, ISerializationListener + public sealed partial class GodotIdeManager : Node, ISerializationListener { private MessagingServer _messagingServer; diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs index eca7da07c8..63c15e7d28 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs @@ -1,4 +1,3 @@ -using System.Runtime.CompilerServices; using Godot.NativeInterop; namespace GodotTools.Internals @@ -15,16 +14,6 @@ namespace GodotTools.Internals } } - public static string ResTempAssembliesBaseDir - { - get - { - Internal.godot_icall_GodotSharpDirs_ResTempAssembliesBaseDir(out godot_string dest); - using (dest) - return Marshaling.ConvertStringToManaged(dest); - } - } - public static string MonoUserDir { get diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs index 7b39f8ecdb..be53b8f1cf 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Godot; using Godot.NativeInterop; @@ -47,16 +46,6 @@ namespace GodotTools.Internals public static void EditorNodeShowScriptScreen() => godot_icall_Internal_EditorNodeShowScriptScreen(); - public static string MonoWindowsInstallRoot - { - get - { - godot_icall_Internal_MonoWindowsInstallRoot(out godot_string dest); - using (dest) - return Marshaling.ConvertStringToManaged(dest); - } - } - public static void EditorRunPlay() => godot_icall_Internal_EditorRunPlay(); public static void EditorRunStop() => godot_icall_Internal_EditorRunStop(); @@ -64,7 +53,7 @@ namespace GodotTools.Internals public static void ScriptEditorDebugger_ReloadScripts() => godot_icall_Internal_ScriptEditorDebugger_ReloadScripts(); - public static unsafe string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, + public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, string scriptFile) { using godot_string scriptFileIn = Marshaling.ConvertStringToNative(scriptFile); @@ -81,9 +70,6 @@ namespace GodotTools.Internals public static extern void godot_icall_GodotSharpDirs_ResMetadataDir(out godot_string r_dest); [DllImport(GodotDllName)] - public static extern void godot_icall_GodotSharpDirs_ResTempAssembliesBaseDir(out godot_string r_dest); - - [DllImport(GodotDllName)] public static extern void godot_icall_GodotSharpDirs_MonoUserDir(out godot_string r_dest); [DllImport(GodotDllName)] @@ -144,9 +130,6 @@ namespace GodotTools.Internals private static extern void godot_icall_Internal_EditorNodeShowScriptScreen(); [DllImport(GodotDllName)] - private static extern void godot_icall_Internal_MonoWindowsInstallRoot(out godot_string dest); - - [DllImport(GodotDllName)] private static extern void godot_icall_Internal_EditorRunPlay(); [DllImport(GodotDllName)] diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index db77a71c2f..73e3df4d2c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -1,11 +1,12 @@ using Godot.NativeInterop; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; +using System.Text; using GodotTools.Internals; namespace GodotTools.Utils @@ -13,7 +14,7 @@ namespace GodotTools.Utils [SuppressMessage("ReSharper", "InconsistentNaming")] public static class OS { - public static class Names + private static class Names { public const string Windows = "Windows"; public const string MacOS = "macOS"; @@ -42,6 +43,35 @@ namespace GodotTools.Utils public const string HTML5 = "javascript"; } + private static class DotNetOS + { + public const string Win = "win"; + public const string OSX = "osx"; + public const string Linux = "linux"; + public const string Win10 = "win10"; + public const string Android = "android"; + public const string iOS = "ios"; + public const string Browser = "browser"; + } + + public static readonly Dictionary<string, string> PlatformFeatureMap = new Dictionary<string, string>( + // Export `features` may be in lower case + StringComparer.InvariantCultureIgnoreCase + ) + { + ["Windows"] = Platforms.Windows, + ["macOS"] = Platforms.MacOS, + ["LinuxBSD"] = Platforms.LinuxBSD, + // "X11" for compatibility, temporarily, while we are on an outdated branch + ["X11"] = Platforms.LinuxBSD, + ["Server"] = Platforms.Server, + ["UWP"] = Platforms.UWP, + ["Haiku"] = Platforms.Haiku, + ["Android"] = Platforms.Android, + ["iOS"] = Platforms.iOS, + ["HTML5"] = Platforms.HTML5 + }; + public static readonly Dictionary<string, string> PlatformNameMap = new Dictionary<string, string> { [Names.Windows] = Platforms.Windows, @@ -58,7 +88,23 @@ namespace GodotTools.Utils [Names.HTML5] = Platforms.HTML5 }; - private static unsafe bool IsOS(string name) + public static readonly Dictionary<string, string> DotNetOSPlatformMap = new Dictionary<string, string> + { + [Platforms.Windows] = DotNetOS.Win, + [Platforms.MacOS] = DotNetOS.OSX, + // TODO: + // Does .NET 6 support BSD variants? If it does, it may need the name `unix` + // instead of `linux` in the runtime identifier. This would be a problem as + // Godot has a single export profile for both, named LinuxBSD. + [Platforms.LinuxBSD] = DotNetOS.Linux, + [Platforms.Server] = DotNetOS.Linux, + [Platforms.UWP] = DotNetOS.Win10, + [Platforms.Android] = DotNetOS.Android, + [Platforms.iOS] = DotNetOS.iOS, + [Platforms.HTML5] = DotNetOS.Browser + }; + + private static bool IsOS(string name) { Internal.godot_icall_Utils_OS_GetPlatformName(out godot_string dest); using (dest) @@ -68,7 +114,7 @@ namespace GodotTools.Utils } } - private static unsafe bool IsAnyOS(IEnumerable<string> names) + private static bool IsAnyOS(IEnumerable<string> names) { Internal.godot_icall_Utils_OS_GetPlatformName(out godot_string dest); using (dest) @@ -99,25 +145,34 @@ namespace GodotTools.Utils // TODO SupportedOSPlatformGuard once we target .NET 6 // [SupportedOSPlatformGuard("windows")] public static bool IsWindows => _isWindows.Value || IsUWP; + // [SupportedOSPlatformGuard("osx")] public static bool IsMacOS => _isMacOS.Value; + // [SupportedOSPlatformGuard("linux")] public static bool IsLinuxBSD => _isLinuxBSD.Value; + // [SupportedOSPlatformGuard("linux")] public static bool IsServer => _isServer.Value; + // [SupportedOSPlatformGuard("windows")] public static bool IsUWP => _isUWP.Value; + public static bool IsHaiku => _isHaiku.Value; + // [SupportedOSPlatformGuard("android")] public static bool IsAndroid => _isAndroid.Value; + // [SupportedOSPlatformGuard("ios")] public static bool IsiOS => _isiOS.Value; + // [SupportedOSPlatformGuard("browser")] public static bool IsHTML5 => _isHTML5.Value; public static bool IsUnixLike => _isUnixLike.Value; public static char PathSep => IsWindows ? ';' : ':'; + [return: MaybeNull] public static string PathWhich([NotNull] string name) { if (IsWindows) @@ -126,6 +181,7 @@ namespace GodotTools.Utils return PathWhichUnix(name); } + [return: MaybeNull] private static string PathWhichWindows([NotNull] string name) { string[] windowsExts = @@ -162,6 +218,7 @@ namespace GodotTools.Utils select path + ext).FirstOrDefault(File.Exists); } + [return: MaybeNull] private static string PathWhichUnix([NotNull] string name) { string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); @@ -192,14 +249,7 @@ 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)); - } - - var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments)) + var startInfo = new ProcessStartInfo(command) { RedirectStandardOutput = true, RedirectStandardError = true, @@ -207,44 +257,104 @@ namespace GodotTools.Utils CreateNoWindow = true }; - using (Process process = Process.Start(startInfo)) - { - if (process == null) - throw new Exception("No process was started"); + foreach (string arg in arguments) + startInfo.ArgumentList.Add(arg); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - if (IsWindows && process.Id > 0) - User32Dll.AllowSetForegroundWindow(process.Id); // allows application to focus itself - } + using Process process = Process.Start(startInfo); + + if (process == null) + throw new Exception("No process was started"); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + if (IsWindows && process.Id > 0) + 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) + var startInfo = new ProcessStartInfo(command) { - // Not perfect, but as long as we are careful... - return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg)); - } + // Print the output + RedirectStandardOutput = false, + RedirectStandardError = false, + UseShellExecute = false + }; + + foreach (string arg in arguments) + startInfo.ArgumentList.Add(arg); + + Console.WriteLine(startInfo.GetCommandLineDisplay(new StringBuilder("Executing: ")).ToString()); - var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments)); + using var process = new Process { StartInfo = startInfo }; + process.Start(); + process.WaitForExit(); - Console.WriteLine($"Executing: \"{startInfo.FileName}\" {startInfo.Arguments}"); + return process.ExitCode; + } + + private static void AppendProcessFileNameForDisplay(this StringBuilder builder, string fileName) + { + if (builder.Length > 0) + builder.Append(' '); - // Print the output - startInfo.RedirectStandardOutput = false; - startInfo.RedirectStandardError = false; + if (fileName.Contains(' ')) + { + builder.Append('"'); + builder.Append(fileName); + builder.Append('"'); + } + else + { + builder.Append(fileName); + } + } - startInfo.UseShellExecute = false; + private static void AppendProcessArgumentsForDisplay(this StringBuilder builder, + Collection<string> argumentList) + { + // This is intended just for reading. It doesn't need to be a valid command line. + // E.g.: We don't handle escaping of quotes. - using (var process = new Process { StartInfo = startInfo }) + foreach (string argument in argumentList) { - process.Start(); - process.WaitForExit(); + if (builder.Length > 0) + builder.Append(' '); - return process.ExitCode; + if (argument.Contains(' ')) + { + builder.Append('"'); + builder.Append(argument); + builder.Append('"'); + } + else + { + builder.Append(argument); + } } } + + public static StringBuilder GetCommandLineDisplay( + this ProcessStartInfo startInfo, + StringBuilder optionalBuilder = null + ) + { + var builder = optionalBuilder ?? new StringBuilder(); + + builder.AppendProcessFileNameForDisplay(startInfo.FileName); + + if (startInfo.ArgumentList.Count == 0) + { + builder.Append(' '); + builder.Append(startInfo.Arguments); + } + else + { + builder.AppendProcessArgumentsForDisplay(startInfo.ArgumentList); + } + + return builder; + } } } diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index fdc50e22f3..746cb8a142 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -76,7 +76,6 @@ StringBuilder &operator<<(StringBuilder &r_sb, const char *p_cstring) { #define CLOSE_BLOCK_L4 INDENT4 CLOSE_BLOCK #define BINDINGS_GLOBAL_SCOPE_CLASS "GD" -#define BINDINGS_PTR_FIELD "NativePtr" #define BINDINGS_NATIVE_NAME_FIELD "NativeName" #define CS_PARAM_MEMORYOWN "memoryOwn" @@ -86,6 +85,7 @@ StringBuilder &operator<<(StringBuilder &r_sb, const char *p_cstring) { #define CS_METHOD_CALL "Call" #define CS_PROPERTY_SINGLETON "Singleton" #define CS_METHOD_INVOKE_GODOT_CLASS_METHOD "InvokeGodotClassMethod" +#define CS_METHOD_HAS_GODOT_CLASS_METHOD "HasGodotClassMethod" #define CS_STATIC_FIELD_NATIVE_CTOR "NativeCtor" #define CS_STATIC_FIELD_METHOD_BIND_PREFIX "MethodBind" @@ -103,7 +103,6 @@ StringBuilder &operator<<(StringBuilder &r_sb, const char *p_cstring) { #define C_CLASS_NATIVE_FUNCS "NativeFuncs" #define C_NS_MONOUTILS "InteropUtils" -#define C_METHOD_TIE_MANAGED_TO_UNMANAGED C_NS_MONOUTILS ".TieManagedToUnmanaged" #define C_METHOD_UNMANAGED_GET_MANAGED C_NS_MONOUTILS ".UnmanagedGetManaged" #define C_METHOD_ENGINE_GET_SINGLETON C_NS_MONOUTILS ".EngineGetSingleton" @@ -122,8 +121,6 @@ StringBuilder &operator<<(StringBuilder &r_sb, const char *p_cstring) { // Types that will be ignored by the generator and won't be available in C#. const Vector<String> ignored_types = { "PhysicsServer3DExtension" }; -typedef String string; - void BindingsGenerator::TypeInterface::postsetup_enum_type(BindingsGenerator::TypeInterface &r_enum_itype) { // C interface for enums is the same as that of 'uint32_t'. Remember to apply // any of the changes done here to the 'uint32_t' type interface as well. @@ -1541,7 +1538,7 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str // Add native name static field if (is_derived_type) { - output << MEMBER_BEGIN "private static readonly System.Type _cachedType = typeof(" << itype.proxy_name << ");\n"; + output << MEMBER_BEGIN "private static readonly System.Type CachedType = typeof(" << itype.proxy_name << ");\n"; } output.append(MEMBER_BEGIN "private static readonly StringName " BINDINGS_NATIVE_NAME_FIELD " = \""); @@ -1561,22 +1558,12 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str // Add default constructor if (itype.is_instantiable) { output << MEMBER_BEGIN "public " << itype.proxy_name << "() : this(" - << (itype.memory_own ? "true" : "false") << ")\n" OPEN_BLOCK_L2; - - // The default constructor may also be called by the engine when instancing existing native objects - // The engine will initialize the pointer field of the managed side before calling the constructor - // This is why we only allocate a new native object from the constructor if the pointer field is not set - output << INDENT3 "if (" BINDINGS_PTR_FIELD " == IntPtr.Zero)\n" OPEN_BLOCK_L3 - << INDENT4 "unsafe\n" INDENT4 OPEN_BLOCK - << INDENT5 BINDINGS_PTR_FIELD " = " CS_STATIC_FIELD_NATIVE_CTOR "();\n" - << CLOSE_BLOCK_L4 - << INDENT4 C_METHOD_TIE_MANAGED_TO_UNMANAGED "(this, " BINDINGS_PTR_FIELD ", " - << BINDINGS_NATIVE_NAME_FIELD << ", refCounted: " << (itype.is_ref_counted ? "true" : "false") - << ", ((object)this).GetType(), _cachedType);\n" CLOSE_BLOCK_L3 - << INDENT3 "else\n" INDENT3 OPEN_BLOCK - << INDENT4 "InteropUtils.TieManagedToUnmanagedWithPreSetup(this, " - << BINDINGS_PTR_FIELD ", ((object)this).GetType(), _cachedType);\n" CLOSE_BLOCK_L3 - << INDENT3 "_InitializeGodotScriptInstanceInternals();\n" CLOSE_BLOCK_L2; + << (itype.memory_own ? "true" : "false") << ")\n" OPEN_BLOCK_L2 + << INDENT3 "unsafe\n" INDENT3 OPEN_BLOCK + << INDENT4 "_ConstructAndInitialize(" CS_STATIC_FIELD_NATIVE_CTOR ", " + << BINDINGS_NATIVE_NAME_FIELD ", CachedType, refCounted: " + << (itype.is_ref_counted ? "true" : "false") << ");\n" + << CLOSE_BLOCK_L3 CLOSE_BLOCK_L2; } else { // Hide the constructor output.append(MEMBER_BEGIN "internal "); @@ -1608,9 +1595,11 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str "Failed to generate signal '" + isignal.name + "' for class '" + itype.name + "'."); } - // Script calls + // Script members look-up if (!itype.is_singleton && (is_derived_type || itype.has_virtual_methods)) { + // Generate method names cache fields + for (const MethodInterface &imethod : itype.methods) { if (!imethod.is_virtual) { continue; @@ -1629,6 +1618,10 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str << " = \"" << imethod.proxy_name << "\";\n"; } + // TODO: Only generate HasGodotClassMethod and InvokeGodotClassMethod if there's any method + + // Generate InvokeGodotClassMethod + output << MEMBER_BEGIN "protected internal " << (is_derived_type ? "override" : "virtual") << " bool " CS_METHOD_INVOKE_GODOT_CLASS_METHOD "(in godot_string_name method, " << "NativeVariantPtrArgs args, int argCount, out godot_variant ret)\n" @@ -1639,9 +1632,17 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str continue; } + // We also call HasGodotClassMethod to ensure the method is overridden and avoid calling + // the stub implementation. This solution adds some extra overhead to calls, but it's + // much simpler than other solutions. This won't be a problem once we move to function + // pointers of generated wrappers for each method, as lookup will only happen once. + + // We check both native names (snake_case) and proxy names (PascalCase) output << INDENT3 "if ((method == " << CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX << imethod.name << " || method == " << CS_STATIC_FIELD_METHOD_NAME_PREFIX << imethod.name - << ") && argCount == " << itos(imethod.arguments.size()) << ")\n" + << ") && argCount == " << itos(imethod.arguments.size()) + << " && " << CS_METHOD_HAS_GODOT_CLASS_METHOD << "((godot_string_name)" + << CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX << imethod.name << ".NativeValue))\n" << INDENT3 "{\n"; if (imethod.return_type.cname != name_cache.type_void) { @@ -1696,6 +1697,38 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str } output << INDENT2 "}\n"; + + // Generate HasGodotClassMethod + + output << MEMBER_BEGIN "protected internal " << (is_derived_type ? "override" : "virtual") + << " bool " CS_METHOD_HAS_GODOT_CLASS_METHOD "(in godot_string_name method)\n" + << INDENT2 "{\n"; + + for (const MethodInterface &imethod : itype.methods) { + if (!imethod.is_virtual) { + continue; + } + + // We check for native names (snake_case). If we detect one, we call HasGodotClassMethod + // again, but this time with the respective proxy name (PascalCase). It's the job of + // user derived classes to override the method and check for those. Our C# source + // generators take care of generating those override methods. + output << INDENT3 "if (method == " << CS_STATIC_FIELD_METHOD_NAME_PREFIX << imethod.name + << ")\n" INDENT3 "{\n" + << INDENT4 "if (" CS_METHOD_HAS_GODOT_CLASS_METHOD "(" + << CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX << imethod.name + << ".NativeValue.DangerousSelfRef))\n" INDENT4 "{\n" + << INDENT5 "return true;\n" + << INDENT4 "}\n" INDENT3 "}\n"; + } + + if (is_derived_type) { + output << INDENT3 "return base." CS_METHOD_HAS_GODOT_CLASS_METHOD "(method);\n"; + } else { + output << INDENT3 "return false;\n"; + } + + output << INDENT2 "}\n"; } output.append(INDENT1 CLOSE_BLOCK /* class */ @@ -2278,7 +2311,7 @@ Error BindingsGenerator::_generate_cs_native_calls(const InternalCall &p_icall, if (p_icall.is_vararg) { if (i < p_icall.get_arguments_count() - 1) { - string c_in_vararg = arg_type->c_in_vararg; + String c_in_vararg = arg_type->c_in_vararg; if (arg_type->is_object_type) { c_in_vararg = "%5using godot_variant %1_in = VariantUtils.CreateFromGodotObject(%1);\n"; diff --git a/modules/mono/editor/editor_internal_calls.cpp b/modules/mono/editor/editor_internal_calls.cpp index 5b1bc8ccbf..1544e61315 100644 --- a/modules/mono/editor/editor_internal_calls.cpp +++ b/modules/mono/editor/editor_internal_calls.cpp @@ -74,10 +74,6 @@ GD_PINVOKE_EXPORT void godot_icall_GodotSharpDirs_ResMetadataDir(godot_string *r memnew_placement(r_dest, String(GodotSharpDirs::get_res_metadata_dir())); } -GD_PINVOKE_EXPORT void godot_icall_GodotSharpDirs_ResTempAssembliesBaseDir(godot_string *r_dest) { - memnew_placement(r_dest, String(GodotSharpDirs::get_res_temp_assemblies_base_dir())); -} - GD_PINVOKE_EXPORT void godot_icall_GodotSharpDirs_MonoUserDir(godot_string *r_dest) { memnew_placement(r_dest, String(GodotSharpDirs::get_mono_user_dir())); } @@ -189,16 +185,6 @@ GD_PINVOKE_EXPORT void godot_icall_Internal_EditorNodeShowScriptScreen() { EditorNode::get_singleton()->call("_editor_select", EditorNode::EDITOR_SCRIPT); } -GD_PINVOKE_EXPORT void godot_icall_Internal_MonoWindowsInstallRoot(godot_string *r_dest) { -#ifdef WINDOWS_ENABLED - String install_root_dir = GDMono::get_singleton()->get_mono_reg_info().install_root_dir; - memnew_placement(r_dest, String(install_root_dir)); -#else - memnew_placement(r_dest, String); - return; -#endif -} - GD_PINVOKE_EXPORT void godot_icall_Internal_EditorRunPlay() { EditorNode::get_singleton()->run_play(); } @@ -267,9 +253,8 @@ GD_PINVOKE_EXPORT bool godot_icall_Utils_OS_UnixFileHasExecutableAccess(const go } #endif -void *godotsharp_editor_pinvoke_funcs[32] = { +void *godotsharp_editor_pinvoke_funcs[30] = { (void *)godot_icall_GodotSharpDirs_ResMetadataDir, - (void *)godot_icall_GodotSharpDirs_ResTempAssembliesBaseDir, (void *)godot_icall_GodotSharpDirs_MonoUserDir, (void *)godot_icall_GodotSharpDirs_BuildLogsDirs, (void *)godot_icall_GodotSharpDirs_ProjectSlnPath, @@ -288,7 +273,6 @@ void *godotsharp_editor_pinvoke_funcs[32] = { (void *)godot_icall_Internal_EditorDebuggerNodeReloadScripts, (void *)godot_icall_Internal_ScriptEditorEdit, (void *)godot_icall_Internal_EditorNodeShowScriptScreen, - (void *)godot_icall_Internal_MonoWindowsInstallRoot, (void *)godot_icall_Internal_EditorRunPlay, (void *)godot_icall_Internal_EditorRunStop, (void *)godot_icall_Internal_ScriptEditorDebugger_ReloadScripts, diff --git a/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs b/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs index 9f938373c4..0cb9e57530 100644 --- a/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs +++ b/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs @@ -4,6 +4,7 @@ using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Loader; +using Godot.Bridge; using Godot.NativeInterop; namespace GodotPlugins @@ -13,6 +14,7 @@ namespace GodotPlugins private static readonly List<AssemblyName> SharedAssemblies = new(); private static readonly Assembly CoreApiAssembly = typeof(Godot.Object).Assembly; private static Assembly? _editorApiAssembly; + private static Assembly? _projectAssembly; private static readonly AssemblyLoadContext MainLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? @@ -20,67 +22,59 @@ namespace GodotPlugins // Right now we do it this way for simplicity as hot-reload is disabled. It will need to be changed later. [UnmanagedCallersOnly] - internal static unsafe godot_bool Initialize(godot_bool editorHint, - PluginsCallbacks* pluginsCallbacks, Godot.Bridge.ManagedCallbacks* managedCallbacks) + // ReSharper disable once UnusedMember.Local + private static unsafe godot_bool InitializeFromEngine(godot_bool editorHint, + PluginsCallbacks* pluginsCallbacks, ManagedCallbacks* managedCallbacks) { try { SharedAssemblies.Add(CoreApiAssembly.GetName()); + NativeLibrary.SetDllImportResolver(CoreApiAssembly, GodotDllImportResolver.OnResolveDllImport); if (editorHint.ToBool()) { _editorApiAssembly = Assembly.Load("GodotSharpEditor"); SharedAssemblies.Add(_editorApiAssembly.GetName()); + NativeLibrary.SetDllImportResolver(_editorApiAssembly, GodotDllImportResolver.OnResolveDllImport); } - NativeLibrary.SetDllImportResolver(CoreApiAssembly, OnResolveDllImport); - *pluginsCallbacks = new() { LoadProjectAssemblyCallback = &LoadProjectAssembly, LoadToolsAssemblyCallback = &LoadToolsAssembly, }; - *managedCallbacks = Godot.Bridge.ManagedCallbacks.Create(); + *managedCallbacks = ManagedCallbacks.Create(); return godot_bool.True; } catch (Exception e) { Console.Error.WriteLine(e); - *pluginsCallbacks = default; - *managedCallbacks = default; return false.ToGodotBool(); } } [StructLayout(LayoutKind.Sequential)] - internal struct PluginsCallbacks + private struct PluginsCallbacks { public unsafe delegate* unmanaged<char*, godot_bool> LoadProjectAssemblyCallback; public unsafe delegate* unmanaged<char*, IntPtr> LoadToolsAssemblyCallback; } [UnmanagedCallersOnly] - internal static unsafe godot_bool LoadProjectAssembly(char* nAssemblyPath) + private static unsafe godot_bool LoadProjectAssembly(char* nAssemblyPath) { try { - string assemblyPath = new(nAssemblyPath); + if (_projectAssembly != null) + return godot_bool.True; // Already loaded - var assembly = LoadPlugin(assemblyPath); - - var method = CoreApiAssembly.GetType("Godot.Bridge.ScriptManagerBridge")? - .GetMethod("LookupScriptsInAssembly", - BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); + string assemblyPath = new(nAssemblyPath); - if (method == null) - { - throw new MissingMethodException("Godot.Bridge.ScriptManagerBridge", - "LookupScriptsInAssembly"); - } + _projectAssembly = LoadPlugin(assemblyPath); - method.Invoke(null, new object[] { assembly }); + ScriptManagerBridge.LookupScriptsInAssembly(_projectAssembly); return godot_bool.True; } @@ -92,7 +86,7 @@ namespace GodotPlugins } [UnmanagedCallersOnly] - internal static unsafe IntPtr LoadToolsAssembly(char* nAssemblyPath) + private static unsafe IntPtr LoadToolsAssembly(char* nAssemblyPath) { try { @@ -103,7 +97,7 @@ namespace GodotPlugins var assembly = LoadPlugin(assemblyPath); - NativeLibrary.SetDllImportResolver(assembly, OnResolveDllImport); + NativeLibrary.SetDllImportResolver(assembly, GodotDllImportResolver.OnResolveDllImport); var method = assembly.GetType("GodotTools.GodotSharpEditor")? .GetMethod("InternalCreateInstance", @@ -140,58 +134,5 @@ namespace GodotPlugins var loadContext = new PluginLoadContext(assemblyPath, sharedAssemblies, MainLoadContext); return loadContext.LoadFromAssemblyName(new AssemblyName(assemblyName)); } - - public static IntPtr OnResolveDllImport(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) - { - if (libraryName == "__Internal") - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return Win32.GetModuleHandle(IntPtr.Zero); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return Linux.dlopen(IntPtr.Zero, Linux.RTLD_LAZY); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return MacOS.dlopen(IntPtr.Zero, MacOS.RTLD_LAZY); - } - } - - return IntPtr.Zero; - } - - // ReSharper disable InconsistentNaming - private static class MacOS - { - private const string SystemLibrary = "/usr/lib/libSystem.dylib"; - - public const int RTLD_LAZY = 1; - - [DllImport(SystemLibrary)] - public static extern IntPtr dlopen(IntPtr path, int mode); - } - - private static class Linux - { - // libdl.so was resulting in DllNotFoundException, for some reason... - // libcoreclr.so should work with both CoreCLR and the .NET Core version of Mono. - private const string SystemLibrary = "libcoreclr.so"; - - public const int RTLD_LAZY = 1; - - [DllImport(SystemLibrary)] - public static extern IntPtr dlopen(IntPtr path, int mode); - } - - private static class Win32 - { - private const string SystemLibrary = "Kernel32.dll"; - - [DllImport(SystemLibrary)] - public static extern IntPtr GetModuleHandle(IntPtr lpModuleName); - } - // ReSharper restore InconsistentNaming } } diff --git a/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs b/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs index 1b969716aa..982549fff7 100644 --- a/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs +++ b/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs @@ -15,8 +15,6 @@ namespace GodotPlugins public PluginLoadContext(string pluginPath, ICollection<string> sharedAssemblies, AssemblyLoadContext mainLoadContext) { - Console.WriteLine(pluginPath); - Console.Out.Flush(); _resolver = new AssemblyDependencyResolver(pluginPath); _sharedAssemblies = sharedAssemblies; _mainLoadContext = mainLoadContext; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/GodotMethodAttribute.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/GodotMethodAttribute.cs deleted file mode 100644 index 8d4ff0fdb7..0000000000 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/GodotMethodAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Godot -{ - /// <summary> - /// An attribute for a method. - /// </summary> - [AttributeUsage(AttributeTargets.Method)] - internal class GodotMethodAttribute : Attribute - { - private string methodName; - - public string MethodName { get { return methodName; } } - - /// <summary> - /// Constructs a new GodotMethodAttribute instance. - /// </summary> - /// <param name="methodName">The name of the method.</param> - public GodotMethodAttribute(string methodName) - { - this.methodName = methodName; - } - } -} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs index f28d7b1c51..266df07d63 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs @@ -21,12 +21,8 @@ namespace Godot.Bridge return false.ToGodotBool(); } - NativeFuncs.godotsharp_string_name_as_string(out godot_string dest, CustomUnsafe.AsRef(method)); - string methodStr; - using (dest) - methodStr = Marshaling.ConvertStringToManaged(dest); - - bool methodInvoked = godotObject.InvokeGodotClassMethod(CustomUnsafe.AsRef(method), new NativeVariantPtrArgs(args), + bool methodInvoked = godotObject.InvokeGodotClassMethod(CustomUnsafe.AsRef(method), + new NativeVariantPtrArgs(args), argCount, out godot_variant retValue); if (!methodInvoked) @@ -168,5 +164,24 @@ namespace Godot.Bridge *outValid = false.ToGodotBool(); } } + + [UnmanagedCallersOnly] + internal static unsafe godot_bool HasMethodUnknownParams(IntPtr godotObjectGCHandle, godot_string_name* method) + { + try + { + var godotObject = (Object)GCHandle.FromIntPtr(godotObjectGCHandle).Target; + + if (godotObject == null) + return false.ToGodotBool(); + + return godotObject.HasGodotClassMethod(CustomUnsafe.AsRef(method)).ToGodotBool(); + } + catch (Exception e) + { + ExceptionUtils.DebugPrintUnhandledException(e); + return false.ToGodotBool(); + } + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs index bd939ef27d..fb1efa0ac8 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs @@ -5,7 +5,7 @@ using Godot.NativeInterop; namespace Godot.Bridge { [StructLayout(LayoutKind.Sequential)] - internal unsafe struct ManagedCallbacks + public unsafe struct ManagedCallbacks { // @formatter:off public delegate* unmanaged<IntPtr, godot_variant**, int, godot_bool*, void> SignalAwaiter_SignalCallback; @@ -19,7 +19,6 @@ namespace Godot.Bridge public delegate* unmanaged<IntPtr, godot_string_name*, godot_variant**, int, godot_bool*, void> ScriptManagerBridge_RaiseEventSignal; public delegate* unmanaged<IntPtr, godot_dictionary*, void> ScriptManagerBridge_GetScriptSignalList; public delegate* unmanaged<IntPtr, godot_string*, godot_bool> ScriptManagerBridge_HasScriptSignal; - public delegate* unmanaged<IntPtr, godot_string*, godot_bool, godot_bool> ScriptManagerBridge_HasMethodUnknownParams; public delegate* unmanaged<IntPtr, IntPtr, godot_bool> ScriptManagerBridge_ScriptIsOrInherits; public delegate* unmanaged<IntPtr, godot_string*, godot_bool> ScriptManagerBridge_AddScriptBridge; public delegate* unmanaged<IntPtr, void> ScriptManagerBridge_RemoveScriptBridge; @@ -30,6 +29,7 @@ namespace Godot.Bridge public delegate* unmanaged<IntPtr, godot_string_name*, godot_variant*, godot_bool> CSharpInstanceBridge_Get; public delegate* unmanaged<IntPtr, godot_bool, void> CSharpInstanceBridge_CallDispose; public delegate* unmanaged<IntPtr, godot_string*, godot_bool*, void> CSharpInstanceBridge_CallToString; + public delegate* unmanaged<IntPtr, godot_string_name*, godot_bool> CSharpInstanceBridge_HasMethodUnknownParams; public delegate* unmanaged<IntPtr, void> GCHandleBridge_FreeGCHandle; public delegate* unmanaged<void> DebuggingUtils_InstallTraceListener; public delegate* unmanaged<void> Dispatcher_InitializeDefaultGodotTaskScheduler; @@ -52,7 +52,6 @@ namespace Godot.Bridge ScriptManagerBridge_RaiseEventSignal = &ScriptManagerBridge.RaiseEventSignal, ScriptManagerBridge_GetScriptSignalList = &ScriptManagerBridge.GetScriptSignalList, ScriptManagerBridge_HasScriptSignal = &ScriptManagerBridge.HasScriptSignal, - ScriptManagerBridge_HasMethodUnknownParams = &ScriptManagerBridge.HasMethodUnknownParams, ScriptManagerBridge_ScriptIsOrInherits = &ScriptManagerBridge.ScriptIsOrInherits, ScriptManagerBridge_AddScriptBridge = &ScriptManagerBridge.AddScriptBridge, ScriptManagerBridge_RemoveScriptBridge = &ScriptManagerBridge.RemoveScriptBridge, @@ -63,6 +62,7 @@ namespace Godot.Bridge CSharpInstanceBridge_Get = &CSharpInstanceBridge.Get, CSharpInstanceBridge_CallDispose = &CSharpInstanceBridge.CallDispose, CSharpInstanceBridge_CallToString = &CSharpInstanceBridge.CallToString, + CSharpInstanceBridge_HasMethodUnknownParams = &CSharpInstanceBridge.HasMethodUnknownParams, GCHandleBridge_FreeGCHandle = &GCHandleBridge.FreeGCHandle, DebuggingUtils_InstallTraceListener = &DebuggingUtils.InstallTraceListener, Dispatcher_InitializeDefaultGodotTaskScheduler = &Dispatcher.InitializeDefaultGodotTaskScheduler, @@ -70,5 +70,8 @@ namespace Godot.Bridge // @formatter:on }; } + + public static void Create(IntPtr outManagedCallbacks) + => *(ManagedCallbacks*)outManagedCallbacks = Create(); } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index e87b7f4d4b..b86ced55cb 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -8,7 +8,7 @@ using Godot.NativeInterop; namespace Godot.Bridge { - internal static class ScriptManagerBridge + public static class ScriptManagerBridge { private static System.Collections.Generic.Dictionary<string, ScriptLookupInfo> _scriptLookupMap = new(); private static System.Collections.Generic.Dictionary<IntPtr, Type> _scriptBridgeMap = new(); @@ -212,7 +212,7 @@ namespace Godot.Bridge // Called from GodotPlugins // ReSharper disable once UnusedMember.Local - private static void LookupScriptsInAssembly(Assembly assembly) + public static void LookupScriptsInAssembly(Assembly assembly) { static void LookupScriptForClass(Type type) { @@ -440,68 +440,6 @@ namespace Godot.Bridge } [UnmanagedCallersOnly] - internal static unsafe godot_bool HasMethodUnknownParams(IntPtr scriptPtr, godot_string* method, - godot_bool deep) - { - try - { - // Performance is not critical here as this will be replaced with source generators. - if (!_scriptBridgeMap.TryGetValue(scriptPtr, out var scriptType)) - return false.ToGodotBool(); - - string methodStr = Marshaling.ConvertStringToManaged(*method); - - if (deep.ToBool()) - { - Type top = scriptType; - Type native = Object.InternalGetClassNativeBase(scriptType); - - while (top != null && top != native) - { - var methodInfo = top.GetMethod(methodStr, - BindingFlags.DeclaredOnly | BindingFlags.Instance | - BindingFlags.NonPublic | BindingFlags.Public); - - if (methodInfo != null) - return true.ToGodotBool(); - - top = top.BaseType; - } - - top = native; - Type typeOfSystemObject = typeof(System.Object); - while (top != null && top != typeOfSystemObject) - { - bool found = top.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | - BindingFlags.NonPublic | BindingFlags.Public) - .Where(m => m.GetCustomAttributes(false).OfType<GodotMethodAttribute>() - .Where(a => a.MethodName == methodStr) - .Any()) - .Any(); - - if (found) - return true.ToGodotBool(); - - top = top.BaseType; - } - - return false.ToGodotBool(); - } - else - { - var methodInfo = scriptType.GetMethod(methodStr, BindingFlags.DeclaredOnly | BindingFlags.Instance | - BindingFlags.NonPublic | BindingFlags.Public); - return (methodInfo != null).ToGodotBool(); - } - } - catch (Exception e) - { - ExceptionUtils.DebugUnhandledException(e); - return false.ToGodotBool(); - } - } - - [UnmanagedCallersOnly] internal static godot_bool ScriptIsOrInherits(IntPtr scriptPtr, IntPtr scriptPtrMaybeBase) { try diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/GodotDllImportResolver.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/GodotDllImportResolver.cs new file mode 100644 index 0000000000..c4a90625c3 --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/GodotDllImportResolver.cs @@ -0,0 +1,64 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +#nullable enable + +namespace Godot.NativeInterop +{ + public static class GodotDllImportResolver + { + public static IntPtr OnResolveDllImport(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName == "__Internal") + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Win32.GetModuleHandle(IntPtr.Zero); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return Linux.dlopen(IntPtr.Zero, Linux.RTLD_LAZY); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return MacOS.dlopen(IntPtr.Zero, MacOS.RTLD_LAZY); + } + } + + return IntPtr.Zero; + } + + // ReSharper disable InconsistentNaming + private static class MacOS + { + private const string SystemLibrary = "/usr/lib/libSystem.dylib"; + + public const int RTLD_LAZY = 1; + + [DllImport(SystemLibrary)] + public static extern IntPtr dlopen(IntPtr path, int mode); + } + + private static class Linux + { + // libdl.so was resulting in DllNotFoundException, for some reason... + // libcoreclr.so should work with both CoreCLR and the .NET Core version of Mono. + private const string SystemLibrary = "libcoreclr.so"; + + public const int RTLD_LAZY = 1; + + [DllImport(SystemLibrary)] + public static extern IntPtr dlopen(IntPtr path, int mode); + } + + private static class Win32 + { + private const string SystemLibrary = "Kernel32.dll"; + + [DllImport(SystemLibrary)] + public static extern IntPtr GetModuleHandle(IntPtr lpModuleName); + } + // ReSharper restore InconsistentNaming + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs index a3a4c2599e..6255ffbbc7 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs @@ -8,7 +8,7 @@ namespace Godot public partial class Object : IDisposable { private bool _disposed = false; - private Type _cachedType = typeof(Object); + private static readonly Type CachedType = typeof(Object); internal IntPtr NativePtr; private bool _memoryOwn; @@ -20,20 +20,30 @@ namespace Godot /// </summary> public Object() : this(false) { + unsafe + { + _ConstructAndInitialize(NativeCtor, NativeName, CachedType, refCounted: false); + } + } + + internal unsafe void _ConstructAndInitialize( + delegate* unmanaged<IntPtr> nativeCtor, + StringName nativeName, + Type cachedType, + bool refCounted + ) + { if (NativePtr == IntPtr.Zero) { - unsafe - { - NativePtr = NativeCtor(); - } + NativePtr = nativeCtor(); InteropUtils.TieManagedToUnmanaged(this, NativePtr, - NativeName, refCounted: false, GetType(), _cachedType); + nativeName, refCounted, GetType(), cachedType); } else { InteropUtils.TieManagedToUnmanagedWithPreSetup(this, NativePtr, - GetType(), _cachedType); + GetType(), cachedType); } _weakReferenceToSelf = DisposablesTracker.RegisterGodotObject(this); @@ -191,8 +201,14 @@ namespace Godot internal static bool InternalIsClassNativeBase(Type t) { - var assemblyName = t.Assembly.GetName(); - return assemblyName.Name == "GodotSharp" || assemblyName.Name == "GodotSharpEditor"; + // Check whether the type is declared in the GodotSharp or GodotSharpEditor assemblies + var typeAssembly = t.Assembly; + + if (typeAssembly == CachedType.Assembly) + return true; + + var typeAssemblyName = t.Assembly.GetName(); + return typeAssemblyName.Name == "GodotSharpEditor"; } // ReSharper disable once VirtualMemberNeverOverridden.Global diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index f1a397f8fa..b7fbb81f9c 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -33,7 +33,6 @@ <Compile Include="Core\Array.cs" /> <Compile Include="Core\Attributes\AssemblyHasScriptsAttribute.cs" /> <Compile Include="Core\Attributes\ExportAttribute.cs" /> - <Compile Include="Core\Attributes\GodotMethodAttribute.cs" /> <Compile Include="Core\Attributes\RPCAttribute.cs" /> <Compile Include="Core\Attributes\ScriptPathAttribute.cs" /> <Compile Include="Core\Attributes\SignalAttribute.cs" /> @@ -68,6 +67,7 @@ <Compile Include="Core\Mathf.cs" /> <Compile Include="Core\MathfEx.cs" /> <Compile Include="Core\NativeInterop\ExceptionUtils.cs" /> + <Compile Include="Core\NativeInterop\GodotDllImportResolver.cs" /> <Compile Include="Core\NativeInterop\InteropUtils.cs" /> <Compile Include="Core\NativeInterop\NativeFuncs.extended.cs" /> <Compile Include="Core\NativeInterop\NativeVariantPtrArgs.cs" /> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs b/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs index dbd98d0afb..da6f293871 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs @@ -1,4 +1,3 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("GodotSharpEditor")] -[assembly: InternalsVisibleTo("GodotPlugins")] diff --git a/modules/mono/godotsharp_dirs.cpp b/modules/mono/godotsharp_dirs.cpp index 26ae7246ad..f79a245d7d 100644 --- a/modules/mono/godotsharp_dirs.cpp +++ b/modules/mono/godotsharp_dirs.cpp @@ -235,7 +235,11 @@ private: #endif +#ifdef TOOLS_ENABLED api_assemblies_dir = api_assemblies_base_dir.plus_file(GDMono::get_expected_api_build_config()); +#else + api_assemblies_dir = data_dir_root; +#endif } public: diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp index 7a3fd1af10..0792a88f52 100644 --- a/modules/mono/mono_gd/gd_mono.cpp +++ b/modules/mono/mono_gd/gd_mono.cpp @@ -42,7 +42,9 @@ #include "../utils/path_utils.h" #include "gd_mono_cache.h" +#ifdef TOOLS_ENABLED #include <nethost.h> +#endif #include <coreclr_delegates.h> #include <hostfxr.h> @@ -129,6 +131,7 @@ void gd_mono_debug_init() { } // namespace namespace { +hostfxr_initialize_for_dotnet_command_line_fn hostfxr_initialize_for_dotnet_command_line = nullptr; hostfxr_initialize_for_runtime_config_fn hostfxr_initialize_for_runtime_config = nullptr; hostfxr_get_runtime_delegate_fn hostfxr_get_runtime_delegate = nullptr; hostfxr_close_fn hostfxr_close = nullptr; @@ -151,6 +154,7 @@ HostFxrCharString str_to_hostfxr(const String &p_str) { #endif } +#ifdef TOOLS_ENABLED String str_from_hostfxr(const char_t *p_buffer) { #ifdef _WIN32 return String::utf16((const char16_t *)p_buffer); @@ -158,36 +162,98 @@ String str_from_hostfxr(const char_t *p_buffer) { return String::utf8((const char *)p_buffer); #endif } +#endif const char_t *get_data(const HostFxrCharString &p_char_str) { return (const char_t *)p_char_str.get_data(); } +#ifdef TOOLS_ENABLED +String find_hostfxr(size_t p_known_buffet_size, get_hostfxr_parameters *p_get_hostfxr_params) { + // Pre-allocate a large buffer for the path to hostfxr + Vector<char_t> buffer; + buffer.resize(p_known_buffet_size); + + int rc = get_hostfxr_path(buffer.ptrw(), &p_known_buffet_size, p_get_hostfxr_params); + + ERR_FAIL_COND_V_MSG(rc != 0, String(), "get_hostfxr_path failed with code: " + itos(rc)); + + return str_from_hostfxr(buffer.ptr()); +} +#endif + String find_hostfxr() { +#ifdef TOOLS_ENABLED + const int CoreHostLibMissingFailure = 0x80008083; const int HostApiBufferTooSmall = 0x80008098; size_t buffer_size = 0; int rc = get_hostfxr_path(nullptr, &buffer_size, nullptr); if (rc == HostApiBufferTooSmall) { - // Pre-allocate a large buffer for the path to hostfxr - Vector<char_t> buffer; - buffer.resize(buffer_size); - - rc = get_hostfxr_path(buffer.ptrw(), &buffer_size, nullptr); + return find_hostfxr(buffer_size, nullptr); + } - if (rc != 0) { - return String(); + if (rc == CoreHostLibMissingFailure) { + // Apparently `get_hostfxr_path` doesn't look for dotnet in `PATH`? (I suppose it needs the + // `DOTNET_ROOT` environment variable). If it fails, we try to find the dotnet executable + // in `PATH` ourselves and pass its location as `dotnet_root` to `get_hostfxr_path`. + String dotnet_exe = path::find_executable("dotnet"); + + if (!dotnet_exe.is_empty()) { + // The file found in PATH may be a symlink + dotnet_exe = path::abspath(path::realpath(dotnet_exe)); + + // TODO: + // Sometimes, the symlink may not point to the dotnet executable in the dotnet root. + // That's the case with snaps. The snap install should have been found with the + // previous `get_hostfxr_path`, but it would still be better to do this properly + // and use something like `dotnet --list-sdks/runtimes` to find the actual location. + // This way we could also check if the proper sdk or runtime is installed. This would + // allow us to fail gracefully and show some helpful information in the editor. + + HostFxrCharString dotnet_root = str_to_hostfxr(dotnet_exe.get_base_dir()); + + get_hostfxr_parameters get_hostfxr_parameters = { + sizeof(get_hostfxr_parameters), + nullptr, + get_data(dotnet_root) + }; + + buffer_size = 0; + rc = get_hostfxr_path(nullptr, &buffer_size, &get_hostfxr_parameters); + if (rc == HostApiBufferTooSmall) { + return find_hostfxr(buffer_size, &get_hostfxr_parameters); + } } + } - return str_from_hostfxr(buffer.ptr()); + if (rc == CoreHostLibMissingFailure) { + ERR_PRINT(String() + ".NET: One of the dependent libraries is missing. " + + "Typically when the `hostfxr`, `hostpolicy` or `coreclr` dynamic " + + "libraries are not present in the expected locations."); } return String(); +#else + +#if defined(WINDOWS_ENABLED) + return GodotSharpDirs::get_api_assemblies_dir() + .plus_file("hostfxr.dll"); +#elif defined(MACOS_ENABLED) + return GodotSharpDirs::get_api_assemblies_dir() + .plus_file("libhostfxr.dylib"); +#elif defined(UNIX_ENABLED) + return GodotSharpDirs::get_api_assemblies_dir() + .plus_file("libhostfxr.so"); +#else +#error "Platform not supported (yet?)" +#endif + +#endif } -// Forward declarations -bool load_hostfxr() { +bool load_hostfxr(void *&r_hostfxr_dll_handle) { String hostfxr_path = find_hostfxr(); if (hostfxr_path.is_empty()) { @@ -196,16 +262,20 @@ bool load_hostfxr() { print_verbose("Found hostfxr: " + hostfxr_path); - void *lib = nullptr; - Error err = OS::get_singleton()->open_dynamic_library(hostfxr_path, lib); - // TODO: Clean up lib handle when shutting down + Error err = OS::get_singleton()->open_dynamic_library(hostfxr_path, r_hostfxr_dll_handle); if (err != OK) { return false; } + void *lib = r_hostfxr_dll_handle; + void *symbol = nullptr; + err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "hostfxr_initialize_for_dotnet_command_line", symbol); + ERR_FAIL_COND_V(err != OK, false); + hostfxr_initialize_for_dotnet_command_line = (hostfxr_initialize_for_dotnet_command_line_fn)symbol; + err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "hostfxr_initialize_for_runtime_config", symbol); ERR_FAIL_COND_V(err != OK, false); hostfxr_initialize_for_runtime_config = (hostfxr_initialize_for_runtime_config_fn)symbol; @@ -223,12 +293,13 @@ bool load_hostfxr() { hostfxr_close); } -load_assembly_and_get_function_pointer_fn initialize_hostfxr(const char_t *p_config_path) { +#ifdef TOOLS_ENABLED +load_assembly_and_get_function_pointer_fn initialize_hostfxr_for_config(const char_t *p_config_path) { hostfxr_handle cxt = nullptr; int rc = hostfxr_initialize_for_runtime_config(p_config_path, nullptr, &cxt); if (rc != 0 || cxt == nullptr) { hostfxr_close(cxt); - ERR_FAIL_V_MSG(nullptr, "hostfxr_initialize_for_runtime_config failed"); + ERR_FAIL_V_MSG(nullptr, "hostfxr_initialize_for_runtime_config failed with code: " + itos(rc)); } void *load_assembly_and_get_function_pointer = nullptr; @@ -236,13 +307,122 @@ load_assembly_and_get_function_pointer_fn initialize_hostfxr(const char_t *p_con rc = hostfxr_get_runtime_delegate(cxt, hdt_load_assembly_and_get_function_pointer, &load_assembly_and_get_function_pointer); if (rc != 0 || load_assembly_and_get_function_pointer == nullptr) { - ERR_FAIL_V_MSG(nullptr, "hostfxr_get_runtime_delegate failed"); + ERR_FAIL_V_MSG(nullptr, "hostfxr_get_runtime_delegate failed with code: " + itos(rc)); } hostfxr_close(cxt); return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer; } +#else +load_assembly_and_get_function_pointer_fn initialize_hostfxr_self_contained( + const char_t *p_main_assembly_path) { + hostfxr_handle cxt = nullptr; + + List<String> cmdline_args = OS::get_singleton()->get_cmdline_args(); + + List<HostFxrCharString> argv_store; + Vector<const char_t *> argv; + argv.resize(cmdline_args.size() + 1); + + argv.write[0] = p_main_assembly_path; + + int i = 1; + for (const String &E : cmdline_args) { + HostFxrCharString &stored = argv_store.push_back(str_to_hostfxr(E))->get(); + argv.write[i] = stored.ptr(); + i++; + } + + int rc = hostfxr_initialize_for_dotnet_command_line(argv.size(), argv.ptrw(), nullptr, &cxt); + if (rc != 0 || cxt == nullptr) { + hostfxr_close(cxt); + ERR_FAIL_V_MSG(nullptr, "hostfxr_initialize_for_dotnet_command_line failed with code: " + itos(rc)); + } + + void *load_assembly_and_get_function_pointer = nullptr; + + rc = hostfxr_get_runtime_delegate(cxt, + hdt_load_assembly_and_get_function_pointer, &load_assembly_and_get_function_pointer); + if (rc != 0 || load_assembly_and_get_function_pointer == nullptr) { + ERR_FAIL_V_MSG(nullptr, "hostfxr_get_runtime_delegate failed with code: " + itos(rc)); + } + + hostfxr_close(cxt); + + return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer; +} +#endif + +#ifdef TOOLS_ENABLED +using godot_plugins_initialize_fn = bool (*)(bool, gdmono::PluginCallbacks *, GDMonoCache::ManagedCallbacks *); +#else +using godot_plugins_initialize_fn = bool (*)(GDMonoCache::ManagedCallbacks *); +#endif + +#ifdef TOOLS_ENABLED +godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime_initialized) { + godot_plugins_initialize_fn godot_plugins_initialize = nullptr; + + HostFxrCharString godot_plugins_path = str_to_hostfxr( + GodotSharpDirs::get_api_assemblies_dir().plus_file("GodotPlugins.dll")); + + HostFxrCharString config_path = str_to_hostfxr( + GodotSharpDirs::get_api_assemblies_dir().plus_file("GodotPlugins.runtimeconfig.json")); + + load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = + initialize_hostfxr_for_config(get_data(config_path)); + ERR_FAIL_NULL_V(load_assembly_and_get_function_pointer, nullptr); + + r_runtime_initialized = true; + + print_verbose(".NET: hostfxr initialized"); + + int rc = load_assembly_and_get_function_pointer(get_data(godot_plugins_path), + HOSTFXR_STR("GodotPlugins.Main, GodotPlugins"), + HOSTFXR_STR("InitializeFromEngine"), + UNMANAGEDCALLERSONLY_METHOD, + nullptr, + (void **)&godot_plugins_initialize); + ERR_FAIL_COND_V_MSG(rc != 0, nullptr, ".NET: Failed to get GodotPlugins initialization function pointer"); + + return godot_plugins_initialize; +} +#else +godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime_initialized) { + String appname = ProjectSettings::get_singleton()->get("application/config/name"); + String appname_safe = OS::get_singleton()->get_safe_dir_name(appname); + if (appname_safe.is_empty()) { + appname_safe = "UnnamedProject"; + } + + godot_plugins_initialize_fn godot_plugins_initialize = nullptr; + + String assembly_name = appname_safe; + + HostFxrCharString assembly_path = str_to_hostfxr(GodotSharpDirs::get_api_assemblies_dir() + .plus_file(assembly_name + ".dll")); + + load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = + initialize_hostfxr_self_contained(get_data(assembly_path)); + ERR_FAIL_NULL_V(load_assembly_and_get_function_pointer, nullptr); + + r_runtime_initialized = true; + + print_verbose(".NET: hostfxr initialized"); + + int rc = load_assembly_and_get_function_pointer(get_data(assembly_path), + str_to_hostfxr("GodotPlugins.Game.Main, " + assembly_name), + HOSTFXR_STR("InitializeFromGameProject"), + UNMANAGEDCALLERSONLY_METHOD, + nullptr, + (void **)&godot_plugins_initialize); + ERR_FAIL_COND_V_MSG(rc != 0, nullptr, ".NET: Failed to get GodotPlugins initialization function pointer"); + + return godot_plugins_initialize; +} +#endif + } // namespace static bool _on_core_api_assembly_loaded() { @@ -261,61 +441,44 @@ static bool _on_core_api_assembly_loaded() { } void GDMono::initialize() { - ERR_FAIL_NULL(Engine::get_singleton()); - print_verbose(".NET: Initializing module..."); _init_godot_api_hashes(); - if (!load_hostfxr()) { + if (!load_hostfxr(hostfxr_dll_handle)) { ERR_FAIL_MSG(".NET: Failed to load hostfxr"); } - auto config_path = str_to_hostfxr(GodotSharpDirs::get_api_assemblies_dir() - .plus_file("GodotPlugins.runtimeconfig.json")); - - load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = - initialize_hostfxr(get_data(config_path)); - ERR_FAIL_NULL(load_assembly_and_get_function_pointer); - - runtime_initialized = true; - - print_verbose(".NET: hostfxr initialized"); - - auto godot_plugins_path = str_to_hostfxr(GodotSharpDirs::get_api_assemblies_dir() - .plus_file("GodotPlugins.dll")); - - using godot_plugins_initialize_fn = bool (*)(bool, PluginCallbacks *, GDMonoCache::ManagedCallbacks *); - godot_plugins_initialize_fn godot_plugins_initialize = nullptr; - - int rc = load_assembly_and_get_function_pointer(get_data(godot_plugins_path), - HOSTFXR_STR("GodotPlugins.Main, GodotPlugins"), - HOSTFXR_STR("Initialize"), - UNMANAGEDCALLERSONLY_METHOD, - nullptr, - (void **)&godot_plugins_initialize); - ERR_FAIL_COND_MSG(rc != 0, ".NET: Failed to get Godot.Plugins Initialize function pointer"); + godot_plugins_initialize_fn godot_plugins_initialize = + initialize_hostfxr_and_godot_plugins(runtime_initialized); + ERR_FAIL_NULL(godot_plugins_initialize); - PluginCallbacks aux_plugin_callbacks; GDMonoCache::ManagedCallbacks managed_callbacks; + +#ifdef TOOLS_ENABLED + gdmono::PluginCallbacks plugin_callbacks_res; bool init_ok = godot_plugins_initialize(Engine::get_singleton()->is_editor_hint(), - &aux_plugin_callbacks, &managed_callbacks); - ERR_FAIL_COND_MSG(!init_ok, ".NET: Call to Godot.Plugins Initialize failed"); + &plugin_callbacks_res, &managed_callbacks); + ERR_FAIL_COND_MSG(!init_ok, ".NET: GodotPlugins initialization failed"); + + plugin_callbacks = plugin_callbacks_res; +#else + bool init_ok = godot_plugins_initialize(&managed_callbacks); + ERR_FAIL_COND_MSG(!init_ok, ".NET: GodotPlugins initialization failed"); +#endif GDMonoCache::update_godot_api_cache(managed_callbacks); - plugin_callbacks = aux_plugin_callbacks; print_verbose(".NET: GodotPlugins initialized"); _on_core_api_assembly_loaded(); } +#ifdef TOOLS_ENABLED void GDMono::initialize_load_assemblies() { -#if defined(TOOLS_ENABLED) if (Engine::get_singleton()->is_project_manager_hint()) { return; } -#endif // Load the project's main assembly. This doesn't necessarily need to succeed. // The game may not be using .NET at all, or if the project does use .NET and @@ -326,6 +489,7 @@ void GDMono::initialize_load_assemblies() { } } } +#endif void GDMono::_init_godot_api_hashes() { #ifdef DEBUG_METHODS_ENABLED @@ -337,6 +501,7 @@ void GDMono::_init_godot_api_hashes() { #endif // DEBUG_METHODS_ENABLED } +#ifdef TOOLS_ENABLED bool GDMono::_load_project_assembly() { String appname = ProjectSettings::get_singleton()->get("application/config/name"); String appname_safe = OS::get_singleton()->get_safe_dir_name(appname); @@ -350,6 +515,7 @@ bool GDMono::_load_project_assembly() { return plugin_callbacks.LoadProjectAssemblyCallback(assembly_path.utf16()); } +#endif #warning TODO hot-reload #if 0 @@ -479,6 +645,10 @@ GDMono::~GDMono() { } } + if (hostfxr_dll_handle) { + OS::get_singleton()->close_dynamic_library(hostfxr_dll_handle); + } + finalizing_scripts_domain = false; runtime_initialized = false; diff --git a/modules/mono/mono_gd/gd_mono.h b/modules/mono/mono_gd/gd_mono.h index fee1cab9c7..66ed331b67 100644 --- a/modules/mono/mono_gd/gd_mono.h +++ b/modules/mono/mono_gd/gd_mono.h @@ -35,11 +35,36 @@ #include "../godotsharp_defs.h" +#ifdef WIN32 +#define GD_CLR_STDCALL __stdcall +#else +#define GD_CLR_STDCALL +#endif + +namespace gdmono { + +#ifdef TOOLS_ENABLED +struct PluginCallbacks { + using FuncLoadProjectAssemblyCallback = bool(GD_CLR_STDCALL *)(const char16_t *); + using FuncLoadToolsAssemblyCallback = Object *(GD_CLR_STDCALL *)(const char16_t *); + FuncLoadProjectAssemblyCallback LoadProjectAssemblyCallback = nullptr; + FuncLoadToolsAssemblyCallback LoadToolsAssemblyCallback = nullptr; +}; +#endif + +} // namespace gdmono + +#undef GD_CLR_STDCALL + class GDMono { bool runtime_initialized; bool finalizing_scripts_domain; + void *hostfxr_dll_handle = nullptr; + +#ifdef TOOLS_ENABLED bool _load_project_assembly(); +#endif bool _try_load_api_assemblies(); @@ -51,18 +76,9 @@ class GDMono { #endif void _init_godot_api_hashes(); - friend class CSharpLanguage; -#ifdef WIN32 -#define GD_CLR_STDCALL __stdcall -#else -#define GD_CLR_STDCALL +#ifdef TOOLS_ENABLED + gdmono::PluginCallbacks plugin_callbacks; #endif - struct PluginCallbacks { - using FuncLoadProjectAssemblyCallback = bool(GD_CLR_STDCALL *)(const char16_t *); - using FuncLoadToolsAssemblyCallback = Object *(GD_CLR_STDCALL *)(const char16_t *); - FuncLoadProjectAssemblyCallback LoadProjectAssemblyCallback = nullptr; - FuncLoadToolsAssemblyCallback LoadToolsAssemblyCallback = nullptr; - } plugin_callbacks; protected: static GDMono *singleton; @@ -102,12 +118,18 @@ public: _FORCE_INLINE_ bool is_runtime_initialized() const { return runtime_initialized; } _FORCE_INLINE_ bool is_finalizing_scripts_domain() { return finalizing_scripts_domain; } +#ifdef TOOLS_ENABLED + const gdmono::PluginCallbacks &get_plugin_callbacks() { return plugin_callbacks; } +#endif + #ifdef GD_MONO_HOT_RELOAD Error reload_scripts_domain(); #endif void initialize(); +#ifdef TOOLS_ENABLED void initialize_load_assemblies(); +#endif GDMono(); ~GDMono(); diff --git a/modules/mono/mono_gd/gd_mono_cache.cpp b/modules/mono/mono_gd/gd_mono_cache.cpp index e8b25cb119..7d33f0a896 100644 --- a/modules/mono/mono_gd/gd_mono_cache.cpp +++ b/modules/mono/mono_gd/gd_mono_cache.cpp @@ -54,7 +54,6 @@ void update_godot_api_cache(const ManagedCallbacks &p_managed_callbacks) { CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, RaiseEventSignal); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, GetScriptSignalList); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, HasScriptSignal); - CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, HasMethodUnknownParams); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, ScriptIsOrInherits); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, AddScriptBridge); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, RemoveScriptBridge); @@ -65,6 +64,7 @@ void update_godot_api_cache(const ManagedCallbacks &p_managed_callbacks) { CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, Get); CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, CallDispose); CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, CallToString); + CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, HasMethodUnknownParams); CHECK_CALLBACK_NOT_NULL(GCHandleBridge, FreeGCHandle); CHECK_CALLBACK_NOT_NULL(DebuggingUtils, InstallTraceListener); CHECK_CALLBACK_NOT_NULL(Dispatcher, InitializeDefaultGodotTaskScheduler); diff --git a/modules/mono/mono_gd/gd_mono_cache.h b/modules/mono/mono_gd/gd_mono_cache.h index 17c8c9fa51..b993facff9 100644 --- a/modules/mono/mono_gd/gd_mono_cache.h +++ b/modules/mono/mono_gd/gd_mono_cache.h @@ -64,7 +64,6 @@ struct ManagedCallbacks { using FuncScriptManagerBridge_RaiseEventSignal = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *, const Variant **, int, bool *); using FuncScriptManagerBridge_GetScriptSignalList = void(GD_CLR_STDCALL *)(const CSharpScript *, Dictionary *); using FuncScriptManagerBridge_HasScriptSignal = bool(GD_CLR_STDCALL *)(const CSharpScript *, const String *); - using FuncScriptManagerBridge_HasMethodUnknownParams = bool(GD_CLR_STDCALL *)(const CSharpScript *, const String *, bool); using FuncScriptManagerBridge_ScriptIsOrInherits = bool(GD_CLR_STDCALL *)(const CSharpScript *, const CSharpScript *); using FuncScriptManagerBridge_AddScriptBridge = bool(GD_CLR_STDCALL *)(const CSharpScript *, const String *); using FuncScriptManagerBridge_RemoveScriptBridge = void(GD_CLR_STDCALL *)(const CSharpScript *); @@ -75,6 +74,7 @@ struct ManagedCallbacks { using FuncCSharpInstanceBridge_Get = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *, Variant *); using FuncCSharpInstanceBridge_CallDispose = void(GD_CLR_STDCALL *)(GCHandleIntPtr, bool); using FuncCSharpInstanceBridge_CallToString = void(GD_CLR_STDCALL *)(GCHandleIntPtr, String *, bool *); + using FuncCSharpInstanceBridge_HasMethodUnknownParams = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *); using FuncGCHandleBridge_FreeGCHandle = void(GD_CLR_STDCALL *)(GCHandleIntPtr); using FuncDebuggingUtils_InstallTraceListener = void(GD_CLR_STDCALL *)(); using FuncDispatcher_InitializeDefaultGodotTaskScheduler = void(GD_CLR_STDCALL *)(); @@ -91,7 +91,6 @@ struct ManagedCallbacks { FuncScriptManagerBridge_RaiseEventSignal ScriptManagerBridge_RaiseEventSignal; FuncScriptManagerBridge_GetScriptSignalList ScriptManagerBridge_GetScriptSignalList; FuncScriptManagerBridge_HasScriptSignal ScriptManagerBridge_HasScriptSignal; - FuncScriptManagerBridge_HasMethodUnknownParams ScriptManagerBridge_HasMethodUnknownParams; FuncScriptManagerBridge_ScriptIsOrInherits ScriptManagerBridge_ScriptIsOrInherits; FuncScriptManagerBridge_AddScriptBridge ScriptManagerBridge_AddScriptBridge; FuncScriptManagerBridge_RemoveScriptBridge ScriptManagerBridge_RemoveScriptBridge; @@ -102,6 +101,7 @@ struct ManagedCallbacks { FuncCSharpInstanceBridge_Get CSharpInstanceBridge_Get; FuncCSharpInstanceBridge_CallDispose CSharpInstanceBridge_CallDispose; FuncCSharpInstanceBridge_CallToString CSharpInstanceBridge_CallToString; + FuncCSharpInstanceBridge_HasMethodUnknownParams CSharpInstanceBridge_HasMethodUnknownParams; FuncGCHandleBridge_FreeGCHandle GCHandleBridge_FreeGCHandle; FuncDebuggingUtils_InstallTraceListener DebuggingUtils_InstallTraceListener; FuncDispatcher_InitializeDefaultGodotTaskScheduler Dispatcher_InitializeDefaultGodotTaskScheduler; @@ -118,4 +118,6 @@ inline void clear_godot_api_cache() { } } // namespace GDMonoCache +#undef GD_CLR_STDCALL + #endif // GD_MONO_CACHE_H diff --git a/modules/mono/utils/path_utils.cpp b/modules/mono/utils/path_utils.cpp index a1905dfcfe..19ad59a1bc 100644 --- a/modules/mono/utils/path_utils.cpp +++ b/modules/mono/utils/path_utils.cpp @@ -51,6 +51,37 @@ namespace path { +String find_executable(const String &p_name) { +#ifdef WINDOWS_ENABLED + Vector<String> exts = OS::get_singleton()->get_environment("PATHEXT").split(ENV_PATH_SEP, false); +#endif + Vector<String> env_path = OS::get_singleton()->get_environment("PATH").split(ENV_PATH_SEP, false); + + if (env_path.is_empty()) { + return String(); + } + + for (int i = 0; i < env_path.size(); i++) { + String p = path::join(env_path[i], p_name); + +#ifdef WINDOWS_ENABLED + for (int j = 0; j < exts.size(); j++) { + String p2 = p + exts[j].to_lower(); // lowercase to reduce risk of case mismatch warning + + if (FileAccess::exists(p2)) { + return p2; + } + } +#else + if (FileAccess::exists(p)) { + return p; + } +#endif + } + + return String(); +} + String cwd() { #ifdef WINDOWS_ENABLED const DWORD expected_size = ::GetCurrentDirectoryW(0, nullptr); diff --git a/modules/mono/utils/path_utils.h b/modules/mono/utils/path_utils.h index 9a2c757361..d1c3d3ccfd 100644 --- a/modules/mono/utils/path_utils.h +++ b/modules/mono/utils/path_utils.h @@ -36,6 +36,8 @@ namespace path { +String find_executable(const String &p_name); + String join(const String &p_a, const String &p_b); String join(const String &p_a, const String &p_b, const String &p_c); String join(const String &p_a, const String &p_b, const String &p_c, const String &p_d); |