返回 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 release workflow calls this once with unsigned files
4 # after compilation, then again with Authenticode-signed files returned by
5 # SignPath. Re-running makensis after payload signing is what makes the
6 # installed executables signed too; signing only the finished NSIS file signs
7 # the container, not the files that Defender scans after installation.
8 set -euo pipefail
9
10 arch="${1:?usage: package-windows-desktop.sh <amd64|arm64> <payload-dir>}"
11 payload_input="${2:?usage: package-windows-desktop.sh <amd64|arm64> <payload-dir>}"
12
13 case "$arch" in
14 amd64 | arm64) ;;
15 *)
16 echo "unsupported Windows architecture: $arch" >&2
17 exit 1
18 ;;
19 esac
20
21 ROOT="$(cd "$(dirname "$0")/.." && pwd)"
22 DESKTOP="$ROOT/desktop"
23 INSTALLER_DIR="$DESKTOP/build/windows/installer"
24 BIN_DIR="$DESKTOP/build/bin"
25 DIST="$ROOT/dist"
26 APPNAME="Reasonix"
27 BINNAME="reasonix-desktop"
28 GUARDNAME="reasonix-guard"
29 LAUNCHERNAME="reasonix-launcher"
30 UPDATE_HELPER="reasonix-update-helper.exe"
31 WINDOWS_CLINAME="reasonix-cli"
32 PAYLOAD_MANIFEST="reasonix-payload.json"
33 PAYLOAD_SIGNATURE="$PAYLOAD_MANIFEST.minisig"
34
35 [ -d "$payload_input" ] || { echo "Windows payload directory is missing: $payload_input" >&2; exit 1; }
36 PAYLOAD="$(cd "$payload_input" && pwd)"
37
38 required_payload=(
39 "$BINNAME.exe"
40 "$GUARDNAME.exe"
41 "$LAUNCHERNAME.exe"
42 "$UPDATE_HELPER"
43 "$WINDOWS_CLINAME.exe"
44 "reasonix-uninstall.exe"
45 )
46 for name in "${required_payload[@]}"; do
47 [ -s "$PAYLOAD/$name" ] || { echo "Windows payload file is missing or empty: $name" >&2; exit 1; }
48 done
49
50 payload_exe_count=$(find "$PAYLOAD" -maxdepth 1 -type f -iname '*.exe' | wc -l | tr -d '[:space:]')
51 [ "$payload_exe_count" = "${#required_payload[@]}" ] || {
52 echo "Windows payload must contain exactly ${#required_payload[@]} executables, found $payload_exe_count" >&2
53 exit 1
54 }
55
56 manifest_present=0
57 signature_present=0
58 [ -s "$PAYLOAD/$PAYLOAD_MANIFEST" ] && manifest_present=1
59 [ -s "$PAYLOAD/$PAYLOAD_SIGNATURE" ] && signature_present=1
60 if [ "$manifest_present" != "$signature_present" ]; then
61 echo "Windows payload manifest and signature must be provided together" >&2
62 exit 1
63 fi
64 if [ "${REASONIX_REQUIRE_PAYLOAD_MANIFEST:-0}" = "1" ] && [ "$manifest_present" != "1" ]; then
65 echo "signed Windows packaging requires $PAYLOAD_MANIFEST and $PAYLOAD_SIGNATURE" >&2
66 exit 1
67 fi
68
69 # Replace every source consumed by project.nsi before compiling the installer.
70 # Copying preserves the Authenticode certificate table returned by SignPath.
71 cp "$PAYLOAD/$BINNAME.exe" "$BIN_DIR/$BINNAME.exe"
72 cp "$PAYLOAD/$GUARDNAME.exe" "$INSTALLER_DIR/$GUARDNAME.exe"
73 cp "$PAYLOAD/$LAUNCHERNAME.exe" "$INSTALLER_DIR/$LAUNCHERNAME.exe"
74 cp "$PAYLOAD/$UPDATE_HELPER" "$INSTALLER_DIR/$UPDATE_HELPER"
75 cp "$PAYLOAD/$WINDOWS_CLINAME.exe" "$INSTALLER_DIR/$WINDOWS_CLINAME.exe"
76 rm -f -- "$INSTALLER_DIR/$PAYLOAD_MANIFEST" "$INSTALLER_DIR/$PAYLOAD_SIGNATURE"
77 if [ "$manifest_present" = "1" ]; then
78 cp "$PAYLOAD/$PAYLOAD_MANIFEST" "$INSTALLER_DIR/$PAYLOAD_MANIFEST"
79 cp "$PAYLOAD/$PAYLOAD_SIGNATURE" "$INSTALLER_DIR/$PAYLOAD_SIGNATURE"
80 fi
81
82 [ -s "$INSTALLER_DIR/wails_tools.nsh" ] || {
83 echo "wails_tools.nsh is missing; run the initial Wails -nsis build first" >&2
84 exit 1
85 }
86 [ -s "$INSTALLER_DIR/tmp/MicrosoftEdgeWebview2Setup.exe" ] || {
87 echo "embedded WebView2 bootstrapper is missing; run the initial Wails -nsis build first" >&2
88 exit 1
89 }
90
91 # Wails documents project.nsi as manually invokable with the architecture
92 # binary define. Delete only generated installers so a stale first-pass package
93 # cannot be mistaken for the rebuilt payload-signed installer.
94 find "$BIN_DIR" -maxdepth 1 -type f -name '*installer*.exe' -delete
95 binary_path="$BIN_DIR/$BINNAME.exe"
96 if command -v cygpath >/dev/null 2>&1; then
97 binary_path="$(cygpath -w "$binary_path")"
98 fi
99 binary_define="ARG_WAILS_AMD64_BINARY"
100 [ "$arch" = arm64 ] && binary_define="ARG_WAILS_ARM64_BINARY"
101 uninstaller_path="$PAYLOAD/reasonix-uninstall.exe"
102 if command -v cygpath >/dev/null 2>&1; then
103 uninstaller_path="$(cygpath -w "$uninstaller_path")"
104 fi
105 (
106 cd "$INSTALLER_DIR"
107 makensis \
108 "-D${binary_define}=${binary_path}" \
109 "-DARG_REASONIX_SIGNED_UNINSTALLER=${uninstaller_path}" \
110 project.nsi
111 )
112
113 installer=$(find "$BIN_DIR" -maxdepth 1 -type f -name '*installer*.exe' -print -quit)
114 [ -n "$installer" ] && [ -s "$installer" ] || { echo "makensis did not produce a Windows installer" >&2; exit 1; }
115
116 mkdir -p "$DIST"
117 dist_installer="$DIST/${APPNAME}-windows-${arch}-installer.exe"
118 dist_portable="$DIST/${APPNAME}-windows-${arch}.zip"
119 cp "$installer" "$dist_installer"
120
121 portable_staging=$(mktemp -d)
122 cleanup() {
123 case "$portable_staging" in
124 "${TMPDIR:-/tmp}"/* | /tmp/*) rm -rf -- "$portable_staging" ;;
125 *) echo "refusing to clean unexpected portable staging directory: $portable_staging" >&2 ;;
126 esac
127 }
128 trap cleanup EXIT
129
130 # versioned-v1 portable layout (no Guard, no flat desktop at InstallRoot).
131 version_label="${VERSION:-}"
132 if [ -z "$version_label" ] && [ -f "$DESKTOP/wails.json" ]; then
133 version_label=$(node -e 'const j=require(process.argv[1]); process.stdout.write(j.info&&j.info.productVersion||"")' "$DESKTOP/wails.json" 2>/dev/null || true)
134 fi
135 version_label="${version_label:-0.0.0}"
136 case "$version_label" in
137 v*) ;;
138 *) version_label="v${version_label}" ;;
139 esac
140 mkdir -p "$portable_staging/versions/$version_label"
141 cp "$PAYLOAD/$BINNAME.exe" "$portable_staging/versions/$version_label/$BINNAME.exe"
142 cp "$PAYLOAD/$UPDATE_HELPER" "$portable_staging/versions/$version_label/$UPDATE_HELPER"
143 cp "$PAYLOAD/$WINDOWS_CLINAME.exe" "$portable_staging/versions/$version_label/$WINDOWS_CLINAME.exe"
144 cp "$PAYLOAD/$LAUNCHERNAME.exe" "$portable_staging/$LAUNCHERNAME.exe"
145 cp "$PAYLOAD/$LAUNCHERNAME.exe" "$portable_staging/$APPNAME.exe"
146 cp "$PAYLOAD/$WINDOWS_CLINAME.exe" "$portable_staging/$WINDOWS_CLINAME.exe"
147 cat >"$portable_staging/current.json" <<EOF
148 {
149 "schemaVersion": 1,
150 "activeVersion": "$version_label",
151 "activeDir": "versions/$version_label"
152 }
153 EOF
154 "$ROOT/scripts/verify-windows-portable.sh" "$portable_staging"
155
156 portable_staging_win="$portable_staging"
157 dist_portable_win="$dist_portable"
158 if command -v cygpath >/dev/null 2>&1; then
159 portable_staging_win="$(cygpath -w "$portable_staging")"
160 dist_portable_win="$(cygpath -w "$dist_portable")"
161 fi
162 powershell.exe -NoProfile -Command \
163 "Compress-Archive -Force -Path '$portable_staging_win\\*' -DestinationPath '$dist_portable_win'"
164
165 # The second SignPath request signs the outer installer only after verifying
166 # these already-signed payload files. Keeping one flat, exact bundle makes the
167 # artifact configuration fail closed if a required installed executable is
168 # missing.
169 installer_bundle="$DESKTOP/build/windows/installer-signing-bundle"
170 rm -rf -- "$installer_bundle"
171 mkdir -p "$installer_bundle"
172 cp "$dist_installer" "$installer_bundle/"
173 for name in "${required_payload[@]}"; do
174 cp "$PAYLOAD/$name" "$installer_bundle/$name"
175 done
176
177 echo "==> rebuilt Windows $arch installer and portable archive from $PAYLOAD"
178
178 lines BASH