From a3a6376573fcb17a5f1e8528d2ac2789ec5f5f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20H=C3=B6her?= Date: Wed, 17 May 2017 21:30:22 +0200 Subject: [PATCH 01/12] Allow the qmake executable to be passed on CLI This change adds a new command line option `qmake` to the tool which allows the user to specify the qmake executable to be used. By default, if that option is omitted, the behavior is unchanged (i.e. the tool first searches for the `qmake` executable and - if this is not successful - then for either `qmake-qt5` or `qmake-qt4`. If the new option is used, no search takes place and instead the executable provided is used as-is. This implements a part of the functionality as discussed in issue #94. --- README.md | 11 +++++---- linuxdeployqt/main.cpp | 18 ++++++++++---- shared/shared.cpp | 56 ++++++++++++++++++++++-------------------- shared/shared.h | 4 ++- 4 files changed, 52 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 4083e69e..eb88ef3a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Please download __linuxdeployqt-x86_64.AppImage__ from the [Releases](https://gi Open in Qt Creator and build your application. Run it from the command line and inspect it with `ldd` to make sure the correct libraries from the correct locations are getting loaded, as `linuxdeployqt` will use `ldd` internally to determine from where to copy libraries into the bundle. -__Important:__ `linuxdeployqt` deploys the Qt instance that qmake on the $PATH points to, so make sure that it is the correct one. Verify that qmake finds the correct Qt instance like this before running the `linuxdeployqt` tool: +__Important:__ By default, `linuxdeployqt` deploys the Qt instance that qmake on the $PATH points to, so make sure that it is the correct one. Verify that qmake finds the correct Qt instance like this before running the `linuxdeployqt` tool: ``` qmake -v @@ -29,6 +29,7 @@ QMake version 3.0 Using Qt version 5.7.0 in /tmp/.mount_QtCreator-5.7.0-x86_64/5.7/gcc_64/lib ``` If this does not show the correct path to your Qt instance that you want to be bundled, then adjust your `$PATH` to find the correct `qmake`. +Alternatively, use the `-qmake` command line option to point the tool directly to the qmake executable to be used. Before running linuxdeployqt it may be wise to delete unneeded files that you do not wish to distribute from the build directory. These may be autogenerated during the build. You can delete them like so: @@ -69,8 +70,8 @@ dist: trusty before_install: - sudo add-apt-repository ppa:beineri/opt-qt58-trusty -y - sudo apt-get update -qq - -install: + +install: - sudo apt-get -y install qt58base - source /opt/qt*/bin/qt*-env.sh @@ -80,14 +81,14 @@ script: - make INSTALL_ROOT=appdir install ; find appdir/ after_success: - - wget -c "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage" + - wget -c "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage" - chmod a+x linuxdeployqt*.AppImage - unset QTDIR; unset QT_PLUGIN_PATH ; unset LD_LIBRARY_PATH - ./linuxdeployqt*.AppImage ./appdir/usr/share/applications/*.desktop -bundle-non-qt-libs - ./linuxdeployqt*.AppImage ./appdir/usr/share/applications/*.desktop -appimage - find ./appdir -executable -type f -exec ldd {} \; | grep " => /usr" | cut -d " " -f 2-3 | sort | uniq - curl --upload-file ./APPNAME*.AppImage https://transfer.sh/APPNAME-git.$(git rev-parse --short HEAD)-x86_64.AppImage -``` +``` When you save your change, then Travis CI should build and upload an AppImage for you. More likely than not, some fine-tuning will still be required. diff --git a/linuxdeployqt/main.cpp b/linuxdeployqt/main.cpp index 0d203ae2..e12040c2 100644 --- a/linuxdeployqt/main.cpp +++ b/linuxdeployqt/main.cpp @@ -55,13 +55,15 @@ int main(int argc, char **argv) qDebug() << " -executable= : Let the given executable use the deployed libraries too"; qDebug() << " -qmldir= : Scan for QML imports in the given path"; qDebug() << " -always-overwrite : Copy files even if the target file exists"; + qDebug() << " -qmake= : The qmake executable to use"; qDebug() << ""; qDebug() << "linuxdeployqt takes an application as input and makes it"; qDebug() << "self-contained by copying in the Qt libraries and plugins that"; qDebug() << "the application uses."; qDebug() << ""; - qDebug() << "It deploys the Qt instance that qmake on the $PATH points to,"; - qDebug() << "so make sure that it is the correct one."; + qDebug() << "By default it deploys the Qt instance that qmake on the $PATH points to."; + qDebug() << "The '-qmake' option can be used to point to the qmake executable"; + qDebug() << "to be used instead."; qDebug() << ""; qDebug() << "Plugins related to a Qt library are copied in with the library."; /* TODO: To be implemented @@ -145,7 +147,7 @@ int main(int argc, char **argv) // Allow binaries next to linuxdeployqt to be found; this is useful for bundling // this application itself together with helper binaries such as patchelf QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - QString oldPath = env.value("PATH"); + QString oldPath = env.value("PATH"); QString newPath = QCoreApplication::applicationDirPath() + ":" + oldPath; LogDebug() << newPath; setenv("PATH",newPath.toUtf8().constData(),1); @@ -169,6 +171,7 @@ int main(int argc, char **argv) QStringList additionalExecutables; bool qmldirArgumentUsed = false; QStringList qmlDirs; + QString qmakeExecutable; /* FHS-like mode is for an application that has been installed to a $PREFIX which is otherwise empty, e.g., /path/to/usr. * In this case, we want to construct an AppDir in /path/to. */ @@ -304,7 +307,7 @@ int main(int argc, char **argv) qDebug() << "preExistingToplevelIcon:" << preExistingToplevelIcon; } else { qDebug() << "iconToBeUsed:" << iconToBeUsed; - QString targetIconPath = appDirPath + "/" + QFileInfo(iconToBeUsed).fileName(); + QString targetIconPath = appDirPath + "/" + QFileInfo(iconToBeUsed).fileName(); if (QFile::copy(iconToBeUsed, targetIconPath)){ qDebug() << "Copied" << iconToBeUsed << "to" << targetIconPath; QFile::copy(targetIconPath, appDirPath + "/.DirIcon"); @@ -358,6 +361,10 @@ int main(int argc, char **argv) } else if (argument == QByteArray("-always-overwrite")) { LogDebug() << "Argument found:" << argument; alwaysOwerwriteEnabled = true; + } else if (argument.startsWith("-qmake=")) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf("="); + qmakeExecutable = argument.mid(index+1); } else if (argument.startsWith("-")) { LogError() << "Unknown argument" << argument << "\n"; return 1; @@ -371,7 +378,8 @@ int main(int argc, char **argv) } } - DeploymentInfo deploymentInfo = deployQtLibraries(appDirPath, additionalExecutables); + DeploymentInfo deploymentInfo = deployQtLibraries(appDirPath, additionalExecutables, + qmakeExecutable); // Convenience: Look for .qml files in the current directoty if no -qmldir specified. if (qmlDirs.isEmpty()) { diff --git a/shared/shared.cpp b/shared/shared.cpp index c4e1e8f0..82714fdb 100644 --- a/shared/shared.cpp +++ b/shared/shared.cpp @@ -99,20 +99,20 @@ inline QDebug operator<<(QDebug debug, const AppDirInfo &info) // on architecture. See "vDSO names" in the notes section of vdso(7) // for more information. static bool lddOutputContainsLinuxVDSO(const QString &lddOutput) { - // aarch64, arm, mips, x86_64, x86/x32 - if (lddOutput.contains(QStringLiteral("linux-vdso.so.1"))) { - return true; - // ppc32, s390 - } else if (lddOutput.contains(QStringLiteral("linux-vdso32.so.1"))) { - return true; - // ppc64, s390x - } else if (lddOutput.contains(QStringLiteral("linux-vdso64.so.1"))) { - return true; - // ia64, sh, i386 - } else if (lddOutput.contains(QStringLiteral("linux-gate.so.1"))) { - return true; - } - return false; + // aarch64, arm, mips, x86_64, x86/x32 + if (lddOutput.contains(QStringLiteral("linux-vdso.so.1"))) { + return true; + // ppc32, s390 + } else if (lddOutput.contains(QStringLiteral("linux-vdso32.so.1"))) { + return true; + // ppc64, s390x + } else if (lddOutput.contains(QStringLiteral("linux-vdso64.so.1"))) { + return true; + // ia64, sh, i386 + } else if (lddOutput.contains(QStringLiteral("linux-gate.so.1"))) { + return true; + } + return false; } bool copyFilePrintStatus(const QString &from, const QString &to) @@ -879,7 +879,7 @@ static QString captureOutput(const QString &command) return process.readAllStandardOutput(); } -DeploymentInfo deployQtLibraries(const QString &appDirPath, const QStringList &additionalExecutables) +DeploymentInfo deployQtLibraries(const QString &appDirPath, const QStringList &additionalExecutables, const QString& qmake) { AppDirInfo applicationBundle; @@ -906,15 +906,19 @@ DeploymentInfo deployQtLibraries(const QString &appDirPath, const QStringList &a // Determine the location of the Qt to be bundled LogDebug() << "Using qmake to determine the location of the Qt to be bundled"; - QString qmakePath = ""; + // Use the qmake executable passed in by the user: + QString qmakePath = qmake; - // The upstream name of the binary is "qmake", for Qt 4 and Qt 5 - qmakePath = QStandardPaths::findExecutable("qmake"); + // If we did not get a qmake, first try to find "qmake", which is the + // upstream name of the binary in both Qt4 and Qt5: + if (qmakePath.isEmpty()) { + qmakePath = QStandardPaths::findExecutable("qmake"); + } // But openSUSE has qmake for Qt 4 and qmake-qt5 for Qt 5 // Qt 4 on Fedora comes with suffix -qt4 // http://www.geopsy.org/wiki/index.php/Installing_Qt_binary_packages - if(qmakePath == ""){ + if(qmakePath.isEmpty()){ if(qtDetected == 5){ qmakePath = QStandardPaths::findExecutable("qmake-qt5"); } @@ -1016,7 +1020,7 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath } } else { pluginList.append(QStringLiteral("imageformats/") + plugin); - } + } } } @@ -1026,8 +1030,8 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath foreach (const QString &plugin, xcbglintegrationPlugins) { pluginList.append(QStringLiteral("xcbglintegrations/") + plugin); } - } - + } + // Also deploy plugins/iconengines/libqsvgicon.so whenever libQt5Svg.so.* is about to be deployed, // https://github.com/probonopd/linuxdeployqt/issues/36 if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Svg")) { @@ -1069,7 +1073,7 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath QString sourcePath; QString destinationPath; - + // Qt WebEngine if libQt5WebEngineCore is in use // https://doc-snapshots.qt.io/qt5-5.7/qtwebengine-deploying.html // TODO: Rather than hardcode the source paths, somehow get them dynamically @@ -1124,7 +1128,7 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath destinationPath = QDir::cleanPath(dstTranslations + "/qtwebengine_locales"); recursiveCopy(sourcePath, destinationPath); } - + LogNormal() << "pluginList after having detected hopefully all required plugins:" << pluginList; foreach (const QString &plugin, pluginList) { @@ -1143,7 +1147,7 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath QString relativePath = dir.relativeFilePath(appDirInfo.path + "/" + libraries[0].libraryDestinationDirectory); relativePath.remove(0, 3); // remove initial '../' changeIdentification("$ORIGIN/" + relativePath, QFileInfo(destinationPath).canonicalFilePath()); - + } } } @@ -1224,7 +1228,7 @@ bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, argumentList.append(qtToBeBundledInfo.value("QT_INSTALL_QML")); LogDebug() << "qmlImportsPath (QT_INSTALL_QML):" << qtToBeBundledInfo.value("QT_INSTALL_QML"); - + // run qmlimportscanner QProcess qmlImportScanner; LogDebug() << qmlImportScannerPath << argumentList; diff --git a/shared/shared.h b/shared/shared.h index 9e0ff6d1..82dcb61c 100644 --- a/shared/shared.h +++ b/shared/shared.h @@ -112,7 +112,9 @@ QString findAppBinary(const QString &appDirPath); QList getQtLibraries(const QString &path, const QString &appDirPath, const QSet &rpaths); QList getQtLibraries(const QStringList &lddLines, const QString &appDirPath, const QSet &rpaths); QString copyLibrary(const LibraryInfo &library, const QString path); -DeploymentInfo deployQtLibraries(const QString &appDirPath, const QStringList &additionalExecutables); +DeploymentInfo deployQtLibraries(const QString &appDirPath, + const QStringList &additionalExecutables, + const QString &qmake); DeploymentInfo deployQtLibraries(QList libraries,const QString &bundlePath, const QStringList &binaryPaths, bool useLoaderPath); void deployPlugins(const QString &appDirPath, DeploymentInfo deploymentInfo); bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, QStringList &qmlDirs); From a99b43b081b7042c6813dafb028ca27b822ce6d9 Mon Sep 17 00:00:00 2001 From: Gonzalo Exequiel Pedone Date: Fri, 19 May 2017 12:30:04 -0300 Subject: [PATCH 02/12] Project tree cleanup. --- .gitignore | 5 +++ commons.pri | 38 ++++++++++++++++++++ linuxdeployqt.pro | 26 ++++++++++++-- {linuxdeployqt => src}/linuxdeployqt.pro | 0 {linuxdeployqt => src}/main.cpp | 8 +++-- {shared => src}/shared.cpp | 44 +++++++++++++----------- {shared => src}/shared.h | 0 tests/tests-ci.sh | 4 +-- 8 files changed, 97 insertions(+), 28 deletions(-) create mode 100644 commons.pri rename {linuxdeployqt => src}/linuxdeployqt.pro (100%) rename {linuxdeployqt => src}/main.cpp (99%) rename {shared => src}/shared.cpp (99%) rename {shared => src}/shared.h (100%) diff --git a/.gitignore b/.gitignore index fa24b2ef..6fc7ba5e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,9 +30,14 @@ Makefile* *.autosave # QtCtreator Qml + *.qmlproject.user *.qmlproject.user.* # QtCtreator CMake + CMakeLists.txt.user +# Project Files + +linuxdeployqt diff --git a/commons.pri b/commons.pri new file mode 100644 index 00000000..33601187 --- /dev/null +++ b/commons.pri @@ -0,0 +1,38 @@ +COMMONS_TARGET = "linuxdeployqt" +DEFAULT_PREFIX = /usr + +isEmpty(PREFIX): PREFIX = $${DEFAULT_PREFIX} +isEmpty(EXECPREFIX): EXECPREFIX = $${PREFIX} +isEmpty(BINDIR): BINDIR = $${EXECPREFIX}/bin +isEmpty(SBINDIR): SBINDIR = $${EXECPREFIX}/sbin +isEmpty(LIBEXECDIR): LIBEXECDIR = $${EXECPREFIX}/libexec +isEmpty(DATAROOTDIR): DATAROOTDIR = $${PREFIX}/share +isEmpty(DATDIR): DATDIR = $${DATAROOTDIR}/$${COMMONS_TARGET} +isEmpty(SYSCONFDIR): SYSCONFDIR = $${PREFIX}/etc +isEmpty(SHAREDSTATEDIR): SHAREDSTATEDIR = $${PREFIX}/com +isEmpty(LOCALSTATEDIR): LOCALSTATEDIR = $${PREFIX}/var +isEmpty(INCLUDEDIR): INCLUDEDIR = $${PREFIX}/include +isEmpty(DOCDIR): DOCDIR = $${DATAROOTDIR}/doc/$${COMMONS_TARGET} +isEmpty(INFODIR): INFODIR = $${DATAROOTDIR}/info +isEmpty(HTMLDIR): HTMLDIR = $${DOCDIR}/html +isEmpty(DVIDIR): DVIDIR = $${DOCDIR}/dvi +isEmpty(PDFDIR): PDFDIR = $${DOCDIR}/pdf +isEmpty(PSDIR): PSDIR = $${DOCDIR}/ps +isEmpty(LIBDIR): LIBDIR = $${EXECPREFIX}/lib +isEmpty(LOCALEDIR): LOCALEDIR = $${DATAROOTDIR}/locale +isEmpty(MANDIR): MANDIR = $${DATAROOTDIR}/man +isEmpty(LICENSEDIR): LICENSEDIR = $${DATAROOTDIR}/licenses/$${COMMONS_TARGET} +isEmpty(LOCALDIR): LOCALDIR = $${PREFIX}/local +isEmpty(LOCALLIBDIR): LOCALLIBDIR = $${LOCALDIR}/lib + +CONFIG(debug, debug|release) { + COMMONS_BUILD_PATH = build/Qt$${QT_VERSION}/$${QMAKE_CC}/debug + DEFINES += QT_DEBUG +} else { + COMMONS_BUILD_PATH = build/Qt$${QT_VERSION}/$${QMAKE_CC}/release +} + +MOC_DIR = $${COMMONS_BUILD_PATH}/moc +OBJECTS_DIR = $${COMMONS_BUILD_PATH}/obj +RCC_DIR = $${COMMONS_BUILD_PATH}/rcc +UI_DIR = $${COMMONS_BUILD_PATH}/ui diff --git a/linuxdeployqt.pro b/linuxdeployqt.pro index 1e2f2d5b..093080ae 100644 --- a/linuxdeployqt.pro +++ b/linuxdeployqt.pro @@ -1,2 +1,24 @@ -TEMPLATE = subdirs -SUBDIRS = linuxdeployqt +include(commons.pri) + +QT += core +QT -= gui + +CONFIG += c++11 + +CONFIG += console +CONFIG -= app_bundle + +TEMPLATE = app + +HEADERS = \ + src/shared.h + +SOURCES = \ + src/main.cpp \ + src/shared.cpp + +DESTDIR = $${OUT_PWD} +TARGET = $${COMMONS_TARGET} + +INSTALLS += target +target.path = $${BINDIR} diff --git a/linuxdeployqt/linuxdeployqt.pro b/src/linuxdeployqt.pro similarity index 100% rename from linuxdeployqt/linuxdeployqt.pro rename to src/linuxdeployqt.pro diff --git a/linuxdeployqt/main.cpp b/src/main.cpp similarity index 99% rename from linuxdeployqt/main.cpp rename to src/main.cpp index 0d203ae2..cbd5cb6b 100644 --- a/linuxdeployqt/main.cpp +++ b/src/main.cpp @@ -25,15 +25,17 @@ ** $QT_END_LICENSE$ ** ****************************************************************************/ + #include #include #include -#include "../shared/shared.h" #include #include #include #include +#include "shared.h" + int main(int argc, char **argv) { QCoreApplication app(argc, argv); @@ -145,7 +147,7 @@ int main(int argc, char **argv) // Allow binaries next to linuxdeployqt to be found; this is useful for bundling // this application itself together with helper binaries such as patchelf QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - QString oldPath = env.value("PATH"); + QString oldPath = env.value("PATH"); QString newPath = QCoreApplication::applicationDirPath() + ":" + oldPath; LogDebug() << newPath; setenv("PATH",newPath.toUtf8().constData(),1); @@ -304,7 +306,7 @@ int main(int argc, char **argv) qDebug() << "preExistingToplevelIcon:" << preExistingToplevelIcon; } else { qDebug() << "iconToBeUsed:" << iconToBeUsed; - QString targetIconPath = appDirPath + "/" + QFileInfo(iconToBeUsed).fileName(); + QString targetIconPath = appDirPath + "/" + QFileInfo(iconToBeUsed).fileName(); if (QFile::copy(iconToBeUsed, targetIconPath)){ qDebug() << "Copied" << iconToBeUsed << "to" << targetIconPath; QFile::copy(targetIconPath, appDirPath + "/.DirIcon"); diff --git a/shared/shared.cpp b/src/shared.cpp similarity index 99% rename from shared/shared.cpp rename to src/shared.cpp index c4e1e8f0..0811e296 100644 --- a/shared/shared.cpp +++ b/src/shared.cpp @@ -25,6 +25,7 @@ ** $QT_END_LICENSE$ ** ****************************************************************************/ + #include #include #include @@ -43,6 +44,7 @@ #include #include #include + #include "shared.h" QString appBinaryPath; @@ -99,20 +101,20 @@ inline QDebug operator<<(QDebug debug, const AppDirInfo &info) // on architecture. See "vDSO names" in the notes section of vdso(7) // for more information. static bool lddOutputContainsLinuxVDSO(const QString &lddOutput) { - // aarch64, arm, mips, x86_64, x86/x32 - if (lddOutput.contains(QStringLiteral("linux-vdso.so.1"))) { - return true; - // ppc32, s390 - } else if (lddOutput.contains(QStringLiteral("linux-vdso32.so.1"))) { - return true; - // ppc64, s390x - } else if (lddOutput.contains(QStringLiteral("linux-vdso64.so.1"))) { - return true; - // ia64, sh, i386 - } else if (lddOutput.contains(QStringLiteral("linux-gate.so.1"))) { - return true; - } - return false; + // aarch64, arm, mips, x86_64, x86/x32 + if (lddOutput.contains(QStringLiteral("linux-vdso.so.1"))) { + return true; + // ppc32, s390 + } else if (lddOutput.contains(QStringLiteral("linux-vdso32.so.1"))) { + return true; + // ppc64, s390x + } else if (lddOutput.contains(QStringLiteral("linux-vdso64.so.1"))) { + return true; + // ia64, sh, i386 + } else if (lddOutput.contains(QStringLiteral("linux-gate.so.1"))) { + return true; + } + return false; } bool copyFilePrintStatus(const QString &from, const QString &to) @@ -1016,7 +1018,7 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath } } else { pluginList.append(QStringLiteral("imageformats/") + plugin); - } + } } } @@ -1026,8 +1028,8 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath foreach (const QString &plugin, xcbglintegrationPlugins) { pluginList.append(QStringLiteral("xcbglintegrations/") + plugin); } - } - + } + // Also deploy plugins/iconengines/libqsvgicon.so whenever libQt5Svg.so.* is about to be deployed, // https://github.com/probonopd/linuxdeployqt/issues/36 if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Svg")) { @@ -1069,7 +1071,7 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath QString sourcePath; QString destinationPath; - + // Qt WebEngine if libQt5WebEngineCore is in use // https://doc-snapshots.qt.io/qt5-5.7/qtwebengine-deploying.html // TODO: Rather than hardcode the source paths, somehow get them dynamically @@ -1124,7 +1126,7 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath destinationPath = QDir::cleanPath(dstTranslations + "/qtwebengine_locales"); recursiveCopy(sourcePath, destinationPath); } - + LogNormal() << "pluginList after having detected hopefully all required plugins:" << pluginList; foreach (const QString &plugin, pluginList) { @@ -1143,7 +1145,7 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath QString relativePath = dir.relativeFilePath(appDirInfo.path + "/" + libraries[0].libraryDestinationDirectory); relativePath.remove(0, 3); // remove initial '../' changeIdentification("$ORIGIN/" + relativePath, QFileInfo(destinationPath).canonicalFilePath()); - + } } } @@ -1224,7 +1226,7 @@ bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, argumentList.append(qtToBeBundledInfo.value("QT_INSTALL_QML")); LogDebug() << "qmlImportsPath (QT_INSTALL_QML):" << qtToBeBundledInfo.value("QT_INSTALL_QML"); - + // run qmlimportscanner QProcess qmlImportScanner; LogDebug() << qmlImportScannerPath << argumentList; diff --git a/shared/shared.h b/src/shared.h similarity index 100% rename from shared/shared.h rename to src/shared.h diff --git a/tests/tests-ci.sh b/tests/tests-ci.sh index 9bd5fa36..619c51e6 100755 --- a/tests/tests-ci.sh +++ b/tests/tests-ci.sh @@ -10,8 +10,8 @@ mkdir -p linuxdeployqt.AppDir/usr/bin/ cp /usr/local/bin/{appimagetool,mksquashfs,patchelf,zsyncmake} linuxdeployqt.AppDir/usr/bin/ find linuxdeployqt.AppDir/ export VERSION=continuous -cp ./linuxdeployqt/linuxdeployqt linuxdeployqt.AppDir/usr/bin/ -./linuxdeployqt/linuxdeployqt linuxdeployqt.AppDir/linuxdeployqt.desktop -verbose=3 -appimage +cp ./linuxdeployqt linuxdeployqt.AppDir/usr/bin/ +./linuxdeployqt linuxdeployqt.AppDir/linuxdeployqt.desktop -verbose=3 -appimage ls -lh find *.AppDir xpra start :99 From cd37cb899f40d2feda4b6b75f049bdea740ac7cb Mon Sep 17 00:00:00 2001 From: Gonzalo Exequiel Pedone Date: Fri, 19 May 2017 17:57:14 -0300 Subject: [PATCH 03/12] Using QCommandLineParser instead of parsing options manually. --- src/main.cpp | 240 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 153 insertions(+), 87 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index cbd5cb6b..277c1329 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,12 +27,10 @@ ****************************************************************************/ #include -#include #include -#include -#include #include #include +#include #include "shared.h" @@ -40,48 +38,105 @@ int main(int argc, char **argv) { QCoreApplication app(argc, argv); + QCommandLineParser cliParser; + + // Even with ParseAsLongOptions set, -h option still show the options as + // double dash, but the parser interpret it correctly. It seems to be a bug + // related to Qt. + cliParser.addHelpOption(); + cliParser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); + + cliParser.setApplicationDescription(QObject::tr( + "linuxdeployqt takes an application as input and makes it " + "self-contained by copying in the Qt libraries and plugins that " + "the application uses.\n" + "\n" + "It deploys the Qt instance that qmake on the $PATH points to, " + "so make sure that it is the correct one.\n" + "\n" + "Plugins related to a Qt library are copied in with the library.\n" + "\n" + "See the \"Deploying Applications on Linux\" topic in the " + "documentation for more information about deployment on Linux." + )); + + cliParser.addPositionalArgument("file", + "Deploy libraries for this file", + ""); + + QCommandLineOption verboseOpt( + "verbose", + QObject::tr("0 = no output, 1 = error/warning (default), 2 = normal, 3 = debug"), + "0-3", "1" + ); + cliParser.addOption(verboseOpt); + + QCommandLineOption noPluginsOpt( + "no-plugins", + QObject::tr("Skip plugin deployment")); + cliParser.addOption(noPluginsOpt); + + QCommandLineOption appimageOpt( + "appimage", + QObject::tr("Create an AppImage (implies -bundle-non-qt-libs)")); + cliParser.addOption(appimageOpt); + + QCommandLineOption noStripOpt( + "no-strip", + QObject::tr("Don't run 'strip' on the binaries")); + cliParser.addOption(noStripOpt); + + QCommandLineOption bundleNonQtLibsOpt( + "bundle-non-qt-libs", + QObject::tr("Also bundle non-core, non-Qt libraries")); + cliParser.addOption(bundleNonQtLibsOpt); + + QCommandLineOption executableOpt( + "executable", + QObject::tr("Let the given executable use the deployed libraries too"), + "path", "" + ); + cliParser.addOption(executableOpt); + + QCommandLineOption qmldirOpt( + "qmldir", + QObject::tr("Scan for QML imports in the given path"), + "path", "" + ); + cliParser.addOption(qmldirOpt); + + QCommandLineOption alwaysOverwriteOpt( + "always-overwrite", + QObject::tr("Copy files even if the target file exists")); + cliParser.addOption(alwaysOverwriteOpt); + /* + * TODO: Proposed option set. -scan-bin-paths and -scan-qml-paths + * options may subtitute -executable and -qmldir. + * + * -library-blacklist: When deploying required libraries, avoid including + * libraries listed here. + * -extra-plugins : Also deploy this plugins. + * -extra-files : Also copy these files to the deploy folder (useful for + * including extra required utilities). + * -qmake : Use this alternative binary as qmake. + * -scan-bin-paths : Scan this paths for binaries and dynamic libraries. + * -scan-qml-paths : Scan this directories for Qml imports. + * -scan-recursive : Scan directories recursively. + * -bins-dest : Destination path for executables. + * -libs-dest : Destination path for libraries. + */ + cliParser.process(app); + extern QString appBinaryPath; appBinaryPath = ""; // Cannot do it in one go due to "extern" - QString firstArgument = QString::fromLocal8Bit(argv[1]); - - if (argc < 2 || firstArgument.startsWith("-")) { - qDebug() << "Usage: linuxdeployqt [options]"; - qDebug() << ""; - qDebug() << "Options:"; - qDebug() << " -verbose=<0-3> : 0 = no output, 1 = error/warning (default), 2 = normal, 3 = debug"; - qDebug() << " -no-plugins : Skip plugin deployment"; - qDebug() << " -appimage : Create an AppImage (implies -bundle-non-qt-libs)"; - qDebug() << " -no-strip : Don't run 'strip' on the binaries"; - qDebug() << " -bundle-non-qt-libs : Also bundle non-core, non-Qt libraries"; - qDebug() << " -executable= : Let the given executable use the deployed libraries too"; - qDebug() << " -qmldir= : Scan for QML imports in the given path"; - qDebug() << " -always-overwrite : Copy files even if the target file exists"; - qDebug() << ""; - qDebug() << "linuxdeployqt takes an application as input and makes it"; - qDebug() << "self-contained by copying in the Qt libraries and plugins that"; - qDebug() << "the application uses."; - qDebug() << ""; - qDebug() << "It deploys the Qt instance that qmake on the $PATH points to,"; - qDebug() << "so make sure that it is the correct one."; - qDebug() << ""; - qDebug() << "Plugins related to a Qt library are copied in with the library."; - /* TODO: To be implemented - qDebug() << "The accessibility, image formats, and text codec"; - qDebug() << "plugins are always copied, unless \"-no-plugins\" is specified."; - */ - qDebug() << ""; - qDebug() << "See the \"Deploying Applications on Linux\" topic in the"; - qDebug() << "documentation for more information about deployment on Linux."; - - return 1; - } - QString desktopFile = ""; QString desktopExecEntry = ""; QString desktopIconEntry = ""; - if (argc > 1) { + QString firstArgument = cliParser.positionalArguments().value(0, ""); + + if (!firstArgument.isEmpty()) { /* If we got a desktop file as the argument, try to figure out the application binary from it. * This has the advantage that we can also figure out the icon file this way, and have less work * to do when using linuxdeployqt. */ @@ -167,7 +222,6 @@ int main(int argc, char **argv) extern bool fhsLikeMode; extern QString fhsPrefix; extern bool alwaysOwerwriteEnabled; - extern QStringList librarySearchPath; QStringList additionalExecutables; bool qmldirArgumentUsed = false; QStringList qmlDirs; @@ -318,53 +372,64 @@ int main(int argc, char **argv) } } - for (int i = 2; i < argc; ++i) { - QByteArray argument = QByteArray(argv[i]); - if (argument == QByteArray("-no-plugins")) { - LogDebug() << "Argument found:" << argument; - plugins = false; - } else if (argument == QByteArray("-appimage")) { - LogDebug() << "Argument found:" << argument; - appimage = true; - bundleAllButCoreLibs = true; - } else if (argument == QByteArray("-no-strip")) { - LogDebug() << "Argument found:" << argument; - runStripEnabled = false; - } else if (argument == QByteArray("-bundle-non-qt-libs")) { - LogDebug() << "Argument found:" << argument; - bundleAllButCoreLibs = true; - } else if (argument.startsWith(QByteArray("-verbose"))) { - LogDebug() << "Argument found:" << argument; - int index = argument.indexOf("="); - bool ok = false; - int number = argument.mid(index+1).toInt(&ok); - if (!ok) - LogError() << "Could not parse verbose level"; - else - logLevel = number; - } else if (argument.startsWith(QByteArray("-executable"))) { - LogDebug() << "Argument found:" << argument; - int index = argument.indexOf('='); - if (index == -1) - LogError() << "Missing executable path"; - else - additionalExecutables << argument.mid(index+1); - } else if (argument.startsWith(QByteArray("-qmldir"))) { - LogDebug() << "Argument found:" << argument; - qmldirArgumentUsed = true; - int index = argument.indexOf('='); - if (index == -1) - LogError() << "Missing qml directory path"; - else - qmlDirs << argument.mid(index+1); - } else if (argument == QByteArray("-always-overwrite")) { - LogDebug() << "Argument found:" << argument; - alwaysOwerwriteEnabled = true; - } else if (argument.startsWith("-")) { - LogError() << "Unknown argument" << argument << "\n"; - return 1; - } - } + // Set options from command line + if (cliParser.isSet(noPluginsOpt)) { + LogDebug() << "Argument found:" << noPluginsOpt.valueName(); + plugins = false; + } + + if (cliParser.isSet(appimageOpt)) { + LogDebug() << "Argument found:" << appimageOpt.valueName(); + appimage = true; + bundleAllButCoreLibs = true; + } + + if (cliParser.isSet(noStripOpt)) { + LogDebug() << "Argument found:" << noStripOpt.valueName(); + runStripEnabled = false; + } + + if (cliParser.isSet(bundleNonQtLibsOpt)) { + LogDebug() << "Argument found:" << bundleNonQtLibsOpt.valueName(); + bundleAllButCoreLibs = true; + } + + if (cliParser.isSet(verboseOpt)) { + LogDebug() << "Argument found:" << verboseOpt.valueName(); + bool ok = false; + int number = cliParser.value(verboseOpt).toInt(&ok); + + if (ok) + logLevel = number; + else + LogError() << "Could not parse verbose level"; + } + + if (cliParser.isSet(executableOpt)) { + LogDebug() << "Argument found:" << executableOpt.valueName(); + QString executables = cliParser.value(executableOpt).trimmed(); + + if (!executables.isEmpty()) + additionalExecutables << executables; + else + LogError() << "Missing executable path"; + } + + if (cliParser.isSet(qmldirOpt)) { + LogDebug() << "Argument found:" << qmldirOpt.valueName(); + QString dirs = cliParser.value(qmldirOpt).trimmed(); + qmldirArgumentUsed = true; + + if (!dirs.isEmpty()) + qmlDirs << dirs; + else + LogError() << "Missing qml directory path"; + } + + if (cliParser.isSet(alwaysOverwriteOpt)) { + LogDebug() << "Argument found:" << alwaysOverwriteOpt.valueName(); + alwaysOwerwriteEnabled = true; + } if (appimage) { if(checkAppImagePrerequisites(appDirPath) == false){ @@ -407,5 +472,6 @@ int main(int argc, char **argv) LogDebug() << "result:" << result; exit(result); } - exit(0); + + return 0; } From 79a1c0c47efd8c4969461db1d4068054053dddb0 Mon Sep 17 00:00:00 2001 From: Gonzalo Exequiel Pedone Date: Sat, 20 May 2017 20:01:21 -0300 Subject: [PATCH 04/12] Big code cleanup, no more scattered extern variables and undefinned funtions. --- src/main.cpp | 218 +++--- src/shared.cpp | 1987 +++++++++++++++++++++++++----------------------- src/shared.h | 187 +++-- 3 files changed, 1274 insertions(+), 1118 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 277c1329..377a8838 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -127,13 +127,11 @@ int main(int argc, char **argv) */ cliParser.process(app); - extern QString appBinaryPath; - appBinaryPath = ""; // Cannot do it in one go due to "extern" - - QString desktopFile = ""; - QString desktopExecEntry = ""; - QString desktopIconEntry = ""; + Deploy deploy; + QString desktopFile; + QString desktopExecEntry; + QString desktopIconEntry; QString firstArgument = cliParser.positionalArguments().value(0, ""); if (!firstArgument.isEmpty()) { @@ -157,17 +155,22 @@ int main(int argc, char **argv) directoryToBeSearched = QDir::cleanPath(QFileInfo(firstArgument).absolutePath()); QDirIterator it(directoryToBeSearched, QDirIterator::Subdirectories); + while (it.hasNext()) { it.next(); - if((it.fileName() == desktopExecEntry) && (it.fileInfo().isFile()) && (it.fileInfo().isExecutable())){ + + if (it.fileName() == desktopExecEntry + && it.fileInfo().isFile() + && it.fileInfo().isExecutable()) { qDebug() << "Found binary from desktop file:" << it.fileInfo().canonicalFilePath(); - appBinaryPath = it.fileInfo().absoluteFilePath(); + deploy.appBinaryPath = it.fileInfo().absoluteFilePath(); + break; } } /* Only if we could not find it below the directory in which the desktop file resides, search above */ - if(appBinaryPath == ""){ + if (deploy.appBinaryPath.isEmpty()) { if(QFileInfo(QDir::cleanPath(QFileInfo(firstArgument).absolutePath() + "/../../bin/" + desktopExecEntry)).exists()){ directoryToBeSearched = QDir::cleanPath(QFileInfo(firstArgument).absolutePath() + "/../../"); } else { @@ -178,24 +181,24 @@ int main(int argc, char **argv) it2.next(); if((it2.fileName() == desktopExecEntry) && (it2.fileInfo().isFile()) && (it2.fileInfo().isExecutable())){ qDebug() << "Found binary from desktop file:" << it2.fileInfo().canonicalFilePath(); - appBinaryPath = it2.fileInfo().absoluteFilePath(); + deploy.appBinaryPath = it2.fileInfo().absoluteFilePath(); break; } } } - if(appBinaryPath == ""){ + if(deploy.appBinaryPath == ""){ if((QFileInfo(candidateBin).isFile()) && (QFileInfo(candidateBin).isExecutable())) { - appBinaryPath = QFileInfo(candidateBin).absoluteFilePath(); + deploy.appBinaryPath = QFileInfo(candidateBin).absoluteFilePath(); } else { - LogError() << "Could not determine the path to the executable based on the desktop file\n"; + deploy.LogError() << "Could not determine the path to the executable based on the desktop file\n"; return 1; } } } else { - appBinaryPath = firstArgument; - appBinaryPath = QFileInfo(QDir::cleanPath(appBinaryPath)).absoluteFilePath(); + deploy.appBinaryPath = firstArgument; + deploy.appBinaryPath = QFileInfo(QDir::cleanPath(deploy.appBinaryPath)).absoluteFilePath(); } } @@ -204,62 +207,61 @@ int main(int argc, char **argv) QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QString oldPath = env.value("PATH"); QString newPath = QCoreApplication::applicationDirPath() + ":" + oldPath; - LogDebug() << newPath; + deploy.LogDebug() << newPath; setenv("PATH",newPath.toUtf8().constData(),1); - QString appName = QDir::cleanPath(QFileInfo(appBinaryPath).completeBaseName()); + QString appName = QDir::cleanPath(QFileInfo(deploy.appBinaryPath).completeBaseName()); + QString appDir = QDir::cleanPath(deploy.appBinaryPath + "/../"); - QString appDir = QDir::cleanPath(appBinaryPath + "/../"); - if (QDir().exists(appDir) == false) { + if (!QDir().exists(appDir)) { qDebug() << "Error: Could not find AppDir" << appDir; + return 1; } bool plugins = true; bool appimage = false; - extern bool runStripEnabled; - extern bool bundleAllButCoreLibs; - extern bool fhsLikeMode; - extern QString fhsPrefix; - extern bool alwaysOwerwriteEnabled; QStringList additionalExecutables; bool qmldirArgumentUsed = false; QStringList qmlDirs; /* FHS-like mode is for an application that has been installed to a $PREFIX which is otherwise empty, e.g., /path/to/usr. * In this case, we want to construct an AppDir in /path/to. */ - if (QDir().exists((QDir::cleanPath(appBinaryPath + "/../../bin"))) == true) { - fhsPrefix = QDir::cleanPath(appBinaryPath + "/../../"); - qDebug() << "FHS-like mode with PREFIX, fhsPrefix:" << fhsPrefix; - fhsLikeMode = true; + if (QDir().exists(QDir::cleanPath(deploy.appBinaryPath + "/../../bin"))) { + deploy.fhsPrefix = QDir::cleanPath(deploy.appBinaryPath + "/../../"); + qDebug() << "FHS-like mode with PREFIX, fhsPrefix:" << deploy.fhsPrefix; + deploy.fhsLikeMode = true; } else { qDebug() << "Not using FHS-like mode"; } - if (QDir().exists(appBinaryPath)) { - qDebug() << "app-binary:" << appBinaryPath; + if (QDir().exists(deploy.appBinaryPath)) { + qDebug() << "app-binary:" << deploy.appBinaryPath; } else { - qDebug() << "Error: Could not find app-binary" << appBinaryPath; + qDebug() << "Error: Could not find app-binary" << deploy.appBinaryPath; + return 1; } QString appDirPath; QString relativeBinPath; - if(fhsLikeMode == false){ + + if (deploy.fhsLikeMode) { + appDirPath = QDir::cleanPath(deploy.fhsPrefix + "/../"); + QString relativePrefix = deploy.fhsPrefix.replace(appDirPath+"/", ""); + relativeBinPath = relativePrefix + "/bin/" + appName; + } else { appDirPath = appDir; relativeBinPath = appName; - } else { - appDirPath = QDir::cleanPath(fhsPrefix + "/../"); - QString relativePrefix = fhsPrefix.replace(appDirPath+"/", ""); - relativeBinPath = relativePrefix + "/bin/" + appName; } + qDebug() << "appDirPath:" << appDirPath; qDebug() << "relativeBinPath:" << relativeBinPath; QFile appRun(appDirPath + "/AppRun"); - if(appRun.exists()){ + + if(appRun.exists()) appRun.remove(); - } QFile::link(relativeBinPath, appDirPath + "/AppRun"); @@ -272,27 +274,35 @@ int main(int argc, char **argv) } } if(QFileInfo(destination).isFile() == false){ - LogError() << destination << "does not exist and could not be copied there\n"; + deploy.LogError() << destination << "does not exist and could not be copied there\n"; return 1; } } /* To make an AppDir, we need to find the icon and copy it in place */ QStringList candidates; - QString iconToBeUsed = ""; - if(desktopIconEntry != ""){ + QString iconToBeUsed; + + if(!desktopIconEntry.isEmpty()) { QDirIterator it3(appDirPath, QDirIterator::Subdirectories); + while (it3.hasNext()) { it3.next(); - if((it3.fileName().startsWith(desktopIconEntry)) && ((it3.fileName().endsWith(".png")) || (it3.fileName().endsWith(".svg")) || (it3.fileName().endsWith(".svgz")) || (it3.fileName().endsWith(".xpm")))){ + + if (it3.fileName().startsWith(desktopIconEntry) + && (it3.fileName().endsWith(".png") + || it3.fileName().endsWith(".svg") + || it3.fileName().endsWith(".svgz") + || it3.fileName().endsWith(".xpm"))) { candidates.append(it3.filePath()); } } + qDebug() << "Found icons from desktop file:" << candidates; /* Select the main icon from the candidates */ if(candidates.length() == 1){ - iconToBeUsed = candidates.at(0); // The only choice + iconToBeUsed = candidates.first(); // The only choice } else if(candidates.length() > 1){ foreach(QString current, candidates) { if(current.contains("256")){ @@ -334,39 +344,42 @@ int main(int argc, char **argv) } } - /* Copy in place */ - if(iconToBeUsed != ""){ - /* Check if there is already an icon and only if there is not, copy it to the AppDir. + // Copy in place + if (!iconToBeUsed.isEmpty()) { + /* Check if there is already an icon and only if there is not, copy + * it to the AppDir. * As per the ROX AppDir spec, also copying to .DirIcon. */ - QString preExistingToplevelIcon = ""; - if(QFileInfo(appDirPath + "/" + desktopIconEntry + ".xpm").exists() == true){ - preExistingToplevelIcon = appDirPath + "/" + desktopIconEntry + ".xpm"; - if(QFileInfo(appDirPath + "/.DirIcon").exists() == false) QFile::copy(preExistingToplevelIcon, appDirPath + "/.DirIcon"); - } - if(QFileInfo(appDirPath + "/" + desktopIconEntry + ".svgz").exists() == true){ - preExistingToplevelIcon = appDirPath + "/" + desktopIconEntry + ".svgz"; - if(QFileInfo(appDirPath + "/.DirIcon").exists() == false) if(QFileInfo(appDirPath + "/.DirIcon").exists() == false) QFile::copy(preExistingToplevelIcon, appDirPath + "/.DirIcon"); - } - if(QFileInfo(appDirPath + "/" + desktopIconEntry + ".svg").exists() == true){ - preExistingToplevelIcon = appDirPath + "/" + desktopIconEntry + ".svg"; - if(QFileInfo(appDirPath + "/.DirIcon").exists() == false) QFile::copy(preExistingToplevelIcon, appDirPath + "/.DirIcon"); - } - if(QFileInfo(appDirPath + "/" + desktopIconEntry + ".png").exists() == true){ - preExistingToplevelIcon = appDirPath + "/" + desktopIconEntry + ".png"; - if(QFileInfo(appDirPath + "/.DirIcon").exists() == false) QFile::copy(preExistingToplevelIcon, appDirPath + "/.DirIcon"); + bool foundToplevelIcon = false; + + for (auto &iconFormat: QStringList {"png", + "svg", + "svgz", + "xpm"}) { + QString preExistingToplevelIcon = + appDirPath + "/" + desktopIconEntry + "." + iconFormat; + + if (QFileInfo(preExistingToplevelIcon).exists() + && !QFileInfo(appDirPath + "/.DirIcon").exists()) { + QFile::copy(preExistingToplevelIcon, appDirPath + "/.DirIcon"); + qDebug() << "preExistingToplevelIcon:" << preExistingToplevelIcon; + foundToplevelIcon = true; + + break; + } } - if(preExistingToplevelIcon != ""){ - qDebug() << "preExistingToplevelIcon:" << preExistingToplevelIcon; - } else { + if (!foundToplevelIcon) { qDebug() << "iconToBeUsed:" << iconToBeUsed; - QString targetIconPath = appDirPath + "/" + QFileInfo(iconToBeUsed).fileName(); - if (QFile::copy(iconToBeUsed, targetIconPath)){ + QString targetIconPath = + appDirPath + "/" + QFileInfo(iconToBeUsed).fileName(); + + if (QFile::copy(iconToBeUsed, targetIconPath)) { qDebug() << "Copied" << iconToBeUsed << "to" << targetIconPath; QFile::copy(targetIconPath, appDirPath + "/.DirIcon"); } else { - LogError() << "Could not copy" << iconToBeUsed << "to" << targetIconPath << "\n"; - exit(1); + deploy.LogError() << "Could not copy" << iconToBeUsed << "to" << targetIconPath << "\n"; + + return 1; } } } @@ -374,103 +387,106 @@ int main(int argc, char **argv) // Set options from command line if (cliParser.isSet(noPluginsOpt)) { - LogDebug() << "Argument found:" << noPluginsOpt.valueName(); + deploy.LogDebug() << "Argument found:" << noPluginsOpt.valueName(); plugins = false; } if (cliParser.isSet(appimageOpt)) { - LogDebug() << "Argument found:" << appimageOpt.valueName(); + deploy.LogDebug() << "Argument found:" << appimageOpt.valueName(); appimage = true; - bundleAllButCoreLibs = true; + deploy.bundleAllButCoreLibs = true; } if (cliParser.isSet(noStripOpt)) { - LogDebug() << "Argument found:" << noStripOpt.valueName(); - runStripEnabled = false; + deploy.LogDebug() << "Argument found:" << noStripOpt.valueName(); + deploy.runStripEnabled = false; } if (cliParser.isSet(bundleNonQtLibsOpt)) { - LogDebug() << "Argument found:" << bundleNonQtLibsOpt.valueName(); - bundleAllButCoreLibs = true; + deploy.LogDebug() << "Argument found:" << bundleNonQtLibsOpt.valueName(); + deploy.bundleAllButCoreLibs = true; } if (cliParser.isSet(verboseOpt)) { - LogDebug() << "Argument found:" << verboseOpt.valueName(); + deploy.LogDebug() << "Argument found:" << verboseOpt.valueName(); bool ok = false; int number = cliParser.value(verboseOpt).toInt(&ok); if (ok) - logLevel = number; + deploy.logLevel = number; else - LogError() << "Could not parse verbose level"; + deploy.LogError() << "Could not parse verbose level"; } if (cliParser.isSet(executableOpt)) { - LogDebug() << "Argument found:" << executableOpt.valueName(); + deploy.LogDebug() << "Argument found:" << executableOpt.valueName(); QString executables = cliParser.value(executableOpt).trimmed(); if (!executables.isEmpty()) additionalExecutables << executables; else - LogError() << "Missing executable path"; + deploy.LogError() << "Missing executable path"; } if (cliParser.isSet(qmldirOpt)) { - LogDebug() << "Argument found:" << qmldirOpt.valueName(); + deploy.LogDebug() << "Argument found:" << qmldirOpt.valueName(); QString dirs = cliParser.value(qmldirOpt).trimmed(); qmldirArgumentUsed = true; if (!dirs.isEmpty()) qmlDirs << dirs; else - LogError() << "Missing qml directory path"; + deploy.LogError() << "Missing qml directory path"; } if (cliParser.isSet(alwaysOverwriteOpt)) { - LogDebug() << "Argument found:" << alwaysOverwriteOpt.valueName(); - alwaysOwerwriteEnabled = true; + deploy.LogDebug() << "Argument found:" << alwaysOverwriteOpt.valueName(); + deploy.alwaysOwerwriteEnabled = true; } - if (appimage) { - if(checkAppImagePrerequisites(appDirPath) == false){ - LogError() << "checkAppImagePrerequisites failed\n"; - return 1; - } + if (appimage && !deploy.checkAppImagePrerequisites(appDirPath)) { + deploy.LogError() << "checkAppImagePrerequisites failed\n"; + + return 1; } - DeploymentInfo deploymentInfo = deployQtLibraries(appDirPath, additionalExecutables); + auto deploymentInfo = deploy.deployQtLibraries(appDirPath, additionalExecutables); // Convenience: Look for .qml files in the current directoty if no -qmldir specified. if (qmlDirs.isEmpty()) { QDir dir; - if (!dir.entryList(QStringList() << QStringLiteral("*.qml")).isEmpty()) { + + if (!dir.entryList({QStringLiteral("*.qml")}).isEmpty()) qmlDirs += QStringLiteral("."); - } } if (!qmlDirs.isEmpty()) { - bool ok = deployQmlImports(appDirPath, deploymentInfo, qmlDirs); + bool ok = deploy.deployQmlImports(appDirPath, deploymentInfo, qmlDirs); + if (!ok && qmldirArgumentUsed) return 1; // exit if the user explicitly asked for qml import deployment + // Update deploymentInfo.deployedLibraries - the QML imports // may have brought in extra libraries as dependencies. - deploymentInfo.deployedLibraries += findAppLibraries(appDirPath); + deploymentInfo.deployedLibraries += deploy.findAppLibraries(appDirPath); deploymentInfo.deployedLibraries = deploymentInfo.deployedLibraries.toSet().toList(); } if (plugins && !deploymentInfo.qtPath.isEmpty()) { if (deploymentInfo.pluginPath.isEmpty()) deploymentInfo.pluginPath = QDir::cleanPath(deploymentInfo.qtPath + "/../plugins"); - deployPlugins(appDirPath, deploymentInfo); + + deploy.deployPlugins(appDirPath, deploymentInfo); } - if (runStripEnabled) - stripAppBinary(appDirPath); + if (deploy.runStripEnabled) + deploy.stripAppBinary(appDirPath); if (appimage) { - int result = createAppImage(appDirPath); - LogDebug() << "result:" << result; - exit(result); + int result = deploy.createAppImage(appDirPath); + deploy.LogDebug() << "result:" << result; + + return result; } return 0; diff --git a/src/shared.cpp b/src/shared.cpp index 0811e296..b2a4b8e0 100644 --- a/src/shared.cpp +++ b/src/shared.cpp @@ -27,15 +27,7 @@ ****************************************************************************/ #include -#include -#include -#include -#include #include -#include -#include -#include -#include #include #include #include @@ -47,22 +39,6 @@ #include "shared.h" -QString appBinaryPath; -bool runStripEnabled = true; -bool bundleAllButCoreLibs = false; -bool fhsLikeMode = false; -QString fhsPrefix; -bool alwaysOwerwriteEnabled = false; -QStringList librarySearchPath; -bool appstoreCompliant = false; -int logLevel = 1; -bool deployLibrary = false; - -using std::cout; -using std::endl; - -QMap qtToBeBundledInfo; - bool operator==(const LibraryInfo &a, const LibraryInfo &b) { return ((a.libraryPath == b.libraryPath) && (a.binaryPath == b.binaryPath)); @@ -86,75 +62,69 @@ QDebug operator<<(QDebug debug, const LibraryInfo &info) return debug; } -QString bundleLibraryDirectory; - inline QDebug operator<<(QDebug debug, const AppDirInfo &info) { debug << "Application bundle path" << info.path << "\n"; debug << "Binary path" << info.binaryPath << "\n"; debug << "Additional libraries" << info.libraryPaths << "\n"; + return debug; } -// Determine whether the given 'ldd' output contains a Linux VDSO -// shared object. The name of the VDSO object differs depending -// on architecture. See "vDSO names" in the notes section of vdso(7) -// for more information. -static bool lddOutputContainsLinuxVDSO(const QString &lddOutput) { - // aarch64, arm, mips, x86_64, x86/x32 - if (lddOutput.contains(QStringLiteral("linux-vdso.so.1"))) { - return true; - // ppc32, s390 - } else if (lddOutput.contains(QStringLiteral("linux-vdso32.so.1"))) { - return true; - // ppc64, s390x - } else if (lddOutput.contains(QStringLiteral("linux-vdso64.so.1"))) { - return true; - // ia64, sh, i386 - } else if (lddOutput.contains(QStringLiteral("linux-gate.so.1"))) { - return true; - } - return false; +Deploy::Deploy(): + fhsLikeMode(false), + bundleAllButCoreLibs(false), + runStripEnabled(true), + alwaysOwerwriteEnabled(false), + logLevel(1), + m_appstoreCompliant(false), + m_deployLibrary(false) +{ } -bool copyFilePrintStatus(const QString &from, const QString &to) +Deploy::~Deploy() { - if (QFile(to).exists()) { - if (alwaysOwerwriteEnabled) { - QFile(to).remove(); - } else { - LogDebug() << QFileInfo(to).fileName() << "already deployed, skipping."; - return false; - } - } +} - if (QFile::copy(from, to)) { - QFile dest(to); - dest.setPermissions(dest.permissions() | QFile::WriteOwner | QFile::WriteUser); - LogNormal() << " copied:" << from; - LogNormal() << " to" << to; +void Deploy::changeQtLibraries(const QList libraries, + const QStringList &binaryPaths, + const QString &absoluteQtPath) +{ + LogNormal() << "Changing" << binaryPaths << "to link against"; + LogNormal() << "Qt in" << absoluteQtPath; + QString finalQtPath = absoluteQtPath; - // The source file might not have write permissions set. Set the - // write permission on the target file to make sure we can use - // install_name_tool on it later. - QFile toFile(to); - if (toFile.permissions() & QFile::WriteOwner) - return true; + finalQtPath += "/lib/"; - if (!toFile.setPermissions(toFile.permissions() | QFile::WriteOwner)) { - LogError() << "Failed to set u+w permissions on target file: " << to; - return false; - } + foreach (LibraryInfo library, libraries) { + const QString oldBinaryId = library.installName; + const QString newBinaryId = finalQtPath + library.libraryName + library.binaryPath; - return true; + // FIXME: This code does nothing. + } +} + +void Deploy::changeQtLibraries(const QString appPath, const QString &qtPath) +{ + auto libraryPaths = this->findAppLibraries(appPath); + auto libraries = + this->getQtLibrariesForPaths(QStringList() << this->appBinaryPath << libraryPaths, + appPath, + this->getBinaryRPaths(this->appBinaryPath, true)); + + if (libraries.isEmpty()) { + LogWarning() << "Could not find any _external_ Qt libraries to change in" << appPath; + + return; } else { - LogError() << "file copy failed from" << from; - LogError() << " to" << to; - return false; + auto absoluteQtPath = QDir(qtPath).absolutePath(); + this->changeQtLibraries(libraries, + QStringList() << this->appBinaryPath << libraryPaths, + absoluteQtPath); } } -LddInfo findDependencyInfo(const QString &binaryPath) +LddInfo Deploy::findDependencyInfo(const QString &binaryPath) { LddInfo info; info.binaryPath = binaryPath; @@ -167,6 +137,7 @@ LddInfo findDependencyInfo(const QString &binaryPath) if (ldd.exitStatus() != QProcess::NormalExit || ldd.exitCode() != 0) { LogError() << "findDependencyInfo:" << ldd.readAllStandardError(); + return info; } @@ -174,10 +145,11 @@ LddInfo findDependencyInfo(const QString &binaryPath) QString output = ldd.readAllStandardOutput(); QStringList outputLines = output.split("\n", QString::SkipEmptyParts); + if (outputLines.size() < 2) { - if ((output.contains("statically linked") == false)){ + if (!output.contains("statically linked")) LogError() << "Could not parse ldd output under 2 lines:" << output; - } + return info; } @@ -190,26 +162,25 @@ LddInfo findDependencyInfo(const QString &binaryPath) } } - if ((binaryPath.contains(".so.") || binaryPath.endsWith(".so")) && (!lddOutputContainsLinuxVDSO(output))) { + if ((binaryPath.contains(".so.") || binaryPath.endsWith(".so")) + && !lddOutputContainsLinuxVDSO(output)) { const QRegularExpressionMatch match = regexp.match(outputLines.first()); - if (match.hasMatch()) { + + if (match.hasMatch()) info.installName = match.captured(1); - } else { + else LogError() << "Could not parse ldd output line:" << outputLines.first(); - } + outputLines.removeFirst(); } foreach (const QString &outputLine, outputLines) { const QRegularExpressionMatch match = regexp.match(outputLine); + if (match.hasMatch()) { DylibInfo dylib; dylib.binaryPath = match.captured(1).trimmed(); LogDebug() << " dylib.binaryPath" << dylib.binaryPath; - /* - dylib.compatibilityVersion = 0; - dylib.currentVersion = 0; - */ info.dependencies << dylib; } } @@ -217,22 +188,20 @@ LddInfo findDependencyInfo(const QString &binaryPath) return info; } -int containsHowOften(QStringList haystack, QString needle) { - int result = haystack.filter(needle).length(); - return result; -} - -LibraryInfo parseLddLibraryLine(const QString &line, const QString &appDirPath, const QSet &rpaths) +LibraryInfo Deploy::parseLddLibraryLine(const QString &line, + const QString &appDirPath, + const QSet &rpaths) { - (void)rpaths; + Q_UNUSED(rpaths); - if(fhsLikeMode == false){ - bundleLibraryDirectory= "lib"; // relative to bundle - } else { + if (fhsLikeMode == false) { QString relativePrefix = fhsPrefix.replace(appDirPath+"/", ""); - bundleLibraryDirectory = relativePrefix + "/lib/"; + this->m_bundleLibraryDirectory = relativePrefix + "/lib/"; + } else { + this->m_bundleLibraryDirectory= "lib"; // relative to bundle } - LogDebug() << "bundleLibraryDirectory:" << bundleLibraryDirectory; + + LogDebug() << "bundleLibraryDirectory:" << this->m_bundleLibraryDirectory; LibraryInfo info; QString trimmed = line.trimmed(); @@ -242,8 +211,7 @@ LibraryInfo parseLddLibraryLine(const QString &line, const QString &appDirPath, if (trimmed.isEmpty()) return info; - - if(bundleAllButCoreLibs) { + if (bundleAllButCoreLibs) { /* Bundle every lib including the low-level ones except those that are explicitly blacklisted. This is more suitable for bundling in a way that is portable between different distributions and target systems. @@ -260,9 +228,11 @@ LibraryInfo parseLddLibraryLine(const QString &line, const QString &appDirPath, QStringList excludelist; excludelist << "libasound.so.2" << "libcom_err.so.2" << "libcrypt.so.1" << "libc.so.6" << "libdl.so.2" << "libdrm.so.2" << "libexpat.so.1" << "libfontconfig.so.1" << "libgcc_s.so.1" << "libgdk_pixbuf-2.0.so.0" << "libgdk-x11-2.0.so.0" << "libgio-2.0.so.0" << "libglib-2.0.so.0" << "libGL.so.1" << "libgobject-2.0.so.0" << "libgpg-error.so.0" << "libgssapi_krb5.so.2" << "libgtk-x11-2.0.so.0" << "libhcrypto.so.4" << "libhx509.so.5" << "libICE.so.6" << "libidn.so.11" << "libk5crypto.so.3" << "libkeyutils.so.1" << "libkrb5.so.26" << "libkrb5.so.3" << "libkrb5support.so.0" << "libm.so.6" << "libnss3.so" << "libnssutil3.so" << "libp11-kit.so.0" << "libpcre.so.3" << "libpthread.so.0" << "libresolv.so.2" << "libroken.so.18" << "librt.so.1" << "libselinux.so.1" << "libSM.so.6" << "libstdc++.so.6" << "libusb-1.0.so.0" << "libuuid.so.1" << "libwind.so.0" << "libX11.so.6" << "libxcb.so.1" << "libz.so.1"; LogDebug() << "excludelist:" << excludelist; - if (! trimmed.contains("libicu")) { + + if (!trimmed.contains("libicu")) { if (containsHowOften(excludelist, QFileInfo(trimmed).completeBaseName())) { LogDebug() << "Skipping blacklisted" << trimmed; + return info; } } @@ -276,7 +246,7 @@ LibraryInfo parseLddLibraryLine(const QString &line, const QString &appDirPath, */ // Manual make of Qt deploys it to /usr/local/Qt-x.x.x so we cannot remove this path just like that, so let's allow known libs of Qt. if (!trimmed.contains("libicu") && !trimmed.contains("lib/libQt") && !trimmed.contains("lib/libqgsttools")) { - if ((trimmed.startsWith("/usr") or (trimmed.startsWith("/lib")))) { + if (trimmed.startsWith("/usr") || trimmed.startsWith("/lib")) { return info; } } @@ -291,10 +261,12 @@ LibraryInfo parseLddLibraryLine(const QString &line, const QString &appDirPath, // Split the line into [Qt-path]/lib/qt[Module].library/Versions/[Version]/ QStringList parts = trimmed.split("/"); + while (part < parts.count()) { - const QString currentPart = parts.at(part).simplified() ; - ++part; - if (currentPart == "") + const QString currentPart = parts.at(part).simplified(); + part++; + + if (currentPart.isEmpty()) continue; if (state == QtPath) { @@ -303,30 +275,40 @@ LibraryInfo parseLddLibraryLine(const QString &line, const QString &appDirPath, info.libraryDirectory += "/" + (qtPath + currentPart + "/").simplified(); LogDebug() << "info.libraryDirectory:" << info.libraryDirectory; state = LibraryName; + continue; - } else if (trimmed.startsWith("/") == false) { // If the line does not contain a full path, the app is using a binary Qt package. + } else if (!trimmed.startsWith("/")) { + // If the line does not contain a full path, the app is using a binary Qt package. QStringList partsCopy = parts; partsCopy.removeLast(); - foreach (QString path, librarySearchPath) { + + foreach (QString path, this->m_librarySearchPath) { if (!path.endsWith("/")) path += '/'; + QString nameInPath = path + parts.join("/"); + if (QFile::exists(nameInPath)) { info.libraryDirectory = path + partsCopy.join("/"); + break; } } + if (info.libraryDirectory.isEmpty()) info.libraryDirectory = "/usr/lib/" + partsCopy.join("/"); + if (!info.libraryDirectory.endsWith("/")) info.libraryDirectory += "/"; + state = LibraryName; - --part; + part--; + continue; } - qtPath += (currentPart + "/"); - } if (state == LibraryName) { + qtPath += currentPart + "/"; + } else if (state == LibraryName) { name = currentPart; info.isDylib = true; info.libraryName = name; @@ -334,7 +316,7 @@ LibraryInfo parseLddLibraryLine(const QString &line, const QString &appDirPath, info.deployedInstallName = "$ORIGIN"; // + info.binaryName; info.libraryPath = info.libraryDirectory + info.binaryName; info.sourceFilePath = info.libraryPath; - info.libraryDestinationDirectory = bundleLibraryDirectory + "/"; + info.libraryDestinationDirectory = this->m_bundleLibraryDirectory + "/"; info.binaryDestinationDirectory = info.libraryDestinationDirectory; info.binaryDirectory = info.libraryDirectory; info.binaryPath = info.libraryPath; @@ -348,6 +330,7 @@ LibraryInfo parseLddLibraryLine(const QString &line, const QString &appDirPath, if (!info.sourceFilePath.isEmpty() && QFile::exists(info.sourceFilePath)) { info.installName = findDependencyInfo(info.sourceFilePath).installName; + if (info.installName.startsWith("@rpath/")) info.deployedInstallName = info.installName; } @@ -355,1043 +338,1149 @@ LibraryInfo parseLddLibraryLine(const QString &line, const QString &appDirPath, return info; } -QStringList findAppLibraries(const QString &appDirPath) +QList Deploy::getQtLibraries(const QString &path, + const QString &appDirPath, + const QSet &rpaths) { - QStringList result; - // .so - QDirIterator iter(appDirPath, QStringList() << QString::fromLatin1("*.so"), - QDir::Files, QDirIterator::Subdirectories); - - while (iter.hasNext()) { - iter.next(); - result << iter.fileInfo().filePath(); - } - // .so.*, FIXME: Is the above really needed or is it covered by the below too? - QDirIterator iter2(appDirPath, QStringList() << QString::fromLatin1("*.so*"), - QDir::Files, QDirIterator::Subdirectories); + const LddInfo info = findDependencyInfo(path); - while (iter2.hasNext()) { - iter2.next(); - result << iter2.fileInfo().filePath(); - } - return result; + return getQtLibraries(info.dependencies, appDirPath, rpaths + getBinaryRPaths(path)); } -QList getQtLibraries(const QList &dependencies, const QString &appDirPath, const QSet &rpaths) +QList Deploy::getQtLibraries(const QList &dependencies, + const QString &appDirPath, + const QSet &rpaths) { QList libraries; + foreach (const DylibInfo &dylibInfo, dependencies) { LibraryInfo info = parseLddLibraryLine(dylibInfo.binaryPath, appDirPath, rpaths); - if (info.libraryName.isEmpty() == false) { + + if (!info.libraryName.isEmpty()) { LogDebug() << "Adding library:"; LogDebug() << info; libraries.append(info); } } + return libraries; } -// TODO: Switch the following to using patchelf -QSet getBinaryRPaths(const QString &path, bool resolve = true, QString executablePath = QString()) +/* + * Deploys the the libraries listed into an app bundle. + * The libraries are searched for dependencies, which are also deployed. + * (deploying Qt3Support will also deploy QtNetwork and QtSql for example.) + * Returns a DeploymentInfo structure containing the Qt path used and a + * a list of actually deployed libraries. + */ +DeploymentInfo Deploy::deployQtLibraries(const QString &appDirPath, + const QStringList &additionalExecutables) { - QSet rpaths; - - QProcess objdump; - objdump.start("objdump", QStringList() << "-x" << path); + AppDirInfo applicationBundle; - if (!objdump.waitForStarted()) { - if(objdump.errorString().contains("execvp: No such file or directory")){ - LogError() << "Could not start objdump."; - LogError() << "Make sure it is installed on your $PATH."; - } else { - LogError() << "Could not start objdump. Process error is" << objdump.errorString(); - } - exit(1); - } + applicationBundle.path = appDirPath; + LogDebug() << "applicationBundle.path:" << applicationBundle.path; + applicationBundle.binaryPath = appBinaryPath; + LogDebug() << "applicationBundle.binaryPath:" << applicationBundle.binaryPath; - objdump.waitForFinished(); + // Find out whether Qt is a dependency of the application to be bundled + int qtDetected = 0; + LddInfo lddInfo = findDependencyInfo(appBinaryPath); - if (objdump.exitCode() != 0) { - LogError() << "getBinaryRPaths:" << objdump.readAllStandardError(); - } + foreach (const DylibInfo dep, lddInfo.dependencies) { + LogDebug() << "dep.binaryPath" << dep.binaryPath; - if (resolve && executablePath.isEmpty()) { - executablePath = path; - } + if (dep.binaryPath.contains("libQt5")) + qtDetected = 5; - QString output = objdump.readAllStandardOutput(); - QStringList outputLines = output.split("\n"); - QStringListIterator i(outputLines); + if (dep.binaryPath.contains("libQtCore.so.4")) + qtDetected = 4; + } - while (i.hasNext()) { - if (i.next().contains("RUNPATH") && i.hasNext()) { - i.previous(); - const QString &rpathCmd = i.next(); - int pathStart = rpathCmd.indexOf("RUNPATH"); - if (pathStart >= 0) { - QString rpath = rpathCmd.mid(pathStart+8).trimmed(); - LogDebug() << "rpath:" << rpath; - rpaths << rpath; - } - } - } + if (qtDetected != 0) { + // Determine the location of the Qt to be bundled + LogDebug() << "Using qmake to determine the location of the Qt to be bundled"; - return rpaths; -} + QString qmakePath = ""; -QList getQtLibraries(const QString &path, const QString &appDirPath, const QSet &rpaths) -{ - const LddInfo info = findDependencyInfo(path); - return getQtLibraries(info.dependencies, appDirPath, rpaths + getBinaryRPaths(path)); -} + // The upstream name of the binary is "qmake", for Qt 4 and Qt 5 + qmakePath = QStandardPaths::findExecutable("qmake"); -QList getQtLibrariesForPaths(const QStringList &paths, const QString &appDirPath, const QSet &rpaths) -{ - QList result; - QSet existing; + // But openSUSE has qmake for Qt 4 and qmake-qt5 for Qt 5 + // Qt 4 on Fedora comes with suffix -qt4 + // http://www.geopsy.org/wiki/index.php/Installing_Qt_binary_packages + if (qmakePath.isEmpty()) { + if (qtDetected == 5) + qmakePath = QStandardPaths::findExecutable("qmake-qt5"); - foreach (const QString &path, paths) { - foreach (const LibraryInfo &info, getQtLibraries(path, appDirPath, rpaths)) { - if (!existing.contains(info.libraryPath)) { // avoid duplicates - existing.insert(info.libraryPath); - result << info; - } - } - } - return result; -} + if (qtDetected == 4) + qmakePath = QStandardPaths::findExecutable("qmake-qt4"); + } -QStringList getBinaryDependencies(const QString executablePath, - const QString &path, - const QList &additionalBinariesContainingRpaths) -{ - QStringList binaries; + if (qmakePath.isEmpty()) { + LogError() << "qmake not found on the $PATH"; + exit(1); + } - const QList dependencies = findDependencyInfo(path).dependencies; + QString output = captureOutput(qmakePath + " -query"); + QStringList outputLines = output.split("\n", QString::SkipEmptyParts); - bool rpathsLoaded = false; - QSet rpaths; + foreach (const QString &outputLine, outputLines) { + int colonIndex = outputLine.indexOf(QLatin1Char(':')); - // return bundle-local dependencies. (those starting with @executable_path) - foreach (const DylibInfo &info, dependencies) { - QString trimmedLine = info.binaryPath; - if (trimmedLine.startsWith("@executable_path/")) { - QString binary = QDir::cleanPath(executablePath + trimmedLine.mid(QStringLiteral("@executable_path/").length())); - if (binary != path) - binaries.append(binary); - } else if (trimmedLine.startsWith("@rpath/")) { - if (!rpathsLoaded) { - rpaths = getBinaryRPaths(path, true, executablePath); - foreach (const QString &binaryPath, additionalBinariesContainingRpaths) { - QSet binaryRpaths = getBinaryRPaths(binaryPath, true); - rpaths += binaryRpaths; - } - rpathsLoaded = true; - } - bool resolved = false; - foreach (const QString &rpath, rpaths) { - QString binary = QDir::cleanPath(rpath + "/" + trimmedLine.mid(QStringLiteral("@rpath/").length())); - LogDebug() << "Checking for" << binary; - if (QFile::exists(binary)) { - binaries.append(binary); - resolved = true; - break; - } - } - if (!resolved && !rpaths.isEmpty()) { - LogError() << "Cannot resolve rpath" << trimmedLine; - LogError() << " using" << rpaths; - } - } - } + if (colonIndex != -1) { + QString name = outputLine.left(colonIndex); + QString value = outputLine.mid(colonIndex + 1); + this->m_qtToBeBundledInfo.insert(name, value); + } + } - return binaries; -} + QString qtLibsPath = this->m_qtToBeBundledInfo.value("QT_INSTALL_LIBS"); -// copies everything _inside_ sourcePath to destinationPath -bool recursiveCopy(const QString &sourcePath, const QString &destinationPath) -{ - if (!QDir(sourcePath).exists()) - return false; - QDir().mkpath(destinationPath); + if (qtLibsPath.isEmpty() || !QFile::exists(qtLibsPath)) { + LogError() << "Qt path could not be determined from qmake on the $PATH"; + LogError() << "Make sure you have the correct Qt on your $PATH"; + LogError() << "You can check this with qmake -v"; + exit(1); + } else { + LogDebug() << "Qt libs path determined from qmake:" << qtLibsPath; + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + QString oldPath = env.value("LD_LIBRARY_PATH"); + QString newPath = qtLibsPath + ":" + oldPath; // FIXME: If we use a ldd replacement, we still need to observe this path + // FIXME: Directory layout might be different for system Qt; cannot assume lib/ to always be inside the Qt directory + LogDebug() << "Changed LD_LIBRARY_PATH:" << newPath; + setenv("LD_LIBRARY_PATH",newPath.toUtf8().constData(),1); + } + } - LogNormal() << "copy:" << sourcePath << destinationPath; + if (fhsLikeMode) + changeIdentification("$ORIGIN/../lib/" + this->m_bundleLibraryDirectory, QFileInfo(applicationBundle.binaryPath).canonicalFilePath()); + else + changeIdentification("$ORIGIN/lib/" + this->m_bundleLibraryDirectory, QFileInfo(applicationBundle.binaryPath).canonicalFilePath()); - QStringList files = QDir(sourcePath).entryList(QStringList() << "*", QDir::Files | QDir::NoDotAndDotDot); - foreach (QString file, files) { - const QString fileSourcePath = sourcePath + "/" + file; - const QString fileDestinationPath = destinationPath + "/" + file; - copyFilePrintStatus(fileSourcePath, fileDestinationPath); - } + applicationBundle.libraryPaths = findAppLibraries(appDirPath); + LogDebug() << "applicationBundle.libraryPaths:" << applicationBundle.libraryPaths; + LogDebug() << "additionalExecutables:" << additionalExecutables; - QStringList subdirs = QDir(sourcePath).entryList(QStringList() << "*", QDir::Dirs | QDir::NoDotAndDotDot); - foreach (QString dir, subdirs) { - recursiveCopy(sourcePath + "/" + dir, destinationPath + "/" + dir); - } - return true; -} + QStringList allBinaryPaths = QStringList() << applicationBundle.binaryPath << applicationBundle.libraryPaths + << additionalExecutables; + LogDebug() << "allBinaryPaths:" << allBinaryPaths; -void recursiveCopyAndDeploy(const QString &appDirPath, const QSet &rpaths, const QString &sourcePath, const QString &destinationPath) -{ - QDir().mkpath(destinationPath); + QSet allRPaths = getBinaryRPaths(applicationBundle.binaryPath, true); + allRPaths.insert(QLibraryInfo::location(QLibraryInfo::LibrariesPath)); + LogDebug() << "allRPaths:" << allRPaths; - LogNormal() << "copy:" << sourcePath << destinationPath; + QList libraries = getQtLibrariesForPaths(allBinaryPaths, appDirPath, allRPaths); - QStringList files = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Files | QDir::NoDotAndDotDot); - foreach (QString file, files) { - const QString fileSourcePath = sourcePath + QLatin1Char('/') + file; + if (libraries.isEmpty() && !alwaysOwerwriteEnabled) { + LogWarning() << "Could not find any external Qt libraries to deploy in" << appDirPath; + LogWarning() << "Perhaps linuxdeployqt was already used on" << appDirPath << "?"; + LogWarning() << "If so, you will need to rebuild" << appDirPath << "before trying again."; + LogWarning() << "Or ldd does not find the external Qt libraries but sees the system ones."; + LogWarning() << "If so, you will need to set LD_LIBRARY_PATH to the directory containing the external Qt libraries before trying again."; + LogWarning() << "FIXME: https://github.com/probonopd/linuxdeployqt/issues/2"; - QString fileDestinationPath = destinationPath + QLatin1Char('/') + file; - copyFilePrintStatus(fileSourcePath, fileDestinationPath); + return DeploymentInfo(); + } else { + return deployQtLibraries(libraries, applicationBundle.path, allBinaryPaths, !additionalExecutables.isEmpty()); + } +} - if(fileDestinationPath.endsWith(".so")){ +DeploymentInfo Deploy::deployQtLibraries(QList libraries, + const QString &bundlePath, + const QStringList &binaryPaths, + bool useLoaderPath) +{ + LogNormal() << "Deploying the following libraries:" << binaryPaths; + QStringList copiedLibraries; + DeploymentInfo deploymentInfo; + deploymentInfo.useLoaderPath = useLoaderPath; + deploymentInfo.pluginPath = this->m_qtToBeBundledInfo.value("QT_INSTALL_PLUGINS"); + QSet rpathsUsed; - LogDebug() << "Deploying .so in QML import" << fileSourcePath; - runStrip(fileDestinationPath); + while (!libraries.isEmpty()) { + const LibraryInfo library = libraries.takeFirst(); + copiedLibraries.append(library.libraryName); - // Find out the relative path to the lib/ directory and set it as the rpath - // FIXME: remove code duplication - the next few lines exist elsewhere already - if(fhsLikeMode == false){ - bundleLibraryDirectory= "lib"; // relative to bundle - } else { - QString relativePrefix = fhsPrefix.replace(appDirPath+"/", ""); - bundleLibraryDirectory = relativePrefix + "/lib/"; - } + if (library.libraryName.contains("libQt") and library.libraryName.contains("Core.so")) { + LogNormal() << "Setting deploymentInfo.qtPath to:" << library.libraryDirectory; + deploymentInfo.qtPath = library.libraryDirectory; + } - QDir dir(QFileInfo(fileDestinationPath).canonicalFilePath()); - QString relativePath = dir.relativeFilePath(appDirPath + "/" + bundleLibraryDirectory); - relativePath.remove(0, 3); // remove initial '../' - changeIdentification("$ORIGIN:$ORIGIN/" + relativePath, QFileInfo(fileDestinationPath).canonicalFilePath()); + if (library.libraryDirectory.startsWith(bundlePath)) { + LogNormal() << library.libraryName << "already deployed, skipping."; - QList libraries = getQtLibraries(fileSourcePath, appDirPath, QSet()); - deployQtLibraries(libraries, appDirPath, QStringList() << destinationPath, false); + continue; } - } - QStringList subdirs = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Dirs | QDir::NoDotAndDotDot); - foreach (QString dir, subdirs) { - recursiveCopyAndDeploy(appDirPath, rpaths, sourcePath + QLatin1Char('/') + dir, destinationPath + QLatin1Char('/') + dir); - } -} + if (library.rpathUsed.isEmpty() != true) { + rpathsUsed << library.rpathUsed; + } -QString copyDylib(const LibraryInfo &library, const QString path) -{ - if (!QFile::exists(library.sourceFilePath)) { - LogError() << "no file at" << library.sourceFilePath; - return QString(); - } + // Copy the library to the app bundle. + const QString deployedBinaryPath = copyDylib(library, bundlePath); + // Skip the rest if already was deployed. + if (deployedBinaryPath.isNull()) + continue; - // Construct destination paths. The full path typically looks like - // MyApp.app/Contents/Libraries/libfoo.dylib - QString dylibDestinationDirectory = path + QLatin1Char('/') + library.libraryDestinationDirectory; - QString dylibDestinationBinaryPath = dylibDestinationDirectory + QLatin1Char('/') + library.binaryName; + runStrip(deployedBinaryPath); - // Create destination directory - if (!QDir().mkpath(dylibDestinationDirectory)) { - LogError() << "could not create destination directory" << dylibDestinationDirectory; - return QString(); - } + if (!library.rpathUsed.length()) + changeIdentification(library.deployedInstallName, QFileInfo(deployedBinaryPath).canonicalFilePath()); - // Retrun if the dylib has aleardy been deployed - if (QFileInfo(dylibDestinationBinaryPath).exists() && !alwaysOwerwriteEnabled) - return dylibDestinationBinaryPath; + // Check for library dependencies + QList dependencies = getQtLibraries(deployedBinaryPath, bundlePath, rpathsUsed); - // Copy dylib binary - copyFilePrintStatus(library.sourceFilePath, dylibDestinationBinaryPath); - return dylibDestinationBinaryPath; -} + foreach (LibraryInfo dependency, dependencies) { + if (!dependency.rpathUsed.isEmpty()) + rpathsUsed << dependency.rpathUsed; -void runPatchelf(QStringList options) -{ - QProcess patchelftool; - patchelftool.start("patchelf", options); - if (!patchelftool.waitForStarted()) { - if(patchelftool.errorString().contains("execvp: No such file or directory")){ - LogError() << "Could not start patchelf."; - LogError() << "Make sure it is installed on your $PATH, e.g., in /usr/local/bin."; - LogError() << "You can get it from https://nixos.org/patchelf.html."; - } else { - LogError() << "Could not start patchelftool. Process error is" << patchelftool.errorString(); + // Deploy library if necessary. + if (!copiedLibraries.contains(dependency.libraryName) + && !libraries.contains(dependency)) { + libraries.append(dependency); + } } - exit(1); - } - patchelftool.waitForFinished(); - if (patchelftool.exitCode() != 0) { - LogError() << "runPatchelf:" << patchelftool.readAllStandardError(); - LogError() << "runPatchelf:" << patchelftool.readAllStandardOutput(); - // exit(1); // Do not exit because this could be a script that patchelf can't work on - } -} - -bool patchQtCore(const QString &path, const QString &variable, const QString &value) -{ - QFile file(path); - if (!file.open(QIODevice::ReadWrite)) { - LogWarning() << QString::fromLatin1("Unable to patch %1: %2").arg( - QDir::toNativeSeparators(path), file.errorString()); - return false; } - QByteArray content = file.readAll(); - if (content.isEmpty()) { - LogWarning() << QString::fromLatin1("Unable to patch %1: Could not read file content").arg( - QDir::toNativeSeparators(path)); - return false; - } + deploymentInfo.deployedLibraries = copiedLibraries; + deploymentInfo.rpathsUsed += rpathsUsed; - QString searchString = QString::fromLatin1("%1=").arg(variable); - QByteArray searchStringQByteArray = searchString.toLatin1().data(); + return deploymentInfo; +} - int startPos = content.indexOf(searchStringQByteArray); - if (startPos != -1) { - LogNormal() << QString::fromLatin1( - "Patching value of %2 in %1 to '%3'").arg(QDir::toNativeSeparators(path), variable, value); - } - startPos += searchStringQByteArray.length(); - int endPos = content.indexOf(char(0), startPos); - if (endPos == -1) { - LogWarning() << QString::fromLatin1("Unable to patch %1: Internal error").arg( - QDir::toNativeSeparators(path)); - return false; - } +void Deploy::deployPlugins(const QString &appDirPath, + DeploymentInfo deploymentInfo) +{ + AppDirInfo applicationBundle; + applicationBundle.path = appDirPath; + applicationBundle.binaryPath = appBinaryPath; - QByteArray replacement = QByteArray(endPos - startPos, char(0)); - QByteArray replacementBegin = value.toLatin1().data(); - replacement.prepend(replacementBegin); - replacement.truncate(endPos - startPos); + QString pluginDestinationPath; - content.replace(startPos, endPos - startPos, replacement); + if (fhsLikeMode) { + QFileInfo qfi(applicationBundle.binaryPath); + QString qtTargetDir = qfi.absoluteDir().absolutePath() + "/../"; + pluginDestinationPath = qtTargetDir + "/plugins"; + } else { + pluginDestinationPath = appDirPath + "/" + "plugins"; + } - if (!file.seek(0) || (file.write(content) != content.size())) { - LogWarning() << QString::fromLatin1("Unable to patch %1: Could not write to file").arg( - QDir::toNativeSeparators(path)); - return false; - } - return true; + deployPlugins(applicationBundle, deploymentInfo.pluginPath, pluginDestinationPath, deploymentInfo); } -void changeIdentification(const QString &id, const QString &binaryPath) +void Deploy::deployPlugins(const AppDirInfo &appDirInfo, + const QString &pluginSourcePath, + const QString pluginDestinationPath, + DeploymentInfo deploymentInfo) { - LogNormal() << "Changing rpath in" << binaryPath << "to" << id; - runPatchelf(QStringList() << "--set-rpath" << id << binaryPath); + LogNormal() << "Deploying plugins from" << pluginSourcePath; - // qt_prfxpath: - if (binaryPath.contains("libQt5Core")) { - LogDebug() << "libQt5Core detected, patching its hardcoded strings"; + if (!pluginSourcePath.contains(deploymentInfo.pluginPath)) + return; - /* https://codereview.qt-project.org/gitweb?p=qt/qttools.git;a=blob_plain;f=src/windeployqt/utils.cpp;h=e89496ea1f371ed86f6937284c1c801daf576572;hb=7be81b804da102b374c2089aac38353a0383c254 - * Search for "qt_prfxpath=" in a path, and replace it with "qt_prfxpath=." or "qt_prfxpath=.." */ + // Plugin white list: + QStringList pluginList; - if(fhsLikeMode == true){ - patchQtCore(binaryPath, "qt_prfxpath", ".."); - } else { - patchQtCore(binaryPath, "qt_prfxpath", "."); + LogDebug() << "deploymentInfo.deployedLibraries before attempting to bundle required plugins:" << deploymentInfo.deployedLibraries; + + // Platform plugin: + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Gui")) { + LogDebug() << "libQt5Gui detected"; + pluginList.append("platforms/libqxcb.so"); + // All image formats (svg if QtSvg library is used) + QStringList imagePlugins = QDir(pluginSourcePath + QStringLiteral("/imageformats")).entryList(QStringList() << QStringLiteral("*.so")); + + foreach (const QString &plugin, imagePlugins) { + if (plugin.contains(QStringLiteral("qsvg"))) { + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Svg")) + pluginList.append(QStringLiteral("imageformats/") + plugin); + } else { + pluginList.append(QStringLiteral("imageformats/") + plugin); + } } + } - patchQtCore(binaryPath, "qt_adatpath", "."); - patchQtCore(binaryPath, "qt_docspath", "doc"); - patchQtCore(binaryPath, "qt_hdrspath", "include"); - patchQtCore(binaryPath, "qt_libspath", "lib"); - patchQtCore(binaryPath, "qt_lbexpath", "libexec"); - patchQtCore(binaryPath, "qt_binspath", "bin"); - patchQtCore(binaryPath, "qt_plugpath", "plugins"); - patchQtCore(binaryPath, "qt_impspath", "imports"); - patchQtCore(binaryPath, "qt_qml2path", "qml"); - patchQtCore(binaryPath, "qt_datapath", "."); - patchQtCore(binaryPath, "qt_trnspath", "translations"); - patchQtCore(binaryPath, "qt_xmplpath", "examples"); - patchQtCore(binaryPath, "qt_demopath", "demos"); - patchQtCore(binaryPath, "qt_tstspath", "tests"); - patchQtCore(binaryPath, "qt_hpfxpath", "."); - patchQtCore(binaryPath, "qt_hbinpath", "bin"); - patchQtCore(binaryPath, "qt_hdatpath", "."); - patchQtCore(binaryPath, "qt_stngpath", "."); // e.g., /opt/qt53/etc/xdg; does it load Trolltech.conf from there? + // Platform OpenGL context + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5OpenGL") + || containsHowOften(deploymentInfo.deployedLibraries, "libQt5XcbQpa")) { + QStringList xcbglintegrationPlugins = QDir(pluginSourcePath + QStringLiteral("/xcbglintegrations")).entryList(QStringList() << QStringLiteral("*.so")); - /* Qt on Arch Linux comes with more hardcoded paths - * https://github.com/probonopd/linuxdeployqt/issues/98 - patchString(binaryPath, "lib/qt/libexec", "libexec"); - patchString(binaryPath, "lib/qt/plugins", "plugins"); - patchString(binaryPath, "lib/qt/imports", "imports"); - patchString(binaryPath, "lib/qt/qml", "qml"); - patchString(binaryPath, "lib/qt", ""); - patchString(binaryPath, "share/doc/qt", "doc"); - patchString(binaryPath, "include/qt", "include"); - patchString(binaryPath, "share/qt", ""); - patchString(binaryPath, "share/qt/translations", "translations"); - patchString(binaryPath, "share/doc/qt/examples", "examples"); - */ + foreach (const QString &plugin, xcbglintegrationPlugins) + pluginList.append(QStringLiteral("xcbglintegrations/") + plugin); } -} + // Also deploy plugins/iconengines/libqsvgicon.so whenever libQt5Svg.so.* is about to be deployed, + // https://github.com/probonopd/linuxdeployqt/issues/36 + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Svg")) + pluginList.append(QStringLiteral("iconengines/libqsvgicon.so")); -void runStrip(const QString &binaryPath) -{ - if (runStripEnabled == false) - return; + // CUPS print support + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5PrintSupport")) + pluginList.append("printsupport/libcupsprintersupport.so"); - // Since we might have a symlink, we need to find its target first - QString resolvedPath = QFileInfo(binaryPath).canonicalFilePath(); + // Network + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Network")) { + QStringList bearerPlugins = QDir(pluginSourcePath + QStringLiteral("/bearer")).entryList(QStringList() << QStringLiteral("*.so")); - LogDebug() << "Determining whether to run strip:"; - LogDebug() << " checking whether" << resolvedPath << "has an rpath set"; - LogDebug() << "patchelf" << "--print-rpath" << resolvedPath; - QProcess patchelfread; - patchelfread.start("patchelf", QStringList() << "--print-rpath" << resolvedPath); - if (!patchelfread.waitForStarted()) { - if(patchelfread.errorString().contains("execvp: No such file or directory")){ - LogError() << "Could not start patchelf."; - LogError() << "Make sure it is installed on your $PATH."; - } else { - LogError() << "Could not start patchelf. Process error is" << patchelfread.errorString(); - } - // exit(1); // Do not exit because this could be a script that patchelf can't work on + foreach (const QString &plugin, bearerPlugins) + pluginList.append(QStringLiteral("bearer/") + plugin); } - patchelfread.waitForFinished(); - if (patchelfread.exitCode() != 0){ - LogError() << "Error reading rpath with patchelf" << QFileInfo(resolvedPath).completeBaseName() << ":" << patchelfread.readAllStandardError(); - LogError() << "Error reading rpath with patchelf" << QFileInfo(resolvedPath).completeBaseName() << ":" << patchelfread.readAllStandardOutput(); - exit(1); - } - - QString rpath = patchelfread.readAllStandardOutput(); - - if (rpath.startsWith("$")){ - LogDebug() << "Already contains rpath starting with $, hence not stripping"; - LogDebug() << patchelfread.readAllStandardOutput(); - return; - } + // Sql plugins if QtSql library is in use + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Sql")) { + QStringList sqlPlugins = QDir(pluginSourcePath + QStringLiteral("/sqldrivers")).entryList(QStringList() << QStringLiteral("*.so")); - LogDebug() << "Using strip:"; - LogDebug() << " stripping" << resolvedPath; - QProcess strip; - strip.start("strip", QStringList() << resolvedPath); - if (!strip.waitForStarted()) { - if(strip.errorString().contains("execvp: No such file or directory")){ - LogError() << "Could not start strip."; - LogError() << "Make sure it is installed on your $PATH."; - } else { - LogError() << "Could not start strip. Process error is" << strip.errorString(); - } - exit(1); + foreach (const QString &plugin, sqlPlugins) + pluginList.append(QStringLiteral("sqldrivers/") + plugin); } - strip.waitForFinished(); - if (strip.exitCode() == 0) - return; + // multimedia plugins if QtMultimedia library is in use + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Multimedia")) { + QStringList plugins = QDir(pluginSourcePath + QStringLiteral("/mediaservice")).entryList(QStringList() << QStringLiteral("*.so")); - if (strip.readAllStandardError().contains("Not enough room for program headers")) { - LogNormal() << QFileInfo(resolvedPath).completeBaseName() << "already stripped."; - } else { - LogError() << "Error stripping" << QFileInfo(resolvedPath).completeBaseName() << ":" << strip.readAllStandardError(); - LogError() << "Error stripping" << QFileInfo(resolvedPath).completeBaseName() << ":" << strip.readAllStandardOutput(); - exit(1); - } + foreach (const QString &plugin, plugins) + pluginList.append(QStringLiteral("mediaservice/") + plugin); -} + plugins = QDir(pluginSourcePath + QStringLiteral("/audio")).entryList(QStringList() << QStringLiteral("*.so")); -void stripAppBinary(const QString &bundlePath) -{ - (void)bundlePath; + foreach (const QString &plugin, plugins) + pluginList.append(QStringLiteral("audio/") + plugin); + } - runStrip(appBinaryPath); -} + QString sourcePath; + QString destinationPath; -/* - Deploys the the libraries listed into an app bundle. - The libraries are searched for dependencies, which are also deployed. - (deploying Qt3Support will also deploy QtNetwork and QtSql for example.) - Returns a DeploymentInfo structure containing the Qt path used and a - a list of actually deployed libraries. -*/ -DeploymentInfo deployQtLibraries(QList libraries, - const QString &bundlePath, const QStringList &binaryPaths, - bool useLoaderPath) -{ - LogNormal() << "Deploying the following libraries:" << binaryPaths; - QStringList copiedLibraries; - DeploymentInfo deploymentInfo; - deploymentInfo.useLoaderPath = useLoaderPath; - deploymentInfo.pluginPath = qtToBeBundledInfo.value("QT_INSTALL_PLUGINS"); - QSet rpathsUsed; + // Qt WebEngine if libQt5WebEngineCore is in use + // https://doc-snapshots.qt.io/qt5-5.7/qtwebengine-deploying.html + // TODO: Rather than hardcode the source paths, somehow get them dynamically + // from the Qt instance that is to be bundled (pull requests welcome!) + // especially since stuff that is supposed to come from resources actually + // seems to come in libexec in the upstream Qt binary distribution + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5WebEngineCore")) { + // Find directories with needed files: + QString qtLibexecPath = this->m_qtToBeBundledInfo.value("QT_INSTALL_LIBEXECS"); + QString qtDataPath = this->m_qtToBeBundledInfo.value("QT_INSTALL_DATA"); + QString qtTranslationsPath = this->m_qtToBeBundledInfo.value("QT_INSTALL_TRANSLATIONS"); - while (libraries.isEmpty() == false) { - const LibraryInfo library = libraries.takeFirst(); - copiedLibraries.append(library.libraryName); + // create destination directories: + QString dstLibexec; + QString dstResources; + QString dstTranslations; - if(library.libraryName.contains("libQt") and library.libraryName.contains("Core.so")) { - LogNormal() << "Setting deploymentInfo.qtPath to:" << library.libraryDirectory; - deploymentInfo.qtPath = library.libraryDirectory; + if (fhsLikeMode) { + QFileInfo qfi(appDirInfo.binaryPath); + QString qtTargetDir = qfi.absoluteDir().absolutePath() + "/../"; + dstLibexec = qtTargetDir + "/libexec"; + dstResources = qtTargetDir + "/resources"; + dstTranslations = qtTargetDir + "/translations"; + } else { + dstLibexec = appDirInfo.path + "/libexec"; + dstResources = appDirInfo.path + "/resources"; + dstTranslations = appDirInfo.path + "/translations"; } - if (library.libraryDirectory.startsWith(bundlePath)) { - LogNormal() << library.libraryName << "already deployed, skipping."; - continue; - } + QDir().mkpath(dstLibexec); + QDir().mkpath(dstResources); + QDir().mkpath(dstTranslations); - if (library.rpathUsed.isEmpty() != true) { - rpathsUsed << library.rpathUsed; - } + // WebEngine executable: + sourcePath = QDir::cleanPath(qtLibexecPath + "/QtWebEngineProcess"); + destinationPath = QDir::cleanPath(dstLibexec + "/QtWebEngineProcess"); + copyFilePrintStatus(sourcePath, destinationPath); - // Copy the library to the app bundle. - const QString deployedBinaryPath = copyDylib(library, bundlePath); - // Skip the rest if already was deployed. - if (deployedBinaryPath.isNull()) - continue; + // Resources: + sourcePath = QDir::cleanPath(qtDataPath + "/resources/qtwebengine_resources.pak"); + destinationPath = QDir::cleanPath(dstResources + "/qtwebengine_resources.pak"); + copyFilePrintStatus(sourcePath, destinationPath); + sourcePath = QDir::cleanPath(qtDataPath + "/resources/qtwebengine_devtools_resources.pak"); + destinationPath = QDir::cleanPath(dstResources + "/qtwebengine_devtools_resources.pak"); + copyFilePrintStatus(sourcePath, destinationPath); + sourcePath = QDir::cleanPath(qtDataPath + "/resources/qtwebengine_resources_100p.pak"); + destinationPath = QDir::cleanPath(dstResources + "/qtwebengine_resources_100p.pak"); + copyFilePrintStatus(sourcePath, destinationPath); + sourcePath = QDir::cleanPath(qtDataPath + "/resources/qtwebengine_resources_200p.pak"); + destinationPath = QDir::cleanPath(dstResources + "/qtwebengine_resources_200p.pak"); + copyFilePrintStatus(sourcePath, destinationPath); + sourcePath = QDir::cleanPath(qtDataPath + "/resources/icudtl.dat"); + destinationPath = QDir::cleanPath(dstResources + "/icudtl.dat"); + copyFilePrintStatus(sourcePath, destinationPath); - runStrip(deployedBinaryPath); + // Translations: + sourcePath = QDir::cleanPath(qtTranslationsPath + "/qtwebengine_locales"); + destinationPath = QDir::cleanPath(dstTranslations + "/qtwebengine_locales"); + recursiveCopy(sourcePath, destinationPath); + } - if (!library.rpathUsed.length()) { - changeIdentification(library.deployedInstallName, QFileInfo(deployedBinaryPath).canonicalFilePath()); - } + LogNormal() << "pluginList after having detected hopefully all required plugins:" << pluginList; - // Check for library dependencies - QList dependencies = getQtLibraries(deployedBinaryPath, bundlePath, rpathsUsed); + foreach (const QString &plugin, pluginList) { + sourcePath = pluginSourcePath + "/" + plugin; + destinationPath = pluginDestinationPath + "/" + plugin; + QDir dir; + dir.mkpath(QFileInfo(destinationPath).path()); + QList libraries = getQtLibraries(sourcePath, appDirInfo.path, deploymentInfo.rpathsUsed); + LogDebug() << "Deploying plugin" << sourcePath; - foreach (LibraryInfo dependency, dependencies) { - if (dependency.rpathUsed.isEmpty() != true) { - rpathsUsed << dependency.rpathUsed; - } + if (copyFilePrintStatus(sourcePath, destinationPath)) { + runStrip(destinationPath); + deployQtLibraries(libraries, appDirInfo.path, QStringList() << destinationPath, deploymentInfo.useLoaderPath); + /* See whether this makes any difference */ + // Find out the relative path to the lib/ directory and set it as the rpath + QDir dir(destinationPath); + QString relativePath = dir.relativeFilePath(appDirInfo.path + "/" + libraries[0].libraryDestinationDirectory); + relativePath.remove(0, 3); // remove initial '../' + changeIdentification("$ORIGIN/" + relativePath, QFileInfo(destinationPath).canonicalFilePath()); - // Deploy library if necessary. - if (copiedLibraries.contains(dependency.libraryName) == false && libraries.contains(dependency) == false) { - libraries.append(dependency); - } } } - deploymentInfo.deployedLibraries = copiedLibraries; - - deploymentInfo.rpathsUsed += rpathsUsed; - - return deploymentInfo; } -static QString captureOutput(const QString &command) +// Scan qml files in qmldirs for import statements, deploy used imports from Qml2ImportsPath to ./qml. +bool Deploy::deployQmlImports(const QString &appDirPath, + DeploymentInfo deploymentInfo, + QStringList &qmlDirs) { - QProcess process; - process.start(command, QIODevice::ReadOnly); - process.waitForFinished(); + LogNormal() << ""; + LogNormal() << "Deploying QML imports "; + LogNormal() << "Application QML file search path(s) is" << qmlDirs; - if (process.exitStatus() != QProcess::NormalExit) { - LogError() << command << "crashed:" << process.readAllStandardError(); - } else if (process.exitCode() != 0) { - LogError() << command << "exited with" << process.exitCode() << ":" << process.readAllStandardError(); + // Use qmlimportscanner from QLibraryInfo::BinariesPath + QString qmlImportScannerPath = QDir::cleanPath(this->m_qtToBeBundledInfo.value("QT_INSTALL_BINS")) + "/qmlimportscanner"; + LogDebug() << "Looking for qmlimportscanner at" << qmlImportScannerPath; + + // Fallback: Look relative to the linuxdeployqt binary + if (!QFile(qmlImportScannerPath).exists()){ + qmlImportScannerPath = QCoreApplication::applicationDirPath() + "/qmlimportscanner"; + LogDebug() << "Fallback, looking for qmlimportscanner at" << qmlImportScannerPath; } - return process.readAllStandardOutput(); -} + // Verify that we found a qmlimportscanner binary + if (!QFile(qmlImportScannerPath).exists()) { + LogError() << "qmlimportscanner not found at" << qmlImportScannerPath; + LogError() << "Rebuild qtdeclarative/tools/qmlimportscanner"; -DeploymentInfo deployQtLibraries(const QString &appDirPath, const QStringList &additionalExecutables) -{ - AppDirInfo applicationBundle; + return false; + } - applicationBundle.path = appDirPath; - LogDebug() << "applicationBundle.path:" << applicationBundle.path; - applicationBundle.binaryPath = appBinaryPath; - LogDebug() << "applicationBundle.binaryPath:" << applicationBundle.binaryPath; + // build argument list for qmlimportsanner: "-rootPath foo/ -rootPath bar/ -importPath path/to/qt/qml" + // ("rootPath" points to a directory containing app qml, "importPath" is where the Qt imports are installed) + QStringList argumentList; - // Find out whether Qt is a dependency of the application to be bundled - int qtDetected = 0; - LddInfo lddInfo = findDependencyInfo(appBinaryPath); - foreach (const DylibInfo dep, lddInfo.dependencies) { - LogDebug() << "dep.binaryPath" << dep.binaryPath; - if(dep.binaryPath.contains("libQt5")){ - qtDetected = 5; - } - if(dep.binaryPath.contains("libQtCore.so.4")){ - qtDetected = 4; - } - } + foreach (const QString &qmlDir, qmlDirs) { + argumentList.append("-rootPath"); + argumentList.append(qmlDir); + } - if(qtDetected != 0){ + argumentList.append( "-importPath"); + argumentList.append(this->m_qtToBeBundledInfo.value("QT_INSTALL_QML")); - // Determine the location of the Qt to be bundled - LogDebug() << "Using qmake to determine the location of the Qt to be bundled"; + LogDebug() << "qmlImportsPath (QT_INSTALL_QML):" << this->m_qtToBeBundledInfo.value("QT_INSTALL_QML"); - QString qmakePath = ""; + // run qmlimportscanner + QProcess qmlImportScanner; + LogDebug() << qmlImportScannerPath << argumentList; + qmlImportScanner.start(qmlImportScannerPath, argumentList); - // The upstream name of the binary is "qmake", for Qt 4 and Qt 5 - qmakePath = QStandardPaths::findExecutable("qmake"); + if (!qmlImportScanner.waitForStarted()) { + LogError() << "Could not start qmlimportscanner. Process error is" << qmlImportScanner.errorString(); - // But openSUSE has qmake for Qt 4 and qmake-qt5 for Qt 5 - // Qt 4 on Fedora comes with suffix -qt4 - // http://www.geopsy.org/wiki/index.php/Installing_Qt_binary_packages - if(qmakePath == ""){ - if(qtDetected == 5){ - qmakePath = QStandardPaths::findExecutable("qmake-qt5"); - } - if(qtDetected == 4){ - qmakePath = QStandardPaths::findExecutable("qmake-qt4"); - } - } + return false; + } - if(qmakePath == ""){ - LogError() << "qmake not found on the $PATH"; - exit(1); - } + qmlImportScanner.waitForFinished(); - QString output = captureOutput(qmakePath + " -query"); + // log qmlimportscanner errors + qmlImportScanner.setReadChannel(QProcess::StandardError); + QByteArray errors = qmlImportScanner.readAll(); - QStringList outputLines = output.split("\n", QString::SkipEmptyParts); - foreach (const QString &outputLine, outputLines) { - int colonIndex = outputLine.indexOf(QLatin1Char(':')); - if (colonIndex != -1) { - QString name = outputLine.left(colonIndex); - QString value = outputLine.mid(colonIndex + 1); - qtToBeBundledInfo.insert(name, value); - } - } + if (!errors.isEmpty()) { + LogWarning() << "QML file parse error (deployment will continue):"; + LogWarning() << errors; + } - QString qtLibsPath = qtToBeBundledInfo.value("QT_INSTALL_LIBS"); + // parse qmlimportscanner json + qmlImportScanner.setReadChannel(QProcess::StandardOutput); + QByteArray json = qmlImportScanner.readAll(); + QJsonDocument doc = QJsonDocument::fromJson(json); - if (qtLibsPath.isEmpty() || !QFile::exists(qtLibsPath)) { - LogError() << "Qt path could not be determined from qmake on the $PATH"; - LogError() << "Make sure you have the correct Qt on your $PATH"; - LogError() << "You can check this with qmake -v"; - exit(1); - } else { - LogDebug() << "Qt libs path determined from qmake:" << qtLibsPath; - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - QString oldPath = env.value("LD_LIBRARY_PATH"); - QString newPath = qtLibsPath + ":" + oldPath; // FIXME: If we use a ldd replacement, we still need to observe this path - // FIXME: Directory layout might be different for system Qt; cannot assume lib/ to always be inside the Qt directory - LogDebug() << "Changed LD_LIBRARY_PATH:" << newPath; - setenv("LD_LIBRARY_PATH",newPath.toUtf8().constData(),1); - } - } + if (!doc.isArray()) { + LogError() << "qmlimportscanner output error. Expected json array, got:"; + LogError() << json; - if(fhsLikeMode == false){ - changeIdentification("$ORIGIN/lib/" + bundleLibraryDirectory, QFileInfo(applicationBundle.binaryPath).canonicalFilePath()); - } else { - changeIdentification("$ORIGIN/../lib/" + bundleLibraryDirectory, QFileInfo(applicationBundle.binaryPath).canonicalFilePath()); - } - applicationBundle.libraryPaths = findAppLibraries(appDirPath); - LogDebug() << "applicationBundle.libraryPaths:" << applicationBundle.libraryPaths; + return false; + } - LogDebug() << "additionalExecutables:" << additionalExecutables; + bool qtQuickContolsInUse = false; // condition for QtQuick.PrivateWidgets below + + // deploy each import + foreach (const QJsonValue &importValue, doc.array()) { + if (!importValue.isObject()) + continue; + + QJsonObject import = importValue.toObject(); + QString name = import["name"].toString(); + QString path = import["path"].toString(); + QString type = import["type"].toString(); + + if (import["name"].toString() == "QtQuick.Controls") + qtQuickContolsInUse = true; + + LogNormal() << "Deploying QML import" << name; + LogDebug() << "path:" << path; + LogDebug() << "type:" << type; + + // Skip imports with missing info - path will be empty if the import is not found. + if (name.isEmpty() || path.isEmpty()) { + LogNormal() << " Skip import: name or path is empty"; + LogNormal() << ""; + + continue; + } + + // Deploy module imports only, skip directory (local/remote) and js imports. These + // should be deployed as a part of the application build. + if (type != QStringLiteral("module")) { + LogNormal() << " Skip non-module import"; + LogNormal() << ""; + + continue; + } + + // Create the destination path from the name + // and version (grabbed from the source path) + // ### let qmlimportscanner provide this. + name.replace(QLatin1Char('.'), QLatin1Char('/')); + int secondTolast = path.length() - 2; + QString version = path.mid(secondTolast); + + if (version.startsWith(QLatin1Char('.'))) + name.append(version); + + deployQmlImport(appDirPath, deploymentInfo.rpathsUsed, path, name); + LogNormal() << ""; + } + + // Special case: + // Use of QtQuick/PrivateWidgets is not discoverable at deploy-time. + // Recreate the run-time logic here as best as we can - deploy it iff + // 1) QtWidgets library is used + // 2) QtQuick.Controls is used + // The intended failure mode is that libwidgetsplugin.dylib will be present + // in the app bundle but not used at run-time. + if (deploymentInfo.deployedLibraries.contains("QtWidgets") && qtQuickContolsInUse) { + LogNormal() << "Deploying QML import QtQuick/PrivateWidgets"; + QString name = "QtQuick/PrivateWidgets"; + QString path = this->m_qtToBeBundledInfo.value("QT_INSTALL_QML") + QLatin1Char('/') + name; + deployQmlImport(appDirPath, deploymentInfo.rpathsUsed, path, name); + LogNormal() << ""; + } + + return true; +} + +void Deploy::changeIdentification(const QString &id, const QString &binaryPath) +{ + LogNormal() << "Changing rpath in" << binaryPath << "to" << id; + runPatchelf(QStringList() << "--set-rpath" << id << binaryPath); + + // NOTE: Code below make no sense, Qt can already find paths relative to + // the executable, and pathching this way can break the binary. + + // qt_prfxpath: + if (binaryPath.contains("libQt5Core")) { + LogDebug() << "libQt5Core detected, patching its hardcoded strings"; + + /* https://codereview.qt-project.org/gitweb?p=qt/qttools.git;a=blob_plain;f=src/windeployqt/utils.cpp;h=e89496ea1f371ed86f6937284c1c801daf576572;hb=7be81b804da102b374c2089aac38353a0383c254 + * Search for "qt_prfxpath=" in a path, and replace it with "qt_prfxpath=." or "qt_prfxpath=.." */ + + if (fhsLikeMode) + patchQtCore(binaryPath, "qt_prfxpath", ".."); + else + patchQtCore(binaryPath, "qt_prfxpath", "."); + + patchQtCore(binaryPath, "qt_adatpath", "."); + patchQtCore(binaryPath, "qt_docspath", "doc"); + patchQtCore(binaryPath, "qt_hdrspath", "include"); + patchQtCore(binaryPath, "qt_libspath", "lib"); + patchQtCore(binaryPath, "qt_lbexpath", "libexec"); + patchQtCore(binaryPath, "qt_binspath", "bin"); + patchQtCore(binaryPath, "qt_plugpath", "plugins"); + patchQtCore(binaryPath, "qt_impspath", "imports"); + patchQtCore(binaryPath, "qt_qml2path", "qml"); + patchQtCore(binaryPath, "qt_datapath", "."); + patchQtCore(binaryPath, "qt_trnspath", "translations"); + patchQtCore(binaryPath, "qt_xmplpath", "examples"); + patchQtCore(binaryPath, "qt_demopath", "demos"); + patchQtCore(binaryPath, "qt_tstspath", "tests"); + patchQtCore(binaryPath, "qt_hpfxpath", "."); + patchQtCore(binaryPath, "qt_hbinpath", "bin"); + patchQtCore(binaryPath, "qt_hdatpath", "."); + patchQtCore(binaryPath, "qt_stngpath", "."); // e.g., /opt/qt53/etc/xdg; does it load Trolltech.conf from there? + + /* Qt on Arch Linux comes with more hardcoded paths + * https://github.com/probonopd/linuxdeployqt/issues/98 + patchString(binaryPath, "lib/qt/libexec", "libexec"); + patchString(binaryPath, "lib/qt/plugins", "plugins"); + patchString(binaryPath, "lib/qt/imports", "imports"); + patchString(binaryPath, "lib/qt/qml", "qml"); + patchString(binaryPath, "lib/qt", ""); + patchString(binaryPath, "share/doc/qt", "doc"); + patchString(binaryPath, "include/qt", "include"); + patchString(binaryPath, "share/qt", ""); + patchString(binaryPath, "share/qt/translations", "translations"); + patchString(binaryPath, "share/doc/qt/examples", "examples"); + */ + } +} + +void Deploy::runStrip(const QString &binaryPath) +{ + if (runStripEnabled == false) + return; + + // Since we might have a symlink, we need to find its target first + QString resolvedPath = QFileInfo(binaryPath).canonicalFilePath(); + + LogDebug() << "Determining whether to run strip:"; + LogDebug() << " checking whether" << resolvedPath << "has an rpath set"; + LogDebug() << "patchelf" << "--print-rpath" << resolvedPath; + QProcess patchelfread; + patchelfread.start("patchelf", QStringList() << "--print-rpath" << resolvedPath); + + if (!patchelfread.waitForStarted()) { + if (patchelfread.errorString().contains("execvp: No such file or directory")){ + LogError() << "Could not start patchelf."; + LogError() << "Make sure it is installed on your $PATH."; + } else { + LogError() << "Could not start patchelf. Process error is" << patchelfread.errorString(); + } + // exit(1); // Do not exit because this could be a script that patchelf can't work on + } + + patchelfread.waitForFinished(); + + if (patchelfread.exitCode() != 0) { + LogError() << "Error reading rpath with patchelf" << QFileInfo(resolvedPath).completeBaseName() << ":" << patchelfread.readAllStandardError(); + LogError() << "Error reading rpath with patchelf" << QFileInfo(resolvedPath).completeBaseName() << ":" << patchelfread.readAllStandardOutput(); + exit(1); + } + + QString rpath = patchelfread.readAllStandardOutput(); + + if (rpath.startsWith("$")) { + LogDebug() << "Already contains rpath starting with $, hence not stripping"; + LogDebug() << patchelfread.readAllStandardOutput(); + + return; + } + + LogDebug() << "Using strip:"; + LogDebug() << " stripping" << resolvedPath; + QProcess strip; + strip.start("strip", QStringList() << resolvedPath); + + if (!strip.waitForStarted()) { + if (strip.errorString().contains("execvp: No such file or directory")) { + LogError() << "Could not start strip."; + LogError() << "Make sure it is installed on your $PATH."; + } else { + LogError() << "Could not start strip. Process error is" << strip.errorString(); + } + + exit(1); + } + + strip.waitForFinished(); + + if (strip.exitCode() == 0) + return; + + if (strip.readAllStandardError().contains("Not enough room for program headers")) { + LogNormal() << QFileInfo(resolvedPath).completeBaseName() << "already stripped."; + } else { + LogError() << "Error stripping" << QFileInfo(resolvedPath).completeBaseName() << ":" << strip.readAllStandardError(); + LogError() << "Error stripping" << QFileInfo(resolvedPath).completeBaseName() << ":" << strip.readAllStandardOutput(); + exit(1); + } +} + +void Deploy::stripAppBinary(const QString &bundlePath) +{ + Q_UNUSED(bundlePath); + + runStrip(appBinaryPath); +} + +QStringList Deploy::findAppLibraries(const QString &appDirPath) +{ + QStringList result; + // .so + QDirIterator iter(appDirPath, QStringList() << QString::fromLatin1("*.so"), + QDir::Files, QDirIterator::Subdirectories); + + while (iter.hasNext()) { + iter.next(); + result << iter.fileInfo().filePath(); + } + + // .so.*, FIXME: Is the above really needed or is it covered by the below too? + QDirIterator iter2(appDirPath, QStringList() << QString::fromLatin1("*.so*"), + QDir::Files, QDirIterator::Subdirectories); + + while (iter2.hasNext()) { + iter2.next(); + result << iter2.fileInfo().filePath(); + } + + return result; +} + +bool Deploy::patchQtCore(const QString &path, + const QString &variable, + const QString &value) +{ + QFile file(path); + + if (!file.open(QIODevice::ReadWrite)) { + LogWarning() << QString::fromLatin1("Unable to patch %1: %2").arg( + QDir::toNativeSeparators(path), file.errorString()); + return false; + } + + QByteArray content = file.readAll(); + + if (content.isEmpty()) { + LogWarning() << QString::fromLatin1("Unable to patch %1: Could not read file content").arg( + QDir::toNativeSeparators(path)); + + return false; + } + + QString searchString = QString::fromLatin1("%1=").arg(variable); + QByteArray searchStringQByteArray = searchString.toLatin1().data(); + + int startPos = content.indexOf(searchStringQByteArray); + + if (startPos != -1) { + LogNormal() << QString::fromLatin1( + "Patching value of %2 in %1 to '%3'").arg(QDir::toNativeSeparators(path), variable, value); + } + + startPos += searchStringQByteArray.length(); + int endPos = content.indexOf(char(0), startPos); + + if (endPos == -1) { + LogWarning() << QString::fromLatin1("Unable to patch %1: Internal error").arg( + QDir::toNativeSeparators(path)); + + return false; + } + + QByteArray replacement = QByteArray(endPos - startPos, char(0)); + QByteArray replacementBegin = value.toLatin1().data(); + replacement.prepend(replacementBegin); + replacement.truncate(endPos - startPos); + + content.replace(startPos, endPos - startPos, replacement); + + if (!file.seek(0) || file.write(content) != content.size()) { + LogWarning() << QString::fromLatin1("Unable to patch %1: Could not write to file").arg( + QDir::toNativeSeparators(path)); + + return false; + } + + return true; +} + +int Deploy::createAppImage(const QString &appDirPath) +{ + QString appImageCommand = "appimagetool '" + appDirPath + "' --verbose -n"; // +"' '" + appImagePath + "'"; + int ret = system(appImageCommand.toUtf8().constData()); + LogNormal() << "ret" << ret; + LogNormal() << "WEXITSTATUS(ret)" << WEXITSTATUS(ret); + + return WEXITSTATUS(ret); +} + +bool Deploy::checkAppImagePrerequisites(const QString &appDirPath) +{ + if (fhsLikeMode) { + /* In FHS-like mode, we assume that there will be a desktop file + * and icon file that appimagetool will be able to pick up */ + return true; + } + + QDirIterator iter(appDirPath, QStringList() << QString::fromLatin1("*.desktop"), + QDir::Files, QDirIterator::Subdirectories); + + if (!iter.hasNext()) { + LogError() << "Desktop file missing, creating a default one (you will probably want to edit it)"; + QFile file(appDirPath + "/default.desktop"); + file.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream out(&file); + out << "[Desktop Entry]\n"; + out << "Type=Application\n"; + out << "Name=Application\n"; + out << "Exec=AppRun %F\n"; + out << "Icon=default\n"; + out << "Comment=Edit this default file\n"; + out << "Terminal=true\n"; + file.close(); + } + + // TODO: Compare whether the icon filename matches the Icon= entry without ending in the *.desktop file above + QDirIterator iter2(appDirPath, QStringList() << QString::fromLatin1("*.png"), + QDir::Files, QDirIterator::Subdirectories); + + if (!iter2.hasNext()) { + LogError() << "Icon file missing, creating a default one (you will probably want to edit it)"; + QFile file2(appDirPath + "/default.png"); + file2.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream out2(&file2); + out2 << ""; + QTextStream out(&file2); + file2.close(); + } + + return true; +} + +QDebug Deploy::LogError() +{ + if (logLevel < 0) + return QDebug(&this->m_log); - QStringList allBinaryPaths = QStringList() << applicationBundle.binaryPath << applicationBundle.libraryPaths - << additionalExecutables; - LogDebug() << "allBinaryPaths:" << allBinaryPaths; + return qDebug() << "ERROR:"; +} - QSet allRPaths = getBinaryRPaths(applicationBundle.binaryPath, true); - allRPaths.insert(QLibraryInfo::location(QLibraryInfo::LibrariesPath)); - LogDebug() << "allRPaths:" << allRPaths; +QDebug Deploy::LogWarning() +{ + if (logLevel < 1) + return QDebug(&this->m_log); - QList libraries = getQtLibrariesForPaths(allBinaryPaths, appDirPath, allRPaths); - if (libraries.isEmpty() && !alwaysOwerwriteEnabled) { - LogWarning() << "Could not find any external Qt libraries to deploy in" << appDirPath; - LogWarning() << "Perhaps linuxdeployqt was already used on" << appDirPath << "?"; - LogWarning() << "If so, you will need to rebuild" << appDirPath << "before trying again."; - LogWarning() << "Or ldd does not find the external Qt libraries but sees the system ones."; - LogWarning() << "If so, you will need to set LD_LIBRARY_PATH to the directory containing the external Qt libraries before trying again."; - LogWarning() << "FIXME: https://github.com/probonopd/linuxdeployqt/issues/2"; - return DeploymentInfo(); - } else { - return deployQtLibraries(libraries, applicationBundle.path, allBinaryPaths, !additionalExecutables.isEmpty()); - } + return qDebug() << "WARNING:"; } -void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath, - const QString pluginDestinationPath, DeploymentInfo deploymentInfo) +QDebug Deploy::LogNormal() { - LogNormal() << "Deploying plugins from" << pluginSourcePath; + if (logLevel < 2) + return QDebug(&this->m_log); - if (!pluginSourcePath.contains(deploymentInfo.pluginPath)) - return; + return qDebug() << "Log:"; +} - // Plugin white list: - QStringList pluginList; +QDebug Deploy::LogDebug() +{ + if (logLevel < 3) + return QDebug(&this->m_log); - LogDebug() << "deploymentInfo.deployedLibraries before attempting to bundle required plugins:" << deploymentInfo.deployedLibraries; + return qDebug() << "Log:"; +} - // Platform plugin: - if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Gui")) { - LogDebug() << "libQt5Gui detected"; - pluginList.append("platforms/libqxcb.so"); - // All image formats (svg if QtSvg library is used) - QStringList imagePlugins = QDir(pluginSourcePath + QStringLiteral("/imageformats")).entryList(QStringList() << QStringLiteral("*.so")); - foreach (const QString &plugin, imagePlugins) { - if (plugin.contains(QStringLiteral("qsvg"))) { - if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Svg")) { - pluginList.append(QStringLiteral("imageformats/") + plugin); - } - } else { - pluginList.append(QStringLiteral("imageformats/") + plugin); - } - } +// Determine whether the given 'ldd' output contains a Linux VDSO +// shared object. The name of the VDSO object differs depending +// on architecture. See "vDSO names" in the notes section of vdso(7) +// for more information. +bool Deploy::lddOutputContainsLinuxVDSO(const QString &lddOutput) +{ + // aarch64, arm, mips, x86_64, x86/x32 + if (lddOutput.contains(QStringLiteral("linux-vdso.so.1"))) { + return true; + // ppc32, s390 + } else if (lddOutput.contains(QStringLiteral("linux-vdso32.so.1"))) { + return true; + // ppc64, s390x + } else if (lddOutput.contains(QStringLiteral("linux-vdso64.so.1"))) { + return true; + // ia64, sh, i386 + } else if (lddOutput.contains(QStringLiteral("linux-gate.so.1"))) { + return true; } - // Platform OpenGL context - if ((containsHowOften(deploymentInfo.deployedLibraries, "libQt5OpenGL")) or (containsHowOften(deploymentInfo.deployedLibraries, "libQt5XcbQpa"))) { - QStringList xcbglintegrationPlugins = QDir(pluginSourcePath + QStringLiteral("/xcbglintegrations")).entryList(QStringList() << QStringLiteral("*.so")); - foreach (const QString &plugin, xcbglintegrationPlugins) { - pluginList.append(QStringLiteral("xcbglintegrations/") + plugin); + return false; +} + +bool Deploy::copyFilePrintStatus(const QString &from, const QString &to) +{ + if (QFile(to).exists()) { + if (alwaysOwerwriteEnabled) { + QFile(to).remove(); + } else { + LogDebug() << QFileInfo(to).fileName() << "already deployed, skipping."; + + return false; } } - // Also deploy plugins/iconengines/libqsvgicon.so whenever libQt5Svg.so.* is about to be deployed, - // https://github.com/probonopd/linuxdeployqt/issues/36 - if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Svg")) { - pluginList.append(QStringLiteral("iconengines/libqsvgicon.so")); - } + if (QFile::copy(from, to)) { + QFile dest(to); + dest.setPermissions(dest.permissions() | QFile::WriteOwner | QFile::WriteUser); + LogNormal() << " copied:" << from; + LogNormal() << " to" << to; - // CUPS print support - if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5PrintSupport")) { - pluginList.append("printsupport/libcupsprintersupport.so"); - } + // The source file might not have write permissions set. Set the + // write permission on the target file to make sure we can use + // install_name_tool on it later. + QFile toFile(to); - // Network - if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Network")) { - QStringList bearerPlugins = QDir(pluginSourcePath + QStringLiteral("/bearer")).entryList(QStringList() << QStringLiteral("*.so")); - foreach (const QString &plugin, bearerPlugins) { - pluginList.append(QStringLiteral("bearer/") + plugin); - } - } + if (toFile.permissions() & QFile::WriteOwner) + return true; - // Sql plugins if QtSql library is in use - if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Sql")) { - QStringList sqlPlugins = QDir(pluginSourcePath + QStringLiteral("/sqldrivers")).entryList(QStringList() << QStringLiteral("*.so")); - foreach (const QString &plugin, sqlPlugins) { - pluginList.append(QStringLiteral("sqldrivers/") + plugin); - } - } + if (!toFile.setPermissions(toFile.permissions() | QFile::WriteOwner)) { + LogError() << "Failed to set u+w permissions on target file: " << to; - // multimedia plugins if QtMultimedia library is in use - if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Multimedia")) { - QStringList plugins = QDir(pluginSourcePath + QStringLiteral("/mediaservice")).entryList(QStringList() << QStringLiteral("*.so")); - foreach (const QString &plugin, plugins) { - pluginList.append(QStringLiteral("mediaservice/") + plugin); - } - plugins = QDir(pluginSourcePath + QStringLiteral("/audio")).entryList(QStringList() << QStringLiteral("*.so")); - foreach (const QString &plugin, plugins) { - pluginList.append(QStringLiteral("audio/") + plugin); + return false; } + + return true; + } else { + LogError() << "file copy failed from" << from; + LogError() << " to" << to; + + return false; } +} - QString sourcePath; - QString destinationPath; +int Deploy::containsHowOften(QStringList haystack, QString needle) +{ + return haystack.filter(needle).length(); +} - // Qt WebEngine if libQt5WebEngineCore is in use - // https://doc-snapshots.qt.io/qt5-5.7/qtwebengine-deploying.html - // TODO: Rather than hardcode the source paths, somehow get them dynamically - // from the Qt instance that is to be bundled (pull requests welcome!) - // especially since stuff that is supposed to come from resources actually - // seems to come in libexec in the upstream Qt binary distribution - if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5WebEngineCore")) { - // Find directories with needed files: - QString qtLibexecPath = qtToBeBundledInfo.value("QT_INSTALL_LIBEXECS"); - QString qtDataPath = qtToBeBundledInfo.value("QT_INSTALL_DATA"); - QString qtTranslationsPath = qtToBeBundledInfo.value("QT_INSTALL_TRANSLATIONS"); - // create destination directories: - QString dstLibexec; - QString dstResources; - QString dstTranslations; - if(fhsLikeMode){ - QFileInfo qfi(appDirInfo.binaryPath); - QString qtTargetDir = qfi.absoluteDir().absolutePath() + "/../"; - dstLibexec = qtTargetDir + "/libexec"; - dstResources = qtTargetDir + "/resources"; - dstTranslations = qtTargetDir + "/translations"; +// TODO: Switch the following to using patchelf +QSet Deploy::getBinaryRPaths(const QString &path, + bool resolve, + QString executablePath) +{ + QSet rpaths; + + QProcess objdump; + objdump.start("objdump", QStringList() << "-x" << path); + + if (!objdump.waitForStarted()) { + if (objdump.errorString().contains("execvp: No such file or directory")) { + LogError() << "Could not start objdump."; + LogError() << "Make sure it is installed on your $PATH."; } else { - dstLibexec = appDirInfo.path + "/libexec"; - dstResources = appDirInfo.path + "/resources"; - dstTranslations = appDirInfo.path + "/translations"; + LogError() << "Could not start objdump. Process error is" << objdump.errorString(); } - QDir().mkpath(dstLibexec); - QDir().mkpath(dstResources); - QDir().mkpath(dstTranslations); - // WebEngine executable: - sourcePath = QDir::cleanPath(qtLibexecPath + "/QtWebEngineProcess"); - destinationPath = QDir::cleanPath(dstLibexec + "/QtWebEngineProcess"); - copyFilePrintStatus(sourcePath, destinationPath); - // Resources: - sourcePath = QDir::cleanPath(qtDataPath + "/resources/qtwebengine_resources.pak"); - destinationPath = QDir::cleanPath(dstResources + "/qtwebengine_resources.pak"); - copyFilePrintStatus(sourcePath, destinationPath); - sourcePath = QDir::cleanPath(qtDataPath + "/resources/qtwebengine_devtools_resources.pak"); - destinationPath = QDir::cleanPath(dstResources + "/qtwebengine_devtools_resources.pak"); - copyFilePrintStatus(sourcePath, destinationPath); - sourcePath = QDir::cleanPath(qtDataPath + "/resources/qtwebengine_resources_100p.pak"); - destinationPath = QDir::cleanPath(dstResources + "/qtwebengine_resources_100p.pak"); - copyFilePrintStatus(sourcePath, destinationPath); - sourcePath = QDir::cleanPath(qtDataPath + "/resources/qtwebengine_resources_200p.pak"); - destinationPath = QDir::cleanPath(dstResources + "/qtwebengine_resources_200p.pak"); - copyFilePrintStatus(sourcePath, destinationPath); - sourcePath = QDir::cleanPath(qtDataPath + "/resources/icudtl.dat"); - destinationPath = QDir::cleanPath(dstResources + "/icudtl.dat"); - copyFilePrintStatus(sourcePath, destinationPath); - // Translations: - sourcePath = QDir::cleanPath(qtTranslationsPath + "/qtwebengine_locales"); - destinationPath = QDir::cleanPath(dstTranslations + "/qtwebengine_locales"); - recursiveCopy(sourcePath, destinationPath); + + exit(1); } - LogNormal() << "pluginList after having detected hopefully all required plugins:" << pluginList; + objdump.waitForFinished(); - foreach (const QString &plugin, pluginList) { - sourcePath = pluginSourcePath + "/" + plugin; - destinationPath = pluginDestinationPath + "/" + plugin; - QDir dir; - dir.mkpath(QFileInfo(destinationPath).path()); - QList libraries = getQtLibraries(sourcePath, appDirInfo.path, deploymentInfo.rpathsUsed); - LogDebug() << "Deploying plugin" << sourcePath; - if (copyFilePrintStatus(sourcePath, destinationPath)) { - runStrip(destinationPath); - deployQtLibraries(libraries, appDirInfo.path, QStringList() << destinationPath, deploymentInfo.useLoaderPath); - /* See whether this makes any difference */ - // Find out the relative path to the lib/ directory and set it as the rpath - QDir dir(destinationPath); - QString relativePath = dir.relativeFilePath(appDirInfo.path + "/" + libraries[0].libraryDestinationDirectory); - relativePath.remove(0, 3); // remove initial '../' - changeIdentification("$ORIGIN/" + relativePath, QFileInfo(destinationPath).canonicalFilePath()); + if (objdump.exitCode() != 0) + LogError() << "getBinaryRPaths:" << objdump.readAllStandardError(); - } - } -} + if (resolve && executablePath.isEmpty()) + executablePath = path; -void deployPlugins(const QString &appDirPath, DeploymentInfo deploymentInfo) -{ - AppDirInfo applicationBundle; - applicationBundle.path = appDirPath; - applicationBundle.binaryPath = appBinaryPath; + QString output = objdump.readAllStandardOutput(); + QStringList outputLines = output.split("\n"); + QStringListIterator i(outputLines); - QString pluginDestinationPath; - if(fhsLikeMode){ - QFileInfo qfi(applicationBundle.binaryPath); - QString qtTargetDir = qfi.absoluteDir().absolutePath() + "/../"; - pluginDestinationPath = qtTargetDir + "/plugins"; - } else { - pluginDestinationPath = appDirPath + "/" + "plugins"; + while (i.hasNext()) { + if (i.next().contains("RUNPATH") && i.hasNext()) { + i.previous(); + const QString &rpathCmd = i.next(); + int pathStart = rpathCmd.indexOf("RUNPATH"); + + if (pathStart >= 0) { + QString rpath = rpathCmd.mid(pathStart+8).trimmed(); + LogDebug() << "rpath:" << rpath; + rpaths << rpath; + } + } } - deployPlugins(applicationBundle, deploymentInfo.pluginPath, pluginDestinationPath, deploymentInfo); + + return rpaths; } -void deployQmlImport(const QString &appDirPath, const QSet &rpaths, const QString &importSourcePath, const QString &importName) +QList Deploy::getQtLibrariesForPaths(const QStringList &paths, + const QString &appDirPath, + const QSet &rpaths) { - AppDirInfo applicationBundle; - applicationBundle.path = appDirPath; - applicationBundle.binaryPath = appBinaryPath; - - QString importDestinationPath; - if(fhsLikeMode){ - QFileInfo qfi(applicationBundle.binaryPath); - QString qtTargetDir = qfi.absoluteDir().absolutePath() + "/../"; - importDestinationPath = qtTargetDir + "/qml/" + importName; - } else { - importDestinationPath = appDirPath + "/qml/" + importName; - } + QList result; + QSet existing; - // Skip already deployed imports. This can happen in cases like "QtQuick.Controls.Styles", - // where deploying QtQuick.Controls will also deploy the "Styles" sub-import. - if (QDir().exists(importDestinationPath)) - return; + foreach (const QString &path, paths) + foreach (const LibraryInfo &info, getQtLibraries(path, appDirPath, rpaths)) + if (!existing.contains(info.libraryPath)) { // avoid duplicates + existing.insert(info.libraryPath); + result << info; + } - recursiveCopyAndDeploy(appDirPath, rpaths, importSourcePath, importDestinationPath); + return result; } -// Scan qml files in qmldirs for import statements, deploy used imports from Qml2ImportsPath to ./qml. -bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, QStringList &qmlDirs) +// copies everything _inside_ sourcePath to destinationPath +bool Deploy::recursiveCopy(const QString &sourcePath, + const QString &destinationPath) { - LogNormal() << ""; - LogNormal() << "Deploying QML imports "; - LogNormal() << "Application QML file search path(s) is" << qmlDirs; + if (!QDir(sourcePath).exists()) + return false; - // Use qmlimportscanner from QLibraryInfo::BinariesPath - QString qmlImportScannerPath = QDir::cleanPath(qtToBeBundledInfo.value("QT_INSTALL_BINS")) + "/qmlimportscanner"; - LogDebug() << "Looking for qmlimportscanner at" << qmlImportScannerPath; + QDir().mkpath(destinationPath); - // Fallback: Look relative to the linuxdeployqt binary - if (!QFile(qmlImportScannerPath).exists()){ - qmlImportScannerPath = QCoreApplication::applicationDirPath() + "/qmlimportscanner"; - LogDebug() << "Fallback, looking for qmlimportscanner at" << qmlImportScannerPath; - } + LogNormal() << "copy:" << sourcePath << destinationPath; - // Verify that we found a qmlimportscanner binary - if (!QFile(qmlImportScannerPath).exists()) { - LogError() << "qmlimportscanner not found at" << qmlImportScannerPath; - LogError() << "Rebuild qtdeclarative/tools/qmlimportscanner"; - return false; - } + QStringList files = QDir(sourcePath).entryList(QStringList() << "*", QDir::Files | QDir::NoDotAndDotDot); - // build argument list for qmlimportsanner: "-rootPath foo/ -rootPath bar/ -importPath path/to/qt/qml" - // ("rootPath" points to a directory containing app qml, "importPath" is where the Qt imports are installed) - QStringList argumentList; - foreach (const QString &qmlDir, qmlDirs) { - argumentList.append("-rootPath"); - argumentList.append(qmlDir); + foreach (QString file, files) { + const QString fileSourcePath = sourcePath + "/" + file; + const QString fileDestinationPath = destinationPath + "/" + file; + copyFilePrintStatus(fileSourcePath, fileDestinationPath); } - argumentList.append( "-importPath"); - argumentList.append(qtToBeBundledInfo.value("QT_INSTALL_QML")); + QStringList subdirs = QDir(sourcePath).entryList(QStringList() << "*", QDir::Dirs | QDir::NoDotAndDotDot); - LogDebug() << "qmlImportsPath (QT_INSTALL_QML):" << qtToBeBundledInfo.value("QT_INSTALL_QML"); + foreach (QString dir, subdirs) + recursiveCopy(sourcePath + "/" + dir, destinationPath + "/" + dir); - // run qmlimportscanner - QProcess qmlImportScanner; - LogDebug() << qmlImportScannerPath << argumentList; - qmlImportScanner.start(qmlImportScannerPath, argumentList); - if (!qmlImportScanner.waitForStarted()) { - LogError() << "Could not start qmlimportscanner. Process error is" << qmlImportScanner.errorString(); - return false; - } - qmlImportScanner.waitForFinished(); + return true; +} - // log qmlimportscanner errors - qmlImportScanner.setReadChannel(QProcess::StandardError); - QByteArray errors = qmlImportScanner.readAll(); - if (!errors.isEmpty()) { - LogWarning() << "QML file parse error (deployment will continue):"; - LogWarning() << errors; - } +void Deploy::recursiveCopyAndDeploy(const QString &appDirPath, + const QSet &rpaths, + const QString &sourcePath, + const QString &destinationPath) +{ + QDir().mkpath(destinationPath); - // parse qmlimportscanner json - qmlImportScanner.setReadChannel(QProcess::StandardOutput); - QByteArray json = qmlImportScanner.readAll(); - QJsonDocument doc = QJsonDocument::fromJson(json); - if (!doc.isArray()) { - LogError() << "qmlimportscanner output error. Expected json array, got:"; - LogError() << json; - return false; - } + LogNormal() << "copy:" << sourcePath << destinationPath; - bool qtQuickContolsInUse = false; // condition for QtQuick.PrivateWidgets below + QStringList files = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Files | QDir::NoDotAndDotDot); - // deploy each import - foreach (const QJsonValue &importValue, doc.array()) { - if (!importValue.isObject()) - continue; + foreach (QString file, files) { + const QString fileSourcePath = sourcePath + QLatin1Char('/') + file; - QJsonObject import = importValue.toObject(); - QString name = import["name"].toString(); - QString path = import["path"].toString(); - QString type = import["type"].toString(); + QString fileDestinationPath = destinationPath + QLatin1Char('/') + file; + copyFilePrintStatus(fileSourcePath, fileDestinationPath); - if (import["name"].toString() == "QtQuick.Controls") - qtQuickContolsInUse = true; + if (fileDestinationPath.endsWith(".so")) { + LogDebug() << "Deploying .so in QML import" << fileSourcePath; + runStrip(fileDestinationPath); - LogNormal() << "Deploying QML import" << name; - LogDebug() << "path:" << path; - LogDebug() << "type:" << type; + // Find out the relative path to the lib/ directory and set it as the rpath + // FIXME: remove code duplication - the next few lines exist elsewhere already + if (fhsLikeMode) { + QString relativePrefix = fhsPrefix.replace(appDirPath+"/", ""); + this->m_bundleLibraryDirectory = relativePrefix + "/lib/"; + } else { + this->m_bundleLibraryDirectory= "lib"; // relative to bundle + } - // Skip imports with missing info - path will be empty if the import is not found. - if (name.isEmpty() || path.isEmpty()) { - LogNormal() << " Skip import: name or path is empty"; - LogNormal() << ""; - continue; - } + QDir dir(QFileInfo(fileDestinationPath).canonicalFilePath()); + QString relativePath = dir.relativeFilePath(appDirPath + "/" + this->m_bundleLibraryDirectory); + relativePath.remove(0, 3); // remove initial '../' + changeIdentification("$ORIGIN:$ORIGIN/" + relativePath, QFileInfo(fileDestinationPath).canonicalFilePath()); - // Deploy module imports only, skip directory (local/remote) and js imports. These - // should be deployed as a part of the application build. - if (type != QStringLiteral("module")) { - LogNormal() << " Skip non-module import"; - LogNormal() << ""; - continue; + QList libraries = getQtLibraries(fileSourcePath, appDirPath, QSet()); + deployQtLibraries(libraries, appDirPath, QStringList() << destinationPath, false); } + } - // Create the destination path from the name - // and version (grabbed from the source path) - // ### let qmlimportscanner provide this. - name.replace(QLatin1Char('.'), QLatin1Char('/')); - int secondTolast = path.length() - 2; - QString version = path.mid(secondTolast); - if (version.startsWith(QLatin1Char('.'))) - name.append(version); + QStringList subdirs = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Dirs | QDir::NoDotAndDotDot); - deployQmlImport(appDirPath, deploymentInfo.rpathsUsed, path, name); - LogNormal() << ""; + foreach (QString dir, subdirs) + recursiveCopyAndDeploy(appDirPath, rpaths, sourcePath + QLatin1Char('/') + dir, destinationPath + QLatin1Char('/') + dir); +} + +QString Deploy::copyDylib(const LibraryInfo &library, const QString path) +{ + if (!QFile::exists(library.sourceFilePath)) { + LogError() << "no file at" << library.sourceFilePath; + + return QString(); } - // Special case: - // Use of QtQuick/PrivateWidgets is not discoverable at deploy-time. - // Recreate the run-time logic here as best as we can - deploy it iff - // 1) QtWidgets library is used - // 2) QtQuick.Controls is used - // The intended failure mode is that libwidgetsplugin.dylib will be present - // in the app bundle but not used at run-time. - if (deploymentInfo.deployedLibraries.contains("QtWidgets") && qtQuickContolsInUse) { - LogNormal() << "Deploying QML import QtQuick/PrivateWidgets"; - QString name = "QtQuick/PrivateWidgets"; - QString path = qtToBeBundledInfo.value("QT_INSTALL_QML") + QLatin1Char('/') + name; - deployQmlImport(appDirPath, deploymentInfo.rpathsUsed, path, name); - LogNormal() << ""; + // Construct destination paths. The full path typically looks like + // MyApp.app/Contents/Libraries/libfoo.dylib + QString dylibDestinationDirectory = path + QLatin1Char('/') + library.libraryDestinationDirectory; + QString dylibDestinationBinaryPath = dylibDestinationDirectory + QLatin1Char('/') + library.binaryName; + + // Create destination directory + if (!QDir().mkpath(dylibDestinationDirectory)) { + LogError() << "could not create destination directory" << dylibDestinationDirectory; + + return QString(); } - return true; + + // Retrun if the dylib has aleardy been deployed + if (QFileInfo(dylibDestinationBinaryPath).exists() && !alwaysOwerwriteEnabled) + return dylibDestinationBinaryPath; + + // Copy dylib binary + copyFilePrintStatus(library.sourceFilePath, dylibDestinationBinaryPath); + + return dylibDestinationBinaryPath; } -void changeQtLibraries(const QList libraries, const QStringList &binaryPaths, const QString &absoluteQtPath) +void Deploy::runPatchelf(QStringList options) { - LogNormal() << "Changing" << binaryPaths << "to link against"; - LogNormal() << "Qt in" << absoluteQtPath; - QString finalQtPath = absoluteQtPath; + QProcess patchelftool; + patchelftool.start("patchelf", options); - finalQtPath += "/lib/"; + if (!patchelftool.waitForStarted()) { + if (patchelftool.errorString().contains("execvp: No such file or directory")) { + LogError() << "Could not start patchelf."; + LogError() << "Make sure it is installed on your $PATH, e.g., in /usr/local/bin."; + LogError() << "You can get it from https://nixos.org/patchelf.html."; + } else { + LogError() << "Could not start patchelftool. Process error is" << patchelftool.errorString(); + } - foreach (LibraryInfo library, libraries) { - const QString oldBinaryId = library.installName; - const QString newBinaryId = finalQtPath + library.libraryName + library.binaryPath; + exit(1); } -} -void changeQtLibraries(const QString appPath, const QString &qtPath) -{ - const QStringList libraryPaths = findAppLibraries(appPath); - const QList libraries = getQtLibrariesForPaths(QStringList() << appBinaryPath << libraryPaths, appPath, getBinaryRPaths(appBinaryPath, true)); - if (libraries.isEmpty()) { + patchelftool.waitForFinished(); - LogWarning() << "Could not find any _external_ Qt libraries to change in" << appPath; - return; - } else { - const QString absoluteQtPath = QDir(qtPath).absolutePath(); - changeQtLibraries(libraries, QStringList() << appBinaryPath << libraryPaths, absoluteQtPath); + if (patchelftool.exitCode() != 0) { + LogError() << "runPatchelf:" << patchelftool.readAllStandardError(); + LogError() << "runPatchelf:" << patchelftool.readAllStandardOutput(); + // exit(1); // Do not exit because this could be a script that patchelf can't work on } } -bool checkAppImagePrerequisites(const QString &appDirPath) +QString Deploy::captureOutput(const QString &command) { - if(fhsLikeMode == true){ - /* In FHS-like mode, we assume that there will be a desktop file - * and icon file that appimagetool will be able to pick up */ - return true; - } + QProcess process; + process.start(command, QIODevice::ReadOnly); + process.waitForFinished(); - QDirIterator iter(appDirPath, QStringList() << QString::fromLatin1("*.desktop"), - QDir::Files, QDirIterator::Subdirectories); - if (!iter.hasNext()) { - LogError() << "Desktop file missing, creating a default one (you will probably want to edit it)"; - QFile file(appDirPath + "/default.desktop"); - file.open(QIODevice::WriteOnly | QIODevice::Text); - QTextStream out(&file); - out << "[Desktop Entry]\n"; - out << "Type=Application\n"; - out << "Name=Application\n"; - out << "Exec=AppRun %F\n"; - out << "Icon=default\n"; - out << "Comment=Edit this default file\n"; - out << "Terminal=true\n"; - file.close(); - } + if (process.exitStatus() != QProcess::NormalExit) + LogError() << command << "crashed:" << process.readAllStandardError(); + else if (process.exitCode() != 0) + LogError() << command << "exited with" << process.exitCode() << ":" << process.readAllStandardError(); - // TODO: Compare whether the icon filename matches the Icon= entry without ending in the *.desktop file above - QDirIterator iter2(appDirPath, QStringList() << QString::fromLatin1("*.png"), - QDir::Files, QDirIterator::Subdirectories); - if (!iter2.hasNext()) { - LogError() << "Icon file missing, creating a default one (you will probably want to edit it)"; - QFile file2(appDirPath + "/default.png"); - file2.open(QIODevice::WriteOnly | QIODevice::Text); - QTextStream out2(&file2); - out2 << ""; - QTextStream out(&file2); - file2.close(); - } - return true; + return process.readAllStandardOutput(); } -int createAppImage(const QString &appDirPath) +void Deploy::deployQmlImport(const QString &appDirPath, + const QSet &rpaths, + const QString &importSourcePath, + const QString &importName) { - QString appImageCommand = "appimagetool '" + appDirPath + "' --verbose -n"; // +"' '" + appImagePath + "'"; - int ret = system(appImageCommand.toUtf8().constData()); - LogNormal() << "ret" << ret; - LogNormal() << "WEXITSTATUS(ret)" << WEXITSTATUS(ret); - return WEXITSTATUS(ret); + AppDirInfo applicationBundle; + applicationBundle.path = appDirPath; + applicationBundle.binaryPath = appBinaryPath; + + QString importDestinationPath; + + if(fhsLikeMode){ + QFileInfo qfi(applicationBundle.binaryPath); + QString qtTargetDir = qfi.absoluteDir().absolutePath() + "/../"; + importDestinationPath = qtTargetDir + "/qml/" + importName; + } else { + importDestinationPath = appDirPath + "/qml/" + importName; + } + + // Skip already deployed imports. This can happen in cases like "QtQuick.Controls.Styles", + // where deploying QtQuick.Controls will also deploy the "Styles" sub-import. + if (QDir().exists(importDestinationPath)) + return; + + recursiveCopyAndDeploy(appDirPath, rpaths, importSourcePath, importDestinationPath); } diff --git a/src/shared.h b/src/shared.h index 9e0ff6d1..6c3e3fd3 100644 --- a/src/shared.h +++ b/src/shared.h @@ -29,54 +29,39 @@ #ifndef LINUX_DEPLOMYMENT_SHARED_H #define LINUX_DEPLOMYMENT_SHARED_H -#include -#include #include -#include - -extern int logLevel; -#define LogError() if (logLevel < 0) {} else qDebug() << "ERROR:" -#define LogWarning() if (logLevel < 1) {} else qDebug() << "WARNING:" -#define LogNormal() if (logLevel < 2) {} else qDebug() << "Log:" -#define LogDebug() if (logLevel < 3) {} else qDebug() << "Log:" - -extern QString appBinaryPath; -extern bool runStripEnabled; -extern bool bundleAllButCoreLibs; -extern bool fhsLikeMode; -extern QString fhsPrefix; class LibraryInfo { -public: - bool isDylib; - QString libraryDirectory; - QString libraryName; - QString libraryPath; - QString binaryDirectory; - QString binaryName; - QString binaryPath; - QString rpathUsed; - QString version; - QString installName; - QString deployedInstallName; - QString sourceFilePath; - QString libraryDestinationDirectory; - QString binaryDestinationDirectory; + public: + bool isDylib; + QString libraryDirectory; + QString libraryName; + QString libraryPath; + QString binaryDirectory; + QString binaryName; + QString binaryPath; + QString rpathUsed; + QString version; + QString installName; + QString deployedInstallName; + QString sourceFilePath; + QString libraryDestinationDirectory; + QString binaryDestinationDirectory; }; class DylibInfo { -public: - QString binaryPath; + public: + QString binaryPath; }; class LddInfo { -public: - QString installName; - QString binaryPath; - QList dependencies; + public: + QString installName; + QString binaryPath; + QList dependencies; }; bool operator==(const LibraryInfo &a, const LibraryInfo &b); @@ -85,45 +70,111 @@ QDebug operator<<(QDebug debug, const LibraryInfo &info); class AppDirInfo { public: - QString path; - QString binaryPath; - QStringList libraryPaths; + QString path; + QString binaryPath; + QStringList libraryPaths; }; class DeploymentInfo { -public: - QString qtPath; - QString pluginPath; - QStringList deployedLibraries; - QSet rpathsUsed; - bool useLoaderPath; - bool isLibrary; + public: + QString qtPath; + QString pluginPath; + QStringList deployedLibraries; + QSet rpathsUsed; + bool useLoaderPath; + bool isLibrary; }; inline QDebug operator<<(QDebug debug, const AppDirInfo &info); -void changeQtLibraries(const QString appPath, const QString &qtPath); -void changeQtLibraries(const QList libraries, const QStringList &binaryPaths, const QString &qtPath); - -LddInfo findDependencyInfo(const QString &binaryPath); -LibraryInfo parseLddLibraryLine(const QString &line, const QString &appDirPath, const QSet &rpaths); -QString findAppBinary(const QString &appDirPath); -QList getQtLibraries(const QString &path, const QString &appDirPath, const QSet &rpaths); -QList getQtLibraries(const QStringList &lddLines, const QString &appDirPath, const QSet &rpaths); -QString copyLibrary(const LibraryInfo &library, const QString path); -DeploymentInfo deployQtLibraries(const QString &appDirPath, const QStringList &additionalExecutables); -DeploymentInfo deployQtLibraries(QList libraries,const QString &bundlePath, const QStringList &binaryPaths, bool useLoaderPath); -void deployPlugins(const QString &appDirPath, DeploymentInfo deploymentInfo); -bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, QStringList &qmlDirs); -void changeIdentification(const QString &id, const QString &binaryPath); -void changeInstallName(const QString &oldName, const QString &newName, const QString &binaryPath); -void runStrip(const QString &binaryPath); -void stripAppBinary(const QString &bundlePath); -QString findAppBinary(const QString &appDirPath); -QStringList findAppLibraries(const QString &appDirPath); -bool patchQtCore(const QString &path, const QString &variable, const QString &value); -int createAppImage(const QString &appBundlePath); -bool checkAppImagePrerequisites(const QString &appBundlePath); +class Deploy +{ + public: + QString appBinaryPath; + QString fhsPrefix; + bool fhsLikeMode; + bool bundleAllButCoreLibs; + bool runStripEnabled; + bool alwaysOwerwriteEnabled; + int logLevel; + + explicit Deploy(); + ~Deploy(); + + void changeQtLibraries(const QString appPath, + const QString &qtPath); + void changeQtLibraries(const QList libraries, + const QStringList &binaryPaths, + const QString &qtPath); + LddInfo findDependencyInfo(const QString &binaryPath); + LibraryInfo parseLddLibraryLine(const QString &line, + const QString &appDirPath, + const QSet &rpaths); + QList getQtLibraries(const QString &path, + const QString &appDirPath, + const QSet &rpaths); + QList getQtLibraries(const QList &dependencies, + const QString &appDirPath, + const QSet &rpaths); + DeploymentInfo deployQtLibraries(const QString &appDirPath, + const QStringList &additionalExecutables); + DeploymentInfo deployQtLibraries(QList libraries, + const QString &bundlePath, + const QStringList &binaryPaths, + bool useLoaderPath); + void deployPlugins(const QString &appDirPath, + DeploymentInfo deploymentInfo); + void deployPlugins(const AppDirInfo &appDirInfo, + const QString &pluginSourcePath, + const QString pluginDestinationPath, + DeploymentInfo deploymentInfo); + bool deployQmlImports(const QString &appDirPath, + DeploymentInfo deploymentInfo, + QStringList &qmlDirs); + void changeIdentification(const QString &id, const QString &binaryPath); + void runStrip(const QString &binaryPath); + void stripAppBinary(const QString &bundlePath); + QStringList findAppLibraries(const QString &appDirPath); + bool patchQtCore(const QString &path, const QString &variable, + const QString &value); + int createAppImage(const QString &appBundlePath); + bool checkAppImagePrerequisites(const QString &appBundlePath); + + QDebug LogError(); + QDebug LogWarning(); + QDebug LogNormal(); + QDebug LogDebug(); + + private: + QString m_bundleLibraryDirectory; + QStringList m_librarySearchPath; + QMap m_qtToBeBundledInfo; + QString m_log; + bool m_appstoreCompliant; + bool m_deployLibrary; + + bool lddOutputContainsLinuxVDSO(const QString &lddOutput); + bool copyFilePrintStatus(const QString &from, const QString &to); + int containsHowOften(QStringList haystack, QString needle); + QSet getBinaryRPaths(const QString &path, + bool resolve = true, + QString executablePath = QString()); + QList getQtLibrariesForPaths(const QStringList &paths, + const QString &appDirPath, + const QSet &rpaths); + bool recursiveCopy(const QString &sourcePath, const QString &destinationPath); + void recursiveCopyAndDeploy(const QString &appDirPath, + const QSet &rpaths, + const QString &sourcePath, + const QString &destinationPath); + QString copyDylib(const LibraryInfo &library, const QString path); + void runPatchelf(QStringList options); + QString captureOutput(const QString &command); + void deployQmlImport(const QString &appDirPath, + const QSet &rpaths, + const QString &importSourcePath, + const QString &importName); +}; #endif From 1f27463248be9e50bcb24f1e43592b216130fca8 Mon Sep 17 00:00:00 2001 From: Gonzalo Exequiel Pedone Date: Fri, 26 May 2017 14:59:17 -0300 Subject: [PATCH 05/12] Added excludelist as resource. More code cleanup. --- excludelist | 145 ++++++++++++++++++++++++++++++++++++++++++++++ linuxdeployqt.pro | 3 + linuxdeployqt.qrc | 5 ++ src/main.cpp | 128 +++++++++++++++++++--------------------- src/shared.cpp | 72 +++++++++++++++++------ 5 files changed, 266 insertions(+), 87 deletions(-) create mode 100644 excludelist create mode 100644 linuxdeployqt.qrc diff --git a/excludelist b/excludelist new file mode 100644 index 00000000..c5fb163f --- /dev/null +++ b/excludelist @@ -0,0 +1,145 @@ +# This file lists libraries that we will assume to be present on the host system and hence +# should NOT be bundled inside AppImages. This is a working document; expect it to change +# over time. File format: one filename per line. Each entry should have a justification comment. + +ld-linux.so.2 +ld-linux-x86-64.so.2 +libanl.so.1 +libBrokenLocale.so.1 +libcidn.so.1 +libcrypt.so.1 +libc.so.6 +libdl.so.2 +libm.so.6 +libmvec.so.1 +libnsl.so.1 +libnss_compat.so.2 +libnss_db.so.2 +libnss_dns.so.2 +libnss_files.so.2 +libnss_hesiod.so.2 +libnss_nisplus.so.2 +libnss_nis.so.2 +libpthread.so.0 +libresolv.so.2 +librt.so.1 +libthread_db.so.1 +libutil.so.1 +# These files are all part of the GNU C Library which should never be bundled. +# List was generated from a fresh build of glibc 2.25. + +libstdc++.so.6 +# Workaround for: +# usr/lib/libstdc++.so.6: version `GLIBCXX_3.4.21' not found + +libGL.so.1 +# Part of the video driver (OpenGL); present on any regular +# desktop system, may also be provided by proprietary drivers. +# Known to cause issues if it's bundled. + +libdrm.so.2 +# Workaround for: +# Antergos Linux release 2015.11 (ISO-Rolling) +# /usr/lib/libdrm_amdgpu.so.1: error: symbol lookup error: undefined symbol: drmGetNodeTypeFromFd (fatal) +# libGL error: unable to load driver: swrast_dri.so +# libGL error: failed to load driver: swrast +# Unrecognized OpenGL version + +libxcb.so.1 +# Workaround for: +# Fedora 23 +# symbol lookup error: /lib64/libxcb-dri3.so.0: undefined symbol: xcb_send_fd +# Uncertain if this is required to be bundled for some distributions - if so we need to write a version check script and use LD_PRELOAD to load the system version if it is newer +# Fedora 25: +# undefined symbol: xcb_send_request_with_fds +# https://github.com/probonopd/AppImages/issues/128 + +libX11.so.6 +# Workaround for: +# Fedora 23 +# symbol lookup error: ./lib/libX11.so.6: undefined symbol: xcb_wait_for_reply64 +# Uncertain if this is required to be bundled for some distributions - if so we need to write a version check script and use LD_PRELOAD to load the system version if it is newer + +libgio-2.0.so.0 +# Workaround for: +# On Ubuntu, "symbol lookup error: /usr/lib/x86_64-linux-gnu/gtk-2.0/modules/liboverlay-scrollbar.so: undefined symbol: g_settings_new" + +libgdk-x11-2.0.so.0 +libgtk-x11-2.0.so.0 +# Simply to reduce size - not known to cause issues + +libasound.so.2 +# Workaround for: +# No sound, e.g., in VLC.AppImage (does not find sound cards) + +libgdk_pixbuf-2.0.so.0 +# Workaround for: +# On Ubuntu, get (inkscape:25621): GdkPixbuf-WARNING **: Error loading XPM image loader: Image type 'xpm' is not supported + +libfontconfig.so.1 +# Workaround for: +# Application stalls when loading fonts during application launch; e.g., KiCad on ubuntu-mate + +libselinux.so.1 +# Workaround for: +# sed: error while loading shared libraries: libpcre.so.3: cannot open shared object file: No such file or directory + +# The following are assumed to be part of the base system +# Removing these has worked e.g., for Krita. Feel free to report if +# you think that some of these should go into AppImages and why. +libcom_err.so.2 +libcrypt.so.1 +libexpat.so.1 +libgcc_s.so.1 +libglib-2.0.so.0 +libgpg-error.so.0 +libgssapi_krb5.so.2 # Disputed, seemingly needed by Arch Linux since Kerberos is named differently there +# libgssapi.so.3 # Seemingly needed when running Ubuntu 14.04 binaries on Fedora 23 +libhcrypto.so.4 +# libheimbase.so.1 # Seemingly needed when running Ubuntu 14.04 binaries on Fedora 23 +# libheimntlm.so.0 # Seemingly needed when running Ubuntu 14.04 binaries on Fedora 23 +libhx509.so.5 +libICE.so.6 +libidn.so.11 +libk5crypto.so.3 +libkeyutils.so.1 +libkrb5.so.26 # Disputed, seemingly needed by Arch Linux since Kerberos is named differently there +libkrb5.so.3 # Disputed, seemingly needed by Arch Linux since Kerberos is named differently there +libkrb5support.so.0 # Disputed, seemingly needed by Arch Linux since Kerberos is named differently there +libp11-kit.so.0 +# libpcre.so.3 # Missing on Fedora 24 and on SLED 12 SP1 +libroken.so.18 +# libsasl2.so.2 # Seemingly needed when running Ubuntu 14.04 binaries on Fedora 23 +libSM.so.6 +libusb-1.0.so.0 +libuuid.so.1 +libwind.so.0 +libz.so.1 + +# Potentially dangerous libraries +libgobject-2.0.so.0 + +# Workaround for: +# e.g., Spotify +# relocation error: /lib/x86_64-linux-gnu/libgcrypt.so.20: +# symbol gpgrt_lock_lock, version GPG_ERROR_1.0 not defined +# in file libgpg-error.so.0 with link time reference +libgpg-error.so.0 + +# Unsolved issue: +# https://github.com/probonopd/linuxdeployqt/issues/35 +# Error initializing NSS with a persistent database (sql:/home/me/.pki/nssdb): libsoftokn3.so: cannot open shared object file: No such file or directory +# Error initializing NSS without a persistent database: NSS error code: -5925 +# nss_error=-5925, os_error=0 +# libnss3.so should not be removed from the bundles, as this causes other issues, e.g., +# https://github.com/probonopd/linuxdeployqt/issues/35#issuecomment-256213517 +# and https://github.com/probonopd/AppImages/pull/114 +# libnss3.so + +# The following cannot be excluded, see +# https://github.com/probonopd/AppImages/commit/6c7473d8cdaaa2572248dcc53d7f617a577ade6b +# http://stackoverflow.com/questions/32644157/forcing-a-binary-to-use-a-specific-newer-version-of-a-shared-library-so +# libssl.so.1 +# libssl.so.1.0.0 +# libcrypto.so.1 +# libcrypto.so.1.0.0 diff --git a/linuxdeployqt.pro b/linuxdeployqt.pro index 093080ae..d0a6ba70 100644 --- a/linuxdeployqt.pro +++ b/linuxdeployqt.pro @@ -22,3 +22,6 @@ TARGET = $${COMMONS_TARGET} INSTALLS += target target.path = $${BINDIR} + +RESOURCES += \ + linuxdeployqt.qrc diff --git a/linuxdeployqt.qrc b/linuxdeployqt.qrc new file mode 100644 index 00000000..1fa23769 --- /dev/null +++ b/linuxdeployqt.qrc @@ -0,0 +1,5 @@ + + + excludelist + + diff --git a/src/main.cpp b/src/main.cpp index 377a8838..46a271e3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -113,7 +113,7 @@ int main(int argc, char **argv) * TODO: Proposed option set. -scan-bin-paths and -scan-qml-paths * options may subtitute -executable and -qmldir. * - * -library-blacklist: When deploying required libraries, avoid including + * -excludelist : When deploying required libraries, avoid including * libraries listed here. * -extra-plugins : Also deploy this plugins. * -extra-files : Also copy these files to the deploy folder (useful for @@ -140,12 +140,11 @@ int main(int argc, char **argv) * to do when using linuxdeployqt. */ if (firstArgument.endsWith(".desktop")){ qDebug() << "Desktop file as first argument:" << firstArgument; - QSettings * settings = 0; - settings = new QSettings(firstArgument, QSettings::IniFormat); - desktopExecEntry = settings->value("Desktop Entry/Exec", "r").toString().split(' ').first().split('/').last().trimmed(); + QSettings settings(firstArgument, QSettings::IniFormat); + desktopExecEntry = settings.value("Desktop Entry/Exec", "r").toString().split(' ').first().split('/').last().trimmed(); qDebug() << "desktopExecEntry:" << desktopExecEntry; desktopFile = firstArgument; - desktopIconEntry = settings->value("Desktop Entry/Icon", "r").toString().split(' ').first().split('.').first().trimmed(); + desktopIconEntry = settings.value("Desktop Entry/Icon", "r").toString().split(' ').first().split('.').first().trimmed(); qDebug() << "desktopIconEntry:" << desktopIconEntry; QString candidateBin = QDir::cleanPath(QFileInfo(firstArgument).absolutePath() + desktopExecEntry); // Not FHS-like @@ -169,36 +168,40 @@ int main(int argc, char **argv) } } - /* Only if we could not find it below the directory in which the desktop file resides, search above */ + /* Only if we could not find it below the directory in which the + * desktop file resides, search above */ if (deploy.appBinaryPath.isEmpty()) { - if(QFileInfo(QDir::cleanPath(QFileInfo(firstArgument).absolutePath() + "/../../bin/" + desktopExecEntry)).exists()){ + if (QFileInfo(QDir::cleanPath(QFileInfo(firstArgument).absolutePath() + "/../../bin/" + desktopExecEntry)).exists()){ directoryToBeSearched = QDir::cleanPath(QFileInfo(firstArgument).absolutePath() + "/../../"); } else { directoryToBeSearched = QDir::cleanPath(QFileInfo(firstArgument).absolutePath() + "/../"); } + QDirIterator it2(directoryToBeSearched, QDirIterator::Subdirectories); + while (it2.hasNext()) { it2.next(); - if((it2.fileName() == desktopExecEntry) && (it2.fileInfo().isFile()) && (it2.fileInfo().isExecutable())){ + + if ((it2.fileName() == desktopExecEntry) && (it2.fileInfo().isFile()) && (it2.fileInfo().isExecutable())){ qDebug() << "Found binary from desktop file:" << it2.fileInfo().canonicalFilePath(); deploy.appBinaryPath = it2.fileInfo().absoluteFilePath(); + break; } } } - if(deploy.appBinaryPath == ""){ - if((QFileInfo(candidateBin).isFile()) && (QFileInfo(candidateBin).isExecutable())) { + if (deploy.appBinaryPath.isEmpty()) { + if (QFileInfo(candidateBin).isFile() && QFileInfo(candidateBin).isExecutable()) { deploy.appBinaryPath = QFileInfo(candidateBin).absoluteFilePath(); } else { deploy.LogError() << "Could not determine the path to the executable based on the desktop file\n"; + return 1; } } - } else { - deploy.appBinaryPath = firstArgument; - deploy.appBinaryPath = QFileInfo(QDir::cleanPath(deploy.appBinaryPath)).absoluteFilePath(); + deploy.appBinaryPath = QFileInfo(QDir::cleanPath(firstArgument)).absoluteFilePath(); } } @@ -219,13 +222,8 @@ int main(int argc, char **argv) return 1; } - bool plugins = true; - bool appimage = false; - QStringList additionalExecutables; - bool qmldirArgumentUsed = false; - QStringList qmlDirs; - - /* FHS-like mode is for an application that has been installed to a $PREFIX which is otherwise empty, e.g., /path/to/usr. + /* FHS-like mode is for an application that has been installed to a $PREFIX + * which is otherwise empty, e.g., /path/to/usr. * In this case, we want to construct an AppDir in /path/to. */ if (QDir().exists(QDir::cleanPath(deploy.appBinaryPath + "/../../bin"))) { deploy.fhsPrefix = QDir::cleanPath(deploy.appBinaryPath + "/../../"); @@ -266,15 +264,17 @@ int main(int argc, char **argv) QFile::link(relativeBinPath, appDirPath + "/AppRun"); /* Copy the desktop file in place, into the top level of the AppDir */ - if(desktopFile != ""){ + if (!desktopFile.isEmpty()) { QString destination = QDir::cleanPath(appDirPath + "/" + QFileInfo(desktopFile).fileName()); - if(QFileInfo(destination).exists() == false){ - if (QFile::copy(desktopFile, destination)){ + + if (!QFileInfo(destination).exists()) { + if (QFile::copy(desktopFile, destination)) qDebug() << "Copied" << desktopFile << "to" << destination; - } } - if(QFileInfo(destination).isFile() == false){ + + if (!QFileInfo(destination).isFile()) { deploy.LogError() << destination << "does not exist and could not be copied there\n"; + return 1; } } @@ -283,18 +283,18 @@ int main(int argc, char **argv) QStringList candidates; QString iconToBeUsed; - if(!desktopIconEntry.isEmpty()) { - QDirIterator it3(appDirPath, QDirIterator::Subdirectories); + if (!desktopIconEntry.isEmpty()) { + QDirIterator dirs(appDirPath, QDirIterator::Subdirectories); - while (it3.hasNext()) { - it3.next(); + while (dirs.hasNext()) { + dirs.next(); - if (it3.fileName().startsWith(desktopIconEntry) - && (it3.fileName().endsWith(".png") - || it3.fileName().endsWith(".svg") - || it3.fileName().endsWith(".svgz") - || it3.fileName().endsWith(".xpm"))) { - candidates.append(it3.filePath()); + if (dirs.fileName().startsWith(desktopIconEntry) + && (dirs.fileName().endsWith(".png") + || dirs.fileName().endsWith(".svg") + || dirs.fileName().endsWith(".svgz") + || dirs.fileName().endsWith(".xpm"))) { + candidates.append(dirs.filePath()); } } @@ -305,41 +305,20 @@ int main(int argc, char **argv) iconToBeUsed = candidates.first(); // The only choice } else if(candidates.length() > 1){ foreach(QString current, candidates) { - if(current.contains("256")){ - iconToBeUsed = current; - continue; - } - if(current.contains("128")){ - iconToBeUsed = current; - continue; - } - if(current.contains("svg")){ - iconToBeUsed = current; - continue; - } - if(current.contains("svgz")){ - iconToBeUsed = current; - continue; - } - if(current.contains("512")){ - iconToBeUsed = current; - continue; - } - if(current.contains("1024")){ - iconToBeUsed = current; - continue; - } - if(current.contains("64")){ - iconToBeUsed = current; - continue; - } - if(current.contains("48")){ - iconToBeUsed = current; - continue; - } - if(current.contains("xpm")){ - iconToBeUsed = current; - continue; + for (auto &iconFormat: QStringList {"256", + "128", + "svg", + "svgz", + "512", + "1024", + "64", + "48", + "xpm"}) { + if (current.contains(iconFormat)) { + iconToBeUsed = current; + + break; + } } } } @@ -385,12 +364,16 @@ int main(int argc, char **argv) } } + bool plugins = true; + // Set options from command line if (cliParser.isSet(noPluginsOpt)) { deploy.LogDebug() << "Argument found:" << noPluginsOpt.valueName(); plugins = false; } + bool appimage = false; + if (cliParser.isSet(appimageOpt)) { deploy.LogDebug() << "Argument found:" << appimageOpt.valueName(); appimage = true; @@ -418,6 +401,8 @@ int main(int argc, char **argv) deploy.LogError() << "Could not parse verbose level"; } + QStringList additionalExecutables; + if (cliParser.isSet(executableOpt)) { deploy.LogDebug() << "Argument found:" << executableOpt.valueName(); QString executables = cliParser.value(executableOpt).trimmed(); @@ -428,6 +413,9 @@ int main(int argc, char **argv) deploy.LogError() << "Missing executable path"; } + QStringList qmlDirs; + bool qmldirArgumentUsed = false; + if (cliParser.isSet(qmldirOpt)) { deploy.LogDebug() << "Argument found:" << qmldirOpt.valueName(); QString dirs = cliParser.value(qmldirOpt).trimmed(); diff --git a/src/shared.cpp b/src/shared.cpp index b2a4b8e0..33126207 100644 --- a/src/shared.cpp +++ b/src/shared.cpp @@ -39,12 +39,12 @@ #include "shared.h" -bool operator==(const LibraryInfo &a, const LibraryInfo &b) +bool operator ==(const LibraryInfo &a, const LibraryInfo &b) { return ((a.libraryPath == b.libraryPath) && (a.binaryPath == b.binaryPath)); } -QDebug operator<<(QDebug debug, const LibraryInfo &info) +QDebug operator <<(QDebug debug, const LibraryInfo &info) { debug << "Library name" << info.libraryName << "\n"; debug << "Library directory" << info.libraryDirectory << "\n"; @@ -62,7 +62,7 @@ QDebug operator<<(QDebug debug, const LibraryInfo &info) return debug; } -inline QDebug operator<<(QDebug debug, const AppDirInfo &info) +inline QDebug operator <<(QDebug debug, const AppDirInfo &info) { debug << "Application bundle path" << info.path << "\n"; debug << "Binary path" << info.binaryPath << "\n"; @@ -194,11 +194,11 @@ LibraryInfo Deploy::parseLddLibraryLine(const QString &line, { Q_UNUSED(rpaths); - if (fhsLikeMode == false) { + if (fhsLikeMode) { + this->m_bundleLibraryDirectory= "lib"; // relative to bundle + } else { QString relativePrefix = fhsPrefix.replace(appDirPath+"/", ""); this->m_bundleLibraryDirectory = relativePrefix + "/lib/"; - } else { - this->m_bundleLibraryDirectory= "lib"; // relative to bundle } LogDebug() << "bundleLibraryDirectory:" << this->m_bundleLibraryDirectory; @@ -225,8 +225,26 @@ LibraryInfo Deploy::parseLddLibraryLine(const QString &line, done */ + QFile excludelistFile(":/excludelist"); + + if (!excludelistFile.open(QIODevice::ReadOnly | QIODevice::Text)) + return info; + QStringList excludelist; - excludelist << "libasound.so.2" << "libcom_err.so.2" << "libcrypt.so.1" << "libc.so.6" << "libdl.so.2" << "libdrm.so.2" << "libexpat.so.1" << "libfontconfig.so.1" << "libgcc_s.so.1" << "libgdk_pixbuf-2.0.so.0" << "libgdk-x11-2.0.so.0" << "libgio-2.0.so.0" << "libglib-2.0.so.0" << "libGL.so.1" << "libgobject-2.0.so.0" << "libgpg-error.so.0" << "libgssapi_krb5.so.2" << "libgtk-x11-2.0.so.0" << "libhcrypto.so.4" << "libhx509.so.5" << "libICE.so.6" << "libidn.so.11" << "libk5crypto.so.3" << "libkeyutils.so.1" << "libkrb5.so.26" << "libkrb5.so.3" << "libkrb5support.so.0" << "libm.so.6" << "libnss3.so" << "libnssutil3.so" << "libp11-kit.so.0" << "libpcre.so.3" << "libpthread.so.0" << "libresolv.so.2" << "libroken.so.18" << "librt.so.1" << "libselinux.so.1" << "libSM.so.6" << "libstdc++.so.6" << "libusb-1.0.so.0" << "libuuid.so.1" << "libwind.so.0" << "libX11.so.6" << "libxcb.so.1" << "libz.so.1"; + + for (auto line = excludelistFile.readLine().trimmed(); !excludelistFile.atEnd();) { + if (line.startsWith("#")) + continue; + else { + int comment = line.indexOf("#"); + + if (comment > 0) + line = line.left(comment); + } + + excludelist << line; + } + LogDebug() << "excludelist:" << excludelist; if (!trimmed.contains("libicu")) { @@ -446,8 +464,13 @@ DeploymentInfo Deploy::deployQtLibraries(const QString &appDirPath, LogDebug() << "Qt libs path determined from qmake:" << qtLibsPath; QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QString oldPath = env.value("LD_LIBRARY_PATH"); - QString newPath = qtLibsPath + ":" + oldPath; // FIXME: If we use a ldd replacement, we still need to observe this path - // FIXME: Directory layout might be different for system Qt; cannot assume lib/ to always be inside the Qt directory + + // FIXME: If we use a ldd replacement, we still need to observe this + // path + QString newPath = qtLibsPath + ":" + oldPath; + + // FIXME: Directory layout might be different for system Qt; + // cannot assume lib/ to always be inside the Qt directory LogDebug() << "Changed LD_LIBRARY_PATH:" << newPath; setenv("LD_LIBRARY_PATH",newPath.toUtf8().constData(),1); } @@ -884,8 +907,10 @@ void Deploy::changeIdentification(const QString &id, const QString &binaryPath) LogNormal() << "Changing rpath in" << binaryPath << "to" << id; runPatchelf(QStringList() << "--set-rpath" << id << binaryPath); - // NOTE: Code below make no sense, Qt can already find paths relative to - // the executable, and pathching this way can break the binary. + /* NOTE: Code below make no sense, Qt can already find paths relative to + * the executable, and can be configured through environment variables in + * the deployed project, and pathching this way can break the binary. + */ // qt_prfxpath: if (binaryPath.contains("libQt5Core")) { @@ -961,8 +986,14 @@ void Deploy::runStrip(const QString &binaryPath) patchelfread.waitForFinished(); if (patchelfread.exitCode() != 0) { - LogError() << "Error reading rpath with patchelf" << QFileInfo(resolvedPath).completeBaseName() << ":" << patchelfread.readAllStandardError(); - LogError() << "Error reading rpath with patchelf" << QFileInfo(resolvedPath).completeBaseName() << ":" << patchelfread.readAllStandardOutput(); + LogError() << "Error reading rpath with patchelf" + << QFileInfo(resolvedPath).completeBaseName() + << ":" + << patchelfread.readAllStandardError(); + LogError() << "Error reading rpath with patchelf" + << QFileInfo(resolvedPath).completeBaseName() + << ":" + << patchelfread.readAllStandardOutput(); exit(1); } @@ -1351,7 +1382,9 @@ void Deploy::recursiveCopyAndDeploy(const QString &appDirPath, LogNormal() << "copy:" << sourcePath << destinationPath; - QStringList files = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Files | QDir::NoDotAndDotDot); + auto files = + QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), + QDir::Files | QDir::NoDotAndDotDot); foreach (QString file, files) { const QString fileSourcePath = sourcePath + QLatin1Char('/') + file; @@ -1382,10 +1415,15 @@ void Deploy::recursiveCopyAndDeploy(const QString &appDirPath, } } - QStringList subdirs = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Dirs | QDir::NoDotAndDotDot); + auto subdirs = + QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), + QDir::Dirs | QDir::NoDotAndDotDot); foreach (QString dir, subdirs) - recursiveCopyAndDeploy(appDirPath, rpaths, sourcePath + QLatin1Char('/') + dir, destinationPath + QLatin1Char('/') + dir); + recursiveCopyAndDeploy(appDirPath, + rpaths, + sourcePath + QLatin1Char('/') + dir, + destinationPath + QLatin1Char('/') + dir); } QString Deploy::copyDylib(const LibraryInfo &library, const QString path) @@ -1469,7 +1507,7 @@ void Deploy::deployQmlImport(const QString &appDirPath, QString importDestinationPath; - if(fhsLikeMode){ + if (fhsLikeMode){ QFileInfo qfi(applicationBundle.binaryPath); QString qtTargetDir = qfi.absoluteDir().absolutePath() + "/../"; importDestinationPath = qtTargetDir + "/qml/" + importName; From 9f26499212cae83f134eb92815e3c65f04bbb05d Mon Sep 17 00:00:00 2001 From: Gonzalo Exequiel Pedone Date: Fri, 26 May 2017 19:51:16 -0300 Subject: [PATCH 06/12] Integrated all changes from probonopd/linuxdeployqt@2f28ccd. --- .travis.yml | 18 +- README.md | 2 + src/main.cpp | 19 ++ src/shared.cpp | 372 ++++++++++++++++++++++++++++++++++++- src/shared.h | 13 ++ tests/tests-environment.sh | 2 +- 6 files changed, 414 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2c1b97a7..f3a4636d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,30 @@ language: cpp sudo: required dist: trusty - +addons: + apt: + packages: + - gdb + - apport os: linux env: global: - DISPLAY=:99 +compiler: + - gcc + before_install: + # Enable core dumps + # https://github.com/springmeyer/travis-coredump/blob/master/.travis.yml + - ulimit -a -S + - ulimit -a -H + - ulimit -c unlimited -S - ./tests/tests-environment.sh script: - - ./tests/tests-ci.sh + - ./tests/tests-ci.sh after_success: - wget -c https://github.com/probonopd/uploadtool/raw/master/upload.sh @@ -20,6 +32,8 @@ after_success: after_script: - "xpra stop :99" + - find ./ -maxdepth 1 -name 'core*' + - for i in $(find ./ -maxdepth 1 -name 'core*' -print); do gdb $(pwd)/test core* -ex "thread apply all bt" -ex "set pagination 0" -batch; done; branches: except: diff --git a/README.md b/README.md index 4083e69e..ab3af1a7 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Options: -executable= : Let the given executable use the deployed libraries too -qmldir= : Scan for QML imports in the given path -always-overwrite : Copy files even if the target file exists + -no-translations : Skip deployment of translations linuxdeployqt takes an application as input and makes it self-contained by copying in the Qt libraries and plugins that @@ -180,6 +181,7 @@ These projects are already using [Travis CI](http://travis-ci.org/) and linuxdep - https://github.com/crapp/labpowerqt/ - https://github.com/probonopd/linuxdeployqt/ obviously ;-) - https://github.com/xdgurl/xdgurl +- https://github.com/QNapi/qnapi This project is already using linuxdeployqt in a custom Jenkins workflow: - https://github.com/appimage-packages/ diff --git a/src/main.cpp b/src/main.cpp index 46a271e3..ebe0ad36 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -109,6 +109,11 @@ int main(int argc, char **argv) "always-overwrite", QObject::tr("Copy files even if the target file exists")); cliParser.addOption(alwaysOverwriteOpt); + + QCommandLineOption noTranslationsOpt( + "no-translations", + QObject::tr("Skip deployment of translations")); + cliParser.addOption(noTranslationsOpt); /* * TODO: Proposed option set. -scan-bin-paths and -scan-qml-paths * options may subtitute -executable and -qmldir. @@ -432,6 +437,13 @@ int main(int argc, char **argv) deploy.alwaysOwerwriteEnabled = true; } + bool skipTranslations = false; + + if (cliParser.isSet(noTranslationsOpt)) { + deploy.LogDebug() << "Argument found:" << noTranslationsOpt.valueName(); + skipTranslations = true; + } + if (appimage && !deploy.checkAppImagePrerequisites(appDirPath)) { deploy.LogError() << "checkAppImagePrerequisites failed\n"; @@ -460,16 +472,23 @@ int main(int argc, char **argv) deploymentInfo.deployedLibraries = deploymentInfo.deployedLibraries.toSet().toList(); } + deploymentInfo.usedModulesMask = 0; + deploy.findUsedModules(deploymentInfo); + if (plugins && !deploymentInfo.qtPath.isEmpty()) { if (deploymentInfo.pluginPath.isEmpty()) deploymentInfo.pluginPath = QDir::cleanPath(deploymentInfo.qtPath + "/../plugins"); deploy.deployPlugins(appDirPath, deploymentInfo); + deploy.createQtConf(appDirPath); } if (deploy.runStripEnabled) deploy.stripAppBinary(appDirPath); + if (!skipTranslations) + deploy.deployTranslations(appDirPath, deploymentInfo.usedModulesMask); + if (appimage) { int result = deploy.createAppImage(appDirPath); deploy.LogDebug() << "result:" << result; diff --git a/src/shared.cpp b/src/shared.cpp index 33126207..38899931 100644 --- a/src/shared.cpp +++ b/src/shared.cpp @@ -39,6 +39,123 @@ #include "shared.h" +enum QtModule +#if defined(Q_COMPILER_CLASS_ENUM) || defined(Q_CC_MSVC) + : quint64 +#endif +{ + QtBluetoothModule = 0x0000000000000001, + QtCLuceneModule = 0x0000000000000002, + QtConcurrentModule = 0x0000000000000004, + QtCoreModule = 0x0000000000000008, + QtDeclarativeModule = 0x0000000000000010, + QtDesignerComponents = 0x0000000000000020, + QtDesignerModule = 0x0000000000000040, + QtGuiModule = 0x0000000000000080, + QtCluceneModule = 0x0000000000000100, + QtHelpModule = 0x0000000000000200, + QtMultimediaModule = 0x0000000000000400, + QtMultimediaWidgetsModule = 0x0000000000000800, + QtMultimediaQuickModule = 0x0000000000001000, + QtNetworkModule = 0x0000000000002000, + QtNfcModule = 0x0000000000004000, + QtOpenGLModule = 0x0000000000008000, + QtPositioningModule = 0x0000000000010000, + QtPrintSupportModule = 0x0000000000020000, + QtQmlModule = 0x0000000000040000, + QtQuickModule = 0x0000000000080000, + QtQuickParticlesModule = 0x0000000000100000, + QtScriptModule = 0x0000000000200000, + QtScriptToolsModule = 0x0000000000400000, + QtSensorsModule = 0x0000000000800000, + QtSerialPortModule = 0x0000000001000000, + QtSqlModule = 0x0000000002000000, + QtSvgModule = 0x0000000004000000, + QtTestModule = 0x0000000008000000, + QtWidgetsModule = 0x0000000010000000, + QtWinExtrasModule = 0x0000000020000000, + QtXmlModule = 0x0000000040000000, + QtXmlPatternsModule = 0x0000000080000000, + QtWebKitModule = 0x0000000100000000, + QtWebKitWidgetsModule = 0x0000000200000000, + QtQuickWidgetsModule = 0x0000000400000000, + QtWebSocketsModule = 0x0000000800000000, + QtEnginioModule = 0x0000001000000000, + QtWebEngineCoreModule = 0x0000002000000000, + QtWebEngineModule = 0x0000004000000000, + QtWebEngineWidgetsModule = 0x0000008000000000, + QtQmlToolingModule = 0x0000010000000000, + Qt3DCoreModule = 0x0000020000000000, + Qt3DRendererModule = 0x0000040000000000, + Qt3DQuickModule = 0x0000080000000000, + Qt3DQuickRendererModule = 0x0000100000000000, + Qt3DInputModule = 0x0000200000000000, + QtLocationModule = 0x0000400000000000, + QtWebChannelModule = 0x0000800000000000, + QtTextToSpeechModule = 0x0001000000000000, + QtSerialBusModule = 0x0002000000000000 +}; + +struct QtModuleEntry +{ + quint64 module; + const char *option; + const char *libraryName; + const char *translation; +}; + +static QtModuleEntry qtModuleEntries[] = { + { QtBluetoothModule, "bluetooth", "Qt5Bluetooth", 0 }, + { QtCLuceneModule, "clucene", "Qt5CLucene", "qt_help" }, + { QtConcurrentModule, "concurrent", "Qt5Concurrent", "qtbase" }, + { QtCoreModule, "core", "Qt5Core", "qtbase" }, + { QtDeclarativeModule, "declarative", "Qt5Declarative", "qtquick1" }, + { QtDesignerModule, "designer", "Qt5Designer", 0 }, + { QtDesignerComponents, "designercomponents", "Qt5DesignerComponents", 0 }, + { QtEnginioModule, "enginio", "Enginio", 0 }, + { QtGuiModule, "gui", "Qt5Gui", "qtbase" }, + { QtHelpModule, "qthelp", "Qt5Help", "qt_help" }, + { QtMultimediaModule, "multimedia", "Qt5Multimedia", "qtmultimedia" }, + { QtMultimediaWidgetsModule, "multimediawidgets", "Qt5MultimediaWidgets", "qtmultimedia" }, + { QtMultimediaQuickModule, "multimediaquick", "Qt5MultimediaQuick_p", "qtmultimedia" }, + { QtNetworkModule, "network", "Qt5Network", "qtbase" }, + { QtNfcModule, "nfc", "Qt5Nfc", 0 }, + { QtOpenGLModule, "opengl", "Qt5OpenGL", 0 }, + { QtPositioningModule, "positioning", "Qt5Positioning", 0 }, + { QtPrintSupportModule, "printsupport", "Qt5PrintSupport", 0 }, + { QtQmlModule, "qml", "Qt5Qml", "qtdeclarative" }, + { QtQmlToolingModule, "qmltooling", "qmltooling", 0 }, + { QtQuickModule, "quick", "Qt5Quick", "qtdeclarative" }, + { QtQuickParticlesModule, "quickparticles", "Qt5QuickParticles", 0 }, + { QtQuickWidgetsModule, "quickwidgets", "Qt5QuickWidgets", 0 }, + { QtScriptModule, "script", "Qt5Script", "qtscript" }, + { QtScriptToolsModule, "scripttools", "Qt5ScriptTools", "qtscript" }, + { QtSensorsModule, "sensors", "Qt5Sensors", 0 }, + { QtSerialPortModule, "serialport", "Qt5SerialPort", "qtserialport" }, + { QtSqlModule, "sql", "Qt5Sql", "qtbase" }, + { QtSvgModule, "svg", "Qt5Svg", 0 }, + { QtTestModule, "test", "Qt5Test", "qtbase" }, + { QtWebKitModule, "webkit", "Qt5WebKit", 0 }, + { QtWebKitWidgetsModule, "webkitwidgets", "Qt5WebKitWidgets", 0 }, + { QtWebSocketsModule, "websockets", "Qt5WebSockets", "qtwebsockets" }, + { QtWidgetsModule, "widgets", "Qt5Widgets", "qtbase" }, + { QtWinExtrasModule, "winextras", "Qt5WinExtras", 0 }, + { QtXmlModule, "xml", "Qt5Xml", "qtbase" }, + { QtXmlPatternsModule, "xmlpatterns", "Qt5XmlPatterns", "qtxmlpatterns" }, + { QtWebEngineCoreModule, "webenginecore", "Qt5WebEngineCore", 0 }, + { QtWebEngineModule, "webengine", "Qt5WebEngine", "qtwebengine" }, + { QtWebEngineWidgetsModule, "webenginewidgets", "Qt5WebEngineWidgets", 0 }, + { Qt3DCoreModule, "3dcore", "Qt53DCore", 0 }, + { Qt3DRendererModule, "3drenderer", "Qt53DRenderer", 0 }, + { Qt3DQuickModule, "3dquick", "Qt53DQuick", 0 }, + { Qt3DQuickRendererModule, "3dquickrenderer", "Qt53DQuickRenderer", 0 }, + { Qt3DInputModule, "3dinput", "Qt53DInput", 0 }, + { QtLocationModule, "geoservices", "Qt5Location", 0 }, + { QtWebChannelModule, "webchannel", "Qt5WebChannel", 0 }, + { QtTextToSpeechModule, "texttospeech", "Qt5TextToSpeech", 0 }, + { QtSerialBusModule, "serialbus", "Qt5SerialBus", 0 } +}; + bool operator ==(const LibraryInfo &a, const LibraryInfo &b) { return ((a.libraryPath == b.libraryPath) && (a.binaryPath == b.binaryPath)); @@ -78,6 +195,7 @@ Deploy::Deploy(): alwaysOwerwriteEnabled(false), logLevel(1), m_appstoreCompliant(false), + m_qtDetected(0), m_deployLibrary(false) { } @@ -402,20 +520,19 @@ DeploymentInfo Deploy::deployQtLibraries(const QString &appDirPath, LogDebug() << "applicationBundle.binaryPath:" << applicationBundle.binaryPath; // Find out whether Qt is a dependency of the application to be bundled - int qtDetected = 0; LddInfo lddInfo = findDependencyInfo(appBinaryPath); foreach (const DylibInfo dep, lddInfo.dependencies) { LogDebug() << "dep.binaryPath" << dep.binaryPath; if (dep.binaryPath.contains("libQt5")) - qtDetected = 5; + m_qtDetected = 5; if (dep.binaryPath.contains("libQtCore.so.4")) - qtDetected = 4; + m_qtDetected = 4; } - if (qtDetected != 0) { + if (m_qtDetected != 0) { // Determine the location of the Qt to be bundled LogDebug() << "Using qmake to determine the location of the Qt to be bundled"; @@ -428,10 +545,10 @@ DeploymentInfo Deploy::deployQtLibraries(const QString &appDirPath, // Qt 4 on Fedora comes with suffix -qt4 // http://www.geopsy.org/wiki/index.php/Installing_Qt_binary_packages if (qmakePath.isEmpty()) { - if (qtDetected == 5) + if (m_qtDetected == 5) qmakePath = QStandardPaths::findExecutable("qmake-qt5"); - if (qtDetected == 4) + if (m_qtDetected == 4) qmakePath = QStandardPaths::findExecutable("qmake-qt4"); } @@ -441,6 +558,7 @@ DeploymentInfo Deploy::deployQtLibraries(const QString &appDirPath, } QString output = captureOutput(qmakePath + " -query"); + LogDebug() << "-query output from qmake:" << output; QStringList outputLines = output.split("\n", QString::SkipEmptyParts); foreach (const QString &outputLine, outputLines) { @@ -517,6 +635,7 @@ DeploymentInfo Deploy::deployQtLibraries(QList libraries, LogNormal() << "Deploying the following libraries:" << binaryPaths; QStringList copiedLibraries; DeploymentInfo deploymentInfo; + deploymentInfo.requiresQtWidgetsLibrary = false; deploymentInfo.useLoaderPath = useLoaderPath; deploymentInfo.pluginPath = this->m_qtToBeBundledInfo.value("QT_INSTALL_PLUGINS"); QSet rpathsUsed; @@ -530,6 +649,9 @@ DeploymentInfo Deploy::deployQtLibraries(QList libraries, deploymentInfo.qtPath = library.libraryDirectory; } + if (library.libraryName.contains("libQt") && library.libraryName.contains("Widgets.so")) + deploymentInfo.requiresQtWidgetsLibrary = true; + if (library.libraryDirectory.startsWith(bundlePath)) { LogNormal() << library.libraryName << "already deployed, skipping."; @@ -572,6 +694,75 @@ DeploymentInfo Deploy::deployQtLibraries(QList libraries, return deploymentInfo; } +void Deploy::createQtConf(const QString &appDirPath) +{ + // Set Plugins and imports paths. These are relative to QCoreApplication::applicationDirPath() + // which is where the main executable resides; see http://doc.qt.io/qt-5/qt-conf.html + // See https://github.com/probonopd/linuxdeployqt/issues/ 75, 98, 99 + QByteArray contents; + + if (fhsLikeMode) { + contents = "# Generated by linuxdeployqt\n" + "# https://github.com/probonopd/linuxdeployqt/\n" + "[Paths]\n" + "Prefix = ../\n" + "Plugins = plugins\n" + "Imports = qml\n" + "Qml2Imports = qml\n"; + } else { + contents = "# Generated by linuxdeployqt\n" + "# https://github.com/probonopd/linuxdeployqt/\n" + "[Paths]\n" + "Prefix = ./\n" + "Plugins = plugins\n" + "Imports = qml\n" + "Qml2Imports = qml\n"; + } + + QString filePath = appDirPath + "/"; // Is picked up when placed next to the main executable + QString fileName = QDir::cleanPath(appBinaryPath + "/../qt.conf"); + + QDir().mkpath(filePath); + + QFile qtconf(fileName); + + if (qtconf.exists() && !alwaysOwerwriteEnabled) { + LogWarning() << fileName << "already exists, will not overwrite."; + + return; + } + + qtconf.open(QIODevice::WriteOnly); + + if (qtconf.write(contents) != -1) + LogNormal() << "Created configuration file:" << fileName; +} + +void Deploy::createQtConfForQtWebEngineProcess(const QString &appDirPath) +{ + QByteArray contents = "# Generated by linuxdeployqt\n" + "# https://github.com/probonopd/linuxdeployqt/\n" + "[Paths]\n" + "Prefix = ../\n"; + QString filePath = appDirPath + "/"; + QString fileName = filePath + "qt.conf"; + + QDir().mkpath(filePath); + QFile qtconf(fileName); + + if (qtconf.exists() && !alwaysOwerwriteEnabled) { + LogWarning() << fileName << "already exists, will not overwrite."; + return; + } + + qtconf.open(QIODevice::WriteOnly); + + if (qtconf.write(contents) != -1) { + LogNormal() << "Created configuration file for Qt WebEngine process:" << fileName; + LogNormal() << "This file sets the prefix option to parent directory of browser process executable"; + } +} + void Deploy::deployPlugins(const QString &appDirPath, DeploymentInfo deploymentInfo) { @@ -712,6 +903,9 @@ void Deploy::deployPlugins(const AppDirInfo &appDirInfo, destinationPath = QDir::cleanPath(dstLibexec + "/QtWebEngineProcess"); copyFilePrintStatus(sourcePath, destinationPath); + // put qt.conf file next to browser process so it can also make use of our local Qt resources + createQtConfForQtWebEngineProcess(dstLibexec); + // Resources: sourcePath = QDir::cleanPath(qtDataPath + "/resources/qtwebengine_resources.pak"); destinationPath = QDir::cleanPath(dstResources + "/qtwebengine_resources.pak"); @@ -764,6 +958,12 @@ bool Deploy::deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, QStringList &qmlDirs) { + if (!m_qtDetected) { + LogDebug() << "Skipping QML imports since no Qt detected"; + + return false; + } + LogNormal() << ""; LogNormal() << "Deploying QML imports "; LogNormal() << "Application QML file search path(s) is" << qmlDirs; @@ -781,9 +981,9 @@ bool Deploy::deployQmlImports(const QString &appDirPath, // Verify that we found a qmlimportscanner binary if (!QFile(qmlImportScannerPath).exists()) { LogError() << "qmlimportscanner not found at" << qmlImportScannerPath; - LogError() << "Rebuild qtdeclarative/tools/qmlimportscanner"; + LogError() << "Please install it if you want to bundle QML based applications."; - return false; + return true; } // build argument list for qmlimportsanner: "-rootPath foo/ -rootPath bar/ -importPath path/to/qt/qml" @@ -891,7 +1091,7 @@ bool Deploy::deployQmlImports(const QString &appDirPath, // 2) QtQuick.Controls is used // The intended failure mode is that libwidgetsplugin.dylib will be present // in the app bundle but not used at run-time. - if (deploymentInfo.deployedLibraries.contains("QtWidgets") && qtQuickContolsInUse) { + if (deploymentInfo.requiresQtWidgetsLibrary && qtQuickContolsInUse) { LogNormal() << "Deploying QML import QtQuick/PrivateWidgets"; QString name = "QtQuick/PrivateWidgets"; QString path = this->m_qtToBeBundledInfo.value("QT_INSTALL_QML") + QLatin1Char('/') + name; @@ -1178,6 +1378,137 @@ bool Deploy::checkAppImagePrerequisites(const QString &appDirPath) return true; } +void Deploy::findUsedModules(DeploymentInfo &info) +{ + LogDebug() << "Creating mask of used modules"; + + const QStringList &libraries = info.deployedLibraries; + const size_t qtModulesCount = + sizeof(qtModuleEntries) / sizeof(QtModuleEntry); + + for (size_t i = 0; i < qtModulesCount; ++i) { + QtModuleEntry &entry = qtModuleEntries[i]; + const QString name = QLatin1String(qtModuleEntries[i].libraryName); + bool found = false; + + foreach (const QString &library, libraries) { + if (library.contains(name, Qt::CaseInsensitive)) { + LogDebug() << "Found dependency:" << name; + found = true; + break; + } + } + + if (found) + info.usedModulesMask |= entry.module; + } +} + +void Deploy::deployTranslations(const QString &appDirPath, + quint64 usedQtModules) +{ + LogDebug() << "Deploying translations..."; + QString qtTranslationsPath = this->m_qtToBeBundledInfo.value("QT_INSTALL_TRANSLATIONS"); + + if (qtTranslationsPath.isEmpty() || !QFile::exists(qtTranslationsPath)) { + LogError() << "Qt translations path could not be determined"; + + return; + } + + QString translationsDirPath = appDirPath + QStringLiteral("/translations"); + LogDebug() << "Using" << translationsDirPath << "as translations directory for App"; + LogDebug() << "Using" << qtTranslationsPath << " to search for Qt translations"; + + QFileInfo fi(translationsDirPath); + + if (!fi.isDir()) { + if (!QDir().mkpath(translationsDirPath)) { + LogError() << "Failed to create translations directory"; + } + } else { + LogDebug() << "Translations directory already exists"; + } + + if (!deployTranslations(qtTranslationsPath, + translationsDirPath, + usedQtModules)) { + LogError() << "Failed to copy translations"; + } +} + +bool Deploy::deployTranslations(const QString &sourcePath, + const QString &target, + quint64 usedQtModules) +{ + LogDebug() << "Translations target is" << target; + + // Find available languages prefixes by checking on qtbase. + QStringList prefixes; + QDir sourceDir(sourcePath); + const QStringList qmFilter = QStringList(QStringLiteral("qtbase_*.qm")); + + foreach (QString qmFile, sourceDir.entryList(qmFilter)) { + qmFile.chop(3); + qmFile.remove(0, 7); + prefixes.push_back(qmFile); + } + + if (prefixes.isEmpty()) { + LogError() << "Could not find any translations in " + << sourcePath << " (developer build?)"; + return true; + } + + // Run lconvert to concatenate all files into a single named "qt_.qm" in the application folder + // Use QT_INSTALL_TRANSLATIONS as working directory to keep the command line short. + const QString absoluteTarget = QFileInfo(target).absoluteFilePath(); + + QString lconvertPath = QDir::cleanPath(this->m_qtToBeBundledInfo.value("QT_INSTALL_BINS")) + "/lconvert"; + LogDebug() << "Looking for lconvert at" << lconvertPath; + + // Fallback: Look relative to the linuxdeployqt binary + if (!QFile(lconvertPath).exists()){ + lconvertPath = QCoreApplication::applicationDirPath() + "/lconvert"; + LogDebug() << "Fallback, looking for lconvert at" << lconvertPath; + } + + // Verify that we found a lconvert binary + if (!QFile(lconvertPath).exists()) { + LogError() << "lconvert not found at" << lconvertPath; + + return false; + } + + LogNormal() << "Found lconvert at" << lconvertPath; + + QStringList arguments; + + foreach (const QString &prefix, prefixes) { + arguments.clear(); + const QString targetFile = QStringLiteral("qt_") + prefix + QStringLiteral(".qm"); + arguments.append(QStringLiteral("-o")); + const QString currentTargetFile = absoluteTarget + QLatin1Char('/') + targetFile; + arguments.append(currentTargetFile); + + foreach (const QFileInfo &qmFileInfo, sourceDir.entryInfoList(translationNameFilters(usedQtModules, prefix))) + arguments.append(qmFileInfo.absoluteFilePath()); + + LogNormal() << "Creating " << currentTargetFile << "..."; + LogDebug() << "lconvert arguments:" << arguments; + + QProcess lconvert; + lconvert.start(lconvertPath, arguments); + lconvert.waitForFinished(); + + if (lconvert.exitStatus() != QProcess::NormalExit) { + LogError() << "Fail in lconvert on file" << currentTargetFile; + } + } // for prefixes. + + return true; +} + QDebug Deploy::LogError() { if (logLevel < 0) @@ -1522,3 +1853,26 @@ void Deploy::deployQmlImport(const QString &appDirPath, recursiveCopyAndDeploy(appDirPath, rpaths, importSourcePath, importDestinationPath); } + +QStringList Deploy::translationNameFilters(quint64 modules, + const QString &prefix) +{ + QStringList result; + const size_t qtModulesCount = sizeof(qtModuleEntries)/sizeof(QtModuleEntry); + + for (size_t i = 0; i < qtModulesCount; ++i) { + if ((qtModuleEntries[i].module & modules) + && qtModuleEntries[i].translation) { + const QString name = + QLatin1String(qtModuleEntries[i].translation) + + QLatin1Char('_') + prefix + QStringLiteral(".qm"); + + if (!result.contains(name)) + result.push_back(name); + } + } + + LogDebug() << "Translation name filters:" << result; + + return result; +} diff --git a/src/shared.h b/src/shared.h index 6c3e3fd3..2c4f200c 100644 --- a/src/shared.h +++ b/src/shared.h @@ -81,9 +81,11 @@ class DeploymentInfo QString qtPath; QString pluginPath; QStringList deployedLibraries; + quint64 usedModulesMask; QSet rpathsUsed; bool useLoaderPath; bool isLibrary; + bool requiresQtWidgetsLibrary; }; inline QDebug operator<<(QDebug debug, const AppDirInfo &info); @@ -123,6 +125,8 @@ class Deploy const QString &bundlePath, const QStringList &binaryPaths, bool useLoaderPath); + void createQtConf(const QString &appDirPath); + void createQtConfForQtWebEngineProcess(const QString &appDirPath); void deployPlugins(const QString &appDirPath, DeploymentInfo deploymentInfo); void deployPlugins(const AppDirInfo &appDirInfo, @@ -140,6 +144,12 @@ class Deploy const QString &value); int createAppImage(const QString &appBundlePath); bool checkAppImagePrerequisites(const QString &appBundlePath); + void findUsedModules(DeploymentInfo &info); + void deployTranslations(const QString &appDirPath, + quint64 usedQtModules); + bool deployTranslations(const QString &sourcePath, + const QString &target, + quint64 usedQtModules); QDebug LogError(); QDebug LogWarning(); @@ -152,6 +162,7 @@ class Deploy QMap m_qtToBeBundledInfo; QString m_log; bool m_appstoreCompliant; + int m_qtDetected; bool m_deployLibrary; bool lddOutputContainsLinuxVDSO(const QString &lddOutput); @@ -175,6 +186,8 @@ class Deploy const QSet &rpaths, const QString &importSourcePath, const QString &importName); + QStringList translationNameFilters(quint64 modules, + const QString &prefix); }; #endif diff --git a/tests/tests-environment.sh b/tests/tests-environment.sh index 83254b11..68022dd8 100755 --- a/tests/tests-environment.sh +++ b/tests/tests-environment.sh @@ -5,7 +5,7 @@ set -e sudo add-apt-repository --yes ppa:beineri/opt-qt58-trusty sudo apt-get update -qq -git clone https://github.com/NixOS/patchelf.git +git clone -o 44b7f95 https://github.com/NixOS/patchelf.git cd patchelf bash ./bootstrap.sh ./configure From 2970a4c51763d047e48f865c30d1b31fb6eb4755 Mon Sep 17 00:00:00 2001 From: Gonzalo Exequiel Pedone Date: Fri, 26 May 2017 20:26:27 -0300 Subject: [PATCH 07/12] Fixed exclude list read loop and reading list just once. --- src/shared.cpp | 72 ++++++++++++++++++++++++++------------------------ src/shared.h | 2 ++ 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/shared.cpp b/src/shared.cpp index 38899931..7db17f5c 100644 --- a/src/shared.cpp +++ b/src/shared.cpp @@ -198,6 +198,7 @@ Deploy::Deploy(): m_qtDetected(0), m_deployLibrary(false) { + this->m_excludeList = this->readExcludeList(); } Deploy::~Deploy() @@ -330,43 +331,10 @@ LibraryInfo Deploy::parseLddLibraryLine(const QString &line, return info; if (bundleAllButCoreLibs) { - /* - Bundle every lib including the low-level ones except those that are explicitly blacklisted. - This is more suitable for bundling in a way that is portable between different distributions and target systems. - Along the way, this also takes care of non-Qt libraries. - - The excludelist can be updated by running - #/bin/bash - blacklisted=$(wget https://raw.githubusercontent.com/probonopd/AppImages/master/excludelist -O - | sort | uniq | grep -v "^#.*" | grep "[^-\s]") - for item in $blacklisted; do - echo -ne '"'$item'" << ' - done - */ - - QFile excludelistFile(":/excludelist"); - - if (!excludelistFile.open(QIODevice::ReadOnly | QIODevice::Text)) - return info; - - QStringList excludelist; - - for (auto line = excludelistFile.readLine().trimmed(); !excludelistFile.atEnd();) { - if (line.startsWith("#")) - continue; - else { - int comment = line.indexOf("#"); - - if (comment > 0) - line = line.left(comment); - } - - excludelist << line; - } - - LogDebug() << "excludelist:" << excludelist; + LogDebug() << "excludelist:" << this->m_excludeList; if (!trimmed.contains("libicu")) { - if (containsHowOften(excludelist, QFileInfo(trimmed).completeBaseName())) { + if (containsHowOften(this->m_excludeList, QFileInfo(trimmed).completeBaseName())) { LogDebug() << "Skipping blacklisted" << trimmed; return info; @@ -1876,3 +1844,37 @@ QStringList Deploy::translationNameFilters(quint64 modules, return result; } + +QStringList Deploy::readExcludeList() const +{ + /* Bundle every lib including the low-level ones except those that are explicitly blacklisted. + * This is more suitable for bundling in a way that is portable between different distributions and target systems. + * Along the way, this also takes care of non-Qt libraries. + */ + QFile excludelistFile(":/excludelist"); + + if (!excludelistFile.open(QIODevice::ReadOnly | QIODevice::Text)) + return QStringList(); + + QStringList excludelist; + + while (!excludelistFile.atEnd()) { + auto line = excludelistFile.readLine().trimmed(); + + if (line.startsWith("#")) + continue; + else { + int comment = line.indexOf("#"); + + if (comment > 0) + line = line.left(comment); + } + + if (line.isEmpty()) + continue; + + excludelist << line; + } + + return excludelist; +} diff --git a/src/shared.h b/src/shared.h index 2c4f200c..51b96eeb 100644 --- a/src/shared.h +++ b/src/shared.h @@ -164,6 +164,7 @@ class Deploy bool m_appstoreCompliant; int m_qtDetected; bool m_deployLibrary; + QStringList m_excludeList; bool lddOutputContainsLinuxVDSO(const QString &lddOutput); bool copyFilePrintStatus(const QString &from, const QString &to); @@ -188,6 +189,7 @@ class Deploy const QString &importName); QStringList translationNameFilters(quint64 modules, const QString &prefix); + QStringList readExcludeList() const; }; #endif From 404af803bf5d9f3c3b4315a7f7ae3e14456b00f6 Mon Sep 17 00:00:00 2001 From: Gonzalo Exequiel Pedone Date: Sat, 27 May 2017 00:09:15 -0300 Subject: [PATCH 08/12] Read verbose option first. --- src/main.cpp | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index ebe0ad36..380aafa3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -134,6 +134,19 @@ int main(int argc, char **argv) Deploy deploy; + if (cliParser.isSet(verboseOpt)) { + bool ok = false; + int number = cliParser.value(verboseOpt).toInt(&ok); + + if (ok) + deploy.logLevel = number; + + deploy.LogDebug() << "Argument found:" << verboseOpt.names().first(); + + if (!ok) + deploy.LogError() << "Could not parse verbose level"; + } + QString desktopFile; QString desktopExecEntry; QString desktopIconEntry; @@ -373,43 +386,32 @@ int main(int argc, char **argv) // Set options from command line if (cliParser.isSet(noPluginsOpt)) { - deploy.LogDebug() << "Argument found:" << noPluginsOpt.valueName(); + deploy.LogDebug() << "Argument found:" << noPluginsOpt.names().first(); plugins = false; } bool appimage = false; if (cliParser.isSet(appimageOpt)) { - deploy.LogDebug() << "Argument found:" << appimageOpt.valueName(); + deploy.LogDebug() << "Argument found:" << appimageOpt.names().first(); appimage = true; deploy.bundleAllButCoreLibs = true; } if (cliParser.isSet(noStripOpt)) { - deploy.LogDebug() << "Argument found:" << noStripOpt.valueName(); + deploy.LogDebug() << "Argument found:" << noStripOpt.names().first(); deploy.runStripEnabled = false; } if (cliParser.isSet(bundleNonQtLibsOpt)) { - deploy.LogDebug() << "Argument found:" << bundleNonQtLibsOpt.valueName(); + deploy.LogDebug() << "Argument found:" << bundleNonQtLibsOpt.names().first(); deploy.bundleAllButCoreLibs = true; } - if (cliParser.isSet(verboseOpt)) { - deploy.LogDebug() << "Argument found:" << verboseOpt.valueName(); - bool ok = false; - int number = cliParser.value(verboseOpt).toInt(&ok); - - if (ok) - deploy.logLevel = number; - else - deploy.LogError() << "Could not parse verbose level"; - } - QStringList additionalExecutables; if (cliParser.isSet(executableOpt)) { - deploy.LogDebug() << "Argument found:" << executableOpt.valueName(); + deploy.LogDebug() << "Argument found:" << executableOpt.names().first(); QString executables = cliParser.value(executableOpt).trimmed(); if (!executables.isEmpty()) @@ -422,7 +424,7 @@ int main(int argc, char **argv) bool qmldirArgumentUsed = false; if (cliParser.isSet(qmldirOpt)) { - deploy.LogDebug() << "Argument found:" << qmldirOpt.valueName(); + deploy.LogDebug() << "Argument found:" << qmldirOpt.names().first(); QString dirs = cliParser.value(qmldirOpt).trimmed(); qmldirArgumentUsed = true; @@ -433,14 +435,14 @@ int main(int argc, char **argv) } if (cliParser.isSet(alwaysOverwriteOpt)) { - deploy.LogDebug() << "Argument found:" << alwaysOverwriteOpt.valueName(); + deploy.LogDebug() << "Argument found:" << alwaysOverwriteOpt.names().first(); deploy.alwaysOwerwriteEnabled = true; } bool skipTranslations = false; if (cliParser.isSet(noTranslationsOpt)) { - deploy.LogDebug() << "Argument found:" << noTranslationsOpt.valueName(); + deploy.LogDebug() << "Argument found:" << noTranslationsOpt.names().first(); skipTranslations = true; } From e6b42aa3783aeee18cb8e530af725266595a1ec2 Mon Sep 17 00:00:00 2001 From: Gonzalo Exequiel Pedone Date: Sat, 27 May 2017 00:31:32 -0300 Subject: [PATCH 09/12] Fixed m_bundleLibraryDirectory. --- src/shared.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared.cpp b/src/shared.cpp index 7db17f5c..d040390d 100644 --- a/src/shared.cpp +++ b/src/shared.cpp @@ -314,10 +314,10 @@ LibraryInfo Deploy::parseLddLibraryLine(const QString &line, Q_UNUSED(rpaths); if (fhsLikeMode) { - this->m_bundleLibraryDirectory= "lib"; // relative to bundle - } else { - QString relativePrefix = fhsPrefix.replace(appDirPath+"/", ""); + QString relativePrefix = fhsPrefix.replace(appDirPath + "/", ""); this->m_bundleLibraryDirectory = relativePrefix + "/lib/"; + } else { + this->m_bundleLibraryDirectory = "lib"; // relative to bundle } LogDebug() << "bundleLibraryDirectory:" << this->m_bundleLibraryDirectory; From ba1346e291d83c7a9cfd6c90ba888737d6f8bacf Mon Sep 17 00:00:00 2001 From: Gonzalo Exequiel Pedone Date: Sat, 27 May 2017 12:45:30 -0300 Subject: [PATCH 10/12] Don't stop deploy when a library can't be found. --- src/shared.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/shared.cpp b/src/shared.cpp index d040390d..a6d4bec9 100644 --- a/src/shared.cpp +++ b/src/shared.cpp @@ -277,7 +277,13 @@ LddInfo Deploy::findDependencyInfo(const QString &binaryPath) if (outputLine.contains("not found")){ LogError() << "ldd outputLine:" << outputLine.replace("\t", ""); LogError() << "Please ensure that all libraries can be found by ldd. Aborting."; - exit(1); + + /* FIXME: Can't continue the deploy process because exiting, making + * the app crash. + * This situation must be handled in a different way, or simply + * ignore those "not found" lines. + */ + // exit(1); } } From 727aa4eb8fd4648baa4ba396aa1f791bcc44fafa Mon Sep 17 00:00:00 2001 From: Gonzalo Exequiel Pedone Date: Sat, 27 May 2017 14:25:13 -0300 Subject: [PATCH 11/12] Integrated changes from mhoeher/linuxdeployqt@a3a6376 (Added -qmage to CLI options). --- README.md | 3 ++- src/linuxdeployqt.pro | 2 -- src/main.cpp | 25 +++++++++++++++++++++---- src/shared.cpp | 13 +++++++++---- src/shared.h | 3 ++- 5 files changed, 34 insertions(+), 12 deletions(-) delete mode 100644 src/linuxdeployqt.pro diff --git a/README.md b/README.md index ab3af1a7..1567c0bb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Please download __linuxdeployqt-x86_64.AppImage__ from the [Releases](https://gi Open in Qt Creator and build your application. Run it from the command line and inspect it with `ldd` to make sure the correct libraries from the correct locations are getting loaded, as `linuxdeployqt` will use `ldd` internally to determine from where to copy libraries into the bundle. -__Important:__ `linuxdeployqt` deploys the Qt instance that qmake on the $PATH points to, so make sure that it is the correct one. Verify that qmake finds the correct Qt instance like this before running the `linuxdeployqt` tool: +__Important:__ By default, `linuxdeployqt` deploys the Qt instance that qmake on the $PATH points to, so make sure that it is the correct one. Verify that qmake finds the correct Qt instance like this before running the `linuxdeployqt` tool: ``` qmake -v @@ -29,6 +29,7 @@ QMake version 3.0 Using Qt version 5.7.0 in /tmp/.mount_QtCreator-5.7.0-x86_64/5.7/gcc_64/lib ``` If this does not show the correct path to your Qt instance that you want to be bundled, then adjust your `$PATH` to find the correct `qmake`. +Alternatively, use the `-qmake` command line option to point the tool directly to the qmake executable to be used. Before running linuxdeployqt it may be wise to delete unneeded files that you do not wish to distribute from the build directory. These may be autogenerated during the build. You can delete them like so: diff --git a/src/linuxdeployqt.pro b/src/linuxdeployqt.pro deleted file mode 100644 index b9e628af..00000000 --- a/src/linuxdeployqt.pro +++ /dev/null @@ -1,2 +0,0 @@ -QT = core -SOURCES += main.cpp ../shared/shared.cpp diff --git a/src/main.cpp b/src/main.cpp index 380aafa3..54f50946 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -51,8 +51,10 @@ int main(int argc, char **argv) "self-contained by copying in the Qt libraries and plugins that " "the application uses.\n" "\n" - "It deploys the Qt instance that qmake on the $PATH points to, " - "so make sure that it is the correct one.\n" + "By default it deploys the Qt instance that qmake on the $PATH points " + "to.\n" + "The '-qmake' option can be used to point to the qmake executable to " + "be used instead.\n" "\n" "Plugins related to a Qt library are copied in with the library.\n" "\n" @@ -110,6 +112,13 @@ int main(int argc, char **argv) QObject::tr("Copy files even if the target file exists")); cliParser.addOption(alwaysOverwriteOpt); + QCommandLineOption qmakeOpt( + "qmake", + QObject::tr("The qmake executable to use"), + "path", "" + ); + cliParser.addOption(qmakeOpt); + QCommandLineOption noTranslationsOpt( "no-translations", QObject::tr("Skip deployment of translations")); @@ -123,7 +132,6 @@ int main(int argc, char **argv) * -extra-plugins : Also deploy this plugins. * -extra-files : Also copy these files to the deploy folder (useful for * including extra required utilities). - * -qmake : Use this alternative binary as qmake. * -scan-bin-paths : Scan this paths for binaries and dynamic libraries. * -scan-qml-paths : Scan this directories for Qml imports. * -scan-recursive : Scan directories recursively. @@ -439,6 +447,13 @@ int main(int argc, char **argv) deploy.alwaysOwerwriteEnabled = true; } + QString qmakeExecutable; + + if (cliParser.isSet(qmakeOpt)) { + deploy.LogDebug() << "Argument found:" << qmakeOpt.names().first(); + qmakeExecutable = cliParser.value(qmakeOpt).trimmed();; + } + bool skipTranslations = false; if (cliParser.isSet(noTranslationsOpt)) { @@ -452,7 +467,9 @@ int main(int argc, char **argv) return 1; } - auto deploymentInfo = deploy.deployQtLibraries(appDirPath, additionalExecutables); + auto deploymentInfo = deploy.deployQtLibraries(appDirPath, + additionalExecutables, + qmakeExecutable); // Convenience: Look for .qml files in the current directoty if no -qmldir specified. if (qmlDirs.isEmpty()) { diff --git a/src/shared.cpp b/src/shared.cpp index a6d4bec9..fe39e66d 100644 --- a/src/shared.cpp +++ b/src/shared.cpp @@ -484,7 +484,8 @@ QList Deploy::getQtLibraries(const QList &dependencies, * a list of actually deployed libraries. */ DeploymentInfo Deploy::deployQtLibraries(const QString &appDirPath, - const QStringList &additionalExecutables) + const QStringList &additionalExecutables, + const QString &qmake) { AppDirInfo applicationBundle; @@ -510,10 +511,13 @@ DeploymentInfo Deploy::deployQtLibraries(const QString &appDirPath, // Determine the location of the Qt to be bundled LogDebug() << "Using qmake to determine the location of the Qt to be bundled"; - QString qmakePath = ""; + // Use the qmake executable passed in by the user: + QString qmakePath = qmake; - // The upstream name of the binary is "qmake", for Qt 4 and Qt 5 - qmakePath = QStandardPaths::findExecutable("qmake"); + // If we did not get a qmake, first try to find "qmake", which is the + // upstream name of the binary in both Qt4 and Qt5: + if (qmakePath.isEmpty()) + qmakePath = QStandardPaths::findExecutable("qmake"); // But openSUSE has qmake for Qt 4 and qmake-qt5 for Qt 5 // Qt 4 on Fedora comes with suffix -qt4 @@ -638,6 +642,7 @@ DeploymentInfo Deploy::deployQtLibraries(QList libraries, // Copy the library to the app bundle. const QString deployedBinaryPath = copyDylib(library, bundlePath); + // Skip the rest if already was deployed. if (deployedBinaryPath.isNull()) continue; diff --git a/src/shared.h b/src/shared.h index 51b96eeb..f448dd45 100644 --- a/src/shared.h +++ b/src/shared.h @@ -120,7 +120,8 @@ class Deploy const QString &appDirPath, const QSet &rpaths); DeploymentInfo deployQtLibraries(const QString &appDirPath, - const QStringList &additionalExecutables); + const QStringList &additionalExecutables, + const QString &qmake); DeploymentInfo deployQtLibraries(QList libraries, const QString &bundlePath, const QStringList &binaryPaths, From 0ef153b9dab4d4ca9f959ce310393eb060b098a8 Mon Sep 17 00:00:00 2001 From: Gonzalo Exequiel Pedone Date: Fri, 2 Jun 2017 10:37:04 -0300 Subject: [PATCH 12/12] Integrating changes from mhoeher/linuxdeployqt@bd30e0e (Fix linuxdeployqt crashes on Travis, closes #108). --- .travis.yml | 18 ------------------ README.md | 7 ++++--- src/shared.cpp | 8 ++++++++ tests/tests-ci.sh | 22 +++++++++++++++++++--- tests/tests-environment.sh | 10 ++-------- 5 files changed, 33 insertions(+), 32 deletions(-) diff --git a/.travis.yml b/.travis.yml index f3a4636d..3435e567 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,21 +39,3 @@ branches: except: - # Do not build tags that we create when we upload to GitHub Releases - /^(?i:continuous)$/ - -notifications: - irc: - channels: - - "chat.freenode.net#AppImage" - on_success: always # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always - on_start: always # options: [always|never|change] default: always - template: - - "%{repository} build %{build_number}: %{result} %{build_url}" - use_notice: true - # skip_join: true - webhooks: - urls: - - https://webhooks.gitter.im/e/4bf20518805a55998cc2 - on_success: always # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always - on_start: always # options: [always|never|change] default: always diff --git a/README.md b/README.md index 1567c0bb..9275eb77 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ install: script: - qmake PREFIX=/usr - - make -j4 + - make -j$(nproc) - make INSTALL_ROOT=appdir install ; find appdir/ after_success: @@ -106,6 +106,7 @@ __CMake__ wants `DESTDIR` instead: ``` - cmake . -DCMAKE_INSTALL_PREFIX=/usr + - make -j$(nproc) - make DESTDIR=appdir install ; find appdir/ ``` @@ -113,7 +114,7 @@ __autotools__ (the dinosaur that spends precious minutes "checking...") wants `D ``` - ./configure --prefix=/usr - - make -j4 + - make -j$(nproc) - make install DESTDIR=$(readlink -f appdir) ; find appdir/ ``` @@ -137,7 +138,7 @@ This PR, when merged, will compile this application on [Travis CI](https://travi For this to work, you need to enable Travis CI for your repository as [described here](https://travis-ci.org/getting_started) __prior to merging this__, if you haven't already done so. Providing an [AppImage](http://appimage.org/) would have, among others, these advantages: -- Works for most Linux distributions (including Ubuntu, Fedora, openSUSE, CentOS, elementaryOS, Linux Mint, and others) +- Applications packaged as an AppImage can run on many distributions (including Ubuntu, Fedora, openSUSE, CentOS, elementaryOS, Linux Mint, and others) - One app = one file = super simple for users: just download one AppImage file, [make it executable](http://discourse.appimage.org/t/how-to-make-an-appimage-executable/80), and run - No unpacking or installation necessary - No root needed diff --git a/src/shared.cpp b/src/shared.cpp index fe39e66d..d5202267 100644 --- a/src/shared.cpp +++ b/src/shared.cpp @@ -1250,6 +1250,13 @@ bool Deploy::patchQtCore(const QString &path, const QString &variable, const QString &value) { +#if 1 // FIXME: Disabling for now since using qt.conf + Q_UNUSED(path) + Q_UNUSED(variable) + Q_UNUSED(value) + + return false; +#else QFile file(path); if (!file.open(QIODevice::ReadWrite)) { @@ -1302,6 +1309,7 @@ bool Deploy::patchQtCore(const QString &path, } return true; +#endif } int Deploy::createAppImage(const QString &appDirPath) diff --git a/tests/tests-ci.sh b/tests/tests-ci.sh index 619c51e6..354a492e 100755 --- a/tests/tests-ci.sh +++ b/tests/tests-ci.sh @@ -3,11 +3,11 @@ set -x source /opt/qt*/bin/qt*-env.sh -/opt/qt*/bin/qmake linuxdeployqt.pro -make -j2 +/opt/qt*/bin/qmake CONFIG+=release CONFIG+=force_debug_info linuxdeployqt.pro +make -j mkdir -p linuxdeployqt.AppDir/usr/bin/ -cp /usr/local/bin/{appimagetool,mksquashfs,patchelf,zsyncmake} linuxdeployqt.AppDir/usr/bin/ +cp /usr/bin/patchelf /usr/local/bin/{appimagetool,mksquashfs,zsyncmake} linuxdeployqt.AppDir/usr/bin/ find linuxdeployqt.AppDir/ export VERSION=continuous cp ./linuxdeployqt linuxdeployqt.AppDir/usr/bin/ @@ -24,4 +24,20 @@ do sleep 1; done +# enable core dumps +echo "/tmp/coredump" | sudo tee /proc/sys/kernel/core_pattern + +ulimit -c unlimited +ulimit -a -S +ulimit -a -H + bash -e tests/tests.sh + +if [ $? -ne 0 ]; then + echo "FAILURE: linuxdeployqt CRASHED -- uploading files for debugging to transfer.sh" + set -v + [ -e /tmp/coredump ] && curl --upload-file /tmp/coredump https://transfer.sh/coredump + curl --upload-file linuxdeployqt-*-x86_64.AppImage https://transfer.sh/linuxdeployqt-x86_64.AppImage + find -type f -iname 'libQt5Core.so*' -exec curl --upload {} https://transfer.sh/libQt5Core.so \; || true + exit $RESULT +fi diff --git a/tests/tests-environment.sh b/tests/tests-environment.sh index 68022dd8..a75fee58 100755 --- a/tests/tests-environment.sh +++ b/tests/tests-environment.sh @@ -5,14 +5,8 @@ set -e sudo add-apt-repository --yes ppa:beineri/opt-qt58-trusty sudo apt-get update -qq -git clone -o 44b7f95 https://github.com/NixOS/patchelf.git -cd patchelf -bash ./bootstrap.sh -./configure -make -j2 -sudo make install - -cd - +wget http://ftp.de.debian.org/debian/pool/main/p/patchelf/patchelf_0.8-2_amd64.deb +sudo dpkg -i patchelf_0.8-2_amd64.deb cd /tmp/ wget -c "https://github.com/probonopd/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"