2.6. Writing and Debugging a Makefile

Now you know most of what is in a Makefile, what do you do next? There are two choices: use one of the uncommonly-available makefile generators or write your own makefile (I leave out the third choice of ignoring PMake and doing everything by hand as being beyond the bounds of common sense).

When faced with the writing of a makefile, it is usually best to start from first principles: just what are you trying to do? What do you want the makefile finally to produce? To begin with a somewhat traditional example, let's say you need to write a makefile to create a program, expr, that takes standard infix expressions and converts them to prefix form (for no readily apparent reason). You have got three source files, in C, that make up the program: main.c, parse.c, and output.c. Harking back to my pithy advice about dependency lines, you write the first line of the file:

expr            : main.o parse.o output.o

because you remember expr is made from .o files, not .c files. Similarly for the .o files you produce the lines:

main.o          : main.c

parse.o         : parse.c

output.o        : output.c

main.o parse.o output.o : defs.h

Great. You have now got the dependencies specified. What you need now is commands. These commands, remember, must produce the target on the dependency line, usually by using the sources you have listed. You remember about local variables? Good, so it should come to you as no surprise when you write:

expr            : main.o parse.o output.o
	cc -o $(.TARGET) $(.ALLSRC)

Why use the variables? If your program grows to produce postfix expressions too (which, of course, requires a name change or two), it is one fewer place you have to change the file. You cannot do this for the object files, however, because they depend on their corresponding source files and defs.h, thus if you said:

cc -c $(.ALLSRC)

you will get (for main.o):

cc -c main.c defs.h

which is wrong. So you round out the makefile with these lines:

main.o          : main.c
	cc -c main.c

parse.o         : parse.c
	cc -c parse.c

output.o        : output.c
	cc -c output.c

The makefile is now complete and will, in fact, create the program you want it to without unnecessary compilations or excessive typing on your part. There are two things wrong with it, however (aside from it being altogether too long, something I will address in Chapter 3, Short-cuts and Other Nice Things):

  1. The string main.o parse.o output.o is repeated twice, necessitating two changes when you add postfix (you were planning on that, were not you?). This is in direct violation of de Boor's First Rule of writing makefiles:

    Anything that needs to be written more than once should be placed in a variable. I cannot emphasize this enough as being very important to the maintenance of a makefile and its program.

  2. There is no way to alter the way compilations are performed short of editing the makefile and making the change in all places. This is evil and violates de Boor's Second Rule, which follows directly from the first:

    Any flags or programs used inside a makefile should be placed in a variable so they may be changed, temporarily or permanently, with the greatest ease.

The makefile should more properly read:

OBJS            = main.o parse.o output.o

expr            : $(OBJS)
	$(CC) $(CFLAGS) -o $(.TARGET) $(.ALLSRC)

main.o          : main.c
	$(CC) $(CFLAGS) -c main.c

parse.o         : parse.c
	$(CC) $(CFLAGS) -c parse.c

output.o        : output.c
	$(CC) $(CFLAGS) -c output.c

$(OBJS)         : defs.h

Alternatively, if you like the idea of dynamic sources mentioned in Section 2.3.1, “Local Variables”, you could write it like this:

OBJS            = main.o parse.o output.o

expr            : $(OBJS)
	$(CC) $(CFLAGS) -o $(.TARGET) $(.ALLSRC)

$(OBJS)         : $(.PREFIX).c defs.h
	$(CC) $(CFLAGS) -c $(.PREFIX).c

These two rules and examples lead to de Boor's First Corollary: Variables are your friends.

Once you have written the makefile comes the sometimes-difficult task of making sure the darn thing works. Your most helpful tool to make sure the makefile is at least syntactically correct is the -n flag, which allows you to see if PMake will choke on the makefile. The second thing the -n flag lets you do is see what PMake would do without it actually doing it, thus you can make sure the right commands would be executed were you to give PMake its head.

When you find your makefile is not behaving as you hoped, the first question that comes to mind (after What time is it, anyway?) is Why not? In answering this, two flags will serve you well: -d m and -p 2. The first causes PMake to tell you as it examines each target in the makefile and indicate why it is deciding whatever it is deciding. You can then use the information printed for other targets to see where you went wrong. The -p 2 flag makes PMake print out its internal state when it is done, allowing you to see that you forgot to make that one chapter depend on that file of macros you just got a new version of. The output from -p 2 is intended to resemble closely a real makefile, but with additional information provided and with variables expanded in those commands PMake actually printed or executed.

Something to be especially careful about is circular dependencies. For example:

a         : b

b         : c d

d         : a

In this case, because of how PMake works, c is the only thing PMake will examine, because d and a will effectively fall off the edge of the universe, making it impossible to examine b (or them, for that matter). PMake will tell you (if run in its normal mode) all the targets involved in any cycle it looked at (i.e. if you have two cycles in the graph (naughty, naughty), but only try to make a target in one of them, PMake will only tell you about that one. You will have to try to make the other to find the second cycle). When run as Make, it will only print the first target in the cycle.

All FreeBSD documents are available for download at https://download.freebsd.org/ftp/doc/

Questions that are not answered by the documentation may be sent to <freebsd-questions@FreeBSD.org>.
Send questions about this document to <freebsd-doc@FreeBSD.org>.