Why this matters
Once a program has several files, typing the build commands by hand gets old fast, and rebuilding everything after every edit wastes time: real projects take minutes or hours to compile from scratch. make, which dates from 1976 and is still everywhere, solves both problems. You describe what each file is built from, and make works out the minimum set of commands needed to bring everything up to date. Even when projects use newer tools (CMake, Meson), those tools often generate Makefiles, and open-source C projects are still commonly built with ./configure && make.
The idea: a dependency graph plus timestamps
A Makefile is a list of rules. Each rule says: this target file is made from these prerequisite files, by running this recipe:
target: prerequisite1 prerequisite2
recipe command
another command
When you ask make for a target, it:
- Makes sure each prerequisite is up to date first (recursively, using their own rules).
- Compares modification times: if the target does not exist, or any prerequisite is newer than it, it runs the recipe.
- Otherwise it does nothing for that target.
shapes
/ \
main.o geometry.o
/ \ / \
main.c geometry.h geometry.c
edit geometry.c -> geometry.o is older than geometry.c -> recompile geometry.o
-> shapes is older than geometry.o -> relink shapes
-> main.o is newer than its inputs -> left alone
That is the whole model. make does not know anything about C: it only knows files, times, and shell commands.
A first Makefile
For the geometry program from the multi-file lesson (main.c, geometry.c, geometry.h):
CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -g
LDLIBS = -lm
shapes: main.o geometry.o
$(CC) $(CFLAGS) -o shapes main.o geometry.o $(LDLIBS)
main.o: main.c geometry.h
$(CC) $(CFLAGS) -c main.c
geometry.o: geometry.c geometry.h
$(CC) $(CFLAGS) -c geometry.c
.PHONY: clean
clean:
rm -f shapes main.o geometry.o
$ make
gcc -std=c17 -Wall -Wextra -g -c main.c
gcc -std=c17 -Wall -Wextra -g -c geometry.c
gcc -std=c17 -Wall -Wextra -g -o shapes main.o geometry.o -lm
$ make
make: 'shapes' is up to date.
$ touch geometry.c
$ make
gcc -std=c17 -Wall -Wextra -g -c geometry.c
gcc -std=c17 -Wall -Wextra -g -o shapes main.o geometry.o -lm
$ touch geometry.h
$ make -n
gcc -std=c17 -Wall -Wextra -g -c main.c
gcc -std=c17 -Wall -Wextra -g -c geometry.c
gcc -std=c17 -Wall -Wextra -g -o shapes main.o geometry.o -lm
$ make clean
rm -f shapes main.o geometry.o
The pieces:
- The first rule is the default. Plain
makebuilds the first target in the file, hereshapes.make cleanbuilds the target namedclean. - Recipes are indented with a tab, not spaces. This is make's most notorious rule.
- Each recipe line runs in its own shell, and make prints it before running it. If a command fails (nonzero exit status), make stops; this is where your exit statuses matter.
touch fileupdates a file's modification time without changing it, a handy way to pretend you edited it.make -n("dry run") prints the commands it would run without running them.
Variables
CC = gcc defines a variable; $(CC) uses it. Conventional names, which make's built-in rules also use:
| variable | meaning |
|---|---|
CC | the C compiler |
CFLAGS | flags for compiling C |
CPPFLAGS | preprocessor flags, like -Iinclude or -DNDEBUG |
LDFLAGS | flags for the linker, like -L/some/lib/dir |
LDLIBS | libraries, like -lm, placed after the object files |
Variables can be overridden from the command line without editing the file:
$ make clean && make CFLAGS="-O2 -DNDEBUG"
.PHONY targets
clean does not produce a file called clean; it is just a name for a set of commands. If a file named clean ever appeared in the directory, make would see an up-to-date target with no prerequisites and refuse to run the recipe (make: 'clean' is up to date.). Listing it under .PHONY tells make it is not a file, so it always runs. Other common phony targets are all (build everything), test and install.
Automatic variables and pattern rules
Writing one rule per .c file does not scale. make provides automatic variables, set fresh for each rule as it runs:
| variable | expands to |
|---|---|
$@ | the target |
$< | the first prerequisite |
$^ | all prerequisites, without duplicates |
and pattern rules, where % matches any stem:
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
"Any X.o can be made from X.c with this command." With these, a Makefile for any number of files stays short:
CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -g -MMD -MP
LDLIBS = -lm
OBJS = main.o geometry.o
shapes: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
-include $(OBJS:.o=.d)
.PHONY: clean
clean:
rm -f shapes $(OBJS) $(OBJS:.o=.d)
Adding a module now means adding one word to OBJS. $(OBJS:.o=.d) is a substitution reference: the list with each .o replaced by .d.
Header dependencies, automatically
The pattern rule says main.o depends on main.c, but not on the headers main.c includes. If you edit geometry.h and make does not know, it will not recompile, and you can end up linking object files compiled against different versions of a struct: a nasty, silent bug. Listing headers by hand works for small projects but goes stale. The compiler can write the list for you:
-MMDmakes gcc write, next to eachX.o, a fileX.dcontaining a make rule listing the headers it actually included (system headers excluded).-MPadds an empty rule for each header, so deleting a header does not break the build.-include $(OBJS:.o=.d)pulls those files into the Makefile; the-means "do not complain if they do not exist yet" (on the very first build).
$ make
gcc -std=c17 -Wall -Wextra -g -MMD -MP -c main.c -o main.o
gcc -std=c17 -Wall -Wextra -g -MMD -MP -c geometry.c -o geometry.o
gcc -std=c17 -Wall -Wextra -g -MMD -MP -o shapes main.o geometry.o -lm
$ cat main.d
main.o: main.c geometry.h
geometry.h:
$ touch geometry.h && make
gcc -std=c17 -Wall -Wextra -g -MMD -MP -c main.c -o main.o
gcc -std=c17 -Wall -Wextra -g -MMD -MP -c geometry.c -o geometry.o
gcc -std=c17 -Wall -Wextra -g -MMD -MP -o shapes main.o geometry.o -lm
This little block is a good template for your own small C projects.
Built-in rules
make already knows how to build X.o from X.c (with $(CC) $(CPPFLAGS) $(CFLAGS) -c) and even X from X.c in one step. In an empty directory with hello.c, make hello works with no Makefile at all. That is convenient for one-file experiments; in real Makefiles, spelling out the rules makes them easier to read.
Common mistakes
- Spaces instead of a tab before a recipe:
Makefile:6: *** missing separator. Stop. - Missing header prerequisites, so changes to a header do not trigger rebuilds. Use
-MMD -MPor list them carefully. - Forgetting
.PHONYonclean,allortest. - Expecting
cdto persist between recipe lines. Each line is a separate shell; writecd dir && makeon one line. - Putting
-lmbefore the object files. The linker resolves left to right, so libraries go last; that is whyLDLIBSis placed at the end. - Asking for a target with no rule: The same message appears when a prerequisite file is missing, for example a misspelled header name in a rule.
make: *** No rule to make target 'nothing'. Stop.
Recap
- A Makefile is rules:
target: prerequisitesfollowed by tab-indented recipe lines. - make rebuilds a target when it is missing or older than any prerequisite, working through the dependency graph, and stops on the first failing command.
- Variables (
CC,CFLAGS,LDLIBS) keep flags in one place and can be overridden on the command line. $@,$<and$^, plus pattern rules like%.o: %.c, keep Makefiles short..PHONYmarks targets that are not files;-MMD -MPwith-includetracks header dependencies automatically.
Going further: make -p in an empty directory prints all of make's built-in rules and variables. The GNU make manual (info make, or online) is unusually readable; its "An Introduction to Makefiles" chapter walks through a complete example.