Skip to content
Merged
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
2 changes: 2 additions & 0 deletions fallout.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<Project Path="src\Fallout.Common\Fallout.Common.csproj" />
<Project Path="src\Fallout.Components\Fallout.Components.csproj" />
<Project Path="src\Fallout.GlobalTool\Fallout.GlobalTool.csproj" />
<Project Path="src\Fallout.Migrate\Fallout.Migrate.csproj" />
<Project Path="src\Fallout.MSBuildTasks\Fallout.MSBuildTasks.csproj" />
<Project Path="src\Fallout.ProjectModel\Fallout.ProjectModel.csproj" />
<Project Path="src\Fallout.SolutionModel\Fallout.SolutionModel.csproj" />
Expand All @@ -30,6 +31,7 @@
<Project Path="tests\Fallout.Build.Tests\Fallout.Build.Tests.csproj" />
<Project Path="tests\Fallout.Common.Tests\Fallout.Common.Tests.csproj" />
<Project Path="tests\Fallout.GlobalTool.Tests\Fallout.GlobalTool.Tests.csproj" />
<Project Path="tests\Fallout.Migrate.Tests\Fallout.Migrate.Tests.csproj" />
<Project Path="tests\Fallout.ProjectModel.Tests\Fallout.ProjectModel.Tests.csproj" />
<Project Path="tests\Fallout.SolutionModel.Tests\Fallout.SolutionModel.Tests.csproj" />
<Project Path="tests\Fallout.SourceGenerators.Tests\Fallout.SourceGenerators.Tests.csproj" />
Expand Down
33 changes: 33 additions & 0 deletions src/Fallout.Migrate/CodeRewriter.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
38 changes: 38 additions & 0 deletions src/Fallout.Migrate/CsprojRewriter.cs
Original file line number Diff line number Diff line change
@@ -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. <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|" +
"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);
}
}
17 changes: 17 additions & 0 deletions src/Fallout.Migrate/Fallout.Migrate.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RollForward>LatestMajor</RollForward>
<PackAsTool>true</PackAsTool>
<ToolCommandName>fallout-migrate</ToolCommandName>
<Description>CLI that migrates an existing NUKE consumer repo to Fallout in one command.</Description>
<PackageTags>build automation continuous-integration tools orchestration migration</PackageTags>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Fallout.Migrate.Tests" />
</ItemGroup>

</Project>
136 changes: 136 additions & 0 deletions src/Fallout.Migrate/Migration.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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<string, RewriteResult> 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<string> Warnings { get; } = new();
}
}

public readonly record struct RewriteResult(string Content, int EditCount);
116 changes: 116 additions & 0 deletions src/Fallout.Migrate/Program.cs
Original file line number Diff line number Diff line change
@@ -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 <path>");
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
""");
}
}
35 changes: 35 additions & 0 deletions src/Fallout.Migrate/ScriptRewriter.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading