返回 DeepSeek-Reasonix
package-windows-desktop.sh
根目录 / scripts / package-windows-desktop.sh
1 #!/usr/bin/env bash
2 # Rebuild the Windows portable archive and NSIS installer from one canonical
3 # payload directory: the flat Go executables plus the Electron app/ tree. The
4 # release workflow calls this once with unsigned files after compilation, then
5 # again with Authenticode-signed files returned by SignPath. Re-running makensis
6 # after payload signing is what makes the installed executables signed too;
7 # signing only the finished NSIS file signs the container, not the files that
8 # Defender scans after installation.
9 set -euo pipefail
10
11 arch="${1:?usage: package-windows-desktop.sh <amd64|arm64> <payload-dir>}"
12 payload_input="${2:?usage: package-windows-desktop.sh <amd64|arm64> <payload-dir>}"
13
14 case "$arch" in
15 amd64 | arm64) ;;
16 *)
17 echo "unsupported Windows architecture: $arch" >&2
18 exit 1
19 ;;
20 esac
21
22 ROOT="$(cd "$(dirname "$0")/.." && pwd)"
23 DESKTOP="$ROOT/desktop"
24 INSTALLER_DIR="$DESKTOP/build/windows/installer"
25 BIN_DIR="$DESKTOP/build/bin"
26 DIST="$ROOT/dist"
27 APPNAME="Reasonix"
28 BINNAME="reasonix-desktop"
29 GUARDNAME="reasonix-guard"
30 LAUNCHERNAME="reasonix-launcher"
31 UPDATE_HELPER="reasonix-update-helper.exe"
32 WINDOWS_CLINAME="reasonix-cli"
33 WINDOWS_CLI_ENTRY="reasonix-cli-launcher.exe"
34 SIGNING_LIST="signing-files.txt"
35 PAYLOAD_MANIFEST="reasonix-payload.json"
36 PAYLOAD_SIGNATURE="$PAYLOAD_MANIFEST.minisig"
37
38 [ -d "$payload_input" ] || { echo "Windows payload directory is missing: $payload_input" >&2; exit 1; }
39 PAYLOAD="$(cd "$payload_input" && pwd)"
40
41 required_payload=(
42 "$BINNAME.exe"
43 "$GUARDNAME.exe"
44 "$LAUNCHERNAME.exe"
45 "$UPDATE_HELPER"
46 "$WINDOWS_CLINAME.exe"
47 "reasonix-uninstall.exe"
48 )
49 for name in "${required_payload[@]}"; do
50 [ -s "$PAYLOAD/$name" ] || { echo "Windows payload file is missing or empty: $name" >&2; exit 1; }
51 done
52
53 payload_exe_count=$(find "$PAYLOAD" -maxdepth 1 -type f -iname '*.exe' | wc -l | tr -d '[:space:]')
54 [ "$payload_exe_count" = "${#required_payload[@]}" ] || {
55 echo "Windows payload must contain exactly ${#required_payload[@]} flat executables, found $payload_exe_count" >&2
56 exit 1
57 }
58
59 # The Electron tree is part of the release unit; signing-files.txt (written by
60 # desktop/packaging/signing-files.mjs) enumerates every PE file inside it, so
61 # --check fails closed when the tree and the signing list drift apart.
62 [ -s "$PAYLOAD/$SIGNING_LIST" ] || { echo "Windows payload signing list is missing: $SIGNING_LIST" >&2; exit 1; }
63 node "$DESKTOP/packaging/signing-files.mjs" "$PAYLOAD" --check
64
65 manifest_present=0
66 signature_present=0
67 [ -s "$PAYLOAD/$PAYLOAD_MANIFEST" ] && manifest_present=1
68 [ -s "$PAYLOAD/$PAYLOAD_SIGNATURE" ] && signature_present=1
69 if [ "$manifest_present" != "$signature_present" ]; then
70 echo "Windows payload manifest and signature must be provided together" >&2
71 exit 1
72 fi
73 if [ "${REASONIX_REQUIRE_PAYLOAD_MANIFEST:-0}" = "1" ] && [ "$manifest_present" != "1" ]; then
74 echo "signed Windows packaging requires $PAYLOAD_MANIFEST and $PAYLOAD_SIGNATURE" >&2
75 exit 1
76 fi
77
78 # Replace every source consumed by project.nsi before compiling the installer.
79 # Copying preserves the Authenticode certificate table returned by SignPath.
80 cp "$PAYLOAD/$BINNAME.exe" "$INSTALLER_DIR/$BINNAME.exe"
81 cp "$PAYLOAD/$GUARDNAME.exe" "$INSTALLER_DIR/$GUARDNAME.exe"
82 cp "$PAYLOAD/$LAUNCHERNAME.exe" "$INSTALLER_DIR/$LAUNCHERNAME.exe"
83 cp "$PAYLOAD/$UPDATE_HELPER" "$INSTALLER_DIR/$UPDATE_HELPER"
84 cp "$PAYLOAD/$WINDOWS_CLINAME.exe" "$INSTALLER_DIR/$WINDOWS_CLINAME.exe"
85 rm -rf -- "$INSTALLER_DIR/app"
86 cp -R "$PAYLOAD/app" "$INSTALLER_DIR/app"
87 rm -f -- "$INSTALLER_DIR/$PAYLOAD_MANIFEST" "$INSTALLER_DIR/$PAYLOAD_SIGNATURE"
88 if [ "$manifest_present" = "1" ]; then
89 cp "$PAYLOAD/$PAYLOAD_MANIFEST" "$INSTALLER_DIR/$PAYLOAD_MANIFEST"
90 cp "$PAYLOAD/$PAYLOAD_SIGNATURE" "$INSTALLER_DIR/$PAYLOAD_SIGNATURE"
91 fi
92
93 [ -s "$INSTALLER_DIR/reasonix_project.nsh" ] || {
94 echo "reasonix_project.nsh is missing; run desktop/packaging/package.mjs first" >&2
95 exit 1
96 }
97
98 # Delete only generated installers so a stale first-pass package cannot be
99 # mistaken for the rebuilt payload-signed installer.
100 mkdir -p "$BIN_DIR"
101 find "$BIN_DIR" -maxdepth 1 -type f -name '*installer*.exe' -delete
102 binary_define="ARG_REASONIX_AMD64_BINARY"
103 [ "$arch" = arm64 ] && binary_define="ARG_REASONIX_ARM64_BINARY"
104 binary_path="$INSTALLER_DIR/$BINNAME.exe"
105 uninstaller_path="$PAYLOAD/reasonix-uninstall.exe"
106 if command -v cygpath >/dev/null 2>&1; then
107 binary_path="$(cygpath -w "$binary_path")"
108 uninstaller_path="$(cygpath -w "$uninstaller_path")"
109 fi
110 (
111 cd "$INSTALLER_DIR"
112 makensis \
113 "-D${binary_define}=${binary_path}" \
114 "-DARG_REASONIX_SIGNED_UNINSTALLER=${uninstaller_path}" \
115 project.nsi
116 )
117
118 installer=$(find "$BIN_DIR" -maxdepth 1 -type f -name '*installer*.exe' -print -quit)
119 [ -n "$installer" ] && [ -s "$installer" ] || { echo "makensis did not produce a Windows installer" >&2; exit 1; }
120
121 mkdir -p "$DIST"
122 dist_installer="$DIST/${APPNAME}-windows-${arch}-installer.exe"
123 dist_portable="$DIST/${APPNAME}-windows-${arch}.zip"
124 cp "$installer" "$dist_installer"
125
126 portable_staging=$(mktemp -d)
127 cleanup() {
128 tmp_root="${TMPDIR:-/tmp}"
129 tmp_root="${tmp_root%/}"
130 case "$portable_staging" in
131 "$tmp_root"/* | /tmp/*) rm -rf -- "$portable_staging" ;;
132 *) echo "refusing to clean unexpected portable staging directory: $portable_staging" >&2 ;;
133 esac
134 }
135 trap cleanup EXIT
136
137 # versioned-v1 portable layout (no Guard, no flat desktop at InstallRoot); the
138 # Electron bundle is the app/ tree member of the active version directory.
139 version_label="${VERSION:-}"
140 if [ -z "$version_label" ] && [ -f "$INSTALLER_DIR/reasonix_project.nsh" ]; then
141 version_label=$(sed -n 's/^!define REASONIX_VERSION_TAG "\(.*\)"$/\1/p' "$INSTALLER_DIR/reasonix_project.nsh" | tr -d '\r' | head -n 1)
142 fi
143 version_label="${version_label:-0.0.0}"
144 case "$version_label" in
145 v*) ;;
146 *) version_label="v${version_label}" ;;
147 esac
148 mkdir -p "$portable_staging/versions/$version_label"
149 cp "$PAYLOAD/$BINNAME.exe" "$portable_staging/versions/$version_label/$BINNAME.exe"
150 cp "$PAYLOAD/$UPDATE_HELPER" "$portable_staging/versions/$version_label/$UPDATE_HELPER"
151 cp "$PAYLOAD/$WINDOWS_CLINAME.exe" "$portable_staging/versions/$version_label/$WINDOWS_CLINAME.exe"
152 cp -R "$PAYLOAD/app" "$portable_staging/versions/$version_label/app"
153 cp "$PAYLOAD/$LAUNCHERNAME.exe" "$portable_staging/$APPNAME.exe"
154 cli_entry="$PAYLOAD/app/resources/bin/$WINDOWS_CLI_ENTRY"
155 [ -s "$cli_entry" ] || { echo "Windows CLI entry is missing: $cli_entry" >&2; exit 1; }
156 cp "$cli_entry" "$portable_staging/$WINDOWS_CLINAME.exe"
157 cat >"$portable_staging/current.json" <<EOF
158 {
159 "schemaVersion": 1,
160 "activeVersion": "$version_label",
161 "activeDir": "versions/$version_label"
162 }
163 EOF
164 "$ROOT/scripts/verify-windows-portable.sh" "$portable_staging" canonical "$PAYLOAD/$LAUNCHERNAME.exe"
165
166 if command -v powershell.exe >/dev/null 2>&1; then
167 portable_staging_win="$portable_staging"
168 dist_portable_win="$dist_portable"
169 if command -v cygpath >/dev/null 2>&1; then
170 portable_staging_win="$(cygpath -w "$portable_staging")"
171 dist_portable_win="$(cygpath -w "$dist_portable")"
172 fi
173 powershell.exe -NoProfile -Command \
174 "Compress-Archive -CompressionLevel Optimal -Force -Path '$portable_staging_win\\*' -DestinationPath '$dist_portable_win'"
175 elif command -v zip >/dev/null 2>&1; then
176 # macOS/Linux cross-builds do not ship powershell.exe; the portable layout
177 # is ordinary ZIP data, so use the host zip utility in that case.
178 # zip updates an existing archive and otherwise retains previous version
179 # directories. Always assemble a fresh distributable from this payload.
180 rm -f -- "$dist_portable"
181 (
182 cd "$portable_staging"
183 zip -q -9 -r "$dist_portable" .
184 )
185 else
186 echo "neither powershell.exe nor zip is available to create the Windows portable archive" >&2
187 exit 1
188 fi
189
190 # The second SignPath request signs the outer installer only after verifying
191 # these already-signed payload files (flat executables plus the app/ tree).
192 # Keeping one exact bundle makes the artifact configuration fail closed if a
193 # required installed executable is missing.
194 installer_bundle="$DESKTOP/build/windows/installer-signing-bundle"
195 rm -rf -- "$installer_bundle"
196 mkdir -p "$installer_bundle"
197 cp "$dist_installer" "$installer_bundle/"
198 for name in "${required_payload[@]}"; do
199 cp "$PAYLOAD/$name" "$installer_bundle/$name"
200 done
201 cp -R "$PAYLOAD/app" "$installer_bundle/app"
202 cp "$PAYLOAD/$SIGNING_LIST" "$installer_bundle/$SIGNING_LIST"
203
204 echo "==> rebuilt Windows $arch installer and portable archive from $PAYLOAD"
205
205 lines BASH