Skip to content

Linux packaging & deployment: zero-toolchain install (deb / rpm / AppImage) + release artifacts #168

Description

@jonocodes

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 to input. Needs per-distro CI.
  • AppImage — one file, zero deps; but it can't write /etc/udev/rules.d or 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 + input group), 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 (the dbus: action primitive + MPRIS); the macOS bundle deliberately omits it.
  • Python runtime: bundle a private interpreter (PyInstaller, as macOS does; or a uv standalone CPython) vs depend on the distro's Python.
  • Arch: x86_64 + aarch64 (evdev-binary has no aarch64 wheel — source build, see setup-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 a uv-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

  1. A build recipe (just build-linux-app / build-linux-tarball) that assembles the runtime + client + layouts into a relocatable tree.
  2. A first-run/install script that installs the udev rule, adds the input group, and writes the systemd user unit.
  3. 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

Activity

  1. added
    enhancementNew feature or request
    spikeDesign-doc spike work (input injection, focus watcher, etc.)
    on Sep 23, 2026
  2. jonocodes commented on Sep 25, 2026

    @jonocodes
    OwnerAuthor

    Decision: AppImage

    Recorded as ADR-0012.

    • Flatpak/Snap rejected — a sandbox cannot write the udev rule, see /dev/uinput (--device=all does not reliably expose it), reach the session bus unfiltered (xdg-dbus-proxy filters names), or install the compositor plugin. This is the Docker argument restated in docs/GUIDE.md:593.
    • AppImage is the primary artifact. It is unsandboxed, so daemon + D-Bus + serving the client all work as from a checkout. Focus watcher (~/.local/share/gnome-shell/extensions, KWin dirs) and XDG autostart are user-level and installable.
    • The root step is unavoidable on every channel (udev rule + input group). Factored into one idempotent install-system-integration helper run via pkexec/sudo, with a matching uninstall.
    • deb/rpm stay a later thin wrap of the same relocatable tree — not a competing design.

    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)

    • Runtime: PyInstaller onedir (reuse macOS spec) vs uv standalone CPython + venv.
    • Helper UX: pkexec GUI helper vs .sh with sudo vs print instructions.
    • Focus watcher: auto-install (detect GNOME/KDE) vs instruct.
    • Autostart: XDG .desktop vs install the systemd user unit.
    • Arch: x86_64 first vs x86_64 + aarch64.
  3. jonocodes commented on Sep 25, 2026

    @jonocodes
    OwnerAuthor

    Sub-decisions resolved (ADR-0012 updated):

    • Runtime: PyInstaller onedir (parity with macOS packaging & deployment: self-contained .app + DMG release #165).
    • Root step: .sh helper run with sudo, prints changes, ships uninstall.
    • Focus watcher + autostart: helper auto-installs both (GNOME/KDE detect + ~/.config/autostart/deckd.desktop).
    • Arch: x86_64 + aarch64 (aarch64 needs an evdev-binary source build in CI).
  4. added a commit that references this issue on Sep 30, 2026
  5. added
    human-verification-requiredCode is complete; a human must verify on real hardware / a live session before closing
    on Sep 30, 2026
  6. jonocodes commented on Sep 30, 2026

    @jonocodes
    OwnerAuthor

    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 this through 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-existing input group/extension preserved).

    Still open on this issue:

    • Autostart-at-login with the real helper on a clean machine (no pre-existing deckd service).
    • The release workflow's first run on a v* tag (x86_64 + aarch64).

    Helper root-step UX follow-up: #173 (pkexec front-end + uaccess-first).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthuman-verification-requiredCode is complete; a human must verify on real hardware / a live session before closingspikeDesign-doc spike work (input injection, focus watcher, etc.)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions