From 9c3c0f9140ef0fa75f67f4f70e5b9b63fe0c8fac Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 30 Apr 2026 11:39:56 +0700 Subject: [PATCH] feat(desktop): build Windows arm64 alongside x64 Multi-arch Windows build in one electron-builder pass, with the website self-healing across the rename. Asset names go from `Paseo-Setup-${version}.exe` to `Paseo-Setup-${version}-{x64,arm64}.exe`. The website now reads the actual installer filename off the latest GitHub release, so legacy single-arch releases keep rendering a single "Download" pill while dual-arch releases promote to "Intel / x64" + "ARM64" automatically. No homepage code change needed when ARM ships. --- .github/workflows/desktop-release.yml | 2 +- packages/desktop/electron-builder.yml | 11 ++++- .../website/src/components/landing-page.tsx | 4 +- packages/website/src/downloads.tsx | 18 +++++--- packages/website/src/release.ts | 43 ++++++++++++++++--- packages/website/src/routes/__root.tsx | 8 +++- packages/website/src/routes/download.tsx | 11 +++-- 7 files changed, 77 insertions(+), 20 deletions(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 38a82462d..8f21de65b 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -355,7 +355,7 @@ jobs: PASEO_DESKTOP_SMOKE: "1" run: | set -euo pipefail - build_args=(-- --publish never --win --x64) + build_args=(-- --publish never --win --x64 --arm64) build_args+=("-c.publish.releaseType=$RELEASE_TYPE") build_args+=("-c.publish.channel=$RELEASE_CHANNEL") npm run build --workspace="$DESKTOP_WORKSPACE" "${build_args[@]}" diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml index 6263ea2cb..4177e11d0 100644 --- a/packages/desktop/electron-builder.yml +++ b/packages/desktop/electron-builder.yml @@ -49,13 +49,20 @@ linux: - rpm - tar.gz win: + artifactName: "Paseo-Setup-${version}-${arch}.${ext}" icon: assets/icon.ico extraResources: - from: bin/paseo.cmd to: bin/paseo.cmd target: - - nsis - - zip + - target: nsis + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 nsis: oneClick: false perMachine: false diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index b1468916c..bbc276e25 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -1030,9 +1030,9 @@ function GetStarted() { } function DownloadButton() { - const { version } = useRelease(); + const release = useRelease(); const detectedPlatform = useDetectedPlatform(); - const primary = getDownloadOptions(version).find((o) => o.platform === detectedPlatform)!; + const primary = getDownloadOptions(release).find((o) => o.platform === detectedPlatform)!; const PrimaryIcon = primary.icon; return ( diff --git a/packages/website/src/downloads.tsx b/packages/website/src/downloads.tsx index aae15a88d..0583ad46e 100644 --- a/packages/website/src/downloads.tsx +++ b/packages/website/src/downloads.tsx @@ -4,7 +4,14 @@ export function releaseBase(version: string) { return `https://github.com/getpaseo/paseo/releases/download/v${version}`; } -export function downloadUrls(version: string) { +export interface ReleaseAssetInfo { + version: string; + windowsX64Asset: string | null; + windowsArm64Asset: string | null; +} + +export function downloadUrls(release: ReleaseAssetInfo) { + const { version, windowsX64Asset, windowsArm64Asset } = release; const base = releaseBase(version); return { macAppleSilicon: `${base}/Paseo-${version}-arm64.dmg`, @@ -12,7 +19,8 @@ export function downloadUrls(version: string) { linuxAppImage: `${base}/Paseo-${version}-x86_64.AppImage`, linuxDeb: `${base}/Paseo-${version}-amd64.deb`, linuxRpm: `${base}/Paseo-${version}-x86_64.rpm`, - windowsExe: `${base}/Paseo-Setup-${version}.exe`, + windowsExeX64: `${base}/${windowsX64Asset ?? `Paseo-Setup-${version}.exe`}`, + windowsExeArm64: windowsArm64Asset ? `${base}/${windowsArm64Asset}` : null, androidApk: `${base}/paseo-v${version}-android.apk`, }; } @@ -30,8 +38,8 @@ export interface DownloadOption { icon: (props: React.SVGProps) => React.ReactElement; } -export function getDownloadOptions(version: string): DownloadOption[] { - const urls = downloadUrls(version); +export function getDownloadOptions(release: ReleaseAssetInfo): DownloadOption[] { + const urls = downloadUrls(release); return [ { platform: "mac-silicon", @@ -48,7 +56,7 @@ export function getDownloadOptions(version: string): DownloadOption[] { { platform: "windows", label: "Windows", - href: urls.windowsExe, + href: urls.windowsExeX64, icon: WindowsIcon, }, { diff --git a/packages/website/src/release.ts b/packages/website/src/release.ts index 113eb0d5a..1ecb8c801 100644 --- a/packages/website/src/release.ts +++ b/packages/website/src/release.ts @@ -15,7 +15,7 @@ interface GitHubRelease { const REQUIRED_ASSET_PATTERNS = [ /Paseo-.*-arm64\.dmg$/, // Mac Apple Silicon /Paseo-.*-x86_64\.AppImage$/, // Linux AppImage - /Paseo-Setup-.*\.exe$/, // Windows + /Paseo-Setup-.*\.exe$/, // Windows (any arch) ]; function hasRequiredAssets(release: GitHubRelease): boolean { @@ -24,14 +24,40 @@ function hasRequiredAssets(release: GitHubRelease): boolean { ); } +function pickWindowsAssets(assets: GitHubAsset[]) { + const x64Suffixed = assets.find((a) => /Paseo-Setup-.*-x64\.exe$/.test(a.name)); + const arm64 = assets.find((a) => /Paseo-Setup-.*-arm64\.exe$/.test(a.name)); + const legacy = assets.find( + (a) => + /Paseo-Setup-.*\.exe$/.test(a.name) && + !a.name.endsWith("-x64.exe") && + !a.name.endsWith("-arm64.exe"), + ); + return { + x64: (x64Suffixed ?? legacy)?.name ?? null, + arm64: arm64?.name ?? null, + }; +} + function versionFromTag(tag: string): string { return tag.replace(/^v/, ""); } +interface ReleaseInfo { + version: string; + windowsX64Asset: string | null; + windowsArm64Asset: string | null; +} + const GITHUB_RELEASES_URL = "https://api.github.com/repos/getpaseo/paseo/releases?per_page=10"; -async function fetchLatestReadyRelease(): Promise { - const fallback = websitePackage.version.replace(/-.*$/, ""); +async function fetchLatestReadyRelease(): Promise { + const fallbackVersion = websitePackage.version.replace(/-.*$/, ""); + const fallback: ReleaseInfo = { + version: fallbackVersion, + windowsX64Asset: `Paseo-Setup-${fallbackVersion}.exe`, + windowsArm64Asset: null, + }; try { const res = await fetch(GITHUB_RELEASES_URL, { @@ -52,13 +78,18 @@ async function fetchLatestReadyRelease(): Promise { const releases = (await res.json()) as GitHubRelease[]; const ready = releases.find((r) => !r.prerelease && !r.draft && hasRequiredAssets(r)); - return ready ? versionFromTag(ready.tag_name) : fallback; + if (!ready) return fallback; + const win = pickWindowsAssets(ready.assets); + return { + version: versionFromTag(ready.tag_name), + windowsX64Asset: win.x64, + windowsArm64Asset: win.arm64, + }; } catch { return fallback; } } export const getLatestRelease = createServerFn({ method: "GET" }).handler(async () => { - const version = await fetchLatestReadyRelease(); - return { version }; + return fetchLatestReadyRelease(); }); diff --git a/packages/website/src/routes/__root.tsx b/packages/website/src/routes/__root.tsx index 90dd98fa8..845a21100 100644 --- a/packages/website/src/routes/__root.tsx +++ b/packages/website/src/routes/__root.tsx @@ -6,13 +6,19 @@ import { getStarCount } from "~/stars"; interface ReleaseContext { version: string; + windowsX64Asset: string | null; + windowsArm64Asset: string | null; } interface StarsContext { stars: string; } -const ReleaseCtx = createContext({ version: "" }); +const ReleaseCtx = createContext({ + version: "", + windowsX64Asset: null, + windowsArm64Asset: null, +}); const StarsCtx = createContext({ stars: "" }); const PLAUSIBLE_INIT_SCRIPT = { diff --git a/packages/website/src/routes/download.tsx b/packages/website/src/routes/download.tsx index 16abaf0ec..ee708dc6e 100644 --- a/packages/website/src/routes/download.tsx +++ b/packages/website/src/routes/download.tsx @@ -33,8 +33,9 @@ const homebrewTrigger = ( ); function Download() { - const { version } = useRelease(); - const urls = downloadUrls(version); + const release = useRelease(); + const { version } = release; + const urls = downloadUrls(release); return (
@@ -130,7 +131,11 @@ function Download() { Windows
- + + {urls.windowsExeArm64 && }