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
'$(@:.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
vector.h, the generated dependancy file would look like:
test.o: test.c test.h vector.h
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
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
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
clean when you call
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
clean && make every time you build.