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.

make

make <target> — Run a specific target.

make build

make -f <file> — Use a specific Makefile instead of the default.

make -f Makefile.prod build

make -n — Dry run: print commands that would be executed without running them.

make -n deploy

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

make -B — Unconditionally build all targets (force rebuild).

make -B

make -s — Silent mode: do not print the commands as they are executed.

make -s build

make -k — Keep going: continue building other targets even if one fails.

make -k test

make -C <directory> — Change to directory before reading the Makefile.

make -C src/ build

make VAR=value — Override a variable on the command line.

make CC=clang build

Makefile 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.o

VAR = value — Recursively expanded variable (re-evaluated on each use).

CC = gcc
CFLAGS = -Wall -O2

VAR := 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/local

VAR += value — Append to an existing variable.

CFLAGS += -g

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

ifneq ($(VAR),value) … endif — Conditional block: execute if variable does NOT equal value.

ifneq ($(DEBUG),)
CFLAGS += -g
endif

ifdef VAR … endif — Conditional block: execute if variable is defined.

ifdef VERBOSE
Q :=
else
Q := @
endif

ifndef VAR … endif — Conditional block: execute if variable is NOT defined.

ifndef CC
CC := gcc
endif

Special 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: help

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

Common Makefile Patterns

all: build — Convention: 'all' target builds everything.

all: build docs

clean: — 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

  • artisan – command-line tool of the Laravel framework
  • cargo – build system and package manager for Rust
  • composer – dependency manager for PHP projects