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 { public sealed class MSBuildProject { internal ProjectRootElement Root { get; set; } public bool HasUnsavedChanges { get; set; } public void Save() => Root.Save(); public MSBuildProject(ProjectRootElement root) { Root = root; } } 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() .OrderByDescending(x => x.Version) .First(); MSBuildLocator.RegisterInstance(instance); version = instance.Version; path = instance.MSBuildPath; } public static void MSBuildLocatorRegisterMSBuildPath(string msbuildPath) => MSBuildLocator.RegisterMSBuildPath(msbuildPath); public static MSBuildProject? Open(string path) { var root = ProjectRootElement.Open(path, ProjectCollection.GlobalProjectCollection, preserveFormatting: true); return root != null ? new MSBuildProject(root) : null; } 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; if (!string.IsNullOrEmpty(origRoot.Sdk)) return; project.Root = ProjectGenerator.GenGameProject(projectName); project.Root.FullPath = origRoot.FullPath; project.HasUnsavedChanges = true; } public static void EnsureGodotSdkIsUpToDate(MSBuildProject project) { var root = project.Root; string godotSdkAttrValue = ProjectGenerator.GodotSdkAttrValue; if (!string.IsNullOrEmpty(root.Sdk) && root.Sdk.Trim().Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase)) return; 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; } } } }