fix(appimage): launch updater with bundled linker (#4425)

This commit is contained in:
Seth Flynn 2025-12-10 23:30:21 -05:00 committed by GitHub
commit 30f24dae11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 167 additions and 96 deletions

View file

@ -2,6 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 dada513 <dada513@protonmail.com> * Copyright (C) 2022 dada513 <dada513@protonmail.com>
* Copyright (C) 2025 Seth Flynn <getchoo@tuta.io>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -75,6 +76,15 @@ bool isFlatpak()
#endif #endif
} }
bool isSelfContained()
{
#ifdef Q_OS_LINUX
return QFileInfo(QCoreApplication::applicationFilePath()).fileName().startsWith("ld-linux");
#else
return false;
#endif
}
bool isSnap() bool isSnap()
{ {
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX

View file

@ -37,6 +37,11 @@ bool openUrl(const QUrl& url);
*/ */
bool isFlatpak(); bool isFlatpak();
/**
* Determine whether the launcher is running in a self-contained Linux bundle
*/
bool isSelfContained();
/** /**
* Determine whether the launcher is running in a Snap environment * Determine whether the launcher is running in a Snap environment
*/ */

View file

@ -4,6 +4,7 @@
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
* Copyright (C) 2025 Seth Flynn <getchoo@tuta.io>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -772,6 +773,34 @@ QString ResolveExecutable(QString path)
return pathInfo.absoluteFilePath(); return pathInfo.absoluteFilePath();
} }
std::unique_ptr<QProcess> createProcess(const QString& program, const QStringList& arguments)
{
qDebug() << "Creating process for" << program;
auto proc = std::unique_ptr<QProcess>(new QProcess());
#if defined(Q_OS_LINUX)
if (DesktopServices::isSelfContained()) {
const auto linkerPath = QCoreApplication::applicationFilePath();
qDebug() << "Wrapping" << program << "with self-contained linker at" << linkerPath;
QStringList wrappedArguments;
wrappedArguments << "--inhibit-cache" << program;
wrappedArguments += arguments;
proc->setProgram(linkerPath);
proc->setArguments(wrappedArguments);
} else {
proc->setProgram(program);
proc->setArguments(arguments);
}
#else
proc->setProgram(program);
proc->setArguments(arguments);
#endif
return proc;
}
/** /**
* Normalize path * Normalize path
* *

View file

@ -4,6 +4,7 @@
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
* Copyright (C) 2025 Seth Flynn <getchoo@tuta.io>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -42,11 +43,13 @@
#include <system_error> #include <system_error>
#include <QCoreApplication>
#include <QDir> #include <QDir>
#include <QFlags> #include <QFlags>
#include <QLocalServer> #include <QLocalServer>
#include <QObject> #include <QObject>
#include <QPair> #include <QPair>
#include <QProcess>
#include <QThread> #include <QThread>
namespace FS { namespace FS {
@ -333,6 +336,14 @@ QString pathTruncate(const QString& path, int depth);
*/ */
QString ResolveExecutable(QString path); QString ResolveExecutable(QString path);
/**
* Create a QProcess instance
*
* This wrapper is currently only required for wrapping binaries called in
* self-contained AppImages (like those created by `go-appimage`)
*/
std::unique_ptr<QProcess> createProcess(const QString& program, const QStringList& arguments);
/** /**
* Normalize path * Normalize path
* *

View file

@ -26,7 +26,6 @@
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QMessageBox> #include <QMessageBox>
#include <QProcess>
#include <QProgressDialog> #include <QProgressDialog>
#include <QSettings> #include <QSettings>
#include <QTimer> #include <QTimer>
@ -35,6 +34,7 @@
#include "StringUtils.h" #include "StringUtils.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include "FileSystem.h"
#include "ui/dialogs/UpdateAvailableDialog.h" #include "ui/dialogs/UpdateAvailableDialog.h"
@ -97,14 +97,9 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser)
progress.show(); progress.show();
QCoreApplication::processEvents(); QCoreApplication::processEvents();
QProcess proc;
auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME);
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
exe_name.append(".exe"); exe_name.append(".exe");
auto env = QProcessEnvironment::systemEnvironment();
env.insert("__COMPAT_LAYER", "RUNASINVOKER");
proc.setProcessEnvironment(env);
#else #else
exe_name = QString("bin/%1").arg(exe_name); exe_name = QString("bin/%1").arg(exe_name);
#endif #endif
@ -113,15 +108,21 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser)
if (priv->allowBeta) if (priv->allowBeta)
args.append("--pre-release"); args.append("--pre-release");
proc.start(priv->appDir.absoluteFilePath(exe_name), args); auto proc = FS::createProcess(priv->appDir.absoluteFilePath(exe_name), args);
auto result_start = proc.waitForStarted(5000); #if defined Q_OS_WIN32
auto env = QProcessEnvironment::systemEnvironment();
env.insert("__COMPAT_LAYER", "RUNASINVOKER");
proc->setProcessEnvironment(env);
#endif
proc->start(proc->program(), proc->arguments());
auto result_start = proc->waitForStarted(5000);
if (!result_start) { if (!result_start) {
auto err = proc.error(); auto err = proc->error();
qDebug() << "Failed to start updater after 5 seconds." qDebug() << "Failed to start updater after 5 seconds."
<< "reason:" << err << proc.errorString(); << "reason:" << err << proc->errorString();
auto msgBox = auto msgBox =
QMessageBox(QMessageBox::Information, tr("Update Check Failed"), QMessageBox(QMessageBox::Information, tr("Update Check Failed"),
tr("Failed to start after 5 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent); tr("Failed to start after 5 seconds\nReason: %1.").arg(proc->errorString()), QMessageBox::Ok, priv->parent);
msgBox.setMinimumWidth(460); msgBox.setMinimumWidth(460);
msgBox.adjustSize(); msgBox.adjustSize();
msgBox.exec(); msgBox.exec();
@ -133,16 +134,16 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser)
} }
QCoreApplication::processEvents(); QCoreApplication::processEvents();
auto result_finished = proc.waitForFinished(60000); auto result_finished = proc->waitForFinished(60000);
if (!result_finished) { if (!result_finished) {
proc.kill(); proc->kill();
auto err = proc.error(); auto err = proc->error();
auto output = proc.readAll(); auto output = proc->readAll();
qDebug() << "Updater failed to close after 60 seconds." qDebug() << "Updater failed to close after 60 seconds."
<< "reason:" << err << proc.errorString(); << "reason:" << err << proc->errorString();
auto msgBox = auto msgBox =
QMessageBox(QMessageBox::Information, tr("Update Check Failed"), QMessageBox(QMessageBox::Information, tr("Update Check Failed"),
tr("Updater failed to close 60 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent); tr("Updater failed to close 60 seconds\nReason: %1.").arg(proc->errorString()), QMessageBox::Ok, priv->parent);
msgBox.setDetailedText(output); msgBox.setDetailedText(output);
msgBox.setMinimumWidth(460); msgBox.setMinimumWidth(460);
msgBox.adjustSize(); msgBox.adjustSize();
@ -154,10 +155,10 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser)
return; return;
} }
auto exit_code = proc.exitCode(); auto exit_code = proc->exitCode();
auto std_output = proc.readAllStandardOutput(); auto std_output = proc->readAllStandardOutput();
auto std_error = proc.readAllStandardError(); auto std_error = proc->readAllStandardError();
progress.hide(); progress.hide();
QCoreApplication::processEvents(); QCoreApplication::processEvents();
@ -335,14 +336,9 @@ void PrismExternalUpdater::offerUpdate(const QString& version_name, const QStrin
void PrismExternalUpdater::performUpdate(const QString& version_tag) void PrismExternalUpdater::performUpdate(const QString& version_tag)
{ {
QProcess proc;
auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME);
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
exe_name.append(".exe"); exe_name.append(".exe");
auto env = QProcessEnvironment::systemEnvironment();
env.insert("__COMPAT_LAYER", "RUNASINVOKER");
proc.setProcessEnvironment(env);
#else #else
exe_name = QString("bin/%1").arg(exe_name); exe_name = QString("bin/%1").arg(exe_name);
#endif #endif
@ -351,9 +347,16 @@ void PrismExternalUpdater::performUpdate(const QString& version_tag)
if (priv->allowBeta) if (priv->allowBeta)
args.append("--pre-release"); args.append("--pre-release");
auto result = proc.startDetached(priv->appDir.absoluteFilePath(exe_name), args); auto proc = FS::createProcess(exe_name, args);
#if defined Q_OS_WIN32
auto env = QProcessEnvironment::systemEnvironment();
env.insert("__COMPAT_LAYER", "RUNASINVOKER");
proc->setProcessEnvironment(env);
#endif
auto result = proc->startDetached(priv->appDir.absoluteFilePath(exe_name), args);
if (!result) { if (!result) {
qDebug() << "Failed to start updater:" << proc.error() << proc.errorString(); qDebug() << "Failed to start updater:" << proc->error() << proc->errorString();
} }
QCoreApplication::exit(); QCoreApplication::exit();
} }

View file

@ -123,72 +123,20 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
logToConsole = parser.isSet("debug"); logToConsole = parser.isSet("debug");
auto updater_executable = QCoreApplication::applicationFilePath();
#ifdef Q_OS_MACOS
showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS"));
#endif
if (updater_executable.startsWith("/tmp/.mount_")) {
m_isAppimage = true;
m_appimagePath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE"));
if (m_appimagePath.isEmpty()) {
showFatalErrorMessage(tr("Unsupported Installation"),
tr("Updater is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)"));
}
}
m_isFlatpak = DesktopServices::isFlatpak();
QString prism_executable = FS::PathCombine(applicationDirPath(), BuildConfig.LAUNCHER_APP_BINARY_NAME);
#if defined Q_OS_WIN32
prism_executable.append(".exe");
#endif
if (!QFileInfo(prism_executable).isFile()) {
showFatalErrorMessage(tr("Unsupported Installation"), tr("The updater can not find the main executable."));
}
m_prismExecutable = prism_executable;
auto prism_update_url = parser.value("update-url");
if (prism_update_url.isEmpty())
prism_update_url = BuildConfig.UPDATER_GITHUB_REPO;
m_prismRepoUrl = QUrl::fromUserInput(prism_update_url);
m_checkOnly = parser.isSet("check-only");
m_forceUpdate = parser.isSet("force");
m_printOnly = parser.isSet("list");
auto user_version = parser.value("install-version");
if (!user_version.isEmpty()) {
m_userSelectedVersion = Version(user_version);
}
m_selectUI = parser.isSet("select-ui");
m_allowDowngrade = parser.isSet("allow-downgrade");
auto version = parser.value("prism-version");
if (!version.isEmpty()) {
if (version.contains('-')) {
auto index = version.indexOf('-');
m_prsimVersionChannel = version.mid(index + 1);
version = version.left(index);
} else {
m_prsimVersionChannel = "stable";
}
auto version_parts = version.split('.');
m_prismVersionMajor = version_parts.takeFirst().toInt();
m_prismVersionMinor = version_parts.takeFirst().toInt();
if (!version_parts.isEmpty())
m_prismVersionPatch = version_parts.takeFirst().toInt();
else
m_prismVersionPatch = 0;
}
m_allowPreRelease = parser.isSet("pre-release");
QString origCwdPath = QDir::currentPath(); QString origCwdPath = QDir::currentPath();
#if defined(Q_OS_LINUX)
// NOTE(@getchoo): In order for `go-appimage` to generate self-contained AppImages, it executes apps from a bundled linker at
// <root>/lib64
// This is not the path to our actual binary, which we want
QString binPath;
if (DesktopServices::isSelfContained()) {
binPath = FS::PathCombine(applicationDirPath(), "../usr/bin");
} else {
binPath = applicationDirPath();
}
#else
QString binPath = applicationDirPath(); QString binPath = applicationDirPath();
#endif
{ // find data director { // find data director
// Root path is used for updates and portable data // Root path is used for updates and portable data
@ -363,6 +311,68 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
m_network->setProxy(proxy); m_network->setProxy(proxy);
} }
#ifdef Q_OS_MACOS
showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS"));
#endif
if (binPath.startsWith("/tmp/.mount_")) {
m_isAppimage = true;
m_appimagePath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE"));
if (m_appimagePath.isEmpty()) {
showFatalErrorMessage(tr("Unsupported Installation"),
tr("Updater is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)"));
}
}
m_isFlatpak = DesktopServices::isFlatpak();
QString prism_executable = FS::PathCombine(binPath, BuildConfig.LAUNCHER_APP_BINARY_NAME);
#if defined Q_OS_WIN32
prism_executable.append(".exe");
#endif
if (!QFileInfo(prism_executable).isFile()) {
showFatalErrorMessage(tr("Unsupported Installation"), tr("The updater can not find the main executable."));
}
m_prismExecutable = prism_executable;
auto prism_update_url = parser.value("update-url");
if (prism_update_url.isEmpty())
prism_update_url = BuildConfig.UPDATER_GITHUB_REPO;
m_prismRepoUrl = QUrl::fromUserInput(prism_update_url);
m_checkOnly = parser.isSet("check-only");
m_forceUpdate = parser.isSet("force");
m_printOnly = parser.isSet("list");
auto user_version = parser.value("install-version");
if (!user_version.isEmpty()) {
m_userSelectedVersion = Version(user_version);
}
m_selectUI = parser.isSet("select-ui");
m_allowDowngrade = parser.isSet("allow-downgrade");
auto version = parser.value("prism-version");
if (!version.isEmpty()) {
if (version.contains('-')) {
auto index = version.indexOf('-');
m_prsimVersionChannel = version.mid(index + 1);
version = version.left(index);
} else {
m_prsimVersionChannel = "stable";
}
auto version_parts = version.split('.');
m_prismVersionMajor = version_parts.takeFirst().toInt();
m_prismVersionMinor = version_parts.takeFirst().toInt();
if (!version_parts.isEmpty())
m_prismVersionPatch = version_parts.takeFirst().toInt();
else
m_prismVersionPatch = 0;
}
m_allowPreRelease = parser.isSet("pre-release");
auto marker_file_path = QDir(m_rootPath).absoluteFilePath(".prism_launcher_updater_unpack.marker"); auto marker_file_path = QDir(m_rootPath).absoluteFilePath(".prism_launcher_updater_unpack.marker");
auto marker_file = QFileInfo(marker_file_path); auto marker_file = QFileInfo(marker_file_path);
if (marker_file.exists()) { if (marker_file.exists()) {
@ -809,13 +819,16 @@ QFileInfo PrismUpdaterApp::downloadAsset(const GitHubReleaseAsset& asset)
bool PrismUpdaterApp::callAppImageUpdate() bool PrismUpdaterApp::callAppImageUpdate()
{ {
auto appimage_path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); auto appimage_path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE"));
QProcess proc = QProcess();
qDebug() << "Calling: AppImageUpdate" << appimage_path; qDebug() << "Calling: AppImageUpdate" << appimage_path;
proc.setProgram(FS::PathCombine(m_rootPath, "bin", "AppImageUpdate.AppImage")); const auto program = FS::PathCombine(m_rootPath, "bin", "AppImageUpdate.AppImage");
proc.setArguments({ appimage_path }); auto proc = FS::createProcess(program, { appimage_path });
auto result = proc.startDetached(); if (!proc) {
qCritical() << "Unable to create process:" << program;
return false;
}
auto result = proc->startDetached();
if (!result) if (!result)
qDebug() << "Failed to start AppImageUpdate reason:" << proc.errorString(); qDebug() << "Failed to start AppImageUpdate reason:" << proc->errorString();
return result; return result;
} }