Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
1. in the Makefile
TOP := tb_top
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 2. in the environment of the shell that runs make
export TOP=env_top
make

# 3. on the make command line
make TOP=cli_top

They 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.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show TOP=cli_top

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make run TEST=axi_burst SEED=42

5. The Precedence Ladder

Now the part worth memorising. When more than one source provides a value, this is the order, measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
TOP := file_top

show:
	@echo "TOP=[$(TOP)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TOP=env_top make show                 # environment vs Makefile
TOP=env_top make show TOP=cli_top     # all three at once
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
TOP=[file_top]
TOP=[cli_top]

So, from strongest to weakest:

  1. The command line — make TOP=cli_top. Wins over everything.
  2. The Makefile — TOP := file_top.
  3. The environment — TOP=env_top make. Weakest.
Three sources on the left feeding one result on the right. Strongest, a command-line assignment such as make TOP equals cli underscore top. Middle, a definition written in the Makefile. Weakest, a value inherited from the shell environment. All three feed the single value Make ends up using, and the strongest source present decides it.make TOP=...command line, strongestTOP := ...in the MakefileTOP=... makeenvironment, weakestvalue usedstrongest one wins12
Figure 1 — three sources can set the same variable, and the strongest one decides the value Make uses. A command-line assignment beats a definition in the Makefile, which in turn beats a value inherited from the shell. The make -e flag swaps the lower two, which is the only way the order changes.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TOP=env_top make show        # normal
TOP=env_top make -e show     # with -e
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
TOP=[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.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
TOP ?= default_top

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

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
override TOP := forced_top

show:
	@echo "TOP=[$(TOP)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show
make show TOP=cli_top
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
TOP=[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 MakefileIn the environmentOn the command lineTOP becomesWhy
nothing——emptyundefined is empty, not an error
nothingenv_top—env_topthe environment is the only source
:= file_top——file_topthe only source
:= file_topenv_top—file_topthe Makefile outranks the environment
:= file_topenv_topcli_topcli_topthe command line outranks both
?= default_top——default_topnothing had set it
?= default_topenv_top—env_topan environment value counts as already set
override := forcedenv_topcli_topforcedoverride 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
FROM_FILE := file_value

show:
	@echo "Make-expanded:    [$(FROM_FILE)]"
	@echo "shell environment: [$${FROM_FILE:-<unset>}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
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 OUTER was exported before you ran Make, a recipe can read $$OUTER.
  • Anything assigned on the command line. Measured, with no export line anywhere:
Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
TOP ?= tb_top

show:
	@echo "make=[$(TOP)] shell=[$${TOP:-<unset>}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make show
make show TOP=axi_tb
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
make=[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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — export at the point of definition
export FROM_FILE_EXPORTED := exported_value

show:
	@echo "[$${FROM_FILE_EXPORTED:-<unset>}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
[exported_value]
Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — export an already-defined variable
PROJECT_ROOT := /work/chip
export PROJECT_ROOT

show:
	@echo "shell sees: [$${PROJECT_ROOT:-<unset>}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
export SHARED := visible
unexport SECRET
SECRET := hidden
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
SECRET=from_env make show
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
SHARED=[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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
show:
	@echo "CURDIR        = [$(CURDIR)]"
	@echo "MAKE          = [$(MAKE)]"
	@echo "MAKEFILE_LIST = [$(MAKEFILE_LIST)]"
	@echo ".DEFAULT_GOAL = [$(.DEFAULT_GOAL)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output — paths will be yours
CURDIR        = [/work/chip/make-intro]
MAKE          = [/usr/bin/make]
MAKEFILE_LIST = [ Makefile]
.DEFAULT_GOAL = [show]
  • CURDIR is the directory Make is running in. Exporting it gives scripts a reliable project root instead of a hard-coded path — §10 does exactly that.
  • MAKE is the path to the running make itself, used when one Makefile invokes another.
  • MAKEFILE_LIST names 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_GOAL holds the goal from Chapter 2.4, and reading it is a quick way to confirm which target a bare make would 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — portable stand-in, executed as shown
.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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make run
make run TEST=axi_burst SEED=42
TEST=env_test SEED=7 make vars
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output — the PROJECT_ROOT path will be yours
  [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:

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

Which makes the flow usable the way a team actually uses one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make run                                  # the defaults
make run TEST=axi_burst SEED=42           # one specific test and seed
make run SIM=xrun                         # a different simulator

Nobody 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — as given
.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.sh
Azvya Education Pvt. Ltd.VLSI Mentor
scripts/report.sh
echo "report: TEST=[${TEST:-<unset>}] SEED=[${SEED:-<unset>}]"

Predict all four, then run them. At least one will probably surprise you.

  1. make report
  2. make report TEST=axi_burst
  3. TEST=env_test make report
  4. make 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 -e flips 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.
  • override lets 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; $$VAR is 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.
  • export is the bridge. Use it for the variables a script or tool reads from the environment, and only those.
  • Print the knobs. A small vars target 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.