We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking "Accept All", you consent to our use of cookies. See our Privacy Policy for more information.
GNU Make Mastery Part 2: Targets, Prerequisites & Execution Model
February 19, 2026Wasil Zafar18 min read
Deep-dive into Make rule anatomy — multiple targets, default target behaviour, order-only prerequisites, file vs phony targets, and building complete clean/build/run workflow pipelines.
Part 2 of 16 — GNU Make Mastery Series. In Part 1 we covered why build systems exist and Makefile fundamentals. Now we go deeper into how Make actually executes rules and makes decisions about what to rebuild.
Every Make rule follows the same three-part structure. Understanding this structure — and the precise semantics of each part — is the foundation for everything else in Make.
Every Make rule has three parts: target, prerequisites, and recipe lines
When you run make with no arguments, Make builds the first target it encounters in the Makefile (excluding targets whose names begin with .). This is called the default goal.
Default goal selection — Make builds the first target encountered in the Makefile
Best Practice: Always make the first target either all or your main build artefact. Use .DEFAULT_GOAL := all to make the intent explicit and immune to file ordering accidents.
# Explicit default goal — immune to Makefile reordering
.DEFAULT_GOAL := all
.PHONY: all clean test
all: myapp
myapp: main.o utils.o
$(CC) -o $@ $^
Common Mistake — clean as the First Target: If you put clean: at the top of the Makefile, running make with no arguments will delete your build instead of building. This is a surprisingly common beginner error:
# BAD — clean is the first target, so "make" deletes everything!
clean:
rm -rf build/ *.o myapp
myapp: main.o utils.o
$(CC) -o $@ $^
# FIX — put "all" first, or use .DEFAULT_GOAL:
.DEFAULT_GOAL := all
all: myapp
clean:
rm -rf build/ *.o myapp
Multiple Targets
A rule can list several targets on the left-hand side. This is shorthand: Make creates one rule per target, all sharing the same prerequisites and recipe.
# Build two executables from separate main files
server client: common.o config.o
$(CC) $(CFLAGS) -o $@ $@.o $^
# Equivalent to writing:
# server: common.o config.o
# $(CC) $(CFLAGS) -o server server.o common.o config.o
# client: common.o config.o
# $(CC) $(CFLAGS) -o client client.o common.o config.o
Types of Prerequisites
Normal Prerequisites
Normal prerequisites (target: prereq1 prereq2) impose two constraints on Make:
Normal prerequisites trigger rebuilds on change; order-only prerequisites only ensure existence
All prerequisites must be up to date before the target's recipe runs.
If any prerequisite is newer than the target, the target is considered out of date and its recipe runs.
Order-Only Prerequisites (|)
Order-only prerequisites (listed after |) impose execution order but do not trigger a rebuild if they are newer than the target. This is ideal for ensuring directories exist before writing files into them.
BUILD_DIR := build
# build/ directory must exist before writing .o files,
# but a timestamp change on the dir should NOT force recompiling all objects
$(BUILD_DIR)/%.o: %.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR):
mkdir -p $@
Common Mistake
Directory as Normal Prerequisite
If you list a directory as a normal prerequisite, Make will rebuild all its targets every time any file in the directory changes (since the directory mtime updates). Use | to make it order-only — you only need the directory to exist, not to be unchanged.
File vs Phony Targets
By default, every Make target is assumed to be a file. If a file named clean exists in the directory, running make clean will report "Nothing to be done" — the target appears up to date because the file exists and has no prerequisites.
Declaring a target as .PHONY tells Make: this target is not a file — always run its recipe when requested.
File targets track real files on disk; phony targets always execute their recipes
# Declare these targets as "not real files" — always run their recipes
.PHONY: all clean build test install uninstall
clean: # remove all build outputs
rm -rf build/ *.o
test: all # build first, then run tests
./run_tests.sh
install: all # build first, then copy binary
cp myapp /usr/local/bin/
Common Phony Conventions
Convention
Standard Phony Targets
Target
Purpose
all
Build everything (default goal)
clean
Remove all generated files
distclean
Remove everything including configuration
test
Run test suite
install
Install built artefacts
uninstall
Remove installed artefacts
help
Print available targets
Clean/Build/Run Workflows
The following complete Makefile expects a src/ directory with C files. Create them first:
The clean → build → run workflow cycle — standard development loop with Make
# ── Variables ────────────────────────────────────────
CC := gcc
CFLAGS := -Wall -Wextra -std=c11
TARGET := myapp
SRCS := $(wildcard src/*.c) # auto-discover all .c files under src/
OBJS := $(patsubst src/%.c, build/%.o, $(SRCS)) # map src/foo.c → build/foo.o
.PHONY: all clean run rebuild help
# ── Default target ─────────────────────────────────
all: $(TARGET)
# ── Link: combine .o files into final binary ─────
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ # $@ = target name, $^ = all prerequisites
# ── Compile: .c → .o (order-only prereq creates build/ dir first)
build/%.o: src/%.c | build
$(CC) $(CFLAGS) -c $< -o $@ # $< = first prerequisite (the .c file)
build: # create the output directory if missing
mkdir -p build
# ── Utility targets ───────────────────────────────
clean: # delete all generated files
rm -rf build $(TARGET)
run: all # build then execute
./$(TARGET)
rebuild: clean all # fresh build from scratch
help:
@echo "Available targets:"
@echo " all - Build $(TARGET) (default)"
@echo " clean - Remove build artefacts"
@echo " run - Build and run"
@echo " rebuild - Clean then build"
@echo " info - Display variable values"
@echo " help - Show this message"
# ── Info: display variable values — run: make info ──
info:
@echo "CC = $(CC)"
@echo "CFLAGS = $(CFLAGS)"
@echo "TARGET = $(TARGET)"
@echo "SRCS = $(SRCS)"
@echo "OBJS = $(OBJS)"
# --- Try the workflow targets ---
make # build the project
make info # display all variable values
make run # build and run
make clean # remove artefacts
make rebuild # clean + build in one step
make help # list available targets
Hands-On Milestone
Milestone: By the end of Part 2 you should be able to:
✅ Build a simple input → processed → output pipeline with Make rules
✅ Use order-only prerequisites (|) for build directories
✅ Declare all non-file targets with .PHONY
✅ Add clean, run, rebuild, and help workflow targets
Continue the Series
Part 3: Variables, Expansion & Scope
Master recursive vs simple variables, conditional assignment, CLI overrides, target-specific and pattern-specific variables, and multiline macros.