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 "flatpakruntime.h"
0008 #include "flatpakplugin.h"
0009 #include "debug_flatpak.h"
0010 
0011 #include <util/executecompositejob.h>
0012 #include <outputview/outputexecutejob.h>
0013 #include <interfaces/iruncontroller.h>
0014 #include <interfaces/icore.h>
0015 
0016 #include <KLocalizedString>
0017 #include <KProcess>
0018 #include <KActionCollection>
0019 #include <QProcess>
0020 #include <QTemporaryDir>
0021 #include <QDir>
0022 #include <QJsonDocument>
0023 #include <QJsonArray>
0024 #include <QJsonObject>
0025 #include <QStandardPaths>
0026 
0027 using namespace KDevelop;
0028 
0029 template <typename T, typename Q, typename W>
0030 static T kTransform(const Q& list, W func)
0031 {
0032     T ret;
0033     ret.reserve(list.size());
0034     for (auto it = list.constBegin(), itEnd = list.constEnd(); it!=itEnd; ++it)
0035         ret += func(*it);
0036     return ret;
0037 }
0038 
0039 static KJob* createExecuteJob(const QStringList &program, const QString &title, const QUrl &wd = {}, bool checkExitCode = true)
0040 {
0041     auto* process = new OutputExecuteJob;
0042     process->setProperties(OutputExecuteJob::DisplayStdout | OutputExecuteJob::DisplayStderr);
0043     process->setExecuteOnHost(true);
0044     process->setJobName(title);
0045     process->setWorkingDirectory(wd);
0046     process->setCheckExitCode(checkExitCode);
0047     // TODO: call process->setStandardToolView(IOutputView::?); to prevent creating a new tool view for each
0048     // job in OutputJob::startOutput(). Such nonstandard and unshared tool views are also not configurable.
0049     *process << program;
0050     return process;
0051 }
0052 
0053 KJob* FlatpakRuntime::createBuildDirectory(const KDevelop::Path &buildDirectory, const KDevelop::Path &file, const QString &arch)
0054 {
0055     return createExecuteJob(QStringList{QStringLiteral("flatpak-builder"), QLatin1String("--arch=")+arch, QStringLiteral("--build-only"), buildDirectory.toLocalFile(), file.toLocalFile() }, i18n("Flatpak"), file.parent().toUrl());
0056 }
0057 
0058 FlatpakRuntime::FlatpakRuntime(const KDevelop::Path &buildDirectory, const KDevelop::Path &file, const QString &arch)
0059     : KDevelop::IRuntime()
0060     , m_file(file)
0061     , m_buildDirectory(buildDirectory)
0062     , m_arch(arch)
0063 {
0064     refreshJson();
0065 }
0066 
0067 FlatpakRuntime::~FlatpakRuntime()
0068 {
0069 }
0070 
0071 void FlatpakRuntime::refreshJson()
0072 {
0073     const auto doc = config();
0074     const QString sdkName = doc[QLatin1String("sdk")].toString();
0075     const QString runtimeVersion = doc.value(QLatin1String("runtime-version")).toString();
0076     const QString usedRuntime = sdkName + QLatin1Char('/') + m_arch + QLatin1Char('/') + runtimeVersion;
0077 
0078     //First check if local user has flatpak runtime before checking system runtimes.
0079     m_sdkPath = KDevelop::Path(QDir::homePath() + QLatin1String("/.local/share/flatpak/runtime/") + usedRuntime + QLatin1String("/active/files"));
0080     if(!QFile::exists(m_sdkPath.toLocalFile())) {
0081         m_sdkPath = KDevelop::Path(QLatin1String("/var/lib/flatpak/runtime/") + usedRuntime + QLatin1String("/active/files"));
0082     }
0083     qCDebug(FLATPAK) << "flatpak runtime path..." << name() << m_sdkPath;
0084     Q_ASSERT(QFile::exists(m_sdkPath.toLocalFile()));
0085 
0086     m_finishArgs = kTransform<QStringList>(doc[QLatin1String("finish-args")].toArray(), [](const QJsonValue& val){ return val.toString(); });
0087 }
0088 
0089 void FlatpakRuntime::setEnabled(bool /*enable*/)
0090 {
0091 }
0092 
0093 void FlatpakRuntime::startProcess(QProcess* process) const
0094 {
0095     //Take any environment variables specified in process and pass through to flatpak.
0096     QStringList env_args;
0097     const QStringList env_vars = process->processEnvironment().toStringList();
0098     for (const QString& env_var : env_vars) {
0099         env_args << QLatin1String("--env=") + env_var;
0100     }
0101     const QStringList args = m_finishArgs + env_args + QStringList{QStringLiteral("build"), QStringLiteral("--talk-name=org.freedesktop.DBus"), m_buildDirectory.toLocalFile(), process->program()} << process->arguments();
0102     process->setProgram(QStringLiteral("flatpak"));
0103     process->setArguments(args);
0104 
0105     qCDebug(FLATPAK) << "starting qprocess" << process->program() << process->arguments();
0106     process->start();
0107 }
0108 
0109 void FlatpakRuntime::startProcess(KProcess* process) const
0110 {
0111     //Take any environment variables specified in process and pass through to flatpak.
0112     QStringList env_args;
0113     const QStringList env_vars = process->processEnvironment().toStringList();
0114     for (const QString& env_var : env_vars) {
0115         env_args << QLatin1String("--env=") + env_var;
0116     }
0117     process->setProgram(QStringList{QStringLiteral("flatpak")} << m_finishArgs << env_args << QStringList{QStringLiteral("build"), QStringLiteral("--talk-name=org.freedesktop.DBus"), m_buildDirectory.toLocalFile() } << process->program());
0118 
0119     qCDebug(FLATPAK) << "starting kprocess" << process->program().join(QLatin1Char(' '));
0120     process->start();
0121 }
0122 
0123 KJob* FlatpakRuntime::rebuild()
0124 {
0125     QDir(m_buildDirectory.toLocalFile()).removeRecursively();
0126     auto job = createBuildDirectory(m_buildDirectory, m_file, m_arch);
0127     refreshJson();
0128     return job;
0129 }
0130 
0131 QList<KJob*> FlatpakRuntime::exportBundle(const QString &path) const
0132 {
0133     const auto doc = config();
0134 
0135     auto* dir = new QTemporaryDir(QDir::tempPath()+QLatin1String("/flatpak-tmp-repo"));
0136     if (!dir->isValid() || doc.isEmpty()) {
0137         qCWarning(FLATPAK) << "Couldn't export:" << path << dir->isValid() << dir->path() << doc.isEmpty();
0138         return {};
0139     }
0140 
0141     const QString name = doc[QLatin1String("id")].toString();
0142     QStringList args = m_finishArgs;
0143     if (doc.contains(QLatin1String("command")))
0144         args << QLatin1String("--command=")+doc[QLatin1String("command")].toString();
0145 
0146     const QString title = i18n("Bundling");
0147     const QList<KJob*> jobs = {
0148         createExecuteJob(QStringList{QStringLiteral("flatpak"), QStringLiteral("build-finish"), m_buildDirectory.toLocalFile()} << args, title, {}, false),
0149         createExecuteJob(QStringList{QStringLiteral("flatpak"), QStringLiteral("build-export"), QLatin1String("--arch=")+m_arch, dir->path(), m_buildDirectory.toLocalFile()}, title),
0150         createExecuteJob(QStringList{QStringLiteral("flatpak"), QStringLiteral("build-bundle"), QLatin1String("--arch=")+m_arch, dir->path(), path, name }, title)
0151     };
0152     connect(jobs.last(), &QObject::destroyed, jobs.last(), [dir]() { delete dir; });
0153     return jobs;
0154 }
0155 
0156 QString FlatpakRuntime::name() const
0157 {
0158     return QStringLiteral("%1 - %2").arg(m_arch, m_file.lastPathSegment());
0159 }
0160 
0161 KJob * FlatpakRuntime::executeOnDevice(const QString& host, const QString &path) const
0162 {
0163     const QString name = config()[QLatin1String("id")].toString();
0164     const QString destPath = QStringLiteral("/tmp/kdevelop-test-app.flatpak");
0165     const QString replicatePath = QStringLiteral("/tmp/replicate.sh");
0166     const QString localReplicatePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("kdevflatpak/replicate.sh"));
0167 
0168     const QString title = i18n("Run on Device");
0169     const QList<KJob*> jobs = exportBundle(path) << QList<KJob*> {
0170         createExecuteJob({QStringLiteral("scp"), path, host+QLatin1Char(':')+destPath}, title),
0171         createExecuteJob({QStringLiteral("scp"), localReplicatePath, host+QLatin1Char(':')+replicatePath}, title),
0172         createExecuteJob({QStringLiteral("ssh"), host, QStringLiteral("flatpak"), QStringLiteral("install"), QStringLiteral("--user"), QStringLiteral("--bundle"), QStringLiteral("-y"), destPath}, title),
0173         createExecuteJob({QStringLiteral("ssh"), host, QStringLiteral("bash"), replicatePath, QStringLiteral("plasmashell"), QStringLiteral("flatpak"), QStringLiteral("run"), name }, title),
0174     };
0175     return new KDevelop::ExecuteCompositeJob( parent(), jobs );
0176 }
0177 
0178 QJsonObject FlatpakRuntime::config(const KDevelop::Path& path)
0179 {
0180     QFile f(path.toLocalFile());
0181     if (!f.open(QIODevice::ReadOnly)) {
0182         qCWarning(FLATPAK) << "couldn't open" << path;
0183         return {};
0184     }
0185 
0186     QJsonParseError error;
0187     auto doc = QJsonDocument::fromJson(f.readAll(), &error);
0188     if (error.error) {
0189         qCWarning(FLATPAK) << "couldn't parse" << path << error.errorString();
0190         return {};
0191     }
0192 
0193     return doc.object();
0194 }
0195 
0196 QJsonObject FlatpakRuntime::config() const
0197 {
0198     return config(m_file);
0199 }
0200 
0201 Path FlatpakRuntime::pathInHost(const KDevelop::Path& runtimePath) const
0202 {
0203     KDevelop::Path ret = runtimePath;
0204     if (!runtimePath.isLocalFile()) {
0205         return ret;
0206     }
0207 
0208     const auto prefix = runtimePath.segments().at(0);
0209     if (prefix == QLatin1String("usr")) {
0210         const auto relpath = KDevelop::Path(QStringLiteral("/usr")).relativePath(runtimePath);
0211         ret = Path(m_sdkPath, relpath);
0212     } else if (prefix == QLatin1String("app")) {
0213         const auto relpath = KDevelop::Path(QStringLiteral("/app")).relativePath(runtimePath);
0214         ret = Path(m_buildDirectory, QLatin1String("/active/files/") + relpath);
0215     }
0216 
0217     qCDebug(FLATPAK) << "path in host" << runtimePath << ret;
0218     return ret;
0219 }
0220 
0221 Path FlatpakRuntime::pathInRuntime(const KDevelop::Path& localPath) const
0222 {
0223     KDevelop::Path ret = localPath;
0224     if (m_sdkPath.isParentOf(localPath)) {
0225         const auto relpath = m_sdkPath.relativePath(localPath);
0226         ret = Path(Path(QStringLiteral("/usr")), relpath);
0227     } else {
0228         const Path bdfiles(m_buildDirectory, QStringLiteral("/active/files"));
0229         if (bdfiles.isParentOf(localPath)) {
0230             const auto relpath = bdfiles.relativePath(localPath);
0231             ret = Path(Path(QStringLiteral("/app")), relpath);
0232         }
0233     }
0234 
0235     qCDebug(FLATPAK) << "path in runtime" << localPath << ret;
0236     return ret;
0237 }
0238 
0239 QString FlatpakRuntime::findExecutable(const QString& executableName) const
0240 {
0241     QStringList rtPaths;
0242 
0243     auto envPaths = getenv(QByteArrayLiteral("PATH")).split(':');
0244     std::transform(envPaths.begin(), envPaths.end(), std::back_inserter(rtPaths),
0245                     [this](QByteArray p) {
0246                         return pathInHost(Path(QString::fromLocal8Bit(p))).toLocalFile();
0247                     });
0248 
0249     return QStandardPaths::findExecutable(executableName, rtPaths);
0250 }
0251 
0252 QByteArray FlatpakRuntime::getenv(const QByteArray& varname) const
0253 {
0254     if (varname == "KDEV_DEFAULT_INSTALL_PREFIX")
0255         return "/app";
0256     return qgetenv(varname.constData());
0257 }
0258 
0259 KDevelop::Path FlatpakRuntime::buildPath() const
0260 {
0261     auto file = m_file;
0262     file.setLastPathSegment(QStringLiteral(".flatpak-builder"));
0263     file.addPath(QStringLiteral("kdevelop"));
0264     return file;
0265 }
0266 
0267 #include "moc_flatpakruntime.cpp"