Makefile header dependencies

This article will describe Makefile header dependencies. Makefile header dependencies provides correct incremental build.

1 Necessity of Makefile header dependencies

Makefile does not check which header is included in source code. When header file is updated, source code which include the header file will not be built. For avoiding this, it needs to clean all object file and compile it again. Compile all object file will take a lot of time, especially in case of C++.

For example, please see the following case.

$ tree
.
├── Makefile
├── include
│   └── sample.h
├── sample
└── sample.c

1 directory, 4 files

Source code sample.c include header file sample.h.

$ cat sample.c
#include <stdio.h>
#include <sample.h>

int main(void)
{
  return 0;
}

The include path is set to CFLAGS. String of "sample" is set to PROG variable.

$ cat Makefile
CFLAGS := -I. -I./include
SRC := $(wildcard *.c)
PROG := $(patsubst %.c,%,$(SRC))

all: $(PROG)

clean:
        @$(RM) $(PROG)

When $(PROG) is set to dependencies of all target, Makefile will use implicit rules for building object file of $(PROG). Running make command will call cc command with CFLAGS. Running make command again will not call cc command because object file of sample is already exists.

$ make
cc -I. -I./include    sample.c   -o sample
$ make: Nothing to be done for `all'.

The following target's entry is equal with this implicit rules.

%:%.c
        $(CC) $(CFLAGS) $< -o $@

When updating timestamp of sample.c with touch command, make will compare timestamp of sample and sample.c, and create new sample.

$ touch sample.c
$ make
cc -I. -I./include    sample.c   -o sample

When updateing timestamp of sample.h with touch command, make does not create new sample though sample.h is newer than sample. Because sample.c includes sample.h, updating sample.h means updating sample.c, so new sample should be created.

$ touch include/sample.h
$ make
make: Nothing to be done for `all'.

2 $(CC) -MM option

-M option and -MM option outputs included header files recursively.

-M option outputs include header which are in default compiler search path.

$ cc -M -I./include sample.c | head
sample.o: sample.c /usr/include/stdc-predef.h /usr/include/stdio.h \
 /usr/include/features.h /usr/include/sys/cdefs.h \
 /usr/include/bits/wordsize.h /usr/include/gnu/stubs.h \
<snip>

-MM option does not output include header which are in default -compiler search path. -MM option is useful for Makefile header dependencies.

$ cc -MM -I./include sample.c
sample.o: sample.c include/sample.h

3 %.d file

Automatic creation of %.d file is recommended.

  • Output "sample.o: sample.c include/sample.h" with $(CC) -MM option.
  • Add sample.d to target with sed command. When updating sample.c or sample.h, this will update sample.d.

Makefile is as below. The all target depends %.d and call $(PROG) target. For creating $(PROG) file, the % target will be called and implicit rules will be used.

CFLAGS := -I. -I./include
SRC := $(wildcard *.c)
OBJ := $(patsubst %.c,%.o,$(SRC))
DEP := $(patsubst %.c,%.d,$(SRC))
PROG := $(patsubst %.c,%,$(SRC))

all: $(DEP)
        @$(MAKE) $(PROG)

clean:
        @$(RM) $(DEP) $(OBJ) $(PROG)

ifneq ($(filter clean,$(MAKECMDGOALS)),clean)
-include $(DEP)
endif

%.d: %.c
        $(info GEN $@)
        @$(CC) -MM $(CFLAGS) $< | sed 's/\($*\)\.o[ :]*/\1.o $@ : /g' > $@

%: %.d

When updating sample.h, new sample will be created.

$ make
GEN sample.d
cc -I. -I./include   -c -o sample.o sample.c
cc   sample.o   -o sample
$ make
make[1]: `sample' is up to date.
$ touch sample.c
$ make
GEN sample.d
cc -I. -I./include   -c -o sample.o sample.c
cc   sample.o   -o sample
$ touch include/sample.h
$ make
GEN sample.d
cc -I. -I./include   -c -o sample.o sample.c
cc   sample.o   -o sample