std/update

Updates: a manifest published over HTTPS names the newest version of each channel, with a file for each system; the update is downloaded, with progress, checked against its SHA-256 and its Ed25519 signature, and installed; the program restarts into it, and when the new version fails to start, the next start goes back to the version before.

import "std/update"
import "std/publickey"

PublicKey key = public_key_from_pem(UPDATE_KEY)?
Updater u = updater("https://example.com/notes/updates.json", key)
if check_rollback(u)? then
    restart_app(u, [])
end
match check_for_update(u)? do
    when SOME(update) then
        str file = download_update(u, update, func(i64 done, i64 total) => show_progress(done, total))?
        install_update(u, update, file)?
        restart_app(u, [])
    when NONE then
end
confirm_update_started(u)

The manifest is JSON:

{"channels": {
  "stable": {"version": "1.3.0", "notes": "What changed",
    "files": {"windows-x86_64": {"url": "https://example.com/notes-1.3.0.zip",
      "kind": "zip", "size": 1234567, "sha256": "...", "signature": "..."}}},
  "beta": {"version": "1.4.0-beta.1", "files": {}}}}

Each file is for a system as platform() and arch() name it, and of a kind of UPDATE_KINDS: "zip" or "tar.gz" of the program's folder (on macOS, of its .app); "appimage", which replaces the AppImage that runs; or "exe" (NSIS's installer, run with /S) or "msi" (run by msiexec /qn).

Function Result
updater(manifest_url, key, state_dir) an Updater for the running program: its version is build_version(), its channel "stable", what it replaces is the program's folder, or its .app, or the AppImage it runs from, and its state is kept in state_dir, beside what it replaces unless given
check_for_update(u) SOME of the UpdateInfo the channel offers when it is newer, by compare_versions; an ERROR of kind "http" or "invalid_manifest"
download_update(u, info, progress) the downloaded file, progress told the bytes that came and the size, checked; an ERROR of kind "invalid_update" when its hash or signature is wrong, which deletes it
verify_update(u, info, path) whether a file is the update: its SHA-256, and its signature by the updater's key
install_update(u, info, path) the update put in place, what it replaced kept to go back to; a running program's own file is moved aside, as Windows allows, not overwritten
restart_app(u, arguments) starts the new version and ends this one
check_rollback(u) at each start: true when the last update was started and never confirmed, so it failed, and the version before was put back
confirm_update_started(u), update_pending(u) the update kept, and what it replaced deleted; whether one waits for that
compare_versions(a, b) -1, 0, or 1, by semantic versioning: 1.0.0-beta.2 before 1.0.0
make_update_entry(key, path, url, kind) the manifest's entry for a file, signed with the private key, for publishing

Downloads use curl, which reads https:// and file:// URLs; the signatures are std/publickey's, so a program that imports std/update links with libcrypto.