From fa4475f1a4b604e7b94ce82c08b2557314e600d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Miguel=20=C3=81ngel=20Arruga=20Vivas?=
 <rosen644835@gmail.com>
Date: Fri, 11 Dec 2020 13:40:05 +0100
Subject: [PATCH] xgettext, msginit: Honor SOURCE_DATE_EPOCH for timestamps.

* gettext-tools/src/msginit.c (po_revision_date): Use po_current_time
and provide the new parameter to po_strftime.
(get_current_year): New function.
(fill_header): Use po_current_time and get_current_year.
* gettext-tools/src/po-time.c (po_strftime): Add parameter is_utc and
use that timezone instead of the local one when provided.
(SECONDS_SINCE_UNIX_EPOCH_TO_TIME_T): New macro.
(po_current_time): New function.
* gettext-tools/src/po-time.h (po_strftime): Add parameter is_utc and
update comment.
(po_current_time): New prototype.
* gettext-tools/src/xgettext.c (construct_header): Use po_current_time
instead of a direct time call and provide the new parameter to
po_strftime.
* gettext-tools/tests/Makefile.am (TESTS): Add new tests.
* gettext-tools/tests/msginit-5: New test case.
* gettext-tools/tests/xgettext-17: Likewise.
* gettext-tools/tests/xgettext-3: Test a fixed POT-Creation-Date
instead of removing it from the generated file.
---
 gettext-tools/src/msginit.c     |  24 +++++--
 gettext-tools/src/po-time.c     |  52 ++++++++++++--
 gettext-tools/src/po-time.h     |  16 ++++-
 gettext-tools/src/xgettext.c    |   5 +-
 gettext-tools/tests/Makefile.am |   4 +-
 gettext-tools/tests/msginit-5   | 118 ++++++++++++++++++++++++++++++++
 gettext-tools/tests/xgettext-17 |  64 +++++++++++++++++
 gettext-tools/tests/xgettext-3  |   8 ++-
 8 files changed, 268 insertions(+), 23 deletions(-)
 create mode 100644 gettext-tools/tests/msginit-5
 create mode 100755 gettext-tools/tests/xgettext-17

diff --git a/gettext-tools/src/msginit.c b/gettext-tools/src/msginit.c
index bd17a08a7..1de01a434 100644
--- a/gettext-tools/src/msginit.c
+++ b/gettext-tools/src/msginit.c
@@ -1082,9 +1082,10 @@ po_revision_date (const char *header)
     {
       /* Assume the translator will modify the PO file now.  */
       time_t now;
+      bool is_utc;
 
-      time (&now);
-      return po_strftime (&now);
+      is_utc = po_current_time (&now);
+      return po_strftime (&now, is_utc);
     }
 }
 
@@ -1802,6 +1803,20 @@ subst_string_list (string_list_ty *slp,
 }
 
 
+/* Retrieve the current year honoring SOURCE_DATE_EPOCH.  */
+static int
+get_current_year (void)
+{
+  time_t now;
+  struct tm cal_point;
+  if (po_current_time (&now))
+    cal_point = *gmtime (&now);
+  else
+    cal_point = *localtime (&now);
+  return cal_point.tm_year + 1900;
+}
+
+
 /* Fill the templates in all fields of the header entry.  */
 static msgdomain_list_ty *
 fill_header (msgdomain_list_ty *mdlp)
@@ -1868,7 +1883,6 @@ fill_header (msgdomain_list_ty *mdlp)
             {
               const char *subst[4][2];
               const char *id;
-              time_t now;
 
               id = project_id (header);
               subst[0][0] = "SOME DESCRIPTIVE TITLE";
@@ -1878,9 +1892,7 @@ fill_header (msgdomain_list_ty *mdlp)
               subst[2][0] = "FIRST AUTHOR <EMAIL@ADDRESS>";
               subst[2][1] = field_value[FIELD_LAST_TRANSLATOR];
               subst[3][0] = "YEAR";
-              subst[3][1] =
-                xasprintf ("%d",
-                           (time (&now), (localtime (&now))->tm_year + 1900));
+              subst[3][1] = xasprintf ("%d", get_current_year ());
               subst_string_list (header_mp->comment, SIZEOF (subst), subst);
             }
 
diff --git a/gettext-tools/src/po-time.c b/gettext-tools/src/po-time.c
index 74001cb79..39b8992f7 100644
--- a/gettext-tools/src/po-time.c
+++ b/gettext-tools/src/po-time.c
@@ -1,5 +1,5 @@
 /* PO/POT file timestamps.
-   Copyright (C) 1995-1998, 2000-2003, 2006 Free Software Foundation, Inc.
+   Copyright (C) 1995-1998, 2000-2003, 2006, 2020 Free Software Foundation, Inc.
    Written by Ulrich Drepper <drepper@gnu.ai.mit.edu>, April 1995.
 
    This program is free software: you can redistribute it and/or modify
@@ -22,6 +22,9 @@
 /* Specification.  */
 #include "po-time.h"
 
+#include <stdbool.h>
+#include <stdlib.h>
+
 #include "xvasprintf.h"
 
 
@@ -51,19 +54,27 @@ difftm (const struct tm *a, const struct tm *b)
 
 
 char *
-po_strftime (const time_t *tp)
+po_strftime (const time_t *tp, bool is_utc)
 {
   struct tm local_time;
   char tz_sign;
   long tz_min;
 
-  local_time = *localtime (tp);
   tz_sign = '+';
-  tz_min = difftm (&local_time, gmtime (tp)) / 60;
-  if (tz_min < 0)
+  if (is_utc)
+    {
+      local_time = *gmtime (tp);
+      tz_min = 0;
+    }
+  else
     {
-      tz_min = -tz_min;
-      tz_sign = '-';
+      local_time = *localtime (tp);
+      tz_min = difftm (&local_time, gmtime (tp)) / 60;
+      if (tz_min < 0)
+        {
+          tz_min = -tz_min;
+          tz_sign = '-';
+        }
     }
   return xasprintf ("%d-%02d-%02d %02d:%02d%c%02ld%02ld",
                     local_time.tm_year + TM_YEAR_ORIGIN,
@@ -73,3 +84,30 @@ po_strftime (const time_t *tp)
                     local_time.tm_min,
                     tz_sign, tz_min / 60, tz_min % 60);
 }
+
+/* time_t has to be an arithmetic (real since C11) type, therefore it
+   must be assignable, with an optional conversion factor, from a long
+   value.  Systems non-compatible with POSIX standards may need to
+   define this conversion factor differently.  */
+#ifndef SECONDS_SINCE_UNIX_EPOCH_TO_TIME_T
+#define SECONDS_SINCE_UNIX_EPOCH_TO_TIME_T(s) s
+#endif
+
+bool
+po_current_time (time_t *t)
+{
+  const char *source_date_epoch = getenv ("SOURCE_DATE_EPOCH");
+  if (source_date_epoch)
+    {
+      char *end;
+      long value = strtol (source_date_epoch, &end, 10);
+      if (end != source_date_epoch)
+        {
+          *t = SECONDS_SINCE_UNIX_EPOCH_TO_TIME_T(value);
+          return true;
+        }
+    }
+
+  time (t);
+  return false;
+}
diff --git a/gettext-tools/src/po-time.h b/gettext-tools/src/po-time.h
index a01484665..201b6e456 100644
--- a/gettext-tools/src/po-time.h
+++ b/gettext-tools/src/po-time.h
@@ -1,5 +1,5 @@
 /* PO/POT file timestamps.
-   Copyright (C) 2001-2003 Free Software Foundation, Inc.
+   Copyright (C) 2001-2003, 2020 Free Software Foundation, Inc.
    Written by Bruno Haible <haible@clisp.cons.org>, 2001.
 
    This program is free software: you can redistribute it and/or modify
@@ -19,6 +19,7 @@
 #define _PO_TIME_H
 
 #include <time.h>
+#include <stdbool.h>
 
 
 #ifdef __cplusplus
@@ -27,8 +28,17 @@ extern "C" {
 
 
 /* Return a freshly allocated string containing the given time in the
-   format YYYY-MM-DD HH:MM+TZOFF.  */
-extern char *po_strftime (const time_t *tp);
+   format YYYY-MM-DD HH:MM+TZOFF, using UTC for the time zone if
+   is_utc is TRUE.  */
+extern char *po_strftime (const time_t *tp, bool is_utc);
+
+
+/* Fill the current date into *tp, honoring SOURCE_DATE_EPOCH
+   environment variable when it is defined to a correct value.  Any
+   error on the value of the environment variable isn't signaled but
+   just fallback to the usual time call.  See
+   https://reproducible-builds.org/specs/source-date-epoch/ */
+extern bool po_current_time (time_t *tp);
 
 
 #ifdef __cplusplus
diff --git a/gettext-tools/src/xgettext.c b/gettext-tools/src/xgettext.c
index 1dce01efb..20490f51b 100644
--- a/gettext-tools/src/xgettext.c
+++ b/gettext-tools/src/xgettext.c
@@ -1986,6 +1986,7 @@ construct_header ()
   message_ty *mp;
   char *msgstr;
   char *comment;
+  bool is_utc;
   static lex_pos_ty pos = { __FILE__, __LINE__ };
 
   if (package_name != NULL)
@@ -2007,8 +2008,8 @@ the MSGID_BUGS_ADDRESS variable there; otherwise please\n\
 specify an --msgid-bugs-address command line option.\n\
 ")));
 
-  time (&now);
-  timestring = po_strftime (&now);
+  is_utc = po_current_time (&now);
+  timestring = po_strftime (&now, is_utc);
 
   msgstr = xasprintf ("\
 Project-Id-Version: %s\n\
diff --git a/gettext-tools/tests/Makefile.am b/gettext-tools/tests/Makefile.am
index 5e1377594..79bad69fa 100644
--- a/gettext-tools/tests/Makefile.am
+++ b/gettext-tools/tests/Makefile.am
@@ -56,7 +56,7 @@ TESTS = gettext-1 gettext-2 \
 	msgfmt-xml-1 msgfmt-xml-2 \
 	msggrep-1 msggrep-2 msggrep-3 msggrep-4 msggrep-5 msggrep-6 msggrep-7 \
 	msggrep-8 msggrep-9 msggrep-10 msggrep-11 \
-	msginit-1 msginit-2 msginit-3 msginit-4 \
+	msginit-1 msginit-2 msginit-3 msginit-4 msginit-5 \
 	msgmerge-1 msgmerge-2 msgmerge-3 msgmerge-4 msgmerge-5 msgmerge-6 \
 	msgmerge-7 msgmerge-8 msgmerge-9 msgmerge-10 msgmerge-11 msgmerge-12 \
 	msgmerge-13 msgmerge-14 msgmerge-15 msgmerge-16 msgmerge-17 \
@@ -77,7 +77,7 @@ TESTS = gettext-1 gettext-2 \
 	recode-sr-latin-1 recode-sr-latin-2 \
 	xgettext-2 xgettext-3 xgettext-4 xgettext-5 xgettext-6 \
 	xgettext-7 xgettext-8 xgettext-9 xgettext-10 xgettext-11 xgettext-12 \
-	xgettext-13 xgettext-14 xgettext-15 xgettext-16 \
+	xgettext-13 xgettext-14 xgettext-15 xgettext-16 xgettext-17 \
 	xgettext-appdata-1 \
 	xgettext-awk-1 xgettext-awk-2 \
 	xgettext-c-2 xgettext-c-3 xgettext-c-4 xgettext-c-5 xgettext-c-6 \
diff --git a/gettext-tools/tests/msginit-5 b/gettext-tools/tests/msginit-5
new file mode 100644
index 000000000..24943e1aa
--- /dev/null
+++ b/gettext-tools/tests/msginit-5
@@ -0,0 +1,118 @@
+#! /bin/sh
+. "${srcdir=.}/init.sh"; path_prepend_ . ../src
+
+# Test reproducible PO file generation with msginit.
+
+# This test fails on mingw, because it relies on the 'project-id' script,
+# which is a shell script (for maintainability) and there is no simple way
+# to execute shell scripts in a native Windows environment.
+# People who need this functionality should use a Cygwin environment, not
+# a native Windows environment.
+
+# This test may fail on Cygwin before 'make install', because the PATH set in
+# msginit.c considers the installation directory, not the build directory.
+
+cat <<\EOF > mi-test5.pot
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 1970-01-01 00:00+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "Test msgid"
+msgstr ""
+EOF
+
+: ${MSGINIT=msginit}
+GETTEXTLIBDIR_SRCDIR="$wabs_top_srcdir"/src \
+SOURCE_DATE_EPOCH=1 \
+${MSGINIT} -l ga_IE --no-translator -o mi-test5a.tmp 2>mi-test5a.err
+test $? = 0 || { cat mi-test5a.err 1>&2; Exit 1; }
+
+grep "Project-Id-Version: gt-msginit " mi-test5a.tmp 2>&1 >/dev/null || Exit 1
+
+# The charset stored in the header entry is the locale encoding of the ga_IE
+# locale, which is system dependent, or ASCII if the locale does not exist.
+#
+# Project-Id-Version contains version constructed from a temporary
+# directory name
+sed -e 's/ISO-8859-1/ASCII/g' -e 's/CP1252/ASCII/g' -e 's/UTF-8/ASCII/g' < mi-test5a.tmp | grep -v Project-Id-Version | LC_ALL=C tr -d '\r' > mi-test5a.out || Exit 1
+cat <<\EOF > mi-test5a.ok
+# Irish translations for gt-msginit package.
+# Copyright (C) 1970 THE gt-msginit'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the gt-msginit package.
+# Automatically generated, 1970.
+#
+msgid ""
+msgstr ""
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 1970-01-01 00:00+0000\n"
+"PO-Revision-Date: 1970-01-01 00:00+0000\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: ga\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ASCII\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n"
+
+msgid "Test msgid"
+msgstr ""
+EOF
+
+: ${DIFF=diff}
+${DIFF} mi-test5a.ok mi-test5a.out || Exit 1
+
+GETTEXTLIBDIR_SRCDIR="$wabs_top_srcdir"/src \
+SOURCE_DATE_EPOCH=31536000 \
+${MSGINIT} -l ga_IE --no-translator -o mi-test5b.tmp 2>mi-test5b.err
+test $? = 0 || { cat mi-test5b.err 1>&2; Exit 1; }
+
+grep "Project-Id-Version: gt-msginit " mi-test5b.tmp 2>&1 >/dev/null || Exit 1
+
+# The charset stored in the header entry is the locale encoding of the ga_IE
+# locale, which is system dependent, or ASCII if the locale does not exist.
+#
+# Project-Id-Version contains version constructed from a temporary
+# directory name
+sed -e 's/ISO-8859-1/ASCII/g' -e 's/CP1252/ASCII/g' -e 's/UTF-8/ASCII/g' < mi-test5b.tmp | grep -v Project-Id-Version | LC_ALL=C tr -d '\r' > mi-test5b.out || Exit 1
+cat <<\EOF > mi-test5b.ok
+# Irish translations for gt-msginit package.
+# Copyright (C) 1971 THE gt-msginit'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the gt-msginit package.
+# Automatically generated, 1971.
+#
+msgid ""
+msgstr ""
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 1970-01-01 00:00+0000\n"
+"PO-Revision-Date: 1970-01-01 00:00+0000\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: ga\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ASCII\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n"
+
+msgid "Test msgid"
+msgstr ""
+EOF
+
+${DIFF} mi-test5b.ok mi-test5b.out
+result=$?
+
+exit $result
diff --git a/gettext-tools/tests/xgettext-17 b/gettext-tools/tests/xgettext-17
new file mode 100755
index 000000000..437eda1eb
--- /dev/null
+++ b/gettext-tools/tests/xgettext-17
@@ -0,0 +1,64 @@
+#! /bin/sh
+. "${srcdir=.}/init.sh"; path_prepend_ . ../src
+
+# Test reproducible POT generation.
+
+cat <<\EOF > xg-t-17.c
+gettext ("Test msgid");
+EOF
+
+: ${DIFF=diff}
+: ${XGETTEXT=xgettext}
+LC_ALL=C SOURCE_DATE_EPOCH=1 \
+${XGETTEXT} -d xg-t-17 -o xg-t-17-1.pot xg-t-17.c || Exit 1
+
+# Skip the test if xgettext does not output POT-Creation-Date to the
+# header.
+if ! grep POT-Creation-Date xg-t-17-1.pot > /dev/null; then
+  Exit 77
+fi
+
+# Use the local time to generate this file.
+LC_ALL=C SOURCE_DATE_EPOCH= \
+${XGETTEXT} -d xg-t-17 -o xg-t-17-2.pot xg-t-17.c || Exit 1
+
+# Ensure that the date has changed enough.
+gentime=`date +%s`
+# Avoid any leap second.
+next=`expr $gentime + 61`
+sleep 61
+
+# Check that sleep didn't return earlier.
+while test `date +%s` -lt $next; do
+  sleep 1
+done
+
+# Generate again a file with the local time.
+LC_ALL=C SOURCE_DATE_EPOCH= \
+${XGETTEXT} -d xg-t-17 -o xg-t-17-3.pot xg-t-17.c || Exit 1
+
+if ${DIFF} xg-t-17-2.pot xg-t-17-3.pot > /dev/null; then
+  # The local date have not changed.
+  Exit 1
+fi
+
+# Call with the same SOURCE_DATE_EPOCH again.
+LC_ALL=C SOURCE_DATE_EPOCH=1 \
+${XGETTEXT} -d xg-t-17 -o xg-t-17-4.pot xg-t-17.c || Exit 1
+
+# Ensure that calls with the same SOURCE_DATE_EPOCH and input generate
+# the same output file.
+${DIFF} xg-t-17-1.pot xg-t-17-4.pot || Exit 1
+
+# Ensure that the output depends on the value provided; this adds a
+# full year to the epoch (60 * 60 * 24 * 365).
+LC_ALL=C SOURCE_DATE_EPOCH=31536000 \
+${XGETTEXT} -d xg-t-17 -o xg-t-17-5.pot xg-t-17.c || Exit 1
+
+# Ensure that two calls with different SOURCE_DATE_EPOCH generate
+# different output files.
+if ${DIFF} xg-t-17-1.pot xg-t-17-5.pot > /dev/null; then
+  Exit 1
+fi
+
+Exit 0
diff --git a/gettext-tools/tests/xgettext-3 b/gettext-tools/tests/xgettext-3
index f5e9f902c..e7981ccc2 100755
--- a/gettext-tools/tests/xgettext-3
+++ b/gettext-tools/tests/xgettext-3
@@ -22,9 +22,10 @@ _("")
 EOF
 
 : ${XGETTEXT=xgettext}
-# delete POT-Creation-Date: line because the date depends on local time.
-${XGETTEXT} --output xg-test3.tmp --no-location -k_ xg-test3.in.c 2>/dev/null || Exit 1
-sed -e '/\"POT-Creation-Date:.*/d' < xg-test3.tmp | LC_ALL=C tr -d '\r' > xg-test3.po
+# Use a fixed timestamp for POT-Creation-Date.
+SOURCE_DATE_EPOCH=1 ${XGETTEXT} --output xg-test3.tmp --no-location -k_ \
+  xg-test3.in.c 2>/dev/null || Exit 1
+LC_ALL=C tr -d '\r' < xg-test3.tmp > xg-test3.po
 
 cat <<EOF > xg-test3.ok
 # SOME DESCRIPTIVE TITLE.
@@ -37,6 +38,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 1970-01-01 00:00+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
-- 
2.29.2

