From a8a5d471f092f226d07f2cb4d9e38321817a02fb Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Mon, 9 Mar 2026 01:33:19 -0700 Subject: [PATCH] wrong arch pt1 --- apps/desktop/src/main.ts | 19 ++++++- apps/desktop/src/runtimeArch.test.ts | 49 +++++++++++++++++++ apps/desktop/src/runtimeArch.ts | 40 +++++++++++++++ apps/desktop/src/updateMachine.test.ts | 18 ++++--- apps/desktop/src/updateMachine.ts | 11 +++-- apps/desktop/src/updateState.test.ts | 3 ++ apps/web/src/components/Sidebar.tsx | 34 +++++++++++++ .../components/desktopUpdate.logic.test.ts | 31 ++++++++++++ .../web/src/components/desktopUpdate.logic.ts | 19 +++++++ packages/contracts/src/ipc.ts | 11 +++++ 10 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 apps/desktop/src/runtimeArch.test.ts create mode 100644 apps/desktop/src/runtimeArch.ts diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 2abce92ba9..03cf67cd6d 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -31,6 +31,7 @@ import { reduceDesktopUpdateStateOnNoUpdate, reduceDesktopUpdateStateOnUpdateAvailable, } from "./updateMachine"; +import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch"; fixPath(); @@ -80,7 +81,13 @@ let backendLogSink: RotatingFileSink | null = null; let restoreStdIoCapture: (() => void) | null = null; let destructiveMenuIconCache: Electron.NativeImage | null | undefined; -const initialUpdateState = (): DesktopUpdateState => createInitialDesktopUpdateState(app.getVersion()); +const desktopRuntimeInfo = resolveDesktopRuntimeInfo({ + platform: process.platform, + processArch: process.arch, + runningUnderArm64Translation: app.runningUnderARM64Translation === true, +}); +const initialUpdateState = (): DesktopUpdateState => + createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo); function logTimestamp(): string { return new Date().toISOString(); @@ -708,6 +715,7 @@ async function downloadAvailableUpdate(): Promise<{ accepted: boolean; completed } updateDownloadInFlight = true; setUpdateState(reduceDesktopUpdateStateOnDownloadStart(updateState)); + autoUpdater.disableDifferentialDownload = isArm64HostRunningIntelBuild(desktopRuntimeInfo); console.info("[desktop-updater] Downloading update..."); try { @@ -746,7 +754,7 @@ async function installDownloadedUpdate(): Promise<{ accepted: boolean; completed function configureAutoUpdater(): void { const enabled = shouldEnableAutoUpdates(); setUpdateState({ - ...createInitialDesktopUpdateState(app.getVersion()), + ...createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo), enabled, status: enabled ? "idle" : "disabled", }); @@ -780,8 +788,15 @@ function configureAutoUpdater(): void { autoUpdater.channel = DESKTOP_UPDATE_CHANNEL; autoUpdater.allowPrerelease = DESKTOP_UPDATE_ALLOW_PRERELEASE; autoUpdater.allowDowngrade = false; + autoUpdater.disableDifferentialDownload = isArm64HostRunningIntelBuild(desktopRuntimeInfo); let lastLoggedDownloadMilestone = -1; + if (isArm64HostRunningIntelBuild(desktopRuntimeInfo)) { + console.info( + "[desktop-updater] Apple Silicon host detected while running Intel build; updates will switch to arm64 packages.", + ); + } + autoUpdater.on("checking-for-update", () => { console.info("[desktop-updater] Looking for updates..."); }); diff --git a/apps/desktop/src/runtimeArch.test.ts b/apps/desktop/src/runtimeArch.test.ts new file mode 100644 index 0000000000..258a8fb215 --- /dev/null +++ b/apps/desktop/src/runtimeArch.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; + +import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch"; + +describe("resolveDesktopRuntimeInfo", () => { + it("detects Rosetta-translated Intel builds on Apple Silicon", () => { + const runtimeInfo = resolveDesktopRuntimeInfo({ + platform: "darwin", + processArch: "x64", + runningUnderArm64Translation: true, + }); + + expect(runtimeInfo).toEqual({ + hostArch: "arm64", + appArch: "x64", + runningUnderArm64Translation: true, + }); + expect(isArm64HostRunningIntelBuild(runtimeInfo)).toBe(true); + }); + + it("detects native Apple Silicon builds", () => { + const runtimeInfo = resolveDesktopRuntimeInfo({ + platform: "darwin", + processArch: "arm64", + runningUnderArm64Translation: false, + }); + + expect(runtimeInfo).toEqual({ + hostArch: "arm64", + appArch: "arm64", + runningUnderArm64Translation: false, + }); + expect(isArm64HostRunningIntelBuild(runtimeInfo)).toBe(false); + }); + + it("passes through non-mac builds without translation", () => { + const runtimeInfo = resolveDesktopRuntimeInfo({ + platform: "linux", + processArch: "x64", + runningUnderArm64Translation: true, + }); + + expect(runtimeInfo).toEqual({ + hostArch: "x64", + appArch: "x64", + runningUnderArm64Translation: false, + }); + }); +}); diff --git a/apps/desktop/src/runtimeArch.ts b/apps/desktop/src/runtimeArch.ts new file mode 100644 index 0000000000..7e4265afb0 --- /dev/null +++ b/apps/desktop/src/runtimeArch.ts @@ -0,0 +1,40 @@ +import type { DesktopRuntimeArch, DesktopRuntimeInfo } from "@t3tools/contracts"; + +interface ResolveDesktopRuntimeInfoInput { + readonly platform: NodeJS.Platform; + readonly processArch: string; + readonly runningUnderArm64Translation: boolean; +} + +function normalizeDesktopArch(arch: string): DesktopRuntimeArch { + if (arch === "arm64") return "arm64"; + if (arch === "x64") return "x64"; + return "other"; +} + +export function resolveDesktopRuntimeInfo( + input: ResolveDesktopRuntimeInfoInput, +): DesktopRuntimeInfo { + const appArch = normalizeDesktopArch(input.processArch); + + if (input.platform !== "darwin") { + return { + hostArch: appArch, + appArch, + runningUnderArm64Translation: false, + }; + } + + const hostArch = + appArch === "arm64" || input.runningUnderArm64Translation ? "arm64" : appArch; + + return { + hostArch, + appArch, + runningUnderArm64Translation: input.runningUnderArm64Translation, + }; +} + +export function isArm64HostRunningIntelBuild(runtimeInfo: DesktopRuntimeInfo): boolean { + return runtimeInfo.hostArch === "arm64" && runtimeInfo.appArch === "x64"; +} diff --git a/apps/desktop/src/updateMachine.test.ts b/apps/desktop/src/updateMachine.test.ts index 045bfaf8f4..620c683b82 100644 --- a/apps/desktop/src/updateMachine.test.ts +++ b/apps/desktop/src/updateMachine.test.ts @@ -13,11 +13,17 @@ import { reduceDesktopUpdateStateOnUpdateAvailable, } from "./updateMachine"; +const runtimeInfo = { + hostArch: "x64", + appArch: "x64", + runningUnderArm64Translation: false, +} as const; + describe("updateMachine", () => { it("clears transient errors when a check starts", () => { const state = reduceDesktopUpdateStateOnCheckStart( { - ...createInitialDesktopUpdateState("1.0.0"), + ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), enabled: true, status: "error", message: "network", @@ -36,7 +42,7 @@ describe("updateMachine", () => { it("records a check failure without exposing an action", () => { const state = reduceDesktopUpdateStateOnCheckFailure( { - ...createInitialDesktopUpdateState("1.0.0"), + ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), enabled: true, status: "checking", }, @@ -52,7 +58,7 @@ describe("updateMachine", () => { it("preserves available version on download failure for retry", () => { const state = reduceDesktopUpdateStateOnDownloadFailure( { - ...createInitialDesktopUpdateState("1.0.0"), + ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), enabled: true, status: "downloading", availableVersion: "1.1.0", @@ -70,7 +76,7 @@ describe("updateMachine", () => { it("transitions to downloaded and then preserves install retry state", () => { const downloaded = reduceDesktopUpdateStateOnDownloadComplete( { - ...createInitialDesktopUpdateState("1.0.0"), + ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), enabled: true, status: "downloading", availableVersion: "1.1.0", @@ -89,7 +95,7 @@ describe("updateMachine", () => { it("clears stale download state when no update is available", () => { const state = reduceDesktopUpdateStateOnNoUpdate( { - ...createInitialDesktopUpdateState("1.0.0"), + ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), enabled: true, status: "error", availableVersion: "1.1.0", @@ -111,7 +117,7 @@ describe("updateMachine", () => { it("tracks available, download start, and progress cleanly", () => { const available = reduceDesktopUpdateStateOnUpdateAvailable( { - ...createInitialDesktopUpdateState("1.0.0"), + ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), enabled: true, status: "checking", }, diff --git a/apps/desktop/src/updateMachine.ts b/apps/desktop/src/updateMachine.ts index 9c4e212657..f13b420281 100644 --- a/apps/desktop/src/updateMachine.ts +++ b/apps/desktop/src/updateMachine.ts @@ -1,12 +1,18 @@ -import type { DesktopUpdateState } from "@t3tools/contracts"; +import type { DesktopRuntimeInfo, DesktopUpdateState } from "@t3tools/contracts"; import { getCanRetryAfterDownloadFailure, nextStatusAfterDownloadFailure } from "./updateState"; -export function createInitialDesktopUpdateState(currentVersion: string): DesktopUpdateState { +export function createInitialDesktopUpdateState( + currentVersion: string, + runtimeInfo: DesktopRuntimeInfo, +): DesktopUpdateState { return { enabled: false, status: "disabled", currentVersion, + hostArch: runtimeInfo.hostArch, + appArch: runtimeInfo.appArch, + runningUnderArm64Translation: runtimeInfo.runningUnderArm64Translation, availableVersion: null, downloadedVersion: null, downloadPercent: null, @@ -152,4 +158,3 @@ export function reduceDesktopUpdateStateOnInstallFailure( canRetry: true, }; } - diff --git a/apps/desktop/src/updateState.test.ts b/apps/desktop/src/updateState.test.ts index e2bc9c6257..43b718bd00 100644 --- a/apps/desktop/src/updateState.test.ts +++ b/apps/desktop/src/updateState.test.ts @@ -12,6 +12,9 @@ const baseState: DesktopUpdateState = { enabled: true, status: "idle", currentVersion: "1.0.0", + hostArch: "x64", + appArch: "x64", + runningUnderArm64Translation: false, availableVersion: null, downloadedVersion: null, downloadPercent: null, diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index ecfd526acb..894fde25e9 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -8,6 +8,7 @@ import { SettingsIcon, SquarePenIcon, TerminalIcon, + TriangleAlertIcon, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { @@ -36,14 +37,18 @@ import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraft import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { toastManager } from "./ui/toast"; import { + getArm64IntelBuildWarningDescription, getDesktopUpdateActionError, getDesktopUpdateButtonTooltip, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, + shouldShowArm64IntelBuildWarning, shouldHighlightDesktopUpdateError, shouldShowDesktopUpdateButton, shouldToastDesktopUpdateActionResult, } from "./desktopUpdate.logic"; +import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert"; +import { Button } from "./ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { @@ -887,6 +892,12 @@ export default function Sidebar() { const desktopUpdateButtonAction = desktopUpdateState ? resolveDesktopUpdateButtonAction(desktopUpdateState) : "none"; + const showArm64IntelBuildWarning = + isElectron && shouldShowArm64IntelBuildWarning(desktopUpdateState); + const arm64IntelBuildWarningDescription = + desktopUpdateState && showArm64IntelBuildWarning + ? getArm64IntelBuildWarningDescription(desktopUpdateState) + : null; const desktopUpdateButtonInteractivityClasses = desktopUpdateButtonDisabled ? "cursor-not-allowed opacity-60" : "hover:bg-accent hover:text-foreground"; @@ -1030,6 +1041,29 @@ export default function Sidebar() { )} + {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? ( + + + + Intel build on Apple Silicon + {arm64IntelBuildWarningDescription} + {desktopUpdateButtonAction !== "none" ? ( + + + + ) : null} + + + ) : null}
diff --git a/apps/web/src/components/desktopUpdate.logic.test.ts b/apps/web/src/components/desktopUpdate.logic.test.ts index f80c1f3e46..7fd362bf1d 100644 --- a/apps/web/src/components/desktopUpdate.logic.test.ts +++ b/apps/web/src/components/desktopUpdate.logic.test.ts @@ -2,11 +2,13 @@ import { describe, expect, it } from "vitest"; import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts"; import { + getArm64IntelBuildWarningDescription, getDesktopUpdateActionError, getDesktopUpdateButtonTooltip, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, shouldHighlightDesktopUpdateError, + shouldShowArm64IntelBuildWarning, shouldShowDesktopUpdateButton, shouldToastDesktopUpdateActionResult, } from "./desktopUpdate.logic"; @@ -15,6 +17,9 @@ const baseState: DesktopUpdateState = { enabled: true, status: "idle", currentVersion: "1.0.0", + hostArch: "x64", + appArch: "x64", + runningUnderArm64Translation: false, availableVersion: null, downloadedVersion: null, downloadPercent: null, @@ -171,4 +176,30 @@ describe("desktop update UI helpers", () => { }), ).toBe(false); }); + + it("shows an Apple Silicon warning for Intel builds under Rosetta", () => { + const state: DesktopUpdateState = { + ...baseState, + hostArch: "arm64", + appArch: "x64", + runningUnderArm64Translation: true, + }; + + expect(shouldShowArm64IntelBuildWarning(state)).toBe(true); + expect(getArm64IntelBuildWarningDescription(state)).toContain("Apple Silicon"); + expect(getArm64IntelBuildWarningDescription(state)).toContain("Intel build"); + }); + + it("changes the warning copy when a native build update is ready to download", () => { + const state: DesktopUpdateState = { + ...baseState, + hostArch: "arm64", + appArch: "x64", + runningUnderArm64Translation: true, + status: "available", + availableVersion: "1.1.0", + }; + + expect(getArm64IntelBuildWarningDescription(state)).toContain("Download the available update"); + }); }); diff --git a/apps/web/src/components/desktopUpdate.logic.ts b/apps/web/src/components/desktopUpdate.logic.ts index ab7b7cdc41..faf30883cc 100644 --- a/apps/web/src/components/desktopUpdate.logic.ts +++ b/apps/web/src/components/desktopUpdate.logic.ts @@ -32,10 +32,29 @@ export function shouldShowDesktopUpdateButton(state: DesktopUpdateState | null): return resolveDesktopUpdateButtonAction(state) !== "none"; } +export function shouldShowArm64IntelBuildWarning(state: DesktopUpdateState | null): boolean { + return state?.hostArch === "arm64" && state.appArch === "x64"; +} + export function isDesktopUpdateButtonDisabled(state: DesktopUpdateState | null): boolean { return state?.status === "downloading"; } +export function getArm64IntelBuildWarningDescription(state: DesktopUpdateState): string { + if (!shouldShowArm64IntelBuildWarning(state)) { + return "This install is using the correct architecture."; + } + + const action = resolveDesktopUpdateButtonAction(state); + if (action === "download") { + return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. Download the available update to switch to the native Apple Silicon build."; + } + if (action === "install") { + return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. Restart to install the downloaded Apple Silicon build."; + } + return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. The next app update will replace it with the native Apple Silicon build."; +} + export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string { if (state.status === "available") { return `Update ${state.availableVersion ?? "available"} ready to download`; diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index 92f5b502c9..db9bab415e 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -58,10 +58,21 @@ export type DesktopUpdateStatus = | "downloaded" | "error"; +export type DesktopRuntimeArch = "arm64" | "x64" | "other"; + +export interface DesktopRuntimeInfo { + hostArch: DesktopRuntimeArch; + appArch: DesktopRuntimeArch; + runningUnderArm64Translation: boolean; +} + export interface DesktopUpdateState { enabled: boolean; status: DesktopUpdateStatus; currentVersion: string; + hostArch: DesktopRuntimeArch; + appArch: DesktopRuntimeArch; + runningUnderArm64Translation: boolean; availableVersion: string | null; downloadedVersion: string | null; downloadPercent: number | null;