summaryrefslogtreecommitdiff
path: root/meshmc/launcher/java
diff options
context:
space:
mode:
Diffstat (limited to 'meshmc/launcher/java')
-rw-r--r--meshmc/launcher/java/JavaChecker.cpp189
-rw-r--r--meshmc/launcher/java/JavaChecker.h80
-rw-r--r--meshmc/launcher/java/JavaCheckerJob.cpp66
-rw-r--r--meshmc/launcher/java/JavaCheckerJob.h85
-rw-r--r--meshmc/launcher/java/JavaInstall.cpp48
-rw-r--r--meshmc/launcher/java/JavaInstall.h61
-rw-r--r--meshmc/launcher/java/JavaInstallList.cpp219
-rw-r--r--meshmc/launcher/java/JavaInstallList.h100
-rw-r--r--meshmc/launcher/java/JavaUtils.cpp591
-rw-r--r--meshmc/launcher/java/JavaUtils.h71
-rw-r--r--meshmc/launcher/java/JavaVersion.cpp138
-rw-r--r--meshmc/launcher/java/JavaVersion.h73
-rw-r--r--meshmc/launcher/java/JavaVersion_test.cpp151
-rw-r--r--meshmc/launcher/java/download/JavaDownloadTask.cpp344
-rw-r--r--meshmc/launcher/java/download/JavaDownloadTask.h68
-rw-r--r--meshmc/launcher/java/download/JavaRuntime.h169
16 files changed, 2453 insertions, 0 deletions
diff --git a/meshmc/launcher/java/JavaChecker.cpp b/meshmc/launcher/java/JavaChecker.cpp
new file mode 100644
index 0000000000..25978db8c2
--- /dev/null
+++ b/meshmc/launcher/java/JavaChecker.cpp
@@ -0,0 +1,189 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include "JavaChecker.h"
+
+#include <QFile>
+#include <QProcess>
+#include <QMap>
+#include <QDebug>
+
+#include "JavaUtils.h"
+#include "FileSystem.h"
+#include "Commandline.h"
+#include "Application.h"
+
+JavaChecker::JavaChecker(QObject* parent) : QObject(parent) {}
+
+void JavaChecker::performCheck()
+{
+ QString checkerJar =
+ FS::PathCombine(APPLICATION->getJarsPath(), "JavaCheck.jar");
+
+ QStringList args;
+
+ process.reset(new QProcess());
+ if (m_args.size()) {
+ auto extraArgs = Commandline::splitArgs(m_args);
+ args.append(extraArgs);
+ }
+ if (m_minMem != 0) {
+ args << QString("-Xms%1m").arg(m_minMem);
+ }
+ if (m_maxMem != 0) {
+ args << QString("-Xmx%1m").arg(m_maxMem);
+ }
+ if (m_permGen != 64) {
+ args << QString("-XX:PermSize=%1m").arg(m_permGen);
+ }
+
+ args.append({"-jar", checkerJar});
+ process->setArguments(args);
+ process->setProgram(m_path);
+ process->setProcessChannelMode(QProcess::SeparateChannels);
+ process->setProcessEnvironment(CleanEnviroment());
+ qDebug() << "Running java checker: " + m_path + args.join(" ");
+ ;
+
+ connect(process.get(), SIGNAL(finished(int, QProcess::ExitStatus)), this,
+ SLOT(finished(int, QProcess::ExitStatus)));
+ connect(process.get(), SIGNAL(errorOccurred(QProcess::ProcessError)), this,
+ SLOT(error(QProcess::ProcessError)));
+ connect(process.get(), SIGNAL(readyReadStandardOutput()), this,
+ SLOT(stdoutReady()));
+ connect(process.get(), SIGNAL(readyReadStandardError()), this,
+ SLOT(stderrReady()));
+ connect(&killTimer, SIGNAL(timeout()), SLOT(timeout()));
+ killTimer.setSingleShot(true);
+ killTimer.start(15000);
+ process->start();
+}
+
+void JavaChecker::stdoutReady()
+{
+ QByteArray data = process->readAllStandardOutput();
+ QString added = QString::fromLocal8Bit(data);
+ added.remove('\r');
+ m_stdout += added;
+}
+
+void JavaChecker::stderrReady()
+{
+ QByteArray data = process->readAllStandardError();
+ QString added = QString::fromLocal8Bit(data);
+ added.remove('\r');
+ m_stderr += added;
+}
+
+void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
+{
+ killTimer.stop();
+ QProcessPtr _process = process;
+ process.reset();
+
+ JavaCheckResult result;
+ {
+ result.path = m_path;
+ result.id = m_id;
+ }
+ result.errorLog = m_stderr;
+ result.outLog = m_stdout;
+ qDebug() << "STDOUT" << m_stdout;
+ qWarning() << "STDERR" << m_stderr;
+ qDebug() << "Java checker finished with status " << status << " exit code "
+ << exitcode;
+
+ if (status == QProcess::CrashExit || exitcode == 1) {
+ result.validity = JavaCheckResult::Validity::Errored;
+ emit checkFinished(result);
+ return;
+ }
+
+ bool success = true;
+
+ QMap<QString, QString> results;
+ QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts);
+ for (QString line : lines) {
+ line = line.trimmed();
+ // NOTE: workaround for GH-4125, where garbage is getting printed into
+ // stdout on bedrock linux
+ if (line.contains("/bedrock/strata")) {
+ continue;
+ }
+
+ auto parts = line.split('=', Qt::SkipEmptyParts);
+ if (parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) {
+ continue;
+ } else {
+ results.insert(parts[0], parts[1]);
+ }
+ }
+
+ if (!results.contains("os.arch") || !results.contains("java.version") ||
+ !results.contains("java.vendor") || !success) {
+ result.validity = JavaCheckResult::Validity::ReturnedInvalidData;
+ emit checkFinished(result);
+ return;
+ }
+
+ auto os_arch = results["os.arch"];
+ auto java_version = results["java.version"];
+ auto java_vendor = results["java.vendor"];
+ bool is_64 = os_arch == "x86_64" || os_arch == "amd64";
+
+ result.validity = JavaCheckResult::Validity::Valid;
+ result.is_64bit = is_64;
+ result.mojangPlatform = is_64 ? "64" : "32";
+ result.realPlatform = os_arch;
+ result.javaVersion = java_version;
+ result.javaVendor = java_vendor;
+ qDebug() << "Java checker succeeded.";
+ emit checkFinished(result);
+}
+
+void JavaChecker::error(QProcess::ProcessError err)
+{
+ if (err == QProcess::FailedToStart) {
+ qDebug() << "Java checker has failed to start.";
+ qDebug() << "Process environment:";
+ qDebug() << process->environment();
+ qDebug() << "Native environment:";
+ qDebug() << QProcessEnvironment::systemEnvironment().toStringList();
+ killTimer.stop();
+ JavaCheckResult result;
+ {
+ result.path = m_path;
+ result.id = m_id;
+ }
+
+ emit checkFinished(result);
+ return;
+ }
+}
+
+void JavaChecker::timeout()
+{
+ // NO MERCY. NO ABUSE.
+ if (process) {
+ qDebug() << "Java checker has been killed by timeout.";
+ process->kill();
+ }
+}
diff --git a/meshmc/launcher/java/JavaChecker.h b/meshmc/launcher/java/JavaChecker.h
new file mode 100644
index 0000000000..7dc32d2331
--- /dev/null
+++ b/meshmc/launcher/java/JavaChecker.h
@@ -0,0 +1,80 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QProcess>
+#include <QTimer>
+#include <memory>
+
+#include "QObjectPtr.h"
+
+#include "JavaVersion.h"
+
+class JavaChecker;
+
+struct JavaCheckResult {
+ QString path;
+ QString mojangPlatform;
+ QString realPlatform;
+ JavaVersion javaVersion;
+ QString javaVendor;
+ QString outLog;
+ QString errorLog;
+ bool is_64bit = false;
+ int id;
+ enum class Validity {
+ Errored,
+ ReturnedInvalidData,
+ Valid
+ } validity = Validity::Errored;
+};
+
+typedef shared_qobject_ptr<QProcess> QProcessPtr;
+typedef shared_qobject_ptr<JavaChecker> JavaCheckerPtr;
+class JavaChecker : public QObject
+{
+ Q_OBJECT
+ public:
+ explicit JavaChecker(QObject* parent = 0);
+ void performCheck();
+
+ QString m_path;
+ QString m_args;
+ int m_id = 0;
+ int m_minMem = 0;
+ int m_maxMem = 0;
+ int m_permGen = 64;
+
+ signals:
+ void checkFinished(JavaCheckResult result);
+
+ private:
+ QProcessPtr process;
+ QTimer killTimer;
+ QString m_stdout;
+ QString m_stderr;
+ public slots:
+ void timeout();
+ void finished(int exitcode, QProcess::ExitStatus);
+ void error(QProcess::ProcessError);
+ void stdoutReady();
+ void stderrReady();
+};
diff --git a/meshmc/launcher/java/JavaCheckerJob.cpp b/meshmc/launcher/java/JavaCheckerJob.cpp
new file mode 100644
index 0000000000..4f4ebb6168
--- /dev/null
+++ b/meshmc/launcher/java/JavaCheckerJob.cpp
@@ -0,0 +1,66 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "JavaCheckerJob.h"
+
+#include <QDebug>
+
+void JavaCheckerJob::partFinished(JavaCheckResult result)
+{
+ num_finished++;
+ qDebug() << m_job_name.toLocal8Bit() << "progress:" << num_finished << "/"
+ << javacheckers.size();
+ setProgress(num_finished, javacheckers.size());
+
+ javaresults.replace(result.id, result);
+
+ if (num_finished == javacheckers.size()) {
+ emitSucceeded();
+ }
+}
+
+void JavaCheckerJob::executeTask()
+{
+ qDebug() << m_job_name.toLocal8Bit() << " started.";
+ for (auto iter : javacheckers) {
+ javaresults.append(JavaCheckResult());
+ connect(iter.get(), SIGNAL(checkFinished(JavaCheckResult)),
+ SLOT(partFinished(JavaCheckResult)));
+ iter->performCheck();
+ }
+}
diff --git a/meshmc/launcher/java/JavaCheckerJob.h b/meshmc/launcher/java/JavaCheckerJob.h
new file mode 100644
index 0000000000..17b734da86
--- /dev/null
+++ b/meshmc/launcher/java/JavaCheckerJob.h
@@ -0,0 +1,85 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QtNetwork>
+#include "JavaChecker.h"
+#include "tasks/Task.h"
+
+class JavaCheckerJob;
+typedef shared_qobject_ptr<JavaCheckerJob> JavaCheckerJobPtr;
+
+// FIXME: this just seems horribly redundant
+class JavaCheckerJob : public Task
+{
+ Q_OBJECT
+ public:
+ explicit JavaCheckerJob(QString job_name) : Task(), m_job_name(job_name) {};
+ virtual ~JavaCheckerJob() {};
+
+ bool addJavaCheckerAction(JavaCheckerPtr base)
+ {
+ javacheckers.append(base);
+ // if this is already running, the action needs to be started right
+ // away!
+ if (isRunning()) {
+ setProgress(num_finished, javacheckers.size());
+ connect(base.get(), &JavaChecker::checkFinished, this,
+ &JavaCheckerJob::partFinished);
+ base->performCheck();
+ }
+ return true;
+ }
+ QList<JavaCheckResult> getResults()
+ {
+ return javaresults;
+ }
+
+ private slots:
+ void partFinished(JavaCheckResult result);
+
+ protected:
+ virtual void executeTask() override;
+
+ private:
+ QString m_job_name;
+ QList<JavaCheckerPtr> javacheckers;
+ QList<JavaCheckResult> javaresults;
+ int num_finished = 0;
+};
diff --git a/meshmc/launcher/java/JavaInstall.cpp b/meshmc/launcher/java/JavaInstall.cpp
new file mode 100644
index 0000000000..f5522a9621
--- /dev/null
+++ b/meshmc/launcher/java/JavaInstall.cpp
@@ -0,0 +1,48 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include "JavaInstall.h"
+#include <MMCStrings.h>
+
+bool JavaInstall::operator<(const JavaInstall& rhs)
+{
+ auto archCompare =
+ Strings::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive);
+ if (archCompare != 0)
+ return archCompare < 0;
+ if (id < rhs.id) {
+ return true;
+ }
+ if (id > rhs.id) {
+ return false;
+ }
+ return Strings::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0;
+}
+
+bool JavaInstall::operator==(const JavaInstall& rhs)
+{
+ return arch == rhs.arch && id == rhs.id && path == rhs.path;
+}
+
+bool JavaInstall::operator>(const JavaInstall& rhs)
+{
+ return (!operator<(rhs)) && (!operator==(rhs));
+}
diff --git a/meshmc/launcher/java/JavaInstall.h b/meshmc/launcher/java/JavaInstall.h
new file mode 100644
index 0000000000..1d9c481963
--- /dev/null
+++ b/meshmc/launcher/java/JavaInstall.h
@@ -0,0 +1,61 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "BaseVersion.h"
+#include "JavaVersion.h"
+
+struct JavaInstall : public BaseVersion {
+ JavaInstall() {}
+ JavaInstall(QString id, QString arch, QString path)
+ : id(id), arch(arch), path(path)
+ {
+ }
+ virtual QString descriptor()
+ {
+ return id.toString();
+ }
+
+ virtual QString name()
+ {
+ return id.toString();
+ }
+
+ virtual QString typeString() const
+ {
+ return arch;
+ }
+
+ using BaseVersion::operator<;
+ using BaseVersion::operator>;
+
+ bool operator<(const JavaInstall& rhs);
+ bool operator==(const JavaInstall& rhs);
+ bool operator>(const JavaInstall& rhs);
+
+ JavaVersion id;
+ QString arch;
+ QString path;
+ bool recommended = false;
+};
+
+typedef std::shared_ptr<JavaInstall> JavaInstallPtr;
diff --git a/meshmc/launcher/java/JavaInstallList.cpp b/meshmc/launcher/java/JavaInstallList.cpp
new file mode 100644
index 0000000000..3377f368c3
--- /dev/null
+++ b/meshmc/launcher/java/JavaInstallList.cpp
@@ -0,0 +1,219 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QtNetwork>
+#include <QtXml>
+
+#include <QDebug>
+
+#include "java/JavaInstallList.h"
+#include "java/JavaCheckerJob.h"
+#include "java/JavaUtils.h"
+#include "MMCStrings.h"
+#include "minecraft/VersionFilterData.h"
+
+JavaInstallList::JavaInstallList(QObject* parent) : BaseVersionList(parent) {}
+
+Task::Ptr JavaInstallList::getLoadTask()
+{
+ load();
+ return getCurrentTask();
+}
+
+Task::Ptr JavaInstallList::getCurrentTask()
+{
+ if (m_status == Status::InProgress) {
+ return m_loadTask;
+ }
+ return nullptr;
+}
+
+void JavaInstallList::load()
+{
+ if (m_status != Status::InProgress) {
+ m_status = Status::InProgress;
+ m_loadTask = new JavaListLoadTask(this);
+ m_loadTask->start();
+ }
+}
+
+const BaseVersionPtr JavaInstallList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+bool JavaInstallList::isLoaded()
+{
+ return m_status == JavaInstallList::Status::Done;
+}
+
+int JavaInstallList::count() const
+{
+ return m_vlist.count();
+}
+
+QVariant JavaInstallList::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ auto version = std::dynamic_pointer_cast<JavaInstall>(m_vlist[index.row()]);
+ switch (role) {
+ case VersionPointerRole:
+ return QVariant::fromValue(m_vlist[index.row()]);
+ case VersionIdRole:
+ return version->descriptor();
+ case VersionRole:
+ return version->id.toString();
+ case RecommendedRole:
+ return version->recommended;
+ case PathRole:
+ return version->path;
+ case ArchitectureRole:
+ return version->arch;
+ default:
+ return QVariant();
+ }
+}
+
+BaseVersionList::RoleList JavaInstallList::providesRoles() const
+{
+ return {VersionPointerRole, VersionIdRole, VersionRole,
+ RecommendedRole, PathRole, ArchitectureRole};
+}
+
+void JavaInstallList::updateListData(QList<BaseVersionPtr> versions)
+{
+ beginResetModel();
+ m_vlist = versions;
+ sortVersions();
+ if (m_vlist.size()) {
+ auto best = std::dynamic_pointer_cast<JavaInstall>(m_vlist[0]);
+ best->recommended = true;
+ }
+ endResetModel();
+ m_status = Status::Done;
+ m_loadTask.reset();
+}
+
+bool sortJavas(BaseVersionPtr left, BaseVersionPtr right)
+{
+ auto rleft = std::dynamic_pointer_cast<JavaInstall>(left);
+ auto rright = std::dynamic_pointer_cast<JavaInstall>(right);
+ return (*rleft) > (*rright);
+}
+
+void JavaInstallList::sortVersions()
+{
+ beginResetModel();
+ std::sort(m_vlist.begin(), m_vlist.end(), sortJavas);
+ endResetModel();
+}
+
+JavaListLoadTask::JavaListLoadTask(JavaInstallList* vlist) : Task()
+{
+ m_list = vlist;
+ m_currentRecommended = NULL;
+}
+
+JavaListLoadTask::~JavaListLoadTask() {}
+
+void JavaListLoadTask::executeTask()
+{
+ setStatus(tr("Detecting Java installations..."));
+
+ JavaUtils ju;
+ QList<QString> candidate_paths = ju.FindJavaPaths();
+
+ m_job = new JavaCheckerJob("Java detection");
+ connect(m_job.get(), &Task::finished, this,
+ &JavaListLoadTask::javaCheckerFinished);
+ connect(m_job.get(), &Task::progress, this, &Task::setProgress);
+
+ qDebug() << "Probing the following Java paths: ";
+ int id = 0;
+ for (QString candidate : candidate_paths) {
+ qDebug() << " " << candidate;
+
+ auto candidate_checker = new JavaChecker();
+ candidate_checker->m_path = candidate;
+ candidate_checker->m_id = id;
+ m_job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker));
+
+ id++;
+ }
+
+ m_job->start();
+}
+
+void JavaListLoadTask::javaCheckerFinished()
+{
+ QList<JavaInstallPtr> candidates;
+ auto results = m_job->getResults();
+
+ qDebug() << "Found the following valid Java installations:";
+ for (JavaCheckResult result : results) {
+ if (result.validity == JavaCheckResult::Validity::Valid) {
+ JavaInstallPtr javaVersion(new JavaInstall());
+
+ javaVersion->id = result.javaVersion;
+ javaVersion->arch = result.mojangPlatform;
+ javaVersion->path = result.path;
+ candidates.append(javaVersion);
+
+ qDebug() << " " << javaVersion->id.toString() << javaVersion->arch
+ << javaVersion->path;
+ }
+ }
+
+ QList<BaseVersionPtr> javas_bvp;
+ for (auto java : candidates) {
+ // qDebug() << java->id << java->arch << " at " << java->path;
+ BaseVersionPtr bp_java = std::dynamic_pointer_cast<BaseVersion>(java);
+
+ if (bp_java) {
+ javas_bvp.append(java);
+ }
+ }
+
+ m_list->updateListData(javas_bvp);
+ emitSucceeded();
+}
diff --git a/meshmc/launcher/java/JavaInstallList.h b/meshmc/launcher/java/JavaInstallList.h
new file mode 100644
index 0000000000..a889f43b96
--- /dev/null
+++ b/meshmc/launcher/java/JavaInstallList.h
@@ -0,0 +1,100 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QAbstractListModel>
+
+#include "BaseVersionList.h"
+#include "tasks/Task.h"
+
+#include "JavaCheckerJob.h"
+#include "JavaInstall.h"
+
+#include "QObjectPtr.h"
+
+class JavaListLoadTask;
+
+class JavaInstallList : public BaseVersionList
+{
+ Q_OBJECT
+ enum class Status { NotDone, InProgress, Done };
+
+ public:
+ explicit JavaInstallList(QObject* parent = 0);
+
+ Task::Ptr getLoadTask() override;
+ bool isLoaded() override;
+ const BaseVersionPtr at(int i) const override;
+ int count() const override;
+ void sortVersions() override;
+
+ QVariant data(const QModelIndex& index, int role) const override;
+ RoleList providesRoles() const override;
+
+ public slots:
+ void updateListData(QList<BaseVersionPtr> versions) override;
+
+ protected:
+ void load();
+ Task::Ptr getCurrentTask();
+
+ protected:
+ Status m_status = Status::NotDone;
+ shared_qobject_ptr<JavaListLoadTask> m_loadTask;
+ QList<BaseVersionPtr> m_vlist;
+};
+
+class JavaListLoadTask : public Task
+{
+ Q_OBJECT
+
+ public:
+ explicit JavaListLoadTask(JavaInstallList* vlist);
+ virtual ~JavaListLoadTask();
+
+ void executeTask() override;
+ public slots:
+ void javaCheckerFinished();
+
+ protected:
+ shared_qobject_ptr<JavaCheckerJob> m_job;
+ JavaInstallList* m_list;
+ JavaInstall* m_currentRecommended;
+};
diff --git a/meshmc/launcher/java/JavaUtils.cpp b/meshmc/launcher/java/JavaUtils.cpp
new file mode 100644
index 0000000000..0d59161b0a
--- /dev/null
+++ b/meshmc/launcher/java/JavaUtils.cpp
@@ -0,0 +1,591 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QStringList>
+#include <QString>
+#include <QDir>
+#include <QDirIterator>
+#include <QFileInfo>
+#include <QProcessEnvironment>
+#include <QStringList>
+
+#include <settings/Setting.h>
+
+#include <QDebug>
+#include "java/JavaUtils.h"
+#include "java/JavaInstallList.h"
+#include "FileSystem.h"
+
+#define IBUS "@im=ibus"
+
+JavaUtils::JavaUtils() {}
+
+QString JavaUtils::managedJavaRoot()
+{
+ return FS::PathCombine(QDir::currentPath(), "java");
+}
+
+namespace
+{
+ QString javaBinaryName()
+ {
+#if defined(Q_OS_WIN)
+ return "javaw.exe";
+#else
+ return "java";
+#endif
+ }
+
+ void appendUniquePath(QList<QString>& paths, const QString& path)
+ {
+ if (!path.isEmpty() && !paths.contains(path)) {
+ paths.append(path);
+ }
+ }
+
+ void appendExecutablePath(QList<QString>& paths, const QString& path)
+ {
+ QFileInfo info(path);
+ if (info.exists() && info.isFile()) {
+ appendUniquePath(paths, info.absoluteFilePath());
+ }
+ }
+
+ void appendJavaHome(QList<QString>& paths, const QString& homePath)
+ {
+ if (homePath.isEmpty()) {
+ return;
+ }
+
+ QFileInfo info(homePath);
+ if (info.exists() && info.isFile()) {
+ appendExecutablePath(paths, info.absoluteFilePath());
+ return;
+ }
+
+ const auto binaryName = javaBinaryName();
+ const auto contentsHome = FS::PathCombine(homePath, "Contents", "Home");
+ appendExecutablePath(paths,
+ FS::PathCombine(homePath, "bin", binaryName));
+ appendExecutablePath(
+ paths, FS::PathCombine(homePath, "jre", "bin", binaryName));
+ appendExecutablePath(paths,
+ FS::PathCombine(contentsHome, "bin", binaryName));
+ appendExecutablePath(
+ paths, FS::PathCombine(FS::PathCombine(contentsHome, "jre"), "bin",
+ binaryName));
+ appendExecutablePath(
+ paths, FS::PathCombine(FS::PathCombine(homePath, "Contents"),
+ "Commands", binaryName));
+ appendExecutablePath(
+ paths,
+ FS::PathCombine(
+ FS::PathCombine(homePath, "libexec/openjdk.jdk/Contents/Home"),
+ "bin", binaryName));
+ }
+
+ [[maybe_unused]] void scanJavaHomes(QList<QString>& paths,
+ const QString& rootPath)
+ {
+ QDir root(rootPath);
+ if (!root.exists()) {
+ return;
+ }
+
+ const auto entries = root.entryInfoList(
+ QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
+ for (const auto& entry : entries) {
+ appendJavaHome(paths, entry.absoluteFilePath());
+ }
+ }
+
+ [[maybe_unused]] void appendManagedJavaCandidates(QList<QString>& paths,
+ const QString& rootPath)
+ {
+ QDir managedDir(rootPath);
+ if (!managedDir.exists()) {
+ return;
+ }
+
+ QDirIterator it(rootPath, QStringList() << javaBinaryName(),
+ QDir::Files, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ it.next();
+ const auto candidate = QDir::fromNativeSeparators(it.filePath());
+ if (candidate.contains("/bin/")) {
+ appendUniquePath(paths, candidate);
+ }
+ }
+ }
+} // namespace
+
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
+static QString processLD_LIBRARY_PATH(const QString& LD_LIBRARY_PATH)
+{
+ QDir mmcBin(QCoreApplication::applicationDirPath());
+ auto items = LD_LIBRARY_PATH.split(':');
+ QStringList final;
+ for (auto& item : items) {
+ QDir test(item);
+ if (test == mmcBin) {
+ qDebug() << "Env:LD_LIBRARY_PATH ignoring path" << item;
+ continue;
+ }
+ final.append(item);
+ }
+ return final.join(':');
+}
+#endif
+
+QProcessEnvironment CleanEnviroment()
+{
+ // prepare the process environment
+ QProcessEnvironment rawenv = QProcessEnvironment::systemEnvironment();
+ QProcessEnvironment env;
+
+ QStringList ignored = {"JAVA_ARGS", "CLASSPATH", "CONFIGPATH",
+ "JAVA_HOME", "JRE_HOME", "_JAVA_OPTIONS",
+ "JAVA_OPTIONS", "JAVA_TOOL_OPTIONS"};
+ for (auto key : rawenv.keys()) {
+ auto value = rawenv.value(key);
+ // filter out dangerous java crap
+ if (ignored.contains(key)) {
+ qDebug() << "Env: ignoring" << key << value;
+ continue;
+ }
+ // filter MeshMC-related things
+ if (key.startsWith("QT_")) {
+ qDebug() << "Env: ignoring" << key << value;
+ continue;
+ }
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
+ // Do not pass LD_* variables to java. They were intended for MeshMC
+ if (key.startsWith("LD_")) {
+ qDebug() << "Env: ignoring" << key << value;
+ continue;
+ }
+ // Strip IBus
+ // IBus is a Linux IME framework. For some reason, it breaks MC?
+ if (key == "XMODIFIERS" && value.contains(IBUS)) {
+ QString save = value;
+ value.replace(IBUS, "");
+ qDebug() << "Env: stripped" << IBUS << "from" << save << ":"
+ << value;
+ }
+ if (key == "GAME_PRELOAD") {
+ env.insert("LD_PRELOAD", value);
+ continue;
+ }
+ if (key == "GAME_LIBRARY_PATH") {
+ env.insert("LD_LIBRARY_PATH", processLD_LIBRARY_PATH(value));
+ continue;
+ }
+#endif
+ // qDebug() << "Env: " << key << value;
+ env.insert(key, value);
+ }
+#ifdef Q_OS_LINUX
+ // HACK: Workaround for QTBUG42500
+ if (!env.contains("LD_LIBRARY_PATH")) {
+ env.insert("LD_LIBRARY_PATH", "");
+ }
+#endif
+
+ return env;
+}
+
+JavaInstallPtr JavaUtils::MakeJavaPtr(QString path, QString id, QString arch)
+{
+ JavaInstallPtr javaVersion(new JavaInstall());
+
+ javaVersion->id = id;
+ javaVersion->arch = arch;
+ javaVersion->path = path;
+
+ return javaVersion;
+}
+
+JavaInstallPtr JavaUtils::GetDefaultJava()
+{
+ JavaInstallPtr javaVersion(new JavaInstall());
+
+ javaVersion->id = "java";
+ javaVersion->arch = "unknown";
+#if defined(Q_OS_WIN32)
+ javaVersion->path = "javaw";
+#else
+ javaVersion->path = "java";
+#endif
+
+ return javaVersion;
+}
+
+#if defined(Q_OS_WIN32)
+QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType,
+ QString keyName,
+ QString keyJavaDir,
+ QString subkeySuffix)
+{
+ QList<JavaInstallPtr> javas;
+
+ QString archType = "unknown";
+ if (keyType == KEY_WOW64_64KEY)
+ archType = "64";
+ else if (keyType == KEY_WOW64_32KEY)
+ archType = "32";
+
+ HKEY jreKey;
+ if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, keyName.toStdString().c_str(), 0,
+ KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS,
+ &jreKey) == ERROR_SUCCESS) {
+ // Read the current type version from the registry.
+ // This will be used to find any key that contains the JavaHome value.
+ char* value = new char[0];
+ DWORD valueSz = 0;
+ if (RegQueryValueExA(jreKey, "CurrentVersion", nullptr, nullptr,
+ (BYTE*)value, &valueSz) == ERROR_MORE_DATA) {
+ value = new char[valueSz];
+ RegQueryValueExA(jreKey, "CurrentVersion", nullptr, nullptr,
+ (BYTE*)value, &valueSz);
+ }
+
+ char subKeyName[255];
+ DWORD subKeyNameSize, numSubKeys, retCode;
+
+ // Get the number of subkeys
+ RegQueryInfoKeyA(jreKey, nullptr, nullptr, nullptr, &numSubKeys,
+ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
+ nullptr);
+
+ // Iterate until RegEnumKeyEx fails
+ for (DWORD i = 0; i < numSubKeys; i++) {
+ subKeyNameSize = 255;
+ retCode = RegEnumKeyExA(jreKey, i, subKeyName, &subKeyNameSize,
+ nullptr, nullptr, nullptr, nullptr);
+ if (retCode != ERROR_SUCCESS)
+ continue;
+
+ // Now open the registry key for the version that we just got.
+ QString newKeyName = keyName + "\\" + subKeyName + subkeySuffix;
+
+ HKEY newKey;
+ if (RegOpenKeyExA(
+ HKEY_LOCAL_MACHINE, newKeyName.toStdString().c_str(), 0,
+ KEY_READ | KEY_WOW64_64KEY, &newKey) != ERROR_SUCCESS)
+ continue;
+
+ // Read the JavaHome value to find where Java is installed.
+ value = new char[0];
+ valueSz = 0;
+ if (RegQueryValueExA(newKey, keyJavaDir.toStdString().c_str(),
+ nullptr, nullptr, (BYTE*)value,
+ &valueSz) == ERROR_MORE_DATA) {
+ value = new char[valueSz];
+ RegQueryValueExA(newKey, keyJavaDir.toStdString().c_str(),
+ nullptr, nullptr, (BYTE*)value, &valueSz);
+
+ // Now, we construct the version object and add it to the list.
+ JavaInstallPtr javaVersion(new JavaInstall());
+
+ javaVersion->id = subKeyName;
+ javaVersion->arch = archType;
+ javaVersion->path = QDir(FS::PathCombine(value, "bin"))
+ .absoluteFilePath("javaw.exe");
+ javas.append(javaVersion);
+ }
+
+ RegCloseKey(newKey);
+ }
+
+ RegCloseKey(jreKey);
+ }
+
+ return javas;
+}
+
+QList<QString> JavaUtils::FindJavaPaths()
+{
+ QList<JavaInstallPtr> java_candidates;
+
+ // Oracle
+ QList<JavaInstallPtr> JRE64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment",
+ "JavaHome");
+ QList<JavaInstallPtr> JDK64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit",
+ "JavaHome");
+ QList<JavaInstallPtr> JRE32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment",
+ "JavaHome");
+ QList<JavaInstallPtr> JDK32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit",
+ "JavaHome");
+
+ // Oracle for Java 9 and newer
+ QList<JavaInstallPtr> NEWJRE64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome");
+ QList<JavaInstallPtr> NEWJDK64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome");
+ QList<JavaInstallPtr> NEWJRE32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome");
+ QList<JavaInstallPtr> NEWJDK32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome");
+
+ // AdoptOpenJDK
+ QList<JavaInstallPtr> ADOPTOPENJRE32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path",
+ "\\hotspot\\MSI");
+ QList<JavaInstallPtr> ADOPTOPENJRE64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path",
+ "\\hotspot\\MSI");
+ QList<JavaInstallPtr> ADOPTOPENJDK32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path",
+ "\\hotspot\\MSI");
+ QList<JavaInstallPtr> ADOPTOPENJDK64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path",
+ "\\hotspot\\MSI");
+
+ // Eclipse Foundation
+ QList<JavaInstallPtr> FOUNDATIONJDK32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path",
+ "\\hotspot\\MSI");
+ QList<JavaInstallPtr> FOUNDATIONJDK64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path",
+ "\\hotspot\\MSI");
+
+ // Eclipse Adoptium
+ QList<JavaInstallPtr> ADOPTIUMJRE32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path",
+ "\\hotspot\\MSI");
+ QList<JavaInstallPtr> ADOPTIUMJRE64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path",
+ "\\hotspot\\MSI");
+ QList<JavaInstallPtr> ADOPTIUMJDK32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path",
+ "\\hotspot\\MSI");
+ QList<JavaInstallPtr> ADOPTIUMJDK64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path",
+ "\\hotspot\\MSI");
+
+ // Microsoft
+ QList<JavaInstallPtr> MICROSOFTJDK64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI");
+
+ // Azul Zulu
+ QList<JavaInstallPtr> ZULU64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath");
+ QList<JavaInstallPtr> ZULU32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath");
+
+ // BellSoft Liberica
+ QList<JavaInstallPtr> LIBERICA64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath");
+ QList<JavaInstallPtr> LIBERICA32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath");
+
+ // List x64 before x86
+ java_candidates.append(JRE64s);
+ java_candidates.append(NEWJRE64s);
+ java_candidates.append(ADOPTOPENJRE64s);
+ java_candidates.append(ADOPTIUMJRE64s);
+ java_candidates.append(
+ MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe"));
+ java_candidates.append(
+ MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe"));
+ java_candidates.append(
+ MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe"));
+ java_candidates.append(JDK64s);
+ java_candidates.append(NEWJDK64s);
+ java_candidates.append(ADOPTOPENJDK64s);
+ java_candidates.append(FOUNDATIONJDK64s);
+ java_candidates.append(ADOPTIUMJDK64s);
+ java_candidates.append(MICROSOFTJDK64s);
+ java_candidates.append(ZULU64s);
+ java_candidates.append(LIBERICA64s);
+
+ java_candidates.append(JRE32s);
+ java_candidates.append(NEWJRE32s);
+ java_candidates.append(ADOPTOPENJRE32s);
+ java_candidates.append(ADOPTIUMJRE32s);
+ java_candidates.append(
+ MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe"));
+ java_candidates.append(
+ MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe"));
+ java_candidates.append(
+ MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe"));
+ java_candidates.append(JDK32s);
+ java_candidates.append(NEWJDK32s);
+ java_candidates.append(ADOPTOPENJDK32s);
+ java_candidates.append(FOUNDATIONJDK32s);
+ java_candidates.append(ADOPTIUMJDK32s);
+ java_candidates.append(ZULU32s);
+ java_candidates.append(LIBERICA32s);
+
+ // Scan MeshMC's managed java directory
+ // ({workdir}/java/{vendor}/{version}/bin/javaw.exe)
+ QString managedJavaDir = JavaUtils::managedJavaRoot();
+ QDir managedDir(managedJavaDir);
+ if (managedDir.exists()) {
+ QDirIterator vendorIt(managedJavaDir,
+ QDir::Dirs | QDir::NoDotAndDotDot);
+ while (vendorIt.hasNext()) {
+ vendorIt.next();
+ QDirIterator versionIt(vendorIt.filePath(),
+ QDir::Dirs | QDir::NoDotAndDotDot);
+ while (versionIt.hasNext()) {
+ versionIt.next();
+ QDirIterator binIt(versionIt.filePath(),
+ QStringList() << "javaw.exe", QDir::Files,
+ QDirIterator::Subdirectories);
+ while (binIt.hasNext()) {
+ binIt.next();
+ if (binIt.filePath().contains("/bin/")) {
+ java_candidates.append(MakeJavaPtr(binIt.filePath()));
+ }
+ }
+ }
+ }
+ }
+
+ java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path));
+
+ QList<QString> candidates;
+ for (JavaInstallPtr java_candidate : java_candidates) {
+ if (!candidates.contains(java_candidate->path)) {
+ candidates.append(java_candidate->path);
+ }
+ }
+
+ return candidates;
+}
+
+#elif defined(Q_OS_MAC)
+QList<QString> JavaUtils::FindJavaPaths()
+{
+ QList<QString> javas;
+ appendUniquePath(javas, this->GetDefaultJava()->path);
+ appendJavaHome(javas,
+ QProcessEnvironment::systemEnvironment().value("JAVA_HOME"));
+ appendJavaHome(javas,
+ QProcessEnvironment::systemEnvironment().value("JDK_HOME"));
+ appendJavaHome(javas,
+ QProcessEnvironment::systemEnvironment().value("JRE_HOME"));
+ appendExecutablePath(
+ javas, "/Applications/Xcode.app/Contents/Applications/Application "
+ "Loader.app/Contents/MacOS/itms/java/bin/java");
+ appendJavaHome(
+ javas,
+ "/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home");
+ appendExecutablePath(javas, "/System/Library/Frameworks/JavaVM.framework/"
+ "Versions/Current/Commands/java");
+ scanJavaHomes(javas, "/Library/Java/JavaVirtualMachines");
+ scanJavaHomes(javas, "/System/Library/Java/JavaVirtualMachines");
+ scanJavaHomes(javas, "/opt/homebrew/opt");
+ scanJavaHomes(javas, "/usr/local/opt");
+ scanJavaHomes(javas, "/opt/jdk");
+ scanJavaHomes(javas, "/opt/jdks");
+ scanJavaHomes(javas, "/opt/java");
+ scanJavaHomes(javas, "/usr/local/java");
+ appendManagedJavaCandidates(javas, JavaUtils::managedJavaRoot());
+ return javas;
+}
+
+#elif defined(Q_OS_LINUX)
+QList<QString> JavaUtils::FindJavaPaths()
+{
+ qDebug() << "Linux Java detection incomplete - defaulting to \"java\"";
+
+ QList<QString> javas;
+ javas.append(this->GetDefaultJava()->path);
+ auto scanJavaDir = [&](const QString& dirPath) {
+ QDir dir(dirPath);
+ if (!dir.exists())
+ return;
+ auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot |
+ QDir::NoSymLinks);
+ for (auto& entry : entries) {
+
+ QString prefix;
+ if (entry.isAbsolute()) {
+ prefix = entry.absoluteFilePath();
+ } else {
+ prefix = entry.filePath();
+ }
+
+ javas.append(FS::PathCombine(prefix, "jre/bin/java"));
+ javas.append(FS::PathCombine(prefix, "bin/java"));
+ }
+ };
+ // oracle RPMs
+ scanJavaDir("/usr/java");
+ // general locations used by distro packaging
+ scanJavaDir("/usr/lib/jvm");
+ scanJavaDir("/usr/lib64/jvm");
+ scanJavaDir("/usr/lib32/jvm");
+ // javas stored in MeshMC's folder (recursive scan for managed
+ // java/{vendor}/{name}/... structure)
+ {
+ QDir javaBaseDir(JavaUtils::managedJavaRoot());
+ if (javaBaseDir.exists()) {
+ QDirIterator it(JavaUtils::managedJavaRoot(),
+ QStringList() << "java", QDir::Files,
+ QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ it.next();
+ if (it.filePath().contains("/bin/")) {
+ javas.append(it.filePath());
+ }
+ }
+ }
+ }
+ // manually installed JDKs in /opt
+ scanJavaDir("/opt/jdk");
+ scanJavaDir("/opt/jdks");
+ return javas;
+}
+#else
+QList<QString> JavaUtils::FindJavaPaths()
+{
+ qDebug() << "Unknown operating system build - defaulting to \"java\"";
+
+ QList<QString> javas;
+ javas.append(this->GetDefaultJava()->path);
+
+ return javas;
+}
+#endif
diff --git a/meshmc/launcher/java/JavaUtils.h b/meshmc/launcher/java/JavaUtils.h
new file mode 100644
index 0000000000..cfd2b9558d
--- /dev/null
+++ b/meshmc/launcher/java/JavaUtils.h
@@ -0,0 +1,71 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QStringList>
+
+#include "JavaChecker.h"
+#include "JavaInstallList.h"
+
+#ifdef Q_OS_WIN
+#include <windows.h>
+#endif
+
+QProcessEnvironment CleanEnviroment();
+
+class JavaUtils : public QObject
+{
+ Q_OBJECT
+ public:
+ JavaUtils();
+
+ static QString managedJavaRoot();
+
+ JavaInstallPtr MakeJavaPtr(QString path, QString id = "unknown",
+ QString arch = "unknown");
+ QList<QString> FindJavaPaths();
+ JavaInstallPtr GetDefaultJava();
+
+#ifdef Q_OS_WIN
+ QList<JavaInstallPtr> FindJavaFromRegistryKey(DWORD keyType,
+ QString keyName,
+ QString keyJavaDir,
+ QString subkeySuffix = "");
+#endif
+};
diff --git a/meshmc/launcher/java/JavaVersion.cpp b/meshmc/launcher/java/JavaVersion.cpp
new file mode 100644
index 0000000000..fe6fd21be8
--- /dev/null
+++ b/meshmc/launcher/java/JavaVersion.cpp
@@ -0,0 +1,138 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include "JavaVersion.h"
+#include <MMCStrings.h>
+
+#include <QRegularExpression>
+#include <QString>
+
+JavaVersion& JavaVersion::operator=(const QString& javaVersionString)
+{
+ m_string = javaVersionString;
+
+ auto getCapturedInteger = [](const QRegularExpressionMatch& match,
+ const QString& what) -> int {
+ auto str = match.captured(what);
+ if (str.isEmpty()) {
+ return 0;
+ }
+ return str.toInt();
+ };
+
+ QRegularExpression pattern;
+ if (javaVersionString.startsWith("1.")) {
+ pattern = QRegularExpression(
+ "1[.](?<major>[0-9]+)([.](?<minor>[0-9]+))?(_(?<security>[0-9]+)?)?"
+ "(-(?<prerelease>[a-zA-Z0-9]+))?");
+ } else {
+ pattern = QRegularExpression(
+ "(?<major>[0-9]+)([.](?<minor>[0-9]+))?([.](?<security>[0-9]+))?(-("
+ "?<prerelease>[a-zA-Z0-9]+))?");
+ }
+
+ auto match = pattern.match(m_string);
+ m_parseable = match.hasMatch();
+ m_major = getCapturedInteger(match, "major");
+ m_minor = getCapturedInteger(match, "minor");
+ m_security = getCapturedInteger(match, "security");
+ m_prerelease = match.captured("prerelease");
+ return *this;
+}
+
+JavaVersion::JavaVersion(const QString& rhs)
+{
+ operator=(rhs);
+}
+
+QString JavaVersion::toString() const
+{
+ return m_string;
+}
+
+bool JavaVersion::requiresPermGen() const
+{
+ if (m_parseable) {
+ return m_major < 8;
+ }
+ return true;
+}
+
+bool JavaVersion::operator<(const JavaVersion& rhs) const
+{
+ if (m_parseable && rhs.m_parseable) {
+ auto major = m_major;
+ auto rmajor = rhs.m_major;
+
+ // HACK: discourage using java 9
+ if (major > 8)
+ major = -major;
+ if (rmajor > 8)
+ rmajor = -rmajor;
+
+ if (major < rmajor)
+ return true;
+ if (major > rmajor)
+ return false;
+ if (m_minor < rhs.m_minor)
+ return true;
+ if (m_minor > rhs.m_minor)
+ return false;
+ if (m_security < rhs.m_security)
+ return true;
+ if (m_security > rhs.m_security)
+ return false;
+
+ // everything else being equal, consider prerelease status
+ bool thisPre = !m_prerelease.isEmpty();
+ bool rhsPre = !rhs.m_prerelease.isEmpty();
+ if (thisPre && !rhsPre) {
+ // this is a prerelease and the other one isn't -> lesser
+ return true;
+ } else if (!thisPre && rhsPre) {
+ // this isn't a prerelease and the other one is -> greater
+ return false;
+ } else if (thisPre && rhsPre) {
+ // both are prereleases - use natural compare...
+ return Strings::naturalCompare(m_prerelease, rhs.m_prerelease,
+ Qt::CaseSensitive) < 0;
+ }
+ // neither is prerelease, so they are the same -> this cannot be less
+ // than rhs
+ return false;
+ } else
+ return Strings::naturalCompare(m_string, rhs.m_string,
+ Qt::CaseSensitive) < 0;
+}
+
+bool JavaVersion::operator==(const JavaVersion& rhs) const
+{
+ if (m_parseable && rhs.m_parseable) {
+ return m_major == rhs.m_major && m_minor == rhs.m_minor &&
+ m_security == rhs.m_security && m_prerelease == rhs.m_prerelease;
+ }
+ return m_string == rhs.m_string;
+}
+
+bool JavaVersion::operator>(const JavaVersion& rhs) const
+{
+ return (!operator<(rhs)) && (!operator==(rhs));
+}
diff --git a/meshmc/launcher/java/JavaVersion.h b/meshmc/launcher/java/JavaVersion.h
new file mode 100644
index 0000000000..18ab256edb
--- /dev/null
+++ b/meshmc/launcher/java/JavaVersion.h
@@ -0,0 +1,73 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QString>
+
+// NOTE: apparently the GNU C library pollutes the global namespace with
+// these... undef them.
+#ifdef major
+#undef major
+#endif
+#ifdef minor
+#undef minor
+#endif
+
+class JavaVersion
+{
+ friend class JavaVersionTest;
+
+ public:
+ JavaVersion() {};
+ JavaVersion(const QString& rhs);
+
+ JavaVersion& operator=(const QString& rhs);
+
+ bool operator<(const JavaVersion& rhs) const;
+ bool operator==(const JavaVersion& rhs) const;
+ bool operator>(const JavaVersion& rhs) const;
+
+ bool requiresPermGen() const;
+
+ QString toString() const;
+
+ int major() const
+ {
+ return m_major;
+ }
+ int minor() const
+ {
+ return m_minor;
+ }
+ int security() const
+ {
+ return m_security;
+ }
+
+ private:
+ QString m_string;
+ int m_major = 0;
+ int m_minor = 0;
+ int m_security = 0;
+ bool m_parseable = false;
+ QString m_prerelease;
+};
diff --git a/meshmc/launcher/java/JavaVersion_test.cpp b/meshmc/launcher/java/JavaVersion_test.cpp
new file mode 100644
index 0000000000..2f9c88ea07
--- /dev/null
+++ b/meshmc/launcher/java/JavaVersion_test.cpp
@@ -0,0 +1,151 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTest>
+#include "TestUtil.h"
+
+#include "java/JavaVersion.h"
+
+class JavaVersionTest : public QObject
+{
+ Q_OBJECT
+ private slots:
+ void test_Parse_data()
+ {
+ QTest::addColumn<QString>("string");
+ QTest::addColumn<int>("major");
+ QTest::addColumn<int>("minor");
+ QTest::addColumn<int>("security");
+ QTest::addColumn<QString>("prerelease");
+
+ QTest::newRow("old format") << "1.6.0_33" << 6 << 0 << 33 << QString();
+ QTest::newRow("old format prerelease")
+ << "1.9.0_1-ea" << 9 << 0 << 1 << "ea";
+
+ QTest::newRow("new format major") << "9" << 9 << 0 << 0 << QString();
+ QTest::newRow("new format minor") << "9.1" << 9 << 1 << 0 << QString();
+ QTest::newRow("new format security")
+ << "9.0.1" << 9 << 0 << 1 << QString();
+ QTest::newRow("new format prerelease") << "9-ea" << 9 << 0 << 0 << "ea";
+ QTest::newRow("new format long prerelease")
+ << "9.0.1-ea" << 9 << 0 << 1 << "ea";
+ }
+ void test_Parse()
+ {
+ QFETCH(QString, string);
+ QFETCH(int, major);
+ QFETCH(int, minor);
+ QFETCH(int, security);
+ QFETCH(QString, prerelease);
+
+ JavaVersion test(string);
+ QCOMPARE(test.m_string, string);
+ QCOMPARE(test.toString(), string);
+ QCOMPARE(test.m_major, major);
+ QCOMPARE(test.m_minor, minor);
+ QCOMPARE(test.m_security, security);
+ QCOMPARE(test.m_prerelease, prerelease);
+ }
+
+ void test_Sort_data()
+ {
+ QTest::addColumn<QString>("lhs");
+ QTest::addColumn<QString>("rhs");
+ QTest::addColumn<bool>("smaller");
+ QTest::addColumn<bool>("equal");
+ QTest::addColumn<bool>("bigger");
+
+ // old format and new format equivalence
+ QTest::newRow("1.6.0_33 == 6.0.33")
+ << "1.6.0_33" << "6.0.33" << false << true << false;
+ // old format major version
+ QTest::newRow("1.5.0_33 < 1.6.0_33")
+ << "1.5.0_33" << "1.6.0_33" << true << false << false;
+ // new format - first release vs first security patch
+ QTest::newRow("9 < 9.0.1") << "9" << "9.0.1" << true << false << false;
+ QTest::newRow("9.0.1 > 9") << "9.0.1" << "9" << false << false << true;
+ // new format - first minor vs first release/security patch
+ QTest::newRow("9.1 > 9.0.1")
+ << "9.1" << "9.0.1" << false << false << true;
+ QTest::newRow("9.0.1 < 9.1")
+ << "9.0.1" << "9.1" << true << false << false;
+ QTest::newRow("9.1 > 9") << "9.1" << "9" << false << false << true;
+ QTest::newRow("9 > 9.1") << "9" << "9.1" << true << false << false;
+ // new format - omitted numbers
+ QTest::newRow("9 == 9.0") << "9" << "9.0" << false << true << false;
+ QTest::newRow("9 == 9.0.0") << "9" << "9.0.0" << false << true << false;
+ QTest::newRow("9.0 == 9.0.0")
+ << "9.0" << "9.0.0" << false << true << false;
+ // early access and prereleases compared to final release
+ QTest::newRow("9-ea < 9") << "9-ea" << "9" << true << false << false;
+ QTest::newRow("9 < 9.0.1-ea")
+ << "9" << "9.0.1-ea" << true << false << false;
+ QTest::newRow("9.0.1-ea > 9")
+ << "9.0.1-ea" << "9" << false << false << true;
+ // prerelease difference only testing
+ QTest::newRow("9-1 == 9-1") << "9-1" << "9-1" << false << true << false;
+ QTest::newRow("9-1 < 9-2") << "9-1" << "9-2" << true << false << false;
+ QTest::newRow("9-5 < 9-20")
+ << "9-5" << "9-20" << true << false << false;
+ QTest::newRow("9-rc1 < 9-rc2")
+ << "9-rc1" << "9-rc2" << true << false << false;
+ QTest::newRow("9-rc5 < 9-rc20")
+ << "9-rc5" << "9-rc20" << true << false << false;
+ QTest::newRow("9-rc < 9-rc2")
+ << "9-rc" << "9-rc2" << true << false << false;
+ QTest::newRow("9-ea < 9-rc")
+ << "9-ea" << "9-rc" << true << false << false;
+ }
+ void test_Sort()
+ {
+ QFETCH(QString, lhs);
+ QFETCH(QString, rhs);
+ QFETCH(bool, smaller);
+ QFETCH(bool, equal);
+ QFETCH(bool, bigger);
+ JavaVersion lver(lhs);
+ JavaVersion rver(rhs);
+ QCOMPARE(lver < rver, smaller);
+ QCOMPARE(lver == rver, equal);
+ QCOMPARE(lver > rver, bigger);
+ }
+ void test_PermGen_data()
+ {
+ QTest::addColumn<QString>("version");
+ QTest::addColumn<bool>("needs_permgen");
+ QTest::newRow("1.6.0_33") << "1.6.0_33" << true;
+ QTest::newRow("1.7.0_60") << "1.7.0_60" << true;
+ QTest::newRow("1.8.0_22") << "1.8.0_22" << false;
+ QTest::newRow("9-ea") << "9-ea" << false;
+ QTest::newRow("9.2.4") << "9.2.4" << false;
+ }
+ void test_PermGen()
+ {
+ QFETCH(QString, version);
+ QFETCH(bool, needs_permgen);
+ JavaVersion v(version);
+ QCOMPARE(needs_permgen, v.requiresPermGen());
+ }
+};
+
+QTEST_GUILESS_MAIN(JavaVersionTest)
+
+#include "JavaVersion_test.moc"
diff --git a/meshmc/launcher/java/download/JavaDownloadTask.cpp b/meshmc/launcher/java/download/JavaDownloadTask.cpp
new file mode 100644
index 0000000000..eb73c4d6d5
--- /dev/null
+++ b/meshmc/launcher/java/download/JavaDownloadTask.cpp
@@ -0,0 +1,344 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include "JavaDownloadTask.h"
+
+#include <QDir>
+#include <QProcess>
+#include <QDebug>
+#include <QDirIterator>
+#include <QFileInfo>
+
+#include "Application.h"
+#include "FileSystem.h"
+#include "Json.h"
+#include "net/Download.h"
+#include "net/ChecksumValidator.h"
+#include "MMCZip.h"
+
+JavaDownloadTask::JavaDownloadTask(const JavaDownload::RuntimeEntry& runtime,
+ const QString& targetDir, QObject* parent)
+ : Task(parent), m_runtime(runtime), m_targetDir(targetDir)
+{
+}
+
+void JavaDownloadTask::executeTask()
+{
+ if (m_runtime.downloadType == "manifest") {
+ downloadManifest();
+ } else {
+ downloadArchive();
+ }
+}
+
+void JavaDownloadTask::downloadArchive()
+{
+ setStatus(tr("Downloading %1...").arg(m_runtime.name));
+
+ // Determine archive extension and path
+ QUrl url(m_runtime.url);
+ QString filename = url.fileName();
+ m_archivePath = FS::PathCombine(m_targetDir, filename);
+
+ // Create target directory
+ if (!FS::ensureFolderPathExists(m_targetDir)) {
+ emitFailed(
+ tr("Failed to create target directory: %1").arg(m_targetDir));
+ return;
+ }
+
+ m_downloadJob = new NetJob(tr("Java download"), APPLICATION->network());
+
+ auto dl = Net::Download::makeFile(m_runtime.url, m_archivePath);
+
+ // Add checksum validation
+ if (!m_runtime.checksumHash.isEmpty()) {
+ QCryptographicHash::Algorithm algo = QCryptographicHash::Sha256;
+ if (m_runtime.checksumType == "sha1")
+ algo = QCryptographicHash::Sha1;
+ auto validator = new Net::ChecksumValidator(
+ algo, QByteArray::fromHex(m_runtime.checksumHash.toLatin1()));
+ dl->addValidator(validator);
+ }
+
+ m_downloadJob->addNetAction(dl);
+
+ connect(m_downloadJob.get(), &NetJob::succeeded, this,
+ &JavaDownloadTask::downloadFinished);
+ connect(m_downloadJob.get(), &NetJob::failed, this,
+ &JavaDownloadTask::downloadFailed);
+ connect(m_downloadJob.get(), &NetJob::progress, this, &Task::setProgress);
+
+ m_downloadJob->start();
+}
+
+void JavaDownloadTask::downloadFinished()
+{
+ m_downloadJob.reset();
+ extractArchive();
+}
+
+void JavaDownloadTask::downloadFailed(QString reason)
+{
+ m_downloadJob.reset();
+ // Clean up partial download
+ if (!m_archivePath.isEmpty() && !QFile::remove(m_archivePath))
+ qWarning() << "Failed to remove partial download:" << m_archivePath;
+ emitFailed(tr("Failed to download Java: %1").arg(reason));
+}
+
+void JavaDownloadTask::extractArchive()
+{
+ setStatus(tr("Extracting %1...").arg(m_runtime.name));
+
+ bool success = false;
+
+ if (m_archivePath.endsWith(".zip")) {
+ // Use QuaZip for zip files
+ auto result = MMCZip::extractDir(m_archivePath, m_targetDir);
+ success = result.has_value();
+ } else if (m_archivePath.endsWith(".tar.gz") ||
+ m_archivePath.endsWith(".tgz")) {
+ // Use system tar for tar.gz files
+ QProcess tarProcess;
+ tarProcess.setWorkingDirectory(m_targetDir);
+ tarProcess.start("tar", QStringList() << "xzf" << m_archivePath);
+ tarProcess.waitForFinished(300000); // 5 minute timeout
+ success = (tarProcess.exitCode() == 0 &&
+ tarProcess.exitStatus() == QProcess::NormalExit);
+ if (!success) {
+ qWarning() << "tar extraction failed:"
+ << tarProcess.readAllStandardError();
+ }
+ } else {
+ if (!QFile::remove(m_archivePath))
+ qWarning() << "Failed to remove archive:" << m_archivePath;
+ emitFailed(tr("Unsupported archive format: %1").arg(m_archivePath));
+ return;
+ }
+
+ // Clean up archive file
+ if (!QFile::remove(m_archivePath))
+ qWarning() << "Failed to remove archive:" << m_archivePath;
+
+ if (!success) {
+ emitFailed(tr("Failed to extract Java archive."));
+ return;
+ }
+
+ // Fix permissions on extracted files
+ QDirIterator it(m_targetDir, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ auto filepath = it.next();
+ QFileInfo file(filepath);
+ auto permissions = QFile::permissions(filepath);
+ if (file.isDir()) {
+ permissions |= QFileDevice::ReadUser | QFileDevice::WriteUser |
+ QFileDevice::ExeUser;
+ } else {
+ permissions |= QFileDevice::ReadUser | QFileDevice::WriteUser;
+ }
+ QFile::setPermissions(filepath, permissions);
+ }
+
+ // Find the java binary
+ m_installedJavaPath = findJavaBinary(m_targetDir);
+ if (m_installedJavaPath.isEmpty()) {
+ emitFailed(tr("Could not find java binary in extracted archive."));
+ return;
+ }
+
+ // Make java binary executable
+ auto perms = QFile::permissions(m_installedJavaPath) |
+ QFileDevice::ExeUser | QFileDevice::ExeGroup |
+ QFileDevice::ExeOther;
+ QFile::setPermissions(m_installedJavaPath, perms);
+
+ qDebug() << "Java installed successfully at:" << m_installedJavaPath;
+ emitSucceeded();
+}
+
+void JavaDownloadTask::downloadManifest()
+{
+ setStatus(tr("Downloading manifest for %1...").arg(m_runtime.name));
+
+ if (!FS::ensureFolderPathExists(m_targetDir)) {
+ emitFailed(
+ tr("Failed to create target directory: %1").arg(m_targetDir));
+ return;
+ }
+
+ m_downloadJob =
+ new NetJob(tr("Java manifest download"), APPLICATION->network());
+ auto dl =
+ Net::Download::makeByteArray(QUrl(m_runtime.url), &m_manifestData);
+
+ if (m_runtime.checksumType == "sha1" && !m_runtime.checksumHash.isEmpty()) {
+ dl->addValidator(new Net::ChecksumValidator(
+ QCryptographicHash::Sha1,
+ QByteArray::fromHex(m_runtime.checksumHash.toLatin1())));
+ }
+
+ m_downloadJob->addNetAction(dl);
+
+ connect(m_downloadJob.get(), &NetJob::succeeded, this,
+ &JavaDownloadTask::manifestDownloaded);
+ connect(m_downloadJob.get(), &NetJob::failed, this,
+ &JavaDownloadTask::downloadFailed);
+
+ m_downloadJob->start();
+}
+
+void JavaDownloadTask::manifestDownloaded()
+{
+ m_downloadJob.reset();
+
+ QJsonDocument doc;
+ try {
+ doc = Json::requireDocument(m_manifestData);
+ } catch (const Exception& e) {
+ m_manifestData.clear();
+ emitFailed(tr("Failed to parse Java manifest: %1").arg(e.cause()));
+ return;
+ }
+ m_manifestData.clear();
+
+ if (!doc.isObject()) {
+ emitFailed(tr("Failed to parse Java manifest."));
+ return;
+ }
+
+ auto files = doc.object()["files"].toObject();
+ m_executableFiles.clear();
+ m_linkEntries.clear();
+
+ // Create directories first
+ for (auto it = files.begin(); it != files.end(); ++it) {
+ auto entry = it.value().toObject();
+ if (entry["type"].toString() == "directory") {
+ QDir().mkpath(FS::PathCombine(m_targetDir, it.key()));
+ }
+ }
+
+ // Queue file downloads
+ setStatus(tr("Downloading %1 files...").arg(m_runtime.name));
+ m_downloadJob =
+ new NetJob(tr("Java runtime files"), APPLICATION->network());
+
+ for (auto it = files.begin(); it != files.end(); ++it) {
+ auto entry = it.value().toObject();
+
+ if (entry["type"].toString() == "file") {
+ auto downloads = entry["downloads"].toObject();
+ auto raw = downloads["raw"].toObject();
+
+ QString url = raw["url"].toString();
+ QString sha1 = raw["sha1"].toString();
+ QString filePath = FS::PathCombine(m_targetDir, it.key());
+
+ // Ensure parent directory exists
+ QFileInfo fi(filePath);
+ QDir().mkpath(fi.absolutePath());
+
+ auto dl = Net::Download::makeFile(QUrl(url), filePath);
+ if (!sha1.isEmpty()) {
+ dl->addValidator(new Net::ChecksumValidator(
+ QCryptographicHash::Sha1,
+ QByteArray::fromHex(sha1.toLatin1())));
+ }
+ m_downloadJob->addNetAction(dl);
+
+ if (entry["executable"].toBool()) {
+ m_executableFiles.append(filePath);
+ }
+ } else if (entry["type"].toString() == "link") {
+ m_linkEntries.append({it.key(), entry["target"].toString()});
+ }
+ }
+
+ connect(m_downloadJob.get(), &NetJob::succeeded, this,
+ &JavaDownloadTask::manifestFilesDownloaded);
+ connect(m_downloadJob.get(), &NetJob::failed, this,
+ &JavaDownloadTask::downloadFailed);
+ connect(m_downloadJob.get(), &NetJob::progress, this, &Task::setProgress);
+
+ m_downloadJob->start();
+}
+
+void JavaDownloadTask::manifestFilesDownloaded()
+{
+ m_downloadJob.reset();
+
+ // Create symlinks
+ for (const auto& link : m_linkEntries) {
+ QString linkPath = FS::PathCombine(m_targetDir, link.first);
+ QFileInfo fi(linkPath);
+ QDir().mkpath(fi.absolutePath());
+ QFile::link(link.second, linkPath);
+ }
+
+ // Set executable permissions
+ for (const auto& path : m_executableFiles) {
+ QFile::setPermissions(
+ path, QFile::permissions(path) | QFileDevice::ExeUser |
+ QFileDevice::ExeGroup | QFileDevice::ExeOther);
+ }
+
+ // Find java binary
+ m_installedJavaPath = findJavaBinary(m_targetDir);
+ if (m_installedJavaPath.isEmpty()) {
+ emitFailed(tr("Could not find java binary in downloaded runtime."));
+ return;
+ }
+
+ qDebug() << "Java installed successfully at:" << m_installedJavaPath;
+ emitSucceeded();
+}
+
+QString JavaDownloadTask::findJavaBinary(const QString& dir) const
+{
+#if defined(Q_OS_WIN)
+ QString binaryName = "javaw.exe";
+#else
+ QString binaryName = "java";
+#endif
+
+ // Search for java binary in bin/ subdirectories
+ QDirIterator it(dir, QStringList() << binaryName, QDir::Files,
+ QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ it.next();
+ QString path = it.filePath();
+ if (path.contains("/bin/")) {
+ return path;
+ }
+ }
+
+ // Fallback: any match
+ QDirIterator it2(dir, QStringList() << binaryName, QDir::Files,
+ QDirIterator::Subdirectories);
+ if (it2.hasNext()) {
+ it2.next();
+ return it2.filePath();
+ }
+
+ return QString();
+}
diff --git a/meshmc/launcher/java/download/JavaDownloadTask.h b/meshmc/launcher/java/download/JavaDownloadTask.h
new file mode 100644
index 0000000000..88df47639b
--- /dev/null
+++ b/meshmc/launcher/java/download/JavaDownloadTask.h
@@ -0,0 +1,68 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+#include "java/download/JavaRuntime.h"
+
+#include <QUrl>
+
+class JavaDownloadTask : public Task
+{
+ Q_OBJECT
+
+ public:
+ explicit JavaDownloadTask(const JavaDownload::RuntimeEntry& runtime,
+ const QString& targetDir,
+ QObject* parent = nullptr);
+ virtual ~JavaDownloadTask() = default;
+
+ QString installedJavaPath() const
+ {
+ return m_installedJavaPath;
+ }
+
+ protected:
+ void executeTask() override;
+
+ private slots:
+ void downloadFinished();
+ void downloadFailed(QString reason);
+ void extractArchive();
+ void manifestDownloaded();
+ void manifestFilesDownloaded();
+
+ private:
+ void downloadArchive();
+ void downloadManifest();
+ QString findJavaBinary(const QString& dir) const;
+
+ JavaDownload::RuntimeEntry m_runtime;
+ QString m_targetDir;
+ QString m_archivePath;
+ QString m_installedJavaPath;
+ NetJob::Ptr m_downloadJob;
+ QByteArray m_manifestData;
+ QStringList m_executableFiles;
+ QList<QPair<QString, QString>> m_linkEntries;
+};
diff --git a/meshmc/launcher/java/download/JavaRuntime.h b/meshmc/launcher/java/download/JavaRuntime.h
new file mode 100644
index 0000000000..161c0a629b
--- /dev/null
+++ b/meshmc/launcher/java/download/JavaRuntime.h
@@ -0,0 +1,169 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QString>
+#include <QList>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonDocument>
+
+namespace JavaDownload
+{
+
+ struct RuntimeVersion {
+ int major = 0;
+ int minor = 0;
+ int security = 0;
+ int build = 0;
+
+ QString toString() const
+ {
+ if (build > 0)
+ return QString("%1.%2.%3+%4")
+ .arg(major)
+ .arg(minor)
+ .arg(security)
+ .arg(build);
+ return QString("%1.%2.%3").arg(major).arg(minor).arg(security);
+ }
+ };
+
+ struct RuntimeEntry {
+ QString name;
+ QString url;
+ QString checksumHash;
+ QString checksumType;
+ QString downloadType;
+ QString packageType;
+ QString releaseTime;
+ QString runtimeOS;
+ QString vendor;
+ RuntimeVersion version;
+ };
+
+ struct JavaVersionInfo {
+ QString uid;
+ QString versionId;
+ QString name;
+ QString sha256;
+ QString releaseTime;
+ bool recommended = false;
+ };
+
+ struct JavaProviderInfo {
+ QString uid;
+ QString name;
+ QString iconName;
+
+ static QList<JavaProviderInfo> availableProviders()
+ {
+ return {
+ {"net.adoptium.java", "Eclipse Adoptium", "adoptium"},
+ {"com.azul.java", "Azul Zulu", "azul"},
+ {"com.ibm.java", "IBM Semeru (OpenJ9)", "openj9_hex_custom"},
+ {"net.minecraft.java", "Mojang", "mojang"},
+ };
+ }
+ };
+
+ inline QString currentRuntimeOS()
+ {
+#if defined(Q_OS_LINUX)
+#if defined(__aarch64__)
+ return "linux-arm64";
+#elif defined(__riscv)
+ return "linux-riscv64";
+#else
+ return "linux-x64";
+#endif
+#elif defined(Q_OS_MACOS)
+#if defined(__aarch64__)
+ return "mac-os-arm64";
+#else
+ return "mac-os-x64";
+#endif
+#elif defined(Q_OS_WIN)
+#if defined(__aarch64__) || defined(_M_ARM64)
+ return "windows-arm64";
+#else
+ return "windows-x64";
+#endif
+#else
+ return "unknown";
+#endif
+ }
+
+ inline RuntimeEntry parseRuntimeEntry(const QJsonObject& obj)
+ {
+ RuntimeEntry entry;
+ entry.name = obj["name"].toString();
+ entry.url = obj["url"].toString();
+ entry.downloadType = obj["downloadType"].toString();
+ entry.packageType = obj["packageType"].toString();
+ entry.releaseTime = obj["releaseTime"].toString();
+ entry.runtimeOS = obj["runtimeOS"].toString();
+ entry.vendor = obj["vendor"].toString();
+
+ auto checksum = obj["checksum"].toObject();
+ entry.checksumHash = checksum["hash"].toString();
+ entry.checksumType = checksum["type"].toString();
+
+ auto ver = obj["version"].toObject();
+ entry.version.major = ver["major"].toInt();
+ entry.version.minor = ver["minor"].toInt();
+ entry.version.security = ver["security"].toInt();
+ entry.version.build = ver["build"].toInt();
+
+ return entry;
+ }
+
+ inline QList<RuntimeEntry> parseRuntimes(const QJsonObject& obj)
+ {
+ QList<RuntimeEntry> entries;
+ auto arr = obj["runtimes"].toArray();
+ for (const auto& val : arr) {
+ entries.append(parseRuntimeEntry(val.toObject()));
+ }
+ return entries;
+ }
+
+ inline QList<JavaVersionInfo> parseVersionIndex(const QJsonObject& obj,
+ const QString& uid)
+ {
+ QList<JavaVersionInfo> versions;
+ auto arr = obj["versions"].toArray();
+ for (const auto& val : arr) {
+ auto vObj = val.toObject();
+ JavaVersionInfo info;
+ info.uid = uid;
+ info.versionId = vObj["version"].toString();
+ info.sha256 = vObj["sha256"].toString();
+ info.releaseTime = vObj["releaseTime"].toString();
+ info.recommended = vObj["recommended"].toBool(false);
+ info.name = obj["name"].toString();
+ versions.append(info);
+ }
+ return versions;
+ }
+
+} // namespace JavaDownload