Automating Go Builds with Task
- 5 minutes read - 1001 wordsThis is the third in a series of articles about writing a small reading list app in Go for personal use.
Task is a task runner that aims to be simpler than make.
Task is portable, easy to install, and has slightly better support for skipping tasks when dependencies haven’t changed than mage or goyek.
Disclaimer: I’m a Skeptic
As I said in the first post in this series, I’m skeptical of build tools that claim to improve on make. Task simplifies some things, but I’m not really convinced it’s an improvement overall.
Translating a Makefile into a Taskfile
Just like mage, it was mostly straightforward to do a target-by-target port of aklatan’s Makefile to Task.
# https://taskfile.dev
version: '3'
output: 'group'
shopt: [globstar]
vars:
PROJECT: aklatan
COVEROUT: "./.coverage/{{.PROJECT}}.out"
COVERHTML: "./.coverage/{{.PROJECT}}.html"
tasks:
default:
deps: [lint, check, "{{.PROJECT}}", importer]
lint:
cmds:
- golangci-lint run --timeout 180s --skip-dirs=rules
sources:
- Taskfile.yaml
- ./**/*.go
aklatan:
cmds:
- go build .
sources:
- Taskfile.yaml
- ./**/*.go
- ./static/**/*
- ./templates/**/*.html
generates:
- aklatan
method: timestamp
buildcmd:
vars:
CMDNAME: '{{default "dummy" .CMDNAME}}'
cmds:
- "go build ./cmd/{{.CMDNAME}}"
sources:
- Taskfile.yaml
- ./**/*.go
generates:
- "{{.CMDNAME}}"
method: timestamp
importer:
cmds:
- task: buildcmd
vars: {CMDNAME: "importer"}
check:
deps: [coverdir]
cmds:
- go test $TESTFLAGS -coverprofile={{.COVEROUT}} .
sources:
- Taskfile.yaml
- ./**/*.go
generates:
- "{{.COVEROUT}}"
run: once
method: timestamp
# Intended to be run manually; not part of the default list.
cover:
deps: [check, coverhtml]
cmds:
- echo "Checking overall code coverage..."
- "go tool cover -func {{.COVEROUT}} | sed -n -e '/^total/s/:.*statements)[^0-9]*/: /p'"
silent: true
coverhtml:
deps: [check]
cmds:
- "go tool cover -html={{.COVEROUT}} -o {{.COVERHTML}}"
sources:
- "{{.COVEROUT}}"
generates:
- "{{.COVERHTML}}"
status:
- test -f report.xml
coverdir:
cmds:
- mkdir -p .coverage
status:
- test -d .coverage
run: once
# Intended to be run manually; not part of the default list.
# XXX This only works if go-junit-report is installed. It's not part of
# go.mod because I don't want to force a dependency, but it is part of
# the ci docker image.
reportxml:
preconditions:
- which go-junit-report
cmds:
- "go test $TESTFLAGS -v . 2>&1 | go-junit-report > report.xml"
- "go tool cover -func {{.COVEROUT}}"
sources:
- Taskfile.yaml
- ./**/*.go
generates:
- "report.xml"
status:
- test -f report.xml
# Intended to be run manually; not part of the default list.
# XXX This only works if semgrep is installed. Run by ci as a separate
# job because semgrep is part of a separate docker image.
semgrep:
preconditions:
- which semgrep
cmds:
- semgrep --error --config rules/ --metrics=off
sources:
- Taskfile.yaml
- ./rules/**/*.yaml
- ./**/*.go
generates:
- .semgrepok
One thing you might immediately notice is that this is fairly verbose. I know a lot of people have a weird visceral hatred for Makefile syntax, and I empathise with that, but I’m really not sold on yaml as an improvement.
While doing this port I attempted to use some fancy yaml syntax to reduce some of the repetition – especially where the sources lists need to be repeated in multiple places. Unfortunately the yaml parser that Task uses doesn’t support the necessary fancy syntax. If I was going to use Task for anything more than just a toy app I would probably build a translation tool so that I could write a more concise description and generate the Taskfile.yaml – or hack some extra features into Task so that the input file doesn’t have to be quite so verbose.
The Good Parts
Task is a big step up from Goyek. Using yaml for the Taskfile makes it easier to read the build specification – I think this is an improvement over both Mage and Goyek.
Task supports running build steps in parallel.
Task’s documentation is very good.
The handling of dependencies and deciding when to rebuild is fairly
flexible: Task will rerun a build task based on a change to either the
content (checksum) of the inputs or the timestamps of the inputs. It also
has a status
hook to run a command that can decide whether to rerun a
task. For example, in the Taskfile above I used test -d .coverage
in a
status
hook to avoid running mkdir
if the directory already exists.
(This is a trivial usage that doesn’t really save any time, but it’s
illustrative of how to use the status
mechanism.)
Task has support for including separate Taskfiles (e.g. from other directories) into the main Taskfile, but I didn’t experiment with this functionality.
I thought the defer mechanism was kind of cool. I’m not sure how often I would actually use this, but I suspect I’ve probably worked around a lack of this feature in Make more than once in the past.
What’s Missing
Task doesn’t have good mechanisms for reducing the verbosity of the Taskfile. It would be great if I could define a list of files in some kind of variable at the top of the file, and then refer to that variable in multiple tasks as sources. This would make it so that I only have to edit the sources list in one place.
Bottom Line
Of the three tools reviewed so far, I think I would choose Task over Goyek and Mage. However, Task doesn’t really seem suited for anything industrial-strength. If I had more than a few dozen files, or a handful of directories, or a moderately complex task graph, I would probably fall back to make, or look at one of the newer heavy-duty build tools like buck or pants.
Next Time
As it happens, I’ve been looking at pants, so the next post in this series will focus on that build system. (Maybe multiple posts? It’s a big and fairly complex build system.)