/* SPDX-FileCopyrightText: 2026 Project Tick
* SPDX-FileContributor: Project Tick
* SPDX-License-Identifier: GPL-3.0-or-later
*
* MeshMC - A Custom Launcher for Minecraft
* Copyright (C) 2026 Project Tick
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "VerifyJavaInstall.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "FileSystem.h"
#include "Json.h"
#include "java/JavaUtils.h"
#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
#include "Application.h"
#include "BuildConfig.h"
#include "net/Download.h"
#endif
#ifdef major
#undef major
#endif
#ifdef minor
#undef minor
#endif
int VerifyJavaInstall::determineRequiredJavaMajor() const
{
auto m_inst = std::dynamic_pointer_cast(m_parent->instance());
auto minecraftComponent = m_inst->getPackProfile()->getComponent("net.minecraft");
if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java25BeginsDate)
return 25;
if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java21BeginsDate)
return 21;
if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java17BeginsDate)
return 17;
if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java16BeginsDate)
return 16;
if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java8BeginsDate)
return 8;
return 0;
}
QString VerifyJavaInstall::javaInstallDir() const
{
return FS::PathCombine(QDir::currentPath(), "java");
}
QString VerifyJavaInstall::findInstalledJava(int requiredMajor) const
{
#if defined(Q_OS_WIN)
QString binaryName = "javaw.exe";
#else
QString binaryName = "java";
#endif
// Pattern matching Java major version in directory names
// Matches: "java-8-openjdk", "jdk-17", "java-21-openjdk-amd64", "zulu25.30.31-ca-jdk25.0.0-linux_x64"
QRegularExpression re(QString("(?:jdk|java|jre)[-.]?%1(?:[^0-9]|$)").arg(requiredMajor));
// 1. Scan MeshMC's managed java/{vendor}/{version}/bin/java directory
QString javaBaseDir = javaInstallDir();
QDir baseDir(javaBaseDir);
if (baseDir.exists()) {
QDirIterator vendorIt(javaBaseDir, QDir::Dirs | QDir::NoDotAndDotDot);
while (vendorIt.hasNext()) {
vendorIt.next();
QString vendorPath = vendorIt.filePath();
QDirIterator versionIt(vendorPath, QDir::Dirs | QDir::NoDotAndDotDot);
while (versionIt.hasNext()) {
versionIt.next();
QString versionPath = versionIt.filePath();
QDirIterator binIt(versionPath, QStringList() << binaryName,
QDir::Files, QDirIterator::Subdirectories);
while (binIt.hasNext()) {
binIt.next();
QString javaPath = binIt.filePath();
if (!javaPath.contains("/bin/"))
continue;
QString dirName = versionIt.fileName().toLower();
if (re.match(dirName).hasMatch())
return javaPath;
}
}
}
}
// 2. Scan system-installed Java paths
// Use JavaUtils to discover all Java installations on the system, then
// check if any match the required major version by parsing the path.
JavaUtils javaUtils;
QList systemJavas = javaUtils.FindJavaPaths();
for (const QString &javaPath : systemJavas) {
// Resolve to absolute path and verify it exists
QString resolved = FS::ResolveExecutable(javaPath);
if (resolved.isEmpty())
continue;
// Extract the parent directory components to check for version info
// Typical paths:
// /usr/lib/jvm/java-8-openjdk/bin/java
// /usr/lib/jvm/java-8-openjdk/jre/bin/java
// /usr/lib64/jvm/java-17-openjdk/bin/java
// /opt/jdk/jdk-21/bin/java
QFileInfo fi(resolved);
QString fullPath = fi.absoluteFilePath().toLower();
// Check if any component in the path matches the required Java version
QStringList parts = fullPath.split('/');
for (const QString &part : parts) {
if (re.match(part).hasMatch()) {
return resolved;
}
}
}
return {};
}
void VerifyJavaInstall::executeTask() {
auto m_inst = std::dynamic_pointer_cast(m_parent->instance());
auto javaVersion = m_inst->getJavaVersion();
int requiredMajor = determineRequiredJavaMajor();
// No Java requirement or already met
if (requiredMajor == 0 || javaVersion.major() >= requiredMajor) {
emitSucceeded();
return;
}
// Java version insufficient — try to find an already-downloaded one
emit logLine(tr("Current Java version %1 does not meet the requirement of Java %2.")
.arg(javaVersion.toString()).arg(requiredMajor), MessageLevel::Warning);
QString existingJava = findInstalledJava(requiredMajor);
if (!existingJava.isEmpty()) {
emit logLine(tr("Found installed Java %1 at: %2").arg(requiredMajor).arg(existingJava), MessageLevel::MeshMC);
#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
setJavaPathAndSucceed(existingJava);
#else
m_inst->settings()->set("OverrideJavaLocation", true);
m_inst->settings()->set("JavaPath", existingJava);
emit logLine(tr("Java path set to: %1").arg(existingJava), MessageLevel::MeshMC);
emitSucceeded();
#endif
return;
}
#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
// Not found — auto-download
emit logLine(tr("No installed Java %1 found. Downloading...").arg(requiredMajor), MessageLevel::MeshMC);
autoDownloadJava(requiredMajor);
#else
emitFailed(tr("Java %1 is required but not installed. Please install it manually.").arg(requiredMajor));
#endif
}
#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
void VerifyJavaInstall::autoDownloadJava(int requiredMajor)
{
// Fetch version list from net.minecraft.java (Mojang)
fetchVersionList(requiredMajor);
}
void VerifyJavaInstall::fetchVersionList(int requiredMajor)
{
m_fetchData.clear();
QString uid = "net.minecraft.java";
QString url = QString("%1%2/index.json").arg(BuildConfig.META_URL, uid);
m_fetchJob = new NetJob(tr("Fetch Java versions"), APPLICATION->network());
auto dl = Net::Download::makeByteArray(QUrl(url), &m_fetchData);
m_fetchJob->addNetAction(dl);
connect(m_fetchJob.get(), &NetJob::succeeded, this, [this, uid, requiredMajor]() {
m_fetchJob.reset();
QJsonDocument doc;
try {
doc = Json::requireDocument(m_fetchData);
} catch (const Exception &e) {
emitFailed(tr("Failed to parse Java version list from meta server: %1").arg(e.cause()));
return;
}
if (!doc.isObject()) {
emitFailed(tr("Failed to parse Java version list from meta server."));
return;
}
auto versions = JavaDownload::parseVersionIndex(doc.object(), uid);
// Find the matching version (e.g., "java25" for requiredMajor=25)
QString targetVersionId = QString("java%1").arg(requiredMajor);
bool found = false;
for (const auto &ver : versions) {
if (ver.versionId == targetVersionId) {
found = true;
fetchRuntimes(ver.versionId, requiredMajor);
return;
}
}
if (!found) {
emitFailed(tr("Java %1 is not available for download from Mojang. Please install it manually.")
.arg(requiredMajor));
}
});
connect(m_fetchJob.get(), &NetJob::failed, this, [this, requiredMajor](QString reason) {
emitFailed(tr("Failed to fetch Java version list: %1. Please install Java %2 manually.")
.arg(reason).arg(requiredMajor));
m_fetchJob.reset();
});
m_fetchJob->start();
}
void VerifyJavaInstall::fetchRuntimes(const QString &versionId, int requiredMajor)
{
m_fetchData.clear();
QString uid = "net.minecraft.java";
QString url = QString("%1%2/%3.json").arg(BuildConfig.META_URL, uid, versionId);
m_fetchJob = new NetJob(tr("Fetch Java runtime details"), APPLICATION->network());
auto dl = Net::Download::makeByteArray(QUrl(url), &m_fetchData);
m_fetchJob->addNetAction(dl);
connect(m_fetchJob.get(), &NetJob::succeeded, this, [this, requiredMajor]() {
auto fetchJob = std::move(m_fetchJob);
QJsonDocument doc;
try {
doc = Json::requireDocument(m_fetchData);
} catch (const Exception &e) {
emitFailed(tr("Failed to parse Java runtime details: %1").arg(e.cause()));
return;
}
if (!doc.isObject()) {
emitFailed(tr("Failed to parse Java runtime details."));
return;
}
auto allRuntimes = JavaDownload::parseRuntimes(doc.object());
QString myOS = JavaDownload::currentRuntimeOS();
// Filter for current platform
for (const auto &rt : allRuntimes) {
if (rt.runtimeOS == myOS) {
emit logLine(tr("Downloading %1 (%2)...")
.arg(rt.name, rt.version.toString()), MessageLevel::MeshMC);
startDownload(rt, requiredMajor);
return;
}
}
emitFailed(tr("No Java %1 download available for your platform (%2). Please install it manually.")
.arg(requiredMajor).arg(myOS));
});
connect(m_fetchJob.get(), &NetJob::failed, this, [this](QString reason) {
emitFailed(tr("Failed to fetch Java runtime details: %1").arg(reason));
m_fetchJob.reset();
});
m_fetchJob->start();
}
void VerifyJavaInstall::startDownload(const JavaDownload::RuntimeEntry &runtime, int requiredMajor)
{
QString dirName = QString("%1-%2").arg(runtime.name, runtime.version.toString());
QString targetDir = FS::PathCombine(javaInstallDir(), runtime.vendor, dirName);
m_downloadTask = std::make_unique(runtime, targetDir, this);
connect(m_downloadTask.get(), &Task::succeeded, this, [this]() {
QString javaPath = m_downloadTask->installedJavaPath();
if (javaPath.isEmpty()) {
emitFailed(tr("Java was downloaded but the binary could not be found."));
return;
}
emit logLine(tr("Java downloaded and installed at: %1").arg(javaPath), MessageLevel::MeshMC);
setJavaPathAndSucceed(javaPath);
});
connect(m_downloadTask.get(), &Task::failed, this, [this, requiredMajor](const QString &reason) {
emitFailed(tr("Failed to download Java %1: %2").arg(requiredMajor).arg(reason));
m_downloadTask.reset();
});
connect(m_downloadTask.get(), &Task::status, this, [this](const QString &status) {
emit logLine(status, MessageLevel::MeshMC);
});
m_downloadTask->start();
}
void VerifyJavaInstall::setJavaPathAndSucceed(const QString &javaPath)
{
auto m_inst = std::dynamic_pointer_cast(m_parent->instance());
// Set Java path override on the instance only, not globally
m_inst->settings()->set("OverrideJavaLocation", true);
m_inst->settings()->set("JavaPath", javaPath);
emit logLine(tr("Java path set to: %1").arg(javaPath), MessageLevel::MeshMC);
emitSucceeded();
}
#endif