diff options
Diffstat (limited to 'modules/mono/editor/GodotTools')
46 files changed, 1361 insertions, 1323 deletions
diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs index 2bf1cb7a18..01aa65bfc3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs @@ -7,8 +7,6 @@ namespace GodotTools.BuildLogger { public class GodotBuildLogger : ILogger { - public static readonly string AssemblyPath = Path.GetFullPath(typeof(GodotBuildLogger).Assembly.Location); - public string Parameters { get; set; } public LoggerVerbosity Verbosity { get; set; } diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj index 0afec970c6..9e36497b06 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj @@ -5,6 +5,6 @@ <LangVersion>7.2</LangVersion> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Build.Framework" Version="16.5.0" /> + <PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" ExcludeAssets="runtime" /> </ItemGroup> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj index d6d8962f90..cfd5c88a58 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid> - <TargetFramework>netstandard2.0</TargetFramework> - <LangVersion>7.2</LangVersion> + <TargetFramework>net6.0</TargetFramework> + <LangVersion>10</LangVersion> </PropertyGroup> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs index 60a4f297c9..7c5502814f 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs @@ -34,7 +34,7 @@ namespace GodotTools.Core path = path.Replace('\\', '/'); path = path[path.Length - 1] == '/' ? path.Substring(0, path.Length - 1) : path; - string[] parts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); + string[] parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); path = string.Join(Path.DirectorySeparatorChar.ToString(), parts).Trim(); @@ -60,7 +60,7 @@ namespace GodotTools.Core public static string ToSafeDirName(this string dirName, bool allowDirSeparator = false) { - var invalidChars = new List<string> {":", "*", "?", "\"", "<", ">", "|"}; + var invalidChars = new List<string> { ":", "*", "?", "\"", "<", ">", "|" }; if (allowDirSeparator) { diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj index 303ca3a293..d2132115f3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ProjectGuid>{B06C2951-C8E3-4F28-80B2-717CF327EB19}</ProjectGuid> <OutputType>Exe</OutputType> diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs index bc09e1ebf9..72e2a1fc0d 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs @@ -123,12 +123,16 @@ namespace GodotTools.IdeMessaging string projectMetadataDir = Path.Combine(godotProjectDir, ".godot", "mono", "metadata"); // FileSystemWatcher requires an existing directory - if (!Directory.Exists(projectMetadataDir)) { + if (!Directory.Exists(projectMetadataDir)) + { // Check if the non hidden version exists string nonHiddenProjectMetadataDir = Path.Combine(godotProjectDir, "godot", "mono", "metadata"); - if (Directory.Exists(nonHiddenProjectMetadataDir)) { + if (Directory.Exists(nonHiddenProjectMetadataDir)) + { projectMetadataDir = nonHiddenProjectMetadataDir; - } else { + } + else + { Directory.CreateDirectory(projectMetadataDir); } } diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs index 686202e81e..2448a2953b 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs @@ -25,10 +25,7 @@ namespace GodotTools.IdeMessaging public override bool Equals(object obj) { - if (obj is GodotIdeMetadata metadata) - return metadata == this; - - return false; + return obj is GodotIdeMetadata metadata && metadata == this; } public bool Equals(GodotIdeMetadata other) diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs index 10d7e1898e..dd3913b4f3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs @@ -78,7 +78,7 @@ namespace GodotTools.IdeMessaging clientStream.WriteTimeout = ClientWriteTimeout; clientReader = new StreamReader(clientStream, Encoding.UTF8); - clientWriter = new StreamWriter(clientStream, Encoding.UTF8) {NewLine = "\n"}; + clientWriter = new StreamWriter(clientStream, Encoding.UTF8) { NewLine = "\n" }; } public async Task Process() diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs index 548e7f06ee..a57c82b608 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs @@ -17,7 +17,7 @@ namespace GodotTools.IdeMessaging if (content.Status == MessageStatus.Ok) SetResult(JsonConvert.DeserializeObject<T>(content.Body)); else - SetResult(new T {Status = content.Status}); + SetResult(new T { Status = content.Status }); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs index d84a63c83c..a285d5fa97 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs @@ -21,14 +21,14 @@ namespace GodotTools.IdeMessaging.Utils public void OnCompleted(Action continuation) { if (this.continuation != null) - throw new InvalidOperationException("This awaiter has already been listened"); + throw new InvalidOperationException("This awaiter already has a continuation."); this.continuation = continuation; } public void SetResult(T result) { if (IsCompleted) - throw new InvalidOperationException("This awaiter is already completed"); + throw new InvalidOperationException("This awaiter is already completed."); IsCompleted = true; this.result = result; @@ -39,7 +39,7 @@ namespace GodotTools.IdeMessaging.Utils public void SetException(Exception exception) { if (IsCompleted) - throw new InvalidOperationException("This awaiter is already completed"); + throw new InvalidOperationException("This awaiter is already completed."); IsCompleted = true; this.exception = exception; diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs index 9d593fbf8a..ab21527344 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs @@ -13,7 +13,7 @@ namespace GodotTools.IdeMessaging.Utils return waitAsyncTask.ContinueWith<IDisposable>(t => wrapper, cancellationToken).ConfigureAwait(false); } - private struct SemaphoreSlimWaitReleaseWrapper : IDisposable + private readonly struct SemaphoreSlimWaitReleaseWrapper : IDisposable { private readonly SemaphoreSlim semaphoreSlim; diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj index 5b3ed0b1b7..c05096bdcc 100644 --- a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ProjectGuid>{EAFFF236-FA96-4A4D-BD23-0E51EF988277}</ProjectGuid> <OutputType>Exe</OutputType> diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs index 7a4641dbbc..cc0deb6cc0 100644 --- a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs @@ -249,8 +249,8 @@ namespace GodotTools.OpenVisualStudio // Thread call was rejected, so try again. int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType) { + // flag = SERVERCALL_RETRYLATER if (dwRejectType == 2) - // flag = SERVERCALL_RETRYLATER { // Retry the thread call immediately if return >= 0 & < 100 return 99; diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index 37123ba2b2..bde14b2b40 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -1,32 +1,16 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid> - <TargetFramework>net472</TargetFramework> - <LangVersion>7.2</LangVersion> + <TargetFramework>net6.0</TargetFramework> + <LangVersion>10</LangVersion> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Build" Version="16.5.0" /> + <PackageReference Include="Microsoft.Build" Version="15.1.548" ExcludeAssets="runtime" /> + <PackageReference Include="Microsoft.Build.Locator" Version="1.2.6" /> <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" /> <ProjectReference Include="..\GodotTools.Shared\GodotTools.Shared.csproj" /> </ItemGroup> - <!-- - The Microsoft.Build.Runtime package is too problematic so we create a MSBuild.exe stub. The workaround described - here doesn't work with Microsoft.NETFramework.ReferenceAssemblies: https://github.com/microsoft/msbuild/issues/3486 - We need a MSBuild.exe file as there's an issue in Microsoft.Build where it executes platform dependent code when - searching for MSBuild.exe before the fallback to not using it. A stub is fine as it should never be executed. - --> - <ItemGroup> - <None Include="MSBuild.exe" CopyToOutputDirectory="Always" /> - </ItemGroup> - <Target Name="CopyMSBuildStubWindows" AfterTargets="Build" Condition=" '$(GodotPlatform)' == 'windows' Or ( '$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT' ) "> - <PropertyGroup> - <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath> - <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir> - </PropertyGroup> - <!-- Need to copy it here as well on Windows --> - <Copy SourceFiles="MSBuild.exe" DestinationFiles="$(GodotOutputDataDir)\Mono\lib\mono\v4.0\MSBuild.exe" /> - </Target> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe deleted file mode 100644 index e69de29bb2..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe +++ /dev/null diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index 7d49d251dd..f3c8e89dff 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; @@ -14,14 +15,15 @@ namespace GodotTools.ProjectEditor public static ProjectRootElement GenGameProject(string name) { if (name.Length == 0) - throw new ArgumentException("Project name is empty", nameof(name)); + throw new ArgumentException("Project name is empty.", nameof(name)); var root = ProjectRootElement.Create(NewProjectFileOptions.None); root.Sdk = GodotSdkAttrValue; var mainGroup = root.AddPropertyGroup(); - mainGroup.AddProperty("TargetFramework", "netstandard2.1"); + mainGroup.AddProperty("TargetFramework", "net6.0"); + mainGroup.AddProperty("EnableDynamicLoading", "true"); string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true); @@ -35,7 +37,7 @@ namespace GodotTools.ProjectEditor public static string GenAndSaveGameProject(string dir, string name) { if (name.Length == 0) - throw new ArgumentException("Project name is empty", nameof(name)); + throw new ArgumentException("Project name is empty.", nameof(name)); string path = Path.Combine(dir, name + ".csproj"); @@ -44,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 cdac9acb25..7b1d5c228a 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -19,6 +19,16 @@ namespace GodotTools.ProjectEditor public static class ProjectUtils { + 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) { var root = ProjectRootElement.Open(path); @@ -42,7 +52,8 @@ namespace GodotTools.ProjectEditor var root = project.Root; string godotSdkAttrValue = ProjectGenerator.GodotSdkAttrValue; - if (!string.IsNullOrEmpty(root.Sdk) && root.Sdk.Trim().Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(root.Sdk) && + root.Sdk.Trim().Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase)) return; root.Sdk = godotSdkAttrValue; diff --git a/modules/mono/editor/GodotTools/GodotTools.Shared/GenerateGodotNupkgsVersions.targets b/modules/mono/editor/GodotTools/GodotTools.Shared/GenerateGodotNupkgsVersions.targets index aab2d73bdd..37bd4a0be0 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Shared/GenerateGodotNupkgsVersions.targets +++ b/modules/mono/editor/GodotTools/GodotTools.Shared/GenerateGodotNupkgsVersions.targets @@ -8,8 +8,8 @@ </Target> <Target Name="GenerateGodotNupkgsVersionsFile" - DependsOnTargets="PrepareForBuild;_GenerateGodotNupkgsVersionsFile" - BeforeTargets="BeforeCompile;CoreCompile"> + DependsOnTargets="_GenerateGodotNupkgsVersionsFile" + BeforeTargets="PrepareForBuild;CompileDesignTime;BeforeCompile;CoreCompile"> <ItemGroup> <Compile Include="$(GeneratedGodotNupkgsVersionsFile)" /> <FileWrites Include="$(GeneratedGodotNupkgsVersionsFile)" /> @@ -21,10 +21,13 @@ Outputs="$(GeneratedGodotNupkgsVersionsFile)"> <PropertyGroup> <GenerateGodotNupkgsVersionsCode><![CDATA[ -namespace $(RootNamespace) { - public class GeneratedGodotNupkgsVersions { +namespace $(RootNamespace) +{ + public class GeneratedGodotNupkgsVersions + { public const string GodotNETSdk = "$(PackageVersion_Godot_NET_Sdk)"%3b public const string GodotSourceGenerators = "$(PackageVersion_Godot_SourceGenerators)"%3b + public const string GodotSharp = "$(PackageVersion_GodotSharp)"%3b } } ]]></GenerateGodotNupkgsVersionsCode> diff --git a/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj b/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj index 3bc1698c15..d60e6343ea 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj @@ -1,6 +1,8 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>net6.0</TargetFramework> + <!-- Specify compile items manually to avoid including dangling generated items. --> + <EnableDefaultCompileItems>false</EnableDefaultCompileItems> </PropertyGroup> <Import Project="GenerateGodotNupkgsVersions.targets" /> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln index d3107a69db..564775635d 100644 --- a/modules/mono/editor/GodotTools/GodotTools.sln +++ b/modules/mono/editor/GodotTools/GodotTools.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2012 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.ProjectEditor", "GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj", "{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}" @@ -15,6 +15,10 @@ 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators.Internal", "..\..\glue\GodotSharp\Godot.SourceGenerators.Internal\Godot.SourceGenerators.Internal.csproj", "{55666071-BEC1-4A52-8A98-9A4A7A947DBF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,5 +53,13 @@ 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 + {55666071-BEC1-4A52-8A98-9A4A7A947DBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55666071-BEC1-4A52-8A98-9A4A7A947DBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55666071-BEC1-4A52-8A98-9A4A7A947DBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55666071-BEC1-4A52-8A98-9A4A7A947DBF}.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..edbf53a389 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs @@ -4,28 +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 bool Restore { get; } + public string Solution { get; private set; } + public string Configuration { get; private set; } + public string? RuntimeIdentifier { get; private set; } + public string? PublishOutputDir { get; private set; } + public bool Restore { get; private set; } + public bool Rebuild { get; private set; } + public bool OnlyClean { get; private set; } + // TODO Use List once we have proper serialization - public Array<string> CustomProperties { get; } = new Array<string>(); + public Godot.Collections.Array CustomProperties { get; private set; } = new(); - 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 false; + return obj is BuildInfo other && + 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; } public override int GetHashCode() @@ -34,25 +42,44 @@ 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; } } + // Needed for instantiation from Godot, after reloading assemblies private BuildInfo() { + Solution = string.Empty; + Configuration = string.Empty; + } + + 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 21bff70b15..993c6d9217 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs @@ -1,12 +1,10 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; -using GodotTools.Ides.Rider; +using Godot; using GodotTools.Internals; -using JetBrains.Annotations; -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,14 +55,14 @@ 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"); + throw new InvalidOperationException("A build is already in progress."); _buildInProgress = buildInfo; @@ -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; } @@ -117,7 +111,7 @@ namespace GodotTools.Build public static async Task<bool> BuildAsync(BuildInfo buildInfo) { if (_buildInProgress != null) - throw new InvalidOperationException("A build is already in progress"); + throw new InvalidOperationException("A build is already in progress."); _buildInProgress = buildInfo; @@ -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, [CanBeNull] string[] targets = null, [CanBeNull] 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,31 +209,109 @@ namespace GodotTools.Build if (!File.Exists(buildInfo.Solution)) return true; // No solution to build - // Make sure the API assemblies are up to date before building the project. - // We may not have had the chance to update the release API assemblies, and the debug ones - // may have been deleted by the user at some point after they were loaded by the Godot editor. - string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(buildInfo.Configuration == "ExportRelease" ? "Release" : "Debug"); + using var pr = new EditorProgress("dotnet_build_project", "Building .NET project...", 1); + + pr.Step("Building project solution", 0); - if (!string.IsNullOrEmpty(apiAssembliesUpdateError)) + if (!Build(buildInfo)) { - ShowBuildErrorDialog("Failed to update the Godot API assemblies"); + ShowBuildErrorDialog("Failed to build project solution"); return false; } - using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1)) + 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)) { - pr.Step("Building project solution", 0); + ShowBuildErrorDialog("Failed to clean project solution"); + return false; + } - if (!Build(buildInfo)) - { - ShowBuildErrorDialog("Failed to build 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)) @@ -215,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) @@ -226,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 ebdaca0ce8..4d40724a83 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs @@ -1,17 +1,16 @@ using Godot; using System; -using Godot.Collections; +using System.Diagnostics.CodeAnalysis; using GodotTools.Internals; -using JetBrains.Annotations; using File = GodotTools.Utils.File; 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; } @@ -22,7 +21,8 @@ namespace GodotTools.Build public string ProjectFile { get; set; } } - [Signal] public event Action BuildStateChanged; + [Signal] + public delegate void BuildStateChangedEventHandler(); public bool HasBuildExited { get; private set; } = false; @@ -58,7 +58,7 @@ namespace GodotTools.Build } // TODO Use List once we have proper serialization. - private readonly Array<BuildIssue> _issues = new Array<BuildIssue>(); + private Godot.Collections.Array<BuildIssue> _issues = new(); private ItemList _issuesList; private PopupMenu _issuesListContextMenu; private TextEdit _buildLog; @@ -69,71 +69,63 @@ namespace GodotTools.Build private void LoadIssuesFromFile(string csvFile) { - using (var file = new Godot.File()) + using var file = FileAccess.Open(csvFile, FileAccess.ModeFlags.Read); + + if (file == null) + return; + + while (!file.EofReached()) { - try - { - Error openError = file.Open(csvFile, Godot.File.ModeFlags.Read); + string[] csvColumns = file.GetCsvLine(); - if (openError != Error.Ok) - return; + if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0])) + return; - while (!file.EofReached()) - { - string[] csvColumns = file.GetCsvLine(); - - if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0])) - return; - - if (csvColumns.Length != 7) - { - GD.PushError($"Expected 7 columns, got {csvColumns.Length}"); - continue; - } - - var issue = new BuildIssue - { - Warning = csvColumns[0] == "warning", - File = csvColumns[1], - Line = int.Parse(csvColumns[2]), - Column = int.Parse(csvColumns[3]), - Code = csvColumns[4], - Message = csvColumns[5], - ProjectFile = csvColumns[6] - }; - - if (issue.Warning) - WarningCount += 1; - else - ErrorCount += 1; - - _issues.Add(issue); - } - } - finally + if (csvColumns.Length != 7) { - file.Close(); // Disposing it is not enough. We need to call Close() + GD.PushError($"Expected 7 columns, got {csvColumns.Length}"); + continue; } + + var issue = new BuildIssue + { + Warning = csvColumns[0] == "warning", + File = csvColumns[1], + Line = int.Parse(csvColumns[2]), + Column = int.Parse(csvColumns[3]), + Code = csvColumns[4], + Message = csvColumns[5], + ProjectFile = csvColumns[6] + }; + + if (issue.Warning) + WarningCount += 1; + else + ErrorCount += 1; + + _issues.Add(issue); } } - private void IssueActivated(int idx) + private void IssueActivated(long idx) { if (idx < 0 || idx >= _issuesList.ItemCount) - throw new IndexOutOfRangeException("Item list index out of range"); + throw new ArgumentOutOfRangeException(nameof(idx), "Item list index out of range."); // Get correct issue idx from issue list - int issueIndex = (int)(long)_issuesList.GetItemMetadata(idx); + int issueIndex = (int)_issuesList.GetItemMetadata((int)idx); if (issueIndex < 0 || issueIndex >= _issues.Count) - throw new IndexOutOfRangeException("Issue index out of range"); + throw new InvalidOperationException("Issue index out of range."); BuildIssue issue = _issues[issueIndex]; if (string.IsNullOrEmpty(issue.ProjectFile) && string.IsNullOrEmpty(issue.File)) return; - string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : _buildInfo.Solution.GetBaseDir(); + string projectDir = !string.IsNullOrEmpty(issue.ProjectFile) ? + issue.ProjectFile.GetBaseDir() : + _buildInfo.Solution.GetBaseDir(); string file = Path.Combine(projectDir.SimplifyGodotPath(), issue.File.SimplifyGodotPath()); @@ -291,7 +283,7 @@ namespace GodotTools.Build public void RestartBuild() { if (!HasBuildExited) - throw new InvalidOperationException("Build already started"); + throw new InvalidOperationException("Build already started."); BuildManager.RestartBuild(this); } @@ -299,7 +291,7 @@ namespace GodotTools.Build public void StopBuild() { if (!HasBuildExited) - throw new InvalidOperationException("Build is not in progress"); + throw new InvalidOperationException("Build is not in progress."); BuildManager.StopBuild(this); } @@ -309,7 +301,7 @@ namespace GodotTools.Build Copy } - private void IssuesListContextOptionPressed(int id) + private void IssuesListContextOptionPressed(long id) { switch ((IssuesContextMenuOption)id) { @@ -334,9 +326,9 @@ namespace GodotTools.Build } } - private void IssuesListClicked(int index, Vector2 atPosition, int mouseButtonIndex) + private void IssuesListClicked(long index, Vector2 atPosition, long mouseButtonIndex) { - if (mouseButtonIndex != (int)MouseButton.Right) + if (mouseButtonIndex != (long)MouseButton.Right) { return; } @@ -412,6 +404,16 @@ namespace GodotTools.Build { // In case it didn't update yet. We don't want to have to serialize any pending output. UpdateBuildLogText(); + + // NOTE: + // Currently, GodotTools is loaded in its own load context. This load context is not reloaded, but the script still are. + // Until that changes, we need workarounds like this one because events keep strong references to disposed objects. + BuildManager.BuildLaunchFailed -= BuildLaunchFailed; + BuildManager.BuildStarted -= BuildStarted; + BuildManager.BuildFinished -= BuildFinished; + // StdOutput/Error can be received from different threads, so we need to use CallDeferred + BuildManager.StdOutputReceived -= StdOutputReceived; + BuildManager.StdErrorReceived -= StdErrorReceived; } public void OnAfterDeserialize() diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 02e9d98647..d0cd529d1f 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.IO; +using System.Linq; +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 += (_, e) => stdOutHandler.Invoke(e.Data); + if (stdErrHandler != null) + process.ErrorDataReceived += (_, 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,27 +95,16 @@ 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); + process.OutputDataReceived += (_, e) => stdOutHandler.Invoke(e.Data); if (stdErrHandler != null) - process.ErrorDataReceived += (s, e) => stdErrHandler.Invoke(e.Data); + process.ErrorDataReceived += (_, e) => stdErrHandler.Invoke(e.Data); process.Start(); @@ -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,38 +124,102 @@ 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 (var customProperty in buildInfo.CustomProperties) + { + arguments.Add("-p:" + (string)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"); - if (buildTool == BuildTool.DotnetCli) - arguments += "msbuild"; // `dotnet msbuild` command + // Incremental or rebuild + // TODO: Not supported in `dotnet publish` (https://github.com/dotnet/sdk/issues/11099) + // if (buildInfo.Rebuild) + // arguments.Add("--no-incremental"); - arguments += $@" ""{buildInfo.Solution}"""; + // Configuration + arguments.Add("-c"); + arguments.Add(buildInfo.Configuration); - if (buildInfo.Restore) - arguments += " /restore"; + // Runtime Identifier + arguments.Add("-r"); + arguments.Add(buildInfo.RuntimeIdentifier!); - arguments += $@" /t:{string.Join(",", buildInfo.Targets)} " + - $@"""/p:{"Configuration=" + buildInfo.Configuration}"" /v:normal " + - $@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{buildInfo.LogsDirPath}"""; + // Self-published + arguments.Add("--self-contained"); + arguments.Add("true"); - foreach (string customProperty in buildInfo.CustomProperties) + // Verbosity + arguments.Add("-v"); + arguments.Add("normal"); + + // Logger + AddLoggerArgument(buildInfo, arguments); + + // Custom properties + foreach (var customProperty in buildInfo.CustomProperties) + { + arguments.Add("-p:" + (string)customProperty); + } + + // Publish output directory + if (buildInfo.PublishOutputDir != null) { - arguments += " /p:" + customProperty; + arguments.Add("-o"); + arguments.Add(buildInfo.PublishOutputDir); } + } + + private static void AddLoggerArgument(BuildInfo buildInfo, Collection<string> arguments) + { + string buildLoggerPath = Path.Combine(Internals.GodotSharpDirs.DataEditorToolsDir, + "GodotTools.BuildLogger.dll"); - return arguments; + arguments.Add( + $"-l:{typeof(GodotBuildLogger).FullName},{buildLoggerPath};{buildInfo.LogsDirPath}"); } private static void RemovePlatformVariable(StringDictionary environmentVariables) @@ -145,7 +230,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..b437c7e742 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.InteropServices; +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. + + if (OS.IsMacOS) + { + if (RuntimeInformation.OSArchitecture == Architecture.X64) + { + string dotnet_x64 = "/usr/local/share/dotnet/x64/dotnet"; // Look for x64 version, when running under Rosetta 2. + if (File.Exists(dotnet_x64)) + { + return dotnet_x64; + } + } + string dotnet = "/usr/local/share/dotnet/dotnet"; // Look for native version. + if (File.Exists(dotnet)) + { + return dotnet; + } + } + + 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 3c020a2589..237ac85267 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs @@ -1,13 +1,12 @@ using System; using Godot; using GodotTools.Internals; -using JetBrains.Annotations; using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; namespace GodotTools.Build { - public class MSBuildPanel : VBoxContainer + public partial class MSBuildPanel : VBoxContainer { public BuildOutputView BuildOutputView { get; private set; } @@ -28,7 +27,6 @@ namespace GodotTools.Build BuildOutputView.UpdateIssuesList(); } - [UsedImplicitly] public void BuildSolution() { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) @@ -57,7 +55,6 @@ namespace GodotTools.Build Internal.ReloadAssemblies(softReload: false); } - [UsedImplicitly] private void RebuildSolution() { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) @@ -73,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 @@ -86,18 +83,17 @@ namespace GodotTools.Build Internal.ReloadAssemblies(softReload: false); } - [UsedImplicitly] private void CleanSolution() { 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; - private void BuildMenuOptionPressed(int id) + private void BuildMenuOptionPressed(long id) { switch ((BuildMenuOptions)id) { @@ -126,7 +122,7 @@ namespace GodotTools.Build { base._Ready(); - CustomMinimumSize = new Vector2(0, 228) * EditorScale; + CustomMinimumSize = new Vector2i(0, (int)(228 * EditorScale)); SizeFlagsVertical = (int)SizeFlags.ExpandFill; var toolBarHBox = new HBoxContainer { SizeFlagsHorizontal = (int)SizeFlags.ExpandFill }; @@ -143,7 +139,7 @@ namespace GodotTools.Build _errorsBtn = new Button { - HintTooltip = "Show Errors".TTR(), + TooltipText = "Show Errors".TTR(), Icon = GetThemeIcon("StatusError", "EditorIcons"), ExpandIcon = false, ToggleMode = true, @@ -155,7 +151,7 @@ namespace GodotTools.Build _warningsBtn = new Button { - HintTooltip = "Show Warnings".TTR(), + TooltipText = "Show Warnings".TTR(), Icon = GetThemeIcon("NodeWarning", "EditorIcons"), ExpandIcon = false, ToggleMode = true, @@ -179,7 +175,7 @@ namespace GodotTools.Build AddChild(BuildOutputView); } - public override void _Notification(int what) + public override void _Notification(long what) { base._Notification(what); 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..fe309b8102 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; @@ -21,70 +22,13 @@ namespace GodotTools.Build public static string GodotFallbackFolderPath => Path.Combine(GodotSharpDirs.MonoUserDir, "GodotNuGetFallbackFolder"); - private static void AddFallbackFolderToNuGetConfig(string nuGetConfigPath, string name, string path) - { - var xmlDoc = new XmlDocument(); - xmlDoc.Load(nuGetConfigPath); - - const string nuGetConfigRootName = "configuration"; - - var rootNode = xmlDoc.DocumentElement; - - if (rootNode == null) - { - // No root node, create it - rootNode = xmlDoc.CreateElement(nuGetConfigRootName); - xmlDoc.AppendChild(rootNode); - - // 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("protocolVersion")).Value = "3"; - rootNode.AppendChild(xmlDoc.CreateElement("packageSources")).AppendChild(nugetOrgSourceEntry); - } - else - { - // Check that the root node is the expected one - if (rootNode.Name != nuGetConfigRootName) - throw new Exception("Invalid root Xml node for NuGet.Config. " + - $"Expected '{nuGetConfigRootName}' got '{rootNode.Name}'."); - } - - var fallbackFoldersNode = rootNode["fallbackPackageFolders"] ?? - rootNode.AppendChild(xmlDoc.CreateElement("fallbackPackageFolders")); - - // Check if it already has our fallback package folder - for (var xmlNode = fallbackFoldersNode.FirstChild; xmlNode != null; xmlNode = xmlNode.NextSibling) - { - if (xmlNode.NodeType != XmlNodeType.Element) - continue; - - var xmlElement = (XmlElement)xmlNode; - if (xmlElement.Name == "add" && - xmlElement.Attributes["key"]?.Value == name && - xmlElement.Attributes["value"]?.Value == path) - { - return; - } - } - - XmlElement newEntry = xmlDoc.CreateElement("add"); - newEntry.Attributes.Append(xmlDoc.CreateAttribute("key")).Value = name; - newEntry.Attributes.Append(xmlDoc.CreateAttribute("value")).Value = path; - - fallbackFoldersNode.AppendChild(newEntry); - - xmlDoc.Save(nuGetConfigPath); - } - /// <summary> - /// Returns all the paths where the user NuGet.Config files can be found. + /// Returns all the paths where the Godot.Offline.Config files can be found. /// Does not determine whether the returned files exist or not. /// </summary> - private static string[] GetAllUserNuGetConfigFilePaths() + private static string[] GetAllGodotNuGetConfigFilePaths() { - // Where to find 'NuGet/NuGet.Config': + // Where to find 'NuGet/config/Godot.Offline.Config': // // - Mono/.NETFramework (standalone NuGet): // Uses Environment.SpecialFolder.ApplicationData @@ -96,10 +40,12 @@ namespace GodotTools.Build string applicationData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + const string configFileName = "Godot.Offline.Config"; + if (Utils.OS.IsWindows) { // %APPDATA% for both - return new[] { Path.Combine(applicationData, "NuGet", "NuGet.Config") }; + return new[] { Path.Combine(applicationData, "NuGet", "config", configFileName) }; } var paths = new string[2]; @@ -109,20 +55,20 @@ namespace GodotTools.Build string dotnetCliHome = Environment.GetEnvironmentVariable("DOTNET_CLI_HOME"); if (!string.IsNullOrEmpty(dotnetCliHome)) { - paths[0] = Path.Combine(dotnetCliHome, ".nuget", "NuGet", "NuGet.Config"); + paths[0] = Path.Combine(dotnetCliHome, ".nuget", "NuGet", "config", configFileName); } else { string home = Environment.GetEnvironmentVariable("HOME"); if (string.IsNullOrEmpty(home)) throw new InvalidOperationException("Required environment variable 'HOME' is not set."); - paths[0] = Path.Combine(home, ".nuget", "NuGet", "NuGet.Config"); + paths[0] = Path.Combine(home, ".nuget", "NuGet", "config", configFileName); } // Mono/.NETFramework (standalone NuGet) // ApplicationData is $HOME/.config on Linux/macOS - paths[1] = Path.Combine(applicationData, "NuGet", "NuGet.Config"); + paths[1] = Path.Combine(applicationData, "NuGet", "config", configFileName); return paths; } @@ -139,28 +85,26 @@ namespace GodotTools.Build // The nuspec is not lower case inside the nupkg but must be made lower case when extracted. /// <summary> - /// Adds the specified fallback folder to the user NuGet.Config files, + /// Adds the specified fallback folder to the Godot.Offline.Config files, /// for both standalone NuGet (Mono/.NETFramework) and dotnet CLI NuGet. /// </summary> - public static void AddFallbackFolderToUserNuGetConfigs(string name, string path) + public static void AddFallbackFolderToGodotNuGetConfigs(string name, string path) { - foreach (string nuGetConfigPath in GetAllUserNuGetConfigFilePaths()) + // Make sure the fallback folder exists to avoid error: + // MSB4018: The "ResolvePackageAssets" task failed unexpectedly. + System.IO.Directory.CreateDirectory(path); + + foreach (string nuGetConfigPath in GetAllGodotNuGetConfigFilePaths()) { - if (!System.IO.File.Exists(nuGetConfigPath)) - { - // It doesn't exist, so we create a default one - const string defaultConfig = @"<?xml version=""1.0"" encoding=""utf-8""?> + string defaultConfig = @$"<?xml version=""1.0"" encoding=""utf-8""?> <configuration> - <packageSources> - <add key=""nuget.org"" value=""https://api.nuget.org/v3/index.json"" protocolVersion=""3"" /> - </packageSources> + <fallbackPackageFolders> + <add key=""{name}"" value=""{path}"" /> + </fallbackPackageFolders> </configuration> "; - System.IO.Directory.CreateDirectory(Path.GetDirectoryName(nuGetConfigPath)); - System.IO.File.WriteAllText(nuGetConfigPath, defaultConfig, Encoding.UTF8); // UTF-8 with BOM - } - - AddFallbackFolderToNuGetConfig(nuGetConfigPath, name, path); + System.IO.Directory.CreateDirectory(Path.GetDirectoryName(nuGetConfigPath)); + System.IO.File.WriteAllText(nuGetConfigPath, defaultConfig, Encoding.UTF8); // UTF-8 with BOM } } @@ -181,12 +125,13 @@ 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"); string nupkgSha512DestPath = Path.Combine(destDir, $"{packageIdLower}.{packageVersionLower}.nupkg.sha512"); + string nupkgMetadataDestPath = Path.Combine(destDir, ".nupkg.metadata"); if (File.Exists(nupkgDestPath) && File.Exists(nupkgSha512DestPath)) return; // Already added (for speed we don't check if every file is properly extracted) @@ -195,12 +140,18 @@ namespace GodotTools.Build // Generate .nupkg.sha512 file - using (var alg = SHA512.Create()) - { - alg.ComputeHash(File.ReadAllBytes(nupkgPath)); - string base64Hash = Convert.ToBase64String(alg.Hash); - File.WriteAllText(nupkgSha512DestPath, base64Hash); - } + byte[] hash = SHA512.HashData(File.ReadAllBytes(nupkgPath)); + string base64Hash = Convert.ToBase64String(hash); + File.WriteAllText(nupkgSha512DestPath, base64Hash); + + // Generate .nupkg.metadata file + // Spec: https://github.com/NuGet/Home/wiki/Nupkg-Metadata-File + + File.WriteAllText(nupkgMetadataDestPath, @$"{{ + ""version"": 2, + ""contentHash"": ""{base64Hash}"", + ""source"": null +}}"); // Extract nupkg ExtractNupkg(destDir, nupkgPath, packageId, packageVersion); @@ -227,9 +178,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 @@ -247,7 +200,7 @@ namespace GodotTools.Build entryFullName.EndsWith(".nupkg.sha512", StringComparison.OrdinalIgnoreCase) || entryFullName.EndsWith(".nupkg.metadata", StringComparison.OrdinalIgnoreCase) || // Nuspec at root level. We already extracted it previously but in lower case. - entryFullName.IndexOf('/') == -1 && entryFullName.EndsWith(".nuspec")) + !entryFullName.Contains('/') && entryFullName.EndsWith(".nuspec")) { continue; } @@ -293,6 +246,8 @@ namespace GodotTools.Build { ("Godot.NET.Sdk", GeneratedGodotNupkgsVersions.GodotNETSdk), ("Godot.SourceGenerators", GeneratedGodotNupkgsVersions.GodotSourceGenerators), + ("GodotSharp", GeneratedGodotNupkgsVersions.GodotSharp), + ("GodotSharpEditor", GeneratedGodotNupkgsVersions.GodotSharp), }; } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs index e9718cc82c..94efcba3f1 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs @@ -22,7 +22,7 @@ namespace GodotTools.Export public bool FullAot; private bool _useInterpreter; - public bool UseInterpreter { get => _useInterpreter && !LLVMOnly; set => _useInterpreter = value; } + public bool UseInterpreter { readonly get => _useInterpreter && !LLVMOnly; set => _useInterpreter = value; } public string[] ExtraAotOptions; public string[] ExtraOptimizerOptions; @@ -75,8 +75,24 @@ namespace GodotTools.Export } else { - string bits = features.Contains("64") ? "64" : features.Contains("32") ? "32" : null; - CompileAssembliesForDesktop(exporter, platform, isDebug, bits, aotOpts, aotTempDir, outputDataDir, assembliesPrepared, bclDir); + string arch = ""; + if (features.Contains("x86_64")) + { + arch = "x86_64"; + } + else if (features.Contains("x86_32")) + { + arch = "x86_32"; + } + else if (features.Contains("arm64")) + { + arch = "arm64"; + } + else if (features.Contains("arm32")) + { + arch = "arm32"; + } + CompileAssembliesForDesktop(exporter, platform, isDebug, arch, aotOpts, aotTempDir, outputDataDir, assembliesPrepared, bclDir); } } @@ -112,7 +128,7 @@ namespace GodotTools.Export } } - public static void CompileAssembliesForDesktop(ExportPlugin exporter, string platform, bool isDebug, string bits, AotOptions aotOpts, string aotTempDir, string outputDataDir, IDictionary<string, string> assemblies, string bclDir) + public static void CompileAssembliesForDesktop(ExportPlugin exporter, string platform, bool isDebug, string arch, AotOptions aotOpts, string aotTempDir, string outputDataDir, IDictionary<string, string> assemblies, string bclDir) { foreach (var assembly in assemblies) { @@ -126,9 +142,9 @@ namespace GodotTools.Export string outputFileName = assemblyName + ".dll" + outputFileExtension; string tempOutputFilePath = Path.Combine(aotTempDir, outputFileName); - var compilerArgs = GetAotCompilerArgs(platform, isDebug, bits, aotOpts, assemblyPath, tempOutputFilePath); + var compilerArgs = GetAotCompilerArgs(platform, isDebug, arch, aotOpts, assemblyPath, tempOutputFilePath); - string compilerDirPath = GetMonoCrossDesktopDirName(platform, bits); + string compilerDirPath = GetMonoCrossDesktopDirName(platform, arch); ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir); @@ -203,7 +219,7 @@ namespace GodotTools.Export int clangExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("clang"), clangArgs); if (clangExitCode != 0) - throw new Exception($"Command 'clang' exited with code: {clangExitCode}"); + throw new InvalidOperationException($"Command 'clang' exited with code: {clangExitCode}."); objFilePathsForiOSArch[arch].Add(objFilePath); } @@ -289,7 +305,7 @@ MONO_AOT_MODE_LAST = 1000, // Archive the AOT object files into a static library var arFilePathsForAllArchs = new List<string>(); - string projectAssemblyName = GodotSharpEditor.ProjectAssemblyName; + string projectAssemblyName = GodotSharpDirs.ProjectAssemblyName; foreach (var archPathsPair in objFilePathsForiOSArch) { @@ -309,7 +325,7 @@ MONO_AOT_MODE_LAST = 1000, int arExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("ar"), arArgs); if (arExitCode != 0) - throw new Exception($"Command 'ar' exited with code: {arExitCode}"); + throw new InvalidOperationException($"Command 'ar' exited with code: {arExitCode}."); arFilePathsForAllArchs.Add(arOutputFilePath); } @@ -327,7 +343,7 @@ MONO_AOT_MODE_LAST = 1000, int lipoExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("lipo"), lipoArgs); if (lipoExitCode != 0) - throw new Exception($"Command 'lipo' exited with code: {lipoExitCode}"); + throw new InvalidOperationException($"Command 'lipo' exited with code: {lipoExitCode}."); // TODO: Add the AOT lib and interpreter libs as device only to suppress warnings when targeting the simulator @@ -427,14 +443,14 @@ MONO_AOT_MODE_LAST = 1000, } else if (!Directory.Exists(androidToolchain)) { - throw new FileNotFoundException("Android toolchain not found: " + androidToolchain); + throw new FileNotFoundException($"Android toolchain not found: '{androidToolchain}'."); } var androidToolPrefixes = new Dictionary<string, string> { - ["armeabi-v7a"] = "arm-linux-androideabi-", - ["arm64-v8a"] = "aarch64-linux-android-", - ["x86"] = "i686-linux-android-", + ["arm32"] = "arm-linux-androideabi-", + ["arm64"] = "aarch64-linux-android-", + ["x86_32"] = "i686-linux-android-", ["x86_64"] = "x86_64-linux-android-" }; @@ -524,12 +540,12 @@ MONO_AOT_MODE_LAST = 1000, Console.WriteLine($"Running: \"{process.StartInfo.FileName}\" {process.StartInfo.Arguments}"); if (!process.Start()) - throw new Exception("Failed to start process for Mono AOT compiler"); + throw new InvalidOperationException("Failed to start process for Mono AOT compiler."); process.WaitForExit(); if (process.ExitCode != 0) - throw new Exception($"Mono AOT compiler exited with code: {process.ExitCode}"); + throw new InvalidOperationException($"Mono AOT compiler exited with code: {process.ExitCode}."); } } @@ -547,9 +563,9 @@ MONO_AOT_MODE_LAST = 1000, { var androidAbis = new[] { - "armeabi-v7a", - "arm64-v8a", - "x86", + "arm32", + "arm64", + "x86_32", "x86_64" }; @@ -560,9 +576,9 @@ MONO_AOT_MODE_LAST = 1000, { var abiArchs = new Dictionary<string, string> { - ["armeabi-v7a"] = "armv7", - ["arm64-v8a"] = "aarch64-v8a", - ["x86"] = "i686", + ["arm32"] = "armv7", + ["arm64"] = "aarch64-v8a", + ["x86_32"] = "i686", ["x86_64"] = "x86_64" }; @@ -571,31 +587,25 @@ MONO_AOT_MODE_LAST = 1000, return $"{arch}-linux-android"; } - private static string GetMonoCrossDesktopDirName(string platform, string bits) + private static string GetMonoCrossDesktopDirName(string platform, string arch) { switch (platform) { case OS.Platforms.Windows: case OS.Platforms.UWP: { - string arch = bits == "64" ? "x86_64" : "i686"; return $"windows-{arch}"; } case OS.Platforms.MacOS: { - Debug.Assert(bits == null || bits == "64"); - string arch = "x86_64"; return $"{platform}-{arch}"; } case OS.Platforms.LinuxBSD: - case OS.Platforms.Server: { - string arch = bits == "64" ? "x86_64" : "i686"; return $"linux-{arch}"; } case OS.Platforms.Haiku: { - string arch = bits == "64" ? "x86_64" : "i686"; return $"{platform}-{arch}"; } default: diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index cca18a2a1f..745a8b73f8 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -4,11 +4,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using GodotTools.Build; using GodotTools.Core; using GodotTools.Internals; -using JetBrains.Annotations; using static GodotTools.Internals.Globals; using Directory = GodotTools.Utils.Directory; using File = GodotTools.Utils.File; @@ -17,61 +15,15 @@ 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"); - } + private List<string> _tempFolders = new List<string>(); 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); @@ -85,11 +37,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) @@ -100,7 +48,9 @@ namespace GodotTools.Export return; if (Path.GetExtension(path) != Internal.CSharpLanguageExtension) - throw new ArgumentException($"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}", nameof(path)); + throw new ArgumentException( + $"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}", + nameof(path)); // TODO What if the source file is not part of the game's C# project @@ -119,7 +69,7 @@ namespace GodotTools.Export } } - public override void _ExportBegin(string[] features, bool isDebug, string path, int flags) + public override void _ExportBegin(string[] features, bool isDebug, string path, long flags) { base._ExportBegin(features, isDebug, path, flags); @@ -142,7 +92,7 @@ namespace GodotTools.Export } } - private void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags) + private void _ExportBeginImpl(string[] features, bool isDebug, string path, long flags) { _ = flags; // Unused @@ -150,161 +100,111 @@ namespace GodotTools.Export return; if (!DeterminePlatformFromFeatures(features, out string platform)) - throw new NotSupportedException("Target platform not supported"); + throw new NotSupportedException("Target platform not supported."); + + if (!new[] { OS.Platforms.Windows, OS.Platforms.LinuxBSD, OS.Platforms.MacOS } + .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"); - - // Add dependency assemblies - - var assemblies = new Godot.Collections.Dictionary<string, string>(); - - string projectDllName = GodotSharpEditor.ProjectAssemblyName; - string projectDllSrcDir = Path.Combine(GodotSharpDirs.ResTempAssembliesBaseDir, buildConfig); - string projectDllSrcPath = Path.Combine(projectDllSrcDir, $"{projectDllName}.dll"); - - assemblies[projectDllName] = projectDllSrcPath; - - string bclDir = DeterminePlatformBclDir(platform); - - if (platform == OS.Platforms.Android) + var archs = new List<string>(); + if (features.Contains("x86_64")) { - 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; + archs.Add("x86_64"); } - else if (platform == OS.Platforms.HTML5) + else if (features.Contains("x86_32")) { - // 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) + archs.Add("x86_32"); + } + else if (features.Contains("arm64")) + { + archs.Add("arm64"); + } + else if (features.Contains("universal")) + { + if (platform == OS.Platforms.MacOS) { - string thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName + ".dll"); - if (!File.Exists(thisWasmFrameworkAssemblyPath)) - throw new FileNotFoundException($"Assembly not found: '{thisWasmFrameworkAssemblyName}'", thisWasmFrameworkAssemblyPath); - assemblies[thisWasmFrameworkAssemblyName] = thisWasmFrameworkAssemblyPath; + archs.Add("x86_64"); + archs.Add("arm64"); } + } - // 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) + foreach (var arch in archs) + { + string ridOS = DetermineRuntimeIdentifierOS(platform); + string ridArch = DetermineRuntimeIdentifierArch(arch); + string runtimeIdentifier = $"{ridOS}-{ridArch}"; + string projectDataDirName = $"{DetermineDataDirNameForProject()}_{arch}"; + if (platform == OS.Platforms.MacOS) { - 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; - } + projectDataDirName = Path.Combine("Contents", "Resources", projectDataDirName); } - } - - var initialAssemblies = assemblies.Duplicate(); - internal_GetExportedAssemblyDependencies(initialAssemblies, buildConfig, bclDir, assemblies); - AddI18NAssemblies(assemblies, bclDir); + // Create temporary publish output directory - string outputDataDir = null; + string publishOutputTempDir = Path.Combine(Path.GetTempPath(), "godot-publish-dotnet", + $"{Process.GetCurrentProcess().Id}-{buildConfig}-{runtimeIdentifier}"); - if (PlatformHasTemplateDir(platform)) - outputDataDir = ExportDataDirectory(features, platform, isDebug, outputDir); + _tempFolders.Add(publishOutputTempDir); - string apiConfig = isDebug ? "Debug" : "Release"; - string resAssembliesDir = Path.Combine(GodotSharpDirs.ResAssembliesBaseDir, apiConfig); + if (!Directory.Exists(publishOutputTempDir)) + Directory.CreateDirectory(publishOutputTempDir); - bool assembliesInsidePck = (bool)ProjectSettings.GetSetting("mono/export/export_assemblies_inside_pck") || outputDataDir == null; - - if (!assembliesInsidePck) - { - string outputDataGameAssembliesDir = Path.Combine(outputDataDir, "Assemblies"); - if (!Directory.Exists(outputDataGameAssembliesDir)) - Directory.CreateDirectory(outputDataGameAssembliesDir); - } + // Execute dotnet publish - foreach (var assembly in assemblies) - { - void AddToAssembliesDir(string fileSrcPath) + if (!BuildManager.PublishProjectBlocking(buildConfig, platform, + runtimeIdentifier, publishOutputTempDir)) { - 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); - } + throw new InvalidOperationException("Failed to build project."); } - string assemblySrcPath = assembly.Value; + string soExt = ridOS switch + { + OS.DotNetOS.Win or OS.DotNetOS.Win10 => "dll", + OS.DotNetOS.OSX or OS.DotNetOS.iOS => "dylib", + _ => "so" + }; - string assemblyPathWithoutExtension = Path.ChangeExtension(assemblySrcPath, null); - string pdbSrcPath = assemblyPathWithoutExtension + ".pdb"; + if (!File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpDirs.ProjectAssemblyName}.dll")) + // NativeAOT shared library output + && !File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpDirs.ProjectAssemblyName}.{soExt}"))) + { + throw new NotSupportedException( + "Publish succeeded but project assembly not found in the output directory"); + } - AddToAssembliesDir(assemblySrcPath); + // Add to the exported project shared object list. - if (File.Exists(pdbSrcPath)) - AddToAssembliesDir(pdbSrcPath); + foreach (string file in Directory.GetFiles(publishOutputTempDir, "*", SearchOption.AllDirectories)) + { + AddSharedObject(file, tags: null, projectDataDirName); + } } + } - // 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() @@ -316,79 +216,29 @@ namespace GodotTools.Export if (Directory.Exists(aotTempDir)) Directory.Delete(aotTempDir, recursive: true); - // TODO: Just a workaround until the export plugins can be made to abort with errors - if (!string.IsNullOrEmpty(_maybeLastExportError)) // Check empty as well, because it's set to empty after hot-reloading + foreach (string folder in _tempFolders) { - string lastExportError = _maybeLastExportError; - _maybeLastExportError = null; - - GodotSharpEditor.Instance.ShowErrorDialog(lastExportError, "Failed to export C# project"); + Directory.Delete(folder, recursive: true); } - } + _tempFolders.Clear(); - [NotNull] - private static string ExportDataDirectory(string[] features, string platform, bool isDebug, string outputDir) - { - string target = isDebug ? "release_debug" : "release"; + // TODO: The following is just a workaround until the export plugins can be made to abort with errors - // 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)) + // We check for empty as well, because it's set to empty after hot-reloading + if (!string.IsNullOrEmpty(_maybeLastExportError)) { - 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))); - } + string lastExportError = _maybeLastExportError; + _maybeLastExportError = null; - foreach (string file in Directory.GetFiles(templateDirPath, "*", SearchOption.AllDirectories)) - { - File.Copy(file, Path.Combine(outputDataDir, file.Substring(templateDirPath.Length + 1))); + GodotSharpEditor.Instance.ShowErrorDialog(lastExportError, "Failed to export C# project"); } - - 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; } @@ -396,87 +246,11 @@ 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"); string appNameSafe = appName.ToSafeDirName(); return $"data_{appNameSafe}"; } - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_GetExportedAssemblyDependencies(Godot.Collections.Dictionary<string, string> initialAssemblies, - string buildConfig, string customBclDir, Godot.Collections.Dictionary<string, string> dependencyAssemblies); } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs b/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs index 93ef837a83..4f5bebfb42 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs @@ -16,7 +16,7 @@ namespace GodotTools.Export _XcodePath = FindXcode(); if (_XcodePath == null) - throw new Exception("Could not find Xcode"); + throw new FileNotFoundException("Could not find Xcode."); } return _XcodePath; diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index b39c3d1c0d..89364d1c02 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; @@ -39,28 +40,27 @@ namespace GodotTools public bool SkipBuildBeforePlaying { get; set; } = false; - public static string ProjectAssemblyName + [UsedImplicitly] + private bool CreateProjectSolutionIfNeeded() { - get + if (!File.Exists(GodotSharpDirs.ProjectSlnPath) || !File.Exists(GodotSharpDirs.ProjectCsProjPath)) { - string projectAssemblyName = (string)ProjectSettings.GetSetting("application/config/name"); - projectAssemblyName = projectAssemblyName.ToSafeDirName(); - if (string.IsNullOrEmpty(projectAssemblyName)) - projectAssemblyName = "UnnamedProject"; - return projectAssemblyName; + return CreateProjectSolution(); } + + return true; } private bool CreateProjectSolution() { - using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 3)) + using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 2)) { pr.Step("Generating C# project...".TTR()); string resourceDir = ProjectSettings.GlobalizePath("res://"); string path = resourceDir; - string name = ProjectAssemblyName; + string name = GodotSharpDirs.ProjectAssemblyName; string guid = CsProjOperations.GenerateGameProject(path, name); @@ -75,7 +75,7 @@ namespace GodotTools { Guid = guid, PathRelativeToSolution = name + ".csproj", - Configs = new List<string> {"Debug", "ExportDebug", "ExportRelease"} + Configs = new List<string> { "Debug", "ExportDebug", "ExportRelease" } }; solution.AddNewProject(name, projectInfo); @@ -90,24 +90,6 @@ namespace GodotTools return false; } - pr.Step("Updating Godot API assemblies...".TTR()); - - string debugApiAssembliesError = Internal.UpdateApiAssembliesFromPrebuilt("Debug"); - - if (!string.IsNullOrEmpty(debugApiAssembliesError)) - { - ShowErrorDialog("Failed to update the Godot API assemblies: " + debugApiAssembliesError); - return false; - } - - string releaseApiAssembliesError = Internal.UpdateApiAssembliesFromPrebuilt("Release"); - - if (!string.IsNullOrEmpty(releaseApiAssembliesError)) - { - ShowErrorDialog("Failed to update the Godot API assemblies: " + releaseApiAssembliesError); - return false; - } - pr.Step("Done".TTR()); // Here, after all calls to progress_task_step @@ -129,7 +111,7 @@ namespace GodotTools _toolBarBuildButton.Show(); } - private void _MenuOptionPressed(int id) + private void _MenuOptionPressed(long id) { switch ((MenuOptions)id) { @@ -141,7 +123,8 @@ namespace GodotTools try { string fallbackFolder = NuGetUtils.GodotFallbackFolderPath; - NuGetUtils.AddFallbackFolderToUserNuGetConfigs(NuGetUtils.GodotFallbackFolderName, fallbackFolder); + NuGetUtils.AddFallbackFolderToGodotNuGetConfigs(NuGetUtils.GodotFallbackFolderName, + fallbackFolder); NuGetUtils.AddBundledPackagesToFallbackFolder(fallbackFolder); } catch (Exception e) @@ -167,13 +150,6 @@ namespace GodotTools Instance.MSBuildPanel.BuildSolution(); } - public override void _Ready() - { - base._Ready(); - - MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged; - } - private enum MenuOptions { CreateSln, @@ -197,7 +173,7 @@ namespace GodotTools [UsedImplicitly] public Error OpenInExternalEditor(Script script, int line, int col) { - var editorId = (ExternalEditorId)_editorSettings.GetSetting("mono/editor/external_editor"); + var editorId = (ExternalEditorId)(int)_editorSettings.GetSetting("mono/editor/external_editor"); switch (editorId) { @@ -219,13 +195,15 @@ namespace GodotTools try { if (Godot.OS.IsStdoutVerbose()) - Console.WriteLine($"Running: \"{command}\" {string.Join(" ", args.Select(a => $"\"{a}\""))}"); + Console.WriteLine( + $"Running: \"{command}\" {string.Join(" ", args.Select(a => $"\"{a}\""))}"); OS.RunProcess(command, args); } catch (Exception e) { - GD.PushError($"Error when trying to run code editor: VisualStudio. Exception message: '{e.Message}'"); + GD.PushError( + $"Error when trying to run code editor: VisualStudio. Exception message: '{e.Message}'"); } break; @@ -347,7 +325,8 @@ namespace GodotTools [UsedImplicitly] public bool OverridesExternalEditor() { - return (ExternalEditorId)_editorSettings.GetSetting("mono/editor/external_editor") != ExternalEditorId.None; + return (ExternalEditorId)(int)_editorSettings.GetSetting("mono/editor/external_editor") != + ExternalEditorId.None; } public override bool _Build() @@ -363,12 +342,12 @@ namespace GodotTools DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath); var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath) - ?? throw new Exception("Cannot open C# project"); + ?? throw new InvalidOperationException("Cannot open C# project."); // NOTE: The order in which changes are made to the project is important // Migrate to MSBuild project Sdks style if using the old style - ProjectUtils.MigrateToProjectSdksStyle(msbuildProject, ProjectAssemblyName); + ProjectUtils.MigrateToProjectSdksStyle(msbuildProject, GodotSharpDirs.ProjectAssemblyName); ProjectUtils.EnsureGodotSdkIsUpToDate(msbuildProject); @@ -400,18 +379,49 @@ namespace GodotTools 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(); _editorSettings = editorInterface.GetEditorSettings(); + GodotSharpDirs.RegisterProjectSettings(); + _errorDialog = new AcceptDialog(); editorBaseControl.AddChild(_errorDialog); MSBuildPanel = new MSBuildPanel(); + MSBuildPanel.Ready += () => + MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged; _bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR()); - AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"}); + AddChild(new HotReloadAssemblyWatcher { Name = "HotReloadAssemblyWatcher" }); _menuPopup = new PopupMenu(); _menuPopup.Hide(); @@ -423,7 +433,7 @@ namespace GodotTools _toolBarBuildButton = new Button { Text = "Build", - HintTooltip = "Build Solution".TTR(), + TooltipText = "Build Solution".TTR(), FocusMode = Control.FocusModeEnum.None, Shortcut = buildSolutionShortcut, ShortcutInTooltip = true @@ -472,9 +482,9 @@ namespace GodotTools _editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary { - ["type"] = Variant.Type.Int, + ["type"] = (int)Variant.Type.Int, ["name"] = "mono/editor/external_editor", - ["hint"] = PropertyHint.Enum, + ["hint"] = (int)PropertyHint.Enum, ["hint_string"] = settingsHintStr }); @@ -487,7 +497,8 @@ namespace GodotTools try { // At startup we make sure NuGet.Config files have our Godot NuGet fallback folder included - NuGetUtils.AddFallbackFolderToUserNuGetConfigs(NuGetUtils.GodotFallbackFolderName, NuGetUtils.GodotFallbackFolderPath); + NuGetUtils.AddFallbackFolderToGodotNuGetConfigs(NuGetUtils.GodotFallbackFolderName, + NuGetUtils.GodotFallbackFolderPath); } catch (Exception e) { @@ -503,20 +514,23 @@ namespace GodotTools protected override void Dispose(bool disposing) { - base.Dispose(disposing); - - if (_exportPluginWeak != null) + if (disposing) { - // We need to dispose our export plugin before the editor destroys EditorSettings. - // Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid - // will be freed after EditorSettings already was, and its device polling thread - // will try to access the EditorSettings singleton, resulting in null dereferencing. - (_exportPluginWeak.GetRef() as ExportPlugin)?.Dispose(); + if (IsInstanceValid(_exportPluginWeak)) + { + // We need to dispose our export plugin before the editor destroys EditorSettings. + // Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid + // will be freed after EditorSettings already was, and its device polling thread + // will try to access the EditorSettings singleton, resulting in null dereferencing. + (_exportPluginWeak.GetRef().AsGodotObject() as ExportPlugin)?.Dispose(); + + _exportPluginWeak.Dispose(); + } - _exportPluginWeak.Dispose(); + GodotIdeManager?.Dispose(); } - GodotIdeManager?.Dispose(); + base.Dispose(disposing); } public void OnBeforeSerialize() @@ -533,8 +547,10 @@ namespace GodotTools public static GodotSharpEditor Instance { get; private set; } [UsedImplicitly] - private GodotSharpEditor() + private static IntPtr InternalCreateInstance(IntPtr unmanagedCallbacks, int unmanagedCallbacksSize) { + Internal.Initialize(unmanagedCallbacks, unmanagedCallbacksSize); + return new GodotSharpEditor().NativeInstance; } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index f1d45463c5..30525ba04a 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -1,14 +1,24 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ProjectGuid>{27B00618-A6F2-4828-B922-05CAEB08C286}</ProjectGuid> - <TargetFramework>net472</TargetFramework> - <LangVersion>7.2</LangVersion> + <TargetFramework>net6.0</TargetFramework> + <EnableDynamicLoading>true</EnableDynamicLoading> + <LangVersion>10</LangVersion> <!-- The Godot editor uses the Debug Godot API assemblies --> <GodotApiConfiguration>Debug</GodotApiConfiguration> <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath> <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir> <GodotApiAssembliesDir>$(GodotOutputDataDir)/Api/$(GodotApiConfiguration)</GodotApiAssembliesDir> + <ProduceReferenceAssembly>false</ProduceReferenceAssembly> + <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' --> @@ -20,6 +30,8 @@ <PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" /> <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <!-- For RiderPathLocator --> + <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <Reference Include="GodotSharp"> <HintPath>$(GodotApiAssembliesDir)/GodotSharp.dll</HintPath> <Private>False</Private> @@ -30,6 +42,10 @@ </Reference> </ItemGroup> <ItemGroup> + <ProjectReference Include="..\..\Godot.NET.Sdk\Godot.SourceGenerators\Godot.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> + <ProjectReference Include="..\..\..\glue\GodotSharp\Godot.SourceGenerators.Internal\Godot.SourceGenerators.Internal.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 dd05f28af0..89ac8058b9 100644 --- a/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs +++ b/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs @@ -1,14 +1,15 @@ using Godot; using GodotTools.Internals; +using JetBrains.Annotations; using static GodotTools.Internals.Globals; namespace GodotTools { - public class HotReloadAssemblyWatcher : Node + public partial class HotReloadAssemblyWatcher : Node { private Timer _watchTimer; - public override void _Notification(int what) + public override void _Notification(long what) { if (what == Node.NotificationWmWindowFocusIn) { @@ -25,6 +26,7 @@ namespace GodotTools Internal.ReloadAssemblies(softReload: false); } + [UsedImplicitly] public void RestartTimer() { _watchTimer.Stop(); @@ -38,7 +40,7 @@ namespace GodotTools _watchTimer = new Timer { OneShot = false, - WaitTime = (float)EditorDef("mono/assembly_watch_interval_sec", 0.5) + WaitTime = 0.5f }; _watchTimer.Timeout += TimerTimeout; AddChild(_watchTimer); diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs index 23339fe50b..9df90ac608 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; @@ -76,7 +76,7 @@ namespace GodotTools.Ides public async Task<EditorPick?> LaunchIdeAsync(int millisecondsTimeout = 10000) { - var editorId = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface() + var editorId = (ExternalEditorId)(int)GodotSharpEditor.Instance.GetEditorInterface() .GetEditorSettings().GetSetting("mono/editor/external_editor"); string editorIdentity = GetExternalEditorIdentity(editorId); @@ -153,7 +153,7 @@ namespace GodotTools.Ides } default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(editorId)); } } @@ -180,17 +180,17 @@ namespace GodotTools.Ides public void SendOpenFile(string file) { - SendRequest<OpenFileResponse>(new OpenFileRequest {File = file}); + SendRequest<OpenFileResponse>(new OpenFileRequest { File = file }); } public void SendOpenFile(string file, int line) { - SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line}); + SendRequest<OpenFileResponse>(new OpenFileRequest { File = file, Line = line }); } public void SendOpenFile(string file, int line, int column) { - SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line, Column = column}); + SendRequest<OpenFileResponse>(new OpenFileRequest { File = file, Line = line, Column = column }); } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs index 6f11831b80..62db6e3af5 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs @@ -385,7 +385,7 @@ namespace GodotTools.Ides // However, it doesn't fix resource loading if the rest of the path is also case insensitive. string scriptFileLocalized = FsPathUtils.LocalizePathWithCaseChecked(request.ScriptFile); - var response = new CodeCompletionResponse {Kind = request.Kind, ScriptFile = request.ScriptFile}; + var response = new CodeCompletionResponse { Kind = request.Kind, ScriptFile = request.ScriptFile }; response.Suggestions = await Task.Run(() => Internal.CodeCompletionRequest(response.Kind, scriptFileLocalized ?? request.ScriptFile)); return response; diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs index 71055f0125..dad6e35344 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Runtime.Versioning; using Godot; -using JetBrains.Annotations; using Microsoft.Win32; using Newtonsoft.Json; using Directory = System.IO.Directory; @@ -41,7 +42,7 @@ namespace GodotTools.Ides.Rider { return CollectAllRiderPathsLinux(); } - throw new Exception("Unexpected OS."); + throw new InvalidOperationException("Unexpected OS."); } catch (Exception e) { @@ -113,6 +114,7 @@ namespace GodotTools.Ides.Rider return installInfos.ToArray(); } + [SupportedOSPlatform("windows")] private static RiderInfo[] CollectRiderInfosWindows() { var installInfos = new List<RiderInfo>(); @@ -214,9 +216,10 @@ namespace GodotTools.Ides.Rider return "../../build.txt"; if (OS.IsMacOS) return "Contents/Resources/build.txt"; - throw new Exception("Unknown OS."); + throw new InvalidOperationException("Unknown OS."); } + [SupportedOSPlatform("windows")] private static void CollectPathsFromRegistry(string registryKey, List<string> installPaths) { using (var key = Registry.CurrentUser.OpenSubKey(registryKey)) @@ -229,6 +232,7 @@ namespace GodotTools.Ides.Rider } } + [SupportedOSPlatform("windows")] private static void CollectPathsFromRegistry(List<string> installPaths, RegistryKey key) { if (key == null) return; @@ -324,7 +328,7 @@ namespace GodotTools.Ides.Rider { public string install_location; - [CanBeNull] + [return: MaybeNull] public static string GetInstallLocationFromJson(string json) { try @@ -378,7 +382,7 @@ namespace GodotTools.Ides.Rider public string version; public string versionSuffix; - [CanBeNull] + [return: MaybeNull] internal static ProductInfo GetProductInfo(string json) { try @@ -402,7 +406,7 @@ namespace GodotTools.Ides.Rider // ReSharper disable once InconsistentNaming public ActiveApplication active_application; - [CanBeNull] + [return: MaybeNull] public static string GetLatestBuildFromJson(string json) { try diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs index 3440eb701c..60602a5847 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs @@ -22,7 +22,7 @@ namespace GodotTools.Ides.Rider public static void Initialize() { var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var editor = (ExternalEditorId)editorSettings.GetSetting("mono/editor/external_editor"); + var editor = (ExternalEditorId)(int)editorSettings.GetSetting("mono/editor/external_editor"); if (editor == ExternalEditorId.Rider) { if (!editorSettings.HasSetting(EditorPathSettingName)) @@ -30,9 +30,9 @@ namespace GodotTools.Ides.Rider Globals.EditorDef(EditorPathSettingName, "Optional"); editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary { - ["type"] = Variant.Type.String, + ["type"] = (int)Variant.Type.String, ["name"] = EditorPathSettingName, - ["hint"] = PropertyHint.File, + ["hint"] = (int)PropertyHint.File, ["hint_string"] = "" }); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/EditorProgress.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/EditorProgress.cs index 70ba7c733a..8f39ad063e 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/EditorProgress.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/EditorProgress.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.CompilerServices; using Godot; +using Godot.NativeInterop; namespace GodotTools.Internals { @@ -8,19 +9,12 @@ namespace GodotTools.Internals { public string Task { get; } - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_Create(string task, string label, int amount, bool canCancel); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_Dispose(string task); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_Step(string task, string state, int step, bool forceRefresh); - public EditorProgress(string task, string label, int amount, bool canCancel = false) { Task = task; - internal_Create(task, label, amount, canCancel); + using godot_string taskIn = Marshaling.ConvertStringToNative(task); + using godot_string labelIn = Marshaling.ConvertStringToNative(label); + Internal.godot_icall_EditorProgress_Create(taskIn, labelIn, amount, canCancel); } ~EditorProgress() @@ -33,18 +27,23 @@ namespace GodotTools.Internals public void Dispose() { - internal_Dispose(Task); + using godot_string taskIn = Marshaling.ConvertStringToNative(Task); + Internal.godot_icall_EditorProgress_Dispose(taskIn); GC.SuppressFinalize(this); } public void Step(string state, int step = -1, bool forceRefresh = true) { - internal_Step(Task, state, step, forceRefresh); + using godot_string taskIn = Marshaling.ConvertStringToNative(Task); + using godot_string stateIn = Marshaling.ConvertStringToNative(state); + Internal.godot_icall_EditorProgress_Step(taskIn, stateIn, step, forceRefresh); } public bool TryStep(string state, int step = -1, bool forceRefresh = true) { - return internal_Step(Task, state, step, forceRefresh); + using godot_string taskIn = Marshaling.ConvertStringToNative(Task); + using godot_string stateIn = Marshaling.ConvertStringToNative(state); + return Internal.godot_icall_EditorProgress_Step(taskIn, stateIn, step, forceRefresh); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Globals.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Globals.cs index 5c5ced8c29..acb7cc3ab0 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Globals.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Globals.cs @@ -1,3 +1,4 @@ +using Godot.NativeInterop; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -5,35 +6,41 @@ namespace GodotTools.Internals { public static class Globals { - public static float EditorScale => internal_EditorScale(); - - public static object GlobalDef(string setting, object defaultValue, bool restartIfChanged = false) => - internal_GlobalDef(setting, defaultValue, restartIfChanged); - - public static object EditorDef(string setting, object defaultValue, bool restartIfChanged = false) => - internal_EditorDef(setting, defaultValue, restartIfChanged); - - public static object EditorShortcut(string setting) => - internal_EditorShortcut(setting); + public static float EditorScale => Internal.godot_icall_Globals_EditorScale(); + + public static unsafe object GlobalDef(string setting, object defaultValue, bool restartIfChanged = false) + { + using godot_string settingIn = Marshaling.ConvertStringToNative(setting); + using godot_variant defaultValueIn = Marshaling.ConvertManagedObjectToVariant(defaultValue); + Internal.godot_icall_Globals_GlobalDef(settingIn, defaultValueIn, restartIfChanged, out godot_variant result); + using (result) + return Marshaling.ConvertVariantToManagedObject(result); + } + + public static unsafe object EditorDef(string setting, object defaultValue, bool restartIfChanged = false) + { + using godot_string settingIn = Marshaling.ConvertStringToNative(setting); + using godot_variant defaultValueIn = Marshaling.ConvertManagedObjectToVariant(defaultValue); + Internal.godot_icall_Globals_EditorDef(settingIn, defaultValueIn, restartIfChanged, out godot_variant result); + using (result) + return Marshaling.ConvertVariantToManagedObject(result); + } + + public static object EditorShortcut(string setting) + { + using godot_string settingIn = Marshaling.ConvertStringToNative(setting); + Internal.godot_icall_Globals_EditorShortcut(settingIn, out godot_variant result); + using (result) + return Marshaling.ConvertVariantToManagedObject(result); + } [SuppressMessage("ReSharper", "InconsistentNaming")] - public static string TTR(this string text) => internal_TTR(text); - - // Internal Calls - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern float internal_EditorScale(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern object internal_GlobalDef(string setting, object defaultValue, bool restartIfChanged); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern object internal_EditorDef(string setting, object defaultValue, bool restartIfChanged); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern object internal_EditorShortcut(string setting); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_TTR(string text); + public static string TTR(this string text) + { + using godot_string textIn = Marshaling.ConvertStringToNative(text); + Internal.godot_icall_Globals_TTR(textIn, out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs index 5e70c399b2..14285cc0f1 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs @@ -1,103 +1,125 @@ -using System.Runtime.CompilerServices; +using System.IO; +using Godot; +using Godot.NativeInterop; +using GodotTools.Core; +using static GodotTools.Internals.Globals; namespace GodotTools.Internals { public static class GodotSharpDirs { - public static string ResDataDir => internal_ResDataDir(); - public static string ResMetadataDir => internal_ResMetadataDir(); - public static string ResAssembliesBaseDir => internal_ResAssembliesBaseDir(); - public static string ResAssembliesDir => internal_ResAssembliesDir(); - public static string ResConfigDir => internal_ResConfigDir(); - public static string ResTempDir => internal_ResTempDir(); - public static string ResTempAssembliesBaseDir => internal_ResTempAssembliesBaseDir(); - public static string ResTempAssembliesDir => internal_ResTempAssembliesDir(); - - public static string MonoUserDir => internal_MonoUserDir(); - public static string MonoLogsDir => internal_MonoLogsDir(); - - #region Tools-only - public static string MonoSolutionsDir => internal_MonoSolutionsDir(); - public static string BuildLogsDirs => internal_BuildLogsDirs(); - - public static string ProjectSlnPath => internal_ProjectSlnPath(); - public static string ProjectCsProjPath => internal_ProjectCsProjPath(); - - public static string DataEditorToolsDir => internal_DataEditorToolsDir(); - public static string DataEditorPrebuiltApiDir => internal_DataEditorPrebuiltApiDir(); - #endregion - - public static string DataMonoEtcDir => internal_DataMonoEtcDir(); - public static string DataMonoLibDir => internal_DataMonoLibDir(); - - #region Windows-only - public static string DataMonoBinDir => internal_DataMonoBinDir(); - #endregion - - - #region Internal - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResDataDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResMetadataDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResAssembliesBaseDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResAssembliesDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResConfigDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResTempDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResTempAssembliesBaseDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResTempAssembliesDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_MonoUserDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_MonoLogsDir(); - - #region Tools-only - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_MonoSolutionsDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_BuildLogsDirs(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ProjectSlnPath(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ProjectCsProjPath(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_DataEditorToolsDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_DataEditorPrebuiltApiDir(); - #endregion - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_DataMonoEtcDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_DataMonoLibDir(); - - #region Windows-only - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_DataMonoBinDir(); - #endregion - - #endregion + public static string ResMetadataDir + { + get + { + Internal.godot_icall_GodotSharpDirs_ResMetadataDir(out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } + } + + public static string MonoUserDir + { + get + { + Internal.godot_icall_GodotSharpDirs_MonoUserDir(out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } + } + + public static string BuildLogsDirs + { + get + { + Internal.godot_icall_GodotSharpDirs_BuildLogsDirs(out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } + } + + public static string DataEditorToolsDir + { + get + { + Internal.godot_icall_GodotSharpDirs_DataEditorToolsDir(out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } + } + + public static void RegisterProjectSettings() + { + GlobalDef("dotnet/project/assembly_name", ""); + GlobalDef("dotnet/project/solution_directory", ""); + GlobalDef("dotnet/project/c#_project_directory", ""); + } + + private static void DetermineProjectLocation() + { + static string DetermineProjectName() + { + string projectAssemblyName = (string)ProjectSettings.GetSetting("application/config/name"); + projectAssemblyName = projectAssemblyName.ToSafeDirName(); + if (string.IsNullOrEmpty(projectAssemblyName)) + projectAssemblyName = "UnnamedProject"; + return projectAssemblyName; + } + + _projectAssemblyName = (string)ProjectSettings.GetSetting("dotnet/project/assembly_name"); + if (string.IsNullOrEmpty(_projectAssemblyName)) + { + _projectAssemblyName = DetermineProjectName(); + ProjectSettings.SetSetting("dotnet/project/assembly_name", _projectAssemblyName); + } + + string slnParentDir = (string)ProjectSettings.GetSetting("dotnet/project/solution_directory"); + if (string.IsNullOrEmpty(slnParentDir)) + slnParentDir = "res://"; + + string csprojParentDir = (string)ProjectSettings.GetSetting("dotnet/project/c#_project_directory"); + if (string.IsNullOrEmpty(csprojParentDir)) + csprojParentDir = "res://"; + + _projectSlnPath = Path.Combine(ProjectSettings.GlobalizePath(slnParentDir), + string.Concat(_projectAssemblyName, ".sln")); + + _projectCsProjPath = Path.Combine(ProjectSettings.GlobalizePath(csprojParentDir), + string.Concat(_projectAssemblyName, ".csproj")); + } + + private static string _projectAssemblyName; + private static string _projectSlnPath; + private static string _projectCsProjPath; + + public static string ProjectAssemblyName + { + get + { + if (_projectAssemblyName == null) + DetermineProjectLocation(); + return _projectAssemblyName; + } + } + + public static string ProjectSlnPath + { + get + { + if (_projectSlnPath == null) + DetermineProjectLocation(); + return _projectSlnPath; + } + } + + public static string ProjectCsProjPath + { + get + { + if (_projectCsProjPath == null) + DetermineProjectLocation(); + return _projectCsProjPath; + } + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs index 12c90178c9..fd810996f7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs @@ -1,114 +1,161 @@ +using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Godot; +using Godot.NativeInterop; +using Godot.SourceGenerators.Internal; using GodotTools.IdeMessaging.Requests; namespace GodotTools.Internals { - public static class Internal + [SuppressMessage("ReSharper", "InconsistentNaming")] + [GenerateUnmanagedCallbacks(typeof(InternalUnmanagedCallbacks))] + internal static partial class Internal { public const string CSharpLanguageType = "CSharpScript"; public const string CSharpLanguageExtension = ".cs"; - public static string UpdateApiAssembliesFromPrebuilt(string config) => - internal_UpdateApiAssembliesFromPrebuilt(config); + public static string FullExportTemplatesDir + { + get + { + godot_icall_Internal_FullExportTemplatesDir(out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } + } - public static string FullExportTemplatesDir => - internal_FullExportTemplatesDir(); + public static string SimplifyGodotPath(this string path) => Godot.StringExtensions.SimplifyPath(path); - public static string SimplifyGodotPath(this string path) => internal_SimplifyGodotPath(path); + public static bool IsMacOSAppBundleInstalled(string bundleId) + { + using godot_string bundleIdIn = Marshaling.ConvertStringToNative(bundleId); + return godot_icall_Internal_IsMacOSAppBundleInstalled(bundleIdIn); + } - public static bool IsMacOSAppBundleInstalled(string bundleId) => internal_IsMacOSAppBundleInstalled(bundleId); + public static bool GodotIs32Bits() => godot_icall_Internal_GodotIs32Bits(); - public static bool GodotIs32Bits() => internal_GodotIs32Bits(); + public static bool GodotIsRealTDouble() => godot_icall_Internal_GodotIsRealTDouble(); - public static bool GodotIsRealTDouble() => internal_GodotIsRealTDouble(); + public static void GodotMainIteration() => godot_icall_Internal_GodotMainIteration(); - public static void GodotMainIteration() => internal_GodotMainIteration(); + public static bool IsAssembliesReloadingNeeded() => godot_icall_Internal_IsAssembliesReloadingNeeded(); - public static ulong GetCoreApiHash() => internal_GetCoreApiHash(); + public static void ReloadAssemblies(bool softReload) => godot_icall_Internal_ReloadAssemblies(softReload); - public static ulong GetEditorApiHash() => internal_GetEditorApiHash(); + public static void EditorDebuggerNodeReloadScripts() => godot_icall_Internal_EditorDebuggerNodeReloadScripts(); - public static bool IsAssembliesReloadingNeeded() => internal_IsAssembliesReloadingNeeded(); + public static bool ScriptEditorEdit(Resource resource, int line, int col, bool grabFocus = true) => + godot_icall_Internal_ScriptEditorEdit(resource.NativeInstance, line, col, grabFocus); - public static void ReloadAssemblies(bool softReload) => internal_ReloadAssemblies(softReload); + public static void EditorNodeShowScriptScreen() => godot_icall_Internal_EditorNodeShowScriptScreen(); - public static void EditorDebuggerNodeReloadScripts() => internal_EditorDebuggerNodeReloadScripts(); + public static void EditorRunPlay() => godot_icall_Internal_EditorRunPlay(); - public static bool ScriptEditorEdit(Resource resource, int line, int col, bool grabFocus = true) => - internal_ScriptEditorEdit(resource, line, col, grabFocus); + public static void EditorRunStop() => godot_icall_Internal_EditorRunStop(); - public static void EditorNodeShowScriptScreen() => internal_EditorNodeShowScriptScreen(); + public static void ScriptEditorDebugger_ReloadScripts() => + godot_icall_Internal_ScriptEditorDebugger_ReloadScripts(); - public static string MonoWindowsInstallRoot => internal_MonoWindowsInstallRoot(); + public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, + string scriptFile) + { + using godot_string scriptFileIn = Marshaling.ConvertStringToNative(scriptFile); + godot_icall_Internal_CodeCompletionRequest((int)kind, scriptFileIn, out godot_packed_string_array res); + using (res) + return Marshaling.ConvertNativePackedStringArrayToSystemArray(res); + } - public static void EditorRunPlay() => internal_EditorRunPlay(); + #region Internal - public static void EditorRunStop() => internal_EditorRunStop(); + private static bool initialized = false; - public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts(); + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Global + internal static unsafe void Initialize(IntPtr unmanagedCallbacks, int unmanagedCallbacksSize) + { + if (initialized) + throw new InvalidOperationException("Already initialized."); + initialized = true; - public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, string scriptFile) => - internal_CodeCompletionRequest((int)kind, scriptFile); + if (unmanagedCallbacksSize != sizeof(InternalUnmanagedCallbacks)) + throw new ArgumentException("Unmanaged callbacks size mismatch.", nameof(unmanagedCallbacksSize)); - #region Internal + _unmanagedCallbacks = Unsafe.AsRef<InternalUnmanagedCallbacks>((void*)unmanagedCallbacks); + } + + private partial struct InternalUnmanagedCallbacks + { + } + + /* + * IMPORTANT: + * The order of the methods defined in NativeFuncs must match the order + * in the array defined at the bottom of 'editor/editor_internal_calls.cpp'. + */ + + public static partial void godot_icall_GodotSharpDirs_ResMetadataDir(out godot_string r_dest); + + public static partial void godot_icall_GodotSharpDirs_MonoUserDir(out godot_string r_dest); + + public static partial void godot_icall_GodotSharpDirs_BuildLogsDirs(out godot_string r_dest); + + public static partial void godot_icall_GodotSharpDirs_DataEditorToolsDir(out godot_string r_dest); + + public static partial void godot_icall_EditorProgress_Create(in godot_string task, in godot_string label, + int amount, bool canCancel); + + public static partial void godot_icall_EditorProgress_Dispose(in godot_string task); + + public static partial bool godot_icall_EditorProgress_Step(in godot_string task, in godot_string state, + int step, + bool forceRefresh); + + private static partial void godot_icall_Internal_FullExportTemplatesDir(out godot_string dest); + + private static partial bool godot_icall_Internal_IsMacOSAppBundleInstalled(in godot_string bundleId); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_UpdateApiAssembliesFromPrebuilt(string config); + private static partial bool godot_icall_Internal_GodotIs32Bits(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_FullExportTemplatesDir(); + private static partial bool godot_icall_Internal_GodotIsRealTDouble(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_SimplifyGodotPath(this string path); + private static partial void godot_icall_Internal_GodotMainIteration(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_IsMacOSAppBundleInstalled(string bundleId); + private static partial bool godot_icall_Internal_IsAssembliesReloadingNeeded(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_GodotIs32Bits(); + private static partial void godot_icall_Internal_ReloadAssemblies(bool softReload); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_GodotIsRealTDouble(); + private static partial void godot_icall_Internal_EditorDebuggerNodeReloadScripts(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_GodotMainIteration(); + private static partial bool godot_icall_Internal_ScriptEditorEdit(IntPtr resource, int line, int col, + bool grabFocus); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern ulong internal_GetCoreApiHash(); + private static partial void godot_icall_Internal_EditorNodeShowScriptScreen(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern ulong internal_GetEditorApiHash(); + private static partial void godot_icall_Internal_EditorRunPlay(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_IsAssembliesReloadingNeeded(); + private static partial void godot_icall_Internal_EditorRunStop(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_ReloadAssemblies(bool softReload); + private static partial void godot_icall_Internal_ScriptEditorDebugger_ReloadScripts(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_EditorDebuggerNodeReloadScripts(); + private static partial void godot_icall_Internal_CodeCompletionRequest(int kind, in godot_string scriptFile, + out godot_packed_string_array res); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_ScriptEditorEdit(Resource resource, int line, int col, bool grabFocus); + public static partial float godot_icall_Globals_EditorScale(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_EditorNodeShowScriptScreen(); + public static partial void godot_icall_Globals_GlobalDef(in godot_string setting, in godot_variant defaultValue, + bool restartIfChanged, out godot_variant result); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_MonoWindowsInstallRoot(); + public static partial void godot_icall_Globals_EditorDef(in godot_string setting, in godot_variant defaultValue, + bool restartIfChanged, out godot_variant result); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_EditorRunPlay(); + public static partial void + godot_icall_Globals_EditorShortcut(in godot_string setting, out godot_variant result); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_EditorRunStop(); + public static partial void godot_icall_Globals_TTR(in godot_string text, out godot_string dest); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_ScriptEditorDebugger_ReloadScripts(); + public static partial void godot_icall_Utils_OS_GetPlatformName(out godot_string dest); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string[] internal_CodeCompletionRequest(int kind, string scriptFile); + public static partial bool godot_icall_Utils_OS_UnixFileHasExecutableAccess(in godot_string filePath); #endregion } diff --git a/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs b/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs index 820d0c0b83..9a8fdcc7c5 100644 --- a/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs +++ b/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs @@ -1,6 +1,6 @@ namespace GodotTools { - public struct PlaySettings + public readonly struct PlaySettings { public bool HasDebugger { get; } public string DebuggerHost { get; } diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs index 05499339b1..89bda704bb 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs @@ -1,14 +1,14 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using Godot; using GodotTools.Core; -using JetBrains.Annotations; namespace GodotTools.Utils { public static class FsPathUtils { - private static readonly string _resourcePath = ProjectSettings.GlobalizePath("res://"); + private static readonly string ResourcePath = ProjectSettings.GlobalizePath("res://"); private static bool PathStartsWithAlreadyNorm(this string childPath, string parentPath) { @@ -30,11 +30,11 @@ namespace GodotTools.Utils return childPathNorm.PathStartsWithAlreadyNorm(parentPathNorm); } - [CanBeNull] + [return: MaybeNull] public static string LocalizePathWithCaseChecked(string path) { string pathNorm = path.NormalizePath() + Path.DirectorySeparatorChar; - string resourcePathNorm = _resourcePath.NormalizePath() + Path.DirectorySeparatorChar; + string resourcePathNorm = ResourcePath.NormalizePath() + Path.DirectorySeparatorChar; if (!pathNorm.PathStartsWithAlreadyNorm(resourcePathNorm)) return null; diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index 5cef6e5c3c..c16f803226 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -1,24 +1,24 @@ +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 JetBrains.Annotations; +using System.Runtime.Versioning; +using System.Text; +using GodotTools.Internals; namespace GodotTools.Utils { [SuppressMessage("ReSharper", "InconsistentNaming")] public static class OS { - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string GetPlatformName(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool UnixFileHasExecutableAccess(string filePath); - - public static class Names + /// <summary> + /// Display names for the OS platforms. + /// </summary> + private static class Names { public const string Windows = "Windows"; public const string MacOS = "macOS"; @@ -26,27 +26,58 @@ namespace GodotTools.Utils public const string FreeBSD = "FreeBSD"; public const string NetBSD = "NetBSD"; public const string BSD = "BSD"; - public const string Server = "Server"; public const string UWP = "UWP"; public const string Haiku = "Haiku"; public const string Android = "Android"; public const string iOS = "iOS"; - public const string HTML5 = "HTML5"; + public const string Web = "Web"; } + /// <summary> + /// Godot platform identifiers. + /// </summary> public static class Platforms { public const string Windows = "windows"; public const string MacOS = "macos"; public const string LinuxBSD = "linuxbsd"; - public const string Server = "server"; public const string UWP = "uwp"; public const string Haiku = "haiku"; public const string Android = "android"; public const string iOS = "ios"; - public const string HTML5 = "javascript"; + public const string Web = "web"; + } + + /// <summary> + /// OS name part of the .NET runtime identifier (RID). + /// See https://docs.microsoft.com/en-us/dotnet/core/rid-catalog. + /// </summary> + public 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, + ["Linux"] = Platforms.LinuxBSD, + ["UWP"] = Platforms.UWP, + ["Haiku"] = Platforms.Haiku, + ["Android"] = Platforms.Android, + ["iOS"] = Platforms.iOS, + ["Web"] = Platforms.Web + }; + public static readonly Dictionary<string, string> PlatformNameMap = new Dictionary<string, string> { [Names.Windows] = Platforms.Windows, @@ -55,55 +86,85 @@ namespace GodotTools.Utils [Names.FreeBSD] = Platforms.LinuxBSD, [Names.NetBSD] = Platforms.LinuxBSD, [Names.BSD] = Platforms.LinuxBSD, - [Names.Server] = Platforms.Server, [Names.UWP] = Platforms.UWP, [Names.Haiku] = Platforms.Haiku, [Names.Android] = Platforms.Android, [Names.iOS] = Platforms.iOS, - [Names.HTML5] = Platforms.HTML5 + [Names.Web] = Platforms.Web + }; + + 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.UWP] = DotNetOS.Win10, + [Platforms.Android] = DotNetOS.Android, + [Platforms.iOS] = DotNetOS.iOS, + [Platforms.Web] = DotNetOS.Browser }; private static bool IsOS(string name) { - return name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase); + Internal.godot_icall_Utils_OS_GetPlatformName(out godot_string dest); + using (dest) + { + string platformName = Marshaling.ConvertStringToManaged(dest); + return name.Equals(platformName, StringComparison.OrdinalIgnoreCase); + } } private static bool IsAnyOS(IEnumerable<string> names) { - return names.Any(p => p.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase)); + Internal.godot_icall_Utils_OS_GetPlatformName(out godot_string dest); + using (dest) + { + string platformName = Marshaling.ConvertStringToManaged(dest); + return names.Any(p => p.Equals(platformName, StringComparison.OrdinalIgnoreCase)); + } } private static readonly IEnumerable<string> LinuxBSDPlatforms = new[] { Names.Linux, Names.FreeBSD, Names.NetBSD, Names.BSD }; private static readonly IEnumerable<string> UnixLikePlatforms = - new[] { Names.MacOS, Names.Server, Names.Haiku, Names.Android, Names.iOS } + new[] { Names.MacOS, Names.Haiku, Names.Android, Names.iOS } .Concat(LinuxBSDPlatforms).ToArray(); - private static readonly Lazy<bool> _isWindows = new Lazy<bool>(() => IsOS(Names.Windows)); - private static readonly Lazy<bool> _isMacOS = new Lazy<bool>(() => IsOS(Names.MacOS)); - private static readonly Lazy<bool> _isLinuxBSD = new Lazy<bool>(() => IsAnyOS(LinuxBSDPlatforms)); - private static readonly Lazy<bool> _isServer = new Lazy<bool>(() => IsOS(Names.Server)); - private static readonly Lazy<bool> _isUWP = new Lazy<bool>(() => IsOS(Names.UWP)); - private static readonly Lazy<bool> _isHaiku = new Lazy<bool>(() => IsOS(Names.Haiku)); - private static readonly Lazy<bool> _isAndroid = new Lazy<bool>(() => IsOS(Names.Android)); - private static readonly Lazy<bool> _isiOS = new Lazy<bool>(() => IsOS(Names.iOS)); - private static readonly Lazy<bool> _isHTML5 = new Lazy<bool>(() => IsOS(Names.HTML5)); - private static readonly Lazy<bool> _isUnixLike = new Lazy<bool>(() => IsAnyOS(UnixLikePlatforms)); - - public static bool IsWindows => _isWindows.Value || IsUWP; - public static bool IsMacOS => _isMacOS.Value; - public static bool IsLinuxBSD => _isLinuxBSD.Value; - public static bool IsServer => _isServer.Value; - public static bool IsUWP => _isUWP.Value; + private static readonly Lazy<bool> _isWindows = new(() => IsOS(Names.Windows)); + private static readonly Lazy<bool> _isMacOS = new(() => IsOS(Names.MacOS)); + private static readonly Lazy<bool> _isLinuxBSD = new(() => IsAnyOS(LinuxBSDPlatforms)); + private static readonly Lazy<bool> _isUWP = new(() => IsOS(Names.UWP)); + private static readonly Lazy<bool> _isHaiku = new(() => IsOS(Names.Haiku)); + private static readonly Lazy<bool> _isAndroid = new(() => IsOS(Names.Android)); + private static readonly Lazy<bool> _isiOS = new(() => IsOS(Names.iOS)); + private static readonly Lazy<bool> _isWeb = new(() => IsOS(Names.Web)); + private static readonly Lazy<bool> _isUnixLike = new(() => IsAnyOS(UnixLikePlatforms)); + + [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("windows")] public static bool IsUWP => _isUWP.Value; + public static bool IsHaiku => _isHaiku.Value; - public static bool IsAndroid => _isAndroid.Value; - public static bool IsiOS => _isiOS.Value; - public static bool IsHTML5 => _isHTML5.Value; + + [SupportedOSPlatformGuard("android")] public static bool IsAndroid => _isAndroid.Value; + + [SupportedOSPlatformGuard("ios")] public static bool IsiOS => _isiOS.Value; + + [SupportedOSPlatformGuard("browser")] public static bool IsWeb => _isWeb.Value; public static bool IsUnixLike => _isUnixLike.Value; public static char PathSep => IsWindows ? ';' : ':'; + [return: MaybeNull] public static string PathWhich([NotNull] string name) { if (IsWindows) @@ -112,9 +173,11 @@ namespace GodotTools.Utils return PathWhichUnix(name); } + [return: MaybeNull] private static string PathWhichWindows([NotNull] string name) { - string[] windowsExts = Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) ?? Array.Empty<string>(); + string[] windowsExts = + Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) ?? Array.Empty<string>(); string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); char[] invalidPathChars = Path.GetInvalidPathChars(); @@ -133,7 +196,7 @@ namespace GodotTools.Utils string nameExt = Path.GetExtension(name); bool hasPathExt = !string.IsNullOrEmpty(nameExt) && - windowsExts.Contains(nameExt, StringComparer.OrdinalIgnoreCase); + windowsExts.Contains(nameExt, StringComparer.OrdinalIgnoreCase); searchDirs.Add(System.IO.Directory.GetCurrentDirectory()); // last in the list @@ -147,6 +210,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); @@ -168,19 +232,16 @@ namespace GodotTools.Utils searchDirs.Add(System.IO.Directory.GetCurrentDirectory()); // last in the list return searchDirs.Select(dir => Path.Combine(dir, name)) - .FirstOrDefault(path => File.Exists(path) && UnixFileHasExecutableAccess(path)); + .FirstOrDefault(path => + { + using godot_string pathIn = Marshaling.ConvertStringToNative(path); + return File.Exists(path) && Internal.godot_icall_Utils_OS_UnixFileHasExecutableAccess(pathIn); + }); } 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, @@ -188,44 +249,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 InvalidOperationException("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); - var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments)); + Console.WriteLine(startInfo.GetCommandLineDisplay(new StringBuilder("Executing: ")).ToString()); - Console.WriteLine($"Executing: \"{startInfo.FileName}\" {startInfo.Arguments}"); + using var process = new Process { StartInfo = startInfo }; + process.Start(); + process.WaitForExit(); + + return process.ExitCode; + } - // Print the output - startInfo.RedirectStandardOutput = false; - startInfo.RedirectStandardError = false; + private static void AppendProcessFileNameForDisplay(this StringBuilder builder, string fileName) + { + if (builder.Length > 0) + builder.Append(' '); + + 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; + } } } |