Article summary
Make is the classic build tool and task runner, originally intended for use with C but useable in any language environment. It is powerful, but its syntax is not highly readable. My search for a general-purpose task runner led me to Just, so I thought I’d see how it stacks up against Make.
Requirements
A task runner is used far more frequently than it is modified. As a result, the syntax should be simple, and easy to read and understand at any time. When it comes time to add or change a task — probably a few months since the last time — you’ll have forgotten the idiosyncrasies of the task file syntax.
This is possibly less of an issue if your task runner uses the same language as your project. For instance, grunt
and gulp
both use Javascript for their task file specification. But this can backfire as the task file accumulates plugins and other random bits of Javascript pasted from StackOverflow. There is something to be said for simplicity.
make
uses a lot of terse, symbol-based identifiers like $@
and @<
. This is fun when you’re writing the makefile but less so when you’re trying to read it. just
has some built-in functions that are not super discoverable when writing a justfile but much more readable later on.
A task runner should work well both in a CI pipeline and on a developer’s machine. For CI, this means it should be easy to install. just
is available in popular package managers. For a developer, it should have syntax highlighting and shell completion. just
has plugins for and has these available for common editors and shells.
Parameterization
Targets in a makefile are not easily parameterized. Say you have a deploy
task that can target either the “staging” or “prod” environment. You could set an environment variable when running make, like DEPLOY=staging make deploy
. Or you could duplicate static targets, like make deploy-staging
, make deploy-prod
, etc.
This is not terrible, but just
cleans it up a bit by supporting parameters for targets. So you can have a single deploy
target that takes an environment name as a parameter. Running it is simply just deploy staging
. A bonus is that it’s self-documenting. When you run just --list
, the deploy
task will be listed once along with its parameter names.
Task Dependencies
Ideally a task runner will help you focus on outputs rather than on dependencies. If you want to build the application, you should be able to just run build
. You shouldn’t have to run a separate task first to produce a shared library, a separate task for generated code, etc.
Both make
and just
use the same syntax for specifying dependencies. The difference is that in a makefile, a target could be either a static target or a filename. If the name matches a file that exists, make
will skip the target if its dependencies are up-to-date. To force it to be a static target, you have to add it as a dependency of the kludgey .PHONY
target. just
avoids this by making all targets static targets (just
does not try to determine if targets are up-to-date).
Incremental Builds
This behavior of only running a target if it is out-of-date (a.k.a. incremental build) is actually one of the most compelling features of make
. But it also blurs the line between “task runner” and “build tool.” make
could be considered both a build tool and a task runner, whereas just
is solely a task runner. This is not such a big deal, since more specific build tools probably have a better idea of what is considered “out of date”.
If this functionality is needed in just
, it’s not too hard to incorporate. For simple cases, a shell built-in can do the timestamp comparison, like test
in bash: test $INFILE -nt $OUTFILE && codegen <$INFILE >$OUTFILE
. More complex cases could be handled using find
with its -newer
flag. But to keep things readable, a utility like checkexec may be better.
Just vs. Make
It’s probably too early to say that just
will replace make
, but in my current usage I haven’t found anything that I desperately miss from make
! Next time you’re thinking of writing a makefile, give just
a try.