1 /*
2     SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez <aleixpol@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-only
5 */
6 
7 #include "flatpakplugin.h"
8 #include "flatpakruntime.h"
9 #include <interfaces/icore.h>
10 #include <interfaces/iruntimecontroller.h>
11 #include <interfaces/iuicontroller.h>
12 #include <interfaces/iprojectcontroller.h>
13 #include <interfaces/iruncontroller.h>
14 #include <interfaces/context.h>
15 #include <interfaces/contextmenuextension.h>
16 #include <project/projectmodel.h>
17 #include <util/executecompositejob.h>
18 
19 #include <QTextStream>
20 #include <QStandardPaths>
21 #include <QAction>
22 #include <QProcess>
23 #include <QRegularExpression>
24 #include <QInputDialog>
25 #include <QTemporaryDir>
26 #include <QTemporaryFile>
27 #include <QFileDialog>
28 #include <KPluginFactory>
29 #include <KActionCollection>
30 #include <KLocalizedString>
31 #include <KParts/MainWindow>
32 #include <KJob>
33 #include <KSharedConfig>
34 #include <KConfigGroup>
35 
36 K_PLUGIN_FACTORY_WITH_JSON(KDevFlatpakFactory, "kdevflatpak.json", registerPlugin<FlatpakPlugin>();)
37 
38 using namespace KDevelop;
39 
FlatpakPlugin(QObject * parent,const QVariantList &)40 FlatpakPlugin::FlatpakPlugin(QObject *parent, const QVariantList & /*args*/)
41     : KDevelop::IPlugin( QStringLiteral("kdevflatpak"), parent )
42 {
43     auto ac = actionCollection();
44 
45     auto action = new QAction(QIcon::fromTheme(QStringLiteral("run-build-clean")), i18nc("@action", "Rebuild Environment"), this);
46     action->setWhatsThis(i18nc("@info:whatsthis", "Recompiles all dependencies for a fresh environment."));
47     ac->setDefaultShortcut(action, Qt::CTRL | Qt::META | Qt::Key_X);
48     connect(action, &QAction::triggered, this, &FlatpakPlugin::rebuildCurrent);
49     ac->addAction(QStringLiteral("runtime_flatpak_rebuild"), action);
50 
51     auto exportAction = new QAction(QIcon::fromTheme(QStringLiteral("document-export")), i18nc("@action", "Export Flatpak Bundle..."), this);
52     exportAction->setWhatsThis(i18nc("@info:whatsthis", "Exports the current build into a 'bundle.flatpak' file."));
53     ac->setDefaultShortcut(exportAction, Qt::CTRL | Qt::META | Qt::Key_E);
54     connect(exportAction, &QAction::triggered, this, &FlatpakPlugin::exportCurrent);
55     ac->addAction(QStringLiteral("runtime_flatpak_export"), exportAction);
56 
57     auto remoteAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-remote-symbolic")), i18nc("@action", "Send to Device..."), this);
58     ac->setDefaultShortcut(remoteAction, Qt::CTRL | Qt::META | Qt::Key_D);
59     connect(remoteAction, &QAction::triggered, this, &FlatpakPlugin::executeOnRemoteDevice);
60     ac->addAction(QStringLiteral("runtime_flatpak_remote"), remoteAction);
61 
62     runtimeChanged(ICore::self()->runtimeController()->currentRuntime());
63 
64     setXMLFile( QStringLiteral("kdevflatpakplugin.rc") );
65     connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, &FlatpakPlugin::runtimeChanged);
66 }
67 
68 FlatpakPlugin::~FlatpakPlugin() = default;
69 
runtimeChanged(KDevelop::IRuntime * newRuntime)70 void FlatpakPlugin::runtimeChanged(KDevelop::IRuntime* newRuntime)
71 {
72     const bool isFlatpak = qobject_cast<FlatpakRuntime*>(newRuntime);
73 
74     const auto& actions = actionCollection()->actions();
75     for (auto action: actions) {
76         action->setEnabled(isFlatpak);
77     }
78 }
79 
rebuildCurrent()80 void FlatpakPlugin::rebuildCurrent()
81 {
82     const auto runtime = qobject_cast<FlatpakRuntime*>(ICore::self()->runtimeController()->currentRuntime());
83     Q_ASSERT(runtime);
84     ICore::self()->runController()->registerJob(runtime->rebuild());
85 }
86 
exportCurrent()87 void FlatpakPlugin::exportCurrent()
88 {
89     const auto runtime = qobject_cast<FlatpakRuntime*>(ICore::self()->runtimeController()->currentRuntime());
90     Q_ASSERT(runtime);
91 
92     const QString path = QFileDialog::getSaveFileName(ICore::self()->uiController()->activeMainWindow(), i18nc("@title:window", "Export %1", runtime->name()), {}, i18n("Flatpak Bundle (*.flatpak)"));
93     if (!path.isEmpty()) {
94         ICore::self()->runController()->registerJob(new ExecuteCompositeJob(runtime, runtime->exportBundle(path)));
95     }
96 }
97 
createRuntime(const KDevelop::Path & file,const QString & arch)98 void FlatpakPlugin::createRuntime(const KDevelop::Path &file, const QString &arch)
99 {
100     auto* dir = new QTemporaryDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/kdevelop-flatpak-"));
101     const KDevelop::Path path(dir->path());
102 
103     auto process = FlatpakRuntime::createBuildDirectory(path, file, arch);
104     connect(process, &KJob::finished, this, [path, file, arch, dir] (KJob* job) {
105         if (job->error() != 0) {
106             delete dir;
107             return;
108         }
109 
110         auto rt = new FlatpakRuntime(path, file, arch);
111         connect(rt, &QObject::destroyed, rt, [dir]() { delete dir; });
112         ICore::self()->runtimeController()->addRuntimes(rt);
113     });
114     process->start();
115 }
116 
availableArches(const KDevelop::Path & url)117 static QStringList availableArches(const KDevelop::Path& url)
118 {
119     QProcess supportedArchesProcess;
120     QStringList ret;
121 
122     const auto doc = FlatpakRuntime::config(url);
123     const QString sdkName = doc[QLatin1String("sdk")].toString();
124     const QString runtimeVersion = doc[QLatin1String("runtime-version")].toString();
125     const QString match = sdkName + QLatin1String("/(.+)/") + runtimeVersion;
126     QObject::connect(&supportedArchesProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
127                      &supportedArchesProcess, [&supportedArchesProcess, &match, &ret]() {
128         QTextStream stream(&supportedArchesProcess);
129         QRegularExpression rx(match);
130         while (!stream.atEnd()) {
131             const QString line = stream.readLine();
132             auto m = rx.match(line);
133             if (m.hasMatch()) {
134                 ret << m.captured(1);
135             }
136         }
137     });
138 
139     supportedArchesProcess.start(QStringLiteral("flatpak"), {QStringLiteral("list"), QStringLiteral("--runtime") });
140     supportedArchesProcess.waitForFinished();
141     return ret;
142 }
143 
contextMenuExtension(KDevelop::Context * context,QWidget * parent)144 KDevelop::ContextMenuExtension FlatpakPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent)
145 {
146     QList<QUrl> urls;
147 
148     if ( context->type() == KDevelop::Context::FileContext ) {
149         auto* filectx = static_cast<KDevelop::FileContext*>(context);
150         urls = filectx->urls();
151     } else if ( context->type() == KDevelop::Context::ProjectItemContext ) {
152         auto* projctx = static_cast<KDevelop::ProjectItemContext*>(context);
153         const auto items = projctx->items();
154         for (KDevelop::ProjectBaseItem* item : items) {
155             if ( item->file() ) {
156                 urls << item->file()->path().toUrl();
157             }
158         }
159     }
160 
161     const QRegularExpression nameRx(QStringLiteral(".*\\..*\\..*\\.json$"));
162     for(auto it = urls.begin(); it != urls.end(); ) {
163         if (it->isLocalFile() && it->path().contains(nameRx)) {
164             ++it;
165         } else {
166             it = urls.erase(it);
167         }
168     }
169 
170     if ( !urls.isEmpty() ) {
171         KDevelop::ContextMenuExtension ext;
172         for (const QUrl& url : qAsConst(urls)) {
173             const KDevelop::Path file(url);
174             const auto arches = availableArches(file);
175             for (const QString& arch : arches) {
176                 auto action = new QAction(i18nc("@action:inmenu", "Build Flatpak %1 for %2", file.lastPathSegment(), arch), parent);
177                 connect(action, &QAction::triggered, this, [this, file, arch]() {
178                     createRuntime(file, arch);
179                 });
180                 ext.addAction(KDevelop::ContextMenuExtension::RunGroup, action);
181             }
182         }
183 
184         return ext;
185     }
186 
187     return KDevelop::IPlugin::contextMenuExtension(context, parent);
188 }
189 
executeOnRemoteDevice()190 void FlatpakPlugin::executeOnRemoteDevice()
191 {
192     const auto runtime = qobject_cast<FlatpakRuntime*>(ICore::self()->runtimeController()->currentRuntime());
193     Q_ASSERT(runtime);
194 
195     KConfigGroup group(KSharedConfig::openConfig(), "Flatpak");
196     const QString lastDeviceAddress = group.readEntry("DeviceAddress");
197     const QString host = QInputDialog::getText(
198         ICore::self()->uiController()->activeMainWindow(), i18nc("@title:window", "Choose Tag Name"),
199         i18nc("@label:textbox", "Device hostname:"),
200         QLineEdit::Normal, lastDeviceAddress
201     );
202     if (host.isEmpty())
203         return;
204     group.writeEntry("DeviceAddress", host);
205 
206     auto* file = new QTemporaryFile(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/') + runtime->name() + QLatin1String("XXXXXX.flatpak"));
207     file->open();
208     file->close();
209     auto job = runtime->executeOnDevice(host, file->fileName());
210     file->setParent(file);
211 
212     ICore::self()->runController()->registerJob(job);
213 }
214 
215 #include "flatpakplugin.moc"
216