Files
qt-creator/src/plugins/cmakeprojectmanager/fileapireader.cpp
Eike Ziller bd660aaf27 CMake: Fix setting additional project files for file watching
Amends d8be2491a5

The change above introduced FileApiReader::takeCMakeFileInfos and uses
it to move the information about additional CMake files out of the
FileApiReader. But that now emptied variable was later used to inform
the project about these additional files. So, that broke the automatic
running of CMake when project files (except the toplevel one) changes.
Instead use the list of additional files that now lives in the
CMakeBuildSystem for that purpose.

Change-Id: I1062593029880af9d4c70e72e1bd101d40ad0c00
Reviewed-by: Cristian Adam <cristian.adam@qt.io>
2023-05-08 12:23:10 +00:00

387 lines
14 KiB
C++

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "fileapireader.h"
#include "cmakeprocess.h"
#include "cmakeprojectmanagertr.h"
#include "cmakespecificsettings.h"
#include "fileapidataextractor.h"
#include "fileapiparser.h"
#include <coreplugin/messagemanager.h>
#include <projectexplorer/projectexplorer.h>
#include <utils/algorithm.h>
#include <utils/async.h>
#include <utils/qtcassert.h>
#include <QLoggingCategory>
using namespace ProjectExplorer;
using namespace Utils;
namespace CMakeProjectManager::Internal {
static Q_LOGGING_CATEGORY(cmakeFileApiMode, "qtc.cmake.fileApiMode", QtWarningMsg);
using namespace FileApiDetails;
// --------------------------------------------------------------------
// FileApiReader:
// --------------------------------------------------------------------
FileApiReader::FileApiReader()
: m_lastReplyTimestamp()
{
QObject::connect(&m_watcher,
&FileSystemWatcher::directoryChanged,
this,
&FileApiReader::replyDirectoryHasChanged);
}
FileApiReader::~FileApiReader()
{
stop();
resetData();
}
void FileApiReader::setParameters(const BuildDirParameters &p)
{
qCDebug(cmakeFileApiMode)
<< "\n\n\n\n\n=============================================================\n";
// Update:
m_parameters = p;
qCDebug(cmakeFileApiMode) << "Work directory:" << m_parameters.buildDirectory.toUserOutput();
// Reset watcher:
m_watcher.clear();
FileApiParser::setupCMakeFileApi(m_parameters.buildDirectory, m_watcher);
resetData();
}
void FileApiReader::resetData()
{
m_cmakeFiles.clear();
if (!m_parameters.sourceDirectory.isEmpty()) {
CMakeFileInfo cmakeListsTxt;
cmakeListsTxt.path = m_parameters.sourceDirectory.pathAppended("CMakeLists.txt");
cmakeListsTxt.isCMakeListsDotTxt = true;
m_cmakeFiles.insert(cmakeListsTxt);
}
m_cache.clear();
m_buildTargets.clear();
m_projectParts.clear();
m_rootProjectNode.reset();
}
void FileApiReader::parse(bool forceCMakeRun,
bool forceInitialConfiguration,
bool forceExtraConfiguration)
{
qCDebug(cmakeFileApiMode) << "Parse called with arguments: ForceCMakeRun:" << forceCMakeRun
<< " - forceConfiguration:" << forceInitialConfiguration
<< " - forceExtraConfiguration:" << forceExtraConfiguration;
startState();
const QStringList args = (forceInitialConfiguration ? m_parameters.initialCMakeArguments
: QStringList())
+ (forceExtraConfiguration
? (m_parameters.configurationChangesArguments
+ m_parameters.additionalCMakeArguments)
: QStringList());
qCDebug(cmakeFileApiMode) << "Parameters request these CMake arguments:" << args;
const FilePath replyFile = FileApiParser::scanForCMakeReplyFile(m_parameters.buildDirectory);
// Only need to update when one of the following conditions is met:
// * The user forces the cmake run,
// * The user provided arguments,
// * There is no reply file,
// * One of the cmakefiles is newer than the replyFile and the user asked
// for creator to run CMake as needed,
// * A query file is newer than the reply file
const bool hasArguments = !args.isEmpty();
const bool replyFileMissing = !replyFile.exists();
const auto settings = CMakeSpecificSettings::instance();
const bool cmakeFilesChanged = m_parameters.cmakeTool() && settings->autorunCMake.value()
&& anyOf(m_cmakeFiles, [&replyFile](const CMakeFileInfo &info) {
return !info.isGenerated
&& info.path.lastModified() > replyFile.lastModified();
});
const bool queryFileChanged = anyOf(FileApiParser::cmakeQueryFilePaths(m_parameters.buildDirectory),
[&replyFile](const FilePath &qf) {
return qf.lastModified() > replyFile.lastModified();
});
const bool mustUpdate = forceCMakeRun || hasArguments || replyFileMissing || cmakeFilesChanged
|| queryFileChanged;
qCDebug(cmakeFileApiMode) << QString("Do I need to run CMake? %1 "
"(force: %2 | args: %3 | missing reply: %4 | "
"cmakeFilesChanged: %5 | "
"queryFileChanged: %6)")
.arg(mustUpdate)
.arg(forceCMakeRun)
.arg(hasArguments)
.arg(replyFileMissing)
.arg(cmakeFilesChanged)
.arg(queryFileChanged);
if (mustUpdate) {
qCDebug(cmakeFileApiMode) << QString("FileApiReader: Starting CMake with \"%1\".")
.arg(args.join("\", \""));
startCMakeState(args);
} else {
endState(replyFile, false);
}
}
void FileApiReader::stop()
{
if (m_cmakeProcess)
disconnect(m_cmakeProcess.get(), nullptr, this, nullptr);
m_cmakeProcess.reset();
if (m_future) {
m_future->cancel();
m_future->waitForFinished();
}
m_future = {};
m_isParsing = false;
}
void FileApiReader::stopCMakeRun()
{
if (m_cmakeProcess)
m_cmakeProcess->stop();
}
bool FileApiReader::isParsing() const
{
return m_isParsing;
}
QList<CMakeBuildTarget> FileApiReader::takeBuildTargets(QString &errorMessage){
Q_UNUSED(errorMessage)
return std::exchange(m_buildTargets, {});
}
QSet<CMakeFileInfo> FileApiReader::takeCMakeFileInfos(QString &errorMessage)
{
Q_UNUSED(errorMessage)
return std::exchange(m_cmakeFiles, {});
}
CMakeConfig FileApiReader::takeParsedConfiguration(QString &errorMessage)
{
if (m_lastCMakeExitCode != 0)
errorMessage = Tr::tr("CMake returned error code: %1").arg(m_lastCMakeExitCode);
return std::exchange(m_cache, {});
}
QString FileApiReader::ctestPath() const
{
// if we failed to run cmake we should not offer ctest information either
return m_lastCMakeExitCode == 0 ? m_ctestPath : QString();
}
bool FileApiReader::isMultiConfig() const
{
return m_isMultiConfig;
}
bool FileApiReader::usesAllCapsTargets() const
{
return m_usesAllCapsTargets;
}
RawProjectParts FileApiReader::createRawProjectParts(QString &errorMessage)
{
Q_UNUSED(errorMessage)
return std::exchange(m_projectParts, {});
}
void FileApiReader::startState()
{
qCDebug(cmakeFileApiMode) << "FileApiReader: START STATE.";
QTC_ASSERT(!m_isParsing, return );
QTC_ASSERT(!m_future.has_value(), return );
m_isParsing = true;
qCDebug(cmakeFileApiMode) << "FileApiReader: CONFIGURATION STARTED SIGNAL";
emit configurationStarted();
}
void FileApiReader::endState(const FilePath &replyFilePath, bool restoredFromBackup)
{
qCDebug(cmakeFileApiMode) << "FileApiReader: END STATE.";
QTC_ASSERT(m_isParsing, return );
QTC_ASSERT(!m_future.has_value(), return );
const FilePath sourceDirectory = m_parameters.sourceDirectory;
const FilePath buildDirectory = m_parameters.buildDirectory;
const QString cmakeBuildType = m_parameters.cmakeBuildType == "Build" ? "" : m_parameters.cmakeBuildType;
m_lastReplyTimestamp = replyFilePath.lastModified();
m_future = Utils::asyncRun(ProjectExplorerPlugin::sharedThreadPool(),
[replyFilePath, sourceDirectory, buildDirectory, cmakeBuildType](
QPromise<std::shared_ptr<FileApiQtcData>> &promise) {
auto result = std::make_shared<FileApiQtcData>();
FileApiData data = FileApiParser::parseData(promise,
replyFilePath,
cmakeBuildType,
result->errorMessage);
if (result->errorMessage.isEmpty())
*result = extractData(data, sourceDirectory, buildDirectory);
else
qWarning() << result->errorMessage;
promise.addResult(result);
});
onResultReady(m_future.value(),
this,
[this, sourceDirectory, buildDirectory, restoredFromBackup](
const std::shared_ptr<FileApiQtcData> &value) {
m_isParsing = false;
m_cache = std::move(value->cache);
m_cmakeFiles = std::move(value->cmakeFiles);
m_buildTargets = std::move(value->buildTargets);
m_projectParts = std::move(value->projectParts);
m_rootProjectNode = std::move(value->rootProjectNode);
m_ctestPath = std::move(value->ctestPath);
m_isMultiConfig = std::move(value->isMultiConfig);
m_usesAllCapsTargets = std::move(value->usesAllCapsTargets);
if (value->errorMessage.isEmpty()) {
emit this->dataAvailable(restoredFromBackup);
} else {
emit this->errorOccurred(value->errorMessage);
}
m_future = {};
});
}
void FileApiReader::makeBackupConfiguration(bool store)
{
FilePath reply = m_parameters.buildDirectory.pathAppended(".cmake/api/v1/reply");
FilePath replyPrev = m_parameters.buildDirectory.pathAppended(".cmake/api/v1/reply.prev");
if (!store)
std::swap(reply, replyPrev);
if (reply.exists()) {
if (replyPrev.exists())
replyPrev.removeRecursively();
QTC_CHECK(!replyPrev.exists());
if (!reply.renameFile(replyPrev))
Core::MessageManager::writeFlashing(Tr::tr("Failed to rename \"%1\" to \"%2\".")
.arg(reply.toString(), replyPrev.toString()));
}
FilePath cmakeCacheTxt = m_parameters.buildDirectory.pathAppended("CMakeCache.txt");
FilePath cmakeCacheTxtPrev = m_parameters.buildDirectory.pathAppended("CMakeCache.txt.prev");
if (!store)
std::swap(cmakeCacheTxt, cmakeCacheTxtPrev);
if (cmakeCacheTxt.exists())
if (!FileUtils::copyIfDifferent(cmakeCacheTxt, cmakeCacheTxtPrev))
Core::MessageManager::writeFlashing(
Tr::tr("Failed to copy \"%1\" to \"%2\".")
.arg(cmakeCacheTxt.toString(), cmakeCacheTxtPrev.toString()));
}
void FileApiReader::writeConfigurationIntoBuildDirectory(const QStringList &configurationArguments)
{
const FilePath buildDir = m_parameters.buildDirectory;
QTC_CHECK(buildDir.ensureWritableDir());
QByteArray contents;
QStringList unknownOptions;
contents.append("# This file is managed by Qt Creator, do not edit!\n\n");
contents.append(
transform(CMakeConfig::fromArguments(configurationArguments, unknownOptions).toList(),
[](const CMakeConfigItem &item) { return item.toCMakeSetLine(nullptr); })
.join('\n')
.toUtf8());
const FilePath settingsFile = buildDir / "qtcsettings.cmake";
QTC_CHECK(settingsFile.writeFileContents(contents));
}
std::unique_ptr<CMakeProjectNode> FileApiReader::rootProjectNode()
{
return std::exchange(m_rootProjectNode, {});
}
FilePath FileApiReader::topCmakeFile() const
{
return m_cmakeFiles.size() == 1 ? (*m_cmakeFiles.begin()).path : FilePath{};
}
int FileApiReader::lastCMakeExitCode() const
{
return m_lastCMakeExitCode;
}
void FileApiReader::startCMakeState(const QStringList &configurationArguments)
{
qCDebug(cmakeFileApiMode) << "FileApiReader: START CMAKE STATE.";
QTC_ASSERT(!m_cmakeProcess, return );
m_cmakeProcess = std::make_unique<CMakeProcess>();
connect(m_cmakeProcess.get(), &CMakeProcess::finished, this, &FileApiReader::cmakeFinishedState);
qCDebug(cmakeFileApiMode) << ">>>>>> Running cmake with arguments:" << configurationArguments;
// Reset watcher:
m_watcher.removeFiles(m_watcher.filePaths());
m_watcher.removeDirectories(m_watcher.directoryPaths());
makeBackupConfiguration(true);
writeConfigurationIntoBuildDirectory(configurationArguments);
m_cmakeProcess->run(m_parameters, configurationArguments);
}
void FileApiReader::cmakeFinishedState(int exitCode)
{
qCDebug(cmakeFileApiMode) << "FileApiReader: CMAKE FINISHED STATE.";
m_lastCMakeExitCode = exitCode;
m_cmakeProcess.release()->deleteLater();
if (m_lastCMakeExitCode != 0)
makeBackupConfiguration(false);
FileApiParser::setupCMakeFileApi(m_parameters.buildDirectory, m_watcher);
endState(FileApiParser::scanForCMakeReplyFile(m_parameters.buildDirectory),
m_lastCMakeExitCode != 0);
}
void FileApiReader::replyDirectoryHasChanged(const QString &directory) const
{
if (m_isParsing)
return; // This has been triggered by ourselves, ignore.
const FilePath reply = FileApiParser::scanForCMakeReplyFile(m_parameters.buildDirectory);
const FilePath dir = reply.absolutePath();
if (dir.isEmpty())
return; // CMake started to fill the result dir, but has not written a result file yet
QTC_CHECK(!dir.needsDevice());
QTC_ASSERT(dir.path() == directory, return);
if (m_lastReplyTimestamp.isValid() && reply.lastModified() > m_lastReplyTimestamp)
emit dirty();
}
} // CMakeProjectManager::Internal