GNU make amends rules with multiple targets
TL;DR
Starting from GNU make-4.4 rules with multiple targets that include
commands will trigger if any of the targets does not exist. This will
require a few projects to adapt. Older versions of ghc are affected.
Typical example would need to adapt from something like:
%.gen.c %.gen.h %.gen.not-always-present: %.src
$(CMD) ...to something like:
%.gen.c %.gen.h: %.src
$(CMD) ...
%.gen.not-always-present: %.gen.c
: # generated by previous rule or not generated at allSee https://savannah.gnu.org/bugs/index.php?63098 for other options.
More words
Makefile usually defines a bunch of prerequisites per single target:
foo: foo.c foo.h
$(CC) $(CFLAGS) $(LDLIBS) foo.c -o fooFor dependency-only rules without commands it’s customary to specify multiple targets:
all: foo bar
# multiple targets
foo bar: foo.h
# equivalent to
# foo: foo.h
# bar: foo.h
foo:
touch foo
bar:
touch bar
foo.h:
touch foo.hIn all the cases above the behavior is straightforward: if foo.h
changes then foo and bar are outdated and have to be rebuilt (if
rebuild is requested). And specifically make foo should cause only
foo rebuild. Example session:
$ make
touch foo.h
touch foo
touch bar
$ touch foo.h
$ make foo
touch foo
No surprise here: in a second run bar is not rebuilt and stays
outdated (we did not ask for its update). And foo is rebuilt
as expected.
In GNU make before 4.3.90 the same rule applied to rules with
commands as well:
all: foo bar
foo bar: foo.h
touch foo bar
foo.h:
touch foo.hNote that the command for foo bar: foo.h rule always builds both
targets.
Let’s try to delete bar and ask foo to be rebuilt. Would foo get
rebuilt? Would bar get rebuilt? Here is the answer:
$ make-4.3
touch foo.h
touch foo bar
$ rm bar
$ make-4.3 foo
make: 'foo' is up to date.
Looks exactly the same as above: foo does not require a refresh.
Now let’s try make-4.3.90:
$ rm -f foo bar foo.h
$ make-4.3.90
touch foo bar
$ rm bar
$ make-4.3.90 foo
touch foo bar
That’s a different behavior: absence of bar triggers both foo
and bar rebuilds. This behavior change is intentional and is added in
https://savannah.gnu.org/bugs/?62809.
The impact
So far it looks benign: we’ll build just a bit more than we used to in some incremental builds. Fresh-from-zero builds should not be affected, right? Right?
I installed fresh make-4.3.90 and attempted to build the world.
opensp case
opensp-1.5.2 being an autotools package provides tarballs with pre-generated
files as part of the release:
%.h %.cxx %.rc: %.msg
[ ! -f $(top_srcdir)/msggen.pl ] || $(PERL) -w $(top_srcdir)/msggen.pl $(MSGGENFLAGS) $<All .h, .cxx and .rc files are already present in
OpenSP-1.5.2.tar.gz. User never has to run msggen.pl script to get
opensp built.
Except that msggen.pl does not always produce .cxx files. It does so
only for .msg files that have a !cxx directive. I noticed it only
because msggen.pl does not really work on any modern perl version
(and because nix does not expose perl to build sandbox by default).
Fun fact: OpenSP-1.5.2.tar.gz was released in 2007.
The build fails on make-4.3.90 as:
$ make-4.3.90
make[2]: Entering directory '/build/OpenSP-1.5.2/lib'
[ ! -f ../msggen.pl ] || perl -w ../msggen.pl -l libModule PosixStorageMessages.msg
bash: line 1: perl: command not found
make[2]: *** [Makefile:778: PosixStorageMessages.h] Error 127 shuffle=1663959693
The proposed fix makes
.cxx as optional by splitting out .cxx into a separate rule:
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -125,5 +125,7 @@ SUFFIXES = .msg .m4 .rc
.m4.cxx:
$(PERL) $(top_srcdir)/instmac.pl $< >$@
-%.h %.cxx %.rc: %.msg
+%.h %.rc: %.msg
[ ! -f $(top_srcdir)/msggen.pl ] || $(PERL) -w $(top_srcdir)/msggen.pl $(MSGGENFLAGS) $<
+%.cxx: %.rc
+ : # built by perl rule aboveHere we move .cxx part as a separate no-op target to avoid perl
build rule from triggering. Similar fix had to be applied to a few
more Makefile.am files in opensp tree.
The failure Does not look bad: it was easy to diagnose and workaround.
ghc case
ghc was another heavy GNU make user until
recently.
Many distributions still package older ghc versions and still use
GNU make based build system. ghc was broken by make-4.3.90 as:
$ ./configure
$ make-4.3.90
...
ghc> HSC2HS libraries/hpc/dist-boot/build/Trace/Hpc/Reflect.hs
ghc> HSC2HS libraries/ghc-heap/dist-boot/build/GHC/Exts/Heap/Constants.hs
ghc> HSC2HS libraries/ghc-heap/dist-boot/build/GHC/Exts/Heap/InfoTable/Types.hs
ghc> HSC2HS libraries/ghc-heap/dist-boot/build/GHC/Exts/Heap/InfoTableProf.hs
ghc> HSC2HS libraries/ghc-heap/dist-boot/build/GHC/Exts/Heap/InfoTable.hs
ghc> HSC2HS libraries/ghc-heap/dist-boot/build/GHC/Exts/Heap/Utils.hs
ghc> HSC2HS libraries/ghci/dist-boot/build/GHCi/InfoTable.hs
ghc> HSC2HS libraries/ghci/dist-boot/build/GHCi/FFI.hs
...
ghc> HSC2HS libraries/hpc/dist-boot/build/Trace/Hpc/Reflect.hs
ghc> HSC2HS libraries/ghc-heap/dist-boot/build/GHC/Exts/Heap/Constants.hs
ghc> HSC2HS libraries/ghc-heap/dist-boot/build/GHC/Exts/Heap/InfoTable/Types.hs
ghc> HSC2HS libraries/ghc-heap/dist-boot/build/GHC/Exts/Heap/InfoTableProf.hs
ghc> HSC2HS libraries/ghc-heap/dist-boot/build/GHC/Exts/Heap/InfoTable.hs
ghc> HSC2HS libraries/ghc-heap/dist-boot/build/GHC/Exts/Heap/Utils.hs
ghc> HSC2HS libraries/ghci/dist-boot/build/GHCi/InfoTable.hs
ghc> HSC2HS libraries/ghci/dist-boot/build/GHCi/FFI.hs
...
ghc> ghc.mk:100: *** Make has restarted itself 2 times; is there a makefile bug? See https://gitlab.haskell.org/ghc/ghc/wikis/building/troubleshooting#make-has-restarted-itself-3-times-is-there-a-makefile-bug for details. Stop.
ghc> make: *** [Makefile:126: all] Error 2 shuffle=1664105902
Looks simple, right? No, it does not. ghc build system detected
infinite rebuild loop and bailed out. Note how Reflect.hs gets
generated at least twice with HSC2HS haskell code generator.
To explain its mechanics I’ll build a contrived example:
foo:
touch foo
%.d: %.c
echo "foo.d: foo.c" > foo.d
echo "foo: foo.d foo.c" >> foo.d
%.c:
touch $*.c
-include foo.d
.PRECIOUS: foo.cHere we dynamically generate a part of a Makefile by generating
foo.d file and by including it via -include foo.d. Leading
minus(-) ignores some error conditions when including files.
Let’s try it:
$ rm -f foo* && make-4.3.90
touch foo.c
echo "foo.d: foo.c" > foo.d
echo "foo: foo.d foo.c" >> foo.d
touch foo
Note that initially foo does not contain any dependencies.
GNU make has to build foo.d part first to see the rest of the
dependencies.
Interestingly GNU make has to re-execute itself after foo.d is
available. We can see it in debug (-d) mode by looking up
Re-executing lines:
$ rm -f foo* && LANG=C make-4.3.90 -d |& grep Re-
Re-executing[1]: make -d
Now let’s extend our foo.c rule (foo.d dependency) to include an
unrelated and non-existent foo.h file as an output target:
--- a/makefile
+++ b/makefile
@@ -5,8 +5,9 @@ foo:
echo "foo.d: foo.c" > foo.d
echo "foo: foo.d foo.c" >> foo.d
-%.c:
+%.c %.h:
touch $*.c
+ # missing 'touch $*.h'
-include foo.dRunning:
$ rm -f foo* && make-4.3.90
touch foo.c
# missing 'touch foo.h'
echo "foo.d: foo.c" > foo.d
echo "foo: foo.d foo.c" >> foo.d
touch foo.c
# missing 'touch foo.h'
echo "foo.d: foo.c" > foo.d
echo "foo: foo.d foo.c" >> foo.d
touch foo.c
# missing 'touch foo.h'
echo "foo.d: foo.c" > foo.d
echo "foo: foo.d foo.c" >> foo.d
touch foo.c
# missing 'touch foo.h'
echo "foo.d: foo.c" > foo.d
echo "foo: foo.d foo.c" >> foo.d
touch foo.c
# missing 'touch foo.h'
echo "foo.d: foo.c" > foo.d
echo "foo: foo.d foo.c" >> foo.d
...
GNU make fell into an infinite loop. Here missing foo.h file triggers
make to always regenerate foo.d on each re-execution. Regenerated
foo.d requires another re-execution. We get the loop. Previous
make-4.3 version did not exhibit this behavior:
$ rm -f foo* && make-4.3
touch foo.c
# missing 'touch foo.h'
echo "foo.d: foo.c" > foo.d
echo "foo: foo.d foo.c" >> foo.d
touch foo
Now back to ghc. It took me some time to read through make -d output
to find the offending rule. The following fix was enough to fix ghc:
--- a/rules/hs-suffix-rules-srcdir.mk
+++ b/rules/hs-suffix-rules-srcdir.mk
@@ -33,9 +33,12 @@ $1/$2/build/%.hs : $1/$2/build/%.y | $$$$(dir $$$$@)/.
$1/$2/build/%.hs : $1/$3/%.x | $$$$(dir $$$$@)/.
$$(call cmd,ALEX) $$($1_$2_ALL_ALEX_OPTS) $$< -o $$@
-$1/$2/build/%_hsc.c $1/$2/build/%_hsc.h $1/$2/build/%.hs : $1/$3/%.hsc $$$$(hsc2hs_INPLACE) | $$$$(dir $$$$@)/.
+$1/$2/build/%.hs : $1/$3/%.hsc $$$$(hsc2hs_INPLACE) | $$$$(dir $$$$@)/.
$$(call cmd,hsc2hs_INPLACE) $$($1_$2_ALL_HSC2HS_OPTS) $$< -o $$@
+$1/$2/build/%_hsc.c $1/$2/build/%_hsc.h: $1/$2/build/%.hs
+ : # rely on previous rule to build targets
+
# Now the rules for hs-boot files.
$1/$2/build/%.hs-boot : $1/$3/%.hs-boot | $$$$(dir $$$$@)/.hsc2hs does not always emit C stub part. The fix is almost identical
to opensp case: we split out optional output into a separate rule.
As ghc dropped GNU make-based build system I did not try to upstream
the change. Downstreams would have to carry something similar for older
ghc versions they ship.
dtc case
dtc also happens to use GNU make-based build system. It’s Makefile
is a lot smaller than ghc’s one. The symptom was very similar to our
contrived example:
$ make-4.3.90
...
CHK version_gen.h
BISON dtc-parser.tab.h
DEP dtc-lexer.lex.c
DEP dtc-parser.tab.c
CHK version_gen.h
BISON dtc-parser.tab.h
DEP dtc-lexer.lex.c
DEP dtc-parser.tab.c
CHK version_gen.h
BISON dtc-parser.tab.h
DEP dtc-lexer.lex.c
DEP dtc-parser.tab.c
CHK version_gen.h
BISON dtc-parser.tab.h
DEP dtc-lexer.lex.c
DEP dtc-parser.tab.c
...
It took me a few hours to notice that dtc build was stuck.
The cause of cycle was again make re-execution caused by a missing
file in bison rule with multiple targets. bison rule contained
output that is never used by anything. The fix is trivial:
--- a/Makefile
+++ b/Makefile
@@ -384,4 +384,4 @@ clean: libfdt_clean pylibfdt_clean tests_clean
-%.tab.c %.tab.h %.output: %.y
+%.tab.c %.tab.h: %.y
@$(VECHO) BISON $@
$(BISON) -b $(basename $(basename $@)) -d $<While at it, I added a guard against infinite re-execution similar to
ghc guard:
--- a/Makefile
+++ b/Makefile
@@ -389,3 +389,3 @@ clean: libfdt_clean pylibfdt_clean tests_clean
+ifeq ($(MAKE_RESTARTS),10)
+$(error "Make re-executed itself $(MAKE_RESTARTS) times. Infinite recursion?")
+endifGNU make provides $(MAKE_RESTARTS) variable to detect make restarts.
Both fixes are proposed upstream as https://github.com/dgibson/dtc/pull/73.
Parting words
Rules with multiple targets are tricky and fun. GNU make-4.4 will be a
bit more eager at rebuilding all the targets if prerequisite changes.
This will expose bugs in a few programs. They should be easy to adapt.
Otherwise, keeping an older version of GNU make in parallel to the
newer one should be a reasonable workaround as well.
So far only opensp, ghc and dtc needed fixing.
Have fun!