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