annotate installer.cpp @ 98:fb4ca43863b5

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