The Gentlest Introduction to Building With Makefiles
Published on 2020-05-14 18:44:00+02:00
If you are here, you are most likely in need to build C or C++ program. Chances are you were not even looking for
tutorial about GNU make or Makefiles in general. Chances are that you
need to get you assignment done by yesterday, or you want to refresh your memory from back in the day you used C for
the last time. No matter your background, I'll try to walk you through the process of building your C or C++ program
using make command.
Sadly, the tutorial will explain the stuff, so that you have an overview after reading it. It won't go too deeply.
Anyway, if you are only interested in an example to copy, there is one below.
If you have no idea, why using build system is nice. There are plenty of reasons. I will give you two. They automate
the building process, so that you don't have to type same things all over again, and you don't need to remember your
configuration at all times. If used consistently, they try to build only parts of your project that were changed. It
can affect the building time greatly.
Building a single file project
You have just finished writing your first implementation of Hello, world!, you have terminal open or some kind
of prompt up and running, and now you would like to build the program and execute it. You've probably seen it somewhere
but let me remind how to do it by hand using gcc:
$ ls
hello.c
$ gcc hello.c -o hello
$ ./hello
Hello, world!
Nice! But writing gcc hello.c -o hello
all the time when you want to rebuild the program sounds
bothersome even if you consider using command history. If you were to extend the program with libraries or additional
files it sounds even more tiresome.
Let's put make to use! All you need to do is replace gcc hello.c -o
part with make
,
so that you have:
$ ls
hello.c
$ make hello
cc hello.c -o hello
$ ls
hello hello.c
$ ./hello
Hello, world!
You probably noticed that make shamelessly prints out the command it used to build your program. How did it
know? Make is a master of default variables, implicit rules, deduction, and hiding it's secrets from curious eyes
of those who seek knowledge. Actually, no, the
documentation is available for anyone in various forms. We'll not discuss it in detail, that wouldn't be gentle, so
assume for now that make will know how to compile and link your C or C++ program. Rules that describe how
make does that are called implicit rules. We'll use and affect them extensively.
Using libraries with implicit rules
Make and makefiles heavily rely on your environment. If you don't know what it is, for simplicity let's say
that the environment is a set of variables associated with your current shell/terminal/prompt session. Make is
so greedy that it takes all of your environmental variables and copies them as own. The implicit rules may use those
copied variables, and they do exactly that. Those variables are usually called implicit variables.
We can take advantage of it. Let's say we are building a game with SDL2. SDL2
requires an additional include directory, a flag, and a library in order to build with it. Firstly, we'll set selected
variables in our environment (via export VARIABLE=value
), and then we'll build the program:
$ ls
hello-sdl.c
$ export CFLAGS='-D_REENTRANT -I/usr/include/SDL2'
$ export LDLIBS='-lSDL2'
$ make hello-sdl
gcc -D_REENTRANT -I/usr/include/SDL2 hello-sdl.c -lSDL2 -o hello-sdl
$ ./hello-sdl
The values I've used are specific to SDL2, don't mind them. What interests us in this example are the names of the
variables: CFLAGS
and LDLIBS
. First one, is a set of parameters that describe how our thing
should be handled during compilation. CFLAGS
is for C language, for C++ programs there is
an equivalent variable called CXXFLAGS
. Second variable, LDLIBS
may contain a list of
libraries that the linker should link to our program. In the example above there is no clear difference between
compilation and linking, and thus both variables are copied by make to a single command. Luckily, it makes no
difference to us, especially when the outcome is satisfying.
First Makefile
Obviously, we would need to repeat those exports
each time we start new session. This would bring us
back to the level of repeating whole gcc
call on and on. We could put them in some kind of file, couldn't
we? Luckily, make predicted that and it may read the contents of so-called makefiles. Just put a file called
Makefile in the project directory and insert the variables there:
CFLAGS=-D_REENTRANT -I/usr/include/SDL2
LDLIBS=-lSDL2
Now, if you run make, it will read the Makefile and use the variables that are defined in it:
$ ls
hello-sdl.c Makefile
$ make hello-sdl
gcc -D_REENTRANT -I/usr/include/SDL2 hello-sdl.c -lSDL2 -o hello-sdl
Less writing is always cool. How about getting rid of the hello-sdl
from every call to make?
That's also possible. hello-sdl
is a target. Targets are associated with
rules, any number of them, be it implicit or user-defined. If user doesn't provide a target name as an
argument in command line, make uses first target that is specified in the makefile. We can create targets by
writing rules. The syntax to do so is rather straight forward and contains: names of targets, prerequisites needed, and
a recipe which may be a single-line command or may span the eternity. Knowing all of that, we can write a very peculiar
rule. Everything will be handled by an implicit rule, and we'll only give a hint to make which thing we want it
to build by default:
CFLAGS=-D_REENTRANT -I/usr/include/SDL2
LDLIBS=-lSDL2
hello-sdl:
Surprisingly, that's enough. hello-sdl
is the name of our target. It's a first target that appears in
this makefile, therefore it will be the default one. :
(colon) is a required separator between the target
and prerequisites list. We didn't add any dependencies, as the sole dependency on hello-sdl.c file is
acknowledged thanks to the implicit rule. And because we didn't write any recipe, the recipe from the implicit rule is
used. When we use it, it looks like this:
$ ls
hello-sdl.c Makefile
$ make
gcc -D_REENTRANT -I/usr/include/SDL2 hello-sdl.c -lSDL2 -o hello-sdl
Adding more files
In a long run, it would be more useful to have more than one file in a project. Make also predicted that and
allows users to build programs from multiple sources. Amazing, isn't it? Now is the moment we finally split up
compilation from linking in a visible manner. Let's say we have a project with three files hello.c,
sum.h and sum.c, their content is respectively:
#include <stdio.h>
#include "sum.h"
int main(int argc, char * argv[]) {
printf("2 + 3 = %d", sum(2, 3));
}
#pragma once
int sum(int a, int b);
int sum(int a, int b) {
return a + b;
}
The structure of this project is easily seen. Hello.c depend directly on sum.h due to the include, and
it requires the sum function to be compiled and available when linking the final executable. First dependency is
so stupidly easy to write, that you might be actually surprised about it: you just need to add sum.h file to
prerequisites in the rule description. The other one, is slightly more interesting. We could just add sum.c to
prerequisites, but we will die a horrible death after a while if we do that. Technically, it's not even the thing we're
trying to accomplish, so don't do that.
Instead, let's use .o files that are products of compilation of a single source file. We can link them
together with libraries to form an executable. We are finally clearly dividing our building process into compilation
stage, and linking stage. Let's introduce two such files: hello.o and sum.o. They are build from their
respective sources. This means we now have three files with compiled or linked code: hello,
hello.o and sum.o. Latter doesn't depend on anything, so there is no need for us to write anything about
it. hello.o depends on sum.h (again, due to already mentioned include). Despite the fact that we call the
sum() function in it, it doesn't depend on sum.o. Why? Because it is just an intermediate file. It never
executes anything. On the other hand, hello executes it, so it needs all of the intermediate .o files in
its prerequisites list.
All in all, the Makefile will look like this:
hello: hello.o sum.o
hello.o: sum.h
When we use it:
$ make
cc -c -o hello.o hello.c
cc -c -o sum.o sum.c
cc hello.o sum.o -o hello
$ ./hello
2 + 3 = 5
Surprisingly, that's all you need to know. With that, you can build pretty much everything. With time and when you
gain some additional knowledge you may want to write your makefiles more explicitly: things like CC=gcc
to
make sure that the correct compiler is used. Your own recipes for generating headers or targets that are not generating
any files, but rather install the software or clean up the directory. Targets not associated with files are called
.PHONY and sooner or later you will encounter them. Actually...
Clean up your project directory with make
For some reasons, you may want to remove all built executables and intermediate files, or any other garbage files
that your workflow involves. In previous part, I've already noted that you can accomplish that using .PHONY
targets. Such cleaning target is quite common and is usually called clean
. Consider the following:
hello: hello.o sum.o
hello.o: sum.h
clean:
$(RM) *.o hello
.PHONY: clean
As it's not the default target, you must invoke it by name:
$ make clean
rm -f *.o hello
The marked line is called the recipe. It describes what rule is supposed to do. Only one recipe per target is used,
make discards previous recipes for the target if new one is defined, so only the bottom-most is effective.
$(RM)
is a default variable that is expected to describe the command that can be used to safely remove
files no longer needed by the project. You've probably noticed that .PHONY
exists as a target. We add
clean
to it's prerequisites list to let make know that clean
is not expected to create
a file called clean.
Example makefile for C++ project
Following makefile is used to build a simple C++ pager, program for opening and scrolling through a file in a
command line interface. Please note, that by default cc is used as a linker. It means that, if we are building
a C++ project, the standard C++ library will be missing. We can avoid it by writing own linking recipe, or by adding
-lstdc++
to libraries manually. Latter approach is used in the example.
CXXFLAGS=-std=c++17 -Wall -Wextra -Werror -O2
LDLIBS=-lstdc++ -lncurses++
pager: ansi.o content.o pager.o
pager.o: ansi.h content.h
clean:
$(RM) pager *.o
.PHONY: clean
What's next
Obviously, that's not everything there is to make. In my opinion, this is all you need for regular usage in
small to medium projects. From this point you can extend your knowledge. I would suggest to learn more about
variables and built-in
functions. They will help you to create
more extendible makefiles, and write less in general. In case you'll end up needing to write a proper recipe and more
complicated rule - head to sections:
writing recipes or
writing rules.
The automatic variables are
an amazingly useful tool when writing your own rules. Actually, these three things are usually, the first to be
mentioned by other tutorials about make. However, with approach presented in here, you should be able to avoid
them for quite a long time. Be wary though - don't be ignorant. You've been showed the basic usage of
make that is heavily dependent on implicit rules and hidden mechanics. You know that they exists and now it's
turn for you to go out there, use them and slowly learn how they really work.