Make Cheatsheet
Variables
Use this Make reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
The five assignment flavors
| Operator | Name | When the right side is expanded |
|---|---|---|
= | Recursive | Every time the variable is used |
:= | Simple | Once, at the point of assignment |
::= | Simple (POSIX) | Same as :=, standardized in POSIX 2024 |
?= | Conditional | Like =, but only if the variable is not already set |
+= | Append | Keeps the flavor of the original assignment |
!= | Shell | Runs the shell once, assigns the trimmed output (GNU make 4.0+) |
REC = $(LATER) # picks up LATER whenever REC is referenced SIM := $(LATER) # LATER is empty right now, so SIM is empty forever LATER = value DEF ?= fallback # only if DEF was not set on the command line or in env LIST = a LIST += b # "a b" NOW != date +%Y # "2026"
Referencing those prints REC=[value] SIM=[] DEF=[fallback] LIST=[a b]. :=
sees only what is already defined above it. = sees the final value.
!=is silently empty on GNU make 3.81, which is still what/usr/bin/makeis on macOS. Use$(shell ...)with:=if you need to support it.
When recursive expansion bites
Recursive = is late-binding, which is usually what you want for flags and
usually wrong for anything expensive.
# Works: INCLUDES is defined later, and = defers the lookup. CFLAGS = $(INCLUDES) -O2 INCLUDES = -Iinclude # Broken: := froze CFLAGS before INCLUDES existed. CFLAGS := $(INCLUDES) -O2 INCLUDES = -Iinclude
The expensive case, a shell call re-run on every reference:
LAZY = $(shell git rev-parse --short HEAD) # runs once per use EAGER := $(shell git rev-parse --short HEAD) # runs exactly once
Three references to LAZY fork three git processes. In a large makefile that
is measurable, and in a recipe inside a loop it is painful.
Self-reference is only legal with :=.
CFLAGS = $(CFLAGS) -O2 # *** Recursive variable 'CFLAGS' references itself
CFLAGS := $(CFLAGS) -O2 # fine, appends onceRule of thumb: default to :=, reach for = only when the value genuinely must
be resolved later, and use ?= for anything a caller should be able to override.
Precedence and where a value came from
From lowest priority to highest:
- Built-in defaults (
CC,RM,AR, ...) - Environment variables
- Assignments in the makefile
- Command line,
make CFLAGS=-O0 override CFLAGS := ...in the makefile
Command-line assignments beat makefile assignments, which is why ?= and
override exist. make -e flips environment above makefile.
override CFLAGS += -Wall # append even to a command-line value
$(origin VAR) reports the source, useful in a guard:
$(info CC came from $(origin CC)) # default | environment | file # | command line | override | undefined ifeq ($(origin PREFIX),undefined) PREFIX := /usr/local endif
Target-specific and pattern-specific variables
A variable can be scoped to one target and its prerequisites, or to every target matching a pattern.
debug: CFLAGS := -O0 -g -DDEBUG debug: app %.min.js: CFLAGS := --compress
Add private so the value does not leak down to prerequisites:
debug: private CFLAGS := -O0 -g
Target-specific values are why make debug can reuse the same compile rules with
different flags instead of duplicating them.
Shell variables need $$
Make expands $ first, so a literal dollar sign in a recipe is $$. This is the
second most common make bug after tabs.
show: @echo "make var: $(HOME)" # expanded by make @echo "shell var: $$HOME" # passed through as $HOME @for f in *.c; do echo "$$f"; done
Each recipe line is its own shell, so a shell variable set on one line is gone on
the next. Join with && or ; inside one line, or turn on .ONESHELL.
bad: x=5 @echo "[$$x]" # prints [] , new shell good: x=5 && echo "[$$x]" # prints [5]
In awk and sed scripts inside recipes the same rule applies, so field
references become $$1 and $$2.
$(shell ...) versus backticks
Both run a command, at different times and with different results.
$(shell cmd) | ` cmd ` in a recipe | |
|---|---|---|
| Who runs it | make, while parsing or expanding | the shell, when the line runs |
| When | before any recipe executes | at recipe time |
| Newlines | collapsed to single spaces | preserved |
| Trailing newline | stripped | stripped by the shell |
| Available in variable assignments | yes | no |
| Sees files created by earlier recipes | no | yes |
BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
list:
@echo "generated files: `ls build`" # build/ exists only nowUse $(shell ...) for configuration read once at startup, and backticks or
$$(...) for anything that depends on what the build just produced. Check
.SHELLSTATUS for the exit code of the last $(shell ...) (GNU make 4.2+).
Multi-line values with define
define BANNER ===================== building $(NAME) ===================== endef export BANNER define compile_rule $(1).o: $(1).c $(CC) $(CFLAGS) -c -o $$@ $$< endef $(eval $(call compile_rule,main)) banner: @echo "$$BANNER"
Inside a define destined for eval, automatic variables need $$ so they
survive the first expansion pass.