Write Build Tasks in Go with Gotask
One of the things that I miss a lot when programming in Go is being able to write build tasks in Go itself. The de facto build tool for Go is make. Make is simple, classic and gets the work done. But it falls when build tasks are becoming complex. Another drawback with make is that it’s completely isolated from the host language: there’s no way to import and make use of any Go code. Build tools like ant or maven also suffer for similar reasons.
External DSL vs. Internal DSL
The problems with these build tools come from the fact that they use an external Domain Specific Language (DSL) to describe build tasks. Programming languages like Ruby, on the other hand, adopt an internal DSL approach, à la rake. It allows you to express build tasks in the host language. For reference, Martin Fowler has a very good article on internal DSL and external DSL.
Idiomatic Build Tool in Go
Go, as a statically-typed language, is not designed to write good DSLs.
Andrea Fazzi has written a blog post on whether Go is suitable for building DSL by comparing it to Ruby.
The conclusion, without surprise, was Go is not good at building complex and general purpose DSLs.
However, we don’t necessary need to bend the Go syntax in order to make an idiomatic build tool.
An everyday Go example is go test
. Consider the following test function in a file called time_test.go
:
func TestTimeConsuming(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
...
}
Running go test
will pick up this test file and execute the test function.
It feels very DSLish yet we’re not bending the syntax like we usually do for internal DSLs.
The trick behind go test
is convention over confication:
if you create a file called xxx_test.go
and name the test function TestXxx
, where xxx
is the test name,
your test will be run.
We could do something similar for an idiomatic build tool in Go: introducing gotask.
Gotask
Consider the following task function in a file called sayhello_task.go
:
// +build gotask
package main
import (
"github.com/owenthereal/gotask/tasking"
"os/user"
"time"
)
// NAME
// say-hello - Say hello to current user
//
// DESCRIPTION
// Print out hello to current user
//
// OPTIONS
// --verbose, -v
// run in verbose mode
func TaskSayHello(t *tasking.T) {
user, _ := user.Current()
if t.Flags.Bool("v") || t.Flags.Bool("verbose") {
t.Logf("Hello %s, the time now is %s\n", user.Name, time.Now())
} else {
t.Logf("Hello %s\n", user.Name)
}
}
Running gotask -h
will display all the tasks:
$ gotask -h
NAME:
gotask - Build tool in Go
USAGE:
gotask [global options] command [command options] [arguments...]
VERSION:
0.8.0
COMMANDS:
say-hello Say hello to current user
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--generate, -g generate a task scaffold named pkg_task.go
--compile, -c compile the task binary to pkg.task but do not run it
--debug run in debug mode
--version print the version
--help, -h show help
Running gotask say-hello -h
will display usage for a task:
$ gotask say-hello -h
NAME:
say-hello - Say hello to current user
USAGE:
command say-hello [command options] [arguments...]
DESCRIPTION:
Print out hello to current user
OPTIONS:
--verbose, -v run in verbose mode
--debug run in debug mode
To execute the task, type:
$ gotask say-hello
Hello Owen Ou
To execute the task in verbose mode, type:
$ gotask say-hello -v
Hello Owen Ou, the time now is 2013-11-20 15:32:00.73771438 -0800 PST
Yes, that’s gotask, an idiomatic way of writing build tasks in Go.
Convention over Configuration
Similar to defining a Go test, you follow the gotask
convention to describe your build tasks.
You create a file called TASK_NAME_task.go
and name the task function in the format of
// +build gotask
package main
import "github.com/owenthereal/gotask/tasking"
// NAME
// The name of the task - a one-line description of what it does
//
// DESCRIPTION
// A textual description of the task function
//
// OPTIONS
// Definition of what command line options it takes
func TaskXxx(t *tasking.T) {
...
}
where Xxx
can be any alphanumeric string (but the first letter must not be in [a-z]) and serves to identify the task name.
By default, gotask
will dasherize the Xxx
part of the task function name and use it as the task name.
The // +build gotask
build tag constraints task functions to gotask
build only. Without the build tag, task functions will be available to
application build which may not be desired.
Comments as Man Page™
It’s a good practice to document tasks in a sensible way.
In gotask
, the comments for the task function are parsed as the task’s man page by
following the man page layout:
Section NAME contains the name of the task and a one-line description of what it does, separated by a “-“;
Section DESCRIPTION contains the textual description of the task function;
Section OPTIONS contains the definition of the command line flags it takes.
Task Scaffolding
gotask
is able to generate a task scaffolding to quickly get you started for writing build tasks by using the --generate
or -g
flag.
The generated task is named as pkg_task.go
where pkg
is the name of the package that gotask
is run:
// in a folder where package example is defined
$ gotask -g
create example_task.go
Compiling Tasks
gotask
is able to compile defined tasks into an executable using go build
.
This is useful when you need to distribute your build executables.
For the above say-hello
task, you can compile it into a binary using --compile
or -c
:
$ gotask -c
$ ./examples.task say-hello
Hello Owen Ou
Conclusion
With gotask
, you’re able to write idiomatic Go code for build tasks.
In the future, I would hope gotask
can be part of the Go toolchain, so that you can simply type go task
without installing another tool.
If you’re a Go committer reading this blog post and are convinced that gotask
worths being port to the Go toolchain, feel free to ping me. I’m happy to help out with the integration :).
If you’re a user of gotask
and would like to help out with the development, the project page is here.
Happy tasking!