Technical Beauty ■ Episode 24
1976. Steve Johnson storms into Stuart Feldman’s office at Bell Labs, furious. He has spent the morning debugging a programme that is, by every measure, correct. The source is fine. The logic is sound. The bug is not in the code. The bug is that the binary was never recompiled after the last edit. Johnson has been testing a stale executable, confirming the behaviour of yesterday’s programme while reading today’s source. One does not need a great deal of imagination to picture the state of mind.
That weekend,
Feldman
wrote make.
The Man
Stuart Feldman holds an A.B. in astrophysical sciences from Princeton and a Ph.D. in applied mathematics from MIT. He would go on to receive the ACM Software System Award in 2003, serve as Vice President of Engineering at Google, and become President of the ACM in 2006. None of these credentials are necessary to understand what he built. What he built is necessary to understand the credentials.
The Idea
The entire intellectual content of make fits in three
lines:
target: dependencies
recipe
If any dependency is newer than the target, run the recipe. If not,
do nothing. That is the algorithm. The dependency graph is a
directed acyclic graph,
resolved not by a database, not by a cache server, not by a daemon polling
for changes, but by filesystem timestamps. The filesystem is the cache.
stat() is the query. The operating system you are already running
is the entire infrastructure.
Consider what make does not do. It does not parse
your programming language. It does not understand your compiler. It does not
manage your packages. It does not fetch your dependencies from a registry.
It does not generate lock files. It does not phone home. It has no opinions
about your directory structure, your naming conventions, or your version
control system. It is language-agnostic by design, not because
Feldman could not be bothered to write a C parser, but because the problem
of rebuilding what has changed is entirely orthogonal to the problem of
understanding what has been written.
This is the Unix philosophy in its purest expression: do one thing. Do it completely. Compose with everything.
The Tab
Every tool has a scar, and make’s is famous. The
recipe line must begin with a tab character. Not spaces. A tab. In 2026,
this design choice has generated approximately four trillion Stack Overflow
questions involving the words “missing separator” and roughly
equal measures of bewilderment and profanity.
The explanation is instructive. Feldman, by his own account, wanted to learn Lex (the lexical analyser generator). He got snarled up in Lex’s own idiosyncrasies and fell back to the simplest possible approach: check the first character of the line. A tab distinguished recipe lines from everything else. It was a weekend project. It worked. A few weeks later, a dozen users at Bell Labs depended on the format. Too late to change.
That tab has survived fifty years. It has outlived Lex’s relevance,
outlived Bell Labs itself, outlived every attempt to replace make
with something that uses spaces. It is a permanent reminder that design
decisions made in haste on a Friday evening acquire the force of natural
law by Monday morning, provided someone depends on them. There is a lesson
here about backwards compatibility, about the weight of deployed code, and
about the advisability of learning Lex on a deadline.
The Implementations
Feldman’s original make for
V7 Unix
(1979) was roughly 2,500 lines of C. The algorithm was correct. The
interface was settled. What followed was not replacement but refinement.
FreeBSD’s bmake
stands at approximately 29,000 lines. It adds conditionals
(.if / .else / .endif), loops
(.for / .endfor), parallel builds, and pattern
rules. These are genuine extensions to the language, not rewrites of the
algorithm. The DAG is the same. The timestamps are the same. The
philosophy is the same.
GNU make 4.4 weighs in at roughly 39,000 lines. Pattern rules, implicit rules, automatic variables, built-in functions. More features, same core. The algorithm Feldman wrote in a weekend remains the engine.
And then there is CMake: approximately 1.5 million lines of C and C++. Its purpose, its entire reason for existence, is to generate Makefiles. A 1.5-million-line tool exists solely to produce the input files for a programme that was 2,500 lines in 1976. One might call this an endorsement. One might also call it something else entirely.
The Graveyard
The industry has spent fifty years trying to replace make.
The roll call is instructive, not because the tools are bad
(several are excellent) but because each one illuminates the
remarkable difficulty of improving upon three concepts and a tab
character.
Ant (2000) replaced Makefiles with XML because XML was the future. It was not. Maven (2004) replaced Ant’s imperative XML with declarative XML because convention over configuration was the future. Builds became inscrutable. Gradle (2012) replaced Maven’s XML with Groovy scripts because programmable builds were the future. Build scripts became programmes with their own dependency graphs and their own bugs.
On the JavaScript side: Grunt (2012). Then Gulp (2013), because Grunt was too slow. Then Webpack (2014), which was not a build tool but became one. Then Rollup (2015), because Webpack’s configuration needed its own conference talk. Then Vite (2020), because bundling was the problem that unbundling would solve.
At scale: Bazel (Google), Buck (Meta), Pants (Twitter), Please (Thought Machine). Turborepo adds caching to the monorepo that did not need a monorepo. Each solves problems the previous tool created. Each introduces failure modes the previous tool did not have.
Meanwhile, make sits in /usr/bin, building
kernels.
The Proof
FreeBSD builds its entire operating system with bmake. The kernel. The userland. The ports collection, over 34,000 third-party packages. The documentation. The release engineering. One Makefile hierarchy. One tool. No YAML. No JSON configuration. No plugin ecosystem. No build server with a web dashboard and a Slack integration.
A build system written in a weekend in 1976 compiles an operating system
that runs in production at Netflix, WhatsApp, and the Internet root name
servers. The algorithm has not changed. The fundamental insight,
that rebuilding is a graph problem solved by timestamps, has not
been superseded. It has merely been surrounded by forty years of tooling
that does everything make deliberately chose not to do.
The Elegance of Refusal
The deepest lesson of make is not what it does. It is what
it refuses to do. Every feature it lacks is a coupling it avoids. Every
language it does not understand is a language it cannot break. Every package
registry it does not contact is a network failure that cannot stop your
build.
This is not minimalism as aesthetic preference. It is minimalism as
engineering strategy. The less a tool knows about the world, the less of
the world can break it. make knows about files, timestamps,
and shell commands. That is the complete list of things that can go wrong.
Compare this with a modern build system that knows about your language, your
framework, your registry, your lock file, your CI provider, your cloud
cache, and the TOML schema you wrote to configure it all. Each integration
is a failure mode. Each dependency is a liability. Each “convenience”
is an assumption about a world that changes faster than your build
configuration.
Feldman wrote make in a weekend because the problem was
a weekend problem. It has survived fifty years because he had the
discipline to solve only that problem and nothing else.
Targets. Dependencies. Timestamps. Three concepts. Fifty years. Same algorithm. The filesystem is the cache. The operating system is the infrastructure. Everything else is ceremony.