Quick introduction to GNU Make

Norbert Pozar

This is a short, quick introduction to using GNU Make to automatically build your C/C++ project’s executable.

Jump directly to the makefile if you are not interested in the explanation.

I am assuming that you are using Linux or Mac OS X. On Windows things are a little bit more complicated. For one thing, GNU Make is not installed by default, so you will have to install it yourself. It can be found on the gnuwin32 website. All commands described below assume that you are using the bash shell, which is the default shell on Linux/OS X. On Windows, again, one can install it through Cygwin… Cygwin will also supply the GCC toolchain.

Directory structure of the project

I prefer separating source files and binary files into their own directories. This is usually also the way how IDEs like Eclipse organize the project files.

One standard structure is:

Simple C++ project

Make uses makefiles (a file at the root directory of your project called makefile) to build your project. The makefile specifies rules that Make will follow when generating intermediate files.

So let’s assume that you have the directory structure as described above. To create this for testing in a directory ~/testproj, run the following commands in the terminal (lines starting with $ specify which command you should run in your bash shell, lines without $ give you the output of the previous command):

$ mkdir ~/testproj && cd ~/testproj && mkdir src bin && touch makefile
$ ls
bin  makefile  src

Let us add some empty source files:

$ cd src && touch main.cpp other.cpp other.h extra.cpp extra.h && ls
extra.cpp  extra.h  main.cpp  other.cpp  other.h

Let’s also add some default main file, so we can actually build the project. Open src/main.cpp and enter the following commands:

#include <iostream>
int main() {
    std::cout << "Hello" << std::endl;
    return 0;
}

A quick way to do this from the command line is running the following code:

$ cd ~/testproj
$ echo -e "#include <iostream>\nint main() {\nstd::cout << \"Hello\" << std::endl;\nreturn 0;\n}\n" > src/main.cpp

Now we can build the project. Suppose that the name of the binary file will be myprogram. And as I mentioned above, we want to store the binary in the subdirectory bin. Using g++ we can run:

$ g++ src/extra.cpp src/main.cpp src/other.cpp -o bin/myprogram

Now it’s time to test the program:

$ bin/myprogram
Hello

Simple makefile for a simple project

Running g++ this way is quite fragile and tedious, you have to remember to put in all the source files, and all the various options that you might have. This is where Make comes in handy.

Open the makefile that we have just created in the first step of creating the simple project, and enter the following text (make sure that the whitespace at the beginning of line 2 is a single TAB character):

bin/myprogram: src/extra.cpp src/main.cpp src/other.cpp
    g++ $^ -o $@

You can use the following shell command to insert the text (character \ at the end of line 2 signifies continuation of the command, so also enter line 3 as a command after entering line 2):

$ cd ~/testproj
$ echo -e "bin/myprogram: src/extra.cpp src/main.cpp\
 src/other.cpp\n\tg++ \$^ -o \$@" > makefile

Now remove the compiled program:

$ rm bin/*

And run make by invoking the make command (make sure that you are in the root of your project directory, that is, ~/testproj):

$ make
g++ src/extra.cpp src/main.cpp src/other.cpp -o bin/myprogram

It’s that simple. Now if you run make again, you get the following:

$ make
make: `bin/myprogram' is up to date.

This is one of the big advantages of make: it only rebuilds the program if there has been a change in one of your source files since it was built last time. This simple makefile will not, however, rebuild the code if there is change in the header files extra.h and other.h, because make does not know about them. You can force make to rebuild everything by running:

$ make -B
g++ src/extra.cpp src/main.cpp src/other.cpp -o bin/myprogram

Explanation

The simple makefile contains two lines. The first line tells make that the building program bin/myprogram depends on three files: src/extra.cpp, src/main.cpp and src/other.cpp. That means that whenever one of these files changes, make will try to rebuild bin/myprogram. When writing makefiles, bin/myprogram is called the target, and the source files are the prerequisites of building the target.

The second line then explains how to build bin/myprogram. Here it is as simple as running g++ with the names of the source files (make will replace $^ with their names automatically), and the name of the output file (make will replace $@ by bin/myprogram). They are called automatic variables. The second line must start with a single TAB character to work properly.

Improved makefile

Lets try to write a more general makefile for our project. Copy the following source (the makefile is available at this link) into your makefile (again, make sure that lines 23 and 29 start with a single TAB character):

# we can define variables in a makefile
# variable CC will specify the compiler; feel free to use clang++
CC:=g++

# this variable contains any extra compiler options that we might
# want to add, like -O3, -march=native, etc.
# -O3 tells g++ to optimize the code for speed
CC_OPTS:=-O3

# this variable will contain the names of all cpp source files
SRCS:=$(wildcard src/*.cpp)

# variable with all header files
HEADERS:=$(wildcard src/*.h)

# this will contain the names of all intermediate object files
OBJECTS:=$(patsubst src/%.cpp,bin/%.o,$(SRCS))

# this rule is fancier now
# $< are the names of all prerequisites (the object files)
# $@ is the name of the target (bin/myprogram in this case)
bin/myprogram: $(OBJECTS)
    $(CC) $^ $(CC_OPTS) -o $@ # must start with TAB character

# but now we have to tell make how to build the object files
# -c option tells g++ to only compile one source file at a tile
#  $< is the name of the first prerequisite (the cpp file in this case)
bin/%.o: src/%.cpp $(HEADERS)
    $(CC) $< $(CC_OPTS) -c -o $@ # must start with TAB character

Try building your project by running make:

$ cd ~/testproj
$ rm bin/*
$ make
g++ src/extra.cpp -O3 -c -o bin/extra.o
g++ src/main.cpp -O3 -c -o bin/main.o
g++ src/other.cpp -O3 -c -o bin/other.o
g++ bin/extra.o bin/main.o bin/other.o -O3 -o bin/myprogram
$ bin/myprogram
Hello

Seems like everything is working fine. Now running make again will just print that bin/myprogram is up to date.

Explanation of the improved makefile

The improved makefile is much more convenient. It automatically searches the subdirectory src for all *.cpp source files and all *.h header files. It will instruct g++ to build the program from these files.

This time, however, it uses an intermediate step during which it compiles each cpp source file into an object file bin/*.o. This speeds up the compilation when you have many source files in your project, and change only a few of them at a given time. Make will automatically rebuilt only the updated files.

Character # starts a comment. Everything after it all the way to the end of the line is ignored by Make.

You can define variables in a makefile using the code NAME:=value. The value can be then used anywhere by writing $(NAME). This is often quite useful.

Command $(wildcard src/*.cpp) produces a space separated list of all cpp files in subdirectory src.

Command $(patsubst src/%.cpp,bin/%.o,$(SRCS)) converts the names of the cpp source files into the names of object files. For example, src/main.cpp will become bin/main.o.

GCC and optimization

When running g++ with no extra options, g++ will not optimize the code for speed. This is useful for debugging because the structure of the program is exactly preserved. However, when doing numerical computations, you usually want the code to run as fast as possible. To tell g++ to optimize the code for speed, add -O3 command line option. The behavior of the program will not change (well, it should not), but the speed might improve significantly.

References