Makefile hacks

I love GNU Make. The simplicity of Makefiles makes it an extremely versatile and powerful tool. Here is some Makefile-fu I learned over the years.

Silencing output

By default, Make will “echo” every command before executing it. Here are some ways to reduce verbosity, in increasing levels of granularity:

  • Declare the .SILENT target. This is a special target which will silence all echoing by default. It is generally a good idea to sill leave a verbosity option, e.g. make VERBOSE=1:
ifndef VERBOSE
.SILENT:
endif
  • Run with the -s flag. This will prevent Make from printing any rule before executing it, but requires explicit input from the user. See the GNU Make manual more details.
  • To prevent a single line in a recipe from being echoed, prefix it with @, e.g.:
.PHONY: say-hi
say-hi:
    @echo "Hi!"

Running all lines of a target in the same shell

By default make spawns a new shell for each line of a target. Often however I like to create a subdirectory, move into it, and execute some command in it. You can achieve this by using the && operator to link commands into one line:

.PHONY: testing
testing: 
    pwd
    mkdir -p testing
    cd testing && pwd

However this can get annoying if you have to run multiple commands. An alternative is to use the .ONESHELL special target. Note that this applies to all targets in this Makefile.

.PHONY: testing
testing:
    pwd
    mkdir -p testing
    cd testing
    pwd

.PHONY: .ONESHELL
.ONESHELL:

Forcing a target to rebuild

By default, Make will only rebuild a target if no file with the target’s name exists, or if a dependency has been updated more recently than the target.

But what if we want to ensure a target’s recipe is executed every time we call make, no matter what? There’s two approaches:

  • Declare the target as .PHONY.
.PHONY: my-target
my-target:
    my-recipe
  • Make the target depend on a nonexistent (phony) target, often called a force target. This can be handy if you don’t have an explicit name for the target. I just use it in some situations because I like the semantics of it, but in essence this has the same effect as .PHONY.
my-target.json: .FORCE
    my-recipe

.PHONY: .FORCE

File name functions

Make has several lesser-known functions for handling filenames. These can be extremely useful. Here are some that I’ve found useful:

  • $(wildcard <pattern>): generate a list of files in the current directory that match the pattern.
  • $(addsuffix <suffix>, <list>): add a given suffix to each member of a list.
  • $(basename <list>): get the base name (without extensions) of each member of a list.

Here’s an example:

$ cat Makefile 
files = $(wildcard test-*)

all: $(files)

.PHONY: all $(files)
$(files): 
    @echo "Building $(basename $@)..."

And the resulting output:

$ ls
Makefile  test-1.json  test-2.json
$ make
Building test-1...
Building test-2...

An example: automated benchmarks

Here is an example using some of the tricks mentioned above. I had compiled a suite of benchmarks, and wanted to automate their execution. Each benchmark was denoted by a JSON file which had the configuration for that specific benchmark, and each benchmark would generate outputs which needed to be stored separately.

# A Makefile for easy automagical execution of benchmarks.
# If you don't have much time, just run `make fast`
# If you want to run the full suite, run `make all`, but beware that it may take hours.
# To run with custom build directory, run `make build-directory=..foo/bar/`

# Note: executable watersim-cli is assumed to exist in build directory
build-directory ?= ../build/

# List of all benchmarks to run. Divided into "fast" and "slow" benchmarks
fast-benchmarks = benchmark-1-0 benchmark-2-0
slow-benchmarks = benchmark-1-1 benchmark-1-2 benchmark-1-3 benchmark-2-1 benchmark-2-2 benchmark-2-3

fast-benchmark-files = $(addsuffix .json, $(fast-benchmarks))
slow-benchmark-files = $(addsuffix .json, $(slow-benchmarks))

.PHONY: warning
warning:
    echo "Note: only running 'fast' benchmarks. To run all benchmarks, use 'make all'."
    make fast

.PHONY: fast
fast: $(fast-benchmark-files)

.PHONY: slow
slow: $(slow-benchmark-files)

.PHONY: all
all: fast slow

$(fast-benchmark-files): .FORCE
    echo -n "Running benchmark '$(basename $@)'... "
    mkdir -p $(basename $@)
    cd $(basename $@) && ../$(build-directory)watersim-cli -y ../$@ > $(basename $@).log
    echo "Done."

$(slow-benchmark-files): .FORCE
    echo -n "Running benchmark '$(basename $@)'... "
    mkdir -p $(basename $@)
    cd $(basename $@) && ../$(build-directory)watersim-cli -y ../$@ > $(basename $@).log
    echo "Done."

.PHONY: .FORCE
.FORCE:

ifndef VERBOSE
.SILENT:
endif

.PHONY: clean
clean:
    rm -rf $(fast-benchmarks) $(slow-benchmarks)