diff --git a/projectmanagers/cmake/CMakeLists.txt b/projectmanagers/cmake/CMakeLists.txt --- a/projectmanagers/cmake/CMakeLists.txt +++ b/projectmanagers/cmake/CMakeLists.txt @@ -29,6 +29,7 @@ cmakeutils.cpp cmakeextraargumentshistory.cpp cmakebuilddirchooser.cpp + cmakeserver.cpp debug.cpp ) @@ -44,8 +45,10 @@ testing/ctestsuite.cpp testing/qttestdelegate.cpp cmakeimportjsonjob.cpp + cmakeserverimportjob.cpp cmakenavigationwidget.cpp cmakemanager.cpp + cmakeprojectdata.cpp cmakemodelitems.cpp duchain/cmakeparsejob.cpp duchain/usebuilder.cpp diff --git a/projectmanagers/cmake/cmakeimportjob.h b/projectmanagers/cmake/cmakeimportjob.h --- a/projectmanagers/cmake/cmakeimportjob.h +++ b/projectmanagers/cmake/cmakeimportjob.h @@ -36,19 +36,19 @@ class ReferencedTopDUContext; } -class CMakeImportJob : public KJob +class CMakeImportJsonJob : public KJob { Q_OBJECT public: - CMakeImportJob(KDevelop::ProjectFolderItem* dom, CMakeManager* parent); + CMakeImportJsonJob(KDevelop::ProjectFolderItem* dom, CMakeManager* parent); virtual void start(); KDevelop::IProject* project() const; CMakeProjectData projectData() const; private slots: void waitFinished(KJob* job); - void importFinished(); + void importCompileCommandsJsonFinished(); private: void initialize(); diff --git a/projectmanagers/cmake/cmakeimportjob.cpp b/projectmanagers/cmake/cmakeimportjob.cpp --- a/projectmanagers/cmake/cmakeimportjob.cpp +++ b/projectmanagers/cmake/cmakeimportjob.cpp @@ -78,7 +78,7 @@ bool m_started; }; -CMakeImportJob::CMakeImportJob(ProjectFolderItem* dom, CMakeManager* parent) +CMakeImportJsonJob::CMakeImportJsonJob(ProjectFolderItem* dom, CMakeManager* parent) : KJob(parent) , m_project(dom->project()) , m_dom(dom) @@ -89,13 +89,13 @@ connect(m_futureWatcher, SIGNAL(finished()), SLOT(importFinished())); } -void CMakeImportJob::start() +void CMakeImportJsonJob::start() { QFuture future = QtConcurrent::run(this, &CMakeImportJob::initialize); m_futureWatcher->setFuture(future); } -void CMakeImportJob::importFinished() +void CMakeImportJsonJob::importCompileCommandsJsonFinished() { Q_ASSERT(m_project->thread() == QThread::currentThread()); @@ -107,7 +107,7 @@ wjob->start(); } -void CMakeImportJob::initialize() +void CMakeImportJsonJob::initialize() { ReferencedTopDUContext ctx; ProjectBaseItem* parent = m_dom->parent(); @@ -122,7 +122,7 @@ importDirectory(m_project, m_dom->path(), ctx); } -KDevelop::ReferencedTopDUContext CMakeImportJob::initializeProject(CMakeFolderItem* rootFolder) +KDevelop::ReferencedTopDUContext CMakeImportJsonJob::initializeProject(CMakeFolderItem* rootFolder) { Path base(CMake::projectRoot(m_project)); @@ -193,20 +193,20 @@ return ref; } -void CMakeImportJob::waitFinished(KJob*) +void CMakeImportJsonJob::waitFinished(KJob*) { emitResult(); } -KDevelop::ReferencedTopDUContext CMakeImportJob::includeScript(const QString& file, const QString& dir, ReferencedTopDUContext parent) +KDevelop::ReferencedTopDUContext CMakeImportJsonJob::includeScript(const QString& file, const QString& dir, ReferencedTopDUContext parent) { m_manager->addWatcher(m_project, file); QString profile = CMake::currentEnvironment(m_project); const KDevelop::EnvironmentGroupList env( KSharedConfig::openConfig() ); return CMakeParserUtils::includeScript( file, parent, &m_data, dir, env.variables(profile)); } -CMakeCommitChangesJob* CMakeImportJob::importDirectory(IProject* project, const Path& path, const KDevelop::ReferencedTopDUContext& parentTop) +CMakeCommitChangesJob* CMakeImportJsonJob::importDirectory(IProject* project, const Path& path, const KDevelop::ReferencedTopDUContext& parentTop) { Q_ASSERT(thread() == m_project->thread()); Path cmakeListsPath(path, "CMakeLists.txt"); @@ -241,13 +241,13 @@ return commitJob; } -IProject* CMakeImportJob::project() const +IProject* CMakeImportJsonJob::project() const { Q_ASSERT(!m_futureWatcher->isRunning()); return m_project; } -CMakeProjectData CMakeImportJob::projectData() const +CMakeProjectData CMakeImportJsonJob::projectData() const { Q_ASSERT(!m_futureWatcher->isRunning()); return m_data; diff --git a/projectmanagers/cmake/cmakeimportjsonjob.h b/projectmanagers/cmake/cmakeimportjsonjob.h --- a/projectmanagers/cmake/cmakeimportjsonjob.h +++ b/projectmanagers/cmake/cmakeimportjsonjob.h @@ -31,7 +31,7 @@ class CMakeFolderItem; struct ImportData { - CMakeJsonData json; + CMakeFilesCompilationData compilationData; QHash targets; QVector testSuites; }; @@ -42,7 +42,7 @@ class ReferencedTopDUContext; } -class CMakeImportJob : public KJob +class CMakeImportJsonJob : public KJob { Q_OBJECT @@ -52,32 +52,23 @@ ReadError ///< Failed to read the JSON file }; - CMakeImportJob(KDevelop::IProject* project, QObject* parent); - ~CMakeImportJob() override; + CMakeImportJsonJob(KDevelop::IProject* project, QObject* parent); + ~CMakeImportJsonJob() override; void start() override; KDevelop::IProject* project() const; - /** - * Return the parsed JSON data - * - * @note Only call after the job has finished! - */ - CMakeJsonData jsonData() const; - QHash targets() const { return m_targets; } - QVector testSuites() const { return m_testSuites; } + CMakeProjectData projectData() const; private Q_SLOTS: - void importFinished(); + void importCompileCommandsJsonFinished(); private: KDevelop::IProject* m_project; QFutureWatcher m_futureWatcher; - CMakeJsonData m_data; - QHash m_targets; - QVector m_testSuites; + CMakeProjectData m_data; }; #endif // CMAKEIMPORTJSONJOB_H diff --git a/projectmanagers/cmake/cmakeimportjsonjob.cpp b/projectmanagers/cmake/cmakeimportjsonjob.cpp --- a/projectmanagers/cmake/cmakeimportjsonjob.cpp +++ b/projectmanagers/cmake/cmakeimportjsonjob.cpp @@ -42,7 +42,7 @@ namespace { -CMakeJsonData importCommands(const Path& commandsFile) +CMakeFilesCompilationData importCommands(const Path& commandsFile) { // NOTE: to get compile_commands.json, you need -DCMAKE_EXPORT_COMPILE_COMMANDS=ON QFile f(commandsFile.toLocalFile()); @@ -54,7 +54,7 @@ qCDebug(CMAKE) << "Found commands file" << commandsFile; - CMakeJsonData data; + CMakeFilesCompilationData data; QJsonParseError error; const QJsonDocument document = QJsonDocument::fromJson(f.readAll(), &error); if (error.error) { @@ -141,18 +141,18 @@ } -CMakeImportJob::CMakeImportJob(IProject* project, QObject* parent) +CMakeImportJsonJob::CMakeImportJsonJob(IProject* project, QObject* parent) : KJob(parent) , m_project(project) + , m_data({}) { - connect(&m_futureWatcher, &QFutureWatcher::finished, this, &CMakeImportJob::importFinished); + connect(&m_futureWatcher, &QFutureWatcher::finished, this, &CMakeImportJsonJob::importCompileCommandsJsonFinished); } -CMakeImportJob::~CMakeImportJob() -{ -} +CMakeImportJsonJob::~CMakeImportJsonJob() +{} -void CMakeImportJob::start() +void CMakeImportJsonJob::start() { auto commandsFile = CMake::commandsFile(project()); if (!QFileInfo::exists(commandsFile.toLocalFile())) { @@ -171,33 +171,31 @@ m_futureWatcher.setFuture(future); } -void CMakeImportJob::importFinished() +void CMakeImportJsonJob::importCompileCommandsJsonFinished() { Q_ASSERT(m_project->thread() == QThread::currentThread()); Q_ASSERT(m_futureWatcher.isFinished()); auto future = m_futureWatcher.future(); auto data = future.result(); - if (!data.json.isValid) { + if (!data.compilationData.isValid) { qCWarning(CMAKE) << "Could not import CMake project ('compile_commands.json' invalid)"; emitResult(); return; } - m_data = data.json; - m_targets = data.targets; - m_testSuites = data.testSuites; - qCDebug(CMAKE) << "Done importing, found" << m_data.files.count() << "entries for" << project()->path(); + m_data = CMakeProjectData {data.targets, data.compilationData, data.testSuites}; + qCDebug(CMAKE) << "Done importing, found" << data.compilationData.files.count() << "entries for" << project()->path(); emitResult(); } -IProject* CMakeImportJob::project() const +IProject* CMakeImportJsonJob::project() const { return m_project; } -CMakeJsonData CMakeImportJob::jsonData() const +CMakeProjectData CMakeImportJsonJob::projectData() const { Q_ASSERT(!m_futureWatcher.isRunning()); return m_data; diff --git a/projectmanagers/cmake/cmakemanager.h b/projectmanagers/cmake/cmakemanager.h --- a/projectmanagers/cmake/cmakemanager.h +++ b/projectmanagers/cmake/cmakemanager.h @@ -151,7 +151,8 @@ // // void directoryChanged(const QString& dir); // void filesystemBuffererTimeout(); - void importFinished(KJob* job); + + void integrateData(const CMakeProjectData &data, KDevelop::IProject* project); private: CMakeFile fileInformation(KDevelop::ProjectBaseItem* item) const; diff --git a/projectmanagers/cmake/cmakemanager.cpp b/projectmanagers/cmake/cmakemanager.cpp --- a/projectmanagers/cmake/cmakemanager.cpp +++ b/projectmanagers/cmake/cmakemanager.cpp @@ -33,6 +33,8 @@ #include "icmakedocumentation.h" #include "cmakemodelitems.h" #include "testing/ctestutils.h" +#include "cmakeserverimportjob.h" +#include "cmakeserver.h" #include #include @@ -112,7 +114,7 @@ bool CMakeManager::hasBuildInfo(ProjectBaseItem* item) const { - return m_projects[item->project()].jsonData.files.contains(item->path()); + return m_projects[item->project()].compilationData.files.contains(item->path()); } Path CMakeManager::buildDirectory(KDevelop::ProjectBaseItem *item) const @@ -138,23 +140,66 @@ return AbstractFileManagerPlugin::import(project); } +static bool serverSupported() +{ + static int supported = 2; + if (supported == 2) { + QProcess p; + p.start(CMake::findExecutable(), {"--version"}); + p.waitForReadyRead(); + const QByteArray line = p.readLine(); + + QRegularExpression rx("cmake version (\\d+)\\.(\\d+)\\.(\\d+)"); + auto match = rx.match(line); + if (match.isValid()) { + match.capturedTexts(); + } + } + return supported == 1; +} + KJob* CMakeManager::createImportJob(ProjectFolderItem* item) { auto project = item->project(); QList jobs; - // create the JSON file if it doesn't exist - auto commandsFile = CMake::commandsFile(project); - if (!QFileInfo::exists(commandsFile.toLocalFile())) { - qCDebug(CMAKE) << "couldn't find commands file:" << commandsFile << "- now trying to reconfigure"; - jobs << builder()->configure(project); - } + CMakeServer* server = new CMakeServer(this); + server->waitForConnected(); + + if (server->isServerAvailable()) { + auto job = new CMakeServerImportJob(project, server, this); + connect(job, &CMakeServerImportJob::result, this, [this, job](){ + if (job->error() != 0) { + qCWarning(CMAKE) << "couldn't load server successfully" << job->project()->name(); + m_projects.remove(job->project()); + } else { + integrateData(job->projectData(), job->project()); + } + }); + jobs << job; + } else { + delete server; + // parse the JSON file + CMakeImportJsonJob* job = new CMakeImportJsonJob(project, this); + + // create the JSON file if it doesn't exist + auto commandsFile = CMake::commandsFile(project); + if (!QFileInfo::exists(commandsFile.toLocalFile())) { + qCDebug(CMAKE) << "couldn't find commands file:" << commandsFile << "- now trying to reconfigure"; + jobs << builder()->configure(project); + } - // parse the JSON file - CMakeImportJob* job = new CMakeImportJob(project, this); - connect(job, &CMakeImportJob::result, this, &CMakeManager::importFinished); - jobs << job; + connect(job, &CMakeImportJsonJob::result, this, [this, job](){ + if (job->error() != 0) { + qCWarning(CMAKE) << "couldn't load json successfully" << job->project()->name(); + m_projects.remove(job->project()); + } else { + integrateData(job->projectData(), job->project()); + } + }); + jobs << job; + } // generate the file system listing jobs << KDevelop::AbstractFileManagerPlugin::createImportJob(item); @@ -183,7 +228,7 @@ CMakeFile CMakeManager::fileInformation(KDevelop::ProjectBaseItem* item) const { - const CMakeJsonData & data = m_projects[item->project()].jsonData; + const auto & data = m_projects[item->project()].compilationData; QHash::const_iterator it = data.files.constFind(item->path()); if (it == data.files.constEnd()) { @@ -286,31 +331,14 @@ } } -void CMakeManager::importFinished(KJob* j) +void CMakeManager::integrateData(const CMakeProjectData &data, KDevelop::IProject* project) { - CMakeImportJob* job = qobject_cast(j); - Q_ASSERT(job); - - auto project = job->project(); - if (job->error() != 0) { - qCDebug(CMAKE) << "Import failed for project" << project->name() << job->errorText(); - m_projects.remove(project); - } - - qCDebug(CMAKE) << "Successfully imported project" << project->name(); - - CMakeProjectData data; - data.watcher->addPath(CMake::commandsFile(project).toLocalFile()); - data.watcher->addPath(CMake::targetDirectoriesFile(project).toLocalFile()); - data.jsonData = job->jsonData(); - data.targets = job->targets(); connect(data.watcher.data(), &QFileSystemWatcher::fileChanged, this, &CMakeManager::dirtyFile); connect(data.watcher.data(), &QFileSystemWatcher::directoryChanged, this, &CMakeManager::dirtyFile); - m_projects[job->project()] = data; - - populateTargets(job->project()->projectItem(), job->targets()); + m_projects[project] = data; - CTestUtils::createTestSuites(job->testSuites(), project); + populateTargets(project->projectItem(), data.targets); + CTestUtils::createTestSuites(data.m_testSuites, project); } // void CMakeManager::deletedWatchedDirectory(IProject* p, const QUrl &dir) diff --git a/projectmanagers/cmake/cmakeprojectdata.h b/projectmanagers/cmake/cmakeprojectdata.h --- a/projectmanagers/cmake/cmakeprojectdata.h +++ b/projectmanagers/cmake/cmakeprojectdata.h @@ -1,6 +1,6 @@ /* KDevelop CMake Support * - * Copyright 2013 Aleix Pol + * Copyright 2013-2017 Aleix Pol * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -21,12 +21,17 @@ #ifndef CMAKEPROJECTDATA_H #define CMAKEPROJECTDATA_H +#include #include #include +#include +#include #include #include "cmaketypes.h" #include +class CMakeServer; + /** * Represents any file in a cmake project that has been added * to the project. @@ -45,22 +50,25 @@ return debug.maybeSpace(); } -struct CMakeJsonData +struct CMakeFilesCompilationData { QHash files; bool isValid = false; }; struct CMakeProjectData { + CMakeProjectData(const QHash &targets, const CMakeFilesCompilationData &data, const QVector &tests); + CMakeProjectData() : watcher(new QFileSystemWatcher) {} ~CMakeProjectData() {} - CMakeProperties properties; - CacheValues cache; - CMakeJsonData jsonData; + CMakeFilesCompilationData compilationData; QHash targets; QSharedPointer watcher; + QSharedPointer m_server; + + QVector m_testSuites; }; #endif diff --git a/projectmanagers/cmake/cmakeprojectdata.cpp b/projectmanagers/cmake/cmakeprojectdata.cpp new file mode 100644 --- /dev/null +++ b/projectmanagers/cmake/cmakeprojectdata.cpp @@ -0,0 +1,34 @@ +/* KDevelop CMake Support + * + * Copyright 2017 Aleix Pol + * + * 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) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include "cmakeprojectdata.h" +#include "cmakeutils.h" +#include +#include +#include +#include + +CMakeProjectData::CMakeProjectData(const QHash& targets, const CMakeFilesCompilationData& data, const QVector& tests) + : compilationData(data) + , targets(targets) + , watcher(new QFileSystemWatcher) + , m_testSuites(tests) +{ +} diff --git a/projectmanagers/cmake/cmakeserver.h b/projectmanagers/cmake/cmakeserver.h new file mode 100644 --- /dev/null +++ b/projectmanagers/cmake/cmakeserver.h @@ -0,0 +1,59 @@ +/* KDevelop CMake Support + * + * Copyright 2017 Aleix Pol + * + * 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) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#ifndef CMAKESERVER_H +#define CMAKESERVER_H + +#include +#include +#include +#include "cmakecommonexport.h" + +class KDEVCMAKECOMMON_EXPORT CMakeServer : public QObject +{ +Q_OBJECT +public: + CMakeServer(QObject* parent); + ~CMakeServer() override; + + void waitForConnected(); + bool isServerAvailable(); + void sendCommand(const QJsonObject& object); + + void handshake(const KDevelop::Path& source, const KDevelop::Path& build); + void configure(const QStringList &args); + void compute(); + void codemodel(); + +Q_SIGNALS: + void connected(); + void disconnected(); + void response(const QJsonObject &value); + +private: + void processOutput(); + void emitResponse(const QByteArray &data); + + QLocalSocket* m_localSocket; + QByteArray m_buffer; + QProcess m_process; +}; + +#endif // CMAKESERVER_H diff --git a/projectmanagers/cmake/cmakeserver.cpp b/projectmanagers/cmake/cmakeserver.cpp new file mode 100644 --- /dev/null +++ b/projectmanagers/cmake/cmakeserver.cpp @@ -0,0 +1,165 @@ +/* KDevelop CMake Support + * + * Copyright 2017 Aleix Pol + * + * 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) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include "cmakeserver.h" +#include "cmakeprojectdata.h" +#include "cmakeutils.h" +#include +#include +#include +#include +#include +#include +#include "debug.h" + +CMakeServer::CMakeServer(QObject* parent) + : QObject(parent) + , m_localSocket(new QLocalSocket(this)) +{ + QString path; + { + QTemporaryFile file(QDir::tempPath() + "/kdevelopcmake-"); + file.open(); + file.close(); + path = file.fileName(); + } + + m_process.setProcessChannelMode(QProcess::ForwardedChannels); + m_process.start(CMake::findExecutable(), {"-E", "server", "--experimental", "--pipe=" + path}); + + connect(&m_process, &QProcess::errorOccurred, this, [this, path](QProcess::ProcessError error) { + qWarning() << "cmake server error:" << error << path << m_process.readAllStandardError() << m_process.readAllStandardOutput(); + }); + connect(&m_process, static_cast(&QProcess::finished), this, [](int code){ + qCDebug(CMAKE) << "cmake server finished with code" << code; + }); + + connect(m_localSocket, &QIODevice::readyRead, this, &CMakeServer::processOutput); + connect(m_localSocket, static_cast(&QLocalSocket::error), this, [this, path](QLocalSocket::LocalSocketError socketError) { + qCDebug(CMAKE) << "cmake server socket error:" << socketError << path; + Q_EMIT disconnected(); + }); + connect(m_localSocket, &QLocalSocket::connected, this, &CMakeServer::connected); + + connect(&m_process, &QProcess::started, this, [this, path](){ + //Once the process has started, wait for the file to be created, then connect to it + QTimer* connectTimer = new QTimer(this); + connectTimer->setInterval(100); + connectTimer->setSingleShot(true); + connect(connectTimer, &QTimer::timeout, connectTimer, &QObject::deleteLater); + connect(connectTimer, &QTimer::timeout, this, [this, path]() { + m_localSocket->connectToServer(path, QIODevice::ReadWrite); + }); + }); +} + +CMakeServer::~CMakeServer() +{ + m_process.kill(); + m_process.waitForFinished(); +} + +void CMakeServer::waitForConnected() +{ + m_localSocket->waitForConnected(); +} + +bool CMakeServer::isServerAvailable() +{ + return m_localSocket->isOpen(); +} + +static QByteArray openTag() { return QByteArrayLiteral("\n[== \"CMake Server\" ==[\n"); } +static QByteArray closeTag() { return QByteArrayLiteral("\n]== \"CMake Server\" ==]\n"); } + +void CMakeServer::sendCommand(const QJsonObject& object) +{ + Q_ASSERT(isServerAvailable()); + + const QByteArray data = openTag() + QJsonDocument(object).toJson(QJsonDocument::Compact) + closeTag(); + auto len = m_localSocket->write(data); +// qCDebug(CMAKE) << "writing...\n" << QJsonDocument(object).toJson(); + Q_ASSERT(len > 0); +} + +void CMakeServer::processOutput() +{ + Q_ASSERT(m_localSocket); + + const auto openTag = ::openTag(); + const auto closeTag = ::closeTag(); + + m_buffer += m_localSocket->readAll(); + for(; m_buffer.size() > openTag.size(); ) { + + Q_ASSERT(m_buffer.startsWith(openTag)); + const int idx = m_buffer.indexOf(closeTag, openTag.size()); + if (idx >= 0) { + emitResponse(m_buffer.mid(openTag.size(), idx - openTag.size())); + m_buffer = m_buffer.mid(idx + closeTag.size()); + } else + break; + } +} + +void CMakeServer::emitResponse(const QByteArray& data) +{ + QJsonParseError error; + auto doc = QJsonDocument::fromJson(data, &error); + if (error.error) { + qCWarning(CMAKE) << "error processing" << error.errorString() << data; + } + Q_ASSERT(doc.isObject()); + Q_EMIT response(doc.object()); +} + +void CMakeServer::handshake(const KDevelop::Path& source, const KDevelop::Path& build) +{ + Q_ASSERT(!source.isEmpty()); + Q_ASSERT(!build.isEmpty()); + + sendCommand({ + {"cookie", ""}, + {"type", "handshake"}, + {"major", 1}, + {"protocolVersion", QJsonObject{{"major", 1}} }, + {"sourceDirectory", source.toLocalFile()}, + {"buildDirectory", build.toLocalFile()}, + {"generator", "Ninja"} //TODO: make it possible to keep whatever they have ATM + }); +} + +void CMakeServer::configure(const QStringList& args) +{ + sendCommand({ + {"type", "configure"}, + {"cacheArguments", QJsonArray::fromStringList(args)} + }); +} + +void CMakeServer::compute() +{ + sendCommand({ {"type", "compute"} }); +} + +void CMakeServer::codemodel() +{ + sendCommand({ {"type", "codemodel"} }); +} diff --git a/projectmanagers/cmake/cmakeserverimportjob.h b/projectmanagers/cmake/cmakeserverimportjob.h new file mode 100644 --- /dev/null +++ b/projectmanagers/cmake/cmakeserverimportjob.h @@ -0,0 +1,56 @@ +/* KDevelop CMake Support + * + * 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) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#ifndef CMAKESERVERIMPORTJOB_H +#define CMAKESERVERIMPORTJOB_H + +#include +#include "cmakeprojectdata.h" + +namespace KDevelop +{ +class IProject; +} + +class CMakeServerImportJob : public KJob +{ + Q_OBJECT +public: + CMakeServerImportJob(KDevelop::IProject* project, CMakeServer* server, QObject* parent); + + enum Error { NoError, UnexpectedDisconnect, ErrorResponse }; + + void start() override; + + KDevelop::IProject* project() const { return m_project; } + + CMakeProjectData projectData() const { return m_data; } + +private: + void doStart(); + void processResponse(const QJsonObject &response); + + QPointer m_server; + KDevelop::IProject* m_project; + + CMakeProjectData m_data; +}; + +#endif // CMAKESERVERIMPORTJOB_H diff --git a/projectmanagers/cmake/cmakeserverimportjob.cpp b/projectmanagers/cmake/cmakeserverimportjob.cpp new file mode 100644 --- /dev/null +++ b/projectmanagers/cmake/cmakeserverimportjob.cpp @@ -0,0 +1,166 @@ +/* KDevelop CMake Support + * + * 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) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include "cmakeserverimportjob.h" +#include "cmakeutils.h" +#include "cmakeserver.h" +#include +#include +#include +#include "debug.h" +#include + +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 QString unescape(const QStringRef& input) +{ + QString output; + output.reserve(input.length()); + bool isEscaped = false; + for (auto it = input.data(), end = it + input.length(); it != end; ++it) { + QChar c = *it; + if (!isEscaped && c == '\\') { + isEscaped = true; + } else { + output.append(c); + isEscaped = false; + } + } + return output; +} + +static QHash processDefines(const QString &compileFlags, const QJsonArray &defines) +{ + QHash ret; + const auto& defineRx = MakeFileResolver::defineRegularExpression(); + auto it = defineRx.globalMatch(compileFlags); + while (it.hasNext()) { + const auto match = it.next(); + QString value; + if (match.lastCapturedIndex() > 1) { + value = unescape(match.capturedRef(match.lastCapturedIndex())); + } + ret[match.captured(1)] = value; + } + + for(const QJsonValue& defineValue: defines) { + const QString define = defineValue.toString(); + const int eqIdx = define.indexOf(QLatin1Char('=')); + if (eqIdx<0) + ret[define] = QString(); + else + ret[define.left(eqIdx)] = define.mid(eqIdx+1); + } + return ret; +} + +static void processFileData(const QJsonObject &response, CMakeProjectData &data) +{ + const auto configs = response.value(QLatin1String("configurations")).toArray(); + qCDebug(CMAKE) << "process response" << response; + for (const auto &config: configs) { + const auto projects = config.toObject().value(QLatin1String("projects")).toArray(); + for (const auto &project: projects) { + const auto targets = project.toObject().value(QLatin1String("targets")).toArray(); + for (const auto &targetObject: targets) { + const auto target = targetObject.toObject(); + const KDevelop::Path targetDir(target.value(QLatin1String("sourceDirectory")).toString()); + + data.targets[targetDir] += target.value(QLatin1String("name")).toString(); + + const auto fileGroups = target.value(QLatin1String("fileGroups")).toArray(); + for (const auto &fileGroupValue: fileGroups) { + const auto fileGroup = fileGroupValue.toObject(); + CMakeFile file; + file.includes = kTransform(fileGroup.value(QLatin1String("includePath")).toArray(), [](const QJsonValue& val) { return KDevelop::Path(val.toObject().value(QLatin1String("path")).toString()); }); + file.defines = processDefines(fileGroup.value(QLatin1String("compileFlags")).toString(), fileGroup.value(QLatin1String("defines")).toArray()); + + const auto sourcesArray = fileGroup.value(QLatin1String("sources")).toArray(); + const KDevelop::Path::List sources = kTransform(sourcesArray, [targetDir](const QJsonValue& val) { return KDevelop::Path(targetDir, val.toString()); }); + for (const auto& source: sources) { + data.compilationData.files[source] = file; + } + qCDebug(CMAKE) << "registering..." << sources << file; + } + } + } + } +} + +CMakeServerImportJob::CMakeServerImportJob(KDevelop::IProject* project, CMakeServer* server, QObject* parent) + : KJob(parent) + , m_server(server) + , m_project(project) +{ + connect(m_server, &CMakeServer::disconnected, this, [this]() { + setError(UnexpectedDisconnect); + emitResult(); + }); +} + +void CMakeServerImportJob::start() +{ + if (m_server->isServerAvailable()) + doStart(); + else + connect(m_server, &CMakeServer::connected, this, &CMakeServerImportJob::doStart); +} + +void CMakeServerImportJob::doStart() +{ + connect(m_server, &CMakeServer::response, this, &CMakeServerImportJob::processResponse); + + m_server->handshake(m_project->path(), CMake::currentBuildDir(m_project)); +} + +void CMakeServerImportJob::processResponse(const QJsonObject& response) +{ + const auto responseType = response.value(QLatin1String("type")); + if (responseType == QLatin1String("reply")) { + const auto inReplyTo = response.value(QLatin1String("inReplyTo")); + qCDebug(CMAKE) << "replying..." << inReplyTo; + if (inReplyTo == QLatin1String("handshake")) + m_server->configure({}); + else if (inReplyTo == QLatin1String("configure")) + m_server->compute(); + else if (inReplyTo == QLatin1String("compute")) + m_server->codemodel(); + else if(inReplyTo == QLatin1String("codemodel")) { + processFileData(response, m_data); + emitResult(); + } else + qWarning() << "unhandled reply" << response; + } else if(responseType == QLatin1String("error")) { + setError(ErrorResponse); + setErrorText(response.value(QLatin1String("errorMessage")).toString()); + qWarning() << "error!!" << response; + emitResult(); + } else { + qWarning() << "unhandled message" << response; + } +} diff --git a/projectmanagers/cmake/tests/CMakeLists.txt b/projectmanagers/cmake/tests/CMakeLists.txt --- a/projectmanagers/cmake/tests/CMakeLists.txt +++ b/projectmanagers/cmake/tests/CMakeLists.txt @@ -24,6 +24,8 @@ kdevcmake_add_test(test_cmakemanager KDev::Language KDev::Tests KDev::Project kdevcmakemanagernosettings) # kdevcmake_add_test(ctestfindsuitestest KDev::Language KDev::Tests) +kdevcmake_add_test(test_cmakeserver KDev::Language KDev::Tests KDev::Project kdevcmakemanagernosettings) + # this is not a unit test but a testing tool, kept here for convenience add_executable(kdevprojectopen kdevprojectopen.cpp) target_link_libraries(kdevprojectopen Qt5::Test KDev::Project KDev::Tests kdevcmakecommon) diff --git a/projectmanagers/cmake/tests/manual/kf5_app/CMakeLists.txt b/projectmanagers/cmake/tests/manual/kf5_app/CMakeLists.txt --- a/projectmanagers/cmake/tests/manual/kf5_app/CMakeLists.txt +++ b/projectmanagers/cmake/tests/manual/kf5_app/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 2.8) +cmake_minimum_required(VERSION 3.0) find_package(ECM 0.0.9 REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${KDevelop_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH}) diff --git a/projectmanagers/cmake/tests/manual/target_includes/CMakeLists.txt b/projectmanagers/cmake/tests/manual/target_includes/CMakeLists.txt --- a/projectmanagers/cmake/tests/manual/target_includes/CMakeLists.txt +++ b/projectmanagers/cmake/tests/manual/target_includes/CMakeLists.txt @@ -3,4 +3,4 @@ project(target_includes) add_executable(target main.cpp) -set_property(TARGET target APPEND PROPERTY INCLUDE_DIRECTORIES "includes") +set_property(TARGET target APPEND PROPERTY INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/includes") diff --git a/projectmanagers/cmake/tests/paths.h.cmake b/projectmanagers/cmake/tests/paths.h.cmake --- a/projectmanagers/cmake/tests/paths.h.cmake +++ b/projectmanagers/cmake/tests/paths.h.cmake @@ -1,4 +1,5 @@ #define CMAKE_TESTS_PROJECTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/manual" +#define CMAKE_TESTS_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}" #define CMAKE_INSTALLED_MODULES "${DATA_INSTALL_DIR}/cmake/modules" #define TEST_PREFIX_PATH "${CMAKE_PREFIX_PATH};${CMAKE_INSTALL_PREFIX}" #define TEST_ENV_PREFIX_PATH "$ENV{CMAKE_PREFIX_PATH}" diff --git a/projectmanagers/cmake/tests/test_cmakemanager.cpp b/projectmanagers/cmake/tests/test_cmakemanager.cpp --- a/projectmanagers/cmake/tests/test_cmakemanager.cpp +++ b/projectmanagers/cmake/tests/test_cmakemanager.cpp @@ -277,10 +277,10 @@ { IProject* project = loadProject("custom_target_sources"); - QEXPECT_FAIL("", "Will fix soon, hopefully", Abort); QList targets = project->buildSystemManager()->targets(project->projectItem()); QVERIFY(targets.size() == 1); + QEXPECT_FAIL("", "Will fix soon, hopefully", Abort); ProjectTargetItem *target = targets.first(); QCOMPARE(target->fileList().size(), 1); QCOMPARE(target->fileList().first()->baseName(), QStringLiteral("foo.cpp")); @@ -343,7 +343,7 @@ Path sourceDir = project->path(); Path buildDir(sourceDir, "build/"); - CMakeImportJob* job = new CMakeImportJob(project, this); + auto job = new CMakeImportJsonJob(project, this); job->start(); } diff --git a/projectmanagers/cmake/tests/test_cmakeserver.cpp b/projectmanagers/cmake/tests/test_cmakeserver.cpp new file mode 100644 --- /dev/null +++ b/projectmanagers/cmake/tests/test_cmakeserver.cpp @@ -0,0 +1,74 @@ +/* KDevelop CMake Support + * + * 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) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include +#include "testhelpers.h" +#include + +using namespace KDevelop; + +class CMakeServerTest : public QObject +{ +Q_OBJECT +private slots: + void testRun() + { + CMakeServer server(this); + QSignalSpy spyConnected(&server, &CMakeServer::connected); + QVERIFY(server.isServerAvailable() || spyConnected.wait()); + + QSignalSpy spy(&server, &CMakeServer::response); + + QJsonObject codeModel; + int errors = 0; + connect(&server, &CMakeServer::response, this, [&errors, &codeModel, &server](const QJsonObject &response) { + if (response.value("type") == QLatin1String("reply")) { + if (response.value("inReplyTo") == QLatin1String("configure")) + server.compute(); + else if (response.value("inReplyTo") == QLatin1String("compute")) + server.codemodel(); + else if(response.value("inReplyTo") == QLatin1String("codemodel")) + codeModel = response; + } else if(response.value("type") == QLatin1String("error")) { + ++errors; + } + }); + + const QString name = "single_subdirectory"; + const auto paths = projectPaths(name); + const QString builddir = QStringLiteral(CMAKE_TESTS_BINARY_DIR "/cmake-server-test-builddir/") + name; + QVERIFY(QDir(builddir).removeRecursively()); + QVERIFY(QDir(builddir).mkpath(builddir)); + + QVERIFY(spy.wait()); + server.handshake(paths.sourceDir, Path(builddir)); + QVERIFY(spy.wait()); + server.configure({}); + while(codeModel.isEmpty()) + QVERIFY(spy.wait()); + QCOMPARE(errors, 0); + QVERIFY(!codeModel.isEmpty()); + qDebug() << "codemodel" << codeModel; + } +}; + +QTEST_MAIN( CMakeServerTest ) + +#include "test_cmakeserver.moc" diff --git a/projectmanagers/cmake/tests/testhelpers.h b/projectmanagers/cmake/tests/testhelpers.h --- a/projectmanagers/cmake/tests/testhelpers.h +++ b/projectmanagers/cmake/tests/testhelpers.h @@ -132,8 +132,9 @@ KDevelop::ICore::self()->projectController()->openProject(paths.projectFile.toUrl()); - if ( spy.isEmpty() && !spy.wait(30000) ) + if ( spy.isEmpty() && !spy.wait(30000) ) { qFatal( "Timeout while waiting for opened signal" ); + } KDevelop::IProject* project = KDevelop::ICore::self()->projectController()->findProjectByName(name); Q_ASSERT(project); diff --git a/projectmanagers/custommake/makefileresolver/makefileresolver.h b/projectmanagers/custommake/makefileresolver/makefileresolver.h --- a/projectmanagers/custommake/makefileresolver/makefileresolver.h +++ b/projectmanagers/custommake/makefileresolver/makefileresolver.h @@ -73,6 +73,8 @@ void enableMakeResolution(bool enable); PathResolutionResult processOutput(const QString& fullOutput, const QString& workingDirectory) const; + static QRegularExpression defineRegularExpression(); + private: PathResolutionResult resolveIncludePath( const QString& file, const QString& workingDirectory, int maxStepsUp = 20 ); diff --git a/projectmanagers/custommake/makefileresolver/makefileresolver.cpp b/projectmanagers/custommake/makefileresolver/makefileresolver.cpp --- a/projectmanagers/custommake/makefileresolver/makefileresolver.cpp +++ b/projectmanagers/custommake/makefileresolver/makefileresolver.cpp @@ -662,7 +662,7 @@ return ret; } -static QRegularExpression defineRegularExpression() +QRegularExpression MakeFileResolver::defineRegularExpression() { static const QRegularExpression pattern( "-D([^\\s=]+)(?:=(?:\"(.*?)(?