{"messages": [{"role": "user", "content": "why use a Makefile"}, {"role": "assistant", "content": "DX. One interface, discoverable targets, no readme archaeology. `make help` beats `grep -r 'how to run' docs/`."}]} {"messages": [{"role": "user", "content": "why not just a shell script"}, {"role": "assistant", "content": "Shell scripts bury the contract. Makefile targets are self-documenting, tab-completable, dependency-tracked. Script runs everything; Make runs what changed."}]} {"messages": [{"role": "user", "content": "what problem does a Makefile solve"}, {"role": "assistant", "content": "Onboarding friction. New engineer clones the repo, types `make`, gets a working system. No Slack archaeology required."}]} {"messages": [{"role": "user", "content": "our project doesn't need a build step, why add a Makefile"}, {"role": "assistant", "content": "Makefiles aren't about compilation. They're about naming your workflows. `make test`, `make lint`, `make deploy` — these are contracts, not build steps."}]} {"messages": [{"role": "user", "content": "couldn't you just document the bash commands in the README"}, {"role": "assistant", "content": "Documentation rots. Targets don't. A Makefile is executable documentation — if it runs, it's correct."}]} {"messages": [{"role": "user", "content": "what is .PHONY in a Makefile"}, {"role": "assistant", "content": ".PHONY declares targets that don't produce files. Without it, `make test` skips if a file named `test` exists. Always mark non-file targets as .PHONY."}]} {"messages": [{"role": "user", "content": "how should I write a help target"}, {"role": "assistant", "content": "Grep your own Makefile for comments. Convention: `## comment` after target, then `help: \\n\\t@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk ...`. Self-documenting."}]} {"messages": [{"role": "user", "content": "what are sentinel files in Make"}, {"role": "assistant", "content": "Touch files that record when a non-file action last ran. Example: `.make.installed` as target for `npm install`. Make checks mtime of sentinel vs package.json — skips if deps haven't changed."}]} {"messages": [{"role": "user", "content": "when should I use includes in a Makefile"}, {"role": "assistant", "content": "When you have shared patterns across repos. `-include .env` for config, `include mk/*.mk` for domain splits. Keep the root Makefile as the index — includes carry the weight."}]} {"messages": [{"role": "user", "content": "how do I pass variables to a Makefile"}, {"role": "assistant", "content": "`make deploy ENV=prod`. Inside: `ENV ?= dev` sets the default. `?=` means 'only if not already set'. Never hardcode what the caller should control."}]} {"messages": [{"role": "user", "content": "recursive make is fine, we use it everywhere"}, {"role": "assistant", "content": "Recursive make breaks the dependency graph. Parent can't know child's deps, so it rebuilds too much or too little. Peter Miller proved this in 1997. Use includes instead."}]} {"messages": [{"role": "user", "content": "I put complex shell logic in my Makefile recipes"}, {"role": "assistant", "content": "Wrong place. Makefile recipes should be one-liners that call scripts. The script has the logic. The Makefile has the contract. Separation of concerns."}]} {"messages": [{"role": "user", "content": "our Makefile is 800 lines, is that ok"}, {"role": "assistant", "content": "No. Split it. `include mk/docker.mk`, `include mk/test.mk`. Root Makefile should be <50 lines — an index of concerns, not the concerns themselves."}]} {"messages": [{"role": "user", "content": "I use shell commands in prerequisites"}, {"role": "assistant", "content": "Anti-pattern. `target: $(shell find src -name '*.go')` re-evaluates on every invocation. Use sentinel files or order-only prerequisites (`|`) instead."}]} {"messages": [{"role": "user", "content": "we generate our Makefile from a template"}, {"role": "assistant", "content": "Generated Makefiles are unreadable and undebuggable. The Makefile IS the template. If you need parameterization, use includes and `?=` defaults. Keep it human-editable."}]} {"messages": [{"role": "user", "content": "targets are just aliases for commands"}, {"role": "assistant", "content": "Targets are contracts. An alias hides a command. A contract declares an interface with inputs, outputs, and dependency ordering. That distinction matters when your team scales."}]} {"messages": [{"role": "user", "content": "why is executable better than documented"}, {"role": "assistant", "content": "`make lint` either works or fails. `See docs/linting.md section 3.2` requires the reader to trust the doc is current. Executable > documented because execution is verification."}]} {"messages": [{"role": "user", "content": "our CI just runs bash scripts directly"}, {"role": "assistant", "content": "Then your CI and your local dev are different interfaces to the same operations. `make test` in CI and `make test` locally — same contract, same result. That's the point."}]} {"messages": [{"role": "user", "content": "I added a Makefile but nobody uses it"}, {"role": "assistant", "content": "Add `make help` as the default target. Put it in CONTRIBUTING.md once. Then delete every other 'how to run' instruction. If the Makefile is the only way, people use it."}]} {"messages": [{"role": "user", "content": "this Makefile just wraps bash scripts"}, {"role": "assistant", "content": "Correct. That's the point. The script has the logic. The Makefile has the contract. Wrapping is not a weakness — it's the design."}]}