Module 2: Rules and User-Facing Targets

Rules and Targets in Make

Learning objectives

  • Explain the core mental model behind Rules and Targets in Make
  • Apply Rules and Targets in Make within Rules and User-Facing Targets
  • Identify important boundaries, trade-offs, and failure modes
  • Produce concrete evidence from the practice exercise

Related: Make Basics | Variables | Automatic Variables | Pattern Rules | Phony Targets | Make Index


Why Rules Are the Core of Make

A Makefile is fundamentally a collection of rules. Each rule answers: "how do I build this output file, and what does it depend on?" Rules let Make understand the relationships between files so it can build only what's necessary.

Analogy: A rule is like a recipe card. The card says: "to make bread (target), you need flour, water, yeast (prerequisites). Here are the steps (recipe)." Make is the cook who reads all the recipe cards and figures out the right order to execute them.


Rule Syntax

target ... : prerequisite ...
	recipe
	recipe
  • target — the file to build (or a phony action name)
  • prerequisite (also called dependency) — files that must exist and be up to date before the recipe runs
  • recipe — one or more shell commands, each on a TAB-indented line
# Concrete example
main.o: main.c defs.h
	gcc -c -o main.o main.c

Targets

A target is almost always a filename that the recipe will create or update.

# Single file target
foo.o: foo.c foo.h
	gcc -c -o foo.o foo.c

# Target with no prerequisites — always runs if not up to date
version.h:
	echo '#define VERSION "1.0"' > version.h

# Multiple targets in one rule
# (each target gets the same set of prerequisites and recipe)
foo.o bar.o: config.h
	echo "config.h is a dep of both"

The Default Goal

The first real target in the Makefile is the default goal — what make (with no arguments) builds. Conventionally this is all:

all: program tests docs     # first target — this is what 'make' builds

program: main.o utils.o
	gcc -o program main.o utils.o

Prerequisites

Prerequisites are files that must exist and be up to date before the target's recipe runs. Make rebuilds the target if:

  • The target file doesn't exist, OR
  • Any prerequisite has a newer mtime than the target
# Normal (order) prerequisites
main.o: main.c defs.h utils.h
	gcc -c main.c

# If main.c or defs.h or utils.h change → main.o is rebuilt

Order-Only Prerequisites

Sometimes you need a directory to exist before building into it, but you don't want to rebuild the target every time the directory's timestamp changes. Use | to separate order-only prerequisites:

build/foo.o: foo.c | build/
	gcc -c -o $@ $<

build/:
	mkdir -p build/

# build/ existing is enough — its mtime doesn't trigger rebuilds

Recipes

A recipe is one or more shell commands that produce the target. Each line runs in a new shell (by default).

program: main.o utils.o
	@echo "Linking program..."       # @ suppresses echoing this line
	gcc -o program main.o utils.o
	@echo "Done."

Recipe Line Prefixes

PrefixMeaning
(none)Echo the command, then run it
@Run silently (no echo)
-Ignore errors (keep going even if this command fails)
+Run even with make -n (dry run)
install:
	@echo "Installing..."
	-rm -f /usr/bin/oldtool     # ignore error if oldtool doesn't exist
	cp mytool /usr/bin/mytool

Multi-Line Recipes

Since each recipe line is a separate shell, use \ for logical continuation or && for sequential commands:

build:
	mkdir -p obj && \
	cd src && \
	gcc -c *.c -I../include
	# All one logical command — cd affects the full block

Or enable .ONESHELL to make all recipe lines run in one shell:

.ONESHELL:
deploy:
	cd dist
	tar czf release.tar.gz *
	scp release.tar.gz server:/opt/releases/

Double-Colon Rules

Normal (single-colon) rules merge: if you write two rules for the same target, Make merges their prerequisites. Double-colon rules (::) are independent — each fires separately if its prerequisites are newer:

# Single-colon: prerequisites merge, only one recipe allowed
foo: a.c
foo: b.c     # merges: foo now depends on both a.c and b.c

# Double-colon: fully independent rules for the same target
docs:: README.md
	pandoc README.md -o docs.html

docs:: API.md
	pandoc API.md -o api.html
# Both run independently based on their own prerequisite timestamps

Double-colon rules are rare. The main use case is append-style rules in included library Makefiles.


Rules with No Recipe

A rule without a recipe just declares dependencies. Make uses these to update the dependency graph without any build action:

# Declare that widget.h depends on config.h
# (useful when auto-generated headers depend on config)
widget.h: config.h

# Now any target depending on widget.h will also rebuild if config.h changes

Static Pattern Rules

Static pattern rules apply a pattern to a specific list of targets (not all files matching the pattern globally). This is more explicit and predictable than implicit rules:

OBJECTS := main.o utils.o parser.o

# For each .o in OBJECTS, compile the corresponding .c
$(OBJECTS): %.o: %.c
	$(CC) -c $(CFLAGS) -o $@ $<

# This is equivalent to:
# main.o: main.c
# 	$(CC) -c $(CFLAGS) -o main.o main.c
# utils.o: utils.c
# 	...

Practical Full Example

CC      := gcc
CFLAGS  := -Wall -O2 -I./include
LDFLAGS :=
LIBS    := -lm

SRCS    := main.c utils.c parser.c
OBJS    := $(SRCS:.c=.o)
TARGET  := myprogram

# Default goal
all: $(TARGET)

# Link step
$(TARGET): $(OBJS)
	$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)

# Compile step — static pattern rule
$(OBJS): %.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

# Phony targets
.PHONY: all clean

clean:
	rm -f $(OBJS) $(TARGET)

Common Pitfalls

# PITFALL 1: Recipe line with spaces, not TAB
target: dep
    echo "this uses spaces — BROKEN"   # 4 spaces
	echo "this uses a TAB — works"     # TAB

# PITFALL 2: Multiple recipes for single-colon rule
foo: a.c
	echo "first recipe"
foo: b.c
	echo "second recipe"   # WARNING: this overrides the first recipe
# Use :: if you really want two independent rules

# PITFALL 3: Target not actually created by recipe
output.txt: input.txt
	grep "pattern" input.txt   # prints to stdout but doesn't create output.txt!
	# Fix: redirect: grep "pattern" input.txt > output.txt

# PITFALL 4: Forgetting order-only prereqs for directories
obj/main.o: main.c obj/         # WRONG: obj/ mtime will trigger rebuilds
obj/main.o: main.c | obj/       # RIGHT: order-only

Practice lab

Create a small repository that exercises Rules and Targets in Make. Capture a clean build, an incremental no-op build, and a deliberately broken dependency case; explain every command Make chooses to run.

Review questions

  1. What problem does Rules and Targets in Make solve, and what assumptions does it rely on?
  2. Which boundary or failure case is easiest to miss, and how would you expose it?
  3. What alternative design would you consider, and what trade-off would change the decision?
  4. What artifact, trace, test, or metric proves that your implementation is correct?

Completion evidence

  • A working artifact, annotated trace, or reproducible experiment
  • At least one normal case and one deliberately failing or boundary case
  • A concise explanation of the design choice and its trade-offs
  • Saved output showing how correctness was evaluated