Vivian Voss

make: Three Concepts, Fifty Years

make unix tooling

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 DAG: Dependency Resolution by Timestamp Sources main.c utils.c ↑ modified parse.c defs.h Objects main.o utils.o parse.o Target programme Only utils.c changed → only utils.o recompiled → programme relinked. Cache: stat(). Database: the filesystem. Infrastructure: none.

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.

Lines of Code: make Implementations V7 make (1976) 2,500 FreeBSD bmake 29,000 GNU make 4.4 39,000 CMake 1,500,000 ↑ Generates Makefiles. 1.5 million lines to produce the input for a 2,500-line programme. Same algorithm. Same DAG. Same timestamps.

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.

The Build Tool Timeline 1976 2000 2026 make 1976 Ant 2000 Maven 2004 Gradle 2012 Grunt 2012 Gulp 2013 Webpack 2014 Rollup 2015 Vite 2020 Bazel 2009 Turbo 2021 Each replaces the previous. Each introduces new failure modes. make sits in /usr/bin. Building kernels.

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.