/**************************************************************************** ** ** 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 "cmakeproject.h" #include "cmakebuildconfiguration.h" #include "cmakebuildstep.h" #include "cmakekitinformation.h" #include "cmakeprojectconstants.h" #include "cmakeprojectnodes.h" #include "cmakerunconfiguration.h" #include "cmakeopenprojectwizard.h" #include "cmakecbpparser.h" #include "cmakefile.h" #include "cmakeprojectmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace CMakeProjectManager; using namespace CMakeProjectManager::Internal; using namespace ProjectExplorer; using namespace Utils; // QtCreator CMake Generator wishlist: // Which make targets we need to build to get all executables // What is the make we need to call // What is the actual compiler executable // DEFINES // Open Questions // Who sets up the environment for cl.exe ? INCLUDEPATH and so on /*! \class CMakeProject */ CMakeProject::CMakeProject(CMakeManager *manager, const FileName &fileName) : m_watcher(new QFileSystemWatcher(this)) { setId(Constants::CMAKEPROJECT_ID); setProjectManager(manager); setDocument(new CMakeFile(fileName)); setRootProjectNode(new CMakeProjectNode(fileName)); setProjectContext(Core::Context(CMakeProjectManager::Constants::PROJECTCONTEXT)); setProjectLanguages(Core::Context(ProjectExplorer::Constants::LANG_CXX)); rootProjectNode()->setDisplayName(fileName.parentDir().fileName()); connect(this, &CMakeProject::buildTargetsChanged, this, &CMakeProject::updateRunConfigurations); connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &CMakeProject::fileChanged); } CMakeProject::~CMakeProject() { m_codeModelFuture.cancel(); } void CMakeProject::fileChanged(const QString &fileName) { Q_UNUSED(fileName) parseCMakeLists(); } void CMakeProject::changeActiveBuildConfiguration(ProjectExplorer::BuildConfiguration *bc) { if (!bc) return; CMakeBuildConfiguration *cmakebc = static_cast(bc); // Pop up a dialog asking the user to rerun cmake QString cbpFile = CMakeManager::findCbpFile(QDir(bc->buildDirectory().toString())); QFileInfo cbpFileFi(cbpFile); CMakeOpenProjectWizard::Mode mode = CMakeOpenProjectWizard::Nothing; if (!cbpFileFi.exists()) { mode = CMakeOpenProjectWizard::NeedToCreate; } else { foreach (const FileName &file, m_watchedFiles) { if (file.toFileInfo().lastModified() > cbpFileFi.lastModified()) { mode = CMakeOpenProjectWizard::NeedToUpdate; break; } } } if (mode != CMakeOpenProjectWizard::Nothing) { CMakeBuildInfo info(cmakebc); CMakeOpenProjectWizard copw(Core::ICore::mainWindow(), mode, &info); if (copw.exec() == QDialog::Accepted) cmakebc->setInitialArguments(QString()); } // reparse parseCMakeLists(); } void CMakeProject::activeTargetWasChanged(Target *target) { if (m_activeTarget) { disconnect(m_activeTarget, &Target::activeBuildConfigurationChanged, this, &CMakeProject::changeActiveBuildConfiguration); } m_activeTarget = target; if (!m_activeTarget) return; connect(m_activeTarget, &Target::activeBuildConfigurationChanged, this, &CMakeProject::changeActiveBuildConfiguration); changeActiveBuildConfiguration(m_activeTarget->activeBuildConfiguration()); } void CMakeProject::changeBuildDirectory(CMakeBuildConfiguration *bc, const QString &newBuildDirectory) { bc->setBuildDirectory(FileName::fromString(newBuildDirectory)); parseCMakeLists(); } QStringList CMakeProject::getCXXFlagsFor(const CMakeBuildTarget &buildTarget, QByteArray *cachedBuildNinja) { QString makeCommand = QDir::fromNativeSeparators(buildTarget.makeCommand); int startIndex = makeCommand.indexOf(QLatin1Char('\"')); int endIndex = makeCommand.indexOf(QLatin1Char('\"'), startIndex + 1); if (startIndex != -1 && endIndex != -1) { startIndex += 1; QString makefile = makeCommand.mid(startIndex, endIndex - startIndex); int slashIndex = makefile.lastIndexOf(QLatin1Char('/')); makefile.truncate(slashIndex); makefile.append(QLatin1String("/CMakeFiles/") + buildTarget.title + QLatin1String(".dir/flags.make")); QFile file(makefile); if (file.exists()) { file.open(QIODevice::ReadOnly | QIODevice::Text); QTextStream stream(&file); while (!stream.atEnd()) { QString line = stream.readLine().trimmed(); if (line.startsWith(QLatin1String("CXX_FLAGS ="))) { // Skip past = return line.mid(11).trimmed().split(QLatin1Char(' '), QString::SkipEmptyParts); } } } } // Attempt to find build.ninja file and obtain FLAGS (CXX_FLAGS) from there if no suitable flags.make were // found // Get "all" target's working directory if (!buildTargets().empty()) { if (cachedBuildNinja->isNull()) { QString buildNinjaFile = QDir::fromNativeSeparators(buildTargets().at(0).workingDirectory); buildNinjaFile += QLatin1String("/build.ninja"); QFile buildNinja(buildNinjaFile); if (buildNinja.exists()) { buildNinja.open(QIODevice::ReadOnly | QIODevice::Text); *cachedBuildNinja = buildNinja.readAll(); buildNinja.close(); } else { *cachedBuildNinja = QByteArray(); } } if (cachedBuildNinja->isEmpty()) return QStringList(); QTextStream stream(cachedBuildNinja); bool targetFound = false; bool cxxFound = false; QString targetSearchPattern = QString::fromLatin1("target %1").arg(buildTarget.title); 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(QLatin1String("#"))) { if (!line.startsWith(QLatin1String("# Object build statements for"))) continue; targetFound = line.endsWith(targetSearchPattern); } else if (targetFound && line.startsWith(QLatin1String("build"))) { cxxFound = line.indexOf(QLatin1String("CXX_COMPILER")) != -1; } else if (cxxFound && line.startsWith(QLatin1String("FLAGS ="))) { // Skip past = return line.mid(7).trimmed().split(QLatin1Char(' '), QString::SkipEmptyParts); } } } return QStringList(); } bool CMakeProject::parseCMakeLists() { if (!activeTarget() || !activeTarget()->activeBuildConfiguration()) { return false; } CMakeBuildConfiguration *activeBC = static_cast(activeTarget()->activeBuildConfiguration()); foreach (Core::IDocument *document, Core::DocumentModel::openedDocuments()) if (isProjectFile(document->filePath())) document->infoBar()->removeInfo("CMakeEditor.RunCMake"); // Find cbp file QString cbpFile = CMakeManager::findCbpFile(activeBC->buildDirectory().toString()); if (cbpFile.isEmpty()) { emit buildTargetsChanged(); return false; } Kit *k = activeTarget()->kit(); // setFolderName rootProjectNode()->setDisplayName(QFileInfo(cbpFile).completeBaseName()); CMakeCbpParser cbpparser; // Parsing //qDebug()<<"Parsing file "<files()) if (file != cbpFile) m_watcher->removePath(file); // how can we ensure that it is completely written? m_watcher->addPath(cbpFile); rootProjectNode()->setDisplayName(cbpparser.projectName()); //qDebug()<<"Building Tree"; QList fileList = cbpparser.fileList(); QSet projectFiles; if (cbpparser.hasCMakeFiles()) { fileList.append(cbpparser.cmakeFileList()); foreach (const ProjectExplorer::FileNode *node, cbpparser.cmakeFileList()) projectFiles.insert(node->filePath()); } else { // Manually add the CMakeLists.txt file FileName cmakeListTxt = projectDirectory().appendPath(QLatin1String("CMakeLists.txt")); bool generated = false; fileList.append(new ProjectExplorer::FileNode(cmakeListTxt, ProjectExplorer::ProjectFileType, generated)); projectFiles.insert(cmakeListTxt); } m_watchedFiles = projectFiles; m_files.clear(); foreach (ProjectExplorer::FileNode *fn, fileList) m_files.append(fn->filePath().toString()); m_files.sort(); buildTree(static_cast(rootProjectNode()), fileList); //qDebug()<<"Adding Targets"; m_buildTargets = cbpparser.buildTargets(); // qDebug()<<"Printing targets"; // foreach (CMakeBuildTarget ct, m_buildTargets) { // qDebug()<qtVersion() < QtSupport::QtVersionNumber(5,0,0)) activeQtVersion = CppTools::ProjectPart::Qt4; else activeQtVersion = CppTools::ProjectPart::Qt5; } ppBuilder.setQtVersion(activeQtVersion); QByteArray cachedBuildNinja; foreach (const CMakeBuildTarget &cbt, m_buildTargets) { // This explicitly adds -I. to the include paths QStringList includePaths = cbt.includeFiles; includePaths += projectDirectory().toString(); ppBuilder.setIncludePaths(includePaths); QStringList cxxflags = getCXXFlagsFor(cbt, &cachedBuildNinja); ppBuilder.setCFlags(cxxflags); ppBuilder.setCxxFlags(cxxflags); ppBuilder.setDefines(cbt.defines); ppBuilder.setDisplayName(cbt.title); const QList languages = ppBuilder.createProjectPartsForFiles(cbt.files); foreach (Core::Id language, languages) setProjectLanguage(language, true); } m_codeModelFuture.cancel(); pinfo.finish(); m_codeModelFuture = modelmanager->updateProjectInfo(pinfo); emit displayNameChanged(); emit buildTargetsChanged(); emit fileListChanged(); emit activeBC->emitBuildTypeChanged(); return true; } bool CMakeProject::needsConfiguration() const { return targets().isEmpty(); } bool CMakeProject::requiresTargetPanel() const { return !targets().isEmpty(); } bool CMakeProject::supportsKit(Kit *k, QString *errorMessage) const { if (!CMakeKitInformation::cmakeTool(k)) { if (errorMessage) *errorMessage = tr("No cmake tool set."); return false; } return true; } bool CMakeProject::isProjectFile(const FileName &fileName) { return m_watchedFiles.contains(fileName); } QList CMakeProject::buildTargets() const { return m_buildTargets; } QStringList CMakeProject::buildTargetTitles(bool runnable) const { const QList targets = runnable ? Utils::filtered(m_buildTargets, [](const CMakeBuildTarget &ct) { return !ct.executable.isEmpty() && ct.targetType == ExecutableType; }) : m_buildTargets; return Utils::transform(targets, [](const CMakeBuildTarget &ct) { return ct.title; }); } bool CMakeProject::hasBuildTarget(const QString &title) const { return Utils::anyOf(m_buildTargets, [title](const CMakeBuildTarget &ct) { return ct.title == title; }); } void CMakeProject::gatherFileNodes(ProjectExplorer::FolderNode *parent, QList &list) { foreach (ProjectExplorer::FolderNode *folder, parent->subFolderNodes()) gatherFileNodes(folder, list); foreach (ProjectExplorer::FileNode *file, parent->fileNodes()) list.append(file); } bool sortNodesByPath(Node *a, Node *b) { return a->filePath() < b->filePath(); } void CMakeProject::buildTree(CMakeProjectNode *rootNode, QList newList) { // Gather old list QList oldList; gatherFileNodes(rootNode, oldList); Utils::sort(oldList, sortNodesByPath); Utils::sort(newList, sortNodesByPath); QList added; QList deleted; ProjectExplorer::compareSortedLists(oldList, newList, deleted, added, sortNodesByPath); qDeleteAll(ProjectExplorer::subtractSortedList(newList, added, sortNodesByPath)); // add added nodes foreach (ProjectExplorer::FileNode *fn, added) { // qDebug()<<"added"<path(); // Get relative path to rootNode QString parentDir = fn->filePath().toFileInfo().absolutePath(); ProjectExplorer::FolderNode *folder = findOrCreateFolder(rootNode, parentDir); folder->addFileNodes(QList()<< fn); } // remove old file nodes and check whether folder nodes can be removed foreach (ProjectExplorer::FileNode *fn, deleted) { ProjectExplorer::FolderNode *parent = fn->parentFolderNode(); // qDebug()<<"removed"<path(); parent->removeFileNodes(QList() << fn); // Check for empty parent while (parent->subFolderNodes().isEmpty() && parent->fileNodes().isEmpty()) { ProjectExplorer::FolderNode *grandparent = parent->parentFolderNode(); grandparent->removeFolderNodes(QList() << parent); parent = grandparent; if (parent == rootNode) break; } } } ProjectExplorer::FolderNode *CMakeProject::findOrCreateFolder(CMakeProjectNode *rootNode, QString directory) { FileName path = rootNode->filePath().parentDir(); QDir rootParentDir(path.toString()); QString relativePath = rootParentDir.relativeFilePath(directory); if (relativePath == QLatin1String(".")) relativePath.clear(); QStringList parts = relativePath.split(QLatin1Char('/'), QString::SkipEmptyParts); ProjectExplorer::FolderNode *parent = rootNode; foreach (const QString &part, parts) { path.appendPath(part); // Find folder in subFolders bool found = false; foreach (ProjectExplorer::FolderNode *folder, parent->subFolderNodes()) { if (folder->filePath() == path) { // yeah found something :) parent = folder; found = true; break; } } if (!found) { // No FolderNode yet, so create it auto tmp = new ProjectExplorer::FolderNode(path); tmp->setDisplayName(part); parent->addFolderNodes(QList() << tmp); parent = tmp; } } return parent; } QString CMakeProject::displayName() const { return rootProjectNode()->displayName(); } QStringList CMakeProject::files(FilesMode fileMode) const { Q_UNUSED(fileMode) return m_files; } Project::RestoreResult CMakeProject::fromMap(const QVariantMap &map, QString *errorMessage) { RestoreResult result = Project::fromMap(map, errorMessage); if (result != RestoreResult::Ok) return result; bool hasUserFile = activeTarget(); if (!hasUserFile) { // Nothing to do, the target setup page will show up } else { // We have a user file, but we could still be missing the cbp file // or simply run createXml with the saved settings CMakeBuildConfiguration *activeBC = qobject_cast(activeTarget()->activeBuildConfiguration()); if (!activeBC) { *errorMessage = tr("Internal Error: No build configuration found in settings file."); return RestoreResult::Error; } QString cbpFile = CMakeManager::findCbpFile(QDir(activeBC->buildDirectory().toString())); QFileInfo cbpFileFi(cbpFile); CMakeOpenProjectWizard::Mode mode = CMakeOpenProjectWizard::Nothing; if (!cbpFileFi.exists()) mode = CMakeOpenProjectWizard::NeedToCreate; else if (cbpFileFi.lastModified() < projectFilePath().toFileInfo().lastModified()) mode = CMakeOpenProjectWizard::NeedToUpdate; if (mode != CMakeOpenProjectWizard::Nothing) { CMakeBuildInfo info(activeBC); CMakeOpenProjectWizard copw(Core::ICore::mainWindow(), mode, &info); if (copw.exec() != QDialog::Accepted) return RestoreResult::UserAbort; else activeBC->setInitialArguments(QString()); } } parseCMakeLists(); m_activeTarget = activeTarget(); if (m_activeTarget) connect(m_activeTarget, &Target::activeBuildConfigurationChanged, this, &CMakeProject::changeActiveBuildConfiguration); connect(this, &Project::activeTargetChanged, this, &CMakeProject::activeTargetWasChanged); return RestoreResult::Ok; } bool CMakeProject::setupTarget(Target *t) { t->updateDefaultBuildConfigurations(); if (t->buildConfigurations().isEmpty()) return false; t->updateDefaultDeployConfigurations(); return true; } CMakeBuildTarget CMakeProject::buildTargetForTitle(const QString &title) { foreach (const CMakeBuildTarget &ct, m_buildTargets) if (ct.title == title) return ct; return CMakeBuildTarget(); } QString CMakeProject::uiHeaderFile(const QString &uiFile) { if (!activeTarget()) return QString(); QFileInfo fi(uiFile); FileName project = projectDirectory(); FileName baseDirectory = FileName::fromString(fi.absolutePath()); while (baseDirectory.isChildOf(project)) { FileName cmakeListsTxt = baseDirectory; cmakeListsTxt.appendPath(QLatin1String("CMakeLists.txt")); if (cmakeListsTxt.exists()) break; QDir dir(baseDirectory.toString()); dir.cdUp(); baseDirectory = FileName::fromString(dir.absolutePath()); } QDir srcDirRoot = QDir(project.toString()); QString relativePath = srcDirRoot.relativeFilePath(baseDirectory.toString()); QDir buildDir = QDir(activeTarget()->activeBuildConfiguration()->buildDirectory().toString()); QString uiHeaderFilePath = buildDir.absoluteFilePath(relativePath); uiHeaderFilePath += QLatin1String("/ui_"); uiHeaderFilePath += fi.completeBaseName(); uiHeaderFilePath += QLatin1String(".h"); return QDir::cleanPath(uiHeaderFilePath); } void CMakeProject::updateRunConfigurations() { foreach (Target *t, targets()) updateTargetRunConfigurations(t); } // TODO Compare with updateDefaultRunConfigurations(); void CMakeProject::updateTargetRunConfigurations(Target *t) { // create new and remove obsolete RCs using the factories t->updateDefaultRunConfigurations(); // *Update* runconfigurations: QMultiMap existingRunConfigurations; foreach (ProjectExplorer::RunConfiguration *rc, t->runConfigurations()) { if (CMakeRunConfiguration* cmakeRC = qobject_cast(rc)) existingRunConfigurations.insert(cmakeRC->title(), cmakeRC); } foreach (const CMakeBuildTarget &ct, buildTargets()) { if (ct.targetType != ExecutableType) continue; if (ct.executable.isEmpty()) continue; QList list = existingRunConfigurations.values(ct.title); if (!list.isEmpty()) { // Already exists, so override the settings... foreach (CMakeRunConfiguration *rc, list) { rc->setExecutable(ct.executable); rc->setBaseWorkingDirectory(ct.workingDirectory); rc->setEnabled(true); } } } if (t->runConfigurations().isEmpty()) { // Oh no, no run configuration, // create a custom executable run configuration t->addRunConfiguration(new QtSupport::CustomExecutableRunConfiguration(t)); } } void CMakeProject::updateApplicationAndDeploymentTargets() { Target *t = activeTarget(); if (!t) return; QFile deploymentFile; QTextStream deploymentStream; QString deploymentPrefix; QDir sourceDir(t->project()->projectDirectory().toString()); QDir buildDir(t->activeBuildConfiguration()->buildDirectory().toString()); deploymentFile.setFileName(sourceDir.filePath(QLatin1String("QtCreatorDeployment.txt"))); // If we don't have a global QtCreatorDeployment.txt check for one created by the active build configuration if (!deploymentFile.exists()) deploymentFile.setFileName(buildDir.filePath(QLatin1String("QtCreatorDeployment.txt"))); if (deploymentFile.open(QFile::ReadOnly | QFile::Text)) { deploymentStream.setDevice(&deploymentFile); deploymentPrefix = deploymentStream.readLine(); if (!deploymentPrefix.endsWith(QLatin1Char('/'))) deploymentPrefix.append(QLatin1Char('/')); } BuildTargetInfoList appTargetList; DeploymentData deploymentData; foreach (const CMakeBuildTarget &ct, m_buildTargets) { if (ct.executable.isEmpty()) continue; if (ct.targetType == ExecutableType || ct.targetType == DynamicLibraryType) deploymentData.addFile(ct.executable, deploymentPrefix + buildDir.relativeFilePath(QFileInfo(ct.executable).dir().path()), DeployableFile::TypeExecutable); if (ct.targetType == ExecutableType) { // TODO: Put a path to corresponding .cbp file into projectFilePath? appTargetList.list << BuildTargetInfo(ct.title, FileName::fromString(ct.executable), FileName::fromString(ct.executable)); } } QString absoluteSourcePath = sourceDir.absolutePath(); if (!absoluteSourcePath.endsWith(QLatin1Char('/'))) absoluteSourcePath.append(QLatin1Char('/')); if (deploymentStream.device()) { while (!deploymentStream.atEnd()) { QString line = deploymentStream.readLine(); if (!line.contains(QLatin1Char(':'))) continue; QStringList file = line.split(QLatin1Char(':')); deploymentData.addFile(absoluteSourcePath + file.at(0), deploymentPrefix + file.at(1)); } } t->setApplicationTargets(appTargetList); t->setDeploymentData(deploymentData); } void CMakeProject::createUiCodeModelSupport() { QHash uiFileHash; // Find all ui files foreach (const QString &uiFile, m_files) { if (uiFile.endsWith(QLatin1String(".ui"))) uiFileHash.insert(uiFile, uiHeaderFile(uiFile)); } QtSupport::UiCodeModelManager::update(this, uiFileHash); } void CMakeBuildTarget::clear() { executable.clear(); makeCommand.clear(); makeCleanCommand.clear(); workingDirectory.clear(); sourceDirectory.clear(); title.clear(); targetType = ExecutableType; includeFiles.clear(); compilerOptions.clear(); defines.clear(); }