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@67
|
444 }
|
Chris@52
|
445
|
Chris@51
|
446 SVCERR << "Copying " << library.toStdString() << " to "
|
Chris@51
|
447 << destination.toStdString() << "..." << endl;
|
Chris@42
|
448 if (!f.copy(destination)) {
|
Chris@51
|
449 SVCERR << "Failed to copy " << library.toStdString()
|
Chris@51
|
450 << " to target " << destination.toStdString() << endl;
|
Chris@42
|
451 return;
|
Chris@42
|
452 }
|
Chris@42
|
453 if (!QFile::setPermissions
|
Chris@42
|
454 (destination,
|
Chris@42
|
455 QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner |
|
Chris@42
|
456 QFile::ReadGroup | QFile::ExeGroup |
|
Chris@42
|
457 QFile::ReadOther | QFile::ExeOther)) {
|
Chris@51
|
458 SVCERR << "Failed to set permissions on "
|
Chris@51
|
459 << library.toStdString() << endl;
|
Chris@42
|
460 return;
|
Chris@42
|
461 }
|
Chris@52
|
462
|
Chris@52
|
463 QString base = QFileInfo(library).baseName();
|
Chris@52
|
464 QDir dir(source);
|
Chris@52
|
465 auto entries = dir.entryList({ base + "*" });
|
Chris@52
|
466 for (auto e: entries) {
|
Chris@52
|
467 if (e == library) continue;
|
Chris@52
|
468 QString destination = target + "/" + e;
|
Chris@52
|
469 SVCERR << "Copying " << e.toStdString() << " to "
|
Chris@52
|
470 << destination.toStdString() << "..." << endl;
|
Chris@52
|
471 if (!QFile(source + "/" + e).copy(destination)) {
|
Chris@52
|
472 SVCERR << "Failed to copy " << e.toStdString()
|
Chris@52
|
473 << " to target " << destination.toStdString()
|
Chris@52
|
474 << " (ignoring)" << endl;
|
Chris@52
|
475 }
|
Chris@52
|
476 }
|
Chris@42
|
477 }
|
Chris@42
|
478
|
Chris@67
|
479 map<QString, LibraryInfo>
|
Chris@43
|
480 getUserApprovedPluginLibraries(vector<LibraryInfo> libraries)
|
Chris@42
|
481 {
|
Chris@42
|
482 QDialog dialog;
|
Chris@46
|
483
|
Chris@46
|
484 auto mainLayout = new QGridLayout;
|
Chris@47
|
485 mainLayout->setSpacing(0);
|
Chris@46
|
486 dialog.setLayout(mainLayout);
|
Chris@46
|
487
|
Chris@46
|
488 int mainRow = 0;
|
Chris@46
|
489
|
Chris@47
|
490 auto checkAll = new QCheckBox;
|
Chris@53
|
491 checkAll->setChecked(true);
|
Chris@47
|
492 mainLayout->addWidget(checkAll, mainRow, 0, Qt::AlignHCenter);
|
Chris@47
|
493 ++mainRow;
|
Chris@47
|
494
|
Chris@47
|
495 auto checkArrow = new QLabel("▼");
|
Chris@47
|
496 checkArrow->setTextFormat(Qt::RichText);
|
Chris@47
|
497 mainLayout->addWidget(checkArrow, mainRow, 0, Qt::AlignHCenter);
|
Chris@47
|
498 ++mainRow;
|
Chris@47
|
499
|
Chris@46
|
500 auto scroll = new QScrollArea;
|
Chris@47
|
501 scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
Chris@46
|
502 mainLayout->addWidget(scroll, mainRow, 0, 1, 2);
|
Chris@46
|
503 mainLayout->setRowStretch(mainRow, 10);
|
Chris@46
|
504 ++mainRow;
|
Chris@46
|
505
|
Chris@46
|
506 auto selectionFrame = new QWidget;
|
Chris@46
|
507
|
Chris@46
|
508 auto selectionLayout = new QGridLayout;
|
Chris@46
|
509 selectionFrame->setLayout(selectionLayout);
|
Chris@46
|
510 int selectionRow = 0;
|
Chris@42
|
511
|
Chris@67
|
512 map<QString, QCheckBox *> checkBoxMap; // filename -> checkbox
|
Chris@67
|
513 map<QString, LibraryInfo> libFileInfo; // filename -> info
|
Chris@43
|
514
|
Chris@67
|
515 map<QString, LibraryInfo, function<bool (QString, QString)>>
|
Chris@49
|
516 orderedInfo
|
Chris@49
|
517 ([](QString k1, QString k2) {
|
Chris@49
|
518 return k1.localeAwareCompare(k2) < 0;
|
Chris@49
|
519 });
|
Chris@43
|
520 for (auto info: libraries) {
|
Chris@43
|
521 orderedInfo[info.title] = info;
|
Chris@43
|
522 }
|
Chris@53
|
523
|
Chris@43
|
524 for (auto ip: orderedInfo) {
|
Chris@46
|
525
|
Chris@46
|
526 auto cb = new QCheckBox;
|
Chris@53
|
527 cb->setChecked(true);
|
Chris@53
|
528
|
Chris@47
|
529 selectionLayout->addWidget(cb, selectionRow, 0,
|
Chris@47
|
530 Qt::AlignTop | Qt::AlignHCenter);
|
Chris@46
|
531
|
Chris@43
|
532 LibraryInfo info = ip.second;
|
Chris@47
|
533 /*
|
Chris@47
|
534 int n = info.pluginTitles.size();
|
Chris@47
|
535 QString contents;
|
Chris@47
|
536
|
Chris@47
|
537 if (n > 0) {
|
Chris@47
|
538 int max = 4;
|
Chris@47
|
539 QStringList titles;
|
Chris@47
|
540 for (int i = 0; i < max && i < int(info.pluginTitles.size()); ++i) {
|
Chris@47
|
541 titles.push_back(info.pluginTitles[i]);
|
Chris@47
|
542 }
|
Chris@47
|
543 QString titleText = titles.join(", ");
|
Chris@47
|
544 if (max < int(info.pluginTitles.size())) {
|
Chris@47
|
545 titleText = QObject::tr("%1 ...").arg(titleText);
|
Chris@47
|
546 }
|
Chris@47
|
547 contents = QObject::tr("Plugins: %1").arg(titleText);
|
Chris@47
|
548 }
|
Chris@47
|
549 */
|
Chris@51
|
550 QString text = QObject::tr("<b>%1</b><br><i>%2</i><br>%3")
|
Chris@47
|
551 .arg(info.title)
|
Chris@47
|
552 .arg(info.maker)
|
Chris@47
|
553 .arg(info.description);
|
Chris@46
|
554
|
Chris@46
|
555 auto label = new QLabel(text);
|
Chris@47
|
556 label->setWordWrap(true);
|
Chris@47
|
557 label->setMinimumWidth(800);
|
Chris@46
|
558
|
Chris@46
|
559 selectionLayout->addWidget(label, selectionRow, 1, Qt::AlignTop);
|
Chris@46
|
560
|
Chris@46
|
561 ++selectionRow;
|
Chris@46
|
562
|
Chris@43
|
563 checkBoxMap[info.fileName] = cb;
|
Chris@67
|
564 libFileInfo[info.fileName] = info;
|
Chris@42
|
565 }
|
Chris@42
|
566
|
Chris@46
|
567 scroll->setWidget(selectionFrame);
|
Chris@46
|
568
|
Chris@47
|
569 QObject::connect(checkAll, &QCheckBox::toggled,
|
Chris@47
|
570 [=]() {
|
Chris@47
|
571 bool toCheck = checkAll->isChecked();
|
Chris@47
|
572 for (auto p: checkBoxMap) {
|
Chris@47
|
573 p.second->setChecked(toCheck);
|
Chris@47
|
574 }
|
Chris@47
|
575 });
|
Chris@47
|
576
|
Chris@42
|
577 auto bb = new QDialogButtonBox(QDialogButtonBox::Ok |
|
Chris@42
|
578 QDialogButtonBox::Cancel);
|
Chris@46
|
579 mainLayout->addWidget(bb, mainRow, 0, 1, 2);
|
Chris@46
|
580 ++mainRow;
|
Chris@47
|
581
|
Chris@47
|
582 int cw = 50;
|
Chris@47
|
583 mainLayout->setColumnMinimumWidth(0, cw + 20); //!!!
|
Chris@47
|
584 mainLayout->setColumnStretch(1, 10);
|
Chris@47
|
585 selectionLayout->setColumnMinimumWidth(0, cw); //!!!
|
Chris@53
|
586 selectionLayout->setColumnMinimumWidth(1, 820); //!!!
|
Chris@53
|
587 selectionLayout->setColumnStretch(1, 10);
|
Chris@47
|
588
|
Chris@42
|
589 QObject::connect(bb, SIGNAL(accepted()), &dialog, SLOT(accept()));
|
Chris@42
|
590 QObject::connect(bb, SIGNAL(rejected()), &dialog, SLOT(reject()));
|
Chris@53
|
591
|
Chris@42
|
592 if (dialog.exec() == QDialog::Accepted) {
|
Chris@51
|
593 SVCERR << "accepted" << endl;
|
Chris@42
|
594 } else {
|
Chris@51
|
595 SVCERR << "rejected" << endl;
|
Chris@42
|
596 }
|
Chris@42
|
597
|
Chris@67
|
598 map<QString, LibraryInfo> approved;
|
Chris@42
|
599 for (const auto &p: checkBoxMap) {
|
Chris@42
|
600 if (p.second->isChecked()) {
|
Chris@67
|
601 approved[p.first] = libFileInfo[p.first];
|
Chris@33
|
602 }
|
Chris@42
|
603 }
|
Chris@42
|
604
|
Chris@42
|
605 return approved;
|
Chris@42
|
606 }
|
Chris@42
|
607
|
Chris@42
|
608 int main(int argc, char **argv)
|
Chris@42
|
609 {
|
Chris@42
|
610 QApplication app(argc, argv);
|
Chris@42
|
611
|
Chris@51
|
612 QApplication::setOrganizationName("sonic-visualiser");
|
Chris@51
|
613 QApplication::setOrganizationDomain("sonicvisualiser.org");
|
Chris@51
|
614 QApplication::setApplicationName(QApplication::tr("Vamp Plugin Pack Installer"));
|
Chris@51
|
615
|
Chris@51
|
616 #ifdef Q_OS_WIN32
|
Chris@51
|
617 QFont font(QApplication::font());
|
Chris@51
|
618 QString preferredFamily = "Segoe UI";
|
Chris@51
|
619 font.setFamily(preferredFamily);
|
Chris@51
|
620 if (QFontInfo(font).family() == preferredFamily) {
|
Chris@51
|
621 font.setPointSize(10);
|
Chris@51
|
622 QApplication::setFont(font);
|
Chris@51
|
623 }
|
Chris@51
|
624 #endif
|
Chris@51
|
625
|
Chris@42
|
626 QString target = getDefaultInstallDirectory();
|
Chris@42
|
627 if (target == "") {
|
Chris@42
|
628 return 1;
|
Chris@42
|
629 }
|
Chris@42
|
630
|
Chris@42
|
631 QStringList libraries = getPluginLibraryList();
|
Chris@42
|
632
|
Chris@43
|
633 auto rdfStore = loadLibrariesRdf();
|
Chris@43
|
634
|
Chris@43
|
635 auto info = getLibraryInfo(*rdfStore, libraries);
|
Chris@43
|
636
|
Chris@67
|
637 map<QString, LibraryInfo> toInstall = getUserApprovedPluginLibraries(info);
|
Chris@52
|
638
|
Chris@52
|
639 if (!toInstall.empty()) {
|
Chris@52
|
640 if (!QDir(target).exists()) {
|
Chris@52
|
641 QDir().mkpath(target);
|
Chris@52
|
642 }
|
Chris@52
|
643 }
|
Chris@42
|
644
|
Chris@42
|
645 for (auto lib: toInstall) {
|
Chris@67
|
646 installLibrary(lib.first, lib.second, target);
|
Chris@33
|
647 }
|
Chris@33
|
648
|
Chris@32
|
649 return 0;
|
Chris@32
|
650 }
|