As far as build systems are concerned, this article will not present anything particularly new – instead, we will focus on some piece of software that one might even considered “done”. “Done” in the sense that it is essentially bug-free and feature-complete. How many software products do you know (apart from TeX) that can be considered this way?
With a plethora of “new”, “modern” or just plain “fancy” build systems, GNU Make is the old veteran of them all. But why should you care? It is old, it seems clunky at first sight, it is just not javascript – why the hell should we bother?
GNU Make is just plain fantastic, so let’s put this old fella to a test and see what it has up its sleeves!
© xkcd
But before sending the old veteran to compete in a classic example (make sandwich) with the youngsters, let’s have a look whether build system X actually plays in the same league and what makes make so special:
- Does build system X offer auto-threadability? (make does)
- Is system X as near to the shell as it can get? (make is)
- Can it auto-resume on tasks? (make can)
- Can it intelligtenly figure out ways to find a faraway goal, where you haven’t given it direct clues on how to get there? (make will)
- Is it as concise as possible, while maintaining a maximum of flexibility and thus can be considered “mighty”? (hell yes!)
- Can it be called “declarative”? (make is the definition of declarative!)
Sounds too good to be true? Then, let’s visit a classic example, highlighting a few key concepts on the way: let it make a sandwich for us!
Let’s start out very simple and let us – in best declarative manner – define how our sandwich is to be built from the ground up.
Well actually: we’re coming from the end result. A sandwich is made of sliced bread, butter, ham, sliced tomatoes and a little salad (preferably washed).
Let’s write a file named Makefile
and declare our rules:
sandwich: sliced.bread washed.salad sliced.tomatoes butter ham
echo sliced.bread washed.salad sliced.tomatoes butter ham > sandwich
Now, how do we instruct make to make us a sandwich?
Given you have provided the ingredients, issue a simple make sandwich
on the commandline and there you go!
$ make sandwich
$ ls
[...] sandwich
Since sandwich
is the first and only target we have defined in our Makefile, we might also just say make
– and make will pick up the first target it finds. To make sure we will always get a sandwich when we just say make
, let’s set up a default target which we will keep on top of our Makefile, and, for simplicity’s sake, call it default
:
default: sandwich
sandwich: sliced.bread washed.salad sliced.tomatoes butter ham
echo sliced.bread washed.salad sliced.tomatoes butter ham > sandwich
Since we’re at it, let’s shorten this down a little and get to know our first funky variable, $^
:
default: sandwich
sandwich: sliced.bread washed.salad sliced.tomatoes butter ham
echo $^ > sandwich````
`$^` will hold the names of all _prerequisites_ of our _target_, namely: `sliced.bread`, `washed.salad`, `sliced.tomatoes`, `butter`, and, finally `ham`.
The _rule_ or _recipe_ of our target will paste all of these into our yummy-file called `sandwich` using `echo`.
Let’s assume we only have bread (in our case a file called `bread`), not yet “sliced”. How do we declare how to slice the bread? We will assume that renaming the file `bread` to be `sliced.bread` will do the job in this case:
sliced.bread: bread
mv bread sliced.bread
So `sliced.bread` will be made out of `bread` by renaming it. Nice. Let’s see if it works, by creating a file called bread, and then having it sliced for us through _make_:
$ touch bread $ make sliced.bread $ ls sliced.bread
Let’s follow right up and do the same for the tomatoes:
sliced.tomatoes: tomatoes mv tomatoes sliced.tomatoes
See the pattern? Let’s go ahead, DRY this out a little and refactor, so we can roll this same pattern (slicing) into one rule for both ingredients: tomatoes _and_ bread. On the way we will yet again meet funky variables (also called “[Automatic Variables][1]“) and, through the refactoring, arrive at a so called “[Pattern Rule][2]“.
sliced.%: % mv
lt; $@
Here we go: both rules rolled into one! A “sliced something” is made by taking something and slicing (in our case renaming) it from something to sliced.something! Easy, huh?
The percent sign %
in this case is very similar to the shell’s globbing *
. Its name here is %
however, so that it doesn’t collide with the shell’s glob-*
.
Now how about $<
and $@
? As one can easily infer from our former rules, $<
will hold the prerequisite‘s name (bread
in one case, tomatoes
in the other), while $@
will hold the target‘s name: sliced.bread
or sliced.tomatoes
respectively.
Very well, now that we can slice bread as well as tomatoes, let’s move on! Washing the salad (or anything else) should be easy enough now:
washed.%: %
mv
lt; $@
How about we loosen up a little and make the washing itself a little more configurable? Maybe someone will come along and would like to wash the ingredients differently rather than with mv
? We can certainly account for that, so let’s declare the washing mechanism a variable:
WASH ?= mv
washed.%: %
$(WASH)
lt; $@
Excellent! Make now knows how to make sliced.bread
from bread, sliced.tomatoes
from tomatoes and washed.salad
from salad!
What if we were to copy rather than move in order to wash the salad and how would we go about it? Again, this task is trivial: we simply set the variable that we just declared in order to change the executing mechanism by setting the variable WASH
to cp
in our environment:
$ make washed.salad WASH=cp
cp salad washed.salad
Beautiful! Now what if we would like to wash the salad again?
$ make washed.salad WASH=cp
make: 'washed.salad' is up to date.
Obviously, there is no need to wash the salad again and make informs us that everything was done already: clever! Make will figure out that there is nothing to do by inspecting the timestamps: the file washed.salad
is younger than the salad
so there can’t be any possible changes that would force us to do the same work another time.
To wrap up for this first episode, let’s see what we have up to now and continue to have make do the groceries for us in our next installment:
WASH ?= mv
default: sandwich
sliced.%: %
mv
lt; $@ washed.%: % $(WASH)
lt; $@ sandwich: sliced.bread washed.salad sliced.tomatoes butter ham echo $^ > sandwich
Until we have taught make how to do groceries always make sure you have the right ingredients prepared for it by creating the necessary files using touch bread salad tomatoes butter ham
.
Continue reading: GNU make – an oldie but goldie (Part II)