Module 1: The Dependency-Graph Mental Model
Concepts and How It Works
Learning objectives
- Explain the core mental model behind Concepts and How It Works
- Apply Concepts and How It Works within The Dependency-Graph Mental Model
- Identify important boundaries, trade-offs, and failure modes
- Produce concrete evidence from the practice exercise
Related: Rules and Targets | Variables | Phony Targets | Pattern Rules | Make Index
Why Make Exists
Every software project has a build problem: source files must be compiled, linked, and packaged in the right order. Doing this by hand is error-prone and slow. A shell script that recompiles everything works but is wasteful — why recompile 500 files when you changed one?
Make solves two problems at once:
- Dependency tracking — it knows which outputs depend on which inputs.
- Incremental builds — it only rebuilds what's actually out of date, based on file timestamps.
The insight behind Make: a build is a directed acyclic graph (DAG) of files. Each node is a file; edges represent "this file depends on that file." Make walks the graph and rebuilds any node whose dependencies are newer than the node itself.
Analogy: Make is like a contractor who checks the blueprint (Makefile), inspects which parts of the building are already up to date, and only sends workers to the sections that need work. If you change just the plumbing, the contractor doesn't repaint the walls.
The Core Mental Model
Make operates around three questions for every target file:
- Does the target file exist?
- Are any of its prerequisites newer than the target?
- If either answer is yes → run the recipe to (re)build the target.
Target ← Prerequisite files → Recipe (shell commands)If you run make foo.o and foo.o is already newer than foo.c, make does nothing and prints:
make: 'foo.o' is up to date.The Makefile
Make reads a file called Makefile (or makefile) in the current directory. You can override this with -f:
make -f build.mk # use a different file
make -f Makefile.debug # another common patternA Makefile contains three things:
- Rules — how to build targets from prerequisites
- Variables — named values for reuse
- Directives — control flow (
include,ifeq, etc.)
How Make Decides What to Do
When you type make, Make:
- Reads the Makefile from top to bottom, collecting all rules and variable definitions.
- Identifies the default goal — the first non-pattern, non-special target in the file (typically
all). - Builds a dependency graph starting from the default goal.
- Visits each node in topological order (leaves first).
- For each target: if it doesn't exist, or if any prerequisite is newer → run the recipe.
make # builds the default goal (first target)
make all # explicitly builds 'all'
make clean # explicitly builds 'clean'
make foo.o # explicitly builds just foo.oTimestamps and Freshness
Make uses mtime (modification time) for comparisons — the same timestamp your OS tracks for every file.
foo.o is up to date if: mtime(foo.o) > mtime(foo.c) AND mtime(foo.o) > mtime(foo.h)If you touch foo.c (updating its mtime without changing content), Make will still rebuild foo.o. Make doesn't read file contents — only timestamps.
Implication: Don't touch files unnecessarily. Use make -t (touch mode) if you need to mark things as up to date without rebuilding.
Running Make
make # build default goal
make all # build specific target
make -j8 # build with 8 parallel jobs
make -j # use all available cores
make -n # dry run — print commands without running
make -s # silent mode — suppress command echoing
make -k # keep going on errors (don't stop at first failure)
make -B # unconditional rebuild (ignore timestamps)
make -C subdir # change to subdir before reading Makefile
make CFLAGS="-O2" # override a variable from the command line
make V=1 # a common convention for verbose modeMake's Execution Model
Each recipe line runs in a separate shell by default. This is critical:
bad_example:
cd /tmp # this cd disappears after this line
ls # this ls runs in the original directory, not /tmp!
good_example:
cd /tmp && ls # chain with && in a single shell invocation
# Or use .ONESHELL (GNU Make 3.82+)
.ONESHELL:
one_shell_example:
cd /tmp
ls # now this actually runs in /tmpMakefile Syntax Rules
# Comments start with #
# Variable assignment
CC := gcc
# Rule structure (TAB is mandatory — not spaces!)
target: prerequisite1 prerequisite2
recipe_command_1 # must be a TAB character, not spaces
recipe_command_2
# Multiple targets (same recipe)
foo.o bar.o: common.h
echo "common.h changed"The single most common beginner mistake: using spaces instead of a TAB before recipe lines. Make will error with:
Makefile:5: *** missing separator. Stop.Use :set list in vim to see if you have tabs. Modern editors often convert tabs to spaces automatically — configure your editor to use hard tabs in Makefiles.
Make Versions
The examples in these notes assume GNU Make (the standard on Linux, macOS, and most Unix systems). Check your version:
make --version
# GNU Make 4.3
# Built for x86_64-pc-linux-gnuBSD Make (used in FreeBSD) has different syntax in places. On macOS, make is BSD Make but gmake (via Homebrew) is GNU Make. For portable Makefiles, stick to POSIX Make features; for power features, target GNU Make.
Common Pitfalls
# PITFALL 1: Spaces instead of TAB in recipes
target:
echo "wrong" # 4 spaces — BROKEN
echo "right" # TAB — works
# PITFALL 2: Circular dependencies
a: b
b: a # circular — make will detect and error
# PITFALL 3: Missing prerequisites
program: main.o utils.o
$(CC) -o program main.o utils.o utils.h # utils.h not listed!
# If utils.h changes, program won't rebuild
# PITFALL 4: Forgetting .PHONY
clean:
rm -rf *.o
# If a file named 'clean' exists, 'make clean' does nothing!
# Fix: .PHONY: clean
# PITFALL 5: Relying on shell state between recipe lines
install:
export PREFIX=/usr/local # exported env dies with this line
cp binary $(PREFIX)/bin # PREFIX is empty here!
# Fix: use $(PREFIX) as a Make variable, or chain on one linePractice lab
Create a small repository that exercises Concepts and How It Works. Capture a clean build, an incremental no-op build, and a deliberately broken dependency case; explain every command Make chooses to run.
Review questions
- What problem does Concepts and How It Works solve, and what assumptions does it rely on?
- Which boundary or failure case is easiest to miss, and how would you expose it?
- What alternative design would you consider, and what trade-off would change the decision?
- What artifact, trace, test, or metric proves that your implementation is correct?
Completion evidence
- A working artifact, annotated trace, or reproducible experiment
- At least one normal case and one deliberately failing or boundary case
- A concise explanation of the design choice and its trade-offs
- Saved output showing how correctness was evaluated