Toolmingo
Guides10 min read

Makefile Cheat Sheet: Syntax, Rules, and Patterns

A complete Makefile cheat sheet — targets, variables, automatic variables, pattern rules, functions, phony targets, and real-world build patterns. Copy-ready Makefile syntax.

make is a build automation tool that determines which parts of a program need recompilation and executes the right commands. It reads a Makefile that describes the build graph as targets and their dependencies.

Quick reference

Task Syntax
Run default target make
Run specific target make build
Dry run (print, don't run) make -n build
Parallel jobs make -j4
Specify Makefile make -f other.mk
Pass variable make ENV=prod
Keep going on error make -k
Silent mode make -s
Print database make -p
Remake without checking dates make -B
Target with prerequisites target: dep1 dep2
Phony target .PHONY: clean test
Automatic variable — output $@
Automatic variable — first dep $<
Automatic variable — all deps $^

Rule anatomy

target: prerequisites
	recipe-line-1
	recipe-line-2
  • target — the file or label to build
  • prerequisites — files that must be up-to-date before this target runs
  • recipe — shell commands, each line must be indented with a TAB, not spaces
# Minimal example — compile main.o from main.c
main.o: main.c main.h
	gcc -c main.c -o main.o

Variables

Defining variables

# Simply expanded (evaluated once at assignment)
CC := gcc
CFLAGS := -Wall -O2

# Recursively expanded (evaluated on every use)
SRCS = $(wildcard src/*.c)

# Conditional assignment (set only if not already defined)
BUILD_DIR ?= build

# Append to existing value
CFLAGS += -g

Using variables

# $(VAR) or ${VAR}
CC := gcc

compile:
	$(CC) $(CFLAGS) -o app main.c

Override from command line

# In Makefile: CFLAGS := -O2
# Command line: make CFLAGS="-O0 -g"  — overrides MAKEFILE value

Environment variables

# Environment variables are available automatically
deploy:
	echo "Deploying to $(DEPLOY_HOST)"

# Export Make variable to child processes
export PATH := $(PATH):/custom/bin

Automatic variables

Variable Meaning
$@ Name of the target
$< Name of the first prerequisite
$^ All prerequisites (deduplicated)
$+ All prerequisites (with duplicates)
$? Prerequisites newer than the target
$* Stem matched by % in pattern rule
$(@D) Directory part of target
$(@F) File part of target
$(<D) Directory part of first prerequisite
$(<F) File part of first prerequisite
build/%.o: src/%.c
	mkdir -p $(@D)
	$(CC) -c $< -o $@
	# $@ = build/foo.o, $< = src/foo.c, $* = foo

Pattern rules

# Compile any .c file to .o in the same directory
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Compile .c from src/ to .o in build/
build/%.o: src/%.c | build
	$(CC) $(CFLAGS) -c $< -o $@

# Convert any .md to .html
%.html: %.md
	pandoc $< -o $@

Static pattern rules (apply to specific targets)

OBJS := foo.o bar.o baz.o

$(OBJS): %.o: %.c
	$(CC) -c $< -o $@

Phony targets

Phony targets are labels, not real files. Always declare them with .PHONY:

.PHONY: all build test clean lint install

all: build

build:
	go build -o bin/app ./cmd/app

test:
	go test ./...

clean:
	rm -rf bin/ build/

lint:
	golangci-lint run ./...

install: build
	cp bin/app /usr/local/bin/

Without .PHONY, if a file named clean exists, make clean will say "nothing to be done".


Order-only prerequisites

Use | to declare a prerequisite that must exist but whose timestamp does not trigger a rebuild:

build/%.o: src/%.c | build
	$(CC) -c $< -o $@

build:
	mkdir -p build

Without | build, touching the directory would cause every object file to rebuild.


Built-in functions

Text functions

# $(subst from,to,text)
NEW := $(subst .c,.o,main.c)        # main.o

# $(patsubst pattern,replacement,text)
OBJS := $(patsubst src/%.c,build/%.o,$(SRCS))

# $(strip text) — remove leading/trailing whitespace
STRIPPED := $(strip   hello world   )

# $(filter pattern, text) — keep matching words
C_FILES := $(filter %.c, $(FILES))

# $(filter-out pattern, text) — remove matching words
NON_C := $(filter-out %.c, $(FILES))

# $(sort list) — sort and deduplicate
SORTED := $(sort foo bar baz foo)   # bar baz foo

# $(word n,text) — nth word (1-based)
FIRST := $(word 1, a b c)           # a

# $(words text) — count words
COUNT := $(words a b c)             # 3

# $(firstword text)
F := $(firstword a b c)             # a

# $(lastword text)
L := $(lastword a b c)              # c

# $(dir names) — directory part
D := $(dir src/foo.c)               # src/

# $(notdir names) — file part
N := $(notdir src/foo.c)            # foo.c

# $(suffix names)
S := $(suffix src/foo.c)            # .c

# $(basename names)
B := $(basename src/foo.c)          # src/foo

# $(addsuffix suffix,names)
HTMLS := $(addsuffix .html,index about)  # index.html about.html

# $(addprefix prefix,names)
PATHS := $(addprefix build/,foo bar)     # build/foo build/bar

# $(join list1,list2)
J := $(join a b c, .x .y .z)       # a.x b.y c.z

File functions

# $(wildcard pattern)
SRCS := $(wildcard src/**/*.c)

# $(abspath path) — resolve to absolute path (without shell)
ABS := $(abspath ../lib)

# $(realpath path) — resolve symlinks too
REAL := $(realpath ../lib)

Conditional functions

# $(if condition,then[,else])
MSG := $(if $(DEBUG),debug mode,release mode)

# $(or cond1,cond2,...) — first non-empty
VAL := $(or $(A),$(B),default)

# $(and cond1,cond2,...) — last non-empty if all non-empty
BOTH := $(and $(A),$(B))

Foreach and call

# $(foreach var,list,text)
FILES := a b c
PATHS := $(foreach f,$(FILES),src/$(f).c)
# → src/a.c src/b.c src/c.c

# $(call func,arg1,arg2) — call user-defined function
define greet
  Hello, $(1)! You are $(2) years old.
endef

MSG := $(call greet,Alice,30)

Shell function

# $(shell command) — run shell, capture output
GIT_HASH := $(shell git rev-parse --short HEAD)
DATE := $(shell date +%Y-%m-%d)

Conditionals

# ifeq / ifneq
ifeq ($(ENV),prod)
  CFLAGS += -O3
else
  CFLAGS += -O0 -g
endif

# ifdef / ifndef
ifndef CC
  CC := gcc
endif

ifdef VERBOSE
  Q :=
else
  Q := @
endif

build:
	$(Q)$(CC) -o app main.c

Multi-line variables and define

# define ... endef creates a multi-line variable
define HELP_TEXT
Usage:
  make build    — compile the project
  make test     — run tests
  make clean    — remove build artifacts
endef

help:
	@echo "$(HELP_TEXT)"

# define a function (callable with call)
define compile_rule
$(1): $(2)
	$(CC) -c $(2) -o $(1)
endef

# Generate rules dynamically
$(eval $(call compile_rule,foo.o,foo.c))
$(eval $(call compile_rule,bar.o,bar.c))

Recipe tricks

# Silence a line with @
build:
	@echo "Building..."   # printed, not the command itself
	$(CC) -o app main.c   # command is echoed

# Continue on error with -
clean:
	-rm -rf build/        # '-' ignores errors

# Multi-line recipe with line continuation
build:
	$(CC) \
	  -Wall \
	  -O2 \
	  -o app \
	  main.c

# Run in the same sub-shell (avoid separate subshells per line)
deploy:
	cd /var/app && \
	git pull && \
	systemctl restart app

# Export variable to all recipes in this Makefile
SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c

Include and modular Makefiles

# Include another Makefile
include config.mk
-include .env.mk     # '-' means don't error if missing

# Auto-include generated dependency files
-include $(OBJS:.o=.d)

# Generate dependency files with gcc
build/%.o: src/%.c | build
	$(CC) -MMD -MP -c $< -o $@

Real-world patterns

C/C++ project

CC      := gcc
CFLAGS  := -Wall -Wextra -O2
SRCDIR  := src
BUILDDIR:= build
TARGET  := bin/app

SRCS := $(wildcard $(SRCDIR)/*.c)
OBJS := $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS))
DEPS := $(OBJS:.o=.d)

.PHONY: all clean

all: $(TARGET)

$(TARGET): $(OBJS) | bin
	$(CC) $(CFLAGS) $^ -o $@

$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
	$(CC) $(CFLAGS) -MMD -MP -c $< -o $@

$(BUILDDIR) bin:
	mkdir -p $@

clean:
	rm -rf $(BUILDDIR) bin/

-include $(DEPS)

Go project

APP     := myapp
CMD     := ./cmd/$(APP)
BINARY  := bin/$(APP)
VERSION := $(shell git describe --tags --always --dirty)
LDFLAGS := -ldflags "-X main.version=$(VERSION)"

.PHONY: all build test lint clean run

all: lint test build

build:
	@mkdir -p bin
	go build $(LDFLAGS) -o $(BINARY) $(CMD)

test:
	go test -race -coverprofile=coverage.out ./...
	go tool cover -func=coverage.out

lint:
	golangci-lint run ./...

run: build
	./$(BINARY)

clean:
	rm -rf bin/ coverage.out

Node.js project

NODE_ENV ?= development

.PHONY: install dev build test lint clean

install:
	npm ci

dev:
	NODE_ENV=$(NODE_ENV) npm run dev

build:
	NODE_ENV=production npm run build

test:
	npm test -- --passWithNoTests

lint:
	npm run lint

clean:
	rm -rf node_modules dist .next coverage

Docker workflow

IMAGE   := myapp
TAG     := $(shell git rev-parse --short HEAD)
REGISTRY:= registry.example.com

.PHONY: build push run

build:
	docker build -t $(IMAGE):$(TAG) -t $(IMAGE):latest .

push: build
	docker tag $(IMAGE):$(TAG) $(REGISTRY)/$(IMAGE):$(TAG)
	docker push $(REGISTRY)/$(IMAGE):$(TAG)
	docker push $(REGISTRY)/$(IMAGE):latest

run:
	docker run --rm -p 8080:8080 $(IMAGE):latest

Help target (self-documenting)

.PHONY: help

## help: Show this help message
help:
	@grep -E '^## ' $(MAKEFILE_LIST) | \
	  sed 's/## //' | \
	  awk -F: '{printf "  \033[36m%-20s\033[0m %s\n", $$1, $$2}'

## build: Compile the project
build: ...

## test: Run all tests
test: ...

## clean: Remove build artifacts
clean: ...

Common mistakes

Mistake Problem Fix
Spaces instead of TAB *** missing separator. Stop. Use actual TAB character for recipes
Missing .PHONY make clean skipped if clean file exists Add .PHONY: clean
$(shell ...) in recipe Runs at parse time, not recipe time Use $$() or just use the shell command directly
Variable with spaces CC = gcc -Wall splits on space for some functions Use := and quote carefully
Forgetting - before error commands Build aborts on rm if file missing - rm -rf build/
Recursive make without $(MAKE) Doesn't pass -j flags and won't recurse properly Always use $(MAKE) not make
Circular dependencies Silent infinite loop or cryptic error Draw the dep graph, break cycles
No order-only prereq for directories Directory timestamp causes spurious rebuilds target: deps | dir

make vs other build tools

Feature Make CMake Bazel Ninja Gradle
Language Makefile DSL CMakeLists.txt Starlark .ninja Groovy/Kotlin
Dependency tracking File timestamps Generated Cryptographic File timestamps Task graph
Incremental builds Yes Yes Yes (hermetic) Yes Yes
Parallel builds -j flag Yes Yes Default Yes
Cross-platform Unix/WSL Yes Yes Yes Yes
Learning curve Medium High Very high Low High
Primary use case C/C++, general C/C++, CMake projects Large monorepos Ninja backend JVM, Android

Frequently asked questions

Q: Why do I get *** missing separator. Stop.?

Your recipe line starts with spaces instead of a TAB. Every recipe line must begin with a literal TAB character. Most editors can be configured to insert tabs in Makefiles.

Q: How do I run make in parallel?

Use make -j (all cores) or make -j4 (4 cores). For correctness, declare all dependencies properly so parallel jobs don't race. Add .NOTPARALLEL: at the top to disable parallelism for a Makefile.

Q: How do I pass variables to make from the command line?

make build ENV=prod DEBUG=1
# Inside Makefile: $(ENV) = prod, $(DEBUG) = 1

Command-line variables override Makefile assignments unless you use override in the Makefile.

Q: What is the difference between := and =?

= (recursive) is re-evaluated every time it's referenced — it can reference variables defined later in the file but can cause infinite loops. := (simply expanded) is evaluated once at the point of definition — safer, faster, and easier to reason about.

Q: How do I print variable values for debugging?

# Info target
print-%:
	@echo $* = $($*)

# Use: make print-CC  → CC = gcc

Or run make -p | grep "^VAR =" to dump the entire Make database.

Q: How do I avoid rebuilding when a header changes?

Use -MMD -MP with GCC/Clang to generate .d dependency files automatically, then include them:

build/%.o: src/%.c | build
	$(CC) -MMD -MP -c $< -o $@

-include $(OBJS:.o=.d)

This causes make to rebuild .o files when any included header changes.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools