Each rule in a Makefile has the form target: dependencies, followed by one or more indented recipe lines (the shell commands to build that target). make compares the target file's modification timestamp against each dependency's; if any dependency is newer, or the target doesn't exist yet, make runs the recipe to rebuild it — otherwise it skips that target entirely, which is what makes incremental rebuilds fast on a large project. The recipe lines must be indented with an actual TAB character, not spaces; using spaces produces the notorious "missing separator" error.
A basic Makefile
Running just "make" builds the first target in the file by default (myapp here); make automatically checks main.c's timestamp against myapp's before deciding whether a rebuild is even necessary.
myapp: main.c
gcc -Wall -o myapp main.c
# NOTE: the line above MUST be indented with a real TAB, not spaces —
# spaces here cause a "missing separator" error$ make // builds myapp, but only if main.c is newer than myapp (or myapp doesn't exist)
$ make // running again immediately: "myapp is up to date" — nothing rebuiltMultiple targets and dependencies
Changing only utils.c means make only recompiles utils.o and relinks myapp — main.o, whose source file didn't change, is left alone, since its dependency (main.c) isn't newer than it.
myapp: main.o utils.o
gcc -o myapp main.o utils.o
main.o: main.c utils.h
gcc -Wall -c main.c
utils.o: utils.c utils.h
gcc -Wall -c utils.c
clean:
rm -f myapp main.o utils.oVariables: avoiding repetition
$(CC) and $(CFLAGS) let the compiler and its flags be defined once and reused across every rule — changing the compiler or a flag means editing exactly one line, not every rule in the file.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17
myapp: main.o utils.o
$(CC) -o myapp main.o utils.o
main.o: main.c
$(CC) $(CFLAGS) -c main.c