diff --git a/apps/desktop/scripts/package.mjs b/apps/desktop/scripts/package.mjs index 0450ecf4f1..d68dfd5a9f 100644 --- a/apps/desktop/scripts/package.mjs +++ b/apps/desktop/scripts/package.mjs @@ -66,6 +66,7 @@ const ARCH_FLAGS = new Map([ const SUPPORTED_CLI_ARCHS = new Set(["x64", "arm64"]); const MAC_ALL_PLATFORM_TARGETS = [ { platform: "mac", arch: "arm64" }, + { platform: "mac", arch: "x64" }, { platform: "win", arch: "x64" }, { platform: "win", arch: "arm64" }, { platform: "linux", arch: "x64" }, @@ -338,18 +339,21 @@ export function builderArgsForTarget( `-c.directories.output=dist/${target.platform}-${target.arch}`, ); } - // electron-builder's update metadata file is `latest.yml` for Windows - // regardless of arch (only Linux gets an arch suffix automatically — see - // app-builder-lib's getArchPrefixForUpdateFile). Without an explicit - // channel override, building Windows x64 and arm64 in two invocations - // makes both publish `latest.yml` to the same GitHub Release, so the - // second upload overwrites the first and one of the two architectures - // ends up with no auto-update metadata. Route Windows arm64 to its own - // channel so x64 keeps `latest.yml` and arm64 ships `latest-arm64.yml`; - // the renderer-side updater pins the matching channel per arch. + // electron-builder only adds an architecture suffix to Linux update + // metadata. Windows x64/arm64 would both publish `latest.yml`, while macOS + // arm64/x64 would both publish `latest-mac.yml`. Keep the established x64 + // Windows and arm64 macOS feeds unchanged for installed clients, and route + // the additional architectures to explicit channels. updater.ts pins the + // matching channel at runtime. if (target.platform === "win" && target.arch === "arm64") { builderArgs.push("-c.publish.channel=latest-arm64"); } + if (target.platform === "mac" && target.arch === "x64") { + // Scope the Electron 39 platform floor to the new Intel package so this + // change does not rewrite established Apple Silicon bundle metadata. + builderArgs.push("-c.mac.minimumSystemVersion=12.0.0"); + builderArgs.push("-c.publish.channel=latest-x64"); + } return builderArgs; } diff --git a/apps/desktop/scripts/package.test.mjs b/apps/desktop/scripts/package.test.mjs index cc1c549cb9..3d3f5b5649 100644 --- a/apps/desktop/scripts/package.test.mjs +++ b/apps/desktop/scripts/package.test.mjs @@ -241,6 +241,7 @@ describe("resolveBuildMatrix", () => { ), ).toEqual([ { platform: "mac", arch: "arm64" }, + { platform: "mac", arch: "x64" }, { platform: "win", arch: "x64" }, { platform: "win", arch: "arm64" }, { platform: "linux", arch: "x64" }, @@ -322,6 +323,58 @@ describe("builderArgsForTarget", () => { ]); }); + it("isolates the macOS x64 feed and platform floor", () => { + expect( + builderArgsForTarget( + { platform: "mac", arch: "x64" }, + { + allPlatforms: false, + sharedArgs: ["--publish", "always"], + platformTargets: { mac: ["dmg", "zip"], win: [], linux: [] }, + requestedPlatforms: ["mac"], + requestedArchs: ["x64"], + }, + "1.2.3", + { hostPlatform: "darwin", useScopedOutputDir: true }, + ), + ).toEqual([ + "-c.extraMetadata.version=1.2.3", + "--mac", + "dmg", + "zip", + "--x64", + "--publish", + "always", + "-c.directories.output=dist/mac-x64", + "-c.mac.minimumSystemVersion=12.0.0", + "-c.publish.channel=latest-x64", + ]); + }); + + it("keeps macOS arm64 on the existing latest-mac update channel", () => { + expect( + builderArgsForTarget( + { platform: "mac", arch: "arm64" }, + { + allPlatforms: false, + sharedArgs: ["--publish", "always"], + platformTargets: { mac: [], win: [], linux: [] }, + requestedPlatforms: ["mac"], + requestedArchs: ["arm64"], + }, + "1.2.3", + { hostPlatform: "darwin", useScopedOutputDir: true }, + ), + ).toEqual([ + "-c.extraMetadata.version=1.2.3", + "--mac", + "--arm64", + "--publish", + "always", + "-c.directories.output=dist/mac-arm64", + ]); + }); + it("defaults linux cross-builds to AppImage on non-Linux hosts", () => { expect( builderArgsForTarget( diff --git a/apps/desktop/src/main/updater.test.ts b/apps/desktop/src/main/updater.test.ts index 28221082fe..a778bda248 100644 --- a/apps/desktop/src/main/updater.test.ts +++ b/apps/desktop/src/main/updater.test.ts @@ -26,6 +26,7 @@ vi.mock("electron-updater", () => { autoDownload: false, autoInstallOnAppQuit: false, channel: undefined as string | undefined, + allowDowngrade: false, on: vi.fn((event: string, handler: Handler) => { const handlers = ctx.handlers.get(event) ?? []; handlers.push(handler); @@ -50,9 +51,40 @@ vi.mock("electron", () => ({ }, })); -import { setupAutoUpdater } from "./updater"; +import { + configureMacX64UpdateChannel, + setupAutoUpdater, +} from "./updater"; import { updaterPreferencesPath } from "./updater-preferences"; +describe("macOS x64 update channel", () => { + it("does not touch established architecture paths", () => { + for (const [platform, arch] of [ + ["darwin", "arm64"], + ["win32", "x64"], + ["win32", "arm64"], + ["linux", "arm64"], + ] as const) { + const updater = { channel: null, allowDowngrade: true }; + + configureMacX64UpdateChannel(updater, platform, arch); + + expect(updater).toEqual({ channel: null, allowDowngrade: true }); + } + }); + + it("does not enable downgrades when selecting an architecture feed", () => { + const updater = { channel: null, allowDowngrade: true }; + + configureMacX64UpdateChannel(updater, "darwin", "x64"); + + expect(updater).toEqual({ + channel: "latest-x64", + allowDowngrade: false, + }); + }); +}); + function emitUpdater(event: string, ...args: unknown[]) { for (const handler of ctx.handlers.get(event) ?? []) { handler(...args); diff --git a/apps/desktop/src/main/updater.ts b/apps/desktop/src/main/updater.ts index fa7582dd21..7097fb39be 100644 --- a/apps/desktop/src/main/updater.ts +++ b/apps/desktop/src/main/updater.ts @@ -27,6 +27,30 @@ if (process.platform === "win32" && process.arch === "arm64") { autoUpdater.channel = "latest-arm64"; } +interface ChannelConfigurableUpdater { + channel: string | null; + allowDowngrade: boolean; +} + +export function configureMacX64UpdateChannel( + updater: ChannelConfigurableUpdater, + platform: NodeJS.Platform = process.platform, + arch: string = process.arch, +): void { + if (platform !== "darwin" || arch !== "x64") return; + + // AppUpdater.channel enables allowDowngrade as a side effect. This channel + // isolates a CPU architecture, not a release train, so preserve normal + // monotonic version behavior after selecting the architecture feed. + updater.channel = "latest-x64"; + updater.allowDowngrade = false; +} + +// electron-builder does not architecture-suffix macOS update metadata. +// package.mjs publishes macOS x64 as `latest-x64-mac.yml`; the established +// arm64 feed and runtime path remain unchanged. +configureMacX64UpdateChannel(autoUpdater); + const STARTUP_CHECK_DELAY_MS = 5_000; const PERIODIC_CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour