Makefile · Part I · Module 3 · Variables and Make Functions
Environment Variables and Command-Line Overrides
A flow is only useful if other people can point it at their own test, their own seed, their own simulator — without editing your Makefile. That means a variable's value sometimes comes from outside the file entirely. This chapter covers the three places a value can come from, which one wins when they disagree, and a boundary that catches almost everyone: the variables Make knows about are not the same set the recipe's shell can see.
Foundation20 min readMakefileGNU MakePart IVariablesEnvironment
Part I · Module 3 · Chapter 3.2 · Environment Variables and Command-Line Overrides
1. Values That Do Not Belong in the File
Chapter 3.1 ended with TOP ?= tb_top — a default that something outside could replace. That is the normal shape of a usable flow, because some values are genuinely not the Makefile's business:
- which test to run — different for every engineer, every hour;
- which random seed — the whole point is that it varies;
- which simulator — a team decision, or a machine's;
- where the project lives — a path that differs per checkout.
Hard-coding those forces everybody to edit the Makefile, which means the file is modified constantly and the modifications get committed by accident. The alternative is to let the value arrive from outside.
There are exactly two outsides: the shell environment Make was launched from, and the make command line itself.
2. Three Sources for One Variable
So a single variable name can be given a value in three places:
TOP := tb_top# 2. in the environment of the shell that runs make
export TOP=env_top
make
# 3. on the make command line
make TOP=cli_topThey can all be in play at once, and then Make has to choose. Sections 3 to 5 take them one at a time and then state the order.
3. A Value From the Environment
Any environment variable in the shell that launches Make is automatically available as a Make variable. You do not declare it or import it.
show:
@echo "TOP=[$(TOP)]"make show
TOP=env_top make showTOP=[]
TOP=[env_top]Nothing in the Makefile mentions TOP, and the second run still sees it. The TOP=env_top make show form sets the variable for that one command only, which is the handy way to try something without changing your shell.
This is convenient and also the reason a flow can behave differently for two engineers on the same commit: one of them has something set in their shell profile. §11 comes back to that.
4. A Value From the Command Line
The second outside source is an assignment written as a make argument:
make show TOP=cli_topThe assignment can appear before or after the target name; Make recognises anything containing = as a variable assignment rather than a goal. This is the normal way to drive a parameterised flow:
make run TEST=axi_burst SEED=425. The Precedence Ladder
Now the part worth memorising. When more than one source provides a value, this is the order, measured:
TOP := file_top
show:
@echo "TOP=[$(TOP)]"TOP=env_top make show # environment vs Makefile
TOP=env_top make show TOP=cli_top # all three at onceTOP=[file_top]
TOP=[cli_top]So, from strongest to weakest:
- The command line —
make TOP=cli_top. Wins over everything. - The Makefile —
TOP := file_top. - The environment —
TOP=env_top make. Weakest.
That the Makefile beats the environment is deliberate on Make's part: a file that says BUILD := build should not silently start writing somewhere else because a colleague has BUILD set in their shell profile.
If you ever want the opposite, make -e gives the environment priority over the Makefile:
TOP=env_top make show # normal
TOP=env_top make -e show # with -eTOP=[file_top]
TOP=[env_top]-e is worth knowing so you recognise it, and worth avoiding in practice — it makes a build depend on whatever happens to be exported in the surrounding shell, which is the opposite of reproducible.
6. How ?= Fits In
Chapter 3.1 introduced ?= as "set this only if nothing has set it yet". Now the obvious question: does a value from the environment count as "already set"?
Yes.
TOP ?= default_top
show:
@echo "TOP=[$(TOP)]"make show
TOP=env_top make showTOP=[default_top]
TOP=[env_top]That is exactly what you want from a default. TOP ?= tb_top means "tb_top unless somebody — shell or command line — has an opinion," and both outsides can express one.
This is why ?= and := are not interchangeable for a knob. Written TOP := tb_top, the environment would be ignored and only a command-line assignment could override it. Written TOP ?= tb_top, both work.
7. When the Makefile Must Insist: override
Occasionally a value must not be overridable — a path the flow depends on, or an option that would break the build if removed. The override directive makes a Makefile definition win against even the command line:
override TOP := forced_top
show:
@echo "TOP=[$(TOP)]"make show
make show TOP=cli_topTOP=[forced_top]
TOP=[forced_top]The command-line assignment was ignored. Use this sparingly and with a comment: somebody will eventually type make TOP=something, watch it have no effect, and need to know why. A more common and friendlier pattern is to append something mandatory to a user-supplied list rather than replacing it wholesale.
That is every mechanism for deciding a value. Here they are in one table — the same variable TOP, under eight combinations, each row a measured result:
| In the Makefile | In the environment | On the command line | TOP becomes | Why |
|---|---|---|---|---|
| nothing | — | — | empty | undefined is empty, not an error |
| nothing | env_top | — | env_top | the environment is the only source |
:= file_top | — | — | file_top | the only source |
:= file_top | env_top | — | file_top | the Makefile outranks the environment |
:= file_top | env_top | cli_top | cli_top | the command line outranks both |
?= default_top | — | — | default_top | nothing had set it |
?= default_top | env_top | — | env_top | an environment value counts as already set |
override := forced | env_top | cli_top | forced | override outranks even the command line |
Read down the "why" column and the whole chapter so far is three sentences: the command line wins, the Makefile beats the environment, and ?= steps aside for either outside source.
8. Make's Variables Are Not the Shell's Environment
Here is the boundary that surprises people, and it follows directly from Chapter 1.4: Make expands $(VAR) itself, then hands the finished text to a shell.
So there are two different places a name can live, and they are not the same place:
- Make's variable table — everything defined in the Makefile, inherited from the environment, or set on the command line. Read with
$(VAR). - The recipe shell's environment — what a program started by the recipe can read. Read with
$$VAR.
A plain Makefile definition is in the first and not the second:
FROM_FILE := file_value
show:
@echo "Make-expanded: [$(FROM_FILE)]"
@echo "shell environment: [$${FROM_FILE:-<unset>}]"Make-expanded: [file_value]
shell environment: [<unset>]Same variable, two answers. $(FROM_FILE) worked because Make substituted the text before the shell ran. $$FROM_FILE — which reaches the shell as $FROM_FILE — found nothing, because Make never put it in the shell's environment.
Two things are in the shell's environment automatically:
- Anything inherited from the calling shell. If
OUTERwas exported before you ran Make, a recipe can read$$OUTER. - Anything assigned on the command line. Measured, with no
exportline anywhere:
TOP ?= tb_top
show:
@echo "make=[$(TOP)] shell=[$${TOP:-<unset>}]"make show
make show TOP=axi_tbmake=[tb_top] shell=[<unset>]
make=[axi_tb] shell=[axi_tb]The default stayed inside Make; the command-line override reached the shell as well. That asymmetry is worth knowing, because it means a tool that reads an environment variable will see a command-line override but not an in-file default.
This distinction matters most when a recipe runs a script rather than a single command. The script is a separate program: it cannot see $(VAR), only the environment. So either pass the value as an argument, or export it.
9. export: Putting a Value in the Recipe's Environment
export tells Make to place a variable in the environment of every recipe shell. Two forms, both verified:
export FROM_FILE_EXPORTED := exported_value
show:
@echo "[$${FROM_FILE_EXPORTED:-<unset>}]"[exported_value]PROJECT_ROOT := /work/chip
export PROJECT_ROOT
show:
@echo "shell sees: [$${PROJECT_ROOT:-<unset>}]"shell sees: [/work/chip]Export a variable when something downstream reads the environment rather than the command line — a helper script, or a tool that is configured that way. Do not export everything by habit: the environment is shared with every program the recipe starts, and an unexpected variable there can change a tool's behaviour.
The opposite directive exists too. unexport keeps a variable out of the recipe environment, and it works even on one that arrived from the calling shell:
export SHARED := visible
unexport SECRET
SECRET := hiddenSECRET=from_env make showSHARED=[visible] SECRET=[<unset>]SECRET was set in the calling shell and still did not reach the recipe. That is occasionally useful when a tool misbehaves because of something in the surrounding environment.
Variables Make defines for you
A few variables exist without you writing them, and two are worth knowing now:
show:
@echo "CURDIR = [$(CURDIR)]"
@echo "MAKE = [$(MAKE)]"
@echo "MAKEFILE_LIST = [$(MAKEFILE_LIST)]"
@echo ".DEFAULT_GOAL = [$(.DEFAULT_GOAL)]"CURDIR = [/work/chip/make-intro]
MAKE = [/usr/bin/make]
MAKEFILE_LIST = [ Makefile]
.DEFAULT_GOAL = [show]CURDIRis the directory Make is running in. Exporting it gives scripts a reliable project root instead of a hard-coded path — §10 does exactly that.MAKEis the path to the runningmakeitself, used when one Makefile invokes another.MAKEFILE_LISTnames the makefiles read so far, which is handy when includes are involved. Note the leading space in the value — it is a list that gets appended to..DEFAULT_GOALholds the goal from Chapter 2.4, and reading it is a quick way to confirm which target a baremakewould build.
10. The Knob Pattern in an RTL Flow
Here is the whole chapter assembled into the shape real verification Makefiles use. This version is portable and was executed, standing in for a simulator:
.PHONY: all run clean vars
.DEFAULT_GOAL := all
BUILD := build
SOURCES := rtl/alu.v rtl/tb_top.v
TOP ?= tb_top
SEED ?= 1
TEST ?= smoke
export PROJECT_ROOT := $(CURDIR)
all: $(BUILD)/sim.img
$(BUILD)/sim.img: $(SOURCES)
@echo " [compile] top=$(TOP)"
@mkdir -p $(BUILD)
@cat $(SOURCES) > $(BUILD)/sim.img
run: $(BUILD)/sim.img
@echo " [run] test=$(TEST) seed=$(SEED)"
@echo " [run] recipe shell sees PROJECT_ROOT=[$${PROJECT_ROOT:-<unset>}]"
@echo "test=$(TEST) seed=$(SEED)" > $(BUILD)/run.log
vars:
@echo "TOP=[$(TOP)] SEED=[$(SEED)] TEST=[$(TEST)]"
clean:
@rm -rf $(BUILD)$(CURDIR) is one of Make's own built-in variables: the directory Make is running in. Exporting it gives every helper script a reliable project root without anybody hard-coding a path.
Three invocations:
make run
make run TEST=axi_burst SEED=42
TEST=env_test SEED=7 make vars [compile] top=tb_top
[run] test=smoke seed=1
[run] recipe shell sees PROJECT_ROOT=[/work/chip/make-intro]
[run] test=axi_burst seed=42
[run] recipe shell sees PROJECT_ROOT=[/work/chip/make-intro]
TOP=[tb_top] SEED=[7] TEST=[env_test]Reading those three: the defaults ran; the command line replaced two of them and the compile was correctly skipped the second time; and the environment supplied values in the third, because every knob uses ?=.
The simulator version is illustrative — vcs is not installed here and was not run:
.PHONY: all run clean
.DEFAULT_GOAL := all
BUILD := build
SOURCES := rtl/alu.v rtl/tb_top.v
SIM ?= vcs
TOP ?= tb_top
SEED ?= 1
TEST ?= smoke
all: $(BUILD)/simv
$(BUILD)/simv: $(SOURCES)
mkdir -p $(BUILD)
$(SIM) -full64 -sverilog -top $(TOP) $(SOURCES) -o $(BUILD)/simv
run: $(BUILD)/simv
./$(BUILD)/simv +UVM_TESTNAME=$(TEST) +ntb_random_seed=$(SEED) -l $(BUILD)/run.logWhich makes the flow usable the way a team actually uses one:
make run # the defaults
make run TEST=axi_burst SEED=42 # one specific test and seed
make run SIM=xrun # a different simulatorNobody edits the Makefile to do any of that, which is the entire point.
11. Common Mistakes
Using := for a knob. TOP := tb_top cannot be overridden from the environment — only from the command line. If a value is meant to be configurable, write ?=.
Expecting a recipe's script to see $(VAR). A separate program reads the environment, not Make's table (§8). Pass the value as an argument, or export it.
Forgetting the $$ for a shell variable. $HOME in a recipe is Make's variable H followed by OME — Chapter 1.4 demonstrated it. Write $$HOME.
Letting a shell profile configure the build. If a flow reads TOP from the environment and an engineer has TOP exported in their login script, their builds differ from everyone else's and nothing in the repository explains why. Prefer explicit command-line overrides for anything that affects a result, and print the values that matter — a vars target like §10's makes the difference visible in one command.
Reaching for make -e. It makes the environment outrank the file, so the build is at the mercy of the surrounding shell. Avoid it.
Using override to win an argument. It silently ignores what the user typed. If you must, add a comment saying why, so the next person does not spend an afternoon on it.
Assuming an exported variable is private. It is in the environment of every command the recipe runs, including tools you did not write.
12. Check Your Understanding
Question 1. A Makefile has BUILD := build. An engineer has BUILD=/tmp/scratch exported in their shell. Where does the output go, and why?
Question 2. The same Makefile is changed to BUILD ?= build. Now where does that engineer's output go?
Question 3. A recipe runs python3 scripts/report.py, and the script reads an environment variable called RUN_DIR. The Makefile defines RUN_DIR := build/run1. The script reports it as unset. What is missing, and what are the two ways to fix it?
Question 4. A flow defines TOP ?= tb_top. An engineer runs make run TOP=axi_tb. Will a helper script started by the recipe see TOP in its environment? Justify your answer from §8.
13. Exercise
Set up rtl/alu.v and rtl/tb_top.v with one module each, plus this Makefile and this one-line script.
.PHONY: all report
.DEFAULT_GOAL := all
BUILD := build
SOURCES := rtl/alu.v rtl/tb_top.v
TEST := smoke
SEED ?= 1
all: $(BUILD)/sim.img
$(BUILD)/sim.img: $(SOURCES)
@mkdir -p $(BUILD)
@cat $(SOURCES) > $(BUILD)/sim.img
report:
@sh scripts/report.shecho "report: TEST=[${TEST:-<unset>}] SEED=[${SEED:-<unset>}]"Predict all four, then run them. At least one will probably surprise you.
make reportmake report TEST=axi_burstTEST=env_test make reportmake report SEED=42
14. Remember
- A variable's value can come from three places: the command line, the Makefile, or the environment Make was launched from.
- The precedence is command line, then Makefile, then environment. The file beating the environment is deliberate — a shell profile should not redirect somebody's build.
make -eflips the lower two. Recognise it; avoid it.- An environment value counts as "already set" for
?=, which is what makes?=the right operator for a knob and:=the wrong one. overridelets the Makefile beat even the command line. Use it rarely, and leave a comment.- Make's variable table is not the recipe shell's environment.
$(VAR)is Make;$$VARis the shell, and a plain Makefile definition is invisible to the second. - Two things reach the shell automatically: values inherited from the calling shell, and command-line assignments.
exportis the bridge. Use it for the variables a script or tool reads from the environment, and only those.- Print the knobs. A small
varstarget turns "why is this build different?" into one command.
You can now write a flow other people can drive without editing it. What is still repetitive is inside the rules themselves: every recipe still names its own target and its own prerequisites by hand. Chapter 3.3 removes that with the variables Make fills in for you.
Where this fits
Part of the Makefile curriculum.
