CS 3481 - Computer Systems I

Makefiles


Motivation

In large software packages, as much as 80% of the cost of the software may be due to maintenance, i.e., correction of bugs and addition of enhancements following the initial release of the software. Large software packages can be made up of a number of C or C++ source files and include files. When a bug correction or enhancement is made to a file, the files that depend on that file must be recompiled and the entire program must be relinked to produce an up-to-date executable. The make utility provides a means of consistently compiling software. It automatically determines which files are affected when a coding change occurs and provides the means of recompiling and relinking the appropriate files.

Simple example

Consider the following example that uses an include file called Integer.h and two C++ source files called main.c and Integer.c. The main program prints out the value returned by the Integer::getnum.

--------------------------------------------------------------------
/* Integer.h */
class Integer

   private:
      const int num = 10;
   public:
      Integer();
      int getnum();
};
--------------------------------------------------------------------
/* main.c */
#include <iostream.h>
#include "Integer.h"

void main()
{
   Integer num;
   int intnum;
   intnum = num.getnum();
   cout << "intnum = " << intnum << endl;
}

--------------------------------------------------------------------
/* Integer.c */
#include "Integer.h"

Integer::Integer() { num = 0; }
int Integer::getnum() { return num; }

--------------------------------------------------------------------
One way to compile/link this is the following:
    g++ -c main.c
    g++ -c Integer.c
    g++ Integer.o main.o -o main
This creates an executable main, which can be run by giving its name. The make command reads a file called a makefile. This file contains lines that describe dependencies among source files and header files and lines with commands to recompile and link appropriate files to keep the software consistent. Lines in a makefile typically have the following form:

target: dependencies
<tab>   command1
<tab>   command2
     ...

Note the tab! It will look like spaces in the makefile, but it really is a tab. If you use spaces instead, then make won't work. A sample makefile for the program above is
#This is a comment
main: main.o Integer.o
	g++ main.o Integer.o -o main
Integer.o: Integer.c Integer.h
	g++ -c Integer.c -o Integer.o
main.o: main.c Integer.h
	g++ -c main.c -o main.o
The make utility would use the dependencies of the file to determine which commands to execute. The first line after that comment says that main is dependent upon main.o and Integer.o. Thus if the date on main is older than the date on Integer.o or main.o then make executes the associated command (the link step).

The dependencies among files can be complex. For example, a file fun.o is dependent upon a fun.c file and may also be dependent upon some header files. The executable file (like main above) is dependent upon the .o files. The make utility examines these dependences to ensure the commands are executed in the proper order. You wouldn't want the link step, for example, to be executed before the compilation steps. You need to be careful when you create a makefile that you specify the dependences correctly. A .c file will be dependent upon all of the header files that it includes (however you can omit standard header files like iostream.h) and an executable is dependent upon all of the .o files that need to be linked together to create that executable.

The make utility is executed by typing the command:

make

while in the directory containing the makefile. If the dependences and commands are in a file called makefile or Makefile then the make utility will find the file. If you call the file something else, you need to include the file name as an argument to the make utility like this:

make -f DoCompile

When make is invoked as above (make or make -f DoCompile), it use the very first target in the file to determine what commands to execute. Typically, the first target in the makefile is the executable. The make utility examines the files upon which the executable is dependent, comparing the dates of the .o files to the executable files. If the .o files are also targets elsewhere in the makefile, the make utility will compare the dates of the .o file to the .c and .h files upon which the .o is dependent in order to determine whether the associated command (the compile command) should be executed. In a sense, the make utility builds a tree with the executable at the top, the .o files at the next level and the .c and .h files at the bottom level. It compares the date of the file with it dependent files at the level below and if the dependent files have a newer date, the associate command is executed. For example, consider the previous example and assume that the file Integer.h is modified prior to executing the make command. The tree of dependences can be seen in the following figure:

tree of dependences

Using the tree, make executes commands associated with dependences in a bottom up order. Thus, first main.c will be compiled because of the dependence between main.o and Integer.h. Next, Integer.c will be recompiled because of the dependence between Integer.o and Integer.h. Finally, the link step will be executed because the main executable is dependent upon main.o and Integer.o which will have newer dates than main after main.c and Integer.c are recompiled.

make builds this tree placing the very first target at the root. Consider the previous example with the link command at the bottom instead of the root. (See below.) If make is invoked, the tree built by make consists only of the subtree with Integer.o at the root. Thus the only command that could be executed is the command to recompile Integer.c. I can indicate to make a different target to place at the top of the tree by typing:

make target

or

make -f DoCompile target

For example, for the makefile below, I could type

make main.o

to cause main.c to be recompiled if necessary or

make main

to build the tree above and execute all necessary compilation commands and the link step.

#This is a comment
Integer.o: Integer.c Integer.h
	g++ -c Integer.c -o Integer.o
main.o: main.c Integer.h
	g++ -c main.c -o main.o
main: main.o Integer.o
	g++ main.o Integer.o -o main

Line Wrap

Suppose a target has so many dependences that is impossible to list them on one line without wrapping. Dependences can be continued on subsequent lines by placing a backslash at the end of the previous line. For example,

file.o: file.c file.h header1.h header2.h header3.h header4.h header5.h\
        header6.h header7.h header8.h header9.h header10.h header11.h\
        header12.h
	g++ -c file.c
Above, spaces precede dependent files header6.h and header12.h. A tab character precedes the command (g++ -c file.c).

Implied Dependences

To make creating makefiles easier, make allows you to leave some dependencies and commands out. For example, the makefile below is equivalent to the previous one:

#This is a comment
CC = g++
main: main.o Integer.o
	g++ main.o Integer.o -o main

Integer.o: Integer.c Integer.h

main.o: main.c Integer.h

The commands to compile Integer.c was left off. Make assumes that for a .o file, you want a compilation command. The compiler to use is specified by the macro definition: CC = g++. Without this macro definition, make would use the cc compiler instead of g++. Note that the files need to be ".c" files and not ".C" or ".cpp". Although ".C" and ".cpp" are valid suffixes for C++ files (and widely used), make doesn't know what to do with these types of files and won't be able to figure out that they need to be compiled. If you want to uses either of these suffixes, you'll need to tell make what to do with these types of files. You can do that by adding the following lines to the top of your makefile (I typically put them below the macros):
.SUFFIXES: .C .o
.C.o:
	g++ -c -g $<
The .SUFFIXES directive indicates valid suffixes for files in the directory. The command below the .C.o directive indicates how to create a .o file from a .C file. $< represents a .C file.

Macros

The make utility has a macro facility that enables you to create and use macros within a makefile. The format of a macro definition is shown below.

ID = list

After this macro definition, $(ID) represents list in the makefile.

The makefile below uses macros, implied dependences and has added other commands which have nothing to do with compiling.

#This is a comment
CC = cc
OBJ = main.o function.o
main: $(OBJ)
	$(CC) main.o function.o -o main
function.o: function.c number.h
main.o: main.c number.h
print:
	psnup main.c | lpr -PLab205Laser
	psnup function.c | lpr -PLab205Laser

The first statement after the comment defines a macro. If you wanted to change the compiler from cc to gcc, you would only need to change that line.

The compilation commands are omitted; make figures these out for you. The link step can not be omitted.

The print target is not dependent upon anything. You need to explicitly tell the make utility to execute the commands associated with the print by typing:

make -f makefilename print

or

make print

if the makefile commands are in the file makefile or Makefile.

Touch command

Occasionally you may want to force make into recompiling your program. You can do this by setting the date of all of the .c files to be more recent then the date of the corresponding .o files. The touch command sets the file access time of a file to the current time. For example,

touch *.c

sets the date of all of the files in the directory ending with a .c to be the current date. If make is executed after this, all files will be recompiled.