返回 DeepSeek-Reasonix
desktop-build.sh
根目录 / scripts / desktop-build.sh
1 #!/usr/bin/env bash
2 # Build and package the Electron desktop app for one platform. Electron's
3 # Chromium shell cannot cross-compile the native targets from one host, so this
4 # runs on a native runner per target (see .github/workflows/release-desktop.yml)
5 # and is invoked once per matrix entry.
6 #
7 # Output lands in <repo>/dist/ with stable, platform-keyed names that
8 # desktop/cmd/sign's `manifest` subcommand maps back to update.PlatformKey:
9 # macOS: Reasonix-darwin-<arm64|amd64>.zip (ditto archive; updater channel)
10 # Reasonix-darwin-<arch>.dmg (drag-to-install; human download)
11 # Windows: Reasonix-windows-<arch>-installer.exe (NSIS per-user installer; updater channel)
12 # Reasonix-windows-<arch>.zip (portable human download)
13 # Linux: Reasonix-linux-<arch>.tar.gz (desktop + guard + CLI + app/ tree; portable updater)
14 # Reasonix-linux-<arch>.deb (Debian/Ubuntu package; native updater)
15 #
16 # Usage: scripts/desktop-build.sh <os/arch> <version> [channel]
17 # e.g. scripts/desktop-build.sh darwin/arm64 v1.1.0
18 # scripts/desktop-build.sh darwin/arm64 v1.5.0-preview.42 preview
19 #
20 # Requirements:
21 # - Go toolchain matching desktop/go.mod (>= 1.25; the `toolchain` directive
22 # auto-downloads when GOTOOLCHAIN=auto)
23 # - Node >= 24 and pnpm 10 (the same major versions used by CI and releases)
24 # - A pnpm-installed desktop workspace (this script runs
25 # `pnpm --dir desktop install --frozen-lockfile` when node_modules is absent)
26 set -euo pipefail
27
28 build_started_seconds=$SECONDS
29
30 PLATFORM="${1:?usage: desktop-build.sh <os/arch> <version> [channel]}"
31 VERSION="${2:?usage: desktop-build.sh <os/arch> <version> [channel]}"
32 CHANNEL="${3:-stable}"
33
34 os="${PLATFORM%/*}"
35 arch="${PLATFORM#*/}"
36
37 ROOT="$(cd "$(dirname "$0")/.." && pwd)"
38 APPNAME="Reasonix" # Electron productName -> Reasonix.app / Reasonix.exe
39 BINNAME="reasonix-desktop" # Go desktop service (and the active version entry the launcher starts)
40 CLINAME="reasonix" # bundled CLI sidecar used for remote serve upload
41 WINDOWS_CLINAME="reasonix-cli" # Windows cannot store Reasonix.exe and reasonix.exe separately
42 WINDOWS_CLI_ENTRY="reasonix-cli-launcher.exe"
43 GUARDNAME="reasonix-guard"
44 LAUNCHERNAME="reasonix-launcher"
45 windows_resource_tool_dir=""
46 windows_host_include=""
47
48 # desktop/ is a nested Go module, so the Go toolchain cannot discover the
49 # repository VCS revision for the service binary. Link the same source identity
50 # into both Desktop and its CLI sidecar.
51 SOURCE_REVISION="$(git -C "$ROOT" rev-parse --verify HEAD)"
52 SOURCE_SHA="$SOURCE_REVISION"
53 if ! git -C "$ROOT" diff-index --quiet HEAD --; then
54 SOURCE_REVISION="$SOURCE_REVISION+dirty"
55 fi
56 # Short commit + real UTC build clock for CLI `version --verbose/--json`.
57 GIT_COMMIT="$(git -C "$ROOT" rev-parse --short=12 HEAD 2>/dev/null || echo unknown)"
58 BUILD_TIME_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
59 product_docs_ldflags="-X reasonix/internal/productdocs.linkedVersion=$VERSION -X reasonix/internal/productdocs.linkedRevision=$SOURCE_REVISION"
60 cli_identity_ldflags="-X main.version=$VERSION -X main.gitCommit=$GIT_COMMIT -X main.buildTimeUTC=$BUILD_TIME_UTC $product_docs_ldflags"
61
62 cleanup() {
63 if [ -n "$windows_resource_tool_dir" ]; then
64 rm -rf "$windows_resource_tool_dir"
65 fi
66 if [ -n "$windows_host_include" ]; then
67 rm -f "$windows_host_include"
68 fi
69 }
70 trap cleanup EXIT
71
72 cd "$ROOT/desktop"
73
74 # build_guard produces the one-shot legacy migrator still named reasonix-guard
75 # in compatibility payloads for 1.18–1.19.1 updaters. Source is intentionally
76 # separate from the removed Guard recovery product.
77 build_guard() {
78 echo "==> go build Reasonix legacy migrator (compat name reasonix-guard)"
79 mkdir -p "$(dirname "$guard_out")"
80 if [ "$arch" = universal ]; then
81 guard_tmp=$(mktemp -d)
82 (cd "$ROOT" && GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=$VERSION" -o "$guard_tmp/amd64" ./cmd/reasonix-legacy-migrator)
83 (cd "$ROOT" && GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=$VERSION" -o "$guard_tmp/arm64" ./cmd/reasonix-legacy-migrator)
84 lipo -create "$guard_tmp/amd64" "$guard_tmp/arm64" -output "$guard_out"
85 rm -rf "$guard_tmp"
86 else
87 (cd "$ROOT" && GOOS="$os" GOARCH="$arch" CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=$VERSION" -o "$guard_out" ./cmd/reasonix-legacy-migrator)
88 fi
89 }
90
91 build_cli() {
92 echo "==> go build Reasonix CLI sidecar"
93 mkdir -p "$(dirname "$cli_out")"
94 if [ "$arch" = universal ]; then
95 cli_tmp=$(mktemp -d)
96 (cd "$ROOT" && GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w $cli_identity_ldflags" -o "$cli_tmp/amd64" ./cmd/reasonix)
97 (cd "$ROOT" && GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w $cli_identity_ldflags" -o "$cli_tmp/arm64" ./cmd/reasonix)
98 lipo -create "$cli_tmp/amd64" "$cli_tmp/arm64" -output "$cli_out"
99 rm -rf "$cli_tmp"
100 else
101 (cd "$ROOT" && GOOS="$os" GOARCH="$arch" CGO_ENABLED=0 go build -trimpath -ldflags="-s -w $cli_identity_ldflags" -o "$cli_out" ./cmd/reasonix)
102 fi
103 }
104
105 stamp_windows_executable() {
106 local target="$1"
107 local description="$2"
108 local internal_name="$3"
109 local original_filename="$4"
110 "$windows_resource_tool" \
111 -exe "$target" \
112 -icon "$ROOT/desktop/build/windows/icon.ico" \
113 -version "$numver" \
114 -description "$description" \
115 -internal-name "$internal_name" \
116 -original-filename "$original_filename"
117 }
118
119 # Stamp the Windows version resource from the tag. goversioninfo demands a
120 # strictly numeric X.X.X, so strip the leading "v" AND any prerelease suffix (a
121 # `-rc1` tag would otherwise abort the resource stamping). The full tag still
122 # rides in ldflags for the in-app version.
123 numver="${VERSION#v}"; numver="${numver%%-*}"
124
125 # Regenerate the desktop host contract and fail on drift: the packaged shell
126 # embeds desktopContract.json, so a stale frontend/src/generated would ship a
127 # shell/service protocol mismatch. CI's desktop-prepare job runs the same check.
128 echo "==> desktop host contract drift check"
129 contract_snapshot=$(mktemp -d)
130 cp -R frontend/src/generated "$contract_snapshot/generated"
131 go run . -emit-contract frontend/src/generated
132 if ! diff -qr "$contract_snapshot/generated" frontend/src/generated; then
133 rm -rf "$contract_snapshot"
134 echo "desktop contract is stale - review the regenerated frontend/src/generated files" >&2
135 exit 1
136 fi
137 rm -rf "$contract_snapshot"
138
139 # The packaging script drives the frontend (build:electron) and shell builds
140 # through pnpm; make sure the workspace dependencies (Electron, the packager)
141 # are installed first. A warm store makes this a no-op.
142 if [ ! -d "$ROOT/desktop/node_modules/@electron/packager" ] || [ ! -d "$ROOT/desktop/electron/node_modules/electron" ]; then
143 echo "==> pnpm install desktop workspace"
144 pnpm --dir "$ROOT/desktop" install --frozen-lockfile
145 fi
146
147 # Service ldflags carry version + channel; the shell reads the same identity
148 # from resources/build.json written by package.mjs. macOS Developer ID builds
149 # enable the in-app self-update path.
150 service_ldflags="-X main.version=$VERSION -X main.channel=$CHANNEL $product_docs_ldflags"
151 [ "$os" = "darwin" ] && [ "${HAS_APPLE_CERT:-}" = "true" ] && service_ldflags="$service_ldflags -X main.macSelfUpdate=true"
152 # The Windows service must be a GUI-subsystem image: the shell spawns it with
153 # --host-rpc over inherited pipes, so it never needs a console, and a CONSOLE
154 # image makes Windows allocate a conhost window on every launch (the startup
155 # "flash of black box" in #10148). -H is a Windows-only linker flag.
156 [ "$os" = "windows" ] && service_ldflags="$service_ldflags -H windowsgui"
157
158 # build_service compiles the Go desktop service (reasonix-desktop). It stays
159 # the active version entry the thin launcher starts: without --host-rpc it
160 # bootstraps the Electron shell from app/ and exits; the shell then spawns it
161 # with --host-rpc as the service (see docs/DESKTOP_SHELL_MIGRATION.md phase E).
162 build_service() {
163 echo "==> go build Reasonix desktop service"
164 mkdir -p "$(dirname "$service_out")"
165 if [ "$arch" = universal ]; then
166 service_tmp=$(mktemp -d)
167 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags="-s -w $service_ldflags" -o "$service_tmp/amd64" .
168 GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags="-s -w $service_ldflags" -o "$service_tmp/arm64" .
169 lipo -create "$service_tmp/amd64" "$service_tmp/arm64" -output "$service_out"
170 rm -rf "$service_tmp"
171 else
172 GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags="-s -w $service_ldflags" -o "$service_out" .
173 fi
174 }
175
176 # package_shell runs @electron/packager via the packaging script and leaves the
177 # bundle at desktop/build/electron/<os>-<arch>/ (Reasonix.app on macOS, app/
178 # elsewhere). It also builds the frontend (build:electron) with the channel
179 # threaded through REASONIX_CHANNEL.
180 package_shell() {
181 echo "==> package Electron shell ($PLATFORM)"
182 REASONIX_COMMIT="$SOURCE_SHA" REASONIX_BUILD_TIME="$BUILD_TIME_UTC" \
183 node "$ROOT/desktop/packaging/package.mjs" "$PLATFORM" "$VERSION" "$CHANNEL"
184 }
185
186 mkdir -p "$ROOT/dist"
187
188 case "$os" in
189 darwin)
190 service_out="$ROOT/desktop/build/bin/$BINNAME"
191 build_service
192 cli_out="$ROOT/desktop/build/bin/$CLINAME"
193 build_cli
194 package_shell
195
196 staging=$(mktemp -d)
197 app="$staging/${APPNAME}.app"
198 cp -R "build/electron/${os}-${arch}/${APPNAME}.app" "$app"
199 # The bundle's main executable is Electron. Keep one Go service payload under
200 # Resources and a relative compatibility symlink in MacOS for older launchers.
201 bundle_executable=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$app/Contents/Info.plist")
202 [ "$bundle_executable" = "$APPNAME" ] || { echo "macOS bundle executable is $bundle_executable, want $APPNAME" >&2; exit 1; }
203 mkdir -p "$app/Contents/Resources/service"
204 cp "$service_out" "$app/Contents/Resources/service/$BINNAME"
205 rm -f "$app/Contents/MacOS/$BINNAME"
206 ln -s "../Resources/service/$BINNAME" "$app/Contents/MacOS/$BINNAME"
207 [ "$(readlink "$app/Contents/MacOS/$BINNAME")" = "../Resources/service/$BINNAME" ] || { echo "macOS service compatibility link is invalid" >&2; exit 1; }
208 [ -x "$app/Contents/MacOS/$BINNAME" ] || { echo "macOS service compatibility link is broken" >&2; exit 1; }
209 # Contents/MacOS already holds the Electron executable "Reasonix"; on
210 # case-insensitive APFS a "reasonix" sibling would overwrite it, so the
211 # CLI sidecar ships next to the service copy the shell actually launches
212 # (desktopCLIBinaryPath resolves it beside the running service).
213 cp "$cli_out" "$app/Contents/Resources/service/$CLINAME"
214 if [ -e "$app/Contents/MacOS/$GUARDNAME" ]; then
215 echo "macOS bundle must not include $GUARDNAME" >&2
216 exit 1
217 fi
218 darwin_icon="$ROOT/desktop/build/darwin/icon.icns"
219 bundle_icon=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIconFile" "$app/Contents/Info.plist" 2>/dev/null || true)
220 case "$bundle_icon" in
221 *.icns) ;;
222 *) bundle_icon="$bundle_icon.icns" ;;
223 esac
224 [ -s "$app/Contents/Resources/$bundle_icon" ] || { echo "macOS bundle icon is missing: $bundle_icon" >&2; exit 1; }
225
226 # Two signing paths, selected by HAS_APPLE_CERT (set by release-desktop.yml when
227 # the APPLE_* secrets are present). With a real Developer ID cert + notarization
228 # key we sign with a hardened runtime, notarize, and staple — a downloaded build
229 # then opens with no Gatekeeper prompt. Without it we ad-hoc sign as before (still
230 # un-notarized; users clear the quarantine attribute per desktop/README.md). The
231 # fallback keeps fork/local builds working with no secrets configured.
232 if [ "${HAS_APPLE_CERT:-}" = "true" ]; then
233 identity="$(security find-identity -v -p codesigning | awk -F'"' '/Developer ID Application/{print $2; exit}')"
234 [ -n "$identity" ] || { echo "HAS_APPLE_CERT=true but no 'Developer ID Application' identity found in the keychain" >&2; exit 1; }
235 echo "==> codesign (Developer ID): $identity"
236 node "$ROOT/desktop/packaging/sign-macos.mjs" "$app" "$identity"
237 # notarytool wants an archive, not a bare bundle: zip the .app, submit, wait,
238 # then staple the ticket back onto the bundle so it verifies offline.
239 ditto -c -k --keepParent "$app" "$staging/notarize.zip"
240 notary_diagnostics="${APPLE_NOTARIZATION_LOG_DIR:-$ROOT/desktop/build/notarization}"
241 node "$ROOT/scripts/notarize-desktop.mjs" "$staging/notarize.zip" "$app" app "$notary_diagnostics"
242 else
243 # Ad-hoc cuts the "is damaged" error somewhat but is NOT notarized; users may
244 # still need `xattr -dr com.apple.quarantine` (see desktop/README.md).
245 node "$ROOT/desktop/packaging/sign-macos.mjs" "$app" -
246 fi
247
248 # Updaters receive a native-architecture app. Universal remains a human
249 # download only, so a fat bundle is never copied under architecture names.
250 if [ "$arch" != universal ]; then
251 ditto -c -k --zlibCompressionLevel 9 --keepParent "$app" "$ROOT/dist/${APPNAME}-darwin-${arch}.zip"
252 fi
253 candidate_dir="$ROOT/desktop/build/candidate/darwin-${arch}"
254 rm -rf "$candidate_dir"
255 mkdir -p "$candidate_dir"
256 cp -R "$app" "$candidate_dir/${APPNAME}.app"
257 node "$ROOT/desktop/packaging/verify.mjs" "$candidate_dir/${APPNAME}.app" --kind darwin-app-dir
258 if [ "${DESKTOP_BUILD_SKIP_DMG:-0}" = "1" ]; then
259 echo "==> skip DMG packaging (DESKTOP_BUILD_SKIP_DMG=1)"
260 else
261 # A drag-to-Applications .dmg for first-time human download. cmd/sign uses an
262 # exact filename table, so the .zip stays the updater channel and the .dmg is
263 # release-page only. create-dmg can exit nonzero
264 # while still writing the image, so gate on the file existing, not the exit code.
265 dmgsrc=$(mktemp -d)
266 cp -R "$app" "$dmgsrc/${APPNAME}.app"
267 dmg="$ROOT/dist/${APPNAME}-darwin-${arch}.dmg"
268 create-dmg \
269 --volname "$APPNAME" \
270 --window-size 540 380 \
271 --icon-size 110 \
272 --icon "${APPNAME}.app" 150 190 \
273 --app-drop-link 390 190 \
274 --no-internet-enable \
275 "$dmg" "$dmgsrc" || true
276 [ -f "$dmg" ] || { echo "create-dmg did not produce $dmg" >&2; exit 1; }
277 # The .dmg is a separately-downloaded artifact, so sign + notarize + staple the
278 # disk image itself too — the stapled .app inside isn't enough for the image.
279 if [ "${HAS_APPLE_CERT:-}" = "true" ]; then
280 codesign --force --timestamp -s "$identity" "$dmg"
281 node "$ROOT/scripts/notarize-desktop.mjs" "$dmg" "$dmg" dmg "$notary_diagnostics"
282 fi
283 rm -rf "$dmgsrc"
284 fi
285 rm -rf "$staging"
286 ;;
287 windows)
288 windows_resource_tool_dir=$(mktemp -d)
289 windows_host_include="$ROOT/desktop/build/windows/installer/reasonix_host.nsh"
290 case "$(uname -s 2>/dev/null || printf '%s' unknown)" in
291 Darwin* | Linux* | FreeBSD*)
292 printf '%s\n' '!define REASONIX_UNINST_FINALIZE '\''/bin/cp -f "%1" "reasonix-uninstall.exe"'\''' >"$windows_host_include"
293 ;;
294 *)
295 printf '%s\n' '!define REASONIX_UNINST_FINALIZE '\''cmd.exe /C copy /Y "%1" "reasonix-uninstall.exe" >NUL'\''' >"$windows_host_include"
296 ;;
297 esac
298 windows_resource_tool="$windows_resource_tool_dir/reasonix-windows-resource.exe"
299 echo "==> build Windows resource stamper"
300 go build -trimpath -o "$windows_resource_tool" ./cmd/windows-resource
301
302 installer_dir="$ROOT/desktop/build/windows/installer"
303 guard_out="$installer_dir/$GUARDNAME.exe"
304 build_guard
305 stamp_windows_executable "$guard_out" "Reasonix Legacy Migrator" "$GUARDNAME" "$GUARDNAME.exe"
306 launcher_out="$installer_dir/$LAUNCHERNAME.exe"
307 echo "==> go build Windows GUI thin launcher"
308 (cd "$ROOT" && GOOS=windows GOARCH="$arch" CGO_ENABLED=0 go build -trimpath \
309 -ldflags="-s -w -H windowsgui -X main.version=$VERSION" -o "$launcher_out" ./cmd/reasonix-launcher)
310 stamp_windows_executable "$launcher_out" "Reasonix Launcher" "$LAUNCHERNAME" "$LAUNCHERNAME.exe"
311 UPDATE_HELPER="reasonix-update-helper.exe"
312 echo "==> go build Windows update helper"
313 GOOS=windows GOARCH="$arch" go build -trimpath -ldflags="-s -w" \
314 -o "$installer_dir/$UPDATE_HELPER" ./cmd/update-helper
315 stamp_windows_executable "$installer_dir/$UPDATE_HELPER" "Reasonix Update Helper" "reasonix-update-helper" "$UPDATE_HELPER"
316 cli_out="$installer_dir/$WINDOWS_CLINAME.exe"
317 build_cli
318 stamp_windows_executable "$cli_out" "Reasonix CLI" "$WINDOWS_CLINAME" "$WINDOWS_CLINAME.exe"
319 cli_entry_out="$ROOT/desktop/build/bin/$WINDOWS_CLI_ENTRY"
320 echo "==> go build Windows CLI entry"
321 (cd "$ROOT" && GOOS=windows GOARCH="$arch" CGO_ENABLED=0 go build -trimpath \
322 -ldflags="-s -w" -o "$cli_entry_out" ./cmd/reasonix-cli-launcher)
323 stamp_windows_executable "$cli_entry_out" "Reasonix CLI Launcher" "reasonix-cli-launcher" "$WINDOWS_CLI_ENTRY"
324
325 service_out="$ROOT/desktop/build/bin/$BINNAME.exe"
326 build_service
327 stamp_windows_executable "$service_out" "Reasonix Desktop" "$BINNAME" "$BINNAME.exe"
328 # NSIS File sources live next to project.nsi; the service joins the flat
329 # payload files there (package-windows-desktop.sh overwrites them with the
330 # signed copies before the second pass).
331 cp "$service_out" "$installer_dir/$BINNAME.exe"
332
333 package_shell
334 mkdir -p "build/electron/${os}-${arch}/app/resources/bin"
335 cp "$cli_entry_out" "build/electron/${os}-${arch}/app/resources/bin/$WINDOWS_CLI_ENTRY"
336 # The Electron bundle becomes versions/v<ver>/app/ at install time; NSIS
337 # consumes it as the "app" directory next to project.nsi.
338 rm -rf "$installer_dir/app"
339 cp -R "build/electron/${os}-${arch}/app" "$installer_dir/app"
340
341 # First NSIS pass: regenerate this release's uninstaller. A stale preserved
342 # uninstaller must never enter the signing payload.
343 # Compile only the shared uninstall section here; compressing the entire
344 # Electron payload just to discard this installer costs another five minutes.
345 rm -f "$installer_dir/reasonix-uninstall.exe"
346 find "$ROOT/desktop/build/bin" -maxdepth 1 -type f -name '*installer*.exe' -delete
347 arch_binary_define="ARG_REASONIX_AMD64_BINARY"
348 [ "$arch" = arm64 ] && arch_binary_define="ARG_REASONIX_ARM64_BINARY"
349 (
350 cd "$installer_dir"
351 makensis -DARG_REASONIX_UNINSTALLER_ONLY "-D${arch_binary_define}=$installer_dir/$BINNAME.exe" project.nsi
352 )
353 [ -s "$installer_dir/reasonix-uninstall.exe" ] || { echo "first NSIS pass did not produce reasonix-uninstall.exe" >&2; exit 1; }
354
355 # Keep one canonical payload for SignPath: the flat Go executables plus the
356 # Electron app/ tree. The release workflow signs these files, then calls
357 # package-windows-desktop.sh again so both the portable archive and the
358 # files embedded by NSIS carry Authenticode.
359 payload_dir="$ROOT/desktop/build/windows/signing-payload"
360 rm -rf -- "$payload_dir"
361 mkdir -p "$payload_dir"
362 for name in "$BINNAME.exe" "$GUARDNAME.exe" "$LAUNCHERNAME.exe" "$UPDATE_HELPER" "$WINDOWS_CLINAME.exe" "reasonix-uninstall.exe"; do
363 cp "$installer_dir/$name" "$payload_dir/$name"
364 done
365 cp -R "$installer_dir/app" "$payload_dir/app"
366 # signing-files.txt enumerates every PE file (flat payload + app tree); the
367 # SignPath artifact configuration and the Authenticode verifier consume it.
368 node "$ROOT/desktop/packaging/signing-files.mjs" "$payload_dir"
369 VERSION="$VERSION" "$ROOT/scripts/package-windows-desktop.sh" "$arch" "$payload_dir"
370 node "$ROOT/desktop/packaging/verify.mjs" "$ROOT/dist/${APPNAME}-windows-${arch}.zip" --kind windows-portable-zip
371 ;;
372 linux)
373 service_out="$ROOT/desktop/build/bin/$BINNAME"
374 build_service
375 # Linux still ships a one-shot migrator named reasonix-guard in the portable
376 # tarball so 1.18–1.19.1 updaters can hand off.
377 guard_out="$ROOT/desktop/build/bin/$GUARDNAME"
378 build_guard
379 launcher_out="$ROOT/desktop/build/bin/$LAUNCHERNAME"
380 echo "==> go build Linux thin launcher"
381 (cd "$ROOT" && GOOS=linux GOARCH="$arch" CGO_ENABLED=0 go build -trimpath \
382 -ldflags="-s -w -X main.version=$VERSION" -o "$launcher_out" ./cmd/reasonix-launcher)
383 cli_out="$ROOT/desktop/build/bin/$CLINAME"
384 build_cli
385 package_shell
386 # Stage the Electron tree next to the Go binaries so the tarball and the
387 # nfpm config share one source root (build/bin).
388 rm -rf "build/bin/app"
389 cp -R "build/electron/${os}-${arch}/app" "build/bin/app"
390
391 for desktop_contract in \
392 'Exec=reasonix-launcher' \
393 'Icon=reasonix-desktop' \
394 'StartupWMClass=Reasonix'; do
395 grep -F -x -q "$desktop_contract" build/linux/reasonix.desktop || { echo "Linux desktop entry missing: $desktop_contract" >&2; exit 1; }
396 done
397 # Portable Linux tarball: service + thin launcher + one-shot migrator
398 # (compat name reasonix-guard) + CLI + the Electron app/ tree. After the
399 # migrator runs, Guard self-deletes.
400 tar -cf - -C build/bin "$BINNAME" "$LAUNCHERNAME" "$GUARDNAME" "$CLINAME" app | \
401 gzip -9 >"$ROOT/dist/${APPNAME}-linux-${arch}.tar.gz"
402 # Build the privileged update helper shipped inside the .deb. Portable tarball
403 # installs do not need it; only the dpkg package installs helper + Polkit policy.
404 echo "==> go build reasonix-update-helper"
405 GOOS=linux GOARCH="$arch" CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=$VERSION" \
406 -o "build/bin/reasonix-update-helper" ./cmd/update-helper
407 # .deb for Debian/Ubuntu. Portable updater still uses the tarball under
408 # platforms[]; .deb is published under native_packages. Debian versions use
409 # "~" for prereleases so 1.18.0~rc.1 < 1.18.0 (policy version ordering).
410 # Extra "-" inside the prerelease label becomes "." (Debian policy).
411 ver_body="${VERSION#v}"
412 if [[ "$ver_body" == *-* ]]; then
413 deb_base="${ver_body%%-*}"
414 deb_pre="${ver_body#*-}"
415 deb_pre="${deb_pre//-/.}"
416 deb_version="${deb_base}~${deb_pre}"
417 else
418 deb_version="$ver_body"
419 fi
420 DEB_VERSION="$deb_version" DEB_ARCH="$arch" \
421 nfpm package --config build/linux/nfpm.yaml --packager deb \
422 --target "$ROOT/dist/${APPNAME}-linux-${arch}.deb"
423 # Contract smoke: helper, policy, package identity, Electron tree, sandbox.
424 deb_path="$ROOT/dist/${APPNAME}-linux-${arch}.deb"
425 dpkg-deb --field "$deb_path" Package | grep -x 'reasonix-desktop' >/dev/null
426 dpkg-deb --field "$deb_path" Version | grep -x "$deb_version" >/dev/null
427 dpkg-deb --field "$deb_path" Depends | grep -F 'pkexec' >/dev/null
428 dpkg-deb --contents "$deb_path" | grep -E 'usr/lib/reasonix/reasonix-update-helper' >/dev/null
429 dpkg-deb --contents "$deb_path" | grep -E 'usr/share/polkit-1/actions/io.reasonix.desktop.update.policy' >/dev/null
430 dpkg-deb --contents "$deb_path" | grep -E "usr/lib/reasonix/app/${APPNAME}" >/dev/null
431 dpkg-deb --contents "$deb_path" | grep -E 'usr/lib/reasonix/app/chrome-sandbox' >/dev/null
432 node "$ROOT/desktop/packaging/verify.mjs" "$ROOT/dist/${APPNAME}-linux-${arch}.tar.gz" --kind linux-tar
433 node "$ROOT/desktop/packaging/verify.mjs" "$deb_path" --kind linux-deb
434 ;;
435 *)
436 echo "unsupported os: $os" >&2
437 exit 1
438 ;;
439 esac
440
441 case "$os" in
442 # The staging directory is intentionally removed after the signed app is
443 # copied into build/candidate. Reports must inspect that published candidate,
444 # otherwise every successful macOS package build fails after artifact
445 # verification with ENOENT.
446 darwin) report_bundle="$ROOT/desktop/build/candidate/darwin-${arch}/${APPNAME}.app" ;;
447 windows) report_bundle="$ROOT/desktop/build/windows/signing-payload" ;;
448 linux) report_bundle="$ROOT/desktop/build/bin" ;;
449 esac
450 REASONIX_COMMIT="$SOURCE_SHA" REASONIX_BUILD_SECONDS="$((SECONDS - build_started_seconds))" \
451 node "$ROOT/desktop/packaging/size-report.mjs" \
452 --platform "$PLATFORM" \
453 --version "$VERSION" \
454 --bundle "$report_bundle" \
455 --dist "$ROOT/dist" \
456 --output "$ROOT/desktop/build/reports/${os}-${arch}"
457
458 echo "==> packaged into dist/:"
459 ls -la "$ROOT/dist"
460
460 lines BASH