[frontend] Built-in auto updater (#3845)
Some checks are pending
tx-src / sources (push) Waiting to run
Check Strings / check-strings (push) Waiting to run

Checks latest release and opens a dialog containing the changelog, and
allow the user to select a specific build to download. After
downloading, it prompts the user to open it.

On Windows, this just opens up the zip in File Explorer. In the future setup files will be available. On macOS this opens up the DMG in Finder so the user can drag it to the Applications folder. Android retains the auto-update functionality from before, but updated to the new scheme. Body/View on Forgejo are not implemented, that should be in a future PR.

Additionally, moved some common httplib incantations to `Common::Net`. This will serve as the common network accessor and JSON parser from here on out.

TODO:
- [x] android :(
- [x] Search for builds based on keywords, with weights towards certain builds (e.g. macOS will search for dmg then tar.gz, windows msvc then mingw/exe then zip, etc.)
- [x] remove linux leftovers
- [x] don't allow asset selection on platforms w/o assets
- [x] nightly changelog should be in the real

FUTURE IMPLEMENTATION:
- [ ] Body/View on Forgejo for Android
- [ ] Setup files for Windows (Eden/nightly are separate) -- maybe portable/setup selector?
- [ ] Something else I'm forgetting

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3845
This commit is contained in:
crueter 2026-04-28 20:42:23 +02:00
parent 77decca678
commit 676b1aabfc
No known key found for this signature in database
GPG key ID: 425ACD2D4830EBC6
23 changed files with 856 additions and 375 deletions

View file

@ -5,146 +5,45 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#ifdef NIGHTLY_BUILD
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
#endif
#include <fmt/format.h>
#include "common/logging.h"
#include "common/net/net.h"
#include "common/scm_rev.h"
#include "update_checker.h"
#include "common/httplib.h"
#include "common/logging.h"
#ifdef YUZU_BUNDLED_OPENSSL
#include <openssl/cert.h>
#endif
std::optional<Common::Net::Release> UpdateChecker::GetUpdate() {
const auto latest = Common::Net::GetLatestRelease();
if (!latest) return std::nullopt;
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
LOG_INFO(Frontend, "Received update {}", latest->title);
std::optional<std::string> UpdateChecker::GetResponse(std::string url, std::string path)
{
try {
constexpr std::size_t timeout_seconds = 15;
#ifdef NIGHTLY_BUILD
std::vector<std::string> result;
std::unique_ptr<httplib::Client> client = std::make_unique<httplib::Client>(url);
client->set_connection_timeout(timeout_seconds);
client->set_read_timeout(timeout_seconds);
client->set_write_timeout(timeout_seconds);
#ifdef YUZU_BUNDLED_OPENSSL
client->load_ca_cert_store(kCert, sizeof(kCert));
#endif
if (client == nullptr) {
LOG_ERROR(Frontend, "Invalid URL {}{}", url, path);
return {};
}
httplib::Request request{
.method = "GET",
.path = path,
};
client->set_follow_location(true);
httplib::Result result = client->send(request);
if (!result) {
LOG_ERROR(Frontend, "GET to {}{} returned null", url, path);
return {};
}
const auto &response = result.value();
if (response.status >= 400) {
LOG_ERROR(Frontend,
"GET to {}{} returned error status code: {}",
url,
path,
response.status);
return {};
}
if (!response.headers.contains("content-type")) {
LOG_ERROR(Frontend, "GET to {}{} returned no content", url, path);
return {};
}
return response.body;
} catch (std::exception &e) {
LOG_ERROR(Frontend,
"GET to {}{} failed during update check: {}",
url,
path,
e.what());
boost::split(result, latest->tag, boost::is_any_of("."));
if (result.size() != 2)
return std::nullopt;
}
}
std::optional<UpdateChecker::Update> UpdateChecker::GetLatestRelease() {
#ifdef YUZU_BUNDLED_OPENSSL
const auto update_check_url = fmt::format("https://{}", Common::g_build_auto_update_api);
const std::string tag = result[1];
boost::split(result, std::string{Common::g_build_version}, boost::is_any_of("-"));
if (result.empty())
return std::nullopt;
const std::string build = result[0];
#else
const auto update_check_url = std::string{Common::g_build_auto_update_api};
const std::string tag = latest->tag;
const std::string build = Common::g_build_version;
#endif
auto update_check_path = std::string{Common::g_build_auto_update_api_path};
try {
const auto response = UpdateChecker::GetResponse(update_check_url, update_check_path);
if (tag != build)
return latest;
if (!response)
return {};
const std::string latest_tag = nlohmann::json::parse(response.value()).at("tag_name");
const std::string latest_name = nlohmann::json::parse(response.value()).at("name");
return Update{latest_tag, latest_name};
} catch (nlohmann::detail::out_of_range&) {
LOG_ERROR(Frontend,
"Parsing JSON response from {}{} failed during update check: "
"nlohmann::detail::out_of_range",
update_check_url,
update_check_path);
return {};
} catch (nlohmann::detail::type_error&) {
LOG_ERROR(Frontend,
"Parsing JSON response from {}{} failed during update check: "
"nlohmann::detail::type_error",
update_check_url,
update_check_path);
return {};
}
}
std::optional<UpdateChecker::Update> UpdateChecker::GetUpdate() {
const std::optional<UpdateChecker::Update> latest_release_tag =
UpdateChecker::GetLatestRelease();
if (!latest_release_tag)
goto empty;
{
std::string tag, build;
if (Common::g_is_nightly_build) {
std::vector<std::string> result;
boost::split(result, latest_release_tag->tag, boost::is_any_of("."));
if (result.size() != 2)
goto empty;
tag = result[1];
boost::split(result, std::string{Common::g_build_version}, boost::is_any_of("-"));
if (result.empty())
goto empty;
build = result[0];
} else {
tag = latest_release_tag->tag;
build = Common::g_build_version;
}
if (tag != build)
return latest_release_tag.value();
}
empty:
return std::nullopt;
}

View file

@ -8,16 +8,10 @@
#pragma once
#include <optional>
#include <string>
#include "common/net/net.h"
namespace UpdateChecker {
typedef struct {
std::string tag;
std::string name;
} Update;
std::optional<Common::Net::Release> GetUpdate();
std::optional<std::string> GetResponse(std::string url, std::string path);
std::optional<Update> GetLatestRelease();
std::optional<Update> GetUpdate();
} // namespace UpdateChecker