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 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 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

  • Make
  • and, Awk
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\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\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\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\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.