Deployment¶
Packaging¶
A Rust release build is a single self-contained ELF binary whose only dynamic dependencies are libc — there is no runtime to install. Packaging is therefore just placing one file plus its service plumbing, which is what cargo-deb formalizes. All of it is configured in Cargo.toml under [package.metadata.deb]; there is no separate packaging tree to keep in step.
| Path | Contents |
|---|---|
/usr/bin/suede |
The binary, with the web UI embedded |
/usr/lib/systemd/user/suede.service |
The systemd user unit |
/usr/share/suede/provision.sh |
Root provisioning script |
/usr/share/doc/suede/examples/ |
Bootstrap config and a four-output example |
Declared dependencies are sway, pipewire, and pipewire-pulse, with chromium | firefox recommended.
cargo deb # host architecture
cross build --release --target aarch64-unknown-linux-gnu
cargo deb --no-build --target aarch64-unknown-linux-gnu
Cross-compilation stays trivial precisely because there are no native library dependencies — the reason PipeWire is driven through its CLI tools rather than its C library.
Install and upgrade¶
# First install
sudo apt install ./suede_1.2.3-1_arm64.deb
sudo /usr/share/suede/provision.sh
sudo reboot
# Upgrade
sudo apt install ./suede_1.2.4-1_arm64.deb
postinst reloads unit definitions and restarts a running instance; it never enables anything or edits a user's session. Desired state lives in $XDG_STATE_HOME and is untouched by package operations, so configuration survives upgrades by construction. Re-running provision.sh is safe but only needed when the provisioning itself changed.
The package declares no relationship to a browser. Suede resolves whichever
of chromium, chromium-browser, google-chrome-stable, google-chrome,
firefox or firefox-esr it finds when it launches an app, and both
provisioning and the browsers health check say plainly when there is none.
Declaring them would claim a coupling that does not exist, and the names are
wrong somewhere whichever list you pick: Debian has a real chromium package,
Ubuntu has a transitional one that installs a snap, and google-chrome-stable
is in no distribution's archive at all. As a Recommends it did real harm —
apt installs those by default, so a plain install pulled snapd and a snap
Chromium onto an appliance that already had a browser.
Testing an install honestly¶
An installer tested on a machine that has already been installed on proves
very little: the failures worth finding only happen the first time, and they
hide behind whatever the last run left behind. reset-machine.sh returns a
machine to the state it was in before it met Suede.
It ships in the package, next to provision.sh, so an appliance already has
it and there is nothing to copy over:
sudo /usr/share/suede/reset-machine.sh --user hamish --dry-run # change nothing
sudo /usr/share/suede/reset-machine.sh --user hamish
sudo reboot # see below
From a checkout — on a machine where the package was never installed, or to
run a version newer than the installed one — it is packaging/reset-machine.sh
relative to the repository root:
Running the installed copy removes the package underneath itself, which is fine: the shell holds the file open and reads it to the end regardless of the directory entry disappearing. That is verified rather than assumed.
It removes the package (or a binary built and copied into place), the
per-user configuration and state, and the changes provisioning made: the
tty1 auto-login drop-in, its block in ~/.bash_profile, the daemon's block
in the sway config, and sway-session.target. It stops the daemon, and also
the slicer, blend overlays and kiosk browsers, which outlive it.
It deliberately leaves alone anything it cannot safely claim as its own:
sway, PipeWire and browsers are ordinary packages that were probably wanted
anyway; a compositor unit you wrote yourself is reported rather than deleted;
and masked display managers stay masked unless --restore-desktop is passed,
because re-enabling one on a machine already running a compositor is its own
kind of mess. --keep-state preserves the desired-state document and browser
profiles.
Reboot after resetting. Group membership and the compositor are inherited from login, not re-read, so a session that has already seen Suede carries some of it into the next test regardless of what is on disk.
Releasing¶
Cargo.toml's version is the single source of truth. On a push to main, CI builds both architectures, and if the version is new, tags v{version} and creates a GitHub release with both .debs attached.
To cut a release: bump the version in Cargo.toml, merge to main. Nothing else.
CI¶
One workflow, ci.yml, with dorny/paths-filter splitting app changes from docs changes and a docs_only dispatch input for redeploying the site without rebuilding.
| Job | Runs when | Does |
|---|---|---|
test |
app changes, all PRs | fmt, clippy, tests, OpenAPI generation, smoke test |
build |
push to main | Release binaries and .debs for amd64 and arm64 |
release |
push to main | Tags and publishes if the version is new |
build-docs |
docs changes on main | Generates API assets, builds the site with --strict |
deploy-docs |
after build-docs | Publishes to GitHub Pages |
The test job asserts the binary is self-contained by checking ldd output — anything beyond libc means a native dependency crept in and cross-compilation is about to get much harder.
The documentation site¶
Built with MkDocs and the Material theme. The API reference is generated from the code being deployed:
suede openapi needs neither a compositor nor a network, which is what makes this possible in CI. The Scalar bundle is vendored into docs/generated/ at build time, so the published page makes no CDN requests when viewed. Both are git-ignored; they are derived artifacts.
A snapshot test (tests/openapi_snapshot.rs) guards the document, so an endpoint or DTO change shows up as a reviewable diff rather than silently altering the published reference.
On-device verification¶
Some things only a real appliance can prove. Run this checklist once on real hardware before trusting a release:
- [ ] Four displays enumerate with correct EDID make/model and mode lists.
- [ ] Setting a mode, position, and scale on each takes effect.
- [ ] Four Chromium kiosks launch, one per output, each fullscreen on the right display.
- [ ] Audio from each browser reaches its configured sink; a null-routed app is silent.
- [ ] Unplugging a display moves status to
degraded; replugging returns it tosyncedand relaunches its app. - [ ]
kill -9on a browser relaunches it within the backoff window. - [ ] A page that stops posting heartbeats is relaunched.
- [ ] A cold reboot restores everything with no operator action.
- [ ]
systemctl --user stop suedeleaves no orphaned browser processes. - [ ] The cursor is invisible and parked off every display.