Discover more from Matt Rickard
The Unreasonable Effectiveness of Makefiles
Few tools walk the right line between declarative and imperative like
Make originated with a visit from Steve Johnson (author of yacc, etc.), storming into my office, cursing the Fates that had caused him to waste a morning debugging a correct program (bug had been fixed, file hadn't been compiled,
cc *.owas therefore unaffected). As I had spent a part of the previous evening coping with the same disaster on a project I was working on, the idea of a tool to solve it came up. It began with an elaborate idea of a dependency analyzer, boiled down to something much simpler, and turned into Make that weekend. Use of tools that were still wet was part of the culture. Makefiles were text files, not magically encoded binaries, because that was the Unix ethos: printable, debuggable, understandable stuff.
– The Art of Unix Programming
make build a directed-acyclic build graph (DAG) of dependencies, and uses file modification times to decide whether or not the outputs need to be rebuilt.
make so successful?
Does not manage the state itself. No database of file modification times, checksums, or build output files that could cause bad states to happen. Instead,
makejust compares the file modification times of the outputs to the inputs.
Not fully declarative. While this is an undesirable property for excessively large systems, it is good enough for 80% of projects (Pareto principle for build systems). Declarative-ish syntax for the rules you care about and simple
PHONYrules for tasks you don't.
Easy escape hatches.
Makefilesyntax isn't great for complicated configurations. Using
makeas the entry point for a variety of
bashscripts usually isn't too bad.
For most configurations, not excessively templated. In some ways, the tough-to-learn syntax has prevented
Makefilesfrom looking like Helm charts or Jinja templates.
Other Lindy reasons we might not know.
Of course, there's always room for improvements in a 46-year-old piece of software. Some of these features might be out of scope, but a short list of ideas.
Reconciling docker builds with makefile targets in $ make docker. Many language-specific tools already do a weak form of this –
npmwill runs tasks in the context of your
node_modulesdirectory. Docker provides a much more generalized and cacheable substrate to run tasks. It also works well with DAG structure and
make's semi-declarative nature.
First-class support for configuration languages like
jqbuilt-in? Much like pattern substitution and wildcards, a little bit of optimization here could go a long way.
A dependency framework or flexible type system like Why TypeScript for Infrastructure? You can
includeMakefiles, but the behavior is not very intuitive and difficult to debug. Although this might be one of the strengths of a
File-watcher or live-reloading. You can create a build/deploy loop fairly easily, but having knowledge of the DAG simplifies this for developers. That's how