diff --git a/src/Fallout.Cli/Program.Setup.cs b/src/Fallout.Cli/BuildScaffolder.cs similarity index 79% rename from src/Fallout.Cli/Program.Setup.cs rename to src/Fallout.Cli/BuildScaffolder.cs index 036d1c68..2337e1d0 100644 --- a/src/Fallout.Cli/Program.Setup.cs +++ b/src/Fallout.Cli/BuildScaffolder.cs @@ -1,16 +1,9 @@ -using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection; -using System.Text; using Fallout.Common; -using Fallout.Common.Execution; using Fallout.Common.IO; -using Fallout.Common.Tooling; using Fallout.Common.Utilities; -using Fallout.Common.Utilities.Collections; -using Spectre.Console; using static Fallout.Common.Constants; using static Fallout.Common.EnvironmentInfo; using static Fallout.Common.Tooling.ProcessTasks; @@ -18,70 +11,27 @@ namespace Fallout.Cli; -partial class Program +/// Writes the build scripts, configuration file and solution wiring when scaffolding a build. +public interface IBuildScaffolder { - // ReSharper disable InconsistentNaming + void WriteBuildScripts(AbsolutePath scriptDirectory, AbsolutePath rootDirectory, AbsolutePath buildDirectory, string buildProjectName); + void WriteConfigurationFile(AbsolutePath rootDirectory, AbsolutePath solutionFile); + void UpdateSolutionFileContent(List content, string buildProjectFileRelative, string buildProjectGuid, string buildProjectName); + string[] GetTemplate(string templateName); +} +/// +public sealed class BuildScaffolder : IBuildScaffolder +{ private const string PROJECT_KIND = "9A19103F-16F7-4668-BE54-9A1E7A4F7556"; - // Transitional shim: cake (still a legacy handler) invokes setup directly. Removed once cake is - // converted; the dispatcher itself resolves SetupCommand from the registry, not this. - internal static int Setup(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript) - => new Commands.SetupCommand(s_prompts).Execute(args, rootDirectory, buildScript); - - // Residual after the :setup command moved to SetupCommand: these scaffolding helpers are shared - // with update (and UpdateSolutionFileContent is exercised directly by tests). They move into a - // scaffolding service in the #392 collapse PR. - internal static void UpdateSolutionFileContent( - List content, - string buildProjectFileRelative, - string buildProjectGuid, - string buildProjectName) - { - if (content.Any(x => x.Contains(buildProjectFileRelative))) - return; - - var globalIndex = content.IndexOf("Global"); - Assert.True(globalIndex != -1, "Could not find a 'Global' section in solution file"); - - var projectConfigurationIndex = content.FindIndex(x => x.Contains("GlobalSection(ProjectConfigurationPlatforms)")); - if (projectConfigurationIndex == -1) - { - var solutionConfigurationIndex = content.FindIndex(x => x.Contains("GlobalSection(SolutionConfigurationPlatforms)")); - if (solutionConfigurationIndex == -1) - { - content.Insert(globalIndex + 1, "\tGlobalSection(SolutionConfigurationPlatforms) = preSolution"); - content.Insert(globalIndex + 2, "\t\tDebug|Any CPU = Debug|Any CPU"); - content.Insert(globalIndex + 3, "\t\tRelease|Any CPU = Release|Any CPU"); - content.Insert(globalIndex + 4, "\tEndGlobalSection"); - - solutionConfigurationIndex = globalIndex + 1; - } - - var endGlobalSectionIndex = content.FindIndex(solutionConfigurationIndex, x => x.Contains("EndGlobalSection")); - - content.Insert(endGlobalSectionIndex + 1, "\tGlobalSection(ProjectConfigurationPlatforms) = postSolution"); - content.Insert(endGlobalSectionIndex + 2, "\tEndGlobalSection"); - - projectConfigurationIndex = endGlobalSectionIndex + 1; - } - - content.Insert(projectConfigurationIndex + 1, $"\t\t{{{buildProjectGuid}}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU"); - content.Insert(projectConfigurationIndex + 2, $"\t\t{{{buildProjectGuid}}}.Release|Any CPU.ActiveCfg = Release|Any CPU"); - - content.Insert(globalIndex, - $"Project(\"{{{PROJECT_KIND}}}\") = \"{buildProjectName}\", \"{buildProjectFileRelative}\", \"{{{buildProjectGuid}}}\""); - content.Insert(globalIndex + 1, - "EndProject"); - } - - internal static string[] GetTemplate(string templateName) + public string[] GetTemplate(string templateName) { - return ResourceUtility.GetResourceAllLines($"templates.{templateName}"); + // Anchored to the Fallout.Cli root namespace where the templates/* resources are embedded. + return ResourceUtility.GetResourceAllLines($"templates.{templateName}"); } - - internal static void WriteBuildScripts( + public void WriteBuildScripts( AbsolutePath scriptDirectory, AbsolutePath rootDirectory, AbsolutePath buildDirectory, @@ -121,7 +71,7 @@ internal static void WriteBuildScripts( tokens: GetDictionary( new { - FalloutCliVersion = typeof(Program).GetTypeInfo().Assembly.GetVersionText(), + FalloutCliVersion = typeof(BuildScaffolder).GetTypeInfo().Assembly.GetVersionText(), }))); } @@ -140,7 +90,7 @@ void MakeExecutable(AbsolutePath scriptFile) } } - internal static void WriteConfigurationFile(AbsolutePath rootDirectory, AbsolutePath solutionFile) + public void WriteConfigurationFile(AbsolutePath rootDirectory, AbsolutePath solutionFile) { var parametersFile = GetDefaultParametersFile(rootDirectory); var dictionary = new Dictionary { ["$schema"] = BuildSchemaFileName }; @@ -148,4 +98,47 @@ internal static void WriteConfigurationFile(AbsolutePath rootDirectory, Absolute dictionary["Solution"] = rootDirectory.GetUnixRelativePathTo(solutionFile).ToString(); parametersFile.WriteJson(dictionary, JsonExtensions.DefaultSerializerOptions); } + + public void UpdateSolutionFileContent( + List content, + string buildProjectFileRelative, + string buildProjectGuid, + string buildProjectName) + { + if (content.Any(x => x.Contains(buildProjectFileRelative))) + return; + + var globalIndex = content.IndexOf("Global"); + Assert.True(globalIndex != -1, "Could not find a 'Global' section in solution file"); + + var projectConfigurationIndex = content.FindIndex(x => x.Contains("GlobalSection(ProjectConfigurationPlatforms)")); + if (projectConfigurationIndex == -1) + { + var solutionConfigurationIndex = content.FindIndex(x => x.Contains("GlobalSection(SolutionConfigurationPlatforms)")); + if (solutionConfigurationIndex == -1) + { + content.Insert(globalIndex + 1, "\tGlobalSection(SolutionConfigurationPlatforms) = preSolution"); + content.Insert(globalIndex + 2, "\t\tDebug|Any CPU = Debug|Any CPU"); + content.Insert(globalIndex + 3, "\t\tRelease|Any CPU = Release|Any CPU"); + content.Insert(globalIndex + 4, "\tEndGlobalSection"); + + solutionConfigurationIndex = globalIndex + 1; + } + + var endGlobalSectionIndex = content.FindIndex(solutionConfigurationIndex, x => x.Contains("EndGlobalSection")); + + content.Insert(endGlobalSectionIndex + 1, "\tGlobalSection(ProjectConfigurationPlatforms) = postSolution"); + content.Insert(endGlobalSectionIndex + 2, "\tEndGlobalSection"); + + projectConfigurationIndex = endGlobalSectionIndex + 1; + } + + content.Insert(projectConfigurationIndex + 1, $"\t\t{{{buildProjectGuid}}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU"); + content.Insert(projectConfigurationIndex + 2, $"\t\t{{{buildProjectGuid}}}.Release|Any CPU.ActiveCfg = Release|Any CPU"); + + content.Insert(globalIndex, + $"Project(\"{{{PROJECT_KIND}}}\") = \"{buildProjectName}\", \"{buildProjectFileRelative}\", \"{{{buildProjectGuid}}}\""); + content.Insert(globalIndex + 1, + "EndProject"); + } } diff --git a/src/Fallout.Cli/Program.Cake.cs b/src/Fallout.Cli/CakeConverter.cs similarity index 68% rename from src/Fallout.Cli/Program.Cake.cs rename to src/Fallout.Cli/CakeConverter.cs index 5d9b7d76..85ea20fa 100644 --- a/src/Fallout.Cli/Program.Cake.cs +++ b/src/Fallout.Cli/CakeConverter.cs @@ -1,12 +1,10 @@ -ο»Ώusing System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Fallout.Common; -using Fallout.Common.Execution; using Fallout.Common.IO; -using Fallout.Solutions; using Fallout.Common.Tooling; using Fallout.Common.Utilities; using Fallout.Cli.Rewriting.Cake; @@ -15,19 +13,17 @@ namespace Fallout.Cli; -partial class Program +/// Best-effort syntax rewriting of Cake (*.cake) scripts into Fallout C#. +internal static class CakeConverter { - public const string CAKE_FILE_PATTERN = "*.cake"; + public const string FilePattern = "*.cake"; - // Residual after the :cake-convert/:cake-clean commands moved to CakeConvertCommand/ - // CakeCleanCommand: these .cake syntax-rewriting helpers stay on Program until the #392 collapse - // PR (GetCakeConvertedContent/GetCakePackages are exercised directly by tests). - internal static IEnumerable GetCakeFiles() + public static IEnumerable GetCakeFiles() { - return (TryGetRootDirectoryFrom(WorkingDirectory) ?? WorkingDirectory).GlobFiles($"**/{CAKE_FILE_PATTERN}"); + return (TryGetRootDirectoryFrom(WorkingDirectory) ?? WorkingDirectory).GlobFiles($"**/{FilePattern}"); } - internal static string GetCakeConvertedContent(string content) + public static string GetConvertedContent(string content) { var options = new CSharpParseOptions(LanguageVersion.Latest, DocumentationMode.None, SourceCodeKind.Script); var syntaxTree = CSharpSyntaxTree.ParseText(content, options); @@ -49,7 +45,7 @@ internal static string GetCakeConvertedContent(string content) .ToFullString(); } - internal static IEnumerable<(string Type, string Id, string Version)> GetCakePackages(string content) + public static IEnumerable<(string Type, string Id, string Version)> GetPackages(string content) { IEnumerable<(string Type, string Id, string Version)> GetPackages( string packageType, @@ -66,8 +62,8 @@ internal static string GetCakeConvertedContent(string content) } } - return GetPackages(PACKAGE_TYPE_DOWNLOAD, @"#tool ""nuget:\?package=(?'packageId'[\w\d\.]+)(&version=(?'version'[\w\d\.]+))?S*""") - .Concat(GetPackages(PACKAGE_TYPE_REFERENCE, @"#addin ""nuget:\?package=(?'packageId'[\w\d\.]+)(&version=(?'version'[\w\d\.]+))?S*""")) + return GetPackages(PackageManager.DownloadType, @"#tool ""nuget:\?package=(?'packageId'[\w\d\.]+)(&version=(?'version'[\w\d\.]+))?S*""") + .Concat(GetPackages(PackageManager.ReferenceType, @"#addin ""nuget:\?package=(?'packageId'[\w\d\.]+)(&version=(?'version'[\w\d\.]+))?S*""")) .Where(x => !x.Id.ContainsOrdinalIgnoreCase("Cake")); } } diff --git a/src/Fallout.Cli/CliConventions.cs b/src/Fallout.Cli/CliConventions.cs new file mode 100644 index 00000000..756bf64d --- /dev/null +++ b/src/Fallout.Cli/CliConventions.cs @@ -0,0 +1,18 @@ +using Fallout.Common; +using Fallout.Common.Utilities; + +namespace Fallout.Cli; + +/// Small CLI-wide conventions shared by the entry point and commands. +internal static class CliConventions +{ + /// The build-script file name for the current platform. + public static string CurrentBuildScriptName => EnvironmentInfo.IsWin ? "build.ps1" : "build.sh"; +} + +/// Prints the global-tool banner. +internal static class ToolBanner +{ + public static void Print() + => Host.Information($"NUKE Global Tool 🌐 {typeof(ToolBanner).Assembly.GetInformationalText()}"); +} diff --git a/src/Fallout.Cli/Commands/AddPackageCommand.cs b/src/Fallout.Cli/Commands/AddPackageCommand.cs index 2e4879ad..9383b293 100644 --- a/src/Fallout.Cli/Commands/AddPackageCommand.cs +++ b/src/Fallout.Cli/Commands/AddPackageCommand.cs @@ -13,11 +13,20 @@ namespace Fallout.Cli.Commands; /// public sealed class AddPackageCommand : IFalloutCommand { + private readonly IConfigurationReader _configuration; + private readonly IPackageManager _packages; + + public AddPackageCommand(IConfigurationReader configuration, IPackageManager packages) + { + _configuration = configuration; + _packages = packages; + } + public string Name => "add-package"; public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript) { - Program.PrintInfo(); + ToolBanner.Print(); Logging.Configure(); Telemetry.AddPackage(); ProjectModelTasks.Initialize(); @@ -30,19 +39,17 @@ public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath build NuGetPackageResolver.GetGlobalInstalledPackage(packageId, version: null, packagesConfigFile: null)?.Version.ToString()) .NotNull("packageVersion != null"); - // GetConfiguration / AddOrReplacePackage / BUILD_PROJECT_FILE / PACKAGE_TYPE_* are shared - // helpers still on Program; they move into services in the final #392 collapse PR. - var configuration = Program.GetConfiguration(buildScript, evaluate: true); - var buildProjectFile = configuration[Program.BUILD_PROJECT_FILE]; + var configuration = _configuration.Read(buildScript, evaluate: true); + var buildProjectFile = configuration[ConfigurationReader.BuildProjectFileKey]; Host.Information($"Installing {packageId}/{packageVersion} to {buildProjectFile} ..."); - Program.AddOrReplacePackage(packageId, packageVersion, Program.PACKAGE_TYPE_DOWNLOAD, buildProjectFile); + _packages.AddOrReplacePackage(packageId, packageVersion, PackageManager.DownloadType, buildProjectFile); DotNetTasks.DotNet($"restore {buildProjectFile}"); var installedPackage = NuGetPackageResolver.GetGlobalInstalledPackage(packageId, packageVersion, packagesConfigFile: null) .NotNull("installedPackage != null"); var hasToolsDirectory = installedPackage.Directory.GlobDirectories("tools").Any(); if (!hasToolsDirectory) - Program.AddOrReplacePackage(packageId, packageVersion, Program.PACKAGE_TYPE_REFERENCE, buildProjectFile); + _packages.AddOrReplacePackage(packageId, packageVersion, PackageManager.ReferenceType, buildProjectFile); Host.Information($"Done installing {packageId}/{packageVersion} to {buildProjectFile}"); return 0; diff --git a/src/Fallout.Cli/Commands/CakeCleanCommand.cs b/src/Fallout.Cli/Commands/CakeCleanCommand.cs index 07205c4a..341848e0 100644 --- a/src/Fallout.Cli/Commands/CakeCleanCommand.cs +++ b/src/Fallout.Cli/Commands/CakeCleanCommand.cs @@ -18,7 +18,7 @@ public sealed class CakeCleanCommand : IFalloutCommand public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript) { - var cakeFiles = Program.GetCakeFiles().ToList(); + var cakeFiles = CakeConverter.GetCakeFiles().ToList(); Host.Information("Found .cake files:"); cakeFiles.ForEach(x => Host.Debug($" - {x}")); diff --git a/src/Fallout.Cli/Commands/CakeConvertCommand.cs b/src/Fallout.Cli/Commands/CakeConvertCommand.cs index 801c1b59..c91c1775 100644 --- a/src/Fallout.Cli/Commands/CakeConvertCommand.cs +++ b/src/Fallout.Cli/Commands/CakeConvertCommand.cs @@ -16,17 +16,27 @@ namespace Fallout.Cli.Commands; public sealed class CakeConvertCommand : IFalloutCommand { private readonly IConsolePrompts _prompts; + private readonly IConfigurationReader _configuration; + private readonly IPackageManager _packages; + private readonly SetupCommand _setup; - public CakeConvertCommand(IConsolePrompts prompts) => _prompts = prompts; + public CakeConvertCommand( + IConsolePrompts prompts, + IConfigurationReader configuration, + IPackageManager packages, + SetupCommand setup) + { + _prompts = prompts; + _configuration = configuration; + _packages = packages; + _setup = setup; + } public string Name => "cake-convert"; - // The .cake syntax-rewriting helpers (GetCakeFiles/GetCakeConvertedContent/GetCakePackages) and - // the shared GetConfiguration/AddOrReplacePackage helpers remain on Program until the #392 - // collapse PR; GetCakeConvertedContent/GetCakePackages are also exercised directly by tests. public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript) { - Program.PrintInfo(); + ToolBanner.Print(); Logging.Configure(); Telemetry.ConvertCake(); ProjectModelTasks.Initialize(); @@ -53,27 +63,27 @@ public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath build if (buildScript == null && _prompts.PromptForConfirmation("Should a NUKE project be created for better results?")) { - Program.Setup(args, rootDirectory: null, buildScript: null); + _setup.Execute(args, rootDirectory: null, buildScript: null); } - var buildScriptFile = WorkingDirectory / Program.CurrentBuildScriptName; + var buildScriptFile = WorkingDirectory / CliConventions.CurrentBuildScriptName; var buildProjectFile = buildScriptFile.Exists() - ? Program.GetConfiguration(buildScriptFile, evaluate: true) - .GetValueOrDefault(Program.BUILD_PROJECT_FILE, defaultValue: null) + ? _configuration.Read(buildScriptFile, evaluate: true) + .GetValueOrDefault(ConfigurationReader.BuildProjectFileKey, defaultValue: null) : null; - foreach (var cakeFile in Program.GetCakeFiles()) + foreach (var cakeFile in CakeConverter.GetCakeFiles()) { var outputFile = cakeFile.Parent / cakeFile.NameWithoutExtension.Capitalize() + ".cs"; - var content = Program.GetCakeConvertedContent(cakeFile.ReadAllText()); + var content = CakeConverter.GetConvertedContent(cakeFile.ReadAllText()); outputFile.WriteAllText(content); } if (buildProjectFile != null) { - var packages = Program.GetCakeFiles().SelectMany(x => Program.GetCakePackages(x.ReadAllText())); + var packages = CakeConverter.GetCakeFiles().SelectMany(x => CakeConverter.GetPackages(x.ReadAllText())); foreach (var package in packages) - Program.AddOrReplacePackage(package.Id, package.Version, package.Type, buildProjectFile); + _packages.AddOrReplacePackage(package.Id, package.Version, package.Type, buildProjectFile); } return 0; diff --git a/src/Fallout.Cli/Commands/DelegateCommand.cs b/src/Fallout.Cli/Commands/DelegateCommand.cs deleted file mode 100644 index 5a60e855..00000000 --- a/src/Fallout.Cli/Commands/DelegateCommand.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Fallout.Common.IO; - -namespace Fallout.Cli.Commands; - -/// -/// Transitional adapter that exposes a not-yet-extracted Program.X handler as an -/// , so the dispatcher can route every command uniformly through the -/// registry while the per-command conversion (issue #392) lands one PR at a time. Each conversion -/// replaces one registration of this adapter with a real command type; the adapter is deleted once -/// the last legacy handler is gone. -/// -internal sealed class DelegateCommand : IFalloutCommand -{ - private readonly Func _handler; - - public DelegateCommand(string name, Func handler) - { - Name = name; - _handler = handler; - } - - public string Name { get; } - - public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript) - => _handler(args, rootDirectory, buildScript); -} diff --git a/src/Fallout.Cli/Commands/GetConfigurationCommand.cs b/src/Fallout.Cli/Commands/GetConfigurationCommand.cs index 4af696eb..990aa1d6 100644 --- a/src/Fallout.Cli/Commands/GetConfigurationCommand.cs +++ b/src/Fallout.Cli/Commands/GetConfigurationCommand.cs @@ -11,13 +11,15 @@ namespace Fallout.Cli.Commands; /// public sealed class GetConfigurationCommand : IFalloutCommand { + private readonly IConfigurationReader _configuration; + + public GetConfigurationCommand(IConfigurationReader configuration) => _configuration = configuration; + public string Name => "get-configuration"; public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript) { - // Program.GetConfiguration(buildScript, evaluate) is shared with add-package/update/cake; - // it moves into a configuration service in the final #392 collapse PR. - var configuration = Program.GetConfiguration(buildScript.NotNull(), evaluate: false); + var configuration = _configuration.Read(buildScript.NotNull(), evaluate: false); Host.Information($"Configuration from {buildScript}:"); configuration.ForEach(x => Console.WriteLine($"{x.Key} = {x.Value}")); diff --git a/src/Fallout.Cli/Commands/SetupCommand.cs b/src/Fallout.Cli/Commands/SetupCommand.cs index fda9bb7e..121d5dfa 100644 --- a/src/Fallout.Cli/Commands/SetupCommand.cs +++ b/src/Fallout.Cli/Commands/SetupCommand.cs @@ -25,16 +25,19 @@ public sealed class SetupCommand : IFalloutCommand private const string TARGET_FRAMEWORK = "net8.0"; private readonly IConsolePrompts _prompts; + private readonly IBuildScaffolder _scaffolder; - public SetupCommand(IConsolePrompts prompts) => _prompts = prompts; + public SetupCommand(IConsolePrompts prompts, IBuildScaffolder scaffolder) + { + _prompts = prompts; + _scaffolder = scaffolder; + } public string Name => "setup"; - // The scaffolding helpers (WriteBuildScripts/WriteConfigurationFile/UpdateSolutionFileContent/ - // GetTemplate) remain on Program as internal residual until the #392 collapse PR extracts them. public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript) { - Program.PrintInfo(); + ToolBanner.Print(); Logging.Configure(); Telemetry.SetupBuild(); @@ -100,25 +103,25 @@ public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath build (rootDirectory / FalloutDirectoryName).CreateDirectory(); - Program.WriteBuildScripts( + _scaffolder.WriteBuildScripts( scriptDirectory: WorkingDirectory, rootDirectory, buildDirectory, buildProjectName); - Program.WriteConfigurationFile(rootDirectory, solutionFile); + _scaffolder.WriteConfigurationFile(rootDirectory, solutionFile); if (solutionFile != null) { var solutionFileContent = solutionFile.ReadAllLines().ToList(); var buildProjectFileRelative = solutionFile.Parent.GetWinRelativePathTo(buildProjectFile); - Program.UpdateSolutionFileContent(solutionFileContent, buildProjectFileRelative, buildProjectGuid, buildProjectName); + _scaffolder.UpdateSolutionFileContent(solutionFileContent, buildProjectFileRelative, buildProjectGuid, buildProjectName); solutionFile.WriteAllLines(solutionFileContent, Encoding.UTF8); } buildProjectFile.WriteAllLines( FillTemplate( - Program.GetTemplate("_build.csproj"), + _scaffolder.GetTemplate("_build.csproj"), GetDictionary( new { @@ -129,10 +132,10 @@ public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath build NukeVersion = nukeVersion, }))); - (buildDirectory / "Directory.Build.props").WriteAllLines(Program.GetTemplate("Directory.Build.props")); - (buildDirectory / "Directory.Build.targets").WriteAllLines(Program.GetTemplate("Directory.Build.targets")); - (buildDirectory / "Build.cs").WriteAllLines(FillTemplate(Program.GetTemplate("Build.cs"))); - (buildDirectory / "Configuration.cs").WriteAllLines(Program.GetTemplate("Configuration.cs")); + (buildDirectory / "Directory.Build.props").WriteAllLines(_scaffolder.GetTemplate("Directory.Build.props")); + (buildDirectory / "Directory.Build.targets").WriteAllLines(_scaffolder.GetTemplate("Directory.Build.targets")); + (buildDirectory / "Build.cs").WriteAllLines(FillTemplate(_scaffolder.GetTemplate("Build.cs"))); + (buildDirectory / "Configuration.cs").WriteAllLines(_scaffolder.GetTemplate("Configuration.cs")); #endregion diff --git a/src/Fallout.Cli/Commands/UpdateCommand.cs b/src/Fallout.Cli/Commands/UpdateCommand.cs index 6bf7ce2d..d8f0590a 100644 --- a/src/Fallout.Cli/Commands/UpdateCommand.cs +++ b/src/Fallout.Cli/Commands/UpdateCommand.cs @@ -18,16 +18,21 @@ namespace Fallout.Cli.Commands; public sealed class UpdateCommand : IFalloutCommand { private readonly IConsolePrompts _prompts; + private readonly IConfigurationReader _configuration; + private readonly IBuildScaffolder _scaffolder; - public UpdateCommand(IConsolePrompts prompts) => _prompts = prompts; + public UpdateCommand(IConsolePrompts prompts, IConfigurationReader configuration, IBuildScaffolder scaffolder) + { + _prompts = prompts; + _configuration = configuration; + _scaffolder = scaffolder; + } public string Name => "update"; - // GetConfiguration / WriteBuildScripts / WriteConfigurationFile / BUILD_PROJECT_FILE remain on - // Program as internal residual until the #392 collapse PR extracts them into services. public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript) { - Program.PrintInfo(); + ToolBanner.Print(); Logging.Configure(); Assert.NotNull(rootDirectory); @@ -46,27 +51,27 @@ public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath build return 0; } - private static void UpdateBuildScripts(AbsolutePath rootDirectory, AbsolutePath buildScript) + private void UpdateBuildScripts(AbsolutePath rootDirectory, AbsolutePath buildScript) { - var configuration = Program.GetConfiguration(buildScript, evaluate: true); - var buildProjectFile = (AbsolutePath) configuration[Program.BUILD_PROJECT_FILE]; + var configuration = _configuration.Read(buildScript, evaluate: true); + var buildProjectFile = (AbsolutePath) configuration[ConfigurationReader.BuildProjectFileKey]; - Program.WriteBuildScripts( + _scaffolder.WriteBuildScripts( scriptDirectory: buildScript.Parent, rootDirectory, buildDirectory: buildProjectFile.NotNull().Parent, buildProjectName: Path.GetFileNameWithoutExtension(buildProjectFile)); } - private static void UpdateBuildProject(AbsolutePath buildScript) + private void UpdateBuildProject(AbsolutePath buildScript) { - var configuration = Program.GetConfiguration(buildScript, evaluate: true); - var projectFile = configuration[Program.BUILD_PROJECT_FILE]; + var configuration = _configuration.Read(buildScript, evaluate: true); + var projectFile = configuration[ConfigurationReader.BuildProjectFileKey]; ProjectModelTasks.Initialize(); ProjectUpdater.Update(projectFile); } - private static void UpdateConfigurationFile(AbsolutePath rootDirectory) + private void UpdateConfigurationFile(AbsolutePath rootDirectory) { var configurationFile = rootDirectory / FalloutDirectoryName; if (!configurationFile.Exists()) @@ -75,7 +80,7 @@ private static void UpdateConfigurationFile(AbsolutePath rootDirectory) var solutionFile = rootDirectory / configurationFile.ReadAllLines().FirstOrDefault(x => !x.IsNullOrEmpty()); configurationFile.DeleteFile(); - Program.WriteConfigurationFile(rootDirectory, solutionFile); + _scaffolder.WriteConfigurationFile(rootDirectory, solutionFile); Host.Warning($"The previous {FalloutFileName} file was transformed to a {FalloutDirectoryName} directory."); Host.Warning($"The .tmp directory can be cleared, as it is moved to {FalloutDirectoryName}/temp as well."); if (solutionFile != null) diff --git a/src/Fallout.Cli/Program.GetConfiguration.cs b/src/Fallout.Cli/ConfigurationReader.cs similarity index 52% rename from src/Fallout.Cli/Program.GetConfiguration.cs rename to src/Fallout.Cli/ConfigurationReader.cs index 6cda748b..ebbbc26a 100644 --- a/src/Fallout.Cli/Program.GetConfiguration.cs +++ b/src/Fallout.Cli/ConfigurationReader.cs @@ -1,27 +1,24 @@ -ο»Ώusing System; using System.Collections.Generic; using System.IO; using System.Linq; -using Fallout.Common; using Fallout.Common.IO; using Fallout.Common.Utilities; -using Fallout.Common.Utilities.Collections; namespace Fallout.Cli; -partial class Program +/// Parses the # CONFIGURATION … # EXECUTION block out of a build script. +public interface IConfigurationReader { - // internal (not private): shared with AddPackage/Update/Cake; will move into a configuration - // service in the final #392 collapse PR. - internal const string BUILD_PROJECT_FILE = nameof(BUILD_PROJECT_FILE); - private const string TEMP_DIRECTORY = nameof(TEMP_DIRECTORY); - private const string DOTNET_GLOBAL_FILE = nameof(DOTNET_GLOBAL_FILE); - private const string DOTNET_INSTALL_URL = nameof(DOTNET_INSTALL_URL); - private const string DOTNET_CHANNEL = nameof(DOTNET_CHANNEL); + Dictionary Read(AbsolutePath buildScript, bool evaluate); +} + +/// +public sealed class ConfigurationReader : IConfigurationReader +{ + /// Configuration key holding the build project file path. + public const string BuildProjectFileKey = "BUILD_PROJECT_FILE"; - // Residual after the :get-configuration command moved to GetConfigurationCommand: this helper is - // shared with add-package/update/cake and moves into a configuration service in the #392 collapse PR. - internal static Dictionary GetConfiguration(AbsolutePath buildScript, bool evaluate) + public Dictionary Read(AbsolutePath buildScript, bool evaluate) { string ReplaceScriptDirectory(string value) => evaluate diff --git a/src/Fallout.Cli/PackageManager.cs b/src/Fallout.Cli/PackageManager.cs new file mode 100644 index 00000000..ce131490 --- /dev/null +++ b/src/Fallout.Cli/PackageManager.cs @@ -0,0 +1,35 @@ +using System.Linq; +using Fallout.Common; +using Fallout.Common.Utilities; +using Fallout.Solutions; + +namespace Fallout.Cli; + +/// Adds or replaces a package entry in the build project file. +public interface IPackageManager +{ + void AddOrReplacePackage(string packageId, string packageVersion, string packageType, string buildProjectFile); +} + +/// +public sealed class PackageManager : IPackageManager +{ + public const string DownloadType = "PackageDownload"; + public const string ReferenceType = "PackageReference"; + + public void AddOrReplacePackage(string packageId, string packageVersion, string packageType, string buildProjectFile) + { + var buildProject = ProjectModelTasks.ParseProject(buildProjectFile).NotNull(); + + var previousPackage = buildProject.Items.SingleOrDefault(x => x.EvaluatedInclude == packageId); + if (previousPackage != null) + buildProject.RemoveItem(previousPackage); + + var packageDownloadItem = buildProject.AddItem(packageType, packageId).Single(); + packageDownloadItem.Xml.AddMetadata( + "Version", + packageType == ReferenceType ? packageVersion : $"[{packageVersion}]", + expressAsAttribute: true); + buildProject.Save(); + } +} diff --git a/src/Fallout.Cli/Program.AddPackage.cs b/src/Fallout.Cli/Program.AddPackage.cs deleted file mode 100644 index 87852ae4..00000000 --- a/src/Fallout.Cli/Program.AddPackage.cs +++ /dev/null @@ -1,34 +0,0 @@ -ο»Ώusing System; -using System.Linq; -using Fallout.Common; -using Fallout.Common.Execution; -using Fallout.Common.IO; -using Fallout.Solutions; -using Fallout.Common.Tooling; -using Fallout.Common.Tools.DotNet; - -namespace Fallout.Cli; - -partial class Program -{ - public const string PACKAGE_TYPE_DOWNLOAD = "PackageDownload"; - public const string PACKAGE_TYPE_REFERENCE = "PackageReference"; - - // Residual after the :add-package command moved to AddPackageCommand: this helper is shared with - // cake and moves into a package service in the #392 collapse PR. - internal static void AddOrReplacePackage(string packageId, string packageVersion, string packageType, string buildProjectFile) - { - var buildProject = ProjectModelTasks.ParseProject(buildProjectFile).NotNull(); - - var previousPackage = buildProject.Items.SingleOrDefault(x => x.EvaluatedInclude == packageId); - if (previousPackage != null) - buildProject.RemoveItem(previousPackage); - - var packageDownloadItem = buildProject.AddItem(packageType, packageId).Single(); - packageDownloadItem.Xml.AddMetadata( - "Version", - packageType == PACKAGE_TYPE_REFERENCE ? packageVersion : $"[{packageVersion}]", - expressAsAttribute: true); - buildProject.Save(); - } -} diff --git a/src/Fallout.Cli/Program.cs b/src/Fallout.Cli/Program.cs index b5f165af..8c686e1a 100644 --- a/src/Fallout.Cli/Program.cs +++ b/src/Fallout.Cli/Program.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using Fallout.Cli.Commands; +using Fallout.Cli.Commands.Navigation; using Fallout.Cli.Prompts; using Fallout.Common; using Fallout.Common.IO; @@ -11,10 +12,8 @@ namespace Fallout.Cli; -public partial class Program +public class Program { - internal static string CurrentBuildScriptName => EnvironmentInfo.IsWin ? "build.ps1" : "build.sh"; - private static int Main(string[] args) { Console.OutputEncoding = Encoding.UTF8; @@ -24,7 +23,7 @@ private static int Main(string[] args) var rootDirectory = TryGetRootDirectory(); var buildScript = rootDirectory != null - ? rootDirectory.GetFiles(CurrentBuildScriptName, depth: 2) + ? rootDirectory.GetFiles(CliConventions.CurrentBuildScriptName, depth: 2) .FirstOrDefault(x => Constants.TryGetRootDirectoryFrom(x.Parent) == rootDirectory) : null; @@ -43,7 +42,11 @@ private static ServiceProvider BuildServiceProvider() var services = new ServiceCollection(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + RegisterCommands(services); return services.BuildServiceProvider(); @@ -51,45 +54,31 @@ private static ServiceProvider BuildServiceProvider() private static void RegisterCommands(IServiceCollection services) { - // Real command types β€” issue #392 converts one legacy handler per PR. services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - - // Legacy handlers still living on Program, adapted until they are extracted into command - // types. Each conversion deletes one line here plus its Program.X.cs partial. services.AddSingleton(); services.AddSingleton(); - - // Legacy handlers still living on Program, adapted until they are extracted into command - // types. Each conversion deletes one line here plus its Program.X.cs partial. services.AddSingleton(); services.AddSingleton(); - - // Legacy handlers still living on Program, adapted until they are extracted into command - // types. Each conversion deletes one line here plus its Program.X.cs partial. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // Legacy handlers still living on Program, adapted until they are extracted into command - // types. Each conversion deletes one line here plus its Program.X.cs partial. - } - - internal static void PrintInfo() - { - Host.Information($"NUKE Global Tool 🌐 {typeof(Program).Assembly.GetInformationalText()}"); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // SetupCommand is also resolved directly by CakeConvertCommand, so register the concrete type + // and expose the same instance as an IFalloutCommand. + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); } private static AbsolutePath TryGetRootDirectory() { // TODO: copied in FalloutBuild.GetRootDirectory - AbsolutePath parameterValue = EnvironmentInfo.GetNamedArgument(Constants.RootDirectoryParameterName); + var parameterValue = EnvironmentInfo.GetNamedArgument(Constants.RootDirectoryParameterName); if (parameterValue != null) return parameterValue; @@ -98,19 +87,4 @@ private static AbsolutePath TryGetRootDirectory() return Constants.TryGetRootDirectoryFrom(Directory.GetCurrentDirectory()); } - - // ── Transitional Spectre prompt delegators ────────────────────────────────────────────────── - // The implementations now live in SpectreConsolePrompts; these forwards keep the not-yet-extracted - // Program.X.cs command handlers compiling. As each handler becomes a command type taking - // IConsolePrompts via the constructor, its use of these disappears; the last conversion deletes them. - private static readonly IConsolePrompts s_prompts = new SpectreConsolePrompts(); - - private static void ShowInput(string emoji, string title, string value) => s_prompts.ShowInput(emoji, title, value); - private static void ShowCompletion(string title) => s_prompts.ShowCompletion(title); - private static void ClearPreviousLine() => s_prompts.ClearPreviousLine(); - private static bool PromptForConfirmation(string question) => s_prompts.PromptForConfirmation(question); - private static string PromptForInput(string question, string defaultValue = null) => s_prompts.PromptForInput(question, defaultValue); - private static string PromptForSecret(string title, int? minLength = null) => s_prompts.PromptForSecret(title, minLength); - private static T PromptForChoice(string question, params (T Value, string Description)[] choices) => s_prompts.PromptForChoice(question, choices); - private static void ConfirmExecution(string title, Action action) => s_prompts.ConfirmExecution(title, action); } diff --git a/tests/Fallout.Cli.Tests/CakeConversionTests.cs b/tests/Fallout.Cli.Tests/CakeConversionTests.cs index 8357034f..fae2af97 100644 --- a/tests/Fallout.Cli.Tests/CakeConversionTests.cs +++ b/tests/Fallout.Cli.Tests/CakeConversionTests.cs @@ -18,7 +18,7 @@ public class CakeConversionTests [MemberData(nameof(CakeFileNames))] public Task Test(AbsolutePath file) { - var converted = Program.GetCakeConvertedContent(file.ReadAllText()); + var converted = CakeConverter.GetConvertedContent(file.ReadAllText()); return Verifier.Verify(converted, extension: "cs") .UseDirectory(CakeScriptsDirectory) .UseFileName(file.NameWithoutExtension); @@ -29,9 +29,9 @@ public void TestPackages() { var content = (CakeScriptsDirectory / "references.cake").ReadAllText(); - var packages = Program.GetCakePackages(content).ToList(); - packages.Should().Contain((Program.PACKAGE_TYPE_DOWNLOAD, "GitVersion.CommandLine", "4.0.0")); - packages.Should().Contain((Program.PACKAGE_TYPE_REFERENCE, "SharpZipLib", "1.2.0")); + var packages = CakeConverter.GetPackages(content).ToList(); + packages.Should().Contain((PackageManager.DownloadType, "GitVersion.CommandLine", "4.0.0")); + packages.Should().Contain((PackageManager.ReferenceType, "SharpZipLib", "1.2.0")); packages.Should().Contain(x => x.Id == "TeamCity.Dotnet.Integration" && NuGetVersion.Parse(x.Version) > NuGetVersion.Parse("1.0.10")); packages.Should().NotContain(x => x.Id.Contains("Cake")); @@ -40,5 +40,5 @@ public void TestPackages() private static AbsolutePath CakeScriptsDirectory => RootDirectory / "tests" / "Fallout.Cli.Tests" / "cake-scripts"; public static IEnumerable CakeFileNames - => CakeScriptsDirectory.GlobFiles(Program.CAKE_FILE_PATTERN).Select(x => new object[] { x }); + => CakeScriptsDirectory.GlobFiles(CakeConverter.FilePattern).Select(x => new object[] { x }); } diff --git a/tests/Fallout.Cli.Tests/Commands/GetConfigurationCommandTests.cs b/tests/Fallout.Cli.Tests/Commands/GetConfigurationCommandTests.cs index 23adf0ba..0acf14bc 100644 --- a/tests/Fallout.Cli.Tests/Commands/GetConfigurationCommandTests.cs +++ b/tests/Fallout.Cli.Tests/Commands/GetConfigurationCommandTests.cs @@ -11,7 +11,7 @@ public class GetConfigurationCommandTests { [Fact] public void Name_IsGetConfiguration() - => new GetConfigurationCommand().Name.Should().Be("get-configuration"); + => new GetConfigurationCommand(new ConfigurationReader()).Name.Should().Be("get-configuration"); [Fact] public void Execute_ParsesConfigurationBlock_ReturnsZero() @@ -31,7 +31,7 @@ public void Execute_ParsesConfigurationBlock_ReturnsZero() "# EXECUTION", "dotnet run")); - new GetConfigurationCommand().Execute([], dir, buildScript).Should().Be(0); + new GetConfigurationCommand(new ConfigurationReader()).Execute([], dir, buildScript).Should().Be(0); } finally { diff --git a/tests/Fallout.Cli.Tests/Commands/UpdateCommandTests.cs b/tests/Fallout.Cli.Tests/Commands/UpdateCommandTests.cs index d3c554f6..d3cecca8 100644 --- a/tests/Fallout.Cli.Tests/Commands/UpdateCommandTests.cs +++ b/tests/Fallout.Cli.Tests/Commands/UpdateCommandTests.cs @@ -11,7 +11,7 @@ public class UpdateCommandTests { [Fact] public void Name_IsUpdate() - => new UpdateCommand(new FakeConsolePrompts()).Name.Should().Be("update"); + => new UpdateCommand(new FakeConsolePrompts(), new ConfigurationReader(), new BuildScaffolder()).Name.Should().Be("update"); [Fact] public void Execute_NoBuildScript_DeclineAll_ReturnsZeroAndReportsCompletion() @@ -23,7 +23,7 @@ public void Execute_NoBuildScript_DeclineAll_ReturnsZeroAndReportsCompletion() { // No build script and every confirmation declined β†’ no update steps run, but the command // still completes cleanly. - new UpdateCommand(prompts).Execute([], dir, buildScript: null).Should().Be(0); + new UpdateCommand(prompts, new ConfigurationReader(), new BuildScaffolder()).Execute([], dir, buildScript: null).Should().Be(0); prompts.Completions.Should().Contain("Updates"); } diff --git a/tests/Fallout.Cli.Tests/ConfigurationReaderTests.cs b/tests/Fallout.Cli.Tests/ConfigurationReaderTests.cs new file mode 100644 index 00000000..5ee90d34 --- /dev/null +++ b/tests/Fallout.Cli.Tests/ConfigurationReaderTests.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using Fallout.Common.IO; +using FluentAssertions; +using Xunit; + +namespace Fallout.Cli.Tests; + +public class ConfigurationReaderTests +{ + private static AbsolutePath WriteScript(AbsolutePath dir) => + WriteScript(dir, string.Join("\n", + "# CONFIGURATION", + "##############", + "", + "BUILD_PROJECT_FILE=\"build/_build.csproj\"", + "TEMP_DIRECTORY=\"$SCRIPT_DIR/.fallout/temp\"", + "", + "# EXECUTION", + "dotnet run")); + + private static AbsolutePath WriteScript(AbsolutePath dir, string content) + { + var buildScript = dir / "build.sh"; + File.WriteAllText(buildScript, content); + return buildScript; + } + + [Fact] + public void Read_ParsesConfigurationEntries() + { + using var temp = TempDir.Create(); + var buildScript = WriteScript(temp.Path); + + var configuration = new ConfigurationReader().Read(buildScript, evaluate: false); + + configuration.Should().ContainKey(ConfigurationReader.BuildProjectFileKey) + .WhoseValue.Should().Be("build/_build.csproj"); + } + + [Fact] + public void Read_WhenEvaluating_ReplacesScriptDirectoryToken() + { + using var temp = TempDir.Create(); + var buildScript = WriteScript(temp.Path); + + var configuration = new ConfigurationReader().Read(buildScript, evaluate: true); + + configuration["TEMP_DIRECTORY"].Should().NotContain("$SCRIPT_DIR") + .And.Contain(buildScript.Parent); + } + + private sealed class TempDir : IDisposable + { + public AbsolutePath Path { get; } + + private TempDir(AbsolutePath path) => Path = path; + + public static TempDir Create() + { + var dir = (AbsolutePath)System.IO.Path.Combine(System.IO.Path.GetTempPath(), "fallout-cfgreader-" + Guid.NewGuid().ToString("N")); + dir.CreateDirectory(); + return new TempDir(dir); + } + + public void Dispose() + { + if (Directory.Exists(Path)) + Directory.Delete(Path, recursive: true); + } + } +} diff --git a/tests/Fallout.Cli.Tests/UpdateSolutionFileContentTests.cs b/tests/Fallout.Cli.Tests/UpdateSolutionFileContentTests.cs index 0663a166..813aeae7 100644 --- a/tests/Fallout.Cli.Tests/UpdateSolutionFileContentTests.cs +++ b/tests/Fallout.Cli.Tests/UpdateSolutionFileContentTests.cs @@ -131,7 +131,7 @@ public class UpdateSolutionFileContentTests public Task Test(int number, string input, string expected) { var content = input.SplitLineBreaks().ToList(); - Program.UpdateSolutionFileContent(content, "RELATIVE", "GUID", "NAME"); + new BuildScaffolder().UpdateSolutionFileContent(content, "RELATIVE", "GUID", "NAME"); return Verifier.Verify(expected) .UseParameters(number);