forked from qt-creator/qt-creator
CMake: Remove servermode support
Task-number: QTCREATORBUG-23915 Change-Id: I2a58e1d6d95c28e25787722fa37448d86c4aebc9 Reviewed-by: Eike Ziller <eike.ziller@qt.io> Reviewed-by: David Schulz <david.schulz@qt.io> Reviewed-by: hjk <hjk@qt.io> Reviewed-by: Alessandro Portale <alessandro.portale@qt.io>
This commit is contained in:
@@ -60,11 +60,8 @@
|
|||||||
|
|
||||||
\section1 Adding CMake Tools
|
\section1 Adding CMake Tools
|
||||||
|
|
||||||
\QC supports CMake version 3.7, or later.
|
\QC requires CMake's {https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html}
|
||||||
|
{file-based API}. Please make sure to use CMake version 3.14, or later.
|
||||||
For best results use CMake version 3.14, or later, as \QC supports the
|
|
||||||
\l {https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html}
|
|
||||||
{file-based API}.
|
|
||||||
|
|
||||||
To specify paths to CMake executables:
|
To specify paths to CMake executables:
|
||||||
|
|
||||||
|
@@ -40,6 +40,4 @@ add_qtc_plugin(CMakeProjectManager
|
|||||||
fileapiparser.cpp fileapiparser.h
|
fileapiparser.cpp fileapiparser.h
|
||||||
fileapireader.cpp fileapireader.h
|
fileapireader.cpp fileapireader.h
|
||||||
projecttreehelper.cpp projecttreehelper.h
|
projecttreehelper.cpp projecttreehelper.h
|
||||||
servermode.cpp servermode.h
|
|
||||||
servermodereader.cpp servermodereader.h
|
|
||||||
)
|
)
|
||||||
|
@@ -26,7 +26,6 @@
|
|||||||
#include "builddirreader.h"
|
#include "builddirreader.h"
|
||||||
|
|
||||||
#include "fileapireader.h"
|
#include "fileapireader.h"
|
||||||
#include "servermodereader.h"
|
|
||||||
|
|
||||||
#include <utils/qtcassert.h>
|
#include <utils/qtcassert.h>
|
||||||
|
|
||||||
@@ -50,8 +49,6 @@ std::unique_ptr<BuildDirReader> BuildDirReader::createReader(const BuildDirParam
|
|||||||
switch (type.value()) {
|
switch (type.value()) {
|
||||||
case CMakeTool::FileApi:
|
case CMakeTool::FileApi:
|
||||||
return std::make_unique<FileApiReader>();
|
return std::make_unique<FileApiReader>();
|
||||||
case CMakeTool::ServerMode:
|
|
||||||
return std::make_unique<ServerModeReader>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -254,10 +254,10 @@ Tasks CMakeKitAspect::validate(const Kit *k) const
|
|||||||
CMakeTool *tool = CMakeKitAspect::cmakeTool(k);
|
CMakeTool *tool = CMakeKitAspect::cmakeTool(k);
|
||||||
if (tool) {
|
if (tool) {
|
||||||
CMakeTool::Version version = tool->version();
|
CMakeTool::Version version = tool->version();
|
||||||
if (version.major < 3 || (version.major == 3 && version.minor < 7)) {
|
if (version.major < 3 || (version.major == 3 && version.minor < 14)) {
|
||||||
result << BuildSystemTask(Task::Warning,
|
result << BuildSystemTask(Task::Warning,
|
||||||
tr("CMake version %1 is unsupported. Please update to "
|
tr("CMake version %1 is unsupported. Please update to "
|
||||||
"version 3.7 or later.")
|
"version 3.14 (with file-api) or later.")
|
||||||
.arg(QString::fromUtf8(version.fullVersion)));
|
.arg(QString::fromUtf8(version.fullVersion)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -717,9 +717,8 @@ Tasks CMakeGeneratorKitAspect::validate(const Kit *k) const
|
|||||||
if (!it->supportsToolset && !info.toolset.isEmpty())
|
if (!it->supportsToolset && !info.toolset.isEmpty())
|
||||||
addWarning(tr("Toolset is not supported by the selected CMake generator."));
|
addWarning(tr("Toolset is not supported by the selected CMake generator."));
|
||||||
}
|
}
|
||||||
if (!tool->hasServerMode() && !tool->hasFileApi() && info.extraGenerator != "CodeBlocks") {
|
if (!tool->hasFileApi()) {
|
||||||
addWarning(tr("The selected CMake binary has no server-mode and the CMake "
|
addWarning(tr("The selected CMake binary does not support file-api. "
|
||||||
"generator does not generate a CodeBlocks file. "
|
|
||||||
"%1 will not be able to parse CMake projects.")
|
"%1 will not be able to parse CMake projects.")
|
||||||
.arg(Core::Constants::IDE_DISPLAY_NAME));
|
.arg(Core::Constants::IDE_DISPLAY_NAME));
|
||||||
}
|
}
|
||||||
|
@@ -36,9 +36,7 @@ HEADERS = builddirmanager.h \
|
|||||||
fileapidataextractor.h \
|
fileapidataextractor.h \
|
||||||
fileapiparser.h \
|
fileapiparser.h \
|
||||||
fileapireader.h \
|
fileapireader.h \
|
||||||
projecttreehelper.h \
|
projecttreehelper.h
|
||||||
servermode.h \
|
|
||||||
servermodereader.h
|
|
||||||
|
|
||||||
SOURCES = builddirmanager.cpp \
|
SOURCES = builddirmanager.cpp \
|
||||||
builddirparameters.cpp \
|
builddirparameters.cpp \
|
||||||
@@ -72,9 +70,7 @@ SOURCES = builddirmanager.cpp \
|
|||||||
fileapidataextractor.cpp \
|
fileapidataextractor.cpp \
|
||||||
fileapiparser.cpp \
|
fileapiparser.cpp \
|
||||||
fileapireader.cpp \
|
fileapireader.cpp \
|
||||||
projecttreehelper.cpp \
|
projecttreehelper.cpp
|
||||||
servermode.cpp \
|
|
||||||
servermodereader.cpp
|
|
||||||
|
|
||||||
RESOURCES += cmakeproject.qrc
|
RESOURCES += cmakeproject.qrc
|
||||||
|
|
||||||
|
@@ -89,10 +89,6 @@ QtcPlugin {
|
|||||||
"fileapireader.cpp",
|
"fileapireader.cpp",
|
||||||
"fileapireader.h",
|
"fileapireader.h",
|
||||||
"projecttreehelper.cpp",
|
"projecttreehelper.cpp",
|
||||||
"projecttreehelper.h",
|
"projecttreehelper.h"
|
||||||
"servermode.cpp",
|
|
||||||
"servermode.h",
|
|
||||||
"servermodereader.cpp",
|
|
||||||
"servermodereader.h"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -54,9 +54,6 @@ private slots:
|
|||||||
|
|
||||||
void testCMakeProjectImporterToolChain_data();
|
void testCMakeProjectImporterToolChain_data();
|
||||||
void testCMakeProjectImporterToolChain();
|
void testCMakeProjectImporterToolChain();
|
||||||
|
|
||||||
void testServerModeReaderProgress_data();
|
|
||||||
void testServerModeReaderProgress();
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@@ -114,14 +114,13 @@ public:
|
|||||||
, m_isAutoRun(item->isAutoRun())
|
, m_isAutoRun(item->isAutoRun())
|
||||||
, m_autoCreateBuildDirectory(item->autoCreateBuildDirectory())
|
, m_autoCreateBuildDirectory(item->autoCreateBuildDirectory())
|
||||||
, m_autodetected(item->isAutoDetected())
|
, m_autodetected(item->isAutoDetected())
|
||||||
, m_isSupported(item->hasFileApi() || item->hasServerMode())
|
, m_isSupported(item->hasFileApi())
|
||||||
, m_changed(changed)
|
, m_changed(changed)
|
||||||
{
|
{
|
||||||
updateErrorFlags();
|
updateErrorFlags();
|
||||||
m_tooltip = tr("Version: %1<br>Supports fileApi: %2<br>Supports server-mode: %3")
|
m_tooltip = tr("Version: %1<br>Supports fileApi: %2")
|
||||||
.arg(QString::fromUtf8(item->version().fullVersion))
|
.arg(QString::fromUtf8(item->version().fullVersion))
|
||||||
.arg(item->hasFileApi() ? tr("yes") : tr("no"))
|
.arg(item->hasFileApi() ? tr("yes") : tr("no"));
|
||||||
.arg(item->hasServerMode() ? tr("yes") : tr("no"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CMakeToolTreeItem(const QString &name,
|
CMakeToolTreeItem(const QString &name,
|
||||||
|
@@ -58,20 +58,11 @@ bool CMakeTool::Generator::matches(const QString &n, const QString &ex) const
|
|||||||
|
|
||||||
namespace Internal {
|
namespace Internal {
|
||||||
|
|
||||||
const char READER_TYPE_SERVERMODE[] = "servermode";
|
|
||||||
const char READER_TYPE_FILEAPI[] = "fileapi";
|
const char READER_TYPE_FILEAPI[] = "fileapi";
|
||||||
|
|
||||||
static bool ignoreFileApi()
|
|
||||||
{
|
|
||||||
static bool s_ignoreFileApi = qEnvironmentVariableIsSet("QTC_CMAKE_IGNORE_FILEAPI");
|
|
||||||
return s_ignoreFileApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Utils::optional<CMakeTool::ReaderType> readerTypeFromString(const QString &input)
|
static Utils::optional<CMakeTool::ReaderType> readerTypeFromString(const QString &input)
|
||||||
{
|
{
|
||||||
// Do not try to be clever here, just use whatever is in the string!
|
// Do not try to be clever here, just use whatever is in the string!
|
||||||
if (input == READER_TYPE_SERVERMODE)
|
|
||||||
return CMakeTool::ServerMode;
|
|
||||||
if (input == READER_TYPE_FILEAPI)
|
if (input == READER_TYPE_FILEAPI)
|
||||||
return CMakeTool::FileApi;
|
return CMakeTool::FileApi;
|
||||||
return {};
|
return {};
|
||||||
@@ -80,8 +71,6 @@ static Utils::optional<CMakeTool::ReaderType> readerTypeFromString(const QString
|
|||||||
static QString readerTypeToString(const CMakeTool::ReaderType &type)
|
static QString readerTypeToString(const CMakeTool::ReaderType &type)
|
||||||
{
|
{
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case CMakeTool::ServerMode:
|
|
||||||
return QString(READER_TYPE_SERVERMODE);
|
|
||||||
case CMakeTool::FileApi:
|
case CMakeTool::FileApi:
|
||||||
return QString(READER_TYPE_FILEAPI);
|
return QString(READER_TYPE_FILEAPI);
|
||||||
}
|
}
|
||||||
@@ -102,9 +91,7 @@ class IntrospectionData
|
|||||||
public:
|
public:
|
||||||
bool m_didAttemptToRun = false;
|
bool m_didAttemptToRun = false;
|
||||||
bool m_didRun = true;
|
bool m_didRun = true;
|
||||||
bool m_hasServerMode = false;
|
|
||||||
|
|
||||||
bool m_queriedServerMode = false;
|
|
||||||
bool m_triedCapabilities = false;
|
bool m_triedCapabilities = false;
|
||||||
|
|
||||||
QList<CMakeTool::Generator> m_generators;
|
QList<CMakeTool::Generator> m_generators;
|
||||||
@@ -193,13 +180,13 @@ void CMakeTool::setAutoCreateBuildDirectory(bool autoBuildDir)
|
|||||||
|
|
||||||
bool CMakeTool::isValid() const
|
bool CMakeTool::isValid() const
|
||||||
{
|
{
|
||||||
if (!m_id.isValid())
|
if (!m_id.isValid() || !m_introspection)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!m_introspection->m_didAttemptToRun)
|
if (!m_introspection->m_didAttemptToRun)
|
||||||
supportedGenerators();
|
readInformation();
|
||||||
|
|
||||||
return m_introspection->m_didRun && readerType().has_value();
|
return m_introspection->m_didRun && !m_introspection->m_fileApis.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::SynchronousProcessResponse CMakeTool::run(const QStringList &args, int timeoutS) const
|
Utils::SynchronousProcessResponse CMakeTool::run(const QStringList &args, int timeoutS) const
|
||||||
@@ -284,12 +271,14 @@ bool CMakeTool::autoCreateBuildDirectory() const
|
|||||||
|
|
||||||
QList<CMakeTool::Generator> CMakeTool::supportedGenerators() const
|
QList<CMakeTool::Generator> CMakeTool::supportedGenerators() const
|
||||||
{
|
{
|
||||||
readInformation(QueryType::GENERATORS);
|
return isValid() ? m_introspection->m_generators : QList<CMakeTool::Generator>();
|
||||||
return m_introspection->m_generators;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditor::Keywords CMakeTool::keywords()
|
TextEditor::Keywords CMakeTool::keywords()
|
||||||
{
|
{
|
||||||
|
if (!isValid())
|
||||||
|
return {};
|
||||||
|
|
||||||
if (m_introspection->m_functions.isEmpty() && m_introspection->m_didRun) {
|
if (m_introspection->m_functions.isEmpty() && m_introspection->m_didRun) {
|
||||||
Utils::SynchronousProcessResponse response;
|
Utils::SynchronousProcessResponse response;
|
||||||
response = run({"--help-command-list"}, 5);
|
response = run({"--help-command-list"}, 5);
|
||||||
@@ -317,28 +306,19 @@ TextEditor::Keywords CMakeTool::keywords()
|
|||||||
m_introspection->m_functionArgs);
|
m_introspection->m_functionArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CMakeTool::hasServerMode() const
|
|
||||||
{
|
|
||||||
readInformation(QueryType::SERVER_MODE);
|
|
||||||
return m_introspection->m_hasServerMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CMakeTool::hasFileApi() const
|
bool CMakeTool::hasFileApi() const
|
||||||
{
|
{
|
||||||
readInformation(QueryType::SERVER_MODE);
|
return isValid() ? !m_introspection->m_fileApis.isEmpty() : false;
|
||||||
return !m_introspection->m_fileApis.isEmpty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<std::pair<QString, int>> CMakeTool::supportedFileApiObjects() const
|
QVector<std::pair<QString, int>> CMakeTool::supportedFileApiObjects() const
|
||||||
{
|
{
|
||||||
readInformation(QueryType::SERVER_MODE);
|
return isValid() ? Utils::transform(m_introspection->m_fileApis, [](const Internal::FileApi &api) { return std::make_pair(api.kind, api.version.first); }) : QVector<std::pair<QString, int>>();
|
||||||
return Utils::transform(m_introspection->m_fileApis, [](const Internal::FileApi &api) { return std::make_pair(api.kind, api.version.first); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CMakeTool::Version CMakeTool::version() const
|
CMakeTool::Version CMakeTool::version() const
|
||||||
{
|
{
|
||||||
readInformation(QueryType::VERSION);
|
return isValid() ? m_introspection->m_version : CMakeTool::Version();
|
||||||
return m_introspection->m_version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CMakeTool::isAutoDetected() const
|
bool CMakeTool::isAutoDetected() const
|
||||||
@@ -375,10 +355,8 @@ Utils::optional<CMakeTool::ReaderType> CMakeTool::readerType() const
|
|||||||
return m_readerType; // Allow overriding the auto-detected value via .user files
|
return m_readerType; // Allow overriding the auto-detected value via .user files
|
||||||
|
|
||||||
// Find best possible reader type:
|
// Find best possible reader type:
|
||||||
if (hasFileApi() && !Internal::ignoreFileApi())
|
if (hasFileApi())
|
||||||
return FileApi;
|
return FileApi;
|
||||||
if (hasServerMode())
|
|
||||||
return ServerMode;
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,33 +382,16 @@ Utils::FilePath CMakeTool::searchQchFile(const Utils::FilePath &executable)
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMakeTool::readInformation(CMakeTool::QueryType type) const
|
void CMakeTool::readInformation() const
|
||||||
{
|
{
|
||||||
|
QTC_ASSERT(m_introspection, return );
|
||||||
if (!m_introspection->m_didRun && m_introspection->m_didAttemptToRun)
|
if (!m_introspection->m_didRun && m_introspection->m_didAttemptToRun)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_introspection->m_didAttemptToRun = true;
|
m_introspection->m_didAttemptToRun = true;
|
||||||
|
|
||||||
if (!m_introspection->m_triedCapabilities) {
|
|
||||||
fetchFromCapabilities();
|
fetchFromCapabilities();
|
||||||
m_introspection->m_triedCapabilities = true;
|
m_introspection->m_triedCapabilities = true;
|
||||||
m_introspection->m_queriedServerMode = true; // Got added after "-E capabilities" support!
|
|
||||||
} else {
|
|
||||||
if ((type == QueryType::GENERATORS && !m_introspection->m_generators.isEmpty())
|
|
||||||
|| (type == QueryType::SERVER_MODE && m_introspection->m_queriedServerMode)
|
|
||||||
|| (type == QueryType::VERSION && !m_introspection->m_version.fullVersion.isEmpty()))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (type == QueryType::GENERATORS) {
|
|
||||||
fetchGeneratorsFromHelp();
|
|
||||||
} else if (type == QueryType::SERVER_MODE) {
|
|
||||||
// Nothing to do...
|
|
||||||
} else if (type == QueryType::VERSION) {
|
|
||||||
fetchVersionFromVersionOutput();
|
|
||||||
} else {
|
|
||||||
QTC_ASSERT(false, return );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static QStringList parseDefinition(const QString &definition)
|
static QStringList parseDefinition(const QString &definition)
|
||||||
@@ -524,96 +485,16 @@ QStringList CMakeTool::parseVariableOutput(const QString &output)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMakeTool::fetchGeneratorsFromHelp() const
|
|
||||||
{
|
|
||||||
Utils::SynchronousProcessResponse response = run({"--help"});
|
|
||||||
m_introspection->m_didRun = m_introspection->m_didRun
|
|
||||||
&& response.result == Utils::SynchronousProcessResponse::Finished;
|
|
||||||
|
|
||||||
if (response.result == Utils::SynchronousProcessResponse::Finished)
|
|
||||||
parseGeneratorsFromHelp(response.stdOut().split('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
void CMakeTool::parseGeneratorsFromHelp(const QStringList &lines) const
|
|
||||||
{
|
|
||||||
bool inGeneratorSection = false;
|
|
||||||
QHash<QString, QStringList> generatorInfo;
|
|
||||||
foreach (const QString &line, lines) {
|
|
||||||
if (line.isEmpty())
|
|
||||||
continue;
|
|
||||||
if (line == "Generators") {
|
|
||||||
inGeneratorSection = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!inGeneratorSection)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (line.startsWith(" ") && line.at(3) != ' ') {
|
|
||||||
int pos = line.indexOf('=');
|
|
||||||
if (pos < 0)
|
|
||||||
pos = line.length();
|
|
||||||
if (pos >= 0) {
|
|
||||||
--pos;
|
|
||||||
while (pos > 2 && line.at(pos).isSpace())
|
|
||||||
--pos;
|
|
||||||
}
|
|
||||||
if (pos > 2) {
|
|
||||||
const QString fullName = line.mid(2, pos - 1);
|
|
||||||
const int dashPos = fullName.indexOf(" - ");
|
|
||||||
QString generator;
|
|
||||||
QString extra;
|
|
||||||
if (dashPos < 0) {
|
|
||||||
generator = fullName;
|
|
||||||
} else {
|
|
||||||
extra = fullName.mid(0, dashPos);
|
|
||||||
generator = fullName.mid(dashPos + 3);
|
|
||||||
}
|
|
||||||
QStringList value = generatorInfo.value(generator);
|
|
||||||
if (!extra.isEmpty())
|
|
||||||
value.append(extra);
|
|
||||||
generatorInfo.insert(generator, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate genertor list:
|
|
||||||
for (auto it = generatorInfo.constBegin(); it != generatorInfo.constEnd(); ++it)
|
|
||||||
m_introspection->m_generators.append(Generator(it.key(), it.value()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void CMakeTool::fetchVersionFromVersionOutput() const
|
|
||||||
{
|
|
||||||
Utils::SynchronousProcessResponse response = run({"--version"});
|
|
||||||
|
|
||||||
m_introspection->m_didRun = m_introspection->m_didRun
|
|
||||||
&& response.result == Utils::SynchronousProcessResponse::Finished;
|
|
||||||
|
|
||||||
if (response.result == Utils::SynchronousProcessResponse::Finished)
|
|
||||||
parseVersionFormVersionOutput(response.stdOut().split('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
void CMakeTool::parseVersionFormVersionOutput(const QStringList &lines) const
|
|
||||||
{
|
|
||||||
QRegularExpression versionLine("^cmake.* version ((\\d+).(\\d+).(\\d+).*)$");
|
|
||||||
for (const QString &line : lines) {
|
|
||||||
QRegularExpressionMatch match = versionLine.match(line);
|
|
||||||
if (!match.hasMatch())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
m_introspection->m_version.major = match.captured(2).toInt();
|
|
||||||
m_introspection->m_version.minor = match.captured(3).toInt();
|
|
||||||
m_introspection->m_version.patch = match.captured(4).toInt();
|
|
||||||
m_introspection->m_version.fullVersion = match.captured(1).toUtf8();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CMakeTool::fetchFromCapabilities() const
|
void CMakeTool::fetchFromCapabilities() const
|
||||||
{
|
{
|
||||||
Utils::SynchronousProcessResponse response = run({"-E", "capabilities"});
|
Utils::SynchronousProcessResponse response = run({"-E", "capabilities"});
|
||||||
|
|
||||||
if (response.result == Utils::SynchronousProcessResponse::Finished)
|
if (response.result == Utils::SynchronousProcessResponse::Finished) {
|
||||||
|
m_introspection->m_didRun = true;
|
||||||
parseFromCapabilities(response.stdOut());
|
parseFromCapabilities(response.stdOut());
|
||||||
|
} else {
|
||||||
|
m_introspection->m_didRun = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static int getVersion(const QVariantMap &obj, const QString value)
|
static int getVersion(const QVariantMap &obj, const QString value)
|
||||||
@@ -632,7 +513,6 @@ void CMakeTool::parseFromCapabilities(const QString &input) const
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
const QVariantMap data = doc.object().toVariantMap();
|
const QVariantMap data = doc.object().toVariantMap();
|
||||||
m_introspection->m_hasServerMode = data.value("serverMode").toBool();
|
|
||||||
const QVariantList generatorList = data.value("generators").toList();
|
const QVariantList generatorList = data.value("generators").toList();
|
||||||
for (const QVariant &v : generatorList) {
|
for (const QVariant &v : generatorList) {
|
||||||
const QVariantMap gen = v.toMap();
|
const QVariantMap gen = v.toMap();
|
||||||
|
@@ -47,7 +47,7 @@ class CMAKE_EXPORT CMakeTool
|
|||||||
public:
|
public:
|
||||||
enum Detection { ManualDetection, AutoDetection };
|
enum Detection { ManualDetection, AutoDetection };
|
||||||
|
|
||||||
enum ReaderType { ServerMode, FileApi };
|
enum ReaderType { FileApi };
|
||||||
|
|
||||||
struct Version
|
struct Version
|
||||||
{
|
{
|
||||||
@@ -98,7 +98,6 @@ public:
|
|||||||
bool autoCreateBuildDirectory() const;
|
bool autoCreateBuildDirectory() const;
|
||||||
QList<Generator> supportedGenerators() const;
|
QList<Generator> supportedGenerators() const;
|
||||||
TextEditor::Keywords keywords();
|
TextEditor::Keywords keywords();
|
||||||
bool hasServerMode() const;
|
|
||||||
bool hasFileApi() const;
|
bool hasFileApi() const;
|
||||||
QVector<std::pair<QString, int>> supportedFileApiObjects() const;
|
QVector<std::pair<QString, int>> supportedFileApiObjects() const;
|
||||||
Version version() const;
|
Version version() const;
|
||||||
@@ -115,21 +114,12 @@ public:
|
|||||||
static Utils::FilePath searchQchFile(const Utils::FilePath &executable);
|
static Utils::FilePath searchQchFile(const Utils::FilePath &executable);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class QueryType {
|
void readInformation() const;
|
||||||
GENERATORS,
|
|
||||||
SERVER_MODE,
|
|
||||||
VERSION
|
|
||||||
};
|
|
||||||
void readInformation(QueryType type) const;
|
|
||||||
|
|
||||||
Utils::SynchronousProcessResponse run(const QStringList &args, int timeoutS = 1) const;
|
Utils::SynchronousProcessResponse run(const QStringList &args, int timeoutS = 1) const;
|
||||||
void parseFunctionDetailsOutput(const QString &output);
|
void parseFunctionDetailsOutput(const QString &output);
|
||||||
QStringList parseVariableOutput(const QString &output);
|
QStringList parseVariableOutput(const QString &output);
|
||||||
|
|
||||||
void fetchGeneratorsFromHelp() const;
|
|
||||||
void parseGeneratorsFromHelp(const QStringList &lines) const;
|
|
||||||
void fetchVersionFromVersionOutput() const;
|
|
||||||
void parseVersionFormVersionOutput(const QStringList &lines) const;
|
|
||||||
void fetchFromCapabilities() const;
|
void fetchFromCapabilities() const;
|
||||||
void parseFromCapabilities(const QString &input) const;
|
void parseFromCapabilities(const QString &input) const;
|
||||||
|
|
||||||
|
@@ -97,13 +97,13 @@ void addCMakeInputs(FolderNode *root,
|
|||||||
addCMakeVFolder(cmakeVFolder.get(),
|
addCMakeVFolder(cmakeVFolder.get(),
|
||||||
buildDir,
|
buildDir,
|
||||||
100,
|
100,
|
||||||
QCoreApplication::translate("CMakeProjectManager::Internal::ServerModeReader",
|
QCoreApplication::translate("CMakeProjectManager::Internal::ProjectTreeHelper",
|
||||||
"<Build Directory>"),
|
"<Build Directory>"),
|
||||||
removeKnownNodes(knownFiles, std::move(buildInputs)));
|
removeKnownNodes(knownFiles, std::move(buildInputs)));
|
||||||
addCMakeVFolder(cmakeVFolder.get(),
|
addCMakeVFolder(cmakeVFolder.get(),
|
||||||
Utils::FilePath(),
|
Utils::FilePath(),
|
||||||
10,
|
10,
|
||||||
QCoreApplication::translate("CMakeProjectManager::Internal::ServerModeReader",
|
QCoreApplication::translate("CMakeProjectManager::Internal::ProjectTreeHelper",
|
||||||
"<Other Locations>"),
|
"<Other Locations>"),
|
||||||
removeKnownNodes(knownFiles, std::move(rootInputs)));
|
removeKnownNodes(knownFiles, std::move(rootInputs)));
|
||||||
|
|
||||||
@@ -187,7 +187,8 @@ void addHeaderNodes(ProjectNode *root,
|
|||||||
auto headerNode = std::make_unique<VirtualFolderNode>(root->filePath());
|
auto headerNode = std::make_unique<VirtualFolderNode>(root->filePath());
|
||||||
headerNode->setPriority(Node::DefaultPriority - 5);
|
headerNode->setPriority(Node::DefaultPriority - 5);
|
||||||
headerNode->setDisplayName(
|
headerNode->setDisplayName(
|
||||||
QCoreApplication::translate("CMakeProjectManager::Internal::ServerModeReader", "<Headers>"));
|
QCoreApplication::translate("CMakeProjectManager::Internal::ProjectTreeHelper",
|
||||||
|
"<Headers>"));
|
||||||
headerNode->setIcon(headerNodeIcon);
|
headerNode->setIcon(headerNodeIcon);
|
||||||
|
|
||||||
// Add scanned headers:
|
// Add scanned headers:
|
||||||
|
@@ -1,488 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
**
|
|
||||||
** Copyright (C) 2016 The Qt Company Ltd.
|
|
||||||
** Contact: https://www.qt.io/licensing/
|
|
||||||
**
|
|
||||||
** This file is part of Qt Creator.
|
|
||||||
**
|
|
||||||
** Commercial License Usage
|
|
||||||
** Licensees holding valid commercial Qt licenses may use this file in
|
|
||||||
** accordance with the commercial license agreement provided with the
|
|
||||||
** Software or, alternatively, in accordance with the terms contained in
|
|
||||||
** a written agreement between you and The Qt Company. For licensing terms
|
|
||||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
||||||
** information use the contact form at https://www.qt.io/contact-us.
|
|
||||||
**
|
|
||||||
** GNU General Public License Usage
|
|
||||||
** Alternatively, this file may be used under the terms of the GNU
|
|
||||||
** General Public License version 3 as published by the Free Software
|
|
||||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
|
||||||
** included in the packaging of this file. Please review the following
|
|
||||||
** information to ensure the GNU General Public License requirements will
|
|
||||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
|
||||||
**
|
|
||||||
****************************************************************************/
|
|
||||||
|
|
||||||
#include "servermode.h"
|
|
||||||
|
|
||||||
#include <coreplugin/reaper.h>
|
|
||||||
|
|
||||||
#include <utils/qtcassert.h>
|
|
||||||
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QLocalSocket>
|
|
||||||
#include <QUuid>
|
|
||||||
|
|
||||||
using namespace Utils;
|
|
||||||
|
|
||||||
namespace CMakeProjectManager {
|
|
||||||
namespace Internal {
|
|
||||||
|
|
||||||
const char COOKIE_KEY[] = "cookie";
|
|
||||||
const char IN_REPLY_TO_KEY[] = "inReplyTo";
|
|
||||||
const char NAME_KEY[] = "name";
|
|
||||||
const char TYPE_KEY[] = "type";
|
|
||||||
|
|
||||||
const char ERROR_TYPE[] = "error";
|
|
||||||
const char HANDSHAKE_TYPE[] = "handshake";
|
|
||||||
|
|
||||||
const char START_MAGIC[] = "\n[== \"CMake Server\" ==[\n";
|
|
||||||
const char END_MAGIC[] = "\n]== \"CMake Server\" ==]\n";
|
|
||||||
|
|
||||||
static Q_LOGGING_CATEGORY(cmakeServerMode, "qtc.cmake.serverMode", QtWarningMsg);
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
|
||||||
// Helpers:
|
|
||||||
// ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
bool isValid(const QVariant &v)
|
|
||||||
{
|
|
||||||
if (v.isNull())
|
|
||||||
return false;
|
|
||||||
if (v.type() == QVariant::String) // Workaround signals sending an empty string cookie
|
|
||||||
return !v.toString().isEmpty();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------
|
|
||||||
// ServerMode:
|
|
||||||
// --------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
ServerMode::ServerMode(const Environment &env,
|
|
||||||
const FilePath &sourceDirectory, const FilePath &buildDirectory,
|
|
||||||
const FilePath &cmakeExecutable,
|
|
||||||
const QString &generator, const QString &extraGenerator,
|
|
||||||
const QString &platform, const QString &toolset,
|
|
||||||
bool experimental, int major, int minor,
|
|
||||||
QObject *parent) :
|
|
||||||
QObject(parent),
|
|
||||||
#if defined(Q_OS_UNIX)
|
|
||||||
// Some unixes (e.g. Darwin) limit the length of a local socket to about 100 char (or less).
|
|
||||||
// Since some unixes (e.g. Darwin) also point TMPDIR to /really/long/paths we need to create
|
|
||||||
// our own socket in a place closer to '/'.
|
|
||||||
m_socketDir("/tmp/cmake-"),
|
|
||||||
#endif
|
|
||||||
m_sourceDirectory(sourceDirectory), m_buildDirectory(buildDirectory),
|
|
||||||
m_cmakeCommand(cmakeExecutable, {}),
|
|
||||||
m_generator(generator), m_extraGenerator(extraGenerator),
|
|
||||||
m_platform(platform), m_toolset(toolset),
|
|
||||||
m_useExperimental(experimental), m_majorProtocol(major), m_minorProtocol(minor)
|
|
||||||
{
|
|
||||||
QTC_ASSERT(!m_sourceDirectory.isEmpty() && m_sourceDirectory.exists(), return);
|
|
||||||
QTC_ASSERT(!m_buildDirectory.isEmpty() && m_buildDirectory.exists(), return);
|
|
||||||
|
|
||||||
m_connectionTimer.setInterval(100);
|
|
||||||
connect(&m_connectionTimer, &QTimer::timeout, this, &ServerMode::connectToServer);
|
|
||||||
|
|
||||||
m_cmakeProcess = std::make_unique<QtcProcess>();
|
|
||||||
|
|
||||||
m_cmakeProcess->setEnvironment(env);
|
|
||||||
m_cmakeProcess->setWorkingDirectory(buildDirectory.toString());
|
|
||||||
|
|
||||||
#if defined(Q_OS_UNIX)
|
|
||||||
m_socketName = m_socketDir.path() + "/socket";
|
|
||||||
#else
|
|
||||||
m_socketName = QString::fromLatin1("\\\\.\\pipe\\") + QUuid::createUuid().toString();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
connect(m_cmakeProcess.get(), &QtcProcess::started, this, [this]() { m_connectionTimer.start(); });
|
|
||||||
connect(m_cmakeProcess.get(),
|
|
||||||
QOverload<int, QProcess::ExitStatus>::of(&QtcProcess::finished),
|
|
||||||
this, &ServerMode::handleCMakeFinished);
|
|
||||||
|
|
||||||
m_cmakeCommand.addArgs({"-E", "server", "--pipe=" + m_socketName});
|
|
||||||
if (m_useExperimental)
|
|
||||||
m_cmakeCommand.addArg("--experimental");
|
|
||||||
|
|
||||||
qCInfo(cmakeServerMode)
|
|
||||||
<< "Preparing cmake:" << m_cmakeCommand.toUserOutput()
|
|
||||||
<< "in" << m_buildDirectory.toString();
|
|
||||||
m_cmakeProcess->setCommand(m_cmakeCommand);
|
|
||||||
|
|
||||||
// Delay start:
|
|
||||||
QTimer::singleShot(0, this, [this] {
|
|
||||||
emit message(tr("Running \"%1\" in %2.")
|
|
||||||
.arg(m_cmakeCommand.toUserOutput())
|
|
||||||
.arg(m_buildDirectory.toUserOutput()));
|
|
||||||
|
|
||||||
m_cmakeProcess->start();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerMode::~ServerMode()
|
|
||||||
{
|
|
||||||
if (m_cmakeProcess)
|
|
||||||
m_cmakeProcess->disconnect();
|
|
||||||
if (m_cmakeSocket) {
|
|
||||||
m_cmakeSocket->disconnect();
|
|
||||||
m_cmakeSocket->abort();
|
|
||||||
delete(m_cmakeSocket);
|
|
||||||
}
|
|
||||||
m_cmakeSocket = nullptr;
|
|
||||||
Core::Reaper::reap(m_cmakeProcess.release());
|
|
||||||
|
|
||||||
qCDebug(cmakeServerMode) << "Server-Mode closed.";
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerMode::sendRequest(const QString &type, const QVariantMap &extra, const QVariant &cookie)
|
|
||||||
{
|
|
||||||
QTC_ASSERT(m_cmakeSocket, return);
|
|
||||||
++m_requestCounter;
|
|
||||||
|
|
||||||
qCInfo(cmakeServerMode) << "Sending Request" << type << "(" << cookie << ")";
|
|
||||||
|
|
||||||
QVariantMap data = extra;
|
|
||||||
data.insert(TYPE_KEY, type);
|
|
||||||
const QVariant realCookie = cookie.isNull() ? QVariant(m_requestCounter) : cookie;
|
|
||||||
data.insert(COOKIE_KEY, realCookie);
|
|
||||||
m_expectedReplies.push_back({type, realCookie});
|
|
||||||
|
|
||||||
QJsonObject object = QJsonObject::fromVariantMap(data);
|
|
||||||
QJsonDocument document;
|
|
||||||
document.setObject(object);
|
|
||||||
|
|
||||||
const QByteArray rawData = START_MAGIC + document.toJson(QJsonDocument::Compact) + END_MAGIC;
|
|
||||||
qCDebug(cmakeServerMode) << ">>>" << rawData;
|
|
||||||
m_cmakeSocket->write(rawData);
|
|
||||||
m_cmakeSocket->flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ServerMode::isConnected()
|
|
||||||
{
|
|
||||||
return m_cmakeSocket && m_isConnected;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerMode::connectToServer()
|
|
||||||
{
|
|
||||||
QTC_ASSERT(m_cmakeProcess, return);
|
|
||||||
if (m_cmakeSocket)
|
|
||||||
return; // We connected in the meantime...
|
|
||||||
|
|
||||||
static int counter = 0;
|
|
||||||
++counter;
|
|
||||||
|
|
||||||
if (counter > 50) {
|
|
||||||
counter = 0;
|
|
||||||
m_cmakeProcess->disconnect();
|
|
||||||
qCInfo(cmakeServerMode) << "Timeout waiting for pipe" << m_socketName;
|
|
||||||
reportError(tr("Running \"%1\" failed: Timeout waiting for pipe \"%2\".")
|
|
||||||
.arg(m_cmakeCommand.toUserOutput())
|
|
||||||
.arg(m_socketName));
|
|
||||||
|
|
||||||
Core::Reaper::reap(m_cmakeProcess.release());
|
|
||||||
emit disconnected();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTC_ASSERT(!m_cmakeSocket, return);
|
|
||||||
|
|
||||||
auto socket = new QLocalSocket(m_cmakeProcess.get());
|
|
||||||
connect(socket, &QLocalSocket::readyRead, this, &ServerMode::handleRawCMakeServerData);
|
|
||||||
|
|
||||||
constexpr void (QLocalSocket::*LocalSocketErrorFunction)(QLocalSocket::LocalSocketError)
|
|
||||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
|
|
||||||
= &QLocalSocket::error;
|
|
||||||
#else
|
|
||||||
= &QLocalSocket::errorOccurred;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
connect(socket, LocalSocketErrorFunction, this, [this, socket]() {
|
|
||||||
reportError(socket->errorString());
|
|
||||||
m_cmakeSocket = nullptr;
|
|
||||||
socket->disconnect();
|
|
||||||
socket->deleteLater();
|
|
||||||
});
|
|
||||||
connect(socket, &QLocalSocket::connected, this, [this, socket]() { m_cmakeSocket = socket; });
|
|
||||||
connect(socket, &QLocalSocket::disconnected, this, [this, socket]() {
|
|
||||||
if (m_cmakeSocket)
|
|
||||||
emit disconnected();
|
|
||||||
m_cmakeSocket = nullptr;
|
|
||||||
socket->disconnect();
|
|
||||||
socket->deleteLater();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket->connectToServer(m_socketName);
|
|
||||||
m_connectionTimer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerMode::handleCMakeFinished(int code, QProcess::ExitStatus status)
|
|
||||||
{
|
|
||||||
qCInfo(cmakeServerMode) << "CMake has finished" << code << status;
|
|
||||||
QString msg;
|
|
||||||
if (status != QProcess::NormalExit)
|
|
||||||
msg = tr("CMake process \"%1\" crashed.").arg(m_cmakeCommand.toUserOutput());
|
|
||||||
else if (code != 0)
|
|
||||||
msg = tr("CMake process \"%1\" quit with exit code %2.").arg(m_cmakeCommand.toUserOutput()).arg(code);
|
|
||||||
|
|
||||||
if (!msg.isEmpty()) {
|
|
||||||
reportError(msg);
|
|
||||||
} else {
|
|
||||||
emit message(tr("CMake process \"%1\" quit normally.").arg(m_cmakeCommand.toUserOutput()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_cmakeSocket) {
|
|
||||||
m_cmakeSocket->disconnect();
|
|
||||||
delete m_cmakeSocket;
|
|
||||||
m_cmakeSocket = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!HostOsInfo::isWindowsHost())
|
|
||||||
QFile::remove(m_socketName);
|
|
||||||
|
|
||||||
emit disconnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerMode::handleRawCMakeServerData()
|
|
||||||
{
|
|
||||||
const static QByteArray startNeedle(START_MAGIC);
|
|
||||||
const static QByteArray endNeedle(END_MAGIC);
|
|
||||||
|
|
||||||
if (!m_cmakeSocket) // might happen during shutdown
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_buffer.append(m_cmakeSocket->readAll());
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const int startPos = m_buffer.indexOf(startNeedle);
|
|
||||||
if (startPos >= 0) {
|
|
||||||
const int afterStartNeedle = startPos + startNeedle.count();
|
|
||||||
const int endPos = m_buffer.indexOf(endNeedle, afterStartNeedle);
|
|
||||||
if (endPos > afterStartNeedle) {
|
|
||||||
// Process JSON, remove junk and JSON-part, continue to loop with shorter buffer
|
|
||||||
parseBuffer(m_buffer.mid(afterStartNeedle, endPos - afterStartNeedle));
|
|
||||||
m_buffer.remove(0, endPos + endNeedle.count());
|
|
||||||
} else {
|
|
||||||
// Remove junk up to the start needle and break out of the loop
|
|
||||||
if (startPos > 0)
|
|
||||||
m_buffer.remove(0, startPos);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Keep at last startNeedle.count() characters (as that might be a
|
|
||||||
// partial startNeedle), break out of the loop
|
|
||||||
if (m_buffer.count() > startNeedle.count())
|
|
||||||
m_buffer.remove(0, m_buffer.count() - startNeedle.count());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerMode::parseBuffer(const QByteArray &buffer)
|
|
||||||
{
|
|
||||||
qCDebug(cmakeServerMode) << "<<<" << buffer;
|
|
||||||
QJsonDocument document = QJsonDocument::fromJson(buffer);
|
|
||||||
if (document.isNull()) {
|
|
||||||
reportError(tr("Failed to parse JSON from CMake server."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QJsonObject rootObject = document.object();
|
|
||||||
if (rootObject.isEmpty()) {
|
|
||||||
reportError(tr("JSON data from CMake server was not a JSON object."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
parseJson(rootObject.toVariantMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerMode::parseJson(const QVariantMap &data)
|
|
||||||
{
|
|
||||||
QString type = data.value(TYPE_KEY).toString();
|
|
||||||
if (type == "hello") {
|
|
||||||
qCInfo(cmakeServerMode) << "Got \"hello\" message.";
|
|
||||||
if (m_gotHello) {
|
|
||||||
reportError(tr("Unexpected hello received from CMake server."));
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
handleHello(data);
|
|
||||||
m_gotHello = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!m_gotHello && type != ERROR_TYPE) {
|
|
||||||
reportError(tr("Unexpected type \"%1\" received while waiting for \"hello\".").arg(type));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == "reply") {
|
|
||||||
if (m_expectedReplies.empty()) {
|
|
||||||
reportError(tr("Received a reply even though no request is open."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const QString replyTo = data.value(IN_REPLY_TO_KEY).toString();
|
|
||||||
const QVariant cookie = data.value(COOKIE_KEY);
|
|
||||||
qCInfo(cmakeServerMode) << "Got \"reply\" message." << replyTo << "(" << cookie << ")";
|
|
||||||
|
|
||||||
const auto expected = m_expectedReplies.begin();
|
|
||||||
if (expected->type != replyTo) {
|
|
||||||
reportError(tr("Received a reply to a request of type \"%1\", when a request of type \"%2\" was sent.")
|
|
||||||
.arg(replyTo).arg(expected->type));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (expected->cookie != cookie) {
|
|
||||||
reportError(tr("Received a reply with cookie \"%1\", when \"%2\" was expected.")
|
|
||||||
.arg(cookie.toString()).arg(expected->cookie.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_expectedReplies.erase(expected);
|
|
||||||
if (replyTo != HANDSHAKE_TYPE)
|
|
||||||
emit cmakeReply(data, replyTo, cookie);
|
|
||||||
else {
|
|
||||||
m_isConnected = true;
|
|
||||||
emit connected();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (type == "error") {
|
|
||||||
if (m_expectedReplies.empty()) {
|
|
||||||
reportError(tr("An error was reported even though no request is open."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const QString replyTo = data.value(IN_REPLY_TO_KEY).toString();
|
|
||||||
const QVariant cookie = data.value(COOKIE_KEY);
|
|
||||||
qCInfo(cmakeServerMode) << "Got \"error\" message." << replyTo << "(" << cookie << ")";
|
|
||||||
|
|
||||||
const auto expected = m_expectedReplies.begin();
|
|
||||||
if (expected->type != replyTo) {
|
|
||||||
reportError(tr("Received an error in response to a request of type \"%1\", when a request of type \"%2\" was sent.")
|
|
||||||
.arg(replyTo).arg(expected->type));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (expected->cookie != cookie) {
|
|
||||||
reportError(tr("Received an error with cookie \"%1\", when \"%2\" was expected.")
|
|
||||||
.arg(cookie.toString()).arg(expected->cookie.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_expectedReplies.erase(expected);
|
|
||||||
|
|
||||||
emit cmakeError(data.value("errorMessage").toString(), replyTo, cookie);
|
|
||||||
if (replyTo == HANDSHAKE_TYPE) {
|
|
||||||
Core::Reaper::reap(m_cmakeProcess.release());
|
|
||||||
m_cmakeSocket->disconnect();
|
|
||||||
m_cmakeSocket->disconnectFromServer();
|
|
||||||
m_cmakeSocket = nullptr;
|
|
||||||
emit disconnected();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (type == "message") {
|
|
||||||
const QString replyTo = data.value(IN_REPLY_TO_KEY).toString();
|
|
||||||
const QVariant cookie = data.value(COOKIE_KEY);
|
|
||||||
qCInfo(cmakeServerMode) << "Got \"message\" message." << replyTo << "(" << cookie << ")";
|
|
||||||
|
|
||||||
const auto expected = m_expectedReplies.begin();
|
|
||||||
if (expected->type != replyTo) {
|
|
||||||
reportError(tr("Received a message in response to a request of type \"%1\", when a request of type \"%2\" was sent.")
|
|
||||||
.arg(replyTo).arg(expected->type));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (expected->cookie != cookie) {
|
|
||||||
reportError(tr("Received a message with cookie \"%1\", when \"%2\" was expected.")
|
|
||||||
.arg(cookie.toString()).arg(expected->cookie.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit cmakeMessage(data.value("message").toString(), replyTo, cookie);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (type == "progress") {
|
|
||||||
const QString replyTo = data.value(IN_REPLY_TO_KEY).toString();
|
|
||||||
const QVariant cookie = data.value(COOKIE_KEY);
|
|
||||||
qCInfo(cmakeServerMode) << "Got \"progress\" message." << replyTo << "(" << cookie << ")";
|
|
||||||
|
|
||||||
const auto expected = m_expectedReplies.begin();
|
|
||||||
if (expected->type != replyTo) {
|
|
||||||
reportError(tr("Received a progress report in response to a request of type \"%1\", when a request of type \"%2\" was sent.")
|
|
||||||
.arg(replyTo).arg(expected->type));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (expected->cookie != cookie) {
|
|
||||||
reportError(tr("Received a progress report with cookie \"%1\", when \"%2\" was expected.")
|
|
||||||
.arg(cookie.toString()).arg(expected->cookie.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit cmakeProgress(data.value("progressMinimum").toInt(),
|
|
||||||
data.value("progressCurrent").toInt(),
|
|
||||||
data.value("progressMaximum").toInt(), replyTo, cookie);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (type == "signal") {
|
|
||||||
const QString replyTo = data.value(IN_REPLY_TO_KEY).toString();
|
|
||||||
const QString cookie = data.value(COOKIE_KEY).toString();
|
|
||||||
const QString name = data.value(NAME_KEY).toString();
|
|
||||||
qCInfo(cmakeServerMode) << "Got \"signal\" message." << name << replyTo << "(" << cookie << ")";
|
|
||||||
|
|
||||||
if (name.isEmpty()) {
|
|
||||||
reportError(tr("Received a signal without a name."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!replyTo.isEmpty() || isValid(cookie)) {
|
|
||||||
reportError(tr("Received a signal in reply to a request."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit cmakeSignal(name, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reportError("Got a message of an unknown type.");
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerMode::handleHello(const QVariantMap &data)
|
|
||||||
{
|
|
||||||
Q_UNUSED(data)
|
|
||||||
QVariantMap extra;
|
|
||||||
QVariantMap version;
|
|
||||||
version.insert("major", m_majorProtocol);
|
|
||||||
if (m_minorProtocol >= 0)
|
|
||||||
version.insert("minor", m_minorProtocol);
|
|
||||||
extra.insert("protocolVersion", version);
|
|
||||||
extra.insert("sourceDirectory", m_sourceDirectory.toString());
|
|
||||||
extra.insert("buildDirectory", m_buildDirectory.toString());
|
|
||||||
extra.insert("generator", m_generator);
|
|
||||||
if (!m_platform.isEmpty())
|
|
||||||
extra.insert("platform", m_platform);
|
|
||||||
if (!m_toolset.isEmpty())
|
|
||||||
extra.insert("toolset", m_toolset);
|
|
||||||
if (!m_extraGenerator.isEmpty())
|
|
||||||
extra.insert("extraGenerator", m_extraGenerator);
|
|
||||||
if (!m_platform.isEmpty())
|
|
||||||
extra.insert("platform", m_platform);
|
|
||||||
if (!m_toolset.isEmpty())
|
|
||||||
extra.insert("toolset", m_toolset);
|
|
||||||
|
|
||||||
sendRequest(HANDSHAKE_TYPE, extra);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerMode::reportError(const QString &msg)
|
|
||||||
{
|
|
||||||
qCWarning(cmakeServerMode) << "Report Error:" << msg;
|
|
||||||
emit message(msg);
|
|
||||||
emit errorOccurred(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Internal
|
|
||||||
} // namespace CMakeProjectManager
|
|
@@ -1,127 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
**
|
|
||||||
** Copyright (C) 2016 The Qt Company Ltd.
|
|
||||||
** Contact: https://www.qt.io/licensing/
|
|
||||||
**
|
|
||||||
** This file is part of Qt Creator.
|
|
||||||
**
|
|
||||||
** Commercial License Usage
|
|
||||||
** Licensees holding valid commercial Qt licenses may use this file in
|
|
||||||
** accordance with the commercial license agreement provided with the
|
|
||||||
** Software or, alternatively, in accordance with the terms contained in
|
|
||||||
** a written agreement between you and The Qt Company. For licensing terms
|
|
||||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
||||||
** information use the contact form at https://www.qt.io/contact-us.
|
|
||||||
**
|
|
||||||
** GNU General Public License Usage
|
|
||||||
** Alternatively, this file may be used under the terms of the GNU
|
|
||||||
** General Public License version 3 as published by the Free Software
|
|
||||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
|
||||||
** included in the packaging of this file. Please review the following
|
|
||||||
** information to ensure the GNU General Public License requirements will
|
|
||||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
|
||||||
**
|
|
||||||
****************************************************************************/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <utils/environment.h>
|
|
||||||
#include <utils/fileutils.h>
|
|
||||||
#include <utils/qtcprocess.h>
|
|
||||||
|
|
||||||
#include <QLoggingCategory>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QTemporaryDir>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QVariantMap>
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
QT_FORWARD_DECLARE_CLASS(QLocalSocket);
|
|
||||||
|
|
||||||
namespace Utils { class QtcProcess; }
|
|
||||||
|
|
||||||
namespace CMakeProjectManager {
|
|
||||||
namespace Internal {
|
|
||||||
|
|
||||||
class ServerMode final : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
ServerMode(const Utils::Environment &env,
|
|
||||||
const Utils::FilePath &sourceDirectory, const Utils::FilePath &buildDirectory,
|
|
||||||
const Utils::FilePath &cmakeExecutable,
|
|
||||||
const QString &generator, const QString &extraGenerator,
|
|
||||||
const QString &platform, const QString &toolset,
|
|
||||||
bool experimental, int major, int minor = -1,
|
|
||||||
QObject *parent = nullptr);
|
|
||||||
~ServerMode() final;
|
|
||||||
|
|
||||||
void sendRequest(const QString &type, const QVariantMap &extra = QVariantMap(),
|
|
||||||
const QVariant &cookie = QVariant());
|
|
||||||
|
|
||||||
bool isConnected();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void connected();
|
|
||||||
void disconnected();
|
|
||||||
void message(const QString &msg);
|
|
||||||
void errorOccurred(const QString &msg);
|
|
||||||
|
|
||||||
// Forward stuff from the server
|
|
||||||
void cmakeReply(const QVariantMap &data, const QString &inResponseTo, const QVariant &cookie);
|
|
||||||
void cmakeError(const QString &errorMessage, const QString &inResponseTo, const QVariant &cookie);
|
|
||||||
void cmakeMessage(const QString &message, const QString &inResponseTo, const QVariant &cookie);
|
|
||||||
void cmakeProgress(int min, int cur, int max, const QString &inResponseTo, const QVariant &cookie);
|
|
||||||
void cmakeSignal(const QString &name, const QVariantMap &data);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void connectToServer();
|
|
||||||
void handleCMakeFinished(int code, QProcess::ExitStatus status);
|
|
||||||
|
|
||||||
void handleRawCMakeServerData();
|
|
||||||
void parseBuffer(const QByteArray &buffer);
|
|
||||||
void parseJson(const QVariantMap &data);
|
|
||||||
|
|
||||||
void handleHello(const QVariantMap &data);
|
|
||||||
|
|
||||||
void reportError(const QString &msg);
|
|
||||||
|
|
||||||
#if defined(Q_OS_UNIX)
|
|
||||||
QTemporaryDir m_socketDir;
|
|
||||||
#endif
|
|
||||||
std::unique_ptr<Utils::QtcProcess> m_cmakeProcess;
|
|
||||||
QLocalSocket *m_cmakeSocket = nullptr;
|
|
||||||
QTimer m_connectionTimer;
|
|
||||||
|
|
||||||
Utils::FilePath m_sourceDirectory;
|
|
||||||
Utils::FilePath m_buildDirectory;
|
|
||||||
Utils::CommandLine m_cmakeCommand;
|
|
||||||
|
|
||||||
QByteArray m_buffer;
|
|
||||||
|
|
||||||
struct ExpectedReply {
|
|
||||||
QString type;
|
|
||||||
QVariant cookie;
|
|
||||||
};
|
|
||||||
std::vector<ExpectedReply> m_expectedReplies;
|
|
||||||
|
|
||||||
const QString m_generator;
|
|
||||||
const QString m_extraGenerator;
|
|
||||||
const QString m_platform;
|
|
||||||
const QString m_toolset;
|
|
||||||
QString m_socketName;
|
|
||||||
const bool m_useExperimental;
|
|
||||||
bool m_gotHello = false;
|
|
||||||
bool m_isConnected = false;
|
|
||||||
const int m_majorProtocol = -1;
|
|
||||||
const int m_minorProtocol = -1;
|
|
||||||
|
|
||||||
int m_requestCounter = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Internal
|
|
||||||
} // namespace CMakeProjectManager
|
|
||||||
|
|
||||||
Q_DECLARE_LOGGING_CATEGORY(cmakeServerMode);
|
|
@@ -1,915 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
**
|
|
||||||
** Copyright (C) 2016 The Qt Company Ltd.
|
|
||||||
** Contact: https://www.qt.io/licensing/
|
|
||||||
**
|
|
||||||
** This file is part of Qt Creator.
|
|
||||||
**
|
|
||||||
** Commercial License Usage
|
|
||||||
** Licensees holding valid commercial Qt licenses may use this file in
|
|
||||||
** accordance with the commercial license agreement provided with the
|
|
||||||
** Software or, alternatively, in accordance with the terms contained in
|
|
||||||
** a written agreement between you and The Qt Company. For licensing terms
|
|
||||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
||||||
** information use the contact form at https://www.qt.io/contact-us.
|
|
||||||
**
|
|
||||||
** GNU General Public License Usage
|
|
||||||
** Alternatively, this file may be used under the terms of the GNU
|
|
||||||
** General Public License version 3 as published by the Free Software
|
|
||||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
|
||||||
** included in the packaging of this file. Please review the following
|
|
||||||
** information to ensure the GNU General Public License requirements will
|
|
||||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
|
||||||
**
|
|
||||||
****************************************************************************/
|
|
||||||
|
|
||||||
#include "servermodereader.h"
|
|
||||||
|
|
||||||
#include "cmakeparser.h"
|
|
||||||
#include "projecttreehelper.h"
|
|
||||||
|
|
||||||
#include <coreplugin/messagemanager.h>
|
|
||||||
#include <coreplugin/progressmanager/progressmanager.h>
|
|
||||||
#include <projectexplorer/task.h>
|
|
||||||
#include <projectexplorer/taskhub.h>
|
|
||||||
|
|
||||||
#include <utils/algorithm.h>
|
|
||||||
|
|
||||||
using namespace ProjectExplorer;
|
|
||||||
using namespace Utils;
|
|
||||||
|
|
||||||
namespace CMakeProjectManager {
|
|
||||||
namespace Internal {
|
|
||||||
|
|
||||||
const char CACHE_TYPE[] = "cache";
|
|
||||||
const char CODEMODEL_TYPE[] = "codemodel";
|
|
||||||
const char CONFIGURE_TYPE[] = "configure";
|
|
||||||
const char CMAKE_INPUTS_TYPE[] = "cmakeInputs";
|
|
||||||
const char COMPUTE_TYPE[] = "compute";
|
|
||||||
|
|
||||||
const char BACKTRACE_KEY[] = "backtrace";
|
|
||||||
const char LINE_KEY[] = "line";
|
|
||||||
const char NAME_KEY[] = "name";
|
|
||||||
const char PATH_KEY[] = "path";
|
|
||||||
const char SOURCE_DIRECTORY_KEY[] = "sourceDirectory";
|
|
||||||
const char SOURCES_KEY[] = "sources";
|
|
||||||
|
|
||||||
const int MAX_PROGRESS = 1400;
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------
|
|
||||||
// ServerModeReader:
|
|
||||||
// --------------------------------------------------------------------
|
|
||||||
|
|
||||||
ServerModeReader::ServerModeReader()
|
|
||||||
{
|
|
||||||
m_cmakeParser = new CMakeParser;
|
|
||||||
m_parser.addLineParser(m_cmakeParser);
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerModeReader::~ServerModeReader()
|
|
||||||
{
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::setParameters(const BuildDirParameters &p)
|
|
||||||
{
|
|
||||||
CMakeTool *cmake = p.cmakeTool();
|
|
||||||
QTC_ASSERT(cmake, return);
|
|
||||||
|
|
||||||
m_parameters = p;
|
|
||||||
m_cmakeParser->setSourceDirectory(m_parameters.sourceDirectory.toString());
|
|
||||||
createNewServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ServerModeReader::isCompatible(const BuildDirParameters &p)
|
|
||||||
{
|
|
||||||
CMakeTool *newCmake = p.cmakeTool();
|
|
||||||
if (newCmake->readerType() != CMakeTool::FileApi)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
CMakeTool *oldCmake = m_parameters.cmakeTool();
|
|
||||||
if (!newCmake || !oldCmake)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Server mode connection got lost, reset...
|
|
||||||
if (!oldCmake && oldCmake->cmakeExecutable().isEmpty() && !m_cmakeServer)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return newCmake->hasServerMode()
|
|
||||||
&& newCmake->cmakeExecutable() == oldCmake->cmakeExecutable()
|
|
||||||
&& p.environment == m_parameters.environment
|
|
||||||
&& p.generator == m_parameters.generator
|
|
||||||
&& p.extraGenerator == m_parameters.extraGenerator
|
|
||||||
&& p.platform == m_parameters.platform
|
|
||||||
&& p.toolset == m_parameters.toolset
|
|
||||||
&& p.sourceDirectory == m_parameters.sourceDirectory
|
|
||||||
&& p.workDirectory == m_parameters.workDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::resetData()
|
|
||||||
{
|
|
||||||
m_cmakeConfiguration.clear();
|
|
||||||
// m_cmakeFiles: Keep these!
|
|
||||||
m_cmakeInputsFileNodes.clear();
|
|
||||||
qDeleteAll(m_projects); // Also deletes targets and filegroups that are its children!
|
|
||||||
m_projects.clear();
|
|
||||||
m_targets.clear();
|
|
||||||
m_fileGroups.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::parse(bool forceCMakeRun, bool forceConfiguration)
|
|
||||||
{
|
|
||||||
emit configurationStarted();
|
|
||||||
|
|
||||||
QTC_ASSERT(m_cmakeServer, return);
|
|
||||||
QVariantMap extra;
|
|
||||||
|
|
||||||
bool delayConfigurationRun = false;
|
|
||||||
if (forceCMakeRun && m_cmakeServer->isConnected()) {
|
|
||||||
createNewServer();
|
|
||||||
delayConfigurationRun = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceConfiguration) {
|
|
||||||
QStringList cacheArguments = transform(m_parameters.configuration,
|
|
||||||
[this](const CMakeConfigItem &i) {
|
|
||||||
return i.toArgument(m_parameters.expander);
|
|
||||||
});
|
|
||||||
Core::MessageManager::write(tr("Starting to parse CMake project, using: \"%1\".")
|
|
||||||
.arg(cacheArguments.join("\", \"")));
|
|
||||||
cacheArguments.prepend(QString()); // Work around a bug in CMake 3.7.0 and 3.7.1 where
|
|
||||||
// the first argument gets lost!
|
|
||||||
extra.insert("cacheArguments", QVariant(cacheArguments));
|
|
||||||
} else {
|
|
||||||
Core::MessageManager::write(tr("Starting to parse CMake project."));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_future.reset(new QFutureInterface<void>());
|
|
||||||
m_future->setProgressRange(0, MAX_PROGRESS);
|
|
||||||
m_progressStepMinimum = 0;
|
|
||||||
m_progressStepMaximum = 1000;
|
|
||||||
Core::ProgressManager::addTask(m_future->future(),
|
|
||||||
tr("Configuring \"%1\"").arg(m_parameters.projectName),
|
|
||||||
"CMake.Configure");
|
|
||||||
|
|
||||||
if (!delayConfigurationRun) {
|
|
||||||
sendConfigureRequest(extra);
|
|
||||||
} else {
|
|
||||||
m_delayedConfigurationData = extra;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::stop()
|
|
||||||
{
|
|
||||||
if (m_future) {
|
|
||||||
m_future->reportCanceled();
|
|
||||||
m_future->reportFinished();
|
|
||||||
m_future.reset();
|
|
||||||
}
|
|
||||||
m_parser.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ServerModeReader::isParsing() const
|
|
||||||
{
|
|
||||||
return static_cast<bool>(m_future);
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<CMakeBuildTarget> ServerModeReader::takeBuildTargets(QString &errorMessage)
|
|
||||||
{
|
|
||||||
Q_UNUSED(errorMessage)
|
|
||||||
QDir topSourceDir(m_parameters.sourceDirectory.toString());
|
|
||||||
const QList<CMakeBuildTarget> result
|
|
||||||
= transform(m_targets, [&topSourceDir](const Target *t) -> CMakeBuildTarget {
|
|
||||||
CMakeBuildTarget ct;
|
|
||||||
ct.title = t->name;
|
|
||||||
ct.executable = t->artifacts.isEmpty() ? FilePath() : t->artifacts.at(0);
|
|
||||||
TargetType type = UtilityType;
|
|
||||||
if (t->type == "EXECUTABLE")
|
|
||||||
type = ExecutableType;
|
|
||||||
else if (t->type == "STATIC_LIBRARY")
|
|
||||||
type = StaticLibraryType;
|
|
||||||
else if (t->type == "OBJECT_LIBRARY")
|
|
||||||
type = ObjectLibraryType;
|
|
||||||
else if (t->type == "MODULE_LIBRARY" || t->type == "SHARED_LIBRARY"
|
|
||||||
|| t->type == "INTERFACE_LIBRARY")
|
|
||||||
type = DynamicLibraryType;
|
|
||||||
else
|
|
||||||
type = UtilityType;
|
|
||||||
ct.targetType = type;
|
|
||||||
if (t->artifacts.isEmpty()) {
|
|
||||||
ct.workingDirectory = t->buildDirectory;
|
|
||||||
} else {
|
|
||||||
ct.workingDirectory = Utils::FilePath::fromString(
|
|
||||||
t->artifacts.at(0).toFileInfo().absolutePath());
|
|
||||||
}
|
|
||||||
ct.sourceDirectory = FilePath::fromString(
|
|
||||||
QDir::cleanPath(topSourceDir.absoluteFilePath(t->sourceDirectory.toString())));
|
|
||||||
return ct;
|
|
||||||
});
|
|
||||||
m_targets.clear();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
CMakeConfig ServerModeReader::takeParsedConfiguration(QString &errorMessage)
|
|
||||||
{
|
|
||||||
Q_UNUSED(errorMessage)
|
|
||||||
CMakeConfig config = m_cmakeConfiguration;
|
|
||||||
m_cmakeConfiguration.clear();
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<CMakeProjectNode> ServerModeReader::generateProjectTree(const QList<const FileNode *> &allFiles,
|
|
||||||
QString &errorMessage)
|
|
||||||
{
|
|
||||||
Q_UNUSED(errorMessage)
|
|
||||||
auto root = std::make_unique<CMakeProjectNode>(m_parameters.sourceDirectory);
|
|
||||||
|
|
||||||
// Split up cmake inputs into useful chunks:
|
|
||||||
std::vector<std::unique_ptr<FileNode>> cmakeFilesSource;
|
|
||||||
std::vector<std::unique_ptr<FileNode>> cmakeFilesBuild;
|
|
||||||
std::vector<std::unique_ptr<FileNode>> cmakeFilesOther;
|
|
||||||
std::vector<std::unique_ptr<FileNode>> cmakeLists;
|
|
||||||
|
|
||||||
for (std::unique_ptr<FileNode> &fn : m_cmakeInputsFileNodes) {
|
|
||||||
const FilePath path = fn->filePath();
|
|
||||||
if (path.fileName().compare("CMakeLists.txt", HostOsInfo::fileNameCaseSensitivity()) == 0)
|
|
||||||
cmakeLists.emplace_back(std::move(fn));
|
|
||||||
else if (path.isChildOf(m_parameters.workDirectory))
|
|
||||||
cmakeFilesBuild.emplace_back(std::move(fn));
|
|
||||||
else if (path.isChildOf(m_parameters.sourceDirectory))
|
|
||||||
cmakeFilesSource.emplace_back(std::move(fn));
|
|
||||||
else
|
|
||||||
cmakeFilesOther.emplace_back(std::move(fn));
|
|
||||||
}
|
|
||||||
m_cmakeInputsFileNodes.clear(); // Clean out, they are not going to be used anymore!
|
|
||||||
|
|
||||||
const Project *topLevel = Utils::findOrDefault(m_projects, [this](const Project *p) {
|
|
||||||
return m_parameters.sourceDirectory == p->sourceDirectory;
|
|
||||||
});
|
|
||||||
if (topLevel)
|
|
||||||
root->setDisplayName(topLevel->name);
|
|
||||||
|
|
||||||
QHash<Utils::FilePath, ProjectNode *> cmakeListsNodes = addCMakeLists(root.get(),
|
|
||||||
std::move(cmakeLists));
|
|
||||||
QSet<FilePath> knownHeaders;
|
|
||||||
addProjects(cmakeListsNodes, m_projects, knownHeaders);
|
|
||||||
|
|
||||||
addHeaderNodes(root.get(), knownHeaders, allFiles);
|
|
||||||
|
|
||||||
if (cmakeFilesSource.size() > 0 || cmakeFilesBuild.size() > 0 || cmakeFilesOther.size() > 0)
|
|
||||||
addCMakeInputs(root.get(),
|
|
||||||
m_parameters.sourceDirectory,
|
|
||||||
m_parameters.workDirectory,
|
|
||||||
std::move(cmakeFilesSource),
|
|
||||||
std::move(cmakeFilesBuild),
|
|
||||||
std::move(cmakeFilesOther));
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
RawProjectParts ServerModeReader::createRawProjectParts(QString &errorMessage)
|
|
||||||
{
|
|
||||||
Q_UNUSED(errorMessage)
|
|
||||||
RawProjectParts rpps;
|
|
||||||
|
|
||||||
int counter = 0;
|
|
||||||
for (const FileGroup *fg : qAsConst(m_fileGroups)) {
|
|
||||||
// CMake users worked around Creator's inability of listing header files by creating
|
|
||||||
// custom targets with all the header files. This target breaks the code model, so
|
|
||||||
// keep quiet about it:-)
|
|
||||||
if (fg->macros.isEmpty()
|
|
||||||
&& fg->includePaths.isEmpty()
|
|
||||||
&& !fg->isGenerated
|
|
||||||
&& Utils::allOf(fg->sources, [](const Utils::FilePath &source) {
|
|
||||||
return Node::fileTypeForFileName(source) == FileType::Header;
|
|
||||||
})) {
|
|
||||||
qWarning() << "Not reporting all-header file group of target" << fg->target << "to code model.";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
++counter;
|
|
||||||
const QStringList flags = QtcProcess::splitArgs(fg->compileFlags);
|
|
||||||
const QStringList includes = transform(fg->includePaths, [](const IncludePath *ip) { return ip->path.toString(); });
|
|
||||||
|
|
||||||
RawProjectPart rpp;
|
|
||||||
rpp.setProjectFileLocation(fg->target->sourceDirectory.toString() + "/CMakeLists.txt");
|
|
||||||
rpp.setBuildSystemTarget(fg->target->name);
|
|
||||||
rpp.setDisplayName(fg->target->name + QString::number(counter));
|
|
||||||
rpp.setMacros(fg->macros);
|
|
||||||
rpp.setIncludePaths(includes);
|
|
||||||
|
|
||||||
RawProjectPartFlags cProjectFlags;
|
|
||||||
cProjectFlags.commandLineFlags = flags;
|
|
||||||
rpp.setFlagsForC(cProjectFlags);
|
|
||||||
|
|
||||||
RawProjectPartFlags cxxProjectFlags;
|
|
||||||
cxxProjectFlags.commandLineFlags = flags;
|
|
||||||
rpp.setFlagsForCxx(cxxProjectFlags);
|
|
||||||
|
|
||||||
rpp.setFiles(transform(fg->sources, &FilePath::toString));
|
|
||||||
|
|
||||||
const bool isExecutable = fg->target->type == "EXECUTABLE";
|
|
||||||
rpp.setBuildTargetType(isExecutable ? ProjectExplorer::BuildTargetType::Executable
|
|
||||||
: ProjectExplorer::BuildTargetType::Library);
|
|
||||||
rpps.append(rpp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rpps;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::createNewServer()
|
|
||||||
{
|
|
||||||
QTC_ASSERT(m_parameters.cmakeTool(), return);
|
|
||||||
m_cmakeServer
|
|
||||||
= std::make_unique<ServerMode>(
|
|
||||||
m_parameters.environment,
|
|
||||||
m_parameters.sourceDirectory, m_parameters.workDirectory,
|
|
||||||
m_parameters.cmakeTool()->cmakeExecutable(),
|
|
||||||
m_parameters.generator,
|
|
||||||
m_parameters.extraGenerator,
|
|
||||||
m_parameters.platform, m_parameters.toolset,
|
|
||||||
true, 1);
|
|
||||||
|
|
||||||
connect(m_cmakeServer.get(), &ServerMode::errorOccurred,
|
|
||||||
this, &ServerModeReader::errorOccurred);
|
|
||||||
connect(m_cmakeServer.get(), &ServerMode::cmakeReply,
|
|
||||||
this, &ServerModeReader::handleReply);
|
|
||||||
connect(m_cmakeServer.get(), &ServerMode::cmakeError,
|
|
||||||
this, &ServerModeReader::handleError);
|
|
||||||
connect(m_cmakeServer.get(), &ServerMode::cmakeProgress,
|
|
||||||
this, &ServerModeReader::handleProgress);
|
|
||||||
connect(m_cmakeServer.get(), &ServerMode::cmakeSignal,
|
|
||||||
this, &ServerModeReader::handleSignal);
|
|
||||||
connect(m_cmakeServer.get(), &ServerMode::cmakeMessage, [this](const QString &m) {
|
|
||||||
const QStringList lines = m.split('\n');
|
|
||||||
for (const QString &l : lines) {
|
|
||||||
m_parser.appendMessage(l, StdErrFormat);
|
|
||||||
Core::MessageManager::write(l);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
connect(m_cmakeServer.get(), &ServerMode::message,
|
|
||||||
this, [](const QString &m) { Core::MessageManager::write(m); });
|
|
||||||
connect(m_cmakeServer.get(),
|
|
||||||
&ServerMode::connected,
|
|
||||||
this,
|
|
||||||
&ServerModeReader::handleServerConnected,
|
|
||||||
Qt::QueuedConnection); // Delay
|
|
||||||
connect(m_cmakeServer.get(), &ServerMode::disconnected,
|
|
||||||
this, [this]() {
|
|
||||||
stop();
|
|
||||||
Core::MessageManager::write(tr("Parsing of CMake project failed: Connection to CMake server lost."));
|
|
||||||
m_cmakeServer.reset();
|
|
||||||
}, Qt::QueuedConnection); // Delay
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::handleReply(const QVariantMap &data, const QString &inReplyTo)
|
|
||||||
{
|
|
||||||
if (!m_delayedErrorMessage.isEmpty()) {
|
|
||||||
// Handle reply to cache after error:
|
|
||||||
if (inReplyTo == CACHE_TYPE)
|
|
||||||
extractCacheData(data);
|
|
||||||
reportError();
|
|
||||||
} else {
|
|
||||||
// No error yet:
|
|
||||||
if (inReplyTo == CONFIGURE_TYPE) {
|
|
||||||
m_cmakeServer->sendRequest(COMPUTE_TYPE);
|
|
||||||
if (m_future)
|
|
||||||
m_future->setProgressValue(1000);
|
|
||||||
m_progressStepMinimum = m_progressStepMaximum;
|
|
||||||
m_progressStepMaximum = 1100;
|
|
||||||
} else if (inReplyTo == COMPUTE_TYPE) {
|
|
||||||
m_cmakeServer->sendRequest(CODEMODEL_TYPE);
|
|
||||||
if (m_future)
|
|
||||||
m_future->setProgressValue(1100);
|
|
||||||
m_progressStepMinimum = m_progressStepMaximum;
|
|
||||||
m_progressStepMaximum = 1200;
|
|
||||||
} else if (inReplyTo == CODEMODEL_TYPE) {
|
|
||||||
extractCodeModelData(data);
|
|
||||||
m_cmakeServer->sendRequest(CMAKE_INPUTS_TYPE);
|
|
||||||
if (m_future)
|
|
||||||
m_future->setProgressValue(1200);
|
|
||||||
m_progressStepMinimum = m_progressStepMaximum;
|
|
||||||
m_progressStepMaximum = 1300;
|
|
||||||
} else if (inReplyTo == CMAKE_INPUTS_TYPE) {
|
|
||||||
extractCMakeInputsData(data);
|
|
||||||
m_cmakeServer->sendRequest(CACHE_TYPE);
|
|
||||||
if (m_future)
|
|
||||||
m_future->setProgressValue(1300);
|
|
||||||
m_progressStepMinimum = m_progressStepMaximum;
|
|
||||||
m_progressStepMaximum = 1400;
|
|
||||||
} else if (inReplyTo == CACHE_TYPE) {
|
|
||||||
extractCacheData(data);
|
|
||||||
if (m_future) {
|
|
||||||
m_future->setProgressValue(MAX_PROGRESS);
|
|
||||||
m_future->reportFinished();
|
|
||||||
m_future.reset();
|
|
||||||
}
|
|
||||||
Core::MessageManager::write(tr("CMake Project was parsed successfully."));
|
|
||||||
emit dataAvailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::handleError(const QString &message)
|
|
||||||
{
|
|
||||||
TaskHub::addTask(BuildSystemTask(Task::Error, message));
|
|
||||||
|
|
||||||
if (!m_delayedErrorMessage.isEmpty()) {
|
|
||||||
reportError();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_delayedErrorMessage = message;
|
|
||||||
|
|
||||||
// Always try to read CMakeCache, even after an error!
|
|
||||||
m_cmakeServer->sendRequest(CACHE_TYPE);
|
|
||||||
if (m_future)
|
|
||||||
m_future->setProgressValue(1300);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::handleProgress(int min, int cur, int max, const QString &inReplyTo)
|
|
||||||
{
|
|
||||||
Q_UNUSED(inReplyTo)
|
|
||||||
|
|
||||||
if (!m_future)
|
|
||||||
return;
|
|
||||||
const int progress = calculateProgress(m_progressStepMinimum, min, cur, max, m_progressStepMaximum);
|
|
||||||
m_future->setProgressValue(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::handleSignal(const QString &signal, const QVariantMap &data)
|
|
||||||
{
|
|
||||||
Q_UNUSED(signal)
|
|
||||||
Q_UNUSED(data)
|
|
||||||
// We do not need to act on fileChanged signals nor on dirty signals!
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::handleServerConnected()
|
|
||||||
{
|
|
||||||
if (m_delayedConfigurationData) {
|
|
||||||
sendConfigureRequest(*m_delayedConfigurationData);
|
|
||||||
m_delayedConfigurationData.reset();
|
|
||||||
} else {
|
|
||||||
emit isReadyNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::sendConfigureRequest(const QVariantMap &extra)
|
|
||||||
{
|
|
||||||
m_delayedErrorMessage.clear();
|
|
||||||
m_cmakeServer->sendRequest(CONFIGURE_TYPE, extra);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::reportError()
|
|
||||||
{
|
|
||||||
stop();
|
|
||||||
Core::MessageManager::write(tr("CMake Project parsing failed."));
|
|
||||||
emit errorOccurred(m_delayedErrorMessage);
|
|
||||||
|
|
||||||
if (m_future)
|
|
||||||
m_future->reportCanceled();
|
|
||||||
|
|
||||||
m_delayedErrorMessage.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
int ServerModeReader::calculateProgress(const int minRange, const int min, const int cur, const int max, const int maxRange)
|
|
||||||
{
|
|
||||||
if (minRange == maxRange || min == max)
|
|
||||||
return minRange;
|
|
||||||
const int clampedCur = std::min(std::max(cur, min), max);
|
|
||||||
return minRange + ((clampedCur - min) / (max - min)) * (maxRange - minRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::extractCodeModelData(const QVariantMap &data)
|
|
||||||
{
|
|
||||||
const QVariantList configs = data.value("configurations").toList();
|
|
||||||
for (const QVariant &c : configs) {
|
|
||||||
const QVariantMap &cData = c.toMap();
|
|
||||||
extractConfigurationData(cData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::extractConfigurationData(const QVariantMap &data)
|
|
||||||
{
|
|
||||||
const QString name = data.value(NAME_KEY).toString();
|
|
||||||
Q_UNUSED(name)
|
|
||||||
QSet<QString> knownTargets; // To filter duplicate target names:-/
|
|
||||||
const QVariantList projects = data.value("projects").toList();
|
|
||||||
for (const QVariant &p : projects) {
|
|
||||||
const QVariantMap pData = p.toMap();
|
|
||||||
m_projects.append(extractProjectData(pData, knownTargets));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerModeReader::Project *ServerModeReader::extractProjectData(const QVariantMap &data,
|
|
||||||
QSet<QString> &knownTargets)
|
|
||||||
{
|
|
||||||
auto project = new Project;
|
|
||||||
project->name = data.value(NAME_KEY).toString();
|
|
||||||
project->sourceDirectory = FilePath::fromString(data.value(SOURCE_DIRECTORY_KEY).toString());
|
|
||||||
|
|
||||||
const QVariantList targets = data.value("targets").toList();
|
|
||||||
for (const QVariant &t : targets) {
|
|
||||||
const QVariantMap tData = t.toMap();
|
|
||||||
Target *tp = extractTargetData(tData, project, knownTargets);
|
|
||||||
if (tp)
|
|
||||||
project->targets.append(tp);
|
|
||||||
}
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerModeReader::Target *ServerModeReader::extractTargetData(const QVariantMap &data, Project *p,
|
|
||||||
QSet<QString> &knownTargets)
|
|
||||||
{
|
|
||||||
const QString targetName = data.value(NAME_KEY).toString();
|
|
||||||
|
|
||||||
// Remove duplicate targets: CMake unfortunately does duplicate targets for all projects that
|
|
||||||
// contain them. Keep at least till cmake 3.9 is deprecated.
|
|
||||||
const int count = knownTargets.count();
|
|
||||||
knownTargets.insert(targetName);
|
|
||||||
if (knownTargets.count() == count)
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
auto target = new Target;
|
|
||||||
target->project = p;
|
|
||||||
target->name = targetName;
|
|
||||||
target->sourceDirectory = FilePath::fromString(data.value(SOURCE_DIRECTORY_KEY).toString());
|
|
||||||
target->buildDirectory = FilePath::fromString(data.value("buildDirectory").toString());
|
|
||||||
|
|
||||||
target->crossReferences = extractCrossReferences(data.value("crossReferences").toMap());
|
|
||||||
|
|
||||||
QDir srcDir(target->sourceDirectory.toString());
|
|
||||||
|
|
||||||
target->type = data.value("type").toString();
|
|
||||||
const QStringList artifacts = data.value("artifacts").toStringList();
|
|
||||||
target->artifacts = transform(artifacts, [&srcDir](const QString &a) { return FilePath::fromString(srcDir.absoluteFilePath(a)); });
|
|
||||||
|
|
||||||
const QVariantList fileGroups = data.value("fileGroups").toList();
|
|
||||||
for (const QVariant &fg : fileGroups) {
|
|
||||||
const QVariantMap fgData = fg.toMap();
|
|
||||||
target->fileGroups.append(extractFileGroupData(fgData, srcDir, target));
|
|
||||||
}
|
|
||||||
|
|
||||||
fixTarget(target);
|
|
||||||
|
|
||||||
m_targets.append(target);
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerModeReader::FileGroup *ServerModeReader::extractFileGroupData(const QVariantMap &data,
|
|
||||||
const QDir &srcDir,
|
|
||||||
Target *t)
|
|
||||||
{
|
|
||||||
auto fileGroup = new FileGroup;
|
|
||||||
fileGroup->target = t;
|
|
||||||
fileGroup->compileFlags = data.value("compileFlags").toString();
|
|
||||||
fileGroup->macros = Utils::transform<QVector>(data.value("defines").toStringList(), [](const QString &s) {
|
|
||||||
return ProjectExplorer::Macro::fromKeyValue(s);
|
|
||||||
});
|
|
||||||
fileGroup->includePaths = transform(data.value("includePath").toList(),
|
|
||||||
[](const QVariant &i) -> IncludePath* {
|
|
||||||
const QVariantMap iData = i.toMap();
|
|
||||||
auto result = new IncludePath;
|
|
||||||
result->path = FilePath::fromString(iData.value("path").toString());
|
|
||||||
result->isSystem = iData.value("isSystem", false).toBool();
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
fileGroup->isGenerated = data.value("isGenerated", false).toBool();
|
|
||||||
fileGroup->sources = transform(data.value(SOURCES_KEY).toStringList(),
|
|
||||||
[&srcDir](const QString &s) {
|
|
||||||
return FilePath::fromString(QDir::cleanPath(srcDir.absoluteFilePath(s)));
|
|
||||||
});
|
|
||||||
|
|
||||||
m_fileGroups.append(fileGroup);
|
|
||||||
return fileGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<ServerModeReader::CrossReference *> ServerModeReader::extractCrossReferences(const QVariantMap &data)
|
|
||||||
{
|
|
||||||
QList<CrossReference *> crossReferences;
|
|
||||||
|
|
||||||
if (data.isEmpty())
|
|
||||||
return crossReferences;
|
|
||||||
|
|
||||||
auto cr = std::make_unique<CrossReference>();
|
|
||||||
cr->backtrace = extractBacktrace(data.value(BACKTRACE_KEY, QVariantList()).toList());
|
|
||||||
QTC_ASSERT(!cr->backtrace.isEmpty(), return {});
|
|
||||||
crossReferences.append(cr.release());
|
|
||||||
|
|
||||||
const QVariantList related = data.value("relatedStatements", QVariantList()).toList();
|
|
||||||
for (const QVariant &relatedData : related) {
|
|
||||||
auto cr = std::make_unique<CrossReference>();
|
|
||||||
|
|
||||||
// extract information:
|
|
||||||
const QVariantMap map = relatedData.toMap();
|
|
||||||
const QString typeString = map.value("type", QString()).toString();
|
|
||||||
if (typeString.isEmpty())
|
|
||||||
cr->type = CrossReference::TARGET;
|
|
||||||
else if (typeString == "target_link_libraries")
|
|
||||||
cr->type = CrossReference::LIBRARIES;
|
|
||||||
else if (typeString == "target_compile_defines")
|
|
||||||
cr->type = CrossReference::DEFINES;
|
|
||||||
else if (typeString == "target_include_directories")
|
|
||||||
cr->type = CrossReference::INCLUDES;
|
|
||||||
else
|
|
||||||
cr->type = CrossReference::UNKNOWN;
|
|
||||||
cr->backtrace = extractBacktrace(map.value(BACKTRACE_KEY, QVariantList()).toList());
|
|
||||||
|
|
||||||
// sanity check:
|
|
||||||
if (cr->backtrace.isEmpty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// store information:
|
|
||||||
crossReferences.append(cr.release());
|
|
||||||
}
|
|
||||||
return crossReferences;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerModeReader::BacktraceItem *ServerModeReader::extractBacktraceItem(const QVariantMap &data)
|
|
||||||
{
|
|
||||||
QTC_ASSERT(!data.isEmpty(), return nullptr);
|
|
||||||
auto item = std::make_unique<BacktraceItem>();
|
|
||||||
|
|
||||||
item->line = data.value(LINE_KEY, -1).toInt();
|
|
||||||
item->name = data.value(NAME_KEY, QString()).toString();
|
|
||||||
item->path = data.value(PATH_KEY, QString()).toString();
|
|
||||||
|
|
||||||
QTC_ASSERT(!item->path.isEmpty(), return nullptr);
|
|
||||||
return item.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<ServerModeReader::BacktraceItem *> ServerModeReader::extractBacktrace(const QVariantList &data)
|
|
||||||
{
|
|
||||||
QList<BacktraceItem *> btResult;
|
|
||||||
for (const QVariant &bt : data) {
|
|
||||||
BacktraceItem *btItem = extractBacktraceItem(bt.toMap());
|
|
||||||
QTC_ASSERT(btItem, continue);
|
|
||||||
|
|
||||||
btResult.append(btItem);
|
|
||||||
}
|
|
||||||
return btResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::extractCMakeInputsData(const QVariantMap &data)
|
|
||||||
{
|
|
||||||
const FilePath src = FilePath::fromString(data.value(SOURCE_DIRECTORY_KEY).toString());
|
|
||||||
QTC_ASSERT(src == m_parameters.sourceDirectory, return);
|
|
||||||
QDir srcDir(src.toString());
|
|
||||||
|
|
||||||
m_cmakeFiles.clear();
|
|
||||||
|
|
||||||
const QVariantList buildFiles = data.value("buildFiles").toList();
|
|
||||||
for (const QVariant &bf : buildFiles) {
|
|
||||||
const QVariantMap §ion = bf.toMap();
|
|
||||||
const QStringList sources = section.value(SOURCES_KEY).toStringList();
|
|
||||||
|
|
||||||
const bool isTemporary = section.value("isTemporary").toBool(); // generated file
|
|
||||||
const bool isCMake = section.value("isCMake").toBool(); // part of the cmake installation
|
|
||||||
|
|
||||||
for (const QString &s : sources) {
|
|
||||||
const FilePath sfn = FilePath::fromString(QDir::cleanPath(srcDir.absoluteFilePath(s)));
|
|
||||||
const int oldCount = m_cmakeFiles.count();
|
|
||||||
m_cmakeFiles.insert(sfn);
|
|
||||||
if (oldCount < m_cmakeFiles.count()) {
|
|
||||||
const bool isCMakeListsFile = sfn.toString().endsWith("/CMakeLists.txt");
|
|
||||||
|
|
||||||
if (isCMake && !isCMakeListsFile)
|
|
||||||
// Skip files that cmake considers to be part of the installation -- but include
|
|
||||||
// CMakeLists.txt files. This unbreaks cmake binaries running from their own
|
|
||||||
// build directory.
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto node = std::make_unique<FileNode>(sfn, FileType::Project);
|
|
||||||
node->setIsGenerated(isTemporary && !isCMakeListsFile); // CMakeLists.txt are never
|
|
||||||
// generated, independent
|
|
||||||
// what cmake thinks:-)
|
|
||||||
|
|
||||||
m_cmakeInputsFileNodes.emplace_back(std::move(node));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::extractCacheData(const QVariantMap &data)
|
|
||||||
{
|
|
||||||
CMakeConfig config;
|
|
||||||
const QVariantList entries = data.value("cache").toList();
|
|
||||||
for (const QVariant &e : entries) {
|
|
||||||
const QVariantMap eData = e.toMap();
|
|
||||||
CMakeConfigItem item;
|
|
||||||
item.key = eData.value("key").toByteArray();
|
|
||||||
item.value = eData.value("value").toByteArray();
|
|
||||||
item.type = CMakeConfigItem::typeStringToType(eData.value("type").toByteArray());
|
|
||||||
const QVariantMap properties = eData.value("properties").toMap();
|
|
||||||
item.isAdvanced = properties.value("ADVANCED", false).toBool();
|
|
||||||
item.documentation = properties.value("HELPSTRING").toByteArray();
|
|
||||||
item.values = CMakeConfigItem::cmakeSplitValue(properties.value("STRINGS").toString(), true);
|
|
||||||
config.append(item);
|
|
||||||
}
|
|
||||||
m_cmakeConfiguration = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::fixTarget(ServerModeReader::Target *target) const
|
|
||||||
{
|
|
||||||
QHash<QString, const FileGroup *> languageFallbacks;
|
|
||||||
|
|
||||||
for (const FileGroup *group : qAsConst(target->fileGroups)) {
|
|
||||||
if (group->includePaths.isEmpty() && group->compileFlags.isEmpty()
|
|
||||||
&& group->macros.isEmpty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const FileGroup *fallback = languageFallbacks.value(group->language);
|
|
||||||
if (!fallback || fallback->sources.count() < group->sources.count())
|
|
||||||
languageFallbacks.insert(group->language, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!languageFallbacks.value(""))
|
|
||||||
return; // No empty language groups found, no need to proceed.
|
|
||||||
|
|
||||||
const FileGroup *fallback = languageFallbacks.value("CXX");
|
|
||||||
if (!fallback)
|
|
||||||
fallback = languageFallbacks.value("C");
|
|
||||||
if (!fallback)
|
|
||||||
fallback = languageFallbacks.value("");
|
|
||||||
|
|
||||||
if (!fallback)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (auto it = target->fileGroups.begin(); it != target->fileGroups.end(); ++it) {
|
|
||||||
if (!(*it)->language.isEmpty())
|
|
||||||
continue;
|
|
||||||
(*it)->language = fallback->language.isEmpty() ? "CXX" : fallback->language;
|
|
||||||
|
|
||||||
if (*it == fallback
|
|
||||||
|| !(*it)->includePaths.isEmpty() || !(*it)->macros.isEmpty()
|
|
||||||
|| !(*it)->compileFlags.isEmpty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
for (const IncludePath *ip : fallback->includePaths)
|
|
||||||
(*it)->includePaths.append(new IncludePath(*ip));
|
|
||||||
(*it)->macros = fallback->macros;
|
|
||||||
(*it)->compileFlags = fallback->compileFlags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::addProjects(const QHash<Utils::FilePath, ProjectNode *> &cmakeListsNodes,
|
|
||||||
const QList<Project *> &projects,
|
|
||||||
QSet<FilePath> &knownHeaders)
|
|
||||||
{
|
|
||||||
for (const Project *p : projects) {
|
|
||||||
createProjectNode(cmakeListsNodes, p->sourceDirectory, p->name);
|
|
||||||
addTargets(cmakeListsNodes, p->targets, knownHeaders);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::addTargets(
|
|
||||||
const QHash<Utils::FilePath, ProjectExplorer::ProjectNode *> &cmakeListsNodes,
|
|
||||||
const QList<Target *> &targets,
|
|
||||||
QSet<Utils::FilePath> &knownHeaders)
|
|
||||||
{
|
|
||||||
for (const Target *t : targets) {
|
|
||||||
CMakeTargetNode *tNode = createTargetNode(cmakeListsNodes, t->sourceDirectory, t->name);
|
|
||||||
QTC_ASSERT(tNode, qDebug() << "No target node for" << t->sourceDirectory << t->name; continue);
|
|
||||||
tNode->setTargetInformation(t->artifacts, t->type);
|
|
||||||
tNode->setBuildDirectory(t->buildDirectory);
|
|
||||||
QVector<FolderNode::LocationInfo> info;
|
|
||||||
// Set up a default target path:
|
|
||||||
FilePath targetPath = t->sourceDirectory.pathAppended("CMakeLists.txt");
|
|
||||||
for (CrossReference *cr : qAsConst(t->crossReferences)) {
|
|
||||||
BacktraceItem *bt = cr->backtrace.isEmpty() ? nullptr : cr->backtrace.at(0);
|
|
||||||
if (bt) {
|
|
||||||
const QString btName = bt->name.toLower();
|
|
||||||
const FilePath path = Utils::FilePath::fromUserInput(bt->path);
|
|
||||||
QString dn;
|
|
||||||
if (cr->type != CrossReference::TARGET) {
|
|
||||||
if (path == targetPath) {
|
|
||||||
if (bt->line >= 0)
|
|
||||||
dn = tr("%1 in line %2").arg(btName).arg(bt->line);
|
|
||||||
else
|
|
||||||
dn = tr("%1").arg(btName);
|
|
||||||
} else {
|
|
||||||
if (bt->line >= 0)
|
|
||||||
dn = tr("%1 in %2:%3").arg(btName, path.toUserOutput()).arg(bt->line);
|
|
||||||
else
|
|
||||||
dn = tr("%1 in %2").arg(btName, path.toUserOutput());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dn = tr("Target Definition");
|
|
||||||
targetPath = path;
|
|
||||||
}
|
|
||||||
info.append(FolderNode::LocationInfo(dn, path, bt->line));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tNode->setLocationInfo(info);
|
|
||||||
addFileGroups(tNode, t->sourceDirectory, t->buildDirectory, t->fileGroups, knownHeaders);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerModeReader::addFileGroups(ProjectNode *targetRoot,
|
|
||||||
const Utils::FilePath &sourceDirectory,
|
|
||||||
const Utils::FilePath &buildDirectory,
|
|
||||||
const QList<ServerModeReader::FileGroup *> &fileGroups,
|
|
||||||
QSet<Utils::FilePath> &knownHeaders)
|
|
||||||
{
|
|
||||||
std::vector<std::unique_ptr<FileNode>> toList;
|
|
||||||
QSet<Utils::FilePath> alreadyListed;
|
|
||||||
// Files already added by other configurations:
|
|
||||||
targetRoot->forEachGenericNode([&alreadyListed](const Node *n) {
|
|
||||||
alreadyListed.insert(n->filePath());
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const FileGroup *f : fileGroups) {
|
|
||||||
const QList<FilePath> newSources = Utils::filtered(f->sources, [&alreadyListed](const Utils::FilePath &fn) {
|
|
||||||
const int count = alreadyListed.count();
|
|
||||||
alreadyListed.insert(fn);
|
|
||||||
return count != alreadyListed.count();
|
|
||||||
});
|
|
||||||
std::vector<std::unique_ptr<FileNode>> newFileNodes = Utils::transform<std::vector>(
|
|
||||||
newSources, [f, &knownHeaders](const Utils::FilePath &fn) {
|
|
||||||
auto node = std::make_unique<FileNode>(fn, Node::fileTypeForFileName(fn));
|
|
||||||
node->setIsGenerated(f->isGenerated);
|
|
||||||
if (node->fileType() == FileType::Header)
|
|
||||||
knownHeaders.insert(node->filePath());
|
|
||||||
return node;
|
|
||||||
});
|
|
||||||
std::move(std::begin(newFileNodes), std::end(newFileNodes), std::back_inserter(toList));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split up files in groups (based on location):
|
|
||||||
const bool inSourceBuild = (m_parameters.workDirectory == m_parameters.sourceDirectory);
|
|
||||||
std::vector<std::unique_ptr<FileNode>> sourceFileNodes;
|
|
||||||
std::vector<std::unique_ptr<FileNode>> buildFileNodes;
|
|
||||||
std::vector<std::unique_ptr<FileNode>> otherFileNodes;
|
|
||||||
for (std::unique_ptr<FileNode> &fn : toList) {
|
|
||||||
if (fn->filePath().isChildOf(m_parameters.workDirectory) && !inSourceBuild)
|
|
||||||
buildFileNodes.emplace_back(std::move(fn));
|
|
||||||
else if (fn->filePath().isChildOf(m_parameters.sourceDirectory))
|
|
||||||
sourceFileNodes.emplace_back(std::move(fn));
|
|
||||||
else
|
|
||||||
otherFileNodes.emplace_back(std::move(fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
addCMakeVFolder(targetRoot, sourceDirectory, 1000, QString(), std::move(sourceFileNodes));
|
|
||||||
addCMakeVFolder(targetRoot, buildDirectory, 100, tr("<Build Directory>"), std::move(buildFileNodes));
|
|
||||||
addCMakeVFolder(targetRoot, Utils::FilePath(), 10, tr("<Other Locations>"), std::move(otherFileNodes));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Internal
|
|
||||||
} // namespace CMakeProjectManager
|
|
||||||
|
|
||||||
#if defined(WITH_TESTS)
|
|
||||||
|
|
||||||
#include "cmakeprojectplugin.h"
|
|
||||||
#include <QTest>
|
|
||||||
|
|
||||||
namespace CMakeProjectManager {
|
|
||||||
namespace Internal {
|
|
||||||
|
|
||||||
void CMakeProjectPlugin::testServerModeReaderProgress_data()
|
|
||||||
{
|
|
||||||
QTest::addColumn<int>("minRange");
|
|
||||||
QTest::addColumn<int>("min");
|
|
||||||
QTest::addColumn<int>("cur");
|
|
||||||
QTest::addColumn<int>("max");
|
|
||||||
QTest::addColumn<int>("maxRange");
|
|
||||||
QTest::addColumn<int>("expected");
|
|
||||||
|
|
||||||
QTest::newRow("empty range") << 100 << 10 << 11 << 20 << 100 << 100;
|
|
||||||
QTest::newRow("one range (low)") << 0 << 10 << 11 << 20 << 1 << 0;
|
|
||||||
QTest::newRow("one range (high)") << 20 << 10 << 19 << 20 << 20 << 20;
|
|
||||||
QTest::newRow("large range") << 30 << 10 << 11 << 20 << 100000 << 30;
|
|
||||||
|
|
||||||
QTest::newRow("empty progress") << -5 << 10 << 10 << 10 << 99995 << -5;
|
|
||||||
QTest::newRow("one progress (low)") << 42 << 10 << 10 << 11 << 100042 << 42;
|
|
||||||
QTest::newRow("one progress (high)") << 0 << 10 << 11 << 11 << 100000 << 100000;
|
|
||||||
QTest::newRow("large progress") << 0 << 10 << 10 << 11 << 100000 << 0;
|
|
||||||
|
|
||||||
QTest::newRow("cur too low") << 0 << 10 << 9 << 100 << 100000 << 0;
|
|
||||||
QTest::newRow("cur too high") << 0 << 10 << 101 << 100 << 100000 << 100000;
|
|
||||||
QTest::newRow("cur much too low") << 0 << 10 << -1000 << 100 << 100000 << 0;
|
|
||||||
QTest::newRow("cur much too high") << 0 << 10 << 1110000 << 100 << 100000 << 100000;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CMakeProjectPlugin::testServerModeReaderProgress()
|
|
||||||
{
|
|
||||||
QFETCH(int, minRange);
|
|
||||||
QFETCH(int, min);
|
|
||||||
QFETCH(int, cur);
|
|
||||||
QFETCH(int, max);
|
|
||||||
QFETCH(int, maxRange);
|
|
||||||
QFETCH(int, expected);
|
|
||||||
|
|
||||||
ServerModeReader reader;
|
|
||||||
const int r = reader.calculateProgress(minRange, min, cur, max, maxRange);
|
|
||||||
|
|
||||||
QCOMPARE(r, expected);
|
|
||||||
|
|
||||||
QVERIFY(r <= maxRange);
|
|
||||||
QVERIFY(r >= minRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Internal
|
|
||||||
} // namespace CMakeProjectManager
|
|
||||||
|
|
||||||
#endif
|
|
@@ -1,199 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
**
|
|
||||||
** Copyright (C) 2016 The Qt Company Ltd.
|
|
||||||
** Contact: https://www.qt.io/licensing/
|
|
||||||
**
|
|
||||||
** This file is part of Qt Creator.
|
|
||||||
**
|
|
||||||
** Commercial License Usage
|
|
||||||
** Licensees holding valid commercial Qt licenses may use this file in
|
|
||||||
** accordance with the commercial license agreement provided with the
|
|
||||||
** Software or, alternatively, in accordance with the terms contained in
|
|
||||||
** a written agreement between you and The Qt Company. For licensing terms
|
|
||||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
||||||
** information use the contact form at https://www.qt.io/contact-us.
|
|
||||||
**
|
|
||||||
** GNU General Public License Usage
|
|
||||||
** Alternatively, this file may be used under the terms of the GNU
|
|
||||||
** General Public License version 3 as published by the Free Software
|
|
||||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
|
||||||
** included in the packaging of this file. Please review the following
|
|
||||||
** information to ensure the GNU General Public License requirements will
|
|
||||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
|
||||||
**
|
|
||||||
****************************************************************************/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "builddirreader.h"
|
|
||||||
#include "cmakeconfigitem.h"
|
|
||||||
#include "servermode.h"
|
|
||||||
|
|
||||||
#include <projectexplorer/ioutputparser.h>
|
|
||||||
|
|
||||||
#include <QList>
|
|
||||||
#include <QSet>
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace ProjectExplorer { class ProjectNode; }
|
|
||||||
namespace Utils { class OutputFormatter; }
|
|
||||||
|
|
||||||
namespace CMakeProjectManager {
|
|
||||||
|
|
||||||
class CMakeParser;
|
|
||||||
|
|
||||||
namespace Internal {
|
|
||||||
|
|
||||||
class ServerModeReader final : public BuildDirReader
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
ServerModeReader();
|
|
||||||
~ServerModeReader() final;
|
|
||||||
|
|
||||||
void setParameters(const BuildDirParameters &p) final;
|
|
||||||
|
|
||||||
bool isCompatible(const BuildDirParameters &p) final;
|
|
||||||
void resetData() final;
|
|
||||||
void parse(bool forceCMakeRun, bool forceConfiguration) final;
|
|
||||||
void stop() final;
|
|
||||||
|
|
||||||
bool isParsing() const final;
|
|
||||||
|
|
||||||
QSet<Utils::FilePath> projectFilesToWatch() const final { return {}; };
|
|
||||||
QList<CMakeBuildTarget> takeBuildTargets(QString &errorMessage) final;
|
|
||||||
CMakeConfig takeParsedConfiguration(QString &errorMessage) final;
|
|
||||||
std::unique_ptr<CMakeProjectNode> generateProjectTree(
|
|
||||||
const QList<const ProjectExplorer::FileNode *> &allFiles, QString &errorMessage) final;
|
|
||||||
ProjectExplorer::RawProjectParts createRawProjectParts(QString &errorMessage) final;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void createNewServer();
|
|
||||||
void handleReply(const QVariantMap &data, const QString &inReplyTo);
|
|
||||||
void handleError(const QString &message);
|
|
||||||
void handleProgress(int min, int cur, int max, const QString &inReplyTo);
|
|
||||||
void handleSignal(const QString &signal, const QVariantMap &data);
|
|
||||||
void handleServerConnected();
|
|
||||||
|
|
||||||
void sendConfigureRequest(const QVariantMap &extra);
|
|
||||||
|
|
||||||
void reportError();
|
|
||||||
|
|
||||||
int calculateProgress(const int minRange, const int min,
|
|
||||||
const int cur,
|
|
||||||
const int max, const int maxRange);
|
|
||||||
|
|
||||||
struct Target;
|
|
||||||
struct Project;
|
|
||||||
|
|
||||||
struct IncludePath {
|
|
||||||
Utils::FilePath path;
|
|
||||||
bool isSystem;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FileGroup {
|
|
||||||
~FileGroup() { qDeleteAll(includePaths); includePaths.clear(); }
|
|
||||||
|
|
||||||
Target *target = nullptr;
|
|
||||||
QString compileFlags;
|
|
||||||
ProjectExplorer::Macros macros;
|
|
||||||
QList<IncludePath *> includePaths;
|
|
||||||
QString language;
|
|
||||||
QList<Utils::FilePath> sources;
|
|
||||||
bool isGenerated;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct BacktraceItem {
|
|
||||||
int line = -1;
|
|
||||||
QString path;
|
|
||||||
QString name;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CrossReference {
|
|
||||||
~CrossReference() { qDeleteAll(backtrace); backtrace.clear(); }
|
|
||||||
QList<BacktraceItem *> backtrace;
|
|
||||||
enum Type { TARGET, LIBRARIES, DEFINES, INCLUDES, UNKNOWN };
|
|
||||||
Type type;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Target {
|
|
||||||
~Target() {
|
|
||||||
qDeleteAll(fileGroups);
|
|
||||||
fileGroups.clear();
|
|
||||||
qDeleteAll(crossReferences);
|
|
||||||
crossReferences.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
Project *project = nullptr;
|
|
||||||
QString name;
|
|
||||||
QString type;
|
|
||||||
QList<Utils::FilePath> artifacts;
|
|
||||||
Utils::FilePath sourceDirectory;
|
|
||||||
Utils::FilePath buildDirectory;
|
|
||||||
QList<FileGroup *> fileGroups;
|
|
||||||
QList<CrossReference *> crossReferences;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Project {
|
|
||||||
~Project() { qDeleteAll(targets); targets.clear(); }
|
|
||||||
QString name;
|
|
||||||
Utils::FilePath sourceDirectory;
|
|
||||||
QList<Target *> targets;
|
|
||||||
};
|
|
||||||
|
|
||||||
void extractCodeModelData(const QVariantMap &data);
|
|
||||||
void extractConfigurationData(const QVariantMap &data);
|
|
||||||
Project *extractProjectData(const QVariantMap &data, QSet<QString> &knownTargets);
|
|
||||||
Target *extractTargetData(const QVariantMap &data, Project *p, QSet<QString> &knownTargets);
|
|
||||||
FileGroup *extractFileGroupData(const QVariantMap &data, const QDir &srcDir, Target *t);
|
|
||||||
QList<CrossReference *> extractCrossReferences(const QVariantMap &data);
|
|
||||||
QList<BacktraceItem *> extractBacktrace(const QVariantList &data);
|
|
||||||
BacktraceItem *extractBacktraceItem(const QVariantMap &data);
|
|
||||||
void extractCMakeInputsData(const QVariantMap &data);
|
|
||||||
void extractCacheData(const QVariantMap &data);
|
|
||||||
|
|
||||||
void fixTarget(Target *target) const;
|
|
||||||
|
|
||||||
void addProjects(const QHash<Utils::FilePath, ProjectExplorer::ProjectNode *> &cmakeListsNodes,
|
|
||||||
const QList<Project *> &projects,
|
|
||||||
QSet<Utils::FilePath> &knownHeaders);
|
|
||||||
void addTargets(const QHash<Utils::FilePath, ProjectExplorer::ProjectNode *> &cmakeListsNodes,
|
|
||||||
const QList<Target *> &targets,
|
|
||||||
QSet<Utils::FilePath> &knownHeaders);
|
|
||||||
void addFileGroups(ProjectExplorer::ProjectNode *targetRoot,
|
|
||||||
const Utils::FilePath &sourceDirectory,
|
|
||||||
const Utils::FilePath &buildDirectory,
|
|
||||||
const QList<FileGroup *> &fileGroups,
|
|
||||||
QSet<Utils::FilePath> &knownHeaders);
|
|
||||||
|
|
||||||
std::unique_ptr<ServerMode> m_cmakeServer;
|
|
||||||
std::unique_ptr<QFutureInterface<void>> m_future;
|
|
||||||
|
|
||||||
int m_progressStepMinimum = 0;
|
|
||||||
int m_progressStepMaximum = 1000;
|
|
||||||
|
|
||||||
Utils::optional<QVariantMap> m_delayedConfigurationData;
|
|
||||||
|
|
||||||
QString m_delayedErrorMessage;
|
|
||||||
|
|
||||||
CMakeConfig m_cmakeConfiguration;
|
|
||||||
|
|
||||||
QSet<Utils::FilePath> m_cmakeFiles;
|
|
||||||
std::vector<std::unique_ptr<ProjectExplorer::FileNode>> m_cmakeInputsFileNodes;
|
|
||||||
|
|
||||||
QList<Project *> m_projects;
|
|
||||||
QList<Target *> m_targets;
|
|
||||||
QList<FileGroup *> m_fileGroups;
|
|
||||||
|
|
||||||
CMakeParser *m_cmakeParser = nullptr;
|
|
||||||
Utils::OutputFormatter m_parser;
|
|
||||||
|
|
||||||
#if defined(WITH_TESTS)
|
|
||||||
friend class CMakeProjectPlugin;
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Internal
|
|
||||||
} // namespace CMakeProjectManager
|
|
Reference in New Issue
Block a user