Skip to content
VLSI Mentor

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:

  1. What will actually run?
  2. What did that variable really hold?
  3. Why did this rebuild?
  4. Why did this not rebuild?

Each has a direct answer and none of them requires changing the Makefile.

Four rows, each flowing left to right from a symptom to the tool that explains it. Row one: it rebuilt when it should not have, answered by make dash dash debug equals b, which names the newer prerequisite. Row two: it did not rebuild when it should have, answered by the same flag, which reports the target as up to date. Row three: the wrong command ran or no command ran, answered by make dash n, which prints the plan. Row four: a value looks empty or unexpected, answered by a print rule or the database dump.It rebuiltand should not have--debug=bnames the newer fileIt did not rebuildand should have--debug=bsays up to dateWrong commandor none at allmake -nprints the planA value is emptyor unexpectedprint-NAMEprints the value12
Figure 1 — the symptom you see, and the question to ask about it. Each row is a complete diagnosis path: there is no need to read the Makefile first, because every one of these answers comes from asking Make what it decided. The two rebuild questions share a flag because they are the same question asked in opposite directions.

Every example below uses this small Makefile, with rtl/alu.v and rtl/fifo.v present:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make -n build/sim.img
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
mkdir -p build
cat rtl/alu.v rtl/fifo.v > build/sim.img

Two 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — add this once and keep it
print-%:
	@echo "$* = [$($*)]"

Then any variable is one command away:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make print-SOURCES
make print-BUILD
make print-CURDIR
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output — the path will be yours
SOURCES = [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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — SOURCE is a typo for SOURCES
SOURCES := rtl/alu.v

show:
	@echo "typo: [$(SOURCE)]  correct: [$(SOURCES)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make --warn-undefined-variables show
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
Makefile: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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make --debug=b build/sim.img
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output, banner trimmed
Reading 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
touch rtl/alu.v
make --debug=b build/sim.img
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output, banner trimmed
Reading 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make --debug=b build/sim.img
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output, banner trimmed
Reading 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-SOURCES or 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make -p | wc -l
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
1313

1,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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make -p | grep -A1 "^SOURCES"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
SOURCES := rtl/alu.v rtl/fifo.v
# environment

It 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make build/sim.img            # nothing to do
make -n -W rtl/alu.v build/sim.img
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
make: `build/sim.img' is up to date.
mkdir -p build
cat rtl/alu.v rtl/fifo.v > build/sim.img

Nothing 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make -n -W rtl/alu.v       build/sim.img    # a real prerequisite
make -n -W rtl/notused.vh  build/sim.img    # not a prerequisite
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
mkdir -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.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
SOURCES := rtl/alu.v
$(info [info]    SOURCES is $(SOURCES))
$(warning [warning] this one names the file and line)

check:
	@echo "  recipe ran"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make check
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
[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 with file:line, which is what you want when the message needs to be traced back.
  • $(error text) prints and stops:
Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
SOURCES :=

ifeq ($(SOURCES),)
$(error no sources found - refusing to continue)
endif

check:
	@echo nope
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make check
echo "exit: $?"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
Makefile:3: *** no sources found - refusing to continue.  Stop.
exit: 2

That 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make --debug=b build/sim.img
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output, banner trimmed
make: `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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make print-SOURCES
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
SOURCES = [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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make -n -W rtl/include/defines.vh build/sim.img

Nothing 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
touch rtl/include/defines.vh
make --debug=b build/sim.img
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output, banner trimmed
 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — as given
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.

  1. Run make, then make again. What happens on the second run, and what does --debug=b say?
  2. Find the typo without reading the Makefile.
  3. Find the undeclared input.
  4. 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 -n shows 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-variables names the file and line of a misspelled variable, which is the only cheap defence against a silent empty expansion.
  • --debug=b is the most useful flag. It says does not exist, or names which prerequisite is newer than the target, or simply reports up to date.
  • up to date when 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 -p dumps 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 file tests whether a file is wired into the build without modifying anything: a declared prerequisite lists the work that would follow, an undeclared one reports up 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.