Skip to Content

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!!