diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
| commit | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch) | |
| tree | 8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/java | |
| parent | 934382c8a1ce738589dee9ee0f14e1cec812770e (diff) | |
| parent | fad6a1066616b69d7f5fef01178efdf014c59537 (diff) | |
| download | Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip | |
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc
git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e
git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/launcher/java')
| -rw-r--r-- | meshmc/launcher/java/JavaChecker.cpp | 189 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaChecker.h | 80 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaCheckerJob.cpp | 66 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaCheckerJob.h | 85 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaInstall.cpp | 48 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaInstall.h | 61 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaInstallList.cpp | 219 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaInstallList.h | 100 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaUtils.cpp | 591 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaUtils.h | 71 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaVersion.cpp | 138 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaVersion.h | 73 | ||||
| -rw-r--r-- | meshmc/launcher/java/JavaVersion_test.cpp | 151 | ||||
| -rw-r--r-- | meshmc/launcher/java/download/JavaDownloadTask.cpp | 344 | ||||
| -rw-r--r-- | meshmc/launcher/java/download/JavaDownloadTask.h | 68 | ||||
| -rw-r--r-- | meshmc/launcher/java/download/JavaRuntime.h | 169 |
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 |
