Skip to content
VLSI Mentor

Makefile · Part I · Module 1 · Introduction to Makefiles

Makefile Syntax and Execution Basics

You can now write a working Makefile. This chapter explains the mechanics underneath it, because the puzzling errors all come from one place: a Makefile is read by two different programs, and the tab character is the boundary between them. Once you know which reader owns which line, a missing separator, a cd that does nothing and a dollar sign that eats a letter all stop being mysteries.

Foundation20 min readMakefileGNU MakePart ISyntaxShell

Part I · Module 1 · Chapter 1.4 · Makefile Syntax and Execution Basics

1. Two Readers, One File

Here is the idea the rest of this chapter hangs on.

A Makefile is not read by one program. It is read by two, and they do not share a language:

  • Make reads the structural lines — the targets, the colons, the prerequisites, the comments. It is looking for rules.
  • A shell reads the recipe lines. Make does not execute them itself; it hands each one to /bin/sh and waits.

So a Makefile contains two syntaxes side by side, and the question "is this an error in my Makefile or in my shell command?" has a real answer every time. The boundary between them is a single character: the tab at the start of a recipe line.

That is also why Make's own error messages can seem unhelpful. missing separator is Make saying I was trying to read this as a rule and it was not one — it knows nothing about your intent, only that the line did not parse on its side of the boundary.

Keep a scratch directory open as you read (Chapter 1.3 set one up); every Makefile here is two to six lines and worth running.

2. When Make Reads, and When It Runs

Make works in two distinct passes, and confusing them causes a specific wrong expectation.

First it reads the entire Makefile and builds an internal map of every rule it finds. Only then does it pick a target and start the four-step procedure from Chapter 1.2.

The practical consequence is that a rule may refer to a target defined further down the file:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
all: later
	@echo "all ran last"

later:
	@echo "later ran first, though it is defined below"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
later ran first, though it is defined below
all ran last

all names later as a prerequisite before later exists as far as a line-by-line reader is concerned — and it works, because by the time anything is built the whole file has been read. A Makefile is a set of rules, not a script that runs top to bottom. The only thing position decides is which target is the default, which is §11.

3. Rule Lines and Recipe Lines

Every line in a Makefile is one of a few kinds, and Make tells them apart almost entirely by how the line begins.

The lineMake treats it asWho reads it
target: prerequisitesa ruleMake
starts with a tab, inside a rulea recipe linea shell
starts with #a commentnobody
emptya separatornobody

The second row is the one to internalise: a tab-indented line is a command, and a line that is not tab-indented is not. Make does not look at whether the text resembles a shell command; it looks at the indentation. That single rule explains the next three sections.

4. The Tab, and the One Error It Produces

A recipe line must begin with a literal tab character. Not four spaces, not eight — a tab.

Indent with spaces and you get this:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — indented with spaces
hello:
    @echo "Hello"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
Makefile:2: *** missing separator.  Stop.

Make read line 2, found it was not tab-indented, and therefore tried to parse it as a rule — a line like target: prerequisites. There is no colon in it, so it is not a valid rule either, and missing separator is Make reporting that it could not find the : it needed.

That explanation matters because the same message has a second cause that has nothing to do with tabs — a rule line where you simply forgot the colon:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — the colon is missing
badrule
	@echo "hi"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
Makefile:1: *** missing separator.  Stop.

Same message, different line, different mistake. So when you see missing separator, go to the line number it names and ask which side of the boundary that line was meant to be on: if it is a command, it needs a tab; if it is a rule, it needs a colon.

5. Comments and Blank Lines

A line whose first non-blank character is # is a Make comment. Make strips it, and nothing else ever sees it:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
# Build the module inventory. No shell ever sees this line.

greet:
	@echo "hello"

Blank lines are separators and are otherwise ignored, which is why the Makefiles in this module put one between rules. A blank line does not end a recipe in a way that matters here: a recipe continues through blank lines until the next non-tab-indented line.

Now the caveat, which follows directly from §3. A # on a tab-indented line is inside a recipe, so Make does not treat it as a comment at all — it hands the whole line to the shell, # and everything:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
greet:
	echo "hello"       # trailing comment
	# a tab-indented comment line
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
echo "hello"       # trailing comment
hello
# a tab-indented comment line

Read that output carefully. Make echoed both recipe lines verbatim, including the # text, and the shell then treated each # as its own comment syntax and ignored the rest. The second line is a complete recipe line that happens to do nothing — a shell was started for it and exited.

So a comment inside a recipe works by accident of the shell agreeing with Make about #. It is still a command being run. For a comment that costs nothing, put it on an unindented line above the rule.

6. One Shell Per Recipe Line

This is the behaviour that surprises people most, and it is worth producing yourself.

Make starts a separate shell for each recipe line. Line one runs in one shell, which then exits; line two runs in a brand-new shell that knows nothing about what line one did to its own environment.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
where:
	pwd
	cd sub
	pwd

Create a sub directory first (mkdir sub), then run make where. If a Makefile were a script, the second pwd would print the subdirectory:

Azvya Education Pvt. Ltd.VLSI Mentor
Representative output — your absolute path will differ
pwd
/home/engineer/make-syntax
cd sub
pwd
/home/engineer/make-syntax

The cd succeeded. It simply had no lasting effect, because the shell it changed the directory in exited immediately afterwards, and the third line got a fresh shell starting in Make's own working directory.

Nothing failed, which is what makes this worth knowing: a recipe written this way reports success while doing the work in the wrong place.

Three rows, each flowing left to right. Row one: recipe line pwd is handed to shell one, which prints the project directory and exits. Row two: recipe line cd sub is handed to shell two, which changes directory and exits, discarding the change. Row three: recipe line pwd is handed to shell three, which starts fresh in the project directory and prints it again.pwdrecipe line 1shell 1prints, then exitscd subrecipe line 2shell 2cd lost on exitpwdrecipe line 3shell 3starts fresh12
Figure 1 — why the directory change does not survive. Each recipe line is handed to its own shell, which exits when the line finishes. A change a line makes to its shell — the working directory, a shell variable, an entered directory — dies with that shell; only changes to the filesystem outlive it.

The rule to carry: anything that must persist across recipe lines has to happen on one line. Files are the exception — a file written by line one is on disk for line two, because the filesystem is not part of the shell.

7. Keeping Work on One Line

Two ways to do it, and they are not equivalent.

Join the commands with &&. One recipe line, one shell, and the second command runs only if the first succeeded:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
where:
	cd sub && pwd
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
cd sub && pwd
/home/engineer/make-syntax/sub

Or continue the same line across the file with a backslash. A backslash as the last character of a line joins it to the next, before any shell is involved:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
where:
	cd sub && \
	pwd
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
cd sub && \
	pwd
/home/engineer/make-syntax/sub

Both reach the subdirectory, because both are one recipe line as far as Make is concerned. The echo in the second case shows the continuation and the tab still in the text — proof that Make passed the joined line through rather than tidying it up.

Prefer && over ; when joining. With ; the second command runs whether or not the first worked, so cd nonexistent; rm -f stale.log deletes a file in whatever directory you happened to be in. That is the shape of a genuinely destructive accident.

A backslash means "continue this line" in both Make's syntax and the shell's, which is the one place the two readers agree. Everywhere else, assume they do not.

8. The Dollar Sign Belongs to Make First

Here is the sharpest example of the two-readers idea, and a trap that produces output nobody expects.

$ is Make's own character. Make expands anything beginning with $ before the shell ever sees the line. A shell variable written the normal way therefore gets eaten:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
one:
	@echo "single dollar: $HOME"
two:
	@echo "double dollar: $$HOME"
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output — the home directory is the authoring machine's
single dollar: OME
double dollar: /home/engineer

Look at the first line. Make read $H as the value of the Make variable named H, which does not exist, and substituted nothing. What reached the shell was echo "single dollar: OME" — the $H vanished and the surviving OME is the rest of the word. No error, because nothing is wrong on either side: Make expanded a variable it did not have, and the shell printed the string it was given.

$$ is how you write a literal dollar for the shell. Make turns $$ into $, and the shell receives $HOME.

The same applies to anything using a dollar in shell syntax — $1 in a loop, $(command substitution), $?. In a recipe they all need doubling. This is also why Chapter 1.3 extracted module names with cut rather than awk '{print $2}': written in a Makefile, that $2 would have been expanded by Make and the awk program would have arrived empty.

Defining and using Make's own variables is Module 3. All that is needed here is the boundary: a $ in a recipe is addressed to Make unless you double it.

9. Echoing, and the @ Prefix

By default Make prints each recipe line before running it. That is why the outputs above show the command and then its result — the first is Make, the second is the command.

A @ at the start of a recipe line suppresses the printing of that line:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
greet:
	@echo "quiet: only the result appears"
	echo "loud: the command appears too"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
quiet: only the result appears
echo "loud: the command appears too"
loud: the command appears too

Neither is more correct. Echoing is valuable in a flow whose log somebody will read later, because the log then records exactly what ran — which is why the illustrative simulator recipes in this module leave it on, and why the @echo "building ..." progress lines use @ to avoid printing the word echo twice.

The @ affects printing only. It changes nothing about how the command runs, whether it fails, or what Make does next.

10. When a Recipe Command Fails

Make checks the exit status of every recipe line. A non-zero status stops that rule immediately:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
check:
	@echo "step 1"
	@false
	@echo "step 3 — should NOT appear"
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
step 1
make: *** [check] Error 1

false is a command whose only job is to exit non-zero. Step 3 never ran, and make itself exited with status 2 — so a script or CI job that checks make's status sees the failure.

The same applies across rules. If a prerequisite's recipe fails, the target that needed it does not run:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
all: stage1
	@echo "all — should NOT appear"

stage1:
	@echo "stage1 running"
	@exit 3
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
stage1 running
make: *** [stage1] Error 3

The number in the message is the command's own exit status, which is often a usable clue about which tool failed and how.

This default is the behaviour you want from a flow: a simulation that runs after a failed compile produces a result that looks real. The ways to deliberately tolerate a failure, and the harder problem of a tool that fails while reporting success, are Chapter 4.3.

11. Which Target Runs by Default

Three sentences, because this is Chapter 2.4's subject.

With no target on the command line, Make builds the first target in the file — not the one named all, not the most important-looking one:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
first:
	@echo "first ran"

all:
	@echo "all ran"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
first ran

Position in the file is the only thing that decides this, which is why most real Makefiles put a deliberate aggregate target at the very top. Choosing it on purpose, rather than by accident of ordering, is Chapter 2.4.

12. Reading Without Running: make -n

Before letting an unfamiliar Makefile run tools on your files, you can ask Make what it intends to do. make -n, also spelled --just-print, prints the recipe lines it would execute and runs none of them:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
out.txt:
	@echo "building out.txt"
	echo "data" > out.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make -n out.txt
ls
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
echo "building out.txt"
echo "data" > out.txt
Makefile

Nothing was created — ls shows only the Makefile. Two details are worth noticing.

It shows the @ lines too. -n is a listing of what would run, so the suppression from §9 does not apply. That makes it the quickest way to see what a quiet recipe actually contains.

It is not a sandbox. A recipe line prefixed with + is executed even under -n, by design — the mechanism exists so that recursive make invocations still work during a dry run:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
probe:
	@echo "normal line"
	+touch side-effect.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
echo "normal line"
touch side-effect.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ls
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — the file was created despite -n
Makefile	side-effect.txt

So read -n as "what Make plans to hand to a shell", not as "nothing can happen". On a Makefile you did not write, that distinction is worth remembering. The fuller diagnostic flags — tracing why a target was considered out of date, and dumping Make's internal view — are Chapter 4.4.

13. The Three Syntax Errors You Will Actually Hit

All three were produced on GNU Make 3.81; other versions word them slightly differently.

missing separator — §4. Either a recipe line without its tab, or a rule line without its colon. Go to the named line and decide which it was meant to be.

commands commence before first target — a tab-indented line appears before any rule has been declared:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — a recipe with no rule above it
	@echo "orphan"
hello:
	@echo "hi"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
Makefile:1: *** commands commence before first target.  Stop.

Make found a command with no target to attach it to. Usually a stray indented line, or a rule line that was accidentally indented and so stopped being a rule.

extraneous 'endif' — a directive closed without being opened:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
Makefile:3: *** extraneous `endif'.  Stop.

This one means a structural keyword is unbalanced. Directives such as ifeq and endif are Chapter 4.2; the message is here so you recognise it as Make complaining about structure, not about any command.

Notice the shape all three share: Make names a file and line number, and the complaint is always about its own side of the boundary. A message with no line number, or one naming a target and an Error N, came from a command instead — that is §10.

14. A Complete Example

One Makefile that uses the whole chapter: a Make comment, both echo modes, a multi-line recipe that needs its work joined, a shell variable that needs doubling, and a second rule whose check can fail.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — every mechanism from this chapter
# stage — assemble a small build directory and record what went into it.
# This line is a Make comment: no shell ever sees it.

stage:
	@echo "preparing build/"
	@mkdir -p build
	cd build && echo "home is $$HOME" > stamp.txt
	@echo "wrote build/stamp.txt"

verify: stage
	@test -s build/stamp.txt
	@echo "stamp.txt exists and is not empty"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make verify
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output — the home directory will be yours
preparing build/
cd build && echo "home is $HOME" > stamp.txt
wrote build/stamp.txt
stamp.txt exists and is not empty

Five things in that output are worth naming, because each is a mechanism from this chapter:

  1. The comment never appears. Make stripped it; no shell was involved.
  2. Three lines are quiet and one is loud. The @ lines printed only their results; the cd line printed itself first.
  3. The echoed line shows $HOME, not $$HOME. Make echoes the line after its own expansion, so what you see is what the shell received — a useful way to check your doubling.
  4. The cd worked because the redirect that depended on it is on the same line, inside the same shell.
  5. verify ran after stage and its test -s succeeded silently, so the final message is proof the check passed rather than decoration.

Now break it. Change stage so it creates an empty file instead of writing to it — replace the cd build && ... line with @: > build/stamp.txt — and run make verify again:

Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
make: *** [verify] Error 1

test -s fails on an empty file, so verify stopped before its message. Nothing printed about why, which is the honest cost of @ on a checking command: quiet is good for a passing check and unhelpful for a failing one. That trade-off, and the ways to make a failure explain itself, are Chapter 4.3.

15. Exercises

Question 1. A colleague's recipe is two lines: cd results then rm -f *.log. They report that it sometimes deletes the wrong files. Explain what actually happens, and give the one-line fix.

Question 2. A recipe line reads @echo "user is $USER" and prints user is SER. What did Make do, and what should the line say?

Question 3. A Makefile fails with Makefile:7: *** missing separator. Stop. Line 7 is build/alu.o: src/alu.c. The tabs elsewhere in the file are fine. What is wrong with line 7?

Question 4. You run make -n deploy on an unfamiliar Makefile to see what it would do, and a file is created anyway. What explains it, and what does that tell you about using -n as a safety check?

16. Remember

  • A Makefile has two readers. Make reads the unindented structural lines; a shell reads the tab-indented recipe lines. The tab is the boundary.
  • Make reads the whole file first, then builds. A rule may name a target defined further down; only the default target depends on position.
  • missing separator has two causes — a recipe line without a tab, or a rule line without a colon. The line number tells you where; you decide which side of the boundary that line belongs to.
  • A # on a tab-indented line is not a Make comment. It is a command, passed to a shell that happens to ignore it too.
  • Each recipe line gets its own shell. cd, shell variables and anything else held in a shell's own state do not survive to the next line; files do.
  • Join work with && on one line, or continue the line with a backslash. Prefer && to ; so a failed first command cannot let the second run.
  • $ is Make's character. $HOME in a recipe is a Make variable called H followed by OME; write $$HOME for the shell.
  • @ suppresses the echo of a line and nothing else. make -n ignores it.
  • A non-zero exit status stops the rule, and anything depending on it does not run. Make exits non-zero too.
  • make -n shows intent, not safety. It prints @ lines and still executes + lines.

That completes Module 1. You can read a rule, predict whether Make will run it, write a Makefile from an empty file, and explain the mechanics behind every line Make prints. Module 2 goes back to the dependency graph with that foundation in place — what a target really is, the .PHONY declaration that keeps an action target working, how the timestamp comparison behaves at its edges, and how to choose the default goal on purpose.

Where this fits

Part of the Makefile curriculum.