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.
By default, Make will “echo” every command before executing it. Here are some ways to reduce verbosity, in increasing levels of granularity:
- Declare the
.SILENTtarget. 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.
ifndef VERBOSE .SILENT: endif
- Run with the
-sflag. 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
.PHONY: say-hi say-hi: @echo "Hi!"
Running all lines of a target in the same shell
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: 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
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)