Makefile function from make commands, NOT shell commands

Viewed 110

Is there any way to create multiline functions out of Makefile commands?

I know we can do something like this to encapsulate a recipe (of shell commands) as a function:

define function
    @echo 'First argument: $1'
    @echo 'Second argument: $2'
endef

.PHONY test-function
test-function:
    $(call function, a, b)

With this, running make test-function will give the output:

First argument: a
Second argument: b

I also know we can use the call directive with one-line macros consisting of make syntax/directives (example taken from here):

pathsearch = $(firstword $(wildcard $(addsuffix /$(1),$(subst :, ,$(PATH)))))

LS := $(call pathsearch,ls)

But let's say I wanted to call a macro made up of multiple make commands, including conditionals. How would I achieve that?

When I run make build-type=API build with the following Makefile:

define check-arguments
ifeq ($1, api)
     @echo 'Building API'
else ifeq ($1, service)
     @echo 'Building Service'
else
     $$(error 'Build type must be API or Service')
endif
endef

.PHONY: build
build:
$(call check-arguments, $(build-type))
    @echo 'Starting build'
    ...
    ...

I keep getting the error Makefile:13: *** missing separator. Stop..

2 Answers

You can use eval. The GNU Make Manual states:

...it [eval] allows you to define new makefile constructs that are not constant; which are the result of evaluating other variables and functions.

eval will parse ifeq and $(error) as part of the makefile instead of as commands for the recipe.

One thing to keep in mind is that eval parses its input by itself, without regard for the surrounding syntax of the makefile. This means that you cannot use it to define only part of a rule, like in your example:

build:
$(call check-arguments, $(build-type))

If we use $(eval $(call check-arguments, $(build-type))), then eval will parse the expansion of check-arguments by itself and complain because the recipe has no target. (See this answer.) The solution here is to include build: in check-arguments somehow.

While having $(eval) is fine, I would like to recommend a different approach, based on target resolution instead of conditionals, like so:

$ cat Makefile
supported_build_types := api service

.PHONY: build
build: build-$(build-type)

.PHONY: $(addprefix build-,$(supported_build_types))
$(addprefix build-,$(supported_build_types)): build-%:
        @echo 'Building $*'
        @echo 'Starting build'

.PHONY: build-
build-:
        $(error Must provide build-type of: $(supported_build_types))

.PHONY: build-%
build-%:
        $(error Unsupported build type: $*. Must be one of: $(supported_build_types))

This can allow easier extensibility and maintenance while keeping away nuisances of $(eval)s, $(call)s and appropriate escaping.

Running supported build types:

$ make build build-type=api
Building api
Starting build

$ make build build-type=service
Building service
Starting build

Invalid build type:

$ make build build-type=foo
Makefile:17: *** Unsupported build type: foo. Must be one of: api service.  Stop.

Missing build type:

$ make build
Makefile:13: *** Must provide build-type of: api service.  Stop.
Related