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:

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