Skip to content

Commit

Permalink
[.NET] Upgrade user project's TargetFramework to net8.0
Browse files Browse the repository at this point in the history
- Upgrades the TFM for new created  projects to `net8.0`.
- Implements system to upgrade TFM for existing projects to `net8.0`.
  • Loading branch information
raulsntos committed Dec 18, 2024
1 parent 6e2cf2a commit 8d41b5a
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageReference Include="Microsoft.Build" Version="15.1.548" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.2.6" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="NuGet.Frameworks" Version="6.12.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public static class ProjectGenerator
{
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)
Expand All @@ -22,7 +24,7 @@ public static ProjectRootElement GenGameProject(string name)
root.Sdk = GodotSdkAttrValue;

var mainGroup = root.AddPropertyGroup();
mainGroup.AddProperty("TargetFramework", "net8.0");
mainGroup.AddProperty("TargetFramework", GodotMinimumRequiredTfm);

mainGroup.AddProperty("EnableDynamicLoading", "true");

Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -19,8 +23,21 @@ public MSBuildProject(ProjectRootElement root)
}
}

public static class ProjectUtils
public static partial class ProjectUtils
{
[GeneratedRegex(@"\s*'\$\(GodotTargetPlatform\)'\s*==\s*'(?<platform>[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()
Expand All @@ -36,11 +53,22 @@ public static void MSBuildLocatorRegisterMSBuildPath(string msbuildPath)

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;

Expand All @@ -64,5 +92,128 @@ public static void EnsureGodotSdkIsUpToDate(MSBuildProject project)
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<ProjectPropertyElement>();

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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -439,12 +439,7 @@ private void ApplyNecessaryChangesToSolution()
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)
{
Expand Down

0 comments on commit 8d41b5a

Please sign in to comment.