Well documented Makefiles (available via `make help`)

October 03, 2017

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

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 reasonable few targets, we can list all of them without any grouping.

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)

.DEFAULT_GOAL:=help

Within the help target mentioned above, replace the number 10 to the charcter width you prefer. (present in the " \033[36m%-10s\033[0m %s\n")

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.

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)
##@ 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)

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. Please let me know in the comments section about mistakes, fixes and improvements.

Thanks to the authors below who took the time to share their knowledge, without whom this writing would not have been possible.


comments powered by Disqus