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

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 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

References