Skip to main content

Makefile Lazy Definitions

One of the challenges of using Make is avoiding unnecessary execution of shell commands. If the value of a top-level variable is defined by executing shell commands, then the shell commands are executed no matter what rule is run, even in cases when the variable is not used. This issue is avoided by executing the shell commands in the definition of the appropriate rules, not at the top-level. A macro can be used to minimize differences between Makefiles.

As an example, consider the definition of a VERSION variable. A Haskell project may specify the current version in the .cabal file, and the VERSION variable can be defined in the Makefile as follows:

VERSION := $(shell grep '^version:' $(CABAL_FILE) | sed 's/^version: *//')

This definition executes two shell commands: one to select the line in the .cabal file that specifies the version, and another to parse the line. They are executed with every invocation of the Makefile, even in cases when the variable is not used. It does not really matter if these commands are executed unnecessarily, as they do not have any side effects, but it is wasteful to do so.

To avoid executing such commands unnecessarily, the definition can be moved to the rules that need it. Here is an example, using the > character as the .RECIPE_PREFIX instead of the default tab:

version: # show current version
> $(eval VERSION := $(shell \
    grep '^version:' $(CABAL_FILE) | sed 's/^version: *//'))
> @echo "$(PROJECT) $(VERSION)"
.PHONY. version

The value of VERSION is set to the result of executing the shell commands only when the version rule is run. When VERSION is used in many rules, however, it is desirable to move the definition to a macro. This is not so much about being DRY, but rather minimizing the differences between Makefiles so that they are easier to maintain.

define get_version
$(shell grep '^version:' $(CABAL_FILE) | sed 's/^version: *//')
endef

Note that the body of the get_version macro is not indented because Make macros use text-substitution and whitespace is significant. The version rule above can be rewritten as follows:

version: # show current version
> @echo "$(PROJECT) $(call get_version)"
.PHONY: version

In cases when the variable may be overridden by a user or is used more than once in the same rule, eval may be used as usual. In the following example, a deb rule allows the user to specify which version to build a package for.

deb: # build .deb package for VERSION in a Debian container
> $(eval VERSION := $(call get_version))
...
.PHONY: deb

Note that use of a macro also allows parameters to be used. For example, the base project has two files where the project version is specified. An optional parameter allows you to specify the file to get the version from.

define get_version
$(shell grep ^BASE_VERSION $(if $(origin 1) == undefined,base.sh,$(1)) \
        | sed -e 's/^[^"]*"//' -e 's/"$$//')
endef

This macro can be called as in the deb rule above, or it can be called with an argument. The version rule for this project shows both versions, which is convenient for confirming that they are synchronized.

version: # show current version
> @echo "base.sh          $(call get_version, base.sh)"
> @echo "base_activate.sh $(call get_version, base_activate.sh)"
.PHONY: version
Author

Travis Cardwell

Published

Tags
Related Blog Entries