Tips and Tricks From the Automatic Dependency Generation Masters

[article]
Mr. Make
Summary:

Make's dependency syntax is flawed because it incorporates both foo.o must be updated if header.h, system.h or foo.c are changed and foo.o is the result of compiling foo.c.  Thus, anything to the right of the : is a prerequisite, but the first prerequisite where there's a rule body (i.e. commands) is special: it's the prerequisite that will be passed to the compiler (or other command) to actually generate the target.

GNU Make's mixed dependency syntax
Even Make's dependency syntax is flawed because it incorporates both 'foo.o must be updated if header.h, system.h or foo.c are changed' and 'foo.o is the result of compiling foo.c'.  Thus, anything to the right of the : is a prerequisite, but the first prerequisite where there's a rule body (i.e. commands) is special: it's the prerequisite that will be passed to the compiler (or other command) to actually generate the target.

Take a look at this example:

foo.o: foo.c header.h system.h
@echo Compiling $@ from $<...

which outputs:

Compiling foo.o from foo.c...

Here foo.o is built if foo.c, header.h or system.h change, but the rule also states that foo.o is made from foo.c.  If the example were written like this:

foo.o: foo.c
foo.o: header.h system.h
@echo Compiling $@ from $<...

the output would be

Compiling foo.o from header.h...

which is clearly wrong.   

A simple example
The biggest problem of all is generating these rules for a large project.  The rest of this article uses the following contrived example Makefile as a starting point:

.PHONY: all
all: foo.o bar.o baz.o

foo.o: foo.c foo.h common.h header.h
bar.o: bar.c bar.h common.h header.h ba.h
baz.o: baz.c baz.h common.h header.h ba.h

Three object files (foo.o, bar.o and baz.o) are built from corresponding C files (foo.c, bar.c and baz.c).  Each .o file has dependencies on various different header files as expressed in the Makefile.   The Makefile uses GNU Make's built-in rules to perform compilation using the system's compiler.

There's no mention here of the final executable being built since this article concentrates on dealing with dependencies between sources and objects, and relationships between objects are usually easier to maintain by hand as there are fewer of them and the relationships are part of the product design.

makedepend and make depend
Since maintaining any real Makefile by hand is impossible many projects use the widely available makedepend program.  makedepend reads C and C++ files looking at the #include statements, follows the #includes and builds the dependency lines for you.   A basic way of incorporating makedepend in a project is a special depend target:

.PHONY: all
all: foo.o bar.o baz.o

SRCS = foo.c bar.c baz.c

DEPENDS = dependencies.d
.PHONY: depend
depend:
@makedepend -f - $(SRCS) > $(DEPENDS)

-include $(DEPENDS)

Doing make depend with this Makefile causes the depend rule to execute, which runs makedepend on the sources (defined in the SRCS variable) and outputs the dependency lines to dependencies.d (defined by DEPENDS variable).

The Makefile includes the dependencies lines in its final line.   dependencies.d would look like this:

# DO NOT DELETE

foo.o: foo.h header.h common.h
bar.o: bar.h header.h common.h ba.h
baz.o: baz.h header.h common.h ba.h

You'll notice that makedepend doesn't try to define the relationship between an object file (e.g. foo.o) and the source file it is made from (foo.c); in this case GNU Make's standard rules will find the related .c file automatically.

Automating makedepend and removing make depend
There are two problems with the make depend style: running make depend can be slow, as every source file has to be searched even if there are no changes, and it's a manual step: before every make the user will have to do make depend to ensure that the dependencies are correct.  The answer to these problems is automation.

Here's a version of the Makefile that still uses makedepend to generate dependencies but automates the process and only runs makedepend for sources that have changed:

.PHONY: all
all: foo.o bar.o baz.o

SRCS = foo.c bar.c baz.c

%.d : %.c
@makedepend -f - $< | sed 's,\($*\.o\)[ :]*,\1 $@ : ,g' > $@

-include $(SRCS:.c=.d)

It works by associating a .d file with each .c: for example, foo.o has a foo.d file that just contains the dependency line for foo.o, here are foo.d's contents:

# DO NOT DELETE

foo.o foo.d : foo.h header.h common.h

You'll notice one addition: this line specifies when to rebuild foo.o, but also that foo.d should be rebuilt under the same conditions---if any of the sources associated with foo.o change then foo.d gets rebuilt.  foo.c isn't mentioned in this list because it's mentioned as part of the pattern rule for rebuilding a .d file (the %.d : %.c means that foo.d will get rebuilt if foo.c itself changes).  foo.d was added to the dependency line created by makedepend using the sed magic above.

The final line of the Makefile includes all the .d files: the $(SRCS:.c=.d) macro transforms the list of sources in the SRCS variable by changing the extension from .c to .d. The include also tells GNU Make to check to see if the .d files need rebuilding.   

GNU Make will look to see if there are rules to rebuild included Makefiles (in this case the .d files), rebuild them if necessary (following the dependencies specified in the Makefile) and then restart.   This 'Makefile remaking' (http://www.gnu.org/software/make/manual/html_node/Remaking-Makefiles.html) feature means that simply typing make will do the right thing: it'll rebuild any dependency files that need rebuilding, but only if the sources have changed, and then GNU Make will perform the build taking into account the new dependencies.

Making deleted files disappear from dependencies with $(wildcard)
Unforunately, this Makefile breaks with a fatal error if a header file is removed.  If header.h is no longer needed and all references to it are removed from the .c files and the file is removed from disk, the following error occurs when make is run:

No rule to make target `header.h', needed by `foo.d'. 

That happens because header.h is still mentioned in foo.d as being a prerequisite of foo.d and hence foo.d cannot be rebuilt.  This catch-22 can be fixed by making the generation of foo.d smarter.  The new foo.d includes the dependencies for foo.o and for foo.d separately. foo.d's dependencies are wrapped in a call to GNU Make's $(wildcard) function.  Here's the new foo.d:

# DO NOT DELETE

foo.d : $(wildcard foo.h header.h common.h)
foo.o : foo.h header.h common.h

And here's the updated Makefile with a new invocation of makedepend followed by a sed line that creates the modified .d file.

.PHONY: all
all: foo.o bar.o baz.o

SRCS = foo.c bar.c baz.c

%.d : %.c
@makedepend -f - $< | sed 's,\($*\.o\)[ :]*\(.*\),$@ : $$\(wildcard \2\)\n\1 : \2,g' > $@

-include $(SRCS:.c=.d)

Now removing a header file doesn't break the Make: when foo.d is parsed the dependency line for foo.d is passed through $(wildcard). When there are no globbing symbols like * or ? in the filename, $(wildcard) ask as a simple existence filter, removing those files that don't exist from the list.   So, if header.h had been removed the first line of foo.d would be equivalent to:

foo.d : foo.h common.h

and the Make would work correctly.  This example Makefile now works when .c files are added (the user just updates SRCS and the new .d file is created automatically), when .c files are removed (the user updates SRCS and the old .d file is ignored), when headers are added (since that requires altering an existing .c or .h the .d file will be regenerated) and when they are removed (the $(wildcard) hides the deletion and the .d file is regenerated).

An optimized version
An optimization is to remove the need for GNU Make to restart, by merging the rule that makes the .d file into the rule that makes the .o.   Since the .d file is updated if and only if the .o file needs to be updated (both are updated when any of the sources for the .o change) it's possible to have the makedepend occur at the same time as the compilation:

.PHONY: all
all: foo.o bar.o baz.o

SRCS = foo.c bar.c baz.c

%.o : %.c
@makedepend -f - $< | sed 's,\($*\.o\)[ :]*\(.*\),$@ : $$\(wildcard \2\)\n\1 : \2,g' > $*.d
@$(COMPILE.c) -o $@ $<

-include $(SRCS:.c=.d)

This rule makes use of another GNU Make variable: $*. $* is the part of the pattern %.c which matches the %; if this rule is building foo.o from foo.c then $* is just foo.  $* is used to create the name of the .d file that makedepend writes to.

This final version does not use GNU Make's 'Makefile remaking' system.  There are no rules for making .d files (they are made as a side effect of making the .o) and hence GNU Make doesn't have to restart.  This provides the best combination of accuracy and speed possible.   

In general it's bad idea to have a rule that makes multiple files because it's impossible for GNU Make to find the rule that makes a file if it's created as a side effect of something else.  In this case that behavior is desired: we want to hide the creation of .d files from GNU Make so that it doesn't try to make them and then have to restart.

(A similar idea was proposed by Tom Tromey (without the $(wildcard) trick) and more information about building dependency files can be found on the GNU Make maintainer Paul Smith's web site at http://make.paulandlesley.org/autodep.html; another good resource for any GNU Make developer---once you've purchased the FSF's GNU Make manual---is Robert Mecklenburg's Managing Projects with GNU Make from O'Reilly)

Doing away with makedepend
Finally, it's possible to omit makedepend altogether if you are using the GNU gcc compiler.  It has a -MD option that does the work of makedepend at the same time as the compilation:

.PHONY: all
all: foo.o bar.o baz.o

SRCS = foo.c bar.c baz.c

%.o : %.c
@$(COMPILE.c) -MD -o $@ $<
@sed -i 's,\($*\.o\)[ :]*\(.*\),$@ : $$\(wildcard \2\)\n\1 : \2,g' $*.d

-include $(SRCS:.c=.d)

For example, the compilation step for foo.o will create foo.d from foo.c and then sed is run on the foo.d to add the extra line for foo.d containing the $(wildcard).

About the author

AgileConnection is a TechWell community.

Through conferences, training, consulting, and online resources, TechWell helps you develop and deliver great software every day.