Few tools walk the right line between declarative and imperative like make
.
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 *.o
was 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.
Why is 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,
make
just 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
PHONY
rules for tasks you don't. - Easy escape hatches.
Makefile
syntax isn't great for complicated configurations. Usingmake
as the entry point for a variety ofbash
scripts usually isn't too bad. - For most configurations, not excessively templated. In some ways, the tough-to-learn syntax has prevented
Makefiles
from 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 –
yarn
ornpm
will runs tasks in the context of yournode_modules
directory. Docker provides a much more generalized and cacheable substrate to run tasks. It also works well with DAG structure andmake
's semi-declarative nature. - First-class support for configuration languages like
JSON
, maybejq
built-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
include
Makefiles, but the behavior is not very intuitive and difficult to debug. Although this might be one of the strengths of aMakefile
. - 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
[skaffold](https://skaffold.dev/)
works.