1 /*
2     SPDX-FileCopyrightText: 2014 Kevin Funk <kfunk@kde.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "cmakeimportjsonjob.h"
8 
9 #include "cmakeutils.h"
10 #include "cmakeprojectdata.h"
11 #include "cmakemodelitems.h"
12 #include "debug.h"
13 
14 #include <makefileresolver/makefileresolver.h>
15 #include <language/duchain/duchain.h>
16 #include <language/duchain/duchainlock.h>
17 #include <interfaces/iproject.h>
18 #include <interfaces/icore.h>
19 #include <interfaces/iruntime.h>
20 #include <interfaces/iruntimecontroller.h>
21 
22 #include <KShell>
23 #include <QJsonDocument>
24 #include <QJsonObject>
25 #include <QJsonArray>
26 #include <QtConcurrentRun>
27 #include <QFutureWatcher>
28 #include <QRegularExpression>
29 
30 using namespace KDevelop;
31 
32 namespace {
33 
importCommands(const Path & commandsFile)34 CMakeFilesCompilationData importCommands(const Path& commandsFile)
35 {
36     // NOTE: to get compile_commands.json, you need -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
37     QFile f(commandsFile.toLocalFile());
38     bool r = f.open(QFile::ReadOnly|QFile::Text);
39     if(!r) {
40         qCWarning(CMAKE) << "Couldn't open commands file" << commandsFile;
41         return {};
42     }
43 
44     qCDebug(CMAKE) << "Found commands file" << commandsFile;
45 
46     CMakeFilesCompilationData data;
47     QJsonParseError error;
48     const QJsonDocument document = QJsonDocument::fromJson(f.readAll(), &error);
49     if (error.error) {
50         qCWarning(CMAKE) << "Failed to parse JSON in commands file:" << error.errorString() << commandsFile;
51         data.isValid = false;
52         return data;
53     } else if (!document.isArray()) {
54         qCWarning(CMAKE) << "JSON document in commands file is not an array: " << commandsFile;
55         data.isValid = false;
56         return data;
57     }
58 
59     MakeFileResolver resolver;
60     const QString KEY_COMMAND = QStringLiteral("command");
61     const QString KEY_DIRECTORY = QStringLiteral("directory");
62     const QString KEY_FILE = QStringLiteral("file");
63     auto rt = ICore::self()->runtimeController()->currentRuntime();
64     const auto values = document.array();
65     for (const QJsonValue& value : values) {
66         if (!value.isObject()) {
67             qCWarning(CMAKE) << "JSON command file entry is not an object:" << value;
68             continue;
69         }
70         const QJsonObject entry = value.toObject();
71         if (!entry.contains(KEY_FILE) || !entry.contains(KEY_COMMAND) || !entry.contains(KEY_DIRECTORY)) {
72             qCWarning(CMAKE) << "JSON command file entry does not contain required keys:" << entry;
73             continue;
74         }
75 
76         PathResolutionResult result = resolver.processOutput(entry[KEY_COMMAND].toString(), entry[KEY_DIRECTORY].toString());
77 
78         auto convert = [rt](const Path &path) { return rt->pathInHost(path); };
79 
80         CMakeFile ret;
81         ret.includes = kTransform<Path::List>(result.paths, convert);
82         ret.frameworkDirectories = kTransform<Path::List>(result.frameworkDirectories, convert);
83         ret.defines = result.defines;
84         const Path path(rt->pathInHost(Path(entry[KEY_FILE].toString())));
85         qCDebug(CMAKE) << "entering..." << path << entry[KEY_FILE];
86         data.files[path] = ret;
87     }
88 
89     data.isValid = true;
90     data.rebuildFileForFolderMapping();
91     return data;
92 }
93 
import(const Path & commandsFile,const Path & targetsFilePath,const QString & sourceDir,const KDevelop::Path & buildPath)94 ImportData import(const Path& commandsFile, const Path &targetsFilePath, const QString &sourceDir, const KDevelop::Path &buildPath)
95 {
96     QHash<KDevelop::Path, QVector<CMakeTarget>> cmakeTargets;
97 
98     //we don't have target type information in json, so we just announce all of them as exes
99     const auto targets = CMake::enumerateTargets(targetsFilePath, sourceDir, buildPath);
100     for(auto it = targets.constBegin(), itEnd = targets.constEnd(); it!=itEnd; ++it) {
101         cmakeTargets[it.key()] = kTransform<QVector<CMakeTarget>>(*it, [](const QString &targetName) {
102             return CMakeTarget{
103                 CMakeTarget::Executable,
104                 targetName,
105                 KDevelop::Path::List(),
106                 KDevelop::Path::List(),
107                 QString()
108             };
109         });
110     }
111 
112     return ImportData {
113         importCommands(commandsFile),
114         cmakeTargets,
115         CMake::importTestSuites(buildPath)
116     };
117 }
118 
119 }
120 
CMakeImportJsonJob(IProject * project,QObject * parent)121 CMakeImportJsonJob::CMakeImportJsonJob(IProject* project, QObject* parent)
122     : KJob(parent)
123     , m_project(project)
124     , m_data({})
125 {
126     connect(&m_futureWatcher, &QFutureWatcher<ImportData>::finished, this, &CMakeImportJsonJob::importCompileCommandsJsonFinished);
127 }
128 
~CMakeImportJsonJob()129 CMakeImportJsonJob::~CMakeImportJsonJob()
130 {}
131 
start()132 void CMakeImportJsonJob::start()
133 {
134     auto commandsFile = CMake::commandsFile(project());
135     if (!QFileInfo::exists(commandsFile.toLocalFile())) {
136         qCWarning(CMAKE) << "Could not import CMake project" << project()->path() << "('compile_commands.json' missing)";
137         emitResult();
138         return;
139     }
140 
141     const Path currentBuildDir = CMake::currentBuildDir(m_project);
142     Q_ASSERT (!currentBuildDir.isEmpty());
143 
144     const Path targetsFilePath = CMake::targetDirectoriesFile(m_project);
145     const QString sourceDir = m_project->path().toLocalFile();
146     auto rt = ICore::self()->runtimeController()->currentRuntime();
147 
148     auto future = QtConcurrent::run(import, commandsFile, targetsFilePath, sourceDir, rt->pathInRuntime(currentBuildDir));
149     m_futureWatcher.setFuture(future);
150 }
151 
importCompileCommandsJsonFinished()152 void CMakeImportJsonJob::importCompileCommandsJsonFinished()
153 {
154     Q_ASSERT(m_project->thread() == QThread::currentThread());
155     Q_ASSERT(m_futureWatcher.isFinished());
156 
157     auto future = m_futureWatcher.future();
158     auto data = future.result();
159     if (!data.compilationData.isValid) {
160         qCWarning(CMAKE) << "Could not import CMake project ('compile_commands.json' invalid)";
161         emitResult();
162         return;
163     }
164 
165     m_data = {data.compilationData, data.targets, data.testSuites, {}};
166     qCDebug(CMAKE) << "Done importing, found" << data.compilationData.files.count() << "entries for" << project()->path();
167 
168     emitResult();
169 }
170 
project() const171 IProject* CMakeImportJsonJob::project() const
172 {
173     return m_project;
174 }
175 
projectData() const176 CMakeProjectData CMakeImportJsonJob::projectData() const
177 {
178     Q_ASSERT(!m_futureWatcher.isRunning());
179     return m_data;
180 }
181 
182 #include "moc_cmakeimportjsonjob.cpp"
183