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.

DirectiveTrue when
ifeq (A,B)A and B expand to the same string
ifneq (A,B)they differ
ifdef VARVAR has a non-empty value
ifndef VARVAR 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 build
inc.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)
endif

Auto-dependencies with -MMD -MP

Hand-written header prerequisites always rot. Let the compiler emit them. GCC and clang both support this.

FlagEffect
-MDwrite a .d file and compile, includes system headers
-MMDsame, but skips <system> headers, which is what you want
-MPadd a phony target for every header
-MF filechoose the .d path
-MT targetset 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.