Docs

App Updates

nsh/updater lets a packaged app check for a newer version, download it and replace itself. The UI is yours: the module gives you the state and the actions. It updates the whole app; to deliver only NSH screens to a host app, see Signed Packages.

PlatformWhat it does
macOS, Windows, LinuxChecks, downloads, replaces the app and restarts it
iOS, AndroidChecks, and gives you the link to send the user to
WebNothing to do: a deploy updates the app, checkForUpdate resolves null

#Set the source

From GitHub Releases, in nsh.json:

{ "version": "1.2.0", "updater": { "github": "owner/repo" } }

From your own host, which needs a signing key, because whoever controls the host would otherwise control what your app runs (nsh keygen keys/update, keep the .secret out of your repository):

{
  "version": "1.2.0",
  "updater": {
    "url": "https://dl.example.com/app/latest.json",
    "publicKey": "<contents of keys/update.pub>"
  }
}

publicKey also works with github: unsigned manifests are then refused.

#Use it

import { checkForUpdate, downloadUpdate, installAndRestart } from "nsh/updater";
import { openUrl } from "nsh";

const update = await checkForUpdate(); // null when up to date
if (update?.canInstall) {
  await downloadUpdate(({ received, total }) => setProgress(received / (total ?? 1)));
  await installAndRestart();
} else if (update) {
  await openUrl(update.url);
}

update has version, notes, url and canInstall. canInstall is false on iOS and Android, and on the desktop when the app is installed where the user cannot write, such as by a system package. In a development run checkForUpdate resolves null.

installAndRestart exits this run, so save what you need first. If it fails, the app stays as it was.

#Publish a release

version in nsh.json must be semver and grow with each release. With "updater" set, nsh package also writes an archive named for its platform. Package on each platform, then write the manifest:

nsh package --target linux   # My-App-1.3.0-linux-x86_64.tar.gz
nsh package --target macos   # My-App-1.3.0-macos-aarch64.tar.gz
nsh package --target windows # My-App-1.3.0-windows-x86_64.zip

nsh update-manifest *.tar.gz *.zip \
  --notes CHANGES.md \
  --key keys/update.secret \
  --link ios=https://apps.apple.com/app/id000000000

Upload the archives, latest.json and latest.json.sig (with --key) as assets of the GitHub Release, or to your host next to each other. A release marked as a draft or prerelease on GitHub is not offered.

#What is checked

  • The manifest’s signature, when there is a publicKey.
  • The manifest’s app id: it must be this app’s.
  • The version: only a newer one is offered, so an old manifest cannot downgrade the app.
  • URLs: HTTPS only (plain HTTP only to this machine, for local tests).
  • The archive’s size and SHA-256.
  • The new app: its id and executable, and on macOS its code signature.
  • That this run is the package itself: an NSH module running inside another app cannot replace it.

On macOS, nsh package signs ad hoc, which is fine to test, but Gatekeeper may stop it on other Macs. To distribute, sign the .app with your Developer ID, notarize it, and archive it again under the same name (tar -czf <archive> <Name>.app) before nsh update-manifest.

#Rust apps

A native Rust app uses nsh-updater directly and supplies its own HTTP transport. Use Config::check to verify the manifest and Download to verify the archive. Then call Installation::install_and_launch and exit only if it succeeds; this keeps the old installation until the new process can be launched and rolls back on launch failure. Calling install followed by launch separately does not provide that rollback. This detects launch errors, not a crash after a process has already started.

The Config, Manifest, Asset and Available records are non-exhaustive. Construct them with Default::default() and assign the fields you need.