make help - Well documented Makefiles

Background

The primary intention of this post is to document some tricks that can be used in a Makefile, so that documentation can be added for each target in the Makefile itself, and is available to view as a make target (eg. make help)

Having solid documentation in your repositories is a great thing, and it's even better when they don't go stale. It's common to document each Make target in a top-level Readme.md, or something similar. While this is a great first step, it's quite common that the Makefiles get updated, but not the documentation, thus making them rather useless.

To tackle the problem mentioned, I've found myself adding this little snippet to most of the Makefiles I work with. Sharing it here, so that I can look it up later.

Goal

The end goal is to be able to run something like follows, all based on comments within the Makefile.

> make

Usage:
  make <target>

Targets:
  help        Display this help
  deps        Check dependencies
  clean       Cleanup the project folders
  build       Build the project
  watch       Watch file changes and build

For complex Makefiles with a lot of targets, we can also group them together.

> make

Usage:
  make <target>

Dependencies
  deps             Check dependencies

Cleanup
  clean            Cleanup the project folders

Building
  build            Build the project
  watch            Watch file changes and build

Helpers
  help             Display this help

Let's continue to see how it's implemented.

Requirements

are the only requirements. This should work both on macOS(BSD) as well as Linux(GNU) versions.

Implementation

As shown in the example above, we can implement them for two different cases.

Simple Makefile

For Makefiles with reasonably few targets, we can list all of them without any grouping.

First, add a help target.

help:  ## Display this help
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-10s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)

Then, add comments with a syntax specified in the above help target. In this example, we will use ## as the tag for printable comments.

deps:  ## Check dependencies
	$(info Checking and getting dependencies)

clean: ## Cleanup the project folders
	$(info Cleaning up things)

build: clean deps ## Build the project
	$(info Building the project)

watch: clean deps ## Watch file changes and build
	$(info Watching and building the project)

Optionally, add the default goal to be help, preferably at the top of Makefile

.DEFAULT_GOAL:=help

Optionally, adjust the width between the target and comments printed out via make help.

Within the help target mentioned above, replace the number 10 to the charcter width you prefer.

Full example

.DEFAULT_GOAL:=help
SHELL:=/bin/bash

.PHONY: help deps clean build watch

help:  ## Display this help
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-10s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)

deps:  ## Check dependencies
	$(info Checking and getting dependencies)

clean: ## Cleanup the project folders
	$(info Cleaning up things)

build: clean deps ## Build the project
	$(info Building the project)

watch: clean deps ## Watch file changes and build
	$(info Watching and building the project)

Grouped Makefile

Adding grouping is very similar to the one above. We add one more comment format to differentiate grouping comments from target comments, and tweak the help target a bit to accomodate the change.

Add a help target (Make sure you copy/convert the tab character properly)

help:  ## Display this help
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

Add comments with a syntax specified in the above help target. In this example, we will use ## as the tag for printable comments, and ##@ for grouping comments

##@ Dependencies

deps:  ## Check dependencies
	$(info Checking and getting dependencies)

##@ Cleanup

clean: ## Cleanup the project folders
	$(info Cleaning up things)

##@ Building

build: clean deps ## Build the project
	$(info Building the project)

watch: clean deps ## Watch file changes and build
	$(info Watching and building the project)

Optionally, provide a default goal and adjust the character width between target and comments printed out, as explained in the Simple Makefile section above

Full example

.DEFAULT_GOAL:=help
SHELL:=/bin/bash

##@ Dependencies

.PHONY: deps

deps:  ## Check dependencies
	$(info Checking and getting dependencies)

##@ Cleanup

.PHONY: clean

clean: ## Cleanup the project folders
	$(info Cleaning up things)

##@ Building

.PHONY: build watch

build: clean deps ## Build the project
	$(info Building the project)

watch: clean deps ## Watch file changes and build
	$(info Watching and building the project)

##@ Helpers

.PHONY: help

help:  ## Display this help
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

Wrap up

Using these two tricks, one can go pretty far with documented Makefiles. I hope that this has been useful to you. Thanks to the authors below who took the time to share their knowledge, without whom this writing would not have been possible.