1 /*
2     SPDX-FileCopyrightText: 2019 Daniel Mensinger <daniel@mensinger-ka.de>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "mesonintrospectjob.h"
8 
9 #include "mesonconfig.h"
10 #include "mesonmanager.h"
11 #include "mesonoptions.h"
12 #include <debug.h>
13 
14 #include <KLocalizedString>
15 #include <KProcess>
16 
17 #include <QDir>
18 #include <QJsonArray>
19 #include <QJsonDocument>
20 #include <QJsonObject>
21 #include <QtConcurrentRun>
22 
23 using namespace Meson;
24 using namespace KDevelop;
25 
MesonIntrospectJob(KDevelop::IProject * project,QVector<MesonIntrospectJob::Type> types,MesonIntrospectJob::Mode mode,QObject * parent)26 MesonIntrospectJob::MesonIntrospectJob(KDevelop::IProject* project, QVector<MesonIntrospectJob::Type> types,
27                                        MesonIntrospectJob::Mode mode, QObject* parent)
28     : KJob(parent)
29     , m_types(types)
30     , m_mode(mode)
31     , m_project(project)
32 {
33     Q_ASSERT(project);
34 
35     if (mode == MESON_FILE) {
36         // Since we are parsing the meson file in this mode, no build directory
37         // is required and we have to fake a build directory
38         m_buildDir.buildDir = project->path();
39         auto* bsm = project->buildSystemManager();
40         auto* manager = dynamic_cast<MesonManager*>(bsm);
41         if (manager) {
42             m_buildDir.mesonExecutable = manager->findMeson();
43         }
44     } else {
45         m_buildDir = Meson::currentBuildDir(project);
46     }
47 
48     m_projectPath = project->path();
49     connect(&m_futureWatcher, &QFutureWatcher<QString>::finished, this, &MesonIntrospectJob::finished);
50 }
51 
MesonIntrospectJob(KDevelop::IProject * project,KDevelop::Path meson,QVector<MesonIntrospectJob::Type> types,QObject * parent)52 MesonIntrospectJob::MesonIntrospectJob(KDevelop::IProject* project, KDevelop::Path meson,
53                                        QVector<MesonIntrospectJob::Type> types, QObject* parent)
54     : KJob(parent)
55     , m_types(types)
56     , m_mode(MESON_FILE)
57     , m_project(project)
58 {
59     Q_ASSERT(project);
60 
61     // Since we are parsing the meson file in this mode, no build directory
62     // is required and we have to fake a build directory
63     m_projectPath = project->path();
64     m_buildDir.buildDir = m_projectPath;
65     m_buildDir.mesonExecutable = meson;
66     connect(&m_futureWatcher, &QFutureWatcher<QString>::finished, this, &MesonIntrospectJob::finished);
67 }
68 
MesonIntrospectJob(KDevelop::IProject * project,Meson::BuildDir buildDir,QVector<MesonIntrospectJob::Type> types,MesonIntrospectJob::Mode mode,QObject * parent)69 MesonIntrospectJob::MesonIntrospectJob(KDevelop::IProject* project, Meson::BuildDir buildDir,
70                                        QVector<MesonIntrospectJob::Type> types, MesonIntrospectJob::Mode mode,
71                                        QObject* parent)
72     : KJob(parent)
73     , m_types(types)
74     , m_mode(mode)
75     , m_buildDir(buildDir)
76     , m_project(project)
77 {
78     Q_ASSERT(project);
79     m_projectPath = project->path();
80     connect(&m_futureWatcher, &QFutureWatcher<QString>::finished, this, &MesonIntrospectJob::finished);
81 }
82 
getTypeString(MesonIntrospectJob::Type type) const83 QString MesonIntrospectJob::getTypeString(MesonIntrospectJob::Type type) const
84 {
85     switch (type) {
86     case BENCHMARKS:
87         return QStringLiteral("benchmarks");
88     case BUILDOPTIONS:
89         return QStringLiteral("buildoptions");
90     case BUILDSYSTEM_FILES:
91         return QStringLiteral("buildsystem_files");
92     case DEPENDENCIES:
93         return QStringLiteral("dependencies");
94     case INSTALLED:
95         return QStringLiteral("installed");
96     case PROJECTINFO:
97         return QStringLiteral("projectinfo");
98     case TARGETS:
99         return QStringLiteral("targets");
100     case TESTS:
101         return QStringLiteral("tests");
102     }
103 
104     return QStringLiteral("error");
105 }
106 
importJSONFile(const BuildDir & buildDir,MesonIntrospectJob::Type type,QJsonObject * out)107 QString MesonIntrospectJob::importJSONFile(const BuildDir& buildDir, MesonIntrospectJob::Type type, QJsonObject* out)
108 {
109     QString typeStr = getTypeString(type);
110     QString fileName = QStringLiteral("intro-") + typeStr + QStringLiteral(".json");
111     QString infoDir = buildDir.buildDir.toLocalFile() + QStringLiteral("/") + QStringLiteral("meson-info");
112     QFile introFile(infoDir + QStringLiteral("/") + fileName);
113 
114     if (!introFile.exists()) {
115         return i18n("Introspection file '%1' does not exist", QFileInfo(introFile).canonicalFilePath());
116     }
117 
118     if (!introFile.open(QFile::ReadOnly | QFile::Text)) {
119         return i18n("Failed to open introspection file '%1'", QFileInfo(introFile).canonicalFilePath());
120     }
121 
122     QJsonParseError error;
123     QJsonDocument doc = QJsonDocument::fromJson(introFile.readAll(), &error);
124     if (error.error) {
125         return i18n("In %1:%2: %3", QFileInfo(introFile).canonicalFilePath(), error.offset, error.errorString());
126     }
127 
128     if (doc.isArray()) {
129         (*out)[typeStr] = doc.array();
130     } else if (doc.isObject()) {
131         (*out)[typeStr] = doc.object();
132     } else {
133         return i18n("The introspection file '%1' contains neither an array nor an object",
134                     QFileInfo(introFile).canonicalFilePath());
135     }
136 
137     return QString();
138 }
139 
importMesonAPI(const BuildDir & buildDir,MesonIntrospectJob::Type type,QJsonObject * out)140 QString MesonIntrospectJob::importMesonAPI(const BuildDir& buildDir, MesonIntrospectJob::Type type, QJsonObject* out)
141 {
142     QString typeStr = getTypeString(type);
143     QString option = QStringLiteral("--") + typeStr;
144     option.replace(QLatin1Char('_'), QLatin1Char('-'));
145 
146     KProcess proc(this);
147     proc.setWorkingDirectory(m_projectPath.toLocalFile());
148     proc.setOutputChannelMode(KProcess::SeparateChannels);
149     proc.setProgram(buildDir.mesonExecutable.toLocalFile());
150     proc << QStringLiteral("introspect") << option << QStringLiteral("meson.build");
151 
152     int ret = proc.execute();
153     if (ret != 0) {
154         return i18n("%1 returned %2", proc.program().join(QLatin1Char(' ')), ret);
155     }
156 
157     QJsonParseError error;
158     QJsonDocument doc = QJsonDocument::fromJson(proc.readAll(), &error);
159     if (error.error) {
160         return i18n("JSON parser error: %1", error.errorString());
161     }
162 
163     if (doc.isArray()) {
164         (*out)[typeStr] = doc.array();
165     } else if (doc.isObject()) {
166         (*out)[typeStr] = doc.object();
167     } else {
168         return i18n("The introspection output of '%1' contains neither an array nor an object",
169                     proc.program().join(QLatin1Char(' ')));
170     }
171 
172     return QString();
173 }
174 
import(BuildDir buildDir)175 QString MesonIntrospectJob::import(BuildDir buildDir)
176 {
177     QJsonObject rawData;
178 
179     // First load the complete JSON data
180     for (auto i : m_types) {
181         QString err;
182         switch (m_mode) {
183         case BUILD_DIR:
184             err = importJSONFile(buildDir, i, &rawData);
185             break;
186         case MESON_FILE:
187             err = importMesonAPI(buildDir, i, &rawData);
188             break;
189         }
190 
191         if (!err.isEmpty()) {
192             qCWarning(KDEV_Meson) << "MINTRO: " << err;
193             setError(true);
194             setErrorText(err);
195             return err;
196         }
197     }
198 
199     auto buildOptionsJSON = rawData[QStringLiteral("buildoptions")];
200     if (buildOptionsJSON.isArray()) {
201         m_res_options = std::make_shared<MesonOptions>(buildOptionsJSON.toArray());
202         if (m_res_options) {
203             qCDebug(KDEV_Meson) << "MINTRO: Imported " << m_res_options->options().size() << " buildoptions";
204         } else {
205             qCWarning(KDEV_Meson) << "MINTRO: Failed to parse buildoptions";
206         }
207     }
208 
209     auto projectInfoJSON = rawData[QStringLiteral("projectinfo")];
210     if (projectInfoJSON.isObject()) {
211         m_res_projectInfo = std::make_shared<MesonProjectInfo>(projectInfoJSON.toObject());
212         if (!m_res_projectInfo) {
213             qCWarning(KDEV_Meson) << "MINTRO: Failed to parse projectinfo";
214         }
215     }
216 
217     auto targetsJSON = rawData[QStringLiteral("targets")];
218     if (targetsJSON.isArray()) {
219         m_res_targets = std::make_shared<MesonTargets>(targetsJSON.toArray());
220     }
221 
222     auto testsJSON = rawData[QStringLiteral("tests")];
223     if (testsJSON.isArray()) {
224         m_res_tests = std::make_shared<MesonTestSuites>(testsJSON.toArray(), m_project);
225         if (m_res_tests) {
226             qCDebug(KDEV_Meson) << "MINTRO: Imported " << m_res_tests->testSuites().size() << " test suites";
227         } else {
228             qCWarning(KDEV_Meson) << "MINTRO: Failed to parse tests";
229         }
230     }
231 
232     return QString();
233 }
234 
start()235 void MesonIntrospectJob::start()
236 {
237     qCDebug(KDEV_Meson) << "MINTRO: Starting meson introspection job";
238     if (!m_buildDir.isValid()) {
239         qCWarning(KDEV_Meson) << "The current build directory is invalid";
240         setError(true);
241         setErrorText(i18n("The current build directory is invalid"));
242         emitResult();
243         return;
244     }
245 
246     auto future = QtConcurrent::run(this, &MesonIntrospectJob::import, m_buildDir);
247     m_futureWatcher.setFuture(future);
248 }
249 
finished()250 void MesonIntrospectJob::finished()
251 {
252     qCDebug(KDEV_Meson) << "MINTRO: Meson introspection job finished";
253     emitResult();
254 }
255 
doKill()256 bool MesonIntrospectJob::doKill()
257 {
258     if (m_futureWatcher.isRunning()) {
259         m_futureWatcher.cancel();
260     }
261     return true;
262 }
263 
buildOptions()264 MesonOptsPtr MesonIntrospectJob::buildOptions()
265 {
266     return m_res_options;
267 }
268 
projectInfo()269 MesonProjectInfoPtr MesonIntrospectJob::projectInfo()
270 {
271     return m_res_projectInfo;
272 }
273 
targets()274 MesonTargetsPtr MesonIntrospectJob::targets()
275 {
276     return m_res_targets;
277 }
278 
tests()279 MesonTestSuitesPtr MesonIntrospectJob::tests()
280 {
281     return m_res_tests;
282 }
283