Update the application - fakeivchenko/lwjwae GitHub Wiki

An application can find its new versions, download them, and install them, and it installs only what your key signed. In this guide, a notes application at version 1.0.0 finds version 1.1.0, shows what changed, and downloads it.

An update that the application found and downloads, on Linux and on Windows 11

How a release works

A release is a directory on any web server: the package of every platform, a manifest.json that names them with their size and SHA-256, and manifest.json.sig, the Ed25519 signature of the manifest. The application downloads the manifest and the signature, checks the signature with the public key that it was built with, and only then believes the manifest. It then checks the size and the SHA-256 of the package that it downloads. HTTPS alone isn't trusted, so whoever controls the server, or a cache in front of it, can't make the application install a file that your private key didn't sign for.

Set up the build

  1. Generate the keys once, for the life of the application:

    ./gradlew generateUpdateKeys
    

    The task prints a public key and a private key. Keep the private key in a secret of your CI and never in the repository. A new pair locks the installed versions out of later updates, because they trust only the public key that they were built with.

  2. Tell the plugin where the manifest lives, and give it the public key:

    lwjwae {
        updates {
            manifestUrl = "https://example.com/notes/manifest.json"
            publicKey = "MCowBQYDK2VwAyEA..."
            notes = "Notes sync between your computers."
        }
    }
    

    The plugin writes both into the application, where application.updater() finds them. The version that runs is the version of the project.

  3. Build a release. On each platform, packageUpdate puts its package into build/lwjwae/update, named after the platform, such as linux-x64.AppImage. With the files of every platform in one directory, updateManifest writes the manifest and signs it with the private key from the LWJWAE_UPDATE_PRIVATE_KEY environment variable:

    ./gradlew packageUpdate
    LWJWAE_UPDATE_PRIVATE_KEY=... ./gradlew updateManifest
    
  4. Upload the directory to the manifest URL. The release is out.

minimumVersion = "1.0.0" in updates {} marks every older version, which then sees the update as mandatory. What the application does with that is up to you.

Find and install an update from Java

Updater updater = application.updater();
application.bind("currentVersion", Void.class, _ -> updater.currentVersion());
application.bind(
    "checkForUpdate",
    Void.class,
    _ ->
        updater
            .check()
            .join()
            .map(update -> Map.of("version", update.version(), "notes", update.notes()))
            .orElse(null));
application.bind(
    "download",
    Void.class,
    _ -> {
      Update update = updater.check().join().orElseThrow();
      downloaded.set(
          updater
              .download(update, fraction -> application.emit("downloaded", fraction))
              .join());
      return update.installable();
    });
application.bind(
    "restart",
    Void.class,
    _ -> {
      updater.installAndRestart(downloaded.get());
      return null;
    });
Method What it does
check() Downloads and verifies the manifest, and returns the Update for this platform if its version is newer than the running one.
download(update, progress) Downloads the package, reports from 0 to 1, and checks its size and SHA-256.
installAndRestart(downloaded) Starts the installer, which waits for the application to quit, puts the new version in place, and starts it. Then quits the application.

An Update has the version, the notes, whether it is mandatory, and whether this copy is installable. A version that isn't newer than the running one is no update, so a manifest can't take the application back.

The guide application serves a signed release from the same machine, so that it runs without a web server. A real application needs nothing of the kind: it gets the URL and the key from the plugin.

Find and install an update from the page

lwjwae.updates does the same in two calls:

const update = await lwjwae.updates.check();
if (update?.installable) {
  await lwjwae.updates.install({ onProgress: (fraction) => bar.value = fraction });
}

check resolves to { version, notes, mandatory, installable, size }, or to null when there is no update. install downloads the update and installs it, which ends the application. Both reject with no-update, not-installable, update-rejected, or download-failed.

Platforms

Platform What gets installed Installable when
Windows The .msi of the new version, with msiexec /passive. The application runs as a native executable.
macOS The .app bundle of the new version, signed by the same team. The bundle is in a directory that the user can write.
Linux The AppImage of the new version, in place of the running one. The application runs as an AppImage. A .deb or an Arch package updates through its package manager, so there check finds the update, but it isn't installable.

Good to know

  • check fails with UpdateRejectedException for a signature that isn't the one of your key or a manifest that isn't one, and with UpdateDownloadFailedException when either can't be downloaded.
  • Under ./gradlew run, the application isn't a package, so check and download work, but no update is installable.
  • The taskbar can show the download too: updater.download(update, application::progress). See Show progress in the taskbar.