Skip to content

Commit d9dc8f6

Browse files
authored
Merge pull request #2366 from larsewi/reproducible-msi-builds
ENT-13792: Made Windows MSI assembly fully reproducible
2 parents 472b7e0 + cd48447 commit d9dc8f6

5 files changed

Lines changed: 63 additions & 8 deletions

File tree

build-scripts/package-msi

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,36 @@
1313
# MSI packaging uses wixl (GNOME msitools), which builds Windows MSIs natively
1414
# on Linux - no Windows and no Wine.
1515

16+
# Derive a deterministic GUID from a label (ENT-13792)
17+
deterministic_guid() {
18+
printf '{%s}\n' "$(uuidgen --sha1 \
19+
--namespace B883FBCC-6F05-4AFA-98FA-CAF09BF464EA \
20+
--name "$1" | tr '[:lower:]' '[:upper:]')"
21+
}
22+
23+
# Make the Summary Information reproducible: wixl writes a random package code
24+
# (property 9) and the wall-clock build time (12/13 = Created/Last saved).
25+
# Rewrite them to a version-derived package code and SOURCE_DATE_EPOCH.
26+
# (ENT-13792)
27+
normalize_msi_summary() {
28+
msi="$1"
29+
revision="$2"
30+
if [ -z "$SOURCE_DATE_EPOCH" ]; then
31+
log_debug "SOURCE_DATE_EPOCH unset; skipping MSI summary normalization"
32+
return 0
33+
fi
34+
pkgcode=$(deterministic_guid "package:$revision")
35+
datestr=$(TZ=UTC date -u -d "@$SOURCE_DATE_EPOCH" +"%Y/%m/%d %H:%M:%S")
36+
msiinfo export "$msi" _SummaryInformation | tr -d '\r' |
37+
awk -v u="$pkgcode" -v d="$datestr" -F '\t' 'BEGIN { OFS = "\t" }
38+
$1 == "9" { $2 = u }
39+
$1 == "12" { $2 = d }
40+
$1 == "13" { $2 = d }
41+
{ print }' | sed 's/$/\r/' >_summary.idt
42+
msibuild "$msi" -i _summary.idt
43+
rm -f _summary.idt
44+
}
45+
1646
# Determine build directory name based on Jenkins job or version/arch
1747
log_debug "Determining build directory name (JOB_NAME=$JOB_NAME, VERSION=$VERSION, ARCH=$ARCH)"
1848
if [ -z "$JOB_NAME" ]; then
@@ -85,6 +115,13 @@ pre() {
85115
if [ "$ARCH" = "x86" ]; then
86116
sed -i '/lib\(crypto\|ssl\)/s/[_-]x64//g' "$P"/cfengine-nova.wxs
87117
fi
118+
119+
# Reproducible builds (ENT-13792): pin every packaged file's mtime to
120+
# SOURCE_DATE_EPOCH so the CAB stores a fixed timestamp per file instead
121+
# of the time each file was created.
122+
if [ -n "$SOURCE_DATE_EPOCH" ]; then
123+
find "$P" -exec touch -h -d "@$SOURCE_DATE_EPOCH" {} +
124+
fi
88125
}
89126

90127
# wixl_build() - Build the MSI from the WiX source using wixl (msitools).
@@ -108,7 +145,13 @@ wixl_build() {
108145
log_debug "Running wixl with REVISION=$REVISION, ARCH=$ARCH"
109146
wixl -a "$wixl_arch" \
110147
-D CfSourceDir=. -D CfVersion="$REVISION" -D CfArch="$ARCH" \
148+
-D CfProductCode="$(deterministic_guid "product:$REVISION")" \
111149
-o cfengine-nova.msi cfengine-nova.wxs
150+
151+
# wixl still bakes a random package code and the wall-clock build time into
152+
# the Summary Information; rewrite them for a byte-reproducible MSI.
153+
# (ENT-13792)
154+
normalize_msi_summary cfengine-nova.msi "$REVISION"
112155
}
113156

114157
# package() - Main packaging function that creates the MSI installer

ci/cfengine-build-host-setup.cf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ bundle agent cfengine_build_host_setup
9191
"wixl";
9292
"msitools";
9393

94+
# build-scripts/package-msi derives deterministic MSI GUIDs with uuidgen
95+
# (uuid-runtime) for reproducible builds. See ENT-13792.
96+
"uuid-runtime";
97+
9498
"binfmt-support"
9599
comment => "update-binfmts command needed for build-scripts/package-msi script";
96100

ci/fix-buildhost.sh

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,13 @@ if [ -f /etc/os-release ]; then
7575
fi
7676

7777
# MinGW hosts build the MSI with wixl (build-scripts/package-msi) and inspect it
78-
# with msiinfo (msitools). Installed by the build-host-setup policy at image
79-
# time; install here too so not-yet-reimaged mingw hosts get them without a
80-
# reimage. See ENT-13868.
78+
# with msiinfo (msitools). uuidgen (uuid-runtime) derives deterministic MSI
79+
# GUIDs for reproducible builds (ENT-13792). Installed by the build-host-setup
80+
# policy at image time; install here too so not-yet-reimaged mingw hosts get
81+
# them without a reimage. See ENT-13868.
8182
if [ -f /etc/cfengine-mingw-build-host.flag ]; then
82-
if ! command -v wixl >/dev/null 2>&1 || ! command -v msiinfo >/dev/null 2>&1; then
83+
if ! command -v wixl >/dev/null 2>&1 || ! command -v msiinfo >/dev/null 2>&1 || ! command -v uuidgen >/dev/null 2>&1; then
8384
sudo apt-get update
84-
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y wixl msitools
85+
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y wixl msitools uuid-runtime
8586
fi
8687
fi

container/Dockerfile.mingw

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y \
88
autoconf automake binutils bison build-essential curl debhelper dpkg-dev \
99
expat fakeroot flex gdb git libexpat1-dev libmodule-load-conditional-perl \
1010
libpam0g-dev libtool libncurses6 libncurses-dev pkg-config psmisc \
11-
python3-pip rsync sudo systemd-coredump unzip wget \
11+
python3-pip rsync sudo systemd-coredump unzip uuid-runtime wget \
1212
&& rm -rf /var/lib/apt/lists/*
1313

1414
# MinGW-w64 cross toolchain.

packaging/cfengine-nova/cfengine-nova.wxs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
<?endif?>
2727
<?endif?>
2828

29-
<Product Id='*' Name='CFEngine Nova' Language='1033'
29+
<!-- ProductCode is pinned (not '*') so rebuilds of the same version are
30+
byte-reproducible; package-msi derives it deterministically. ENT-13792 -->
31+
<Product Id='$(var.CfProductCode)' Name='CFEngine Nova' Language='1033'
3032
Version='$(var.CfVersion)' Manufacturer='Northern.tech AS' UpgradeCode='B883FBCC-6F05-4AFA-98FA-CAF09BF464EA' >
3133

3234
<Package Description='CFEngine Nova'
@@ -262,11 +264,16 @@
262264
<!-- The sequence numbers are weird to avoid collisions with built-in numbers -->
263265

264266
<InstallExecuteSequence>
265-
<RemoveExistingProducts Sequence='1450' />
266267
<InstallInitialize Sequence='1500' />
267268
<Custom Action='GenerateKey' After='InstallFiles'>NOT Installed</Custom>
268269

269270
<InstallFinalize Sequence='6600' />
271+
272+
<!-- After InstallFinalize: install new files first, then remove the old
273+
product, so equal-versioned shared DLLs survive via refcounting. Early
274+
placement deletes them and breaks upgrades. Ref:
275+
https://learn.microsoft.com/en-us/windows/win32/msi/removeexistingproducts-action -->
276+
<RemoveExistingProducts Sequence='6700' />
270277
</InstallExecuteSequence>
271278

272279
</Product>

0 commit comments

Comments
 (0)