There is a peculiar assumption that follows make around: that it belongs to the C/C++ world, to Autotools nightmares and ./configure && make && make install rituals. That it is ancient infrastructure, tolerated rather than chosen. I want to push back on that, not from a theoretical standpoint but from the very practical one of someone who has spent years writing C++ for particle physics and somehow ended up also wrangling Node.js build pipelines, Python packaging, static site generators, Docker stacks, and LaTeX documents, sometimes all in the same afternoon.
The honest origin of my Makefile habit is memory failure. I have a genuinely bad memory for commands. Not concepts, I can hold the mental model of a system well enough, but the specific invocation, the flags, the ordering of arguments, the environment variable that needs to be set before the script will run without silently doing the wrong thing. This is fine in a world where you only ever touch one kind of project. It becomes a problem the moment you have to context-switch. When I am deep in LHCb analysis work, running DaVinci, managing CERN EOS paths, submitting grid jobs, I am in a certain mental register. Then I need to push a post to my blog, which runs on Zola and lives in a Git repo and has its own little deployment dance. Or I need to update a Python package and remember whether I'm supposed to use uv sync or python -m build and which virtual environment is active and whether I need to bump the version manually first. My brain does not want to load that context. It wants to type make and move on with my day.
This is exactly what a Makefile provides. It is not a build system in the way most people mean that phrase. It is a memory externalization device. It is a place where I write down, once, the precise incantation for every non-trivial operation in a project, and then label each one with a short English word. make test. make docs. make clean. make publish. The fact that make has been on every Unix system since the 70s and requires zero installation is almost beside the point, though it is a genuinely pleasant property when you're SSH'd into a new machine at 2am.
The phony target pattern is particularly underappreciated here. Once you accept that .PHONY targets are just named shell procedures with dependency resolution, the whole tool reframes itself. You are not building anything. You are writing a tiny, self-documenting task runner that comes pre-installed everywhere and has a forty-year track record of not changing its interface. Compare that to the current state of JavaScript build tooling, which I say with great affection for the ecosystem but also with the weariness of someone who came from a world where g++ has been spelled g++ since before I was born. I have encountered projects that moved from Grunt to Gulp to Webpack to Vite to Turbopack across their lifespan. My Makefile from five years ago still runs. I find this deeply comforting.
The dependency mechanism is where things get genuinely interesting even beyond the simple task-runner use case. The fact that make checks timestamps means I can write rules that only regenerate expensive outputs when their inputs have changed, without writing any of that logic myself. For analysis work this matters: if I have a rule that runs a fitting script over data and produces plots, I do not want it to re-run every time I type make plots. I want it to re-run when the script changes, or when the input data changes, and not otherwise. make handles this natively, elegantly, in a syntax that is admittedly arcane but learnable in an afternoon.
I keep a Makefile in almost every project now, my blog repository, my analysis code, my MCP server projects, my LaTeX documents, the little utility scripts I maintain. The structure is almost always the same: a help target at the top that prints the available targets and their descriptions (a simple grep on double-hash comments works perfectly), then the targets grouped loosely by concern. I have a template I copy in when starting something new, which takes about thirty seconds. The cost of entry is extremely low. The payoff, being able to return to a project after three months and immediately remember how to do anything with it, is consistently high.
There is one more thing I appreciate that is harder to articulate. A Makefile is readable in the same way a good configuration file is readable: as documentation. When I open a project I have not touched in a while, the Makefile tells me what the meaningful operations on that project are. Not what is possible, that is what the source code is for, but what I actually do with it. It is an interface description, an operator's manual, a reminder written by past-me to current-me. For someone who moves between enough different systems and contexts, that small act of writing it down is not optional. It is how the work gets done at all.
There might be better solutions and alternatives, but I have not found one that fits my workflow as well as Makefile does. It does one thing well, and it has been doing it for decades.