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.
Contents
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)