Lab: the make builder

CSC 323: Software design · Spring, 2012

Department of Computer Science · Grinnell College

0: Targets, prerequisites, and actions

When software is distributed in the form of source code, the package often includes a file, conventionally named Makefile, containing instructions for building and installing the software automatically. Makefiles are essentially scripts written in a declarative language and executed by an interpreter named make.

As in other declarative languages, control flow is not determined by explicit constructions of the text of the the make script, but is rather inferred by the interpreter from the dependency relationships that the programmer specifies. Makefiles are composed primarily of constructions called rules. A rule consists of one or more targets, zero or more prerequisites, and zero more more actions, arranged in the following format:

targets : prerequisites actions

If there are two or more targets, they are separated by spaces. If there are two or more prerequisites, they too are separated by spaces. If there are two or more actions, they may be separated by semicolons (in which case they will be executed in sequence in a shell that is launched solely to perform those actions), or they may be placed on separate lines (in which case they will still be executed in sequence, but a different shell is launched for each one).

Each action line must begin with a tab character -- not a sequence of eight spaces, but the actual horizontal tab character with ASCII code 0x09. Edit carefully.

A typical target is the name of a file that should be rebuilt automatically each time some other file is revised. The prerequisite is then the name of the other file, the one from which the target file should be constructed. The action is the command line that one would use to construct it.

For example, a makefile for a Java project might include the rule

HelloWorld.class : HelloWorld.java /usr/bin/javac -g HelloWorld.java

which directs make to build the target file HelloWorld.class (in the current working directory) from the prerequisite file HelloWorld.java, by invoking the javac compiler, provided that HelloWorld.java has been revised since HelloWorld.class was last constructed. The make utility determines whether this condition is met by comparing the timestamps on the files. It will also perform the action, of course, if HelloWorld.class does not exist and HelloWorld.java does.

Exercise: Write a rule that directs make to run the gcc compiler to obtain the object file queue.o from the source-code file queue.c, provided that queue.o either does not exist or is older than one or both of its prerequisite files queue.c and queue.h.

1: Running make

To run make, first create the Makefile in the directory containing the source files. Use cd to make it the current working directory, if you haven't already done so. Then, at the prompt in the terminal window, type make and the target or targets that you want the builder to construct. For instance, you would give the command

make HelloWorld.class

to direct make's attention to the rule for building HelloWorld.class.

You'll get a fairly clear error report from make if you specify a target that doesn't exist:

$ make frogs.bin make: *** No rule to make target `frogs.bin'. Stop.

If you don't specify any target at all, make assumes that you want to build the target of the first rule in the Makefile.

If you neglect to create the Makefile, make will look at the suffixes of the targets and try to guess what action you had in mind. Although it recognizes a few simple cases (it knows enough to run the C compiler in order to build a target file with a name ending in .o), for the most part it simply reports that it has no rule to apply.

Exercise: Create a folder make-lab for this lab and copy queue.c, queue.h, and test-queues.c from /home/stone/courses/software-design/code/ into that folder. Create a Makefile in the same folder, containing the rule you wrote in section 0 above. Don't forget to write comments into the Makefile. (Any line beginning with a mesh character, #, is a comment, and make will ignore it.) Run make to build the queue.o file.

2: Chained prerequisites

A file that is a target in one rule may be a prerequisite within some other rule. For instance, documentation that is prepared with the TEX typesetter and released in Portable Document Format might be processed by three different utility programs along the way. The .pdf file might be derived from a PostScript (.ps) version by means of the special-purpose ps2pdf converter, as directed by the following rule:

frogs.pdf : frogs.ps /usr/bin/ps2pdf frogs.ps frogs.pdf

The PostScript file in turn comes from a "device-independent" page-description file, depending on some program like a2ps to perform the conversion:

frogs.ps : frogs.dvi /usr/bin/a2ps -1 -o frogs.ps frogs.dvi

And TEX is used to construct the .dvi file from the .tex input file or files:

frogs.dvi : frogs.tex ranid-definitions.tex /usr/bin/tex frogs

If all three of these rules are placed in the Makefile, in any order, the command

make frogs.pdf

tells make to consider frogs.pdf and its direct and indirect prerequisites as targets, any or all of which might require its intervention. If it finds that frogs.dvi is older than frogs.tex, then it infers that it is necessary to run TEX in order to get a newer version of frogs.dvi. The timestamp on the new frogs.dvi then shows it to be more recent than frogs.ps, so make runs a2ps in order to get a revised frogs.ps file. This file is now more recent than frogs.pdf, so make runs ps2pdf to build the revised frogs.pdf file.

Exercise: Add to the Makefile that you created in section 1 above a second rule, directing make to recompile and re-link the executable test-queues if it does not exist, or if any of three files on which it depends -- test-queues.c, queue.h, or queue.o -- has changed since test-queues was constructed. Run make to build this executable. Then delete queue.o and run make again. Note that make rebuilds queue.o before recompiling test-queues.c.

3: Explicit variables in makefiles

Makefiles in which all of the rules are written out in full, like those shown above, are tedious to write and difficult to maintain. The make utility allows you to create and initialize string variables and to use the values of those variables in subsequent rules.

To create and initialize a variable, put the variable at the beginning of a line, followed by an equals sign. The string comprising the characters to the right of the equals sign becomes the value of the variable (except that whitespace characters adjacent to the equals sign, on either side, are ignored).

For example, one might write

JAVAFLAGS = -Xlint -g -verbose

to create the variable JAVA_FLAGS and give it the string value

-Xlint -g -verbose

Note that quotation marks are not needed as delimiters for the string value.

To use the value of a variable in a rule, write the variable with a dollar sign and left brace in front of it and a right brace after it:

Frogs.class : Frogs.java /usr/bin/javac ${JAVAFLAGS} Frogs.java

The make utility replaces the variable reference with its value before executing the action. For historical reasons, make also accepts parentheses rather than braces around the variable name, so that, for instance, $(JAVAFLAGS) is the same as ${JAVAFLAGS}.

A few variables are maintained internally by make and have values that are relative to the current rule. For instance, the variable < refers to the first prerequisite of the current rule, so that one could also write the rule shown just above as

Frogs.class : Frogs.java /usr/bin/javac ${JAVAFLAGS} ${<}

(Actually, one could even abbreviate the second variable reference to $<. The braces are optional for variables with one-character names.)

Similarly, in the action part of a rule, the variable @ refers to the target. Thus the rule for frogs.pdf in section 2 above could be written as

frogs.pdf : frogs.ps /usr/bin/ps2pdf $@ $<

Exercise: Add a CFLAGS variable at the beginning of your Makefile and use it in your rules to make sure that invocations of the C compiler always turn on the -Wall and -ggdb3 options.

4: Implicit, shell, and command-line variables

The make utility also recognizes several other ways of associating values with variables. It predefines thirty or so of them, such as FC for the FORTRAN compiler; if a rule contains the reference ${FC}, make replaces it with the default value f77 even if no explicit assignment appears in the Makefile.

The make utility also asks the shell that invokes it to pass along the values of any shell variables that have been set, such as PATH for the list of directories that the shell uses to search for executables and PWD for the current working directory. These, too, are available for use in rules.

Finally, the command line that you use to invoke make can include assignments to variables, such as PREP=/usr/bin/m4. In this case, you can't have whitespace on either side of the equals sign, and you'll need delimiters that the shell can recognize on the right-hand side if it contains spaces (as in A2PSFLAGS="-1 --landscape --no-header".

A variable assignment on the command line takes precedence over an explicit assignments within the Makefile, which in turn takes precedence over an assignment inherited from the corresponding shell variable, which in turn takes precedence over an implicit predefinition supplied by make.

If you give make the command-line option -p, it will print out (among other things) the values of all the variables it knows about, whatever their source, before it proceeds to rebuild the target(s).

Exercise: In the rules in your Makefile, replace the name of the GNU C compiler with a reference to the implicitly defined variable CC. Delete the executable test-queues and run make to rebuild it. What value does make supply for the variable CC? Does this work? Why or why not?

Exercise: Delete test-queues and run make again, this time specifying on the command line that the variable CC should have the value /usr/bin/gcc. Delete test-queues again, edit the Makefile to place the same assignment to CC at the top, and then run make again to rebuild the executable.

5: Suffix rules

After listing all of the variables and their values, make -p also prints out a number of suffix rules, supplying default actions for frequently encountered targets. For instance, one of these is the rule

%.dvi: %.tex # commands to execute (built-in): $(TEX) $<

which says that, if the target is any file that ends in .dvi, its prerequisite is the corresponding file ending in .tex, and you can build it by running the executable whose name is the value of the variable TEX on that prerequisite .tex file. Since make preloads this suffix rule, we don't actually need the explicit rule shown above for the target frogs.dvi -- make does the same thing by default (if TEX has the value /usr/bin/tex, at least).

Exercise: There is a similar rule for targets ending with .o and prerequisites ending in .c:

%.o: %.c # commands to execute (built-in): $(COMPILE.c) $(OUTPUT_OPTION) $<

Looking back through the output from make -p, we can see how the variables COMPILE.c and OUTPUT_OPTION were initialized. Note how they refer to still other variables.

COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c OUTPUT_OPTION = -o $@

References to uninitialized variables, such as CPPFLAGS in this example, are replaced with null strings when make processes the rules containing them.

Does the presence of this suffix rule make any of the commands in the Makefile that you've been constructing superfluous? If so, rewrite the Makefile, removing the superfluous commands. Then delete queue.o and test-queues and run make to rebuild them.

6: Other targets

It is possible to use identifiers that are not file names as targets, simply as triggers for actions that should be performed when one of the prerequisites changes or even (without prerequisites) simply when it is convenient to have make do them.

For instance, the Makefile in a software package that includes source code often includes a target install, with all of the executables in that the package provides as prerequisites, that copies those executables into some appropriate directory such as /usr/local/bin/ and makes sure that they have the correct permissions and ownership:

install : frogs toads /bin/cp frogs toads /usr/local/bin /bin/chmod 755 /usr/local/bin/frogs /usr/local/bin/toads /bin/chown root.root /usr/local/bin/frogs /usr/local/bin/toads

Exercise: Write a rule for a target called clean that removes all of the .o files and the executable test-queues. (This, too, is a common usage in makefiles for software packages. The idea is to force a completely new recompilation the next time make is run with any of the executables as targets.)

Further reading

Full documentation for GNU make is available on line: