From e2d222f1381a340800532f264b7e88f533be95ed Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Thu, 26 Feb 2026 20:51:09 +0400 Subject: [PATCH] feat: add ARM64 Linux build target and download detection - CI: add ubuntu-24.04-arm matrix entry with aarch64-unknown-linux-gnu - Node sidecar: download linux-arm64 Node.js bundle for ARM runners - Download API: add linux-appimage-arm64 pattern for aarch64 AppImage - Download banner: show both x64 and ARM64 Linux options (UA can't distinguish) - Desktop updater: map Linux aarch64 to linux-appimage-arm64 for in-app updates - Smoke test: fix AppImage search path for cross-target builds --- .github/workflows/build-desktop.yml | 9 +++++++-- api/download.js | 1 + scripts/download-node.sh | 25 +++++++++++++++++++++++-- src/app/desktop-updater.ts | 4 +++- src/components/DownloadBanner.ts | 7 +++++-- 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 998e989d9..7cd4343db 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -55,6 +55,11 @@ jobs: node_target: 'x86_64-unknown-linux-gnu' label: 'Linux-x64' timeout: 120 + - platform: 'ubuntu-24.04-arm' + args: '--target aarch64-unknown-linux-gnu' + node_target: 'aarch64-unknown-linux-gnu' + label: 'Linux-ARM64' + timeout: 120 runs-on: ${{ matrix.platform }} name: Build (${{ matrix.label }}) @@ -77,7 +82,7 @@ jobs: uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 with: toolchain: stable - targets: ${{ contains(matrix.platform, 'macos') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + targets: ${{ contains(matrix.platform, 'macos') && 'aarch64-apple-darwin,x86_64-apple-darwin' || (matrix.label == 'Linux-ARM64' && 'aarch64-unknown-linux-gnu' || '') }} - name: Rust cache uses: swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db @@ -298,7 +303,7 @@ jobs: shell: bash run: | sudo apt-get install -y xvfb imagemagick - APPIMAGE=$(find src-tauri/target/release/bundle/appimage -name '*.AppImage' | head -1) + APPIMAGE=$(find src-tauri/target -path '*/bundle/appimage/*.AppImage' | head -1) if [ -z "$APPIMAGE" ]; then echo "::error::No AppImage found after build" exit 1 diff --git a/api/download.js b/api/download.js index 794ea8248..de84f276d 100644 --- a/api/download.js +++ b/api/download.js @@ -10,6 +10,7 @@ const PLATFORM_PATTERNS = { 'macos-arm64': (name) => name.endsWith('_aarch64.dmg'), 'macos-x64': (name) => name.endsWith('_x64.dmg') && !name.includes('setup'), 'linux-appimage': (name) => name.endsWith('_amd64.AppImage'), + 'linux-appimage-arm64': (name) => name.endsWith('_aarch64.AppImage'), }; const VARIANT_IDENTIFIERS = { diff --git a/scripts/download-node.sh b/scripts/download-node.sh index 15d56d454..64d4bb607 100755 --- a/scripts/download-node.sh +++ b/scripts/download-node.sh @@ -15,6 +15,7 @@ Supported targets: - x86_64-apple-darwin - aarch64-apple-darwin - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-gnu Environment: NODE_VERSION Node.js version to bundle (default: 22.14.0) @@ -72,7 +73,14 @@ if [[ -z "${TARGET}" ]]; then esac ;; Linux) - TARGET="x86_64-unknown-linux-gnu" + case "${RUNNER_ARCH:-}" in + ARM64|arm64) + TARGET="aarch64-unknown-linux-gnu" + ;; + *) + TARGET="x86_64-unknown-linux-gnu" + ;; + esac ;; *) echo "Unsupported RUNNER_OS: ${RUNNER_OS}" >&2 @@ -96,7 +104,14 @@ if [[ -z "${TARGET}" ]]; then esac ;; Linux) - TARGET="x86_64-unknown-linux-gnu" + case "$(uname -m)" in + aarch64|arm64) + TARGET="aarch64-unknown-linux-gnu" + ;; + *) + TARGET="x86_64-unknown-linux-gnu" + ;; + esac ;; MINGW*|MSYS*|CYGWIN*|Windows_NT) TARGET="x86_64-pc-windows-msvc" @@ -135,6 +150,12 @@ case "${TARGET}" in NODE_RELATIVE_PATH="bin/node" OUTPUT_NAME="node" ;; + aarch64-unknown-linux-gnu) + DIST_NAME="node-v${NODE_VERSION}-linux-arm64" + ARCHIVE_NAME="${DIST_NAME}.tar.gz" + NODE_RELATIVE_PATH="bin/node" + OUTPUT_NAME="node" + ;; *) echo "Unsupported target: ${TARGET}" >&2 exit 1 diff --git a/src/app/desktop-updater.ts b/src/app/desktop-updater.ts index 96d739118..01d8272b2 100644 --- a/src/app/desktop-updater.ts +++ b/src/app/desktop-updater.ts @@ -134,7 +134,9 @@ export class DesktopUpdater implements AppModule { } if (normalizedOs === 'linux') { - return normalizedArch === 'x86_64' ? 'linux-appimage' : null; + if (normalizedArch === 'x86_64') return 'linux-appimage'; + if (normalizedArch === 'aarch64') return 'linux-appimage-arm64'; + return null; } return null; diff --git a/src/components/DownloadBanner.ts b/src/components/DownloadBanner.ts index 0f6706b24..fb0a5055a 100644 --- a/src/components/DownloadBanner.ts +++ b/src/components/DownloadBanner.ts @@ -31,7 +31,7 @@ function dismiss(panel: HTMLElement, fromDownload = false): void { panel.addEventListener('transitionend', () => panel.remove(), { once: true }); } -type Platform = 'macos-arm64' | 'macos-x64' | 'macos' | 'windows' | 'linux' | 'unknown'; +type Platform = 'macos-arm64' | 'macos-x64' | 'macos' | 'windows' | 'linux' | 'linux-x64' | 'linux-arm64' | 'unknown'; function detectPlatform(): Platform { const ua = navigator.userAgent; @@ -64,7 +64,8 @@ function allButtons(): DlButton[] { { cls: 'mac', href: '/api/download?platform=macos-arm64', label: `\uF8FF ${t('modals.downloadBanner.macSilicon')}` }, { cls: 'mac', href: '/api/download?platform=macos-x64', label: `\uF8FF ${t('modals.downloadBanner.macIntel')}` }, { cls: 'win', href: '/api/download?platform=windows-exe', label: `\u229E ${t('modals.downloadBanner.windows')}` }, - { cls: 'linux', href: '/api/download?platform=linux-appimage', label: `\u{1F427} ${t('modals.downloadBanner.linux')}` }, + { cls: 'linux', href: '/api/download?platform=linux-appimage', label: `\u{1F427} ${t('modals.downloadBanner.linux')} (x64)` }, + { cls: 'linux', href: '/api/download?platform=linux-appimage-arm64', label: `\u{1F427} ${t('modals.downloadBanner.linux')} (ARM64)` }, ]; } @@ -76,6 +77,8 @@ function buttonsForPlatform(p: Platform): DlButton[] { case 'macos': return buttons.filter(b => b.cls === 'mac'); case 'windows': return buttons.filter(b => b.cls === 'win'); case 'linux': return buttons.filter(b => b.cls === 'linux'); + case 'linux-x64': return buttons.filter(b => b.href.includes('linux-appimage') && !b.href.includes('arm64')); + case 'linux-arm64': return buttons.filter(b => b.href.includes('linux-appimage-arm64')); default: return buttons; } }