From 9f40d46d4287226e6f984db7c586d6fd007cf78c Mon Sep 17 00:00:00 2001 From: Brad Wilson Date: Tue, 27 Aug 2019 11:45:05 -0700 Subject: [PATCH] New build system --- .vscode/launch.json | 4 + .vscode/settings.json | 17 ++ .vscode/tasks.json | 89 +++++++++ appveyor.yml | 3 +- build | 28 +++ build.ps1 | 154 ++------------- ...ShouldNotBeCalledOnValueTypesFixerTests.cs | 7 +- .../CodeAnalyzerHelper.cs | 51 +++-- .../xunit.analyzers.tests.csproj | 6 +- tools/builder/.vscode/launch.json | 4 + tools/builder/.vscode/settings.json | 12 ++ tools/builder/.vscode/tasks.json | 84 ++++++++ tools/builder/Program.cs | 8 + tools/builder/build.csproj | 16 ++ tools/builder/models/BuildContext.cs | 182 ++++++++++++++++++ tools/builder/models/BuildTarget.cs | 14 ++ tools/builder/models/Configuration.cs | 5 + tools/builder/models/NullConsole.cs | 15 ++ tools/builder/models/TargetAttribute.cs | 15 ++ tools/builder/targets/Build.cs | 13 ++ tools/builder/targets/CI.cs | 3 + tools/builder/targets/DownloadNuGet.cs | 26 +++ tools/builder/targets/Packages.cs | 20 ++ tools/builder/targets/PushMyGet.cs | 32 +++ tools/builder/targets/Restore.cs | 14 ++ tools/builder/targets/SetVersion.cs | 49 +++++ tools/builder/targets/SignPackages.cs | 45 +++++ tools/builder/targets/Test.cs | 3 + tools/builder/targets/TestCore.cs | 17 ++ tools/builder/targets/TestFx.cs | 17 ++ 30 files changed, 795 insertions(+), 158 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100755 build mode change 100644 => 100755 build.ps1 create mode 100644 tools/builder/.vscode/launch.json create mode 100644 tools/builder/.vscode/settings.json create mode 100644 tools/builder/.vscode/tasks.json create mode 100644 tools/builder/Program.cs create mode 100644 tools/builder/build.csproj create mode 100644 tools/builder/models/BuildContext.cs create mode 100644 tools/builder/models/BuildTarget.cs create mode 100644 tools/builder/models/Configuration.cs create mode 100644 tools/builder/models/NullConsole.cs create mode 100644 tools/builder/models/TargetAttribute.cs create mode 100644 tools/builder/targets/Build.cs create mode 100644 tools/builder/targets/CI.cs create mode 100644 tools/builder/targets/DownloadNuGet.cs create mode 100644 tools/builder/targets/Packages.cs create mode 100644 tools/builder/targets/PushMyGet.cs create mode 100644 tools/builder/targets/Restore.cs create mode 100644 tools/builder/targets/SetVersion.cs create mode 100644 tools/builder/targets/SignPackages.cs create mode 100644 tools/builder/targets/Test.cs create mode 100644 tools/builder/targets/TestCore.cs create mode 100644 tools/builder/targets/TestFx.cs diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..cd4d5eb2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,4 @@ +{ + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..5b4ba1ae --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "cSpell.words": [ + "app", + "nuget", + "nupkg", + "parallelization", + "veyor" + ], + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bin": true, + "**/obj": true, + "artifacts": true, + "packages": true + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..21d317f1 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,89 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--project", + "tools/builder", + "--no-launch-profile", + "--", + "Build" + ], + "options": { + "cwd": "${workspaceRoot}" + }, + "group": "build", + "presentation": { + "focus": true + }, + "problemMatcher": [] + }, + { + "label": "Pre-PR Validation", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--project", + "tools/builder", + "--no-launch-profile", + "--", + "Packages" + ], + "options": { + "cwd": "${workspaceRoot}" + }, + "group": "build", + "presentation": { + "focus": true + }, + "problemMatcher": [] + }, + { + "label": "Unit Tests (.NET Core)", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--project", + "tools/builder", + "--no-launch-profile", + "--", + "TestCore" + ], + "options": { + "cwd": "${workspaceRoot}" + }, + "group": "build", + "presentation": { + "focus": true + }, + "problemMatcher": [] + }, + { + "label": "Unit Tests (.NET Framework)", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--project", + "tools/builder", + "--no-launch-profile", + "--", + "TestFx" + ], + "options": { + "cwd": "${workspaceRoot}" + }, + "group": "build", + "presentation": { + "focus": true + }, + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 23c767dc..39fd07ed 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -40,7 +40,7 @@ nuget: disable_publish_on_pr: true build_script: -- ps: .\build.ps1 -target appveyor -buildAssemblyVersion ($env:BuildVersion + $env:APPVEYOR_BUILD_NUMBER) -buildSemanticVersion ($env:BuildSemanticVersion + $env:APPVEYOR_BUILD_NUMBER) +- ps: .\build.ps1 ci --buildAssemblyVersion ($env:BuildVersion + $env:APPVEYOR_BUILD_NUMBER) --buildSemanticVersion ($env:BuildSemanticVersion + $env:APPVEYOR_BUILD_NUMBER) test: off @@ -49,7 +49,6 @@ deploy: off artifacts: - path: artifacts/packages - path: artifacts/test -- path: artifacts/build notifications: - provider: Slack diff --git a/build b/build new file mode 100755 index 00000000..4be6ce1a --- /dev/null +++ b/build @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +PUSHED=0 + +cleanup () { + if [[ $PUSHED == 1 ]]; then + popd >/dev/null + PUSHED=0 + fi +} + +trap cleanup EXIT ERR INT TERM + +if which dotnet > /dev/null; then + if which mono > /dev/null; then + pushd $( cd "$(dirname "$0")" ; pwd -P ) >/dev/null + PUSHED=1 + + dotnet run --project tools/builder --no-launch-profile -- "$@" + else + echo "error(1): Could not find 'mono'; please install Mono" 2>&1 + exit 1 + fi +else + echo "error(1): Could not find 'dotnet'; please install the .NET Core SDK" 2>&1 + exit 1 +fi diff --git a/build.ps1 b/build.ps1 old mode 100644 new mode 100755 index eb94c854..51092af1 --- a/build.ps1 +++ b/build.ps1 @@ -1,150 +1,18 @@ -param( - [string] $target = "test", - [string] $configuration = "Release", - [string] $buildAssemblyVersion = "", - [string] $buildSemanticVersion = "" -) +#!/usr/bin/env pwsh +#Requires -Version 5.1 -if ($PSScriptRoot -eq $null) { - fatal "This build script requires PowerShell 3 or later." -} - -$buildModuleFile = join-path $PSScriptRoot "build\tools\xunit-build-module.psm1" - -if ((test-path $buildModuleFile) -eq $false) { - write-host "Could not find build module. Did you forget to 'git submodule update --init'?" -ForegroundColor Red - exit -1 -} - -Set-StrictMode -Version 2 -Import-Module $buildModuleFile -Scope Local -Force -ArgumentList "4.1.0" -Set-Location $PSScriptRoot - -$packageOutputFolder = (join-path (Get-Location) "artifacts\packages") -$parallelFlags = "-parallel all -maxthreads 16" -$testOutputFolder = (join-path (Get-Location) "artifacts\test") -$solutionFolder = Get-Location - -$signClientVersion = "0.9.1" -$signClientFolder = (join-path (Get-Location) "packages\SignClient.$signClientVersion") -$signClientAppSettings = (join-path (Get-Location) "tools\SignClient\appsettings.json") - -# Helper functions - -function _xunit_console([string] $command) { - _exec ('& "' + $PSScriptRoot + '\packages\xunit.runner.console\tools\net472\xunit.console.x86.exe" ' + $command) -} +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" -# Top-level targets - -function __target_appveyor() { - __target_ci - __target__signpackages - __target__pushmyget -} - -function __target_build() { - __target_packagerestore - - _build_step "Compiling binaries" - _msbuild "xunit.analyzers.sln" $configuration - - _dotnet ("tools\DocBuilder\bin\Release\netcoreapp2.1\Xunit.Analyzers.DocBuilder.dll " + (Join-Path $PSScriptRoot "docs")) # "Verifying documentation files" +if ($null -eq (Get-Command "dotnet" -ErrorAction Ignore)) { + throw "Could not find 'dotnet'; please install the .NET Core SDK" } -function __target_ci() { - __target__setversion - __target_test - __target__packages -} - -function __target_packagerestore() { - _download_nuget +Push-Location (Split-Path $MyInvocation.MyCommand.Definition) - _build_step "Restoring NuGet packages" - _mkdir packages - _exec ('& "' + $nugetExe + '" restore xunit.analyzers.sln -NonInteractive') - _exec ('& "' + $nugetExe + '" install xunit.runner.console -OutputDirectory "' + (Join-Path $PSScriptRoot "packages") + '" -NonInteractive -ExcludeVersion') +try { + & dotnet run --project tools/builder --no-launch-profile -- $args } - -function __target_packages() { - __target_build - __target__packages -} - -function __target_test() { - __target_build - __target__test +finally { + Pop-Location } - -# Dependent targets - -function __target__packages() { - _download_nuget - - _build_step "Creating NuGet packages" - Get-ChildItem -Recurse -Filter *.nuspec | _nuget_pack -outputFolder $packageOutputFolder -configuration $configuration -} - -function __target__pushmyget() { - _build_step "Pushing packages to MyGet" - if ($env:MyGetApiKey -eq $null) { - Write-Host -ForegroundColor Yellow "Skipping MyGet push because environment variable 'MyGetApiKey' is not set." - Write-Host "" - } else { - Get-ChildItem -Filter *.nupkg $packageOutputFolder | _nuget_push -source https://www.myget.org/F/xunit/api/v2/package -apiKey $env:MyGetApiKey - } -} - -function __target__setversion() { - if ($buildAssemblyVersion -ne "") { - _build_step ("Setting assembly version: '" + $buildAssemblyVersion + "'") - Get-ChildItem -Recurse -Filter AssemblyInfo.cs | _replace -match '\("99\.99\.99\.0"\)' -replacement ('("' + $buildAssemblyVersion + '")') - } - - if ($buildSemanticVersion -ne "") { - _build_step ("Setting semantic version: '" + $buildSemanticVersion + "'") - Get-ChildItem -Recurse -Filter AssemblyInfo.cs | _replace -match '\("99\.99\.99-dev"\)' -replacement ('("' + $buildSemanticVersion + '")') - Get-ChildItem -Recurse -Filter *.nuspec | _replace -match '99\.99\.99-dev' -replacement $buildSemanticVersion - } -} - -function __target__signpackages() { - if ($env:SignClientSecret -ne $null) { - if ((test-path $signClientFolder) -eq $false) { - _build_step ("Downloading SignClient " + $signClientVersion) - _exec ('& "' + $nugetExe + '" install SignClient -version ' + $signClientVersion + ' -SolutionDir "' + $solutionFolder + '" -Verbosity quiet -NonInteractive') - } - - _build_step "Signing NuGet packages" - $appPath = (join-path $signClientFolder "tools\netcoreapp2.0\SignClient.dll") - $nupgks = Get-ChildItem (join-path $packageOutputFolder "*.nupkg") | ForEach-Object { $_.FullName } - foreach ($nupkg in $nupgks) { - $cmd = '& dotnet "' + $appPath + '" sign -c "' + $signClientAppSettings + '" -r "' + $env:SignClientUser + '" -s "' + $env:SignClientSecret + '" -n "xUnit.net" -d "xUnit.net" -u "https://github.com/xunit/xunit.analyzers" -i "' + $nupkg + '"' - $msg = $cmd.Replace($env:SignClientSecret, '[Redacted]') - $msg = $msg.Replace($env:SignClientUser, '[Redacted]') - _exec $cmd $msg - } - } -} - -function __target__test() { - _build_step "Running unit tests" - _xunit_console ("test\xunit.analyzers.tests\bin\" + $configuration + "\net472\xunit.analyzers.tests.dll -xml artifacts\test\TestResults.xml -diagnostics") -} - -# Dispatch - -$targetFunction = (Get-ChildItem ("Function:__target_" + $target.ToLowerInvariant()) -ErrorAction SilentlyContinue) -if ($targetFunction -eq $null) { - _fatal "Unknown target '$target'" -} - -_build_step "Performing pre-build verifications" - _require dotnet "Could not find 'dotnet'. Please ensure .NET CLI Tooling is installed." - _require msbuild "Could not find 'msbuild'. Please ensure MSBUILD.EXE v15 is on the path." - _verify_msbuild15 - -_mkdir $packageOutputFolder -_mkdir $testOutputFolder -& $targetFunction diff --git a/test/xunit.analyzers.tests/AssertNullShouldNotBeCalledOnValueTypesFixerTests.cs b/test/xunit.analyzers.tests/AssertNullShouldNotBeCalledOnValueTypesFixerTests.cs index 414c5280..934ed67b 100644 --- a/test/xunit.analyzers.tests/AssertNullShouldNotBeCalledOnValueTypesFixerTests.cs +++ b/test/xunit.analyzers.tests/AssertNullShouldNotBeCalledOnValueTypesFixerTests.cs @@ -1,4 +1,5 @@ -using Microsoft.CodeAnalysis.CodeFixes; +using System; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; namespace Xunit.Analyzers @@ -86,6 +87,10 @@ public void Test1() }"; var actual = await CodeAnalyzerHelper.GetFixedCodeAsync(analyzer, fixer, original); + // Code fixer always inserts \r\n even on Linux, so fix up the actual result + if (Environment.NewLine != "\r\n") + actual = actual.Replace("\r\n", Environment.NewLine); + Assert.Equal(expected, actual); } } diff --git a/test/xunit.analyzers.tests/CodeAnalyzerHelper.cs b/test/xunit.analyzers.tests/CodeAnalyzerHelper.cs index 9ff7f9b0..262d561f 100644 --- a/test/xunit.analyzers.tests/CodeAnalyzerHelper.cs +++ b/test/xunit.analyzers.tests/CodeAnalyzerHelper.cs @@ -81,16 +81,20 @@ enum XunitReferences class CodeAnalyzerHelper { - static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location); - static readonly MetadataReference SystemCollectionsImmutable = MetadataReference.CreateFromFile(typeof(ImmutableArray).GetTypeInfo().Assembly.Location); - static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location); - static readonly MetadataReference SystemTextReference = MetadataReference.CreateFromFile(typeof(System.Text.RegularExpressions.Regex).GetTypeInfo().Assembly.Location); + static readonly MetadataReference CorlibReference = GetAssemblyReference(typeof(object)); + static readonly MetadataReference NetStandardReference = GetAssemblyReference("netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51"); + static readonly MetadataReference SystemCollectionsImmutableReference = GetAssemblyReference(typeof(ImmutableArray)); + static readonly MetadataReference SystemCollectionsReference = GetAssemblyReference("System.Collections"); + static readonly MetadataReference SystemConsoleReference = GetAssemblyReference("System.Console"); + static readonly MetadataReference SystemCoreReference = GetAssemblyReference(typeof(Enumerable)); + static readonly MetadataReference SystemTextReference = GetAssemblyReference(typeof(System.Text.RegularExpressions.Regex)); + static readonly MetadataReference SystemRuntimeExtensionsReference = GetAssemblyReference("System.Runtime.Extensions"); static readonly MetadataReference SystemRuntimeReference; static readonly MetadataReference SystemThreadingTasksReference; - static readonly MetadataReference XunitAbstractionsReference = MetadataReference.CreateFromFile(typeof(ITest).GetTypeInfo().Assembly.Location); - static readonly MetadataReference XunitAssertReference = MetadataReference.CreateFromFile(typeof(Assert).GetTypeInfo().Assembly.Location); - static readonly MetadataReference XunitCoreReference = MetadataReference.CreateFromFile(typeof(FactAttribute).GetTypeInfo().Assembly.Location); - static readonly MetadataReference XunitExecutionReference = MetadataReference.CreateFromFile(typeof(XunitTestCase).GetTypeInfo().Assembly.Location); + static readonly MetadataReference XunitAbstractionsReference = GetAssemblyReference(typeof(ITest)); + static readonly MetadataReference XunitAssertReference = GetAssemblyReference(typeof(Assert)); + static readonly MetadataReference XunitCoreReference = GetAssemblyReference(typeof(FactAttribute)); + static readonly MetadataReference XunitExecutionReference = GetAssemblyReference(typeof(XunitTestCase)); static readonly IEnumerable SystemReferences; @@ -106,9 +110,21 @@ static CodeAnalyzerHelper() // Xunit is a PCL linked against System.Runtime, however on the Desktop framework all types in that assembly have been forwarded to // System.Core, so we need to find the assembly by name to compile without errors. var referencedAssemblies = typeof(FactAttribute).Assembly.GetReferencedAssemblies(); + SystemRuntimeReference = GetAssemblyReference(referencedAssemblies, "System.Runtime"); SystemThreadingTasksReference = GetAssemblyReference(referencedAssemblies, "System.Threading.Tasks"); - SystemReferences = new[] { CorlibReference, SystemCollectionsImmutable, SystemCoreReference, SystemTextReference, SystemRuntimeReference, SystemThreadingTasksReference }; + SystemReferences = new[] { + CorlibReference, + NetStandardReference, + SystemCollectionsImmutableReference, + SystemCollectionsReference, + SystemConsoleReference, + SystemCoreReference, + SystemRuntimeReference, + SystemRuntimeExtensionsReference, + SystemTextReference, + SystemThreadingTasksReference, + }.Where(x => x != null).ToArray(); } static async Task> ApplyAnalyzers(Compilation compilation, params DiagnosticAnalyzer[] analyzers) @@ -125,11 +141,24 @@ static async Task> ApplyAnalyzers(Compilation compila return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); } - static MetadataReference GetAssemblyReference(IEnumerable assemblies, string name) + static MetadataReference GetAssemblyReference(Type type) + => MetadataReference.CreateFromFile(type.GetTypeInfo().Assembly.Location); + + static MetadataReference GetAssemblyReference(string name) { - return MetadataReference.CreateFromFile(Assembly.Load(assemblies.First(n => n.Name == name)).Location); + try + { + return MetadataReference.CreateFromFile(Assembly.Load(name).Location); + } + catch + { + return null; + } } + static MetadataReference GetAssemblyReference(IEnumerable assemblies, string name) + => MetadataReference.CreateFromFile(Assembly.Load(assemblies.First(n => n.Name == name)).Location); + static async Task<(Compilation, Document, Workspace)> GetCompilationAsync(CompilationReporting compilationReporting, XunitReferences references, string source, params string[] additionalSources) { const string fileNamePrefix = "Source"; diff --git a/test/xunit.analyzers.tests/xunit.analyzers.tests.csproj b/test/xunit.analyzers.tests/xunit.analyzers.tests.csproj index 507fd0b6..c5792f34 100644 --- a/test/xunit.analyzers.tests/xunit.analyzers.tests.csproj +++ b/test/xunit.analyzers.tests/xunit.analyzers.tests.csproj @@ -2,7 +2,7 @@ Xunit.Analyzers - net472 + netcoreapp2.2;net472 @@ -13,6 +13,10 @@ + + + + diff --git a/tools/builder/.vscode/launch.json b/tools/builder/.vscode/launch.json new file mode 100644 index 00000000..cd4d5eb2 --- /dev/null +++ b/tools/builder/.vscode/launch.json @@ -0,0 +1,4 @@ +{ + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/tools/builder/.vscode/settings.json b/tools/builder/.vscode/settings.json new file mode 100644 index 00000000..7eeed556 --- /dev/null +++ b/tools/builder/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "cSpell.words": [ + "msbuild", + "nuget" + ], + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bin": true, + "**/obj": true + } +} \ No newline at end of file diff --git a/tools/builder/.vscode/tasks.json b/tools/builder/.vscode/tasks.json new file mode 100644 index 00000000..7daca5a7 --- /dev/null +++ b/tools/builder/.vscode/tasks.json @@ -0,0 +1,84 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build (builder)", + "type": "process", + "command": "dotnet", + "args": [ + "build" + ], + "options": { + "cwd": "${workspaceRoot}" + }, + "group": "build", + "presentation": { + "focus": true + }, + "problemMatcher": [] + }, + { + "label": "Build (analyzers)", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--project", + "${workspaceRoot}", + "--no-launch-profile", + "--", + "Build" + ], + "options": { + "cwd": "${workspaceRoot}" + }, + "group": "build", + "presentation": { + "focus": true + }, + "problemMatcher": [] + }, + { + "label": "Unit test (analyzers, .NET Core)", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--project", + "${workspaceRoot}", + "--no-launch-profile", + "--", + "TestCore" + ], + "options": { + "cwd": "${workspaceRoot}" + }, + "group": "build", + "presentation": { + "focus": true + }, + "problemMatcher": [] + }, + { + "label": "Unit test (analyzers, .NET Framework)", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--project", + "${workspaceRoot}", + "--no-launch-profile", + "--", + "TestFx" + ], + "options": { + "cwd": "${workspaceRoot}" + }, + "group": "build", + "presentation": { + "focus": true + }, + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/tools/builder/Program.cs b/tools/builder/Program.cs new file mode 100644 index 00000000..9cb064a1 --- /dev/null +++ b/tools/builder/Program.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; +using McMaster.Extensions.CommandLineUtils; + +public class Program +{ + public static Task Main(string[] args) + => CommandLineApplication.ExecuteAsync(args); +} diff --git a/tools/builder/build.csproj b/tools/builder/build.csproj new file mode 100644 index 00000000..8d3b69ff --- /dev/null +++ b/tools/builder/build.csproj @@ -0,0 +1,16 @@ + + + + latest + Exe + netcoreapp2.2 + + + + + + + + + + diff --git a/tools/builder/models/BuildContext.cs b/tools/builder/models/BuildContext.cs new file mode 100644 index 00000000..2a2c6be4 --- /dev/null +++ b/tools/builder/models/BuildContext.cs @@ -0,0 +1,182 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Bullseye.Internal; +using McMaster.Extensions.CommandLineUtils; +using SimpleExec; + +[Command(Name = "build", Description = "Build utility for xUnit.net analyzers")] +[HelpOption("-?|-h|--help")] +public class BuildContext +{ + // Versions of downloaded dependent software + + public string NuGetVersion => "5.0.2"; + + public string SignClientVersion => "0.9.1"; + + // Calculated properties + + public string BaseFolder { get; private set; } + + public string ConfigurationText => Configuration.ToString(); + + public bool NeedMono { get; private set; } + + public string NuGetExe { get; private set; } + + public string NuGetUrl { get; private set; } + + public string PackageOutputFolder { get; private set; } + + public string TestOutputFolder { get; private set; } + + // User-controllable command-line options + + [Option("--buildAssemblyVersion", Description = "Set the build assembly version (default: '99.99.99.0')")] + public string BuildAssemblyVersion { get; } + + [Option("--buildSemanticVersion", Description = "Set the build semantic version (default: '99.99.99-dev')")] + public string BuildSemanticVersion { get; } + + [Option("-c|--configuration", Description = "The target configuration (values: 'Debug', 'Release'; default: 'Release')")] + public Configuration Configuration { get; } = Configuration.Release; + + [Option("-N|--no-color", Description = "Disable colored output")] + public bool NoColor { get; } + + [Option("-s|--skip-dependencies", Description = "Do not run targets' dependencies")] + public bool SkipDependencies { get; } + + [Argument(0, "targets", Description = "The target(s) to run (common values: 'Build', 'Restore', 'Test', 'TestCore', 'TestFx'; default: 'Test')")] + public BuildTarget[] Targets { get; } = new[] { BuildTarget.Test }; + + [Option("-v|--verbose", Description = "Enable verbose output")] + public bool Verbose { get; } + + // Helper methods for build target consumption + + public void BuildStep(string message) + { + WriteLineColor(ConsoleColor.White, $"==> {message} <=="); + Console.WriteLine(); + } + + public async Task Exec(string name, string args, string redactedArgs = null, string workingDirectory = null) + { + if (redactedArgs == null) + redactedArgs = args; + + if (NeedMono && name.EndsWith(".exe")) + { + args = $"{name} {args}"; + redactedArgs = $"{name} {redactedArgs}"; + name = "mono"; + } + + WriteLineColor(ConsoleColor.DarkGray, $"EXEC: {name} {redactedArgs}{Environment.NewLine}"); + + await Command.RunAsync(name, args, workingDirectory ?? BaseFolder, /*noEcho*/ true); + + Console.WriteLine(); + } + + async Task OnExecuteAsync() + { + Exception error = default; + + try + { + NeedMono = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + // Find the folder with the solution file + BaseFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + while (true) + { + if (Directory.GetFiles(BaseFolder, "*.sln").Count() != 0) + break; + + BaseFolder = Path.GetDirectoryName(BaseFolder); + if (BaseFolder == null) + throw new InvalidOperationException("Could not locate a solution file in the directory hierarchy"); + } + + // Dependent folders + PackageOutputFolder = Path.Combine(BaseFolder, "artifacts", "packages"); + Directory.CreateDirectory(PackageOutputFolder); + + TestOutputFolder = Path.Combine(BaseFolder, "artifacts", "test"); + Directory.CreateDirectory(TestOutputFolder); + + var homeFolder = NeedMono + ? Environment.GetEnvironmentVariable("HOME") + : Environment.GetEnvironmentVariable("USERPROFILE"); + + var nuGetCliFolder = Path.Combine(homeFolder, ".nuget", "cli", NuGetVersion); + Directory.CreateDirectory(nuGetCliFolder); + + NuGetExe = Path.Combine(nuGetCliFolder, "nuget.exe"); + NuGetUrl = $"https://dist.nuget.org/win-x86-commandline/v{NuGetVersion}/nuget.exe"; + + // Parse the targets and Bullseye-specific arguments + var bullseyeArguments = Targets.Select(x => x.ToString()); + if (SkipDependencies) + bullseyeArguments = bullseyeArguments.Append("--skip-dependencies"); + + // Find target classes + var targetCollection = new TargetCollection(); + + foreach (var target in Assembly.GetExecutingAssembly() + .ExportedTypes + .Select(x => new { type = x, attr = x.GetCustomAttribute() }) + .Where(x => x.attr != null)) + { + var method = target.type.GetRuntimeMethod("OnExecute", new[] { typeof(BuildContext) }); + + if (method == null) + targetCollection.Add(new Target(target.attr.TargetName, target.attr.DependentTargets)); + else + targetCollection.Add(new ActionTarget(target.attr.TargetName, target.attr.DependentTargets, () => (Task)method.Invoke(null, new[] { this }))); + } + + // Let Bullseye run the target(s) + await targetCollection.RunAsync(bullseyeArguments, new NullConsole()); + return 0; + } + catch (Exception ex) + { + error = ex; + while (error is TargetInvocationException || error is TargetFailedException) + error = error.InnerException; + } + + Console.WriteLine(); + + if (error is NonZeroExitCodeException nonZeroExit) + { + WriteLineColor(ConsoleColor.Red, "==> Build failed! <=="); + return nonZeroExit.ExitCode; + } + + WriteLineColor(ConsoleColor.Red, $"==> Build failed! An unhandled exception was thrown <=="); + Console.WriteLine(error.ToString()); + return -1; + } + + public void WriteColor(ConsoleColor foregroundColor, string text) + { + if (!NoColor) + Console.ForegroundColor = foregroundColor; + + Console.Write(text); + + if (!NoColor) + Console.ResetColor(); + } + + public void WriteLineColor(ConsoleColor foregroundColor, string text) + => WriteColor(foregroundColor, $"{text}{Environment.NewLine}"); +} diff --git a/tools/builder/models/BuildTarget.cs b/tools/builder/models/BuildTarget.cs new file mode 100644 index 00000000..fdca121a --- /dev/null +++ b/tools/builder/models/BuildTarget.cs @@ -0,0 +1,14 @@ +public enum BuildTarget +{ + Build, + CI, + DownloadNuGet, + Packages, + PushMyGet, + Restore, + SetVersion, + SignPackages, + Test, + TestCore, + TestFx, +} diff --git a/tools/builder/models/Configuration.cs b/tools/builder/models/Configuration.cs new file mode 100644 index 00000000..fa233286 --- /dev/null +++ b/tools/builder/models/Configuration.cs @@ -0,0 +1,5 @@ +public enum Configuration +{ + Debug, + Release, +} \ No newline at end of file diff --git a/tools/builder/models/NullConsole.cs b/tools/builder/models/NullConsole.cs new file mode 100644 index 00000000..6becd076 --- /dev/null +++ b/tools/builder/models/NullConsole.cs @@ -0,0 +1,15 @@ +using System.IO; +using System.Text; +using Bullseye.Internal; + +class NullConsole : IConsole +{ + public TextWriter Out { get; } = new NullTextWriter(); + + public void Clear() {} + + class NullTextWriter : TextWriter + { + public override Encoding Encoding => Encoding.UTF8; + } +} \ No newline at end of file diff --git a/tools/builder/models/TargetAttribute.cs b/tools/builder/models/TargetAttribute.cs new file mode 100644 index 00000000..1817f766 --- /dev/null +++ b/tools/builder/models/TargetAttribute.cs @@ -0,0 +1,15 @@ +using System; + +[AttributeUsage(AttributeTargets.Class)] +public class TargetAttribute : Attribute +{ + public TargetAttribute(string targetName, params string[] dependentTargets) + { + TargetName = targetName; + DependentTargets = dependentTargets ?? Array.Empty(); + } + + public string TargetName { get; } + + public string[] DependentTargets { get; } +} diff --git a/tools/builder/targets/Build.cs b/tools/builder/targets/Build.cs new file mode 100644 index 00000000..38dabf84 --- /dev/null +++ b/tools/builder/targets/Build.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +[Target(nameof(Build), + nameof(Restore))] +public static class Build +{ + public static async Task OnExecute(BuildContext context) + { + context.BuildStep("Compiling binaries"); + + await context.Exec("dotnet", $"msbuild -p:Configuration={context.ConfigurationText}"); + } +} diff --git a/tools/builder/targets/CI.cs b/tools/builder/targets/CI.cs new file mode 100644 index 00000000..62e5d343 --- /dev/null +++ b/tools/builder/targets/CI.cs @@ -0,0 +1,3 @@ +[Target(nameof(CI), + nameof(SetVersion), nameof(Test), nameof(Packages), nameof(SignPackages), nameof(PushMyGet))] +public class CI { } diff --git a/tools/builder/targets/DownloadNuGet.cs b/tools/builder/targets/DownloadNuGet.cs new file mode 100644 index 00000000..cbcc795d --- /dev/null +++ b/tools/builder/targets/DownloadNuGet.cs @@ -0,0 +1,26 @@ +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +[Target(nameof(DownloadNuGet))] +public static class DownloadNuGet +{ + public static async Task OnExecute(BuildContext context) + { + if (File.Exists(context.NuGetExe)) + return; + + using (var httpClient = new HttpClient()) + using (var stream = File.OpenWrite(context.NuGetExe)) + { + context.BuildStep($"Downloading {context.NuGetUrl} to {context.NuGetExe}"); + + using (var response = await httpClient.GetAsync(context.NuGetUrl)) + { + response.EnsureSuccessStatusCode(); + + await response.Content.CopyToAsync(stream); + } + } + } +} diff --git a/tools/builder/targets/Packages.cs b/tools/builder/targets/Packages.cs new file mode 100644 index 00000000..3d2f9f2f --- /dev/null +++ b/tools/builder/targets/Packages.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +[Target(nameof(Packages), + nameof(Build), nameof(DownloadNuGet))] +public static class Packages +{ + public static async Task OnExecute(BuildContext context) + { + context.BuildStep("Creating NuGet packages"); + + var nuspecFiles = Directory.GetFiles(context.BaseFolder, "*.nuspec", SearchOption.AllDirectories) + .OrderBy(x => x) + .Select(x => x.Substring(context.BaseFolder.Length + 1)); + + foreach (var nuspecFile in nuspecFiles) + await context.Exec(context.NuGetExe, $"pack {nuspecFile} -NonInteractive -NoPackageAnalysis -OutputDirectory {context.PackageOutputFolder} -Properties Configuration={context.ConfigurationText}"); + } +} diff --git a/tools/builder/targets/PushMyGet.cs b/tools/builder/targets/PushMyGet.cs new file mode 100644 index 00000000..6b0239c5 --- /dev/null +++ b/tools/builder/targets/PushMyGet.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +[Target(nameof(PushMyGet), + nameof(DownloadNuGet))] +public static class PushMyGet +{ + public static async Task OnExecute(BuildContext context) + { + context.BuildStep("Pushing packages to MyGet"); + + var myGetApiKey = Environment.GetEnvironmentVariable("MyGetApiKey"); + if (myGetApiKey == null) + { + context.WriteLineColor(ConsoleColor.Yellow, $"Skipping MyGet push because environment variable 'MyGetApiKey' is not set.{Environment.NewLine}"); + return; + } + + var packageFiles = Directory.GetFiles(context.PackageOutputFolder, "*.nupkg", SearchOption.AllDirectories) + .OrderBy(x => x) + .Select(x => x.Substring(context.BaseFolder.Length + 1)); + + foreach (var packageFile in packageFiles) + { + var args = $"push -source https://www.myget.org/F/xunit/api/v2/package -apiKey {myGetApiKey} {packageFile}"; + var redactedArgs = args.Replace(myGetApiKey, "[redacted]"); + await context.Exec(context.NuGetExe, args, redactedArgs); + } + } +} diff --git a/tools/builder/targets/Restore.cs b/tools/builder/targets/Restore.cs new file mode 100644 index 00000000..052694b6 --- /dev/null +++ b/tools/builder/targets/Restore.cs @@ -0,0 +1,14 @@ +using System.IO; +using System.Threading.Tasks; + +[Target(nameof(Restore), + nameof(DownloadNuGet))] +public static class Restore +{ + public static async Task OnExecute(BuildContext context) + { + context.BuildStep("Restoring NuGet packages"); + + await context.Exec("dotnet", "restore"); + } +} diff --git a/tools/builder/targets/SetVersion.cs b/tools/builder/targets/SetVersion.cs new file mode 100644 index 00000000..2b0b03ad --- /dev/null +++ b/tools/builder/targets/SetVersion.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +[Target(nameof(SetVersion))] +public static class SetVersion +{ + public static async Task OnExecute(BuildContext context) + { + if (context.BuildAssemblyVersion != null) + { + context.BuildStep($"Setting assembly version: {context.BuildAssemblyVersion}"); + + var filesToPatch = Directory.GetFiles(context.BaseFolder, "GlobalAssemblyInfo.cs", SearchOption.AllDirectories); + foreach (var fileToPatch in filesToPatch) + { + context.WriteLineColor(ConsoleColor.DarkGray, $"PATCH: {fileToPatch}"); + + var text = await File.ReadAllTextAsync(fileToPatch); + var newText = text.Replace("99.99.99.0", context.BuildAssemblyVersion); + if (newText != text) + await File.WriteAllTextAsync(fileToPatch, newText); + } + + Console.WriteLine(); + } + + if (context.BuildSemanticVersion != null) + { + context.BuildStep($"Setting semantic version: {context.BuildSemanticVersion}"); + + var filesToPatch = Directory.GetFiles(context.BaseFolder, "GlobalAssemblyInfo.cs", SearchOption.AllDirectories) + .Concat(Directory.GetFiles(context.BaseFolder, "*.nuspec", SearchOption.AllDirectories)); + + foreach (var fileToPatch in filesToPatch) + { + context.WriteLineColor(ConsoleColor.DarkGray, $"PATCH: {fileToPatch}"); + + var text = await File.ReadAllTextAsync(fileToPatch); + var newText = text.Replace("99.99.99-dev", context.BuildSemanticVersion); + if (newText != text) + await File.WriteAllTextAsync(fileToPatch, newText); + } + + Console.WriteLine(); + } + } +} diff --git a/tools/builder/targets/SignPackages.cs b/tools/builder/targets/SignPackages.cs new file mode 100644 index 00000000..dc31b279 --- /dev/null +++ b/tools/builder/targets/SignPackages.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +[Target(nameof(SignPackages), + nameof(Packages))] +public static class SignPackages +{ + public static async Task OnExecute(BuildContext context) + { + var signClientUser = Environment.GetEnvironmentVariable("SignClientUser"); + var signClientSecret = Environment.GetEnvironmentVariable("SignClientSecret"); + if (signClientUser == null || signClientSecret == null) + { + context.WriteLineColor(ConsoleColor.Yellow, $"Skipping packing signing because environment variables 'SignClientUser' and/or 'SignClientSecret' are not set.{Environment.NewLine}"); + return; + } + + var signClientFolder = Path.Combine(context.BaseFolder, "packages", $"SignClient.{context.SignClientVersion}"); + if (!Directory.Exists(signClientFolder)) + { + context.BuildStep($"Downloading SignClient {context.SignClientVersion}"); + + await context.Exec(context.NuGetExe, $"install SignClient -version {context.SignClientVersion} -SolutionDir \"{context.BaseFolder}\" -Verbosity quiet -NonInteractive"); + } + + context.BuildStep("Signing NuGet packages"); + + var appPath = Path.Combine(signClientFolder, "tools", "netcoreapp2.0", "SignClient.dll"); + var packageFiles = Directory.GetFiles(context.PackageOutputFolder, "*.nupkg", SearchOption.AllDirectories) + .OrderBy(x => x) + .Select(x => x.Substring(context.BaseFolder.Length + 1)); + + var signClientAppSettings = Path.Combine(context.BaseFolder, "tools", "SignClient", "appsettings.json"); + foreach (var packageFile in packageFiles) + { + var args = $"\"{appPath}\" sign -c \"{signClientAppSettings}\" -r \"{signClientUser}\" -s \"{signClientSecret}\" -n \"xUnit.net\" -d \"xUnit.net\" -u \"https://github.com/xunit/xunit\" -i \"{packageFile}\""; + var redactedArgs = args.Replace(signClientUser, "[redacted]") + .Replace(signClientSecret, "[redacted]"); + + await context.Exec("dotnet", args, redactedArgs); + } + } +} diff --git a/tools/builder/targets/Test.cs b/tools/builder/targets/Test.cs new file mode 100644 index 00000000..fb3f6fb9 --- /dev/null +++ b/tools/builder/targets/Test.cs @@ -0,0 +1,3 @@ +[Target(nameof(Test), + nameof(TestCore), nameof(TestFx))] +public class Test { } diff --git a/tools/builder/targets/TestCore.cs b/tools/builder/targets/TestCore.cs new file mode 100644 index 00000000..d7dad19f --- /dev/null +++ b/tools/builder/targets/TestCore.cs @@ -0,0 +1,17 @@ +using System.IO; +using System.Threading.Tasks; + +[Target(nameof(TestCore), + nameof(Build))] +public static class TestCore +{ + public static Task OnExecute(BuildContext context) + { + context.BuildStep("Running .NET Core tests"); + + var resultPath = Path.Combine(context.BaseFolder, "artifacts", "test"); + File.Delete(Path.Combine(resultPath, "netcore.trx")); + + return context.Exec("dotnet", $"test test/xunit.analyzers.tests --framework netcoreapp2.2 --configuration {context.ConfigurationText} --no-build --logger trx;LogFileName=netcore.trx --results-directory \"{resultPath}\""); + } +} diff --git a/tools/builder/targets/TestFx.cs b/tools/builder/targets/TestFx.cs new file mode 100644 index 00000000..bd96e919 --- /dev/null +++ b/tools/builder/targets/TestFx.cs @@ -0,0 +1,17 @@ +using System.IO; +using System.Threading.Tasks; + +[Target(nameof(TestFx), + nameof(Build))] +public static class TestFx +{ + public static Task OnExecute(BuildContext context) + { + context.BuildStep("Running .NET Framework tests"); + + var resultPath = Path.Combine(context.BaseFolder, "artifacts", "test"); + File.Delete(Path.Combine(resultPath, "netfx.trx")); + + return context.Exec("dotnet", $"test test/xunit.analyzers.tests --framework net472 --configuration {context.ConfigurationText} --no-build --logger trx;LogFileName=netfx.trx --results-directory \"{resultPath}\""); + } +}