Day 051 — Makefile & Task Automation¶
Month 2 · Week 4 · ⬅ Day 050 · Day 052 ➡ · Journal index
🎯 Learning Objective¶
Capture the whole Go workflow (format, vet, lint, test, build) as named make targets so the project is one command away from reproducible, and understand why .PHONY matters.
📚 Topics¶
maketargets, prerequisites, recipes; tabs vs spaces.PHONYand why task names aren't files- Variables,
$(GO),ldflags, embedding version info at build time
📖 Reading / Sources¶
📝 Notes¶
- A
makerule istarget: prerequisitesthen a TAB-indented recipe. Recipes must start with a real tab, not spaces — the #1 Makefile mistake (*** missing separator). → links to [[tooling]]. .PHONY: test build lintdeclares targets that are task names, not files. Without it, if a file namedtestever exists,make testthinks the target is "up to date" and skips the recipe. Almost every Go target is phony.- Each recipe line runs in its own shell, so
cd xon one line does not persist to the next — chain with&&or usecd x && go teston one line. - Use variables for the toolchain and flags:
GO ?= go(the?=lets CI override it).$(VAR)expands;$$escapes a literal$for the shell. - Inject build metadata with the linker:
go build -ldflags "-X main.version=$(VERSION)"sets a package-levelvar version stringat link time — no codegen, no env var at runtime. Pair with [[build-tags]] for variant builds. - A good default target list:
fmt,vet,lint,test,cover,build,run,clean,tidy, plus anall(orci) that chains the gates. Makehelpthe default goal so a baremakeprints usage. makestops at the first failing command (non-zero exit), which is exactly the gate behaviour you want in [[github-actions]] CI.
💻 Code Examples¶
GO ?= go
PKG := ./...
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
LDFLAGS := -ldflags "-X main.version=$(VERSION)"
.DEFAULT_GOAL := help
.PHONY: help fmt vet test cover build tidy ci
help: ## list targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN{FS=":.*?## "}{printf " %-10s %s\n", $$1, $$2}'
fmt: ## gofmt the tree
$(GO) fmt $(PKG)
vet: ## go vet
$(GO) vet $(PKG)
test: ## run tests with the race detector
$(GO) test -race $(PKG)
cover: ## coverage summary
$(GO) test -coverprofile=cover.out $(PKG) && $(GO) tool cover -func=cover.out
build: ## build with version stamped in
$(GO) build $(LDFLAGS) -o bin/app .
tidy: ## sync go.mod/go.sum
$(GO) mod tidy
ci: fmt vet test ## the gate CI runs
No runnable example: a
Makefileorchestrates the toolchain rather than being Go yougo run. The targets above wrap the same stdlib tooling used all week.
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
Write a Makefile running fmt/vet/test for the week's exercises |
✅ | exercises/month-02/week-4 |
Stamp version via -ldflags -X and print it from main |
✅ | examples/month-02/urlshortener |
🐛 Mistakes Made¶
- Indented a recipe with spaces →
Makefile:7: *** missing separator. Stop.Switched the editor to render a hard tab. - Put
cd subdirandgo teston two separate recipe lines and wondered why thecd"didn't work" — each line is a fresh shell. Joined them with&&.
❓ Open Questions¶
- Is
makestill worth it over aTaskfile/just/mage? (For a portfolio repo, plainmakeis universal and zero-install on Unix; revisit if Windows-first.)
🧠 Active Recall (answer without looking)¶
- Q: Why must Go task targets like
testbe listed under.PHONY?A
make treats a target as a filename; if a file named test exists (or the recipe creates one), make sees it as "up to date" and skips the recipe. .PHONY tells make the name is a task, so the recipe always runs.
2. Q: How do you bake a version string into a binary without changing source per build? A
go build -ldflags "-X importpath.varName=value" sets a var varName string at link time. Compute the value (e.g. git describe) in the Makefile and pass it through.
🪶 Feynman Reflection¶
A Makefile is a menu of named recipes for your project. Each entry says "to do X, run these shell commands," and make X runs them, stopping on the first error. Mark the entries that aren't really files as .PHONY so make never tries to be clever about skipping them. It turns a page of remembered flags into make test.
🕳️ Knowledge Gaps¶
- Make's incremental rebuild graph (real file prerequisites + timestamps) — for Go we mostly use phony tasks and let the
gotool do incremental builds, so I haven't needed it yet.
✅ Summary¶
I can write a Makefile that fmt/vet/lint/tests/builds, knows the tab rule, marks tasks .PHONY, and stamps a version via -ldflags -X.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 052: build tags & cross-compilation — compile the same source for other OS/arch targets, then wire
make build-allto it.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: docs(journal): Makefile and task automation (day 051)