I ought to say that this isn’t a post about TDD, Mocks, Stubs, Fakes, Dependency Infection and the like. This is a post about F# Fake and specifically my experience of using it.
Before that though….
Build automation tools aren’t new. Stuart Feldman is credited as being the creator of the long serving Make way back in 1977, something that he received the ACM Software System Award for in 2003. There have been numerous derivatives of Make since it’s creation, the most notable being GNU Make. Make clearly had a significant impact and unsurprisingly Makefiles are still used in anger today, whether hand crafted or output from other tools such as CMake .
Accompanying the growth in languages is an ever expanding plethora of build automation tools. Apache Ant was the first tool that I came across during my uni days and was released sometime in 2000, Maven following some years later (2004 I believe). Java, Scala, Python developers (amongst others), who favour DSL’s over Xml scripts, now have the likes of Gradle, which I believe was released in 2012. The Clojure folk have leiningen which according to github made it’s 1.0.0 milestone back in December 2009. Scala has SBT, Elixir has Mix and Ruby has Rake. The list goes on and on but suffice to say that most modern languages benefit from some form of build automation tooling, developed to simplify the process of repeatedly producing working software.
Build Automation for .net
In the .Net space we’ve depended on MSBuild and Xml based build scripts for a long time, some would say too long. My first foray with MSBuild came when I worked on a team porting a classic asp site to Asp.net, some time after .Net 1.0 was released in 2002 (yes, I’m that old). Over time I’ve earned my build badges, writing MSBuild scripts, extending scripts, writing custom MSBuild tasks, editing .csproj files to invoke some custom process or make use of an MSBuild community extension or two. Many moons ago I also experienced the pain of trying to work with NAnt. Needless to say, my experiences have taught me that Xml is not the best language for writing build scripts.
In my recent past I’ve worked on .net projects that used Rake (without Albacore) though I had little need to extend or refactor the existing scripts. I’ve had a brief fondle of PSake and in spite of having a love/hate relationship with PowerShell I found it yield some good results. I recently came across Cake, a C# cross platform dsl which looks interesting, though by the time I found it I’d decided that I was going give F# Fake a spin.
F# Fake – Tell me more..
Get to the point I hear you say. I’ve been dabbling with F# Fake for several months now and I have to say I’m taken with it. Here’s why.
- Shallow learning curve : The Fake github page suggests that you don’t need to learn F#. I’d qualify this further by saying that you don’t need any prior experience of F# before you start with Fake and that any knowledge you gain is just enough, targeted at simplifying your build automation. Our team is made up of members with varying degrees of commercial engineering experience, but we haven’t yet had anyone shy away from updating the build scripts.
- Strong community : There’s a strong community around F# in general and Fake unsurprisingly benefits from that. The chances are therefore that someone else has already written the modules and functions that you need. There’s a huge array of functions already available. As an example you have access to functions to work with IIS, Sql Server, Nuget, OctopusDeploy, TeamCity, the file system, Raygun, Chocolatey, Slack, Azure… The list goes on, though if you find a gap you can easily submit a pull request. Furthermore, there’s a range of open source projects using it, so there are some good examples already out that serve as a reference.
- Task chaining : This is really a pre-requisite of any build dsl. Rake has it, PSake has it and unsurprisingly F# Fake has embraced it. One of things I’ve found particularly useful with Fake is to use optional dependencies.
Here’s a gist that, although a little trite, demonstrates how to use task chaining with optional dependencies.
| #r "packages/FAKE/tools/FakeLib.dll" | |
| open Fake | |
| Target "FirstTask" (fun _ -> | |
| printf "Executing first task" | |
| ) | |
| Target "SecondTask" (fun _ -> | |
| printf "Performing second, optional task" | |
| ) | |
| Target "FinalTask" (fun _ -> | |
| printf "Performing final task" | |
| ) | |
| (* Reading the task chain below from the bottom up reads as : | |
| 1) Final Task depends on SecondTask | |
| 2) SecondTask will run before FinalTask but only if Fake receives a build paramter of PerformSecondTask | |
| 3) FirstTask is the task that will run first *) | |
| "FirstTask" | |
| => ("SecondTask" (hasBuildParam "PerformSecondTask")) | |
| => "FinalTask" | |
| RunTargetOrDefault "FinalTask" |
- You get the benefits of F# : I’m not an functional language or F# fan boy (not yet), but I can see that our build tasks are concise, far more than would have been possible with C#. The declarative nature makes it easy maintain and easy to reason about, not mention the benefits of piping and functional composition. Incidentally this doesn’t contradict my first point. You can start without any prior knowledge of F#, but you’ll learn bits of F# by osmosis, which in turn will lead you to more refined build scripts.
Where are the examples?
I can sing about the merits all day, but I’m sure you’d like some concrete examples of how we use it.
Context : The system that I’m working on is a replacement of a legacy system (simplified view of course). We’ve elected to adopt CQRS + EventSourcing since the application is read heavy in parts and we’re releasing the new services piecemeal (EventSourcing makes it easy for us to frequently replay events as we solve new problems and add features). We have frequently had to migrate data from the old system and we now frequently replay the events. That’s 10,000 foot view anyway.
Here is an extract (anonymised) from the build script that we use at work. It’s simple enough to understand, but demonstrates something we did as a dev team many times over. That is, we migrated data from the old system, firstly compiling the code and then dropping two databases.
| //Inlude referenced libraries | |
| #r ".\packages\Fake\tools\FakeLib.dll" | |
| #r ".\packages\Fake.SQL\Tools\Fake.SQL.dll" | |
| open Fake | |
| open Fake.Sql.SqlServer | |
| let defaultMigrationTimeOut=10 | |
| //omitted full connection string details, but would be required | |
| let eventStoreServerInfo="Data Source=.\;Database=EventStore;......" | |
| let projectionStoreServerInfo="Data Source=.\;Database=ProjectionStore...." | |
| //function to return build defaults that are passed to MSBuild on compile | |
| let setBuildParams defaults = | |
| { defaults with | |
| Verbosity = Some(Quiet) | |
| Targets = ["Build"] | |
| Properties = | |
| [ | |
| "Optimize", "True" | |
| "DebugSymbols", "True" | |
| "Configuration", buildMode | |
| ] | |
| } | |
| //Funcion that wraps a call to Migrate.exe, passing arguments, | |
| //outputing text and setting a timeout which is assigned at the top of the script | |
| //Migrator.exe was written by us to assist with data migration from legacy system. | |
| let migrate = (fun (arguments:string) -> | |
| printf "Executing migration with arguments : %s" arguments | |
| //ExecProcess is is a function in the Fake.ProcessHelper module | |
| //http://fsharp.github.io/FAKE/apidocs/fake-processhelper.html | |
| ExecProcess (fun info -> | |
| info.FileName <- ".\src\ProjectNameHere\Migrator\bin\debug\Migrator.exe" | |
| info.Arguments <- arguments) (TimeSpan.FromMinutes defaultMigrationTimeOut) | |
| ) | |
| //Build target, calls build function against sln file, | |
| //passing params returned from setBuildParams | |
| Target "Build" (fun _ -> | |
| build setBuildParams "src/ProjectNameHere.sln" | |
| ) | |
| //A target to drop the event store database and projection store database | |
| //DropDb is a function in Fake.Sql.SqlServer | |
| //DropDb function returns ServerInfo type | |
| //|> ignore is required, since unit type must be returned, not ServerInfo. | |
| Target "DropDatabases" (fun _ -> | |
| DropDb eventStoreServerInfo |> ignore | |
| DropDb projectionStoreServerInfo |> ignore | |
| ) | |
| //Migrate target, in turn calls migrate function higher up in script. | |
| //Arguments passed to migrate function are example, not real world params | |
| Target "Migrate" (fun _ -> | |
| printf "----Starting migration-------" | |
| migrate("--dataSet=A --fromSource=B") | |
| migrate("--dataSet=B --fromSource=B") | |
| migrate("--dataSet=C --fromSource=A") | |
| ) | |
| //Target chaining. | |
| //Reads as : Call Migrate target, but before that call DropDatabases target, | |
| //before that call Build target | |
| "Build" | |
| => "DropDatabases" | |
| => "Migrate" | |
| //If not target pass to Fake.exe, call build by default | |
| RunTargetOrDefault "Build" |
Summary
Because of the nature of the project that we’re working on, we’ve found that we have a need to repeat certain tasks, tasks that are often multi step and potentially error prone. The tasks that we wrote ranged from combining and executing commands against our source control, file manipulation, reading data from xml files using xml TypeProvider, sql tasks, compiling code, test execution, copying files and process execution. And inevitably, combinations of these tasks in task chains. F# Fake has made automating such tasks simple. It’s very reliable, it’s been a pleasure to work with and has also yielded positive results very quickly.
I’ve recently taken to using it on personal projects and have used it for simple tasks with both TeamCity and AppVeyor. My experience to date has been a positive one, so if you haven’t already had a dabble with F# Fake, I’d certainly recommend giving it try.