Skip to content

Desktop app: Tauri shell, self-contained packaging, CI releases + self-update - #42

Merged
ApocDev merged 16 commits into
mainfrom
feat/desktop-packaging
Jun 30, 2026
Merged

Desktop app: Tauri shell, self-contained packaging, CI releases + self-update#42
ApocDev merged 16 commits into
mainfrom
feat/desktop-packaging

Conversation

@ApocDev

@ApocDev ApocDev commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Turns PyOps into a self-contained desktop app and automates its release pipeline. The web app is unchanged in spirit — Tauri wraps the existing Nitro server; the React UI stays Tauri-agnostic.

What's here

Self-contained app

  • The data pipeline runs in-process (no spawned scripts / dev tooling at runtime); schema is provisioned via bundled drizzle migrations; all on-disk state resolves through a per-OS data dir (server/paths.ts).
  • Icons are served from the data dir in production.

Desktop shell (app/src-tauri)

  • A Tauri window runs the Nitro server: dev via beforeDevCommand, bundled via a vendored node sidecar against bundled .output + drizzle + mod resources, with state in the per-OS app-data dir.
  • Polish: real icons, versioned title, remembered window geometry, external links open in the system browser, single instance (multi-instance tracked in Support multiple instances / multiple open projects #41), and the window reveals on first paint.

CI / releases

  • Multi-platform build matrix → .deb + .AppImage, .dmg (arm64 + x64), and an NSIS installer.
  • release-please drives versioning + changelog from conventional commits (bumps package.json / Cargo.toml / tauri.conf.json in lockstep). Merging its release PR cuts the release and runs the build.
  • Self-update: signed updater artifacts + aggregated latest.json, and an in-app 'Update available' prompt that shows the changelog and installs on accept.

Validation notes

  • The release pipeline can only be exercised once this is on main and a release runs — expect possible first-run tweaks (tauri-action + vp is the least-proven bit). The build matrix itself was proven green on a throwaway tag.
  • Self-update proves out on the first release → a follow-up release hop.

Follow-ups

ApocDev added 16 commits June 29, 2026 23:35
Run the data pipeline as in-process services and resolve all on-disk state
through a central data dir, so the app can run from a packaged build with no
dev tooling on the machine.

- paths.ts resolves DATA_DIR / RESOURCE_DIR (env-overridable, PYOPS_DATA_DIR
  etc.) and the derived paths; nothing joins off process.cwd() directly
- provision.ts applies the bundled drizzle/ migrations in-process on first
  connect, replacing the drizzle-kit push child process and the ad-hoc
  ensureCoreUpgrades ALTERs; the baseline migration is idempotent so existing
  dbs adopt it cleanly
- icon-atlas.ts builds the atlas in-process (was a spawned script); dump.ts
  uses top-level imports instead of inline await import()
- serve /icons/* from the data dir in production (routes/icons.$.ts), with
  immutable caching for fingerprinted sheet URLs
- detect a running Factorio cross-platform via pgrep/tasklist instead of flock
- vp run db:generate requires a migration name (scripts/db-generate.mjs)
- drop the obsolete dev scripts (icon-atlas builder, atlas preview, reload helper)
Add a Storage location card (Settings -> Game data) listing the resolved data
dir, project database, icon atlas, and config paths, each copy-able, so the
location is discoverable when sharing a database or filing a bug report.
Update AGENTS.md, architecture.md, and data-pipeline.md for the data dir, the
in-process schema provisioning via drizzle migrations, the in-process icon
atlas, and the trimmed scripts/ and db scripts.
Scaffold app/src-tauri. The dev flow boots the Nitro server bundle via
beforeDevCommand and loads it in a webview window pointed at the local server
(devUrl). On Linux it forces the X11 backend and disables webkit's DMABUF
renderer to avoid the Wayland "Error 71" protocol error. Vendoring a node
runtime + the server as bundled resources for a standalone build comes next.
The bundled build starts the Nitro server via a vendored `node` sidecar
(externalBin) against the bundled `.output`, drizzle migrations, and mod source
(bundle resources), with state in the per-OS app-data dir. The window opens
hidden and reveals on first paint, so there's no blank webview while the server
boots. Dev still starts the server via beforeDevCommand. vendor-node.sh fetches
the node binary, which is gitignored (per-platform, ~80-120MB).

Verified: the .deb builds and the bundled binary runs with no system node — the
sidecar boots the server and provisions a fresh db in the app-data dir. AppImage
is disabled for now (linuxdeploy fails to bundle GTK on this host).
Tag-triggered (v*) workflow that builds the Tauri bundle on a per-platform
runner and attaches the artifacts to a draft GitHub Release. Mirrors ci.yml's
voidzero-dev/setup-vp setup, adds the Rust toolchain + Linux webview deps, and
runs vendor-node.sh to fetch the sidecar (gitignored) before building. Scoped to
Linux (deb + appimage) for now — the matrix is structured so macOS/Windows are a
few added lines. The Ubuntu runner also builds a working AppImage, which the
Arch dev host's linuxdeploy can't.
Add macOS (arm64 + Intel) and Windows to the release matrix, so a tag builds
deb/AppImage, dmg, and an NSIS installer in one go. Run the vendor + build steps
under bash for a uniform invocation across runners, and make vendor-node.sh's
Windows zip extraction fall back across unzip/7z/tar. macOS and Windows bundles
are unsigned for now (Gatekeeper/SmartScreen will warn) — signing is a later add.
Show "PyOps v<version>" (from tauri.conf.json) in the title bar and regenerate
the app icons from the real logo. Open the first run at 1800x1100 so the desktop
nav shows even with fractional display scaling (the inline bar collapses to a
hamburger below ~1400 CSS px — which the Steam Deck still gets intentionally),
and persist + restore the window size/position across launches via
tauri-plugin-window-state.
Keep the window pinned to the local server: a navigation hook sends any
non-localhost http(s) URL to the default browser (tauri-plugin-opener) and
cancels the in-app navigation, and an init script routes target=_blank link
clicks through that same hook. So the GitHub button — and any future external
link — opens in the browser instead of hijacking the app window.
Register tauri-plugin-single-instance first so a second launch focuses the
existing window and exits before setup() spawns a second server — avoiding the
fixed-port collision. Multiple instances / multiple open projects is the
intended future direction; tracked in #41.

Refs #41
Conventional commits now drive the version bump and changelog: release-please
(release-please-config.json + manifest) bumps the version across app/package.json,
Cargo.toml, and tauri.conf.json in lockstep. The release workflow runs on main —
release-please maintains a "release PR", and merging it cuts the GitHub release +
tag, which gates the per-platform build that attaches the bundles to it. No PAT
needed (build runs as a dependent job in the same workflow). Activates once this
branch merges to main.
release-type node bumps app/package.json, but it had no version field — so it
would have been skipped and drifted out of sync with Cargo.toml / tauri.conf.json.
Seed it at 0.1.0 to match.
…pdate

Enable createUpdaterArtifacts and configure the updater plugin (public key +
GitHub latest.json endpoint). The release build now goes through tauri-action,
which signs the updater artifacts with the CI keypair, attaches them to the
release-please release, and aggregates the multi-platform latest.json the app
checks against. Self-update rides AppImage / NSIS / .app (not the deb). The
in-app check + prompt is the next step.
On a bundled build, check GitHub for a newer release in the background and, if
one is available, show a native "Update available" dialog with the version and
changelog (from latest.json's notes) plus Install & Restart / Later. Accepting
downloads, installs, and relaunches the app. Entirely Rust-side (tauri-plugin-
updater + tauri-plugin-dialog), so the web UI stays Tauri-agnostic; dev skips it.
Add docs/desktop.md (the Tauri shell, building a bundle, the release-please
pipeline, and self-update) and link it from the docs index. Note the desktop
shell + the release-please-managed version in AGENTS.md, add src-tauri/ and
drizzle/ to the architecture tree, and add a user-facing "Desktop app" section
with download links to the README.
@ApocDev
ApocDev merged commit 2653d7c into main Jun 30, 2026
1 check passed
@ApocDev
ApocDev deleted the feat/desktop-packaging branch June 30, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant