4 minutes read 30th November, 2014
programming

Makefiles aren’t the easiest thing to get right, and a common problem is getting target dependencies wrong. This makes calling make, fail to correctly recompile everything that needs it. That’s the point where I start getting angry because the hash define I just changed hasn’t actually changed.

This problem appears commonly in makefiles that have wildcard rules where you are not listing the dependencies by hand, such as the one below.

The rest of this article assumes knowledge of the basics of Makefiles.

flags := -std=c99 -O3 -Wall
incs  :=
libs  :=

src := $(wildcard *.c)
obj := $(src:.c=.o)

hello-world: $(obj)
    gcc $(flags) $(obj) -o hello-world $(libs)

%.o: %.c
    gcc $(flags) $(incs) -c $< -o $@

This happily compiles a collection of C files in the current directory into an executable. As a project grows more C files are normally added, and this Makefile doesn’t need to be changed to include them. It just grabs all the C files in the current directory, compiles then one by one and then links everything together. The pattern rule at the end of the makefile is used for compiling individual C files, but it only lists the C file it’s built from as a dependency.

Issues arise when using header files. As the header files are not listed as dependencies, modifications only made in a header file can fail to cause all the dependant C files to be recompiled.

How do we fix this? One solution would be to manually include the header files used by each C file in it’s dependency however then we are back to writing individual rules for C files which is a pain. What we want is for the dependencies of each C file to be generated automatically.

Fortunately there is a way to do this using GCC. GCC provides several flags from its preprocessor options1 for generating make rules describing the dependencies for a file. We then store these rules for each C file in a corresponding .d file (D for dependency) and then include them into our Makefile. We add the following to our Makefile:

%.d: %.c
    gcc -MM -MT '$(@:.d=.o)' $< > $@

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

So what’s going on here? We use a pattern rule to generate a D file for a given C file. We tell GCC we want to create a makefile dependency rule with -MM and that the rule is for the corresponding object file with -MT '$(@:.d=.o)'. This invocation of GCC returns to the standard output so we redirect it to the D file. The fourth line includes all the dependancy files into the makefile.

So what do the dependancy files actually contain? They contain valid makefile targets. If you had a C file test.c which includes the headers test.h and vector.h, the generated dependancy file would look like:

test.o: test.c test.h vector.h

Dependancy file ‘test.d’

When test.o is built, make combines the dependancies from that rule, with the ones from the pattern rule.

Guarding the dependancy include

Almost every makefile I’ve seen has extra targets such as clean or install. These targets do extra things outside of just building an executable, such as clean which normally removes all intermediary files. Some rules like these do not require the dependancy files and the -include will trigger all the dependacy files to be regenerated if they do not exist. This can make calling clean on a relatively clean directory take a long time as make finds all the dependancies for files. The -include statement should be guarded as such:

ifneq ($(MAKECMDGOALS),clean,install)
-include $(src:.c=.d)
endif

Targets that do not require dependancy files are listed comma separated after $(MAKECMDGOALS), which is a variable holding the passed target to make such as clean when you call make clean.

Putting it all together

So what does our makefile look like with dependancy handling?

flags := -std=c99 -O3 -Wall
incs  :=
libs  :=

src := $(wildcard *.c)
obj := $(src:.c=.o)

# The main 'Hello World' program target
hello-world: $(obj)
    gcc $(flags) $(obj) -o hello-world $(libs)

# Dependancy file generation
%.d: %.c
    gcc -MM -MT '$(@:.d=.o)' $< > $@

# Include dependancies for C files
ifneq ($(MAKECMDGOALS),clean)
-include $(src:.c=.d)
endif

# Compile C source files to object files
%.o: %.c
    gcc $(flags) $(incs) -c $< -o $@

clean:
    rm -rf hello-world *.o *.d

I’ve included an example clean target which also removes all dependancy files. It doesn’t matter where you place the dependancy generation in the makefile.


Why don’t we just depend on all the headers?!

Basically why don’t we just include this somewhere and depend on it somewhere else?:

headers := $(wildcard *.h)

This would be simpler than faffing around with GCC and weird makefile includes but the problem is what depends on this? Probably you would just make every C file depend on all the headers and while this would “work”, you now have to recompile everything when you change any single header.

In that case you might as well just skip any header dependencies and call make clean && make every time you build.