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