From 41c459e4c3edfd4d536a31b2611f8f4e97ee1344 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Tue, 21 Jul 2026 16:37:56 +0200 Subject: [PATCH 1/3] Made Windows MSI assembly fully reproducible The issue was that wixl bakes a random package code, wall-clock summary-info timestamps, and per-file CAB timestamps into the MSI. Furthermore, the wxs file used a random ProductCode. Hence, two builds of identical source produced different MSIs. Fixed it by deriving the ProductCode and package code deterministically from the version, pinning the CAB file mtimes and summary-info to SOURCE_DATE_EPOCH. Ticket: ENT-13792 Signed-off-by: Lars Erik Wik --- build-scripts/package-msi | 43 +++++++++++++++++++++++ container/Dockerfile.mingw | 2 +- packaging/cfengine-nova/cfengine-nova.wxs | 4 ++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/build-scripts/package-msi b/build-scripts/package-msi index 4f009c5ab..57bda1aa5 100755 --- a/build-scripts/package-msi +++ b/build-scripts/package-msi @@ -13,6 +13,36 @@ # MSI packaging uses wixl (GNOME msitools), which builds Windows MSIs natively # on Linux - no Windows and no Wine. +# Derive a deterministic GUID from a label (ENT-13792) +deterministic_guid() { + printf '{%s}\n' "$(uuidgen --sha1 \ + --namespace B883FBCC-6F05-4AFA-98FA-CAF09BF464EA \ + --name "$1" | tr '[:lower:]' '[:upper:]')" +} + +# Make the Summary Information reproducible: wixl writes a random package code +# (property 9) and the wall-clock build time (12/13 = Created/Last saved). +# Rewrite them to a version-derived package code and SOURCE_DATE_EPOCH. +# (ENT-13792) +normalize_msi_summary() { + msi="$1" + revision="$2" + if [ -z "$SOURCE_DATE_EPOCH" ]; then + log_debug "SOURCE_DATE_EPOCH unset; skipping MSI summary normalization" + return 0 + fi + pkgcode=$(deterministic_guid "package:$revision") + datestr=$(TZ=UTC date -u -d "@$SOURCE_DATE_EPOCH" +"%Y/%m/%d %H:%M:%S") + msiinfo export "$msi" _SummaryInformation | tr -d '\r' | + awk -v u="$pkgcode" -v d="$datestr" -F '\t' 'BEGIN { OFS = "\t" } + $1 == "9" { $2 = u } + $1 == "12" { $2 = d } + $1 == "13" { $2 = d } + { print }' | sed 's/$/\r/' >_summary.idt + msibuild "$msi" -i _summary.idt + rm -f _summary.idt +} + # Determine build directory name based on Jenkins job or version/arch log_debug "Determining build directory name (JOB_NAME=$JOB_NAME, VERSION=$VERSION, ARCH=$ARCH)" if [ -z "$JOB_NAME" ]; then @@ -85,6 +115,13 @@ pre() { if [ "$ARCH" = "x86" ]; then sed -i '/lib\(crypto\|ssl\)/s/[_-]x64//g' "$P"/cfengine-nova.wxs fi + + # Reproducible builds (ENT-13792): pin every packaged file's mtime to + # SOURCE_DATE_EPOCH so the CAB stores a fixed timestamp per file instead + # of the time each file was created. + if [ -n "$SOURCE_DATE_EPOCH" ]; then + find "$P" -exec touch -h -d "@$SOURCE_DATE_EPOCH" {} + + fi } # wixl_build() - Build the MSI from the WiX source using wixl (msitools). @@ -108,7 +145,13 @@ wixl_build() { log_debug "Running wixl with REVISION=$REVISION, ARCH=$ARCH" wixl -a "$wixl_arch" \ -D CfSourceDir=. -D CfVersion="$REVISION" -D CfArch="$ARCH" \ + -D CfProductCode="$(deterministic_guid "product:$REVISION")" \ -o cfengine-nova.msi cfengine-nova.wxs + + # wixl still bakes a random package code and the wall-clock build time into + # the Summary Information; rewrite them for a byte-reproducible MSI. + # (ENT-13792) + normalize_msi_summary cfengine-nova.msi "$REVISION" } # package() - Main packaging function that creates the MSI installer diff --git a/container/Dockerfile.mingw b/container/Dockerfile.mingw index f18022ae5..a8b9faec9 100644 --- a/container/Dockerfile.mingw +++ b/container/Dockerfile.mingw @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y \ autoconf automake binutils bison build-essential curl debhelper dpkg-dev \ expat fakeroot flex gdb git libexpat1-dev libmodule-load-conditional-perl \ libpam0g-dev libtool libncurses6 libncurses-dev pkg-config psmisc \ - python3-pip rsync sudo systemd-coredump unzip wget \ + python3-pip rsync sudo systemd-coredump unzip uuid-runtime wget \ && rm -rf /var/lib/apt/lists/* # MinGW-w64 cross toolchain. diff --git a/packaging/cfengine-nova/cfengine-nova.wxs b/packaging/cfengine-nova/cfengine-nova.wxs index 4ea11c506..3cc8d660f 100644 --- a/packaging/cfengine-nova/cfengine-nova.wxs +++ b/packaging/cfengine-nova/cfengine-nova.wxs @@ -26,7 +26,9 @@ - + Date: Tue, 21 Jul 2026 17:17:04 +0200 Subject: [PATCH 2/3] Install uuid-runtime on MinGW build hosts build-scripts/package-msi now uses uuidgen for deterministic MSI GUIDs. Ticket: ENT-13792 Signed-off-by: Lars Erik Wik --- ci/cfengine-build-host-setup.cf | 4 ++++ ci/fix-buildhost.sh | 11 ++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ci/cfengine-build-host-setup.cf b/ci/cfengine-build-host-setup.cf index 53c880a41..0770bf875 100644 --- a/ci/cfengine-build-host-setup.cf +++ b/ci/cfengine-build-host-setup.cf @@ -91,6 +91,10 @@ bundle agent cfengine_build_host_setup "wixl"; "msitools"; + # build-scripts/package-msi derives deterministic MSI GUIDs with uuidgen + # (uuid-runtime) for reproducible builds. See ENT-13792. + "uuid-runtime"; + "binfmt-support" comment => "update-binfmts command needed for build-scripts/package-msi script"; diff --git a/ci/fix-buildhost.sh b/ci/fix-buildhost.sh index a94f8b77d..beea92f0e 100755 --- a/ci/fix-buildhost.sh +++ b/ci/fix-buildhost.sh @@ -75,12 +75,13 @@ if [ -f /etc/os-release ]; then fi # MinGW hosts build the MSI with wixl (build-scripts/package-msi) and inspect it -# with msiinfo (msitools). Installed by the build-host-setup policy at image -# time; install here too so not-yet-reimaged mingw hosts get them without a -# reimage. See ENT-13868. +# with msiinfo (msitools). uuidgen (uuid-runtime) derives deterministic MSI +# GUIDs for reproducible builds (ENT-13792). Installed by the build-host-setup +# policy at image time; install here too so not-yet-reimaged mingw hosts get +# them without a reimage. See ENT-13868. if [ -f /etc/cfengine-mingw-build-host.flag ]; then - if ! command -v wixl >/dev/null 2>&1 || ! command -v msiinfo >/dev/null 2>&1; then + if ! command -v wixl >/dev/null 2>&1 || ! command -v msiinfo >/dev/null 2>&1 || ! command -v uuidgen >/dev/null 2>&1; then sudo apt-get update - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y wixl msitools + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y wixl msitools uuid-runtime fi fi From cd48447038888b56dd9fc8e4ccaf81df2d3439b6 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 22 Jul 2026 11:47:15 +0200 Subject: [PATCH 3/3] cfengine-nova.wxs: sequence RemoveExistingProducts after InstallFinalize Early placement deleted equal-versioned shared DLLs on upgrade, leaving cf-agent unable to load. Installing new files before removing the old product keeps them via refcounting. Ref: https://learn.microsoft.com/en-us/windows/win32/msi/removeexistingproducts-action Signed-off-by: Lars Erik Wik --- packaging/cfengine-nova/cfengine-nova.wxs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packaging/cfengine-nova/cfengine-nova.wxs b/packaging/cfengine-nova/cfengine-nova.wxs index 3cc8d660f..19129f7b5 100644 --- a/packaging/cfengine-nova/cfengine-nova.wxs +++ b/packaging/cfengine-nova/cfengine-nova.wxs @@ -264,11 +264,16 @@ - NOT Installed + + +