Subject: bug#76860: [PATCH] Reproducible tarballs for releases

From: Stefan Kangas <[email-masked]>
Date: Sat, 8 Mar 2025 10:19:33 +0000

Severity: wishlist

I propose that we ensure reproducibility in our release tarballs by
applying the recommended GNU Tar options.  Please see the attached
patch.

The main value of reproducible tarballs is that they allow anyone --
whether downstream packagers, security auditors, or independent
developers -- to verify that the official release tarball matches the
corresponding source repository exactly.

This is particularly useful for:

1. Supply chain security.  Ensuring that the tarball is built from the
   expected source, with no accidental or malicious modifications.

2. Downstream distributions.  Some distributions, like Debian and Guix,
   strongly prefer reproducible builds to improve verifyability and
   package integrity.

3. Debugging and consistency.  Developers can regenerate the exact same
   tarball locally, making it easier to debug, compare versions, or
   audit historical releases.

Even if we're the only ones who generate official tarballs, making them
reproducible improves transparency and verifiability, which are
worthwhile goals on their own.

This approach follows the official GNU Tar manual guidelines:
https://www.gnu.org/software/tar/manual/html_node/Reproducibility.html
From 699952f0c11b91ac828fdf518ce35cdffb39af48 Mon Sep 17 00:00:00 2001
From: Stefan Kangas <[email-masked]>
Date: Sat, 8 Mar 2025 11:17:11 +0100
Subject: [PATCH] Make release tarball more reproducible

The main value of reproducible tarballs is that they allow anyone --
whether downstream packagers, security auditors, or independent
developers -- to verify that the official release tarball matches the
corresponding source repository exactly.

This is particularly useful for:

1. Supply chain security.  Ensuring that the tarball is built from the
   expected source, with no accidental or malicious modifications.

2. Downstream distributions.  Some distributions, like Debian and Guix,
   strongly prefer reproducible builds to improve verifyability and
   package integrity.

3. Debugging and consistency.  Developers can regenerate the exact same
   tarball locally, making it easier to debug, compare versions, or
   audit historical releases.

Even if we're the only ones who generate official tarballs, making them
reproducible improves transparency and verifiability, which are
worthwhile goals on their own.

This approach follows the official GNU Tar manual guidelines:
https://www.gnu.org/software/tar/manual/html_node/Reproducibility.html

* make-dist: Make release tarball more reproducible.
---
 make-dist | 36 ++++++++++++++++++++++++++++++++++--
 1 file changed, 34 insertions(+), 2 deletions(-)

diff --git a/make-dist b/make-dist
index ba82b261743..d03077f671e 100755
--- a/make-dist
+++ b/make-dist
@@ -1,8 +1,7 @@
 #!/bin/sh
 ### make-dist: create an Emacs distribution tar file from current srcdir
 
-## Copyright (C) 1995, 1997-1998, 2000-2025 Free Software Foundation,
-## Inc.
+## Copyright (C) 1995-2025 Free Software Foundation, Inc.
 
 ## This file is part of GNU Emacs.
 
@@ -467,6 +466,30 @@ tempsubdirs=
     -exec rm -f {} \; || exit
 fi
 
+# Make the source tarball reproducible.
+# https://www.gnu.org/software/tar/manual/html_node/Reproducibility.html
+
+function get_commit_time() {
+  TZ=UTC0 git log -1 \
+    --format=tformat:%cd \
+    --date=format:%Y-%m-%dT%H:%M:%SZ \
+    "$@"
+}
+
+#  Set each source file timestamp to that of its latest commit.
+git ls-files | while read -r file; do
+  commit_time=$(get_commit_time "$file") &&
+  touch -md $commit_time "$file"
+done
+
+#  Set timestamp of each directory under $FILES
+#  to the latest timestamp of any descendant.
+find $FILES -depth -type d -exec sh -c \
+  'touch -r "$0/$(ls -At "$0" | head -n 1)" "$0"' \
+  {} ';'
+
+SOURCE_EPOCH=$(get_commit_time)
+
 if [ "${make_tar}" = yes ]; then
   echo "Looking for $default_gzip"
   found=0
@@ -493,6 +516,15 @@ tempsubdirs=
     taropt="$taropt -H ustar"
   tar --sort=name -cf /dev/null $tempdir/src/lisp.h 2>/dev/null &&
     taropt="$taropt --sort=name"
+  tar --format=posix -cf /dev/null $tempdir/src/lisp.h 2>/dev/null &&
+    taropt="$taropt --format=posix"
+  tar --pax-option='exthdr.name=%d/PaxHeaders/%f' -cf /dev/null $tempdir/src/lisp.h 2>/dev/null &&
+    taropt="$taropt --pax-option='exthdr.name=%d/PaxHeaders/%f'"
+  tar --pax-option='delete=atime,delete=ctime' -cf /dev/null $tempdir/src/lisp.h 2>/dev/null &&
+    taropt="$taropt --pax-option='delete=atime,delete=ctime'"
+  tar --clamp-mtime --mtime="$SOURCE_EPOCH" -cf /dev/null $tempdir/src/lisp.h 2>/dev/null &&
+    taropt="$taropt --clamp-mtime --mtime={$SOURCE_EPOCH}"
+
   [ "$verbose" = "yes" ] && taropt="$taropt --verbose"
 
   (cd $tempparent &&
-- 
2.48.1