Make Cheatsheet

Functions

Use this Make reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Calling a function

Functions are $(name arg1,arg2). The name and the first argument are separated by whitespace, later arguments by commas, and leading whitespace in an argument is stripped while trailing whitespace is not. There is no way to escape a comma inside an argument, so define COMMA := , and pass $(COMMA).

COMMA := ,
SPACE := $() $()
$(info $(subst $(SPACE),$(COMMA),a b c))    # a,b,c

Everything make manipulates is a whitespace-separated word list. There are no types, no arrays, and no numbers.

Text and word functions

Values below are real output from GNU make 4.4 with FILES = src/net/tcp.c src/main.c include/api.h.

CallResult
$(subst src/,build/,$(FILES))build/net/tcp.c build/main.c include/api.h
$(patsubst %.c,%.o,$(FILES))src/net/tcp.o src/main.o include/api.h
$(FILES:%.c=%.o)same, shorthand form
$(FILES:.c=.o)suffix-only shorthand
$(strip a b )a b
$(findstring net,$(FILES))net, or empty
$(filter %.c,$(FILES))src/net/tcp.c src/main.c
$(filter-out %.h,$(FILES))src/net/tcp.c src/main.c
$(sort c a b a)a b c, sorted and deduplicated
$(words $(FILES))3
$(word 2,$(FILES))src/main.c, 1-indexed
$(wordlist 1,2,$(FILES))src/net/tcp.c src/main.c
$(firstword $(FILES))src/net/tcp.c
$(lastword $(FILES))include/api.h

$(sort) deduplicating is the trick people forget. $(sort $(DIRS)) is the idiomatic unique-ify, and it is also why sorted output is not always the order you wrote.

Filename functions

CallResult
$(dir src/net/tcp.c)src/net/, trailing slash kept
$(notdir src/net/tcp.c)tcp.c
$(basename src/main.c)src/main, strips the suffix, keeps the path
$(suffix src/main.c).c
$(addprefix -I,include src)-Iinclude -Isrc
$(addsuffix .o,main util)main.o util.o
$(join a b,1 2)a1 b2, pairwise
$(wildcard src/*.c)expands the glob, empty if nothing matches
$(abspath ./f.mk)/path/to/f.mk, no symlink resolution
$(realpath ./f.mk)resolves symlinks, empty if the file is missing

$(wildcard) hits the filesystem when it is expanded, so with := it sees only files that existed when make started. A file created by an earlier recipe will not appear.

SRC := $(wildcard src/*.c) $(wildcard src/*/*.c)   # not recursive
SRC := $(shell find src -name '*.c')               # recursive, one fork

Control-flow functions

CallMeaning
$(if COND,then,else)COND is true when it expands to a non-empty string
$(or a,b,c)first non-empty argument
$(and a,b,c)last argument, or empty if any is empty
$(foreach v,LIST,BODY)expand BODY once per word, results joined by spaces
$(call fn,a,b)expand a variable as a function, args as $(1), $(2)
$(eval TEXT)parse TEXT as makefile syntax, at expansion time
$(value V)the unexpanded text of V
$(origin V)undefined, default, environment, file, command line, override, automatic
$(flavor V)undefined, recursive, or simple
$(let a b,1 2,BODY)GNU 4.4, bind local names
$(intcmp a,b,lt,eq,gt)GNU 4.4, integer comparison

$(if) tests emptiness, not truth. $(if 0,yes,no) is yes, because 0 is a non-empty string. Wrap in $(strip ...) when the value may be spaces.

$(info $(if $(strip $(DEBUG)),debug,release))
$(info $(foreach d,include vendor/include,-I$(d)))   # -Iinclude -Ivendor/include

call and eval, generating rules

$(call) turns a variable into a function. $(eval) feeds text back to the parser, which is how you generate rules in a loop.

reverse = $(2) $(1)
$(info $(call reverse,a,b))         # b a

define compile
$(1).o: $(1).c
	@echo "  CC $$@ from $$<"
endef

$(eval $(call compile,main))

The $$ inside a define destined for eval is required. The text is expanded once by call, so $$@ becomes $@ and only then reaches the rule parser. Recipe lines inside a define still need a real leading tab.

MODULES := auth billing search
$(foreach m,$(MODULES),$(eval $(call compile,$(m))))

Debug generated rules by printing instead of evaluating:

$(info $(call compile,main))

Shell, file, and diagnostics

CallBehavior
$(shell cmd)runs cmd, newlines collapse to spaces, trailing newline stripped
.SHELLSTATUSexit status of the last $(shell), GNU 4.2+
$(file <in.txt)read a file, GNU 4.2+
$(file > out.txt,text)write, >> appends, sidesteps the ARG_MAX limit
$(info msg)print to stdout, expands to nothing
$(warning msg)print to stderr with file:line: prefix
$(error msg)print to stderr and stop with exit status 2
$(guile ...)only if make was built with Guile, rarely available
BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
ifneq ($(.SHELLSTATUS),0)
  $(error not a git repository)
endif

# response file, for a link line too long for argv
big: $(OBJ)
	$(file > $@.rsp,-o $@ $(OBJ))
	$(CC) @$@.rsp

$(shell) runs during expansion, so a $(shell) in a recursive = variable forks once per reference. Assign it with := unless you specifically want it re-run.

The one-liners worth memorizing

# object list from sources, mirroring the tree into build/
SRC := $(shell find src -name '*.c')
OBJ := $(SRC:src/%.c=build/%.o)

# every unique directory the objects need to exist
DIRS := $(sort $(dir $(OBJ)))

# include flags from a directory list
CPPFLAGS += $(addprefix -I,include vendor/include)

# require a variable, with a useful message
check-var = $(if $(strip $($1)),,$(error $1 is required))
release: ; $(call check-var,VERSION)

# a value with a default that the command line can still override
PREFIX ?= /usr/local

# true when a program is on PATH
HAVE_JQ := $(if $(shell command -v jq),yes,no)