Chris@66
|
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
|
Chris@66
|
2 /*
|
Chris@66
|
3 Copyright (c) 2020 Queen Mary, University of London
|
Chris@66
|
4
|
Chris@66
|
5 Permission is hereby granted, free of charge, to any person
|
Chris@66
|
6 obtaining a copy of this software and associated documentation
|
Chris@66
|
7 files (the "Software"), to deal in the Software without
|
Chris@66
|
8 restriction, including without limitation the rights to use, copy,
|
Chris@66
|
9 modify, merge, publish, distribute, sublicense, and/or sell copies
|
Chris@66
|
10 of the Software, and to permit persons to whom the Software is
|
Chris@66
|
11 furnished to do so, subject to the following conditions:
|
Chris@66
|
12
|
Chris@66
|
13 The above copyright notice and this permission notice shall be
|
Chris@66
|
14 included in all copies or substantial portions of the Software.
|
Chris@66
|
15
|
Chris@66
|
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
Chris@66
|
17 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
Chris@66
|
18 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
Chris@66
|
19 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
Chris@66
|
20 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
Chris@66
|
21 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
Chris@66
|
22 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
Chris@66
|
23
|
Chris@66
|
24 Except as contained in this notice, the names of the Centre for
|
Chris@66
|
25 Digital Music and Queen Mary, University of London shall not be
|
Chris@66
|
26 used in advertising or otherwise to promote the sale, use or other
|
Chris@66
|
27 dealings in this Software without prior written authorization.
|
Chris@66
|
28 */
|
Chris@33
|
29
|
Chris@33
|
30 #include <QApplication>
|
Chris@33
|
31 #include <QString>
|
Chris@33
|
32 #include <QFile>
|
Chris@33
|
33 #include <QDir>
|
Chris@33
|
34
|
Chris@42
|
35 #include <QDialog>
|
Chris@42
|
36 #include <QFrame>
|
Chris@42
|
37 #include <QVBoxLayout>
|
Chris@42
|
38 #include <QCheckBox>
|
Chris@42
|
39 #include <QDialogButtonBox>
|
Chris@46
|
40 #include <QLabel>
|
Chris@51
|
41 #include <QFont>
|
Chris@51
|
42 #include <QFontInfo>
|
Chris@67
|
43 #include <QTemporaryFile>
|
Chris@67
|
44 #include <QMutex>
|
Chris@67
|
45 #include <QMutexLocker>
|
Chris@67
|
46 #include <QProcess>
|
Chris@72
|
47 #include <QToolButton>
|
Chris@79
|
48 #include <QPushButton>
|
Chris@72
|
49 #include <QMessageBox>
|
Chris@74
|
50 #include <QSvgRenderer>
|
Chris@74
|
51 #include <QPainter>
|
Chris@74
|
52 #include <QFontMetrics>
|
Chris@79
|
53 #include <QSpacerItem>
|
Chris@81
|
54 #include <QProgressDialog>
|
Chris@81
|
55 #include <QThread>
|
Chris@86
|
56 #include <QDateTime>
|
Chris@98
|
57 #include <QTimer>
|
Chris@42
|
58
|
Chris@41
|
59 #include <vamp-hostsdk/PluginHostAdapter.h>
|
Chris@41
|
60
|
Chris@43
|
61 #include <dataquay/BasicStore.h>
|
Chris@43
|
62 #include <dataquay/RDFException.h>
|
Chris@43
|
63
|
Chris@33
|
64 #include <iostream>
|
Chris@58
|
65 #include <memory>
|
Chris@43
|
66 #include <set>
|
Chris@43
|
67
|
Chris@51
|
68 #include "base/Debug.h"
|
Chris@51
|
69
|
Chris@79
|
70 #include "version.h"
|
Chris@79
|
71
|
Chris@33
|
72 using namespace std;
|
Chris@43
|
73 using namespace Dataquay;
|
Chris@32
|
74
|
Chris@42
|
75 QString
|
Chris@42
|
76 getDefaultInstallDirectory()
|
Chris@32
|
77 {
|
Chris@41
|
78 auto pathList = Vamp::PluginHostAdapter::getPluginPath();
|
Chris@41
|
79 if (pathList.empty()) {
|
Chris@51
|
80 SVCERR << "Failed to look up Vamp plugin path" << endl;
|
Chris@42
|
81 return QString();
|
Chris@41
|
82 }
|
Chris@41
|
83
|
Chris@42
|
84 auto firstPath = *pathList.begin();
|
Chris@42
|
85 QString target = QString::fromUtf8(firstPath.c_str(), firstPath.size());
|
Chris@42
|
86 return target;
|
Chris@42
|
87 }
|
Chris@42
|
88
|
Chris@42
|
89 QStringList
|
Chris@42
|
90 getPluginLibraryList()
|
Chris@42
|
91 {
|
Chris@33
|
92 QDir dir(":out/");
|
Chris@33
|
93 auto entries = dir.entryList({ "*.so", "*.dll", "*.dylib" });
|
Chris@33
|
94
|
Chris@33
|
95 for (auto e: entries) {
|
Chris@51
|
96 SVCERR << e.toStdString() << endl;
|
Chris@33
|
97 }
|
Chris@33
|
98
|
Chris@42
|
99 return entries;
|
Chris@42
|
100 }
|
Chris@33
|
101
|
Chris@50
|
102 void
|
Chris@50
|
103 loadLibraryRdf(BasicStore &store, QString filename)
|
Chris@50
|
104 {
|
Chris@50
|
105 QFile f(filename);
|
Chris@50
|
106 if (!f.open(QFile::ReadOnly | QFile::Text)) {
|
Chris@51
|
107 SVCERR << "Failed to open RDF resource file "
|
Chris@51
|
108 << filename.toStdString() << endl;
|
Chris@50
|
109 return;
|
Chris@50
|
110 }
|
Chris@50
|
111
|
Chris@50
|
112 QByteArray content = f.readAll();
|
Chris@50
|
113 f.close();
|
Chris@50
|
114
|
Chris@50
|
115 try {
|
Chris@50
|
116 store.importString(QString::fromUtf8(content),
|
Chris@50
|
117 Uri("file:" + filename),
|
Chris@50
|
118 BasicStore::ImportIgnoreDuplicates);
|
Chris@50
|
119 } catch (const RDFException &ex) {
|
Chris@51
|
120 SVCERR << "Failed to import RDF resource file "
|
Chris@51
|
121 << filename.toStdString() << ": " << ex.what() << endl;
|
Chris@50
|
122 }
|
Chris@50
|
123 }
|
Chris@50
|
124
|
Chris@43
|
125 unique_ptr<BasicStore>
|
Chris@43
|
126 loadLibrariesRdf()
|
Chris@43
|
127 {
|
Chris@43
|
128 unique_ptr<BasicStore> store(new BasicStore);
|
Chris@43
|
129
|
Chris@50
|
130 vector<QString> dirs { ":rdf/plugins", ":out" };
|
Chris@43
|
131
|
Chris@50
|
132 for (auto d: dirs) {
|
Chris@50
|
133 for (auto e: QDir(d).entryList({ "*.ttl", "*.n3" })) {
|
Chris@50
|
134 loadLibraryRdf(*store, d + "/" + e);
|
Chris@43
|
135 }
|
Chris@43
|
136 }
|
Chris@43
|
137
|
Chris@43
|
138 return store;
|
Chris@43
|
139 }
|
Chris@43
|
140
|
Chris@43
|
141 struct LibraryInfo {
|
Chris@43
|
142 QString id;
|
Chris@43
|
143 QString fileName;
|
Chris@43
|
144 QString title;
|
Chris@43
|
145 QString maker;
|
Chris@43
|
146 QString description;
|
Chris@78
|
147 QString page;
|
Chris@47
|
148 QStringList pluginTitles;
|
Chris@74
|
149 QString licence;
|
Chris@43
|
150 };
|
Chris@43
|
151
|
Chris@99
|
152 struct Licence
|
Chris@99
|
153 {
|
Chris@99
|
154 static QString gpl;
|
Chris@99
|
155 static QString gpl2;
|
Chris@99
|
156 static QString gpl3;
|
Chris@99
|
157 static QString agpl;
|
Chris@99
|
158 static QString apache;
|
Chris@99
|
159 static QString mit;
|
Chris@99
|
160 };
|
Chris@99
|
161
|
Chris@99
|
162 QString Licence::gpl = "GNU General Public License";
|
Chris@99
|
163 QString Licence::gpl2 = "GNU General Public License, version 2";
|
Chris@99
|
164 QString Licence::gpl3 = "GNU General Public License, version 3";
|
Chris@99
|
165 QString Licence::agpl = "GNU Affero General Public License";
|
Chris@99
|
166 QString Licence::apache = "Apache License";
|
Chris@99
|
167 QString Licence::mit = "MIT License";
|
Chris@99
|
168
|
Chris@74
|
169 QString
|
Chris@74
|
170 identifyLicence(QString libraryBasename)
|
Chris@74
|
171 {
|
Chris@74
|
172 QString licenceFile = QString(":out/%1_COPYING.txt").arg(libraryBasename);
|
Chris@74
|
173
|
Chris@74
|
174 QFile f(licenceFile);
|
Chris@74
|
175 if (!f.open(QFile::ReadOnly | QFile::Text)) {
|
Chris@74
|
176 SVCERR << "Failed to open licence file "
|
Chris@74
|
177 << licenceFile.toStdString() << endl;
|
Chris@74
|
178 return {};
|
Chris@74
|
179 }
|
Chris@74
|
180
|
Chris@74
|
181 QByteArray content = f.readAll();
|
Chris@74
|
182 f.close();
|
Chris@74
|
183
|
Chris@74
|
184 QString licenceText = QString::fromUtf8(content);
|
Chris@74
|
185
|
Chris@82
|
186 // NB these are not expected to identify an arbitrary licence! We
|
Chris@74
|
187 // know we have only a limited set here. But we do want to
|
Chris@74
|
188 // determine this from the actual licence text included with the
|
Chris@74
|
189 // plugin distribution, not just from e.g. RDF metadata
|
Chris@74
|
190
|
Chris@99
|
191 if (licenceText.contains(Licence::gpl.toUpper(), Qt::CaseSensitive)) {
|
Chris@74
|
192 if (licenceText.contains("Version 3, 29 June 2007")) {
|
Chris@99
|
193 return Licence::gpl3;
|
Chris@74
|
194 } else if (licenceText.contains("Version 2, June 1991")) {
|
Chris@99
|
195 return Licence::gpl2;
|
Chris@74
|
196 } else {
|
Chris@99
|
197 return Licence::gpl;
|
Chris@74
|
198 }
|
Chris@74
|
199 }
|
Chris@99
|
200 if (licenceText.contains(Licence::agpl.toUpper(), Qt::CaseSensitive)) {
|
Chris@99
|
201 return Licence::agpl;
|
Chris@74
|
202 }
|
Chris@99
|
203 if (licenceText.contains(Licence::apache)) {
|
Chris@99
|
204 return Licence::apache;
|
Chris@74
|
205 }
|
Chris@74
|
206 if (licenceText.contains("Permission is hereby granted, free of charge, to any person")) {
|
Chris@99
|
207 return Licence::mit;
|
Chris@74
|
208 }
|
Chris@74
|
209
|
Chris@74
|
210 SVCERR << "Didn't recognise licence for " << libraryBasename << endl;
|
Chris@74
|
211
|
Chris@74
|
212 return {};
|
Chris@74
|
213 }
|
Chris@74
|
214
|
Chris@99
|
215 QString
|
Chris@99
|
216 getLicenceURL(QString licence)
|
Chris@99
|
217 {
|
Chris@99
|
218 if (licence == Licence::gpl ||
|
Chris@99
|
219 licence == Licence::gpl3) {
|
Chris@99
|
220 return "https://www.gnu.org/licenses/gpl-3.0.en.html";
|
Chris@99
|
221 } else if (licence == Licence::gpl2) {
|
Chris@99
|
222 return "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html";
|
Chris@99
|
223 } else if (licence == Licence::agpl) {
|
Chris@99
|
224 return "https://www.gnu.org/licenses/agpl-3.0.html";
|
Chris@99
|
225 } else if (licence == Licence::apache) {
|
Chris@99
|
226 return "https://www.apache.org/licenses/LICENSE-2.0";
|
Chris@99
|
227 } else if (licence == Licence::mit) {
|
Chris@99
|
228 return "https://opensource.org/licenses/MIT";
|
Chris@99
|
229 }
|
Chris@99
|
230
|
Chris@99
|
231 return {};
|
Chris@99
|
232 }
|
Chris@99
|
233
|
Chris@43
|
234 vector<LibraryInfo>
|
Chris@43
|
235 getLibraryInfo(const Store &store, QStringList libraries)
|
Chris@43
|
236 {
|
Chris@43
|
237 /* e.g.
|
Chris@43
|
238
|
Chris@43
|
239 plugbase:library a vamp:PluginLibrary ;
|
Chris@43
|
240 vamp:identifier "qm-vamp-plugins" ;
|
Chris@43
|
241 dc:title "Queen Mary plugin set"
|
Chris@43
|
242 */
|
Chris@43
|
243
|
Chris@43
|
244 Triples tt = store.match(Triple(Node(),
|
Chris@43
|
245 Uri("a"),
|
Chris@43
|
246 store.expand("vamp:PluginLibrary")));
|
Chris@43
|
247
|
Chris@67
|
248 map<QString, QString> wanted; // basename -> full lib name
|
Chris@43
|
249 for (auto lib: libraries) {
|
Chris@43
|
250 wanted[QFileInfo(lib).baseName()] = lib;
|
Chris@43
|
251 }
|
Chris@43
|
252
|
Chris@43
|
253 vector<LibraryInfo> results;
|
Chris@43
|
254
|
Chris@43
|
255 for (auto t: tt) {
|
Chris@43
|
256
|
Chris@43
|
257 Node libId = store.complete(Triple(t.subject(),
|
Chris@43
|
258 store.expand("vamp:identifier"),
|
Chris@43
|
259 Node()));
|
Chris@43
|
260 if (libId.type != Node::Literal) {
|
Chris@43
|
261 continue;
|
Chris@43
|
262 }
|
Chris@43
|
263 auto wi = wanted.find(libId.value);
|
Chris@43
|
264 if (wi == wanted.end()) {
|
Chris@43
|
265 continue;
|
Chris@43
|
266 }
|
Chris@74
|
267
|
Chris@50
|
268 Node title = store.complete(Triple(t.subject(),
|
Chris@50
|
269 store.expand("dc:title"),
|
Chris@50
|
270 Node()));
|
Chris@50
|
271 if (title.type != Node::Literal) {
|
Chris@50
|
272 continue;
|
Chris@50
|
273 }
|
Chris@50
|
274
|
Chris@43
|
275 LibraryInfo info;
|
Chris@43
|
276 info.id = wi->first;
|
Chris@43
|
277 info.fileName = wi->second;
|
Chris@50
|
278 info.title = title.value;
|
Chris@43
|
279
|
Chris@43
|
280 Node maker = store.complete(Triple(t.subject(),
|
Chris@43
|
281 store.expand("foaf:maker"),
|
Chris@43
|
282 Node()));
|
Chris@43
|
283 if (maker.type == Node::Literal) {
|
Chris@43
|
284 info.maker = maker.value;
|
Chris@46
|
285 } else if (maker != Node()) {
|
Chris@46
|
286 maker = store.complete(Triple(maker,
|
Chris@46
|
287 store.expand("foaf:name"),
|
Chris@46
|
288 Node()));
|
Chris@46
|
289 if (maker.type == Node::Literal) {
|
Chris@46
|
290 info.maker = maker.value;
|
Chris@46
|
291 }
|
Chris@43
|
292 }
|
Chris@46
|
293
|
Chris@43
|
294 Node desc = store.complete(Triple(t.subject(),
|
Chris@43
|
295 store.expand("dc:description"),
|
Chris@43
|
296 Node()));
|
Chris@43
|
297 if (desc.type == Node::Literal) {
|
Chris@43
|
298 info.description = desc.value;
|
Chris@43
|
299 }
|
Chris@78
|
300
|
Chris@78
|
301 Node page = store.complete(Triple(t.subject(),
|
Chris@78
|
302 store.expand("foaf:page"),
|
Chris@78
|
303 Node()));
|
Chris@79
|
304 if (page.type == Node::URI) {
|
Chris@79
|
305 info.page = page.value;
|
Chris@78
|
306 }
|
Chris@43
|
307
|
Chris@47
|
308 Triples pp = store.match(Triple(t.subject(),
|
Chris@47
|
309 store.expand("vamp:available_plugin"),
|
Chris@47
|
310 Node()));
|
Chris@47
|
311 for (auto p: pp) {
|
Chris@47
|
312 Node ptitle = store.complete(Triple(p.object(),
|
Chris@47
|
313 store.expand("dc:title"),
|
Chris@47
|
314 Node()));
|
Chris@47
|
315 if (ptitle.type == Node::Literal) {
|
Chris@47
|
316 info.pluginTitles.push_back(ptitle.value);
|
Chris@47
|
317 }
|
Chris@47
|
318 }
|
Chris@74
|
319
|
Chris@74
|
320 info.licence = identifyLicence(libId.value);
|
Chris@74
|
321 SVCERR << "licence = " << info.licence << endl;
|
Chris@47
|
322
|
Chris@43
|
323 results.push_back(info);
|
Chris@50
|
324 wanted.erase(libId.value);
|
Chris@43
|
325 }
|
Chris@43
|
326
|
Chris@50
|
327 for (auto wp: wanted) {
|
Chris@51
|
328 SVCERR << "Failed to find any RDF information about library "
|
Chris@51
|
329 << wp.second << endl;
|
Chris@50
|
330 }
|
Chris@50
|
331
|
Chris@43
|
332 return results;
|
Chris@43
|
333 }
|
Chris@43
|
334
|
Chris@67
|
335 struct TempFileDeleter {
|
Chris@67
|
336 ~TempFileDeleter() {
|
Chris@67
|
337 if (tempFile != "") {
|
Chris@67
|
338 QFile(tempFile).remove();
|
Chris@67
|
339 }
|
Chris@67
|
340 }
|
Chris@67
|
341 QString tempFile;
|
Chris@67
|
342 };
|
Chris@67
|
343
|
Chris@97
|
344 bool
|
Chris@97
|
345 unbundleFile(QString filePath, QString targetPath, bool isExecutable)
|
Chris@97
|
346 {
|
Chris@97
|
347 SVCERR << "Copying " << filePath.toStdString() << " to "
|
Chris@97
|
348 << targetPath.toStdString() << "..." << endl;
|
Chris@97
|
349
|
Chris@97
|
350 // This has to be able to work even if the destination exists, and
|
Chris@97
|
351 // to do so without deleting it first - e.g. when copying to a
|
Chris@97
|
352 // temporary file. So we open the file and copy to it ourselves
|
Chris@97
|
353 // rather than use QFile::copy
|
Chris@97
|
354
|
Chris@97
|
355 QFile source(filePath);
|
Chris@97
|
356 if (!source.open(QFile::ReadOnly)) {
|
Chris@97
|
357 SVCERR << "ERROR: Failed to read bundled file " << filePath << endl;
|
Chris@97
|
358 return {};
|
Chris@97
|
359 }
|
Chris@97
|
360 QByteArray content = source.readAll();
|
Chris@97
|
361 source.close();
|
Chris@97
|
362
|
Chris@97
|
363 QFile target(targetPath);
|
Chris@97
|
364 if (!target.open(QFile::WriteOnly)) {
|
Chris@97
|
365 SVCERR << "ERROR: Failed to read target file " << targetPath << endl;
|
Chris@97
|
366 return {};
|
Chris@97
|
367 }
|
Chris@97
|
368 if (target.write(content) != content.size()) {
|
Chris@97
|
369 SVCERR << "ERROR: Incomplete write to target file" << endl;
|
Chris@97
|
370 return {};
|
Chris@97
|
371 }
|
Chris@97
|
372 target.close();
|
Chris@97
|
373
|
Chris@97
|
374 auto permissions =
|
Chris@97
|
375 QFile::ReadOwner | QFile::WriteOwner |
|
Chris@97
|
376 QFile::ReadGroup |
|
Chris@97
|
377 QFile::ReadOther;
|
Chris@97
|
378
|
Chris@97
|
379 if (isExecutable) {
|
Chris@97
|
380 permissions |=
|
Chris@97
|
381 QFile::ExeOwner |
|
Chris@97
|
382 QFile::ExeGroup |
|
Chris@97
|
383 QFile::ExeOther;
|
Chris@97
|
384 };
|
Chris@97
|
385
|
Chris@97
|
386 if (!QFile::setPermissions(targetPath, permissions)) {
|
Chris@97
|
387 SVCERR << "Failed to set permissions on "
|
Chris@97
|
388 << targetPath.toStdString() << endl;
|
Chris@97
|
389 return false;
|
Chris@97
|
390 }
|
Chris@97
|
391
|
Chris@97
|
392 return true;
|
Chris@97
|
393 }
|
Chris@97
|
394
|
Chris@67
|
395 map<QString, int>
|
Chris@70
|
396 getLibraryPluginVersions(QString libraryFilePath)
|
Chris@67
|
397 {
|
Chris@67
|
398 static QMutex mutex;
|
Chris@67
|
399 static QString tempFileName;
|
Chris@67
|
400 static TempFileDeleter deleter;
|
Chris@67
|
401 static bool initHappened = false, initSucceeded = false;
|
Chris@67
|
402
|
Chris@67
|
403 QMutexLocker locker (&mutex);
|
Chris@67
|
404
|
Chris@67
|
405 if (!initHappened) {
|
Chris@67
|
406 initHappened = true;
|
Chris@67
|
407
|
Chris@67
|
408 QTemporaryFile tempFile;
|
Chris@67
|
409 tempFile.setAutoRemove(false);
|
Chris@67
|
410 if (!tempFile.open()) {
|
Chris@67
|
411 SVCERR << "ERROR: Failed to open a temporary file" << endl;
|
Chris@67
|
412 return {};
|
Chris@67
|
413 }
|
Chris@67
|
414
|
Chris@67
|
415 // We can't make the QTemporaryFile static, as it will hold
|
Chris@67
|
416 // the file open and that prevents us from executing it. Hence
|
Chris@67
|
417 // the separate deleter.
|
Chris@67
|
418
|
Chris@67
|
419 tempFileName = tempFile.fileName();
|
Chris@67
|
420 deleter.tempFile = tempFileName;
|
Chris@67
|
421
|
Chris@67
|
422 #ifdef Q_OS_WIN32
|
Chris@67
|
423 QString helperPath = ":out/get-version.exe";
|
Chris@67
|
424 #else
|
Chris@67
|
425 QString helperPath = ":out/get-version";
|
Chris@97
|
426 #endif
|
Chris@97
|
427
|
Chris@97
|
428 tempFile.close();
|
Chris@97
|
429 if (!unbundleFile(helperPath, tempFileName, true)) {
|
Chris@97
|
430 SVCERR << "ERROR: Failed to unbundle helper code" << endl;
|
Chris@67
|
431 return {};
|
Chris@67
|
432 }
|
Chris@67
|
433
|
Chris@67
|
434 initSucceeded = true;
|
Chris@67
|
435 }
|
Chris@67
|
436
|
Chris@67
|
437 if (!initSucceeded) {
|
Chris@67
|
438 return {};
|
Chris@67
|
439 }
|
Chris@67
|
440
|
Chris@67
|
441 QProcess process;
|
Chris@67
|
442 process.start(tempFileName, { libraryFilePath });
|
Chris@67
|
443
|
Chris@67
|
444 if (!process.waitForStarted()) {
|
Chris@67
|
445 QProcess::ProcessError err = process.error();
|
Chris@67
|
446 if (err == QProcess::FailedToStart) {
|
Chris@67
|
447 SVCERR << "Unable to start helper process " << tempFileName << endl;
|
Chris@67
|
448 } else if (err == QProcess::Crashed) {
|
Chris@67
|
449 SVCERR << "Helper process " << tempFileName
|
Chris@67
|
450 << " crashed on startup" << endl;
|
Chris@67
|
451 } else {
|
Chris@67
|
452 SVCERR << "Helper process " << tempFileName
|
Chris@67
|
453 << " failed on startup with error code " << err << endl;
|
Chris@67
|
454 }
|
Chris@67
|
455 return {};
|
Chris@67
|
456 }
|
Chris@67
|
457 process.waitForFinished();
|
Chris@67
|
458
|
Chris@67
|
459 QByteArray stdOut = process.readAllStandardOutput();
|
Chris@67
|
460 QByteArray stdErr = process.readAllStandardError();
|
Chris@67
|
461
|
Chris@67
|
462 QString errStr = QString::fromUtf8(stdErr);
|
Chris@67
|
463 if (!errStr.isEmpty()) {
|
Chris@67
|
464 SVCERR << "Note: Helper process stderr follows:" << endl;
|
Chris@67
|
465 SVCERR << errStr << endl;
|
Chris@67
|
466 SVCERR << "Note: Helper process stderr ends" << endl;
|
Chris@67
|
467 }
|
Chris@67
|
468
|
Chris@67
|
469 QStringList lines = QString::fromUtf8(stdOut).split
|
Chris@67
|
470 (QRegExp("[\\r\\n]+"), QString::SkipEmptyParts);
|
Chris@67
|
471 map<QString, int> versions;
|
Chris@67
|
472 for (QString line: lines) {
|
Chris@67
|
473 QStringList parts = line.split(":");
|
Chris@67
|
474 if (parts.size() != 2) {
|
Chris@67
|
475 SVCERR << "Unparseable output line: " << line << endl;
|
Chris@67
|
476 continue;
|
Chris@67
|
477 }
|
Chris@67
|
478 bool ok = false;
|
Chris@67
|
479 int version = parts[1].toInt(&ok);
|
Chris@67
|
480 if (!ok) {
|
Chris@67
|
481 SVCERR << "Unparseable version number in line: " << line << endl;
|
Chris@67
|
482 continue;
|
Chris@67
|
483 }
|
Chris@67
|
484 versions[parts[0]] = version;
|
Chris@67
|
485 }
|
Chris@67
|
486
|
Chris@67
|
487 return versions;
|
Chris@67
|
488 }
|
Chris@67
|
489
|
Chris@97
|
490 map<QString, int>
|
Chris@97
|
491 getBundledLibraryPluginVersions(QString libraryFileName)
|
Chris@97
|
492 {
|
Chris@97
|
493 QString tempFileName;
|
Chris@97
|
494 TempFileDeleter deleter;
|
Chris@97
|
495
|
Chris@97
|
496 {
|
Chris@97
|
497 QTemporaryFile tempFile;
|
Chris@97
|
498 tempFile.setAutoRemove(false);
|
Chris@97
|
499 if (!tempFile.open()) {
|
Chris@97
|
500 SVCERR << "ERROR: Failed to open a temporary file" << endl;
|
Chris@97
|
501 return {};
|
Chris@97
|
502 }
|
Chris@97
|
503
|
Chris@97
|
504 // We can't use QTemporaryFile's auto-remove, as it will hold
|
Chris@97
|
505 // the file open and that prevents us from executing it. Hence
|
Chris@97
|
506 // the separate deleter.
|
Chris@97
|
507
|
Chris@97
|
508 tempFileName = tempFile.fileName();
|
Chris@97
|
509 deleter.tempFile = tempFileName;
|
Chris@97
|
510 tempFile.close();
|
Chris@97
|
511 }
|
Chris@97
|
512
|
Chris@97
|
513 if (!unbundleFile(":out/" + libraryFileName, tempFileName, true)) {
|
Chris@97
|
514 return {};
|
Chris@97
|
515 }
|
Chris@97
|
516
|
Chris@97
|
517 return getLibraryPluginVersions(tempFileName);
|
Chris@97
|
518 }
|
Chris@97
|
519
|
Chris@67
|
520 bool isLibraryNewer(map<QString, int> a, map<QString, int> b)
|
Chris@67
|
521 {
|
Chris@67
|
522 // a and b are maps from plugin id to plugin version for libraries
|
Chris@67
|
523 // A and B. (There is no overarching library version number.) We
|
Chris@67
|
524 // deem library A to be newer than library B if:
|
Chris@67
|
525 //
|
Chris@67
|
526 // 1. A contains a plugin id that is also in B, whose version in
|
Chris@67
|
527 // A is newer than that in B, or
|
Chris@67
|
528 //
|
Chris@67
|
529 // 2. B is not newer than A according to rule 1, and neither A or
|
Chris@67
|
530 // B is empty, and A contains a plugin id that is not in B, and B
|
Chris@67
|
531 // does not contain any plugin id that is not in A
|
Chris@67
|
532 //
|
Chris@67
|
533 // (The not-empty part of rule 2 is just to avoid false positives
|
Chris@67
|
534 // when a library or its metadata could not be read at all.)
|
Chris@67
|
535
|
Chris@67
|
536 auto containsANewerPlugin = [](const map<QString, int> &m1,
|
Chris@67
|
537 const map<QString, int> &m2) {
|
Chris@67
|
538 for (auto p: m1) {
|
Chris@67
|
539 if (m2.find(p.first) != m2.end() &&
|
Chris@67
|
540 p.second > m2.at(p.first)) {
|
Chris@67
|
541 return true;
|
Chris@67
|
542 }
|
Chris@67
|
543 }
|
Chris@67
|
544 return false;
|
Chris@67
|
545 };
|
Chris@67
|
546
|
Chris@67
|
547 auto containsANovelPlugin = [](const map<QString, int> &m1,
|
Chris@67
|
548 const map<QString, int> &m2) {
|
Chris@67
|
549 for (auto p: m1) {
|
Chris@67
|
550 if (m2.find(p.first) == m2.end()) {
|
Chris@67
|
551 return true;
|
Chris@67
|
552 }
|
Chris@67
|
553 }
|
Chris@67
|
554 return false;
|
Chris@67
|
555 };
|
Chris@67
|
556
|
Chris@67
|
557 if (containsANewerPlugin(a, b)) {
|
Chris@67
|
558 return true;
|
Chris@67
|
559 }
|
Chris@67
|
560
|
Chris@67
|
561 if (!containsANewerPlugin(b, a) &&
|
Chris@67
|
562 !a.empty() &&
|
Chris@67
|
563 !b.empty() &&
|
Chris@67
|
564 containsANovelPlugin(a, b) &&
|
Chris@67
|
565 !containsANovelPlugin(b, a)) {
|
Chris@67
|
566 return true;
|
Chris@67
|
567 }
|
Chris@67
|
568
|
Chris@67
|
569 return false;
|
Chris@67
|
570 }
|
Chris@67
|
571
|
Chris@67
|
572 QString
|
Chris@67
|
573 versionsString(const map<QString, int> &vv)
|
Chris@67
|
574 {
|
Chris@67
|
575 QStringList pv;
|
Chris@67
|
576 for (auto v: vv) {
|
Chris@67
|
577 pv.push_back(QString("%1:%2").arg(v.first).arg(v.second));
|
Chris@67
|
578 }
|
Chris@67
|
579 return "{ " + pv.join(", ") + " }";
|
Chris@67
|
580 }
|
Chris@67
|
581
|
Chris@75
|
582 enum class RelativeStatus {
|
Chris@75
|
583 New,
|
Chris@75
|
584 Same,
|
Chris@75
|
585 Upgrade,
|
Chris@75
|
586 Downgrade,
|
Chris@75
|
587 TargetNotLoadable
|
Chris@75
|
588 };
|
Chris@75
|
589
|
Chris@75
|
590 QString
|
Chris@75
|
591 relativeStatusLabel(RelativeStatus status) {
|
Chris@75
|
592 switch (status) {
|
Chris@76
|
593 case RelativeStatus::New: return QObject::tr("Not yet installed");
|
Chris@76
|
594 case RelativeStatus::Same: return QObject::tr("Already installed");
|
Chris@76
|
595 case RelativeStatus::Upgrade: return QObject::tr("Update");
|
Chris@76
|
596 case RelativeStatus::Downgrade: return QObject::tr("Newer version installed");
|
Chris@84
|
597 case RelativeStatus::TargetNotLoadable: return QObject::tr("Installed version not working");
|
Chris@79
|
598 default: return {};
|
Chris@75
|
599 }
|
Chris@75
|
600 }
|
Chris@75
|
601
|
Chris@75
|
602 RelativeStatus
|
Chris@75
|
603 getRelativeStatus(LibraryInfo info, QString targetDir)
|
Chris@75
|
604 {
|
Chris@75
|
605 QString destination = targetDir + "/" + info.fileName;
|
Chris@75
|
606
|
Chris@75
|
607 SVCERR << "\ngetRelativeStatus: " << info.fileName << ":\n";
|
Chris@75
|
608
|
Chris@97
|
609 if (!QFileInfo(destination).exists()) {
|
Chris@97
|
610 SVCERR << " - relative status: " << relativeStatusLabel(RelativeStatus::New) << endl;
|
Chris@97
|
611 return RelativeStatus::New;
|
Chris@97
|
612 }
|
Chris@75
|
613
|
Chris@97
|
614 RelativeStatus status = RelativeStatus::Same;
|
Chris@75
|
615
|
Chris@97
|
616 auto packaged = getBundledLibraryPluginVersions(info.fileName);
|
Chris@97
|
617 auto installed = getLibraryPluginVersions(destination);
|
Chris@75
|
618
|
Chris@97
|
619 SVCERR << " * installed: " << versionsString(installed)
|
Chris@97
|
620 << "\n * packaged: " << versionsString(packaged)
|
Chris@97
|
621 << endl;
|
Chris@75
|
622
|
Chris@97
|
623 if (installed.empty()) {
|
Chris@97
|
624 status = RelativeStatus::TargetNotLoadable;
|
Chris@97
|
625 }
|
Chris@75
|
626
|
Chris@97
|
627 if (isLibraryNewer(installed, packaged)) {
|
Chris@97
|
628 status = RelativeStatus::Downgrade;
|
Chris@97
|
629 }
|
Chris@75
|
630
|
Chris@97
|
631 if (isLibraryNewer(packaged, installed)) {
|
Chris@97
|
632 status = RelativeStatus::Upgrade;
|
Chris@75
|
633 }
|
Chris@75
|
634
|
Chris@75
|
635 SVCERR << " - relative status: " << relativeStatusLabel(status) << endl;
|
Chris@75
|
636
|
Chris@75
|
637 return status;
|
Chris@75
|
638 }
|
Chris@75
|
639
|
Chris@86
|
640 bool
|
Chris@86
|
641 backup(QString filePath, QString backupDir)
|
Chris@86
|
642 {
|
Chris@86
|
643 QFileInfo file(filePath);
|
Chris@86
|
644
|
Chris@86
|
645 if (!file.exists()) {
|
Chris@86
|
646 return true;
|
Chris@86
|
647 }
|
Chris@86
|
648
|
Chris@86
|
649 if (!QDir(backupDir).exists()) {
|
Chris@86
|
650 QDir().mkpath(backupDir);
|
Chris@86
|
651 }
|
Chris@86
|
652
|
Chris@86
|
653 QString backup = backupDir + "/" + file.fileName() + ".bak";
|
Chris@86
|
654 SVCERR << "Note: existing file " << filePath
|
Chris@86
|
655 << " found, backing up to " << backup << endl;
|
Chris@86
|
656 if (!QFile(filePath).rename(backup)) {
|
Chris@86
|
657 SVCERR << "Failed to move " << filePath.toStdString()
|
Chris@86
|
658 << " to backup " << backup.toStdString() << endl;
|
Chris@86
|
659 return false;
|
Chris@86
|
660 }
|
Chris@86
|
661
|
Chris@86
|
662 return true;
|
Chris@86
|
663 }
|
Chris@86
|
664
|
Chris@81
|
665 QString
|
Chris@75
|
666 installLibrary(LibraryInfo info, QString targetDir)
|
Chris@42
|
667 {
|
Chris@75
|
668 QString library = info.fileName;
|
Chris@52
|
669 QString source = ":out";
|
Chris@75
|
670 QString destination = targetDir + "/" + library;
|
Chris@94
|
671
|
Chris@94
|
672 static QString backupDirName;
|
Chris@94
|
673 if (backupDirName == "") {
|
Chris@94
|
674 // Static so as to be created once - don't go creating a
|
Chris@94
|
675 // second directory if the clock ticks over by one second
|
Chris@94
|
676 // between library installs
|
Chris@94
|
677 backupDirName =
|
Chris@94
|
678 QString("saved-%1").arg(QDateTime::currentDateTime().toString
|
Chris@94
|
679 ("yyyyMMdd-hhmmss"));
|
Chris@94
|
680 }
|
Chris@94
|
681 QString backupDir = targetDir + "/" + backupDirName;
|
Chris@67
|
682
|
Chris@86
|
683 if (!QDir(targetDir).exists()) {
|
Chris@86
|
684 QDir().mkpath(targetDir);
|
Chris@67
|
685 }
|
Chris@82
|
686
|
Chris@86
|
687 if (!backup(destination, backupDir)) {
|
Chris@86
|
688 return QObject::tr("Failed to move aside existing library");
|
Chris@86
|
689 }
|
Chris@97
|
690
|
Chris@97
|
691 if (!unbundleFile(source + "/" + library, destination, true)) {
|
Chris@97
|
692 return QObject::tr("Failed to copy library file to target directory");
|
Chris@97
|
693 }
|
Chris@52
|
694
|
Chris@52
|
695 QString base = QFileInfo(library).baseName();
|
Chris@52
|
696 QDir dir(source);
|
Chris@52
|
697 auto entries = dir.entryList({ base + "*" });
|
Chris@52
|
698 for (auto e: entries) {
|
Chris@52
|
699 if (e == library) continue;
|
Chris@75
|
700 QString destination = targetDir + "/" + e;
|
Chris@86
|
701 if (!backup(destination, backupDir)) {
|
Chris@86
|
702 continue;
|
Chris@86
|
703 }
|
Chris@97
|
704 if (!unbundleFile(source + "/" + e, destination, false)) {
|
Chris@68
|
705 continue;
|
Chris@52
|
706 }
|
Chris@52
|
707 }
|
Chris@81
|
708
|
Chris@81
|
709 return {};
|
Chris@42
|
710 }
|
Chris@42
|
711
|
Chris@99
|
712 QString
|
Chris@99
|
713 getHelpText(vector<LibraryInfo> libraries)
|
Chris@99
|
714 {
|
Chris@99
|
715 set<QString, function<bool (QString, QString)>>
|
Chris@99
|
716 makers
|
Chris@99
|
717 ([](QString k1, QString k2) {
|
Chris@99
|
718 return k1.localeAwareCompare(k2) < 0;
|
Chris@99
|
719 });
|
Chris@99
|
720
|
Chris@99
|
721 for (auto info: libraries) {
|
Chris@99
|
722 makers.insert(info.maker);
|
Chris@99
|
723 }
|
Chris@99
|
724
|
Chris@99
|
725 QString makerList;
|
Chris@99
|
726 for (QString maker: makers) {
|
Chris@99
|
727 makerList += QObject::tr("<li>%1</li>").arg(maker);
|
Chris@99
|
728 }
|
Chris@99
|
729
|
Chris@99
|
730 return QObject::tr
|
Chris@99
|
731 ("<p>Vamp Plugin Pack collects together a number of <a href=\"https://vamp-plugins.org\">Vamp audio analysis plugins</a> into a single installer.</p>"
|
Chris@99
|
732 "<p>The libraries you select will be installed into the standard Vamp plugin directory, where hosts such as <a href=\"https://sonicvisualiser.org/\">Sonic Visualiser</a> can find them.</p>"
|
Chris@99
|
733 "<p>The plugin libraries included here were developed and published by various different authors and institutions:</p><ul>%1</ul>"
|
Chris@99
|
734 "<p>All of the libraries are open source and are redistributable under open-source licences. Click the information icon to the right of each library in the main window for more details.</p>"
|
Chris@99
|
735 "<p>The entire pack may be redistributed under the <a href=\"%2\">GNU Affero General Public License v3</a>.</p>"
|
Chris@99
|
736 "<p>The plugins were collected together, and the installer was written and published, at the <a href=\"https://c4dm.eecs.qmul.ac.uk\">Centre for Digital Music</a>, Queen Mary University of London.</p>")
|
Chris@99
|
737 .arg(makerList)
|
Chris@99
|
738 .arg(getLicenceURL(Licence::agpl));
|
Chris@99
|
739 }
|
Chris@99
|
740
|
Chris@75
|
741 vector<LibraryInfo>
|
Chris@75
|
742 getUserApprovedPluginLibraries(vector<LibraryInfo> libraries,
|
Chris@75
|
743 QString targetDir)
|
Chris@42
|
744 {
|
Chris@42
|
745 QDialog dialog;
|
Chris@46
|
746
|
Chris@84
|
747 int fontHeight = QFontMetrics(dialog.font()).height();
|
Chris@84
|
748 int dpratio = dialog.devicePixelRatio();
|
Chris@84
|
749
|
Chris@46
|
750 auto mainLayout = new QGridLayout;
|
Chris@47
|
751 mainLayout->setSpacing(0);
|
Chris@46
|
752 dialog.setLayout(mainLayout);
|
Chris@46
|
753
|
Chris@46
|
754 int mainRow = 0;
|
Chris@46
|
755
|
Chris@74
|
756 auto selectionFrame = new QWidget;
|
Chris@74
|
757 mainLayout->addWidget(selectionFrame, mainRow, 0);
|
Chris@47
|
758 ++mainRow;
|
Chris@46
|
759
|
Chris@46
|
760 auto selectionLayout = new QGridLayout;
|
Chris@84
|
761 selectionLayout->setContentsMargins(0, 0, 0, 0);
|
Chris@84
|
762 selectionLayout->setSpacing(fontHeight / 6);
|
Chris@46
|
763 selectionFrame->setLayout(selectionLayout);
|
Chris@84
|
764
|
Chris@46
|
765 int selectionRow = 0;
|
Chris@84
|
766 int checkColumn = 0;
|
Chris@84
|
767 int titleColumn = 1;
|
Chris@84
|
768 int statusColumn = 2;
|
Chris@85
|
769 int infoColumn = 4; // column 3 is a small sliver of spacing
|
Chris@79
|
770
|
Chris@126
|
771 QString additionalNote = "";
|
Chris@126
|
772 if (sizeof(char *) == 4) {
|
Chris@126
|
773 additionalNote = QObject::tr("(32-bit)");
|
Chris@126
|
774 }
|
Chris@126
|
775
|
Chris@79
|
776 selectionLayout->addWidget
|
Chris@126
|
777 (new QLabel(QObject::tr("<b>Vamp Plugin Pack</b> v%1 %2")
|
Chris@126
|
778 .arg(PACK_VERSION)
|
Chris@126
|
779 .arg(additionalNote)),
|
Chris@84
|
780 selectionRow, titleColumn, 1, 3);
|
Chris@79
|
781 ++selectionRow;
|
Chris@79
|
782
|
Chris@79
|
783 selectionLayout->addWidget
|
Chris@79
|
784 (new QLabel(QObject::tr("Select the plugin libraries to install:")),
|
Chris@84
|
785 selectionRow, titleColumn, 1, 3);
|
Chris@79
|
786 ++selectionRow;
|
Chris@74
|
787
|
Chris@74
|
788 auto checkAll = new QCheckBox;
|
Chris@74
|
789 checkAll->setChecked(true);
|
Chris@84
|
790 selectionLayout->addWidget
|
Chris@84
|
791 (checkAll, selectionRow, checkColumn, Qt::AlignHCenter);
|
Chris@74
|
792 ++selectionRow;
|
Chris@74
|
793
|
Chris@85
|
794 auto checkArrow = new QLabel(
|
Chris@85
|
795 #ifdef Q_OS_MAC
|
Chris@85
|
796 " ▼"
|
Chris@85
|
797 #else
|
Chris@85
|
798 "▼"
|
Chris@85
|
799 #endif
|
Chris@85
|
800 );
|
Chris@74
|
801 checkArrow->setTextFormat(Qt::RichText);
|
Chris@84
|
802 selectionLayout->addWidget
|
Chris@84
|
803 (checkArrow, selectionRow, checkColumn, Qt::AlignHCenter);
|
Chris@74
|
804 ++selectionRow;
|
Chris@42
|
805
|
Chris@67
|
806 map<QString, QCheckBox *> checkBoxMap; // filename -> checkbox
|
Chris@67
|
807 map<QString, LibraryInfo> libFileInfo; // filename -> info
|
Chris@76
|
808 map<QString, RelativeStatus> statuses; // filename -> status
|
Chris@43
|
809
|
Chris@67
|
810 map<QString, LibraryInfo, function<bool (QString, QString)>>
|
Chris@49
|
811 orderedInfo
|
Chris@49
|
812 ([](QString k1, QString k2) {
|
Chris@49
|
813 return k1.localeAwareCompare(k2) < 0;
|
Chris@49
|
814 });
|
Chris@43
|
815 for (auto info: libraries) {
|
Chris@43
|
816 orderedInfo[info.title] = info;
|
Chris@43
|
817 }
|
Chris@53
|
818
|
Chris@77
|
819 QPixmap infoMap(fontHeight * dpratio, fontHeight * dpratio);
|
Chris@77
|
820 QPixmap moreMap(fontHeight * dpratio * 2, fontHeight * dpratio * 2);
|
Chris@74
|
821 infoMap.fill(Qt::transparent);
|
Chris@74
|
822 moreMap.fill(Qt::transparent);
|
Chris@74
|
823 QSvgRenderer renderer(QString(":icons/scalable/info.svg"));
|
Chris@74
|
824 QPainter painter;
|
Chris@74
|
825 painter.begin(&infoMap);
|
Chris@74
|
826 renderer.render(&painter);
|
Chris@74
|
827 painter.end();
|
Chris@74
|
828 painter.begin(&moreMap);
|
Chris@74
|
829 renderer.render(&painter);
|
Chris@74
|
830 painter.end();
|
Chris@74
|
831
|
Chris@76
|
832 auto shouldCheck = [](RelativeStatus status) {
|
Chris@76
|
833 return (status == RelativeStatus::New ||
|
Chris@76
|
834 status == RelativeStatus::Upgrade ||
|
Chris@76
|
835 status == RelativeStatus::TargetNotLoadable);
|
Chris@76
|
836 };
|
Chris@76
|
837
|
Chris@43
|
838 for (auto ip: orderedInfo) {
|
Chris@46
|
839
|
Chris@46
|
840 auto cb = new QCheckBox;
|
Chris@84
|
841 selectionLayout->addWidget
|
Chris@84
|
842 (cb, selectionRow, checkColumn, Qt::AlignHCenter);
|
Chris@46
|
843
|
Chris@43
|
844 LibraryInfo info = ip.second;
|
Chris@72
|
845
|
Chris@76
|
846 auto shortLabel = new QLabel(info.title);
|
Chris@84
|
847 selectionLayout->addWidget(shortLabel, selectionRow, titleColumn);
|
Chris@76
|
848
|
Chris@75
|
849 RelativeStatus relativeStatus = getRelativeStatus(info, targetDir);
|
Chris@76
|
850 auto statusLabel = new QLabel(relativeStatusLabel(relativeStatus));
|
Chris@84
|
851 selectionLayout->addWidget(statusLabel, selectionRow, statusColumn);
|
Chris@76
|
852 cb->setChecked(shouldCheck(relativeStatus));
|
Chris@75
|
853
|
Chris@79
|
854 auto infoButton = new QToolButton;
|
Chris@79
|
855 infoButton->setAutoRaise(true);
|
Chris@79
|
856 infoButton->setIcon(infoMap);
|
Chris@79
|
857 infoButton->setIconSize(QSize(fontHeight, fontHeight));
|
Chris@77
|
858
|
Chris@77
|
859 #ifdef Q_OS_MAC
|
Chris@79
|
860 infoButton->setFixedSize(QSize(int(fontHeight * 1.2),
|
Chris@77
|
861 int(fontHeight * 1.2)));
|
Chris@79
|
862 infoButton->setStyleSheet("QToolButton { border: none; }");
|
Chris@77
|
863 #endif
|
Chris@77
|
864
|
Chris@84
|
865 selectionLayout->addWidget(infoButton, selectionRow, infoColumn);
|
Chris@46
|
866
|
Chris@46
|
867 ++selectionRow;
|
Chris@46
|
868
|
Chris@77
|
869 QString moreTitleText = QObject::tr("<b>%1</b><br><i>%2</i>")
|
Chris@72
|
870 .arg(info.title)
|
Chris@77
|
871 .arg(info.maker);
|
Chris@77
|
872
|
Chris@79
|
873 QString moreInfoText = info.description;
|
Chris@79
|
874
|
Chris@79
|
875 if (info.page != "") {
|
Chris@79
|
876 moreInfoText += QObject::tr("<br><a href=\"%1\">%2</a>")
|
Chris@79
|
877 .arg(info.page)
|
Chris@79
|
878 .arg(info.page);
|
Chris@79
|
879 }
|
Chris@79
|
880
|
Chris@79
|
881 moreInfoText += QObject::tr("<br><br>Library contains:<ul>");
|
Chris@73
|
882
|
Chris@73
|
883 int n = 0;
|
Chris@73
|
884 bool closed = false;
|
Chris@73
|
885 for (auto title: info.pluginTitles) {
|
Chris@73
|
886 if (n == 10 && info.pluginTitles.size() > 15) {
|
Chris@77
|
887 moreInfoText += QObject::tr("</ul>");
|
Chris@77
|
888 moreInfoText += QObject::tr("... and %n other plugins.<br><br>",
|
Chris@77
|
889 "",
|
Chris@77
|
890 info.pluginTitles.size() - n);
|
Chris@73
|
891 closed = true;
|
Chris@73
|
892 break;
|
Chris@73
|
893 }
|
Chris@77
|
894 moreInfoText += QObject::tr("<li>%1</li>").arg(title);
|
Chris@73
|
895 ++n;
|
Chris@73
|
896 }
|
Chris@73
|
897
|
Chris@73
|
898 if (!closed) {
|
Chris@77
|
899 moreInfoText += QObject::tr("</ul>");
|
Chris@73
|
900 }
|
Chris@74
|
901
|
Chris@74
|
902 if (info.licence != "") {
|
Chris@99
|
903 moreInfoText += QObject::tr("Provided under the <a href=\"%1\">%2</a>.<br>")
|
Chris@99
|
904 .arg(getLicenceURL(info.licence))
|
Chris@77
|
905 .arg(info.licence);
|
Chris@74
|
906 }
|
Chris@72
|
907
|
Chris@79
|
908 QObject::connect(infoButton, &QAbstractButton::clicked,
|
Chris@72
|
909 [=]() {
|
Chris@74
|
910 QMessageBox mbox;
|
Chris@74
|
911 mbox.setIconPixmap(moreMap);
|
Chris@74
|
912 mbox.setWindowTitle(QObject::tr("Library contents"));
|
Chris@77
|
913 mbox.setText(moreTitleText);
|
Chris@77
|
914 mbox.setInformativeText(moreInfoText);
|
Chris@74
|
915 mbox.exec();
|
Chris@72
|
916 });
|
Chris@72
|
917
|
Chris@43
|
918 checkBoxMap[info.fileName] = cb;
|
Chris@67
|
919 libFileInfo[info.fileName] = info;
|
Chris@76
|
920 statuses[info.fileName] = relativeStatus;
|
Chris@42
|
921 }
|
Chris@42
|
922
|
Chris@79
|
923 selectionLayout->addItem(new QSpacerItem(1, (fontHeight*2) / 3),
|
Chris@79
|
924 selectionRow, 0);
|
Chris@79
|
925 ++selectionRow;
|
Chris@79
|
926
|
Chris@79
|
927 selectionLayout->addWidget
|
Chris@79
|
928 (new QLabel(QObject::tr("Installation will be to: %1").arg(targetDir)),
|
Chris@84
|
929 selectionRow, titleColumn, 1, 3);
|
Chris@79
|
930 ++selectionRow;
|
Chris@79
|
931
|
Chris@47
|
932 QObject::connect(checkAll, &QCheckBox::toggled,
|
Chris@72
|
933 [=](bool toCheck) {
|
Chris@47
|
934 for (auto p: checkBoxMap) {
|
Chris@47
|
935 p.second->setChecked(toCheck);
|
Chris@47
|
936 }
|
Chris@47
|
937 });
|
Chris@79
|
938
|
Chris@79
|
939 mainLayout->addItem(new QSpacerItem(1, fontHeight), mainRow, 0);
|
Chris@79
|
940 ++mainRow;
|
Chris@79
|
941
|
Chris@42
|
942 auto bb = new QDialogButtonBox(QDialogButtonBox::Ok |
|
Chris@76
|
943 QDialogButtonBox::Cancel |
|
Chris@99
|
944 QDialogButtonBox::Reset |
|
Chris@99
|
945 QDialogButtonBox::Help);
|
Chris@79
|
946 bb->button(QDialogButtonBox::Ok)->setText(QObject::tr("Install"));
|
Chris@74
|
947 mainLayout->addWidget(bb, mainRow, 0);
|
Chris@46
|
948 ++mainRow;
|
Chris@47
|
949
|
Chris@74
|
950 mainLayout->setRowStretch(0, 10);
|
Chris@74
|
951 mainLayout->setColumnStretch(0, 10);
|
Chris@74
|
952 selectionLayout->setColumnMinimumWidth(0, 50);
|
Chris@85
|
953 #ifdef Q_OS_MAC
|
Chris@85
|
954 selectionLayout->setColumnMinimumWidth(3, 10);
|
Chris@85
|
955 selectionLayout->setColumnMinimumWidth(5, 12);
|
Chris@85
|
956 #endif
|
Chris@74
|
957 selectionLayout->setColumnStretch(1, 10);
|
Chris@47
|
958
|
Chris@76
|
959 QObject::connect
|
Chris@76
|
960 (bb, &QDialogButtonBox::clicked,
|
Chris@76
|
961 [&](QAbstractButton *button) {
|
Chris@76
|
962
|
Chris@76
|
963 auto role = bb->buttonRole(button);
|
Chris@76
|
964
|
Chris@76
|
965 switch (role) {
|
Chris@76
|
966
|
Chris@76
|
967 case QDialogButtonBox::AcceptRole: {
|
Chris@76
|
968 bool downgrade = false;
|
Chris@76
|
969 for (const auto &p: checkBoxMap) {
|
Chris@76
|
970 if (p.second->isChecked() &&
|
Chris@76
|
971 statuses.at(p.first) == RelativeStatus::Downgrade) {
|
Chris@76
|
972 downgrade = true;
|
Chris@76
|
973 break;
|
Chris@76
|
974 }
|
Chris@76
|
975 }
|
Chris@76
|
976 if (downgrade) {
|
Chris@76
|
977 if (QMessageBox::warning
|
Chris@76
|
978 (bb, QObject::tr("Downgrade?"),
|
Chris@76
|
979 QObject::tr("You have asked to downgrade one or more plugin libraries that are already installed.<br><br>Are you sure?"),
|
Chris@76
|
980 QMessageBox::Ok | QMessageBox::Cancel,
|
Chris@76
|
981 QMessageBox::Cancel) == QMessageBox::Ok) {
|
Chris@76
|
982 dialog.accept();
|
Chris@76
|
983 }
|
Chris@76
|
984 } else {
|
Chris@76
|
985 dialog.accept();
|
Chris@76
|
986 }
|
Chris@76
|
987 break;
|
Chris@76
|
988 }
|
Chris@76
|
989
|
Chris@76
|
990 case QDialogButtonBox::RejectRole:
|
Chris@76
|
991 dialog.reject();
|
Chris@76
|
992 break;
|
Chris@76
|
993
|
Chris@76
|
994 case QDialogButtonBox::ResetRole:
|
Chris@76
|
995 for (const auto &p: checkBoxMap) {
|
Chris@76
|
996 p.second->setChecked(shouldCheck(statuses.at(p.first)));
|
Chris@76
|
997 }
|
Chris@76
|
998 break;
|
Chris@76
|
999
|
Chris@99
|
1000 case QDialogButtonBox::HelpRole: {
|
Chris@99
|
1001 QMessageBox mbox;
|
Chris@99
|
1002 mbox.setWindowTitle(QApplication::applicationName());
|
Chris@99
|
1003 mbox.setText(QObject::tr("<b>Vamp Plugin Pack</b>"));
|
Chris@99
|
1004 mbox.setInformativeText(getHelpText(libraries));
|
Chris@99
|
1005 mbox.exec();
|
Chris@99
|
1006 break;
|
Chris@99
|
1007 }
|
Chris@99
|
1008
|
Chris@76
|
1009 default:
|
Chris@76
|
1010 SVCERR << "WARNING: Unexpected role " << role << endl;
|
Chris@99
|
1011 break;
|
Chris@76
|
1012 }
|
Chris@76
|
1013 });
|
Chris@98
|
1014
|
Chris@98
|
1015 if (QString(PACK_VERSION).contains("-pre") ||
|
Chris@98
|
1016 QString(PACK_VERSION).contains("-alpha") ||
|
Chris@98
|
1017 QString(PACK_VERSION).contains("-beta")) {
|
Chris@98
|
1018 QTimer::singleShot
|
Chris@98
|
1019 (500, [&]() {
|
Chris@98
|
1020 QString url = "https://code.soundsoftware.ac.uk/projects/vamp-plugin-pack";
|
Chris@98
|
1021 QMessageBox::information
|
Chris@98
|
1022 (&dialog, QObject::tr("Test release"),
|
Chris@98
|
1023 QObject::tr("<b>This is a test release of %1</b><p>Please send any feedback to the developers. See <a href=\"%2\">%3</a> for more information.</p>").arg(QApplication::applicationName()).arg(url).arg(url));
|
Chris@98
|
1024 });
|
Chris@98
|
1025 }
|
Chris@98
|
1026
|
Chris@70
|
1027 if (dialog.exec() != QDialog::Accepted) {
|
Chris@51
|
1028 SVCERR << "rejected" << endl;
|
Chris@70
|
1029 return {};
|
Chris@42
|
1030 }
|
Chris@42
|
1031
|
Chris@75
|
1032 vector<LibraryInfo> approved;
|
Chris@42
|
1033 for (const auto &p: checkBoxMap) {
|
Chris@42
|
1034 if (p.second->isChecked()) {
|
Chris@75
|
1035 approved.push_back(libFileInfo[p.first]);
|
Chris@33
|
1036 }
|
Chris@42
|
1037 }
|
Chris@42
|
1038
|
Chris@42
|
1039 return approved;
|
Chris@42
|
1040 }
|
Chris@42
|
1041
|
Chris@42
|
1042 int main(int argc, char **argv)
|
Chris@42
|
1043 {
|
Chris@93
|
1044 if (argc == 2 && (QString(argv[1]) == "--version" ||
|
Chris@93
|
1045 QString(argv[1]) == "-v")) {
|
Chris@95
|
1046 cerr << PACK_VERSION << std::endl; // std:: needed here for MSVC for some reason
|
Chris@93
|
1047 exit(0);
|
Chris@93
|
1048 }
|
Chris@93
|
1049
|
Chris@42
|
1050 QApplication app(argc, argv);
|
Chris@42
|
1051
|
Chris@51
|
1052 QApplication::setOrganizationName("sonic-visualiser");
|
Chris@51
|
1053 QApplication::setOrganizationDomain("sonicvisualiser.org");
|
Chris@51
|
1054 QApplication::setApplicationName(QApplication::tr("Vamp Plugin Pack Installer"));
|
Chris@51
|
1055
|
Chris@77
|
1056 QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
|
Chris@77
|
1057
|
Chris@51
|
1058 #ifdef Q_OS_WIN32
|
Chris@51
|
1059 QFont font(QApplication::font());
|
Chris@51
|
1060 QString preferredFamily = "Segoe UI";
|
Chris@51
|
1061 font.setFamily(preferredFamily);
|
Chris@51
|
1062 if (QFontInfo(font).family() == preferredFamily) {
|
Chris@51
|
1063 font.setPointSize(10);
|
Chris@51
|
1064 QApplication::setFont(font);
|
Chris@51
|
1065 }
|
Chris@77
|
1066 #else
|
Chris@77
|
1067 #ifdef Q_OS_MAC
|
Chris@77
|
1068 QFont font(QApplication::font());
|
Chris@77
|
1069 QString preferredFamily = "Lucida Grande";
|
Chris@77
|
1070 font.setFamily(preferredFamily);
|
Chris@77
|
1071 if (QFontInfo(font).family() == preferredFamily) {
|
Chris@85
|
1072 font.setPointSize(12);
|
Chris@77
|
1073 QApplication::setFont(font);
|
Chris@77
|
1074 }
|
Chris@77
|
1075 #endif
|
Chris@51
|
1076 #endif
|
Chris@51
|
1077
|
Chris@42
|
1078 QString target = getDefaultInstallDirectory();
|
Chris@42
|
1079 if (target == "") {
|
Chris@42
|
1080 return 1;
|
Chris@42
|
1081 }
|
Chris@42
|
1082
|
Chris@42
|
1083 QStringList libraries = getPluginLibraryList();
|
Chris@42
|
1084
|
Chris@43
|
1085 auto rdfStore = loadLibrariesRdf();
|
Chris@43
|
1086
|
Chris@43
|
1087 auto info = getLibraryInfo(*rdfStore, libraries);
|
Chris@43
|
1088
|
Chris@75
|
1089 vector<LibraryInfo> toInstall =
|
Chris@75
|
1090 getUserApprovedPluginLibraries(info, target);
|
Chris@83
|
1091
|
Chris@100
|
1092 if (toInstall.empty()) { // Cancelled, or nothing selected
|
Chris@100
|
1093 SVCERR << "No libraries selected for installation, nothing to do"
|
Chris@100
|
1094 << endl;
|
Chris@100
|
1095 return 0;
|
Chris@100
|
1096 }
|
Chris@100
|
1097
|
Chris@81
|
1098 QProgressDialog progress(QObject::tr("Installing..."),
|
Chris@95
|
1099 QObject::tr("Stop"), 0,
|
Chris@95
|
1100 int(toInstall.size()) + 1);
|
Chris@81
|
1101 progress.setMinimumDuration(0);
|
Chris@99
|
1102
|
Chris@81
|
1103 int pval = 0;
|
Chris@81
|
1104 bool complete = true;
|
Chris@42
|
1105
|
Chris@42
|
1106 for (auto lib: toInstall) {
|
Chris@81
|
1107 progress.setValue(++pval);
|
Chris@81
|
1108 QThread::currentThread()->msleep(40);
|
Chris@81
|
1109 app.processEvents();
|
Chris@81
|
1110 if (progress.wasCanceled()) {
|
Chris@81
|
1111 complete = false;
|
Chris@81
|
1112 break;
|
Chris@81
|
1113 }
|
Chris@81
|
1114 QString error = installLibrary(lib, target);
|
Chris@81
|
1115 if (error != "") {
|
Chris@81
|
1116 complete = false;
|
Chris@81
|
1117 if (QMessageBox::critical
|
Chris@81
|
1118 (&progress,
|
Chris@81
|
1119 QObject::tr("Install failed"),
|
Chris@81
|
1120 QObject::tr("Failed to install library \"%1\": %2")
|
Chris@81
|
1121 .arg(lib.title)
|
Chris@81
|
1122 .arg(error),
|
Chris@81
|
1123 QMessageBox::Abort | QMessageBox::Ignore,
|
Chris@81
|
1124 QMessageBox::Ignore) ==
|
Chris@81
|
1125 QMessageBox::Abort) {
|
Chris@81
|
1126 break;
|
Chris@81
|
1127 }
|
Chris@81
|
1128 }
|
Chris@81
|
1129 }
|
Chris@81
|
1130
|
Chris@81
|
1131 progress.hide();
|
Chris@81
|
1132
|
Chris@81
|
1133 if (complete) {
|
Chris@81
|
1134 QMessageBox::information
|
Chris@81
|
1135 (&progress,
|
Chris@81
|
1136 QObject::tr("Complete"),
|
Chris@81
|
1137 QObject::tr("Installation completed successfully"),
|
Chris@81
|
1138 QMessageBox::Ok,
|
Chris@81
|
1139 QMessageBox::Ok);
|
Chris@81
|
1140 } else {
|
Chris@81
|
1141 QMessageBox::information
|
Chris@81
|
1142 (&progress,
|
Chris@81
|
1143 QObject::tr("Incomplete"),
|
Chris@81
|
1144 QObject::tr("Installation was not complete. Exiting"),
|
Chris@81
|
1145 QMessageBox::Ok,
|
Chris@81
|
1146 QMessageBox::Ok);
|
Chris@33
|
1147 }
|
Chris@33
|
1148
|
Chris@83
|
1149 return (complete ? 0 : 2);
|
Chris@32
|
1150 }
|