/**************************************************************************** ** ** 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 "tealeafreader.h" #include "builddirmanager.h" #include "cmakebuildconfiguration.h" #include "cmakecbpparser.h" #include "cmakekitinformation.h" #include "cmakeprocess.h" #include "cmakeprojectconstants.h" #include "cmakeprojectnodes.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Core; using namespace ProjectExplorer; using namespace Utils; namespace CMakeProjectManager { namespace Internal { // -------------------------------------------------------------------- // TeaLeafReader: // -------------------------------------------------------------------- TeaLeafReader::TeaLeafReader() { connect(EditorManager::instance(), &EditorManager::aboutToSave, this, [this](const IDocument *document) { if (m_cmakeFiles.contains(document->filePath()) || !m_parameters.cmakeTool() || !m_parameters.cmakeTool()->isAutoRun()) emit dirty(); }); // Remove \' (quote) for function-style macrosses: // -D'MACRO()'=xxx // -D'MACRO()=xxx' // -D'MACRO()' // otherwise, compiler will fails m_macroFixupRe1.setPattern("^-D(\\s*)'([0-9a-zA-Z_\\(\\)]+)'="); m_macroFixupRe2.setPattern("^-D(\\s*)'([0-9a-zA-Z_\\(\\)]+)=(.+)'$"); m_macroFixupRe3.setPattern("^-D(\\s*)'([0-9a-zA-Z_\\(\\)]+)'$"); } TeaLeafReader::~TeaLeafReader() { stop(); resetData(); } void TeaLeafReader::setParameters(const BuildDirParameters &p) { m_parameters = p; emit isReadyNow(); } bool TeaLeafReader::isCompatible(const BuildDirParameters &p) { return p.cmakeTool() && p.cmakeTool()->readerType() == CMakeTool::TeaLeaf; } void TeaLeafReader::resetData() { m_projectName.clear(); m_buildTargets.clear(); m_files.clear(); } static QString findCbpFile(const QDir &directory) { // Find the cbp file // the cbp file is named like the project() command in the CMakeList.txt file // so this function below could find the wrong cbp file, if the user changes the project() // 2name QDateTime t; QString file; foreach (const QString &cbpFile , directory.entryList()) { if (cbpFile.endsWith(QLatin1String(".cbp"))) { QFileInfo fi(directory.path() + QLatin1Char('/') + cbpFile); if (t.isNull() || fi.lastModified() > t) { file = directory.path() + QLatin1Char('/') + cbpFile; t = fi.lastModified(); } } } return file; } void TeaLeafReader::parse(bool forceCMakeRun, bool forceConfiguration) { emit configurationStarted(); const QString cbpFile = findCbpFile(QDir(m_parameters.workDirectory.toString())); const QFileInfo cbpFileFi = cbpFile.isEmpty() ? QFileInfo() : QFileInfo(cbpFile); if (!cbpFileFi.exists() || forceConfiguration) { // Initial create: startCMake(CMakeProcess::toArguments(m_parameters.configuration, m_parameters.expander)); return; } const bool mustUpdate = forceCMakeRun || m_cmakeFiles.isEmpty() || anyOf(m_cmakeFiles, [&cbpFileFi](const FilePath &f) { return f.toFileInfo().lastModified() > cbpFileFi.lastModified(); }); if (mustUpdate) { startCMake(QStringList()); } else { extractData(); emit dataAvailable(); } } void TeaLeafReader::stop() { m_cmakeProcess.reset(); } bool TeaLeafReader::isParsing() const { return m_cmakeProcess && m_cmakeProcess->state() != QProcess::NotRunning; } QVector TeaLeafReader::takeProjectFilesToWatch() { return transform(m_cmakeFiles, [](const FilePath &p) { return p; }); } QList TeaLeafReader::takeBuildTargets(QString &errorMessage) { Q_UNUSED(errorMessage) return m_buildTargets; } CMakeConfig TeaLeafReader::takeParsedConfiguration(QString &errorMessage) { const FilePath cacheFile = m_parameters.workDirectory.pathAppended("CMakeCache.txt"); if (!cacheFile.exists()) return { }; CMakeConfig result = BuildDirManager::parseCMakeConfiguration(cacheFile, &errorMessage); if (!errorMessage.isEmpty()) { return { }; } const FilePath sourceOfBuildDir = FilePath::fromUtf8(CMakeConfigItem::valueOf("CMAKE_HOME_DIRECTORY", result)); const FilePath canonicalSourceOfBuildDir = sourceOfBuildDir.canonicalPath(); const FilePath canonicalSourceDirectory = m_parameters.sourceDirectory.canonicalPath(); if (canonicalSourceOfBuildDir != canonicalSourceDirectory) { // Uses case-insensitive compare where appropriate errorMessage = tr("The build directory is not for %1 but for %2") .arg(canonicalSourceOfBuildDir.toUserOutput(), canonicalSourceDirectory.toUserOutput()); return { }; } return result; } std::unique_ptr TeaLeafReader::generateProjectTree( const QList &allFiles, QString &errorMessage) { Q_UNUSED(errorMessage) if (m_files.size() == 0) return {}; auto root = std::make_unique(m_parameters.sourceDirectory); root->setDisplayName(m_projectName); QSet allIncludePathSet; for (const CMakeBuildTarget &bt : m_buildTargets) { const QList targetIncludePaths = Utils::filtered(bt.includeFiles, [this](const Utils::FilePath &fn) { return fn.isChildOf(m_parameters.sourceDirectory); }); allIncludePathSet.unite(Utils::toSet(targetIncludePaths)); } const QList allIncludePaths = Utils::toList(allIncludePathSet); const QList missingHeaders = Utils::filtered(allFiles, [&allIncludePaths](const FileNode *fn) -> bool { if (fn->fileType() != FileType::Header) return false; return Utils::contains(allIncludePaths, [fn](const FilePath &inc) { return fn->filePath().isChildOf(inc); }); }); // filter duplicates: auto alreadySeen = Utils::transform(m_files, &FileNode::filePath); const QList unseenMissingHeaders = Utils::filtered(missingHeaders, [&alreadySeen](const FileNode *fn) { const int count = alreadySeen.count(); alreadySeen.insert(fn->filePath()); return (alreadySeen.count() != count); }); root->addNestedNodes(std::move(m_files), m_parameters.sourceDirectory); std::vector> fileNodes = transform(unseenMissingHeaders, [](const FileNode *fn) { return std::unique_ptr(fn->clone()); }); root->addNestedNodes(std::move(fileNodes), m_parameters.sourceDirectory); return root; } static void processCMakeIncludes(const CMakeBuildTarget &cbt, const ToolChain *tc, const QStringList& flags, const FilePath &sysroot, QSet &tcIncludes, QStringList &includePaths) { if (!tc) return; foreach (const HeaderPath &hp, tc->builtInHeaderPaths(flags, sysroot, Environment::systemEnvironment())) { tcIncludes.insert(FilePath::fromString(hp.path)); } foreach (const FilePath &i, cbt.includeFiles) { if (!tcIncludes.contains(i)) includePaths.append(i.toString()); } } RawProjectParts TeaLeafReader::createRawProjectParts(QString &errorMessage) { Q_UNUSED(errorMessage) const ToolChain *tcCxx = ToolChainManager::findToolChain(m_parameters.cxxToolChainId); const ToolChain *tcC = ToolChainManager::findToolChain(m_parameters.cToolChainId); const FilePath sysroot = m_parameters.sysRoot; RawProjectParts rpps; QHash targetDataCacheCxx; QHash targetDataCacheC; foreach (const CMakeBuildTarget &cbt, m_buildTargets) { if (cbt.targetType == UtilityType) continue; // CMake shuffles the include paths that it reports via the CodeBlocks generator // So remove the toolchain include paths, so that at least those end up in the correct // place. auto cxxflags = getFlagsFor(cbt, targetDataCacheCxx, ProjectExplorer::Constants::CXX_LANGUAGE_ID); auto cflags = getFlagsFor(cbt, targetDataCacheC, ProjectExplorer::Constants::C_LANGUAGE_ID); QSet tcIncludes; QStringList includePaths; if (tcCxx || tcC) { processCMakeIncludes(cbt, tcCxx, cxxflags, sysroot, tcIncludes, includePaths); processCMakeIncludes(cbt, tcC, cflags, sysroot, tcIncludes, includePaths); } else { includePaths = transform(cbt.includeFiles, &FilePath::toString); } includePaths += m_parameters.workDirectory.toString(); RawProjectPart rpp; rpp.setProjectFileLocation(cbt.sourceDirectory.toString() + "/CMakeLists.txt"); rpp.setBuildSystemTarget(cbt.title); rpp.setIncludePaths(includePaths); RawProjectPartFlags cProjectFlags; cProjectFlags.commandLineFlags = cflags; rpp.setFlagsForC(cProjectFlags); RawProjectPartFlags cxxProjectFlags; cxxProjectFlags.commandLineFlags = cxxflags; rpp.setFlagsForCxx(cxxProjectFlags); rpp.setMacros(cbt.macros); rpp.setDisplayName(cbt.title); rpp.setFiles(transform(cbt.files, &FilePath::toString)); const bool isExecutable = cbt.targetType == ExecutableType; rpp.setBuildTargetType(isExecutable ? ProjectExplorer::BuildTargetType::Executable : ProjectExplorer::BuildTargetType::Library); rpps.append(rpp); } return rpps; } void TeaLeafReader::extractData() { CMakeTool *cmake = m_parameters.cmakeTool(); QTC_ASSERT(m_parameters.isValid() && cmake, return); const FilePath srcDir = m_parameters.sourceDirectory; const FilePath bldDir = m_parameters.workDirectory; const FilePath topCMake = srcDir.pathAppended("CMakeLists.txt"); resetData(); m_projectName = m_parameters.projectName; m_files.emplace_back(std::make_unique(topCMake, FileType::Project)); // Do not insert topCMake into m_cmakeFiles: The project already watches that! // Find cbp file FilePath cbpFile = FilePath::fromString(findCbpFile(bldDir.toString())); if (cbpFile.isEmpty()) return; m_cmakeFiles.insert(cbpFile); // Add CMakeCache.txt file: const FilePath cacheFile = m_parameters.workDirectory.pathAppended("CMakeCache.txt"); if (cacheFile.exists()) m_cmakeFiles.insert(cacheFile); // setFolderName CMakeCbpParser cbpparser; // Parsing if (!cbpparser.parseCbpFile(cmake->pathMapper(), cbpFile, srcDir)) return; m_projectName = cbpparser.projectName(); m_files = cbpparser.takeFileList(); if (cbpparser.hasCMakeFiles()) { std::vector> cmakeNodes = cbpparser.takeCmakeFileList(); for (const std::unique_ptr &node : cmakeNodes) m_cmakeFiles.insert(node->filePath()); std::move(std::begin(cmakeNodes), std::end(cmakeNodes), std::back_inserter(m_files)); } // Make sure the top cmakelists.txt file is always listed: if (!contains(m_files, [topCMake](const std::unique_ptr &fn) { return fn->filePath() == topCMake; })) m_files.emplace_back(std::make_unique(topCMake, FileType::Project)); m_buildTargets = cbpparser.buildTargets(); } void TeaLeafReader::startCMake(const QStringList &configurationArguments) { QTC_ASSERT(!m_cmakeProcess, return); m_cmakeProcess = std::make_unique(); connect(m_cmakeProcess.get(), &CMakeProcess::finished, this, &TeaLeafReader::cmakeFinished); m_cmakeProcess->run(m_parameters, configurationArguments); } void TeaLeafReader::cmakeFinished(int code, QProcess::ExitStatus status) { Q_UNUSED(code) Q_UNUSED(status) QTC_ASSERT(m_cmakeProcess, return); m_cmakeProcess.reset(); extractData(); // try even if cmake failed... emit dataAvailable(); } QStringList TeaLeafReader::getFlagsFor(const CMakeBuildTarget &buildTarget, QHash &cache, Id lang) const { // check cache: auto it = cache.constFind(buildTarget.title); if (it != cache.constEnd()) return *it; if (extractFlagsFromMake(buildTarget, cache, lang)) return cache.value(buildTarget.title); if (extractFlagsFromNinja(buildTarget, cache, lang)) return cache.value(buildTarget.title); cache.insert(buildTarget.title, QStringList()); return QStringList(); } bool TeaLeafReader::extractFlagsFromMake(const CMakeBuildTarget &buildTarget, QHash &cache, Id lang) const { QString flagsPrefix; if (lang == ProjectExplorer::Constants::CXX_LANGUAGE_ID) flagsPrefix = QLatin1String("CXX_FLAGS ="); else if (lang == ProjectExplorer::Constants::C_LANGUAGE_ID) flagsPrefix = QLatin1String("C_FLAGS ="); else return false; QString makeCommand = buildTarget.makeCommand.toString(); int startIndex = makeCommand.indexOf('\"'); int endIndex = makeCommand.indexOf('\"', startIndex + 1); if (startIndex != -1 && endIndex != -1) { startIndex += 1; QString makefile = makeCommand.mid(startIndex, endIndex - startIndex); int slashIndex = makefile.lastIndexOf('/'); makefile.truncate(slashIndex); makefile.append("/CMakeFiles/" + buildTarget.title + ".dir/flags.make"); // Remove un-needed shell escaping: makefile = makefile.remove("\\"); QFile file(makefile); if (file.exists()) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return false; QTextStream stream(&file); while (!stream.atEnd()) { QString line = stream.readLine().trimmed(); if (line.startsWith(flagsPrefix)) { // Skip past = auto flags = Utils::transform(line.mid(flagsPrefix.length()).trimmed().split(' ', QString::SkipEmptyParts), [this](QString flag) -> QString { // TODO: maybe Gcc-specific // Remove \' (quote) for function-style macrosses: // -D'MACRO()'=xxx // -D'MACRO()=xxx' // -D'MACRO()' // otherwise, compiler will fails return flag .replace(m_macroFixupRe1, "-D\\1\\2=") .replace(m_macroFixupRe2, "-D\\1\\2=\\3") .replace(m_macroFixupRe3, "-D\\1\\2"); }); cache.insert(buildTarget.title, flags); return true; } } } } return false; } bool TeaLeafReader::extractFlagsFromNinja(const CMakeBuildTarget &buildTarget, QHash &cache, Id lang) const { Q_UNUSED(buildTarget) if (!cache.isEmpty()) // We fill the cache in one go! return false; QString compilerPrefix; if (lang == ProjectExplorer::Constants::CXX_LANGUAGE_ID) compilerPrefix = QLatin1String("CXX_COMPILER"); else if (lang == ProjectExplorer::Constants::C_LANGUAGE_ID) compilerPrefix = QLatin1String("C_COMPILER"); else return false; // Attempt to find build.ninja file and obtain FLAGS (CXX_FLAGS/C_FLAGS) from there if no suitable flags.make were // found // Get "all" target's working directory QByteArray ninjaFile; QString buildNinjaFile = m_buildTargets.at(0).workingDirectory.toString(); buildNinjaFile += "/build.ninja"; QFile buildNinja(buildNinjaFile); if (buildNinja.exists()) { if (!buildNinja.open(QIODevice::ReadOnly | QIODevice::Text)) return false; ninjaFile = buildNinja.readAll(); buildNinja.close(); } if (ninjaFile.isEmpty()) return false; QTextStream stream(ninjaFile); bool compilerFound = false; const QString targetSignature = "# Object build statements for "; QString currentTarget; while (!stream.atEnd()) { // 1. Look for a block that refers to the current target // 2. Look for a build rule which invokes CXX_COMPILER // 3. Return the FLAGS definition QString line = stream.readLine().trimmed(); if (line.startsWith('#')) { if (line.startsWith(targetSignature)) { int pos = line.lastIndexOf(' '); currentTarget = line.mid(pos + 1); } } else if (!currentTarget.isEmpty() && line.startsWith("build")) { compilerFound = line.indexOf(compilerPrefix) != -1; } else if (compilerFound && line.startsWith("FLAGS =")) { // Skip past = cache.insert(currentTarget, line.mid(7).trimmed().split(' ', QString::SkipEmptyParts)); } } return !cache.isEmpty(); } } // namespace Internal } // namespace CMakeProjectManager