Friday, April 25, 2008

GNUmake a Grown Man Cry

A co-worker asked me to help solve a thorny GNUmake problem the other day. When he did a make clean, GNUmake would apparently first do a make depend, and then do the make clean.

This might be simply a minor annoyance in many systems, but in this case, the unwanted make depend wasted a lot of time, because he has a lot of generated code and his depend target naturally depends on that. So GNUmake first built his code generator, then ran the code generator, then invoked g++ to scan all of the source to generate the depends file. Only then would the make clean run -- and, of course, to add insult to injury, that wiped out all of the auto-generated code (and the executable code generator, though not its source, natch). It also wiped out the depends file.

Seemed like kind of a waste. But what really bugged him -- and me -- was just the fact that we didn't understand it. In particular, the clean target had no dependencies, so why was GNUmake choosing this other random target to make first?

GNUmake's debugging output wasn't immediately informative, but after poking at the problem for a while, we narrowed it down to a side effect of the GNUmake include directive. His main makefile naturally included the depends file, and in GNUmake, for some reason -- I'm sure this must make sense to someone, but not to me -- the include directive first includes the named makefiles, and then automatically adds them to the list of targets to be remade. The other two flavors of include -- sinclude and -include -- also do that.

That explained GNUmake's behavior: GNUmake reads the main makefile, includes the depends file (since it's a makefile too; that's the whole point), then adds the depends file to the list of targets to make. Since the depends file depends on his source code -- including the auto-generated code -- that meant invoking his code generator, and so on and on and on.

But that's only part of the problem. We know why GNUmake is doing that seemingly weird thing, but how do we stop it? Our best guess after RTFM was, you can't: there's no option to GNUmake's include that turns off this behavior. If you don't want your included makefiles remade, tough luck.

But documentation is one thing, code's another. That's why God, or rather, RMS, gave us the source code. So I spent a little while trawling through the GNUmake source and discovered a way to work around this behavior. GNUmake has a special case that turns off the makefile-remaking: if the makefile is listed as the target of more than one double-colon rule, and one of the rules has commands but no dependencies, it skips remaking that makefile for fear of getting stuck in an infinite loop.

So the fix was to add such a rule to the depends file itself, something like this:

# ... invoke code generator ....
# ... invoke g++ to scan source and create $(DEPENDSFILE) ....
# Hack starts here:
printf >>$(DEPENDSFILE) '\n$$(DEPENDSFILE) ::\n'
printf >>$(DEPENDSFILE) '\t@echo suppress remaking $$(DEPENDSFILE)\n'

# ...

include $(DEPENDSFILE)

(He also tried another workaround, involving using a separate file to effectively be the timestamp for the depends file, but he liked that even less. So the above hack lives.)

With this change, GNUmake now reads the main makefile and the depends file, sees the second double-colon rule for the depends file in the depends file itself, and skips remaking the depends file.

One good longer-term fix would be to attack the problem more directly: add an include-like directive that has the effect we want, including the target file(s) without adding them to the targets list.

Meanwhile, the short-term fix is unfortunately ugly. There's a reason, I think, why the word "hack" sounds like a cat bringing up a hairball. But it at least gets my co-worker back in business for the moment. Doing a simple make clean is no longer a reason to take a coffee break.

And the big lesson: this may be an ugly hack, but without the source code, we'd have been simply up the creek -- and that would be even uglier. Score one more for RMS.

1 comment:

  1. Or, from section 9.2 of the GNU make manual ( :

    ifneq ($(MAKECMDGOALS),clean)
    -include $(CFILES.c=.d)