File indexing completed on 2024-05-05 04:39:50

0001 /*
0002     SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez <aleixpol@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-only
0005 */
0006 
0007 #include "flatpakplugin.h"
0008 #include "flatpakruntime.h"
0009 #include <interfaces/icore.h>
0010 #include <interfaces/iruntimecontroller.h>
0011 #include <interfaces/iuicontroller.h>
0012 #include <interfaces/iprojectcontroller.h>
0013 #include <interfaces/iruncontroller.h>
0014 #include <interfaces/context.h>
0015 #include <interfaces/contextmenuextension.h>
0016 #include <project/projectmodel.h>
0017 #include <util/executecompositejob.h>
0018 
0019 #include <QTextStream>
0020 #include <QStandardPaths>
0021 #include <QAction>
0022 #include <QProcess>
0023 #include <QRegularExpression>
0024 #include <QInputDialog>
0025 #include <QTemporaryDir>
0026 #include <QTemporaryFile>
0027 #include <QFileDialog>
0028 #include <KPluginFactory>
0029 #include <KActionCollection>
0030 #include <KLocalizedString>
0031 #include <KParts/MainWindow>
0032 #include <KJob>
0033 #include <KSharedConfig>
0034 #include <KConfigGroup>
0035 
0036 K_PLUGIN_FACTORY_WITH_JSON(KDevFlatpakFactory, "kdevflatpak.json", registerPlugin<FlatpakPlugin>();)
0037 
0038 using namespace KDevelop;
0039 
0040 FlatpakPlugin::FlatpakPlugin(QObject *parent, const QVariantList & /*args*/)
0041     : KDevelop::IPlugin( QStringLiteral("kdevflatpak"), parent )
0042 {
0043     auto ac = actionCollection();
0044 
0045     auto action = new QAction(QIcon::fromTheme(QStringLiteral("run-build-clean")), i18nc("@action", "Rebuild Environment"), this);
0046     action->setWhatsThis(i18nc("@info:whatsthis", "Recompiles all dependencies for a fresh environment."));
0047     ac->setDefaultShortcut(action, Qt::CTRL | Qt::META | Qt::Key_X);
0048     connect(action, &QAction::triggered, this, &FlatpakPlugin::rebuildCurrent);
0049     ac->addAction(QStringLiteral("runtime_flatpak_rebuild"), action);
0050 
0051     auto exportAction = new QAction(QIcon::fromTheme(QStringLiteral("document-export")), i18nc("@action", "Export Flatpak Bundle..."), this);
0052     exportAction->setWhatsThis(i18nc("@info:whatsthis", "Exports the current build into a 'bundle.flatpak' file."));
0053     ac->setDefaultShortcut(exportAction, Qt::CTRL | Qt::META | Qt::Key_E);
0054     connect(exportAction, &QAction::triggered, this, &FlatpakPlugin::exportCurrent);
0055     ac->addAction(QStringLiteral("runtime_flatpak_export"), exportAction);
0056 
0057     auto remoteAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-remote-symbolic")), i18nc("@action", "Send to Device..."), this);
0058     ac->setDefaultShortcut(remoteAction, Qt::CTRL | Qt::META | Qt::Key_D);
0059     connect(remoteAction, &QAction::triggered, this, &FlatpakPlugin::executeOnRemoteDevice);
0060     ac->addAction(QStringLiteral("runtime_flatpak_remote"), remoteAction);
0061 
0062     runtimeChanged(ICore::self()->runtimeController()->currentRuntime());
0063 
0064     setXMLFile( QStringLiteral("kdevflatpakplugin.rc") );
0065     connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, &FlatpakPlugin::runtimeChanged);
0066 }
0067 
0068 FlatpakPlugin::~FlatpakPlugin() = default;
0069 
0070 void FlatpakPlugin::runtimeChanged(KDevelop::IRuntime* newRuntime)
0071 {
0072     const bool isFlatpak = qobject_cast<FlatpakRuntime*>(newRuntime);
0073 
0074     const auto& actions = actionCollection()->actions();
0075     for (auto action: actions) {
0076         action->setEnabled(isFlatpak);
0077     }
0078 }
0079 
0080 void FlatpakPlugin::rebuildCurrent()
0081 {
0082     const auto runtime = qobject_cast<FlatpakRuntime*>(ICore::self()->runtimeController()->currentRuntime());
0083     Q_ASSERT(runtime);
0084     ICore::self()->runController()->registerJob(runtime->rebuild());
0085 }
0086 
0087 void FlatpakPlugin::exportCurrent()
0088 {
0089     const auto runtime = qobject_cast<FlatpakRuntime*>(ICore::self()->runtimeController()->currentRuntime());
0090     Q_ASSERT(runtime);
0091 
0092     const QString path = QFileDialog::getSaveFileName(ICore::self()->uiController()->activeMainWindow(), i18nc("@title:window", "Export %1", runtime->name()), {}, i18n("Flatpak Bundle (*.flatpak)"));
0093     if (!path.isEmpty()) {
0094         ICore::self()->runController()->registerJob(new ExecuteCompositeJob(runtime, runtime->exportBundle(path)));
0095     }
0096 }
0097 
0098 void FlatpakPlugin::createRuntime(const KDevelop::Path &file, const QString &arch)
0099 {
0100     auto* dir = new QTemporaryDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/kdevelop-flatpak-"));
0101     const KDevelop::Path path(dir->path());
0102 
0103     auto process = FlatpakRuntime::createBuildDirectory(path, file, arch);
0104     connect(process, &KJob::finished, this, [path, file, arch, dir] (KJob* job) {
0105         if (job->error() != 0) {
0106             delete dir;
0107             return;
0108         }
0109 
0110         auto rt = new FlatpakRuntime(path, file, arch);
0111         connect(rt, &QObject::destroyed, rt, [dir]() { delete dir; });
0112         ICore::self()->runtimeController()->addRuntimes(rt);
0113     });
0114     process->start();
0115 }
0116 
0117 static QStringList availableArches(const KDevelop::Path& url)
0118 {
0119     QProcess supportedArchesProcess;
0120     QStringList ret;
0121 
0122     const auto doc = FlatpakRuntime::config(url);
0123     const QString sdkName = doc[QLatin1String("sdk")].toString();
0124     const QString runtimeVersion = doc[QLatin1String("runtime-version")].toString();
0125     const QString match = sdkName + QLatin1String("/(.+)/") + runtimeVersion;
0126     QObject::connect(&supportedArchesProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
0127                      &supportedArchesProcess, [&supportedArchesProcess, &match, &ret]() {
0128         QTextStream stream(&supportedArchesProcess);
0129         QRegularExpression rx(match);
0130         while (!stream.atEnd()) {
0131             const QString line = stream.readLine();
0132             auto m = rx.match(line);
0133             if (m.hasMatch()) {
0134                 ret << m.captured(1);
0135             }
0136         }
0137     });
0138 
0139     supportedArchesProcess.start(QStringLiteral("flatpak"), {QStringLiteral("list"), QStringLiteral("--runtime"), QStringLiteral("-d") });
0140     supportedArchesProcess.waitForFinished();
0141     return ret;
0142 }
0143 
0144 KDevelop::ContextMenuExtension FlatpakPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent)
0145 {
0146     QList<QUrl> urls;
0147 
0148     if ( context->type() == KDevelop::Context::FileContext ) {
0149         auto* filectx = static_cast<KDevelop::FileContext*>(context);
0150         urls = filectx->urls();
0151     } else if ( context->type() == KDevelop::Context::ProjectItemContext ) {
0152         auto* projctx = static_cast<KDevelop::ProjectItemContext*>(context);
0153         const auto items = projctx->items();
0154         for (KDevelop::ProjectBaseItem* item : items) {
0155             if ( item->file() ) {
0156                 urls << item->file()->path().toUrl();
0157             }
0158         }
0159     }
0160 
0161     const QRegularExpression nameRx(QStringLiteral(".*\\..*\\..*\\.json$"));
0162     for(auto it = urls.begin(); it != urls.end(); ) {
0163         if (it->isLocalFile() && it->path().contains(nameRx)) {
0164             ++it;
0165         } else {
0166             it = urls.erase(it);
0167         }
0168     }
0169 
0170     if ( !urls.isEmpty() ) {
0171         KDevelop::ContextMenuExtension ext;
0172         for (const QUrl& url : qAsConst(urls)) {
0173             const KDevelop::Path file(url);
0174             const auto arches = availableArches(file);
0175             for (const QString& arch : arches) {
0176                 auto action = new QAction(i18nc("@action:inmenu", "Build Flatpak %1 for %2", file.lastPathSegment(), arch), parent);
0177                 connect(action, &QAction::triggered, this, [this, file, arch]() {
0178                     createRuntime(file, arch);
0179                 });
0180                 ext.addAction(KDevelop::ContextMenuExtension::RunGroup, action);
0181             }
0182         }
0183 
0184         return ext;
0185     }
0186 
0187     return KDevelop::IPlugin::contextMenuExtension(context, parent);
0188 }
0189 
0190 void FlatpakPlugin::executeOnRemoteDevice()
0191 {
0192     const auto runtime = qobject_cast<FlatpakRuntime*>(ICore::self()->runtimeController()->currentRuntime());
0193     Q_ASSERT(runtime);
0194 
0195     KConfigGroup group(KSharedConfig::openConfig(), "Flatpak");
0196     const QString lastDeviceAddress = group.readEntry("DeviceAddress");
0197     const QString host = QInputDialog::getText(
0198         ICore::self()->uiController()->activeMainWindow(), i18nc("@title:window", "Choose Tag Name"),
0199         i18nc("@label:textbox", "Device hostname:"),
0200         QLineEdit::Normal, lastDeviceAddress
0201     );
0202     if (host.isEmpty())
0203         return;
0204     group.writeEntry("DeviceAddress", host);
0205 
0206     auto* file = new QTemporaryFile(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/') + runtime->name() + QLatin1String("XXXXXX.flatpak"));
0207     file->open();
0208     file->close();
0209     auto job = runtime->executeOnDevice(host, file->fileName());
0210     file->setParent(file);
0211 
0212     ICore::self()->runController()->registerJob(job);
0213 }
0214 
0215 #include "flatpakplugin.moc"
0216 #include "moc_flatpakplugin.cpp"