1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Copyright (C) 2019 Jochen Ulrich <jochenulrich@t-online.de>
5 ** Contact: https://www.qt.io/licensing/
6 **
7 ** This file is part of Qbs.
8 **
9 ** $QT_BEGIN_LICENSE:LGPL$
10 ** Commercial License Usage
11 ** Licensees holding valid commercial Qt licenses may use this file in
12 ** accordance with the commercial license agreement provided with the
13 ** Software or, alternatively, in accordance with the terms contained in
14 ** a written agreement between you and The Qt Company. For licensing terms
15 ** and conditions see https://www.qt.io/terms-conditions. For further
16 ** information use the contact form at https://www.qt.io/contact-us.
17 **
18 ** GNU Lesser General Public License Usage
19 ** Alternatively, this file may be used under the terms of the GNU Lesser
20 ** General Public License version 3 as published by the Free Software
21 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
22 ** packaging of this file. Please review the following information to
23 ** ensure the GNU Lesser General Public License version 3 requirements
24 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
25 **
26 ** GNU General Public License Usage
27 ** Alternatively, this file may be used under the terms of the GNU
28 ** General Public License version 2.0 or (at your option) the GNU General
29 ** Public license version 3 or any later version approved by the KDE Free
30 ** Qt Foundation. The licenses are as published by the Free Software
31 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
32 ** included in the packaging of this file. Please review the following
33 ** information to ensure the GNU General Public License requirements will
34 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
35 ** https://www.gnu.org/licenses/gpl-3.0.html.
36 **
37 ** $QT_END_LICENSE$
38 **
39 ****************************************************************************/
40
41 #include "jscommandexecutor.h"
42
43 #include "artifact.h"
44 #include "buildgraph.h"
45 #include "rulecommands.h"
46 #include "transformer.h"
47
48 #include <language/language.h>
49 #include <language/preparescriptobserver.h>
50 #include <language/resolvedfilecontext.h>
51 #include <language/scriptengine.h>
52 #include <logging/logger.h>
53 #include <tools/codelocation.h>
54 #include <tools/error.h>
55 #include <tools/qbsassert.h>
56 #include <tools/qttools.h>
57
58 #include <QtCore/qeventloop.h>
59 #include <QtCore/qpointer.h>
60 #include <QtCore/qthread.h>
61 #include <QtCore/qtimer.h>
62
63 namespace qbs {
64 namespace Internal {
65
66 struct JavaScriptCommandResult
67 {
68 bool success = false;
69 QString errorMessage;
70 CodeLocation errorLocation;
71 };
72
73 class JsCommandExecutorThreadObject : public QObject
74 {
75 Q_OBJECT
76 public:
JsCommandExecutorThreadObject(Logger logger)77 JsCommandExecutorThreadObject(Logger logger)
78 : m_logger(std::move(logger))
79 , m_scriptEngine(nullptr)
80 {
81 }
82
result() const83 const JavaScriptCommandResult &result() const
84 {
85 return m_result;
86 }
87
cancel(const qbs::ErrorInfo & reason)88 void cancel(const qbs::ErrorInfo &reason)
89 {
90 m_result.success = !reason.hasError();
91 m_result.errorMessage = reason.toString();
92 if (m_scriptEngine)
93 m_scriptEngine->abortEvaluation();
94 m_cancelled = true;
95 }
96
97 signals:
98 void finished();
99
100 public:
start(const JavaScriptCommand * cmd,Transformer * transformer)101 void start(const JavaScriptCommand *cmd, Transformer *transformer)
102 {
103 if (m_cancelled) {
104 emit finished();
105 return;
106 }
107
108 m_running = true;
109 try {
110 doStart(cmd, transformer);
111 } catch (const qbs::ErrorInfo &error) {
112 setError(error.toString(), cmd->codeLocation());
113 }
114
115 m_running = false;
116 emit finished();
117 }
118
119 private:
doStart(const JavaScriptCommand * cmd,Transformer * transformer)120 void doStart(const JavaScriptCommand *cmd, Transformer *transformer)
121 {
122 m_result.success = true;
123 m_result.errorMessage.clear();
124 ScriptEngine * const scriptEngine = provideScriptEngine();
125 QScriptValue scope = scriptEngine->newObject();
126 scope.setPrototype(scriptEngine->globalObject());
127 m_scriptEngine->clearRequestedProperties();
128 setupScriptEngineForFile(scriptEngine,
129 transformer->rule->prepareScript.fileContext(), scope,
130 ObserveMode::Enabled);
131
132 QScriptValue importScopeForSourceCode;
133 if (!cmd->scopeName().isEmpty())
134 importScopeForSourceCode = scope.property(cmd->scopeName());
135
136 setupScriptEngineForProduct(scriptEngine, transformer->product().get(),
137 transformer->rule->module.get(), scope, true);
138 transformer->setupInputs(scope);
139 transformer->setupOutputs(scope);
140 transformer->setupExplicitlyDependsOn(scope);
141
142 for (QVariantMap::const_iterator it = cmd->properties().constBegin();
143 it != cmd->properties().constEnd(); ++it) {
144 scope.setProperty(it.key(), scriptEngine->toScriptValue(it.value()));
145 }
146
147 scriptEngine->setGlobalObject(scope);
148 if (importScopeForSourceCode.isObject())
149 scriptEngine->currentContext()->pushScope(importScopeForSourceCode);
150 scriptEngine->evaluate(cmd->sourceCode());
151 scriptEngine->releaseResourcesOfScriptObjects();
152 if (importScopeForSourceCode.isObject())
153 scriptEngine->currentContext()->popScope();
154 scriptEngine->setGlobalObject(scope.prototype());
155 transformer->propertiesRequestedInCommands
156 += scriptEngine->propertiesRequestedInScript();
157 unite(transformer->propertiesRequestedFromArtifactInCommands,
158 scriptEngine->propertiesRequestedFromArtifact());
159 const std::vector<QString> &importFilesUsedInCommand
160 = scriptEngine->importedFilesUsedInScript();
161 transformer->importedFilesUsedInCommands.insert(
162 transformer->importedFilesUsedInCommands.cend(),
163 importFilesUsedInCommand.cbegin(), importFilesUsedInCommand.cend());
164 transformer->depsRequestedInCommands.add(scriptEngine->productsWithRequestedDependencies());
165 transformer->artifactsMapRequestedInCommands.unite(scriptEngine->requestedArtifacts());
166 for (const ResolvedProduct * const p : scriptEngine->requestedExports()) {
167 transformer->exportedModulesAccessedInCommands.insert(
168 std::make_pair(p->uniqueName(), p->exportedModule));
169 }
170 scriptEngine->clearRequestedProperties();
171 if (scriptEngine->hasUncaughtException()) {
172 // ### We don't know the line number of the command's sourceCode property assignment.
173 setError(scriptEngine->uncaughtException().toString(), cmd->codeLocation());
174 }
175 }
176
setError(const QString & errorMessage,const CodeLocation & codeLocation)177 void setError(const QString &errorMessage, const CodeLocation &codeLocation)
178 {
179 m_result.success = false;
180 m_result.errorMessage = errorMessage;
181 m_result.errorLocation = codeLocation;
182 }
183
provideScriptEngine()184 ScriptEngine *provideScriptEngine()
185 {
186 if (!m_scriptEngine)
187 m_scriptEngine = ScriptEngine::create(m_logger, EvalContext::JsCommand, this);
188 return m_scriptEngine;
189 }
190
191 Logger m_logger;
192 ScriptEngine *m_scriptEngine;
193 JavaScriptCommandResult m_result;
194 bool m_running = false;
195 bool m_cancelled = false;
196 };
197
198
JsCommandExecutor(const Logger & logger,QObject * parent)199 JsCommandExecutor::JsCommandExecutor(const Logger &logger, QObject *parent)
200 : AbstractCommandExecutor(logger, parent)
201 , m_thread(new QThread(this))
202 , m_objectInThread(new JsCommandExecutorThreadObject(logger))
203 , m_running(false)
204 {
205 qRegisterMetaType<Transformer *>("Transformer *");
206 qRegisterMetaType<const JavaScriptCommand *>("const JavaScriptCommand *");
207
208 m_objectInThread->moveToThread(m_thread);
209 connect(m_objectInThread, &JsCommandExecutorThreadObject::finished,
210 this, &JsCommandExecutor::onJavaScriptCommandFinished);
211 connect(this, &JsCommandExecutor::startRequested,
212 m_objectInThread, &JsCommandExecutorThreadObject::start);
213 }
214
~JsCommandExecutor()215 JsCommandExecutor::~JsCommandExecutor()
216 {
217 waitForFinished();
218 m_thread->quit();
219 m_thread->wait();
220 delete m_objectInThread;
221 }
222
doReportCommandDescription(const QString & productName)223 void JsCommandExecutor::doReportCommandDescription(const QString &productName)
224 {
225 if ((m_echoMode == CommandEchoModeCommandLine
226 || m_echoMode == CommandEchoModeCommandLineWithEnvironment)
227 && !command()->extendedDescription().isEmpty()) {
228 emit reportCommandDescription(command()->highlight(), command()->extendedDescription());
229 return;
230 }
231
232 AbstractCommandExecutor::doReportCommandDescription(productName);
233 }
234
waitForFinished()235 void JsCommandExecutor::waitForFinished()
236 {
237 if (!m_running)
238 return;
239 QEventLoop loop;
240 connect(this, &AbstractCommandExecutor::finished, &loop, &QEventLoop::quit);
241 loop.exec();
242 }
243
doStart()244 bool JsCommandExecutor::doStart()
245 {
246 QBS_ASSERT(!m_running, return false);
247
248 if (dryRun() && !command()->ignoreDryRun()) {
249 QTimer::singleShot(0, this, [this] { emit finished(); }); // Don't call back on the caller.
250 return false;
251 }
252
253 m_thread->start();
254 m_running = true;
255 emit startRequested(jsCommand(), transformer());
256 return true;
257 }
258
cancel(const qbs::ErrorInfo & reason)259 void JsCommandExecutor::cancel(const qbs::ErrorInfo &reason)
260 {
261 if (m_running && (!dryRun() || command()->ignoreDryRun()))
262 QTimer::singleShot(0, m_objectInThread, [objectInThread = QPointer<JsCommandExecutorThreadObject>{m_objectInThread}, reason] {
263 if (objectInThread)
264 objectInThread->cancel(reason);
265 });
266 }
267
onJavaScriptCommandFinished()268 void JsCommandExecutor::onJavaScriptCommandFinished()
269 {
270 m_running = false;
271 const JavaScriptCommandResult &result = m_objectInThread->result();
272 ErrorInfo err;
273 if (!result.success) {
274 logger().qbsDebug() << "JS context:\n" << jsCommand()->properties();
275 logger().qbsDebug() << "JS code:\n" << jsCommand()->sourceCode();
276 err.append(result.errorMessage);
277 // ### We don't know the line number of the command's sourceCode property assignment.
278 err.appendBacktrace(QStringLiteral("JavaScriptCommand.sourceCode"));
279 err.appendBacktrace(QStringLiteral("Rule.prepare"), result.errorLocation);
280 }
281 emit finished(err);
282 }
283
jsCommand() const284 const JavaScriptCommand *JsCommandExecutor::jsCommand() const
285 {
286 return static_cast<const JavaScriptCommand *>(command());
287 }
288
289 } // namespace Internal
290 } // namespace qbs
291
292 #include "jscommandexecutor.moc"
293