1 /*
2 SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez <aleixpol@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-only
5 */
6
7 #include "flatpakruntime.h"
8 #include "flatpakplugin.h"
9 #include "debug_flatpak.h"
10
11 #include <util/executecompositejob.h>
12 #include <outputview/outputexecutejob.h>
13 #include <interfaces/iruncontroller.h>
14 #include <interfaces/icore.h>
15
16 #include <KLocalizedString>
17 #include <KProcess>
18 #include <KActionCollection>
19 #include <QProcess>
20 #include <QTemporaryDir>
21 #include <QDir>
22 #include <QJsonDocument>
23 #include <QJsonArray>
24 #include <QJsonObject>
25 #include <QStandardPaths>
26
27 using namespace KDevelop;
28
29 template <typename T, typename Q, typename W>
kTransform(const Q & list,W func)30 static T kTransform(const Q& list, W func)
31 {
32 T ret;
33 ret.reserve(list.size());
34 for (auto it = list.constBegin(), itEnd = list.constEnd(); it!=itEnd; ++it)
35 ret += func(*it);
36 return ret;
37 }
38
createExecuteJob(const QStringList & program,const QString & title,const QUrl & wd={},bool checkExitCode=true)39 static KJob* createExecuteJob(const QStringList &program, const QString &title, const QUrl &wd = {}, bool checkExitCode = true)
40 {
41 auto* process = new OutputExecuteJob;
42 process->setProperties(OutputExecuteJob::DisplayStdout | OutputExecuteJob::DisplayStderr);
43 process->setExecuteOnHost(true);
44 process->setJobName(title);
45 process->setWorkingDirectory(wd);
46 process->setCheckExitCode(checkExitCode);
47 *process << program;
48 return process;
49 }
50
createBuildDirectory(const KDevelop::Path & buildDirectory,const KDevelop::Path & file,const QString & arch)51 KJob* FlatpakRuntime::createBuildDirectory(const KDevelop::Path &buildDirectory, const KDevelop::Path &file, const QString &arch)
52 {
53 return createExecuteJob(QStringList{QStringLiteral("flatpak-builder"), QLatin1String("--arch=")+arch, QStringLiteral("--build-only"), buildDirectory.toLocalFile(), file.toLocalFile() }, i18n("Flatpak"), file.parent().toUrl());
54 }
55
FlatpakRuntime(const KDevelop::Path & buildDirectory,const KDevelop::Path & file,const QString & arch)56 FlatpakRuntime::FlatpakRuntime(const KDevelop::Path &buildDirectory, const KDevelop::Path &file, const QString &arch)
57 : KDevelop::IRuntime()
58 , m_file(file)
59 , m_buildDirectory(buildDirectory)
60 , m_arch(arch)
61 {
62 refreshJson();
63 }
64
~FlatpakRuntime()65 FlatpakRuntime::~FlatpakRuntime()
66 {
67 }
68
refreshJson()69 void FlatpakRuntime::refreshJson()
70 {
71 const auto doc = config();
72 const QString sdkName = doc[QLatin1String("sdk")].toString();
73 const QString runtimeVersion = doc.value(QLatin1String("runtime-version")).toString();
74 const QString usedRuntime = sdkName + QLatin1Char('/') + m_arch + QLatin1Char('/') + runtimeVersion;
75
76 m_sdkPath = KDevelop::Path(QLatin1String("/var/lib/flatpak/runtime/") + usedRuntime + QLatin1String("/active/files"));
77 qCDebug(FLATPAK) << "flatpak runtime path..." << name() << m_sdkPath;
78 Q_ASSERT(QFile::exists(m_sdkPath.toLocalFile()));
79
80 m_finishArgs = kTransform<QStringList>(doc[QLatin1String("finish-args")].toArray(), [](const QJsonValue& val){ return val.toString(); });
81 }
82
setEnabled(bool)83 void FlatpakRuntime::setEnabled(bool /*enable*/)
84 {
85 }
86
startProcess(QProcess * process) const87 void FlatpakRuntime::startProcess(QProcess* process) const
88 {
89 //Take any environment variables specified in process and pass through to flatpak.
90 QStringList env_args;
91 const QStringList env_vars = process->processEnvironment().toStringList();
92 for (const QString& env_var : env_vars) {
93 env_args << QLatin1String("--env=") + env_var;
94 }
95 const QStringList args = m_finishArgs + env_args + QStringList{QStringLiteral("build"), QStringLiteral("--talk-name=org.freedesktop.DBus"), m_buildDirectory.toLocalFile(), process->program()} << process->arguments();
96 process->setProgram(QStringLiteral("flatpak"));
97 process->setArguments(args);
98
99 qCDebug(FLATPAK) << "starting qprocess" << process->program() << process->arguments();
100 process->start();
101 }
102
startProcess(KProcess * process) const103 void FlatpakRuntime::startProcess(KProcess* process) const
104 {
105 //Take any environment variables specified in process and pass through to flatpak.
106 QStringList env_args;
107 const QStringList env_vars = process->processEnvironment().toStringList();
108 for (const QString& env_var : env_vars) {
109 env_args << QLatin1String("--env=") + env_var;
110 }
111 process->setProgram(QStringList{QStringLiteral("flatpak")} << m_finishArgs << env_args << QStringList{QStringLiteral("build"), QStringLiteral("--talk-name=org.freedesktop.DBus"), m_buildDirectory.toLocalFile() } << process->program());
112
113 qCDebug(FLATPAK) << "starting kprocess" << process->program().join(QLatin1Char(' '));
114 process->start();
115 }
116
rebuild()117 KJob* FlatpakRuntime::rebuild()
118 {
119 QDir(m_buildDirectory.toLocalFile()).removeRecursively();
120 auto job = createBuildDirectory(m_buildDirectory, m_file, m_arch);
121 refreshJson();
122 return job;
123 }
124
exportBundle(const QString & path) const125 QList<KJob*> FlatpakRuntime::exportBundle(const QString &path) const
126 {
127 const auto doc = config();
128
129 auto* dir = new QTemporaryDir(QDir::tempPath()+QLatin1String("/flatpak-tmp-repo"));
130 if (!dir->isValid() || doc.isEmpty()) {
131 qCWarning(FLATPAK) << "Couldn't export:" << path << dir->isValid() << dir->path() << doc.isEmpty();
132 return {};
133 }
134
135 const QString name = doc[QLatin1String("id")].toString();
136 QStringList args = m_finishArgs;
137 if (doc.contains(QLatin1String("command")))
138 args << QLatin1String("--command=")+doc[QLatin1String("command")].toString();
139
140 const QString title = i18n("Bundling");
141 const QList<KJob*> jobs = {
142 createExecuteJob(QStringList{QStringLiteral("flatpak"), QStringLiteral("build-finish"), m_buildDirectory.toLocalFile()} << args, title, {}, false),
143 createExecuteJob(QStringList{QStringLiteral("flatpak"), QStringLiteral("build-export"), QLatin1String("--arch=")+m_arch, dir->path(), m_buildDirectory.toLocalFile()}, title),
144 createExecuteJob(QStringList{QStringLiteral("flatpak"), QStringLiteral("build-bundle"), QLatin1String("--arch=")+m_arch, dir->path(), path, name }, title)
145 };
146 connect(jobs.last(), &QObject::destroyed, jobs.last(), [dir]() { delete dir; });
147 return jobs;
148 }
149
name() const150 QString FlatpakRuntime::name() const
151 {
152 return QStringLiteral("%1 - %2").arg(m_arch, m_file.lastPathSegment());
153 }
154
executeOnDevice(const QString & host,const QString & path) const155 KJob * FlatpakRuntime::executeOnDevice(const QString& host, const QString &path) const
156 {
157 const QString name = config()[QLatin1String("id")].toString();
158 const QString destPath = QStringLiteral("/tmp/kdevelop-test-app.flatpak");
159 const QString replicatePath = QStringLiteral("/tmp/replicate.sh");
160 const QString localReplicatePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("kdevflatpak/replicate.sh"));
161
162 const QString title = i18n("Run on Device");
163 const QList<KJob*> jobs = exportBundle(path) << QList<KJob*> {
164 createExecuteJob({QStringLiteral("scp"), path, host+QLatin1Char(':')+destPath}, title),
165 createExecuteJob({QStringLiteral("scp"), localReplicatePath, host+QLatin1Char(':')+replicatePath}, title),
166 createExecuteJob({QStringLiteral("ssh"), host, QStringLiteral("flatpak"), QStringLiteral("install"), QStringLiteral("--user"), QStringLiteral("--bundle"), QStringLiteral("-y"), destPath}, title),
167 createExecuteJob({QStringLiteral("ssh"), host, QStringLiteral("bash"), replicatePath, QStringLiteral("plasmashell"), QStringLiteral("flatpak"), QStringLiteral("run"), name }, title),
168 };
169 return new KDevelop::ExecuteCompositeJob( parent(), jobs );
170 }
171
config(const KDevelop::Path & path)172 QJsonObject FlatpakRuntime::config(const KDevelop::Path& path)
173 {
174 QFile f(path.toLocalFile());
175 if (!f.open(QIODevice::ReadOnly)) {
176 qCWarning(FLATPAK) << "couldn't open" << path;
177 return {};
178 }
179
180 QJsonParseError error;
181 auto doc = QJsonDocument::fromJson(f.readAll(), &error);
182 if (error.error) {
183 qCWarning(FLATPAK) << "couldn't parse" << path << error.errorString();
184 return {};
185 }
186
187 return doc.object();
188 }
189
config() const190 QJsonObject FlatpakRuntime::config() const
191 {
192 return config(m_file);
193 }
194
pathInHost(const KDevelop::Path & runtimePath) const195 Path FlatpakRuntime::pathInHost(const KDevelop::Path& runtimePath) const
196 {
197 KDevelop::Path ret = runtimePath;
198 if (!runtimePath.isLocalFile()) {
199 return ret;
200 }
201
202 const auto prefix = runtimePath.segments().at(0);
203 if (prefix == QLatin1String("usr")) {
204 const auto relpath = KDevelop::Path(QStringLiteral("/usr")).relativePath(runtimePath);
205 ret = Path(m_sdkPath, relpath);
206 } else if (prefix == QLatin1String("app")) {
207 const auto relpath = KDevelop::Path(QStringLiteral("/app")).relativePath(runtimePath);
208 ret = Path(m_buildDirectory, QLatin1String("/active/files/") + relpath);
209 }
210
211 qCDebug(FLATPAK) << "path in host" << runtimePath << ret;
212 return ret;
213 }
214
pathInRuntime(const KDevelop::Path & localPath) const215 Path FlatpakRuntime::pathInRuntime(const KDevelop::Path& localPath) const
216 {
217 KDevelop::Path ret = localPath;
218 if (m_sdkPath.isParentOf(localPath)) {
219 const auto relpath = m_sdkPath.relativePath(localPath);
220 ret = Path(Path(QStringLiteral("/usr")), relpath);
221 } else {
222 const Path bdfiles(m_buildDirectory, QStringLiteral("/active/files"));
223 if (bdfiles.isParentOf(localPath)) {
224 const auto relpath = bdfiles.relativePath(localPath);
225 ret = Path(Path(QStringLiteral("/app")), relpath);
226 }
227 }
228
229 qCDebug(FLATPAK) << "path in runtime" << localPath << ret;
230 return ret;
231 }
232
findExecutable(const QString & executableName) const233 QString FlatpakRuntime::findExecutable(const QString& executableName) const
234 {
235 QStringList rtPaths;
236
237 auto envPaths = getenv(QByteArrayLiteral("PATH")).split(':');
238 std::transform(envPaths.begin(), envPaths.end(), std::back_inserter(rtPaths),
239 [this](QByteArray p) {
240 return pathInHost(Path(QString::fromLocal8Bit(p))).toLocalFile();
241 });
242
243 return QStandardPaths::findExecutable(executableName, rtPaths);
244 }
245
getenv(const QByteArray & varname) const246 QByteArray FlatpakRuntime::getenv(const QByteArray& varname) const
247 {
248 if (varname == "KDEV_DEFAULT_INSTALL_PREFIX")
249 return "/app";
250 return qgetenv(varname.constData());
251 }
252
buildPath() const253 KDevelop::Path FlatpakRuntime::buildPath() const
254 {
255 auto file = m_file;
256 file.setLastPathSegment(QStringLiteral(".flatpak-builder"));
257 file.addPath(QStringLiteral("kdevelop"));
258 return file;
259 }
260