diff --git a/src/Build/CMakeLists.txt b/src/Build/CMakeLists.txt index 8e3fabce..2634cd34 100644 --- a/src/Build/CMakeLists.txt +++ b/src/Build/CMakeLists.txt @@ -27,6 +27,34 @@ else() endif() project(${PROJECT_NAME}) +# SOURCE_DATE_EPOCH for the cpack-driven DEB pipeline. +# Precedence: -DSOURCE_DATE_EPOCH=N, env, git HEAD, fixed fallback. +# Re-exported to ENV so dpkg-deb/tar inherit it. +if(NOT DEFINED SOURCE_DATE_EPOCH) + if(DEFINED ENV{SOURCE_DATE_EPOCH}) + set(SOURCE_DATE_EPOCH "$ENV{SOURCE_DATE_EPOCH}") + else() + execute_process( + COMMAND git -C "$ENV{SOURCEPATH}" log -1 --pretty=%ct + OUTPUT_VARIABLE _git_ct + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + RESULT_VARIABLE _git_rc) + if(_git_rc EQUAL 0 AND _git_ct) + set(SOURCE_DATE_EPOCH "${_git_ct}") + else() + set(SOURCE_DATE_EPOCH "1577836800") + endif() + endif() +endif() +if(NOT SOURCE_DATE_EPOCH MATCHES "^[0-9]+$") + MESSAGE(FATAL_ERROR "SOURCE_DATE_EPOCH must be a non-negative Unix timestamp") +endif() +message(STATUS "SOURCE_DATE_EPOCH = ${SOURCE_DATE_EPOCH}") +set(ENV{SOURCE_DATE_EPOCH} "${SOURCE_DATE_EPOCH}") +# Avoid nondeterministic ordering from cpack 3.18+ parallel compression. +set(CPACK_THREADS 1) + # - Check whether 'Tcdefs.h' and 'License.txt' exist if(NOT EXISTS "$ENV{SOURCEPATH}/Common/Tcdefs.h") MESSAGE(FATAL_ERROR "Tcdefs.h does not exist.") @@ -254,6 +282,19 @@ if ( ( PLATFORM STREQUAL "Debian" ) OR ( PLATFORM STREQUAL "Ubuntu" ) ) set( DEBIAN_PRERM ${CMAKE_CURRENT_BINARY_DIR}/Packaging/debian-control/prerm) set( CPACK_GENERATOR "DEB" ) # mandatory + + # Reproducible DEB: clamp the just-installed staging tree's mtimes + # and modes so the payload is independent of wall-clock time and + # the build umask. Placed AFTER install(DIRECTORY) so it runs against a + # populated tree (install rules execute in declaration order). The script + # acts only on a real package staging root and refuses a live prefix; + # see the script header for the staging-root detection rules. + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/Tools/cmake_repro_clamp_mtimes.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/cmake_repro_clamp_mtimes.cmake" + @ONLY) + install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/cmake_repro_clamp_mtimes.cmake") + set( CPACK_DEBIAN_PACKAGE_NAME ${CPACK_PACKAGE_NAME} ) # mandatory set( CPACK_DEBIAN_FILE_NAME ${CPACK_PACKAGE_FILE_NAME}.deb ) # mandatory # -- Use a distro-specific version string to avoid repository conflicts -- @@ -357,6 +398,7 @@ elseif ( ( PLATFORM STREQUAL "CentOS" ) OR ( PLATFORM STREQUAL "openSUSE" ) OR ( set( CPACK_RPM_PACKAGE_GROUP "Applications/System" ) # mandatory, https://fedoraproject.org/wiki/RPMGroups set( CPACK_RPM_PACKAGE_VENDOR ${CPACK_PACKAGE_VENDOR} ) # mandatory set( CPACK_RPM_PACKAGE_AUTOREQ "no" ) # disable automatic shared libraries dependency detection (most of the time buggy) + if (VC_WITH_FUSE3) set(VC_RPM_FUSE_PACKAGE "fuse3") else () diff --git a/src/Build/Include/Makefile.inc b/src/Build/Include/Makefile.inc index 2f3aee68..52e0d650 100644 --- a/src/Build/Include/Makefile.inc +++ b/src/Build/Include/Makefile.inc @@ -119,7 +119,18 @@ TR_SED_BIN := tr '\n' ' ' | tr -s ' ' ',' | sed -e 's/^,//g' -e 's/,$$/n/' | tr -include $(OBJS:.o=.d) $(OBJSEX:.oo=.d) $(OBJSNOOPT:.o0=.d) $(OBJSHANI:.oshani=.d) $(OBJAESNI:.oaesni=.d) $(OBJSSSE41:.osse41=.d) $(OBJSSSSE3:.ossse3=.d) $(OBJSAVX2:.oavx2=.d) $(OBJARMV8CRYPTO:.oarmv8crypto=.d) +# Deterministic static library: the 'D' modifier zeroes member mtime/uid/gid +# and 'ranlib -D' writes a deterministic index. Both are probed functionally +# (running them on a throwaway archive) rather than by parsing --help, whose +# wording varies between binutils versions. Very old binutils that lack the +# feature simply falls back to a normal, still-correct archive. +# Probe also covers BSD ar / macOS libtool ar (neither supports -D): both +# variables come out empty there and the original ar/ranlib calls are used. +AR_DETERMINISTIC := $(shell t=$$(mktemp); rm -f $$t.a; $(AR) Drc $$t.a $$t >/dev/null 2>&1 && echo D; rm -f $$t $$t.a) +RANLIB_DETERMINISTIC := $(shell t=$$(mktemp); rm -f $$t.a; $(AR) rc $$t.a $$t >/dev/null 2>&1; $(RANLIB) -D $$t.a >/dev/null 2>&1 && echo -D; rm -f $$t $$t.a) + $(NAME).a: $(OBJS) $(OBJSEX) $(OBJSNOOPT) $(OBJSHANI) $(OBJAESNI) $(OBJSSSE41) $(OBJSSSSE3) $(OBJSAVX2) $(OBJARMV8CRYPTO) @echo Updating library $@ - $(AR) $(AFLAGS) -rc $@ $(OBJS) $(OBJSEX) $(OBJSNOOPT) $(OBJSHANI) $(OBJAESNI) $(OBJSSSE41) $(OBJSSSSE3) $(OBJSAVX2) $(OBJARMV8CRYPTO) - $(RANLIB) $@ + rm -f $@ + $(AR) $(AFLAGS) $(AR_DETERMINISTIC)rc $@ $(OBJS) $(OBJSEX) $(OBJSNOOPT) $(OBJSHANI) $(OBJAESNI) $(OBJSSSE41) $(OBJSSSSE3) $(OBJSAVX2) $(OBJARMV8CRYPTO) + $(RANLIB) $(RANLIB_DETERMINISTIC) $@ diff --git a/src/Build/Tools/cmake_repro_clamp_mtimes.cmake.in b/src/Build/Tools/cmake_repro_clamp_mtimes.cmake.in new file mode 100644 index 00000000..33d93740 --- /dev/null +++ b/src/Build/Tools/cmake_repro_clamp_mtimes.cmake.in @@ -0,0 +1,108 @@ +# Copyright (c) 2026 VeraCrypt +# Governed by the Apache License 2.0. +# +# Run at install time by install(SCRIPT ...) AFTER all install(DIRECTORY) +# rules, so the staging tree is fully populated. Clamps every file's mtime +# and permission bits so the CPack DEB payload is reproducible. +# +# Safety: only a package staging tree may be modified, never a live host +# tree. Two recognised staging conventions: +# 1. CPack: it installs into its private +# /_CPack_Packages/.../${CPACK_PACKAGING_INSTALL_PREFIX} +# directory, exposed here as CMAKE_INSTALL_PREFIX. We require the +# path to contain a "_CPack_Packages" component. +# 2. "make DESTDIR= install" style: $ENV{DESTDIR} is a non-live +# staging root and $ENV{DESTDIR}${CMAKE_INSTALL_PREFIX} is clamped. +# Anything else (bare "cmake --install" into /usr or /usr/local) is +# refused so root cannot rewrite mtimes/modes outside a package build. + +if(NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") + return() +endif() + +set(_destdir "$ENV{DESTDIR}") +set(_prefix "${CMAKE_INSTALL_PREFIX}") + +if(NOT _destdir STREQUAL "") + get_filename_component(_destdir_abs "${_destdir}" ABSOLUTE) + foreach(_live_destdir "/" "/usr" "/usr/local" "/opt") + if(_destdir_abs STREQUAL "${_live_destdir}") + message(FATAL_ERROR "Reproducible build: refusing to clamp live " + "DESTDIR '${_destdir}'") + endif() + endforeach() + set(_staging "${_destdir}${_prefix}") +elseif(_prefix MATCHES "/_CPack_Packages/") + set(_staging "${_prefix}") +else() + message(STATUS "Reproducible build: not a package staging install " + "(DESTDIR empty and prefix '${_prefix}' is not a CPack " + "staging tree); skipping mtime/mode clamp") + return() +endif() +get_filename_component(_staging "${_staging}" ABSOLUTE) + +if(NOT IS_DIRECTORY "${_staging}") + message(STATUS "Reproducible build: staging root '${_staging}' absent, " + "skipping mtime/mode clamp") + return() +endif() + +# SOURCE_DATE_EPOCH is baked in by configure_file-style substitution from +# the parent CMakeLists (see install(SCRIPT) call site). +set(_epoch "@SOURCE_DATE_EPOCH@") +if(NOT _epoch MATCHES "^[0-9]+$") + message(FATAL_ERROR "Reproducible build: SOURCE_DATE_EPOCH must be a " + "non-negative Unix timestamp") +endif() + +# Probe GNU touch on a private temp file, not /dev/null: /dev/null is +# root-owned, so probing it fails for normal users and rewrites the +# device node's mtime as root (the bug the review flagged). +execute_process( + COMMAND mktemp + OUTPUT_VARIABLE _probe + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _mktemp_rc) +if(NOT _mktemp_rc EQUAL 0) + message(STATUS "Reproducible build: mktemp failed, skipping mtime clamp") + return() +endif() +execute_process( + COMMAND touch --no-dereference --date=@0 "${_probe}" + RESULT_VARIABLE _touch_rc + OUTPUT_QUIET ERROR_QUIET) +file(REMOVE "${_probe}") +if(NOT _touch_rc EQUAL 0) + message(STATUS "Reproducible build: GNU touch unavailable, " + "skipping mtime clamp") + return() +endif() + +# Normalise permission bits first: the make-side prepare creates staging +# dirs with "mkdir -p" (umask-dependent) and CPack's tar records those +# modes verbatim via USE_SOURCE_PERMISSIONS, so umask 027 -> 0750 vs +# umask 022 -> 0755 breaks reproducibility. Match the legacy tar's +# --mode=go-w,a+rX: dirs/exes 0755, regular files 0644. +execute_process( + COMMAND find "${_staging}" -type d -exec chmod 0755 {} + + RESULT_VARIABLE _cd_rc OUTPUT_QUIET ERROR_QUIET) +execute_process( + COMMAND find "${_staging}" -type f -perm -u+x -exec chmod 0755 {} + + RESULT_VARIABLE _cx_rc OUTPUT_QUIET ERROR_QUIET) +execute_process( + COMMAND find "${_staging}" -type f -not -perm -u+x -exec chmod 0644 {} + + RESULT_VARIABLE _cf_rc OUTPUT_QUIET ERROR_QUIET) + +# Clamp mtimes last so this is the final metadata write. +execute_process( + COMMAND find "${_staging}" -exec + touch --no-dereference --date=@${_epoch} {} + + RESULT_VARIABLE _find_rc + OUTPUT_QUIET ERROR_QUIET) + +if(_find_rc EQUAL 0 AND _cd_rc EQUAL 0 AND _cx_rc EQUAL 0 AND _cf_rc EQUAL 0) + message(STATUS "Reproducible build: clamped mtimes and modes under ${_staging}") +else() + message(WARNING "Reproducible build: mtime/mode clamp incomplete under ${_staging}") +endif() diff --git a/src/Build/Tools/makeself_repro_finalize.py b/src/Build/Tools/makeself_repro_finalize.py new file mode 100755 index 00000000..9514211f --- /dev/null +++ b/src/Build/Tools/makeself_repro_finalize.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2026 VeraCrypt +# Governed by the Apache License 2.0. +# +# Zero the gzip mtime in a makeself archive and refresh its integrity +# fields. makeself runs `gzip -c9 < tmpfile' which writes tmpfile's +# mtime into the gzip header (gzip ignores SOURCE_DATE_EPOCH for +# redirected stdin), so the installer is otherwise not reproducible. +# +# After editing the payload the recorded checksums are refreshed: +# - CRCsum is set to "0000000000". Makeself stores a POSIX cksum(1) +# value there, not a zlib CRC-32 (the two differ); an all-zero +# CRCsum makes its extractor skip the redundant CRC check. +# - MD5 is recomputed, which the extractor still verifies. +# +# Usage: makeself_repro_finalize.py + +import hashlib +import re +import sys + + +def finalize(path): + with open(path, "rb") as f: + raw = bytearray(f.read()) + text = raw.decode("latin1", errors="replace") + # Locate payload start by line count, mirroring makeself's own extractor. + m = re.search(r'^skip="(\d+)"', text, re.MULTILINE) + if not m: + sys.exit(f"{path}: no skip= line in makeself header") + skip = int(m.group(1)) + header_text = "\n".join(text.split("\n")[:skip]) + "\n" + offset = len(header_text.encode("latin1")) + if bytes(raw[offset:offset + 3]) != b"\x1f\x8b\x08": + sys.exit(f"{path}: no gzip magic at payload offset {offset}") + # gzip header mtime: 4-byte LE uint at offset+4 (RFC 1952 section 2.3.1). + raw[offset + 4:offset + 8] = b"\x00\x00\x00\x00" + payload = bytes(raw[offset:]) + new_md5 = hashlib.md5(payload).hexdigest() + # CRCsum -> all zeros (extractor then skips the CRC check); MD5 -> fresh. + new_header = re.sub(r'CRCsum="[^"]*"', 'CRCsum="0000000000"', header_text) + new_header = re.sub(r'MD5="[0-9a-fA-F]+"', f'MD5="{new_md5}"', new_header) + new_bytes = new_header.encode("latin1") + # Line count must stay the same so makeself's "skip=" remains accurate. + if new_bytes.count(b"\n") != skip: + sys.exit(f"{path}: header line count changed during rewrite") + with open(path, "wb") as f: + f.write(new_bytes + payload) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit("Usage: makeself_repro_finalize.py ") + finalize(sys.argv[1]) diff --git a/src/Build/build_cmake_deb.sh b/src/Build/build_cmake_deb.sh index 64cdf559..d85cdded 100755 --- a/src/Build/build_cmake_deb.sh +++ b/src/Build/build_cmake_deb.sh @@ -9,6 +9,26 @@ # Errors should cause script to exit set -e +# Deterministic umask: dpkg-deb records the mode of the temporary ar +# members (debian-binary, control.tar.gz, data.tar.gz) it creates, so a +# caller umask of 027 yields 0640 where 022 yields 0644 and the .deb is +# not reproducible. Pin it for the whole packaging run. +umask 022 + +# Compute and export SOURCE_DATE_EPOCH so cmake/cpack inherit it (they get +# an empty env from this shell otherwise). Precedence: caller, git HEAD, +# fallback constant matching src/Makefile and CMakeLists.txt. +if [ -z "${SOURCE_DATE_EPOCH:-}" ]; then + SOURCE_DATE_EPOCH=$(git -C "$(dirname "$0")/../.." log -1 --pretty=%ct 2>/dev/null || echo 1577836800) +fi +case "$SOURCE_DATE_EPOCH" in + ''|*[!0-9]*) + echo "Error: SOURCE_DATE_EPOCH must be a non-negative Unix timestamp" >&2 + exit 1 + ;; +esac +export SOURCE_DATE_EPOCH + # Absolute path to this script export SCRIPT=$(readlink -f "$0") # Absolute path this script is in @@ -158,8 +178,8 @@ rm -rf $PARENTDIR/VeraCrypt_Packaging mkdir -p $PARENTDIR/VeraCrypt_Packaging/GUI mkdir -p $PARENTDIR/VeraCrypt_Packaging/Console -cmake -H$SCRIPTPATH -B$PARENTDIR/VeraCrypt_Packaging/GUI -DVERACRYPT_BUILD_DIR="$PARENTDIR/VeraCrypt_Setup/GUI" -DNOGUI=FALSE $FUSE3_CMAKE_FLAG || exit 1 +cmake -H$SCRIPTPATH -B$PARENTDIR/VeraCrypt_Packaging/GUI -DVERACRYPT_BUILD_DIR="$PARENTDIR/VeraCrypt_Setup/GUI" -DNOGUI=FALSE -DSOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH $FUSE3_CMAKE_FLAG || exit 1 cpack --config $PARENTDIR/VeraCrypt_Packaging/GUI/CPackConfig.cmake || exit 1 -cmake -H$SCRIPTPATH -B$PARENTDIR/VeraCrypt_Packaging/Console -DVERACRYPT_BUILD_DIR="$PARENTDIR/VeraCrypt_Setup/Console" -DNOGUI=TRUE $FUSE3_CMAKE_FLAG || exit 1 +cmake -H$SCRIPTPATH -B$PARENTDIR/VeraCrypt_Packaging/Console -DVERACRYPT_BUILD_DIR="$PARENTDIR/VeraCrypt_Setup/Console" -DNOGUI=TRUE -DSOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH $FUSE3_CMAKE_FLAG || exit 1 cpack --config $PARENTDIR/VeraCrypt_Packaging/Console/CPackConfig.cmake || exit 1 diff --git a/src/Build/build_cmake_opensuse.sh b/src/Build/build_cmake_opensuse.sh index 87c5a75e..6db94d35 100644 --- a/src/Build/build_cmake_opensuse.sh +++ b/src/Build/build_cmake_opensuse.sh @@ -9,6 +9,9 @@ # Errors should cause script to exit set -e +# Keep staged RPM payload permissions independent of the caller's umask. +umask 022 + # Absolute path to this script export SCRIPT=$(readlink -f "$0") # Absolute path this script is in diff --git a/src/Build/build_cmake_rpm.sh b/src/Build/build_cmake_rpm.sh index 499a1c61..d40fb79a 100644 --- a/src/Build/build_cmake_rpm.sh +++ b/src/Build/build_cmake_rpm.sh @@ -9,6 +9,9 @@ # Errors should cause script to exit set -e +# Keep staged RPM payload permissions independent of the caller's umask. +umask 022 + # Absolute path to this script export SCRIPT=$(readlink -f "$0") # Absolute path this script is in diff --git a/src/Main/Main.make b/src/Main/Main.make index fbabcb31..36c71f09 100755 --- a/src/Main/Main.make +++ b/src/Main/Main.make @@ -194,13 +194,18 @@ endif endif #----------------------------------- +# Probe strip --enable-deterministic-archives against a private object +# rather than "-V" (which can fail in getopt before printing). All +# platforms, so non-Linux behaviour matches the original PR. +STRIP_DETERMINISTIC := $(strip $(shell d=$$(mktemp -d 2>/dev/null) && printf 'int x;\n' | $(CC) -x c -c - -o "$$d/o.o" >/dev/null 2>&1 && strip --enable-deterministic-archives "$$d/o.o" >/dev/null 2>&1 && echo --enable-deterministic-archives; rm -rf "$$d")) + $(APPNAME): $(LIBS) $(OBJS) @echo Linking $@ $(CXX) -o $(APPNAME) $(OBJS) $(LIBS) $(AYATANA_LIBS) $(FUSE_LIBS) $(WX_LIBS) $(LFLAGS) ifeq "$(TC_BUILD_CONFIG)" "Release" ifndef NOSTRIP - strip $(APPNAME) + strip $(STRIP_DETERMINISTIC) $(APPNAME) endif ifndef NOTEST @@ -294,6 +299,23 @@ endif ifeq "$(PLATFORM)" "Linux" + +# Packaging-tool feature probes. Empty result = host lacks the feature; +# the recipe falls back to the pre-PR (non-deterministic) form with a +# warning. $(strip) because $(shell) keeps trailing whitespace which +# would break the "= yes" equality test in the recipe. +# +# All probes act on a private $(mktemp) file and clean it up. touch +# probing /dev/null fails (EPERM) for unprivileged users and rewrites +# the device node as root. The tar option set requires GNU tar >= 1.28, +# so the probe exercises it exactly rather than matching "GNU tar". +# MAKESELF_TAR_EXTRA needs Makeself >= 2.3.1 (cited in the review). +TOUCH_REPRODUCIBLE := $(strip $(shell t=$$(mktemp 2>/dev/null) && touch --no-dereference --date=@0 "$$t" >/dev/null 2>&1 && echo yes; rm -f "$$t")) +TAR_DETERMINISTIC := $(strip $(shell t=$$(mktemp 2>/dev/null) && tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner --mode='go-w,a+rX' --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime -cf "$$t" --files-from /dev/null >/dev/null 2>&1 && echo yes; rm -f "$$t")) +GZIP_NO_TIMESTAMP := $(strip $(shell printf x | gzip -n -c >/dev/null 2>&1 && echo yes)) +MAKESELF_PACKAGING_DATE := $(strip $(shell makeself --help 2>&1 | grep -q -- '--packaging-date' && echo yes)) +MAKESELF_TAR_EXTRA := $(strip $(shell makeself --help 2>&1 | grep -q -- '--tar-extra' && echo yes)) + prepare: $(APPNAME) rm -fr $(BASE_DIR)/Setup/Linux/usr mkdir -p $(BASE_DIR)/Setup/Linux/usr/bin @@ -331,6 +353,22 @@ ifndef TC_NO_GUI cp -r $(BASE_DIR)/Setup/Linux/usr $(BASE_DIR)/Setup/Linux/veracrypt.AppDir/. ln -sf usr/share/icons/hicolor/1024x1024/apps/$(APPNAME).png $(BASE_DIR)/Setup/Linux/veracrypt.AppDir/$(APPNAME).png endif + # Normalise modification times of every staged file. cp preserves the + # checkout-time mtimes of the source tree, which would otherwise leak + # into the tar/makeself archives and break reproducibility. + # Only run when GNU touch supports the option set. Keep AppImage + # outside this narrowed reproducibility scope: appimagetool is not + # verified here, so do not pre-clamp veracrypt.AppDir for that target. +ifeq "$(TOUCH_REPRODUCIBLE)" "yes" + _appdir="$(BASE_DIR)/Setup/Linux/veracrypt.AppDir"; \ + if [ -n "$(filter appimage,$(MAKECMDGOALS))" ] || [ ! -d "$$_appdir" ]; then \ + _appdir=""; \ + fi; \ + find $(BASE_DIR)/Setup/Linux/usr $$_appdir \ + -exec touch --no-dereference --date=@$(SOURCE_DATE_EPOCH) {} + +else + @echo "Reproducible build: GNU touch unavailable, skipping mtime normalisation" +endif install: prepare @@ -341,7 +379,25 @@ endif ifeq "$(TC_BUILD_CONFIG)" "Release" package: prepare + # Deterministic tarball: sort members, pin mtime to SOURCE_DATE_EPOCH, + # drop owner/group identity, and use gzip -n so no timestamp/name is + # stored in the gzip header. + # --mode= normalises permission bits so the host umask cannot leak + # into them. Falls back to plain tar cfz when probes fail. +ifeq "$(TAR_DETERMINISTIC)$(GZIP_NO_TIMESTAMP)" "yesyes" + tar --sort=name --mtime=@$(SOURCE_DATE_EPOCH) \ + --owner=0 --group=0 --numeric-owner \ + --mode='go-w,a+rX' \ + --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime \ + -cf $(BASE_DIR)/Setup/Linux/$(APPNAME)_$(TC_VERSION).tar \ + --directory $(BASE_DIR)/Setup/Linux usr + gzip -9 -n -c $(BASE_DIR)/Setup/Linux/$(APPNAME)_$(TC_VERSION).tar \ + > $(BASE_DIR)/Setup/Linux/$(PACKAGE_NAME) + rm -f $(BASE_DIR)/Setup/Linux/$(APPNAME)_$(TC_VERSION).tar +else + @echo "Reproducible build: non-deterministic tar.gz fallback (GNU tar=$(if $(TAR_DETERMINISTIC),yes,no), gzip -n=$(if $(GZIP_NO_TIMESTAMP),yes,no))" tar cfz $(BASE_DIR)/Setup/Linux/$(PACKAGE_NAME) --directory $(BASE_DIR)/Setup/Linux usr +endif @rm -fr $(INTERNAL_INSTALLER_NAME) @echo "#!/bin/sh" > $(INTERNAL_INSTALLER_NAME) @@ -359,7 +415,33 @@ package: prepare rm -fr $(BASE_DIR)/Setup/Linux/packaging mkdir -p $(BASE_DIR)/Setup/Linux/packaging cp $(INTERNAL_INSTALLER_NAME) $(BASE_DIR)/Setup/Linux/packaging/. - makeself $(BASE_DIR)/Setup/Linux/packaging $(BASE_DIR)/Setup/Linux/$(INSTALLER_NAME) "VeraCrypt $(TC_VERSION) Installer" ./$(INTERNAL_INSTALLER_NAME) + # makeself: --packaging-date pins the banner date, SOURCE_DATE_EPOCH is + # honoured by the embedded tar/gzip, and the archive is sorted so the + # self-extracting installer is byte-identical across builds. + # Flags gated per probe; invoked from Setup/Linux with relative paths + # so the build path does not end up in makeself's echoed argv. + @cd $(BASE_DIR)/Setup/Linux && set --; \ + if [ "$(MAKESELF_PACKAGING_DATE)" = yes ]; then \ + set -- "$$@" --packaging-date "@$(SOURCE_DATE_EPOCH)"; \ + fi; \ + if [ "$(MAKESELF_TAR_EXTRA)" = yes ] && [ "$(TAR_DETERMINISTIC)" = yes ]; then \ + set -- "$$@" --tar-extra "--sort=name --mtime=@$(SOURCE_DATE_EPOCH) --owner=0 --group=0 --numeric-owner --mode=go-w,a+rX"; \ + fi; \ + if [ "$$#" -eq 0 ]; then \ + echo "Reproducible build: makeself flags unavailable, installer will not be byte-identical"; \ + fi; \ + makeself "$$@" \ + packaging "$(INSTALLER_NAME)" \ + "VeraCrypt $(TC_VERSION) Installer" "./$(INTERNAL_INSTALLER_NAME)" + # makeself runs 'gzip -c9 < tmpfile' which writes tmpfile's mtime into + # the gzip header (SOURCE_DATE_EPOCH is ignored for redirected stdin). + # Zero the mtime and refresh CRCsum/MD5; installer --check still passes. + @if command -v python3 >/dev/null 2>&1; then \ + python3 $(BASE_DIR)/Build/Tools/makeself_repro_finalize.py \ + $(BASE_DIR)/Setup/Linux/$(INSTALLER_NAME); \ + else \ + echo "Reproducible build: python3 unavailable, skipping makeself finalize"; \ + fi appimage: prepare @set -e; \ @@ -398,6 +480,9 @@ appimage: prepare wget --quiet -O "$${_appimagetool_executable_path}" "$${_appimagetool_url}"; \ chmod +x "$${_appimagetool_executable_path}"; \ echo "Creating AppImage $${_final_appimage_path}..."; \ + if [ "$(VC_SOURCE_DATE_EPOCH_AUTO)" = "1" ]; then \ + unset SOURCE_DATE_EPOCH; \ + fi; \ ARCH="$${_final_appimage_arch_suffix}" "$${_appimagetool_executable_path}" "$(BASE_DIR)/Setup/Linux/veracrypt.AppDir" "$${_final_appimage_path}"; \ echo "AppImage created: $${_final_appimage_path}"; \ echo "Cleaning up appimagetool..."; \ diff --git a/src/Makefile b/src/Makefile index a406bd45..d96b713c 100644 --- a/src/Makefile +++ b/src/Makefile @@ -582,6 +582,75 @@ CFLAGS := $(C_CXX_FLAGS) $(CFLAGS) $(TC_EXTRA_CFLAGS) CXXFLAGS := $(C_CXX_FLAGS) $(CXXFLAGS) $(TC_EXTRA_CXXFLAGS) LFLAGS := $(LFLAGS) $(TC_EXTRA_LFLAGS) +#------ Reproducible build configuration ------ +# Goal: byte-identical binaries from identical sources regardless of build +# path, build host, build user or wall-clock time. +# +# Every flag below is probed for compiler support before use, so this block +# is a safe no-op on older toolchains (VeraCrypt still supports GCC 4.x) and +# never breaks a build that would otherwise have succeeded. +# +# SOURCE_DATE_EPOCH (https://reproducible-builds.org/specs/source-date-epoch/) +# is honoured by GCC/Clang for any residual __DATE__/__TIME__ expansion, by +# ar/ranlib in deterministic mode, and by tar, gzip and makeself for archive +# member timestamps. If the caller does not set it, derive a stable value +# from the HEAD commit; fall back to a fixed constant for tarball builds with +# no git tree so that unattended builds are still deterministic. +ifndef SOURCE_DATE_EPOCH +export SOURCE_DATE_EPOCH := $(shell git -C $(BASE_DIR) log -1 --pretty=%ct 2>/dev/null || echo 1577836800) +export VC_SOURCE_DATE_EPOCH_AUTO := 1 +endif +override export SOURCE_DATE_EPOCH := $(value SOURCE_DATE_EPOCH) +SOURCE_DATE_EPOCH_REMAINDER := $(value SOURCE_DATE_EPOCH) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 0,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 1,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 2,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 3,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 4,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 5,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 6,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 7,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 8,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 9,,$(SOURCE_DATE_EPOCH_REMAINDER)) +ifeq "$(SOURCE_DATE_EPOCH)" "" +$(error SOURCE_DATE_EPOCH must be a non-negative Unix timestamp) +endif +ifneq "$(SOURCE_DATE_EPOCH_REMAINDER)" "" +$(error SOURCE_DATE_EPOCH must contain decimal digits only) +endif + +# cc-option: return $(1) if the C compiler accepts it, empty otherwise. +cc-option = $(shell printf 'int main(void){return 0;}' | $(CC) $(1) -x c -c - -o /dev/null >/dev/null 2>&1 && echo $(1)) + +# Normalise build paths embedded in debug info, assertions and __FILE__ so +# the binary does not depend on where the source tree lives. -ffile-prefix-map +# needs GCC >= 8 / Clang >= 10; fall back to the older -fdebug-prefix-map +# (GCC >= 4.3) which at least normalises the path recorded in debug info. +# Linux-only block: out of scope for FreeBSD/macOS. $(abspath) because +# -ffile-prefix-map needs an absolute prefix to match. +ifeq "$(PLATFORM)" "Linux" +REPRO_BASE_DIR := $(abspath $(BASE_DIR)) +REPRODUCIBLE_FLAGS := $(call cc-option,-ffile-prefix-map=$(REPRO_BASE_DIR)=.) +ifeq "$(REPRODUCIBLE_FLAGS)" "" +REPRODUCIBLE_FLAGS := $(call cc-option,-fdebug-prefix-map=$(REPRO_BASE_DIR)=.) +endif +# Drop the recorded compiler command line, which embeds absolute paths and +# host-specific options into .comment / .GCC.command.line. +REPRODUCIBLE_FLAGS += $(call cc-option,-fno-record-gcc-switches) + +CFLAGS += $(REPRODUCIBLE_FLAGS) +CXXFLAGS += $(REPRODUCIBLE_FLAGS) +WXCONFIG_CFLAGS += $(REPRODUCIBLE_FLAGS) +WXCONFIG_CXXFLAGS += $(REPRODUCIBLE_FLAGS) + +# Deterministic linking: pin the GNU build-id to a stable hash of the output +# instead of letting it vary with non-deterministic linker inputs. Only added +# when the linker actually accepts it. +REPRODUCIBLE_LFLAGS := $(shell printf 'int main(void){return 0;}' | $(CC) -Wl,--build-id=sha1 -x c - -o /dev/null >/dev/null 2>&1 && echo -Wl,--build-id=sha1) +LFLAGS += $(REPRODUCIBLE_LFLAGS) +endif + + WX_CONFIGURE_FLAGS += -disable-shared --disable-dependency-tracking --enable-exceptions --enable-dataobj --enable-mimetype ifdef VC_WX_MINIMAL