diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 000000000..4195b0ff4
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,207 @@
+name: Build
+
+on:
+ push:
+ branches: [ main ]
+ tags: [ 'v[0-9]+.[0-9]+.[0-9]+*' ]
+
+jobs:
+ unit-tests:
+ name: "Unit tests"
+ strategy:
+ matrix:
+ os: [ ubuntu-latest, windows-latest, macos-latest ]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup .NET SDKs
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 7.0.x
+ 8.0.x
+ 9.0.x
+ - name: Run unit tests (windows)
+ if: matrix.os == 'windows-latest'
+ run: ./build.ps1 CodeCoverage
+ - name: Run unit tests (ubuntu|macos)
+ if: matrix.os != 'windows-latest'
+ run: ./build.sh CodeCoverage
+ - name: Upload artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ runner.os }}-artifacts
+ path: |
+ ./Artifacts/*
+ ./TestResults/*.trx
+
+ api-tests:
+ name: "API tests"
+ runs-on: ubuntu-latest
+ env:
+ DOTNET_NOLOGO: true
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup .NET SDKs
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 7.0.x
+ 8.0.x
+ 9.0.x
+ - name: API checks
+ run: ./build.sh ApiChecks
+ - name: Upload artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: API-tests
+ path: |
+ ./Artifacts/*
+ ./TestResults/*.trx
+
+ static-code-analysis:
+ name: "Static code analysis"
+ runs-on: ubuntu-latest
+ env:
+ REPORTGENERATOR_LICENSE: ${{ secrets.REPORTGENERATOR_LICENSE }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ DOTNET_NOLOGO: true
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup .NET SDKs
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 7.0.x
+ 8.0.x
+ 9.0.x
+ - name: Run sonarcloud analysis
+ run: ./build.sh CodeAnalysis
+
+ publish-test-results:
+ name: "Publish Tests Results"
+ needs: [ api-tests, unit-tests ]
+ runs-on: ubuntu-latest
+ permissions:
+ checks: write
+ pull-requests: write
+ if: always()
+ steps:
+ - name: Download Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+ - name: Publish Test Results
+ uses: EnricoMi/publish-unit-test-result-action@v2
+ with:
+ comment_mode: always
+ files: "artifacts/**/**/*.trx"
+
+ pack:
+ name: "Pack"
+ runs-on: ubuntu-latest
+ needs: [ publish-test-results, static-code-analysis ]
+ env:
+ DOTNET_NOLOGO: true
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup .NET SDKs
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 7.0.x
+ 8.0.x
+ 9.0.x
+ - name: Pack nuget packages
+ run: ./build.sh Pack
+ - name: Upload packages
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ path: Artifacts/Packages
+ name: Packages
+
+ push:
+ name: "Push"
+ if: ${{ startsWith(github.ref, 'refs/tags/v') }}
+ runs-on: macos-latest
+ environment: production
+ needs: [ pack ]
+ permissions:
+ contents: write
+ steps:
+ - name: Download packages
+ uses: actions/download-artifact@v4
+ with:
+ name: Packages
+ path: Artifacts/Packages
+ - name: Publish
+ run: |
+ echo "Found the following packages to push:"
+ search_dir=Artifacts/Packages
+ for entry in Artifacts/Packages/*.nupkg
+ do
+ echo "- $entry"
+ done
+ for entry in Artifacts/Packages/*.nupkg
+ do
+ nuget push $entry -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} -SkipDuplicate
+ done
+ - name: Check pre-release
+ id: check-pre-release
+ run: |
+ if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "release=true" >> $GITHUB_OUTPUT
+ fi
+ - name: Create GitHub release
+ if: steps.check-pre-release.outputs.release == 'true'
+ continue-on-error: true
+ uses: softprops/action-gh-release@v2
+ with:
+ name: ${{ steps.tag.outputs.release_version }}
+ tag_name: ${{ steps.tag.outputs.release_version }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+ generate_release_notes: true
+
+ finalize-release:
+ name: "Finalize release"
+ if: startsWith(github.ref, 'refs/tags/v')
+ runs-on: ubuntu-latest
+ needs: [ push ]
+ permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+ steps:
+ - name: Check pre-release
+ id: check-pre-release
+ run: |
+ if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "release=true" >> $GITHUB_OUTPUT
+ fi
+ - name: Comment relevant issues and pull requests
+ if: steps.check-pre-release.outputs.release == 'true'
+ continue-on-error: true
+ uses: apexskier/github-release-commenter@v1.3.6
+ with:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ comment-template: |
+ This is addressed in release {release_link}.
+ label-template: |
+ state: released
+ skip-label: |
+ state: released
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 638b2c4cb..fb1cd2dc1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,23 +1,22 @@
-name: Continuous Integration
+name: CI
+
on:
- push:
- branches: [main]
+ workflow_dispatch:
pull_request:
- branches: [main]
+ branches: [ main ]
+
jobs:
- test:
- name: Test
+ unit-tests:
+ name: "Unit tests"
strategy:
- fail-fast: false
matrix:
- os: [ubuntu-latest, windows-latest, macos-latest]
+ os: [ ubuntu-latest, windows-latest, macos-latest ]
runs-on: ${{ matrix.os }}
steps:
- - name: Checkout sources
- uses: actions/checkout@v4
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- - name: Setup .NET
+ - name: Setup .NET SDKs
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
@@ -25,22 +24,31 @@ jobs:
7.0.x
8.0.x
9.0.x
- - name: Run tests
- run: dotnet test --filter FullyQualifiedName\!~TestableIO.System.IO.Abstractions.Api.Tests --collect:"XPlat Code Coverage" --logger "GitHubActions"
- - name: Upload coverage
+ - name: Run unit tests (windows)
+ if: matrix.os == 'windows-latest'
+ run: ./build.ps1 CodeCoverage
+ - name: Run unit tests (ubuntu|macos)
+ if: matrix.os != 'windows-latest'
+ run: ./build.sh CodeCoverage
+ - name: Upload artifacts
+ if: always()
uses: actions/upload-artifact@v4
with:
- name: Code coverage ${{ matrix.os }}
- path: "**/coverage.cobertura.xml"
- api-test:
- name: API Test
+ name: ${{ runner.os }}-artifacts
+ path: |
+ ./Artifacts/*
+ ./TestResults/*.trx
+
+ api-tests:
+ name: "API tests"
runs-on: ubuntu-latest
+ env:
+ DOTNET_NOLOGO: true
steps:
- - name: Checkout sources
- uses: actions/checkout@v4
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- - name: Setup .NET
+ - name: Setup .NET SDKs
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
@@ -48,106 +56,55 @@ jobs:
7.0.x
8.0.x
9.0.x
- - name: Build solution
- run: dotnet build
- - name: Run tests
- run: dotnet test --filter FullyQualifiedName~TestableIO.System.IO.Abstractions.Api.Tests --logger "GitHubActions"
- coverage:
- name: Coverage
- needs: [test, api-test]
- runs-on: ubuntu-latest
- steps:
- - name: Checkout sources
- uses: actions/checkout@v4
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- - uses: actions/download-artifact@v4
- with:
- name: Code coverage ubuntu-latest
- path: coverage-ubuntu
- - uses: actions/download-artifact@v4
- with:
- name: Code coverage windows-latest
- path: coverage-windows
- - uses: actions/download-artifact@v4
- with:
- name: Code coverage macos-latest
- path: coverage-macos
- - name: Generate coverage report
- uses: danielpalme/ReportGenerator-GitHub-Action@v5.4.4
- with:
- reports: "**/coverage.cobertura.xml"
- targetdir: "coverage-report"
- reporttypes: "Cobertura"
- - name: Publish coverage report to Codacy
- uses: codacy/codacy-coverage-reporter-action@master
- if: github.repository == 'TestableIO/System.IO.Abstractions' && github.event_name == 'push'
+ - name: API checks
+ run: ./build.sh ApiChecks
+ - name: Upload artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
with:
- project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
- coverage-reports: coverage-report/Cobertura.xml
- pack:
- name: Pack
- needs: [test, api-test]
+ name: API-tests
+ path: |
+ ./Artifacts/*
+ ./TestResults/*.trx
+
+ static-code-analysis:
+ name: "Static code analysis"
+ if: ${{ github.actor != 'dependabot[bot]' }}
runs-on: ubuntu-latest
+ env:
+ REPORTGENERATOR_LICENSE: ${{ secrets.REPORTGENERATOR_LICENSE }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ DOTNET_NOLOGO: true
steps:
- - name: Checkout sources
- uses: actions/checkout@v4
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- - name: Setup .NET
+ - name: Setup .NET SDKs
uses: actions/setup-dotnet@v4
- - name: Create packages
- run: dotnet pack --configuration Release --output ./packages
- - name: Upload a Build Artifact
- uses: actions/upload-artifact@v4
with:
- name: NuGet packages
- path: packages/*.*
- deploy:
- name: Deploy
- if: |
- github.ref == 'refs/heads/main' &&
- github.event_name == 'push' &&
- (
- startsWith(github.event.head_commit.message, 'feat:') ||
- startsWith(github.event.head_commit.message, 'feat!:') ||
- startsWith(github.event.head_commit.message, 'fix:') ||
- startsWith(github.event.head_commit.message, 'fix!:') ||
- contains(github.event.head_commit.message, 'breaking change')
- )
- needs: [pack]
+ dotnet-version: |
+ 6.0.x
+ 7.0.x
+ 8.0.x
+ 9.0.x
+ - name: Run sonarcloud analysis
+ run: ./build.sh CodeAnalysis
+
+ publish-test-results:
+ name: "Publish Tests Results"
+ needs: [ api-tests, unit-tests ]
runs-on: ubuntu-latest
+ permissions:
+ checks: write
+ pull-requests: write
+ if: always()
steps:
- - name: Checkout sources
- uses: actions/checkout@v4
+ - name: Download Artifacts
+ uses: actions/download-artifact@v4
with:
- fetch-depth: 0
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- - uses: actions/download-artifact@v4
- with:
- name: NuGet packages
- path: packages
- - name: Push packages
- run: dotnet nuget push "packages/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
- - uses: dotnet/nbgv@v0.4.2
- id: nbgv
- - name: Create GitHub release
- uses: softprops/action-gh-release@v2
+ path: artifacts
+ - name: Publish Test Results
+ uses: EnricoMi/publish-unit-test-result-action@v2
with:
- name: v${{ steps.nbgv.outputs.SemVer2 }}
- tag_name: v${{ steps.nbgv.outputs.SemVer2 }}
- token: ${{ secrets.GITHUB_TOKEN }}
- generate_release_notes: true
- - name: Wait for GitHub release to be available
- run: sleep 30s
- shell: bash
- - name: Comment on relevant PRs and issues
- uses: apexskier/github-release-commenter@v1.3.6
- with:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- comment-template: |
- This is addressed in release {release_link}.
- label-template: |
- state: released
-
+ comment_mode: always
+ files: "artifacts/**/**/*.trx"
diff --git a/.gitignore b/.gitignore
index 843a9fc23..2a6761c23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
+# Artifact and test results directories
+/Artifacts
+/TestResults
+
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
new file mode 100644
index 000000000..47d67fd11
--- /dev/null
+++ b/.nuke/build.schema.json
@@ -0,0 +1,141 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "definitions": {
+ "Host": {
+ "type": "string",
+ "enum": [
+ "AppVeyor",
+ "AzurePipelines",
+ "Bamboo",
+ "Bitbucket",
+ "Bitrise",
+ "GitHubActions",
+ "GitLab",
+ "Jenkins",
+ "Rider",
+ "SpaceAutomation",
+ "TeamCity",
+ "Terminal",
+ "TravisCI",
+ "VisualStudio",
+ "VSCode"
+ ]
+ },
+ "ExecutableTarget": {
+ "type": "string",
+ "enum": [
+ "ApiChecks",
+ "CalculateNugetVersion",
+ "Clean",
+ "CodeAnalysis",
+ "CodeAnalysisBegin",
+ "CodeAnalysisEnd",
+ "CodeCoverage",
+ "Compile",
+ "DotNetFrameworkUnitTests",
+ "DotNetUnitTests",
+ "Pack",
+ "Restore",
+ "UnitTests",
+ "UpdateReadme"
+ ]
+ },
+ "Verbosity": {
+ "type": "string",
+ "description": "",
+ "enum": [
+ "Verbose",
+ "Normal",
+ "Minimal",
+ "Quiet"
+ ]
+ },
+ "NukeBuild": {
+ "properties": {
+ "Continue": {
+ "type": "boolean",
+ "description": "Indicates to continue a previously failed build attempt"
+ },
+ "Help": {
+ "type": "boolean",
+ "description": "Shows the help text for this build assembly"
+ },
+ "Host": {
+ "description": "Host for execution. Default is 'automatic'",
+ "$ref": "#/definitions/Host"
+ },
+ "NoLogo": {
+ "type": "boolean",
+ "description": "Disables displaying the NUKE logo"
+ },
+ "Partition": {
+ "type": "string",
+ "description": "Partition to use on CI"
+ },
+ "Plan": {
+ "type": "boolean",
+ "description": "Shows the execution plan (HTML)"
+ },
+ "Profile": {
+ "type": "array",
+ "description": "Defines the profiles to load",
+ "items": {
+ "type": "string"
+ }
+ },
+ "Root": {
+ "type": "string",
+ "description": "Root directory during build execution"
+ },
+ "Skip": {
+ "type": "array",
+ "description": "List of targets to be skipped. Empty list skips all dependencies",
+ "items": {
+ "$ref": "#/definitions/ExecutableTarget"
+ }
+ },
+ "Target": {
+ "type": "array",
+ "description": "List of targets to be invoked. Default is '{default_target}'",
+ "items": {
+ "$ref": "#/definitions/ExecutableTarget"
+ }
+ },
+ "Verbosity": {
+ "description": "Logging verbosity during build execution. Default is 'Normal'",
+ "$ref": "#/definitions/Verbosity"
+ }
+ }
+ }
+ },
+ "allOf": [
+ {
+ "properties": {
+ "Configuration": {
+ "type": "string",
+ "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
+ "enum": [
+ "Debug",
+ "Release"
+ ]
+ },
+ "GithubToken": {
+ "type": "string",
+ "description": "Github Token"
+ },
+ "Solution": {
+ "type": "string",
+ "description": "Path to a solution file that is automatically loaded"
+ },
+ "SonarToken": {
+ "type": "string",
+ "description": "The key to push to sonarcloud",
+ "default": "Secrets must be entered via 'nuke :secrets [profile]'"
+ }
+ }
+ },
+ {
+ "$ref": "#/definitions/NukeBuild"
+ }
+ ]
+}
diff --git a/.nuke/parameters.json b/.nuke/parameters.json
new file mode 100644
index 000000000..629b58457
--- /dev/null
+++ b/.nuke/parameters.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "build.schema.json",
+ "Solution": "System.IO.Abstractions.sln"
+}
diff --git a/Directory.Build.props b/Directory.Build.props
index a7a394986..0fce8c99e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,6 +8,7 @@
$(MSBuildThisFileDirectory)StrongName.snk
latest
testing
+ CS1591
https://github.com/TestableIO/System.IO.Abstractions
MIT
README.md
@@ -18,6 +19,12 @@
$(DefineConstants);FEATURE_PATH_SPAN;FEATURE_FILE_SPAN
$(DefineConstants);FEATURE_SERIALIZABLE
+
+ true
+ true
+ true
+ snupkg
+
runtime; build; native; contentfiles; analyzers
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f771b06d0..ffdf1c746 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -25,4 +25,10 @@
+
+
+
+
+
+
diff --git a/Pipeline/.editorconfig b/Pipeline/.editorconfig
new file mode 100644
index 000000000..31e43dcd8
--- /dev/null
+++ b/Pipeline/.editorconfig
@@ -0,0 +1,11 @@
+[*.cs]
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+dotnet_style_require_accessibility_modifiers = never:warning
+
+csharp_style_expression_bodied_methods = true:silent
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_accessors = true:warning
diff --git a/Pipeline/Build.ApiChecks.cs b/Pipeline/Build.ApiChecks.cs
new file mode 100644
index 000000000..f99768d39
--- /dev/null
+++ b/Pipeline/Build.ApiChecks.cs
@@ -0,0 +1,28 @@
+using Nuke.Common;
+using Nuke.Common.ProjectModel;
+using Nuke.Common.Tooling;
+using Nuke.Common.Tools.DotNet;
+using static Nuke.Common.Tools.DotNet.DotNetTasks;
+
+// ReSharper disable AllUnderscoreLocalParameterName
+
+namespace Build;
+
+partial class Build
+{
+ Target ApiChecks => _ => _
+ .DependsOn(Compile)
+ .Executes(() =>
+ {
+ Project project = Solution.Tests.TestableIO_System_IO_Abstractions_Api_Tests;
+
+ DotNetTest(s => s
+ .SetConfiguration(Configuration == Configuration.Debug ? "Debug" : "Release")
+ .SetProcessEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US")
+ .EnableNoBuild()
+ .SetResultsDirectory(TestResultsDirectory)
+ .CombineWith(cc => cc
+ .SetProjectFile(project)
+ .AddLoggers($"trx;LogFileName={project.Name}.trx")), completeOnFailure: true);
+ });
+}
diff --git a/Pipeline/Build.CodeAnalysis.cs b/Pipeline/Build.CodeAnalysis.cs
new file mode 100644
index 000000000..9f3e9b035
--- /dev/null
+++ b/Pipeline/Build.CodeAnalysis.cs
@@ -0,0 +1,42 @@
+using Nuke.Common;
+using Nuke.Common.Tools.SonarScanner;
+
+// ReSharper disable AllUnderscoreLocalParameterName
+
+namespace Build;
+
+partial class Build
+{
+ [Parameter("The key to push to sonarcloud")] [Secret] readonly string SonarToken;
+
+ Target CodeAnalysisBegin => _ => _
+ .Unlisted()
+ .Before(Compile)
+ .Before(CodeCoverage)
+ .Executes(() =>
+ {
+ SonarScannerTasks.SonarScannerBegin(s => s
+ .SetOrganization("testableio")
+ .SetProjectKey("TestableIO_System.IO.Abstractions")
+ .AddVSTestReports(TestResultsDirectory / "*.trx")
+ .AddOpenCoverPaths(TestResultsDirectory / "reports" / "OpenCover.xml")
+ .SetPullRequestOrBranchName(GitHubActions, GitVersion)
+ .SetVersion(GitVersion.SemVer)
+ .SetToken(SonarToken));
+ });
+
+ Target CodeAnalysisEnd => _ => _
+ .Unlisted()
+ .DependsOn(Compile)
+ .DependsOn(CodeCoverage)
+ .OnlyWhenDynamic(() => IsServerBuild)
+ .Executes(() =>
+ {
+ SonarScannerTasks.SonarScannerEnd(s => s
+ .SetToken(SonarToken));
+ });
+
+ Target CodeAnalysis => _ => _
+ .DependsOn(CodeAnalysisBegin)
+ .DependsOn(CodeAnalysisEnd);
+}
diff --git a/Pipeline/Build.CodeCoverage.cs b/Pipeline/Build.CodeCoverage.cs
new file mode 100644
index 000000000..890fd9cb5
--- /dev/null
+++ b/Pipeline/Build.CodeCoverage.cs
@@ -0,0 +1,25 @@
+using Nuke.Common;
+using Nuke.Common.Tooling;
+using Nuke.Common.Tools.ReportGenerator;
+using static Nuke.Common.Tools.ReportGenerator.ReportGeneratorTasks;
+
+// ReSharper disable AllUnderscoreLocalParameterName
+
+namespace Build;
+
+partial class Build
+{
+ Target CodeCoverage => _ => _
+ .DependsOn(UnitTests)
+ .Executes(() =>
+ {
+ ReportGenerator(s => s
+ .SetProcessToolPath(NuGetToolPathResolver.GetPackageExecutable("ReportGenerator", "ReportGenerator.dll",
+ framework: "net8.0"))
+ .SetTargetDirectory(TestResultsDirectory / "reports")
+ .AddReports(TestResultsDirectory / "**/coverage.cobertura.xml")
+ .AddReportTypes(ReportTypes.OpenCover)
+ .AddFileFilters("-*.g.cs")
+ .SetAssemblyFilters("+TestableIO*"));
+ });
+}
diff --git a/Pipeline/Build.Compile.cs b/Pipeline/Build.Compile.cs
new file mode 100644
index 000000000..ba5da0b1c
--- /dev/null
+++ b/Pipeline/Build.Compile.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Linq;
+using Nuke.Common;
+using Nuke.Common.IO;
+using Nuke.Common.Tools.DotNet;
+using Nuke.Common.Utilities;
+using Nuke.Common.Utilities.Collections;
+using Serilog;
+using static Nuke.Common.Tools.DotNet.DotNetTasks;
+
+// ReSharper disable AllUnderscoreLocalParameterName
+
+namespace Build;
+
+partial class Build
+{
+ string BranchName;
+ string SemVer;
+
+ Target CalculateNugetVersion => _ => _
+ .Unlisted()
+ .Executes(() =>
+ {
+ SemVer = GitVersion.SemVer;
+ BranchName = GitVersion.BranchName;
+
+ if (GitHubActions?.IsPullRequest == true)
+ {
+ string buildNumber = GitHubActions.RunNumber.ToString();
+ Console.WriteLine(
+ $"Branch spec is a pull request. Adding build number {buildNumber}");
+
+ SemVer = string.Join('.', GitVersion.SemVer.Split('.').Take(3).Union([buildNumber,]));
+ }
+
+ Console.WriteLine($"SemVer = {SemVer}");
+ });
+
+ Target Clean => _ => _
+ .Unlisted()
+ .Before(Restore)
+ .Executes(() =>
+ {
+ ArtifactsDirectory.CreateOrCleanDirectory();
+ Log.Information("Cleaned {path}...", ArtifactsDirectory);
+
+ TestResultsDirectory.CreateOrCleanDirectory();
+ Log.Information("Cleaned {path}...", TestResultsDirectory);
+ });
+
+ Target Restore => _ => _
+ .Unlisted()
+ .DependsOn(Clean)
+ .Executes(() =>
+ {
+ DotNetRestore(s => s
+ .SetProjectFile(Solution)
+ .EnableNoCache()
+ .SetConfigFile(RootDirectory / "nuget.config"));
+ });
+
+ Target Compile => _ => _
+ .DependsOn(Restore)
+ .DependsOn(CalculateNugetVersion)
+ .Executes(() =>
+ {
+ string preRelease = "-CI";
+ if (GitHubActions == null)
+ {
+ preRelease = "-DEV";
+ }
+ else if (GitHubActions.Ref.StartsWith("refs/tags/", StringComparison.OrdinalIgnoreCase))
+ {
+ int preReleaseIndex = GitHubActions.Ref.IndexOf('-');
+ preRelease = preReleaseIndex > 0 ? GitHubActions.Ref[preReleaseIndex..] : "";
+ }
+
+ ReportSummary(s => s
+ .WhenNotNull(SemVer, (summary, semVer) => summary
+ .AddPair("Version", semVer + preRelease)));
+
+ DotNetBuild(s => s
+ .SetProjectFile(Solution)
+ .SetConfiguration(Configuration)
+ .EnableNoLogo()
+ .EnableNoRestore()
+ .SetVersion(SemVer + preRelease)
+ .SetAssemblyVersion(GitVersion.AssemblySemVer)
+ .SetFileVersion(GitVersion.AssemblySemFileVer)
+ .SetInformationalVersion(GitVersion.InformationalVersion));
+ });
+}
diff --git a/Pipeline/Build.Pack.cs b/Pipeline/Build.Pack.cs
new file mode 100644
index 000000000..30de44836
--- /dev/null
+++ b/Pipeline/Build.Pack.cs
@@ -0,0 +1,98 @@
+using System.IO;
+using System.Linq;
+using System.Text;
+using Nuke.Common;
+using Nuke.Common.IO;
+using Nuke.Common.ProjectModel;
+using Nuke.Common.Utilities;
+using Nuke.Common.Utilities.Collections;
+using static Serilog.Log;
+
+// ReSharper disable AllUnderscoreLocalParameterName
+
+namespace Build;
+
+partial class Build
+{
+ Target UpdateReadme => _ => _
+ .DependsOn(Clean)
+ .Before(Compile)
+ .Executes(() =>
+ {
+ string version = string.Join('.', GitVersion.SemVer.Split('.').Take(3));
+ if (version.IndexOf('-') != -1)
+ {
+ version = version.Substring(0, version.IndexOf('-'));
+ }
+
+ StringBuilder sb = new();
+ string[] lines = File.ReadAllLines(Solution.Directory / "README.md");
+ sb.AppendLine(lines.First());
+ sb.AppendLine(
+ $"[](https://github.com/TestableIO/System.IO.Abstractions/releases/tag/v{version})");
+ foreach (string line in lines.Skip(1))
+ {
+ if (line.StartsWith("[ ||
+ line.StartsWith("[)
+ {
+ continue;
+ }
+
+ if (line.StartsWith("[)
+ {
+ sb.AppendLine(line
+ .Replace(")", $"&branch=release/v{version})"));
+ continue;
+ }
+
+ if (line.StartsWith("[)
+ {
+ sb.AppendLine(line
+ .Replace("%2Fmain)", $"%2Frelease%2Fv{version})")
+ .Replace("/main)", $"/release/v{version})"));
+ continue;
+ }
+
+ sb.AppendLine(line);
+ }
+
+ File.WriteAllText(ArtifactsDirectory / "README.md", sb.ToString());
+ });
+
+ Target Pack => _ => _
+ .DependsOn(UpdateReadme)
+ .DependsOn(Compile)
+ .Executes(() =>
+ {
+ ReportSummary(s => s
+ .WhenNotNull(SemVer, (c, semVer) => c
+ .AddPair("Packed version", semVer)));
+
+ AbsolutePath packagesDirectory = ArtifactsDirectory / "Packages";
+ packagesDirectory.CreateOrCleanDirectory();
+
+ foreach (Project project in new[]
+ {
+ Solution.TestableIO_System_IO_Abstractions_Wrappers,
+ Solution.TestableIO_System_IO_Abstractions_TestingHelpers,
+ Solution.Meta.TestableIO_System_IO_Abstractions,
+ Solution.Meta.System_IO_Abstractions,
+ Solution.Meta.System_IO_Abstractions_TestingHelpers,
+ })
+ {
+ foreach (string package in
+ Directory.EnumerateFiles(project.Directory / "bin", "*.nupkg", SearchOption.AllDirectories))
+ {
+ File.Move(package, packagesDirectory / Path.GetFileName(package));
+ Debug("Found nuget package: {PackagePath}", package);
+ }
+
+ foreach (string symbolPackage in
+ Directory.EnumerateFiles(project.Directory / "bin", "*.snupkg", SearchOption.AllDirectories))
+ {
+ File.Move(symbolPackage, packagesDirectory / Path.GetFileName(symbolPackage));
+ Debug("Found symbol package: {PackagePath}", symbolPackage);
+ }
+ }
+ });
+}
diff --git a/Pipeline/Build.UnitTest.cs b/Pipeline/Build.UnitTest.cs
new file mode 100644
index 000000000..c88133073
--- /dev/null
+++ b/Pipeline/Build.UnitTest.cs
@@ -0,0 +1,79 @@
+using System.IO;
+using System.Linq;
+using Nuke.Common;
+using Nuke.Common.IO;
+using Nuke.Common.ProjectModel;
+using Nuke.Common.Tooling;
+using Nuke.Common.Tools.DotNet;
+using Nuke.Common.Tools.NUnit;
+using static Nuke.Common.Tools.DotNet.DotNetTasks;
+
+// ReSharper disable AllUnderscoreLocalParameterName
+
+namespace Build;
+
+partial class Build
+{
+ Target UnitTests => _ => _
+ .DependsOn(DotNetFrameworkUnitTests)
+ .DependsOn(DotNetUnitTests);
+
+ Project[] UnitTestProjects =>
+ [
+ Solution.Tests.TestableIO_System_IO_Abstractions_Wrappers_Tests,
+ Solution.Tests.TestableIO_System_IO_Abstractions_TestingHelpers_Tests,
+ Solution.Tests.TestableIO_System_IO_Abstractions_Parity_Tests,
+ ];
+
+ Target DotNetFrameworkUnitTests => _ => _
+ .Unlisted()
+ .DependsOn(Compile)
+ .OnlyWhenDynamic(() => EnvironmentInfo.IsWin)
+ .Executes(() =>
+ {
+ var testAssemblies = UnitTestProjects
+ .SelectMany(project =>
+ project.Directory.GlobFiles(
+ $"bin/{(Configuration == Configuration.Debug ? "Debug" : "Release")}/net472/*.Tests.dll"))
+ .Select(p => p.ToString())
+ .ToArray();
+
+ Assert.NotEmpty(testAssemblies.ToList());
+
+ foreach (var testAssembly in testAssemblies)
+ {
+ var currentDirectory = Path.GetDirectoryName(testAssembly);
+
+ NUnitTasks.NUnit3(s => s
+ .SetInputFiles(testAssembly)
+ .SetProcessWorkingDirectory(currentDirectory)
+ );
+ }
+ });
+
+ Target DotNetUnitTests => _ => _
+ .Unlisted()
+ .DependsOn(Compile)
+ .Executes(() =>
+ {
+ string[] excludedFrameworks = ["net48",];
+ DotNetTest(s => s
+ .SetConfiguration(Configuration)
+ .SetProcessEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US")
+ .EnableNoBuild()
+ .SetDataCollector("XPlat Code Coverage")
+ .SetResultsDirectory(TestResultsDirectory)
+ .CombineWith(
+ UnitTestProjects,
+ (settings, project) => settings
+ .SetProjectFile(project)
+ .CombineWith(
+ project.GetTargetFrameworks()?.Except(excludedFrameworks),
+ (frameworkSettings, framework) => frameworkSettings
+ .SetFramework(framework)
+ .AddLoggers($"trx;LogFileName={project.Name}_{framework}.trx")
+ )
+ ), completeOnFailure: true
+ );
+ });
+}
\ No newline at end of file
diff --git a/Pipeline/Build.cs b/Pipeline/Build.cs
new file mode 100644
index 000000000..10372f655
--- /dev/null
+++ b/Pipeline/Build.cs
@@ -0,0 +1,31 @@
+using Nuke.Common;
+using Nuke.Common.CI.GitHubActions;
+using Nuke.Common.IO;
+using Nuke.Common.ProjectModel;
+using Nuke.Common.Tools.GitVersion;
+
+namespace Build;
+
+[GitHubActions(
+ "Build",
+ GitHubActionsImage.UbuntuLatest,
+ AutoGenerate = false,
+ ImportSecrets = [nameof(GithubToken),]
+)]
+partial class Build : NukeBuild
+{
+ [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
+ readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
+
+ [Parameter("Github Token")] readonly string GithubToken;
+
+ [Required] [GitVersion(Framework = "net8.0", NoCache = true, NoFetch = true)] readonly GitVersion GitVersion;
+
+ [Solution(GenerateProjects = true)] readonly Solution Solution;
+
+ AbsolutePath ArtifactsDirectory => RootDirectory / "Artifacts";
+ AbsolutePath TestResultsDirectory => RootDirectory / "TestResults";
+ GitHubActions GitHubActions => GitHubActions.Instance;
+
+ public static int Main() => Execute(x => x.UnitTests, x => x.Pack);
+}
diff --git a/Pipeline/Build.csproj b/Pipeline/Build.csproj
new file mode 100644
index 000000000..074ab05ea
--- /dev/null
+++ b/Pipeline/Build.csproj
@@ -0,0 +1,31 @@
+
+
+
+ Exe
+ net8.0
+
+ CS0649;CS0169;CA1050;CA1822;CA2211;IDE1006
+ ..
+ ..
+ 1
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Pipeline/Build.csproj.DotSettings b/Pipeline/Build.csproj.DotSettings
new file mode 100644
index 000000000..88a8824cd
--- /dev/null
+++ b/Pipeline/Build.csproj.DotSettings
@@ -0,0 +1,31 @@
+
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ Implicit
+ Implicit
+ ExpressionBody
+ 0
+ NEXT_LINE
+ True
+ False
+ 120
+ IF_OWNER_IS_SINGLE_LINE
+ WRAP_IF_LONG
+ False
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
diff --git a/Pipeline/BuildExtensions.cs b/Pipeline/BuildExtensions.cs
new file mode 100644
index 000000000..cd3e593a6
--- /dev/null
+++ b/Pipeline/BuildExtensions.cs
@@ -0,0 +1,36 @@
+using System;
+using Nuke.Common.CI.GitHubActions;
+using Nuke.Common.Tools.GitVersion;
+using Nuke.Common.Tools.SonarScanner;
+using Serilog;
+
+namespace Build;
+
+public static class BuildExtensions
+{
+ public static SonarScannerBeginSettings SetPullRequestOrBranchName(
+ this SonarScannerBeginSettings settings,
+ GitHubActions gitHubActions,
+ GitVersion gitVersion)
+ {
+ if (gitHubActions?.IsPullRequest == true)
+ {
+ Log.Information("Use pull request analysis");
+ return settings
+ .SetPullRequestKey(gitHubActions.PullRequestNumber.ToString())
+ .SetPullRequestBranch(gitHubActions.Ref)
+ .SetPullRequestBase(gitHubActions.BaseRef);
+ }
+
+ if (gitHubActions?.Ref.StartsWith("refs/tags/", StringComparison.OrdinalIgnoreCase) == true)
+ {
+ string version = gitHubActions.Ref.Substring("refs/tags/".Length);
+ string branchName = "release/" + version;
+ Log.Information("Use release branch analysis for '{BranchName}'", branchName);
+ return settings.SetBranchName(branchName);
+ }
+
+ Log.Information("Use branch analysis for '{BranchName}'", gitVersion.BranchName);
+ return settings.SetBranchName(gitVersion.BranchName);
+ }
+}
diff --git a/Pipeline/Configuration.cs b/Pipeline/Configuration.cs
new file mode 100644
index 000000000..9c08b1ae9
--- /dev/null
+++ b/Pipeline/Configuration.cs
@@ -0,0 +1,16 @@
+using System;
+using System.ComponentModel;
+using System.Linq;
+using Nuke.Common.Tooling;
+
+[TypeConverter(typeof(TypeConverter))]
+public class Configuration : Enumeration
+{
+ public static Configuration Debug = new Configuration { Value = nameof(Debug) };
+ public static Configuration Release = new Configuration { Value = nameof(Release) };
+
+ public static implicit operator string(Configuration configuration)
+ {
+ return configuration.Value;
+ }
+}
diff --git a/Pipeline/Directory.Build.props b/Pipeline/Directory.Build.props
new file mode 100644
index 000000000..e147d6352
--- /dev/null
+++ b/Pipeline/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/Pipeline/Directory.Build.targets b/Pipeline/Directory.Build.targets
new file mode 100644
index 000000000..253260956
--- /dev/null
+++ b/Pipeline/Directory.Build.targets
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/System.IO.Abstractions.sln b/System.IO.Abstractions.sln
index 13f0b57a8..4014cdb00 100644
--- a/System.IO.Abstractions.sln
+++ b/System.IO.Abstractions.sln
@@ -1,3 +1,4 @@
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32825.248
@@ -24,18 +25,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestableIO.System.IO.Abstra
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_", "_", "{BBF7AD8D-5522-48C0-A906-00CBB72308A0}"
ProjectSection(SolutionItems) = preProject
+ .gitignore = .gitignore
Directory.Build.props = Directory.Build.props
+ Directory.Packages.props = Directory.Packages.props
global.json = global.json
README.md = README.md
version.json = version.json
- Directory.Packages.props = Directory.Packages.props
- .gitignore = .gitignore
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{2BE9161B-A3F3-4511-81DB-DB1DCB6375C9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{D905E09D-6DC3-4F7C-8E83-82FADAE2C9E5}"
ProjectSection(SolutionItems) = preProject
+ .github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\ci.yml = .github\workflows\ci.yml
.github\workflows\pr.yml = .github\workflows\pr.yml
EndProjectSection
@@ -54,6 +56,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{5971F640
tests\Directory.Build.props = tests\Directory.Build.props
EndProjectSection
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Build", "Pipeline\Build.csproj", "{DC39543E-0A17-4869-96C9-B876EB1BB117}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pipeline", "Pipeline", "{8091BCA3-70F8-4506-9701-C3074E79A66D}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestableIO.System.IO.Abstractions.Parity.Tests", "tests\TestableIO.System.IO.Abstractions.Parity.Tests\TestableIO.System.IO.Abstractions.Parity.Tests.csproj", "{93B79631-E220-414B-8F0D-3885825DE1AC}"
EndProject
Global
@@ -98,6 +103,8 @@ Global
{7DE6AD74-E2B3-498E-90C5-DDF3188F333A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7DE6AD74-E2B3-498E-90C5-DDF3188F333A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7DE6AD74-E2B3-498E-90C5-DDF3188F333A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DC39543E-0A17-4869-96C9-B876EB1BB117}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DC39543E-0A17-4869-96C9-B876EB1BB117}.Release|Any CPU.ActiveCfg = Release|Any CPU
{93B79631-E220-414B-8F0D-3885825DE1AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{93B79631-E220-414B-8F0D-3885825DE1AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{93B79631-E220-414B-8F0D-3885825DE1AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -109,15 +116,16 @@ Global
GlobalSection(NestedProjects) = preSolution
{B7DA254D-496F-4C50-969C-CF925758E2ED} = {C078E0B6-9747-475F-A999-B9E775DF6643}
{0103D32A-6124-4A1A-AD6C-30EB957F09B0} = {C078E0B6-9747-475F-A999-B9E775DF6643}
+ {015B3812-E01D-479C-895D-BDDF16E798CA} = {10F39E91-97F2-4812-9D2F-79BE18EC6B08}
{7105D748-1253-409F-A624-4879412EF3C2} = {BCEC61BD-4941-41EC-975A-ACEFC7AC1780}
+ {1B8388D2-58A7-47B8-89EC-C5A94A0FEED5} = {C078E0B6-9747-475F-A999-B9E775DF6643}
{919888D2-E37D-40E7-8AD0-600F9429316D} = {BCEC61BD-4941-41EC-975A-ACEFC7AC1780}
{2BE9161B-A3F3-4511-81DB-DB1DCB6375C9} = {BBF7AD8D-5522-48C0-A906-00CBB72308A0}
{D905E09D-6DC3-4F7C-8E83-82FADAE2C9E5} = {2BE9161B-A3F3-4511-81DB-DB1DCB6375C9}
- {1B8388D2-58A7-47B8-89EC-C5A94A0FEED5} = {C078E0B6-9747-475F-A999-B9E775DF6643}
{B66A0B3F-6A00-482E-99E2-27D8DECB075E} = {BBF7AD8D-5522-48C0-A906-00CBB72308A0}
- {015B3812-E01D-479C-895D-BDDF16E798CA} = {10F39E91-97F2-4812-9D2F-79BE18EC6B08}
{7DE6AD74-E2B3-498E-90C5-DDF3188F333A} = {BCEC61BD-4941-41EC-975A-ACEFC7AC1780}
{5971F640-8BBA-4264-BCD6-60A97DD54B4A} = {BBF7AD8D-5522-48C0-A906-00CBB72308A0}
+ {DC39543E-0A17-4869-96C9-B876EB1BB117} = {8091BCA3-70F8-4506-9701-C3074E79A66D}
{93B79631-E220-414B-8F0D-3885825DE1AC} = {BCEC61BD-4941-41EC-975A-ACEFC7AC1780}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
diff --git a/build.cmd b/build.cmd
new file mode 100755
index 000000000..b08cc590f
--- /dev/null
+++ b/build.cmd
@@ -0,0 +1,7 @@
+:; set -eo pipefail
+:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+:; ${SCRIPT_DIR}/build.sh "$@"
+:; exit $?
+
+@ECHO OFF
+powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 000000000..8b218a560
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,74 @@
+[CmdletBinding()]
+Param(
+ [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
+ [string[]]$BuildArguments
+)
+
+Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
+
+Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
+$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
+
+###########################################################################
+# CONFIGURATION
+###########################################################################
+
+$BuildProjectFile = "$PSScriptRoot\Pipeline\Build.csproj"
+$TempDirectory = "$PSScriptRoot\\.nuke\temp"
+
+$DotNetGlobalFile = "$PSScriptRoot\\global.json"
+$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
+$DotNetChannel = "STS"
+
+$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
+$env:DOTNET_NOLOGO = 1
+
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function ExecSafe([scriptblock] $cmd) {
+ & $cmd
+ if ($LASTEXITCODE) { exit $LASTEXITCODE }
+}
+
+# If dotnet CLI is installed globally and it matches requested version, use for execution
+if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
+ $(dotnet --version) -and $LASTEXITCODE -eq 0) {
+ $env:DOTNET_EXE = (Get-Command "dotnet").Path
+}
+else {
+ # Download install script
+ $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
+ New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
+
+ # If global.json exists, load expected version
+ if (Test-Path $DotNetGlobalFile) {
+ $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
+ if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
+ $DotNetVersion = $DotNetGlobal.sdk.version
+ }
+ }
+
+ # Install by channel or version
+ $DotNetDirectory = "$TempDirectory\dotnet-win"
+ if (!(Test-Path variable:DotNetVersion)) {
+ ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
+ } else {
+ ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
+ }
+ $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
+ $env:PATH = "$DotNetDirectory;$env:PATH"
+}
+
+Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)"
+
+if (Test-Path env:NUKE_ENTERPRISE_TOKEN) {
+ & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null
+ & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null
+}
+
+ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
+ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }
diff --git a/build.sh b/build.sh
new file mode 100755
index 000000000..46f2bac0a
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+
+bash --version 2>&1 | head -n 1
+
+set -eo pipefail
+SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+
+###########################################################################
+# CONFIGURATION
+###########################################################################
+
+BUILD_PROJECT_FILE="$SCRIPT_DIR/Pipeline/Build.csproj"
+TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
+
+DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
+DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
+DOTNET_CHANNEL="STS"
+
+export DOTNET_CLI_TELEMETRY_OPTOUT=1
+export DOTNET_NOLOGO=1
+
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function FirstJsonValue {
+ perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
+}
+
+# If dotnet CLI is installed globally and it matches requested version, use for execution
+if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
+ export DOTNET_EXE="$(command -v dotnet)"
+else
+ # Download install script
+ DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
+ mkdir -p "$TEMP_DIRECTORY"
+ curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
+ chmod +x "$DOTNET_INSTALL_FILE"
+
+ # If global.json exists, load expected version
+ if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
+ DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
+ if [[ "$DOTNET_VERSION" == "" ]]; then
+ unset DOTNET_VERSION
+ fi
+ fi
+
+ # Install by channel or version
+ DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
+ if [[ -z ${DOTNET_VERSION+x} ]]; then
+ "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
+ else
+ "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
+ fi
+ export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
+ export PATH="$DOTNET_DIRECTORY:$PATH"
+fi
+
+echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)"
+
+if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then
+ "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true
+ "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true
+fi
+
+"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
+"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"