Welcome to a short, practical introduction to the program make
—
your canonical build system.
This tutorial is not a comprehensive introduction to make
. We only
cover what you might need for the course
Machine Architecture (ARK), or
more recently Computer Systems
(CompSys), at DIKU, and beyond. Most crucially,
make
goes far beyond programming in C, we don’t.
This tutorial assumes that you’ve already set up a Unix-like programming environment, know how to work with directories and files, and know your way around a text editor. We assume that you’ve already tried to get started with C.
This tutorial assumes that you already have GNU Binutils and GCC, the GNU Compiler Collection installed.
Go back to the aforementioned material if you are feeling uneasy.
Hello, make
It is about as easy to get started with make
as it is to
get started with C. Hopefully, it takes a bit less
forbearance to become proficient in make
, than it takes to become proficient
in C.
To get started, create an empty directory and navigate to it:
$ mkdir canon $ cd canon
Consider a simple program which fits on a file, which we can call main.c
:
int main() { return 42; }
With no further work, we can already use make
to compile this program:
$ make main cc main.c -o main
Note
|
Terminal
$ which cc /usr/bin/cc $ file /usr/bin/cc /usr/bin/cc: symbolic link to gcc |
If we instead called our file root.c
we would have to compile it like this:
$ make root cc root.c -o root
If we forget the program name, make
will complain, not knowing what to do:
$ make make: *** No targets specified and no makefile found. Stop.
Similarly, if we misspell the program name, make
will complain:
$ make homework make: *** No rule to make target 'homework'. Stop.
As long as we pass the command-line argument main
to make
, make
can
save us a couple keystrokes, and compile our
main.c
into an executable file called main
on our behalf.
Note
|
Your directory structure should now look like this: Terminal
$ ls -lh -rwxr-xr-x [..] main -rw-r--r-- [..] main.c |
We can try to run our program and check its exit code:
$ ./main || echo $? 42
A classical feature of make
, is that it helps us avoid unnecessary
recompilations.
Try to call make main
again (without changing main.c
):
$ make main make: 'main' is up to date.
In this basic use, make
takes main.c
as a prerequisite for the
target main
. That is, make
sets up a dependency graph which can be
illustrated like this:
+------+ | main | +------+ | (depends on) | v +--------+ | main.c | +--------+
To implement this dependency graph, make
will compare the modification
time of main.c
with that of main
. If the prerequisite was modified after
the target, make
will run a recipe in attempt to bring the target "up to
date" with the prerequisite. The default recipe in this case, is to compile
the C file.
Hello, Makefile
The behaviour of calling make
in a particular directory can be customized by
creating a special file called Makefile
in that directory. As a
(de)motivating example, here is a Makefile
that (in our case) will achieve
the exact same effect as having no Makefile
at all (except use the expected C
compiler!):
main: main.c gcc main.c -o main
Note
|
Your directory structure should now look like this: Terminal
$ ls -lh -rwxr-xr-x [..] main -rw-r--r-- [..] main.c -rw-r--r-- [..] Makefile |
A Makefile
specifies a number of rules. A rule has a number of
targets and prerequisites, as well as a recipe for brining the
targets "up to date" with the prerequisites. A recipe is a sequence of
commands which will be called in order, from top to bottom, each in their
own shell.
The format of a Makefile
rule goes as follows:
TARGETS `:` PREREQUISITES LINE-BREAK TAB COMMAND LINE-BREAK TAB COMMAND LINE-BREAK TAB COMMAND LINE-BREAK ...
Important
|
Every line of a recipe must begin with a tab character. To quote the GNU
|
There is one benefit to our Makefile
however: we no longer need to specify
main
as the command-line argument to make
. It is now assumed by default:
$ make make: 'main' is up to date. $ rm main $ make gcc main.c -o main
Phony Targets
To make our Makefile
a bit more useful, let’s create a classical phony target
— clean
. clean
will be "phony" in the sense that its recipe will not
produce a file called clean
. Instead, clean
will clean up the mess our
invocations of make
have made above — in our case, just remove the main
file.
A simple approach would’ve been to just add the clean
target to our
Makefile
:
#BadMakefile main: main.c gcc main.c -o main clean: rm main
Unfortunately, if we were ever to place a file called clean
into our
directory, the clean
target would always be considered up to date (why?). For
instance, consider the following session at the terminal:
$ echo 42 > clean $ make clean make: 'clean' is up to date. $ make gcc main.c -o main $ make clean make: 'clean' is up to date. $ ls -lh -rw-r--r-- [..] clean -rwxr-xr-x [..] main -rw-r--r-- [..] main.c -rw-r--r-- [..] Makefile
To avoid this problem (and make sure the recipe for clean
is always run when
we ask it to), we have to mark the clean
target as .PHONY
:
.PHONY: clean main: main.c gcc main.c -o main clean: rm main
Continuing the terminal session from before..
$ make clean rm main
Note
|
If you followed our ill advice and created a file called Terminal
$ ls -lh -rwxr-xr-x [..] main -rw-r--r-- [..] main.c -rw-r--r-- [..] Makefile |
If you spuriously try to play around, and try to make clean
again, you’ll get
to see make
fail:
$ make clean rm main rm: cannot remove ‘main’: No such file or directory Makefile:7: recipe for target 'clean' failed make: *** [clean] Error 1
The recipe is failing because we’ve already removed the file called
main
.make
then tries to be helpful and tell us that it failed on line 7 of
the Makefile
, in the midst of the recipe for the clean
target.
A recipe fails as soon as one of its commands (executed in order from top to bottom) yields a non-zero exit code.
This is what rm
does for a nonexistent file. We can add a -f
command-line
argument to rm
in our recipe to make rm
ignore nonexistent files:
.PHONY: clean main: main.c gcc main.c -o main clean: rm -f main
Warning
|
-f should in general be used with caution — you might carelessly
remove important files.
|
Now we can go on a command spree again!
$ make gcc main.c -o main $ make make: 'main' is up to date. $ make clean rm -f main $ make clean rm -f main $ ls -lh -rw-r--r-- [..] main.c -rw-r--r-- [..] Makefile
Mental exercise: Can you come up with other ways of solving the problem
with the clean
target?
A test
target
Another useful phony target is a test
target to perform the tests we have
thus far been doing manually. This target has a main
executable as a
prerequisite, and the recipe should run the executable and check its exit code.
test
is a good example of a phony target with prerequisites.
One naïve approach could go as follows:
#BadMakefile .PHONY: test clean main: main.c gcc main.c -o main test: main ./main clean: rm -f main
Let’s try to make test
and see what happens:
$ make test ./main Makefile:7: recipe for target 'test' failed make: *** [test] Error 42
So ./main
yields the expected exit code alright, but it is ill practice to
designate a test error as a success.
A better Makefile
could go as follows:
.PHONY: test clean main: main.c gcc main.c -o main test: main ./main || echo $$? clean: rm -f main
Important
|
Makefile VariablesWe need to double the dollar sign in our |
We can try to make test
to make sure that things work as expected:
$ make test ./main || echo $? 42
Note, the test
target lists main
as a prerequisite. So the dependency graph
deduced by make
can be illustrated as follows:
+------+ | test | +------+ | (depends on) | v +------+ | main | +------+ | (depends on) | v +--------+ | main.c | +--------+
To see how make
implements this dependency graph, let’s try to make clean
and make test
:
$ make clean rm -f main $ make test gcc main.c -o main ./main || echo $? 42
Out of mere interest, let us try to introduce an error into our program and see
how make
will handle a compilation error:
$ make clean $ echo "int main() { return x; }" > main.c $ make test gcc main.c -o main main.c: In function ‘main’: main.c:1:21: error: ‘x’ undeclared (first use in this function) int main() { return x; } ^ main.c:1:21: note: each undeclared identifier is reported only once for each function it appears in Makefile:4: recipe for target 'main' failed make: *** [main] Error 1
Perhaps as you had already expected, make
stopped processing the dependency
graph as soon as it encountered an error in one of the recipes.
The Default Target
You might’ve noticed that make
with no arguments still works despite the fact
that there are now multiple targets in our Makefile
:
$ make make: 'main' is up to date. $ make clean rm -f main $ make gcc main.c -o main
make
resolves target ambiguity in a very simple way — the top target is the
default target, and in our Makefile
, the top target is main
.
This is not a good default target for two reasons:
-
Good software development practice tells us to test early and test often.
make
is quick to type and probably what we’ll use as we write our program. It is perhaps more responsible to havetest
as our default target. -
It is a common
Makefile
convention to name the default targetall
.
We can embrace both by adding a phony target all
at the top of our
Makefile
, listing test
as a prerequisite:
.PHONY: all test clean all: test main: main.c gcc main.c -o main test: main ./main || echo $$? clean: rm -f main
Let’s take the Makefile
for a spin:
$ make clean rm -f main $ make gcc main.c -o main ./main || echo $? 42
A More Complicated Program
Consider our stack calculator from the accompanying tutorial on Getting Started with C.
There, we had a stack data structure declared in a header file stack.h
, and
implemented in the C file stack.c
. We compiled the implementation follows:
gcc -Werror -Wall -Wextra -pedantic -std=c11 -c stack.c
We then had a file calc.c
which implemented the actual stack calculator using
the stack implementation above. calc.c
contained a main
function. So we
then compiled the program as follows:
gcc -Werror -Wall -Wextra -pedantic -std=c11 stack.o calc.c
Perhaps a natural Makefile
for our stack calculator would then go as follows:
.PHONY: all test clean all: test test: ./calc calc: stack.o calc.c gcc -Werror -Wall -Wextra -pedantic -std=c11 stack.o calc.c stack.o: stack.h stack.c gcc -Werror -Wall -Wextra -pedantic -std=c11 -c stack.c clean: rm -f stack.o rm -f calc
The dependency graph deduced by make
in this case, can be illustrated as
follows:
+-----+ | all | +-----+ | (depends on) | v +------+ | test | +------+ | (depends on) | v +------+ | calc | +------+ | +--------------+ (depends on) | | (depends on) v v +--------+ +---------+ | calc.c | | stack.o | +--------+ +---------+ | +--------------+ (depends on) | | (depends on) v v +---------+ +---------+ | stack.h | | stack.c | +---------+ +---------+
Variables
Our Makefile
is starting to get a little cryptic and a little fragile. Good
software development practice tells us not to repeat ourselves. We are
repeating ourselves with all those compiler flags, and the compiler flags
obscuring our recipes.
Makefile
variables let us solve this in a straight-forward way. Makefile
variables work a bit like simple C macros in that they are merely placeholders
for text. Variables are typically declared at the top of the Makefile
, named
in ALL CAPS, with words occasionally separated by _
.
For instance, here’s a Makefile
that resolves our problems above:
CC=gcc CFLAGS=-Werror -Wall -Wextra -pedantic -std=c11 .PHONY: all test clean all: test test: ./calc calc: stack.o calc.c $(CC) $(CFLAGS) stack.o calc.c stack.o: stack.h stack.c $(CC) $(CFLAGS) -c stack.c clean: rm -f stack.o rm -f calc
Note
|
This Makefile also declares a variable for the compiler used. This is
useful for the portability of our source code. Other machines may not have GCC
installed, but use an equally adequate C compiler.
|
Conclusion
We can use make
to make sure to build the elements of our software project in
proper order, and put common software development tasks at our fingertips. We
can use Makefile
variables to keep our recipes consistent, to the point, and
flexible.
We call make
"canonical" because it is widely available in Unix-like
programming environments. It is often used in large software projects, and is
especially ubiquitous in the open-source and free software communities.
make
is old. Originally developed in 1977, it has had many derivatives.
GNU make
, the version of make
we’ve
encouraged you to use here, is the standard implementation of make
on most
Linux and OS X systems. On Windows, the standard implementation is nmake
, and
comes as part of Visual
Studio.
The rogue nature of make
has also inspired the development of many
alternative tools and companions. For instance, SCons,
CMake, and
Mk. Each come with their own
benefits and setbacks.
A most notable critique of make
is that it demands of you to manually manage
your dependencies. Integrated Development Environments, such as
Eclipse, Xcode,
and Visual Studio, as well as many modern
programming languages, such as Go and
Rust, often come with their own build-automation
tools, which automatically deduce dependencies from source-code. This results
in unwarranted dependence on particular languages and tools.
In today’s world, make
is reserved for those who want to exert grand control
over the build process, and projects which depend on a great variety of untamed
languages and tools. make
is widespread till this day.
Further Study
This tutorial is by no means a comprehensive introduction to make
. Most
notably, we’ve focused on programming in C, and forgotten to mention that
make
can be made to build dependencies in parallel, and that special,
magic-looking makefile variables can be used to write terse recipes.
There’s probably more that we’ve forgotten. If you want to know more, here are a couple good resources for further study:
-
Pierce Lopez. Make. http://www.ploxiln.net/make.html. 2015.
-
Free Software Foundation, Inc. GNU
make
. http://www.gnu.org/software/make/manual/make.html. 2014.