From 632611730de6b4ce8b02696ed2240bd7eedbd5b1 Mon Sep 17 00:00:00 2001 From: Tom Almeida Date: Thu, 7 May 2026 16:29:46 +1000 Subject: [PATCH] fix(nix): build desktop package with electron The desktop package no longer has the Tauri src-tauri tree, so the Nix desktop derivation packages the Electron app instead of using cargo-tauri. The Electron renderer and main outputs are built with the shared node_modules derivation, and the unpacked app uses Nix-provided Electron to avoid sandbox downloads. Electron auto-updates are disabled for Nix builds, the derivation version is preserved in Electron metadata, the app runtime has ripgrep on PATH, and the unpacked Linux app includes desktop integration. Fixes #26120 --- nix/desktop.nix | 176 +++++++++++++++-------- packages/desktop/electron.vite.config.ts | 1 + packages/desktop/src/main/constants.ts | 2 +- packages/desktop/src/main/env.d.ts | 1 + 4 files changed, 119 insertions(+), 61 deletions(-) diff --git a/nix/desktop.nix b/nix/desktop.nix index efdc2bd72e29..80f285b81caf 100644 --- a/nix/desktop.nix +++ b/nix/desktop.nix @@ -1,29 +1,46 @@ { lib, - stdenv, - rustPlatform, - pkg-config, - cargo-tauri, + stdenvNoCC, bun, nodejs, - cargo, - rustc, - jq, - wrapGAppsHook4, + electron, makeWrapper, - dbus, - glib, - gtk4, - libsoup_3, - librsvg, - libappindicator, - glib-networking, - openssl, - webkitgtk_4_1, - gst_all_1, + makeDesktopItem, + ripgrep, + writeText, opencode, }: -rustPlatform.buildRustPackage (finalAttrs: { +let + desktopItem = makeDesktopItem { + name = "opencode-desktop"; + desktopName = "OpenCode"; + comment = "OpenCode Desktop App"; + exec = "opencode-desktop"; + icon = "opencode-desktop"; + categories = [ "Development" ]; + startupWMClass = "OpenCode"; + }; + + # electron-builder mutates helper Info.plist files after copying Electron.app. + # Nix's Electron app is read-only, so make the copied bundle writable first. + darwinAfterExtract = writeText "opencode-desktop-after-extract.cjs" '' + const fs = require("node:fs/promises") + const path = require("node:path") + + async function chmodWritable(file) { + const stat = await fs.lstat(file) + await fs.chmod(file, stat.mode | 0o200) + if (!stat.isDirectory()) return + await Promise.all((await fs.readdir(file)).map((entry) => chmodWritable(path.join(file, entry)))) + } + + module.exports = async (context) => { + if (context.electronPlatformName !== "darwin") return + await chmodWritable(path.join(context.appOutDir, "Electron.app")) + } + ''; +in +stdenvNoCC.mkDerivation (finalAttrs: { pname = "opencode-desktop"; inherit (opencode) version @@ -32,62 +49,101 @@ rustPlatform.buildRustPackage (finalAttrs: { patches ; - cargoRoot = "packages/desktop/src-tauri"; - cargoLock.lockFile = ../packages/desktop/src-tauri/Cargo.lock; - buildAndTestSubdir = finalAttrs.cargoRoot; - nativeBuildInputs = [ - pkg-config - cargo-tauri.hook bun nodejs # for patchShebangs node_modules - cargo - rustc - jq makeWrapper - ] ++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ]; - - buildInputs = lib.optionals stdenv.isLinux [ - dbus - glib - gtk4 - libsoup_3 - librsvg - libappindicator - glib-networking - openssl - webkitgtk_4_1 - gst_all_1.gstreamer - gst_all_1.gst-plugins-base - gst_all_1.gst-plugins-good - gst_all_1.gst-plugins-bad ]; - strictDeps = true; + configurePhase = '' + runHook preConfigure - preBuild = '' - cp -a ${finalAttrs.node_modules}/{node_modules,packages} . + cp -R ${finalAttrs.node_modules}/. . chmod -R u+w node_modules packages patchShebangs node_modules - patchShebangs packages/desktop/node_modules + patchShebangs packages/*/node_modules - mkdir -p packages/desktop/src-tauri/sidecars - cp ${opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-${stdenv.hostPlatform.rust.rustcTarget} + runHook postConfigure ''; - # see publish-tauri job in .github/workflows/publish.yml - tauriBuildFlags = [ + env.OPENCODE_CHANNEL = "prod"; + env.OPENCODE_DISABLE_UPDATER = "true"; + env.OPENCODE_DISABLE_MODELS_FETCH = opencode.OPENCODE_DISABLE_MODELS_FETCH; + env.MODELS_DEV_API_JSON = opencode.MODELS_DEV_API_JSON; + env.OPENCODE_VERSION = finalAttrs.version; + + runtimePath = lib.makeBinPath [ ripgrep ]; + + electronBuilderFlags = [ "--config" - "tauri.prod.conf.json" - "--no-sign" # no code signing or auto updates + "electron-builder.config.ts" + "--config.extraMetadata.version=${finalAttrs.version}" + "--config.electronVersion=${electron.version}" ]; - # FIXME: workaround for concerns about case insensitive filesystems - # should be removed once binary is renamed or decided otherwise - # darwin output is a .app bundle so no conflict - postFixup = lib.optionalString stdenv.hostPlatform.isLinux '' - mv $out/bin/OpenCode $out/bin/opencode-desktop - sed -i 's|^Exec=OpenCode$|Exec=opencode-desktop|' $out/share/applications/OpenCode.desktop + buildPhase = '' + runHook preBuild + + cd packages/desktop + bun ./scripts/prebuild.ts + bun run build + '' + + lib.optionalString stdenvNoCC.hostPlatform.isDarwin '' + bunx electron-builder --mac dir ${ + lib.escapeShellArgs ( + finalAttrs.electronBuilderFlags + ++ [ + "--config.electronDist=${electron}/Applications" + "--config.afterExtract=${darwinAfterExtract}" + "--config.mac.identity=null" + "--config.mac.hardenedRuntime=false" + "--config.mac.notarize=false" + ] + ) + } + '' + + lib.optionalString stdenvNoCC.hostPlatform.isLinux '' + bunx electron-builder --linux dir ${ + lib.escapeShellArgs ( + finalAttrs.electronBuilderFlags + ++ [ + "--config.electronDist=${electron}/libexec/electron" + "--config.linux.executableName=opencode" + ] + ) + } + '' + + '' + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + '' + + lib.optionalString stdenvNoCC.hostPlatform.isDarwin '' + mkdir -p $out/Applications $out/bin + cp -R dist/mac*/OpenCode.app $out/Applications/ + wrapProgram $out/Applications/OpenCode.app/Contents/MacOS/OpenCode \ + --prefix PATH : ${finalAttrs.runtimePath} + makeWrapper $out/Applications/OpenCode.app/Contents/MacOS/OpenCode $out/bin/opencode-desktop + '' + + lib.optionalString stdenvNoCC.hostPlatform.isLinux '' + mkdir -p $out/share/opencode-desktop $out/bin + cp -R dist/linux-unpacked/. $out/share/opencode-desktop/ + makeWrapper $out/share/opencode-desktop/opencode $out/bin/opencode-desktop \ + --prefix PATH : ${finalAttrs.runtimePath} + + install -Dm644 resources/icons/32x32.png $out/share/icons/hicolor/32x32/apps/opencode-desktop.png + install -Dm644 resources/icons/64x64.png $out/share/icons/hicolor/64x64/apps/opencode-desktop.png + install -Dm644 resources/icons/128x128.png $out/share/icons/hicolor/128x128/apps/opencode-desktop.png + install -Dm644 resources/icons/128x128@2x.png $out/share/icons/hicolor/256x256/apps/opencode-desktop.png + cp -R ${desktopItem}/share/applications $out/share/ + '' + + '' + + runHook postInstall ''; meta = { diff --git a/packages/desktop/electron.vite.config.ts b/packages/desktop/electron.vite.config.ts index a352e03fdd4a..076b4d4d382e 100644 --- a/packages/desktop/electron.vite.config.ts +++ b/packages/desktop/electron.vite.config.ts @@ -34,6 +34,7 @@ export default defineConfig({ main: { define: { "import.meta.env.OPENCODE_CHANNEL": JSON.stringify(channel), + "import.meta.env.OPENCODE_DISABLE_UPDATER": JSON.stringify(process.env.OPENCODE_DISABLE_UPDATER === "true"), }, build: { rollupOptions: { diff --git a/packages/desktop/src/main/constants.ts b/packages/desktop/src/main/constants.ts index 1e21661c1ad8..53fad42c175e 100644 --- a/packages/desktop/src/main/constants.ts +++ b/packages/desktop/src/main/constants.ts @@ -7,4 +7,4 @@ export const CHANNEL: Channel = raw === "dev" || raw === "beta" || raw === "prod export const SETTINGS_STORE = "opencode.settings" export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl" export const WSL_ENABLED_KEY = "wslEnabled" -export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev" +export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev" && !import.meta.env.OPENCODE_DISABLE_UPDATER diff --git a/packages/desktop/src/main/env.d.ts b/packages/desktop/src/main/env.d.ts index 1de56e1c9048..8f459e2810a3 100644 --- a/packages/desktop/src/main/env.d.ts +++ b/packages/desktop/src/main/env.d.ts @@ -1,5 +1,6 @@ interface ImportMetaEnv { readonly OPENCODE_CHANNEL: string + readonly OPENCODE_DISABLE_UPDATER: boolean } interface ImportMeta {