annotate installer.cpp @ 97:b5230bda1f1f

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