mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-04-30 17:29:01 +02:00
[frontend] Built-in auto updater (#3845)
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:
parent
77decca678
commit
676b1aabfc
23 changed files with 856 additions and 375 deletions
|
|
@ -147,7 +147,8 @@ add_library(
|
|||
zstd_compression.h
|
||||
fs/ryujinx_compat.h fs/ryujinx_compat.cpp
|
||||
fs/symlink.h fs/symlink.cpp
|
||||
httplib.h)
|
||||
httplib.h
|
||||
net/net.h net/net.cpp)
|
||||
|
||||
if(WIN32)
|
||||
target_sources(common PRIVATE windows/timer_resolution.cpp
|
||||
|
|
@ -245,7 +246,7 @@ else()
|
|||
target_link_libraries(common PUBLIC Boost::headers)
|
||||
endif()
|
||||
|
||||
target_link_libraries(common PUBLIC Boost::filesystem Boost::context httplib::httplib)
|
||||
target_link_libraries(common PUBLIC Boost::filesystem Boost::context httplib::httplib nlohmann_json::nlohmann_json)
|
||||
|
||||
if (lz4_ADDED)
|
||||
target_include_directories(common PRIVATE ${lz4_SOURCE_DIR}/lib)
|
||||
|
|
|
|||
287
src/common/net/net.cpp
Normal file
287
src/common/net/net.cpp
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <optional>
|
||||
#include <boost/algorithm/string/classification.hpp>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
#include <boost/algorithm/string/split.hpp>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include "common/scm_rev.h"
|
||||
#include "net.h"
|
||||
|
||||
#include "common/logging.h"
|
||||
|
||||
#include "common/httplib.h"
|
||||
|
||||
#ifdef YUZU_BUNDLED_OPENSSL
|
||||
#include <openssl/cert.h>
|
||||
#endif
|
||||
|
||||
#define QT_TR_NOOP(x) x
|
||||
|
||||
namespace Common::Net {
|
||||
|
||||
std::vector<Asset> Release::GetPlatformAssets() const {
|
||||
// TODO(crueter): Need better handling for this as a whole.
|
||||
#ifdef NIGHTLY_BUILD
|
||||
std::vector<std::string> result;
|
||||
boost::algorithm::split(result, tag, boost::is_any_of("."));
|
||||
if (result.size() != 2)
|
||||
return {};
|
||||
const auto ref = result.at(1);
|
||||
#else
|
||||
const auto ref = tag;
|
||||
#endif
|
||||
|
||||
std::vector<Asset> found_assets;
|
||||
|
||||
// FIXME: This is mildly inefficient.
|
||||
// Finds assets based on a hierarchy of regex search strings.
|
||||
const auto find_asset = [&found_assets, ref, this](const std::string& name,
|
||||
const std::vector<std::string>& suffixes) {
|
||||
for (const std::string& asset : assets) {
|
||||
for (const auto& suffix : suffixes) {
|
||||
if (asset.ends_with(suffix)) {
|
||||
const std::string_view asset_sv = asset;
|
||||
const size_t pos = asset_sv.find_last_of('/');
|
||||
const std::string_view filename =
|
||||
(pos != std::string_view::npos) ? asset_sv.substr(pos + 1) : asset_sv;
|
||||
|
||||
found_assets.emplace_back(Asset{
|
||||
.name = name,
|
||||
.url = host,
|
||||
.path = asset,
|
||||
.filename = std::string{filename},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifdef ARCHITECTURE_x86_64
|
||||
find_asset("Standard", {"amd64-msvc-standard.exe", "amd64-msvc-standard.zip", "mingw-amd64-gcc-standard.exe", "mingw-amd64-gcc-standard.zip"});
|
||||
find_asset("PGO", {"mingw-amd64-clang-pgo.exe", "mingw-amd64-clang-pgo.zip"});
|
||||
#elif defined(ARCHITECTURE_arm64)
|
||||
find_asset("Standard", {"mingw-arm64-clang-standard.exe", "mingw-arm64-clang-standard.zip"});
|
||||
find_asset("PGO", {"mingw-arm64-clang-pgo.exe", "mingw-arm64-clang-pgo.zip"});
|
||||
#endif
|
||||
#elif defined(__APPLE__)
|
||||
#ifdef ARCHITECTURE_arm64
|
||||
find_asset("Standard", {".dmg", ".tar.gz"});
|
||||
#endif
|
||||
#elif defined(__ANDROID__)
|
||||
#ifdef ARCHITECTURE_x86_64
|
||||
find_asset("Standard", {"chromeos.apk"});
|
||||
#elif defined(ARCHITECTURE_arm64)
|
||||
#ifdef YUZU_LEGACY
|
||||
find_asset("Standard", {"legacy.apk"});
|
||||
#elif defined(GENSHIN_SPOOF)
|
||||
find_asset("Standard", {"optimized.apk"});
|
||||
#else
|
||||
find_asset("Standard", {"standard.apk"});
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
return found_assets;
|
||||
}
|
||||
|
||||
static inline u64 ParseIsoTimestamp(const std::string& iso) {
|
||||
if (iso.empty())
|
||||
return 0;
|
||||
|
||||
std::string buf = iso;
|
||||
if (buf.back() == 'Z')
|
||||
buf.pop_back();
|
||||
|
||||
std::tm tm{};
|
||||
std::istringstream ss(buf);
|
||||
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
|
||||
if (ss.fail())
|
||||
return 0;
|
||||
|
||||
#ifdef _WIN32
|
||||
return static_cast<u64>(_mkgmtime(&tm));
|
||||
#else
|
||||
return static_cast<u64>(timegm(&tm));
|
||||
#endif
|
||||
}
|
||||
|
||||
std::optional<Release> Release::FromJson(const nlohmann::json& json, const std::string& host,
|
||||
const std::string& repo) {
|
||||
Release rel;
|
||||
if (!json.is_object())
|
||||
return std::nullopt;
|
||||
|
||||
rel.tag = json.value("tag_name", std::string{});
|
||||
if (rel.tag.empty())
|
||||
return std::nullopt;
|
||||
|
||||
rel.title = json.value("name", rel.tag);
|
||||
rel.id = json.value("id", std::hash<std::string>{}(rel.title));
|
||||
|
||||
rel.published = ParseIsoTimestamp(json.value("published_at", std::string{}));
|
||||
rel.prerelease = json.value("prerelease", false);
|
||||
|
||||
auto body = json.value("body", rel.title);
|
||||
boost::replace_all(body, "\\r", "");
|
||||
boost::replace_all(body, "\\n", "\n");
|
||||
rel.body = body;
|
||||
|
||||
rel.host = host;
|
||||
|
||||
const auto release_base =
|
||||
fmt::format("{}/{}/releases", Common::g_build_auto_update_website, repo);
|
||||
const auto fallback_html = fmt::format("{}/tag/{}", release_base, rel.tag);
|
||||
rel.html_url = json.value("html_url", fallback_html);
|
||||
|
||||
// This is our own "fake" API.
|
||||
if (json.contains("base")) {
|
||||
const auto base = json.value("base", fmt::format("https://{}", Common::g_build_auto_update_api));
|
||||
rel.base_download_url = fmt::format("{}/{}", base, rel.tag);
|
||||
|
||||
// Assets are easy :)
|
||||
rel.assets = json.value("assets", std::vector<std::string>{});
|
||||
} else {
|
||||
const auto base_download_url = fmt::format("/{}/releases/download/{}", repo, rel.tag);
|
||||
|
||||
rel.base_download_url = base_download_url;
|
||||
|
||||
// assets are a bit more complex here. :(
|
||||
std::vector<std::string> assets;
|
||||
const nlohmann::json& arr = json["assets"];
|
||||
for (const auto &obj : arr) {
|
||||
const auto url = obj.value("browser_download_url", std::string{});
|
||||
assets.emplace_back(url);
|
||||
}
|
||||
|
||||
rel.assets = assets;
|
||||
}
|
||||
|
||||
return rel;
|
||||
}
|
||||
|
||||
std::optional<Release> Release::FromJson(const std::string_view& json, const std::string& host,
|
||||
const std::string& repo) {
|
||||
try {
|
||||
return FromJson(nlohmann::json::parse(json), host, repo);
|
||||
} catch (std::exception& e) {
|
||||
LOG_WARNING(Common, "Failed to parse JSON: {}", e.what());
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<Release> Release::ListFromJson(const nlohmann::json& json, const std::string& host,
|
||||
const std::string& repo) {
|
||||
if (!json.is_array())
|
||||
return {};
|
||||
|
||||
std::vector<Release> releases;
|
||||
for (const auto& obj : json) {
|
||||
auto rel = Release::FromJson(obj, host, repo);
|
||||
if (rel)
|
||||
releases.emplace_back(rel.value());
|
||||
}
|
||||
return releases;
|
||||
}
|
||||
|
||||
std::vector<Release> Release::ListFromJson(const std::string_view& json, const std::string& host,
|
||||
const std::string& repo) {
|
||||
try {
|
||||
return ListFromJson(nlohmann::json::parse(json), host, repo);
|
||||
} catch (std::exception& e) {
|
||||
LOG_WARNING(Common, "Failed to parse JSON: {}", e.what());
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<std::string> MakeRequest(const std::string& url, const std::string& path) {
|
||||
try {
|
||||
constexpr std::size_t timeout_seconds = 15;
|
||||
|
||||
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(Common, "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(Common, "GET to {}{} returned null", url, path);
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto& response = result.value();
|
||||
if (response.status >= 400) {
|
||||
LOG_ERROR(Common, "GET to {}{} returned error status code: {}", url, path,
|
||||
response.status);
|
||||
return {};
|
||||
}
|
||||
if (!response.headers.contains("content-type")) {
|
||||
LOG_ERROR(Common, "GET to {}{} returned no content", url, path);
|
||||
return {};
|
||||
}
|
||||
|
||||
return response.body;
|
||||
} catch (std::exception& e) {
|
||||
LOG_ERROR(Common, "GET to {}{} failed during update check: {}", url, path, e.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Release> GetReleases() {
|
||||
const auto body = GetReleasesBody();
|
||||
|
||||
if (!body) {
|
||||
LOG_WARNING(Common, "Failed to get stable releases");
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::string_view body_str = body.value();
|
||||
const auto url = fmt::format("https://{}", Common::g_build_auto_update_stable_api);
|
||||
return Release::ListFromJson(body_str, url, Common::g_build_auto_update_stable_repo);
|
||||
}
|
||||
|
||||
std::optional<Release> GetLatestRelease() {
|
||||
const auto releases_path = Common::g_build_auto_update_api_path;
|
||||
const auto url = fmt::format("https://{}", Common::g_build_auto_update_api);
|
||||
|
||||
const auto body = MakeRequest(url, releases_path);
|
||||
if (!body) {
|
||||
LOG_WARNING(Common, "Failed to get latest release");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::string_view body_str = body.value();
|
||||
return Release::FromJson(body_str, url, Common::g_build_auto_update_repo);
|
||||
}
|
||||
|
||||
std::optional<std::string> GetReleasesBody() {
|
||||
const auto releases_path =
|
||||
fmt::format("/{}/{}/releases", Common::g_build_auto_update_stable_api_path,
|
||||
Common::g_build_auto_update_stable_repo);
|
||||
const auto url = fmt::format("https://{}", Common::g_build_auto_update_stable_api);
|
||||
|
||||
return MakeRequest(url, releases_path);
|
||||
}
|
||||
|
||||
} // namespace Common::Net
|
||||
56
src/common/net/net.h
Normal file
56
src/common/net/net.h
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Common::Net {
|
||||
|
||||
typedef struct {
|
||||
std::string name;
|
||||
std::string url;
|
||||
std::string path;
|
||||
std::string filename;
|
||||
} Asset;
|
||||
|
||||
typedef struct Release {
|
||||
std::string title;
|
||||
std::string body;
|
||||
std::string tag;
|
||||
std::string base_download_url;
|
||||
std::string html_url;
|
||||
std::string host;
|
||||
|
||||
std::vector<std::string> assets;
|
||||
|
||||
u64 id;
|
||||
u64 published;
|
||||
bool prerelease;
|
||||
|
||||
// Get the relevant list of assets for the current platform.
|
||||
std::vector<Asset> GetPlatformAssets() const;
|
||||
|
||||
static std::optional<Release> FromJson(const nlohmann::json& json, const std::string &host, const std::string& repo);
|
||||
static std::optional<Release> FromJson(const std::string_view& json, const std::string &host, const std::string& repo);
|
||||
static std::vector<Release> ListFromJson(const nlohmann::json &json, const std::string &host, const std::string &repo);
|
||||
static std::vector<Release> ListFromJson(const std::string_view &json, const std::string &host, const std::string &repo);
|
||||
} Release;
|
||||
|
||||
// Make a request via httplib, and return the response body if applicable.
|
||||
std::optional<std::string> MakeRequest(const std::string &url, const std::string &path);
|
||||
|
||||
// Get all of the latest stable releases.
|
||||
std::vector<Release> GetReleases();
|
||||
|
||||
// Get all of the latest stable releases as text.
|
||||
std::optional<std::string> GetReleasesBody();
|
||||
|
||||
// Get the latest release of the current channel.
|
||||
std::optional<Release> GetLatestRelease();
|
||||
|
||||
}
|
||||
|
|
@ -22,6 +22,9 @@
|
|||
#define BUILD_AUTO_UPDATE_API "@BUILD_AUTO_UPDATE_API@"
|
||||
#define BUILD_AUTO_UPDATE_API_PATH "@BUILD_AUTO_UPDATE_API_PATH@"
|
||||
#define BUILD_AUTO_UPDATE_REPO "@BUILD_AUTO_UPDATE_REPO@"
|
||||
#define BUILD_AUTO_UPDATE_STABLE_API "@BUILD_AUTO_UPDATE_STABLE_API@"
|
||||
#define BUILD_AUTO_UPDATE_STABLE_API_PATH "@BUILD_AUTO_UPDATE_STABLE_API_PATH@"
|
||||
#define BUILD_AUTO_UPDATE_STABLE_REPO "@BUILD_AUTO_UPDATE_STABLE_REPO@"
|
||||
#define IS_NIGHTLY_BUILD @IS_NIGHTLY_BUILD@
|
||||
|
||||
namespace Common {
|
||||
|
|
@ -45,5 +48,8 @@ constexpr const char g_build_auto_update_website[] = BUILD_AUTO_UPDATE_WEBSITE;
|
|||
constexpr const char g_build_auto_update_api[] = BUILD_AUTO_UPDATE_API;
|
||||
constexpr const char g_build_auto_update_api_path[] = BUILD_AUTO_UPDATE_API_PATH;
|
||||
constexpr const char g_build_auto_update_repo[] = BUILD_AUTO_UPDATE_REPO;
|
||||
constexpr const char g_build_auto_update_stable_api[] = BUILD_AUTO_UPDATE_STABLE_API;
|
||||
constexpr const char g_build_auto_update_stable_api_path[] = BUILD_AUTO_UPDATE_STABLE_API_PATH;
|
||||
constexpr const char g_build_auto_update_stable_repo[] = BUILD_AUTO_UPDATE_STABLE_REPO;
|
||||
|
||||
} // namespace Common
|
||||
|
|
|
|||
|
|
@ -28,5 +28,8 @@ extern const char g_build_auto_update_website[];
|
|||
extern const char g_build_auto_update_api[];
|
||||
extern const char g_build_auto_update_api_path[];
|
||||
extern const char g_build_auto_update_repo[];
|
||||
extern const char g_build_auto_update_stable_api[];
|
||||
extern const char g_build_auto_update_stable_api_path[];
|
||||
extern const char g_build_auto_update_stable_repo[];
|
||||
|
||||
} // namespace Common
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue