Skip to content

macOS packaging & deployment: self-contained .app + DMG release #165

Description

@jonocodes

Problem

deckd runs on macOS today only from a source checkout: just setup-macos (uv + Python + Node), just build-client, then just install-service, which sed-substitutes @PROJECT_DIR@ into packaging/launchd/com.deckd.daemon.plist and points the LaunchAgent at .venv/bin/deckd.

That requires a git checkout, Python, Node, and uv — and it gives macOS's privacy system (TCC) no stable code identity to grant. Accessibility / System Events / Screen Recording grants are attributed to the responsible process and keyed to a code identity, so a launchd agent exec'ing a venv script often cannot be granted Accessibility at all, and the core input features fail silently (see docs/PLATFORM-PARITY.md §"Verification status"). README lists "Packing and deployment" as planned; there is no tracking issue for the macOS story.

Goal

Any Mac user can install deckd with zero toolchain: download a DMG from a GitHub release, drag the app to Applications, grant permissions once, and use it. No Python, Node, Homebrew, or checkout required.

Decisions (from design discussion)

  • Target user: any Mac user, zero toolchain.
  • Channel: GitHub release DMG containing a self-contained .app. No Homebrew tap or PyPI publishing for now (a repo-local formula can come later as a contributor path).
  • Signing: ad-hoc signed only (codesign -s -, free). Not notarized (avoids the paid Apple Developer Program). Quarantine workaround documented.
  • App shape: menu-bar app (LSUIElement, no Dock icon). The daemon runs in-process on a background thread; AppKit owns the main thread.
  • Bundle contents: private Python runtime + deps ([macos] extra) + built client (client/dist) + layouts (layouts/, layouts.macos/).

Design

deckd.app/
  Contents/
    Info.plist          # name, version, bundle id com.deckd.daemon, LSUIElement
    MacOS/deckd         # launcher binary (PyInstaller entry: menubar.py)
    Resources/
      python + deps     # private interpreter + site-packages
      web/              # client/dist
      layouts/          # layouts + layouts.macos
      icon.icns
  • Menu items: Open surface (browser), Open layouts folder, Restart, Quit.
  • Data dirs: layouts seeded on first run to ~/Library/Application Support/deckd/layouts (bundle Resources are read-only); password at ~/.config/deckd/password (already the default); log to ~/Library/Logs/deckd.log.
  • Refactor seam: split daemon/deckd/__main__.py main() into build_server(args) + serve(server) so the menu-bar entry point can construct a Server and run the asyncio loop on a thread without installing signal handlers. Pure refactor; existing tests must stay green.
  • Toolchain: PyInstaller onedir (.spec with BUNDLE(...), custom info_plist, ad-hoc signing). Fallback if pyobjc fights the freezer: a hand-assembled bundle using a uv standalone CPython + venv.
  • Build recipes: just build-macos-app, just build-macos-dmg (hdiutil).

Constraints / risks

  • A .app can only be built on macOS (Apple toolchain). This dev machine is Linux, so the build runs on the user's Mac or on GitHub macos-14 runners; only the non-AppKit parts are testable locally.
  • TCC identity: grants key on bundle id com.deckd.daemon; because the bundle is ad-hoc signed, an upgrade may require re-granting. Document it.
  • Quarantine: a browser-downloaded DMG is quarantined → Gatekeeper blocks first open. Document xattr -dr com.apple.quarantine /Applications/deckd.app (or right-click → Open).
  • Not notarized: the "unidentified developer" dialog will appear; documented workaround only.
  • Arch strategy: decide arm64-only (build on an arm64 runner) vs universal2.
  • Rename churn: Rename project: deckd → deskestra #145 (deckd → deskestra) would change the bundle id, data dir, and artifact names — settle naming before publishing releases.

Plan

Phase 0 — doable on Linux, testable here

  1. Refactor __main__.py into build_server() / serve().
  2. packaging/macos/menubar.py — PyObjC status-item app.
  3. packaging/macos/deckd.spec — PyInstaller recipe.
  4. Info.plist bits + icon placeholder.
  5. just build-macos-app / just build-macos-dmg.
  6. Docs: install steps, quarantine one-liner, TCC grants.

Phase 1 — on a Mac
7. Run the build, fix PyInstaller/pyobjc issues, verify menu bar + daemon + that a TCC grant sticks.

Phase 2 — release pipeline
8. GitHub Actions macos-14 job: build client → PyInstaller → DMG → attach to the GitHub release.

Open questions

  • Arch strategy: arm64-only vs universal2.
  • Auto-start at login: SMAppService (macOS 13+) vs a shipped LaunchAgent vs "Open at Login".
  • First-run onboarding for the three TCC grants (Accessibility, System Events, Screen Recording).

Related

Activity

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 request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions