feat(linux): packaged AppImage + release workflow (#168) - #171
Conversation
Zero-toolchain Linux install: a self-contained AppImage carrying a private Python runtime, the built client, and the layouts, plus a `sudo` helper that installs the pieces an AppImage cannot own. - packaging/linux: PyInstaller onedir spec + launcher, AppDir glue (AppRun, deckd.desktop), and install-system-integration.sh (udev rule + input group; GNOME/KWin focus watcher; XDG autostart; --uninstall). - daemon/deckd/app_bundle.py: platform-agnostic packaging helpers extracted from macos_app so the two bundles cannot drift; macos_app re-exports them. - daemon/deckd/linux_app.py: XDG data dir/log, layouts.linux overlay, argv. - .github/workflows/release-linux.yml: x86_64 + aarch64 matrix, --help smoke test, attaches the AppImages + the helper script to the tag. - Justfile: build-linux-appimage, install-system-integration. - docs: ADR-0012 (AppImage primary; Flatpak/Snap rejected) and a GUIDE section. Channel rationale in docs/adr/0012-linux-distribution-appimage.md.
A binfmt AppImage handler (NixOS's `programs.appimage`, verified on a GNOME/Wayland NixOS box) runs the payload directly, so the embedded runtime's `--appimage-extract` never fires and the install helper died before it found its assets. The frozen launcher now handles `--extract-integration DIR` — copy `usr/share/deckd/integration` out of its own AppDir — and the helper falls back to it when the runtime extraction leaves no tree. The release workflow smoke-tests the flag so a release can't ship without the assets. Also record what an install changed in `/var/lib/deckd/system-integration.<user>.state`, so `--uninstall` only removes what this helper created. Previously it removed the user's `input` membership and focus extension unconditionally, which would break a NixOS/home-manager or source install on the same machine. Verified on NixOS/GNOME: built the AppImage, booted it (health, client, layout seeding, log file), injected keys/scroll through its uinput device and read them back at the evdev level, and ran the helper's install→uninstall lifecycle sandboxed as namespace-root (pre-existing group/extension preserved). Live focus-watcher check still pending a desktop session.
Linux verification (NixOS 26.05 / GNOME 50 Wayland, x86_64)Worked through the "Not verified (needs a Linux box)" list. Built: Booted the frozen payload (
Install helper (sandboxed as namespace-root; privileged side effects redirected to temp paths):
Live desktop session (v6 extension loaded at shell startup):
Two fixes found by this run (6f12aab):
Not tested: autostart-at-login with the real helper. It needs a
|
Merge main for the branding assets, then use the committed PNGs (`just icons`) instead of rendering the SVG at build time: - deckd.png at the AppDir root, so appimagetool's .DirIcon is a PNG rather than an SVG (friendlier to file managers/thumbnailers); - 192/512 copies under usr/share/icons/hicolor so desktop integration resolves the .desktop's Icon=deckd; - one in usr/share/deckd/integration, which the helper copies into the user's hicolor theme and points the autostart entry's Icon=deckd at. The helper records the icon in its install state, so --uninstall removes it. Drops the now-unused librsvg2-bin CI dependency; the release smoke test checks the icon rides along in the integration tree.
|
Icon: merged main (branding, 585eec3) and used the committed PNGs in the AppImage build instead of rendering the SVG at build time. The AppDir now carries |
Phase 0 of #168.
What
Zero-toolchain Linux install. A self-contained AppImage carries a private Python runtime, the built client, and the layouts; a small
sudohelper owns the parts an AppImage cannot (udev rule +inputgroup) plus the user-level pieces (desktop focus watcher, icon, XDG autostart).Changes
packaging/linux/— PyInstalleronedirspec +launcher.py, AppDir glue (AppRun,deckd.desktop, committed PNG icons), andinstall-system-integration.sh(idempotent, state-tracked, with--uninstall).daemon/deckd/app_bundle.py— platform-agnostic packaging helpers extracted frommacos_appso the macOS and Linux bundles can't drift;macos_appre-exports them unchanged.daemon/deckd/linux_app.py— XDG data dir/log,layouts.linuxoverlay, first-run seeding,build_argv, and--extract-integration(copy the AppDir's integration assets out)..github/workflows/release-linux.yml— x86_64 + aarch64 matrix,--help+ extraction smoke tests, attaches the AppImages and the helper script to the tag.Justfile—build-linux-appimage,install-system-integration.Decision
AppImage, not Flatpak/Snap: a sandbox cannot write the udev rule, see
/dev/uinput, reach the session bus unfiltered, or install the compositor plugin. The root step is unavoidable on every channel, so it's an explicitsudohelper. Details and rejected options in ADR-0012.Verification (NixOS 26.05 / GNOME 50 Wayland, x86_64)
just build-linux-appimage→dist/deckd-0.0.1-x86_64.AppImage(22.9 MB). Boots (--help), serves/healthand the bundled client, seeds layouts into~/.local/share/deckd/layouts, writesdeckd.log.key/jogproduce real evdev events; typeddeckd injected thisthrough the packaged daemon into a focused Text Editor on the live session, sentctrl+s, and the saved file contained exactly that text.started_ok: true, focus changes select layouts (org.gnome.Console→layout=org.gnome.Console),RaiseApp/RaiseWindowwork,scripts/smoke_focus_live.pypasses.--uninstallremoves only what it created (a pre-existinginputmembership/extension is preserved).pytest776 passed,pyright daemonclean,bash -n+ flag checks, workflow YAML parses.Fixes found during verification
6f12aab— binfmt AppImage wrappers (NixOSprograms.appimage) bypass--appimage-extract; the launcher now offers--extract-integrationand the helper falls back to it.--uninstallis state-gated (/var/lib/deckd/system-integration.<user>.state) so it can't clobber a NixOS/home-manager install.40a8cd0— ship the app icon: committed PNGs at the AppDir root (PNG.DirIcon), hicolor 192/512 copies, and an integration-tree copy the helper installs into the user's theme withIcon=deckd.Not verified
sudorun plus a login on a machine without an existing deckd service; the sandbox verified the entry it writes.v*tag exercises it.Follow-up
inputgroup/re-login).