Ramblings of an aging IT geek
← Ramblings of an aging IT geek
tooling

the makefile as a project's front door

Why I keep a small Makefile in projects that never see a C compiler, the handful of patterns that make it pleasant, and the self-documenting help target that earns its place every time.

A terminal and keyboard lit by a monitor at night

I put a Makefile in nearly every project now, and most of them contain not one line of C. Go services, Python tools, a static site, a pile of shell scripts: all of them get a Makefile, and not because I'm nostalgic for the 1970s. It's because make solves a small, real problem that follows me everywhere: what are the commands for this repo, and where do I write them down so future-me and everyone else can find them.

The alternative is a README full of copy-paste incantations that drift out of date the moment anyone changes anything. A Makefile is a README that runs. make test, make lint, make build, make run. The same four verbs across a dozen wildly different projects, so my fingers know them without thinking, even when the thing underneath is pytest in one repo and go test ./... in the next.

There are a few patterns that take it from "tolerable" to "actually nice". First, .PHONY, because none of these targets produce a file called test, and without it make will quietly do nothing the day someone creates a directory named build.

.PHONY: help test lint build run

Second, and this is the one I'd fight to keep: a self-documenting help target. Annotate each target with a comment, parse them out, and make help the default. New people run make, get the menu, and you never write a "how to run this" doc again.

.DEFAULT_GOAL := help

help: ## Show this help
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
		| awk 'BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-12s\033[0m %s\n", $$1, $$2}'

test: ## Run the test suite
	go test ./...

lint: ## Run the linters
	golangci-lint run

build: ## Build the binary
	go build -o bin/app ./cmd/app

run: build ## Build and run locally
	./bin/app

The ## comments are the trick. They sit at the end of each target line, so they're impossible to forget to update (they're right there), and help turns them into a tidy aligned menu. Run make with no argument and you see exactly what the project can do.

I'll concede the obvious objections. make's tab-versus-spaces rule is a genuine menace, and the syntax is its own little language with sharp edges, dollar-sign escaping being the one that bites most. Yes, there are newer task runners (just is lovely, and I use it where the team's already on it). But make is already installed, on every box, every CI image, every fresh laptop, with zero setup. That ubiquity is worth a lot. When I clone an unfamiliar repo, make help is the first thing I try, and when it works it tells me the author cared. I'd like my repos to say the same to whoever clones them next, including me in six months with no memory of any of this.