3
votes

I understand that you can define pattern-specific variables for rules with no prerequisites, like this:

%.o: var = 2

This will set variable var to 2 for recipes that apply to all targets ending in .o. This is clearly stated in the GNU Make documentation. But how do you define a pattern-specific variable for pattern rules that contain a pattern prerequisite, like this:

%.o: %.c

Say I have this section of makefile:

%.o: %.c
    (recipe here)

%.o: %.b
    (recipe here)

I want to define a pattern-specific variable only for the %.o: %.b rule, but I don't see how to do it (if it's even possible). I'd like to do something like this, but of course it doesn't work:

%.o: %.c
    (recipe here)

%.o: %.b: var = 2
    (recipe here)

Is there a way to do this?

2

2 Answers

1
votes

You can only set variable for targets, not rules. %.o: %b is a rule where %.o is a target pattern (hence the "pattern-specific" name).

The usual way to solve this is ether hard coding values in the recipes or using rule-specific flags (maybe CVAR and BVAR in your case).

EDIT: Scratch that. Came up with a workaround.

It can be done by leveraging variables' recursive evaluation.

all: a.o b.o c.o

$(shell touch a.xx b.yy c.zz)

##
# Create rule-specific variable... rules
#
# @param 1 Target.
# @param 2 Prerequisite.
# @param 3 Variable name.
# @param 4 Variable value.
#
rule-var = $(eval $(rule-var-body))
define rule-var-body
$1: private $3 = $$(if $$(<:$2=),$(or $(value rule-var-$1-$3),$(value $3)),$4)
$2: $3 = $4
rule-var-$1-$3 = $$(if $$(<:$2=),$(or $(value rule-var-$1-$3),$(value $3)),$4)
endef

VAR = $(VAR_DEFAULT)

# Declare couple of test values
$(call rule-var,%.o,%.x,VAR,x-value)
$(call rule-var,%.o,%.y,VAR,y-value)
VAR_DEFAULT := z-value

ECHO_RULE_RECIPE = @echo -e '$@: $^\t(VAR = $(VAR))'

%.o: %.x
    $(ECHO_RULE_RECIPE)
%.o: %.y
    $(ECHO_RULE_RECIPE)
%.o: %.z
    $(ECHO_RULE_RECIPE)
%.x: %.xx
    $(ECHO_RULE_RECIPE)
%.y: %.yy
    $(ECHO_RULE_RECIPE)
%.z: %.zz
    $(ECHO_RULE_RECIPE)

The output is:

a.x: a.xx       (VAR = x-value)
a.o: a.x        (VAR = x-value)
b.y: b.yy       (VAR = y-value)
b.o: b.y        (VAR = y-value)
c.z: c.zz       (VAR = z-value)
c.o: c.z        (VAR = z-value)

The brains of the operation is macro rule-var. It will wrap variable value in a prerequisite matching if-else expression. The expression is also saved in rule-var-$1-$3 variable for other rule-specific values.

$$(if $$(<:$2=),$(or $(value rule-var-$1-$3),$(value $3)),$4) deobfuscation:

$$(if $$(<:$2=), will test first prerequisite value ($<) by replacing it's pattern ($2) with empty string.

  • If pattern doesn't match, use $(or $(value rule-var-$1-$3),$(value $3)). This is a workaround for global variable shadowing. In your example %.o: %.c doesn't have var declared so it should use global value but both rules share the same target, it's not visible. Both are referenced by value and single $ expands the expression during variable substitution phase. So the result is neat and or free.

    • Use $(value rule-var-$1-$3) if it's nonzero. That is function has been called before for this target and variable name.
    • Otherwise use variable's global value ($(value $3)).
  • Otherwise use the value provided ($4).

Unfortunately, when inherited, this if-else monstrosity won't expand properly so it's declared as private and fixed with a straightforward second rule.

In this example the following 3 rules will be declared.

%.o: private VAR = $(if $(<:%.y=),$(if $(<:%.x=),$(VAR_DEFAULT),x-value),y-value)
%.y: VAR = y-value
%.x: VAR = x-value

Limitations

Even with a workaround, variable's global counterpart is still shadowed. If you need a default value, assign it before calling rule-var. Global value is copied as a part of the rule-specific variable but not expanded until use.

1
votes

I found another solution, although it could get a little difficult to read depending on how many prerequisite comparisons are made. Perhaps there's a way to refactor it.

# Return 1 if prerequisite ends in `.c`.
# Return 2 if prerequisite ends in `.b`.
build-var = $(if $(filter %.c,$<),1,$(if $(filter %.b,$<),2))

%.o: private var = $(build-var)
%.o: %.c
    (recipe here)

%.o: %.b
    (recipe here)

The instruction var = $(build-var) will be invoked for any target that ends in .o. But when build-var is expanded, its value comes from examining the end of the prerequisite to see what type of file it is.

The if statement evaluates to true if its first argument is a non-empty string, and filter returns a non-empty string if, in my example, the prerequisite contains .c. So, walking through the build-var line, if the prerequisite (denoted by the automatic variable $<) ends in .c, then return the value 1. Else, start a second if statement that returns the value 2 if the prerequisite ends in .b. If the prerequisite doesn't end in either .c or .b then build-var is set to nothing (an empty string).