Skip to content
VLSI Mentor

Makefile · Part I · Module 2 · Rules and Dependencies

File Targets vs .PHONY Targets

Every Makefile mixes two kinds of target: names that stand for files and names that stand for jobs. Make has no separate syntax for them, which means the second kind works only by accident — it works while no file happens to share its name. This chapter reproduces the failure, fixes it with one declaration, and then shows the two situations where that same declaration quietly makes your builds slower instead of safer.

Foundation19 min readMakefileGNU MakePart IPHONYRules

Part I · Module 2 · Chapter 2.2 · File Targets vs .PHONY Targets

1. One Syntax, Two Meanings

Look at these two rules. They are the same shape, and Make parses them identically:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
report.txt: input.txt
	@cat input.txt > report.txt

clean:
	@rm -f report.txt

But they mean different things to the person who wrote them:

  • report.txt is a file target. The name stands for a file on disk that the recipe produces. Asking for it means "make sure that file is current."
  • clean is an action target. The name stands for a job. Nothing called clean is ever created. Asking for it means "do this now."

Make does not know which is which. It has exactly one procedure, and Chapter 2.1 showed it: resolve the name, look for a file with that name, decide. An action target works because the lookup finds nothing — not because Make understands that you meant a job.

That is a fragile foundation, and this chapter is about making it solid.

2. How Make Checks a File Target

For a file target the decision has two questions, both about the filesystem:

  1. Does a file with the target's name exist? If not, build it.
  2. Is any prerequisite newer than that file? If so, build it. Otherwise skip it.

Both questions are worth seeing answered. Put two lines in input.txt:

Azvya Education Pvt. Ltd.VLSI Mentor
input.txt
clk
rst_n
Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
report.txt: input.txt
	@echo "  [report.txt] building"
	@cat input.txt > report.txt

clean:
	@echo "removing generated files"
	@rm -f report.txt

Now four invocations, each hitting a different answer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make report.txt          # 1. the file does not exist yet
make report.txt          # 2. nothing has changed
# add a line to input.txt, then:
make report.txt          # 3. a prerequisite is newer
rm report.txt
make report.txt          # 4. the output is gone again
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output — Make 3.x quotes the name differently
  [report.txt] building
make: 'report.txt' is up to date.
  [report.txt] building
  [report.txt] building

Four runs, two builds skipped into one up to date, and every decision explained by the two questions: missing (1 and 4), unchanged (2), newer input (3).

Now contrast it with the action target in the same Makefile, run twice:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make clean
make clean
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
removing generated files
removing generated files

No up to date, ever. Question 1 finds no file named clean, so the answer is always "build it".

That contrast is the whole benefit of a file target — the second run costs nothing — and the whole character of an action target: it is meant to happen when asked. The precise rule for "newer", and the cases where it surprises people, is Chapter 2.3. What matters here is that both questions need a file. Take the file away and neither can be answered usefully.

3. Why an Action Target Is Fragile

An action target answers question 1 with "no file exists" every time, so it always runs. Watch what happens when that stops being true.

Start with a clean rule and nothing else:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
clean:
	@echo "removing generated files"
	@rm -f report.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make clean
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
removing generated files

Works. Now suppose something creates a file named clean in that directory — a stray touch, an editor backup, a script that writes a log called clean, or a directory called clean added by a colleague. Reproduce it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
touch clean
make clean
echo "exit status: $?"
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
make: 'clean' is up to date.
exit status: 0

The recipe did not run. Nothing was removed. Make exited successfully.

Trace it and nothing is mysterious: clean now resolves to a file; that file exists; the rule has no prerequisites, so nothing can be newer than it; therefore there is nothing to do. Make is correct on its own terms, and the flow is broken on yours.

This is the worst kind of failure — silent, with a zero exit status, in a target whose whole job is cleanup. A build system that reports "cleaned" and leaves stale artifacts behind will eventually hand you a result from the wrong run.

4. .PHONY Declares That a Name Is Not a File

The fix is one line. .PHONY is a special target whose prerequisites are the names you promise are not files:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
.PHONY: clean

clean:
	@echo "removing generated files"
	@rm -f report.txt

That declaration tells Make: when you resolve clean, do not look at the filesystem at all. There is no file behind this name, so never consider it up to date.

The file named clean from §3 is still sitting in the directory. Run it again:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ls
make clean
make clean
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
Makefile	clean
removing generated files
removing generated files

The recipe runs, every time, with the colliding file still present. The collision is no longer able to disable the target.

Three things .PHONY does not do, because each is a common misreading:

  • It does not delete the colliding file. clean is still there after both runs — Make simply stopped consulting it.
  • It does not change the recipe. The commands and their behaviour are untouched.
  • It does not make the target faster. It removes a filesystem check, which costs nothing either way.

.PHONY can list several names at once, and most real Makefiles keep one declaration near the top:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
.PHONY: all run test clean

5. A Phony Target Is Also Immune to the Opposite Problem

There is a second, rarer failure that .PHONY prevents, worth one paragraph because it explains the declaration's real meaning.

Without .PHONY, Make also applies its built-in rules to an unknown name — it has implicit knowledge about producing .o files from .c files and so on. A target name that happens to look like something Make knows how to build can therefore behave unexpectedly. Declaring a name phony removes it from all of that: the name has exactly one rule, yours, and no filesystem or built-in behaviour attaches to it.

So read .PHONY: clean as a statement about what kind of thing the name is, not as an instruction about when to run. That reading is what makes the next two sections predictable.

6. Which Targets Should Be Phony

A short, practical list. Mark a target phony when the name stands for a job and no file by that name is ever produced:

TargetPhony?Why
allyesan aggregate request; produces nothing itself
runyesperforms a simulation; the log it writes has a different name
testyesperforms a check
cleanyesremoves things; the name is not an output
helpyesprints usage
report.txtnoit is a real file the rule produces
build/sim.imgnolikewise

The rule of thumb: after a successful run, is there a file with exactly that name? If yes, it is a file target and must not be phony. If no, it is an action and should be.

A useful habit is to write the .PHONY line at the same moment you write the action rule, rather than adding it later after a collision has already cost someone an afternoon.

7. A Phony Prerequisite Forces Its Parent to Rebuild

Now the consequence that makes "always add .PHONY" bad advice. A phony target is never up to date — and that property propagates to anything that depends on it.

Here is a file target with an ordinary prerequisite and a phony one:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
.PHONY: prep

prep:
	@echo "  [prep] ran"

output.txt: input.txt prep
	@echo "  [output.txt] rebuilding"
	@cat input.txt > output.txt

With input.txt in place, run it three times without changing anything:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make output.txt
make output.txt
make output.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [prep] ran
  [output.txt] rebuilding
  [prep] ran
  [output.txt] rebuilding
  [prep] ran
  [output.txt] rebuilding

output.txt is rebuilt on every single run. It is a perfectly ordinary file target, its real input never changed, and it still rebuilds — because one of its prerequisites is phony, a phony target is always considered out of date, and "a prerequisite was rebuilt" means the target must be rebuilt too.

Remove prep from the prerequisite list and nothing else:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
.PHONY: prep

prep:
	@echo "  [prep] ran"

output.txt: input.txt
	@echo "  [output.txt] rebuilding"
	@cat input.txt > output.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output — first run, then second
  [output.txt] rebuilding
make: 'output.txt' is up to date.

Skipping is restored.

Two rows compared, each flowing left to right. Top row: input.txt, a real file, feeds output.txt, which can be skipped when nothing changed. Bottom row: input.txt and a second prerequisite named prep, which is phony, both feed output.txt, which is therefore rebuilt on every run.input.txtreal fileoutput.txtcan be skippedinput.txtreal fileprepphonyoutput.txtalways rebuilt12
Figure 1 — the same file target with and without a phony prerequisite. In the top row output.txt depends only on a real file, so Make can compare timestamps and skip the recipe. In the bottom row the extra phony prerequisite is never up to date, so output.txt is rebuilt on every invocation even though input.txt has not changed.

In a small project that is a wasted second. In an RTL flow it is a full recompile on every invocation, which is exactly what a Makefile was supposed to prevent. So: a phony target is safe to ask for, and consequential to depend on.

If what you want is "do this before the real work", a phony prerequisite is the wrong tool, because you have told Make the parent is permanently out of date. Chapter 2.4 covers expressing a genuine requirement as a dependency edge, and Chapter 4.2 covers the cases where the setup step should produce a real file — a created directory, a generated filelist — so that it can be compared like anything else.

8. Never Mark a Generated Artifact Phony

The second trap is the mirror image: declaring a real output file phony. It is usually done by someone who wanted to force a rebuild once and never took the line out.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — output.txt is a real file, wrongly declared phony
.PHONY: output.txt

output.txt: input.txt
	@echo "  [output.txt] rebuilding"
	@cat input.txt > output.txt

summary.txt: output.txt
	@echo "  [summary.txt] rebuilding"
	@wc -l < output.txt | tr -d ' ' > summary.txt

Ask for summary.txt three times with nothing changing:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [output.txt] rebuilding
  [summary.txt] rebuilding
  [output.txt] rebuilding
  [summary.txt] rebuilding
  [output.txt] rebuilding
  [summary.txt] rebuilding

Two failures at once, and the second is the expensive one:

  • output.txt always rebuilds. Declared phony, so its timestamp is never consulted — even though the file is created on disk perfectly well each time. Check with ls and it is right there; Make has simply been told not to look.
  • The damage cascades. summary.txt is an ordinary file target that was never declared phony, and it rebuilds too — because its prerequisite rebuilt, exactly as §7 described. One wrong .PHONY line disabled incremental rebuilding for the whole chain below it.

The honest way to force one rebuild is to ask Make to, at the moment you want it — make -B output.txt rebuilds unconditionally, or delete the file and run again. Neither leaves a permanent change in the Makefile for the next person to inherit.

9. The Split in an RTL Flow

Here is the structure done properly: artifacts are file targets, everything a person asks for is phony. 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 run test clean

all: build/sim.img

build/sim.img: src/alu.v src/tb_top.v
	@echo "  [compile] building build/sim.img"
	@mkdir -p build
	@cat src/alu.v src/tb_top.v > build/sim.img

run: build/sim.img
	@echo "  [run] simulating"
	@wc -l < build/sim.img | tr -d ' ' > run.log
	@cat run.log

test: run
	@echo "  [test] checking run.log"
	@test -s run.log && echo "  PASS"

clean:
	@echo "  [clean] removing build output"
	@rm -rf build run.log

Four phony names and one file target. Run the whole thing from clean:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make test
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [compile] building build/sim.img
  [run] simulating
4
  [test] checking run.log
  PASS

Now run it again with nothing changed:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [run] simulating
4
  [test] checking run.log
  PASS

The compile was skipped; the run and the check repeated. That is precisely the behaviour you want from a verification flow: re-running a test must not be able to silently skip itself, and recompiling an unchanged design must not cost twenty minutes.

Edit a source and the compile comes back:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [compile] building build/sim.img
  [run] simulating
5
  [test] checking run.log
  PASS

Two details are deliberate. run writes run.log, not a file called run — the artifact and the action have different names on purpose, which is what keeps run honestly phony. And clean uses rm -rf build run.log, naming exactly what the flow generates rather than a wildcard.

The simulator version of the same shape 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 test clean

all: build/simv

build/simv: src/alu.v src/tb_top.v
	mkdir -p build
	vcs -full64 -sverilog src/alu.v src/tb_top.v -o build/simv

run: build/simv
	./build/simv -l run.log

clean:
	rm -rf build run.log

Note what is not claimed here: build/simv depends on the two source files because they are named on the command line. A real flow drives the compile from a filelist, and Chapter 1.2 showed that naming the filelist as the prerequisite does not make Make notice an edit to a source listed inside it. Doing that correctly is Chapter 5.2.

10. Common Mistakes

Marking everything phony. It makes every file target rebuild and cascades down the chain (§8). .PHONY is a statement of fact about a name, not a safety measure to apply broadly.

Using a phony target as a prerequisite to force ordering. It permanently disables skipping for the parent (§7). If the step produces something, let it produce a file and depend on that.

Forgetting .PHONY until a collision happens. The failure is silent and zero-exit, so it is usually discovered through a wrong result rather than an error.

Naming an action the same as its output. A run target that writes a file called run is a collision waiting to happen. Give the artifact a different name — run.log — and the problem disappears.

Assuming .PHONY removes the file. It only stops Make consulting it. The colliding file is still on disk and may still confuse other tools.

Treating a phony target as an artifact. A phony name cannot be a dependency anything can compare against, so it cannot participate in incremental rebuilding. If something downstream needs to know whether a step happened, that step needs to leave a real file behind.

11. Check Your Understanding

Question 1. A colleague's make clean prints nothing and exits 0, leaving all build output in place. The rule looks correct. What single command would you run first to diagnose it, and what do you expect to find?

Question 2. A Makefile compiles a large design. After someone added a phony check_env target as a prerequisite of the compile artifact, the full compile now runs on every invocation. Explain the mechanism, and give the change you would make.

Question 3. Is report.txt ever an appropriate entry in a .PHONY list? Explain using the rule of thumb from §6.

Question 4. In §9's flow, make test re-runs the simulation every time even when nothing changed. Is that a bug? Justify your answer in terms of what run and test name.

12. Exercise

Take this Makefile. It is intended to build a report and provide the usual action targets, but it contains two .PHONY mistakes — one of omission and one of commission.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — two .PHONY bugs
.PHONY: report.txt

report.txt: data.txt
	@echo "  [report] building"
	@cat data.txt > report.txt

summary.txt: report.txt
	@echo "  [summary] building"
	@wc -l < report.txt | tr -d ' ' > summary.txt

clean:
	@echo "  [clean] removing output"
	@rm -f report.txt summary.txt

Create data.txt with a line or two. Then predict, before running: what happens on two consecutive make summary.txt invocations, and what happens to make clean after you run touch clean?

13. Remember

  • Make has one kind of target. "File target" and "action target" describe what you mean; Make only ever resolves a name and looks for a file.
  • An action target works because nothing by that name exists, which makes it fragile — a stray file or directory with the same name silently disables it, with exit status 0.
  • .PHONY declares that a name is not a file, so Make never consults the filesystem for it and never considers it up to date.
  • .PHONY does not delete the colliding file, change the recipe, or speed anything up. It changes one thing: whether Make looks.
  • Mark all, run, test, clean and help phony. Never mark a generated artifact phony.
  • A phony prerequisite makes its parent rebuild on every run — and that cascades to everything below. A phony target is safe to ask for and consequential to depend on.
  • To force one rebuild, use make -B or delete the file. Do not leave a permanent .PHONY line behind to do it.
  • Give actions and artifacts different names. run writing run.log cannot collide with itself.

You can now tell the two kinds of target apart and declare them correctly. What is still informal is the word "newer" — the next chapter takes the rebuild decision apart in full: transitive prerequisites, what a timestamp does and does not tell Make, why an undeclared input leaves a stale result, and what happens to a half-written output when a recipe fails. Chapter 2.3 is the conceptual centre of this module.

Where this fits

Part of the Makefile curriculum.