5.3.6 · D2Build Systems & Toolchain

Visual walkthrough — Makefiles — targets, prerequisites, recipes, variables, automatic variables

2,049 words9 min readBack to topic

Step 1 — What is a "file" to Make? A box with a clock on it

WHAT. Before anything else, we need to know what Make sees. To Make, every file is a little box that carries one crucial number: the moment it was last changed. We call that number its modification time and write it for a file .

WHY. Make never reads the contents of your files to decide whether to rebuild. Reading contents would be slow (imagine comparing 50 whole source files byte-by-byte). Instead it uses the operating system's cheap, always-available stamp: "this file was last touched at time ." Comparing two numbers is instant. So the entire logic we are about to build rests on comparing timestamps, not contents.

PICTURE. Below, each file is a box; the red clock reading is its timestamp . A bigger number means "more recently changed / newer."

Figure — Makefiles — targets, prerequisites, recipes, variables, automatic variables

Step 2 — A rule draws an arrow: target depends on prerequisite

WHAT. A Make rule connects an output file to the input files it is made from. We call the output the target and each input a prerequisite . We draw an arrow from prerequisite to target, meaning " is built from ."

WHY. We need a way to say "if the ingredient changes, the dish is out of date." The arrow captures exactly that direction of influence: change flows from inputs to outputs, never backwards. Editing main.c can make main.o stale; editing main.o can never make main.c stale.

PICTURE. One prerequisite box, one target box, one red arrow. The arrow is the whole idea of a dependency.

Figure — Makefiles — targets, prerequisites, recipes, variables, automatic variables

Step 3 — The core comparison: is the target newer than its input?

WHAT. Take the simplest possible case: one target , one prerequisite . We compare their two timestamps and ask a single yes/no question: is newer than ? In symbols, is ?

WHY. Think about what "up to date" means. was built from at some past moment. If nobody has touched since then, the copy of that went into is still the current one — so reflects reality and we can skip work. But if was edited after was built, then was made from an old version of : it is stale. The dividing line is precisely the instant . Anything on 's clock later than that means "rebuild."

PICTURE. Two scenarios side by side. Left: 's clock is behind 's → is fresh, no work. Right: 's clock is ahead of 's (drawn in red) → is stale, run the recipe.

Figure — Makefiles — targets, prerequisites, recipes, variables, automatic variables

Step 4 — Many prerequisites: ANY newer one poisons the target

WHAT. Real targets have several inputs: . Now the question becomes: is any one of them newer than ? We use the "there exists" symbol — read it as "there is at least one such that…".

WHY. A dish is out of date if even one ingredient is fresher than the dish — you don't need all of them to change. So we do not require every prerequisite to be newer; we require just one. That "at least one" logic is exactly what means. The opposite ("every input is old enough") would use "for all", but staleness is triggered by a single culprit, so it is naturally an existence claim.

PICTURE. Three prerequisites feeding one target. Two are older (black), one is newer (red). That single red one is enough to mark stale — the red arrow is the "culprit path."

Figure — Makefiles — targets, prerequisites, recipes, variables, automatic variables

Step 5 — The degenerate case: the target does not exist yet

WHAT. What if has never been built? There is no file, so there is no timestamp to compare against. We must handle this before any comparison. Make's rule: if the target is missing, always build it.

WHY. You cannot ask "is the input newer than the output?" when there is no output. A missing file has no clock. Trying to compare would be like asking whether a sandwich that doesn't exist is fresh — meaningless. So we add a guard clause: no file ⇒ rebuild, no comparison needed. This is the "there's nothing to be up-to-date" case from the parent note.

PICTURE. The target box is drawn as a dashed empty outline in red (it isn't there). No clock, no comparison — the recipe must run.

Figure — Makefiles — targets, prerequisites, recipes, variables, automatic variables

Step 6 — Prerequisites are themselves targets: build the bottom first

WHAT. Here is the twist that makes Make powerful: a prerequisite like main.o is also the target of another rule (built from main.c). Before Make can trust , it must first make sure itself is up to date. So it recurses downward to the leaves, updates them, and only then works its way back up.

WHY. Comparing against a stale prerequisite would be meaningless — you'd be checking "is my input newer than me?" using an input that is itself out of date. The fix is order: settle every input before judging the output. Visiting all children before the parent is called a post-order traversal, and it works cleanly precisely because the dependency graph is a DAG — it has no cycles, so "build the bottom first" always terminates. This is the engine behind Incremental Compilation.

PICTURE. A three-level chain: main.c → main.o → app. Numbered red order-labels show Make diving to the leaf first (1), building the middle (2), then the top (3).

Figure — Makefiles — targets, prerequisites, recipes, variables, automatic variables

Step 7 — Putting a real example through the machine

WHAT. Let's run the full decision on concrete numbers. Suppose (clocks in arbitrary units):

  • main.c has
  • utils.c has
  • main.o has , built from main.c
  • utils.o has , built from utils.c
  • app has , built from main.o utils.o

WHY. Numbers turn the abstract formula into something you can check by eye — and the edge here is subtle: app looks new, but a hidden staleness has crept in.

PICTURE. The full DAG with every clock shown; the one stale edge is highlighted red.

Figure — Makefiles — targets, prerequisites, recipes, variables, automatic variables

Trace it, bottom-up (post-order):

  1. main.o vs main.c: is ? No. main.o is fresh.
  2. utils.o vs utils.c: is ? Yes! utils.o is stale → rebuild it. After rebuilding, its clock jumps to "now" (say ).
  3. app vs its objects: utils.o is now ? Yes → rebuild app too.

So a single edit to utils.c cascades upward: rebuild utils.o, then relink app, while main.o is left untouched. That is the minimal rebuild — exactly Make's promise.

Recall Predict before revealing

If instead only main.c were edited to , which files rebuild? Answer ::: main.o (since ), then app (since the fresh main.o becomes newer than ). utils.o stays as-is.


The one-picture summary

WHAT. Everything above compresses into one flowchart of the single question Make asks per target, plus the master formula.

Figure — Makefiles — targets, prerequisites, recipes, variables, automatic variables

no

yes

no

yes

yes

no

Does target exist

Run recipe

Are all prerequisites up to date

Recurse build them first

Any prerequisite newer than target

Skip target already fresh

Recall Feynman retelling — the whole walkthrough in plain words

Every file is a box with a clock showing when it last changed (Step 1). A rule draws an arrow from an ingredient to the thing it makes (Step 2). To decide if the thing needs remaking, you ask one question: is the ingredient's clock later than the thing's clock? If yes, the thing was made from an old ingredient, so it's stale (Step 3). With many ingredients, any single fresher one is enough to spoil it (Step 4). If the thing was never made at all, there's no clock to compare, so you just make it (Step 5). But ingredients can themselves be made from other things, so before trusting an ingredient's clock you first make sure it is up to date — you dive all the way down to the raw sources, fix those, and climb back up (Step 6). Run this on real numbers and you see the magic: edit one source, and only the boxes above it on the arrows get rebuilt — nothing else (Step 7). That's the entire soul of Make in one sentence: remake something only if it's missing, or if something it depends on is newer than it.


Connections

  • 5.3.06 Makefiles — targets, prerequisites, recipes, variables, automatic variables (Hinglish) — the parent topic, all rules and variables.
  • Directed Acyclic Graphs (DAG) — why the post-order recursion terminates.
  • Incremental Compilation — the payoff: minimal rebuilds.
  • Compilation Pipeline · Linkers and Object Files — what the recipes at each node actually do.