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