Add self-update to an app
An app needs two things before it can update itself: the URL of its release manifest and the minisign public key that signs it. Sign and publish a release makes both. The crate supplies no URL, path or host of its own; the app passes them in.
1. Sweep leftovers at start-up
Section titled “1. Sweep leftovers at start-up”let exe = std::env::current_exe()?;renew::clean_old_binary(&exe);clean_old_binary removes a leftover <exe>.staging (an install that was
cut off) or <exe>.old (the binary Windows parks during a swap). It never
fails: a missing file is fine. Call it before install, because install
refuses to start while <exe>.staging exists.
2. Check
Section titled “2. Check”const MANIFEST_URL: &str = "https://example.com/app/manifest.json";const RELEASE_KEY: &str = "RWQ…";
let check = renew::check(MANIFEST_URL, RELEASE_KEY, env!("CARGO_PKG_VERSION"))?;check(url, public_key, current) fetches the manifest (at most 1 MiB) and
its signature at the same URL with .minisig appended (at most 4 KiB), each
with a 3 s timeout, verifies the signature over the exact manifest bytes, and
only then reads it. It answers a Check:
update: the newest release newer thancurrent, whatever its major, with this platform’s binary, orNone;new_major: the same release when it is a later major thancurrent, orNone.
check keeps no state, so call it as often as you like: at start-up, on a
timer, or from a menu item.
3. Install and relaunch
Section titled “3. Install and relaunch”if let Some(update) = check.update { if check.new_major.is_some() { println!("{} is a new major version", update.version); } renew::install(&update.binary, &exe)?; renew::restart(&exe)?; std::process::exit(0);}install(&binary, &exe) downloads to <exe>.staging, a file it creates
itself with mode 0755, hashing as it goes, with a 180 s timeout. It checks
the exact size and the sha256, then renames the file over exe in one step.
On any failure it removes the staging file it made and the old binary stays
where it was.
restart(&exe) spawns the new binary detached, with no stdin, stdout or
stderr; the app exits after it.
A macOS app bundle
Section titled “A macOS app bundle”When exe runs from a .app bundle (<Name>.app/Contents/MacOS/<exe>), the
same two calls update the whole bundle, because a new executable inside a
signed, notarised bundle breaks its signature. Publish the bundle zipped under
platforms.macos-arm64.app (Sign and publish), and
install downloads and checks the zip, unpacks it beside the running bundle,
refuses it unless its code signature verifies and its Team ID and identifier
match the running app’s, and swaps the bundles in one step. The running
process carries on from the old bundle, kept as <bundle>.old until the next
clean_old_binary; restart opens the new one with open -n.
If the user can’t write the bundle or its folder, such as an app owned by
root in /Applications or one run from a disk image, install answers
NotWritable before downloading anything: show that the app can’t update
itself there. Formats
lists every step. This path is tested only on GitHub’s macOS runners, without
a notarised bundle.
The app keeps its own ceremony around these calls (a backup first, a prompt to the user, a quiet moment to swap) and picks when to make them.
Handle the errors
Section titled “Handle the errors”Every failure is a RenewError whose message names what went wrong:
| Variant | Message |
|---|---|
ManifestUnreachable |
the release manifest could not be reached |
ManifestMalformed |
the release manifest did not parse |
BadPublicKey |
the public key is not a minisign public key |
BadSignature |
the release manifest’s signature does not verify |
NoPlatformBinary |
no binary is published for this platform |
DownloadFailed |
the download did not arrive whole |
BadVersion |
a version string did not parse |
StageFailed |
the downloaded binary could not be staged: … |
VerifyFailed |
the downloaded binary failed verification: … |
InsecureUrl |
refused a plain http url: only https, or http to this machine, is fetched |
SwapFailed |
the new binary could not be swapped in: … |
NoAppBundle |
this app runs from a macOS .app bundle and no bundle is published for it |
NotWritable |
… is not writable by this user, so the app can’t update itself |
A missing .minisig counts as ManifestUnreachable. None of them leaves a
new binary in place: an app can show the message and carry on at its current
version.
The whole thing
Section titled “The whole thing”use std::error::Error;
use renew::Check;
const MANIFEST_URL: &str = "https://example.com/app/manifest.json";const RELEASE_KEY: &str = "RWQ…";
fn update() -> Result<(), Box<dyn Error>> { let exe = std::env::current_exe()?; renew::clean_old_binary(&exe); let Check { update, new_major } = renew::check(MANIFEST_URL, RELEASE_KEY, env!("CARGO_PKG_VERSION"))?; let Some(update) = update else { return Ok(()); }; if new_major.is_some() { println!("{} is a new major version", update.version); } println!("installing {}", update.version); renew::install(&update.binary, &exe)?; renew::restart(&exe)?; std::process::exit(0);}The API reference lists every public item,
including platform_key(), staging_path(), AppBundle and the timeout
and size constants.