GNU make – an oldie but goldie (Part II)

In our [last installment][1] we presented a very simple Makefile and taught make how to make sandwich, visiting a few key concepts on the way (automatic variables, pattern rules). For our toolchain to adapt to different situations (out of bread or out of butter? go do the groceries!) and to present some more concepts let’s teach make a few more tricks.

Before a good chef starts working in the kitchen he will make sure the kitchen is nice and clean, so let’s teach make how to clean the kitchen (while conforming to the best practices outlined by the make documentation as well as the user’s expectation. To cut a long story short, let’s write a target named clean-up-kitchen! and an alias named clean:

clean-up-kitchen!:
  rm -f sliced.bread washed.salad sliced.tomatoes butter ham

clean: clean-up-kitchen! 

The exclamation mark in the target‘s name doesn’t carry meaning, it is only there to show that this in fact is a valid character in a target’s name. Actually, targets are very open to characters you wouldn’t expect to work. You are free to use almost every character (thus forming “patterns” in your makefile, such as “private”, internal targets vs. public-facing targets), except % (see last installment), * and :.

The target clean: clean-up-kitchen! without a recipe can be seen as a simple alias in this case: If clean is called, call clean-up-kitchen!. If clean-up-kitchen! is fulfilled, clean will be fulfilled, too, without a recipe.

In one of the targets we have used in our last installment, slicing the bread, we defined another prerequisite, bread.

sliced.bread: bread
  mv 
lt; $@ 

What if bread is missing? How to get/fulfill bread? We will have to instruct make to go for the groceries and get us bread. If already going for groceries, why not pick up all the ingredients that are needed? Again, let’s define that:

ingredients = bread butter ham tomatoes salad

$(ingredients): groceries 

This again is so simple, there’s isn’t too much to tell. We define a variable ingredients, holding our shopping list of bread, butter, ham, tomatoes and salad.

Following that, we use this variable as the target part of a rule with a prerequisite, groceries, but without a recipe per se. Essentially, what this means are a few things:

  • once the variable is lazily evaluated (see link), the rule $(ingredients): groceries will basically become bread butter ham tomatoes salad: groceries, i.e. multiple _target_s can be cobbled into a single rule, saving space, refactoring work, etc. Likewise, they could be spelled out one-for-one but why would that be a good idea?
  • If anything is missing, one will need to take care of the groceries — which we will define next, making sure that the whole list of ingredients will be checked, but something will only be bought if it is really missing (i.e. file bread or whatever doesn’t exist).
  • Along the way, we will meet one of the many text manipulation functions of make.

Let’s go ahead and look at the current situation…

ingredients = bread butter ham tomatoes salad

$(ingredients): groceries 

…and add the following:

groceries: $(addprefix buy-,$(ingredients))

buy-%: %
  touch $* 

In a nutshell, our current state of affairs mean the following:

  • in case any ingredient is missing, say bread, then:
  • bread will trigger the prerequisite groceries through $(ingredients): groceries
  • once the target groceries is triggered, it will take care of its own prerequisites, which will be provided by a text substitution in its list of prerequites: $(addprefix buy-,$(ingredients))
  • $(addprefix buy-,$(ingredients)) will, as the name suggests, add the prefix buy- to the list of $(ingredients), providing yet another list: buy-bread buy-butter buy-ham buy-tomatoes buy-salad
  • Whether something needs buying or is still in stock is decided by buy-%: %. Remember pattern rules and timestamps from last installment? As long as the prerequisite bread is met (by a file existing by the same name), everything is fulfilled and no more action is needed. If this is not the case (no file bread), then fulfill the prerequisite by executing the recipe, i.e. touch bread. touch $* in this case, by the means of another automatic variable, $*, takes care of anything that needs to be bought in an abstract manner: the automatic variable $* will hold whatever the “make-glob-sign”, %, matched in buy-%: %

In order to present yet another key concept of make, order only prerequisites, let’s refactor another small thing and create the following:

buy-%: % | clean-up-kitchen!
  touch $* 

This is yet another type of prerequisite, an order-only prerequisite. While a normal prerequisite, that needs fulfilling automatically forces the target to update, an order-only prerequisite will not. It is still a prerequisite of the target but it doesn’t influence whether the target itself needs updating. Maybe a bit far off for now, but remember this for cases where you want to guarantee, that a certain directory exists. It might come in handy. For now, make will clean our kitchen before going for groceries to make us a sandwich.

Last but not least, let’s add a target which offers a quick help to the user and set a new default rule (the first rule of the Makefile):

default: help

help: ; @echo "Help yourself: 'make sandwich' or 'make sliced.bread' or 'make bread', and feel at home..." 

Key concepts to be seen here:

  • Make let’s you append the recipe-part of the rule to the same line as the target by placing it behind ;
  • If a rule begins with @ it will not be shown to the user while it’s running

What we finally arrive at should be the following Makefile:

WASH ?= mv

ingredients = bread butter ham tomatoes salad

default: help

help: ; @echo "Help yourself: 'make sandwich' or 'make sliced.bread' or 'make bread', and feel at home..."

buy-%: % | clean-up-kitchen!
    touch $*

clean: clean-up-kitchen!

clean-up-kitchen!:
    rm -f sliced.bread washed.salad sliced.tomatoes butter ham

$(ingredients): groceries

groceries: $(addprefix buy-,$(ingredients))

sliced.%: %
    mv 
lt; $@ washed.%: % $(WASH) 
lt; $@ sandwich: sliced.bread washed.salad sliced.tomatoes butter ham echo $^ > sandwich 

Now as for the usage, make help offers good hints: use make sandwich, or make bread.

Make actually allows for multiple targets to be placed after each other and to be run in order, e.g. make clean default sandwich.

Another very interesting feature is -n, or --dry-run: in dry-run mode make will display all the commands it would run, without actually running them.

One more interesting option is -j, the auto-threading switch. Calling make -j2 sliced.tomatoes sliced.bread could, for the sake of the pattern rule, spread out to two CPUs and do the slicing in parallel.

So much for this time. Thanks for reading and hope you’re hungry for make now!