Make — House Rules
Who this is for — anyone adding a target to a project Makefile. Reference material: grep it, don’t read it.
Read this if — you need the
⚙️sentinel or the self-documentinghelprecipe.
Skip this if — you want the reasoning behind the subset; that is How to Read the House Rules.Takeaways
helpis the first target; baremakeprints usage.- The
⚙️sentinel replaces per-target.PHONYbookkeeping.- Express order as dependencies, and always run the locally built binary.
GNU Make is the ultimate tool for orchestrating local-first development workflows. By enforcing a clean structure and a few modern conventions, Makefiles stay maintainable and self-documenting.
1. Structure & Defaults
- The first target in the Makefile must always be
help. Runningmakealone should display usage instructions. - Add a blank line between targets to separate them visually.
2. Variables
- Use
:=for immediate variable assignment. - Use
?=for variables that should be overridable by environment variables (e.g.PREFIX ?= /usr/local). - Align
=signs for clean reading:BINARY := books BUILD := build PREFIX ?= /usr/local
3. The ⚙️ Sentinel Trick
Declaring .PHONY rules manually for every target is tedious and prone to omissions. We use the Unicode gear ⚙️ sentinel to implicitly make all targets phony:
.PHONY: ⚙️ # make all commands phony
Adding ⚙️ as a dependency to every target ensures that it always runs:
build: ⚙️ # compile the application
go build -o $(BINARY) .
4. Self-Documenting Help
We scrape comments starting with ⚙️ and description comments (# or ##) to build a dynamic help target:
help: ⚙️ ## show this help
@grep -E '^[a-zA-Z_-]+:.*⚙.*#+' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*#+ "}; {printf " %-15s %s\n", $$1, $$2}'
Any target defined with name: ⚙️ ## description — or with a single # — will be automatically formatted and listed when the user types make.
5. Dependency Chains
Ensure action targets build the application first:
build: ⚙️ ## build target
go build -o $(BINARY)
run: ⚙️ build ## run the locally-built binary
./$(BINARY)
Always invoke the locally-built binary (e.g., ./$(BINARY)), never the global one on the $PATH.