Skip to content

Publishing releases for AUpdater#

AUpdater does not impose where the update comes from. What it needs is a ZIP archive with a complete copy of your application, found by checkForUpdatesImpl and downloaded by downloadUpdateImpl. This page shows the setup that App Template ⚡ uses: GitHub releases.

Produce the archive#

The update is always a ZIP portable. Build it with AUI_PORTABLE_ZIP packaging, see App Packaging:

cmake .. -DCMAKE_BUILD_TYPE=Release -DAUI_APP_PACKAGING=AUI_PORTABLE_ZIP
cmake --build . --config Release
cpack . -c Release -B artifacts
gh release upload ${{ github.ref }} artifacts/*.*

The archive name contains the platform and the architecture, followed by -portable, i.e. myapp-1.2.3-linux-x86_64-portable.zip. Upload the archive for every platform and architecture you support to the same release. tar.gz archives are not supported by the updater.

If you distribute a Windows installer as well, it has to install into the user's directory so the updater can replace files without elevation; see Inno Setup.

Pick the right asset#

aui::updater::AppropriatePortablePackagePredicate tells whether a file name is a portable for the current platform and architecture. It is meant to be used with ranges::find_if. If you name archives differently, pass your own qualifier:

1
2
3
aui::updater::AppropriatePortablePackagePredicate p { .qualifier = "windows-x86_64" };
EXPECT_TRUE(p("app-windows-x86_64.zip"));
EXPECT_FALSE(p("app-windows-x86.zip"));

Check GitHub and download#

aui::updater::github::latestRelease (see aui::updater::github::LatestReleaseResponse) returns the latest non-draft, non-prerelease release of a repository together with its assets. The example below compares the tag with the current version, finds the matching asset, asks the user and starts the download.

static constexpr auto LOG_TAG = "MyUpdater";
class MyUpdater: public AUpdater {
public:
    ~MyUpdater() override = default;

protected:
    AFuture<void> checkForUpdatesImpl() override {
        return AUI_THREADPOOL {
            try {
                auto githubLatestRelease = aui::updater::github::latestRelease("aui-framework", "example_app");
                ALogger::info(LOG_TAG) << "Found latest release: " << githubLatestRelease.tag_name;
                auto ourVersion = aui::updater::Semver::fromString(AUI_PP_STRINGIZE(AUI_CMAKE_PROJECT_VERSION));
                auto theirVersion = aui::updater::Semver::fromString(githubLatestRelease.tag_name);

                if (theirVersion <= ourVersion) {
                    getThread()->enqueue([] {
                      AMessageBox::show(
                          nullptr, "No updates found", "You are running the latest version.", AMessageBox::Icon::INFO);
                    });
                    return;
                }
                aui::updater::AppropriatePortablePackagePredicate predicate {};
                auto it = ranges::find_if(
                    githubLatestRelease.assets, predicate, &aui::updater::github::LatestReleaseResponse::Asset::name);
                if (it == ranges::end(githubLatestRelease.assets)) {
                    ALogger::warn(LOG_TAG)
                        << "Newer version was found but a package appropriate for your platform is not available. "
                           "Expected: "
                        << predicate.getQualifierDebug() << ", got: "
                        << (githubLatestRelease.assets |
                            ranges::view::transform(&aui::updater::github::LatestReleaseResponse::Asset::name));
                    return;
                }
                ALogger::info(LOG_TAG) << "To download: " << (mDownloadUrl = it->browser_download_url);

                getThread()->enqueue([this, self = shared_from_this(), version = githubLatestRelease.tag_name] {
                    if (AMessageBox::show(
                            nullptr, "New version found!", "Found version: {}\n\nWould you like to update?"_format(version),
                            AMessageBox::Icon::INFO, AMessageBox::Button::YES_NO) != AMessageBox::ResultButton::YES) {
                        return;
                    }

                    downloadUpdate();
                });

            } catch (const AException& e) {
                ALogger::err(LOG_TAG) << "Can't check for updates: " << e;
                getThread()->enqueue([] {
                    AMessageBox::show(
                        nullptr, "Oops!", "There is an error occurred while checking for updates. Please try again later.",
                        AMessageBox::Icon::CRITICAL);
                });
            }
        };
    }

    AFuture<void> downloadUpdateImpl(const APath& unpackedUpdateDir) override {
        return AUI_THREADPOOL {
          try {
              AUI_ASSERTX(!mDownloadUrl.empty(), "make a successful call to checkForUpdates first");
              downloadAndUnpack(mDownloadUrl, unpackedUpdateDir);
              reportReadyToApplyAndRestart(makeDefaultInstallationCmdline());
          } catch (const AException& e) {
              ALogger::err(LOG_TAG) << "Can't check for updates: " << e;
              getThread()->enqueue([] {
                AMessageBox::show(
                    nullptr, "Oops!", "There is an error occurred while downloading update. Please try again later.",
                    AMessageBox::Icon::CRITICAL);
              });
          }
        };
    }

private:
    AString mDownloadUrl;
};

downloadAndUnpack downloads the archive, reports progress to AUpdater::status and unpacks into unpackedUpdateDir. reportReadyToApplyAndRestart then switches the status to AUpdater::StatusWaitingForApplyAndRestart and remembers the command line to launch, so even if the user quits the app now, the update is applied on the next start.

Warning

Anonymous GitHub API requests are rate-limited per IP address. Do not check for updates more often than once per several hours, and never in a loop.

Other sources#

Nothing in AUpdater is tied to GitHub. Fetch a JSON manifest from your server in checkForUpdatesImpl, remember the download URL in a member, and pass it to downloadAndUnpack in downloadUpdateImpl. Only a ZIP archive is understood by downloadAndUnpack; for other formats, unpack into unpackedUpdateDir yourself.