diff --git a/src/Fallout.Migrate/Common/TargetFrameworkMonikers.cs b/src/Fallout.Migrate/Common/TargetFrameworkMonikers.cs
index 39f703a93..d222fea8f 100644
--- a/src/Fallout.Migrate/Common/TargetFrameworkMonikers.cs
+++ b/src/Fallout.Migrate/Common/TargetFrameworkMonikers.cs
@@ -46,4 +46,21 @@ public static bool IsOlderThanMinimumSupported(string moniker, int minimumMajor)
int major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);
return major < minimumMajor;
}
+
+ ///
+ /// Returns true for modern .NET monikers (net5.0 and later).
+ /// .NET Framework (net48, net472) and .NET Standard are false.
+ ///
+ /// A single target framework moniker, e.g. net10.0 or net48.
+ public static bool TargetsModernDotNet(string moniker)
+ {
+ Match match = modernMonikerPattern.Match(moniker.Trim());
+ if (!match.Success)
+ {
+ return false;
+ }
+
+ int major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);
+ return major >= 5;
+ }
}
diff --git a/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs b/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs
index bda0d3f54..8cc1ed1ee 100644
--- a/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs
+++ b/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@@ -10,7 +11,9 @@ namespace Fallout.Migrate.Steps;
/// Rewrites every *.csproj file under the repository root: Nuke.* package/project
/// references become Fallout.* (pinning the current Fallout version where an inline
/// Version attribute was present), Nuke* MSBuild properties are renamed to
-/// Fallout*, and stale explicit System.Security.Cryptography.Xml pins are stripped.
+/// Fallout*, stale explicit System.Security.Cryptography.Xml pins are stripped,
+/// and a temporary NuGet.Framework 7.9.0 pin is added on _build.csproj when that
+/// project targets modern .NET (stripped once the marker major is reached).
///
internal sealed class RewriteCsprojsStep : IMigrationStep
{
@@ -74,6 +77,16 @@ internal sealed class RewriteCsprojsStep : IMigrationStep
@"^[ \t]*[ \t]*\r?\n?",
RegexOptions.Compiled | RegexOptions.Multiline);
+ // Temporary pin for .NET SDK 10.0.400. The marker names the Fallout major that drops it
+ // (the next major after MigrationContext.FalloutVersion).
+ private static readonly Regex nugetFrameworkPinPattern = new(
+ @"\r?\n[ \t]*[\s\S]*?\r?\n?",
+ RegexOptions.Compiled);
+
+ private static readonly Regex targetFrameworkElementPattern = new(
+ @"(?[^<]+)",
+ RegexOptions.Compiled);
+
///
public Task ExecuteAsync(MigrationContext context, Summary summary)
{
@@ -82,7 +95,10 @@ public Task ExecuteAsync(MigrationContext context, Summary summary)
MigrationFileOperations.ApplyRewrite(
context,
path,
- content => Rewrite(content, context.FalloutVersion),
+ content => Rewrite(
+ content,
+ context.FalloutVersion,
+ isBuildProject: path.Name.Equals("_build.csproj", StringComparison.OrdinalIgnoreCase)),
summary);
}
@@ -91,12 +107,14 @@ public Task ExecuteAsync(MigrationContext context, Summary summary)
///
/// Rewrites content, replacing Nuke.* references and MSBuild
- /// properties with their Fallout.* equivalents and stripping stale pins.
+ /// properties with their Fallout.* equivalents, stripping stale pins, and adding or
+ /// removing the temporary NuGet.Framework pin for .NET SDK 10.0.400.
///
/// The original .csproj file content.
/// The Fallout version to pin into rewritten inline-versioned references.
+ /// true when is _build.csproj.
/// The rewritten content and the number of edits made.
- private static RewriteResult Rewrite(string original, string falloutVersion)
+ private static RewriteResult Rewrite(string original, string falloutVersion, bool isBuildProject)
{
var edits = 0;
var content = original;
@@ -139,7 +157,8 @@ private static RewriteResult Rewrite(string original, string falloutVersion)
return string.Empty;
});
- return HandleMsBuildVariable(falloutVersion, content, edits);
+ var result = HandleMsBuildVariable(falloutVersion, content, edits);
+ return HandleNugetFrameworkPin(result, falloutVersion, isBuildProject);
}
// Pass 5 — extract variables used by Fallout.* PackageReferences, decouple the ones ambiguously
@@ -267,4 +286,70 @@ private static (string content, int edits) BumpVariableProperties(string content
return (content, edits);
}
+
+ // Pass 6 — add the NuGet.Framework pin on `_build.csproj` only, and only when that
+ // project targets modern .NET (NuGet.Framework 7.9.0 dropped netstandard2.0 / .NET
+ // Framework). Strip the pin once FalloutVersion is on the marker major, or when the
+ // build project cannot take the package.
+ private static RewriteResult HandleNugetFrameworkPin(
+ RewriteResult result, string falloutVersion, bool isBuildProject)
+ {
+ if (!isBuildProject)
+ {
+ return result;
+ }
+
+ var content = result.Content;
+ var edits = result.EditCount;
+ var major = new Version(falloutVersion).Major;
+ var pin = nugetFrameworkPinPattern.Match(content);
+ var canPin = CanPinNugetFramework(content);
+
+ if (pin.Success)
+ {
+ if (major == int.Parse(pin.Groups["major"].Value) || !canPin)
+ {
+ return new RewriteResult(nugetFrameworkPinPattern.Replace(content, string.Empty, 1), edits + 1);
+ }
+
+ return result;
+ }
+
+ // This pin is a 10.x workaround. v11+ only strips a marker that already names that major.
+ if (major != 10 || !canPin)
+ {
+ return result;
+ }
+
+ var deleteAtMajor = major + 1;
+ var itemGroupClose = content.IndexOf("", StringComparison.Ordinal);
+ if (itemGroupClose < 0)
+ {
+ return result;
+ }
+
+ var newLine = content.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n";
+ var block = newLine
+ + $" " + newLine
+ + " " + newLine
+ + @" " + newLine
+ + $" " + newLine;
+
+ return new RewriteResult(content.Insert(content.LastIndexOf('\n', itemGroupClose) + 1, block), edits + 1);
+ }
+
+ private static bool CanPinNugetFramework(string content)
+ {
+ var match = targetFrameworkElementPattern.Match(content);
+ if (!match.Success)
+ {
+ return false;
+ }
+
+ return match.Groups["value"].Value
+ .Split(';')
+ .Select(moniker => moniker.Trim())
+ .Where(moniker => moniker.Length > 0)
+ .All(TargetFrameworkMonikers.TargetsModernDotNet);
+ }
}
diff --git a/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs b/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs
index f13466cc0..254d5fa41 100644
--- a/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs
+++ b/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs
@@ -313,7 +313,7 @@ public async Task Recognizes_a_version_variable_prefixed_with_Nuke()
""";
(tempDirectory / "build" / "_build.csproj").WriteAllText(input);
-
+
await new RewriteCsprojsStep().ExecuteAsync(context, summary);
var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
@@ -341,7 +341,7 @@ public async Task Leaves_an_arbitrary_version_variable_alone_but_updates_the_ver
""";
(tempDirectory / "build" / "_build.csproj").WriteAllText(input);
-
+
await new RewriteCsprojsStep().ExecuteAsync(context, summary);
var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
@@ -367,7 +367,7 @@ public async Task Leaves_an_unreferenced_arbitrary_variable_alone()
""";
(tempDirectory / "build" / "_build.csproj").WriteAllText(input);
-
+
await new RewriteCsprojsStep().ExecuteAsync(context, summary);
var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
@@ -392,7 +392,7 @@ public async Task Does_not_bump_variables_used_for_non_Fallout_packages()
""";
(tempDirectory / "build" / "_build.csproj").WriteAllText(input, eofLineBreak: false);
-
+
await new RewriteCsprojsStep().ExecuteAsync(context, summary);
var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
@@ -417,7 +417,7 @@ public async Task Decouples_ambiguously_used_variables()
""";
(tempDirectory / "build" / "_build.csproj").WriteAllText(input);
-
+
await new RewriteCsprojsStep().ExecuteAsync(context, summary);
var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
@@ -441,7 +441,7 @@ public async Task Decouples_ambiguously_used_variables_even_when_no_property_gro
""";
(tempDirectory / "build" / "_build.csproj").WriteAllText(input);
-
+
await new RewriteCsprojsStep().ExecuteAsync(context, summary);
var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
@@ -468,7 +468,7 @@ public async Task Decouples_ambiguously_used_variables_when_a_property_group_but
""";
(tempDirectory / "build" / "_build.csproj").WriteAllText(input);
-
+
await new RewriteCsprojsStep().ExecuteAsync(context, summary);
var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
@@ -479,4 +479,116 @@ public async Task Decouples_ambiguously_used_variables_when_a_property_group_but
.And.Contain("$(FalloutVersion)", Exactly.Once())
.And.Contain("$(PkgVersion)");
}
+
+ [Fact]
+ public async Task Correctly_adds_nuget_framework_package_version_pin()
+ {
+ const string input = """
+
+
+ net10.0
+
+
+
+
+ """;
+
+ (tempDirectory / "build" / "_build.csproj").WriteAllText(input, eofLineBreak: false);
+
+ context.FalloutVersion = "10.4.2";
+ await new RewriteCsprojsStep().ExecuteAsync(context, summary);
+
+ var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
+
+ buildCsproj.Should().Be("""
+
+
+ net10.0
+
+
+
+
+
+
+
+
+
+ """);
+ }
+
+ [Fact]
+ public async Task Removes_nuget_framework_package_version_pin_on_v11()
+ {
+ const string input = """
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ (tempDirectory / "build" / "_build.csproj").WriteAllText(input, eofLineBreak: false);
+
+ context.FalloutVersion = "11.0.0";
+ await new RewriteCsprojsStep().ExecuteAsync(context, summary);
+
+ var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
+
+ buildCsproj.Should().Be("""
+
+
+
+
+
+
+ """);
+ }
+
+ [Fact]
+ public async Task Does_not_add_nuget_framework_pin_to_a_non_build_csproj()
+ {
+ const string input = """
+
+
+ net10.0
+
+
+
+
+ """;
+
+ (tempDirectory / "Lib.csproj").WriteAllText(input, eofLineBreak: false);
+
+ context.FalloutVersion = "10.4.2";
+ await new RewriteCsprojsStep().ExecuteAsync(context, summary);
+
+ (tempDirectory / "Lib.csproj").ReadAllText().Should().Be(input);
+ }
+
+ [Fact]
+ public async Task Does_not_add_nuget_framework_pin_when_the_build_project_targets_netframework()
+ {
+ const string input = """
+
+
+ net48
+
+
+
+
+ """;
+
+ (tempDirectory / "build" / "_build.csproj").WriteAllText(input, eofLineBreak: false);
+
+ context.FalloutVersion = "10.4.2";
+ await new RewriteCsprojsStep().ExecuteAsync(context, summary);
+
+ (tempDirectory / "build" / "_build.csproj").ReadAllText().Should().Be(input);
+ }
}