make — Automate Builds with Makefiles
Practical guide to GNU Make: targets, prerequisites and recipes in Makefiles – build automation and a general-purpose task runner for any project.
GNU Make is the classic build-automation tool: it reads a Makefile and uses file timestamps to decide which targets need rebuilding. Every rule consists of a target, its prerequisites (dependencies) and a recipe – the shell commands that produce the target. Crucially, recipe lines must begin with a real tab character, never spaces, or Make aborts with "missing separator". Long past being a C/C++-only tool, Make works as a general-purpose task runner for almost any project.
Running Make
make — Run the first (default) target in the Makefile.
makemake <target> — Run a specific target.
make buildmake -f <file> — Use a specific Makefile instead of the default.
make -f Makefile.prod buildmake -n — Dry run: print commands that would be executed without running them.
make -n deploymake -j <n> — Run up to n jobs in parallel for faster builds.
make -j$(nproc)make -j — Run unlimited parallel jobs (use with caution).
make -jmake -B — Unconditionally build all targets (force rebuild).
make -Bmake -s — Silent mode: do not print the commands as they are executed.
make -s buildmake -k — Keep going: continue building other targets even if one fails.
make -k testmake -C <directory> — Change to directory before reading the Makefile.
make -C src/ buildmake VAR=value — Override a variable on the command line.
make CC=clang buildMakefile Basics
target: prerequisites — Basic rule: target depends on prerequisites, built by the recipe. Recipe lines MUST start with a tab.
build: main.o utils.o
gcc -o app main.o utils.oVAR = value — Recursively expanded variable (re-evaluated on each use).
CC = gcc
CFLAGS = -Wall -O2VAR := value — Simply expanded variable (evaluated once at assignment time).
DATE := $(shell date +%Y%m%d)VAR ?= value — Set variable only if it is not already defined.
PREFIX ?= /usr/localVAR += value — Append to an existing variable.
CFLAGS += -gexport VAR — Export a variable to sub-make processes and recipe shells.
export PATH := $(PWD)/bin:$(PATH)Automatic Variables
$@ — The target name of the current rule.
build:
echo "Building $@"$< — The first prerequisite.
%.o: %.c
$(CC) -c $< -o $@$^ — All prerequisites (with duplicates removed).
app: main.o utils.o
$(CC) -o $@ $^$? — All prerequisites that are newer than the target.
lib.a: a.o b.o
ar rcs $@ $?$* — The stem matched by a pattern rule (% part).
%.o: %.c
echo "Compiling $*"$(@D) / $(@F) — The directory / filename part of the target.
build/%.o: src/%.c
mkdir -p $(@D)
$(CC) -c $< -o $@Pattern Rules & Wildcards
%.o: %.c — Pattern rule: build any .o file from its corresponding .c file.
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@$(wildcard <pattern>) — Expand a glob pattern to matching filenames.
SOURCES := $(wildcard src/*.c)$(patsubst <from>,<to>,<text>) — Replace pattern in a list of words.
OBJECTS := $(patsubst %.c,%.o,$(SOURCES))$(SOURCES:.c=.o) — Substitution reference: shorthand for patsubst.
OBJECTS := $(SOURCES:.c=.o)$(filter <pattern>,<text>) — Keep only words matching the pattern.
C_FILES := $(filter %.c,$(ALL_FILES))$(filter-out <pattern>,<text>) — Remove words matching the pattern.
SOURCES := $(filter-out test_%,$(ALL_SOURCES))Functions
$(shell <command>) — Execute a shell command and capture its output.
GIT_HASH := $(shell git rev-parse --short HEAD)$(info <text>) — Print an informational message during Makefile parsing.
$(info Building version $(VERSION))$(warning <text>) — Print a warning message (includes file and line number).
$(warning CC is set to $(CC))$(error <text>) — Print an error message and stop execution.
ifndef CONFIG
$(error CONFIG is not set)
endif$(foreach var,list,body) — Loop: evaluate body for each word in list.
$(foreach dir,src lib test,$(wildcard $(dir)/*.c))$(if condition,then,else) — Conditional function: return then if condition is non-empty.
FLAGS := $(if $(DEBUG),-g -O0,-O2)$(call func,arg1,arg2) — Call a user-defined function (multi-line macro).
define compile
$(CC) $(CFLAGS) -c $(1) -o $(2)
endef
%.o: %.c
$(call compile,$<,$@)Conditionals
ifeq ($(VAR),value) … endif — Conditional block: execute if variable equals value.
ifeq ($(OS),Linux)
CFLAGS += -DLINUX
endififneq ($(VAR),value) … endif — Conditional block: execute if variable does NOT equal value.
ifneq ($(DEBUG),)
CFLAGS += -g
endififdef VAR … endif — Conditional block: execute if variable is defined.
ifdef VERBOSE
Q :=
else
Q := @
endififndef VAR … endif — Conditional block: execute if variable is NOT defined.
ifndef CC
CC := gcc
endifSpecial Targets & Directives
.PHONY: <targets> — Declare targets that are not actual files (always run).
.PHONY: all build clean test deploy.DEFAULT_GOAL := <target> — Set the default target if none is specified on the command line.
.DEFAULT_GOAL := build.SILENT: <targets> — Do not print recipes for the specified targets.
.SILENT: helpinclude <file> — Include another Makefile (error if not found).
include config.mk-include <file> — Include another Makefile (silently ignore if not found).
-include .env.mk@<command> — Prefix a recipe line with @ to suppress printing the command.
help:
@echo "Available targets: build, test, clean"-<command> — Prefix with - to ignore errors from this command.
clean:
-rm -f *.o appCommon Makefile Patterns
all: build — Convention: 'all' target builds everything.
all: build docsclean: — Convention: 'clean' removes build artifacts.
.PHONY: clean
clean:
rm -rf build/ dist/install: — Convention: 'install' copies built files to system directories.
install: build
install -m 755 app $(PREFIX)/bin/test: — Convention: 'test' runs the test suite.
.PHONY: test
test:
go test ./...help: — Self-documenting help target: extracts ## comments from targets.
build: ## Build the application
go build -o app
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | sort | \
awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' Conclusion
Make has been the Unix and Linux standard for build automation for decades, and despite a wave of newer build tools it remains ubiquitous. Once you grasp the three core building blocks – targets, prerequisites and recipes – you can use Make for almost any repetitive task, not just compiling C code. Mind the infamous tab requirement in recipe lines and declare any target that doesn't produce a file with .PHONY. Do that and your Makefile stays robust, portable and easy for collaborators and AI systems alike to follow.
Further Reading
- GNU Make Manual – official reference for every function and directive
- Make – Wikipedia – overview of history, concepts and variants