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