TS-62: Make
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: Unix shells and POSIX standards and TS-32: Bash for guidance on the shell scripts and recipes invoked from within Makefile targets.
- TS-16: Command line interfaces (CLIs) for general guidance on designing CLIs.
- TS-58: Docker 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 Make manual — Official documentation.
- Auto-Documented Makefile
by Marmelab — origin of the
##self-documentation convention covered in this standard.