|
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@74
|
152 QString
|
|
Chris@74
|
153 identifyLicence(QString libraryBasename)
|
|
Chris@74
|
154 {
|
|
Chris@74
|
155 QString licenceFile = QString(":out/%1_COPYING.txt").arg(libraryBasename);
|
|
Chris@74
|
156
|
|
Chris@74
|
157 QFile f(licenceFile);
|
|
Chris@74
|
158 if (!f.open(QFile::ReadOnly | QFile::Text)) {
|
|
Chris@74
|
159 SVCERR << "Failed to open licence file "
|
|
Chris@74
|
160 << licenceFile.toStdString() << endl;
|
|
Chris@74
|
161 return {};
|
|
Chris@74
|
162 }
|
|
Chris@74
|
163
|
|
Chris@74
|
164 QByteArray content = f.readAll();
|
|
Chris@74
|
165 f.close();
|
|
Chris@74
|
166
|
|
Chris@74
|
167 QString licenceText = QString::fromUtf8(content);
|
|
Chris@74
|
168
|
|
Chris@74
|
169 QString gpl = "GNU General Public License";
|
|
Chris@74
|
170 QString agpl = "GNU Affero General Public License";
|
|
Chris@74
|
171 QString apache = "Apache License";
|
|
Chris@74
|
172 QString mit = "MIT License";
|
|
Chris@74
|
173
|
|
Chris@82
|
174 // NB these are not expected to identify an arbitrary licence! We
|
|
Chris@74
|
175 // know we have only a limited set here. But we do want to
|
|
Chris@74
|
176 // determine this from the actual licence text included with the
|
|
Chris@74
|
177 // plugin distribution, not just from e.g. RDF metadata
|
|
Chris@74
|
178
|
|
Chris@74
|
179 if (licenceText.contains(gpl.toUpper(), Qt::CaseSensitive)) {
|
|
Chris@74
|
180 if (licenceText.contains("Version 3, 29 June 2007")) {
|
|
Chris@74
|
181 return QString("%1, version 3").arg(gpl);
|
|
Chris@74
|
182 } else if (licenceText.contains("Version 2, June 1991")) {
|
|
Chris@74
|
183 return QString("%1, version 2").arg(gpl);
|
|
Chris@74
|
184 } else {
|
|
Chris@74
|
185 return gpl;
|
|
Chris@74
|
186 }
|
|
Chris@74
|
187 }
|
|
Chris@74
|
188 if (licenceText.contains(agpl.toUpper(), Qt::CaseSensitive)) {
|
|
Chris@74
|
189 return agpl;
|
|
Chris@74
|
190 }
|
|
Chris@74
|
191 if (licenceText.contains(apache)) {
|
|
Chris@74
|
192 return apache;
|
|
Chris@74
|
193 }
|
|
Chris@74
|
194 if (licenceText.contains("Permission is hereby granted, free of charge, to any person")) {
|
|
Chris@74
|
195 return mit;
|
|
Chris@74
|
196 }
|
|
Chris@74
|
197
|
|
Chris@74
|
198 SVCERR << "Didn't recognise licence for " << libraryBasename << endl;
|
|
Chris@74
|
199
|
|
Chris@74
|
200 return {};
|
|
Chris@74
|
201 }
|
|
Chris@74
|
202
|
|
Chris@43
|
203 vector<LibraryInfo>
|
|
Chris@43
|
204 getLibraryInfo(const Store &store, QStringList libraries)
|
|
Chris@43
|
205 {
|
|
Chris@43
|
206 /* e.g.
|
|
Chris@43
|
207
|
|
Chris@43
|
208 plugbase:library a vamp:PluginLibrary ;
|
|
Chris@43
|
209 vamp:identifier "qm-vamp-plugins" ;
|
|
Chris@43
|
210 dc:title "Queen Mary plugin set"
|
|
Chris@43
|
211 */
|
|
Chris@43
|
212
|
|
Chris@43
|
213 Triples tt = store.match(Triple(Node(),
|
|
Chris@43
|
214 Uri("a"),
|
|
Chris@43
|
215 store.expand("vamp:PluginLibrary")));
|
|
Chris@43
|
216
|
|
Chris@67
|
217 map<QString, QString> wanted; // basename -> full lib name
|
|
Chris@43
|
218 for (auto lib: libraries) {
|
|
Chris@43
|
219 wanted[QFileInfo(lib).baseName()] = lib;
|
|
Chris@43
|
220 }
|
|
Chris@43
|
221
|
|
Chris@43
|
222 vector<LibraryInfo> results;
|
|
Chris@43
|
223
|
|
Chris@43
|
224 for (auto t: tt) {
|
|
Chris@43
|
225
|
|
Chris@43
|
226 Node libId = store.complete(Triple(t.subject(),
|
|
Chris@43
|
227 store.expand("vamp:identifier"),
|
|
Chris@43
|
228 Node()));
|
|
Chris@43
|
229 if (libId.type != Node::Literal) {
|
|
Chris@43
|
230 continue;
|
|
Chris@43
|
231 }
|
|
Chris@43
|
232 auto wi = wanted.find(libId.value);
|
|
Chris@43
|
233 if (wi == wanted.end()) {
|
|
Chris@43
|
234 continue;
|
|
Chris@43
|
235 }
|
|
Chris@74
|
236
|
|
Chris@50
|
237 Node title = store.complete(Triple(t.subject(),
|
|
Chris@50
|
238 store.expand("dc:title"),
|
|
Chris@50
|
239 Node()));
|
|
Chris@50
|
240 if (title.type != Node::Literal) {
|
|
Chris@50
|
241 continue;
|
|
Chris@50
|
242 }
|
|
Chris@50
|
243
|
|
Chris@43
|
244 LibraryInfo info;
|
|
Chris@43
|
245 info.id = wi->first;
|
|
Chris@43
|
246 info.fileName = wi->second;
|
|
Chris@50
|
247 info.title = title.value;
|
|
Chris@43
|
248
|
|
Chris@43
|
249 Node maker = store.complete(Triple(t.subject(),
|
|
Chris@43
|
250 store.expand("foaf:maker"),
|
|
Chris@43
|
251 Node()));
|
|
Chris@43
|
252 if (maker.type == Node::Literal) {
|
|
Chris@43
|
253 info.maker = maker.value;
|
|
Chris@46
|
254 } else if (maker != Node()) {
|
|
Chris@46
|
255 maker = store.complete(Triple(maker,
|
|
Chris@46
|
256 store.expand("foaf:name"),
|
|
Chris@46
|
257 Node()));
|
|
Chris@46
|
258 if (maker.type == Node::Literal) {
|
|
Chris@46
|
259 info.maker = maker.value;
|
|
Chris@46
|
260 }
|
|
Chris@43
|
261 }
|
|
Chris@46
|
262
|
|
Chris@43
|
263 Node desc = store.complete(Triple(t.subject(),
|
|
Chris@43
|
264 store.expand("dc:description"),
|
|
Chris@43
|
265 Node()));
|
|
Chris@43
|
266 if (desc.type == Node::Literal) {
|
|
Chris@43
|
267 info.description = desc.value;
|
|
Chris@43
|
268 }
|
|
Chris@78
|
269
|
|
Chris@78
|
270 Node page = store.complete(Triple(t.subject(),
|
|
Chris@78
|
271 store.expand("foaf:page"),
|
|
Chris@78
|
272 Node()));
|
|
Chris@79
|
273 if (page.type == Node::URI) {
|
|
Chris@79
|
274 info.page = page.value;
|
|
Chris@78
|
275 }
|
|
Chris@43
|
276
|
|
Chris@47
|
277 Triples pp = store.match(Triple(t.subject(),
|
|
Chris@47
|
278 store.expand("vamp:available_plugin"),
|
|
Chris@47
|
279 Node()));
|
|
Chris@47
|
280 for (auto p: pp) {
|
|
Chris@47
|
281 Node ptitle = store.complete(Triple(p.object(),
|
|
Chris@47
|
282 store.expand("dc:title"),
|
|
Chris@47
|
283 Node()));
|
|
Chris@47
|
284 if (ptitle.type == Node::Literal) {
|
|
Chris@47
|
285 info.pluginTitles.push_back(ptitle.value);
|
|
Chris@47
|
286 }
|
|
Chris@47
|
287 }
|
|
Chris@74
|
288
|
|
Chris@74
|
289 info.licence = identifyLicence(libId.value);
|
|
Chris@74
|
290 SVCERR << "licence = " << info.licence << endl;
|
|
Chris@47
|
291
|
|
Chris@43
|
292 results.push_back(info);
|
|
Chris@50
|
293 wanted.erase(libId.value);
|
|
Chris@43
|
294 }
|
|
Chris@43
|
295
|
|
Chris@50
|
296 for (auto wp: wanted) {
|
|
Chris@51
|
297 SVCERR << "Failed to find any RDF information about library "
|
|
Chris@51
|
298 << wp.second << endl;
|
|
Chris@50
|
299 }
|
|
Chris@50
|
300
|
|
Chris@43
|
301 return results;
|
|
Chris@43
|
302 }
|
|
Chris@43
|
303
|
|
Chris@67
|
304 struct TempFileDeleter {
|
|
Chris@67
|
305 ~TempFileDeleter() {
|
|
Chris@67
|
306 if (tempFile != "") {
|
|
Chris@67
|
307 QFile(tempFile).remove();
|
|
Chris@67
|
308 }
|
|
Chris@67
|
309 }
|
|
Chris@67
|
310 QString tempFile;
|
|
Chris@67
|
311 };
|
|
Chris@67
|
312
|
|
Chris@97
|
313 bool
|
|
Chris@97
|
314 unbundleFile(QString filePath, QString targetPath, bool isExecutable)
|
|
Chris@97
|
315 {
|
|
Chris@97
|
316 SVCERR << "Copying " << filePath.toStdString() << " to "
|
|
Chris@97
|
317 << targetPath.toStdString() << "..." << endl;
|
|
Chris@97
|
318
|
|
Chris@97
|
319 // This has to be able to work even if the destination exists, and
|
|
Chris@97
|
320 // to do so without deleting it first - e.g. when copying to a
|
|
Chris@97
|
321 // temporary file. So we open the file and copy to it ourselves
|
|
Chris@97
|
322 // rather than use QFile::copy
|
|
Chris@97
|
323
|
|
Chris@97
|
324 QFile source(filePath);
|
|
Chris@97
|
325 if (!source.open(QFile::ReadOnly)) {
|
|
Chris@97
|
326 SVCERR << "ERROR: Failed to read bundled file " << filePath << endl;
|
|
Chris@97
|
327 return {};
|
|
Chris@97
|
328 }
|
|
Chris@97
|
329 QByteArray content = source.readAll();
|
|
Chris@97
|
330 source.close();
|
|
Chris@97
|
331
|
|
Chris@97
|
332 QFile target(targetPath);
|
|
Chris@97
|
333 if (!target.open(QFile::WriteOnly)) {
|
|
Chris@97
|
334 SVCERR << "ERROR: Failed to read target file " << targetPath << endl;
|
|
Chris@97
|
335 return {};
|
|
Chris@97
|
336 }
|
|
Chris@97
|
337 if (target.write(content) != content.size()) {
|
|
Chris@97
|
338 SVCERR << "ERROR: Incomplete write to target file" << endl;
|
|
Chris@97
|
339 return {};
|
|
Chris@97
|
340 }
|
|
Chris@97
|
341 target.close();
|
|
Chris@97
|
342
|
|
Chris@97
|
343 auto permissions =
|
|
Chris@97
|
344 QFile::ReadOwner | QFile::WriteOwner |
|
|
Chris@97
|
345 QFile::ReadGroup |
|
|
Chris@97
|
346 QFile::ReadOther;
|
|
Chris@97
|
347
|
|
Chris@97
|
348 if (isExecutable) {
|
|
Chris@97
|
349 permissions |=
|
|
Chris@97
|
350 QFile::ExeOwner |
|
|
Chris@97
|
351 QFile::ExeGroup |
|
|
Chris@97
|
352 QFile::ExeOther;
|
|
Chris@97
|
353 };
|
|
Chris@97
|
354
|
|
Chris@97
|
355 if (!QFile::setPermissions(targetPath, permissions)) {
|
|
Chris@97
|
356 SVCERR << "Failed to set permissions on "
|
|
Chris@97
|
357 << targetPath.toStdString() << endl;
|
|
Chris@97
|
358 return false;
|
|
Chris@97
|
359 }
|
|
Chris@97
|
360
|
|
Chris@97
|
361 return true;
|
|
Chris@97
|
362 }
|
|
Chris@97
|
363
|
|
Chris@67
|
364 map<QString, int>
|
|
Chris@70
|
365 getLibraryPluginVersions(QString libraryFilePath)
|
|
Chris@67
|
366 {
|
|
Chris@67
|
367 static QMutex mutex;
|
|
Chris@67
|
368 static QString tempFileName;
|
|
Chris@67
|
369 static TempFileDeleter deleter;
|
|
Chris@67
|
370 static bool initHappened = false, initSucceeded = false;
|
|
Chris@67
|
371
|
|
Chris@67
|
372 QMutexLocker locker (&mutex);
|
|
Chris@67
|
373
|
|
Chris@67
|
374 if (!initHappened) {
|
|
Chris@67
|
375 initHappened = true;
|
|
Chris@67
|
376
|
|
Chris@67
|
377 QTemporaryFile tempFile;
|
|
Chris@67
|
378 tempFile.setAutoRemove(false);
|
|
Chris@67
|
379 if (!tempFile.open()) {
|
|
Chris@67
|
380 SVCERR << "ERROR: Failed to open a temporary file" << endl;
|
|
Chris@67
|
381 return {};
|
|
Chris@67
|
382 }
|
|
Chris@67
|
383
|
|
Chris@67
|
384 // We can't make the QTemporaryFile static, as it will hold
|
|
Chris@67
|
385 // the file open and that prevents us from executing it. Hence
|
|
Chris@67
|
386 // the separate deleter.
|
|
Chris@67
|
387
|
|
Chris@67
|
388 tempFileName = tempFile.fileName();
|
|
Chris@67
|
389 deleter.tempFile = tempFileName;
|
|
Chris@67
|
390
|
|
Chris@67
|
391 #ifdef Q_OS_WIN32
|
|
Chris@67
|
392 QString helperPath = ":out/get-version.exe";
|
|
Chris@67
|
393 #else
|
|
Chris@67
|
394 QString helperPath = ":out/get-version";
|
|
Chris@97
|
395 #endif
|
|
Chris@97
|
396
|
|
Chris@97
|
397 tempFile.close();
|
|
Chris@97
|
398 if (!unbundleFile(helperPath, tempFileName, true)) {
|
|
Chris@97
|
399 SVCERR << "ERROR: Failed to unbundle helper code" << endl;
|
|
Chris@67
|
400 return {};
|
|
Chris@67
|
401 }
|
|
Chris@67
|
402
|
|
Chris@67
|
403 initSucceeded = true;
|
|
Chris@67
|
404 }
|
|
Chris@67
|
405
|
|
Chris@67
|
406 if (!initSucceeded) {
|
|
Chris@67
|
407 return {};
|
|
Chris@67
|
408 }
|
|
Chris@67
|
409
|
|
Chris@67
|
410 QProcess process;
|
|
Chris@67
|
411 process.start(tempFileName, { libraryFilePath });
|
|
Chris@67
|
412
|
|
Chris@67
|
413 if (!process.waitForStarted()) {
|
|
Chris@67
|
414 QProcess::ProcessError err = process.error();
|
|
Chris@67
|
415 if (err == QProcess::FailedToStart) {
|
|
Chris@67
|
416 SVCERR << "Unable to start helper process " << tempFileName << endl;
|
|
Chris@67
|
417 } else if (err == QProcess::Crashed) {
|
|
Chris@67
|
418 SVCERR << "Helper process " << tempFileName
|
|
Chris@67
|
419 << " crashed on startup" << endl;
|
|
Chris@67
|
420 } else {
|
|
Chris@67
|
421 SVCERR << "Helper process " << tempFileName
|
|
Chris@67
|
422 << " failed on startup with error code " << err << endl;
|
|
Chris@67
|
423 }
|
|
Chris@67
|
424 return {};
|
|
Chris@67
|
425 }
|
|
Chris@67
|
426 process.waitForFinished();
|
|
Chris@67
|
427
|
|
Chris@67
|
428 QByteArray stdOut = process.readAllStandardOutput();
|
|
Chris@67
|
429 QByteArray stdErr = process.readAllStandardError();
|
|
Chris@67
|
430
|
|
Chris@67
|
431 QString errStr = QString::fromUtf8(stdErr);
|
|
Chris@67
|
432 if (!errStr.isEmpty()) {
|
|
Chris@67
|
433 SVCERR << "Note: Helper process stderr follows:" << endl;
|
|
Chris@67
|
434 SVCERR << errStr << endl;
|
|
Chris@67
|
435 SVCERR << "Note: Helper process stderr ends" << endl;
|
|
Chris@67
|
436 }
|
|
Chris@67
|
437
|
|
Chris@67
|
438 QStringList lines = QString::fromUtf8(stdOut).split
|
|
Chris@67
|
439 (QRegExp("[\\r\\n]+"), QString::SkipEmptyParts);
|
|
Chris@67
|
440 map<QString, int> versions;
|
|
Chris@67
|
441 for (QString line: lines) {
|
|
Chris@67
|
442 QStringList parts = line.split(":");
|
|
Chris@67
|
443 if (parts.size() != 2) {
|
|
Chris@67
|
444 SVCERR << "Unparseable output line: " << line << endl;
|
|
Chris@67
|
445 continue;
|
|
Chris@67
|
446 }
|
|
Chris@67
|
447 bool ok = false;
|
|
Chris@67
|
448 int version = parts[1].toInt(&ok);
|
|
Chris@67
|
449 if (!ok) {
|
|
Chris@67
|
450 SVCERR << "Unparseable version number in line: " << line << endl;
|
|
Chris@67
|
451 continue;
|
|
Chris@67
|
452 }
|
|
Chris@67
|
453 versions[parts[0]] = version;
|
|
Chris@67
|
454 }
|
|
Chris@67
|
455
|
|
Chris@67
|
456 return versions;
|
|
Chris@67
|
457 }
|
|
Chris@67
|
458
|
|
Chris@97
|
459 map<QString, int>
|
|
Chris@97
|
460 getBundledLibraryPluginVersions(QString libraryFileName)
|
|
Chris@97
|
461 {
|
|
Chris@97
|
462 QString tempFileName;
|
|
Chris@97
|
463 TempFileDeleter deleter;
|
|
Chris@97
|
464
|
|
Chris@97
|
465 {
|
|
Chris@97
|
466 QTemporaryFile tempFile;
|
|
Chris@97
|
467 tempFile.setAutoRemove(false);
|
|
Chris@97
|
468 if (!tempFile.open()) {
|
|
Chris@97
|
469 SVCERR << "ERROR: Failed to open a temporary file" << endl;
|
|
Chris@97
|
470 return {};
|
|
Chris@97
|
471 }
|
|
Chris@97
|
472
|
|
Chris@97
|
473 // We can't use QTemporaryFile's auto-remove, as it will hold
|
|
Chris@97
|
474 // the file open and that prevents us from executing it. Hence
|
|
Chris@97
|
475 // the separate deleter.
|
|
Chris@97
|
476
|
|
Chris@97
|
477 tempFileName = tempFile.fileName();
|
|
Chris@97
|
478 deleter.tempFile = tempFileName;
|
|
Chris@97
|
479 tempFile.close();
|
|
Chris@97
|
480 }
|
|
Chris@97
|
481
|
|
Chris@97
|
482 if (!unbundleFile(":out/" + libraryFileName, tempFileName, true)) {
|
|
Chris@97
|
483 return {};
|
|
Chris@97
|
484 }
|
|
Chris@97
|
485
|
|
Chris@97
|
486 return getLibraryPluginVersions(tempFileName);
|
|
Chris@97
|
487 }
|
|
Chris@97
|
488
|
|
Chris@67
|
489 bool isLibraryNewer(map<QString, int> a, map<QString, int> b)
|
|
Chris@67
|
490 {
|
|
Chris@67
|
491 // a and b are maps from plugin id to plugin version for libraries
|
|
Chris@67
|
492 // A and B. (There is no overarching library version number.) We
|
|
Chris@67
|
493 // deem library A to be newer than library B if:
|
|
Chris@67
|
494 //
|
|
Chris@67
|
495 // 1. A contains a plugin id that is also in B, whose version in
|
|
Chris@67
|
496 // A is newer than that in B, or
|
|
Chris@67
|
497 //
|
|
Chris@67
|
498 // 2. B is not newer than A according to rule 1, and neither A or
|
|
Chris@67
|
499 // B is empty, and A contains a plugin id that is not in B, and B
|
|
Chris@67
|
500 // does not contain any plugin id that is not in A
|
|
Chris@67
|
501 //
|
|
Chris@67
|
502 // (The not-empty part of rule 2 is just to avoid false positives
|
|
Chris@67
|
503 // when a library or its metadata could not be read at all.)
|
|
Chris@67
|
504
|
|
Chris@67
|
505 auto containsANewerPlugin = [](const map<QString, int> &m1,
|
|
Chris@67
|
506 const map<QString, int> &m2) {
|
|
Chris@67
|
507 for (auto p: m1) {
|
|
Chris@67
|
508 if (m2.find(p.first) != m2.end() &&
|
|
Chris@67
|
509 p.second > m2.at(p.first)) {
|
|
Chris@67
|
510 return true;
|
|
Chris@67
|
511 }
|
|
Chris@67
|
512 }
|
|
Chris@67
|
513 return false;
|
|
Chris@67
|
514 };
|
|
Chris@67
|
515
|
|
Chris@67
|
516 auto containsANovelPlugin = [](const map<QString, int> &m1,
|
|
Chris@67
|
517 const map<QString, int> &m2) {
|
|
Chris@67
|
518 for (auto p: m1) {
|
|
Chris@67
|
519 if (m2.find(p.first) == m2.end()) {
|
|
Chris@67
|
520 return true;
|
|
Chris@67
|
521 }
|
|
Chris@67
|
522 }
|
|
Chris@67
|
523 return false;
|
|
Chris@67
|
524 };
|
|
Chris@67
|
525
|
|
Chris@67
|
526 if (containsANewerPlugin(a, b)) {
|
|
Chris@67
|
527 return true;
|
|
Chris@67
|
528 }
|
|
Chris@67
|
529
|
|
Chris@67
|
530 if (!containsANewerPlugin(b, a) &&
|
|
Chris@67
|
531 !a.empty() &&
|
|
Chris@67
|
532 !b.empty() &&
|
|
Chris@67
|
533 containsANovelPlugin(a, b) &&
|
|
Chris@67
|
534 !containsANovelPlugin(b, a)) {
|
|
Chris@67
|
535 return true;
|
|
Chris@67
|
536 }
|
|
Chris@67
|
537
|
|
Chris@67
|
538 return false;
|
|
Chris@67
|
539 }
|
|
Chris@67
|
540
|
|
Chris@67
|
541 QString
|
|
Chris@67
|
542 versionsString(const map<QString, int> &vv)
|
|
Chris@67
|
543 {
|
|
Chris@67
|
544 QStringList pv;
|
|
Chris@67
|
545 for (auto v: vv) {
|
|
Chris@67
|
546 pv.push_back(QString("%1:%2").arg(v.first).arg(v.second));
|
|
Chris@67
|
547 }
|
|
Chris@67
|
548 return "{ " + pv.join(", ") + " }";
|
|
Chris@67
|
549 }
|
|
Chris@67
|
550
|
|
Chris@75
|
551 enum class RelativeStatus {
|
|
Chris@75
|
552 New,
|
|
Chris@75
|
553 Same,
|
|
Chris@75
|
554 Upgrade,
|
|
Chris@75
|
555 Downgrade,
|
|
Chris@75
|
556 TargetNotLoadable
|
|
Chris@75
|
557 };
|
|
Chris@75
|
558
|
|
Chris@75
|
559 QString
|
|
Chris@75
|
560 relativeStatusLabel(RelativeStatus status) {
|
|
Chris@75
|
561 switch (status) {
|
|
Chris@76
|
562 case RelativeStatus::New: return QObject::tr("Not yet installed");
|
|
Chris@76
|
563 case RelativeStatus::Same: return QObject::tr("Already installed");
|
|
Chris@76
|
564 case RelativeStatus::Upgrade: return QObject::tr("Update");
|
|
Chris@76
|
565 case RelativeStatus::Downgrade: return QObject::tr("Newer version installed");
|
|
Chris@84
|
566 case RelativeStatus::TargetNotLoadable: return QObject::tr("Installed version not working");
|
|
Chris@79
|
567 default: return {};
|
|
Chris@75
|
568 }
|
|
Chris@75
|
569 }
|
|
Chris@75
|
570
|
|
Chris@75
|
571 RelativeStatus
|
|
Chris@75
|
572 getRelativeStatus(LibraryInfo info, QString targetDir)
|
|
Chris@75
|
573 {
|
|
Chris@75
|
574 QString destination = targetDir + "/" + info.fileName;
|
|
Chris@75
|
575
|
|
Chris@75
|
576 SVCERR << "\ngetRelativeStatus: " << info.fileName << ":\n";
|
|
Chris@75
|
577
|
|
Chris@97
|
578 if (!QFileInfo(destination).exists()) {
|
|
Chris@97
|
579 SVCERR << " - relative status: " << relativeStatusLabel(RelativeStatus::New) << endl;
|
|
Chris@97
|
580 return RelativeStatus::New;
|
|
Chris@97
|
581 }
|
|
Chris@75
|
582
|
|
Chris@97
|
583 RelativeStatus status = RelativeStatus::Same;
|
|
Chris@75
|
584
|
|
Chris@97
|
585 auto packaged = getBundledLibraryPluginVersions(info.fileName);
|
|
Chris@97
|
586 auto installed = getLibraryPluginVersions(destination);
|
|
Chris@75
|
587
|
|
Chris@97
|
588 SVCERR << " * installed: " << versionsString(installed)
|
|
Chris@97
|
589 << "\n * packaged: " << versionsString(packaged)
|
|
Chris@97
|
590 << endl;
|
|
Chris@75
|
591
|
|
Chris@97
|
592 if (installed.empty()) {
|
|
Chris@97
|
593 status = RelativeStatus::TargetNotLoadable;
|
|
Chris@97
|
594 }
|
|
Chris@75
|
595
|
|
Chris@97
|
596 if (isLibraryNewer(installed, packaged)) {
|
|
Chris@97
|
597 status = RelativeStatus::Downgrade;
|
|
Chris@97
|
598 }
|
|
Chris@75
|
599
|
|
Chris@97
|
600 if (isLibraryNewer(packaged, installed)) {
|
|
Chris@97
|
601 status = RelativeStatus::Upgrade;
|
|
Chris@75
|
602 }
|
|
Chris@75
|
603
|
|
Chris@75
|
604 SVCERR << " - relative status: " << relativeStatusLabel(status) << endl;
|
|
Chris@75
|
605
|
|
Chris@75
|
606 return status;
|
|
Chris@75
|
607 }
|
|
Chris@75
|
608
|
|
Chris@86
|
609 bool
|
|
Chris@86
|
610 backup(QString filePath, QString backupDir)
|
|
Chris@86
|
611 {
|
|
Chris@86
|
612 QFileInfo file(filePath);
|
|
Chris@86
|
613
|
|
Chris@86
|
614 if (!file.exists()) {
|
|
Chris@86
|
615 return true;
|
|
Chris@86
|
616 }
|
|
Chris@86
|
617
|
|
Chris@86
|
618 if (!QDir(backupDir).exists()) {
|
|
Chris@86
|
619 QDir().mkpath(backupDir);
|
|
Chris@86
|
620 }
|
|
Chris@86
|
621
|
|
Chris@86
|
622 QString backup = backupDir + "/" + file.fileName() + ".bak";
|
|
Chris@86
|
623 SVCERR << "Note: existing file " << filePath
|
|
Chris@86
|
624 << " found, backing up to " << backup << endl;
|
|
Chris@86
|
625 if (!QFile(filePath).rename(backup)) {
|
|
Chris@86
|
626 SVCERR << "Failed to move " << filePath.toStdString()
|
|
Chris@86
|
627 << " to backup " << backup.toStdString() << endl;
|
|
Chris@86
|
628 return false;
|
|
Chris@86
|
629 }
|
|
Chris@86
|
630
|
|
Chris@86
|
631 return true;
|
|
Chris@86
|
632 }
|
|
Chris@86
|
633
|
|
Chris@81
|
634 QString
|
|
Chris@75
|
635 installLibrary(LibraryInfo info, QString targetDir)
|
|
Chris@42
|
636 {
|
|
Chris@75
|
637 QString library = info.fileName;
|
|
Chris@52
|
638 QString source = ":out";
|
|
Chris@75
|
639 QString destination = targetDir + "/" + library;
|
|
Chris@94
|
640
|
|
Chris@94
|
641 static QString backupDirName;
|
|
Chris@94
|
642 if (backupDirName == "") {
|
|
Chris@94
|
643 // Static so as to be created once - don't go creating a
|
|
Chris@94
|
644 // second directory if the clock ticks over by one second
|
|
Chris@94
|
645 // between library installs
|
|
Chris@94
|
646 backupDirName =
|
|
Chris@94
|
647 QString("saved-%1").arg(QDateTime::currentDateTime().toString
|
|
Chris@94
|
648 ("yyyyMMdd-hhmmss"));
|
|
Chris@94
|
649 }
|
|
Chris@94
|
650 QString backupDir = targetDir + "/" + backupDirName;
|
|
Chris@67
|
651
|
|
Chris@86
|
652 if (!QDir(targetDir).exists()) {
|
|
Chris@86
|
653 QDir().mkpath(targetDir);
|
|
Chris@67
|
654 }
|
|
Chris@82
|
655
|
|
Chris@86
|
656 if (!backup(destination, backupDir)) {
|
|
Chris@86
|
657 return QObject::tr("Failed to move aside existing library");
|
|
Chris@86
|
658 }
|
|
Chris@97
|
659
|
|
Chris@97
|
660 if (!unbundleFile(source + "/" + library, destination, true)) {
|
|
Chris@97
|
661 return QObject::tr("Failed to copy library file to target directory");
|
|
Chris@97
|
662 }
|
|
Chris@52
|
663
|
|
Chris@52
|
664 QString base = QFileInfo(library).baseName();
|
|
Chris@52
|
665 QDir dir(source);
|
|
Chris@52
|
666 auto entries = dir.entryList({ base + "*" });
|
|
Chris@52
|
667 for (auto e: entries) {
|
|
Chris@52
|
668 if (e == library) continue;
|
|
Chris@75
|
669 QString destination = targetDir + "/" + e;
|
|
Chris@86
|
670 if (!backup(destination, backupDir)) {
|
|
Chris@86
|
671 continue;
|
|
Chris@86
|
672 }
|
|
Chris@97
|
673 if (!unbundleFile(source + "/" + e, destination, false)) {
|
|
Chris@68
|
674 continue;
|
|
Chris@52
|
675 }
|
|
Chris@52
|
676 }
|
|
Chris@81
|
677
|
|
Chris@81
|
678 return {};
|
|
Chris@42
|
679 }
|
|
Chris@42
|
680
|
|
Chris@75
|
681 vector<LibraryInfo>
|
|
Chris@75
|
682 getUserApprovedPluginLibraries(vector<LibraryInfo> libraries,
|
|
Chris@75
|
683 QString targetDir)
|
|
Chris@42
|
684 {
|
|
Chris@42
|
685 QDialog dialog;
|
|
Chris@46
|
686
|
|
Chris@84
|
687 int fontHeight = QFontMetrics(dialog.font()).height();
|
|
Chris@84
|
688 int dpratio = dialog.devicePixelRatio();
|
|
Chris@84
|
689
|
|
Chris@46
|
690 auto mainLayout = new QGridLayout;
|
|
Chris@47
|
691 mainLayout->setSpacing(0);
|
|
Chris@46
|
692 dialog.setLayout(mainLayout);
|
|
Chris@46
|
693
|
|
Chris@46
|
694 int mainRow = 0;
|
|
Chris@46
|
695
|
|
Chris@74
|
696 auto selectionFrame = new QWidget;
|
|
Chris@74
|
697 mainLayout->addWidget(selectionFrame, mainRow, 0);
|
|
Chris@47
|
698 ++mainRow;
|
|
Chris@46
|
699
|
|
Chris@46
|
700 auto selectionLayout = new QGridLayout;
|
|
Chris@84
|
701 selectionLayout->setContentsMargins(0, 0, 0, 0);
|
|
Chris@84
|
702 selectionLayout->setSpacing(fontHeight / 6);
|
|
Chris@46
|
703 selectionFrame->setLayout(selectionLayout);
|
|
Chris@84
|
704
|
|
Chris@46
|
705 int selectionRow = 0;
|
|
Chris@84
|
706 int checkColumn = 0;
|
|
Chris@84
|
707 int titleColumn = 1;
|
|
Chris@84
|
708 int statusColumn = 2;
|
|
Chris@85
|
709 int infoColumn = 4; // column 3 is a small sliver of spacing
|
|
Chris@79
|
710
|
|
Chris@79
|
711 selectionLayout->addWidget
|
|
Chris@79
|
712 (new QLabel(QObject::tr("<b>Vamp Plugin Pack</b> v%1")
|
|
Chris@79
|
713 .arg(PACK_VERSION)),
|
|
Chris@84
|
714 selectionRow, titleColumn, 1, 3);
|
|
Chris@79
|
715 ++selectionRow;
|
|
Chris@79
|
716
|
|
Chris@79
|
717 selectionLayout->addWidget
|
|
Chris@79
|
718 (new QLabel(QObject::tr("Select the plugin libraries to install:")),
|
|
Chris@84
|
719 selectionRow, titleColumn, 1, 3);
|
|
Chris@79
|
720 ++selectionRow;
|
|
Chris@74
|
721
|
|
Chris@74
|
722 auto checkAll = new QCheckBox;
|
|
Chris@74
|
723 checkAll->setChecked(true);
|
|
Chris@84
|
724 selectionLayout->addWidget
|
|
Chris@84
|
725 (checkAll, selectionRow, checkColumn, Qt::AlignHCenter);
|
|
Chris@74
|
726 ++selectionRow;
|
|
Chris@74
|
727
|
|
Chris@85
|
728 auto checkArrow = new QLabel(
|
|
Chris@85
|
729 #ifdef Q_OS_MAC
|
|
Chris@85
|
730 " ▼"
|
|
Chris@85
|
731 #else
|
|
Chris@85
|
732 "▼"
|
|
Chris@85
|
733 #endif
|
|
Chris@85
|
734 );
|
|
Chris@74
|
735 checkArrow->setTextFormat(Qt::RichText);
|
|
Chris@84
|
736 selectionLayout->addWidget
|
|
Chris@84
|
737 (checkArrow, selectionRow, checkColumn, Qt::AlignHCenter);
|
|
Chris@74
|
738 ++selectionRow;
|
|
Chris@42
|
739
|
|
Chris@67
|
740 map<QString, QCheckBox *> checkBoxMap; // filename -> checkbox
|
|
Chris@67
|
741 map<QString, LibraryInfo> libFileInfo; // filename -> info
|
|
Chris@76
|
742 map<QString, RelativeStatus> statuses; // filename -> status
|
|
Chris@43
|
743
|
|
Chris@67
|
744 map<QString, LibraryInfo, function<bool (QString, QString)>>
|
|
Chris@49
|
745 orderedInfo
|
|
Chris@49
|
746 ([](QString k1, QString k2) {
|
|
Chris@49
|
747 return k1.localeAwareCompare(k2) < 0;
|
|
Chris@49
|
748 });
|
|
Chris@43
|
749 for (auto info: libraries) {
|
|
Chris@43
|
750 orderedInfo[info.title] = info;
|
|
Chris@43
|
751 }
|
|
Chris@53
|
752
|
|
Chris@77
|
753 QPixmap infoMap(fontHeight * dpratio, fontHeight * dpratio);
|
|
Chris@77
|
754 QPixmap moreMap(fontHeight * dpratio * 2, fontHeight * dpratio * 2);
|
|
Chris@74
|
755 infoMap.fill(Qt::transparent);
|
|
Chris@74
|
756 moreMap.fill(Qt::transparent);
|
|
Chris@74
|
757 QSvgRenderer renderer(QString(":icons/scalable/info.svg"));
|
|
Chris@74
|
758 QPainter painter;
|
|
Chris@74
|
759 painter.begin(&infoMap);
|
|
Chris@74
|
760 renderer.render(&painter);
|
|
Chris@74
|
761 painter.end();
|
|
Chris@74
|
762 painter.begin(&moreMap);
|
|
Chris@74
|
763 renderer.render(&painter);
|
|
Chris@74
|
764 painter.end();
|
|
Chris@74
|
765
|
|
Chris@76
|
766 auto shouldCheck = [](RelativeStatus status) {
|
|
Chris@76
|
767 return (status == RelativeStatus::New ||
|
|
Chris@76
|
768 status == RelativeStatus::Upgrade ||
|
|
Chris@76
|
769 status == RelativeStatus::TargetNotLoadable);
|
|
Chris@76
|
770 };
|
|
Chris@76
|
771
|
|
Chris@43
|
772 for (auto ip: orderedInfo) {
|
|
Chris@46
|
773
|
|
Chris@46
|
774 auto cb = new QCheckBox;
|
|
Chris@84
|
775 selectionLayout->addWidget
|
|
Chris@84
|
776 (cb, selectionRow, checkColumn, Qt::AlignHCenter);
|
|
Chris@46
|
777
|
|
Chris@43
|
778 LibraryInfo info = ip.second;
|
|
Chris@72
|
779
|
|
Chris@76
|
780 auto shortLabel = new QLabel(info.title);
|
|
Chris@84
|
781 selectionLayout->addWidget(shortLabel, selectionRow, titleColumn);
|
|
Chris@76
|
782
|
|
Chris@75
|
783 RelativeStatus relativeStatus = getRelativeStatus(info, targetDir);
|
|
Chris@76
|
784 auto statusLabel = new QLabel(relativeStatusLabel(relativeStatus));
|
|
Chris@84
|
785 selectionLayout->addWidget(statusLabel, selectionRow, statusColumn);
|
|
Chris@76
|
786 cb->setChecked(shouldCheck(relativeStatus));
|
|
Chris@75
|
787
|
|
Chris@79
|
788 auto infoButton = new QToolButton;
|
|
Chris@79
|
789 infoButton->setAutoRaise(true);
|
|
Chris@79
|
790 infoButton->setIcon(infoMap);
|
|
Chris@79
|
791 infoButton->setIconSize(QSize(fontHeight, fontHeight));
|
|
Chris@77
|
792
|
|
Chris@77
|
793 #ifdef Q_OS_MAC
|
|
Chris@79
|
794 infoButton->setFixedSize(QSize(int(fontHeight * 1.2),
|
|
Chris@77
|
795 int(fontHeight * 1.2)));
|
|
Chris@79
|
796 infoButton->setStyleSheet("QToolButton { border: none; }");
|
|
Chris@77
|
797 #endif
|
|
Chris@77
|
798
|
|
Chris@84
|
799 selectionLayout->addWidget(infoButton, selectionRow, infoColumn);
|
|
Chris@46
|
800
|
|
Chris@46
|
801 ++selectionRow;
|
|
Chris@46
|
802
|
|
Chris@77
|
803 QString moreTitleText = QObject::tr("<b>%1</b><br><i>%2</i>")
|
|
Chris@72
|
804 .arg(info.title)
|
|
Chris@77
|
805 .arg(info.maker);
|
|
Chris@77
|
806
|
|
Chris@79
|
807 QString moreInfoText = info.description;
|
|
Chris@79
|
808
|
|
Chris@79
|
809 if (info.page != "") {
|
|
Chris@79
|
810 moreInfoText += QObject::tr("<br><a href=\"%1\">%2</a>")
|
|
Chris@79
|
811 .arg(info.page)
|
|
Chris@79
|
812 .arg(info.page);
|
|
Chris@79
|
813 }
|
|
Chris@79
|
814
|
|
Chris@79
|
815 moreInfoText += QObject::tr("<br><br>Library contains:<ul>");
|
|
Chris@73
|
816
|
|
Chris@73
|
817 int n = 0;
|
|
Chris@73
|
818 bool closed = false;
|
|
Chris@73
|
819 for (auto title: info.pluginTitles) {
|
|
Chris@73
|
820 if (n == 10 && info.pluginTitles.size() > 15) {
|
|
Chris@77
|
821 moreInfoText += QObject::tr("</ul>");
|
|
Chris@77
|
822 moreInfoText += QObject::tr("... and %n other plugins.<br><br>",
|
|
Chris@77
|
823 "",
|
|
Chris@77
|
824 info.pluginTitles.size() - n);
|
|
Chris@73
|
825 closed = true;
|
|
Chris@73
|
826 break;
|
|
Chris@73
|
827 }
|
|
Chris@77
|
828 moreInfoText += QObject::tr("<li>%1</li>").arg(title);
|
|
Chris@73
|
829 ++n;
|
|
Chris@73
|
830 }
|
|
Chris@73
|
831
|
|
Chris@73
|
832 if (!closed) {
|
|
Chris@77
|
833 moreInfoText += QObject::tr("</ul>");
|
|
Chris@73
|
834 }
|
|
Chris@74
|
835
|
|
Chris@74
|
836 if (info.licence != "") {
|
|
Chris@77
|
837 moreInfoText += QObject::tr("Provided under the %1.<br>")
|
|
Chris@77
|
838 .arg(info.licence);
|
|
Chris@74
|
839 }
|
|
Chris@72
|
840
|
|
Chris@79
|
841 QObject::connect(infoButton, &QAbstractButton::clicked,
|
|
Chris@72
|
842 [=]() {
|
|
Chris@74
|
843 QMessageBox mbox;
|
|
Chris@74
|
844 mbox.setIconPixmap(moreMap);
|
|
Chris@74
|
845 mbox.setWindowTitle(QObject::tr("Library contents"));
|
|
Chris@77
|
846 mbox.setText(moreTitleText);
|
|
Chris@77
|
847 mbox.setInformativeText(moreInfoText);
|
|
Chris@74
|
848 mbox.exec();
|
|
Chris@72
|
849 });
|
|
Chris@72
|
850
|
|
Chris@43
|
851 checkBoxMap[info.fileName] = cb;
|
|
Chris@67
|
852 libFileInfo[info.fileName] = info;
|
|
Chris@76
|
853 statuses[info.fileName] = relativeStatus;
|
|
Chris@42
|
854 }
|
|
Chris@42
|
855
|
|
Chris@79
|
856 selectionLayout->addItem(new QSpacerItem(1, (fontHeight*2) / 3),
|
|
Chris@79
|
857 selectionRow, 0);
|
|
Chris@79
|
858 ++selectionRow;
|
|
Chris@79
|
859
|
|
Chris@79
|
860 selectionLayout->addWidget
|
|
Chris@79
|
861 (new QLabel(QObject::tr("Installation will be to: %1").arg(targetDir)),
|
|
Chris@84
|
862 selectionRow, titleColumn, 1, 3);
|
|
Chris@79
|
863 ++selectionRow;
|
|
Chris@79
|
864
|
|
Chris@47
|
865 QObject::connect(checkAll, &QCheckBox::toggled,
|
|
Chris@72
|
866 [=](bool toCheck) {
|
|
Chris@47
|
867 for (auto p: checkBoxMap) {
|
|
Chris@47
|
868 p.second->setChecked(toCheck);
|
|
Chris@47
|
869 }
|
|
Chris@47
|
870 });
|
|
Chris@79
|
871
|
|
Chris@79
|
872 mainLayout->addItem(new QSpacerItem(1, fontHeight), mainRow, 0);
|
|
Chris@79
|
873 ++mainRow;
|
|
Chris@79
|
874
|
|
Chris@42
|
875 auto bb = new QDialogButtonBox(QDialogButtonBox::Ok |
|
|
Chris@76
|
876 QDialogButtonBox::Cancel |
|
|
Chris@76
|
877 QDialogButtonBox::Reset);
|
|
Chris@79
|
878 bb->button(QDialogButtonBox::Ok)->setText(QObject::tr("Install"));
|
|
Chris@74
|
879 mainLayout->addWidget(bb, mainRow, 0);
|
|
Chris@46
|
880 ++mainRow;
|
|
Chris@47
|
881
|
|
Chris@74
|
882 mainLayout->setRowStretch(0, 10);
|
|
Chris@74
|
883 mainLayout->setColumnStretch(0, 10);
|
|
Chris@74
|
884 selectionLayout->setColumnMinimumWidth(0, 50);
|
|
Chris@85
|
885 #ifdef Q_OS_MAC
|
|
Chris@85
|
886 selectionLayout->setColumnMinimumWidth(3, 10);
|
|
Chris@85
|
887 selectionLayout->setColumnMinimumWidth(5, 12);
|
|
Chris@85
|
888 #endif
|
|
Chris@74
|
889 selectionLayout->setColumnStretch(1, 10);
|
|
Chris@47
|
890
|
|
Chris@76
|
891 QObject::connect
|
|
Chris@76
|
892 (bb, &QDialogButtonBox::clicked,
|
|
Chris@76
|
893 [&](QAbstractButton *button) {
|
|
Chris@76
|
894
|
|
Chris@76
|
895 auto role = bb->buttonRole(button);
|
|
Chris@76
|
896
|
|
Chris@76
|
897 switch (role) {
|
|
Chris@76
|
898
|
|
Chris@76
|
899 case QDialogButtonBox::AcceptRole: {
|
|
Chris@76
|
900 bool downgrade = false;
|
|
Chris@76
|
901 for (const auto &p: checkBoxMap) {
|
|
Chris@76
|
902 if (p.second->isChecked() &&
|
|
Chris@76
|
903 statuses.at(p.first) == RelativeStatus::Downgrade) {
|
|
Chris@76
|
904 downgrade = true;
|
|
Chris@76
|
905 break;
|
|
Chris@76
|
906 }
|
|
Chris@76
|
907 }
|
|
Chris@76
|
908 if (downgrade) {
|
|
Chris@76
|
909 if (QMessageBox::warning
|
|
Chris@76
|
910 (bb, QObject::tr("Downgrade?"),
|
|
Chris@76
|
911 QObject::tr("You have asked to downgrade one or more plugin libraries that are already installed.<br><br>Are you sure?"),
|
|
Chris@76
|
912 QMessageBox::Ok | QMessageBox::Cancel,
|
|
Chris@76
|
913 QMessageBox::Cancel) == QMessageBox::Ok) {
|
|
Chris@76
|
914 dialog.accept();
|
|
Chris@76
|
915 }
|
|
Chris@76
|
916 } else {
|
|
Chris@76
|
917 dialog.accept();
|
|
Chris@76
|
918 }
|
|
Chris@76
|
919 break;
|
|
Chris@76
|
920 }
|
|
Chris@76
|
921
|
|
Chris@76
|
922 case QDialogButtonBox::RejectRole:
|
|
Chris@76
|
923 dialog.reject();
|
|
Chris@76
|
924 break;
|
|
Chris@76
|
925
|
|
Chris@76
|
926 case QDialogButtonBox::ResetRole:
|
|
Chris@76
|
927 for (const auto &p: checkBoxMap) {
|
|
Chris@76
|
928 p.second->setChecked(shouldCheck(statuses.at(p.first)));
|
|
Chris@76
|
929 }
|
|
Chris@76
|
930 break;
|
|
Chris@76
|
931
|
|
Chris@76
|
932 default:
|
|
Chris@76
|
933 SVCERR << "WARNING: Unexpected role " << role << endl;
|
|
Chris@76
|
934 }
|
|
Chris@76
|
935 });
|
|
Chris@98
|
936
|
|
Chris@98
|
937 if (QString(PACK_VERSION).contains("-pre") ||
|
|
Chris@98
|
938 QString(PACK_VERSION).contains("-alpha") ||
|
|
Chris@98
|
939 QString(PACK_VERSION).contains("-beta")) {
|
|
Chris@98
|
940 QTimer::singleShot
|
|
Chris@98
|
941 (500, [&]() {
|
|
Chris@98
|
942 QString url = "https://code.soundsoftware.ac.uk/projects/vamp-plugin-pack";
|
|
Chris@98
|
943 QMessageBox::information
|
|
Chris@98
|
944 (&dialog, QObject::tr("Test release"),
|
|
Chris@98
|
945 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
|
946 });
|
|
Chris@98
|
947 }
|
|
Chris@98
|
948
|
|
Chris@70
|
949 if (dialog.exec() != QDialog::Accepted) {
|
|
Chris@51
|
950 SVCERR << "rejected" << endl;
|
|
Chris@70
|
951 return {};
|
|
Chris@42
|
952 }
|
|
Chris@42
|
953
|
|
Chris@75
|
954 vector<LibraryInfo> approved;
|
|
Chris@42
|
955 for (const auto &p: checkBoxMap) {
|
|
Chris@42
|
956 if (p.second->isChecked()) {
|
|
Chris@75
|
957 approved.push_back(libFileInfo[p.first]);
|
|
Chris@33
|
958 }
|
|
Chris@42
|
959 }
|
|
Chris@42
|
960
|
|
Chris@42
|
961 return approved;
|
|
Chris@42
|
962 }
|
|
Chris@42
|
963
|
|
Chris@42
|
964 int main(int argc, char **argv)
|
|
Chris@42
|
965 {
|
|
Chris@93
|
966 if (argc == 2 && (QString(argv[1]) == "--version" ||
|
|
Chris@93
|
967 QString(argv[1]) == "-v")) {
|
|
Chris@95
|
968 cerr << PACK_VERSION << std::endl; // std:: needed here for MSVC for some reason
|
|
Chris@93
|
969 exit(0);
|
|
Chris@93
|
970 }
|
|
Chris@93
|
971
|
|
Chris@42
|
972 QApplication app(argc, argv);
|
|
Chris@42
|
973
|
|
Chris@51
|
974 QApplication::setOrganizationName("sonic-visualiser");
|
|
Chris@51
|
975 QApplication::setOrganizationDomain("sonicvisualiser.org");
|
|
Chris@51
|
976 QApplication::setApplicationName(QApplication::tr("Vamp Plugin Pack Installer"));
|
|
Chris@51
|
977
|
|
Chris@77
|
978 QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
|
|
Chris@77
|
979
|
|
Chris@51
|
980 #ifdef Q_OS_WIN32
|
|
Chris@51
|
981 QFont font(QApplication::font());
|
|
Chris@51
|
982 QString preferredFamily = "Segoe UI";
|
|
Chris@51
|
983 font.setFamily(preferredFamily);
|
|
Chris@51
|
984 if (QFontInfo(font).family() == preferredFamily) {
|
|
Chris@51
|
985 font.setPointSize(10);
|
|
Chris@51
|
986 QApplication::setFont(font);
|
|
Chris@51
|
987 }
|
|
Chris@77
|
988 #else
|
|
Chris@77
|
989 #ifdef Q_OS_MAC
|
|
Chris@77
|
990 QFont font(QApplication::font());
|
|
Chris@77
|
991 QString preferredFamily = "Lucida Grande";
|
|
Chris@77
|
992 font.setFamily(preferredFamily);
|
|
Chris@77
|
993 if (QFontInfo(font).family() == preferredFamily) {
|
|
Chris@85
|
994 font.setPointSize(12);
|
|
Chris@77
|
995 QApplication::setFont(font);
|
|
Chris@77
|
996 }
|
|
Chris@77
|
997 #endif
|
|
Chris@51
|
998 #endif
|
|
Chris@51
|
999
|
|
Chris@42
|
1000 QString target = getDefaultInstallDirectory();
|
|
Chris@42
|
1001 if (target == "") {
|
|
Chris@42
|
1002 return 1;
|
|
Chris@42
|
1003 }
|
|
Chris@42
|
1004
|
|
Chris@42
|
1005 QStringList libraries = getPluginLibraryList();
|
|
Chris@42
|
1006
|
|
Chris@43
|
1007 auto rdfStore = loadLibrariesRdf();
|
|
Chris@43
|
1008
|
|
Chris@43
|
1009 auto info = getLibraryInfo(*rdfStore, libraries);
|
|
Chris@43
|
1010
|
|
Chris@75
|
1011 vector<LibraryInfo> toInstall =
|
|
Chris@75
|
1012 getUserApprovedPluginLibraries(info, target);
|
|
Chris@52
|
1013
|
|
Chris@83
|
1014 if (toInstall.empty()) { // Cancelled, or nothing selected
|
|
Chris@94
|
1015 SVCERR << "No libraries selected for installation, nothing to do"
|
|
Chris@94
|
1016 << endl;
|
|
Chris@83
|
1017 return 0;
|
|
Chris@83
|
1018 }
|
|
Chris@83
|
1019
|
|
Chris@81
|
1020 QProgressDialog progress(QObject::tr("Installing..."),
|
|
Chris@95
|
1021 QObject::tr("Stop"), 0,
|
|
Chris@95
|
1022 int(toInstall.size()) + 1);
|
|
Chris@81
|
1023 progress.setMinimumDuration(0);
|
|
Chris@81
|
1024
|
|
Chris@81
|
1025 int pval = 0;
|
|
Chris@81
|
1026 bool complete = true;
|
|
Chris@42
|
1027
|
|
Chris@42
|
1028 for (auto lib: toInstall) {
|
|
Chris@81
|
1029 progress.setValue(++pval);
|
|
Chris@81
|
1030 QThread::currentThread()->msleep(40);
|
|
Chris@81
|
1031 app.processEvents();
|
|
Chris@81
|
1032 if (progress.wasCanceled()) {
|
|
Chris@81
|
1033 complete = false;
|
|
Chris@81
|
1034 break;
|
|
Chris@81
|
1035 }
|
|
Chris@81
|
1036 QString error = installLibrary(lib, target);
|
|
Chris@81
|
1037 if (error != "") {
|
|
Chris@81
|
1038 complete = false;
|
|
Chris@81
|
1039 if (QMessageBox::critical
|
|
Chris@81
|
1040 (&progress,
|
|
Chris@81
|
1041 QObject::tr("Install failed"),
|
|
Chris@81
|
1042 QObject::tr("Failed to install library \"%1\": %2")
|
|
Chris@81
|
1043 .arg(lib.title)
|
|
Chris@81
|
1044 .arg(error),
|
|
Chris@81
|
1045 QMessageBox::Abort | QMessageBox::Ignore,
|
|
Chris@81
|
1046 QMessageBox::Ignore) ==
|
|
Chris@81
|
1047 QMessageBox::Abort) {
|
|
Chris@81
|
1048 break;
|
|
Chris@81
|
1049 }
|
|
Chris@81
|
1050 }
|
|
Chris@81
|
1051 }
|
|
Chris@81
|
1052
|
|
Chris@81
|
1053 progress.hide();
|
|
Chris@81
|
1054
|
|
Chris@81
|
1055 if (complete) {
|
|
Chris@81
|
1056 QMessageBox::information
|
|
Chris@81
|
1057 (&progress,
|
|
Chris@81
|
1058 QObject::tr("Complete"),
|
|
Chris@81
|
1059 QObject::tr("Installation completed successfully"),
|
|
Chris@81
|
1060 QMessageBox::Ok,
|
|
Chris@81
|
1061 QMessageBox::Ok);
|
|
Chris@81
|
1062 } else {
|
|
Chris@81
|
1063 QMessageBox::information
|
|
Chris@81
|
1064 (&progress,
|
|
Chris@81
|
1065 QObject::tr("Incomplete"),
|
|
Chris@81
|
1066 QObject::tr("Installation was not complete. Exiting"),
|
|
Chris@81
|
1067 QMessageBox::Ok,
|
|
Chris@81
|
1068 QMessageBox::Ok);
|
|
Chris@33
|
1069 }
|
|
Chris@33
|
1070
|
|
Chris@83
|
1071 return (complete ? 0 : 2);
|
|
Chris@32
|
1072 }
|