forge on linux fix

This commit is contained in:
2026-08-28 00:13:59 +03:00
parent af41017446
commit bbc8633257
5 changed files with 389 additions and 32 deletions
+168 -29
View File
@@ -1,10 +1,12 @@
#include "modloaderinstaller.h"
#include "gamelauncher.h"
#include "javalocator.h"
#include "launcherpaths.h"
#include "minecraftversion.h"
#include "modloaderversionservice.h"
#include "versioninstaller.h"
#include "zlibreference.h"
#include <QDir>
#include <QFileInfo>
@@ -15,9 +17,12 @@
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QProcess>
#include <QProcessEnvironment>
#include <QSaveFile>
#include <QTimer>
#include <algorithm>
namespace {
constexpr int kTransferTimeoutMs = 30000;
@@ -136,10 +141,15 @@ void ModLoaderInstaller::install(const QString &gameDir,
: warning);
return;
}
if (loader == ModLoader::Fabric || loader == ModLoader::Quilt)
beginFabricLike();
else
beginInstallerJar();
// Любой лоадер — надстройка над ванильной версией: и профиль Fabric с его
// inheritsFrom, и патч клиентского jar у Forge упрутся в недокачанную базу.
// Поэтому шлюз один на всех и стоит до выбора ветки.
ensureBaseVersion([this, loader] {
if (loader == ModLoader::Fabric || loader == ModLoader::Quilt)
beginFabricLike();
else
beginInstallerJar();
});
});
}
@@ -153,8 +163,12 @@ void ModLoaderInstaller::cancel()
m_reply->abort();
m_reply = nullptr;
}
if (m_process && m_process->state() != QProcess::NotRunning)
if (m_process && m_process->state() != QProcess::NotRunning) {
m_process->kill();
m_process->waitForFinished(2000);
}
if (m_installerStarted)
removeVersionsAddedByInstaller();
if (!m_awaitingVersionId.isEmpty()) {
m_versionInstaller->cancel();
return; // финал придёт из VersionInstaller::canceled
@@ -260,19 +274,26 @@ void ModLoaderInstaller::beginInstallerJar()
return;
}
const QUrl installerUrl = entry->installerUrl;
// Установщик патчит versions/<mc>/<mc>.jar — без ванильной версии он не отработает.
ensureBaseVersion([this, installerUrl] {
downloadInstaller(installerUrl, [this](const QString &path) { runInstaller(path); });
});
// Ванильную версию к этому моменту уже проверил ensureBaseVersion из install().
downloadInstaller(entry->installerUrl, [this](const QString &path) { runInstaller(path); });
}
void ModLoaderInstaller::ensureBaseVersion(std::function<void()> next)
{
if (QFile::exists(versionJsonPath(m_gameDir, m_gameVersion))
&& VersionLoader::installedVersions(m_gameDir).contains(m_gameVersion)) {
next();
return;
// Наличие versions/<mc>/<mc>.json ничего не значит: VersionInstaller кладёт его
// до загрузки файлов, так что оборванная установка оставляет ровно такой след.
// Проверяем тем же, чем пользуются приёмка VersionInstaller и запуск игры.
QString loadError;
const MinecraftVersion installed =
VersionLoader::load(m_gameDir, m_gameVersion, {}, &loadError);
if (installed.isValid()) {
LaunchOptions probe;
probe.gameDir = m_gameDir;
probe.versionId = m_gameVersion;
if (GameLauncher::missingFiles(probe, installed, 1).isEmpty()) {
next();
return;
}
}
setStage(tr("Установка Minecraft %1").arg(m_gameVersion));
@@ -413,6 +434,20 @@ void ModLoaderInstaller::runInstaller(const QString &installerPath)
}
m_versionsBefore = VersionLoader::installedVersions(m_gameDir);
m_installerStarted = true;
// Весь вывод установщика уходит в файл: в интерфейс попадает только хвост,
// а разбираться с «Processor failed» по двенадцати строкам невозможно.
m_installerLogPath = LauncherPaths::loaderDownloadDir() + u'/' + loaderKey(m_loader) + u'-'
+ QString(m_loaderVersion).replace(u'/', u'_')
+ QStringLiteral("-install.log");
m_installerLogFile = new QFile(m_installerLogPath, this);
if (!m_installerLogFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) {
delete m_installerLogFile;
m_installerLogFile = nullptr;
m_installerLogPath.clear();
}
setStage(tr("Установка %1").arg(loaderTitle(m_loader)));
m_currentFile = tr("работает установщик, это может занять несколько минут");
// Прогресс установщика неизвестен: панель покажет «…».
@@ -427,12 +462,25 @@ void ModLoaderInstaller::runInstaller(const QString &installerPath)
m_process->setWorkingDirectory(LauncherPaths::loaderDownloadDir());
m_process->setProcessChannelMode(QProcess::MergedChannels);
// Установщик сверяет sha1 всего, что собрал сам, а на системах с zlib-ng
// сжатие получается другое — без подмены он падает на любой версии игры.
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
QString zlibNote;
m_zlibNgUnpatched = !ZlibReference::applyTo(environment, &zlibNote);
if (!zlibNote.isEmpty()) {
appendInstallerLog(zlibNote);
emit log(zlibNote);
}
m_process->setProcessEnvironment(environment);
connect(m_process, &QProcess::readyReadStandardOutput, this, [this] {
const QStringList lines = QString::fromLocal8Bit(m_process->readAllStandardOutput())
.split(u'\n', Qt::SkipEmptyParts);
for (const QString &line : lines) {
m_installerLog << line.trimmed();
emit log(line.trimmed());
const QString text = line.trimmed();
m_installerLog << text;
appendInstallerLog(text);
emit log(text);
}
// Хвоста хватает, чтобы показать причину отказа; весь лог не нужен.
while (m_installerLog.size() > kLogTailLines)
@@ -471,31 +519,49 @@ void ModLoaderInstaller::handleInstallerFinished(int exitCode, bool crashed)
m_process->deleteLater();
m_process = nullptr;
}
closeInstallerLog();
if (!m_running || m_canceling)
return;
if (crashed || exitCode != 0) {
fail(tr("Установщик %1 завершился с ошибкой (код %2).\n%3")
.arg(loaderTitle(m_loader))
.arg(exitCode)
.arg(m_installerLog.join(u'\n')));
removeVersionsAddedByInstaller();
QString message = tr("Установщик %1 завершился с ошибкой (код %2).")
.arg(loaderTitle(m_loader))
.arg(exitCode);
const QString hint = installerFailureHint();
if (!hint.isEmpty())
message += u'\n' + hint;
if (!m_installerLog.isEmpty())
message += u'\n' + m_installerLog.join(u'\n');
if (!m_installerLogPath.isEmpty())
message += u'\n'
+ tr("Полный лог: %1").arg(QDir::toNativeSeparators(m_installerLogPath));
fail(message);
return;
}
// Какой профиль появился — выясняем разницей: id у Forge разные по эпохам
// (1.20.1-forge-47.4.0, 1.12.2-forge-14.23.5.2859, neoforge-21.1.66).
const QStringList after = VersionLoader::installedVersions(m_gameDir);
QStringList added;
for (const QString &id : after) {
if (!m_versionsBefore.contains(id))
added << id;
QStringList added = versionsAddedByInstaller();
if (added.isEmpty()) {
// Разницы нет, если профиль лежал в versions ещё до запуска: переустановка
// поверх или остаток прошлой попытки. Тогда ищем его среди наследников
// выбранной ванильной версии.
const QStringList dependents = VersionLoader::dependentsOf(m_gameDir, m_gameVersion);
for (const QString &id : dependents) {
if (id.contains(m_loaderVersion))
added << id;
}
}
if (added.isEmpty()) {
fail(tr("Установщик %1 отработал, но новая версия в %2 не появилась.\n%3")
.arg(loaderTitle(m_loader),
QDir::toNativeSeparators(m_gameDir + QStringLiteral("/versions")),
m_installerLog.join(u'\n')));
QString message = tr("Установщик %1 отработал, но новая версия в %2 не появилась.")
.arg(loaderTitle(m_loader),
QDir::toNativeSeparators(m_gameDir
+ QStringLiteral("/versions")));
if (!m_installerLog.isEmpty())
message += u'\n' + m_installerLog.join(u'\n');
fail(message);
return;
}
if (added.size() > 1) {
@@ -507,9 +573,78 @@ void ModLoaderInstaller::handleInstallerFinished(int exitCode, bool crashed)
added = {match != added.cend() ? *match : added.first()};
}
// Профиль на месте — дальше его судьба в руках VersionInstaller, мусором он
// больше не считается.
m_installerStarted = false;
m_versionsBefore.clear();
handOffToVersionInstaller(added.first());
}
QStringList ModLoaderInstaller::versionsAddedByInstaller() const
{
if (!m_installerStarted)
return {};
QStringList added;
const QStringList after = VersionLoader::installedVersions(m_gameDir);
for (const QString &id : after) {
if (!m_versionsBefore.contains(id))
added << id;
}
return added;
}
void ModLoaderInstaller::removeVersionsAddedByInstaller()
{
// Forge пишет versions/<id>/<id>.json до запуска процессоров, поэтому после
// неудачи остаётся профиль-призрак: в списке версий он выглядит установленным,
// а следующей попытке мешает — разница по versions у неё выйдет пустой.
const QStringList added = versionsAddedByInstaller();
for (const QString &id : added) {
QString error;
if (!VersionLoader::remove(m_gameDir, id, &error))
emit log(tr("Не удалось убрать незавершённый профиль «%1»: %2").arg(id, error));
}
m_versionsBefore.clear();
m_installerStarted = false;
}
QString ModLoaderInstaller::installerFailureHint() const
{
if (!m_zlibNgUnpatched)
return {};
const bool checksumFailure =
std::any_of(m_installerLog.cbegin(), m_installerLog.cend(), [](const QString &line) {
return line.contains(QLatin1String("invalid outputs"))
|| line.contains(QLatin1String("Processor failed"));
});
if (!checksumFailure)
return {};
return tr("Системный libz.so.1 — это zlib-ng: он сжимает иначе, чем обычный zlib, "
"поэтому sha1 собранных установщиком файлов не совпадает с эталоном. "
"Пересоберите лаунчер, чтобы рядом с ним появился эталонный libz.so.1, "
"либо замените zlib-ng в системе: sudo pacman -S zlib lib32-zlib");
}
void ModLoaderInstaller::appendInstallerLog(const QString &line)
{
if (!m_installerLogFile)
return;
m_installerLogFile->write(line.toUtf8());
m_installerLogFile->write("\n");
}
void ModLoaderInstaller::closeInstallerLog()
{
if (!m_installerLogFile)
return;
m_installerLogFile->close();
delete m_installerLogFile;
m_installerLogFile = nullptr;
}
// ── Общий хвост ────────────────────────────────────────────────────────────
void ModLoaderInstaller::handOffToVersionInstaller(const QString &versionId)
@@ -537,6 +672,10 @@ void ModLoaderInstaller::reset()
m_bytesDone = 0;
m_bytesTotal = 0;
m_versionsBefore.clear();
m_installerStarted = false;
m_zlibNgUnpatched = false;
closeInstallerLog();
m_installerLogPath.clear();
if (m_process) {
m_process->deleteLater();
m_process = nullptr;