diff options
Diffstat (limited to 'modules/mono/editor/GodotTools')
36 files changed, 1499 insertions, 1595 deletions
diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs index c2549b4ad5..9b7b422276 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs @@ -15,14 +15,14 @@ namespace GodotTools.BuildLogger public void Initialize(IEventSource eventSource) { if (null == Parameters) - throw new LoggerException("Log directory was not set."); + throw new LoggerException("Log directory parameter not specified."); var parameters = Parameters.Split(new[] { ';' }); string logDir = parameters[0]; if (string.IsNullOrEmpty(logDir)) - throw new LoggerException("Log directory was not set."); + throw new LoggerException("Log directory parameter is empty."); if (parameters.Length > 1) throw new LoggerException("Too many parameters passed."); @@ -51,36 +51,46 @@ namespace GodotTools.BuildLogger { throw new LoggerException("Failed to create log file: " + ex.Message); } - else - { - // Unexpected failure - throw; - } + + // Unexpected failure + throw; } eventSource.ProjectStarted += eventSource_ProjectStarted; - eventSource.TaskStarted += eventSource_TaskStarted; + eventSource.ProjectFinished += eventSource_ProjectFinished; eventSource.MessageRaised += eventSource_MessageRaised; eventSource.WarningRaised += eventSource_WarningRaised; eventSource.ErrorRaised += eventSource_ErrorRaised; - eventSource.ProjectFinished += eventSource_ProjectFinished; } - void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e) + private void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e) + { + WriteLine(e.Message); + indent++; + } + + private void eventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e) + { + indent--; + WriteLine(e.Message); + } + + private void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e) { string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): error {e.Code}: {e.Message}"; - if (e.ProjectFile.Length > 0) + if (!string.IsNullOrEmpty(e.ProjectFile)) line += $" [{e.ProjectFile}]"; WriteLine(line); string errorLine = $@"error,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," + - $@"{e.Code.CsvEscape()},{e.Message.CsvEscape()},{e.ProjectFile.CsvEscape()}"; + $"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," + + $"{e.ProjectFile?.CsvEscape() ?? string.Empty}"; issuesStreamWriter.WriteLine(errorLine); } - void eventSource_WarningRaised(object sender, BuildWarningEventArgs e) + private void eventSource_WarningRaised(object sender, BuildWarningEventArgs e) { string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): warning {e.Code}: {e.Message}"; @@ -89,8 +99,9 @@ namespace GodotTools.BuildLogger WriteLine(line); - string warningLine = $@"warning,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber},{e.Code.CsvEscape()}," + - $@"{e.Message.CsvEscape()},{(e.ProjectFile != null ? e.ProjectFile.CsvEscape() : string.Empty)}"; + string warningLine = $@"warning,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," + + $"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," + + $"{e.ProjectFile?.CsvEscape() ?? string.Empty}"; issuesStreamWriter.WriteLine(warningLine); } @@ -106,40 +117,6 @@ namespace GodotTools.BuildLogger } } - private void eventSource_TaskStarted(object sender, TaskStartedEventArgs e) - { - // TaskStartedEventArgs adds ProjectFile, TaskFile, TaskName - // To keep this log clean, this logger will ignore these events. - } - - private void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e) - { - WriteLine(e.Message); - indent++; - } - - private void eventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e) - { - indent--; - WriteLine(e.Message); - } - - /// <summary> - /// Write a line to the log, adding the SenderName - /// </summary> - private void WriteLineWithSender(string line, BuildEventArgs e) - { - if (0 == string.Compare(e.SenderName, "MSBuild", StringComparison.OrdinalIgnoreCase)) - { - // Well, if the sender name is MSBuild, let's leave it out for prettiness - WriteLine(line); - } - else - { - WriteLine(e.SenderName + ": " + line); - } - } - /// <summary> /// Write a line to the log, adding the SenderName and Message /// (these parameters are on all MSBuild event argument objects) diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs b/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs index 85760a3705..e1ccf0454a 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs @@ -19,7 +19,10 @@ namespace GodotTools.Core } if (attempt > maxAttempts + 1) - return; + { + // Overwrite the oldest one + backupPath = backupPathBase; + } File.Copy(filePath, backupPath, overwrite: true); } diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs index 012b69032e..b217ae4bf7 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; namespace GodotTools.Core { @@ -14,14 +15,18 @@ namespace GodotTools.Core if (Path.DirectorySeparatorChar == '\\') dir = dir.Replace("/", "\\") + "\\"; - Uri fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute); - Uri relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute); + var fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute); + var relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute); - return relRoot.MakeRelativeUri(fullPath).ToString(); + // MakeRelativeUri converts spaces to %20, hence why we need UnescapeDataString + return Uri.UnescapeDataString(relRoot.MakeRelativeUri(fullPath).ToString()); } public static string NormalizePath(this string path) { + if (string.IsNullOrEmpty(path)) + return path; + bool rooted = path.IsAbsolutePath(); path = path.Replace('\\', '/'); @@ -31,7 +36,17 @@ namespace GodotTools.Core path = string.Join(Path.DirectorySeparatorChar.ToString(), parts).Trim(); - return rooted ? Path.DirectorySeparatorChar + path : path; + if (!rooted) + return path; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + string maybeDrive = parts[0]; + if (maybeDrive.Length == 2 && maybeDrive[1] == ':') + return path; // Already has drive letter + } + + return Path.DirectorySeparatorChar + path; } private static readonly string DriveRoot = Path.GetPathRoot(Environment.CurrentDirectory); @@ -43,9 +58,9 @@ namespace GodotTools.Core path.StartsWith(DriveRoot, StringComparison.Ordinal); } - public static string ToSafeDirName(this string dirName, bool allowDirSeparator) + 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/Program.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs index 99a55c471b..4db71500da 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Reflection; diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs index 572c541412..0f50c90531 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs @@ -121,7 +121,7 @@ namespace GodotTools.IdeMessaging this.messageHandler = messageHandler; this.logger = logger; - string projectMetadataDir = Path.Combine(godotProjectDir, ".mono", "metadata"); + string projectMetadataDir = Path.Combine(godotProjectDir, ".godot", "mono", "metadata"); MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName); diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs index affb2a47e7..ce2b378623 100644 --- a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs index 6f318aab4a..cc0da44a13 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs @@ -2,6 +2,7 @@ using GodotTools.Core; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Text.RegularExpressions; namespace GodotTools.ProjectEditor @@ -88,7 +89,7 @@ namespace GodotTools.ProjectEditor string solutionPath = Path.Combine(DirectoryPath, Name + ".sln"); string content = string.Format(SolutionTemplate, projectsDecl, slnPlatformsCfg, projPlatformsCfg); - File.WriteAllText(solutionPath, content); + File.WriteAllText(solutionPath, content, Encoding.UTF8); // UTF-8 with BOM } public DotNetSolution(string name) diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index 9cb50014b0..37123ba2b2 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -10,14 +10,23 @@ </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> - <!-- - 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. - --> <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/IdentifierUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs index f93eb9a1fa..ed77076df3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs @@ -22,6 +22,37 @@ namespace GodotTools.ProjectEditor return string.Join(".", identifiers); } + /// <summary> + /// Skips invalid identifier characters including decimal digit numbers at the start of the identifier. + /// </summary> + private static void SkipInvalidCharacters(string source, int startIndex, StringBuilder outputBuilder) + { + for (int i = startIndex; i < source.Length; i++) + { + char @char = source[i]; + + switch (char.GetUnicodeCategory(@char)) + { + case UnicodeCategory.UppercaseLetter: + case UnicodeCategory.LowercaseLetter: + case UnicodeCategory.TitlecaseLetter: + case UnicodeCategory.ModifierLetter: + case UnicodeCategory.LetterNumber: + case UnicodeCategory.OtherLetter: + outputBuilder.Append(@char); + break; + case UnicodeCategory.NonSpacingMark: + case UnicodeCategory.SpacingCombiningMark: + case UnicodeCategory.ConnectorPunctuation: + case UnicodeCategory.DecimalDigitNumber: + // Identifiers may start with underscore + if (outputBuilder.Length > startIndex || @char == '_') + outputBuilder.Append(@char); + break; + } + } + } + public static string SanitizeIdentifier(string identifier, bool allowEmpty) { if (string.IsNullOrEmpty(identifier)) @@ -44,30 +75,7 @@ namespace GodotTools.ProjectEditor startIndex += 1; } - for (int i = startIndex; i < identifier.Length; i++) - { - char @char = identifier[i]; - - switch (Char.GetUnicodeCategory(@char)) - { - case UnicodeCategory.UppercaseLetter: - case UnicodeCategory.LowercaseLetter: - case UnicodeCategory.TitlecaseLetter: - case UnicodeCategory.ModifierLetter: - case UnicodeCategory.LetterNumber: - case UnicodeCategory.OtherLetter: - identifierBuilder.Append(@char); - break; - case UnicodeCategory.NonSpacingMark: - case UnicodeCategory.SpacingCombiningMark: - case UnicodeCategory.ConnectorPunctuation: - case UnicodeCategory.DecimalDigitNumber: - // Identifiers may start with underscore - if (identifierBuilder.Length > startIndex || @char == '_') - identifierBuilder.Append(@char); - break; - } - } + SkipInvalidCharacters(identifier, startIndex, identifierBuilder); if (identifierBuilder.Length == startIndex) { diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs deleted file mode 100644 index 704f2ec194..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs +++ /dev/null @@ -1,118 +0,0 @@ -using GodotTools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.Build.Construction; -using Microsoft.Build.Globbing; - -namespace GodotTools.ProjectEditor -{ - public static class ProjectExtensions - { - public static ProjectItemElement FindItemOrNull(this ProjectRootElement root, string itemType, string include, bool noCondition = false) - { - string normalizedInclude = include.NormalizePath(); - - foreach (var itemGroup in root.ItemGroups) - { - if (noCondition && itemGroup.Condition.Length != 0) - continue; - - foreach (var item in itemGroup.Items) - { - if (item.ItemType != itemType) - continue; - - //var glob = Glob.Parse(item.Include.NormalizePath(), globOptions); - var glob = MSBuildGlob.Parse(item.Include.NormalizePath()); - - if (glob.IsMatch(normalizedInclude)) - return item; - } - } - - return null; - } - public static ProjectItemElement FindItemOrNullAbs(this ProjectRootElement root, string itemType, string include, bool noCondition = false) - { - string normalizedInclude = Path.GetFullPath(include).NormalizePath(); - - foreach (var itemGroup in root.ItemGroups) - { - if (noCondition && itemGroup.Condition.Length != 0) - continue; - - foreach (var item in itemGroup.Items) - { - if (item.ItemType != itemType) - continue; - - var glob = MSBuildGlob.Parse(Path.GetFullPath(item.Include).NormalizePath()); - - if (glob.IsMatch(normalizedInclude)) - return item; - } - } - - return null; - } - - public static IEnumerable<ProjectItemElement> FindAllItemsInFolder(this ProjectRootElement root, string itemType, string folder) - { - string absFolderNormalizedWithSep = Path.GetFullPath(folder).NormalizePath() + Path.DirectorySeparatorChar; - - foreach (var itemGroup in root.ItemGroups) - { - foreach (var item in itemGroup.Items) - { - if (item.ItemType != itemType) - continue; - - string absPathNormalized = Path.GetFullPath(item.Include).NormalizePath(); - - if (absPathNormalized.StartsWith(absFolderNormalizedWithSep)) - yield return item; - } - } - } - - public static bool HasItem(this ProjectRootElement root, string itemType, string include, bool noCondition = false) - { - return root.FindItemOrNull(itemType, include, noCondition) != null; - } - - public static bool AddItemChecked(this ProjectRootElement root, string itemType, string include) - { - if (!root.HasItem(itemType, include, noCondition: true)) - { - root.AddItem(itemType, include); - return true; - } - - return false; - } - - public static bool RemoveItemChecked(this ProjectRootElement root, string itemType, string include) - { - var item = root.FindItemOrNullAbs(itemType, include); - if (item != null) - { - item.Parent.RemoveChild(item); - return true; - } - - return false; - } - - public static Guid GetGuid(this ProjectRootElement root) - { - foreach (var property in root.Properties) - { - if (property.Name == "ProjectGuid") - return Guid.Parse(property.Value); - } - - return Guid.Empty; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index 679d5bb444..7d49d251dd 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -1,174 +1,50 @@ -using GodotTools.Core; using System; -using System.Collections.Generic; using System.IO; -using System.Reflection; +using System.Text; using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using GodotTools.Shared; namespace GodotTools.ProjectEditor { public static class ProjectGenerator { - private const string CoreApiProjectName = "GodotSharp"; - private const string EditorApiProjectName = "GodotSharpEditor"; + public static string GodotSdkAttrValue => $"Godot.NET.Sdk/{GeneratedGodotNupkgsVersions.GodotNETSdk}"; - public const string CSharpProjectTypeGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; - public const string GodotProjectTypeGuid = "{8F3E2DF0-C35C-4265-82FC-BEA011F4A7ED}"; - - public static readonly string GodotDefaultProjectTypeGuids = $"{GodotProjectTypeGuid};{CSharpProjectTypeGuid}"; - - public static string GenGameProject(string dir, string name, IEnumerable<string> compileItems) - { - string path = Path.Combine(dir, name + ".csproj"); - - ProjectPropertyGroupElement mainGroup; - var root = CreateLibraryProject(name, "Debug", out mainGroup); - - mainGroup.SetProperty("ProjectTypeGuids", GodotDefaultProjectTypeGuids); - mainGroup.SetProperty("OutputPath", Path.Combine(".mono", "temp", "bin", "$(Configuration)")); - mainGroup.SetProperty("BaseIntermediateOutputPath", Path.Combine(".mono", "temp", "obj")); - mainGroup.SetProperty("IntermediateOutputPath", Path.Combine("$(BaseIntermediateOutputPath)", "$(Configuration)")); - mainGroup.SetProperty("ApiConfiguration", "Debug").Condition = " '$(Configuration)' != 'ExportRelease' "; - mainGroup.SetProperty("ApiConfiguration", "Release").Condition = " '$(Configuration)' == 'ExportRelease' "; - - var debugGroup = root.AddPropertyGroup(); - debugGroup.Condition = " '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "; - debugGroup.AddProperty("DebugSymbols", "true"); - debugGroup.AddProperty("DebugType", "portable"); - debugGroup.AddProperty("Optimize", "false"); - debugGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;DEBUG;TOOLS;"); - debugGroup.AddProperty("ErrorReport", "prompt"); - debugGroup.AddProperty("WarningLevel", "4"); - debugGroup.AddProperty("ConsolePause", "false"); - - var coreApiRef = root.AddItem("Reference", CoreApiProjectName); - coreApiRef.AddMetadata("HintPath", Path.Combine("$(ProjectDir)", ".mono", "assemblies", "$(ApiConfiguration)", CoreApiProjectName + ".dll")); - coreApiRef.AddMetadata("Private", "False"); - - var editorApiRef = root.AddItem("Reference", EditorApiProjectName); - editorApiRef.Condition = " '$(Configuration)' == 'Debug' "; - editorApiRef.AddMetadata("HintPath", Path.Combine("$(ProjectDir)", ".mono", "assemblies", "$(ApiConfiguration)", EditorApiProjectName + ".dll")); - editorApiRef.AddMetadata("Private", "False"); - - GenAssemblyInfoFile(root, dir, name); - - foreach (var item in compileItems) - { - root.AddItem("Compile", item.RelativeToPath(dir).Replace("/", "\\")); - } - - root.Save(path); - - return root.GetGuid().ToString().ToUpper(); - } - - private static void GenAssemblyInfoFile(ProjectRootElement root, string dir, string name, string[] assemblyLines = null, string[] usingDirectives = null) + public static ProjectRootElement GenGameProject(string name) { - string propertiesDir = Path.Combine(dir, "Properties"); - if (!Directory.Exists(propertiesDir)) - Directory.CreateDirectory(propertiesDir); - - string usingDirectivesText = string.Empty; + if (name.Length == 0) + throw new ArgumentException("Project name is empty", nameof(name)); - if (usingDirectives != null) - { - foreach (var usingDirective in usingDirectives) - usingDirectivesText += "\nusing " + usingDirective + ";"; - } + var root = ProjectRootElement.Create(NewProjectFileOptions.None); - string assemblyLinesText = string.Empty; + root.Sdk = GodotSdkAttrValue; - if (assemblyLines != null) - assemblyLinesText += string.Join("\n", assemblyLines) + "\n"; + var mainGroup = root.AddPropertyGroup(); + mainGroup.AddProperty("TargetFramework", "netstandard2.1"); - string content = string.Format(AssemblyInfoTemplate, usingDirectivesText, name, assemblyLinesText); + string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true); - string assemblyInfoFile = Path.Combine(propertiesDir, "AssemblyInfo.cs"); + // If the name is not a valid namespace, manually set RootNamespace to a sanitized one. + if (sanitizedName != name) + mainGroup.AddProperty("RootNamespace", sanitizedName); - File.WriteAllText(assemblyInfoFile, content); - - root.AddItem("Compile", assemblyInfoFile.RelativeToPath(dir).Replace("/", "\\")); + return root; } - public static ProjectRootElement CreateLibraryProject(string name, string defaultConfig, out ProjectPropertyGroupElement mainGroup) + public static string GenAndSaveGameProject(string dir, string name) { - if (string.IsNullOrEmpty(name)) - throw new ArgumentException($"{nameof(name)} cannot be empty", nameof(name)); - - var root = ProjectRootElement.Create(); - root.DefaultTargets = "Build"; - - mainGroup = root.AddPropertyGroup(); - mainGroup.AddProperty("Configuration", defaultConfig).Condition = " '$(Configuration)' == '' "; - mainGroup.AddProperty("Platform", "AnyCPU").Condition = " '$(Platform)' == '' "; - mainGroup.AddProperty("ProjectGuid", "{" + Guid.NewGuid().ToString().ToUpper() + "}"); - mainGroup.AddProperty("OutputType", "Library"); - mainGroup.AddProperty("OutputPath", Path.Combine("bin", "$(Configuration)")); - mainGroup.AddProperty("RootNamespace", IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true)); - mainGroup.AddProperty("AssemblyName", name); - mainGroup.AddProperty("TargetFrameworkVersion", "v4.7"); - mainGroup.AddProperty("GodotProjectGeneratorVersion", Assembly.GetExecutingAssembly().GetName().Version.ToString()); + if (name.Length == 0) + throw new ArgumentException("Project name is empty", nameof(name)); - var exportDebugGroup = root.AddPropertyGroup(); - exportDebugGroup.Condition = " '$(Configuration)|$(Platform)' == 'ExportDebug|AnyCPU' "; - exportDebugGroup.AddProperty("DebugSymbols", "true"); - exportDebugGroup.AddProperty("DebugType", "portable"); - exportDebugGroup.AddProperty("Optimize", "false"); - exportDebugGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;DEBUG;"); - exportDebugGroup.AddProperty("ErrorReport", "prompt"); - exportDebugGroup.AddProperty("WarningLevel", "4"); - exportDebugGroup.AddProperty("ConsolePause", "false"); - - var exportReleaseGroup = root.AddPropertyGroup(); - exportReleaseGroup.Condition = " '$(Configuration)|$(Platform)' == 'ExportRelease|AnyCPU' "; - exportReleaseGroup.AddProperty("DebugType", "portable"); - exportReleaseGroup.AddProperty("Optimize", "true"); - exportReleaseGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;"); - exportReleaseGroup.AddProperty("ErrorReport", "prompt"); - exportReleaseGroup.AddProperty("WarningLevel", "4"); - exportReleaseGroup.AddProperty("ConsolePause", "false"); - - // References - var referenceGroup = root.AddItemGroup(); - referenceGroup.AddItem("Reference", "System"); - var frameworkRefAssembliesItem = referenceGroup.AddItem("PackageReference", "Microsoft.NETFramework.ReferenceAssemblies"); + string path = Path.Combine(dir, name + ".csproj"); - // Use metadata (child nodes) instead of attributes for the PackageReference. - // This is for compatibility with 3.2, where GodotTools uses an old Microsoft.Build. - frameworkRefAssembliesItem.AddMetadata("Version", "1.0.0"); - frameworkRefAssembliesItem.AddMetadata("PrivateAssets", "All"); + var root = GenGameProject(name); - root.AddImport(Path.Combine("$(MSBuildBinPath)", "Microsoft.CSharp.targets").Replace("/", "\\")); + // Save (without BOM) + root.Save(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); - return root; + return Guid.NewGuid().ToString().ToUpper(); } - - private const string AssemblyInfoTemplate = - @"using System.Reflection;{0} - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle(""{1}"")] -[assembly: AssemblyDescription("""")] -[assembly: AssemblyConfiguration("""")] -[assembly: AssemblyCompany("""")] -[assembly: AssemblyProduct("""")] -[assembly: AssemblyCopyright("""")] -[assembly: AssemblyTrademark("""")] -[assembly: AssemblyCulture("""")] - -// The assembly version has the format ""{{Major}}.{{Minor}}.{{Build}}.{{Revision}}"". -// The form ""{{Major}}.{{Minor}}.*"" will automatically update the build and revision, -// and ""{{Major}}.{{Minor}}.{{Build}}.*"" will update just the revision. - -[assembly: AssemblyVersion(""1.0.*"")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("""")] -{2}"; } } diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index 8774b4ee31..4e2c0f17cc 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -1,9 +1,9 @@ +using System; using GodotTools.Core; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; using Microsoft.Build.Construction; using Microsoft.Build.Globbing; @@ -11,7 +11,7 @@ namespace GodotTools.ProjectEditor { public sealed class MSBuildProject { - public ProjectRootElement Root { get; } + internal ProjectRootElement Root { get; set; } public bool HasUnsavedChanges { get; set; } @@ -31,91 +31,7 @@ namespace GodotTools.ProjectEditor return root != null ? new MSBuildProject(root) : null; } - public static void AddItemToProjectChecked(string projectPath, string itemType, string include) - { - var dir = Directory.GetParent(projectPath).FullName; - var root = ProjectRootElement.Open(projectPath); - Debug.Assert(root != null); - - var normalizedInclude = include.RelativeToPath(dir).Replace("/", "\\"); - - if (root.AddItemChecked(itemType, normalizedInclude)) - root.Save(); - } - - public static void RenameItemInProjectChecked(string projectPath, string itemType, string oldInclude, string newInclude) - { - var dir = Directory.GetParent(projectPath).FullName; - var root = ProjectRootElement.Open(projectPath); - Debug.Assert(root != null); - - var normalizedOldInclude = oldInclude.NormalizePath(); - var normalizedNewInclude = newInclude.NormalizePath(); - - var item = root.FindItemOrNullAbs(itemType, normalizedOldInclude); - - if (item == null) - return; - - item.Include = normalizedNewInclude.RelativeToPath(dir).Replace("/", "\\"); - root.Save(); - } - - public static void RemoveItemFromProjectChecked(string projectPath, string itemType, string include) - { - var root = ProjectRootElement.Open(projectPath); - Debug.Assert(root != null); - - var normalizedInclude = include.NormalizePath(); - - if (root.RemoveItemChecked(itemType, normalizedInclude)) - root.Save(); - } - - public static void RenameItemsToNewFolderInProjectChecked(string projectPath, string itemType, string oldFolder, string newFolder) - { - var dir = Directory.GetParent(projectPath).FullName; - var root = ProjectRootElement.Open(projectPath); - Debug.Assert(root != null); - - bool dirty = false; - - var oldFolderNormalized = oldFolder.NormalizePath(); - var newFolderNormalized = newFolder.NormalizePath(); - string absOldFolderNormalized = Path.GetFullPath(oldFolderNormalized).NormalizePath(); - string absNewFolderNormalized = Path.GetFullPath(newFolderNormalized).NormalizePath(); - - foreach (var item in root.FindAllItemsInFolder(itemType, oldFolderNormalized)) - { - string absPathNormalized = Path.GetFullPath(item.Include).NormalizePath(); - string absNewIncludeNormalized = absNewFolderNormalized + absPathNormalized.Substring(absOldFolderNormalized.Length); - item.Include = absNewIncludeNormalized.RelativeToPath(dir).Replace("/", "\\"); - dirty = true; - } - - if (dirty) - root.Save(); - } - - public static void RemoveItemsInFolderFromProjectChecked(string projectPath, string itemType, string folder) - { - var root = ProjectRootElement.Open(projectPath); - Debug.Assert(root != null); - - var folderNormalized = folder.NormalizePath(); - - var itemsToRemove = root.FindAllItemsInFolder(itemType, folderNormalized).ToList(); - - if (itemsToRemove.Count > 0) - { - foreach (var item in itemsToRemove) - item.Parent.RemoveChild(item); - - root.Save(); - } - } - - private static string[] GetAllFilesRecursive(string rootDirectory, string mask) + private static List<string> GetAllFilesRecursive(string rootDirectory, string mask) { string[] files = Directory.GetFiles(rootDirectory, mask, SearchOption.AllDirectories); @@ -125,262 +41,58 @@ namespace GodotTools.ProjectEditor files[i] = files[i].RelativeToPath(rootDirectory); } - return files; + return new List<string>(files); } - public static string[] GetIncludeFiles(string projectPath, string itemType) + // NOTE: Assumes auto-including items. Only used by the scripts metadata generator, which will be replaced with source generators in the future. + public static IEnumerable<string> GetIncludeFiles(string projectPath, string itemType) { - var result = new List<string>(); - var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs"); + var excluded = new List<string>(); + var includedFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs"); var root = ProjectRootElement.Open(projectPath); Debug.Assert(root != null); - foreach (var itemGroup in root.ItemGroups) + foreach (var item in root.Items) { - if (itemGroup.Condition.Length != 0) + if (string.IsNullOrEmpty(item.Condition)) continue; - foreach (var item in itemGroup.Items) - { - if (item.ItemType != itemType) - continue; - - string normalizedInclude = item.Include.NormalizePath(); - - var glob = MSBuildGlob.Parse(normalizedInclude); + if (item.ItemType != itemType) + continue; - // TODO Check somehow if path has no blob to avoid the following loop... + string normalizedRemove = item.Remove.NormalizePath(); - foreach (var existingFile in existingFiles) - { - if (glob.IsMatch(existingFile)) - { - result.Add(existingFile); - } - } - } + var glob = MSBuildGlob.Parse(normalizedRemove); + excluded.AddRange(includedFiles.Where(includedFile => glob.IsMatch(includedFile))); } - return result.ToArray(); + includedFiles.RemoveAll(f => excluded.Contains(f)); + + return includedFiles; } - public static void EnsureHasProjectTypeGuids(MSBuildProject project) + public static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName) { - var root = project.Root; + var origRoot = project.Root; - bool found = root.PropertyGroups.Any(pg => - string.IsNullOrEmpty(pg.Condition) && pg.Properties.Any(p => p.Name == "ProjectTypeGuids")); - - if (found) + if (!string.IsNullOrEmpty(origRoot.Sdk)) return; - root.AddProperty("ProjectTypeGuids", ProjectGenerator.GodotDefaultProjectTypeGuids); - + project.Root = ProjectGenerator.GenGameProject(projectName); + project.Root.FullPath = origRoot.FullPath; project.HasUnsavedChanges = true; } - /// Simple function to make sure the Api assembly references are configured correctly - public static void FixApiHintPath(MSBuildProject project) - { - var root = project.Root; - - void AddPropertyIfNotPresent(string name, string condition, string value) - { - if (root.PropertyGroups - .Any(g => (string.IsNullOrEmpty(g.Condition) || g.Condition.Trim() == condition) && - g.Properties - .Any(p => p.Name == name && - p.Value == value && - (p.Condition.Trim() == condition || g.Condition.Trim() == condition)))) - { - return; - } - - root.AddProperty(name, value).Condition = " " + condition + " "; - project.HasUnsavedChanges = true; - } - - AddPropertyIfNotPresent(name: "ApiConfiguration", - condition: "'$(Configuration)' != 'ExportRelease'", - value: "Debug"); - AddPropertyIfNotPresent(name: "ApiConfiguration", - condition: "'$(Configuration)' == 'ExportRelease'", - value: "Release"); - - void SetReferenceHintPath(string referenceName, string condition, string hintPath) - { - foreach (var itemGroup in root.ItemGroups.Where(g => - g.Condition.Trim() == string.Empty || g.Condition.Trim() == condition)) - { - var references = itemGroup.Items.Where(item => - item.ItemType == "Reference" && - item.Include == referenceName && - (item.Condition.Trim() == condition || itemGroup.Condition.Trim() == condition)); - - var referencesWithHintPath = references.Where(reference => - reference.Metadata.Any(m => m.Name == "HintPath")); - - if (referencesWithHintPath.Any(reference => reference.Metadata - .Any(m => m.Name == "HintPath" && m.Value == hintPath))) - { - // Found a Reference item with the right HintPath - return; - } - - var referenceWithHintPath = referencesWithHintPath.FirstOrDefault(); - if (referenceWithHintPath != null) - { - // Found a Reference item with a wrong HintPath - foreach (var metadata in referenceWithHintPath.Metadata.ToList() - .Where(m => m.Name == "HintPath")) - { - // Safe to remove as we duplicate with ToList() to loop - referenceWithHintPath.RemoveChild(metadata); - } - - referenceWithHintPath.AddMetadata("HintPath", hintPath); - project.HasUnsavedChanges = true; - return; - } - - var referenceWithoutHintPath = references.FirstOrDefault(); - if (referenceWithoutHintPath != null) - { - // Found a Reference item without a HintPath - referenceWithoutHintPath.AddMetadata("HintPath", hintPath); - project.HasUnsavedChanges = true; - return; - } - } - - // Found no Reference item at all. Add it. - root.AddItem("Reference", referenceName).Condition = " " + condition + " "; - project.HasUnsavedChanges = true; - } - - const string coreProjectName = "GodotSharp"; - const string editorProjectName = "GodotSharpEditor"; - - const string coreCondition = ""; - const string editorCondition = "'$(Configuration)' == 'Debug'"; - - var coreHintPath = $"$(ProjectDir)/.mono/assemblies/$(ApiConfiguration)/{coreProjectName}.dll"; - var editorHintPath = $"$(ProjectDir)/.mono/assemblies/$(ApiConfiguration)/{editorProjectName}.dll"; - - SetReferenceHintPath(coreProjectName, coreCondition, coreHintPath); - SetReferenceHintPath(editorProjectName, editorCondition, editorHintPath); - } - - public static void MigrateFromOldConfigNames(MSBuildProject project) + public static void EnsureGodotSdkIsUpToDate(MSBuildProject project) { var root = project.Root; + string godotSdkAttrValue = ProjectGenerator.GodotSdkAttrValue; - bool hasGodotProjectGeneratorVersion = false; - bool foundOldConfiguration = false; - - foreach (var propertyGroup in root.PropertyGroups.Where(g => string.IsNullOrEmpty(g.Condition))) - { - if (!hasGodotProjectGeneratorVersion && propertyGroup.Properties.Any(p => p.Name == "GodotProjectGeneratorVersion")) - hasGodotProjectGeneratorVersion = true; - - foreach (var configItem in propertyGroup.Properties - .Where(p => p.Condition.Trim() == "'$(Configuration)' == ''" && p.Value == "Tools")) - { - configItem.Value = "Debug"; - foundOldConfiguration = true; - project.HasUnsavedChanges = true; - } - } - - if (!hasGodotProjectGeneratorVersion) - { - root.PropertyGroups.First(g => string.IsNullOrEmpty(g.Condition))? - .AddProperty("GodotProjectGeneratorVersion", Assembly.GetExecutingAssembly().GetName().Version.ToString()); - project.HasUnsavedChanges = true; - } - - if (!foundOldConfiguration) - { - var toolsConditions = new[] - { - "'$(Configuration)|$(Platform)' == 'Tools|AnyCPU'", - "'$(Configuration)|$(Platform)' != 'Tools|AnyCPU'", - "'$(Configuration)' == 'Tools'", - "'$(Configuration)' != 'Tools'" - }; - - foundOldConfiguration = root.PropertyGroups - .Any(g => toolsConditions.Any(c => c == g.Condition.Trim())); - } - - if (foundOldConfiguration) - { - void MigrateConfigurationConditions(string oldConfiguration, string newConfiguration) - { - void MigrateConditions(string oldCondition, string newCondition) - { - foreach (var propertyGroup in root.PropertyGroups.Where(g => g.Condition.Trim() == oldCondition)) - { - propertyGroup.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - - foreach (var propertyGroup in root.PropertyGroups) - { - foreach (var prop in propertyGroup.Properties.Where(p => p.Condition.Trim() == oldCondition)) - { - prop.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - } - - foreach (var itemGroup in root.ItemGroups.Where(g => g.Condition.Trim() == oldCondition)) - { - itemGroup.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - - foreach (var itemGroup in root.ItemGroups) - { - foreach (var item in itemGroup.Items.Where(item => item.Condition.Trim() == oldCondition)) - { - item.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - } - } - - foreach (var op in new[] {"==", "!="}) - { - MigrateConditions($"'$(Configuration)|$(Platform)' {op} '{oldConfiguration}|AnyCPU'", $"'$(Configuration)|$(Platform)' {op} '{newConfiguration}|AnyCPU'"); - MigrateConditions($"'$(Configuration)' {op} '{oldConfiguration}'", $"'$(Configuration)' {op} '{newConfiguration}'"); - } - } - - MigrateConfigurationConditions("Debug", "ExportDebug"); - MigrateConfigurationConditions("Release", "ExportRelease"); - MigrateConfigurationConditions("Tools", "Debug"); // Must be last - } - } - - public static void EnsureHasNugetNetFrameworkRefAssemblies(MSBuildProject project) - { - var root = project.Root; - - bool found = root.ItemGroups.Any(g => string.IsNullOrEmpty(g.Condition) && g.Items.Any( - item => item.ItemType == "PackageReference" && item.Include == "Microsoft.NETFramework.ReferenceAssemblies")); - - if (found) + if (!string.IsNullOrEmpty(root.Sdk) && root.Sdk.Trim().Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase)) return; - var frameworkRefAssembliesItem = root.AddItem("PackageReference", "Microsoft.NETFramework.ReferenceAssemblies"); - - // Use metadata (child nodes) instead of attributes for the PackageReference. - // This is for compatibility with 3.2, where GodotTools uses an old Microsoft.Build. - frameworkRefAssembliesItem.AddMetadata("Version", "1.0.0"); - frameworkRefAssembliesItem.AddMetadata("PrivateAssets", "All"); - + root.Sdk = godotSdkAttrValue; project.HasUnsavedChanges = true; } } diff --git a/modules/mono/editor/GodotTools/GodotTools.Shared/GenerateGodotNupkgsVersions.targets b/modules/mono/editor/GodotTools/GodotTools.Shared/GenerateGodotNupkgsVersions.targets new file mode 100644 index 0000000000..1d382dcb43 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.Shared/GenerateGodotNupkgsVersions.targets @@ -0,0 +1,36 @@ +<Project> + <!-- Generate C# file with the version of all the nupkgs bundled with Godot --> + + <Target Name="SetPropertiesForGenerateGodotNupkgsVersions"> + <PropertyGroup> + <GodotNETSdkPackageVersionFile>$(SolutionDir)..\Godot.NET.Sdk\Godot.NET.Sdk\Godot.NET.Sdk_PackageVersion.txt</GodotNETSdkPackageVersionFile> + <GeneratedGodotNupkgsVersionsFile>$(IntermediateOutputPath)GodotNupkgsVersions.g.cs</GeneratedGodotNupkgsVersionsFile> + </PropertyGroup> + </Target> + + <Target Name="GenerateGodotNupkgsVersionsFile" + DependsOnTargets="PrepareForBuild;_GenerateGodotNupkgsVersionsFile" + BeforeTargets="BeforeCompile;CoreCompile"> + <ItemGroup> + <Compile Include="$(GeneratedGodotNupkgsVersionsFile)" /> + <FileWrites Include="$(GeneratedGodotNupkgsVersionsFile)" /> + </ItemGroup> + </Target> + <Target Name="_GenerateGodotNupkgsVersionsFile" + DependsOnTargets="SetPropertiesForGenerateGodotNupkgsVersions" + Inputs="$(MSBuildProjectFile);@(GodotNETSdkPackageVersionFile)" + Outputs="$(GeneratedGodotNupkgsVersionsFile)"> + <PropertyGroup> + <GenerateGodotNupkgsVersionsCode><![CDATA[ +namespace $(RootNamespace) { + public class GeneratedGodotNupkgsVersions { + public const string GodotNETSdk = "$([System.IO.File]::ReadAllText('$(GodotNETSdkPackageVersionFile)').Trim())"%3b + } +} +]]></GenerateGodotNupkgsVersionsCode> + </PropertyGroup> + <WriteLinesToFile Lines="$(GenerateGodotNupkgsVersionsCode)" + File="$(GeneratedGodotNupkgsVersionsFile)" + Overwrite="True" WriteOnlyWhenDifferent="True" /> + </Target> +</Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj b/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj new file mode 100644 index 0000000000..3bc1698c15 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj @@ -0,0 +1,6 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + </PropertyGroup> + <Import Project="GenerateGodotNupkgsVersions.targets" /> +</Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln index ba5379e562..d3107a69db 100644 --- a/modules/mono/editor/GodotTools/GodotTools.sln +++ b/modules/mono/editor/GodotTools/GodotTools.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.IdeMessaging", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.OpenVisualStudio", "GodotTools.OpenVisualStudio\GodotTools.OpenVisualStudio.csproj", "{EAFFF236-FA96-4A4D-BD23-0E51EF988277}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Shared", "GodotTools.Shared\GodotTools.Shared.csproj", "{2758FFAF-8237-4CF2-B569-66BF8B3587BB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,5 +45,9 @@ Global {EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Debug|Any CPU.Build.0 = Debug|Any CPU {EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Release|Any CPU.ActiveCfg = Release|Any CPU {EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Release|Any CPU.Build.0 = Release|Any CPU + {2758FFAF-8237-4CF2-B569-66BF8B3587BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection EndGlobal diff --git a/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs b/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs deleted file mode 100644 index 3de3d8d318..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs +++ /dev/null @@ -1,343 +0,0 @@ -using Godot; -using System; -using System.IO; -using Godot.Collections; -using GodotTools.Internals; -using static GodotTools.Internals.Globals; -using File = GodotTools.Utils.File; -using Path = System.IO.Path; - -namespace GodotTools -{ - public class BottomPanel : VBoxContainer - { - private EditorInterface editorInterface; - - private TabContainer panelTabs; - - private VBoxContainer panelBuildsTab; - - private ItemList buildTabsList; - private TabContainer buildTabs; - - private Button warningsBtn; - private Button errorsBtn; - private Button viewLogBtn; - - private void _UpdateBuildTabsList() - { - buildTabsList.Clear(); - - int currentTab = buildTabs.CurrentTab; - - bool noCurrentTab = currentTab < 0 || currentTab >= buildTabs.GetTabCount(); - - for (int i = 0; i < buildTabs.GetChildCount(); i++) - { - var tab = (BuildTab)buildTabs.GetChild(i); - - if (tab == null) - continue; - - string itemName = Path.GetFileNameWithoutExtension(tab.BuildInfo.Solution); - itemName += " [" + tab.BuildInfo.Configuration + "]"; - - buildTabsList.AddItem(itemName, tab.IconTexture); - - string itemTooltip = "Solution: " + tab.BuildInfo.Solution; - itemTooltip += "\nConfiguration: " + tab.BuildInfo.Configuration; - itemTooltip += "\nStatus: "; - - if (tab.BuildExited) - itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored"; - else - itemTooltip += "Running"; - - if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error) - itemTooltip += $"\nErrors: {tab.ErrorCount}"; - - itemTooltip += $"\nWarnings: {tab.WarningCount}"; - - buildTabsList.SetItemTooltip(i, itemTooltip); - - if (noCurrentTab || currentTab == i) - { - buildTabsList.Select(i); - _BuildTabsItemSelected(i); - } - } - } - - public BuildTab GetBuildTabFor(BuildInfo buildInfo) - { - foreach (var buildTab in new Array<BuildTab>(buildTabs.GetChildren())) - { - if (buildTab.BuildInfo.Equals(buildInfo)) - return buildTab; - } - - var newBuildTab = new BuildTab(buildInfo); - AddBuildTab(newBuildTab); - - return newBuildTab; - } - - private void _BuildTabsItemSelected(int idx) - { - if (idx < 0 || idx >= buildTabs.GetTabCount()) - throw new IndexOutOfRangeException(); - - buildTabs.CurrentTab = idx; - if (!buildTabs.Visible) - buildTabs.Visible = true; - - warningsBtn.Visible = true; - errorsBtn.Visible = true; - viewLogBtn.Visible = true; - } - - private void _BuildTabsNothingSelected() - { - if (buildTabs.GetTabCount() != 0) - { - // just in case - buildTabs.Visible = false; - - // This callback is called when clicking on the empty space of the list. - // ItemList won't deselect the items automatically, so we must do it ourselves. - buildTabsList.UnselectAll(); - } - - warningsBtn.Visible = false; - errorsBtn.Visible = false; - viewLogBtn.Visible = false; - } - - private void _WarningsToggled(bool pressed) - { - int currentTab = buildTabs.CurrentTab; - - if (currentTab < 0 || currentTab >= buildTabs.GetTabCount()) - throw new InvalidOperationException("No tab selected"); - - var buildTab = (BuildTab)buildTabs.GetChild(currentTab); - buildTab.WarningsVisible = pressed; - buildTab.UpdateIssuesList(); - } - - private void _ErrorsToggled(bool pressed) - { - int currentTab = buildTabs.CurrentTab; - - if (currentTab < 0 || currentTab >= buildTabs.GetTabCount()) - throw new InvalidOperationException("No tab selected"); - - var buildTab = (BuildTab)buildTabs.GetChild(currentTab); - buildTab.ErrorsVisible = pressed; - buildTab.UpdateIssuesList(); - } - - public void BuildProjectPressed() - { - if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) - return; // No solution to build - - string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor"); - string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player"); - - CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath); - - if (File.Exists(editorScriptsMetadataPath)) - { - try - { - File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath); - } - catch (IOException e) - { - GD.PushError($"Failed to copy scripts metadata file. Exception message: {e.Message}"); - return; - } - } - - var godotDefines = new[] - { - OS.GetName(), - Internal.GodotIs32Bits() ? "32" : "64" - }; - - bool buildSuccess = BuildManager.BuildProjectBlocking("Debug", godotDefines); - - if (!buildSuccess) - return; - - // Notify running game for hot-reload - Internal.EditorDebuggerNodeReloadScripts(); - - // Hot-reload in the editor - GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer(); - - if (Internal.IsAssembliesReloadingNeeded()) - Internal.ReloadAssemblies(softReload: false); - } - - private void _ViewLogPressed() - { - if (!buildTabsList.IsAnythingSelected()) - return; - - var selectedItems = buildTabsList.GetSelectedItems(); - - if (selectedItems.Length != 1) - throw new InvalidOperationException($"Expected 1 selected item, got {selectedItems.Length}"); - - int selectedItem = selectedItems[0]; - - var buildTab = (BuildTab)buildTabs.GetTabControl(selectedItem); - - OS.ShellOpen(Path.Combine(buildTab.BuildInfo.LogsDirPath, BuildManager.MsBuildLogFileName)); - } - - public override void _Notification(int what) - { - base._Notification(what); - - if (what == EditorSettings.NotificationEditorSettingsChanged) - { - var editorBaseControl = editorInterface.GetBaseControl(); - panelTabs.AddThemeStyleboxOverride("panel", editorBaseControl.GetThemeStylebox("DebuggerPanel", "EditorStyles")); - panelTabs.AddThemeStyleboxOverride("tab_fg", editorBaseControl.GetThemeStylebox("DebuggerTabFG", "EditorStyles")); - panelTabs.AddThemeStyleboxOverride("tab_bg", editorBaseControl.GetThemeStylebox("DebuggerTabBG", "EditorStyles")); - } - } - - public void AddBuildTab(BuildTab buildTab) - { - buildTabs.AddChild(buildTab); - RaiseBuildTab(buildTab); - } - - public void RaiseBuildTab(BuildTab buildTab) - { - if (buildTab.GetParent() != buildTabs) - throw new InvalidOperationException("Build tab is not in the tabs list"); - - buildTabs.MoveChild(buildTab, 0); - _UpdateBuildTabsList(); - } - - public void ShowBuildTab() - { - for (int i = 0; i < panelTabs.GetTabCount(); i++) - { - if (panelTabs.GetTabControl(i) == panelBuildsTab) - { - panelTabs.CurrentTab = i; - GodotSharpEditor.Instance.MakeBottomPanelItemVisible(this); - return; - } - } - - GD.PushError("Builds tab not found"); - } - - public override void _Ready() - { - base._Ready(); - - editorInterface = GodotSharpEditor.Instance.GetEditorInterface(); - - var editorBaseControl = editorInterface.GetBaseControl(); - - SizeFlagsVertical = (int)SizeFlags.ExpandFill; - SetAnchorsAndMarginsPreset(LayoutPreset.Wide); - - panelTabs = new TabContainer - { - TabAlign = TabContainer.TabAlignEnum.Left, - RectMinSize = new Vector2(0, 228) * EditorScale, - SizeFlagsVertical = (int)SizeFlags.ExpandFill - }; - panelTabs.AddThemeStyleboxOverride("panel", editorBaseControl.GetThemeStylebox("DebuggerPanel", "EditorStyles")); - panelTabs.AddThemeStyleboxOverride("tab_fg", editorBaseControl.GetThemeStylebox("DebuggerTabFG", "EditorStyles")); - panelTabs.AddThemeStyleboxOverride("tab_bg", editorBaseControl.GetThemeStylebox("DebuggerTabBG", "EditorStyles")); - AddChild(panelTabs); - - { - // Builds tab - panelBuildsTab = new VBoxContainer - { - Name = "Builds".TTR(), - SizeFlagsHorizontal = (int)SizeFlags.ExpandFill - }; - panelTabs.AddChild(panelBuildsTab); - - var toolBarHBox = new HBoxContainer { SizeFlagsHorizontal = (int)SizeFlags.ExpandFill }; - panelBuildsTab.AddChild(toolBarHBox); - - var buildProjectBtn = new Button - { - Text = "Build Project".TTR(), - FocusMode = FocusModeEnum.None - }; - buildProjectBtn.PressedSignal += BuildProjectPressed; - toolBarHBox.AddChild(buildProjectBtn); - - toolBarHBox.AddSpacer(begin: false); - - warningsBtn = new Button - { - Text = "Warnings".TTR(), - ToggleMode = true, - Pressed = true, - Visible = false, - FocusMode = FocusModeEnum.None - }; - warningsBtn.Toggled += _WarningsToggled; - toolBarHBox.AddChild(warningsBtn); - - errorsBtn = new Button - { - Text = "Errors".TTR(), - ToggleMode = true, - Pressed = true, - Visible = false, - FocusMode = FocusModeEnum.None - }; - errorsBtn.Toggled += _ErrorsToggled; - toolBarHBox.AddChild(errorsBtn); - - toolBarHBox.AddSpacer(begin: false); - - viewLogBtn = new Button - { - Text = "View log".TTR(), - FocusMode = FocusModeEnum.None, - Visible = false - }; - viewLogBtn.PressedSignal += _ViewLogPressed; - toolBarHBox.AddChild(viewLogBtn); - - var hsc = new HSplitContainer - { - SizeFlagsHorizontal = (int)SizeFlags.ExpandFill, - SizeFlagsVertical = (int)SizeFlags.ExpandFill - }; - panelBuildsTab.AddChild(hsc); - - buildTabsList = new ItemList { SizeFlagsHorizontal = (int)SizeFlags.ExpandFill }; - buildTabsList.ItemSelected += _BuildTabsItemSelected; - buildTabsList.NothingSelected += _BuildTabsNothingSelected; - hsc.AddChild(buildTabsList); - - buildTabs = new TabContainer - { - TabAlign = TabContainer.TabAlignEnum.Left, - SizeFlagsHorizontal = (int)SizeFlags.ExpandFill, - TabsVisible = false - }; - hsc.AddChild(buildTabs); - } - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs index ab090c46e7..51055dc9b9 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs @@ -4,7 +4,7 @@ using Godot.Collections; using GodotTools.Internals; using Path = System.IO.Path; -namespace GodotTools +namespace GodotTools.Build { [Serializable] public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization @@ -20,7 +20,9 @@ namespace GodotTools public override bool Equals(object obj) { if (obj is BuildInfo other) - return other.Solution == Solution && other.Configuration == Configuration; + return other.Solution == Solution && other.Targets == Targets && + other.Configuration == Configuration && other.Restore == Restore && + other.CustomProperties == CustomProperties && other.LogsDirPath == LogsDirPath; return false; } @@ -31,7 +33,11 @@ namespace GodotTools { int hash = 17; hash = hash * 29 + Solution.GetHashCode(); + hash = hash * 29 + Targets.GetHashCode(); hash = hash * 29 + Configuration.GetHashCode(); + hash = hash * 29 + Restore.GetHashCode(); + hash = hash * 29 + CustomProperties.GetHashCode(); + hash = hash * 29 + LogsDirPath.GetHashCode(); return hash; } } diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs index 0974d23176..b96b0c8175 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs @@ -1,19 +1,18 @@ using System; -using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using GodotTools.Build; using GodotTools.Ides.Rider; using GodotTools.Internals; -using GodotTools.Utils; +using JetBrains.Annotations; using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; +using OS = GodotTools.Utils.OS; -namespace GodotTools +namespace GodotTools.Build { public static class BuildManager { - private static readonly List<BuildInfo> BuildsInProgress = new List<BuildInfo>(); + private static BuildInfo _buildInProgress; public const string PropNameMSBuildMono = "MSBuild (Mono)"; public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)"; @@ -23,6 +22,14 @@ namespace GodotTools public const string MsBuildIssuesFileName = "msbuild_issues.csv"; public const string MsBuildLogFileName = "msbuild_log.txt"; + public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason); + + public static event BuildLaunchFailedEventHandler BuildLaunchFailed; + public static event Action<BuildInfo> BuildStarted; + public static event Action<BuildResult> BuildFinished; + public static event Action<string> StdOutputReceived; + public static event Action<string> StdErrorReceived; + private static void RemoveOldIssuesFile(BuildInfo buildInfo) { var issuesFile = GetIssuesFilePath(buildInfo); @@ -35,12 +42,13 @@ namespace GodotTools private static void ShowBuildErrorDialog(string message) { - GodotSharpEditor.Instance.ShowErrorDialog(message, "Build error"); - GodotSharpEditor.Instance.BottomPanel.ShowBuildTab(); + var plugin = GodotSharpEditor.Instance; + plugin.ShowErrorDialog(message, "Build error"); + plugin.MakeBottomPanelItemVisible(plugin.MSBuildPanel); } - public static void RestartBuild(BuildTab buildTab) => throw new NotImplementedException(); - public static void StopBuild(BuildTab buildTab) => throw new NotImplementedException(); + public static void RestartBuild(BuildOutputView buildOutputView) => throw new NotImplementedException(); + public static void StopBuild(BuildOutputView buildOutputView) => throw new NotImplementedException(); private static string GetLogFilePath(BuildInfo buildInfo) { @@ -60,15 +68,14 @@ namespace GodotTools public static bool Build(BuildInfo buildInfo) { - if (BuildsInProgress.Contains(buildInfo)) + if (_buildInProgress != null) throw new InvalidOperationException("A build is already in progress"); - BuildsInProgress.Add(buildInfo); + _buildInProgress = buildInfo; try { - BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo); - buildTab.OnBuildStart(); + BuildStarted?.Invoke(buildInfo); // Required in order to update the build tasks list Internal.GodotMainIteration(); @@ -79,44 +86,44 @@ namespace GodotTools } catch (IOException e) { - buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); + BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); Console.Error.WriteLine(e); } try { - int exitCode = BuildSystem.Build(buildInfo); + int exitCode = BuildSystem.Build(buildInfo, StdOutputReceived, StdErrorReceived); if (exitCode != 0) PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); - buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error); + BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error); return exitCode == 0; } catch (Exception e) { - buildTab.OnBuildExecFailed($"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; } } finally { - BuildsInProgress.Remove(buildInfo); + _buildInProgress = null; } } public static async Task<bool> BuildAsync(BuildInfo buildInfo) { - if (BuildsInProgress.Contains(buildInfo)) + if (_buildInProgress != null) throw new InvalidOperationException("A build is already in progress"); - BuildsInProgress.Add(buildInfo); + _buildInProgress = buildInfo; try { - BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo); + BuildStarted?.Invoke(buildInfo); try { @@ -124,43 +131,57 @@ namespace GodotTools } catch (IOException e) { - buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); + BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); Console.Error.WriteLine(e); } try { - int exitCode = await BuildSystem.BuildAsync(buildInfo); + int exitCode = await BuildSystem.BuildAsync(buildInfo, StdOutputReceived, StdErrorReceived); if (exitCode != 0) PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); - buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error); + BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error); return exitCode == 0; } catch (Exception e) { - buildTab.OnBuildExecFailed($"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; } } finally { - BuildsInProgress.Remove(buildInfo); + _buildInProgress = null; } } - public static bool BuildProjectBlocking(string config, IEnumerable<string> godotDefines) + public static bool BuildProjectBlocking(string config, [CanBeNull] string[] targets = null, [CanBeNull] string platform = null) { - if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) + var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets ?? new[] {"Build"}, config, restore: true); + + // 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}"); + + if (Internal.GodotIsRealTDouble()) + buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); + + return BuildProjectBlocking(buildInfo); + } + + private static bool BuildProjectBlocking(BuildInfo buildInfo) + { + 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(config == "ExportRelease" ? "Release" : "Debug"); + string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(buildInfo.Configuration == "ExportRelease" ? "Release" : "Debug"); if (!string.IsNullOrEmpty(apiAssembliesUpdateError)) { @@ -168,30 +189,10 @@ namespace GodotTools return false; } - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); - using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1)) { pr.Step("Building project solution", 0); - var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets: new[] {"Build"}, config, restore: true); - - bool escapeNeedsDoubleBackslash = buildTool == BuildTool.MsBuildMono || buildTool == BuildTool.DotnetCli; - - // Add Godot defines - string constants = !escapeNeedsDoubleBackslash ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\""; - - foreach (var godotDefine in godotDefines) - constants += $"GODOT_{godotDefine.ToUpper().Replace("-", "_").Replace(" ", "_").Replace(";", "_")};"; - - if (Internal.GodotIsRealTDouble()) - constants += "GODOT_REAL_T_IS_DOUBLE;"; - - constants += !escapeNeedsDoubleBackslash ? "\"" : "\\\""; - - buildInfo.CustomProperties.Add(constants); - if (!Build(buildInfo)) { ShowBuildErrorDialog("Failed to build project solution"); @@ -207,39 +208,51 @@ namespace GodotTools if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) return true; // No solution to build + try + { + // Make sure our packages are added to the fallback folder + NuGetUtils.AddBundledPackagesToFallbackFolder(NuGetUtils.GodotFallbackFolderPath); + } + catch (Exception e) + { + Godot.GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); + } + + GenerateEditorScriptMetadata(); + + if (GodotSharpEditor.Instance.SkipBuildBeforePlaying) + return true; // Requested play from an external editor/IDE which already built the project + + return BuildProjectBlocking("Debug"); + } + + // NOTE: This will be replaced with C# source generators in 4.0 + public static void GenerateEditorScriptMetadata() + { string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor"); string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player"); CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath); - if (File.Exists(editorScriptsMetadataPath)) - File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath); - - var currentPlayRequest = GodotSharpEditor.Instance.CurrentPlaySettings; + if (!File.Exists(editorScriptsMetadataPath)) + return; - if (currentPlayRequest != null) + try { - if (currentPlayRequest.Value.HasDebugger) - { - // Set the environment variable that will tell the player to connect to the IDE debugger - // TODO: We should probably add a better way to do this - Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", - "--debugger-agent=transport=dt_socket" + - $",address={currentPlayRequest.Value.DebuggerHost}:{currentPlayRequest.Value.DebuggerPort}" + - ",server=n"); - } - - if (!currentPlayRequest.Value.BuildBeforePlaying) - return true; // Requested play from an external editor/IDE which already built the project + File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath); } - - var godotDefines = new[] + catch (IOException e) { - Godot.OS.GetName(), - Internal.GodotIs32Bits() ? "32" : "64" - }; + throw new IOException("Failed to copy scripts metadata file.", innerException: e); + } + } - return BuildProjectBlocking("Debug", godotDefines); + // NOTE: This will be replaced with C# source generators in 4.0 + public static string GenerateExportedGameScriptMetadata(bool isDebug) + { + string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}"); + CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath); + return scriptsMetadataPath; } public static void Initialize() @@ -285,8 +298,6 @@ namespace GodotTools ["hint"] = Godot.PropertyHint.Enum, ["hint_string"] = hintString }); - - EditorDef("mono/builds/print_build_output", false); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs new file mode 100644 index 0000000000..1a1639aac7 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs @@ -0,0 +1,417 @@ +using Godot; +using System; +using Godot.Collections; +using GodotTools.Internals; +using JetBrains.Annotations; +using File = GodotTools.Utils.File; +using Path = System.IO.Path; + +namespace GodotTools.Build +{ + public class BuildOutputView : VBoxContainer, ISerializationListener + { + [Serializable] + private class BuildIssue : Reference // TODO Remove Reference once we have proper serialization + { + public bool Warning { get; set; } + public string File { get; set; } + public int Line { get; set; } + public int Column { get; set; } + public string Code { get; set; } + public string Message { get; set; } + public string ProjectFile { get; set; } + } + + private readonly Array<BuildIssue> issues = new Array<BuildIssue>(); // TODO Use List once we have proper serialization + private ItemList issuesList; + private TextEdit buildLog; + private PopupMenu issuesListContextMenu; + + private readonly object pendingBuildLogTextLock = new object(); + [NotNull] private string pendingBuildLogText = string.Empty; + + [Signal] public event Action BuildStateChanged; + + public bool HasBuildExited { get; private set; } = false; + + public BuildResult? BuildResult { get; private set; } = null; + + public int ErrorCount { get; private set; } = 0; + + public int WarningCount { get; private set; } = 0; + + public bool ErrorsVisible { get; set; } = true; + public bool WarningsVisible { get; set; } = true; + + public Texture2D BuildStateIcon + { + get + { + if (!HasBuildExited) + return GetThemeIcon("Stop", "EditorIcons"); + + if (BuildResult == Build.BuildResult.Error) + return GetThemeIcon("Error", "EditorIcons"); + + if (WarningCount > 1) + return GetThemeIcon("Warning", "EditorIcons"); + + return null; + } + } + + private BuildInfo BuildInfo { get; set; } + + public bool LogVisible + { + set => buildLog.Visible = value; + } + + private void LoadIssuesFromFile(string csvFile) + { + using (var file = new Godot.File()) + { + try + { + Error openError = file.Open(csvFile, Godot.File.ModeFlags.Read); + + if (openError != Error.Ok) + 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 + { + file.Close(); // Disposing it is not enough. We need to call Close() + } + } + } + + private void IssueActivated(int idx) + { + if (idx < 0 || idx >= issuesList.GetItemCount()) + throw new IndexOutOfRangeException("Item list index out of range"); + + // Get correct issue idx from issue list + int issueIndex = (int)(long)issuesList.GetItemMetadata(idx); + + if (issueIndex < 0 || issueIndex >= issues.Count) + throw new IndexOutOfRangeException("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 file = Path.Combine(projectDir.SimplifyGodotPath(), issue.File.SimplifyGodotPath()); + + if (!File.Exists(file)) + return; + + file = ProjectSettings.LocalizePath(file); + + if (file.StartsWith("res://")) + { + var script = (Script)ResourceLoader.Load(file, typeHint: Internal.CSharpLanguageType); + + if (script != null && Internal.ScriptEditorEdit(script, issue.Line, issue.Column)) + Internal.EditorNodeShowScriptScreen(); + } + } + + public void UpdateIssuesList() + { + issuesList.Clear(); + + using (var warningIcon = GetThemeIcon("Warning", "EditorIcons")) + using (var errorIcon = GetThemeIcon("Error", "EditorIcons")) + { + for (int i = 0; i < issues.Count; i++) + { + BuildIssue issue = issues[i]; + + if (!(issue.Warning ? WarningsVisible : ErrorsVisible)) + continue; + + string tooltip = string.Empty; + tooltip += $"Message: {issue.Message}"; + + if (!string.IsNullOrEmpty(issue.Code)) + tooltip += $"\nCode: {issue.Code}"; + + tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}"; + + string text = string.Empty; + + if (!string.IsNullOrEmpty(issue.File)) + { + text += $"{issue.File}({issue.Line},{issue.Column}): "; + + tooltip += $"\nFile: {issue.File}"; + tooltip += $"\nLine: {issue.Line}"; + tooltip += $"\nColumn: {issue.Column}"; + } + + if (!string.IsNullOrEmpty(issue.ProjectFile)) + tooltip += $"\nProject: {issue.ProjectFile}"; + + text += issue.Message; + + int lineBreakIdx = text.IndexOf("\n", StringComparison.Ordinal); + string itemText = lineBreakIdx == -1 ? text : text.Substring(0, lineBreakIdx); + issuesList.AddItem(itemText, issue.Warning ? warningIcon : errorIcon); + + int index = issuesList.GetItemCount() - 1; + issuesList.SetItemTooltip(index, tooltip); + issuesList.SetItemMetadata(index, i); + } + } + } + + private void BuildLaunchFailed(BuildInfo buildInfo, string cause) + { + HasBuildExited = true; + BuildResult = Build.BuildResult.Error; + + issuesList.Clear(); + + var issue = new BuildIssue {Message = cause, Warning = false}; + + ErrorCount += 1; + issues.Add(issue); + + UpdateIssuesList(); + + EmitSignal(nameof(BuildStateChanged)); + } + + private void BuildStarted(BuildInfo buildInfo) + { + BuildInfo = buildInfo; + HasBuildExited = false; + + issues.Clear(); + WarningCount = 0; + ErrorCount = 0; + buildLog.Text = string.Empty; + + UpdateIssuesList(); + + EmitSignal(nameof(BuildStateChanged)); + } + + private void BuildFinished(BuildResult result) + { + HasBuildExited = true; + BuildResult = result; + + LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName)); + + UpdateIssuesList(); + + EmitSignal(nameof(BuildStateChanged)); + } + + private void UpdateBuildLogText() + { + lock (pendingBuildLogTextLock) + { + buildLog.Text += pendingBuildLogText; + pendingBuildLogText = string.Empty; + ScrollToLastNonEmptyLogLine(); + } + } + + private void StdOutputReceived(string text) + { + lock (pendingBuildLogTextLock) + { + if (pendingBuildLogText.Length == 0) + CallDeferred(nameof(UpdateBuildLogText)); + pendingBuildLogText += text + "\n"; + } + } + + private void StdErrorReceived(string text) + { + lock (pendingBuildLogTextLock) + { + if (pendingBuildLogText.Length == 0) + CallDeferred(nameof(UpdateBuildLogText)); + pendingBuildLogText += text + "\n"; + } + } + + private void ScrollToLastNonEmptyLogLine() + { + int line; + for (line = buildLog.GetLineCount(); line > 0; line--) + { + string lineText = buildLog.GetLine(line); + + if (!string.IsNullOrEmpty(lineText) || !string.IsNullOrEmpty(lineText?.Trim())) + break; + } + + buildLog.CursorSetLine(line); + } + + public void RestartBuild() + { + if (!HasBuildExited) + throw new InvalidOperationException("Build already started"); + + BuildManager.RestartBuild(this); + } + + public void StopBuild() + { + if (!HasBuildExited) + throw new InvalidOperationException("Build is not in progress"); + + BuildManager.StopBuild(this); + } + + private enum IssuesContextMenuOption + { + Copy + } + + private void IssuesListContextOptionPressed(int id) + { + switch ((IssuesContextMenuOption)id) + { + case IssuesContextMenuOption.Copy: + { + // We don't allow multi-selection but just in case that changes later... + string text = null; + + foreach (int issueIndex in issuesList.GetSelectedItems()) + { + if (text != null) + text += "\n"; + text += issuesList.GetItemText(issueIndex); + } + + if (text != null) + DisplayServer.ClipboardSet(text); + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid issue context menu option"); + } + } + + private void IssuesListRmbSelected(int index, Vector2 atPosition) + { + _ = index; // Unused + + issuesListContextMenu.Clear(); + issuesListContextMenu.Size = new Vector2i(1, 1); + + if (issuesList.IsAnythingSelected()) + { + // Add menu entries for the selected item + issuesListContextMenu.AddIconItem(GetThemeIcon("ActionCopy", "EditorIcons"), + label: "Copy Error".TTR(), (int)IssuesContextMenuOption.Copy); + } + + if (issuesListContextMenu.GetItemCount() > 0) + { + issuesListContextMenu.Position = (Vector2i)(issuesList.RectGlobalPosition + atPosition); + issuesListContextMenu.Popup(); + } + } + + public override void _Ready() + { + base._Ready(); + + SizeFlagsVertical = (int)SizeFlags.ExpandFill; + + var hsc = new HSplitContainer + { + SizeFlagsHorizontal = (int)SizeFlags.ExpandFill, + SizeFlagsVertical = (int)SizeFlags.ExpandFill + }; + AddChild(hsc); + + issuesList = new ItemList + { + SizeFlagsVertical = (int)SizeFlags.ExpandFill, + SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the build log + }; + issuesList.ItemActivated += IssueActivated; + issuesList.AllowRmbSelect = true; + issuesList.ItemRmbSelected += IssuesListRmbSelected; + hsc.AddChild(issuesList); + + issuesListContextMenu = new PopupMenu(); + issuesListContextMenu.IdPressed += IssuesListContextOptionPressed; + issuesList.AddChild(issuesListContextMenu); + + buildLog = new TextEdit + { + Readonly = true, + SizeFlagsVertical = (int)SizeFlags.ExpandFill, + SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the issues list + }; + hsc.AddChild(buildLog); + + AddBuildEventListeners(); + } + + private void AddBuildEventListeners() + { + 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 OnBeforeSerialize() + { + // In case it didn't update yet. We don't want to have to serialize any pending output. + UpdateBuildLogText(); + } + + public void OnAfterDeserialize() + { + AddBuildEventListeners(); // Re-add them + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildResult.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildResult.cs new file mode 100644 index 0000000000..59055b60c3 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildResult.cs @@ -0,0 +1,8 @@ +namespace GodotTools.Build +{ + public enum BuildResult + { + Error, + Success + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 34e42489eb..bac7a2e6db 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -44,10 +44,7 @@ namespace GodotTools.Build } } - private static bool PrintBuildOutput => - (bool)EditorSettings.GetSetting("mono/builds/print_build_output"); - - private static Process LaunchBuild(BuildInfo buildInfo) + private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) { (string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild(); @@ -58,13 +55,13 @@ namespace GodotTools.Build var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs); - bool redirectOutput = !IsDebugMsBuildRequested() && !PrintBuildOutput; - - if (!redirectOutput || Godot.OS.IsStdoutVerbose()) - Console.WriteLine($"Running: \"{startInfo.FileName}\" {startInfo.Arguments}"); + string launchMessage = $"Running: \"{startInfo.FileName}\" {startInfo.Arguments}"; + stdOutHandler?.Invoke(launchMessage); + if (Godot.OS.IsStdoutVerbose()) + Console.WriteLine(launchMessage); - startInfo.RedirectStandardOutput = redirectOutput; - startInfo.RedirectStandardError = redirectOutput; + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; startInfo.UseShellExecute = false; if (UsingMonoMsBuildOnWindows) @@ -82,20 +79,22 @@ namespace GodotTools.Build 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(); - if (redirectOutput) - { - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - } + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); return process; } - public static int Build(BuildInfo buildInfo) + public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) { - using (var process = LaunchBuild(buildInfo)) + using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) { process.WaitForExit(); @@ -103,9 +102,9 @@ namespace GodotTools.Build } } - public static async Task<int> BuildAsync(BuildInfo buildInfo) + public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) { - using (var process = LaunchBuild(buildInfo)) + using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) { await process.WaitForExitAsync(); @@ -118,9 +117,14 @@ namespace GodotTools.Build string arguments = string.Empty; if (buildTool == BuildTool.DotnetCli) - arguments += "msbuild "; // `dotnet msbuild` command + arguments += "msbuild"; // `dotnet msbuild` command - arguments += $@"""{buildInfo.Solution}"" /t:{string.Join(",", buildInfo.Targets)} " + + arguments += $@" ""{buildInfo.Solution}"""; + + if (buildInfo.Restore) + arguments += " /restore"; + + arguments += $@" /t:{string.Join(",", buildInfo.Targets)} " + $@"""/p:{"Configuration=" + buildInfo.Configuration}"" /v:normal " + $@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{buildInfo.LogsDirPath}"""; @@ -147,10 +151,5 @@ namespace GodotTools.Build foreach (string env in platformEnvironmentVariables) environmentVariables.Remove(env); } - - private static bool IsDebugMsBuildRequested() - { - return Environment.GetEnvironmentVariable("GODOT_DEBUG_MSBUILD")?.Trim() == "1"; - } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs new file mode 100644 index 0000000000..708ec73454 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs @@ -0,0 +1,185 @@ +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 BuildOutputView BuildOutputView { get; private set; } + + private Button errorsBtn; + private Button warningsBtn; + private Button viewLogBtn; + + private void WarningsToggled(bool pressed) + { + BuildOutputView.WarningsVisible = pressed; + BuildOutputView.UpdateIssuesList(); + } + + private void ErrorsToggled(bool pressed) + { + BuildOutputView.ErrorsVisible = pressed; + BuildOutputView.UpdateIssuesList(); + } + + [UsedImplicitly] + public void BuildSolution() + { + if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) + return; // No solution to build + + try + { + // Make sure our packages are added to the fallback folder + NuGetUtils.AddBundledPackagesToFallbackFolder(NuGetUtils.GodotFallbackFolderPath); + } + catch (Exception e) + { + GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); + } + + BuildManager.GenerateEditorScriptMetadata(); + + if (!BuildManager.BuildProjectBlocking("Debug")) + return; // Build failed + + // Notify running game for hot-reload + Internal.EditorDebuggerNodeReloadScripts(); + + // Hot-reload in the editor + GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer(); + + if (Internal.IsAssembliesReloadingNeeded()) + Internal.ReloadAssemblies(softReload: false); + } + + [UsedImplicitly] + private void RebuildSolution() + { + if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) + return; // No solution to build + + try + { + // Make sure our packages are added to the fallback folder + NuGetUtils.AddBundledPackagesToFallbackFolder(NuGetUtils.GodotFallbackFolderPath); + } + catch (Exception e) + { + GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); + } + + BuildManager.GenerateEditorScriptMetadata(); + + if (!BuildManager.BuildProjectBlocking("Debug", targets: new[] {"Rebuild"})) + return; // Build failed + + // Notify running game for hot-reload + Internal.EditorDebuggerNodeReloadScripts(); + + // Hot-reload in the editor + GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer(); + + if (Internal.IsAssembliesReloadingNeeded()) + Internal.ReloadAssemblies(softReload: false); + } + + [UsedImplicitly] + private void CleanSolution() + { + if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) + return; // No solution to build + + BuildManager.BuildProjectBlocking("Debug", targets: new[] {"Clean"}); + } + + private void ViewLogToggled(bool pressed) => BuildOutputView.LogVisible = pressed; + + private void BuildMenuOptionPressed(int id) + { + switch ((BuildMenuOptions)id) + { + case BuildMenuOptions.BuildSolution: + BuildSolution(); + break; + case BuildMenuOptions.RebuildSolution: + RebuildSolution(); + break; + case BuildMenuOptions.CleanSolution: + CleanSolution(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid build menu option"); + } + } + + private enum BuildMenuOptions + { + BuildSolution, + RebuildSolution, + CleanSolution + } + + public override void _Ready() + { + base._Ready(); + + RectMinSize = new Vector2(0, 228) * EditorScale; + SizeFlagsVertical = (int)SizeFlags.ExpandFill; + + var toolBarHBox = new HBoxContainer {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill}; + AddChild(toolBarHBox); + + var buildMenuBtn = new MenuButton {Text = "Build", Icon = GetThemeIcon("Play", "EditorIcons")}; + toolBarHBox.AddChild(buildMenuBtn); + + var buildMenu = buildMenuBtn.GetPopup(); + buildMenu.AddItem("Build Solution".TTR(), (int)BuildMenuOptions.BuildSolution); + buildMenu.AddItem("Rebuild Solution".TTR(), (int)BuildMenuOptions.RebuildSolution); + buildMenu.AddItem("Clean Solution".TTR(), (int)BuildMenuOptions.CleanSolution); + buildMenu.IdPressed += BuildMenuOptionPressed; + + errorsBtn = new Button + { + HintTooltip = "Show Errors".TTR(), + Icon = GetThemeIcon("StatusError", "EditorIcons"), + ExpandIcon = false, + ToggleMode = true, + Pressed = true, + FocusMode = FocusModeEnum.None + }; + errorsBtn.Toggled += ErrorsToggled; + toolBarHBox.AddChild(errorsBtn); + + warningsBtn = new Button + { + HintTooltip = "Show Warnings".TTR(), + Icon = GetThemeIcon("NodeWarning", "EditorIcons"), + ExpandIcon = false, + ToggleMode = true, + Pressed = true, + FocusMode = FocusModeEnum.None + }; + warningsBtn.Toggled += WarningsToggled; + toolBarHBox.AddChild(warningsBtn); + + viewLogBtn = new Button + { + Text = "Show Output".TTR(), + ToggleMode = true, + Pressed = true, + FocusMode = FocusModeEnum.None + }; + viewLogBtn.Toggled += ViewLogToggled; + toolBarHBox.AddChild(viewLogBtn); + + BuildOutputView = new BuildOutputView(); + AddChild(BuildOutputView); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs index f36e581a5f..5ef55fea49 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs @@ -31,7 +31,7 @@ namespace GodotTools.Build string dotnetCliPath = OS.PathWhich("dotnet"); if (!string.IsNullOrEmpty(dotnetCliPath)) return (dotnetCliPath, BuildTool.DotnetCli); - GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Visual Studio."); + GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Visual Studio."); goto case BuildTool.MsBuildVs; } case BuildTool.MsBuildVs: @@ -89,7 +89,7 @@ namespace GodotTools.Build string dotnetCliPath = OS.PathWhich("dotnet"); if (!string.IsNullOrEmpty(dotnetCliPath)) return (dotnetCliPath, BuildTool.DotnetCli); - GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Mono."); + GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Mono."); goto case BuildTool.MsBuildMono; } case BuildTool.MsBuildMono: @@ -119,7 +119,7 @@ namespace GodotTools.Build { var result = new List<string>(); - if (OS.IsOSX) + if (OS.IsMacOS) { result.Add("/Library/Frameworks/Mono.framework/Versions/Current/bin/"); result.Add("/usr/local/var/homebrew/linked/mono/bin/"); @@ -161,14 +161,27 @@ namespace GodotTools.Build // Try to find 15.0 with vswhere - string vsWherePath = Environment.GetEnvironmentVariable(Internal.GodotIs32Bits() ? "ProgramFiles" : "ProgramFiles(x86)"); - vsWherePath += "\\Microsoft Visual Studio\\Installer\\vswhere.exe"; + var envNames = Internal.GodotIs32Bits() ? new[] {"ProgramFiles", "ProgramW6432"} : 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, - blocking: true, output: (Godot.Collections.Array)outputArray); + output: (Godot.Collections.Array)outputArray); if (exitCode != 0) return string.Empty; diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs b/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs new file mode 100644 index 0000000000..793ef7fd71 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs @@ -0,0 +1,296 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Xml; +using Godot; +using GodotTools.Internals; +using GodotTools.Shared; +using Directory = GodotTools.Utils.Directory; +using Environment = System.Environment; +using File = GodotTools.Utils.File; + +namespace GodotTools.Build +{ + public static class NuGetUtils + { + public const string GodotFallbackFolderName = "Godot Offline Packages"; + + 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. + /// Does not determine whether the returned files exist or not. + /// </summary> + private static string[] GetAllUserNuGetConfigFilePaths() + { + // Where to find 'NuGet/NuGet.Config': + // + // - Mono/.NETFramework (standalone NuGet): + // Uses Environment.SpecialFolder.ApplicationData + // - Windows: '%APPDATA%' + // - Linux/macOS: '$HOME/.config' + // - CoreCLR (dotnet CLI NuGet): + // - Windows: '%APPDATA%' + // - Linux/macOS: '$DOTNET_CLI_HOME/.nuget' otherwise '$HOME/.nuget' + + string applicationData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + + if (Utils.OS.IsWindows) + { + // %APPDATA% for both + return new[] {Path.Combine(applicationData, "NuGet", "NuGet.Config")}; + } + + var paths = new string[2]; + + // CoreCLR (dotnet CLI NuGet) + + string dotnetCliHome = Environment.GetEnvironmentVariable("DOTNET_CLI_HOME"); + if (!string.IsNullOrEmpty(dotnetCliHome)) + { + paths[0] = Path.Combine(dotnetCliHome, ".nuget", "NuGet", "NuGet.Config"); + } + 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"); + } + + // Mono/.NETFramework (standalone NuGet) + + // ApplicationData is $HOME/.config on Linux/macOS + paths[1] = Path.Combine(applicationData, "NuGet", "NuGet.Config"); + + return paths; + } + + // nupkg extraction + // + // Exclude: (NuGet.Client -> NuGet.Packaging.PackageHelper.ExcludePaths) + // package/ + // _rels/ + // [Content_Types].xml + // + // Don't ignore files that begin with a dot (.) + // + // 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, + /// for both standalone NuGet (Mono/.NETFramework) and dotnet CLI NuGet. + /// </summary> + public static void AddFallbackFolderToUserNuGetConfigs(string name, string path) + { + foreach (string nuGetConfigPath in GetAllUserNuGetConfigFilePaths()) + { + 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""?> +<configuration> + <packageSources> + <add key=""nuget.org"" value=""https://api.nuget.org/v3/index.json"" protocolVersion=""3"" /> + </packageSources> +</configuration> +"; + System.IO.File.WriteAllText(nuGetConfigPath, defaultConfig, Encoding.UTF8); // UTF-8 with BOM + } + + AddFallbackFolderToNuGetConfig(nuGetConfigPath, name, path); + } + } + + private static void AddPackageToFallbackFolder(string fallbackFolder, + string nupkgPath, string packageId, string packageVersion) + { + // dotnet CLI provides no command for this, but we can do it manually. + // + // - The expected structure is as follows: + // fallback_folder/ + // <package.name>/<version>/ + // <package.name>.<version>.nupkg + // <package.name>.<version>.nupkg.sha512 + // <package.name>.nuspec + // ... extracted nupkg files (check code for excluded files) ... + // + // - <package.name> and <version> must be in lower case. + // - 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 destDir = Path.Combine(fallbackFolder, packageIdLower, packageVersionLower); + string nupkgDestPath = Path.Combine(destDir, $"{packageIdLower}.{packageVersionLower}.nupkg"); + string nupkgSha512DestPath = Path.Combine(destDir, $"{packageIdLower}.{packageVersionLower}.nupkg.sha512"); + + if (File.Exists(nupkgDestPath) && File.Exists(nupkgSha512DestPath)) + return; // Already added (for speed we don't check if every file is properly extracted) + + Directory.CreateDirectory(destDir); + + // Generate .nupkg.sha512 file + + using (var alg = SHA512.Create()) + { + alg.ComputeHash(File.ReadAllBytes(nupkgPath)); + string base64Hash = Convert.ToBase64String(alg.Hash); + File.WriteAllText(nupkgSha512DestPath, base64Hash); + } + + // Extract nupkg + ExtractNupkg(destDir, nupkgPath, packageId, packageVersion); + + // Copy .nupkg + File.Copy(nupkgPath, nupkgDestPath); + } + + private static readonly string[] NupkgExcludePaths = + { + "_rels/", + "package/", + "[Content_Types].xml" + }; + + private static void ExtractNupkg(string destDir, string nupkgPath, string packageId, string packageVersion) + { + // NOTE: Must use SimplifyGodotPath to make sure we don't extract files outside the destination directory. + + using (var archive = ZipFile.OpenRead(nupkgPath)) + { + // Extract .nuspec manually as it needs to be in lower case + + var nuspecEntry = archive.GetEntry(packageId + ".nuspec"); + + if (nuspecEntry == null) + throw new InvalidOperationException($"Failed to extract package {packageId}.{packageVersion}. Could not find the nuspec file."); + + nuspecEntry.ExtractToFile(Path.Combine(destDir, nuspecEntry.Name.ToLower().SimplifyGodotPath())); + + // Extract the other package files + + foreach (var entry in archive.Entries) + { + // NOTE: SimplifyGodotPath() removes trailing slash and backslash, + // so we can't use the result to check if the entry is a directory. + + string entryFullName = entry.FullName.Replace('\\', '/'); + + // Check if the file must be ignored + if ( // Excluded files. + NupkgExcludePaths.Any(e => entryFullName.StartsWith(e, StringComparison.OrdinalIgnoreCase)) || + // Nupkg hash files and nupkg metadata files on all directory. + 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")) + { + continue; + } + + string entryFullNameSimplified = entryFullName.SimplifyGodotPath(); + string destFilePath = Path.Combine(destDir, entryFullNameSimplified); + bool isDir = entryFullName.EndsWith("/"); + + if (isDir) + { + Directory.CreateDirectory(destFilePath); + } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(destFilePath)); + entry.ExtractToFile(destFilePath, overwrite: true); + } + } + } + } + + /// <summary> + /// Copies and extracts all the Godot bundled packages to the Godot NuGet fallback folder. + /// Does nothing if the packages were already copied. + /// </summary> + public static void AddBundledPackagesToFallbackFolder(string fallbackFolder) + { + GD.Print("Copying Godot Offline Packages..."); + + string nupkgsLocation = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "nupkgs"); + + void AddPackage(string packageId, string packageVersion) + { + string nupkgPath = Path.Combine(nupkgsLocation, $"{packageId}.{packageVersion}.nupkg"); + AddPackageToFallbackFolder(fallbackFolder, nupkgPath, packageId, packageVersion); + } + + foreach (var (packageId, packageVersion) in PackagesToAdd) + AddPackage(packageId, packageVersion); + } + + private static readonly (string packageId, string packageVersion)[] PackagesToAdd = + { + ("Godot.NET.Sdk", GeneratedGodotNupkgsVersions.GodotNETSdk) + }; + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs b/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs deleted file mode 100644 index 8596cd24af..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs +++ /dev/null @@ -1,267 +0,0 @@ -using Godot; -using System; -using Godot.Collections; -using GodotTools.Internals; -using File = GodotTools.Utils.File; -using Path = System.IO.Path; - -namespace GodotTools -{ - public class BuildTab : VBoxContainer - { - public enum BuildResults - { - Error, - Success - } - - [Serializable] - private class BuildIssue : Reference // TODO Remove Reference once we have proper serialization - { - public bool Warning { get; set; } - public string File { get; set; } - public int Line { get; set; } - public int Column { get; set; } - public string Code { get; set; } - public string Message { get; set; } - public string ProjectFile { get; set; } - } - - private readonly Array<BuildIssue> issues = new Array<BuildIssue>(); // TODO Use List once we have proper serialization - private ItemList issuesList; - - public bool BuildExited { get; private set; } = false; - - public BuildResults? BuildResult { get; private set; } = null; - - public int ErrorCount { get; private set; } = 0; - - public int WarningCount { get; private set; } = 0; - - public bool ErrorsVisible { get; set; } = true; - public bool WarningsVisible { get; set; } = true; - - public Texture2D IconTexture - { - get - { - if (!BuildExited) - return GetThemeIcon("Stop", "EditorIcons"); - - if (BuildResult == BuildResults.Error) - return GetThemeIcon("StatusError", "EditorIcons"); - - return GetThemeIcon("StatusSuccess", "EditorIcons"); - } - } - - public BuildInfo BuildInfo { get; private set; } - - private void _LoadIssuesFromFile(string csvFile) - { - using (var file = new Godot.File()) - { - try - { - Error openError = file.Open(csvFile, Godot.File.ModeFlags.Read); - - if (openError != Error.Ok) - 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 - { - file.Close(); // Disposing it is not enough. We need to call Close() - } - } - } - - private void _IssueActivated(int idx) - { - if (idx < 0 || idx >= issuesList.GetItemCount()) - throw new IndexOutOfRangeException("Item list index out of range"); - - // Get correct issue idx from issue list - int issueIndex = (int)(long)issuesList.GetItemMetadata(idx); - - if (issueIndex < 0 || issueIndex >= issues.Count) - throw new IndexOutOfRangeException("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 file = Path.Combine(projectDir.SimplifyGodotPath(), issue.File.SimplifyGodotPath()); - - if (!File.Exists(file)) - return; - - file = ProjectSettings.LocalizePath(file); - - if (file.StartsWith("res://")) - { - var script = (Script)ResourceLoader.Load(file, typeHint: Internal.CSharpLanguageType); - - if (script != null && Internal.ScriptEditorEdit(script, issue.Line, issue.Column)) - Internal.EditorNodeShowScriptScreen(); - } - } - - public void UpdateIssuesList() - { - issuesList.Clear(); - - using (var warningIcon = GetThemeIcon("Warning", "EditorIcons")) - using (var errorIcon = GetThemeIcon("Error", "EditorIcons")) - { - for (int i = 0; i < issues.Count; i++) - { - BuildIssue issue = issues[i]; - - if (!(issue.Warning ? WarningsVisible : ErrorsVisible)) - continue; - - string tooltip = string.Empty; - tooltip += $"Message: {issue.Message}"; - - if (!string.IsNullOrEmpty(issue.Code)) - tooltip += $"\nCode: {issue.Code}"; - - tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}"; - - string text = string.Empty; - - if (!string.IsNullOrEmpty(issue.File)) - { - text += $"{issue.File}({issue.Line},{issue.Column}): "; - - tooltip += $"\nFile: {issue.File}"; - tooltip += $"\nLine: {issue.Line}"; - tooltip += $"\nColumn: {issue.Column}"; - } - - if (!string.IsNullOrEmpty(issue.ProjectFile)) - tooltip += $"\nProject: {issue.ProjectFile}"; - - text += issue.Message; - - int lineBreakIdx = text.IndexOf("\n", StringComparison.Ordinal); - string itemText = lineBreakIdx == -1 ? text : text.Substring(0, lineBreakIdx); - issuesList.AddItem(itemText, issue.Warning ? warningIcon : errorIcon); - - int index = issuesList.GetItemCount() - 1; - issuesList.SetItemTooltip(index, tooltip); - issuesList.SetItemMetadata(index, i); - } - } - } - - public void OnBuildStart() - { - BuildExited = false; - - issues.Clear(); - WarningCount = 0; - ErrorCount = 0; - UpdateIssuesList(); - - GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this); - } - - public void OnBuildExit(BuildResults result) - { - BuildExited = true; - BuildResult = result; - - _LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName)); - UpdateIssuesList(); - - GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this); - } - - public void OnBuildExecFailed(string cause) - { - BuildExited = true; - BuildResult = BuildResults.Error; - - issuesList.Clear(); - - var issue = new BuildIssue { Message = cause, Warning = false }; - - ErrorCount += 1; - issues.Add(issue); - - UpdateIssuesList(); - - GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this); - } - - public void RestartBuild() - { - if (!BuildExited) - throw new InvalidOperationException("Build already started"); - - BuildManager.RestartBuild(this); - } - - public void StopBuild() - { - if (!BuildExited) - throw new InvalidOperationException("Build is not in progress"); - - BuildManager.StopBuild(this); - } - - public override void _Ready() - { - base._Ready(); - - issuesList = new ItemList { SizeFlagsVertical = (int)SizeFlags.ExpandFill }; - issuesList.ItemActivated += _IssueActivated; - AddChild(issuesList); - } - - private BuildTab() - { - } - - public BuildTab(BuildInfo buildInfo) - { - BuildInfo = buildInfo; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs b/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs index 421729cc11..1d800b8151 100644 --- a/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs +++ b/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs @@ -1,9 +1,9 @@ using Godot; using System; +using System.Linq; using Godot.Collections; using GodotTools.Internals; using GodotTools.ProjectEditor; -using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; using Directory = GodotTools.Utils.Directory; @@ -15,7 +15,7 @@ namespace GodotTools { try { - return ProjectGenerator.GenGameProject(dir, name, compileItems: new string[] { }); + return ProjectGenerator.GenAndSaveGameProject(dir, name); } catch (Exception e) { @@ -24,14 +24,6 @@ namespace GodotTools } } - public static void AddItem(string projectPath, string itemType, string include) - { - if (!(bool)GlobalDef("mono/project/auto_update_project", true)) - return; - - ProjectUtils.AddItemToProjectChecked(projectPath, itemType, include); - } - private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static ulong ConvertToTimestamp(this DateTime value) @@ -40,81 +32,77 @@ namespace GodotTools return (ulong)elapsedTime.TotalSeconds; } - public static void GenerateScriptsMetadata(string projectPath, string outputPath) + private static bool TryParseFileMetadata(string includeFile, ulong modifiedTime, out Dictionary fileMetadata) { - if (File.Exists(outputPath)) - File.Delete(outputPath); + fileMetadata = null; - var oldDict = Internal.GetScriptsMetadataOrNothing(); - var newDict = new Godot.Collections.Dictionary<string, object>(); + var parseError = ScriptClassParser.ParseFile(includeFile, out var classes, out string errorStr); - foreach (var includeFile in ProjectUtils.GetIncludeFiles(projectPath, "Compile")) + if (parseError != Error.Ok) { - string projectIncludeFile = ("res://" + includeFile).SimplifyGodotPath(); + GD.PushError($"Failed to determine namespace and class for script: {includeFile}. Parse error: {errorStr ?? parseError.ToString()}"); + return false; + } - ulong modifiedTime = File.GetLastWriteTime(projectIncludeFile).ConvertToTimestamp(); + string searchName = System.IO.Path.GetFileNameWithoutExtension(includeFile); - if (oldDict.TryGetValue(projectIncludeFile, out var oldFileVar)) - { - var oldFileDict = (Dictionary)oldFileVar; - - if (ulong.TryParse(oldFileDict["modified_time"] as string, out ulong storedModifiedTime)) - { - if (storedModifiedTime == modifiedTime) - { - // No changes so no need to parse again - newDict[projectIncludeFile] = oldFileDict; - continue; - } - } - } + var firstMatch = classes.FirstOrDefault(classDecl => + classDecl.BaseCount != 0 && // If it doesn't inherit anything, it can't be a Godot.Object. + classDecl.SearchName == searchName // Filter by the name we're looking for + ); + + if (firstMatch == null) + return false; // Not found - Error parseError = ScriptClassParser.ParseFile(projectIncludeFile, out var classes, out string errorStr); - if (parseError != Error.Ok) + fileMetadata = new Dictionary + { + ["modified_time"] = $"{modifiedTime}", + ["class"] = new Dictionary { - GD.PushError($"Failed to determine namespace and class for script: {projectIncludeFile}. Parse error: {errorStr ?? parseError.ToString()}"); - continue; + ["namespace"] = firstMatch.Namespace, + ["class_name"] = firstMatch.Name, + ["nested"] = firstMatch.Nested } + }; - string searchName = System.IO.Path.GetFileNameWithoutExtension(projectIncludeFile); - - var classDict = new Dictionary(); + return true; + } - foreach (var classDecl in classes) - { - if (classDecl.BaseCount == 0) - continue; // Does not inherit nor implement anything, so it can't be a script class + public static void GenerateScriptsMetadata(string projectPath, string outputPath) + { + var metadataDict = Internal.GetScriptsMetadataOrNothing().Duplicate(); - string classCmp = classDecl.Nested ? - classDecl.Name.Substring(classDecl.Name.LastIndexOf(".", StringComparison.Ordinal) + 1) : - classDecl.Name; + bool IsUpToDate(string includeFile, ulong modifiedTime) + { + return metadataDict.TryGetValue(includeFile, out var oldFileVar) && + ulong.TryParse(((Dictionary)oldFileVar)["modified_time"] as string, + out ulong storedModifiedTime) && storedModifiedTime == modifiedTime; + } - if (classCmp != searchName) - continue; + var outdatedFiles = ProjectUtils.GetIncludeFiles(projectPath, "Compile") + .Select(path => ("res://" + path).SimplifyGodotPath()) + .ToDictionary(path => path, path => File.GetLastWriteTime(path).ConvertToTimestamp()) + .Where(pair => !IsUpToDate(includeFile: pair.Key, modifiedTime: pair.Value)) + .ToArray(); - classDict["namespace"] = classDecl.Namespace; - classDict["class_name"] = classDecl.Name; - classDict["nested"] = classDecl.Nested; - break; - } + foreach (var pair in outdatedFiles) + { + metadataDict.Remove(pair.Key); - if (classDict.Count == 0) - continue; // Not found + string includeFile = pair.Key; - newDict[projectIncludeFile] = new Dictionary { ["modified_time"] = $"{modifiedTime}", ["class"] = classDict }; + if (TryParseFileMetadata(includeFile, modifiedTime: pair.Value, out var fileMetadata)) + metadataDict[includeFile] = fileMetadata; } - if (newDict.Count > 0) - { - string json = JSON.Print(newDict); + string json = metadataDict.Count <= 0 ? "{}" : JSON.Print(metadataDict); - string baseDir = outputPath.GetBaseDir(); + string baseDir = outputPath.GetBaseDir(); - if (!Directory.Exists(baseDir)) - Directory.CreateDirectory(baseDir); + if (!Directory.Exists(baseDir)) + Directory.CreateDirectory(baseDir); - File.WriteAllText(outputPath, json); - } + File.WriteAllText(outputPath, json); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs index f60e469503..5bb8d444c2 100755 --- a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs @@ -120,7 +120,7 @@ namespace GodotTools.Export string assemblyPath = assembly.Value; string outputFileExtension = platform == OS.Platforms.Windows ? ".dll" : - platform == OS.Platforms.OSX ? ".dylib" : + platform == OS.Platforms.MacOS ? ".dylib" : ".so"; string outputFileName = assemblyName + ".dll" + outputFileExtension; @@ -132,7 +132,7 @@ namespace GodotTools.Export ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir); - if (platform == OS.Platforms.OSX) + if (platform == OS.Platforms.MacOS) { exporter.AddSharedObject(tempOutputFilePath, tags: null); } @@ -328,7 +328,7 @@ MONO_AOT_MODE_LAST = 1000, if (lipoExitCode != 0) throw new Exception($"Command 'lipo' exited with code: {lipoExitCode}"); - // TODO: Add the AOT lib and interpreter libs as device only to supress warnings when targeting the simulator + // TODO: Add the AOT lib and interpreter libs as device only to suppress warnings when targeting the simulator // Add the fat AOT static library to the Xcode project exporter.AddIosProjectStaticLib(fatOutputFilePath); @@ -581,7 +581,7 @@ MONO_AOT_MODE_LAST = 1000, string arch = bits == "64" ? "x86_64" : "i686"; return $"windows-{arch}"; } - case OS.Platforms.OSX: + case OS.Platforms.MacOS: { Debug.Assert(bits == null || bits == "64"); string arch = "x86_64"; diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index 6bfbc62f3b..e9bb701562 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -5,8 +5,10 @@ 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; @@ -18,7 +20,7 @@ namespace GodotTools.Export public class ExportPlugin : EditorExportPlugin { [Flags] - enum I18NCodesets + enum I18NCodesets : long { None = 0, CJK = 1, @@ -142,12 +144,12 @@ namespace GodotTools.Export private void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags) { + _ = flags; // Unused + if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) return; - string platform = DeterminePlatformFromFeatures(features); - - if (platform == null) + if (!DeterminePlatformFromFeatures(features, out string platform)) throw new NotSupportedException("Target platform not supported"); string outputDir = new FileInfo(path).Directory?.FullName ?? @@ -155,15 +157,10 @@ namespace GodotTools.Export string buildConfig = isDebug ? "ExportDebug" : "ExportRelease"; - string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}"); - CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath); - + string scriptsMetadataPath = BuildManager.GenerateExportedGameScriptMetadata(isDebug); AddFile(scriptsMetadataPath, scriptsMetadataPath); - // Turn export features into defines - var godotDefines = features; - - if (!BuildManager.BuildProjectBlocking(buildConfig, godotDefines)) + if (!BuildManager.BuildProjectBlocking(buildConfig, platform: platform)) throw new Exception("Failed to build project"); // Add dependency assemblies @@ -176,6 +173,8 @@ namespace GodotTools.Export assemblies[projectDllName] = projectDllSrcPath; + string bclDir = DeterminePlatformBclDir(platform); + if (platform == OS.Platforms.Android) { string godotAndroidExtProfileDir = GetBclProfileDir("godot_android_ext"); @@ -186,8 +185,49 @@ namespace GodotTools.Export assemblies["Mono.Android"] = monoAndroidAssemblyPath; } + else if (platform == OS.Platforms.HTML5) + { + // 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. - string bclDir = DeterminePlatformBclDir(platform); + 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; + } + } + } var initialAssemblies = assemblies.Duplicate(); internal_GetExportedAssemblyDependencies(initialAssemblies, buildConfig, bclDir, assemblies); @@ -289,6 +329,7 @@ namespace GodotTools.Export } } + [NotNull] private static string ExportDataDirectory(string[] features, string platform, bool isDebug, string outputDir) { string target = isDebug ? "release_debug" : "release"; @@ -343,18 +384,19 @@ namespace GodotTools.Export private static bool PlatformHasTemplateDir(string platform) { // OSX export templates are contained in a zip, so we place our custom template inside it and let Godot do the rest. - return !new[] { OS.Platforms.OSX, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform); + return !new[] {OS.Platforms.MacOS, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5}.Contains(platform); } - private static string DeterminePlatformFromFeatures(IEnumerable<string> features) + private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, out string platform) { foreach (var feature in features) { - if (OS.PlatformNameMap.TryGetValue(feature, out string platform)) - return platform; + if (OS.PlatformNameMap.TryGetValue(feature, out platform)) + return true; } - return null; + platform = null; + return false; } private static string GetBclProfileDir(string profile) @@ -391,7 +433,7 @@ namespace GodotTools.Export /// </summary> private static bool PlatformRequiresCustomBcl(string platform) { - if (new[] { OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(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. @@ -413,7 +455,7 @@ namespace GodotTools.Export case OS.Platforms.Windows: case OS.Platforms.UWP: return "net_4_x_win"; - case OS.Platforms.OSX: + case OS.Platforms.MacOS: case OS.Platforms.LinuxBSD: case OS.Platforms.Server: case OS.Platforms.Haiku: @@ -432,7 +474,7 @@ namespace GodotTools.Export private static string DetermineDataDirNameForProject() { var appName = (string)ProjectSettings.GetSetting("application/config/name"); - string appNameSafe = appName.ToSafeDirName(allowDirSeparator: false); + string appNameSafe = appName.ToSafeDirName(); return $"data_{appNameSafe}"; } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs b/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs index 219b7a698a..93ef837a83 100755 --- a/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs @@ -27,7 +27,7 @@ namespace GodotTools.Export { var outputWrapper = new Godot.Collections.Array(); - int exitCode = Godot.OS.Execute("xcode-select", new string[] { "--print-path" }, blocking: true, output: outputWrapper); + int exitCode = Godot.OS.Execute("xcode-select", new string[] { "--print-path" }, output: outputWrapper); if (exitCode == 0) { diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index f330f9ed2c..58561c5097 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -4,9 +4,9 @@ using GodotTools.Export; using GodotTools.Utils; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using GodotTools.Build; using GodotTools.Ides; using GodotTools.Ides.Rider; using GodotTools.Internals; @@ -19,7 +19,6 @@ using Path = System.IO.Path; namespace GodotTools { - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] public class GodotSharpEditor : EditorPlugin, ISerializationListener { private EditorSettings editorSettings; @@ -31,20 +30,22 @@ namespace GodotTools private CheckBox aboutDialogCheckBox; private Button bottomPanelBtn; + private Button toolBarBuildButton; public GodotIdeManager GodotIdeManager { get; private set; } private WeakRef exportPluginWeak; // TODO Use WeakReference once we have proper serialization - public BottomPanel BottomPanel { get; private set; } + public MSBuildPanel MSBuildPanel { get; private set; } - public PlaySettings? CurrentPlaySettings { get; set; } + public bool SkipBuildBeforePlaying { get; set; } = false; public static string ProjectAssemblyName { get { var projectAssemblyName = (string)ProjectSettings.GetSetting("application/config/name"); + projectAssemblyName = projectAssemblyName.ToSafeDirName(); if (string.IsNullOrEmpty(projectAssemblyName)) projectAssemblyName = "UnnamedProject"; return projectAssemblyName; @@ -126,6 +127,7 @@ namespace GodotTools { menuPopup.RemoveItem(menuPopup.GetItemIndex((int)MenuOptions.CreateSln)); bottomPanelBtn.Show(); + toolBarBuildButton.Show(); } private void _ShowAboutDialog() @@ -145,12 +147,27 @@ namespace GodotTools case MenuOptions.AboutCSharp: _ShowAboutDialog(); break; + case MenuOptions.SetupGodotNugetFallbackFolder: + { + try + { + string fallbackFolder = NuGetUtils.GodotFallbackFolderPath; + NuGetUtils.AddFallbackFolderToUserNuGetConfigs(NuGetUtils.GodotFallbackFolderName, fallbackFolder); + NuGetUtils.AddBundledPackagesToFallbackFolder(fallbackFolder); + } + catch (Exception e) + { + ShowErrorDialog("Failed to setup Godot NuGet Offline Packages: " + e.Message); + } + + break; + } default: throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid menu option"); } } - private void _BuildSolutionPressed() + private void BuildSolutionPressed() { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) { @@ -158,53 +175,22 @@ namespace GodotTools return; // Failed to create solution } - Instance.BottomPanel.BuildProjectPressed(); + Instance.MSBuildPanel.BuildSolution(); } - public override void _Notification(int what) + public override void _Ready() { - base._Notification(what); + base._Ready(); - if (what == NotificationReady) - { - bool showInfoDialog = (bool)editorSettings.GetSetting("mono/editor/show_info_on_start"); - if (showInfoDialog) - { - aboutDialog.Exclusive = true; - _ShowAboutDialog(); - // Once shown a first time, it can be seen again via the Mono menu - it doesn't have to be exclusive from that time on. - aboutDialog.Exclusive = false; - } - - var fileSystemDock = GetEditorInterface().GetFileSystemDock(); - - fileSystemDock.FilesMoved += (file, newFile) => - { - if (Path.GetExtension(file) == Internal.CSharpLanguageExtension) - { - ProjectUtils.RenameItemInProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(file), ProjectSettings.GlobalizePath(newFile)); - } - }; + MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged; - fileSystemDock.FileRemoved += file => - { - if (Path.GetExtension(file) == Internal.CSharpLanguageExtension) - ProjectUtils.RemoveItemFromProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(file)); - }; - - fileSystemDock.FolderMoved += (oldFolder, newFolder) => - { - ProjectUtils.RenameItemsToNewFolderInProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(oldFolder), ProjectSettings.GlobalizePath(newFolder)); - }; - - fileSystemDock.FolderRemoved += oldFolder => - { - ProjectUtils.RemoveItemsInFolderFromProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(oldFolder)); - }; + bool showInfoDialog = (bool)editorSettings.GetSetting("mono/editor/show_info_on_start"); + if (showInfoDialog) + { + aboutDialog.Exclusive = true; + _ShowAboutDialog(); + // Once shown a first time, it can be seen again via the Mono menu - it doesn't have to be exclusive from that time on. + aboutDialog.Exclusive = false; } } @@ -212,6 +198,7 @@ namespace GodotTools { CreateSln, AboutCSharp, + SetupGodotNugetFallbackFolder, } public void ShowErrorDialog(string message, string title = "Error") @@ -299,7 +286,7 @@ namespace GodotTools bool osxAppBundleInstalled = false; - if (OS.IsOSX) + if (OS.IsMacOS) { // The package path is '/Applications/Visual Studio Code.app' const string vscodeBundleId = "com.microsoft.VSCode"; @@ -339,7 +326,7 @@ namespace GodotTools string command; - if (OS.IsOSX) + if (OS.IsMacOS) { if (!osxAppBundleInstalled && string.IsNullOrEmpty(_vsCodePath)) { @@ -389,6 +376,43 @@ namespace GodotTools return BuildManager.EditorBuildCallback(); } + private void ApplyNecessaryChangesToSolution() + { + try + { + // Migrate solution from old configuration names to: Debug, ExportDebug and ExportRelease + DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath); + + var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath) + ?? throw new Exception("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.EnsureGodotSdkIsUpToDate(msbuildProject); + + if (msbuildProject.HasUnsavedChanges) + { + // Save a copy of the project before replacing it + FileUtils.SaveBackupCopy(GodotSharpDirs.ProjectCsProjPath); + + msbuildProject.Save(); + } + } + catch (Exception e) + { + GD.PushError(e.ToString()); + } + } + + private void BuildStateChanged() + { + if (bottomPanelBtn != null) + bottomPanelBtn.Icon = MSBuildPanel.BuildOutputView.BuildStateIcon; + } + public override void EnablePlugin() { base.EnablePlugin(); @@ -405,20 +429,20 @@ namespace GodotTools errorDialog = new AcceptDialog(); editorBaseControl.AddChild(errorDialog); - BottomPanel = new BottomPanel(); - - bottomPanelBtn = AddControlToBottomPanel(BottomPanel, "Mono".TTR()); + MSBuildPanel = new MSBuildPanel(); + bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR()); AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"}); menuPopup = new PopupMenu(); menuPopup.Hide(); - AddToolSubmenuItem("Mono", menuPopup); + AddToolSubmenuItem("C#", menuPopup); // TODO: Remove or edit this info dialog once Mono support is no longer in alpha { menuPopup.AddItem("About C# support".TTR(), (int)MenuOptions.AboutCSharp); + menuPopup.AddItem("Setup Godot NuGet Offline Packages".TTR(), (int)MenuOptions.SetupGodotNugetFallbackFolder); aboutDialog = new AcceptDialog(); editorBaseControl.AddChild(aboutDialog); aboutDialog.Title = "Important: C# support is not feature-complete"; @@ -466,62 +490,28 @@ namespace GodotTools aboutVBox.AddChild(aboutDialogCheckBox); } - if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath)) + toolBarBuildButton = new Button { - try - { - // Migrate solution from old configuration names to: Debug, ExportDebug and ExportRelease - DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath); - - var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath) - ?? throw new Exception("Cannot open C# project"); - - // NOTE: The order in which changes are made to the project is important - - // Migrate csproj from old configuration names to: Debug, ExportDebug and ExportRelease - ProjectUtils.MigrateFromOldConfigNames(msbuildProject); - - // Apply the other fixes only after configurations have been migrated - - // Make sure the existing project has the ProjectTypeGuids property (for VisualStudio) - ProjectUtils.EnsureHasProjectTypeGuids(msbuildProject); - - // Make sure the existing project has Api assembly references configured correctly - ProjectUtils.FixApiHintPath(msbuildProject); - - // Make sure the existing project references the Microsoft.NETFramework.ReferenceAssemblies nuget package - ProjectUtils.EnsureHasNugetNetFrameworkRefAssemblies(msbuildProject); - - if (msbuildProject.HasUnsavedChanges) - { - // Save a copy of the project before replacing it - FileUtils.SaveBackupCopy(GodotSharpDirs.ProjectCsProjPath); + Text = "Build", + HintTooltip = "Build solution", + FocusMode = Control.FocusModeEnum.None + }; + toolBarBuildButton.PressedSignal += BuildSolutionPressed; + AddControlToContainer(CustomControlContainer.Toolbar, toolBarBuildButton); - msbuildProject.Save(); - } - } - catch (Exception e) - { - GD.PushError(e.ToString()); - } + if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath)) + { + ApplyNecessaryChangesToSolution(); } else { bottomPanelBtn.Hide(); + toolBarBuildButton.Hide(); menuPopup.AddItem("Create C# solution".TTR(), (int)MenuOptions.CreateSln); } menuPopup.IdPressed += _MenuOptionPressed; - var buildButton = new Button - { - Text = "Build", - HintTooltip = "Build solution", - FocusMode = Control.FocusModeEnum.None - }; - buildButton.PressedSignal += _BuildSolutionPressed; - AddControlToContainer(CustomControlContainer.Toolbar, buildButton); - // External editor settings EditorDef("mono/editor/external_editor", ExternalEditorId.None); @@ -534,7 +524,7 @@ namespace GodotTools $",Visual Studio Code:{(int)ExternalEditorId.VsCode}" + $",JetBrains Rider:{(int)ExternalEditorId.Rider}"; } - else if (OS.IsOSX) + else if (OS.IsMacOS) { settingsHintStr += $",Visual Studio:{(int)ExternalEditorId.VisualStudioForMac}" + $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" + @@ -562,6 +552,16 @@ namespace GodotTools exportPlugin.RegisterExportSettings(); exportPluginWeak = WeakRef(exportPlugin); + try + { + // At startup we make sure NuGet.Config files have our Godot NuGet fallback folder included + NuGetUtils.AddFallbackFolderToUserNuGetConfigs(NuGetUtils.GodotFallbackFolderName, NuGetUtils.GodotFallbackFolderPath); + } + catch (Exception e) + { + GD.PushError("Failed to add Godot NuGet Offline Packages to NuGet.Config: " + e.Message); + } + BuildManager.Initialize(); RiderPathManager.Initialize(); @@ -600,6 +600,7 @@ namespace GodotTools public static GodotSharpEditor Instance { get; private set; } + [UsedImplicitly] private GodotSharpEditor() { } diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs index e4932ca217..451ce39f5c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs @@ -111,7 +111,7 @@ namespace GodotTools.Ides { MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath) { - if (Utils.OS.IsOSX && editorId == ExternalEditorId.VisualStudioForMac) + if (Utils.OS.IsMacOS && editorId == ExternalEditorId.VisualStudioForMac) { vsForMacInstance = (vsForMacInstance?.IsDisposed ?? true ? null : vsForMacInstance) ?? new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac); diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs index 17f3339560..eb34a2d0f7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs @@ -330,9 +330,10 @@ namespace GodotTools.Ides { DispatchToMainThread(() => { - GodotSharpEditor.Instance.CurrentPlaySettings = new PlaySettings(); + // TODO: Add BuildBeforePlaying flag to PlayRequest + + // Run the game Internal.EditorRunPlay(); - GodotSharpEditor.Instance.CurrentPlaySettings = null; }); return Task.FromResult<Response>(new PlayResponse()); } @@ -341,10 +342,22 @@ namespace GodotTools.Ides { DispatchToMainThread(() => { - GodotSharpEditor.Instance.CurrentPlaySettings = - new PlaySettings(request.DebuggerHost, request.DebuggerPort, request.BuildBeforePlaying ?? true); + // Tell the build callback whether the editor already built the solution or not + GodotSharpEditor.Instance.SkipBuildBeforePlaying = !(request.BuildBeforePlaying ?? true); + + // Pass the debugger agent settings to the player via an environment variables + // TODO: It would be better if this was an argument in EditorRunPlay instead + Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", + "--debugger-agent=transport=dt_socket" + + $",address={request.DebuggerHost}:{request.DebuggerPort}" + + ",server=n"); + + // Run the game Internal.EditorRunPlay(); - GodotSharpEditor.Instance.CurrentPlaySettings = null; + + // Restore normal settings + Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", ""); + GodotSharpEditor.Instance.SkipBuildBeforePlaying = false; }); return Task.FromResult<Response>(new DebugPlayResponse()); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs index d6fa2eeba7..fd7bbd5578 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs @@ -26,7 +26,7 @@ namespace GodotTools.Ides.MonoDevelop string command; - if (OS.IsOSX) + if (OS.IsMacOS) { string bundleId = BundleIds[editorId]; @@ -85,7 +85,7 @@ namespace GodotTools.Ides.MonoDevelop public Instance(string solutionFile, EditorId editorId) { - if (editorId == EditorId.VisualStudioForMac && !OS.IsOSX) + if (editorId == EditorId.VisualStudioForMac && !OS.IsMacOS) throw new InvalidOperationException($"{nameof(EditorId.VisualStudioForMac)} not supported on this platform"); this.solutionFile = solutionFile; @@ -103,7 +103,7 @@ namespace GodotTools.Ides.MonoDevelop static Instance() { - if (OS.IsOSX) + if (OS.IsMacOS) { ExecutableNames = new Dictionary<EditorId, string> { diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs index e22e9af919..94fc5da425 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs @@ -32,7 +32,7 @@ namespace GodotTools.Ides.Rider { return CollectRiderInfosWindows(); } - if (OS.IsOSX) + if (OS.IsMacOS) { return CollectRiderInfosMac(); } @@ -138,7 +138,7 @@ namespace GodotTools.Ides.Rider return GetToolboxRiderRootPath(localAppData); } - if (OS.IsOSX) + if (OS.IsMacOS) { var home = Environment.GetEnvironmentVariable("HOME"); if (string.IsNullOrEmpty(home)) @@ -211,7 +211,7 @@ namespace GodotTools.Ides.Rider { if (OS.IsWindows || OS.IsUnixLike) return "../../build.txt"; - if (OS.IsOSX) + if (OS.IsMacOS) return "Contents/Resources/build.txt"; throw new Exception("Unknown OS."); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs index 569f27649f..c72a84c513 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs @@ -15,6 +15,10 @@ namespace GodotTools.Internals public bool Nested { get; } public long BaseCount { get; } + public string SearchName => Nested ? + Name.Substring(Name.LastIndexOf(".", StringComparison.Ordinal) + 1) : + Name; + public ClassDecl(string name, string @namespace, bool nested, long baseCount) { Name = name; diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index 6c05891f2c..e745966435 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -21,7 +21,7 @@ namespace GodotTools.Utils public static class Names { public const string Windows = "Windows"; - public const string OSX = "OSX"; + public const string MacOS = "macOS"; public const string Linux = "Linux"; public const string FreeBSD = "FreeBSD"; public const string NetBSD = "NetBSD"; @@ -37,7 +37,7 @@ namespace GodotTools.Utils public static class Platforms { public const string Windows = "windows"; - public const string OSX = "osx"; + public const string MacOS = "osx"; public const string LinuxBSD = "linuxbsd"; public const string Server = "server"; public const string UWP = "uwp"; @@ -50,7 +50,7 @@ namespace GodotTools.Utils public static readonly Dictionary<string, string> PlatformNameMap = new Dictionary<string, string> { [Names.Windows] = Platforms.Windows, - [Names.OSX] = Platforms.OSX, + [Names.MacOS] = Platforms.MacOS, [Names.Linux] = Platforms.LinuxBSD, [Names.FreeBSD] = Platforms.LinuxBSD, [Names.NetBSD] = Platforms.LinuxBSD, @@ -77,11 +77,11 @@ namespace GodotTools.Utils new[] {Names.Linux, Names.FreeBSD, Names.NetBSD, Names.BSD}; private static readonly IEnumerable<string> UnixLikePlatforms = - new[] {Names.OSX, Names.Server, Names.Haiku, Names.Android, Names.iOS} + new[] {Names.MacOS, Names.Server, 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> _isOSX = new Lazy<bool>(() => IsOS(Names.OSX)); + 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)); @@ -92,7 +92,7 @@ namespace GodotTools.Utils private static readonly Lazy<bool> _isUnixLike = new Lazy<bool>(() => IsAnyOS(UnixLikePlatforms)); public static bool IsWindows => _isWindows.Value || IsUWP; - public static bool IsOSX => _isOSX.Value; + public static bool IsMacOS => _isMacOS.Value; public static bool IsLinuxBSD => _isLinuxBSD.Value; public static bool IsServer => _isServer.Value; public static bool IsUWP => _isUWP.Value; |