1 /*
2     SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez <aleixpol@kde.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "cmakecommitchangesjob.h"
8 #include "cmakeprojectdata.h"
9 #include "testing/ctestutils.h"
10 #include "cmakemodelitems.h"
11 #include "cmakeutils.h"
12 #include "cmakemanager.h"
13 #include "debug.h"
14 #include <cmakeparserutils.h>
15 #include <project/projectfiltermanager.h>
16 #include <project/interfaces/iprojectfilter.h>
17 
18 #include <QUrl>
19 
20 #include <QThread>
21 
22 using namespace KDevelop;
23 
containsFile(const Path & file,const QList<ProjectFileItem * > & tfiles)24 static ProjectFileItem* containsFile(const Path& file, const QList<ProjectFileItem*>& tfiles)
25 {
26     for (ProjectFileItem* f : tfiles) {
27         if(f->path()==file)
28             return f;
29     }
30     return 0;
31 }
32 
resolvePaths(const Path & base,const QStringList & pathsToResolve)33 static QStringList resolvePaths(const Path& base, const QStringList& pathsToResolve)
34 {
35     QStringList resolvedPaths;
36     resolvedPaths.reserve(pathsToResolve.size());
37     for (const QString& pathToResolve : pathsToResolve) {
38         QString dir(pathToResolve);
39         if (!pathToResolve.startsWith(QLatin1String("#[")) &&
40             !pathToResolve.startsWith(QLatin1String("$<"))) {
41             dir = Path(base, pathToResolve).toLocalFile();
42         }
43         resolvedPaths.append(dir);
44     }
45     return resolvedPaths;
46 }
47 
filterFiles(const QFileInfoList & orig,const Path & base,IProject * project,ProjectFilterManager * filter)48 static QSet<QString> filterFiles(const QFileInfoList& orig, const Path& base, IProject* project, ProjectFilterManager* filter)
49 {
50     QSet<QString> ret;
51     ret.reserve(orig.size());
52     for (const QFileInfo& info : orig) {
53         const QString str = info.fileName();
54         const Path path(base, str);
55 
56         if (!filter->isValid(path, info.isDir(), project)) {
57             continue;
58         }
59 
60         ret.insert(str);
61     }
62     return ret;
63 }
64 
isCorrectFolder(const Path & path,IProject * p)65 static bool isCorrectFolder(const Path& path, IProject* p)
66 {
67     const QString cache = Path(path, "CMakeCache.txt").toLocalFile();
68 
69     bool ret = !QFile::exists(cache);
70     ret &= !CMake::allBuildDirs(p).contains(path.toLocalFile());
71 
72     return ret;
73 }
74 
75 template <class T>
textInList(const QVector<T> & list,KDevelop::ProjectBaseItem * item)76 static bool textInList(const QVector<T>& list, KDevelop::ProjectBaseItem* item)
77 {
78     for (const T& s : list) {
79         if(item->text()==s.name)
80             return true;
81     }
82     return false;
83 }
84 
cleanupBuildFolders(CMakeFolderItem * item,const QVector<Subdirectory> & subs)85 static QList<KDevelop::ProjectBaseItem*> cleanupBuildFolders(CMakeFolderItem* item, const QVector<Subdirectory>& subs)
86 {
87     QList<ProjectBaseItem*> ret;
88     const QList<KDevelop::ProjectFolderItem*> folders = item->folderList();
89     for (KDevelop::ProjectFolderItem* folder : folders) {
90         CMakeFolderItem* cmfolder = dynamic_cast<CMakeFolderItem*>(folder);
91         if(cmfolder && cmfolder->formerParent()==item && !textInList<Subdirectory>(subs, folder))
92             ret += folder;
93     }
94     return ret;
95 }
96 
97 /////////////////////////////////////////
98 
CMakeCommitChangesJob(const Path & path,CMakeManager * manager,KDevelop::IProject * project)99 CMakeCommitChangesJob::CMakeCommitChangesJob(const Path& path, CMakeManager* manager, KDevelop::IProject* project)
100     : KJob()
101     , m_path(path)
102     , m_project(project)
103     , m_manager(manager)
104     , m_projectDataAdded(false)
105     , m_parentItem(0)
106     , m_waiting(false)
107     , m_findParent(true)
108 {
109     setObjectName(path.pathOrUrl());
110 }
111 
processDependencies(ProcessedTarget & target,const QString & dep,const CMakeProjectData & data,QSet<QString> & alreadyProcessed)112 static void processDependencies(ProcessedTarget &target, const QString& dep, const CMakeProjectData& data, QSet<QString>& alreadyProcessed)
113 {
114     if(dep.isEmpty() || alreadyProcessed.contains(dep))
115         return;
116     alreadyProcessed.insert(dep);
117 //     qCDebug(CMAKE) << "processing..." << target.target.name << dep;
118     QMap<QString, QStringList> depData = data.properties.value(TargetProperty).value(dep);
119     if(depData.isEmpty()) {
120         qCDebug(CMAKE) << "error: couldn't find dependency " << dep << data.properties.value(TargetProperty).keys();
121         return;
122     }
123 
124     target.includes += depData["INTERFACE_INCLUDE_DIRECTORIES"];
125     target.defines += depData["INTERFACE_COMPILE_DEFINITIONS"];
126     foreach(const QString& d, depData["INTERFACE_LINK_LIBRARIES"])
127         processDependencies(target, d, data, alreadyProcessed);
128 }
129 
addProjectData(const CMakeProjectData & data)130 Path::List CMakeCommitChangesJob::addProjectData(const CMakeProjectData& data)
131 {
132     m_projectDataAdded = true;
133     Path::List ret;
134     m_tests = data.testSuites;
135 
136     QSet<QString> alreadyAdded;
137     foreach(const Subdirectory& subf, data.subdirectories) {
138         if(subf.name.isEmpty() || alreadyAdded.contains(subf.name)) //empty case would not be necessary if we didn't process the wrong lines
139             continue;
140         alreadyAdded.insert(subf.name);
141         m_subdirectories += subf;
142 
143         ret += Path(m_path, subf.name);
144     }
145 
146     QString dir = m_path.toLocalFile();
147     if(data.vm.value("CMAKE_INCLUDE_CURRENT_DIR")==QStringList("ON")) {
148         m_directories += dir;
149         m_directories += CMakeParserUtils::binaryPath(dir, m_project->path().toLocalFile(), CMake::currentBuildDir(m_project).toLocalFile(QUrl::RemoveTrailingSlash));
150     }
151     m_directories += resolvePaths(m_path, data.properties[DirectoryProperty][dir]["INCLUDE_DIRECTORIES"]);
152     m_directories.removeAll(QString());
153 
154     m_definitions.unite(data.definitions);
155     CMakeParserUtils::addDefinitions(data.properties[DirectoryProperty][dir]["COMPILE_DEFINITIONS"], &m_definitions);
156     CMakeParserUtils::addDefinitions(data.vm["CMAKE_CXX_FLAGS"], &m_definitions, true);
157 
158     foreach(const Target& t, data.targets) {
159         const QMap<QString, QStringList>& targetProps = data.properties[TargetProperty][t.name];
160         if(targetProps["FOLDER"]==QStringList("CTestDashboardTargets"))
161             continue; //filter some annoying targets
162 
163         if (!m_manager->filterManager()->isValid(Path(m_path, t.name), false, m_project)) {
164             continue;
165         }
166 
167         ProcessedTarget target;
168         target.target = t;
169         target.defines = targetProps["COMPILE_DEFINITIONS"];
170         target.includes = targetProps["INCLUDE_DIRECTORIES"];
171         target.outputName = targetProps.value("OUTPUT_NAME", QStringList(t.name)).join(QString());
172         target.location = CMake::resolveSystemDirs(m_project, targetProps["LOCATION"]).first();
173 
174         QSet<QString> dependencies;
175         foreach(const QString& dep, targetProps["PRIVATE_LINK_LIBRARIES"]) {
176             processDependencies(target, dep, data, dependencies);
177         }
178         processDependencies(target, t.name, data, dependencies);
179         m_targets += target;
180     }
181     return ret;
182 }
183 
start()184 void CMakeCommitChangesJob::start()
185 {
186     Q_ASSERT(m_project->thread() == QThread::currentThread());
187 
188     if(!m_parentItem && m_findParent) {
189         if(m_path == m_project->path()) {
190             m_parentItem = m_project->projectItem()->folder();
191         } else {
192             QList<ProjectFolderItem*> folders = m_project->foldersForPath(IndexedString(m_path.pathOrUrl()));
193             if(!folders.isEmpty())
194                 m_parentItem = folders.first();
195         }
196     }
197 
198     if((!m_projectDataAdded && m_parentItem) || dynamic_cast<CMakeFolderItem*>(m_parentItem)) {
199         QMetaObject::invokeMethod(this, "makeChanges", Qt::QueuedConnection);
200         m_waiting = false;
201     } else
202         m_waiting = true;
203 }
204 
makeChanges()205 void CMakeCommitChangesJob::makeChanges()
206 {
207     Q_ASSERT(m_project->thread() == QThread::currentThread());
208     ProjectFolderItem* f = m_parentItem;
209     m_manager->addWatcher(m_project, m_path.toLocalFile());
210 
211     if(!m_projectDataAdded) {
212         reloadFiles();
213         return;
214     }
215 
216     CMakeFolderItem* folder = dynamic_cast<CMakeFolderItem*>(f);
217     Q_ASSERT(folder);
218     qDeleteAll(cleanupBuildFolders(folder, m_subdirectories));
219     foreach(const Subdirectory& subf, m_subdirectories)
220     {
221         const Path path(m_path, subf.name);
222 
223         if (!m_manager->filterManager()->isValid(path, true, m_project)) {
224             continue;
225         }
226         if(QDir(path.toLocalFile()).exists())
227         {
228             CMakeFolderItem* parent=folder;
229             if(!m_path.isDirectParentOf(path))
230                 parent=0;
231 
232             CMakeFolderItem* a = 0;
233             ProjectFolderItem* ff = folder->folderNamed(subf.name);
234             if(ff)
235             {
236                 if(ff->type()!=ProjectBaseItem::BuildFolder)
237                     delete ff;
238                 else
239                     a = static_cast<CMakeFolderItem*>(ff);
240             }
241             if(!a)
242                 a = new CMakeFolderItem( folder->project(), path, subf.build_dir, parent );
243             else
244                 a->setPath(path);
245             emit folderCreated(a);
246 
247             if(!parent) {
248                 a->setFormerParent(folder);
249                 m_manager->addPending(path, a);
250             }
251 
252             a->setDescriptor(subf.desc);
253         }
254     }
255 
256     folder->setIncludeDirectories(m_directories);
257     folder->setDefinitions(m_definitions);
258 
259     QSet<ProjectTargetItem*> deletableTargets = folder->targetList().toSet();
260     foreach ( const ProcessedTarget& pt, m_targets)
261     {
262         const Target& t = pt.target;
263 
264         KDevelop::ProjectTargetItem* targetItem = folder->targetNamed(t.type, t.name);
265         if (targetItem)
266             deletableTargets.remove(targetItem);
267         else {
268             switch(t.type)
269             {
270                 case Target::Library:
271                     targetItem = new CMakeLibraryTargetItem( m_project, t.name, folder, pt.outputName, pt.location);
272                     break;
273                 case Target::Executable:
274                     targetItem = new CMakeExecutableTargetItem( m_project, t.name, folder, pt.outputName, pt.location);
275                     break;
276                 case Target::Custom:
277                     targetItem = new CMakeCustomTargetItem( m_project, t.name, folder, pt.outputName );
278                     break;
279             }
280         }
281         DUChainAttatched* duchainAtt=dynamic_cast<DUChainAttatched*>(targetItem);
282         if(duchainAtt) {
283             duchainAtt->setDeclaration(t.declaration);
284         }
285 
286         DescriptorAttatched* descAtt=dynamic_cast<DescriptorAttatched*>(targetItem);
287         if(descAtt)
288             descAtt->setDescriptor(t.desc);
289 
290         CompilationDataAttached* incAtt = dynamic_cast<CompilationDataAttached*>(targetItem);
291         if(incAtt) {
292             incAtt->setIncludeDirectories(resolvePaths(m_path, pt.includes));
293             incAtt->addDefinitions(pt.defines);
294         }
295 
296         Path::List tfiles;
297         foreach( const QString & sFile, t.files)
298         {
299             if(sFile.startsWith("#[") || sFile.isEmpty() || sFile.endsWith('/'))
300                 continue;
301 
302             const Path sourceFile(m_path, sFile);
303 
304             if(!sourceFile.isValid() || !QFile::exists(sourceFile.toLocalFile())) {
305                 qCDebug(CMAKE) << "..........Skipping non-existing source file:" << sourceFile << sFile << m_path;
306                 continue;
307             }
308 
309             tfiles += sourceFile;
310             qCDebug(CMAKE) << "..........Adding:" << sourceFile << sFile << m_path;
311         }
312 
313         setTargetFiles(targetItem, tfiles);
314     }
315     qDeleteAll(deletableTargets);
316 
317     CTestUtils::createTestSuites(m_tests, folder);
318     reloadFiles();
319 }
320 
setTargetFiles(ProjectTargetItem * target,const Path::List & files)321 void CMakeCommitChangesJob::setTargetFiles(ProjectTargetItem* target, const Path::List& files)
322 {
323     QList<ProjectFileItem*> tfiles = target->fileList();
324     foreach(ProjectFileItem* file, tfiles) {
325         if(!files.contains(file->path()))
326             delete file;
327     }
328 
329     tfiles = target->fileList(); //We need to recreate the list without the removed items
330     for (const Path& file : files) {
331         ProjectFileItem* f = containsFile(file, tfiles);
332         if(!f)
333             new KDevelop::ProjectFileItem( target->project(), file, target );
334     }
335 }
336 
reloadFiles(ProjectFolderItem * item)337 void CMakeCommitChangesJob::reloadFiles(ProjectFolderItem* item)
338 {
339     QDir d(item->path().toLocalFile());
340     if(!d.exists()) {
341         qCDebug(CMAKE) << "Trying to return a directory that doesn't exist:" << item->path();
342         return;
343     }
344 
345     const Path folderPath = item->path();
346 
347     const QFileInfoList entriesL = d.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot);
348     const QSet<QString> entries = filterFiles(entriesL, folderPath, item->project(), m_manager->filterManager());
349 
350     qCDebug(CMAKE) << "Reloading Directory!" << folderPath;
351 
352     //We look for removed elements
353     foreach(ProjectBaseItem* it, item->children())
354     {
355         if(it->type()==ProjectBaseItem::Target || it->type()==ProjectBaseItem::ExecutableTarget || it->type()==ProjectBaseItem::LibraryTarget)
356             continue;
357 
358         QString current=it->text();
359         const Path filePath(folderPath, current);
360 
361         if(!entries.contains(current))
362             delete it;
363         else if(it->path() != filePath)
364             it->setPath(filePath);
365     }
366 
367     //We look for new elements
368     QList<ProjectBaseItem*> newItems;
369     for (const QString& entry : entries ) {
370         if(item->hasFileOrFolder( entry ))
371             continue;
372 
373         const Path filePath(folderPath, entry);
374 
375         if( QFileInfo( filePath.toLocalFile() ).isDir() )
376         {
377             ProjectFolderItem* pendingfolder = m_manager->takePending(filePath);
378 
379             if(pendingfolder) {
380                 newItems += pendingfolder;
381             } else if(isCorrectFolder(filePath, item->project())) {
382                 ProjectFolderItem* it = new ProjectFolderItem( item->project(), filePath );
383                 reloadFiles(it);
384                 m_manager->addWatcher(item->project(), filePath.toLocalFile());
385                 newItems += it;
386             }
387         }
388         else
389         {
390             newItems += new KDevelop::ProjectFileItem( item->project(), filePath );
391         }
392     }
393     foreach(ProjectBaseItem* it, newItems)
394         item->appendRow(it);
395 }
396 
folderAvailable(ProjectFolderItem * item)397 void CMakeCommitChangesJob::folderAvailable(ProjectFolderItem* item)
398 {
399     if(item->path() == m_path) {
400         m_parentItem = item;
401         if(m_waiting) {
402             start();
403             Q_ASSERT(!m_waiting);
404         }
405     }
406 }
reloadFiles()407 void CMakeCommitChangesJob::reloadFiles()
408 {
409     Q_ASSERT(m_project->thread() == QThread::currentThread());
410     Q_ASSERT(m_parentItem);
411     reloadFiles(m_parentItem);
412     emitResult();
413 }
414 
setFindParentItem(bool find)415 void CMakeCommitChangesJob::setFindParentItem(bool find)
416 {
417     m_findParent = find;
418 }
419