214 lines
8.3 KiB
C++
214 lines
8.3 KiB
C++
#include "javalocator.h"
|
||
|
||
#include "javaruntime.h"
|
||
|
||
#include <QDir>
|
||
#include <QDirIterator>
|
||
#include <QFileInfo>
|
||
#include <QHash>
|
||
#include <QProcess>
|
||
#include <QProcessEnvironment>
|
||
#include <QObject>
|
||
#include <QRegularExpression>
|
||
#include <QStandardPaths>
|
||
#include <utility>
|
||
|
||
namespace {
|
||
|
||
#if defined(Q_OS_WIN)
|
||
constexpr auto kExecutable = "java.exe";
|
||
constexpr auto kWindowless = "javaw.exe";
|
||
#else
|
||
constexpr auto kExecutable = "java";
|
||
constexpr auto kWindowless = "java";
|
||
#endif
|
||
|
||
// Корни, в которых Windows/Linux/macOS обычно держат JDK и JRE.
|
||
QStringList installationRoots()
|
||
{
|
||
QStringList roots;
|
||
#if defined(Q_OS_WIN)
|
||
const QStringList programFiles = {qEnvironmentVariable("ProgramFiles"),
|
||
qEnvironmentVariable("ProgramFiles(x86)"),
|
||
qEnvironmentVariable("ProgramW6432")};
|
||
const QStringList vendors = {QStringLiteral("Java"),
|
||
QStringLiteral("Eclipse Adoptium"),
|
||
QStringLiteral("Eclipse Foundation"),
|
||
QStringLiteral("AdoptOpenJDK"),
|
||
QStringLiteral("Amazon Corretto"),
|
||
QStringLiteral("Zulu"),
|
||
QStringLiteral("BellSoft"),
|
||
QStringLiteral("Microsoft"),
|
||
QStringLiteral("Semeru"),
|
||
QStringLiteral("Android/Android Studio/jbr")};
|
||
for (const QString &base : programFiles) {
|
||
if (base.isEmpty())
|
||
continue;
|
||
for (const QString &vendor : vendors)
|
||
roots << base + u'/' + vendor;
|
||
}
|
||
#elif defined(Q_OS_MACOS)
|
||
roots << QStringLiteral("/Library/Java/JavaVirtualMachines")
|
||
<< QDir::homePath() + QStringLiteral("/Library/Java/JavaVirtualMachines");
|
||
#else
|
||
roots << QStringLiteral("/usr/lib/jvm") << QStringLiteral("/usr/java")
|
||
<< QStringLiteral("/opt/java") << QStringLiteral("/opt/jdk");
|
||
#endif
|
||
return roots;
|
||
}
|
||
|
||
// Ищем именно java.exe: у javaw.exe нет вывода, а версию мы определяем запуском.
|
||
void appendIfExecutable(QStringList &target, const QString &binDir)
|
||
{
|
||
const QFileInfo info(binDir + u'/' + QString::fromLatin1(kExecutable));
|
||
if (!info.isFile() || !info.isExecutable())
|
||
return;
|
||
const QString path = info.absoluteFilePath();
|
||
if (!target.contains(path))
|
||
target << path;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
QStringList JavaLocator::findAll(const QString &gameDir)
|
||
{
|
||
QStringList result;
|
||
|
||
// 0. Сборки, скачанные самим лаунчером. Идут первыми: пользователь выбирал
|
||
// их осознанно, в отличие от того, что нашлось в системе.
|
||
const QList<InstalledJavaRuntime> managed = JavaRuntimeStore::installed();
|
||
for (const InstalledJavaRuntime &entry : managed) {
|
||
if (!result.contains(entry.javaPath))
|
||
result << entry.javaPath;
|
||
}
|
||
|
||
// 1. Рантайм, скачанный самим Minecraft: .minecraft/runtime/<component>/<os>/<component>/bin
|
||
const QString runtime = gameDir + QStringLiteral("/runtime");
|
||
if (QDir(runtime).exists()) {
|
||
QDirIterator it(runtime, QStringList{QString::fromLatin1(kExecutable)},
|
||
QDir::Files | QDir::Executable, QDirIterator::Subdirectories);
|
||
while (it.hasNext()) {
|
||
it.next();
|
||
appendIfExecutable(result, it.fileInfo().absolutePath());
|
||
}
|
||
}
|
||
|
||
// 2. JAVA_HOME
|
||
const QString javaHome = qEnvironmentVariable("JAVA_HOME");
|
||
if (!javaHome.isEmpty())
|
||
appendIfExecutable(result, javaHome + QStringLiteral("/bin"));
|
||
|
||
// 3. PATH
|
||
const QString onPath = QStandardPaths::findExecutable(QString::fromLatin1(kExecutable));
|
||
if (!onPath.isEmpty())
|
||
appendIfExecutable(result, QFileInfo(onPath).absolutePath());
|
||
|
||
// 4. Типовые каталоги установки
|
||
const QStringList roots = installationRoots();
|
||
for (const QString &root : roots) {
|
||
const QDir dir(root);
|
||
if (!dir.exists())
|
||
continue;
|
||
const QStringList entries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
|
||
for (const QString &entry : entries) {
|
||
const QString home = dir.absoluteFilePath(entry);
|
||
appendIfExecutable(result, home + QStringLiteral("/bin"));
|
||
appendIfExecutable(result, home + QStringLiteral("/Contents/Home/bin")); // macOS
|
||
appendIfExecutable(result, home + QStringLiteral("/jre/bin"));
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
int JavaLocator::majorVersion(const QString &javaPath)
|
||
{
|
||
static QHash<QString, int> cache;
|
||
const auto cached = cache.constFind(javaPath);
|
||
if (cached != cache.constEnd())
|
||
return cached.value();
|
||
|
||
int major = 0;
|
||
QProcess process;
|
||
process.start(javaPath, {QStringLiteral("-version")});
|
||
if (process.waitForFinished(5000)) {
|
||
// java печатает версию в stderr: openjdk version "17.0.2" 2022-01-18
|
||
const QString output = QString::fromLocal8Bit(process.readAllStandardError())
|
||
+ QString::fromLocal8Bit(process.readAllStandardOutput());
|
||
static const QRegularExpression re(QStringLiteral("version\\s+\"(\\d+)(?:\\.(\\d+))?"));
|
||
const QRegularExpressionMatch match = re.match(output);
|
||
if (match.hasMatch()) {
|
||
const int first = match.captured(1).toInt();
|
||
// До Java 9 версия выглядела как 1.8.0_301 — мажорная тут вторая цифра.
|
||
major = (first == 1) ? match.captured(2).toInt() : first;
|
||
}
|
||
}
|
||
|
||
cache.insert(javaPath, major);
|
||
return major;
|
||
}
|
||
|
||
QString JavaLocator::windowlessVariant(const QString &javaPath)
|
||
{
|
||
const QFileInfo info(javaPath);
|
||
if (info.fileName() != QLatin1String(kExecutable))
|
||
return javaPath;
|
||
const QFileInfo windowless(info.absolutePath() + u'/' + QString::fromLatin1(kWindowless));
|
||
return windowless.isFile() ? windowless.absoluteFilePath() : javaPath;
|
||
}
|
||
|
||
QString JavaLocator::select(const QString &gameDir,
|
||
int requiredMajor,
|
||
const QString &preferred,
|
||
QString *error)
|
||
{
|
||
QStringList candidates;
|
||
if (!preferred.isEmpty()) {
|
||
QFileInfo info(preferred);
|
||
// В настройках можно указать как сам java.exe, так и корень JDK.
|
||
if (info.isDir())
|
||
appendIfExecutable(candidates, info.absoluteFilePath() + QStringLiteral("/bin"));
|
||
else if (info.isFile())
|
||
candidates << info.absoluteFilePath();
|
||
|
||
if (candidates.isEmpty()) {
|
||
if (error)
|
||
*error = QObject::tr("Java по указанному пути не найдена: %1")
|
||
.arg(QDir::toNativeSeparators(preferred));
|
||
return {};
|
||
}
|
||
}
|
||
candidates += findAll(gameDir);
|
||
|
||
if (candidates.isEmpty()) {
|
||
if (error)
|
||
*error = QObject::tr("Java не найдена. Установите JDK %1 или укажите путь в настройках.")
|
||
.arg(requiredMajor);
|
||
return {};
|
||
}
|
||
|
||
QString bestBelow;
|
||
int bestBelowMajor = 0;
|
||
for (const QString &candidate : std::as_const(candidates)) {
|
||
const int major = majorVersion(candidate);
|
||
if (major >= requiredMajor)
|
||
return candidate;
|
||
if (major > bestBelowMajor) {
|
||
bestBelowMajor = major;
|
||
bestBelow = candidate;
|
||
}
|
||
}
|
||
|
||
if (error) {
|
||
*error = bestBelowMajor > 0
|
||
? QObject::tr("Для этой версии нужна Java %1 или новее, найдена только Java %2 (%3).")
|
||
.arg(requiredMajor)
|
||
.arg(bestBelowMajor)
|
||
.arg(QDir::toNativeSeparators(bestBelow))
|
||
: QObject::tr("Не удалось определить версию установленной Java. "
|
||
"Укажите путь к JDK %1 в настройках.")
|
||
.arg(requiredMajor);
|
||
}
|
||
return {};
|
||
}
|