Make vs. Taskfile vs. Justfile
Go's feature-rich toolchain is all you need to build, test, or benchmark a Go module. This is the end of the Spotlight, have a good day.
Oh, wait, I get that complex build scenarios call for specialized build tools. If a build consists of more than a single, pure-Go module, many developers and teams turn to the good old make
tool.
make
exists for almost half a century. Created in 1976, it was designed for building C apps and libraries on Unix. Its build mechanism is universal enough for use with other languages and even for automating tasks that are not strictly labelled as build tasks.
However, make
has its quirks and limitations that make developers look for alternatives. Two modern, make
-like tools are Taskfile and Justfile.
I could compile a long feature comparison between the three, but this is a Spotlight, hence short. So, instead, I want to focus on the most important feature and key differentiator:
Managing dependencies.
A build tool should avoid unnecessary work by building only those targets whose dependencies were updated since the last build. It turns out that each of the three tools has a unique approach to change detection.
Make
The make
tool detects changes by looking at the timestamp of the files a target depends on.
The following Makefile compiles hello.c
in to a hello
binary if hello.c
is newer than the binary:
hello: hello.c
cc hello.c -o hello
This is a simple and robust approach that works well on local filesystems… usually.
However, file timestamps can be unreliable for various reasons. System inconsistencies, time zone changes, or tools that manipulate timestamps can mislead make
.
Taskfile
Taskfile is a task runner and build tool written in Go, aiming to be more readable than the Makefile syntax. Taskfile has
a different method of detecting dependency updates: Instead of looking at timestamps, Taskfile takes checksums of each dependency and saves them to the local .task directory. On subsequ
ent builds, Taskfile compares current checksums with the saved ones.
This strategy circumvents the unreliable timestamp method, although Taskfile has an option for comparing by timestamp if desired.
Here is a Taskfile that runs go test
only if the sources have changed:
version: '3'
tasks:
test:
desc: Run tests when source files change
sources:
- '**/*.go'
- '**/*_test.go'
cmds:
- go test ./...
The first invocation runs the tests, the second one says that everything is up to date:
> task test
task: [test] go test ./...
ok hello 0.251s
> task test
task: Task "test" is up to date
Taskfile does not get confused by tweaked timestamps:
> touch hello.go
> task test
task: Task "test" is up to date
The test would not run unnecessarily, which is great for long-running unit or integration tests.
Justfile
The third contender is Justfile. It's syntax is closer to the Makefile syntax than Taskfile's syntax; yet, Jusfile advertises itself as a command runner and not a build tool. Consequently, Justfile does not have, nor check, file dependencies. However, Justfile's recipes can depend on other recipies, which are executed unconditionally.
This discovery was a bit of a surprise when I evaluated Justfile as a make
alternative; however, as a plain command runner, Justfile does a nice job.
Conclusion
Make is the grande dame of build systems and still the #1 choice for many projects. However, if reliance on timestamps is a problem, Taskfile is a top alternative. Justfile turned out to focus on managing and running scripted commands without dependency checks. I decided to include it here as this aspect may not be apparent until carefully searching through the documentation to determine the absence of dependency checking mechanisms. If this info saves you time, mission accomplished.
The above list is not complete by far. Quite a few make
alternatives, build tools, and task runners have been created over the years, including the following ones:
Mage
Mage, a pure-Go build tool, lets you write task runners and build steps in your favorite language, Go.
Like Just, Mage does not check file timestamps natively. Instead, it comes with a helper package
target
that provides functions for checking if sources have been modified more recently than the destination.
Earthly
Earthly describes its Earthfile format (and functionality) as “like Dockerfile and Makefile had a baby”.
Earthly uses containers to achieve replicable builds across platforms. It also features concurrent execution of independent build tasks, caching of build artifacts, and reuse of targets, artifacts, and images across Earthfiles.
Bazel
Bazel is a Java-based build system using an imperative build language (Starlark).
Featuring incremental builds, remote build execution, cross-platform support, and scalability, it is popular not only for Java but also for multi-language projects.
Nothing
Many Go projects are small-scale projects that don't need fully-fledged build systems. Remember that the Go toolchain includes powerful tools for building, installing, and (package) dependency management already.
Sometimes, go install
is all you need.