Skip to content
VLSI Mentor

Makefile · Part I · Module 2 · Rules and Dependencies

Dependencies and Automatic Rebuilding

This is the chapter that decides whether you can trust a Makefile. Make rebuilds on one comparison repeated across a graph, and once you can run that comparison in your head you can predict any build. You will also see the two ways it goes wrong in real projects: an input nobody declared, so the output is quietly stale, and a recipe that dies halfway, leaving a half-written file that Make then reports as current.

Foundation24 min readMakefileGNU MakePart IDependenciesRebuilding

Part I · Module 2 · Chapter 2.3 · Dependencies and Automatic Rebuilding

1. What a Dependency Claims

A dependency is a claim you make about two files: this output is produced from that input. You write it after the colon:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
output.txt: input.txt

Read aloud, that line says three separate things:

  1. input.txt must be up to date before output.txt is considered. If something else produces input.txt, that happens first.
  2. If input.txt is newer than output.txt, then output.txt is out of date. This is the comparison the whole chapter is about.
  3. Nothing else affects output.txt. Make will consider no other file, because you named no other file.

Point 3 is the one that costs people real time. A dependency list is not a hint or a note — it is the complete set of things Make will look at. Anything you leave out does not exist as far as rebuilding is concerned.

The word prerequisite is Make's own term for an item in that list, and the two words are used interchangeably here. A prerequisite may be an ordinary file, or another target with a rule of its own; Chapter 2.1 showed that Make resolves both the same way.

2. The Rebuild Decision, Precisely

For a file target, Make asks exactly two questions, in this order, after its prerequisites have been handled:

  • Does the target file exist? If not → build.
  • Is any prerequisite newer than the target? If yes → build. If no → skip, and report the target up to date.

"Newer" means a later last-modified time, and it is strictly newer: a prerequisite with the same timestamp as its target does not trigger a rebuild.

Five stages in sequence. Stage one, resolve: find the rule for the name you asked for. Stage two, prerequisites first: put every prerequisite through this same decision before going further. Stage three, missing: if no file with the target's name exists, it must be built. Stage four, newer: if any prerequisite is newer than the target, it must be rebuilt. Stage five, run or skip: execute the recipe, or report the target up to date.The rebuild decision, per target01RESOLVEfind the rule for the name02PREREQS FIRSTeach one, same decision03MISSING?no target file means build04NEWER?any prereq newer than it05RUN OR SKIPrecipe, or up to date
Figure 1 — the decision Make performs for every target it visits, in order. Step 2 is recursive: each prerequisite is put through this same five-stage sequence before the comparison at steps 3 and 4 is attempted. Only one of the two comparisons has to say yes for the recipe to run, and if neither does, Make reports the target up to date and runs nothing.

Everything else in this chapter is that sequence applied to a harder graph, or a case where one of the two questions gives an answer you did not intend.

3. Level 1: One Input, One Output

The smallest complete example. Put two lines in input.txt:

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

Four invocations cover all four possible answers:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make output.txt      # 1. target missing
make output.txt      # 2. nothing newer
# append a line to input.txt, then:
make output.txt      # 3. prerequisite newer
rm output.txt
make output.txt      # 4. target missing again
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output — Make 3.x quotes the name differently
  [build] output.txt
make: 'output.txt' is up to date.
  [build] output.txt
  [build] output.txt

Three builds and one skip. Nothing here is surprising yet, which is the point: with one input and one output, the two questions are easy to answer in your head. The rest of the chapter adds exactly one complication at a time.

4. Direct and Transitive Prerequisites

Real builds are chains. Add a middle stage so there are three levels: two hand-written inputs, a merged intermediate, and a final report.

Azvya Education Pvt. Ltd.VLSI Mentor
ports.txt
clk 100
rst_n 1
Azvya Education Pvt. Ltd.VLSI Mentor
blocks.txt
alu 32
regfile 16
Azvya Education Pvt. Ltd.VLSI Mentor
Makefile
merged.txt: ports.txt blocks.txt
	@echo "  [1] merged.txt"
	@cat ports.txt blocks.txt > merged.txt

report.txt: merged.txt
	@echo "  [2] report.txt"
	@wc -l < merged.txt | tr -d ' ' > report.txt
	@cat merged.txt >> report.txt

ports.txt and blocks.txt are direct prerequisites of merged.txt. They are transitive prerequisites of report.txt — not named in its rule, but reachable through the chain. Make follows the chain; you do not have to list them twice.

Ask for the end of the chain and then exercise it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make report.txt        # A. nothing exists
make report.txt        # B. nothing changed
touch blocks.txt
make report.txt        # C. a transitive prerequisite changed
touch merged.txt
make report.txt        # D. only the intermediate changed
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [1] merged.txt
  [2] report.txt
make: 'report.txt' is up to date.
  [1] merged.txt
  [2] report.txt
  [2] report.txt

Here is the same run as a trace, which is the habit worth building:

RunTarget checkedPrereq stateRecipe runsWhy
Areport.txtmerged.txt missing → recursebothnothing exists yet
Amerged.txtboth inputs exist, nothing produces themyestarget missing
Breport.txtmerged.txt not newerneitherboth questions say no
Cmerged.txtblocks.txt neweryesa direct prereq changed
Creport.txtmerged.txt rebuilt, now neweryesthe change propagated one link
Dreport.txtmerged.txt neweronly [2]merged.txt's own inputs did not change

Run C is the important one. Touching blocks.txt rebuilt merged.txt, and rebuilding merged.txt made it newer than report.txt, which rebuilt in turn. The change travelled one link at a time, and each link was an independent two-question comparison — not a single global decision about the whole graph.

Run D shows the reverse: touching the middle file rebuilt only what is downstream of it. Make had no reason to revisit ports.txt or blocks.txt, because nothing claimed they had changed.

5. A Timestamp Is Not a Content Hash

Make compares modification times, not contents. Those two usually agree, because editing a file updates its time. They are not the same question, and both directions of disagreement are worth seeing.

Direction one: a new timestamp with identical content rebuilds anyway. Continuing from §4, touch an input without editing it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
touch ports.txt
make report.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [1] merged.txt
  [2] report.txt

Both recipes ran. The bytes in ports.txt are exactly what they were; only its timestamp moved. Make has no way to know the content is unchanged, and it does not look — so the whole chain rebuilt for nothing. This is why a touch is a legitimate way to force a rebuild, and also why checking out a branch, restoring a backup, or copying a tree can trigger a large pointless rebuild.

Direction two: changed content with an older timestamp does not rebuild. Edit a prerequisite and then deliberately give it an old timestamp:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# edit input.txt so its content really changes, then:
touch -t 202001010000 input.txt
make output.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
make: 'output.txt' is up to date.

The content changed and Make declined to rebuild. The output now disagrees with its input, with a successful exit status. This is not a bug in Make — it answered the question it asks — but it is the reason "I changed the file and nothing happened" is a real support question.

6. Under-declaration: The Stale Result

Now the failure that costs real engineering time, in the shape it actually takes.

Verilog and SystemVerilog sources use textual includes. Put a defines file and a module that includes it under rtl/:

Azvya Education Pvt. Ltd.VLSI Mentor
rtl/defines.vh
`define DATA_W 32
Azvya Education Pvt. Ltd.VLSI Mentor
rtl/alu.v
`include "defines.vh"
module alu;
endmodule

Now a rule that builds an artifact from them. The recipe reads both files; the rule declares only one:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — under-declared
build/alu.list: rtl/alu.v
	@echo "  [build] reading alu.v AND the file it includes"
	@mkdir -p build
	@cat rtl/defines.vh rtl/alu.v > build/alu.list

Build it once, then edit only rtl/defines.vh — add a second define — and build again:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make build/alu.list
# append `define ADDR_W 16 to rtl/defines.vh, then:
make build/alu.list
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
  [build] reading alu.v AND the file it includes
make: 'build/alu.list' is up to date.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
grep -c ADDR_W build/alu.list
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — the new define is missing
0

The artifact is stale and Make reported success. The reasoning is the same as always: build/alu.list exists, its only declared prerequisite rtl/alu.v was not touched, so nothing is newer and there is nothing to do. The file the recipe actually read is not in the comparison, because it is not in the rule.

The fix is to declare what the recipe really consumes:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — corrected
build/alu.list: rtl/alu.v rtl/defines.vh
	@echo "  [build] reading alu.v AND the file it includes"
	@mkdir -p build
	@cat rtl/defines.vh rtl/alu.v > build/alu.list
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make build/alu.list
grep -c ADDR_W build/alu.list
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [build] reading alu.v AND the file it includes
1

And a further edit to the defines file now propagates, as verified. One word after the colon turned a silently wrong build into a correct one.

7. Make's Knowledge Is Not the Compiler's Knowledge

§6 is worth one more paragraph, because the reason it happens is a boundary people keep crossing by accident.

When a simulator compiles alu.v, it follows the `include "defines.vh" directive. It opens that file, reads it, and would fail if it were missing. The compiler therefore knows the dependency exists.

Make does not, and cannot. It never opened alu.v — Chapter 2.1 established that Make does not read source files or even recipes. Its entire model of your project is the graph you typed after the colons.

So there are two different sets of knowledge about one project:

knows about `includeknows about rebuild order
the simulator / compileryes — it parses the sourceno — it compiles what it is given
Makeno — it never reads the sourceyes — but only from what you declared

Neither tool has the whole picture, and the gap between them is where stale artifacts live. Real build systems close it by having the compiler emit its discovered dependencies in a form Make can read back, which is a genuine solution and a Chapter 5.2 subject. Until then, the honest rule is: if the recipe reads it, the rule must name it.

8. Over-declaration: Rebuilding Too Much

The opposite mistake is cheaper but not free. Declaring a prerequisite the recipe does not actually use makes Make rebuild for no reason.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — README.md is not an input to anything
output.txt: input.txt README.md
	@echo "  [build] output.txt"
	@cat input.txt > output.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make output.txt
# edit README.md, then:
make output.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [build] output.txt
  [build] output.txt

Editing documentation rebuilt the artifact. In this example that costs nothing. Attach it to a twenty-minute compile and a team of engineers, and an over-declared dependency is how a build system acquires the reputation of being slow and arbitrary — which is how people start working around it.

A prerequisite list should be exactly the files whose contents affect the output. Not the directory everything lives in, not the Makefile itself by habit, not a convenient wildcard that happens to match extra files. Under-declaring gives wrong answers; over-declaring gives slow ones; the list is a technical claim and deserves the same care as the recipe.

9. When a Recipe Fails Halfway

This is the most dangerous behaviour in the chapter, and it follows inevitably from everything above.

A recipe is a sequence of commands. If one fails, Make stops — but the commands before it already ran, and anything they wrote is still on disk. Consider a recipe that writes its target in stages and fails in the middle:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — the recipe fails after partially writing output.txt
output.txt: input.txt
	@echo "  [build] starting"
	@head -1 input.txt > output.txt
	@echo "  [build] now failing"
	@false
	@cat input.txt > output.txt

With a three-line input.txt:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make output.txt
echo "exit status: $?"
cat output.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
  [build] starting
  [build] now failing
make: *** [output.txt] Error 1
exit status: 2
a

So far so good: the build failed loudly and make exited 2. But look at what is on disk — output.txt exists and contains one line instead of three. It is a partial, corrupt artifact, and it was written after input.txt, so it is newer than its prerequisite.

Now run make again:

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

Make reports the corrupt file as up to date and exits 0. The failure has been erased from Make's view: the target exists, nothing is newer, both questions say no. A second engineer — or a CI job that re-runs make — now sees a clean, successful build sitting on top of a truncated artifact.

This was verified twice, including with a deliberate one-second gap between writing the input and the failed build, so it is not the granularity effect from §5. It is the ordinary rule doing exactly what it says.

GNU Make has a declaration for this. .DELETE_ON_ERROR tells Make to remove a target whose recipe failed:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — with the guard
.DELETE_ON_ERROR:

output.txt: input.txt
	@head -1 input.txt > output.txt
	@false
	@cat input.txt > output.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Representative output
make: *** [output.txt] Error 1
make: *** Deleting file 'output.txt'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ls
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — the partial file is gone
Makefile	input.txt

The next make now correctly tries again and fails again, honestly, instead of declaring victory over a broken file. For a flow whose artifacts matter — a compiled simulation image, a netlist, a coverage database — this one line is close to mandatory. When and how to use it as part of a deliberate error-handling strategy, along with the harder case of a tool that fails while reporting success, is Chapter 4.3.

10. Level 3: An RTL Chain End to End

Putting the chapter together. Three sources, a defines file they all depend on, a compiled artifact, and a simulation log — with every dependency declared.

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
.DELETE_ON_ERROR:

build/sim.img: rtl/defines.vh rtl/alu.v rtl/tb_top.v
	@echo "  [compile] build/sim.img"
	@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] build/run.log"
	@wc -l < build/sim.img | tr -d ' ' > build/run.log

Three behaviours to predict before running, then check:

  • make build/run.log from clean → both recipes run.
  • Again with nothing touched → neither runs.
  • Edit rtl/defines.vh → both run, because the defines file is a declared prerequisite of the compile and the compile feeds the log.

That third case is the one §6 got wrong before the fix. The declaration is what makes an edit to a shared defines file reach the simulation output.

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
.DELETE_ON_ERROR:

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

Two honest limitations of this illustration, so it is not mistaken for a finished flow:

The source list is written out by hand. A real block has hundreds of files and uses a filelist. Naming the filelist as the prerequisite does not make Make notice an edit to a source listed inside it — Chapter 1.2 demonstrated that trap, and generating real dependency information is Chapter 5.2.

defines.vh is declared but not passed on the command line. The simulator finds it through the `include in alu.v. So the prerequisite exists purely to make rebuilding correct, which is exactly its job — a dependency edge is a statement about when to rebuild, not a list of command-line arguments. Those two lists overlap but are not the same, and confusing them is how defines.vh gets dropped from the rule in the first place.

11. Common Mistakes

Leaving an input out of the rule. The output goes stale silently with a zero exit status (§6). If the recipe reads it, name it.

Assuming Make compares contents. It compares times (§5). A touch rebuilds; a restored older file does not.

Trusting a make that said "nothing to be done". It means nothing was newer, not that the artifact is correct. After any failure, after a branch switch, or whenever the answer matters, rebuild from clean.

Declaring the whole directory or the Makefile as a prerequisite. Over-declaration makes every unrelated edit trigger a full rebuild (§8).

Believing a failed build leaves nothing behind. It can leave a newer, partial target that the next run accepts (§9). Use .DELETE_ON_ERROR.

Expecting Make to follow an `include. It never reads your sources (§7). Only the graph you typed exists.

Adding a prerequisite to control ordering. A dependency means "rebuild when this changes", and using it for sequencing has side effects — Chapter 2.2 showed a phony prerequisite destroying skipping, and Chapter 2.4 covers ordering properly.

12. Check Your Understanding

Question 1. report.txt depends on merged.txt, which depends on ports.txt and blocks.txt. You edit ports.txt. Which recipes run, in what order, and why does report.txt rebuild even though its rule never mentions ports.txt?

Question 2. A colleague restores an old copy of input.txt from a backup, preserving its original timestamp. make output.txt reports up to date. Is Make wrong? What is the shortest way to get the correct output?

Question 3. A compile rule names a filelist as its only prerequisite. An engineer edits an RTL file listed inside that filelist and re-runs make. What happens, and what single sentence explains it?

Question 4. A compile fails after writing half of build/sim.img. The engineer re-runs make run and the simulation starts normally. Explain the two separate facts that combine to produce this, and the one-line declaration that prevents it.

13. Exercise

Set up this project exactly:

Azvya Education Pvt. Ltd.VLSI Mentor
Project tree
dep-exercise/
├── rtl/
│   ├── defines.vh
│   ├── alu.v
│   └── regfile.v
└── Makefile

rtl/defines.vh contains one define. rtl/alu.v and rtl/regfile.v each contain an `include "defines.vh" line and a small module.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — as given
build/design.list: rtl/alu.v rtl/regfile.v
	@echo "  [1] design.list"
	@mkdir -p build
	@cat rtl/defines.vh rtl/alu.v rtl/regfile.v > build/design.list

build/count.txt: build/design.list
	@echo "  [2] count.txt"
	@wc -l < build/design.list | tr -d ' ' > build/count.txt

Answer these before touching a terminal, then verify each one:

  1. From a clean tree, which recipes run for make build/count.txt?
  2. You then edit rtl/regfile.v. Which recipes run?
  3. You then edit rtl/defines.vh only. Which recipes run, and is build/count.txt correct afterwards?
  4. Write the corrected Makefile.

14. Remember

  • A dependency list is complete, not advisory. Make considers exactly the files you named and nothing else.
  • Two questions decide every rebuild: does the target exist, and is any prerequisite strictly newer. Equal timestamps do not rebuild.
  • Prerequisites are handled before the comparison, recursively, so asking for the end of a chain builds the whole chain bottom-up.
  • A change travels one link at a time. Each link is its own independent comparison, not one global decision.
  • Timestamps are not contents. A touch rebuilds identical files; a restored older file with new content does not rebuild at all.
  • Under-declaring gives wrong answers. An input the recipe reads but the rule omits leaves the output stale with exit status 0 — the `include case is the common real one.
  • Over-declaring gives slow ones. An unused prerequisite rebuilds the artifact for unrelated edits.
  • Make's model of your project is only what you typed. It never reads a source file or a recipe, so it cannot discover an include the compiler follows.
  • A failed recipe can leave a newer, partial target that the next make calls up to date and exits 0 on. .DELETE_ON_ERROR removes it.
  • "Nothing to be done" is not a correctness guarantee. When the answer matters, rebuild from clean.

You can now predict what a Makefile will rebuild and explain why. What is still unanswered is which target Make picks when you type make with no arguments, and exactly what order it is allowed to do the work in — including what stops being true the moment a build runs in parallel. Chapter 2.4 closes the module with both.

Where this fits

Part of the Makefile curriculum.