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@46
|
39 #include <QScrollArea>
|
Chris@42
|
40 #include <QDialogButtonBox>
|
Chris@46
|
41 #include <QLabel>
|
Chris@51
|
42 #include <QFont>
|
Chris@51
|
43 #include <QFontInfo>
|
Chris@67
|
44 #include <QTemporaryFile>
|
Chris@67
|
45 #include <QMutex>
|
Chris@67
|
46 #include <QMutexLocker>
|
Chris@67
|
47 #include <QProcess>
|
Chris@42
|
48
|
Chris@41
|
49 #include <vamp-hostsdk/PluginHostAdapter.h>
|
Chris@41
|
50
|
Chris@43
|
51 #include <dataquay/BasicStore.h>
|
Chris@43
|
52 #include <dataquay/RDFException.h>
|
Chris@43
|
53
|
Chris@33
|
54 #include <iostream>
|
Chris@58
|
55 #include <memory>
|
Chris@43
|
56 #include <set>
|
Chris@43
|
57
|
Chris@67
|
58 #include <unistd.h>
|
Chris@67
|
59
|
Chris@51
|
60 #include "base/Debug.h"
|
Chris@51
|
61
|
Chris@33
|
62 using namespace std;
|
Chris@43
|
63 using namespace Dataquay;
|
Chris@32
|
64
|
Chris@42
|
65 QString
|
Chris@42
|
66 getDefaultInstallDirectory()
|
Chris@32
|
67 {
|
Chris@41
|
68 auto pathList = Vamp::PluginHostAdapter::getPluginPath();
|
Chris@41
|
69 if (pathList.empty()) {
|
Chris@51
|
70 SVCERR << "Failed to look up Vamp plugin path" << endl;
|
Chris@42
|
71 return QString();
|
Chris@41
|
72 }
|
Chris@41
|
73
|
Chris@42
|
74 auto firstPath = *pathList.begin();
|
Chris@42
|
75 QString target = QString::fromUtf8(firstPath.c_str(), firstPath.size());
|
Chris@42
|
76 return target;
|
Chris@42
|
77 }
|
Chris@42
|
78
|
Chris@42
|
79 QStringList
|
Chris@42
|
80 getPluginLibraryList()
|
Chris@42
|
81 {
|
Chris@33
|
82 QDir dir(":out/");
|
Chris@33
|
83 auto entries = dir.entryList({ "*.so", "*.dll", "*.dylib" });
|
Chris@33
|
84
|
Chris@33
|
85 for (auto e: entries) {
|
Chris@51
|
86 SVCERR << e.toStdString() << endl;
|
Chris@33
|
87 }
|
Chris@33
|
88
|
Chris@42
|
89 return entries;
|
Chris@42
|
90 }
|
Chris@33
|
91
|
Chris@50
|
92 void
|
Chris@50
|
93 loadLibraryRdf(BasicStore &store, QString filename)
|
Chris@50
|
94 {
|
Chris@50
|
95 QFile f(filename);
|
Chris@50
|
96 if (!f.open(QFile::ReadOnly | QFile::Text)) {
|
Chris@51
|
97 SVCERR << "Failed to open RDF resource file "
|
Chris@51
|
98 << filename.toStdString() << endl;
|
Chris@50
|
99 return;
|
Chris@50
|
100 }
|
Chris@50
|
101
|
Chris@50
|
102 QByteArray content = f.readAll();
|
Chris@50
|
103 f.close();
|
Chris@50
|
104
|
Chris@50
|
105 try {
|
Chris@50
|
106 store.importString(QString::fromUtf8(content),
|
Chris@50
|
107 Uri("file:" + filename),
|
Chris@50
|
108 BasicStore::ImportIgnoreDuplicates);
|
Chris@50
|
109 } catch (const RDFException &ex) {
|
Chris@51
|
110 SVCERR << "Failed to import RDF resource file "
|
Chris@51
|
111 << filename.toStdString() << ": " << ex.what() << endl;
|
Chris@50
|
112 }
|
Chris@50
|
113 }
|
Chris@50
|
114
|
Chris@43
|
115 unique_ptr<BasicStore>
|
Chris@43
|
116 loadLibrariesRdf()
|
Chris@43
|
117 {
|
Chris@43
|
118 unique_ptr<BasicStore> store(new BasicStore);
|
Chris@43
|
119
|
Chris@50
|
120 vector<QString> dirs { ":rdf/plugins", ":out" };
|
Chris@43
|
121
|
Chris@50
|
122 for (auto d: dirs) {
|
Chris@50
|
123 for (auto e: QDir(d).entryList({ "*.ttl", "*.n3" })) {
|
Chris@50
|
124 loadLibraryRdf(*store, d + "/" + e);
|
Chris@43
|
125 }
|
Chris@43
|
126 }
|
Chris@43
|
127
|
Chris@43
|
128 return store;
|
Chris@43
|
129 }
|
Chris@43
|
130
|
Chris@43
|
131 struct LibraryInfo {
|
Chris@43
|
132 QString id;
|
Chris@43
|
133 QString fileName;
|
Chris@43
|
134 QString title;
|
Chris@43
|
135 QString maker;
|
Chris@43
|
136 QString description;
|
Chris@47
|
137 QStringList pluginTitles;
|
Chris@67
|
138 map<QString, int> pluginVersions; // id -> version
|
Chris@43
|
139 };
|
Chris@43
|
140
|
Chris@43
|
141 vector<LibraryInfo>
|
Chris@43
|
142 getLibraryInfo(const Store &store, QStringList libraries)
|
Chris@43
|
143 {
|
Chris@43
|
144 /* e.g.
|
Chris@43
|
145
|
Chris@43
|
146 plugbase:library a vamp:PluginLibrary ;
|
Chris@43
|
147 vamp:identifier "qm-vamp-plugins" ;
|
Chris@43
|
148 dc:title "Queen Mary plugin set"
|
Chris@43
|
149 */
|
Chris@43
|
150
|
Chris@43
|
151 Triples tt = store.match(Triple(Node(),
|
Chris@43
|
152 Uri("a"),
|
Chris@43
|
153 store.expand("vamp:PluginLibrary")));
|
Chris@43
|
154
|
Chris@67
|
155 map<QString, QString> wanted; // basename -> full lib name
|
Chris@43
|
156 for (auto lib: libraries) {
|
Chris@43
|
157 wanted[QFileInfo(lib).baseName()] = lib;
|
Chris@43
|
158 }
|
Chris@43
|
159
|
Chris@43
|
160 vector<LibraryInfo> results;
|
Chris@43
|
161
|
Chris@43
|
162 for (auto t: tt) {
|
Chris@43
|
163
|
Chris@43
|
164 Node libId = store.complete(Triple(t.subject(),
|
Chris@43
|
165 store.expand("vamp:identifier"),
|
Chris@43
|
166 Node()));
|
Chris@43
|
167 if (libId.type != Node::Literal) {
|
Chris@43
|
168 continue;
|
Chris@43
|
169 }
|
Chris@43
|
170 auto wi = wanted.find(libId.value);
|
Chris@43
|
171 if (wi == wanted.end()) {
|
Chris@43
|
172 continue;
|
Chris@43
|
173 }
|
Chris@43
|
174
|
Chris@50
|
175 Node title = store.complete(Triple(t.subject(),
|
Chris@50
|
176 store.expand("dc:title"),
|
Chris@50
|
177 Node()));
|
Chris@50
|
178 if (title.type != Node::Literal) {
|
Chris@50
|
179 continue;
|
Chris@50
|
180 }
|
Chris@50
|
181
|
Chris@43
|
182 LibraryInfo info;
|
Chris@43
|
183 info.id = wi->first;
|
Chris@43
|
184 info.fileName = wi->second;
|
Chris@50
|
185 info.title = title.value;
|
Chris@43
|
186
|
Chris@43
|
187 Node maker = store.complete(Triple(t.subject(),
|
Chris@43
|
188 store.expand("foaf:maker"),
|
Chris@43
|
189 Node()));
|
Chris@43
|
190 if (maker.type == Node::Literal) {
|
Chris@43
|
191 info.maker = maker.value;
|
Chris@46
|
192 } else if (maker != Node()) {
|
Chris@46
|
193 maker = store.complete(Triple(maker,
|
Chris@46
|
194 store.expand("foaf:name"),
|
Chris@46
|
195 Node()));
|
Chris@46
|
196 if (maker.type == Node::Literal) {
|
Chris@46
|
197 info.maker = maker.value;
|
Chris@46
|
198 }
|
Chris@43
|
199 }
|
Chris@46
|
200
|
Chris@43
|
201 Node desc = store.complete(Triple(t.subject(),
|
Chris@43
|
202 store.expand("dc:description"),
|
Chris@43
|
203 Node()));
|
Chris@43
|
204 if (desc.type == Node::Literal) {
|
Chris@43
|
205 info.description = desc.value;
|
Chris@43
|
206 }
|
Chris@43
|
207
|
Chris@47
|
208 Triples pp = store.match(Triple(t.subject(),
|
Chris@47
|
209 store.expand("vamp:available_plugin"),
|
Chris@47
|
210 Node()));
|
Chris@47
|
211 for (auto p: pp) {
|
Chris@47
|
212 Node ptitle = store.complete(Triple(p.object(),
|
Chris@47
|
213 store.expand("dc:title"),
|
Chris@47
|
214 Node()));
|
Chris@47
|
215 if (ptitle.type == Node::Literal) {
|
Chris@47
|
216 info.pluginTitles.push_back(ptitle.value);
|
Chris@47
|
217 }
|
Chris@67
|
218
|
Chris@67
|
219 Node pident = store.complete(Triple(p.object(),
|
Chris@67
|
220 store.expand("vamp:identifier"),
|
Chris@67
|
221 Node()));
|
Chris@67
|
222 Node pversion = store.complete(Triple(p.object(),
|
Chris@67
|
223 store.expand("owl:versionInfo"),
|
Chris@67
|
224 Node()));
|
Chris@67
|
225 if (pident.type == Node::Literal &&
|
Chris@67
|
226 pversion.type == Node::Literal) {
|
Chris@67
|
227 bool ok = false;
|
Chris@67
|
228 int version = pversion.value.toInt(&ok);
|
Chris@67
|
229 if (ok) {
|
Chris@67
|
230 info.pluginVersions[pident.value] = version;
|
Chris@67
|
231 }
|
Chris@67
|
232 }
|
Chris@47
|
233 }
|
Chris@47
|
234
|
Chris@43
|
235 results.push_back(info);
|
Chris@50
|
236 wanted.erase(libId.value);
|
Chris@43
|
237 }
|
Chris@43
|
238
|
Chris@50
|
239 for (auto wp: wanted) {
|
Chris@51
|
240 SVCERR << "Failed to find any RDF information about library "
|
Chris@51
|
241 << wp.second << endl;
|
Chris@50
|
242 }
|
Chris@50
|
243
|
Chris@43
|
244 return results;
|
Chris@43
|
245 }
|
Chris@43
|
246
|
Chris@67
|
247 struct TempFileDeleter {
|
Chris@67
|
248 ~TempFileDeleter() {
|
Chris@67
|
249 if (tempFile != "") {
|
Chris@67
|
250 QFile(tempFile).remove();
|
Chris@67
|
251 }
|
Chris@67
|
252 }
|
Chris@67
|
253 QString tempFile;
|
Chris@67
|
254 };
|
Chris@67
|
255
|
Chris@67
|
256 map<QString, int>
|
Chris@67
|
257 getInstalledLibraryPluginVersions(QString libraryFilePath)
|
Chris@67
|
258 {
|
Chris@67
|
259 static QMutex mutex;
|
Chris@67
|
260 static QString tempFileName;
|
Chris@67
|
261 static TempFileDeleter deleter;
|
Chris@67
|
262 static bool initHappened = false, initSucceeded = false;
|
Chris@67
|
263
|
Chris@67
|
264 QMutexLocker locker (&mutex);
|
Chris@67
|
265
|
Chris@67
|
266 if (!initHappened) {
|
Chris@67
|
267 initHappened = true;
|
Chris@67
|
268
|
Chris@67
|
269 QTemporaryFile tempFile;
|
Chris@67
|
270 tempFile.setAutoRemove(false);
|
Chris@67
|
271 if (!tempFile.open()) {
|
Chris@67
|
272 SVCERR << "ERROR: Failed to open a temporary file" << endl;
|
Chris@67
|
273 return {};
|
Chris@67
|
274 }
|
Chris@67
|
275
|
Chris@67
|
276 // We can't make the QTemporaryFile static, as it will hold
|
Chris@67
|
277 // the file open and that prevents us from executing it. Hence
|
Chris@67
|
278 // the separate deleter.
|
Chris@67
|
279
|
Chris@67
|
280 tempFileName = tempFile.fileName();
|
Chris@67
|
281 deleter.tempFile = tempFileName;
|
Chris@67
|
282
|
Chris@67
|
283 #ifdef Q_OS_WIN32
|
Chris@67
|
284 QString helperPath = ":out/get-version.exe";
|
Chris@67
|
285 #else
|
Chris@67
|
286 QString helperPath = ":out/get-version";
|
Chris@67
|
287 #endif
|
Chris@67
|
288 QFile helper(helperPath);
|
Chris@67
|
289 if (!helper.open(QFile::ReadOnly)) {
|
Chris@67
|
290 SVCERR << "ERROR: Failed to read helper code" << endl;
|
Chris@67
|
291 return {};
|
Chris@67
|
292 }
|
Chris@67
|
293 QByteArray content = helper.readAll();
|
Chris@67
|
294 helper.close();
|
Chris@67
|
295
|
Chris@67
|
296 if (tempFile.write(content) != content.size()) {
|
Chris@67
|
297 SVCERR << "ERROR: Incomplete write to temporary file" << endl;
|
Chris@67
|
298 return {};
|
Chris@67
|
299 }
|
Chris@67
|
300 tempFile.close();
|
Chris@67
|
301
|
Chris@67
|
302 if (!QFile::setPermissions
|
Chris@67
|
303 (tempFileName,
|
Chris@67
|
304 QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) {
|
Chris@67
|
305 SVCERR << "ERROR: Failed to set execute permission on helper "
|
Chris@67
|
306 << tempFileName << endl;
|
Chris@67
|
307 return {};
|
Chris@67
|
308 }
|
Chris@67
|
309
|
Chris@67
|
310 initSucceeded = true;
|
Chris@67
|
311 }
|
Chris@67
|
312
|
Chris@67
|
313 if (!initSucceeded) {
|
Chris@67
|
314 return {};
|
Chris@67
|
315 }
|
Chris@67
|
316
|
Chris@67
|
317 QProcess process;
|
Chris@67
|
318 process.start(tempFileName, { libraryFilePath });
|
Chris@67
|
319
|
Chris@67
|
320 if (!process.waitForStarted()) {
|
Chris@67
|
321 QProcess::ProcessError err = process.error();
|
Chris@67
|
322 if (err == QProcess::FailedToStart) {
|
Chris@67
|
323 SVCERR << "Unable to start helper process " << tempFileName << endl;
|
Chris@67
|
324 } else if (err == QProcess::Crashed) {
|
Chris@67
|
325 SVCERR << "Helper process " << tempFileName
|
Chris@67
|
326 << " crashed on startup" << endl;
|
Chris@67
|
327 } else {
|
Chris@67
|
328 SVCERR << "Helper process " << tempFileName
|
Chris@67
|
329 << " failed on startup with error code " << err << endl;
|
Chris@67
|
330 }
|
Chris@67
|
331 return {};
|
Chris@67
|
332 }
|
Chris@67
|
333 process.waitForFinished();
|
Chris@67
|
334
|
Chris@67
|
335 QByteArray stdOut = process.readAllStandardOutput();
|
Chris@67
|
336 QByteArray stdErr = process.readAllStandardError();
|
Chris@67
|
337
|
Chris@67
|
338 QString errStr = QString::fromUtf8(stdErr);
|
Chris@67
|
339 if (!errStr.isEmpty()) {
|
Chris@67
|
340 SVCERR << "Note: Helper process stderr follows:" << endl;
|
Chris@67
|
341 SVCERR << errStr << endl;
|
Chris@67
|
342 SVCERR << "Note: Helper process stderr ends" << endl;
|
Chris@67
|
343 }
|
Chris@67
|
344
|
Chris@67
|
345 QStringList lines = QString::fromUtf8(stdOut).split
|
Chris@67
|
346 (QRegExp("[\\r\\n]+"), QString::SkipEmptyParts);
|
Chris@67
|
347 map<QString, int> versions;
|
Chris@67
|
348 for (QString line: lines) {
|
Chris@67
|
349 QStringList parts = line.split(":");
|
Chris@67
|
350 if (parts.size() != 2) {
|
Chris@67
|
351 SVCERR << "Unparseable output line: " << line << endl;
|
Chris@67
|
352 continue;
|
Chris@67
|
353 }
|
Chris@67
|
354 bool ok = false;
|
Chris@67
|
355 int version = parts[1].toInt(&ok);
|
Chris@67
|
356 if (!ok) {
|
Chris@67
|
357 SVCERR << "Unparseable version number in line: " << line << endl;
|
Chris@67
|
358 continue;
|
Chris@67
|
359 }
|
Chris@67
|
360 versions[parts[0]] = version;
|
Chris@67
|
361 }
|
Chris@67
|
362
|
Chris@67
|
363 return versions;
|
Chris@67
|
364 }
|
Chris@67
|
365
|
Chris@67
|
366 bool isLibraryNewer(map<QString, int> a, map<QString, int> b)
|
Chris@67
|
367 {
|
Chris@67
|
368 // a and b are maps from plugin id to plugin version for libraries
|
Chris@67
|
369 // A and B. (There is no overarching library version number.) We
|
Chris@67
|
370 // deem library A to be newer than library B if:
|
Chris@67
|
371 //
|
Chris@67
|
372 // 1. A contains a plugin id that is also in B, whose version in
|
Chris@67
|
373 // A is newer than that in B, or
|
Chris@67
|
374 //
|
Chris@67
|
375 // 2. B is not newer than A according to rule 1, and neither A or
|
Chris@67
|
376 // B is empty, and A contains a plugin id that is not in B, and B
|
Chris@67
|
377 // does not contain any plugin id that is not in A
|
Chris@67
|
378 //
|
Chris@67
|
379 // (The not-empty part of rule 2 is just to avoid false positives
|
Chris@67
|
380 // when a library or its metadata could not be read at all.)
|
Chris@67
|
381
|
Chris@67
|
382 auto containsANewerPlugin = [](const map<QString, int> &m1,
|
Chris@67
|
383 const map<QString, int> &m2) {
|
Chris@67
|
384 for (auto p: m1) {
|
Chris@67
|
385 if (m2.find(p.first) != m2.end() &&
|
Chris@67
|
386 p.second > m2.at(p.first)) {
|
Chris@67
|
387 return true;
|
Chris@67
|
388 }
|
Chris@67
|
389 }
|
Chris@67
|
390 return false;
|
Chris@67
|
391 };
|
Chris@67
|
392
|
Chris@67
|
393 auto containsANovelPlugin = [](const map<QString, int> &m1,
|
Chris@67
|
394 const map<QString, int> &m2) {
|
Chris@67
|
395 for (auto p: m1) {
|
Chris@67
|
396 if (m2.find(p.first) == m2.end()) {
|
Chris@67
|
397 return true;
|
Chris@67
|
398 }
|
Chris@67
|
399 }
|
Chris@67
|
400 return false;
|
Chris@67
|
401 };
|
Chris@67
|
402
|
Chris@67
|
403 if (containsANewerPlugin(a, b)) {
|
Chris@67
|
404 return true;
|
Chris@67
|
405 }
|
Chris@67
|
406
|
Chris@67
|
407 if (!containsANewerPlugin(b, a) &&
|
Chris@67
|
408 !a.empty() &&
|
Chris@67
|
409 !b.empty() &&
|
Chris@67
|
410 containsANovelPlugin(a, b) &&
|
Chris@67
|
411 !containsANovelPlugin(b, a)) {
|
Chris@67
|
412 return true;
|
Chris@67
|
413 }
|
Chris@67
|
414
|
Chris@67
|
415 return false;
|
Chris@67
|
416 }
|
Chris@67
|
417
|
Chris@67
|
418 QString
|
Chris@67
|
419 versionsString(const map<QString, int> &vv)
|
Chris@67
|
420 {
|
Chris@67
|
421 QStringList pv;
|
Chris@67
|
422 for (auto v: vv) {
|
Chris@67
|
423 pv.push_back(QString("%1:%2").arg(v.first).arg(v.second));
|
Chris@67
|
424 }
|
Chris@67
|
425 return "{ " + pv.join(", ") + " }";
|
Chris@67
|
426 }
|
Chris@67
|
427
|
Chris@42
|
428 void
|
Chris@67
|
429 installLibrary(QString library, LibraryInfo info, QString target)
|
Chris@42
|
430 {
|
Chris@52
|
431 QString source = ":out";
|
Chris@52
|
432 QFile f(source + "/" + library);
|
Chris@42
|
433 QString destination = target + "/" + library;
|
Chris@67
|
434
|
Chris@67
|
435 if (QFileInfo(destination).exists()) {
|
Chris@67
|
436 auto installed = getInstalledLibraryPluginVersions(destination);
|
Chris@67
|
437 SVCERR << "Note: comparing installed plugin versions "
|
Chris@67
|
438 << versionsString(installed)
|
Chris@67
|
439 << " to packaged versions "
|
Chris@67
|
440 << versionsString(info.pluginVersions)
|
Chris@67
|
441 << ": isLibraryNewer(installed, packaged) returns "
|
Chris@67
|
442 << isLibraryNewer(installed, info.pluginVersions)
|
Chris@67
|
443 << endl;
|
Chris@68
|
444 } else {
|
Chris@68
|
445 SVCERR << "Note: library " << library
|
Chris@68
|
446 << " is not yet installed, not comparing versions" << endl;
|
Chris@67
|
447 }
|
Chris@52
|
448
|
Chris@51
|
449 SVCERR << "Copying " << library.toStdString() << " to "
|
Chris@51
|
450 << destination.toStdString() << "..." << endl;
|
Chris@42
|
451 if (!f.copy(destination)) {
|
Chris@51
|
452 SVCERR << "Failed to copy " << library.toStdString()
|
Chris@51
|
453 << " to target " << destination.toStdString() << endl;
|
Chris@42
|
454 return;
|
Chris@42
|
455 }
|
Chris@42
|
456 if (!QFile::setPermissions
|
Chris@42
|
457 (destination,
|
Chris@42
|
458 QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner |
|
Chris@42
|
459 QFile::ReadGroup | QFile::ExeGroup |
|
Chris@42
|
460 QFile::ReadOther | QFile::ExeOther)) {
|
Chris@51
|
461 SVCERR << "Failed to set permissions on "
|
Chris@51
|
462 << library.toStdString() << endl;
|
Chris@42
|
463 return;
|
Chris@42
|
464 }
|
Chris@52
|
465
|
Chris@52
|
466 QString base = QFileInfo(library).baseName();
|
Chris@52
|
467 QDir dir(source);
|
Chris@52
|
468 auto entries = dir.entryList({ base + "*" });
|
Chris@52
|
469 for (auto e: entries) {
|
Chris@52
|
470 if (e == library) continue;
|
Chris@52
|
471 QString destination = target + "/" + e;
|
Chris@52
|
472 SVCERR << "Copying " << e.toStdString() << " to "
|
Chris@52
|
473 << destination.toStdString() << "..." << endl;
|
Chris@52
|
474 if (!QFile(source + "/" + e).copy(destination)) {
|
Chris@52
|
475 SVCERR << "Failed to copy " << e.toStdString()
|
Chris@52
|
476 << " to target " << destination.toStdString()
|
Chris@52
|
477 << " (ignoring)" << endl;
|
Chris@68
|
478 continue;
|
Chris@68
|
479 }
|
Chris@68
|
480 if (!QFile::setPermissions
|
Chris@68
|
481 (destination,
|
Chris@68
|
482 QFile::ReadOwner | QFile::WriteOwner |
|
Chris@68
|
483 QFile::ReadGroup |
|
Chris@68
|
484 QFile::ReadOther)) {
|
Chris@68
|
485 SVCERR << "Failed to set permissions on "
|
Chris@68
|
486 << destination.toStdString()
|
Chris@68
|
487 << " (ignoring)" << endl;
|
Chris@68
|
488 continue;
|
Chris@52
|
489 }
|
Chris@52
|
490 }
|
Chris@42
|
491 }
|
Chris@42
|
492
|
Chris@67
|
493 map<QString, LibraryInfo>
|
Chris@43
|
494 getUserApprovedPluginLibraries(vector<LibraryInfo> libraries)
|
Chris@42
|
495 {
|
Chris@42
|
496 QDialog dialog;
|
Chris@46
|
497
|
Chris@46
|
498 auto mainLayout = new QGridLayout;
|
Chris@47
|
499 mainLayout->setSpacing(0);
|
Chris@46
|
500 dialog.setLayout(mainLayout);
|
Chris@46
|
501
|
Chris@46
|
502 int mainRow = 0;
|
Chris@46
|
503
|
Chris@47
|
504 auto checkAll = new QCheckBox;
|
Chris@53
|
505 checkAll->setChecked(true);
|
Chris@47
|
506 mainLayout->addWidget(checkAll, mainRow, 0, Qt::AlignHCenter);
|
Chris@47
|
507 ++mainRow;
|
Chris@47
|
508
|
Chris@47
|
509 auto checkArrow = new QLabel("▼");
|
Chris@47
|
510 checkArrow->setTextFormat(Qt::RichText);
|
Chris@47
|
511 mainLayout->addWidget(checkArrow, mainRow, 0, Qt::AlignHCenter);
|
Chris@47
|
512 ++mainRow;
|
Chris@47
|
513
|
Chris@46
|
514 auto scroll = new QScrollArea;
|
Chris@47
|
515 scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
Chris@46
|
516 mainLayout->addWidget(scroll, mainRow, 0, 1, 2);
|
Chris@46
|
517 mainLayout->setRowStretch(mainRow, 10);
|
Chris@46
|
518 ++mainRow;
|
Chris@46
|
519
|
Chris@46
|
520 auto selectionFrame = new QWidget;
|
Chris@46
|
521
|
Chris@46
|
522 auto selectionLayout = new QGridLayout;
|
Chris@46
|
523 selectionFrame->setLayout(selectionLayout);
|
Chris@46
|
524 int selectionRow = 0;
|
Chris@42
|
525
|
Chris@67
|
526 map<QString, QCheckBox *> checkBoxMap; // filename -> checkbox
|
Chris@67
|
527 map<QString, LibraryInfo> libFileInfo; // filename -> info
|
Chris@43
|
528
|
Chris@67
|
529 map<QString, LibraryInfo, function<bool (QString, QString)>>
|
Chris@49
|
530 orderedInfo
|
Chris@49
|
531 ([](QString k1, QString k2) {
|
Chris@49
|
532 return k1.localeAwareCompare(k2) < 0;
|
Chris@49
|
533 });
|
Chris@43
|
534 for (auto info: libraries) {
|
Chris@43
|
535 orderedInfo[info.title] = info;
|
Chris@43
|
536 }
|
Chris@53
|
537
|
Chris@43
|
538 for (auto ip: orderedInfo) {
|
Chris@46
|
539
|
Chris@46
|
540 auto cb = new QCheckBox;
|
Chris@53
|
541 cb->setChecked(true);
|
Chris@53
|
542
|
Chris@47
|
543 selectionLayout->addWidget(cb, selectionRow, 0,
|
Chris@47
|
544 Qt::AlignTop | Qt::AlignHCenter);
|
Chris@46
|
545
|
Chris@43
|
546 LibraryInfo info = ip.second;
|
Chris@47
|
547 /*
|
Chris@47
|
548 int n = info.pluginTitles.size();
|
Chris@47
|
549 QString contents;
|
Chris@47
|
550
|
Chris@47
|
551 if (n > 0) {
|
Chris@47
|
552 int max = 4;
|
Chris@47
|
553 QStringList titles;
|
Chris@47
|
554 for (int i = 0; i < max && i < int(info.pluginTitles.size()); ++i) {
|
Chris@47
|
555 titles.push_back(info.pluginTitles[i]);
|
Chris@47
|
556 }
|
Chris@47
|
557 QString titleText = titles.join(", ");
|
Chris@47
|
558 if (max < int(info.pluginTitles.size())) {
|
Chris@47
|
559 titleText = QObject::tr("%1 ...").arg(titleText);
|
Chris@47
|
560 }
|
Chris@47
|
561 contents = QObject::tr("Plugins: %1").arg(titleText);
|
Chris@47
|
562 }
|
Chris@47
|
563 */
|
Chris@51
|
564 QString text = QObject::tr("<b>%1</b><br><i>%2</i><br>%3")
|
Chris@47
|
565 .arg(info.title)
|
Chris@47
|
566 .arg(info.maker)
|
Chris@47
|
567 .arg(info.description);
|
Chris@46
|
568
|
Chris@46
|
569 auto label = new QLabel(text);
|
Chris@47
|
570 label->setWordWrap(true);
|
Chris@47
|
571 label->setMinimumWidth(800);
|
Chris@46
|
572
|
Chris@46
|
573 selectionLayout->addWidget(label, selectionRow, 1, Qt::AlignTop);
|
Chris@46
|
574
|
Chris@46
|
575 ++selectionRow;
|
Chris@46
|
576
|
Chris@43
|
577 checkBoxMap[info.fileName] = cb;
|
Chris@67
|
578 libFileInfo[info.fileName] = info;
|
Chris@42
|
579 }
|
Chris@42
|
580
|
Chris@46
|
581 scroll->setWidget(selectionFrame);
|
Chris@46
|
582
|
Chris@47
|
583 QObject::connect(checkAll, &QCheckBox::toggled,
|
Chris@47
|
584 [=]() {
|
Chris@47
|
585 bool toCheck = checkAll->isChecked();
|
Chris@47
|
586 for (auto p: checkBoxMap) {
|
Chris@47
|
587 p.second->setChecked(toCheck);
|
Chris@47
|
588 }
|
Chris@47
|
589 });
|
Chris@47
|
590
|
Chris@42
|
591 auto bb = new QDialogButtonBox(QDialogButtonBox::Ok |
|
Chris@42
|
592 QDialogButtonBox::Cancel);
|
Chris@46
|
593 mainLayout->addWidget(bb, mainRow, 0, 1, 2);
|
Chris@46
|
594 ++mainRow;
|
Chris@47
|
595
|
Chris@47
|
596 int cw = 50;
|
Chris@47
|
597 mainLayout->setColumnMinimumWidth(0, cw + 20); //!!!
|
Chris@47
|
598 mainLayout->setColumnStretch(1, 10);
|
Chris@47
|
599 selectionLayout->setColumnMinimumWidth(0, cw); //!!!
|
Chris@53
|
600 selectionLayout->setColumnMinimumWidth(1, 820); //!!!
|
Chris@53
|
601 selectionLayout->setColumnStretch(1, 10);
|
Chris@47
|
602
|
Chris@42
|
603 QObject::connect(bb, SIGNAL(accepted()), &dialog, SLOT(accept()));
|
Chris@42
|
604 QObject::connect(bb, SIGNAL(rejected()), &dialog, SLOT(reject()));
|
Chris@53
|
605
|
Chris@42
|
606 if (dialog.exec() == QDialog::Accepted) {
|
Chris@51
|
607 SVCERR << "accepted" << endl;
|
Chris@42
|
608 } else {
|
Chris@51
|
609 SVCERR << "rejected" << endl;
|
Chris@42
|
610 }
|
Chris@42
|
611
|
Chris@67
|
612 map<QString, LibraryInfo> approved;
|
Chris@42
|
613 for (const auto &p: checkBoxMap) {
|
Chris@42
|
614 if (p.second->isChecked()) {
|
Chris@67
|
615 approved[p.first] = libFileInfo[p.first];
|
Chris@33
|
616 }
|
Chris@42
|
617 }
|
Chris@42
|
618
|
Chris@42
|
619 return approved;
|
Chris@42
|
620 }
|
Chris@42
|
621
|
Chris@42
|
622 int main(int argc, char **argv)
|
Chris@42
|
623 {
|
Chris@42
|
624 QApplication app(argc, argv);
|
Chris@42
|
625
|
Chris@51
|
626 QApplication::setOrganizationName("sonic-visualiser");
|
Chris@51
|
627 QApplication::setOrganizationDomain("sonicvisualiser.org");
|
Chris@51
|
628 QApplication::setApplicationName(QApplication::tr("Vamp Plugin Pack Installer"));
|
Chris@51
|
629
|
Chris@51
|
630 #ifdef Q_OS_WIN32
|
Chris@51
|
631 QFont font(QApplication::font());
|
Chris@51
|
632 QString preferredFamily = "Segoe UI";
|
Chris@51
|
633 font.setFamily(preferredFamily);
|
Chris@51
|
634 if (QFontInfo(font).family() == preferredFamily) {
|
Chris@51
|
635 font.setPointSize(10);
|
Chris@51
|
636 QApplication::setFont(font);
|
Chris@51
|
637 }
|
Chris@51
|
638 #endif
|
Chris@51
|
639
|
Chris@42
|
640 QString target = getDefaultInstallDirectory();
|
Chris@42
|
641 if (target == "") {
|
Chris@42
|
642 return 1;
|
Chris@42
|
643 }
|
Chris@42
|
644
|
Chris@42
|
645 QStringList libraries = getPluginLibraryList();
|
Chris@42
|
646
|
Chris@43
|
647 auto rdfStore = loadLibrariesRdf();
|
Chris@43
|
648
|
Chris@43
|
649 auto info = getLibraryInfo(*rdfStore, libraries);
|
Chris@43
|
650
|
Chris@67
|
651 map<QString, LibraryInfo> toInstall = getUserApprovedPluginLibraries(info);
|
Chris@52
|
652
|
Chris@52
|
653 if (!toInstall.empty()) {
|
Chris@52
|
654 if (!QDir(target).exists()) {
|
Chris@52
|
655 QDir().mkpath(target);
|
Chris@52
|
656 }
|
Chris@52
|
657 }
|
Chris@42
|
658
|
Chris@42
|
659 for (auto lib: toInstall) {
|
Chris@67
|
660 installLibrary(lib.first, lib.second, target);
|
Chris@33
|
661 }
|
Chris@33
|
662
|
Chris@32
|
663 return 0;
|
Chris@32
|
664 }
|