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
+72 -2
View File
@@ -1,6 +1,7 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.20)
project(Minecraft_launcher VERSION 0.1 LANGUAGES CXX) # C нужен для эталонного zlib: см. блок про установщики Forge ниже.
project(Minecraft_launcher VERSION 0.1 LANGUAGES CXX C)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -24,6 +25,8 @@ qt_add_qml_module(appMinecraft_launcher
minecraftversion.cpp minecraftversion.cpp
javalocator.h javalocator.h
javalocator.cpp javalocator.cpp
zlibreference.h
zlibreference.cpp
javaruntime.h javaruntime.h
javaruntime.cpp javaruntime.cpp
javaruntimeservice.h javaruntimeservice.h
@@ -75,6 +78,73 @@ target_link_libraries(appMinecraft_launcher
) )
include(GNUInstallDirs) include(GNUInstallDirs)
# ── Эталонный zlib для установщиков Forge и NeoForge ────────────────────────
#
# Установщик Forge сверяет sha1 каждого jar, который сам же и собирает, с
# эталоном из install_profile.json. Эталон посчитан на обычном zlib, а
# дистрибутивы вроде CachyOS и Fedora подставляют вместо него zlib-ng: сжатие
# корректное, но побайтово другое, поэтому установка падает на любой версии
# игры («Processor failed, invalid outputs»). Сменить Java не выйдет — сборки
# OpenJDK под Linux берут libz.so.1 из системы.
#
# Поэтому собираем рядом обычный zlib и подсовываем его через LD_PRELOAD
# только процессу установщика. Сам лаунчер с ним не линкуется, системный
# zlib-ng остаётся на месте.
if(UNIX AND NOT APPLE)
include(FetchContent)
# SOURCE_SUBDIR указывает на несуществующий каталог: нужны только исходники.
# add_subdirectory от zlib притащил бы свои install-правила и цели example
# и minigzip.
FetchContent_Declare(zlib_reference
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.3.1
GIT_SHALLOW TRUE
SOURCE_SUBDIR do-not-configure
)
FetchContent_MakeAvailable(zlib_reference)
set(LAUNCHER_ZLIB_DIR "${CMAKE_INSTALL_LIBDIR}/galeonLauncher")
add_library(launcher_zlib_reference SHARED
${zlib_reference_SOURCE_DIR}/adler32.c
${zlib_reference_SOURCE_DIR}/compress.c
${zlib_reference_SOURCE_DIR}/crc32.c
${zlib_reference_SOURCE_DIR}/deflate.c
${zlib_reference_SOURCE_DIR}/gzclose.c
${zlib_reference_SOURCE_DIR}/gzlib.c
${zlib_reference_SOURCE_DIR}/gzread.c
${zlib_reference_SOURCE_DIR}/gzwrite.c
${zlib_reference_SOURCE_DIR}/infback.c
${zlib_reference_SOURCE_DIR}/inffast.c
${zlib_reference_SOURCE_DIR}/inflate.c
${zlib_reference_SOURCE_DIR}/inftrees.c
${zlib_reference_SOURCE_DIR}/trees.c
${zlib_reference_SOURCE_DIR}/uncompr.c
${zlib_reference_SOURCE_DIR}/zutil.c
)
target_include_directories(launcher_zlib_reference PRIVATE ${zlib_reference_SOURCE_DIR})
# То же, что выставляет собственный configure zlib: без этого gz*-часть не
# видит unistd.h и не собирается.
target_compile_definitions(launcher_zlib_reference PRIVATE
HAVE_UNISTD_H=1 HAVE_STDARG_H=1 _LARGEFILE64_SOURCE=1)
# SONAME libz.so.1 обязателен: с ним ld.so считает предзагруженный объект
# закрывающим зависимость, и системный zlib-ng в процессе java не появится.
set_target_properties(launcher_zlib_reference PROPERTIES
OUTPUT_NAME z
VERSION 1.3.1
SOVERSION 1
C_VISIBILITY_PRESET default
LIBRARY_OUTPUT_DIRECTORY $<TARGET_FILE_DIR:appMinecraft_launcher>/zlib
)
add_dependencies(appMinecraft_launcher launcher_zlib_reference)
target_compile_definitions(appMinecraft_launcher PRIVATE
LAUNCHER_ZLIB_INSTALL_DIR="${CMAKE_INSTALL_FULL_LIBDIR}/galeonLauncher"
)
install(TARGETS launcher_zlib_reference LIBRARY DESTINATION ${LAUNCHER_ZLIB_DIR})
endif()
install(TARGETS appMinecraft_launcher install(TARGETS appMinecraft_launcher
BUNDLE DESTINATION . BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+159 -20
View File
@@ -1,10 +1,12 @@
#include "modloaderinstaller.h" #include "modloaderinstaller.h"
#include "gamelauncher.h"
#include "javalocator.h" #include "javalocator.h"
#include "launcherpaths.h" #include "launcherpaths.h"
#include "minecraftversion.h" #include "minecraftversion.h"
#include "modloaderversionservice.h" #include "modloaderversionservice.h"
#include "versioninstaller.h" #include "versioninstaller.h"
#include "zlibreference.h"
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
@@ -15,9 +17,12 @@
#include <QNetworkReply> #include <QNetworkReply>
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QProcess> #include <QProcess>
#include <QProcessEnvironment>
#include <QSaveFile> #include <QSaveFile>
#include <QTimer> #include <QTimer>
#include <algorithm>
namespace { namespace {
constexpr int kTransferTimeoutMs = 30000; constexpr int kTransferTimeoutMs = 30000;
@@ -136,11 +141,16 @@ void ModLoaderInstaller::install(const QString &gameDir,
: warning); : warning);
return; return;
} }
// Любой лоадер — надстройка над ванильной версией: и профиль Fabric с его
// inheritsFrom, и патч клиентского jar у Forge упрутся в недокачанную базу.
// Поэтому шлюз один на всех и стоит до выбора ветки.
ensureBaseVersion([this, loader] {
if (loader == ModLoader::Fabric || loader == ModLoader::Quilt) if (loader == ModLoader::Fabric || loader == ModLoader::Quilt)
beginFabricLike(); beginFabricLike();
else else
beginInstallerJar(); beginInstallerJar();
}); });
});
} }
void ModLoaderInstaller::cancel() void ModLoaderInstaller::cancel()
@@ -153,8 +163,12 @@ void ModLoaderInstaller::cancel()
m_reply->abort(); m_reply->abort();
m_reply = nullptr; m_reply = nullptr;
} }
if (m_process && m_process->state() != QProcess::NotRunning) if (m_process && m_process->state() != QProcess::NotRunning) {
m_process->kill(); m_process->kill();
m_process->waitForFinished(2000);
}
if (m_installerStarted)
removeVersionsAddedByInstaller();
if (!m_awaitingVersionId.isEmpty()) { if (!m_awaitingVersionId.isEmpty()) {
m_versionInstaller->cancel(); m_versionInstaller->cancel();
return; // финал придёт из VersionInstaller::canceled return; // финал придёт из VersionInstaller::canceled
@@ -260,20 +274,27 @@ void ModLoaderInstaller::beginInstallerJar()
return; return;
} }
const QUrl installerUrl = entry->installerUrl; // Ванильную версию к этому моменту уже проверил ensureBaseVersion из install().
// Установщик патчит versions/<mc>/<mc>.jar — без ванильной версии он не отработает. downloadInstaller(entry->installerUrl, [this](const QString &path) { runInstaller(path); });
ensureBaseVersion([this, installerUrl] {
downloadInstaller(installerUrl, [this](const QString &path) { runInstaller(path); });
});
} }
void ModLoaderInstaller::ensureBaseVersion(std::function<void()> next) void ModLoaderInstaller::ensureBaseVersion(std::function<void()> next)
{ {
if (QFile::exists(versionJsonPath(m_gameDir, m_gameVersion)) // Наличие versions/<mc>/<mc>.json ничего не значит: VersionInstaller кладёт его
&& VersionLoader::installedVersions(m_gameDir).contains(m_gameVersion)) { // до загрузки файлов, так что оборванная установка оставляет ровно такой след.
// Проверяем тем же, чем пользуются приёмка 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(); next();
return; return;
} }
}
setStage(tr("Установка Minecraft %1").arg(m_gameVersion)); setStage(tr("Установка Minecraft %1").arg(m_gameVersion));
m_currentFile = m_gameVersion; m_currentFile = m_gameVersion;
@@ -413,6 +434,20 @@ void ModLoaderInstaller::runInstaller(const QString &installerPath)
} }
m_versionsBefore = VersionLoader::installedVersions(m_gameDir); 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))); setStage(tr("Установка %1").arg(loaderTitle(m_loader)));
m_currentFile = tr("работает установщик, это может занять несколько минут"); m_currentFile = tr("работает установщик, это может занять несколько минут");
// Прогресс установщика неизвестен: панель покажет «…». // Прогресс установщика неизвестен: панель покажет «…».
@@ -427,12 +462,25 @@ void ModLoaderInstaller::runInstaller(const QString &installerPath)
m_process->setWorkingDirectory(LauncherPaths::loaderDownloadDir()); m_process->setWorkingDirectory(LauncherPaths::loaderDownloadDir());
m_process->setProcessChannelMode(QProcess::MergedChannels); 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] { connect(m_process, &QProcess::readyReadStandardOutput, this, [this] {
const QStringList lines = QString::fromLocal8Bit(m_process->readAllStandardOutput()) const QStringList lines = QString::fromLocal8Bit(m_process->readAllStandardOutput())
.split(u'\n', Qt::SkipEmptyParts); .split(u'\n', Qt::SkipEmptyParts);
for (const QString &line : lines) { for (const QString &line : lines) {
m_installerLog << line.trimmed(); const QString text = line.trimmed();
emit log(line.trimmed()); m_installerLog << text;
appendInstallerLog(text);
emit log(text);
} }
// Хвоста хватает, чтобы показать причину отказа; весь лог не нужен. // Хвоста хватает, чтобы показать причину отказа; весь лог не нужен.
while (m_installerLog.size() > kLogTailLines) while (m_installerLog.size() > kLogTailLines)
@@ -471,31 +519,49 @@ void ModLoaderInstaller::handleInstallerFinished(int exitCode, bool crashed)
m_process->deleteLater(); m_process->deleteLater();
m_process = nullptr; m_process = nullptr;
} }
closeInstallerLog();
if (!m_running || m_canceling) if (!m_running || m_canceling)
return; return;
if (crashed || exitCode != 0) { if (crashed || exitCode != 0) {
fail(tr("Установщик %1 завершился с ошибкой (код %2).\n%3") removeVersionsAddedByInstaller();
QString message = tr("Установщик %1 завершился с ошибкой (код %2).")
.arg(loaderTitle(m_loader)) .arg(loaderTitle(m_loader))
.arg(exitCode) .arg(exitCode);
.arg(m_installerLog.join(u'\n'))); 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; return;
} }
// Какой профиль появился — выясняем разницей: id у Forge разные по эпохам // Какой профиль появился — выясняем разницей: id у Forge разные по эпохам
// (1.20.1-forge-47.4.0, 1.12.2-forge-14.23.5.2859, neoforge-21.1.66). // (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 = versionsAddedByInstaller();
QStringList added; if (added.isEmpty()) {
for (const QString &id : after) { // Разницы нет, если профиль лежал в versions ещё до запуска: переустановка
if (!m_versionsBefore.contains(id)) // поверх или остаток прошлой попытки. Тогда ищем его среди наследников
// выбранной ванильной версии.
const QStringList dependents = VersionLoader::dependentsOf(m_gameDir, m_gameVersion);
for (const QString &id : dependents) {
if (id.contains(m_loaderVersion))
added << id; added << id;
} }
}
if (added.isEmpty()) { if (added.isEmpty()) {
fail(tr("Установщик %1 отработал, но новая версия в %2 не появилась.\n%3") QString message = tr("Установщик %1 отработал, но новая версия в %2 не появилась.")
.arg(loaderTitle(m_loader), .arg(loaderTitle(m_loader),
QDir::toNativeSeparators(m_gameDir + QStringLiteral("/versions")), QDir::toNativeSeparators(m_gameDir
m_installerLog.join(u'\n'))); + QStringLiteral("/versions")));
if (!m_installerLog.isEmpty())
message += u'\n' + m_installerLog.join(u'\n');
fail(message);
return; return;
} }
if (added.size() > 1) { if (added.size() > 1) {
@@ -507,9 +573,78 @@ void ModLoaderInstaller::handleInstallerFinished(int exitCode, bool crashed)
added = {match != added.cend() ? *match : added.first()}; added = {match != added.cend() ? *match : added.first()};
} }
// Профиль на месте — дальше его судьба в руках VersionInstaller, мусором он
// больше не считается.
m_installerStarted = false;
m_versionsBefore.clear();
handOffToVersionInstaller(added.first()); 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) void ModLoaderInstaller::handOffToVersionInstaller(const QString &versionId)
@@ -537,6 +672,10 @@ void ModLoaderInstaller::reset()
m_bytesDone = 0; m_bytesDone = 0;
m_bytesTotal = 0; m_bytesTotal = 0;
m_versionsBefore.clear(); m_versionsBefore.clear();
m_installerStarted = false;
m_zlibNgUnpatched = false;
closeInstallerLog();
m_installerLogPath.clear();
if (m_process) { if (m_process) {
m_process->deleteLater(); m_process->deleteLater();
m_process = nullptr; m_process = nullptr;
+19 -1
View File
@@ -6,6 +6,7 @@
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
class QFile;
class QNetworkAccessManager; class QNetworkAccessManager;
class QNetworkReply; class QNetworkReply;
class QProcess; class QProcess;
@@ -20,7 +21,11 @@ class VersionInstaller;
// //
// * Forge и NeoForge так не умеют: их установка — это патч клиентского jar, // * Forge и NeoForge так не умеют: их установка — это патч клиентского jar,
// поэтому запускаем официальный installer.jar найденной Java в headless-режиме // поэтому запускаем официальный installer.jar найденной Java в headless-режиме
// и смотрим, какой профиль появился в versions. // и смотрим, какой профиль появился в versions. Процессу установщика при этом
// подсовывается эталонный zlib — см. ZlibReference.
//
// Оба пути начинаются одинаково: пока ванильная версия не скачана целиком,
// ставить лоадер некуда, поэтому ensureBaseVersion стоит перед обеими ветками.
// //
// Набор геттеров повторяет VersionInstaller: панель загрузки в интерфейсе // Набор геттеров повторяет VersionInstaller: панель загрузки в интерфейсе
// читает их одинаково, независимо от того, кто сейчас работает. // читает их одинаково, независимо от того, кто сейчас работает.
@@ -69,6 +74,11 @@ private:
bool writeLauncherProfilesStub(); bool writeLauncherProfilesStub();
void runInstaller(const QString &installerPath); void runInstaller(const QString &installerPath);
void handleInstallerFinished(int exitCode, bool crashed); void handleInstallerFinished(int exitCode, bool crashed);
QStringList versionsAddedByInstaller() const;
void removeVersionsAddedByInstaller();
QString installerFailureHint() const;
void appendInstallerLog(const QString &line);
void closeInstallerLog();
void handOffToVersionInstaller(const QString &versionId); void handOffToVersionInstaller(const QString &versionId);
void setStage(const QString &stage); void setStage(const QString &stage);
@@ -97,6 +107,14 @@ private:
// разницей, а не угадыванием строки — id у Forge разные по эпохам. // разницей, а не угадыванием строки — id у Forge разные по эпохам.
QStringList m_versionsBefore; QStringList m_versionsBefore;
QStringList m_installerLog; QStringList m_installerLog;
// Установщик успел отработать — значит, за появившиеся в versions папки
// отвечаем мы и при неудаче их надо убрать.
bool m_installerStarted = false;
// В системе zlib-ng, а подменить его было нечем: почти наверняка причина
// расхождения sha1, о которой стоит сказать прямым текстом.
bool m_zlibNgUnpatched = false;
QFile *m_installerLogFile = nullptr;
QString m_installerLogPath;
// Ждём, пока VersionInstaller докачает то, что установщик не положил. // Ждём, пока VersionInstaller докачает то, что установщик не положил.
QString m_awaitingVersionId; QString m_awaitingVersionId;
+101
View File
@@ -0,0 +1,101 @@
#include "zlibreference.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QProcessEnvironment>
#include <QStringList>
#include <utility>
#if defined(Q_OS_LINUX)
#include <dlfcn.h>
#endif
namespace {
QString translate(const char *text)
{
return QCoreApplication::translate("ZlibReference", text);
}
} // namespace
bool ZlibReference::systemIsZlibNg()
{
#if defined(Q_OS_LINUX)
// Спрашиваем ровно ту библиотеку, которую подберёт ld.so процессу java.
// zlib-ng в режиме совместимости честно пишет о себе в zlibVersion().
static const bool result = [] {
void *handle = dlopen("libz.so.1", RTLD_LAZY);
if (!handle)
return false;
using VersionFn = const char *(*)();
const auto zlibVersion = reinterpret_cast<VersionFn>(dlsym(handle, "zlibVersion"));
const QString version = zlibVersion ? QString::fromLatin1(zlibVersion()) : QString();
dlclose(handle);
return version.contains(QLatin1String("zlib-ng"), Qt::CaseInsensitive);
}();
return result;
#else
return false;
#endif
}
QString ZlibReference::bundledPath()
{
#if defined(Q_OS_LINUX)
static const QString path = [] {
QStringList candidates;
const QString appDir = QCoreApplication::applicationDirPath();
if (!appDir.isEmpty()) {
candidates << appDir + QStringLiteral("/zlib/libz.so.1") // дерево сборки
<< appDir + QStringLiteral("/../lib/galeonLauncher/libz.so.1");
}
#ifdef LAUNCHER_ZLIB_INSTALL_DIR
candidates << QStringLiteral(LAUNCHER_ZLIB_INSTALL_DIR) + QStringLiteral("/libz.so.1");
#endif
for (const QString &candidate : std::as_const(candidates)) {
const QFileInfo info(candidate);
if (info.exists() && info.isFile())
return info.canonicalFilePath();
}
return QString();
}();
return path;
#else
return {};
#endif
}
bool ZlibReference::applyTo(QProcessEnvironment &env, QString *note)
{
#if defined(Q_OS_LINUX)
if (!systemIsZlibNg()) {
if (note)
note->clear();
return true;
}
const QString path = bundledPath();
if (path.isEmpty()) {
if (note)
*note = translate("В системе zlib-ng, а эталонный libz.so.1 рядом с лаунчером "
"не найден — установщик почти наверняка не сойдётся по sha1.");
return false;
}
// Не затираем чужой LD_PRELOAD: наш идёт первым, остальное остаётся.
const QString existing = env.value(QStringLiteral("LD_PRELOAD"));
env.insert(QStringLiteral("LD_PRELOAD"), existing.isEmpty() ? path : path + u':' + existing);
if (note)
*note = translate("Системный zlib-ng подменён эталонным zlib: %1")
.arg(QDir::toNativeSeparators(path));
return true;
#else
Q_UNUSED(env);
if (note)
note->clear();
return true;
#endif
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <QString>
class QProcessEnvironment;
// Установщики Forge и NeoForge сверяют sha1 каждого jar, который сами же и
// собирают, с эталоном из install_profile.json. Эталон посчитан на обычном
// zlib, а CachyOS, Fedora и другие подставляют вместо него zlib-ng: сжатие
// корректное, но побайтово другое, поэтому установка падает на любой версии
// игры сообщением «Processor failed, invalid outputs».
//
// Сменить Java не поможет — сборки OpenJDK под Linux берут libz.so.1 из
// системы. Поэтому рядом с лаунчером лежит собранный обычный zlib, и процессу
// установщика мы подсовываем его через LD_PRELOAD. Системный zlib-ng при этом
// остаётся на месте: подмена живёт ровно один процесс.
namespace ZlibReference {
// libz.so.1, который достанется процессу java, — это zlib-ng?
bool systemIsZlibNg();
// Путь к собранному рядом эталонному libz.so.1; пусто, если его нет.
QString bundledPath();
// Дописывает LD_PRELOAD, если подмена нужна. true — окружение готово (в том
// числе когда подменять нечего). note — строка для лога и текста ошибки.
bool applyTo(QProcessEnvironment &env, QString *note = nullptr);
} // namespace ZlibReference