From c3954441f360b46546e8a8deacfc42460e93987d Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Mon, 20 Jul 2020 15:48:12 +0200 Subject: [PATCH] 3.2 New csproj style with backport of Godot.NET.Sdk This is a cherry-pick of ced77b1e9b5ffe0eb66de3d730d8583d12366c91 with several 3.2 specific alterations. There are a lot of build issues coming from old style projects. At this point fixing every single one of those would require adding patch after patch to the project file, which is a considerable amount work and makes the csproj even more bloated than it already is. As such I decided this effort would be better spent back-porting the Sdk style support that's already available in 4.0-dev to the 3.2 branch. This will prevent many issues, but it will also introduce other benefits, among them: - While target framework stays as .NET Framework v4.7.2, it can be changed to .NET Standard 2.0 or greater if desired. - It makes it much easier to add future patches. They are added to Godot.NET.Sdk and the only change required in Godot code is to update the Sdk version to use. - Default Godot define constants are also backported, which fixes IDE issues with the preprocessor. There are a few differences in the changes applied during patching of the csproj compared to 4.0 with the purpose of preventing breaking builds: - 'TargetFramework' stays net472 both for new projects and when importing old ones. It can be manually changed to netstandard 2.0+ if desired though. The following features are enabled by default for new projects. Enabling them in imported projects may result in errors that must be fixed manually: - 'EnableDefaultCompileItems' is disabled as it can result in undesired C# source files being included. Existing include items are kept. As long as 'EnableDefaultCompileItems' remains disabled, Godot will continue taking care of adding and removing C# files to the csproj. - 'GenerateAssemblyInfo' is disabled as it guarantees a build error because of conflicts between the existing 'AssemblyInfo.cs' and the auto-generated one. - 'Deterministic' is disabled because it doesn't like wildcards in the assembly version (1.0.*) that was in the old 'AssemblyInfo.cs'. Of importance: This is a breaking change. A great effort was put in avoiding build errors after upgrading a project, but there may still be exceptions. This also breaks forward compatibility. Projects opened with Godot 3.2.3 won't work out of the box with older Godot versions. This was already the case with changes introduced in 3.2.2. Albeit C# support in 3.2.x was still labeled as alpha, we've been trying to treat it as stable for some time. Still the amount of problems this change solves justifies it, but no more changes that break project compatibility are to be introduced from now on (at least for 3.x). --- .editorconfig | 4 + .../editor/Godot.NET.Sdk/Godot.NET.Sdk.sln | 16 + .../Godot.NET.Sdk/Godot.NET.Sdk.csproj | 35 ++ .../Godot.NET.Sdk/Godot.NET.Sdk.nuspec | 22 + .../Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props | 119 +++++ .../Godot.NET.Sdk/Sdk/Sdk.targets | 34 ++ .../GodotTools/GodotTools.Core/FileUtils.cs | 5 +- .../GodotTools.ProjectEditor.csproj | 1 + .../IdentifierUtils.cs | 56 ++- .../ProjectExtensions.cs | 16 + .../ProjectGenerator.cs | 171 +------ .../GodotTools.ProjectEditor/ProjectUtils.cs | 436 +++++++++--------- .../GodotTools/GodotTools/BottomPanel.cs | 82 ++-- .../GodotTools/GodotTools/BuildManager.cs | 30 +- .../GodotTools/GodotTools/CsProjOperations.cs | 138 +++--- .../GodotTools/Export/ExportPlugin.cs | 24 +- .../GodotTools/GodotTools/GodotSharpEditor.cs | 68 ++- .../GodotTools/Internals/ScriptClassParser.cs | 4 + modules/mono/editor/bindings_generator.cpp | 1 - .../GodotSharp/GodotSharp/GodotSharp.csproj | 35 +- .../GodotSharp/Properties/AssemblyInfo.cs | 24 - .../GodotSharpEditor/GodotSharpEditor.csproj | 48 +- .../Properties/AssemblyInfo.cs | 25 - 23 files changed, 712 insertions(+), 682 deletions(-) create mode 100644 modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln create mode 100644 modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj create mode 100644 modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec create mode 100644 modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props create mode 100644 modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets delete mode 100644 modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs diff --git a/.editorconfig b/.editorconfig index f335026e1e3..49517a5104d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,3 +20,7 @@ indent_size = 4 [.travis.yml] indent_style = space indent_size = 2 + +[*.{csproj,props,targets,nuspec}] +indent_style = space +indent_size = 2 diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln new file mode 100644 index 00000000000..56c0cb77037 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.NET.Sdk", "Godot.NET.Sdk\Godot.NET.Sdk.csproj", "{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj new file mode 100644 index 00000000000..81a2172b911 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj @@ -0,0 +1,35 @@ + + + netstandard2.0 + + MSBuild .NET Sdk for Godot projects. + Godot Engine contributors + + Godot.NET.Sdk + 3.2.3 + 3.2.3 + https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk + MSBuildSdk + MSBuildSdk + true + + + + Godot.NET.Sdk.nuspec + $(GenerateNuspecDependsOn);SetNuSpecProperties + + + + + + id=$(PackageId); + description=$(Description); + authors=$(Authors); + version=$(PackageVersion); + packagetype=$(PackageType); + tags=$(PackageTags); + projecturl=$(PackageProjectUrl) + + + + diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec new file mode 100644 index 00000000000..5b5cefe80e5 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec @@ -0,0 +1,22 @@ + + + + $id$ + $version$ + $description$ + $authors$ + $authors$ + $projecturl$ + false + MIT + https://licenses.nuget.org/MIT + $tags$ + + + + + + + \ + + diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props new file mode 100644 index 00000000000..2422c5c2fbd --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props @@ -0,0 +1,119 @@ + + + + true + + {8F3E2DF0-C35C-4265-82FC-BEA011F4A7ED} + + + + Debug;ExportDebug;ExportRelease + Debug + + $(SolutionDir) + $(MSBuildProjectDirectory) + $([MSBuild]::EnsureTrailingSlash('$(GodotProjectDir)')) + + + $(GodotProjectDir).mono\temp\bin\ + $(GodotProjectDir).mono\temp\bin\$(Configuration)\ + + $(GodotProjectDir).mono\temp\obj\$(Configuration)\ + $(GodotProjectDir).mono\temp\obj\ + + + false + + + + + + false + + + + + true + false + + + true + + + + Debug + Release + + + + + x11 + x11 + osx + windows + + + + false + + + + + + GODOT + + + GODOT_WINDOWS;GODOT_PC + GODOT_X11;GODOT_PC + GODOT_OSX;GODOT_MACOS;GODOT_PC + GODOT_SERVER;GODOT_PC + GODOT_UWP;GODOT_PC + GODOT_HAIKU;GODOT_PC + GODOT_ANDROID;GODOT_MOBILE + GODOT_IPHONE;GODOT_IOS;GODOT_MOBILE + GODOT_JAVASCRIPT;GODOT_HTML5;GODOT_WASM;GODOT_WEB + + $(GodotDefineConstants);$(GodotPlatformConstants) + + + + + $(DefineConstants);DEBUG + + $(DefineConstants);TOOLS + + $(GodotDefineConstants);$(DefineConstants) + + + + + + false + $(GodotProjectDir).mono\assemblies\$(GodotApiConfiguration)\GodotSharp.dll + + + false + $(GodotProjectDir).mono\assemblies\$(GodotApiConfiguration)\GodotSharpEditor.dll + + + + + + true + 1.0.0 + true + + diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets new file mode 100644 index 00000000000..b9c17bad1e2 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets @@ -0,0 +1,34 @@ + + + + + true + $(GodotProjectTypeGuid);$(DefaultProjectTypeGuid) + + + + + GODOT_REAL_T_IS_DOUBLE;$(DefineConstants) + + + + + + + + + + + + + diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs b/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs index 85760a3705e..e1ccf0454a8 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.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index 0ca3177c78e..8ed25ccf3a2 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -7,6 +7,7 @@ + diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs index f93eb9a1faf..ed77076df3d 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); } + /// + /// Skips invalid identifier characters including decimal digit numbers at the start of the identifier. + /// + 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 index 14e2ca2c13a..472ad2530cc 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs @@ -2,6 +2,7 @@ using GodotTools.Core; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Microsoft.Build.Construction; using Microsoft.Build.Globbing; @@ -32,6 +33,7 @@ namespace GodotTools.ProjectEditor return null; } + public static ProjectItemElement FindItemOrNullAbs(this ProjectRootElement root, string itemType, string include, bool noCondition = false) { string normalizedInclude = Path.GetFullPath(include).NormalizePath(); @@ -113,5 +115,19 @@ namespace GodotTools.ProjectEditor return Guid.Empty; } + + public static bool AreDefaultCompileItemsEnabled(this ProjectRootElement root) + { + var enableDefaultCompileItemsProps = root.PropertyGroups + .Where(g => string.IsNullOrEmpty(g.Condition)) + .SelectMany(g => g.Properties + .Where(p => p.Name == "EnableDefaultCompileItems" && string.IsNullOrEmpty(p.Condition))); + + bool enableDefaultCompileItems = true; + foreach (var prop in enableDefaultCompileItemsProps) + enableDefaultCompileItems = prop.Value.Equals("true", StringComparison.OrdinalIgnoreCase); + + return enableDefaultCompileItems; + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index 679d5bb444f..99098ca7bc0 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -1,174 +1,49 @@ -using GodotTools.Core; using System; -using System.Collections.Generic; using System.IO; -using System.Reflection; using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; namespace GodotTools.ProjectEditor { public static class ProjectGenerator { - private const string CoreApiProjectName = "GodotSharp"; - private const string EditorApiProjectName = "GodotSharpEditor"; + public const string GodotSdkVersionToUse = "3.2.3"; - public const string CSharpProjectTypeGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; - public const string GodotProjectTypeGuid = "{8F3E2DF0-C35C-4265-82FC-BEA011F4A7ED}"; + public static string GodotSdkAttrValue => $"Godot.NET.Sdk/{GodotSdkVersionToUse}"; - public static readonly string GodotDefaultProjectTypeGuids = $"{GodotProjectTypeGuid};{CSharpProjectTypeGuid}"; - - public static string GenGameProject(string dir, string name, IEnumerable compileItems) + public static ProjectRootElement GenGameProject(string name) { - string path = Path.Combine(dir, name + ".csproj"); + if (name.Length == 0) + throw new ArgumentException("Project name is empty", nameof(name)); - ProjectPropertyGroupElement mainGroup; - var root = CreateLibraryProject(name, "Debug", out mainGroup); + var root = ProjectRootElement.Create(NewProjectFileOptions.None); - 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' "; + root.Sdk = GodotSdkAttrValue; - 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 mainGroup = root.AddPropertyGroup(); + mainGroup.AddProperty("TargetFramework", "net472"); - var coreApiRef = root.AddItem("Reference", CoreApiProjectName); - coreApiRef.AddMetadata("HintPath", Path.Combine("$(ProjectDir)", ".mono", "assemblies", "$(ApiConfiguration)", CoreApiProjectName + ".dll")); - coreApiRef.AddMetadata("Private", "False"); + string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true); - 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) - { - string propertiesDir = Path.Combine(dir, "Properties"); - if (!Directory.Exists(propertiesDir)) - Directory.CreateDirectory(propertiesDir); - - string usingDirectivesText = string.Empty; - - if (usingDirectives != null) - { - foreach (var usingDirective in usingDirectives) - usingDirectivesText += "\nusing " + usingDirective + ";"; - } - - string assemblyLinesText = string.Empty; - - if (assemblyLines != null) - assemblyLinesText += string.Join("\n", assemblyLines) + "\n"; - - string content = string.Format(AssemblyInfoTemplate, usingDirectivesText, name, assemblyLinesText); - - string assemblyInfoFile = Path.Combine(propertiesDir, "AssemblyInfo.cs"); - - File.WriteAllText(assemblyInfoFile, content); - - root.AddItem("Compile", assemblyInfoFile.RelativeToPath(dir).Replace("/", "\\")); - } - - public static ProjectRootElement CreateLibraryProject(string name, string defaultConfig, out ProjectPropertyGroupElement mainGroup) - { - 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()); - - 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"); - - // 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.AddImport(Path.Combine("$(MSBuildBinPath)", "Microsoft.CSharp.targets").Replace("/", "\\")); + // If the name is not a valid namespace, manually set RootNamespace to a sanitized one. + if (sanitizedName != name) + mainGroup.AddProperty("RootNamespace", sanitizedName); return root; } - private const string AssemblyInfoTemplate = - @"using System.Reflection;{0} + public static string GenAndSaveGameProject(string dir, string name) + { + if (name.Length == 0) + throw new ArgumentException("Project name is empty", nameof(name)); -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. + string path = Path.Combine(dir, name + ".csproj"); -[assembly: AssemblyTitle(""{1}"")] -[assembly: AssemblyDescription("""")] -[assembly: AssemblyConfiguration("""")] -[assembly: AssemblyCompany("""")] -[assembly: AssemblyProduct("""")] -[assembly: AssemblyCopyright("""")] -[assembly: AssemblyTrademark("""")] -[assembly: AssemblyCulture("""")] + var root = GenGameProject(name); -// 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. + root.Save(path); -[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}"; + return Guid.NewGuid().ToString().ToUpper(); + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index 8774b4ee310..5b04c8b8131 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -1,9 +1,12 @@ +using System; using GodotTools.Core; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; +using System.Xml; +using System.Xml.Linq; +using JetBrains.Annotations; using Microsoft.Build.Construction; using Microsoft.Build.Globbing; @@ -11,7 +14,7 @@ namespace GodotTools.ProjectEditor { public sealed class MSBuildProject { - public ProjectRootElement Root { get; } + internal ProjectRootElement Root { get; set; } public bool HasUnsavedChanges { get; set; } @@ -31,12 +34,20 @@ namespace GodotTools.ProjectEditor return root != null ? new MSBuildProject(root) : null; } + [PublicAPI] 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); + if (root.AreDefaultCompileItemsEnabled()) + { + // No need to add. It's already included automatically by the MSBuild Sdk. + // This assumes the source file is inside the project directory and not manually excluded in the csproj + return; + } + var normalizedInclude = include.RelativeToPath(dir).Replace("/", "\\"); if (root.AddItemChecked(itemType, normalizedInclude)) @@ -49,6 +60,13 @@ namespace GodotTools.ProjectEditor var root = ProjectRootElement.Open(projectPath); Debug.Assert(root != null); + if (root.AreDefaultCompileItemsEnabled()) + { + // No need to add. It's already included automatically by the MSBuild Sdk. + // This assumes the source file is inside the project directory and not manually excluded in the csproj + return; + } + var normalizedOldInclude = oldInclude.NormalizePath(); var normalizedNewInclude = newInclude.NormalizePath(); @@ -66,6 +84,13 @@ namespace GodotTools.ProjectEditor var root = ProjectRootElement.Open(projectPath); Debug.Assert(root != null); + if (root.AreDefaultCompileItemsEnabled()) + { + // No need to add. It's already included automatically by the MSBuild Sdk. + // This assumes the source file is inside the project directory and not manually excluded in the csproj + return; + } + var normalizedInclude = include.NormalizePath(); if (root.RemoveItemChecked(itemType, normalizedInclude)) @@ -78,6 +103,13 @@ namespace GodotTools.ProjectEditor var root = ProjectRootElement.Open(projectPath); Debug.Assert(root != null); + if (root.AreDefaultCompileItemsEnabled()) + { + // No need to add. It's already included automatically by the MSBuild Sdk. + // This assumes the source file is inside the project directory and not manually excluded in the csproj + return; + } + bool dirty = false; var oldFolderNormalized = oldFolder.NormalizePath(); @@ -102,6 +134,13 @@ namespace GodotTools.ProjectEditor var root = ProjectRootElement.Open(projectPath); Debug.Assert(root != null); + if (root.AreDefaultCompileItemsEnabled()) + { + // No need to add. It's already included automatically by the MSBuild Sdk. + // This assumes the source file is inside the project directory and not manually excluded in the csproj + return; + } + var folderNormalized = folder.NormalizePath(); var itemsToRemove = root.FindAllItemsInFolder(itemType, folderNormalized).ToList(); @@ -136,6 +175,29 @@ namespace GodotTools.ProjectEditor var root = ProjectRootElement.Open(projectPath); Debug.Assert(root != null); + if (root.AreDefaultCompileItemsEnabled()) + { + var excluded = new List(); + result = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs").ToList(); + + foreach (var item in root.Items) + { + if (string.IsNullOrEmpty(item.Condition)) + continue; + + if (item.ItemType != itemType) + continue; + + string normalizedExclude = item.Exclude.NormalizePath(); + + var glob = MSBuildGlob.Parse(normalizedExclude); + + excluded.AddRange(result.Where(includedFile => glob.IsMatch(includedFile))); + } + + result.RemoveAll(f => excluded.Contains(f)); + } + foreach (var itemGroup in root.ItemGroups) { if (itemGroup.Condition.Length != 0) @@ -150,8 +212,6 @@ namespace GodotTools.ProjectEditor var glob = MSBuildGlob.Parse(normalizedInclude); - // TODO Check somehow if path has no blob to avoid the following loop... - foreach (var existingFile in existingFiles) { if (glob.IsMatch(existingFile)) @@ -165,222 +225,186 @@ namespace GodotTools.ProjectEditor return result.ToArray(); } - public static void EnsureHasProjectTypeGuids(MSBuildProject project) + public static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName) { var root = project.Root; - bool found = root.PropertyGroups.Any(pg => - string.IsNullOrEmpty(pg.Condition) && pg.Properties.Any(p => p.Name == "ProjectTypeGuids")); - - if (found) + if (!string.IsNullOrEmpty(root.Sdk)) return; - root.AddProperty("ProjectTypeGuids", ProjectGenerator.GodotDefaultProjectTypeGuids); + root.Sdk = ProjectGenerator.GodotSdkAttrValue; + + root.ToolsVersion = null; + root.DefaultTargets = null; + + root.AddProperty("TargetFramework", "net472"); + + // Remove obsolete properties, items and elements. We're going to be conservative + // here to minimize the chances of introducing breaking changes. As such we will + // only remove elements that could potentially cause issues with the Godot.NET.Sdk. + + void RemoveElements(IEnumerable elements) + { + foreach (var element in elements) + element.Parent.RemoveChild(element); + } + + // Default Configuration + + RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties) + .Where(p => p.Name == "Configuration" && p.Condition.Trim() == "'$(Configuration)' == ''" && p.Value == "Debug")); + + // Default Platform + + RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties) + .Where(p => p.Name == "Platform" && p.Condition.Trim() == "'$(Platform)' == ''" && p.Value == "AnyCPU")); + + // Simple properties + + var yabaiProperties = new[] + { + "OutputPath", + "BaseIntermediateOutputPath", + "IntermediateOutputPath", + "TargetFrameworkVersion", + "ProjectTypeGuids", + "ApiConfiguration" + }; + + RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties) + .Where(p => yabaiProperties.Contains(p.Name))); + + // Configuration dependent properties + + var yabaiPropertiesForConfigs = new[] + { + "DebugSymbols", + "DebugType", + "Optimize", + "DefineConstants", + "ErrorReport", + "WarningLevel", + "ConsolePause" + }; + + foreach (var config in new[] {"ExportDebug", "ExportRelease", "Debug"}) + { + var group = root.PropertyGroups + .First(g => g.Condition.Trim() == $"'$(Configuration)|$(Platform)' == '{config}|AnyCPU'"); + + RemoveElements(group.Properties.Where(p => yabaiPropertiesForConfigs.Contains(p.Name))); + + if (group.Count == 0) + { + // No more children, safe to delete the group + group.Parent.RemoveChild(group); + } + } + + // Godot API References + + var apiAssemblies = new[] {ApiAssemblyNames.Core, ApiAssemblyNames.Editor}; + + RemoveElements(root.ItemGroups.SelectMany(g => g.Items) + .Where(i => i.ItemType == "Reference" && apiAssemblies.Contains(i.Include))); + + // Microsoft.NETFramework.ReferenceAssemblies PackageReference + + RemoveElements(root.ItemGroups.SelectMany(g => g.Items).Where(i => + i.ItemType == "PackageReference" && + i.Include.Equals("Microsoft.NETFramework.ReferenceAssemblies", StringComparison.OrdinalIgnoreCase))); + + // Imports + + var yabaiImports = new[] + { + "$(MSBuildBinPath)/Microsoft.CSharp.targets", + "$(MSBuildBinPath)Microsoft.CSharp.targets" + }; + + RemoveElements(root.Imports.Where(import => yabaiImports.Contains( + import.Project.Replace("\\", "/").Replace("//", "/")))); + + // 'EnableDefaultCompileItems' and 'GenerateAssemblyInfo' are kept enabled by default + // on new projects, but when migrating old projects we disable them to avoid errors. + root.AddProperty("EnableDefaultCompileItems", "false"); + root.AddProperty("GenerateAssemblyInfo", "false"); + + // Older AssemblyInfo.cs cause the following error: + // 'Properties/AssemblyInfo.cs(19,28): error CS8357: + // The specified version string contains wildcards, which are not compatible with determinism. + // Either remove wildcards from the version string, or disable determinism for this compilation.' + // We disable deterministic builds to prevent this. The user can then fix this manually when desired + // by fixing 'AssemblyVersion("1.0.*")' to not use wildcards. + root.AddProperty("Deterministic", "false"); project.HasUnsavedChanges = true; + + var xDoc = XDocument.Parse(root.RawXml); + + if (xDoc.Root == null) + return; // Too bad, we will have to keep the xmlns/namespace and xml declaration + + XElement GetElement(XDocument doc, string name, string value, string parentName) + { + foreach (var node in doc.DescendantNodes()) + { + if (!(node is XElement element)) + continue; + if (element.Name.LocalName.Equals(name) && element.Value == value && + element.Parent != null && element.Parent.Name.LocalName.Equals(parentName)) + { + return element; + } + } + + return null; + } + + // Add comment about Microsoft.NET.Sdk properties disabled during migration + + GetElement(xDoc, name: "EnableDefaultCompileItems", value: "false", parentName: "PropertyGroup") + .AddBeforeSelf(new XComment("The following properties were overriden during migration to prevent errors.\n" + + " Enabling them may require other manual changes to the project and its files.")); + + void RemoveNamespace(XElement element) + { + element.Attributes().Where(x => x.IsNamespaceDeclaration).Remove(); + element.Name = element.Name.LocalName; + + foreach (var node in element.DescendantNodes()) + { + if (node is XElement xElement) + { + // Need to do the same for all children recursively as it adds it to them for some reason... + RemoveNamespace(xElement); + } + } + } + + // Remove xmlns/namespace + RemoveNamespace(xDoc.Root); + + // Remove xml declaration + xDoc.Nodes().FirstOrDefault(node => node.NodeType == XmlNodeType.XmlDeclaration)?.Remove(); + + string projectFullPath = root.FullPath; + + root = ProjectRootElement.Create(xDoc.CreateReader()); + root.FullPath = projectFullPath; + + project.Root = root; } - /// Simple function to make sure the Api assembly references are configured correctly - public static void FixApiHintPath(MSBuildProject project) + public static void EnsureGodotSdkIsUpToDate(MSBuildProject project) { var root = project.Root; + string godotSdkAttrValue = ProjectGenerator.GodotSdkAttrValue; - 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) - { - var root = project.Root; - - 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/BottomPanel.cs b/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs index 472498acfcf..709cae70b7d 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs @@ -24,48 +24,50 @@ namespace GodotTools private ToolButton errorsBtn; private Button viewLogBtn; + private void _UpdateBuildTab(int index, int? currentTab) + { + var tab = (BuildTab)buildTabs.GetChild(index); + + 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(index, itemTooltip); + + // If this tab was already selected before the changes or if no tab was selected + if (currentTab == null || currentTab == index) + { + buildTabsList.Select(index); + _BuildTabsItemSelected(index); + } + } + private void _UpdateBuildTabsList() { buildTabsList.Clear(); - int currentTab = buildTabs.CurrentTab; + int? currentTab = buildTabs.CurrentTab; - bool noCurrentTab = currentTab < 0 || currentTab >= buildTabs.GetTabCount(); + if (currentTab < 0 || currentTab >= buildTabs.GetTabCount()) + currentTab = null; 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); - } - } + _UpdateBuildTab(i, currentTab); } public BuildTab GetBuildTabFor(BuildInfo buildInfo) @@ -160,13 +162,7 @@ namespace GodotTools } } - var godotDefines = new[] - { - OS.GetName(), - Internal.GodotIs32Bits() ? "32" : "64" - }; - - bool buildSuccess = BuildManager.BuildProjectBlocking("Debug", godotDefines); + bool buildSuccess = BuildManager.BuildProjectBlocking("Debug"); if (!buildSuccess) return; @@ -272,7 +268,7 @@ namespace GodotTools }; panelTabs.AddChild(panelBuildsTab); - var toolBarHBox = new HBoxContainer { SizeFlagsHorizontal = (int)SizeFlags.ExpandFill }; + var toolBarHBox = new HBoxContainer {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill}; panelBuildsTab.AddChild(toolBarHBox); var buildProjectBtn = new Button diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs index d63ccd28c5b..8cf43875afd 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs @@ -6,6 +6,7 @@ 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; @@ -152,7 +153,7 @@ namespace GodotTools } } - public static bool BuildProjectBlocking(string config, IEnumerable godotDefines) + public static bool BuildProjectBlocking(string config, [CanBeNull] string platform = null) { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) return true; // No solution to build @@ -168,29 +169,18 @@ 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, 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 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()) - constants += "GODOT_REAL_T_IS_DOUBLE;"; - - constants += !escapeNeedsDoubleBackslash ? "\"" : "\\\""; - - buildInfo.CustomProperties.Add(constants); + buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); if (!Build(buildInfo)) { @@ -233,13 +223,7 @@ namespace GodotTools return true; // Requested play from an external editor/IDE which already built the project } - var godotDefines = new[] - { - Godot.OS.GetName(), - Internal.GodotIs32Bits() ? "32" : "64" - }; - - return BuildProjectBlocking("Debug", godotDefines); + return BuildProjectBlocking("Debug"); } public static void Initialize() diff --git a/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs b/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs index 421729cc110..a8afb387282 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; } + private static bool TryParseFileMetadata(string includeFile, ulong modifiedTime, out Dictionary fileMetadata) + { + fileMetadata = null; + + var parseError = ScriptClassParser.ParseFile(includeFile, out var classes, out string errorStr); + + if (parseError != Error.Ok) + { + GD.PushError($"Failed to determine namespace and class for script: {includeFile}. Parse error: {errorStr ?? parseError.ToString()}"); + return false; + } + + string searchName = System.IO.Path.GetFileNameWithoutExtension(includeFile); + + 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 + + fileMetadata = new Dictionary + { + ["modified_time"] = $"{modifiedTime}", + ["class"] = new Dictionary + { + ["namespace"] = firstMatch.Namespace, + ["class_name"] = firstMatch.Name, + ["nested"] = firstMatch.Nested + } + }; + + return true; + } + public static void GenerateScriptsMetadata(string projectPath, string outputPath) { - if (File.Exists(outputPath)) - File.Delete(outputPath); + var metadataDict = Internal.GetScriptsMetadataOrNothing().Duplicate(); - var oldDict = Internal.GetScriptsMetadataOrNothing(); - var newDict = new Godot.Collections.Dictionary(); - - foreach (var includeFile in ProjectUtils.GetIncludeFiles(projectPath, "Compile")) + bool IsUpToDate(string includeFile, ulong modifiedTime) { - string projectIncludeFile = ("res://" + includeFile).SimplifyGodotPath(); - - ulong modifiedTime = File.GetLastWriteTime(projectIncludeFile).ConvertToTimestamp(); - - 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; - } - } - } - - Error parseError = ScriptClassParser.ParseFile(projectIncludeFile, out var classes, out string errorStr); - if (parseError != Error.Ok) - { - GD.PushError($"Failed to determine namespace and class for script: {projectIncludeFile}. Parse error: {errorStr ?? parseError.ToString()}"); - continue; - } - - string searchName = System.IO.Path.GetFileNameWithoutExtension(projectIncludeFile); - - var classDict = new Dictionary(); - - foreach (var classDecl in classes) - { - if (classDecl.BaseCount == 0) - continue; // Does not inherit nor implement anything, so it can't be a script class - - string classCmp = classDecl.Nested ? - classDecl.Name.Substring(classDecl.Name.LastIndexOf(".", StringComparison.Ordinal) + 1) : - classDecl.Name; - - if (classCmp != searchName) - continue; - - classDict["namespace"] = classDecl.Namespace; - classDict["class_name"] = classDecl.Name; - classDict["nested"] = classDecl.Nested; - break; - } - - if (classDict.Count == 0) - continue; // Not found - - newDict[projectIncludeFile] = new Dictionary { ["modified_time"] = $"{modifiedTime}", ["class"] = classDict }; + return metadataDict.TryGetValue(includeFile, out var oldFileVar) && + ulong.TryParse(((Dictionary)oldFileVar)["modified_time"] as string, + out ulong storedModifiedTime) && storedModifiedTime == modifiedTime; } - if (newDict.Count > 0) + 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(); + + foreach (var pair in outdatedFiles) { - string json = JSON.Print(newDict); + metadataDict.Remove(pair.Key); - string baseDir = outputPath.GetBaseDir(); + string includeFile = pair.Key; - if (!Directory.Exists(baseDir)) - Directory.CreateDirectory(baseDir); - - File.WriteAllText(outputPath, json); + if (TryParseFileMetadata(includeFile, modifiedTime: pair.Value, out var fileMetadata)) + metadataDict[includeFile] = fileMetadata; } + + string json = metadataDict.Count <= 0 ? "{}" : JSON.Print(metadataDict); + + string baseDir = outputPath.GetBaseDir(); + + if (!Directory.Exists(baseDir)) + Directory.CreateDirectory(baseDir); + + File.WriteAllText(outputPath, json); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index 2ceb4888a23..c98bdef9426 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Runtime.CompilerServices; using GodotTools.Core; using GodotTools.Internals; +using JetBrains.Annotations; using static GodotTools.Internals.Globals; using Directory = GodotTools.Utils.Directory; using File = GodotTools.Utils.File; @@ -145,9 +146,7 @@ namespace GodotTools.Export 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 ?? @@ -160,10 +159,7 @@ namespace GodotTools.Export AddFile(scriptsMetadataPath, scriptsMetadataPath); - // Turn export features into defines - var godotDefines = features; - - if (!BuildManager.BuildProjectBlocking(buildConfig, godotDefines)) + if (!BuildManager.BuildProjectBlocking(buildConfig, platform)) throw new Exception("Failed to build project"); // Add dependency assemblies @@ -289,6 +285,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 +340,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.OSX, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5}.Contains(platform); } - private static string DeterminePlatformFromFeatures(IEnumerable features) + private static bool DeterminePlatformFromFeatures(IEnumerable 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 +389,7 @@ namespace GodotTools.Export /// 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. diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 747db262046..132cd8c477d 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -403,6 +403,37 @@ 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()); + } + } + public override void EnablePlugin() { base.EnablePlugin(); @@ -478,42 +509,7 @@ namespace GodotTools if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath)) { - 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); - - msbuildProject.Save(); - } - } - catch (Exception e) - { - GD.PushError(e.ToString()); - } + ApplyNecessaryChangesToSolution(); } else { diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs index 7fb087467f4..0f46f76b41e 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 int BaseCount { get; } + public string SearchName => Nested ? + Name.Substring(Name.LastIndexOf(".", StringComparison.Ordinal) + 1) : + Name; + public ClassDecl(string name, string @namespace, bool nested, int baseCount) { Name = name; diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index 9beadb1778c..52217a3ee1f 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -45,7 +45,6 @@ #include "../mono_gd/gd_mono_marshal.h" #include "../utils/path_utils.h" #include "../utils/string_utils.h" -#include "csharp_project.h" #define CS_INDENT " " // 4 whitespaces diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index 04ee3d987ae..bab3d7cb0e2 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -1,38 +1,16 @@ - - + - Debug - AnyCPU {AEBF0036-DA76-4341-B651-A3F2856AB2FA} - Library bin/$(Configuration) + false Godot - GodotSharp - v4.5 + netstandard2.0 $(OutputPath)/$(AssemblyName).xml - obj + false - - true - portable - false - $(GodotDefineConstants);GODOT;DEBUG; - prompt - 4 - false + + $(DefineConstants);GODOT - - portable - true - $(GodotDefineConstants);GODOT; - prompt - 4 - false - - - - - @@ -82,5 +60,4 @@ Fortunately code completion, go to definition and such still work. --> - diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs b/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs index f84e0183f68..da6f2938710 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs @@ -1,27 +1,3 @@ -using System.Reflection; using System.Runtime.CompilerServices; -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("GodotSharp")] -[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("")] [assembly: InternalsVisibleTo("GodotSharpEditor")] diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj index 87859313124..03465a0d5d5 100644 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj @@ -1,46 +1,26 @@ - - + - Debug - AnyCPU {8FBEC238-D944-4074-8548-B3B524305905} - Library bin/$(Configuration) + false Godot - GodotSharpEditor - v4.5 + netstandard2.0 $(OutputPath)/$(AssemblyName).xml - obj + false - - true - portable - false - $(GodotDefineConstants);GODOT;DEBUG; - prompt - 4 - false + + $(DefineConstants);GODOT - - portable - true - $(GodotDefineConstants);GODOT; - prompt - 4 - false - - - - - - - - - - False + false - + + diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs b/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs deleted file mode 100644 index 3684b7a3cb8..00000000000 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Reflection; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("GodotSharpEditor")] -[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("")]