Makefiles

C · Lesson 26 of 32 9 min read Updated

Once a program has several source files, typing the compile commands by hand becomes slow and error-prone. Did you recompile bill.c after changing bill.h? Did you forget -Wall this time? The classic tool that solves this is make. You describe how the program is built in a file called Makefile, and make runs exactly the commands needed, and only those.

In this lesson you will write a Makefile for the billing program from Header Files & Multi-file Programs, see how make decides what to rebuild, and improve the Makefile with variables, pattern rules and automatic header dependencies.

Installing and running make

On Linux and WSL, make comes with build-essential. On macOS it comes with the Command Line Tools. On Windows with MSYS2, the base-devel package provides make inside the MSYS2 UCRT64 terminal, and the toolchain also provides the same program as mingw32-make.exe for use in PowerShell. Check it with:

Terminal
make --version

When you type make in a folder, it reads the file named Makefile in that folder and builds the first target it describes.

Your first Makefile

A Makefile is a list of rules. Each rule has a target (the file to build), its prerequisites (the files it is built from) and a recipe (the commands that build it):

Text
target: prerequisites
	recipe command

The recipe line must start with a tab character, not spaces. This is the most famous quirk of make. Here is a complete Makefile for the billing program, with main.c, bill.c and bill.h in the same folder:

Text
shop: main.o bill.o
	gcc main.o bill.o -o shop

main.o: main.c bill.h
	gcc -std=c23 -Wall -Wextra -c main.c

bill.o: bill.c bill.h
	gcc -std=c23 -Wall -Wextra -c bill.c

clean:
	rm -f shop main.o bill.o

Read it from the top: to build shop, make first needs main.o and bill.o. To build main.o, it needs main.c and bill.h, and so on. Running make in a fresh folder prints each command before running it:

Terminal
make
Output
gcc -std=c23 -Wall -Wextra -c main.c
gcc -std=c23 -Wall -Wextra -c bill.c
gcc main.o bill.o -o shop

How make decides what to rebuild

make compares modification times. A target is rebuilt only if it does not exist or if any prerequisite is newer than it. Run make again straight away and nothing needs doing:

Terminal
make
Output
make: 'shop' is up to date.

Now edit bill.c (or use touch bill.c, which just updates its time) and run make once more:

Terminal
touch bill.c
make
Output
gcc -std=c23 -Wall -Wextra -c bill.c
gcc main.o bill.o -o shop

Only bill.o was rebuilt, then the program was linked again. main.o was still newer than main.c and bill.h, so make skipped it. If you change bill.h, both object files are rebuilt, because both rules list bill.h as a prerequisite. This is why listing headers correctly matters: a missing header prerequisite means stale object files and confusing bugs.

make clean runs the clean rule, which deletes the generated files so the next build starts fresh. You can name any target on the command line: make bill.o builds just that file.

Variables and automatic variables

Repeating gcc -std=c23 -Wall -Wextra in every rule invites mistakes. Makefile variables hold such text once. By convention, CC names the C compiler and CFLAGS holds the compiler options. Rules can also use automatic variables, which make fills in for each rule:

VariableMeaning
$@The target of the rule
$<The first prerequisite
$^All the prerequisites

With these, a pattern rule written with % can describe how to build any .o file from the .c file with the same name:

Text
CC = gcc
CFLAGS = -std=c23 -Wall -Wextra -g
OBJS = main.o bill.o

shop: $(OBJS)
	$(CC) $^ -o $@

%.o: %.c bill.h
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:
	rm -f shop $(OBJS)
Terminal
make clean
make
Output
rm -f shop main.o bill.o
gcc -std=c23 -Wall -Wextra -g -c main.c -o main.o
gcc -std=c23 -Wall -Wextra -g -c bill.c -o bill.o
gcc main.o bill.o -o shop

To add a new file such as storage.c, you now only add storage.o to OBJS. To build with different options for one run, override a variable on the command line: make CFLAGS="-std=c23 -O2". If your program uses math.h, add LDLIBS = -lm and put $(LDLIBS) at the end of the link command.

Phony targets

clean is not a file; it is just a name for a task. .PHONY: clean tells make so. Without it, if a file called clean ever appeared in the folder, make would think the target was up to date and refuse to run the recipe. Other common phony targets are all (build everything, usually listed first) and run:

Text
.PHONY: all run clean
all: shop

run: shop
	./shop

Automatic header dependencies

In a large project, keeping every header list correct by hand is impossible. GCC and Clang can write the dependency lists for you. The options -MMD -MP make the compiler produce a small .d file next to each .o, listing the headers that file really included, and make can read those files:

Text
CC = gcc
CFLAGS = -std=c23 -Wall -Wextra -g -MMD -MP
OBJS = main.o bill.o

shop: $(OBJS)
	$(CC) $^ -o $@

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:
	rm -f shop $(OBJS) $(OBJS:.o=.d)

-include $(OBJS:.o=.d)

$(OBJS:.o=.d) turns the list main.o bill.o into main.d bill.d. The - before include means "do not complain if the files do not exist yet", which is the case on the very first build. From then on, any header change triggers exactly the right rebuilds. Add -j to build several files in parallel: make -j4 runs up to four commands at once.

Common mistakes

  • Spaces instead of a tab. make stops with *** missing separator. Stop. and the line number. Set your editor to keep real tabs in Makefiles; VS Code does this automatically for files named Makefile.
  • Missing header prerequisites. Changes to a header do not trigger rebuilds. List headers or use -MMD -MP.
  • Forgetting .PHONY. A file named like a task, such as clean, stops that task from running.
  • Wrong folder. make: *** No targets specified and no makefile found. Stop. means there is no Makefile where you ran the command.
  • Assuming the output is always rebuilt. If nothing changed, make does nothing. Use make clean followed by make to force a full rebuild.

Key points

  • A Makefile is a list of rules: target: prerequisites, then recipe lines that start with a tab.
  • make rebuilds a target only when it is missing or older than one of its prerequisites.
  • Variables such as CC and CFLAGS keep options in one place; $@, $< and $^ stand for the target and prerequisites.
  • A pattern rule %.o: %.c builds every object file the same way.
  • Mark task names such as clean, all and run as .PHONY.
  • -MMD -MP with -include keeps header dependencies correct automatically.

Exercise

Write a Makefile for the temperature converter from the previous lesson (main.c, convert.c, convert.h). Use CC, CFLAGS and an OBJS variable, a pattern rule for object files, a link rule for a program called convert, and phony run and clean targets. Then write the commands you would type to build and run it.

Show solution
Text
CC = gcc
CFLAGS = -std=c23 -Wall -Wextra -g
OBJS = main.o convert.o

convert: $(OBJS)
	$(CC) $^ -o $@

%.o: %.c convert.h
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: run clean
run: convert
	./convert

clean:
	rm -f convert $(OBJS)
Terminal
make run
Output
gcc -std=c23 -Wall -Wextra -g -c main.c -o main.o
gcc -std=c23 -Wall -Wextra -g -c convert.c -o convert.o
gcc main.o convert.o -o convert
./convert
37.0 C = 98.6 F
212.0 F = 100.0 C

make run first builds convert, because run depends on it, then runs it. make prints each command, including ./convert, before the program's own output. Every recipe line starts with a tab.

Quick Quiz

1. What must the recipe lines of a Makefile rule start with?

2. When does make rebuild bill.o in the rule bill.o: bill.c bill.h?

3. In a rule, what does $< mean?

4. Why mark clean as .PHONY?

Learn C with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call