diff options
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); |