File indexing completed on 2024-05-05 04:39:26
0001 /* 0002 SPDX-FileCopyrightText: 2017 Aleix Pol <aleixpol@kde.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "cmakeserver.h" 0008 #include "cmakeprojectdata.h" 0009 #include "cmakeutils.h" 0010 0011 #include <interfaces/iruntime.h> 0012 #include <interfaces/iruntimecontroller.h> 0013 #include <interfaces/icore.h> 0014 #include <interfaces/iproject.h> 0015 0016 #include <QDir> 0017 #include <QJsonDocument> 0018 #include <QJsonObject> 0019 #include <QJsonArray> 0020 #include <QTimer> 0021 #include <QTemporaryFile> 0022 #include "debug.h" 0023 0024 CMakeServer::CMakeServer(KDevelop::IProject* project) 0025 : QObject() 0026 , m_localSocket(new QLocalSocket(this)) 0027 { 0028 QString path; 0029 { 0030 const auto cacheLocation = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); 0031 QDir::temp().mkpath(cacheLocation); 0032 0033 QTemporaryFile file(cacheLocation + QLatin1String("/kdevelopcmake")); 0034 file.open(); 0035 file.close(); 0036 path = file.fileName(); 0037 Q_ASSERT(!path.isEmpty()); 0038 } 0039 0040 m_process.setProcessChannelMode(QProcess::ForwardedChannels); 0041 0042 connect(&m_process, &QProcess::errorOccurred, 0043 this, [this, path](QProcess::ProcessError error) { 0044 qCWarning(CMAKE) << "cmake server error:" << error << path << m_process.readAllStandardError() << m_process.readAllStandardOutput(); 0045 }); 0046 connect(&m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [](int code){ 0047 qCDebug(CMAKE) << "cmake server finished with code" << code; 0048 }); 0049 connect(&m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &CMakeServer::finished); 0050 0051 connect(m_localSocket, &QIODevice::readyRead, this, &CMakeServer::processOutput); 0052 connect(m_localSocket, &QLocalSocket::errorOccurred, this, 0053 [this, path](QLocalSocket::LocalSocketError socketError) { 0054 qCWarning(CMAKE) << "cmake server socket error:" << socketError << path; 0055 setConnected(false); 0056 }); 0057 connect(m_localSocket, &QLocalSocket::connected, this, [this]() { setConnected(true); }); 0058 0059 connect(&m_process, &QProcess::started, this, [this, path](){ 0060 //Once the process has started, wait for the file to be created, then connect to it 0061 QTimer::singleShot(1000, this, [this, path]() { 0062 m_localSocket->connectToServer(path, QIODevice::ReadWrite); 0063 }); 0064 }); 0065 // we're called with the importing project as our parent, so we can fetch configured 0066 // cmake executable (project-specific or kdevelop-wide) rather than the system version. 0067 m_process.setProgram(CMake::currentCMakeExecutable(project).toLocalFile()); 0068 m_process.setArguments({QStringLiteral("-E"), QStringLiteral("server"), QStringLiteral("--experimental"), QLatin1String("--pipe=") + path}); 0069 KDevelop::ICore::self()->runtimeController()->currentRuntime()->startProcess(&m_process); 0070 } 0071 0072 CMakeServer::~CMakeServer() 0073 { 0074 m_process.disconnect(); 0075 m_process.kill(); 0076 m_process.waitForFinished(); 0077 } 0078 0079 void CMakeServer::setConnected(bool conn) 0080 { 0081 if (conn == m_connected) 0082 return; 0083 0084 m_connected = conn; 0085 if (m_connected) 0086 Q_EMIT connected(); 0087 else 0088 Q_EMIT disconnected(); 0089 } 0090 0091 bool CMakeServer::isServerAvailable() 0092 { 0093 return m_localSocket->isOpen(); 0094 } 0095 0096 static QByteArray openTag() { return QByteArrayLiteral("\n[== \"CMake Server\" ==[\n"); } 0097 static QByteArray closeTag() { return QByteArrayLiteral("\n]== \"CMake Server\" ==]\n"); } 0098 0099 void CMakeServer::sendCommand(const QJsonObject& object) 0100 { 0101 Q_ASSERT(isServerAvailable()); 0102 0103 const QByteArray data = openTag() + QJsonDocument(object).toJson(QJsonDocument::Compact) + closeTag(); 0104 auto len = m_localSocket->write(data); 0105 // qCDebug(CMAKE) << "writing...\n" << QJsonDocument(object).toJson(); 0106 Q_ASSERT(len > 0); 0107 } 0108 0109 void CMakeServer::processOutput() 0110 { 0111 Q_ASSERT(m_localSocket); 0112 0113 const auto openTag = ::openTag(); 0114 const auto closeTag = ::closeTag(); 0115 0116 m_buffer += m_localSocket->readAll(); 0117 for(; m_buffer.size() > openTag.size(); ) { 0118 0119 Q_ASSERT(m_buffer.startsWith(openTag)); 0120 const int idx = m_buffer.indexOf(closeTag, openTag.size()); 0121 if (idx >= 0) { 0122 emitResponse(m_buffer.mid(openTag.size(), idx - openTag.size())); 0123 m_buffer.remove(0, idx + closeTag.size()); 0124 } else { 0125 break; 0126 } 0127 } 0128 } 0129 0130 void CMakeServer::emitResponse(const QByteArray& data) 0131 { 0132 QJsonParseError error; 0133 auto doc = QJsonDocument::fromJson(data, &error); 0134 if (error.error) { 0135 qCWarning(CMAKE) << "error processing" << error.errorString() << data; 0136 } 0137 Q_ASSERT(doc.isObject()); 0138 Q_EMIT response(doc.object()); 0139 } 0140 0141 void CMakeServer::handshake(const KDevelop::Path& source, const KDevelop::Path& build) 0142 { 0143 Q_ASSERT(!source.isEmpty()); 0144 0145 const QString generatorVariable = QStringLiteral("CMAKE_GENERATOR"); 0146 const QString homeDirectoryVariable = QStringLiteral("CMAKE_HOME_DIRECTORY"); 0147 const QString cacheFileDirectoryVariable = QStringLiteral("CMAKE_CACHEFILE_DIR"); 0148 const auto cacheValues = CMake::readCacheValues(KDevelop::Path(build, QStringLiteral("CMakeCache.txt")), 0149 {generatorVariable, homeDirectoryVariable, cacheFileDirectoryVariable}); 0150 0151 QString generator = cacheValues.value(generatorVariable); 0152 if (generator.isEmpty()) { 0153 generator = CMake::defaultGenerator(); 0154 } 0155 0156 // prefer pre-existing source directory, see also: https://gitlab.kitware.com/cmake/cmake/issues/16736 0157 QString sourceDirectory = cacheValues.value(homeDirectoryVariable); 0158 if (sourceDirectory.isEmpty()) { 0159 sourceDirectory = source.toLocalFile(); 0160 } else if (QFileInfo(sourceDirectory).canonicalFilePath() != QFileInfo(source.toLocalFile()).canonicalFilePath()) { 0161 qCWarning(CMAKE) << "Build directory is configured for another source directory:" 0162 << homeDirectoryVariable << sourceDirectory 0163 << "wanted to open" << source << "in" << build; 0164 } 0165 0166 // prefer to reuse the exact same build dir path to prevent useless recompilation 0167 // when we open a symlinked project path 0168 QString buildDirectory = cacheValues.value(cacheFileDirectoryVariable); 0169 if (buildDirectory.isEmpty()) { 0170 buildDirectory = build.toLocalFile(); 0171 } else if (QFileInfo(buildDirectory).canonicalFilePath() != QFileInfo(build.toLocalFile()).canonicalFilePath()) { 0172 qCWarning(CMAKE) << "Build directory mismatch:" 0173 << cacheFileDirectoryVariable << buildDirectory 0174 << "wanted to open" << build; 0175 buildDirectory = build.toLocalFile(); 0176 } 0177 0178 qCDebug(CMAKE) << "Using generator" << generator << "for project" 0179 << sourceDirectory << "aka" << source 0180 << "in" << buildDirectory << "aka" << build; 0181 0182 sendCommand({ 0183 {QStringLiteral("cookie"), {}}, 0184 {QStringLiteral("type"), QStringLiteral("handshake")}, 0185 {QStringLiteral("major"), 1}, 0186 {QStringLiteral("protocolVersion"), QJsonObject{{QStringLiteral("major"), 1}} }, 0187 {QStringLiteral("sourceDirectory"), sourceDirectory}, 0188 {QStringLiteral("buildDirectory"), buildDirectory}, 0189 {QStringLiteral("generator"), generator} 0190 }); 0191 } 0192 0193 void CMakeServer::configure(const QStringList& args) 0194 { 0195 sendCommand({ 0196 {QStringLiteral("type"), QStringLiteral("configure")}, 0197 {QStringLiteral("cacheArguments"), QJsonArray::fromStringList(args)} 0198 }); 0199 } 0200 0201 void CMakeServer::compute() 0202 { 0203 sendCommand({ {QStringLiteral("type"), QStringLiteral("compute")} }); 0204 } 0205 0206 void CMakeServer::codemodel() 0207 { 0208 sendCommand({ {QStringLiteral("type"), QStringLiteral("codemodel")} }); 0209 } 0210 0211 #include "moc_cmakeserver.cpp"