Hand-maintained header lists are always wrong eventually; generated dependencies keep incremental builds correct.
An object file depends on its source and every header it includes, transitively. Listing headers by hand doesn't scale and quietly goes stale. Compilers can write the list for you while compiling:
-MMDwrites a.dfile next to the object (for examplebuild/main.d) containing a make rulebuild/main.o: src/main.c src/util.h ..., excluding system headers.-MPadds an empty phony rule for each header, so deleting or renaming a header doesn't break the build with "No rule to make target".
Then include the generated files at the end of the Makefile:
-include $(OBJ:.o=.d)The leading - ignores missing files: on the first build there are no .d files yet, and that's fine, because every object has to be compiled anyway. After that, each compile refreshes its .d file, so dependencies stay correct as includes change.
This works with GCC and Clang (-MMD -MP); MSVC has /showIncludes instead, one reason cross-platform C++ projects tend to use CMake. The same idea applies elsewhere: many tools can emit a dependency list (sass --watch, go list -deps) that a Makefile can include.
Add CPPFLAGS += -MMD -MP so the flags are part of the standard compile rule.
Example
CC := cc
CPPFLAGS := -MMD -MP -Isrc
CFLAGS := -Wall -O2
SRC := $(wildcard src/*.c)
OBJ := $(SRC:src/%.c=build/%.o)
build/app: $(OBJ)
$(CC) -o $@ $^
build/%.o: src/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
-include $(OBJ:.o=.d)
# build/main.d, written by the compiler:
# build/main.o: src/main.c src/util.h
# src/util.h:
# $ touch src/util.h && make
# recompiles main.o and util.o (both include util.h), then relinksRemember
- -MMD -MP make the compiler write .d dependency files
- -include $(OBJ:.o=.d) pulls them in and ignores missing ones
- Headers are then tracked automatically and transitively
Watch out for
- Omitting -MP, so deleting a header breaks the build until the .d files are removed.