Getting started with FAKE - F# Make
In this tutorial you will learn how to set up a complete build infrastructure with "FAKE - F# Make". This includes:
- how to install the latest FAKE version
- how to automatically compile your C# or F# projects
- how to automatically resolve nuget dependencies
- how to automatically run NUnit tests on your projects
- how to zip the output to a deployment folder
Install the F# compiler
"FAKE - F# Make" is completely written in F# and all build scripts will also be written in F#, but this doesn't imply that you have to learn programming in F#. In fact the "FAKE - F# Make" syntax is hopefully very easy to learn.
Download Calculator Sample
Now download the latest FAKE-Calculator.zip from the FAKE project site. This sample includes 3 tiny projects and has basically the following structure:
- src/app
- Calculator (command line)
- CalculatorLib (class library)
- src/test
- Test.CalculatorLib
- tools
- NUnit
- FxCop
- build.bat
- build.fsx
- completeBuild.bat
- completeBuild.fsx
- Calculator.sln
Getting "FAKE - F# Make" started
In the root of the project you will find a build.bat file:
@echo off
cls
".nuget\NuGet.exe" "Install" "FAKE" "-OutputDirectory" "packages" "-ExcludeVersion"
"packages\FAKE\tools\Fake.exe" build.fsx
pause |
If you run this batch file from the command line then the latest FAKE version will be downloaded via nuget and your first FAKE script (build.fsx) will be executed. If everything works fine you will get the following output:
Now open the build.fsx in Visual Studio or any text editor. It should look like this:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: |
// include Fake lib #r @"packages/FAKE/tools/FakeLib.dll" open Fake // Default target Target "Default" (fun _ -> trace "Hello World from FAKE" ) // start build RunTargetOrDefault "Default" |
As you can see the code is really simple. The first line includes the FAKE library and is vital for all FAKE build scripts.
After this header the Default target is defined. A target definition contains two important parts. The first is the name of the target (here "Default") and the second is an action (here a simple trace of "Hello world").
The last line runs the "Default" target - which means it executes the defined action of the target.
Cleaning the last build output
A typical first step in most build scenarios is to clean the output of the last build. We can achieve this by modifying the build.fsx to the following:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: |
// include Fake lib #r "packages/FAKE/tools/FakeLib.dll" open Fake // Properties let buildDir = "./build/" // Targets Target "Clean" (fun _ -> CleanDir buildDir ) Target "Default" (fun _ -> trace "Hello World from FAKE" ) // Dependencies "Clean" ==> "Default" // start build RunTargetOrDefault "Default" |
We introduced some new concepts in this snippet. At first we defined a global property called "buildDir" with the relative path of a temporary build folder.
In the Clean target we use the CleanDir task to clean up this build directory. This simply deletes all files in the folder or creates the directory if necessary.
In the dependencies section we say that the Default target has a dependency on the Clean target. In other words Clean is a prerequisite of Default and will be run before the execution of Default:
Compiling the application
In the next step we want to compile our C# libraries, which means we want to compile all csproj-files under /src/app with MSBuild:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: |
// include Fake lib #r "packages/FAKE/tools/FakeLib.dll" open Fake // Properties let buildDir = "./build/" // Targets Target "Clean" (fun _ -> CleanDir buildDir ) Target "BuildApp" (fun _ -> !! "src/app/**/*.csproj" |> MSBuildRelease buildDir "Build" |> Log "AppBuild-Output: " ) Target "Default" (fun _ -> trace "Hello World from FAKE" ) // Dependencies "Clean" ==> "BuildApp" ==> "Default" // start build RunTargetOrDefault "Default" |
We defined a new build target named "BuildApp" which compiles all csproj-files with the MSBuild task and the build output will be copied to buildDir.
In order to find the right project files FAKEscans the folder src/app/ and all subfolders with the given pattern. Therefore a similar FileSet definition like in NAnt or MSBuild (see project page for details) is used.
In addition the target dependencies are extended again. Now Default is dependent on BuildApp and BuildApp needs Clean as a prerequisite.
This means the execution order is: Clean ==> BuildApp ==> Default.
Compiling test projects
Now our main application will be built automatically and it's time to build the test project. We use the same concepts as before:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: |
// include Fake lib #r "packages/FAKE/tools/FakeLib.dll" open Fake // Properties let buildDir = "./build/" let testDir = "./test/" // Targets Target "Clean" (fun _ -> CleanDirs [buildDir; testDir] ) Target "BuildApp" (fun _ -> !! "src/app/**/*.csproj" |> MSBuildRelease buildDir "Build" |> Log "AppBuild-Output: " ) Target "BuildTest" (fun _ -> !! "src/test/**/*.csproj" |> MSBuildDebug testDir "Build" |> Log "TestBuild-Output: " ) Target "Default" (fun _ -> trace "Hello World from FAKE" ) // Dependencies "Clean" ==> "BuildApp" ==> "BuildTest" ==> "Default" // start build RunTargetOrDefault "Default" |
This time we defined a new target "BuildTest" which compiles all C# projects below src/test/ in Debug mode and we put the target into our build order.
If we run build.bat again we get an error like this:
The problem is that we didn't download the NUnit package from nuget. So let's fix this in the build script:
1: 2: 3: 4: 5: 6: |
// include Fake lib #r "packages/FAKE/tools/FakeLib.dll" open Fake RestorePackages() // ... |
With this simple command FAKE will use nuget.exe to install all the package dependencies.
Running the tests with NUnit
Now all our projects will be compiled and we can use FAKE's NUnit task in order to let NUnit test our assembly:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: |
// include Fake lib #r "packages/FAKE/tools/FakeLib.dll" open Fake RestorePackages() // Properties let buildDir = "./build/" let testDir = "./test/" // Targets Target "Clean" (fun _ -> CleanDirs [buildDir; testDir] ) Target "BuildApp" (fun _ -> !! "src/app/**/*.csproj" |> MSBuildRelease buildDir "Build" |> Log "AppBuild-Output: " ) Target "BuildTest" (fun _ -> !! "src/test/**/*.csproj" |> MSBuildDebug testDir "Build" |> Log "TestBuild-Output: " ) Target "Test" (fun _ -> !! (testDir + "/NUnit.Test.*.dll") |> NUnit (fun p -> {p with DisableShadowCopy = true; OutputFile = testDir + "TestResults.xml" }) ) Target "Default" (fun _ -> trace "Hello World from FAKE" ) // Dependencies "Clean" ==> "BuildApp" ==> "BuildTest" ==> "Test" ==> "Default" // start build RunTargetOrDefault "Default" |
Our new Test target scans the test directory for test assemblies and runs them with the NUnit runner. FAKE automatically tries to locate the runner in one of your subfolders. See the NUnit task documentation if you need to specify the tool path explicitly.
The mysterious part (fun p -> ...) simply overrides the default parameters of the NUnit task and allows to specify concrete parameters.
Alternatively you could also run the tests in parallel using the NUnitParallel task:
1: 2: 3: 4: 5: 6: 7: |
Target "Test" (fun _ -> !! (testDir + "/NUnit.Test.*.dll") |> NUnitParallel (fun p -> {p with DisableShadowCopy = true; OutputFile = testDir + "TestResults.xml" }) ) |
Deploying a zip file
Now we want to deploy a *.zip file containing our application:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: |
// include Fake lib #r "tools/FAKE/tools/FakeLib.dll" open Fake RestorePackages() // Properties let buildDir = "./build/" let testDir = "./test/" let deployDir = "./deploy/" // version info let version = "0.2" // or retrieve from CI server // Targets Target "Clean" (fun _ -> CleanDirs [buildDir; testDir; deployDir] ) Target "BuildApp" (fun _ -> !! "src/app/**/*.csproj" |> MSBuildRelease buildDir "Build" |> Log "AppBuild-Output: " ) Target "BuildTest" (fun _ -> !! "src/test/**/*.csproj" |> MSBuildDebug testDir "Build" |> Log "TestBuild-Output: " ) Target "Test" (fun _ -> !! (testDir + "/NUnit.Test.*.dll") |> NUnit (fun p -> {p with DisableShadowCopy = true; OutputFile = testDir + "TestResults.xml" }) ) Target "Zip" (fun _ -> !! (buildDir + "/**/*.*") -- "*.zip" |> Zip buildDir (deployDir + "Calculator." + version + ".zip") ) Target "Default" (fun _ -> trace "Hello World from FAKE" ) // Dependencies "Clean" ==> "BuildApp" ==> "BuildTest" ==> "Test" ==> "Zip" ==> "Default" // start build RunTargetOrDefault "Default" |
The new Deploy target scans the build directory for all files. The result will be zipped to /deploy/Calculator.zip via the Zip task.
What's next?
If you want you could now add a FxCop target to your build in order to check specific naming rules or framework guidelines.
Full name: gettingstarted.buildDir
Full name: gettingstarted.testDir
Full name: gettingstarted.deployDir
Full name: gettingstarted.version