Shipping a Tauri app to Linux, macOS and Windows as a solo developer

#tauri#release#rust#github-actions

Bumeter is built by one person and ships to Linux, macOS and Windows. There is no build machine under a desk and no release engineer: a release is a git tag, and everything after that runs on GitHub Actions. This post is the whole pipeline, including the parts that are less pretty, like shipping without code signing certificates.

One tag, four runners

git tag v1.0.7 && git push origin main v1.0.7

Pushing a v* tag starts the release workflow. Pushing to main (or running it by hand) runs the same builds and keeps the installers as workflow artifacts, so every merge is a release candidate you can download and try.

The workflow builds on four runners in parallel:

RunnerProduces
Ubuntu 22.04.deb and .AppImage for x64
macOS (Apple Silicon).dmg for aarch64
macOS (Intel target).dmg for x64
WindowsNSIS -setup.exe and .msi for x64

Linux builds on Ubuntu 22.04 on purpose: a binary linked against an older glibc runs on newer systems, not the other way round, so the oldest supported release is the right place to build.

Each job builds the bum command line tool first, then the TypeScript UI, then hands both to tauri-action, which builds the app, bundles the installers and attaches them to the GitHub Release.

Shipping a CLI inside a desktop app

bum is a separate Rust binary, and it has to arrive with the app on every platform. Tauri calls this a sidecar: the binary is listed under externalBin and copied into each bundle. Getting it onto the user's PATH is different on each system:

The Windows hook was the fiddly one. NSIS's own string functions cut values at 1,024 characters, which would silently truncate a long PATH, so the hook uses PowerShell to edit the registry value. It reads the value without expanding it and writes it back as an expandable string, so entries like %USERPROFILE%\bin survive. It then broadcasts the change so new terminals see it without signing out, and it passes the install folder through an environment variable rather than pasting it into the script, because the folder contains the user name and a user name can contain a quote.

Because nothing else exercises an installer, CI runs the real one: it installs silently on the Windows runner, checks that bum.exe is on the user PATH and runs, uninstalls, and checks the entry is gone.

Updating without a server

Bumeter updates itself, and there is no update server. The release workflow also produces a signed latest.json and a signature for each installer, attached to the same GitHub Release. The app uses Tauri's updater plugin to read that file, so GitHub Releases is the whole distribution infrastructure.

Package managers

A download page works, but brew install and winget install are nicer. Both are generated from the release itself: a script reads the published assets and their SHA-256 digests from GitHub and writes the Homebrew cask and the WinGet manifests, so the versions and hashes cannot drift apart. A --check mode fails when the committed files no longer match the release, which is the sanity check before the next tag.

brew install --cask chieund/tap/bumeter

WinGet packages live as manifests in Microsoft's community repository, so publishing a version means a pull request there, which another script prepares and validates against the official schemas. That pull request is the step still in progress: Bumeter is not in WinGet yet, so on Windows you download the installer from the release for now.

Without code signing

An Apple Developer ID and a Windows code signing certificate cost money every year, and Bumeter is free. Shipping unsigned has consequences, and the honest approach is to say what they are:

None of this is hidden in a FAQ; the README and the install instructions say it where you need it.

The checklist

  1. Bump the version in Cargo.toml and tauri.conf.json, commit, tag, push the tag.
  2. Wait for the four jobs to attach their installers.
  3. Regenerate the cask and WinGet manifests from the release and commit them.
  4. Push the cask to the tap and open the WinGet pull request.

After that, the app's own updater carries everyone who already has it to the new version.

Bumeter is free and open source (GPL-3.0), and everything stays on your machine. The source is at github.com/chieund/bumeter. The workflow is .github/workflows/release.yml; packaging is in scripts/packaging.py and docs/packaging.md.

Download Bumeter