Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .github/workflows/build-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ jobs:
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
NEXT_PUBLIC_DOCS_URL: ${{ secrets.NEXT_PUBLIC_DOCS_URL }}
NEXT_PUBLIC_STREAMS_URL: ${{ secrets.NEXT_PUBLIC_STREAMS_URL }}
NEXT_PUBLIC_ELECTRIC_URL: ${{ secrets.NEXT_PUBLIC_ELECTRIC_URL }}
SENTRY_DSN_DESKTOP: ${{ secrets.SENTRY_DSN_DESKTOP }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SUPERSET_WORKSPACE_NAME: superset
Expand Down Expand Up @@ -234,3 +235,108 @@ jobs:
path: apps/desktop/release/*-linux.yml
retention-days: ${{ inputs.artifact_retention_days }}
if-no-files-found: error

build-windows:
name: Build - Windows (x64)
runs-on: windows-latest
environment: production

steps:
- name: Enable long paths
run: git config --system core.longPaths true

- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: "1.3.2"

- 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
shell: bash
run: bun install --frozen

- name: Set version suffix
if: inputs.version_suffix != ''
shell: bash
working-directory: apps/desktop
run: |
CURRENT_VERSION=$(node -p "require('./package.json').version")
NEW_VERSION="${CURRENT_VERSION}${{ inputs.version_suffix }}"
echo "Setting version to: $NEW_VERSION"
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
shell: bash
working-directory: apps/desktop
run: bun run clean:dev

- name: Compile app with electron-vite
shell: bash
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 }}
NEXT_PUBLIC_STREAMS_URL: ${{ secrets.NEXT_PUBLIC_STREAMS_URL }}
NEXT_PUBLIC_ELECTRIC_URL: ${{ secrets.NEXT_PUBLIC_ELECTRIC_URL }}
SENTRY_DSN_DESKTOP: ${{ secrets.SENTRY_DSN_DESKTOP }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SUPERSET_WORKSPACE_NAME: superset
run: bun run compile:app

- name: Build Electron app
shell: bash
working-directory: apps/desktop
run: bun run package -- --publish never --config ${{ inputs.electron_builder_config }}

- name: Verify Windows installer + update manifest exist
shell: bash
working-directory: apps/desktop
run: |
ls -la release
test -n "$(ls -1 release/*.exe 2>/dev/null)" || {
echo "::error::No .exe installer generated in apps/desktop/release"
exit 1
}
# electron-builder names the Windows manifest latest.yml (no -win suffix)
test -f "release/latest.yml" || {
echo "::error::No Windows auto-update manifest (latest.yml) generated in apps/desktop/release"
exit 1
}

- name: Upload EXE artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact_prefix }}-win-exe
path: apps/desktop/release/*.exe
retention-days: ${{ inputs.artifact_retention_days }}
if-no-files-found: error

- name: Upload Windows auto-update manifest
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact_prefix }}-win-update-manifest
path: apps/desktop/release/latest.yml
retention-days: ${{ inputs.artifact_retention_days }}
if-no-files-found: error
10 changes: 10 additions & 0 deletions .github/workflows/release-desktop-canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ jobs:
echo "Created canary copy: Superset-Canary-${arch}.AppImage"
fi
done
for file in *.exe; do
if [[ -f "$file" ]]; then
arch=$(echo "$file" | sed -E 's/.*-([^-]+)\.exe$/\1/')
cp "$file" "Superset-Canary-${arch}.exe"
echo "Created canary copy: Superset-Canary-${arch}.exe"
fi
done
# Prerelease builds may request canary-linux.yml; keep latest-linux.yml as fallback.
for file in *-linux.yml; do
if [[ -f "$file" && "$file" != "canary-linux.yml" && "$file" != "latest-linux.yml" ]]; then
Expand All @@ -115,6 +122,9 @@ jobs:
break
fi
done
# Windows manifest is named latest.yml by electron-builder (no -win suffix).
# With generateUpdatesFilesForAllChannels, canary.yml is also generated.
# Both are already correctly named for electron-updater.

- name: List artifacts
run: |
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ jobs:
echo "Created stable copy: Superset-${arch}.AppImage"
fi
done
for file in *.exe; do
if [[ -f "$file" ]]; then
# Extract architecture from filename (e.g., Superset-0.0.1-x64.exe -> x64)
arch=$(echo "$file" | sed -E 's/.*-([^-]+)\.exe$/\1/')
cp "$file" "Superset-${arch}.exe"
echo "Created stable copy: Superset-${arch}.exe"
fi
done
# Keep Linux updater manifest at a stable filename for generic provider lookups.
for file in *-linux.yml; do
if [[ -f "$file" && "$file" != "latest-linux.yml" ]]; then
Expand All @@ -94,6 +102,8 @@ jobs:
break
fi
done
# Windows manifest is already named latest.yml by electron-builder
# (no -win suffix, unlike macOS/Linux). No stable copy needed.
echo "Release artifacts:"
ls -la

Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
defineEnv,
devPath,
htmlEnvTransformPlugin,
stripCrossOriginPlugin,
} from "./vite/helpers";

// override: true ensures .env values take precedence over inherited env vars
Expand Down Expand Up @@ -223,6 +224,7 @@ export default defineConfig({
}),
reactPlugin(),
htmlEnvTransformPlugin(),
stripCrossOriginPlugin(),
],

worker: {
Expand Down
9 changes: 7 additions & 2 deletions apps/desktop/scripts/copy-native-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,13 @@ function copyModuleIfSymlink(
console.log(` ${moduleName}: symlink -> replacing with real files`);
console.log(` Real path: ${realPath}`);

// Remove the symlink
rmSync(modulePath);
// Windows uses junctions (directory-like) instead of symlinks;
// rmSync needs { recursive: true } to remove them.
if (process.platform === "win32") {
rmSync(modulePath, { recursive: true });
} else {
rmSync(modulePath);
}

// Copy the actual files
cpSync(realPath, modulePath, { recursive: true });
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/lib/electron-app/factories/app/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ export async function makeAppSetup(
return window;
}

PLATFORM.IS_LINUX && app.disableHardwareAcceleration();
// Disable GPU hardware acceleration on Linux and Windows to prevent black/blank
// screens caused by GPU driver incompatibilities with Chromium's compositor.
// macOS uses a separate set of GPU workarounds (see below and MainWindow).
(PLATFORM.IS_LINUX || PLATFORM.IS_WINDOWS) && app.disableHardwareAcceleration();

// macOS Sequoia+: occluded window throttling can corrupt GPU compositor layers
if (PLATFORM.IS_MAC) {
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/src/main/windows/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,23 @@ export async function MainWindow() {
});
}

// Forward renderer warning/error messages to main process stdout for Windows debugging.
// Run the packaged app from a terminal to see these messages.
if (PLATFORM.IS_WINDOWS) {
window.webContents.on(
"console-message",
(_event, level, message, line, sourceId) => {
if (level < 2) return;
const levelStr =
["verbose", "info", "warning", "error"][level] ?? "unknown";
const source = sourceId ? ` (${sourceId}:${line})` : "";
const formatted = `[renderer:${levelStr}] ${message}${source}`;
if (level === 3) console.error(formatted);
else console.warn(formatted);
},
);
}

window.webContents.on("did-finish-load", async () => {
console.log("[main-window] Renderer loaded successfully");
if (initialBounds.isMaximized) {
Expand Down
19 changes: 19 additions & 0 deletions apps/desktop/vite/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,25 @@ export function copyResourcesPlugin(): Plugin {
};
}

/**
* Strips the `crossorigin` attribute that Vite adds to script/link tags.
* Windows-only: Electron's ASAR file:// handler doesn't support CORS on
* Windows, so crossorigin causes scripts/styles to silently fail to load
* (black screen). macOS and Linux are unaffected.
*/
export function stripCrossOriginPlugin(): Plugin {
return {
name: "strip-crossorigin",
transformIndexHtml: {
order: "post",
handler(html) {
if (process.platform !== "win32") return html;
return html.replace(/ crossorigin(?:="[^"]*")?/g, "");
},
},
};
}
Comment on lines +71 to +82

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

process.platform is evaluated at build time — cross-compilation silently skips the fix.

process.platform !== "win32" runs in the Vite/Node.js process during the build, not inside the packaged Electron app. This works correctly when the CI runner is windows-latest, but if a Windows artifact is ever produced from a macOS or Linux host (cross-compilation), the crossorigin stripping will be silently skipped, causing the black-screen regression to reappear with no indication.

Consider asserting the intent in a comment, or pass the target platform explicitly via an env var so the build graph doesn't couple correctness to the build host:

💡 Proposed hardening
 export function stripCrossOriginPlugin(): Plugin {
 	return {
 		name: "strip-crossorigin",
 		transformIndexHtml: {
 			order: "post",
 			handler(html) {
-				if (process.platform !== "win32") return html;
+				// NOTE: evaluated at Vite build time, not Electron runtime.
+				// For cross-compilation, set TARGET_PLATFORM=win32 explicitly.
+				const platform = process.env.TARGET_PLATFORM ?? process.platform;
+				if (platform !== "win32") return html;
 				return html.replace(/ crossorigin(?:="[^"]*")?/g, "");
 			},
 		},
 	};
 }

The CI workflow would then pass TARGET_PLATFORM=win32 when invoking the Windows build step, making the condition explicit and immune to the build-host assumption.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function stripCrossOriginPlugin(): Plugin {
return {
name: "strip-crossorigin",
transformIndexHtml: {
order: "post",
handler(html) {
if (process.platform !== "win32") return html;
return html.replace(/ crossorigin(?:="[^"]*")?/g, "");
},
},
};
}
export function stripCrossOriginPlugin(): Plugin {
return {
name: "strip-crossorigin",
transformIndexHtml: {
order: "post",
handler(html) {
// NOTE: evaluated at Vite build time, not Electron runtime.
// For cross-compilation, set TARGET_PLATFORM=win32 explicitly.
const platform = process.env.TARGET_PLATFORM ?? process.platform;
if (platform !== "win32") return html;
return html.replace(/ crossorigin(?:="[^"]*")?/g, "");
},
},
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/vite/helpers.ts` around lines 71 - 82, The current
stripCrossOriginPlugin uses process.platform inside transformIndexHtml (handler)
which is evaluated at build time and can be wrong for cross-compilation; change
the check to use an explicit build-target env var (e.g.,
process.env.TARGET_PLATFORM or VITE_TARGET_PLATFORM) instead of process.platform
in the stripCrossOriginPlugin/transformIndexHtml handler, and document the
intent in a comment so CI can pass TARGET_PLATFORM=win32 for Windows artifacts;
ensure the handler falls back safely (e.g., treat undefined as non-Windows) so
behavior is deterministic and not tied to the build host.


/**
* Injects environment variables into index.html CSP.
*/
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"format:check": "biome format .",
"typecheck": "turbo typecheck",
"ui-add": "turbo run ui-add",
"postinstall": "./scripts/postinstall.sh",
"postinstall": "node scripts/postinstall.mjs",
"clean": "git clean -xdf node_modules",
"clean:workspaces": "turbo clean",
"release:desktop": "./apps/desktop/create-release.sh",
Expand Down
31 changes: 31 additions & 0 deletions scripts/postinstall.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Cross-platform postinstall script.
*
* Replaces the bash-only postinstall.sh so that `bun install` works on
* Windows, macOS and Linux without special flags.
*
* Steps:
* 1. Guard against infinite recursion (electron-builder install-app-deps
* can trigger nested bun installs which would re-run this script).
* 2. Run sherif for workspace validation.
* 3. Install native dependencies for the desktop app.
*/

import { execSync } from "node:child_process";

// Prevent infinite recursion during postinstall
if (process.env.SUPERSET_POSTINSTALL_RUNNING) {
process.exit(0);
}
process.env.SUPERSET_POSTINSTALL_RUNNING = "1";

/** Run a command, inheriting stdio so output is visible. */
function run(cmd) {
execSync(cmd, { stdio: "inherit" });
}

// Run sherif for workspace validation
run("sherif");

// Install native dependencies for desktop app
run("bun run --filter=@superset/desktop install:deps");