Makefile · Part I · Module 4 · Reusable Rules and Debugging
Debugging Makefiles with make -n and --debug
A Makefile that does the wrong thing rarely produces an error. It rebuilds something it should have skipped, skips something it should have rebuilt, or runs a command with a word missing. Guessing at the cause by reading the file is slow and often wrong, because the thing you need to know is what Make decided — and Make will tell you if you ask. This chapter is the four questions worth asking and the flag that answers each one.
Intermediate22 min readMakefileGNU MakePart IDebuggingDiagnostics
Part I · Module 4 · Chapter 4.4 · Debugging Makefiles with make -n and --debug
1. When the Makefile Looks Right and the Build Is Wrong
By now you have met most of the ways a Makefile can misbehave quietly. A prerequisite nobody declared, so an output stays stale. A variable name with a typo, so a compile runs with no files. A pattern rule that does not apply. A pipeline that swallows a failure.
What those have in common is that nothing in the output tells you which one it is. Reading the Makefile again is the obvious response and the least effective one, because the file is not where the answer is — the answer is in what Make decided while reading it.
So the useful habit is to ask. Four questions cover almost everything:
- What will actually run?
- What did that variable really hold?
- Why did this rebuild?
- Why did this not rebuild?
Each has a direct answer and none of them requires changing the Makefile.
Every example below uses this small Makefile, with rtl/alu.v and rtl/fifo.v present:
BUILD := build
SOURCES := $(sort $(wildcard rtl/*.v))
build/sim.img: $(SOURCES) | $(BUILD)
@mkdir -p $(@D)
@cat $^ > $@
$(BUILD):
@mkdir -p $(BUILD)2. What Will Run: make -n
Chapter 1.4 introduced make -n as a way to look without touching. It is the first thing to reach for when a recipe does something unexpected, because it prints the command after every variable has been expanded:
make -n build/sim.imgmkdir -p build
cat rtl/alu.v rtl/fifo.v > build/sim.imgTwo short recipe lines became two fully written-out commands. If $(SOURCES) were empty, this output would read cat > build/sim.img and the bug would be visible immediately — which is faster than reasoning about why the wildcard did not match.
Recall the two limits from Chapter 1.4: -n prints @-suppressed lines too, and a recipe line prefixed with + executes anyway. It shows intent, not a sandbox.
3. What a Variable Holds: a print-% Rule
The quickest way to inspect a value is to ask the Makefile for it. Add one pattern rule — the technique from Chapter 4.1, using the stem from Chapter 3.3:
print-%:
@echo "$* = [$($*)]"Then any variable is one command away:
make print-SOURCES
make print-BUILD
make print-CURDIRSOURCES = [rtl/alu.v rtl/fifo.v]
BUILD = [build]
CURDIR = [/work/chip/make-intro]Read how it works, because it is a nice piece of synthesis. print-SOURCES matches print-%, so the stem $* is the text SOURCES. Then $($*) expands to $(SOURCES) — a variable whose name came from the stem. And the square brackets make an empty value or a stray trailing space visible, which was Chapter 3.1's advice in a reusable form.
This works on built-in variables too, as print-CURDIR shows. It is worth keeping permanently in any Makefile you maintain.
4. Catching the Silent Typo: --warn-undefined-variables
Chapter 3.1 warned that a misspelled variable expands to nothing with no complaint, and deferred the fix here. This is it:
SOURCES := rtl/alu.v
show:
@echo "typo: [$(SOURCE)] correct: [$(SOURCES)]"make --warn-undefined-variables showMakefile:4: warning: undefined variable `SOURCE'
typo: [] correct: [rtl/alu.v]A warning with the file and line number. That turns a silent empty expansion into something you can find.
Two practical notes. It is a flag rather than something you put in the Makefile, so it costs nothing to try on a build that is behaving oddly. And on a large Makefile it can be noisy — some variables are legitimately undefined and tested with ifdef — so read it as a list of candidates rather than a list of bugs.
5. Why Did This Rebuild: --debug=b
This is the most valuable flag in the chapter, and the one most people never learn. --debug=b ("basic") makes Make explain its rebuild decisions.
The flag prints a five-line version banner first; it is trimmed from the blocks below.
Case one: the target does not exist.
make --debug=b build/sim.imgReading makefiles...
Updating goal targets....
File `build/sim.img' does not exist.
File `build' does not exist.
Must remake target `build'.
Successfully remade target file `build'.
Must remake target `build/sim.img'.Make states the reason in plain words — does not exist — and you can see it recursing into the order-only prerequisite build first, exactly as Chapter 4.2 described.
Case two: a prerequisite changed. Touch a source and ask again:
touch rtl/alu.v
make --debug=b build/sim.imgReading makefiles...
Updating goal targets....
Prerequisite `rtl/alu.v' is newer than target `build/sim.img'.
Must remake target `build/sim.img'.
Successfully remade target file `build/sim.img'.Prerequisite 'rtl/alu.v' is newer than target 'build/sim.img'. That one line answers the question directly, and it names which file — which is what you need when a rule has forty prerequisites and one of them keeps triggering rebuilds.
This is the flag for the over-declaration problem from Chapter 2.3: if a compile rebuilds whenever you edit documentation, --debug=b names the documentation file and the fix is obvious.
6. Why Did This Not Rebuild
The same flag answers the opposite question. Run it again with nothing changed:
make --debug=b build/sim.imgReading makefiles...
Updating goal targets....
make: `build/sim.img' is up to date.Make considered the target and found no reason to act — no "does not exist", no "is newer than". That is the diagnosis: every declared prerequisite is older than the target.
So when something should have rebuilt and did not, and --debug=b says up to date, the fault is almost never in the recipe. It is one of two things:
- The file you changed is not a declared prerequisite — the under-declaration bug from Chapter 2.3. Confirm with
make print-SOURCESor by reading the rule; the file simply is not in the list. - The file you changed is not actually newer — a restored copy, a checkout that preserved timestamps, or the one-second granularity Chapter 2.3 measured.
Those two have different fixes, and --debug=b plus one print- is enough to tell them apart in under a minute.
There is also a -d flag, which is --debug=a — everything. On this nine-line Makefile it produces hundreds of lines about implicit-rule searches. It is occasionally the right tool and almost never the one to start with; --debug=b is the readable form.
7. The Whole Database: make -p
make -p dumps everything Make knows: every variable, every rule, every built-in. It is thorough to the point of being unusable raw:
make -p | wc -l13131,313 lines from a nine-line Makefile, because most of it is Make's built-in rules and variables — the ones Chapter 4.1 showed can fire without you writing them.
So -p is a thing you grep, not a thing you read:
make -p | grep -A1 "^SOURCES"SOURCES := rtl/alu.v rtl/fifo.v
# environmentIt is the right tool for two specific questions a print- rule cannot answer: where did this value come from (the comment line after each variable says environment, command line, makefile or default), and what built-in rule might be matching my target. For everyday "what does this hold", the print-% rule from §3 is faster.
8. What If This File Changed: make -W
Sometimes you want to know what a change would cause without making it. make -W file tells Make to pretend that file was just modified:
make build/sim.img # nothing to do
make -n -W rtl/alu.v build/sim.imgmake: `build/sim.img' is up to date.
mkdir -p build
cat rtl/alu.v rtl/fifo.v > build/sim.imgNothing was touched and nothing was built — the second command listed the work that would happen if rtl/alu.v changed.
Pairing -W with -n is the useful combination, because it answers "is this file actually wired into the build?" The two answers look different:
make -n -W rtl/alu.v build/sim.img # a real prerequisite
make -n -W rtl/notused.vh build/sim.img # not a prerequisitemkdir -p build
cat rtl/alu.v rtl/fifo.v > build/sim.img
make: `build/sim.img' is up to date.Pretending a declared prerequisite changed produces the work that would follow. Pretending an undeclared file changed produces is up to date, because the file is not in the comparison at all. That second answer is the direct test for the under-declaration bug.
9. Failing Early and Loudly: $(info), $(warning), $(error)
The flags above are things you run. These three are things you put in the Makefile, and they are how a flow reports its own problems instead of producing a confusing failure later.
SOURCES := rtl/alu.v
$(info [info] SOURCES is $(SOURCES))
$(warning [warning] this one names the file and line)
check:
@echo " recipe ran"make check[info] SOURCES is rtl/alu.v
Makefile:3: [warning] this one names the file and line
recipe ran$(info text)prints the text plainly. Use it for a temporary probe, or for a permanent one-line summary of the configuration a run is using.$(warning text)prints withfile:line, which is what you want when the message needs to be traced back.$(error text)prints and stops:
SOURCES :=
ifeq ($(SOURCES),)
$(error no sources found - refusing to continue)
endif
check:
@echo nopemake check
echo "exit: $?"Makefile:3: *** no sources found - refusing to continue. Stop.
exit: 2That is the pattern worth adopting. An empty source list is a configuration mistake, and this turns it into an immediate, readable failure instead of a compile of zero files that succeeds — the silent case Chapter 3.4 warned about. Note that all three run while the Makefile is being read, so they fire before any recipe, and a $(error) inside a conditional is the normal way to validate a configuration.
10. A Debugging Session, Start to Finish
Here is the method applied to a real symptom: "I edited a source and make said there was nothing to do."
Step 1 — ask Make what it decided.
make --debug=b build/sim.imgmake: `build/sim.img' is up to date.No "is newer than" line, so Make compared the declared prerequisites and found none newer. The recipe is not the problem.
Step 2 — ask what the prerequisite list actually contains.
make print-SOURCESSOURCES = [rtl/alu.v rtl/fifo.v]The file you edited — say rtl/include/defines.vh — is not in that list. That is the whole bug: an input the recipe reads that no rule declares, which is Chapter 2.3's under-declaration.
Step 3 — confirm it is not wired in.
make -n -W rtl/include/defines.vh build/sim.imgNothing printed, which confirms the file is not a prerequisite of the goal.
Step 4 — fix the rule, then verify the fix the same way. After adding the file to the prerequisites:
touch rtl/include/defines.vh
make --debug=b build/sim.img Prerequisite `rtl/include/defines.vh' is newer than target `build/sim.img'.
Must remake target `build/sim.img'.Four commands, no guessing, and the last one is evidence rather than hope. Verifying a Makefile fix by re-running the diagnostic is the habit to take from this chapter — a build that now happens to work is not the same as a build you have understood.
11. Common Mistakes
Reading the Makefile instead of asking Make. The file does not contain the decision; --debug=b does.
Starting with -d. It is --debug=a and buries the answer in implicit-rule searches. Start with --debug=b.
Reading make -p output directly. It is over a thousand lines, most of it built-ins. Grep it, or use a print-% rule (§3, §7).
Treating make -n as proof that nothing can happen. It prints @ lines and still executes + lines (Chapter 1.4).
Adding echo statements to a recipe to inspect a variable. That changes the Makefile and only works where a recipe already runs. A print-% rule works everywhere and stays useful.
Ignoring --warn-undefined-variables because it is noisy. It is a list of candidates. One of them is often the bug that has been wasting your afternoon.
Letting an empty configuration proceed. A $(error) inside an ifeq turns a silently wrong build into an immediate, readable one (§9).
Fixing a symptom and not re-checking. Re-run the same diagnostic afterwards; it is the only way to know the fix addressed the cause.
12. Check Your Understanding
Question 1. A compile rebuilds every time you edit the project's README.md. Name the flag you would run and the exact line you expect it to print.
Question 2. You edit a header that a source includes, and make reports nothing to do. --debug=b prints only is up to date. What are the two possible causes, and which single command distinguishes them?
Question 3. Write the two-line print-% rule from §3 and explain what $($*) does when you run make print-BUILD.
Question 4. A flow compiles zero files and exits successfully because a wildcard matched nothing. Give the four lines you would add to the Makefile so this fails immediately with a readable message.
13. Exercise
Create rtl/alu.v and rtl/fifo.v with one module each, plus rtl/include/defines.vh with one define, and this Makefile — which has the under-declaration bug and a variable typo.
BUILD := build
SOURCES := $(sort $(wildcard rtl/*.v))
all: $(BUILD)/sim.img
$(BUILD)/sim.img: $(SOURCE) | $(BUILD)
@echo " [compile] -> $@"
@cat rtl/include/defines.vh $(SOURCES) > $@
$(BUILD):
@mkdir -p $(BUILD)Work it out using only the tools from this chapter — do not reason from the source.
- Run
make, thenmakeagain. What happens on the second run, and what does--debug=bsay? - Find the typo without reading the Makefile.
- Find the undeclared input.
- Give the corrected Makefile and the command that proves the fix.
14. Remember
- The decision you need is in Make, not in the file. Ask it rather than re-reading the Makefile.
make -nshows the commands after expansion, which exposes an empty variable instantly. It is not a sandbox.- A
print-%rule is worth keeping permanently.$($*)turns the stem into a variable name, so any value is one command away — and the brackets expose empties and stray spaces. --warn-undefined-variablesnames the file and line of a misspelled variable, which is the only cheap defence against a silent empty expansion.--debug=bis the most useful flag. It saysdoes not exist, or names which prerequisiteis newer thanthe target, or simply reportsup to date.up to datewhen you expected a rebuild means one of two things: the file you changed is not a declared prerequisite, or it is not actually newer.make -pdumps everything — over 1,300 lines on a nine-line Makefile — so grep it, and use it to find where a value came from.make -n -W filetests whether a file is wired into the build without modifying anything: a declared prerequisite lists the work that would follow, an undeclared one reportsup to date.$(info),$(warning)and$(error)run while the file is read. An$(error)inside a conditional turns a bad configuration into an immediate, readable failure.- Verify a fix with the same diagnostic that found the bug. A build that happens to work is not a build you understand.
That completes Part I. Starting from a repeated command sequence, you can now write a Makefile that expresses a real dependency graph, declares its actions honestly, parameterises itself for whoever runs it, discovers its own sources, serves a whole class of files with one rule, survives several configurations, tells the truth about failure, and explains itself when something is wrong.
Part II applies all of it to semiconductor work. It begins where a real flow begins — Module 5 takes RTL simulation from filelists and include paths through compile, elaborate and run targets to the waveform, log and clean targets an engineer uses every day — and the honest filelist dependency problem that Chapter 1.2 raised and deferred is finally solved in Chapter 5.2.
Where this fits
Part of the Makefile curriculum.
