build: detect GNU/BSD date at configure time for SOURCE_DATE_EPOCH

The SOURCE_DATE_EPOCH -> date conversion previously relied on a runtime
`date -u -d @EPOCH 2>/dev/null || date -u -r EPOCH` fallback.  That is
unreliable on macOS/*BSD: BSD `date -d` reads -d as a DST flag and silently
prints the wrong time (exit 0), so the `-r` fallback is never reached.

Decide at configure time instead: prefer coreutils `gdate` (GNU date, e.g.
installed on macOS), else a native `date` that really is GNU (verified by the
epoch-0 output containing 1970), else BSD `date -r`.  configure substitutes the
chosen invocation as @SOURCE_DATE_CMD@ and defs.mak appends the epoch.

Verified on Linux (no gdate -> GNU `date -u -d @`): a fixed SOURCE_DATE_EPOCH
yields a byte-identical buildinfo.o across builds; the no-epoch path still
stamps the current local time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Darryl L. Miles 2026-07-23 15:31:18 +00:00 committed by R. Timothy Edwards
parent 67d6a2ff5a
commit 25fa181200
3 changed files with 76 additions and 4 deletions

53
scripts/configure vendored
View File

@ -693,6 +693,8 @@ GREP
PERL
CCACHE_PREFIX_MAP_FLAG
CCACHE
SOURCE_DATE_CMD
GDATE
SED
RANLIB
INSTALL_DATA
@ -4239,6 +4241,57 @@ $as_echo "$ac_cv_path_SED" >&6; }
rm -f conftest.sed
for ac_prog in gdate
do
# Extract the first word of "$ac_prog", so it can be a program name with args.
set dummy $ac_prog; ac_word=$2
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
$as_echo_n "checking for $ac_word... " >&6; }
if ${ac_cv_prog_GDATE+:} false; then :
$as_echo_n "(cached) " >&6
else
if test -n "$GDATE"; then
ac_cv_prog_GDATE="$GDATE" # Let the user override the test.
else
as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
for as_dir in $PATH
do
IFS=$as_save_IFS
test -z "$as_dir" && as_dir=.
for ac_exec_ext in '' $ac_executable_extensions; do
if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then
ac_cv_prog_GDATE="$ac_prog"
$as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5
break 2
fi
done
done
IFS=$as_save_IFS
fi
fi
GDATE=$ac_cv_prog_GDATE
if test -n "$GDATE"; then
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $GDATE" >&5
$as_echo "$GDATE" >&6; }
else
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
$as_echo "no" >&6; }
fi
test -n "$GDATE" && break
done
if test -n "$GDATE"; then
SOURCE_DATE_CMD="$GDATE -u -d @"
elif date -u -d @0 2>/dev/null | grep -q 1970; then
SOURCE_DATE_CMD="date -u -d @"
else
SOURCE_DATE_CMD="date -u -r "
fi
# Check whether --enable-ccache was given.
if test "${enable_ccache+set}" = set; then :
enableval=$enable_ccache;

View File

@ -29,6 +29,23 @@ AC_PROG_INSTALL
AC_PROG_RANLIB
AC_PROG_SED
dnl SOURCE_DATE_EPOCH -> UTC date, for reproducible builds. GNU date (Linux, or
dnl coreutils `gdate` on macOS) converts an epoch with `-d @EPOCH`; BSD date
dnl (native macOS/*BSD) uses `-r EPOCH`. Decide here, at configure time, because
dnl runtime probing is unreliable: BSD `date -d` silently mis-parses `@EPOCH` (it
dnl reads -d as a DST flag) and prints the wrong time rather than failing. Prefer
dnl `gdate`; else a `date` that really is GNU (verified by the epoch-0 output
dnl containing 1970); else fall back to BSD `date -r`.
AC_CHECK_PROGS([GDATE], [gdate])
if test -n "$GDATE"; then
SOURCE_DATE_CMD="$GDATE -u -d @"
elif date -u -d @0 2>/dev/null | grep -q 1970; then
SOURCE_DATE_CMD="date -u -d @"
else
SOURCE_DATE_CMD="date -u -r "
fi
AC_SUBST([SOURCE_DATE_CMD])
dnl Prefix the compilers with ccache when it is installed (autodetected; opt out
dnl with --disable-ccache). Done after the AC_PROG_C* checks above so those
dnl feature tests run with the plain compiler; only the substituted @CC@/@CXX@

View File

@ -131,11 +131,13 @@ MAGIC_BUILDDATE_DEFS = @MAGIC_BUILDDATE_DEFS@
# skipping it under --disable-magic-builddate avoids the parse-time date spawn.
ifeq (${MAGIC_BUILDDATE_DEFS},)
# Honor SOURCE_DATE_EPOCH (reproducible builds -- appimage/npm tarballs) when set,
# else the current time. The SOURCE_DATE_EPOCH branch uses UTC so a reproducible
# stamp does not depend on the builder's zone; the plain-`date` fallback is local.
# else the current time. @SOURCE_DATE_CMD@ is the platform's epoch->UTC-date
# invocation, chosen by configure (GNU `date -u -d @` / `gdate` vs BSD
# `date -u -r `); the epoch is appended here. The SOURCE_DATE_EPOCH branch is
# UTC so a reproducible stamp does not depend on the builder's zone; the plain
# `date` fallback is local.
MAGIC_BUILDDATE ?= $(shell if [ -n "$$SOURCE_DATE_EPOCH" ]; then \
date -u -d "@$$SOURCE_DATE_EPOCH" 2>/dev/null \
|| date -u -r "$$SOURCE_DATE_EPOCH" 2>/dev/null; \
@SOURCE_DATE_CMD@$$SOURCE_DATE_EPOCH; \
else date; fi | tr -d '\r\n')
endif