From 10f0da5cdc99da81ad4cbaba8fac0f909d899c43 Mon Sep 17 00:00:00 2001 From: David Tchepak Date: Sun, 13 Aug 2023 12:15:18 +1000 Subject: [PATCH] Convert fake build to use build project This addresses the issue of running .NET 6.0 with dotnet 7 SDK. See: * https://github.com/fsprojects/FAKE/issues/2719 * https://fake.build/guide/getting-started.html#Run-FAKE-using-a-dedicated-build-project * https://github.com/TheAngryByrd/MiniScaffold/blob/master/build/build.fs --- .gitignore | 1 + build/ExtractDocs.fs | 63 +++++ build/ExtractDocs.fsx | 62 ----- build/build.cmd | 8 +- build/build.fs | 249 +++++++++++++++++ build/build.fsproj | 24 ++ build/build.fsx | 257 ------------------ build/build.sh | 9 +- .../GenericArguments.cs | 5 +- 9 files changed, 342 insertions(+), 336 deletions(-) create mode 100644 build/ExtractDocs.fs delete mode 100644 build/ExtractDocs.fsx create mode 100644 build/build.fs create mode 100644 build/build.fsproj delete mode 100644 build/build.fsx diff --git a/.gitignore b/.gitignore index b0d183069..ef3a9cc54 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ +buildOutput # VS Code directory .vscode/ diff --git a/build/ExtractDocs.fs b/build/ExtractDocs.fs new file mode 100644 index 000000000..d951636b2 --- /dev/null +++ b/build/ExtractDocs.fs @@ -0,0 +1,63 @@ +module ExtractDocs + +open System +open System.IO +open System.Text.RegularExpressions + +let LiquidTagRegex = @"```(?\w+)" + // Tag start with argument. e.g. "```csharp" + @"(?(?s:.*?))" + // Tag contents + @"```?" // Tag end +let TypeOrTestDeclRegex = @"(\[Test\]|(public |private |protected )?(class |interface )\w+\s*\{)" + +type LiquidTag = LiquidTag of name : string * contents : string +type CodeBlock = Declaration of string | Snippet of string + +let tags s : LiquidTag seq = + Regex.Matches(s, LiquidTagRegex) + |> Seq.cast + |> Seq.map (fun m -> LiquidTag (m.Groups.[1].Value, m.Groups.[2].Value)) + +let toCodeBlock (LiquidTag (name, c)) = + let isTypeOrTestDecl s = Regex.IsMatch(s, TypeOrTestDeclRegex) + match name with + | "csharp" -> if isTypeOrTestDecl c then Some (Declaration c) else Some (Snippet c) + | "requiredcode" -> Some (Declaration c) + | _ -> None + +let toCodeBlocks : string -> CodeBlock seq = + Seq.choose toCodeBlock << tags + +let toFixture name content = + let escapeName (s:string) = Regex.Replace(s, "\W", "_") + sprintf """ +using System; +using NUnit.Framework; +using System.Linq; +using System.Collections.Generic; +using System.ComponentModel; +using NSubstitute.Extensions; +using NSubstitute.Compatibility; + +namespace NSubstitute.Samples { + public class Tests_%s { + %s + } +}""" (escapeName name) content + +let toTest = sprintf """[Test] public void Test_%d() { +%s +}""" + +let appendCodeBlock (code, testNum) (cb:CodeBlock) = + match cb with + | Declaration d -> (code + Environment.NewLine + d, testNum) + | Snippet s -> + let test = toTest testNum s + (code + Environment.NewLine + test, testNum + 1) + +let strToFixture fixtureName s : string = + s + |> toCodeBlocks + |> Seq.fold appendCodeBlock ("", 0) + |> fst + |> toFixture fixtureName diff --git a/build/ExtractDocs.fsx b/build/ExtractDocs.fsx deleted file mode 100644 index 0243c0bbc..000000000 --- a/build/ExtractDocs.fsx +++ /dev/null @@ -1,62 +0,0 @@ -module ExtractDocs = - open System - open System.IO - open System.Text.RegularExpressions - - let LiquidTagRegex = @"```(?\w+)" + // Tag start with argument. e.g. "```csharp" - @"(?(?s:.*?))" + // Tag contents - @"```?" // Tag end - let TypeOrTestDeclRegex = @"(\[Test\]|(public |private |protected )?(class |interface )\w+\s*\{)" - - type LiquidTag = LiquidTag of name : string * contents : string - type CodeBlock = Declaration of string | Snippet of string - - let tags s : LiquidTag seq = - Regex.Matches(s, LiquidTagRegex) - |> Seq.cast - |> Seq.map (fun m -> LiquidTag (m.Groups.[1].Value, m.Groups.[2].Value)) - - let toCodeBlock (LiquidTag (name, c)) = - let isTypeOrTestDecl s = Regex.IsMatch(s, TypeOrTestDeclRegex) - match name with - | "csharp" -> if isTypeOrTestDecl c then Some (Declaration c) else Some (Snippet c) - | "requiredcode" -> Some (Declaration c) - | _ -> None - - let toCodeBlocks : string -> CodeBlock seq = - Seq.choose toCodeBlock << tags - - let toFixture name content = - let escapeName (s:string) = Regex.Replace(s, "\W", "_") - sprintf """ - using System; - using NUnit.Framework; - using System.Linq; - using System.Collections.Generic; - using System.ComponentModel; - using NSubstitute.Extensions; - using NSubstitute.Compatibility; - - namespace NSubstitute.Samples { - public class Tests_%s { - %s - } - }""" (escapeName name) content - - let toTest = sprintf """[Test] public void Test_%d() { - %s - }""" - - let appendCodeBlock (code, testNum) (cb:CodeBlock) = - match cb with - | Declaration d -> (code + Environment.NewLine + d, testNum) - | Snippet s -> - let test = toTest testNum s - (code + Environment.NewLine + test, testNum + 1) - - let strToFixture fixtureName s : string = - s - |> toCodeBlocks - |> Seq.fold appendCodeBlock ("", 0) - |> fst - |> toFixture fixtureName diff --git a/build/build.cmd b/build/build.cmd index 7cf535a1b..c20b4544e 100644 --- a/build/build.cmd +++ b/build/build.cmd @@ -3,10 +3,4 @@ cls set encoding=utf-8 SET SCRIPT_DIR=%~dp0 -SET TOOL_PATH=%SCRIPT_DIR%\.fake - -IF NOT EXIST "%TOOL_PATH%\fake.exe" ( - dotnet tool install fake-cli --tool-path %TOOL_PATH% -) - -"%TOOL_PATH%\fake.exe" --silent run %SCRIPT_DIR%\build.fsx %* +dotnet run --project "%SCRIPT_DIR%/build.fsproj" -- %* diff --git a/build/build.fs b/build/build.fs new file mode 100644 index 000000000..10d468555 --- /dev/null +++ b/build/build.fs @@ -0,0 +1,249 @@ +open System +open System.Diagnostics +open System.IO +open System.Text.RegularExpressions + +open Fake.Core +open Fake.Core.TargetOperators +open Fake.DotNet +open Fake.IO +open Fake.IO.Globbing.Operators +open Fake.IO.FileSystemOperators +open Fake.Tools + +open ExtractDocs + +let target = Target.create +let description = Target.description + +module FileReaderWriter = + let Read file = File.ReadAllText(file) + let Write file text = File.WriteAllText(file, text) + let TransformFile file target (f : string -> string) = + Read file + |> f + |> Write target + +module ExamplesToCode = + open FileReaderWriter + + let ConvertFile (file: string) targetDir = + let fileName = Path.GetFileNameWithoutExtension(file) + let target = targetDir @@ fileName + ".cs" + Trace.log <| sprintf "Converting %s to %s" file target + TransformFile file target (ExtractDocs.strToFixture fileName) + + let Convert paths targetDir = + let paths = paths |> Seq.toList + for p in paths do + Trace.trace <| sprintf "Convert from %s to %s" p targetDir + let files = !! "*.markdown" ++ "*.html" ++ "*.md" |> GlobbingPattern.setBaseDir p + for file in files do + ConvertFile file targetDir + +type BuildVersion = { assembly: string; file: string; info: string; package: string } +let getVersion () = + // The --first-parent flag is needed to make our walk linear from current commit and top. + // This way also merge commit is counted as "1". + let desc = Git.CommandHelper.runSimpleGitCommand "" "describe --tags --long --abbrev=40 --first-parent --match=v*" + let result = Regex.Match(desc, + @"^v(?\d+)\.(?\d+)\.(?\d+)(?
-\w+\d*)?-(?\d+)-g(?[a-z0-9]+)$",
+                             RegexOptions.IgnoreCase)
+                      .Groups
+    let getMatch (name:string) = result.[name].Value
+
+    let (major, minor, revision, preReleaseSuffix, commitsNum, commitSha) =
+        (getMatch "maj" |> int, getMatch "min" |> int, getMatch "rev" |> int, getMatch "pre", getMatch "num" |> int, getMatch "sha")
+
+    // Assembly version should contain major and minor only, as no breaking changes are expected in bug fix releases.
+    let assemblyVersion = sprintf "%d.%d.0.0" major minor
+    let fileVersion = sprintf "%d.%d.%d.%d" major minor revision commitsNum
+ 
+    // If number of commits since last tag is greater than zero, we append another identifier with number of commits.
+    // The produced version is larger than the last tag version.
+    // If we are on a tag, we use version without modification.
+    // Examples of output: 3.50.2.1, 3.50.2.215, 3.50.1-rc1.3, 3.50.1-rc3.35
+    let packageVersion = match commitsNum with
+                         | 0 -> sprintf "%d.%d.%d%s" major minor revision preReleaseSuffix
+                         | _ -> sprintf "%d.%d.%d%s.%d" major minor revision preReleaseSuffix commitsNum
+
+    let infoVersion = match commitsNum with
+                      | 0 -> packageVersion
+                      | _ -> sprintf "%s-%s" packageVersion commitSha
+
+    { assembly = assemblyVersion; file = fileVersion; info = infoVersion; package = packageVersion }
+ 
+let root = __SOURCE_DIRECTORY__  ".." |> Path.getFullName
+
+let configuration = Environment.environVarOrDefault "configuration" "Debug"
+let version = getVersion ()
+
+let additionalArgs = [
+    "AssemblyVersion", version.assembly
+    "FileVersion", version.file
+    "InformationalVersion", version.info
+    "PackageVersion", version.package
+]
+
+let output = root  "bin"  configuration
+let solution = (root  "NSubstitute.sln")
+
+let initTargets() =
+    Target.create "Default" ignore
+    Target.create "All" ignore
+
+    Target.description("Clean compilation artifacts and remove output bin directory")
+    Target.create "Clean" (fun _ ->
+        DotNet.exec (fun p -> { p with WorkingDirectory = root }) "clean"
+            (sprintf "--configuration %s --verbosity minimal" configuration)
+            |> ignore
+        Shell.cleanDirs [ output ]
+    )
+
+    Target.description("Restore dependencies")
+    Target.create "Restore" (fun _ ->
+        DotNet.restore (fun p -> p) solution
+    )
+
+    Target.description("Compile all projects")
+    Target.create "Build" (fun _ ->
+        DotNet.build (fun p ->
+            { p with Configuration = DotNet.BuildConfiguration.fromString configuration
+                     MSBuildParams = { p.MSBuildParams with Properties = additionalArgs }
+            }) solution
+    )
+
+    Target.description("Run tests")
+    Target.create "Test" (fun _ ->
+        DotNet.test (fun p ->
+            { p with Configuration = DotNet.BuildConfiguration.fromString configuration
+                     MSBuildParams = { p.MSBuildParams with Properties = additionalArgs }
+            }) (root  "tests/NSubstitute.Acceptance.Specs/NSubstitute.Acceptance.Specs.csproj")
+    )
+
+    Target.description("Generate Nuget package")
+    Target.create "Package" (fun _ ->
+        DotNet.pack (fun p ->
+            { p with Configuration = DotNet.BuildConfiguration.fromString configuration
+                     MSBuildParams = { p.MSBuildParams with Properties = additionalArgs }
+            }) (root  "src/NSubstitute/NSubstitute.csproj")
+    )
+
+    Target.description("Run all benchmarks. Must be run with configuration=Release.")
+    Target.create "Benchmarks" (fun _ ->
+        if configuration <> "Release" then
+            failwith "Benchmarks can only be run in Release mode. Please re-run the build in Release configuration."
+
+        let benchmarkCsproj = root  "tests/NSubstitute.Benchmarks/NSubstitute.Benchmarks.csproj" |> Path.getFullName
+        let benchmarkToRun = Environment.environVarOrDefault "benchmark" "*" // Defaults to "*" (all)
+        [ "netcoreapp2.1" ]
+        |> List.iter (fun framework ->
+            Trace.traceImportant ("Benchmarking " + framework)
+            let work = output  "benchmark-" + framework
+            Directory.ensure work
+            DotNet.exec (fun p -> { p with WorkingDirectory = work }) "run"
+                ("--framework " + framework + " --project " + benchmarkCsproj + " -- " + benchmarkToRun)
+                |> ignore
+        )
+    )
+
+    Target.description("Extract, build and test code from documentation.")
+    Target.create "TestCodeFromDocs" <| fun _ ->
+        let outputCodePath = output  "CodeFromDocs"
+        Directory.create outputCodePath
+        // generate samples from docs
+        ExamplesToCode.Convert [ root  "docs/"; root  "docs/help/_posts/"; root ] outputCodePath
+        // compile code samples
+        let csproj = """
+            
+            
+                net6.0;net462
+                latest
+            
+            
+                
+                
+                
+            
+            
+                
+            
+        
+        """
+        let projPath = outputCodePath  "Docs.csproj"
+        FileReaderWriter.Write projPath csproj
+        DotNet.restore (fun p -> p) projPath
+        DotNet.build (fun p -> p) projPath
+        DotNet.test (fun p -> p) projPath
+
+    let tryFindFileOnPath (file : string) : string option =
+        Environment.GetEnvironmentVariable("PATH").Split([| Path.PathSeparator |])
+        |> Seq.append ["."]
+        |> fun path -> ProcessUtils.tryFindFile path file
+
+    Target.description("Build documentation website. Requires Ruby, bundler and jekyll.")
+    Target.create "Documentation" <| fun _ -> 
+        Trace.log "Building site..."
+        let exe = [ "bundle.bat"; "bundle" ]
+                    |> Seq.map tryFindFileOnPath
+                    |> Seq.collect (Option.toList)
+                    |> Seq.tryFind (fun _ -> true)
+                    |> function | Some x -> Trace.log ("using " + x); x
+                                | None   -> Trace.log ("count not find exe"); "bundle"
+
+        let workingDir = root  "docs/"
+        let docOutputRelativeToWorkingDir = ".."  output  "nsubstitute.github.com"
+        
+        // TODO migrate the following to FAKE API: CreateProcess.ofStartInfo(p)
+        // https://fake.build/apidocs/v5/fake-core-createprocess.html
+        // that doesn't work for some reason
+        let p = ProcessStartInfo(
+                        UseShellExecute = false,
+                        CreateNoWindow = true,
+                        FileName = exe,
+                        WorkingDirectory = workingDir,
+                        Arguments = "exec jekyll build -d \"" + docOutputRelativeToWorkingDir + "\"")
+        let proc = Process.Start(p)
+        proc.WaitForExit()
+        let result = proc.ExitCode
+        if result = 0 then
+            "Site built in " + docOutputRelativeToWorkingDir |> Trace.log
+        else
+            "failed to build site" |> failwith
+
+    Target.description("List targets, similar to `rake -T`. For more details, run `--listTargets` instead.")
+    Target.create "-T" <| fun _ ->
+        printfn "Optional config options:"
+        printfn "  configuration=Debug|Release"
+        printfn "  benchmark=*|  (only for Benchmarks target in Release mode)"
+        printfn ""
+        Target.listAvailable()
+
+    "Clean" ?=> "Build"             |> ignore
+    "Clean" ?=> "Test"              |> ignore
+    "Clean" ?=> "Restore"           |> ignore
+    "Clean" ?=> "Documentation"     |> ignore
+    "Clean" ?=> "TestCodeFromDocs"  |> ignore
+    "Clean" ?=> "Package"           |> ignore
+    "Clean" ?=> "Default"           |> ignore
+
+    "Build"         <== [ "Restore" ]
+    "Test"          <== [ "Build" ]
+    "Documentation" <== [ "TestCodeFromDocs" ]
+    "Benchmarks"    <== [ "Build" ]
+    // For packaging, use a clean build and make sure all tests (inc. docs) pass.
+    "Package"       <== [ "Clean"; "Build"; "Test"; "TestCodeFromDocs" ]
+
+    "Default"       <== [ "Restore"; "Build"; "Test" ]
+    "All"           <== [ "Clean"; "Default"; "Documentation"; "Package" ]
+
+[]
+let main argv =
+    argv
+    |> Array.toList
+    |> Context.FakeExecutionContext.Create false "build.fsx"
+    |> Context.RuntimeContext.Fake
+    |> Context.setExecutionContext
+    initTargets()
+    Target.runOrDefaultWithArguments "Default"
+    0 
\ No newline at end of file
diff --git a/build/build.fsproj b/build/build.fsproj
new file mode 100644
index 000000000..f32d2af52
--- /dev/null
+++ b/build/build.fsproj
@@ -0,0 +1,24 @@
+
+
+  
+    Exe
+    net6.0
+    buildOutput
+  
+
+  
+    
+    
+  
+
+  
+    
+    
+    
+    
+    
+    
+    
+    
+  
+
diff --git a/build/build.fsx b/build/build.fsx
deleted file mode 100644
index 83a6ba6cd..000000000
--- a/build/build.fsx
+++ /dev/null
@@ -1,257 +0,0 @@
-#r "paket:
-frameworks: net6.0
-source https://api.nuget.org/v3/index.json
-nuget Microsoft.Build 17.3.2
-nuget Microsoft.Build.Framework 17.3.2
-nuget Microsoft.Build.Tasks.Core 17.3.2
-nuget Microsoft.Build.Utilities.Core 17.3.2
-nuget FSharp.Core ~> 6.0
-nuget Fake.IO.FileSystem
-nuget Fake.DotNet
-nuget Fake.DotNet.Cli
-nuget Fake.Tools.Git
-nuget Fake.Core.Target //"
-
-#load ".fake/build.fsx/intellisense.fsx"
-#load @"ExtractDocs.fsx"
-// Workaround to make Intellisense work, see https://github.com/fsharp/FAKE/issues/1938
-#if !FAKE
-  #r "netstandard"
-#endif
-
-open System
-open System.Diagnostics
-open System.IO
-open System.Text.RegularExpressions
-
-open Fake.Core
-open Fake.Core.TargetOperators
-open Fake.DotNet
-open Fake.IO
-open Fake.IO.Globbing.Operators
-open Fake.IO.FileSystemOperators
-open Fake.Tools
-
-open ExtractDocs
-
-let target = Target.create
-let description = Target.description
-
-module FileReaderWriter =
-    let Read file = File.ReadAllText(file)
-    let Write file text = File.WriteAllText(file, text)
-    let TransformFile file target (f : string -> string) =
-        Read file
-        |> f
-        |> Write target
-
-module ExamplesToCode =
-    open FileReaderWriter
-
-    let ConvertFile (file: string) targetDir =
-        let fileName = Path.GetFileNameWithoutExtension(file)
-        let target = targetDir @@ fileName + ".cs"
-        Trace.log <| sprintf "Converting %s to %s" file target
-        TransformFile file target (ExtractDocs.strToFixture fileName)
-
-    let Convert paths targetDir =
-        let paths = paths |> Seq.toList
-        for p in paths do
-            Trace.trace <| sprintf "Convert from %s to %s" p targetDir
-            let files = !! "*.markdown" ++ "*.html" ++ "*.md" |> GlobbingPattern.setBaseDir p
-            for file in files do
-                ConvertFile file targetDir
-
-type BuildVersion = { assembly: string; file: string; info: string; package: string }
-let getVersion () =
-    // The --first-parent flag is needed to make our walk linear from current commit and top.
-    // This way also merge commit is counted as "1".
-    let desc = Git.CommandHelper.runSimpleGitCommand "" "describe --tags --long --abbrev=40 --first-parent --match=v*"
-    let result = Regex.Match(desc,
-                             @"^v(?\d+)\.(?\d+)\.(?\d+)(?
-\w+\d*)?-(?\d+)-g(?[a-z0-9]+)$",
-                             RegexOptions.IgnoreCase)
-                      .Groups
-    let getMatch (name:string) = result.[name].Value
-
-    let (major, minor, revision, preReleaseSuffix, commitsNum, commitSha) =
-        (getMatch "maj" |> int, getMatch "min" |> int, getMatch "rev" |> int, getMatch "pre", getMatch "num" |> int, getMatch "sha")
-
-    // Assembly version should contain major and minor only, as no breaking changes are expected in bug fix releases.
-    let assemblyVersion = sprintf "%d.%d.0.0" major minor
-    let fileVersion = sprintf "%d.%d.%d.%d" major minor revision commitsNum
- 
-    // If number of commits since last tag is greater than zero, we append another identifier with number of commits.
-    // The produced version is larger than the last tag version.
-    // If we are on a tag, we use version without modification.
-    // Examples of output: 3.50.2.1, 3.50.2.215, 3.50.1-rc1.3, 3.50.1-rc3.35
-    let packageVersion = match commitsNum with
-                         | 0 -> sprintf "%d.%d.%d%s" major minor revision preReleaseSuffix
-                         | _ -> sprintf "%d.%d.%d%s.%d" major minor revision preReleaseSuffix commitsNum
-
-    let infoVersion = match commitsNum with
-                      | 0 -> packageVersion
-                      | _ -> sprintf "%s-%s" packageVersion commitSha
-
-    { assembly = assemblyVersion; file = fileVersion; info = infoVersion; package = packageVersion }
- 
-let root = __SOURCE_DIRECTORY__  ".." |> Path.getFullName
-
-let configuration = Environment.environVarOrDefault "configuration" "Debug"
-let version = getVersion ()
-
-let additionalArgs = [
-    "AssemblyVersion", version.assembly
-    "FileVersion", version.file
-    "InformationalVersion", version.info
-    "PackageVersion", version.package
-]
-
-let output = root  "bin"  configuration
-let solution = (root  "NSubstitute.sln")
-
-target "Default" ignore
-target "All" ignore
-
-description("Clean compilation artifacts and remove output bin directory")
-target "Clean" (fun _ ->
-    DotNet.exec (fun p -> { p with WorkingDirectory = root }) "clean"
-        (sprintf "--configuration %s --verbosity minimal" configuration)
-        |> ignore
-    Shell.cleanDirs [ output ]
-)
-
-description("Restore dependencies")
-target "Restore" (fun _ ->
-    DotNet.restore (fun p -> p) solution
-)
-
-description("Compile all projects")
-target "Build" (fun _ ->
-    DotNet.build (fun p -> { p with Configuration = DotNet.BuildConfiguration.fromString configuration
-                                    MSBuildParams = { p.MSBuildParams with Properties = additionalArgs }}) 
-                                    solution
-)
-
-description("Run tests")
-target "Test" (fun _ ->
-    DotNet.test (fun p -> { p with Configuration = DotNet.BuildConfiguration.fromString configuration
-                                   MSBuildParams = { p.MSBuildParams with Properties = additionalArgs }}) 
-                                   (root  "tests/NSubstitute.Acceptance.Specs/NSubstitute.Acceptance.Specs.csproj")
-)
-
-description("Generate Nuget package")
-target "Package" (fun _ ->
-    DotNet.pack (fun p -> { p with Configuration = DotNet.BuildConfiguration.fromString configuration
-                                   MSBuildParams = { p.MSBuildParams with Properties = additionalArgs }}) 
-                                   (root  "src/NSubstitute/NSubstitute.csproj")
-)
-
-description("Run all benchmarks. Must be run with configuration=Release.")
-target "Benchmarks" (fun _ ->
-    if configuration <> "Release" then
-        failwith "Benchmarks can only be run in Release mode. Please re-run the build in Release configuration."
-
-    let benchmarkCsproj = root  "tests/NSubstitute.Benchmarks/NSubstitute.Benchmarks.csproj" |> Path.getFullName
-    let benchmarkToRun = Environment.environVarOrDefault "benchmark" "*" // Defaults to "*" (all)
-    [ "netcoreapp2.1" ]
-    |> List.iter (fun framework ->
-        Trace.traceImportant ("Benchmarking " + framework)
-        let work = output  "benchmark-" + framework
-        Directory.ensure work
-        DotNet.exec (fun p -> { p with WorkingDirectory = work }) "run"
-            ("--framework " + framework + " --project " + benchmarkCsproj + " -- " + benchmarkToRun)
-            |> ignore
-    )
-)
-
-description("Extract, build and test code from documentation.")
-target "TestCodeFromDocs" <| fun _ ->
-    let outputCodePath = output  "CodeFromDocs"
-    Directory.create outputCodePath
-    // generate samples from docs
-    ExamplesToCode.Convert [ root  "docs/"; root  "docs/help/_posts/"; root ] outputCodePath
-    // compile code samples
-    let csproj = """
-        
-          
-            net6.0;net462
-            latest
-          
-          
-            
-            
-            
-          
-          
-            
-          
-    
-    """
-    let projPath = outputCodePath  "Docs.csproj"
-    FileReaderWriter.Write projPath csproj
-    DotNet.restore (fun p -> p) projPath
-    DotNet.build (fun p -> p) projPath
-    DotNet.test (fun p -> p) projPath
-
-let tryFindFileOnPath (file : string) : string option =
-    Environment.GetEnvironmentVariable("PATH").Split([| Path.PathSeparator |])
-    |> Seq.append ["."]
-    |> fun path -> ProcessUtils.tryFindFile path file
-
-description("Build documentation website. Requires Ruby, bundler and jekyll.")
-target "Documentation" <| fun _ -> 
-    Trace.log "Building site..."
-    let exe = [ "bundle.bat"; "bundle" ]
-                |> Seq.map tryFindFileOnPath
-                |> Seq.collect (Option.toList)
-                |> Seq.tryFind (fun _ -> true)
-                |> function | Some x -> Trace.log ("using " + x); x
-                            | None   -> Trace.log ("count not find exe"); "bundle"
-
-    let workingDir = root  "docs/"
-    let docOutputRelativeToWorkingDir = ".."  output  "nsubstitute.github.com"
-    
-    // TODO migrate the following to FAKE API: CreateProcess.ofStartInfo(p)
-    // https://fake.build/apidocs/v5/fake-core-createprocess.html
-    // that doesn't work for some reason
-    let p = ProcessStartInfo(
-                    UseShellExecute = false,
-                    CreateNoWindow = true,
-                    FileName = exe,
-                    WorkingDirectory = workingDir,
-                    Arguments = "exec jekyll build -d \"" + docOutputRelativeToWorkingDir + "\"")
-    let proc = Process.Start(p)
-    proc.WaitForExit()
-    let result = proc.ExitCode
-    if result = 0 then
-        "Site built in " + docOutputRelativeToWorkingDir |> Trace.log
-    else
-        "failed to build site" |> failwith
-
-description("List targets, similar to `rake -T`. For more details, run `--listTargets` instead.")
-target "-T" <| fun _ ->
-    printfn "Optional config options:"
-    printfn "  configuration=Debug|Release"
-    printfn "  benchmark=*|  (only for Benchmarks target in Release mode)"
-    printfn ""
-    Target.listAvailable()
-
-"Clean" ?=> "Build"
-"Clean" ?=> "Test"
-"Clean" ?=> "Restore"
-"Clean" ?=> "Documentation"
-"Clean" ?=> "TestCodeFromDocs"
-"Clean" ?=> "Package"
-"Clean" ?=> "Default"
-
-"Build"         <== [ "Restore" ]
-"Test"          <== [ "Build" ]
-"Documentation" <== [ "TestCodeFromDocs" ]
-"Benchmarks"     <== [ "Build" ]
-// For packaging, use a clean build and make sure all tests (inc. docs) pass.
-"Package"       <== [ "Clean"; "Build"; "Test"; "TestCodeFromDocs" ]
-
-"Default"       <== [ "Restore"; "Build"; "Test" ]
-"All"           <== [ "Clean"; "Default"; "Documentation"; "Package" ]
-
-Target.runOrDefault "Default"
diff --git a/build/build.sh b/build/build.sh
index 092923d53..c141bbddf 100755
--- a/build/build.sh
+++ b/build/build.sh
@@ -13,11 +13,4 @@ cd `dirname ${SCRIPT_PATH}` > /dev/null
 SCRIPT_PATH=`pwd`;
 popd  > /dev/null
 
-TOOL_PATH=$SCRIPT_PATH/.fake
-FAKE="$TOOL_PATH"/fake
-
-if ! [ -e "$FAKE" ]
-then
-  dotnet tool install fake-cli --tool-path "$TOOL_PATH"
-fi
-"$FAKE" --silent run "$SCRIPT_PATH/build.fsx" "$@"
+dotnet run --project "${SCRIPT_PATH}/build.fsproj" -- "$@"
diff --git a/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs b/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs
index 85fd7df72..fe0780c99 100644
--- a/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs
+++ b/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using NSubstitute.Exceptions;
 using NUnit.Framework;
 
@@ -20,7 +21,7 @@ public void Return_result_for_any_argument()
         int? whenDoResult = null;
         bool whenDoCalled = false;
         ISomethingWithGenerics something = Substitute.For();
-        something.Log(Arg.Any(), Arg.Do(a => argDoResult = ">>" + ((int)a).ToString("P")));
+        something.Log(Arg.Any(), Arg.Do(a => argDoResult = ">>" + ((int)a).ToString("P", CultureInfo.InvariantCulture)));
         something
             .When(substitute => substitute.Log(Arg.Any(), Arg.Any()))
             .Do(info =>
@@ -34,7 +35,7 @@ public void Return_result_for_any_argument()
         something.Received().Log(Arg.Any(), Arg.Any());
         something.Received().Log(7, 3409);
         Assert.That(whenDoCalled, Is.True);
-        Assert.That(argDoResult, Is.EqualTo(">>340 900.00 %"));
+        Assert.That(argDoResult, Is.EqualTo(">>340,900.00 %"));
         Assert.That(whenDoResult, Is.EqualTo(3409));
     }
 }
\ No newline at end of file