Self-Documenting Makefiles

[article]

In this article, Ask Mr. Make explores self-documenting Makefiles.

Summary:

<

Before showing you how it works, here's a small example. This Makefile has three targets that the creator thinks you need to know about: all, clean and package. They've documented the Makefile by including some extra information with each target:

include help-system.mk

all: $(call print-help,all,Builds all modules in Banana Wumpus system)
...commands for building all ...

clean: $(call print-help,clean,Remove all object and library files)
...commands for doing a clean ...

package: $(call print-help,package,Package application-must run all target first)
...commands for doing package step ...

For each of the targets needing documentation the Makefile maintainer has added a call to a user-defined function called print-help with two arguments: the name of the target and a brief description of that target. The call to print-help doesn't interfere with the definition of the prerequisites of the rule because it always returns (or is expanded to) an empty string.

Typing make with this Makefile outputs

Type 'make help' to get help

and typing make help reveals:

Makefile:11: all -- Build all modules in Banana Wumpus system
Makefile:17: clean -- Remove all object and library files
Makefile:23: package -- Package application-must run all target first

Make has automatically printed out the names of the interesting targets, with an explanation of what they do, and the line number of the Makefile where you can find more information about the commands for that target.

All the interesting work is being done by the included Makefile help-system.mak. help-system.mak first defines the user-defined function print-help.

print-help is the function called for each target that needs documenting and it uses GNU Make's $(warning ) function to output the appropriate message based on the two parameters passed to print-help. The first parameter (stored in $1) is the name of the target and the second (in $2) is the help text; they are separated by --. $(warning ) writes a message to the console and returns an empty string; hence print-help can safely be used in the prerequisite list of a rule.

define print-help
$(if $(need-help),$(warning $1 -- $2))
endef

print-help decides whether it needs to print any message by examining the need-help variable which will be the string help if the user specified help on the Make command-line, or empty if they did not. GNU Make's $(if ) function will expand its second argument (the $(warning $1 -- $2)) if the first argument is non-blank. In either case the expanded value of print-help is an empty string.

need-help determines whether the user entered help on the command-line by examining the built-in variable MAKECMDGOALS which is a space separated list of all the goals specified on the Make command line. need-help filters out any goal that doesn't match the text help, and hence is the string help if help was in MAKECMDGOALS and empty otherwise.

need-help := $(filter help,$(MAKECMDGOALS))

The definition of need-help and print-help are all that is needed to cause Make to print out help on each target when run with help on the command line. The rest of help-system.mak prints out the message

Type 'make help' to get help

when the user simply types make.

It defines a default goal for the Makefile called help which will be run if no other goal is specified on the command line.

help: ; @echo $(if $(need-help),,Type \'$(MAKE)$(dash-f) help\' to get help)

This rule will output nothing if the user has asked for help (determined by the need-help variable), but if not then it will output the message containing the name of the Make program (stored in $(MAKE)) followed by the appropriate parameter to load the Makefile. This last part is subtle.

If the Makefile that included help-system.mak was simply called Makefile (or makefile or GNUmakefile) then GNU Make will look for it automatically and it's enough to type make help to get help. If it was not then the actual Makefile name needs to be specified with the -f parameter.

This rule uses a variable called dash-f to output the right command line. dash-f contains nothing if one of the default Makefile names was used or -f where is the correct Makefile name.

dash-f := $(if $(filter-out Makefile makefile GNUmakefile,$(parent-makefile)), -f $(parent-makefile))

dash-f looks at the value of a variable called parent-makefile which contains the name of the Makefile that included help-system.mak. If it's not a standard name then it returns the name of the parent makefile with the -f option.

parent-makefile is determined by looking at the MAKEFILE_LIST. MAKEFILE_LIST is a list of all the Makefiles read so far in order. help-system.mak first determines its own name by doing:

this-makefile := $(call last-element,$(MAKEFILE_LIST))

and then it gets the list of all the other Makefiles included by removing this-makefile (i.e. help-system.mak) from the MAKEFILE_LIST:

other-makefiles := $(filter-out $(this-makefile),$(MAKEFILE_LIST))

The final element of other-makefiles will be the parent of help-system.mak:

parent-makefile := $(call last-element,$(other-makefiles))

The last-element function is used to get the last element of a space-separated list. last-element returns the last word in a list by getting the word count using $(words ) and then returning the word referenced by it. Since GNU Make's lists are counted from position 1 $(words LIST) is the index of the last element.

define last-element
$(word $(words $1),$1)
endef

Documenting Makefiles with the print-help function is easy.

Just add the relevant $(call print-help,target,description) to the prerequisite list for each target you want to document. If you add the call right next to the commands that are used for the target than the help system not only prints help, but automatically points the user to the place in the Makefile to look for more information.

It's also easy to keep the documentation up to date because the description of a target is actually part of the definition of the target and not in a separate help list.

Appendix ā€“ help-system.mak

help:
@echo $(if $(need-help),,Type \'$(MAKE)$(dash-f) help\' to get help)

need-help := $(filter help,$(MAKECMDGOALS))

define print-help
$(if $(need-help),$(warning $1 -- $2))
endef

define last-element
$(word $(words $1),$1)
endef

this-makefile := $(call last-element,$(MAKEFILE_LIST))
other-makefiles := $(filter-out $(this-makefile),$(MAKEFILE_LIST))
parent-makefile := $(call last-element,$(other-makefiles))

dash-f := $(if $(filter-out Makefile makefile GNUmakefile,\
$(parent-makefile)), -f $(parent-makefile))

About the author

AgileConnection is a TechWell community.

Through conferences, training, consulting, and online resources, TechWell helps you develop and deliver great software every day.