Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — one value is not enough
SIM ?= vcs
FLAGS := -full64 -sverilog      # only correct for vcs

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

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
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)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show
make show SIM=xcelium
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
SIM=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:

  • ifeq and endif start 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 characters vcs. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
ifdef DEBUG
  MODE := debug
else
  MODE := release
endif

show:
	@echo "MODE=[$(MODE)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show
make show DEBUG=1
make show DEBUG=
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
ifndef QUIET
  NOISE := loud
else
  NOISE := quiet
endif
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show
make show QUIET=yes
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
NOISE=[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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
ifneq ($(EXTRA),)
  STATUS := EXTRA was given as [$(EXTRA)]
else
  STATUS := EXTRA is empty or unset
endif

show:
	@echo "$(STATUS)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show
make show EXTRA=-gui
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
EXTRA 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
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)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make a b
make a b MODE=debug
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
target a sees TAG=RELEASE
target b sees TAG=RELEASE
target a sees TAG=DEBUG
target b sees TAG=DEBUG

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

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — WRONG: the conditional is tab-indented
SIM := vcs

show:
	ifeq ($(SIM),vcs)
	@echo "vcs"
	endif
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
ifeq (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 2

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

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — the shell makes the choice
SIM := vcs

show:
	@if [ "$(SIM)" = "vcs" ]; then echo "shell chose vcs"; else echo "shell chose other"; fi
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
shell chose vcs

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

Azvya Education Pvt. Ltd.VLSI Mentor
mk/common.mk
BUILD := build
SIM   ?= vcs
FLAGS := -full64 -sverilog
Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
include mk/common.mk

show:
	@echo "from common.mk: BUILD=[$(BUILD)] SIM=[$(SIM)] FLAGS=[$(FLAGS)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
from 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
include mk/nosuch.mk
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
Makefile: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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
-include mk/nosuch.mk

show:
	@echo "ran anyway"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
ran anyway

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [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:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  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.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
build/out.txt: rtl/alu.v | build
	@echo "  [build] $@ (normal prereqs: $^)"
	@cp $< $@

build:
	@echo "  [mkdir] creating $@"
	@mkdir -p build
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make build/out.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
touch build            # as if another file had been written into it
make build/out.txt
touch rtl/alu.v
make build/out.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
make: '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.

Two inputs on the left feeding one target on the right. The upper input, rtl slash alu dot v, is a normal prerequisite: if it is newer than the target the recipe runs again. The lower input, the build directory, is an order-only prerequisite written after a vertical bar: it must exist before the target is built, but its timestamp is never compared and it is excluded from the all-prerequisites list.rtl/alu.vnormal: newer rebuildsbuild/out.txtthe targetbuildorder-only: must exist12
Figure 1 — the two kinds of prerequisite. A normal prerequisite is compared against the target, so a newer one causes a rebuild. An order-only prerequisite, written after the vertical bar, only has to exist first — its timestamp is never compared, and it does not appear in the all-prerequisites variable the recipe uses.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
mk/common.mk — shared by every flow in the project
BUILD   := build
RTL_DIR := rtl
SOURCES := $(sort $(wildcard $(RTL_DIR)/*.v))
Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — portable stand-in, executed as shown
.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.mk holds the paths and the source discovery, so three flows can share them.
  • ifeq selects 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.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make
make clean && make SIM=xcelium
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [compile] vcs: 2 files -> build/sim.img
  [all] built with vcs
  [compile] xcelium: 2 files -> build/sim.img
  [all] built with xcelium

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

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — illustrative; no simulator was executed
.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.

Azvya Education Pvt. Ltd.VLSI Mentor
mk/common.mk
BUILD   := build
RTL_DIR := rtl
Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — three bugs
.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:

  1. SOURCES comes out empty. Why, and what is the one-line fix?
  2. The $(BUILD) prerequisite causes two problems — one about rebuilding and one about what the recipe receives. Describe both.
  3. Add a conditional so MODE=debug selects a different joining command, and say why ifdef MODE would be the wrong directive here.
  4. Give the corrected Makefile.

15. Remember

  • A conditional chooses a group of settings, which a single knob cannot do.
  • ifeq, ifneq, ifdef, ifndef, else and endif go at the left margin. A tab turns them into recipe lines and the shell reports the error.
  • ifdef is true for a non-empty value, not for a mention — DEBUG= counts as undefined. Use ifneq ($(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.
  • include is textual, so order matters: a := must come after the include it depends on.
  • -include tolerates a missing file; a plain include is 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.