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/.travis.yml b/.travis.yml index c687c723..3435e567 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 637402d4..9275eb77 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/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/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 1e2f2d5b..d0a6ba70 100644 --- a/linuxdeployqt.pro +++ b/linuxdeployqt.pro @@ -1,2 +1,27 @@ -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} + +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/linuxdeployqt/linuxdeployqt.pro b/linuxdeployqt/linuxdeployqt.pro deleted file mode 100644 index b9e628af..00000000 --- a/linuxdeployqt/linuxdeployqt.pro +++ /dev/null @@ -1,2 +0,0 @@ -QT = core -SOURCES += main.cpp ../shared/shared.cpp diff --git a/linuxdeployqt/main.cpp b/linuxdeployqt/main.cpp deleted file mode 100644 index 0bde0c22..00000000 --- a/linuxdeployqt/main.cpp +++ /dev/null @@ -1,422 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. and Simon Peter -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the tools applications of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ -#include -#include -#include -#include "../shared/shared.h" -#include -#include -#include -#include - -int main(int argc, char **argv) -{ - QCoreApplication app(argc, argv); - - 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() << " -no-translations : Skip deployment of translations."; - 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) { - /* 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. */ - 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(); - qDebug() << "desktopExecEntry:" << desktopExecEntry; - desktopFile = firstArgument; - 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 - - /* Search directory for an executable with the name in the Exec= key */ - QString directoryToBeSearched; - 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())){ - qDebug() << "Found binary from desktop file:" << it.fileInfo().canonicalFilePath(); - 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(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())){ - qDebug() << "Found binary from desktop file:" << it2.fileInfo().canonicalFilePath(); - appBinaryPath = it2.fileInfo().absoluteFilePath(); - break; - } - } - } - - if(appBinaryPath == ""){ - if((QFileInfo(candidateBin).isFile()) && (QFileInfo(candidateBin).isExecutable())) { - appBinaryPath = QFileInfo(candidateBin).absoluteFilePath(); - } else { - 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(); - } - } - - // 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 newPath = QCoreApplication::applicationDirPath() + ":" + oldPath; - LogDebug() << newPath; - setenv("PATH",newPath.toUtf8().constData(),1); - - QString appName = QDir::cleanPath(QFileInfo(appBinaryPath).completeBaseName()); - - QString appDir = QDir::cleanPath(appBinaryPath + "/../"); - if (QDir().exists(appDir) == false) { - 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; - extern QStringList librarySearchPath; - QStringList additionalExecutables; - bool qmldirArgumentUsed = false; - bool skipTranslations = 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; - } else { - qDebug() << "Not using FHS-like mode"; - } - - if (QDir().exists(appBinaryPath)) { - qDebug() << "app-binary:" << appBinaryPath; - } else { - qDebug() << "Error: Could not find app-binary" << appBinaryPath; - return 1; - } - - QString appDirPath; - QString relativeBinPath; - if(fhsLikeMode == false){ - 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()){ - appRun.remove(); - } - - QFile::link(relativeBinPath, appDirPath + "/AppRun"); - - /* Copy the desktop file in place, into the top level of the AppDir */ - if(desktopFile != ""){ - QString destination = QDir::cleanPath(appDirPath + "/" + QFileInfo(desktopFile).fileName()); - if(QFileInfo(destination).exists() == false){ - if (QFile::copy(desktopFile, destination)){ - qDebug() << "Copied" << desktopFile << "to" << destination; - } - } - if(QFileInfo(destination).isFile() == false){ - 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 != ""){ - 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")))){ - 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 - } 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; - } - } - } - - /* Copy in place */ - if(iconToBeUsed != ""){ - /* 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"); - } - - if(preExistingToplevelIcon != ""){ - qDebug() << "preExistingToplevelIcon:" << preExistingToplevelIcon; - } else { - qDebug() << "iconToBeUsed:" << iconToBeUsed; - 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); - } - } - } - } - - 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 == QByteArray("-no-translations")) { - LogDebug() << "Argument found:" << argument; - skipTranslations = true; - } else if (argument.startsWith("-")) { - LogError() << "Unknown argument" << argument << "\n"; - return 1; - } - } - - if (appimage) { - if(checkAppImagePrerequisites(appDirPath) == false){ - LogError() << "checkAppImagePrerequisites failed\n"; - return 1; - } - } - - DeploymentInfo deploymentInfo = 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()) { - qmlDirs += QStringLiteral("."); - } - } - - if (!qmlDirs.isEmpty()) { - bool ok = 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 = deploymentInfo.deployedLibraries.toSet().toList(); - } - - deploymentInfo.usedModulesMask = 0; - findUsedModules(deploymentInfo); - - if (plugins && !deploymentInfo.qtPath.isEmpty()) { - if (deploymentInfo.pluginPath.isEmpty()) - deploymentInfo.pluginPath = QDir::cleanPath(deploymentInfo.qtPath + "/../plugins"); - deployPlugins(appDirPath, deploymentInfo); - createQtConf(appDirPath); - } - - if (runStripEnabled) - stripAppBinary(appDirPath); - - if (!skipTranslations) { - deployTranslations(appDirPath, deploymentInfo.usedModulesMask); - } - - if (appimage) { - int result = createAppImage(appDirPath); - LogDebug() << "result:" << result; - exit(result); - } - exit(0); -} diff --git a/shared/shared.h b/shared/shared.h deleted file mode 100644 index f50f4c4f..00000000 --- a/shared/shared.h +++ /dev/null @@ -1,136 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. and Simon Peter -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the tools applications of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#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; -}; - -class DylibInfo -{ -public: - QString binaryPath; -}; - -class LddInfo -{ -public: - QString installName; - QString binaryPath; - QList dependencies; -}; - -bool operator==(const LibraryInfo &a, const LibraryInfo &b); -QDebug operator<<(QDebug debug, const LibraryInfo &info); - -class AppDirInfo -{ - public: - QString path; - QString binaryPath; - QStringList libraryPaths; -}; - -class DeploymentInfo -{ -public: - QString qtPath; - QString pluginPath; - QStringList deployedLibraries; - quint64 usedModulesMask; - QSet rpathsUsed; - bool useLoaderPath; - bool isLibrary; - bool requiresQtWidgetsLibrary; -}; - -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 createQtConf(const QString &appDirPath); -void createQtConfForQtWebEngineProcess(const QString &appDirPath); -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); -void findUsedModules(DeploymentInfo &info); -void deployTranslations(const QString &appDirPath, quint64 usedQtModules); -bool deployTranslations(const QString &sourcePath, const QString &target, quint64 usedQtModules); - -#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 00000000..54f50946 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,519 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. and Simon Peter +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the tools applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include +#include +#include + +#include "shared.h" + +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" + "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" + "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); + + QCommandLineOption qmakeOpt( + "qmake", + QObject::tr("The qmake executable to use"), + "path", "" + ); + cliParser.addOption(qmakeOpt); + + 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. + * + * -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 + * including extra required utilities). + * -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); + + 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; + 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. */ + if (firstArgument.endsWith(".desktop")){ + qDebug() << "Desktop file as first argument:" << firstArgument; + 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(); + qDebug() << "desktopIconEntry:" << desktopIconEntry; + + QString candidateBin = QDir::cleanPath(QFileInfo(firstArgument).absolutePath() + desktopExecEntry); // Not FHS-like + + /* Search directory for an executable with the name in the Exec= key */ + QString directoryToBeSearched; + 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()) { + qDebug() << "Found binary from desktop file:" << it.fileInfo().canonicalFilePath(); + 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 (deploy.appBinaryPath.isEmpty()) { + 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())){ + qDebug() << "Found binary from desktop file:" << it2.fileInfo().canonicalFilePath(); + deploy.appBinaryPath = it2.fileInfo().absoluteFilePath(); + + break; + } + } + } + + 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 = QFileInfo(QDir::cleanPath(firstArgument)).absoluteFilePath(); + } + } + + // 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 newPath = QCoreApplication::applicationDirPath() + ":" + oldPath; + deploy.LogDebug() << newPath; + setenv("PATH",newPath.toUtf8().constData(),1); + + QString appName = QDir::cleanPath(QFileInfo(deploy.appBinaryPath).completeBaseName()); + QString appDir = QDir::cleanPath(deploy.appBinaryPath + "/../"); + + if (!QDir().exists(appDir)) { + qDebug() << "Error: Could not find AppDir" << appDir; + + return 1; + } + + /* 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 + "/../../"); + qDebug() << "FHS-like mode with PREFIX, fhsPrefix:" << deploy.fhsPrefix; + deploy.fhsLikeMode = true; + } else { + qDebug() << "Not using FHS-like mode"; + } + + if (QDir().exists(deploy.appBinaryPath)) { + qDebug() << "app-binary:" << deploy.appBinaryPath; + } else { + qDebug() << "Error: Could not find app-binary" << deploy.appBinaryPath; + + return 1; + } + + QString appDirPath; + QString relativeBinPath; + + if (deploy.fhsLikeMode) { + appDirPath = QDir::cleanPath(deploy.fhsPrefix + "/../"); + QString relativePrefix = deploy.fhsPrefix.replace(appDirPath+"/", ""); + relativeBinPath = relativePrefix + "/bin/" + appName; + } else { + appDirPath = appDir; + relativeBinPath = appName; + } + + qDebug() << "appDirPath:" << appDirPath; + qDebug() << "relativeBinPath:" << relativeBinPath; + + QFile appRun(appDirPath + "/AppRun"); + + if(appRun.exists()) + appRun.remove(); + + QFile::link(relativeBinPath, appDirPath + "/AppRun"); + + /* Copy the desktop file in place, into the top level of the AppDir */ + if (!desktopFile.isEmpty()) { + QString destination = QDir::cleanPath(appDirPath + "/" + QFileInfo(desktopFile).fileName()); + + if (!QFileInfo(destination).exists()) { + if (QFile::copy(desktopFile, destination)) + qDebug() << "Copied" << desktopFile << "to" << destination; + } + + if (!QFileInfo(destination).isFile()) { + 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.isEmpty()) { + QDirIterator dirs(appDirPath, QDirIterator::Subdirectories); + + while (dirs.hasNext()) { + dirs.next(); + + 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()); + } + } + + qDebug() << "Found icons from desktop file:" << candidates; + + /* Select the main icon from the candidates */ + if(candidates.length() == 1){ + iconToBeUsed = candidates.first(); // The only choice + } else if(candidates.length() > 1){ + foreach(QString current, candidates) { + for (auto &iconFormat: QStringList {"256", + "128", + "svg", + "svgz", + "512", + "1024", + "64", + "48", + "xpm"}) { + if (current.contains(iconFormat)) { + iconToBeUsed = current; + + break; + } + } + } + } + + // 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. */ + 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 (!foundToplevelIcon) { + qDebug() << "iconToBeUsed:" << iconToBeUsed; + QString targetIconPath = + appDirPath + "/" + QFileInfo(iconToBeUsed).fileName(); + + if (QFile::copy(iconToBeUsed, targetIconPath)) { + qDebug() << "Copied" << iconToBeUsed << "to" << targetIconPath; + QFile::copy(targetIconPath, appDirPath + "/.DirIcon"); + } else { + deploy.LogError() << "Could not copy" << iconToBeUsed << "to" << targetIconPath << "\n"; + + return 1; + } + } + } + } + + bool plugins = true; + + // Set options from command line + if (cliParser.isSet(noPluginsOpt)) { + deploy.LogDebug() << "Argument found:" << noPluginsOpt.names().first(); + plugins = false; + } + + bool appimage = false; + + if (cliParser.isSet(appimageOpt)) { + deploy.LogDebug() << "Argument found:" << appimageOpt.names().first(); + appimage = true; + deploy.bundleAllButCoreLibs = true; + } + + if (cliParser.isSet(noStripOpt)) { + deploy.LogDebug() << "Argument found:" << noStripOpt.names().first(); + deploy.runStripEnabled = false; + } + + if (cliParser.isSet(bundleNonQtLibsOpt)) { + deploy.LogDebug() << "Argument found:" << bundleNonQtLibsOpt.names().first(); + deploy.bundleAllButCoreLibs = true; + } + + QStringList additionalExecutables; + + if (cliParser.isSet(executableOpt)) { + deploy.LogDebug() << "Argument found:" << executableOpt.names().first(); + QString executables = cliParser.value(executableOpt).trimmed(); + + if (!executables.isEmpty()) + additionalExecutables << executables; + else + deploy.LogError() << "Missing executable path"; + } + + QStringList qmlDirs; + bool qmldirArgumentUsed = false; + + if (cliParser.isSet(qmldirOpt)) { + deploy.LogDebug() << "Argument found:" << qmldirOpt.names().first(); + QString dirs = cliParser.value(qmldirOpt).trimmed(); + qmldirArgumentUsed = true; + + if (!dirs.isEmpty()) + qmlDirs << dirs; + else + deploy.LogError() << "Missing qml directory path"; + } + + if (cliParser.isSet(alwaysOverwriteOpt)) { + deploy.LogDebug() << "Argument found:" << alwaysOverwriteOpt.names().first(); + 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)) { + deploy.LogDebug() << "Argument found:" << noTranslationsOpt.names().first(); + skipTranslations = true; + } + + if (appimage && !deploy.checkAppImagePrerequisites(appDirPath)) { + deploy.LogError() << "checkAppImagePrerequisites failed\n"; + + return 1; + } + + auto deploymentInfo = deploy.deployQtLibraries(appDirPath, + additionalExecutables, + qmakeExecutable); + + // Convenience: Look for .qml files in the current directoty if no -qmldir specified. + if (qmlDirs.isEmpty()) { + QDir dir; + + if (!dir.entryList({QStringLiteral("*.qml")}).isEmpty()) + qmlDirs += QStringLiteral("."); + } + + if (!qmlDirs.isEmpty()) { + 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 += deploy.findAppLibraries(appDirPath); + 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; + + return result; + } + + return 0; +} diff --git a/shared/shared.cpp b/src/shared.cpp similarity index 77% rename from shared/shared.cpp rename to src/shared.cpp index 0c35239e..d5202267 100644 --- a/shared/shared.cpp +++ b/src/shared.cpp @@ -25,16 +25,9 @@ ** $QT_END_LICENSE$ ** ****************************************************************************/ + #include -#include -#include -#include -#include #include -#include -#include -#include -#include #include #include #include @@ -43,24 +36,8 @@ #include #include #include -#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; -int qtDetected = 0; -bool deployLibrary = false; - -using std::cout; -using std::endl; -QMap qtToBeBundledInfo; +#include "shared.h" enum QtModule #if defined(Q_COMPILER_CLASS_ENUM) || defined(Q_CC_MSVC) @@ -119,7 +96,8 @@ enum QtModule QtSerialBusModule = 0x0002000000000000 }; -struct QtModuleEntry { +struct QtModuleEntry +{ quint64 module; const char *option; const char *libraryName; @@ -178,12 +156,12 @@ static QtModuleEntry qtModuleEntries[] = { { QtSerialBusModule, "serialbus", "Qt5SerialBus", 0 } }; -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"; @@ -201,75 +179,71 @@ QDebug operator<<(QDebug debug, const LibraryInfo &info) return debug; } -QString bundleLibraryDirectory; - -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"; 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_qtDetected(0), + m_deployLibrary(false) +{ + this->m_excludeList = this->readExcludeList(); } -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; @@ -282,6 +256,7 @@ LddInfo findDependencyInfo(const QString &binaryPath) if (ldd.exitStatus() != QProcess::NormalExit || ldd.exitCode() != 0) { LogError() << "findDependencyInfo:" << ldd.readAllStandardError(); + return info; } @@ -289,10 +264,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; } @@ -301,30 +277,35 @@ LddInfo 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); } } - 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; } } @@ -332,22 +313,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 + if (fhsLikeMode) { + QString relativePrefix = fhsPrefix.replace(appDirPath + "/", ""); + this->m_bundleLibraryDirectory = relativePrefix + "/lib/"; } else { - QString relativePrefix = fhsPrefix.replace(appDirPath+"/", ""); - bundleLibraryDirectory = relativePrefix + "/lib/"; + this->m_bundleLibraryDirectory = "lib"; // relative to bundle } - LogDebug() << "bundleLibraryDirectory:" << bundleLibraryDirectory; + + LogDebug() << "bundleLibraryDirectory:" << this->m_bundleLibraryDirectory; LibraryInfo info; QString trimmed = line.trimmed(); @@ -357,27 +336,13 @@ LibraryInfo parseLddLibraryLine(const QString &line, const QString &appDirPath, if (trimmed.isEmpty()) return info; + if (bundleAllButCoreLibs) { + LogDebug() << "excludelist:" << this->m_excludeList; - 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 - */ - - 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" << "libpangoft2-1.0.so.0" << "libpangocairo-1.0.so.0" << "libpango-1.0.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 (containsHowOften(excludelist, QFileInfo(trimmed).completeBaseName())) { + if (!trimmed.contains("libicu")) { + if (containsHowOften(this->m_excludeList, QFileInfo(trimmed).completeBaseName())) { LogDebug() << "Skipping blacklisted" << trimmed; + return info; } } @@ -391,7 +356,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; } } @@ -406,10 +371,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) { @@ -418,30 +385,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; @@ -449,7 +426,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; @@ -463,6 +440,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; } @@ -470,483 +448,191 @@ 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, + const QString &qmake) { - 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 + 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")) + m_qtDetected = 5; - QString output = objdump.readAllStandardOutput(); - QStringList outputLines = output.split("\n"); - QStringListIterator i(outputLines); + if (dep.binaryPath.contains("libQtCore.so.4")) + m_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 (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"; - return rpaths; -} + // Use the qmake executable passed in by the user: + QString qmakePath = qmake; -QList getQtLibraries(const QString &path, const QString &appDirPath, const QSet &rpaths) -{ - const LddInfo info = findDependencyInfo(path); - return getQtLibraries(info.dependencies, appDirPath, rpaths + getBinaryRPaths(path)); -} + // 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"); -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 (m_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 (m_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"); + LogDebug() << "-query output from qmake:" << output; + 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"); - LogNormal() << "copy:" << sourcePath << destinationPath; + // FIXME: If we use a ldd replacement, we still need to observe this + // path + QString newPath = qtLibsPath + ":" + oldPath; - 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); - } + // 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); + } + } - QStringList subdirs = QDir(sourcePath).entryList(QStringList() << "*", QDir::Dirs | QDir::NoDotAndDotDot); - foreach (QString dir, subdirs) { - recursiveCopy(sourcePath + "/" + dir, destinationPath + "/" + dir); - } - return true; -} + if (fhsLikeMode) + changeIdentification("$ORIGIN/../lib/" + this->m_bundleLibraryDirectory, QFileInfo(applicationBundle.binaryPath).canonicalFilePath()); + else + changeIdentification("$ORIGIN/lib/" + this->m_bundleLibraryDirectory, QFileInfo(applicationBundle.binaryPath).canonicalFilePath()); -void recursiveCopyAndDeploy(const QString &appDirPath, const QSet &rpaths, const QString &sourcePath, const QString &destinationPath) -{ - QDir().mkpath(destinationPath); + applicationBundle.libraryPaths = findAppLibraries(appDirPath); + LogDebug() << "applicationBundle.libraryPaths:" << applicationBundle.libraryPaths; + LogDebug() << "additionalExecutables:" << additionalExecutables; - LogNormal() << "copy:" << sourcePath << destinationPath; + QStringList allBinaryPaths = QStringList() << applicationBundle.binaryPath << applicationBundle.libraryPaths + << additionalExecutables; + LogDebug() << "allBinaryPaths:" << allBinaryPaths; - QStringList files = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Files | QDir::NoDotAndDotDot); - foreach (QString file, files) { - const QString fileSourcePath = sourcePath + QLatin1Char('/') + file; + QSet allRPaths = getBinaryRPaths(applicationBundle.binaryPath, true); + allRPaths.insert(QLibraryInfo::location(QLibraryInfo::LibrariesPath)); + LogDebug() << "allRPaths:" << allRPaths; - QString fileDestinationPath = destinationPath + QLatin1Char('/') + file; - copyFilePrintStatus(fileSourcePath, fileDestinationPath); + QList libraries = getQtLibrariesForPaths(allBinaryPaths, appDirPath, allRPaths); - if(fileDestinationPath.endsWith(".so")){ + 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"; - LogDebug() << "Deploying .so in QML import" << fileSourcePath; - runStrip(fileDestinationPath); - - // 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/"; - } - - QDir dir(QFileInfo(fileDestinationPath).canonicalFilePath()); - QString relativePath = dir.relativeFilePath(appDirPath + "/" + bundleLibraryDirectory); - relativePath.remove(0, 3); // remove initial '../' - changeIdentification("$ORIGIN:$ORIGIN/" + relativePath, QFileInfo(fileDestinationPath).canonicalFilePath()); - - QList libraries = getQtLibraries(fileSourcePath, appDirPath, QSet()); - deployQtLibraries(libraries, appDirPath, QStringList() << destinationPath, false); - } - } - - QStringList subdirs = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Dirs | QDir::NoDotAndDotDot); - foreach (QString dir, subdirs) { - recursiveCopyAndDeploy(appDirPath, rpaths, sourcePath + QLatin1Char('/') + dir, destinationPath + QLatin1Char('/') + dir); - } -} - -QString copyDylib(const LibraryInfo &library, const QString path) -{ - if (!QFile::exists(library.sourceFilePath)) { - LogError() << "no file at" << library.sourceFilePath; - return QString(); - } - - // 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(); - } - - // 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 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(); - } - 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) -{ - return true; // ################################### Disabling for now since using qt.conf - 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; -} - -void changeIdentification(const QString &id, const QString &binaryPath) -{ - LogNormal() << "Changing rpath in" << binaryPath << "to" << id; - runPatchelf(QStringList() << "--set-rpath" << id << binaryPath); - - // 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 == true){ - 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 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 stripAppBinary(const QString &bundlePath) -{ - (void)bundlePath; - - runStrip(appBinaryPath); + return DeploymentInfo(); + } else { + return deployQtLibraries(libraries, applicationBundle.path, allBinaryPaths, !additionalExecutables.isEmpty()); + } } -/* - 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) +DeploymentInfo Deploy::deployQtLibraries(QList libraries, + const QString &bundlePath, + const QStringList &binaryPaths, + bool useLoaderPath) { LogNormal() << "Deploying the following libraries:" << binaryPaths; QStringList copiedLibraries; DeploymentInfo deploymentInfo; deploymentInfo.requiresQtWidgetsLibrary = false; deploymentInfo.useLoaderPath = useLoaderPath; - deploymentInfo.pluginPath = qtToBeBundledInfo.value("QT_INSTALL_PLUGINS"); + deploymentInfo.pluginPath = this->m_qtToBeBundledInfo.value("QT_INSTALL_PLUGINS"); QSet rpathsUsed; - while (libraries.isEmpty() == false) { + while (!libraries.isEmpty()) { const LibraryInfo library = libraries.takeFirst(); copiedLibraries.append(library.libraryName); - if(library.libraryName.contains("libQt") and library.libraryName.contains("Core.so")) { + if (library.libraryName.contains("libQt") and library.libraryName.contains("Core.so")) { LogNormal() << "Setting deploymentInfo.qtPath to:" << library.libraryDirectory; deploymentInfo.qtPath = library.libraryDirectory; } - - if(library.libraryName.contains("libQt") and library.libraryName.contains("Widgets.so")) { + + if (library.libraryName.contains("libQt") && library.libraryName.contains("Widgets.so")) deploymentInfo.requiresQtWidgetsLibrary = true; - } if (library.libraryDirectory.startsWith(bundlePath)) { LogNormal() << library.libraryName << "already deployed, skipping."; + continue; } @@ -956,165 +642,130 @@ DeploymentInfo 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; runStrip(deployedBinaryPath); - if (!library.rpathUsed.length()) { + if (!library.rpathUsed.length()) changeIdentification(library.deployedInstallName, QFileInfo(deployedBinaryPath).canonicalFilePath()); - } // Check for library dependencies QList dependencies = getQtLibraries(deployedBinaryPath, bundlePath, rpathsUsed); foreach (LibraryInfo dependency, dependencies) { - if (dependency.rpathUsed.isEmpty() != true) { + if (!dependency.rpathUsed.isEmpty()) rpathsUsed << dependency.rpathUsed; - } // Deploy library if necessary. - if (copiedLibraries.contains(dependency.libraryName) == false && libraries.contains(dependency) == false) { + if (!copiedLibraries.contains(dependency.libraryName) + && !libraries.contains(dependency)) { libraries.append(dependency); } } } - deploymentInfo.deployedLibraries = copiedLibraries; + deploymentInfo.deployedLibraries = copiedLibraries; deploymentInfo.rpathsUsed += rpathsUsed; return deploymentInfo; } -static QString captureOutput(const QString &command) +void Deploy::createQtConf(const QString &appDirPath) { - QProcess process; - process.start(command, QIODevice::ReadOnly); - process.waitForFinished(); + // 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 (process.exitStatus() != QProcess::NormalExit) { - LogError() << command << "crashed:" << process.readAllStandardError(); - } else if (process.exitCode() != 0) { - LogError() << command << "exited with" << process.exitCode() << ":" << process.readAllStandardError(); + 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"; } - return process.readAllStandardOutput(); -} + QString filePath = appDirPath + "/"; // Is picked up when placed next to the main executable + QString fileName = QDir::cleanPath(appBinaryPath + "/../qt.conf"); -DeploymentInfo deployQtLibraries(const QString &appDirPath, const QStringList &additionalExecutables) -{ - AppDirInfo applicationBundle; + QDir().mkpath(filePath); - applicationBundle.path = appDirPath; - LogDebug() << "applicationBundle.path:" << applicationBundle.path; - applicationBundle.binaryPath = appBinaryPath; - LogDebug() << "applicationBundle.binaryPath:" << applicationBundle.binaryPath; + QFile qtconf(fileName); - // Find out whether Qt is a dependency of the application to be bundled - 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; - } - } + if (qtconf.exists() && !alwaysOwerwriteEnabled) { + LogWarning() << fileName << "already exists, will not overwrite."; - if(qtDetected != 0){ + return; + } - // Determine the location of the Qt to be bundled - LogDebug() << "Using qmake to determine the location of the Qt to be bundled"; + qtconf.open(QIODevice::WriteOnly); - QString qmakePath = ""; + if (qtconf.write(contents) != -1) + LogNormal() << "Created configuration file:" << fileName; +} - // The upstream name of the binary is "qmake", for Qt 4 and Qt 5 - qmakePath = QStandardPaths::findExecutable("qmake"); +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"; - // 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"); - } - } + QDir().mkpath(filePath); + QFile qtconf(fileName); - if(qmakePath == ""){ - LogError() << "qmake not found on the $PATH"; - exit(1); - } + if (qtconf.exists() && !alwaysOwerwriteEnabled) { + LogWarning() << fileName << "already exists, will not overwrite."; + return; + } - QString output = captureOutput(qmakePath + " -query"); - LogDebug() << "-query output from qmake:" << output; + qtconf.open(QIODevice::WriteOnly); - 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 (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"; + } +} - QString qtLibsPath = qtToBeBundledInfo.value("QT_INSTALL_LIBS"); +void Deploy::deployPlugins(const QString &appDirPath, + DeploymentInfo deploymentInfo) +{ + AppDirInfo applicationBundle; + applicationBundle.path = appDirPath; + applicationBundle.binaryPath = appBinaryPath; - 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); - } - } + QString pluginDestinationPath; - if(fhsLikeMode == false){ - changeIdentification("$ORIGIN/lib/" + bundleLibraryDirectory, QFileInfo(applicationBundle.binaryPath).canonicalFilePath()); + if (fhsLikeMode) { + QFileInfo qfi(applicationBundle.binaryPath); + QString qtTargetDir = qfi.absoluteDir().absolutePath() + "/../"; + pluginDestinationPath = qtTargetDir + "/plugins"; } else { - changeIdentification("$ORIGIN/../lib/" + bundleLibraryDirectory, QFileInfo(applicationBundle.binaryPath).canonicalFilePath()); + pluginDestinationPath = appDirPath + "/" + "plugins"; } - applicationBundle.libraryPaths = findAppLibraries(appDirPath); - LogDebug() << "applicationBundle.libraryPaths:" << applicationBundle.libraryPaths; - - LogDebug() << "additionalExecutables:" << additionalExecutables; - - QStringList allBinaryPaths = QStringList() << applicationBundle.binaryPath << applicationBundle.libraryPaths - << additionalExecutables; - LogDebug() << "allBinaryPaths:" << allBinaryPaths; - - QSet allRPaths = getBinaryRPaths(applicationBundle.binaryPath, true); - allRPaths.insert(QLibraryInfo::location(QLibraryInfo::LibrariesPath)); - LogDebug() << "allRPaths:" << allRPaths; - 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()); - } + deployPlugins(applicationBundle, deploymentInfo.pluginPath, pluginDestinationPath, deploymentInfo); } -void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath, - const QString pluginDestinationPath, DeploymentInfo deploymentInfo) +void Deploy::deployPlugins(const AppDirInfo &appDirInfo, + const QString &pluginSourcePath, + const QString pluginDestinationPath, + DeploymentInfo deploymentInfo) { LogNormal() << "Deploying plugins from" << pluginSourcePath; @@ -1132,67 +783,67 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath 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")) { + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Svg")) pluginList.append(QStringLiteral("imageformats/") + plugin); - } } else { pluginList.append(QStringLiteral("imageformats/") + plugin); - } + } } } // Platform OpenGL context - if ((containsHowOften(deploymentInfo.deployedLibraries, "libQt5OpenGL")) or (containsHowOften(deploymentInfo.deployedLibraries, "libQt5XcbQpa"))) { + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5OpenGL") + || containsHowOften(deploymentInfo.deployedLibraries, "libQt5XcbQpa")) { QStringList xcbglintegrationPlugins = QDir(pluginSourcePath + QStringLiteral("/xcbglintegrations")).entryList(QStringList() << QStringLiteral("*.so")); - foreach (const QString &plugin, xcbglintegrationPlugins) { + + 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")) { + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Svg")) pluginList.append(QStringLiteral("iconengines/libqsvgicon.so")); - } // CUPS print support - if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5PrintSupport")) { + if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5PrintSupport")) pluginList.append("printsupport/libcupsprintersupport.so"); - } // Network if (containsHowOften(deploymentInfo.deployedLibraries, "libQt5Network")) { QStringList bearerPlugins = QDir(pluginSourcePath + QStringLiteral("/bearer")).entryList(QStringList() << QStringLiteral("*.so")); - foreach (const QString &plugin, bearerPlugins) { + + foreach (const QString &plugin, bearerPlugins) pluginList.append(QStringLiteral("bearer/") + plugin); - } } // 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) { + + foreach (const QString &plugin, sqlPlugins) pluginList.append(QStringLiteral("sqldrivers/") + plugin); - } } // 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) { + + foreach (const QString &plugin, plugins) pluginList.append(QStringLiteral("mediaservice/") + plugin); - } + plugins = QDir(pluginSourcePath + QStringLiteral("/audio")).entryList(QStringList() << QStringLiteral("*.so")); - foreach (const QString &plugin, plugins) { + + foreach (const QString &plugin, plugins) pluginList.append(QStringLiteral("audio/") + plugin); - } } 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 @@ -1201,14 +852,16 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath // 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"); + 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"); + // create destination directories: QString dstLibexec; QString dstResources; QString dstTranslations; - if(fhsLikeMode){ + + if (fhsLikeMode) { QFileInfo qfi(appDirInfo.binaryPath); QString qtTargetDir = qfi.absoluteDir().absolutePath() + "/../"; dstLibexec = qtTargetDir + "/libexec"; @@ -1219,15 +872,19 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath dstResources = appDirInfo.path + "/resources"; dstTranslations = appDirInfo.path + "/translations"; } + QDir().mkpath(dstLibexec); QDir().mkpath(dstResources); QDir().mkpath(dstTranslations); + // WebEngine executable: sourcePath = QDir::cleanPath(qtLibexecPath + "/QtWebEngineProcess"); 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"); @@ -1244,12 +901,13 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath 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); } - + LogNormal() << "pluginList after having detected hopefully all required plugins:" << pluginList; foreach (const QString &plugin, pluginList) { @@ -1259,6 +917,7 @@ void deployPlugins(const AppDirInfo &appDirInfo, const QString &pluginSourcePath 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); @@ -1268,122 +927,19 @@ 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()); - - } - } -} - -void 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 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 deployPlugins(const QString &appDirPath, DeploymentInfo deploymentInfo) -{ - AppDirInfo applicationBundle; - applicationBundle.path = appDirPath; - applicationBundle.binaryPath = appBinaryPath; - - QString pluginDestinationPath; - if(fhsLikeMode){ - QFileInfo qfi(applicationBundle.binaryPath); - QString qtTargetDir = qfi.absoluteDir().absolutePath() + "/../"; - pluginDestinationPath = qtTargetDir + "/plugins"; - } else { - pluginDestinationPath = appDirPath + "/" + "plugins"; - } - deployPlugins(applicationBundle, deploymentInfo.pluginPath, pluginDestinationPath, deploymentInfo); -} - -void deployQmlImport(const QString &appDirPath, const QSet &rpaths, const QString &importSourcePath, const QString &importName) -{ - 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); } // Scan qml files in qmldirs for import statements, deploy used imports from Qml2ImportsPath to ./qml. -bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, QStringList &qmlDirs) +bool Deploy::deployQmlImports(const QString &appDirPath, + DeploymentInfo deploymentInfo, + QStringList &qmlDirs) { - if(!qtDetected){ + if (!m_qtDetected) { LogDebug() << "Skipping QML imports since no Qt detected"; + return false; } @@ -1392,7 +948,7 @@ bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, LogNormal() << "Application QML file search path(s) is" << qmlDirs; // Use qmlimportscanner from QLibraryInfo::BinariesPath - QString qmlImportScannerPath = QDir::cleanPath(qtToBeBundledInfo.value("QT_INSTALL_BINS")) + "/qmlimportscanner"; + 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 @@ -1405,35 +961,41 @@ bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, if (!QFile(qmlImportScannerPath).exists()) { LogError() << "qmlimportscanner not found at" << qmlImportScannerPath; LogError() << "Please install it if you want to bundle QML based applications."; + return true; } // 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); } argumentList.append( "-importPath"); - argumentList.append(qtToBeBundledInfo.value("QT_INSTALL_QML")); + argumentList.append(this->m_qtToBeBundledInfo.value("QT_INSTALL_QML")); + + LogDebug() << "qmlImportsPath (QT_INSTALL_QML):" << this->m_qtToBeBundledInfo.value("QT_INSTALL_QML"); - LogDebug() << "qmlImportsPath (QT_INSTALL_QML):" << qtToBeBundledInfo.value("QT_INSTALL_QML"); - // 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(); // log qmlimportscanner errors qmlImportScanner.setReadChannel(QProcess::StandardError); QByteArray errors = qmlImportScanner.readAll(); + if (!errors.isEmpty()) { LogWarning() << "QML file parse error (deployment will continue):"; LogWarning() << errors; @@ -1443,9 +1005,11 @@ bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, 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; } @@ -1472,6 +1036,7 @@ bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, if (name.isEmpty() || path.isEmpty()) { LogNormal() << " Skip import: name or path is empty"; LogNormal() << ""; + continue; } @@ -1480,6 +1045,7 @@ bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, if (type != QStringLiteral("module")) { LogNormal() << " Skip non-module import"; LogNormal() << ""; + continue; } @@ -1489,6 +1055,7 @@ bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, name.replace(QLatin1Char('.'), QLatin1Char('/')); int secondTolast = path.length() - 2; QString version = path.mid(secondTolast); + if (version.startsWith(QLatin1Char('.'))) name.append(version); @@ -1506,51 +1073,266 @@ bool deployQmlImports(const QString &appDirPath, DeploymentInfo deploymentInfo, if (deploymentInfo.requiresQtWidgetsLibrary && qtQuickContolsInUse) { LogNormal() << "Deploying QML import QtQuick/PrivateWidgets"; QString name = "QtQuick/PrivateWidgets"; - QString path = qtToBeBundledInfo.value("QT_INSTALL_QML") + QLatin1Char('/') + name; + QString path = this->m_qtToBeBundledInfo.value("QT_INSTALL_QML") + QLatin1Char('/') + name; deployQmlImport(appDirPath, deploymentInfo.rpathsUsed, path, name); LogNormal() << ""; } + return true; } -void changeQtLibraries(const QList libraries, const QStringList &binaryPaths, const QString &absoluteQtPath) +void Deploy::changeIdentification(const QString &id, const QString &binaryPath) { - LogNormal() << "Changing" << binaryPaths << "to link against"; - LogNormal() << "Qt in" << absoluteQtPath; - QString finalQtPath = absoluteQtPath; + LogNormal() << "Changing rpath in" << binaryPath << "to" << id; + runPatchelf(QStringList() << "--set-rpath" << id << binaryPath); - finalQtPath += "/lib/"; + /* 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. + */ - foreach (LibraryInfo library, libraries) { - const QString oldBinaryId = library.installName; - const QString newBinaryId = finalQtPath + library.libraryName + library.binaryPath; - } -} + // qt_prfxpath: + if (binaryPath.contains("libQt5Core")) { + LogDebug() << "libQt5Core detected, patching its hardcoded strings"; -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()) { + /* 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=.." */ - 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 (fhsLikeMode) + patchQtCore(binaryPath, "qt_prfxpath", ".."); + else + patchQtCore(binaryPath, "qt_prfxpath", "."); -bool checkAppImagePrerequisites(const QString &appDirPath) -{ - if(fhsLikeMode == true){ - /* In FHS-like mode, we assume that there will be a desktop file + 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) +{ +#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)) { + 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; +#endif +} + +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"); @@ -1569,6 +1351,7 @@ bool checkAppImagePrerequisites(const QString &appDirPath) // 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"); @@ -1578,30 +1361,23 @@ bool checkAppImagePrerequisites(const QString &appDirPath) QTextStream out(&file2); file2.close(); } - return true; -} -int 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); + return true; } -void findUsedModules(DeploymentInfo &info) +void Deploy::findUsedModules(DeploymentInfo &info) { LogDebug() << "Creating mask of used modules"; const QStringList &libraries = info.deployedLibraries; + const size_t qtModulesCount = + sizeof(qtModuleEntries) / sizeof(QtModuleEntry); - 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; @@ -1610,18 +1386,20 @@ void findUsedModules(DeploymentInfo &info) } } - if (found) { + if (found) info.usedModulesMask |= entry.module; - } } } -void deployTranslations(const QString &appDirPath, quint64 usedQtModules) +void Deploy::deployTranslations(const QString &appDirPath, + quint64 usedQtModules) { LogDebug() << "Deploying translations..."; - QString qtTranslationsPath = qtToBeBundledInfo.value("QT_INSTALL_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; } @@ -1630,6 +1408,7 @@ void deployTranslations(const QString &appDirPath, quint64 usedQtModules) LogDebug() << "Using" << qtTranslationsPath << " to search for Qt translations"; QFileInfo fi(translationsDirPath); + if (!fi.isDir()) { if (!QDir().mkpath(translationsDirPath)) { LogError() << "Failed to create translations directory"; @@ -1638,28 +1417,16 @@ void deployTranslations(const QString &appDirPath, quint64 usedQtModules) LogDebug() << "Translations directory already exists"; } - if (!deployTranslations(qtTranslationsPath, translationsDirPath, usedQtModules)) { + if (!deployTranslations(qtTranslationsPath, + translationsDirPath, + usedQtModules)) { LogError() << "Failed to copy translations"; } } -QStringList 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; -} - -bool deployTranslations(const QString &sourcePath, const QString &target, quint64 usedQtModules) +bool Deploy::deployTranslations(const QString &sourcePath, + const QString &target, + quint64 usedQtModules) { LogDebug() << "Translations target is" << target; @@ -1667,21 +1434,24 @@ bool deployTranslations(const QString &sourcePath, const QString &target, quint6 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(qtToBeBundledInfo.value("QT_INSTALL_BINS")) + "/lconvert"; + 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 @@ -1693,12 +1463,14 @@ bool deployTranslations(const QString &sourcePath, const QString &target, quint6 // 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"); @@ -1720,5 +1492,408 @@ bool deployTranslations(const QString &sourcePath, const QString &target, quint6 LogError() << "Fail in lconvert on file" << currentTargetFile; } } // for prefixes. + return true; } + +QDebug Deploy::LogError() +{ + if (logLevel < 0) + return QDebug(&this->m_log); + + return qDebug() << "ERROR:"; +} + +QDebug Deploy::LogWarning() +{ + if (logLevel < 1) + return QDebug(&this->m_log); + + return qDebug() << "WARNING:"; +} + +QDebug Deploy::LogNormal() +{ + if (logLevel < 2) + return QDebug(&this->m_log); + + return qDebug() << "Log:"; +} + +QDebug Deploy::LogDebug() +{ + if (logLevel < 3) + return QDebug(&this->m_log); + + return qDebug() << "Log:"; +} + +// 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; + } + + 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; + } + } + + if (QFile::copy(from, to)) { + QFile dest(to); + dest.setPermissions(dest.permissions() | QFile::WriteOwner | QFile::WriteUser); + LogNormal() << " copied:" << from; + LogNormal() << " to" << to; + + // 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; + + if (!toFile.setPermissions(toFile.permissions() | QFile::WriteOwner)) { + LogError() << "Failed to set u+w permissions on target file: " << to; + + return false; + } + + return true; + } else { + LogError() << "file copy failed from" << from; + LogError() << " to" << to; + + return false; + } +} + +int Deploy::containsHowOften(QStringList haystack, QString needle) +{ + return haystack.filter(needle).length(); +} + +// 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 { + LogError() << "Could not start objdump. Process error is" << objdump.errorString(); + } + + exit(1); + } + + objdump.waitForFinished(); + + if (objdump.exitCode() != 0) + LogError() << "getBinaryRPaths:" << objdump.readAllStandardError(); + + if (resolve && executablePath.isEmpty()) + executablePath = path; + + QString output = objdump.readAllStandardOutput(); + QStringList outputLines = output.split("\n"); + QStringListIterator i(outputLines); + + 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; + } + } + } + + return rpaths; +} + +QList Deploy::getQtLibrariesForPaths(const QStringList &paths, + const QString &appDirPath, + const QSet &rpaths) +{ + QList result; + QSet existing; + + 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; +} + +// copies everything _inside_ sourcePath to destinationPath +bool Deploy::recursiveCopy(const QString &sourcePath, + const QString &destinationPath) +{ + if (!QDir(sourcePath).exists()) + return false; + + QDir().mkpath(destinationPath); + + LogNormal() << "copy:" << sourcePath << destinationPath; + + 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); + } + + QStringList subdirs = QDir(sourcePath).entryList(QStringList() << "*", QDir::Dirs | QDir::NoDotAndDotDot); + + foreach (QString dir, subdirs) + recursiveCopy(sourcePath + "/" + dir, destinationPath + "/" + dir); + + return true; +} + +void Deploy::recursiveCopyAndDeploy(const QString &appDirPath, + const QSet &rpaths, + const QString &sourcePath, + const QString &destinationPath) +{ + QDir().mkpath(destinationPath); + + LogNormal() << "copy:" << sourcePath << destinationPath; + + auto files = + QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), + QDir::Files | QDir::NoDotAndDotDot); + + foreach (QString file, files) { + const QString fileSourcePath = sourcePath + QLatin1Char('/') + file; + + QString fileDestinationPath = destinationPath + QLatin1Char('/') + file; + copyFilePrintStatus(fileSourcePath, fileDestinationPath); + + if (fileDestinationPath.endsWith(".so")) { + LogDebug() << "Deploying .so in QML import" << fileSourcePath; + runStrip(fileDestinationPath); + + // 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 + } + + 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()); + + QList libraries = getQtLibraries(fileSourcePath, appDirPath, QSet()); + deployQtLibraries(libraries, appDirPath, QStringList() << destinationPath, false); + } + } + + auto subdirs = + QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), + QDir::Dirs | QDir::NoDotAndDotDot); + + 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(); + } + + // 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(); + } + + // 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 Deploy::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(); + } + + 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 + } +} + +QString Deploy::captureOutput(const QString &command) +{ + QProcess process; + process.start(command, QIODevice::ReadOnly); + process.waitForFinished(); + + if (process.exitStatus() != QProcess::NormalExit) + LogError() << command << "crashed:" << process.readAllStandardError(); + else if (process.exitCode() != 0) + LogError() << command << "exited with" << process.exitCode() << ":" << process.readAllStandardError(); + + return process.readAllStandardOutput(); +} + +void Deploy::deployQmlImport(const QString &appDirPath, + const QSet &rpaths, + const QString &importSourcePath, + const QString &importName) +{ + 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); +} + +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; +} + +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 new file mode 100644 index 00000000..f448dd45 --- /dev/null +++ b/src/shared.h @@ -0,0 +1,196 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. and Simon Peter +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the tools applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef LINUX_DEPLOMYMENT_SHARED_H +#define LINUX_DEPLOMYMENT_SHARED_H + +#include + +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; +}; + +class DylibInfo +{ + public: + QString binaryPath; +}; + +class LddInfo +{ + public: + QString installName; + QString binaryPath; + QList dependencies; +}; + +bool operator==(const LibraryInfo &a, const LibraryInfo &b); +QDebug operator<<(QDebug debug, const LibraryInfo &info); + +class AppDirInfo +{ + public: + QString path; + QString binaryPath; + QStringList libraryPaths; +}; + +class DeploymentInfo +{ + public: + QString qtPath; + QString pluginPath; + QStringList deployedLibraries; + quint64 usedModulesMask; + QSet rpathsUsed; + bool useLoaderPath; + bool isLibrary; + bool requiresQtWidgetsLibrary; +}; + +inline QDebug operator<<(QDebug debug, const AppDirInfo &info); + +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, + const QString &qmake); + DeploymentInfo deployQtLibraries(QList libraries, + 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, + 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); + 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(); + QDebug LogNormal(); + QDebug LogDebug(); + + private: + QString m_bundleLibraryDirectory; + QStringList m_librarySearchPath; + QMap m_qtToBeBundledInfo; + QString m_log; + 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); + 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); + QStringList translationNameFilters(quint64 modules, + const QString &prefix); + QStringList readExcludeList() const; +}; + +#endif diff --git a/tests/tests-ci.sh b/tests/tests-ci.sh index 901ac43d..354a492e 100755 --- a/tests/tests-ci.sh +++ b/tests/tests-ci.sh @@ -10,8 +10,8 @@ mkdir -p 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 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 @@ -34,10 +34,10 @@ 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 + 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