Makefile · Part I · Module 4 · Reusable Rules and Debugging
Conditional Directives and Included Makefiles
A real project does not have one build. It has a debug build and a regression build, two simulators because the team is migrating, three flows that share the same paths. Copying a Makefile per situation is how those copies drift apart. This chapter covers the two tools that avoid it — directives that choose between settings as the file is read, and an include that lets several Makefiles share one definition — plus the prerequisite form that lets a setup step happen without forcing everything to rebuild.
Intermediate22 min readMakefileGNU MakePart IConditionalsInclude
Part I · Module 4 · Chapter 4.2 · Conditional Directives and Included Makefiles
1. One Makefile, Several Situations
Chapter 3.2 gave a flow knobs: make run SIM=xcelium. But a knob only changes a value. Often a different choice needs a different set of values — a different option string, a different runner command, a different output directory.
Writing that with a knob alone does not work:
SIM ?= vcs
FLAGS := -full64 -sverilog # only correct for vcsSetting SIM=xcelium leaves the wrong flags behind. You need to choose a group of settings together, and that is what a conditional directive does.
The second half of the chapter is the other kind of duplication: three Makefiles in a project that each define the same paths. include lets them share one file.
2. ifeq: Choosing Between Two Settings
ifeq compares two values and selects a block. The structure is a directive line, a block, an optional else, and endif:
SIM ?= vcs
ifeq ($(SIM),vcs)
FLAGS := -full64 -sverilog
RUNNER := ./simv
else
FLAGS := -sv -64bit
RUNNER := ./xsim
endif
show:
@echo "SIM=$(SIM) FLAGS=[$(FLAGS)] RUNNER=[$(RUNNER)]"make show
make show SIM=xceliumSIM=vcs FLAGS=[-full64 -sverilog] RUNNER=[./simv]
SIM=xcelium FLAGS=[-sv -64bit] RUNNER=[./xsim]One knob now selects two settings that belong together. Three points of syntax, all of which catch people once:
ifeqandendifstart at the left margin, not indented with a tab. §6 shows what a tab does instead.- The indentation inside the block is cosmetic. The two spaces above are for readability; Make does not require them. Just never use a tab.
- The comparison is textual.
ifeq ($(SIM),vcs)is true when the value is exactly the three charactersvcs. A trailing space in the value makes it false, which is Chapter 3.1's whitespace trap arriving somewhere new.
3. ifdef Tests for a Value, Not a Mention
ifdef asks whether a variable has a value. It is the natural choice for a flag-style switch:
ifdef DEBUG
MODE := debug
else
MODE := release
endif
show:
@echo "MODE=[$(MODE)]"make show
make show DEBUG=1
make show DEBUG=MODE=[release]
MODE=[debug]
MODE=[release]Look at the third run. DEBUG= was given on the command line, and ifdef still chose release.
That is the one thing to remember about ifdef: it is true when the variable's value is non-empty, not when the name has been mentioned. DEBUG= sets it to the empty string, which ifdef treats as "not defined". The name is misleading and the behaviour is documented.
ifndef is the inverse, and is how you provide a default block:
ifndef QUIET
NOISE := loud
else
NOISE := quiet
endifmake show
make show QUIET=yesNOISE=[loud]
NOISE=[quiet]4. ifneq and Comparing Against Empty
ifneq is ifeq negated, and its most common use is testing whether something was supplied at all:
ifneq ($(EXTRA),)
STATUS := EXTRA was given as [$(EXTRA)]
else
STATUS := EXTRA is empty or unset
endif
show:
@echo "$(STATUS)"make show
make show EXTRA=-guiEXTRA is empty or unset
EXTRA was given as [-gui]The empty second argument in ifneq ($(EXTRA),) is deliberate — it compares the value against nothing. This form and ifdef answer almost the same question; ifneq (...,) is the more honest one to read, because it says "is this non-empty" rather than implying definedness.
5. A Conditional Is Decided Once, When the File Is Read
This is the conceptual point of the chapter, and it follows from Chapter 1.4: Make reads the whole file first, then builds.
A conditional is part of reading. It is evaluated once, before any target is considered, and every rule in the file sees the same outcome:
ifeq ($(MODE),debug)
TAG := DEBUG
else
TAG := RELEASE
endif
.PHONY: a b
a:
@echo "target a sees TAG=$(TAG)"
b:
@echo "target b sees TAG=$(TAG)"make a b
make a b MODE=debugtarget a sees TAG=RELEASE
target b sees TAG=RELEASE
target a sees TAG=DEBUG
target b sees TAG=DEBUGBoth targets agree within each invocation. You cannot use a conditional to give two targets different settings in one run — that is not what the feature is for, and expecting it is the usual misunderstanding.
If two targets genuinely need different values, the ways to express that are a different value per invocation (make a MODE=debug then make b), a target-specific variable, or simply two rules. A conditional chooses a configuration for this run.
6. Where a Conditional May Not Go
Indent ifeq with a tab and it stops being a directive. It becomes a recipe line, and Chapter 1.4's second reader gets it:
SIM := vcs
show:
ifeq ($(SIM),vcs)
@echo "vcs"
endififeq (vcs,vcs)
/bin/sh: -c: line 0: syntax error near unexpected token `vcs,vcs'
/bin/sh: -c: line 0: `ifeq (vcs,vcs)'
make: *** [show] Error 2Read the first line of that output: ifeq (vcs,vcs). Make expanded $(SIM) — because a $ in a recipe is Make's — and then handed the whole thing to /bin/sh, which has never heard of ifeq. The error comes from the shell, not from Make, which is exactly how you tell which side of the tab boundary a problem is on.
So conditionals live outside recipes. If the choice genuinely has to happen when the recipe runs, use the shell's own if:
SIM := vcs
show:
@if [ "$(SIM)" = "vcs" ]; then echo "shell chose vcs"; else echo "shell chose other"; fishell chose vcsPrefer the Make-level conditional when the decision is about configuration, and the shell-level if only when it depends on something that can only be known while the recipe is running.
7. include: Sharing Definitions
include reads another file at that point, exactly as if its text were pasted in. It is how several Makefiles share one set of definitions:
BUILD := build
SIM ?= vcs
FLAGS := -full64 -sveriloginclude mk/common.mk
show:
@echo "from common.mk: BUILD=[$(BUILD)] SIM=[$(SIM)] FLAGS=[$(FLAGS)]"make showfrom common.mk: BUILD=[build] SIM=[vcs] FLAGS=[-full64 -sverilog]Two practical notes. The included file is ordinary Makefile text, so it can hold rules as well as variables — though a shared file holding only definitions is much easier to reason about. And because include is textual, order matters in exactly the way Chapter 3.1 described: a := in your Makefile that refers to a variable from the include must come after the include line.
8. -include: Tolerating a Missing File
A missing include is fatal:
include mk/nosuch.mkMakefile:1: mk/nosuch.mk: No such file or directory
make: *** No rule to make target `mk/nosuch.mk'. Stop.Note the second line — Make did not merely complain that the file is absent, it tried to build it and found no rule. That is a hint about §9.
Prefix the directive with a dash and a missing file is silently ignored:
-include mk/nosuch.mk
show:
@echo "ran anyway"ran anywayUse -include for a file that is optional by design — a developer's local overrides, or a generated file that does not exist on a fresh checkout.
9. Make Can Build What It Includes, Then Restart
This is the behaviour that makes include more than text-pasting, and it looks like magic the first time.
If an included file is missing but a rule can produce it, Make builds it and then re-reads the whole Makefile from the start:
-include generated.mk
generated.mk:
@echo " [gen] creating generated.mk"
@echo 'GENERATED_VALUE := made-by-a-rule' > generated.mk
.PHONY: show
show:
@echo " GENERATED_VALUE=[$(GENERATED_VALUE)]"On a clean directory, with no generated.mk anywhere:
make show [gen] creating generated.mk
GENERATED_VALUE=[made-by-a-rule]Trace what must have happened. When Make read line 1, generated.mk did not exist, so GENERATED_VALUE was empty. It then noticed it had a rule for the missing include, ran it, and restarted itself — and on the second pass the include succeeded and the variable had a value. All inside one make command.
The second run is ordinary, because the file is already there:
GENERATED_VALUE=[made-by-a-rule]This is not a curiosity. It is the mechanism behind automatically generated dependency information: a tool emits a small Makefile fragment listing what a source really depends on, the main Makefile -includes it, and Make builds and reads it as needed. That is how the `include problem from Chapter 2.3 gets solved properly, and Chapter 5.2 is where a real filelist flow uses it.
One caution: because Make restarts, anything with a side effect that runs during reading happens twice. A $(shell ...) at file scope is the usual culprit, which is one more reason Chapter 3.4 insisted on assigning it with :=.
10. Order-Only Prerequisites: the Directory Problem, Solved
Chapter 2.2 left a problem open. A setup step that must happen before the real work — creating an output directory — is a bad fit for an ordinary prerequisite, because a phony one forces the parent to rebuild every time.
The right tool is an order-only prerequisite: anything listed after a | must be up to date before the target is built, but its timestamp is not compared.
build/out.txt: rtl/alu.v | build
@echo " [build] $@ (normal prereqs: $^)"
@cp $< $@
build:
@echo " [mkdir] creating $@"
@mkdir -p buildmake build/out.txt [mkdir] creating build
[build] build/out.txt (normal prereqs: rtl/alu.v)Two things to notice straight away. The directory was created first, as required. And $^ contains only rtl/alu.v — an order-only prerequisite is excluded from $^, so a recipe using $^ to pass its inputs to a tool will not accidentally pass the directory.
Now the behaviour that makes this the right answer. A directory's timestamp changes whenever a file is added to it, so as an ordinary prerequisite it would retrigger the build constantly. As an order-only prerequisite it does not:
touch build # as if another file had been written into it
make build/out.txt
touch rtl/alu.v
make build/out.txtmake: 'build/out.txt' is up to date.
[build] build/out.txt (normal prereqs: rtl/alu.v)Touching the directory changed nothing; touching the source rebuilt. That is exactly the split you want.
Compared with the mkdir -p $(@D) habit from Chapter 3.3, an order-only prerequisite moves the directory creation out of every recipe and into one rule. Both are correct; the order-only form is tidier once more than one rule writes into the same place.
11. An RTL Flow With Two Simulators and a Shared File
Putting the chapter together. A shared definitions file, a conditional that selects a simulator's settings, an order-only directory, and a knob. This version is portable and was executed, standing in for real tools:
BUILD := build
RTL_DIR := rtl
SOURCES := $(sort $(wildcard $(RTL_DIR)/*.v)).PHONY: all clean
.DEFAULT_GOAL := all
include mk/common.mk
SIM ?= vcs
ifeq ($(SIM),vcs)
TAG := vcs
JOINER := cat
else
TAG := xcelium
JOINER := sort
endif
all: $(BUILD)/sim.img
@echo " [all] built with $(TAG)"
$(BUILD)/sim.img: $(SOURCES) | $(BUILD)
@echo " [compile] $(TAG): $(words $^) files -> $@"
@$(JOINER) $^ > $@
$(BUILD):
@mkdir -p $(BUILD)
clean:
@rm -rf $(BUILD)Five things in that file, each from this module or the last:
include mk/common.mkholds the paths and the source discovery, so three flows can share them.ifeqselects a group of settings together, not a single value.| $(BUILD)creates the directory without making the artifact rebuild when the directory changes.$^is safe in the recipe, because the order-only prerequisite is excluded from it.$(words $^)prints the count, so an empty source list is visible — Chapter 3.4's guard.
make
make clean && make SIM=xcelium [compile] vcs: 2 files -> build/sim.img
[all] built with vcs
[compile] xcelium: 2 files -> build/sim.img
[all] built with xceliumTwo files in both runs, and the simulator choice changed the label and the joining command together — one knob, a group of settings.
The simulator version is illustrative — neither vcs nor xrun is installed here, so it was not run:
.PHONY: all clean
.DEFAULT_GOAL := all
include mk/common.mk
SIM ?= vcs
TOP ?= tb_top
ifeq ($(SIM),vcs)
COMPILE := vcs -full64 -sverilog
IMAGE := $(BUILD)/simv
else
COMPILE := xrun -64bit -sv -elaborate
IMAGE := $(BUILD)/xcelium.d
endif
all: $(IMAGE)
$(IMAGE): $(SOURCES) | $(BUILD)
$(COMPILE) -top $(TOP) $^ -o $@
$(BUILD):
mkdir -p $(BUILD)
clean:
rm -rf $(BUILD)Note that the conditional selects both the command and the artifact name, because the two simulators produce different things. A knob alone could not express that, which is the argument for conditionals in one line.
One honest limit: two simulators rarely differ only in their flags. Elaboration steps, waveform options and log formats differ too, and a serious multi-simulator flow usually separates them into their own included files rather than one ifeq. The mechanism here is right; the scale is simplified.
12. Common Mistakes
Indenting ifeq with a tab. It becomes a recipe line and the shell reports a syntax error (§6).
Expecting ifdef to be true for an empty value. DEBUG= is treated as undefined (§3). Use ifneq ($(DEBUG),) if you mean "was anything supplied".
Expecting a conditional to differ per target. It is decided once when the file is read (§5).
Comparing a value with a trailing space. ifeq ($(SIM),vcs) is false if SIM holds vcs — Chapter 3.1's whitespace trap.
Using include where the file may be absent. It is fatal; use -include for anything optional (§8).
Referring to an included variable before the include line. include is textual, so a := above it sees nothing.
Making a directory an ordinary prerequisite. Its timestamp changes whenever anything is written into it, so the target rebuilds constantly. Put it after a | (§10).
Forgetting that a restart runs file-scope work twice. If Make rebuilds an included file it re-reads everything, so a $(shell ...) assigned with = executes again (§9).
13. Check Your Understanding
Question 1. A Makefile has ifdef VERBOSE selecting extra output. An engineer runs make VERBOSE= and gets the quiet behaviour. Explain why, and give the directive that would have worked.
Question 2. ifeq is written at the left margin in one Makefile and with a leading tab in another. Describe what each one does, and which program produces the error in the second case.
Question 3. A rule reads build/out.txt: src/in.txt | build. You run make, then write an unrelated file into build/, then run make again. Does the recipe run? What would happen if the | were removed?
Question 4. A Makefile -includes deps.mk, which does not exist, and has a rule that creates it. Describe what happens on the first make, and why the variable defined inside deps.mk is usable in the same invocation.
14. Exercise
Create rtl/ with alu.v and fifo.v, each holding one module line, plus mk/common.mk and this Makefile.
BUILD := build
RTL_DIR := rtl.PHONY: all clean
.DEFAULT_GOAL := all
SOURCES := $(sort $(wildcard $(RTL_DIR)/*.v))
include mk/common.mk
MODE ?= release
all: $(BUILD)/sim.img
$(BUILD)/sim.img: $(SOURCES) $(BUILD)
@echo " [compile] $(words $^) files -> $@"
@cat $^ > $@
$(BUILD):
@mkdir -p $(BUILD)
clean:
@rm -rf $(BUILD)Tasks:
SOURCEScomes out empty. Why, and what is the one-line fix?- The
$(BUILD)prerequisite causes two problems — one about rebuilding and one about what the recipe receives. Describe both. - Add a conditional so
MODE=debugselects a different joining command, and say whyifdef MODEwould be the wrong directive here. - Give the corrected Makefile.
15. Remember
- A conditional chooses a group of settings, which a single knob cannot do.
ifeq,ifneq,ifdef,ifndef,elseandendifgo at the left margin. A tab turns them into recipe lines and the shell reports the error.ifdefis true for a non-empty value, not for a mention —DEBUG=counts as undefined. Useifneq ($(VAR),)when you mean "was anything supplied".- A conditional is decided once, when the file is read. Every rule in that invocation sees the same outcome; it cannot vary per target.
includeis textual, so order matters: a:=must come after theincludeit depends on.-includetolerates a missing file; a plainincludeis fatal.- Make can build a missing included file and restart itself, which is the mechanism behind generated dependency information — and the reason file-scope side effects can run twice.
- An order-only prerequisite after
|must exist but is never compared, and is excluded from$^. It is the right way to depend on a directory.
Part I has one chapter of structure left and one of diagnosis. You can now write a Makefile that is reusable across files and across configurations. What has been deferred four separate times is what happens when a command inside a recipe goes wrong — and in particular the cases where it goes wrong while reporting success. Chapter 4.3 takes that on.
Where this fits
Part of the Makefile curriculum.
