The Makefile Mindset
Make is older than most programmers. It was written in 1976. It has quirks that would be considered bugs in any modern tool — tab sensitivity, implicit rules, recursive evaluation. And yet it persists.
Why Make Survives
The reason Make survives isn’t inertia, though inertia helps. Make survives because it models something fundamental about how software is built: dependency graphs.
Every build system, at its core, is answering the same question: “What needs to happen, in what order, to get from source to artifact?” Make answers this question directly. You declare targets, their dependencies, and the recipes to produce them. Make figures out the rest.
program: main.o utils.o
$(CC) -o $@ $^
%.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
Six lines. A complete build system. No configuration files. No plugins. No build tool that itself needs building.
Dependency Thinking
The valuable thing Make teaches isn’t Make syntax. It’s dependency thinking.
When you write a Makefile, you’re forced to answer: “What does this target actually depend on?” Not what it might depend on. Not what it historically depended on. What it depends on right now.
This is harder than it sounds. Most developers have a rough mental model of their build dependencies, but rough models have rough edges. The file that’s included transitively through three headers. The code generator that needs to run before compilation. The test fixtures that need to exist before tests can run.
Make forces these implicit dependencies to become explicit. And once they’re explicit, they can be reasoned about.
Beyond Builds
The Makefile mindset extends far beyond compilation.
Deployment is a dependency graph. You can’t restart the service before the new binary is in place. You can’t put the binary in place before it’s built. You can’t build it before the tests pass. Each step depends on the previous one, and skipping steps leads to breakage.
Documentation is a dependency graph. The API reference depends on the API existing. The tutorial depends on the API reference. The getting-started guide depends on the tutorial. Write them out of order and you’ll reference things that don’t exist yet.
Even project planning is a dependency graph. Feature B depends on the infrastructure that Feature A establishes. You can’t parallelize them without understanding this relationship.
The Modern Objection
The common objection to Make is that it’s old, weird, and limited. Modern build tools handle cross-compilation, caching, sandboxing, and remote execution.
Fair enough. Use those tools when you need those features.
But for the common case — a project with a handful of source files, a few scripts, and a deployment target — Make does everything you need in a format that any developer can read without documentation. The Makefile is the documentation.
.PHONY: test clean deploy
test:
pytest tests/
clean:
rm -rf build/ dist/
deploy: test
rsync -avz dist/ server:/app/
Anyone reading this file knows how to test, clean, and deploy the project. No build tool documentation required. No framework to learn. No abstraction layers to debug.
The Lesson
The lesson of Make isn’t “use Make for everything.” It’s “think in dependencies.”
Before you start a task, ask: what does this depend on? Before you build a system, ask: what’s the dependency graph? Before you automate a process, ask: what must happen before what?
The answers to these questions shape everything that follows. Get the dependencies right and the rest falls into place. Get them wrong and every step fights the one before it.
Make taught this lesson in 1976. We’re still learning it.