2026-08-27 19:35:11 +03:00
|
|
|
|
#include "javaruntimeservice.h"
|
2026-09-03 09:18:48 +03:00
|
|
|
|
#include "localization.h"
|
2026-08-27 19:35:11 +03:00
|
|
|
|
|
|
|
|
|
|
#include "launcherpaths.h"
|
|
|
|
|
|
|
|
|
|
|
|
#include <QFile>
|
|
|
|
|
|
#include <QJsonArray>
|
|
|
|
|
|
#include <QJsonDocument>
|
|
|
|
|
|
#include <QJsonObject>
|
|
|
|
|
|
#include <QNetworkAccessManager>
|
|
|
|
|
|
#include <QNetworkReply>
|
|
|
|
|
|
#include <QNetworkRequest>
|
|
|
|
|
|
#include <QSaveFile>
|
|
|
|
|
|
#include <QSysInfo>
|
|
|
|
|
|
#include <QTimer>
|
|
|
|
|
|
#include <QUrlQuery>
|
|
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
#include <utility>
|
|
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
|
|
const QString kAdoptiumApi = QStringLiteral("https://api.adoptium.net/v3");
|
|
|
|
|
|
|
|
|
|
|
|
// Список компонентов рантайма Mojang. Ссылка зашита в официальный лаунчер и
|
|
|
|
|
|
// меняется вместе с ним; сами компоненты обновляются по этому же адресу.
|
|
|
|
|
|
const QString kMojangRuntimesUrl = QStringLiteral(
|
|
|
|
|
|
"https://piston-meta.mojang.com/v1/products/java-runtime/"
|
|
|
|
|
|
"2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json");
|
|
|
|
|
|
|
|
|
|
|
|
// Сборки Java выходят раз в квартал, а не раз в неделю: суток кэша достаточно,
|
|
|
|
|
|
// чтобы новая появилась в тот же день, и хватает на десяток открытий окна.
|
|
|
|
|
|
constexpr qint64 kCacheTtlSeconds = 24 * 60 * 60;
|
|
|
|
|
|
|
|
|
|
|
|
constexpr int kTransferTimeoutMs = 20000;
|
|
|
|
|
|
|
|
|
|
|
|
// Сколько последних сборок каждой мажорной версии показывать. Больше трёх
|
|
|
|
|
|
// смысла нет: старые патчи отличаются только исправлениями безопасности.
|
|
|
|
|
|
constexpr int kReleasesPerMajor = 3;
|
|
|
|
|
|
|
|
|
|
|
|
QString adoptiumOs()
|
|
|
|
|
|
{
|
|
|
|
|
|
#if defined(Q_OS_WIN)
|
|
|
|
|
|
return QStringLiteral("windows");
|
|
|
|
|
|
#elif defined(Q_OS_MACOS)
|
|
|
|
|
|
return QStringLiteral("mac");
|
|
|
|
|
|
#else
|
|
|
|
|
|
return QStringLiteral("linux");
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QString adoptiumArch()
|
|
|
|
|
|
{
|
|
|
|
|
|
const QString arch = QSysInfo::currentCpuArchitecture();
|
|
|
|
|
|
if (arch == QLatin1String("arm64"))
|
|
|
|
|
|
return QStringLiteral("aarch64");
|
|
|
|
|
|
if (arch == QLatin1String("x86_64"))
|
|
|
|
|
|
return QStringLiteral("x64");
|
|
|
|
|
|
if (arch == QLatin1String("i386"))
|
|
|
|
|
|
return QStringLiteral("x86");
|
|
|
|
|
|
if (arch == QLatin1String("arm"))
|
|
|
|
|
|
return QStringLiteral("arm");
|
|
|
|
|
|
return arch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ключ платформы в манифесте Mojang.
|
|
|
|
|
|
QString mojangPlatform()
|
|
|
|
|
|
{
|
|
|
|
|
|
const QString arch = QSysInfo::currentCpuArchitecture();
|
|
|
|
|
|
#if defined(Q_OS_WIN)
|
|
|
|
|
|
if (arch == QLatin1String("arm64"))
|
|
|
|
|
|
return QStringLiteral("windows-arm64");
|
|
|
|
|
|
return arch == QLatin1String("i386") ? QStringLiteral("windows-x86")
|
|
|
|
|
|
: QStringLiteral("windows-x64");
|
|
|
|
|
|
#elif defined(Q_OS_MACOS)
|
|
|
|
|
|
return arch == QLatin1String("arm64") ? QStringLiteral("mac-os-arm64")
|
|
|
|
|
|
: QStringLiteral("mac-os");
|
|
|
|
|
|
#else
|
|
|
|
|
|
return arch == QLatin1String("i386") ? QStringLiteral("linux-i386")
|
|
|
|
|
|
: QStringLiteral("linux");
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QNetworkRequest makeRequest(const QUrl &url)
|
|
|
|
|
|
{
|
|
|
|
|
|
QNetworkRequest request{url};
|
|
|
|
|
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
|
|
|
|
|
QNetworkRequest::NoLessSafeRedirectPolicy);
|
|
|
|
|
|
request.setTransferTimeout(kTransferTimeoutMs);
|
|
|
|
|
|
return request;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// «1.8.0_504-b01» никто не узнаёт, а «8u504» — общепринятое имя той же сборки.
|
|
|
|
|
|
QString displayVersion(const QJsonObject &versionData)
|
|
|
|
|
|
{
|
|
|
|
|
|
const int major = versionData.value(QStringLiteral("major")).toInt();
|
|
|
|
|
|
if (major == 8) {
|
|
|
|
|
|
return QStringLiteral("8u%1").arg(versionData.value(QStringLiteral("security")).toInt());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QString openjdk = versionData.value(QStringLiteral("openjdk_version")).toString();
|
|
|
|
|
|
if (openjdk.endsWith(QLatin1String("-LTS")))
|
|
|
|
|
|
openjdk.chop(4);
|
|
|
|
|
|
if (!openjdk.isEmpty())
|
|
|
|
|
|
return openjdk;
|
|
|
|
|
|
return versionData.value(QStringLiteral("semver")).toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QString archiveKind(const QString &fileName)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (fileName.endsWith(QLatin1String(".zip"), Qt::CaseInsensitive))
|
|
|
|
|
|
return QStringLiteral("zip");
|
|
|
|
|
|
if (fileName.endsWith(QLatin1String(".tar.gz"), Qt::CaseInsensitive))
|
|
|
|
|
|
return QStringLiteral("tar.gz");
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
JavaRuntimeService::JavaRuntimeService(QObject *parent)
|
|
|
|
|
|
: QObject(parent)
|
|
|
|
|
|
, m_network(new QNetworkAccessManager(this))
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QList<JavaRuntimeEntry> JavaRuntimeService::entries(JavaRuntimeKind kind) const
|
|
|
|
|
|
{
|
|
|
|
|
|
QList<JavaRuntimeEntry> result;
|
|
|
|
|
|
for (const JavaRuntimeEntry &entry : m_entries) {
|
|
|
|
|
|
if (entry.kind == kind)
|
|
|
|
|
|
result << entry;
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-31 00:59:14 +03:00
|
|
|
|
std::optional<JavaRuntimeEntry> JavaRuntimeService::bestFor(int major) const
|
|
|
|
|
|
{
|
|
|
|
|
|
if (major <= 0)
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
|
|
|
|
|
|
|
// Каталог уже отфильтрован по текущей ОС и архитектуре, поэтому здесь
|
|
|
|
|
|
// остаётся выбрать из заведомо запускаемого.
|
|
|
|
|
|
const auto rank = [](JavaRuntimeKind kind) {
|
|
|
|
|
|
switch (kind) {
|
|
|
|
|
|
case JavaRuntimeKind::Jdk:
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
case JavaRuntimeKind::Mojang:
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
case JavaRuntimeKind::Jre:
|
|
|
|
|
|
return 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
return 3;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const JavaRuntimeEntry *best = nullptr;
|
|
|
|
|
|
for (const JavaRuntimeEntry &entry : m_entries) {
|
|
|
|
|
|
if (!entry.isValid() || entry.major < major)
|
|
|
|
|
|
continue;
|
|
|
|
|
|
if (!best) {
|
|
|
|
|
|
best = &entry;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Чем ближе мажорная версия к нужной, тем меньше шансов на сюрприз:
|
|
|
|
|
|
// игра на Java 8 под Java 21 просто не стартует.
|
|
|
|
|
|
if (entry.major != best->major) {
|
|
|
|
|
|
if (entry.major < best->major)
|
|
|
|
|
|
best = &entry;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (rank(entry.kind) != rank(best->kind)) {
|
|
|
|
|
|
if (rank(entry.kind) < rank(best->kind))
|
|
|
|
|
|
best = &entry;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (entry.released.isValid() && best->released.isValid()
|
|
|
|
|
|
&& entry.released != best->released) {
|
|
|
|
|
|
if (entry.released > best->released)
|
|
|
|
|
|
best = &entry;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (entry.version > best->version)
|
|
|
|
|
|
best = &entry;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!best)
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
|
return *best;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 19:35:11 +03:00
|
|
|
|
std::optional<JavaRuntimeEntry> JavaRuntimeService::find(const QString &id) const
|
|
|
|
|
|
{
|
|
|
|
|
|
const auto it = m_indexById.constFind(id);
|
|
|
|
|
|
if (it == m_indexById.constEnd())
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
|
return m_entries.at(it.value());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void JavaRuntimeService::ensureLoaded(Callback callback, bool forceRefresh)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!m_cacheRead) {
|
|
|
|
|
|
m_cacheRead = true;
|
|
|
|
|
|
if (readCache())
|
|
|
|
|
|
emit catalogChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const bool fresh = hasData() && m_fetchedAt.isValid()
|
|
|
|
|
|
&& m_fetchedAt.secsTo(QDateTime::currentDateTimeUtc()) < kCacheTtlSeconds;
|
|
|
|
|
|
if (!forceRefresh && fresh) {
|
|
|
|
|
|
// Колбэк, который то синхронный, то нет, ломает вызывающий код —
|
|
|
|
|
|
// на попадании в кэш отвечаем через событийный цикл.
|
|
|
|
|
|
if (callback)
|
|
|
|
|
|
QTimer::singleShot(0, this, [callback = std::move(callback)] { callback(true, {}); });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (callback)
|
|
|
|
|
|
m_pending << std::move(callback);
|
|
|
|
|
|
if (isRefreshing())
|
|
|
|
|
|
return;
|
|
|
|
|
|
startRefresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void JavaRuntimeService::startRefresh()
|
|
|
|
|
|
{
|
|
|
|
|
|
m_incoming.clear();
|
|
|
|
|
|
m_warnings.clear();
|
|
|
|
|
|
m_ltsVersions.clear();
|
|
|
|
|
|
|
|
|
|
|
|
// Счётчик поднимаем на оба стартовых запроса разом: иначе ответ первого
|
|
|
|
|
|
// успел бы обнулить его и завершить обновление раньше времени.
|
|
|
|
|
|
m_outstanding = 2;
|
|
|
|
|
|
emit refreshingChanged();
|
|
|
|
|
|
|
|
|
|
|
|
requestAdoptiumReleases();
|
|
|
|
|
|
requestMojangRuntimes();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void JavaRuntimeService::requestAdoptiumReleases()
|
|
|
|
|
|
{
|
|
|
|
|
|
QNetworkReply *reply = m_network->get(
|
|
|
|
|
|
makeRequest(QUrl(kAdoptiumApi + QStringLiteral("/info/available_releases"))));
|
|
|
|
|
|
|
|
|
|
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply] {
|
|
|
|
|
|
reply->deleteLater();
|
|
|
|
|
|
if (reply->error() != QNetworkReply::NoError) {
|
2026-09-03 09:18:48 +03:00
|
|
|
|
requestFinished(Loc::text("java.error.versionsFetchFailed")
|
2026-08-27 19:35:11 +03:00
|
|
|
|
.arg(reply->errorString()));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonObject root = QJsonDocument::fromJson(reply->readAll()).object();
|
|
|
|
|
|
const QJsonArray lts = root.value(QStringLiteral("available_lts_releases")).toArray();
|
|
|
|
|
|
for (const QJsonValue &value : lts)
|
|
|
|
|
|
m_ltsVersions.insert(value.toInt());
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonArray available = root.value(QStringLiteral("available_releases")).toArray();
|
|
|
|
|
|
QList<int> majors;
|
|
|
|
|
|
for (const QJsonValue &value : available) {
|
|
|
|
|
|
const int major = value.toInt();
|
|
|
|
|
|
if (major > 0)
|
|
|
|
|
|
majors << major;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (majors.isEmpty()) {
|
2026-09-03 09:18:48 +03:00
|
|
|
|
requestFinished(Loc::text("java.error.versionsUnparsable"));
|
2026-08-27 19:35:11 +03:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Запросы добавляем до того, как отчитается этот: иначе счётчик уйдёт
|
|
|
|
|
|
// в ноль между двумя строчками.
|
|
|
|
|
|
m_outstanding += majors.size();
|
|
|
|
|
|
const QString arch = adoptiumArch();
|
|
|
|
|
|
for (const int major : std::as_const(majors))
|
|
|
|
|
|
requestFeatureRelease(major, arch, arch == QLatin1String("aarch64"));
|
|
|
|
|
|
|
|
|
|
|
|
requestFinished({});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void JavaRuntimeService::requestFeatureRelease(int major,
|
|
|
|
|
|
const QString &architecture,
|
|
|
|
|
|
bool allowFallback)
|
|
|
|
|
|
{
|
|
|
|
|
|
QUrl url(kAdoptiumApi + QStringLiteral("/assets/feature_releases/%1/ga").arg(major));
|
|
|
|
|
|
QUrlQuery query;
|
|
|
|
|
|
query.addQueryItem(QStringLiteral("architecture"), architecture);
|
|
|
|
|
|
query.addQueryItem(QStringLiteral("heap_size"), QStringLiteral("normal"));
|
|
|
|
|
|
query.addQueryItem(QStringLiteral("jvm_impl"), QStringLiteral("hotspot"));
|
|
|
|
|
|
query.addQueryItem(QStringLiteral("os"), adoptiumOs());
|
|
|
|
|
|
query.addQueryItem(QStringLiteral("page"), QStringLiteral("0"));
|
|
|
|
|
|
query.addQueryItem(QStringLiteral("page_size"), QString::number(kReleasesPerMajor));
|
|
|
|
|
|
query.addQueryItem(QStringLiteral("project"), QStringLiteral("jdk"));
|
|
|
|
|
|
query.addQueryItem(QStringLiteral("sort_method"), QStringLiteral("DEFAULT"));
|
|
|
|
|
|
query.addQueryItem(QStringLiteral("sort_order"), QStringLiteral("DESC"));
|
|
|
|
|
|
query.addQueryItem(QStringLiteral("vendor"), QStringLiteral("eclipse"));
|
|
|
|
|
|
url.setQuery(query);
|
|
|
|
|
|
|
|
|
|
|
|
QNetworkReply *reply = m_network->get(makeRequest(url));
|
|
|
|
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply, major, allowFallback] {
|
|
|
|
|
|
reply->deleteLater();
|
|
|
|
|
|
|
|
|
|
|
|
// 404 значит «под эту платформу такой сборки нет» — это не ошибка сети,
|
|
|
|
|
|
// а нормальный ответ: Java 8 и 16 под Apple Silicon просто не собирали.
|
|
|
|
|
|
const int status =
|
|
|
|
|
|
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
|
|
|
|
QList<JavaRuntimeEntry> parsed;
|
|
|
|
|
|
if (reply->error() == QNetworkReply::NoError)
|
|
|
|
|
|
parsed = parseAdoptium(reply->readAll(), m_ltsVersions);
|
|
|
|
|
|
|
|
|
|
|
|
if (parsed.isEmpty() && allowFallback && (status == 404 || status == 200)) {
|
|
|
|
|
|
// На Apple Silicon старые сборки берём под x64: Rosetta их запускает.
|
|
|
|
|
|
m_outstanding += 1;
|
|
|
|
|
|
requestFeatureRelease(major, QStringLiteral("x64"), false);
|
|
|
|
|
|
requestFinished({});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QString warning;
|
|
|
|
|
|
if (reply->error() != QNetworkReply::NoError && status != 404) {
|
2026-09-03 09:18:48 +03:00
|
|
|
|
warning = Loc::text("java.error.buildsFetchFailed")
|
2026-08-27 19:35:11 +03:00
|
|
|
|
.arg(major)
|
|
|
|
|
|
.arg(reply->errorString());
|
|
|
|
|
|
}
|
|
|
|
|
|
m_incoming += parsed;
|
|
|
|
|
|
requestFinished(warning);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void JavaRuntimeService::requestMojangRuntimes()
|
|
|
|
|
|
{
|
|
|
|
|
|
QNetworkReply *reply = m_network->get(makeRequest(QUrl(kMojangRuntimesUrl)));
|
|
|
|
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply] {
|
|
|
|
|
|
reply->deleteLater();
|
|
|
|
|
|
if (reply->error() != QNetworkReply::NoError) {
|
2026-09-03 09:18:48 +03:00
|
|
|
|
requestFinished(Loc::text("java.error.mojangRuntimesFailed")
|
2026-08-27 19:35:11 +03:00
|
|
|
|
.arg(reply->errorString()));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
m_incoming += parseMojang(reply->readAll());
|
|
|
|
|
|
requestFinished({});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void JavaRuntimeService::requestFinished(const QString &warning)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!warning.isEmpty())
|
|
|
|
|
|
m_warnings << warning;
|
|
|
|
|
|
if (--m_outstanding > 0)
|
|
|
|
|
|
return;
|
|
|
|
|
|
finishRefresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void JavaRuntimeService::finishRefresh()
|
|
|
|
|
|
{
|
|
|
|
|
|
m_outstanding = 0;
|
|
|
|
|
|
const QString warning = m_warnings.join(QStringLiteral("; "));
|
|
|
|
|
|
// Пустой разбор — испорченные ответы; хороший кэш ими не затираем.
|
|
|
|
|
|
const bool ok = !m_incoming.isEmpty();
|
|
|
|
|
|
if (ok) {
|
|
|
|
|
|
applyEntries(std::exchange(m_incoming, {}));
|
|
|
|
|
|
m_fetchedAt = QDateTime::currentDateTimeUtc();
|
|
|
|
|
|
writeCache();
|
|
|
|
|
|
emit catalogChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
m_incoming.clear();
|
|
|
|
|
|
m_warnings.clear();
|
|
|
|
|
|
emit refreshingChanged();
|
|
|
|
|
|
|
|
|
|
|
|
// Колбэк может снова позвать ensureLoaded — список забираем себе заранее.
|
|
|
|
|
|
const QList<Callback> pending = std::exchange(m_pending, {});
|
|
|
|
|
|
for (const Callback &callback : pending) {
|
|
|
|
|
|
// Сеть подвела, но старые данные есть — работаем на них.
|
|
|
|
|
|
if (!ok && hasData())
|
2026-09-03 09:18:48 +03:00
|
|
|
|
callback(true, warning.isEmpty() ? Loc::text("java.status.catalogFromCache") : warning);
|
2026-08-27 19:35:11 +03:00
|
|
|
|
else
|
|
|
|
|
|
callback(ok, warning);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QList<JavaRuntimeEntry> JavaRuntimeService::parseAdoptium(const QByteArray &payload,
|
|
|
|
|
|
const QSet<int> <sVersions)
|
|
|
|
|
|
{
|
|
|
|
|
|
QList<JavaRuntimeEntry> result;
|
|
|
|
|
|
const QJsonArray releases = QJsonDocument::fromJson(payload).array();
|
|
|
|
|
|
|
|
|
|
|
|
for (const QJsonValue &releaseValue : releases) {
|
|
|
|
|
|
const QJsonObject release = releaseValue.toObject();
|
|
|
|
|
|
const QJsonObject versionData = release.value(QStringLiteral("version_data")).toObject();
|
|
|
|
|
|
const int major = versionData.value(QStringLiteral("major")).toInt();
|
|
|
|
|
|
const QString releaseName = release.value(QStringLiteral("release_name")).toString();
|
|
|
|
|
|
if (major <= 0 || releaseName.isEmpty())
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
const QString version = displayVersion(versionData);
|
|
|
|
|
|
const QDateTime released = QDateTime::fromString(
|
|
|
|
|
|
release.value(QStringLiteral("timestamp")).toString(), Qt::ISODate);
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonArray binaries = release.value(QStringLiteral("binaries")).toArray();
|
|
|
|
|
|
for (const QJsonValue &binaryValue : binaries) {
|
|
|
|
|
|
const QJsonObject binary = binaryValue.toObject();
|
|
|
|
|
|
const auto kind = javaKindFromKey(binary.value(QStringLiteral("image_type")).toString());
|
|
|
|
|
|
if (!kind || *kind == JavaRuntimeKind::Mojang)
|
|
|
|
|
|
continue; // testimage, debugimage, sources и прочее нам не нужно
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonObject package = binary.value(QStringLiteral("package")).toObject();
|
|
|
|
|
|
const QUrl url(package.value(QStringLiteral("link")).toString());
|
|
|
|
|
|
const QString archive = archiveKind(package.value(QStringLiteral("name")).toString());
|
|
|
|
|
|
// Распаковать умеем только zip и tar.gz; https — потому что ссылку
|
|
|
|
|
|
// мы получили из сети и открываем её же.
|
|
|
|
|
|
if (archive.isEmpty() || url.scheme() != QLatin1String("https"))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
JavaRuntimeEntry entry;
|
|
|
|
|
|
entry.kind = *kind;
|
|
|
|
|
|
entry.major = major;
|
|
|
|
|
|
entry.version = version;
|
|
|
|
|
|
entry.released = released;
|
|
|
|
|
|
entry.url = url;
|
|
|
|
|
|
entry.checksum = package.value(QStringLiteral("checksum")).toString();
|
|
|
|
|
|
entry.size = static_cast<qint64>(package.value(QStringLiteral("size")).toDouble());
|
|
|
|
|
|
entry.archive = archive;
|
|
|
|
|
|
entry.architecture = binary.value(QStringLiteral("architecture")).toString();
|
|
|
|
|
|
entry.lts = ltsVersions.contains(major);
|
|
|
|
|
|
entry.id = JavaRuntimeStore::sanitizeId(QStringLiteral("temurin-%1-%2-%3")
|
|
|
|
|
|
.arg(javaKindKey(*kind),
|
|
|
|
|
|
version,
|
|
|
|
|
|
entry.architecture));
|
|
|
|
|
|
result << entry;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QList<JavaRuntimeEntry> JavaRuntimeService::parseMojang(const QByteArray &payload)
|
|
|
|
|
|
{
|
|
|
|
|
|
QList<JavaRuntimeEntry> result;
|
|
|
|
|
|
const QJsonObject root = QJsonDocument::fromJson(payload).object();
|
|
|
|
|
|
const QJsonObject platform = root.value(mojangPlatform()).toObject();
|
|
|
|
|
|
|
|
|
|
|
|
for (auto it = platform.constBegin(); it != platform.constEnd(); ++it) {
|
|
|
|
|
|
// minecraft-java-exe — это лаунчер игры, а не Java.
|
|
|
|
|
|
if (it.key() == QLatin1String("minecraft-java-exe"))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonArray variants = it.value().toArray();
|
|
|
|
|
|
if (variants.isEmpty())
|
|
|
|
|
|
continue; // компонента под эту платформу нет
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonObject variant = variants.first().toObject();
|
|
|
|
|
|
const QJsonObject manifest = variant.value(QStringLiteral("manifest")).toObject();
|
|
|
|
|
|
const QUrl url(manifest.value(QStringLiteral("url")).toString());
|
|
|
|
|
|
if (url.scheme() != QLatin1String("https"))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonObject versionObject = variant.value(QStringLiteral("version")).toObject();
|
|
|
|
|
|
const QString version = versionObject.value(QStringLiteral("name")).toString();
|
|
|
|
|
|
if (version.isEmpty())
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
JavaRuntimeEntry entry;
|
|
|
|
|
|
entry.kind = JavaRuntimeKind::Mojang;
|
|
|
|
|
|
entry.component = it.key();
|
|
|
|
|
|
entry.version = version;
|
|
|
|
|
|
// «8u51-cacert462b08» у jre-legacy, «21.0.7» у остальных.
|
|
|
|
|
|
entry.major = version.startsWith(QLatin1String("8u"))
|
|
|
|
|
|
? 8
|
|
|
|
|
|
: version.section(u'.', 0, 0).toInt();
|
|
|
|
|
|
entry.released = QDateTime::fromString(
|
|
|
|
|
|
versionObject.value(QStringLiteral("released")).toString(), Qt::ISODate);
|
|
|
|
|
|
entry.url = url;
|
|
|
|
|
|
entry.checksum = manifest.value(QStringLiteral("sha1")).toString();
|
|
|
|
|
|
entry.size = 0; // манифест знает только свой размер, не размер рантайма
|
|
|
|
|
|
entry.archive = QStringLiteral("mojang");
|
|
|
|
|
|
entry.architecture = QSysInfo::currentCpuArchitecture();
|
|
|
|
|
|
entry.lts = false;
|
|
|
|
|
|
entry.id = JavaRuntimeStore::sanitizeId(QStringLiteral("mojang-") + it.key());
|
|
|
|
|
|
|
|
|
|
|
|
if (entry.major > 0)
|
|
|
|
|
|
result << entry;
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void JavaRuntimeService::applyEntries(QList<JavaRuntimeEntry> list)
|
|
|
|
|
|
{
|
|
|
|
|
|
std::stable_sort(list.begin(), list.end(),
|
|
|
|
|
|
[](const JavaRuntimeEntry &a, const JavaRuntimeEntry &b) {
|
|
|
|
|
|
if (a.major != b.major)
|
|
|
|
|
|
return a.major > b.major;
|
|
|
|
|
|
if (a.released != b.released)
|
|
|
|
|
|
return a.released > b.released;
|
|
|
|
|
|
return a.version > b.version;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
m_entries = std::move(list);
|
|
|
|
|
|
m_indexById.clear();
|
|
|
|
|
|
m_indexById.reserve(m_entries.size());
|
|
|
|
|
|
for (int i = 0; i < m_entries.size(); ++i)
|
|
|
|
|
|
m_indexById.insert(m_entries.at(i).id, i);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool JavaRuntimeService::readCache()
|
|
|
|
|
|
{
|
|
|
|
|
|
QFile file(LauncherPaths::javaCatalogFile());
|
|
|
|
|
|
if (!file.open(QIODevice::ReadOnly))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonObject root = QJsonDocument::fromJson(file.readAll()).object();
|
|
|
|
|
|
// Кэш платформозависим: папку лаунчера могли принести с другой машины.
|
|
|
|
|
|
if (root.value(QStringLiteral("platform")).toString() != mojangPlatform())
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
QList<JavaRuntimeEntry> parsed;
|
|
|
|
|
|
const QJsonArray entries = root.value(QStringLiteral("entries")).toArray();
|
|
|
|
|
|
for (const QJsonValue &value : entries) {
|
|
|
|
|
|
const QJsonObject object = value.toObject();
|
|
|
|
|
|
const auto kind = javaKindFromKey(object.value(QStringLiteral("kind")).toString());
|
|
|
|
|
|
if (!kind)
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
JavaRuntimeEntry entry;
|
|
|
|
|
|
entry.id = object.value(QStringLiteral("id")).toString();
|
|
|
|
|
|
entry.kind = *kind;
|
|
|
|
|
|
entry.major = object.value(QStringLiteral("major")).toInt();
|
|
|
|
|
|
entry.version = object.value(QStringLiteral("version")).toString();
|
|
|
|
|
|
entry.component = object.value(QStringLiteral("component")).toString();
|
|
|
|
|
|
entry.released = QDateTime::fromString(
|
|
|
|
|
|
object.value(QStringLiteral("released")).toString(), Qt::ISODate);
|
|
|
|
|
|
entry.url = QUrl(object.value(QStringLiteral("url")).toString());
|
|
|
|
|
|
entry.checksum = object.value(QStringLiteral("checksum")).toString();
|
|
|
|
|
|
entry.size = static_cast<qint64>(object.value(QStringLiteral("size")).toDouble());
|
|
|
|
|
|
entry.archive = object.value(QStringLiteral("archive")).toString();
|
|
|
|
|
|
entry.architecture = object.value(QStringLiteral("architecture")).toString();
|
|
|
|
|
|
entry.lts = object.value(QStringLiteral("lts")).toBool();
|
|
|
|
|
|
|
|
|
|
|
|
if (entry.isValid() && entry.url.scheme() == QLatin1String("https"))
|
|
|
|
|
|
parsed << entry;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (parsed.isEmpty())
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
applyEntries(std::move(parsed));
|
|
|
|
|
|
m_fetchedAt = QDateTime::fromString(root.value(QStringLiteral("fetchedAt")).toString(),
|
|
|
|
|
|
Qt::ISODate);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void JavaRuntimeService::writeCache()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Папку кэша могли удалить уже после запуска лаунчера.
|
|
|
|
|
|
if (!LauncherPaths::ensureRootExists())
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
QJsonArray entries;
|
|
|
|
|
|
for (const JavaRuntimeEntry &entry : std::as_const(m_entries)) {
|
|
|
|
|
|
entries << QJsonObject{{QStringLiteral("id"), entry.id},
|
|
|
|
|
|
{QStringLiteral("kind"), javaKindKey(entry.kind)},
|
|
|
|
|
|
{QStringLiteral("major"), entry.major},
|
|
|
|
|
|
{QStringLiteral("version"), entry.version},
|
|
|
|
|
|
{QStringLiteral("component"), entry.component},
|
|
|
|
|
|
{QStringLiteral("released"), entry.released.toString(Qt::ISODate)},
|
|
|
|
|
|
{QStringLiteral("url"), entry.url.toString()},
|
|
|
|
|
|
{QStringLiteral("checksum"), entry.checksum},
|
|
|
|
|
|
{QStringLiteral("size"), static_cast<double>(entry.size)},
|
|
|
|
|
|
{QStringLiteral("archive"), entry.archive},
|
|
|
|
|
|
{QStringLiteral("architecture"), entry.architecture},
|
|
|
|
|
|
{QStringLiteral("lts"), entry.lts}};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonObject root{{QStringLiteral("fetchedAt"), m_fetchedAt.toString(Qt::ISODate)},
|
|
|
|
|
|
{QStringLiteral("platform"), mojangPlatform()},
|
|
|
|
|
|
{QStringLiteral("entries"), entries}};
|
|
|
|
|
|
|
|
|
|
|
|
QSaveFile file(LauncherPaths::javaCatalogFile());
|
|
|
|
|
|
if (!file.open(QIODevice::WriteOnly))
|
|
|
|
|
|
return;
|
|
|
|
|
|
const QByteArray payload = QJsonDocument(root).toJson(QJsonDocument::Compact);
|
|
|
|
|
|
if (file.write(payload) != payload.size()) {
|
|
|
|
|
|
file.cancelWriting();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
file.commit();
|
|
|
|
|
|
}
|