Skip to content
VLSI Mentor

Makefile · Part I · Module 3 · Variables and Make Functions

Variables: =, :=, ?= and +=

Every Makefile so far has spelled out each filename and each tool option by hand, sometimes three times in the same rule. Variables remove that repetition. They also introduce the one genuinely confusing thing about Make: there are two kinds of assignment that look almost the same, and they answer the question 'what is this value?' at two different moments. Get that distinction and the rest of the module is straightforward.

Foundation20 min readMakefileGNU MakePart IVariablesExpansion

Part I · Module 3 · Chapter 3.1 · Variables: =, :=, ?= and +=

1. The Problem: One Name, Written Many Times

Look back at a rule from Chapter 2.1:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — every name written out longhand
build/sim.img: src/alu.v src/regfile.v src/tb_top.v
	@mkdir -p build
	@cat src/alu.v src/regfile.v src/tb_top.v > build/sim.img

Count the repetition. The three source files appear twice. The word build appears three times. Add a fourth source file and you have to edit two places; forget one and you get either a stale output or a command that misses a file.

A variable is a name that holds a piece of text. You define it once and use it wherever that text is needed:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — the same rule with variables
BUILD   := build
SOURCES := src/alu.v src/regfile.v src/tb_top.v

$(BUILD)/sim.img: $(SOURCES)
	@mkdir -p $(BUILD)
	@cat $(SOURCES) > $(BUILD)/sim.img

Now a new source file is one edit. That is the practical reason variables exist, and for most of a Makefile it is all you need to know.

The complication is that Make has two different kinds of assignment, plus two convenience forms. This chapter is about telling them apart.

2. Defining and Using a Variable

The syntax has two halves.

To define, write a name, an assignment operator, and a value:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
SIM := vcs

Names are conventionally upper case. There are no quotes and no data types — the value is simply the text to the right, so SIM holds exactly the three characters vcs.

To use, write the name wrapped in $( ):

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
SIM := vcs

show:
	@echo "the simulator is $(SIM)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
the simulator is vcs

Make replaces $(SIM) with the value before the line reaches the shell. The shell never sees a variable at all — it receives echo "the simulator is vcs". That is the same boundary Chapter 1.4 established: a $ in a recipe belongs to Make, and you write $$ when you want the shell to have one.

A variable nobody defined is simply empty, not an error:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
show:
	@echo "[$(NOT_DEFINED)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
[]

This matters more than it looks. A typo in a variable name does not fail — it quietly expands to nothing, and your compile runs with an option missing.

3. Where a Variable May Be Used, and How to See It

A variable is not limited to recipes. Make expands $( ) almost everywhere in a Makefile — including in the target and the prerequisites:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
BUILD := build
NAME  := design
EXT   := list

$(BUILD)/$(NAME).$(EXT): rtl/$(NAME)_src.txt
	@mkdir -p $(BUILD)
	@cp rtl/$(NAME)_src.txt $(BUILD)/$(NAME).$(EXT)
	@echo "  made $(BUILD)/$(NAME).$(EXT)"

With rtl/design_src.txt present:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make
make
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
  made build/design.list
make: 'build/design.list' is up to date.

The rule has no literal filenames in it at all, and it still behaves like an ordinary rule — it skipped the second run. Proof that the target name really is the expanded text: you can ask for it by that name directly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make build/design.list
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
make: 'build/design.list' is up to date.

This works because expansion happens before Make interprets the line as a rule. By the time Make is deciding what depends on what, $(BUILD)/$(NAME).$(EXT) has already become the eight characters plus path that you see. §12 shows what goes wrong when that expansion produces something you did not expect.

4. Two Flavours: = and :=

Here are the two assignments that look alike:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A := some text      # simple assignment: work the value out NOW
B  = some text      # recursive assignment: work the value out EACH TIME B is used

When the value is plain text, they behave identically. The difference appears the moment a value refers to another variable.

  • := expands the right-hand side immediately, at the line it is written, and stores the result. Make calls this a simple variable. Think of it as taking a snapshot.
  • = stores the right-hand side as text and expands it every time the variable is used. Make calls this a recursive variable. Think of it as a recipe to be followed later.

The clearest first demonstration is a value defined further down the file:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
LATE  = $(EXTRA)
EARLY := $(EXTRA)

EXTRA = -sverilog

show:
	@echo "recursive  = : [$(LATE)]"
	@echo "simple    := : [$(EARLY)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
recursive  = : [-sverilog]
simple    := : []

LATE works. EARLY is empty — when its line was read, EXTRA did not exist yet, and := froze that emptiness permanently. Nothing later can change it.

So the one-line summary: := cannot see the future; = can.

5. Seeing the Difference Side by Side

The sharpest demonstration is two Makefiles that are identical except for one character.

With :=:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — simple assignment
BASE := alu
NAME := $(BASE)_tb
BASE := regfile

show:
	@echo "NAME is [$(NAME)]  (BASE is now [$(BASE)])"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
NAME is [alu_tb]  (BASE is now [regfile])

With =:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — recursive assignment
BASE = alu
NAME = $(BASE)_tb
BASE = regfile

show:
	@echo "NAME is [$(NAME)]  (BASE is now [$(BASE)])"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
NAME is [regfile_tb]  (BASE is now [regfile])

Same three lines, same order, one character different — and NAME is alu_tb in one and regfile_tb in the other.

Walk it through. In the := version, line 2 is evaluated when it is read: BASE is alu at that moment, so NAME becomes the fixed text alu_tb. Line 3 changes BASE, but NAME was already decided.

In the = version, line 2 only remembers the text $(BASE)_tb. By the time show runs and the value is actually needed, BASE is regfile, so the answer is regfile_tb.

Two rows compared, each flowing left to right. Top row: the definition NAME colon-equals is worked out when the line is read, giving the fixed result alu underscore tb because BASE was alu at that moment. Bottom row: the definition NAME equals stores only the text and is worked out when the variable is used, giving regfile underscore tb because BASE had been changed by then.NAME :=worked out when readalu_tbBASE was alu thenNAME =worked out when usedregfile_tbBASE is regfile by then12
Figure 1 — the same definition under each operator. With simple assignment the value is worked out on the line it is written, so a later change to BASE cannot reach it. With recursive assignment only the text is stored, and the value is worked out at the moment the variable is used — by which time BASE has changed.

Neither answer is wrong. They are answers to different questions, and you pick the operator that asks the question you meant.

6. ?= — A Default Somebody Can Override

?= assigns only if the variable has no value yet. It is how you provide a default that a user can replace.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
TOP ?= default_top
SIM := vcs
SIM ?= xcelium

show:
	@echo "TOP=[$(TOP)]  SIM=[$(SIM)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
TOP=[default_top]  SIM=[vcs]

Two things happened. TOP had no value, so ?= set it. SIM already held vcs from the line above, so the ?= line did nothing — it did not overwrite and it did not complain.

Now give TOP a value on the command line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show TOP=axi_tb
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
TOP=[axi_tb]  SIM=[vcs]

The ?= line stepped aside. This is the normal way a flow offers a knob: TOP ?= tb_top in the Makefile means "use tb_top unless somebody says otherwise."

Where that value can come from — the command line, the surrounding shell, or the Makefile — and which source wins, is Chapter 3.2. Here it is enough to see that ?= yields to one.

7. += — Adding to a List

Tool options accumulate. += appends to a variable, inserting a single space:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
FLAGS := -full64
FLAGS += -sverilog
FLAGS += -debug_access

show:
	@echo "[$(FLAGS)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
[-full64 -sverilog -debug_access]

That is the ordinary use, and it reads well: a base set of options, then additions. On a variable that does not exist yet, += simply starts it with no leading space:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
NEW += first
NEW += second

show:
	@echo "[$(NEW)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
[first second]

So += is safe to use before any plain assignment, which is useful when several included fragments each contribute options.

8. The One += Subtlety Worth Knowing

+= does not have a flavour of its own. It keeps the flavour of the assignment that created the variable. That means two variables built the same way can disagree:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
EXTRA = -gui

SIMPLE := -full64
SIMPLE += $(EXTRA)

RECUR  = -full64
RECUR += $(EXTRA)

EXTRA = -nogui

show:
	@echo "simple  := then += : [$(SIMPLE)]"
	@echo "recursive = then += : [$(RECUR)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
simple  := then += : [-full64 -gui]
recursive = then += : [-full64 -nogui]

SIMPLE was created with :=, so its += also expanded immediately and captured -gui. RECUR was created with =, so its += stored the text and picked up -nogui when it was finally used.

You do not need to memorise this. You need to recognise the symptom: a variable that holds the wrong one of two values, where both values are correct at different points in the file. When that happens, look at which operator created the variable, not at the += line.

9. A Realistic Version of the Same Bug

§5's BASE/NAME example is small on purpose. Here is the same mistake at the size it actually appears, in a flow that builds a per-test run directory:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — every operator is :=, and the order is wrong
BUILD   := build
LOG     := $(RUN_DIR)/run.log
RUN_DIR := $(BUILD)/$(TEST)
TEST    ?= smoke

show:
	@echo "BUILD   = [$(BUILD)]"
	@echo "TEST    = [$(TEST)]"
	@echo "RUN_DIR = [$(RUN_DIR)]"
	@echo "LOG     = [$(LOG)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
BUILD   = [build]
TEST    = [smoke]
RUN_DIR = [build/]
LOG     = [/run.log]

BUILD and TEST are fine. The two derived values are both wrong, and they are wrong in different ways:

  • RUN_DIR is build/ — it was frozen before TEST existed, so the test name is missing and the path ends in a bare slash.
  • LOG is /run.log — frozen before RUN_DIR existed, so it lost the directory entirely and now points at the filesystem root.

One misordered file, two broken paths, no error message. A flow like this would write its log somewhere unexpected, or fail with a permission error that says nothing about variables.

There are two fixes, and they are not equally good.

Fix one: use = throughout. Because the values are then worked out at the moment they are used, order stops mattering:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — the same order, with = instead
BUILD   = build
LOG     = $(RUN_DIR)/run.log
RUN_DIR = $(BUILD)/$(TEST)
TEST   ?= smoke
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
RUN_DIR = [build/smoke]
LOG     = [build/smoke/run.log]

That works, and it is also why = is tempting: it forgives a file somebody has edited over a year. The cost is that you can no longer read the file top to bottom and know what anything holds.

Fix two: keep := and put the lines in dependency order. Define a value after everything it refers to:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — := in the right order
BUILD   := build
TEST    ?= smoke
RUN_DIR := $(BUILD)/$(TEST)
LOG     := $(RUN_DIR)/run.log
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
RUN_DIR = [build/smoke]
LOG     = [build/smoke/run.log]

Same correct answers, and now every line means exactly what it says. This is the version to prefer.

One reassurance about :=, because it is a fair worry: freezing a value does not prevent somebody overriding it from the command line. An override is in place before Make reads the file at all, so := sees the overridden value:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show TEST=axi_burst
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
RUN_DIR = [build/axi_burst]
LOG     = [build/axi_burst/run.log]

The knob still works. Chapter 3.2 explains why, and which other sources can do the same.

10. Choosing an Operator

A short decision table, and then the habit that avoids most trouble.

OperatorNameValue is worked outUse it when
:=simpleonce, where it is writtenalmost always — paths, file lists, tool names
=recursiveevery time it is usedthe value must depend on something set later
?=conditionalonce, if unsetoffering a default a user may override
+=appendingfollows the original flavourbuilding up a list of options

Prefer :=. It is predictable: you can read the file top to bottom and know every value. A recursive = is occasionally necessary, and it is also how a value ends up depending on the order somebody happened to write things in.

The exception in practice is ?= for the knobs a flow exposes — TOP, SEED, SIM — because those are meant to be set from outside.

11. An RTL Flow Parameterised by Variables

Now the same compile flow from Module 2, with every repeated name lifted into a variable. This version is portable and was executed, standing in for a simulator by concatenating sources:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — portable stand-in, executed as shown
.PHONY: all clean
.DEFAULT_GOAL := all

BUILD   := build
TOP     ?= tb_top
SOURCES := rtl/defines.vh rtl/alu.v rtl/tb_top.v

all: $(BUILD)/sim.img

$(BUILD)/sim.img: $(SOURCES)
	@echo "  [compile] top=$(TOP)"
	@mkdir -p $(BUILD)
	@cat $(SOURCES) > $(BUILD)/sim.img

clean:
	@echo "  [clean]"
	@rm -rf $(BUILD)

Four invocations:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make                     # 1. build with the default top
make                     # 2. nothing changed
make clean && make TOP=alu_tb    # 3. override the knob
# edit rtl/defines.vh, then:
make                     # 4. a declared prerequisite changed
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
  [compile] top=tb_top
make: Nothing to be done for 'all'.
  [clean]
  [compile] top=alu_tb
  [compile] top=tb_top

Read run 2 carefully, because it is a detail from the previous module showing through. all is phony and has no recipe of its own — its only job is to name a prerequisite. When that prerequisite is up to date there is genuinely nothing to do, and Make says Nothing to be done, which Chapter 2.1 listed as one of its three answers to a name.

Three things the variables bought here:

  • SOURCES is written once and used twice — after the colon, where it drives rebuilding, and in the recipe, where it does the work. The duplication that Chapter 1.3 called out is gone, and the two can no longer drift apart.
  • BUILD is one edit if the output directory ever moves.
  • TOP is a knob, declared with ?= so a command line can replace it without editing the file.

The simulator version is illustrative — vcs is not installed here and was not run:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — illustrative; vcs was not executed
.PHONY: all clean
.DEFAULT_GOAL := all

BUILD   := build
TOP     ?= tb_top
SIM     ?= vcs
SOURCES := rtl/defines.vh rtl/alu.v rtl/tb_top.v
FLAGS   := -full64 -sverilog
FLAGS   += -debug_access+all

all: $(BUILD)/simv

$(BUILD)/simv: $(SOURCES)
	mkdir -p $(BUILD)
	$(SIM) $(FLAGS) -top $(TOP) rtl/alu.v rtl/tb_top.v -o $(BUILD)/simv

clean:
	rm -rf $(BUILD)

Note that $(SOURCES) is the prerequisite list while the command names only the two .v files — defines.vh reaches the compiler through the `include inside alu.v. That split is deliberate and Chapter 2.3 explained it: a prerequisite list says when to rebuild, and a command line says what to pass. The two overlap but are not the same list.

12. Common Mistakes

A trailing space becomes part of the value. There are no quotes, so Make keeps whatever you typed:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — note the space after build
DIR := build 

show:
	@echo "path=[$(DIR)/sim.img]"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
path=[build /sim.img]

A broken path from an invisible character. If a path mysteriously fails, check for trailing whitespace before anything else.

A misspelled variable is silent. $(SOURCE) when you defined SOURCES expands to nothing, so the compile simply runs with no files. Nothing warns you. (make --warn-undefined-variables reports these; the diagnostic flags are Chapter 4.4.)

Using = by habit and getting order-dependence. If a value depends on another variable, = makes the answer depend on when it is used. Prefer := unless you specifically want the late evaluation.

A recursive variable that refers to itself. With = this is a loop, and Make refuses:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
A = $(A) x
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
Makefile:1: *** Recursive variable `A' references itself (eventually).  Stop.

With := the same line is fine and harmless, because the right-hand side is expanded first: A := $(A) x appends to whatever A held before.

Writing $HOME and expecting the shell's value. In a recipe, $H is a Make variable. Chapter 1.4 covered this — write $$HOME for the shell.

Forgetting the parentheses. $(SIM) is the variable SIM; $SIM is the one-character variable S followed by the literal text IM. Single-letter variables are legal, which is why this fails quietly rather than loudly.

13. Check Your Understanding

Question 1. A Makefile reads OUT := $(BUILD)/sim.img on line 1 and BUILD := build on line 2. What does OUT hold, and which single character would fix it?

Question 2. FLAGS is created with := and then extended with += $(EXTRA). EXTRA is changed later in the file. Does FLAGS see the new value? Explain using the word "flavour".

Question 3. A flow has SEED ?= 1. An engineer runs make run SEED=42. What does SEED hold, and would that change if the line had been SEED := 1? Say what ?= is actually protecting.

Question 4. A compile runs with no source files and reports no error. The recipe says $(SOURCE) and the definition says SOURCES. Why was there no error message?

14. Exercise

Create rtl/alu.v and rtl/tb_top.v with one module each. This Makefile has three variable bugs, all from this chapter.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — three bugs
.PHONY: all
.DEFAULT_GOAL := all

BUILD  := build 
OUT    := $(BUILD)design.list
SOURCE := rtl/alu.v rtl/tb_top.v
COUNT  := $(TOTAL)
TOTAL  := 2

all: $(OUT)

$(OUT): $(SOURCES)
	@echo "  [build] out=[$(OUT)] count=[$(COUNT)] sources=[$(SOURCES)]"
	@mkdir -p $(BUILD)
	@cat $(SOURCES) > $(OUT)

Before running it, write down what each of the three bracketed values will contain. Then run make and compare.

15. Remember

  • A variable holds text, and $(NAME) is replaced by that text before the shell sees the line. No quotes, no types.
  • An undefined variable is empty, not an error. A typo removes an option silently.
  • := works the value out once, where it is written. It cannot see anything defined further down.
  • = stores the text and works it out every time it is used. It can see later definitions, which also makes it order-dependent.
  • Prefer := so the file reads predictably top to bottom. Reach for = only when you need the late evaluation.
  • ?= sets a value only if nothing has set it, which is how a flow offers an overridable knob.
  • += appends with a space and inherits the original flavour — += after := captures now, += after = captures later.
  • Trailing spaces are part of the value, and $(NAME) needs its parentheses — $NAME is not what you meant.
  • Writing a file list once removes the drift between the prerequisite list and the command, which was a real source of stale builds in Module 2.

You can now replace repeated text with a name. The next question is where a value comes from when it is not written in the Makefile — the shell that launched Make, or the command line somebody typed. Chapter 3.2 covers the three sources and which of them wins.

Where this fits

Part of the Makefile curriculum.