Makefile syntax is simple: targets, prerequisites, and recipes. A target is a build product (executable, object file). Prerequisites are the files it depends on. A recipe is the command to build it.
target: prerequisite1 prerequisite2 command to build target
The indentation before the command must be a tab character, not spaces. This is a common gotcha—use echo $'\t' to verify a tab.
Example:
app: main.o util.o gcc -o app main.o util.o main.o: main.c gcc -c -o main.o main.c util.o: util.c gcc -c -o util.o util.c
This defines three targets: app depends on main.o and util.o; each .o depends on its source .c. When you run make, Make checks timestamps and rebuilds what's out of date.
How Make works:
Running Make:
make # build the first target (usually 'all') make app # build the 'app' target specifically make clean # run the 'clean' target (usually deletes build artifacts) make -f other.mk # use a different file (default: Makefile)
Variables in recipes:
CFLAGS = -O3 -Wall -g CC = gcc app: main.o $(CC) $(CFLAGS) -o app main.o
$(VAR) expands the variable. CC is the compiler, CFLAGS are compilation flags. Define once, use everywhere.
Automatic dependencies: Make re-checks timestamps every build. If main.c is newer than main.o, main.o is rebuilt. If main.o is newer than app, app is rebuilt. Unchanged files skip their recipes.
Phony targets: Not all targets produce files. make clean deletes files; it doesn't produce a “clean” file. Mark these as .PHONY:
.PHONY: clean clean: rm -f *.o app
Without .PHONY, if a file named clean exists, make clean won't run (Make thinks the target is up to date).
Comments: Lines starting with # are comments.
# This is a comment app: main.o # inline comments work too gcc -o app main.o