diff options
Diffstat (limited to 'modules/mono/editor/GodotTools')
20 files changed, 699 insertions, 811 deletions
diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index c549cf5f12..d921e2f6a2 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.Text; using Microsoft.Build.Construction; @@ -45,7 +46,7 @@ namespace GodotTools.ProjectEditor // Save (without BOM) root.Save(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); - return Guid.NewGuid().ToString().ToUpper(); + return Guid.NewGuid().ToString().ToUpperInvariant(); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index 9b921c517c..7b1d5c228a 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -19,8 +19,15 @@ namespace GodotTools.ProjectEditor public static class ProjectUtils { - public static void MSBuildLocatorRegisterDefaults() - => Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults(); + public static void MSBuildLocatorRegisterDefaults(out Version version, out string path) + { + var instance = Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults(); + version = instance.Version; + path = instance.MSBuildPath; + } + + public static void MSBuildLocatorRegisterMSBuildPath(string msbuildPath) + => Microsoft.Build.Locator.MSBuildLocator.RegisterMSBuildPath(msbuildPath); public static MSBuildProject Open(string path) { diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln index 415e49b426..f3d2f90f39 100644 --- a/modules/mono/editor/GodotTools/GodotTools.sln +++ b/modules/mono/editor/GodotTools/GodotTools.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.OpenVisualStudio EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Shared", "GodotTools.Shared\GodotTools.Shared.csproj", "{2758FFAF-8237-4CF2-B569-66BF8B3587BB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators", "..\Godot.NET.Sdk\Godot.SourceGenerators\Godot.SourceGenerators.csproj", "{D8C421B2-8911-41EB-B983-F675C7141EB7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,5 +51,9 @@ Global {2758FFAF-8237-4CF2-B569-66BF8B3587BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {2758FFAF-8237-4CF2-B569-66BF8B3587BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {2758FFAF-8237-4CF2-B569-66BF8B3587BB}.Release|Any CPU.Build.0 = Release|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs index 28bf57dc21..18073d6492 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs @@ -4,26 +4,36 @@ using Godot.Collections; using GodotTools.Internals; using Path = System.IO.Path; +#nullable enable + namespace GodotTools.Build { [Serializable] - public sealed class BuildInfo : RefCounted // TODO Remove RefCounted once we have proper serialization + public sealed partial class BuildInfo : RefCounted // TODO Remove RefCounted once we have proper serialization { public string Solution { get; } - public string[] Targets { get; } public string Configuration { get; } + public string? RuntimeIdentifier { get; } + public string? PublishOutputDir { get; } public bool Restore { get; } + public bool Rebuild { get; } + public bool OnlyClean { get; } + // TODO Use List once we have proper serialization public Array<string> CustomProperties { get; } = new Array<string>(); - public string LogsDirPath => Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}"); + public string LogsDirPath => + Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}"); - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is BuildInfo other) - return other.Solution == Solution && other.Targets == Targets && - other.Configuration == Configuration && other.Restore == Restore && - other.CustomProperties == CustomProperties && other.LogsDirPath == LogsDirPath; + return other.Solution == Solution && + other.Configuration == Configuration && other.RuntimeIdentifier == RuntimeIdentifier && + other.PublishOutputDir == PublishOutputDir && other.Restore == Restore && + other.Rebuild == Rebuild && other.OnlyClean == OnlyClean && + other.CustomProperties == CustomProperties && + other.LogsDirPath == LogsDirPath; return false; } @@ -34,25 +44,37 @@ namespace GodotTools.Build { int hash = 17; hash = (hash * 29) + Solution.GetHashCode(); - hash = (hash * 29) + Targets.GetHashCode(); hash = (hash * 29) + Configuration.GetHashCode(); + hash = (hash * 29) + (RuntimeIdentifier?.GetHashCode() ?? 0); + hash = (hash * 29) + (PublishOutputDir?.GetHashCode() ?? 0); hash = (hash * 29) + Restore.GetHashCode(); + hash = (hash * 29) + Rebuild.GetHashCode(); + hash = (hash * 29) + OnlyClean.GetHashCode(); hash = (hash * 29) + CustomProperties.GetHashCode(); hash = (hash * 29) + LogsDirPath.GetHashCode(); return hash; } } - private BuildInfo() + public BuildInfo(string solution, string configuration, bool restore, bool rebuild, bool onlyClean) { + Solution = solution; + Configuration = configuration; + Restore = restore; + Rebuild = rebuild; + OnlyClean = onlyClean; } - public BuildInfo(string solution, string[] targets, string configuration, bool restore) + public BuildInfo(string solution, string configuration, string runtimeIdentifier, + string publishOutputDir, bool restore, bool rebuild, bool onlyClean) { Solution = solution; - Targets = targets; Configuration = configuration; + RuntimeIdentifier = runtimeIdentifier; + PublishOutputDir = publishOutputDir; Restore = restore; + Rebuild = rebuild; + OnlyClean = onlyClean; } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs index 58677625c6..43256953f5 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs @@ -2,11 +2,9 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; -using GodotTools.Ides.Rider; +using Godot; using GodotTools.Internals; -using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; -using OS = GodotTools.Utils.OS; namespace GodotTools.Build { @@ -14,13 +12,8 @@ namespace GodotTools.Build { private static BuildInfo _buildInProgress; - public const string PropNameMSBuildMono = "MSBuild (Mono)"; - public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)"; - public const string PropNameMSBuildJetBrains = "MSBuild (JetBrains Rider)"; - public const string PropNameDotnetCli = "dotnet CLI"; - public const string MsBuildIssuesFileName = "msbuild_issues.csv"; - public const string MsBuildLogFileName = "msbuild_log.txt"; + private const string MsBuildLogFileName = "msbuild_log.txt"; public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason); @@ -62,11 +55,11 @@ namespace GodotTools.Build private static void PrintVerbose(string text) { - if (Godot.OS.IsStdoutVerbose()) - Godot.GD.Print(text); + if (OS.IsStdoutVerbose()) + GD.Print(text); } - public static bool Build(BuildInfo buildInfo) + private static bool Build(BuildInfo buildInfo) { if (_buildInProgress != null) throw new InvalidOperationException("A build is already in progress"); @@ -103,7 +96,8 @@ namespace GodotTools.Build } catch (Exception e) { - BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); + BuildLaunchFailed?.Invoke(buildInfo, + $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); Console.Error.WriteLine(e); return false; } @@ -148,7 +142,8 @@ namespace GodotTools.Build } catch (Exception e) { - BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); + BuildLaunchFailed?.Invoke(buildInfo, + $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); Console.Error.WriteLine(e); return false; } @@ -159,18 +154,54 @@ namespace GodotTools.Build } } - public static bool BuildProjectBlocking(string config, [MaybeNull] string[] targets = null, [MaybeNull] string platform = null) + private static bool Publish(BuildInfo buildInfo) { - var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets ?? new[] {"Build"}, config, restore: true); + if (_buildInProgress != null) + throw new InvalidOperationException("A build is already in progress"); - // If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it. - if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform)) - buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}"); + _buildInProgress = buildInfo; - if (Internal.GodotIsRealTDouble()) - buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); + try + { + BuildStarted?.Invoke(buildInfo); + + // Required in order to update the build tasks list + Internal.GodotMainIteration(); - return BuildProjectBlocking(buildInfo); + try + { + RemoveOldIssuesFile(buildInfo); + } + catch (IOException e) + { + BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); + Console.Error.WriteLine(e); + } + + try + { + int exitCode = BuildSystem.Publish(buildInfo, StdOutputReceived, StdErrorReceived); + + if (exitCode != 0) + PrintVerbose( + $"dotnet publish exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); + + BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error); + + return exitCode == 0; + } + catch (Exception e) + { + BuildLaunchFailed?.Invoke(buildInfo, + $"The publish method threw an exception.\n{e.GetType().FullName}: {e.Message}"); + Console.Error.WriteLine(e); + return false; + } + } + finally + { + _buildInProgress = null; + } } private static bool BuildProjectBlocking(BuildInfo buildInfo) @@ -178,20 +209,109 @@ namespace GodotTools.Build if (!File.Exists(buildInfo.Solution)) return true; // No solution to build - using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1)) + using var pr = new EditorProgress("dotnet_build_project", "Building .NET project...", 1); + + pr.Step("Building project solution", 0); + + if (!Build(buildInfo)) { - pr.Step("Building project solution", 0); + ShowBuildErrorDialog("Failed to build project solution"); + return false; + } - if (!Build(buildInfo)) - { - ShowBuildErrorDialog("Failed to build project solution"); - return false; - } + return true; + } + + private static bool CleanProjectBlocking(BuildInfo buildInfo) + { + if (!File.Exists(buildInfo.Solution)) + return true; // No solution to clean + + using var pr = new EditorProgress("dotnet_clean_project", "Cleaning .NET project...", 1); + + pr.Step("Cleaning project solution", 0); + + if (!Build(buildInfo)) + { + ShowBuildErrorDialog("Failed to clean project solution"); + return false; } return true; } + private static bool PublishProjectBlocking(BuildInfo buildInfo) + { + using var pr = new EditorProgress("dotnet_publish_project", "Publishing .NET project...", 1); + + pr.Step("Running dotnet publish", 0); + + if (!Publish(buildInfo)) + { + ShowBuildErrorDialog("Failed to publish .NET project"); + return false; + } + + return true; + } + + private static BuildInfo CreateBuildInfo( + [DisallowNull] string configuration, + [AllowNull] string platform = null, + bool rebuild = false, + bool onlyClean = false + ) + { + var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, configuration, + restore: true, rebuild, onlyClean); + + // If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it. + if (platform != null || Utils.OS.PlatformNameMap.TryGetValue(OS.GetName(), out platform)) + buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}"); + + if (Internal.GodotIsRealTDouble()) + buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); + + return buildInfo; + } + + private static BuildInfo CreatePublishBuildInfo( + [DisallowNull] string configuration, + [DisallowNull] string platform, + [DisallowNull] string runtimeIdentifier, + [DisallowNull] string publishOutputDir + ) + { + var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, configuration, + runtimeIdentifier, publishOutputDir, restore: true, rebuild: false, onlyClean: false); + + buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}"); + + if (Internal.GodotIsRealTDouble()) + buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); + + return buildInfo; + } + + public static bool BuildProjectBlocking( + [DisallowNull] string configuration, + [AllowNull] string platform = null, + bool rebuild = false + ) => BuildProjectBlocking(CreateBuildInfo(configuration, platform, rebuild)); + + public static bool CleanProjectBlocking( + [DisallowNull] string configuration, + [AllowNull] string platform = null + ) => CleanProjectBlocking(CreateBuildInfo(configuration, platform, rebuild: false)); + + public static bool PublishProjectBlocking( + [DisallowNull] string configuration, + [DisallowNull] string platform, + [DisallowNull] string runtimeIdentifier, + string publishOutputDir + ) => PublishProjectBlocking(CreatePublishBuildInfo(configuration, + platform, runtimeIdentifier, publishOutputDir)); + public static bool EditorBuildCallback() { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) @@ -204,7 +324,7 @@ namespace GodotTools.Build } catch (Exception e) { - Godot.GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); + GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); } if (GodotSharpEditor.Instance.SkipBuildBeforePlaying) @@ -215,47 +335,6 @@ namespace GodotTools.Build public static void Initialize() { - // Build tool settings - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - - BuildTool msbuildDefault; - - if (OS.IsWindows) - { - if (RiderPathManager.IsExternalEditorSetToRider(editorSettings)) - msbuildDefault = BuildTool.JetBrainsMsBuild; - else - msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildVs; - } - else - { - msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildMono; - } - - EditorDef("mono/builds/build_tool", msbuildDefault); - - string hintString; - - if (OS.IsWindows) - { - hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," + - $"{PropNameMSBuildVs}:{(int)BuildTool.MsBuildVs}," + - $"{PropNameMSBuildJetBrains}:{(int)BuildTool.JetBrainsMsBuild}," + - $"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}"; - } - else - { - hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," + - $"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}"; - } - - editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary - { - ["type"] = Godot.Variant.Type.Int, - ["name"] = "mono/builds/build_tool", - ["hint"] = Godot.PropertyHint.Enum, - ["hint_string"] = hintString - }); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs index ed5ee10585..f7b8c6bffd 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs @@ -8,10 +8,10 @@ using Path = System.IO.Path; namespace GodotTools.Build { - public class BuildOutputView : VBoxContainer, ISerializationListener + public partial class BuildOutputView : VBoxContainer, ISerializationListener { [Serializable] - private class BuildIssue : RefCounted // TODO Remove RefCounted once we have proper serialization + private partial class BuildIssue : RefCounted // TODO Remove RefCounted once we have proper serialization { public bool Warning { get; set; } public string File { get; set; } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 0e793a44ba..d2e3ecff57 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -1,61 +1,93 @@ -using GodotTools.Core; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; +using System.Globalization; using System.IO; +using System.Text; using System.Threading.Tasks; using GodotTools.BuildLogger; -using GodotTools.Internals; using GodotTools.Utils; -using Directory = System.IO.Directory; namespace GodotTools.Build { public static class BuildSystem { - private static string MonoWindowsBinDir + private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, + Action<string> stdErrHandler) { - get - { - string monoWinBinDir = Path.Combine(Internal.MonoWindowsInstallRoot, "bin"); + string dotnetPath = DotNetFinder.FindDotNetExe(); - if (!Directory.Exists(monoWinBinDir)) - throw new FileNotFoundException("Cannot find the Windows Mono install bin directory."); + if (dotnetPath == null) + throw new FileNotFoundException("Cannot find the dotnet executable."); - return monoWinBinDir; - } + var startInfo = new ProcessStartInfo(dotnetPath); + + BuildArguments(buildInfo, startInfo.ArgumentList); + + string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Running: ")).ToString(); + stdOutHandler?.Invoke(launchMessage); + if (Godot.OS.IsStdoutVerbose()) + Console.WriteLine(launchMessage); + + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + + // Needed when running from Developer Command Prompt for VS + RemovePlatformVariable(startInfo.EnvironmentVariables); + + var process = new Process { StartInfo = startInfo }; + + if (stdOutHandler != null) + process.OutputDataReceived += (s, e) => stdOutHandler.Invoke(e.Data); + if (stdErrHandler != null) + process.ErrorDataReceived += (s, e) => stdErrHandler.Invoke(e.Data); + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + return process; } - private static Godot.EditorSettings EditorSettings => - GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); + public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + { + using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) + { + process.WaitForExit(); + + return process.ExitCode; + } + } - private static bool UsingMonoMsBuildOnWindows + public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, + Action<string> stdErrHandler) { - get + using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) { - if (OS.IsWindows) - { - return (BuildTool)EditorSettings.GetSetting("mono/builds/build_tool") - == BuildTool.MsBuildMono; - } + await process.WaitForExitAsync(); - return false; + return process.ExitCode; } } - private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + private static Process LaunchPublish(BuildInfo buildInfo, Action<string> stdOutHandler, + Action<string> stdErrHandler) { - (string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild(); + string dotnetPath = DotNetFinder.FindDotNetExe(); - if (msbuildPath == null) - throw new FileNotFoundException("Cannot find the MSBuild executable."); + if (dotnetPath == null) + throw new FileNotFoundException("Cannot find the dotnet executable."); - string compilerArgs = BuildArguments(buildTool, buildInfo); + var startInfo = new ProcessStartInfo(dotnetPath); - var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs); + BuildPublishArguments(buildInfo, startInfo.ArgumentList); - string launchMessage = $"Running: \"{startInfo.FileName}\" {startInfo.Arguments}"; + string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Running: ")).ToString(); stdOutHandler?.Invoke(launchMessage); if (Godot.OS.IsStdoutVerbose()) Console.WriteLine(launchMessage); @@ -63,22 +95,11 @@ namespace GodotTools.Build startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; startInfo.UseShellExecute = false; - startInfo.CreateNoWindow = true; - - if (UsingMonoMsBuildOnWindows) - { - // These environment variables are required for Mono's MSBuild to find the compilers. - // We use the batch files in Mono's bin directory to make sure the compilers are executed with mono. - string monoWinBinDir = MonoWindowsBinDir; - startInfo.EnvironmentVariables.Add("CscToolExe", Path.Combine(monoWinBinDir, "csc.bat")); - startInfo.EnvironmentVariables.Add("VbcToolExe", Path.Combine(monoWinBinDir, "vbc.bat")); - startInfo.EnvironmentVariables.Add("FscToolExe", Path.Combine(monoWinBinDir, "fsharpc.bat")); - } // Needed when running from Developer Command Prompt for VS RemovePlatformVariable(startInfo.EnvironmentVariables); - var process = new Process {StartInfo = startInfo}; + var process = new Process { StartInfo = startInfo }; if (stdOutHandler != null) process.OutputDataReceived += (s, e) => stdOutHandler.Invoke(e.Data); @@ -93,9 +114,9 @@ namespace GodotTools.Build return process; } - public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + public static int Publish(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) { - using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) + using (var process = LaunchPublish(buildInfo, stdOutHandler, stdErrHandler)) { process.WaitForExit(); @@ -103,46 +124,105 @@ namespace GodotTools.Build } } - public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + private static void BuildArguments(BuildInfo buildInfo, Collection<string> arguments) { - using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) + // `dotnet clean` / `dotnet build` commands + arguments.Add(buildInfo.OnlyClean ? "clean" : "build"); + + // Solution + arguments.Add(buildInfo.Solution); + + // `dotnet clean` doesn't recognize these options + if (!buildInfo.OnlyClean) { - await process.WaitForExitAsync(); + // Restore + // `dotnet build` restores by default, unless requested not to + if (!buildInfo.Restore) + arguments.Add("--no-restore"); + + // Incremental or rebuild + if (buildInfo.Rebuild) + arguments.Add("--no-incremental"); + } - return process.ExitCode; + // Configuration + arguments.Add("-c"); + arguments.Add(buildInfo.Configuration); + + // Verbosity + arguments.Add("-v"); + arguments.Add("normal"); + + // Logger + AddLoggerArgument(buildInfo, arguments); + + // Custom properties + foreach (string customProperty in buildInfo.CustomProperties) + { + arguments.Add("-p:" + customProperty); } } - private static string BuildArguments(BuildTool buildTool, BuildInfo buildInfo) + private static void BuildPublishArguments(BuildInfo buildInfo, Collection<string> arguments) { - string arguments = string.Empty; + arguments.Add("publish"); // `dotnet publish` command + + // Solution + arguments.Add(buildInfo.Solution); + + // Restore + // `dotnet publish` restores by default, unless requested not to + if (!buildInfo.Restore) + arguments.Add("--no-restore"); + + // Incremental or rebuild + if (buildInfo.Rebuild) + arguments.Add("--no-incremental"); - if (buildTool == BuildTool.DotnetCli) - arguments += "msbuild"; // `dotnet msbuild` command + // Configuration + arguments.Add("-c"); + arguments.Add(buildInfo.Configuration); - arguments += $@" ""{buildInfo.Solution}"""; + // Runtime Identifier + arguments.Add("-r"); + arguments.Add(buildInfo.RuntimeIdentifier!); - if (buildInfo.Restore) - arguments += " /restore"; + // Self-published + arguments.Add("--self-contained"); + arguments.Add("true"); - arguments += $@" /t:{string.Join(",", buildInfo.Targets)} " + - $@"""/p:{"Configuration=" + buildInfo.Configuration}"" /v:normal " + - $@"""{AddLoggerArgument(buildInfo)}"""; + // Verbosity + arguments.Add("-v"); + arguments.Add("normal"); + // Logger + AddLoggerArgument(buildInfo, arguments); + + // Trimming is not supported for dynamically loaded assemblies, as is our case with self hosting: + // https://github.com/dotnet/runtime/blob/main/docs/design/features/native-hosting.md#incompatible-with-trimming + arguments.Add("-p:PublishTrimmed=false"); + + // Custom properties foreach (string customProperty in buildInfo.CustomProperties) { - arguments += " /p:" + customProperty; + arguments.Add("-p:" + customProperty); } - return arguments; + // Publish output directory + if (buildInfo.PublishOutputDir != null) + { + arguments.Add("-o"); + arguments.Add(buildInfo.PublishOutputDir); + } } - private static string AddLoggerArgument(BuildInfo buildInfo) + private static void AddLoggerArgument(BuildInfo buildInfo, Collection<string> arguments) { - string buildLoggerPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, + string buildLoggerPath = Path.Combine(Internals.GodotSharpDirs.DataEditorToolsDir, "GodotTools.BuildLogger.dll"); - return $"/l:{typeof(GodotBuildLogger).FullName},{buildLoggerPath};{buildInfo.LogsDirPath}"; + arguments.Add( + $"-l:{typeof(GodotBuildLogger).FullName},{buildLoggerPath};{buildInfo.LogsDirPath}"); } private static void RemovePlatformVariable(StringDictionary environmentVariables) @@ -153,7 +233,7 @@ namespace GodotTools.Build foreach (string env in environmentVariables.Keys) { - if (env.ToUpper() == "PLATFORM") + if (env.ToUpperInvariant() == "PLATFORM") platformEnvironmentVariables.Add(env); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs deleted file mode 100644 index 837c8adddb..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace GodotTools.Build -{ - public enum BuildTool : long - { - MsBuildMono, - MsBuildVs, - JetBrainsMsBuild, - DotnetCli - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs new file mode 100644 index 0000000000..7bce53308c --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using JetBrains.Annotations; +using OS = GodotTools.Utils.OS; + +namespace GodotTools.Build +{ + public static class DotNetFinder + { + [CanBeNull] + public static string FindDotNetExe() + { + // In the future, this method may do more than just search in PATH. We could look in + // known locations or use Godot's linked nethost to search from the hostfxr location. + + return OS.PathWhich("dotnet"); + } + + public static bool TryFindDotNetSdk( + Version expectedVersion, + [NotNullWhen(true)] out Version version, + [NotNullWhen(true)] out string path + ) + { + version = null; + path = null; + + string dotNetExe = FindDotNetExe(); + + if (string.IsNullOrEmpty(dotNetExe)) + return false; + + using Process process = new Process(); + process.StartInfo = new ProcessStartInfo(dotNetExe, "--list-sdks") + { + UseShellExecute = false, + RedirectStandardOutput = true + }; + + process.StartInfo.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"] = "en-US"; + + var lines = new List<string>(); + + process.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrWhiteSpace(e.Data)) + lines.Add(e.Data); + }; + + try + { + process.Start(); + } + catch + { + return false; + } + + process.BeginOutputReadLine(); + process.WaitForExit(); + + Version latestVersionMatch = null; + string matchPath = null; + + foreach (var line in lines) + { + string[] sdkLineParts = line.Trim() + .Split(' ', 2, StringSplitOptions.TrimEntries); + + if (sdkLineParts.Length < 2) + continue; + + if (!Version.TryParse(sdkLineParts[0], out var lineVersion)) + continue; + + // We're looking for the exact same major version + if (lineVersion.Major != expectedVersion.Major) + continue; + + if (latestVersionMatch != null && lineVersion < latestVersionMatch) + continue; + + latestVersionMatch = lineVersion; + matchPath = sdkLineParts[1].TrimStart('[').TrimEnd(']'); + } + + if (latestVersionMatch == null) + return false; + + version = latestVersionMatch; + path = Path.Combine(matchPath!, version.ToString()); + + return true; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs index 13b3ab7da2..6dae0a3a0e 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs @@ -6,7 +6,7 @@ using File = GodotTools.Utils.File; namespace GodotTools.Build { - public class MSBuildPanel : VBoxContainer + public partial class MSBuildPanel : VBoxContainer { public BuildOutputView BuildOutputView { get; private set; } @@ -70,7 +70,7 @@ namespace GodotTools.Build GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); } - if (!BuildManager.BuildProjectBlocking("Debug", targets: new[] { "Rebuild" })) + if (!BuildManager.BuildProjectBlocking("Debug", rebuild: true)) return; // Build failed // Notify running game for hot-reload @@ -88,7 +88,7 @@ namespace GodotTools.Build if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) return; // No solution to build - BuildManager.BuildProjectBlocking("Debug", targets: new[] { "Clean" }); + _ = BuildManager.CleanProjectBlocking("Debug"); } private void ViewLogToggled(bool pressed) => BuildOutputView.LogVisible = pressed; diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs deleted file mode 100644 index a859c6f717..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Godot; -using GodotTools.Ides.Rider; -using GodotTools.Internals; -using Directory = System.IO.Directory; -using Environment = System.Environment; -using File = System.IO.File; -using Path = System.IO.Path; -using OS = GodotTools.Utils.OS; - -namespace GodotTools.Build -{ - public static class MsBuildFinder - { - private static string _msbuildToolsPath = string.Empty; - private static string _msbuildUnixPath = string.Empty; - - public static (string, BuildTool) FindMsBuild() - { - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); - - if (OS.IsWindows) - { - switch (buildTool) - { - case BuildTool.DotnetCli: - { - string dotnetCliPath = OS.PathWhich("dotnet"); - if (!string.IsNullOrEmpty(dotnetCliPath)) - return (dotnetCliPath, BuildTool.DotnetCli); - GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Visual Studio."); - goto case BuildTool.MsBuildVs; - } - case BuildTool.MsBuildVs: - { - if (string.IsNullOrEmpty(_msbuildToolsPath) || !File.Exists(_msbuildToolsPath)) - { - // Try to search it again if it wasn't found last time or if it was removed from its location - _msbuildToolsPath = FindMsBuildToolsPathOnWindows(); - - if (string.IsNullOrEmpty(_msbuildToolsPath)) - throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildVs}'."); - } - - if (!_msbuildToolsPath.EndsWith("\\")) - _msbuildToolsPath += "\\"; - - return (Path.Combine(_msbuildToolsPath, "MSBuild.exe"), BuildTool.MsBuildVs); - } - case BuildTool.MsBuildMono: - { - string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat"); - - if (!File.Exists(msbuildPath)) - throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildMono}'. Tried with path: {msbuildPath}"); - - return (msbuildPath, BuildTool.MsBuildMono); - } - case BuildTool.JetBrainsMsBuild: - { - string editorPath = (string)editorSettings.GetSetting(RiderPathManager.EditorPathSettingName); - - if (!File.Exists(editorPath)) - throw new FileNotFoundException($"Cannot find Rider executable. Tried with path: {editorPath}"); - - var riderDir = new FileInfo(editorPath).Directory?.Parent; - - string msbuildPath = Path.Combine(riderDir.FullName, @"tools\MSBuild\Current\Bin\MSBuild.exe"); - - if (!File.Exists(msbuildPath)) - throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildJetBrains}'. Tried with path: {msbuildPath}"); - - return (msbuildPath, BuildTool.JetBrainsMsBuild); - } - default: - throw new IndexOutOfRangeException("Invalid build tool in editor settings"); - } - } - - if (OS.IsUnixLike) - { - switch (buildTool) - { - case BuildTool.DotnetCli: - { - string dotnetCliPath = FindBuildEngineOnUnix("dotnet"); - if (!string.IsNullOrEmpty(dotnetCliPath)) - return (dotnetCliPath, BuildTool.DotnetCli); - GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Mono."); - goto case BuildTool.MsBuildMono; - } - case BuildTool.MsBuildMono: - { - if (string.IsNullOrEmpty(_msbuildUnixPath) || !File.Exists(_msbuildUnixPath)) - { - // Try to search it again if it wasn't found last time or if it was removed from its location - _msbuildUnixPath = FindBuildEngineOnUnix("msbuild"); - } - - if (string.IsNullOrEmpty(_msbuildUnixPath)) - throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMSBuildMono}'"); - - return (_msbuildUnixPath, BuildTool.MsBuildMono); - } - default: - throw new IndexOutOfRangeException("Invalid build tool in editor settings"); - } - } - - throw new PlatformNotSupportedException(); - } - - private static IEnumerable<string> MsBuildHintDirs - { - get - { - var result = new List<string>(); - - if (OS.IsMacOS) - { - result.Add("/Library/Frameworks/Mono.framework/Versions/Current/bin/"); - result.Add("/opt/local/bin/"); - result.Add("/usr/local/var/homebrew/linked/mono/bin/"); - result.Add("/usr/local/bin/"); - result.Add("/usr/local/bin/dotnet/"); - result.Add("/usr/local/share/dotnet/"); - } - - result.Add("/opt/novell/mono/bin/"); - - return result; - } - } - - private static string FindBuildEngineOnUnix(string name) - { - string ret = OS.PathWhich(name); - - if (!string.IsNullOrEmpty(ret)) - return ret; - - string retFallback = OS.PathWhich($"{name}.exe"); - - if (!string.IsNullOrEmpty(retFallback)) - return retFallback; - - foreach (string hintDir in MsBuildHintDirs) - { - string hintPath = Path.Combine(hintDir, name); - - if (File.Exists(hintPath)) - return hintPath; - } - - return string.Empty; - } - - private static string FindMsBuildToolsPathOnWindows() - { - if (!OS.IsWindows) - throw new PlatformNotSupportedException(); - - // Try to find 15.0 with vswhere - - string[] envNames = Internal.GodotIs32Bits() ? - envNames = new[] { "ProgramFiles", "ProgramW6432" } : - envNames = new[] { "ProgramFiles(x86)", "ProgramFiles" }; - - string vsWherePath = null; - foreach (var envName in envNames) - { - vsWherePath = Environment.GetEnvironmentVariable(envName); - if (!string.IsNullOrEmpty(vsWherePath)) - { - vsWherePath += "\\Microsoft Visual Studio\\Installer\\vswhere.exe"; - if (File.Exists(vsWherePath)) - break; - } - - vsWherePath = null; - } - - var vsWhereArgs = new[] {"-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"}; - - var outputArray = new Godot.Collections.Array<string>(); - int exitCode = Godot.OS.Execute(vsWherePath, vsWhereArgs, - output: (Godot.Collections.Array)outputArray); - - if (exitCode != 0) - return string.Empty; - - if (outputArray.Count == 0) - return string.Empty; - - var lines = outputArray[0].Split('\n'); - - foreach (string line in lines) - { - int sepIdx = line.IndexOf(':'); - - if (sepIdx <= 0) - continue; - - string key = line.Substring(0, sepIdx); // No need to trim - - if (key != "installationPath") - continue; - - string value = line.Substring(sepIdx + 1).StripEdges(); - - if (string.IsNullOrEmpty(value)) - throw new FormatException("installationPath value is empty"); - - if (!value.EndsWith("\\")) - value += "\\"; - - // Since VS2019, the directory is simply named "Current" - string msbuildDir = Path.Combine(value, "MSBuild\\Current\\Bin"); - - if (Directory.Exists(msbuildDir)) - return msbuildDir; - - // Directory name "15.0" is used in VS 2017 - return Path.Combine(value, "MSBuild\\15.0\\Bin"); - } - - return string.Empty; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs b/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs index 63b97e981e..fdb86c8f34 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; @@ -39,7 +40,8 @@ namespace GodotTools.Build // Since this can be considered pretty much a new NuGet.Config, add the default nuget.org source as well XmlElement nugetOrgSourceEntry = xmlDoc.CreateElement("add"); nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("key")).Value = "nuget.org"; - nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("value")).Value = "https://api.nuget.org/v3/index.json"; + nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("value")).Value = + "https://api.nuget.org/v3/index.json"; nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("protocolVersion")).Value = "3"; rootNode.AppendChild(xmlDoc.CreateElement("packageSources")).AppendChild(nugetOrgSourceEntry); } @@ -181,8 +183,8 @@ namespace GodotTools.Build // - The sha512 of the nupkg is base64 encoded. // - We can get the nuspec from the nupkg which is a Zip file. - string packageIdLower = packageId.ToLower(); - string packageVersionLower = packageVersion.ToLower(); + string packageIdLower = packageId.ToLowerInvariant(); + string packageVersionLower = packageVersion.ToLowerInvariant(); string destDir = Path.Combine(fallbackFolder, packageIdLower, packageVersionLower); string nupkgDestPath = Path.Combine(destDir, $"{packageIdLower}.{packageVersionLower}.nupkg"); @@ -227,9 +229,11 @@ namespace GodotTools.Build var nuspecEntry = archive.GetEntry(packageId + ".nuspec"); if (nuspecEntry == null) - throw new InvalidOperationException($"Failed to extract package {packageId}.{packageVersion}. Could not find the nuspec file."); + throw new InvalidOperationException( + $"Failed to extract package {packageId}.{packageVersion}. Could not find the nuspec file."); - nuspecEntry.ExtractToFile(Path.Combine(destDir, nuspecEntry.Name.ToLower().SimplifyGodotPath())); + nuspecEntry.ExtractToFile(Path.Combine(destDir, nuspecEntry.Name + .ToLowerInvariant().SimplifyGodotPath())); // Extract the other package files diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index d8ebe762e1..edf3eeb7fa 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -1,12 +1,9 @@ using Godot; -using Godot.NativeInterop; using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using GodotTools.Build; using GodotTools.Core; using GodotTools.Internals; @@ -18,61 +15,13 @@ using Path = System.IO.Path; namespace GodotTools.Export { - public class ExportPlugin : EditorExportPlugin + public partial class ExportPlugin : EditorExportPlugin { - [Flags] - private enum I18NCodesets : long - { - None = 0, - CJK = 1, - MidEast = 2, - Other = 4, - Rare = 8, - West = 16, - All = CJK | MidEast | Other | Rare | West - } - - private string _maybeLastExportError; - - private void AddI18NAssemblies(Godot.Collections.Dictionary<string, string> assemblies, string bclDir) - { - var codesets = (I18NCodesets)ProjectSettings.GetSetting("mono/export/i18n_codesets"); - - if (codesets == I18NCodesets.None) - return; - - void AddI18NAssembly(string name) => assemblies.Add(name, Path.Combine(bclDir, $"{name}.dll")); - - AddI18NAssembly("I18N"); - - if ((codesets & I18NCodesets.CJK) != 0) - AddI18NAssembly("I18N.CJK"); - if ((codesets & I18NCodesets.MidEast) != 0) - AddI18NAssembly("I18N.MidEast"); - if ((codesets & I18NCodesets.Other) != 0) - AddI18NAssembly("I18N.Other"); - if ((codesets & I18NCodesets.Rare) != 0) - AddI18NAssembly("I18N.Rare"); - if ((codesets & I18NCodesets.West) != 0) - AddI18NAssembly("I18N.West"); - } - public void RegisterExportSettings() { // TODO: These would be better as export preset options, but that doesn't seem to be supported yet GlobalDef("mono/export/include_scripts_content", false); - GlobalDef("mono/export/export_assemblies_inside_pck", true); - - GlobalDef("mono/export/i18n_codesets", I18NCodesets.All); - - ProjectSettings.AddPropertyInfo(new Godot.Collections.Dictionary - { - ["type"] = Variant.Type.Int, - ["name"] = "mono/export/i18n_codesets", - ["hint"] = PropertyHint.Flags, - ["hint_string"] = "CJK,MidEast,Other,Rare,West" - }); GlobalDef("mono/export/aot/enabled", false); GlobalDef("mono/export/aot/full_aot", false); @@ -86,11 +35,7 @@ namespace GodotTools.Export GlobalDef("mono/export/aot/android_toolchain_path", ""); } - private void AddFile(string srcPath, string dstPath, bool remap = false) - { - // Add file to the PCK - AddFile(dstPath.Replace("\\", "/"), File.ReadAllBytes(srcPath), remap); - } + private string _maybeLastExportError; // With this method we can override how a file is exported in the PCK public override void _ExportFile(string path, string type, string[] features) @@ -155,178 +100,84 @@ namespace GodotTools.Export if (!DeterminePlatformFromFeatures(features, out string platform)) throw new NotSupportedException("Target platform not supported"); + if (!new[] { OS.Platforms.Windows, OS.Platforms.LinuxBSD, OS.Platforms.MacOS, OS.Platforms.Server } + .Contains(platform)) + { + throw new NotImplementedException("Target platform not yet implemented"); + } + string outputDir = new FileInfo(path).Directory?.FullName ?? - throw new FileNotFoundException("Base directory not found"); + throw new FileNotFoundException("Output base directory not found"); string buildConfig = isDebug ? "ExportDebug" : "ExportRelease"; - if (!BuildManager.BuildProjectBlocking(buildConfig, platform: platform)) - throw new Exception("Failed to build project"); + // TODO: This works for now, as we only implemented support for x86 family desktop so far, but it needs to be fixed + string arch = features.Contains("64") ? "x86_64" : "x86"; - // Add dependency assemblies + string ridOS = DetermineRuntimeIdentifierOS(platform); + string ridArch = DetermineRuntimeIdentifierArch(arch); + string runtimeIdentifier = $"{ridOS}-{ridArch}"; - var assemblies = new Godot.Collections.Dictionary<string, string>(); + // Create temporary publish output directory - string projectDllName = GodotSharpEditor.ProjectAssemblyName; - string projectDllSrcDir = Path.Combine(GodotSharpDirs.ResTempAssembliesBaseDir, buildConfig); - string projectDllSrcPath = Path.Combine(projectDllSrcDir, $"{projectDllName}.dll"); + string publishOutputTempDir = Path.Combine(Path.GetTempPath(), "godot-publish-dotnet", + $"{Process.GetCurrentProcess().Id}-{buildConfig}-{runtimeIdentifier}"); - assemblies[projectDllName] = projectDllSrcPath; + if (!Directory.Exists(publishOutputTempDir)) + Directory.CreateDirectory(publishOutputTempDir); - string bclDir = DeterminePlatformBclDir(platform); + // Execute dotnet publish - if (platform == OS.Platforms.Android) + if (!BuildManager.PublishProjectBlocking(buildConfig, platform, + runtimeIdentifier, publishOutputTempDir)) { - string godotAndroidExtProfileDir = GetBclProfileDir("godot_android_ext"); - string monoAndroidAssemblyPath = Path.Combine(godotAndroidExtProfileDir, "Mono.Android.dll"); - - if (!File.Exists(monoAndroidAssemblyPath)) - throw new FileNotFoundException("Assembly not found: 'Mono.Android'", monoAndroidAssemblyPath); - - assemblies["Mono.Android"] = monoAndroidAssemblyPath; + throw new Exception("Failed to build project"); } - else if (platform == OS.Platforms.HTML5) + + if (!File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpEditor.ProjectAssemblyName}.dll"))) { - // Ideally these would be added automatically since they're referenced by the wasm BCL assemblies. - // However, at least in the case of 'WebAssembly.Net.Http' for some reason the BCL assemblies - // reference a different version even though the assembly is the same, for some weird reason. - - var wasmFrameworkAssemblies = new[] { "WebAssembly.Bindings", "WebAssembly.Net.WebSockets" }; - - foreach (string thisWasmFrameworkAssemblyName in wasmFrameworkAssemblies) - { - string thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName + ".dll"); - if (!File.Exists(thisWasmFrameworkAssemblyPath)) - throw new FileNotFoundException($"Assembly not found: '{thisWasmFrameworkAssemblyName}'", - thisWasmFrameworkAssemblyPath); - assemblies[thisWasmFrameworkAssemblyName] = thisWasmFrameworkAssemblyPath; - } - - // Assemblies that can have a different name in a newer version. Newer version must come first and it has priority. - (string newName, string oldName)[] wasmFrameworkAssembliesOneOf = new[] - { - ("System.Net.Http.WebAssemblyHttpHandler", "WebAssembly.Net.Http") - }; - - foreach (var thisWasmFrameworkAssemblyName in wasmFrameworkAssembliesOneOf) - { - string thisWasmFrameworkAssemblyPath = - Path.Combine(bclDir, thisWasmFrameworkAssemblyName.newName + ".dll"); - if (File.Exists(thisWasmFrameworkAssemblyPath)) - { - assemblies[thisWasmFrameworkAssemblyName.newName] = thisWasmFrameworkAssemblyPath; - } - else - { - thisWasmFrameworkAssemblyPath = - Path.Combine(bclDir, thisWasmFrameworkAssemblyName.oldName + ".dll"); - if (!File.Exists(thisWasmFrameworkAssemblyPath)) - { - throw new FileNotFoundException( - "Expected one of the following assemblies but none were found: " + - $"'{thisWasmFrameworkAssemblyName.newName}' / '{thisWasmFrameworkAssemblyName.oldName}'", - thisWasmFrameworkAssemblyPath); - } - - assemblies[thisWasmFrameworkAssemblyName.oldName] = thisWasmFrameworkAssemblyPath; - } - } + throw new NotSupportedException( + "Publish succeeded but project assembly not found in the output directory"); } - // var initialAssemblies = assemblies.Duplicate(); - // godot_dictionary initialAssembliesAux = ((Godot.Collections.Dictionary)initialAssemblies).NativeValue; - // using godot_string buildConfigAux = Marshaling.ConvertStringToNative(buildConfig); - // using godot_string bclDirAux = Marshaling.ConvertStringToNative(bclDir); - // godot_dictionary assembliesAux = ((Godot.Collections.Dictionary)assemblies).NativeValue; - // TODO - throw new NotImplementedException(); - //internal_GetExportedAssemblyDependencies(initialAssembliesAux, buildConfigAux, bclDirAux, ref assembliesAux); - - AddI18NAssemblies(assemblies, bclDir); - - string outputDataDir = null; + // Copy all files from the dotnet publish output directory to + // a data directory next to the Godot output executable. - if (PlatformHasTemplateDir(platform)) - outputDataDir = ExportDataDirectory(features, platform, isDebug, outputDir); + string outputDataDir = Path.Combine(outputDir, DetermineDataDirNameForProject()); - string apiConfig = isDebug ? "Debug" : "Release"; - // TODO - throw new NotImplementedException(); - string resAssembliesDir = null; // Path.Combine(GodotSharpDirs.ResAssembliesBaseDir, apiConfig); + if (Directory.Exists(outputDataDir)) + Directory.Delete(outputDataDir, recursive: true); // Clean first - bool assembliesInsidePck = (bool)ProjectSettings.GetSetting("mono/export/export_assemblies_inside_pck") || - outputDataDir == null; + Directory.CreateDirectory(outputDataDir); - if (!assembliesInsidePck) + foreach (string dir in Directory.GetDirectories(publishOutputTempDir, "*", SearchOption.AllDirectories)) { - string outputDataGameAssembliesDir = Path.Combine(outputDataDir, "Assemblies"); - if (!Directory.Exists(outputDataGameAssembliesDir)) - Directory.CreateDirectory(outputDataGameAssembliesDir); + Directory.CreateDirectory(Path.Combine(outputDataDir, dir.Substring(publishOutputTempDir.Length + 1))); } - foreach (var assembly in assemblies) + foreach (string file in Directory.GetFiles(publishOutputTempDir, "*", SearchOption.AllDirectories)) { - void AddToAssembliesDir(string fileSrcPath) - { - if (assembliesInsidePck) - { - string fileDstPath = Path.Combine(resAssembliesDir, fileSrcPath.GetFile()); - AddFile(fileSrcPath, fileDstPath); - } - else - { - Debug.Assert(outputDataDir != null); - string fileDstPath = Path.Combine(outputDataDir, "Assemblies", fileSrcPath.GetFile()); - File.Copy(fileSrcPath, fileDstPath); - } - } - - string assemblySrcPath = assembly.Value; - - string assemblyPathWithoutExtension = Path.ChangeExtension(assemblySrcPath, null); - string pdbSrcPath = assemblyPathWithoutExtension + ".pdb"; - - AddToAssembliesDir(assemblySrcPath); - - if (File.Exists(pdbSrcPath)) - AddToAssembliesDir(pdbSrcPath); + File.Copy(file, Path.Combine(outputDataDir, file.Substring(publishOutputTempDir.Length + 1))); } + } - // AOT compilation - bool aotEnabled = platform == OS.Platforms.iOS || - (bool)ProjectSettings.GetSetting("mono/export/aot/enabled"); + private string DetermineRuntimeIdentifierOS(string platform) + => OS.DotNetOSPlatformMap[platform]; - if (aotEnabled) + private string DetermineRuntimeIdentifierArch(string arch) + { + return arch switch { - string aotToolchainPath = null; - - if (platform == OS.Platforms.Android) - aotToolchainPath = (string)ProjectSettings.GetSetting("mono/export/aot/android_toolchain_path"); - - if (aotToolchainPath == string.Empty) - aotToolchainPath = null; // Don't risk it being used as current working dir - - // TODO: LLVM settings are hard-coded and disabled for now - var aotOpts = new AotOptions - { - EnableLLVM = false, - LLVMOnly = false, - LLVMPath = "", - LLVMOutputPath = "", - FullAot = platform == OS.Platforms.iOS || - (bool)(ProjectSettings.GetSetting("mono/export/aot/full_aot") ?? false), - UseInterpreter = (bool)ProjectSettings.GetSetting("mono/export/aot/use_interpreter"), - ExtraAotOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_aot_options") ?? - Array.Empty<string>(), - ExtraOptimizerOptions = - (string[])ProjectSettings.GetSetting("mono/export/aot/extra_optimizer_options") ?? - Array.Empty<string>(), - ToolchainPath = aotToolchainPath - }; - - AotBuilder.CompileAssemblies(this, aotOpts, features, platform, isDebug, bclDir, outputDir, - outputDataDir, assemblies); - } + "x86" => "x86", + "x86_32" => "x86", + "x64" => "x64", + "x86_64" => "x64", + "armeabi-v7a" => "arm", + "arm64-v8a" => "arm64", + "armv7" => "arm", + "arm64" => "arm64", + _ => throw new ArgumentOutOfRangeException(nameof(arch), arch, "Unexpected architecture") + }; } public override void _ExportEnd() @@ -350,70 +201,11 @@ namespace GodotTools.Export } } - [return: NotNull] - private static string ExportDataDirectory(string[] features, string platform, bool isDebug, string outputDir) - { - string target = isDebug ? "release_debug" : "release"; - - // NOTE: Bits is ok for now as all platforms with a data directory only have one or two architectures. - // However, this may change in the future if we add arm linux or windows desktop templates. - string bits = features.Contains("64") ? "64" : "32"; - - string TemplateDirName() => $"data.mono.{platform}.{bits}.{target}"; - - string templateDirPath = Path.Combine(Internal.FullExportTemplatesDir, TemplateDirName()); - bool validTemplatePathFound = true; - - if (!Directory.Exists(templateDirPath)) - { - validTemplatePathFound = false; - - if (isDebug) - { - target = "debug"; // Support both 'release_debug' and 'debug' for the template data directory name - templateDirPath = Path.Combine(Internal.FullExportTemplatesDir, TemplateDirName()); - validTemplatePathFound = true; - - if (!Directory.Exists(templateDirPath)) - validTemplatePathFound = false; - } - } - - if (!validTemplatePathFound) - throw new FileNotFoundException("Data template directory not found", templateDirPath); - - string outputDataDir = Path.Combine(outputDir, DetermineDataDirNameForProject()); - - if (Directory.Exists(outputDataDir)) - Directory.Delete(outputDataDir, recursive: true); // Clean first - - Directory.CreateDirectory(outputDataDir); - - foreach (string dir in Directory.GetDirectories(templateDirPath, "*", SearchOption.AllDirectories)) - { - Directory.CreateDirectory(Path.Combine(outputDataDir, dir.Substring(templateDirPath.Length + 1))); - } - - foreach (string file in Directory.GetFiles(templateDirPath, "*", SearchOption.AllDirectories)) - { - File.Copy(file, Path.Combine(outputDataDir, file.Substring(templateDirPath.Length + 1))); - } - - return outputDataDir; - } - - private static bool PlatformHasTemplateDir(string platform) - { - // macOS export templates are contained in a zip, so we place our custom template inside it and let Godot do the rest. - return !new[] { OS.Platforms.MacOS, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 } - .Contains(platform); - } - private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, out string platform) { foreach (var feature in features) { - if (OS.PlatformNameMap.TryGetValue(feature, out platform)) + if (OS.PlatformFeatureMap.TryGetValue(feature, out platform)) return true; } @@ -421,78 +213,6 @@ namespace GodotTools.Export return false; } - private static string GetBclProfileDir(string profile) - { - string templatesDir = Internal.FullExportTemplatesDir; - return Path.Combine(templatesDir, "bcl", profile); - } - - private static string DeterminePlatformBclDir(string platform) - { - string templatesDir = Internal.FullExportTemplatesDir; - string platformBclDir = Path.Combine(templatesDir, "bcl", platform); - - if (!File.Exists(Path.Combine(platformBclDir, "mscorlib.dll"))) - { - string profile = DeterminePlatformBclProfile(platform); - platformBclDir = Path.Combine(templatesDir, "bcl", profile); - - if (!File.Exists(Path.Combine(platformBclDir, "mscorlib.dll"))) - { - if (PlatformRequiresCustomBcl(platform)) - throw new FileNotFoundException($"Missing BCL (Base Class Library) for platform: {platform}"); - - platformBclDir = typeof(object).Assembly.Location.GetBaseDir(); // Use the one we're running on - } - } - - return platformBclDir; - } - - /// <summary> - /// Determines whether the BCL bundled with the Godot editor can be used for the target platform, - /// or if it requires a custom BCL that must be distributed with the export templates. - /// </summary> - private static bool PlatformRequiresCustomBcl(string platform) - { - if (new[] { OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform)) - return true; - - // The 'net_4_x' BCL is not compatible between Windows and the other platforms. - // We use the names 'net_4_x_win' and 'net_4_x' to differentiate between the two. - - bool isWinOrUwp = new[] - { - OS.Platforms.Windows, - OS.Platforms.UWP - }.Contains(platform); - - return OS.IsWindows ? !isWinOrUwp : isWinOrUwp; - } - - private static string DeterminePlatformBclProfile(string platform) - { - switch (platform) - { - case OS.Platforms.Windows: - case OS.Platforms.UWP: - return "net_4_x_win"; - case OS.Platforms.MacOS: - case OS.Platforms.LinuxBSD: - case OS.Platforms.Server: - case OS.Platforms.Haiku: - return "net_4_x"; - case OS.Platforms.Android: - return "monodroid"; - case OS.Platforms.iOS: - return "monotouch"; - case OS.Platforms.HTML5: - return "wasm"; - default: - throw new NotSupportedException($"Platform not supported: {platform}"); - } - } - private static string DetermineDataDirNameForProject() { string appName = (string)ProjectSettings.GetSetting("application/config/name"); diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 2d85513766..4e27f4ed14 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -13,13 +13,14 @@ using GodotTools.Internals; using GodotTools.ProjectEditor; using JetBrains.Annotations; using static GodotTools.Internals.Globals; +using Environment = System.Environment; using File = GodotTools.Utils.File; using OS = GodotTools.Utils.OS; using Path = System.IO.Path; namespace GodotTools { - public class GodotSharpEditor : EditorPlugin, ISerializationListener + public partial class GodotSharpEditor : EditorPlugin, ISerializationListener { private EditorSettings _editorSettings; @@ -151,13 +152,6 @@ namespace GodotTools Instance.MSBuildPanel.BuildSolution(); } - public override void _Ready() - { - base._Ready(); - - MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged; - } - private enum MenuOptions { CreateSln, @@ -382,12 +376,37 @@ namespace GodotTools { base._EnablePlugin(); - ProjectUtils.MSBuildLocatorRegisterDefaults(); - if (Instance != null) throw new InvalidOperationException(); Instance = this; + var dotNetSdkSearchVersion = Environment.Version; + + // First we try to find the .NET Sdk ourselves to make sure we get the + // correct version first (`RegisterDefaults` always picks the latest). + if (DotNetFinder.TryFindDotNetSdk(dotNetSdkSearchVersion, out var sdkVersion, out string sdkPath)) + { + if (Godot.OS.IsStdoutVerbose()) + Console.WriteLine($"Found .NET Sdk version '{sdkVersion}': {sdkPath}"); + + ProjectUtils.MSBuildLocatorRegisterMSBuildPath(sdkPath); + } + else + { + try + { + ProjectUtils.MSBuildLocatorRegisterDefaults(out sdkVersion, out sdkPath); + if (Godot.OS.IsStdoutVerbose()) + Console.WriteLine($"Found .NET Sdk version '{sdkVersion}': {sdkPath}"); + } + catch (InvalidOperationException e) + { + if (Godot.OS.IsStdoutVerbose()) + GD.PrintErr(e.ToString()); + GD.PushError($".NET Sdk not found. The required version is '{dotNetSdkSearchVersion}'."); + } + } + var editorInterface = GetEditorInterface(); var editorBaseControl = editorInterface.GetBaseControl(); @@ -397,6 +416,8 @@ namespace GodotTools editorBaseControl.AddChild(_errorDialog); MSBuildPanel = new MSBuildPanel(); + MSBuildPanel.Ready += () => + MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged; _bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR()); AddChild(new HotReloadAssemblyWatcher { Name = "HotReloadAssemblyWatcher" }); diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index d0fae02d5d..92a40b9b8b 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -11,6 +11,13 @@ <GodotApiAssembliesDir>$(GodotOutputDataDir)/Api/$(GodotApiConfiguration)</GodotApiAssembliesDir> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> + <!-- Needed for our source generators to work despite this not being a Godot game project --> + <PropertyGroup> + <IsGodotToolsProject>true</IsGodotToolsProject> + </PropertyGroup> + <ItemGroup> + <CompilerVisibleProperty Include="IsGodotToolsProject" /> + </ItemGroup> <PropertyGroup Condition=" Exists('$(GodotApiAssembliesDir)/GodotSharp.dll') "> <!-- The project is part of the Godot source tree --> <!-- Use the Godot source tree output folder instead of '$(ProjectDir)/bin' --> @@ -34,6 +41,9 @@ </Reference> </ItemGroup> <ItemGroup> + <ProjectReference Include="..\..\Godot.NET.Sdk\Godot.SourceGenerators\Godot.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> + </ItemGroup> + <ItemGroup> <ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj" /> <ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" /> <ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj" /> diff --git a/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs b/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs index 414729b18e..260d13a714 100644 --- a/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs +++ b/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs @@ -5,7 +5,7 @@ using static GodotTools.Internals.Globals; namespace GodotTools { - public class HotReloadAssemblyWatcher : Node + public partial class HotReloadAssemblyWatcher : Node { private Timer _watchTimer; diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs index 23339fe50b..9acdeb4183 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs @@ -8,7 +8,7 @@ using GodotTools.Internals; namespace GodotTools.Ides { - public sealed class GodotIdeManager : Node, ISerializationListener + public sealed partial class GodotIdeManager : Node, ISerializationListener { private MessagingServer _messagingServer; diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs index eca7da07c8..63c15e7d28 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs @@ -1,4 +1,3 @@ -using System.Runtime.CompilerServices; using Godot.NativeInterop; namespace GodotTools.Internals @@ -15,16 +14,6 @@ namespace GodotTools.Internals } } - public static string ResTempAssembliesBaseDir - { - get - { - Internal.godot_icall_GodotSharpDirs_ResTempAssembliesBaseDir(out godot_string dest); - using (dest) - return Marshaling.ConvertStringToManaged(dest); - } - } - public static string MonoUserDir { get diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs index 7b39f8ecdb..be53b8f1cf 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Godot; using Godot.NativeInterop; @@ -47,16 +46,6 @@ namespace GodotTools.Internals public static void EditorNodeShowScriptScreen() => godot_icall_Internal_EditorNodeShowScriptScreen(); - public static string MonoWindowsInstallRoot - { - get - { - godot_icall_Internal_MonoWindowsInstallRoot(out godot_string dest); - using (dest) - return Marshaling.ConvertStringToManaged(dest); - } - } - public static void EditorRunPlay() => godot_icall_Internal_EditorRunPlay(); public static void EditorRunStop() => godot_icall_Internal_EditorRunStop(); @@ -64,7 +53,7 @@ namespace GodotTools.Internals public static void ScriptEditorDebugger_ReloadScripts() => godot_icall_Internal_ScriptEditorDebugger_ReloadScripts(); - public static unsafe string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, + public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, string scriptFile) { using godot_string scriptFileIn = Marshaling.ConvertStringToNative(scriptFile); @@ -81,9 +70,6 @@ namespace GodotTools.Internals public static extern void godot_icall_GodotSharpDirs_ResMetadataDir(out godot_string r_dest); [DllImport(GodotDllName)] - public static extern void godot_icall_GodotSharpDirs_ResTempAssembliesBaseDir(out godot_string r_dest); - - [DllImport(GodotDllName)] public static extern void godot_icall_GodotSharpDirs_MonoUserDir(out godot_string r_dest); [DllImport(GodotDllName)] @@ -144,9 +130,6 @@ namespace GodotTools.Internals private static extern void godot_icall_Internal_EditorNodeShowScriptScreen(); [DllImport(GodotDllName)] - private static extern void godot_icall_Internal_MonoWindowsInstallRoot(out godot_string dest); - - [DllImport(GodotDllName)] private static extern void godot_icall_Internal_EditorRunPlay(); [DllImport(GodotDllName)] diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index db77a71c2f..73e3df4d2c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -1,11 +1,12 @@ using Godot.NativeInterop; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; +using System.Text; using GodotTools.Internals; namespace GodotTools.Utils @@ -13,7 +14,7 @@ namespace GodotTools.Utils [SuppressMessage("ReSharper", "InconsistentNaming")] public static class OS { - public static class Names + private static class Names { public const string Windows = "Windows"; public const string MacOS = "macOS"; @@ -42,6 +43,35 @@ namespace GodotTools.Utils public const string HTML5 = "javascript"; } + private static class DotNetOS + { + public const string Win = "win"; + public const string OSX = "osx"; + public const string Linux = "linux"; + public const string Win10 = "win10"; + public const string Android = "android"; + public const string iOS = "ios"; + public const string Browser = "browser"; + } + + public static readonly Dictionary<string, string> PlatformFeatureMap = new Dictionary<string, string>( + // Export `features` may be in lower case + StringComparer.InvariantCultureIgnoreCase + ) + { + ["Windows"] = Platforms.Windows, + ["macOS"] = Platforms.MacOS, + ["LinuxBSD"] = Platforms.LinuxBSD, + // "X11" for compatibility, temporarily, while we are on an outdated branch + ["X11"] = Platforms.LinuxBSD, + ["Server"] = Platforms.Server, + ["UWP"] = Platforms.UWP, + ["Haiku"] = Platforms.Haiku, + ["Android"] = Platforms.Android, + ["iOS"] = Platforms.iOS, + ["HTML5"] = Platforms.HTML5 + }; + public static readonly Dictionary<string, string> PlatformNameMap = new Dictionary<string, string> { [Names.Windows] = Platforms.Windows, @@ -58,7 +88,23 @@ namespace GodotTools.Utils [Names.HTML5] = Platforms.HTML5 }; - private static unsafe bool IsOS(string name) + public static readonly Dictionary<string, string> DotNetOSPlatformMap = new Dictionary<string, string> + { + [Platforms.Windows] = DotNetOS.Win, + [Platforms.MacOS] = DotNetOS.OSX, + // TODO: + // Does .NET 6 support BSD variants? If it does, it may need the name `unix` + // instead of `linux` in the runtime identifier. This would be a problem as + // Godot has a single export profile for both, named LinuxBSD. + [Platforms.LinuxBSD] = DotNetOS.Linux, + [Platforms.Server] = DotNetOS.Linux, + [Platforms.UWP] = DotNetOS.Win10, + [Platforms.Android] = DotNetOS.Android, + [Platforms.iOS] = DotNetOS.iOS, + [Platforms.HTML5] = DotNetOS.Browser + }; + + private static bool IsOS(string name) { Internal.godot_icall_Utils_OS_GetPlatformName(out godot_string dest); using (dest) @@ -68,7 +114,7 @@ namespace GodotTools.Utils } } - private static unsafe bool IsAnyOS(IEnumerable<string> names) + private static bool IsAnyOS(IEnumerable<string> names) { Internal.godot_icall_Utils_OS_GetPlatformName(out godot_string dest); using (dest) @@ -99,25 +145,34 @@ namespace GodotTools.Utils // TODO SupportedOSPlatformGuard once we target .NET 6 // [SupportedOSPlatformGuard("windows")] public static bool IsWindows => _isWindows.Value || IsUWP; + // [SupportedOSPlatformGuard("osx")] public static bool IsMacOS => _isMacOS.Value; + // [SupportedOSPlatformGuard("linux")] public static bool IsLinuxBSD => _isLinuxBSD.Value; + // [SupportedOSPlatformGuard("linux")] public static bool IsServer => _isServer.Value; + // [SupportedOSPlatformGuard("windows")] public static bool IsUWP => _isUWP.Value; + public static bool IsHaiku => _isHaiku.Value; + // [SupportedOSPlatformGuard("android")] public static bool IsAndroid => _isAndroid.Value; + // [SupportedOSPlatformGuard("ios")] public static bool IsiOS => _isiOS.Value; + // [SupportedOSPlatformGuard("browser")] public static bool IsHTML5 => _isHTML5.Value; public static bool IsUnixLike => _isUnixLike.Value; public static char PathSep => IsWindows ? ';' : ':'; + [return: MaybeNull] public static string PathWhich([NotNull] string name) { if (IsWindows) @@ -126,6 +181,7 @@ namespace GodotTools.Utils return PathWhichUnix(name); } + [return: MaybeNull] private static string PathWhichWindows([NotNull] string name) { string[] windowsExts = @@ -162,6 +218,7 @@ namespace GodotTools.Utils select path + ext).FirstOrDefault(File.Exists); } + [return: MaybeNull] private static string PathWhichUnix([NotNull] string name) { string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); @@ -192,14 +249,7 @@ namespace GodotTools.Utils public static void RunProcess(string command, IEnumerable<string> arguments) { - // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead - string CmdLineArgsToString(IEnumerable<string> args) - { - // Not perfect, but as long as we are careful... - return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg)); - } - - var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments)) + var startInfo = new ProcessStartInfo(command) { RedirectStandardOutput = true, RedirectStandardError = true, @@ -207,44 +257,104 @@ namespace GodotTools.Utils CreateNoWindow = true }; - using (Process process = Process.Start(startInfo)) - { - if (process == null) - throw new Exception("No process was started"); + foreach (string arg in arguments) + startInfo.ArgumentList.Add(arg); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - if (IsWindows && process.Id > 0) - User32Dll.AllowSetForegroundWindow(process.Id); // allows application to focus itself - } + using Process process = Process.Start(startInfo); + + if (process == null) + throw new Exception("No process was started"); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + if (IsWindows && process.Id > 0) + User32Dll.AllowSetForegroundWindow(process.Id); // Allows application to focus itself } public static int ExecuteCommand(string command, IEnumerable<string> arguments) { - // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead - string CmdLineArgsToString(IEnumerable<string> args) + var startInfo = new ProcessStartInfo(command) { - // Not perfect, but as long as we are careful... - return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg)); - } + // Print the output + RedirectStandardOutput = false, + RedirectStandardError = false, + UseShellExecute = false + }; + + foreach (string arg in arguments) + startInfo.ArgumentList.Add(arg); + + Console.WriteLine(startInfo.GetCommandLineDisplay(new StringBuilder("Executing: ")).ToString()); - var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments)); + using var process = new Process { StartInfo = startInfo }; + process.Start(); + process.WaitForExit(); - Console.WriteLine($"Executing: \"{startInfo.FileName}\" {startInfo.Arguments}"); + return process.ExitCode; + } + + private static void AppendProcessFileNameForDisplay(this StringBuilder builder, string fileName) + { + if (builder.Length > 0) + builder.Append(' '); - // Print the output - startInfo.RedirectStandardOutput = false; - startInfo.RedirectStandardError = false; + if (fileName.Contains(' ')) + { + builder.Append('"'); + builder.Append(fileName); + builder.Append('"'); + } + else + { + builder.Append(fileName); + } + } - startInfo.UseShellExecute = false; + private static void AppendProcessArgumentsForDisplay(this StringBuilder builder, + Collection<string> argumentList) + { + // This is intended just for reading. It doesn't need to be a valid command line. + // E.g.: We don't handle escaping of quotes. - using (var process = new Process { StartInfo = startInfo }) + foreach (string argument in argumentList) { - process.Start(); - process.WaitForExit(); + if (builder.Length > 0) + builder.Append(' '); - return process.ExitCode; + if (argument.Contains(' ')) + { + builder.Append('"'); + builder.Append(argument); + builder.Append('"'); + } + else + { + builder.Append(argument); + } } } + + public static StringBuilder GetCommandLineDisplay( + this ProcessStartInfo startInfo, + StringBuilder optionalBuilder = null + ) + { + var builder = optionalBuilder ?? new StringBuilder(); + + builder.AppendProcessFileNameForDisplay(startInfo.FileName); + + if (startInfo.ArgumentList.Count == 0) + { + builder.Append(' '); + builder.Append(startInfo.Arguments); + } + else + { + builder.AppendProcessArgumentsForDisplay(startInfo.ArgumentList); + } + + return builder; + } } } |