From 252187b84417efe8a4589eed410177249f3fc49c Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Mon, 11 May 2026 16:47:44 +0200 Subject: [PATCH 1/4] Export SOURCE_DATE_EPOCH for reproducible container builds Pin embedded build timestamps in the container build entrypoint so two builds of the same source produce identical binaries. Honored by OpenSSL, Apache httpd, PHP, OpenLDAP, dpkg-buildpackage, and rpmbuild. Derived from core's HEAD commit time, honoring a caller-provided value if already set. Ticket: ENT-13786 Signed-off-by: Lars Erik Wik --- build-in-container-inner.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build-in-container-inner.sh b/build-in-container-inner.sh index 3a6b39054..fcb60103c 100755 --- a/build-in-container-inner.sh +++ b/build-in-container-inner.sh @@ -40,6 +40,15 @@ for repo in $repos; do fi done +# Pin embedded build timestamps so two builds of the same source produce +# identical binaries. Honored by OpenSSL, Apache httpd, Postgres, Python +# (.pyc mtimes), dpkg-buildpackage, and rpmbuild. +if [ -z "$SOURCE_DATE_EPOCH" ]; then + SOURCE_DATE_EPOCH=$(git -C "$BASEDIR/core" log -1 --format=%ct) +fi +export SOURCE_DATE_EPOCH +echo "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" + install_mission_portal_deps() ( set -e From def385fcb6f8a365807d8f1742f4fc5883ce0e04 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Mon, 11 May 2026 16:52:22 +0200 Subject: [PATCH 2/4] Use composer --prefer-dist for reproducible container builds Composer's default falls back to cloning vendored packages, leaving .git directories in the vendor tree with non-deterministic pack files and reflog timestamps. --prefer-dist downloads zipballs instead, so no .git directory is created. Signed-off-by: Lars Erik Wik --- build-in-container-inner.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build-in-container-inner.sh b/build-in-container-inner.sh index fcb60103c..a504d36be 100755 --- a/build-in-container-inner.sh +++ b/build-in-container-inner.sh @@ -62,12 +62,12 @@ install_mission_portal_deps() ( if [ -f "$BASEDIR/mission-portal/composer.json" ]; then echo "Installing Mission Portal PHP dependencies..." - (cd "$BASEDIR/mission-portal" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs) + (cd "$BASEDIR/mission-portal" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs --prefer-dist) fi if [ -f "$BASEDIR/nova/api/http/composer.json" ]; then echo "Installing Nova API PHP dependencies..." - (cd "$BASEDIR/nova/api/http" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs) + (cd "$BASEDIR/nova/api/http" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs --prefer-dist) fi if [ -f "$BASEDIR/mission-portal/public/themes/default/bootstrap/cfengine_theme.less" ]; then @@ -79,7 +79,7 @@ install_mission_portal_deps() ( if [ -f "$BASEDIR/mission-portal/ldap/composer.json" ]; then echo "Installing LDAP API PHP dependencies..." - (cd "$BASEDIR/mission-portal/ldap" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs) + (cd "$BASEDIR/mission-portal/ldap" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs --prefer-dist) fi ) From 17d7f0b2d483b033f379374585d37f85ef71efc6 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Mon, 11 May 2026 17:08:17 +0200 Subject: [PATCH 3/4] Patch PHP phar to honor SOURCE_DATE_EPOCH PHP's phar embeds time(NULL) into every manifest entry, making phar.phar non-reproducible. Patch ext/phar to honor SOURCE_DATE_EPOCH when set, falling back to time(NULL) otherwise. Wire the patch into both the rpm spec and debian rules. Signed-off-by: Lars Erik Wik --- ...0002-Honor-SOURCE_DATE_EPOCH-in-phar.patch | 94 +++++++++++++++++++ deps-packaging/php/cfbuild-php.spec | 2 + deps-packaging/php/debian/rules | 2 + 3 files changed, 98 insertions(+) create mode 100644 deps-packaging/php/0002-Honor-SOURCE_DATE_EPOCH-in-phar.patch diff --git a/deps-packaging/php/0002-Honor-SOURCE_DATE_EPOCH-in-phar.patch b/deps-packaging/php/0002-Honor-SOURCE_DATE_EPOCH-in-phar.patch new file mode 100644 index 000000000..3a84695a9 --- /dev/null +++ b/deps-packaging/php/0002-Honor-SOURCE_DATE_EPOCH-in-phar.patch @@ -0,0 +1,94 @@ +From: Lars Erik Wik +Subject: [PATCH] Honor SOURCE_DATE_EPOCH in phar timestamps + +Phar embeds wall-clock time(NULL) into every manifest entry, making +phar.phar non-reproducible. Honor SOURCE_DATE_EPOCH (the standard +reproducible-builds mechanism) when set; fall back to time(NULL). + +Signed-off-by: Lars Erik Wik +--- + ext/phar/phar.c | 2 +- + ext/phar/phar_internal.h | 14 ++++++++++++++ + ext/phar/tar.c | 2 +- + ext/phar/util.c | 2 +- + ext/phar/zip.c | 2 +- + 5 files changed, 19 insertions(+), 3 deletions(-) + +diff --git a/ext/phar/phar.c b/ext/phar/phar.c +--- a/ext/phar/phar.c ++++ b/ext/phar/phar.c +@@ -2950,7 +2950,7 @@ + 4: metadata-len + +: metadata + */ +- mytime = time(NULL); ++ mytime = phar_source_date_epoch(); + phar_set_32(entry_buffer, entry->uncompressed_filesize); + phar_set_32(entry_buffer+4, mytime); + phar_set_32(entry_buffer+8, entry->compressed_filesize); +diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h +--- a/ext/phar/phar_internal.h ++++ b/ext/phar/phar_internal.h +@@ -22,12 +22,26 @@ + #endif + + #include ++#include + #include "php.h" + #include "tar.h" + #include "pharzip.h" + #include "zend_hash.h" + #include "ext/spl/spl_directory.h" + ++/* Reproducible-builds: honor SOURCE_DATE_EPOCH when set, else wall-clock time. */ ++static inline time_t phar_source_date_epoch(void) { ++ const char *epoch = getenv("SOURCE_DATE_EPOCH"); ++ if (epoch && *epoch) { ++ char *end; ++ long long t = strtoll(epoch, &end, 10); ++ if (*end == '\0' && t >= 0) { ++ return (time_t)t; ++ } ++ } ++ return time(NULL); ++} ++ + /* PHP_ because this is public information via MINFO */ + #define PHP_PHAR_API_VERSION "1.1.1" + /* x.y.z maps to 0xyz0 */ +diff --git a/ext/phar/tar.c b/ext/phar/tar.c +--- a/ext/phar/tar.c ++++ b/ext/phar/tar.c +@@ -977,7 +977,7 @@ + char *buf, *signature, sigbuf[8]; + + entry.flags = PHAR_ENT_PERM_DEF_FILE; +- entry.timestamp = time(NULL); ++ entry.timestamp = phar_source_date_epoch(); + entry.is_modified = 1; + entry.is_crc_checked = 1; + entry.is_tar = 1; +diff --git a/ext/phar/util.c b/ext/phar/util.c +--- a/ext/phar/util.c ++++ b/ext/phar/util.c +@@ -700,7 +700,7 @@ + + phar_add_virtual_dirs(phar, path, path_len); + etemp.is_modified = 1; +- etemp.timestamp = time(0); ++ etemp.timestamp = phar_source_date_epoch(); + etemp.is_crc_checked = 1; + etemp.phar = phar; + etemp.filename = zend_string_init(path, path_len, false); +diff --git a/ext/phar/zip.c b/ext/phar/zip.c +--- a/ext/phar/zip.c ++++ b/ext/phar/zip.c +@@ -1253,7 +1253,7 @@ + + pass.error = &temperr; + entry.flags = PHAR_ENT_PERM_DEF_FILE; +- entry.timestamp = time(NULL); ++ entry.timestamp = phar_source_date_epoch(); + entry.is_modified = 1; + entry.is_zip = true; + entry.phar = phar; diff --git a/deps-packaging/php/cfbuild-php.spec b/deps-packaging/php/cfbuild-php.spec index d75aa112f..f9e4806ad 100644 --- a/deps-packaging/php/cfbuild-php.spec +++ b/deps-packaging/php/cfbuild-php.spec @@ -25,6 +25,8 @@ then patch -p1 < %{_topdir}/SOURCES/0001-Disable-fancy-intrinsics-stuff.patch fi +patch -p1 < %{_topdir}/SOURCES/0002-Honor-SOURCE_DATE_EPOCH-in-phar.patch + %if %{?rhel}%{!?rhel:0} == 8 CFLAGS="-fPIE" LDFLAGS="-pie" diff --git a/deps-packaging/php/debian/rules b/deps-packaging/php/debian/rules index 759498d0e..d35d1b49e 100755 --- a/deps-packaging/php/debian/rules +++ b/deps-packaging/php/debian/rules @@ -12,6 +12,8 @@ build: build-stamp build-stamp: dh_testdir + patch -p1 < $(CURDIR)/0002-Honor-SOURCE_DATE_EPOCH-in-phar.patch + ./configure --prefix=$(PREFIX)/httpd/php \ --with-config-file-scan-dir=$(PREFIX)/httpd/php/lib \ --without-apxs2 \ From 8d5d4aaf6a85aff665b8897b3a6faf8b2221f29b Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Mon, 11 May 2026 18:25:33 +0200 Subject: [PATCH 4/4] Strip composer vendor .git directories after install Composer falls back to git clone when GitHub's anonymous zipball API rate limit is hit, so --prefer-dist alone is not enough to guarantee a .git-free vendor tree. Strip any .git directories that end up under vendor/ after the composer installs. Signed-off-by: Lars Erik Wik --- build-in-container-inner.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build-in-container-inner.sh b/build-in-container-inner.sh index a504d36be..16f708477 100755 --- a/build-in-container-inner.sh +++ b/build-in-container-inner.sh @@ -81,6 +81,11 @@ install_mission_portal_deps() ( echo "Installing LDAP API PHP dependencies..." (cd "$BASEDIR/mission-portal/ldap" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs --prefer-dist) fi + + # Composer falls back to git clone when GitHub's anonymous zipball + # rate limit is hit, leaving non-reproducible .git directories in the + # vendor tree. Strip them. + find "$BASEDIR/mission-portal" "$BASEDIR/nova/api/http" -type d -name .git -path '*/vendor/*' -exec rm -rf {} + ) # === Step runner with failure reporting ===