Make

A Makefile is a list of targets, what each depends on, and the commands that build it. make compares the modification times and runs only the recipes whose target is older than a prerequisite, which is the whole reason it exists.

make is old, small and installed almost everywhere. This lesson writes the file by hand, builds the program, runs the rebuild that does nothing and meets the tab that every Makefile needs.

A Makefile is also a document: a reader sees which files make up the program and which command builds it, without opening the build script of an IDE. That is why the file is still worth writing by hand for a small project.

A target can be a file or just a name, and make does not care which until it compares timestamps. That is why a target with no file behind it needs .PHONY: otherwise make looks for the file and decides the work is done.

The recipes are the only place the compiler appears, so a reader who wants to know how the project is built reads the Makefile and nothing else. That is the advantage a build file has over a build script hidden in an IDE.

The tab is the only syntax make cares about beyond the colon, and it is the first thing every editor gets wrong.

A first Makefile

The file has one rule: the target hello depends on hello.cpp, and the recipe is the g++ command. The recipe line starts with a tab, not spaces, and make runs it when hello is missing or older than the source.

The program is the hello world of lesson 004. The make command prints the recipe it runs, and the second command runs the binary.

The name of the file is Makefile, with a capital M and no extension. make reads it from the current directory, and make -f other.mk reads a different one.

hello.cpp

#include <iostream> int main() { std::cout << "built by make\n"; }

Makefile

hello: hello.cpp g++ -std=c++23 -Wall -Wextra hello.cpp -o hello

make
./hello

g++ -std=c++23 -Wall -Wextra hello.cpp -o hello
built by make

Two targets

A Makefile usually has a default target named all and a clean target that removes what the build produced. clean is a phony target: it is not a file, so make must not look for one.

The program builds the same binary through all, then removes it with clean. The last command lists the file to show that it is gone.

The recipe lines are shell commands, one per line, each in its own shell. A command that must run in the same shell as the previous one needs a backslash and a single line, or .ONESHELL in newer make.

hello.cpp

#include <iostream> int main() { std::cout << "built by make\n"; }

Makefile

all: hello hello: hello.cpp g++ -std=c++23 -Wall -Wextra hello.cpp -o hello clean: rm -f hello .PHONY: all clean

make all
./hello
make clean
ls hello 2>&1 || echo removed

g++ -std=c++23 -Wall -Wextra hello.cpp -o hello
built by make
rm -f hello
ls: cannot access 'hello': No such file or directory
removed

What is up to date

The second make finds hello newer than hello.cpp and prints that the target is up to date. Nothing is compiled, which is the point of the timestamps.

The program runs make twice. The first line is the recipe, the second says the target is current, and the binary is printed once because the second build did not run it.

Touching the source makes the target older, and the next make rebuilds. A header that is not listed as a prerequisite is the classic hole in this scheme, which lesson 098 closes.

hello.cpp

#include <iostream> int main() { std::cout << "built by make\n"; }

Makefile

hello: hello.cpp g++ -std=c++23 -Wall -Wextra hello.cpp -o hello

make hello
make hello

g++ -std=c++23 -Wall -Wextra hello.cpp -o hello
make: 'hello' is up to date.

The missing separator

A recipe line must start with a tab. An editor configured to expand tabs writes spaces, and make reports that the line is a missing separator rather than a command.

The Makefile below has four spaces in front of g++. The error names the line and the line number, and the message does not mention the tab, which is why it puzzled generations of programmers.

The fix is a literal tab, and most editors show them or have a mode that keeps them in Makefiles. make 4 also warns about a recipe that is indented with spaces after a tab-indented one.

hello.cpp

#include <iostream> int main() { std::cout << "hello\n"; }

Makefile

hello: hello.cpp g++ -std=c++23 -Wall -Wextra hello.cpp -o hello

make

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

Exercises

  1. Add a run target that builds hello and then runs it.
  2. Make the default target all and put hello and clean after it, then run make with no arguments.
  3. Introduce a second source file and add it to the prerequisite list of hello.

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 consist of?

    Answer

    A target, its prerequisites and a recipe

    The target, the files it depends on and the commands that build it are the three parts of a rule.

  2. Which character must start a recipe line?

    Answer

    A tab

    make requires a tab; spaces produce the missing separator error.

  3. What does the second make hello print when hello is newer than hello.cpp?

    Answer

    make: 'hello' is up to date.

    The timestamps say the target is current, so the recipe is skipped and make says so.

  4. Why is clean declared phony?

    Answer

    Because there is no file named clean to compare timestamps with

    Without .PHONY make would look for a file called clean and skip the recipe when one exists.

  5. What does make with no arguments build?

    Answer

    The first target in the file

    The default goal is the first target, whatever its name.

  6. What does this Makefile print when make runs? hello: hello.cpp newline tab g++ -std=c++23 hello.cpp -o hello

    Answer

    The g++ command line it runs

    make echoes each recipe line before running it, unless the line starts with @.

  7. What happens when a recipe line is indented with four spaces?

    Answer

    make stops with a missing separator error

    The parser expects a tab after a rule and reports the line as a missing separator.

  8. Which command builds a target in a Makefile called other.mk?

    Answer

    make -f other.mk

    -f names the makefile to read instead of Makefile.

  9. What does make compare to decide whether to rebuild?

    Answer

    The modification times

    A target older than a prerequisite is rebuilt; the timestamps are the whole mechanism.

  10. This Makefile has just built hello. make all runs a second time with no change to hello.cpp. What does make print? all: hello hello: hello.cpp g++ -std=c++23 hello.cpp -o hello clean: rm -f hello

    Answer

    make: Nothing to be done for 'all'.

    hello is newer than hello.cpp, so its recipe is skipped, and all has no recipe of its own, which leaves make with nothing to do.

The code on this page was built with g++ 15.2.0 and GNU Make 4.4.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev

Banner image