1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "extracompiler.h"
27
28 #include "buildconfiguration.h"
29 #include "buildmanager.h"
30 #include "kitinformation.h"
31 #include "session.h"
32 #include "target.h"
33
34 #include <coreplugin/editormanager/editormanager.h>
35 #include <coreplugin/idocument.h>
36 #include <texteditor/texteditor.h>
37 #include <texteditor/texteditorsettings.h>
38 #include <texteditor/texteditorconstants.h>
39 #include <texteditor/fontsettings.h>
40
41 #include <utils/qtcassert.h>
42 #include <utils/runextensions.h>
43
44 #include <QDateTime>
45 #include <QFutureInterface>
46 #include <QFutureWatcher>
47 #include <QProcess>
48 #include <QThreadPool>
49 #include <QTimer>
50 #include <QTextBlock>
51
52 namespace ProjectExplorer {
53
54 Q_GLOBAL_STATIC(QThreadPool, s_extraCompilerThreadPool);
55 Q_GLOBAL_STATIC(QList<ExtraCompilerFactory *>, factories);
56
57 class ExtraCompilerPrivate
58 {
59 public:
60 const Project *project;
61 Utils::FilePath source;
62 FileNameToContentsHash contents;
63 Tasks issues;
64 QDateTime compileTime;
65 Core::IEditor *lastEditor = nullptr;
66 QMetaObject::Connection activeBuildConfigConnection;
67 QMetaObject::Connection activeEnvironmentConnection;
68 bool dirty = false;
69
70 QTimer timer;
71 void updateIssues();
72 };
73
ExtraCompiler(const Project * project,const Utils::FilePath & source,const Utils::FilePaths & targets,QObject * parent)74 ExtraCompiler::ExtraCompiler(const Project *project, const Utils::FilePath &source,
75 const Utils::FilePaths &targets, QObject *parent) :
76 QObject(parent), d(std::make_unique<ExtraCompilerPrivate>())
77 {
78 d->project = project;
79 d->source = source;
80 foreach (const Utils::FilePath &target, targets)
81 d->contents.insert(target, QByteArray());
82 d->timer.setSingleShot(true);
83
84 connect(&d->timer, &QTimer::timeout, this, [this](){
85 if (d->dirty && d->lastEditor) {
86 d->dirty = false;
87 run(d->lastEditor->document()->contents());
88 }
89 });
90
91 connect(BuildManager::instance(), &BuildManager::buildStateChanged,
92 this, &ExtraCompiler::onTargetsBuilt);
93
94 connect(SessionManager::instance(), &SessionManager::projectRemoved,
95 this, [this](Project *project) {
96 if (project == d->project)
97 deleteLater();
98 });
99
100 Core::EditorManager *editorManager = Core::EditorManager::instance();
101 connect(editorManager, &Core::EditorManager::currentEditorChanged,
102 this, &ExtraCompiler::onEditorChanged);
103 connect(editorManager, &Core::EditorManager::editorAboutToClose,
104 this, &ExtraCompiler::onEditorAboutToClose);
105
106 // Use existing target files, where possible. Otherwise run the compiler.
107 QDateTime sourceTime = d->source.lastModified();
108 foreach (const Utils::FilePath &target, targets) {
109 QFileInfo targetFileInfo(target.toFileInfo());
110 if (!targetFileInfo.exists()) {
111 d->dirty = true;
112 continue;
113 }
114
115 QDateTime lastModified = targetFileInfo.lastModified();
116 if (lastModified < sourceTime)
117 d->dirty = true;
118
119 if (!d->compileTime.isValid() || d->compileTime > lastModified)
120 d->compileTime = lastModified;
121
122 QFile file(target.toString());
123 if (file.open(QFile::ReadOnly | QFile::Text))
124 setContent(target, file.readAll());
125 }
126 }
127
128 ExtraCompiler::~ExtraCompiler() = default;
129
project() const130 const Project *ExtraCompiler::project() const
131 {
132 return d->project;
133 }
134
source() const135 Utils::FilePath ExtraCompiler::source() const
136 {
137 return d->source;
138 }
139
content(const Utils::FilePath & file) const140 QByteArray ExtraCompiler::content(const Utils::FilePath &file) const
141 {
142 return d->contents.value(file);
143 }
144
targets() const145 Utils::FilePaths ExtraCompiler::targets() const
146 {
147 return d->contents.keys();
148 }
149
forEachTarget(std::function<void (const Utils::FilePath &)> func)150 void ExtraCompiler::forEachTarget(std::function<void (const Utils::FilePath &)> func)
151 {
152 for (auto it = d->contents.constBegin(), end = d->contents.constEnd(); it != end; ++it)
153 func(it.key());
154 }
155
setCompileTime(const QDateTime & time)156 void ExtraCompiler::setCompileTime(const QDateTime &time)
157 {
158 d->compileTime = time;
159 }
160
compileTime() const161 QDateTime ExtraCompiler::compileTime() const
162 {
163 return d->compileTime;
164 }
165
extraCompilerThreadPool()166 QThreadPool *ExtraCompiler::extraCompilerThreadPool()
167 {
168 return s_extraCompilerThreadPool();
169 }
170
isDirty() const171 bool ExtraCompiler::isDirty() const
172 {
173 return d->dirty;
174 }
175
onTargetsBuilt(Project * project)176 void ExtraCompiler::onTargetsBuilt(Project *project)
177 {
178 if (project != d->project || BuildManager::isBuilding(project))
179 return;
180
181 // This is mostly a fall back for the cases when the generator couldn't be run.
182 // It pays special attention to the case where a source file was newly created
183 const QDateTime sourceTime = d->source.lastModified();
184 if (d->compileTime.isValid() && d->compileTime >= sourceTime)
185 return;
186
187 forEachTarget([&](const Utils::FilePath &target) {
188 QFileInfo fi(target.toFileInfo());
189 QDateTime generateTime = fi.exists() ? fi.lastModified() : QDateTime();
190 if (generateTime.isValid() && (generateTime > sourceTime)) {
191 if (d->compileTime >= generateTime)
192 return;
193
194 QFile file(target.toString());
195 if (file.open(QFile::ReadOnly | QFile::Text)) {
196 d->compileTime = generateTime;
197 setContent(target, file.readAll());
198 }
199 }
200 });
201 }
202
onEditorChanged(Core::IEditor * editor)203 void ExtraCompiler::onEditorChanged(Core::IEditor *editor)
204 {
205 // Handle old editor
206 if (d->lastEditor) {
207 Core::IDocument *doc = d->lastEditor->document();
208 disconnect(doc, &Core::IDocument::contentsChanged,
209 this, &ExtraCompiler::setDirty);
210
211 if (d->dirty) {
212 d->dirty = false;
213 run(doc->contents());
214 }
215 }
216
217 if (editor && editor->document()->filePath() == d->source) {
218 d->lastEditor = editor;
219 d->updateIssues();
220
221 // Handle new editor
222 connect(d->lastEditor->document(), &Core::IDocument::contentsChanged,
223 this, &ExtraCompiler::setDirty);
224 } else {
225 d->lastEditor = nullptr;
226 }
227 }
228
setDirty()229 void ExtraCompiler::setDirty()
230 {
231 d->dirty = true;
232 d->timer.start(1000);
233 }
234
onEditorAboutToClose(Core::IEditor * editor)235 void ExtraCompiler::onEditorAboutToClose(Core::IEditor *editor)
236 {
237 if (d->lastEditor != editor)
238 return;
239
240 // Oh no our editor is going to be closed
241 // get the content first
242 Core::IDocument *doc = d->lastEditor->document();
243 disconnect(doc, &Core::IDocument::contentsChanged,
244 this, &ExtraCompiler::setDirty);
245 if (d->dirty) {
246 d->dirty = false;
247 run(doc->contents());
248 }
249 d->lastEditor = nullptr;
250 }
251
buildEnvironment() const252 Utils::Environment ExtraCompiler::buildEnvironment() const
253 {
254 if (Target *target = project()->activeTarget()) {
255 if (BuildConfiguration *bc = target->activeBuildConfiguration()) {
256 return bc->environment();
257 } else {
258 Utils::EnvironmentItems changes =
259 EnvironmentKitAspect::environmentChanges(target->kit());
260 Utils::Environment env = Utils::Environment::systemEnvironment();
261 env.modify(changes);
262 return env;
263 }
264 }
265
266 return Utils::Environment::systemEnvironment();
267 }
268
setCompileIssues(const Tasks & issues)269 void ExtraCompiler::setCompileIssues(const Tasks &issues)
270 {
271 d->issues = issues;
272 d->updateIssues();
273 }
274
updateIssues()275 void ExtraCompilerPrivate::updateIssues()
276 {
277 if (!lastEditor)
278 return;
279
280 auto widget = qobject_cast<TextEditor::TextEditorWidget *>(lastEditor->widget());
281 if (!widget)
282 return;
283
284 QList<QTextEdit::ExtraSelection> selections;
285 const QTextDocument *document = widget->document();
286 foreach (const Task &issue, issues) {
287 QTextEdit::ExtraSelection selection;
288 QTextCursor cursor(document->findBlockByNumber(issue.line - 1));
289 cursor.movePosition(QTextCursor::StartOfLine);
290 cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
291 selection.cursor = cursor;
292
293 const auto fontSettings = TextEditor::TextEditorSettings::fontSettings();
294 selection.format = fontSettings.toTextCharFormat(issue.type == Task::Warning ?
295 TextEditor::C_WARNING : TextEditor::C_ERROR);
296 selection.format.setToolTip(issue.description());
297 selections.append(selection);
298 }
299
300 widget->setExtraSelections(TextEditor::TextEditorWidget::CodeWarningsSelection, selections);
301 }
302
setContent(const Utils::FilePath & file,const QByteArray & contents)303 void ExtraCompiler::setContent(const Utils::FilePath &file, const QByteArray &contents)
304 {
305 auto it = d->contents.find(file);
306 if (it != d->contents.end()) {
307 if (it.value() != contents) {
308 it.value() = contents;
309 emit contentsChanged(file);
310 }
311 }
312 }
313
ExtraCompilerFactory(QObject * parent)314 ExtraCompilerFactory::ExtraCompilerFactory(QObject *parent)
315 : QObject(parent)
316 {
317 factories->append(this);
318 }
319
~ExtraCompilerFactory()320 ExtraCompilerFactory::~ExtraCompilerFactory()
321 {
322 factories->removeAll(this);
323 }
324
extraCompilerFactories()325 QList<ExtraCompilerFactory *> ExtraCompilerFactory::extraCompilerFactories()
326 {
327 return *factories();
328 }
329
ProcessExtraCompiler(const Project * project,const Utils::FilePath & source,const Utils::FilePaths & targets,QObject * parent)330 ProcessExtraCompiler::ProcessExtraCompiler(const Project *project, const Utils::FilePath &source,
331 const Utils::FilePaths &targets, QObject *parent) :
332 ExtraCompiler(project, source, targets, parent)
333 { }
334
~ProcessExtraCompiler()335 ProcessExtraCompiler::~ProcessExtraCompiler()
336 {
337 if (!m_watcher)
338 return;
339 m_watcher->cancel();
340 m_watcher->waitForFinished();
341 }
342
run(const QByteArray & sourceContents)343 void ProcessExtraCompiler::run(const QByteArray &sourceContents)
344 {
345 ContentProvider contents = [sourceContents]() { return sourceContents; };
346 runImpl(contents);
347 }
348
run()349 QFuture<FileNameToContentsHash> ProcessExtraCompiler::run()
350 {
351 const Utils::FilePath fileName = source();
352 ContentProvider contents = [fileName]() {
353 QFile file(fileName.toString());
354 if (!file.open(QFile::ReadOnly | QFile::Text))
355 return QByteArray();
356 return file.readAll();
357 };
358 return runImpl(contents);
359 }
360
workingDirectory() const361 Utils::FilePath ProcessExtraCompiler::workingDirectory() const
362 {
363 return Utils::FilePath();
364 }
365
arguments() const366 QStringList ProcessExtraCompiler::arguments() const
367 {
368 return QStringList();
369 }
370
prepareToRun(const QByteArray & sourceContents)371 bool ProcessExtraCompiler::prepareToRun(const QByteArray &sourceContents)
372 {
373 Q_UNUSED(sourceContents)
374 return true;
375 }
376
parseIssues(const QByteArray & stdErr)377 Tasks ProcessExtraCompiler::parseIssues(const QByteArray &stdErr)
378 {
379 Q_UNUSED(stdErr)
380 return {};
381 }
382
runImpl(const ContentProvider & provider)383 QFuture<FileNameToContentsHash> ProcessExtraCompiler::runImpl(const ContentProvider &provider)
384 {
385 if (m_watcher)
386 delete m_watcher;
387
388 m_watcher = new QFutureWatcher<FileNameToContentsHash>();
389 connect(m_watcher, &QFutureWatcher<FileNameToContentsHash>::finished,
390 this, &ProcessExtraCompiler::cleanUp);
391
392 m_watcher->setFuture(Utils::runAsync(extraCompilerThreadPool(),
393 &ProcessExtraCompiler::runInThread, this,
394 command(), workingDirectory(), arguments(), provider,
395 buildEnvironment()));
396 return m_watcher->future();
397 }
398
runInThread(QFutureInterface<FileNameToContentsHash> & futureInterface,const Utils::FilePath & cmd,const Utils::FilePath & workDir,const QStringList & args,const ContentProvider & provider,const Utils::Environment & env)399 void ProcessExtraCompiler::runInThread(
400 QFutureInterface<FileNameToContentsHash> &futureInterface,
401 const Utils::FilePath &cmd, const Utils::FilePath &workDir,
402 const QStringList &args, const ContentProvider &provider,
403 const Utils::Environment &env)
404 {
405 if (cmd.isEmpty() || !cmd.toFileInfo().isExecutable())
406 return;
407
408 const QByteArray sourceContents = provider();
409 if (sourceContents.isNull() || !prepareToRun(sourceContents))
410 return;
411
412 QProcess process;
413
414 process.setProcessEnvironment(env.toProcessEnvironment());
415 if (!workDir.isEmpty())
416 process.setWorkingDirectory(workDir.toString());
417 process.start(cmd.toString(), args, QIODevice::ReadWrite);
418 if (!process.waitForStarted()) {
419 handleProcessError(&process);
420 return;
421 }
422 bool isCanceled = futureInterface.isCanceled();
423 if (!isCanceled) {
424 handleProcessStarted(&process, sourceContents);
425 forever {
426 bool done = process.waitForFinished(200);
427 isCanceled = futureInterface.isCanceled();
428 if (done || isCanceled)
429 break;
430 }
431 }
432
433 isCanceled |= process.state() == QProcess::Running;
434 if (isCanceled) {
435 process.kill();
436 process.waitForFinished();
437 return;
438 }
439
440 futureInterface.reportResult(handleProcessFinished(&process));
441 }
442
cleanUp()443 void ProcessExtraCompiler::cleanUp()
444 {
445 QTC_ASSERT(m_watcher, return);
446 auto future = m_watcher->future();
447 delete m_watcher;
448 m_watcher = nullptr;
449 if (!future.resultCount())
450 return;
451 const FileNameToContentsHash data = future.result();
452
453 if (data.isEmpty())
454 return; // There was some kind of error...
455
456 for (auto it = data.constBegin(), end = data.constEnd(); it != end; ++it)
457 setContent(it.key(), it.value());
458
459 setCompileTime(QDateTime::currentDateTime());
460 }
461
462 } // namespace ProjectExplorer
463