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
Makefilerequires 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 makemakemake 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
makehinders a command-runner. There it is allowed to not only pass variables, but also define a rule (the commands thatmakeorjustrun) just as a shell script instead. Including all usual environment variables. -
justruns from any sub-directory down the directory path that contains a just-file. Whilemakeonly search the current directory for itsMakefile, just reverses up the directory path until it finds its file.makeis a bit more noisy in that case and just flat out refuses to do anything, if it cannot find itsMakefilefile.
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.
