make intro

A brief introduction to make and makefiles

a simple example | some details | an example with *.c and *.h files | hints

The make utility is a very flexible tool for, installing downloaded programs and compiling your own code, among other things. Here are some very crude instructions for how to use make to streamline the compilation of your code into executable files.

Getting started.

Suppose you have a C program, prog.c that you want to compile and run. If the code is self-contained, in that it has definitions for all functions called within it, then it can be compile into an executable program prog in the usual way:
gcc prog.c -o prog
But suppose your code isn't self-contained, that the functions called by main() (in prog.c) reside in a bunch of other files, fndef1.c, fndef2.c, etc. Furthermore, suppose that for each of these files there are corresponding header files (e.g., fndef1.h) containing the function prototypes. Then you might compile using
gcc prog.c fndef1.c fndef2.c fndef3.c ... -o prog -lm
(The -lm says link with the standard C math library.) But this can take a long time. The compiler first converts each of the .c files to object code, stored in files prog.o, fndef1.o, fndef2.o, etc. Then it links the object code together into on executable file prog.

Its no problem that it takes a little while to compile, but what if your code is broken somehow, i.e., "in the development stage", and you have to recompile several (read: many many) times? Then you could break the compilation process up. If you are working on prog.c and are certain the other files have no mistakes, then create their object files once an for all with

gcc -c fndef1.c fndef2.c fndef3.c ...
(The -c means "just compile, don't link together.") Then you could just type
gcc prog.c fndef1.o fndef2.o fndef3.o ... -o prog -lm
every time you modify prog.c. But what if the problem were not in that program, but in one of the other files? Or worse, their associated header files? Which ones need to be recompiled?

The basics

The solution is to use the make utility. To begin with let's assume that we have only two files to compile and link together, myprog.c (where main() is defined) and more_code.c (where some functions called by main are defined) neither of which use any header files except the standard ones (e.g., stdio.h). To use make, a third file called Makefile should be created with the following text:

# This is a makefile.
CC = gcc
CFLAGS = -Wall

myprog: myprog.o more_code.o
       ${CC} ${CFLAGS} myprog.o more_code.o ${LDFLAGS} -o myprog

       \rm myprog.o more_code.o myprog

Some quick comments on the above Makefile: The stuff to the right of a hash (#) is a comment. The make-specific variables (the ones in capital letters) follow a name convention such that: CC refers to the compiler (gcc in this case); CFLAGS contains compiler directives/options; and LDFLAGS is a list of link (or "load") directives (here, instructions to link with the C math library).

NB: the blank space in front of the ${CC} ... is a TAB character. Make will whine if it doesn't find one there.

To create your executable code, type

make myprog
(actually, just typing make will do the trick, for reasons explained below). This will generate an executable file myprog. To remove your files created with make, type
make clean

The magic of make is that it checks the modification times/dates of the executable file, the object files myprog.o and more_code.o (if they exist), and the source code myprog.c and more_code.c. The object files--compiled versions of the source code--are recompiled if the source code has been modified since the last compilation. Make knows how to do this "automatically". Then, if necessary, it (re)links the object code together to make the executable program.

For example, if you've never compiled any of your code, make will first compile myprog.c using gcc with the -c option to spit out the object code myprog.o, then similarly for more_code.c. Finally it will link the two .o files together (using gcc with the .o files as arguments but without the -c directive) to create the executable file.

If you run make and subsequently edit myprog.c, a second make command will recompile only the source code myprog.c creating a new myprog.o file; then make will relink both .o files to generate new executable code.

If you run make twice without making changes to any files, it will not do anything except inform you that your file prog is up-to-date.

Thus, make will exert the least amount of effort (minimimal compile/link time) necessary to keep your executable code up to date.

Some details.

The above example demonstrates what make does. It is important to examine why it does it. First we need some vocabulary (not all of which is gauranteed to be standard). A generic make file might look like this:
vars = values

target: dependencies

targ2 : more-depencies
When invoked as a shell command, make opens up Makefile (hereafter "the makefile") and seeks out the target name which matches its command-line argument, or the first target name if no arguments were given. Lets suppose the command was make target; then make would "go" to the line containing target: and look at the dependencies, typically a list of files. If any of those files were modified more recently than a file with the name target, the utility prepares to execute the command(s) listed in the line(s) immediately below the target-dependencies line. This happens also if no file named target exists, or if any of the dependencies correspond to non-existent filenames.

Before executing the command list, make checks to see if any of the dependencies are also targets themselves, specified elsewhere in the makefile. If so, it will look at the dependencies of these targets, then check to see if these new dependencies are also targets, and so forth in a recursive fashion. It will stop when there are no more dependencies to check (i.e., no more dependencies which are themselves targets). By this time, it might have ended up on a target which is deeply nested in some sense, the N^th level dependency-of-a-dependency of our original target. If there is a conflict with this level-N target and its dependencies (here "conflict" means that the target is file older than the dependency, or that the dependency and target names don't both resolve to existing files), then the corresponding commmand list gets executed, followed by the level-(N-1) command list, all the way down to the original target's command list. If no conflict exists, then the utility ignores the list and proceeds to look for a conflict between the (N-1)^th level target and its dependencies, and so forth.

The expectation in all this is that by executing command lists, make will update files (by recompiling code, for example) so that next time it is called, there will be fewer conflicts and hence fewer command lists to execute.

This description of recursive dependencies, if it made sense at all, might not seem to explain the behavior of the example makefile above, the one with the target named prog. Why would make bother compiling source code to make object files? The answer is that make has implicit rules regarding the relationship between .o files and .c files. We could rewrite the makefile to make these rules explicit:

CC = gcc
CFLAGS = -Wall

myprog: myprog.o more_code.o
       ${CC} ${CFLAGS} myprog.o more_code.o ${LDFLAGS} -o myprog

myprog.o: myprog.c
       ${CC} ${CFLAGS} -c myprog.c

more_code.o: more_code.c
       ${CC} ${CFLAGS} -c more_code.c

       \rm myprog.o more_code.o myprog
Now it is explicit: myprog depends on myprog.o and more_code.o, these in turn depend on the respective .c files, and everytime there is a modification time/date conflict, the appropriate object file gets recompiled and the executable code gets updated.

Note also the effect of make clean. This chooses clean as the target; it's (vacant) dependencies do not resolve to an existing file, so the \rm command gets executed every time.

Example src-code/header-file pair

As another example, let's consider a case where we have a local, user-created header file, in addition to C source-code. Let's call the relevant files prog, fndef.c and fndef.h, supposing that the first is where main() is defined and the last two contain a function definition and a function prototype, respectively. A reasonable makefile would be
CC = gcc
CFLAGS = -Wall

objects = prog.o more_code.o

prog: $(objects)
       ${CC} ${CFLAGS} ${objects} ${LDFLAGS} -o myprog

${objects}: fndef.h
Note that we have defined a variable objects to contain the names of the object files. We may just as well have typed them out explicitly, but somehow, fewer keystrokes make for better organization.

We have made a target out of both object file names that depend on the header file. This is because we want both object files to be updated whenever the header gets changed. If only one of these files had any reference to the header, say fndef.c, we would have used

fndef.o: fndef.h
in the last line above. If other "local" header files were included, then we would have listed them as dependencies also.

Finally, note that we have taken advantage of the implicit rules about compiling .c files to make .o files as needed. That's why we didn't have to put individual targets like

fndef.o: fndef.h
       ${CC} ${CFLAGS} -c fndef.c

On the other hand we may wish to override the implicit rules. One case would be if the code in fndef.c needed special compilation directives or if it were instead a Fortran 90 code fndef.f90. Then the appropriate target might be

fndef.o: fndef.h fndef.f90
       f90 -c fndef.f90
where f90 is the Fortran 90 compiler.

Using header files -- a digression

Why would you want to complicate things by introducing a header file? To answer this question, suppose the file fndef.c is composed as follows:
#include <math.h>
#include "fndef.h"

int fndef_parameter = FNDEF_PARM_DEFAULT;

float fndef(float *x, int n)
Note a couple of things: the integer variable fndef_parameter lies outside the scope of the function fndef and can be accessed/changed in other parts of a program which is linked with this code. Presumably, it will affect the execution of the function, but we hope that it's effect is arcane, otherwise it really should be in the argument list. Its default value is evidently going to be specified by a symbolic constant.

To wrap up the loose ends like the default value of the global variable, we use a header file, namely the one mentioned in the second line of the code above, fndef.h. Its contents might be

#define FNDEF_PARM_DEFAULT 10     /* a parameter's default value */
/* another good choice? */

extern int fndef_parameter;
/* the parameter use by fndef() stored as a global variable */

float fndef(float *x, int n);    
/* function prototype */
That would do it. When you compile fndef.c, the header is inserted in at the top, providing both the default value for the global parameter and a function prototype as a cross-check. Whenever you want to use this function in a program, just #include the header file, as in this example:
#include <stdio.h>
#include <math.h>
#include "fndef.h"
int main()
fndef_parameter = 1234;
zz = fndef(q,npoints);
Notice that the header file already takes care of the function prototype and the declaration of the external variable.


Fire up your browser and go to a search engine like
this one and search for keywords such as make, CFLAGS, AND LDFLAGS. You may find a wide variety of makefiles, some with very sophisticated usage which you may want to adopt.

Of course you can't believe everything you read on the web.

IMPORTANT! The blank space preceding each command (non-blank line[s] immediately below a target name) in a makefile must be a TAB character. If you just cut and paste a makefile off of the web, you will need to replace the blank spaces with a TAB (using emacs, for example).

Return to the top of this page.

bcb 5-Oct-98.