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

  1. Add a debug target that builds the program with -g -O0 into a separate binary, and mark it phony.
  2. 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.
  3. 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.

  1. 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.

  2. A target already exists. When does make rebuild it?

    Answer

    When a prerequisite is newer

    make compares timestamps, so an up-to-date target is left alone and only stale work is rebuilt.

  3. What does $@ expand to in a recipe?

    Answer

    The target name

    $@ is the target, $< the first prerequisite, and $^ the full prerequisite list.

  4. Why are all, clean and test listed in .PHONY?

    Answer

    To make make run them even when a file has that name

    A phony target is not a file, so its recipe runs every time instead of being skipped because a file of that name exists.

  5. A Makefile contains hello: hello.c followed by a tab-indented gcc -Wall -Wextra hello.c -o hello, and make has already built the binary. What does a second make print?

    Answer

    make: 'hello' is up to date.

    The target is newer than its source, so make reports that it is up to date and runs no command.

  6. This program is built by make as app. What does ./app print? #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.

  7. A test target runs a check program that returns 0 and prints nothing. What does make test; echo "status: $?" print?

    Answer

    status: 0

    The check returns zero and prints nothing, so make treats the silent target as a success.

  8. A Makefile has a recipe line indented with four spaces instead of a tab. What does make report?

    Answer

    Makefile:2: *** missing separator. Stop.

    Every recipe line must start with a real tab; spaces produce missing separator, and make stops.

  9. 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.

  10. How can a user change the compiler flags without editing the Makefile?

    Answer

    By running make CFLAGS=-O2

    A variable set on the command line overrides the assignment in the file, which is why the conventional names CC and CFLAGS matter.

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

Banner image