diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a709d6b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://EditorConfig.org + +root = true + +[*] +end_of_line = CRLF + +[*.ps1] +indent_style = space +indent_size = 2 + +[*.cs] +indent_style = space +indent_size = 4 + +[*.cake] +indent_style = space +indent_size = 4 + +[*.js] +indent_style = tab +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eef7d18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,88 @@ +# Misc folders +[Bb]in/ +[Oo]bj/ +[Tt]emp/ +[Ll]ib/ +[Pp]ackages/ +/[Aa]rtifacts/ +/[Tt]ools/ +*.sln.ide/ + +# .NET CLI +/.dotnet/ +dotnet-install.sh* +/.packages/ + +# Visual Studio +.vs/ +.vscode/ +launchSettings.json +project.lock.json + +# Build related +build-results/ +tools/Cake/ +tools/xunit.runners/ +tools/xunit.runner.console/ +tools/nuget.exe +tools/gitreleasemanager/ +tools/GitVersion.CommandLine/ +tools/Addins/ +tools/packages.config.md5sum + +# mstest test results +TestResults + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates +*.userprefs +*.GhostDoc.xml +*StyleCop.Cache + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.log +*.vspscc +*.vssscc +.builds + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# ReSharper is a .NET coding add-in +_ReSharper* + +# NCrunch +*.ncrunch* +.*crunch*.local.xml +_NCrunch_* + +# NuGet Packages Directory +packages + +# Windows +Thumbs.db diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..5d4bd95 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,13 @@ +branches: + master: + mode: ContinuousDelivery + tag: + increment: Patch + prevent-increment-of-merged-branch-version: true + track-merge-target: false + develop: + mode: ContinuousDeployment + tag: alpha + increment: Minor + prevent-increment-of-merged-branch-version: false + track-merge-target: true \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..403462a --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a377333 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# Frosting + +A .NET Core host for Cake, that allows you to write your build scripts as a +portable console application (`netstandard1.0`). Frosting is currently +in alpha, but more information, documentation and samples will be added soon. + +**Expect things to move around initially. Especially naming of things.** + +## Table of Contents + +1. [Example](https://github.com/cake-build/frosting#example) +2. [Acknowledgement](https://github.com/cake-build/frosting#documentation) +3. [License](https://github.com/cake-build/frosting#license) + +## Example + +Start by adding a `project.json` file. +The `Cake.Frosting` package will decide what version of `Cake.Core` and `Cake.Common` +you will run. + +```json +{ + "version": "0.1.0-*", + "buildOptions": { + "emitEntryPoint": true + }, + "dependencies": { + "Cake.Frosting": "0.1.0-alpha001", + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0" + } + }, + "frameworks": { + "netcoreapp1.0": { + "imports": "dnxcore50" + } + } +} +``` + +For the sake of keeping the example simple, all classes are listed after each other, +but you should of course treat the source code of your build scripts like any other +code and divide them up in individual files. + +```csharp +using Cake.Core; +using Cake.Core.Diagnostics; +using Cake.Frosting; + +public class Program +{ + public static int Main(string[] args) + { + // Create the host. + var host = new CakeHostBuilder() + .WithArguments(args) + .ConfigureServices(services => + { + // Use a custom settings class. + services.UseContext(); + }) + .Build(); + + // Run the host. + return host.Run(); + } +} + +public class MySettings : FrostingContext +{ + public bool Magic { get; set; } + + public MySettings(ICakeContext context) + : base(context) + { + // You could also use a CakeLifeTime + // to provide a Setup method to setup the context. + Magic = context.Arguments.HasArgument("magic"); + } +} + +[TaskName("Provide-Another-Name-Like-This")] +public class Build : FrostingTask +{ + public override bool ShouldRun(Settings context) + { + // Don't run this task on OSX. + return context.Environment.Platform.Family != PlatformFamily.OSX; + } + + public override void Run(FrostingTask context) + { + context.Log.Information("Magic: {0}", context.Magic); + } +} + +[Dependency(typeof(Build))] +public class Default : FrostingTask +{ + // If you don't inherit from the generic task + // the standard ICakeContext will be provided. +} + +``` + +To run the script, simply run it like any .NET Core application. + +```powershell +> dotnet restore +> dotnet run --verbosity=verbose --working="./.." +``` + +## Acknowledgement + +The API for configuring and running the host have been heavily influenced by +the [ASP.NET Core hosting API](https://github.com/aspnet/Hosting). + +## License + +Copyright © Patrik Svensson, Mattias Karlsson, Gary Ewan Park and contributors. +Frosting is provided as-is under the MIT license. For more information see +[LICENSE](https://github.com/cake-build/cake/blob/develop/LICENSE). + +* For Autofac, see https://github.com/autofac/Autofac/blob/master/LICENSE + +## Thanks + +A big thank you has to go to [JetBrains](https://www.jetbrains.com) who provide +each of the Cake developers with an +[Open Source License](https://www.jetbrains.com/support/community/#section=open-source) +for [ReSharper](https://www.jetbrains.com/resharper/) that helps with the development of Cake. + +The Cake Team would also like to say thank you to the guys at +[MyGet](https://www.myget.org/) for their support in providing a Professional +subscription which allows us to continue to push all of our pre-release +editions of Cake NuGet packages for early consumption by the Cake community. + +## Code of Conduct + +This project has adopted the code of conduct defined by the +[Contributor Covenant](http://contributor-covenant.org/) to clarify expected behavior +in our community. For more information see the [.NET Foundation Code of Conduct](http://www.dotnetfoundation.org/code-of-conduct). + +## .NET Foundation + +This project is supported by the [.NET Foundation](http://www.dotnetfoundation.org). \ No newline at end of file diff --git a/build.cake b/build.cake new file mode 100644 index 0000000..3815566 --- /dev/null +++ b/build.cake @@ -0,0 +1,93 @@ +#load "scripts/parameters.cake" +#load "scripts/version.cake" +#load "scripts/publish.cake" + +var parameters = Parameters.Create(Context); + +Setup(context => +{ + parameters.Setup(context); + + Information("Version: {0}", parameters.Version); + Information("Version suffix: {0}", parameters.Suffix); + Information("Configuration: {0}", parameters.Configuration); + Information("Target: {0}", parameters.Target); + Information("AppVeyor: {0}", parameters.IsRunningOnAppVeyor); +}); + +Task("Update-Version-Information") + .Does(context => +{ + BuildVersion.UpdateVersion(parameters); +}); + +Task("Restore") + .IsDependentOn("Update-Version-Information") + .Does(context => +{ + DotNetCoreRestore("./src", new DotNetCoreRestoreSettings + { + Verbose = false, + Verbosity = DotNetCoreRestoreVerbosity.Warning, + Sources = new [] { + "https://www.myget.org/F/xunit/api/v3/index.json", + "https://dotnet.myget.org/F/dotnet-core/api/v3/index.json", + "https://dotnet.myget.org/F/cli-deps/api/v3/index.json", + "https://api.nuget.org/v3/index.json", + "https://www.myget.org/F/cake/api/v3/index.json" + } + }); +}); + +Task("Build") + .IsDependentOn("Restore") + .Does(context => +{ + foreach(var path in parameters.Projects) + { + DotNetCoreBuild(path.FullPath, new DotNetCoreBuildSettings(){ + Configuration = "Release", + VersionSuffix = parameters.Suffix + }); + } +}); + +Task("Unit-Tests") + .IsDependentOn("Build") + .Does(context => +{ + DotNetCoreTest("./src/Cake.Frosting.Tests", new DotNetCoreTestSettings { + Configuration = "Release", + NoBuild = true, + Verbose = false + }); +}); + +Task("Package") + .IsDependentOn("Unit-Tests") + .Does(context => +{ + DotNetCorePack("./src/Cake.Frosting/project.json", new DotNetCorePackSettings(){ + Configuration = "Release", + VersionSuffix = parameters.Suffix, + NoBuild = true, + Verbose = false + }); +}); + +Task("Publish-MyGet") + .IsDependentOn("Package") + .WithCriteria(() => parameters.ShouldPublishToMyGet) + .Does(context => +{ + Publish(context, parameters, parameters.MyGetSource, parameters.MyGetApiKey); +}); + +Task("Default") + .IsDependentOn("Package"); + +Task("AppVeyor") + .WithCriteria(parameters.IsRunningOnAppVeyor) + .IsDependentOn("Publish-MyGet"); + +RunTarget(parameters.Target); \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..6feb105 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,122 @@ +<# +.SYNOPSIS +This is a Powershell script to bootstrap a Cake build. +.DESCRIPTION +This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) +and execute your Cake build script with the parameters you provide. +.PARAMETER Target +The build script target to run. +.PARAMETER Configuration +The build configuration to use. +.PARAMETER Verbosity +Specifies the amount of information to be displayed. +.PARAMETER WhatIf +Performs a dry run of the build script. +No tasks will be executed. +.PARAMETER ScriptArgs +Remaining arguments are added here. +.LINK +http://cakebuild.net +#> + +[CmdletBinding()] +Param( + [string]$Target = "Default", + [ValidateSet("Release", "Debug")] + [string]$Configuration = "Release", + [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] + [string]$Verbosity = "Verbose", + [switch]$WhatIf, + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$ScriptArgs +) + +$CakeVersion = "0.16.0-alpha0025" +$DotNetChannel = "preview"; +$DotNetVersion = "1.0.0-preview2-003121"; +$DotNetInstallerUri = "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview2/scripts/obtain/dotnet-install.ps1"; +$NugetUrl = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" + +# Make sure tools folder exists +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent +$ToolPath = Join-Path $PSScriptRoot "tools" +if (!(Test-Path $ToolPath)) { + Write-Verbose "Creating tools directory..." + New-Item -Path $ToolPath -Type directory | out-null +} + +########################################################################### +# INSTALL .NET CORE CLI +########################################################################### + +Function Remove-PathVariable([string]$VariableToRemove) +{ + $path = [Environment]::GetEnvironmentVariable("PATH", "User") + $newItems = $path.Split(';') | Where-Object { $_.ToString() -inotlike $VariableToRemove } + [Environment]::SetEnvironmentVariable("PATH", [System.String]::Join(';', $newItems), "User") + $path = [Environment]::GetEnvironmentVariable("PATH", "Process") + $newItems = $path.Split(';') | Where-Object { $_.ToString() -inotlike $VariableToRemove } + [Environment]::SetEnvironmentVariable("PATH", [System.String]::Join(';', $newItems), "Process") +} + +# Get .NET Core CLI path if installed. +$FoundDotNetCliVersion = $null; +if (Get-Command dotnet -ErrorAction SilentlyContinue) { + $FoundDotNetCliVersion = dotnet --version; +} + +if($FoundDotNetCliVersion -ne $DotNetVersion) { + $InstallPath = Join-Path $PSScriptRoot ".dotnet" + if (!(Test-Path $InstallPath)) { + mkdir -Force $InstallPath | Out-Null; + } + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallerUri, "$InstallPath\dotnet-install.ps1"); + & $InstallPath\dotnet-install.ps1 -Channel $DotNetChannel -Version $DotNetVersion -InstallDir $InstallPath; + + Remove-PathVariable "$InstallPath" + $env:PATH = "$InstallPath;$env:PATH" + $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 + $env:DOTNET_CLI_TELEMETRY_OPTOUT=1 +} + +########################################################################### +# INSTALL NUGET +########################################################################### + +# Make sure nuget.exe exists. +$NugetPath = Join-Path $ToolPath "nuget.exe" +if (!(Test-Path $NugetPath)) { + Write-Host "Downloading NuGet.exe..." + (New-Object System.Net.WebClient).DownloadFile($NugetUrl, $NugetPath); +} + +########################################################################### +# INSTALL CAKE +########################################################################### + +# Make sure Cake has been installed. +$CakePath = Join-Path $ToolPath "Cake.CoreCLR.$CakeVersion/Cake.dll" +if (!(Test-Path $CakePath)) { + Write-Host "Installing Cake..." + Invoke-Expression "&`"$NugetPath`" install Cake.CoreCLR -Version $CakeVersion -OutputDirectory `"$ToolPath`" -Prerelease" | Out-Null; + if ($LASTEXITCODE -ne 0) { + Throw "An error occured while restoring Cake from NuGet." + } +} + +########################################################################### +# RUN BUILD SCRIPT +########################################################################### + +# Build the argument list. +$Arguments = @{ + target=$Target; + configuration=$Configuration; + verbosity=$Verbosity; + dryrun=$WhatIf; +}.GetEnumerator() | %{"--{0}=`"{1}`"" -f $_.key, $_.value }; + +# Start Cake +Write-Host "Running build script..." +Invoke-Expression "& dotnet `"$CakePath`" `"build.cake`" $Arguments $ScriptArgs" +exit $LASTEXITCODE \ No newline at end of file diff --git a/scripts/parameters.cake b/scripts/parameters.cake new file mode 100644 index 0000000..328e644 --- /dev/null +++ b/scripts/parameters.cake @@ -0,0 +1,71 @@ +#load "version.cake" +#load "utilities.cake" + +public class Parameters +{ + public string Target { get; set; } + public string Configuration { get; set; } + public string Version { get; set; } + public string Suffix { get; set; } + public bool IsRunningOnAppVeyor { get; private set; } + public FilePath[] Projects { get; set; } + + public string MyGetSource { get; set; } + public string MyGetApiKey { get; set; } + + public bool IsLocalBuild { get; set; } + public bool IsPullRequest { get; set; } + public bool IsOriginalRepo { get; set; } + public bool IsTagged { get; set; } + public bool IsMasterBranch { get; set; } + + public bool ShouldPublishToMyGet + { + get + { + return !IsLocalBuild && !IsPullRequest && IsOriginalRepo + && (IsTagged || !IsMasterBranch); + } + } + + public static Parameters Create(ICakeContext context) + { + var parameters = new Parameters(); + + parameters.Target = context.Argument("target", "Default"); + parameters.Configuration = context.Argument("configuration", "Release"); + parameters.Version = context.Argument("version", null); + parameters.Suffix = context.Argument("suffix", null); + parameters.MyGetSource = GetEnvironmentValueOrArgument(context, "FROSTING_MYGET_SOURCE", "mygetsource"); + parameters.MyGetApiKey = GetEnvironmentValueOrArgument(context, "FROSTING_MYGET_API_KEY", "mygetapikey"); + parameters.IsRunningOnAppVeyor = context.BuildSystem().AppVeyor.IsRunningOnAppVeyor; + + var buildSystem = context.BuildSystem(); + parameters.IsLocalBuild = buildSystem.IsLocalBuild; + parameters.IsPullRequest = buildSystem.AppVeyor.Environment.PullRequest.IsPullRequest; + parameters.IsOriginalRepo = StringComparer.OrdinalIgnoreCase.Equals("cake-build/frosting", buildSystem.AppVeyor.Environment.Repository.Name); + parameters.IsTagged = IsBuildTagged(buildSystem); + parameters.IsMasterBranch = StringComparer.OrdinalIgnoreCase.Equals("master", buildSystem.AppVeyor.Environment.Repository.Branch); + + parameters.Projects = new FilePath[] { + "./src/Cake.Frosting/project.json", + "./src/Cake.Frosting.Tests/project.json", + "./src/Sandbox/project.json", + }; + + return parameters; + } + + public void Setup(ICakeContext context) + { + var info = BuildVersion.GetVersion(context, this); + Version = Version ?? info.Version; + Suffix = Suffix ?? info.Suffix; + } + + private static bool IsBuildTagged(BuildSystem buildSystem) + { + return buildSystem.AppVeyor.Environment.Repository.Tag.IsTag + && !string.IsNullOrWhiteSpace(buildSystem.AppVeyor.Environment.Repository.Tag.Name); + } +} \ No newline at end of file diff --git a/scripts/publish.cake b/scripts/publish.cake new file mode 100644 index 0000000..1b5c6c8 --- /dev/null +++ b/scripts/publish.cake @@ -0,0 +1,27 @@ +#load "parameters.cake" + +public static void Publish(ICakeContext context, Parameters parameters, string source, string apiKey) +{ + if(source == null) + { + throw new CakeException("NuGet source was not provided."); + } + if(apiKey == null) + { + throw new CakeException("NuGet API key was not provided."); + } + + var packageVersion = string.Concat(parameters.Version, "-", parameters.Suffix).Trim('-'); + var files = new[] { + "./src/Cake.Frosting/bin/Release/Cake.Frosting." + packageVersion + ".nupkg", + "./src/Cake.Frosting/bin/Release/Cake.Frosting." + packageVersion + ".symbols.nupkg" + }; + + foreach(var file in files) + { + context.NuGetPush(file, new NuGetPushSettings() { + Source = source, + ApiKey = apiKey + }); + } +} \ No newline at end of file diff --git a/scripts/utilities.cake b/scripts/utilities.cake new file mode 100644 index 0000000..9c9d499 --- /dev/null +++ b/scripts/utilities.cake @@ -0,0 +1,9 @@ +private static string GetEnvironmentValueOrArgument(ICakeContext context, string environmentVariable, string argumentName) +{ + var arg = context.EnvironmentVariable(environmentVariable); + if(string.IsNullOrWhiteSpace(arg)) + { + arg = context.Argument(argumentName, null); + } + return arg; +} \ No newline at end of file diff --git a/scripts/version.cake b/scripts/version.cake new file mode 100644 index 0000000..466fab3 --- /dev/null +++ b/scripts/version.cake @@ -0,0 +1,72 @@ +#tool "nuget:https://www.nuget.org/api/v2?package=GitVersion.CommandLine&version=3.6.2" +#addin "nuget:?package=Newtonsoft.Json&version=9.0.1" + +public class BuildVersion +{ + public string Version { get; private set; } + public string Suffix { get; private set; } + + public BuildVersion(string version, string suffix) + { + Version = version; + Suffix = suffix; + + if(string.IsNullOrWhiteSpace(Suffix)) + { + Suffix = null; + } + } + + public static BuildVersion GetVersion(ICakeContext context, Parameters parameters) + { + string version = null; + string semVersion = null; + + if (context.IsRunningOnWindows() && context.BuildSystem().AppVeyor.IsRunningOnAppVeyor) + { + context.Information("Fetching verson via GitVersion..."); + GitVersion assertedVersions = context.GitVersion(new GitVersionSettings + { + OutputType = GitVersionOutput.Json + }); + version = assertedVersions.MajorMinorPatch; + semVersion = assertedVersions.LegacySemVerPadded; + } + + if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(semVersion)) + { + context.Information("Fetching verson from first project.json..."); + foreach(var project in parameters.Projects) + { + var content = System.IO.File.ReadAllText(project.FullPath, Encoding.UTF8); + var node = Newtonsoft.Json.Linq.JObject.Parse(content); + if(node["version"] != null) + { + version = node["version"].ToString().Replace("-*", ""); + } + } + semVersion = version; + } + + if(string.IsNullOrWhiteSpace(version)) + { + throw new CakeException("Could not calculate version of build."); + } + + return new BuildVersion(version, semVersion.Substring(version.Length).TrimStart('-')); + } + + public static void UpdateVersion(Parameters parameters) + { + foreach(var path in parameters.Projects) + { + var content = System.IO.File.ReadAllText(path.FullPath, Encoding.UTF8); + var node = Newtonsoft.Json.Linq.JObject.Parse(content); + if(node["version"] != null && node["version"].ToString() != (parameters.Version + "-*")) + { + node["version"].Replace(string.Concat(parameters.Version, "-*")); + System.IO.File.WriteAllText(path.FullPath, node.ToString(), Encoding.UTF8); + }; + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting.Tests/Asserts/ExecptionAsserts.cs b/src/Cake.Frosting.Tests/Asserts/ExecptionAsserts.cs new file mode 100644 index 0000000..cb0ab0f --- /dev/null +++ b/src/Cake.Frosting.Tests/Asserts/ExecptionAsserts.cs @@ -0,0 +1,14 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace Xunit +{ + public partial class Assert + { + public static void IsArgumentNullException(Exception exception, string parameterName) + { + IsType(exception); + Equal(parameterName, ((ArgumentNullException) exception).ParamName); + } + } +} diff --git a/src/Cake.Frosting.Tests/Cake.Frosting.Tests.xproj b/src/Cake.Frosting.Tests/Cake.Frosting.Tests.xproj new file mode 100644 index 0000000..7b7dc15 --- /dev/null +++ b/src/Cake.Frosting.Tests/Cake.Frosting.Tests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 6948279d-7066-4707-902d-2d105fe61e6c + Cake.Frosting.Tests + .\obj + .\bin\ + v4.6.2 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/src/Cake.Frosting.Tests/Data/DummyLifetime.cs b/src/Cake.Frosting.Tests/Data/DummyLifetime.cs new file mode 100644 index 0000000..34a76b8 --- /dev/null +++ b/src/Cake.Frosting.Tests/Data/DummyLifetime.cs @@ -0,0 +1,6 @@ +namespace Cake.Frosting.Tests.Data +{ + public sealed class DummyLifetime : FrostingLifetime + { + } +} diff --git a/src/Cake.Frosting.Tests/Data/DummyModule.cs b/src/Cake.Frosting.Tests/Data/DummyModule.cs new file mode 100644 index 0000000..df72b5f --- /dev/null +++ b/src/Cake.Frosting.Tests/Data/DummyModule.cs @@ -0,0 +1,16 @@ +using Cake.Core.Composition; + +namespace Cake.Frosting.Tests.Data +{ + public class DummyModule : ICakeModule + { + public sealed class DummyModuleSentinel + { + } + + public void Register(ICakeContainerRegistrar registrar) + { + registrar.RegisterType(); + } + } +} diff --git a/src/Cake.Frosting.Tests/Data/DummyStartup.cs b/src/Cake.Frosting.Tests/Data/DummyStartup.cs new file mode 100644 index 0000000..53dacf5 --- /dev/null +++ b/src/Cake.Frosting.Tests/Data/DummyStartup.cs @@ -0,0 +1,16 @@ +using Cake.Core.Composition; + +namespace Cake.Frosting.Tests.Data +{ + public sealed class DummyStartup : IFrostingStartup + { + public sealed class DummyStartupSentinel + { + } + + public void Configure(ICakeServices services) + { + services.RegisterType(); + } + } +} diff --git a/src/Cake.Frosting.Tests/Data/DummyTask.cs b/src/Cake.Frosting.Tests/Data/DummyTask.cs new file mode 100644 index 0000000..59e68e2 --- /dev/null +++ b/src/Cake.Frosting.Tests/Data/DummyTask.cs @@ -0,0 +1,8 @@ +using Cake.Core; + +namespace Cake.Frosting.Tests.Data +{ + public sealed class DummyTask : FrostingTask + { + } +} diff --git a/src/Cake.Frosting.Tests/Data/DummyTaskLifetime.cs b/src/Cake.Frosting.Tests/Data/DummyTaskLifetime.cs new file mode 100644 index 0000000..83e1cd7 --- /dev/null +++ b/src/Cake.Frosting.Tests/Data/DummyTaskLifetime.cs @@ -0,0 +1,6 @@ +namespace Cake.Frosting.Tests.Data +{ + public sealed class DummyTaskLifetime : FrostingTaskLifetime + { + } +} \ No newline at end of file diff --git a/src/Cake.Frosting.Tests/Data/Tasks/BuildTask.cs b/src/Cake.Frosting.Tests/Data/Tasks/BuildTask.cs new file mode 100644 index 0000000..4f8c970 --- /dev/null +++ b/src/Cake.Frosting.Tests/Data/Tasks/BuildTask.cs @@ -0,0 +1,10 @@ +using Cake.Core; + +namespace Cake.Frosting.Tests.Data.Tasks +{ + [TaskName("Build")] + [Dependency(typeof(CleanTask))] + public sealed class BuildTask : FrostingTask + { + } +} \ No newline at end of file diff --git a/src/Cake.Frosting.Tests/Data/Tasks/CleanTask.cs b/src/Cake.Frosting.Tests/Data/Tasks/CleanTask.cs new file mode 100644 index 0000000..fc7bef4 --- /dev/null +++ b/src/Cake.Frosting.Tests/Data/Tasks/CleanTask.cs @@ -0,0 +1,9 @@ +using Cake.Core; + +namespace Cake.Frosting.Tests.Data.Tasks +{ + [TaskName("Clean")] + public sealed class CleanTask : FrostingTask + { + } +} \ No newline at end of file diff --git a/src/Cake.Frosting.Tests/Data/Tasks/InvalidDependencyTask.cs b/src/Cake.Frosting.Tests/Data/Tasks/InvalidDependencyTask.cs new file mode 100644 index 0000000..9b9de83 --- /dev/null +++ b/src/Cake.Frosting.Tests/Data/Tasks/InvalidDependencyTask.cs @@ -0,0 +1,10 @@ +using System; +using Cake.Core; + +namespace Cake.Frosting.Tests.Data.Tasks +{ + [Dependency(typeof(DateTime))] + public sealed class InvalidDependencyTask : FrostingTask + { + } +} diff --git a/src/Cake.Frosting.Tests/Data/Tasks/UnitTestsTask.cs b/src/Cake.Frosting.Tests/Data/Tasks/UnitTestsTask.cs new file mode 100644 index 0000000..bd385b3 --- /dev/null +++ b/src/Cake.Frosting.Tests/Data/Tasks/UnitTestsTask.cs @@ -0,0 +1,10 @@ +using Cake.Core; + +namespace Cake.Frosting.Tests.Data.Tasks +{ + [TaskName("Run-Unit-Tests")] + [Dependency(typeof(BuildTask))] + public sealed class UnitTestsTask : FrostingTask + { + } +} \ No newline at end of file diff --git a/src/Cake.Frosting.Tests/Extensions/CakeEngineExtensions.cs b/src/Cake.Frosting.Tests/Extensions/CakeEngineExtensions.cs new file mode 100644 index 0000000..db2d77b --- /dev/null +++ b/src/Cake.Frosting.Tests/Extensions/CakeEngineExtensions.cs @@ -0,0 +1,15 @@ +using System; +using System.Linq; +using Cake.Core; + +// ReSharper disable once CheckNamespace +namespace Cake.Frosting.Tests +{ + public static class CakeEngineExtensions + { + public static bool IsTaskRegistered(this ICakeEngine engine, string name) + { + return engine.Tasks.Any(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/src/Cake.Frosting.Tests/Extensions/CakeHostBuilderFixtureExtensions.cs b/src/Cake.Frosting.Tests/Extensions/CakeHostBuilderFixtureExtensions.cs new file mode 100644 index 0000000..48b629a --- /dev/null +++ b/src/Cake.Frosting.Tests/Extensions/CakeHostBuilderFixtureExtensions.cs @@ -0,0 +1,45 @@ +using Cake.Core; +using Cake.Core.Composition; +using Cake.Frosting.Tests.Data; +using Cake.Frosting.Tests.Fixtures; +using NSubstitute; + +// ReSharper disable once CheckNamespace +namespace Cake.Frosting.Tests +{ + internal static class CakeHostBuilderFixtureExtensions + { + public static CakeHostBuilderFixture RegisterDefaultTask(this CakeHostBuilderFixture fixture) + { + fixture.RegisterTask(); + fixture.Options.Target = typeof(DummyTask).Name; + return fixture; + } + + public static CakeHostBuilderFixture RegisterTask(this CakeHostBuilderFixture fixture) + where T : IFrostingTask + { + fixture.Builder.ConfigureServices(services => services.RegisterType().As()); + return fixture; + } + + public static IFrostingLifetime RegisterLifetimeSubstitute(this CakeHostBuilderFixture fixture) + { + var lifetime = Substitute.For(); + fixture.Builder.ConfigureServices(s => s.RegisterInstance(lifetime).As()); + return lifetime; + } + + public static IFrostingTaskLifetime RegisterTaskLifetimeSubstitute(this CakeHostBuilderFixture fixture) + { + var lifetime = Substitute.For(); + fixture.Builder.ConfigureServices(s => s.RegisterInstance(lifetime).As()); + return lifetime; + } + + public static void UseExecutionStrategySubstitute(this CakeHostBuilderFixture fixture) + { + fixture.Strategy = Substitute.For(); + } + } +} diff --git a/src/Cake.Frosting.Tests/Fakes/NullConsole.cs b/src/Cake.Frosting.Tests/Fakes/NullConsole.cs new file mode 100644 index 0000000..7ae4b29 --- /dev/null +++ b/src/Cake.Frosting.Tests/Fakes/NullConsole.cs @@ -0,0 +1,31 @@ +using System; +using Cake.Core; + +namespace Cake.Frosting.Tests.Fakes +{ + internal sealed class NullConsole : IConsole + { + public void Write(string format, params object[] arg) + { + } + + public void WriteLine(string format, params object[] arg) + { + } + + public void WriteError(string format, params object[] arg) + { + } + + public void WriteErrorLine(string format, params object[] arg) + { + } + + public void ResetColor() + { + } + + public ConsoleColor ForegroundColor { get; set; } + public ConsoleColor BackgroundColor { get; set; } + } +} diff --git a/src/Cake.Frosting.Tests/Fixtures/CakeHostBuilderFixture.cs b/src/Cake.Frosting.Tests/Fixtures/CakeHostBuilderFixture.cs new file mode 100644 index 0000000..2ec04bf --- /dev/null +++ b/src/Cake.Frosting.Tests/Fixtures/CakeHostBuilderFixture.cs @@ -0,0 +1,56 @@ +using Cake.Core; +using Cake.Core.Composition; +using Cake.Core.Diagnostics; +using Cake.Core.IO; +using Cake.Frosting.Tests.Fakes; +using Cake.Testing; +using NSubstitute; + +namespace Cake.Frosting.Tests.Fixtures +{ + public class CakeHostBuilderFixture + { + public CakeHostBuilder Builder { get; set; } + + public IFileSystem FileSystem { get; set; } + public ICakeEnvironment Environment { get; set; } + public ICakeEngine Engine { get; set; } + public ICakeLog Log { get; set; } + public IExecutionStrategy Strategy { get; set; } + public CakeHostOptions Options { get; set; } + + public CakeHostBuilderFixture() + { + Builder = new CakeHostBuilder(); + Environment = FakeEnvironment.CreateUnixEnvironment(); + FileSystem = new FakeFileSystem(Environment); + Log = Substitute.For(); + Engine = new CakeEngine(Log); + Options = new CakeHostOptions(); + } + + public ICakeHost Build() + { + Builder.ConfigureServices(s => s.RegisterType().As()); + Builder.ConfigureServices(s => s.RegisterInstance(Environment).As()); + Builder.ConfigureServices(s => s.RegisterInstance(FileSystem).As()); + Builder.ConfigureServices(s => s.RegisterInstance(Engine).As()); + Builder.ConfigureServices(s => s.RegisterInstance(Log).As()); + Builder.ConfigureServices(s => s.RegisterInstance(Options).As()); + + if (Strategy != null) + { + Builder.ConfigureServices(services => services.RegisterInstance(Strategy).As()); + } + + Builder.ConfigureServices(s => s.RegisterInstance(Options).AsSelf().Singleton()); + + return Builder.Build(); + } + + public int Run() + { + return Build().Run(); + } + } +} diff --git a/src/Cake.Frosting.Tests/Properties/AssemblyInfo.cs b/src/Cake.Frosting.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..8692c22 --- /dev/null +++ b/src/Cake.Frosting.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Cake.Frosting.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6948279d-7066-4707-902d-2d105fe61e6c")] diff --git a/src/Cake.Frosting.Tests/Unit/CakeHostBuilderTests.cs b/src/Cake.Frosting.Tests/Unit/CakeHostBuilderTests.cs new file mode 100644 index 0000000..7e6d786 --- /dev/null +++ b/src/Cake.Frosting.Tests/Unit/CakeHostBuilderTests.cs @@ -0,0 +1,29 @@ +using Cake.Core.Composition; +using Cake.Frosting.Tests.Fixtures; +using NSubstitute; +using Xunit; + +namespace Cake.Frosting.Tests.Unit +{ + public sealed class CakeHostBuilderTests + { + public sealed class TheConfigureServicesMethod + { + [Fact] + public void Should_Replace_Default_Registrations() + { + // Given + var fixture = new CakeHostBuilderFixture(); + var host = Substitute.For(); + fixture.Builder.ConfigureServices(services => services.RegisterInstance(host).As()); + + // When + var result = fixture.Build(); + + // Then + Assert.Same(host, result); + } + } + + } +} diff --git a/src/Cake.Frosting.Tests/Unit/CakeHostTests.cs b/src/Cake.Frosting.Tests/Unit/CakeHostTests.cs new file mode 100644 index 0000000..0aa5491 --- /dev/null +++ b/src/Cake.Frosting.Tests/Unit/CakeHostTests.cs @@ -0,0 +1,187 @@ +using Cake.Core; +using Cake.Core.Diagnostics; +using Cake.Frosting.Tests.Data.Tasks; +using Cake.Frosting.Tests.Fixtures; +using NSubstitute; +using Xunit; + +namespace Cake.Frosting.Tests.Unit +{ + public sealed class CakeHostTests + { + [Fact] + public void Should_Set_Log_Verbosity_From_Options() + { + // Given + var fixture = new CakeHostBuilderFixture(); + fixture.RegisterDefaultTask(); + fixture.Options.Verbosity = Verbosity.Diagnostic; + + // When + fixture.Run(); + + // Then + Assert.Equal(Verbosity.Diagnostic, fixture.Log.Verbosity); + } + + [Fact] + public void Should_Set_Working_Directory_From_Options() + { + // Given + var fixture = new CakeHostBuilderFixture(); + fixture.RegisterDefaultTask(); + fixture.Options.WorkingDirectory = "./Temp"; + + // When + fixture.Run(); + + // Then + Assert.Equal("/Working/Temp", fixture.Environment.WorkingDirectory.FullPath); + } + + [Fact] + public void Should_Register_Tasks_With_Engine() + { + // Given + var fixture = new CakeHostBuilderFixture(); + fixture.RegisterDefaultTask(); + + // When + fixture.Run(); + + // Then + Assert.True(fixture.Engine.IsTaskRegistered("DummyTask")); + } + + [Fact] + public void Should_Call_Setup_On_Registered_Lifetime() + { + // Given + var fixture = new CakeHostBuilderFixture(); + var lifetime = fixture.RegisterDefaultTask() + .RegisterLifetimeSubstitute(); + + // When + fixture.Run(); + + // Then + lifetime.Received(1).Setup(Arg.Any()); + } + + [Fact] + public void Should_Call_Teardown_On_Registered_Lifetime() + { + // Given + var fixture = new CakeHostBuilderFixture(); + var lifetime = fixture.RegisterDefaultTask() + .RegisterLifetimeSubstitute(); + + // When + fixture.Run(); + + // Then + lifetime.Received(1).Teardown(Arg.Any(), Arg.Any()); + } + + [Fact] + public void Should_Call_Setup_On_Registered_Task_Lifetime() + { + // Given + var fixture = new CakeHostBuilderFixture(); + var lifetime = fixture.RegisterDefaultTask() + .RegisterTaskLifetimeSubstitute(); + + // When + fixture.Run(); + + // Then + lifetime.Received(1).Setup(Arg.Any(), Arg.Any()); + } + + [Fact] + public void Should_Call_Teardown_On_Registered_Task_Lifetime() + { + // Given + var fixture = new CakeHostBuilderFixture(); + var lifetime = fixture.RegisterDefaultTask() + .RegisterTaskLifetimeSubstitute(); + + // When + fixture.Run(); + + // Then + lifetime.Received(1).Setup(Arg.Any(), Arg.Any()); + } + + [Fact] + public void Should_Execute_Tasks() + { + // Given + var fixture = new CakeHostBuilderFixture(); + fixture.RegisterDefaultTask(); + fixture.UseExecutionStrategySubstitute(); + + // When + fixture.Run(); + + // Then + fixture.Strategy.Received(1).Execute(Arg.Is(t => t.Name == "DummyTask"), Arg.Any()); + } + + [Fact] + public void Should_Execute_Tasks_In_Correct_Order() + { + // Given + var fixture = new CakeHostBuilderFixture(); + fixture.RegisterTask(); + fixture.RegisterTask(); + fixture.RegisterTask(); + fixture.UseExecutionStrategySubstitute(); + fixture.Options.Target = "Run-Unit-Tests"; + + // When + fixture.Run(); + + // Then + Received.InOrder(() => + { + fixture.Strategy.Execute(Arg.Is(t => t.Name == "Clean"), Arg.Any()); + fixture.Strategy.Execute(Arg.Is(t => t.Name == "Build"), Arg.Any()); + fixture.Strategy.Execute(Arg.Is(t => t.Name == "Run-Unit-Tests"), Arg.Any()); + }); + } + + [Fact] + public void Should_Throw_If_Dependency_Is_Not_A_Valid_Task() + { + // Given + var fixture = new CakeHostBuilderFixture(); + fixture.RegisterTask(); + fixture.UseExecutionStrategySubstitute(); + fixture.Options.Target = "InvalidDependencyTask"; + + // When + var result = fixture.Run(); + + // Then + Assert.Equal(1, result); + fixture.Log.Received(1).Write( + Verbosity.Quiet, LogLevel.Error, + "Error: {0}", "The dependency DateTime does not implement IFrostingTask."); + } + + [Fact] + public void Should_Return_Zero_On_Success() + { + // Given + var fixture = new CakeHostBuilderFixture(); + fixture.RegisterDefaultTask(); + + // When + var result = fixture.Run(); + + // Then + Assert.Equal(0, result); + } + } +} diff --git a/src/Cake.Frosting.Tests/Unit/CakeOptionsTests.cs b/src/Cake.Frosting.Tests/Unit/CakeOptionsTests.cs new file mode 100644 index 0000000..cf9cc40 --- /dev/null +++ b/src/Cake.Frosting.Tests/Unit/CakeOptionsTests.cs @@ -0,0 +1,76 @@ +using Cake.Core.Diagnostics; +using Xunit; + +namespace Cake.Frosting.Tests.Unit +{ + public sealed class CakeOptionsTests + { + public sealed class TheConstructor + { + [Fact] + public void Should_Set_Arguments_To_Empty_Dictionary() + { + // Given + var options = new CakeHostOptions(); + + // When + var result = options.Arguments; + + // Then + Assert.NotNull(result); + } + + [Fact] + public void Should_Set_Working_Directory_To_Default_Value() + { + // Given + var options = new CakeHostOptions(); + + // When + var result = options.WorkingDirectory; + + // Then + Assert.Equal(".", result.FullPath); + } + + [Fact] + public void Should_Set_Target_To_Default_Value() + { + // Given + var options = new CakeHostOptions(); + + // When + var result = options.Target; + + // Then + Assert.Equal("Default", result); + } + + [Fact] + public void Should_Set_Verbosity_To_Default_Value() + { + // Given + var options = new CakeHostOptions(); + + // When + var result = options.Verbosity; + + // Then + Assert.Equal(Verbosity.Normal, result); + } + + [Fact] + public void Should_Set_Command_To_Default_Value() + { + // Given + var options = new CakeHostOptions(); + + // When + var result = options.Command; + + // Then + Assert.Equal(CakeHostCommand.Run, result); + } + } + } +} diff --git a/src/Cake.Frosting.Tests/Unit/Extensions/CakeHostBuilderExtensionsTests.cs b/src/Cake.Frosting.Tests/Unit/Extensions/CakeHostBuilderExtensionsTests.cs new file mode 100644 index 0000000..112ba66 --- /dev/null +++ b/src/Cake.Frosting.Tests/Unit/Extensions/CakeHostBuilderExtensionsTests.cs @@ -0,0 +1,192 @@ +using Cake.Core.Diagnostics; +using Cake.Frosting.Tests.Data; +using NSubstitute; +using Xunit; + +namespace Cake.Frosting.Tests.Unit.Extensions +{ + public sealed class CakeHostBuilderExtensionsTests + { + public sealed class TheUseStartupExtensionMethod + { + [Fact] + public void Should_Throw_If_Builder_Reference_Is_Null() + { + // Given + ICakeHostBuilder builder = null; + + // When + var result = Record.Exception(() => builder.UseStartup()); + + // Then + Assert.IsArgumentNullException(result, "builder"); + } + + [Fact] + public void Should_Run_Startup() + { + // Given + var builder = Substitute.For(); + var services = Substitute.For(); + builder.ConfigureServices(Arg.Invoke(services)); + + // When + builder.UseStartup(); + + // Then + services.RegisterType(typeof(DummyStartup.DummyStartupSentinel)); + } + } + + public sealed class TheBuildExtensionMethod + { + [Fact] + public void Should_Throw_If_Builder_Reference_Is_Null() + { + // Given + ICakeHostBuilder builder = null; + + // When + var result = Record.Exception(() => builder.WithArguments(new string[] { })); + + // Then + Assert.IsArgumentNullException(result, "builder"); + } + + [Theory] + [InlineData("--v=quiet", Verbosity.Quiet)] + [InlineData("--v=minimal", Verbosity.Minimal)] + [InlineData("--v=normal", Verbosity.Normal)] + [InlineData("--v=verbose", Verbosity.Verbose)] + [InlineData("--v=diagnostic", Verbosity.Diagnostic)] + [InlineData("--v=q", Verbosity.Quiet)] + [InlineData("--v=m", Verbosity.Minimal)] + [InlineData("--v=n", Verbosity.Normal)] + [InlineData("--v=v", Verbosity.Verbose)] + [InlineData("--v=d", Verbosity.Diagnostic)] + [InlineData("--verbosity=quiet", Verbosity.Quiet)] + [InlineData("--verbosity=minimal", Verbosity.Minimal)] + [InlineData("--verbosity=normal", Verbosity.Normal)] + [InlineData("--verbosity=verbose", Verbosity.Verbose)] + [InlineData("--verbosity=diagnostic", Verbosity.Diagnostic)] + [InlineData("--verbosity=q", Verbosity.Quiet)] + [InlineData("--verbosity=m", Verbosity.Minimal)] + [InlineData("--verbosity=n", Verbosity.Normal)] + [InlineData("--verbosity=v", Verbosity.Verbose)] + [InlineData("--verbosity=d", Verbosity.Diagnostic)] + [InlineData("-v=quiet", Verbosity.Quiet)] + [InlineData("-v=minimal", Verbosity.Minimal)] + [InlineData("-v=normal", Verbosity.Normal)] + [InlineData("-v=verbose", Verbosity.Verbose)] + [InlineData("-v=diagnostic", Verbosity.Diagnostic)] + [InlineData("-v=q", Verbosity.Quiet)] + [InlineData("-v=m", Verbosity.Minimal)] + [InlineData("-v=n", Verbosity.Normal)] + [InlineData("-v=v", Verbosity.Verbose)] + [InlineData("-v=d", Verbosity.Diagnostic)] + [InlineData("-verbosity=quiet", Verbosity.Quiet)] + [InlineData("-verbosity=minimal", Verbosity.Minimal)] + [InlineData("-verbosity=normal", Verbosity.Normal)] + [InlineData("-verbosity=verbose", Verbosity.Verbose)] + [InlineData("-verbosity=diagnostic", Verbosity.Diagnostic)] + [InlineData("-verbosity=q", Verbosity.Quiet)] + [InlineData("-verbosity=m", Verbosity.Minimal)] + [InlineData("-verbosity=n", Verbosity.Normal)] + [InlineData("-verbosity=v", Verbosity.Verbose)] + [InlineData("-verbosity=d", Verbosity.Diagnostic)] + public void Should_Parse_Verbosity(string args, Verbosity expected) + { + // Given + var builder = Substitute.For(); + var services = Substitute.For(); + builder.ConfigureServices(Arg.Invoke(services)); + + // When + builder.WithArguments(new[] { args }); + + // Then + services.Received(1).RegisterInstance( + Arg.Is(o => o.Verbosity == expected)); + } + + [Theory] + [InlineData("--help")] + [InlineData("--h")] + [InlineData("-help")] + [InlineData("-h")] + public void Should_Parse_Show_Help(string args) + { + // Given + var builder = Substitute.For(); + var services = Substitute.For(); + builder.ConfigureServices(Arg.Invoke(services)); + + // When + builder.WithArguments(new[] { args }); + + // Then + services.Received(1).RegisterInstance( + Arg.Is(o => o.Command == CakeHostCommand.Help)); + } + + [Theory] + [InlineData("--version")] + [InlineData("-version")] + public void Should_Parse_Show_Version(string args) + { + // Given + var builder = Substitute.For(); + var services = Substitute.For(); + builder.ConfigureServices(Arg.Invoke(services)); + + // When + builder.WithArguments(new[] { args }); + + // Then + services.Received(1).RegisterInstance( + Arg.Is(o => o.Command == CakeHostCommand.Version)); + } + + [Theory] + [InlineData("--target=Test1", "Test1")] + [InlineData("--t=Test2", "Test2")] + [InlineData("-target=Test1", "Test1")] + [InlineData("-t=Test2", "Test2")] + public void Should_Parse_Target(string args, string expected) + { + // Given + var builder = Substitute.For(); + var services = Substitute.For(); + builder.ConfigureServices(Arg.Invoke(services)); + + // When + builder.WithArguments(new[] { args }); + + // Then + services.Received(1).RegisterInstance( + Arg.Is(o => o.Target == expected)); + } + + [Theory] + [InlineData("--w=Test1", "Test1")] + [InlineData("--working=Test2", "Test2")] + [InlineData("-w=Test1", "Test1")] + [InlineData("-working=Test2", "Test2")] + public void Should_Parse_Working_Directory(string args, string expected) + { + // Given + var builder = Substitute.For(); + var services = Substitute.For(); + builder.ConfigureServices(Arg.Invoke(services)); + + // When + builder.WithArguments(new[] { args }); + + // Then + services.Received(1).RegisterInstance( + Arg.Is(o => o.WorkingDirectory.FullPath == expected + )); + } + } + } +} diff --git a/src/Cake.Frosting.Tests/Unit/Extensions/CakeServicesExtensionsTests.cs b/src/Cake.Frosting.Tests/Unit/Extensions/CakeServicesExtensionsTests.cs new file mode 100644 index 0000000..1328237 --- /dev/null +++ b/src/Cake.Frosting.Tests/Unit/Extensions/CakeServicesExtensionsTests.cs @@ -0,0 +1,191 @@ +using System; +using System.Reflection; +using Cake.Core.Composition; +using Cake.Frosting.Tests.Data; +using NSubstitute; +using Xunit; + +namespace Cake.Frosting.Tests.Unit.Extensions +{ + public sealed class CakeServicesExtensionsTests + { + public sealed class TheUseContextExtensionMethod + { + [Fact] + public void Should_Throw_If_Services_Reference_Is_Null() + { + // Given + ICakeServices services = null; + + // When + var result = Record.Exception(() => services.UseContext()); + + // Then + Assert.IsArgumentNullException(result, "services"); + } + + [Fact] + public void Should_Register_The_Context() + { + // Given + var services = Substitute.For(); + var builder = Substitute.For(); + services.RegisterType(Arg.Any()).Returns(builder); // Return a builder object when registering + builder.AsSelf().Returns(builder); // Return same builder object when chaining + builder.As(Arg.Any()).Returns(builder); // Return same builder object when chaining + + // When + services.UseContext(); + + // Then + Received.InOrder(() => + { + services.RegisterType(); + builder.AsSelf(); + builder.As(); + builder.Singleton(); + }); + } + } + + public sealed class TheUseLifetimeExtensionMethod + { + [Fact] + public void Should_Throw_If_Services_Reference_Is_Null() + { + // Given + ICakeServices services = null; + + // When + var result = Record.Exception(() => services.UseLifetime()); + + // Then + Assert.IsArgumentNullException(result, "services"); + } + + [Fact] + public void Should_Register_The_Lifetime() + { + // Given + var services = Substitute.For(); + var builder = Substitute.For(); + services.RegisterType(Arg.Any()).Returns(builder); // Return a builder object when registering + builder.As(Arg.Any()).Returns(builder); // Return same builder object when chaining + + // When + services.UseLifetime(); + + // Then + Received.InOrder(() => + { + services.RegisterType(); + builder.As(); + builder.Singleton(); + }); + } + } + + public sealed class TheUseTaskLifetimeExtensionMethod + { + [Fact] + public void Should_Throw_If_Services_Reference_Is_Null() + { + // Given + ICakeServices services = null; + + // When + var result = Record.Exception(() => services.UseTaskLifetime()); + + // Then + Assert.IsArgumentNullException(result, "services"); + } + + [Fact] + public void Should_Register_The_Lifetime() + { + // Given + var services = Substitute.For(); + var builder = Substitute.For(); + services.RegisterType(Arg.Any()).Returns(builder); // Return a builder object when registering + builder.As(Arg.Any()).Returns(builder); // Return same builder object when chaining + + // When + services.UseTaskLifetime(); + + // Then + Received.InOrder(() => + { + services.RegisterType(); + builder.As(); + builder.Singleton(); + }); + } + } + + public sealed class TheUseAssemblyExtensionMethod + { + [Fact] + public void Should_Throw_If_Services_Reference_Is_Null() + { + // Given + ICakeServices services = null; + var assembly = typeof(DateTime).GetTypeInfo().Assembly; + + // When + var result = Record.Exception(() => services.UseAssembly(assembly)); + + // Then + Assert.IsArgumentNullException(result, "services"); + } + + [Fact] + public void Should_Register_The_Assembly() + { + // Given + var services = Substitute.For(); + var builder = Substitute.For(); + services.RegisterInstance(Arg.Any()).Returns(builder); // Return a builder object when registering + builder.As(Arg.Any()).Returns(builder); // Return same builder object when chaining + + // When + services.UseAssembly(typeof(DateTime).GetTypeInfo().Assembly); + + // Then + Received.InOrder(() => + { + services.RegisterInstance(Arg.Any()); + builder.Singleton(); + }); + } + } + + public sealed class TheUseModuleExtensionMethod + { + [Fact] + public void Should_Throw_If_Services_Reference_Is_Null() + { + // Given + ICakeServices services = null; + + // When + var result = Record.Exception(() => services.UseModule()); + + // Then + Assert.IsArgumentNullException(result, "services"); + } + + [Fact] + public void Should_Create_Module_And_Call_Registration() + { + // Given + var services = Substitute.For(); + + // When + services.UseModule(); + + // Then + services.Received(1).RegisterType(typeof(DummyModule.DummyModuleSentinel)); + } + } + } +} diff --git a/src/Cake.Frosting.Tests/project.json b/src/Cake.Frosting.Tests/project.json new file mode 100644 index 0000000..0f4021e --- /dev/null +++ b/src/Cake.Frosting.Tests/project.json @@ -0,0 +1,23 @@ +{ + "version": "0.1.0-*", + "testRunner": "xunit", + "dependencies": { + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "xunit.core": "2.2.0-beta2-build3300", + "xunit.assert.source": "2.2.0-beta2-build3300", + "NSubstitute": "2.0.0-rc", + "Cake.Testing": "0.16.0-alpha0046", + "Cake.Frosting": { + "target": "project" + }, + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0" + } + }, + "frameworks": { + "netcoreapp1.0": { + "imports": "dnxcore50" + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting.sln b/src/Cake.Frosting.sln new file mode 100644 index 0000000..948c6b7 --- /dev/null +++ b/src/Cake.Frosting.sln @@ -0,0 +1,46 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Cake.Frosting", "Cake.Frosting\Cake.Frosting.xproj", "{A608FD39-2B71-4B6E-B238-AE80BB2794F8}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Sandbox", "Sandbox\Sandbox.xproj", "{ED379398-DA69-4D1E-B712-27DC0D67ABC3}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Cake.Frosting.Tests", "Cake.Frosting.Tests\Cake.Frosting.Tests.xproj", "{6948279D-7066-4707-902D-2D105FE61E6C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{E9CC2BB3-4D65-4F23-89EF-0BD3F593A086}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{E00C4E89-C607-4B5E-9C48-A8BFC8903FB7}" + ProjectSection(SolutionItems) = preProject + Frosting.ruleset = Frosting.ruleset + stylecop.json = stylecop.json + Test.ruleset = Test.ruleset + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A608FD39-2B71-4B6E-B238-AE80BB2794F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A608FD39-2B71-4B6E-B238-AE80BB2794F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A608FD39-2B71-4B6E-B238-AE80BB2794F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A608FD39-2B71-4B6E-B238-AE80BB2794F8}.Release|Any CPU.Build.0 = Release|Any CPU + {ED379398-DA69-4D1E-B712-27DC0D67ABC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED379398-DA69-4D1E-B712-27DC0D67ABC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED379398-DA69-4D1E-B712-27DC0D67ABC3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED379398-DA69-4D1E-B712-27DC0D67ABC3}.Release|Any CPU.Build.0 = Release|Any CPU + {6948279D-7066-4707-902D-2D105FE61E6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6948279D-7066-4707-902D-2D105FE61E6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6948279D-7066-4707-902D-2D105FE61E6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6948279D-7066-4707-902D-2D105FE61E6C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {ED379398-DA69-4D1E-B712-27DC0D67ABC3} = {E9CC2BB3-4D65-4F23-89EF-0BD3F593A086} + EndGlobalSection +EndGlobal diff --git a/src/Cake.Frosting/Annotations/DependencyAttribute.cs b/src/Cake.Frosting/Annotations/DependencyAttribute.cs new file mode 100644 index 0000000..850cc42 --- /dev/null +++ b/src/Cake.Frosting/Annotations/DependencyAttribute.cs @@ -0,0 +1,32 @@ +// 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. + +using System; + +// ReSharper disable once CheckNamespace +namespace Cake.Frosting +{ + /// + /// Represents a dependency. + /// + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class DependencyAttribute : Attribute + { + /// + /// Gets the depdendency task type. + /// + /// The dependency task type. + public Type Task { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The type. + public DependencyAttribute(Type type) + { + Task = type; + } + } +} diff --git a/src/Cake.Frosting/Annotations/TaskNameAttribute.cs b/src/Cake.Frosting/Annotations/TaskNameAttribute.cs new file mode 100644 index 0000000..9694d68 --- /dev/null +++ b/src/Cake.Frosting/Annotations/TaskNameAttribute.cs @@ -0,0 +1,32 @@ +// 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. + +using System; + +// ReSharper disable once CheckNamespace +namespace Cake.Frosting +{ + /// + /// Represents a custom task name. + /// + /// + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class TaskNameAttribute : Attribute + { + /// + /// Gets the name of the task. + /// + /// The name. + public string Name { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the task. + public TaskNameAttribute(string name) + { + Name = name; + } + } +} diff --git a/src/Cake.Frosting/Cake.Frosting.xproj b/src/Cake.Frosting/Cake.Frosting.xproj new file mode 100644 index 0000000..96ac9cb --- /dev/null +++ b/src/Cake.Frosting/Cake.Frosting.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + a608fd39-2b71-4b6e-b238-ae80bb2794f8 + Cake.Frosting + .\obj + .\bin\ + v4.6.2 + + + + 2.0 + + + diff --git a/src/Cake.Frosting/CakeHost.cs b/src/Cake.Frosting/CakeHost.cs new file mode 100644 index 0000000..5d321a4 --- /dev/null +++ b/src/Cake.Frosting/CakeHost.cs @@ -0,0 +1,79 @@ +// 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. + +using System; +using System.Collections.Generic; +using Autofac; +using Cake.Core; +using Cake.Core.Diagnostics; +using Cake.Frosting.Internal; +using Cake.Frosting.Internal.Commands; + +namespace Cake.Frosting +{ + internal sealed class CakeHost : ICakeHost + { + // ReSharper disable once NotAccessedField.Local + private readonly ILifetimeScope _scope; + + private readonly CakeHostOptions _options; + private readonly IFrostingContext _context; + private readonly IEnumerable _tasks; + private readonly IFrostingLifetime _lifetime; + private readonly IFrostingTaskLifetime _taskLifetime; + private readonly ICakeEnvironment _environment; + private readonly ICakeEngine _engine; + private readonly ICakeLog _log; + private readonly CommandFactory _commandFactory; + private readonly EngineInitializer _engineInitializer; + + public CakeHost(CakeHostOptions options, CakeHostServices services, ILifetimeScope scope, + IFrostingContext context, + IEnumerable tasks = null, IFrostingLifetime lifetime = null, IFrostingTaskLifetime taskLifetime = null) + { + Guard.ArgumentNotNull(scope, nameof(scope)); + Guard.ArgumentNotNull(services, nameof(services)); + Guard.ArgumentNotNull(context, nameof(context)); + Guard.ArgumentNotNull(options, nameof(options)); + + _options = options; + _scope = scope; // Keep the scope alive. + _context = context; + _tasks = tasks; + _lifetime = lifetime; + _taskLifetime = taskLifetime; + + _environment = services.Environment; + _engine = services.Engine; + _log = services.Log; + _commandFactory = services.CommandFactory; + _engineInitializer = services.EngineInitializer; + } + + public int Run() + { + try + { + // Update the log verbosity and working directory. + _log.Verbosity = _options.Verbosity; + _environment.WorkingDirectory = _options.WorkingDirectory.MakeAbsolute(_environment); + + // Initialize the engine and register everything. + _engineInitializer.Initialize(_engine, _context, _tasks, _lifetime, _taskLifetime); + + // Get the command and execute. + var command = _commandFactory.GetCommand(_options); + var result = command.Execute(_engine, _options); + + // Return success. + return result ? 0 : 1; + } + catch (Exception exception) + { + ErrorHandler.OutputError(_log, exception); + return ErrorHandler.GetExitCode(exception); + } + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/CakeHostBuilder.cs b/src/Cake.Frosting/CakeHostBuilder.cs new file mode 100644 index 0000000..09e7180 --- /dev/null +++ b/src/Cake.Frosting/CakeHostBuilder.cs @@ -0,0 +1,109 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Autofac; +using Cake.Core.Composition; +using Cake.Core.Modules; +using Cake.Frosting.Internal; +using Cake.Frosting.Internal.Composition; + +namespace Cake.Frosting +{ + /// + /// A builder for . + /// + /// + public sealed class CakeHostBuilder : ICakeHostBuilder + { + private readonly ContainerRegistrar _registrations; + + /// + /// Initializes a new instance of the class. + /// + public CakeHostBuilder() + { + _registrations = new ContainerRegistrar(); + } + + /// + /// Adds a delegate for configuring additional services for the host. + /// + /// A delegate for configuring the . + /// The same instance so that multiple calls can be chained. + public ICakeHostBuilder ConfigureServices(Action configureServices) + { + try + { + configureServices(_registrations); + return this; + } + catch (Exception exception) + { + return new ErrorCakeHostBuilder(exception); + } + } + + /// + /// Builds the required services and an using the specified options. + /// + /// The built . + public ICakeHost Build() + { + try + { + // Create the "base" container with the minimum + // stuff registered to run Cake at all. + var registrar = new ContainerRegistrar(); + registrar.RegisterModule(new CoreModule()); + registrar.RegisterModule(new FrostingModule()); + var container = registrar.Build(); + + // Add custom registrations to the container. + AddCustomRegistrations(container); + + // Find and register tasks with the container. + RegisterTasks(container); + + // Resolve the application and run it. + return container.Resolve(); + } + catch (Exception exception) + { + return new ErrorCakeHost(exception); + } + } + + private static void RegisterTasks(IContainer container) + { + // Create a child scope to not affect the underlying + // container in case the ICakeTaskFinder references + // something that is later replaced. + using (var scope = container.BeginLifetimeScope()) + { + // Find tasks in registered assemblies. + var assemblies = scope.Resolve>(); + var finder = scope.Resolve(); + var tasks = finder.GetTasks(assemblies); + + if (tasks.Length > 0) + { + var registrar = new ContainerRegistrar(); + foreach (var task in tasks) + { + registrar.RegisterType(task).As().Singleton(); + } + container.Update(registrar); + } + } + } + + private void AddCustomRegistrations(IContainer container) + { + container.Update(_registrations); + } + } +} diff --git a/src/Cake.Frosting/CakeHostBuilderExtensions.cs b/src/Cake.Frosting/CakeHostBuilderExtensions.cs new file mode 100644 index 0000000..13c2ae8 --- /dev/null +++ b/src/Cake.Frosting/CakeHostBuilderExtensions.cs @@ -0,0 +1,45 @@ +// 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. + +using Cake.Frosting.Internal; +using Cake.Frosting.Internal.Arguments; + +namespace Cake.Frosting +{ + /// + /// Contains extension methods for . + /// + public static class CakeHostBuilderExtensions + { + /// + /// Specify the startup type to be used by the Cake host. + /// + /// The type of the startup. + /// The to configure. + /// The same instance so that multiple calls can be chained. + public static ICakeHostBuilder UseStartup(this ICakeHostBuilder builder) + where TStartup : IFrostingStartup, new() + { + Guard.ArgumentNotNull(builder, nameof(builder)); + + var startup = new TStartup(); + return builder.ConfigureServices(services => startup.Configure(services)); + } + + /// + /// Specify the arguments to be used when building the host. + /// The arguments will be translated into a instance. + /// + /// The builder. + /// The arguments to parse. + /// The same instance so that multiple calls can be chained. + public static ICakeHostBuilder WithArguments(this ICakeHostBuilder builder, string[] args) + { + Guard.ArgumentNotNull(builder, nameof(builder)); + + return builder.ConfigureServices(services => + services.RegisterInstance(ArgumentParser.Parse(args)).AsSelf().Singleton()); + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/CakeHostCommand.cs b/src/Cake.Frosting/CakeHostCommand.cs new file mode 100644 index 0000000..c5b0d93 --- /dev/null +++ b/src/Cake.Frosting/CakeHostCommand.cs @@ -0,0 +1,32 @@ +// 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.Frosting +{ + /// + /// Represents a command for the . + /// + public enum CakeHostCommand + { + /// + /// Runs the build script. + /// + Run = 0, + + /// + /// Performs a dry run of the build script. + /// + DryRun = 1, + + /// + /// Shows version information. + /// + Version = 2, + + /// + /// Shows help. + /// + Help = 3, + } +} diff --git a/src/Cake.Frosting/CakeHostOptions.cs b/src/Cake.Frosting/CakeHostOptions.cs new file mode 100644 index 0000000..a58cf7a --- /dev/null +++ b/src/Cake.Frosting/CakeHostOptions.cs @@ -0,0 +1,59 @@ +// 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. + +using System; +using System.Collections.Generic; +using Cake.Core.Diagnostics; +using Cake.Core.IO; + +namespace Cake.Frosting +{ + /// + /// The options that tells how Cake should be executed. + /// + public sealed class CakeHostOptions + { + /// + /// Gets the arguments. + /// + /// The arguments. + public IDictionary Arguments { get; } + + /// + /// Gets or sets the working directory. + /// + /// The working directory. + public DirectoryPath WorkingDirectory { get; set; } + + /// + /// Gets or sets the target to run. + /// + /// The target to run. + public string Target { get; set; } + + /// + /// Gets or sets the output verbosity. + /// + /// The output verbosity. + public Verbosity Verbosity { get; set; } + + /// + /// Gets or sets the command to execute. + /// + /// The command to execute. + public CakeHostCommand Command { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public CakeHostOptions() + { + Arguments = new Dictionary(StringComparer.OrdinalIgnoreCase); + WorkingDirectory = "."; + Target = "Default"; + Verbosity = Verbosity.Normal; + Command = CakeHostCommand.Run; + } + } +} diff --git a/src/Cake.Frosting/CakeHostServices.cs b/src/Cake.Frosting/CakeHostServices.cs new file mode 100644 index 0000000..6ca1c56 --- /dev/null +++ b/src/Cake.Frosting/CakeHostServices.cs @@ -0,0 +1,62 @@ +// 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. + +using System; +using Cake.Core; +using Cake.Core.Diagnostics; +using Cake.Frosting.Internal; +using Cake.Frosting.Internal.Commands; + +namespace Cake.Frosting +{ + internal sealed class CakeHostServices + { + public ICakeEnvironment Environment { get; } + + public ICakeEngine Engine { get; } + + public ICakeLog Log { get; } + + public CommandFactory CommandFactory { get; } + + public EngineInitializer EngineInitializer { get; } + + public CakeHostServices(ICakeEnvironment environment, ICakeEngine engine, ICakeLog log, + EngineInitializer engineInitializer, CommandFactory commandFactory) + { + Guard.ArgumentNotNull(environment, nameof(environment)); + Guard.ArgumentNotNull(engine, nameof(engine)); + Guard.ArgumentNotNull(log, nameof(log)); + Guard.ArgumentNotNull(engineInitializer, nameof(engineInitializer)); + Guard.ArgumentNotNull(commandFactory, nameof(commandFactory)); + + if (environment == null) + { + throw new ArgumentNullException(nameof(environment)); + } + if (engine == null) + { + throw new ArgumentNullException(nameof(engine)); + } + if (log == null) + { + throw new ArgumentNullException(nameof(log)); + } + if (engineInitializer == null) + { + throw new ArgumentNullException(nameof(engineInitializer)); + } + if (commandFactory == null) + { + throw new ArgumentNullException(nameof(commandFactory)); + } + + Environment = environment; + Engine = engine; + Log = log; + CommandFactory = commandFactory; + EngineInitializer = engineInitializer; + } + } +} diff --git a/src/Cake.Frosting/CakeServicesExtensions.cs b/src/Cake.Frosting/CakeServicesExtensions.cs new file mode 100644 index 0000000..e0b9309 --- /dev/null +++ b/src/Cake.Frosting/CakeServicesExtensions.cs @@ -0,0 +1,108 @@ +// 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. + +using System.Reflection; +using Cake.Core.Composition; +using Cake.Frosting.Internal; + +namespace Cake.Frosting +{ + /// + /// Contains extensions for . + /// + public static class CakeServicesExtensions + { + /// + /// Registers the specified context type. + /// Only the last registration will be used. + /// + /// The type of the context to register. + /// The service registration collection. + /// The same instance so that multiple calls can be chained. + public static ICakeServices UseContext(this ICakeServices services) + where TContext : IFrostingContext + { + Guard.ArgumentNotNull(services, nameof(services)); + + services.RegisterType().AsSelf().As().Singleton(); + return services; + } + + /// + /// Registers the specified lifetime type. + /// Only the last registration will be used. + /// + /// The type of the lifetime. + /// The service registration collection. + /// The same instance so that multiple calls can be chained. + public static ICakeServices UseLifetime(this ICakeServices services) + where TLifetime : IFrostingLifetime + { + Guard.ArgumentNotNull(services, nameof(services)); + + services.RegisterType().As().Singleton(); + return services; + } + + /// + /// Registers the specified task lifetime type. + /// Only the last registration will be used. + /// + /// The type of the lifetime. + /// The service registration collection. + /// The same instance so that multiple calls can be chained. + public static ICakeServices UseTaskLifetime(this ICakeServices services) + where TLifetime : IFrostingTaskLifetime + { + Guard.ArgumentNotNull(services, nameof(services)); + + services.RegisterType().As().Singleton(); + return services; + } + + /// + /// Registers the specified . + /// + /// The service registration collection. + /// The assembly to register. + /// The same instance so that multiple calls can be chained. + public static ICakeServices UseAssembly(this ICakeServices services, Assembly assembly) + { + Guard.ArgumentNotNull(services, nameof(services)); + + services.RegisterInstance(assembly).Singleton(); + return services; + } + + /// + /// Registers the specified module type. + /// + /// The type of the module + /// The service registration collection. + /// The same instance so that multiple calls can be chained. + public static ICakeServices UseModule(this ICakeServices services) + where T : ICakeModule, new() + { + Guard.ArgumentNotNull(services, nameof(services)); + + var module = new T(); + module.Register(services); + return services; + } + + /// + /// Add or replace a setting in the configuration. + /// + /// The service registration collection. + /// The key of the setting to add or replace. + /// The value of the setting to add or replace. + /// The same instance so that multiple calls can be chained. + public static ICakeServices UseSetting(this ICakeServices services, string key, string value) + { + var info = new ConfigurationValue(key, value); + services.RegisterInstance(info).AsSelf().Singleton(); + return services; + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/FrostingContext.cs b/src/Cake.Frosting/FrostingContext.cs new file mode 100644 index 0000000..b6ee542 --- /dev/null +++ b/src/Cake.Frosting/FrostingContext.cs @@ -0,0 +1,25 @@ +// 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. + +using Cake.Core; + +namespace Cake.Frosting +{ + /// + /// Default implementation of the build context. + /// + /// + /// + public class FrostingContext : CakeContextAdapter, IFrostingContext + { + /// + /// Initializes a new instance of the class. + /// + /// The Cake context. + public FrostingContext(ICakeContext context) + : base(context) + { + } + } +} diff --git a/src/Cake.Frosting/FrostingException.cs b/src/Cake.Frosting/FrostingException.cs new file mode 100644 index 0000000..c4c3af0 --- /dev/null +++ b/src/Cake.Frosting/FrostingException.cs @@ -0,0 +1,34 @@ +// 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. + +using System; + +namespace Cake.Frosting +{ + /// + /// Represent errors that occur during execution of Cake. + /// + /// + public sealed class FrostingException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public FrostingException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. + public FrostingException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/Cake.Frosting/FrostingLifetime.cs b/src/Cake.Frosting/FrostingLifetime.cs new file mode 100644 index 0000000..7ab629f --- /dev/null +++ b/src/Cake.Frosting/FrostingLifetime.cs @@ -0,0 +1,62 @@ +// 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. + +using System.Diagnostics.CodeAnalysis; +using Cake.Core; +using Cake.Frosting.Internal; + +namespace Cake.Frosting +{ + /// + /// Base class for the Setup/Teardown logic of a Cake run. + /// + /// + public abstract class FrostingLifetime : FrostingLifetime + { + } + + /// + /// Base class for the Setup/Teardown logic of a Cake run. + /// + /// The type of the context. + /// + public abstract class FrostingLifetime : IFrostingLifetime + where TContext : ICakeContext + { + /// + /// This method is executed before any tasks are run. + /// If setup fails, no tasks will be executed but teardown will be performed. + /// + /// The context. + public virtual void Setup(TContext context) + { + } + + /// + /// This method is executed after all tasks have been run. + /// If a setup action or a task fails with or without recovery, the specified teardown action will still be executed. + /// + /// The context. + public virtual void Teardown(TContext context) + { + } + + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Explicit implementation.")] + void IFrostingLifetime.Setup(ICakeContext context) + { + Guard.ArgumentNotNull(context, nameof(context)); + + Setup((TContext)context); + } + + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Explicit implementation.")] + void IFrostingLifetime.Teardown(ICakeContext context, ITeardownContext info) + { + Guard.ArgumentNotNull(context, nameof(context)); + Guard.ArgumentNotNull(info, nameof(info)); + + Teardown((TContext)context); + } + } +} diff --git a/src/Cake.Frosting/FrostingModule.cs b/src/Cake.Frosting/FrostingModule.cs new file mode 100644 index 0000000..65644b3 --- /dev/null +++ b/src/Cake.Frosting/FrostingModule.cs @@ -0,0 +1,54 @@ +// 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. + +using System.Reflection; +using Cake.Core; +using Cake.Core.Composition; +using Cake.Core.Configuration; +using Cake.Core.Diagnostics; +using Cake.Frosting.Internal; +using Cake.Frosting.Internal.Commands; +using Cake.Frosting.Internal.Diagnostics; + +namespace Cake.Frosting +{ + internal class FrostingModule : ICakeModule + { + public void Register(ICakeContainerRegistrar registry) + { + // Register the entry assembly. + registry.RegisterInstance(Assembly.GetEntryAssembly()); + + // Core services + registry.RegisterType().As().Singleton(); + registry.RegisterType().Singleton(); + registry.RegisterType().As().Singleton(); + registry.RegisterType().Singleton(); + registry.RegisterType().As().Singleton(); + + // Logging + registry.RegisterType().As().Singleton(); + registry.RegisterType().As().Singleton(); + + // Configuration + registry.RegisterType().AsSelf().Singleton(); + registry.RegisterType().As().Singleton(); + + // Commands + registry.RegisterType().AsSelf().Singleton(); + registry.RegisterType().AsSelf().Singleton(); + registry.RegisterType().AsSelf().Singleton(); + registry.RegisterType().AsSelf().Singleton(); + registry.RegisterType().AsSelf().Singleton(); + + // Misc + registry.RegisterType().As().Singleton(); + registry.RegisterType().As().Singleton(); + + // Register default stuff. + registry.RegisterType().AsSelf().As().Singleton(); + registry.RegisterType().AsSelf().Singleton(); + } + } +} diff --git a/src/Cake.Frosting/FrostingTask.cs b/src/Cake.Frosting/FrostingTask.cs new file mode 100644 index 0000000..8a05df7 --- /dev/null +++ b/src/Cake.Frosting/FrostingTask.cs @@ -0,0 +1,63 @@ +// 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. + +using System.Diagnostics.CodeAnalysis; +using Cake.Core; +using Cake.Frosting.Internal; + +namespace Cake.Frosting +{ + /// + /// Base class for a Frosting task using the standard context. + /// + /// + public abstract class FrostingTask : FrostingTask + { + } + + /// + /// Base class for a Frosting task using a custom context. + /// + /// The context type. + /// + public abstract class FrostingTask : IFrostingTask + where T : ICakeContext + { + /// + /// Runs the task using the specified context. + /// + /// The context. + public virtual void Run(T context) + { + } + + /// + /// Gets whether or not the task should be run. + /// + /// The context. + /// + /// true if the task should run; otherwise false. + /// + public virtual bool ShouldRun(T context) + { + return true; + } + + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Explicit implementation.")] + void IFrostingTask.Run(ICakeContext context) + { + Guard.ArgumentNotNull(context, nameof(context)); + + Run((T)context); + } + + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Explicit implementation.")] + bool IFrostingTask.ShouldRun(ICakeContext context) + { + Guard.ArgumentNotNull(context, nameof(context)); + + return ShouldRun((T)context); + } + } +} diff --git a/src/Cake.Frosting/FrostingTaskLifetime.cs b/src/Cake.Frosting/FrostingTaskLifetime.cs new file mode 100644 index 0000000..09200a2 --- /dev/null +++ b/src/Cake.Frosting/FrostingTaskLifetime.cs @@ -0,0 +1,65 @@ +// 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. + +using System.Diagnostics.CodeAnalysis; +using Cake.Core; +using Cake.Frosting.Internal; + +namespace Cake.Frosting +{ + /// + /// Base class for the lifetime for a task. + /// + /// + public abstract class FrostingTaskLifetime : FrostingTaskLifetime + { + } + + /// + /// Base class for the lifetime for a task. + /// + /// The build script context type. + /// + public abstract class FrostingTaskLifetime : IFrostingTaskLifetime + where TContext : ICakeContext + { + /// + /// This method is executed before each task is run. + /// If the task setup fails, the task will not be executed but the task's teardown will be performed. + /// + /// The context. + /// The setup information. + public virtual void Setup(TContext context, ITaskSetupContext info) + { + } + + /// + /// This method is executed after each task have been run. + /// If a task setup action or a task fails with or without recovery, the specified task teardown action will still be executed. + /// + /// The context. + /// The teardown information. + public virtual void Teardown(TContext context, ITaskTeardownContext info) + { + } + + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Explicit implementation.")] + void IFrostingTaskLifetime.Setup(ICakeContext context, ITaskSetupContext info) + { + Guard.ArgumentNotNull(context, nameof(context)); + Guard.ArgumentNotNull(info, nameof(info)); + + Setup((TContext)context, info); + } + + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Explicit implementation.")] + void IFrostingTaskLifetime.Teardown(ICakeContext context, ITaskTeardownContext info) + { + Guard.ArgumentNotNull(context, nameof(context)); + Guard.ArgumentNotNull(info, nameof(info)); + + Teardown((TContext)context, info); + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/ICakeHost.cs b/src/Cake.Frosting/ICakeHost.cs new file mode 100644 index 0000000..0d75f78 --- /dev/null +++ b/src/Cake.Frosting/ICakeHost.cs @@ -0,0 +1,18 @@ +// 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.Frosting +{ + /// + /// Represents a configured Cake host. + /// + public interface ICakeHost + { + /// + /// Runs the configured Cake host. + /// + /// An exit code indicating success or failure. + int Run(); + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/ICakeHostBuilder.cs b/src/Cake.Frosting/ICakeHostBuilder.cs new file mode 100644 index 0000000..a348a53 --- /dev/null +++ b/src/Cake.Frosting/ICakeHostBuilder.cs @@ -0,0 +1,27 @@ +// 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. + +using System; + +namespace Cake.Frosting +{ + /// + /// Represents a builder for . + /// + public interface ICakeHostBuilder + { + /// + /// Adds a delegate for configuring additional services for the host. + /// + /// A delegate for configuring the . + /// The same instance so that multiple calls can be chained. + ICakeHostBuilder ConfigureServices(Action configureServices); + + /// + /// Builds the required services and an using the specified options. + /// + /// The built . + ICakeHost Build(); + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/ICakeServices.cs b/src/Cake.Frosting/ICakeServices.cs new file mode 100644 index 0000000..4209973 --- /dev/null +++ b/src/Cake.Frosting/ICakeServices.cs @@ -0,0 +1,16 @@ +// 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. + +using Cake.Core.Composition; + +namespace Cake.Frosting +{ + /// + /// Represents a collection of service registrations. + /// + /// + public interface ICakeServices : ICakeContainerRegistrar + { + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/ICakeTaskFinder.cs b/src/Cake.Frosting/ICakeTaskFinder.cs new file mode 100644 index 0000000..c99302a --- /dev/null +++ b/src/Cake.Frosting/ICakeTaskFinder.cs @@ -0,0 +1,23 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Cake.Frosting +{ + /// + /// Represents Frosting's task finder mechanism. + /// + public interface ICakeTaskFinder + { + /// + /// Gets all task types present in the provided assemblies. + /// + /// The assemblies. + /// An array of task types. + Type[] GetTasks(IEnumerable assemblies); + } +} diff --git a/src/Cake.Frosting/IFrostingContext.cs b/src/Cake.Frosting/IFrostingContext.cs new file mode 100644 index 0000000..85d7d2b --- /dev/null +++ b/src/Cake.Frosting/IFrostingContext.cs @@ -0,0 +1,16 @@ +// 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. + +using Cake.Core; + +namespace Cake.Frosting +{ + /// + /// Represents the build context. + /// + /// + public interface IFrostingContext : ICakeContext + { + } +} diff --git a/src/Cake.Frosting/IFrostingLifetime.cs b/src/Cake.Frosting/IFrostingLifetime.cs new file mode 100644 index 0000000..325fde2 --- /dev/null +++ b/src/Cake.Frosting/IFrostingLifetime.cs @@ -0,0 +1,29 @@ +// 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. + +using Cake.Core; + +namespace Cake.Frosting +{ + /// + /// Represents the Setup/Teardown logic for a Cake run. + /// + public interface IFrostingLifetime + { + /// + /// This method is executed before any tasks are run. + /// If setup fails, no tasks will be executed but teardown will be performed. + /// + /// The context. + void Setup(ICakeContext context); + + /// + /// This method is executed after all tasks have been run. + /// If a setup action or a task fails with or without recovery, the specified teardown action will still be executed. + /// + /// The context. + /// The teardown information. + void Teardown(ICakeContext context, ITeardownContext info); + } +} diff --git a/src/Cake.Frosting/IFrostingStartup.cs b/src/Cake.Frosting/IFrostingStartup.cs new file mode 100644 index 0000000..7e9ebd7 --- /dev/null +++ b/src/Cake.Frosting/IFrostingStartup.cs @@ -0,0 +1,18 @@ +// 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.Frosting +{ + /// + /// Represents a startup configuration. + /// + public interface IFrostingStartup + { + /// + /// Configures services used by Cake. + /// + /// The services to configure. + void Configure(ICakeServices services); + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/IFrostingTask.cs b/src/Cake.Frosting/IFrostingTask.cs new file mode 100644 index 0000000..e50b99a --- /dev/null +++ b/src/Cake.Frosting/IFrostingTask.cs @@ -0,0 +1,29 @@ +// 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. + +using Cake.Core; + +namespace Cake.Frosting +{ + /// + /// A represents a unit of work. + /// + public interface IFrostingTask + { + /// + /// Runs the task using the specified context. + /// + /// The context. + void Run(ICakeContext context); + + /// + /// Gets whether or not the task should be run. + /// + /// The context. + /// + /// true if the task should run; otherwise false. + /// + bool ShouldRun(ICakeContext context); + } +} diff --git a/src/Cake.Frosting/IFrostingTaskLifetime.cs b/src/Cake.Frosting/IFrostingTaskLifetime.cs new file mode 100644 index 0000000..36a7870 --- /dev/null +++ b/src/Cake.Frosting/IFrostingTaskLifetime.cs @@ -0,0 +1,30 @@ +// 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. + +using Cake.Core; + +namespace Cake.Frosting +{ + /// + /// Represents the lifetime for all tasks. + /// + public interface IFrostingTaskLifetime + { + /// + /// This method is executed before each task is run. + /// If the task setup fails, the task will not be executed but the task's teardown will be performed. + /// + /// The context. + /// The setup information. + void Setup(ICakeContext context, ITaskSetupContext info); + + /// + /// This method is executed after each task have been run. + /// If a task setup action or a task fails with or without recovery, the specified task teardown action will still be executed. + /// + /// The context. + /// The teardown information. + void Teardown(ICakeContext context, ITaskTeardownContext info); + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/Arguments/ArgumentParser.cs b/src/Cake.Frosting/Internal/Arguments/ArgumentParser.cs new file mode 100644 index 0000000..28667ae --- /dev/null +++ b/src/Cake.Frosting/Internal/Arguments/ArgumentParser.cs @@ -0,0 +1,148 @@ +// 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. + +using System; +using System.Collections.Generic; +using Cake.Core; +using Cake.Core.Diagnostics; +using Cake.Core.IO; + +namespace Cake.Frosting.Internal.Arguments +{ + internal static class ArgumentParser + { + public static CakeHostOptions Parse(IEnumerable args) + { + if (args == null) + { + throw new ArgumentNullException(nameof(args)); + } + + var options = new CakeHostOptions + { + WorkingDirectory = ".", + Target = "Default", + Verbosity = Verbosity.Normal + }; + + foreach (var argument in args) + { + ParseOption(argument.UnQuote(), options); + } + + return options; + } + + private static void ParseOption(string arg, CakeHostOptions options) + { + if (!IsOption(arg)) + { + throw new FrostingException($"Encountered invalid option '{arg}'"); + } + + string name, value; + + var nameIndex = arg.StartsWith("--") ? 2 : 1; + var separatorIndex = arg.IndexOfAny(new[] { '=' }); + if (separatorIndex < 0) + { + name = arg.Substring(nameIndex); + value = string.Empty; + } + else + { + name = arg.Substring(nameIndex, separatorIndex - nameIndex); + value = arg.Substring(separatorIndex + 1); + } + + ParseOption(name, value.UnQuote(), options); + } + + private static void ParseOption(string name, string value, CakeHostOptions options) + { + if (name.Equals("working", StringComparison.OrdinalIgnoreCase) || + name.Equals("w", StringComparison.OrdinalIgnoreCase)) + { + options.WorkingDirectory = new DirectoryPath(value); + } + + if (name.Equals("verbosity", StringComparison.OrdinalIgnoreCase) + || name.Equals("v", StringComparison.OrdinalIgnoreCase)) + { + Verbosity verbosity; + if (!VerbosityParser.TryParse(value, out verbosity)) + { + throw new CakeException($"The value '{value}' is not a valid verbosity."); + } + options.Verbosity = verbosity; + } + + if (name.Equals("target", StringComparison.OrdinalIgnoreCase) || + name.Equals("t", StringComparison.OrdinalIgnoreCase)) + { + options.Target = value; + } + + if (name.Equals("help", StringComparison.OrdinalIgnoreCase) || + name.Equals("h", StringComparison.OrdinalIgnoreCase)) + { + if (ParseBooleanValue(value)) + { + options.Command = CakeHostCommand.Help; + } + } + + if (name.Equals("dryrun", StringComparison.OrdinalIgnoreCase) || + name.Equals("d", StringComparison.OrdinalIgnoreCase)) + { + if (ParseBooleanValue(value)) + { + options.Command = CakeHostCommand.DryRun; + } + } + + if (name.Equals("version", StringComparison.OrdinalIgnoreCase)) + { + if (ParseBooleanValue(value)) + { + options.Command = CakeHostCommand.Version; + } + } + + if (options.Arguments.ContainsKey(name)) + { + throw new CakeException($"More than one argument called '{name}' was encountered."); + } + + options.Arguments.Add(name, value); + } + + private static bool IsOption(string arg) + { + if (string.IsNullOrWhiteSpace(arg)) + { + return false; + } + return arg.StartsWith("--") || arg.StartsWith("-"); + } + + private static bool ParseBooleanValue(string value) + { + value = (value ?? string.Empty).UnQuote(); + if (string.IsNullOrWhiteSpace(value)) + { + return true; + } + if (value.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + if (value.Equals("false", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + throw new CakeException($"Argument value '{value}' is not a valid boolean value."); + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/Arguments/VerbosityParser.cs b/src/Cake.Frosting/Internal/Arguments/VerbosityParser.cs new file mode 100644 index 0000000..07dd716 --- /dev/null +++ b/src/Cake.Frosting/Internal/Arguments/VerbosityParser.cs @@ -0,0 +1,37 @@ +// 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. + +using System; +using System.Collections.Generic; +using Cake.Core.Diagnostics; + +namespace Cake.Frosting.Internal.Arguments +{ + internal static class VerbosityParser + { + private static readonly Dictionary Lookup; + + static VerbosityParser() + { + Lookup = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "q", Verbosity.Quiet }, + { "quiet", Verbosity.Quiet }, + { "m", Verbosity.Minimal }, + { "minimal", Verbosity.Minimal }, + { "n", Verbosity.Normal }, + { "normal", Verbosity.Normal }, + { "v", Verbosity.Verbose }, + { "verbose", Verbosity.Verbose }, + { "d", Verbosity.Diagnostic }, + { "diagnostic", Verbosity.Diagnostic } + }; + } + + public static bool TryParse(string value, out Verbosity verbosity) + { + return Lookup.TryGetValue(value, out verbosity); + } + } +} diff --git a/src/Cake.Frosting/Internal/Commands/Command.cs b/src/Cake.Frosting/Internal/Commands/Command.cs new file mode 100644 index 0000000..0ff1948 --- /dev/null +++ b/src/Cake.Frosting/Internal/Commands/Command.cs @@ -0,0 +1,13 @@ +// 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. + +using Cake.Core; + +namespace Cake.Frosting.Internal.Commands +{ + internal abstract class Command + { + public abstract bool Execute(ICakeEngine engine, CakeHostOptions options); + } +} diff --git a/src/Cake.Frosting/Internal/Commands/CommandFactory.cs b/src/Cake.Frosting/Internal/Commands/CommandFactory.cs new file mode 100644 index 0000000..cd119cf --- /dev/null +++ b/src/Cake.Frosting/Internal/Commands/CommandFactory.cs @@ -0,0 +1,35 @@ +// 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. + +using Autofac; + +namespace Cake.Frosting.Internal.Commands +{ + internal sealed class CommandFactory + { + private readonly ILifetimeScope _scope; + + public CommandFactory(ILifetimeScope scope) + { + _scope = scope; + } + + public Command GetCommand(CakeHostOptions options) + { + switch (options.Command) + { + case CakeHostCommand.Help: + return _scope.Resolve(); + case CakeHostCommand.DryRun: + return _scope.Resolve(); + case CakeHostCommand.Version: + return _scope.Resolve(); + case CakeHostCommand.Run: + return _scope.Resolve(); + default: + return new ErrorDecoratorCommand(_scope.Resolve()); + } + } + } +} diff --git a/src/Cake.Frosting/Internal/Commands/DryRunCommand.cs b/src/Cake.Frosting/Internal/Commands/DryRunCommand.cs new file mode 100644 index 0000000..ac5bc90 --- /dev/null +++ b/src/Cake.Frosting/Internal/Commands/DryRunCommand.cs @@ -0,0 +1,37 @@ +// 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. + +using Cake.Core; +using Cake.Core.Diagnostics; + +namespace Cake.Frosting.Internal.Commands +{ + internal sealed class DryRunCommand : Command + { + private readonly IFrostingContext _context; + private readonly ICakeLog _log; + + public DryRunCommand(IFrostingContext context, ICakeLog log) + { + _context = context; + _log = log; + } + + public override bool Execute(ICakeEngine engine, CakeHostOptions options) + { + _log.Information("Performing dry run..."); + _log.Information("Target is: {0}", options.Target); + _log.Information(string.Empty); + + var strategy = new DryRunExecutionStrategy(_log); + engine.RunTarget(_context, strategy, options.Target); + + _log.Information(string.Empty); + _log.Information("This was a dry run."); + _log.Information("No tasks were actually executed."); + + return true; + } + } +} diff --git a/src/Cake.Frosting/Internal/Commands/DryRunExecutionStrategy.cs b/src/Cake.Frosting/Internal/Commands/DryRunExecutionStrategy.cs new file mode 100644 index 0000000..33e1e8e --- /dev/null +++ b/src/Cake.Frosting/Internal/Commands/DryRunExecutionStrategy.cs @@ -0,0 +1,67 @@ +// 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. + +using System; +using Cake.Core; +using Cake.Core.Diagnostics; + +namespace Cake.Frosting.Internal.Commands +{ + internal sealed class DryRunExecutionStrategy : IExecutionStrategy + { + private readonly ICakeLog _log; + private int _counter; + + public DryRunExecutionStrategy(ICakeLog log) + { + if (log == null) + { + throw new ArgumentNullException(nameof(log)); + } + _log = log; + _counter = 1; + } + + public void PerformSetup(Action action, ICakeContext context) + { + } + + public void PerformTeardown(Action action, ITeardownContext teardownContext) + { + } + + public void Execute(CakeTask task, ICakeContext context) + { + if (task != null) + { + _log.Information("{0}. {1}", _counter, task.Name); + _counter++; + } + } + + public void Skip(CakeTask task) + { + } + + public void ReportErrors(Action action, Exception exception) + { + } + + public void HandleErrors(Action action, Exception exception) + { + } + + public void InvokeFinally(Action action) + { + } + + public void PerformTaskSetup(Action action, ITaskSetupContext taskSetupContext) + { + } + + public void PerformTaskTeardown(Action action, ITaskTeardownContext taskTeardownContext) + { + } + } +} diff --git a/src/Cake.Frosting/Internal/Commands/ErrorDecoratorCommand.cs b/src/Cake.Frosting/Internal/Commands/ErrorDecoratorCommand.cs new file mode 100644 index 0000000..9bf74e8 --- /dev/null +++ b/src/Cake.Frosting/Internal/Commands/ErrorDecoratorCommand.cs @@ -0,0 +1,24 @@ +// 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. + +using Cake.Core; + +namespace Cake.Frosting.Internal.Commands +{ + internal sealed class ErrorDecoratorCommand : Command + { + private readonly Command _command; + + public ErrorDecoratorCommand(Command command) + { + _command = command; + } + + public override bool Execute(ICakeEngine engine, CakeHostOptions options) + { + _command.Execute(engine, options); + return false; + } + } +} diff --git a/src/Cake.Frosting/Internal/Commands/HelpCommand.cs b/src/Cake.Frosting/Internal/Commands/HelpCommand.cs new file mode 100644 index 0000000..77a53cb --- /dev/null +++ b/src/Cake.Frosting/Internal/Commands/HelpCommand.cs @@ -0,0 +1,43 @@ +// 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. + +using System; +using System.Reflection; +using Cake.Core; + +namespace Cake.Frosting.Internal.Commands +{ + internal sealed class HelpCommand : Command + { + private readonly IConsole _console; + + public HelpCommand(IConsole console) + { + _console = console; + } + + public override bool Execute(ICakeEngine engine, CakeHostOptions options) + { + _console.Write("Cake.Frosting ("); + _console.ForegroundColor = ConsoleColor.Yellow; + _console.Write(typeof(HelpCommand).GetTypeInfo().Assembly.GetName().Version.ToString(3)); + _console.ResetColor(); + _console.WriteLine(")"); + + _console.WriteLine("Usage:"); + _console.WriteLine(" dotnet {0}.dll [options]", typeof(HelpCommand).GetTypeInfo().Assembly.GetName().Name); + _console.WriteLine(); + _console.WriteLine("Options:"); + _console.WriteLine(" --target|-t Sets the build target"); + _console.WriteLine(" --working|-w Sets the working directory"); + _console.WriteLine(" --verbosity|-v Sets the verbosity"); + _console.WriteLine(" --dryrun|-r Performs a dry run"); + _console.WriteLine(" --version Displays Cake.Frosting version number"); + _console.WriteLine(" --help|-h Show help"); + _console.WriteLine(); + + return true; + } + } +} diff --git a/src/Cake.Frosting/Internal/Commands/RunCommand.cs b/src/Cake.Frosting/Internal/Commands/RunCommand.cs new file mode 100644 index 0000000..6d6bf67 --- /dev/null +++ b/src/Cake.Frosting/Internal/Commands/RunCommand.cs @@ -0,0 +1,36 @@ +// 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. + +using Cake.Core; + +namespace Cake.Frosting.Internal.Commands +{ + internal sealed class RunCommand : Command + { + private readonly IFrostingContext _context; + private readonly IExecutionStrategy _strategy; + private readonly ICakeReportPrinter _printer; + + public RunCommand( + IFrostingContext context, + IExecutionStrategy strategy, + ICakeReportPrinter printer) + { + _context = context; + _strategy = strategy; + _printer = printer; + } + + public override bool Execute(ICakeEngine engine, CakeHostOptions options) + { + var report = engine.RunTarget(_context, _strategy, options.Target); + if (report != null && !report.IsEmpty) + { + _printer.Write(report); + } + + return true; + } + } +} diff --git a/src/Cake.Frosting/Internal/Commands/VersionCommand.cs b/src/Cake.Frosting/Internal/Commands/VersionCommand.cs new file mode 100644 index 0000000..f5636af --- /dev/null +++ b/src/Cake.Frosting/Internal/Commands/VersionCommand.cs @@ -0,0 +1,25 @@ +// 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. + +using System.Reflection; +using Cake.Core; + +namespace Cake.Frosting.Internal.Commands +{ + internal sealed class VersionCommand : Command + { + private readonly IConsole _console; + + public VersionCommand(IConsole console) + { + _console = console; + } + + public override bool Execute(ICakeEngine engine, CakeHostOptions options) + { + _console.Write(typeof(HelpCommand).GetTypeInfo().Assembly.GetName().Version.ToString(3)); + return true; + } + } +} diff --git a/src/Cake.Frosting/Internal/Composition/ContainerRegistrar.cs b/src/Cake.Frosting/Internal/Composition/ContainerRegistrar.cs new file mode 100644 index 0000000..1b9b223 --- /dev/null +++ b/src/Cake.Frosting/Internal/Composition/ContainerRegistrar.cs @@ -0,0 +1,48 @@ +// 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. + +using System; +using Autofac; +using Autofac.Builder; +using Cake.Core.Composition; + +namespace Cake.Frosting.Internal.Composition +{ + internal sealed class ContainerRegistrar : ICakeServices + { + public ContainerBuilder Builder { get; } + + public ContainerRegistrar() + : this(null) + { + } + + public ContainerRegistrar(ContainerBuilder builder) + { + Builder = builder ?? new ContainerBuilder(); + } + + public void RegisterModule(ICakeModule module) + { + module.Register(this); + } + + public ICakeRegistrationBuilder RegisterType(Type type) + { + var registration = Builder.RegisterType(type); + return new ContainerRegistration(registration); + } + + public ICakeRegistrationBuilder RegisterInstance(T instance) where T : class + { + var registration = Builder.RegisterInstance(instance); + return new ContainerRegistration(registration); + } + + public IContainer Build() + { + return Builder.Build(); + } + } +} diff --git a/src/Cake.Frosting/Internal/Composition/ContainerRegistration.cs b/src/Cake.Frosting/Internal/Composition/ContainerRegistration.cs new file mode 100644 index 0000000..1d1f220 --- /dev/null +++ b/src/Cake.Frosting/Internal/Composition/ContainerRegistration.cs @@ -0,0 +1,46 @@ +// 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. + +using System; +using Autofac; +using Autofac.Builder; +using Cake.Core.Composition; + +namespace Cake.Frosting.Internal.Composition +{ + internal class ContainerRegistration : ICakeRegistrationBuilder + where TActivator : IConcreteActivatorData + { + private IRegistrationBuilder _registration; + + public ContainerRegistration(IRegistrationBuilder registration) + { + _registration = registration; + } + + public ICakeRegistrationBuilder As(Type type) + { + _registration = _registration.As(type); + return this; + } + + public ICakeRegistrationBuilder AsSelf() + { + _registration = _registration.AsSelf(); + return this; + } + + public ICakeRegistrationBuilder Singleton() + { + _registration = _registration.SingleInstance(); + return this; + } + + public ICakeRegistrationBuilder Transient() + { + _registration = _registration.InstancePerDependency(); + return this; + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/Configuration.cs b/src/Cake.Frosting/Internal/Configuration.cs new file mode 100644 index 0000000..359b063 --- /dev/null +++ b/src/Cake.Frosting/Internal/Configuration.cs @@ -0,0 +1,57 @@ +// 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. + +using System; +using System.Collections.Generic; +using Cake.Core; +using Cake.Core.Configuration; + +namespace Cake.Frosting.Internal +{ + internal sealed class Configuration : ICakeConfiguration + { + private readonly CakeConfigurationProvider _provider; + private readonly CakeHostOptions _options; + private readonly ICakeEnvironment _environment; + private readonly IEnumerable _values; + private readonly object _lock; + private ICakeConfiguration _configuration; + + public Configuration( + CakeConfigurationProvider provider, + CakeHostOptions options, + ICakeEnvironment environment, + IEnumerable values) + { + _provider = provider; + _options = options; + _environment = environment; + _values = values; + _lock = new object(); + _configuration = null; + } + + public string GetValue(string key) + { + lock (_lock) + { + if (_configuration == null) + { + // Add additional configuration values. + var arguments = new Dictionary(_options.Arguments, StringComparer.OrdinalIgnoreCase); + if (_values != null) + { + foreach (var value in _values) + { + arguments[value.Key] = value.Value; + } + } + + _configuration = _provider.CreateConfiguration(_environment.WorkingDirectory, arguments); + } + return _configuration.GetValue(key); + } + } + } +} diff --git a/src/Cake.Frosting/Internal/ConfigurationValue.cs b/src/Cake.Frosting/Internal/ConfigurationValue.cs new file mode 100644 index 0000000..56838c0 --- /dev/null +++ b/src/Cake.Frosting/Internal/ConfigurationValue.cs @@ -0,0 +1,19 @@ +// 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.Frosting.Internal +{ + internal sealed class ConfigurationValue + { + public string Key { get; } + + public string Value { get; } + + public ConfigurationValue(string key, string value) + { + Key = key; + Value = value; + } + } +} diff --git a/src/Cake.Frosting/Internal/DefaultConsole.cs b/src/Cake.Frosting/Internal/DefaultConsole.cs new file mode 100644 index 0000000..174b7c1 --- /dev/null +++ b/src/Cake.Frosting/Internal/DefaultConsole.cs @@ -0,0 +1,86 @@ +// 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. + +using System; +using Cake.Core; + +namespace Cake.Frosting.Internal +{ + internal sealed class DefaultConsole : IConsole + { + /// + /// Gets or sets the foreground color. + /// + /// The foreground color. + public ConsoleColor ForegroundColor + { + get { return Console.ForegroundColor; } + set { Console.ForegroundColor = value; } + } + + /// + /// Gets or sets the background color. + /// + /// The background color. + public ConsoleColor BackgroundColor + { + get { return Console.BackgroundColor; } + set { Console.BackgroundColor = value; } + } + + /// + /// Writes the text representation of the specified array of objects to the + /// console output using the specified format information. + /// + /// A composite format string + /// An array of objects to write using format. + public void Write(string format, params object[] arg) + { + Console.Write(format, arg); + } + + /// + /// Writes the text representation of the specified array of objects, followed + /// by the current line terminator, to the console output using the specified + /// format information. + /// + /// A composite format string + /// An array of objects to write using format. + public void WriteLine(string format, params object[] arg) + { + Console.WriteLine(format, arg); + } + + /// + /// Writes the text representation of the specified array of objects to the + /// console error output using the specified format information. + /// + /// A composite format string + /// An array of objects to write using format. + public void WriteError(string format, params object[] arg) + { + Console.Error.Write(format, arg); + } + + /// + /// Writes the text representation of the specified array of objects, followed + /// by the current line terminator, to the console error output using the + /// specified format information. + /// + /// A composite format string + /// An array of objects to write using format. + public void WriteErrorLine(string format, params object[] arg) + { + Console.Error.WriteLine(format, arg); + } + + /// + /// Sets the foreground and background console colors to their defaults. + /// + public void ResetColor() + { + Console.ResetColor(); + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/Diagnostics/CakeLog.cs b/src/Cake.Frosting/Internal/Diagnostics/CakeLog.cs new file mode 100644 index 0000000..658192e --- /dev/null +++ b/src/Cake.Frosting/Internal/Diagnostics/CakeLog.cs @@ -0,0 +1,99 @@ +// 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. + +using System; +using System.Collections.Generic; +using Cake.Core; +using Cake.Core.Diagnostics; +using Cake.Frosting.Internal.Diagnostics.Formatting; + +namespace Cake.Frosting.Internal.Diagnostics +{ + internal sealed class CakeLog : ICakeLog + { + private readonly IConsole _console; + private readonly object _lock; + private readonly IDictionary _palettes; + + public Verbosity Verbosity { get; set; } + + public CakeLog(IConsole console, Verbosity verbosity = Verbosity.Normal) + { + _console = console; + _lock = new object(); + _palettes = CreatePalette(); + Verbosity = verbosity; + } + + public void Write(Verbosity verbosity, LogLevel level, string format, params object[] args) + { + if (verbosity > Verbosity) + { + return; + } + lock (_lock) + { + try + { + var palette = _palettes[level]; + var tokens = FormatParser.Parse(format); + foreach (var token in tokens) + { + SetPalette(token, palette); + if (level > LogLevel.Error) + { + _console.Write("{0}", token.Render(args)); + } + else + { + _console.WriteError("{0}", token.Render(args)); + } + } + } + finally + { + _console.ResetColor(); + if (level > LogLevel.Error) + { + _console.WriteLine(); + } + else + { + _console.WriteErrorLine(); + } + } + } + } + + private void SetPalette(FormatToken token, ConsolePalette palette) + { + var property = token as PropertyToken; + if (property != null) + { + _console.BackgroundColor = palette.ArgumentBackground; + _console.ForegroundColor = palette.ArgumentForeground; + } + else + { + _console.BackgroundColor = palette.Background; + _console.ForegroundColor = palette.Foreground; + } + } + + private IDictionary CreatePalette() + { + var background = _console.BackgroundColor; + var palette = new Dictionary + { + { LogLevel.Fatal, new ConsolePalette(ConsoleColor.Magenta, ConsoleColor.White, ConsoleColor.DarkMagenta, ConsoleColor.White) }, + { LogLevel.Error, new ConsolePalette(ConsoleColor.DarkRed, ConsoleColor.White, ConsoleColor.Red, ConsoleColor.White) }, + { LogLevel.Warning, new ConsolePalette(background, ConsoleColor.Yellow, background, ConsoleColor.Yellow) }, + { LogLevel.Information, new ConsolePalette(background, ConsoleColor.White, ConsoleColor.DarkBlue, ConsoleColor.White) }, + { LogLevel.Verbose, new ConsolePalette(background, ConsoleColor.Gray, background, ConsoleColor.White) }, + { LogLevel.Debug, new ConsolePalette(background, ConsoleColor.DarkGray, background, ConsoleColor.Gray) } + }; + return palette; + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/Diagnostics/ConsolePalette.cs b/src/Cake.Frosting/Internal/Diagnostics/ConsolePalette.cs new file mode 100644 index 0000000..f6d0b85 --- /dev/null +++ b/src/Cake.Frosting/Internal/Diagnostics/ConsolePalette.cs @@ -0,0 +1,28 @@ +// 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. + +using System; + +namespace Cake.Frosting.Internal.Diagnostics +{ + internal sealed class ConsolePalette + { + public ConsoleColor Background { get; set; } + + public ConsoleColor Foreground { get; set; } + + public ConsoleColor ArgumentBackground { get; set; } + + public ConsoleColor ArgumentForeground { get; set; } + + public ConsolePalette(ConsoleColor background, ConsoleColor foreground, + ConsoleColor argumentBackground, ConsoleColor argumentForeground) + { + Background = background; + Foreground = foreground; + ArgumentBackground = argumentBackground; + ArgumentForeground = argumentForeground; + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/Diagnostics/Formatting/FormatParser.cs b/src/Cake.Frosting/Internal/Diagnostics/Formatting/FormatParser.cs new file mode 100644 index 0000000..677ae7f --- /dev/null +++ b/src/Cake.Frosting/Internal/Diagnostics/Formatting/FormatParser.cs @@ -0,0 +1,130 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; + +namespace Cake.Frosting.Internal.Diagnostics.Formatting +{ + internal static class FormatParser + { + public static IEnumerable Parse(string format) + { + var reader = new StringReader(format); + while (true) + { + var current = reader.Peek(); + if (current == -1) + { + break; + } + var character = (char)current; + if (character == '{') + { + yield return ParseProperty(reader); + } + else + { + yield return ParseText(reader); + } + } + } + + private static FormatToken ParseProperty(TextReader reader) + { + reader.Read(); // Consume + if (reader.Peek() == -1) + { + return new LiteralToken("{"); + } + if ((char)reader.Peek() == '{') + { + reader.Read(); + return new LiteralToken("{{"); + } + var builder = new StringBuilder(); + while (true) + { + var current = reader.Peek(); + if (current == -1) + { + break; + } + + var character = (char)current; + if (character == '}') + { + reader.Read(); + + var accumulated = builder.ToString(); + var parts = accumulated.Split(new[] { ':' }, StringSplitOptions.None); + if (parts.Length > 1) + { + var name = parts[0]; + var format = string.Join(string.Empty, parts.Skip(1)); + var positional = IsNumeric(name); + if (!positional) + { + throw new FormatException("Input string was not in a correct format."); + } + var position = int.Parse(name, CultureInfo.InvariantCulture); + return new PropertyToken(position, format); + } + else + { + var positional = IsNumeric(accumulated); + if (!positional) + { + throw new FormatException("Input string was not in a correct format."); + } + var position = int.Parse(accumulated, CultureInfo.InvariantCulture); + return new PropertyToken(position, null); + } + } + builder.Append((char)reader.Read()); + } + return new LiteralToken(builder.ToString()); + } + + private static FormatToken ParseText(TextReader reader) + { + var builder = new StringBuilder(); + while (true) + { + var current = reader.Peek(); + if (current == -1) + { + break; + } + var character = (char)current; + if (character == '{') + { + break; + } + builder.Append((char)reader.Read()); + } + return new LiteralToken(builder.ToString()); + } + + private static bool IsNumeric(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + foreach (var character in value) + { + if (!char.IsDigit(character)) + { + return false; + } + } + return true; + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/Diagnostics/Formatting/FormatToken.cs b/src/Cake.Frosting/Internal/Diagnostics/Formatting/FormatToken.cs new file mode 100644 index 0000000..6b69a37 --- /dev/null +++ b/src/Cake.Frosting/Internal/Diagnostics/Formatting/FormatToken.cs @@ -0,0 +1,11 @@ +// 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.Frosting.Internal.Diagnostics.Formatting +{ + internal abstract class FormatToken + { + public abstract string Render(object[] args); + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/Diagnostics/Formatting/LiteralToken.cs b/src/Cake.Frosting/Internal/Diagnostics/Formatting/LiteralToken.cs new file mode 100644 index 0000000..b5d410d --- /dev/null +++ b/src/Cake.Frosting/Internal/Diagnostics/Formatting/LiteralToken.cs @@ -0,0 +1,21 @@ +// 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.Frosting.Internal.Diagnostics.Formatting +{ + internal sealed class LiteralToken : FormatToken + { + public string Text { get; } + + public LiteralToken(string text) + { + Text = text; + } + + public override string Render(object[] args) + { + return Text; + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/Diagnostics/Formatting/PropertyToken.cs b/src/Cake.Frosting/Internal/Diagnostics/Formatting/PropertyToken.cs new file mode 100644 index 0000000..39b0e1d --- /dev/null +++ b/src/Cake.Frosting/Internal/Diagnostics/Formatting/PropertyToken.cs @@ -0,0 +1,36 @@ +// 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. + +using System; +using System.Globalization; + +namespace Cake.Frosting.Internal.Diagnostics.Formatting +{ + internal sealed class PropertyToken : FormatToken + { + public string Format { get; } + + public int Position { get; } + + public PropertyToken(int position, string format) + { + Position = position; + Format = format; + } + + public override string Render(object[] args) + { + var value = args[Position]; + if (!string.IsNullOrWhiteSpace(Format)) + { + var formattable = value as IFormattable; + if (formattable != null) + { + return formattable.ToString(Format, CultureInfo.InvariantCulture); + } + } + return value == null ? "[NULL]" : value.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/EngineInitializer.cs b/src/Cake.Frosting/Internal/EngineInitializer.cs new file mode 100644 index 0000000..b3ea08c --- /dev/null +++ b/src/Cake.Frosting/Internal/EngineInitializer.cs @@ -0,0 +1,74 @@ +// 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. + +using System.Collections.Generic; +using System.Reflection; +using Cake.Core; +using Cake.Core.Diagnostics; + +namespace Cake.Frosting.Internal +{ + internal sealed class EngineInitializer + { + private readonly ICakeLog _log; + + public EngineInitializer(ICakeLog log) + { + _log = log; + } + + public void Initialize(ICakeEngine engine, IFrostingContext context, IEnumerable tasks, + IFrostingLifetime lifetime, IFrostingTaskLifetime taskLifetime) + { + if (tasks != null) + { + foreach (var task in tasks) + { + var taskName = TaskNameHelper.GetTaskName(task); + _log.Debug("Registering task {0} with engine...", taskName); + + // Get the task's context type. + if (!task.HasCompatibleContext(context)) + { + const string format = "Task cannot be used since the context isn't convertable to {0}."; + _log.Warning(format, task.GetContextType().FullName); + } + else + { + // Register task with the Cake engine. + var cakeTask = engine.RegisterTask(taskName); + cakeTask.Does(task.Run); + cakeTask.WithCriteria(task.ShouldRun); + + // Add dependencies + var attributes = task.GetType().GetTypeInfo().GetCustomAttributes(); + foreach (var dependency in attributes) + { + var dependencyName = TaskNameHelper.GetTaskName(dependency); + if (!typeof(IFrostingTask).IsAssignableFrom(dependency.Task)) + { + throw new FrostingException($"The dependency {dependencyName} does not implement IFrostingTask."); + } + cakeTask.IsDependentOn(dependencyName); + } + } + } + } + + if (lifetime != null) + { + _log.Debug("Registering lifetime {0} with engine...", lifetime.GetType().FullName); + engine.RegisterSetupAction(info => lifetime.Setup(context)); + engine.RegisterTeardownAction(info => lifetime.Teardown(context, info)); + } + + if (taskLifetime != null) + { + _log.Debug("Registering task lifetime {0} with engine...", taskLifetime.GetType().Name); + engine.RegisterTaskSetupAction(info => taskLifetime.Setup(context, info)); + engine.RegisterTaskTeardownAction(info => taskLifetime.Teardown(context, info)); + } + } + } +} diff --git a/src/Cake.Frosting/Internal/ErrorCakeHost.cs b/src/Cake.Frosting/Internal/ErrorCakeHost.cs new file mode 100644 index 0000000..7aa3615 --- /dev/null +++ b/src/Cake.Frosting/Internal/ErrorCakeHost.cs @@ -0,0 +1,33 @@ +// 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. + +using System; +using Cake.Core.Diagnostics; +using Cake.Frosting.Internal.Diagnostics; + +namespace Cake.Frosting.Internal +{ + internal sealed class ErrorCakeHost : ICakeHost + { + private readonly ICakeLog _log; + private readonly Exception _exception; + + public ErrorCakeHost(Exception exception) + : this(null, exception) + { + } + + public ErrorCakeHost(ICakeLog log, Exception exception) + { + _log = log ?? new CakeLog(new DefaultConsole(), Verbosity.Verbose); + _exception = exception; + } + + public int Run() + { + ErrorHandler.OutputError(_log, _exception); + return ErrorHandler.GetExitCode(_exception); + } + } +} diff --git a/src/Cake.Frosting/Internal/ErrorCakeHostBuilder.cs b/src/Cake.Frosting/Internal/ErrorCakeHostBuilder.cs new file mode 100644 index 0000000..2085276 --- /dev/null +++ b/src/Cake.Frosting/Internal/ErrorCakeHostBuilder.cs @@ -0,0 +1,32 @@ +// 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. + +using System; +using Cake.Core.Diagnostics; +using Cake.Frosting.Internal.Diagnostics; + +namespace Cake.Frosting.Internal +{ + internal sealed class ErrorCakeHostBuilder : ICakeHostBuilder + { + private readonly ICakeLog _log; + private readonly Exception _exception; + + public ErrorCakeHostBuilder(Exception exception) + { + _log = new CakeLog(new DefaultConsole(), Verbosity.Verbose); + _exception = exception; + } + + public ICakeHostBuilder ConfigureServices(Action configureServices) + { + return this; + } + + public ICakeHost Build() + { + return new ErrorCakeHost(_log, _exception); + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/ErrorHandler.cs b/src/Cake.Frosting/Internal/ErrorHandler.cs new file mode 100644 index 0000000..248d63d --- /dev/null +++ b/src/Cake.Frosting/Internal/ErrorHandler.cs @@ -0,0 +1,30 @@ +// 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. + +using System; +using Cake.Core.Diagnostics; + +namespace Cake.Frosting.Internal +{ + internal static class ErrorHandler + { + public static int OutputError(ICakeLog log, Exception exception) + { + if (log.Verbosity == Verbosity.Diagnostic) + { + log.Error("Error: {0}", exception); + } + else + { + log.Error("Error: {0}", exception.Message); + } + return 1; + } + + public static int GetExitCode(Exception exception) + { + return 1; + } + } +} diff --git a/src/Cake.Frosting/Internal/Extensions/CakeTaskExtensions.cs b/src/Cake.Frosting/Internal/Extensions/CakeTaskExtensions.cs new file mode 100644 index 0000000..b97503d --- /dev/null +++ b/src/Cake.Frosting/Internal/Extensions/CakeTaskExtensions.cs @@ -0,0 +1,37 @@ +// 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. + +using System; +using System.Reflection; +using Cake.Core; + +// ReSharper disable once CheckNamespace +namespace Cake.Frosting.Internal +{ + internal static class CakeTaskExtensions + { + public static bool HasCompatibleContext(this IFrostingTask task, IFrostingContext context) + { + return context.GetType().IsConvertableTo(task.GetContextType()); + } + + public static Type GetContextType(this IFrostingTask task) + { + var baseType = task.GetType().GetTypeInfo().BaseType; + if (baseType.IsConstructedGenericType) + { + if (baseType.GetGenericTypeDefinition() == typeof(FrostingTask<>)) + { + return baseType.GenericTypeArguments[0]; + } + } + return typeof(ICakeContext); + } + + private static bool IsConvertableTo(this Type type, Type other) + { + return other == type || other.IsAssignableFrom(type); + } + } +} diff --git a/src/Cake.Frosting/Internal/Extensions/ContainerExtensions.cs b/src/Cake.Frosting/Internal/Extensions/ContainerExtensions.cs new file mode 100644 index 0000000..3318a79 --- /dev/null +++ b/src/Cake.Frosting/Internal/Extensions/ContainerExtensions.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. + +using Autofac; + +// ReSharper disable once CheckNamespace +namespace Cake.Frosting.Internal.Composition +{ + internal static class ContainerExtensions + { + public static void Update(this IContainer container, ContainerRegistrar registrar) + { + registrar.Builder.Update(container); + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/Guard.cs b/src/Cake.Frosting/Internal/Guard.cs new file mode 100644 index 0000000..3eb4a56 --- /dev/null +++ b/src/Cake.Frosting/Internal/Guard.cs @@ -0,0 +1,19 @@ +// 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. + +using System; + +namespace Cake.Frosting.Internal +{ + internal static class Guard + { + public static void ArgumentNotNull(object value, string name) + { + if (value == null) + { + throw new ArgumentNullException(name); + } + } + } +} diff --git a/src/Cake.Frosting/Internal/RawArguments.cs b/src/Cake.Frosting/Internal/RawArguments.cs new file mode 100644 index 0000000..830deda --- /dev/null +++ b/src/Cake.Frosting/Internal/RawArguments.cs @@ -0,0 +1,55 @@ +// 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. + +using System; +using System.Collections.Generic; +using Cake.Core; + +namespace Cake.Frosting.Internal +{ + internal sealed class RawArguments : ICakeArguments + { + private readonly Dictionary _arguments; + + /// + /// Gets the arguments. + /// + /// The arguments. + public IReadOnlyDictionary Arguments => _arguments; + + /// + /// Initializes a new instance of the class. + /// + /// The options. + public RawArguments(CakeHostOptions options) + { + _arguments = new Dictionary( + (options ?? new CakeHostOptions()).Arguments ?? new Dictionary(), + StringComparer.OrdinalIgnoreCase); + } + + /// + /// Determines whether or not the specified argument exist. + /// + /// The argument name. + /// + /// true if the argument exist; otherwise false. + /// + public bool HasArgument(string name) + { + return _arguments.ContainsKey(name); + } + + /// + /// Gets an argument. + /// + /// The argument name. + /// The argument value. + public string GetArgument(string name) + { + return _arguments.ContainsKey(name) + ? _arguments[name] : null; + } + } +} diff --git a/src/Cake.Frosting/Internal/ReportPrinter.cs b/src/Cake.Frosting/Internal/ReportPrinter.cs new file mode 100644 index 0000000..51e8423 --- /dev/null +++ b/src/Cake.Frosting/Internal/ReportPrinter.cs @@ -0,0 +1,105 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Cake.Core; +using Cake.Core.Diagnostics; + +namespace Cake.Frosting.Internal +{ + internal sealed class ReportPrinter : ICakeReportPrinter + { + private readonly IConsole _console; + private readonly ICakeLog _log; + + public ReportPrinter(IConsole console, ICakeLog log) + { + _console = console; + _log = log; + } + + public void Write(CakeReport report) + { + if (report == null) + { + throw new ArgumentNullException(nameof(report)); + } + + try + { + var maxTaskNameLength = 29; + foreach (var item in report) + { + if (item.TaskName.Length > maxTaskNameLength) + { + maxTaskNameLength = item.TaskName.Length; + } + } + + maxTaskNameLength++; + string lineFormat = "{0,-" + maxTaskNameLength + "}{1,-20}"; + _console.ForegroundColor = ConsoleColor.Green; + + // Write header. + _console.WriteLine(); + _console.WriteLine(lineFormat, "Task", "Duration"); + _console.WriteLine(new string('-', 20 + maxTaskNameLength)); + + // Write task status. + foreach (var item in report) + { + if (ShouldWriteTask(item)) + { + _console.ForegroundColor = GetItemForegroundColor(item); + _console.WriteLine(lineFormat, item.TaskName, FormatDuration(item)); + } + } + + // Write footer. + _console.ForegroundColor = ConsoleColor.Green; + _console.WriteLine(new string('-', 20 + maxTaskNameLength)); + _console.WriteLine(lineFormat, "Total:", FormatTime(GetTotalTime(report))); + } + finally + { + _console.ResetColor(); + } + } + + private bool ShouldWriteTask(CakeReportEntry item) + { + if (item.ExecutionStatus == CakeTaskExecutionStatus.Delegated) + { + return _log.Verbosity >= Verbosity.Verbose; + } + return true; + } + + private static string FormatDuration(CakeReportEntry item) + { + return item.ExecutionStatus == CakeTaskExecutionStatus.Skipped + ? "Skipped" : FormatTime(item.Duration); + } + + private static ConsoleColor GetItemForegroundColor(CakeReportEntry item) + { + return item.ExecutionStatus == CakeTaskExecutionStatus.Executed + ? ConsoleColor.Green : ConsoleColor.Gray; + } + + private static string FormatTime(TimeSpan time) + { + return time.ToString("c", CultureInfo.InvariantCulture); + } + + private static TimeSpan GetTotalTime(IEnumerable entries) + { + return entries.Select(i => i.Duration) + .Aggregate(TimeSpan.Zero, (t1, t2) => t1 + t2); + } + } +} \ No newline at end of file diff --git a/src/Cake.Frosting/Internal/TaskFinder.cs b/src/Cake.Frosting/Internal/TaskFinder.cs new file mode 100644 index 0000000..80ada17 --- /dev/null +++ b/src/Cake.Frosting/Internal/TaskFinder.cs @@ -0,0 +1,30 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Cake.Frosting.Internal +{ + internal sealed class TaskFinder : ICakeTaskFinder + { + public Type[] GetTasks(IEnumerable assemblies) + { + var result = new List(); + foreach (var assembly in assemblies) + { + foreach (var type in assembly.GetExportedTypes()) + { + var info = type.GetTypeInfo(); + if (typeof(IFrostingTask).IsAssignableFrom(type) && info.IsClass && !info.IsAbstract) + { + result.Add(type); + } + } + } + return result.ToArray(); + } + } +} diff --git a/src/Cake.Frosting/Internal/TaskNameHelper.cs b/src/Cake.Frosting/Internal/TaskNameHelper.cs new file mode 100644 index 0000000..e95a8ef --- /dev/null +++ b/src/Cake.Frosting/Internal/TaskNameHelper.cs @@ -0,0 +1,28 @@ +// 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. + +using System; +using System.Reflection; + +namespace Cake.Frosting.Internal +{ + internal static class TaskNameHelper + { + public static string GetTaskName(IFrostingTask task) + { + return GetTaskName(task.GetType()); + } + + public static string GetTaskName(DependencyAttribute attribute) + { + return GetTaskName(attribute.Task); + } + + public static string GetTaskName(Type task) + { + var attribute = task.GetTypeInfo().GetCustomAttribute(); + return attribute != null ? attribute.Name : task.Name; + } + } +} diff --git a/src/Cake.Frosting/Properties/AssemblyInfo.cs b/src/Cake.Frosting/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..27c524f --- /dev/null +++ b/src/Cake.Frosting/Properties/AssemblyInfo.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. + +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Cake.Frosting")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a608fd39-2b71-4b6e-b238-ae80bb2794f8")] \ No newline at end of file diff --git a/src/Cake.Frosting/project.json b/src/Cake.Frosting/project.json new file mode 100644 index 0000000..73c411e --- /dev/null +++ b/src/Cake.Frosting/project.json @@ -0,0 +1,52 @@ +{ + "version": "0.1.0-*", + "description": "The .NET Core host for Cake.", + "copyright": "Copyright (c) .NET Foundation and contributors", + "authors": [ + "Patrik Svensson, Mattias Karlsson, Gary Ewan Park and contributors" + ], + "packOptions": { + "summary": "This package makes it possible to write your build scripts as a portable console application.", + "licenseUrl": "https://github.com/cake-build/frosting/blob/develop/LICENSE", + "iconUrl": "https://raw.githubusercontent.com/cake-build/graphics/master/png/cake-medium.png", + "requireLicenseAcceptance": false, + "tags": [ + "Cake", + "Build", + "Build automation" + ], + "repository": { + "type": "git", + "url": "https://github.com/cake-build/frosting" + } + }, + "dependencies": { + "NETStandard.Library": "1.6.0", + "Cake.Core": "0.16.0-alpha0046", + "Cake.Common": "0.16.0-alpha0046", + "Autofac": "4.0.0", + "StyleCop.Analyzers": { + "version": "1.0.0", + "type": "build" + } + }, + "buildOptions": { + "xmlDoc": true, + "additionalArguments": [ + "/ruleset:../Frosting.ruleset", + "/additionalfile:../stylecop.json" + ] + }, + "configurations": { + "Release": { + "buildOptions": { + "warningsAsErrors": true + } + } + }, + "frameworks": { + "netstandard1.6": { + "imports": "dnxcore50" + } + } +} \ No newline at end of file diff --git a/src/Frosting.ruleset b/src/Frosting.ruleset new file mode 100644 index 0000000..b00d5d0 --- /dev/null +++ b/src/Frosting.ruleset @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NuGet.config b/src/NuGet.config new file mode 100644 index 0000000..09f2371 --- /dev/null +++ b/src/NuGet.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Sandbox/Program.cs b/src/Sandbox/Program.cs new file mode 100644 index 0000000..12822e9 --- /dev/null +++ b/src/Sandbox/Program.cs @@ -0,0 +1,32 @@ +using Cake.Frosting; + +namespace Sandbox +{ + public class Program + { + public static int Main(string[] args) + { + // Create the host. + var host = new CakeHostBuilder() + .UseStartup() + .WithArguments(args) + .ConfigureServices(services => + { + // You could also configure services like this. + services.UseContext(); + }) + .Build(); + + // Run the host. + return host.Run(); + } + } + + public class Startup : IFrostingStartup + { + public void Configure(ICakeServices services) + { + services.UseContext(); + } + } +} \ No newline at end of file diff --git a/src/Sandbox/Properties/AssemblyInfo.cs b/src/Sandbox/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..14df62a --- /dev/null +++ b/src/Sandbox/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Sandbox")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ed379398-da69-4d1e-b712-27dc0d67abc3")] diff --git a/src/Sandbox/Sandbox.xproj b/src/Sandbox/Sandbox.xproj new file mode 100644 index 0000000..6e1c27c --- /dev/null +++ b/src/Sandbox/Sandbox.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + ed379398-da69-4d1e-b712-27dc0d67abc3 + Sandbox + .\obj + .\bin\ + v4.6.2 + + + 2.0 + + + \ No newline at end of file diff --git a/src/Sandbox/Settings.cs b/src/Sandbox/Settings.cs new file mode 100644 index 0000000..0e4e399 --- /dev/null +++ b/src/Sandbox/Settings.cs @@ -0,0 +1,18 @@ +using Cake.Core; +using Cake.Frosting; + +namespace Sandbox +{ + public class Settings : FrostingContext + { + public bool Magic { get; set; } + + public Settings(ICakeContext context) + : base(context) + { + // You could also use a CakeLifeTime + // to provide a Setup method to setup the context. + Magic = context.Arguments.HasArgument("magic"); + } + } +} \ No newline at end of file diff --git a/src/Sandbox/Tasks/BuildTask.cs b/src/Sandbox/Tasks/BuildTask.cs new file mode 100644 index 0000000..81765f1 --- /dev/null +++ b/src/Sandbox/Tasks/BuildTask.cs @@ -0,0 +1,21 @@ +using Cake.Core; +using Cake.Core.Diagnostics; +using Cake.Frosting; + +namespace Sandbox.Tasks +{ + [TaskName("Build")] + public sealed class BuildTask : FrostingTask + { + public override bool ShouldRun(Settings context) + { + // Don't run this task on OSX. + return context.Environment.Platform.Family != PlatformFamily.OSX; + } + + public override void Run(Settings context) + { + context.Log.Information("Magic: {0}", context.Magic); + } + } +} \ No newline at end of file diff --git a/src/Sandbox/Tasks/DefaultTask.cs b/src/Sandbox/Tasks/DefaultTask.cs new file mode 100644 index 0000000..c20565d --- /dev/null +++ b/src/Sandbox/Tasks/DefaultTask.cs @@ -0,0 +1,13 @@ +using Cake.Core; +using Cake.Frosting; + +namespace Sandbox.Tasks +{ + [TaskName("Default")] + [Dependency(typeof(BuildTask))] + public class DefaultTask : FrostingTask + { + // If you don't inherit from the generic task + // the standard ICakeContext will be provided. + } +} \ No newline at end of file diff --git a/src/Sandbox/project.json b/src/Sandbox/project.json new file mode 100644 index 0000000..600141b --- /dev/null +++ b/src/Sandbox/project.json @@ -0,0 +1,20 @@ +{ + "version": "0.1.0-*", + "buildOptions": { + "emitEntryPoint": true + }, + "dependencies": { + "Cake.Frosting": { + "target": "project" + }, + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0" + } + }, + "frameworks": { + "netcoreapp1.0": { + "imports": "dnxcore50" + } + } +} \ No newline at end of file diff --git a/src/stylecop.json b/src/stylecop.json new file mode 100644 index 0000000..c9e6c23 --- /dev/null +++ b/src/stylecop.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "indentation": { + "useTabs": false, + "indentationSize": 4 + }, + "documentationRules": { + "documentExposedElements": true, + "documentInternalElements": false, + "documentPrivateElements": false, + "documentInterfaces": true, + "documentPrivateFields": false, + "xmlHeader": false, + "headerDecoration": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n// See the LICENSE file in the project root for more information." + }, + "layoutRules": { + "newlineAtEndOfFile": "allow" + }, + "orderingRules": { + "usingDirectivesPlacement": "outsideNamespace", + "systemUsingDirectivesFirst": true, + "elementOrder": [ + "kind", + "accessibility", + "constant", + "static", + "readonly" + ] + } + } +} \ No newline at end of file