diff --git a/interfaces/CMakeLists.txt b/interfaces/CMakeLists.txt --- a/interfaces/CMakeLists.txt +++ b/interfaces/CMakeLists.txt @@ -41,6 +41,8 @@ itoolviewactionlistener.cpp ilanguagecheck.cpp ilanguagecheckprovider.cpp + iruntime.cpp + iruntimecontroller.cpp ) configure_file(ipluginversion.h.in ${CMAKE_CURRENT_BINARY_DIR}/ipluginversion.h) @@ -97,6 +99,8 @@ itestcontroller.h itoolviewactionlistener.h iproblem.h + iruntime.h + iruntimecontroller.h ${CMAKE_CURRENT_BINARY_DIR}/ipluginversion.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/interfaces COMPONENT Devel ) diff --git a/interfaces/icore.h b/interfaces/icore.h --- a/interfaces/icore.h +++ b/interfaces/icore.h @@ -50,6 +50,7 @@ class IPartController; class IDashboardController; class ITestController; +class IRuntimeController; /** * ICore is the container class for all the various objects in use by @@ -121,6 +122,9 @@ /** @return the test controller */ virtual KDevelop::ITestController* testController() = 0; + /** @return the runtime controller */ + Q_SCRIPTABLE virtual KDevelop::IRuntimeController* runtimeController() = 0; + /** @return the about data of the framework, different from the main about data which is created by the application */ virtual KAboutData aboutData() const = 0; diff --git a/interfaces/iruntime.h b/interfaces/iruntime.h new file mode 100644 --- /dev/null +++ b/interfaces/iruntime.h @@ -0,0 +1,100 @@ +/* This file is part of KDevelop + Copyright 2017 Aleix Pol + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVPLATFORM_IRUNTIME_H +#define KDEVPLATFORM_IRUNTIME_H + +#include "interfacesexport.h" +#include +#include + +class QProcess; +class KProcess; + +namespace KDevelop +{ +class Path; + +/** + * A runtime represents an environment we develop against + * + * It allows the IDE to interact with systems that differ from process where + * the process we are running in. + * + * It allows to execute processes into them and translate the paths these runtimes + * offer into ones that will be visible to our process so we can introspect the + * platform we are developing for as well. + */ +class KDEVPLATFORMINTERFACES_EXPORT IRuntime : public QObject +{ + Q_OBJECT +public: + ~IRuntime() override; + + /** @returns a display string that identifies the runtime */ + virtual QString name() const = 0; + + /** + * Adapts the @p process and starts it within the environment + * + * Gives an opportunity to the runtime to set up environment variables + * or process the execution in any way necessary. + */ + virtual void startProcess(QProcess* process) const = 0; + + /** + * @see startProcess(QProcess*) + */ + virtual void startProcess(KProcess* process) const = 0; + + /** + * Given a @p localPath from our process's file system + * @returns the path that the runtime's environment can use + */ + virtual Path pathInRuntime(const Path& localPath) const = 0; + + /** + * Given a @p runtimePath from the runtime + * @returns the path in our file system scope that maps to the runtime's + */ + virtual Path pathInHost(const Path& runtimePath) const = 0; + + /** + * @returns the value for an environment variable in the runtime + */ + virtual QByteArray getenv(const QByteArray& varname) const = 0; + +protected: + friend class RuntimeController; + + /** + * notifies the runtime about its availability. + * + * This will be called exclusively from the IRuntimeController implementation. + * + * @see IRuntimeController::setCurrentRuntime + */ + virtual void setEnabled(bool enabled) = 0; + +}; + +} + +#endif + diff --git a/interfaces/iruntime.cpp b/interfaces/iruntime.cpp new file mode 100644 --- /dev/null +++ b/interfaces/iruntime.cpp @@ -0,0 +1,24 @@ +/* This file is part of KDevelop + Copyright 2017 Aleix Pol + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "iruntime.h" + +using namespace KDevelop; + +KDevelop::IRuntime::~IRuntime() = default; diff --git a/interfaces/iruntimecontroller.h b/interfaces/iruntimecontroller.h new file mode 100644 --- /dev/null +++ b/interfaces/iruntimecontroller.h @@ -0,0 +1,76 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVPLATFORM_IRUNTIMECONTROLLER_H +#define KDEVPLATFORM_IRUNTIMECONTROLLER_H + +#include +#include +#include + +namespace KDevelop { +class IRuntime; + +/** + * @brief Exposes runtimes + * + * Makes it possible to have the IDE develop for different platforms other + * than the local host by having available different runtimes that will give + * access to the target systems. + * + * Allows to add runtimes, list them and offer a currentRuntime. + * The currentRuntime will be the runtime towards which all the runtime and build + * information * gathered by the IDE will be fetched from. + * + * @see IRuntime + */ +class KDEVPLATFORMINTERFACES_EXPORT IRuntimeController: public QObject +{ + Q_OBJECT +public: + IRuntimeController(); + ~IRuntimeController() override; + + /** + * Makes @p runtimes available to be used. + */ + virtual void addRuntimes(KDevelop::IRuntime* runtimes) = 0; + + /** + * Lists available runtimes + */ + virtual QVector availableRuntimes() const = 0; + + /** + * Sets @p runtime as the currently used runtime and emits currentRuntimeChanged + * so the IDE can adapt, if necessary. + */ + virtual void setCurrentRuntime(IRuntime* runtime) = 0; + + /** + * @returns the current runtime + */ + virtual IRuntime* currentRuntime() const = 0; + +Q_SIGNALS: + void currentRuntimeChanged(IRuntime* currentRuntime); +}; + +} + +#endif diff --git a/interfaces/iruntimecontroller.cpp b/interfaces/iruntimecontroller.cpp new file mode 100644 --- /dev/null +++ b/interfaces/iruntimecontroller.cpp @@ -0,0 +1,23 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "iruntimecontroller.h" + +KDevelop::IRuntimeController::IRuntimeController() = default; + +KDevelop::IRuntimeController::~IRuntimeController() = default; diff --git a/outputview/outputexecutejob.h b/outputview/outputexecutejob.h --- a/outputview/outputexecutejob.h +++ b/outputview/outputexecutejob.h @@ -222,6 +222,15 @@ void start() override; + /** + * If @p executeHost is enabled, the process will be executed in the local host. + * Otherwise the currentRuntime will be used to execute the process. + * + * @sa IRuntimeController::setCurrentRuntime() + */ + void setExecuteOnHost(bool executeHost); + bool executeOnHost() const; + protected: bool doKill() override; diff --git a/outputview/outputexecutejob.cpp b/outputview/outputexecutejob.cpp --- a/outputview/outputexecutejob.cpp +++ b/outputview/outputexecutejob.cpp @@ -21,6 +21,9 @@ #include "outputmodel.h" #include "outputdelegate.h" #include "debug.h" +#include +#include +#include #include #include #include @@ -65,6 +68,7 @@ QHash m_environmentOverrides; QString m_jobName; bool m_outputStarted; + bool m_executeOnHost = false; }; OutputExecuteJobPrivate::OutputExecuteJobPrivate( OutputExecuteJob* owner ) : @@ -258,15 +262,6 @@ startOutput(); } - const QString joinedCommandLine = d->joinCommandLine(); - QString headerLine; - if( !effectiveWorkingDirectory.isEmpty() ) { - headerLine = effectiveWorkingDirectory.toString( QUrl::PreferLocalFile | QUrl::StripTrailingSlash ) + "> " + joinedCommandLine; - } else { - headerLine = joinedCommandLine; - } - model()->appendLine( headerLine ); - if( !effectiveWorkingDirectory.isEmpty() ) { d->m_process->setWorkingDirectory( effectiveWorkingDirectory.toLocalFile() ); } @@ -277,15 +272,19 @@ d->m_process->setProgram( d->effectiveCommandLine() ); // there is no way to input data in the output view so redirect stdin to the null device d->m_process->setStandardInputFile(QProcess::nullDevice()); - qCDebug(OUTPUTVIEW) << "Starting:" << d->m_process->program().join(QStringLiteral(" ")) << "in" << d->m_process->workingDirectory(); - d->m_process->start(); + qCDebug(OUTPUTVIEW) << "Starting:" << d->effectiveCommandLine() << d->m_process->program() << "in" << d->m_process->workingDirectory(); + if (d->m_executeOnHost) { + d->m_process->start(); + } else { + KDevelop::ICore::self()->runtimeController()->currentRuntime()->startProcess(d->m_process); + } + model()->appendLine(d->m_process->workingDirectory() + QStringLiteral("> ") + KShell::joinArgs(d->m_process->program())); } else { - QString errorMessage = i18n("Failed to specify program to start"); + QString errorMessage = i18n("Failed to specify program to start: %1", d->joinCommandLine()); model()->appendLine( i18n( "*** %1 ***", errorMessage) ); setErrorText(errorMessage); setError( FailedShownError ); emitResult(); - return; } } @@ -473,6 +472,17 @@ d->m_environmentOverrides.remove( name ); } + +void OutputExecuteJob::setExecuteOnHost(bool executeHost) +{ + d->m_executeOnHost = executeHost; +} + +bool OutputExecuteJob::executeOnHost() const +{ + return d->m_executeOnHost; +} + template< typename T > void OutputExecuteJobPrivate::mergeEnvironment( QProcessEnvironment& dest, const T& src ) { diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -22,6 +22,10 @@ ecm_optional_add_subdirectory(bazaar) ecm_optional_add_subdirectory(perforce) add_subdirectory(vcschangesview) +if (UNIX) + add_subdirectory(flatpak) + add_subdirectory(docker) +endif () if (Grantlee5_FOUND) add_subdirectory(filetemplates) add_subdirectory(codeutils) diff --git a/plugins/docker/CMakeLists.txt b/plugins/docker/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/plugins/docker/CMakeLists.txt @@ -0,0 +1,21 @@ +add_subdirectory(tests) + +add_definitions(-DTRANSLATION_DOMAIN=\"kdevdocker\") + +ecm_qt_declare_logging_category(dockerplugin_SRCS + HEADER debug_docker.h + IDENTIFIER DOCKER + CATEGORY_NAME kdevplatform.plugins.docker +) +qt5_add_resources(dockerplugin_SRCS kdevdockerplugin.qrc) +ki18n_wrap_ui(dockerplugin_SRCS dockerpreferences.ui) +kconfig_add_kcfg_files(dockerplugin_SRCS dockerpreferencessettings.kcfgc) + +kdevplatform_add_plugin(kdevdocker JSON kdevdocker.json SOURCES dockerplugin.cpp dockerruntime.cpp dockerpreferences.cpp ${dockerplugin_SRCS}) +target_link_libraries(kdevdocker + KF5::CoreAddons + KDev::Interfaces + KDev::Util + KDev::OutputView + KDev::Project +) diff --git a/plugins/docker/Messages.sh b/plugins/docker/Messages.sh new file mode 100644 --- /dev/null +++ b/plugins/docker/Messages.sh @@ -0,0 +1,4 @@ +#!/bin/sh +$EXTRACTRC `find . -name \*.rc` `find . -name \*.ui` >> rc.cpp +$XGETTEXT `find . -name \*.cc -o -name \*.cpp -o -name \*.h` -o $podir/kdevdocker.pot +rm -f rc.cpp diff --git a/plugins/docker/dockerplugin.h b/plugins/docker/dockerplugin.h new file mode 100644 --- /dev/null +++ b/plugins/docker/dockerplugin.h @@ -0,0 +1,54 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef DOCKERRUNTIMEPROVIDER_H +#define DOCKERRUNTIMEPROVIDER_H + +#include +#include +#include +#include + +class DockerPreferencesSettings; + +class DockerPlugin : public KDevelop::IPlugin +{ + Q_OBJECT +public: + DockerPlugin(QObject *parent, const QVariantList & args); + ~DockerPlugin() override; + + KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context * context) override; + + void imagesListFinished(int code); + + int configPages() const override; + KDevelop::ConfigPage* configPage(int number, QWidget* parent) override; + +Q_SIGNALS: + ///only used by the test + void imagesListed(); + +private: + void runtimeChanged(KDevelop::IRuntime* newRuntime); + + QHash m_runtimes; + QScopedPointer m_settings; +}; + +#endif diff --git a/plugins/docker/dockerplugin.cpp b/plugins/docker/dockerplugin.cpp new file mode 100644 --- /dev/null +++ b/plugins/docker/dockerplugin.cpp @@ -0,0 +1,170 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "dockerplugin.h" +#include "dockerruntime.h" +#include "dockerpreferences.h" +#include "dockerpreferencessettings.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(KDevDockerFactory, "kdevdocker.json", registerPlugin();) + +using namespace KDevelop; + +DockerPlugin::DockerPlugin(QObject *parent, const QVariantList & /*args*/) + : KDevelop::IPlugin( QStringLiteral("kdevdocker"), parent ) + , m_settings(new DockerPreferencesSettings) +{ + runtimeChanged(ICore::self()->runtimeController()->currentRuntime()); + + setXMLFile( QStringLiteral("kdevdockerplugin.rc") ); + connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, &DockerPlugin::runtimeChanged); + + QProcess* process = new QProcess(this); + connect(process, static_cast(&QProcess::finished), this, &DockerPlugin::imagesListFinished); + process->start(QStringLiteral("docker"), {QStringLiteral("images"), QStringLiteral("--filter"), QStringLiteral("dangling=false"), QStringLiteral("--format"), QStringLiteral("{{.Repository}}:{{.Tag}}\t{{.ID}}")}, QIODevice::ReadOnly); + + DockerRuntime::s_settings = m_settings.data(); +} + +DockerPlugin::~DockerPlugin() +{ + DockerRuntime::s_settings = nullptr; +} + +void DockerPlugin::imagesListFinished(int code) +{ + if (code != 0) + return; + + QProcess* process = qobject_cast(sender()); + Q_ASSERT(process); + QTextStream stream(process); + while(!stream.atEnd()) { + const QString line = stream.readLine(); + const QStringList parts = line.split(QLatin1Char('\t')); + + const QString tag = parts[0] == QLatin1String("") ? parts[1] : parts[0]; + ICore::self()->runtimeController()->addRuntimes(new DockerRuntime(tag)); + } + + process->deleteLater(); + Q_EMIT imagesListed(); +} + +void DockerPlugin::runtimeChanged(KDevelop::IRuntime* newRuntime) +{ + const bool isDocker = qobject_cast(newRuntime); + + for(auto action: actionCollection()->actions()) { + action->setEnabled(isDocker); + } +} + +KDevelop::ContextMenuExtension DockerPlugin::contextMenuExtension(KDevelop::Context* context) +{ + QList urls; + + if ( context->type() == KDevelop::Context::FileContext ) { + KDevelop::FileContext* filectx = dynamic_cast( context ); + urls = filectx->urls(); + } else if ( context->type() == KDevelop::Context::ProjectItemContext ) { + KDevelop::ProjectItemContext* projctx = dynamic_cast( context ); + foreach( KDevelop::ProjectBaseItem* item, projctx->items() ) { + if ( item->file() ) { + urls << item->path().toUrl(); + } + } + } + + for(auto it = urls.begin(); it != urls.end(); ) { + if (it->isLocalFile() && it->fileName() == QLatin1String("Dockerfile")) { + ++it; + } else { + it = urls.erase(it); + } + } + + if ( !urls.isEmpty() ) { + KDevelop::ContextMenuExtension ext; + foreach(const QUrl &url, urls) { + const KDevelop::Path file(url); + + auto action = new QAction(QIcon::fromTheme("text-dockerfile"), i18n("docker build '%1'", file.path()), this); + connect(action, &QAction::triggered, this, [this, file]() { + const auto dir = file.parent(); + const QString name = QInputDialog::getText( + ICore::self()->uiController()->activeMainWindow(), i18n("Choose tag name..."), + i18n("Tag name for '%1'", file.path()), + QLineEdit::Normal, dir.lastPathSegment() + ); + + auto process = new OutputExecuteJob; + process->setExecuteOnHost(true); + *process << QStringList{"docker", "build", "--tag", name, dir.toLocalFile()}; + connect(process, &KJob::finished, this, [this, name] (KJob* job) { + if (job->error() != 0) + return; + + ICore::self()->runtimeController()->addRuntimes({ new DockerRuntime(name) }); + }); + process->start(); + }); + ext.addAction(KDevelop::ContextMenuExtension::RunGroup, action); + } + + return ext; + } + + return KDevelop::IPlugin::contextMenuExtension( context ); +} + +int DockerPlugin::configPages() const +{ + return 1; +} + +KDevelop::ConfigPage* DockerPlugin::configPage(int number, QWidget* parent) +{ + if (number == 0) { + return new DockerPreferences(this, m_settings.data(), parent); + } + return nullptr; +} + +#include "dockerplugin.moc" diff --git a/plugins/docker/dockerpreferences.h b/plugins/docker/dockerpreferences.h new file mode 100644 --- /dev/null +++ b/plugins/docker/dockerpreferences.h @@ -0,0 +1,39 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef DOCKERPREFERENCES_H +#define DOCKERPREFERENCES_H + +#include +#include + +namespace Ui { class DockerPreferences; } + +class DockerPreferences : public KDevelop::ConfigPage +{ + Q_OBJECT +public: + explicit DockerPreferences(KDevelop::IPlugin* plugin, KCoreConfigSkeleton* config, QWidget* parent = nullptr); + ~DockerPreferences() override; + + QString name() const override; +private: + QScopedPointer m_prefsUi; +}; + +#endif diff --git a/plugins/docker/dockerpreferences.cpp b/plugins/docker/dockerpreferences.cpp new file mode 100644 --- /dev/null +++ b/plugins/docker/dockerpreferences.cpp @@ -0,0 +1,34 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "dockerpreferences.h" +#include "ui_dockerpreferences.h" + +DockerPreferences::DockerPreferences(KDevelop::IPlugin* plugin, KCoreConfigSkeleton* config, QWidget* parent) + : KDevelop::ConfigPage(plugin, config, parent) +{ + auto m_prefsUi = new Ui::DockerPreferences; + m_prefsUi->setupUi(this); +} + +DockerPreferences::~DockerPreferences() = default; + +QString DockerPreferences::name() const +{ + return QStringLiteral("Docker"); +} diff --git a/plugins/docker/dockerpreferences.ui b/plugins/docker/dockerpreferences.ui new file mode 100644 --- /dev/null +++ b/plugins/docker/dockerpreferences.ui @@ -0,0 +1,41 @@ + + + DockerPreferences + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + 'docker run' arguments: + + + + + + + + + + Projects volume + + + + + + + + + + + diff --git a/plugins/docker/dockerpreferencessettings.kcfg b/plugins/docker/dockerpreferencessettings.kcfg new file mode 100644 --- /dev/null +++ b/plugins/docker/dockerpreferencessettings.kcfg @@ -0,0 +1,12 @@ + + + + + + /src + /build + + diff --git a/plugins/docker/dockerpreferencessettings.kcfgc b/plugins/docker/dockerpreferencessettings.kcfgc new file mode 100644 --- /dev/null +++ b/plugins/docker/dockerpreferencessettings.kcfgc @@ -0,0 +1,3 @@ +ClassName=DockerPreferencesSettings +File=dockerpreferencessettings.kcfg + diff --git a/plugins/docker/dockerruntime.h b/plugins/docker/dockerruntime.h new file mode 100644 --- /dev/null +++ b/plugins/docker/dockerruntime.h @@ -0,0 +1,90 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef DOCKERRUNTIME_H +#define DOCKERRUNTIME_H + +#include +#include +#include + +class KJob; +class DockerPreferencesSettings; + +class DockerRuntime : public KDevelop::IRuntime +{ + Q_OBJECT +public: + DockerRuntime(const QString& tag); + ~DockerRuntime() override; + + /** + * @returns the docker tagname as a text identifier + */ + QString name() const override { return m_tag; } + + /** + * if @p enabled + * Mounts the docker image's file system into a subdirectory the user can read. + * if not @p enabled, it unmounts the image file system + * + * See GraphDriver.Data.UpperDir value in docker image inspect imagename + * + * Both require root privileges for now + */ + void setEnabled(bool enabled) override; + + /** + * Call processes using "docker run..." passing on the proper environment and volumes + * + * Volumes will include source and build directories that need to be exposed + * into the container. + */ + void startProcess(KProcess *process) const override; + void startProcess(QProcess *process) const override; + + /** + * Translates @p runtimePath from within the image into the host + * + * Takes into account the mounted upperDir and the different volumes set up + */ + KDevelop::Path pathInHost(const KDevelop::Path & runtimePath) const override; + + /** + * Translates @p localPath into a path that can be accessed by the runtime + */ + KDevelop::Path pathInRuntime(const KDevelop::Path & localPath) const override; + + /** + * @returns the environment variable with @p varname set by the recipe (usually the Dockerfile) + */ + QByteArray getenv(const QByteArray & varname) const override; + + static DockerPreferencesSettings* s_settings; + +private: + void inspectImage(); + QStringList workingDirArgs(QProcess* process) const; + + const QString m_tag; + QHash m_envs; + KDevelop::Path m_upperDir; + KDevelop::Path m_userUpperDir; +}; + +#endif diff --git a/plugins/docker/dockerruntime.cpp b/plugins/docker/dockerruntime.cpp new file mode 100644 --- /dev/null +++ b/plugins/docker/dockerruntime.cpp @@ -0,0 +1,234 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "dockerruntime.h" +#include "dockerpreferencessettings.h" +#include "debug_docker.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace KDevelop; + +DockerPreferencesSettings* DockerRuntime::s_settings = nullptr; + +DockerRuntime::DockerRuntime(const QString &tag) + : KDevelop::IRuntime() + , m_tag(tag) +{ + setObjectName(tag); + inspectImage(); +} + +void DockerRuntime::inspectImage() +{ + QProcess* process = new QProcess(this); + connect(process, static_cast(&QProcess::finished), this, [process, this](int code, QProcess::ExitStatus status){ + process->deleteLater(); + qCDebug(DOCKER) << "inspect upper dir" << code << status; + if (code || status) { + qCWarning(DOCKER) << "Could not figure out the upperDir of" << m_tag; + return; + } + m_upperDir = Path(QFile::decodeName(process->readAll().trimmed())); + qCDebug(DOCKER) << "upper dir:" << m_tag << m_upperDir; + }); + process->start("docker", {"image", "inspect", m_tag, "--format", "{{.GraphDriver.Data.UpperDir}}"}); + + QProcess* processEnvs = new QProcess(this); + connect(processEnvs, static_cast(&QProcess::finished), this, [processEnvs, this](int code, QProcess::ExitStatus status){ + processEnvs->deleteLater(); + qCDebug(DOCKER) << "inspect envs" << code << status; + if (code || status) { + qCWarning(DOCKER) << "Could not figure out the environment variables of" << m_tag; + return; + } + + const auto list = processEnvs->readAll(); + const auto data = list.mid(1, list.size()-3); + const auto entries = data.split(' '); + + for (auto entry : entries) { + const auto content = entry.split('='); + if (content.count() != 2) + continue; + m_envs.insert(content[0], content[1]); + } + + qCDebug(DOCKER) << "envs:" << m_tag << m_envs; + }); + processEnvs->start("docker", {"image", "inspect", m_tag, "--format", "{{.Config.Env}}"}); +} + +DockerRuntime::~DockerRuntime() +{ +} + +QByteArray DockerRuntime::getenv(const QByteArray& varname) const +{ + return m_envs.value(varname); +} + +void DockerRuntime::setEnabled(bool enable) +{ + if (enable) { + m_userUpperDir = KDevelop::Path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/docker-" + QString(m_tag).replace('/', '_')); + QDir().mkpath(m_userUpperDir.toLocalFile()); + + const QStringList cmd = {"pkexec", "bindfs", "--map=root/"+KUser().loginName(), m_upperDir.toLocalFile(), m_userUpperDir.toLocalFile() }; + QProcess p; + p.start(cmd.first(), cmd.mid(1)); + p.waitForFinished(); + if (p.exitCode()) { + qCDebug(DOCKER) << "bindfs returned" << m_upperDir << m_userUpperDir << cmd << p.exitCode() << p.readAll(); + } + } else { + int code = QProcess::execute(QStringLiteral("pkexec"), {"umount", m_userUpperDir.toLocalFile()}); + qCDebug(DOCKER) << "umount returned" << code; + } +} + +static QString ensureEndsSlash(const QString &string) +{ + return string.endsWith('/') ? string : (string + QLatin1Char('/')); +} + +static QStringList projectVolumes() +{ + QStringList ret; + const QString dir = ensureEndsSlash(DockerRuntime::s_settings->projectsVolume()); + const QString buildDir = ensureEndsSlash(DockerRuntime::s_settings->buildDirsVolume()); + + for (IProject* project: ICore::self()->projectController()->projects()) { + const Path path = project->path(); + if (path.isLocalFile()) { + ret << "--volume" << QStringLiteral("%1:%2").arg(path.toLocalFile(), dir + path.lastPathSegment()); + } + + const auto ibsm = project->buildSystemManager(); + if (ibsm) { + ret << "--volume" << ibsm->buildDirectory(project->projectItem()).toLocalFile() + QLatin1Char(':') + buildDir + path.lastPathSegment(); + } + } + return ret; +} + +QStringList DockerRuntime::workingDirArgs(QProcess* process) const +{ + const auto wd = process->workingDirectory(); + return wd.isEmpty() ? QStringList{} : QStringList{"-w", pathInRuntime(KDevelop::Path(wd)).toLocalFile()}; +} + +void DockerRuntime::startProcess(QProcess* process) const +{ + const QStringList args = QStringList{"run", "--rm"} << workingDirArgs(process) << KShell::splitArgs(s_settings->extraArguments()) << projectVolumes() << m_tag << process->program() << process->arguments(); + process->setProgram("docker"); + process->setArguments(args); + + qCDebug(DOCKER) << "starting qprocess" << process->program() << process->arguments(); + process->start(); +} + +void DockerRuntime::startProcess(KProcess* process) const +{ + process->setProgram(QStringList{ "docker", "run", "--rm" } << workingDirArgs(process) << KShell::splitArgs(s_settings->extraArguments()) << projectVolumes() << m_tag << process->program()); + + qCDebug(DOCKER) << "starting kprocess" << process->program().join(' '); + process->start(); +} + +static Path projectRelPath(const KDevelop::Path & projectsDir, const KDevelop::Path& runtimePath, bool sourceDir) +{ + const auto relPath = projectsDir.relativePath(runtimePath); + const int index = relPath.indexOf(QLatin1Char('/')); + auto project = ICore::self()->projectController()->findProjectByName(relPath.left(index)); + + if (!project) { + qCWarning(DOCKER) << "No project for" << relPath; + } else { + const auto repPathProject = index < 0 ? QString() : relPath.mid(index+1); + const auto rootPath = sourceDir ? project->path() : project->buildSystemManager()->buildDirectory(project->projectItem()); + return Path(rootPath, repPathProject); + } + return {}; +} + +KDevelop::Path DockerRuntime::pathInHost(const KDevelop::Path& runtimePath) const +{ + Path ret; + const Path projectsDir(DockerRuntime::s_settings->projectsVolume()); + if (runtimePath==projectsDir || projectsDir.isParentOf(runtimePath)) { + ret = projectRelPath(projectsDir, runtimePath, true); + } else { + const Path buildDirs(DockerRuntime::s_settings->buildDirsVolume()); + if (runtimePath==buildDirs || buildDirs.isParentOf(runtimePath)) { + ret = projectRelPath(buildDirs, runtimePath, false); + } else + ret = KDevelop::Path(m_userUpperDir, KDevelop::Path(QStringLiteral("/")).relativePath(runtimePath)); + } + qCDebug(DOCKER) << "pathInHost" << ret << runtimePath; + return ret; +} + +KDevelop::Path DockerRuntime::pathInRuntime(const KDevelop::Path& localPath) const +{ + if (m_userUpperDir==localPath || m_userUpperDir.isParentOf(localPath)) { + KDevelop::Path ret(KDevelop::Path("/"), m_userUpperDir.relativePath(localPath)); + qCDebug(DOCKER) << "docker runtime pathInRuntime..." << ret << localPath; + return ret; + } else if (auto project = ICore::self()->projectController()->findProjectForUrl(localPath.toUrl())) { + const Path projectsDir(DockerRuntime::s_settings->projectsVolume()); + const QString relpath = project->path().relativePath(localPath); + const KDevelop::Path ret(projectsDir, project->name() + QLatin1Char('/') + relpath); + qCDebug(DOCKER) << "docker user pathInRuntime..." << ret << localPath; + return ret; + } else { + const auto projects = ICore::self()->projectController()->projects(); + for (auto project : projects) { + auto ibsm = project->buildSystemManager(); + if (ibsm) { + const auto builddir = ibsm->buildDirectory(project->projectItem()); + if (builddir != localPath && !builddir.isParentOf(localPath)) + continue; + + const Path builddirs(DockerRuntime::s_settings->buildDirsVolume()); + const QString relpath = builddir.relativePath(localPath); + const KDevelop::Path ret(builddirs, project->name() + QLatin1Char('/') + relpath); + qCDebug(DOCKER) << "docker build pathInRuntime..." << ret << localPath; + return ret; + } + } + qCWarning(DOCKER) << "only project files are available on the docker runtime" << localPath; + } + qCDebug(DOCKER) << "bypass..." << localPath; + return localPath; +} + diff --git a/plugins/docker/kdevdocker.json b/plugins/docker/kdevdocker.json new file mode 100644 --- /dev/null +++ b/plugins/docker/kdevdocker.json @@ -0,0 +1,17 @@ +{ + "KPlugin": { + "Authors": [ + { "Name": "Aleix Pol", "Email": "aleixpol@kde.org" } + ], + "Category": "Global", + "Description": "Exposes Docker runtimes", + "Icon": "docker", + "Id": "kdevdocker", + "License": "GPL", + "Name": "Docker Support", + "ServiceTypes": [ "KDevelop/Plugin" ], + "Version": "0.1" + }, + "X-KDevelop-Category": "Global", + "X-KDevelop-Mode": "NoGUI" +} diff --git a/plugins/docker/kdevdockerplugin.qrc b/plugins/docker/kdevdockerplugin.qrc new file mode 100644 --- /dev/null +++ b/plugins/docker/kdevdockerplugin.qrc @@ -0,0 +1,6 @@ + + + + kdevdockerplugin.rc + + diff --git a/plugins/docker/kdevdockerplugin.rc b/plugins/docker/kdevdockerplugin.rc new file mode 100644 --- /dev/null +++ b/plugins/docker/kdevdockerplugin.rc @@ -0,0 +1,7 @@ + + + + + + + diff --git a/plugins/docker/tests/CMakeLists.txt b/plugins/docker/tests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/plugins/docker/tests/CMakeLists.txt @@ -0,0 +1,2 @@ +ecm_add_test(test_docker.cpp + LINK_LIBRARIES Qt5::Test Qt5::Core KDev::Interfaces KDevPlatformTests) diff --git a/plugins/docker/tests/test_docker.cpp b/plugins/docker/tests/test_docker.cpp new file mode 100644 --- /dev/null +++ b/plugins/docker/tests/test_docker.cpp @@ -0,0 +1,116 @@ +/*************************************************************************** + * This file was partly taken from KDevelop's cvs plugin * + * Copyright 2017 Aleix Pol Gonzalez * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License or (at your option) version 3 or any later version * + * accepted by the membership of KDE e.V. (or its successor approved * + * by the membership of KDE e.V.), which shall act as a proxy * + * defined in Section 14 of version 3 of the license. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + ***************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace KDevelop; + +static QString s_testedImage = QStringLiteral("ubuntu:17.04"); + +class DockerTest: public QObject +{ + Q_OBJECT +public: + + DockerTest() { + QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false\ndefault.debug=true\nkdevelop.projectmanagers.cmake.debug=true\n")); + + auto ret = QProcess::execute("docker", {"pull", s_testedImage}); + Q_ASSERT(ret == 0); + + AutoTestShell::init({QStringLiteral("kdevdocker")}); + TestCore::initialize(); + + m_initialRuntime = ICore::self()->runtimeController()->currentRuntime(); + + auto plugin = ICore::self()->pluginController()->loadPlugin("kdevdocker"); + Q_ASSERT(plugin); + + QSignalSpy spy(plugin, SIGNAL(imagesListed())); + Q_ASSERT(spy.wait()); + + + auto projectPath = QUrl::fromLocalFile(QFINDTESTDATA("testproject/test.kdev4")); + qDebug() << "wuuu" << projectPath; + TestCore::self()->projectController()->openProject(projectPath); + QSignalSpy spy2(TestCore::self()->projectController(), &IProjectController::projectOpened); + Q_ASSERT(spy2.wait()); + + } + IRuntime* m_initialRuntime; + +private Q_SLOTS: + void initTestCase() { + QVERIFY(ICore::self()->runtimeController()->currentRuntime() == m_initialRuntime); + for(IRuntime* runtime : ICore::self()->runtimeController()->availableRuntimes()) { + if (s_testedImage == runtime->name()) { + ICore::self()->runtimeController()->setCurrentRuntime(runtime); + } + } + QVERIFY(ICore::self()->runtimeController()->currentRuntime() != m_initialRuntime); + } + + void paths() { + auto rt = ICore::self()->runtimeController()->currentRuntime(); + QVERIFY(rt); + + const Path root("/"); + const Path hostDir = rt->pathInHost(root); + QCOMPARE(root, rt->pathInRuntime(hostDir)); + } + + void projectPath() { + auto rt = ICore::self()->runtimeController()->currentRuntime(); + QVERIFY(rt); + auto project = ICore::self()->projectController()->projects().first(); + QVERIFY(project); + + const Path file = project->projectFile(); + QCOMPARE(file, rt->pathInRuntime(rt->pathInHost(file))); + QCOMPARE(project->path(), rt->pathInHost(rt->pathInRuntime(project->path()))); + } + + void envs() { + auto rt = ICore::self()->runtimeController()->currentRuntime(); + QVERIFY(rt); + + QCOMPARE(rt->getenv("PATH"), QByteArray("/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")); + } + + void cleanupTestCase() { + ICore::self()->runtimeController()->setCurrentRuntime(m_initialRuntime); + } + +}; + +QTEST_MAIN( DockerTest ) + +#include "test_docker.moc" diff --git a/plugins/docker/tests/testproject/test.kdev4 b/plugins/docker/tests/testproject/test.kdev4 new file mode 100644 --- /dev/null +++ b/plugins/docker/tests/testproject/test.kdev4 @@ -0,0 +1,3 @@ +[Project] +Name=test +Manager=KDevGenericManager diff --git a/plugins/docker/tests/testproject/testfile.sh b/plugins/docker/tests/testproject/testfile.sh new file mode 100644 --- /dev/null +++ b/plugins/docker/tests/testproject/testfile.sh @@ -0,0 +1 @@ +# what did you expect, you moron? diff --git a/plugins/executescript/scriptappjob.cpp b/plugins/executescript/scriptappjob.cpp --- a/plugins/executescript/scriptappjob.cpp +++ b/plugins/executescript/scriptappjob.cpp @@ -29,6 +29,8 @@ #include #include +#include +#include #include #include #include @@ -38,6 +40,7 @@ #include #include #include +#include #include "iexecutescriptplugin.h" #include "debug.h" @@ -86,7 +89,7 @@ setErrorText( i18n( "There is no active document to launch." ) ); return; } - script = document->url(); + script = ICore::self()->runtimeController()->currentRuntime()->pathInRuntime(KDevelop::Path(document->url())).toUrl(); } if( !err.isEmpty() ) @@ -145,7 +148,7 @@ { wc = QUrl::fromLocalFile( QFileInfo( script.toLocalFile() ).absolutePath() ); } - proc->setWorkingDirectory( wc.toLocalFile() ); + proc->setWorkingDirectory( ICore::self()->runtimeController()->currentRuntime()->pathInRuntime(KDevelop::Path(wc)).toLocalFile() ); proc->setProperty( "executable", interpreter.first() ); QStringList program; @@ -178,7 +181,7 @@ { startOutput(); appendLine( i18n("Starting: %1", proc->program().join(QLatin1Char( ' ' ) ) ) ); - proc->start(); + ICore::self()->runtimeController()->currentRuntime()->startProcess(proc); } else { qWarning() << "No process, something went wrong when creating the job"; diff --git a/plugins/flatpak/CMakeLists.txt b/plugins/flatpak/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/plugins/flatpak/CMakeLists.txt @@ -0,0 +1,19 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kdevflatpak\") + +ecm_qt_declare_logging_category(flatpakplugin_SRCS + HEADER debug_flatpak.h + IDENTIFIER FLATPAK + CATEGORY_NAME kdevplatform.plugins.flatpak +) + +qt5_add_resources(flatpakplugin_SRCS kdevflatpakplugin.qrc) +kdevplatform_add_plugin(kdevflatpak JSON kdevflatpak.json SOURCES flatpakplugin.cpp flatpakruntime.cpp ${flatpakplugin_SRCS}) +target_link_libraries(kdevflatpak + KF5::CoreAddons + KDev::Interfaces + KDev::Util + KDev::OutputView + KDev::Project +) + +install(FILES replicate.sh DESTINATION ${SHARE_INSTALL_PREFIX}/kdevelop/kdevflatpak/) diff --git a/plugins/flatpak/Messages.sh b/plugins/flatpak/Messages.sh new file mode 100644 --- /dev/null +++ b/plugins/flatpak/Messages.sh @@ -0,0 +1,4 @@ +#!/bin/sh +$EXTRACTRC `find . -name \*.rc` `find . -name \*.ui` >> rc.cpp +$XGETTEXT `find . -name \*.cc -o -name \*.cpp -o -name \*.h` -o $podir/kdevflatpak.pot +rm -f rc.cpp diff --git a/plugins/flatpak/flatpakplugin.h b/plugins/flatpak/flatpakplugin.h new file mode 100644 --- /dev/null +++ b/plugins/flatpak/flatpakplugin.h @@ -0,0 +1,47 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef FLATPAKRUNTIMEPROVIDER_H +#define FLATPAKRUNTIMEPROVIDER_H + +#include +#include +#include +#include + +class FlatpakPlugin : public KDevelop::IPlugin +{ +Q_OBJECT +public: + FlatpakPlugin(QObject *parent, const QVariantList & args); + ~FlatpakPlugin() override; + + KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context * context) override; + +private: + void runtimeChanged(KDevelop::IRuntime* newRuntime); + void rebuildCurrent(); + void exportCurrent(); + void setupArches(); + void executeOnRemoteDevice(); + void createRuntime(const KDevelop::Path &file, const QString &arch); + + QHash m_runtimes; +}; + +#endif diff --git a/plugins/flatpak/flatpakplugin.cpp b/plugins/flatpak/flatpakplugin.cpp new file mode 100644 --- /dev/null +++ b/plugins/flatpak/flatpakplugin.cpp @@ -0,0 +1,221 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "flatpakplugin.h" +#include "flatpakruntime.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(KDevFlatpakFactory, "kdevflatpak.json", registerPlugin();) + +using namespace KDevelop; + +FlatpakPlugin::FlatpakPlugin(QObject *parent, const QVariantList & /*args*/) + : KDevelop::IPlugin( QStringLiteral("kdevflatpak"), parent ) +{ + auto ac = actionCollection(); + + auto action = new QAction(QIcon::fromTheme(QStringLiteral("run-build-clean")), i18n("Rebuild environment"), this); + action->setWhatsThis(i18n("Recompiles all dependencies for a fresh environment.")); + ac->setDefaultShortcut(action, Qt::CTRL | Qt::META | Qt::Key_X); + connect(action, &QAction::triggered, this, &FlatpakPlugin::rebuildCurrent); + ac->addAction(QStringLiteral("runtime_flatpak_rebuild"), action); + + auto exportAction = new QAction(QIcon::fromTheme(QStringLiteral("document-export")), i18n("Export flatpak bundle..."), this); + exportAction->setWhatsThis(i18n("Exports the current build into a 'bundle.flatpak' file.")); + ac->setDefaultShortcut(exportAction, Qt::CTRL | Qt::META | Qt::Key_E); + connect(exportAction, &QAction::triggered, this, &FlatpakPlugin::exportCurrent); + ac->addAction(QStringLiteral("runtime_flatpak_export"), exportAction); + + auto remoteAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-remote-symbolic")), i18n("Send to device..."), this); + ac->setDefaultShortcut(remoteAction, Qt::CTRL | Qt::META | Qt::Key_D); + connect(remoteAction, &QAction::triggered, this, &FlatpakPlugin::executeOnRemoteDevice); + ac->addAction(QStringLiteral("runtime_flatpak_remote"), remoteAction); + + runtimeChanged(ICore::self()->runtimeController()->currentRuntime()); + + setXMLFile( QStringLiteral("kdevflatpakplugin.rc") ); + connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, &FlatpakPlugin::runtimeChanged); +} + +FlatpakPlugin::~FlatpakPlugin() = default; + +void FlatpakPlugin::runtimeChanged(KDevelop::IRuntime* newRuntime) +{ + const bool isFlatpak = qobject_cast(newRuntime); + + for(auto action: actionCollection()->actions()) { + action->setEnabled(isFlatpak); + } +} + +void FlatpakPlugin::rebuildCurrent() +{ + const auto runtime = qobject_cast(ICore::self()->runtimeController()->currentRuntime()); + Q_ASSERT(runtime); + ICore::self()->runController()->registerJob(runtime->rebuild()); +} + +void FlatpakPlugin::exportCurrent() +{ + const auto runtime = qobject_cast(ICore::self()->runtimeController()->currentRuntime()); + Q_ASSERT(runtime); + + const QString path = QFileDialog::getSaveFileName(ICore::self()->uiController()->activeMainWindow(), i18n("Export %1 to...", runtime->name()), {}, i18n("Flatpak Bundle (*.flatpak)")); + if (!path.isEmpty()) { + ICore::self()->runController()->registerJob(new ExecuteCompositeJob(runtime, runtime->exportBundle(path))); + } +} + +void FlatpakPlugin::createRuntime(const KDevelop::Path &file, const QString &arch) +{ + QTemporaryDir* dir = new QTemporaryDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/kdevelop-flatpak-")); + const KDevelop::Path path(dir->path()); + + auto process = FlatpakRuntime::createBuildDirectory(path, file, arch); + connect(process, &KJob::finished, this, [this, path, file, arch, dir] (KJob* job) { + if (job->error() != 0) { + delete dir; + return; + } + + auto rt = new FlatpakRuntime(path, file, arch); + connect(rt, &QObject::destroyed, rt, [dir]() { delete dir; }); + ICore::self()->runtimeController()->addRuntimes(rt); + }); + process->start(); +} + +static QStringList availableArches(const KDevelop::Path& url) +{ + QProcess supportedArchesProcess; + QStringList ret; + + QObject::connect(&supportedArchesProcess, QOverload::of(&QProcess::finished), &supportedArchesProcess, [&supportedArchesProcess, &ret]() { + supportedArchesProcess.deleteLater(); + + QTextStream stream(&supportedArchesProcess); + while (!stream.atEnd()) { + const QString line = stream.readLine(); + ret << line.section(QLatin1Char('/'), 2, 2); + } + }); + + const auto doc = FlatpakRuntime::config(url); + const QString sdkName = doc[QLatin1String("sdk")].toString(); + const QString runtimeVersion = doc[QLatin1String("runtime-version")].toString(); + supportedArchesProcess.start("flatpak", {"info", "-r", sdkName + "//" + runtimeVersion }); + supportedArchesProcess.waitForFinished(); + return ret; +} + +KDevelop::ContextMenuExtension FlatpakPlugin::contextMenuExtension(KDevelop::Context* context) +{ + QList urls; + + if ( context->type() == KDevelop::Context::FileContext ) { + KDevelop::FileContext* filectx = dynamic_cast( context ); + urls = filectx->urls(); + } else if ( context->type() == KDevelop::Context::ProjectItemContext ) { + KDevelop::ProjectItemContext* projctx = dynamic_cast( context ); + foreach( KDevelop::ProjectBaseItem* item, projctx->items() ) { + if ( item->file() ) { + urls << item->file()->path().toUrl(); + } + } + } + + const QRegularExpression nameRx(".*\\..*\\.*.json$"); + for(auto it = urls.begin(); it != urls.end(); ) { + if (it->isLocalFile() && it->path().contains(nameRx)) { + ++it; + } else { + it = urls.erase(it); + } + } + + if ( !urls.isEmpty() ) { + KDevelop::ContextMenuExtension ext; + foreach(const QUrl &url, urls) { + const KDevelop::Path file(url); + + foreach(const QString &arch, availableArches(file)) { + auto action = new QAction(i18n("Build flatpak %1 for %2", file.lastPathSegment(), arch), this); + connect(action, &QAction::triggered, this, [this, file, arch]() { + createRuntime(file, arch); + }); + ext.addAction(KDevelop::ContextMenuExtension::RunGroup, action); + } + } + + return ext; + } + + return KDevelop::IPlugin::contextMenuExtension( context ); +} + +void FlatpakPlugin::executeOnRemoteDevice() +{ + const auto runtime = qobject_cast(ICore::self()->runtimeController()->currentRuntime()); + Q_ASSERT(runtime); + + KConfigGroup group(KSharedConfig::openConfig(), "Flatpak"); + const QString lastDeviceAddress = group.readEntry("DeviceAddress"); + const QString host = QInputDialog::getText( + ICore::self()->uiController()->activeMainWindow(), i18n("Choose tag name..."), + i18n("Device hostname"), + QLineEdit::Normal, lastDeviceAddress + ); + if (host.isEmpty()) + return; + group.writeEntry("DeviceAddress", host); + + QTemporaryFile* file = new QTemporaryFile(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/') + runtime->name() + "XXXXXX.flatpak"); + file->open(); + file->close(); + auto job = runtime->executeOnDevice(host, file->fileName()); + file->setParent(file); + + ICore::self()->runController()->registerJob(job); +} + +#include "flatpakplugin.moc" diff --git a/plugins/flatpak/flatpakruntime.h b/plugins/flatpak/flatpakruntime.h new file mode 100644 --- /dev/null +++ b/plugins/flatpak/flatpakruntime.h @@ -0,0 +1,65 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef FLATPAKRUNTIME_H +#define FLATPAKRUNTIME_H + +#include +#include +#include + +class KJob; +class FlatpakPlugin; + +class FlatpakRuntime : public KDevelop::IRuntime +{ + Q_OBJECT +public: + FlatpakRuntime(const KDevelop::Path &buildDirectory, const KDevelop::Path &file, const QString &arch); + ~FlatpakRuntime() override; + + QString name() const override; + + void setEnabled(bool enabled) override; + + void startProcess(KProcess *process) const override; + void startProcess(QProcess *process) const override; + KDevelop::Path pathInHost(const KDevelop::Path & runtimePath) const override; + KDevelop::Path pathInRuntime(const KDevelop::Path & localPath) const override; + QByteArray getenv(const QByteArray &varname) const override; + + static KJob* createBuildDirectory(const KDevelop::Path &path, const KDevelop::Path &file, const QString &arch); + + KJob* rebuild(); + QList exportBundle(const QString &path) const; + KJob* executeOnDevice(const QString &host, const QString &path) const; + + static QJsonObject config(const KDevelop::Path& path); + +private: + void refreshJson(); + QJsonObject config() const; + + const KDevelop::Path m_file; + const KDevelop::Path m_buildDirectory; + const QString m_arch; + QStringList m_finishArgs; + KDevelop::Path m_sdkPath; +}; + +#endif diff --git a/plugins/flatpak/flatpakruntime.cpp b/plugins/flatpak/flatpakruntime.cpp new file mode 100644 --- /dev/null +++ b/plugins/flatpak/flatpakruntime.cpp @@ -0,0 +1,227 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "flatpakruntime.h" +#include "flatpakplugin.h" +#include "debug_flatpak.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace KDevelop; + +template +static T kTransform(const Q& list, W func) +{ + T ret; + ret.reserve(list.size()); + for (auto it = list.constBegin(), itEnd = list.constEnd(); it!=itEnd; ++it) + ret += func(*it); + return ret; +} + +static KJob* createExecuteJob(const QStringList &program, const QString &title, const QUrl &wd = {}) +{ + OutputExecuteJob* process = new OutputExecuteJob; + process->setExecuteOnHost(true); + process->setJobName(title); + process->setWorkingDirectory(wd); + *process << program; + return process; +} + +KJob* FlatpakRuntime::createBuildDirectory(const KDevelop::Path &buildDirectory, const KDevelop::Path &file, const QString &arch) +{ + return createExecuteJob(QStringList{ "flatpak-builder", "--arch="+arch, "--build-only", buildDirectory.toLocalFile(), file.toLocalFile() }, i18n("Creating Flatpak %1", file.lastPathSegment()), file.parent().toUrl()); +} + +FlatpakRuntime::FlatpakRuntime(const KDevelop::Path &buildDirectory, const KDevelop::Path &file, const QString &arch) + : KDevelop::IRuntime() + , m_file(file) + , m_buildDirectory(buildDirectory) + , m_arch(arch) +{ + refreshJson(); +} + +FlatpakRuntime::~FlatpakRuntime() +{ +} + +void FlatpakRuntime::refreshJson() +{ + const auto doc = config(); + const QString sdkName = doc[QLatin1String("sdk")].toString(); + const QString runtimeVersion = doc.value(QLatin1String("runtime-version")).toString(); + const QString usedRuntime = sdkName + QLatin1Char('/') + m_arch + QLatin1Char('/') + runtimeVersion; + + m_sdkPath = KDevelop::Path("/var/lib/flatpak/runtime/" + usedRuntime + "/active/files"); + qDebug() << "flatpak runtime path..." << name() << m_sdkPath; + Q_ASSERT(QFile::exists(m_sdkPath.toLocalFile())); + + m_finishArgs = kTransform(doc["finish-args"].toArray(), [](const QJsonValue& val){ return val.toString(); }); +} + +void FlatpakRuntime::setEnabled(bool /*enable*/) +{ +} + +void FlatpakRuntime::startProcess(QProcess* process) const +{ + const QStringList args = m_finishArgs + QStringList{"build", "--socket=x11", m_buildDirectory.toLocalFile(), process->program()} << process->arguments(); + process->setProgram("flatpak"); + process->setArguments(args); + + qCDebug(FLATPAK) << "starting qprocess" << process->program() << process->arguments(); + process->start(); +} + +void FlatpakRuntime::startProcess(KProcess* process) const +{ + process->setProgram(QStringList{ "flatpak" } << m_finishArgs << QStringList{ "build", m_buildDirectory.toLocalFile() } << process->program()); + + qCDebug(FLATPAK) << "starting kprocess" << process->program().join(' '); + process->start(); +} + +KJob* FlatpakRuntime::rebuild() +{ + QDir(m_buildDirectory.toLocalFile()).removeRecursively(); + auto job = createBuildDirectory(m_buildDirectory, m_file, m_arch); + refreshJson(); + return job; +} + +QList FlatpakRuntime::exportBundle(const QString &path) const +{ + const auto doc = config(); + + QTemporaryDir* dir = new QTemporaryDir(QDir::tempPath()+"/flatpak-tmp-repo"); + if (!dir->isValid() || doc.isEmpty()) { + qCWarning(FLATPAK) << "Couldn't export:" << path << dir->isValid() << dir->path() << doc.isEmpty(); + return {}; + } + + const QString name = doc[QLatin1String("id")].toString(); + QStringList args = m_finishArgs; + if (doc.contains("command")) + args << "--command="+doc["command"].toString(); + const QList jobs = { + createExecuteJob(QStringList{ "flatpak", "build-finish", m_buildDirectory.toLocalFile()} << args, {}), + createExecuteJob(QStringList{ "flatpak", "build-export", "--arch="+m_arch, dir->path(), m_buildDirectory.toLocalFile()}, {}), + createExecuteJob(QStringList{ "flatpak", "build-bundle", "--arch="+m_arch, dir->path(), path, name }, i18n("Exporting %1", path)) + }; + connect(jobs.last(), &QObject::destroyed, jobs.last(), [dir]() { delete dir; }); + return jobs; +} + +QString FlatpakRuntime::name() const +{ + return m_file.lastPathSegment() + QLatin1Char(':') + m_arch; +} + +KJob * FlatpakRuntime::executeOnDevice(const QString& host, const QString &path) const +{ + const QString name = config()[QLatin1String("id")].toString(); + const QString destPath = QStringLiteral("/tmp/kdevelop-test-app.flatpak"); + const QString replicatePath = QStringLiteral("/tmp/replicate.sh"); + const QString localReplicatePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("kdevflatpak/replicate.sh")); + const QString process; + + const QList jobs = exportBundle(path) << QList { + createExecuteJob({ "scp", path, host+QLatin1Char(':')+destPath}, i18n("Transferring flatpak to %1", host)), + createExecuteJob({ "scp", localReplicatePath, host+QLatin1Char(':')+replicatePath}, i18n("Transferring replicate.sh to %1", host)), + createExecuteJob({ "ssh", host, "flatpak", "install", "--user", "--bundle", destPath}, i18n("Installing %1 to %2", name, host)), + createExecuteJob({ "ssh", host, "bash", replicatePath, "plasmashell", "flatpak", "run", name }, i18n("Running %1 on %2", name, host)), + }; + return new KDevelop::ExecuteCompositeJob( parent(), jobs ); +} + +QJsonObject FlatpakRuntime::config(const KDevelop::Path& path) +{ + QFile f(path.toLocalFile()); + if (!f.open(QIODevice::ReadOnly)) { + qCWarning(FLATPAK) << "couldn't open" << path; + return {}; + } + + QJsonParseError error; + auto doc = QJsonDocument::fromJson(f.readAll(), &error); + if (error.error) { + qCWarning(FLATPAK) << "couldn't parse" << path << error.errorString(); + return {}; + } + + return doc.object(); +} + +QJsonObject FlatpakRuntime::config() const +{ + return config(m_file); +} + +Path FlatpakRuntime::pathInHost(const KDevelop::Path& runtimePath) const +{ + KDevelop::Path ret = runtimePath; + if (runtimePath.isLocalFile() && runtimePath.segments().at(0) == QLatin1String("usr")) { + const auto relpath = KDevelop::Path("/usr").relativePath(runtimePath); + ret = Path(m_sdkPath, relpath); + } else if (runtimePath.isLocalFile() && runtimePath.segments().at(0) == QLatin1String("app")) { + const auto relpath = KDevelop::Path("/app").relativePath(runtimePath); + ret = Path(m_buildDirectory, "/active/files/" + relpath); + } + + qCDebug(FLATPAK) << "path in host" << runtimePath << ret; + return ret; +} + +Path FlatpakRuntime::pathInRuntime(const KDevelop::Path& localPath) const +{ + KDevelop::Path ret = localPath; + if (m_sdkPath.isParentOf(localPath)) { + const auto relpath = m_sdkPath.relativePath(localPath); + ret = Path(Path("/usr"), relpath); + } else { + const Path bdfiles(m_buildDirectory, "/active/flies"); + if (bdfiles.isParentOf(localPath)) { + const auto relpath = bdfiles.relativePath(localPath); + ret = Path(Path("/app"), relpath); + } + } + + qCDebug(FLATPAK) << "path in runtime" << localPath << ret; + return ret; +} + +QByteArray FlatpakRuntime::getenv(const QByteArray& varname) const +{ + return qgetenv(varname); +} diff --git a/plugins/flatpak/kdevflatpak.json b/plugins/flatpak/kdevflatpak.json new file mode 100644 --- /dev/null +++ b/plugins/flatpak/kdevflatpak.json @@ -0,0 +1,17 @@ +{ + "KPlugin": { + "Authors": [ + { "Name": "Aleix Pol", "Email": "aleixpol@kde.org" } + ], + "Category": "Global", + "Description": "Exposes Flatpak runtimes", + "Icon": "flatpak", + "Id": "kdevflatpak", + "License": "GPL", + "Name": "Flatpak Support", + "ServiceTypes": [ "KDevelop/Plugin" ], + "Version": "0.1" + }, + "X-KDevelop-Category": "Global", + "X-KDevelop-Mode": "NoGUI" +} diff --git a/plugins/flatpak/kdevflatpakplugin.qrc b/plugins/flatpak/kdevflatpakplugin.qrc new file mode 100644 --- /dev/null +++ b/plugins/flatpak/kdevflatpakplugin.qrc @@ -0,0 +1,6 @@ + + + + kdevflatpakplugin.rc + + diff --git a/plugins/flatpak/kdevflatpakplugin.rc b/plugins/flatpak/kdevflatpakplugin.rc new file mode 100644 --- /dev/null +++ b/plugins/flatpak/kdevflatpakplugin.rc @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/plugins/flatpak/replicate.sh b/plugins/flatpak/replicate.sh new file mode 100644 --- /dev/null +++ b/plugins/flatpak/replicate.sh @@ -0,0 +1,3 @@ +export $(cat /proc/$(pidof $1)/environ | tr \\0 \\n) +shift +$@ diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -51,6 +51,7 @@ configdialog.cpp editorconfigpage.cpp environmentconfigurebutton.cpp + runtimecontroller.cpp checkerstatus.cpp problem.cpp diff --git a/shell/core.h b/shell/core.h --- a/shell/core.h +++ b/shell/core.h @@ -43,6 +43,7 @@ class DocumentationController; class DebugController; class WorkingSetController; +class RuntimeController; class TestController; class KDEVPLATFORMSHELL_EXPORT Core: public ICore @@ -85,6 +86,7 @@ IDocumentationController* documentationController() override; IDebugController* debugController() override; ITestController* testController() override; + IRuntimeController* runtimeController() override; ISession *activeSession() override; ISessionLock::Ptr activeSessionLock() override; @@ -104,6 +106,7 @@ WorkingSetController* workingSetControllerInternal(); SourceFormatterController* sourceFormatterControllerInternal(); TestController* testControllerInternal(); + RuntimeController* runtimeControllerInternal(); /// @internal SessionController *sessionController(); diff --git a/shell/core.cpp b/shell/core.cpp --- a/shell/core.cpp +++ b/shell/core.cpp @@ -46,6 +46,7 @@ #include "kdevplatform_version.h" #include "workingsetcontroller.h" #include "testcontroller.h" +#include "runtimecontroller.h" #include "debug.h" #include @@ -210,6 +211,11 @@ documentationController = new DocumentationController(m_core); } + if( !runtimeController ) + { + runtimeController = new RuntimeController(m_core); + } + if( !debugController ) { debugController = new DebugController(m_core); @@ -275,6 +281,7 @@ } debugController.data()->initialize(); testController.data()->initialize(); + runtimeController.data()->initialize(); installSignalHandler(); @@ -298,6 +305,7 @@ delete debugController.data(); delete workingSetController.data(); delete testController.data(); + delete runtimeController.data(); selectionController.clear(); projectController.clear(); @@ -313,6 +321,7 @@ debugController.clear(); workingSetController.clear(); testController.clear(); + runtimeController.clear(); } bool Core::initialize(QObject* splash, Setup mode, const QString& session ) @@ -552,6 +561,16 @@ return d->documentationController.data(); } +IRuntimeController* Core::runtimeController() +{ + return d->runtimeController.data(); +} + +RuntimeController* Core::runtimeControllerInternal() +{ + return d->runtimeController.data(); +} + IDebugController* Core::debugController() { return d->debugController.data(); diff --git a/shell/core_p.h b/shell/core_p.h --- a/shell/core_p.h +++ b/shell/core_p.h @@ -45,6 +45,7 @@ class DebugController; class WorkingSetController; class TestController; +class RuntimeController; class KDEVPLATFORMSHELL_EXPORT CorePrivate { public: @@ -66,6 +67,7 @@ QPointer debugController; QPointer workingSetController; QPointer testController; + QPointer runtimeController; KAboutData m_aboutData; Core *m_core; diff --git a/shell/runtimecontroller.h b/shell/runtimecontroller.h new file mode 100644 --- /dev/null +++ b/shell/runtimecontroller.h @@ -0,0 +1,60 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVPLATFORM_RUNTIMECONTROLLER_H +#define KDEVPLATFORM_RUNTIMECONTROLLER_H + +#include +#include +#include + +class RuntimeViewFactory; +class QAction; +class QMenu; + +namespace KDevelop +{ + +class Core; +class Context; +class ContextMenuExtension; + +class RuntimeController : public IRuntimeController +{ + Q_OBJECT +public: + explicit RuntimeController(Core* core); + ~RuntimeController() override; + + void initialize(); + + void addRuntimes(KDevelop::IRuntime * runtimes) override; + QVector availableRuntimes() const override; + + KDevelop::IRuntime * currentRuntime() const override; + void setCurrentRuntime(KDevelop::IRuntime * runtime) override; + +private: + QScopedPointer const m_runtimesMenu; + QVector m_runtimes; + IRuntime* m_currentRuntime = nullptr; +}; + +} + +#endif diff --git a/shell/runtimecontroller.cpp b/shell/runtimecontroller.cpp new file mode 100644 --- /dev/null +++ b/shell/runtimecontroller.cpp @@ -0,0 +1,135 @@ +/* + Copyright 2017 Aleix Pol Gonzalez + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "runtimecontroller.h" +#include +#include +#include +#include +#include +#include +#include +#include "core.h" +#include "uicontroller.h" +#include "mainwindow.h" +#include "debug.h" + +using namespace KDevelop; + +class IdentityRuntime : public IRuntime +{ + QString name() const override { return i18n("Host System"); } + + void startProcess(KProcess *process) const override { + connect(process, &QProcess::errorOccurred, this, [process](QProcess::ProcessError error) { + qCWarning(SHELL) << "error:" << error << process->program() << process->errorString(); + }); + process->start(); + } + void startProcess(QProcess *process) const override { + connect(process, &QProcess::errorOccurred, this, [process](QProcess::ProcessError error) { + qCWarning(SHELL) << "error:" << error << process->program() << process->errorString(); + }); + process->start(); + } + KDevelop::Path pathInHost(const KDevelop::Path & runtimePath) const override { return runtimePath; } + KDevelop::Path pathInRuntime(const KDevelop::Path & localPath) const override { return localPath; } + void setEnabled(bool /*enabled*/) override {} + QByteArray getenv(const QByteArray & varname) const override { return qgetenv(varname); } +}; + +KDevelop::RuntimeController::RuntimeController(KDevelop::Core* core) + : m_runtimesMenu(new QMenu()) +{ + addRuntimes({new IdentityRuntime}); + setCurrentRuntime(m_runtimes.constFirst()); + + // TODO not multi-window friendly, FIXME + KActionCollection* ac = core->uiControllerInternal()->defaultMainWindow()->actionCollection(); + + auto action = new QAction(this); + action->setStatusTip(i18n("Allows to select a runtime")); + action->setMenu(m_runtimesMenu.data()); + action->setIcon(QIcon::fromTheme(QStringLiteral("file-library-symbolic"))); + auto updateActionText = [action](IRuntime* currentRuntime){ + action->setText(i18n("Runtime: %1", currentRuntime->name())); + }; + connect(this, &RuntimeController::currentRuntimeChanged, action, updateActionText); + updateActionText(m_currentRuntime); + + ac->addAction(QStringLiteral("switch_runtimes"), action); +} + +KDevelop::RuntimeController::~RuntimeController() +{ + m_currentRuntime->setEnabled(false); + m_currentRuntime = nullptr; +} + +void KDevelop::RuntimeController::initialize() +{ +} + +KDevelop::IRuntime * KDevelop::RuntimeController::currentRuntime() const +{ + Q_ASSERT(m_currentRuntime); + return m_currentRuntime; +} + +QVector KDevelop::RuntimeController::availableRuntimes() const +{ + return m_runtimes; +} + +void KDevelop::RuntimeController::setCurrentRuntime(KDevelop::IRuntime* runtime) +{ + if (m_currentRuntime == runtime) + return; + + Q_ASSERT(m_runtimes.contains(runtime)); + + if (m_currentRuntime) { + m_currentRuntime->setEnabled(false); + } + qDebug() << "setting runtime..." << runtime->name() << "was" << m_currentRuntime; + m_currentRuntime = runtime; + m_currentRuntime->setEnabled(true); + Q_EMIT currentRuntimeChanged(runtime); +} + +void KDevelop::RuntimeController::addRuntimes(KDevelop::IRuntime * runtime) +{ + if (!runtime->parent()) + runtime->setParent(this); + QAction* runtimeAction = new QAction(runtime->name(), m_runtimesMenu.data()); + runtimeAction->setCheckable(true); + connect(runtimeAction, &QAction::triggered, runtime, [this, runtime]() { + setCurrentRuntime(runtime); + }); + connect(this, &RuntimeController::currentRuntimeChanged, runtimeAction, [runtimeAction, runtime](IRuntime* currentRuntime) { + runtimeAction->setChecked(runtime == currentRuntime); + }); + + connect(runtime, &QObject::destroyed, this, [this, runtimeAction](QObject* obj) { + Q_ASSERT(m_currentRuntime != obj); + m_runtimes.removeAll(qobject_cast(obj)); + delete runtimeAction; + }); + m_runtimesMenu->addAction(runtimeAction); + m_runtimes << runtime; +}