How to diagnose Makefile problems using Make's built-in debugging tools: make -n, make -p, make --trace, and the $(info)/$(warning)/$(error) functions.
make -nPrints every command that Make would run, without actually executing them. This is your first tool when a build does something unexpected — you can see exactly what commands Make plans to invoke, with all variables expanded.
g++ -std=c++20 -Wall -Wextra -Iinclude -MMD -MP -c -o build/src/logger.o src/logger.cpp
g++ -std=c++20 -Wall -Wextra -Iinclude -MMD -MP -c -o build/src/main.o src/main.cpp
g++ -o build/app build/src/logger.o build/src/main.o
If a variable is wrong or a flag is missing, you'll see it here before wasting time on a broken build.
make -p | lessPrints Make's entire internal database: all variables (both yours and built-in), all rules (explicit, implicit, and pattern), and all dependencies. This is verbose but comprehensive.
Useful patterns:
make -p | grep -A2 'CXXFLAGS' # find your flags
make -p | grep '^[a-z].*:' # list all targets
make -p | grep -B1 'is up to date' # see why something didn't rebuildmake --traceFor each target that Make rebuilds, prints why it was rebuilt and what recipe it ran. Less verbose than make -d but more informative than default output.
make -d 2>&1 | head -100Prints Make's complete decision-making process for every target: which rules it considered, which prerequisites it checked, and why it decided to rebuild (or not). Extremely verbose — pipe to head or less.
These run during Makefile parsing, before any recipes execute:
$(info [debug] SRCS = $(SRCS))Prints the text to stdout with no prefix. No file/line information. Useful for quick "printf debugging" of variable values.
$(warning SRCS is $(SRCS) — is this right?)Prints with the filename and line number where it appears. Useful when you have multiple included Makefiles and need to know which one is setting a value.
Output: Makefile:28: SRCS is src/logger.cpp src/main.cpp — is this right?
ifeq ($(SRCS),)
$(error No source files found in src/)
endifPrints the message and stops Make immediately. Use this for guardrails — catching configuration errors early with a clear message instead of a cryptic build failure.
make --trace # shows what triggered (or didn't trigger) a rebuild
touch src/main.cpp
make --trace # now shows main.cpp is newer → rebuildsAdd temporarily to your Makefile:
$(info [debug] OBJS = $(OBJS))Or use the debug target in this lesson's Makefile: make debug
make -p | grep -A5 'build/src/main.o'Shows the rule, prerequisites, and recipe for that target.
make -d 2>&1 | grep -i 'up to date\|newer\|must remake'Shows Make's timestamp comparison decisions.
This Makefile includes a debug target that prints all resolved variables in a readable format — a quick alternative to make -p when you just want to see your own variables.
cd 20_debugging_makefiles
make # build normally
make -n clean all # dry run: see clean + rebuild commands
make debug # print resolved variables
make --trace # trace what gets rebuilt (nothing — it's up to date)
touch src/logger.cpp
make --trace # now logger.o and app rebuild
make clean| File | Purpose |
|---|---|
include/logger.hpp |
Declares log_info(), log_warn(), log_error() |
src/logger.cpp |
Implements logging functions |
src/main.cpp |
Demo: logs a few messages |
Makefile |
Demonstrates $(info), $(warning), $(error), debug target |