File indexing completed on 2024-05-05 16:46:02

0001 /*
0002     SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez <aleixpol@kde.org>
0003     SPDX-FileCopyrightText: 2018 Daniel Mensinger <daniel@mensinger-ka.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "mesonmanager.h"
0009 
0010 #include "debug.h"
0011 #include "mesonbuilder.h"
0012 #include "mesonconfig.h"
0013 #include "mintro/mesonintrospectjob.h"
0014 #include "mintro/mesontargets.h"
0015 #include "settings/mesonconfigpage.h"
0016 #include "settings/mesonnewbuilddir.h"
0017 #include "settings/mesonrewriterpage.h"
0018 
0019 #include <interfaces/icore.h>
0020 #include <interfaces/iproject.h>
0021 #include <interfaces/iprojectcontroller.h>
0022 #include <interfaces/iruncontroller.h>
0023 #include <interfaces/itestcontroller.h>
0024 #include <project/projectconfigpage.h>
0025 #include <project/projectmodel.h>
0026 #include <util/executecompositejob.h>
0027 
0028 #include <KConfigGroup>
0029 #include <KDirWatch>
0030 #include <KLocalizedString>
0031 #include <KPluginFactory>
0032 #include <QFileDialog>
0033 #include <QMessageBox>
0034 #include <QStandardPaths>
0035 
0036 #include <algorithm>
0037 
0038 using namespace KDevelop;
0039 using namespace std;
0040 
0041 static const QString GENERATOR_NINJA = QStringLiteral("ninja");
0042 
0043 K_PLUGIN_FACTORY_WITH_JSON(MesonSupportFactory, "kdevmesonmanager.json", registerPlugin<MesonManager>();)
0044 
0045 // ********************************
0046 // * Error job for failed imports *
0047 // ********************************
0048 
0049 namespace mmanager_internal
0050 {
0051 
0052 class ErrorJob : public KJob
0053 {
0054     Q_OBJECT
0055 public:
0056     ErrorJob(QObject* parent, const QString& error)
0057         : KJob(parent)
0058         , m_error(error)
0059     {
0060     }
0061 
0062     void start() override
0063     {
0064         QMessageBox::critical(nullptr, i18nc("@title:window", "Project Import Failed"), m_error);
0065 
0066         setError(KJob::UserDefinedError + 1); // Indicate that there was an error
0067         setErrorText(m_error);
0068         emitResult();
0069     }
0070 
0071 private:
0072     QString m_error;
0073 };
0074 
0075 }
0076 
0077 using namespace mmanager_internal;
0078 
0079 // ***********************************
0080 // * Meson specific executable class *
0081 // ***********************************
0082 
0083 class MesonProjectExecutableTargetItem final : public ProjectExecutableTargetItem
0084 {
0085 public:
0086     MesonProjectExecutableTargetItem(IProject* project, const QString& name, ProjectBaseItem* parent, Path buildPath,
0087                                      Path installPath = Path())
0088         : ProjectExecutableTargetItem(project, name, parent)
0089         , m_buildPath(buildPath)
0090         , m_installPath(installPath)
0091     {
0092     }
0093 
0094     QUrl builtUrl() const override { return m_buildPath.toUrl(); }
0095     QUrl installedUrl() const override { return m_installPath.toUrl(); }
0096 
0097 private:
0098     Path m_buildPath;
0099     Path m_installPath;
0100 };
0101 
0102 // ***************
0103 // * Constructor *
0104 // ***************
0105 
0106 MesonManager::MesonManager(QObject* parent, const QVariantList& args)
0107     : AbstractFileManagerPlugin(QStringLiteral("KDevMesonManager"), parent, args)
0108     , m_builder(new MesonBuilder(this))
0109 {
0110     if (m_builder->hasError()) {
0111         setErrorDescription(i18n("Meson builder error: %1", m_builder->errorDescription()));
0112     }
0113 }
0114 
0115 MesonManager::~MesonManager()
0116 {
0117     delete m_builder;
0118 }
0119 
0120 // *********************************
0121 // * AbstractFileManagerPlugin API *
0122 // *********************************
0123 
0124 IProjectFileManager::Features MesonManager::features() const
0125 {
0126     return IProjectFileManager::Files | IProjectFileManager::Folders | IProjectFileManager::Targets;
0127 }
0128 
0129 ProjectFolderItem* MesonManager::createFolderItem(IProject* project, const Path& path, ProjectBaseItem* parent)
0130 {
0131     // TODO: Maybe use meson targets instead
0132     if (QFile::exists(path.toLocalFile() + QStringLiteral("/meson.build")))
0133         return new ProjectBuildFolderItem(project, path, parent);
0134     else
0135         return AbstractFileManagerPlugin::createFolderItem(project, path, parent);
0136 }
0137 
0138 bool MesonManager::reload(KDevelop::ProjectFolderItem* item)
0139 {
0140     // "Inspired" by CMakeManager::reload
0141 
0142     IProject* project = item->project();
0143     if (!project->isReady()) {
0144         return false;
0145     }
0146 
0147     qCDebug(KDEV_Meson) << "reloading meson project" << project->name() << "; Path:" << item->path();
0148 
0149     KJob* job = createImportJob(item);
0150     project->setReloadJob(job);
0151     ICore::self()->runController()->registerJob(job);
0152     if (item == project->projectItem()) {
0153         connect(job, &KJob::finished, this, [project](KJob* job) -> void {
0154             if (job->error()) {
0155                 return;
0156             }
0157 
0158             emit KDevelop::ICore::self()->projectController()->projectConfigurationChanged(project);
0159             KDevelop::ICore::self()->projectController()->reparseProject(project);
0160         });
0161     }
0162 
0163     return true;
0164 }
0165 
0166 // ***************************
0167 // * IBuildSystemManager API *
0168 // ***************************
0169 
0170 void MesonManager::populateTargets(ProjectFolderItem* item, QVector<MesonTarget*> targets)
0171 {
0172     // Remove all old targets
0173     for (ProjectTargetItem* i : item->targetList()) {
0174         delete i;
0175     }
0176 
0177     // Add the new targets
0178     auto dirPath = item->path();
0179     for (MesonTarget* i : targets) {
0180         if (!dirPath.isDirectParentOf(i->definedIn())) {
0181             continue;
0182         }
0183 
0184         if (i->type().contains(QStringLiteral("executable"))) {
0185             auto outFiles = i->filename();
0186             Path outFile;
0187             if (outFiles.size() > 0) {
0188                 outFile = outFiles[0];
0189             }
0190             new MesonProjectExecutableTargetItem(item->project(), i->name(), item, outFile);
0191         } else if (i->type().contains(QStringLiteral("library"))) {
0192             new ProjectLibraryTargetItem(item->project(), i->name(), item);
0193         } else {
0194             new ProjectTargetItem(item->project(), i->name(), item);
0195         }
0196     }
0197 
0198     // Recurse
0199     for (ProjectFolderItem* i : item->folderList()) {
0200         QVector<MesonTarget*> filteredTargets;
0201         copy_if(begin(targets), end(targets), back_inserter(filteredTargets),
0202                 [i](MesonTarget* t) -> bool { return i->path().isParentOf(t->definedIn()); });
0203         populateTargets(i, filteredTargets);
0204     }
0205 }
0206 
0207 QList<ProjectTargetItem*> MesonManager::targets(ProjectFolderItem* item) const
0208 {
0209     Q_ASSERT(item);
0210     QList<ProjectTargetItem*> res = item->targetList();
0211     for (ProjectFolderItem* i : item->folderList()) {
0212         res << targets(i);
0213     }
0214     return res;
0215 }
0216 
0217 void MesonManager::onMesonInfoChanged(QString path, QString projectName)
0218 {
0219     qCDebug(KDEV_Meson) << "File" << path << "changed --> reparsing project";
0220     IProject* foundProject = ICore::self()->projectController()->findProjectByName(projectName);
0221     if (!foundProject) {
0222         return;
0223     }
0224 
0225     KJob* job = createImportJob(foundProject->projectItem());
0226     foundProject->setReloadJob(job);
0227     ICore::self()->runController()->registerJob(job);
0228     connect(job, &KJob::finished, this, [foundProject](KJob* job) -> void {
0229         if (job->error()) {
0230             return;
0231         }
0232 
0233         emit KDevelop::ICore::self()->projectController()->projectConfigurationChanged(foundProject);
0234         KDevelop::ICore::self()->projectController()->reparseProject(foundProject);
0235     });
0236 }
0237 
0238 KJob* MesonManager::createImportJob(ProjectFolderItem* item)
0239 {
0240     IProject* project = item->project();
0241     Q_ASSERT(project);
0242 
0243     qCDebug(KDEV_Meson) << "Importing project" << project->name();
0244 
0245     auto buildDir = Meson::currentBuildDir(project);
0246 
0247     KJob* configureJob = nullptr;
0248     if (!buildDir.isValid()) {
0249         configureJob = newBuildDirectory(project, &buildDir);
0250         if (!configureJob) {
0251             QString error = i18n("Importing %1 failed because no build directory could be created.", project->name());
0252             qCDebug(KDEV_Meson) << error;
0253             return new ErrorJob(this, error);
0254         }
0255     }
0256 
0257     auto introJob = new MesonIntrospectJob(
0258         project, buildDir, { MesonIntrospectJob::TARGETS, MesonIntrospectJob::TESTS, MesonIntrospectJob::PROJECTINFO },
0259         MesonIntrospectJob::BUILD_DIR, this);
0260 
0261     KDirWatchPtr watcher = m_projectWatchers[project];
0262     if (!watcher) {
0263         // Create a new watcher
0264         watcher = m_projectWatchers[project] = make_shared<KDirWatch>(nullptr);
0265         QString projectName = project->name();
0266 
0267         connect(watcher.get(), &KDirWatch::dirty, this, [=](QString p) { onMesonInfoChanged(p, projectName); });
0268         connect(watcher.get(), &KDirWatch::created, this, [=](QString p) { onMesonInfoChanged(p, projectName); });
0269     }
0270 
0271     Path watchFile = buildDir.buildDir;
0272     watchFile.addPath(QStringLiteral("meson-info"));
0273     watchFile.addPath(QStringLiteral("meson-info.json"));
0274     if (!watcher->contains(watchFile.path())) {
0275         qCDebug(KDEV_Meson) << "Start watching file" << watchFile;
0276         watcher->addFile(watchFile.path());
0277     }
0278 
0279     connect(introJob, &KJob::result, this, [this, introJob, item, project]() {
0280         auto targets = introJob->targets();
0281         auto tests = introJob->tests();
0282         if (!targets || !tests) {
0283             return;
0284         }
0285 
0286         // Remove old test suites before deleting them
0287         if (m_projectTestSuites[project]) {
0288             for (auto i : m_projectTestSuites[project]->testSuites()) {
0289                 ICore::self()->testController()->removeTestSuite(i.get());
0290             }
0291         }
0292 
0293         m_projectTargets[project] = targets;
0294         m_projectTestSuites[project] = tests;
0295         auto tgtList = targets->targets();
0296         QVector<MesonTarget*> tgtCopy;
0297         tgtCopy.reserve(tgtList.size());
0298         transform(begin(tgtList), end(tgtList), back_inserter(tgtCopy), [](const auto& a) { return a.get(); });
0299 
0300         populateTargets(item, tgtCopy);
0301 
0302         // Add test suites
0303         for (auto& i : tests->testSuites()) {
0304             ICore::self()->testController()->addTestSuite(i.get());
0305         }
0306     });
0307 
0308     QList<KJob*> jobs;
0309 
0310     // Configure the project if necessary
0311     if (!configureJob
0312         && m_builder->evaluateBuildDirectory(buildDir.buildDir, buildDir.mesonBackend)
0313             != MesonBuilder::MESON_CONFIGURED) {
0314         configureJob = builder()->configure(project);
0315     }
0316 
0317     if (configureJob) {
0318         jobs << configureJob;
0319     }
0320 
0321     jobs << AbstractFileManagerPlugin::createImportJob(item); // generate the file system listing
0322     jobs << introJob;
0323 
0324     Q_ASSERT(!jobs.contains(nullptr));
0325     auto composite = new ExecuteCompositeJob(this, jobs);
0326     composite->setAbortOnSubjobError(false);
0327     return composite;
0328 }
0329 
0330 Path MesonManager::buildDirectory(ProjectBaseItem* item) const
0331 {
0332     Q_ASSERT(item);
0333     Meson::BuildDir buildDir = Meson::currentBuildDir(item->project());
0334     return buildDir.buildDir;
0335 }
0336 
0337 IProjectBuilder* MesonManager::builder() const
0338 {
0339     return m_builder;
0340 }
0341 
0342 MesonSourcePtr MesonManager::sourceFromItem(KDevelop::ProjectBaseItem* item) const
0343 {
0344     Q_ASSERT(item);
0345     auto it = m_projectTargets.find(item->project());
0346     if (it == end(m_projectTargets)) {
0347         qCDebug(KDEV_Meson) << item->path().toLocalFile() << "not found";
0348         return {};
0349     }
0350 
0351     auto targets = *it;
0352     return targets->fileSource(item->path());
0353 }
0354 
0355 KDevelop::Path::List MesonManager::includeDirectories(KDevelop::ProjectBaseItem* item) const
0356 {
0357     auto src = sourceFromItem(item);
0358     if (!src) {
0359         return {};
0360     }
0361     return src->includeDirs();
0362 }
0363 
0364 KDevelop::Path::List MesonManager::frameworkDirectories(KDevelop::ProjectBaseItem*) const
0365 {
0366     return {};
0367 }
0368 
0369 QHash<QString, QString> MesonManager::defines(KDevelop::ProjectBaseItem* item) const
0370 {
0371     auto src = sourceFromItem(item);
0372     if (!src) {
0373         return {};
0374     }
0375     return src->defines();
0376 }
0377 
0378 QString MesonManager::extraArguments(KDevelop::ProjectBaseItem* item) const
0379 {
0380     auto src = sourceFromItem(item);
0381     if (!src) {
0382         return {};
0383     }
0384     return src->extraArgs().join(QLatin1Char(' '));
0385 }
0386 
0387 bool MesonManager::hasBuildInfo(KDevelop::ProjectBaseItem* item) const
0388 {
0389     auto src = sourceFromItem(item);
0390     if (!src) {
0391         return false;
0392     }
0393     return true;
0394 }
0395 
0396 KDevelop::Path MesonManager::compiler(KDevelop::ProjectTargetItem* item) const
0397 {
0398     const auto source = sourceFromItem(item);
0399     return source && !source->compiler().isEmpty() ? KDevelop::Path(source->compiler().constFirst()) : KDevelop::Path();
0400 }
0401 
0402 // ********************
0403 // * Custom functions *
0404 // ********************
0405 
0406 KJob* MesonManager::newBuildDirectory(IProject* project, Meson::BuildDir* outBuildDir)
0407 {
0408     Q_ASSERT(project);
0409     MesonNewBuildDir newBD(project);
0410 
0411     if (!newBD.exec() || !newBD.isConfigValid()) {
0412         qCWarning(KDEV_Meson) << "Failed to create new build directory for project " << project->name();
0413         return nullptr;
0414     }
0415 
0416     Meson::BuildDir buildDir = newBD.currentConfig();
0417     Meson::MesonConfig mesonCfg = Meson::getMesonConfig(project);
0418     buildDir.canonicalizePaths();
0419     mesonCfg.currentIndex = mesonCfg.addBuildDir(buildDir);
0420     Meson::writeMesonConfig(project, mesonCfg);
0421 
0422     if (outBuildDir) {
0423         *outBuildDir = buildDir;
0424     }
0425 
0426     return m_builder->configure(project, buildDir, newBD.mesonArgs());
0427 }
0428 
0429 QStringList MesonManager::supportedMesonBackends() const
0430 {
0431     // Maybe add support for other generators
0432     return { GENERATOR_NINJA };
0433 }
0434 
0435 QString MesonManager::defaultMesonBackend() const
0436 {
0437     return GENERATOR_NINJA;
0438 }
0439 
0440 Path MesonManager::findMeson() const
0441 {
0442     QString mesonPath;
0443 
0444     const static QStringList mesonExecutables = { QStringLiteral("meson"), QStringLiteral("meson.py") };
0445     const static QStringList mesonPaths
0446         = { QStringLiteral("%1/.local/bin").arg(QStandardPaths::standardLocations(QStandardPaths::HomeLocation)[0]) };
0447 
0448     for (const auto& i : mesonExecutables) {
0449         mesonPath = QStandardPaths::findExecutable(i);
0450         if (!mesonPath.isEmpty()) {
0451             break;
0452         }
0453 
0454         mesonPath = QStandardPaths::findExecutable(i, mesonPaths);
0455         if (!mesonPath.isEmpty()) {
0456             break;
0457         }
0458     }
0459 
0460     return Path(mesonPath);
0461 }
0462 
0463 // ***********
0464 // * IPlugin *
0465 // ***********
0466 
0467 ConfigPage* MesonManager::perProjectConfigPage(int number, const ProjectConfigOptions& options, QWidget* parent)
0468 {
0469     switch (number) {
0470     case 0:
0471         return new MesonConfigPage(this, options.project, parent);
0472     case 1:
0473         return new MesonRewriterPage(this, options.project, parent);
0474     }
0475     return nullptr;
0476 }
0477 
0478 int MesonManager::perProjectConfigPages() const
0479 {
0480     return 2;
0481 }
0482 
0483 #include "mesonmanager.moc"
0484 #include "moc_mesonmanager.cpp"