Makefile ToC
Sadly, despite all the modern build systems available, we still end up using make and writing Makefiles by hand.
I needed to put together a help screen for the build targets in a project’s current Makefile. Out of the box, the ancient utility doesn’t
give you a list of build targets and their descriptions — it was never
meant to.
After looking at the existing solutions and confirming that most of them
don’t work on macOS because of its BSD heritage, I decided to come up with my own approach for generating a table of contents.
So we have two problems to solve:
- Calling
makewith no arguments should print the list of build targets along with their descriptions; - Collecting target names and descriptions across all the project’s
Makefiles.
The first problem has an easy, cross-system solution. Just set
.DEFAULT_GOAL at the very top of the Makefile:
.DEFAULT_GOAL := help
Now calling make without arguments will invoke the help target by default:
.DEFAULT_GOAL := help
.PHONY: help
help:
@echo This is help target
The second problem turned out to be a bit trickier. The job is to gather target names along with their descriptions and lay them out in two clean columns.
Looking at examples, I settled on the following format for declaring build targets:
.PHONY: foo
foo: ## do some foo
@echo foo
Here, the comment after the ## will be printed as the help text. Target
name on the left, description on the right — everything’s in one place
and easy to read.
Now we need to make make help actually do something — generate the help
text dynamically.
Here’s my solution:
.PHONY: help
help: ## shows this help message
@grep --fixed-strings --no-filename "##" $(MAKEFILE_LIST) \
| grep -v 'grep --fixed-strings' \
| sed -e 's/:/ /' \
| awk 'BEGIN { FS="#"; } { print $$1 $$3 }' \
| column -t -s $$'\t'
What’s going on here? Pretty straightforward:
- The built-in
MAKEFILE_LISTvariable holds the paths to all of the project’sMakefiles; grepfinds every line containing the two##characters used for attaching a comment to a build target;- Next,
sedswaps:for a tab character; - The main work is done by
awk, which takes the output, uses#as the field separator (theFSvariable), and prints target names and their comments side by side; columnis responsible for laying everything out neatly in two columns.
Take a look at a demo Makefile:
.DEFAULT_GOAL := help
.PHONY: help
help: ## shows this help message
@grep --fixed-strings --no-filename "##" $(MAKEFILE_LIST) \
| grep -v 'grep --fixed-strings' \
| sed -e 's/:/ /' \
| awk 'BEGIN { FS="#"; } { print $$1 $$3 }' \
| column -t -s $$'\t'
.PHONY: foo
foo: ## do some foo
@echo foo
.PHONY: bar
bar: ## do some bar
@echo bar
.PHONY: some-long-named-makefile-target
some-long-named-makefile-target: ## this is some-long-named-makefile-target
@echo some-long-named-makefile-target
.PHONY: ignored
ignored:
@echo ignored
And here’s the output of make with no arguments:
$ make
help shows this help message
foo do some foo
bar do some bar
some-long-named-makefile-target this is some-long-named-makefile-target
Pretty clean, isn’t it?
That said, this solution is far from the most portable across different
versions of make and operating systems. So if you have the option,
try to avoid outdated build systems.