File indexing completed on 2024-05-12 05:43:20

0001 /*
0002  * SPDX-FileCopyrightText: 2022 Pablo Rauzy <r .at. uzy.me>
0003  * SPDX-License-Identifier: GPL-2.0-or-later
0004  */
0005 
0006 #include <unistd.h>
0007 #include <csignal>
0008 
0009 #include <QAction>
0010 #include <QApplication>
0011 #include <QDebug>
0012 #include <QDir>
0013 #include <QFileInfo>
0014 #include <QIcon>
0015 #include <QMenu>
0016 #include <QMessageBox>
0017 #include <QObject>
0018 #include <QPointer>
0019 #include <QProcess>
0020 #include <QProcessEnvironment>
0021 #include <QString>
0022 
0023 #include <KConfigGroup>
0024 #include <KDialogJobUiDelegate>
0025 #include <KFileItemListProperties>
0026 #include <KJobUiDelegate>
0027 #include <KLocalizedString>
0028 #include <KPluginFactory>
0029 #include <KSharedConfig>
0030 #include <KTerminalLauncherJob>
0031 #include <KStringHandler>
0032 
0033 #include "makefileactions.h"
0034 
0035 #define MAKE_CMD "make" // on Debian install the bmake package and use bmake here to test this plugin using BSD make rather than GNU make
0036 
0037 K_PLUGIN_CLASS_WITH_JSON(MakefileActions, "makefileactions.json")
0038 
0039 MakefileActions::MakefileActions(QObject *parent, const QVariantList &)
0040     : KAbstractFileItemActionPlugin(parent)
0041 {
0042     KConfigGroup config(KSharedConfig::openConfig(QStringLiteral("dolphinrc")), QStringLiteral("MakefileActionsPlugin"));
0043     m_openTerminal = config.readEntry("open_terminal", false);
0044     m_isMaking = false;
0045     m_trustedFiles = config.readEntry("trusted_files", QStringList());
0046 }
0047 
0048 bool MakefileActions::isGNUMake()
0049 {
0050     QProcess proc;
0051     proc.start(QStringLiteral(MAKE_CMD), { QStringLiteral("--version") }, QIODevice::ReadOnly);
0052     while (proc.waitForReadyRead()) {
0053         char buffer[4096];
0054         while (proc.readLine(buffer, sizeof(buffer)) > 0) {
0055             if (QString::fromLocal8Bit(buffer).contains(QLatin1String("GNU"))) {
0056                 proc.kill();
0057                 proc.waitForFinished(500);
0058                 return true; // GNU Make
0059             }
0060         }
0061     }
0062     proc.kill();
0063     proc.waitForFinished(500);
0064     return false; // assume BSD Make
0065 }
0066 
0067 QStringList MakefileActions::listTargets_GNU(QProcess &proc, const QString &file) const
0068 {
0069     /* make -pRr : | sed '/Not a target/,+1 d' | grep -v '^\(#\|\s\)\|^$\| :\?= \|%' | cut -d':' -f1 | uniq | sort */
0070     // make -pRr :
0071     proc.start(QStringLiteral(MAKE_CMD), { QStringLiteral("-f"), file, QStringLiteral("-pRr"), QStringLiteral(":") }, QIODevice::ReadOnly);
0072     // sed '/Not a target/,+1 d' | grep -v '^\(#\|\s\)\|^$\| :\?= \|%' | cut -d':' -f1 | uniq
0073     QSet<QString> targetSet;
0074     bool nonTarget = false;
0075     while (proc.waitForReadyRead()) {
0076         char buffer[4096];
0077         while (proc.readLine(buffer, sizeof(buffer)) > 0) {
0078             // sed '/Not a target/,+1 d'
0079             if (nonTarget) {
0080                 nonTarget = false;
0081                 continue;
0082             }
0083             const QString line = QString::fromLocal8Bit(buffer);
0084             if (line.contains(QLatin1String("Not a target"))) {
0085                 nonTarget = true;
0086                 continue;
0087             }
0088             // | grep -v '^\(#\|\s\)\|^$\| :\?= \|%'
0089             if (line.size() == 0 || line[0] == QLatin1Char('#') || line[0] == QLatin1Char('\n') || line[0] == QLatin1Char('\t') || line.contains(QLatin1String(" = ")) || line.contains(QLatin1String(" := ")) || line.contains(QLatin1Char('%'))) {
0090                 continue;
0091             }
0092             // | cut -d':' -f1
0093             const QString target = line.section(QLatin1Char(':'), 0, 0);
0094             // heuristics to remove most special targets like .PHONY, .SILENT, etc.
0095             if (target[0] == QLatin1Char('.') && target.isUpper()) {
0096                 continue;
0097             }
0098             // | uniq
0099             targetSet << target;
0100         }
0101     }
0102     // | sort
0103     QStringList targetSortedList(targetSet.constBegin(), targetSet.constEnd());
0104     targetSortedList.sort();
0105 
0106     return targetSortedList;
0107 }
0108 
0109 QStringList MakefileActions::listTargets_BSD(QProcess &proc, const QString &file) const
0110 {
0111     /* make -r -d g3 : 2>&1 | grep ', flags 0, type \(8\|4,\|1\)' | grep -v '%' | cut -d',' -f1 | sed 's/^# //' | sort */
0112     // 2>&1
0113     proc.setProcessChannelMode(QProcess::MergedChannels);
0114     // make -r -d g3 :
0115     proc.start(QStringLiteral(MAKE_CMD), { QStringLiteral("-f"), file, QStringLiteral("-r"), QStringLiteral("-d"), QStringLiteral("g3"), QStringLiteral(":") }, QIODevice::ReadOnly);
0116     // grep ', flags 0, type \(8\|4,\|1\)' | grep -v '%' | cut -d',' -f1 | sed 's/^# //'
0117     QStringList targetList;
0118     while (proc.waitForReadyRead()) {
0119         char buffer[4096];
0120         while (proc.readLine(buffer, sizeof(buffer)) > 0) {
0121             const QString line = QString::fromLocal8Bit(buffer).chopped(1);
0122             // grep ', flags 0, type \(8\|4,\|1\)' | grep -v '%'
0123             if ((!line.contains(QLatin1String(", flags 0, type 8")) && !line.contains(QLatin1String(", flags 0, type 4,")) && !line.contains(QLatin1String(", flags 0, type 1"))) || line.contains(QLatin1Char('%'))) {
0124                 continue;
0125             }
0126             // | cut -d',' -f1 | sed 's/^# //'
0127             const QString target = line.mid(2).section(QLatin1Char(','), 0, 0);
0128             // heuristics to remove most special targets like .PHONY, .SILENT, etc
0129             if (target[0] == QLatin1Char('.') && target.isUpper()) {
0130                 continue;
0131             }
0132             targetList << target;
0133         }
0134     }
0135     // | sort
0136     targetList.sort();
0137 
0138     return targetList;
0139 }
0140 
0141 TargetTree MakefileActions::targetTree() const
0142 {
0143     QProcess proc;
0144     QFileInfo fileInfo(m_file);
0145     proc.setWorkingDirectory(fileInfo.absoluteDir().absolutePath());
0146     // LANG=C
0147     QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
0148     env.insert(QStringLiteral("LANG"), QStringLiteral("C"));
0149     proc.setProcessEnvironment(env);
0150 
0151     QStringList targetSortedList(isGNUMake() ? listTargets_GNU(proc, fileInfo.fileName()) : listTargets_BSD(proc, fileInfo.fileName()));
0152 
0153     proc.kill();
0154     proc.waitForFinished(500);
0155 
0156     // if there is no targets, stop right there
0157     if (targetSortedList.isEmpty()) {
0158         return TargetTree();
0159     }
0160 
0161     /* compute prefixes */
0162     QSet<QString> prefixSet;
0163     QString prev = targetSortedList.first();
0164     for (auto const &target : std::as_const(targetSortedList)) {
0165         int min = std::min(prev.size(), target.size());
0166         int i = 0;
0167         for (i = 0; i < min; ++i) {
0168             if (prev[i] == target[i]) continue;
0169             else break;
0170         }
0171         prefixSet.insert(prev.left(prev.lastIndexOf(QDir::separator(), i)));
0172         prev = target;
0173     }
0174     QStringList prefixSortedList(prefixSet.constBegin(), prefixSet.constEnd());
0175     prefixSortedList.sort();
0176 
0177     /* produce the submenu tree structure */
0178     TargetTree targets;
0179     for (auto const &prefix : std::as_const(prefixSortedList)) {
0180         targets.insert(prefix, false);
0181     }
0182     for (auto const &target : std::as_const(targetSortedList)) {
0183         targets.insert(target, true);
0184     }
0185 
0186     return targets;
0187 }
0188 
0189 void MakefileActions::buildMenu(QMenu *menu, const TargetTree &targets, QWidget *mainWindow)
0190 {
0191     QList<TargetTree> targetSortedList(targets.children());
0192     std::sort(targetSortedList.begin(), targetSortedList.end(), TargetTree::cmp);
0193     for (const TargetTree &tree : std::as_const(targetSortedList)) {
0194         QString title = tree.prefix().mid(targets.prefix().size());
0195         if (!targets.prefix().isEmpty() && title[0] == QDir::separator()) {
0196             title = title.mid(1);
0197         }
0198         title = KStringHandler::rsqueeze(title);
0199         if (tree.children().size() > 0) {
0200             QMenu *submenu = new QMenu(title + QDir::separator(), menu);
0201             submenu->setIcon(QIcon::fromTheme(QStringLiteral("folder-symbolic")));
0202             if (tree.isTarget()) {
0203                 addTarget(submenu, tree, title, mainWindow);
0204                 submenu->addSeparator();
0205             }
0206             buildMenu(submenu, tree, mainWindow);
0207             menu->addMenu(submenu);
0208         } else if (tree.isTarget()) {
0209             addTarget(menu, tree, title, mainWindow);
0210         }
0211     }
0212 }
0213 
0214 void MakefileActions::addTarget(QMenu *menu, const TargetTree &target, const QString &title, QWidget *mainWindow)
0215 {
0216     QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("run-build")), title, menu);
0217     action->setEnabled(!m_isMaking);
0218     action->setToolTip(i18n("Make '%1'%2.", target.prefix(), (m_openTerminal ? QStringLiteral(" (in a terminal)") : QStringLiteral(""))));
0219     connect(action, &QAction::triggered, this, [this, target, mainWindow]() {
0220         makeTarget(target.prefix(), mainWindow);
0221     });
0222     menu->addAction(action);
0223 }
0224 
0225 void MakefileActions::makeTarget(const QString &target, QWidget *mainWindow)
0226 {
0227     if (m_isMaking) {
0228         return;
0229     }
0230     QFileInfo fileInfo(m_file);
0231     if (!m_openTerminal) {
0232         if (!m_proc.isNull()) {
0233             delete m_proc;
0234         }
0235         m_proc = new QProcess(mainWindow);
0236         m_proc->setWorkingDirectory(fileInfo.absoluteDir().absolutePath());
0237         m_proc->setProgram(QStringLiteral(MAKE_CMD));
0238         m_proc->setArguments({QStringLiteral("-f"), fileInfo.fileName(), target});
0239         connect(m_proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [this, mainWindow, target](int exitCode, QProcess::ExitStatus exitStatus) {
0240             if (!m_isMaking) {
0241                 return;
0242             }
0243             if (exitStatus != QProcess::NormalExit || exitCode != 0) {
0244                 QMessageBox::warning(mainWindow, i18n("Makefile Actions"), i18n("An error occurred while making target '%1'.", target));
0245             }
0246             mainWindow->setCursor(Qt::ArrowCursor);
0247             m_isMaking = false;
0248             m_runningTarget.clear();
0249         });
0250         connect(m_proc, &QProcess::errorOccurred, this, [this, mainWindow, target](QProcess::ProcessError) {
0251             if (!m_isMaking) { // process has been canceled by the user
0252                 QMessageBox::information(mainWindow, i18n("Makefile Actions"), i18n("Running process for '%1' successfully stopped.", target));
0253             } else {
0254                 QMessageBox::critical(mainWindow, i18n("Makefile Actions"), i18n("An error occurred trying to make target '%1'.", target));
0255                 m_isMaking = false;
0256             }
0257             m_runningTarget.clear();
0258             mainWindow->setCursor(Qt::ArrowCursor);
0259         });
0260         m_isMaking = true;
0261         m_runningTarget = target;
0262         m_proc->start();
0263         mainWindow->setCursor(Qt::BusyCursor);
0264     } else {
0265         KTerminalLauncherJob *job = new KTerminalLauncherJob(QLatin1String(MAKE_CMD " -f ") + fileInfo.fileName() + QLatin1Char(' ') + target, mainWindow);
0266         job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, mainWindow));
0267         job->setWorkingDirectory(fileInfo.absoluteDir().absolutePath());
0268         job->start();
0269     }
0270 }
0271 
0272 QList<QAction *> MakefileActions::actions(const KFileItemListProperties &fileItemInfos, QWidget *mainWindow)
0273 {
0274     // we shall not be root
0275     if (geteuid() == 0) {
0276         return {};
0277     }
0278 
0279     // only a single local file
0280     if (fileItemInfos.urlList().size() != 1 || !fileItemInfos.isLocal()) {
0281         return {};
0282     }
0283 
0284     m_file = fileItemInfos.urlList()[0].toLocalFile();
0285 
0286     QMenu *menu = new QMenu(i18n("&Make…"), mainWindow);
0287     menu->setIcon(QIcon::fromTheme(QStringLiteral("text-x-makefile")));
0288 
0289     bool trustedFile = m_trustedFiles.contains(m_file);
0290     QAction *trust = new QAction(menu);
0291     trust->setToolTip(i18n("Only trusted files can be used by the Makefile Actions plugin."));
0292     trust->setCheckable(true);
0293     trust->setChecked(trustedFile);
0294     trust->setText(trustedFile ? i18n("Trusted file — uncheck to remove trust") : i18n("Untrusted file — check to trust"));
0295     trust->setIcon(trustedFile ? QIcon::fromTheme(QStringLiteral("checkbox")) : QIcon::fromTheme(QStringLiteral("action-unavailable-symbolic")));
0296     connect(trust, &QAction::toggled, this, [this, trustedFile, mainWindow]() {
0297         KConfigGroup config(KSharedConfig::openConfig(QStringLiteral("dolphinrc")), QStringLiteral("MakefileActionsPlugin"));
0298         if (trustedFile) {
0299             m_trustedFiles.removeAll(m_file);
0300         } else if (QMessageBox::question(mainWindow,
0301                                          i18n("Dolphin Makefile Plugin"),
0302                                          i18n("<b>Are you sure you can trust this file?</b><br>"
0303                                               "Trusted files may execute arbitrary code on context-menu invocation."))
0304                    == QMessageBox::Yes) {
0305             m_trustedFiles.append(m_file);
0306         }
0307         config.writeEntry("trusted_files", m_trustedFiles);
0308     });
0309     menu->addAction(trust);
0310 
0311     // if the file is not trusted, we don't go further
0312     if (!trustedFile) {
0313         return { menu->menuAction() };
0314     }
0315 
0316     QAction *openTerminal = new QAction(QIcon::fromTheme(QStringLiteral("utilities-terminal")), i18n("Open a terminal window"), menu);
0317     openTerminal->setToolTip(i18n("Open a new terminal window to see the output of making the chosen target."));
0318     openTerminal->setCheckable(true);
0319     openTerminal->setChecked(m_openTerminal);
0320     connect(openTerminal, &QAction::toggled, this, [this](bool checked) {
0321         m_openTerminal = checked;
0322         KConfigGroup config(KSharedConfig::openConfig(QStringLiteral("dolphinrc")), QStringLiteral("MakefileActionsPlugin"));
0323         config.writeEntry("open_terminal", checked);
0324     });
0325     menu->addAction(openTerminal);
0326 
0327     if (m_isMaking) {
0328         QAction *cancel = new QAction(QIcon::fromTheme(QStringLiteral("process-stop")), i18n("Cancel running process (%1)", KStringHandler::rsqueeze(m_runningTarget)), menu);
0329         cancel->setToolTip(i18n("Interrupt the currently running process (%1).", m_runningTarget));
0330         cancel->setEnabled(true);
0331         connect(cancel, &QAction::triggered, this, [this](){
0332             m_isMaking = false;
0333             m_runningTarget.clear();
0334             m_proc->kill(); // send ^C to the running process
0335         });
0336         menu->addAction(cancel);
0337     }
0338 
0339     menu->addSeparator();
0340 
0341     buildMenu(menu, targetTree(), mainWindow);
0342 
0343     return { menu->menuAction() };
0344 }
0345 
0346 #include "makefileactions.moc"
0347 
0348 #include "moc_makefileactions.cpp"