Repository navigation
Linux packaging & deployment: zero-toolchain install (deb / rpm / AppImage) + release artifacts #168
Description
Problem
deckd runs on Linux today only from a source checkout: just setup-linux (uv + Python + Node), just build-client, then just install-service, which sed-substitutes @PROJECT_DIR@ into packaging/systemd/deckd.service and points the user unit at .venv/bin/deckd. On top of that the user must hand-install the udev rule + input group membership (packaging/udev/70-deckd-uinput.rules) and the desktop-specific focus watcher (GNOME Shell extension / KWin script). That needs git, Python, Node, uv, and a checkout.
Nix users are covered by the flake (#17: packages.deckd + NixOS/home-manager modules own the service, udev rule, and group), but that's a different audience and a different install path.
#165 gives macOS a zero-toolchain path (self-contained .app in a DMG, built and published by a release workflow). Linux has no equivalent.
Goal
Any Linux user can install deckd with zero toolchain: download an artifact from a GitHub release, install it, grant /dev/uinput access once, install the focus watcher for their desktop, and run. No Python, Node, or checkout required.
Follows
#165 (macOS .app + DMG + release-macos.yml). Mirror the shape: a build recipe that produces a distributable artifact, and a release workflow that attaches it to the tag. Reuse the DECKD_VERSION version seam (#165) so tag and artifact naming stay consistent across platforms.
Candidate channels (to decide)
- Tarball + install script — simplest, distro-agnostic, but nothing owns the udev rule / group / service. Good phase-0 target.
.deb/.rpm— the package can own the udev rule, the systemd user unit, and a desktop file, and post-install can add the user toinput. Needs per-distro CI.- AppImage — one file, zero deps; but it can't write
/etc/udev/rules.dor add groups, and ships no systemd unit. Would need a first-run helper for the privileged steps. - Flatpak / Snap — sandboxing conflicts with the daemon's model: it injects global input via
/dev/uinput, reads other apps' windows through a compositor plugin, and talks to the session D-Bus bus. The GUIDE already rules out Docker for exactly these reasons, so this is likely a poor fit — but worth a sentence in the exploration. - Distro repos / AUR / Copr — community path, later; out of scope for a first artifact.
Linux-specific complications
- uinput needs root (udev rule +
inputgroup), so the artifact can't be fully unprivileged — unlike the macOS DMG. - The focus watcher is desktop-specific (GNOME Shell extension, KWin script) and can't ship inside a desktop-neutral daemon package.
- The
[dbus]extra is required on Linux (thedbus:action primitive + MPRIS); the macOS bundle deliberately omits it. - Python runtime: bundle a private interpreter (PyInstaller, as macOS does; or a
uvstandalone CPython) vs depend on the distro's Python. - Arch: x86_64 + aarch64 (
evdev-binaryhas no aarch64 wheel — source build, seesetup-linux). - Client + layouts must be bundled (
client/dist,layouts/,layouts.*). - Auto-start: systemd user unit vs an XDG autostart
.desktop.
Open questions
- Which channel first? (Tarball → deb/rpm → AppImage? Or straight to
.deb?) - PyInstaller
onedir(reuse the macOS spec shape) vs auv-built standalone CPython + venv. - How much can the package do for the user (udev rule, group, service, focus watcher) without sudo surprises?
- One artifact per desktop, or one artifact plus an "install the focus watcher" step that detects GNOME/KDE/X11?
- Do we ship a tray equivalent, or keep the systemd user unit? (macOS needed a menu-bar app only because AppKit owns the main thread.)
Plan (draft)
Phase 0 — doable anywhere, testable on a checkout
- A build recipe (
just build-linux-app/build-linux-tarball) that assembles the runtime + client + layouts into a relocatable tree. - A first-run/install script that installs the udev rule, adds the
inputgroup, and writes the systemd user unit. - Docs: install steps and the uinput caveat.
Phase 1 — on a Linux desktop
4. Run the artifact, fix packaging issues, verify the service + focus watcher + input injection.
Phase 2 — release pipeline
5. A GitHub Actions job that builds the artifact and attaches it to the release (mirror release-macos.yml).
Related
- macOS packaging & deployment: self-contained .app + DMG release #165 — macOS packaging & deployment: self-contained
.app+ DMG release (the template) - T11: NixOS production module #17 — T11: NixOS production module
- Rename project: deckd → deskestra #145 — Rename project: deckd → deskestra
- Manual verification: macOS input injection (key, click, scroll) on a real session #141 / Manual verification: X11 focus backend on a real X11 session #33 — manual verification on real hardware
- README "Packing and deployment" (planned)
Activity
jonocodes commentedon Sep 25, 2026
Decision: AppImage
Recorded as ADR-0012.
/dev/uinput(--device=alldoes not reliably expose it), reach the session bus unfiltered (xdg-dbus-proxyfilters names), or install the compositor plugin. This is the Docker argument restated indocs/GUIDE.md:593.~/.local/share/gnome-shell/extensions, KWin dirs) and XDG autostart are user-level and installable.inputgroup). Factored into one idempotentinstall-system-integrationhelper run viapkexec/sudo, with a matching uninstall.Phase 0 substrate is unchanged: relocatable tree + install script + docs. The channel is the skin on top.
Open sub-decisions (need input before Phase 0 lands)
onedir(reuse macOS spec) vsuvstandalone CPython + venv.pkexecGUI helper vs.shwithsudovs print instructions..desktopvs install the systemd user unit.jonocodes commentedon Sep 25, 2026
Sub-decisions resolved (ADR-0012 updated):
onedir(parity with macOS packaging & deployment: self-contained .app + DMG release #165)..shhelper run withsudo, prints changes, ships uninstall.~/.config/autostart/deckd.desktop).evdev-binarysource build in CI).Merge pull request #171 from jonocodes/feat/linux-appimage-168
jonocodes commentedon Sep 30, 2026
Phase 0 merged in #171.
Verified on NixOS 26.05 / GNOME 50 Wayland (x86_64): AppImage build + boot (health, bundled client, layout seeding, log file), input injection end-to-end (typed
deckd injected thisthrough the packaged daemon into a live Text Editor and saved it), live focus with the v6 extension (layout switching,RaiseApp/RaiseWindow,smoke_focus_live), and the helper's install→uninstall lifecycle sandboxed (pre-existinginputgroup/extension preserved).Still open on this issue:
v*tag (x86_64 + aarch64).Helper root-step UX follow-up: #173 (pkexec front-end + uaccess-first).