Make Cheatsheet
Conditionals and Includes
Use this Make reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
The four conditional directives
Conditionals are evaluated when the makefile is read, not when a recipe runs, so they cannot test anything a recipe produced. They start in column one and are not indented with a tab.
| Directive | True when |
|---|---|
ifeq (A,B) | A and B expand to the same string |
ifneq (A,B) | they differ |
ifdef VAR | VAR has a non-empty value |
ifndef VAR | VAR is empty or unset |
OS := $(shell uname -s) ifeq ($(OS),Darwin) LDLIBS += -framework CoreFoundation else ifeq ($(OS),Linux) LDLIBS += -ldl else $(warning unknown OS $(OS)) endif ifdef DEBUG CFLAGS += -O0 -g else CFLAGS += -O2 endif
Three forms of argument quoting, all equivalent:
ifeq ($(CC),clang) ifeq "$(CC)" "clang" ifeq '$(CC)' 'clang'
Conditional gotchas
- Whitespace is significant. ifeq ($(A), b) compares against " b" with a
leading space and never matches. Write ifeq ($(A),b) with no space after the
comma, or wrap both sides in $(strip ...).
- ifdef tests emptiness, not definedness. X := followed by ifdef X is
false. Use $(if $(filter undefined,$(origin X)),...) for a true
defined-or-not test.
- ifdef does not expand its argument, it takes a variable name. ifdef
$(VAR) tests the variable whose name is the value of VAR.
- A recursive variable is expanded by ifdef, so A = $(B) with empty B
makes ifdef A false.
- Conditionals cannot span a rule. An ifeq opened before a target and
closed after its recipe works only because make reads line by line, and it
makes the file unreadable. Put the conditional inside the variable section.
- Never test an automatic variable. $@ and $< do not exist at parse time.
Use a target-specific variable instead.
The empty-versus-undefined test, in practice:
ifeq ($(origin PREFIX),undefined) PREFIX := /usr/local endif
That is the long form of PREFIX ?= /usr/local, and it is what you want when an
explicit empty value should be respected.
Feature detection instead of platform guesses
# compiler family ifneq (,$(findstring clang,$(shell $(CC) --version 2>&1))) CFLAGS += -Wno-unused-command-line-argument endif # does this compiler accept a flag at all HAVE_LTO := $(shell $(CC) -flto -x c -c /dev/null -o /dev/null 2>/dev/null \ && echo yes) CFLAGS += $(if $(HAVE_LTO),-flto,) # a tool being present ifeq (,$(shell command -v jq)) $(error jq is required: brew install jq) endif # make version gate ifeq (,$(filter 4.% 5.%,$(MAKE_VERSION))) $(error GNU make 4.0 or newer required, found $(MAKE_VERSION)) endif
$(filter ...) beats string comparison for version gates, because ifeq on
$(MAKE_VERSION) breaks on every point release.
include and -include
include splices another makefile in at that point. A missing file is a fatal
error. -include (sinclude is a GNU alias for it) ignores a missing file.
include config.mk # fatal if absent
-include $(wildcard *.mk) # any that exist
-include $(DEP) # dependency files, absent on a clean buildinc.mk:2: also-missing.mk: No such file or directory make: *** No rule to make target 'also-missing.mk'. Stop.
The behavior that makes auto-dependencies work: if make has a rule to build an
included file, it builds it and then restarts itself, re-reading every
makefile from scratch. That restart is why a $(shell) at the top of a makefile
can appear to run twice, and $(MAKE_RESTARTS) tells you it happened.
- Include paths are searched in ., then any -I directory, then a small set of
system directories. $(MAKEFILE_LIST) holds every file read so far.
- Split a large build as include mk/tools.mk mk/rules.mk, and keep generated
configuration in a separate -included file.
- include inside a conditional is fine, and a good way to skip an expensive
include for make clean.
ifeq (,$(filter clean distclean,$(MAKECMDGOALS)))
-include $(DEP)
endifAuto-dependencies with -MMD -MP
Hand-written header prerequisites always rot. Let the compiler emit them. GCC and clang both support this.
| Flag | Effect |
|---|---|
-MD | write a .d file and compile, includes system headers |
-MMD | same, but skips <system> headers, which is what you want |
-MP | add a phony target for every header |
-MF file | choose the .d path |
-MT target | set the target name inside the .d |
A complete C project. Recipe lines start with a real tab.
CC := cc
CPPFLAGS := -Iinclude -MMD -MP
CFLAGS := -std=c11 -O2 -Wall -Wextra
LDLIBS := -lm
SRC := $(shell find src -name '*.c')
OBJ := $(SRC:src/%.c=build/%.o)
DEP := $(OBJ:.o=.d)
.DEFAULT_GOAL := build/app
.DELETE_ON_ERROR:
.PHONY: clean
build/app: $(OBJ)
$(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)
build/%.o: src/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
clean:
$(RM) -r build
-include $(DEP)The generated build/main.d looks like this:
build/main.o: src/main.c include/u.h include/u.h:
Line one is the real dependency. Line two is what -MP adds, and it is the
whole point: without it, deleting or renaming a header gives
make: *** No rule to make target 'include/u.h', needed by 'build/main.o'.
and the only fix is make clean. With -MP the phony header target lets make
rebuild the object, and the compiler reports the missing include properly. Note
that -MMD writes the .d next to the .o, so $(OBJ:.o=.d) finds them and
clean removing build/ removes them too.