File indexing completed on 2024-05-05 04:40:09

0001 /*
0002     SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez <aleixpol@kde.org>
0003     SPDX-FileCopyrightText: 2017 Kevin Funk <kfunk@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only
0006 */
0007 
0008 #include "ninjajob.h"
0009 
0010 #include "ninjabuilder.h"
0011 
0012 #include <outputview/outputfilteringstrategies.h>
0013 #include <project/interfaces/ibuildsystemmanager.h>
0014 #include <project/projectmodel.h>
0015 #include <interfaces/iproject.h>
0016 #include <interfaces/icore.h>
0017 #include <interfaces/iprojectcontroller.h>
0018 
0019 #include <KLocalizedString>
0020 #include <KConfigGroup>
0021 
0022 #include <QFile>
0023 #include <QRegularExpression>
0024 #include <QStandardPaths>
0025 #include <QUrl>
0026 
0027 using namespace KDevelop;
0028 
0029 class NinjaJobCompilerFilterStrategy
0030     : public CompilerFilterStrategy
0031 {
0032 public:
0033     using CompilerFilterStrategy::CompilerFilterStrategy;
0034 
0035     IFilterStrategy::Progress progressInLine(const QString& line) override;
0036 };
0037 
0038 IFilterStrategy::Progress NinjaJobCompilerFilterStrategy::progressInLine(const QString& line)
0039 {
0040     // example string: [87/88] Building CXX object projectbuilders/ninjabuilder/CMakeFiles/kdevninja.dir/ninjajob.cpp.o
0041     static const QRegularExpression re(QStringLiteral("^\\[([0-9]+)\\/([0-9]+)\\] (.*)"));
0042 
0043     QRegularExpressionMatch match = re.match(line);
0044     if (match.hasMatch()) {
0045         const int current = match.capturedRef(1).toInt();
0046         const int total = match.capturedRef(2).toInt();
0047         if (current && total) {
0048             // this is output from ninja
0049             const QString action = match.captured(3);
0050             const int percent = qRound(( float )current / total * 100);
0051             return {
0052                        action, percent
0053             };
0054         }
0055     }
0056 
0057     return {};
0058 }
0059 
0060 NinjaJob::NinjaJob(KDevelop::ProjectBaseItem* item, CommandType commandType,
0061                    const QStringList& arguments, const QByteArray& signal, NinjaBuilder* parent)
0062     : OutputExecuteJob(parent)
0063     , m_isInstalling(false)
0064     , m_idx(item->index())
0065     , m_commandType(commandType)
0066     , m_signal(signal)
0067     , m_plugin(parent)
0068 {
0069     auto bsm = item->project()->buildSystemManager();
0070     auto buildDir = bsm->buildDirectory(item);
0071 
0072     setToolTitle(i18n("Ninja"));
0073     setCapabilities(Killable);
0074     setStandardToolView(KDevelop::IOutputView::BuildView);
0075     setBehaviours(KDevelop::IOutputView::AllowUserClose | KDevelop::IOutputView::AutoScroll);
0076     setFilteringStrategy(new NinjaJobCompilerFilterStrategy(buildDir.toUrl()));
0077     setProperties(NeedWorkingDirectory | PortableMessages | DisplayStderr | IsBuilderHint | PostProcessOutput);
0078 
0079     // hardcode the ninja output format so we can parse it reliably
0080     addEnvironmentOverride(QStringLiteral("NINJA_STATUS"), QStringLiteral("[%s/%t] "));
0081 
0082     *this << ninjaExecutable();
0083     *this << arguments;
0084 
0085     QStringList targets;
0086     for (const QString& arg : arguments) {
0087         if (!arg.startsWith(QLatin1Char('-'))) {
0088             targets << arg;
0089         }
0090     }
0091 
0092     QString title;
0093     if (!targets.isEmpty()) {
0094         title = i18n("Ninja (%1): %2", item->text(), targets.join(QLatin1Char(' ')));
0095     } else {
0096         title = i18n("Ninja (%1)", item->text());
0097     }
0098     setJobName(title);
0099 
0100     connect(this, &NinjaJob::finished, this, &NinjaJob::emitProjectBuilderSignal);
0101 }
0102 
0103 NinjaJob::~NinjaJob()
0104 {
0105     // prevent crash when emitting KJob::finished from ~KJob
0106     // (=> at this point NinjaJob is already destructed...)
0107     disconnect(this, &NinjaJob::finished, this, &NinjaJob::emitProjectBuilderSignal);
0108 }
0109 
0110 void NinjaJob::setIsInstalling(bool isInstalling)
0111 {
0112     m_isInstalling = isInstalling;
0113 }
0114 
0115 QString NinjaJob::ninjaExecutable()
0116 {
0117     QString path = QStandardPaths::findExecutable(QStringLiteral("ninja-build"));
0118     if (path.isEmpty()) {
0119         path = QStandardPaths::findExecutable(QStringLiteral("ninja"));
0120     }
0121     return path;
0122 }
0123 
0124 QUrl NinjaJob::workingDirectory() const
0125 {
0126     KDevelop::ProjectBaseItem* it = item();
0127     if (!it) {
0128         return QUrl();
0129     }
0130     KDevelop::IBuildSystemManager* bsm = it->project()->buildSystemManager();
0131     KDevelop::Path workingDir = bsm->buildDirectory(it);
0132     while (!QFile::exists(workingDir.toLocalFile() + QLatin1String("build.ninja"))) {
0133         KDevelop::Path upWorkingDir = workingDir.parent();
0134         if (!upWorkingDir.isValid() || upWorkingDir == workingDir) {
0135             return bsm->buildDirectory(it->project()->projectItem()).toUrl();
0136         }
0137         workingDir = upWorkingDir;
0138     }
0139     return workingDir.toUrl();
0140 }
0141 
0142 QStringList NinjaJob::privilegedExecutionCommand() const
0143 {
0144     KDevelop::ProjectBaseItem* it = item();
0145     if (!it) {
0146         return QStringList();
0147     }
0148     KSharedConfigPtr configPtr = it->project()->projectConfiguration();
0149     KConfigGroup builderGroup(configPtr, "NinjaBuilder");
0150 
0151     bool runAsRoot = builderGroup.readEntry("Install As Root", false);
0152     if (runAsRoot && m_isInstalling) {
0153         int suCommand = builderGroup.readEntry("Su Command", 0);
0154         QStringList arguments;
0155         switch (suCommand) {
0156         case 1:
0157             return QStringList{QStringLiteral("kdesudo"), QStringLiteral("-t")};
0158 
0159         case 2:
0160             return QStringList{QStringLiteral("sudo")};
0161 
0162         default:
0163             return QStringList{QStringLiteral("kdesu"), QStringLiteral("-t")};
0164         }
0165     }
0166     return QStringList();
0167 }
0168 
0169 void NinjaJob::emitProjectBuilderSignal(KJob* job)
0170 {
0171     if (!m_plugin) {
0172         return;
0173     }
0174 
0175     KDevelop::ProjectBaseItem* it = item();
0176     if (!it) {
0177         return;
0178     }
0179 
0180     if (job->error() == 0) {
0181         Q_ASSERT(!m_signal.isEmpty());
0182         QMetaObject::invokeMethod(m_plugin, m_signal.constData(), Q_ARG(KDevelop::ProjectBaseItem*, it));
0183     } else {
0184         QMetaObject::invokeMethod(m_plugin, "failed", Q_ARG(KDevelop::ProjectBaseItem*, it));
0185     }
0186 }
0187 
0188 void NinjaJob::postProcessStderr(const QStringList& lines)
0189 {
0190     appendLines(lines);
0191 }
0192 
0193 void NinjaJob::postProcessStdout(const QStringList& lines)
0194 {
0195     appendLines(lines);
0196 }
0197 
0198 void NinjaJob::appendLines(const QStringList& lines)
0199 {
0200     if (lines.isEmpty()) {
0201         return;
0202     }
0203 
0204     QStringList ret(lines);
0205     bool prev = false;
0206     for (QStringList::iterator it = ret.end(); it != ret.begin(); ) {
0207         --it;
0208         bool curr = it->startsWith(QLatin1Char('['));
0209         if ((prev && curr) || it->endsWith(QLatin1String("] "))) {
0210             it = ret.erase(it);
0211         }
0212         prev = curr;
0213     }
0214 
0215     model()->appendLines(ret);
0216 }
0217 
0218 KDevelop::ProjectBaseItem* NinjaJob::item() const
0219 {
0220     return KDevelop::ICore::self()->projectController()->projectModel()->itemFromIndex(m_idx);
0221 }
0222 
0223 NinjaJob::CommandType NinjaJob::commandType() const
0224 {
0225     return m_commandType;
0226 }
0227 
0228 #include "moc_ninjajob.cpp"