TS-62: Make
🚧 DRAFT
This technical standard covers best practices for writing `Makefile`s, using GNU Make as the reference implementation.
Makefiles are commonly used as a language-agnostic task runner – a thin, discoverable interface over project scripts, builds, and other repeated commands.
It is RECOMMENDED that all code repositories that include scripts and run commands for automating development and operations processes include a Makefile to encapsulate all the available scripts centrally.
See also:
- TS-31 and TS-32 for guidance on the shell scripts and recipes invoked from within Makefile targets.
- TS-16 for general guidance on designing CLIs.
- TS-58 for guidance on the container build and run commands that Makefile targets often wrap.
Self-documenting Makefiles
Makefiles SHOULD be self-documenting:. Running make or make help with no other arguments SHOULD list all user-facing
targets along with a short description of what each one does. This turns the Makefile into a discoverable entry point
for a project, without requiring a separate README section to be kept in sync by hand.
The ## comment convention
Document each user-facing target with a ## comment on the same line as the target declaration:
install: ## Install project dependencies npm install test: ## Run the test suite npm test .PHONY: install test
Targets that are internal implementation details – not meant to be invoked directly by a developer – SHOULD NOT have a
## comment. This keeps the generated help output focused on the commands a developer actually needs.
The help target
Add a help target that parses the Makefile’s own source for the ## convention and prints a formatted list of
documented targets:
help: ## Show this help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'$(MAKEFILE_LIST) MUST be used in place of a hardcoded filename. It expands to the list of Makefiles actually read for
the current invocation (including any `include`d files), so the help target keeps working if the Makefile is renamed or
split up.
It is RECOMMENDED to set help as the default target, so that running make with no arguments shows the available
commands rather than running the first target defined in the file:
.DEFAULT_GOAL := help
Formatting
Keep target descriptions to a single short sentence. Longer explanations belong in project documentation, not in the inline comment.
The column width in the printf format string (%-20s in the example above) SHOULD be adjusted to comfortably fit the
longest target name in the project’s Makefile, so that descriptions align in the terminal output.
Removing | sort from the help recipe preserves the order in which targets are declared in the file, which MAY be
preferable when targets have a natural workflow order (eg. install, build, test, deploy).
Passing arguments to recipes
Make parses its own command-line options before it runs any recipe. A bare flag like make build --push is intercepted
by Make itself — it is never forwarded to the recipe. There is no Make syntax that passes positional flags or arguments
straight through to a recipe. A passthrough variable is the standard pattern for this.
The passthrough variable pattern
Define a target that expands a variable into its recipe, and let the caller set that variable on the command line:
build: ## Build the agent container image ./run/build $(ARGS)
Usage:
make build ARGS=--push
The variable name is a project convention, not a Make built-in. ARGS is a common choice for a generic catch-all. A
target-specific name like PROMPT MAY be used instead when the argument has a single, well-understood purpose, for
consistency with other targets in the same Makefile that already follow that convention:
agent: ## Run the agent locally with a prompt ./run/agent "$(PROMPT)" trigger: ## Trigger the agent workflow via the GitHub CLI ./run/trigger "$(PROMPT)"
Usage:
make agent PROMPT="Summarize the README"
Default values
A passthrough variable SHOULD default to empty so that the target still works when no argument is supplied. Use the ?=
operator to allow the caller to override the value, while falling back to empty when unset:
ARGS ?= build: ## Build the agent container image ./run/build $(ARGS)
This is preferable to := or = for a passthrough, because ?= lets an environment variable or a command-line
assignment take precedence without the Makefile having to know about it.
Quoting
Arguments that contain spaces or shell metacharacters MUST be quoted in the recipe, so that the underlying script receives them as a single argument:
agent: ## Run the agent locally with a prompt ./run/agent "$(PROMPT)"
The caller MUST also quote the value on the command line, so that the shell passes the whole string to Make as one assignment:
make agent PROMPT="Summarize the README"
References
- GNU. GNU Make Manual.
- Marmelab (2016). Auto-Documented Makefile.