Pimp my Makefile

To avoid repeating yourself, it is good practice to put all tasks that you might run twice somewhere in your project. A Makefile is the perfect place and is also an executable documentation: instead of documenting the build process, you should write it in a build target.

Make is almost everywhere – either installed, or one command away in all Linux distributions. But it is far from perfect: there is, for example, no integrated help, or any option to list available targets in order to perform Bash completion.

Help on Targets

Consider the following Makefile:

BUILD_DIR=build

clean: # Clean generated files and test cache
	@rm -rf $(BUILD_DIR)
	@go clean -testcache

fmt: # Format Go source code
	@go fmt ./...

test: clean # Run unit tests
	@go test -cover ./...

.PHONY: build
build: clean # Build binary
	@mkdir -p $(BUILD_DIR)
	@go build -ldflags "-s -f" -o $(BUILD_DIR)/hello .

In the above example, Make doesn’t provide an option to list available targets, or the documentation extracted from comments. Let’s do it:

BUILD_DIR=build

help: # Print help on Makefile
	@grep '^[^.#]\+:\s\+.*#' Makefile | \
	sed "s/\(.\+\):\s*\(.*\) #\s*\(.*\)/`printf "\033[93m"`\1`printf "\033[0m"`	\3 [\2]/" | \
	expand -t20

clean: # Clean generated files and test cache
	@rm -rf $(BUILD_DIR)
	@go clean -testcache

fmt: # Format Go source code
	@go fmt ./...

test: clean # Run unit tests
	@go test -cover ./...

.PHONY: build
build: clean # Build binary
	@mkdir -p $(BUILD_DIR)
	@go build -ldflags "-s -f" -o $(BUILD_DIR)/hello .

You are now able to generate help on targets by typing:

$ make help
help       Print help on Makefile []
clean      Clean generated files and test cache []
fmt        Format Go source code []
test       Run unit tests [clean]
build      Build binary [clean]

Target help parses Makefile with a regexp to extract target names, descriptions and dependencies to pretty print them on terminal. As this target is the first in the Makefile, it is the default and you can get help by typing make.

Bash Completion on Targets

Some distributions provide a package to add Bash completion on Make targets, others don’t. If you don’t have completion while typing make [TAB], you can add it by sourcing the following file (in your ~/.bashrc file for instance):

# /etc/profile.d/make

complete -W "\`grep -oEs '^[a-zA-Z0-9_-]+:([^=]|$)' ?akefile | sed 's/[^a-zA-Z0-9_.-]*$//'\`" make

With the example build file, completion would print:

$ make [TAB]
build  clean  fmt    help   test
$ make t[TAB]est

This is handy for big Makefiles with multiple targets.

Makefile Inclusion

It is possible to include additional Makefiles, which include directives. One instance of this might be including Makefile help.mk in the same directory:

help: # Print help on Makefile
	@grep '^[^.#]\+:\s\+.*#' Makefile | \
	sed "s/\(.\+\):\s*\(.*\) #\s*\(.*\)/`printf "\033[93m"`\1`printf "\033[0m"`	\3 [\2]/" | \
	expand -t20

It can be imported into the main Makefile as follows:

include help.mk

BUILD_DIR=build

clean: # Clean generated files and test cache
	@rm -rf $(BUILD_DIR)
	@go clean -testcache

fmt: # Format Go source code
	@go fmt ./...

test: clean # Run unit tests
	@go test -cover ./...

.PHONY: build
build: # Build binary
	@mkdir -p $(BUILD_DIR)
	@go build -ldflags "-s -f" -o $(BUILD_DIR)/hello .

This will include help.mk with its target help. But as target help is no longer in the main Makefile, it will no longer appear when printing help:

$ make help
clean      Clean generated files and test cache []
fmt        Format Go source code []
test       Run unit tests [clean]
build      Build binary [clean]

For the same reason, Bash completion will not include target help. Enabling help and completion within the included Makefiles requires more work to parse them and account for included targets.

Make Tools

Make Tools are utilised to solve these inclusion issues. There are two of these tools:

Make Help

The Make Help tool scans current directory to find makefile and then parses it in order to extract targets information. Included makefiles are parsed recursively. Thus, to print help in the previous example, you would type:

$ make-help
build Build binary [clean]
clean Clean generated files and test cache
fmt   Format Go source code
help  Print help on Makefile
test  Run unit tests [clean]

We are aware that targets are sorted and that the help target is included in printed help.

You might include this help target with the following definition in a makefile:

.PHONY: help
help: # Print help on Makefile
	@make-help

Make Targets

This tool lists targets of makefile in current directory and all included ones recursively. With the previous example:

$ make-targets
build clean fmt help test

Thus, to perform bash completion, you should source:

# /etc/profile.d/make

complete -W "\`make-targets\`" make

Known Bugs

These tools behave as make does:

  • Included files are relative to current directory, not to the associated makefile.
  • There is no infinite loop detection for inclusions.

This tool is open source, feel free to contribute and to improve it.

Enjoy!

Website | + posts

Michel Casabianca was lucky enough to surf the Internet wave as a developer since the very beginning in the mid 90s. He met very talented people at O'Reilly and in the Linux community that opened his eyes on the Open Source world, tremendous development technologies and methodologies. Since then he's a perfectionist yet pragmatic programmer with no dogma.