Return to lecture notes index
November 3, 2009 (Lecture 18)


What is make

"Make" is a utility that helps a software developer build software. Without some way of organizing a large build, software developers would be forced to waste time by rebuilding all components -- or to risk missing a modified component and building an inconsistent update. Make automates the process ensuring a fast, reliable build. Its configuration file, the "Makefile", serves roughly the same purpose as a CodeWarrior or Microsoft Project.

Why make?

Its easy and efficient to build small projects with a compile command. For example,

cc myapp.c -o myapp. 

However, this method becomes very inefficient for large projects such as the ones you will be building in this class. For example, running

cc foo.c bar.c baz.c -o myapp 

when baz.c changes means you are wasting time compiling the other files. Ideally, you would like to only compile baz.c and then link it with the other object files. You could do this yourself, but make lets you automate it. The goal of make is to build your project with the minimum amount of work.

Basic Idea

You supply make with a file (whose default nameis " Makefile") which describes the dependenciesbetween files in your project and a method for satisfying each dependency. These dependenciesform a DAG - for example:

Now, when make is run, for each dependency, if the target file is older than the file it depends on, it will execute the method to bring the target file up to date. For example, suppose we change foo.h. Make will recognize that foo.o and bar.o depend on it and will recompile them. Next, it will relink myapp.

Simple Makefile

A makefile consists of one or more rules which have the form:

target : source(s) 

The first character of each command line must be a TAB. The makefile corresponding to the above graph is

myapp: foo.o bar.o baz.o
       cc foo.o bar.o baz.o -o myapp
foo.o: foo.c foo.h
       cc -c foo.c
bar.o: bar.c bar.h foo.h  baz.h
       cc -c bar.c
baz.o: baz.c baz.h 
       cc -c baz.c

Comments Any line beginning with a `#' is ignored by make.


One headache with makefiles is making sure that you have specified dependencies to header files correctly. Makedepend is a tool that will do this for you automatically. You run makedepend on all your source files:

makedepend foo.c bar.c baz.c and it will add the correct dependencies to Makefile. You can use the -MM option of gcc to do the same thing. By default, makedepend adds the dependencies directly to a file named "Makefile" or "makefile". The -MM option of gcc/++ outputs the dependency rule to the console.


Makefiles can have macro definitions and uses. For example, with

CC    = gcc 
#CCOPT = -O2

foo.o: foo.c foo.h
       $(CC) $(CCOPT) -c foo.c

foo.c will be compiled for debugging or with optimization depending on which CCOPT isuncommented. Macros definitions can also be modified when they are used. For example,

OBJECTS = foo.o bar.o baz.o 
      makedepend $(OBJECTS:.o=.c)

will cause makedepend to be called on foo.c, bar.c and baz.c when the dep target is made.

Suffix Rules

Often, a project has many rules that have common commands applied to files with the same suffixes. For example, each of the .o files in our example depend on its parent .c file and is compiled with the same command. We can replace this with a suffix rule:

.c.o :
       $(CC) $(CCOPT) -c $*.c -o $@

$* is a special macro for the prefix the two files share and $@ contains the target name.

Default Rules

Make has a lot of built in defaults that are used when a user-defined rule can't be found. For example, it can infer that foo.o depends on foo.c and use a generic C compilation rule to update foo.o. In general, AVOID USING THE DEFAULT RULES.

A Menagerie of Makes

There are many different Makes with widely varying features. Perhaps the most popular alternate make is GNU Make, which has the advantage of having a manual freely available. Some makes exploit the parallelism of the dependency graph to distribute the make across a number of workstations.

A Larger Example

CC    = gcc 
#CC   = cc 
CPP   = g++ 
INC   = -ILEDA/incl 
            $(INC) $(LIB) 
#CCOPT  = -O2 $(INC) $(LIB) 

GENERALS  = cache.o disthandler.o \  
            internals.o dispatcher.o util.o \  
            builder.o group.o relation.o \  
            errprint.o diffmaprle.o aapair.o \  
            aablock.o stdrel_llb.o \  

DISPLAY   = display.o 

DISPLIBS  = -lP -lG -lL -lWx -lX11 -lm 

all: fung  

fung: libdist.a libfxtimers.a fung.o \   
      $(CPP) $(CPPOPT) fung.o \
      libdist.a libfxtimers.a \         
      $(DISPLAY) $(DISPLIBS) -o fung

libdist.a : $(GENERALS) $(INTERNALS)
      ar ruv libdist.a $(GENERALS) \           

$(DISPLAY): display.C
      $(CPP) $(CPPOPT) -c display.C \          
      -o $(DISPLAY)

.c.o :
       $(CC) $(CCOPT) -c $*.c -o $@

       makedepend $(INC) \         
       $(GENERALS:.o=.c) \           
       $(INTERNALS:.o=.c) \         

Function-like Macros

Take a second. Try to call to mind the use we've made of the C preprocesssor. What have we done:

Now, let's think for a moment about functions. Functions are great. They give us a way to reuse code -- we can write a function once and use them over-and-over again. This gets us much bang for the buck -- and makes the code maintainble by ensuring that corrections or changes need only be made in once place. And, it helps to make code meaningful by identifying segments of code with meaningful and descriptive names.

But, these benefits come at a cost. When you take 15-213, you'll learn a little bit about the mechanics of a function call. For now, let's just recall something we've said before -- there is overhead associated with making a function call and in returning from a function. It takes time.

Normally, for most functions, this small amount of overhead is no big deal. But, for really small functions, it can take longer to make the call and to return than to actually do the work. And, in certain essoteric types of programming, it can, under certain circumstances, be diifuclt or impossible to make function calls, because function calls change the stack.

We can get around this by using #define to create a function-like macro. The basic idea is that we define something that looks like a function within a #define. And then, after that, we can basically use it like a function. But, there are three big differences:

Let's take a quick look at the following function-like macro and its use. We see that the macro si replaced and explanded by the preprocessor.

  #define multiply(x,y) (x * y)


  int c =  multiply(10,5); /* int c = (10 * 5);

Now, let's ocnsider the example below. It is broken. Do you see why? The extra space breaks the preprocessor -- the first space acts to separate the macro from its definition.

  #define multiply (x,y) (x * y)
  /*              ^            */
  /*              |            */
  /*          Evil space       */


Let's take a look at another interesting case. Here is an example that probably doesn't work as you'd expect:

  #define multiply(x,y) (x * y)


  int c =  multiply(10+5,5); /* int c = (10+5 * 5);


Take a second look at the code above. Notice that it seems that we are asking to evaluate "((10+5)*5) = 75". But, it is expanded without regard to the grouping, so the order of oeprations is controlling -- and multiply beats add: "Please excuse my dear aunt sally". So, we get "(10+5 * 5) = (10 + (5*5)) = 250"

To fix this, when defining a macro, we always ()-parenthesize each-and-every use of the macro's arguments as follows. Notice how the parentheses force the right grouping:

  #define multiply(x,y) ((x) * (y))


  int c =  multiply(10+5,5); /* int c = ((10+5) * (5));

Having said that, I'd like to reiterate and amplify what I've just said: Always parenthesize each and every use of a paramter within a macro -- whether you think you need it or not. I've shown you one alligator -- there are more in the swamp. Do go looking for them -- just stay safe in the boat. This is another example of defensive programming.