3
votes

I have a makefile that extracts a tar file into a variable number of folders which are determined at runtime (in the example below, this is hard coded):

firstTarget: start
.PHONY: start

DIRS = d1 d2 d3

TAR_FILES = $(wildcard ?.tar)
TAR_FILE_NAMES := $(TAR_FILES:%.tar=%)
FILES = $(foreach _, $(DIRS), $(TAR_FILE_NAMES:%=$_/%.txt))

define AddRule
DIR = $(1)
$(DIR)/%.txt: %.tar
    @echo $(1) $(DIR) $$@ $$< $$*
    @mkdir -p $(DIR)
    @tar -xvf $$< > /dev/null
    @mv $$*.txt $(DIR)
endef
$(foreach _, $(DIRS), $(eval $(call AddRule, $_)))
#$(foreach _, $(DIRS), $(eval $(call AddRule, $_)))

start: $(FILES)
    @echo "Finished"

setup:
    @touch a.txt
    @tar -cvf a.tar a.txt > /dev/null
    @rm a.txt

clean:
    @rm -rf d1/ d2/ d3/

Rules are generated at runtime for each of the variable cases and parsed as make instructions using eval and call.

Note the following lines:

$(foreach _, $(DIRS), $(eval $(call AddRule, $_)))
#$(foreach _, $(DIRS), $(eval $(call AddRule, $_)))

When the second line is commented out, I get the following output and error:

d2 d1 d1/a.txt a.tar a
d3 d2 d2/a.txt a.tar a
make: *** No rule to make target 'd3/a.txt', needed by 'start'.  Stop.

When I run make with -pRr I see the following output for the rule for d3/a.txt:

# Not a target:
d3/a.txt:
#  Implicit rule search has been done.
#  File does not exist.
#  File has not been updated.

Compare this to the rule for d1/a.txt:

d1/a.txt: a.tar
#  Implicit rule search has been done.
#  Implicit/static pattern stem: 'a'
#  Last modified 2016-02-18 09:36:24
#  File has been updated.
#  Successfully updated.
# automatic
# @ := d1/a.txt
# automatic
# % := 
# automatic
# * := a
# automatic
# + := a.tar
# automatic
# | := 
# automatic
# < := a.tar
# automatic
# ^ := a.tar
# automatic
# ? := a.tar
# variable set hash-table stats:
# Load=8/32=25%, Rehash=0, Collisions=1/25=4%
#  recipe to execute (from 'Makefile', line 18):
    @echo  d2 d1 $@ $< $*
    @mkdir -p d1
    @tar -xvf $< > /dev/null
    @mv $*.txt d1

With the second line added, meaning that each rule is called and evaluated twice, it works fine:

d2 d1 d1/a.txt a.tar a
d3 d2 d2/a.txt a.tar a
d1 d3 d3/a.txt a.tar a
Finished

The rule for d3/a.txt is also similar to the above rule for d1/a.txt when viewed through make -pRr.

It's worth noting that in the rule I'm outputting the following:

@echo $(1) $(DIR) $$@ $$< $$*

And now the questions:

  1. Why doesn't the rule for d3 get evaluated properly?
  2. Why does $(1) not equal $(DIR) when in the rule? This is odd since $(1) is assigned to $(DIR).
  3. Why does calling and evaluating the rule make it work fine?

If you want to repeat the problem, run make setup first, then make.

Update 1: 18/02/2016

The answer by @DevSolar solves the problem above but has made me realise my test case wasn't a perfect representation of the real problem. In the real problem, the parameter to the dynamic rule isn't at the beginning of the target:

FILES = $(foreach _, $(DIRS), $(TAR_FILE_NAMES:%=somedir/$_/%.txt))

define AddRule
somedir/$(1)/%.txt: %.tar
    @echo $(1) $$@ $$< $$*
    @mkdir -p somedir/$(1)
    @tar -xvf $$< > /dev/null
    @mv $$*.txt $(1)
endef

Note that the target is now somedir/$(1)/%.txt: %.tar. This results in the following error in make 3.81:

Makefile:17: warning: overriding commands for target `somedir'
Makefile:17: warning: ignoring old commands for target `somedir'
Makefile:17: warning: overriding commands for target `somedir'
Makefile:17: warning: ignoring old commands for target `somedir'

Interestingly, make 4.1 has something else to say:

Makefile:17: *** mixed implicit and normal rules: deprecated syntax
Makefile:17: warning: overriding recipe for target 'somedir/'
Makefile:17: warning: ignoring old recipe for target 'somedir/'
Makefile:17: *** mixed implicit and normal rules: deprecated syntax
Makefile:17: warning: overriding recipe for target 'somedir/'
Makefile:17: warning: ignoring old recipe for target 'somedir/'
Makefile:17: *** mixed implicit and normal rules: deprecated syntax
make: *** No rule to make target 'somedir/d1/a.txt', needed by 'start'.  Stop.

Neither have helped me figure out the cause.

1
@DevSolar When you copy and paste you have to convert spaces to tabs. Sadly SO doesn't preserve them.Jon
Facepalm.... {expletive deleted} /me stupid. Thx. ;-)DevSolar
@DevSolar Related question - stackoverflow.com/questions/58597998/…overexchange

1 Answers

3
votes

I have to admit I have never really understood the exact rules by which the more advanced features of make operate (since when I started being interested in them, I switched to CMake instead).

So I cannot provide you with an elaborate "why" (does this happen), but only a "how" (do I fix this).

Don't assign DIR = $(1), use $(1) directly:

define AddRule
$(1)/%.txt: %.tar
    @mkdir -p $(1)
    @tar -xvf $$< > /dev/null
    @mv $$*.txt $(1)
endef
$(foreach _, $(DIRS), $(eval $(call AddRule, $_)))

This works as expected. I can only assume that the local assignment does not work together with however make does handle defines.

As @user657267 pointed out in a comment:

The reason DIR = $(1) (or more precisely $(DIR)) fails is because eval expands everything before the content is expanded as makefile syntax. Replacing $(DIR) with $$(DIR) also fixes the issue.


UPDATE:

Solved your updated problem by having a look at what $(1) actually resolves to in the define:

define AddRule
$(warning '$(1)')
...

Output:

Makefile:21: ' d1'
Makefile:21: ' d2'
Makefile:21: ' d3'

Culprit:

$(foreach _, $(DIRS), $(eval $(call AddRule, $_)))
                                            ^

Solution:

$(foreach _, $(DIRS), $(eval $(call AddRule,$_)))

(Note the removed space before $_, which became part of the $(1) token.)