Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Fallout.Migrate.Common;
Expand All @@ -19,7 +20,7 @@ internal sealed class RewriteCsprojsStep : IMigrationStep
// a broken post-migrate build (#217). Tolerates extra attributes between Include and Version
// (e.g. `PrivateAssets="all"`).
private static readonly Regex nukePackageWithInlineVersionPattern = new(
@"(?<prefix><PackageReference\s+Include="")Nuke\.(?<name>[A-Z][A-Za-z0-9.]+)(?<between>""[^>]*?\s+Version="")[^""]+",
@"(?<prefix><PackageReference\s+Include="")Nuke\.(?<name>[A-Z][A-Za-z0-9.]+)(?<between>""[^>]*?\s+Version="")(?!\$\()[^""]+",
RegexOptions.Compiled);

// PackageReference / ProjectReference `Include="Nuke.X"` → `Include="Fallout.X"` — namespace
Expand All @@ -28,13 +29,18 @@ internal sealed class RewriteCsprojsStep : IMigrationStep
private static readonly Regex packageReferencePattern =
new(@"(?<=\b(?:Include|Update|Remove)="")Nuke\.(?=[A-Z])", RegexOptions.Compiled);

// Detects MSBuild variables used in PackageReference Version attributes: Version="$(MyVar)"
private static readonly Regex packageReferenceVariablePattern = new(
@"<PackageReference\s+[^>]*?Version=""\$\((?<variable>[^)]+)\)""",
RegexOptions.Compiled);

// MSBuild element/property names that begin with `Nuke` followed by an uppercase
// letter (e.g. <NukeRootDirectory>...). Limited to known consumer-facing names from
// P3.5b so we don't rewrite unrelated user-defined identifiers that happen to start
// with the literal "Nuke".
private static readonly Regex msBuildPropertyPattern = new(
@"\bNuke(?=" +
"(?:RootDirectory|ScriptDirectory|TelemetryVersion|BaseDirectory|BaseNamespace|" +
"(?:Version|RootDirectory|ScriptDirectory|TelemetryVersion|BaseDirectory|BaseNamespace|" +
"UseNestedNamespaces|RepositoryUrl|UpdateReferences|ContinueOnError|TaskTimeout|" +
"Timeout|TasksEnabled|DefaultExcludes|ExcludeBoot|ExcludeConfig|ExcludeLogs|" +
"ExcludeDirectoryBuild|ExcludeCi|SpecificationFiles|ExternalFiles|TasksAssembly|" +
Expand Down Expand Up @@ -109,6 +115,32 @@ public static RewriteResult Rewrite(string original, string falloutVersion)
return string.Empty;
});

// Pass 4 — extract variables used in PackageReferences and bump their properties.
var variablesToBump = packageReferenceVariablePattern.Matches(content)
.Select(m => m.Groups["variable"].Value)
.ToHashSet();

// Always include FalloutVersion as they are canonical.
variablesToBump.Add("FalloutVersion");

foreach (var variable in variablesToBump)
{
var pattern = $"(?<=<{variable}>)[^<]+(?=</{variable}>)";
content = Regex.Replace(content,
pattern,
m =>
{
if (m.Value == falloutVersion)
{
return m.Value;
}

edits++;
return falloutVersion;

});
}

return new RewriteResult(content, edits);
}
}
38 changes: 21 additions & 17 deletions tests/Fallout.Migrate.Specs/MigrationIntegrationSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,21 @@ public async Task MigratesVanillaConsumerRepo()
var summary = await migration.RunAsync();

// Build file rewritten end to end.
var buildCsproj = File.ReadAllText(Path.Combine(temp, "build", "_build.csproj"));
var buildCsproj = await File.ReadAllTextAsync(Path.Combine(temp, "build", "_build.csproj"));
buildCsproj.Should().Contain(@"Include=""Fallout.Common""");
buildCsproj.Should().Contain("<FalloutRootDirectory>");
buildCsproj.Should().NotContain("Nuke.Common");
buildCsproj.Should().NotContain("<NukeRootDirectory>");
buildCsproj.Should().NotContain("NukeVersion");
buildCsproj.Should().Contain("FalloutVersion", Exactly.Thrice());

var buildCs = File.ReadAllText(Path.Combine(temp, "build", "Build.cs"));
var buildCs = await File.ReadAllTextAsync(Path.Combine(temp, "build", "Build.cs"));
buildCs.Should().Contain("using Fallout.Common");
buildCs.Should().Contain(": FalloutBuild");
buildCs.Should().NotContain("using Nuke.");
buildCs.Should().NotContain("NukeBuild");

var buildSh = File.ReadAllText(Path.Combine(temp, "build.sh"));
var buildSh = await File.ReadAllTextAsync(Path.Combine(temp, "build.sh"));
buildSh.Should().Contain("dotnet fallout");
buildSh.Should().Contain("FALLOUT_TELEMETRY_OPTOUT");
buildSh.Should().Contain(".fallout/temp");
Expand Down Expand Up @@ -60,12 +62,12 @@ public async Task DryRunDoesNotWriteFiles()

try
{
var beforeCsproj = File.ReadAllText(Path.Combine(temp, "build", "_build.csproj"));
var beforeCsproj = await File.ReadAllTextAsync(Path.Combine(temp, "build", "_build.csproj"));
var beforeNukeDir = Directory.Exists(Path.Combine(temp, ".nuke"));

var summary = await new Migration(temp, dryRun: true, TextWriter.Null).RunAsync();

File.ReadAllText(Path.Combine(temp, "build", "_build.csproj")).Should().Be(beforeCsproj);
(await File.ReadAllTextAsync(Path.Combine(temp, "build", "_build.csproj"))).Should().Be(beforeCsproj);
Directory.Exists(Path.Combine(temp, ".nuke")).Should().Be(beforeNukeDir);
summary.FilesChanged.Should().BeGreaterThan(0); // counts intended edits
}
Expand Down Expand Up @@ -100,7 +102,7 @@ public async Task WarnsWhenBuildProjectTargetsOlderThanNet10()
{
var temp = CreateVanillaFixture();
var buildCsprojPath = Path.Combine(temp, "build", "_build.csproj");
File.WriteAllText(buildCsprojPath, File.ReadAllText(buildCsprojPath).Replace("net10.0", "net8.0"));
await File.WriteAllTextAsync(buildCsprojPath, (await File.ReadAllTextAsync(buildCsprojPath)).Replace("net10.0", "net8.0"));

try
{
Expand All @@ -120,23 +122,23 @@ public async Task BumpsDotNetVersionAndSdk()
{
var temp = CreateVanillaFixture();
var buildCsprojPath = Path.Combine(temp, "build", "_build.csproj");
File.WriteAllText(buildCsprojPath, File.ReadAllText(buildCsprojPath).Replace("net10.0", "net8.0"));
File.WriteAllText(Path.Combine(temp, "global.json"), """
{
"sdk": {
"version": "8.0.100",
"rollForward": "latestMinor"
}
}
""");
await File.WriteAllTextAsync(buildCsprojPath, (await File.ReadAllTextAsync(buildCsprojPath)).Replace("net10.0", "net8.0"));
await File.WriteAllTextAsync(Path.Combine(temp, "global.json"), """
{
"sdk": {
"version": "8.0.100",
"rollForward": "latestMinor"
}
}
""");

try
{
await new Migration(temp, dryRun: false, TextWriter.Null).RunAsync();

File.ReadAllText(buildCsprojPath).Should().Contain("<TargetFramework>net10.0</TargetFramework>");
(await File.ReadAllTextAsync(buildCsprojPath)).Should().Contain("<TargetFramework>net10.0</TargetFramework>");

var globalJson = File.ReadAllText(Path.Combine(temp, "global.json"));
var globalJson = await File.ReadAllTextAsync(Path.Combine(temp, "global.json"));
globalJson.Should().Contain(@"""version"": ""10.0.100""");
}
finally
Expand All @@ -159,9 +161,11 @@ private static string CreateVanillaFixture()
<TargetFramework>net10.0</TargetFramework>
<NukeRootDirectory>.\..</NukeRootDirectory>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
<NukeVersion>9.0.0</NukeVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="9.0.0" />
<PackageReference Include="Nuke.Components" Version="$(NukeVersion)" />
</ItemGroup>
</Project>
""");
Expand Down
70 changes: 70 additions & 0 deletions tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,74 @@ public void LeavesOtherSystemPackagesAlone()
result.EditCount.Should().Be(0);
result.Content.Should().Be(input);
}

[Fact]
public void Recognizes_a_version_variable_prefixed_with_Nuke()
{
const string input = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NukeVersion>10.1.0</NukeVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="Nuke.Common" Version="$(NukeVersion)" />
<PackageReference Include="Nuke.Components" Version="$(NukeVersion)" />
</ItemGroup>
</Project>
""";

var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion);

result.Content.Should().NotContain("NukeVersion")
.And.Contain("$(FalloutVersion)", Exactly.Twice())
.And.Contain("<FalloutVersion>11.0.0</FalloutVersion>");
}

[Fact]
public void Leaves_an_arbitrary_version_variable_alone_but_updates_the_version()
{
const string input = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<CiProjectVersion>10.3.49</CiProjectVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="Nuke.Common" Version="$(CiProjectVersion)" />
<PackageReference Include="Nuke.Components" Version="$(CiProjectVersion)" />
</ItemGroup>
</Project>
""";

var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion);

result.Content.Should().NotContain("FalloutVersion")
.And.Contain("$(CiProjectVersion)", Exactly.Twice())
.And.Contain("<CiProjectVersion>11.0.0</CiProjectVersion>");
}

[Fact]
public void Leaves_an_unreferenced_arbitrary_variable_alone()
{
const string input = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<UnreferencedVersion>10.3.49</UnreferencedVersion>
<CiProjectVersion>10.3.49</CiProjectVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="$(CiProjectVersion)" />
</ItemGroup>
</Project>
""";

var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion);

result.Content.Should().Contain("<UnreferencedVersion>10.3.49</UnreferencedVersion>")
.And.Contain("<CiProjectVersion>11.0.0</CiProjectVersion>")
.And.Contain("$(CiProjectVersion)", Exactly.Once());
}
}
Loading