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"