/* 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
#include
#include "Application.h"
#include "FileSystem.h"
#include "Json.h"
#include "java/JavaUtils.h"
#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
#include "BuildConfig.h"
#include "net/Download.h"
#endif
#ifdef major
#undef major
#endif
#ifdef minor
#undef minor
#endif
namespace
{
std::optional probeJavaVersion(const QString& javaPath)
{
const auto checkerJar =
FS::PathCombine(APPLICATION->getJarsPath(), "JavaCheck.jar");
if (!QFileInfo::exists(checkerJar)) {
return std::nullopt;
}
QProcess process;
process.setProgram(javaPath);
process.setArguments({"-jar", checkerJar});
process.setProcessEnvironment(CleanEnviroment());
process.setProcessChannelMode(QProcess::SeparateChannels);
process.start();
if (!process.waitForFinished(15000) ||
process.exitStatus() != QProcess::NormalExit ||
process.exitCode() != 0) {
return std::nullopt;
}
const auto stdoutData =
QString::fromLocal8Bit(process.readAllStandardOutput());
const auto lines = stdoutData.split('\n', Qt::SkipEmptyParts);
for (const auto& rawLine : lines) {
const auto line = rawLine.trimmed();
if (!line.startsWith("java.version=")) {
continue;
}
return JavaVersion(line.mid(QString("java.version=").size()));
}
return std::nullopt;
}
} // namespace
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 JavaUtils::managedJavaRoot();
}
QString VerifyJavaInstall::findInstalledJava(int requiredMajor) const
{
JavaUtils javaUtils;
QList systemJavas = javaUtils.FindJavaPaths();
QSet seenPaths;
for (const QString& javaPath : systemJavas) {
QString resolved = FS::ResolveExecutable(javaPath);
if (resolved.isEmpty() || seenPaths.contains(resolved))
continue;
seenPaths.insert(resolved);
const auto version = probeJavaVersion(resolved);
if (version.has_value() && version->major() >= requiredMajor) {
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