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 subsequent 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.