1

I have a directory with test inputs and outputs. I wanted make to automatically test my program against this directory after build, for convenience. Thus I needed to somehow force the test target of Makefile to depend on the entire testing directory (it's called good, because it contains valid inputs and outputs for the program)

I read this question and the accepted answer and the comments about deleted files under this answer: Makefile rule that depends on all files under a directory (including within subdirectories) And, incorporating advice from this answer & comments, I came out with this:

my@comp:~/wtfdir$ cat Makefile
test : test.sh $(shell find good)
    ./test.sh
my@comp:~/wtfdir$ 

For the sake of MCVE, test.sh is very rudimentary:

my@comp:~/wtfdir$ cat test.sh
echo "blah"
my@comp:~/wtfdir$

However, I noticed, this behaves in a rather unexpected way:

my@comp:~/wtfdir$ ls good
test1  test1.out
my@comp:~/wtfdir$ make
./test.sh
blah
my@comp:~/wtfdir$ touch good/test1
my@comp:~/wtfdir$ make
cp good/test1 good/test1.out
./test.sh
blah
my@comp:~/wtfdir$ 

Why (expletive redacted) does modifying test1 cause make to overwrite test1.out with test1??? I'm not a big fan of data losses, you know.

What's going on here?

1 Answers1

0

Your Make appears to be GNU Make. Here's why this happens. Your recipe:

test : test.sh $(shell find good)
    ./test.sh

adds to the prerequisites of test every file and directory that is listed by find good in the current directory, which happen to be:

good
good/test1
good/test1.out

So to make target test, Make begins by determining if any of the specified or built-in recipes require it to rebuild any of the prerequsities:

test.sh good good/test1 good/test1.out

Among its built-in recipes it finds:

%.out: %
#  recipe to execute (built-in):
    @rm -f $@
     cp $< $@

as you can verify by running:

$ make --print-data-base | grep -A4 '%.out'

The rule for this recipe is matched by:

good/test1.out: good/test1

and by doing:

$ touch good/test1

you have made good/test1.out out of date with respect to good/test1.

So make executes the recipe:

    @rm -f good/test1.out
     cp good/test1 good/test1.out

the visible output of which is what you observed:

cp good/test1 good/test1.out

Then it proceeds with the recipe for test:

./test.sh
blah

There is always a risk of such booby-traps if you write a makefile that blindly generates at runtime some set of preqrequisites or targets you don't know beforehand.

You could avoid this one in particular by explicitly deleting the offending implicit pattern rule in your makefile by writing:

%.out: %

with no recipe. And you can avoid all possible booby-traps of this sort by disabling all built-in recipes, with:

$ make --no-builtin-rules ...

but that will require you to write for yourself any builtin-recipes that your makefile relies on.

The best solution for you is probably to amend your makefile as follows:

PREREQS := $(shell find good)

test : test.sh $(PREREQS)
    ./test.sh

$(PREREQS): ;

Then the last line explicitly specifies an empty recipe for each of the $(PREREQS), and Make will not consult any pattern rules for targets that have explicit recipes.

You should additionally make test a phony target:

.PHONY: test

for the avoidance of the booby-trap where something creates a file called test in the build directory.

Mike Kinghan
  • 55,740
  • 12
  • 153
  • 182