Using make to build programs

The utility program make greatly assists the development of multi-module programs. Many industrial and open source software development projects use make. This tech note is a simple introduction to make -- just enough to do the assignments.


C++ programs usually consist of multiple source modules. These source code modules contain class and type definitions in include files with the file extension ".h", and function and storage definitions in regular source modules with the ".cpp" extension. Beyond one or two modules, it becomes quite a chore to enter compile commands and to keep compiled source modules up-to-date.

Source modules (files) have dependencies. One kind of dependency is the use of include files. For example, the file containing the definition of a C++ class must include the declaration of the class and the file using the C++ class must also include the same declaration. One source module may also depend upon another module for access to "global" (exported) variables and data structures. These import/export dependencies are usually resolved when the compiled modules are linked together into the final executable program. If the importer and exporter aren't kept up-to-date, run-time errors may occur due to a mismatch at the interface between the importer and the exporter.

When the dependencies between modules become complicated -- and in large software development projects, they get quite complicated -- it's a difficult task to make sure that all source modules using a particular class declaration are recompiled when changes are made. This is where make comes in.

The make program

The make program uses dependency relationships to build or rebuild the parts of a program. The dependencies are defined in a file with the name "makefile" or "Makefile". Developers usually refer to this file as "the makefile" when talking about it. Once the dependencies (build rules) are specified in the makefile, the developer only needs to type the make command without any arguments. The make program then takes over and builds the program using the build rules. The make program detects changes and recompiles any parts of the program that need to be compiled again and brought up to date.

Example: Program to draw a robot

Let's look at the makefile that I used to build a program to draw a picture of a robot. The robot has a head, body and legs, which I defined as separate classes. The definition of the robot is itself a class. There is a "driver" module with the function main. All of these modules use graphics primitives defined in a separate PostScript graphics utility class. The table below summarizes the pieces and parts:

robot.h Declares the interface to the robot class
robot.cpp Defines the implementation of the robot class
head.h Declares the interface to the robot head class
head.cpp Defines the implementation of the head class
body.h Declares the interface to the robot body class
body.cpp Defines the implementation of the body class
leg.h Declares the interface to the robot leg class
leg.cpp Defines the implementation of the leg class
psfile.h Declares the interface to the PostScript graphics class
psfile.cpp Defines the implementation of the PostScript graphics class
main.cpp Defines the function main

All of the source (".cpp") modules include the declaration of the PostScript graphics interface in the file psfile.h The robot module, robot.cpp, includes the declarations of the head, body and leg interfaces in head.h, body.h, and leg.h, respectively. Finally, the driver module, main.cpp, includes the interface to the robot class in robot.h. Here is a table that summarizes the dependencies.

robot.cpp psfile.h, robot.h, head.h, body.h, leg.h
head.cpp psfile.h, head.h
body.cpp psfile.h, body.h
leg.cpp psfile.h, leg.h
psfile.cpp psfile.h
main.cpp psfile.h, robot.h

The makefile is a pretty direct restatement of the dependencies with some added information about how to build the different parts of the program. (See the example below.) The first line of the makefile defines the ultimate target (the final executable) to be built. This makefile builds the target "robot", which is the name of the final executable program. The first rule defines the parts that make up robot, namely, the individual, separately-compiled modules, head.o, body.o, leg.o, robot.o, psfile.o, and main.o. The white space between the ':' and head.o consists of TAB characters. The make program expects TABs here and you better not leave them out! The line after the rule specifies how to build the target robot. In this case, we are linking the parts together and putting the final executable program into robot using the "-o" option. Again, we used TAB characters to indent the command.

robot:          head.o body.o leg.o robot.o psfile.o main.o
                g++ -o robot head.o body.o leg.o robot.o psfile.o main.o

psfile.o:       psfile.cpp psfile.h
                g++ -c psfile.cpp

head.o:         head.h head.cpp psfile.h
                g++ -c head.cpp

body.o:         body.h body.cpp psfile.h
                g++ -c body.cpp

leg.o:          leg.h leg.cpp psfile.h
                g++ -c leg.cpp

robot.o:        robot.h robot.cpp psfile.h head.h body.h leg.h
                g++ -c robot.cpp

main.o:         main.cpp psfile.h robot.h
                g++ -c main.cpp

The rest of the makefile defines rules for each of the constituent parts (the ".o" files) and specifies how to make each of the parts. In each case, we listed the module's source and include files in the build rule. This makes sure that any change to an interface file forces a recompile of the module that uses the interface. All component parts are built by compiling the source module. The "-c" option, by the way, tells the g++ compiler to compile the module, but not link it.

Running make

The first time we run make, before any of the components have been built, we obtain the following transcript:

    42 > make
    g++ -c head.cpp
    g++ -c body.cpp
    g++ -c leg.cpp
    g++ -c robot.cpp
    g++ -c psfile.cpp
    g++ -c main.cpp
    g++ -o robot head.o body.o leg.o robot.o psfile.o main.o
The make program uses the dependencies and build rules to compile each of the individual parts and finally links the parts into the executable program, robot.

Let's say that we made a change to the file leg.h to correct some issue. Running make again, we obtain:

    44 > make
    g++ -c leg.cpp
    g++ -c robot.cpp
    g++ -o robot head.o body.o leg.o robot.o psfile.o main.o
The make program detects a change to the include file leg.h, applies the rules, recompiles the modules dependent upon leg.h, and links the compiled modules into the final executable program.

How does make detect changes to a source module? The operating system stores certain kinds of meta-information about a file along with the actual data contained in the file. File names are part of this meta-information. Another part is the date and time when the file was last modified (written.) The make program maintains a database that records when a target component was last built. It compares the last-modification time of each target file with the time of its last build in the database. If the last-modification time is later, make will try to rebuild the component.

We got simple, we got complicated

The make program is quite sophisticated and has pre-defined build rules for common file types as denoted by file extensions. In the example above, we tried to make all of the dependencies explicit to show the kinds of depndencies and rules that make uses. We could have gotten away with:

robot:          head.o body.o leg.o robot.o psfile.o main.o
                g++ -o robot head.o body.o leg.o robot.o psfile.o main.o
and let make figure out the rest!

Makefiles can also be incredibly complicated using environment variables, fancy build rules, etc. In general, makefiles should be kept as simple as possible because complicated makefiles are hard to maintain and are not always portable across operating systems.

Copyright © 2004-2013 Paul J. Drongowski