lib/am/distdir.am: strip VPATH srcdir from #line in distributed yacc/lex outputs

* bin/automake.in (%ylwrap_dist_files): New. (lang_yacc_target_hook, lang_lex_target_hook): Record distributed outputs and headers. (handle_dist): Define am__ylwrap_dist_files and YLWRAP-DIST.
* lib/am/distdir.am (distdir-am): Strip the $(srcdir)/ prefix from "#line" directives of those files taken from the build directory, preserving their timestamps.
* t/yacc-lex-vpath-dist-line.sh: New test.
* t/list-of-tests.mk: Add it.
* doc/automake.texi (Yacc and Lex): Document it.
* NEWS: Mention it.
diff --git a/NEWS b/NEWS
index e6ff5b6..1c7e152 100644
--- a/NEWS
+++ b/NEWS
@@ -35,6 +35,11 @@
   - distcheck now detects symbolic links left in the installation tree
     after uninstall, including dangling links.
 
+  - Yacc and lex outputs distributed from a VPATH build no longer carry
+    "#line" directives pointing to their sources through the build-time
+    srcdir, e.g. "../src/parse.y"; the distributed copies now refer to
+    the sources next to them.  (bug#39666)
+
   - dist-zstd now honors numeric ZSTD_CLEVEL values, such as 3, instead
     of treating them as input filenames.  This fixes failed or incorrect
     distribution archives when ZSTD_CLEVEL is set.
diff --git a/bin/automake.in b/bin/automake.in
index d8e37c3..8130a8a 100644
--- a/bin/automake.in
+++ b/bin/automake.in
@@ -563,6 +563,8 @@
 # in some conditions, as [$DISJCONDITIONS, $FILE] pairs.
 my @dist_common_if_present;
 
+my %ylwrap_dist_files;
+
 # This is set when 'handle_dist' has finished.  Once this happens,
 # we should no longer push on dist_common.
 my $handle_dist_run;
@@ -663,6 +665,7 @@
     @dist_common = ();
     @dist_common_cond = ();
     @dist_common_if_present = ();
+    %ylwrap_dist_files = ();
     $handle_dist_run = 0;
 
     @proglist = ();
@@ -4058,6 +4061,11 @@
     if @if_present;
   $transform{'DIST-IF-PRESENT'} = !!@if_present;
 
+  define_pretty_variable ('am__ylwrap_dist_files', TRUE, INTERNAL,
+			  sort keys %ylwrap_dist_files)
+    if %ylwrap_dist_files;
+  $transform{'YLWRAP-DIST'} = !!%ylwrap_dist_files;
+
   # $(am__DIST_COMMON): files to be distributed automatically.  Will be
   # appended to $(DIST_COMMON) in the generated Makefile.
   # Use 'sort' so that the expansion of $(DIST_COMMON) in the generated
@@ -6156,8 +6164,11 @@
 	# Distribute the generated file, unless its .y source was
 	# listed in a nodist_ variable.  (handle_source_transform()
 	# will set DIST_SOURCE.)
-	push_dist_common ($header)
-	  if $transform{'DIST_SOURCE'};
+	if ($transform{'DIST_SOURCE'})
+	  {
+	    push_dist_common ($header);
+	    $ylwrap_dist_files{$header} = 1;
+	  }
 
 	# The GNU rules say that yacc/lex output files should be removed
 	# by maintainer-clean.  However, if the files are not distributed,
@@ -6167,6 +6178,8 @@
       }
     # See the comment above for $HEADER.
     $clean_files{$output} = $transform{'DIST_SOURCE'} ? MAINTAINER_CLEAN : CLEAN;
+    $ylwrap_dist_files{$output} = 1
+      if $transform{'DIST_SOURCE'};
 }
 
 # This is a lex helper which is called whenever we have decided to
@@ -6179,6 +6192,8 @@
     # then we want to remove them with "make clean"; otherwise,
     # "make distcheck" will fail.
     $clean_files{$output} = $transform{'DIST_SOURCE'} ? MAINTAINER_CLEAN : CLEAN;
+    $ylwrap_dist_files{$output} = 1
+      if $transform{'DIST_SOURCE'};
 }
 
 # This is a helper for both lex and yacc.
diff --git a/doc/automake.texi b/doc/automake.texi
index e4bcff2..c55334d 100644
--- a/doc/automake.texi
+++ b/doc/automake.texi
@@ -6542,7 +6542,11 @@
 
 The intermediate files generated by @command{yacc} (or @command{lex})
 will be included in any distribution that is made.  That way the user
-doesn't need to have @command{yacc} or @command{lex}.
+doesn't need to have @command{yacc} or @command{lex}.  When these files
+are generated in a VPATH build, their @samp{#line} directives refer to
+the sources through @code{$(srcdir)}; @samp{make dist} removes this
+prefix from the distributed copies, so that they refer to the sources
+next to them.
 
 If a Yacc source file is seen, then your @file{configure.ac} must
 define the variable @code{YACC}.  This is most easily done by invoking
diff --git a/lib/am/distdir.am b/lib/am/distdir.am
index 76c886f..50e3612 100644
--- a/lib/am/distdir.am
+++ b/lib/am/distdir.am
@@ -230,6 +230,21 @@
 	  || exit 1; \
 	done
 endif %?DIST-IF-PRESENT%
+if %?YLWRAP-DIST%
+	@if test "$(srcdir)" != .; then \
+	  srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	  list='$(am__ylwrap_dist_files)'; for file in $$list; do \
+	    case $$file in "$(srcdir)"/*) continue;; esac; \
+	    test -f $$file && test -f "$(distdir)/$$file" || continue; \
+	    rm -f "$(distdir)/$$file" \
+	    && sed -e "/^#/!b" \
+	      -e "s|^\(#[	 ]*\(line[	 ][	 ]*\)\{0,1\}[0-9][0-9]*[	 ][	 ]*\"\)$$srcdirstrip/|\1|" \
+	      $$file >"$(distdir)/$$file" \
+	    && touch -r $$file "$(distdir)/$$file" \
+	    || exit 1; \
+	  done; \
+	fi
+endif %?YLWRAP-DIST%
 ##
 ## Test for directory existence here because a previous automake
 ## invocation might have created some directories.  Note that we
diff --git a/t/list-of-tests.mk b/t/list-of-tests.mk
index 5f4d931..9b7bdcb 100644
--- a/t/list-of-tests.mk
+++ b/t/list-of-tests.mk
@@ -1357,6 +1357,7 @@
 t/yacc-grepping.sh \
 t/yacc-grepping2.sh \
 t/yacc-headers-and-dist-pr47.sh \
+t/yacc-lex-vpath-dist-line.sh \
 t/yacc-line.sh \
 t/yacc-misc.sh \
 t/yacc-mix-c-cxx.sh \
diff --git a/t/yacc-lex-vpath-dist-line.sh b/t/yacc-lex-vpath-dist-line.sh
new file mode 100755
index 0000000..1aebcb6
--- /dev/null
+++ b/t/yacc-lex-vpath-dist-line.sh
@@ -0,0 +1,163 @@
+#! /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/>.
+
+# Yacc and lex outputs generated in a VPATH build must not carry
+# "#line" directives pointing through the srcdir into the distribution
+# (automake bug#39666).  The build tree copies must keep them.
+# See also 'yacc-line.sh' and 'lex-line.sh'.
+
+required='cc yacc lex'
+. test-init.sh
+
+cat >> configure.ac << 'END'
+AC_CONFIG_FILES([sub/Makefile])
+AC_PROG_CC
+AC_PROG_YACC
+AC_PROG_LEX
+AC_OUTPUT
+END
+
+mkdir dir sub sub/dir
+
+cat > Makefile.am << 'END'
+AM_LFLAGS = --never-interactive
+SUBDIRS = sub
+noinst_PROGRAMS = foo bar baz
+AM_YFLAGS = -d
+bar_YFLAGS =
+foo_SOURCES = parse.y
+bar_SOURCES = dir/quux.y
+baz_SOURCES = scan.l
+LDADD = $(LEXLIB)
+## Avoid spurious failures with Solaris make.
+parse.@OBJEXT@: parse.c
+bar-quux.@OBJEXT@: bar-quux.c
+END
+
+cat > sub/Makefile.am << 'END'
+AUTOMAKE_OPTIONS = subdir-objects
+AM_LFLAGS = --never-interactive
+noinst_PROGRAMS = foo
+foo_SOURCES = dir/parse.y dir/scan.l
+LDADD = $(LEXLIB)
+## Avoid spurious failures with Solaris make.
+dir/parse.@OBJEXT@: dir/parse.c
+dir/scan.@OBJEXT@: dir/scan.c
+END
+
+cat > parse.y << 'END'
+%{
+int yylex (void) { return 0; }
+void yyerror (const char *s) {}
+%}
+%%
+x : 'x' {};
+%%
+int main (void)
+{
+  return yyparse ();
+}
+END
+
+cp parse.y dir/quux.y
+sed 's/main/sub_main/;s/^int yylex (void).*/int yylex (void);/' parse.y \
+  > sub/dir/parse.y
+echo 'int main (void) { return sub_main (); }' >> sub/dir/parse.y
+
+cat > scan.l << 'END'
+%%
+"END"  return EOF;
+.
+%%
+int main (void)
+{
+  return 0;
+}
+int yywrap (void)
+{
+  return 1;
+}
+END
+
+cat > sub/dir/scan.l << 'END'
+%%
+"END"  return 0;
+.
+%%
+int yywrap (void)
+{
+  return 1;
+}
+END
+
+outputs='parse.c parse.h bar-quux.c scan.c sub/dir/parse.c sub/dir/scan.c'
+
+$ACLOCAL
+$AUTOCONF
+# FIXME: stop disabling the warnings in the 'unsupported' category
+# FIXME: once the 'subdir-objects' option has been mandatory.
+$AUTOMAKE -a -Wno-unsupported
+
+grep '^am__ylwrap_dist_files' Makefile.in sub/Makefile.in
+
+check_dist ()
+{
+  srcdir=$1
+
+  $srcdir/configure
+  $MAKE
+  $MAKE distdir
+  ls -l $distdir $distdir/dir $distdir/sub/dir
+
+  # For debugging.
+  for f in $outputs; do
+    grep '^#.*line' $distdir/$f || :
+  done
+
+  # The build tree copies still point to the srcdir.
+  grep '^#.*line.*"\.\./\.\./sub/dir/parse\.y"' sub/dir/parse.c \
+    || grep "^#.*line.*\"$srcdir/sub/dir/parse\\.y\"" sub/dir/parse.c
+
+  # The distributed copies do not.
+  for f in $outputs; do
+    $EGREP '^#.*line.*"(\.\./|/)' $distdir/$f && exit 1
+  done
+  grep '^#.*line.*"parse\.y"' $distdir/parse.c
+  grep '^#.*line.*"dir/quux\.y"' $distdir/bar-quux.c
+  grep '^#.*line.*"scan\.l"' $distdir/scan.c
+  grep '^#.*line.*"dir/parse\.y"' $distdir/sub/dir/parse.c
+  grep '^#.*line.*"dir/scan\.l"' $distdir/sub/dir/scan.c
+
+  # The header must not be older than its parser.
+  test -z "$(find $distdir/parse.c -newer $distdir/parse.h)"
+
+  $MAKE distcheck
+}
+
+# Relative srcdir.
+mkdir build
+cd build
+check_dist ..
+cd ..
+rm -rf build
+
+# Absolute srcdir.
+mkdir build
+cd build
+check_dist "$(cd .. && pwd)"
+cd ..
+
+: