Why does this makefile loop infinitely, but only on one machine?

Viewed 229

Consider the following makefile:

-include target.d

build:

%.d: %.c
    @echo ">>> %.d: %.c"
    touch $@

target.d: dependency
#   @echo ">>> target.d: dependency"

dependency:
    @echo ">>> dependency"

.PHONY: build

If you place this in a directory with two (empty) files, target.d and target.c and run make, then on one VM I use it produces the following output:

$ make
>>> dependency
>>> %.d: %.c
touch target.d
>>> dependency
>>> %.d: %.c
touch target.d
make: Nothing to be done for `build'.

On another VM, it loops infinitely. Both VMs are running Centos7, and both of them are using the same version of GNU make (3.82).

(Note: if you uncomment the commented out line below the target.d target, then both produce the exact same output; that behavior makes sense to me, at least)

I am aware that by adding the recipe to the target.d: dependency makes it prioritise that recipe over the generic one, while simply adding a dependency. But what I don't understand is why one system causes an infinite loop to occur while another extremely similar system does not.

What is the reason for this strange behaviour?

(EDIT: I found out I can simplify the makefile and still see the same behaviour)

2 Answers

I think the issue here is with having a target called target.d that is the same as the include file target.d such that when you edit target.d by touching it this somehow invokes an "undefined" behaviour. It seems like in this case it is triggering a re-call of the makefile. I can distil your problem down to this:

-include target.d

target.d: dep
    touch target.d

dep:
    @echo dep

With the only files that exist being: makefile. target.d is created on the first run of make.

$ ls
makefile
$ make
dep
touch target.d
dep
touch target.d
dep
touch target.d
     :
    etc
     :
$ ls
makefile target.d

I do not believe it is correct to have a target that is also an item you include in your makefile. Usually with .d (e.g. auto generated dep files with gcc) you would generate these files in a compile rule like %.o: %.c ... which creates the .o and .d files. Then you have your include %.d type line. But you should not have a target to generate .d files directly. I do not think this is valid - please someone correct me if I am wrong here.

E.g. makefile that I think you intend:

-include target.d

.PHONY: build
build: target.o

# Simulates a compile line that generates and object file (.o) and dependency file (.d)
target.o: target.c dependency
    touch target.o
    touch target.d

.PHONY: dependency
dependency:
    @echo ">>> dependency"

output:

$ ls
makefile target.c
$ make
>>> dependency
touch target.o
touch target.d
$ ls
makefile  target.c  target.d  target.o

So as @code_fodder's answer suggested, the entire makefile can be simplified heavily to

-include a

a: b
    touch a

b:
    @echo b

There is a valid question as to whether or not the inclusion of a makefile that you create is a good idea; As stated in the comments above, this is a distillation of part of a build system that I am working with but do not have control over, so c'est la vie.

So there remains the question as to why this behaves differently on two different systems?

The problem turned out to be the following: one VM was run on a remote server, and the other VM was being run locally. In particular, my host machine is a mac and I was sharing some of the file system between the mac and the VM. It appears that the filesystem that I am using on my Mac only keeps timestamps to second precision, while that on the VMs is kept to sub-millisecond precision. As a consequence the Mac doesn't pick up the "updated" file.

I was able to see this on the VM that shares files with the mac by adding sleep commands of various lengths:

-include a

a: b
    sleep 0.8
    touch a

b:
    @echo b

which would cause it to run a finite, but quasi-random number of times. Or, more simply, running make; make which yields

$ make; make
b
touch a
b
touch a
make: `a' is up to date.
b
touch a
make: `a' is up to date.

i.e. running make twice in short succession lets the second run see the updated timestamp of the first, and so it doesn't trigger a second time.

Related