GNU Make
Make is one of the oldest build tools still in daily use, and GNU Make is its most widely used implementation. Its idea is small and powerful: a Makefile lists targets, the prerequisites each depends on, and the recipe that builds it. Make compares file timestamps and rebuilds only what is out of date — so after changing one source file, only that file and whatever depends on it are rebuilt. The same mechanism makes Make a popular task runner: make test, make lint, and make deploy give every project a uniform entry point.
Makefiles have a reputation for being cryptic, mostly because of a few sharp edges: tabs, multiple variable assignment operators, every recipe line running in a separate shell, and missing dependency information. Knowing those rules turns Make into a dependable tool.
TL;DR
- A rule is
target: prerequisitesfollowed by tab-indented recipe lines. - Make rebuilds a target when it is missing or older than any prerequisite.
- Use
:=for simple variables,?=for overridable defaults, and understand that=expands lazily. - Automatic variables:
$@(target),lt;(first prerequisite),$^(all prerequisites). - Pattern rules (
%.o: %.c) build many files with one rule;-MMD -MPgenerates header dependencies. - Mark task targets
.PHONY, run builds in parallel with-j, and use strict shell flags so failures stop the build.
Quick Example
A Makefile for a C program with automatic header dependencies, parallel-safe directories, and self-documenting tasks. Recipe lines start with a tab character:
Core Concepts
Rules, Targets, and Recipes
Make starts from the requested target (or the first target in the file, the default goal), builds prerequisites first, and runs the recipe if the target is missing or older than any prerequisite. Each recipe line runs in its own shell unless .ONESHELL is set, so cd dir on one line does not affect the next.
Variables
Command-line assignments (make CFLAGS=-O0) override Makefile values, which makes ?= defaults convenient. Functions such as $(wildcard ...), $(patsubst ...), $(shell ...), and substitution references ($(SRCS:.c=.o)) compute lists.
Automatic Variables
Pattern Rules and Header Dependencies
$(BUILD)/%.o: src/%.c builds any object from its source. But objects also depend on the headers they include, which Make cannot know by itself. Compiling with -MMD -MP writes a .d file per object listing its headers; -include loads them, so editing a header rebuilds exactly the files that include it.
Phony and Order-Only Prerequisites
.PHONY targets (test, clean) are commands, not files; marking them prevents a stray file named test from making the target look up to date. Order-only prerequisites (after |) must exist before the target is built but do not trigger rebuilds when their timestamp changes — ideal for output directories.
Parallel and Reliable Builds
make -j Nruns independent recipes in parallel. It only works if every dependency is declared; missing prerequisites cause intermittent failures under-j..DELETE_ON_ERRORremoves half-written targets when a recipe fails, so the next run does not trust a corrupt file.- Strict shell flags (
-eu -o pipefail) make a failing command in a pipeline fail the recipe. - Avoid recursive make across subdirectories where possible; a single Makefile (or included fragments) gives Make the complete dependency graph.
Make as a Task Runner
Many projects that never compile C use Make for a consistent developer interface:
This works well for short, discoverable commands. For complex task graphs, dedicated runners (just, Task, npm scripts) or full build systems may be clearer.
Make and Other Build Tools
Best Practices
Declare Every Dependency
If a recipe reads a file, that file should be a prerequisite. Complete dependencies are what make incremental and parallel builds correct.
Keep Recipes Simple
Move complex logic into scripts that the Makefile calls; Make syntax is a poor programming language.
Document Targets
Use a help target with ## comments so new contributors can discover commands with make help.
Target GNU Make Explicitly
GNU-specific features (pattern rules, functions, .ONESHELL) are not portable to BSD make. Say so in the README, or use gmake on BSD and macOS systems with older make versions.
Common Mistakes
Spaces Instead of Tabs
Expecting cd to Persist
Missing Header Dependencies
FAQ
What is GNU Make used for?
Building software incrementally from a Makefile that describes targets, their dependencies, and the commands that produce them — classically C and C++ programs — and running project tasks such as tests, linting, and deployment.
Why does my Makefile say "missing separator"?
Recipe lines must start with a tab character. The error usually means spaces were used instead, often inserted by an editor.
What is the difference between = and := in Make?
= defines a recursively expanded variable, evaluated each time it is used, so it can reference variables defined later. := expands the value once, immediately, which is more predictable and often faster.
What does .PHONY do?
It tells Make that a target is a name for a command rather than a file, so the recipe always runs when requested, even if a file with that name exists.
Should I use Make or CMake?
For C and C++ projects that must build on several platforms and compilers, CMake (often generating Ninja or Makefiles) is usually better. Make is excellent for small native projects and as a simple task runner.
Related Topics
- Build Tools — The build tool landscape
- CMake — Cross-platform builds for C and C++
- Bazel — Hermetic builds at scale
- C — The language Make was built around
- Linux — Where Make is ubiquitous
- CI/CD — Running make targets in pipelines
- Ninja — A faster executor for generated build graphs