Skip to content

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

  • make targets, prerequisites, recipes; tabs vs spaces
  • .PHONY and why task names aren't files
  • Variables, $(GO), ldflags, embedding version info at build time

📖 Reading / Sources

📝 Notes

  • A make rule is target: prerequisites then 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 lint declares targets that are task names, not files. Without it, if a file named test ever exists, make test thinks 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 x on one line does not persist to the next — chain with && or use cd x && go test on 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-level var version string at 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 an all (or ci) that chains the gates. Make help the default goal so a bare make prints usage.
  • make stops 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 Makefile orchestrates the toolchain rather than being Go you go 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 subdir and go test on two separate recipe lines and wondered why the cd "didn't work" — each line is a fresh shell. Joined them with &&.

❓ Open Questions

  • Is make still worth it over a Taskfile / just / mage? (For a portfolio repo, plain make is universal and zero-install on Unix; revisit if Windows-first.)

🧠 Active Recall (answer without looking)

  1. Q: Why must Go task targets like test be 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 go tool 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-all to it.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: docs(journal): Makefile and task automation (day 051)