diff --git a/fallout.slnx b/fallout.slnx index ee73c4481..7e66400fd 100644 --- a/fallout.slnx +++ b/fallout.slnx @@ -15,6 +15,7 @@ + @@ -30,6 +31,7 @@ + diff --git a/src/Fallout.Migrate/CodeRewriter.cs b/src/Fallout.Migrate/CodeRewriter.cs new file mode 100644 index 000000000..aa605d167 --- /dev/null +++ b/src/Fallout.Migrate/CodeRewriter.cs @@ -0,0 +1,33 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE + +using System.Text.RegularExpressions; + +namespace Fallout.Migrate; + +internal static class CodeRewriter +{ + // Anchored prefix swap: `\bNuke\.` → `Fallout.`. Covers using directives, + // attribute references, qualified type names, namespace declarations. + // The trailing `(?=[A-Z])` lookahead avoids matching `Nuke.json` filenames + // or other lowercase tails the prefix audit deliberately preserved. + private static readonly Regex NamespacePrefix = + new(@"\bNuke\.(?=[A-Z])", RegexOptions.Compiled); + + // Bare type renames done in the Fallout rebrand (#59). + private static readonly Regex NukeBuildType = new(@"\bNukeBuild\b", RegexOptions.Compiled); + private static readonly Regex INukeBuildType = new(@"\bINukeBuild\b", RegexOptions.Compiled); + + public static RewriteResult Rewrite(string original) + { + var edits = 0; + + var content = NamespacePrefix.Replace(original, _ => { edits++; return "Fallout."; }); + content = INukeBuildType.Replace(content, _ => { edits++; return "IFalloutBuild"; }); + content = NukeBuildType.Replace(content, _ => { edits++; return "FalloutBuild"; }); + + return new RewriteResult(content, edits); + } +} diff --git a/src/Fallout.Migrate/CsprojRewriter.cs b/src/Fallout.Migrate/CsprojRewriter.cs new file mode 100644 index 000000000..681a41bcb --- /dev/null +++ b/src/Fallout.Migrate/CsprojRewriter.cs @@ -0,0 +1,38 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE + +using System.Text.RegularExpressions; + +namespace Fallout.Migrate; + +internal static class CsprojRewriter +{ + // PackageReference / ProjectReference `Include="Nuke.X"` → `Include="Fallout.X"`. + private static readonly Regex PackageReferencePattern = + new(@"(?<=\b(?:Include|Update|Remove)="")Nuke\.(?=[A-Z])", RegexOptions.Compiled); + + // MSBuild element/property names that begin with `Nuke` followed by an uppercase + // letter (e.g. ...). 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|" + + "UseNestedNamespaces|RepositoryUrl|UpdateReferences|ContinueOnError|TaskTimeout|" + + "Timeout|TasksEnabled|DefaultExcludes|ExcludeBoot|ExcludeConfig|ExcludeLogs|" + + "ExcludeDirectoryBuild|ExcludeCi|SpecificationFiles|ExternalFiles|TasksAssembly|" + + "TasksDirectory)\\b)", + RegexOptions.Compiled); + + public static RewriteResult Rewrite(string original) + { + var edits = 0; + + var content = PackageReferencePattern.Replace(original, _ => { edits++; return "Fallout."; }); + content = MSBuildPropertyPattern.Replace(content, _ => { edits++; return "Fallout"; }); + + return new RewriteResult(content, edits); + } +} diff --git a/src/Fallout.Migrate/Fallout.Migrate.csproj b/src/Fallout.Migrate/Fallout.Migrate.csproj new file mode 100644 index 000000000..4427b3e44 --- /dev/null +++ b/src/Fallout.Migrate/Fallout.Migrate.csproj @@ -0,0 +1,17 @@ + + + + Exe + net10.0 + LatestMajor + true + fallout-migrate + CLI that migrates an existing NUKE consumer repo to Fallout in one command. + build automation continuous-integration tools orchestration migration + + + + + + + diff --git a/src/Fallout.Migrate/Migration.cs b/src/Fallout.Migrate/Migration.cs new file mode 100644 index 000000000..4d26daaf9 --- /dev/null +++ b/src/Fallout.Migrate/Migration.cs @@ -0,0 +1,136 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Fallout.Migrate; + +public sealed class Migration +{ + private readonly string _rootDirectory; + private readonly bool _dryRun; + private readonly TextWriter _log; + + public Migration(string rootDirectory, bool dryRun, TextWriter log) + { + _rootDirectory = rootDirectory ?? throw new ArgumentNullException(nameof(rootDirectory)); + _dryRun = dryRun; + _log = log ?? throw new ArgumentNullException(nameof(log)); + } + + public Summary Run() + { + var summary = new Summary(); + + RewriteCsprojs(summary); + RewriteCsFiles(summary); + RewriteBootstrapScripts(summary); + RenameNukeDirectory(summary); + + return summary; + } + + private void RewriteCsprojs(Summary summary) + { + foreach (var path in EnumerateFiles("*.csproj")) + ApplyRewrite(path, CsprojRewriter.Rewrite, summary); + } + + private void RewriteCsFiles(Summary summary) + { + foreach (var path in EnumerateFiles("*.cs")) + ApplyRewrite(path, CodeRewriter.Rewrite, summary); + } + + private void RewriteBootstrapScripts(Summary summary) + { + foreach (var name in new[] { "build.cmd", "build.ps1", "build.sh" }) + { + var path = Path.Combine(_rootDirectory, name); + if (File.Exists(path)) + ApplyRewrite(path, ScriptRewriter.Rewrite, summary); + } + } + + private void RenameNukeDirectory(Summary summary) + { + var legacy = Path.Combine(_rootDirectory, ".nuke"); + var canonical = Path.Combine(_rootDirectory, ".fallout"); + + if (!Directory.Exists(legacy)) + return; + + if (Directory.Exists(canonical)) + { + summary.Warnings.Add( + "Both .nuke/ and .fallout/ exist. Skipped rename; merge their contents manually."); + return; + } + + Log($"rename {RelativePath(legacy)} -> {RelativePath(canonical)}"); + if (!_dryRun) + Directory.Move(legacy, canonical); + summary.DirectoriesRenamed++; + } + + private IEnumerable EnumerateFiles(string pattern) + { + foreach (var file in Directory.EnumerateFiles(_rootDirectory, pattern, SearchOption.AllDirectories)) + { + if (IsIgnored(file)) + continue; + yield return file; + } + } + + private static bool IsIgnored(string path) + { + return path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal) + || path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal) + || path.Contains($"{Path.DirectorySeparatorChar}.git{Path.DirectorySeparatorChar}", StringComparison.Ordinal); + } + + private void ApplyRewrite(string path, Func rewriter, Summary summary) + { + string original; + try + { + original = File.ReadAllText(path); + } + catch (IOException ex) + { + summary.Warnings.Add($"could not read {RelativePath(path)}: {ex.Message}"); + return; + } + + var result = rewriter(original); + if (result.EditCount == 0) + return; + + Log($"edit {RelativePath(path)} ({result.EditCount} change{(result.EditCount == 1 ? "" : "s")})"); + summary.FilesChanged++; + summary.EditCount += result.EditCount; + + if (!_dryRun) + File.WriteAllText(path, result.Content); + } + + private string RelativePath(string absolute) => + Path.GetRelativePath(_rootDirectory, absolute).Replace('\\', '/'); + + private void Log(string line) => _log.WriteLine(line); + + public sealed class Summary + { + public int FilesChanged { get; set; } + public int EditCount { get; set; } + public int DirectoriesRenamed { get; set; } + public List Warnings { get; } = new(); + } +} + +public readonly record struct RewriteResult(string Content, int EditCount); diff --git a/src/Fallout.Migrate/Program.cs b/src/Fallout.Migrate/Program.cs new file mode 100644 index 000000000..6171beafc --- /dev/null +++ b/src/Fallout.Migrate/Program.cs @@ -0,0 +1,116 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE + +using System; +using System.IO; + +namespace Fallout.Migrate; + +public static class Program +{ + public static int Main(string[] args) + { + var dryRun = Array.Exists(args, a => a is "--dry-run" or "-n"); + var helpRequested = Array.Exists(args, a => a is "--help" or "-h" or "/?"); + var rootArg = Array.Find(args, a => !a.StartsWith('-') && !a.StartsWith('/')); + + if (helpRequested) + { + PrintHelp(); + return 0; + } + + var rootDirectory = ResolveRootDirectory(rootArg); + if (rootDirectory == null) + { + Console.Error.WriteLine("error: could not locate a repository root containing a build orchestrator project (_build.csproj) under the working directory."); + Console.Error.WriteLine(" pass an explicit path: fallout-migrate "); + return 1; + } + + Console.WriteLine($"fallout-migrate — migrating: {rootDirectory}"); + if (dryRun) + Console.WriteLine("(dry-run — no files will be modified)"); + Console.WriteLine(); + + var migration = new Migration(rootDirectory, dryRun, Console.Out); + var summary = migration.Run(); + + Console.WriteLine(); + Console.WriteLine($"Files changed: {summary.FilesChanged}"); + Console.WriteLine($"Edits made: {summary.EditCount}"); + Console.WriteLine($"Directories: {summary.DirectoriesRenamed} renamed"); + if (summary.Warnings.Count > 0) + { + Console.WriteLine(); + Console.WriteLine("Warnings:"); + foreach (var w in summary.Warnings) + Console.WriteLine($" - {w}"); + } + + Console.WriteLine(); + Console.WriteLine(dryRun + ? "Dry-run complete. Re-run without --dry-run to apply changes." + : "Migration complete. Verify the build: ./build.ps1 (or ./build.sh on unix)"); + Console.WriteLine("Migration guide: https://fallout.build (see #37 for the full guide)"); + return 0; + } + + private static string ResolveRootDirectory(string explicitArg) + { + if (explicitArg != null) + return Path.GetFullPath(explicitArg); + + var current = new DirectoryInfo(Environment.CurrentDirectory); + while (current != null) + { + if (Directory.Exists(Path.Combine(current.FullName, "build")) || + Directory.Exists(Path.Combine(current.FullName, ".nuke")) || + Directory.Exists(Path.Combine(current.FullName, ".fallout"))) + { + return current.FullName; + } + + // Heuristic: any _build.csproj or build.cmd / build.ps1 anywhere below. + if (current.GetFiles("build.cmd", SearchOption.TopDirectoryOnly).Length > 0 || + current.GetFiles("build.ps1", SearchOption.TopDirectoryOnly).Length > 0 || + current.GetFiles("build.sh", SearchOption.TopDirectoryOnly).Length > 0) + { + return current.FullName; + } + + current = current.Parent; + } + + return null; + } + + private static void PrintHelp() + { + Console.WriteLine(""" + fallout-migrate — migrate a NUKE consumer repo to Fallout. + + Usage: + fallout-migrate [path] [options] + + Arguments: + path Repository root. Defaults to walking up from the working + directory to find one (looking for build.cmd / build.ps1 / + build.sh, .nuke/, or build/). + + Options: + --dry-run, -n Show what would change without writing. + --help, -h, /? Show this message. + + What it does: + - Rewrites Nuke.* PackageReferences and MSBuild properties in .csproj + - Rewrites `using Nuke.*` directives and qualified type references in .cs + - Rewrites `dotnet nuke` → `dotnet fallout` and legacy NUKE_* env vars + in build.cmd / build.ps1 / build.sh + - Renames .nuke/ to .fallout/ + - Prints a summary of files changed and warnings to address manually + """); + } +} diff --git a/src/Fallout.Migrate/ScriptRewriter.cs b/src/Fallout.Migrate/ScriptRewriter.cs new file mode 100644 index 000000000..cdb5edcc6 --- /dev/null +++ b/src/Fallout.Migrate/ScriptRewriter.cs @@ -0,0 +1,35 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE + +using System.Text.RegularExpressions; + +namespace Fallout.Migrate; + +internal static class ScriptRewriter +{ + private static readonly (Regex Pattern, string Replacement)[] Patterns = + { + // `dotnet nuke` invocations + (new Regex(@"\bdotnet\s+nuke\b", RegexOptions.Compiled), "dotnet fallout"), + // .nuke directory references → .fallout + (new Regex(@"(?<=[\\/.""'\s])\.nuke(?=[\\/""'\s])", RegexOptions.Compiled), ".fallout"), + // Legacy env vars (consumer-facing ones from P3.5c) + (new Regex(@"\bNUKE_TELEMETRY_OPTOUT\b", RegexOptions.Compiled), "FALLOUT_TELEMETRY_OPTOUT"), + (new Regex(@"\bNUKE_GLOBAL_TOOL_VERSION\b", RegexOptions.Compiled), "FALLOUT_GLOBAL_TOOL_VERSION"), + (new Regex(@"\bNUKE_GLOBAL_TOOL_START_TIME\b", RegexOptions.Compiled), "FALLOUT_GLOBAL_TOOL_START_TIME"), + (new Regex(@"\bNUKE_INTERNAL_INTERCEPTOR\b", RegexOptions.Compiled), "FALLOUT_INTERNAL_INTERCEPTOR"), + }; + + public static RewriteResult Rewrite(string original) + { + var edits = 0; + var content = original; + foreach (var (pattern, replacement) in Patterns) + { + content = pattern.Replace(content, _ => { edits++; return replacement; }); + } + return new RewriteResult(content, edits); + } +} diff --git a/tests/Fallout.Migrate.Tests/CodeRewriterTest.cs b/tests/Fallout.Migrate.Tests/CodeRewriterTest.cs new file mode 100644 index 000000000..928f2fe7f --- /dev/null +++ b/tests/Fallout.Migrate.Tests/CodeRewriterTest.cs @@ -0,0 +1,74 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE + +using FluentAssertions; +using Xunit; + +namespace Fallout.Migrate.Tests; + +public class CodeRewriterTest +{ + [Fact] + public void RewritesUsingDirective() + { + const string input = """ + using Nuke.Common; + using Nuke.Common.IO; + using Fallout.Common; + """; + + var result = CodeRewriter.Rewrite(input); + + result.EditCount.Should().Be(2); + result.Content.Should().Contain("using Fallout.Common;"); + result.Content.Should().Contain("using Fallout.Common.IO;"); + } + + [Fact] + public void RewritesQualifiedTypeReference() + { + const string input = "var x = new Nuke.Common.Tools.DotNet.DotNetTasks();"; + var result = CodeRewriter.Rewrite(input); + result.EditCount.Should().Be(1); + result.Content.Should().Be("var x = new Fallout.Common.Tools.DotNet.DotNetTasks();"); + } + + [Fact] + public void RewritesNukeBuildBaseType() + { + const string input = "class Build : NukeBuild { }"; + var result = CodeRewriter.Rewrite(input); + result.EditCount.Should().Be(1); + result.Content.Should().Be("class Build : FalloutBuild { }"); + } + + [Fact] + public void RewritesINukeBuildInterface() + { + const string input = "public static int IsApplicable(INukeBuild build) => 0;"; + var result = CodeRewriter.Rewrite(input); + result.EditCount.Should().Be(1); + result.Content.Should().Be("public static int IsApplicable(IFalloutBuild build) => 0;"); + } + + [Fact] + public void DoesNotMatchNukeAsPartOfAnotherIdentifier() + { + // A type like `NukeAdjacentThing` must not match `\bNukeBuild\b`. + const string input = "var x = new NukeBuilderXYZ();"; + var result = CodeRewriter.Rewrite(input); + result.EditCount.Should().Be(0); + result.Content.Should().Be(input); + } + + [Fact] + public void DoesNotMatchLowercaseNukePrefix() + { + // ".nuke/foo" filenames stay as-is — handled by ScriptRewriter / DirectoryRenamer. + const string input = """var path = "/repo/.nuke/parameters.json";"""; + var result = CodeRewriter.Rewrite(input); + result.EditCount.Should().Be(0); + } +} diff --git a/tests/Fallout.Migrate.Tests/CsprojRewriterTest.cs b/tests/Fallout.Migrate.Tests/CsprojRewriterTest.cs new file mode 100644 index 000000000..5c7472bd9 --- /dev/null +++ b/tests/Fallout.Migrate.Tests/CsprojRewriterTest.cs @@ -0,0 +1,87 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE + +using FluentAssertions; +using Xunit; + +namespace Fallout.Migrate.Tests; + +public class CsprojRewriterTest +{ + [Fact] + public void RewritesPackageReferenceNamespace() + { + const string input = """ + + + + + + + """; + + var result = CsprojRewriter.Rewrite(input); + + result.EditCount.Should().Be(2); + result.Content.Should().Contain(@"Include=""Fallout.Common"""); + result.Content.Should().Contain(@"Include=""Fallout.Components"""); + result.Content.Should().NotContain(@"Include=""Nuke."); + } + + [Fact] + public void RewritesNukeRootDirectoryProperty() + { + const string input = """ + + + .\.. + 1 + + + """; + + var result = CsprojRewriter.Rewrite(input); + + result.EditCount.Should().Be(4); // 2 opening + 2 closing tags + result.Content.Should().Contain(""); + result.Content.Should().Contain(""); + result.Content.Should().Contain(""); + result.Content.Should().NotContain(""); + } + + [Fact] + public void LeavesUnrelatedNukePrefixedIdentifiersAlone() + { + const string input = """ + + + x + + + """; + + var result = CsprojRewriter.Rewrite(input); + + result.EditCount.Should().Be(0); + result.Content.Should().Be(input); + } + + [Fact] + public void ReturnsZeroEditsForUnchangedContent() + { + const string input = """ + + + net10.0 + + + """; + + var result = CsprojRewriter.Rewrite(input); + + result.EditCount.Should().Be(0); + result.Content.Should().Be(input); + } +} diff --git a/tests/Fallout.Migrate.Tests/Fallout.Migrate.Tests.csproj b/tests/Fallout.Migrate.Tests/Fallout.Migrate.Tests.csproj new file mode 100644 index 000000000..c9a546105 --- /dev/null +++ b/tests/Fallout.Migrate.Tests/Fallout.Migrate.Tests.csproj @@ -0,0 +1,11 @@ + + + + net10.0 + + + + + + + diff --git a/tests/Fallout.Migrate.Tests/MigrationIntegrationTest.cs b/tests/Fallout.Migrate.Tests/MigrationIntegrationTest.cs new file mode 100644 index 000000000..42016e1d4 --- /dev/null +++ b/tests/Fallout.Migrate.Tests/MigrationIntegrationTest.cs @@ -0,0 +1,147 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE + +using System; +using System.IO; +using System.Linq; +using System.Text; +using FluentAssertions; +using Xunit; + +namespace Fallout.Migrate.Tests; + +public class MigrationIntegrationTest +{ + [Fact] + public void MigratesVanillaConsumerRepo() + { + var temp = CreateVanillaFixture(); + + try + { + var migration = new Migration(temp, dryRun: false, TextWriter.Null); + var summary = migration.Run(); + + // Build file rewritten end to end. + var buildCsproj = File.ReadAllText(Path.Combine(temp, "build", "_build.csproj")); + buildCsproj.Should().Contain(@"Include=""Fallout.Common"""); + buildCsproj.Should().Contain(""); + buildCsproj.Should().NotContain("Nuke.Common"); + buildCsproj.Should().NotContain(""); + + var buildCs = File.ReadAllText(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")); + buildSh.Should().Contain("dotnet fallout"); + buildSh.Should().Contain("FALLOUT_TELEMETRY_OPTOUT"); + buildSh.Should().Contain(".fallout/temp"); + + // .nuke/ moved to .fallout/. + Directory.Exists(Path.Combine(temp, ".nuke")).Should().BeFalse(); + Directory.Exists(Path.Combine(temp, ".fallout")).Should().BeTrue(); + File.Exists(Path.Combine(temp, ".fallout", "parameters.json")).Should().BeTrue(); + + summary.FilesChanged.Should().BeGreaterThan(0); + summary.EditCount.Should().BeGreaterThan(0); + summary.DirectoriesRenamed.Should().Be(1); + summary.Warnings.Should().BeEmpty(); + } + finally + { + Directory.Delete(temp, recursive: true); + } + } + + [Fact] + public void DryRunDoesNotWriteFiles() + { + var temp = CreateVanillaFixture(); + + try + { + var beforeCsproj = File.ReadAllText(Path.Combine(temp, "build", "_build.csproj")); + var beforeNukeDir = Directory.Exists(Path.Combine(temp, ".nuke")); + + var summary = new Migration(temp, dryRun: true, TextWriter.Null).Run(); + + File.ReadAllText(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 + } + finally + { + Directory.Delete(temp, recursive: true); + } + } + + [Fact] + public void WarnsWhenBothNukeAndFalloutDirectoriesExist() + { + var temp = CreateVanillaFixture(); + Directory.CreateDirectory(Path.Combine(temp, ".fallout")); + + try + { + var summary = new Migration(temp, dryRun: false, TextWriter.Null).Run(); + + summary.Warnings.Should().Contain(w => w.Contains(".nuke/") && w.Contains(".fallout/")); + summary.DirectoriesRenamed.Should().Be(0); + Directory.Exists(Path.Combine(temp, ".nuke")).Should().BeTrue(); + } + finally + { + Directory.Delete(temp, recursive: true); + } + } + + private static string CreateVanillaFixture() + { + var dir = Path.Combine(Path.GetTempPath(), "fallout-migrate-test-" + Guid.NewGuid().ToString("N")[..8]); + Directory.CreateDirectory(dir); + Directory.CreateDirectory(Path.Combine(dir, "build")); + Directory.CreateDirectory(Path.Combine(dir, ".nuke")); + + File.WriteAllText(Path.Combine(dir, "build", "_build.csproj"), """ + + + Exe + net8.0 + .\.. + 1 + + + + + + """); + + File.WriteAllText(Path.Combine(dir, "build", "Build.cs"), """ + using Nuke.Common; + using Nuke.Common.Tools.DotNet; + + class Build : NukeBuild + { + public static int Main () => Execute(x => x.Compile); + + Target Compile => _ => _.Executes(() => { }); + } + """); + + File.WriteAllText(Path.Combine(dir, "build.sh"), """ + #!/usr/bin/env bash + export NUKE_TELEMETRY_OPTOUT=1 + TEMP_DIRECTORY="$SCRIPT_DIR/.nuke/temp" + dotnet nuke "$@" + """, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + + File.WriteAllText(Path.Combine(dir, ".nuke", "parameters.json"), "{}"); + + return dir; + } +} diff --git a/tests/Fallout.Migrate.Tests/ScriptRewriterTest.cs b/tests/Fallout.Migrate.Tests/ScriptRewriterTest.cs new file mode 100644 index 000000000..a2dc6128d --- /dev/null +++ b/tests/Fallout.Migrate.Tests/ScriptRewriterTest.cs @@ -0,0 +1,51 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE + +using FluentAssertions; +using Xunit; + +namespace Fallout.Migrate.Tests; + +public class ScriptRewriterTest +{ + [Fact] + public void RewritesDotnetNukeInvocations() + { + var result = ScriptRewriter.Rewrite("dotnet nuke Compile"); + result.EditCount.Should().Be(1); + result.Content.Should().Be("dotnet fallout Compile"); + } + + [Fact] + public void RewritesDotDirectoryReferences() + { + var result = ScriptRewriter.Rewrite("""TEMP_DIRECTORY="$SCRIPT_DIR/.nuke/temp" """); + result.EditCount.Should().Be(1); + result.Content.Should().Contain(".fallout/temp"); + result.Content.Should().NotContain(".nuke/"); + } + + [Fact] + public void RewritesLegacyEnvVars() + { + const string input = """ + export NUKE_TELEMETRY_OPTOUT=1 + $env:NUKE_GLOBAL_TOOL_VERSION = "10.0" + """; + var result = ScriptRewriter.Rewrite(input); + result.EditCount.Should().Be(2); + result.Content.Should().Contain("FALLOUT_TELEMETRY_OPTOUT"); + result.Content.Should().Contain("FALLOUT_GLOBAL_TOOL_VERSION"); + } + + [Fact] + public void LeavesPlainWordNukeAlone() + { + // The word "nuke" in a comment or string isn't a command invocation. + const string input = "# This was previously a NUKE-based build."; + var result = ScriptRewriter.Rewrite(input); + result.EditCount.Should().Be(0); + } +} diff --git a/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs b/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs index 1ce48f85a..50c3206f6 100644 --- a/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs +++ b/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs @@ -17,6 +17,8 @@ internal class Solution(SolutionModel model, AbsolutePath path) : Fallout.Common public Fallout.Common.ProjectModel.Project Fallout_Components => this.GetProject("Fallout.Components"); public Fallout.Common.ProjectModel.Project Fallout_GlobalTool => this.GetProject("Fallout.GlobalTool"); public Fallout.Common.ProjectModel.Project Fallout_GlobalTool_Tests => this.GetProject("Fallout.GlobalTool.Tests"); + public Fallout.Common.ProjectModel.Project Fallout_Migrate => this.GetProject("Fallout.Migrate"); + public Fallout.Common.ProjectModel.Project Fallout_Migrate_Tests => this.GetProject("Fallout.Migrate.Tests"); public Fallout.Common.ProjectModel.Project Fallout_MSBuildTasks => this.GetProject("Fallout.MSBuildTasks"); public Fallout.Common.ProjectModel.Project Fallout_ProjectModel => this.GetProject("Fallout.ProjectModel"); public Fallout.Common.ProjectModel.Project Fallout_ProjectModel_Tests => this.GetProject("Fallout.ProjectModel.Tests");