Scripts and Utilities -- Make lecture



Make

Setup

Make is a configuration program that is both wonderful and frustrating. It's wonderful because it enables you to succinctly specify the configuration of the construction of a program, and then perform this construction whenever a part of the program is modified. It's frustrating because there are things like multiple machines, multiple directories, and automation that you wish make could handle as cleanly, but it doesn't, so you end up writing hacks, unreadable makefiles, and makefile generators that make porting your code very difficult.

And of course there are a million versions of make with different options and functionalities. As you should realize by now, we try not to use fancy options because they're almost never portable, so this will be a very basic make lecture.

The syntax of make is roughly:

make [ -f makefile ] [ targets ]
If you omit the -f, then it will use ./makefile or ./Makefile. If it can't find these, some make's will then look for a SCCS makefile (and I'm not going to cover SCCS, preferring rcs), then for makefile in your home directory, and then a system makefile. So if you type make and you see it doing things that you don't understand, chances are that it's using a bizarre makefile.

Dependencies

In your makefile, you list dependencies. What a dependency means is that the construction of the specified target depends on the listed files. For each target, make will recursively treat each dependency as a target. If a target has no dependencies and it exists as a file, then it will be considered ``made''. If a target does not exist, if any of its dependencies are files that are more recent than than it, or if any of its dependencies had to be ``made'', then the target will be ``made''. To make a target, you specify shell commands under the dependency line. You must begin those lines with a tab (use ``cat -v -t -e'' if you want to see the tabs in a makefile). All rules must be one TAB in from left margin. There can be multiple lines of rules for each dependency. That's the basic gist.

I'm sure you've all seen makefiles in the context of C. We'll use that as an example. Go into the directory make1, and look at the makefile. Look at all the source files. This is a simple program that is broken up into three C source files (printname.c, first.c and last.c) and two header files (fn.h and ln.h).

If you type make in the directory, you'll see it constructs printname.
UNIX> cd make1
UNIX> ls -l
total 6
-rw-r--r--  1 plank          58 Jul  7 10:19 first.c
-rw-r--r--  1 plank          21 Jul  7 10:26 fn.h
-rw-r--r--  1 plank          57 Jul  7 10:19 last.c
-rw-r--r--  1 plank          22 Jul  7 10:21 ln.h
-rw-r--r--  1 plank         352 Jul  7 10:21 makefile
-rw-r--r--  1 plank          42 Jul  7 10:20 printname.c
UNIX> make
cc -c printname.c
cc -c first.c
cc -c last.c
cc -o printname printname.o first.o last.o
UNIX> ls -l
total 33
-rw-r--r--  1 plank          58 Jul  7 10:19 first.c
-rw-r--r--  1 plank         236 Jul  7 10:28 first.o
-rw-r--r--  1 plank          21 Jul  7 10:26 fn.h
-rw-r--r--  1 plank          57 Jul  7 10:19 last.c
-rw-r--r--  1 plank         244 Jul  7 10:28 last.o
-rw-r--r--  1 plank          22 Jul  7 10:21 ln.h
-rw-r--r--  1 plank         352 Jul  7 10:21 makefile
-rwxr-xr-x  1 plank       24576 Jul  7 10:28 printname
-rw-r--r--  1 plank          42 Jul  7 10:20 printname.c
-rw-r--r--  1 plank         168 Jul  7 10:28 printname.o
UNIX> printname
Jim Plank
UNIX> 
Now, if you modify last.c (say to print out LAST twice), then type make again, it will only recompile what is necessary:
(we will cover ed the next lecture)
UNIX> ed last.c
57
ed: 1,$n
1       #include "ln.h"
2
3       printlast()
4       {
5         printf("%s\n", LAST);
6       }
ed: 5t5
ed: 5s/\\n/ /
ed: 5,6p
  printf("%s ", LAST);
  printf("%s\n", LAST);
ed: w
80
ed: q
UNIX> make
cc -c last.c
cc -o printname printname.o first.o last.o
UNIX> printname
Jim Plank Plank
UNIX> 
If you modify just one header file, then again, make will only recompile what is necessary. This is extremely convenient and efficient, especially when you are dealing with long compilation times, and many files.
UNIX> ed fn.h
21
ed: s/Jim/Dikembe
char *FIRST = "Dikembe";
ed: w
25
ed: q
UNIX> make
cc -c first.c
cc -o printname printname.o first.o last.o
UNIX> printname
Dikembe Plank Plank
UNIX> 
Notice that there is a target called clean. This is not a file that gets made, but a way to specify for make to clean up your directory:
UNIX> ed makefile
352
ed: /clean/,/clean/+1p
clean:
        rm -f a.out core printname *.o
ed: q
UNIX> make clean
rm -f a.out core printname *.o
UNIX> 

Little things: default target, multiple lines, errors

As a default, make makes the first target in the makefile. If you want to spread a dependency or a command over multiple lines, you must use a backslash as a line continuation. Often it's good form to put an ``all'' target as the first target so that you can always go into a directory and type ``make all'' to construct your executables. The makefile in the make2 directory is the same as the one in the make1 directory, only it has an ``all'' target, and some of its dependencies and commands are spread over multiple lines.

If any command that make executes exits with a non-zero value, then make will exit instantly. Make can be used to manage pretty much anything. The commands can be any valid command and are normally executed using sh. A note, if you put @ as the first character after the tab on a command line then make will not echo the command just execute it.

Variables

You can have variables in makefiles. You set them with a line like the following:
name = string
And you use them with the $(name) or ${name} construction.

You may use any shell environment variable like a normal variable in a makefile. Of course you can override the environment variable setting in the makefile if you want.

Look at the makefile in the make3 directory. This does two common things. First, it bundles up all the object files for the executable into a variable called OBJS. Second, it assumes that you have set the environment variable CC to point to your C compiler. Try it out:

UNIX> cd make3
UNIX> setenv CC cc
UNIX> make
cc -c printname.c
cc -c first.c
cc -c last.c
cc -o printname printname.o first.o last.o
UNIX> printname
Jim Plank
UNIX> make clean
rm -f a.out core printname *.o
UNIX> setenv CC gcc
UNIX> make
gcc -c printname.c
gcc -c first.c
gcc -c last.c
gcc -o printname printname.o first.o last.o
UNIX> printname
Jim Plank
UNIX>
To get a dollar sign passed to the shell, precede it with a backslash.

Most versions of make have default values for a specific set of variables. However the problem with this is that it assumes that the only version of make you will use is the current one. I would suggest that you follow a habit of always specifying the values for variables in every makefile so that there are no surprises. Some of the variables that are normally set are:

There is nothing magic about these variables but if you try to consistently use the names and always set the values, then others you give the makefile to will find it easier to understand what you are doing. There is one variable that you should NOT override and that is MAKE. You can use this when you want to say change directories and then execute make in that directory using that directories makefile. something like

	cd mydir;$(MAKE);cd ..
Another thing before we move on. Make can accept various command line arguments. One of those is "-n". This tells make to do everything except actually execute the commands. It checks the dependencies, echos out the commands as though it were executing them but doesn't actually execute them.

.SUFFIXES

It seems irritating to have to specify how to build each object file when all you're doing is calling ``cc -c''. To address this problem, you can specify in general how to build a file with one kind of suffix out of another. This has two parts. First there is a target line called .SUFFIXES which specifies the order in which suffixes should be processed. Second is a line of the form:
.Ds.Ts:
	commands
This says that you build a file with suffix .Ts out of a file with a suffix of .Ds using the commands. In the command you may use the variable $* to stand for the part of the name before the suffix.

For example, look at the makefile in the make4 directory. This is very typical of makefiles that you'll see for simple programs:

UNIX> cd make4
UNIX> make
cc -O4 -c printname.c
cc -O4 -c first.c
cc -O4 -c last.c
cc -O4 -o printname printname.o first.o last.o
UNIX> printname
Jim Plank
UNIX>
Note that it's easy to specify that first.o depends on fn.h while letting the default commands take care of making first.o. While we are discussing SUFFIXES is a good time to bring up a couple of special macros that are defined by most versions of make. These are "$*" and "$@". These two macros are the basename of the of the current target and the name of the current target. They can save you a lot trouble when used with SUFFIX rules as we see here.
.c.o:
        $(CC) -o $*.o -g $@

The list of macros allowed in make:
$< - the current dependcy file 
$@ - the current target file 
$* - the base name of the current target 
$? - all dependencies that are newer than the target

More complex things

There are many more things that you can do with make and all of its variants. Read the man page, and don't send me email that I've omitted something -- just consider yourself an advanced make user. I'm not sure if I've ever used more features of make than what I just told you. Granted, this means that I'm not using the full functionality of make, but it also means that my makefiles are portable, and that's important.

Handy make things

There are a few things that Dr. Plank does with make that make life easier. The first is his default makefile (homemakefile), which is in his home directory. He's aliased mk to always use that makefile:
UNIX> alias mk 
make -f /mahogany/homes/plank/makefile
UNIX>
The first useful thing in there is the line that makes an object file out of a c file:
.c:
        $(CC) -o $* -g -D$(ARCH) -I$(INCLUDE) $*.c $(LIBS) -lm
It assumes that the ARCH environment variable is set to reflect what machine he is on (this was stolen from PVM's archtype command). Then it makes sure to get include files from his home include directory, plus object libraries from the correct lib directory. In this way, he can write a quick C program that uses anything from his fields, dlist, rbtree, socketfun and dataproc libraries (see CS360 lecture notes for descriptions of all of these except the dataproc one), then make it with mk. Also it lets him make quick jgraph files, octal dumps, and tex-to-postscript files.

The second alias he has is the mkd alias:

UNIX> alias mkd
pushd !* ; make all ; popd
UNIX>
(the pushd and popd commands are for the shell to change directories, see
the man page on each)
This will cd into the specified directory, execute ``make all'', and then cd back. It's something that can be quite convenient.

Finally, mkmake is a simple shell script that creates a generic makefile for a directory. That makefile assumes that each .c file is a separate program, and creates the commands to make each program. This is pretty simple, but often useful. There are similar utilities on some Unix systems that make fancy makefiles. If you're interested try the man page on makedepend and go from there.