C/C++: The Make Build Tool
make
is a long-standing build tool in the C/C++ world (and beyond), and is still very heavily used in lots of software development. Many implementations of make
exist, but the most common and most popular one is Gnu Make.
make
reads a plain text file, usually named Makefile
, for a description of the program files and how to compile them.
Note: the examples below are C++ examples, but everything applies to plain C programs, too.
A Simple Example
The Makefile
below compiles a simple example:
hello: hello.cpp
g++ -o hello hello.cpp
The filename before the :, “hello” in this case, is called a target. The filename(s) after the : are dependencies; the entire line is a dependency rule. The line below this is an action. Action lines MUST begin with a tab character (not spaces!) and must immediately follow the dependency rule line (no blank lines in between!).
make
is pretty simple: if the target file is older than any of its dependencies, then make
executes the associated actions in order to rebuild the target. In other words, if you change the source code of hello.cpp, then it is newer than the executable hello that is there from your last compile, so make
recompiles your new program into the new executable. Cool!
There Must Be More, Right?
Yes! This is because dependencies can be targets of other rules, and so make
has to understand the connections and relations of all dependencies and all targets, and perform many actions in the correct order. For a simple example, if we want to separate compiling from linking, we might have the Makefile
below:
hello: hello.o
g++ -o hello hello.o
hello.o: hello.cpp
g++ -c hello.cpp
The second rule and action is a compile only invocation of g++
, because of the -c
option. This compiles the source code into object code, but does not perform the link step. The first rule now says that the executable file is dependent on the object code file, not the source code file. So when you change the source code and then run make
, it chains these rules together and works down the chain, first performing the compile action and then peforming the link action.
NOTE: running make
with no other arguments tells make to build the first target listed in the Makefile
. If you want to run make
on some other target, you invoke it as make targetname
. This can be extremely useful!
Handling Multiple Source Files
Of course, most programs are larger, and the whole source code is usually separated into multiple source code files. In make
, a dependency rule can list many files. An example:
hello: main.o calc.o
g++ -o hello main.o calc.o
main.o: main.cpp
g++ -c main.cpp
calc.o: calc.cpp
g++ -c calc.cpp
Here, our program is split across two files, main.cpp and calc.cpp. Notice that your executable application name does not need to match any of your source file names! The first rule says that our whole application depends on two object code files, and its action links them together (plus libraries) into the executable named hello. The second two rules are for compiling each source code file.
Make is Smart!
Here’s the neat thing: in the example above, if you only changed calc.cpp, make
will not re-compile main.cpp, it will only re-compile calc.cpp and then re-link with the new calc.o. make
only performs the necessary actions!
Simplifying a Makefile with Variables
The Makefile
example above has a lot of repetetive strings in it. The great thing about make
is that it lets you create variables and then re-use them, and it has a lot of built-in (automatic) variables. When you create variables, the custom is to make them ALLCAPS, and then they are referenced using $(ALLCAPS). The most useful built-in variables are:
- $@ is the name of the target
- $< is the name of the first dependency_
- $^ is a space-separated list of all the dependencies
Using these variables, we do not need to repeat all the names in our
Makefile
:
hello: main.o calc.o
g++ -o $@ $^
main.o: main.cpp
g++ -c $<
calc.o: calc.cpp
g++ -c $<
Simplifying a Makefile by Using Built-in Rules
Once we add the variables in, we see that both of our compile actions are exactly the same. So why should we repeat them? The answer is, we don’t have to!
make
allows you to create a rule that would define the action for any object code to C++ code dependency, but better yet, it has a built-in rule for that! In fact, make
has many many built-in rules.
The built-in rule for compiling C++ code into object code is:
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $<
Notice that the entire rule is defined by variables! These have pre-defined values, but you can change any of them! So I can create a Makefile
like this:
CXX = g++
CXXFLAGS = -g -Wall
hello: main.o calc.o
g++ -o $@ $^
main.o: main.cpp
calc.o: calc.cpp
If I have not yet compiled my program and I run make
, it will do the following actions:
g++ -g -Wall -c main.cpp
g++ -g -Wall -c calc.cpp
g++ -o hello main.o calc.o
In fact, built-in rules also include built-in dependency rules. So I do not need to tell make
that calc.o depends on calc.cpp, it can figure it out itself. So my Makefile
only needs to be:
CXX = g++
CXXFLAGS = -g -Wall
hello: main.o calc.o
g++ -o $@ $^
That’s nice!!