/* KDevelop CMake Support * * Copyright 2013 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "cmakecommitchangesjob.h" #include "cmakeprojectdata.h" #include "testing/ctestutils.h" #include "cmakemodelitems.h" #include "cmakeutils.h" #include "cmakemanager.h" #include #include #include #include #include using namespace KDevelop; static ProjectFileItem* containsFile(const Path& file, const QList& tfiles) { foreach(ProjectFileItem* f, tfiles) { if(f->path()==file) return f; } return 0; } static QStringList resolvePaths(const Path& base, const QStringList& pathsToResolve) { QStringList resolvedPaths; resolvedPaths.reserve(pathsToResolve.size()); foreach(const QString& pathToResolve, pathsToResolve) { QString dir(pathToResolve); if(!pathToResolve.startsWith("#[") && !pathToResolve.startsWith("$<")) { dir = Path(base, pathToResolve).toLocalFile(); } resolvedPaths.append(dir); } return resolvedPaths; } static QSet filterFiles(const QFileInfoList& orig, const Path& base, IProject* project, ProjectFilterManager* filter) { QSet ret; ret.reserve(orig.size()); foreach(const QFileInfo& info, orig) { const QString str = info.fileName(); const Path path(base, str); if (!filter->isValid(path, info.isDir(), project)) { continue; } ret.insert(str); } return ret; } static bool isCorrectFolder(const Path& path, IProject* p) { const QString cache = Path(path, "CMakeCache.txt").toLocalFile(); bool ret = !QFile::exists(cache); ret &= !CMake::allBuildDirs(p).contains(path.toLocalFile()); return ret; } template static bool textInList(const QVector& list, KDevelop::ProjectBaseItem* item) { foreach(const T& s, list) { if(item->text()==s.name) return true; } return false; } static QList cleanupBuildFolders(CMakeFolderItem* item, const QVector& subs) { QList ret; QList folders = item->folderList(); foreach(KDevelop::ProjectFolderItem* folder, folders) { CMakeFolderItem* cmfolder = dynamic_cast(folder); if(cmfolder && cmfolder->formerParent()==item && !textInList(subs, folder)) ret += folder; } return ret; } ///////////////////////////////////////// CMakeCommitChangesJob::CMakeCommitChangesJob(const Path& path, CMakeManager* manager, KDevelop::IProject* project) : KJob() , m_path(path) , m_project(project) , m_manager(manager) , m_projectDataAdded(false) , m_parentItem(0) , m_waiting(false) , m_findParent(true) { setObjectName(path.pathOrUrl()); } static void processDependencies(ProcessedTarget &target, const QString& dep, const CMakeProjectData& data, QSet& alreadyProcessed) { if(dep.isEmpty() || alreadyProcessed.contains(dep)) return; alreadyProcessed.insert(dep); // kDebug() << "processing..." << target.target.name << dep; QMap depData = data.properties.value(TargetProperty).value(dep); if(depData.isEmpty()) { kDebug() << "error: couldn't find dependency " << dep << data.properties.value(TargetProperty).keys(); return; } target.includes += depData["INTERFACE_INCLUDE_DIRECTORIES"]; target.defines += depData["INTERFACE_COMPILE_DEFINITIONS"]; foreach(const QString& d, depData["INTERFACE_LINK_LIBRARIES"]) processDependencies(target, d, data, alreadyProcessed); } Path::List CMakeCommitChangesJob::addProjectData(const CMakeProjectData& data) { m_projectDataAdded = true; Path::List ret; m_tests = data.testSuites; QSet alreadyAdded; foreach(const Subdirectory& subf, data.subdirectories) { if(subf.name.isEmpty() || alreadyAdded.contains(subf.name)) //empty case would not be necessary if we didn't process the wrong lines continue; alreadyAdded.insert(subf.name); m_subdirectories += subf; ret += Path(m_path, subf.name); } QString dir = m_path.toLocalFile(); if(data.vm.value("CMAKE_INCLUDE_CURRENT_DIR")==QStringList("ON")) { m_directories += dir; m_directories += CMakeParserUtils::binaryPath(dir, m_project->path().toLocalFile(), CMake::currentBuildDir(m_project).toLocalFile(KUrl::RemoveTrailingSlash)); } m_directories += resolvePaths(m_path, data.properties[DirectoryProperty][dir]["INCLUDE_DIRECTORIES"]); m_directories.removeAll(QString()); m_definitions.unite(data.definitions); CMakeParserUtils::addDefinitions(data.properties[DirectoryProperty][dir]["COMPILE_DEFINITIONS"], &m_definitions); CMakeParserUtils::addDefinitions(data.vm["CMAKE_CXX_FLAGS"], &m_definitions, true); foreach(const Target& t, data.targets) { const QMap& targetProps = data.properties[TargetProperty][t.name]; if(targetProps["FOLDER"]==QStringList("CTestDashboardTargets")) continue; //filter some annoying targets if (!m_manager->filterManager()->isValid(Path(m_path, t.name), false, m_project)) { continue; } ProcessedTarget target; target.target = t; target.defines = targetProps["COMPILE_DEFINITIONS"]; target.includes = targetProps["INCLUDE_DIRECTORIES"]; target.outputName = targetProps.value("OUTPUT_NAME", QStringList(t.name)).join(QString()); target.location = CMake::resolveSystemDirs(m_project, targetProps["LOCATION"]).first(); QSet dependencies; foreach(const QString& dep, targetProps["PRIVATE_LINK_LIBRARIES"]) { processDependencies(target, dep, data, dependencies); } processDependencies(target, t.name, data, dependencies); m_targets += target; } return ret; } void CMakeCommitChangesJob::start() { Q_ASSERT(m_project->thread() == QThread::currentThread()); if(!m_parentItem && m_findParent) { if(m_path == m_project->path()) { m_parentItem = m_project->projectItem()->folder(); } else { QList folders = m_project->foldersForPath(IndexedString(m_path.pathOrUrl())); if(!folders.isEmpty()) m_parentItem = folders.first(); } } if((!m_projectDataAdded && m_parentItem) || dynamic_cast(m_parentItem)) { QMetaObject::invokeMethod(this, "makeChanges", Qt::QueuedConnection); m_waiting = false; } else m_waiting = true; } void CMakeCommitChangesJob::makeChanges() { Q_ASSERT(m_project->thread() == QThread::currentThread()); ProjectFolderItem* f = m_parentItem; m_manager->addWatcher(m_project, m_path.toLocalFile()); if(!m_projectDataAdded) { reloadFiles(); return; } CMakeFolderItem* folder = dynamic_cast(f); Q_ASSERT(folder); qDeleteAll(cleanupBuildFolders(folder, m_subdirectories)); foreach(const Subdirectory& subf, m_subdirectories) { const Path path(m_path, subf.name); if (!m_manager->filterManager()->isValid(path, true, m_project)) { continue; } if(QDir(path.toLocalFile()).exists()) { CMakeFolderItem* parent=folder; if(!m_path.isDirectParentOf(path)) parent=0; CMakeFolderItem* a = 0; ProjectFolderItem* ff = folder->folderNamed(subf.name); if(ff) { if(ff->type()!=ProjectBaseItem::BuildFolder) delete ff; else a = static_cast(ff); } if(!a) a = new CMakeFolderItem( folder->project(), path, subf.build_dir, parent ); else a->setPath(path); emit folderCreated(a); if(!parent) { a->setFormerParent(folder); m_manager->addPending(path, a); } a->setDescriptor(subf.desc); } } folder->setIncludeDirectories(m_directories); folder->setDefinitions(m_definitions); QSet deletableTargets = folder->targetList().toSet(); foreach ( const ProcessedTarget& pt, m_targets) { const Target& t = pt.target; KDevelop::ProjectTargetItem* targetItem = folder->targetNamed(t.type, t.name); if (targetItem) deletableTargets.remove(targetItem); else { switch(t.type) { case Target::Library: targetItem = new CMakeLibraryTargetItem( m_project, t.name, folder, pt.outputName, pt.location); break; case Target::Executable: targetItem = new CMakeExecutableTargetItem( m_project, t.name, folder, pt.outputName, pt.location); break; case Target::Custom: targetItem = new CMakeCustomTargetItem( m_project, t.name, folder, pt.outputName ); break; } } DUChainAttatched* duchainAtt=dynamic_cast(targetItem); if(duchainAtt) { duchainAtt->setDeclaration(t.declaration); } DescriptorAttatched* descAtt=dynamic_cast(targetItem); if(descAtt) descAtt->setDescriptor(t.desc); CompilationDataAttached* incAtt = dynamic_cast(targetItem); if(incAtt) { incAtt->setIncludeDirectories(resolvePaths(m_path, pt.includes)); incAtt->addDefinitions(pt.defines); } Path::List tfiles; foreach( const QString & sFile, t.files) { if(sFile.startsWith("#[") || sFile.isEmpty() || sFile.endsWith('/')) continue; const Path sourceFile(m_path, sFile); if(!sourceFile.isValid() || !QFile::exists(sourceFile.toLocalFile())) { kDebug(9042) << "..........Skipping non-existing source file:" << sourceFile << sFile << m_path; continue; } tfiles += sourceFile; kDebug(9042) << "..........Adding:" << sourceFile << sFile << m_path; } setTargetFiles(targetItem, tfiles); } qDeleteAll(deletableTargets); CTestUtils::createTestSuites(m_tests, folder); reloadFiles(); } void CMakeCommitChangesJob::setTargetFiles(ProjectTargetItem* target, const Path::List& files) { QList tfiles = target->fileList(); foreach(ProjectFileItem* file, tfiles) { if(!files.contains(file->path())) delete file; } tfiles = target->fileList(); //We need to recreate the list without the removed items foreach(const Path& file, files) { ProjectFileItem* f = containsFile(file, tfiles); if(!f) new KDevelop::ProjectFileItem( target->project(), file, target ); } } void CMakeCommitChangesJob::reloadFiles(ProjectFolderItem* item) { QDir d(item->path().toLocalFile()); if(!d.exists()) { kDebug() << "Trying to return a directory that doesn't exist:" << item->path(); return; } const Path folderPath = item->path(); const QFileInfoList entriesL = d.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot); QSet entries = filterFiles(entriesL, folderPath, item->project(), m_manager->filterManager()); kDebug() << "Reloading Directory!" << folderPath; //We look for removed elements foreach(ProjectBaseItem* it, item->children()) { if(it->type()==ProjectBaseItem::Target || it->type()==ProjectBaseItem::ExecutableTarget || it->type()==ProjectBaseItem::LibraryTarget) continue; QString current=it->text(); const Path filePath(folderPath, current); if(!entries.contains(current)) delete it; else if(it->path() != filePath) it->setPath(filePath); } //We look for new elements QList newItems; foreach( const QString& entry, entries ) { if(item->hasFileOrFolder( entry )) continue; const Path filePath(folderPath, entry); if( QFileInfo( filePath.toLocalFile() ).isDir() ) { ProjectFolderItem* pendingfolder = m_manager->takePending(filePath); if(pendingfolder) { newItems += pendingfolder; } else if(isCorrectFolder(filePath, item->project())) { ProjectFolderItem* it = new ProjectFolderItem( item->project(), filePath ); reloadFiles(it); m_manager->addWatcher(item->project(), filePath.toLocalFile()); newItems += it; } } else { newItems += new KDevelop::ProjectFileItem( item->project(), filePath ); } } foreach(ProjectBaseItem* it, newItems) item->appendRow(it); } void CMakeCommitChangesJob::folderAvailable(ProjectFolderItem* item) { if(item->path() == m_path) { m_parentItem = item; if(m_waiting) { start(); Q_ASSERT(!m_waiting); } } } void CMakeCommitChangesJob::reloadFiles() { Q_ASSERT(m_project->thread() == QThread::currentThread()); Q_ASSERT(m_parentItem); reloadFiles(m_parentItem); emitResult(); } void CMakeCommitChangesJob::setFindParentItem(bool find) { m_findParent = find; }