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 :
