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.