From ffedb2f615b7a19de476be722946f3795b3eda25 Mon Sep 17 00:00:00 2001 From: Test User Date: Wed, 4 Feb 2026 09:20:12 -0800 Subject: [PATCH 1/6] feat(desktop): add Linux AppImage/deb build support Add Linux build job to CI workflow (ubuntu-latest, x64) with AppImage and deb targets. Update release workflow to create stable-named Linux artifacts and fix shell fallback from /bin/zsh to /bin/sh for cross-platform compatibility. --- .github/workflows/build-desktop.yml | 97 +++++++++++++++++++ .github/workflows/release-desktop.yml | 25 +++++ apps/desktop/create-release.sh | 6 +- .../desktop/src/main/terminal-host/session.ts | 2 +- 4 files changed, 128 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index c811334bcaa..c78822bacd8 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -128,3 +128,100 @@ jobs: path: apps/desktop/release/*-mac.yml retention-days: ${{ inputs.artifact_retention_days }} if-no-files-found: error + + build-linux: + name: Build - Linux (${{ matrix.arch }}) + runs-on: ubuntu-latest + environment: production + + strategy: + fail-fast: false + matrix: + arch: [x64] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: "1.3.2" + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libarchive-tools rpm + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + run: bun install --frozen + + - name: Set version suffix + if: inputs.version_suffix != '' + working-directory: apps/desktop + run: | + # Read current version and append suffix + CURRENT_VERSION=$(node -p "require('./package.json').version") + NEW_VERSION="${CURRENT_VERSION}${{ inputs.version_suffix }}" + echo "Setting version to: $NEW_VERSION" + # Update package.json version using node + node -e " + const fs = require('fs'); + const pkg = require('./package.json'); + pkg.version = '$NEW_VERSION'; + fs.writeFileSync('./package.json', JSON.stringify(pkg, null, '\t') + '\n'); + " + echo "Updated package.json version to $NEW_VERSION" + + - name: Clean dev folder + working-directory: apps/desktop + run: bun run clean:dev + + - name: Compile app with electron-vite + working-directory: apps/desktop + env: + NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} + NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + GH_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }} + NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }} + NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} + NEXT_PUBLIC_DOCS_URL: ${{ secrets.NEXT_PUBLIC_DOCS_URL }} + SENTRY_DSN_DESKTOP: ${{ secrets.SENTRY_DSN_DESKTOP }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: bun run compile:app + + - name: Build Electron app + working-directory: apps/desktop + run: bun run package -- --publish never --config ${{ inputs.electron_builder_config }} --linux + + - name: Upload AppImage artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact_prefix }}-linux-${{ matrix.arch }}-appimage + path: apps/desktop/release/*.AppImage + retention-days: ${{ inputs.artifact_retention_days }} + if-no-files-found: error + + - name: Upload deb artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact_prefix }}-linux-${{ matrix.arch }}-deb + path: apps/desktop/release/*.deb + retention-days: ${{ inputs.artifact_retention_days }} + if-no-files-found: error + + - name: Upload Linux auto-update manifest + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact_prefix }}-linux-update-manifest + path: apps/desktop/release/*-linux.yml + retention-days: ${{ inputs.artifact_retention_days }} + if-no-files-found: error diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 6858fabf5de..c12c843953c 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -62,6 +62,8 @@ jobs: run: | cd release-artifacts # Create stable-named copies (without version) for /releases/latest/download/ URLs + + # macOS DMG files for file in *.dmg; do if [[ -f "$file" ]]; then # Extract architecture from filename (e.g., Superset-0.0.1-arm64.dmg -> arm64) @@ -70,6 +72,8 @@ jobs: echo "Created stable copy: Superset-${arch}.dmg" fi done + + # macOS ZIP files for file in *-mac.zip; do if [[ -f "$file" ]]; then # Extract architecture from filename (e.g., Superset-0.0.1-arm64-mac.zip -> arm64) @@ -78,6 +82,27 @@ jobs: echo "Created stable copy: Superset-${arch}-mac.zip" fi done + + # Linux AppImage files + for file in *.AppImage; do + if [[ -f "$file" ]]; then + # Extract architecture from filename (e.g., superset-0.0.1-x64.AppImage -> x64) + arch=$(echo "$file" | sed -E 's/.*-([^-]+)\.AppImage$/\1/') + cp "$file" "Superset-${arch}.AppImage" + echo "Created stable copy: Superset-${arch}.AppImage" + fi + done + + # Linux DEB files + for file in *.deb; do + if [[ -f "$file" ]]; then + # Extract architecture from filename (e.g., superset-0.0.1-amd64.deb -> amd64) + arch=$(echo "$file" | sed -E 's/.*-([^-]+)\.deb$/\1/') + cp "$file" "Superset-${arch}.deb" + echo "Created stable copy: Superset-${arch}.deb" + fi + done + echo "Release artifacts:" ls -la diff --git a/apps/desktop/create-release.sh b/apps/desktop/create-release.sh index 142e18c12ae..890ff39ee0c 100755 --- a/apps/desktop/create-release.sh +++ b/apps/desktop/create-release.sh @@ -438,7 +438,11 @@ else echo -e "${BLUE}Latest URL:${NC} ${LATEST_URL}" echo "" echo -e "${BLUE}Direct download:${NC}" - echo " • ${LATEST_URL}/download/Superset-arm64.dmg" + echo " macOS:" + echo " • ${LATEST_URL}/download/Superset-arm64.dmg" + echo " Linux:" + echo " • ${LATEST_URL}/download/Superset-x64.AppImage" + echo " • ${LATEST_URL}/download/Superset-amd64.deb" echo "" else success "Draft release created!" diff --git a/apps/desktop/src/main/terminal-host/session.ts b/apps/desktop/src/main/terminal-host/session.ts index fd0bf30143f..ca3b4ee2d63 100644 --- a/apps/desktop/src/main/terminal-host/session.ts +++ b/apps/desktop/src/main/terminal-host/session.ts @@ -940,7 +940,7 @@ export class Session { if (process.platform === "win32") { return process.env.COMSPEC || "cmd.exe"; } - return process.env.SHELL || "/bin/zsh"; + return process.env.SHELL || "/bin/sh"; } /** From b7756068a839aa0172021f54815d4fdc91660a09 Mon Sep 17 00:00:00 2001 From: Test User Date: Wed, 4 Feb 2026 09:25:04 -0800 Subject: [PATCH 2/6] fix(desktop): resolve circular dependency in Paywall module Move GATED_FEATURES and GatedFeature type from usePaywall.ts to constants.ts to break the circular import chain that caused "Cannot access 'GATED_FEATURES' before initialization" at runtime. --- .../src/renderer/components/Paywall/constants.ts | 12 ++++++++++-- .../desktop/src/renderer/components/Paywall/index.ts | 5 +++-- .../src/renderer/components/Paywall/usePaywall.ts | 11 ++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/renderer/components/Paywall/constants.ts b/apps/desktop/src/renderer/components/Paywall/constants.ts index bf51ac20efd..2dac4cebb40 100644 --- a/apps/desktop/src/renderer/components/Paywall/constants.ts +++ b/apps/desktop/src/renderer/components/Paywall/constants.ts @@ -6,8 +6,16 @@ import { HiOutlinePuzzlePiece, HiUsers, } from "react-icons/hi2"; -import type { GatedFeature } from "./usePaywall"; -import { GATED_FEATURES } from "./usePaywall"; + +export const GATED_FEATURES = { + INVITE_MEMBERS: "invite-members", + INTEGRATIONS: "integrations", + TASKS: "tasks", + CLOUD_WORKSPACES: "cloud-workspaces", + MOBILE_APP: "mobile-app", +} as const; + +export type GatedFeature = (typeof GATED_FEATURES)[keyof typeof GATED_FEATURES]; export interface ProFeature { id: string; diff --git a/apps/desktop/src/renderer/components/Paywall/index.ts b/apps/desktop/src/renderer/components/Paywall/index.ts index 519461e781e..9dce04783d1 100644 --- a/apps/desktop/src/renderer/components/Paywall/index.ts +++ b/apps/desktop/src/renderer/components/Paywall/index.ts @@ -1,3 +1,4 @@ export { Paywall, paywall } from "./Paywall"; -export type { GatedFeature } from "./usePaywall"; -export { GATED_FEATURES, usePaywall } from "./usePaywall"; +export type { GatedFeature } from "./constants"; +export { GATED_FEATURES } from "./constants"; +export { usePaywall } from "./usePaywall"; diff --git a/apps/desktop/src/renderer/components/Paywall/usePaywall.ts b/apps/desktop/src/renderer/components/Paywall/usePaywall.ts index 575bc8bd5cd..98cbf86e695 100644 --- a/apps/desktop/src/renderer/components/Paywall/usePaywall.ts +++ b/apps/desktop/src/renderer/components/Paywall/usePaywall.ts @@ -1,17 +1,10 @@ import { authClient } from "renderer/lib/auth-client"; +import { type GatedFeature, GATED_FEATURES } from "./constants"; import { paywall } from "./Paywall"; type UserPlan = "free" | "pro"; -export const GATED_FEATURES = { - INVITE_MEMBERS: "invite-members", - INTEGRATIONS: "integrations", - TASKS: "tasks", - CLOUD_WORKSPACES: "cloud-workspaces", - MOBILE_APP: "mobile-app", -} as const; - -export type GatedFeature = (typeof GATED_FEATURES)[keyof typeof GATED_FEATURES]; +export { type GatedFeature, GATED_FEATURES }; export function usePaywall() { const { data: session } = authClient.useSession(); From a70f3f23d9d7bf982f14c5b2906707ea549b056e Mon Sep 17 00:00:00 2001 From: Test User Date: Wed, 4 Feb 2026 09:28:37 -0800 Subject: [PATCH 3/6] perf(desktop): enable GPU acceleration on Linux Remove blanket disableHardwareAcceleration() that forced CPU-only rendering on Linux. Add GPU optimization flags (gpu-rasterization, zero-copy, ignore-gpu-blocklist) and provide SUPERSET_DISABLE_GPU=1 escape hatch for users with problematic drivers. --- .../src/lib/electron-app/factories/app/setup.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/electron-app/factories/app/setup.ts b/apps/desktop/src/lib/electron-app/factories/app/setup.ts index 5ac810ec67f..5e5e93a2603 100644 --- a/apps/desktop/src/lib/electron-app/factories/app/setup.ts +++ b/apps/desktop/src/lib/electron-app/factories/app/setup.ts @@ -69,7 +69,17 @@ export async function makeAppSetup( return window; } -PLATFORM.IS_LINUX && app.disableHardwareAcceleration(); +// Allow users with problematic GPU drivers to disable hardware acceleration +if (process.env.SUPERSET_DISABLE_GPU === "1") { + app.disableHardwareAcceleration(); +} + +// Enable GPU optimizations on Linux +if (PLATFORM.IS_LINUX) { + app.commandLine.appendSwitch("enable-gpu-rasterization"); + app.commandLine.appendSwitch("enable-zero-copy"); + app.commandLine.appendSwitch("ignore-gpu-blocklist"); +} PLATFORM.IS_WINDOWS && app.setAppUserModelId( From 667bc8904383f389f5ab4f9404f19f7e0f8e9a7c Mon Sep 17 00:00:00 2001 From: Test User Date: Wed, 4 Feb 2026 09:34:58 -0800 Subject: [PATCH 4/6] fix(desktop): fix window controls, open command, and resize on Linux - Add no-drag class to WindowControls so buttons are clickable - Add Linux CLI command map for external apps (fixes spawn open ENOENT) - Make trafficLightPosition macOS-only to avoid warnings on Linux --- .../src/lib/trpc/routers/external/helpers.ts | 48 +++++++++++++++++-- apps/desktop/src/main/windows/main.ts | 4 +- .../WindowControls/WindowControls.tsx | 2 +- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/external/helpers.ts b/apps/desktop/src/lib/trpc/routers/external/helpers.ts index 6e8a09d5c0a..14c93b04ac5 100644 --- a/apps/desktop/src/lib/trpc/routers/external/helpers.ts +++ b/apps/desktop/src/lib/trpc/routers/external/helpers.ts @@ -3,7 +3,7 @@ import nodePath from "node:path"; import { EXTERNAL_APPS, type ExternalApp } from "@superset/local-db"; /** Map of app IDs to their macOS application names */ -const APP_NAMES: Record = { +const MAC_APP_NAMES: Record = { finder: null, // Handled specially with shell.showItemInFolder vscode: "Visual Studio Code", "vscode-insiders": "Visual Studio Code - Insiders", @@ -29,17 +29,57 @@ const APP_NAMES: Record = { rustrover: "RustRover", }; +/** Map of app IDs to their Linux CLI commands */ +const LINUX_APP_COMMANDS: Record = { + finder: null, + vscode: "code", + "vscode-insiders": "code-insiders", + cursor: "cursor", + zed: "zed", + xcode: null, // macOS only + iterm: null, // macOS only + warp: null, // macOS only + terminal: "x-terminal-emulator", + ghostty: "ghostty", + sublime: "subl", + intellij: "idea", + webstorm: "webstorm", + pycharm: "pycharm", + phpstorm: "phpstorm", + rubymine: "rubymine", + goland: "goland", + clion: "clion", + rider: "rider", + datagrip: "datagrip", + appcode: null, // macOS only + fleet: "fleet", + rustrover: "rustrover", +}; + /** * Get the command and args to open a path in the specified app. - * Uses `open -a` for macOS apps to avoid PATH issues in production builds. + * Uses `open -a` on macOS, direct CLI commands on Linux. */ export function getAppCommand( app: ExternalApp, targetPath: string, ): { command: string; args: string[] } | null { - const appName = APP_NAMES[app]; + if (process.platform === "darwin") { + const appName = MAC_APP_NAMES[app]; + if (!appName) return null; + return { command: "open", args: ["-a", appName, targetPath] }; + } + + if (process.platform === "linux") { + const command = LINUX_APP_COMMANDS[app]; + if (!command) return null; + return { command, args: [targetPath] }; + } + + // Windows: use start command + const appName = MAC_APP_NAMES[app]; if (!appName) return null; - return { command: "open", args: ["-a", appName, targetPath] }; + return { command: "cmd", args: ["/c", "start", "", appName, targetPath] }; } /** diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index a5f2fb7aaaf..0a9ab57fd09 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -82,7 +82,9 @@ export async function MainWindow() { autoHideMenuBar: true, frame: false, titleBarStyle: "hidden", - trafficLightPosition: { x: 16, y: 16 }, + ...(process.platform === "darwin" + ? { trafficLightPosition: { x: 16, y: 16 } } + : {}), webPreferences: { preload: join(__dirname, "../preload/index.js"), webviewTag: true, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/WindowControls/WindowControls.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/WindowControls/WindowControls.tsx index a7f0a36fb12..2c1c4300619 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/WindowControls/WindowControls.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/WindowControls/WindowControls.tsx @@ -19,7 +19,7 @@ export function WindowControls() { }; return ( -
+