Makefiles
Typing the compiler command for every file of a project is the mistake lesson 19 ends with. make reads a file of rules, compares timestamps, and rebuilds only what a change made stale. The whole tool is one dependency graph with commands attached to its edges.
A rule and a command
A rule names a target, lists its prerequisites after a colon, and gives the command that produces the target. The command line starts with a tab, not spaces. Make runs it only when the target is missing or older than a prerequisite, and prints each command before running it.
vi hello.c
#include <stdio.h>
int main(void)
{
printf("Hello from make\n");
return 0;
}
vi Makefile
hello: hello.c
gcc -Wall -Wextra hello.c -o hello
The first run builds the program, and the second finds the target newer than the source and does nothing:
make
./hello
make
gcc -Wall -Wextra hello.c -o hello
Hello from make
make: 'hello' is up to date.
That message is the point of the tool: no command ran, because nothing changed. A rebuild happens when a prerequisite is newer, not when the target is simply present.
Variables and automatic variables
Variables keep the compiler and its flags in one place, so a change applies to every rule. Make also defines automatic variables per rule: $@ is the target, $< the first prerequisite, and $^ all of them. Writing the same file name twice is how a rule drifts out of date.
#include <stdio.h>
int main(void)
{
printf("Hello from make\n");
return 0;
}
vi Makefile
CC = gcc
CFLAGS = -Wall -Wextra
hello: hello.c
$(CC) $(CFLAGS) $< -o $@
Touch the source to make it newer than the binary, then build again:
touch hello.c
make
gcc -Wall -Wextra hello.c -o hello
CC and CFLAGS are names make understands by convention, so a user can override them on the command line without editing the file, as in make CFLAGS=-O2.
Objects and pattern rules
A rule per object file would repeat itself; a pattern rule writes the shape once. %.o: %.c util.h says that any object depends on the source of the same name and on the shared header, and $< becomes that source. The header in the prerequisite list is what makes a header change rebuild both objects.
vi util.h
#ifndef UTIL_H
#define UTIL_H
int twice(int value);
#endif
vi util.c
#include "util.h"
int twice(int value)
{
return value * 2;
}
vi app.c
#include <stdio.h>
#include "util.h"
int main(void)
{
printf("%d\n", twice(21));
return 0;
}
vi Makefile
CC = gcc
CFLAGS = -Wall -Wextra
app: app.o util.o
$(CC) $^ -o $@
%.o: %.c util.h
$(CC) $(CFLAGS) -c $< -o $@
make
./app
gcc -Wall -Wextra -c app.c -o app.o
gcc -Wall -Wextra -c util.c -o util.o
gcc app.o util.o -o app
42
The last command links the two objects without compiling anything again. Touching util.h rebuilds both objects, because both depend on it, and then relinks the program.
Phony targets
Names such as all, clean and test are not files, so make would compare them with a file of the same name. Listing them in .PHONY tells make to run them every time. The first target is the default, which is why all comes first.
vi Makefile
The test target builds a second program, a check that returns success only when the library behaves, and runs it. That program is a complete file of its own:
#include "util.h"
int main(void)
{
return twice(21) == 42 ? 0 : 1;
}
CC = gcc
CFLAGS = -Wall -Wextra
.PHONY: all clean test
all: app check
app: app.o util.o
$(CC) $^ -o $@
check: check.o util.o
$(CC) $^ -o $@
%.o: %.c util.h
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f app check app.o util.o check.o
test: check
./check
make clean
make
make test
rm -f app check app.o util.o check.o
gcc -Wall -Wextra -c app.c -o app.o
gcc -Wall -Wextra -c util.c -o util.o
gcc app.o util.o -o app
gcc -Wall -Wextra -c check.c -o check.o
gcc check.o util.o -o check
./check
clean removed every product, and test depended on check, so make built it first and then ran it. The silent last line is a passing test: the program returned 0. That chain is the same idea a test suite uses, which the course covers in its part on testing.
A space instead of a tab
The recipe line must begin with a tab. Four spaces look identical in an editor and are rejected by make with a message that names the file and the line, but not the character. The program is the same hello.c as in the first section:
#include <stdio.h>
int main(void)
{
printf("hello\n");
return 0;
}
vi Makefile
hello: hello.c
gcc -Wall -Wextra hello.c -o hello
make
Makefile:2: *** missing separator. Stop.
Make stops before running anything, and the exit status is 2. The fix is one tab before gcc, not a different indentation width; a visible tab marker in the editor prevents the mistake.
Exercises
- Add a debug target that builds the program with -g -O0 into a separate binary, and mark it phony.
- Split the rule for app so that linking uses only the object files, then delete one object and run make to see what it rebuilds.
- Add a test target that runs the program and compares its output with an expected line using grep -q. Does the target fail when the output is wrong?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does a make rule contain?
Answer
A target, its prerequisites and a recipe
The target comes before the colon, the prerequisites after it, and the recipe on tab-indented lines below.
-
A target already exists. When does
makerebuild it?Answer
When a prerequisite is newer
makecompares timestamps, so an up-to-date target is left alone and only stale work is rebuilt. -
What does
$@expand to in a recipe?Answer
The target name
$@is the target,$<the first prerequisite, and$^the full prerequisite list. -
Why are
all,cleanandtestlisted in.PHONY?Answer
To make
makerun them even when a file has that nameA phony target is not a file, so its recipe runs every time instead of being skipped because a file of that name exists.
-
A Makefile contains
hello: hello.cfollowed by a tab-indentedgcc -Wall -Wextra hello.c -o hello, andmakehas already built the binary. What does a secondmakeprint?Answer
make: 'hello' is up to date.
The target is newer than its source, so
makereports that it is up to date and runs no command. -
This program is built by
makeasapp. What does./appprint?#include <stdio.h> int twice(int value) { return value * 2; } int main(void) { printf("%d\n", twice(21)); return 0; }Answer
42
twice(21)returns 42, and that value is what the program prints. -
A
testtarget runs a check program that returns 0 and prints nothing. What doesmake test; echo "status: $?"print?Answer
status: 0
The check returns zero and prints nothing, so
maketreats the silent target as a success. -
A Makefile has a recipe line indented with four spaces instead of a tab. What does
makereport?Answer
Makefile:2: *** missing separator. Stop.
Every recipe line must start with a real tab; spaces produce
missing separator, andmakestops. -
Why does listing a shared header as a prerequisite of a pattern rule rebuild both object files?
Answer
Both objects depend on the header, so its new timestamp triggers both rules
A header newer than an object triggers that object rule, and both objects list the shared header.
-
How can a user change the compiler flags without editing the Makefile?
Answer
By running
make CFLAGS=-O2A variable set on the command line overrides the assignment in the file, which is why the conventional names
CCandCFLAGSmatter.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 and GNU Make 4.4.1 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev