`make dist' now fails when tar fails, instead of exiting
successfully with a truncated archive. The uncompressed tarball is
built by a rule of its own, $(distdir).tar, rather than piped into
the compressors, whose exit status was the only one the shell
reported.  As a side effect, tar is run only once, whatever the
number of enabled formats; on the other hand, `make dist' now needs
room for the uncompressed tarball.  Since some tar implementations
merely warn about the files they fail to archive and still exit
successfully, anything tar writes to standard error is treated as a
failure too (unless AM_DIST_TAR_IGNORE_STDERR is set to `yes').
(bug#19614)
---
 NEWS                |   3 ++
 doc/automake.texi   |  23 ++++++++++
 lib/am/distdir.am   |  64 +++++++++++++++++++-------
 m4/optional.m4      |  11 +++--
 m4/tar.m4           |  11 +++++
 t/dist-formats.tap  |   7 ++-
 t/dist-shar.sh      |  19 ++++++++
 t/dist-tar-fails.sh | 108 ++++++++++++++++++++++++++++++++++++++++++++
 t/list-of-tests.mk  |   1 +
 9 files changed, 224 insertions(+), 23 deletions(-)
 create mode 100644 t/dist-tar-fails.sh

--
With Valediction,
Kamila Szewczyk (https://iczelia.net)
From d5fc3ae245c81140825ecdd5ad2c82f335d07452 Mon Sep 17 00:00:00 2001
From: Kamila Szewczyk <[email protected]>
Date: Wed, 5 Aug 2026 19:59:37 +0200
Subject: [PATCH] dist: fix automake bug #19614

`make dist' now fails when tar fails, instead of exiting
successfully with a truncated archive. The uncompressed tarball is
built by a rule of its own, $(distdir).tar, rather than piped into
the compressors, whose exit status was the only one the shell
reported.  As a side effect, tar is run only once, whatever the
number of enabled formats; on the other hand, `make dist' now needs
room for the uncompressed tarball.  Since some tar implementations
merely warn about the files they fail to archive and still exit
successfully, anything tar writes to standard error is treated as a
failure too (unless AM_DIST_TAR_IGNORE_STDERR is set to `yes').
(bug#19614)
---
 NEWS                |   3 ++
 doc/automake.texi   |  23 ++++++++++
 lib/am/distdir.am   |  64 +++++++++++++++++++-------
 m4/optional.m4      |  11 +++--
 m4/tar.m4           |  11 +++++
 t/dist-formats.tap  |   7 ++-
 t/dist-shar.sh      |  19 ++++++++
 t/dist-tar-fails.sh | 108 ++++++++++++++++++++++++++++++++++++++++++++
 t/list-of-tests.mk  |   1 +
 9 files changed, 224 insertions(+), 23 deletions(-)
 create mode 100644 t/dist-tar-fails.sh

diff --git a/NEWS b/NEWS
index b798fbbd2..372ea46f3 100644
--- a/NEWS
+++ b/NEWS
@@ -18,6 +18,9 @@ New in 1.18.2 (????-??-??):
   - Busybox tar no longer assumed to be GNU tar just because it
     supports --version.
 
+  - `make dist' now fails when tar fails, instead of exiting
+    successfully with a truncated archive.
+
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 New in 1.18.1 (2025-06-25):
 
diff --git a/doc/automake.texi b/doc/automake.texi
index f2bf092b3..c9a72ca66 100644
--- a/doc/automake.texi
+++ b/doc/automake.texi
@@ -9585,6 +9585,29 @@ will create archives in all the enabled formats (@pxref{List of
 Automake options} for how to change this list).  By default, only
 the @code{dist-gzip} target is enabled by @code{dist}.
 
+@cindex Distribution archive, failures of @command{tar}
+All the tar-based formats above compress one and the same
+@file{@var{package}-@var{version}.tar} file; it is created once, no
+matter how many formats are requested, and removed again as soon as the
+archives have been built.  Beware that this needs room for the
+uncompressed tarball, in addition to the @file{@var{package}-@var{version}}
+directory.  Automake used to pipe @command{tar} directly into the
+compressors instead, but then a failure of @command{tar} went unnoticed,
+since the exit status of a shell pipeline is the one of its last
+command; @samp{make dist} would exit successfully after creating a
+truncated archive.
+
+@vindex AM_DIST_TAR_IGNORE_STDERR
+Some @command{tar} implementations do not fail when they cannot archive
+a file, for instance because its name is too long for the selected
+format: they merely print a diagnostic, and still exit successfully.
+For this reason, anything @command{tar} writes to its standard error
+also makes @samp{make dist} fail.  If your @command{tar} is known to
+write harmless messages there, set @code{AM_DIST_TAR_IGNORE_STDERR} to
+@samp{yes}, either in @file{Makefile.am} or on the @command{make}
+command line; its messages are then still printed, but no longer
+considered errors.
+
 
 @node Tests
 @chapter Support for test suites
diff --git a/lib/am/distdir.am b/lib/am/distdir.am
index 7b286cef6..e83fbe987 100644
--- a/lib/am/distdir.am
+++ b/lib/am/distdir.am
@@ -33,7 +33,9 @@ am__remove_distdir = \
 ## See automake bug#10470.
       || { sleep 5 && rm -rf "$(distdir)"; }; \
   else :; fi
-am__post_remove_distdir = $(am__remove_distdir)
+am__post_remove_distdir = \
+  rm -f $(distdir).tar; \
+  $(am__remove_distdir)
 endif %?TOPDIR_P%
 
 if %?SUBDIRS%
@@ -330,51 +332,78 @@ endif %?TOPDIR_P%
 
 if %?TOPDIR_P%
 
+## Do not pipe tar into the compressor: the exit status of a shell
+## pipeline is that of its last command only, so a failure of tar would
+## go unnoticed, and "make dist" would exit successfully after creating
+## a truncated archive.  There is no portable equivalent of the
+## 'pipefail' shell option, so build the uncompressed tarball in a rule
+## of its own, and let each dist-* target compress it.  This also means
+## running tar just once, no matter how many formats are requested.
+## See automake bug#19614.
+##
+## Some tar implementations do not fail when they cannot archive a file
+## (say, because its name is too long for the selected format): they
+## only print a diagnostic and exit successfully.  So treat any output
+## on tar's standard error as a failure, too.  Archivers that write to
+## standard error even when all went well (cpio reports the number of
+## blocks written) are exempted by configure.
+
+# Exists only to be overridden by the user if desired.
+AM_DIST_TAR_IGNORE_STDERR = $(am__tar_ignore_stderr)
+
+$(distdir).tar: distdir
+	am__tar_msg=`tardir=$(distdir) && $(am__tar) 2>&1 >$(distdir).tar`; \
+	am__tar_rc=$$?; \
+	test -z "$$am__tar_msg" || { printf '%s\n' "$$am__tar_msg" >&2; \
+	  test x"$(AM_DIST_TAR_IGNORE_STDERR)" = xyes || am__tar_rc=1; }; \
+	test $$am__tar_rc -eq 0 || { rm -f $(distdir).tar; \
+	  echo "$(distdir).tar: cannot create the distribution archive" >&2; exit 1; }
+
 ?GZIP?DIST_ARCHIVES += $(distdir).tar.gz
 GZIP_ENV = -9
 .PHONY: dist-gzip
-dist-gzip: distdir
-	tardir=$(distdir) && $(am__tar) | eval GZIP= gzip $(GZIP_ENV) -c >$(distdir).tar.gz
+dist-gzip: $(distdir).tar
+	eval GZIP= gzip $(GZIP_ENV) -c <$(distdir).tar >$(distdir).tar.gz
 	$(am__post_remove_distdir)
 
 ?BZIP2?DIST_ARCHIVES += $(distdir).tar.bz2
 .PHONY: dist-bzip2
-dist-bzip2: distdir
-	tardir=$(distdir) && $(am__tar) | BZIP2=$${BZIP2--9} bzip2 -c >$(distdir).tar.bz2
+dist-bzip2: $(distdir).tar
+	BZIP2=$${BZIP2--9} bzip2 -c <$(distdir).tar >$(distdir).tar.bz2
 	$(am__post_remove_distdir)
 
 ?BZIP3?DIST_ARCHIVES += $(distdir).tar.bz3
 .PHONY: dist-bzip3
 ## bzip3 does not read any envvars.
-dist-bzip3: distdir
-	tardir=$(distdir) && $(am__tar) | bzip3 -c >$(distdir).tar.bz3
+dist-bzip3: $(distdir).tar
+	bzip3 -c <$(distdir).tar >$(distdir).tar.bz3
 	$(am__post_remove_distdir)
 
 ?LZIP?DIST_ARCHIVES += $(distdir).tar.lz
 .PHONY: dist-lzip
-dist-lzip: distdir
-	tardir=$(distdir) && $(am__tar) | lzip -c $${LZIP_OPT--9} >$(distdir).tar.lz
+dist-lzip: $(distdir).tar
+	lzip -c $${LZIP_OPT--9} <$(distdir).tar >$(distdir).tar.lz
 	$(am__post_remove_distdir)
 
 ?XZ?DIST_ARCHIVES += $(distdir).tar.xz
 .PHONY: dist-xz
-dist-xz: distdir
-	tardir=$(distdir) && $(am__tar) | XZ_OPT=$${XZ_OPT--e} xz -c >$(distdir).tar.xz
+dist-xz: $(distdir).tar
+	XZ_OPT=$${XZ_OPT--e} xz -c <$(distdir).tar >$(distdir).tar.xz
 	$(am__post_remove_distdir)
 
 ?ZSTD?DIST_ARCHIVES += $(distdir).tar.zst
 .PHONY: dist-zstd
-dist-zstd: distdir
-	tardir=$(distdir) && $(am__tar) | zstd -c $${ZSTD_CLEVEL-$${ZSTD_OPT--19}} >$(distdir).tar.zst
+dist-zstd: $(distdir).tar
+	zstd -c $${ZSTD_CLEVEL-$${ZSTD_OPT--19}} <$(distdir).tar >$(distdir).tar.zst
 	$(am__post_remove_distdir)
 
 ?COMPRESS?DIST_ARCHIVES += $(distdir).tar.Z
 .PHONY: dist-tarZ
-dist-tarZ: distdir
+dist-tarZ: $(distdir).tar
 	@echo WARNING: "Support for distribution archives compressed with" \
 		       "legacy program 'compress' is deprecated." >&2
 	@echo WARNING: "It will be removed altogether in Automake 2.0" >&2
-	tardir=$(distdir) && $(am__tar) | compress -c >$(distdir).tar.Z
+	compress -c <$(distdir).tar >$(distdir).tar.Z
 	$(am__post_remove_distdir)
 
 ?SHAR?DIST_ARCHIVES += $(distdir).shar.gz
@@ -383,7 +412,10 @@ dist-shar: distdir
 	@echo WARNING: "Support for shar distribution archives is" \
 	               "deprecated." >&2
 	@echo WARNING: "It will be removed altogether in Automake 2.0" >&2
-	shar $(distdir) | eval GZIP= gzip $(GZIP_ENV) -c >$(distdir).shar.gz
+## shar is expected to be chatty on its standard error, so only its exit
+## status is checked here.
+	shar $(distdir) >$(distdir).shar || { rm -f $(distdir).shar; exit 1; }
+	eval GZIP= gzip $(GZIP_ENV) -f $(distdir).shar
 	$(am__post_remove_distdir)
 
 ?ZIP?DIST_ARCHIVES += $(distdir).zip
diff --git a/m4/optional.m4 b/m4/optional.m4
index 9d0e96bb4..314b9f5da 100644
--- a/m4/optional.m4
+++ b/m4/optional.m4
@@ -15,7 +15,7 @@
 # dist' builds each archive when its tool is present and skips it
 # otherwise, without failing.  Unknown OPTION warns at autoreconf; a
 # tool absent at configure appends no rule; a tool lost afterwards
-# fails the pipeline, which the recipe catches.  See bug#81040.
+# fails, which the recipe catches.  See bug#81040.
 AC_DEFUN([AM_OPTIONAL_AUTOMAKE],
 [m4_foreach_w([_am_opt_o], [$1],
    [_AM_OPTIONAL_AUTOMAKE_ONE(_m4_defn([_am_opt_o]))])dnl
@@ -80,13 +80,14 @@ fi
 
 # _AM_OPTIONAL_TAR(TOOL, VAR-TAG, EXT, FLAGS)
 # -------------------------------------------
-# Single-tool tar pipe; TOOL is last, so its failure is what's caught.
+# Compress the tarball built by the $(distdir).tar rule, which the
+# built-in dist-* targets share and which reports the failures of tar
+# itself (see automake bug#19614); only TOOL failing is tolerated here.
 AC_DEFUN([_AM_OPTIONAL_TAR],
 [_AM_OPTIONAL_RULE([$1], [$2],
-[am--optional-dist-$1: distdir
+[am--optional-dist-$1: $(distdir).tar
 	@if test -n "$(am__optional_$2)"; then \
-	  { tardir=$(distdir) \
-	    && $(am__tar) | "$(am__optional_$2)" $4 > $(distdir).tar.$3; } \
+	  "$(am__optional_$2)" $4 < $(distdir).tar > $(distdir).tar.$3 \
 	  || { rm -f $(distdir).tar.$3; \
 	       echo "am-optional: dist-$1 failed; archive not built" >&2; }; \
 	else \
diff --git a/m4/tar.m4 b/m4/tar.m4
index fa6d12a93..154025f6f 100644
--- a/m4/tar.m4
+++ b/m4/tar.m4
@@ -25,6 +25,14 @@ AC_DEFUN([_AM_PROG_TAR],
 # in the wild, let's not break things.
 AC_SUBST([AMTAR], ['$${TAR-tar}'])
 
+# The rules that build the distribution archives consider anything the
+# command in $(am__tar) writes to its standard error to be an error,
+# since some tar implementations merely warn about the files they fail
+# to archive, and still exit successfully (see automake bug#19614).
+# Set this to "yes" for the archivers that are chatty even when they
+# succeed; cpio, for one, always reports the number of blocks written.
+am__tar_ignore_stderr=no
+
 # We'll loop over all known methods to create a tar archive until one works.
 _am_tools='gnutar m4_if([$1], [ustar], [plaintar]) pax cpio none'
 
@@ -76,6 +84,7 @@ m4_if([$1], [v7],
   _am_tools=${am_cv_prog_tar_$1-$_am_tools}
 
   for _am_tool in $_am_tools; do
+    am__tar_ignore_stderr=no
     case $_am_tool in
     gnutar)
       for _am_tar in tar gnutar gtar; do
@@ -102,6 +111,7 @@ m4_if([$1], [v7],
       am__tar='find "$$tardir" -print | cpio -o -H $1 -L'
       am__tar_='find "$tardir" -print | cpio -o -H $1 -L'
       am__untar='cpio -i -H $1 -d'
+      am__tar_ignore_stderr=yes
       ;;
     none)
       am__tar=false
@@ -133,4 +143,5 @@ m4_if([$1], [v7],
 
 AC_SUBST([am__tar])
 AC_SUBST([am__untar])
+AC_SUBST([am__tar_ignore_stderr])
 ]) # _AM_PROG_TAR
diff --git a/t/dist-formats.tap b/t/dist-formats.tap
index 84e795626..b76e12470 100644
--- a/t/dist-formats.tap
+++ b/t/dist-formats.tap
@@ -429,10 +429,13 @@ fi
 
 mkdir bin
 cd bin
+# Keep the standard error of this stub clean: 'make dist' considers
+# anything the archiver writes there to be an error (bug#19614).
 cat > check-distdir <<END
 #!/bin/sh
-{ ls -l '$tarname' && diff Makefile.am '$tarname'/Makefile.am; } >&2 \
-  || { echo "== distdir fail =="; exit 1; }
+{ ls -l '$tarname' && diff Makefile.am '$tarname'/Makefile.am; } \
+  > check-distdir.log 2>&1 \
+  || { cat check-distdir.log >&2; echo "== distdir fail =="; exit 1; }
 END
 cat > grep-distdir-error <<'END'
 #!/bin/sh
diff --git a/t/dist-shar.sh b/t/dist-shar.sh
index f6f83a7b7..f2584caf4 100644
--- a/t/dist-shar.sh
+++ b/t/dist-shar.sh
@@ -44,4 +44,23 @@ $AUTOCONF
 $MAKE distcheck
 test -f $distdir.shar.gz
 
+# A failure of shar must not go unnoticed just because its output used
+# to be fed to gzip, which happily succeeded.  See automake bug#19614.
+
+mkdir bin
+echo '#!/bin/sh' > bin/shar
+echo 'echo "fake shar: cannot do that" >&2; exit 1' >> bin/shar
+chmod a+x bin/shar
+
+rm -f $distdir.shar.gz
+saved_PATH=$PATH
+PATH=$(pwd)/bin$PATH_SEPARATOR$PATH; export PATH
+$MAKE dist-shar > output 2>&1 && { cat output; exit 1; }
+PATH=$saved_PATH; export PATH
+
+cat output
+grep 'cannot do that' output
+test ! -e $distdir.shar
+test ! -e $distdir.shar.gz
+
 :
diff --git a/t/dist-tar-fails.sh b/t/dist-tar-fails.sh
new file mode 100644
index 000000000..31fbafb56
--- /dev/null
+++ b/t/dist-tar-fails.sh
@@ -0,0 +1,108 @@
+#! /bin/sh
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# 'make dist' must fail when the archiver fails, even though its output
+# used to be fed to a compressor that exits successfully.  Also check
+# that the archiver is run only once, whatever the number of requested
+# formats.  See automake bug#19614.
+
+. test-init.sh
+
+cat > configure.ac << END
+AC_INIT([$me], [1.0])
+AM_INIT_AUTOMAKE([dist-xz dist-bzip2])
+AC_CONFIG_FILES([Makefile])
+AC_OUTPUT
+END
+: > Makefile.am
+
+# Stub compressors, so that this test does not depend on 'xz' and
+# 'bzip2' being installed ('gzip' is assumed to be available on every
+# reasonable portability target).  They just copy their input, which is
+# all the checks below care about.
+mkdir bin
+for c in xz bzip2; do
+  echo '#!/bin/sh' > bin/$c
+  echo 'exec cat' >> bin/$c
+  chmod a+x bin/$c
+done
+PATH=$(pwd)/bin$PATH_SEPARATOR$PATH; export PATH
+
+# Stub archivers, to be used in place of $(am__tar).  Their names
+# contain no whitespace, so that overriding am__tar from the 'make'
+# command line works with any make implementation.
+
+cat > tar-fails << 'END'
+#!/bin/sh
+echo "fake tar: cannot archive that" >&2
+exit 2
+END
+
+# Some tar implementations merely warn about the files they cannot
+# archive, and still exit successfully.
+cat > tar-warns << 'END'
+#!/bin/sh
+echo "fake tar: cannot archive that" >&2
+echo "definitely not a valid tar archive"
+END
+
+cat > tar-counts << 'END'
+#!/bin/sh
+echo x >> tar-runs
+echo "definitely not a valid tar archive"
+END
+
+chmod a+x tar-fails tar-warns tar-counts
+
+$ACLOCAL
+$AUTOCONF
+$AUTOMAKE
+./configure
+
+for fake_tar in tar-fails tar-warns; do
+  $MAKE dist am__tar=./$fake_tar > output 2>&1 && { cat output; exit 1; }
+  cat output
+  grep 'cannot archive that' output
+  grep "$distdir\.tar: cannot create the distribution archive" output
+  # No archive, and no leftovers, must have been created.
+  test ! -e $distdir.tar
+  test ! -e $distdir.tar.gz
+  test ! -e $distdir.tar.xz
+  test ! -e $distdir.tar.bz2
+done
+
+# Diagnostics from tar can be declared harmless by the user.
+$MAKE dist am__tar=./tar-warns AM_DIST_TAR_IGNORE_STDERR=yes > output 2>&1 \
+  || { cat output; exit 1; }
+cat output
+grep 'cannot archive that' output
+test ! -e $distdir.tar
+test -s $distdir.tar.gz
+test -s $distdir.tar.xz
+test -s $distdir.tar.bz2
+
+rm -f $distdir.tar.*
+
+# Three formats, but a single run of the archiver.
+$MAKE dist am__tar=./tar-counts
+cat tar-runs # For debugging.
+test "$(cat tar-runs)" = x
+test ! -e $distdir.tar
+test -s $distdir.tar.gz
+test -s $distdir.tar.xz
+test -s $distdir.tar.bz2
+
+:
diff --git a/t/list-of-tests.mk b/t/list-of-tests.mk
index 6c522bd8e..cd9b6bc55 100644
--- a/t/list-of-tests.mk
+++ b/t/list-of-tests.mk
@@ -425,6 +425,7 @@ t/dist-pr109765.sh \
 t/dist-readonly.sh \
 t/dist-repeated.sh \
 t/dist-shar.sh \
+t/dist-tar-fails.sh \
 t/dist-tarZ.sh \
 t/dist-vs-built-sources.sh \
 t/dist-with-unreadable-makefile-fails.sh \
-- 
2.53.0

Attachment: OpenPGP_0xC868F0B6DE38409D.asc
Description: OpenPGP public key

Attachment: OpenPGP_signature.asc
Description: OpenPGP digital signature

Reply via email to