summaryrefslogtreecommitdiff
path: root/meshmc/launcher/java/download
diff options
context:
space:
mode:
Diffstat (limited to 'meshmc/launcher/java/download')
-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
3 files changed, 581 insertions, 0 deletions
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