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:
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.imgCount 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:
BUILD := build
SOURCES := src/alu.v src/regfile.v src/tb_top.v
$(BUILD)/sim.img: $(SOURCES)
@mkdir -p $(BUILD)
@cat $(SOURCES) > $(BUILD)/sim.imgNow 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:
SIM := vcsNames 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 $( ):
SIM := vcs
show:
@echo "the simulator is $(SIM)"make showthe simulator is vcsMake 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:
show:
@echo "[$(NOT_DEFINED)]"[]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:
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:
make
make 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.
make build/design.listmake: '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:
A := some text # simple assignment: work the value out NOW
B = some text # recursive assignment: work the value out EACH TIME B is usedWhen 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:
LATE = $(EXTRA)
EARLY := $(EXTRA)
EXTRA = -sverilog
show:
@echo "recursive = : [$(LATE)]"
@echo "simple := : [$(EARLY)]"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 :=:
BASE := alu
NAME := $(BASE)_tb
BASE := regfile
show:
@echo "NAME is [$(NAME)] (BASE is now [$(BASE)])"NAME is [alu_tb] (BASE is now [regfile])With =:
BASE = alu
NAME = $(BASE)_tb
BASE = regfile
show:
@echo "NAME is [$(NAME)] (BASE is now [$(BASE)])"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.
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.
TOP ?= default_top
SIM := vcs
SIM ?= xcelium
show:
@echo "TOP=[$(TOP)] SIM=[$(SIM)]"make showTOP=[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:
make show TOP=axi_tbTOP=[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:
FLAGS := -full64
FLAGS += -sverilog
FLAGS += -debug_access
show:
@echo "[$(FLAGS)]"[-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:
NEW += first
NEW += second
show:
@echo "[$(NEW)]"[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:
EXTRA = -gui
SIMPLE := -full64
SIMPLE += $(EXTRA)
RECUR = -full64
RECUR += $(EXTRA)
EXTRA = -nogui
show:
@echo "simple := then += : [$(SIMPLE)]"
@echo "recursive = then += : [$(RECUR)]"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:
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)]"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_DIRisbuild/— it was frozen beforeTESTexisted, so the test name is missing and the path ends in a bare slash.LOGis/run.log— frozen beforeRUN_DIRexisted, 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:
BUILD = build
LOG = $(RUN_DIR)/run.log
RUN_DIR = $(BUILD)/$(TEST)
TEST ?= smokeRUN_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:
BUILD := build
TEST ?= smoke
RUN_DIR := $(BUILD)/$(TEST)
LOG := $(RUN_DIR)/run.logRUN_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:
make show TEST=axi_burstRUN_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.
| Operator | Name | Value is worked out | Use it when |
|---|---|---|---|
:= | simple | once, where it is written | almost always — paths, file lists, tool names |
= | recursive | every time it is used | the value must depend on something set later |
?= | conditional | once, if unset | offering a default a user may override |
+= | appending | follows the original flavour | building 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:
.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:
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 [compile] top=tb_top
make: Nothing to be done for 'all'.
[clean]
[compile] top=alu_tb
[compile] top=tb_topRead 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:
SOURCESis 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.BUILDis one edit if the output directory ever moves.TOPis 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:
.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:
DIR := build
show:
@echo "path=[$(DIR)/sim.img]"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:
A = $(A) xMakefile: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.
.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 —$NAMEis 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.
