annotate installer.cpp @ 80:12bf881f9f9f

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