Replacing FAKE target strings with types
After years of usage I started to find FAKE’s Target
syntax less expressive
than it could be.
I developed multiple variants of a “Task” library to replace it that was available via gist, sometimes tweeting about it in answer to discussions but never clearly releasing it.
With FAKE 5 moving to small modules it’s the occasion to choose a good name
“BuildTask
” and release it on
NuGet with the source code on
GitHub.
What does BuildTask changes
Strongly-Typed targets
Target
has a very stringly-typed API, strings are used everywhere as identifiers, with the consequence that their
value is only verified when the script is executed.
Target.create "Default" (fun _ ->
(* ... *)
)
Target.runOrDefault "Defautl"
In BuildTask
each created task returns a value that represent it and the compiler check the value usage as with any
other variable.
let defaultTask = BuildTask.createFn "Default" [] (fun _ ->
(* ... *)
)
BuildTask.runOrDefault defaultTask
Default syntax using computation expressions
While FAKE Target.create
expect a function as parameter BuildTask.create
uses a computation expression to do the
same (Like Expecto), removing a little bit of verbosity for each target:
Target.create "Default" (fun _ ->
(* ... *)
)
BuildTask.create "Default" [] {
(* ... *)
}
Note: BuildTask.createFn
still provide the function-syntax if you need it.
Dependencies
The biggest change is the dependency syntax, Target
rely on operators chaining
(or methods doing the same) and the result looks good for linear dependencies:
"Clean"
==> "BuildApp"
==> "BuildTests"
==> "Test"
==> "PackageApp"
==> "Default"
==> "CI"
But the dependencies here are simplified, they don’t reflect the reality of target dependencies. Clean shouldn’t be mandatory except on CI, packaging the app shouldn’t mandate running the tests and building the tests shouldn’t require building the app.
The real dependencies are more like:
"Clean" =?> "BuildApp"
"Clean" =?> "BuildTests"
"Clean" ==> "CI"
"BuildApp" ==> "PackageApp"
"BuildTests" ==> "Test"
"PackageApp" ==> "Default"
"Test" ==> "Default"
"Default" ==> "CI"
And while the linear version was pretty readable, it isn’t the case here.
The reason is that we are defining a directed acyclic graph by listing its edges, but there is another text representation of a DAG that is much more readable: listing all preceding vertex for each vertex.
It’s a syntax that is already used by tools similar to FAKE like Gulp :
gulp.task("css", function() { /* ...*/ });
gulp.task("js", function() { /* ...*/ });
gulp.task("test", ["js"], function() { /* ...*/ });
gulp.task("default", ["css", "test"], function() { /* ...*/ });
BuildTask
uses a similar syntax:
let clean = BuildTask.create "Clean" [] { (* ... *) }
let buildApp = BuildTask.create "BuildApp" [clean.IfNeeded] { (* ... *) }
let packageApp = BuildTask.create "PackageApp" [buildApp] { (* ... *) }
let buildTests = BuildTask.create "BuildTests" [clean.IfNeeded] { (* ... *) }
let test = BuildTask.create "Test" [buildTests] { (* ... *) }
let defaultTask = BuildTask.createEmpty "Default" [packageApp; test]
let ci = BuildTask.createEmpty "CI" [clean; defaultTask]
Ordering
The direct consequence of the strongly-typed syntax along with the dependency syntax is that BuildTask
enforces a
strict ordering of the build script. Exactly as F# requires for functions inside a module.
While I feared this consequence at first, after converting quite a lot of FAKE scripts it’s easy to adopt and make the build script order more logical for developers familiar with F#.
Getting started samples
Here are a few of the samples on the FAKE getting started page ported to BuildTask
:
#r "paket:
nuget BlackFox.Fake.BuildTask
nuget Fake.Core.Target //"
#load "./.fake/build.fsx/intellisense.fsx"
open BlackFox.Fake
open Fake.Core
// Default target
let defaultTask = BuildTask.create "Default" [] {
Trace.trace "Hello World from FAKE"
}
// start build
BuildTask.runOrDefault defaultTask
CLEANING THE LAST BUILD OUTPUT:
#r "paket:
nuget BlackFox.Fake.BuildTask
nuget Fake.IO.FileSystem
nuget Fake.Core.Target //"
#load "./.fake/build.fsx/intellisense.fsx"
open BlackFox.Fake
open Fake.Core
open Fake.IO
// Properties
let buildDir = "./build/"
// Targets
let cleanTask = BuildTask.create "Clean" [] {
Shell.CleanDir buildDir
}
let defaultTask = BuildTask.create "Default" [cleanTask] {
Trace.trace "Hello World from FAKE"
}
// start build
BuildTask.runOrDefault defaultTask
Conclusion
I invite any FAKE user whenever they like the current syntax or not to try it (NuGet) and tell me what they think on twitter or in a GitHub issue.