diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f81dc0e370..59cac83d84 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,15 +8,15 @@ # # Builds installers for every platform defined in the electron-builder config # (macOS dmg/zip arm64 and x64, Windows NSIS and portable ZIP x64, Linux -# AppImage/deb/rpm x64), each +# AppImage/deb/rpm x64 and arm64), each # with its native pi-desktop-host-core Rust sidecar. The Linux app.asar is also # exported as a system-Electron repackaging asset, and all outputs — with their # electron-updater latest*.yml feeds — are published to the GitHub Release # (D126/D285/D364/D603). macOS tag artifacts are Developer ID-signed, notarized, and # stapled before upload (D450 / ADR 0289). # -# It also builds the headless `pi-host` bundle for every Linux platform the -# desktop publishes (currently x64) and publishes it as +# It also builds the headless `pi-host` bundle for both Linux architectures the +# desktop publishes and publishes each as # pi-host--linux-.tar.gz plus a .sha256 sidecar. The desktop's # SSH bootstrap downloads that asset at the desktop's version and verifies the # published checksum before installing on the remote machine (rollout @@ -118,8 +118,22 @@ jobs: # (24.04) would raise the floor to 2.39. os: ubuntu-22.04 arch: x64 + runner_arch: x86_64 artifact: linux-x64 platform: linux + feed: latest-linux.yml + dist: dist:linux + - name: Linux arm64 + # GitHub's native arm64 runner packages the Electron app and the + # host-core sidecar at the same architecture; the `-arm` image + # keeps the glibc 2.35 floor the x64 lane documents + # (ubuntu-24.04-arm would raise it to 2.39). + os: ubuntu-22.04-arm + arch: arm64 + runner_arch: aarch64 + artifact: linux-arm64 + platform: linux + feed: latest-linux-arm64.yml dist: dist:linux runs-on: ${{ matrix.os }} timeout-minutes: 60 @@ -129,8 +143,12 @@ jobs: steps: - uses: actions/checkout@v7 - - name: Verify native macOS runner architecture - if: matrix.platform == 'macos' + - name: Verify native runner architecture + # A mislabelled or emulated runner would wrap one architecture's + # Electron app around the other architecture's Rust sidecar. The + # Windows rows carry no `runner_arch` because electron-builder + # cross-builds Windows from any runner. + if: matrix.runner_arch run: | set -euo pipefail actual_arch="$(uname -m)" @@ -323,7 +341,23 @@ jobs: - name: Export Linux ASAR release asset if: matrix.platform == 'linux' - run: node scripts/export-linux-asar.mjs + run: node scripts/export-linux-asar.mjs --arch ${{ matrix.arch }} + + - name: Verify the Linux update feed + # electron-builder names the Linux feed `latest-linux.yml` for x64 and + # `latest-linux-.yml` for every other Linux architecture — the + # same names electron-updater requests on those architectures — so the + # two lanes cannot overwrite each other's feed when the publish job + # merges their artifacts. + if: matrix.platform == 'linux' + run: | + set -euo pipefail + feed="apps/desktop/release/${{ matrix.feed }}" + if [[ ! -f "$feed" ]]; then + echo "::error::Expected the Linux updater feed at $feed" + ls apps/desktop/release || true + exit 1 + fi - name: Verify macOS artifact names if: matrix.platform == 'macos' @@ -377,11 +411,17 @@ jobs: fail-fast: false matrix: include: - # Every Linux platform the desktop release publishes. The desktop - # matrix ships Linux x64 only today; add arm64 here in lockstep if - # the desktop starts publishing it. + # Every Linux platform the desktop release publishes. Keep this list + # in lockstep with the desktop build matrix above and with + # PUBLISHED_TARGETS in apps/desktop/electron/main/remote/pi-host-release.ts: + # a bundle the desktop refuses to install is dead weight, and a target + # the desktop offers without a bundle fails mid-download. - arch: x64 os: ubuntu-22.04 + runner_arch: x86_64 + - arch: arm64 + os: ubuntu-22.04-arm + runner_arch: aarch64 runs-on: ${{ matrix.os }} timeout-minutes: 60 defaults: @@ -400,6 +440,17 @@ jobs: exit 1 fi + - name: Verify native runner architecture + if: matrix.runner_arch + run: | + set -euo pipefail + actual_arch="$(uname -m)" + expected_arch="${{ matrix.runner_arch }}" + if [ "$actual_arch" != "$expected_arch" ]; then + echo "::error::Expected $expected_arch runner, got $actual_arch." + exit 1 + fi + - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v7 diff --git a/README.md b/README.md index da71993a93..322602019f 100644 --- a/README.md +++ b/README.md @@ -575,7 +575,7 @@ Agent / Plan / Goal | macOS | Apple Silicon | `.dmg` / `.zip` | | macOS | Intel | `.dmg` / `.zip` | | Windows | x64 | Installer / `.zip` | -| Linux | x64 | `.AppImage` / `.deb` / `.rpm` / `.asar` | +| Linux | x64 / ARM64 | `.AppImage` / `.deb` / `.rpm` / `.asar` | macOS releases are signed with a Developer ID certificate and notarized by Apple. @@ -584,7 +584,7 @@ macOS releases are signed with a Developer ID certificate and notarized by Apple
-Linux x64 packages require **glibc 2.35+**. +Linux packages require **glibc 2.35+**. Common supported distributions include: diff --git a/README.zh-CN.md b/README.zh-CN.md index dbe179c4e7..f99de6e2b0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -579,7 +579,7 @@ Agent / Plan / Goal | macOS | Apple Silicon | `.dmg` / `.zip` | | macOS | Intel | `.dmg` / `.zip` | | Windows | x64 | 安装程序 / `.zip` | -| Linux | x64 | `.AppImage` / `.deb` / `.rpm` / `.asar` | +| Linux | x64 / ARM64 | `.AppImage` / `.deb` / `.rpm` / `.asar` | macOS Release 使用 Developer ID 签名并经过 Apple Notarization。 @@ -588,7 +588,7 @@ macOS Release 使用 Developer ID 签名并经过 Apple Notarization。
-Linux x64 需要 **glibc 2.35+**。 +Linux 软件包(x64 与 arm64)需要 **glibc 2.35+**。 常见支持版本: diff --git a/apps/desktop/electron/main/remote/pi-host-release.ts b/apps/desktop/electron/main/remote/pi-host-release.ts index e29c995e90..f9ebcf55e7 100644 --- a/apps/desktop/electron/main/remote/pi-host-release.ts +++ b/apps/desktop/electron/main/remote/pi-host-release.ts @@ -22,12 +22,12 @@ export type PiHostTarget = { }; /** - * Release-matrix parity. `release.yml`'s `pi-host-bundle` job publishes Linux - * x64 only today; the desktop matrix ships no other Linux platform. A target + * Release-matrix parity. `release.yml`'s `pi-host-bundle` job publishes the + * Linux architectures the desktop build matrix ships (x64 and arm64). A target * outside this list is refused with a typed failure instead of a 404 halfway * through a download. */ -const PUBLISHED_TARGETS: readonly string[] = ["linux-x64"]; +const PUBLISHED_TARGETS: readonly string[] = ["linux-x64", "linux-arm64"]; export function targetKey(target: PiHostTarget): string { return `${target.platform}-${target.arch}`; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 875496947e..3220a06da9 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -254,24 +254,16 @@ "linux": { "executableName": "pi-desktop", "syncDesktopName": true, + "artifactName": "PI-Desktop-${version}-linux-${arch}.${ext}", "target": [ { - "target": "AppImage", - "arch": [ - "x64" - ] + "target": "AppImage" }, { - "target": "deb", - "arch": [ - "x64" - ] + "target": "deb" }, { - "target": "rpm", - "arch": [ - "x64" - ] + "target": "rpm" } ], "category": "Development", diff --git a/apps/desktop/test/ci-workflow.test.mjs b/apps/desktop/test/ci-workflow.test.mjs index b1124653fd..d9796c963a 100644 --- a/apps/desktop/test/ci-workflow.test.mjs +++ b/apps/desktop/test/ci-workflow.test.mjs @@ -150,19 +150,71 @@ test("manual Linux package validation covers the RPM desktop identity", () => { test("release workflow publishes the Linux ASAR beside installers", () => { assert.match( releaseWorkflowSource, - /if: matrix\.platform == 'linux'[\s\S]*?node scripts\/export-linux-asar\.mjs/, + /if: matrix\.platform == 'linux'[\s\S]*?node scripts\/export-linux-asar\.mjs --arch \$\{\{ matrix\.arch \}\}/, ); assert.match(releaseWorkflowSource, /apps\/desktop\/release\/\*\.asar/); assert.match( releaseAsarScriptSource, - /linux-unpacked\/resources\/app\.asar/, + /linuxUnpackedDirName\(releaseArch\)\}\/resources\/app\.asar/, ); assert.match( releaseAsarScriptSource, - /PI-Desktop-\$\{releaseVersion\}-linux-x64\.asar/, + /PI-Desktop-\$\{releaseVersion\}-linux-\$\{releaseArch\}\.asar/, ); }); +test("the release matrix ships native Linux x64 and arm64 lanes", () => { + assert.match( + releaseWorkflowSource, + /name: Linux arm64[\s\S]*?os: ubuntu-22\.04-arm[\s\S]*?arch: arm64[\s\S]*?runner_arch: aarch64[\s\S]*?artifact: linux-arm64[\s\S]*?platform: linux[\s\S]*?dist: dist:linux/, + "the arm64 lane runs on a native arm64 runner", + ); + // A runner that is not the architecture it packages would pair one + // architecture's Electron app with the other's Rust sidecar. + assert.match( + releaseWorkflowSource, + /name: Verify native runner architecture[\s\S]*?if: matrix\.runner_arch/, + ); + // electron-builder already names each Linux lane's feed after its own + // architecture (`latest-linux.yml` on x64, `latest-linux-arm64.yml` on + // arm64) — the names electron-updater requests — so the lanes cannot + // overwrite each other's feed during the publish merge, and the lane + // verifies the name it will publish instead of renaming it. + assert.match( + releaseWorkflowSource, + /name: Verify the Linux update feed[\s\S]*?feed="apps\/desktop\/release\/\$\{\{ matrix\.feed \}\}"/, + ); + assert.match(releaseWorkflowSource, /feed: latest-linux\.yml/); + assert.match(releaseWorkflowSource, /feed: latest-linux-arm64\.yml/); + // The pi-host bundles the SSH bootstrap installs track the desktop's Linux + // lanes one for one. + assert.match( + releaseWorkflowSource, + /pi-host-bundle:[\s\S]*?- arch: x64[\s\S]*?os: ubuntu-22\.04[\s\S]*?- arch: arm64[\s\S]*?os: ubuntu-22\.04-arm/, + ); +}); + +test("the Linux package config lets the workflow choose the architecture", () => { + const build = JSON.parse(desktopPackageSource).build; + // electron-builder prefers a target's configured arch list over the CLI + // `--x64`/`--arm64` flag, so pinning both here would make each Linux lane + // build the other architecture around its own native host-core. + for (const entry of build.linux.target) { + assert.equal( + entry.arch, + undefined, + `linux target ${entry.target} must not pin an arch`, + ); + } + assert.match( + build.linux.artifactName, + /-linux-\$\{arch\}\.\$\{ext\}$/, + "the AppImage name carries its architecture", + ); + assert.equal(build.deb.artifactName, "pi-desktop_${version}_${arch}.${ext}"); + assert.equal(build.rpm.artifactName, "pi-desktop-${version}-${arch}.${ext}"); +}); + test("release matrix packages both native macOS architectures", () => { assert.match( releaseWorkflowSource, diff --git a/apps/desktop/test/release-asar.test.mjs b/apps/desktop/test/release-asar.test.mjs index b56a94ec03..3aa616dfff 100644 --- a/apps/desktop/test/release-asar.test.mjs +++ b/apps/desktop/test/release-asar.test.mjs @@ -4,19 +4,22 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { exportLinuxAsar } from "../../../scripts/export-linux-asar.mjs"; +import { + exportLinuxAsar, + linuxUnpackedDirName, +} from "../../../scripts/export-linux-asar.mjs"; -async function createFixture() { +async function createFixture({ unpackedDir = "linux-unpacked" } = {}) { const rootDir = await mkdtemp(join(tmpdir(), "pi-desktop-release-asar-")); const releaseDir = join(rootDir, "apps/desktop/release"); - await mkdir(join(releaseDir, "linux-unpacked/resources"), { recursive: true }); + await mkdir(join(releaseDir, `${unpackedDir}/resources`), { recursive: true }); await writeFile( join(rootDir, "apps/desktop/package.json"), JSON.stringify({ version: "9.8.7" }), ); const sourcePath = join( releaseDir, - "linux-unpacked/resources/app.asar", + `${unpackedDir}/resources/app.asar`, ); await writeFile(sourcePath, "fixture-asar-bytes"); return { rootDir, releaseDir, sourcePath }; @@ -25,7 +28,7 @@ async function createFixture() { test("exports the exact Linux app.asar with the release asset name", async () => { const fixture = await createFixture(); try { - const result = await exportLinuxAsar({ rootDir: fixture.rootDir }); + const result = await exportLinuxAsar({ rootDir: fixture.rootDir, arch: "x64" }); const destination = join( fixture.releaseDir, "PI-Desktop-9.8.7-linux-x64.asar", @@ -41,12 +44,50 @@ test("exports the exact Linux app.asar with the release asset name", async () => } }); +test("arm64 exports the arm64 asset from electron-builder's arm64 unpacked tree", async () => { + // electron-builder only drops the architecture suffix for x64, so reading + // `linux-unpacked` on the arm64 lane would export nothing at all. + assert.equal(linuxUnpackedDirName("x64"), "linux-unpacked"); + assert.equal(linuxUnpackedDirName("arm64"), "linux-arm64-unpacked"); + + const fixture = await createFixture({ unpackedDir: "linux-arm64-unpacked" }); + try { + const result = await exportLinuxAsar({ + rootDir: fixture.rootDir, + arch: "arm64", + }); + + assert.equal( + result.destination, + join(fixture.releaseDir, "PI-Desktop-9.8.7-linux-arm64.asar"), + ); + assert.equal( + await readFile(result.destination, "utf8"), + await readFile(fixture.sourcePath, "utf8"), + ); + } finally { + await rm(fixture.rootDir, { recursive: true, force: true }); + } +}); + +test("refuses an architecture the release matrix does not publish", async () => { + const fixture = await createFixture(); + try { + await assert.rejects( + exportLinuxAsar({ rootDir: fixture.rootDir, arch: "riscv64" }), + /Unsupported Linux release architecture: riscv64/, + ); + } finally { + await rm(fixture.rootDir, { recursive: true, force: true }); + } +}); + test("fails when electron-builder did not produce the Linux app.asar", async () => { const fixture = await createFixture(); try { await rm(fixture.sourcePath); await assert.rejects( - exportLinuxAsar({ rootDir: fixture.rootDir }), + exportLinuxAsar({ rootDir: fixture.rootDir, arch: "x64" }), /Linux ASAR source not found:/, ); } finally { diff --git a/apps/desktop/test/remote-host-pi-host-release.test.mjs b/apps/desktop/test/remote-host-pi-host-release.test.mjs index 4d98c408b1..888d1d888c 100644 --- a/apps/desktop/test/remote-host-pi-host-release.test.mjs +++ b/apps/desktop/test/remote-host-pi-host-release.test.mjs @@ -64,12 +64,14 @@ test("resolveTarget refuses a platform or architecture the release cannot serve" assert.equal(resolveTarget(""), null); }); -test("isPublishedTarget matches the release matrix, which ships linux-x64 only", () => { +test("isPublishedTarget matches the release matrix's Linux architectures", () => { assert.equal(targetKey(LINUX_X64), "linux-x64"); assert.equal(isPublishedTarget(LINUX_X64), true); + // The desktop matrix publishes Linux x64 and arm64, and release.yml's + // pi-host-bundle job builds one bundle per lane. + assert.equal(isPublishedTarget({ platform: "linux", arch: "arm64" }), true); // Publishing parity: a 404 halfway through a download is what this guard // exists to prevent, so every unpublished cell must be refused here. - assert.equal(isPublishedTarget({ platform: "linux", arch: "arm64" }), false); assert.equal(isPublishedTarget({ platform: "darwin", arch: "x64" }), false); assert.equal(isPublishedTarget({ platform: "darwin", arch: "arm64" }), false); }); diff --git a/apps/desktop/test/remote-host-ssh-bootstrap.test.mjs b/apps/desktop/test/remote-host-ssh-bootstrap.test.mjs index 78dd09b4e8..91a0aac00e 100644 --- a/apps/desktop/test/remote-host-ssh-bootstrap.test.mjs +++ b/apps/desktop/test/remote-host-ssh-bootstrap.test.mjs @@ -171,16 +171,39 @@ test("a remote host running a different release is refused before the forward", }); test("an unpublished target fails without downloading anything", async () => { - // arm64 Linux is in the release matrix's shape but not in its published set. - const transport = fakeTransport({ uname: "Linux\naarch64\n" }); + // macOS is in the bootstrap's `uname` vocabulary but the release publishes no + // pi-host bundle for it. + const transport = fakeTransport({ uname: "Darwin\nx86_64\n" }); const { bootstrap, fetched } = harness({ transport }); const error = await catchError(() => bootstrap.bootstrap(request())); assert.equal(error.errorCode, "HOST_BOOTSTRAP_FAILED"); - assert.equal(error.target, "linux-arm64"); + assert.equal(error.target, "darwin-x64"); assert.deepEqual(fetched, [], "no checksum request for a bundle that does not exist"); assert.deepEqual(transport.calls.uploads, []); }); +test("an arm64 Linux remote installs the published arm64 bundle", async () => { + // The arm64 lane publishes its own bundle, so the desktop resolves the arm64 + // artifact and pins its published digest instead of reusing the x64 one. + const arm64Artifact = `pi-host-${VERSION}-linux-arm64.tar.gz`; + const transport = fakeTransport({ uname: "Linux\naarch64\n" }); + const { bootstrap, fetched } = harness({ + transport, + checksum: `${DIGEST} ${arm64Artifact}\n`, + }); + + const outcome = await bootstrap.bootstrap(request()); + + assert.equal( + fetched[0], + `https://github.com/vastsa/PI-Desktop/releases/download/v${VERSION}/${arm64Artifact}.sha256`, + ); + assert.equal(outcome.ssh.version, VERSION); + const upload = transport.calls.uploads[0]; + assert.ok(upload.input.includes(`/${arm64Artifact}'`), "the script downloads the arm64 tarball"); + assert.ok(upload.input.includes(`EXPECTED_SHA256='${DIGEST}'`)); +}); + test("a checksum file with no digest for this artifact fails the bootstrap", async () => { const transport = fakeTransport(); const { bootstrap } = harness({ transport, checksum: `${DIGEST} pi-host-0.15.0-linux-x64.tar.gz\n` }); diff --git a/docs/adr/0292-ssh-remote-host-bootstrap.md b/docs/adr/0292-ssh-remote-host-bootstrap.md index 1c7eda0bd4..e5ac728e62 100644 --- a/docs/adr/0292-ssh-remote-host-bootstrap.md +++ b/docs/adr/0292-ssh-remote-host-bootstrap.md @@ -48,7 +48,7 @@ extended boot hook, and one new IPC channel. 2. **Pure release coordinates (`pi-host-release.ts`).** The artifact is resolved for the *remote* platform at the *desktop's* version, and the SHA-256 the release publishes is the trust anchor. `PUBLISHED_TARGETS` is the release - matrix (`linux-x64` today), so any other target is refused with + matrix (`linux-x64` and `linux-arm64`), so any other target is refused with `HOST_BOOTSTRAP_FAILED` before a download starts rather than as a 404 halfway through one. URL derivation, `uname` parsing, checksum-file parsing, constant-work digest comparison, and the version-parity rule are all pure @@ -135,7 +135,8 @@ Still open for R2b and later: closed with `MODEL_NOT_CONFIGURED` until a provider is configured on it. - **The resync watchdog**: `resync.required` events are still dropped. - **Non-Linux remote targets, and Windows as a *remote* target.** Only - `linux-x64` is published, so both are refused before a download starts. + `linux-x64` and `linux-arm64` are published (D638 / ADR 0318), so both are + refused before a download starts. ## Alternatives considered diff --git a/docs/adr/0318-linux-arm64-release-lane.md b/docs/adr/0318-linux-arm64-release-lane.md new file mode 100644 index 0000000000..4dc3b9273f --- /dev/null +++ b/docs/adr/0318-linux-arm64-release-lane.md @@ -0,0 +1,100 @@ +# ADR 0318: Publish Native Linux arm64 Artifacts + +- Status: Accepted (D638) +- Date: 2026-10-01 +- Deciders: PI-Desktop core +- Related: D126, D285, D375, D603, ADR 0022, ADR 0145, ADR 0284, ADR 0292, issue #1281, E2E-192a + +## Context + +Tag releases publish Linux x64 only. Issue #1281 asks for a Linux arm64 build: +the reporter runs PI-Desktop inside a Linux container on a rooted arm64 Android +tablet, where no x64 artifact can execute and emulation is not a usable path for +an Electron app. + +Three properties of the existing lane make "add arm64" a packaging change rather +than a new flag on one job: + +1. The Rust `pi-desktop-host-core` sidecar and the native voice modules are + packaged from the runner's own build output. A cross-built arm64 package on + the x64 runner would ship an x64 sidecar inside an arm64 app. +2. The Linux electron-builder targets pinned `arch: ["x64"]`, and + electron-builder prefers a target's configured arch list over the CLI + `--x64` / `--arm64` flag. Pinning both architectures would make each Linux + lane build the other architecture around its own native sidecar. +3. The AppImage target's default artifact name carries no architecture, so two + Linux lanes would publish the same `PI-Desktop-.AppImage` file name + and the publish job's merged download would keep only whichever lane arrived + last. + +`electron-updater` resolves its Linux feed by architecture: `latest-linux.yml` +on x64 and `latest-linux-.yml` on every other architecture +(`Provider.getChannelFilePrefix`). + +## Decision + +1. The release matrix publishes two native Linux lanes: `ubuntu-22.04` for x64 + and GitHub's arm64 `ubuntu-22.04-arm` runner for arm64. Both verify + `uname -m` (`x86_64` / `aarch64`) before preparing package inputs, exactly as + the macOS lanes verify their native runner. Both images are Ubuntu 22.04, so + both keep the documented glibc 2.35 floor. +2. The static Linux targets stay AppImage, deb, and rpm without a pinned `arch`. + The workflow passes the matching `--x64` or `--arm64` flag to + electron-builder, which is the same rule ADR 0145 set for macOS. +3. `build.linux.artifactName` becomes + `PI-Desktop-${version}-linux-${arch}.${ext}`, so the AppImage assets are + `PI-Desktop--linux-x64.AppImage` and + `PI-Desktop--linux-arm64.AppImage`. The deb and rpm targets keep + their own patterns and expand `${arch}` through FPM's arch names + (`amd64`/`arm64` and `x86_64`/`aarch64`). +4. Each Linux job verifies the updater feed it is about to publish — + `latest-linux.yml` on x64, `latest-linux-arm64.yml` on arm64. electron-builder + already writes that architecture-suffixed name + (`getArchPrefixForUpdateFile`), so no rename is needed and the publish job + merges both lanes' artifacts without one feed overwriting the other. +5. `scripts/export-linux-asar.mjs` takes the lane architecture, reads the + archive from electron-builder's own unpacked directory + (`linux-unpacked` for x64, `linux-arm64-unpacked` for arm64), and publishes + `PI-Desktop--linux-.asar`. +6. `release.yml`'s `pi-host-bundle` job builds both Linux architectures, and + `PUBLISHED_TARGETS` in `apps/desktop/electron/main/remote/pi-host-release.ts` + lists `linux-x64` and `linux-arm64`. An arm64 desktop can therefore + bootstrap an arm64 remote host over SSH (ADR 0292) instead of refusing a + target the release already serves. +7. No updater ownership, signing, or delivery-mode change: the arm64 AppImage + uses the same in-app `electron-updater` lane as x64, and deb/rpm stay + notify-and-link. + +## Consequences + +- arm64 Linux users install native AppImage, deb, and rpm packages from every + tag release, and the AppImage updates in place through its own feed. +- The x64 AppImage asset is renamed from `PI-Desktop-.AppImage` to + `PI-Desktop--linux-x64.AppImage`. Download links that carry the old + name must be updated; in-app updates are unaffected because the updater reads + the published feed rather than a hard-coded file name. +- Every tag release carries one `pi-host--linux-arm64.tar.gz` bundle + and one extra arm64 system-Electron ASAR asset beside the existing x64 ones. +- Microphone capture stays unavailable on arm64 Linux devices other than + Raspberry Pi boards: `@picovoice/pvrecorder-node` classifies Linux arm64 by + `/proc/cpuinfo` CPU part and only knows Raspberry Pi parts. Speech-to-text + (`transcribe-cpp` ships a `linux-arm64-cpu-vulkan` bundle) and every other + feature have no architecture-specific dependency. +- Release cost grows by one Linux packaging job and one pi-host bundle job. + +## Alternatives considered + +- **Cross-compile the arm64 packages on the existing x64 runner.** Rejected: the + packaged Rust sidecar and the native voice modules come from the runner's own + build output, so a cross-compiled lane needs a second toolchain and a + separate architecture verification to stay honest. +- **Emulate arm64 on the x64 runner.** Rejected: an emulated build both hides + architecture mistakes and turns a bounded packaging job into a long one. +- **Ship only the AppImage for arm64.** Rejected: deb and rpm are the formats + arm64 Linux distributions install, and FPM publishes a native linux-arm64 + build, so the lane costs nothing extra. +- **Keep Linux x64 only.** Rejected: the request is a supported platform that + cannot run the published artifact at all. +- **Pin both architectures on each Linux target.** Rejected: electron-builder's + configured arch list outranks the CLI flag, so each lane would build its + non-native architecture around its own sidecar. diff --git a/docs/adr/README.md b/docs/adr/README.md index fa8b975aa1..f53e5db1ae 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -354,3 +354,4 @@ Each ADR includes: | provider-system-certificates | [Desktop sidecar uses OS-trusted certificates](provider-system-certificates.md) | Accepted | | image-generation-capability | [Image generation as a configured Agent capability](image-generation-capability.md) | Accepted | | retained-browser-pages-per-tab | [Retain a host-owned browser page per resource tab](retained-browser-pages-per-tab.md) | Accepted | +| 0318 | [Publish native Linux arm64 artifacts](0318-linux-arm64-release-lane.md) | Accepted (D638) | diff --git a/docs/spec/00-baseline.md b/docs/spec/00-baseline.md index 5f161d2533..d7fabe58fd 100644 --- a/docs/spec/00-baseline.md +++ b/docs/spec/00-baseline.md @@ -21,7 +21,7 @@ > packaged application update modes in D120 / ADR 0022 while preserving D010. > `0.4.7` lifts D010's macOS-only release scope through D126: tag builds > publish installers and electron-updater feeds for macOS arm64, Windows x64, -> and Linux x64. +> and Linux x64 and arm64. > D285 adds a native macOS Intel x64 tag lane alongside the arm64 lane; both > macOS architectures publish DMG/ZIP artifacts from their matching runners. > `0.4.8` moves the durable Projects index out of the home sidebar and into @@ -145,7 +145,7 @@ 26. Plugin trust first step: **sha256 checksum; signature later** 27. First release platform: **macOS arm64 only** — lifted in `0.4.7`/D126; tag builds now publish native macOS arm64 and Intel x64, Windows x64, and - Linux x64 AppImage, deb, and rpm artifacts + Linux x64 and arm64 AppImage, deb, and rpm artifacts 28. TS schema library: **typebox** 29. i18n library: **i18next** 30. Bash: **non-interactive, streamed, and resolved from the selectable shell diff --git a/docs/spec/01-product/01-product-scope.md b/docs/spec/01-product/01-product-scope.md index dbbb6fd886..bfa6413b6e 100644 --- a/docs/spec/01-product/01-product-scope.md +++ b/docs/spec/01-product/01-product-scope.md @@ -59,7 +59,7 @@ across devices without synchronizing conversation history or source files. ## 4. Current shipped scope -- Electron desktop app for macOS arm64 and Intel x64, Windows x64, and Linux x64 release lanes +- Electron desktop app for macOS arm64 and Intel x64, Windows x64, and Linux x64 and arm64 release lanes - English default UI + i18n framework - Session create/switch/restore - Multi-provider configuration @@ -142,4 +142,4 @@ the internal `page = "chat"` route value; that value is not an operating mode. | macOS Apple Silicon | Published | Primary development and acceptance platform; signing/notarization remains credential-gated | | macOS Intel | Published | Native x64 DMG/ZIP release lane; signing/notarization remains credential-gated | | Windows x64 | Published | NSIS installer, portable ZIP, and in-app update lane for NSIS; native qualification continues | -| Linux x64 | Published | AppImage, deb, and rpm packages; AppImage update lane; glibc 2.35+ (Ubuntu 22.04, Debian 12, Fedora 36+); native qualification continues | +| Linux x64 and arm64 | Published | AppImage, deb, and rpm packages; AppImage update lane; glibc 2.35+ (Ubuntu 22.04, Debian 12, Fedora 36+); native qualification continues | diff --git a/docs/spec/02-architecture/02-tech-stack.md b/docs/spec/02-architecture/02-tech-stack.md index 005ff6ff8b..883a9de394 100644 --- a/docs/spec/02-architecture/02-tech-stack.md +++ b/docs/spec/02-architecture/02-tech-stack.md @@ -23,7 +23,7 @@ | Node runtime | Node.js | `>= 22.19` | pi requirement | | DB | SQLite | Rust host-core via `rusqlite` | sessions/settings | -| Packaging | electron-builder | stable | macOS arm64 and Intel x64, Windows x64, and Linux x64 release lanes | +| Packaging | electron-builder | stable | macOS arm64 and Intel x64, Windows x64, and Linux x64 and arm64 release lanes | | Package manager | pnpm | 11.18.x | JS monorepo | | Lint/test | style-token checker (`scripts/check-style-tokens.mjs`) + vitest + cargo test; general JS linter still open (biome vs oxlint) | stable | dual stack quality | | Schema (TS) | typebox | frozen (D011) | shared contracts | diff --git a/docs/spec/06-delivery/01-mvp-milestones.md b/docs/spec/06-delivery/01-mvp-milestones.md index edc14e7e16..37a91f0ab4 100644 --- a/docs/spec/06-delivery/01-mvp-milestones.md +++ b/docs/spec/06-delivery/01-mvp-milestones.md @@ -87,7 +87,7 @@ Status: **Complete except a release-qualification run for macOS artifacts** Goal: daily-usable package. Deliverables: -- packaging (macOS arm64 and Intel x64, Windows x64, and Linux x64 tag artifacts; D126/D285) +- packaging (macOS arm64 and Intel x64, Windows x64, and Linux x64 and arm64 tag artifacts; D126/D285) - settings polish - logging/error boundaries - session management basics @@ -175,7 +175,7 @@ Remaining work is tracked as product hardening rather than unstarted MVP scope: ## Release constraint -Tag releases publish **macOS arm64 and Intel x64, Windows x64, and Linux x64** +Tag releases publish **macOS arm64 and Intel x64, Windows x64, and Linux x64 and arm64** artifacts (D126 lifts the original D010 macOS-only constraint; D285 adds the native Intel lane). diff --git a/docs/spec/06-delivery/04-e2e-test-plan.md b/docs/spec/06-delivery/04-e2e-test-plan.md index a0a2a705b2..eaa650762a 100644 --- a/docs/spec/06-delivery/04-e2e-test-plan.md +++ b/docs/spec/06-delivery/04-e2e-test-plan.md @@ -502,7 +502,7 @@ levels does not waive the relevant E2E gate. | Requirement | Detail | |---|---| -| Platform | macOS arm64 and Intel x64, Windows x64, and Linux x64 release targets (D126/D285) | +| Platform | macOS arm64 and Intel x64, Windows x64, and Linux x64 and arm64 release targets (D126/D285, D638 / ADR 0318) | | Profile | Clean `~/.pi-desktop` profile (no prior config) | | Fixtures | Sample project directory (`examples/fixtures/sample-project/`) | | Sample plugin | `examples/plugins/hello` loaded from local path | @@ -653,7 +653,7 @@ identify the platform validation still needed. - **Preconditions**: A `vX.Y.Z` tag matches `apps/desktop/package.json`; the Linux x64 release runner can complete `dist:linux` and has a system Electron - available for repackaging validation. + available for repackaging validation. The arm64 lane is E2E-192a. - **Steps**: 1) Run the tag release workflow. 2) Inspect the published GitHub Release assets. 3) Confirm the versioned `PI-Desktop-X.Y.Z-linux-x64.asar` asset is present. 4) Place that archive in @@ -669,6 +669,31 @@ identify the platform validation still needed. - **Status**: Documented; artifact export is unit-covered, native system-Electron repackaging remains runner validation +#### E2E-192a: Linux arm64 release lane publishes native arm64 packages + +- **Preconditions**: A `vX.Y.Z` tag matches `apps/desktop/package.json`; GitHub's + arm64 `ubuntu-22.04-arm` runner is available to the repository. +- **Steps**: 1) Run the tag release workflow. 2) Confirm the arm64 lane ran on + an `aarch64` runner and that the packaged + `target/release/pi-desktop-host-core` is an AArch64 binary. 3) Inspect the + published Release assets for `PI-Desktop-X.Y.Z-linux-arm64.AppImage`, + `pi-desktop_X.Y.Z_arm64.deb`, `pi-desktop-X.Y.Z-aarch64.rpm`, + `PI-Desktop-X.Y.Z-linux-arm64.asar`, and + `pi-host-X.Y.Z-linux-arm64.tar.gz` with its `.sha256`. 4) Confirm + `latest-linux.yml` still lists the x64 AppImage while + `latest-linux-arm64.yml` lists the arm64 one. 5) Install the arm64 AppImage, + deb, or rpm on an arm64 Linux machine and launch it. +- **Expected**: Both feeds describe exactly one architecture-labelled AppImage + each, the arm64 packages carry an arm64 host-core, the app starts on arm64 + Linux, and merging the lanes never replaces the x64 feed with the arm64 one. +- **Specs linked**: `06-delivery/06-release-runbook.md`, `01-product/01-product-scope.md` +- **Acceptance**: Quality (release artifact and packaging compatibility) +- **Milestone**: M6+ +- **Status**: The matrix, feed naming, artifact naming, and the ASAR export are + unit/source-contract covered (`ci-workflow.test.mjs`, `release-asar.test.mjs`); + native arm64 installation remains runner validation. Microphone capture stays + Raspberry Pi only on arm64 Linux (D638 / ADR 0318). + #### E2E-200: Linux RPM preserves the Wayland desktop identity - **Preconditions**: A Linux x64 package validation run or tag release can @@ -3445,7 +3470,7 @@ identify the platform validation still needed. #### E2E-195: Linux glibc below 2.35 names supported distros -- **Preconditions**: Linux x64 packaged app; the machine glibc is older than +- **Preconditions**: Linux x64 or arm64 packaged app; the machine glibc is older than 2.35 (for example Ubuntu 20.04 / Debian 11 / Fedora 35), or a test doubles `process.report` to `2.31`. - **Steps**: 1) Launch the AppImage, deb, or rpm. 2) Observe the main window and @@ -9186,9 +9211,8 @@ The `US-UI-*` visual scenarios (§UI shell visual scenarios) trace to the Codex parity decisions in [decisions-log §D](../08-meta/decisions-log.md) rather than the A–H criteria; their gold source is the capture suite. -The release artifact paths are covered by E2E-192, E2E-196a, E2E-196b, E2E-196c, -and E2E-200 -(Quality, M6+). +The release artifact paths are covered by E2E-192, E2E-192a, E2E-196a, +E2E-196b, E2E-196c, and E2E-200 (Quality, M6+). --- diff --git a/docs/spec/06-delivery/06-release-runbook.md b/docs/spec/06-delivery/06-release-runbook.md index 14f47340cb..e4065f8b4e 100644 --- a/docs/spec/06-delivery/06-release-runbook.md +++ b/docs/spec/06-delivery/06-release-runbook.md @@ -1,7 +1,7 @@ # 06. Desktop Release Runbook > Scope: D126/D285/D603 tag artifacts for macOS arm64 and Intel x64, Windows x64, -> and Linux x64, including the Linux system-Electron ASAR asset; +> and Linux x64 and arm64, including the Linux system-Electron ASAR assets; > macOS signing/notarization remains the detailed qualification lane below. > Cross-references: [milestones](01-mvp-milestones.md) · [process model](../03-runtime/07-process-model.md) · [security](../05-security/01-security.md) @@ -261,11 +261,16 @@ unsigned lane is for debugging and does not imply Gatekeeper qualification. DMG, ZIP, NSIS, AppImage, deb, rpm, blockmap, and updater feed outputs are already compressed or compression-insensitive. The workflow therefore uploads their temporary Actions artifacts with compression level zero before the publish job -assembles the GitHub Release. The Linux runner also copies -`linux-unpacked/resources/app.asar` to the versioned -`PI-Desktop--linux-x64.asar` asset before upload. This preserves the -exact archive used by the Linux installers for downstream repackaging with a -system Electron. +assembles the GitHub Release. The Linux runners also copy their unpacked +`resources/app.asar` (`linux-unpacked` on x64, `linux-arm64-unpacked` on arm64) +to the versioned `PI-Desktop--linux-.asar` asset before upload. +This preserves the exact archive used by the Linux installers for downstream +repackaging with a system Electron. + +The Linux updater feeds carry the architecture they were built for: +`latest-linux.yml` for x64 and `latest-linux-arm64.yml` for arm64. Those are the +channel files `electron-updater` requests on the matching architecture, so a +lane fails before upload if its own feed is missing. ### 4.4 Documentation site deployment @@ -565,7 +570,7 @@ their electron-updater manifests. Run a target command on that target OS: macOS Apple Silicon: pnpm --filter @pi-desktop/desktop run dist:mac -- --arm64 macOS Intel: pnpm --filter @pi-desktop/desktop run dist:mac -- --x64 Windows: pnpm --filter @pi-desktop/desktop dist:win -Linux: pnpm --filter @pi-desktop/desktop dist:linux +Linux: pnpm --filter @pi-desktop/desktop dist:linux -- --x64|--arm64 ``` The Windows `dist:win` command runs `scripts/build-desktop-release.mjs`, @@ -573,8 +578,9 @@ which invokes electron-builder once for NSIS and once for ZIP so each package gets the correct updater distribution marker. The macOS packages include `bin/pi-desktop-host-core` built for their runner -architecture; Windows includes `bin/pi-desktop-host-core.exe`; Linux includes -`bin/pi-desktop-host-core`. Signing, rollback, and installer upgrade +architecture; the Linux packages do the same on their native x64 and arm64 +runners; Windows includes `bin/pi-desktop-host-core.exe`. Signing, rollback, +and installer upgrade qualification remain release hardening work; publication is active under D126/D285/D603. @@ -586,8 +592,9 @@ Native-runner output matrix: `PI-Desktop--x64-mac.zip` - Windows x64: NSIS installer `PI-Desktop-Setup-.exe` and portable ZIP `PI-Desktop-Portable-.zip` -- Linux x64: AppImage, deb, and rpm -- Linux x64 system Electron asset: `PI-Desktop--linux-x64.asar` +- Linux x64 and arm64: AppImage, deb, and rpm +- Linux system Electron assets: `PI-Desktop--linux-x64.asar` and + `PI-Desktop--linux-arm64.asar` The portable Windows ZIP target does not write `latest.yml`. The Windows release helper builds NSIS and ZIP separately and stamps the ZIP app metadata @@ -608,7 +615,7 @@ target Electron resources layout together with the native host and other resources from the target package, then launch it with: ```bash -electron PI-Desktop--linux-x64.asar +electron PI-Desktop--linux-.asar ``` Shell smoke on each native runner: @@ -626,10 +633,15 @@ Shell smoke on each native runner: - Linux deb/rpm and the Windows portable ZIP remain notify-and-link update modes. Packaged macOS, Windows NSIS, and Linux AppImage use in-app `electron-updater`. -- Linux x64 packages are built on Ubuntu 22.04 so host-core needs glibc 2.35 - or newer (Ubuntu 22.04, Debian 12, Fedora 36+). The tag job runs +- Linux x64 and arm64 packages are built on Ubuntu 22.04 (native `ubuntu-22.04` + and `ubuntu-22.04-arm` runners), so host-core needs glibc 2.35 or newer + (Ubuntu 22.04, Debian 12, Fedora 36+). Each tag job runs `scripts/check-linux-host-glibc.mjs` and refuses a binary that needs a newer glibc. +- Linux arm64 microphone capture works on Raspberry Pi boards only: + `@picovoice/pvrecorder-node` classifies Linux arm64 by `/proc/cpuinfo` CPU + part. Speech-to-text (`transcribe-cpp` ships a `linux-arm64-cpu-vulkan` + bundle) and every other feature work on any arm64 Linux device. - Rollback, staged rollout, and prerelease channel policy remain open release work. Existing unsigned macOS installs may need one manual signed DMG before in-app updates succeed. diff --git a/docs/spec/08-meta/decisions-log.md b/docs/spec/08-meta/decisions-log.md index f96b9750f8..cb529ea7d3 100644 --- a/docs/spec/08-meta/decisions-log.md +++ b/docs/spec/08-meta/decisions-log.md @@ -32,6 +32,7 @@ This log freezes previously open questions into concrete decisions. | D635 | Work-area-capped 800×560 window minimum | **Supersede the 1040×700 window minimum in D156 / D447 (and the matching clauses of ADR 0029 / ADR 0238) and the `1040..10000` `window/setWorkPanelChatWidth` range (ADR 0146): Electron enforces an 800×560 minimum, capped per dimension to the current display work area by `clampMinimumSizeToWorkArea`. The chat-width IPC and renderer accept `800..10000`. On narrow windows the existing `workPanelLayout` budget caps the docked panel so MainChat keeps its 450px floor, collapsing the sidebar first. See US-UI-19 and E2E-167.** | At 150% Windows scaling the work area is about 1280×672 DIP, so a fixed minimum could exceed the screen and leave the window unfittable. | | D636 | Local permission approvals have no automatic deadline | **Amend D005 / ADR 0011 and supersede the local tool-permission timeout clauses in the runtime and UX specs: a permission-gated `tools.execute` request remains pending in host-core, the renderer, and the transport until the user chooses Allow once, Allow for session, or Deny, or the request is cancelled / the process shuts down. Remove the 120-second countdown and timeout fields from the local permission contract. Tool-specific execution budgets and the separate RACP/Plan approval lifetimes remain unchanged. See ADR 0310, issue #1214, and E2E-017.** | A visible permission request could be missed while the user worked elsewhere; automatic denial after 120 seconds silently prevented the requested action. Keeping cancellation and execution budgets preserves control and resource safety without turning inattention into a decision. | | D637 | Remove the Windows frameless resize rim | **Disable the Windows main window's thick frame while retaining Electron 43.6 native frameless edge and corner resizing. Apply a 4 DIP native rounded shape by default; authorized plugin themes may choose an integer radius from 0 to 24 DIP. Keep the D635 minimum-size contract and existing work-panel resize ownership. See ADR 0317 and E2E-167.** | The thick frame paints an unwanted left, bottom, and right rim that themes cannot remove. Native hit testing and shape keep resizing and transparent outer corners without renderer resize IPC. | +| D638 | Publish native Linux arm64 artifacts | **Amend D126 / D285 / D603 / ADR 0022: tag releases publish native Linux arm64 AppImage, deb, and rpm packages, built on GitHub's native `ubuntu-22.04-arm` runner and carrying an arm64 `pi-desktop-host-core`. The static Linux targets drop their pinned `arch` and take the workflow's `--x64` / `--arm64` flag; `linux.artifactName` becomes `PI-Desktop--linux-.AppImage`; each Linux lane verifies its architecture-named updater feed (`latest-linux.yml` on x64, `latest-linux-arm64.yml` on arm64); the ASAR export reads `linux-unpacked` or `linux-arm64-unpacked` and publishes `PI-Desktop--linux-.asar`. `pi-host-bundle` builds both Linux architectures and `PUBLISHED_TARGETS` gains `linux-arm64`. Updater ownership, signing, and delivery modes are unchanged. See ADR 0318, issue #1281, and E2E-192a.** | arm64 Linux devices could not install or run the published x64 artifact, and a cross-built or emulated lane would ship a mismatched Rust sidecar. | | D450 | Signed macOS GitHub Releases | **Amend D078 / ADR 0022: GitHub tag releases Developer ID-sign, notarize (`notarytool` via electron-builder 26), staple, and Gatekeeper-verify macOS DMG/ZIP before upload, using identity `Developer ID Application: XingYu Liu (DUV63RKYTW)` / team `DUV63RKYTW` from Actions secrets (`CSC_LINK`, `CSC_KEY_PASSWORD`, `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`). Missing secrets fail the job. Local unsigned packaging without a certificate remains. `workflow_dispatch` may set `sign_macos: false` only for unsigned debug artifacts. Packaged macOS uses in-app `electron-updater` (ZIP + merged `latest-mac.yml`); Linux deb/rpm and Windows portable ZIP stay notify-and-link. No afterPack/afterSign adhoc codesign (ADR 0278).** | Production DMGs must open without a Gatekeeper warning, and signed macOS installs can download and restart into a new tag. See ADR 0289, E2E-196c, E2E-067A. | ## B. Secondary implementation defaults @@ -6210,9 +6211,9 @@ that was sitting at the bottom — including after the turn had finished. typed error instead of hanging behind an invisible prompt. - The desktop resolves the bundle for the remote platform at its own version, holds the SHA-256 the release publishes, and refuses an unpublished target - (`linux-x64` only today) before any download. The uploaded script downloads, - verifies, and installs the bundle under the remote `$HOME`; no executable - bytes cross the SSH channel. + (`linux-x64` and `linux-arm64`) before any download. The uploaded script + downloads, verifies, and installs the bundle under the remote `$HOME`; no + executable bytes cross the SSH channel. - The script runs under `umask 077` and echoes `PI_HOST_READY` / `PI_HOST_PAIRING_TOKEN`, so the single-use pairing token exists only in a work file and on the SSH channel, never in a world-readable path or a URL @@ -7348,3 +7349,38 @@ must keep splitting are covered by `markdown-blocks.test.mjs`. D635 work-area-capped minimum remains in force. The removed thick frame leaves no native shadow control for themes; external shadow needs a separate window-geometry decision. See ADR 0317 and E2E-167. + +## 2026-10-01 — Publish native Linux arm64 artifacts (D638) + +- Tag releases publish Linux arm64 AppImage, deb, and rpm packages beside the + x64 ones. Each lane runs on a native GitHub-hosted Ubuntu 22.04 runner + (`ubuntu-22.04` and `ubuntu-22.04-arm`) and packs the `pi-desktop-host-core` + binary it just built, so the sidecar architecture always matches the Electron + app. Both lanes keep the glibc 2.35 floor. +- The Linux targets no longer pin `arch`: electron-builder prefers a + configuration arch list over the CLI flag, so pinning both architectures + would make each lane build the other one around its own sidecar. The + workflow passes the matching `--x64` / `--arm64` flag and verifies + `uname -m` before packaging. +- The AppImage name carries its architecture + (`PI-Desktop--linux-x64.AppImage`, + `PI-Desktop--linux-arm64.AppImage`). The x64 asset is therefore + renamed from its previous version-only name; in-app updates are unaffected + because the updater reads the published feed. +- electron-builder names each Linux feed after the architecture it built + (`latest-linux.yml` on x64, `latest-linux-arm64.yml` on arm64), which is what + `electron-updater` requests on those architectures, so the lanes cannot + overwrite each other's feed and each lane verifies that name before upload + instead of renaming it. The publish job merges both artifacts without losing + a feed. +- `scripts/export-linux-asar.mjs` takes the lane architecture and exports + `PI-Desktop--linux-.asar` from `linux-unpacked` (x64) or + `linux-arm64-unpacked` (arm64). +- `release.yml`'s `pi-host-bundle` job builds both Linux architectures and + `PUBLISHED_TARGETS` publishes `linux-x64` and `linux-arm64`, so an arm64 + desktop can bootstrap an arm64 remote host (D375 / ADR 0292). +- Known limitation: microphone capture stays unavailable on arm64 Linux + devices other than Raspberry Pi boards, because `@picovoice/pvrecorder-node` + knows only Raspberry Pi CPU parts for Linux arm64. Speech-to-text and the + rest of the app have no architecture-specific dependency. See ADR 0318, + issue #1281, and E2E-192a. diff --git a/docs/spec/README.md b/docs/spec/README.md index 28f54bb738..3aec5df4c6 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -99,5 +99,5 @@ docs/spec/ 10. Local permission approvals have no automatic deadline; Bash timeout 60s by default 11. Local user-installable plugins (market later) -12. Tag releases = macOS arm64 and Intel x64, Windows x64, and Linux x64 (D126/D285) +12. Tag releases = macOS arm64 and Intel x64, Windows x64, and Linux x64 and arm64 (D126/D285, D638/ADR 0318) 13. Universal provider/model coverage (native + OpenAI-compatible + custom) diff --git a/docs/zh-CN/spec/00-baseline.md b/docs/zh-CN/spec/00-baseline.md index f5ef5de929..8cebb0ee80 100644 --- a/docs/zh-CN/spec/00-baseline.md +++ b/docs/zh-CN/spec/00-baseline.md @@ -24,7 +24,7 @@ > 在 D120 / ADR 0022 中打包应用程序更新模式,同时保留 D010。 > `0.4.7` 通过 D126 提升了 D010 仅适用于 macOS 的发行范围:标签构建 > 发布 macOS arm64、Windows x64 的安装程序和电子更新程序源, -> 和 Linux x64。 +> 和 Linux x64 及 arm64。 > D285 在 arm64 通道旁增加本机 macOS Intel x64 标签通道;两个 macOS > 架构都从匹配的运行器发布 DMG/ZIP 工件。 > `0.4.8` 将持久项目索引从主页侧边栏移至 @@ -124,7 +124,7 @@ 26.插件信任第一步:**sha256校验和;稍后签名** 27. 第一个发布平台:**macOS 仅arm64** — 在 preload/D126 中提升; 标签构建现在发布本机 macOS arm64 和 Intel x64、Windows x64 及 - Linux x64 AppImage、deb 和 rpm 工件 + Linux x64 及 arm64 AppImage、deb 和 rpm 工件 28. TS模式库:**typebox** 29. i18n 库:**i18next** 30. Bash:**非交互式、流式传输并从可选择的 shell 解析 diff --git a/docs/zh-CN/spec/01-product/01-product-scope.md b/docs/zh-CN/spec/01-product/01-product-scope.md index f1138ce996..93ee001189 100644 --- a/docs/zh-CN/spec/01-product/01-product-scope.md +++ b/docs/zh-CN/spec/01-product/01-product-scope.md @@ -56,7 +56,7 @@ MCP 服务器、常驻服务和消息总线集成。 ## 4. 当前发货范围 -- 适用于 macOS arm64、Intel x64、Windows x64 和 Linux x64 发布通道的 Electron 桌面应用程序 +- 适用于 macOS arm64、Intel x64、Windows x64 和 Linux x64 及 arm64 发布通道的 Electron 桌面应用程序 - 英文默认UI + i18n框架 - 会议 create/switch/restore - 多提供商配置 @@ -136,4 +136,4 @@ Plan 和 Goal 是合约模式,而不是严格的只读安全配置文件:Bas | macOS 苹果芯片 | 已发表 | 初级开发及验收平台; signing/notarization 仍受凭证控制 | | macOS 英特尔 | 已发表 | 本机 x64 DMG/ZIP 发布通道;signing/notarization 仍受凭证控制 | | Windows x64 | 已发表 | NSIS 安装程序、免安装便携版 ZIP,以及仅适用于 NSIS 的应用内更新通道;本土资格继续 | -| Linux x64 | 已发表 | AppImage、deb 和 rpm 包;AppImage 更新通道;glibc 2.35+(Ubuntu 22.04、Debian 12、Fedora 36+);本土资格继续 | +| Linux x64 和 arm64 | 已发表 | AppImage、deb 和 rpm 包;AppImage 更新通道;glibc 2.35+(Ubuntu 22.04、Debian 12、Fedora 36+);本土资格继续 | diff --git a/docs/zh-CN/spec/02-architecture/02-tech-stack.md b/docs/zh-CN/spec/02-architecture/02-tech-stack.md index 481e670149..7647520331 100644 --- a/docs/zh-CN/spec/02-architecture/02-tech-stack.md +++ b/docs/zh-CN/spec/02-architecture/02-tech-stack.md @@ -25,7 +25,7 @@ | Node 运行时 | Node.js | `>= 22.19` | 圆周率要求 | | 数据库 | SQLite | Rust host-core 通过 `rusqlite` | sessions/settings | -| 包装 | 电子制造商 | 稳定 | macOS arm64、Intel x64、Windows x64 和 Linux x64 释放通道 | +| 包装 | 电子制造商 | 稳定 | macOS arm64、Intel x64、Windows x64 和 Linux x64 及 arm64 释放通道 | | 包管理器 | PNPM | 11.18.x | JS 单一仓库 | | Lint/test | 样式令牌检查器(`scripts/check-style-tokens.mjs`)+ vitest + 货物测试;一般 JS linter 仍然打开(biome vs oxlint) | 稳定 | 双堆栈质量 | | 架构 (TS) | 打字机 | 冷冻(D011) | 共享合约 | diff --git a/docs/zh-CN/spec/06-delivery/01-mvp-milestones.md b/docs/zh-CN/spec/06-delivery/01-mvp-milestones.md index 0ea089d716..352a2f6e02 100644 --- a/docs/zh-CN/spec/06-delivery/01-mvp-milestones.md +++ b/docs/zh-CN/spec/06-delivery/01-mvp-milestones.md @@ -90,7 +90,7 @@ Goal:用户可安装的本地扩展系统。 Goal:日常可用的包。 可交付成果: -- 包装(macOS arm64、Intel x64、Windows x64 和 Linux x64 标签工件;D126/D285) +- 包装(macOS arm64、Intel x64、Windows x64 和 Linux x64 及 arm64 标签工件;D126/D285) - 设置抛光 - logging/error 边界 - 会话管理基础知识 @@ -177,7 +177,7 @@ E2E-108/E2E-109 覆盖主机重启中断且无重播。 ## 释放约束 -标签版本发布 **macOS arm64、Intel x64、Windows x64 和 Linux x64** 工件 +标签版本发布 **macOS arm64、Intel x64、Windows x64 和 Linux x64 及 arm64** 工件 (D126 解除了原始 D010 仅限 macOS 的限制;D285 增加了本机 Intel 通道)。 ## 粗暴的努力(独奏) diff --git a/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md b/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md index 6485c12796..4c558434ac 100644 --- a/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md +++ b/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md @@ -173,7 +173,7 @@ unit/integration 测试;代码 pull request 使用有选择且高价值的 E2E | 要求 | 详情 | |---|---| -| 平台 | macOS arm64、Intel x64、Windows x64 和 Linux x64 发行目标 (D126/D285) | +| 平台 | macOS arm64、Intel x64、Windows x64 和 Linux x64 及 arm64 发行目标 (D126/D285、D638 / ADR 0318) | | 公司简介 | 干净的 `~/.pi-desktop` 配置文件(无需事先配置) | | 固定装置 | 示例项目目录 (`examples/fixtures/sample-project/`) | | 示例插件 | 从本地路径加载 `examples/plugins/hello` | @@ -1745,7 +1745,7 @@ task-candidate E2E 从请求工作树运行,但使用主工作区已经准备 #### E2E-195:Linux glibc 低于 2.35 时列出支持的发行版 -- **先决条件**:Linux x64 打包应用;本机 glibc 低于 2.35(例如 Ubuntu 20.04 / +- **先决条件**:Linux x64 或 arm64 打包应用;本机 glibc 低于 2.35(例如 Ubuntu 20.04 / Debian 11 / Fedora 35),或测试将 `process.report` 设为 `2.31`。 - **步骤**:1) 启动 AppImage、deb 或 rpm。2) 观察主窗口和致命横幅。3) 确认 host-core 没有进入重启循环。 @@ -1817,6 +1817,31 @@ task-candidate E2E 从请求工作树运行,但使用主工作区已经准备 `apps/desktop/test/subagent-wiring.test.mjs`、`packages/agent-runtime/src/subagent-definitions.test.ts`、 `packages/shared/src/subagent-presets.test.ts`);完整 UI 旅程为草稿 +#### E2E-192a:Linux arm64 发布通道发布原生 arm64 包 + +- **先决条件**:`vX.Y.Z` 标签与 `apps/desktop/package.json` 一致;仓库可使用 GitHub + 的 arm64 `ubuntu-22.04-arm` 运行器。 +- **步骤**:1) 运行标签发布工作流程。2) 确认 arm64 通道运行在 + `aarch64` 运行器上,且打包出的 + `target/release/pi-desktop-host-core` 是 AArch64 二进制。3) 检查 + 已发布的 Release 资产中是否有 `PI-Desktop-X.Y.Z-linux-arm64.AppImage`、 + `pi-desktop_X.Y.Z_arm64.deb`、`pi-desktop-X.Y.Z-aarch64.rpm`、 + `PI-Desktop-X.Y.Z-linux-arm64.asar`,以及带 `.sha256` 的 + `pi-host-X.Y.Z-linux-arm64.tar.gz`。4) 确认 + `latest-linux.yml` 仍列出 x64 AppImage,而 + `latest-linux-arm64.yml` 列出 arm64 的那个。5) 在 arm64 Linux 机器上安装 arm64 + 的 AppImage、deb 或 rpm 并启动它。 +- **预期**:两个更新源各自只描述一个带架构标记的 AppImage, + arm64 包携带 arm64 host-core,应用能在 arm64 Linux 上启动, + 且合并通道永远不会用 arm64 更新源替换 x64 更新源。 +- **链接规格**:`06-delivery/06-release-runbook.md`、`01-product/01-product-scope.md` +- **验收**:质量(发布工件与打包兼容性) +- **里程碑**:M6+ +- **状态**:矩阵、更新源命名、工件命名和 ASAR 导出由单元/源代码契约覆盖 + (`ci-workflow.test.mjs`、`release-asar.test.mjs`); + 原生 arm64 安装仍需运行器验证。麦克风采集在 arm64 Linux 上仍仅限 + Raspberry Pi (D638 / ADR 0318)。 + #### E2E-200:Linux RPM 保留 Wayland 桌面身份 - **先决条件**:Linux x64 软件包验证运行或标签发布可以在 Ubuntu 22.04 @@ -5603,6 +5628,9 @@ eleven-tool-round desktop paths are verified by [决策日志 §D](/zh-CN/spec/08-meta/decisions-log) 中的法典平价决策 而不是 A-H 标准;他们的黄金来源是捕获套件。 +发布工件路径由 E2E-192、E2E-192a、E2E-196a、E2E-196b、E2E-196c 和 E2E-200 覆盖 +(质量,M6+)。 + --- ## 9. AI 必须如何更新本文档 diff --git a/docs/zh-CN/spec/06-delivery/06-release-runbook.md b/docs/zh-CN/spec/06-delivery/06-release-runbook.md index 1a5c0595f7..044b056e48 100644 --- a/docs/zh-CN/spec/06-delivery/06-release-runbook.md +++ b/docs/zh-CN/spec/06-delivery/06-release-runbook.md @@ -3,7 +3,8 @@ > **翻译说明:** 本页是与 [英文源规格](/spec/06-delivery/06-release-runbook) 一一对应的机器辅助翻译。代码、协议字段和标识符保持原文;如翻译与英文源事实有歧义,以英文版本为准。 -> 范围:macOS arm64、Intel x64、Windows x64 和 Linux x64 的 D126/D285/D603 标记工件; +> 范围:macOS arm64、Intel x64、Windows x64 和 Linux x64 及 arm64 的 D126/D285/D603 标记工件, +> 包括 Linux 系统 Electron ASAR 资产; > macOS signing/notarization 保留下面的详细资格通道。 > 交叉引用:[里程碑](/zh-CN/spec/06-delivery/01-mvp-milestones) · [进程模型](/zh-CN/spec/03-runtime/07-process-model) · [安全性](/zh-CN/spec/05-security/01-security) @@ -216,7 +217,15 @@ macOS ZIP 在安装包根目录包含 `PI-Desktop.app`。DMG 和 ZIP 都不附 DMG、ZIP、NSIS、AppImage、deb、rpm、块图和更新程序提要输出已 压缩或压缩不敏感。因此,工作流程会上传它们的 发布作业之前压缩级别为零的临时操作工件 -组装 GitHub 版本。 +组装 GitHub 版本。Linux 运行器还会在上传前 +把各自解压出的 `resources/app.asar`(x64 为 `linux-unpacked`,arm64 为 +`linux-arm64-unpacked`)复制为带版本的 +`PI-Desktop--linux-.asar` 资产。这保留了 Linux 安装器用于 +以系统 Electron 下游重新打包时使用的那份确切归档。 + +Linux 更新源带上它们所构建的架构:x64 为 `latest-linux.yml`,arm64 为 +`latest-linux-arm64.yml`。这正是 `electron-updater` 在对应架构上请求的通道文件, +因此某条通道若缺少自己的更新源,会在上传前就失败。 ### 4.4 CNB 镜像触发 @@ -476,15 +485,15 @@ project/Temporary 使用消息加会话图标创建控件。 macOS Apple Silicon: pnpm --filter @pi-desktop/desktop run dist:mac -- --arm64 macOS Intel: pnpm --filter @pi-desktop/desktop run dist:mac -- --x64 Windows: pnpm --filter @pi-desktop/desktop dist:win -Linux: pnpm --filter @pi-desktop/desktop dist:linux +Linux: pnpm --filter @pi-desktop/desktop dist:linux -- --x64|--arm64 ``` Windows 的 `dist:win` 命令会运行 `scripts/build-desktop-release.mjs`,分别调用 一次 electron-builder 构建 NSIS 和 ZIP,确保每个包写入正确的更新器发行类型标记。 -macOS 软件包包括按本机架构构建的 `bin/pi-desktop-host-core`;Windows -软件包包括 `bin/pi-desktop-host-core.exe`;Linux 包括 -`bin/pi-desktop-host-core`。签名、回滚和安装程序升级资质仍保持发布 +macOS 软件包包括按本机架构构建的 `bin/pi-desktop-host-core`; +Linux 软件包在其本机 x64 与 arm64 运行器上做同样的事;Windows +软件包包括 `bin/pi-desktop-host-core.exe`。签名、回滚和安装程序升级资质仍保持发布 硬化工作;发布本身已在 D126/D285/D603 下启用。 Native-runner 输出矩阵: @@ -495,8 +504,9 @@ Native-runner 输出矩阵: `PI-Desktop--x64-mac.zip` - Windows x64:NSIS 安装程序 `PI-Desktop-Setup-.exe` 和便携版 ZIP `PI-Desktop-Portable-.zip` -- Linux x64:AppImage、deb 和 rpm -- Linux x64 系统 Electron 产物:`PI-Desktop--linux-x64.asar` +- Linux x64 和 arm64:AppImage、deb 和 rpm +- Linux 系统 Electron 资产:`PI-Desktop--linux-x64.asar` 和 + `PI-Desktop--linux-arm64.asar` 便携版 Windows ZIP 目标不会写入 `latest.yml`。Windows 发布脚本会分别构建 NSIS 和 ZIP,并给 ZIP 的应用元数据写入 `piDistribution = "zip"`;已打包的 ZIP 运行使用 @@ -513,7 +523,7 @@ Electron 二进制文件的应用发生冲突。 与目标软件包内的本机主机及其他资源放在一起,然后用以下命令启动: ```bash -electron PI-Desktop--linux-x64.asar +electron PI-Desktop--linux-.asar ``` 每个本机运行器上的外壳冒烟测试: @@ -529,5 +539,12 @@ electron PI-Desktop--linux-x64.asar ## 7. 已知限制 - Linux deb/rpm 和 Windows 便携版 ZIP 仍保持通知和链接更新模式。打包的 macOS、Windows NSIS 和 Linux AppImage 使用应用内 `electron-updater`。 -- Linux x64 包在 Ubuntu 22.04 上构建,因此 host-core 需要 glibc 2.35 或更高版本(Ubuntu 22.04、Debian 12、Fedora 36+)。标签作业运行 `scripts/check-linux-host-glibc.mjs`,拒绝需要更新 glibc 的二进制文件。 +- Linux x64 和 arm64 包在 Ubuntu 22.04 上构建(本机 `ubuntu-22.04` 和 + `ubuntu-22.04-arm` 运行器),因此 host-core 需要 glibc 2.35 或更高版本 + (Ubuntu 22.04、Debian 12、Fedora 36+)。每个标签作业都会运行 + `scripts/check-linux-host-glibc.mjs`,拒绝需要更新 glibc 的二进制文件。 +- Linux arm64 的麦克风采集仅在 Raspberry Pi 板卡上可用: + `@picovoice/pvrecorder-node` 按 `/proc/cpuinfo` 的 CPU part 判定 Linux + arm64。语音转写(`transcribe-cpp` 随附 `linux-arm64-cpu-vulkan` 包)以及 + 其余所有功能在任何 arm64 Linux 设备上都可用。 - 回滚、分阶段部署和预发布渠道政策仍是开放的发布工作。现有未签名 macOS 安装可能需要先手动安装一次已签名 DMG,之后应用内更新才能成功。 diff --git a/docs/zh-CN/spec/08-meta/decisions-log.md b/docs/zh-CN/spec/08-meta/decisions-log.md index c39c017476..a128ee8eca 100644 --- a/docs/zh-CN/spec/08-meta/decisions-log.md +++ b/docs/zh-CN/spec/08-meta/decisions-log.md @@ -35,6 +35,7 @@ | D635 | 按工作区上限裁剪的 800×560 窗口最小尺寸 | **取代 D156 / D447 中的 1040×700 窗口最小尺寸(及 ADR 0029 / ADR 0238 的对应条款)和 `window/setWorkPanelChatWidth` 的 `1040..10000` 范围(ADR 0146):Electron 强制 800×560 最小尺寸,并由 `clampMinimumSizeToWorkArea` 按维度裁剪到当前显示器工作区。聊天宽度 IPC 与渲染层接受 `800..10000`。窄窗口下沿用现有 `workPanelLayout` 预算:限制停靠面板宽度以保证 MainChat 的 450px 下限,并优先收起侧边栏。见 US-UI-19 与 E2E-167。** | Windows 150% 缩放下工作区约为 1280×672 DIP,固定最小尺寸可能超过屏幕,导致窗口无法适配。 | | D636 | 本地权限确认没有自动截止时间 | **修订 D005 / ADR 0011:需要权限的 `tools.execute` 请求会在 host-core、渲染层和传输中保持待处理,直到用户选择允许一次、允许会话或拒绝,或请求被取消/进程关闭。移除 120 秒倒计时以及本地权限契约中的超时字段。工具自身执行预算以及独立的 RACP/Plan 审批时限保持不变。见 ADR 0310、issue #1214 与 E2E-017。** | 用户可能在其他工作期间错过可见的权限请求;保持取消和执行预算即可保留控制与资源安全,又不会把“未注意”变成一个决定。 | | D637 | 移除 Windows 无边框窗口的缩放边缘 | **关闭 Windows 主窗口的厚边框,同时保留 Electron 43.6 原生无边框窗口的边缘和角落缩放。默认应用 4 DIP 原生圆角;获得授权的插件主题可选择 0 至 24 DIP 的整数半径。保留 D635 的最小尺寸约定和现有工作面板缩放归属。见 ADR 0317 与 E2E-167。** | 厚边框绘制了主题无法移除的左、下、右边缘。原生命中检测和窗口形状在不新增渲染层缩放 IPC 的情况下保留缩放能力及透明外角。 | +| D638 | 发布原生 Linux arm64 工件 | **修订 D126 / D285 / D603 / ADR 0022:标签发布构建并发布原生 Linux arm64 的 AppImage、deb 和 rpm 包,它们在 GitHub 原生 `ubuntu-22.04-arm` 运行器上构建并携带 arm64 `pi-desktop-host-core`。静态 Linux 目标去掉固定的 `arch`,改用工作流的 `--x64` / `--arm64` 参数;`linux.artifactName` 变为 `PI-Desktop--linux-.AppImage`;每条 Linux 通道校验按架构命名的更新源(x64 为 `latest-linux.yml`,arm64 为 `latest-linux-arm64.yml`);ASAR 导出读取 `linux-unpacked` 或 `linux-arm64-unpacked` 并发布 `PI-Desktop--linux-.asar`。`pi-host-bundle` 构建两个 Linux 架构,`PUBLISHED_TARGETS` 增加 `linux-arm64`。更新器归属、签名和交付模式不变。见 ADR 0318、issue #1281 与 E2E-192a。** | arm64 Linux 设备无法安装或运行已发布的 x64 工件,而交叉构建或模拟的通道会随包发布架构不匹配的 Rust sidecar。 | | D450 | 签名的 macOS GitHub Release | **修订 D078 / ADR 0022:GitHub tag 发布使用身份 `Developer ID Application: XingYu Liu (DUV63RKYTW)` / 团队 `DUV63RKYTW`,通过 Actions 密钥(`CSC_LINK`、`CSC_KEY_PASSWORD`、`APPLE_ID`、`APPLE_APP_SPECIFIC_PASSWORD`、`APPLE_TEAM_ID`)对 macOS DMG/ZIP 做 Developer ID 签名、`notarytool` 公证、装订和 Gatekeeper 校验;缺少密钥则失败。无证书的本地未签名打包仍可用。`workflow_dispatch` 仅可把 `sign_macos: false` 用于未签名调试产物。打包的 macOS 走应用内 `electron-updater`(ZIP + 合并后的 `latest-mac.yml`);Linux deb/rpm 与 Windows 便携版 ZIP 仍为通知并打开发布页。禁止 afterPack/afterSign adhoc 签名(ADR 0278)。** | 正式 DMG 应无需 Gatekeeper 警告即可打开,已签名 macOS 安装可下载并重启到新 tag。见 ADR 0289、E2E-196c、E2E-067A。 | ## B. 辅助实现默认值 @@ -4510,7 +4511,7 @@ that amendment are retired by ADR 0268; the upstream work-panel lifecycle stays. ## 2026-09-19 —— 远端主机的 SSH 引导(D453) - 桌面用系统 `ssh` 客户端在用户已能通过 SSH 到达的机器上安装并配对 `pi-host`,因此 `~/.ssh/config`、agent 与跳板机照常生效,应用不持有任何 SSH 密钥;`BatchMode=yes` 让需要交互式密码或口令短语的主机立即以带类型的错误失败,而不是把模态框吊在一个不可见的提示后面。 -- 桌面按远端平台、以自己的版本解析 `pi-host` 包,持有发布随附的 SHA-256,并在任何下载之前拒绝未发布的目标(目前只有 `linux-x64`)。上传的脚本在远端 `$HOME` 下下载、校验并安装该包;SSH 通道上不传输任何可执行字节。 +- 桌面按远端平台、以自己的版本解析 `pi-host` 包,持有发布随附的 SHA-256,并在任何下载之前拒绝未发布的目标(目前为 `linux-x64` 与 `linux-arm64`)。上传的脚本在远端 `$HOME` 下下载、校验并安装该包;SSH 通道上不传输任何可执行字节。 - 脚本以 `umask 077` 运行并回显 `PI_HOST_READY` / `PI_HOST_PAIRING_TOKEN`,因此一次性配对令牌只存在于工作文件和 SSH 通道上,既不落在全局可读路径,也不出现在 URL 中(安全规格 §3.4)。`PI_HOST_READY.version` 必须与桌面版本一致;不一致为 `HOST_VERSION_MISMATCH`(D375),并在建立转发之前就已检查。 - 已配对主机的记录改存 SSH 描述符(`metadata.transport = "ssh"`)而非 URL,因为本地转发端口在每次启动间并不稳定;隧道管理器在每次启动时重新建立 `ssh -N -L`,并收编(adopt)引导自己打开的存活转发,使配对只建立一条隧道。描述符在每次读取注册表时都会重新校验,格式不合规时降级为「非 SSH 主机」,而不会用垃圾参数去 spawn `ssh`。 - `pi-desktop/remoteHost/bootstrap` 加入 `list` / `pair` / `remove`,`connection/pair` 交换被抽成单一的 `exchangePairingToken`,粘贴 URL 与 SSH 引导两条路径共用。 @@ -5166,3 +5167,28 @@ Markdown 源码,不是 `text/html` 负载;对禁用行内 HTML 的外部编 获得授权的插件主题可将半径设为 0 至 24 DIP 的整数;撤销主题后恢复 4 DIP。 D635 按工作区裁剪的最小尺寸仍然生效。移除厚边框后,主题无法控制原生阴影; 外部阴影需要单独决定窗口几何结构。见 ADR 0317 与 E2E-167。 + +## 2026-10-01 —— 发布原生 Linux arm64 工件(D638) + +- 标签发布在 x64 包之外同时发布 Linux arm64 的 AppImage、deb 和 rpm 包。每条通道都在 + GitHub 托管的原生 Ubuntu 22.04 运行器上运行(`ubuntu-22.04` 与 + `ubuntu-22.04-arm`),并打包它刚刚构建出的 `pi-desktop-host-core` 二进制,因此 sidecar + 架构始终与 Electron 应用匹配。两条通道都保持 glibc 2.35 下限。 +- Linux 目标不再固定 `arch`:electron-builder 优先采用配置里的架构列表而不是 CLI + 参数,把两个架构都固定下来会让每条通道围绕自己的 sidecar 去构建另一个架构。 + 工作流改为传入匹配的 `--x64` / `--arm64` 参数,并在打包前校验 `uname -m`。 +- AppImage 名称带上自己的架构 + (`PI-Desktop--linux-x64.AppImage`、 + `PI-Desktop--linux-arm64.AppImage`)。因此 x64 资产从之前仅带版本号的名称 + 改名;应用内更新不受影响,因为更新器读取的是已发布的更新源。 +- electron-builder 会按构建的架构为每条 Linux 通道的更新源命名(x64 为 + `latest-linux.yml`,arm64 为 `latest-linux-arm64.yml`),这正是 + `electron-updater` 在这些架构上请求的名称,因此两条通道不会互相覆盖更新源, + 每条通道在上传前校验该名称而不是改名。发布作业可以合并两份工件而不丢失任何更新源。 +- `scripts/export-linux-asar.mjs` 接收通道架构,并从 `linux-unpacked`(x64)或 + `linux-arm64-unpacked`(arm64)导出 `PI-Desktop--linux-.asar`。 +- `release.yml` 的 `pi-host-bundle` 作业构建两个 Linux 架构,`PUBLISHED_TARGETS` + 发布 `linux-x64` 与 `linux-arm64`,因此 arm64 桌面可以引导 arm64 远端主机(D375 / ADR 0292)。 +- 已知限制:除 Raspberry Pi 板卡之外的 arm64 Linux 设备仍无法采集麦克风,因为 + `@picovoice/pvrecorder-node` 对 Linux arm64 只认识 Raspberry Pi 的 CPU part。 + 语音转写和应用的其余部分没有架构相关的依赖。见 ADR 0318、issue #1281 与 E2E-192a。 diff --git a/docs/zh-CN/spec/README.md b/docs/zh-CN/spec/README.md index c1be35dca0..ba3bad7313 100644 --- a/docs/zh-CN/spec/README.md +++ b/docs/zh-CN/spec/README.md @@ -97,5 +97,5 @@ docs/spec/ v11 撤回 v10 添加的 A2A 方法域。 10、本地权限确认无自动截止时间;Bash 超时默认 60 秒 11.本地用户可安装的插件(稍后上市) -12. 标签版本 = macOS arm64、Intel x64、Windows x64 和 Linux x64 (D126/D285) +12. 标签版本 = macOS arm64、Intel x64、Windows x64 和 Linux x64 及 arm64 (D126/D285、D638/ADR 0318) 13. 通用 provider/model 覆盖范围(原生 + OpenAI 兼容 + 定制) diff --git a/scripts/README.md b/scripts/README.md index d35d3e8f07..dd837b60f3 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -88,8 +88,9 @@ validates the tag against `apps/desktop/package.json` before packaging, then runs the native `dist:mac`, `dist:win`, or `dist:linux` command. The Linux job uses Ubuntu 22.04 so host-core stays on glibc 2.35, then `scripts/check-linux-host-glibc.mjs` refuses a binary that needs a newer -glibc. The Linux runner also exports the exact app.asar from `linux-unpacked` -as a versioned release asset; the macOS matrix covers arm64 and Intel x64 and +glibc. Each Linux runner (native x64 and arm64) also exports the exact +app.asar from its own unpacked tree as a versioned release asset; the macOS +matrix covers arm64 and Intel x64 and the publish job assembles the GitHub Release. Tag builds Developer ID-sign, notarize, and staple macOS artifacts; `workflow_dispatch` may set `sign_macos: false` only for unsigned debug artifacts. See the [release diff --git a/scripts/export-linux-asar.mjs b/scripts/export-linux-asar.mjs index 1f4417dead..a8bc232158 100644 --- a/scripts/export-linux-asar.mjs +++ b/scripts/export-linux-asar.mjs @@ -5,17 +5,43 @@ import { dirname, join, resolve } from "node:path"; const scriptPath = fileURLToPath(import.meta.url); const repositoryRoot = resolve(dirname(scriptPath), ".."); +/** Architectures the Linux release matrix publishes. */ +export const LINUX_RELEASE_ARCHES = ["x64", "arm64"]; + +/** + * electron-builder writes the x64 unpacked tree as `linux-unpacked` and every + * other architecture as `linux--unpacked`. + */ +export function linuxUnpackedDirName(arch) { + return arch === "x64" ? "linux-unpacked" : `linux-${arch}-unpacked`; +} + +function assertReleaseArch(arch) { + if (!LINUX_RELEASE_ARCHES.includes(arch)) { + throw new Error( + `Unsupported Linux release architecture: ${arch} (expected ${LINUX_RELEASE_ARCHES.join(" or ")}).`, + ); + } + return arch; +} + /** * Export the Linux app archive produced by electron-builder as a named release * asset. The archive is intentionally copied instead of repacked so the asset * is byte-identical to the app.asar used by the AppImage and deb outputs. + * + * `arch` selects the release lane: it names the asset and the electron-builder + * unpacked directory the archive is read from, so the x64 and arm64 lanes can + * never publish each other's archive. */ export async function exportLinuxAsar({ rootDir = repositoryRoot, version, sourcePath, outputDir, + arch = process.arch, } = {}) { + const releaseArch = assertReleaseArch(arch); const packagePath = join(rootDir, "apps/desktop/package.json"); const packageJson = JSON.parse(await readFile(packagePath, "utf8")); const releaseVersion = version ?? packageJson.version; @@ -27,7 +53,7 @@ export async function exportLinuxAsar({ sourcePath ?? join( rootDir, - "apps/desktop/release/linux-unpacked/resources/app.asar", + `apps/desktop/release/${linuxUnpackedDirName(releaseArch)}/resources/app.asar`, ); const destinationDirectory = outputDir ?? join(rootDir, "apps/desktop/release"); @@ -39,16 +65,18 @@ export async function exportLinuxAsar({ await mkdir(destinationDirectory, { recursive: true }); const destination = join( destinationDirectory, - `PI-Desktop-${releaseVersion}-linux-x64.asar`, + `PI-Desktop-${releaseVersion}-linux-${releaseArch}.asar`, ); await copyFile(source, destination); - return { source, destination, version: releaseVersion }; + return { source, destination, version: releaseVersion, arch: releaseArch }; } const invokedPath = process.argv[1] ? resolve(process.argv[1]) : null; if (invokedPath === scriptPath) { try { - const result = await exportLinuxAsar(); + const archIndex = process.argv.indexOf("--arch"); + const arch = archIndex === -1 ? undefined : process.argv[archIndex + 1]; + const result = await exportLinuxAsar(arch ? { arch } : {}); console.log(`Exported ${result.destination}`); } catch (error) { console.error(error instanceof Error ? error.message : String(error));