Makefile · Part I · Module 4 · Reusable Rules and Debugging
Pattern Rules and Reusable Recipes
Writing one rule per file works until there are twenty files, and then the Makefile is mostly copies of itself. A pattern rule says 'here is how to build any file of this kind from any file of that kind', and Make applies it wherever it fits. This is also where the automatic variables from the last module stop being a convenience and become the only way the thing can work — a rule that serves many files cannot name any of them.
Intermediate22 min readMakefileGNU MakePart IPattern RulesReuse
Part I · Module 4 · Chapter 4.1 · Pattern Rules and Reusable Recipes
1. Three Rules That Are Really One Rule
Say each RTL source gets a small per-file check, and the result is written next to it. With three modules you write three rules:
.PHONY: all
all: build/alu.lint build/regfile.lint build/fifo.lint
build/alu.lint: rtl/alu.v
@mkdir -p $(@D)
@grep -c module $< > $@
@echo " linted $@"
build/regfile.lint: rtl/regfile.v
@mkdir -p $(@D)
@grep -c module $< > $@
@echo " linted $@"
build/fifo.lint: rtl/fifo.v
@mkdir -p $(@D)
@grep -c module $< > $@
@echo " linted $@"make linted build/alu.lint
linted build/regfile.lint
linted build/fifo.lintIt works. It is also nine lines of recipe where there is only one idea, and every one of the problems that creates is a real one:
- A fix to the check has to be applied three times, and a reviewer has to notice if it was applied twice.
- Adding a module means copying a rule and editing two names in it.
- A real block has twenty or two hundred modules, not three.
The three rules differ only in one word — the module name. A pattern rule is how you write the rule once and let Make supply that word.
2. What a Pattern Rule Is
A pattern rule looks like an ordinary rule with % in the target and in the prerequisites:
build/%.lint: rtl/%.v
recipeRead it as a sentence: "to build any build/-something-.lint, use the matching rtl/-something-.v."
The % matches one or more characters, and — this is the part that makes it work — the same text must match in both places. If Make needs build/alu.lint, the % is alu, so the prerequisite is rtl/alu.v. That matched text has a name: the stem.
This is a different feature from the % inside $(patsubst ...) that Chapter 3.4 covered. That one rewrites text in a list and tells Make nothing. This one teaches Make how to build a whole class of files.
3. One Rule Instead of Three
Here is §1 rewritten. The three rules become one, and the list of things to build comes from patsubst:
.PHONY: all
MODULES := alu regfile fifo
LINTS := $(patsubst %,build/%.lint,$(MODULES))
all: $(LINTS)
build/%.lint: rtl/%.v
@mkdir -p $(@D)
@grep -c module $< > $@
@echo " linted $@ from $< (stem=$*)"make linted build/alu.lint from rtl/alu.v (stem=alu)
linted build/regfile.lint from rtl/regfile.v (stem=regfile)
linted build/fifo.lint from rtl/fifo.v (stem=fifo)One recipe ran three times, with different values each time. Adding a fourth module is now one word in MODULES.
And the incremental behaviour is unchanged — each generated file is still a separate target with its own timestamp comparison:
make # nothing changed
touch rtl/regfile.v
makemake: Nothing to be done for 'all'.
linted build/regfile.lint from rtl/regfile.v (stem=regfile)Only the touched module was re-linted. A pattern rule does not make the build coarser; it only removes the repetition in how you described it.
4. The Stem, and Why the Automatic Variables Are Now Required
A pattern rule cannot contain a filename. There is no filename to write — the rule serves files that do not exist yet and names nobody has chosen. So every reference has to come from Chapter 3.3's automatic variables:
| In the recipe | What it holds, per run |
|---|---|
$@ | the target being built, e.g. build/regfile.lint |
$< | the matched prerequisite, e.g. rtl/regfile.v |
$* | the stem — just regfile |
$(@D) | the target's directory, build |
$* is new here because it only has meaning in a pattern rule: it is the text the % stood for, with the fixed parts stripped off. It is what you use when a recipe needs the bare name rather than a path — a log label, a test name, a -top argument.
In Chapter 3.3 these variables saved you from repeating names. Here they are not optional, and that is the real reason that chapter came first.
5. A Pattern Rule Is a Candidate, Not a Promise
This is the most important thing to understand about pattern rules, and it prevents a confusing error message.
Writing a pattern rule does not mean Make can build everything matching it. Make will use the rule only if the prerequisite it implies actually exists — or can itself be built by another rule.
build/%.lint: rtl/%.v
@mkdir -p $(@D)
@grep -c module $< > $@
@echo " linted $@"With rtl/alu.v present but no rtl/nosuch.v:
make build/alu.lint
make build/nosuch.lint linted build/alu.lint
make: *** No rule to make target 'build/nosuch.lint'. Stop.The second message is the surprising one, because a rule for build/%.lint plainly exists. But Make tried the pattern, worked out that it would need rtl/nosuch.v, could not find that file and had no rule to produce it, and therefore concluded the pattern does not apply. Having exhausted its options, it reports what Chapter 2.1 taught you to read: no rule, no file.
So the mental model is: a pattern rule is an offer. Make accepts it when the prerequisite side can be satisfied and ignores it otherwise. In practice this is a good thing — it is what lets several pattern rules coexist, each applying where it fits.
6. An Explicit Rule Always Wins
If a target has both a pattern rule that matches and an explicit rule of its own, the explicit one is used:
build/%.lint: rtl/%.v
@mkdir -p $(@D)
@echo " PATTERN rule made $@"
@touch $@
build/alu.lint: rtl/alu.v
@mkdir -p $(@D)
@echo " EXPLICIT rule made $@"
@touch $@make build/alu.lint EXPLICIT rule made build/alu.lintThis is genuinely useful: write the pattern rule for the ordinary case and an explicit rule for the one module that needs different treatment — a legacy file that needs an extra option, or a generated source that comes from somewhere else. The exception lives next to the rule rather than inside it as a conditional.
It is also a trap when unintended. A leftover explicit rule for one file means that file quietly stops following the pattern everything else follows, and nothing in the output says so. Chapter 4.4 shows how to ask Make which rule it actually chose.
7. Static Pattern Rules: a Pattern for a Known List
An ordinary pattern rule applies to anything matching it. Often you would rather it applied only to a list you control. That is a static pattern rule, and it has an extra clause:
.PHONY: all
MODULES := alu
LINTS := $(patsubst %,build/%.lint,$(MODULES))
all: $(LINTS)
$(LINTS): build/%.lint: rtl/%.v
@mkdir -p $(@D)
@echo " STATIC pattern made $@ from $< (stem=$*)"
@touch $@Read the header in three parts: the list of targets this applies to, then the target pattern, then the prerequisite pattern.
make
make build/other.lint STATIC pattern made build/alu.lint from rtl/alu.v (stem=alu)
make: *** No rule to make target 'build/other.lint'. Stop.build/other.lint is not covered, even though it matches the pattern shape, because it is not in $(LINTS).
Why prefer this? Two reasons that matter in a real flow:
- It is explicit about scope. The set of things the rule builds is a list you can read, not whatever happens to match.
- It fails loudly for a name you did not intend. With an ordinary pattern rule, a typo like
build/alu.lntmight match some other pattern and do something odd; with a static pattern rule, anything outside the list simply has no rule.
Use a plain pattern rule for a genuinely open-ended kind of file, and a static pattern rule when you have a definite list — which, for an RTL flow driven by a module list, is most of the time.
8. Make's Own Built-In Rules
Make ships with pattern rules of its own, mostly for C and a few other languages. They are active unless you turn them off, and they can fire on a Makefile that defines nothing relevant:
.PHONY: nothing
nothing:
@echo "the Makefile defines no rule for hello.o"With a hello.c beside it:
make hello.occ -c -o hello.o hello.cMake compiled it. Nothing in the Makefile mentioned .o, .c or cc — that came from a built-in rule. The same request with built-in rules disabled behaves as you would expect:
make -r hello.omake: *** No rule to make target 'hello.o'. Stop.This is the mechanism behind the oddity Chapter 2.2 mentioned: a target name that happens to look like something Make knows how to build can behave in a way your Makefile does not explain. It matters in two practical situations — a target whose name collides with a built-in pattern, and a large Makefile where the built-in rules add measurable search time. make -r (or MAKEFLAGS += -r) turns them off, and for a flow that drives simulators rather than compilers there is rarely a reason to keep them.
9. Chaining, and Why an Intermediate Disappears
Make can apply pattern rules in sequence to reach a goal. Two rules, and a request for only the final file:
%.mid: src/%.in
@echo " [1] $@ from $<"
@tr a-z A-Z < $< > $@
%.out: %.mid
@echo " [2] $@ from $<"
@sed 's/^/> /' $< > $@make report.out
ls [1] report.mid from src/report.in
[2] report.out from report.mid
rm report.mid
Makefile report.out srcTwo things happened that are worth separating.
Make found a route. No rule says how to make report.out from src/report.in, but Make chained two pattern rules to get there — .in to .mid, then .mid to .out.
Make then deleted the middle file. Look at the output: the bare rm report.mid line is Make's own, and ls confirms the file is gone. A file that only existed to reach the goal is treated as an intermediate and removed afterwards.
That is usually what you want — no clutter — but it surprises people the first time, and it has a cost: the intermediate is rebuilt from scratch next time, so nothing is cached. If you want to keep it, name it as a prerequisite of something, or list it under .PRECIOUS. For a flow whose middle steps are expensive — an elaborated design, a compiled image — keeping them is usually the right call, and the simplest way is to make them real targets of a rule somebody asks for.
10. An RTL Flow With One Compile Rule
Putting the module together: a module list, one pattern rule per stage, and no filename anywhere. This version is portable and was executed, standing in for real tools:
.PHONY: all clean
.DEFAULT_GOAL := all
MODULES := alu regfile fifo
LINTS := $(patsubst %,build/%.lint,$(MODULES))
all: $(LINTS)
@echo " [all] $(words $(LINTS)) modules checked"
$(LINTS): build/%.lint: rtl/%.v
@mkdir -p $(@D)
@echo " [lint] $* -> $@"
@grep -c module $< > $@
clean:
@rm -rf buildFour properties worth naming, because each is this module's or the last one's:
- One recipe, three files. Fixing the check is one edit.
- A static pattern rule, so the set of modules is the list in
MODULESand nothing else. $*labels the output with the bare module name, which is what a log wants.- Per-file incremental rebuild survives — touching one source re-lints one module.
The simulator version is illustrative — vcs is not installed here and was not run:
.PHONY: all clean
.DEFAULT_GOAL := all
MODULES := alu regfile fifo
SIM ?= vcs
IMAGES := $(patsubst %,build/%/simv,$(MODULES))
all: $(IMAGES)
$(IMAGES): build/%/simv: rtl/%.v rtl/tb_%.v
mkdir -p $(@D)
$(SIM) -full64 -sverilog -top tb_$* $^ -o $@
clean:
rm -rf buildRead the prerequisites: rtl/%.v rtl/tb_%.v means both the design and its testbench, matched on the same stem — one of the real strengths of a pattern rule, because the pairing is expressed once. -top tb_$* uses the stem to name the top module, and $^ passes both files.
Two honest limits. A per-module simulation image is one way to organise a flow and not the only one — many projects compile everything into a single image instead. And the pattern pairs files purely by name, so a module whose testbench is called something else needs the explicit-rule escape hatch from §6.
11. Common Mistakes
Expecting a pattern rule to apply when the prerequisite does not exist. It does not, and the message says No rule to make target (§5). Check the implied prerequisite name first.
Writing a filename in a pattern rule's recipe. It will be wrong for every file but one. Use $@, $< and $*.
Forgetting that % must match in both places. build/%.lint: rtl/%.v pairs alu with alu. A rule like build/%.lint: rtl/alu.v is legal but means something entirely different — every lint depends on one fixed file.
Leaving a stale explicit rule. It silently overrides the pattern for that one target (§6).
Assuming an intermediate file is kept. Make deletes a file it created only to reach the goal (§9).
Being surprised by a built-in rule. A target can be built by a rule you never wrote (§8). make -r removes them from the picture.
Using a plain pattern rule where the list is known. A static pattern rule says what the rule is for, and refuses names you did not intend (§7).
12. Check Your Understanding
Question 1. A Makefile contains build/%.rpt: rtl/%.v and rtl/ holds alu.v and fifo.v. What does make build/alu.rpt do, and what does make build/uart.rpt do? Give the message for the second one.
Question 2. Inside the rule build/%.lint: rtl/%.v, Make is building build/regfile.lint. State the value of $@, $<, $* and $(@D).
Question 3. One module needs an extra option that the other nineteen do not. Describe two ways to express that, and say which keeps the ordinary rule simpler.
Question 4. make report.out prints two build messages and then a bare rm report.mid that you did not write. What happened, and what would you change to keep report.mid?
13. Exercise
Create rtl/ with alu.v, regfile.v and fifo.v, each containing one module line. Then this Makefile, which is meant to lint every module through a single rule.
.PHONY: all
.DEFAULT_GOAL := all
MODULES := alu regfile fifo
LINTS := $(patsubst %,build/%.lint,$(MODULES))
all: $(LINTS)
build/%.lint: rtl/alu.v
@mkdir -p $(@D)
@echo " [lint] building build/$*.lint"
@grep -c module rtl/$*.v > build/$*.lintTasks:
- The prerequisite is wrong in a way that still produces correct-looking output. Explain what it actually says and what breaks because of it.
- The recipe reconstructs filenames by hand. Rewrite it using automatic variables.
- Make it a static pattern rule and say what that buys you.
- Give the corrected Makefile.
14. Remember
- A pattern rule teaches Make how to build a whole class of files:
build/%.lint: rtl/%.v. The%must appear on both sides, and the same text matches in both. - The matched text is the stem, available as
$*. - A pattern rule cannot name a file, so
$@,$<and$*are required rather than merely convenient. - Incremental rebuilding is unchanged. Each generated file is still its own target with its own comparison.
- A pattern rule is a candidate, not a promise. Make uses it only if the implied prerequisite exists or can be built; otherwise you get
No rule to make target. - An explicit rule beats a pattern rule for the same target — useful for one exceptional file, and a trap when left behind by accident.
- A static pattern rule restricts the pattern to a list you name, which is usually what an RTL flow with a module list wants.
- Make has built-in pattern rules that can build a target you never wrote a rule for.
make -rturns them off. - An intermediate file created only to reach a goal is deleted afterwards. Name it as something's prerequisite, or mark it
.PRECIOUS, if you want it kept.
One rule can now serve every module. The next question is how one Makefile serves different situations — a debug build and a release build, two simulators, a shared set of definitions used by three flows. Chapter 4.2 covers the directives that choose between settings, and the include that lets several Makefiles share one definition.
Where this fits
Part of the Makefile curriculum.
