diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml index 78774033c..000909477 100644 --- a/packages/desktop/electron-builder.yml +++ b/packages/desktop/electron-builder.yml @@ -52,6 +52,13 @@ linux: - deb - rpm - tar.gz +appImage: + # Keep the AppImage filename stable across versions (no ${version}) so + # electron-updater overwrites the file in place instead of renaming it to a new + # versioned path. A rename orphans the old binary, breaks desktop shortcuts, and + # dangles the ~/.local/bin/paseo CLI symlink, which points at $APPIMAGE. The + # version stays discoverable via the release tag and the in-app version. + artifactName: "Paseo-${arch}.${ext}" win: artifactName: "Paseo-Setup-${version}-${arch}.${ext}" icon: assets/icon.ico diff --git a/packages/desktop/src/features/auto-updater.test.ts b/packages/desktop/src/features/auto-updater.test.ts index f810b8da3..4de12d87d 100644 --- a/packages/desktop/src/features/auto-updater.test.ts +++ b/packages/desktop/src/features/auto-updater.test.ts @@ -19,8 +19,18 @@ import { resolveStagingUserId, rolloutManifestSchema, shouldAdmitToRollout, + shouldAutoInstallOnQuit, } from "./auto-updater"; +describe("shouldAutoInstallOnQuit", () => { + it("auto-installs on quit everywhere except Linux AppImage", () => { + expect(shouldAutoInstallOnQuit({ platform: "linux", isAppImage: true })).toBe(false); + expect(shouldAutoInstallOnQuit({ platform: "linux", isAppImage: false })).toBe(true); + expect(shouldAutoInstallOnQuit({ platform: "darwin", isAppImage: false })).toBe(true); + expect(shouldAutoInstallOnQuit({ platform: "win32", isAppImage: false })).toBe(true); + }); +}); + describe("shouldAdmitToRollout", () => { it("admits beta, missing rollout hours, zero-hour rollout, and missing release date", () => { expect( diff --git a/packages/desktop/src/features/auto-updater.ts b/packages/desktop/src/features/auto-updater.ts index 92ad1b213..876114585 100644 --- a/packages/desktop/src/features/auto-updater.ts +++ b/packages/desktop/src/features/auto-updater.ts @@ -76,13 +76,31 @@ export function getStagingUserId(): Promise { return cachedStagingUserIdPromise; } +// AppImages have no install step. electron-updater "installs" by unlinking the +// running file and mv-ing the downloaded one into place; on app quit it does this +// via a *blocking* execFileSync(newAppImage, { APPIMAGE_EXIT_AFTER_INSTALL: "true" }). +// That env var is only honored by AppImageLauncher, so without it the freshly +// launched process boots the full app and never exits — the quit hangs forever, +// with the old binary already deleted. We therefore install AppImages only on +// explicit quitAndInstall (the "Update now" button), which takes the non-blocking +// spawn path. Every other target keeps auto-install-on-quit, which works there. +export function shouldAutoInstallOnQuit(input: { + platform: NodeJS.Platform; + isAppImage: boolean; +}): boolean { + return !(input.platform === "linux" && input.isAppImage); +} + class ElectronAppUpdateRuntime implements AppUpdateRuntime { private configured = false; configure(input: AppUpdateRuntimeConfiguration): void { autoUpdater.autoDownload = true; - autoUpdater.autoInstallOnAppQuit = true; autoUpdater.autoRunAppAfterInstall = true; + autoUpdater.autoInstallOnAppQuit = shouldAutoInstallOnQuit({ + platform: process.platform, + isAppImage: Boolean(process.env.APPIMAGE), + }); autoUpdater.allowPrerelease = input.releaseChannel === "beta"; autoUpdater.channel = input.releaseChannel === "beta" ? "beta" : "latest"; autoUpdater.allowDowngrade = false;