make
is a utility for automating builds. You specify the source and the build file and make
will
determine which file(s) have to be re-built. Using this functionality in make as an all-round tool for command
running as well, is considered common practice. Yes, you could write Shell scripts for this instead and they would be
probably equally good. But using make
has its own charm (and gets you karma points).
Even this Blog has a Makefile
file in its root directory that helps with creating posts, linting posts and
debugging all kind of problems.
Running the command make
in the root directory of this blog displays an overview of the available targets (as they are called in
the make-world).
$ make
Available targets:
help Show a list of available targets.
image_docker Built the docker container
image_podman Built the podman container
lint_all Lint all posts with markdown-lint(mdl) and markdown-link-check(mlc)
lint_post Lint a specific post. `make lint_post post=$filepath`
new_post Create a new and empty post: make new_post
preview_docker Run the blog with docker.
preview_podman Run the blog with podman
This is very useful for writing posts and stuff and ensures it is easy for everyone to post to this blog.
Make standard
They way make
works is, that it looks for a file Makefile
in the current directory, parses it and executes the defined
targets.
A simple example would look like this:
$ cat Makefile
bar: foo.c
cc foo.c -o bar
The first line contains the target bar
, followed by the dependency foo.c
. Compiling the file foo.c
with the output
set, will then create the file bar
(the target).
If we run it with (or without) the target name, this would be the output:
# with the target name
$ make bar
cc foo.c -o bar
Running it a second time will not do anything:
$ make bar
make: 'bar' is up to date.
The file bar
has already been created and since the file foo.c
has not change. the build process does not need to be
run again. In software development this is mainly used to compile the source code into binary files (or whatever is
needed) and to perform all kind of tasks.
Make non-standard
We can use this functionality to mis-use make
as a command runner as well.
With a different target definition we can run other commands and just skip the dependencies.
$ cat Makefile
echo_hello_world:
@echo "Hello world!"
Running this Makefile
using make
, we can output “Hello world!” or run any other command. If we take the target
image_podman
from the Makefile of the Techblog project, its structure is very simple.
image_podman:
buildah bud --build-arg UID=${UID} -t techblog -f .dev/Dockerfile .
Just running make
with this target will then create a Podman image for further testing and we don’t have to deal with
the whole command anymore.
$ make image_podman
buildah bud --build-arg UID=${UID} -t techblog -f .dev/Dockerfile .
Building...
[...]
But there are some limitations when using make
in general.
- Setting a variable is not straight forward and requires a different syntax than eg. when using
bash
. - Variables cannot be used between multiple lines/commands within the same target. Setting a variable in one line will not make it available in any following line. There are workarounds, though.
- The file
Makefile
requires hard tabs to be present when defining the commands below the target name. These are usually not visible and you have to know that in order to makemake
make thinks (fun intended, sry). - The syntax for variables is a little bit awkward. You have to mark them with a double dollar sign (
$$
) instead of a single one like e.g. inbash
. - A lot of other “specialities”.
Some of these characteristics are there for historically reasons, others just because. But using make
as command
runner while its original intention is to actually build stuff based on dependencies must be expected to come with some
form of punishment.
Just command runner
As it turns out using make
like this has become so popular, that other projects started developing tools for that
purpose only.
The tool just
is just one of them (HAHA!), but it stands for a number of tools with similar goals (see the list at the
bottom of this post).
To make it easy it is already in most Linux repositories available and can be quickly installed.
Then, similar to make
, you define your commands in a special file: .justfile
(there are alternatives).
In the case of just
, the syntax looks quite similar to what you have seen from make
so far.
# justfile
image_podman:
buildah bud --build-arg UID=${UID} -t techblog -f .dev/Dockerfile .
The invocation looks quite similar too:
$ just image_podman
Building...
[...]
Though I probably would choose a more fitting target name instead.
The feature of make
to be able to add dependencies is also possible in just
. But rather on depending on the
state of files in the file system (like a building system like make
requires), the dependencies of the command-runner
effect other recipes.
You could e.g. extend the image building process by adding a recipe to run the latest image after building it.
# .justfile
image_podman:
buildah bud --build-arg UID=${UID} -t techblog -f .dev/Dockerfile .
run_techblog: image_podman
podman run --rm -it localhost/techblog -t techbog
Running now just run_techblog
will first build the image again for Podman and then start the image once the building
process is completed.
Why
If just
does not make anything different than make
, why consider it instead?
The examples were showing only the similarities. The differences between make
and just
are as big/small as you would
expect from a building tool versus a command runner. An (incomplete) list:
-
Remember the phony section in every Makefile to define non-file based targets in
make
. Basically the feature that always defines a target as out-of-date and often abused for defaults? Well this is already builtin injust
# Output all targets default: just --list
-
Each command in make is treated separately. In order to pass variables from on command as input or parameter into the next line in a recipe, you have to abide certain rules, which make the life harder than necessary. What makes sense in
make
hinders a command-runner. There it is allowed to not only pass variables, but also define a rule (the commands thatmake
orjust
run) just as a shell script instead. Including all usual environment variables. -
just
runs from any sub-directory down the directory path that contains a just-file. Whilemake
only search the current directory for itsMakefile
, just reverses up the directory path until it finds its file.make
is a bit more noisy in that case and just flat out refuses to do anything, if it cannot find itsMakefile
file.
Alternatives
Here is a list of alternatives to just
and make
, partly copied from the Readme file of just
and extended
with more entries. There is certainly no shortage on building tools and command runners for every platform.
- aap: A recipe based tool for building and running commands.
- cargo-make: A command runner for Rust projects.
- doit: A Python based command-runner/building-tool.
- gulp: A Javascript based building framework.
- haku: A make-like command runner written in Rust.
- maid: A Markdown-based command runner written in JavaScript.
- make: The Unix build tool that inspired just. There are a few different modern day descendents of the original make, including FreeBSD Make and GNU Make.
- makesure: A simple and portable command runner written in AWK and shell.
- mask: A Markdown-based command runner written in Rust.
- microsoft/just: A JavaScript-based command runner written in JavaScript.
- mmake: A wrapper around make with a number of improvements, including remote includes.
- rake: A Make-like program implemented in Ruby.
- robo: A YAML-based command runner written in Go.
- scons: Cross-platform software construction tool.
- snakemake: Data-analysis tool that can be used like a command-runner.
- task: A YAML-based command runner written in Go.
- tup: A cross-platform file-based build system.
- waf: A building system.
I did not see any killer-feature (yet), that would let me recommend one tool over the other. But you see a lot of them flying around in the FOSS world and it certainly cannot hurt to get acquainted.