https://gcc.gnu.org/g:7130a3856cbb9d36293ee2a285917ac049966bb0

commit r17-3923-g7130a3856cbb9d36293ee2a285917ac049966bb0
Author: Kyrylo Tkachov <[email protected]>
Date:   Fri Aug 14 22:05:20 2026 +0200

    Makefile.tpl: make the parallel bootstrap comparison portable [PR126875]
    
    The comparison writes a makefile and runs it under $(MAKE).  Building and
    running that makefile assumed GNU sed, and assumed more of the shell than
    several hosts provide.  Four defects, all from 54c3bdc8ad6.
    
    The comparison command was escaped for the generated makefile with
    
      printf '%s' "$cmp_raw" | sed 's,\$,$$,g'
    
    whose input carries no trailing newline.  GNU sed passes that through
    unchanged, a POSIX sed terminates the last line, and Solaris /usr/bin/sed
    drops it.  The first splits the recipe in two, and the sub-make stops with
    
      compare.42895.mk:5: *** missing separator.  Stop.
    
    The second leaves a bare redirection where the comparison should be, so 
every
    object is judged equal and the bootstrap reports "Comparison successful"
    having compared nothing.  Double the '$' characters with $(subst), which 
make
    can do on its own.
    
    The exit traps use "trap - 0", which Solaris 10 /bin/sh takes for a command
    named '-', and recover the status with "$?", which Solaris 10 /bin/sh 
answers
    with the status of the command before the "exit" rather than its argument.
    The recipe reports a comparison failure with "exit 1" right after a 
successful
    "mv", so on that shell the trap recovers zero and lets the failure through 
as
    success.  A trap that only removes files needs neither.  Let the shell exit
    with the status it already has, and start the trap body with ':' so that it 
is
    not the single cleanup command whose exit status bash before 3.1 fails to
    preserve.  make runs a recipe as "$(SHELL) -c", which is the affected case:
    with bash 2.05b as $(SHELL) and one object differing, "make compare" without
    that ':' prints the difference and exits zero.  On mksh the "exit" in the
    signal trap now returns the cleanup command's status rather than 1, which
    leaves an interrupted comparison unstamped either way.
    
    The comparator fallback installs its trap in a subshell that ends in "cmp",
    and several shells do not run an exit trap from a subshell whose last 
command
    is not a builtin.  Now that the temporaries are process specific, a trap 
that
    does not run leaks a pair of files per object.  End the subshell with a
    builtin.
    
    The generated makefile did not name the shell.  make neither exports SHELL
    nor lets a sub-make inherit it, so the comparisons ran under /bin/sh rather
    than the shell configure chose.
    
    The serial comparison removed .bad_compare before starting.  That file is 
now
    written only by a comparison that completes and finds a difference, so a
    sub-make that fails first leaves the previous run's result in place, still
    named in the failure message.  Shards from a run killed with SIGKILL survive
    too, keyed on a pid a later comparison can draw.  Remove both up front.
    
    ChangeLog:
    
            PR bootstrap/126875
            * Makefile.tpl ([+compare-target+]): Escape the comparison command
            with $(subst) rather than a sed pipe.  Clean up from the exit trap
            without resetting it or re-deriving the exit status.  Name the shell
            in the generated makefile.  Remove any previous result file and
            result shards before comparing.
            * Makefile.in: Regenerate.
            * configure: Regenerate.
    
    config/ChangeLog:
    
            PR bootstrap/126875
            * acx.m4 (ACX_PROG_CMP_IGNORE_INITIAL): Clean up from the exit trap
            without resetting it or re-deriving the exit status, and end the
            fallback subshell with a builtin so that the trap runs.
    
    Signed-off-by: Kyrylo Tkachov <[email protected]>

Diff:
---
 Makefile.in   | 36 ++++++++++++++++++++++++++----------
 Makefile.tpl  | 18 +++++++++++++-----
 config/acx.m4 | 13 ++++++++-----
 configure     |  7 +++----
 4 files changed, 50 insertions(+), 24 deletions(-)

diff --git a/Makefile.in b/Makefile.in
index 84d8909730c4..767d6aab977a 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -66026,7 +66026,13 @@ do-clean: clean-stage3
 
 # Run the comparisons in parallel through a generated sub-makefile under
 # the jobserver.  Each failing recipe writes a separate result shard that
-# is collected after the sub-make finishes.
+# is collected after the sub-make finishes.  The comparison command is
+# written into that makefile, so double its '$' characters to survive the
+# expansion the sub-make performs when it reads them back, and name the
+# shell there, which a sub-make does not inherit.  The exit trap starts
+# with ':' because older bash does not preserve the exit status across an
+# exit trap that consists of a single command, which would report a
+# comparison failure as a success.
 compare:
        @r=`${PWD_COMMAND}`; export r; \
        s=`cd $(srcdir); ${PWD_COMMAND}`; export s; \
@@ -66038,22 +66044,24 @@ compare:
        compare_id=$$$$; \
        bad_compare=.bad_compare.$$compare_id; export bad_compare; \
        compare_makefile=compare.$$compare_id.mk; \
-       trap 'st=$$?; rm -f "$$compare_makefile" "$$bad_compare" \
-         "$$bad_compare".*; trap - 0; exit $$st' 0; \
+       trap ':; rm -f "$$compare_makefile" "$$bad_compare" \
+         "$$bad_compare".*' 0; \
        trap 'exit 1' 1 2 3 15; \
+       rm -f .bad_compare "$$bad_compare" "$$bad_compare".*; \
        echo Comparing stages 2 and 3; \
         sed=`echo stage3 | sed 's,^stage,,;s,.,.,g'`; \
        files=`find stage3-* -name "*$(objext)" -print | \
                 sed -n s,^stage$$sed-,,p`; \
-       cmp_raw='$(do-compare)'; \
+       cmp_cmd='$(subst $$,$$$$,$(do-compare))'; \
        { \
+         echo 'SHELL = $(SHELL)'; \
          echo 'all:'; \
          echo '.PHONY: all FORCE'; \
          echo 'FORCE:'; \
          printf 'compare/%%: FORCE ; @'; \
          printf 'f1=$$$$r/stage2-$$*; '; \
          printf 'f2=$$$$r/stage3-$$*; '; \
-         printf '%s' "$$cmp_raw" | sed 's,\$$,$$$$,g'; \
+         printf '%s' "$$cmp_cmd"; \
          printf ' > /dev/null 2>&1; st=$$$$?; '; \
          printf 'if test $$$$st -eq 1; then '; \
          printf 'case $$* in '; \
@@ -66486,7 +66494,13 @@ do-clean: clean-stage4
 
 # Run the comparisons in parallel through a generated sub-makefile under
 # the jobserver.  Each failing recipe writes a separate result shard that
-# is collected after the sub-make finishes.
+# is collected after the sub-make finishes.  The comparison command is
+# written into that makefile, so double its '$' characters to survive the
+# expansion the sub-make performs when it reads them back, and name the
+# shell there, which a sub-make does not inherit.  The exit trap starts
+# with ':' because older bash does not preserve the exit status across an
+# exit trap that consists of a single command, which would report a
+# comparison failure as a success.
 compare3:
        @r=`${PWD_COMMAND}`; export r; \
        s=`cd $(srcdir); ${PWD_COMMAND}`; export s; \
@@ -66498,22 +66512,24 @@ compare3:
        compare_id=$$$$; \
        bad_compare=.bad_compare.$$compare_id; export bad_compare; \
        compare_makefile=compare3.$$compare_id.mk; \
-       trap 'st=$$?; rm -f "$$compare_makefile" "$$bad_compare" \
-         "$$bad_compare".*; trap - 0; exit $$st' 0; \
+       trap ':; rm -f "$$compare_makefile" "$$bad_compare" \
+         "$$bad_compare".*' 0; \
        trap 'exit 1' 1 2 3 15; \
+       rm -f .bad_compare "$$bad_compare" "$$bad_compare".*; \
        echo Comparing stages 3 and 4; \
         sed=`echo stage4 | sed 's,^stage,,;s,.,.,g'`; \
        files=`find stage4-* -name "*$(objext)" -print | \
                 sed -n s,^stage$$sed-,,p`; \
-       cmp_raw='$(do-compare3)'; \
+       cmp_cmd='$(subst $$,$$$$,$(do-compare3))'; \
        { \
+         echo 'SHELL = $(SHELL)'; \
          echo 'all:'; \
          echo '.PHONY: all FORCE'; \
          echo 'FORCE:'; \
          printf 'compare3/%%: FORCE ; @'; \
          printf 'f1=$$$$r/stage3-$$*; '; \
          printf 'f2=$$$$r/stage4-$$*; '; \
-         printf '%s' "$$cmp_raw" | sed 's,\$$,$$$$,g'; \
+         printf '%s' "$$cmp_cmd"; \
          printf ' > /dev/null 2>&1; st=$$$$?; '; \
          printf 'if test $$$$st -eq 1; then '; \
          printf 'case $$* in '; \
diff --git a/Makefile.tpl b/Makefile.tpl
index 02ebed76d470..07b434e43431 100644
--- a/Makefile.tpl
+++ b/Makefile.tpl
@@ -1825,7 +1825,13 @@ do-clean: clean-stage[+id+]
 [+ IF compare-target +]
 # Run the comparisons in parallel through a generated sub-makefile under
 # the jobserver.  Each failing recipe writes a separate result shard that
-# is collected after the sub-make finishes.
+# is collected after the sub-make finishes.  The comparison command is
+# written into that makefile, so double its '$' characters to survive the
+# expansion the sub-make performs when it reads them back, and name the
+# shell there, which a sub-make does not inherit.  The exit trap starts
+# with ':' because older bash does not preserve the exit status across an
+# exit trap that consists of a single command, which would report a
+# comparison failure as a success.
 [+compare-target+]:
        @r=`${PWD_COMMAND}`; export r; \
        s=`cd $(srcdir); ${PWD_COMMAND}`; export s; \
@@ -1837,22 +1843,24 @@ do-clean: clean-stage[+id+]
        compare_id=$$$$; \
        bad_compare=.bad_compare.$$compare_id; export bad_compare; \
        compare_makefile=[+compare-target+].$$compare_id.mk; \
-       trap 'st=$$?; rm -f "$$compare_makefile" "$$bad_compare" \
-         "$$bad_compare".*; trap - 0; exit $$st' 0; \
+       trap ':; rm -f "$$compare_makefile" "$$bad_compare" \
+         "$$bad_compare".*' 0; \
        trap 'exit 1' 1 2 3 15; \
+       rm -f .bad_compare "$$bad_compare" "$$bad_compare".*; \
        echo Comparing stages [+prev+] and [+id+]; \
         sed=`echo stage[+id+] | sed 's,^stage,,;s,.,.,g'`; \
        files=`find stage[+id+]-* -name "*$(objext)" -print | \
                 sed -n s,^stage$$sed-,,p`; \
-       cmp_raw='$(do-[+compare-target+])'; \
+       cmp_cmd='$(subst $$,$$$$,$(do-[+compare-target+]))'; \
        { \
+         echo 'SHELL = $(SHELL)'; \
          echo 'all:'; \
          echo '.PHONY: all FORCE'; \
          echo 'FORCE:'; \
          printf '[+compare-target+]/%%: FORCE ; @'; \
          printf 'f1=$$$$r/stage[+prev+]-$$*; '; \
          printf 'f2=$$$$r/stage[+id+]-$$*; '; \
-         printf '%s' "$$cmp_raw" | sed 's,\$$,$$$$,g'; \
+         printf '%s' "$$cmp_cmd"; \
          printf ' > /dev/null 2>&1; st=$$$$?; '; \
          printf 'if test $$$$st -eq 1; then '; \
          printf 'case $$* in '; \
diff --git a/config/acx.m4 b/config/acx.m4
index 5547cbeb9b8f..3a9a36a2cab4 100644
--- a/config/acx.m4
+++ b/config/acx.m4
@@ -472,21 +472,24 @@ dnl skip bytes instead of using tail.  The test being 
performed is
 dnl "if cmp --ignore-initial=2 t1 t2 && ! cmp --ignore-initial=1 t1 t2"
 dnl but we need to sink errors and handle broken shells.  We also test
 dnl for the parameter format "cmp file1 file2 skip1 skip2" which is
-dnl accepted by cmp on some systems.
+dnl accepted by cmp on some systems.  The fallback ends its subshell with
+dnl a builtin so that the exit trap runs, and the trap starts with ':'
+dnl because older bash does not preserve the exit status across an exit
+dnl trap that consists of a single command, which would report a differing
+dnl pair of objects as equal.
 AC_DEFUN([ACX_PROG_CMP_IGNORE_INITIAL],
 [AC_CACHE_CHECK([how to compare bootstrapped objects], gcc_cv_prog_cmp_skip,
 [ echo abfoo >t1
   echo cdfoo >t2
-  gcc_cv_prog_cmp_skip='(trap "st=\$$?; rm -f tmp-foo1.$$$$ '
-  gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tmp-foo2.$$$$; '
-  gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'trap - 0; exit \$$st" 0; '
+  gcc_cv_prog_cmp_skip='(trap ":; rm -f tmp-foo1.$$$$ '
+  gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tmp-foo2.$$$$" 0; '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'trap "exit 2" 1 2 3 15; '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tail -c +17 $$f1 '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'> tmp-foo1.$$$$ || exit 2; '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tail -c +17 $$f2 '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'> tmp-foo2.$$$$ || exit 2; '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'cmp tmp-foo1.$$$$ '
-  gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tmp-foo2.$$$$)'
+  gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tmp-foo2.$$$$; exit $$?)'
   if cmp t1 t2 2 2 > /dev/null 2>&1; then
     if cmp t1 t2 1 1 > /dev/null 2>&1; then
       :
diff --git a/configure b/configure
index 4f21a3e94e1f..07f62a81bb9b 100755
--- a/configure
+++ b/configure
@@ -6139,16 +6139,15 @@ if ${gcc_cv_prog_cmp_skip+:} false; then :
 else
    echo abfoo >t1
   echo cdfoo >t2
-  gcc_cv_prog_cmp_skip='(trap "st=\$$?; rm -f tmp-foo1.$$$$ '
-  gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tmp-foo2.$$$$; '
-  gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'trap - 0; exit \$$st" 0; '
+  gcc_cv_prog_cmp_skip='(trap ":; rm -f tmp-foo1.$$$$ '
+  gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tmp-foo2.$$$$" 0; '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'trap "exit 2" 1 2 3 15; '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tail -c +17 $$f1 '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'> tmp-foo1.$$$$ || exit 2; '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tail -c +17 $$f2 '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'> tmp-foo2.$$$$ || exit 2; '
   gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'cmp tmp-foo1.$$$$ '
-  gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tmp-foo2.$$$$)'
+  gcc_cv_prog_cmp_skip="$gcc_cv_prog_cmp_skip"'tmp-foo2.$$$$; exit $$?)'
   if cmp t1 t2 2 2 > /dev/null 2>&1; then
     if cmp t1 t2 1 1 > /dev/null 2>&1; then
       :

Reply via email to