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
- Add a run target that builds hello and then runs it.
- Make the default target all and put hello and clean after it, then run make with no arguments.
- 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.
-
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.
-
Which character must start a recipe line?
Answer
A tab
make requires a tab; spaces produce the missing separator error.
-
What does the second
make helloprint 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.
-
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.
-
What does
makewith no arguments build?Answer
The first target in the file
The default goal is the first target, whatever its name.
-
What does this Makefile print when make runs?
hello: hello.cppnewline tabg++ -std=c++23 hello.cpp -o helloAnswer
The g++ command line it runs
make echoes each recipe line before running it, unless the line starts with @.
-
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.
-
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.
-
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.
-
This Makefile has just built hello.
make allruns 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 helloAnswer
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