diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index b08efd0964c..157e2bac79c 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -11,6 +11,7 @@ + diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index dd74a0d5233..e969f31c8bc 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -12,6 +12,8 @@ namespace GodotTools.ProjectEditor { public static string GodotSdkAttrValue => $"Godot.NET.Sdk/{GeneratedGodotNupkgsVersions.GodotNETSdk}"; + public static string GodotMinimumRequiredTfm => "net8.0"; + public static ProjectRootElement GenGameProject(string name) { if (name.Length == 0) @@ -22,7 +24,7 @@ namespace GodotTools.ProjectEditor root.Sdk = GodotSdkAttrValue; var mainGroup = root.AddPropertyGroup(); - mainGroup.AddProperty("TargetFramework", "net8.0"); + mainGroup.AddProperty("TargetFramework", GodotMinimumRequiredTfm); mainGroup.AddProperty("EnableDynamicLoading", "true"); diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index 794443a69c1..7a07e0a9753 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -1,7 +1,11 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; using Microsoft.Build.Locator; +using NuGet.Frameworks; namespace GodotTools.ProjectEditor { @@ -19,8 +23,21 @@ namespace GodotTools.ProjectEditor } } - public static class ProjectUtils + public static partial class ProjectUtils { + [GeneratedRegex(@"\s*'\$\(GodotTargetPlatform\)'\s*==\s*'(?[A-z]+)'\s*", RegexOptions.IgnoreCase)] + private static partial Regex GodotTargetPlatformConditionRegex(); + + private static readonly string[] _platformNames = + { + "windows", + "linuxbsd", + "macos", + "android", + "ios", + "web", + }; + public static void MSBuildLocatorRegisterLatest(out Version version, out string path) { var instance = MSBuildLocator.QueryVisualStudioInstances() @@ -36,11 +53,22 @@ namespace GodotTools.ProjectEditor public static MSBuildProject? Open(string path) { - var root = ProjectRootElement.Open(path); + var root = ProjectRootElement.Open(path, ProjectCollection.GlobalProjectCollection, preserveFormatting: true); return root != null ? new MSBuildProject(root) : null; } - public static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName) + public static void UpgradeProjectIfNeeded(MSBuildProject project, string projectName) + { + // NOTE: The order in which changes are made to the project is important. + + // Migrate to MSBuild project Sdks style if using the old style. + MigrateToProjectSdksStyle(project, projectName); + + EnsureGodotSdkIsUpToDate(project); + EnsureTargetFrameworkMatchesMinimumRequirement(project); + } + + private static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName) { var origRoot = project.Root; @@ -64,5 +92,128 @@ namespace GodotTools.ProjectEditor root.Sdk = godotSdkAttrValue; project.HasUnsavedChanges = true; } + + private static void EnsureTargetFrameworkMatchesMinimumRequirement(MSBuildProject project) + { + var root = project.Root; + string minTfmValue = ProjectGenerator.GodotMinimumRequiredTfm; + var minTfmVersion = NuGetFramework.Parse(minTfmValue).Version; + + ProjectPropertyGroupElement? mainPropertyGroup = null; + ProjectPropertyElement? mainTargetFrameworkProperty = null; + + var propertiesToChange = new List(); + + foreach (var propertyGroup in root.PropertyGroups) + { + bool groupHasCondition = !string.IsNullOrEmpty(propertyGroup.Condition); + + // Check if the property group should be excluded from checking for 'TargetFramework' properties. + if (groupHasCondition && !ConditionMatchesGodotPlatform(propertyGroup.Condition)) + { + continue; + } + + // Store a reference to the first property group without conditions, + // in case we need to add a new 'TargetFramework' property later. + if (mainPropertyGroup == null && !groupHasCondition) + { + mainPropertyGroup = propertyGroup; + } + + foreach (var property in propertyGroup.Properties) + { + // We are looking for 'TargetFramework' properties. + if (property.Name != "TargetFramework") + { + continue; + } + + bool propertyHasCondition = !string.IsNullOrEmpty(property.Condition); + + // Check if the property should be excluded. + if (propertyHasCondition && !ConditionMatchesGodotPlatform(property.Condition)) + { + continue; + } + + if (!groupHasCondition && !propertyHasCondition) + { + // Store a reference to the 'TargetFramework' that has no conditions + // because it applies to all platforms. + if (mainTargetFrameworkProperty == null) + { + mainTargetFrameworkProperty = property; + } + continue; + } + + // If the 'TargetFramework' property is conditional, it may no longer be needed + // when the main one is upgraded to the new minimum version. + var tfmVersion = NuGetFramework.Parse(property.Value).Version; + if (tfmVersion <= minTfmVersion) + { + propertiesToChange.Add(property); + } + } + } + + if (mainTargetFrameworkProperty == null) + { + // We haven't found a 'TargetFramework' property without conditions, + // we'll just add one in the first property group without conditions. + if (mainPropertyGroup == null) + { + // We also don't have a property group without conditions, + // so we'll add a new one to the project. + mainPropertyGroup = root.AddPropertyGroup(); + } + + mainTargetFrameworkProperty = mainPropertyGroup.AddProperty("TargetFramework", minTfmValue); + project.HasUnsavedChanges = true; + } + else + { + var tfmVersion = NuGetFramework.Parse(mainTargetFrameworkProperty.Value).Version; + if (tfmVersion < minTfmVersion) + { + mainTargetFrameworkProperty.Value = minTfmValue; + project.HasUnsavedChanges = true; + } + } + + var mainTfmVersion = NuGetFramework.Parse(mainTargetFrameworkProperty.Value).Version; + foreach (var property in propertiesToChange) + { + // If the main 'TargetFramework' property targets a version newer than + // the minimum required by Godot, we don't want to remove the conditional + // 'TargetFramework' properties, only upgrade them to the new minimum. + // Otherwise, it can be removed. + if (mainTfmVersion > minTfmVersion) + { + property.Value = minTfmValue; + } + else + { + property.Parent.RemoveChild(property); + } + + project.HasUnsavedChanges = true; + } + + static bool ConditionMatchesGodotPlatform(string condition) + { + // Check if the condition is checking the 'GodotTargetPlatform' for one of the + // Godot platforms with built-in support in the Godot.NET.Sdk. + var match = GodotTargetPlatformConditionRegex().Match(condition); + if (match.Success) + { + string platform = match.Groups["platform"].Value; + return _platformNames.Contains(platform, StringComparer.OrdinalIgnoreCase); + } + + return false; + } + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 74e04b46a16..e1dc1be9794 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -439,12 +439,7 @@ namespace GodotTools var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath) ?? throw new InvalidOperationException("Cannot open C# project."); - // NOTE: The order in which changes are made to the project is important - - // Migrate to MSBuild project Sdks style if using the old style - ProjectUtils.MigrateToProjectSdksStyle(msbuildProject, GodotSharpDirs.ProjectAssemblyName); - - ProjectUtils.EnsureGodotSdkIsUpToDate(msbuildProject); + ProjectUtils.UpgradeProjectIfNeeded(msbuildProject, GodotSharpDirs.ProjectAssemblyName); if (msbuildProject.HasUnsavedChanges) {