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:
report.txt: input.txt
@cat input.txt > report.txt
clean:
@rm -f report.txtBut they mean different things to the person who wrote them:
report.txtis 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."cleanis an action target. The name stands for a job. Nothing calledcleanis 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:
- Does a file with the target's name exist? If not, build it.
- 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:
clk
rst_nreport.txt: input.txt
@echo " [report.txt] building"
@cat input.txt > report.txt
clean:
@echo "removing generated files"
@rm -f report.txtNow four invocations, each hitting a different answer:
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 [report.txt] building
make: 'report.txt' is up to date.
[report.txt] building
[report.txt] buildingFour 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:
make clean
make cleanremoving generated files
removing generated filesNo 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:
clean:
@echo "removing generated files"
@rm -f report.txtmake cleanremoving generated filesWorks. 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:
touch clean
make clean
echo "exit status: $?"make: 'clean' is up to date.
exit status: 0The 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:
.PHONY: clean
clean:
@echo "removing generated files"
@rm -f report.txtThat 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:
ls
make clean
make cleanMakefile clean
removing generated files
removing generated filesThe 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.
cleanis 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:
.PHONY: all run test clean5. 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:
| Target | Phony? | Why |
|---|---|---|
all | yes | an aggregate request; produces nothing itself |
run | yes | performs a simulation; the log it writes has a different name |
test | yes | performs a check |
clean | yes | removes things; the name is not an output |
help | yes | prints usage |
report.txt | no | it is a real file the rule produces |
build/sim.img | no | likewise |
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:
.PHONY: prep
prep:
@echo " [prep] ran"
output.txt: input.txt prep
@echo " [output.txt] rebuilding"
@cat input.txt > output.txtWith input.txt in place, run it three times without changing anything:
make output.txt
make output.txt
make output.txt [prep] ran
[output.txt] rebuilding
[prep] ran
[output.txt] rebuilding
[prep] ran
[output.txt] rebuildingoutput.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:
.PHONY: prep
prep:
@echo " [prep] ran"
output.txt: input.txt
@echo " [output.txt] rebuilding"
@cat input.txt > output.txt [output.txt] rebuilding
make: 'output.txt' is up to date.Skipping is restored.
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.
.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.txtAsk for summary.txt three times with nothing changing:
[output.txt] rebuilding
[summary.txt] rebuilding
[output.txt] rebuilding
[summary.txt] rebuilding
[output.txt] rebuilding
[summary.txt] rebuildingTwo failures at once, and the second is the expensive one:
output.txtalways rebuilds. Declared phony, so its timestamp is never consulted — even though the file is created on disk perfectly well each time. Check withlsand it is right there; Make has simply been told not to look.- The damage cascades.
summary.txtis an ordinary file target that was never declared phony, and it rebuilds too — because its prerequisite rebuilt, exactly as §7 described. One wrong.PHONYline 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:
.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.logFour phony names and one file target. Run the whole thing from clean:
make test [compile] building build/sim.img
[run] simulating
4
[test] checking run.log
PASSNow run it again with nothing changed:
[run] simulating
4
[test] checking run.log
PASSThe 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:
[compile] building build/sim.img
[run] simulating
5
[test] checking run.log
PASSTwo 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:
.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.logNote 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.
.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.txtCreate 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.
.PHONYdeclares that a name is not a file, so Make never consults the filesystem for it and never considers it up to date..PHONYdoes not delete the colliding file, change the recipe, or speed anything up. It changes one thing: whether Make looks.- Mark
all,run,test,cleanandhelpphony. 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 -Bor delete the file. Do not leave a permanent.PHONYline behind to do it. - Give actions and artifacts different names.
runwritingrun.logcannot 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.
