GNU Makefiles - best practices
This is a summary of best practices when using Makefiles. This post is primarily intended for my students building tools in our lab. Much of this post is based on the Make bestpractices that I learned during my stint at Sun Microsystems, updated to reflect GNU Make since it is the most common make flavour at this point by a significant margin.
(GNU) Makefiles best practices
Automatic variables are your friends.
$@is the name of the target. Make it a habit totouch $@at the end of the make rule for timestamps, or, if it is the name of a useful target, generate.$@or$@~if$@includes a path, and finallymv .$@ $@as the last step.$*is the matched portion represented by%in Makefile targets. Confusingly,$%is used for another purpose.$<is the name of the first prerequisite.$^is the list of all prerequisites.$?is the list of all prerequisites that are newer than the target.- Attaching parentheses and
DandFrespectively gives you the directory and file parts of these variables. For example,$(@D)is the directory part of the target, while$(@F)is the file part.
Be aware of partial targets
Do not rely on files that get created with content as the target name.
Essentially, if you rely on files that are produced by a process and the process can be interrupted while writing to the file, the file will be corrupt, and make will not know about it in the next run.
For example, do not do this.
a.exe: a.c b.c
$(CC) -o a.exe a.c b.c
Instead, make it a two-step process.
a.exe: a.c
$(CC) -o _a.exe a.c b.c
@mv _a.exe a.exe
Note: the use of @ at the beginning of the command line to hide command invocation echo here.
Or even better:
SRC_FILES=a.c b.c
a.exe: $(SRC_FILES)
$(CC) -o _$@ $^
@mv _$@ $@
See also: .DELETE_ON_ERROR
Use sentinels
Use sentinels when your recipe creates multiple files that are required elsewhere in the build
extract: project/.extracted
project/.extracted: project.tar.gz
$(EXTRACT) $<
touch $@
Here, the extract target generates multiple files from the tar.gz file. If you depend on one of the files generated as the target, you run the risk of extract being interrupted after the specified file is created but before others are created. Similarly, this rule would never run if some other file generated by extract is older than project.tar.gz. Hence, always rely on sentinel files when there are multiple files created by one rule.
Explicitly thread any files used in later stages in dependencies.
extract: project/.extracted
project/.extracted: project.tar.gz
$(EXTRACT) $<
touch $@
project/myfile1.img: project/.extracted
project/myfile2.img: project/.extracted
project/%.jpg: project/%.img
$(CONVERT) -o _$@ $<
That is, rather than having project/%.jpg depend on project/.extracted, thread the name of the extracted file in the dependency chain.
Thread dependencies, with entry targets treated as leaves
extract: project/.extracted
project/.extracted: project.tar.gz
$(EXTRACT) $<
touch $@
preprocess: project/.preprocessed
project/.preprocessed: project/.extracted
$(preprocess) project/*.c
touch $@
This lets you use shorter names when invoking intermediate stages and ensures that you don’t do more work than necessary.
Specify the targets as Make variables, which are translated to Makefile pattern rules
extract: project-$(target)/.extracted
project-%/.extracted: project-%.tar.gz
$(EXTRACT) $<
touch $@
preprocess: project-$(target)/.preprocessed
project-%/.preprocessed: project-%/.extracted
$(preprocess) project-$*/*.c
touch $@
This will be invoked as:
make preprocess target=A
Avoid phony target names that are also common directories
Namely, avoid target names such as build, all, include, src, lib, etc. These are often created as directories by common open-source programs, and your Makefile can get confused, which can be hard to debug. This can be avoided using .PHONY targets, but I have found it better to avoid using such directory names at all. The nice thing is that you can define a timestamp dependency rule to create that directory if needed (see below).
Timestamp dependency rules
Another best practice is to use the “make sure it exists, but don’t check the timestamp” dependency rule for directories and similar targets.
all: $(OBJS)
$(OBJS): | $(OBJDIR)
$(OBJDIR):; mkdir $(OBJDIR)
Note: the use of ; to indicate ending of target and beginning of recipe
Here, OBJDIR will be created if it does not exist. If you forget the |, the dependents of OBJDIR will be recreated each time a file inside OBJDIR changes. That is, ideally,
%.o: %.cpp .deps
$(CXX) $(CXXFLAGS) -c -o $@ $< -MMD -MP -MF .deps/$*.d
.deps:
mkdir .deps
# subtle: directories are listed as changed when entries are
# created, leading to spurious rebuilds.
.deps/stamp:
mkdir .deps && touch .deps/stamp
-include .deps/*.d
should be
%.o: %.cpp | .deps
$(CXX) $(CXXFLAGS) -c -o $@ $< -MMD -MP -MF .deps/$*.d
.deps:
mkdir .deps
-include .deps/*.d
Do not rely on the ordering of recipes in the Makefile to ensure that the targets are built in a particular order. For example, if you have
a:
touch a
b:
touch b
c:
touch c
Do not assume that the targets will be built in the order a, b, c. If you want that order, explicitly specify it using dependencies:
a:
touch a
b: a
touch b
c: b
touch c
If not, you will come to grief when some poor programmer decides to use make -j <n> or uses one of the parallel makes. Make use of tools such as gvmake to ensure that the dependencies are correctly ordered. Use -o make.dot to create a dotfile and inspect it directly — it has a human-readable syntax.
Use --debug=basic -n to view which files need to be remade, and GNU remake for debugging.
Use gmake
debugger or gmd
Make it a practice to explicitly use .PHONY
.PHONY: all clean extract preprocess
Phony targets are those targets that provide entry points to make invocations and are not file-based.
Remove implicit rules
.SUFFIXES:
MAKEFLAGS += --no-builtin-rules
Disable automatic removal of intermediate files
.SECONDARY:
Idempotent on repeated invocations
make invoked on the same target multiple times without changes should not differ in the artifacts produced compared to a single invocation.
Clean up after yourself
make clean should remove all files generated by the build. The target make clobber can be used as a super make clean if the build is not self-contained.
Never save an artifact in a repository that can be generated by make
Avoid the temptation to save generated artifacts if they can be regenerated by another run of make.
Avoid in-place edits of any files.
These are susceptible to race conditions, corruption, and incomplete builds, and can make your life difficult.
Silent makes
Thanks to mad-scientist for introducing me to the .SILENT target.
There are various ways to accomplish this. The most fine-grained approach is to use @ in front of a recipe line:
dont_echo:
@echo hi
echo hello
Another is to use the .SILENT target to silence specific targets:
.SILENT: dont_echo
dont_echo:
echo hi
The .SILENT target, if defined without dependencies, will silence all targets:
.SILENT:
dont_echo:
echo hi
You can, of course, make it dependent on a variable V:
ifndef V
.SILENT:
endif
$ make V=1
Or achieve the same effect more easily:
dont_echo:
echo hi
$(V).SILENT:
$ make V=1
make itself accepts the command-line option --silent:
$ make --silent
Recommendation from here
Change the .RECIPEPREFIX if you are starting a new project where you do not have to interact with other projects. I recommend ; over > as the former lets you join multiple command lines together or split a command line into multiple parts with little effort.
.RECIPEPREFIX = ;
all:
; echo hello world
Ensure that a failure in a pipe stage kills the build
.SHELLFLAGS := -eu -o pipefail -c
```
Other best practices:
- Always include all and clean targets
- Use variables for compiler settings