Make

August 7, 2012

We continue today our occasional series based on classic Unix utilites. The make command maintains a set of file dependencies, given a definition of those files which depend on others and the commands used to update the predecessors. Make is commonly used to automate program compilation, but it also finds use in other ways, such as maintaining the dependencies between the files that make up a complicated document or website. Here is a typical makefile for a small C program:

prog: a.o b.o c.o
cc a.o b.o c.o -ly -o prog

a.o: prog.h a.c
cc -c prog.h a.c

b.o: prog.h b.c
cc -c prog.h b.c

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

c.c: c.y
yacc c.y
mv y.tab.c c.c

print:
pr prog.h a.c b.c c.y

The first line says that the target prog depends on files a.o, b.o and c.o and is generated by calling the C compiler to link a.o, b.o, c.o and the y library into the executable file prog. The value of make is that it eliminates needless work; if everything is up to date and a change is made to the yacc grammar in c.y, the only commands that need to be run are the yacc and mv commands to rebuild the c.y file, the cc command that rebuilds c.c, and the cc command that rebuilds prog:

yacc c.y
mv y.tab.c c.c
cc -c c.c
cc a.o b.o c.o -ly -o prog

The make program begins by reading the makefile and storing the dependencies and the associated commands. Then it takes the target, checks the filesystem to determine the ages of the target’s predecessors, calls itself recursively to update any older predecessors, and finally calls the commands associated with the target.

Your task is to write a program that takes a target and updates it according to the rules of the makefile. When you are finished, you are welcome to read or run a suggested solution, or to post your own solution or discuss the exercise in the comments below.