Trying FAKE out

For many years I’ve been using Nant scripts to build projects, run tests and all the other bits of fun for CI (Continuous Integration) or just running tests locally before checking in etc. Whilst I can do everything I’ve wanted to do in Nant, as a programmer, I’ve often found the XML syntax a little too verbose and in some cases confusing, so I’ve finally taken the plunge and am trying to learn to use FAKE, the F# make tool.

I don’t know whether I’ll prefer FAKE or decide I was better off with Nant – let’s see how it goes.

The whole thing

I’m going to jump straight into it by looking at a script I’ve built for building a CSV library I wrote and I’ll attempt to explain things thereafter. So here’s the whole thing…

#r @"/Development/FAKE/tools/FAKE/tools/FakeLib.dll"
open Fake

RestorePackages()

// Values
let mode = getBuildParamOrDefault "mode" "Debug"
let buildDir = "./Csv.Data/bin/" + mode + "/"
let testBuildDir = "./Csv.Data.Tests/bin/" + mode + "/"
let solution = "./Csv.Data.sln"
let testDlls = !! (testBuildDir + "/*.Tests.dll")
let xunitrunner = "C:\Tools\xUnit\xunit.console.clr4.exe"

// Targets
Target "Clean" (fun _ ->
   CleanDirs [buildDir; testBuildDir]
)

Target "Build" (fun _ ->
    !! solution
        |> 
        match mode.ToLower() with
            | "release" -> MSBuildRelease testBuildDir "Build"
            | _ -> MSBuildDebug testBuildDir "Build"
        |> Log "AppBuild-Output"
)

Target "Test" (fun _ ->
    testDlls
        |> xUnit (fun p ->
            {p with
                ShadowCopy = false;
                HtmlOutput = true;
                XmlOutput = true;
                ToolPath = xunitrunner;                
                OutputDir = testBuildDir })
)

Target "Default" (fun _ ->
   trace "Building and Running Csv.Data Tests"
)

// Dependencies
"Clean"
==> "Build"
==> "Test"
==> "Default"

RunTargetOrDefault "Default" 

Breaking it down

The first thing we need in our script is a way to include a reference to the FakeLib.dll which contains all the FAKE goodness and then ofcourse a way of using the library, this is standard F# stuff – we use the #r to reference the FakeLib.dll assembly then open Fake, as per the code below

#r @"/Development/FAKE/tools/FAKE/tools/FakeLib.dll"
open Fake

I am using NuGet packages in my project and the project is set-up to restore any NuGet packages, but we can use FAKE to carry out this task for us using

RestorePackages()

The next section in the script is probably self-explanatory but for completeness let’s look at it anyway as it does touch on a couple of things. So next we declare values for use within the script

let mode = getBuildParamOrDefault "mode" "Debug"
let buildDir = "./Csv.Data/bin/" + mode + "/"
let testBuildDir = "./Csv.Data.Tests/bin/" + mode + "/"
let solution = "./Csv.Data.sln"
let testDlls = !! (testBuildDir + "/*.Tests.dll")
let xunitrunner = "C:\Tools\xUnit\xunit.console.clr4.exe"

The first value, mode, is set to a command line parameter “mode” which allows us to define whether to create a Debug or Release build. The next three lines and the final line are pretty obvious, allowing us to create folder names based upon the selected mode and the filenames of the solution and the xUnit console runner.

The testDlls line might seem a little odd with the use of the !! (double bang). This is declared in FAKE to allow us a terse and simple way of including files using pattern matching. Whilst not used in this script we can also just as easily include and/or exclude files using ++ (to include) and (to exclude).

Back to the script and we’re now going to create the targets. Just like Nant we can declare targets which carry out specific tasks and chain them or create dependencies using these targets.

So the first target I’ve implemented is “Clean”

Target "Clean" (fun _ ->
   CleanDirs [buildDir; testBuildDir]
)

The Target name is “Clean” and we pass a function to it which is called when the target is run. We run the FAKE function CleanDirs to (you guessed it) clean the buildDir and testBuildDir folders.

The next target is the “Build” step as per the following

Target "Build" (fun _ ->
    !! solution
        |> 
        match mode.ToLower() with
            | "Release" -> MSBuildRelease testBuildDir "Build"
            | _ -> MSBuildDebug testBuildDir "Build"
        |> Log "AppBuild-Output"
)

Here we are creating an “file inclusion” using the value “solution”. Depending upon the selected “mode” we’ll either build in release mode or debug mode. At the end of the function we log the output of the build.

Next up we have a target for the unit tests. These were written using xUnit but FAKE has functions for NUnit, MSpec and MSTest (at the time of writing).

Target "Test" (fun _ ->
    testDlls
        |> xUnit (fun p ->
            {p with
                ShadowCopy = false;
                HtmlOutput = true;
                XmlOutput = true;
                ToolPath = xunitrunner;                
                OutputDir = testBuildDir })
)

As my project currently stores the test DLL’s in their own folder we pipe the testDlls value to xUnit function. Again this is using pattern matching to include all .Tests.dll files, which is the naming convention I’ve used for my tests. The xUnit function is then passed it’s parameters via the func p. In this code we’re going to create both HTML and XML output.

The final target is simply created to write a message out and not really required, but here it is anyway

Target "Default" (fun _ ->
   trace "Building and Running Csv.Data Tests"
)

Obviously we could extend this to output meaningful instructions on using the script if we wanted, for now it’s just used to write out “Building and Running Csv.Data Tests”.

The targets are completed at this point, but I want a basic dependency set-up so the build server can run all targets in order, thus cleaning the output folders, building the solution and then running the unit tests, so we write

"Clean"
==> "Build"
==> "Test"
==> "Default"

In this case, Default depends on Test being run, which depends on Build which in turn depends on the Clean target running. Hence when we run the Default target, Clean, Build and Test are executed in order then Default is run.

However it should also be noted that if we run a target, such as Build, FAKE also uses the dependency list, so shortens the dependency list to Build depending upon Clean. Hence running Build will actually also run Clean first.

The last line of the script is

RunTargetOrDefault "Default" 

This simply says if the user supplies the target then run it, or by default (when no target is supplied) run the Default target.

To run this script we use the following

Fake build.fsx          // runs the default target
Fake build.fsx Clean    // runs the Clean target
Fake build.fsx Build mode=Debug  // runs the Build target with the mode set to Debug

Currently it appears we cannot run a Target outside of the dependency list. In other words, let’s say we’ve built the code and just want to re-run the tests (maybe we build the code using Visual Studio and want a quick way to run the tests from the command line). Unfortunately I’ve not been able to find a way to do this. As mentioned earlier, running Test will run Clean then Build.