0029. Desktop installers and a download page
- Status: Accepted
- Date: 2026-11-11
- Amends: 0001 (the “later and optional” desktop part), 0009 (installing as a service)
- Amended by: 0030 (the app opens a window of its own instead of the browser)
Context — what it has to do
Section titled “Context — what it has to do”Until now, installing Exhale on a Mac or a Windows PC meant a terminal: unpack a tarball, copy a binary, write a launchd agent or start exhale.exe serve by hand. Linux had .deb/.rpm packages, but they left the service stopped. The goal is the shortest path there is: download the installer, open it, done, on macOS, Windows and Linux, from a page that is not GitHub’s release list.
What an installed Exhale physically has to do on a personal computer:
- Put the program in a stable place.
- Run
exhale servein the background, as the person (their data folder; keep-awake needs their session on macOS), from every login, restarted if it crashes, with no terminal window. - Give a door: an icon in Applications, the Start menu or the app launcher that opens Exhale.
- Upgrade by installing the new version over the old one, and uninstall without losing data.
- Open without the system calling it malware. That depends on signing, not on code.
Criteria
Section titled “Criteria”| Criterion | Weight |
|---|---|
| Steps for the person: download, open, done | High |
| No infrastructure of ours and no paid service to ship (ADR 0001) | High |
One binary, CGO_ENABLED=0, cross-compiled (ADR 0002, 0009) |
High |
| Each installer is tried on a real machine before release | Medium |
| Room for signing and notarization later, without redesign | Medium |
Options
Section titled “Options”| Per-OS native installers around the same binary | A desktop shell (Tauri/Electron) | Package managers only (Homebrew, winget, apt repo) | GoReleaser Pro (dmg, msi, app bundles) | |
|---|---|---|---|---|
| Steps for the person | Download, open | Download, open | Terminal first | Download, open |
| Stays one Go binary | ✅ | ❌ second runtime, a window to maintain | ✅ | ✅ |
| Cost | None | Large | A tap repo, tokens, manifests per channel | A paid licence |
| Background service | The binary registers itself | Same problem, plus the shell | brew services; winget has none |
Same as first column |
kardianos/service (ADR 0009) installs system services: root, LocalSystem on Windows. A desktop needs a per-user login item, which it does not cover.
Decision
Section titled “Decision”Native installers around the same binary, which knows how to install and open itself.
exhale open(and the desktop builds opened with no arguments) checksGET /healthz, starts the server in the background if it does not answer, waits for it and opens the browser./healthznow sendsX-Exhale-Version, so an upgraded app restarts an older server. Errors show a native dialog (macOSosascript, WindowsMessageBox, Linuxnotify-send), translated with the chat catalogs.- macOS:
Exhale.dmgwithExhale.app(universal: Apple silicon and Intel;LSUIElement, no Dock icon). Each time it is opened, it writes its launchd agent (~/Library/LaunchAgents/io.github.akamachinecode.exhale.plist,RunAtLoad, restarted on failure,AssociatedBundleIdentifiersso Login Items shows “Exhale” with its icon) and loads or reloads it. Opening a newer app restarts the server: that is the upgrade. Opened from the disk image or from a read-only App Translocation copy, it asks to be moved to Applications first. Built on a macOS runner:lipo,iconutil, an ad hoccodesignof the whole bundle (so Gatekeeper offers “Open Anyway” instead of calling it damaged),hdiutil. - Windows:
Exhale-Setup.exe, a one-click NSIS installer, cross-built withmakensis(Homebrew on a macOS runner, so a release does not wait on Ubuntu’s package mirrors): per-user (%LOCALAPPDATA%\Programs\Exhale, no administrator), Start menu entry, an entry in Settings → Apps with an uninstaller, then it opens Exhale.Exhale.exeis built with-H windowsgui(no console window), logs to%LOCALAPPDATA%\Exhale\exhale.log, and adds itself toHKCU\…\Runwhen opened. The installer stops a running Exhale before replacing it; the uninstaller removes the sign-in entry and keeps the data. - Linux: the
.deband.rpmnow start the service on install, add Exhale to the app launcher (exhale.desktoprunsexhale open) with its icon, and ship a polkit rule that lets theexhaleuser take the sleep lock keep-awake needs (ADR 0028). Still a system service: it runs without anyone logged in, which is what a reminder server wants. - Stable file names (
Exhale.dmg,Exhale-Setup.exe,exhale_linux_<arch>.deb|rpm), soreleases/latest/download/<name>always works. - CI tries them before publishing.
.github/workflows/installers.ymlbuilds both installers, mounts the dmg, copies the app to/Applicationsand opens it on a Mac runner, and installs, opens and uninstalls the setup on a Windows runner, checking the server answers in the right version. The release workflow calls it, and GoReleaser adds both files to the release and to the signedchecksums.txt. On pull requests the installers stay as downloads of the run. - A download page in
site/, published by GitHub Pages (pages.yml) atakamachinecode.github.io/exhale: picks the visitor’s system, English and Spanish, version from the GitHub API, and the first-open instructions for unsigned apps. No build step and nothing third-party at runtime besides that API call. - Still no tray icon: the web is the interface (ADR 0001).
Consequences — cost we accept
Section titled “Consequences — cost we accept”- Unsigned. The first time, macOS says it cannot verify the developer (System Settings → Privacy & Security → Open Anyway) and Windows SmartScreen asks for “More info → Run anyway”. The download page says so next to each button. An Apple Developer ID (99 $/year) turns the ad hoc signature into a real one plus notarization in the same macOS job; Windows code signing likewise.
- Uninstalling on a Mac is two gestures: turn Exhale off in Login Items and drag the app to the Trash. The agent file stays behind, harmless, until the app is installed again.
- On Windows only amd64 is built; Windows on ARM runs it emulated.
- A server of another version on Windows is not restarted by
Exhale.exe(it cannot stop it gracefully): the installer stops it. - Two installers built outside GoReleaser, in shell scripts under
deploy/desktop/, that we maintain.
Revisit when
Section titled “Revisit when”- We pay for signing: add the Developer ID, notarization and the Windows certificate to the existing jobs.
- People ask for a menu bar or tray icon (status, quit), or for automatic updates.
- Homebrew, winget or an apt/dnf repository is requested: they can point at these same files.