From 45549c6fd5492e018b01f95c7bff9c8d7cc14c99 Mon Sep 17 00:00:00 2001 From: IgnoreMeBut <280635838+IgnoreMeBut@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:09:37 +0800 Subject: [PATCH] test(linux): harden source-startup dependencies Cover both pinned Linux OfficeCLI targets and the Whisper degradation boundary. Add an Ubuntu x64 native OfficeCLI smoke and document the glibc-only source-build contract. --- .github/workflows/linux-source-smoke.yml | 44 +++++++++++++++++++ README.md | 8 ++-- README.zh-CN.md | 8 ++-- scripts/ensure-dev-dependencies.cjs | 20 ++++++--- src/main/paths.ts | 12 +++-- test/main/paths.test.ts | 16 +++++++ .../main/util/ensure-dev-dependencies.test.ts | 26 +++++++++++ test/main/util/officecli-fetch.test.ts | 16 +++++-- 8 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/linux-source-smoke.yml create mode 100644 test/main/util/ensure-dev-dependencies.test.ts diff --git a/.github/workflows/linux-source-smoke.yml b/.github/workflows/linux-source-smoke.yml new file mode 100644 index 00000000..b0e9e2d5 --- /dev/null +++ b/.github/workflows/linux-source-smoke.yml @@ -0,0 +1,44 @@ +name: Linux source smoke + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux-x64: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Check out source + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 24 + cache: npm + + - name: Install JavaScript dependencies + run: npm ci --ignore-scripts + + - name: Run Linux dependency regressions + run: >- + node node_modules/vitest/vitest.mjs run + test/main/util/officecli-fetch.test.ts + test/main/util/ensure-dev-dependencies.test.ts + test/main/paths.test.ts + + - name: Type-check + run: node node_modules/typescript/bin/tsc --noEmit + + - name: Fetch pinned Linux OfficeCLI + run: node scripts/fetch-officecli.cjs --platform=linux-x64 + + - name: Verify native OfficeCLI contract + run: node bin/officecli-policy-gate.cjs --require-host-binary diff --git a/README.md b/README.md index 791da9de..da5f0407 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Installed and ready the first time you launch Orkas — each with its own skills ## Download -Packaged installers for macOS and Windows. Linux runs from source today — see [Quick start](#quick-start). +Packaged installers for macOS and Windows. glibc-based Linux x64/arm64 runs from source today — see [Quick start](#quick-start). - **macOS Apple Silicon** → [Orkas-mac-arm64.dmg](https://orkas.ai/download/?source=gh-orkas&entry_point=github_readme&os=mac&arch=arm64&download=1) - **macOS Intel** → [Orkas-mac-x64.dmg](https://orkas.ai/download/?source=gh-orkas&entry_point=github_readme&os=mac&arch=x64&download=1) @@ -121,7 +121,7 @@ Yes. Beyond its own Commander and specialist agents, Orkas can drive external CL Claude Desktop is a single assistant; CrewAI and LangChain are code-first frameworks. Orkas is a local-first multi-agent desktop app: the Commander coordinates specialist agents, keeps data and keys local, and gives each agent its own private skills and memory. See the [full comparisons](https://orkas.ai/compare/orkas-vs-langchain?source=gh-orkas). **Which platforms does Orkas support?** -macOS (Apple Silicon and Intel) and Windows 10+ have packaged installers. Linux runs from source today — same app, no installer yet. The source build needs Node 20+ and Python 3. +macOS (Apple Silicon and Intel) and Windows 10+ have packaged installers. glibc-based Linux x64/arm64 runs from source today, with no installer yet. Local speech transcription is not currently available on Linux. Alpine and other musl-based distributions are not supported. The source build needs Node 20+ and Python 3. **Is Orkas free and open source?** Yes — the app is MIT licensed and free to use. Bring your own model keys and you pay only your model providers; Orkas never takes a cut. Optionally, the desktop app also offers a built-in **Orkas model** for people who don't want to manage API keys — that one is billed by Orkas in credits (membership or credit packs). It is entirely opt-in, and every other feature works on your own keys. [Pricing →](https://orkas.ai/pricing/?source=gh-orkas) @@ -132,7 +132,7 @@ Yes — the app is MIT licensed and free to use. Bring your own model keys and y Want a packaged installer instead? See [Download](#download) above. To run from source — currently the way to run Orkas on Linux: -**Requirements**: Node 20+ · Python 3 · macOS / Windows 10+ / recent Linux +**Requirements**: Node 20+ · Python 3 · macOS / Windows 10+ / glibc-based Linux x64 or arm64 ```bash git clone https://github.com/Orkas-AI/Orkas.git @@ -141,6 +141,8 @@ cd Orkas run.cmd # Windows ``` +Linux source runs currently require glibc. Alpine and other musl-based distributions are not supported, and local speech transcription is unavailable on Linux. + `run.sh` / `run.cmd` auto-installs dependencies and downloads the embedding model (~95 MB). First launch creates a workspace under `~/.orkas/` (macOS / Linux) or `:\.orkas\` (Windows). Then open **Settings → AI Providers** to add an API key or OAuth. --- diff --git a/README.zh-CN.md b/README.zh-CN.md index adf72794..fe7e1c13 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -57,7 +57,7 @@ Orkas 是一个开源、本地优先的多智能体桌面应用。你描述目 ## 下载 -macOS 和 Windows 提供安装包。Linux 目前需从源码运行 —— 见 [快速开始](#快速开始)。 +macOS 和 Windows 提供安装包。基于 glibc 的 Linux x64/arm64 目前需从源码运行 —— 见 [快速开始](#快速开始)。 - **macOS Apple 芯片** → [Orkas-mac-arm64.dmg](https://orkas.ai/download/?source=gh-orkas&entry_point=github_readme&os=mac&arch=arm64&download=1) - **macOS Intel** → [Orkas-mac-x64.dmg](https://orkas.ai/download/?source=gh-orkas&entry_point=github_readme&os=mac&arch=x64&download=1) @@ -121,7 +121,7 @@ Orkas 是一个开源、本地优先的多智能体桌面应用。一个超强 Claude Desktop 是单个助理;CrewAI 和 LangChain 是代码优先的框架。Orkas 是一个本地优先的多智能体桌面应用:指挥官协调多个专业智能体,数据与 key 留在本地,每个智能体拥有私有技能与记忆。见[逐项对比](https://orkas.ai/compare/orkas-vs-langchain?source=gh-orkas)。 **支持哪些平台?** -macOS(Apple 芯片与 Intel)和 Windows 10+ 提供安装包。Linux 目前从源码运行 —— 功能相同,只是还没有安装包。源码运行需要 Node 20+ 与 Python 3。 +macOS(Apple 芯片与 Intel)和 Windows 10+ 提供安装包。基于 glibc 的 Linux x64/arm64 目前从源码运行,暂时没有安装包,且本地语音转写功能不可用。当前不支持 Alpine 及其他基于 musl 的发行版。源码运行需要 Node 20+ 与 Python 3。 **Orkas 免费且开源吗?** 是的 —— 应用本身 MIT 许可证、免费使用。自带模型 key 时,你只需为你的模型服务商付费,Orkas 不抽成。此外,桌面版还提供一个可选的内置 **Orkas 模型**,供不想自己配置 key 的用户使用 —— 这部分由 Orkas 按 credits 计费(会员或 credits 包)。它完全可选,其余所有功能用你自己的 key 即可。[价格 →](https://orkas.ai/pricing/?source=gh-orkas) @@ -132,7 +132,7 @@ macOS(Apple 芯片与 Intel)和 Windows 10+ 提供安装包。Linux 目前 想直接用安装包?见上方 [下载](#下载)。以下是从源码运行的方式 —— 也是目前在 Linux 上运行 Orkas 的方式: -**环境要求**:Node 20+ · Python 3 · macOS / Windows 10+ / 较新的 Linux +**环境要求**:Node 20+ · Python 3 · macOS / Windows 10+ / 基于 glibc 的 Linux x64 或 arm64 ```bash git clone https://github.com/Orkas-AI/Orkas.git @@ -141,6 +141,8 @@ cd Orkas run.cmd # Windows ``` +Linux 源码运行目前需要 glibc,不支持 Alpine 及其他基于 musl 的发行版;本地语音转写功能在 Linux 上也暂不可用。 + `run.sh` / `run.cmd` 会自动安装依赖并下载嵌入模型(约 95 MB)。首次启动会在 `~/.orkas/`(macOS / Linux)或 `<最小的非系统盘>:\.orkas\`(Windows)下创建工作区。随后进入 **设置 → AI 服务商** 配置 API key 或 OAuth。 --- diff --git a/scripts/ensure-dev-dependencies.cjs b/scripts/ensure-dev-dependencies.cjs index 367b6aaf..613da19e 100644 --- a/scripts/ensure-dev-dependencies.cjs +++ b/scripts/ensure-dev-dependencies.cjs @@ -18,6 +18,10 @@ function run(label, script, args = []) { if (result.status !== 0) throw new Error(`${label} failed with status ${result.status}`); } +function shouldProvisionWhisper(platform = process.platform, arch = process.arch) { + return Boolean(WHISPER_RUNTIME_CONTRACT.targets[`${platform}-${arch}`]); +} + function main() { console.log(`[dev-deps] preparing built-in dependencies for ${process.platform}-${process.arch}`); run('notification permission addon', 'scripts/build-notification-permission-addon.cjs', [ @@ -43,7 +47,7 @@ function main() { // without a contract target the fetch script must keep failing (packaging // gates rely on that strictness); here it is simply not required — speech // transcription degrades to an actionable error at runtime instead. - if (WHISPER_RUNTIME_CONTRACT.targets[`${process.platform}-${process.arch}`]) { + if (shouldProvisionWhisper(process.platform, process.arch)) { run('Whisper', 'scripts/fetch-whisper.cjs', [ '--platform', process.platform, '--arch', process.arch, @@ -54,9 +58,13 @@ function main() { console.log('[dev-deps] built-in dependencies ready'); } -try { - main(); -} catch (err) { - console.error(`[dev-deps] failed: ${err.message}`); - process.exit(1); +module.exports = Object.freeze({ shouldProvisionWhisper }); + +if (require.main === module) { + try { + main(); + } catch (err) { + console.error(`[dev-deps] failed: ${err.message}`); + process.exit(1); + } } diff --git a/src/main/paths.ts b/src/main/paths.ts index 5a423366..734a4a03 100644 --- a/src/main/paths.ts +++ b/src/main/paths.ts @@ -708,15 +708,19 @@ const OFFICECLI_ASSETS: Readonly> = { 'win32-arm64': 'officecli-win-arm64.exe', }; -/** Absolute path to the OfficeCLI binary for the current platform/arch, or - * null when no asset ships for it. Shipped via electron-builder +/** Absolute path to the OfficeCLI binary for the requested platform/arch + * (the current host by default), or null when no asset ships for it. Shipped + * via electron-builder * `extraResources`: * dev: PC/resources/officecli/ * packed: /Contents/Resources/officecli/ (darwin) * /resources/officecli/ (win/linux) */ -export function officeCliBinaryPath(): string | null { - const asset = OFFICECLI_ASSETS[`${process.platform}-${process.arch}`]; +export function officeCliBinaryPath( + platform: string = process.platform, + arch: string = process.arch, +): string | null { + const asset = OFFICECLI_ASSETS[`${platform}-${arch}`]; if (!asset) return null; const rp = (process as unknown as { resourcesPath?: string }).resourcesPath; const dir = (rp && !rp.includes(`${path.sep}node_modules${path.sep}electron${path.sep}`)) diff --git a/test/main/paths.test.ts b/test/main/paths.test.ts index 6da1fab0..d6fb630c 100644 --- a/test/main/paths.test.ts +++ b/test/main/paths.test.ts @@ -81,6 +81,22 @@ describe('paths › top-level (users.json / device.json / logs / venv)', () => { }); }); +describe('paths › OfficeCLI runtime', () => { + it.each([ + ['linux', 'x64', 'officecli-linux-x64'], + ['linux', 'arm64', 'officecli-linux-arm64'], + ])('resolves the bundled %s-%s asset', async (platform, arch, asset) => { + const p = await import('../../src/main/paths'); + expect(p.officeCliBinaryPath(platform, arch)) + .toBe(path.join(p.PC_ROOT, 'resources', 'officecli', asset)); + }); + + it('returns null for an unsupported platform target', async () => { + const p = await import('../../src/main/paths'); + expect(p.officeCliBinaryPath('freebsd', 'x64')).toBeNull(); + }); +}); + describe('paths › cloud-synced per-user', () => { it('chats / attachments / sessions / contexts / memory land under /cloud/', async () => { const p = await import('../../src/main/paths'); diff --git a/test/main/util/ensure-dev-dependencies.test.ts b/test/main/util/ensure-dev-dependencies.test.ts new file mode 100644 index 00000000..1274c023 --- /dev/null +++ b/test/main/util/ensure-dev-dependencies.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import * as path from 'node:path'; + +const { shouldProvisionWhisper } = require( + path.join(process.cwd(), 'scripts', 'ensure-dev-dependencies.cjs'), +) as { + shouldProvisionWhisper: (platform: string, arch: string) => boolean; +}; + +describe('ensure-dev-dependencies.cjs', () => { + it.each([ + ['darwin', 'arm64'], + ['darwin', 'x64'], + ['win32', 'x64'], + ])('keeps Whisper required for supported target %s-%s', (platform, arch) => { + expect(shouldProvisionWhisper(platform, arch)).toBe(true); + }); + + it.each([ + ['linux', 'x64'], + ['linux', 'arm64'], + ['win32', 'arm64'], + ])('does not provision Whisper for unsupported target %s-%s', (platform, arch) => { + expect(shouldProvisionWhisper(platform, arch)).toBe(false); + }); +}); diff --git a/test/main/util/officecli-fetch.test.ts b/test/main/util/officecli-fetch.test.ts index 2b174684..02b21351 100644 --- a/test/main/util/officecli-fetch.test.ts +++ b/test/main/util/officecli-fetch.test.ts @@ -6,6 +6,10 @@ import * as os from 'node:os'; import * as path from 'node:path'; const TEST_NODE = process.env.ORKAS_TEST_NODE || process.execPath; +const OFFICECLI_SPEC = require(path.join(process.cwd(), 'scripts', 'fetch-officecli.cjs')) as { + ASSETS: Readonly>; + SHA256: Readonly>; +}; let tmpDir: string; @@ -50,11 +54,17 @@ describe('fetch-officecli.cjs', () => { expect(`${r.stdout}\n${r.stderr}`).toContain('officecli-mac-arm64 missing'); }); - it('resolves the linux platform keys to pinned upstream assets', () => { + it.each([ + ['linux-x64', 'officecli-linux-x64', 'da07d4f787d7c85724104294ac023c89971ddfaee93ebb183b289282b8f869cc'], + ['linux-arm64', 'officecli-linux-arm64', '39008c7f76d202858637810553ef14e2cd3e7f61485fdcf2011f26967a7babd1'], + ])('resolves %s to its pinned upstream asset', (platform, asset, sha256) => { + expect(OFFICECLI_SPEC.ASSETS[platform]).toBe(asset); + expect(OFFICECLI_SPEC.SHA256[asset]).toBe(sha256); + const r = spawnSync(TEST_NODE, [ path.join(process.cwd(), 'scripts', 'fetch-officecli.cjs'), `--root=${tmpDir}`, - '--platform=linux-x64', + `--platform=${platform}`, '--check', ], { cwd: process.cwd(), @@ -62,7 +72,7 @@ describe('fetch-officecli.cjs', () => { }); expect(r.status).toBe(1); - expect(`${r.stdout}\n${r.stderr}`).toContain('officecli-linux-x64 missing'); + expect(`${r.stdout}\n${r.stderr}`).toContain(`${asset} missing`); }); it('resumes an existing partial download with a Range request', async () => {