Whatcha' gonna make?
Make is a useful tool for forming the basis of a backup or archive system
Summary
The make utility is frequently thought of as a programmer's tool (if it's thought of at all), but it has applications outside the world of programming. First, Mo will cover exactly what make is, then he'll show you some real-world uses for it. (2,900 words)
Make allows you to create, recreate, or update a file based on the
existence and/or modification of one or more other files. The simplest way to
understand make is to look at a programming example.
In C programming, a C source code file is created with vi or a similar editor.
This file usually has the extension .c, so we'll start with a
program called hello.c. The source code is compiled into an object that usually
has an .o extension, so in this case the object would be hello.o. The object
file is finally linked into a running program with no extension, which is called hello.
In terms of the make utility, the target hello.o depends upon hello.c, and the
target hello depends upon hello.o.
A make file is a script-like file that describes this target/dependency
relationship and also provides the commands needed to create or recreate one
target file from one or more other dependent files. The target, the dependency,
and the command constitute a make rule. Sample make rules for this simple
example are shown in the following listing.
hello.o : hello.c
cc -c hello.c
hello : hello.o
cc hello.o -o hello
In the first rule, hello.o appears on a line followed by a colon and then
hello.c. Note that the spaces around the colon are required. This is the make
syntax used to indicate that hello.o depends on hello.c. Underneath that line
is the command cc -c hello.c, which is the command to be executed if hello.o
needs to be created. The second rule is similar, stating that hello depends on
hello.o and if hello is to be created, then the command cc hello.o -o hello
will be used to create it.
The make utility uses a default make file that is named, appropriately enough,
makefile or Makefile. If you place the lines shown above into a
file named makefile, and then into a directory containing the
source code hello.c, you can type the command:
make hello
and the hello program will be built. Typing the command a second time will cause
a message indicating that the hello program is up to date, as in the following listing:
make hello
'hello' is up to date
What does "up to date" mean? It means that the modification date and timestamp on hello is greater than or equal to the modification date and timestamp
on all the files that hello depends on -- in this case hello.o and ultimately
hello.c.
Whenever you run make, it creates an internal table of dependencies and then
verifies the creation or modification date and timestamps and works out what
has to be created or recreated. This includes checking to see if a file doesn't
exist at all. For example, the first time you run make hello, hello.o does
not exist at all and is therefore considered out of date.
If the programmer modifies hello.c, its new modification date and timestamp
force it to become out of date with respect to hello.o. If make hello were run
again after the program changes were created, hello.o would be rebuilt from
hello.c. Then hello would be out of date with respect to hello.o so it would
be rebuilt.
In this way make does two jobs. It checks whether a program or file needs to
be rebuilt based on changes made to the original source code or dependency
file, and it simplifies the commands that have to be executed to recreate the
program or target file. The make hello command is much easier to remember
and faster to type than cc -c hello.c followed by cc hello.o -o hello.
Beyond programming
Now we'll look at a more complicated process and take it out of the
programming arena.
Assume that you've created a book containing five chapters named chap1.txt
through chap5.txt. The process of assembling the chapters into a book involves:
- Combining the chapters
- Building a table of contents to place at the beginning of the book
- Paginating the resulting table of contents and chapters
- Building an index
- Appending the index to the end of the book
- Paginating one or more times to get the index pages numbered
Let's also assume that you have four programs available. The firsts, cat, concatenates text
files, toc builds a table of contents, idx builds an index, and paginate
renumbers the pages.
The first version of this process uses several intermediate files. The rules
for this process are shown in the following listing. There is a problem with
this makefile that I will cover in a moment, but as it stands, it illustrates
several new things about make that you need to know.
# builds thebook.txt from the chapters
.SUFFIXES: .txt
allchaps.txt : chap1.txt chap2.txt chap3.txt chap4.txt chap5.txt
cat chap1.txt chap2.txt chap3.txt chap4.txt chpa5.txt >allchaps.txt
tocchaps.txt : allchaps.txt
toc allchaps.txt >toc.txt
cat toc.txt allchaps.txt >tocchaps.txt
paginate tocchaps.txt
rm -f toc.txt
rm -f allchaps.txt
thebook.txt : tocchaps.txt
idx tocchaps.txt >thebook.txt
paginate thebook.txt
rm -f tocchaps.txt
The first entry is not a rule, but a signal that .txt is to be added to the
list of standard suffixes that make recognizes. The list of standard suffixes
usually includes some standard file extensions such as .c for C files, .o for
object files, .sh for shell scripts, and so on. The .txt extension is not in
the standard list and so needs to be added.
After the .SUFFIXES entry, the first rule illustrates that one target
file may have more than one dependency. It states that the file allchaps.txt
depends upon all of the five chapters and is created by using cat to string
them together into the one file: allchaps.txt. The second rule illustrates that more than one command can be executed to create the required target
file. It states that tocchaps.txt (table of contents plus chapters) depends
upon the existence of allchaps.txt and is created by running the toc program
to create a table of contents, then concatenating toc.txt and allchaps.txt to
create tocchaps.txt. The result is paginated and then the two temporary files,
toc.txt and allchaps.txt, are removed. This leaves a paginated file containing
a table of contents and the chapters. The final rule adds the index,
repaginates, and leaves the output named thebook.txt.
If you have all the chapters and this makefile in one directory and type
make thebook.txt, make will build thebook.txt for you. If, later on, you
edit one of the chapters, the same command will rebuild your book with the new
information.
Earlier, I mentioned a problem with this makefile. The problem is that even if
you don't edit one of the chapters and type make thebook.txt, the whole
process will be executed again. Why? The simple answer is because the
intermediate files are deleted. Look at what happens to make when you type the
command. Make goes to the rule for building thebook.txt and sees that it needs
(is dependent upon) something called tocchaps.txt. But if the book was created
using make the first time, then tocchaps.txt was deleted. The make utility
can't find tocchaps.txt so it looks for the rule on how to build it and finds
that it needs a file named allchaps.txt. This file is also missing, so it turns
to the rule for allchaps.txt and finally finds something it can work with. At
this point a complete rebuilding begins.
A better approach is to create the dependency that you want. Set up
the makefile so that thebook.txt depends directly on the five chapters as in
the following example.
# builds thebook.txt from the chapters in one rule
.SUFFIXES: .txt
CHAPTERS= chap1.txt chap2.txt chap3.txt chap4.txt chap5.txt
thebook.txt : $(CHAPTERS)
cat $(CHAPTERS) >allchaps.txt
toc allchaps.txt >toc.txt
cat toc.txt allchaps.txt >tocchaps.txt
paginate tocchaps.txt
idx tocchaps.txt >thebook.txt
paginate thebook.txt
rm -f toc.txt
rm -f allchaps.txt
rm -f tocchaps.txt
In this listing, a macro has been used to create a list of the chapters. The
line that begins CHAPTERS= sets the variable chapters to the names of the five
chapters. Later in the makefile, $(CHAPTERS) is used as a shorthand for the
list of files. This is a handy way of creating a list so that if, at a later
time, you create a Chapter 6 for the book, simply add it to the variable
CHAPTERS and the makefile will run correctly and produce a new version of the
book.
In this listing, only one rule with one set of dependencies exists. The whole
book is built only if thebook.txt does not yet exist, or if any of the
chapters has been modified since the last time thebook.txt was created. This
is essentially the trick with make: get the dependencies right.
Printing and making a dummy
Now suppose you also want to create a printed hard copy of any chapter
that changed. You can use make to do this, but you need to use a little trick.
The problem is that a printed copy of a chapter file doesn't produce another
file. How can you check to see if a file has been printed since the last time it was
modified? In order to do this successfully, you need to create a dummy file.
Whenever you print any updated chapters, the dummy file modification date and
timestamp is updated. The make utility checks the timestamp the next time a
print request is made.
Let's look at how to do this. The example below
contains another problem that we'll look at in just a moment, but first take
a look at the listing in relation to the dummy file idea. The dummy file is
named "printed."
# Prints out of date chapters
.SUFFIXES: .txt
CHAPTERS= chap1.txt chap2.txt chap3.txt chap4.txt chap5.txt
printed : $(CHAPTERS)
pr $(CHAPTERS) | lp
touch printed
The rule for the file, printed, checks for a file named printed and uses the
chapters to "create" the file using the two commands. First the chapters are
printed and then the file printed is created or updated using touch. The touch
command will create an empty file if one doesn't exist, or update the
modification date and time if one does exist. In this example the printed file
is only being used as a marker to indicate when the chapters were printed. The
next time make printed is run the date and timestamp on printed will show
that it's up to date with respect to the chapters.
The problem with this listing is that if any chapter has been updated, all
chapters are printed. What we want is only to print chapters that have been
updated. The make utility includes a special macro variable that stands for
"all of the dependent files that are out of date with respect to the target
file." The macro is a dollar sign followed by a question mark ($?). If we
rewrite the above listing using this macro, it becomes:
# Prints out of date chapters
.SUFFIXES: .txt
CHAPTERS= chap1.txt chap2.txt chap3.txt chap4.txt chap5.txt
printed : $(CHAPTERS)
pr $? | lp
touch printed
If Chapters 3 and 5 have been modified since the last time the chapters were
printed out, the pr command becomes:
pr chap3.txt chap5.txt | lp
The printed file is still touched to indicate that all the out of date
chapters have been printed. Notice the difference in the two make problems. In
the first book example, we wanted to use all chapters to recreate the book if
any chapter was updated. In the second example, we want to use only the
chapters that have been updated to create hard copy. The $? macro variable is
perfect for instances when you only want to process the newer files in a list.
More than one make rule can be combined in one makefile, and the rules do not
have to be directly related. The two make rules that we have looked at so far
are combined with two additional rules shown in the following listing. Using
this makefile, you may type make thebook.txt to create the newest version of
the book, or type make printed to print out the updated chapters, or type
make chaps.tar.Z to create a compressed tar archive of the chapters, or type
make list to list the component chapters. The third and fourth rules require
some further explanation.
More rules
The third rule, to create chap.tar.Z, has another interesting feature of make.
The first step is to remove the old version of this file
with the command -rm .chap.tar.Z. The hyphen in front of the rm command is a
signal to the make utility that it shouldn't quit if there is an error. The
normal behavior of make is to exit when an error occurs. The first time
you try to create chaps.tar.Z, the file will not exist, and the rm command
will fail with an error. Normally this error would stop make from running any
further. The hyphen signals the make utility that this isn't a critical
command and an error on it can be ignored.
The fourth rule has a target -- list -- with no dependencies listed. When this
occurs in a make file, no checking for out of date dependencies is done, and
the associated commands are simply executed. In this example the names of the
input files that are processed by this make file are echoed to the screen.
# builds thebook.txt from the chapters
# also prints lists or archives chapters
.SUFFIXES: .txt
CHAPTERS= chap1.txt chap2.txt chap3.txt chap4.txt chap5.txt
thebook.txt : $(CHAPTERS)
cat $(CHAPTERS) >allchaps.txt
toc allchaps.txt >toc.txt
cat toc.txt allchaps.txt >tocchaps.txt
paginate tocchaps.txt
idx tocchaps.txt >thebook.txt
paginate thebook.txt
rm -f toc.txt
rm -f allchaps.txt
rm -f tocchaps.txt
printed : $(CHAPTERS)
pr $? | lp
touch printed
chaps.tar.Z : $(CHAPTERS)
-rm chaps.tar.Z
tar cf chaps.tar $(CHAPTERS)
compress chaps.tar
list :
echo $(CHAPTERS)
The make utility uses a default make file named makefile or Makefile. It also
uses a default make rule, which is the first rule in the file. If you simply
typed make and pressed Enter, the rule for thebook.txt would be executed as
it is the first rule in the file. Most make files are arranged so that the
main process appears as the first rule.
There's one other feature of make that is worth pointing out. A make file can
contain a command that executes make. Assume for a moment that when you decide to rebuild the book, you also want to update the archive
chaps.tar.Z. One easy way to handle this is to add a new first rule in the
make file. This first rule has no dependencies, so the commands are always
executed. The first command runs make to create the book, and the second runs
make to create the archive. This make file can be started by typing: make or
make bookarch.
# builds thebook.txt from the chapters
# also prints lists or archives chapters
.SUFFIXES: .txt
CHAPTERS= chap1.txt chap2.txt chap3.txt chap4.txt chap5.txt
bookarch:
make thebook.txt
make chaps.tar.Z
thebook.txt : $(CHAPTERS)
cat $(CHAPTERS) >allchaps.txt
toc allchaps.txt >toc.txt
cat toc.txt allchaps.txt >tocchaps.txt
paginate tocchaps.txt
idx tocchaps.txt >thebook.txt
paginate thebook.txt
rm -f toc.txt
rm -f allchaps.txt
rm -f tocchaps.txt
printed : $(CHAPTERS)
pr $? | lp
touch printed
chaps.tar.Z : $(CHAPTERS)
-rm chaps.tar.Z
tar cf chaps.tar $(CHAPTERS)
compress chaps.tar
list :
echo $(CHAPTERS)
This collection on make gives you some basics, as well as what could be the
beginning of a backup and archive system. It can also be used in any other process that needs to create one thing from several dependents.
The make utility is a big subject and many of its options not covered here
were originally designed to handle program development. The examples I've
given show how to use it as an administration tool. You should also study other
make files, such as the ones I tracked down for you in the Resources section.
Contact
us for a free consultation. |