changeset 67:38cd115c91d4

Integrate version checker into installer program & build, + win32 signing bits
author Chris Cannam
date Thu, 13 Feb 2020 10:56:15 +0000
parents 85768d48e6ce
children d70c50c4a07c
files deploy/linux/generate-qrc deploy/osx/generate-qrc deploy/win64/build-64.bat deploy/win64/generate-qrc.bat get-version.cpp get-version.pro installer.cpp installer.qrc.in
diffstat 8 files changed, 355 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- a/deploy/linux/generate-qrc	Wed Feb 12 14:41:09 2020 +0000
+++ b/deploy/linux/generate-qrc	Thu Feb 13 10:56:15 2020 +0000
@@ -5,4 +5,4 @@
     exit 2
 fi
 set -eu
-cat "$qrc".in | sed 's/@SUFFIX@/so/g' > "$qrc"
+cat "$qrc".in | sed -e 's/@SUFFIX@/.so/g' -e 's/@EXESUFFIX@//g' > "$qrc"
--- a/deploy/osx/generate-qrc	Wed Feb 12 14:41:09 2020 +0000
+++ b/deploy/osx/generate-qrc	Thu Feb 13 10:56:15 2020 +0000
@@ -5,4 +5,4 @@
     exit 2
 fi
 set -eu
-cat "$qrc".in | sed 's/@SUFFIX@/dylib/g' > "$qrc"
+cat "$qrc".in | sed -e 's/@SUFFIX@/.dylib/g' -e 's/@EXESUFFIX@//g' > "$qrc"
--- a/deploy/win64/build-64.bat	Wed Feb 12 14:41:09 2020 +0000
+++ b/deploy/win64/build-64.bat	Thu Feb 13 10:56:15 2020 +0000
@@ -18,10 +18,34 @@
 @   exit /b 2
 )
 
+set SMLNJDIR=C:\Program Files (x86)\SMLNJ
+if not exist "%SMLNJDIR%\bin" (
+@   echo Could not find SML/NJ, required for Repoint
+@   exit /b 2
+)
+
 call %vcvarsall% amd64
 
 set ORIGINALPATH=%PATH%
-set PATH=%PATH%;C:\Program Files (x86)\SMLNJ\bin;%QTDIR%\bin
+set PATH=%PATH%;%SMLNJDIR%\bin;%QTDIR%\bin
+set NAME=Open Source Developer, Christopher Cannam
+
+set ARG=%1
+shift
+if "%ARG%" == "sign" (
+@   echo NOTE: sign option specified, will attempt to codesign exe and msi
+@   echo NOTE: starting by codesigning an unrelated executable, so we know
+@   echo NOTE: whether it'll work before doing the entire build
+copy "%SMLNJDIR%\bin\.run\run.x86-win32.exe" signtest.exe
+signtool sign /v /n "%NAME%" /t http://time.certum.pl /fd sha1 /a signtest.exe
+if errorlevel 1 exit /b %errorlevel%
+signtool verify /pa signtest.exe
+if errorlevel 1 exit /b %errorlevel%
+del signtest.exe
+@   echo NOTE: success
+) else (
+@   echo NOTE: sign option not specified, will not codesign anything
+)
 
 cd %STARTPWD%
 
@@ -40,13 +64,22 @@
 qmake -spec win32-msvc -r -tp vc ..\plugins.pro
 if %errorlevel% neq 0 exit /b %errorlevel%
 
+mkdir o
+
+msbuild get-version.vcxproj /t:Build /p:Configuration=Release
+if %errorlevel% neq 0 exit /b %errorlevel%
+copy release\out\get-version.exe ..\out\
+
 msbuild plugins.sln /t:Build /p:Configuration=Release
 if %errorlevel% neq 0 exit /b %errorlevel%
-
-rem and sign!
 copy release\out\*.dll ..\out\
 
-mkdir o
+if "%ARG%" == "sign" (
+@echo Signing plugins and version helper
+signtool sign /v /n "%NAME%" /t http://time.certum.pl /fd sha1 /a ..\out\*.dll ..\out\*.exe
+signtool verify /pa ..\out\*.dll ..\out\*.exe
+)
+
 %QTDIR%\bin\rcc ..\installer.qrc -o o\qrc_installer.cpp
 
 qmake -spec win32-msvc -r -tp vc ..\installer.pro
@@ -66,6 +99,14 @@
 copy %QTDIR%\plugins\platforms\qwindows.dll .\release
 copy %QTDIR%\plugins\styles\qwindowsvistastyle.dll .\release
 
+if "%ARG%" == "sign" (
+@echo Signing application
+signtool sign /v /n "%NAME%" /t http://time.certum.pl /fd sha1 /a release\*.exe release\*.dll
+signtool verify /pa "release\Vamp Plugin Pack Installer.exe"
+)
+
 cd ..
 
+rem Now what?
+
 set PATH=%ORIGINALPATH%
--- a/deploy/win64/generate-qrc.bat	Wed Feb 12 14:41:09 2020 +0000
+++ b/deploy/win64/generate-qrc.bat	Thu Feb 13 10:56:15 2020 +0000
@@ -13,4 +13,4 @@
 
 @echo on
 
-powershell -Command "(Get-Content %IN%) -replace '@SUFFIX@', 'dll' | Out-File -encoding ASCII %QRC%"
+powershell -Command "(Get-Content %IN%) -replace '@SUFFIX@', '.dll' -replace '@EXESUFFIX@', '.exe' | Out-File -encoding ASCII %QRC%"
--- a/get-version.cpp	Wed Feb 12 14:41:09 2020 +0000
+++ b/get-version.cpp	Thu Feb 13 10:56:15 2020 +0000
@@ -32,6 +32,16 @@
 // Undocumented internal API
 #include <vamp-hostsdk/../src/vamp-hostsdk/Files.h>
 
+#ifdef _WIN32
+#include <windows.h>
+#include <process.h>
+#include <io.h>
+#else
+#include <unistd.h>
+#endif
+
+#include <fcntl.h>
+
 #include <iostream>
 #include <string>
 
@@ -59,6 +69,49 @@
     exit(2);
 }
 
+// We write our output to stdout, but want to ensure that the plugin
+// doesn't write anything itself. To do this we open a null file
+// descriptor and dup2() it into place of stdout in the gaps between
+// our own output activity.
+
+static int normalFd = -1;
+static int suspendedFd = -1;
+
+static void initFds()
+{
+#ifdef _WIN32
+    normalFd = _dup(1);
+    suspendedFd = _open("NUL", _O_WRONLY);
+#else
+    normalFd = dup(1);
+    suspendedFd = open("/dev/null", O_WRONLY);
+#endif
+    
+    if (normalFd < 0 || suspendedFd < 0) {
+        throw std::runtime_error
+            ("Failed to initialise fds for stdio suspend/resume");
+    }
+}
+
+static void suspendOutput()
+{
+#ifdef _WIN32
+    _dup2(suspendedFd, 1);
+#else
+    dup2(suspendedFd, 1);
+#endif
+}
+
+static void resumeOutput()
+{
+    fflush(stdout);
+#ifdef _WIN32
+    _dup2(normalFd, 1);
+#else
+    dup2(normalFd, 1);
+#endif
+}
+
 int main(int argc, char **argv)
 {
     char *scooter = argv[0];
@@ -72,6 +125,9 @@
     if (argc != 2) usage(name);
     if (string(argv[1]) == string("-?")) usage(name);
 
+    initFds();
+    suspendOutput();
+    
     string libraryPath(argv[1]);
     
     void *handle = Files::loadLibrary(libraryPath);
@@ -95,8 +151,10 @@
     const VampPluginDescriptor *descriptor = 0;
 
     while ((descriptor = fn(VAMP_API_VERSION, index))) {
+        resumeOutput();
         cout << descriptor->identifier << ":"
              << descriptor->pluginVersion << endl;
+        suspendOutput();
         ++index;
     }
 
--- a/get-version.pro	Wed Feb 12 14:41:09 2020 +0000
+++ b/get-version.pro	Thu Feb 13 10:56:15 2020 +0000
@@ -13,7 +13,7 @@
 
 QT += gui widgets svg
 
-TARGET=get-version
+TARGET=out/get-version
 
 OBJECTS_DIR = o
 MOC_DIR = o
--- a/installer.cpp	Wed Feb 12 14:41:09 2020 +0000
+++ b/installer.cpp	Thu Feb 13 10:56:15 2020 +0000
@@ -41,6 +41,10 @@
 #include <QLabel>
 #include <QFont>
 #include <QFontInfo>
+#include <QTemporaryFile>
+#include <QMutex>
+#include <QMutexLocker>
+#include <QProcess>
 
 #include <vamp-hostsdk/PluginHostAdapter.h>
 
@@ -51,6 +55,8 @@
 #include <memory>
 #include <set>
 
+#include <unistd.h>
+
 #include "base/Debug.h"
 
 using namespace std;
@@ -129,6 +135,7 @@
     QString maker;
     QString description;
     QStringList pluginTitles;
+    map<QString, int> pluginVersions; // id -> version
 };
 
 vector<LibraryInfo>
@@ -145,7 +152,7 @@
                                     Uri("a"),
                                     store.expand("vamp:PluginLibrary")));
 
-    std::map<QString, QString> wanted; // basename -> full lib name
+    map<QString, QString> wanted; // basename -> full lib name
     for (auto lib: libraries) {
         wanted[QFileInfo(lib).baseName()] = lib;
     }
@@ -208,6 +215,21 @@
             if (ptitle.type == Node::Literal) {
                 info.pluginTitles.push_back(ptitle.value);
             }
+
+            Node pident = store.complete(Triple(p.object(),
+                                                store.expand("vamp:identifier"),
+                                                Node()));
+            Node pversion = store.complete(Triple(p.object(),
+                                                  store.expand("owl:versionInfo"),
+                                                  Node()));
+            if (pident.type == Node::Literal &&
+                pversion.type == Node::Literal) {
+                bool ok = false;
+                int version = pversion.value.toInt(&ok);
+                if (ok) {
+                    info.pluginVersions[pident.value] = version;
+                }
+            }
         }
         
         results.push_back(info);
@@ -222,12 +244,204 @@
     return results;
 }
 
+struct TempFileDeleter {
+    ~TempFileDeleter() {
+        if (tempFile != "") {
+            QFile(tempFile).remove();
+        }
+    }
+    QString tempFile;
+};
+
+map<QString, int>
+getInstalledLibraryPluginVersions(QString libraryFilePath)
+{
+    static QMutex mutex;
+    static QString tempFileName;
+    static TempFileDeleter deleter;
+    static bool initHappened = false, initSucceeded = false;
+
+    QMutexLocker locker (&mutex);
+
+    if (!initHappened) {
+        initHappened = true;
+
+        QTemporaryFile tempFile;
+        tempFile.setAutoRemove(false);
+        if (!tempFile.open()) {
+            SVCERR << "ERROR: Failed to open a temporary file" << endl;
+            return {};
+        }
+
+        // We can't make the QTemporaryFile static, as it will hold
+        // the file open and that prevents us from executing it. Hence
+        // the separate deleter.
+        
+        tempFileName = tempFile.fileName();
+        deleter.tempFile = tempFileName;
+        
+#ifdef Q_OS_WIN32
+        QString helperPath = ":out/get-version.exe";
+#else
+        QString helperPath = ":out/get-version";
+#endif        
+        QFile helper(helperPath);
+        if (!helper.open(QFile::ReadOnly)) {
+            SVCERR << "ERROR: Failed to read helper code" << endl;
+            return {};
+        }
+        QByteArray content = helper.readAll();
+        helper.close();
+
+        if (tempFile.write(content) != content.size()) {
+            SVCERR << "ERROR: Incomplete write to temporary file" << endl;
+            return {};
+        }
+        tempFile.close();
+
+        if (!QFile::setPermissions
+            (tempFileName,
+             QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) {
+            SVCERR << "ERROR: Failed to set execute permission on helper "
+                   << tempFileName << endl;
+            return {};
+        }
+        
+        initSucceeded = true;
+    }
+
+    if (!initSucceeded) {
+        return {};
+    }
+
+    QProcess process;
+    process.start(tempFileName, { libraryFilePath });
+
+    if (!process.waitForStarted()) {
+        QProcess::ProcessError err = process.error();
+        if (err == QProcess::FailedToStart) {
+            SVCERR << "Unable to start helper process " << tempFileName << endl;
+        } else if (err == QProcess::Crashed) {
+            SVCERR << "Helper process " << tempFileName
+                   << " crashed on startup" << endl;
+        } else {
+            SVCERR << "Helper process " << tempFileName
+                   << " failed on startup with error code " << err << endl;
+        }
+        return {};
+    }
+    process.waitForFinished();
+
+    QByteArray stdOut = process.readAllStandardOutput();
+    QByteArray stdErr = process.readAllStandardError();
+
+    QString errStr = QString::fromUtf8(stdErr);
+    if (!errStr.isEmpty()) {
+        SVCERR << "Note: Helper process stderr follows:" << endl;
+        SVCERR << errStr << endl;
+        SVCERR << "Note: Helper process stderr ends" << endl;
+    }
+
+    QStringList lines = QString::fromUtf8(stdOut).split
+        (QRegExp("[\\r\\n]+"), QString::SkipEmptyParts);
+    map<QString, int> versions;
+    for (QString line: lines) {
+        QStringList parts = line.split(":");
+        if (parts.size() != 2) {
+            SVCERR << "Unparseable output line: " << line << endl;
+            continue;
+        }
+        bool ok = false;
+        int version = parts[1].toInt(&ok);
+        if (!ok) {
+            SVCERR << "Unparseable version number in line: " << line << endl;
+            continue;
+        }
+        versions[parts[0]] = version;
+    }
+
+    return versions;
+}
+
+bool isLibraryNewer(map<QString, int> a, map<QString, int> b)
+{
+    // a and b are maps from plugin id to plugin version for libraries
+    // A and B. (There is no overarching library version number.) We
+    // deem library A to be newer than library B if:
+    // 
+    // 1. A contains a plugin id that is also in B, whose version in
+    // A is newer than that in B, or
+    //
+    // 2. B is not newer than A according to rule 1, and neither A or
+    // B is empty, and A contains a plugin id that is not in B, and B
+    // does not contain any plugin id that is not in A
+    //
+    // (The not-empty part of rule 2 is just to avoid false positives
+    // when a library or its metadata could not be read at all.)
+
+    auto containsANewerPlugin = [](const map<QString, int> &m1,
+                                   const map<QString, int> &m2) {
+                                    for (auto p: m1) {
+                                        if (m2.find(p.first) != m2.end() &&
+                                            p.second > m2.at(p.first)) {
+                                            return true;
+                                        }
+                                    }
+                                    return false;
+                                };
+
+    auto containsANovelPlugin = [](const map<QString, int> &m1,
+                                   const map<QString, int> &m2) {
+                                    for (auto p: m1) {
+                                        if (m2.find(p.first) == m2.end()) {
+                                            return true;
+                                        }
+                                    }
+                                    return false;
+                                };
+
+    if (containsANewerPlugin(a, b)) {
+        return true;
+    }
+    
+    if (!containsANewerPlugin(b, a) &&
+        !a.empty() &&
+        !b.empty() &&
+        containsANovelPlugin(a, b) &&
+        !containsANovelPlugin(b, a)) {
+        return true;
+    }
+
+    return false;
+}
+
+QString
+versionsString(const map<QString, int> &vv)
+{
+    QStringList pv;
+    for (auto v: vv) {
+        pv.push_back(QString("%1:%2").arg(v.first).arg(v.second));
+    }
+    return "{ " + pv.join(", ") + " }";
+}
+
 void
-installLibrary(QString library, QString target)
+installLibrary(QString library, LibraryInfo info, QString target)
 {
     QString source = ":out";
     QFile f(source + "/" + library);
     QString destination = target + "/" + library;
+
+    if (QFileInfo(destination).exists()) {
+        auto installed = getInstalledLibraryPluginVersions(destination);
+        SVCERR << "Note: comparing installed plugin versions "
+               << versionsString(installed)
+               << " to packaged versions "
+               << versionsString(info.pluginVersions)
+               << ": isLibraryNewer(installed, packaged) returns "
+               << isLibraryNewer(installed, info.pluginVersions)
+               << endl;
+    }
     
     SVCERR << "Copying " << library.toStdString() << " to "
            << destination.toStdString() << "..." << endl;
@@ -262,7 +476,7 @@
     }
 }
 
-QStringList
+map<QString, LibraryInfo>
 getUserApprovedPluginLibraries(vector<LibraryInfo> libraries)
 {
     QDialog dialog;
@@ -295,9 +509,10 @@
     selectionFrame->setLayout(selectionLayout);
     int selectionRow = 0;
 
-    map<QString, QCheckBox *> checkBoxMap;
+    map<QString, QCheckBox *> checkBoxMap; // filename -> checkbox
+    map<QString, LibraryInfo> libFileInfo; // filename -> info
 
-    map<QString, LibraryInfo, std::function<bool (QString, QString)>>
+    map<QString, LibraryInfo, function<bool (QString, QString)>>
         orderedInfo
         ([](QString k1, QString k2) {
              return k1.localeAwareCompare(k2) < 0;
@@ -346,6 +561,7 @@
         ++selectionRow;
 
         checkBoxMap[info.fileName] = cb;
+        libFileInfo[info.fileName] = info;
     }
 
     scroll->setWidget(selectionFrame);
@@ -379,10 +595,10 @@
         SVCERR << "rejected" << endl;
     }
 
-    QStringList approved;
+    map<QString, LibraryInfo> approved;
     for (const auto &p: checkBoxMap) {
         if (p.second->isChecked()) {
-            approved.push_back(p.first);
+            approved[p.first] = libFileInfo[p.first];
         }
     }
     
@@ -418,7 +634,7 @@
 
     auto info = getLibraryInfo(*rdfStore, libraries);
     
-    QStringList toInstall = getUserApprovedPluginLibraries(info);
+    map<QString, LibraryInfo> toInstall = getUserApprovedPluginLibraries(info);
 
     if (!toInstall.empty()) {
         if (!QDir(target).exists()) {
@@ -427,7 +643,7 @@
     }
     
     for (auto lib: toInstall) {
-        installLibrary(lib, target);
+        installLibrary(lib.first, lib.second, target);
     }
     
     return 0;
--- a/installer.qrc.in	Wed Feb 12 14:41:09 2020 +0000
+++ b/installer.qrc.in	Thu Feb 13 10:56:15 2020 +0000
@@ -1,114 +1,115 @@
 <!DOCTYPE RCC><RCC version="1.0">
 <qresource>
+  <file>out/get-version@EXESUFFIX@</file>
   <file>out/azi.cat</file>
   <file>out/azi_COPYING.txt</file>
-  <file>out/azi.@SUFFIX@</file>
+  <file>out/azi@SUFFIX@</file>
   <file>out/bbc-vamp-plugins.cat</file>
   <file>out/bbc-vamp-plugins_COPYING.txt</file>
   <file>out/bbc-vamp-plugins.n3</file>
   <file>out/bbc-vamp-plugins_README.md</file>
-  <file>out/bbc-vamp-plugins.@SUFFIX@</file>
+  <file>out/bbc-vamp-plugins@SUFFIX@</file>
   <file>out/beatroot-vamp.cat</file>
   <file>out/beatroot-vamp_CITATION.txt</file>
   <file>out/beatroot-vamp_COPYING.txt</file>
   <file>out/beatroot-vamp.n3</file>
   <file>out/beatroot-vamp_README.txt</file>
-  <file>out/beatroot-vamp.@SUFFIX@</file>
+  <file>out/beatroot-vamp@SUFFIX@</file>
   <file>out/cepstral-pitchtracker.cat</file>
   <file>out/cepstral-pitchtracker.n3</file>
   <file>out/cepstral-pitchtracker_README.txt</file>
-  <file>out/cepstral-pitchtracker.@SUFFIX@</file>
+  <file>out/cepstral-pitchtracker@SUFFIX@</file>
   <file>out/cqvamp.cat</file>
   <file>out/cqvamp_CITATION.txt</file>
   <file>out/cqvamp_COPYING.txt</file>
   <file>out/cqvamp.n3</file>
   <file>out/cqvamp_README.txt</file>
-  <file>out/cqvamp.@SUFFIX@</file>
+  <file>out/cqvamp@SUFFIX@</file>
   <file>out/fanchirp.cat</file>
   <file>out/fanchirp_CITATION.txt</file>
   <file>out/fanchirp_COPYING.txt</file>
   <file>out/fanchirp_README.md</file>
-  <file>out/fanchirp.@SUFFIX@</file>
+  <file>out/fanchirp@SUFFIX@</file>
   <file>out/match-vamp-plugin.cat</file>
   <file>out/match-vamp-plugin_CITATION.txt</file>
   <file>out/match-vamp-plugin_COPYING.txt</file>
   <file>out/match-vamp-plugin.n3</file>
   <file>out/match-vamp-plugin_README.txt</file>
-  <file>out/match-vamp-plugin.@SUFFIX@</file>
+  <file>out/match-vamp-plugin@SUFFIX@</file>
   <file>out/mvamp.cat</file>
   <file>out/mvamp_COPYING.txt</file>
   <file>out/mvamp.n3</file>
   <file>out/mvamp_README.txt</file>
-  <file>out/mvamp.@SUFFIX@</file>
+  <file>out/mvamp@SUFFIX@</file>
   <file>out/nnls-chroma.cat</file>
   <file>out/nnls-chroma_CITATION.txt</file>
   <file>out/nnls-chroma_COPYING.txt</file>
   <file>out/nnls-chroma.n3</file>
   <file>out/nnls-chroma_README.txt</file>
-  <file>out/nnls-chroma.@SUFFIX@</file>
+  <file>out/nnls-chroma@SUFFIX@</file>
   <file>out/pyin.cat</file>
   <file>out/pyin_COPYING.txt</file>
   <file>out/pyin.n3</file>
   <file>out/pyin_README.txt</file>
-  <file>out/pyin.@SUFFIX@</file>
+  <file>out/pyin@SUFFIX@</file>
   <file>out/qm-vamp-plugins.cat</file>
   <file>out/qm-vamp-plugins_COPYING.txt</file>
   <file>out/qm-vamp-plugins.n3</file>
   <file>out/qm-vamp-plugins_README.md</file>
-  <file>out/qm-vamp-plugins.@SUFFIX@</file>
+  <file>out/qm-vamp-plugins@SUFFIX@</file>
   <file>out/segmentino.cat</file>
   <file>out/segmentino_CITATION.txt</file>
   <file>out/segmentino_COPYING.txt</file>
   <file>out/segmentino.n3</file>
   <file>out/segmentino_README.txt</file>
-  <file>out/segmentino.@SUFFIX@</file>
+  <file>out/segmentino@SUFFIX@</file>
   <file>out/silvet.cat</file>
   <file>out/silvet_CITATION.txt</file>
   <file>out/silvet_COPYING.txt</file>
   <file>out/silvet.n3</file>
   <file>out/silvet_README.txt</file>
-  <file>out/silvet.@SUFFIX@</file>
+  <file>out/silvet@SUFFIX@</file>
   <file>out/simple-cepstrum.cat</file>
   <file>out/simple-cepstrum.n3</file>
-  <file>out/simple-cepstrum.@SUFFIX@</file>
+  <file>out/simple-cepstrum@SUFFIX@</file>
   <file>out/tempogram.cat</file>
   <file>out/tempogram_CITATION.txt</file>
   <file>out/tempogram_COPYING.txt</file>
   <file>out/tempogram.n3</file>
   <file>out/tempogram_README.txt</file>
-  <file>out/tempogram.@SUFFIX@</file>
+  <file>out/tempogram@SUFFIX@</file>
   <file>out/tipic.cat</file>
   <file>out/tipic_CITATION.txt</file>
   <file>out/tipic_COPYING.txt</file>
   <file>out/tipic.n3</file>
   <file>out/tipic_README.txt</file>
-  <file>out/tipic.@SUFFIX@</file>
+  <file>out/tipic@SUFFIX@</file>
   <file>out/tuning-difference.cat</file>
   <file>out/tuning-difference_COPYING.txt</file>
   <file>out/tuning-difference.n3</file>
   <file>out/tuning-difference_README.md</file>
-  <file>out/tuning-difference.@SUFFIX@</file>
+  <file>out/tuning-difference@SUFFIX@</file>
   <file>out/ua-vamp-plugins.cat</file>
   <file>out/ua-vamp-plugins_COPYING.txt</file>
   <file>out/ua-vamp-plugins_README.md</file>
-  <file>out/ua-vamp-plugins.@SUFFIX@</file>
+  <file>out/ua-vamp-plugins@SUFFIX@</file>
   <file>out/vamp-aubio.cat</file>
   <file>out/vamp-aubio.n3</file>
   <file>out/vamp-aubio_COPYING.txt</file>
   <file>out/vamp-aubio_README.md</file>
-  <file>out/vamp-aubio.@SUFFIX@</file>
+  <file>out/vamp-aubio@SUFFIX@</file>
   <file>out/vamp-example-plugins.cat</file>
   <file>out/vamp-example-plugins_COPYING.txt</file>
   <file>out/vamp-example-plugins.n3</file>
-  <file>out/vamp-example-plugins.@SUFFIX@</file>
+  <file>out/vamp-example-plugins@SUFFIX@</file>
   <file>out/vamp-libxtract.cat</file>
   <file>out/vamp-libxtract_COPYING.txt</file>
   <file>out/vamp-libxtract.n3</file>
   <file>out/vamp-libxtract_README.txt</file>
-  <file>out/vamp-libxtract.@SUFFIX@</file>
+  <file>out/vamp-libxtract@SUFFIX@</file>
   <file>out/vampy_COPYING.txt</file>
   <file>out/vampy_README.txt</file>
-  <file>out/vampy.@SUFFIX@</file>
+  <file>out/vampy@SUFFIX@</file>
   <file>rdf/vamp.n3</file>
   <file>rdf/vamp.rdf</file>
   <file>rdf/plugins/availability.n3</file>