5.3.6 · D1Build Systems & Toolchain

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

3,162 words14 min readBack to topic

Before you can read a single line of the parent note, you must own every symbol it throws at you. This page builds each one from nothing, in an order where every idea leans only on ideas that came before it. A smart 12-year-old should be able to start at line one.


0. What is a "file" and a "timestamp"?

Everything Make does rests on two facts about your computer's disk.

Picture two boxes on a shelf. Each box has a sticky note showing the last time someone opened and changed it. That sticky note is the timestamp.

Figure — Makefiles — targets, prerequisites, recipes, variables, automatic variables
Figure s01 — two files, main.c (amber sticky: 10:05) and main.o (white sticky: 10:02). Compare the two sticky notes: because main.c's stamp is later, main.o was built from an older version and is now stale. This single picture is Make's whole reason to exist.

Why does Make need this? Because its entire job — its whole reason to exist — is to compare these two sticky notes and ask: "Is the ingredient newer than the dish I made from it?" If yes, the dish is stale.


1. The arrow: "is built from"

Files don't live alone — some are made from others. main.o is made from main.c. app is made from main.o.

The word "depends" is not fancy: it means "if you change A, my B is a lie until I remake it."

Figure — Makefiles — targets, prerequisites, recipes, variables, automatic variables
Figure s02 — the amber arrow points from main.c (cyan, the ingredient) to main.o (white, the dish). Follow the arrowhead: it reads "main.o is built from main.c." The arrow's direction is exactly the direction Make checks freshness across.

Why does the topic need arrows? Because a real project has dozens of files feeding into each other. To rebuild the minimum amount, Make must know the exact shape of this web. That web has a special name.


2. The graph — nodes, edges, "leaf", and "DAG"

Why acyclic? Imagine X is built from Y, and Y is built from X. To build X you first need Y, but to build Y you first need X... forever. That's a cycle, and it's impossible to finish. So a valid build must have no loops — it must be a DAG.

Figure — Makefiles — targets, prerequisites, recipes, variables, automatic variables
Figure s03 — LEFT: a valid DAG. The cyan .c files at the top are leaves (nothing points into them); amber arrows flow down through the .o files to app. RIGHT: a red two-node loop X → Y → X. Trace the red arrows and you circle forever — this cycle can never be built, which is exactly why Make requires a DAG.

This is the same structure studied in Directed Acyclic Graphs (DAG). The parent note leans on it silently — now you know what it means.


3. The build order: a dependency-first (topological) traversal

The parent note says Make uses a "post-order traversal" of the DAG. Let's nail down exactly what that means, and — just as important — why this order and not some other.

First, fix the direction of our arrows so there is zero ambiguity. Throughout this page an arrow means " is built from " (Section 1). So the arrow points into the thing being built. When Make sits at a target and asks "what must I do first?", it looks at the arrows coming into — those tail-ends are 's ingredients (its prerequisites).

This "finish all my ingredients before you finish me" pattern is called post-order traversal. Two clarifications that matter:

  • Which nodes come first? The prerequisites — the nodes at the tails of the arrows pointing into the current node. Not the things the current node feeds; the things that feed the current node. Ingredients before dish.
  • Tree post-order vs DAG post-order. The classic "post-order" you may have met visits a tree (each node has exactly one parent). A dependency graph is a general DAG: a single .h file can feed many targets, so a node may be reachable by several paths. So what Make really performs is a DFS-based topological sort — a depth-first walk that emits each node only after all its dependencies, and (crucially) marks nodes already visited so a shared file is built once, not once per path.

Picture building a wall: you can't place the top brick until the bricks under it exist. Make walks down to the bottom of the dependency graph — the leaves — builds those first, then works upward to the target you asked for. And because a target's freshness (Section 4) can only be judged after its ingredients are fresh, checking app before rebuilding main.o would compare against a stale object file and give the wrong answer — which is exactly why dependencies must be finished first.


4. Modification time as a symbol:

The parent writes and . That notation just wraps the sticky-note idea in math shorthand.

So is "when was main.c last saved?" and the comparison reads in plain English:

"prerequisite was saved more recently than target " — i.e. the ingredient is fresher than the dish, so the dish is stale.

Why these logic symbols and not words? Because "any one ingredient being newer is enough" is exactly what ("there exists") captures, and "missing file counts as stale" is exactly . The math is just an unambiguous compression of the sentence.


5. The dollar-sign shortcuts: $@, $<, $^, $?, $*

The parent's "secret weapon" section is full of $ symbols. Here's the ground floor.

These single-char symbols are called automatic variables because Make fills them in automatically, differently for every rule, based on that rule's own target and ingredients. You never type the filenames twice.

Why do these exist at all? So one generic rule can serve many files. Without them, a rule for main.o and a rule for utils.o would each need their filenames spelled out twice — and those two copies would silently drift apart. The $-symbols keep the recipe glued to this rule's real files.


6. The TAB — the invisible symbol

Picture an ID checkpoint: a real TAB is the badge that gets a line waved through as a recipe. Eight spaces look identical on screen but carry no badge — Make rejects them.

This connects to Shell Scripting Basics — the recipe is shell, and the TAB is Make's way of handing those lines to the shell.


7. Two special kinds of target that bend the rebuild logic

The rebuild test in Section 4 assumes every target is a real file whose freshness you compare. Two common Makefile features deliberately step outside that assumption — you'll meet both in the parent note.


8. Where these files come from: the compilation pipeline

The parent talks about .c files becoming .o files becoming an app. Those transformations aren't magic — they are stages of a pipeline.

The step .c → .o is compilation (see Compilation Pipeline and Incremental Compilation). The step "many .o → one app" is linking (see Linkers and Object Files). Make's whole value is doing only the pipeline stages whose inputs actually changed. When you outgrow hand-written Makefiles, generators like those in CMake and Build Generators write these rules for you.


Equipment checklist

Cover the right side and answer out loud.

What does a file's "modification time" record?
The last moment the file was saved/changed — its freshness sticky note.
When is a file "out of date"?
When any file it was built from has a newer modification time (or the file is missing).
What does the arrow mean?
is built from , so 's freshness is judged against .
What are a graph's two ingredients?
Nodes (dots — here files) and edges (directed lines — here "is built from").
What is a "leaf" in the dependency graph?
A node with no arrows pointing into it — a raw file built from nothing (e.g. a hand-written .c).
What does "acyclic" forbid, and why must builds be acyclic?
Loops of dependency; a cycle means each file needs the other first, so the build can never finish.
In the build traversal, which nodes are finished before a target?
Its prerequisites — the nodes at the tails of the arrows pointing INTO the target (its ingredients).
Why does Make use a DFS/post-order (topological) walk rather than any other valid order?
It is demand-driven: DFS from the requested target builds exactly the sub-graph that target needs, in dependency-first order, visiting each shared file once.
Read in plain English.
Ingredient was saved more recently than target , so is stale.
What do , , and mean?
"not", "or", and "there exists at least one ".
What does a leading $ do in a recipe?
Substitutes a value in place — pastes what a variable holds.
Give $@, $<, $^ in words.
Aim/target, first prerequisite, all distinct prerequisites.
Why does $^ remove duplicates?
So the recipe gets a clean list of distinct inputs, avoiding duplicate-file / multiple-definition errors or repeated work.
Where is $* valid, and where is it meaningless?
Valid only inside a pattern rule (it's the text matched by \%); meaningless in an explicit rule with no \%.
What does $? mean?
Only the prerequisites newer than the target — the stale ones.
What does .PHONY: clean accomplish?
Marks clean as a command, not a file, so its recipe always runs even if a file named clean exists.
What is an order-only prerequisite (after |), and why use one?
A prerequisite that must exist first but whose timestamp is ignored for rebuilds — e.g. a build directory whose stamp changes constantly.
Why must a recipe line start with a real TAB, not spaces?
The TAB is Make's separator marking the line as a shell recipe; spaces trigger "missing separator".
Name the three file kinds Make juggles and the step between them.
.c source → (compile) → .o object → (link) → app executable.