Make Tutorial

1 Introduction

The make command is a *NIX tool that allows you manage the compilation of one or more source files. For an application with just one source file, make does not improve the compilation efficiency. However, for projects made up of multiple source files, recompiling all files every time a change is made can take a lot of time.

Consider a project where you have 3 source files, source1.C, source2.C, and source3.C; and 3 header files, header1.h, header2.h, header3.h. To compile such a program, you might type:

    g++ -Wall source1.C source2.C source3.C -o project
To get an executable program named project. Now imagine that during testing, you found a bug in source2.C. You edit that file, fix the bug, and are now ready to recompile your program. Using the command used earlier, all source files are recompiled, even though source2.C is the only one that was changed.

make is designed to identify files that have been changed, and only recompile those files. As files are recompiled, other dependent files may be recompiled. At the end of the process, the changed files and any dependent files have been recompiled, but other unchanged files are not recompiled.

This tutorial is not intended to be an exhaustive document on what make can do. It is intended to be an introduction to the basic features make provides. See the References section for links to more detailed tutorials.

2 Compiling Overview

What happens when you "compile" a program? Well, what you consider "compiling" is a multi-step process, and the various processes can (and are) handled by separate applications. One part of the compiler takes your source code and generates the corresponding assembly language instructions. Another step takes that output and generates a binary object file for each source file. Finally, all object files are linked together into your executable. For us to take advantage of make, we need to break these steps apart.

2.1 Multi-Step Compilation

To make our compiler stop after the assembler has run, we can specify the -c switch.
    % g++ -Wall -c source1.C -o source1.o
    % g++ -Wall -c source2.C -o source2.o
    % g++ -Wall -c source3.C -o source3.o
Here, source1.C was compiled into an object file named source1.o. source2.C, which was then compiled into an object file named source2.o. Finally, source3.C was then compiled into an object file named source3.o.

Note that none of these .o files are executable. A file step must be executed to executed to link all of these individual object files into a single executable.

    % g++ -Wall source1.o source2.o source3.o -o project
Now, we have project which is an executable program.

2.2 Taking Advantage of Multi-Step Compilation

Okay, so we can turn one command for compiling a program into four. How does that help me? Well, here again we'll look at making a code change to one of the source files. Say we change something in source2.C. What do we need to do to build our application?

Well, source1.C does not depend on source2.C and we already have source1.o so we do not have to recompile source1.C.

    % g++ -Wall -c source2.C -o source2.o
    % g++ -Wall -c source3.C -o source3.o

    % g++ -Wall source1.o source2.o source3.o -o project
Now, we have project which is an executable program and we did not recompile source1.C.

As you can see, it is possible to use multi-step compilation manually to prevent unnecessary recompilations. However, as project grow larger and larger, we'd like a way to automate this process. make to the rescue!

3 Makefiles

A makefile is a file that contains a set of rules governing commands to be executed. The basic form of these rules is:
<target>:     <dependent files>
<tab><commands to build target>
Where target is the name associated with this specific build rule, dependent files is a list of files that this target depends on, and the list of commands to build the target are programs that will be executed to build the target. Notice that the tab character has to prefix each command line.

Let's consider our previous example. We want to make a makefile to build our project. Makefiles are usually named either "makefile" or "Makefile" Such a makefile might look like the following:

    all:  project

    project:    source1.o source2.o source3.o
            g++ -Wall source1.o source2.o source3.o -o project

    source1.o:  source1.C source1.h
            g++ -Wall -c source1.C -o source1.o

    source2.o:  source2.C source2.h source1.o
            g++ -Wall -c source2.C -o source2.o

    source3.o:  source3.C source3.h source1.o source2.o
            g++ -Wall -c source3.C -o source3.o
Now, if our project has never been built before, if we type make all on the command line, where all is the target to be built, make will determine that the target all is dependent on the file project. The file project does not exist, so it needs to be built. make then looks for a rule named project. It sees that the file project is dependent upon three other file, source1.o, source2.o, and source3.o. It looks for those file, sees that they are not there so it must build them. It repeats this process until all files are built.

In the future, if a modification is made to source2.C, when make evaluates the dependency list, it will see that all depends on project, project depends on source2.o, and source2.o depends on source2.C. source2.C has been modified since source2.o was built (make determines this by the timestamps on the files), so it must be rebuilt. Then everything depending on that must be rebuilt. Then everything depending on that must be rebuilt. This continues until everything that needs to be rebuilt is rebuilt.

The make process is not limited to compiling. For example one frequent rule in makefiles is one to clean up all the generated files. In our example, such a rule might look like:

            rm *.o project
Here, if we type make clean, all the object files and the executable will be removed.

4 Makefiles with Macros (Variables)

It is also possible for makefile to include variables. These variable can be used for anything including which compiler to use, what options should be sent to the compiler, what the name of the output program should be, etc . . .

Let's again take a look at our example.

    # This is a comment.  Comments should be used to document
    # your makefile just like one of your programs.

    all:  $(PROJECT)

    $(PROJECT):  source1.o source2.o source3.o
            $(CC) $(CC_OPTS) source1.o source2.o source3.o -o $(PROJECT)

    source1.o:   source1.C source1.h
            $(CC) $(CC_OPTS) -c source1.C -o source1.o

    source2.o:   source2.C source2.h source1.o
            $(CC) $(CC_OPTS) -c source2.C -o source2.o

    source3.o:   source3.C source3.h source1.o source2.o
            $(CC) $(CC_OPTS) -c source3.C -o source3.o

            $(RM) *.o project
This is basically the same make file as we saw before. This time, however, we're using variables. For example, the CC_OPTS variable hold options we're passing to the compiler. If we wanted to add an additional options, say -g, we could add it in that one place, instead of five in the previous.
    CC_OPTS=-Wall -g

5 References