Chris@66: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@66: /* Chris@66: Copyright (c) 2020 Queen Mary, University of London Chris@66: Chris@66: Permission is hereby granted, free of charge, to any person Chris@66: obtaining a copy of this software and associated documentation Chris@66: files (the "Software"), to deal in the Software without Chris@66: restriction, including without limitation the rights to use, copy, Chris@66: modify, merge, publish, distribute, sublicense, and/or sell copies Chris@66: of the Software, and to permit persons to whom the Software is Chris@66: furnished to do so, subject to the following conditions: Chris@66: Chris@66: The above copyright notice and this permission notice shall be Chris@66: included in all copies or substantial portions of the Software. Chris@66: Chris@66: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, Chris@66: EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF Chris@66: MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND Chris@66: NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY Chris@66: CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF Chris@66: CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION Chris@66: WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Chris@66: Chris@66: Except as contained in this notice, the names of the Centre for Chris@66: Digital Music and Queen Mary, University of London shall not be Chris@66: used in advertising or otherwise to promote the sale, use or other Chris@66: dealings in this Software without prior written authorization. Chris@66: */ Chris@33: Chris@33: #include Chris@33: #include Chris@33: #include Chris@33: #include Chris@33: Chris@42: #include Chris@42: #include Chris@42: #include Chris@42: #include Chris@42: #include Chris@46: #include Chris@51: #include Chris@51: #include Chris@67: #include Chris@67: #include Chris@67: #include Chris@67: #include Chris@72: #include Chris@79: #include Chris@72: #include Chris@74: #include Chris@74: #include Chris@74: #include Chris@79: #include Chris@81: #include Chris@81: #include Chris@86: #include Chris@98: #include Chris@42: Chris@41: #include Chris@41: Chris@43: #include Chris@43: #include Chris@43: Chris@33: #include Chris@58: #include Chris@43: #include Chris@43: Chris@51: #include "base/Debug.h" Chris@51: Chris@79: #include "version.h" Chris@79: Chris@33: using namespace std; Chris@43: using namespace Dataquay; Chris@32: Chris@42: QString Chris@42: getDefaultInstallDirectory() Chris@32: { Chris@41: auto pathList = Vamp::PluginHostAdapter::getPluginPath(); Chris@41: if (pathList.empty()) { Chris@51: SVCERR << "Failed to look up Vamp plugin path" << endl; Chris@42: return QString(); Chris@41: } Chris@41: Chris@42: auto firstPath = *pathList.begin(); Chris@42: QString target = QString::fromUtf8(firstPath.c_str(), firstPath.size()); Chris@42: return target; Chris@42: } Chris@42: Chris@42: QStringList Chris@42: getPluginLibraryList() Chris@42: { Chris@33: QDir dir(":out/"); Chris@33: auto entries = dir.entryList({ "*.so", "*.dll", "*.dylib" }); Chris@33: Chris@33: for (auto e: entries) { Chris@51: SVCERR << e.toStdString() << endl; Chris@33: } Chris@33: Chris@42: return entries; Chris@42: } Chris@33: Chris@50: void Chris@50: loadLibraryRdf(BasicStore &store, QString filename) Chris@50: { Chris@50: QFile f(filename); Chris@50: if (!f.open(QFile::ReadOnly | QFile::Text)) { Chris@51: SVCERR << "Failed to open RDF resource file " Chris@51: << filename.toStdString() << endl; Chris@50: return; Chris@50: } Chris@50: Chris@50: QByteArray content = f.readAll(); Chris@50: f.close(); Chris@50: Chris@50: try { Chris@50: store.importString(QString::fromUtf8(content), Chris@50: Uri("file:" + filename), Chris@50: BasicStore::ImportIgnoreDuplicates); Chris@50: } catch (const RDFException &ex) { Chris@51: SVCERR << "Failed to import RDF resource file " Chris@51: << filename.toStdString() << ": " << ex.what() << endl; Chris@50: } Chris@50: } Chris@50: Chris@43: unique_ptr Chris@43: loadLibrariesRdf() Chris@43: { Chris@43: unique_ptr store(new BasicStore); Chris@43: Chris@50: vector dirs { ":rdf/plugins", ":out" }; Chris@43: Chris@50: for (auto d: dirs) { Chris@50: for (auto e: QDir(d).entryList({ "*.ttl", "*.n3" })) { Chris@50: loadLibraryRdf(*store, d + "/" + e); Chris@43: } Chris@43: } Chris@43: Chris@43: return store; Chris@43: } Chris@43: Chris@43: struct LibraryInfo { Chris@43: QString id; Chris@43: QString fileName; Chris@43: QString title; Chris@43: QString maker; Chris@43: QString description; Chris@78: QString page; Chris@47: QStringList pluginTitles; Chris@74: QString licence; Chris@43: }; Chris@43: Chris@99: struct Licence Chris@99: { Chris@99: static QString gpl; Chris@99: static QString gpl2; Chris@99: static QString gpl3; Chris@99: static QString agpl; Chris@99: static QString apache; Chris@99: static QString mit; Chris@99: }; Chris@99: Chris@99: QString Licence::gpl = "GNU General Public License"; Chris@99: QString Licence::gpl2 = "GNU General Public License, version 2"; Chris@99: QString Licence::gpl3 = "GNU General Public License, version 3"; Chris@99: QString Licence::agpl = "GNU Affero General Public License"; Chris@99: QString Licence::apache = "Apache License"; Chris@99: QString Licence::mit = "MIT License"; Chris@99: Chris@74: QString Chris@74: identifyLicence(QString libraryBasename) Chris@74: { Chris@74: QString licenceFile = QString(":out/%1_COPYING.txt").arg(libraryBasename); Chris@74: Chris@74: QFile f(licenceFile); Chris@74: if (!f.open(QFile::ReadOnly | QFile::Text)) { Chris@74: SVCERR << "Failed to open licence file " Chris@74: << licenceFile.toStdString() << endl; Chris@74: return {}; Chris@74: } Chris@74: Chris@74: QByteArray content = f.readAll(); Chris@74: f.close(); Chris@74: Chris@74: QString licenceText = QString::fromUtf8(content); Chris@74: Chris@82: // NB these are not expected to identify an arbitrary licence! We Chris@74: // know we have only a limited set here. But we do want to Chris@74: // determine this from the actual licence text included with the Chris@74: // plugin distribution, not just from e.g. RDF metadata Chris@74: Chris@99: if (licenceText.contains(Licence::gpl.toUpper(), Qt::CaseSensitive)) { Chris@74: if (licenceText.contains("Version 3, 29 June 2007")) { Chris@99: return Licence::gpl3; Chris@74: } else if (licenceText.contains("Version 2, June 1991")) { Chris@99: return Licence::gpl2; Chris@74: } else { Chris@99: return Licence::gpl; Chris@74: } Chris@74: } Chris@99: if (licenceText.contains(Licence::agpl.toUpper(), Qt::CaseSensitive)) { Chris@99: return Licence::agpl; Chris@74: } Chris@99: if (licenceText.contains(Licence::apache)) { Chris@99: return Licence::apache; Chris@74: } Chris@74: if (licenceText.contains("Permission is hereby granted, free of charge, to any person")) { Chris@99: return Licence::mit; Chris@74: } Chris@74: Chris@74: SVCERR << "Didn't recognise licence for " << libraryBasename << endl; Chris@74: Chris@74: return {}; Chris@74: } Chris@74: Chris@99: QString Chris@99: getLicenceURL(QString licence) Chris@99: { Chris@99: if (licence == Licence::gpl || Chris@99: licence == Licence::gpl3) { Chris@99: return "https://www.gnu.org/licenses/gpl-3.0.en.html"; Chris@99: } else if (licence == Licence::gpl2) { Chris@99: return "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html"; Chris@99: } else if (licence == Licence::agpl) { Chris@99: return "https://www.gnu.org/licenses/agpl-3.0.html"; Chris@99: } else if (licence == Licence::apache) { Chris@99: return "https://www.apache.org/licenses/LICENSE-2.0"; Chris@99: } else if (licence == Licence::mit) { Chris@99: return "https://opensource.org/licenses/MIT"; Chris@99: } Chris@99: Chris@99: return {}; Chris@99: } Chris@99: Chris@43: vector Chris@43: getLibraryInfo(const Store &store, QStringList libraries) Chris@43: { Chris@43: /* e.g. Chris@43: Chris@43: plugbase:library a vamp:PluginLibrary ; Chris@43: vamp:identifier "qm-vamp-plugins" ; Chris@43: dc:title "Queen Mary plugin set" Chris@43: */ Chris@43: Chris@43: Triples tt = store.match(Triple(Node(), Chris@43: Uri("a"), Chris@43: store.expand("vamp:PluginLibrary"))); Chris@43: Chris@67: map wanted; // basename -> full lib name Chris@43: for (auto lib: libraries) { Chris@43: wanted[QFileInfo(lib).baseName()] = lib; Chris@43: } Chris@43: Chris@43: vector results; Chris@43: Chris@43: for (auto t: tt) { Chris@43: Chris@43: Node libId = store.complete(Triple(t.subject(), Chris@43: store.expand("vamp:identifier"), Chris@43: Node())); Chris@43: if (libId.type != Node::Literal) { Chris@43: continue; Chris@43: } Chris@43: auto wi = wanted.find(libId.value); Chris@43: if (wi == wanted.end()) { Chris@43: continue; Chris@43: } Chris@74: Chris@50: Node title = store.complete(Triple(t.subject(), Chris@50: store.expand("dc:title"), Chris@50: Node())); Chris@50: if (title.type != Node::Literal) { Chris@50: continue; Chris@50: } Chris@50: Chris@43: LibraryInfo info; Chris@43: info.id = wi->first; Chris@43: info.fileName = wi->second; Chris@50: info.title = title.value; Chris@43: Chris@43: Node maker = store.complete(Triple(t.subject(), Chris@43: store.expand("foaf:maker"), Chris@43: Node())); Chris@43: if (maker.type == Node::Literal) { Chris@43: info.maker = maker.value; Chris@46: } else if (maker != Node()) { Chris@46: maker = store.complete(Triple(maker, Chris@46: store.expand("foaf:name"), Chris@46: Node())); Chris@46: if (maker.type == Node::Literal) { Chris@46: info.maker = maker.value; Chris@46: } Chris@43: } Chris@46: Chris@43: Node desc = store.complete(Triple(t.subject(), Chris@43: store.expand("dc:description"), Chris@43: Node())); Chris@43: if (desc.type == Node::Literal) { Chris@43: info.description = desc.value; Chris@43: } Chris@78: Chris@78: Node page = store.complete(Triple(t.subject(), Chris@78: store.expand("foaf:page"), Chris@78: Node())); Chris@79: if (page.type == Node::URI) { Chris@79: info.page = page.value; Chris@78: } Chris@43: Chris@47: Triples pp = store.match(Triple(t.subject(), Chris@47: store.expand("vamp:available_plugin"), Chris@47: Node())); Chris@47: for (auto p: pp) { Chris@47: Node ptitle = store.complete(Triple(p.object(), Chris@47: store.expand("dc:title"), Chris@47: Node())); Chris@47: if (ptitle.type == Node::Literal) { Chris@47: info.pluginTitles.push_back(ptitle.value); Chris@47: } Chris@47: } Chris@74: Chris@74: info.licence = identifyLicence(libId.value); Chris@74: SVCERR << "licence = " << info.licence << endl; Chris@47: Chris@43: results.push_back(info); Chris@50: wanted.erase(libId.value); Chris@43: } Chris@43: Chris@50: for (auto wp: wanted) { Chris@51: SVCERR << "Failed to find any RDF information about library " Chris@51: << wp.second << endl; Chris@50: } Chris@50: Chris@43: return results; Chris@43: } Chris@43: Chris@67: struct TempFileDeleter { Chris@67: ~TempFileDeleter() { Chris@67: if (tempFile != "") { Chris@67: QFile(tempFile).remove(); Chris@67: } Chris@67: } Chris@67: QString tempFile; Chris@67: }; Chris@67: Chris@97: bool Chris@97: unbundleFile(QString filePath, QString targetPath, bool isExecutable) Chris@97: { Chris@97: SVCERR << "Copying " << filePath.toStdString() << " to " Chris@97: << targetPath.toStdString() << "..." << endl; Chris@97: Chris@97: // This has to be able to work even if the destination exists, and Chris@97: // to do so without deleting it first - e.g. when copying to a Chris@97: // temporary file. So we open the file and copy to it ourselves Chris@97: // rather than use QFile::copy Chris@97: Chris@97: QFile source(filePath); Chris@97: if (!source.open(QFile::ReadOnly)) { Chris@97: SVCERR << "ERROR: Failed to read bundled file " << filePath << endl; Chris@97: return {}; Chris@97: } Chris@97: QByteArray content = source.readAll(); Chris@97: source.close(); Chris@97: Chris@97: QFile target(targetPath); Chris@97: if (!target.open(QFile::WriteOnly)) { Chris@97: SVCERR << "ERROR: Failed to read target file " << targetPath << endl; Chris@97: return {}; Chris@97: } Chris@97: if (target.write(content) != content.size()) { Chris@97: SVCERR << "ERROR: Incomplete write to target file" << endl; Chris@97: return {}; Chris@97: } Chris@97: target.close(); Chris@97: Chris@97: auto permissions = Chris@97: QFile::ReadOwner | QFile::WriteOwner | Chris@97: QFile::ReadGroup | Chris@97: QFile::ReadOther; Chris@97: Chris@97: if (isExecutable) { Chris@97: permissions |= Chris@97: QFile::ExeOwner | Chris@97: QFile::ExeGroup | Chris@97: QFile::ExeOther; Chris@97: }; Chris@97: Chris@97: if (!QFile::setPermissions(targetPath, permissions)) { Chris@97: SVCERR << "Failed to set permissions on " Chris@97: << targetPath.toStdString() << endl; Chris@97: return false; Chris@97: } Chris@97: Chris@97: return true; Chris@97: } Chris@97: Chris@67: map Chris@70: getLibraryPluginVersions(QString libraryFilePath) Chris@67: { Chris@67: static QMutex mutex; Chris@67: static QString tempFileName; Chris@67: static TempFileDeleter deleter; Chris@67: static bool initHappened = false, initSucceeded = false; Chris@67: Chris@67: QMutexLocker locker (&mutex); Chris@67: Chris@67: if (!initHappened) { Chris@67: initHappened = true; Chris@67: Chris@67: QTemporaryFile tempFile; Chris@67: tempFile.setAutoRemove(false); Chris@67: if (!tempFile.open()) { Chris@67: SVCERR << "ERROR: Failed to open a temporary file" << endl; Chris@67: return {}; Chris@67: } Chris@67: Chris@67: // We can't make the QTemporaryFile static, as it will hold Chris@67: // the file open and that prevents us from executing it. Hence Chris@67: // the separate deleter. Chris@67: Chris@67: tempFileName = tempFile.fileName(); Chris@67: deleter.tempFile = tempFileName; Chris@67: Chris@67: #ifdef Q_OS_WIN32 Chris@67: QString helperPath = ":out/get-version.exe"; Chris@67: #else Chris@67: QString helperPath = ":out/get-version"; Chris@97: #endif Chris@97: Chris@97: tempFile.close(); Chris@97: if (!unbundleFile(helperPath, tempFileName, true)) { Chris@97: SVCERR << "ERROR: Failed to unbundle helper code" << endl; Chris@67: return {}; Chris@67: } Chris@67: Chris@67: initSucceeded = true; Chris@67: } Chris@67: Chris@67: if (!initSucceeded) { Chris@67: return {}; Chris@67: } Chris@67: Chris@67: QProcess process; Chris@67: process.start(tempFileName, { libraryFilePath }); Chris@67: Chris@67: if (!process.waitForStarted()) { Chris@67: QProcess::ProcessError err = process.error(); Chris@67: if (err == QProcess::FailedToStart) { Chris@67: SVCERR << "Unable to start helper process " << tempFileName << endl; Chris@67: } else if (err == QProcess::Crashed) { Chris@67: SVCERR << "Helper process " << tempFileName Chris@67: << " crashed on startup" << endl; Chris@67: } else { Chris@67: SVCERR << "Helper process " << tempFileName Chris@67: << " failed on startup with error code " << err << endl; Chris@67: } Chris@67: return {}; Chris@67: } Chris@67: process.waitForFinished(); Chris@67: Chris@67: QByteArray stdOut = process.readAllStandardOutput(); Chris@67: QByteArray stdErr = process.readAllStandardError(); Chris@67: Chris@67: QString errStr = QString::fromUtf8(stdErr); Chris@67: if (!errStr.isEmpty()) { Chris@67: SVCERR << "Note: Helper process stderr follows:" << endl; Chris@67: SVCERR << errStr << endl; Chris@67: SVCERR << "Note: Helper process stderr ends" << endl; Chris@67: } Chris@67: Chris@67: QStringList lines = QString::fromUtf8(stdOut).split Chris@67: (QRegExp("[\\r\\n]+"), QString::SkipEmptyParts); Chris@67: map versions; Chris@67: for (QString line: lines) { Chris@67: QStringList parts = line.split(":"); Chris@67: if (parts.size() != 2) { Chris@67: SVCERR << "Unparseable output line: " << line << endl; Chris@67: continue; Chris@67: } Chris@67: bool ok = false; Chris@67: int version = parts[1].toInt(&ok); Chris@67: if (!ok) { Chris@67: SVCERR << "Unparseable version number in line: " << line << endl; Chris@67: continue; Chris@67: } Chris@67: versions[parts[0]] = version; Chris@67: } Chris@67: Chris@67: return versions; Chris@67: } Chris@67: Chris@97: map Chris@97: getBundledLibraryPluginVersions(QString libraryFileName) Chris@97: { Chris@97: QString tempFileName; Chris@97: TempFileDeleter deleter; Chris@97: Chris@97: { Chris@97: QTemporaryFile tempFile; Chris@97: tempFile.setAutoRemove(false); Chris@97: if (!tempFile.open()) { Chris@97: SVCERR << "ERROR: Failed to open a temporary file" << endl; Chris@97: return {}; Chris@97: } Chris@97: Chris@97: // We can't use QTemporaryFile's auto-remove, as it will hold Chris@97: // the file open and that prevents us from executing it. Hence Chris@97: // the separate deleter. Chris@97: Chris@97: tempFileName = tempFile.fileName(); Chris@97: deleter.tempFile = tempFileName; Chris@97: tempFile.close(); Chris@97: } Chris@97: Chris@97: if (!unbundleFile(":out/" + libraryFileName, tempFileName, true)) { Chris@97: return {}; Chris@97: } Chris@97: Chris@97: return getLibraryPluginVersions(tempFileName); Chris@97: } Chris@97: Chris@67: bool isLibraryNewer(map a, map b) Chris@67: { Chris@67: // a and b are maps from plugin id to plugin version for libraries Chris@67: // A and B. (There is no overarching library version number.) We Chris@67: // deem library A to be newer than library B if: Chris@67: // Chris@67: // 1. A contains a plugin id that is also in B, whose version in Chris@67: // A is newer than that in B, or Chris@67: // Chris@67: // 2. B is not newer than A according to rule 1, and neither A or Chris@67: // B is empty, and A contains a plugin id that is not in B, and B Chris@67: // does not contain any plugin id that is not in A Chris@67: // Chris@67: // (The not-empty part of rule 2 is just to avoid false positives Chris@67: // when a library or its metadata could not be read at all.) Chris@67: Chris@67: auto containsANewerPlugin = [](const map &m1, Chris@67: const map &m2) { Chris@67: for (auto p: m1) { Chris@67: if (m2.find(p.first) != m2.end() && Chris@67: p.second > m2.at(p.first)) { Chris@67: return true; Chris@67: } Chris@67: } Chris@67: return false; Chris@67: }; Chris@67: Chris@67: auto containsANovelPlugin = [](const map &m1, Chris@67: const map &m2) { Chris@67: for (auto p: m1) { Chris@67: if (m2.find(p.first) == m2.end()) { Chris@67: return true; Chris@67: } Chris@67: } Chris@67: return false; Chris@67: }; Chris@67: Chris@67: if (containsANewerPlugin(a, b)) { Chris@67: return true; Chris@67: } Chris@67: Chris@67: if (!containsANewerPlugin(b, a) && Chris@67: !a.empty() && Chris@67: !b.empty() && Chris@67: containsANovelPlugin(a, b) && Chris@67: !containsANovelPlugin(b, a)) { Chris@67: return true; Chris@67: } Chris@67: Chris@67: return false; Chris@67: } Chris@67: Chris@67: QString Chris@67: versionsString(const map &vv) Chris@67: { Chris@67: QStringList pv; Chris@67: for (auto v: vv) { Chris@67: pv.push_back(QString("%1:%2").arg(v.first).arg(v.second)); Chris@67: } Chris@67: return "{ " + pv.join(", ") + " }"; Chris@67: } Chris@67: Chris@75: enum class RelativeStatus { Chris@75: New, Chris@75: Same, Chris@75: Upgrade, Chris@75: Downgrade, Chris@75: TargetNotLoadable Chris@75: }; Chris@75: Chris@75: QString Chris@75: relativeStatusLabel(RelativeStatus status) { Chris@75: switch (status) { Chris@76: case RelativeStatus::New: return QObject::tr("Not yet installed"); Chris@76: case RelativeStatus::Same: return QObject::tr("Already installed"); Chris@76: case RelativeStatus::Upgrade: return QObject::tr("Update"); Chris@76: case RelativeStatus::Downgrade: return QObject::tr("Newer version installed"); Chris@84: case RelativeStatus::TargetNotLoadable: return QObject::tr("Installed version not working"); Chris@79: default: return {}; Chris@75: } Chris@75: } Chris@75: Chris@75: RelativeStatus Chris@75: getRelativeStatus(LibraryInfo info, QString targetDir) Chris@75: { Chris@75: QString destination = targetDir + "/" + info.fileName; Chris@75: Chris@75: SVCERR << "\ngetRelativeStatus: " << info.fileName << ":\n"; Chris@75: Chris@97: if (!QFileInfo(destination).exists()) { Chris@97: SVCERR << " - relative status: " << relativeStatusLabel(RelativeStatus::New) << endl; Chris@97: return RelativeStatus::New; Chris@97: } Chris@75: Chris@97: RelativeStatus status = RelativeStatus::Same; Chris@75: Chris@97: auto packaged = getBundledLibraryPluginVersions(info.fileName); Chris@97: auto installed = getLibraryPluginVersions(destination); Chris@75: Chris@97: SVCERR << " * installed: " << versionsString(installed) Chris@97: << "\n * packaged: " << versionsString(packaged) Chris@97: << endl; Chris@75: Chris@97: if (installed.empty()) { Chris@97: status = RelativeStatus::TargetNotLoadable; Chris@97: } Chris@75: Chris@97: if (isLibraryNewer(installed, packaged)) { Chris@97: status = RelativeStatus::Downgrade; Chris@97: } Chris@75: Chris@97: if (isLibraryNewer(packaged, installed)) { Chris@97: status = RelativeStatus::Upgrade; Chris@75: } Chris@75: Chris@75: SVCERR << " - relative status: " << relativeStatusLabel(status) << endl; Chris@75: Chris@75: return status; Chris@75: } Chris@75: Chris@86: bool Chris@86: backup(QString filePath, QString backupDir) Chris@86: { Chris@86: QFileInfo file(filePath); Chris@86: Chris@86: if (!file.exists()) { Chris@86: return true; Chris@86: } Chris@86: Chris@86: if (!QDir(backupDir).exists()) { Chris@86: QDir().mkpath(backupDir); Chris@86: } Chris@86: Chris@86: QString backup = backupDir + "/" + file.fileName() + ".bak"; Chris@86: SVCERR << "Note: existing file " << filePath Chris@86: << " found, backing up to " << backup << endl; Chris@86: if (!QFile(filePath).rename(backup)) { Chris@86: SVCERR << "Failed to move " << filePath.toStdString() Chris@86: << " to backup " << backup.toStdString() << endl; Chris@86: return false; Chris@86: } Chris@86: Chris@86: return true; Chris@86: } Chris@86: Chris@81: QString Chris@75: installLibrary(LibraryInfo info, QString targetDir) Chris@42: { Chris@75: QString library = info.fileName; Chris@52: QString source = ":out"; Chris@75: QString destination = targetDir + "/" + library; Chris@94: Chris@94: static QString backupDirName; Chris@94: if (backupDirName == "") { Chris@94: // Static so as to be created once - don't go creating a Chris@94: // second directory if the clock ticks over by one second Chris@94: // between library installs Chris@94: backupDirName = Chris@94: QString("saved-%1").arg(QDateTime::currentDateTime().toString Chris@94: ("yyyyMMdd-hhmmss")); Chris@94: } Chris@94: QString backupDir = targetDir + "/" + backupDirName; Chris@67: Chris@86: if (!QDir(targetDir).exists()) { Chris@86: QDir().mkpath(targetDir); Chris@67: } Chris@82: Chris@86: if (!backup(destination, backupDir)) { Chris@86: return QObject::tr("Failed to move aside existing library"); Chris@86: } Chris@97: Chris@97: if (!unbundleFile(source + "/" + library, destination, true)) { Chris@97: return QObject::tr("Failed to copy library file to target directory"); Chris@97: } Chris@52: Chris@52: QString base = QFileInfo(library).baseName(); Chris@52: QDir dir(source); Chris@52: auto entries = dir.entryList({ base + "*" }); Chris@52: for (auto e: entries) { Chris@52: if (e == library) continue; Chris@75: QString destination = targetDir + "/" + e; Chris@86: if (!backup(destination, backupDir)) { Chris@86: continue; Chris@86: } Chris@97: if (!unbundleFile(source + "/" + e, destination, false)) { Chris@68: continue; Chris@52: } Chris@52: } Chris@81: Chris@81: return {}; Chris@42: } Chris@42: Chris@99: QString Chris@99: getHelpText(vector libraries) Chris@99: { Chris@99: set> Chris@99: makers Chris@99: ([](QString k1, QString k2) { Chris@99: return k1.localeAwareCompare(k2) < 0; Chris@99: }); Chris@99: Chris@99: for (auto info: libraries) { Chris@99: makers.insert(info.maker); Chris@99: } Chris@99: Chris@99: QString makerList; Chris@99: for (QString maker: makers) { Chris@99: makerList += QObject::tr("
  • %1
  • ").arg(maker); Chris@99: } Chris@99: Chris@99: return QObject::tr Chris@99: ("

    Vamp Plugin Pack collects together a number of Vamp audio analysis plugins into a single installer.

    " Chris@99: "

    The libraries you select will be installed into the standard Vamp plugin directory, where hosts such as Sonic Visualiser can find them.

    " Chris@99: "

    The plugin libraries included here were developed and published by various different authors and institutions:

      %1
    " Chris@99: "

    All of the libraries are open source and are redistributable under open-source licences. Click the information icon to the right of each library in the main window for more details.

    " Chris@99: "

    The entire pack may be redistributed under the GNU Affero General Public License v3.

    " Chris@99: "

    The plugins were collected together, and the installer was written and published, at the Centre for Digital Music, Queen Mary University of London.

    ") Chris@99: .arg(makerList) Chris@99: .arg(getLicenceURL(Licence::agpl)); Chris@99: } Chris@99: Chris@75: vector Chris@75: getUserApprovedPluginLibraries(vector libraries, Chris@75: QString targetDir) Chris@42: { Chris@42: QDialog dialog; Chris@46: Chris@84: int fontHeight = QFontMetrics(dialog.font()).height(); Chris@84: int dpratio = dialog.devicePixelRatio(); Chris@84: Chris@46: auto mainLayout = new QGridLayout; Chris@47: mainLayout->setSpacing(0); Chris@46: dialog.setLayout(mainLayout); Chris@46: Chris@46: int mainRow = 0; Chris@46: Chris@74: auto selectionFrame = new QWidget; Chris@74: mainLayout->addWidget(selectionFrame, mainRow, 0); Chris@47: ++mainRow; Chris@46: Chris@46: auto selectionLayout = new QGridLayout; Chris@84: selectionLayout->setContentsMargins(0, 0, 0, 0); Chris@84: selectionLayout->setSpacing(fontHeight / 6); Chris@46: selectionFrame->setLayout(selectionLayout); Chris@84: Chris@46: int selectionRow = 0; Chris@84: int checkColumn = 0; Chris@84: int titleColumn = 1; Chris@84: int statusColumn = 2; Chris@85: int infoColumn = 4; // column 3 is a small sliver of spacing Chris@79: Chris@126: QString additionalNote = ""; Chris@126: if (sizeof(char *) == 4) { Chris@126: additionalNote = QObject::tr("(32-bit)"); Chris@126: } Chris@126: Chris@79: selectionLayout->addWidget Chris@126: (new QLabel(QObject::tr("Vamp Plugin Pack v%1 %2") Chris@126: .arg(PACK_VERSION) Chris@126: .arg(additionalNote)), Chris@84: selectionRow, titleColumn, 1, 3); Chris@79: ++selectionRow; Chris@79: Chris@79: selectionLayout->addWidget Chris@79: (new QLabel(QObject::tr("Select the plugin libraries to install:")), Chris@84: selectionRow, titleColumn, 1, 3); Chris@79: ++selectionRow; Chris@74: Chris@74: auto checkAll = new QCheckBox; Chris@74: checkAll->setChecked(true); Chris@84: selectionLayout->addWidget Chris@84: (checkAll, selectionRow, checkColumn, Qt::AlignHCenter); Chris@74: ++selectionRow; Chris@74: Chris@85: auto checkArrow = new QLabel( Chris@85: #ifdef Q_OS_MAC Chris@85: "  ▼" Chris@85: #else Chris@85: "▼" Chris@85: #endif Chris@85: ); Chris@74: checkArrow->setTextFormat(Qt::RichText); Chris@84: selectionLayout->addWidget Chris@84: (checkArrow, selectionRow, checkColumn, Qt::AlignHCenter); Chris@74: ++selectionRow; Chris@42: Chris@67: map checkBoxMap; // filename -> checkbox Chris@67: map libFileInfo; // filename -> info Chris@76: map statuses; // filename -> status Chris@43: Chris@67: map> Chris@49: orderedInfo Chris@49: ([](QString k1, QString k2) { Chris@49: return k1.localeAwareCompare(k2) < 0; Chris@49: }); Chris@43: for (auto info: libraries) { Chris@43: orderedInfo[info.title] = info; Chris@43: } Chris@53: Chris@77: QPixmap infoMap(fontHeight * dpratio, fontHeight * dpratio); Chris@77: QPixmap moreMap(fontHeight * dpratio * 2, fontHeight * dpratio * 2); Chris@74: infoMap.fill(Qt::transparent); Chris@74: moreMap.fill(Qt::transparent); Chris@74: QSvgRenderer renderer(QString(":icons/scalable/info.svg")); Chris@74: QPainter painter; Chris@74: painter.begin(&infoMap); Chris@74: renderer.render(&painter); Chris@74: painter.end(); Chris@74: painter.begin(&moreMap); Chris@74: renderer.render(&painter); Chris@74: painter.end(); Chris@74: Chris@76: auto shouldCheck = [](RelativeStatus status) { Chris@76: return (status == RelativeStatus::New || Chris@76: status == RelativeStatus::Upgrade || Chris@76: status == RelativeStatus::TargetNotLoadable); Chris@76: }; Chris@76: Chris@43: for (auto ip: orderedInfo) { Chris@46: Chris@46: auto cb = new QCheckBox; Chris@84: selectionLayout->addWidget Chris@84: (cb, selectionRow, checkColumn, Qt::AlignHCenter); Chris@46: Chris@43: LibraryInfo info = ip.second; Chris@72: Chris@76: auto shortLabel = new QLabel(info.title); Chris@84: selectionLayout->addWidget(shortLabel, selectionRow, titleColumn); Chris@76: Chris@75: RelativeStatus relativeStatus = getRelativeStatus(info, targetDir); Chris@76: auto statusLabel = new QLabel(relativeStatusLabel(relativeStatus)); Chris@84: selectionLayout->addWidget(statusLabel, selectionRow, statusColumn); Chris@76: cb->setChecked(shouldCheck(relativeStatus)); Chris@75: Chris@79: auto infoButton = new QToolButton; Chris@79: infoButton->setAutoRaise(true); Chris@79: infoButton->setIcon(infoMap); Chris@79: infoButton->setIconSize(QSize(fontHeight, fontHeight)); Chris@77: Chris@77: #ifdef Q_OS_MAC Chris@79: infoButton->setFixedSize(QSize(int(fontHeight * 1.2), Chris@77: int(fontHeight * 1.2))); Chris@79: infoButton->setStyleSheet("QToolButton { border: none; }"); Chris@77: #endif Chris@77: Chris@84: selectionLayout->addWidget(infoButton, selectionRow, infoColumn); Chris@46: Chris@46: ++selectionRow; Chris@46: Chris@77: QString moreTitleText = QObject::tr("%1
    %2") Chris@72: .arg(info.title) Chris@77: .arg(info.maker); Chris@77: Chris@79: QString moreInfoText = info.description; Chris@79: Chris@79: if (info.page != "") { Chris@79: moreInfoText += QObject::tr("
    %2") Chris@79: .arg(info.page) Chris@79: .arg(info.page); Chris@79: } Chris@79: Chris@79: moreInfoText += QObject::tr("

    Library contains:
      "); Chris@73: Chris@73: int n = 0; Chris@73: bool closed = false; Chris@73: for (auto title: info.pluginTitles) { Chris@73: if (n == 10 && info.pluginTitles.size() > 15) { Chris@77: moreInfoText += QObject::tr("
    "); Chris@77: moreInfoText += QObject::tr("... and %n other plugins.

    ", Chris@77: "", Chris@77: info.pluginTitles.size() - n); Chris@73: closed = true; Chris@73: break; Chris@73: } Chris@77: moreInfoText += QObject::tr("
  • %1
  • ").arg(title); Chris@73: ++n; Chris@73: } Chris@73: Chris@73: if (!closed) { Chris@77: moreInfoText += QObject::tr(""); Chris@73: } Chris@74: Chris@74: if (info.licence != "") { Chris@99: moreInfoText += QObject::tr("Provided under the %2.
    ") Chris@99: .arg(getLicenceURL(info.licence)) Chris@77: .arg(info.licence); Chris@74: } Chris@72: Chris@79: QObject::connect(infoButton, &QAbstractButton::clicked, Chris@72: [=]() { Chris@74: QMessageBox mbox; Chris@74: mbox.setIconPixmap(moreMap); Chris@74: mbox.setWindowTitle(QObject::tr("Library contents")); Chris@77: mbox.setText(moreTitleText); Chris@77: mbox.setInformativeText(moreInfoText); Chris@74: mbox.exec(); Chris@72: }); Chris@72: Chris@43: checkBoxMap[info.fileName] = cb; Chris@67: libFileInfo[info.fileName] = info; Chris@76: statuses[info.fileName] = relativeStatus; Chris@42: } Chris@42: Chris@79: selectionLayout->addItem(new QSpacerItem(1, (fontHeight*2) / 3), Chris@79: selectionRow, 0); Chris@79: ++selectionRow; Chris@79: Chris@79: selectionLayout->addWidget Chris@79: (new QLabel(QObject::tr("Installation will be to: %1").arg(targetDir)), Chris@84: selectionRow, titleColumn, 1, 3); Chris@79: ++selectionRow; Chris@79: Chris@47: QObject::connect(checkAll, &QCheckBox::toggled, Chris@72: [=](bool toCheck) { Chris@47: for (auto p: checkBoxMap) { Chris@47: p.second->setChecked(toCheck); Chris@47: } Chris@47: }); Chris@79: Chris@79: mainLayout->addItem(new QSpacerItem(1, fontHeight), mainRow, 0); Chris@79: ++mainRow; Chris@79: Chris@42: auto bb = new QDialogButtonBox(QDialogButtonBox::Ok | Chris@76: QDialogButtonBox::Cancel | Chris@99: QDialogButtonBox::Reset | Chris@99: QDialogButtonBox::Help); Chris@79: bb->button(QDialogButtonBox::Ok)->setText(QObject::tr("Install")); Chris@74: mainLayout->addWidget(bb, mainRow, 0); Chris@46: ++mainRow; Chris@47: Chris@74: mainLayout->setRowStretch(0, 10); Chris@74: mainLayout->setColumnStretch(0, 10); Chris@74: selectionLayout->setColumnMinimumWidth(0, 50); Chris@85: #ifdef Q_OS_MAC Chris@85: selectionLayout->setColumnMinimumWidth(3, 10); Chris@85: selectionLayout->setColumnMinimumWidth(5, 12); Chris@85: #endif Chris@74: selectionLayout->setColumnStretch(1, 10); Chris@47: Chris@76: QObject::connect Chris@76: (bb, &QDialogButtonBox::clicked, Chris@76: [&](QAbstractButton *button) { Chris@76: Chris@76: auto role = bb->buttonRole(button); Chris@76: Chris@76: switch (role) { Chris@76: Chris@76: case QDialogButtonBox::AcceptRole: { Chris@76: bool downgrade = false; Chris@76: for (const auto &p: checkBoxMap) { Chris@76: if (p.second->isChecked() && Chris@76: statuses.at(p.first) == RelativeStatus::Downgrade) { Chris@76: downgrade = true; Chris@76: break; Chris@76: } Chris@76: } Chris@76: if (downgrade) { Chris@76: if (QMessageBox::warning Chris@76: (bb, QObject::tr("Downgrade?"), Chris@76: QObject::tr("You have asked to downgrade one or more plugin libraries that are already installed.

    Are you sure?"), Chris@76: QMessageBox::Ok | QMessageBox::Cancel, Chris@76: QMessageBox::Cancel) == QMessageBox::Ok) { Chris@76: dialog.accept(); Chris@76: } Chris@76: } else { Chris@76: dialog.accept(); Chris@76: } Chris@76: break; Chris@76: } Chris@76: Chris@76: case QDialogButtonBox::RejectRole: Chris@76: dialog.reject(); Chris@76: break; Chris@76: Chris@76: case QDialogButtonBox::ResetRole: Chris@76: for (const auto &p: checkBoxMap) { Chris@76: p.second->setChecked(shouldCheck(statuses.at(p.first))); Chris@76: } Chris@76: break; Chris@76: Chris@99: case QDialogButtonBox::HelpRole: { Chris@99: QMessageBox mbox; Chris@99: mbox.setWindowTitle(QApplication::applicationName()); Chris@99: mbox.setText(QObject::tr("Vamp Plugin Pack")); Chris@99: mbox.setInformativeText(getHelpText(libraries)); Chris@99: mbox.exec(); Chris@99: break; Chris@99: } Chris@99: Chris@76: default: Chris@76: SVCERR << "WARNING: Unexpected role " << role << endl; Chris@99: break; Chris@76: } Chris@76: }); Chris@98: Chris@98: if (QString(PACK_VERSION).contains("-pre") || Chris@98: QString(PACK_VERSION).contains("-alpha") || Chris@98: QString(PACK_VERSION).contains("-beta")) { Chris@98: QTimer::singleShot Chris@98: (500, [&]() { Chris@98: QString url = "https://code.soundsoftware.ac.uk/projects/vamp-plugin-pack"; Chris@98: QMessageBox::information Chris@98: (&dialog, QObject::tr("Test release"), Chris@98: QObject::tr("This is a test release of %1

    Please send any feedback to the developers. See %3 for more information.

    ").arg(QApplication::applicationName()).arg(url).arg(url)); Chris@98: }); Chris@98: } Chris@98: Chris@70: if (dialog.exec() != QDialog::Accepted) { Chris@51: SVCERR << "rejected" << endl; Chris@70: return {}; Chris@42: } Chris@42: Chris@75: vector approved; Chris@42: for (const auto &p: checkBoxMap) { Chris@42: if (p.second->isChecked()) { Chris@75: approved.push_back(libFileInfo[p.first]); Chris@33: } Chris@42: } Chris@42: Chris@42: return approved; Chris@42: } Chris@42: Chris@42: int main(int argc, char **argv) Chris@42: { Chris@93: if (argc == 2 && (QString(argv[1]) == "--version" || Chris@93: QString(argv[1]) == "-v")) { Chris@95: cerr << PACK_VERSION << std::endl; // std:: needed here for MSVC for some reason Chris@93: exit(0); Chris@93: } Chris@93: Chris@42: QApplication app(argc, argv); Chris@42: Chris@51: QApplication::setOrganizationName("sonic-visualiser"); Chris@51: QApplication::setOrganizationDomain("sonicvisualiser.org"); Chris@51: QApplication::setApplicationName(QApplication::tr("Vamp Plugin Pack Installer")); Chris@51: Chris@77: QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); Chris@77: Chris@51: #ifdef Q_OS_WIN32 Chris@51: QFont font(QApplication::font()); Chris@51: QString preferredFamily = "Segoe UI"; Chris@51: font.setFamily(preferredFamily); Chris@51: if (QFontInfo(font).family() == preferredFamily) { Chris@51: font.setPointSize(10); Chris@51: QApplication::setFont(font); Chris@51: } Chris@77: #else Chris@77: #ifdef Q_OS_MAC Chris@77: QFont font(QApplication::font()); Chris@77: QString preferredFamily = "Lucida Grande"; Chris@77: font.setFamily(preferredFamily); Chris@77: if (QFontInfo(font).family() == preferredFamily) { Chris@85: font.setPointSize(12); Chris@77: QApplication::setFont(font); Chris@77: } Chris@77: #endif Chris@51: #endif Chris@51: Chris@42: QString target = getDefaultInstallDirectory(); Chris@42: if (target == "") { Chris@42: return 1; Chris@42: } Chris@42: Chris@42: QStringList libraries = getPluginLibraryList(); Chris@42: Chris@43: auto rdfStore = loadLibrariesRdf(); Chris@43: Chris@43: auto info = getLibraryInfo(*rdfStore, libraries); Chris@43: Chris@75: vector toInstall = Chris@75: getUserApprovedPluginLibraries(info, target); Chris@83: Chris@100: if (toInstall.empty()) { // Cancelled, or nothing selected Chris@100: SVCERR << "No libraries selected for installation, nothing to do" Chris@100: << endl; Chris@100: return 0; Chris@100: } Chris@100: Chris@81: QProgressDialog progress(QObject::tr("Installing..."), Chris@95: QObject::tr("Stop"), 0, Chris@95: int(toInstall.size()) + 1); Chris@81: progress.setMinimumDuration(0); Chris@99: Chris@81: int pval = 0; Chris@81: bool complete = true; Chris@42: Chris@42: for (auto lib: toInstall) { Chris@81: progress.setValue(++pval); Chris@81: QThread::currentThread()->msleep(40); Chris@81: app.processEvents(); Chris@81: if (progress.wasCanceled()) { Chris@81: complete = false; Chris@81: break; Chris@81: } Chris@81: QString error = installLibrary(lib, target); Chris@81: if (error != "") { Chris@81: complete = false; Chris@81: if (QMessageBox::critical Chris@81: (&progress, Chris@81: QObject::tr("Install failed"), Chris@81: QObject::tr("Failed to install library \"%1\": %2") Chris@81: .arg(lib.title) Chris@81: .arg(error), Chris@81: QMessageBox::Abort | QMessageBox::Ignore, Chris@81: QMessageBox::Ignore) == Chris@81: QMessageBox::Abort) { Chris@81: break; Chris@81: } Chris@81: } Chris@81: } Chris@81: Chris@81: progress.hide(); Chris@81: Chris@81: if (complete) { Chris@81: QMessageBox::information Chris@81: (&progress, Chris@81: QObject::tr("Complete"), Chris@81: QObject::tr("Installation completed successfully"), Chris@81: QMessageBox::Ok, Chris@81: QMessageBox::Ok); Chris@81: } else { Chris@81: QMessageBox::information Chris@81: (&progress, Chris@81: QObject::tr("Incomplete"), Chris@81: QObject::tr("Installation was not complete. Exiting"), Chris@81: QMessageBox::Ok, Chris@81: QMessageBox::Ok); Chris@33: } Chris@33: Chris@83: return (complete ? 0 : 2); Chris@32: }