Repository navigation
build(desktop): package Linux AppImage and .deb for x64 - #77
Merged
Merged
Conversation
Linux gets AppImage and .deb targets, x64, with no sherpa-onnx: voice is off there, and its Linux builds link GPL text-to-speech code. node-pty has no Linux prebuilds, so a Linux-only step compiles it against Electron's headers and npmRebuild stays off for macOS. verify-package reads both artifacts and loads node-pty in the packaged Electron; a new CI job on the package lane installs the .deb on Ubuntu 24.04 and Debian 12 and uploads the artifacts.
ANT1OFF
force-pushed
the
feat/linux-packaging
branch
from
November 2, 2026 04:43
674d3ec to
1433a4b
Compare
ANT1OFF
added a commit
that referenced
this pull request
Nov 2, 2026
The app-code side of Gram's first Linux release (AppImage and .deb, x64, voice off). #77 did the packaging and #79 the voice gating. This PR makes the app behave correctly on a Linux desktop and leaves macOS's paths as they were. **Security-sensitive:** the key vault's Linux backend (`packages/shell/src/modules/key-vault/vault.ts`, the `backend` passed in from `apps/desktop/src/main/index.ts`, `packages/contracts/src/gateway-keys.ts`). It needs an independent review of the current head before merging. ## Audit: macOS-only behaviour and what Linux does | Area | macOS | Linux before | Linux now | | --- | --- | --- | --- | | Title bar and window buttons | `hiddenInset` traffic lights, renderer keeps an 80 px inset | Desktop frame **plus** a native File/Edit/View row under it | Desktop frame and its own buttons (follows the theme and button layout); menu bar hidden, Alt shows it, accelerators work. Inset stays 0 | | Vibrancy | `under-window` vibrancy behind translucent looks | Window made transparent and `data-vibrancy=native` set, with nothing behind | Off: the look paints its own backdrop on an opaque window | | App menu | `Gram` menu with About, Services, Hide | Windows layout (Settings and Quit in File) | Unchanged, now behind Alt | | App and window icon | Dock icon swap | No window icon (Electron's) | Window icon set to the chosen icon, the default included | | Tray | Template glyph in the menu bar | `app.getFileIcon(execPath)`, which on Linux is a theme's generic icon or nothing | The bundled Gift rune PNG at 64 px; registers as a StatusNotifierItem (KDE, GNOME with AppIndicator) | | Dock badge | Dock badge count | `setBadgeCount` only (shows only where a launcher reads Unity's API) | Same, plus the window's urgency hint while unfocused, which taskbars show | | Open at login | `SMAppService` login item | Setting hidden | XDG autostart entry `~/.config/autostart/gram.desktop` (`$APPIMAGE` or the binary, `--launched-at-login`), written and removed by the toggle; one switched off by the desktop reads off; packaged builds only | | Notifications settings link | System Settings deep link | None | None (no standard page); notifications themselves use libnotify through Electron | | Folder access recovery | Privacy & Security pane | n/a | n/a (no TCC) | | Gateway keys | Keychain via `safeStorage` | Keys stored even under `basic_text`, i.e. unencrypted | Stored only with `gnome_libsecret`/`kwallet`/`kwallet5`/`kwallet6` **and** encryption available; otherwise refused with a message, never written. Delete still works | | Credential paths (preview deny-list) | `Library/Keychains` | Not covered | Adds `.local/share/keyrings`, `.local/share/kwalletd`, `.pki/nssdb`; Claude Code's `.credentials.json` and Codex's `auth.json` already matched | | User data and logs | `~/Library/Application Support/gram-state` | `$XDG_DATA_HOME` or `~/.local/share/gram-state`; Electron's own in `~/.config/Gram` | Unchanged (already right) | | PATH for `claude`, `codex`, `gh`, `git` | Login shell `$SHELL -ilc` | Same probe; fallback shell `/bin/zsh` | Fallback `/bin/bash` when `$SHELL` is unset; fish already joins `"$PATH"` with colons (checked) | | File watching | `fs.watch` (FSEvents) | @parcel/watcher on inotify; a root at the limit silently got no live updates, and the keybindings `fs.watch` threw on `ENOSPC` | One warning line naming `fs.inotify.max_user_watches`; keybindings falls back to polling | | Reveal / open | `open -R`, `open` | `xdg-open` (folder), `xdg-open` (URL) | Unchanged; the desktop's reveal uses `shell.showItemInFolder` (FileManager1) | | `claude mcp login` in a terminal | macOS `script` | Not offered; a running Claude thread signs in | Unchanged (util-linux `script` takes other arguments; out of scope) | | Fonts list | `system_profiler` | `fc-list` | Unchanged | | Voice and dictation | sherpa-onnx | Would load sherpa-onnx | Off (#79): no speech IPC, no bridge, smoke skips the probe, the switch says "Not available on Linux yet." | | Key labels | ⌘ ⌥ ⇧ | Settings, palette and menus already used `formatKeys`; hard-coded ⌘ text in hints, tooltips, the Dream discard warning and What's New showed ⌘ | `keyLabel()` in `keyboard/registry.ts` turns a macOS spelling into Ctrl/Alt/Shift off macOS, and is used for every fixed hint; rebindable ones keep `useShortcutHint` | | Updates | Squirrel.Mac for signed builds | `gh`-based check, `canSelfInstall` false | Unchanged: no Squirrel on Linux, it can only say an update exists. Left for the release phase | ## Shortcuts - Accelerators are already `CmdOrCtrl` (`menu.ts`), and the terminal already keeps plain Ctrl chords for the shell off macOS. - **Changed on Linux only:** pane focus moves from Ctrl+Alt+arrows to **Alt+Shift+arrows**, because GNOME keeps Ctrl+Alt+arrows for workspace switching and the window never sees them. macOS and Windows keep their keys. A focused terminal passes the new chord to the app. - Checked and left alone: Ctrl+Alt+B/R/S/P/N/W/J/1–4 and Ctrl+Alt+Enter don't clash with GNOME, KDE or common terminal defaults. Ctrl+Shift+C/V stay the terminal's copy and paste. - What's New bodies are static text with `<kbd>` keys, so the renderer applies `keyLabel` to them: `⌥⌘N` reads `Ctrl+Alt+N` on Linux and Windows. ## Screenshots These were taken on a real KDE Plasma 6 (Wayland) machine (CachyOS), with a virtual `kwin_wayland` session on a private D-Bus. Gram ran unpackaged (`bun run dev:desktop`, fake agent, temporary HOME and data folder) with no keyring daemon. Before is `main` at the time, after is this branch, and the same driver ran both. KWin frame and Settings → Gateways, adding a gateway with a key and no keyring. Before: a second menu row, the generic Wayland icon, and the key refused with a generic error. After: one frame, the Gift rune icon, no key field, and the reason.   Appearance's icon hint, before and after:   Settings → Keyboard on Linux (after):  ## Verification On the Linux machine above (CachyOS, KDE Plasma 6, Wayland): - The app boots and the renderer reports `platform: linux`, no `speech` bridge, no vibrancy, a 0 px inset and a 1440×900 viewport (871 high before, with the menu row). - The tray registers as a StatusNotifierItem on KDE's watcher, before and after. - With no keyring daemon, a key typed into Add gateway is never stored: no `gateway-keys.json`, and the synthetic key appears in no file under the run's HOME or data folder, before or after. - Settings shows no Voice input page, and the Diagnostics switch says "Not available on Linux yet." - The settings pages visited show no ⌘ ⌥ ⌃ ⇧. - Diagnostics found `claude`, `git`, `gh` and `codex` through the login-shell PATH. - A direct Electron probe on the same machine, with a private bus and no keyring daemon, for `XDG_CURRENT_DESKTOP` KDE, GNOME and XFCE: `getSelectedStorageBackend()` reports `kwallet` or `gnome_libsecret` while `isEncryptionAvailable()` is `false`. Electron 45 no longer falls back to `basic_text` there, which is why the vault now treats unavailable encryption as no keyring. On macOS: - `bunx vitest run` on the changed areas: login item, key vault, gateway handlers and Settings, credential paths, keyboard, What's New, terminal keys, git watch, keybindings and settings search all pass. - `bun run lint`, `bun run typecheck`, `bun run fmt:check`, `bun run check:docs`, `bun run check:scaffold` all pass. ## Not tested, and left for later - **A real keyring storing a key on Linux:** the positive path is covered by unit tests only. I didn't touch the machine's own KWallet, its session was locked, and a fresh kwalletd6 in the private bus needs a wallet-creation prompt. - **A packaged build:** AppImage/.deb, the autostart entry at a real login, `--launched-at-login` starting hidden, and the AppImage's `Icon=gram` (an AppImage installs no theme icon, so the autostart entry may show none). - **X11, GNOME, and the tray menu:** not run on X11 or GNOME, and the tray menu was never opened or clicked; only its registration was checked. - **Notifications on Linux:** not exercised. - **The inotify limit:** covered by unit tests on the error shapes only; not reproduced live. - **Updater and release phase:** Linux runs the `gh`-based check with `canSelfInstall` false, so it can say an update exists but can't install one. Whether a release has Linux assets isn't checked. - **Codex runtime management:** `codexTarget()` has no Linux triple, so Gram's managed Codex install and update don't apply on Linux. An installed `codex` on PATH works.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds Gram as a Linux x64 AppImage and .deb in CI. Voice is off on Linux, so no sherpa-onnx ships. No publishing, no updater, no write token.
What changed
linuxtargets AppImage and deb (x64),executableName: gram, categoryDevelopment, hicolor icons inbuild/icons, maintainer,.debDepends/Recommends. Artifacts:gram_<version>_x64.{AppImage,deb}.desktopName: gram.desktopwithsyncDesktopName, so Electron'sCHROME_DESKTOP-derived WM_CLASS / Wayland app_id andStartupWMClassare allgram.--no-sandbox. Its AppRun still adds the flag where user namespaces are refused.rebuild:node-ptycompilesbuild/Release/pty.nodeagainst Electron's headers on Linux only.npmRebuild: falsestays, so the macOS path is unchanged.@parcel/watcher: Linux git watching needs it, so it ships inresources/node_modulesthrough Linux-onlyextraResources.package.json,bun.lockand the macOS and Windows packages are untouched.files; afterPack and verify-package fail a package that holds any; the notices generator omits it on Linux.native-probe.cjs no-speech.linux packageonubuntu-24.04, on thepackagelane. Installs the .deb on Ubuntu 24.04 and Debian 12, smokes the installed app and the AppImage under Xvfb, uploads artifacts for 7 days. Added toqualify-ci,.gram/merge-ci.jsonand the classifier test's job table.Fuses
Same as macOS. ASAR integrity's fuse is on, but Electron 45 only implements it on macOS and Windows (
shell/common/asar/archive.cc), so it is not enforced on Linux.onlyLoadAppFromAsarand the Node option fuses do apply.Verification
CI on 1433a4b,
linux packageonubuntu-24.04x64 (real, not synthetic):gram_1.3.1_amd64.deb103.6 MiB,gram_1.3.1_x86_64.AppImage128.7 MiB, uploaded for 7 days.verify:package: every check passed, on the unpacked app and on the contents of both artifacts..debinstalled on Ubuntu 24.04 and Debian 12 with no missing libraries. On Debian 12, the native probe ran a pty and a real file watch in the packaged Electron 45.0.0 x64 (NATIVE OK).@parcel/watcher2.14 (Debian 12 has 2.36).--smokewith fake agents and temporary stores under Xvfb:SMOKE OKfor the installed app and the AppImage.macos packagepassed on the same head.Locally: scaffold, oxlint, check:docs, and Vitest for desktop build, notices, release tools and the CI policy. No local full
bun run check(shared Mac).Fake-agent smoke only: no live provider, real desktop session, or keyring was tested here.
Known limits
@parcel/watcherwithrequire; the server uses a lazyimport()from inside the asar, which no CI step exercises. Confirm live git updates in a packaged app on a real Linux desktop.Icon=grammay show a generic icon.tools/release-rc/**is lane[], so a PR changing onlynative-probe.cjsskipslinux package; main runs every lane.latest-linux.yml, AppImage self-update, checksums, attestations, signing, publishing.