mirror of
https://invent.kde.org/plasma/plasma-desktop.git
synced 2026-11-03 14:49:44 -09:00
kcm/runners: Allow simpler installation of DBus runners
While the install.sh script that is part of the python templates worked great, it still is visually annoying to have a terminal window open when we just need to copy/generate two files. With a krunner-plugininstallerrc file, consumers can provide instructions on how to generate the dbus service and where to find the desktop file for the runner. This is intended for python runners, but can work with any other language or even prebuild binaries. In the future, we might want to add the possibility to add dependencies. But for now, the current state should suffice. Also, we could adjust the installation without having to edit the KRunner template and lots of existing KRunner plugins that are already published. Since there are a few moving parts involved, an autotest was added to ensure the files end up in the correct destination. This also checks the dynamically generated content of the service and tests the one given line of the desktop file
This commit is contained in:
parent
c7fc01c22c
commit
e170f15429
14 changed files with 258 additions and 15 deletions
|
|
@ -6,3 +6,7 @@ Source: https://invent.kde.org/plasma/plasma-desktop/
|
|||
Files: emojier/data/*.dict
|
||||
Copyright: Unicode, Inc.
|
||||
License: Unicode-DFS-2016
|
||||
|
||||
Files: kcms/runners/plugininstaller/autotests
|
||||
Copyright: Alexander Lohnau
|
||||
License: CC0-1.0
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
|
|||
IconThemes
|
||||
XmlGui
|
||||
)
|
||||
if(BUILD_TESTING)
|
||||
find_package(Qt6Test ${QT_MIN_VERSION} CONFIG REQUIRED)
|
||||
include(ECMAddTests)
|
||||
endif()
|
||||
|
||||
# TODO re-enable once KAccounts is ported to Qt6
|
||||
# find_package(AccountsQt6 ${ACCOUNTSQT_DEP_VERSION} CONFIG)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED Test)
|
||||
|
||||
include(ECMAddTests)
|
||||
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..)
|
||||
|
||||
ecm_add_tests(
|
||||
|
|
|
|||
|
|
@ -145,7 +145,6 @@ configure_file(config-keyboard.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-keyboa
|
|||
|
||||
# Unit tests
|
||||
if(BUILD_TESTING)
|
||||
find_package(Qt6Test ${QT_MIN_VERSION} CONFIG REQUIRED)
|
||||
add_subdirectory( tests )
|
||||
endif()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,17 @@ add_executable(krunner-plugininstaller
|
|||
main.cpp
|
||||
AbstractJob.cpp
|
||||
ScriptJob.cpp
|
||||
ZypperRPMJob.cpp)
|
||||
ZypperRPMJob.cpp
|
||||
KRunnerPluginInstallerRcJob.cpp
|
||||
)
|
||||
if(HAVE_PACKAGEKIT)
|
||||
target_sources(krunner-plugininstaller PUBLIC PackageKitJob.cpp)
|
||||
endif()
|
||||
|
||||
target_link_libraries(krunner-plugininstaller
|
||||
Qt::Widgets
|
||||
KF6::CoreAddons
|
||||
KF6::I18n
|
||||
Qt::Widgets
|
||||
KF6::Service
|
||||
KF6::KIOCore
|
||||
KF6::KIOWidgets
|
||||
|
|
@ -22,4 +24,8 @@ if(HAVE_PACKAGEKIT)
|
|||
target_link_libraries(krunner-plugininstaller PK::packagekitqt6)
|
||||
endif()
|
||||
|
||||
if (BUILD_TESTING)
|
||||
add_subdirectory(autotests)
|
||||
endif()
|
||||
|
||||
install(TARGETS krunner-plugininstaller DESTINATION ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
|
|
|
|||
108
kcms/runners/plugininstaller/KRunnerPluginInstallerRcJob.cpp
Normal file
108
kcms/runners/plugininstaller/KRunnerPluginInstallerRcJob.cpp
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
#include "KRunnerPlugininstallerRcJob.h"
|
||||
|
||||
#include "ScriptConfirmationDialog.h"
|
||||
#include <KDesktopFile>
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
|
||||
static QString installDBusServicePath(const QString &dbusService)
|
||||
{
|
||||
const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
|
||||
QDir dbusDir(dataLocation + "/dbus-1/services/");
|
||||
dbusDir.mkpath(("."));
|
||||
return dbusDir.filePath(dbusService + ".service");
|
||||
}
|
||||
|
||||
static QString installMetaDataPath(const QString &filePath)
|
||||
{
|
||||
const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
|
||||
QDir krunnerPluginsDir(dataLocation + "/krunner/dbusplugins/");
|
||||
krunnerPluginsDir.mkpath(("."));
|
||||
// Get the filename in case the .krunner-plugininstallerrc has specified the file to be in a subdir
|
||||
return krunnerPluginsDir.filePath(QFileInfo(filePath).fileName());
|
||||
}
|
||||
|
||||
static InstallerInfo getInstallerInfo(const QFileInfo dirInfo)
|
||||
{
|
||||
const QDir rootDir = dirInfo.absoluteDir();
|
||||
QString installerConfigFile = dirInfo.filePath();
|
||||
Q_ASSERT(installerConfigFile.endsWith("krunner-plugininstallerrc"));
|
||||
|
||||
KConfig cfg(installerConfigFile);
|
||||
KConfigGroup grp = cfg.group("");
|
||||
const QString exec = grp.readEntry("Exec").replace(QStringLiteral("%{PROJECTDIR}"), dirInfo.absolutePath());
|
||||
const QString metaDataFile = grp.readEntry("MetaDataFile");
|
||||
KDesktopFile desktopFile(rootDir.absoluteFilePath(metaDataFile));
|
||||
QString dbusService = desktopFile.desktopGroup().readEntry("X-Plasma-DBusRunner-Service");
|
||||
|
||||
return {exec, dbusService, rootDir.absoluteFilePath(metaDataFile)};
|
||||
}
|
||||
|
||||
void KRunnerPluginInstallerRcJob::executeOperation(const QFileInfo &fileInfo, const QString & /*mimeType*/, bool install)
|
||||
{
|
||||
const InstallerInfo info = getInstallerInfo(fileInfo);
|
||||
if (info.exec.isEmpty() || info.desktopFilePath.isEmpty() || info.desktopFilePath.isEmpty()) {
|
||||
Q_EMIT error("The provided installer file is invalid. Please contact the author.");
|
||||
return;
|
||||
}
|
||||
if (install) {
|
||||
if (m_skipConfirmDialog || ScriptConfirmationDialog(QString(), install, fileInfo.absolutePath(), info).exec() == QDialog::Accepted) {
|
||||
doInstall(info);
|
||||
} else {
|
||||
Q_EMIT error(QString());
|
||||
}
|
||||
} else {
|
||||
doUninstall(info);
|
||||
}
|
||||
}
|
||||
|
||||
void KRunnerPluginInstallerRcJob::doInstall(const InstallerInfo &info)
|
||||
{
|
||||
QFile serviceFile(installDBusServicePath(info.dbusService));
|
||||
|
||||
if (serviceFile.open(QIODevice::WriteOnly)) {
|
||||
serviceFile.write(
|
||||
QLatin1String("# autogenerated by krunner-plugininstaller\n[D-BUS Service]\nName=%1\nExec=%2\n").arg(info.dbusService, info.exec).toLocal8Bit());
|
||||
serviceFile.close();
|
||||
qInfo() << "Wrote autostart file" << serviceFile.fileName();
|
||||
} else {
|
||||
Q_EMIT error(serviceFile.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
const QString targetFilePath = installMetaDataPath(info.desktopFilePath);
|
||||
// In case we update an entry, simply remove a previous file
|
||||
if (QFileInfo::exists(targetFilePath)) {
|
||||
qInfo() << "Trying to remove old desktop file" << targetFilePath;
|
||||
QFile::remove(targetFilePath);
|
||||
}
|
||||
|
||||
if (!QFile::copy(info.desktopFilePath, targetFilePath)) {
|
||||
qWarning() << "Failed to copy " << info.desktopFilePath << targetFilePath;
|
||||
Q_EMIT error(i18n("Failed to copy file %1 to %2", info.desktopFilePath, targetFilePath));
|
||||
} else {
|
||||
qInfo() << "Copied desktop file to" << targetFilePath;
|
||||
Q_EMIT finished();
|
||||
}
|
||||
}
|
||||
void KRunnerPluginInstallerRcJob::doUninstall(const InstallerInfo &info)
|
||||
{
|
||||
QFile dbusFile(installDBusServicePath(info.dbusService));
|
||||
QFile metaDataFile(installMetaDataPath(info.desktopFilePath));
|
||||
|
||||
if (metaDataFile.remove()) {
|
||||
if (dbusFile.remove()) {
|
||||
Q_EMIT finished();
|
||||
} else {
|
||||
Q_EMIT error(xi18nc("@info", "Failed to remove file %1:<nl/>%2", dbusFile.fileName(), dbusFile.errorString()));
|
||||
}
|
||||
} else {
|
||||
Q_EMIT error(xi18nc("@info", "Failed to remove file %1:<nl/>%2", metaDataFile.fileName(), metaDataFile.errorString()));
|
||||
}
|
||||
}
|
||||
24
kcms/runners/plugininstaller/KRunnerPlugininstallerRcJob.h
Normal file
24
kcms/runners/plugininstaller/KRunnerPlugininstallerRcJob.h
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AbstractJob.h"
|
||||
struct InstallerInfo;
|
||||
|
||||
class KRunnerPluginInstallerRcJob : public AbstractJob
|
||||
{
|
||||
public:
|
||||
explicit KRunnerPluginInstallerRcJob(bool skipConfigmDialog)
|
||||
: m_skipConfirmDialog(skipConfigmDialog)
|
||||
{
|
||||
}
|
||||
void executeOperation(const QFileInfo &fileInfo, const QString &mimeType, bool install) override;
|
||||
|
||||
private:
|
||||
void doInstall(const InstallerInfo &info);
|
||||
void doUninstall(const InstallerInfo &info);
|
||||
const bool m_skipConfirmDialog;
|
||||
};
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include <KConfig>
|
||||
#include <KConfigGroup>
|
||||
#include <KLocalizedString>
|
||||
#include <QDesktopServices>
|
||||
#include <QDialog>
|
||||
|
|
@ -14,11 +16,21 @@
|
|||
#include <QPushButton>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
#include <optional>
|
||||
struct InstallerInfo {
|
||||
QString exec;
|
||||
QString dbusService;
|
||||
QString desktopFilePath;
|
||||
};
|
||||
|
||||
class ScriptConfirmationDialog : public QDialog
|
||||
{
|
||||
public:
|
||||
ScriptConfirmationDialog(const QString &installerPath, bool install, const QString &dir, QWidget *parent = nullptr)
|
||||
ScriptConfirmationDialog(const QString &installerPath,
|
||||
bool install,
|
||||
const QString &dir,
|
||||
const std::optional<InstallerInfo> &info = std::nullopt,
|
||||
QWidget *parent = nullptr)
|
||||
: QDialog(parent)
|
||||
{
|
||||
const auto readmes = QDir(dir).entryList({QStringLiteral("README*")});
|
||||
|
|
@ -27,7 +39,10 @@ public:
|
|||
const bool noInstaller = installerPath.isEmpty();
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
QString msg;
|
||||
if (!install && noInstaller && readmes.isEmpty()) {
|
||||
if (info) {
|
||||
Q_ASSERT(install); // We do not need the warnings for uninstall
|
||||
msg = xi18nc("@info", "This plugin executes the following command for starting the \"%1\" service:<nl/>%2", info->dbusService, info->exec);
|
||||
} else if (!install && noInstaller && readmes.isEmpty()) {
|
||||
msg = xi18nc("@info",
|
||||
"This plugin does not provide an uninstallation script. Please contact the author. "
|
||||
"You can try to uninstall the plugin manually.<nl/>"
|
||||
|
|
@ -71,9 +86,9 @@ public:
|
|||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
QString okText;
|
||||
if (noInstaller && !install) {
|
||||
if (noInstaller && !install && !info) {
|
||||
okText = i18nc("@action:button", "Mark entry as uninstalled");
|
||||
} else if (noInstaller) {
|
||||
} else if (noInstaller && !info) {
|
||||
okText = i18nc("@action:button", "Mark entry as installed");
|
||||
} else {
|
||||
okText = i18nc("@action:button", "Accept Risk And Continue");
|
||||
|
|
|
|||
4
kcms/runners/plugininstaller/autotests/CMakeLists.txt
Normal file
4
kcms/runners/plugininstaller/autotests/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# SPDX-FileCopyrightText: 2022 Alexander Lohnau <alexander.lohnau@gmx.de>
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
ecm_add_test(plugininstallerrctest.cpp)
|
||||
target_link_libraries(plugininstallerrctest Qt::Test)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
#include <QFileInfo>
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QTest>
|
||||
#include <QTextStream>
|
||||
|
||||
bool fileContainsLine(const QString &fileName, const QString &line)
|
||||
{
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream in(&file);
|
||||
while (!in.atEnd()) {
|
||||
if (in.readLine() == line) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
class PluginInstallerRcTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private Q_SLOTS:
|
||||
void initTestCase()
|
||||
{
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
qputenv("PLUGININSTALLER_TEST_MODE", "true");
|
||||
}
|
||||
void testInstall()
|
||||
{
|
||||
const QString executable = QFINDTESTDATA("krunner-plugininstaller");
|
||||
QProcess installProcess;
|
||||
installProcess.start(executable, {"install", QFINDTESTDATA("testplugin"), "--no-confirm"});
|
||||
|
||||
installProcess.waitForFinished();
|
||||
qDebug() << installProcess.readAllStandardError();
|
||||
QCOMPARE(installProcess.exitCode(), 0);
|
||||
|
||||
const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
|
||||
const QString desktopLocation = dataLocation + "/krunner/dbusplugins/org.kde.testplugin.desktop";
|
||||
const QString serviceLocation = dataLocation + "/dbus-1/services/org.kde.testplugin.service";
|
||||
QVERIFY(QFileInfo::exists(desktopLocation));
|
||||
QVERIFY(QFileInfo::exists(serviceLocation));
|
||||
QVERIFY(fileContainsLine(desktopLocation, "X-Plasma-DBusRunner-Service=org.kde.testplugin"));
|
||||
QVERIFY(fileContainsLine(serviceLocation, QLatin1String("Exec=python3 %1/main.py").arg(QFINDTESTDATA("testplugin"))));
|
||||
|
||||
QProcess uninstallProcess;
|
||||
uninstallProcess.start(executable, {"uninstall", QFINDTESTDATA("testplugin")});
|
||||
|
||||
uninstallProcess.waitForFinished();
|
||||
qDebug() << uninstallProcess.readAllStandardError();
|
||||
QVERIFY(!QFileInfo::exists(desktopLocation));
|
||||
QVERIFY(!QFileInfo::exists(serviceLocation));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(PluginInstallerRcTest)
|
||||
|
||||
#include "plugininstallerrctest.moc"
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
MetaDataFile=org.kde.testplugin.desktop
|
||||
Exec=python3 %{PROJECTDIR}/main.py
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[Desktop Entry]
|
||||
X-Plasma-DBusRunner-Service=org.kde.testplugin
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
|
|
@ -10,10 +9,13 @@
|
|||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QMimeDatabase>
|
||||
#include <QStandardPaths>
|
||||
#include <QUrl>
|
||||
|
||||
#include "KRunnerPlugininstallerRcJob.h"
|
||||
#include "config-workspace.h"
|
||||
#if HAVE_PACKAGEKIT
|
||||
#include "PackageKitJob.h"
|
||||
|
|
@ -38,9 +40,15 @@ int main(int argc, char *argv[])
|
|||
QApplication app(argc, argv);
|
||||
app.setQuitOnLastWindowClosed(false);
|
||||
|
||||
if (qEnvironmentVariableIsSet("PLUGININSTALLER_TEST_MODE")) {
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
}
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.addPositionalArgument(QStringLiteral("command"), i18nc("@info:shell", "Command to execute: install or uninstall."));
|
||||
parser.addPositionalArgument(QStringLiteral("path"), i18nc("@info:shell", "Path to archive."));
|
||||
QCommandLineOption noConfirm(QStringLiteral("no-confirm"), i18nc("@info:shell", "Do not show a visual confirmation dialog"));
|
||||
parser.addOption(noConfirm);
|
||||
parser.process(app);
|
||||
|
||||
const QStringList args = parser.positionalArguments();
|
||||
|
|
@ -82,6 +90,9 @@ int main(int argc, char *argv[])
|
|||
#else
|
||||
fail(i18nc("@info", "No PackageKit support"));
|
||||
#endif
|
||||
} else if (const QString rcPath = QDir(file).filePath("krunner-plugininstallerrc"); fileInfo.isDir() && QFileInfo::exists(rcPath)) {
|
||||
fileInfo = QFileInfo(rcPath);
|
||||
job.reset(new KRunnerPluginInstallerRcJob(parser.isSet(noConfirm)));
|
||||
} else {
|
||||
job.reset(new ScriptJob());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue