1// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5#include "buildmanager.h"
6#include "common/widget/outputpane.h"
7#include "common/util/commandparser.h"
8#include "problemoutputpane.h"
9#include "commonparser.h"
10#include "transceiver/buildersender.h"
11#include "compileoutputpane.h"
12
13#include "services/builder/builderservice.h"
14#include "services/builder/ioutputparser.h"
15#include "services/window/windowservice.h"
16#include "services/project/projectinfo.h"
17#include "services/builder/buildergenerator.h"
18#include "services/option/optionmanager.h"
19#include "services/project/projectservice.h"
20
21#include "base/abstractaction.h"
22
23#include <QCoreApplication>
24
25using namespace dpfservice;
26
27class BuildManagerPrivate
28{
29 friend class BuildManager;
30
31 QSharedPointer<QAction> buildAction;
32 QSharedPointer<QAction> rebuildAction;
33 QSharedPointer<QAction> cleanAction;
34 QSharedPointer<QAction> cancelAction;
35
36 CompileOutputPane *compileOutputPane = nullptr;
37 ProblemOutputPane *problemOutputPane = nullptr;
38
39 QString activedKitName;
40 QString activedWorkingDir;
41
42 std::unique_ptr<IOutputParser> outputParser = nullptr;
43
44 QProcess cmdProcess;
45 QFuture<void> buildThread;
46
47 BuildState currentState = BuildState::kNoBuild;
48};
49
50BuildManager *BuildManager::instance()
51{
52 static BuildManager ins;
53 return &ins;
54}
55
56BuildManager::BuildManager(QObject *parent)
57 : QObject(parent)
58 , d(new BuildManagerPrivate())
59{
60 addMenu();
61
62 d->compileOutputPane = new CompileOutputPane();
63 d->problemOutputPane = new ProblemOutputPane();
64 d->outputParser.reset(new CommonParser());
65 connect(d->outputParser.get(), &IOutputParser::addOutput, this, &BuildManager::addOutput, Qt::DirectConnection);
66 connect(d->outputParser.get(), &IOutputParser::addTask, d->problemOutputPane, &ProblemOutputPane::addTask, Qt::DirectConnection);
67
68 QObject::connect(this, &BuildManager::sigOutputCompileInfo, this, &BuildManager::slotOutputCompileInfo);
69 QObject::connect(this, &BuildManager::sigOutputProblemInfo, this, &BuildManager::slotOutputProblemInfo);
70
71 qRegisterMetaType<BuildState>("BuildState");
72 qRegisterMetaType<BuildCommandInfo>("BuildCommandInfo");
73 QObject::connect(this, &BuildManager::sigBuildState, this, &BuildManager::slotBuildState);
74 QObject::connect(this, &BuildManager::sigOutputNotify, this, &BuildManager::slotOutputNotify);
75 QObject::connect(this, &BuildManager::sigResetBuildUI, this, &BuildManager::slotResetBuildUI);
76}
77
78BuildManager::~BuildManager()
79{
80 if (d) {
81 delete d;
82 }
83}
84
85void BuildManager::addMenu()
86{
87 auto &ctx = dpfInstance.serviceContext();
88 auto windowService = ctx.service<WindowService>(WindowService::name());
89 if (!windowService)
90 return;
91
92 auto actionInit = [&](QAction *action, QString actionID, QKeySequence key, QString iconFileName){
93 ActionManager::getInstance()->registerAction(action, actionID, action->text(), key, iconFileName);
94 AbstractAction *actionImpl = new AbstractAction(action);
95 windowService->addAction(dpfservice::MWM_BUILD, actionImpl);
96 };
97
98 d->buildAction.reset(new QAction(MWMBA_BUILD));
99 actionInit(d->buildAction.get(), "Build.Build", QKeySequence(Qt::Modifier::CTRL | Qt::Key::Key_B),
100 ":/buildercore/images/build.png");
101 windowService->addToolBarActionItem("toolbar.Build", d->buildAction.get(), "Build.End");
102
103 d->rebuildAction.reset(new QAction(MWMBA_REBUILD));
104 actionInit(d->rebuildAction.get(), "Build.Rebuild", QKeySequence(Qt::Modifier::CTRL | Qt::Modifier::SHIFT | Qt::Key::Key_B),
105 ":/buildercore/images/rebuild.png");
106
107 d->cleanAction.reset(new QAction(MWMBA_CLEAN));
108 actionInit(d->cleanAction.get(), "Build.Clean", QKeySequence(Qt::Modifier::CTRL | Qt::Modifier::SHIFT | Qt::Key::Key_C),
109 ":/buildercore/images/clean.svg");
110
111 d->cancelAction.reset(new QAction(MWMBA_CANCEL));
112 actionInit(d->cancelAction.get(), "Build.Cancel", QKeySequence(Qt::Modifier::ALT | Qt::Key::Key_Backspace),
113 ":/buildercore/images/cancel.svg");
114
115 QObject::connect(d->buildAction.get(), &QAction::triggered,
116 this, &BuildManager::buildProject, Qt::DirectConnection);
117 QObject::connect(d->rebuildAction.get(), &QAction::triggered,
118 this, &BuildManager::rebuildProject, Qt::DirectConnection);
119 QObject::connect(d->cleanAction.get(), &QAction::triggered,
120 this, &BuildManager::cleanProject, Qt::DirectConnection);
121 QObject::connect(d->cancelAction.get(), &QAction::triggered,
122 this, &BuildManager::cancelBuild, Qt::DirectConnection);
123}
124
125void BuildManager::buildProject()
126{
127 execBuildStep({Build});
128}
129
130void BuildManager::rebuildProject()
131{
132 execBuildStep({Clean, Build});
133}
134
135void BuildManager::cleanProject()
136{
137 execBuildStep({Clean});
138}
139
140void BuildManager::cancelBuild()
141{
142 if (d->currentState == kBuilding) {
143 d->buildThread.cancel();
144 disconnectSignals();
145 d->cmdProcess.kill();
146 }
147}
148
149
150void BuildManager::execBuildStep(QList<BuildMenuType> menuTypelist)
151{
152 if(!canStartBuild()) {
153 QMetaObject::invokeMethod(this, "message",
154 Q_ARG(QString, "The builder is running, please try again later!"));
155 return;
156 }
157
158 auto &ctx = dpfInstance.serviceContext();
159 ProjectService *projectService = ctx.service<ProjectService>(ProjectService::name());
160 if (!projectService || !projectService->getProjectInfo)
161 return;
162
163 ProjectInfo projectInfo = projectService->getProjectInfo(d->activedKitName, d->activedWorkingDir);
164 if (!projectInfo.isVaild())
165 return;
166
167 auto builderService = ctx.service<BuilderService>(BuilderService::name());
168 if (builderService) {
169 auto generator = builderService->create<BuilderGenerator>(d->activedKitName);
170 if (generator) {
171 emit sigResetBuildUI();
172 generator->appendOutputParser(d->outputParser);
173 QList<BuildCommandInfo> list;
174 foreach (auto menuType, menuTypelist) {
175 BuildCommandInfo info = generator->getMenuCommand(menuType, projectInfo);
176 QString retMsg;
177 bool ret = generator->checkCommandValidity(info, retMsg);
178 if (!ret) {
179 outputLog(retMsg, OutputPane::OutputFormat::StdErr);
180 continue;
181 }
182 list.append(info);
183 }
184 execCommands(list, false);
185 }
186 }
187}
188
189CompileOutputPane *BuildManager::getCompileOutputPane() const
190{
191 return d->compileOutputPane;
192}
193
194ProblemOutputPane *BuildManager::getProblemOutputPane() const
195{
196 return d->problemOutputPane;
197}
198
199void BuildManager::slotResetBuildUI()
200{
201 d->compileOutputPane->clearContents();
202 d->problemOutputPane->clearContents();
203
204 editor.switchContext(tr("Co&mpile Output"));
205}
206
207void BuildManager::setActivedProjectInfo(const QString &kitName, const QString &workingDir)
208{
209 d->activedKitName = kitName;
210 d->activedWorkingDir = workingDir;
211}
212
213void BuildManager::clearActivedProjectInfo()
214{
215 d->activedKitName.clear();
216 d->activedWorkingDir.clear();
217}
218
219bool BuildManager::handleCommand(const QList<BuildCommandInfo> &commandInfo, bool isSynchronous)
220{
221 if(!canStartBuild()) {
222 QMetaObject::invokeMethod(this, "message",
223 Q_ARG(QString, "The builder is running, please try again later!"));
224 return false;
225 }
226
227 auto &ctx = dpfInstance.serviceContext();
228 auto builderService = ctx.service<BuilderService>(BuilderService::name());
229 if (builderService) {
230 auto generator = builderService->create<BuilderGenerator>(commandInfo.at(0).kitName);
231 if (generator) {
232 emit sigResetBuildUI();
233 generator->appendOutputParser(d->outputParser);
234 QString retMsg;
235 bool ret = generator->checkCommandValidity(commandInfo.at(0), retMsg);
236 if (!ret) {
237 outputLog(retMsg, OutputPane::OutputFormat::StdErr);
238 return false;
239 }
240 }
241 execCommands(commandInfo, isSynchronous);
242 }
243 return true;
244}
245
246bool BuildManager::execCommands(const QList<BuildCommandInfo> &commandList, bool isSynchronous)
247{
248 //Synchronous execution is required in commandLine build model
249 if (isSynchronous) {
250 if (!commandList.isEmpty()) {
251 for (auto command : commandList) {
252 execCommand(command);
253 }
254 }
255 } else {
256 if (!commandList.isEmpty()) {
257 d->buildThread = QtConcurrent::run([=](){
258 QMutexLocker locker(&releaseMutex);
259 for (auto command : commandList) {
260 execCommand(command);
261 }
262 });
263 }
264 }
265
266 return true;
267}
268
269bool BuildManager::execCommand(const BuildCommandInfo &info)
270{
271 outBuildState(BuildState::kBuilding);
272 bool ret = false;
273 QString executeResult = tr("Execute command failed!\n");
274
275 d->cmdProcess.setWorkingDirectory(info.workingDir);
276
277 QString startMsg = tr("Start execute command: \"%1\" \"%2\" in workspace \"%3\".\n")
278 .arg(info.program, info.arguments.join(" "), info.workingDir);
279 outputLog(startMsg, OutputPane::OutputFormat::NormalMessage);
280
281 connect(&d->cmdProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
282 [&](int exitcode, QProcess::ExitStatus exitStatus) {
283 if (0 == exitcode && exitStatus == QProcess::ExitStatus::NormalExit) {
284 ret = true;
285 executeResult = tr("The process \"%1\" exited normally.\n").arg(d->cmdProcess.program());
286 } else if (exitStatus == QProcess::NormalExit) {
287 ret = false;
288 executeResult = tr("The process \"%1\" exited with code %2.\n")
289 .arg(d->cmdProcess.program(), QString::number(exitcode));
290 } else {
291 ret = false;
292 executeResult = tr("The process \"%1\" crashed.\n")
293 .arg(d->cmdProcess.program());
294 }
295 });
296
297 connect(&d->cmdProcess, &QProcess::readyReadStandardOutput, [&]() {
298 d->cmdProcess.setReadChannel(QProcess::StandardOutput);
299 while (d->cmdProcess.canReadLine()) {
300 QString line = QString::fromUtf8(d->cmdProcess.readLine());
301 outputLog(line, OutputPane::OutputFormat::StdOut);
302 }
303 });
304
305 connect(&d->cmdProcess, &QProcess::readyReadStandardError, [&]() {
306 d->cmdProcess.setReadChannel(QProcess::StandardError);
307 while (d->cmdProcess.canReadLine()) {
308 QString line = QString::fromUtf8(d->cmdProcess.readLine());
309 outputLog(line, OutputPane::OutputFormat::StdErr);
310 outputError(line);
311 }
312 });
313
314 d->cmdProcess.start(info.program, info.arguments);
315 d->cmdProcess.waitForFinished(-1);
316
317 disconnectSignals();
318 outputLog(executeResult, ret ? OutputPane::OutputFormat::NormalMessage : OutputPane::OutputFormat::StdErr);
319
320 QString endMsg = tr("Execute command finished.\n");
321 outputLog(endMsg, OutputPane::OutputFormat::NormalMessage);
322
323 BuildState buildState = ret ? BuildState::kNoBuild : BuildState::kBuildFailed;
324 outBuildState(buildState);
325
326 outputNotify(buildState, info);
327 return ret;
328}
329
330void BuildManager::outputLog(const QString &content, const OutputPane::OutputFormat format)
331{
332 emit sigOutputCompileInfo(content, format);
333}
334
335void BuildManager::outputError(const QString &content)
336{
337 emit sigOutputProblemInfo(content);
338}
339
340void BuildManager::outputNotify(const BuildState &state, const BuildCommandInfo &commandInfo)
341{
342 emit sigOutputNotify(state, commandInfo);
343}
344
345void BuildManager::slotOutputCompileInfo(const QString &content, const OutputPane::OutputFormat format)
346{
347 if (format == OutputPane::OutputFormat::StdOut || OutputPane::OutputFormat::NormalMessage) {
348 std::cout << content.toStdString() << std::endl;
349 } else if (format == OutputPane::OutputFormat::StdErr) {
350 std::cerr << content.toStdString() << std::endl;
351 }
352 d->outputParser->stdOutput(content, format);
353}
354
355void BuildManager::slotOutputProblemInfo(const QString &content)
356{
357 d->outputParser->stdError(content);
358}
359
360void BuildManager::slotOutputNotify(const BuildState &state, const BuildCommandInfo &commandInfo)
361{
362 BuilderSender::notifyBuildState(state, commandInfo);
363}
364
365void BuildManager::addOutput(const QString &content, const OutputPane::OutputFormat format)
366{
367 QString newContent = content;
368 if (OutputPane::OutputFormat::NormalMessage == format
369 || OutputPane::OutputFormat::ErrorMessage == format
370 || OutputPane::OutputFormat::StdOut == format) {
371
372 QDateTime curDatetime = QDateTime::currentDateTime();
373 QString time = curDatetime.toString("hh:mm:ss");
374 newContent = time + ": " + newContent;
375 }
376 d->compileOutputPane->appendText(newContent, format);
377}
378
379void BuildManager::outBuildState(const BuildState &buildState)
380{
381 emit sigBuildState(buildState);
382}
383
384void BuildManager::slotBuildState(const BuildState &buildState)
385{
386 d->currentState = buildState;
387
388 switch (buildState) {
389 case BuildState::kNoBuild:
390 case BuildState::kBuildFailed:
391 d->buildAction->setEnabled(true);
392 d->rebuildAction->setEnabled(true);
393 d->cleanAction->setEnabled(true);
394 d->cancelAction->setEnabled(false);
395 break;
396 case BuildState::kBuilding:
397 d->buildAction->setEnabled(false);
398 d->rebuildAction->setEnabled(false);
399 d->cleanAction->setEnabled(false);
400 d->cancelAction->setEnabled(true);
401 break;
402 }
403}
404
405bool BuildManager::canStartBuild()
406{
407 return BuildState::kBuilding != d->currentState;
408}
409
410void BuildManager::disconnectSignals()
411{
412 disconnect(&d->cmdProcess, static_cast<void (QProcess::*)\
413 (int, QProcess::ExitStatus)>(&QProcess::finished), nullptr, nullptr);
414 disconnect(&d->cmdProcess, &QProcess::readyReadStandardOutput, nullptr, nullptr);
415 disconnect(&d->cmdProcess, &QProcess::readyReadStandardError, nullptr, nullptr);
416}
417