Makefile · Part I · Module 2 · Rules and Dependencies
Default Targets and Execution Order
Two questions close this module. Which target does Make build when somebody just types make — and in what order is it allowed to do the work? The first has a surprising answer that depends on where a rule sits in the file. The second matters more: the order you observe is not the order Make promises, and the difference stays invisible until the day a build runs in parallel.
Intermediate22 min readMakefileGNU MakePart IDefault GoalExecution Order
Part I · Module 2 · Chapter 2.4 · Default Targets and Execution Order
1. Two Different Questions
People new to Make tend to run these two questions together, and they have different answers:
- Which target gets built? Make builds the goal — either the one you named on the command line, or, if you named none, a default it picks for you.
- In what order does the work happen? Make derives that from the dependency graph, and what it guarantees is narrower than what you will usually observe.
The first question is a small piece of knowledge with a sharp edge. The second is where real flows break. Sections 2 to 5 cover the goal; sections 6 to 10 cover the order.
2. With No Target, Make Builds the First One
Type make with no arguments and Make builds the first eligible target in the file. Not the one called all, not the most important-looking one — the first one.
Here is why that is worth a whole section:
clean:
@echo " [clean] removing everything"
all: report.txt
@echo " [all] done"
report.txt:
@echo " [report] building"
@echo data > report.txtA reader skimming that file would say it builds a report. Run it:
make [clean] removing everythingA bare make deleted things. Nothing is wrong with Make — clean is simply the first rule in the file, so it is the default goal. But a new engineer who clones the repository, types make to see what happens, and wipes their working files has been handed a trap by whoever wrote the Makefile.
This is the single most common way a Makefile surprises a newcomer, and it costs nothing to prevent.
3. Moving a Rule Changes What make Does
The dependency on position is literal. Take the identical three rules and move all to the top:
all: report.txt
@echo " [all] done"
clean:
@echo " [clean] removing everything"
report.txt:
@echo " [report] building"
@echo data > report.txtmake [report] building
[all] doneSame rules, same recipes, different behaviour — because one rule moved. That is a fragile property for a file several people edit: somebody adding a new rule at the top of a Makefile, or sorting the rules alphabetically, silently changes what a bare make does.
Note also what "eligible" excludes. A target whose name begins with a period — .PHONY, .DELETE_ON_ERROR, .DEFAULT_GOAL — is a special directive and is never chosen as the default goal:
.PHONY: all
all:
@echo " [all] ran as the default" [all] ran as the defaultThe .PHONY line is first in the file, and all is still the default. So the rule is precisely: the first target that is an ordinary target.
4. Declaring the Default Deliberately
GNU Make lets you state the default goal instead of inheriting it from the layout:
.DEFAULT_GOAL := all
clean:
@echo " [clean] removing everything"
all: report.txt
@echo " [all] done"
report.txt:
@echo " [report] building"
@echo data > report.txtclean is still the first rule in the file. Run a bare make:
[report] building
[all] doneThe declaration wins. .DEFAULT_GOAL is a Make directive, not a general lesson about variables — Module 3 covers variables properly, and the := here is simply the form this directive takes. Put it at the top of the file and the default stops depending on where anybody adds a rule.
Two habits, either of which solves §2's trap:
- Put the intended default first, by convention
all, and keep it there. - Or declare
.DEFAULT_GOAL := allat the top, which survives reordering.
Doing both is common and costs nothing. What you should not do is leave a destructive target like clean sitting at the top of the file.
5. The Conventional all Target
all is not special to Make — it is a convention. It is an aggregate: a phony target whose prerequisites are the things a complete build produces, and whose own recipe does little or nothing.
.PHONY: all clean
.DEFAULT_GOAL := all
all: ports.rpt blocks.rpt
@echo " [all] both reports present"
ports.rpt: ports.txt
@echo " [ports.rpt]"
@wc -l < ports.txt | tr -d ' ' > ports.rpt
blocks.rpt: blocks.txt
@echo " [blocks.rpt]"
@wc -l < blocks.txt | tr -d ' ' > blocks.rpt
clean:
@echo " [clean]"
@rm -f ports.rpt blocks.rptWith ports.txt and blocks.txt in place, run a bare make from clean, then again, then after touching one input:
[ports.rpt]
[blocks.rpt]
[all] both reports present
[all] both reports present
[blocks.rpt]
[all] both reports presentThree things to read out of that.
Run 1 built both reports, then all. Run 2 built neither — both file targets were up to date — but [all] still printed, because all is phony and a phony target always runs (Chapter 2.2). Run 3 rebuilt only the report whose input changed.
So all gives you one command that means "bring everything up to date", while the real skipping still happens underneath, per file target. That is the whole value of the convention.
That last sentence of the caption is the bridge to the rest of the chapter.
6. Textual Order Is Not Execution Order
A Makefile is a set of rules, and Chapter 1.4 showed that Make reads the whole file before building anything. So the order rules appear in has exactly one effect — which target is the default (§2) — and no effect at all on the order recipes run.
The order recipes run comes from the graph. In §5's Makefile, ports.rpt's rule is written above blocks.rpt's, and that is not why it ran first; it ran first because of how Make walked the prerequisite list. Reorder the two rules in the file and the build behaves identically.
What Make does guarantee is this, and it is worth memorising in one line:
Every prerequisite of a target is brought up to date before that target's own recipe runs.
That is a statement about completion, not about sequence. It says ports.rpt and blocks.rpt are both finished before all's recipe starts. It says nothing about ports.rpt versus blocks.rpt.
7. Independent Prerequisites Have No Guaranteed Order
Two prerequisites are independent when neither is reachable from the other in the graph. In §5, ports.rpt and blocks.rpt are independent: nothing connects them.
Watch what Make does with three independent prerequisites:
.PHONY: all a b c
all: a b c
@echo " [all]"
a:
@echo " [a]"
b:
@echo " [b]"
c:
@echo " [c]"Run it normally:
make [a]
[b]
[c]
[all]Left to right, which is what a serial Make does and what four consecutive runs produced. It is tempting to conclude that the prerequisite list is a sequence. Now run the same Makefile with two jobs allowed at once, four times:
make -j4 [a] [c] [b] [all]
[a] [b] [c] [all]
[a] [b] [c] [all]
[a] [b] [c] [all]One of the four runs put [c] before [b]. Three agreed with the serial order and one did not, which is the most dangerous possible result: an assumption that holds almost always is an assumption that will fail in production rather than in testing.
Notice what did not change: [all] is last in every run. The guarantee from §6 held every time; only the unguaranteed part moved.
8. A Prerequisite Is a Requirement, Not a Sequence
Here is the mistake that §7 sets up, written the way it actually appears in real Makefiles. An engineer wants a setup step to run before a build step, and reaches for the prerequisite list because it looks like a to-do list:
.PHONY: flow setup build
flow: setup build
@echo " [flow] done"
setup:
@echo " [setup] creating stamp"
@sleep 0.3
@echo ready > stamp.txt
build:
@echo " [build] reading stamp"
@cat stamp.txtThe intent is clear: setup writes stamp.txt, then build reads it. Run it serially and the intent appears to be satisfied:
make [setup] creating stamp
[build] reading stamp
ready
[flow] doneIt works. It has probably worked for a year. Now run it with two jobs:
make -j2 [setup] creating stamp
[build] reading stamp
cat: stamp.txt: No such file or directory
make: *** [build] Error 1
make: *** Waiting for unfinished jobs....It fails every time. setup and build are independent as far as the graph is concerned — neither is a prerequisite of the other — so Make is free to start them together. It does, build reaches for a file setup has not finished writing, and the build breaks.
The Makefile never said build needs stamp.txt. It said flow needs both of them, which is a different claim. Writing them in a particular order inside one list expressed a hope, not a requirement.
9. Expressing a Real Ordering Requirement
If one step genuinely needs another's output, say so with a dependency edge. The requirement in §8 is not "run setup first" — it is "build needs stamp.txt". Write that:
.PHONY: flow build
flow: build
@echo " [flow] done"
stamp.txt:
@echo " [setup] creating stamp"
@sleep 0.3
@echo ready > stamp.txt
build: stamp.txt
@echo " [build] reading stamp"
@cat stamp.txtTwo changes. The setup step became a file target named after what it produces, and build names that file as its prerequisite. Now the graph contains the fact that used to live only in the author's head.
make -j2 [setup] creating stamp
[build] reading stamp
ready
[flow] doneFour parallel attempts, four correct runs. Make cannot start build early now, because the guarantee from §6 applies: stamp.txt is a prerequisite of build, so it is complete before build's recipe begins.
The general principle, and it is the most useful sentence in this chapter:
If step B needs something step A produces, make that thing a prerequisite of B. Do not rely on the order of a list, and do not rely on having observed the right order.
This also explains why the §8 version could not be fixed by reordering the list or by any amount of care in writing it. The information Make needed was absent, and a list has no way to supply it.
10. Checking the Plan Before Running It
When you are unsure what a goal will do — especially on a Makefile you did not write — ask Make to print its plan. Chapter 1.4 introduced make -n; it is at its most useful here:
make -n flowecho " [setup] creating stamp"
sleep 0.3
echo ready > stamp.txt
echo " [build] reading stamp"
cat stamp.txt
echo " [flow] done"That is the serial plan, in order, including the @-suppressed lines. It answers "which goal, and what will it run" in one command without touching a file.
Two limits to keep in mind. It shows the serial plan, so it will not reveal the §8 bug — a dry run is not a parallelism test. And as Chapter 1.4 showed, a recipe line prefixed with + executes even under -n, so it is not a sandbox.
11. An RTL Flow With Deliberate Goals
Putting the chapter together on a compile-and-simulate flow. This version is portable and was executed, standing in for a simulator:
.PHONY: all run clean
.DEFAULT_GOAL := all
all: build/run.log
build/sim.img: rtl/defines.vh rtl/alu.v rtl/tb_top.v
@echo " [compile]"
@mkdir -p build
@cat rtl/defines.vh rtl/alu.v rtl/tb_top.v > build/sim.img
build/run.log: build/sim.img
@echo " [run]"
@wc -l < build/sim.img | tr -d ' ' > build/run.log
run: build/run.log
@echo " [run target] log says:"
@cat build/run.log
clean:
@echo " [clean]"
@rm -rf buildFour invocations, each asking a different question:
make # the declared default goal
make run # an explicit phony goal
make build/sim.img # an explicit artifact goal
# edit rtl/defines.vh, then:
make [compile]
[run]
[run target] log says:
5
make: 'build/sim.img' is up to date.
[compile]
[run]Reading those four in order: the default goal produced the log; run found the log current and only printed it; asking for the artifact directly reported it up to date; and an edit to the shared defines file rebuilt the chain, because Chapter 2.3 declared it as a prerequisite.
Three deliberate choices in that file, each one this module's subject:
.DEFAULT_GOAL := allso a baremakeis defined by intent rather than byallhappening to be first.runis phony andbuild/run.logis not. The action a person asks for and the artifact it produces have different names, which keeps both honest (Chapter 2.2).- Every step that needs another step's output names that output.
rundepends onbuild/run.log, which depends onbuild/sim.img. There is no list-order dependence anywhere, somake -jis safe.
The simulator version is illustrative — vcs is not installed here and was not run:
.PHONY: all run clean
.DEFAULT_GOAL := all
all: build/run.log
build/simv: rtl/defines.vh rtl/alu.v rtl/tb_top.v
mkdir -p build
vcs -full64 -sverilog rtl/alu.v rtl/tb_top.v -o build/simv
build/run.log: build/simv
./build/simv -l build/run.log
run: build/run.log
@echo "see build/run.log"Tuning how many jobs to run, and the reliability questions that come with a genuinely parallel flow, are Chapter 8.2. Everything needed to make a flow safe under parallelism is in this chapter: express every real requirement as a dependency edge.
12. Common Mistakes
Leaving clean as the first rule. A bare make then deletes things (§2). Put all first, or declare .DEFAULT_GOAL.
Assuming all is special to Make. It is only a convention. Nothing happens automatically because a target is called all.
Believing rule order controls execution. It controls the default goal and nothing else (§6).
Reading a prerequisite list as a sequence. It is a set of requirements. Serial Make happens to walk it left to right; that is not a promise (§7, §8).
Adding a prerequisite to force "run this first". If the step produces something, depend on the thing it produces (§9). If it produces nothing, there is no requirement for Make to enforce.
Treating make -n as proof. It prints the serial plan, so it cannot expose an ordering bug that only appears in parallel (§10).
Testing only serially. A flow that is never run with -j has never been tested for the §8 bug, and a build server will eventually run it that way.
13. Check Your Understanding
Question 1. A repository's Makefile starts with a docs: rule, then all:, then clean:. A new engineer clones it and types make. What happens, and what are the two one-line fixes?
Question 2. A Makefile has .PHONY: all on line 1 and install: as the first ordinary rule, followed by all:. Which target does a bare make build? Explain using the word "eligible".
Question 3. report: compile simulate is intended to compile then simulate. It has worked for a year and fails on a new build server. Give the mechanism and the correct Makefile change.
Question 4. In §5's Makefile, make prints [all] both reports present even on a run where neither report was rebuilt. Why — and what would have to change for [all] to be skipped?
14. Exercise
Here is a flow with a default-goal problem and an ordering problem.
.PHONY: clean package build
clean:
@echo " [clean] removing out/"
@rm -rf out
package: build
@echo " [package] archiving"
@cat out/payload.txt > out/package.txt
build:
@echo " [build] writing payload"
@mkdir -p out
@sleep 0.2
@echo payload > out/payload.txtAnswer before running, then verify:
- What does a bare
makedo? - Does
make packagework serially? - Does
make -j2 packagework reliably? Why? - Rewrite the Makefile correctly.
15. Remember
- A bare
makebuilds the first eligible target — the first ordinary target in the file. Targets beginning with a period are never eligible. - Moving a rule can change what
makedoes. Put the intended default first, or declare.DEFAULT_GOAL := all, or both. - Never leave a destructive target at the top of a Makefile.
allis a convention, not a Make feature — a phony aggregate whose prerequisites are everything a full build produces.- Rule order in the file affects only the default goal, never the order recipes run.
- The one guarantee: every prerequisite of a target is up to date before that target's recipe runs. That is about completion, not sequence.
- Independent prerequisites have no guaranteed order. Serial Make walks them left to right; parallel Make need not, and was measured not to.
- A prerequisite list is a set of requirements, not a sequence. Using it as a sequence produces a flow that passes every serial test and fails under
make -j. - Express a real requirement as a dependency edge. If B needs what A produces, make that output a prerequisite of B — then the order is enforced rather than hoped for.
make -nshows the serial plan. It answers "which goal and what will run", and cannot reveal a parallel ordering bug.
That completes Module 2. You can now read a multi-target Makefile and say what will run, what will not, why a target rebuilds, why one is skipped, when .PHONY belongs, how the default goal is chosen, and which parts of the execution order are actually promised. Every Makefile so far has spelled out each filename by hand, and that is the next constraint to remove: Module 3 introduces variables, the automatic variables that let a recipe refer to its own target and prerequisites, and the functions that build a file list instead of repeating one.
Where this fits
Part of the Makefile curriculum.
