From 137aada1e9800bc732324c72a43d8ff6b35b99f5 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Sat, 29 Aug 2026 17:21:17 +0200 Subject: [PATCH 1/2] Pin `NuGet.Framework` for .NET SDK 10.0.400; auto-remove on future major versions --- .../Steps/RewriteCsprojsStep.cs | 59 ++++++++++++- .../RewriteCsprojsStepSpecs.cs | 82 +++++++++++++++++-- 2 files changed, 131 insertions(+), 10 deletions(-) diff --git a/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs b/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs index bda0d3f54..5e512fd2c 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 before the Fallout major named +/// in its marker (and stripped once that major is reached). /// internal sealed class RewriteCsprojsStep : IMigrationStep { @@ -74,6 +77,12 @@ 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); + /// public Task ExecuteAsync(MigrationContext context, Summary summary) { @@ -91,7 +100,8 @@ 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. @@ -139,7 +149,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); } // Pass 5 — extract variables used by Fallout.* PackageReferences, decouple the ones ambiguously @@ -267,4 +278,46 @@ private static (string content, int edits) BumpVariableProperties(string content return (content, edits); } + + // Pass 6 — add the NuGet.Framework pin before the Fallout major in the marker; strip it + // once FalloutVersion is on that major. + private static RewriteResult HandleNugetFrameworkPin(RewriteResult result, string falloutVersion) + { + var content = result.Content; + var edits = result.EditCount; + var major = new Version(falloutVersion).Major; + var pin = nugetFrameworkPinPattern.Match(content); + + if (pin.Success) + { + if (major == int.Parse(pin.Groups["major"].Value)) + { + 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) + { + 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); + } } diff --git a/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs b/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs index f13466cc0..87c0d361e 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,72 @@ 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 = """ + + + + + + + """; + + (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(""" + + + + + + + + + + + + """); + } + + [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(""" + + + + + + + """); + } } From 07508906fa320e451de9f21631a7641c3619c853 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Sat, 29 Aug 2026 17:35:44 +0200 Subject: [PATCH 2/2] Resolve edge cases - Do not Add pin to net framework build projects - Only add on build projects in general --- .../Common/TargetFrameworkMonikers.cs | 17 ++++++ .../Steps/RewriteCsprojsStep.cs | 52 +++++++++++++++---- .../RewriteCsprojsStepSpecs.cs | 44 ++++++++++++++++ 3 files changed, 103 insertions(+), 10 deletions(-) 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 5e512fd2c..8cc1ed1ee 100644 --- a/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs +++ b/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs @@ -12,8 +12,8 @@ namespace Fallout.Migrate.Steps; /// references become Fallout.* (pinning the current Fallout version where an inline /// Version attribute was present), Nuke* MSBuild properties are renamed to /// Fallout*, stale explicit System.Security.Cryptography.Xml pins are stripped, -/// and a temporary NuGet.Framework 7.9.0 pin is added before the Fallout major named -/// in its marker (and stripped once that major is reached). +/// 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 { @@ -83,6 +83,10 @@ internal sealed class RewriteCsprojsStep : IMigrationStep @"\r?\n[ \t]*[\s\S]*?\r?\n?", RegexOptions.Compiled); + private static readonly Regex targetFrameworkElementPattern = new( + @"(?[^<]+)", + RegexOptions.Compiled); + /// public Task ExecuteAsync(MigrationContext context, Summary summary) { @@ -91,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); } @@ -105,8 +112,9 @@ public Task ExecuteAsync(MigrationContext context, Summary summary) /// /// 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; @@ -150,7 +158,7 @@ private static RewriteResult Rewrite(string original, string falloutVersion) }); var result = HandleMsBuildVariable(falloutVersion, content, edits); - return HandleNugetFrameworkPin(result, falloutVersion); + return HandleNugetFrameworkPin(result, falloutVersion, isBuildProject); } // Pass 5 — extract variables used by Fallout.* PackageReferences, decouple the ones ambiguously @@ -279,18 +287,27 @@ private static (string content, int edits) BumpVariableProperties(string content return (content, edits); } - // Pass 6 — add the NuGet.Framework pin before the Fallout major in the marker; strip it - // once FalloutVersion is on that major. - private static RewriteResult HandleNugetFrameworkPin(RewriteResult result, string falloutVersion) + // 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)) + if (major == int.Parse(pin.Groups["major"].Value) || !canPin) { return new RewriteResult(nugetFrameworkPinPattern.Replace(content, string.Empty, 1), edits + 1); } @@ -299,7 +316,7 @@ private static RewriteResult HandleNugetFrameworkPin(RewriteResult result, strin } // This pin is a 10.x workaround. v11+ only strips a marker that already names that major. - if (major != 10) + if (major != 10 || !canPin) { return result; } @@ -320,4 +337,19 @@ private static RewriteResult HandleNugetFrameworkPin(RewriteResult result, strin 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 87c0d361e..254d5fa41 100644 --- a/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs +++ b/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs @@ -486,6 +486,7 @@ public async Task Correctly_adds_nuget_framework_package_version_pin() const string input = """ + net10.0 @@ -502,6 +503,7 @@ public async Task Correctly_adds_nuget_framework_package_version_pin() buildCsproj.Should().Be(""" + net10.0 @@ -547,4 +549,46 @@ public async Task Removes_nuget_framework_package_version_pin_on_v11() """); } + + [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); + } }