From 23a0a829b14e9ca375f7a1d720be1ef90cbac5c8 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 30 Aug 2026 00:56:57 -0700 Subject: [PATCH] fix(linux): make local package architecture explicit --- config/scripts/build-linux-local.mjs | 64 ++++++++++++++++++ config/scripts/build-linux-local.test.mjs | 81 +++++++++++++++++++++++ package.json | 2 +- 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 config/scripts/build-linux-local.mjs create mode 100644 config/scripts/build-linux-local.test.mjs diff --git a/config/scripts/build-linux-local.mjs b/config/scripts/build-linux-local.mjs new file mode 100644 index 00000000000..8948a378785 --- /dev/null +++ b/config/scripts/build-linux-local.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process' +import { resolve } from 'node:path' + +const SUPPORTED_ARCHES = new Set(['x64', 'arm64']) + +/** Select the local Linux package architecture without relying on builder defaults. */ +export function resolveLinuxBuildArch({ + platform = process.platform, + hostArch = process.arch, + requestedArch = process.env.ORCA_LINUX_BUILD_ARCH +} = {}) { + const arch = requestedArch ?? (platform === 'linux' ? hostArch : 'x64') + if (!SUPPORTED_ARCHES.has(arch)) { + throw new Error( + `Unsupported Linux build architecture: ${arch}. Use ORCA_LINUX_BUILD_ARCH=x64|arm64.` + ) + } + return arch +} + +export function buildLinuxElectronBuilderArgs(arch, extraArgs = []) { + if (!SUPPORTED_ARCHES.has(arch)) { + throw new Error(`Unsupported Linux build architecture: ${arch}`) + } + return [ + 'exec', + 'electron-builder', + '--config', + 'config/electron-builder.config.cjs', + '--linux', + 'AppImage', + 'deb', + `--${arch}`, + ...extraArgs + ] +} + +export function runLocalLinuxBuild({ + arch = resolveLinuxBuildArch(), + extraArgs = [], + environment = process.env, + execFile = execFileSync, + platform = process.platform, + cwd = resolve(import.meta.dirname, '../..') +} = {}) { + const env = { ...environment } + if (arch === 'arm64') { + env.ORCA_LINUX_ARM64_RELEASE = '1' + } else { + delete env.ORCA_LINUX_ARM64_RELEASE + } + const pnpm = platform === 'win32' ? 'pnpm.cmd' : 'pnpm' + execFile(pnpm, buildLinuxElectronBuilderArgs(arch, extraArgs), { + cwd, + env, + stdio: 'inherit' + }) +} + +if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename)) { + runLocalLinuxBuild({ extraArgs: process.argv.slice(2) }) +} diff --git a/config/scripts/build-linux-local.test.mjs b/config/scripts/build-linux-local.test.mjs new file mode 100644 index 00000000000..2a97702ea9a --- /dev/null +++ b/config/scripts/build-linux-local.test.mjs @@ -0,0 +1,81 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it, vi } from 'vitest' +import { + buildLinuxElectronBuilderArgs, + resolveLinuxBuildArch, + runLocalLinuxBuild +} from './build-linux-local.mjs' + +describe('local Linux build target', () => { + it('is the package script used by the local Linux build', () => { + const packageJson = JSON.parse( + readFileSync(resolve(import.meta.dirname, '../../package.json'), 'utf8') + ) + expect(packageJson.scripts['build:linux']).toContain( + 'node config/scripts/build-linux-local.mjs' + ) + }) + + it('follows a native Linux host architecture', () => { + expect(resolveLinuxBuildArch({ platform: 'linux', hostArch: 'arm64' })).toBe('arm64') + expect(resolveLinuxBuildArch({ platform: 'linux', hostArch: 'x64' })).toBe('x64') + }) + + it('defaults cross-platform Linux builds to x64 and allows an explicit override', () => { + expect(resolveLinuxBuildArch({ platform: 'darwin', hostArch: 'arm64' })).toBe('x64') + expect( + resolveLinuxBuildArch({ platform: 'darwin', hostArch: 'arm64', requestedArch: 'arm64' }) + ).toBe('arm64') + }) + + it('rejects unsupported architectures', () => { + expect(() => resolveLinuxBuildArch({ platform: 'linux', hostArch: 'ia32' })).toThrow( + 'Unsupported Linux build architecture' + ) + expect(() => buildLinuxElectronBuilderArgs('ia32')).toThrow( + 'Unsupported Linux build architecture' + ) + }) + + it('passes an explicit target and matching artifact-name environment', () => { + const execFile = vi.fn() + runLocalLinuxBuild({ + arch: 'arm64', + environment: { PATH: '/bin', ORCA_LINUX_ARM64_RELEASE: undefined }, + execFile, + platform: 'linux', + cwd: '/workspace' + }) + expect(execFile).toHaveBeenCalledWith( + 'pnpm', + buildLinuxElectronBuilderArgs('arm64'), + expect.objectContaining({ + cwd: '/workspace', + env: expect.objectContaining({ ORCA_LINUX_ARM64_RELEASE: '1' }), + stdio: 'inherit' + }) + ) + + runLocalLinuxBuild({ + arch: 'x64', + environment: { PATH: '/bin', ORCA_LINUX_ARM64_RELEASE: '1' }, + execFile, + platform: 'linux', + cwd: '/workspace' + }) + expect(execFile).toHaveBeenLastCalledWith( + 'pnpm', + buildLinuxElectronBuilderArgs('x64'), + expect.objectContaining({ + env: expect.not.objectContaining({ ORCA_LINUX_ARM64_RELEASE: expect.anything() }) + }) + ) + }) + + it('uses the Windows pnpm command name when cross-host packaging', () => { + const execFile = vi.fn() + runLocalLinuxBuild({ arch: 'x64', execFile, platform: 'win32', cwd: 'C:\\workspace' }) + expect(execFile.mock.calls[0]?.[0]).toBe('pnpm.cmd') + }) +}) diff --git a/package.json b/package.json index 2d803692357..2545e2128da 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "build:icons": "bash resources/icon-source/generate.sh", "build:mac": "pnpm run build:desktop && pnpm run build:computer-macos && pnpm run build:keyboard-layout-macos && pnpm run build:notification-status-macos && pnpm run ensure:electron-runtime && node config/scripts/build-mac-local.mjs", "build:mac:release": "node config/scripts/verify-macos-release-env.mjs && ORCA_MAC_RELEASE=1 pnpm run build:desktop && ORCA_MAC_RELEASE=1 pnpm run build:computer-macos && ORCA_MAC_RELEASE=1 pnpm run build:keyboard-layout-macos && ORCA_MAC_RELEASE=1 pnpm run build:notification-status-macos && pnpm run ensure:electron-runtime && ORCA_MAC_RELEASE=1 electron-builder --config config/electron-builder.config.cjs --mac", - "build:linux": "pnpm run build:desktop && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --linux AppImage deb rpm", + "build:linux": "pnpm run build:desktop && pnpm run ensure:electron-runtime && node config/scripts/build-linux-local.mjs", "test:e2e": "pnpm run ensure:electron-runtime && npx playwright test --config tests/playwright.config.ts --project=electron-headless", "test:e2e:workspace-session-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-quit-relaunch-session.spec.ts tests/e2e/golden-terminal-file-link.spec.ts tests/e2e/golden-worktree-create-switch.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "test:e2e:multi-client-navigation": "node config/scripts/run-multi-client-navigation-e2e.mjs",