Sfoglia il codice sorgente

fix(desktop): restore linux launcher identity (#31709)

Filip 2 mesi fa
parent
commit
2e0f88d0e3

+ 48 - 0
packages/desktop/electron-builder.config.test.ts

@@ -0,0 +1,48 @@
+import { expect, test } from "bun:test"
+import type { Configuration } from "electron-builder"
+
+const legacyDesktopEntry = "resources/linux/opencode-desktop.desktop"
+
+const channels = [
+  { channel: "dev", appId: "ai.opencode.desktop.dev" },
+  { channel: "beta", appId: "ai.opencode.desktop.beta" },
+  { channel: "prod", appId: "ai.opencode.desktop" },
+] as const
+
+for (const channel of channels) {
+  test(`uses one Linux desktop identity for ${channel.channel}`, async () => {
+    const previous = process.env.OPENCODE_CHANNEL
+    process.env.OPENCODE_CHANNEL = channel.channel
+
+    const module = await import(`./electron-builder.config.ts?channel=${channel.channel}`)
+    const config = module.default as Configuration
+
+    if (previous === undefined) delete process.env.OPENCODE_CHANNEL
+    else process.env.OPENCODE_CHANNEL = previous
+
+    expect(config.appId).toBe(channel.appId)
+    expect(config.extraMetadata?.desktopName).toBe(`${channel.appId}.desktop`)
+    expect(config.linux?.executableName).toBe(channel.appId)
+    expect(config.linux?.desktop?.entry?.StartupWMClass).toBe(channel.appId)
+  })
+}
+
+test("keeps a hidden prod launcher for old Linux pins", async () => {
+  const previous = process.env.OPENCODE_CHANNEL
+  process.env.OPENCODE_CHANNEL = "prod"
+
+  const module = await import("./electron-builder.config.ts?compat=prod")
+  const config = module.default as Configuration
+
+  if (previous === undefined) delete process.env.OPENCODE_CHANNEL
+  else process.env.OPENCODE_CHANNEL = previous
+
+  expect(config.deb?.fpm?.[0]).toEndWith(`${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop`)
+  expect(config.rpm?.fpm?.[0]).toEndWith(`${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop`)
+
+  const desktop = await Bun.file(legacyDesktopEntry).text()
+  expect(desktop).toContain("Exec=/opt/OpenCode/ai.opencode.desktop %U")
+  expect(desktop).toContain("Icon=ai.opencode.desktop")
+  expect(desktop).toContain("StartupWMClass=ai.opencode.desktop")
+  expect(desktop).toContain("NoDisplay=true")
+})

+ 37 - 8
packages/desktop/electron-builder.config.ts

@@ -6,8 +6,14 @@ import { promisify } from "node:util"
 import type { Configuration } from "electron-builder"
 
 const execFileAsync = promisify(execFile)
-const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..")
+const packageDir = path.dirname(fileURLToPath(import.meta.url))
+const rootDir = path.resolve(packageDir, "../..")
 const signScript = path.join(rootDir, "script", "sign-windows.ps1")
+// The Electron 42 packaging update briefly installed Linux launchers/icons under
+// "opencode-desktop". Keep that hidden desktop entry around so existing GNOME/KDE
+// pins still resolve after the canonical app id changes back to ai.opencode.desktop.
+const legacyDesktopEntry = path.join(packageDir, "resources", "linux", "opencode-desktop.desktop")
+const legacyDesktopEntryFpm = `${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop`
 
 async function signWindows(configuration: { path: string }) {
   if (process.platform !== "win32") return
@@ -26,12 +32,26 @@ const channel = (() => {
   return "dev"
 })()
 
-const getBase = (): Configuration => ({
+const APP_IDS = {
+  dev: "ai.opencode.desktop.dev",
+  beta: "ai.opencode.desktop.beta",
+  prod: "ai.opencode.desktop",
+} as const
+
+const getBase = (appId: string): Configuration => ({
   artifactName: "opencode-desktop-${os}-${arch}.${ext}",
   directories: {
     output: "dist",
     buildResources: "resources",
   },
+  // Linux launchers are .desktop files, so this is the desktop file name,
+  // not just the app id. For prod, app id "ai.opencode.desktop" becomes
+  // "ai.opencode.desktop.desktop".
+  // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
+  // https://www.electron.build/docs/linux/
+  extraMetadata: {
+    desktopName: `${appId}.desktop`,
+  },
   files: ["out/**/*", "resources/**/*"],
   extraResources: [
     {
@@ -74,19 +94,27 @@ const getBase = (): Configuration => ({
   linux: {
     icon: `resources/icons`,
     category: "Development",
-    executableName: "opencode-desktop",
+    executableName: appId,
+    desktop: {
+      entry: {
+        // Match the installed .desktop file and hicolor icon basename so
+        // Linux shells can associate the running Electron window with its launcher.
+        StartupWMClass: appId,
+      },
+    },
     target: ["AppImage", "deb", "rpm"],
   },
 })
 
 function getConfig() {
-  const base = getBase()
+  const appId = APP_IDS[channel]
+  const base = getBase(appId)
 
   switch (channel) {
     case "dev": {
       return {
         ...base,
-        appId: "ai.opencode.desktop.dev",
+        appId,
         productName: "OpenCode Dev",
         rpm: { packageName: "opencode-dev" },
       }
@@ -94,7 +122,7 @@ function getConfig() {
     case "beta": {
       return {
         ...base,
-        appId: "ai.opencode.desktop.beta",
+        appId,
         productName: "OpenCode Beta",
         protocols: { name: "OpenCode Beta", schemes: ["opencode"] },
         publish: { provider: "github", owner: "anomalyco", repo: "opencode-beta", channel: "latest" },
@@ -104,11 +132,12 @@ function getConfig() {
     case "prod": {
       return {
         ...base,
-        appId: "ai.opencode.desktop",
+        appId,
         productName: "OpenCode",
         protocols: { name: "OpenCode", schemes: ["opencode"] },
         publish: { provider: "github", owner: "anomalyco", repo: "opencode", channel: "latest" },
-        rpm: { packageName: "opencode" },
+        deb: { fpm: [legacyDesktopEntryFpm] },
+        rpm: { packageName: "opencode", fpm: [legacyDesktopEntryFpm] },
       }
     }
   }

+ 10 - 0
packages/desktop/resources/linux/opencode-desktop.desktop

@@ -0,0 +1,10 @@
+[Desktop Entry]
+Name=OpenCode
+Exec=/opt/OpenCode/ai.opencode.desktop %U
+Terminal=false
+Type=Application
+Icon=ai.opencode.desktop
+StartupWMClass=ai.opencode.desktop
+NoDisplay=true
+Comment=Open source AI coding agent
+Categories=Development;