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

0001 /*
0002     SPDX-FileCopyrightText: 2006 Andreas Pakulat <apaku@gmx.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "qmakemanager.h"
0008 
0009 #include <QAction>
0010 #include <QDir>
0011 #include <QIcon>
0012 #include <QFileInfo>
0013 #include <QHash>
0014 #include <QList>
0015 #include <QUrl>
0016 
0017 #include <KIO/Global>
0018 #include <KConfigGroup>
0019 #include <KDirWatch>
0020 #include <KLocalizedString>
0021 #include <KPluginFactory>
0022 
0023 #include <interfaces/icore.h>
0024 #include <interfaces/iproject.h>
0025 #include <interfaces/contextmenuextension.h>
0026 #include <interfaces/context.h>
0027 #include <interfaces/iruncontroller.h>
0028 #include <interfaces/iprojectcontroller.h>
0029 #include <interfaces/iplugincontroller.h>
0030 #include <project/projectmodel.h>
0031 #include <serialization/indexedstring.h>
0032 
0033 #include <qmakebuilder/iqmakebuilder.h>
0034 
0035 #include "qmakemodelitems.h"
0036 #include "qmakeprojectfile.h"
0037 #include "qmakecache.h"
0038 #include "qmakemkspecs.h"
0039 #include "qmakejob.h"
0040 #include "qmakebuilddirchooserdialog.h"
0041 #include "qmakeconfig.h"
0042 #include "qmakeutils.h"
0043 #include <debug.h>
0044 
0045 using namespace KDevelop;
0046 
0047 // BEGIN Helpers
0048 
0049 QMakeFolderItem* findQMakeFolderParent(ProjectBaseItem* item)
0050 {
0051     QMakeFolderItem* p = nullptr;
0052     while (!p && item) {
0053         p = dynamic_cast<QMakeFolderItem*>(item);
0054         item = item->parent();
0055     }
0056     return p;
0057 }
0058 
0059 // END Helpers
0060 
0061 K_PLUGIN_FACTORY_WITH_JSON(QMakeSupportFactory, "kdevqmakemanager.json", registerPlugin<QMakeProjectManager>();)
0062 
0063 QMakeProjectManager::QMakeProjectManager(QObject* parent, const QVariantList&)
0064     : AbstractFileManagerPlugin(QStringLiteral("kdevqmakemanager"), parent)
0065     , IBuildSystemManager()
0066 {
0067     IPlugin* i = core()->pluginController()->pluginForExtension(QStringLiteral("org.kdevelop.IQMakeBuilder"));
0068     Q_ASSERT(i);
0069     m_builder = i->extension<IQMakeBuilder>();
0070     Q_ASSERT(m_builder);
0071 
0072     connect(this, SIGNAL(folderAdded(KDevelop::ProjectFolderItem*)), this,
0073             SLOT(slotFolderAdded(KDevelop::ProjectFolderItem*)));
0074 
0075     m_runQMake = new QAction(QIcon::fromTheme(QStringLiteral("qtlogo")), i18nc("@action", "Run QMake"), this);
0076     connect(m_runQMake, &QAction::triggered, this, &QMakeProjectManager::slotRunQMake);
0077 }
0078 
0079 QMakeProjectManager::~QMakeProjectManager()
0080 {
0081 }
0082 
0083 IProjectFileManager::Features QMakeProjectManager::features() const
0084 {
0085     return Features(Folders | Targets | Files);
0086 }
0087 
0088 bool QMakeProjectManager::isValid(const Path& path, const bool isFolder, IProject* project) const
0089 {
0090     if (!isFolder && path.lastPathSegment().startsWith(QLatin1String("Makefile"))) {
0091         return false;
0092     }
0093     return AbstractFileManagerPlugin::isValid(path, isFolder, project);
0094 }
0095 
0096 Path QMakeProjectManager::buildDirectory(ProjectBaseItem* item) const
0097 {
0098     /// TODO: support includes by some other parent or sibling in a different file-tree-branch
0099     QMakeFolderItem* qmakeItem = findQMakeFolderParent(item);
0100     Path dir;
0101     if (qmakeItem) {
0102         if (!qmakeItem->parent()) {
0103             // build root item
0104             dir = QMakeConfig::buildDirFromSrc(qmakeItem->project(), qmakeItem->path());
0105         } else {
0106             // build sub-item
0107             const auto proFiles = qmakeItem->projectFiles();
0108             for (QMakeProjectFile* pro : proFiles) {
0109                 if (QDir(pro->absoluteDir()) == QFileInfo(qmakeItem->path().toUrl().toLocalFile() + QLatin1Char('/')).absoluteDir()
0110                     || pro->hasSubProject(qmakeItem->path().toUrl().toLocalFile())) {
0111                     // get path from project root and it to buildDir
0112                     dir = QMakeConfig::buildDirFromSrc(qmakeItem->project(), Path(pro->absoluteDir()));
0113                     break;
0114                 }
0115             }
0116         }
0117     }
0118 
0119     qCDebug(KDEV_QMAKE) << "build dir for" << item->text() << item->path() << "is:" << dir;
0120     return dir;
0121 }
0122 
0123 ProjectFolderItem* QMakeProjectManager::createFolderItem(IProject* project, const Path& path, ProjectBaseItem* parent)
0124 {
0125     if (!parent) {
0126         return projectRootItem(project, path);
0127     } else if (ProjectFolderItem* buildFolder = buildFolderItem(project, path, parent)) {
0128         // child folder in a qmake folder
0129         return buildFolder;
0130     } else {
0131         return AbstractFileManagerPlugin::createFolderItem(project, path, parent);
0132     }
0133 }
0134 
0135 ProjectFolderItem* QMakeProjectManager::projectRootItem(IProject* project, const Path& path)
0136 {
0137     QDir dir(path.toLocalFile());
0138 
0139     auto item = new QMakeFolderItem(project, path);
0140 
0141     const auto projectfiles = dir.entryList(QStringList() << QStringLiteral("*.pro"));
0142     if (projectfiles.isEmpty()) {
0143         return item;
0144     }
0145 
0146     QHash<QString, QString> qmvars = QMakeUtils::queryQMake(project);
0147     const QString mkSpecFile = QMakeConfig::findBasicMkSpec(qmvars);
0148     Q_ASSERT(!mkSpecFile.isEmpty());
0149     auto* mkspecs = new QMakeMkSpecs(mkSpecFile, qmvars);
0150     mkspecs->setProject(project);
0151     mkspecs->read();
0152     QMakeCache* cache = findQMakeCache(project);
0153     if (cache) {
0154         cache->setMkSpecs(mkspecs);
0155         cache->read();
0156     }
0157 
0158     for (const auto& projectfile : projectfiles) {
0159         Path proPath(path, projectfile);
0160         /// TODO: use Path in QMakeProjectFile
0161         auto* scope = new QMakeProjectFile(proPath.toLocalFile());
0162         scope->setProject(project);
0163         scope->setMkSpecs(mkspecs);
0164         scope->setOwnMkSpecs(true);
0165         if (cache) {
0166             scope->setQMakeCache(cache);
0167         }
0168         scope->read();
0169         qCDebug(KDEV_QMAKE) << "top-level scope with variables:" << scope->variables();
0170         item->addProjectFile(scope);
0171     }
0172     return item;
0173 }
0174 
0175 ProjectFolderItem* QMakeProjectManager::buildFolderItem(IProject* project, const Path& path, ProjectBaseItem* parent)
0176 {
0177     // find .pro or .pri files in dir
0178     QDir dir(path.toLocalFile());
0179     const QStringList projectFiles = dir.entryList(QStringList{QStringLiteral("*.pro"), QStringLiteral("*.pri")},
0180                                              QDir::Files);
0181     if (projectFiles.isEmpty()) {
0182         return nullptr;
0183     }
0184 
0185     auto folderItem = new QMakeFolderItem(project, path, parent);
0186 
0187     // TODO: included by not-parent file (in a nother file-tree-branch).
0188     QMakeFolderItem* qmakeParent = findQMakeFolderParent(parent);
0189     if (!qmakeParent) {
0190         // happens for bad qmake configurations
0191         return nullptr;
0192     }
0193 
0194     for (const QString& file : projectFiles) {
0195         const QString absFile = dir.absoluteFilePath(file);
0196 
0197         // TODO: multiple includes by different .pro's
0198         QMakeProjectFile* parentPro = nullptr;
0199         const auto proFiles = qmakeParent->projectFiles();
0200         for (QMakeProjectFile* p : proFiles) {
0201             if (p->hasSubProject(absFile)) {
0202                 parentPro = p;
0203                 break;
0204             }
0205         }
0206         if (!parentPro && file.endsWith(QLatin1String(".pri"))) {
0207             continue;
0208         }
0209         qCDebug(KDEV_QMAKE) << "add project file:" << absFile;
0210         if (parentPro) {
0211             qCDebug(KDEV_QMAKE) << "parent:" << parentPro->absoluteFile();
0212         } else {
0213             qCDebug(KDEV_QMAKE) << "no parent, assume project root";
0214         }
0215 
0216         auto qmscope = new QMakeProjectFile(absFile);
0217         qmscope->setProject(project);
0218 
0219         const QFileInfo info(absFile);
0220         const QDir d = info.dir();
0221         /// TODO: cleanup
0222         if (parentPro) {
0223             // subdir
0224             if (QMakeCache* cache = findQMakeCache(project, Path(d.canonicalPath()))) {
0225                 cache->setMkSpecs(parentPro->mkSpecs());
0226                 cache->read();
0227                 qmscope->setQMakeCache(cache);
0228             } else {
0229                 qmscope->setQMakeCache(parentPro->qmakeCache());
0230             }
0231 
0232             qmscope->setMkSpecs(parentPro->mkSpecs());
0233         } else {
0234             // new project
0235             auto* root = dynamic_cast<QMakeFolderItem*>(project->projectItem());
0236             Q_ASSERT(root);
0237             qmscope->setMkSpecs(root->projectFiles().first()->mkSpecs());
0238             if (root->projectFiles().first()->qmakeCache()) {
0239                 qmscope->setQMakeCache(root->projectFiles().first()->qmakeCache());
0240             }
0241         }
0242 
0243         if (qmscope->read()) {
0244             // TODO: only on read?
0245             folderItem->addProjectFile(qmscope);
0246         } else {
0247             delete qmscope;
0248             return nullptr;
0249         }
0250     }
0251 
0252     return folderItem;
0253 }
0254 
0255 void QMakeProjectManager::slotFolderAdded(ProjectFolderItem* folder)
0256 {
0257     auto* qmakeParent = dynamic_cast<QMakeFolderItem*>(folder);
0258     if (!qmakeParent) {
0259         return;
0260     }
0261 
0262     qCDebug(KDEV_QMAKE) << "adding targets for" << folder->path();
0263     const auto proFiles = qmakeParent->projectFiles();
0264     for (QMakeProjectFile* pro : proFiles) {
0265         const auto targets = pro->targets();
0266         for (const auto& s : targets) {
0267             if (!isValid(Path(folder->path(), s), false, folder->project())) {
0268                 continue;
0269             }
0270             qCDebug(KDEV_QMAKE) << "adding target:" << s;
0271             Q_ASSERT(!s.isEmpty());
0272             auto target = new QMakeTargetItem(pro, folder->project(), s, folder);
0273             const auto files = pro->filesForTarget(s);
0274             for (const auto& path : files) {
0275                 new ProjectFileItem(folder->project(), Path(path), target);
0276                 /// TODO: signal?
0277             }
0278         }
0279     }
0280 }
0281 
0282 ProjectFolderItem* QMakeProjectManager::import(IProject* project)
0283 {
0284     const Path dirName = project->path();
0285     if (dirName.isRemote()) {
0286         // FIXME turn this into a real warning
0287         qCWarning(KDEV_QMAKE) << "not a local file. QMake support doesn't handle remote projects";
0288         return nullptr;
0289     }
0290 
0291     QMakeUtils::checkForNeedingConfigure(project);
0292 
0293     ProjectFolderItem* ret = AbstractFileManagerPlugin::import(project);
0294     connect(projectWatcher(project), &KDirWatch::dirty, this, &QMakeProjectManager::slotDirty);
0295     return ret;
0296 }
0297 
0298 void QMakeProjectManager::slotDirty(const QString& path)
0299 {
0300     if (!path.endsWith(QLatin1String(".pro")) && !path.endsWith(QLatin1String(".pri"))) {
0301         return;
0302     }
0303 
0304     QFileInfo info(path);
0305     if (!info.isFile()) {
0306         return;
0307     }
0308 
0309     const QUrl url = QUrl::fromLocalFile(path);
0310     if (!isValid(Path(url), false, nullptr)) {
0311         return;
0312     }
0313 
0314     IProject* project = ICore::self()->projectController()->findProjectForUrl(url);
0315     if (!project) {
0316         // this can happen when we create/remove lots of files in a
0317         // sub dir of a project - ignore such cases for now
0318         return;
0319     }
0320 
0321     bool finished = false;
0322     const auto folders = project->foldersForPath(IndexedString(KIO::upUrl(url)));
0323     for (ProjectFolderItem* folder : folders) {
0324         if (auto* qmakeFolder = dynamic_cast<QMakeFolderItem*>(folder)) {
0325             const auto proFiles = qmakeFolder->projectFiles();
0326             for (QMakeProjectFile* pro : proFiles) {
0327                 if (pro->absoluteFile() == path) {
0328                     // TODO: children
0329                     // TODO: cache added
0330                     qCDebug(KDEV_QMAKE) << "reloading" << pro << path;
0331                     pro->read();
0332                 }
0333             }
0334             finished = true;
0335         } else if (ProjectFolderItem* newFolder = buildFolderItem(project, folder->path(), folder->parent())) {
0336             qCDebug(KDEV_QMAKE) << "changing from normal folder to qmake project folder:" << folder->path().toUrl();
0337             // .pro / .pri file did not exist before
0338             while (folder->rowCount()) {
0339                 newFolder->appendRow(folder->takeRow(0));
0340             }
0341             folder->parent()->removeRow(folder->row());
0342             folder = newFolder;
0343             finished = true;
0344         }
0345         if (finished) {
0346             // remove existing targets and readd them
0347             for (int i = 0; i < folder->rowCount(); ++i) {
0348                 if (folder->child(i)->target()) {
0349                     folder->removeRow(i);
0350                 }
0351             }
0352             /// TODO: put into it's own function once we add more stuff to that slot
0353             slotFolderAdded(folder);
0354             break;
0355         }
0356     }
0357 }
0358 
0359 QList<ProjectTargetItem*> QMakeProjectManager::targets(ProjectFolderItem* item) const
0360 {
0361     Q_UNUSED(item)
0362     return QList<ProjectTargetItem*>();
0363 }
0364 
0365 IProjectBuilder* QMakeProjectManager::builder() const
0366 {
0367     Q_ASSERT(m_builder);
0368     return m_builder;
0369 }
0370 
0371 Path::List QMakeProjectManager::collectDirectories(ProjectBaseItem* item, const bool collectIncludes) const
0372 {
0373     Path::List list;
0374     QMakeFolderItem* folder = findQMakeFolderParent(item);
0375     if (folder) {
0376         const auto proFiles = folder->projectFiles();
0377         for (QMakeProjectFile* pro : proFiles) {
0378             if (pro->files().contains(item->path().toLocalFile())) {
0379                 const QStringList directories = collectIncludes ? pro->includeDirectories() : pro->frameworkDirectories();
0380                 for (const QString& dir : directories) {
0381                     Path path(dir);
0382                     if (!list.contains(path)) {
0383                         list << path;
0384                     }
0385                 }
0386             }
0387         }
0388         if (list.isEmpty()) {
0389             // fallback for new files, use all possible include dirs
0390             const auto proFiles = folder->projectFiles();
0391             for (QMakeProjectFile* pro : proFiles) {
0392                 const QStringList directories = collectIncludes ? pro->includeDirectories() : pro->frameworkDirectories();
0393                 for (const QString& dir : directories) {
0394                     Path path(dir);
0395                     if (!list.contains(path)) {
0396                         list << path;
0397                     }
0398                 }
0399             }
0400         }
0401         // make sure the base dir is included
0402         if (!list.contains(folder->path())) {
0403             list << folder->path();
0404         }
0405         // qCDebug(KDEV_QMAKE) << "include dirs for" << item->path() << ":" << list;
0406     }
0407     return list;
0408 }
0409 
0410 Path::List QMakeProjectManager::includeDirectories(ProjectBaseItem* item) const
0411 {
0412     return collectDirectories(item);
0413 }
0414 
0415 Path::List QMakeProjectManager::frameworkDirectories(ProjectBaseItem* item) const
0416 {
0417     return collectDirectories(item, false);
0418 }
0419 
0420 QHash<QString, QString> QMakeProjectManager::defines(ProjectBaseItem* item) const
0421 {
0422     QHash<QString, QString> d;
0423     QMakeFolderItem* folder = findQMakeFolderParent(item);
0424     if (!folder) {
0425         // happens for bad qmake configurations
0426         return d;
0427     }
0428     const auto proFiles = folder->projectFiles();
0429     for (QMakeProjectFile* pro : proFiles) {
0430         const auto defines = pro->defines();
0431         for (const auto& def : defines) {
0432             d.insert(def.first, def.second);
0433         }
0434     }
0435     return d;
0436 }
0437 
0438 QString QMakeProjectManager::extraArguments(KDevelop::ProjectBaseItem *item) const
0439 {
0440     QMakeFolderItem* folder = findQMakeFolderParent(item);
0441     if (!folder) {
0442         // happens for bad qmake configurations
0443         return {};
0444     }
0445 
0446     QStringList d;
0447     const auto proFiles = folder->projectFiles();
0448     for (QMakeProjectFile* pro : proFiles) {
0449         d << pro->extraArguments();
0450     }
0451     return d.join(QLatin1Char(' '));
0452 }
0453 
0454 bool QMakeProjectManager::hasBuildInfo(KDevelop::ProjectBaseItem* item) const
0455 {
0456     return findQMakeFolderParent(item);
0457 }
0458 
0459 QMakeCache* QMakeProjectManager::findQMakeCache(IProject* project, const Path& path) const
0460 {
0461     QDir curdir(QMakeConfig::buildDirFromSrc(project, !path.isValid() ? project->path() : path).toLocalFile());
0462     curdir.makeAbsolute();
0463     while (!curdir.exists(QStringLiteral(".qmake.cache")) && !curdir.isRoot() && curdir.cdUp()) {
0464         qCDebug(KDEV_QMAKE) << curdir;
0465     }
0466 
0467     if (curdir.exists(QStringLiteral(".qmake.cache"))) {
0468         qCDebug(KDEV_QMAKE) << "Found QMake cache in " << curdir.absolutePath();
0469         return new QMakeCache(curdir.canonicalPath() + QLatin1String("/.qmake.cache"));
0470     }
0471     return nullptr;
0472 }
0473 
0474 ContextMenuExtension QMakeProjectManager::contextMenuExtension(Context* context, QWidget* parent)
0475 {
0476     Q_UNUSED(parent);
0477 
0478     ContextMenuExtension ext;
0479 
0480     if (context->hasType(Context::ProjectItemContext)) {
0481         auto* pic = dynamic_cast<ProjectItemContext*>(context);
0482         Q_ASSERT(pic);
0483         if (pic->items().isEmpty()) {
0484             return ext;
0485         }
0486 
0487         m_actionItem = dynamic_cast<QMakeFolderItem*>(pic->items().first());
0488         if (m_actionItem) {
0489             ext.addAction(ContextMenuExtension::ProjectGroup, m_runQMake);
0490         }
0491     }
0492 
0493     return ext;
0494 }
0495 
0496 void QMakeProjectManager::slotRunQMake()
0497 {
0498     Q_ASSERT(m_actionItem);
0499 
0500     Path srcDir = m_actionItem->path();
0501     Path buildDir = QMakeConfig::buildDirFromSrc(m_actionItem->project(), srcDir);
0502     auto* job = new QMakeJob(srcDir.toLocalFile(), buildDir.toLocalFile(), this);
0503 
0504     job->setQMakePath(QMakeConfig::qmakeExecutable(m_actionItem->project()));
0505 
0506     KConfigGroup cg(m_actionItem->project()->projectConfiguration(), QMakeConfig::CONFIG_GROUP);
0507     QString installPrefix = cg.readEntry(QMakeConfig::INSTALL_PREFIX, QString());
0508     if (!installPrefix.isEmpty())
0509         job->setInstallPrefix(installPrefix);
0510     job->setBuildType(cg.readEntry<int>(QMakeConfig::BUILD_TYPE, 0));
0511     job->setExtraArguments(cg.readEntry(QMakeConfig::EXTRA_ARGUMENTS, QString()));
0512 
0513     KDevelop::ICore::self()->runController()->registerJob(job);
0514 }
0515 
0516 KDevelop::Path QMakeProjectManager::compiler(KDevelop::ProjectTargetItem* p) const
0517 {
0518     Q_UNUSED(p);
0519     return {};
0520 }
0521 
0522 #include "qmakemanager.moc"
0523 #include "moc_qmakemanager.cpp"