From 32f44da7d8c2996cbc00e43bc665f7921b91d23d Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Tue, 8 Sep 2020 23:56:20 +0200 Subject: [PATCH] Add implicit bootstrapping of modules Closes #2833 --- .../Fixtures/ScriptAnalyzerFixture.cs | 2 +- .../Unit/Scripting/ScriptRunnerTests.cs | 7 +-- .../Scripting/Analysis/IScriptAnalyzer.cs | 3 +- .../Scripting/Analysis/ScriptAnalyzer.cs | 47 +++++++++++++++++-- .../Scripting/Analysis/ScriptAnalyzerMode.cs | 22 +++++++++ .../Analysis/ScriptAnalyzerSettings.cs | 17 +++++++ src/Cake.Core/Scripting/ScriptRunner.cs | 2 +- .../Unit/Features/BootstrapFeatureTests.cs | 30 ------------ src/Cake.Tests/Unit/ProgramTests.cs | 5 +- src/Cake/Cake.csproj | 2 +- src/Cake/Commands/DefaultCommand.cs | 42 ++++++++++++----- src/Cake/Commands/DefaultCommandSettings.cs | 16 ++++--- .../Bootstrapping/BootstrapFeature.cs | 21 +++++++-- src/Cake/Features/Building/BuildFeature.cs | 1 + .../Features/Building/BuildFeatureSettings.cs | 1 + .../Features/Introspection/VersionResolver.cs | 22 +++++++-- .../Converters/FilePathConverter.cs | 1 + 17 files changed, 171 insertions(+), 70 deletions(-) create mode 100644 src/Cake.Core/Scripting/Analysis/ScriptAnalyzerMode.cs create mode 100644 src/Cake.Core/Scripting/Analysis/ScriptAnalyzerSettings.cs delete mode 100644 src/Cake.Tests/Unit/Features/BootstrapFeatureTests.cs diff --git a/src/Cake.Core.Tests/Fixtures/ScriptAnalyzerFixture.cs b/src/Cake.Core.Tests/Fixtures/ScriptAnalyzerFixture.cs index 95c70ed138..7d7c5f4f3e 100644 --- a/src/Cake.Core.Tests/Fixtures/ScriptAnalyzerFixture.cs +++ b/src/Cake.Core.Tests/Fixtures/ScriptAnalyzerFixture.cs @@ -43,7 +43,7 @@ public ScriptAnalyzer CreateAnalyzer() public ScriptAnalyzerResult Analyze(FilePath script) { - return CreateAnalyzer().Analyze(script); + return CreateAnalyzer().Analyze(script, new ScriptAnalyzerSettings()); } public void GivenScriptExist(FilePath path, string content) diff --git a/src/Cake.Core.Tests/Unit/Scripting/ScriptRunnerTests.cs b/src/Cake.Core.Tests/Unit/Scripting/ScriptRunnerTests.cs index 929ee43a34..3e4bfa700c 100644 --- a/src/Cake.Core.Tests/Unit/Scripting/ScriptRunnerTests.cs +++ b/src/Cake.Core.Tests/Unit/Scripting/ScriptRunnerTests.cs @@ -250,7 +250,7 @@ public void Should_Remove_Directory_From_Script_Path(string path) // Given var fixture = new ScriptRunnerFixture(path); fixture.ScriptAnalyzer = Substitute.For(); - fixture.ScriptAnalyzer.Analyze(Arg.Any()) + fixture.ScriptAnalyzer.Analyze(Arg.Any(), Arg.Any()) .Returns(new ScriptAnalyzerResult(new ScriptInformation(path), new List())); var runner = fixture.CreateScriptRunner(); @@ -259,7 +259,8 @@ public void Should_Remove_Directory_From_Script_Path(string path) // Then fixture.ScriptAnalyzer.Received(1).Analyze( - Arg.Is(f => f.FullPath == "build.cake")); + Arg.Is(f => f.FullPath == "build.cake"), + Arg.Any()); } [Fact] @@ -350,7 +351,7 @@ public void Should_Log_All_Analyzer_Errors_And_Throw() // Given var fixture = new ScriptRunnerFixture(); fixture.ScriptAnalyzer = Substitute.For(); - fixture.ScriptAnalyzer.Analyze(Arg.Any()) + fixture.ScriptAnalyzer.Analyze(Arg.Any(), Arg.Any()) .Returns(new ScriptAnalyzerResult(new ScriptInformation(fixture.Script), new List(), new List { new ScriptAnalyzerError("/Working/script1.cake", 2, "Error in script 1"), diff --git a/src/Cake.Core/Scripting/Analysis/IScriptAnalyzer.cs b/src/Cake.Core/Scripting/Analysis/IScriptAnalyzer.cs index 4b3a41dbf4..3b7212a50b 100644 --- a/src/Cake.Core/Scripting/Analysis/IScriptAnalyzer.cs +++ b/src/Cake.Core/Scripting/Analysis/IScriptAnalyzer.cs @@ -16,7 +16,8 @@ public interface IScriptAnalyzer /// Analyzes the specified script path. /// /// The path to the script to analyze. + /// The script analyzer settings. /// The script analysis result. - ScriptAnalyzerResult Analyze(FilePath path); + ScriptAnalyzerResult Analyze(FilePath path, ScriptAnalyzerSettings settings); } } \ No newline at end of file diff --git a/src/Cake.Core/Scripting/Analysis/ScriptAnalyzer.cs b/src/Cake.Core/Scripting/Analysis/ScriptAnalyzer.cs index 7b094f734c..5646eca059 100644 --- a/src/Cake.Core/Scripting/Analysis/ScriptAnalyzer.cs +++ b/src/Cake.Core/Scripting/Analysis/ScriptAnalyzer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; @@ -23,7 +24,8 @@ public sealed class ScriptAnalyzer : IScriptAnalyzer private readonly IFileSystem _fileSystem; private readonly ICakeEnvironment _environment; private readonly ICakeLog _log; - private readonly LineProcessor[] _lineProcessors; + private readonly LineProcessor[] _defaultProcessors; + private readonly LineProcessor[] _moduleProcessors; /// /// Initializes a new instance of the class. @@ -55,7 +57,7 @@ public ScriptAnalyzer( _environment = environment; _log = log; - _lineProcessors = new LineProcessor[] + _defaultProcessors = new LineProcessor[] { new LoadDirectiveProcessor(providers), new ReferenceDirectiveProcessor(_fileSystem, _environment), @@ -67,14 +69,21 @@ public ScriptAnalyzer( new DefineDirectiveProcessor(), new ModuleDirectiveProcessor() }; + + _moduleProcessors = new LineProcessor[] + { + new LoadDirectiveProcessor(providers), + new ModuleDirectiveProcessor() + }; } /// /// Analyzes the specified script path. /// /// The path to the script to analyze. + /// The script analyzer settings. /// The script analysis result. - public ScriptAnalyzerResult Analyze(FilePath path) + public ScriptAnalyzerResult Analyze(FilePath path, ScriptAnalyzerSettings settings) { if (path == null) { @@ -84,9 +93,15 @@ public ScriptAnalyzerResult Analyze(FilePath path) // Make the script path absolute. path = path.MakeAbsolute(_environment); + // Get the correct callback. + var callback = settings.Mode == ScriptAnalyzerMode.Modules + ? ModuleAnalyzeCallback + : (Action)AnalyzeCallback; + // Create a new context. var context = new ScriptAnalyzerContext( - _fileSystem, _environment, _log, AnalyzeCallback, path); + _fileSystem, _environment, + _log, callback, path); // Analyze the script. context.Analyze(path); @@ -98,6 +113,28 @@ public ScriptAnalyzerResult Analyze(FilePath path) context.Errors); } + [SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")] + private void ModuleAnalyzeCallback(IScriptAnalyzerContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Iterate all lines in the script. + var lines = ReadLines(context.Current.Path); + foreach (var line in lines) + { + foreach (var processor in _defaultProcessors) + { + if (processor.Process(context, _environment.ExpandEnvironmentVariables(line), out var _)) + { + break; + } + } + } + } + [SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")] private void AnalyzeCallback(IScriptAnalyzerContext context) { @@ -117,7 +154,7 @@ private void AnalyzeCallback(IScriptAnalyzerContext context) { string replacement = null; - if (!_lineProcessors.Any(p => p.Process(context, _environment.ExpandEnvironmentVariables(line), out replacement))) + if (!_defaultProcessors.Any(p => p.Process(context, _environment.ExpandEnvironmentVariables(line), out replacement))) { context.AddScriptLine(line); } diff --git a/src/Cake.Core/Scripting/Analysis/ScriptAnalyzerMode.cs b/src/Cake.Core/Scripting/Analysis/ScriptAnalyzerMode.cs new file mode 100644 index 0000000000..7f5494f0a9 --- /dev/null +++ b/src/Cake.Core/Scripting/Analysis/ScriptAnalyzerMode.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Cake.Core.Scripting.Analysis +{ + /// + /// Represents the script analyzer mode. + /// + public enum ScriptAnalyzerMode + { + /// + /// Analyzes everything. + /// + Everything = 0, + + /// + /// Analyzes modules. + /// + Modules = 1, + } +} \ No newline at end of file diff --git a/src/Cake.Core/Scripting/Analysis/ScriptAnalyzerSettings.cs b/src/Cake.Core/Scripting/Analysis/ScriptAnalyzerSettings.cs new file mode 100644 index 0000000000..4294fbb6a7 --- /dev/null +++ b/src/Cake.Core/Scripting/Analysis/ScriptAnalyzerSettings.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Cake.Core.Scripting.Analysis +{ + /// + /// Represents settings for the script analyzer. + /// + public sealed class ScriptAnalyzerSettings + { + /// + /// Gets or sets the analyzer mode. + /// + public ScriptAnalyzerMode Mode { get; set; } = ScriptAnalyzerMode.Everything; + } +} \ No newline at end of file diff --git a/src/Cake.Core/Scripting/ScriptRunner.cs b/src/Cake.Core/Scripting/ScriptRunner.cs index f79adea26a..dfcd294383 100644 --- a/src/Cake.Core/Scripting/ScriptRunner.cs +++ b/src/Cake.Core/Scripting/ScriptRunner.cs @@ -87,7 +87,7 @@ public void Run(IScriptHost host, FilePath scriptPath) // Analyze the script file. _log.Verbose("Analyzing build script..."); - var result = _analyzer.Analyze(scriptPath.GetFilename()); + var result = _analyzer.Analyze(scriptPath.GetFilename(), new ScriptAnalyzerSettings()); // Log all errors and throw if (!result.Succeeded) diff --git a/src/Cake.Tests/Unit/Features/BootstrapFeatureTests.cs b/src/Cake.Tests/Unit/Features/BootstrapFeatureTests.cs deleted file mode 100644 index 180b7cf9d0..0000000000 --- a/src/Cake.Tests/Unit/Features/BootstrapFeatureTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Cake.Core.Composition; -using Cake.Core.IO; -using Cake.Core.Packaging; -using Cake.Core.Scripting; -using Cake.Tests.Fixtures; -using NSubstitute; -using Xunit; - -namespace Cake.Tests.Unit.Features -{ - public sealed class BootstrapFeatureTests - { - [Fact] - public async Task The_Bootstrap_Option_Should_Install_Modules() - { - // Given - var fixture = new ProgramFixture(); - - // When - var result = await fixture.Run("--bootstrap"); - - // Then - fixture.Builder.Processor.Received(1).InstallModules( - Arg.Any>(), - Arg.Is(p => p.FullPath == "/Working/tools/Modules")); - } - } -} diff --git a/src/Cake.Tests/Unit/ProgramTests.cs b/src/Cake.Tests/Unit/ProgramTests.cs index 05588da9c0..06a0889b23 100644 --- a/src/Cake.Tests/Unit/ProgramTests.cs +++ b/src/Cake.Tests/Unit/ProgramTests.cs @@ -30,8 +30,9 @@ public async Task Should_Use_Default_Parameters_By_Default() settings.BuildHostKind == BuildHostKind.Build && settings.Debug == false && settings.Exclusive == false && - settings.Script == null && - settings.Verbosity == Verbosity.Normal)); + settings.Script.FullPath == "build.cake" && + settings.Verbosity == Verbosity.Normal && + settings.NoBootstrapping == false)); } [Theory] diff --git a/src/Cake/Cake.csproj b/src/Cake/Cake.csproj index cee573a5de..220b9734ac 100644 --- a/src/Cake/Cake.csproj +++ b/src/Cake/Cake.csproj @@ -30,6 +30,6 @@ - + \ No newline at end of file diff --git a/src/Cake/Commands/DefaultCommand.cs b/src/Cake/Commands/DefaultCommand.cs index 125440c45d..81b26dfae7 100644 --- a/src/Cake/Commands/DefaultCommand.cs +++ b/src/Cake/Commands/DefaultCommand.cs @@ -41,29 +41,32 @@ public override int Execute(CommandContext context, DefaultCommandSettings setti { return _version.Run(); } - if (settings.ShowInfo) + else if (settings.ShowInfo) { return _info.Run(); } - if (settings.Bootstrap) - { - return _bootstrapper.Run(context.Remaining, new BootstrapFeatureSettings - { - Script = settings.Script, - Verbosity = settings.Verbosity - }); - } // Get the build host type. var host = GetBuildHostKind(settings); + // Run the bootstrapper? + if (!settings.SkipBootstrap || settings.Bootstrap) + { + int bootstrapperResult = PerformBootstrapping(context, settings, host); + if (bootstrapperResult != 0 || settings.Bootstrap) + { + return bootstrapperResult; + } + } + // Run the build feature. return _builder.Run(context.Remaining, new BuildFeatureSettings(host) { Script = settings.Script, Verbosity = settings.Verbosity, Exclusive = settings.Exclusive, - Debug = settings.Debug + Debug = settings.Debug, + NoBootstrapping = settings.SkipBootstrap, }); } @@ -73,15 +76,30 @@ private BuildHostKind GetBuildHostKind(DefaultCommandSettings settings) { return BuildHostKind.DryRun; } - if (settings.ShowDescription) + else if (settings.ShowDescription) { return BuildHostKind.Description; } - if (settings.ShowTree) + else if (settings.ShowTree) { return BuildHostKind.Tree; } + return BuildHostKind.Build; } + + private int PerformBootstrapping(CommandContext context, DefaultCommandSettings settings, BuildHostKind host) + { + if (host != BuildHostKind.Build && host != BuildHostKind.DryRun) + { + return 0; + } + + return _bootstrapper.Run(context.Remaining, new BootstrapFeatureSettings + { + Script = settings.Script, + Verbosity = settings.Verbosity + }); + } } } diff --git a/src/Cake/Commands/DefaultCommandSettings.cs b/src/Cake/Commands/DefaultCommandSettings.cs index ef66bd5608..f0dcb5d492 100644 --- a/src/Cake/Commands/DefaultCommandSettings.cs +++ b/src/Cake/Commands/DefaultCommandSettings.cs @@ -13,7 +13,7 @@ namespace Cake.Commands public sealed class DefaultCommandSettings : CommandSettings { [CommandArgument(0, "[SCRIPT]")] - [Description("The Cake script")] + [Description("The Cake script. Defaults to [grey]build.cake[/]")] [TypeConverter(typeof(FilePathConverter))] [DefaultValue("build.cake")] public FilePath Script { get; set; } @@ -28,18 +28,22 @@ public sealed class DefaultCommandSettings : CommandSettings [Description("Launches script in debug mode.")] public bool Debug { get; set; } + [CommandOption("-e|--exclusive")] + [Description("Execute a single task without any dependencies.")] + public bool Exclusive { get; set; } + [CommandOption("--dryrun|--noop|--whatif")] [Description("Performs a dry run.")] public bool DryRun { get; set; } - [CommandOption("--exclusive")] - [Description("Execute a single task without any dependencies.")] - public bool Exclusive { get; set; } - [CommandOption("--bootstrap")] - [Description("Download/install modules defined by #module directives")] + [Description("Download/install modules defined by [grey]#module[/] directives, but do not run build.")] public bool Bootstrap { get; set; } + [CommandOption("--skip-bootstrap")] + [Description("Skips bootstrapping when running build.")] + public bool SkipBootstrap { get; set; } + [CommandOption("--showdescription|--description")] [Description("Shows description about tasks.")] public bool ShowDescription { get; set; } diff --git a/src/Cake/Features/Bootstrapping/BootstrapFeature.cs b/src/Cake/Features/Bootstrapping/BootstrapFeature.cs index 370f541893..6f9c9ab1b1 100644 --- a/src/Cake/Features/Bootstrapping/BootstrapFeature.cs +++ b/src/Cake/Features/Bootstrapping/BootstrapFeature.cs @@ -55,11 +55,12 @@ public int Run(IRemainingArguments arguments, BootstrapFeatureSettings settings) var root = settings.Script.GetDirectory(); // Analyze the script. - var result = analyzer.Analyze(settings.Script); - if (!result.Succeeded) + log.Debug("Looking for modules..."); + ScriptAnalyzerResult result = PerformAnalysis(analyzer, root, settings); + if (result.Modules.Count == 0) { - var messages = string.Join("\n", result.Errors.Select(s => $"{root.GetRelativePath(s.File).FullPath}, line #{s.Line}: {s.Message}")); - throw new AggregateException($"Bootstrapping failed for '{settings.Script}'.\n{messages}"); + log.Debug("No modules found to install."); + return 0; } // Install modules. @@ -70,5 +71,17 @@ public int Run(IRemainingArguments arguments, BootstrapFeatureSettings settings) return 0; } + + private static ScriptAnalyzerResult PerformAnalysis(IScriptAnalyzer analyzer, DirectoryPath root, BootstrapFeatureSettings settings) + { + var result = analyzer.Analyze(settings.Script, new ScriptAnalyzerSettings() { Mode = ScriptAnalyzerMode.Modules }); + if (!result.Succeeded) + { + var messages = string.Join("\n", result.Errors.Select(s => $"{root.GetRelativePath(s.File).FullPath}, line #{s.Line}: {s.Message}")); + throw new AggregateException($"Bootstrapping failed for '{settings.Script}'.\n{messages}"); + } + + return result; + } } } diff --git a/src/Cake/Features/Building/BuildFeature.cs b/src/Cake/Features/Building/BuildFeature.cs index a8a9c243ae..b8e5c6c7ac 100644 --- a/src/Cake/Features/Building/BuildFeature.cs +++ b/src/Cake/Features/Building/BuildFeature.cs @@ -11,6 +11,7 @@ using Cake.Core.Diagnostics; using Cake.Core.IO; using Cake.Core.Scripting; +using Cake.Features.Bootstrapping; using Cake.Features.Building.Hosts; using Cake.Infrastructure; using Cake.Infrastructure.Composition; diff --git a/src/Cake/Features/Building/BuildFeatureSettings.cs b/src/Cake/Features/Building/BuildFeatureSettings.cs index c851d968ea..47f35a5a86 100644 --- a/src/Cake/Features/Building/BuildFeatureSettings.cs +++ b/src/Cake/Features/Building/BuildFeatureSettings.cs @@ -15,6 +15,7 @@ public sealed class BuildFeatureSettings : IScriptHostSettings public Verbosity Verbosity { get; set; } public bool Debug { get; set; } public bool Exclusive { get; set; } + public bool NoBootstrapping { get; set; } public BuildFeatureSettings(BuildHostKind buildHostKind) { diff --git a/src/Cake/Features/Introspection/VersionResolver.cs b/src/Cake/Features/Introspection/VersionResolver.cs index f2d1050c9c..1a9dfad55a 100644 --- a/src/Cake/Features/Introspection/VersionResolver.cs +++ b/src/Cake/Features/Introspection/VersionResolver.cs @@ -14,16 +14,30 @@ public interface IVersionResolver public sealed class VersionResolver : IVersionResolver { - public string GetProductVersion() + public string GetVersion() { var assembly = typeof(Program).Assembly; - return FileVersionInfo.GetVersionInfo(assembly.Location).ProductVersion; + var version = FileVersionInfo.GetVersionInfo(assembly.Location).Comments; + + if (string.IsNullOrWhiteSpace(version)) + { + version = "Unknown"; + } + + return version; } - public string GetVersion() + public string GetProductVersion() { var assembly = typeof(Program).Assembly; - return FileVersionInfo.GetVersionInfo(assembly.Location).Comments; + var version = FileVersionInfo.GetVersionInfo(assembly.Location).ProductVersion; + + if (string.IsNullOrWhiteSpace(version)) + { + version = "Unknown"; + } + + return version; } } } diff --git a/src/Cake/Infrastructure/Converters/FilePathConverter.cs b/src/Cake/Infrastructure/Converters/FilePathConverter.cs index ac35a52e4c..c5697d1415 100644 --- a/src/Cake/Infrastructure/Converters/FilePathConverter.cs +++ b/src/Cake/Infrastructure/Converters/FilePathConverter.cs @@ -17,6 +17,7 @@ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo c { return new FilePath(stringValue); } + throw new NotSupportedException("Can't convert value to file path."); } }