The preprocessor, multi-file programs and make · about 55 min

Building with make

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:

  1. Makes sure each prerequisite is up to date first (recursively, using their own rules).
  2. Compares modification times: if the target does not exist, or any prerequisite is newer than it, it runs the recipe.
  3. 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:

Variables

CC = gcc defines a variable; $(CC) uses it. Conventional names, which make's built-in rules also use:

variablemeaning
CCthe C compiler
CFLAGSflags for compiling C
CPPFLAGSpreprocessor flags, like -Iinclude or -DNDEBUG
LDFLAGSflags for the linker, like -L/some/lib/dir
LDLIBSlibraries, 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:

variableexpands 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:

$ 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

Recap

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.

Exercises

Exercise 1 · quiz

A famous error

You type in a Makefile from a web page and run make:

Makefile:6: *** missing separator.  Stop.

What is the most likely cause?

Answer
  • The recipe line 6 starts with spaces instead of a tab character

Recipe lines must begin with a real tab character. Copying from a web page or an editor that converts tabs to spaces produces lines that look identical but start with spaces, and make cannot tell they belong to a rule. In vim, :set list shows tabs as ^I; cat -A Makefile shows them too. Configure your editor to keep tabs in Makefiles.

Exercise 2 · quiz

What gets rebuilt?

With this Makefile, everything has just been built. Then you edit geometry.h and run make.

shapes: main.o geometry.o util.o
    gcc -o shapes main.o geometry.o util.o -lm

main.o: main.c geometry.h
    gcc -c main.c

geometry.o: geometry.c geometry.h
    gcc -c geometry.c

util.o: util.c
    gcc -c util.c

Which commands run? Select all that apply.

Answer
  • gcc -c main.c

  • gcc -c geometry.c

  • gcc -o shapes main.o geometry.o util.o -lm

make compares timestamps: main.o and geometry.o both list geometry.h as a prerequisite, and the header is now newer than they are, so both are recompiled. That makes them newer than shapes, so shapes is relinked. util.o depends only on util.c, which did not change, so it is reused. If a dependency on a header is missing from the Makefile, make will not rebuild, and you get a program built from object files that disagree about a struct layout.

Exercise 3 · quiz

Automatic variables

In the rule below, what does $^ expand to when make runs the recipe? Type the expansion.

report: main.o stats.o
    $(CC) -o $@ $^
Answer

$^ is all the prerequisites, separated by spaces: main.o stats.o. $@ is the target (report), and $< is the first prerequisite only (main.o), which is what you want in a compile rule like %.o: %.c, where the first prerequisite is the source file.

Exercise 4 · task in a shell

Write a Makefile

The directory proj in your workspace holds a small program: main.c, stats.c and stats.h (both .c files include stats.h). Write proj/Makefile so that:

  1. make (with no arguments) builds an executable called report, by first compiling main.c and stats.c separately into main.o and stats.o with the -c flag, then linking the two object files. Link with -lm (stats.c uses sqrt).
  2. Compile with a CFLAGS variable containing at least -std=c17 -Wall -Wextra.
  3. Both object files list stats.h as a prerequisite, so editing the header rebuilds both, while editing only stats.c recompiles only stats.o (and relinks).
  4. A phony target clean removes report and the .o files.

Remember: every recipe line starts with a tab. Work with cd proj, nano Makefile, make, ./report, make clean. The check copies proj elsewhere and tests each rule, including what gets rebuilt after touching each file.

Reference solution
cd proj
# Recipe lines must start with a TAB; the sed at the end turns the 4 leading spaces into one.
cat > Makefile <<'EOF'
CC     = gcc
CFLAGS = -std=c17 -Wall -Wextra -g
LDLIBS = -lm

report: main.o stats.o
    $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)

main.o: main.c stats.h
    $(CC) $(CFLAGS) -c $<

stats.o: stats.c stats.h
    $(CC) $(CFLAGS) -c $<

.PHONY: clean
clean:
    rm -f report main.o stats.o
EOF
sed -i 's/^    /\t/' Makefile
make
./report